@operato/scene-storage 10.0.0-beta.40 → 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.
Files changed (102) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/MIGRATION-plan-a-slot-api.md +266 -0
  3. package/PLAN-A-rack-as-slot-holder.md +164 -0
  4. package/dist/box.js +18 -0
  5. package/dist/box.js.map +1 -1
  6. package/dist/crane-3d.d.ts +47 -2
  7. package/dist/crane-3d.js +246 -89
  8. package/dist/crane-3d.js.map +1 -1
  9. package/dist/crane.d.ts +96 -12
  10. package/dist/crane.js +395 -100
  11. package/dist/crane.js.map +1 -1
  12. package/dist/index.d.ts +3 -4
  13. package/dist/index.js +1 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/pallet.d.ts +15 -0
  16. package/dist/pallet.js +38 -2
  17. package/dist/pallet.js.map +1 -1
  18. package/dist/parcel-3d.js +22 -18
  19. package/dist/parcel-3d.js.map +1 -1
  20. package/dist/parcel.d.ts +4 -3
  21. package/dist/parcel.js +24 -5
  22. package/dist/parcel.js.map +1 -1
  23. package/dist/rack-grid-3d.d.ts +18 -7
  24. package/dist/rack-grid-3d.js +372 -69
  25. package/dist/rack-grid-3d.js.map +1 -1
  26. package/dist/rack-grid-cell.d.ts +21 -72
  27. package/dist/rack-grid-cell.js +147 -243
  28. package/dist/rack-grid-cell.js.map +1 -1
  29. package/dist/rack-grid.d.ts +277 -56
  30. package/dist/rack-grid.js +1230 -695
  31. package/dist/rack-grid.js.map +1 -1
  32. package/dist/rack-materials.d.ts +9 -0
  33. package/dist/rack-materials.js +55 -0
  34. package/dist/rack-materials.js.map +1 -0
  35. package/dist/storage-rack-3d.d.ts +15 -0
  36. package/dist/storage-rack-3d.js +165 -29
  37. package/dist/storage-rack-3d.js.map +1 -1
  38. package/dist/storage-rack.d.ts +253 -32
  39. package/dist/storage-rack.js +726 -66
  40. package/dist/storage-rack.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/box.ts +18 -0
  43. package/src/crane-3d.ts +258 -93
  44. package/src/crane.ts +445 -110
  45. package/src/index.ts +3 -4
  46. package/src/pallet.ts +50 -1
  47. package/src/parcel-3d.ts +23 -18
  48. package/src/parcel.ts +24 -5
  49. package/src/rack-grid-3d.ts +383 -80
  50. package/src/rack-grid-cell.ts +161 -305
  51. package/src/rack-grid.ts +1263 -762
  52. package/src/rack-materials.ts +61 -0
  53. package/src/storage-rack-3d.ts +182 -29
  54. package/src/storage-rack.ts +819 -67
  55. package/test/test-carrier-lifecycle.ts +361 -0
  56. package/test/test-coord-alignment.ts +201 -0
  57. package/test/test-crane-geometry.ts +167 -0
  58. package/test/test-external-to-rack.ts +461 -0
  59. package/test/test-mover-concurrent-bug.ts +304 -0
  60. package/test/test-mover-rollback.ts +290 -0
  61. package/test/test-phase-h-carrier-pickable.ts +4 -3
  62. package/test/test-r19-place-absorb.ts +174 -0
  63. package/test/test-rack-3d-attach-real.ts +301 -0
  64. package/test/test-rack-concurrent.ts +254 -0
  65. package/test/test-rack-edge-cases.ts +323 -0
  66. package/test/test-rack-grid-cell.ts +318 -0
  67. package/test/test-rack-grid-location.ts +657 -0
  68. package/test/test-real-3d-positioning.ts +158 -0
  69. package/test/test-slot-center-convention.ts +116 -0
  70. package/test/test-slot-target.ts +189 -0
  71. package/test/test-storage-rack-batched.ts +606 -0
  72. package/test/test-storage-rack-click.ts +329 -0
  73. package/test/test-storage-rack-slot-api.ts +357 -0
  74. package/test/test-toscene-convention.ts +162 -0
  75. package/test/test-user-scenario-sequential.ts +334 -0
  76. package/translations/en.json +7 -1
  77. package/translations/ja.json +7 -1
  78. package/translations/ko.json +7 -1
  79. package/translations/ms.json +7 -1
  80. package/translations/zh.json +7 -1
  81. package/tsconfig.tsbuildinfo +1 -1
  82. package/dist/rack-column.d.ts +0 -35
  83. package/dist/rack-column.js +0 -258
  84. package/dist/rack-column.js.map +0 -1
  85. package/dist/rack-grid-helpers.d.ts +0 -28
  86. package/dist/rack-grid-helpers.js +0 -71
  87. package/dist/rack-grid-helpers.js.map +0 -1
  88. package/dist/rack-grid-location.d.ts +0 -37
  89. package/dist/rack-grid-location.js +0 -227
  90. package/dist/rack-grid-location.js.map +0 -1
  91. package/dist/storage-cell-3d.d.ts +0 -25
  92. package/dist/storage-cell-3d.js +0 -88
  93. package/dist/storage-cell-3d.js.map +0 -1
  94. package/dist/storage-cell.d.ts +0 -70
  95. package/dist/storage-cell.js +0 -197
  96. package/dist/storage-cell.js.map +0 -1
  97. package/src/rack-column.ts +0 -340
  98. package/src/rack-grid-helpers.ts +0 -77
  99. package/src/rack-grid-location.ts +0 -286
  100. package/src/storage-cell-3d.ts +0 -101
  101. package/src/storage-cell.ts +0 -247
  102. package/test/test-rack-grid.ts +0 -77
@@ -0,0 +1,61 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * Shared frame / shelf / stock materials — module-level singleton.
5
+ *
6
+ * StorageRack 과 RackGrid 의 frame mesh material 을 *공유* — 여러 rack 인스턴스가
7
+ * scene 에 있어도 GPU material 1개. 전체 application lifecycle 동안 살아있음
8
+ * (dispose 안 함).
9
+ *
10
+ * 색상 / 속성을 인스턴스별로 *override* 해야 한다면 인스턴스 내 별도 material 생성
11
+ * (현재는 모든 rack 이 같은 색 — singleton 으로 충분).
12
+ */
13
+ import * as THREE from 'three'
14
+
15
+ const POST_COLOR = 0x6a7080
16
+ const BEAM_COLOR = 0x556070
17
+ const BRACE_COLOR = 0x556070
18
+
19
+ export const POST_MATERIAL = new THREE.MeshStandardMaterial({
20
+ color: POST_COLOR,
21
+ metalness: 0.7,
22
+ roughness: 0.4
23
+ })
24
+
25
+ export const BEAM_MATERIAL = new THREE.MeshStandardMaterial({
26
+ color: BEAM_COLOR,
27
+ metalness: 0.7,
28
+ roughness: 0.4
29
+ })
30
+
31
+ export const BRACE_MATERIAL = new THREE.MeshStandardMaterial({
32
+ color: BRACE_COLOR,
33
+ metalness: 0.7,
34
+ roughness: 0.4
35
+ })
36
+
37
+ export const SHELF_MATERIAL = new THREE.MeshStandardMaterial({
38
+ color: BEAM_COLOR,
39
+ metalness: 0.3,
40
+ roughness: 0.6,
41
+ transparent: true,
42
+ opacity: 0.25,
43
+ side: THREE.DoubleSide
44
+ })
45
+
46
+ /** Stock — white base, instanceColor 가 최종 색상이 됨. */
47
+ export const STOCK_MATERIAL = new THREE.MeshStandardMaterial({
48
+ color: 0xffffff,
49
+ metalness: 0,
50
+ roughness: 0.9
51
+ })
52
+
53
+ /** Empty stock (record 없는 cell, hideEmptyStock=off 시) — 회색 반투명. */
54
+ export const EMPTY_STOCK_MATERIAL = new THREE.MeshStandardMaterial({
55
+ color: 0x888888,
56
+ metalness: 0,
57
+ roughness: 0.9,
58
+ transparent: true,
59
+ opacity: 0.2,
60
+ depthWrite: false
61
+ })
@@ -23,38 +23,61 @@ import * as THREE from 'three'
23
23
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
24
24
  import { RealObjectGroup } from '@hatiolab/things-scene'
25
25
 
26
- const POST_COLOR = 0x6a7080
27
- const BEAM_COLOR = 0x556070
28
- const BRACE_COLOR = 0x556070
26
+ import { POST_MATERIAL, BEAM_MATERIAL, BRACE_MATERIAL, SHELF_MATERIAL, STOCK_MATERIAL } from './rack-materials.js'
27
+
28
+ const BEAM_COLOR = 0x556070 // shelf material 의 color 와 일치 — 일부 코멘트 참조
29
+
30
+ // ── Stock visualization 공용 자원 ────────────────────────────────────────────
31
+ const STOCK_GEOMETRY_CACHE = new Map<string, THREE.BufferGeometry>()
32
+ const STOCK_DEFAULT_COLOR = '#c8a878' // cardboard 색 (legend 매칭 없을 때)
33
+
34
+ function getStockGeometry(w: number, h: number, d: number): THREE.BufferGeometry {
35
+ const k = `${w.toFixed(1)}-${h.toFixed(1)}-${d.toFixed(1)}`
36
+ let g = STOCK_GEOMETRY_CACHE.get(k)
37
+ if (!g) {
38
+ g = new THREE.BoxGeometry(w, d, h) // X=w, Y=d (vertical), Z=h
39
+ STOCK_GEOMETRY_CACHE.set(k, g)
40
+ }
41
+ return g
42
+ }
29
43
 
30
44
  export class StorageRack3D extends RealObjectGroup {
45
+ /** state.data 기반 stock 시각화 InstancedMesh. rebuildStockMesh 가 관리. */
46
+ private _stockMesh?: THREE.InstancedMesh
47
+ /** Horizontal beam 그룹 — hideHorizontalFrame 시 visibility 토글. */
48
+ private _beamGroup?: THREE.Group
49
+
50
+ /** Public read-only — click 핸들러가 instanceId/record 역참조에 사용. */
51
+ get stockMesh(): THREE.InstancedMesh | undefined {
52
+ return this._stockMesh
53
+ }
54
+
55
+ /** hideHorizontalFrame 변경 시 RackGrid 의 onchange 가 호출 — 즉시 반영. */
56
+ applyFrameVisibility(): void {
57
+ const hide = !!(this.component.state as any)?.hideHorizontalFrame
58
+ if (this._beamGroup) this._beamGroup.visible = !hide
59
+ }
60
+
31
61
  build() {
32
62
  super.build()
33
63
 
34
64
  const { width, height, depth = 3000 } = this.component.state
35
65
  const levels = Math.max(1, Math.floor((this.component.state.levels as number) || 4))
36
66
  const bays = Math.max(1, Math.floor((this.component.state.bays as number) || 5))
67
+ const shelfBase = Math.max(0, Math.min(
68
+ (this.component.state.shelfBaseHeight as number) || 0,
69
+ depth * 0.9
70
+ ))
71
+ const shelfZone = depth - shelfBase // 실제 shelf 가 차지하는 Y
37
72
 
38
- const baseY = -depth / 2
73
+ const baseY = -depth / 2 // rack 바닥 (3D Y 의 최저)
74
+ const shelfBaseY = baseY + shelfBase // 첫 shelf 의 시작 (= level 1 의 바닥)
39
75
  const postW = Math.min(width / bays, height) * 0.06
40
- const beamH = depth * 0.025
76
+ // beam 두께 = post 비슷 (산업 beam 이 post 보다 약간 두꺼움 — 1.2배)
77
+ const beamH = postW * 1.2
41
78
  const braceT = postW * 0.6
42
79
 
43
- const postMaterial = new THREE.MeshStandardMaterial({
44
- color: POST_COLOR,
45
- metalness: 0.7,
46
- roughness: 0.4
47
- })
48
- const beamMaterial = new THREE.MeshStandardMaterial({
49
- color: BEAM_COLOR,
50
- metalness: 0.7,
51
- roughness: 0.4
52
- })
53
- const braceMaterial = new THREE.MeshStandardMaterial({
54
- color: BRACE_COLOR,
55
- metalness: 0.7,
56
- roughness: 0.4
57
- })
80
+ // Material module-level singleton (rack-materials.ts) — 인스턴스 별 생성 X.
58
81
 
59
82
  // ── Uprights (vertical posts at every bay boundary) ──────────────
60
83
  // bays + 1 vertical positions; for each, one front post + one back post.
@@ -69,17 +92,17 @@ export class StorageRack3D extends RealObjectGroup {
69
92
  postGeos.push(post)
70
93
  }
71
94
  }
72
- const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), postMaterial)
95
+ const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), POST_MATERIAL)
73
96
  postMesh.castShadow = true
74
97
  postMesh.receiveShadow = true
75
98
  this.object3d.add(postMesh)
76
99
 
77
100
  // ── Horizontal beams (front + back faces at each level) ──────────
78
- // levels + 1 vertical positions (level 0 = ground, level N = top).
101
+ // shelf zone 안 levels+1 위치 (level 0 = shelfBase, level N = 천장).
79
102
  const beamGeos: THREE.BufferGeometry[] = []
80
103
  for (let lv = 0; lv <= levels; lv++) {
81
104
  const yFrac = lv / levels
82
- const y = baseY + yFrac * depth - beamH / 2 + (lv === 0 ? beamH : 0)
105
+ const y = shelfBaseY + yFrac * shelfZone - beamH / 2 + (lv === 0 ? beamH : 0)
83
106
 
84
107
  for (const zSign of [-1, 1]) {
85
108
  const beam = new THREE.BoxGeometry(width, beamH, beamH)
@@ -87,17 +110,21 @@ export class StorageRack3D extends RealObjectGroup {
87
110
  beamGeos.push(beam)
88
111
  }
89
112
  }
90
- const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), beamMaterial)
113
+ const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), BEAM_MATERIAL)
91
114
  beamMesh.castShadow = true
92
115
  beamMesh.receiveShadow = true
93
- this.object3d.add(beamMesh)
116
+ // hideHorizontalFrame 즉시 토글 위한 별도 group
117
+ this._beamGroup = new THREE.Group()
118
+ this._beamGroup.add(beamMesh)
119
+ this._beamGroup.visible = !((this.component.state as any)?.hideHorizontalFrame)
120
+ this.object3d.add(this._beamGroup)
94
121
 
95
122
  // ── Diagonal cross-bracing on the back face (the "X" pattern) ────
96
123
  // Two diagonals per level — "/" and "\" — making an X across each
97
124
  // bay-tall cell. Visual signature of a load-bearing rack.
98
125
  const braceGeos: THREE.BufferGeometry[] = []
99
126
  const cellW = width / bays
100
- const cellH = depth / levels
127
+ const cellH = shelfZone / levels // cell 높이 (shelf zone 안)
101
128
  const braceLen = Math.sqrt(cellW * cellW + cellH * cellH)
102
129
  const braceAngle = Math.atan2(cellH, cellW)
103
130
  const backZ = height / 2 - postW * 0.6
@@ -109,7 +136,7 @@ export class StorageRack3D extends RealObjectGroup {
109
136
  const cellCenterX = (bay - bays / 2 + 0.5) * cellW
110
137
 
111
138
  for (let lv = 0; lv < levels; lv++) {
112
- const cellCenterY = baseY + (lv + 0.5) * cellH
139
+ const cellCenterY = shelfBaseY + (lv + 0.5) * cellH
113
140
 
114
141
  for (const sign of [-1, 1]) {
115
142
  const brace = new THREE.BoxGeometry(braceLen, braceT, braceT)
@@ -120,10 +147,135 @@ export class StorageRack3D extends RealObjectGroup {
120
147
  }
121
148
  }
122
149
  if (braceGeos.length > 0) {
123
- const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), braceMaterial)
150
+ const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), BRACE_MATERIAL)
124
151
  braceMesh.castShadow = true
125
152
  this.object3d.add(braceMesh)
126
153
  }
154
+
155
+ // ── Shelf planes (level 별 반투명 무볼륨 판) ────────────────────────────
156
+ // 각 level 의 *바닥 면* 에 plane — cell 위치 시각 인식. carrier 가 그 위
157
+ // 에 놓이는 *지지면*. 반투명.
158
+ //
159
+ // X-Z 넓이를 *frame 안쪽* 으로 줄여 mesh 겹침 자체 제거 (Z-fight 회피).
160
+ // X: 양 옆 corner post 안쪽 (-postW 양쪽)
161
+ // Z: 앞/뒤 beam 안쪽 (-beamH 양쪽)
162
+ const shelfW = Math.max(0, width - 2 * postW)
163
+ const shelfD = Math.max(0, height - 2 * beamH)
164
+ const shelfGeo = new THREE.PlaneGeometry(shelfW, shelfD)
165
+ shelfGeo.rotateX(-Math.PI / 2) // X-Y plane → X-Z plane (= horizontal)
166
+ const shelfMaterial = SHELF_MATERIAL
167
+ for (let lv = 0; lv < levels; lv++) {
168
+ // shelf plane Y = 해당 level 의 *load beam top* 정확 일치 (cell 바닥 면).
169
+ // beam center Y = shelfBaseY + yFrac*shelfZone - beamH/2 + (lv===0 ? beamH : 0)
170
+ // beam top Y = beam center + beamH/2 = shelfBaseY + yFrac*shelfZone + (lv===0 ? beamH : 0)
171
+ const yFrac = lv / levels
172
+ const y = shelfBaseY + yFrac * shelfZone + (lv === 0 ? beamH : 0)
173
+ const shelf = new THREE.Mesh(shelfGeo, shelfMaterial)
174
+ shelf.position.set(0, y, 0)
175
+ shelf.receiveShadow = true
176
+ this.object3d.add(shelf)
177
+ }
178
+
179
+ // state.data 가 있으면 stock InstancedMesh 도 빌드
180
+ this.rebuildStockMesh()
181
+ }
182
+
183
+ /**
184
+ * state.data 의 각 record 를 한 InstancedMesh instance 로 렌더링.
185
+ * cellMap 에 있는 cellId 만 instance 부여 — 나머지는 무시.
186
+ * Mover/pickAndPlace 와 무관 — 순수 시각화.
187
+ */
188
+ rebuildStockMesh(): void {
189
+ if (this._stockMesh) {
190
+ this.object3d.remove(this._stockMesh)
191
+ this._stockMesh = undefined
192
+ }
193
+
194
+ const rack: any = this.component
195
+ const data = rack.state?.data as Array<{ cellId: string;[key: string]: any }> | undefined
196
+ if (!Array.isArray(data) || data.length === 0) return
197
+
198
+ const cellMap = rack.cellMap
199
+ if (!cellMap) return
200
+
201
+ // rack 의 기하 파라미터 — cellMap / _ensureCellAttachObject3d 의 levelHeight
202
+ // 계산과 *반드시* 일치해야 함 (shelfBaseHeight > 0 일 때 stock 시각 위치와
203
+ // crane fork target 이 어긋나는 회귀 차단).
204
+ const rs: any = this.component.state
205
+ const rackWidth = (rs?.width as number) || 1000
206
+ const rackDepth = (rs?.depth as number) || 3000
207
+ const rackHeight = (rs?.height as number) || 600
208
+ const bays = Math.max(1, Math.floor(rs?.bays || 5))
209
+ const levels = Math.max(1, Math.floor(rs?.levels || 4))
210
+ const shelfBase = Math.max(0, Math.min((rs?.shelfBaseHeight as number) || 0, rackDepth * 0.9))
211
+ const shelfZone = rackDepth - shelfBase
212
+ const bayWidth = rackWidth / bays
213
+ const levelHeight = shelfZone / levels // ← shelfZone 기준 (cellMap 과 동일)
214
+ const rowDepth = rackHeight
215
+
216
+ // 한 stock 의 크기 — cell 의 85% × 85% × 70%
217
+ const stockW = bayWidth * 0.85
218
+ const stockH = rowDepth * 0.85
219
+ const stockD = levelHeight * 0.7
220
+ const geo = getStockGeometry(stockW, stockH, stockD)
221
+
222
+ // 유효한 record 만 추출
223
+ const valid: { cell: any; record: any }[] = []
224
+ for (const r of data) {
225
+ if (!r?.cellId) continue
226
+ const cell = cellMap.findById(r.cellId)
227
+ if (cell) valid.push({ cell, record: r })
228
+ }
229
+ if (valid.length === 0) return
230
+
231
+ const inst = new THREE.InstancedMesh(geo, STOCK_MATERIAL, valid.length)
232
+ inst.castShadow = false
233
+ inst.receiveShadow = false
234
+ inst.frustumCulled = false
235
+ // things-scene EventManager3D 의 raycast hit-test 가 walk-up 하며 찾는 키:
236
+ // userData.context 에 RealObject 자체 set.
237
+ // 객체 전체 대체가 아니라 속성만 set 해 Three.js 의 기존 userData 필드 보존.
238
+ inst.userData.context = this
239
+ // 클릭 핸들러가 instanceId 로 record 를 역참조할 수 있도록 순서대로 저장.
240
+ inst.userData._records = valid.map(v => v.record)
241
+
242
+ const m = new THREE.Matrix4()
243
+ const pos = new THREE.Vector3()
244
+ const q = new THREE.Quaternion()
245
+ const s = new THREE.Vector3(1, 1, 1)
246
+ const c = new THREE.Color()
247
+
248
+ for (let i = 0; i < valid.length; i++) {
249
+ const { cell, record } = valid[i]
250
+ // cell 중심 (rack-center 좌표계)
251
+ const x = cell.localPosition.x + bayWidth / 2 - rackWidth / 2
252
+ const cellCenterY = cell.localPosition.y + levelHeight / 2 - rackDepth / 2
253
+ // stock 바닥 = cell 바닥 → stock 중심 Y = cell 중심 Y - levelHeight/2 + stockD/2
254
+ const y = cellCenterY - levelHeight / 2 + stockD / 2
255
+ const z = cell.localPosition.z + rowDepth / 2 - rackHeight / 2
256
+
257
+ pos.set(x, y, z)
258
+ m.compose(pos, q, s)
259
+ inst.setMatrixAt(i, m)
260
+
261
+ // Legend 색상 매핑 — rack 의 resolveLegendColor 호출, 매칭 없으면 default
262
+ const resolved = (rack as any).resolveLegendColor?.(record) ?? STOCK_DEFAULT_COLOR
263
+ c.set(resolved)
264
+ inst.setColorAt(i, c)
265
+ }
266
+ inst.instanceMatrix.needsUpdate = true
267
+ if (inst.instanceColor) inst.instanceColor.needsUpdate = true
268
+
269
+ // ── CRITICAL: bounding sphere 계산 ────────────────────────────────────────
270
+ // Three.js InstancedMesh.raycast 는 먼저 bounding sphere 로 broad-phase 체크.
271
+ // setMatrixAt 만 호출하고 computeBoundingSphere 를 안 부르면 sphere.radius=0
272
+ // → 어떤 ray 도 안 맞음 → click event 발화 안 됨. raycaster 가 instance 인식
273
+ // 못 하는 것의 *결정적 원인*. needsUpdate 후 명시 호출 필수.
274
+ inst.computeBoundingSphere()
275
+ inst.computeBoundingBox?.()
276
+
277
+ this.object3d.add(inst)
278
+ this._stockMesh = inst
127
279
  }
128
280
 
129
281
  updateDimension() {}
@@ -134,7 +286,8 @@ export class StorageRack3D extends RealObjectGroup {
134
286
  'bays' in after ||
135
287
  'width' in after ||
136
288
  'height' in after ||
137
- 'depth' in after
289
+ 'depth' in after ||
290
+ 'shelfBaseHeight' in after
138
291
  ) {
139
292
  this.update()
140
293
  return