@operato/scene-storage 10.0.0-beta.41 → 10.0.0-beta.42
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 +12 -0
- package/MIGRATION-plan-a-slot-api.md +266 -0
- package/PLAN-A-rack-as-slot-holder.md +164 -0
- package/dist/crane.js +1 -1
- package/dist/crane.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/rack-grid-3d.d.ts +18 -7
- package/dist/rack-grid-3d.js +372 -69
- package/dist/rack-grid-3d.js.map +1 -1
- package/dist/rack-grid-cell.d.ts +21 -72
- package/dist/rack-grid-cell.js +147 -243
- package/dist/rack-grid-cell.js.map +1 -1
- package/dist/rack-grid.d.ts +277 -56
- package/dist/rack-grid.js +1230 -695
- package/dist/rack-grid.js.map +1 -1
- package/dist/rack-materials.d.ts +9 -0
- package/dist/rack-materials.js +55 -0
- package/dist/rack-materials.js.map +1 -0
- package/dist/storage-rack-3d.d.ts +15 -0
- package/dist/storage-rack-3d.js +131 -30
- package/dist/storage-rack-3d.js.map +1 -1
- package/dist/storage-rack.d.ts +242 -45
- package/dist/storage-rack.js +684 -106
- package/dist/storage-rack.js.map +1 -1
- package/package.json +3 -3
- package/src/crane.ts +1 -1
- package/src/index.ts +3 -4
- package/src/rack-grid-3d.ts +383 -80
- package/src/rack-grid-cell.ts +161 -305
- package/src/rack-grid.ts +1263 -762
- package/src/rack-materials.ts +61 -0
- package/src/storage-rack-3d.ts +144 -30
- package/src/storage-rack.ts +763 -111
- package/test/test-carrier-lifecycle.ts +361 -0
- package/test/test-coord-alignment.ts +201 -0
- package/test/test-external-to-rack.ts +461 -0
- package/test/test-mover-concurrent-bug.ts +304 -0
- package/test/test-mover-rollback.ts +290 -0
- package/test/test-r19-place-absorb.ts +174 -0
- package/test/test-rack-3d-attach-real.ts +301 -0
- package/test/test-rack-concurrent.ts +254 -0
- package/test/test-rack-edge-cases.ts +323 -0
- package/test/test-rack-grid-cell.ts +318 -0
- package/test/test-rack-grid-location.ts +657 -0
- package/test/test-real-3d-positioning.ts +158 -0
- package/test/test-slot-center-convention.ts +116 -0
- package/test/test-slot-target.ts +189 -0
- package/test/test-storage-rack-batched.ts +606 -0
- package/test/test-storage-rack-click.ts +329 -0
- package/test/test-storage-rack-slot-api.ts +357 -0
- package/test/test-toscene-convention.ts +162 -0
- package/test/test-user-scenario-sequential.ts +334 -0
- package/translations/en.json +2 -0
- package/translations/ja.json +2 -0
- package/translations/ko.json +2 -0
- package/translations/ms.json +2 -0
- package/translations/zh.json +2 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/rack-column.d.ts +0 -35
- package/dist/rack-column.js +0 -258
- package/dist/rack-column.js.map +0 -1
- package/dist/rack-grid-helpers.d.ts +0 -28
- package/dist/rack-grid-helpers.js +0 -71
- package/dist/rack-grid-helpers.js.map +0 -1
- package/dist/rack-grid-location.d.ts +0 -37
- package/dist/rack-grid-location.js +0 -227
- package/dist/rack-grid-location.js.map +0 -1
- package/dist/storage-cell-3d.d.ts +0 -25
- package/dist/storage-cell-3d.js +0 -88
- package/dist/storage-cell-3d.js.map +0 -1
- package/dist/storage-cell.d.ts +0 -73
- package/dist/storage-cell.js +0 -215
- package/dist/storage-cell.js.map +0 -1
- package/src/rack-column.ts +0 -340
- package/src/rack-grid-helpers.ts +0 -77
- package/src/rack-grid-location.ts +0 -286
- package/src/storage-cell-3d.ts +0 -101
- package/src/storage-cell.ts +0 -267
- package/test/test-cell-position.ts +0 -105
- package/test/test-rack-grid.ts +0 -77
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* RackGridCell + RackGrid hybrid integration tests.
|
|
3
|
+
*
|
|
4
|
+
* 검증 대상:
|
|
5
|
+
* - cell-component 가 있을 때 locationOf / cellIdOfLocation 이 *cell.state* lookup
|
|
6
|
+
* - cellOverrides fallback 도 여전히 동작 (cell-component 없는 경우)
|
|
7
|
+
* - setIsEmpty 가 cell-component 우선 mutate
|
|
8
|
+
* - increaseLocation 이 cell-component 우선 mutate + cellOverrides 비손상
|
|
9
|
+
* - cell.state.shelfLocations 가 RackGrid 의 shelfLocations 보다 우선
|
|
10
|
+
*
|
|
11
|
+
* 진짜 RackGrid/RackGridCell 인스턴스화는 things-scene 의존성으로 비현실적. 따라서
|
|
12
|
+
* MiniGridWithCells — RackGrid 의 알고리즘 + cell-component 들의 state 시뮬.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import 'should'
|
|
16
|
+
|
|
17
|
+
interface FakeCellState {
|
|
18
|
+
type: string
|
|
19
|
+
cellId?: string
|
|
20
|
+
section?: string
|
|
21
|
+
unit?: string
|
|
22
|
+
shelfLocations?: string
|
|
23
|
+
isEmpty?: boolean
|
|
24
|
+
border?: any
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class FakeCell {
|
|
28
|
+
state: FakeCellState
|
|
29
|
+
constructor(initial: FakeCellState) {
|
|
30
|
+
this.state = { type: 'rack-grid-cell', ...initial }
|
|
31
|
+
}
|
|
32
|
+
set(key: any, value?: any) {
|
|
33
|
+
if (typeof key === 'string') {
|
|
34
|
+
if (value === null) delete (this.state as any)[key]
|
|
35
|
+
else (this.state as any)[key] = value
|
|
36
|
+
} else {
|
|
37
|
+
Object.assign(this.state, key)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
getState(key: string) { return (this.state as any)[key] }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class MiniGridWithCells {
|
|
44
|
+
state: any
|
|
45
|
+
components: FakeCell[] = []
|
|
46
|
+
private _locationIndexCache?: Map<string, string>
|
|
47
|
+
|
|
48
|
+
constructor(initialState: any = {}, opts: { withCells?: boolean } = {}) {
|
|
49
|
+
this.state = {
|
|
50
|
+
columns: 4,
|
|
51
|
+
rows: 1,
|
|
52
|
+
shelves: 4,
|
|
53
|
+
sectionDigits: 2,
|
|
54
|
+
unitDigits: 2,
|
|
55
|
+
locPattern: '{z}{s}-{u}-{sh}',
|
|
56
|
+
zone: 'Z',
|
|
57
|
+
cellOverrides: {},
|
|
58
|
+
...initialState
|
|
59
|
+
}
|
|
60
|
+
if (opts.withCells !== false) this._buildCells()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setState(partial: any) {
|
|
64
|
+
this.state = { ...this.state, ...partial }
|
|
65
|
+
this._locationIndexCache = undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
invalidateLocationIndex() { this._locationIndexCache = undefined }
|
|
69
|
+
|
|
70
|
+
get columns(): number { return this.state.columns ?? 4 }
|
|
71
|
+
get rackRows(): number { return this.state.rows ?? 1 }
|
|
72
|
+
get shelves(): number { return this.state.shelves ?? 4 }
|
|
73
|
+
get cellOverrides() { return this.state.cellOverrides ?? {} }
|
|
74
|
+
|
|
75
|
+
private _buildCells() {
|
|
76
|
+
const cols = this.columns
|
|
77
|
+
const rows = this.rackRows
|
|
78
|
+
for (let r = 0; r < rows; r++) {
|
|
79
|
+
for (let c = 0; c < cols; c++) {
|
|
80
|
+
this.components.push(new FakeCell({ type: 'rack-grid-cell', cellId: `${c}-${r}` }))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cellAt(col: number, row: number = 0): FakeCell | null {
|
|
86
|
+
const idx = row * this.columns + col
|
|
87
|
+
return this.components[idx] ?? null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
cellAtBayKey(bayKey: string): FakeCell | null {
|
|
91
|
+
const parts = bayKey.split('-').map(Number)
|
|
92
|
+
if (parts.length !== 2) return null
|
|
93
|
+
return this.cellAt(parts[0], parts[1])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
parseCellId(cellId: string) {
|
|
97
|
+
const parts = cellId.split('-').map(Number)
|
|
98
|
+
if (parts.length !== 3 || parts.some(n => !Number.isFinite(n))) return null
|
|
99
|
+
return { col: parts[0], row: parts[1], shelf: parts[2] }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
parsePosKey(posKey: string) {
|
|
103
|
+
const parts = posKey.split('-').map(Number)
|
|
104
|
+
if (parts.length !== 2 || parts.some(n => !Number.isFinite(n))) return null
|
|
105
|
+
return { col: parts[0], row: parts[1] }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private _shelfLabelsFromCell(cellShelfLocations: string | undefined): string[] {
|
|
109
|
+
const input = cellShelfLocations ?? this.state.shelfLocations
|
|
110
|
+
const levels = this.shelves
|
|
111
|
+
const parts = (input || '').split(',')
|
|
112
|
+
const out: string[] = []
|
|
113
|
+
for (let i = 0; i < levels; i++) {
|
|
114
|
+
const p = parts[i]
|
|
115
|
+
out[i] = p && p.trim().length > 0 ? p.trim() : String(i + 1)
|
|
116
|
+
}
|
|
117
|
+
return out
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
locationOf(cellId: string): string | null {
|
|
121
|
+
const parsed = this.parseCellId(cellId)
|
|
122
|
+
if (!parsed) return null
|
|
123
|
+
const posKey = `${parsed.col}-${parsed.row}`
|
|
124
|
+
|
|
125
|
+
let section, unit, isEmpty, cellShelfLocations: string | undefined
|
|
126
|
+
const cell = this.cellAt(parsed.col, parsed.row)
|
|
127
|
+
if (cell) {
|
|
128
|
+
section = cell.state.section
|
|
129
|
+
unit = cell.state.unit
|
|
130
|
+
isEmpty = cell.state.isEmpty
|
|
131
|
+
cellShelfLocations = cell.state.shelfLocations
|
|
132
|
+
} else {
|
|
133
|
+
const o = this.cellOverrides[posKey]
|
|
134
|
+
section = o?.section; unit = o?.unit; isEmpty = o?.isEmpty
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!section || !unit || isEmpty) return null
|
|
138
|
+
const pattern = this.state.locPattern ?? '{z}{s}-{u}-{sh}'
|
|
139
|
+
const zone = this.state.zone ?? ''
|
|
140
|
+
const shelfLabel = this._shelfLabelsFromCell(cellShelfLocations)[parsed.shelf] ?? String(parsed.shelf + 1)
|
|
141
|
+
return pattern
|
|
142
|
+
.replace(/\{z\}/g, zone)
|
|
143
|
+
.replace(/\{s\}/g, section)
|
|
144
|
+
.replace(/\{u\}/g, unit)
|
|
145
|
+
.replace(/\{sh\}/g, shelfLabel)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
cellIdOfLocation(location: string): string | null {
|
|
149
|
+
return this._locationIndex.get(location) ?? null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private get _locationIndex(): Map<string, string> {
|
|
153
|
+
if (this._locationIndexCache) return this._locationIndexCache
|
|
154
|
+
const map = new Map<string, string>()
|
|
155
|
+
const shelves = this.shelves
|
|
156
|
+
|
|
157
|
+
// cells first
|
|
158
|
+
for (const cell of this.components) {
|
|
159
|
+
if (cell.state.type !== 'rack-grid-cell') continue
|
|
160
|
+
const bayKey = cell.state.cellId
|
|
161
|
+
if (!bayKey) continue
|
|
162
|
+
const parsed = this.parsePosKey(bayKey)
|
|
163
|
+
if (!parsed) continue
|
|
164
|
+
for (let s = 0; s < shelves; s++) {
|
|
165
|
+
const cellId = `${parsed.col}-${parsed.row}-${s}`
|
|
166
|
+
const loc = this.locationOf(cellId)
|
|
167
|
+
if (loc) map.set(loc, cellId)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// overrides fallback
|
|
171
|
+
for (const posKey of Object.keys(this.cellOverrides)) {
|
|
172
|
+
const o = this.cellOverrides[posKey]
|
|
173
|
+
if (!o.section || !o.unit || o.isEmpty) continue
|
|
174
|
+
const parsed = this.parsePosKey(posKey)
|
|
175
|
+
if (!parsed) continue
|
|
176
|
+
for (let s = 0; s < shelves; s++) {
|
|
177
|
+
const cellId = `${parsed.col}-${parsed.row}-${s}`
|
|
178
|
+
const loc = this.locationOf(cellId)
|
|
179
|
+
if (loc && !map.has(loc)) map.set(loc, cellId)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this._locationIndexCache = map
|
|
184
|
+
return map
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setIsEmpty(posKeys: string[], isEmpty: boolean): void {
|
|
188
|
+
const hasCells = this.components.some(c => c.state.type === 'rack-grid-cell')
|
|
189
|
+
if (hasCells) {
|
|
190
|
+
for (const k of posKeys) {
|
|
191
|
+
const cell = this.cellAtBayKey(k)
|
|
192
|
+
cell?.set('isEmpty', isEmpty)
|
|
193
|
+
}
|
|
194
|
+
this.invalidateLocationIndex()
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
// fallback
|
|
198
|
+
const current = this.cellOverrides
|
|
199
|
+
const next = { ...current }
|
|
200
|
+
for (const k of posKeys) {
|
|
201
|
+
const existing = next[k] ?? {}
|
|
202
|
+
const merged: any = { ...existing, isEmpty }
|
|
203
|
+
if (!isEmpty && !existing.section && !existing.unit) delete next[k]
|
|
204
|
+
else next[k] = merged
|
|
205
|
+
}
|
|
206
|
+
this.setState({ cellOverrides: next })
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── 1: cell-component 가 있을 때 locationOf cell.state 직접 lookup ──────────
|
|
211
|
+
|
|
212
|
+
describe('RackGridCell: cell.state 가 source of truth', () => {
|
|
213
|
+
it('cell.state.section/unit 명시 → location 부여', () => {
|
|
214
|
+
const g = new MiniGridWithCells()
|
|
215
|
+
g.cellAt(0, 0)!.set({ section: '01', unit: '01' })
|
|
216
|
+
g.locationOf('0-0-0').should.equal('Z01-01-1')
|
|
217
|
+
g.locationOf('0-0-3').should.equal('Z01-01-4')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('section 만 / unit 만 → null', () => {
|
|
221
|
+
const g = new MiniGridWithCells()
|
|
222
|
+
g.cellAt(0, 0)!.set({ section: '01' })
|
|
223
|
+
;(g.locationOf('0-0-0') === null).should.be.true()
|
|
224
|
+
g.cellAt(1, 0)!.set({ unit: '02' })
|
|
225
|
+
;(g.locationOf('1-0-0') === null).should.be.true()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('cell.state.isEmpty=true → location null', () => {
|
|
229
|
+
const g = new MiniGridWithCells()
|
|
230
|
+
g.cellAt(0, 0)!.set({ section: '01', unit: '01', isEmpty: true })
|
|
231
|
+
;(g.locationOf('0-0-0') === null).should.be.true()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('cell.state.shelfLocations 가 RackGrid.shelfLocations 보다 우선', () => {
|
|
235
|
+
const g = new MiniGridWithCells({ shelfLocations: 'A,B,C,D' })
|
|
236
|
+
g.cellAt(0, 0)!.set({ section: '01', unit: '01' })
|
|
237
|
+
g.locationOf('0-0-0').should.equal('Z01-01-A')
|
|
238
|
+
// cell-specific override
|
|
239
|
+
g.cellAt(0, 0)!.set('shelfLocations', 'X,Y,Z,W')
|
|
240
|
+
g.locationOf('0-0-0').should.equal('Z01-01-X')
|
|
241
|
+
g.locationOf('0-0-3').should.equal('Z01-01-W')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('cell.state 변경 → location 역인덱스 무효화 (invalidate 후 재구축)', () => {
|
|
245
|
+
const g = new MiniGridWithCells()
|
|
246
|
+
g.cellAt(0, 0)!.set({ section: '01', unit: '01' })
|
|
247
|
+
g.cellIdOfLocation('Z01-01-1').should.equal('0-0-0')
|
|
248
|
+
// cell 의 section 변경
|
|
249
|
+
g.cellAt(0, 0)!.set('section', '02')
|
|
250
|
+
g.invalidateLocationIndex() // cell.set 이 parent.invalidate 호출 시뮬
|
|
251
|
+
g.cellIdOfLocation('Z02-01-1').should.equal('0-0-0')
|
|
252
|
+
;(g.cellIdOfLocation('Z01-01-1') === null).should.be.true()
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// ── 2: setIsEmpty 가 cell-component 우선 mutate ─────────────────────────────
|
|
257
|
+
|
|
258
|
+
describe('RackGridCell: setIsEmpty 는 cell.set() 우선', () => {
|
|
259
|
+
it('cell-component 있으면 cell.state.isEmpty 갱신 (cellOverrides 무영향)', () => {
|
|
260
|
+
const g = new MiniGridWithCells()
|
|
261
|
+
g.setIsEmpty(['1-0', '2-0'], true)
|
|
262
|
+
g.cellAt(1, 0)!.state.isEmpty!.should.be.true()
|
|
263
|
+
g.cellAt(2, 0)!.state.isEmpty!.should.be.true()
|
|
264
|
+
Object.keys(g.cellOverrides).length.should.equal(0) // fallback 안 침범
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('cell-component 없으면 cellOverrides 갱신 (fallback)', () => {
|
|
268
|
+
const g = new MiniGridWithCells({}, { withCells: false })
|
|
269
|
+
g.setIsEmpty(['1-0'], true)
|
|
270
|
+
g.cellOverrides['1-0'].isEmpty!.should.be.true()
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// ── 3: cellOverrides fallback 도 여전히 동작 (cell-component 없을 때) ──────
|
|
275
|
+
|
|
276
|
+
describe('RackGridCell: cellOverrides fallback 호환', () => {
|
|
277
|
+
it('cell-component 없으면 cellOverrides 가 source of truth', () => {
|
|
278
|
+
const g = new MiniGridWithCells({
|
|
279
|
+
cellOverrides: { '0-0': { section: '01', unit: '01' } }
|
|
280
|
+
}, { withCells: false })
|
|
281
|
+
g.locationOf('0-0-0').should.equal('Z01-01-1')
|
|
282
|
+
g.cellIdOfLocation('Z01-01-1').should.equal('0-0-0')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('cell-component 있으면 cellOverrides 무시 (cell.state 가 single source of truth)', () => {
|
|
286
|
+
// cell-component 가 있으면 *그 cell.state 만* lookup. cellOverrides 의 동일 bayKey
|
|
287
|
+
// 데이터는 무시됨 → cell-component 의 데이터로 일관. (transitional load 시 별도
|
|
288
|
+
// migrate 로직이 cellOverrides → cell.state 옮김)
|
|
289
|
+
const g = new MiniGridWithCells({
|
|
290
|
+
cellOverrides: { '3-0': { section: '02', unit: '03' } }
|
|
291
|
+
})
|
|
292
|
+
;(g.locationOf('3-0-0') === null).should.be.true() // cell.state 비어있음 → null
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// ── 4: cell-component 의 cellId 동기화 (순서 기반 bayKey) ──────────────────
|
|
297
|
+
|
|
298
|
+
describe('RackGridCell: cellId 가 components 순서 기반', () => {
|
|
299
|
+
it('4×1 grid 에서 cellId 가 0-0, 1-0, 2-0, 3-0', () => {
|
|
300
|
+
const g = new MiniGridWithCells({ columns: 4, rows: 1 })
|
|
301
|
+
g.components[0].state.cellId!.should.equal('0-0')
|
|
302
|
+
g.components[1].state.cellId!.should.equal('1-0')
|
|
303
|
+
g.components[2].state.cellId!.should.equal('2-0')
|
|
304
|
+
g.components[3].state.cellId!.should.equal('3-0')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('3×2 grid 에서 row-major 순서', () => {
|
|
308
|
+
const g = new MiniGridWithCells({ columns: 3, rows: 2 })
|
|
309
|
+
// row 0: 0-0, 1-0, 2-0
|
|
310
|
+
g.components[0].state.cellId!.should.equal('0-0')
|
|
311
|
+
g.components[1].state.cellId!.should.equal('1-0')
|
|
312
|
+
g.components[2].state.cellId!.should.equal('2-0')
|
|
313
|
+
// row 1: 0-1, 1-1, 2-1
|
|
314
|
+
g.components[3].state.cellId!.should.equal('0-1')
|
|
315
|
+
g.components[4].state.cellId!.should.equal('1-1')
|
|
316
|
+
g.components[5].state.cellId!.should.equal('2-1')
|
|
317
|
+
})
|
|
318
|
+
})
|