@operato/scene-storage 10.0.0-beta.33 → 10.0.0-beta.35
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 +23 -0
- package/dist/crane-3d.d.ts +14 -0
- package/dist/crane-3d.js +238 -0
- package/dist/crane-3d.js.map +1 -0
- package/dist/crane.d.ts +163 -0
- package/dist/crane.js +459 -0
- package/dist/crane.js.map +1 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/mobile-storage-rack.d.ts +17 -0
- package/dist/mobile-storage-rack.js +55 -0
- package/dist/mobile-storage-rack.js.map +1 -0
- package/dist/rack-column.d.ts +35 -0
- package/dist/rack-column.js +258 -0
- package/dist/rack-column.js.map +1 -0
- package/dist/rack-grid-3d.d.ts +13 -0
- package/dist/rack-grid-3d.js +94 -0
- package/dist/rack-grid-3d.js.map +1 -0
- package/dist/rack-grid-cell.d.ts +341 -0
- package/dist/rack-grid-cell.js +321 -0
- package/dist/rack-grid-cell.js.map +1 -0
- package/dist/rack-grid-helpers.d.ts +28 -0
- package/dist/rack-grid-helpers.js +71 -0
- package/dist/rack-grid-helpers.js.map +1 -0
- package/dist/rack-grid-location.d.ts +37 -0
- package/dist/rack-grid-location.js +227 -0
- package/dist/rack-grid-location.js.map +1 -0
- package/dist/rack-grid.d.ts +80 -0
- package/dist/rack-grid.js +829 -0
- package/dist/rack-grid.js.map +1 -0
- package/dist/stock.d.ts +78 -0
- package/dist/stock.js +333 -0
- package/dist/stock.js.map +1 -0
- package/dist/{rack-cell-3d.d.ts → storage-cell-3d.d.ts} +1 -1
- package/dist/{rack-cell-3d.js → storage-cell-3d.js} +3 -3
- package/dist/storage-cell-3d.js.map +1 -0
- package/dist/{rack-cell.d.ts → storage-cell.d.ts} +12 -6
- package/dist/{rack-cell.js → storage-cell.js} +9 -9
- package/dist/storage-cell.js.map +1 -0
- package/dist/{asrs-rack-3d.d.ts → storage-rack-3d.d.ts} +1 -1
- package/dist/{asrs-rack-3d.js → storage-rack-3d.js} +4 -4
- package/dist/storage-rack-3d.js.map +1 -0
- package/dist/{asrs-rack.d.ts → storage-rack.d.ts} +22 -16
- package/dist/{asrs-rack.js → storage-rack.js} +32 -26
- package/dist/storage-rack.js.map +1 -0
- package/dist/templates/index.d.ts +60 -0
- package/dist/templates/index.js +59 -17
- package/dist/templates/index.js.map +1 -1
- package/package.json +2 -2
- package/src/crane-3d.ts +273 -0
- package/src/crane.ts +555 -0
- package/src/index.ts +13 -6
- package/src/mobile-storage-rack.ts +56 -0
- package/src/rack-column.ts +340 -0
- package/src/rack-grid-3d.ts +128 -0
- package/src/rack-grid-cell.ts +404 -0
- package/src/rack-grid-helpers.ts +77 -0
- package/src/rack-grid-location.ts +286 -0
- package/src/rack-grid.ts +994 -0
- package/src/stock.ts +426 -0
- package/src/{rack-cell-3d.ts → storage-cell-3d.ts} +2 -2
- package/src/{rack-cell.ts → storage-cell.ts} +19 -13
- package/src/{asrs-rack-3d.ts → storage-rack-3d.ts} +3 -3
- package/src/{asrs-rack.ts → storage-rack.ts} +31 -25
- package/src/templates/index.ts +59 -17
- package/test/test-rack-grid-crane.ts +212 -0
- package/test/test-rack-grid.ts +77 -0
- package/test/{test-asrs-crane.ts → test-storage-rack-crane.ts} +8 -8
- package/translations/en.json +55 -7
- package/translations/ja.json +52 -4
- package/translations/ko.json +52 -4
- package/translations/ms.json +55 -7
- package/translations/zh.json +52 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/asrs-crane-3d.d.ts +0 -17
- package/dist/asrs-crane-3d.js +0 -181
- package/dist/asrs-crane-3d.js.map +0 -1
- package/dist/asrs-crane.d.ts +0 -98
- package/dist/asrs-crane.js +0 -216
- package/dist/asrs-crane.js.map +0 -1
- package/dist/asrs-rack-3d.js.map +0 -1
- package/dist/asrs-rack.js.map +0 -1
- package/dist/rack-cell-3d.js.map +0 -1
- package/dist/rack-cell.js.map +0 -1
- package/src/asrs-crane-3d.ts +0 -211
- package/src/asrs-crane.ts +0 -275
- /package/icons/{asrs-crane.png → crane.png} +0 -0
- /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 {
|
|
17
|
+
import { StorageRack3D } from './storage-rack-3d.js'
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
export interface
|
|
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/
|
|
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.
|
|
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
|
-
*
|
|
64
|
-
*
|
|
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
|
|
68
|
-
*
|
|
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):
|
|
71
|
-
* placed by
|
|
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.
|
|
75
|
-
*
|
|
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('
|
|
80
|
-
export default class
|
|
81
|
-
declare state:
|
|
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 (
|
|
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 === '
|
|
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('
|
|
148
|
+
const RackCellClass = (Component as any).register('storage-cell') as (new (...args: any[]) => Component) | undefined
|
|
143
149
|
if (!RackCellClass) {
|
|
144
|
-
console.warn('
|
|
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: '
|
|
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 === '
|
|
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
|
|
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
|
|
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
|
|
231
|
+
return new StorageRack3D(this)
|
|
226
232
|
}
|
|
227
233
|
}
|
package/src/templates/index.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* things-scene catalog templates for the storage domain
|
|
3
|
-
*
|
|
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
|
|
10
|
-
const
|
|
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: '
|
|
84
|
-
description: '
|
|
90
|
+
type: 'storage-rack',
|
|
91
|
+
description: 'storage rack — multi-level shelves',
|
|
85
92
|
group: 'storage',
|
|
86
|
-
icon:
|
|
93
|
+
icon: rack,
|
|
87
94
|
model: {
|
|
88
|
-
type: '
|
|
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: '
|
|
99
|
-
description: '
|
|
105
|
+
type: 'rack-grid',
|
|
106
|
+
description: 'rack table — N×M rack layout + auto location IDs',
|
|
100
107
|
group: 'storage',
|
|
101
|
-
icon:
|
|
108
|
+
icon: rack,
|
|
102
109
|
model: {
|
|
103
|
-
type: '
|
|
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: '
|
|
156
|
+
description: 'storage aisle — rack pair + crane',
|
|
115
157
|
group: 'storage',
|
|
116
|
-
icon:
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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('
|
|
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('
|
|
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('
|
|
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' })
|