@operato/scene-storage 10.0.0-beta.27 → 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.
Files changed (67) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/asrs-crane-3d.d.ts +10 -0
  3. package/dist/asrs-crane-3d.js +17 -0
  4. package/dist/asrs-crane-3d.js.map +1 -1
  5. package/dist/asrs-crane.d.ts +49 -13
  6. package/dist/asrs-crane.js +120 -16
  7. package/dist/asrs-crane.js.map +1 -1
  8. package/dist/asrs-rack.d.ts +49 -19
  9. package/dist/asrs-rack.js +108 -20
  10. package/dist/asrs-rack.js.map +1 -1
  11. package/dist/box.d.ts +3 -3
  12. package/dist/box.js +1 -2
  13. package/dist/box.js.map +1 -1
  14. package/dist/generic-container.d.ts +2 -2
  15. package/dist/generic-container.js +1 -2
  16. package/dist/generic-container.js.map +1 -1
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/pallet.d.ts +2 -2
  21. package/dist/pallet.js +1 -2
  22. package/dist/pallet.js.map +1 -1
  23. package/dist/parcel.d.ts +3 -3
  24. package/dist/parcel.js +1 -2
  25. package/dist/parcel.js.map +1 -1
  26. package/dist/rack-cell-3d.d.ts +25 -0
  27. package/dist/rack-cell-3d.js +88 -0
  28. package/dist/rack-cell-3d.js.map +1 -0
  29. package/dist/rack-cell.d.ts +56 -0
  30. package/dist/rack-cell.js +200 -0
  31. package/dist/rack-cell.js.map +1 -0
  32. package/dist/spot.d.ts +4 -11
  33. package/dist/spot.js +2 -3
  34. package/dist/spot.js.map +1 -1
  35. package/dist/templates/index.d.ts +42 -0
  36. package/dist/templates/index.js +49 -9
  37. package/dist/templates/index.js.map +1 -1
  38. package/dist/templates/spot.js +1 -1
  39. package/dist/templates/spot.js.map +1 -1
  40. package/icons/asrs-crane.png +0 -0
  41. package/icons/asrs-rack.png +0 -0
  42. package/icons/box.png +0 -0
  43. package/icons/pallet.png +0 -0
  44. package/icons/parcel.png +0 -0
  45. package/icons/spot.png +0 -0
  46. package/package.json +9 -4
  47. package/src/asrs-crane-3d.ts +20 -0
  48. package/src/asrs-crane.ts +137 -16
  49. package/src/asrs-rack.ts +119 -20
  50. package/src/box.ts +2 -4
  51. package/src/generic-container.ts +1 -3
  52. package/src/index.ts +3 -0
  53. package/src/pallet.ts +1 -3
  54. package/src/parcel.ts +2 -4
  55. package/src/rack-cell-3d.ts +101 -0
  56. package/src/rack-cell.ts +228 -0
  57. package/src/spot.ts +4 -5
  58. package/src/templates/index.ts +49 -9
  59. package/src/templates/spot.ts +1 -1
  60. package/test/setup.js +279 -0
  61. package/test/test-asrs-crane.ts +319 -0
  62. package/tsconfig.json +2 -1
  63. package/tsconfig.tsbuildinfo +1 -1
  64. package/icons/box-plastic.png +0 -0
  65. package/icons/box-wood.png +0 -0
  66. package/icons/pallet-plastic.png +0 -0
  67. package/icons/pallet-wood.png +0 -0
@@ -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 Base {
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 Base {
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 { Component, ComponentNature, RealObject, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene'
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 Base {
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
+ }
@@ -0,0 +1,228 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * RackCell — a single storage slot within an AsrsRack.
5
+ *
6
+ * A RackCell is a virtual component: it occupies a specific (bay, row, level)
7
+ * coordinate within the parent rack and acts as a CarrierHolder for one carrier
8
+ * (or several, depending on `cellType`).
9
+ *
10
+ * The crane (AsrsCrane) navigates toward a RackCell as its `place()` destination —
11
+ * because the rack cell is a discrete component, Mover.moveTo() can target it
12
+ * directly and arrive at exactly the right bay × level position.
13
+ *
14
+ * Visual: invisible in 2D (no visible 2D footprint — rack cells don't make
15
+ * sense as 2D top-down boxes). In 3D, each cell is an invisible Group
16
+ * positioned within the rack's coordinate space (RackCell3D handles
17
+ * this via updateTransform override).
18
+ *
19
+ * Lifecycle: AsrsRack._buildCells() instantiates RackCell children.
20
+ * Do not create RackCell components manually — they are managed by the rack.
21
+ *
22
+ * Domain aliases:
23
+ * cell.store(carrier) ← cell.receive(carrier)
24
+ * cell.retrieve(carrier, target) ← cell.dispatch(carrier, target)
25
+ */
26
+
27
+ import {
28
+ Component,
29
+ ComponentNature,
30
+ ContainerAbstract,
31
+ RealObject,
32
+ TRANSFER_SLOT_KEY,
33
+ sceneComponent
34
+ } from '@hatiolab/things-scene'
35
+ import { CarrierHolder, type AttachFrame } from '@operato/scene-base'
36
+
37
+ import { RackCell3D } from './rack-cell-3d.js'
38
+
39
+ /**
40
+ * How many carriers a cell can hold simultaneously.
41
+ * - single: exactly 1 (typical pallet bay)
42
+ * - multi: small stack (up to 4, e.g. a multi-deep tray)
43
+ * - bulk: unlimited (e.g. a floor area measured in slots)
44
+ */
45
+ export type RackCellType = 'single' | 'multi' | 'bulk'
46
+
47
+ const NATURE: ComponentNature = {
48
+ mutable: false,
49
+ resizable: false,
50
+ rotatable: false,
51
+ properties: [
52
+ {
53
+ type: 'string',
54
+ label: 'cell-id',
55
+ name: 'cellId',
56
+ placeholder: 'e.g. 0-0-0'
57
+ },
58
+ {
59
+ type: 'select',
60
+ label: 'cell-type',
61
+ name: 'cellType',
62
+ property: {
63
+ options: [
64
+ { display: 'Single', value: 'single' },
65
+ { display: 'Multi', value: 'multi' },
66
+ { display: 'Bulk', value: 'bulk' }
67
+ ]
68
+ }
69
+ }
70
+ ],
71
+ help: 'scene/component/rack-cell'
72
+ }
73
+
74
+ /**
75
+ * RackCell — single-slot storage cell inside an AsrsRack.
76
+ *
77
+ * Mixin chain: CarrierHolder(ContainerAbstract)
78
+ * - CarrierHolder: publishes attachPointFor(), gates containable() to Carriables
79
+ * - ContainerAbstract: manages child carrier components
80
+ *
81
+ * No Placeable mixin — RackCell3D self-positions from the parent rack's
82
+ * CellMap (via updateTransform override), bypassing things-scene's standard
83
+ * 2D→3D coordinate mapping which cannot express 3D levels.
84
+ */
85
+ @sceneComponent('rack-cell')
86
+ export default class RackCell extends CarrierHolder(ContainerAbstract) {
87
+ // ── Identification ────────────────────────────────────────────────────────
88
+
89
+ get cellId(): string {
90
+ return (this.state.cellId as string) || ''
91
+ }
92
+
93
+ get cellType(): RackCellType {
94
+ return ((this.state.cellType as RackCellType) || 'single')
95
+ }
96
+
97
+ /** Maximum carrier count for this cell based on cellType. */
98
+ get capacity(): number {
99
+ switch (this.cellType) {
100
+ case 'single': return 1
101
+ case 'multi': return 4
102
+ case 'bulk': return Infinity
103
+ }
104
+ }
105
+
106
+ // ── Interface ─────────────────────────────────────────────────────────────
107
+
108
+ get nature(): ComponentNature {
109
+ return NATURE
110
+ }
111
+
112
+ get anchors(): [] {
113
+ return []
114
+ }
115
+
116
+ // ── Transfer protocol ─────────────────────────────────────────────────────
117
+
118
+ /** True when fewer carriers are currently held than capacity. */
119
+ canReceive(_component?: any): boolean {
120
+ const occupied = ((this as any).components as Component[] | undefined)?.length ?? 0
121
+ return occupied < this.capacity
122
+ }
123
+
124
+ /**
125
+ * Accept a carrier into this cell.
126
+ * Sets TRANSFER_SLOT_KEY = cellId on the carrier, then reparents.
127
+ * Fires 'transfer-received' so monitors can react.
128
+ */
129
+ async receive(carrier: any, options: any = {}): Promise<void> {
130
+ if (!this.canReceive(carrier)) {
131
+ ;(this as any).trigger?.('transfer-rejected', {
132
+ type: 'transfer-rejected',
133
+ component: carrier,
134
+ container: this,
135
+ reason: 'no-slot'
136
+ })
137
+ return
138
+ }
139
+ carrier[TRANSFER_SLOT_KEY] = this.cellId
140
+ ;(this as any).reparent?.(carrier, options)
141
+ ;(this as any).trigger?.('transfer-received', {
142
+ type: 'transfer-received',
143
+ component: carrier,
144
+ container: this,
145
+ slotId: this.cellId
146
+ })
147
+ }
148
+
149
+ /**
150
+ * Release a carrier from this cell to `target`.
151
+ * Delegates to `target.receive()` if available, otherwise `target.reparent()`.
152
+ */
153
+ async dispatch(carrier: any, target: any, options: any = {}): Promise<void> {
154
+ if (target?.canReceive && !target.canReceive(carrier)) {
155
+ ;(this as any).trigger?.('transfer-rejected', {
156
+ type: 'transfer-rejected',
157
+ component: carrier,
158
+ container: this,
159
+ reason: 'target-full'
160
+ })
161
+ return
162
+ }
163
+ delete carrier[TRANSFER_SLOT_KEY]
164
+ if (typeof target?.receive === 'function') {
165
+ await target.receive(carrier, options)
166
+ } else {
167
+ ;(target as any).reparent?.(carrier, options)
168
+ }
169
+ ;(this as any).trigger?.('transfer-dispatched', {
170
+ type: 'transfer-dispatched',
171
+ component: carrier,
172
+ container: this,
173
+ target
174
+ })
175
+ }
176
+
177
+ // ── Domain aliases ────────────────────────────────────────────────────────
178
+
179
+ /** Alias for receive() — semantic sugar for the storage domain. */
180
+ store(carrier: any, options?: any): Promise<void> {
181
+ return this.receive(carrier, options)
182
+ }
183
+
184
+ /** Alias for dispatch() — semantic sugar for the storage domain. */
185
+ retrieve(carrier: any, target: any, options?: any): Promise<void> {
186
+ return this.dispatch(carrier, target, options)
187
+ }
188
+
189
+ // ── 3D attach frame ───────────────────────────────────────────────────────
190
+
191
+ /**
192
+ * Return the 3D attach frame for carriers placed in this cell.
193
+ * Carriers are lifted by their own halfDepth so the bottom face
194
+ * rests at the cell's Y-center (which is levelHeight/2 above the beam).
195
+ */
196
+ attachPointFor(carrier: Component): AttachFrame | null {
197
+ const root = (this as any)._realObject?.object3d
198
+ if (!root) return null
199
+ const carrierDepth = resolveCarrierDepth(carrier)
200
+ return {
201
+ attach: root,
202
+ localPosition: { x: 0, y: carrierDepth / 2, z: 0 }
203
+ }
204
+ }
205
+
206
+ // ── 2D rendering ──────────────────────────────────────────────────────────
207
+
208
+ /** RackCell has no 2D visual — the rack draws its own structure. */
209
+ render(_ctx: CanvasRenderingContext2D) {
210
+ // intentional no-op
211
+ }
212
+
213
+ // ── 3D ───────────────────────────────────────────────────────────────────
214
+
215
+ buildRealObject(): RealObject | undefined {
216
+ return new RackCell3D(this as any)
217
+ }
218
+ }
219
+
220
+ function resolveCarrierDepth(c: Component): number {
221
+ const eff = (c as any)._realObject?.effectiveDepth
222
+ if (typeof eff === 'number' && Number.isFinite(eff)) return eff
223
+ return numOr((c as any)?.state?.depth, 0)
224
+ }
225
+
226
+ function numOr(v: unknown, dflt: number): number {
227
+ return typeof v === 'number' && Number.isFinite(v) ? v : dflt
228
+ }
package/src/spot.ts CHANGED
@@ -33,6 +33,7 @@ import { Component, ComponentNature, ContainerAbstract, RealObject, sceneCompone
33
33
  import {
34
34
  CarrierHolder,
35
35
  Placeable,
36
+ type AttachFrame,
36
37
  type Alignment,
37
38
  type Heights,
38
39
  type PlacementArchetype
@@ -58,10 +59,8 @@ const NATURE: ComponentNature = {
58
59
  // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
59
60
  // which forces `isHTMLElement(): true` and trips the 3D pipeline's
60
61
  // addObject DOM-skip gate. Spot is purely 3D.
61
- const Base = CarrierHolder(Placeable(ContainerAbstract)) as unknown as typeof Component
62
-
63
62
  @sceneComponent('spot')
64
- export default class Spot extends Base {
63
+ export default class Spot extends CarrierHolder(Placeable(ContainerAbstract)) {
65
64
  static placement: PlacementArchetype = 'floor'
66
65
  static align: Alignment = 'bottom'
67
66
  static defaultDepth = (_h: Heights) => 2 // a thin pad
@@ -147,10 +146,10 @@ export default class Spot extends Base {
147
146
  * falling back to raw `state.depth` for components built before
148
147
  * RealObject creation.
149
148
  */
150
- attachPointFor(carrier: Component) {
149
+ attachPointFor(carrier: Component): AttachFrame | null {
151
150
  const ro = (this as any)._realObject as Spot3D | undefined
152
151
  const frame = ro?.getAttachFrame?.()
153
- if (!frame) return undefined
152
+ if (!frame) return null
154
153
  const carrierDepth = resolveDepth(carrier)
155
154
  return {
156
155
  attach: frame,
@@ -1,12 +1,10 @@
1
1
  /*
2
2
  * things-scene catalog templates for the storage domain — pallet/box/parcel
3
- * variants, ASRS rack/crane, and the virtual `spot` placement marker.
3
+ * variants, ASRS rack/crane, ASRS aisle composite, and the virtual `spot` placement marker.
4
4
  */
5
5
  import spot from './spot.js'
6
- const palletWood = new URL('../../icons/pallet-wood.png', import.meta.url).href
7
- const palletPlastic = new URL('../../icons/pallet-plastic.png', import.meta.url).href
8
- const boxWood = new URL('../../icons/box-wood.png', import.meta.url).href
9
- const boxPlastic = new URL('../../icons/box-plastic.png', import.meta.url).href
6
+ const pallet = new URL('../../icons/pallet.png', import.meta.url).href
7
+ const box = new URL('../../icons/box.png', import.meta.url).href
10
8
  const parcel = new URL('../../icons/parcel.png', import.meta.url).href
11
9
  const asrsRack = new URL('../../icons/asrs-rack.png', import.meta.url).href
12
10
  const asrsCrane = new URL('../../icons/asrs-crane.png', import.meta.url).href
@@ -16,7 +14,7 @@ export default [
16
14
  type: 'pallet',
17
15
  description: 'wood pallet (EUR / EPAL)',
18
16
  group: 'storage',
19
- icon: palletWood,
17
+ icon: pallet,
20
18
  model: {
21
19
  type: 'pallet',
22
20
  top: 100,
@@ -30,7 +28,7 @@ export default [
30
28
  type: 'pallet',
31
29
  description: 'plastic pallet',
32
30
  group: 'storage',
33
- icon: palletPlastic,
31
+ icon: pallet,
34
32
  model: {
35
33
  type: 'pallet',
36
34
  top: 100,
@@ -44,7 +42,7 @@ export default [
44
42
  type: 'box',
45
43
  description: 'wood crate',
46
44
  group: 'storage',
47
- icon: boxWood,
45
+ icon: box,
48
46
  model: {
49
47
  type: 'box',
50
48
  top: 100,
@@ -58,7 +56,7 @@ export default [
58
56
  type: 'box',
59
57
  description: 'plastic tote',
60
58
  group: 'storage',
61
- icon: boxPlastic,
59
+ icon: box,
62
60
  model: {
63
61
  type: 'box',
64
62
  top: 100,
@@ -111,5 +109,47 @@ export default [
111
109
  carriageHeight: 100
112
110
  }
113
111
  },
112
+ {
113
+ type: 'group',
114
+ description: 'AS/RS aisle — 4-level rack pair + stacker crane',
115
+ group: 'storage',
116
+ icon: asrsRack,
117
+ model: {
118
+ type: 'group',
119
+ top: 100,
120
+ left: 100,
121
+ width: 840,
122
+ height: 220,
123
+ components: [
124
+ {
125
+ type: 'asrs-rack',
126
+ top: 100,
127
+ left: 100,
128
+ width: 800,
129
+ height: 80,
130
+ levels: 4,
131
+ bays: 8
132
+ },
133
+ {
134
+ type: 'asrs-crane',
135
+ top: 140,
136
+ left: 420,
137
+ width: 60,
138
+ height: 220,
139
+ status: 'idle',
140
+ carriageHeight: 0
141
+ },
142
+ {
143
+ type: 'asrs-rack',
144
+ top: 320,
145
+ left: 100,
146
+ width: 800,
147
+ height: 80,
148
+ levels: 4,
149
+ bays: 8
150
+ }
151
+ ]
152
+ }
153
+ },
114
154
  spot
115
155
  ]
@@ -1,5 +1,5 @@
1
1
  // Reuse parcel.png as a placeholder icon until a dedicated spot icon is drawn.
2
- const icon = new URL('../../icons/parcel.png', import.meta.url).href
2
+ const icon = new URL('../../icons/spot.png', import.meta.url).href
3
3
 
4
4
  export default {
5
5
  type: 'spot',