@operato/scene-storage 10.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +59 -0
- package/dist/asrs-crane-3d.d.ts +7 -0
- package/dist/asrs-crane-3d.js +164 -0
- package/dist/asrs-crane-3d.js.map +1 -0
- package/dist/asrs-crane.d.ts +47 -0
- package/dist/asrs-crane.js +104 -0
- package/dist/asrs-crane.js.map +1 -0
- package/dist/asrs-rack-3d.d.ts +7 -0
- package/dist/asrs-rack-3d.js +129 -0
- package/dist/asrs-rack-3d.js.map +1 -0
- package/dist/asrs-rack.d.ts +45 -0
- package/dist/asrs-rack.js +99 -0
- package/dist/asrs-rack.js.map +1 -0
- package/dist/box-3d.d.ts +11 -0
- package/dist/box-3d.js +166 -0
- package/dist/box-3d.js.map +1 -0
- package/dist/box.d.ts +36 -0
- package/dist/box.js +73 -0
- package/dist/box.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/pallet-3d.d.ts +11 -0
- package/dist/pallet-3d.js +162 -0
- package/dist/pallet-3d.js.map +1 -0
- package/dist/pallet.d.ts +56 -0
- package/dist/pallet.js +99 -0
- package/dist/pallet.js.map +1 -0
- package/dist/parcel-3d.d.ts +7 -0
- package/dist/parcel-3d.js +82 -0
- package/dist/parcel-3d.js.map +1 -0
- package/dist/parcel.d.ts +30 -0
- package/dist/parcel.js +67 -0
- package/dist/parcel.js.map +1 -0
- package/dist/spot-3d.d.ts +30 -0
- package/dist/spot-3d.js +176 -0
- package/dist/spot-3d.js.map +1 -0
- package/dist/spot.d.ts +41 -0
- package/dist/spot.js +177 -0
- package/dist/spot.js.map +1 -0
- package/dist/templates/index.d.ts +92 -0
- package/dist/templates/index.js +115 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/spot.d.ts +24 -0
- package/dist/templates/spot.js +26 -0
- package/dist/templates/spot.js.map +1 -0
- package/icons/asrs-crane.png +0 -0
- package/icons/asrs-rack.png +0 -0
- package/icons/box-plastic.png +0 -0
- package/icons/box-wood.png +0 -0
- package/icons/pallet-plastic.png +0 -0
- package/icons/pallet-wood.png +0 -0
- package/icons/parcel.png +0 -0
- package/package.json +44 -0
- package/src/asrs-crane-3d.ts +191 -0
- package/src/asrs-crane.ts +130 -0
- package/src/asrs-rack-3d.ts +146 -0
- package/src/asrs-rack.ts +109 -0
- package/src/box-3d.ts +189 -0
- package/src/box.ts +99 -0
- package/src/index.ts +17 -0
- package/src/pallet-3d.ts +181 -0
- package/src/pallet.ts +125 -0
- package/src/parcel-3d.ts +90 -0
- package/src/parcel.ts +76 -0
- package/src/spot-3d.ts +200 -0
- package/src/spot.ts +197 -0
- package/src/templates/index.ts +115 -0
- package/src/templates/spot.ts +26 -0
- package/things-scene.config.js +5 -0
- package/translations/en.json +12 -0
- package/translations/ja.json +12 -0
- package/translations/ko.json +12 -0
- package/translations/ms.json +12 -0
- package/translations/zh.json +12 -0
- package/tsconfig.json +23 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/asrs-rack.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { Component, ComponentNature, Container, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
5
|
+
import {
|
|
6
|
+
Placeable,
|
|
7
|
+
type Alignment,
|
|
8
|
+
type Heights,
|
|
9
|
+
type PlacementArchetype
|
|
10
|
+
} from '@operato/scene-base'
|
|
11
|
+
|
|
12
|
+
import { AsrsRack3D } from './asrs-rack-3d.js'
|
|
13
|
+
|
|
14
|
+
const NATURE: ComponentNature = {
|
|
15
|
+
mutable: false,
|
|
16
|
+
resizable: true,
|
|
17
|
+
rotatable: true,
|
|
18
|
+
properties: [
|
|
19
|
+
{
|
|
20
|
+
type: 'number',
|
|
21
|
+
label: 'levels',
|
|
22
|
+
name: 'levels',
|
|
23
|
+
placeholder: '# of vertical levels (default 4)'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'number',
|
|
27
|
+
label: 'bays',
|
|
28
|
+
name: 'bays',
|
|
29
|
+
placeholder: '# of horizontal bays (default 5)'
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
help: 'scene/component/asrs-rack'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const Base = Placeable(Container) as unknown as typeof Component
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* AsrsRack — a multi-level high-bay storage rack, the structural backbone of
|
|
39
|
+
* an AS/RS (Automated Storage / Retrieval System).
|
|
40
|
+
*
|
|
41
|
+
* `levels` × `bays` cells form a vertical grid. Each cell holds one logistics
|
|
42
|
+
* package (Pallet / Box / Parcel). A pair of AsrsRacks separated by an aisle
|
|
43
|
+
* (where an AsrsCrane runs) is the typical AS/RS configuration; v1 ships the
|
|
44
|
+
* single-rack unit and lets users compose multi-rack systems by placing them
|
|
45
|
+
* side by side. A future `AsrsAisle` composite may bundle the pair + crane.
|
|
46
|
+
*
|
|
47
|
+
* **Placement**: `floor` archetype, full ceiling depth by default — AS/RS
|
|
48
|
+
* racks typically span floor to ceiling, with levels sized to fit the tallest
|
|
49
|
+
* pallet load. Users can shorten via explicit `state.depth` for warehouses
|
|
50
|
+
* with smaller envelopes.
|
|
51
|
+
*
|
|
52
|
+
* **Container-based**. Cells host stored cargo as children — each child's
|
|
53
|
+
* left/top within the rack's bounds determines which cell it occupies. The
|
|
54
|
+
* stacking pass in `Placeable.computeDefaultZPos` ensures each child cargo's
|
|
55
|
+
* z lands on the rack's overall bottom (parent.zPos + parent.depth = ceiling),
|
|
56
|
+
* which isn't quite cell-level resolution — true per-cell z positioning is
|
|
57
|
+
* a v3 concern (the cargo would need to know which cell-row it's in).
|
|
58
|
+
*
|
|
59
|
+
* No Legendable for v1 — racks are passive structures; their per-cell
|
|
60
|
+
* occupancy state is implicit in the children, not a status flag.
|
|
61
|
+
*/
|
|
62
|
+
@sceneComponent('asrs-rack')
|
|
63
|
+
export default class AsrsRack extends Base {
|
|
64
|
+
static placement: PlacementArchetype = 'floor'
|
|
65
|
+
static align: Alignment = 'bottom'
|
|
66
|
+
static defaultDepth = (h: Heights) => h.ceiling - h.floor
|
|
67
|
+
|
|
68
|
+
get nature() {
|
|
69
|
+
return NATURE
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get anchors() {
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Operation cargo (pallets / boxes / parcels) goes in the rack's cells. */
|
|
77
|
+
containable(component: Component) {
|
|
78
|
+
const archetype = (component.constructor as any).placement
|
|
79
|
+
if (archetype === 'operation') return true
|
|
80
|
+
return component.isDescendible(this as any)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 2D — top-down rectangle showing the rack footprint, with subdivisions
|
|
85
|
+
* suggesting the bay layout (lines parallel to the aisle).
|
|
86
|
+
*/
|
|
87
|
+
render(ctx: CanvasRenderingContext2D) {
|
|
88
|
+
const { width, height, left, top } = this.state
|
|
89
|
+
const bays = Math.max(1, Math.floor((this.state.bays as number) || 5))
|
|
90
|
+
|
|
91
|
+
ctx.beginPath()
|
|
92
|
+
// Outer rectangle
|
|
93
|
+
ctx.rect(left, top, width, height)
|
|
94
|
+
// Bay subdivisions (vertical lines)
|
|
95
|
+
for (let i = 1; i < bays; i++) {
|
|
96
|
+
const x = left + (width * i) / bays
|
|
97
|
+
ctx.moveTo(x, top)
|
|
98
|
+
ctx.lineTo(x, top + height)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get fillStyle() {
|
|
103
|
+
return '#a0a0a8'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
buildRealObject(): RealObject | undefined {
|
|
107
|
+
return new AsrsRack3D(this as any)
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/box-3d.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Box 3D — wood crate and plastic tote variants.
|
|
5
|
+
*
|
|
6
|
+
* Wood crate: 4 vertical corner posts + horizontal slats with gaps → the
|
|
7
|
+
* typical industrial wooden crate look. Forklift-friendly.
|
|
8
|
+
* Plastic tote: solid 4 walls + visible top lip / handle cutouts. Stackable.
|
|
9
|
+
*
|
|
10
|
+
* Both have a defined floor (so they look like containers, not just walls)
|
|
11
|
+
* and an opening at top — as you'd expect from a real crate / tote that's
|
|
12
|
+
* open or has a removable lid.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as THREE from 'three'
|
|
16
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
17
|
+
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
18
|
+
|
|
19
|
+
export class Box3D extends RealObjectGroup {
|
|
20
|
+
build() {
|
|
21
|
+
super.build()
|
|
22
|
+
|
|
23
|
+
const { width, height, depth = 300 } = this.component.state
|
|
24
|
+
const material = (this.component.state.material as string) || 'wood'
|
|
25
|
+
const bodyColor = (this.component.state.bodyColor as string) || '#a87644'
|
|
26
|
+
|
|
27
|
+
if (material === 'plastic') {
|
|
28
|
+
this.buildPlasticTote(width, height, depth, bodyColor)
|
|
29
|
+
} else {
|
|
30
|
+
this.buildWoodCrate(width, height, depth, bodyColor)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Wood crate — visible slats, 4 corner posts, open top. */
|
|
35
|
+
private buildWoodCrate(width: number, height: number, depth: number, bodyColor: string) {
|
|
36
|
+
const baseY = -depth / 2
|
|
37
|
+
const wallThickness = Math.min(width, height) * 0.04
|
|
38
|
+
const postW = wallThickness * 1.6
|
|
39
|
+
const slatH = depth * 0.10
|
|
40
|
+
const slatGap = slatH * 0.6
|
|
41
|
+
const floorH = depth * 0.05
|
|
42
|
+
|
|
43
|
+
const woodMaterial = new THREE.MeshStandardMaterial({
|
|
44
|
+
color: bodyColor,
|
|
45
|
+
metalness: 0,
|
|
46
|
+
roughness: 0.85
|
|
47
|
+
})
|
|
48
|
+
const postColor = new THREE.Color(bodyColor).multiplyScalar(0.8)
|
|
49
|
+
const postMaterial = new THREE.MeshStandardMaterial({
|
|
50
|
+
color: postColor,
|
|
51
|
+
metalness: 0,
|
|
52
|
+
roughness: 0.9
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// ── 4 corner posts ───────────────────────────────────────────────
|
|
56
|
+
const postGeos: THREE.BufferGeometry[] = []
|
|
57
|
+
for (const xSign of [-1, 1]) {
|
|
58
|
+
for (const zSign of [-1, 1]) {
|
|
59
|
+
const post = new THREE.BoxGeometry(postW, depth, postW)
|
|
60
|
+
post.translate(
|
|
61
|
+
xSign * (width / 2 - postW / 2),
|
|
62
|
+
baseY + depth / 2,
|
|
63
|
+
zSign * (height / 2 - postW / 2)
|
|
64
|
+
)
|
|
65
|
+
postGeos.push(post)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), postMaterial)
|
|
69
|
+
postMesh.castShadow = true
|
|
70
|
+
this.object3d.add(postMesh)
|
|
71
|
+
|
|
72
|
+
// ── Slatted walls (horizontal slats with gaps) ───────────────────
|
|
73
|
+
const slatRowCount = Math.max(2, Math.floor((depth - floorH) / (slatH + slatGap)))
|
|
74
|
+
const slatGeos: THREE.BufferGeometry[] = []
|
|
75
|
+
|
|
76
|
+
for (let row = 0; row < slatRowCount; row++) {
|
|
77
|
+
const y = baseY + floorH + slatGap + row * (slatH + slatGap) + slatH / 2
|
|
78
|
+
// Long walls (front / back)
|
|
79
|
+
for (const zSign of [-1, 1]) {
|
|
80
|
+
const slat = new THREE.BoxGeometry(width - postW * 2, slatH, wallThickness)
|
|
81
|
+
slat.translate(0, y, zSign * (height / 2 - wallThickness / 2))
|
|
82
|
+
slatGeos.push(slat)
|
|
83
|
+
}
|
|
84
|
+
// Short walls (left / right)
|
|
85
|
+
for (const xSign of [-1, 1]) {
|
|
86
|
+
const slat = new THREE.BoxGeometry(wallThickness, slatH, height - postW * 2)
|
|
87
|
+
slat.translate(xSign * (width / 2 - wallThickness / 2), y, 0)
|
|
88
|
+
slatGeos.push(slat)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const slatMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(slatGeos), woodMaterial)
|
|
92
|
+
slatMesh.castShadow = true
|
|
93
|
+
slatMesh.receiveShadow = true
|
|
94
|
+
this.object3d.add(slatMesh)
|
|
95
|
+
|
|
96
|
+
// ── Floor (bottom panel) ─────────────────────────────────────────
|
|
97
|
+
const floorGeo = new THREE.BoxGeometry(width - postW * 2, floorH, height - postW * 2)
|
|
98
|
+
const floorMesh = new THREE.Mesh(floorGeo, woodMaterial)
|
|
99
|
+
floorMesh.position.set(0, baseY + floorH / 2, 0)
|
|
100
|
+
floorMesh.receiveShadow = true
|
|
101
|
+
this.object3d.add(floorMesh)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Plastic tote — solid molded walls + top stackable lip. */
|
|
105
|
+
private buildPlasticTote(width: number, height: number, depth: number, bodyColor: string) {
|
|
106
|
+
const baseY = -depth / 2
|
|
107
|
+
const wallThickness = Math.min(width, height) * 0.05
|
|
108
|
+
const lipH = depth * 0.06
|
|
109
|
+
const floorH = depth * 0.06
|
|
110
|
+
|
|
111
|
+
const totMaterial = new THREE.MeshStandardMaterial({
|
|
112
|
+
color: bodyColor,
|
|
113
|
+
metalness: 0.05,
|
|
114
|
+
roughness: 0.55
|
|
115
|
+
})
|
|
116
|
+
const lipColor = new THREE.Color(bodyColor).multiplyScalar(0.85)
|
|
117
|
+
const lipMaterial = new THREE.MeshStandardMaterial({
|
|
118
|
+
color: lipColor,
|
|
119
|
+
metalness: 0.05,
|
|
120
|
+
roughness: 0.55
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// ── 4 solid walls ────────────────────────────────────────────────
|
|
124
|
+
const wallGeos: THREE.BufferGeometry[] = []
|
|
125
|
+
const wallH = depth - lipH - floorH
|
|
126
|
+
const wallY = baseY + floorH + wallH / 2
|
|
127
|
+
|
|
128
|
+
// Long walls
|
|
129
|
+
for (const zSign of [-1, 1]) {
|
|
130
|
+
const wall = new THREE.BoxGeometry(width, wallH, wallThickness)
|
|
131
|
+
wall.translate(0, wallY, zSign * (height / 2 - wallThickness / 2))
|
|
132
|
+
wallGeos.push(wall)
|
|
133
|
+
}
|
|
134
|
+
// Short walls
|
|
135
|
+
for (const xSign of [-1, 1]) {
|
|
136
|
+
const wall = new THREE.BoxGeometry(wallThickness, wallH, height - 2 * wallThickness)
|
|
137
|
+
wall.translate(xSign * (width / 2 - wallThickness / 2), wallY, 0)
|
|
138
|
+
wallGeos.push(wall)
|
|
139
|
+
}
|
|
140
|
+
const wallMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wallGeos), totMaterial)
|
|
141
|
+
wallMesh.castShadow = true
|
|
142
|
+
wallMesh.receiveShadow = true
|
|
143
|
+
this.object3d.add(wallMesh)
|
|
144
|
+
|
|
145
|
+
// ── Top lip (stackable rim — slightly wider than walls) ──────────
|
|
146
|
+
const lipGeos: THREE.BufferGeometry[] = []
|
|
147
|
+
const lipY = baseY + depth - lipH / 2
|
|
148
|
+
// Long sides
|
|
149
|
+
for (const zSign of [-1, 1]) {
|
|
150
|
+
const lip = new THREE.BoxGeometry(width * 1.02, lipH, wallThickness * 1.5)
|
|
151
|
+
lip.translate(0, lipY, zSign * (height / 2 - wallThickness * 0.75))
|
|
152
|
+
lipGeos.push(lip)
|
|
153
|
+
}
|
|
154
|
+
// Short sides
|
|
155
|
+
for (const xSign of [-1, 1]) {
|
|
156
|
+
const lip = new THREE.BoxGeometry(wallThickness * 1.5, lipH, height * 1.02 - 2 * wallThickness * 1.5)
|
|
157
|
+
lip.translate(xSign * (width / 2 - wallThickness * 0.75), lipY, 0)
|
|
158
|
+
lipGeos.push(lip)
|
|
159
|
+
}
|
|
160
|
+
const lipMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(lipGeos), lipMaterial)
|
|
161
|
+
lipMesh.castShadow = true
|
|
162
|
+
this.object3d.add(lipMesh)
|
|
163
|
+
|
|
164
|
+
// ── Floor (solid bottom) ─────────────────────────────────────────
|
|
165
|
+
const floorGeo = new THREE.BoxGeometry(width, floorH, height)
|
|
166
|
+
const floorMesh = new THREE.Mesh(floorGeo, totMaterial)
|
|
167
|
+
floorMesh.position.set(0, baseY + floorH / 2, 0)
|
|
168
|
+
floorMesh.receiveShadow = true
|
|
169
|
+
this.object3d.add(floorMesh)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
updateDimension() {}
|
|
173
|
+
|
|
174
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
175
|
+
if (
|
|
176
|
+
'material' in after ||
|
|
177
|
+
'bodyColor' in after ||
|
|
178
|
+
'width' in after ||
|
|
179
|
+
'height' in after ||
|
|
180
|
+
'depth' in after
|
|
181
|
+
) {
|
|
182
|
+
this.update()
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
super.onchange(after, before)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
updateAlpha() {}
|
|
189
|
+
}
|
package/src/box.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { Component, ComponentNature, RealObject, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene'
|
|
5
|
+
import {
|
|
6
|
+
Carriable,
|
|
7
|
+
Legendable,
|
|
8
|
+
Placeable,
|
|
9
|
+
type Alignment,
|
|
10
|
+
type LegendBinding,
|
|
11
|
+
type PlacementArchetype
|
|
12
|
+
} from '@operato/scene-base'
|
|
13
|
+
|
|
14
|
+
import { Box3D } from './box-3d.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Box material — drives 3D structure and color.
|
|
18
|
+
*
|
|
19
|
+
* - `wood` — wood crate: visible vertical slats, gaps between, open or
|
|
20
|
+
* semi-open top. Used for heavy / industrial parts.
|
|
21
|
+
* - `plastic` — plastic tote / bin: solid molded walls with stackable lip
|
|
22
|
+
* at top. Used for fulfillment, parts kitting.
|
|
23
|
+
*
|
|
24
|
+
* Cardboard parcels are a separate component (see `parcel.ts`) — they have
|
|
25
|
+
* different proportions, taping, and labels that warrant a distinct class.
|
|
26
|
+
*/
|
|
27
|
+
export type BoxMaterial = 'wood' | 'plastic'
|
|
28
|
+
|
|
29
|
+
const BODY_LEGEND = {
|
|
30
|
+
wood: '#a87644',
|
|
31
|
+
plastic: '#3a5078',
|
|
32
|
+
default: '#a87644'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const NATURE: ComponentNature = {
|
|
36
|
+
mutable: false,
|
|
37
|
+
resizable: true,
|
|
38
|
+
rotatable: true,
|
|
39
|
+
properties: [
|
|
40
|
+
{
|
|
41
|
+
type: 'select',
|
|
42
|
+
label: 'material',
|
|
43
|
+
name: 'material',
|
|
44
|
+
property: {
|
|
45
|
+
options: [
|
|
46
|
+
{ display: 'Wood', value: 'wood' },
|
|
47
|
+
{ display: 'Plastic', value: 'plastic' }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
help: 'scene/component/box'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Carriable: a box can be a child of any CarrierHolder (Pallet for stacking,
|
|
56
|
+
// AGV deck, robot-arm gripper, Spot for staging).
|
|
57
|
+
const Base = Carriable(Legendable(Placeable(RectPath(Shape)))) as unknown as typeof Component
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Box — a generic stackable container for goods. Wood crate or plastic tote
|
|
61
|
+
* variants distinguished by `material` prop.
|
|
62
|
+
*
|
|
63
|
+
* Shape-based (not Container) — boxes nesting other components is rare in
|
|
64
|
+
* logistics visualization (a *case* of items inside a box is data, not
|
|
65
|
+
* scene-tree). If a future use case needs nested boxes, extend Container.
|
|
66
|
+
*/
|
|
67
|
+
@sceneComponent('box')
|
|
68
|
+
export default class Box extends Base {
|
|
69
|
+
static legends: Record<string, LegendBinding> = {
|
|
70
|
+
bodyColor: { from: 'material', legend: BODY_LEGEND }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static placement: PlacementArchetype = 'operation'
|
|
74
|
+
static align: Alignment = 'bottom'
|
|
75
|
+
static defaultDepth = 300
|
|
76
|
+
|
|
77
|
+
get nature() {
|
|
78
|
+
return NATURE
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get anchors() {
|
|
82
|
+
return []
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 2D — top-down rectangle. */
|
|
86
|
+
render(ctx: CanvasRenderingContext2D) {
|
|
87
|
+
const { width, height, left, top } = this.state
|
|
88
|
+
ctx.beginPath()
|
|
89
|
+
ctx.rect(left, top, width, height)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get fillStyle() {
|
|
93
|
+
return (this.state.bodyColor as string) || '#a87644'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
buildRealObject(): RealObject | undefined {
|
|
97
|
+
return new Box3D(this as any)
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
export { default as Pallet } from './pallet.js'
|
|
5
|
+
export type { PalletMaterial } from './pallet.js'
|
|
6
|
+
|
|
7
|
+
export { default as Box } from './box.js'
|
|
8
|
+
export type { BoxMaterial } from './box.js'
|
|
9
|
+
|
|
10
|
+
export { default as Parcel } from './parcel.js'
|
|
11
|
+
|
|
12
|
+
export { default as AsrsRack } from './asrs-rack.js'
|
|
13
|
+
export { default as AsrsCrane } from './asrs-crane.js'
|
|
14
|
+
export type { AsrsCraneStatus } from './asrs-crane.js'
|
|
15
|
+
|
|
16
|
+
export { default as Spot } from './spot.js'
|
|
17
|
+
export { Spot3D } from './spot-3d.js'
|
package/src/pallet-3d.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Pallet 3D — wood and plastic variants.
|
|
5
|
+
*
|
|
6
|
+
* LO-POLY but structurally distinguishing the two materials:
|
|
7
|
+
*
|
|
8
|
+
* - wood: parallel top slats with gaps between + 3 perpendicular
|
|
9
|
+
* stringers (the classic EUR pallet silhouette) + parallel
|
|
10
|
+
* bottom slats. The forklift entry holes between stringers are
|
|
11
|
+
* the wood pallet's visual signature.
|
|
12
|
+
* - plastic: solid molded top deck (with a few suggestion cutouts as visual
|
|
13
|
+
* detail) + ribbed underside / feet. No discrete slats — the
|
|
14
|
+
* plastic pallet's signature is the seamless one-piece look.
|
|
15
|
+
*
|
|
16
|
+
* Color comes from `state.bodyColor` (Legendable, driven by `material`).
|
|
17
|
+
* Stringer / underside colors are slightly darker tints derived from bodyColor.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as THREE from 'three'
|
|
21
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
22
|
+
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
23
|
+
|
|
24
|
+
export class Pallet3D extends RealObjectGroup {
|
|
25
|
+
build() {
|
|
26
|
+
super.build()
|
|
27
|
+
|
|
28
|
+
const { width, height, depth = 150 } = this.component.state
|
|
29
|
+
const material = (this.component.state.material as string) || 'wood'
|
|
30
|
+
const bodyColor = (this.component.state.bodyColor as string) || '#a87644'
|
|
31
|
+
|
|
32
|
+
if (material === 'plastic') {
|
|
33
|
+
this.buildPlastic(width, height, depth, bodyColor)
|
|
34
|
+
} else {
|
|
35
|
+
this.buildWood(width, height, depth, bodyColor)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Wood EUR-style: 7 top slats + 3 stringers + 5 bottom slats. */
|
|
40
|
+
private buildWood(width: number, height: number, depth: number, bodyColor: string) {
|
|
41
|
+
const baseY = -depth / 2
|
|
42
|
+
const slatThickness = depth * 0.15
|
|
43
|
+
const stringerThickness = depth * 0.45
|
|
44
|
+
const bottomSlatThickness = depth * 0.13
|
|
45
|
+
|
|
46
|
+
const woodMaterial = new THREE.MeshStandardMaterial({
|
|
47
|
+
color: bodyColor,
|
|
48
|
+
metalness: 0.0,
|
|
49
|
+
roughness: 0.85
|
|
50
|
+
})
|
|
51
|
+
const stringerColor = new THREE.Color(bodyColor).multiplyScalar(0.85)
|
|
52
|
+
const stringerMaterial = new THREE.MeshStandardMaterial({
|
|
53
|
+
color: stringerColor,
|
|
54
|
+
metalness: 0.0,
|
|
55
|
+
roughness: 0.9
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// ── Top + bottom slats — same count, same z-positions, paired vertically ─
|
|
59
|
+
// EUR-pallet style: 5 boards on top, 5 below (under the same z ranges so
|
|
60
|
+
// they read as a single skeleton rather than two unrelated grids).
|
|
61
|
+
const slatCount = 5
|
|
62
|
+
const slatW = width
|
|
63
|
+
const slatD = (height * 0.92) / (slatCount + (slatCount - 1) * 0.4)
|
|
64
|
+
const gapD = slatD * 0.4
|
|
65
|
+
const totalSpan = slatCount * slatD + (slatCount - 1) * gapD
|
|
66
|
+
const startZ = -totalSpan / 2
|
|
67
|
+
|
|
68
|
+
const slatPositions: number[] = []
|
|
69
|
+
for (let i = 0; i < slatCount; i++) {
|
|
70
|
+
slatPositions.push(startZ + i * (slatD + gapD) + slatD / 2)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const topSlatGeos: THREE.BufferGeometry[] = []
|
|
74
|
+
for (const z of slatPositions) {
|
|
75
|
+
const slat = new THREE.BoxGeometry(slatW, slatThickness, slatD)
|
|
76
|
+
slat.translate(0, baseY + depth - slatThickness / 2, z)
|
|
77
|
+
topSlatGeos.push(slat)
|
|
78
|
+
}
|
|
79
|
+
const topSlatMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(topSlatGeos), woodMaterial)
|
|
80
|
+
topSlatMesh.castShadow = true
|
|
81
|
+
topSlatMesh.receiveShadow = true
|
|
82
|
+
this.object3d.add(topSlatMesh)
|
|
83
|
+
|
|
84
|
+
// ── Stringers (3 perpendicular blocks between top and bottom decks) ─
|
|
85
|
+
const stringerCount = 3
|
|
86
|
+
const stringerW = width * 0.07
|
|
87
|
+
const stringerY = baseY + bottomSlatThickness + stringerThickness / 2
|
|
88
|
+
const stringerGeos: THREE.BufferGeometry[] = []
|
|
89
|
+
for (let i = 0; i < stringerCount; i++) {
|
|
90
|
+
const xFrac = i / (stringerCount - 1) - 0.5
|
|
91
|
+
const x = xFrac * (width * 0.85)
|
|
92
|
+
const stringer = new THREE.BoxGeometry(stringerW, stringerThickness, height)
|
|
93
|
+
stringer.translate(x, stringerY, 0)
|
|
94
|
+
stringerGeos.push(stringer)
|
|
95
|
+
}
|
|
96
|
+
const stringerMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(stringerGeos), stringerMaterial)
|
|
97
|
+
stringerMesh.castShadow = true
|
|
98
|
+
this.object3d.add(stringerMesh)
|
|
99
|
+
|
|
100
|
+
// ── Bottom slats — same z-positions as top so the deck reads as paired ─
|
|
101
|
+
const botSlatGeos: THREE.BufferGeometry[] = []
|
|
102
|
+
for (const z of slatPositions) {
|
|
103
|
+
const slat = new THREE.BoxGeometry(width, bottomSlatThickness, slatD)
|
|
104
|
+
slat.translate(0, baseY + bottomSlatThickness / 2, z)
|
|
105
|
+
botSlatGeos.push(slat)
|
|
106
|
+
}
|
|
107
|
+
const botSlatMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(botSlatGeos), woodMaterial)
|
|
108
|
+
botSlatMesh.receiveShadow = true
|
|
109
|
+
this.object3d.add(botSlatMesh)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Plastic molded: solid top deck + ribbed underside / feet. */
|
|
113
|
+
private buildPlastic(width: number, height: number, depth: number, bodyColor: string) {
|
|
114
|
+
const baseY = -depth / 2
|
|
115
|
+
const deckThickness = depth * 0.30
|
|
116
|
+
const footH = depth * 0.55
|
|
117
|
+
const footW = width * 0.12
|
|
118
|
+
|
|
119
|
+
const deckMaterial = new THREE.MeshStandardMaterial({
|
|
120
|
+
color: bodyColor,
|
|
121
|
+
metalness: 0.1,
|
|
122
|
+
roughness: 0.55
|
|
123
|
+
})
|
|
124
|
+
const footColor = new THREE.Color(bodyColor).multiplyScalar(0.85)
|
|
125
|
+
const footMaterial = new THREE.MeshStandardMaterial({
|
|
126
|
+
color: footColor,
|
|
127
|
+
metalness: 0.1,
|
|
128
|
+
roughness: 0.65
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// ── Solid top deck ───────────────────────────────────────────────
|
|
132
|
+
const deckGeo = new THREE.BoxGeometry(width * 0.98, deckThickness, height * 0.98)
|
|
133
|
+
const deckMesh = new THREE.Mesh(deckGeo, deckMaterial)
|
|
134
|
+
deckMesh.position.set(0, baseY + depth - deckThickness / 2, 0)
|
|
135
|
+
deckMesh.castShadow = true
|
|
136
|
+
deckMesh.receiveShadow = true
|
|
137
|
+
this.object3d.add(deckMesh)
|
|
138
|
+
|
|
139
|
+
// ── 9 feet (3×3 grid — typical plastic pallet underside) ─────────
|
|
140
|
+
const footGeos: THREE.BufferGeometry[] = []
|
|
141
|
+
for (let i = -1; i <= 1; i++) {
|
|
142
|
+
for (let j = -1; j <= 1; j++) {
|
|
143
|
+
const x = i * (width * 0.4)
|
|
144
|
+
const z = j * (height * 0.4)
|
|
145
|
+
const foot = new THREE.BoxGeometry(footW, footH, footW)
|
|
146
|
+
foot.translate(x, baseY + footH / 2, z)
|
|
147
|
+
footGeos.push(foot)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const footMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(footGeos), footMaterial)
|
|
151
|
+
footMesh.castShadow = true
|
|
152
|
+
this.object3d.add(footMesh)
|
|
153
|
+
|
|
154
|
+
// ── Cross-bracing along underside (suggests molded reinforcement) ─
|
|
155
|
+
const braceH = depth * 0.10
|
|
156
|
+
const braceGeo = new THREE.BoxGeometry(width * 0.95, braceH, height * 0.04)
|
|
157
|
+
for (const zSign of [-1, 0, 1]) {
|
|
158
|
+
const brace = new THREE.Mesh(braceGeo.clone(), footMaterial)
|
|
159
|
+
brace.position.set(0, baseY + footH - braceH / 2, zSign * height * 0.4)
|
|
160
|
+
this.object3d.add(brace)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
updateDimension() {}
|
|
165
|
+
|
|
166
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
167
|
+
if (
|
|
168
|
+
'material' in after ||
|
|
169
|
+
'bodyColor' in after ||
|
|
170
|
+
'width' in after ||
|
|
171
|
+
'height' in after ||
|
|
172
|
+
'depth' in after
|
|
173
|
+
) {
|
|
174
|
+
this.update()
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
super.onchange(after, before)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
updateAlpha() {}
|
|
181
|
+
}
|