@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +12 -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/crane.js +1 -1
  5. package/dist/crane.js.map +1 -1
  6. package/dist/index.d.ts +3 -4
  7. package/dist/index.js +1 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/rack-grid-3d.d.ts +18 -7
  10. package/dist/rack-grid-3d.js +372 -69
  11. package/dist/rack-grid-3d.js.map +1 -1
  12. package/dist/rack-grid-cell.d.ts +21 -72
  13. package/dist/rack-grid-cell.js +147 -243
  14. package/dist/rack-grid-cell.js.map +1 -1
  15. package/dist/rack-grid.d.ts +277 -56
  16. package/dist/rack-grid.js +1230 -695
  17. package/dist/rack-grid.js.map +1 -1
  18. package/dist/rack-materials.d.ts +9 -0
  19. package/dist/rack-materials.js +55 -0
  20. package/dist/rack-materials.js.map +1 -0
  21. package/dist/storage-rack-3d.d.ts +15 -0
  22. package/dist/storage-rack-3d.js +131 -30
  23. package/dist/storage-rack-3d.js.map +1 -1
  24. package/dist/storage-rack.d.ts +242 -45
  25. package/dist/storage-rack.js +684 -106
  26. package/dist/storage-rack.js.map +1 -1
  27. package/package.json +3 -3
  28. package/src/crane.ts +1 -1
  29. package/src/index.ts +3 -4
  30. package/src/rack-grid-3d.ts +383 -80
  31. package/src/rack-grid-cell.ts +161 -305
  32. package/src/rack-grid.ts +1263 -762
  33. package/src/rack-materials.ts +61 -0
  34. package/src/storage-rack-3d.ts +144 -30
  35. package/src/storage-rack.ts +763 -111
  36. package/test/test-carrier-lifecycle.ts +361 -0
  37. package/test/test-coord-alignment.ts +201 -0
  38. package/test/test-external-to-rack.ts +461 -0
  39. package/test/test-mover-concurrent-bug.ts +304 -0
  40. package/test/test-mover-rollback.ts +290 -0
  41. package/test/test-r19-place-absorb.ts +174 -0
  42. package/test/test-rack-3d-attach-real.ts +301 -0
  43. package/test/test-rack-concurrent.ts +254 -0
  44. package/test/test-rack-edge-cases.ts +323 -0
  45. package/test/test-rack-grid-cell.ts +318 -0
  46. package/test/test-rack-grid-location.ts +657 -0
  47. package/test/test-real-3d-positioning.ts +158 -0
  48. package/test/test-slot-center-convention.ts +116 -0
  49. package/test/test-slot-target.ts +189 -0
  50. package/test/test-storage-rack-batched.ts +606 -0
  51. package/test/test-storage-rack-click.ts +329 -0
  52. package/test/test-storage-rack-slot-api.ts +357 -0
  53. package/test/test-toscene-convention.ts +162 -0
  54. package/test/test-user-scenario-sequential.ts +334 -0
  55. package/translations/en.json +2 -0
  56. package/translations/ja.json +2 -0
  57. package/translations/ko.json +2 -0
  58. package/translations/ms.json +2 -0
  59. package/translations/zh.json +2 -0
  60. package/tsconfig.tsbuildinfo +1 -1
  61. package/dist/rack-column.d.ts +0 -35
  62. package/dist/rack-column.js +0 -258
  63. package/dist/rack-column.js.map +0 -1
  64. package/dist/rack-grid-helpers.d.ts +0 -28
  65. package/dist/rack-grid-helpers.js +0 -71
  66. package/dist/rack-grid-helpers.js.map +0 -1
  67. package/dist/rack-grid-location.d.ts +0 -37
  68. package/dist/rack-grid-location.js +0 -227
  69. package/dist/rack-grid-location.js.map +0 -1
  70. package/dist/storage-cell-3d.d.ts +0 -25
  71. package/dist/storage-cell-3d.js +0 -88
  72. package/dist/storage-cell-3d.js.map +0 -1
  73. package/dist/storage-cell.d.ts +0 -73
  74. package/dist/storage-cell.js +0 -215
  75. package/dist/storage-cell.js.map +0 -1
  76. package/src/rack-column.ts +0 -340
  77. package/src/rack-grid-helpers.ts +0 -77
  78. package/src/rack-grid-location.ts +0 -286
  79. package/src/storage-cell-3d.ts +0 -101
  80. package/src/storage-cell.ts +0 -267
  81. package/test/test-cell-position.ts +0 -105
  82. 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,11 +23,41 @@ 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
 
@@ -47,21 +77,7 @@ export class StorageRack3D extends RealObjectGroup {
47
77
  const beamH = postW * 1.2
48
78
  const braceT = postW * 0.6
49
79
 
50
- const postMaterial = new THREE.MeshStandardMaterial({
51
- color: POST_COLOR,
52
- metalness: 0.7,
53
- roughness: 0.4
54
- })
55
- const beamMaterial = new THREE.MeshStandardMaterial({
56
- color: BEAM_COLOR,
57
- metalness: 0.7,
58
- roughness: 0.4
59
- })
60
- const braceMaterial = new THREE.MeshStandardMaterial({
61
- color: BRACE_COLOR,
62
- metalness: 0.7,
63
- roughness: 0.4
64
- })
80
+ // Material module-level singleton (rack-materials.ts) — 인스턴스 별 생성 X.
65
81
 
66
82
  // ── Uprights (vertical posts at every bay boundary) ──────────────
67
83
  // bays + 1 vertical positions; for each, one front post + one back post.
@@ -76,7 +92,7 @@ export class StorageRack3D extends RealObjectGroup {
76
92
  postGeos.push(post)
77
93
  }
78
94
  }
79
- const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), postMaterial)
95
+ const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), POST_MATERIAL)
80
96
  postMesh.castShadow = true
81
97
  postMesh.receiveShadow = true
82
98
  this.object3d.add(postMesh)
@@ -94,10 +110,14 @@ export class StorageRack3D extends RealObjectGroup {
94
110
  beamGeos.push(beam)
95
111
  }
96
112
  }
97
- const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), beamMaterial)
113
+ const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), BEAM_MATERIAL)
98
114
  beamMesh.castShadow = true
99
115
  beamMesh.receiveShadow = true
100
- 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)
101
121
 
102
122
  // ── Diagonal cross-bracing on the back face (the "X" pattern) ────
103
123
  // Two diagonals per level — "/" and "\" — making an X across each
@@ -127,7 +147,7 @@ export class StorageRack3D extends RealObjectGroup {
127
147
  }
128
148
  }
129
149
  if (braceGeos.length > 0) {
130
- const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), braceMaterial)
150
+ const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), BRACE_MATERIAL)
131
151
  braceMesh.castShadow = true
132
152
  this.object3d.add(braceMesh)
133
153
  }
@@ -143,14 +163,7 @@ export class StorageRack3D extends RealObjectGroup {
143
163
  const shelfD = Math.max(0, height - 2 * beamH)
144
164
  const shelfGeo = new THREE.PlaneGeometry(shelfW, shelfD)
145
165
  shelfGeo.rotateX(-Math.PI / 2) // X-Y plane → X-Z plane (= horizontal)
146
- const shelfMaterial = new THREE.MeshStandardMaterial({
147
- color: BEAM_COLOR,
148
- metalness: 0.3,
149
- roughness: 0.6,
150
- transparent: true,
151
- opacity: 0.25,
152
- side: THREE.DoubleSide
153
- })
166
+ const shelfMaterial = SHELF_MATERIAL
154
167
  for (let lv = 0; lv < levels; lv++) {
155
168
  // shelf plane Y = 해당 level 의 *load beam top* 정확 일치 (cell 바닥 면).
156
169
  // beam center Y = shelfBaseY + yFrac*shelfZone - beamH/2 + (lv===0 ? beamH : 0)
@@ -162,6 +175,107 @@ export class StorageRack3D extends RealObjectGroup {
162
175
  shelf.receiveShadow = true
163
176
  this.object3d.add(shelf)
164
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
165
279
  }
166
280
 
167
281
  updateDimension() {}