@operato/scene-storage 10.0.0-beta.28 → 10.0.0-beta.30
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 +15 -0
- package/dist/asrs-crane-3d.d.ts +10 -0
- package/dist/asrs-crane-3d.js +17 -0
- package/dist/asrs-crane-3d.js.map +1 -1
- package/dist/asrs-crane.d.ts +49 -13
- package/dist/asrs-crane.js +120 -16
- package/dist/asrs-crane.js.map +1 -1
- package/dist/asrs-rack.d.ts +49 -19
- package/dist/asrs-rack.js +108 -20
- package/dist/asrs-rack.js.map +1 -1
- package/dist/box.d.ts +3 -3
- package/dist/box.js +1 -2
- package/dist/box.js.map +1 -1
- package/dist/generic-container.d.ts +2 -2
- package/dist/generic-container.js +1 -2
- package/dist/generic-container.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/pallet.d.ts +2 -2
- package/dist/pallet.js +1 -2
- package/dist/pallet.js.map +1 -1
- package/dist/parcel.d.ts +3 -3
- package/dist/parcel.js +1 -2
- package/dist/parcel.js.map +1 -1
- package/dist/rack-cell-3d.d.ts +25 -0
- package/dist/rack-cell-3d.js +88 -0
- package/dist/rack-cell-3d.js.map +1 -0
- package/dist/rack-cell.d.ts +56 -0
- package/dist/rack-cell.js +200 -0
- package/dist/rack-cell.js.map +1 -0
- package/dist/spot.d.ts +4 -11
- package/dist/spot.js +2 -3
- package/dist/spot.js.map +1 -1
- package/dist/templates/index.d.ts +42 -0
- package/dist/templates/index.js +43 -1
- package/dist/templates/index.js.map +1 -1
- package/package.json +9 -4
- package/src/asrs-crane-3d.ts +20 -0
- package/src/asrs-crane.ts +137 -16
- package/src/asrs-rack.ts +119 -20
- package/src/box.ts +2 -4
- package/src/generic-container.ts +1 -3
- package/src/index.ts +3 -0
- package/src/pallet.ts +1 -3
- package/src/parcel.ts +2 -4
- package/src/rack-cell-3d.ts +101 -0
- package/src/rack-cell.ts +228 -0
- package/src/spot.ts +4 -5
- package/src/templates/index.ts +43 -1
- package/test/setup.js +279 -0
- package/test/test-asrs-crane.ts +319 -0
- package/tsconfig.json +2 -1
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@operato/scene-storage",
|
|
3
3
|
"description": "Storage-domain components for things-scene (smart factory / logistics) — pallet, box, parcel; AS/RS and shelves planned.",
|
|
4
4
|
"author": "heartyoh",
|
|
5
|
-
"version": "10.0.0-beta.
|
|
5
|
+
"version": "10.0.0-beta.30",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"module": "dist/index.js",
|
|
@@ -21,24 +21,29 @@
|
|
|
21
21
|
"build": "tsc",
|
|
22
22
|
"prepublishOnly": "tsc",
|
|
23
23
|
"lint": "eslint src/ && prettier \"src/**/*.ts\" --check",
|
|
24
|
-
"format": "eslint src/ --fix && prettier \"src/**/*.ts\" --write"
|
|
24
|
+
"format": "eslint src/ --fix && prettier \"src/**/*.ts\" --write",
|
|
25
|
+
"test": "mocha --require should --require ./test/setup.js --node-option import=tsx \"test/**/test-*.ts\""
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"@hatiolab/things-scene": "^10.0.0-beta.1",
|
|
28
|
-
"@operato/scene-base": "^10.0.0-beta.
|
|
29
|
+
"@operato/scene-base": "^10.0.0-beta.30",
|
|
29
30
|
"three": "^0.183.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@hatiolab/prettier-config": "^1.0.0",
|
|
34
|
+
"@types/mocha": "^10.0.0",
|
|
33
35
|
"@types/three": "^0.183.0",
|
|
34
36
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
35
37
|
"@typescript-eslint/parser": "^8.0.0",
|
|
36
38
|
"eslint": "^9.18.0",
|
|
37
39
|
"eslint-config-prettier": "^10.0.1",
|
|
40
|
+
"mocha": "^11.0.0",
|
|
38
41
|
"prettier": "^3.2.5",
|
|
42
|
+
"should": "^13.2.3",
|
|
39
43
|
"tslib": "^2.3.1",
|
|
44
|
+
"tsx": "^4.21.0",
|
|
40
45
|
"typescript": "^5.0.4"
|
|
41
46
|
},
|
|
42
47
|
"prettier": "@hatiolab/prettier-config",
|
|
43
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "06b35b1726ec4f27ee76657ce341c6c6f3ba1b3a"
|
|
44
49
|
}
|
package/src/asrs-crane-3d.ts
CHANGED
|
@@ -167,6 +167,26 @@ export class AsrsCrane3D extends RealObjectGroup {
|
|
|
167
167
|
// Place lamp near the corner of the base, away from the mast
|
|
168
168
|
lampMesh.position.set(width * 0.3, baseY + railH + baseH + lampH / 2, height * 0.3)
|
|
169
169
|
this.object3d.add(lampMesh)
|
|
170
|
+
|
|
171
|
+
// ── Carriage frame (invisible anchor for carrier attach) ──────────
|
|
172
|
+
// Placed at the top of the shuttle, where cargo rests.
|
|
173
|
+
this._carriageFrame = new THREE.Object3D()
|
|
174
|
+
this._carriageFrame.name = 'crane-carriage-tcp'
|
|
175
|
+
this._carriageFrame.position.set(0, carriageY - carriageH / 2 - shuttleH, 0)
|
|
176
|
+
this.object3d.add(this._carriageFrame)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Sub-frame where carriers attach during transport (fork tool-centre-point). */
|
|
180
|
+
private _carriageFrame?: THREE.Object3D
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Return the carriage TCP anchor. Carriers attached to this frame will
|
|
184
|
+
* follow carriage movement as `carriageHeight` changes and the crane rebuilds.
|
|
185
|
+
*
|
|
186
|
+
* Callers should re-fetch this after any state change that triggers rebuild.
|
|
187
|
+
*/
|
|
188
|
+
getCarriageFrame(): THREE.Object3D | undefined {
|
|
189
|
+
return this._carriageFrame
|
|
170
190
|
}
|
|
171
191
|
|
|
172
192
|
updateDimension() {}
|
package/src/asrs-crane.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import { Component, ComponentNature,
|
|
4
|
+
import { Component, ComponentNature, ContainerAbstract, ContainerCapacity, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
5
|
+
import type { SlotDef } from '@hatiolab/things-scene'
|
|
5
6
|
import {
|
|
7
|
+
CarrierHolder,
|
|
6
8
|
Legendable,
|
|
9
|
+
Mover,
|
|
7
10
|
Placeable,
|
|
11
|
+
type AttachFrame,
|
|
8
12
|
type Alignment,
|
|
9
13
|
type Heights,
|
|
10
14
|
type LegendBinding,
|
|
15
|
+
type MoveOptions,
|
|
11
16
|
type PlacementArchetype
|
|
12
17
|
} from '@operato/scene-base'
|
|
13
18
|
|
|
@@ -72,27 +77,37 @@ const NATURE: ComponentNature = {
|
|
|
72
77
|
help: 'scene/component/asrs-crane'
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
// Mixin chain: Mover → CarrierHolder → ContainerCapacity → Legendable → Placeable → ContainerAbstract
|
|
81
|
+
//
|
|
82
|
+
// Mover: pick / place / pickAndPlace / moveTo / engage primitives
|
|
83
|
+
// CarrierHolder: attachPointFor() — where the carrier sits on the crane (carriage fork)
|
|
84
|
+
// ContainerCapacity: receive() / dispatch() / canReceive() / slots — slot tracking +
|
|
85
|
+
// TRANSFER_SLOT_KEY bookkeeping during transit
|
|
86
|
+
// Legendable: status → bodyColor / lampEmissive colour mapping
|
|
87
|
+
// Placeable: floor-archetype 3D positioning
|
|
88
|
+
// ContainerAbstract: child management — carrier becomes a child while in transit
|
|
89
|
+
//
|
|
90
|
+
// Note: ContainerAbstract replaces Shape. The 2D outline is drawn manually in
|
|
91
|
+
// render() below (a simple top-down rectangle), matching the old Shape output
|
|
92
|
+
// without the Shape base-class overhead.
|
|
77
93
|
/**
|
|
78
94
|
* AsrsCrane — the stacker / retrieval crane that runs in the aisle of an
|
|
79
95
|
* AS/RS, moving cargo between the load port and the rack cells.
|
|
80
96
|
*
|
|
81
97
|
* Structure: a tall vertical mast that translates along a floor + ceiling
|
|
82
98
|
* rail (the aisle), with a carriage that slides up/down the mast carrying a
|
|
83
|
-
* shuttle / forks.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
99
|
+
* shuttle / forks.
|
|
100
|
+
*
|
|
101
|
+
* **Monitoring mode**: crane status is driven by data binding
|
|
102
|
+
* (`state.status`, `state.carriageHeight`). The carrier is referenced
|
|
103
|
+
* via data binding — it is NOT a child of the crane in monitoring mode.
|
|
86
104
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* fmsim's CarrierManager pattern. Adding the carrier as a child would mix
|
|
91
|
-
* static placement with the dynamic data-driven flow we deliberately keep
|
|
92
|
-
* separate (see Phase A4 commit notes).
|
|
105
|
+
* **Simulation mode**: call `crane.pick(carrier)` / `crane.place(carrier, rackCell)`
|
|
106
|
+
* (or `crane.pickAndPlace(carrier, rackCell)`). Mover handles navigation +
|
|
107
|
+
* engage + reparent. During transit the carrier IS a child of the crane.
|
|
93
108
|
*/
|
|
94
109
|
@sceneComponent('asrs-crane')
|
|
95
|
-
export default class AsrsCrane extends
|
|
110
|
+
export default class AsrsCrane extends Mover(CarrierHolder(ContainerCapacity(Legendable(Placeable(ContainerAbstract))))) {
|
|
96
111
|
static legends: Record<string, LegendBinding> = {
|
|
97
112
|
bodyColor: { from: 'status', legend: BODY_LEGEND },
|
|
98
113
|
lampEmissive: { from: 'status', legend: LAMP_EMISSIVE_LEGEND }
|
|
@@ -102,6 +117,9 @@ export default class AsrsCrane extends Base {
|
|
|
102
117
|
static align: Alignment = 'bottom'
|
|
103
118
|
static defaultDepth = (h: Heights) => h.ceiling - h.floor
|
|
104
119
|
|
|
120
|
+
/** Yaw offset: crane model is drawn with the aisle axis along X (right = forward). */
|
|
121
|
+
static yawOffset = 0
|
|
122
|
+
|
|
105
123
|
get nature() {
|
|
106
124
|
return NATURE
|
|
107
125
|
}
|
|
@@ -110,21 +128,124 @@ export default class AsrsCrane extends Base {
|
|
|
110
128
|
return []
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
// ── ContainerCapacity ─────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/** Stacker crane carries at most one load at a time on its forks. */
|
|
134
|
+
get slots(): SlotDef[] {
|
|
135
|
+
return [{ id: 'forks', maxCount: 1 }]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── CarrierHolder — attach frame (carriage fork position) ─────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Return the 3D attach frame on the crane's carriage (fork tip).
|
|
142
|
+
* Carriers are attached here while the crane is in transit (pick phase).
|
|
143
|
+
*
|
|
144
|
+
* The AsrsCrane3D exposes `getCarriageFrame()` — a sub-Object3D that
|
|
145
|
+
* tracks the carriage height and sits at the fork TCP. If the 3D object
|
|
146
|
+
* isn't built yet (e.g. before scene initialization), fall back to the
|
|
147
|
+
* crane's own object3d centre.
|
|
148
|
+
*/
|
|
149
|
+
attachPointFor(carrier: Component): AttachFrame | null {
|
|
150
|
+
const ro = (this as any)._realObject as AsrsCrane3D | undefined
|
|
151
|
+
const frame = ro?.getCarriageFrame?.()
|
|
152
|
+
if (frame) {
|
|
153
|
+
const carrierDepth = resolveCarrierDepth(carrier)
|
|
154
|
+
return { attach: frame, localPosition: { x: 0, y: carrierDepth / 2, z: 0 } }
|
|
155
|
+
}
|
|
156
|
+
const root = (this as any)._realObject?.object3d
|
|
157
|
+
if (!root) return null
|
|
158
|
+
return { attach: root }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Mover overrides ───────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Domain-specific actuation between arrival and reparent.
|
|
165
|
+
*
|
|
166
|
+
* Simulation sequence for PICK:
|
|
167
|
+
* 1. Mover.pick() navigates crane to carrier position (moveTo).
|
|
168
|
+
* 2. engage('pick') → snap carriage height + status 'loading'.
|
|
169
|
+
* 3. Carrier is reparented to crane (becomes child).
|
|
170
|
+
*
|
|
171
|
+
* For now: set status and snap carriage height. A full ASRS simulation
|
|
172
|
+
* would tween the carriageHeight here (animate AsrsCrane3D).
|
|
173
|
+
*
|
|
174
|
+
* Status lifecycle:
|
|
175
|
+
* idle → (moveTo running) → engage fires → loading/unloading → (reparent) → idle
|
|
176
|
+
* The 'moving' state is not set from Mover.moveTo() because TypeScript
|
|
177
|
+
* can't call super.moveTo() on an `: any`-typed mixin. WCS data binding
|
|
178
|
+
* sets 'moving' in monitoring mode; override pick()/place() to set it
|
|
179
|
+
* in full simulation environments.
|
|
180
|
+
*/
|
|
181
|
+
async engage(
|
|
182
|
+
target: Component,
|
|
183
|
+
kind: 'pick' | 'place',
|
|
184
|
+
_options: MoveOptions = {}
|
|
185
|
+
): Promise<void> {
|
|
186
|
+
if (kind === 'pick') {
|
|
187
|
+
this.setState({ status: 'loading' as AsrsCraneStatus })
|
|
188
|
+
const carrierY = resolveCarrierCenterY(target)
|
|
189
|
+
if (carrierY !== null) {
|
|
190
|
+
this.setState({ carriageHeight: carrierY })
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
this.setState({ status: 'unloading' as AsrsCraneStatus })
|
|
194
|
+
}
|
|
195
|
+
// In a full simulation: await carriage-motion tween here.
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Domain aliases ────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
/** Fetch a carrier from a rack cell (semantically = pick). */
|
|
201
|
+
fetch(carrier: Component, options?: MoveOptions): Promise<void> {
|
|
202
|
+
return (this as any).pick(carrier, options)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Deposit a carrier into a rack cell (semantically = place). */
|
|
206
|
+
deposit(carrier: Component, cell: Component, options?: MoveOptions): Promise<void> {
|
|
207
|
+
return (this as any).place(carrier, cell, options)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── 2D rendering ─────────────────────────────────────────────────────────
|
|
211
|
+
|
|
113
212
|
/**
|
|
114
213
|
* 2D — top-down rectangle showing the crane's footprint along the aisle.
|
|
115
214
|
* The crane is much taller than wide, so the 2D mark is small.
|
|
116
215
|
*/
|
|
117
216
|
render(ctx: CanvasRenderingContext2D) {
|
|
118
217
|
const { width, height, left, top } = this.state
|
|
218
|
+
const fillColor = (this.state.bodyColor as string) || '#888'
|
|
219
|
+
ctx.save()
|
|
220
|
+
ctx.fillStyle = fillColor
|
|
119
221
|
ctx.beginPath()
|
|
120
222
|
ctx.rect(left, top, width, height)
|
|
223
|
+
ctx.fill()
|
|
224
|
+
ctx.restore()
|
|
121
225
|
}
|
|
122
226
|
|
|
123
|
-
|
|
124
|
-
return (this.state.bodyColor as string) || '#888'
|
|
125
|
-
}
|
|
227
|
+
// ── 3D ───────────────────────────────────────────────────────────────────
|
|
126
228
|
|
|
127
229
|
buildRealObject(): RealObject | undefined {
|
|
128
230
|
return new AsrsCrane3D(this as any)
|
|
129
231
|
}
|
|
130
232
|
}
|
|
233
|
+
|
|
234
|
+
function resolveCarrierDepth(c: Component): number {
|
|
235
|
+
const eff = (c as any)._realObject?.effectiveDepth
|
|
236
|
+
if (typeof eff === 'number' && Number.isFinite(eff)) return eff
|
|
237
|
+
return numOr((c as any)?.state?.depth, 0)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function resolveCarrierCenterY(c: Component): number | null {
|
|
241
|
+
const pos = (c as any).state
|
|
242
|
+
if (!pos) return null
|
|
243
|
+
// zPos is the 3D Y center of a Placeable component in things-scene
|
|
244
|
+
const zPos = numOr(pos.zPos, NaN)
|
|
245
|
+
if (!Number.isNaN(zPos)) return zPos
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function numOr(v: unknown, dflt: number): number {
|
|
250
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : dflt
|
|
251
|
+
}
|
package/src/asrs-rack.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
5
5
|
import {
|
|
6
|
+
CellContainer,
|
|
7
|
+
CellMap,
|
|
8
|
+
CarrierHolder,
|
|
6
9
|
Placeable,
|
|
10
|
+
type AttachFrame,
|
|
7
11
|
type Alignment,
|
|
8
12
|
type Heights,
|
|
9
13
|
type PlacementArchetype
|
|
@@ -35,35 +39,31 @@ const NATURE: ComponentNature = {
|
|
|
35
39
|
// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
|
|
36
40
|
// which forces `isHTMLElement(): true` and trips the 3D pipeline's
|
|
37
41
|
// addObject DOM-skip gate. ASRS rack lives only in the 3D scene graph.
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
//
|
|
43
|
+
// Mixin chain: CellContainer → CarrierHolder → Placeable → ContainerAbstract
|
|
44
|
+
// CellContainer: cell topology (cellMap, cell(), findAvailableCell(), occupiedCellIds())
|
|
45
|
+
// CarrierHolder: 3D attach-point protocol (attachPointFor, containable gates)
|
|
46
|
+
// Placeable: floor-archetype positioning
|
|
47
|
+
// ContainerAbstract: child component management
|
|
40
48
|
/**
|
|
41
49
|
* AsrsRack — a multi-level high-bay storage rack, the structural backbone of
|
|
42
50
|
* an AS/RS (Automated Storage / Retrieval System).
|
|
43
51
|
*
|
|
44
52
|
* `levels` × `bays` cells form a vertical grid. Each cell holds one logistics
|
|
45
53
|
* package (Pallet / Box / Parcel). A pair of AsrsRacks separated by an aisle
|
|
46
|
-
* (where an AsrsCrane runs) is the typical AS/RS configuration
|
|
47
|
-
* single-rack unit and lets users compose multi-rack systems by placing them
|
|
48
|
-
* side by side. A future `AsrsAisle` composite may bundle the pair + crane.
|
|
54
|
+
* (where an AsrsCrane runs) is the typical AS/RS configuration.
|
|
49
55
|
*
|
|
50
|
-
* **
|
|
51
|
-
*
|
|
52
|
-
* pallet load. Users can shorten via explicit `state.depth` for warehouses
|
|
53
|
-
* with smaller envelopes.
|
|
56
|
+
* **Monitoring mode** (default): pallets/boxes are direct children of the rack,
|
|
57
|
+
* placed by the WCS data binding. No RackCell children are created.
|
|
54
58
|
*
|
|
55
|
-
* **
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* z lands on the rack's overall bottom (parent.zPos + parent.depth = ceiling),
|
|
59
|
-
* which isn't quite cell-level resolution — true per-cell z positioning is
|
|
60
|
-
* a v3 concern (the cargo would need to know which cell-row it's in).
|
|
59
|
+
* **Simulation mode**: call `rack._buildCells()` after placing the rack on the
|
|
60
|
+
* scene. This creates RackCell children at the correct 3D positions. The
|
|
61
|
+
* AsrsCrane then navigates to individual RackCells for pick-and-place.
|
|
61
62
|
*
|
|
62
|
-
*
|
|
63
|
-
* occupancy state is implicit in the children, not a status flag.
|
|
63
|
+
* **Placement**: `floor` archetype, full ceiling depth by default.
|
|
64
64
|
*/
|
|
65
65
|
@sceneComponent('asrs-rack')
|
|
66
|
-
export default class AsrsRack extends
|
|
66
|
+
export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(ContainerAbstract))) {
|
|
67
67
|
static placement: PlacementArchetype = 'floor'
|
|
68
68
|
static align: Alignment = 'bottom'
|
|
69
69
|
static defaultDepth = (h: Heights) => h.ceiling - h.floor
|
|
@@ -76,13 +76,110 @@ export default class AsrsRack extends Base {
|
|
|
76
76
|
return []
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// ── CellContainer ─────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Derive the cell topology from the rack's current dimensions and bay/level
|
|
83
|
+
* counts. The CellMap is rebuilt fresh each time (state changes trigger
|
|
84
|
+
* re-reads via things-scene's invalidation pipeline).
|
|
85
|
+
*
|
|
86
|
+
* Coordinate convention (matches things-scene 3D):
|
|
87
|
+
* X = bay axis (left → right)
|
|
88
|
+
* Y = level axis (floor → ceiling, the rack's `depth` state property)
|
|
89
|
+
* Z = row axis (front → back, the rack's `height` state property)
|
|
90
|
+
*/
|
|
91
|
+
get cellMap(): CellMap {
|
|
92
|
+
const bays = Math.max(1, Math.floor((this.state.bays as number) || 5))
|
|
93
|
+
const levels = Math.max(1, Math.floor((this.state.levels as number) || 4))
|
|
94
|
+
const width = (this.state.width as number) || 1000
|
|
95
|
+
const rackDepth = (this.state.depth as number) || 3000 // Y: floor→ceiling
|
|
96
|
+
const rackHeight = (this.state.height as number) || 600 // Z: front→back
|
|
97
|
+
|
|
98
|
+
return CellMap.grid({
|
|
99
|
+
bays,
|
|
100
|
+
rows: 1,
|
|
101
|
+
levels,
|
|
102
|
+
bayWidth: width / bays,
|
|
103
|
+
rowDepth: rackHeight,
|
|
104
|
+
levelHeight: rackDepth / levels
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create RackCell child components for each cell in the CellMap.
|
|
110
|
+
*
|
|
111
|
+
* Called explicitly to enter simulation mode — monitoring-mode racks
|
|
112
|
+
* never call this (pallets are direct children, no explicit cells).
|
|
113
|
+
*
|
|
114
|
+
* Idempotent: removes existing rack-cell children first.
|
|
115
|
+
*/
|
|
116
|
+
_buildCells(): void {
|
|
117
|
+
// Remove existing rack-cell children
|
|
118
|
+
const existing = ((this as any).components as Component[] | undefined) ?? []
|
|
119
|
+
for (const child of [...existing]) {
|
|
120
|
+
if ((child as any).state?.type === 'rack-cell') {
|
|
121
|
+
;(this as any).removeComponent?.(child)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create a RackCell for each cell in the map
|
|
126
|
+
const RackCellClass = (Component as any).register('rack-cell') as (new (...args: any[]) => Component) | undefined
|
|
127
|
+
if (!RackCellClass) {
|
|
128
|
+
console.warn('AsrsRack._buildCells: rack-cell type not registered. Import rack-cell.ts first.')
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const context = (this as any)._app
|
|
133
|
+
for (const cell of this.cellMap.cells) {
|
|
134
|
+
const model = {
|
|
135
|
+
type: 'rack-cell',
|
|
136
|
+
cellId: cell.id,
|
|
137
|
+
width: cell.size.width,
|
|
138
|
+
height: cell.size.depth, // 2D height = 3D Z depth
|
|
139
|
+
depth: cell.size.height // 3D Y = level height
|
|
140
|
+
}
|
|
141
|
+
const rackCell = new RackCellClass(model, context)
|
|
142
|
+
;(this as any).addComponent?.(rackCell)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Container gates ───────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Allow:
|
|
150
|
+
* - Carriable components (pallets, boxes, parcels) — direct children in monitoring mode.
|
|
151
|
+
* - RackCell — created by _buildCells() in simulation mode.
|
|
152
|
+
*
|
|
153
|
+
* Block:
|
|
154
|
+
* - Everything else (sensors, labels, etc. can be siblings of the rack, not children).
|
|
155
|
+
*/
|
|
156
|
+
containable(component: Component): boolean {
|
|
157
|
+
if ((component as any).state?.type === 'rack-cell') return true
|
|
81
158
|
const archetype = (component.constructor as any).placement
|
|
82
159
|
if (archetype === 'operation') return true
|
|
83
160
|
return component.isDescendible(this as any)
|
|
84
161
|
}
|
|
85
162
|
|
|
163
|
+
// ── CarrierHolder — attach frame for direct carrier children ─────────────
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Attach frame for carriers that are DIRECT children of the rack
|
|
167
|
+
* (monitoring mode, where pallets go directly into the rack without
|
|
168
|
+
* explicit RackCell components).
|
|
169
|
+
*
|
|
170
|
+
* In simulation mode, carriers become children of their RackCell,
|
|
171
|
+
* and each RackCell provides its own attachPointFor(). So this method
|
|
172
|
+
* is only invoked on direct-child carriers in monitoring mode — it
|
|
173
|
+
* returns the rack's own object3d as the attach frame (default behavior).
|
|
174
|
+
*/
|
|
175
|
+
attachPointFor(_carrier: Component): AttachFrame | null {
|
|
176
|
+
const root = (this as any)._realObject?.object3d
|
|
177
|
+
if (!root) return null
|
|
178
|
+
return { attach: root }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 2D rendering ─────────────────────────────────────────────────────────
|
|
182
|
+
|
|
86
183
|
/**
|
|
87
184
|
* 2D — top-down rectangle showing the rack footprint, with subdivisions
|
|
88
185
|
* suggesting the bay layout (lines parallel to the aisle).
|
|
@@ -106,6 +203,8 @@ export default class AsrsRack extends Base {
|
|
|
106
203
|
return '#a0a0a8'
|
|
107
204
|
}
|
|
108
205
|
|
|
206
|
+
// ── 3D ───────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
109
208
|
buildRealObject(): RealObject | undefined {
|
|
110
209
|
return new AsrsRack3D(this as any)
|
|
111
210
|
}
|
package/src/box.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { ComponentNature, RealObject, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene'
|
|
5
5
|
import {
|
|
6
6
|
Carriable,
|
|
7
7
|
Legendable,
|
|
@@ -54,8 +54,6 @@ const NATURE: ComponentNature = {
|
|
|
54
54
|
|
|
55
55
|
// Carriable: a box can be a child of any CarrierHolder (Pallet for stacking,
|
|
56
56
|
// AGV deck, robot-arm gripper, Spot for staging).
|
|
57
|
-
const Base = Carriable(Legendable(Placeable(RectPath(Shape)))) as unknown as typeof Component
|
|
58
|
-
|
|
59
57
|
/**
|
|
60
58
|
* Box — a generic stackable container for goods. Wood crate or plastic tote
|
|
61
59
|
* variants distinguished by `material` prop.
|
|
@@ -65,7 +63,7 @@ const Base = Carriable(Legendable(Placeable(RectPath(Shape)))) as unknown as typ
|
|
|
65
63
|
* scene-tree). If a future use case needs nested boxes, extend Container.
|
|
66
64
|
*/
|
|
67
65
|
@sceneComponent('box')
|
|
68
|
-
export default class Box extends
|
|
66
|
+
export default class Box extends Carriable(Legendable(Placeable(RectPath(Shape)))) {
|
|
69
67
|
static legends: Record<string, LegendBinding> = {
|
|
70
68
|
bodyColor: { from: 'material', legend: BODY_LEGEND }
|
|
71
69
|
}
|
package/src/generic-container.ts
CHANGED
|
@@ -87,10 +87,8 @@ const NATURE: ComponentNature = {
|
|
|
87
87
|
|
|
88
88
|
// 합성 순서: 안쪽부터 → ContainerAbstract → Placeable → Legendable → GltfComponent
|
|
89
89
|
// (GenericFacility 와 동일 패턴)
|
|
90
|
-
const Base = GltfComponent(Legendable(Placeable(ContainerAbstract))) as unknown as typeof Component
|
|
91
|
-
|
|
92
90
|
@sceneComponent('container')
|
|
93
|
-
export default class GenericContainer extends
|
|
91
|
+
export default class GenericContainer extends GltfComponent(Legendable(Placeable(ContainerAbstract))) {
|
|
94
92
|
static legends: Record<string, LegendBinding> = {
|
|
95
93
|
bodyColor: { from: 'fill', legend: BODY_LEGEND },
|
|
96
94
|
lampEmissive: { from: 'fill', legend: LAMP_EMISSIVE_LEGEND }
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,9 @@ export type { BoxMaterial } from './box.js'
|
|
|
10
10
|
export { default as Parcel } from './parcel.js'
|
|
11
11
|
|
|
12
12
|
export { default as AsrsRack } from './asrs-rack.js'
|
|
13
|
+
export { default as RackCell } from './rack-cell.js'
|
|
14
|
+
export type { RackCellType } from './rack-cell.js'
|
|
15
|
+
export { RackCell3D } from './rack-cell-3d.js'
|
|
13
16
|
export { default as AsrsCrane } from './asrs-crane.js'
|
|
14
17
|
export type { AsrsCraneStatus } from './asrs-crane.js'
|
|
15
18
|
|
package/src/pallet.ts
CHANGED
|
@@ -60,8 +60,6 @@ const NATURE: ComponentNature = {
|
|
|
60
60
|
// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
|
|
61
61
|
// which forces `isHTMLElement(): true` and trips the 3D pipeline's
|
|
62
62
|
// addObject DOM-skip gate. Pallet renders only as a 3D mesh.
|
|
63
|
-
const Base = Carriable(Legendable(Placeable(ContainerAbstract))) as unknown as typeof Component
|
|
64
|
-
|
|
65
63
|
/**
|
|
66
64
|
* Pallet — a flat transport structure that goods are stacked and stored on.
|
|
67
65
|
*
|
|
@@ -89,7 +87,7 @@ const Base = Carriable(Legendable(Placeable(ContainerAbstract))) as unknown as t
|
|
|
89
87
|
* detection.
|
|
90
88
|
*/
|
|
91
89
|
@sceneComponent('pallet')
|
|
92
|
-
export default class Pallet extends
|
|
90
|
+
export default class Pallet extends Carriable(Legendable(Placeable(ContainerAbstract))) {
|
|
93
91
|
static legends: Record<string, LegendBinding> = {
|
|
94
92
|
bodyColor: { from: 'material', legend: BODY_LEGEND }
|
|
95
93
|
}
|
package/src/parcel.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { ComponentNature, RealObject, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene'
|
|
5
5
|
import {
|
|
6
6
|
Carriable,
|
|
7
7
|
Placeable,
|
|
@@ -28,8 +28,6 @@ const NATURE: ComponentNature = {
|
|
|
28
28
|
// Carriable: parcel can be a child of any CarrierHolder (Spot, robot-arm
|
|
29
29
|
// gripper, AGV deck, …). Mixin wraps add() so the parcel's 3D object3d
|
|
30
30
|
// is reattached to the holder's chosen mount frame.
|
|
31
|
-
const Base = Carriable(Placeable(RectPath(Shape))) as unknown as typeof Component
|
|
32
|
-
|
|
33
31
|
/**
|
|
34
32
|
* Parcel — a cardboard package, the typical e-commerce / parcel-sortation unit.
|
|
35
33
|
*
|
|
@@ -46,7 +44,7 @@ const Base = Carriable(Placeable(RectPath(Shape))) as unknown as typeof Componen
|
|
|
46
44
|
* inspected indicators would add a status legend then.
|
|
47
45
|
*/
|
|
48
46
|
@sceneComponent('parcel')
|
|
49
|
-
export default class Parcel extends
|
|
47
|
+
export default class Parcel extends Carriable(Placeable(RectPath(Shape))) {
|
|
50
48
|
static placement: PlacementArchetype = 'operation'
|
|
51
49
|
static align: Alignment = 'bottom'
|
|
52
50
|
static defaultDepth = 150
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* RackCell 3D — invisible anchor group positioned at the cell's location
|
|
5
|
+
* within the parent rack's 3D coordinate space.
|
|
6
|
+
*
|
|
7
|
+
* RackCell has no geometry of its own. Its sole 3D purpose is to provide
|
|
8
|
+
* an Object3D that carriers can be attached to (via Three.js `.attach()`),
|
|
9
|
+
* placed at the exact cell position within the rack. The position is derived
|
|
10
|
+
* from the parent AsrsRack's CellMap (by cellId), not from 2D state fields —
|
|
11
|
+
* rack cells occupy 3D levels that have no 2D analogue.
|
|
12
|
+
*
|
|
13
|
+
* updateTransform() override: things-scene's standard updateTransform
|
|
14
|
+
* reads `component.center` (2D) and flattens it to 3D. For rack cells this
|
|
15
|
+
* is wrong — we need the 3D cell position (bay x, level y, row z) from the
|
|
16
|
+
* parent rack. So we override and read it directly from the CellMap.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as THREE from 'three'
|
|
20
|
+
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
21
|
+
|
|
22
|
+
export class RackCell3D extends RealObjectGroup {
|
|
23
|
+
build() {
|
|
24
|
+
super.build()
|
|
25
|
+
this._repositionFromCellMap()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
updateDimension() {
|
|
29
|
+
// intentional no-op — size comes from the cell definition, not state
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
updateTransform() {
|
|
33
|
+
this._repositionFromCellMap()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
updateAlpha() {
|
|
37
|
+
// invisible — no materials to update
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Position this group at the cell's localPosition within the rack's 3D space.
|
|
42
|
+
*
|
|
43
|
+
* CellMap.grid() places cell origins at:
|
|
44
|
+
* x = b * bayWidth, y = l * levelHeight, z = r * rowDepth
|
|
45
|
+
* (starting from 0,0,0 at the rack's bottom-left-front corner).
|
|
46
|
+
*
|
|
47
|
+
* The rack's object3d is centered: X spans [-width/2, +width/2],
|
|
48
|
+
* Y spans [-depth/2, +depth/2], Z spans [-height/2, +height/2].
|
|
49
|
+
*
|
|
50
|
+
* So the cell centre in rack-local 3D space:
|
|
51
|
+
* x3d = cellPos.x + bayWidth/2 - width/2
|
|
52
|
+
* y3d = cellPos.y + levelHeight/2 - rackDepth/2
|
|
53
|
+
* z3d = cellPos.z + rowDepth/2 - rackHeight/2
|
|
54
|
+
*/
|
|
55
|
+
_repositionFromCellMap() {
|
|
56
|
+
const rack = (this.component as any).parent
|
|
57
|
+
if (!rack?.cellMap) return
|
|
58
|
+
|
|
59
|
+
const cellId = (this.component as any).state?.cellId as string | undefined
|
|
60
|
+
if (!cellId) return
|
|
61
|
+
|
|
62
|
+
const cell = rack.cellMap.findById(cellId)
|
|
63
|
+
if (!cell) return
|
|
64
|
+
|
|
65
|
+
const rs = rack.state as any
|
|
66
|
+
const rackWidth = (rs?.width as number) || 1000
|
|
67
|
+
const rackDepth = (rs?.depth as number) || 3000 // Y dimension (floor→ceiling)
|
|
68
|
+
const rackHeight = (rs?.height as number) || 600 // Z dimension (front→back, 2D height)
|
|
69
|
+
const bays = Math.max(1, Math.floor(rs?.bays || 5))
|
|
70
|
+
const levels = Math.max(1, Math.floor(rs?.levels || 4))
|
|
71
|
+
const rows = 1
|
|
72
|
+
|
|
73
|
+
const bayWidth = rackWidth / bays
|
|
74
|
+
const levelHeight = rackDepth / levels
|
|
75
|
+
const rowDepth = rackHeight / rows
|
|
76
|
+
|
|
77
|
+
const x3d = cell.localPosition.x + bayWidth / 2 - rackWidth / 2
|
|
78
|
+
const y3d = cell.localPosition.y + levelHeight / 2 - rackDepth / 2
|
|
79
|
+
const z3d = cell.localPosition.z + rowDepth / 2 - rackHeight / 2
|
|
80
|
+
|
|
81
|
+
this.object3d.position.set(x3d, y3d, z3d)
|
|
82
|
+
|
|
83
|
+
// Optionally visualise cells in debug mode (outline box, very faint)
|
|
84
|
+
if (process.env.NODE_ENV !== 'production' && (rack.state as any)?.debugCells) {
|
|
85
|
+
this._addDebugBox(bayWidth, levelHeight, rowDepth)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private _debugBox?: THREE.LineSegments
|
|
90
|
+
|
|
91
|
+
private _addDebugBox(w: number, h: number, d: number) {
|
|
92
|
+
if (this._debugBox) return
|
|
93
|
+
const geo = new THREE.BoxGeometry(w * 0.98, h * 0.98, d * 0.98)
|
|
94
|
+
const edges = new THREE.EdgesGeometry(geo)
|
|
95
|
+
this._debugBox = new THREE.LineSegments(
|
|
96
|
+
edges,
|
|
97
|
+
new THREE.LineBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0.2 })
|
|
98
|
+
)
|
|
99
|
+
this.object3d.add(this._debugBox)
|
|
100
|
+
}
|
|
101
|
+
}
|