@operato/scene-storage 10.0.0-beta.32 → 10.0.0-beta.34

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 (89) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/crane-3d.d.ts +14 -0
  3. package/dist/crane-3d.js +238 -0
  4. package/dist/crane-3d.js.map +1 -0
  5. package/dist/crane.d.ts +157 -0
  6. package/dist/crane.js +440 -0
  7. package/dist/crane.js.map +1 -0
  8. package/dist/index.d.ts +13 -6
  9. package/dist/index.js +9 -4
  10. package/dist/index.js.map +1 -1
  11. package/dist/mobile-storage-rack.d.ts +17 -0
  12. package/dist/mobile-storage-rack.js +55 -0
  13. package/dist/mobile-storage-rack.js.map +1 -0
  14. package/dist/rack-column.d.ts +35 -0
  15. package/dist/rack-column.js +258 -0
  16. package/dist/rack-column.js.map +1 -0
  17. package/dist/rack-grid-3d.d.ts +13 -0
  18. package/dist/rack-grid-3d.js +94 -0
  19. package/dist/rack-grid-3d.js.map +1 -0
  20. package/dist/rack-grid-cell.d.ts +341 -0
  21. package/dist/rack-grid-cell.js +321 -0
  22. package/dist/rack-grid-cell.js.map +1 -0
  23. package/dist/rack-grid-helpers.d.ts +28 -0
  24. package/dist/rack-grid-helpers.js +71 -0
  25. package/dist/rack-grid-helpers.js.map +1 -0
  26. package/dist/rack-grid-location.d.ts +37 -0
  27. package/dist/rack-grid-location.js +227 -0
  28. package/dist/rack-grid-location.js.map +1 -0
  29. package/dist/rack-grid.d.ts +80 -0
  30. package/dist/rack-grid.js +829 -0
  31. package/dist/rack-grid.js.map +1 -0
  32. package/dist/stock.d.ts +78 -0
  33. package/dist/stock.js +333 -0
  34. package/dist/stock.js.map +1 -0
  35. package/dist/{rack-cell-3d.d.ts → storage-cell-3d.d.ts} +1 -1
  36. package/dist/{rack-cell-3d.js → storage-cell-3d.js} +3 -3
  37. package/dist/storage-cell-3d.js.map +1 -0
  38. package/dist/{rack-cell.d.ts → storage-cell.d.ts} +12 -6
  39. package/dist/{rack-cell.js → storage-cell.js} +9 -9
  40. package/dist/storage-cell.js.map +1 -0
  41. package/dist/{asrs-rack-3d.d.ts → storage-rack-3d.d.ts} +1 -1
  42. package/dist/{asrs-rack-3d.js → storage-rack-3d.js} +4 -4
  43. package/dist/storage-rack-3d.js.map +1 -0
  44. package/dist/{asrs-rack.d.ts → storage-rack.d.ts} +22 -16
  45. package/dist/{asrs-rack.js → storage-rack.js} +32 -26
  46. package/dist/storage-rack.js.map +1 -0
  47. package/dist/templates/index.d.ts +60 -0
  48. package/dist/templates/index.js +59 -17
  49. package/dist/templates/index.js.map +1 -1
  50. package/package.json +3 -3
  51. package/src/crane-3d.ts +273 -0
  52. package/src/crane.ts +538 -0
  53. package/src/index.ts +13 -6
  54. package/src/mobile-storage-rack.ts +56 -0
  55. package/src/rack-column.ts +340 -0
  56. package/src/rack-grid-3d.ts +128 -0
  57. package/src/rack-grid-cell.ts +404 -0
  58. package/src/rack-grid-helpers.ts +77 -0
  59. package/src/rack-grid-location.ts +286 -0
  60. package/src/rack-grid.ts +994 -0
  61. package/src/stock.ts +426 -0
  62. package/src/{rack-cell-3d.ts → storage-cell-3d.ts} +2 -2
  63. package/src/{rack-cell.ts → storage-cell.ts} +19 -13
  64. package/src/{asrs-rack-3d.ts → storage-rack-3d.ts} +3 -3
  65. package/src/{asrs-rack.ts → storage-rack.ts} +31 -25
  66. package/src/templates/index.ts +59 -17
  67. package/test/test-rack-grid-crane.ts +212 -0
  68. package/test/test-rack-grid.ts +77 -0
  69. package/test/{test-asrs-crane.ts → test-storage-rack-crane.ts} +8 -8
  70. package/translations/en.json +55 -7
  71. package/translations/ja.json +52 -4
  72. package/translations/ko.json +52 -4
  73. package/translations/ms.json +55 -7
  74. package/translations/zh.json +52 -4
  75. package/tsconfig.tsbuildinfo +1 -1
  76. package/dist/asrs-crane-3d.d.ts +0 -17
  77. package/dist/asrs-crane-3d.js +0 -181
  78. package/dist/asrs-crane-3d.js.map +0 -1
  79. package/dist/asrs-crane.d.ts +0 -98
  80. package/dist/asrs-crane.js +0 -216
  81. package/dist/asrs-crane.js.map +0 -1
  82. package/dist/asrs-rack-3d.js.map +0 -1
  83. package/dist/asrs-rack.js.map +0 -1
  84. package/dist/rack-cell-3d.js.map +0 -1
  85. package/dist/rack-cell.js.map +0 -1
  86. package/src/asrs-crane-3d.ts +0 -211
  87. package/src/asrs-crane.ts +0 -275
  88. /package/icons/{asrs-crane.png → crane.png} +0 -0
  89. /package/icons/{asrs-rack.png → storage-rack.png} +0 -0
@@ -14,10 +14,10 @@ import {
14
14
  type PlacementArchetype
15
15
  } from '@operato/scene-base'
16
16
 
17
- import { AsrsRack3D } from './asrs-rack-3d.js'
17
+ import { StorageRack3D } from './storage-rack-3d.js'
18
18
 
19
- /** AsrsRack 컴포넌트 state */
20
- export interface AsrsRackState extends State {
19
+ /** Rack 컴포넌트 state */
20
+ export interface StorageRackState extends State {
21
21
  // ── 토폴로지 ──
22
22
  bays?: number
23
23
  levels?: number
@@ -47,12 +47,12 @@ const NATURE: ComponentNature = {
47
47
  placeholder: '# of horizontal bays (default 5)'
48
48
  }
49
49
  ],
50
- help: 'scene/component/asrs-rack'
50
+ help: 'scene/component/rack'
51
51
  }
52
52
 
53
53
  // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
54
54
  // which forces `isHTMLElement(): true` and trips the 3D pipeline's
55
- // addObject DOM-skip gate. ASRS rack lives only in the 3D scene graph.
55
+ // addObject DOM-skip gate. Rack lives only in the 3D scene graph.
56
56
  //
57
57
  // Mixin chain: CellContainer → CarrierHolder → Placeable → ContainerAbstract
58
58
  // CellContainer: cell topology (cellMap, cell(), findAvailableCell(), occupiedCellIds())
@@ -60,25 +60,31 @@ const NATURE: ComponentNature = {
60
60
  // Placeable: floor-archetype positioning
61
61
  // ContainerAbstract: child component management
62
62
  /**
63
- * AsrsRack — a multi-level high-bay storage rack, the structural backbone of
64
- * an AS/RS (Automated Storage / Retrieval System).
63
+ * Rack — a multi-level storage shelf system. A *Storage* whose role is to hold
64
+ * carriers in a (bay × level) grid of cells.
65
65
  *
66
66
  * `levels` × `bays` cells form a vertical grid. Each cell holds one logistics
67
- * package (Pallet / Box / Parcel). A pair of AsrsRacks separated by an aisle
68
- * (where an AsrsCrane runs) is the typical AS/RS configuration.
67
+ * package (Pallet / Box / Parcel). A picker (Crane / Forklift / robot arm)
68
+ * accesses individual cells via Phase G/H Pickable contract — the picker is
69
+ * Rack-agnostic, knowing only how to interact with a cell.
69
70
  *
70
- * **Monitoring mode** (default): pallets/boxes are direct children of the rack,
71
- * placed by the WCS data binding. No RackCell children are created.
71
+ * **Monitoring mode** (default): carriers are direct children of the rack,
72
+ * placed by external data binding. No RackCell children are created.
72
73
  *
73
74
  * **Simulation mode**: call `rack._buildCells()` after placing the rack on the
74
- * scene. This creates RackCell children at the correct 3D positions. The
75
- * AsrsCrane then navigates to individual RackCells for pick-and-place.
75
+ * scene. This creates RackCell children at the correct 3D positions. A picker
76
+ * (Crane / Forklift / ...) then navigates to individual RackCells for
77
+ * pick-and-place.
76
78
  *
77
79
  * **Placement**: `floor` archetype, full ceiling depth by default.
80
+ *
81
+ * **Mobility**: this Rack is stationary. A `MobileRack = Mover(Rack)` mixin
82
+ * extension can be added later for AGV-mounted or cart-mounted variants —
83
+ * the cell topology and pickable contract stay the same.
78
84
  */
79
- @sceneComponent('asrs-rack')
80
- export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(ContainerAbstract))) {
81
- declare state: AsrsRackState
85
+ @sceneComponent('storage-rack')
86
+ export default class Rack extends CellContainer(CarrierHolder(Placeable(ContainerAbstract))) {
87
+ declare state: StorageRackState
82
88
 
83
89
  static placement: PlacementArchetype = 'floor'
84
90
  static align: Alignment = 'bottom'
@@ -125,7 +131,7 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
125
131
  * Create RackCell child components for each cell in the CellMap.
126
132
  *
127
133
  * Called explicitly to enter simulation mode — monitoring-mode racks
128
- * never call this (pallets are direct children, no explicit cells).
134
+ * never call this (carriers are direct children, no explicit cells).
129
135
  *
130
136
  * Idempotent: removes existing rack-cell children first.
131
137
  */
@@ -133,22 +139,22 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
133
139
  // Remove existing rack-cell children
134
140
  const existing = (this.components as Component[] | undefined) ?? []
135
141
  for (const child of [...existing]) {
136
- if ((child as any).state?.type === 'rack-cell') {
142
+ if ((child as any).state?.type === 'storage-cell') {
137
143
  this.removeComponent(child)
138
144
  }
139
145
  }
140
146
 
141
147
  // Create a RackCell for each cell in the map
142
- const RackCellClass = (Component as any).register('rack-cell') as (new (...args: any[]) => Component) | undefined
148
+ const RackCellClass = (Component as any).register('storage-cell') as (new (...args: any[]) => Component) | undefined
143
149
  if (!RackCellClass) {
144
- console.warn('AsrsRack._buildCells: rack-cell type not registered. Import rack-cell.ts first.')
150
+ console.warn('Rack._buildCells: rack-cell type not registered. Import rack-cell.ts first.')
145
151
  return
146
152
  }
147
153
 
148
154
  const context = this._app
149
155
  for (const cell of this.cellMap.cells) {
150
156
  const model = {
151
- type: 'rack-cell',
157
+ type: 'storage-cell',
152
158
  cellId: cell.id,
153
159
  width: cell.size.width,
154
160
  height: cell.size.depth, // 2D height = 3D Z depth
@@ -170,7 +176,7 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
170
176
  * - Everything else (sensors, labels, etc. can be siblings of the rack, not children).
171
177
  */
172
178
  containable(component: Component): boolean {
173
- if ((component as any).state?.type === 'rack-cell') return true
179
+ if ((component as any).state?.type === 'storage-cell') return true
174
180
  const archetype = (component.constructor as any).placement
175
181
  if (archetype === 'operation') return true
176
182
  return component.isDescendible(this)
@@ -180,7 +186,7 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
180
186
 
181
187
  /**
182
188
  * Attach frame for carriers that are DIRECT children of the rack
183
- * (monitoring mode, where pallets go directly into the rack without
189
+ * (monitoring mode, where carriers go directly into the rack without
184
190
  * explicit RackCell components).
185
191
  *
186
192
  * In simulation mode, carriers become children of their RackCell,
@@ -198,7 +204,7 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
198
204
 
199
205
  /**
200
206
  * 2D — top-down rectangle showing the rack footprint, with subdivisions
201
- * suggesting the bay layout (lines parallel to the aisle).
207
+ * suggesting the bay layout.
202
208
  */
203
209
  render(ctx: CanvasRenderingContext2D) {
204
210
  const { width, height, left, top } = this.state
@@ -222,6 +228,6 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
222
228
  // ── 3D ───────────────────────────────────────────────────────────────────
223
229
 
224
230
  buildRealObject(): RealObject | undefined {
225
- return new AsrsRack3D(this)
231
+ return new StorageRack3D(this)
226
232
  }
227
233
  }
@@ -1,13 +1,20 @@
1
1
  /*
2
- * things-scene catalog templates for the storage domain — pallet/box/parcel
3
- * variants, ASRS rack/crane, ASRS aisle composite, and the virtual `spot` placement marker.
2
+ * things-scene catalog templates for the storage domain.
3
+ *
4
+ * Components:
5
+ * - pallet / box / parcel — carriers
6
+ * - rack — multi-level storage shelf (bays × levels)
7
+ * - rack-table — bulk layout (rows × columns × shelves) helper
8
+ * - crane — stacker picker (Mover + CarrierHolder + ContainerCapacity)
9
+ * - mobile-rack — Mover(Rack), an AGV-mounted / cart-mounted moving rack
10
+ * - spot — virtual placement marker
4
11
  */
5
12
  import spot from './spot.js'
6
13
  const pallet = new URL('../../icons/pallet.png', import.meta.url).href
7
14
  const box = new URL('../../icons/box.png', import.meta.url).href
8
15
  const parcel = new URL('../../icons/parcel.png', import.meta.url).href
9
- const asrsRack = new URL('../../icons/asrs-rack.png', import.meta.url).href
10
- const asrsCrane = new URL('../../icons/asrs-crane.png', import.meta.url).href
16
+ const rack = new URL('../../icons/storage-rack.png', import.meta.url).href
17
+ const crane = new URL('../../icons/crane.png', import.meta.url).href
11
18
 
12
19
  export default [
13
20
  {
@@ -80,12 +87,12 @@ export default [
80
87
  }
81
88
  },
82
89
  {
83
- type: 'asrs-rack',
84
- description: 'AS/RS storage rack (multi-level)',
90
+ type: 'storage-rack',
91
+ description: 'storage rack multi-level shelves',
85
92
  group: 'storage',
86
- icon: asrsRack,
93
+ icon: rack,
87
94
  model: {
88
- type: 'asrs-rack',
95
+ type: 'storage-rack',
89
96
  top: 300,
90
97
  left: 100,
91
98
  width: 800,
@@ -95,12 +102,32 @@ export default [
95
102
  }
96
103
  },
97
104
  {
98
- type: 'asrs-crane',
99
- description: 'AS/RS stacker crane',
105
+ type: 'rack-grid',
106
+ description: 'rack table — N×M rack layout + auto location IDs',
100
107
  group: 'storage',
101
- icon: asrsCrane,
108
+ icon: rack,
102
109
  model: {
103
- type: 'asrs-crane',
110
+ type: 'rack-grid',
111
+ top: 100,
112
+ left: 100,
113
+ width: 1200,
114
+ height: 600,
115
+ rows: 2,
116
+ columns: 4,
117
+ shelves: 4,
118
+ zone: 'A',
119
+ locPattern: '{z}{s}-{u}-{sh}',
120
+ sectionDigits: 2,
121
+ unitDigits: 2
122
+ }
123
+ },
124
+ {
125
+ type: 'crane',
126
+ description: 'stacker crane — picker actor',
127
+ group: 'storage',
128
+ icon: crane,
129
+ model: {
130
+ type: 'crane',
104
131
  top: 550,
105
132
  left: 400,
106
133
  width: 100,
@@ -109,11 +136,26 @@ export default [
109
136
  carriageHeight: 100
110
137
  }
111
138
  },
139
+ {
140
+ type: 'mobile-storage-rack',
141
+ description: 'mobile rack — Mover(Rack), AGV/cart-mounted',
142
+ group: 'storage',
143
+ icon: rack,
144
+ model: {
145
+ type: 'mobile-storage-rack',
146
+ top: 300,
147
+ left: 100,
148
+ width: 400,
149
+ height: 200,
150
+ levels: 3,
151
+ bays: 3
152
+ }
153
+ },
112
154
  {
113
155
  type: 'group',
114
- description: 'AS/RS aisle — 4-level rack pair + stacker crane',
156
+ description: 'storage aisle — rack pair + crane',
115
157
  group: 'storage',
116
- icon: asrsRack,
158
+ icon: rack,
117
159
  model: {
118
160
  type: 'group',
119
161
  top: 100,
@@ -122,7 +164,7 @@ export default [
122
164
  height: 220,
123
165
  components: [
124
166
  {
125
- type: 'asrs-rack',
167
+ type: 'storage-rack',
126
168
  top: 100,
127
169
  left: 100,
128
170
  width: 800,
@@ -131,7 +173,7 @@ export default [
131
173
  bays: 8
132
174
  },
133
175
  {
134
- type: 'asrs-crane',
176
+ type: 'crane',
135
177
  top: 140,
136
178
  left: 420,
137
179
  width: 60,
@@ -140,7 +182,7 @@ export default [
140
182
  carriageHeight: 0
141
183
  },
142
184
  {
143
- type: 'asrs-rack',
185
+ type: 'storage-rack',
144
186
  top: 320,
145
187
  left: 100,
146
188
  width: 800,
@@ -0,0 +1,212 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * RackTable + Crane integration scenario — uses fake/minimal components to
5
+ * verify the location-based addressing flow:
6
+ *
7
+ * 1. RackTable owns N RackCells, each tagged with a locationId
8
+ * 2. Caller looks up source / target cells by locationId
9
+ * 3. Crane.fetch(sourceCell) — pick from source
10
+ * 4. Crane.deposit(carrier, targetCell) — place to target
11
+ *
12
+ * The fake infra mirrors test-rack-crane.ts so the actor (Crane) is the same
13
+ * concrete mixin chain; only the Cell wiring is RackTable-flavored.
14
+ */
15
+
16
+ import 'should'
17
+ import { ContainerCapacity, TRANSFER_SLOT_KEY } from '@hatiolab/things-scene'
18
+ import Mover from '../../scene-base/src/mover.js'
19
+
20
+ // ── Shared fake infrastructure ─────────────────────────────────────────────────
21
+
22
+ class FakeBase {
23
+ _components: any[] = []
24
+ state: Record<string, any>
25
+ parent: any = null
26
+ root: any = null
27
+
28
+ constructor(state: Record<string, any> = {}) {
29
+ this.state = state
30
+ }
31
+
32
+ get components() {
33
+ return this._components
34
+ }
35
+
36
+ setState(keyOrObj: string | Record<string, any>, value?: any) {
37
+ if (typeof keyOrObj === 'object') {
38
+ Object.assign(this.state, keyOrObj)
39
+ } else {
40
+ this.state[keyOrObj as string] = value
41
+ }
42
+ }
43
+
44
+ getState(key: string) {
45
+ return this.state[key]
46
+ }
47
+
48
+ set(keyOrObj: string | Record<string, any>, value?: any) {
49
+ // mimic things-scene Component.set()
50
+ if (typeof keyOrObj === 'object') {
51
+ Object.assign(this.state, keyOrObj)
52
+ } else {
53
+ this.state[keyOrObj as string] = value
54
+ }
55
+ }
56
+
57
+ addComponent(child: any) {
58
+ if (child.parent && child.parent !== this) {
59
+ const idx = child.parent._components?.indexOf(child) ?? -1
60
+ if (idx >= 0) child.parent._components.splice(idx, 1)
61
+ child.parent = null
62
+ }
63
+ if (!this._components.includes(child)) {
64
+ this._components.push(child)
65
+ }
66
+ child.parent = this
67
+ }
68
+
69
+ removeComponent(child: any) {
70
+ const idx = this._components.indexOf(child)
71
+ if (idx >= 0) this._components.splice(idx, 1)
72
+ if (child.parent === this) child.parent = null
73
+ }
74
+
75
+ reparent(child: any, _options?: any) {
76
+ this.addComponent(child)
77
+ }
78
+
79
+ trigger(_name: string, ..._args: any[]) {}
80
+ }
81
+
82
+ function makeCarrier(zPos?: number) {
83
+ return {
84
+ parent: null as any,
85
+ state: { type: 'pallet', ...(zPos !== undefined ? { zPos } : {}) },
86
+ type: 'pallet',
87
+ [TRANSFER_SLOT_KEY]: undefined as any,
88
+ setState(_s: any) {},
89
+ set(_s: any) {}
90
+ }
91
+ }
92
+
93
+ class FakeRackCell extends ContainerCapacity(FakeBase as any) {
94
+ declare state: Record<string, any>
95
+
96
+ constructor(cellId: string, locationId: string) {
97
+ super({ type: 'storage-cell', cellId, locationId })
98
+ }
99
+
100
+ get slots() {
101
+ return [{ id: 'main', maxCount: 1 }]
102
+ }
103
+ }
104
+
105
+ class FakeCrane extends Mover(ContainerCapacity(FakeBase as any)) {
106
+ declare state: Record<string, any>
107
+
108
+ constructor() {
109
+ super({ status: 'idle' })
110
+ }
111
+
112
+ get slots() {
113
+ return [{ id: 'forks', maxCount: 1 }]
114
+ }
115
+
116
+ // Instant moveTo — bypasses things-scene's compile/path machinery which is
117
+ // not available in this Node-only test harness.
118
+ moveTo(_target: any, _options: any = {}): Promise<void> {
119
+ return Promise.resolve()
120
+ }
121
+
122
+ async engage(target: any, kind: 'pick' | 'place'): Promise<void> {
123
+ if (kind === 'pick') {
124
+ this.setState({ status: 'loading' })
125
+ const zPos = (target as any)?.state?.zPos
126
+ if (typeof zPos === 'number') this.setState({ carriageHeight: zPos })
127
+ } else {
128
+ this.setState({ status: 'unloading' })
129
+ }
130
+ }
131
+
132
+ fetch(carrier: any) {
133
+ return (this as any).pick(carrier)
134
+ }
135
+
136
+ deposit(carrier: any, cell: any) {
137
+ return (this as any).place(carrier, cell)
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Fake RackTable — owns N FakeRackCells and exposes `getCellByLocation` that
143
+ * mimics RackTable.getCellByLocation (children iteration → state.locationId).
144
+ */
145
+ class FakeRackTable extends FakeBase {
146
+ _cells = new Map<string, FakeRackCell>()
147
+
148
+ populate(cells: { cellId: string; locationId: string }[]) {
149
+ for (const { cellId, locationId } of cells) {
150
+ const cell = new FakeRackCell(cellId, locationId)
151
+ this.addComponent(cell)
152
+ this._cells.set(locationId, cell)
153
+ }
154
+ }
155
+
156
+ getCellByLocation(locationId: string): FakeRackCell | null {
157
+ return this._cells.get(locationId) ?? null
158
+ }
159
+ }
160
+
161
+ // ── Tests ──────────────────────────────────────────────────────────────────────
162
+
163
+ describe('RackTable + Crane: locationId 기반 fetch / deposit 시나리오', () => {
164
+ it('locationId 로 cell 찾고 carrier 이동', async () => {
165
+ const table = new FakeRackTable()
166
+ table.populate([
167
+ { cellId: '0-0-0', locationId: 'A01-01-1' },
168
+ { cellId: '0-0-1', locationId: 'A01-01-2' },
169
+ { cellId: '0-0-2', locationId: 'A01-01-3' }
170
+ ])
171
+
172
+ const carrier = makeCarrier(100)
173
+ const cellA = table.getCellByLocation('A01-01-1')
174
+ cellA!.addComponent(carrier as any)
175
+
176
+ const crane = new FakeCrane()
177
+ await crane.fetch(carrier as any)
178
+ crane.state.status.should.equal('loading')
179
+ carrier.parent.should.equal(crane)
180
+
181
+ const cellB = table.getCellByLocation('A01-01-3')!
182
+ await crane.deposit(carrier as any, cellB as any)
183
+ crane.state.status.should.equal('unloading')
184
+ carrier.parent.should.equal(cellB)
185
+ })
186
+
187
+ it('존재하지 않는 locationId → null', () => {
188
+ const table = new FakeRackTable()
189
+ table.populate([{ cellId: '0-0-0', locationId: 'A01-01-1' }])
190
+ ;(table.getCellByLocation('X99-99-9') === null).should.be.true()
191
+ })
192
+
193
+ it('동일 carrier 를 동일 RackTable 안에서 cell-to-cell 이동', async () => {
194
+ const table = new FakeRackTable()
195
+ table.populate([
196
+ { cellId: '0-0-0', locationId: 'A01-01-1' },
197
+ { cellId: '0-1-0', locationId: 'A01-02-1' },
198
+ { cellId: '0-2-0', locationId: 'A01-03-1' }
199
+ ])
200
+
201
+ const carrier = makeCarrier()
202
+ const start = table.getCellByLocation('A01-01-1')!
203
+ start.addComponent(carrier as any)
204
+
205
+ const crane = new FakeCrane()
206
+ await (crane as any).pickAndPlace(carrier, table.getCellByLocation('A01-02-1') as any)
207
+ carrier.parent.should.equal(table.getCellByLocation('A01-02-1'))
208
+
209
+ await (crane as any).pickAndPlace(carrier, table.getCellByLocation('A01-03-1') as any)
210
+ carrier.parent.should.equal(table.getCellByLocation('A01-03-1'))
211
+ })
212
+ })
@@ -0,0 +1,77 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * RackTable unit tests — pure helpers (parseShelfLabels, formatLocationId)
5
+ * + state-derived topology accessors (columnWidths, rowHeights).
6
+ *
7
+ * The full _buildRacks() + assignLocations() integration is exercised in
8
+ * test-rack-table-crane.ts where minimal fake Rack/RackCell mocks are
9
+ * wired through a Crane.
10
+ */
11
+
12
+ import 'should'
13
+
14
+ import { parseShelfLabels, formatLocationId, distribute } from '../src/rack-grid-helpers.js'
15
+
16
+ describe('RackTable: parseShelfLabels', () => {
17
+ it('빈 문자열 → 1-based index fallback', () => {
18
+ parseShelfLabels('', 4).should.deepEqual(['1', '2', '3', '4'])
19
+ })
20
+ it('undefined → 1-based index fallback', () => {
21
+ parseShelfLabels(undefined, 3).should.deepEqual(['1', '2', '3'])
22
+ })
23
+ it('완전 명시 라벨 그대로', () => {
24
+ parseShelfLabels('A,B,C', 3).should.deepEqual(['A', 'B', 'C'])
25
+ })
26
+ it('부분 empty entries 는 default 로 fallback', () => {
27
+ parseShelfLabels(',,,04', 4).should.deepEqual(['1', '2', '3', '04'])
28
+ })
29
+ it('input 보다 levels 가 많으면 나머지는 default', () => {
30
+ parseShelfLabels('A,B', 4).should.deepEqual(['A', 'B', '3', '4'])
31
+ })
32
+ it('input 이 levels 보다 많으면 잘림', () => {
33
+ parseShelfLabels('A,B,C,D,E', 3).should.deepEqual(['A', 'B', 'C'])
34
+ })
35
+ it('공백 trim', () => {
36
+ parseShelfLabels(' A , B , C ', 3).should.deepEqual(['A', 'B', 'C'])
37
+ })
38
+ })
39
+
40
+ describe('RackTable: formatLocationId', () => {
41
+ it('default 패턴 {z}{s}-{u}-{sh}', () => {
42
+ formatLocationId('A', '01', '02', '3', '{z}{s}-{u}-{sh}').should.equal('A01-02-3')
43
+ })
44
+ it('zone 빈 값', () => {
45
+ formatLocationId('', '01', '02', '1', '{z}{s}-{u}-{sh}').should.equal('01-02-1')
46
+ })
47
+ it('section/unit zero-padded 4자리', () => {
48
+ formatLocationId('Z', '0042', '0007', '3', '{z}S{s}U{u}-{sh}').should.equal('ZS0042U0007-3')
49
+ })
50
+ it('placeholder 없는 패턴 그대로', () => {
51
+ formatLocationId('A', '01', '02', '3', 'fixed').should.equal('fixed')
52
+ })
53
+ it('동일 placeholder 여러 번', () => {
54
+ formatLocationId('A', '01', '02', '3', '{z}{z}{s}{s}').should.equal('AA0101')
55
+ })
56
+ it('shelf 라벨 문자', () => {
57
+ formatLocationId('A', '01', '02', 'TOP', '{z}{s}-{u}-{sh}').should.equal('A01-02-TOP')
58
+ })
59
+ })
60
+
61
+ describe('RackTable: distribute (per-column / per-row sizing)', () => {
62
+ it('declared 없음 → 모두 균등', () => {
63
+ distribute(1000, 4).should.deepEqual([250, 250, 250, 250])
64
+ })
65
+ it('전부 declared → 그대로', () => {
66
+ distribute(1000, 3, [100, 200, 300]).should.deepEqual([100, 200, 300])
67
+ })
68
+ it('일부 declared → 나머지가 remaining 균등 분배', () => {
69
+ distribute(1000, 4, [200, undefined, 300, undefined]).should.deepEqual([200, 250, 300, 250])
70
+ })
71
+ it('declared 합이 total 초과 → 나머지는 0', () => {
72
+ distribute(500, 3, [400, 200, undefined]).should.deepEqual([400, 200, 0])
73
+ })
74
+ it('n=1 + declared 비어있음', () => {
75
+ distribute(800, 1).should.deepEqual([800])
76
+ })
77
+ })
@@ -1,14 +1,14 @@
1
1
  /*
2
2
  * Copyright © HatioLab Inc. All rights reserved.
3
3
  *
4
- * AsrsCrane integration tests — pick/place data-flow for the ASRS crane.
4
+ * Crane integration tests — pick/place data-flow for the crane.
5
5
  *
6
6
  * Uses fake/minimal containers (no DOM, no Three.js, no RAF) so they run in
7
7
  * Node with tsx.
8
8
  *
9
9
  * Test infrastructure mirrors test-transfer-scenarios.ts in scene-base:
10
10
  * - FakeBase: minimal child-tracking container with setState support
11
- * - FakeCrane: Mover(ContainerCapacity(FakeBase)) — instant moveTo, AsrsCrane
11
+ * - FakeCrane: Mover(ContainerCapacity(FakeBase)) — instant moveTo, Crane
12
12
  * engage() semantics (status + carriageHeight), slots = [{id:'forks', maxCount:1}]
13
13
  * - FakeRackCell: mimics RackCell.receive / dispatch / canReceive protocol
14
14
  */
@@ -81,7 +81,7 @@ function makeCarrier(zPos?: number) {
81
81
  }
82
82
 
83
83
  // FakeCrane: Mover(ContainerCapacity(FakeBase)) with instant moveTo +
84
- // AsrsCrane engage() semantics (status + carriageHeight snap from carrier.state.zPos).
84
+ // Crane engage() semantics (status + carriageHeight snap from carrier.state.zPos).
85
85
  const FakeCraneBase = Mover(ContainerCapacity(FakeBase as any))
86
86
 
87
87
  class FakeCrane extends (FakeCraneBase as any) {
@@ -107,12 +107,12 @@ class FakeCrane extends (FakeCraneBase as any) {
107
107
  }
108
108
  }
109
109
 
110
- /** Semantic alias for pick (matches AsrsCrane.fetch). */
110
+ /** Semantic alias for pick (matches Crane.fetch). */
111
111
  fetch(carrier: any, options?: any): Promise<void> {
112
112
  return (this as any).pick(carrier, options)
113
113
  }
114
114
 
115
- /** Semantic alias for place (matches AsrsCrane.deposit). */
115
+ /** Semantic alias for place (matches Crane.deposit). */
116
116
  deposit(carrier: any, cell: any, options?: any): Promise<void> {
117
117
  return (this as any).place(carrier, cell, options)
118
118
  }
@@ -150,7 +150,7 @@ class FakeRackCell extends FakeBase {
150
150
 
151
151
  // ── Scenario 1: fetch (pick) ──────────────────────────────────────────────────
152
152
 
153
- describe('AsrsCrane: fetch (pick)', () => {
153
+ describe('Crane: fetch (pick)', () => {
154
154
  it('fetch 후 carrier가 crane의 child', async () => {
155
155
  const crane = new FakeCrane()
156
156
  const carrier = makeCarrier()
@@ -220,7 +220,7 @@ describe('AsrsCrane: fetch (pick)', () => {
220
220
 
221
221
  // ── Scenario 2: deposit (place) ───────────────────────────────────────────────
222
222
 
223
- describe('AsrsCrane: deposit (place)', () => {
223
+ describe('Crane: deposit (place)', () => {
224
224
  it('deposit 후 carrier가 rackCell의 child', async () => {
225
225
  const crane = new FakeCrane()
226
226
  const cell = new FakeRackCell({ cellId: 'cell-1-0-2' })
@@ -264,7 +264,7 @@ describe('AsrsCrane: deposit (place)', () => {
264
264
 
265
265
  // ── Scenario 3: fetch → deposit 전체 사이클 ──────────────────────────────────
266
266
 
267
- describe('AsrsCrane: fetch → deposit 전체 사이클', () => {
267
+ describe('Crane: fetch → deposit 전체 사이클', () => {
268
268
  it('cell-A → crane → cell-B', async () => {
269
269
  const crane = new FakeCrane()
270
270
  const cellA = new FakeRackCell({ cellId: 'cell-0-0-0' })