@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
package/src/stock.ts ADDED
@@ -0,0 +1,426 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+
5
+ import { Component, Model, RealObject } from '@hatiolab/things-scene'
6
+ import * as THREE from 'three'
7
+
8
+ const STOCK_COLOR = '#ccaa76'
9
+ const _color = new THREE.Color()
10
+ const _matrix = new THREE.Matrix4()
11
+ const _position = new THREE.Vector3()
12
+ const _quaternion = new THREE.Quaternion()
13
+ const _scale = new THREE.Vector3()
14
+
15
+ function createStockMaterial(color: string | number, opts?: { opacity?: number; transparent?: boolean }): THREE.MeshStandardMaterial {
16
+ const mat = new THREE.MeshStandardMaterial({
17
+ color,
18
+ side: THREE.FrontSide,
19
+ metalness: 0,
20
+ roughness: 1,
21
+ envMapIntensity: 0
22
+ })
23
+ if (opts?.opacity !== undefined) {
24
+ mat.opacity = opts.opacity
25
+ mat.transparent = opts.transparent ?? true
26
+ }
27
+ return mat
28
+ }
29
+
30
+ /**
31
+ * Stock material/legend 정보를 제공하는 컴포넌트 인터페이스.
32
+ * Visualizer와 RackTable 모두 이를 구현한다.
33
+ */
34
+ export interface StockMaterialProvider {
35
+ _stock_materials: THREE.Material[]
36
+ _default_material?: THREE.Material
37
+ _empty_material?: THREE.Material
38
+ legendTarget?: Component
39
+ hideEmptyStock?: boolean
40
+ }
41
+
42
+ export class Stock extends RealObject<THREE.Mesh> {
43
+ static defaultMaterial = createStockMaterial(STOCK_COLOR) as THREE.MeshStandardMaterial
44
+ static defaultEmptyMaterial = createStockMaterial(STOCK_COLOR, { opacity: 0.33, transparent: true })
45
+ static stockGeometry = new THREE.BoxGeometry(1, 1, 1)
46
+
47
+ _hideEmptyStock: boolean = false
48
+ _focused: boolean = false
49
+ declare _focusedAt?: number
50
+
51
+ // InstancedMesh 모드
52
+ _instancedMesh?: THREE.InstancedMesh
53
+ _instanceIndex: number = -1
54
+ _basePosition?: { x: number; y: number; z: number }
55
+ _baseScale?: { x: number; y: number; z: number }
56
+
57
+ model: any
58
+ _data?: any
59
+
60
+ constructor(component: Component, model: any) {
61
+ super(component)
62
+ this.model = model
63
+ }
64
+
65
+ get isInstanced(): boolean {
66
+ return !!this._instancedMesh && this._instanceIndex >= 0
67
+ }
68
+
69
+ protected getObject3dInstance() {
70
+ return new THREE.Mesh()
71
+ }
72
+
73
+ /**
74
+ * 가장 가까운 StockMaterialProvider 조상을 찾는다 (RackTable 또는 Visualizer).
75
+ */
76
+ get provider(): StockMaterialProvider | undefined {
77
+ let component = this.component
78
+
79
+ while (component) {
80
+ if ('_stock_materials' in component) {
81
+ return component as unknown as StockMaterialProvider
82
+ }
83
+ component = component.parent
84
+ }
85
+ }
86
+
87
+ private get _legendStatus(): any | undefined {
88
+ return this.provider?.legendTarget?.getState('status') || undefined
89
+ }
90
+
91
+ /**
92
+ * InstancedMesh의 해당 인스턴스 색상을 설정한다.
93
+ */
94
+ private _setInstanceColor(color: string | number) {
95
+ if (!this._instancedMesh) return
96
+ _color.set(color)
97
+ this._instancedMesh.setColorAt(this._instanceIndex, _color)
98
+ this._instancedMesh.instanceColor!.needsUpdate = true
99
+ }
100
+
101
+ /**
102
+ * 색상을 결정하고 InstancedMesh에 반영한다.
103
+ */
104
+ private _applyMaterialColor(index: number) {
105
+ const status = this._legendStatus
106
+ if (!status) {
107
+ this._setInstanceColor(this._getDefaultColor())
108
+ return
109
+ }
110
+
111
+ const range = status.ranges?.[index]
112
+ if (!(range && range.color)) {
113
+ this._setInstanceColor(this._getDefaultColor())
114
+ return
115
+ }
116
+
117
+ this._setInstanceColor(range.color)
118
+ }
119
+
120
+ private _getDefaultColor(): string | number {
121
+ const status = this._legendStatus
122
+ return status?.defaultColor || STOCK_COLOR
123
+ }
124
+
125
+ // 하위 호환: 개별 mesh 모드 material 접근
126
+ getMaterial(index: number) {
127
+ const status = this._legendStatus
128
+ if (!status) return this.userDefineDefaultMaterial
129
+
130
+ const range = status.ranges?.[index]
131
+ if (!(range && range.color)) {
132
+ this.stockMaterials[index] = this.userDefineDefaultMaterial
133
+ }
134
+
135
+ if (!this.stockMaterials[index]) {
136
+ this.stockMaterials[index] = createStockMaterial(range.color)
137
+ }
138
+
139
+ var alpha = range.color.replace(/^.*,(.+)\)/, '$1')
140
+ if (alpha > 0 && alpha < 1) {
141
+ this.stockMaterials[index].opacity = alpha
142
+ this.stockMaterials[index].transparent = true
143
+ }
144
+
145
+ return this.stockMaterials[index]
146
+ }
147
+
148
+ get stockMaterials(): THREE.Material[] {
149
+ return this.provider?._stock_materials ?? []
150
+ }
151
+
152
+ get userDefineDefaultMaterial(): THREE.Material {
153
+ const p = this.provider
154
+
155
+ if (p) {
156
+ if (!p._default_material) {
157
+ const status = this._legendStatus
158
+ const defaultColor = status?.defaultColor
159
+ if (!defaultColor) return Stock.defaultMaterial
160
+
161
+ var alpha = Number(defaultColor.replace(/^.*,(.+)\)/, '$1'))
162
+ var hasAlpha = alpha > 0 && alpha < 1
163
+ p._default_material = createStockMaterial(defaultColor, hasAlpha ? { opacity: alpha, transparent: true } : undefined)
164
+ }
165
+ return p._default_material
166
+ }
167
+
168
+ return Stock.defaultMaterial
169
+ }
170
+
171
+ get emptyMaterial(): THREE.Material {
172
+ const p = this.provider
173
+
174
+ if (p) {
175
+ if (!p._empty_material) {
176
+ const status = this._legendStatus
177
+ const defaultColor = status?.defaultColor || STOCK_COLOR
178
+
179
+ var alpha = Number(defaultColor.replace(/^.*,(.+)\)/, '$1'))
180
+ var opacity = (alpha > 0 && alpha < 1) ? alpha : 0.33
181
+ p._empty_material = createStockMaterial(defaultColor, { opacity, transparent: true })
182
+ }
183
+ return p._empty_material
184
+ }
185
+
186
+ return Stock.defaultEmptyMaterial
187
+ }
188
+
189
+ build() {
190
+ super.build()
191
+
192
+ // InstancedMesh 모드에서는 개별 mesh를 생성하지 않음
193
+ if (this.isInstanced) return
194
+
195
+ var { width, height, depth } = this.model
196
+
197
+ this._hideEmptyStock = !!this.provider?.hideEmptyStock
198
+
199
+ this.createStock(width, height, depth)
200
+ }
201
+
202
+ createStock(w: number, h: number, d: number) {
203
+ this.object3d.geometry = Stock.stockGeometry
204
+ this.object3d.material = this._hideEmptyStock ? this.emptyMaterial : this.userDefineDefaultMaterial
205
+
206
+ this.object3d.scale.set(w, d, h)
207
+
208
+ this.object3d.receiveShadow = true
209
+
210
+ this.object3d.castShadow = true
211
+ this.object3d.onBeforeRender = () => {
212
+ this.onBeforeRender()
213
+ }
214
+ }
215
+
216
+ onchangeStockData(data: any) {
217
+ this._data = data
218
+
219
+ if (this.isInstanced) {
220
+ this._onchangeStockDataInstanced(data)
221
+ return
222
+ }
223
+
224
+ // 개별 mesh 모드 (하위 호환)
225
+ this.object3d.userData = {
226
+ ...this.object3d.userData,
227
+ data
228
+ }
229
+
230
+ const stockStatus = this._legendStatus as
231
+ | { field: string; aggregation?: string; ranges: { min?: string | number; max?: string | number; color?: string }[] }
232
+ | undefined
233
+ if (!stockStatus) return
234
+
235
+ var statusField = stockStatus.field
236
+ var aggregation = stockStatus.aggregation || 'sum'
237
+ var ranges = stockStatus.ranges
238
+
239
+ if (!(statusField && ranges)) return
240
+
241
+ var items = data.items
242
+
243
+ if (!items) {
244
+ this.object3d.material = this._hideEmptyStock ? this.emptyMaterial : this.userDefineDefaultMaterial
245
+ return
246
+ }
247
+
248
+ var statusValues: any[] = items
249
+ .map((item: any) => item && item[statusField])
250
+ .filter((value: any) => value !== undefined && value !== null)
251
+
252
+ if (statusValues.length === 0) {
253
+ this.object3d.material = this._hideEmptyStock ? this.emptyMaterial : this.userDefineDefaultMaterial
254
+ return
255
+ }
256
+
257
+ var status = this._aggregate(statusValues, aggregation)
258
+
259
+ var isInRanges = ranges.some((range, index) => {
260
+ let { min: minValue, max: maxValue } = range
261
+
262
+ var min: number | undefined =
263
+ minValue !== undefined && minValue !== null && minValue !== '' ? Number(minValue) : undefined
264
+ var max: number | undefined =
265
+ maxValue !== undefined && maxValue !== null && maxValue !== '' ? Number(maxValue) : undefined
266
+
267
+ var maxCheck = max === undefined ? true : max > status
268
+ var minCheck = min === undefined ? true : min <= status
269
+
270
+ if (maxCheck && minCheck) {
271
+ this.object3d.material = this.getMaterial(index)
272
+ return true
273
+ } else {
274
+ this.object3d.material = this._hideEmptyStock ? this.emptyMaterial : this.userDefineDefaultMaterial
275
+ return false
276
+ }
277
+ })
278
+ if (!isInRanges) {
279
+ this.object3d.material = this.userDefineDefaultMaterial
280
+ }
281
+ }
282
+
283
+ /**
284
+ * InstancedMesh 모드에서의 데이터 변경 처리.
285
+ * 개별 mesh의 material 대신 instanceColor를 변경한다.
286
+ */
287
+ private _onchangeStockDataInstanced(data: any) {
288
+ const stockStatus = this._legendStatus as
289
+ | { field: string; aggregation?: string; ranges: { min?: string | number; max?: string | number; color?: string }[] }
290
+ | undefined
291
+ if (!stockStatus) return
292
+
293
+ var statusField = stockStatus.field
294
+ var aggregation = stockStatus.aggregation || 'sum'
295
+ var ranges = stockStatus.ranges
296
+
297
+ if (!(statusField && ranges)) return
298
+
299
+ var items = data.items
300
+
301
+ if (!items) {
302
+ this._setInstanceColor(this._getDefaultColor())
303
+ return
304
+ }
305
+
306
+ var statusValues: any[] = items
307
+ .map((item: any) => item && item[statusField])
308
+ .filter((value: any) => value !== undefined && value !== null)
309
+
310
+ if (statusValues.length === 0) {
311
+ this._setInstanceColor(this._getDefaultColor())
312
+ return
313
+ }
314
+
315
+ var status = this._aggregate(statusValues, aggregation)
316
+
317
+ var matched = false
318
+ for (let index = 0; index < ranges.length; index++) {
319
+ const range = ranges[index]
320
+ let { min: minValue, max: maxValue } = range
321
+
322
+ var min: number | undefined =
323
+ minValue !== undefined && minValue !== null && minValue !== '' ? Number(minValue) : undefined
324
+ var max: number | undefined =
325
+ maxValue !== undefined && maxValue !== null && maxValue !== '' ? Number(maxValue) : undefined
326
+
327
+ var maxCheck = max === undefined ? true : max > status
328
+ var minCheck = min === undefined ? true : min <= status
329
+
330
+ if (maxCheck && minCheck) {
331
+ this._applyMaterialColor(index)
332
+ matched = true
333
+ break
334
+ }
335
+ }
336
+
337
+ if (!matched) {
338
+ this._setInstanceColor(this._getDefaultColor())
339
+ }
340
+ }
341
+
342
+ private _aggregate(values: any[], aggregation: string): number {
343
+ switch (aggregation) {
344
+ case 'sum':
345
+ return values.reduce((sum: number, val: any) => sum + Number(val), 0)
346
+ case 'avg':
347
+ return values.reduce((sum: number, val: any) => sum + Number(val), 0) / values.length
348
+ case 'min':
349
+ return Math.min(...values.map((val: any) => Number(val)))
350
+ case 'max':
351
+ return Math.max(...values.map((val: any) => Number(val)))
352
+ default:
353
+ return values.reduce((sum: number, val: any) => sum + Number(val), 0)
354
+ }
355
+ }
356
+
357
+ onBeforeRender = () => {
358
+ if (!this.isInstanced) {
359
+ // 개별 mesh 모드
360
+ if (this._focused) {
361
+ var lastTime = performance.now() - this._focusedAt!
362
+ var progress = lastTime / 2000
363
+ this.object3d.rotation.y = 2 * Math.PI * progress
364
+ this.component.invalidate()
365
+ } else if (this._focusedAt) {
366
+ delete this._focusedAt
367
+ this.object3d.rotation.y = 0
368
+ this.component.invalidate()
369
+ }
370
+ return
371
+ }
372
+
373
+ // InstancedMesh 모드: 해당 인스턴스의 matrix만 갱신
374
+ if (this._focused && this._instancedMesh && this._basePosition && this._baseScale) {
375
+ var lastTime = performance.now() - this._focusedAt!
376
+ var progress = lastTime / 2000
377
+ _quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), 2 * Math.PI * progress)
378
+ _matrix.compose(
379
+ _position.set(this._basePosition.x, this._basePosition.y, this._basePosition.z),
380
+ _quaternion,
381
+ _scale.set(this._baseScale.x, this._baseScale.y, this._baseScale.z)
382
+ )
383
+ this._instancedMesh.setMatrixAt(this._instanceIndex, _matrix)
384
+ this._instancedMesh.instanceMatrix.needsUpdate = true
385
+ this.component.invalidate()
386
+ } else if (this._focusedAt && this._instancedMesh && this._basePosition && this._baseScale) {
387
+ delete this._focusedAt
388
+ _matrix.compose(
389
+ _position.set(this._basePosition.x, this._basePosition.y, this._basePosition.z),
390
+ _quaternion.identity(),
391
+ _scale.set(this._baseScale.x, this._baseScale.y, this._baseScale.z)
392
+ )
393
+ this._instancedMesh.setMatrixAt(this._instanceIndex, _matrix)
394
+ this._instancedMesh.instanceMatrix.needsUpdate = true
395
+ this.component.invalidate()
396
+ }
397
+ }
398
+
399
+ onmouseup(e: MouseEvent, _context: unknown, callback: (arg: { data: any; location: string }) => void) {
400
+ const data = this._data || this.object3d?.userData?.data
401
+
402
+ if (callback && typeof callback == 'function') {
403
+ callback({
404
+ ...data,
405
+ color: this._getCurrentColorHex()
406
+ })
407
+ }
408
+ }
409
+
410
+ private _getCurrentColorHex(): string {
411
+ if (this.isInstanced && this._instancedMesh) {
412
+ this._instancedMesh.getColorAt(this._instanceIndex, _color)
413
+ return '#' + _color.getHexString()
414
+ }
415
+ return '#' + ((this.object3d.material as any).color?.getHexString() || 'ccaa76')
416
+ }
417
+
418
+ // all update functions should be intentionally empty. important!
419
+ updateTransform() {}
420
+ updateDimension() {}
421
+ updateAlpha() {}
422
+ updateFillStyle() {}
423
+ updateStrokeStyle() {}
424
+ updateHidden() {}
425
+ updateText() {}
426
+ }
@@ -7,7 +7,7 @@
7
7
  * RackCell has no geometry of its own. Its sole 3D purpose is to provide
8
8
  * an Object3D that carriers can be attached to (via Three.js `.attach()`),
9
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 —
10
+ * from the parent Rack's CellMap (by cellId), not from 2D state fields —
11
11
  * rack cells occupy 3D levels that have no 2D analogue.
12
12
  *
13
13
  * updateTransform() override: things-scene's standard updateTransform
@@ -19,7 +19,7 @@
19
19
  import * as THREE from 'three'
20
20
  import { RealObjectGroup } from '@hatiolab/things-scene'
21
21
 
22
- export class RackCell3D extends RealObjectGroup {
22
+ export class StorageCell3D extends RealObjectGroup {
23
23
  build() {
24
24
  super.build()
25
25
  this._repositionFromCellMap()
@@ -1,13 +1,13 @@
1
1
  /*
2
2
  * Copyright © HatioLab Inc. All rights reserved.
3
3
  *
4
- * RackCell — a single storage slot within an AsrsRack.
4
+ * RackCell — a single storage slot within an Rack.
5
5
  *
6
6
  * A RackCell is a virtual component: it occupies a specific (bay, row, level)
7
7
  * coordinate within the parent rack and acts as a CarrierHolder for one carrier
8
8
  * (or several, depending on `cellType`).
9
9
  *
10
- * The crane (AsrsCrane) navigates toward a RackCell as its `place()` destination —
10
+ * The crane (or any picker) navigates toward a RackCell as its `place()` destination —
11
11
  * because the rack cell is a discrete component, Mover.moveTo() can target it
12
12
  * directly and arrive at exactly the right bay × level position.
13
13
  *
@@ -16,7 +16,7 @@
16
16
  * positioned within the rack's coordinate space (RackCell3D handles
17
17
  * this via updateTransform override).
18
18
  *
19
- * Lifecycle: AsrsRack._buildCells() instantiates RackCell children.
19
+ * Lifecycle: Rack._buildCells() instantiates RackCell children.
20
20
  * Do not create RackCell components manually — they are managed by the rack.
21
21
  *
22
22
  * Domain aliases:
@@ -35,7 +35,7 @@ import {
35
35
  import type { State, Material3D } from '@hatiolab/things-scene'
36
36
  import { CarrierHolder, type AttachFrame } from '@operato/scene-base'
37
37
 
38
- import { RackCell3D } from './rack-cell-3d.js'
38
+ import { StorageCell3D } from './storage-cell-3d.js'
39
39
 
40
40
  /**
41
41
  * How many carriers a cell can hold simultaneously.
@@ -43,13 +43,19 @@ import { RackCell3D } from './rack-cell-3d.js'
43
43
  * - multi: small stack (up to 4, e.g. a multi-deep tray)
44
44
  * - bulk: unlimited (e.g. a floor area measured in slots)
45
45
  */
46
- export type RackCellType = 'single' | 'multi' | 'bulk'
46
+ export type StorageCellType = 'single' | 'multi' | 'bulk'
47
47
 
48
48
  /** RackCell 컴포넌트 state */
49
- export interface RackCellState extends State {
49
+ export interface StorageCellState extends State {
50
50
  // ── 식별 ──
51
51
  cellId?: string
52
- cellType?: RackCellType
52
+ cellType?: StorageCellType
53
+ /**
54
+ * 자동 할당된 location ID — RackTable.assignLocations() 가 set. 외부 시스템
55
+ * (WMS, picker 명령 등) 이 cell 을 지칭하는 사람-친화 ID. RackTable 없이
56
+ * 단독으로 Rack 을 사용할 땐 unset.
57
+ */
58
+ locationId?: string
53
59
 
54
60
  // ── 3D 재질 ──
55
61
  material3d?: Material3D
@@ -79,11 +85,11 @@ const NATURE: ComponentNature = {
79
85
  }
80
86
  }
81
87
  ],
82
- help: 'scene/component/rack-cell'
88
+ help: 'scene/component/storage-cell'
83
89
  }
84
90
 
85
91
  /**
86
- * RackCell — single-slot storage cell inside an AsrsRack.
92
+ * RackCell — single-slot storage cell inside an Rack.
87
93
  *
88
94
  * Mixin chain: CarrierHolder(ContainerAbstract)
89
95
  * - CarrierHolder: publishes attachPointFor(), gates containable() to Carriables
@@ -93,9 +99,9 @@ const NATURE: ComponentNature = {
93
99
  * CellMap (via updateTransform override), bypassing things-scene's standard
94
100
  * 2D→3D coordinate mapping which cannot express 3D levels.
95
101
  */
96
- @sceneComponent('rack-cell')
102
+ @sceneComponent('storage-cell')
97
103
  export default class RackCell extends CarrierHolder(ContainerAbstract) {
98
- declare state: RackCellState
104
+ declare state: StorageCellState
99
105
 
100
106
  // ── Identification ────────────────────────────────────────────────────────
101
107
 
@@ -103,7 +109,7 @@ export default class RackCell extends CarrierHolder(ContainerAbstract) {
103
109
  return this.state.cellId ?? ''
104
110
  }
105
111
 
106
- get cellType(): RackCellType {
112
+ get cellType(): StorageCellType {
107
113
  return this.state.cellType ?? 'single'
108
114
  }
109
115
 
@@ -226,7 +232,7 @@ export default class RackCell extends CarrierHolder(ContainerAbstract) {
226
232
  // ── 3D ───────────────────────────────────────────────────────────────────
227
233
 
228
234
  buildRealObject(): RealObject | undefined {
229
- return new RackCell3D(this)
235
+ return new StorageCell3D(this)
230
236
  }
231
237
  }
232
238
 
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * Copyright © HatioLab Inc. All rights reserved.
3
3
  *
4
- * AsrsRack 3D — multi-level high-bay storage rack.
4
+ * Rack 3D — multi-level storage shelf system.
5
5
  *
6
6
  * LO-POLY but visually unambiguous as a rack. The signature parts:
7
7
  *
@@ -13,7 +13,7 @@
13
13
  * this is a load-bearing storage rack, not just a generic frame)
14
14
  *
15
15
  * No floor / ceiling panels — the rack is open by design (cells are accessed
16
- * by the stacker crane from the front/aisle side).
16
+ * by a picker from the front side).
17
17
  *
18
18
  * Cargo (pallets, boxes) added as children render at their own z position.
19
19
  * The rack itself is purely structural geometry.
@@ -27,7 +27,7 @@ const POST_COLOR = 0x6a7080
27
27
  const BEAM_COLOR = 0x556070
28
28
  const BRACE_COLOR = 0x556070
29
29
 
30
- export class AsrsRack3D extends RealObjectGroup {
30
+ export class StorageRack3D extends RealObjectGroup {
31
31
  build() {
32
32
  super.build()
33
33