@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
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* AsrsCrane integration tests — pick/place data-flow for the ASRS crane.
|
|
5
|
+
*
|
|
6
|
+
* Uses fake/minimal containers (no DOM, no Three.js, no RAF) so they run in
|
|
7
|
+
* Node with tsx.
|
|
8
|
+
*
|
|
9
|
+
* Test infrastructure mirrors test-transfer-scenarios.ts in scene-base:
|
|
10
|
+
* - FakeBase: minimal child-tracking container with setState support
|
|
11
|
+
* - FakeCrane: Mover(ContainerCapacity(FakeBase)) — instant moveTo, AsrsCrane
|
|
12
|
+
* engage() semantics (status + carriageHeight), slots = [{id:'forks', maxCount:1}]
|
|
13
|
+
* - FakeRackCell: mimics RackCell.receive / dispatch / canReceive protocol
|
|
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
|
+
addComponent(child: any) {
|
|
49
|
+
if (child.parent && child.parent !== this) {
|
|
50
|
+
const idx = child.parent._components?.indexOf(child) ?? -1
|
|
51
|
+
if (idx >= 0) child.parent._components.splice(idx, 1)
|
|
52
|
+
child.parent = null
|
|
53
|
+
}
|
|
54
|
+
if (!this._components.includes(child)) {
|
|
55
|
+
this._components.push(child)
|
|
56
|
+
}
|
|
57
|
+
child.parent = this
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
removeComponent(child: any) {
|
|
61
|
+
const idx = this._components.indexOf(child)
|
|
62
|
+
if (idx >= 0) this._components.splice(idx, 1)
|
|
63
|
+
if (child.parent === this) child.parent = null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
reparent(child: any, _options?: any) {
|
|
67
|
+
this.addComponent(child)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
trigger(_name: string, ..._args: any[]) {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function makeCarrier(zPos?: number) {
|
|
74
|
+
return {
|
|
75
|
+
parent: null as any,
|
|
76
|
+
state: { type: 'pallet', ...(zPos !== undefined ? { zPos } : {}) },
|
|
77
|
+
type: 'pallet',
|
|
78
|
+
[TRANSFER_SLOT_KEY]: undefined as any,
|
|
79
|
+
setState(_s: any) {}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// FakeCrane: Mover(ContainerCapacity(FakeBase)) with instant moveTo +
|
|
84
|
+
// AsrsCrane engage() semantics (status + carriageHeight snap from carrier.state.zPos).
|
|
85
|
+
const FakeCraneBase = Mover(ContainerCapacity(FakeBase as any))
|
|
86
|
+
|
|
87
|
+
class FakeCrane extends (FakeCraneBase as any) {
|
|
88
|
+
constructor(state: Record<string, any> = {}) {
|
|
89
|
+
super({ status: 'idle', carriageHeight: 0, ...state })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get slots() {
|
|
93
|
+
return [{ id: 'forks', maxCount: 1 }]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
moveTo(_target: any, _options: any = {}): Promise<void> {
|
|
97
|
+
return Promise.resolve()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async engage(target: any, kind: 'pick' | 'place'): Promise<void> {
|
|
101
|
+
if (kind === 'pick') {
|
|
102
|
+
this.state.status = 'loading'
|
|
103
|
+
const carrierY = target?.state?.zPos ?? null
|
|
104
|
+
if (carrierY !== null) this.state.carriageHeight = carrierY
|
|
105
|
+
} else {
|
|
106
|
+
this.state.status = 'unloading'
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Semantic alias for pick (matches AsrsCrane.fetch). */
|
|
111
|
+
fetch(carrier: any, options?: any): Promise<void> {
|
|
112
|
+
return (this as any).pick(carrier, options)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Semantic alias for place (matches AsrsCrane.deposit). */
|
|
116
|
+
deposit(carrier: any, cell: any, options?: any): Promise<void> {
|
|
117
|
+
return (this as any).place(carrier, cell, options)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// FakeRackCell: mimics RackCell.receive / dispatch / canReceive protocol.
|
|
122
|
+
// capacity=1 (single-slot cell).
|
|
123
|
+
class FakeRackCell extends FakeBase {
|
|
124
|
+
cellId: string
|
|
125
|
+
|
|
126
|
+
constructor(state: Record<string, any> = {}) {
|
|
127
|
+
super(state)
|
|
128
|
+
this.cellId = (state.cellId as string) || 'cell-0-0-0'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
canReceive(_component?: any): boolean {
|
|
132
|
+
return this._components.length < 1
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async receive(carrier: any, options: any = {}): Promise<void> {
|
|
136
|
+
if (!this.canReceive(carrier)) return
|
|
137
|
+
carrier[TRANSFER_SLOT_KEY] = this.cellId
|
|
138
|
+
this.reparent(carrier, options)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async dispatch(carrier: any, target: any, options: any = {}): Promise<void> {
|
|
142
|
+
this.removeComponent(carrier)
|
|
143
|
+
if (typeof target?.receive === 'function') {
|
|
144
|
+
await target.receive(carrier, options)
|
|
145
|
+
} else {
|
|
146
|
+
target?.reparent?.(carrier, options)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Scenario 1: fetch (pick) ──────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
describe('AsrsCrane: fetch (pick)', () => {
|
|
154
|
+
it('fetch 후 carrier가 crane의 child', async () => {
|
|
155
|
+
const crane = new FakeCrane()
|
|
156
|
+
const carrier = makeCarrier()
|
|
157
|
+
|
|
158
|
+
await crane.fetch(carrier)
|
|
159
|
+
|
|
160
|
+
crane._components.should.containEql(carrier)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('fetch 후 TRANSFER_SLOT_KEY = forks', async () => {
|
|
164
|
+
const crane = new FakeCrane()
|
|
165
|
+
const carrier = makeCarrier()
|
|
166
|
+
|
|
167
|
+
await crane.fetch(carrier)
|
|
168
|
+
|
|
169
|
+
carrier[TRANSFER_SLOT_KEY].should.equal('forks')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('fetch engage → status loading', async () => {
|
|
173
|
+
const crane = new FakeCrane()
|
|
174
|
+
const carrier = makeCarrier()
|
|
175
|
+
|
|
176
|
+
await crane.fetch(carrier)
|
|
177
|
+
|
|
178
|
+
crane.state.status.should.equal('loading')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('carrier에 zPos 있으면 carriageHeight 스냅', async () => {
|
|
182
|
+
const crane = new FakeCrane()
|
|
183
|
+
const carrier = makeCarrier(1500)
|
|
184
|
+
|
|
185
|
+
await crane.fetch(carrier)
|
|
186
|
+
|
|
187
|
+
crane.state.carriageHeight.should.equal(1500)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('zPos 없으면 carriageHeight 변경 없음', async () => {
|
|
191
|
+
const crane = new FakeCrane({ carriageHeight: 500 })
|
|
192
|
+
const carrier = makeCarrier() // no zPos
|
|
193
|
+
|
|
194
|
+
await crane.fetch(carrier)
|
|
195
|
+
|
|
196
|
+
crane.state.carriageHeight.should.equal(500)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('fetch 전에 carrier가 rack cell에 있었으면 거기서 제거', async () => {
|
|
200
|
+
const crane = new FakeCrane()
|
|
201
|
+
const cell = new FakeRackCell({ cellId: 'cell-0-0-0' })
|
|
202
|
+
const carrier = makeCarrier()
|
|
203
|
+
await cell.receive(carrier)
|
|
204
|
+
|
|
205
|
+
await crane.fetch(carrier)
|
|
206
|
+
|
|
207
|
+
crane._components.should.containEql(carrier)
|
|
208
|
+
cell._components.should.not.containEql(carrier)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('maxCount(1) 가득 차면 canReceive false', async () => {
|
|
212
|
+
const crane = new FakeCrane()
|
|
213
|
+
const c1 = makeCarrier()
|
|
214
|
+
const c2 = makeCarrier()
|
|
215
|
+
|
|
216
|
+
await crane.fetch(c1)
|
|
217
|
+
;(crane as any).canReceive(c2).should.be.false()
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// ── Scenario 2: deposit (place) ───────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
describe('AsrsCrane: deposit (place)', () => {
|
|
224
|
+
it('deposit 후 carrier가 rackCell의 child', async () => {
|
|
225
|
+
const crane = new FakeCrane()
|
|
226
|
+
const cell = new FakeRackCell({ cellId: 'cell-1-0-2' })
|
|
227
|
+
const carrier = makeCarrier()
|
|
228
|
+
|
|
229
|
+
crane.addComponent(carrier)
|
|
230
|
+
carrier[TRANSFER_SLOT_KEY] = 'forks'
|
|
231
|
+
|
|
232
|
+
await crane.deposit(carrier, cell)
|
|
233
|
+
|
|
234
|
+
cell._components.should.containEql(carrier)
|
|
235
|
+
crane._components.should.not.containEql(carrier)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('deposit engage → status unloading', async () => {
|
|
239
|
+
const crane = new FakeCrane()
|
|
240
|
+
const cell = new FakeRackCell()
|
|
241
|
+
const carrier = makeCarrier()
|
|
242
|
+
|
|
243
|
+
crane.addComponent(carrier)
|
|
244
|
+
carrier[TRANSFER_SLOT_KEY] = 'forks'
|
|
245
|
+
|
|
246
|
+
await crane.deposit(carrier, cell)
|
|
247
|
+
|
|
248
|
+
crane.state.status.should.equal('unloading')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('deposit 후 TRANSFER_SLOT_KEY = cellId', async () => {
|
|
252
|
+
const crane = new FakeCrane()
|
|
253
|
+
const cell = new FakeRackCell({ cellId: 'cell-3-0-1' })
|
|
254
|
+
const carrier = makeCarrier()
|
|
255
|
+
|
|
256
|
+
crane.addComponent(carrier)
|
|
257
|
+
carrier[TRANSFER_SLOT_KEY] = 'forks'
|
|
258
|
+
|
|
259
|
+
await crane.deposit(carrier, cell)
|
|
260
|
+
|
|
261
|
+
carrier[TRANSFER_SLOT_KEY].should.equal('cell-3-0-1')
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// ── Scenario 3: fetch → deposit 전체 사이클 ──────────────────────────────────
|
|
266
|
+
|
|
267
|
+
describe('AsrsCrane: fetch → deposit 전체 사이클', () => {
|
|
268
|
+
it('cell-A → crane → cell-B', async () => {
|
|
269
|
+
const crane = new FakeCrane()
|
|
270
|
+
const cellA = new FakeRackCell({ cellId: 'cell-0-0-0' })
|
|
271
|
+
const cellB = new FakeRackCell({ cellId: 'cell-5-0-3' })
|
|
272
|
+
const carrier = makeCarrier()
|
|
273
|
+
|
|
274
|
+
// Pre-place carrier in cellA
|
|
275
|
+
await cellA.receive(carrier)
|
|
276
|
+
cellA._components.should.containEql(carrier)
|
|
277
|
+
carrier[TRANSFER_SLOT_KEY].should.equal('cell-0-0-0')
|
|
278
|
+
|
|
279
|
+
// Fetch from cellA
|
|
280
|
+
await crane.fetch(carrier)
|
|
281
|
+
crane._components.should.containEql(carrier)
|
|
282
|
+
cellA._components.should.not.containEql(carrier)
|
|
283
|
+
carrier[TRANSFER_SLOT_KEY].should.equal('forks')
|
|
284
|
+
|
|
285
|
+
// Deposit into cellB
|
|
286
|
+
await crane.deposit(carrier, cellB)
|
|
287
|
+
cellB._components.should.containEql(carrier)
|
|
288
|
+
crane._components.should.not.containEql(carrier)
|
|
289
|
+
carrier[TRANSFER_SLOT_KEY].should.equal('cell-5-0-3')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('fetch → deposit 후 status 순서: loading → unloading', async () => {
|
|
293
|
+
const crane = new FakeCrane()
|
|
294
|
+
const cell = new FakeRackCell({ cellId: 'cell-0-0-0' })
|
|
295
|
+
const carrier = makeCarrier()
|
|
296
|
+
|
|
297
|
+
await crane.fetch(carrier)
|
|
298
|
+
crane.state.status.should.equal('loading')
|
|
299
|
+
|
|
300
|
+
await crane.deposit(carrier, cell)
|
|
301
|
+
crane.state.status.should.equal('unloading')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('pickAndPlace: carrier를 cellA에서 cellB로 한 번에', async () => {
|
|
305
|
+
const crane = new FakeCrane()
|
|
306
|
+
const cellA = new FakeRackCell({ cellId: 'cell-2-0-1' })
|
|
307
|
+
const cellB = new FakeRackCell({ cellId: 'cell-7-0-3' })
|
|
308
|
+
const carrier = makeCarrier()
|
|
309
|
+
|
|
310
|
+
await cellA.receive(carrier)
|
|
311
|
+
|
|
312
|
+
await (crane as any).pickAndPlace(carrier, cellB)
|
|
313
|
+
|
|
314
|
+
cellB._components.should.containEql(carrier)
|
|
315
|
+
cellA._components.should.not.containEql(carrier)
|
|
316
|
+
crane._components.should.not.containEql(carrier)
|
|
317
|
+
carrier[TRANSFER_SLOT_KEY].should.equal('cell-7-0-3')
|
|
318
|
+
})
|
|
319
|
+
})
|