@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
@@ -21,34 +21,49 @@
21
21
  import * as THREE from 'three';
22
22
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
23
23
  import { RealObjectGroup } from '@hatiolab/things-scene';
24
- const POST_COLOR = 0x6a7080;
25
- const BEAM_COLOR = 0x556070;
26
- const BRACE_COLOR = 0x556070;
24
+ import { POST_MATERIAL, BEAM_MATERIAL, BRACE_MATERIAL, SHELF_MATERIAL, STOCK_MATERIAL } from './rack-materials.js';
25
+ const BEAM_COLOR = 0x556070; // shelf material 의 color 와 일치 — 일부 코멘트 참조
26
+ // ── Stock visualization 공용 자원 ────────────────────────────────────────────
27
+ const STOCK_GEOMETRY_CACHE = new Map();
28
+ const STOCK_DEFAULT_COLOR = '#c8a878'; // cardboard 색 (legend 매칭 없을 때)
29
+ function getStockGeometry(w, h, d) {
30
+ const k = `${w.toFixed(1)}-${h.toFixed(1)}-${d.toFixed(1)}`;
31
+ let g = STOCK_GEOMETRY_CACHE.get(k);
32
+ if (!g) {
33
+ g = new THREE.BoxGeometry(w, d, h); // X=w, Y=d (vertical), Z=h
34
+ STOCK_GEOMETRY_CACHE.set(k, g);
35
+ }
36
+ return g;
37
+ }
27
38
  export class StorageRack3D extends RealObjectGroup {
39
+ /** state.data 기반 stock 시각화 InstancedMesh. rebuildStockMesh 가 관리. */
40
+ _stockMesh;
41
+ /** Horizontal beam 그룹 — hideHorizontalFrame 시 visibility 토글. */
42
+ _beamGroup;
43
+ /** Public read-only — click 핸들러가 instanceId/record 역참조에 사용. */
44
+ get stockMesh() {
45
+ return this._stockMesh;
46
+ }
47
+ /** hideHorizontalFrame 변경 시 RackGrid 의 onchange 가 호출 — 즉시 반영. */
48
+ applyFrameVisibility() {
49
+ const hide = !!this.component.state?.hideHorizontalFrame;
50
+ if (this._beamGroup)
51
+ this._beamGroup.visible = !hide;
52
+ }
28
53
  build() {
29
54
  super.build();
30
55
  const { width, height, depth = 3000 } = this.component.state;
31
56
  const levels = Math.max(1, Math.floor(this.component.state.levels || 4));
32
57
  const bays = Math.max(1, Math.floor(this.component.state.bays || 5));
33
- const baseY = -depth / 2;
58
+ const shelfBase = Math.max(0, Math.min(this.component.state.shelfBaseHeight || 0, depth * 0.9));
59
+ const shelfZone = depth - shelfBase; // 실제 shelf 가 차지하는 Y
60
+ const baseY = -depth / 2; // rack 바닥 (3D Y 의 최저)
61
+ const shelfBaseY = baseY + shelfBase; // 첫 shelf 의 시작 (= level 1 의 바닥)
34
62
  const postW = Math.min(width / bays, height) * 0.06;
35
- const beamH = depth * 0.025;
63
+ // beam 두께 = post 비슷 (산업 beam 이 post 보다 약간 두꺼움 — 1.2배)
64
+ const beamH = postW * 1.2;
36
65
  const braceT = postW * 0.6;
37
- const postMaterial = new THREE.MeshStandardMaterial({
38
- color: POST_COLOR,
39
- metalness: 0.7,
40
- roughness: 0.4
41
- });
42
- const beamMaterial = new THREE.MeshStandardMaterial({
43
- color: BEAM_COLOR,
44
- metalness: 0.7,
45
- roughness: 0.4
46
- });
47
- const braceMaterial = new THREE.MeshStandardMaterial({
48
- color: BRACE_COLOR,
49
- metalness: 0.7,
50
- roughness: 0.4
51
- });
66
+ // Material module-level singleton (rack-materials.ts) — 인스턴스 별 생성 X.
52
67
  // ── Uprights (vertical posts at every bay boundary) ──────────────
53
68
  // bays + 1 vertical positions; for each, one front post + one back post.
54
69
  const postGeos = [];
@@ -62,32 +77,36 @@ export class StorageRack3D extends RealObjectGroup {
62
77
  postGeos.push(post);
63
78
  }
64
79
  }
65
- const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), postMaterial);
80
+ const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), POST_MATERIAL);
66
81
  postMesh.castShadow = true;
67
82
  postMesh.receiveShadow = true;
68
83
  this.object3d.add(postMesh);
69
84
  // ── Horizontal beams (front + back faces at each level) ──────────
70
- // levels + 1 vertical positions (level 0 = ground, level N = top).
85
+ // shelf zone 안 levels+1 위치 (level 0 = shelfBase, level N = 천장).
71
86
  const beamGeos = [];
72
87
  for (let lv = 0; lv <= levels; lv++) {
73
88
  const yFrac = lv / levels;
74
- const y = baseY + yFrac * depth - beamH / 2 + (lv === 0 ? beamH : 0);
89
+ const y = shelfBaseY + yFrac * shelfZone - beamH / 2 + (lv === 0 ? beamH : 0);
75
90
  for (const zSign of [-1, 1]) {
76
91
  const beam = new THREE.BoxGeometry(width, beamH, beamH);
77
92
  beam.translate(0, y, zSign * (height / 2 - beamH / 2));
78
93
  beamGeos.push(beam);
79
94
  }
80
95
  }
81
- const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), beamMaterial);
96
+ const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), BEAM_MATERIAL);
82
97
  beamMesh.castShadow = true;
83
98
  beamMesh.receiveShadow = true;
84
- this.object3d.add(beamMesh);
99
+ // hideHorizontalFrame 즉시 토글 위한 별도 group
100
+ this._beamGroup = new THREE.Group();
101
+ this._beamGroup.add(beamMesh);
102
+ this._beamGroup.visible = !(this.component.state?.hideHorizontalFrame);
103
+ this.object3d.add(this._beamGroup);
85
104
  // ── Diagonal cross-bracing on the back face (the "X" pattern) ────
86
105
  // Two diagonals per level — "/" and "\" — making an X across each
87
106
  // bay-tall cell. Visual signature of a load-bearing rack.
88
107
  const braceGeos = [];
89
108
  const cellW = width / bays;
90
- const cellH = depth / levels;
109
+ const cellH = shelfZone / levels; // cell 높이 (shelf zone 안)
91
110
  const braceLen = Math.sqrt(cellW * cellW + cellH * cellH);
92
111
  const braceAngle = Math.atan2(cellH, cellW);
93
112
  const backZ = height / 2 - postW * 0.6;
@@ -97,7 +116,7 @@ export class StorageRack3D extends RealObjectGroup {
97
116
  continue;
98
117
  const cellCenterX = (bay - bays / 2 + 0.5) * cellW;
99
118
  for (let lv = 0; lv < levels; lv++) {
100
- const cellCenterY = baseY + (lv + 0.5) * cellH;
119
+ const cellCenterY = shelfBaseY + (lv + 0.5) * cellH;
101
120
  for (const sign of [-1, 1]) {
102
121
  const brace = new THREE.BoxGeometry(braceLen, braceT, braceT);
103
122
  brace.rotateZ(sign * braceAngle);
@@ -107,10 +126,126 @@ export class StorageRack3D extends RealObjectGroup {
107
126
  }
108
127
  }
109
128
  if (braceGeos.length > 0) {
110
- const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), braceMaterial);
129
+ const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), BRACE_MATERIAL);
111
130
  braceMesh.castShadow = true;
112
131
  this.object3d.add(braceMesh);
113
132
  }
133
+ // ── Shelf planes (level 별 반투명 무볼륨 판) ────────────────────────────
134
+ // 각 level 의 *바닥 면* 에 plane — cell 위치 시각 인식. carrier 가 그 위
135
+ // 에 놓이는 *지지면*. 반투명.
136
+ //
137
+ // X-Z 넓이를 *frame 안쪽* 으로 줄여 mesh 겹침 자체 제거 (Z-fight 회피).
138
+ // X: 양 옆 corner post 안쪽 (-postW 양쪽)
139
+ // Z: 앞/뒤 beam 안쪽 (-beamH 양쪽)
140
+ const shelfW = Math.max(0, width - 2 * postW);
141
+ const shelfD = Math.max(0, height - 2 * beamH);
142
+ const shelfGeo = new THREE.PlaneGeometry(shelfW, shelfD);
143
+ shelfGeo.rotateX(-Math.PI / 2); // X-Y plane → X-Z plane (= horizontal)
144
+ const shelfMaterial = SHELF_MATERIAL;
145
+ for (let lv = 0; lv < levels; lv++) {
146
+ // shelf plane Y = 해당 level 의 *load beam top* 정확 일치 (cell 바닥 면).
147
+ // beam center Y = shelfBaseY + yFrac*shelfZone - beamH/2 + (lv===0 ? beamH : 0)
148
+ // beam top Y = beam center + beamH/2 = shelfBaseY + yFrac*shelfZone + (lv===0 ? beamH : 0)
149
+ const yFrac = lv / levels;
150
+ const y = shelfBaseY + yFrac * shelfZone + (lv === 0 ? beamH : 0);
151
+ const shelf = new THREE.Mesh(shelfGeo, shelfMaterial);
152
+ shelf.position.set(0, y, 0);
153
+ shelf.receiveShadow = true;
154
+ this.object3d.add(shelf);
155
+ }
156
+ // state.data 가 있으면 stock InstancedMesh 도 빌드
157
+ this.rebuildStockMesh();
158
+ }
159
+ /**
160
+ * state.data 의 각 record 를 한 InstancedMesh instance 로 렌더링.
161
+ * cellMap 에 있는 cellId 만 instance 부여 — 나머지는 무시.
162
+ * Mover/pickAndPlace 와 무관 — 순수 시각화.
163
+ */
164
+ rebuildStockMesh() {
165
+ if (this._stockMesh) {
166
+ this.object3d.remove(this._stockMesh);
167
+ this._stockMesh = undefined;
168
+ }
169
+ const rack = this.component;
170
+ const data = rack.state?.data;
171
+ if (!Array.isArray(data) || data.length === 0)
172
+ return;
173
+ const cellMap = rack.cellMap;
174
+ if (!cellMap)
175
+ return;
176
+ // rack 의 기하 파라미터 — cellMap / _ensureCellAttachObject3d 의 levelHeight
177
+ // 계산과 *반드시* 일치해야 함 (shelfBaseHeight > 0 일 때 stock 시각 위치와
178
+ // crane fork target 이 어긋나는 회귀 차단).
179
+ const rs = this.component.state;
180
+ const rackWidth = rs?.width || 1000;
181
+ const rackDepth = rs?.depth || 3000;
182
+ const rackHeight = rs?.height || 600;
183
+ const bays = Math.max(1, Math.floor(rs?.bays || 5));
184
+ const levels = Math.max(1, Math.floor(rs?.levels || 4));
185
+ const shelfBase = Math.max(0, Math.min(rs?.shelfBaseHeight || 0, rackDepth * 0.9));
186
+ const shelfZone = rackDepth - shelfBase;
187
+ const bayWidth = rackWidth / bays;
188
+ const levelHeight = shelfZone / levels; // ← shelfZone 기준 (cellMap 과 동일)
189
+ const rowDepth = rackHeight;
190
+ // 한 stock 의 크기 — cell 의 85% × 85% × 70%
191
+ const stockW = bayWidth * 0.85;
192
+ const stockH = rowDepth * 0.85;
193
+ const stockD = levelHeight * 0.7;
194
+ const geo = getStockGeometry(stockW, stockH, stockD);
195
+ // 유효한 record 만 추출
196
+ const valid = [];
197
+ for (const r of data) {
198
+ if (!r?.cellId)
199
+ continue;
200
+ const cell = cellMap.findById(r.cellId);
201
+ if (cell)
202
+ valid.push({ cell, record: r });
203
+ }
204
+ if (valid.length === 0)
205
+ return;
206
+ const inst = new THREE.InstancedMesh(geo, STOCK_MATERIAL, valid.length);
207
+ inst.castShadow = false;
208
+ inst.receiveShadow = false;
209
+ inst.frustumCulled = false;
210
+ // things-scene EventManager3D 의 raycast hit-test 가 walk-up 하며 찾는 키:
211
+ // userData.context 에 RealObject 자체 set.
212
+ // 객체 전체 대체가 아니라 속성만 set 해 Three.js 의 기존 userData 필드 보존.
213
+ inst.userData.context = this;
214
+ // 클릭 핸들러가 instanceId 로 record 를 역참조할 수 있도록 순서대로 저장.
215
+ inst.userData._records = valid.map(v => v.record);
216
+ const m = new THREE.Matrix4();
217
+ const pos = new THREE.Vector3();
218
+ const q = new THREE.Quaternion();
219
+ const s = new THREE.Vector3(1, 1, 1);
220
+ const c = new THREE.Color();
221
+ for (let i = 0; i < valid.length; i++) {
222
+ const { cell, record } = valid[i];
223
+ // cell 중심 (rack-center 좌표계)
224
+ const x = cell.localPosition.x + bayWidth / 2 - rackWidth / 2;
225
+ const cellCenterY = cell.localPosition.y + levelHeight / 2 - rackDepth / 2;
226
+ // stock 바닥 = cell 바닥 → stock 중심 Y = cell 중심 Y - levelHeight/2 + stockD/2
227
+ const y = cellCenterY - levelHeight / 2 + stockD / 2;
228
+ const z = cell.localPosition.z + rowDepth / 2 - rackHeight / 2;
229
+ pos.set(x, y, z);
230
+ m.compose(pos, q, s);
231
+ inst.setMatrixAt(i, m);
232
+ // Legend 색상 매핑 — rack 의 resolveLegendColor 호출, 매칭 없으면 default
233
+ const resolved = rack.resolveLegendColor?.(record) ?? STOCK_DEFAULT_COLOR;
234
+ c.set(resolved);
235
+ inst.setColorAt(i, c);
236
+ }
237
+ inst.instanceMatrix.needsUpdate = true;
238
+ if (inst.instanceColor)
239
+ inst.instanceColor.needsUpdate = true;
240
+ // ── CRITICAL: bounding sphere 계산 ────────────────────────────────────────
241
+ // Three.js InstancedMesh.raycast 는 먼저 bounding sphere 로 broad-phase 체크.
242
+ // setMatrixAt 만 호출하고 computeBoundingSphere 를 안 부르면 sphere.radius=0
243
+ // → 어떤 ray 도 안 맞음 → click event 발화 안 됨. raycaster 가 instance 인식
244
+ // 못 하는 것의 *결정적 원인*. needsUpdate 후 명시 호출 필수.
245
+ inst.computeBoundingSphere();
246
+ inst.computeBoundingBox?.();
247
+ this.object3d.add(inst);
248
+ this._stockMesh = inst;
114
249
  }
115
250
  updateDimension() { }
116
251
  onchange(after, before) {
@@ -118,7 +253,8 @@ export class StorageRack3D extends RealObjectGroup {
118
253
  'bays' in after ||
119
254
  'width' in after ||
120
255
  'height' in after ||
121
- 'depth' in after) {
256
+ 'depth' in after ||
257
+ 'shelfBaseHeight' in after) {
122
258
  this.update();
123
259
  return;
124
260
  }
@@ -1 +1 @@
1
- {"version":3,"file":"storage-rack-3d.js","sourceRoot":"","sources":["../src/storage-rack-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Rack 3D — multi-level storage shelf system.\n *\n * LO-POLY but visually unambiguous as a rack. The signature parts:\n *\n * - 4 corner uprights (vertical posts running floor → top)\n * - intermediate uprights between bays (one between each adjacent bay pair)\n * - horizontal beams at each level on both front and back faces (defining\n * the cell decks)\n * - diagonal cross-bracing on the back face (the \"X\" pattern that says\n * this is a load-bearing storage rack, not just a generic frame)\n *\n * No floor / ceiling panels — the rack is open by design (cells are accessed\n * by a picker from the front side).\n *\n * Cargo (pallets, boxes) added as children render at their own z position.\n * The rack itself is purely structural geometry.\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\nconst POST_COLOR = 0x6a7080\nconst BEAM_COLOR = 0x556070\nconst BRACE_COLOR = 0x556070\n\nexport class StorageRack3D extends RealObjectGroup {\n build() {\n super.build()\n\n const { width, height, depth = 3000 } = this.component.state\n const levels = Math.max(1, Math.floor((this.component.state.levels as number) || 4))\n const bays = Math.max(1, Math.floor((this.component.state.bays as number) || 5))\n\n const baseY = -depth / 2\n const postW = Math.min(width / bays, height) * 0.06\n const beamH = depth * 0.025\n const braceT = postW * 0.6\n\n const postMaterial = new THREE.MeshStandardMaterial({\n color: POST_COLOR,\n metalness: 0.7,\n roughness: 0.4\n })\n const beamMaterial = new THREE.MeshStandardMaterial({\n color: BEAM_COLOR,\n metalness: 0.7,\n roughness: 0.4\n })\n const braceMaterial = new THREE.MeshStandardMaterial({\n color: BRACE_COLOR,\n metalness: 0.7,\n roughness: 0.4\n })\n\n // ── Uprights (vertical posts at every bay boundary) ──────────────\n // bays + 1 vertical positions; for each, one front post + one back post.\n const postGeos: THREE.BufferGeometry[] = []\n for (let i = 0; i <= bays; i++) {\n const xFrac = i / bays - 0.5\n const x = xFrac * width\n // Front + back posts\n for (const zSign of [-1, 1]) {\n const post = new THREE.BoxGeometry(postW, depth, postW)\n post.translate(x, 0, zSign * (height / 2 - postW / 2))\n postGeos.push(post)\n }\n }\n const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), postMaterial)\n postMesh.castShadow = true\n postMesh.receiveShadow = true\n this.object3d.add(postMesh)\n\n // ── Horizontal beams (front + back faces at each level) ──────────\n // levels + 1 vertical positions (level 0 = ground, level N = top).\n const beamGeos: THREE.BufferGeometry[] = []\n for (let lv = 0; lv <= levels; lv++) {\n const yFrac = lv / levels\n const y = baseY + yFrac * depth - beamH / 2 + (lv === 0 ? beamH : 0)\n\n for (const zSign of [-1, 1]) {\n const beam = new THREE.BoxGeometry(width, beamH, beamH)\n beam.translate(0, y, zSign * (height / 2 - beamH / 2))\n beamGeos.push(beam)\n }\n }\n const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), beamMaterial)\n beamMesh.castShadow = true\n beamMesh.receiveShadow = true\n this.object3d.add(beamMesh)\n\n // ── Diagonal cross-bracing on the back face (the \"X\" pattern) ────\n // Two diagonals per level — \"/\" and \"\\\" — making an X across each\n // bay-tall cell. Visual signature of a load-bearing rack.\n const braceGeos: THREE.BufferGeometry[] = []\n const cellW = width / bays\n const cellH = depth / levels\n const braceLen = Math.sqrt(cellW * cellW + cellH * cellH)\n const braceAngle = Math.atan2(cellH, cellW)\n const backZ = height / 2 - postW * 0.6\n\n for (let bay = 0; bay < bays; bay++) {\n // Brace only every other bay to keep things visually open\n if (bay % 2 !== 0) continue\n\n const cellCenterX = (bay - bays / 2 + 0.5) * cellW\n\n for (let lv = 0; lv < levels; lv++) {\n const cellCenterY = baseY + (lv + 0.5) * cellH\n\n for (const sign of [-1, 1]) {\n const brace = new THREE.BoxGeometry(braceLen, braceT, braceT)\n brace.rotateZ(sign * braceAngle)\n brace.translate(cellCenterX, cellCenterY, backZ)\n braceGeos.push(brace)\n }\n }\n }\n if (braceGeos.length > 0) {\n const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), braceMaterial)\n braceMesh.castShadow = true\n this.object3d.add(braceMesh)\n }\n }\n\n updateDimension() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if (\n 'levels' in after ||\n 'bays' in after ||\n 'width' in after ||\n 'height' in after ||\n 'depth' in after\n ) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n\n updateAlpha() {}\n}\n"]}
1
+ {"version":3,"file":"storage-rack-3d.js","sourceRoot":"","sources":["../src/storage-rack-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Rack 3D — multi-level storage shelf system.\n *\n * LO-POLY but visually unambiguous as a rack. The signature parts:\n *\n * - 4 corner uprights (vertical posts running floor → top)\n * - intermediate uprights between bays (one between each adjacent bay pair)\n * - horizontal beams at each level on both front and back faces (defining\n * the cell decks)\n * - diagonal cross-bracing on the back face (the \"X\" pattern that says\n * this is a load-bearing storage rack, not just a generic frame)\n *\n * No floor / ceiling panels — the rack is open by design (cells are accessed\n * by a picker from the front side).\n *\n * Cargo (pallets, boxes) added as children render at their own z position.\n * The rack itself is purely structural geometry.\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\nimport { POST_MATERIAL, BEAM_MATERIAL, BRACE_MATERIAL, SHELF_MATERIAL, STOCK_MATERIAL } from './rack-materials.js'\n\nconst BEAM_COLOR = 0x556070 // shelf material 의 color 와 일치 — 일부 코멘트 참조\n\n// ── Stock visualization 공용 자원 ────────────────────────────────────────────\nconst STOCK_GEOMETRY_CACHE = new Map<string, THREE.BufferGeometry>()\nconst STOCK_DEFAULT_COLOR = '#c8a878' // cardboard 색 (legend 매칭 없을 때)\n\nfunction getStockGeometry(w: number, h: number, d: number): THREE.BufferGeometry {\n const k = `${w.toFixed(1)}-${h.toFixed(1)}-${d.toFixed(1)}`\n let g = STOCK_GEOMETRY_CACHE.get(k)\n if (!g) {\n g = new THREE.BoxGeometry(w, d, h) // X=w, Y=d (vertical), Z=h\n STOCK_GEOMETRY_CACHE.set(k, g)\n }\n return g\n}\n\nexport class StorageRack3D extends RealObjectGroup {\n /** state.data 기반 stock 시각화 InstancedMesh. rebuildStockMesh 가 관리. */\n private _stockMesh?: THREE.InstancedMesh\n /** Horizontal beam 그룹 — hideHorizontalFrame 시 visibility 토글. */\n private _beamGroup?: THREE.Group\n\n /** Public read-only — click 핸들러가 instanceId/record 역참조에 사용. */\n get stockMesh(): THREE.InstancedMesh | undefined {\n return this._stockMesh\n }\n\n /** hideHorizontalFrame 변경 시 RackGrid 의 onchange 가 호출 — 즉시 반영. */\n applyFrameVisibility(): void {\n const hide = !!(this.component.state as any)?.hideHorizontalFrame\n if (this._beamGroup) this._beamGroup.visible = !hide\n }\n\n build() {\n super.build()\n\n const { width, height, depth = 3000 } = this.component.state\n const levels = Math.max(1, Math.floor((this.component.state.levels as number) || 4))\n const bays = Math.max(1, Math.floor((this.component.state.bays as number) || 5))\n const shelfBase = Math.max(0, Math.min(\n (this.component.state.shelfBaseHeight as number) || 0,\n depth * 0.9\n ))\n const shelfZone = depth - shelfBase // 실제 shelf 가 차지하는 Y\n\n const baseY = -depth / 2 // rack 바닥 (3D Y 의 최저)\n const shelfBaseY = baseY + shelfBase // 첫 shelf 의 시작 (= level 1 의 바닥)\n const postW = Math.min(width / bays, height) * 0.06\n // beam 두께 = post 와 비슷 (산업 beam 이 post 보다 약간 두꺼움 — 1.2배)\n const beamH = postW * 1.2\n const braceT = postW * 0.6\n\n // Material 은 module-level singleton (rack-materials.ts) — 인스턴스 별 생성 X.\n\n // ── Uprights (vertical posts at every bay boundary) ──────────────\n // bays + 1 vertical positions; for each, one front post + one back post.\n const postGeos: THREE.BufferGeometry[] = []\n for (let i = 0; i <= bays; i++) {\n const xFrac = i / bays - 0.5\n const x = xFrac * width\n // Front + back posts\n for (const zSign of [-1, 1]) {\n const post = new THREE.BoxGeometry(postW, depth, postW)\n post.translate(x, 0, zSign * (height / 2 - postW / 2))\n postGeos.push(post)\n }\n }\n const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), POST_MATERIAL)\n postMesh.castShadow = true\n postMesh.receiveShadow = true\n this.object3d.add(postMesh)\n\n // ── Horizontal beams (front + back faces at each level) ──────────\n // shelf zone 안 levels+1 위치 (level 0 = shelfBase, level N = 천장).\n const beamGeos: THREE.BufferGeometry[] = []\n for (let lv = 0; lv <= levels; lv++) {\n const yFrac = lv / levels\n const y = shelfBaseY + yFrac * shelfZone - beamH / 2 + (lv === 0 ? beamH : 0)\n\n for (const zSign of [-1, 1]) {\n const beam = new THREE.BoxGeometry(width, beamH, beamH)\n beam.translate(0, y, zSign * (height / 2 - beamH / 2))\n beamGeos.push(beam)\n }\n }\n const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), BEAM_MATERIAL)\n beamMesh.castShadow = true\n beamMesh.receiveShadow = true\n // hideHorizontalFrame 즉시 토글 위한 별도 group\n this._beamGroup = new THREE.Group()\n this._beamGroup.add(beamMesh)\n this._beamGroup.visible = !((this.component.state as any)?.hideHorizontalFrame)\n this.object3d.add(this._beamGroup)\n\n // ── Diagonal cross-bracing on the back face (the \"X\" pattern) ────\n // Two diagonals per level — \"/\" and \"\\\" — making an X across each\n // bay-tall cell. Visual signature of a load-bearing rack.\n const braceGeos: THREE.BufferGeometry[] = []\n const cellW = width / bays\n const cellH = shelfZone / levels // cell 높이 (shelf zone 안)\n const braceLen = Math.sqrt(cellW * cellW + cellH * cellH)\n const braceAngle = Math.atan2(cellH, cellW)\n const backZ = height / 2 - postW * 0.6\n\n for (let bay = 0; bay < bays; bay++) {\n // Brace only every other bay to keep things visually open\n if (bay % 2 !== 0) continue\n\n const cellCenterX = (bay - bays / 2 + 0.5) * cellW\n\n for (let lv = 0; lv < levels; lv++) {\n const cellCenterY = shelfBaseY + (lv + 0.5) * cellH\n\n for (const sign of [-1, 1]) {\n const brace = new THREE.BoxGeometry(braceLen, braceT, braceT)\n brace.rotateZ(sign * braceAngle)\n brace.translate(cellCenterX, cellCenterY, backZ)\n braceGeos.push(brace)\n }\n }\n }\n if (braceGeos.length > 0) {\n const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), BRACE_MATERIAL)\n braceMesh.castShadow = true\n this.object3d.add(braceMesh)\n }\n\n // ── Shelf planes (level 별 반투명 무볼륨 판) ────────────────────────────\n // 각 level 의 *바닥 면* 에 plane — cell 위치 시각 인식. carrier 가 그 위\n // 에 놓이는 *지지면*. 반투명.\n //\n // X-Z 넓이를 *frame 안쪽* 으로 줄여 mesh 겹침 자체 제거 (Z-fight 회피).\n // X: 양 옆 corner post 안쪽 (-postW 양쪽)\n // Z: 앞/뒤 beam 안쪽 (-beamH 양쪽)\n const shelfW = Math.max(0, width - 2 * postW)\n const shelfD = Math.max(0, height - 2 * beamH)\n const shelfGeo = new THREE.PlaneGeometry(shelfW, shelfD)\n shelfGeo.rotateX(-Math.PI / 2) // X-Y plane → X-Z plane (= horizontal)\n const shelfMaterial = SHELF_MATERIAL\n for (let lv = 0; lv < levels; lv++) {\n // shelf plane Y = 해당 level 의 *load beam top* 정확 일치 (cell 바닥 면).\n // beam center Y = shelfBaseY + yFrac*shelfZone - beamH/2 + (lv===0 ? beamH : 0)\n // beam top Y = beam center + beamH/2 = shelfBaseY + yFrac*shelfZone + (lv===0 ? beamH : 0)\n const yFrac = lv / levels\n const y = shelfBaseY + yFrac * shelfZone + (lv === 0 ? beamH : 0)\n const shelf = new THREE.Mesh(shelfGeo, shelfMaterial)\n shelf.position.set(0, y, 0)\n shelf.receiveShadow = true\n this.object3d.add(shelf)\n }\n\n // state.data 가 있으면 stock InstancedMesh 도 빌드\n this.rebuildStockMesh()\n }\n\n /**\n * state.data 의 각 record 를 한 InstancedMesh instance 로 렌더링.\n * cellMap 에 있는 cellId 만 instance 부여 — 나머지는 무시.\n * Mover/pickAndPlace 와 무관 — 순수 시각화.\n */\n rebuildStockMesh(): void {\n if (this._stockMesh) {\n this.object3d.remove(this._stockMesh)\n this._stockMesh = undefined\n }\n\n const rack: any = this.component\n const data = rack.state?.data as Array<{ cellId: string;[key: string]: any }> | undefined\n if (!Array.isArray(data) || data.length === 0) return\n\n const cellMap = rack.cellMap\n if (!cellMap) return\n\n // rack 의 기하 파라미터 — cellMap / _ensureCellAttachObject3d 의 levelHeight\n // 계산과 *반드시* 일치해야 함 (shelfBaseHeight > 0 일 때 stock 시각 위치와\n // crane fork target 이 어긋나는 회귀 차단).\n const rs: any = this.component.state\n const rackWidth = (rs?.width as number) || 1000\n const rackDepth = (rs?.depth as number) || 3000\n const rackHeight = (rs?.height as number) || 600\n const bays = Math.max(1, Math.floor(rs?.bays || 5))\n const levels = Math.max(1, Math.floor(rs?.levels || 4))\n const shelfBase = Math.max(0, Math.min((rs?.shelfBaseHeight as number) || 0, rackDepth * 0.9))\n const shelfZone = rackDepth - shelfBase\n const bayWidth = rackWidth / bays\n const levelHeight = shelfZone / levels // ← shelfZone 기준 (cellMap 과 동일)\n const rowDepth = rackHeight\n\n // 한 stock 의 크기 — cell 의 85% × 85% × 70%\n const stockW = bayWidth * 0.85\n const stockH = rowDepth * 0.85\n const stockD = levelHeight * 0.7\n const geo = getStockGeometry(stockW, stockH, stockD)\n\n // 유효한 record 만 추출\n const valid: { cell: any; record: any }[] = []\n for (const r of data) {\n if (!r?.cellId) continue\n const cell = cellMap.findById(r.cellId)\n if (cell) valid.push({ cell, record: r })\n }\n if (valid.length === 0) return\n\n const inst = new THREE.InstancedMesh(geo, STOCK_MATERIAL, valid.length)\n inst.castShadow = false\n inst.receiveShadow = false\n inst.frustumCulled = false\n // things-scene EventManager3D 의 raycast hit-test 가 walk-up 하며 찾는 키:\n // userData.context 에 RealObject 자체 set.\n // 객체 전체 대체가 아니라 속성만 set 해 Three.js 의 기존 userData 필드 보존.\n inst.userData.context = this\n // 클릭 핸들러가 instanceId 로 record 를 역참조할 수 있도록 순서대로 저장.\n inst.userData._records = valid.map(v => v.record)\n\n const m = new THREE.Matrix4()\n const pos = new THREE.Vector3()\n const q = new THREE.Quaternion()\n const s = new THREE.Vector3(1, 1, 1)\n const c = new THREE.Color()\n\n for (let i = 0; i < valid.length; i++) {\n const { cell, record } = valid[i]\n // cell 중심 (rack-center 좌표계)\n const x = cell.localPosition.x + bayWidth / 2 - rackWidth / 2\n const cellCenterY = cell.localPosition.y + levelHeight / 2 - rackDepth / 2\n // stock 바닥 = cell 바닥 → stock 중심 Y = cell 중심 Y - levelHeight/2 + stockD/2\n const y = cellCenterY - levelHeight / 2 + stockD / 2\n const z = cell.localPosition.z + rowDepth / 2 - rackHeight / 2\n\n pos.set(x, y, z)\n m.compose(pos, q, s)\n inst.setMatrixAt(i, m)\n\n // Legend 색상 매핑 — rack 의 resolveLegendColor 호출, 매칭 없으면 default\n const resolved = (rack as any).resolveLegendColor?.(record) ?? STOCK_DEFAULT_COLOR\n c.set(resolved)\n inst.setColorAt(i, c)\n }\n inst.instanceMatrix.needsUpdate = true\n if (inst.instanceColor) inst.instanceColor.needsUpdate = true\n\n // ── CRITICAL: bounding sphere 계산 ────────────────────────────────────────\n // Three.js InstancedMesh.raycast 는 먼저 bounding sphere 로 broad-phase 체크.\n // setMatrixAt 만 호출하고 computeBoundingSphere 를 안 부르면 sphere.radius=0\n // → 어떤 ray 도 안 맞음 → click event 발화 안 됨. raycaster 가 instance 인식\n // 못 하는 것의 *결정적 원인*. needsUpdate 후 명시 호출 필수.\n inst.computeBoundingSphere()\n inst.computeBoundingBox?.()\n\n this.object3d.add(inst)\n this._stockMesh = inst\n }\n\n updateDimension() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if (\n 'levels' in after ||\n 'bays' in after ||\n 'width' in after ||\n 'height' in after ||\n 'depth' in after ||\n 'shelfBaseHeight' in after\n ) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n\n updateAlpha() {}\n}\n"]}