@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
@@ -21,10 +21,35 @@
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;
@@ -38,21 +63,7 @@ export class StorageRack3D extends RealObjectGroup {
38
63
  // beam 두께 = post 와 비슷 (산업 beam 이 post 보다 약간 두꺼움 — 1.2배)
39
64
  const beamH = postW * 1.2;
40
65
  const braceT = postW * 0.6;
41
- const postMaterial = new THREE.MeshStandardMaterial({
42
- color: POST_COLOR,
43
- metalness: 0.7,
44
- roughness: 0.4
45
- });
46
- const beamMaterial = new THREE.MeshStandardMaterial({
47
- color: BEAM_COLOR,
48
- metalness: 0.7,
49
- roughness: 0.4
50
- });
51
- const braceMaterial = new THREE.MeshStandardMaterial({
52
- color: BRACE_COLOR,
53
- metalness: 0.7,
54
- roughness: 0.4
55
- });
66
+ // Material module-level singleton (rack-materials.ts) — 인스턴스 별 생성 X.
56
67
  // ── Uprights (vertical posts at every bay boundary) ──────────────
57
68
  // bays + 1 vertical positions; for each, one front post + one back post.
58
69
  const postGeos = [];
@@ -66,7 +77,7 @@ export class StorageRack3D extends RealObjectGroup {
66
77
  postGeos.push(post);
67
78
  }
68
79
  }
69
- const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), postMaterial);
80
+ const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), POST_MATERIAL);
70
81
  postMesh.castShadow = true;
71
82
  postMesh.receiveShadow = true;
72
83
  this.object3d.add(postMesh);
@@ -82,10 +93,14 @@ export class StorageRack3D extends RealObjectGroup {
82
93
  beamGeos.push(beam);
83
94
  }
84
95
  }
85
- const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), beamMaterial);
96
+ const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), BEAM_MATERIAL);
86
97
  beamMesh.castShadow = true;
87
98
  beamMesh.receiveShadow = true;
88
- 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);
89
104
  // ── Diagonal cross-bracing on the back face (the "X" pattern) ────
90
105
  // Two diagonals per level — "/" and "\" — making an X across each
91
106
  // bay-tall cell. Visual signature of a load-bearing rack.
@@ -111,7 +126,7 @@ export class StorageRack3D extends RealObjectGroup {
111
126
  }
112
127
  }
113
128
  if (braceGeos.length > 0) {
114
- const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), braceMaterial);
129
+ const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), BRACE_MATERIAL);
115
130
  braceMesh.castShadow = true;
116
131
  this.object3d.add(braceMesh);
117
132
  }
@@ -126,14 +141,7 @@ export class StorageRack3D extends RealObjectGroup {
126
141
  const shelfD = Math.max(0, height - 2 * beamH);
127
142
  const shelfGeo = new THREE.PlaneGeometry(shelfW, shelfD);
128
143
  shelfGeo.rotateX(-Math.PI / 2); // X-Y plane → X-Z plane (= horizontal)
129
- const shelfMaterial = new THREE.MeshStandardMaterial({
130
- color: BEAM_COLOR,
131
- metalness: 0.3,
132
- roughness: 0.6,
133
- transparent: true,
134
- opacity: 0.25,
135
- side: THREE.DoubleSide
136
- });
144
+ const shelfMaterial = SHELF_MATERIAL;
137
145
  for (let lv = 0; lv < levels; lv++) {
138
146
  // shelf plane Y = 해당 level 의 *load beam top* 정확 일치 (cell 바닥 면).
139
147
  // beam center Y = shelfBaseY + yFrac*shelfZone - beamH/2 + (lv===0 ? beamH : 0)
@@ -145,6 +153,99 @@ export class StorageRack3D extends RealObjectGroup {
145
153
  shelf.receiveShadow = true;
146
154
  this.object3d.add(shelf);
147
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;
148
249
  }
149
250
  updateDimension() { }
150
251
  onchange(after, before) {
@@ -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 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 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 // 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), 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 = 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), braceMaterial)\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 = new THREE.MeshStandardMaterial({\n color: BEAM_COLOR,\n metalness: 0.3,\n roughness: 0.6,\n transparent: true,\n opacity: 0.25,\n side: THREE.DoubleSide\n })\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\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"]}
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"]}
@@ -1,6 +1,7 @@
1
1
  import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
2
2
  import type { State, Material3D } from '@hatiolab/things-scene';
3
- import { CellMap, type AttachFrame, type Alignment, type Heights, type PlacementArchetype } from '@operato/scene-base';
3
+ import * as THREE from 'three';
4
+ import { CellMap, SlotTarget, type AttachFrame, type Alignment, type Heights, type PlacementArchetype, type SlottedHolder } from '@operato/scene-base';
4
5
  /** Rack 컴포넌트 state */
5
6
  export interface StorageRackState extends State {
6
7
  bays?: number;
@@ -11,6 +12,42 @@ export interface StorageRackState extends State {
11
12
  * 컴포넌트가 들어갈 *빈 공간* 확보. Frame uprights 는 바닥 ~ 천장 그대로.
12
13
  */
13
14
  shelfBaseHeight?: number;
15
+ /**
16
+ * 적재 시각화 데이터. 배열의 각 record 는 최소 `cellId` 필드가 필요. cellMap 에
17
+ * 존재하는 cellId 만 InstancedMesh 의 instance 로 렌더링됨 (한 박스 = 한 cell).
18
+ * Plan A: 이 record 들이 *"매트릭스 안"* 의 carrier — obtainCarrier 가 materialize
19
+ * 시 record 빠짐, receiveAt 시 record push. pickAndPlace 와 양방향 atomic sync.
20
+ */
21
+ data?: Array<{
22
+ cellId: string;
23
+ [key: string]: any;
24
+ }>;
25
+ /**
26
+ * RackCell 컴포넌트의 eager 생성 여부.
27
+ * - `undefined` (default): state.data 있으면 false (batched, 메모리 절약),
28
+ * 없으면 true (legacy non-batched, RackCell-based Mover 호환).
29
+ * - `true`: 명시적 eager — 모든 cell 을 RackCell 로 생성.
30
+ * - `false`: 명시적 skip — Plan A 의 slot API 만 사용 (obtainCarrier / slotTargetAt).
31
+ */
32
+ eagerCells?: boolean;
33
+ /**
34
+ * Legend 컴포넌트의 id. legend 의 `state.status = {field, ranges:[{value|min,max, color}], defaultColor}`
35
+ * 를 사용해 각 record 의 `field` 값을 색상으로 매핑. 미명시 시 scene-wide auto-discovery
36
+ * (type='legend' 인 첫 번째 컴포넌트).
37
+ */
38
+ legendTarget?: string;
39
+ /**
40
+ * Cell 클릭 시 invoke 할 things-scene `Popup` 컴포넌트의 id. Popup 컴포넌트는 scene
41
+ * 어딘가에 일반 컴포넌트로 배치되어 있고, 그 자체의 state (board / modal / closable /
42
+ * draggable / tether / billboard 등) 가 popup 동작을 정의. rack 은 *anchor 만 동적으로*
43
+ * override 해 호출 — 클릭된 cell 위에 popup 이 뜸.
44
+ *
45
+ * 미명시 시 popup 비활성 (rack-cell-click 이벤트는 여전히 발사 — 외부 consumer 가
46
+ * 직접 처리 가능).
47
+ */
48
+ popupRef?: string;
49
+ /** 가로 frame (beam) 만 숨김 — uprights 는 유지. */
50
+ hideHorizontalFrame?: boolean;
14
51
  debugCells?: boolean;
15
52
  material3d?: Material3D;
16
53
  }
@@ -21,16 +58,12 @@ declare const Rack_base: any;
21
58
  *
22
59
  * `levels` × `bays` cells form a vertical grid. Each cell holds one logistics
23
60
  * package (Pallet / Box / Parcel). A picker (Crane / Forklift / robot arm)
24
- * accesses individual cells via Phase G/H Pickable contract — the picker is
25
- * Rack-agnostic, knowing only how to interact with a cell.
61
+ * accesses individual cells via the Plan A slot API — the picker interacts
62
+ * with `SlotTarget` (no explicit cell-component required).
26
63
  *
27
- * **Monitoring mode** (default): carriers are direct children of the rack,
28
- * placed by external data binding. No RackCell children are created.
29
- *
30
- * **Simulation mode**: call `rack._buildCells()` after placing the rack on the
31
- * scene. This creates RackCell children at the correct 3D positions. A picker
32
- * (Crane / Forklift / ...) then navigates to individual RackCells for
33
- * pick-and-place.
64
+ * **Plan A**: carriers are direct children of the rack, addressed by
65
+ * `state.cellId`. Stock visualization uses InstancedMesh (batched). Slot
66
+ * lookup / pick / place via `obtainCarrier` / `receiveAt` / `slotTargetAt`.
34
67
  *
35
68
  * **Placement**: `floor` archetype, full ceiling depth by default.
36
69
  *
@@ -38,7 +71,7 @@ declare const Rack_base: any;
38
71
  * extension can be added later for AGV-mounted or cart-mounted variants —
39
72
  * the cell topology and pickable contract stay the same.
40
73
  */
41
- export default class Rack extends Rack_base {
74
+ export default class Rack extends Rack_base implements SlottedHolder {
42
75
  state: StorageRackState;
43
76
  static placement: PlacementArchetype;
44
77
  static align: Alignment;
@@ -46,22 +79,10 @@ export default class Rack extends Rack_base {
46
79
  get nature(): ComponentNature;
47
80
  get anchors(): never[];
48
81
  /**
49
- * Model serialization storage-cell 자식 자동 제외. cells _buildCells()
50
- * runtime 재생성 (added() 호출 시점). 저장하면 *redundant 모델 크기 폭증* +
51
- * load 시 _buildCells 와 중복. rack 의 bays/levels/shelfBaseHeight 만 저장,
52
- * cells 는 derive.
53
- */
54
- get hierarchy(): Record<string, any>;
55
- /**
56
- * Lifecycle — RackCell child 자동 build. Rack 은 항상 cells 가짐.
57
- */
58
- added(parent: any): void;
59
- /**
60
- * Runtime — bays / levels 변경 시 RackCell child 재구성. _buildCells() 는
61
- * 기존 cell 제거 후 재생성 (idempotent), 단 carrier 보유 시 결함 위험 —
62
- * application 책임.
82
+ * Runtimebays / levels 변경 anchor 캐시 무효화. cell 위치가 바뀌므로 다음
83
+ * `_ensureCellAttachObject3d` 호출이 좌표로 갱신.
63
84
  */
64
- onchange(after: Record<string, unknown>, before: Record<string, unknown>): void;
85
+ onchange(after: Record<string, unknown>, _before: Record<string, unknown>): void;
65
86
  /**
66
87
  * Derive the cell topology from the rack's current dimensions and bay/level
67
88
  * counts. The CellMap is rebuilt fresh each time (state changes trigger
@@ -73,35 +94,148 @@ export default class Rack extends Rack_base {
73
94
  * Z = row axis (front → back, the rack's `height` state property)
74
95
  */
75
96
  get cellMap(): CellMap;
76
- /**
77
- * Create RackCell child components for each cell in the CellMap.
78
- *
79
- * Called explicitly to enter simulation mode — monitoring-mode racks
80
- * never call this (carriers are direct children, no explicit cells).
81
- *
82
- * Idempotent: removes existing rack-cell children first.
83
- */
84
- _buildCells(): void;
85
97
  /**
86
98
  * Allow:
87
- * - Carriable components (pallets, boxes, parcels) — direct children in monitoring mode.
88
- * - RackCell — created by _buildCells() in simulation mode.
99
+ * - Carriable components (pallets, boxes, parcels) — direct children, operation archetype.
89
100
  *
90
101
  * Block:
91
102
  * - Everything else (sensors, labels, etc. can be siblings of the rack, not children).
92
103
  */
93
104
  containable(component: Component): boolean;
94
105
  /**
95
- * Attach frame for carriers that are DIRECT children of the rack
96
- * (monitoring mode, where carriers go directly into the rack without
97
- * explicit RackCell components).
106
+ * Attach frame for direct-child carriers Plan A 모든 carrier rack
107
+ * 직접 자식이므로 매번 호출됨. carrier state.cellId 해당하는 *cell-local
108
+ * anchor object3d* 를 반환 → carrier 의 object3d 가 자동으로 셀 위치에 정렬됨.
109
+ */
110
+ attachPointFor(carrier: Component): AttachFrame | null;
111
+ /** state.data 의 record 목록 (읽기 전용 뷰). */
112
+ get records(): ReadonlyArray<{
113
+ cellId: string;
114
+ [key: string]: any;
115
+ }>;
116
+ /**
117
+ * 1-based (bay, row, level) → 0-based cellId 문자열.
118
+ *
119
+ * rack.cellIdOf(1, 1, 6) → '0-0-5'
120
+ * rack.cellIdOf(3, 1, 4) → '2-0-3'
121
+ */
122
+ cellIdOf(bay: number, row?: number, level?: number): string;
123
+ /** cellId 에 carrier 가 있는가 — child carrier 또는 state.data record 어느 쪽이든. */
124
+ hasCarrierAt(cellId: string): boolean;
125
+ /** cellId 매칭되는 rack 의 직접 자식 carrier (operation archetype). */
126
+ private _carrierChildAt;
127
+ /**
128
+ * carrier 를 obtain — 이미 child 면 그대로, 아니면 state.data record 로 transient
129
+ * materialize 후 rack 의 직접 자식으로 add 하고 state.data 에서 그 record 제거.
130
+ * record 도 child 도 없으면 null.
131
+ *
132
+ * Signature overloads:
133
+ * obtainCarrier('0-0-5') — string cellId 직접
134
+ * obtainCarrier(1, 1, 6) — 1-based (bay, row, level)
135
+ * obtainCarrier(1) ≡ obtainCarrier(1,1,1)
136
+ */
137
+ obtainCarrier(idOrBay: string | number, row?: number, level?: number): Component | null;
138
+ /**
139
+ * State.data 의 *internal* 갱신 — Plan A 의 obtainCarrier / receiveAt 가 사용.
140
+ * `setState` 와 달리 *'change' 이벤트 / onchangeData / mapping cascade 를 우회*.
141
+ *
142
+ * 이유: mapping 시스템이 state.data 변경 시 *자동으로 script fire*. Plan A 의
143
+ * setState 가 그 cascade 를 트리거하면 사용자 script 가 *재귀적으로 자기 자신을 호출*
144
+ * 하는 회귀 (board 의 의도된 binding 일 수도, 우연일 수도) 발생.
145
+ *
146
+ * 대신 *직접 _state 갱신 + rebuildStockMesh 직접 호출* — 시각화는 갱신되지만 외부
147
+ * mapping 은 fire 안 됨. External (WMS / application setState) 호출은 그대로 setState
148
+ * 거치므로 그쪽 mapping 은 정상 동작.
149
+ */
150
+ private _setDataSilently;
151
+ /**
152
+ * cell 이 carrier 를 받을 수 있는가.
153
+ *
154
+ * 규칙:
155
+ * - state.data 에 record 가 있으면 점유 → false
156
+ * - carrier-child 가 있고 *그 child 가 들여오려는 carrier 자기 자신이 아니면* → false
157
+ * - 들여오려는 carrier 가 *바로 그 cell 의 child 자기 자신* 이면 → true (idempotent —
158
+ * obtain('A') 직후 receive('A', sameCarrier) 가 *자기 자리 복귀* 로 동작)
159
+ */
160
+ canReceiveAt(cellId: string, carrier?: Component): boolean;
161
+ /**
162
+ * Carrier 가 rack 의 slot 으로 들어옴 — "매트릭스 진입": 즉시 dispose + state.data 에
163
+ * record 로 환원. 결과: InstancedMesh 가 다시 그 자리에 instance 표시, rack 의 자식
164
+ * 컴포넌트 트리는 깨끗.
165
+ */
166
+ receiveAt(cellId: string, carrier: Component, _options?: any): Promise<void>;
167
+ /**
168
+ * Carrier 의 state 를 state.data record 로 추출. application 이 carrier subclass 별
169
+ * 추가 필드 인코딩 원하면 override. transform/position 관련은 record 와 무관해 skip.
170
+ */
171
+ recordFromCarrier(carrier: Component, cellId: string): {
172
+ cellId: string;
173
+ [key: string]: any;
174
+ };
175
+ /**
176
+ * SlottedHolder 컨트랙 — slot 의 attach object3d 반환. SlotTarget 이 자기
177
+ * `_realObject.object3d` proxy 로 사용하고, Carriable.applyHolderAttachPoint 도
178
+ * 이걸 attach frame 으로 사용 (transit 중 carrier 가 slot 위치에 정렬).
179
+ */
180
+ getSlotAttachObject3d(cellId: string): THREE.Object3D | undefined;
181
+ /**
182
+ * SlottedHolder 컨트랙 — slot 의 *expected carrier* 의 3D 크기 (slot 자체의 기하 크기가
183
+ * 아님). Crane 의 `resolveCarrierBottomY = centerY - depth/2` 에서 *carrier 가 놓일 때
184
+ * 예상되는 carrier depth* 를 써야 fork 가 *carrier 바닥 = shelf* 에 정확히 진입.
185
+ *
186
+ * 즉:
187
+ * depth = stockD (= levelHeight * 0.7) — *carrier 의 vertical extent*. 전체 셀 높이
188
+ * (levelHeight) 가 아닌 *실제 stock 박스 깊이*. anchor 가 stock 시각 중심 (
189
+ * shelf + stockD/2) 에 위치하므로 depth = stockD 여야 bottom 계산이 shelf.
190
+ * width = bayWidth — 그대로
191
+ * height = rowDepth — 그대로 (2D 의 Z 축 폭)
192
+ */
193
+ getSlotSize(cellId: string): {
194
+ width: number;
195
+ height: number;
196
+ depth: number;
197
+ } | undefined;
198
+ /**
199
+ * SlottedHolder 컨트랙 — cellId 에 대한 SlotTarget. Mover.pickAndPlace 의 dest 로 넘김.
98
200
  *
99
- * In simulation mode, carriers become children of their RackCell,
100
- * and each RackCell provides its own attachPointFor(). So this method
101
- * is only invoked on direct-child carriers in monitoring mode — it
102
- * returns the rack's own object3d as the attach frame (default behavior).
201
+ * Signature overloads:
202
+ * slotTargetAt('0-0-5') string cellId 직접
203
+ * slotTargetAt(1, 1, 6) — 1-based (bay, row, level)
204
+ */
205
+ slotTargetAt(idOrBay: string | number, row?: number, level?: number): SlotTarget;
206
+ /**
207
+ * SlotTarget 의 2D center 위임 — Mover.moveTo 의 2D path 계산에 사용.
208
+ *
209
+ * 반환값은 *rack 자체의 local frame* (rack 의 left/top 미포함) — 즉 cell 의 위치를
210
+ * rack 의 *내부 좌표계로* 표현. SlotTarget.toScene 이 rack.toScene 을 위임해 *rack 의
211
+ * rotation / 부모 chain 변환 포함* 한 절대 좌표로 변환.
212
+ *
213
+ * 이전 결함: rack.left/top 을 포함해 model-layer 프레임 좌표 반환 + toScene 미구현 →
214
+ * rack 이 rotated 또는 nested 일 때 X 가 어긋났음.
215
+ */
216
+ cellCenter2D(cellId: string): {
217
+ x: number;
218
+ y: number;
219
+ } | null;
220
+ /**
221
+ * SlotTarget 의 toScene 위임 — *rack-local* 좌표를 *scene-absolute* 로 변환.
222
+ * rack.toScene 이 rack 의 rotation / translation / parent chain 모두 처리.
223
+ */
224
+ cellToScene(localX: number, localY: number): {
225
+ x: number;
226
+ y: number;
227
+ };
228
+ /** cellId 별 attach anchor object3d cache (rack.object3d 의 자식). */
229
+ private _attachAnchorByCell;
230
+ /**
231
+ * cellId 위치에 lightweight anchor object3d 를 *singleton 으로* 보장 + 갱신.
232
+ * 이 anchor 가:
233
+ * - Carriable.applyHolderAttachPoint 가 attach 하는 frame
234
+ * - SlotTarget._realObject.object3d 의 proxy
235
+ * - 두 용도가 *같은 object3d* 를 공유해 carrier 가 transient 동안 SlotTarget 의
236
+ * pose 와 정확히 동기화.
103
237
  */
104
- attachPointFor(_carrier: Component): AttachFrame | null;
238
+ private _ensureCellAttachObject3d;
105
239
  /**
106
240
  * 2D — top-down rectangle showing the rack footprint with bay subdivisions.
107
241
  * 편집/배치 가 가능하도록 *명시 fill + stroke* — pipeline 분기 무관하게 항상
@@ -109,6 +243,69 @@ export default class Rack extends Rack_base {
109
243
  */
110
244
  render(ctx: CanvasRenderingContext2D): void;
111
245
  get fillStyle(): string;
246
+ onchangeData(): void;
247
+ private _legendTarget?;
248
+ /**
249
+ * Legend 컴포넌트 lookup. 우선순위:
250
+ * 1) state.legendTarget id 명시
251
+ * 2) scene 전체에서 `type='legend'` 첫 번째 컴포넌트 (자동 발견)
252
+ */
253
+ get legendTarget(): Component | undefined;
254
+ private _onLegendChanged;
255
+ /**
256
+ * record 의 legend.field 값을 ranges 와 매칭해 색상 해석.
257
+ * - `range.value === recordValue` (카테고리 일치)
258
+ * - `range.min ≤ Number(v) < range.max` (수치 범위)
259
+ * - 매칭 없으면 `defaultColor` 또는 undefined
260
+ */
261
+ resolveLegendColor(record: any): string | undefined;
262
+ /**
263
+ * things-scene EventManager3D 가 raycast → object3d.userData.context.component 의
264
+ * `trigger("click", mouseEvent)` 을 호출 → eventMap 으로 receive.
265
+ * `(self).(self).click` 으로 등록해 *우리 rack 의 어떤 mesh 든 클릭됐을 때* 발사.
266
+ */
267
+ get eventMap(): {
268
+ '(self)': {
269
+ '(self)': {
270
+ click: (mouseEvent: MouseEvent) => void;
271
+ };
272
+ };
273
+ };
274
+ private _onRackClick;
275
+ /**
276
+ * state.popupRef 가 가리키는 Popup 컴포넌트를 invoke. anchor 를 SlotTarget 으로
277
+ * 지정 — SlotTarget._realObject.object3d 가 cellId 위치의 anchor object3d 를
278
+ * 가리켜 tether / projectToScreen 정확.
279
+ *
280
+ * - popupRef 미설정 → no-op (event 만 발사된 상태로 남음)
281
+ * - 다른 cell 클릭 시 popup 이 새 anchor 로 "이동" (Popup 의 board 등 설정 유지)
282
+ * - frame/empty 영역 클릭 시 호출 안 됨 → popup 그대로 유지
283
+ * - 명시적 close 버튼은 popup 자체의 closable 옵션이 처리
284
+ */
285
+ private _invokePopup;
286
+ /**
287
+ * 클릭 시 framework 의 mouse NDC (이미 InteractionManager 가 set 한 상태) 를 재사용해
288
+ * raycast → *우리 rack* 의 어떤 mesh 가 closest hit 인지 반환. 다른 object 가 더 가까우면
289
+ * undefined (다른 rack 또는 무관 mesh 의 hit).
290
+ *
291
+ * 접근 경로:
292
+ * 1. ThreeCapability — ModelLayer 는 `_threeCapability`, ThreeContainer 는 `_capability`.
293
+ * capability 의 `getObjectsByRaycast()` 가 *동일한* mouse NDC 로 framework 가 click
294
+ * 처리 직전에 쓴 그 raycaster 를 재사용 (가장 정확).
295
+ * 2. capability 가 없는 컨테이너 — public scene3d / renderer3d / camera + mouseEvent
296
+ * 좌표로 자체 ndc 변환 후 fresh raycaster.
297
+ */
298
+ private _raycastRackHit;
299
+ /**
300
+ * world point → cellId 역산.
301
+ *
302
+ * 1. rack 의 `matrixWorld.invert()` 로 world → rack-local 변환 (rack 의 회전·이동
303
+ * 반영)
304
+ * 2. rack-local point 의 `(x, y)` 를 (bay, level) 격자에 매핑
305
+ *
306
+ * 범위 밖이면 `"out-of-bounds(...)"` 문자열 반환 (caller 가 무시).
307
+ */
308
+ private _cellIdFromWorldPoint;
112
309
  buildRealObject(): RealObject | undefined;
113
310
  }
114
311
  export {};