@operato/scene-storage 10.0.0-beta.43 → 10.0.0-beta.46

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 (50) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/box-3d.d.ts +2 -0
  3. package/dist/box-3d.js +103 -64
  4. package/dist/box-3d.js.map +1 -1
  5. package/dist/crane-3d.d.ts +10 -0
  6. package/dist/crane-3d.js +34 -5
  7. package/dist/crane-3d.js.map +1 -1
  8. package/dist/crane.d.ts +136 -6
  9. package/dist/crane.js +567 -46
  10. package/dist/crane.js.map +1 -1
  11. package/dist/pallet-3d.d.ts +2 -0
  12. package/dist/pallet-3d.js +103 -53
  13. package/dist/pallet-3d.js.map +1 -1
  14. package/dist/parcel-3d.d.ts +1 -0
  15. package/dist/parcel-3d.js +18 -1
  16. package/dist/parcel-3d.js.map +1 -1
  17. package/dist/rack-grid-3d.js +26 -8
  18. package/dist/rack-grid-3d.js.map +1 -1
  19. package/dist/rack-grid.d.ts +94 -10
  20. package/dist/rack-grid.js +468 -86
  21. package/dist/rack-grid.js.map +1 -1
  22. package/dist/storage-rack-3d.js +1 -1
  23. package/dist/storage-rack-3d.js.map +1 -1
  24. package/dist/storage-rack.d.ts +31 -6
  25. package/dist/storage-rack.js +96 -14
  26. package/dist/storage-rack.js.map +1 -1
  27. package/package.json +3 -3
  28. package/src/box-3d.ts +121 -68
  29. package/src/crane-3d.ts +34 -4
  30. package/src/crane.ts +615 -55
  31. package/src/pallet-3d.ts +122 -55
  32. package/src/parcel-3d.ts +19 -1
  33. package/src/rack-grid-3d.ts +31 -8
  34. package/src/rack-grid.ts +488 -82
  35. package/src/storage-rack-3d.ts +1 -1
  36. package/src/storage-rack.ts +96 -14
  37. package/test/test-coord-alignment.ts +2 -2
  38. package/test/test-crane-bay-match.ts +130 -0
  39. package/test/test-crane-binding-resolve.ts +168 -0
  40. package/test/test-crane-duration.ts +90 -0
  41. package/test/test-crane-rotation-reach.ts +218 -0
  42. package/test/test-rack-grid-3d-alignment.ts +235 -0
  43. package/test/test-rack-grid-3d-attach-real.ts +375 -0
  44. package/test/test-rack-grid-cell.ts +2 -2
  45. package/test/test-rack-grid-location.ts +2 -2
  46. package/test/test-rack-grid-occupied-slots.ts +165 -0
  47. package/test/test-rack-grid-picking-position.ts +154 -0
  48. package/test/test-rack-grid-slot-api.ts +483 -0
  49. package/test/test-slot-ids-enumeration.ts +137 -0
  50. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,43 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [10.0.0-beta.46](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.45...v10.0.0-beta.46) (2026-05-24)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * capability 기반 simulate + binding + RackGrid 셀 리스트 정보 정확화 ([3dbcf66](https://github.com/things-scene/operato-scene/commit/3dbcf6645a1d0d6212523a85197b357380f9815a))
12
+ * **crane:** capability 기반 자동 simulate — 인접 SlottedHolder 의 slot ([16bba83](https://github.com/things-scene/operato-scene/commit/16bba83ecf1cc678fcaee78f04542e50f2bebd14))
13
+ * **crane:** Phase Z PR-2 — dispatcher 명시 작업 통합 main loop ([08f8e65](https://github.com/things-scene/operato-scene/commit/08f8e65cdc038c4dd15244c603c0bd002431747f))
14
+ * **slotted-holder:** occupiedSlotIds/emptySlotIds 에 filter predicate 추가 ([5adecd5](https://github.com/things-scene/operato-scene/commit/5adecd5c87eee2dc12d50c72e0842aec222b9084))
15
+ * **slotted-holder:** slotIds() capability — slot enumeration entry point ([a9de95a](https://github.com/things-scene/operato-scene/commit/a9de95a04dde1758fe2236d80dd177ffad9307ed))
16
+
17
+
18
+ ### :bug: Bug Fix
19
+
20
+ * **crane:** bayKey = first segment (col), reach 박스 X 를 rail 폭으로 정정 ([b3a10e9](https://github.com/things-scene/operato-scene/commit/b3a10e96da0ce1739f8420df0f70210094d90f35))
21
+ * **crane:** rail axis = matrixWorld X — 2D rotation 매핑 무관 + duration 거리 비례 ([4944ea7](https://github.com/things-scene/operato-scene/commit/4944ea7a87d5c9205387ef8272e3d6cafb2a2c74))
22
+ * **crane:** reach 박스 = rail 전체 X + fork 최대 extend Z, row 별 대표 검사 ([9fb88e7](https://github.com/things-scene/operato-scene/commit/9fb88e7a7c7ca8e612a07692f3e399d2a9793ee5))
23
+ * **crane:** 마지막 shelf 도달 못 하는 carriageHeight clamp 의 magic 0.85 제거 ([e6a9c1a](https://github.com/things-scene/operato-scene/commit/e6a9c1af1b762f874ad7a584137efe9fba120fa4))
24
+ * **rack-grid:** get layout() 을 TABLE_LAYOUT 으로 복원 — isEmpty 시각 회귀 차단 ([82d9d79](https://github.com/things-scene/operato-scene/commit/82d9d79d4862658faba96ca8171a8ba1a4ae9552))
25
+ * **storage:** RackGrid SlottedHolder 4 메서드 자체 처리 + NaN safety + carrier carried placement ([84f0392](https://github.com/things-scene/operato-scene/commit/84f0392a6d3093c9a93efa15682538719d4a77e1))
26
+ * **storage:** RackGrid 의 handoff carrier follow + visible size 회복 ([22f51e8](https://github.com/things-scene/operato-scene/commit/22f51e8d8813c9e20d765ac87a687771e4d329fa))
27
+
28
+
29
+ ### :house: Code Refactoring
30
+
31
+ * **storage:** public/private helper 이름의 cell* → slot* rename ([9de4ea0](https://github.com/things-scene/operato-scene/commit/9de4ea07f39fb349f7b52831aa7d4c3c8165f435))
32
+
33
+
34
+
35
+ ## [10.0.0-beta.44](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.43...v10.0.0-beta.44) (2026-05-21)
36
+
37
+ **Note:** Version bump only for package @operato/scene-storage
38
+
39
+
40
+
41
+
42
+
6
43
  ## [10.0.0-beta.43](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.42...v10.0.0-beta.43) (2026-05-21)
7
44
 
8
45
  **Note:** Version bump only for package @operato/scene-storage
package/dist/box-3d.d.ts CHANGED
@@ -3,8 +3,10 @@ export declare class Box3D extends RealObjectGroup {
3
3
  build(): void;
4
4
  /** Wood crate — visible slats, 4 corner posts, open top. */
5
5
  private buildWoodCrate;
6
+ private getWoodGeometries;
6
7
  /** Plastic tote — solid molded walls + top stackable lip. */
7
8
  private buildPlasticTote;
9
+ private getPlasticGeometries;
8
10
  updateDimension(): void;
9
11
  onchange(after: Record<string, unknown>, before: Record<string, unknown>): void;
10
12
  updateAlpha(): void;
package/dist/box-3d.js CHANGED
@@ -14,6 +14,49 @@
14
14
  import * as THREE from 'three';
15
15
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
16
16
  import { RealObjectGroup } from '@hatiolab/things-scene';
17
+ // ── Material cache by bodyColor (shared across all Box3D instances) ──
18
+ const woodBodyMaterials = new Map();
19
+ const woodPostMaterials = new Map();
20
+ const plasticToteMaterials = new Map();
21
+ const plasticLipMaterials = new Map();
22
+ function getWoodBodyMaterial(bodyColor) {
23
+ let m = woodBodyMaterials.get(bodyColor);
24
+ if (!m) {
25
+ m = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0, roughness: 0.85 });
26
+ woodBodyMaterials.set(bodyColor, m);
27
+ }
28
+ return m;
29
+ }
30
+ function getWoodPostMaterial(bodyColor) {
31
+ let m = woodPostMaterials.get(bodyColor);
32
+ if (!m) {
33
+ const tint = new THREE.Color(bodyColor).multiplyScalar(0.8);
34
+ m = new THREE.MeshStandardMaterial({ color: tint, metalness: 0, roughness: 0.9 });
35
+ woodPostMaterials.set(bodyColor, m);
36
+ }
37
+ return m;
38
+ }
39
+ function getPlasticToteMaterial(bodyColor) {
40
+ let m = plasticToteMaterials.get(bodyColor);
41
+ if (!m) {
42
+ m = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.05, roughness: 0.55 });
43
+ plasticToteMaterials.set(bodyColor, m);
44
+ }
45
+ return m;
46
+ }
47
+ function getPlasticLipMaterial(bodyColor) {
48
+ let m = plasticLipMaterials.get(bodyColor);
49
+ if (!m) {
50
+ const tint = new THREE.Color(bodyColor).multiplyScalar(0.85);
51
+ m = new THREE.MeshStandardMaterial({ color: tint, metalness: 0.05, roughness: 0.55 });
52
+ plasticLipMaterials.set(bodyColor, m);
53
+ }
54
+ return m;
55
+ }
56
+ // ── Geometry cache by size (shared across all Box3D instances) ──
57
+ // Floor geo는 local 좌표 + mesh position 으로 두어 mesh 별 position 만 다름.
58
+ const woodGeoCache = new Map();
59
+ const plasticGeoCache = new Map();
17
60
  export class Box3D extends RealObjectGroup {
18
61
  build() {
19
62
  super.build();
@@ -29,24 +72,32 @@ export class Box3D extends RealObjectGroup {
29
72
  }
30
73
  /** Wood crate — visible slats, 4 corner posts, open top. */
31
74
  buildWoodCrate(width, height, depth, bodyColor) {
75
+ const { posts, slats, floor, floorY } = this.getWoodGeometries(width, height, depth);
76
+ const woodMaterial = getWoodBodyMaterial(bodyColor);
77
+ const postMaterial = getWoodPostMaterial(bodyColor);
78
+ const postMesh = new THREE.Mesh(posts, postMaterial);
79
+ postMesh.castShadow = true;
80
+ this.object3d.add(postMesh);
81
+ const slatMesh = new THREE.Mesh(slats, woodMaterial);
82
+ slatMesh.castShadow = true;
83
+ slatMesh.receiveShadow = true;
84
+ this.object3d.add(slatMesh);
85
+ const floorMesh = new THREE.Mesh(floor, woodMaterial);
86
+ floorMesh.position.set(0, floorY, 0);
87
+ floorMesh.receiveShadow = true;
88
+ this.object3d.add(floorMesh);
89
+ }
90
+ getWoodGeometries(width, height, depth) {
91
+ const key = `${width}|${height}|${depth}`;
92
+ let cached = woodGeoCache.get(key);
93
+ if (cached)
94
+ return cached;
32
95
  const baseY = -depth / 2;
33
96
  const wallThickness = Math.min(width, height) * 0.04;
34
97
  const postW = wallThickness * 1.6;
35
98
  const slatH = depth * 0.10;
36
99
  const slatGap = slatH * 0.6;
37
100
  const floorH = depth * 0.05;
38
- const woodMaterial = new THREE.MeshStandardMaterial({
39
- color: bodyColor,
40
- metalness: 0,
41
- roughness: 0.85
42
- });
43
- const postColor = new THREE.Color(bodyColor).multiplyScalar(0.8);
44
- const postMaterial = new THREE.MeshStandardMaterial({
45
- color: postColor,
46
- metalness: 0,
47
- roughness: 0.9
48
- });
49
- // ── 4 corner posts ───────────────────────────────────────────────
50
101
  const postGeos = [];
51
102
  for (const xSign of [-1, 1]) {
52
103
  for (const zSign of [-1, 1]) {
@@ -55,99 +106,87 @@ export class Box3D extends RealObjectGroup {
55
106
  postGeos.push(post);
56
107
  }
57
108
  }
58
- const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), postMaterial);
59
- postMesh.castShadow = true;
60
- this.object3d.add(postMesh);
61
- // ── Slatted walls (horizontal slats with gaps) ───────────────────
109
+ const posts = BufferGeometryUtils.mergeGeometries(postGeos);
62
110
  const slatRowCount = Math.max(2, Math.floor((depth - floorH) / (slatH + slatGap)));
63
111
  const slatGeos = [];
64
112
  for (let row = 0; row < slatRowCount; row++) {
65
113
  const y = baseY + floorH + slatGap + row * (slatH + slatGap) + slatH / 2;
66
- // Long walls (front / back)
67
114
  for (const zSign of [-1, 1]) {
68
115
  const slat = new THREE.BoxGeometry(width - postW * 2, slatH, wallThickness);
69
116
  slat.translate(0, y, zSign * (height / 2 - wallThickness / 2));
70
117
  slatGeos.push(slat);
71
118
  }
72
- // Short walls (left / right)
73
119
  for (const xSign of [-1, 1]) {
74
120
  const slat = new THREE.BoxGeometry(wallThickness, slatH, height - postW * 2);
75
121
  slat.translate(xSign * (width / 2 - wallThickness / 2), y, 0);
76
122
  slatGeos.push(slat);
77
123
  }
78
124
  }
79
- const slatMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(slatGeos), woodMaterial);
80
- slatMesh.castShadow = true;
81
- slatMesh.receiveShadow = true;
82
- this.object3d.add(slatMesh);
83
- // ── Floor (bottom panel) ─────────────────────────────────────────
84
- const floorGeo = new THREE.BoxGeometry(width - postW * 2, floorH, height - postW * 2);
85
- const floorMesh = new THREE.Mesh(floorGeo, woodMaterial);
86
- floorMesh.position.set(0, baseY + floorH / 2, 0);
87
- floorMesh.receiveShadow = true;
88
- this.object3d.add(floorMesh);
125
+ const slats = BufferGeometryUtils.mergeGeometries(slatGeos);
126
+ const floor = new THREE.BoxGeometry(width - postW * 2, floorH, height - postW * 2);
127
+ const floorY = baseY + floorH / 2;
128
+ cached = { posts, slats, floor, floorY };
129
+ woodGeoCache.set(key, cached);
130
+ return cached;
89
131
  }
90
132
  /** Plastic tote — solid molded walls + top stackable lip. */
91
133
  buildPlasticTote(width, height, depth, bodyColor) {
134
+ const { walls, lip, floor, floorY } = this.getPlasticGeometries(width, height, depth);
135
+ const totMaterial = getPlasticToteMaterial(bodyColor);
136
+ const lipMaterial = getPlasticLipMaterial(bodyColor);
137
+ const wallMesh = new THREE.Mesh(walls, totMaterial);
138
+ wallMesh.castShadow = true;
139
+ wallMesh.receiveShadow = true;
140
+ this.object3d.add(wallMesh);
141
+ const lipMesh = new THREE.Mesh(lip, lipMaterial);
142
+ lipMesh.castShadow = true;
143
+ this.object3d.add(lipMesh);
144
+ const floorMesh = new THREE.Mesh(floor, totMaterial);
145
+ floorMesh.position.set(0, floorY, 0);
146
+ floorMesh.receiveShadow = true;
147
+ this.object3d.add(floorMesh);
148
+ }
149
+ getPlasticGeometries(width, height, depth) {
150
+ const key = `${width}|${height}|${depth}`;
151
+ let cached = plasticGeoCache.get(key);
152
+ if (cached)
153
+ return cached;
92
154
  const baseY = -depth / 2;
93
155
  const wallThickness = Math.min(width, height) * 0.05;
94
156
  const lipH = depth * 0.06;
95
157
  const floorH = depth * 0.06;
96
- const totMaterial = new THREE.MeshStandardMaterial({
97
- color: bodyColor,
98
- metalness: 0.05,
99
- roughness: 0.55
100
- });
101
- const lipColor = new THREE.Color(bodyColor).multiplyScalar(0.85);
102
- const lipMaterial = new THREE.MeshStandardMaterial({
103
- color: lipColor,
104
- metalness: 0.05,
105
- roughness: 0.55
106
- });
107
- // ── 4 solid walls ────────────────────────────────────────────────
108
158
  const wallGeos = [];
109
159
  const wallH = depth - lipH - floorH;
110
160
  const wallY = baseY + floorH + wallH / 2;
111
- // Long walls
112
161
  for (const zSign of [-1, 1]) {
113
162
  const wall = new THREE.BoxGeometry(width, wallH, wallThickness);
114
163
  wall.translate(0, wallY, zSign * (height / 2 - wallThickness / 2));
115
164
  wallGeos.push(wall);
116
165
  }
117
- // Short walls
118
166
  for (const xSign of [-1, 1]) {
119
167
  const wall = new THREE.BoxGeometry(wallThickness, wallH, height - 2 * wallThickness);
120
168
  wall.translate(xSign * (width / 2 - wallThickness / 2), wallY, 0);
121
169
  wallGeos.push(wall);
122
170
  }
123
- const wallMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wallGeos), totMaterial);
124
- wallMesh.castShadow = true;
125
- wallMesh.receiveShadow = true;
126
- this.object3d.add(wallMesh);
127
- // ── Top lip (stackable rim — slightly wider than walls) ──────────
171
+ const walls = BufferGeometryUtils.mergeGeometries(wallGeos);
128
172
  const lipGeos = [];
129
173
  const lipY = baseY + depth - lipH / 2;
130
- // Long sides
131
174
  for (const zSign of [-1, 1]) {
132
- const lip = new THREE.BoxGeometry(width * 1.02, lipH, wallThickness * 1.5);
133
- lip.translate(0, lipY, zSign * (height / 2 - wallThickness * 0.75));
134
- lipGeos.push(lip);
175
+ const lipG = new THREE.BoxGeometry(width * 1.02, lipH, wallThickness * 1.5);
176
+ lipG.translate(0, lipY, zSign * (height / 2 - wallThickness * 0.75));
177
+ lipGeos.push(lipG);
135
178
  }
136
- // Short sides
137
179
  for (const xSign of [-1, 1]) {
138
- const lip = new THREE.BoxGeometry(wallThickness * 1.5, lipH, height * 1.02 - 2 * wallThickness * 1.5);
139
- lip.translate(xSign * (width / 2 - wallThickness * 0.75), lipY, 0);
140
- lipGeos.push(lip);
180
+ const lipG = new THREE.BoxGeometry(wallThickness * 1.5, lipH, height * 1.02 - 2 * wallThickness * 1.5);
181
+ lipG.translate(xSign * (width / 2 - wallThickness * 0.75), lipY, 0);
182
+ lipGeos.push(lipG);
141
183
  }
142
- const lipMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(lipGeos), lipMaterial);
143
- lipMesh.castShadow = true;
144
- this.object3d.add(lipMesh);
145
- // ── Floor (solid bottom) ─────────────────────────────────────────
146
- const floorGeo = new THREE.BoxGeometry(width, floorH, height);
147
- const floorMesh = new THREE.Mesh(floorGeo, totMaterial);
148
- floorMesh.position.set(0, baseY + floorH / 2, 0);
149
- floorMesh.receiveShadow = true;
150
- this.object3d.add(floorMesh);
184
+ const lip = BufferGeometryUtils.mergeGeometries(lipGeos);
185
+ const floor = new THREE.BoxGeometry(width, floorH, height);
186
+ const floorY = baseY + floorH / 2;
187
+ cached = { walls, lip, floor, floorY };
188
+ plasticGeoCache.set(key, cached);
189
+ return cached;
151
190
  }
152
191
  updateDimension() { }
153
192
  onchange(after, before) {
@@ -1 +1 @@
1
- {"version":3,"file":"box-3d.js","sourceRoot":"","sources":["../src/box-3d.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,mBAAmB,MAAM,iDAAiD,CAAA;AACtF,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,MAAM,OAAO,KAAM,SAAQ,eAAe;IACxC,KAAK;QACH,KAAK,CAAC,KAAK,EAAE,CAAA;QAEb,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAG,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAA;QAC3D,MAAM,QAAQ,GAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAmB,IAAI,MAAM,CAAA;QACpE,MAAM,SAAS,GAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,SAAoB,IAAI,SAAS,CAAA;QAEzE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;QACxD,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;QACtD,CAAC;IACH,CAAC;IAED,4DAA4D;IACpD,cAAc,CAAC,KAAa,EAAE,MAAc,EAAE,KAAa,EAAE,SAAiB;QACpF,MAAM,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,CAAA;QACxB,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;QACpD,MAAM,KAAK,GAAG,aAAa,GAAG,GAAG,CAAA;QACjC,MAAM,KAAK,GAAG,KAAK,GAAG,IAAI,CAAA;QAC1B,MAAM,OAAO,GAAG,KAAK,GAAG,GAAG,CAAA;QAC3B,MAAM,MAAM,GAAG,KAAK,GAAG,IAAI,CAAA;QAE3B,MAAM,YAAY,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YAClD,KAAK,EAAE,SAAS;YAChB,SAAS,EAAE,CAAC;YACZ,SAAS,EAAE,IAAI;SAChB,CAAC,CAAA;QACF,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,CAAA;QAChE,MAAM,YAAY,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YAClD,KAAK,EAAE,SAAS;YAChB,SAAS,EAAE,CAAC;YACZ,SAAS,EAAE,GAAG;SACf,CAAC,CAAA;QAEF,oEAAoE;QACpE,MAAM,QAAQ,GAA2B,EAAE,CAAA;QAC3C,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5B,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;gBACvD,IAAI,CAAC,SAAS,CACZ,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,EAC/B,KAAK,GAAG,KAAK,GAAG,CAAC,EACjB,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CACjC,CAAA;gBACD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACrB,CAAC;QACH,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,CAAA;QAC5F,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;QAC1B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAE3B,oEAAoE;QACpE,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;QAClF,MAAM,QAAQ,GAA2B,EAAE,CAAA;QAE3C,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,YAAY,EAAE,GAAG,EAAE,EAAE,CAAC;YAC5C,MAAM,CAAC,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,GAAG,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC,GAAG,KAAK,GAAG,CAAC,CAAA;YACxE,4BAA4B;YAC5B,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,GAAG,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAA;gBAC3E,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,aAAa,GAAG,CAAC,CAAC,CAAC,CAAA;gBAC9D,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACrB,CAAC;YACD,6BAA6B;YAC7B,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,GAAG,CAAC,CAAC,CAAA;gBAC5E,IAAI,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,aAAa,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;gBAC7D,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACrB,CAAC;QACH,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,CAAA;QAC5F,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;QAC1B,QAAQ,CAAC,aAAa,GAAG,IAAI,CAAA;QAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAE3B,oEAAoE;QACpE,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,GAAG,KAAK,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,CAAC,CAAC,CAAA;QACrF,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAA;QACxD,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QAChD,SAAS,CAAC,aAAa,GAAG,IAAI,CAAA;QAC9B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAC9B,CAAC;IAED,6DAA6D;IACrD,gBAAgB,CAAC,KAAa,EAAE,MAAc,EAAE,KAAa,EAAE,SAAiB;QACtF,MAAM,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,CAAA;QACxB,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;QACpD,MAAM,IAAI,GAAG,KAAK,GAAG,IAAI,CAAA;QACzB,MAAM,MAAM,GAAG,KAAK,GAAG,IAAI,CAAA;QAE3B,MAAM,WAAW,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YACjD,KAAK,EAAE,SAAS;YAChB,SAAS,EAAE,IAAI;YACf,SAAS,EAAE,IAAI;SAChB,CAAC,CAAA;QACF,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAA;QAChE,MAAM,WAAW,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YACjD,KAAK,EAAE,QAAQ;YACf,SAAS,EAAE,IAAI;YACf,SAAS,EAAE,IAAI;SAChB,CAAC,CAAA;QAEF,oEAAoE;QACpE,MAAM,QAAQ,GAA2B,EAAE,CAAA;QAC3C,MAAM,KAAK,GAAG,KAAK,GAAG,IAAI,GAAG,MAAM,CAAA;QACnC,MAAM,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,CAAC,CAAA;QAExC,aAAa;QACb,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,aAAa,CAAC,CAAA;YAC/D,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,aAAa,GAAG,CAAC,CAAC,CAAC,CAAA;YAClE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACrB,CAAC;QACD,cAAc;QACd,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,aAAa,CAAC,CAAA;YACpF,IAAI,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,aAAa,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;YACjE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACrB,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC,CAAA;QAC3F,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;QAC1B,QAAQ,CAAC,aAAa,GAAG,IAAI,CAAA;QAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAE3B,oEAAoE;QACpE,MAAM,OAAO,GAA2B,EAAE,CAAA;QAC1C,MAAM,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,GAAG,CAAC,CAAA;QACrC,aAAa;QACb,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5B,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,GAAG,IAAI,EAAE,IAAI,EAAE,aAAa,GAAG,GAAG,CAAC,CAAA;YAC1E,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC,CAAA;YACnE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACnB,CAAC;QACD,cAAc;QACd,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5B,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,aAAa,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,aAAa,GAAG,GAAG,CAAC,CAAA;YACrG,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,aAAa,GAAG,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;YAClE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACnB,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC,CAAA;QACzF,OAAO,CAAC,UAAU,GAAG,IAAI,CAAA;QACzB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAE1B,oEAAoE;QACpE,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7D,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;QACvD,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QAChD,SAAS,CAAC,aAAa,GAAG,IAAI,CAAA;QAC9B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAC9B,CAAC;IAED,eAAe,KAAI,CAAC;IAEpB,QAAQ,CAAC,KAA8B,EAAE,MAA+B;QACtE,IACE,UAAU,IAAI,KAAK;YACnB,WAAW,IAAI,KAAK;YACpB,OAAO,IAAI,KAAK;YAChB,QAAQ,IAAI,KAAK;YACjB,OAAO,IAAI,KAAK,EAChB,CAAC;YACD,IAAI,CAAC,MAAM,EAAE,CAAA;YACb,OAAM;QACR,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IAC/B,CAAC;IAED,WAAW,KAAI,CAAC;CACjB","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Box 3D — wood crate and plastic tote variants.\n *\n * Wood crate: 4 vertical corner posts + horizontal slats with gaps → the\n * typical industrial wooden crate look. Forklift-friendly.\n * Plastic tote: solid 4 walls + visible top lip / handle cutouts. Stackable.\n *\n * Both have a defined floor (so they look like containers, not just walls)\n * and an opening at top — as you'd expect from a real crate / tote that's\n * open or has a removable lid.\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\nexport class Box3D extends RealObjectGroup {\n build() {\n super.build()\n\n const { width, height, depth = 300 } = this.component.state\n const material = (this.component.state.material as string) || 'wood'\n const bodyColor = (this.component.state.bodyColor as string) || '#a87644'\n\n if (material === 'plastic') {\n this.buildPlasticTote(width, height, depth, bodyColor)\n } else {\n this.buildWoodCrate(width, height, depth, bodyColor)\n }\n }\n\n /** Wood crate — visible slats, 4 corner posts, open top. */\n private buildWoodCrate(width: number, height: number, depth: number, bodyColor: string) {\n const baseY = -depth / 2\n const wallThickness = Math.min(width, height) * 0.04\n const postW = wallThickness * 1.6\n const slatH = depth * 0.10\n const slatGap = slatH * 0.6\n const floorH = depth * 0.05\n\n const woodMaterial = new THREE.MeshStandardMaterial({\n color: bodyColor,\n metalness: 0,\n roughness: 0.85\n })\n const postColor = new THREE.Color(bodyColor).multiplyScalar(0.8)\n const postMaterial = new THREE.MeshStandardMaterial({\n color: postColor,\n metalness: 0,\n roughness: 0.9\n })\n\n // ── 4 corner posts ───────────────────────────────────────────────\n const postGeos: THREE.BufferGeometry[] = []\n for (const xSign of [-1, 1]) {\n for (const zSign of [-1, 1]) {\n const post = new THREE.BoxGeometry(postW, depth, postW)\n post.translate(\n xSign * (width / 2 - postW / 2),\n baseY + depth / 2,\n zSign * (height / 2 - postW / 2)\n )\n postGeos.push(post)\n }\n }\n const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), postMaterial)\n postMesh.castShadow = true\n this.object3d.add(postMesh)\n\n // ── Slatted walls (horizontal slats with gaps) ───────────────────\n const slatRowCount = Math.max(2, Math.floor((depth - floorH) / (slatH + slatGap)))\n const slatGeos: THREE.BufferGeometry[] = []\n\n for (let row = 0; row < slatRowCount; row++) {\n const y = baseY + floorH + slatGap + row * (slatH + slatGap) + slatH / 2\n // Long walls (front / back)\n for (const zSign of [-1, 1]) {\n const slat = new THREE.BoxGeometry(width - postW * 2, slatH, wallThickness)\n slat.translate(0, y, zSign * (height / 2 - wallThickness / 2))\n slatGeos.push(slat)\n }\n // Short walls (left / right)\n for (const xSign of [-1, 1]) {\n const slat = new THREE.BoxGeometry(wallThickness, slatH, height - postW * 2)\n slat.translate(xSign * (width / 2 - wallThickness / 2), y, 0)\n slatGeos.push(slat)\n }\n }\n const slatMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(slatGeos), woodMaterial)\n slatMesh.castShadow = true\n slatMesh.receiveShadow = true\n this.object3d.add(slatMesh)\n\n // ── Floor (bottom panel) ─────────────────────────────────────────\n const floorGeo = new THREE.BoxGeometry(width - postW * 2, floorH, height - postW * 2)\n const floorMesh = new THREE.Mesh(floorGeo, woodMaterial)\n floorMesh.position.set(0, baseY + floorH / 2, 0)\n floorMesh.receiveShadow = true\n this.object3d.add(floorMesh)\n }\n\n /** Plastic tote — solid molded walls + top stackable lip. */\n private buildPlasticTote(width: number, height: number, depth: number, bodyColor: string) {\n const baseY = -depth / 2\n const wallThickness = Math.min(width, height) * 0.05\n const lipH = depth * 0.06\n const floorH = depth * 0.06\n\n const totMaterial = new THREE.MeshStandardMaterial({\n color: bodyColor,\n metalness: 0.05,\n roughness: 0.55\n })\n const lipColor = new THREE.Color(bodyColor).multiplyScalar(0.85)\n const lipMaterial = new THREE.MeshStandardMaterial({\n color: lipColor,\n metalness: 0.05,\n roughness: 0.55\n })\n\n // ── 4 solid walls ────────────────────────────────────────────────\n const wallGeos: THREE.BufferGeometry[] = []\n const wallH = depth - lipH - floorH\n const wallY = baseY + floorH + wallH / 2\n\n // Long walls\n for (const zSign of [-1, 1]) {\n const wall = new THREE.BoxGeometry(width, wallH, wallThickness)\n wall.translate(0, wallY, zSign * (height / 2 - wallThickness / 2))\n wallGeos.push(wall)\n }\n // Short walls\n for (const xSign of [-1, 1]) {\n const wall = new THREE.BoxGeometry(wallThickness, wallH, height - 2 * wallThickness)\n wall.translate(xSign * (width / 2 - wallThickness / 2), wallY, 0)\n wallGeos.push(wall)\n }\n const wallMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wallGeos), totMaterial)\n wallMesh.castShadow = true\n wallMesh.receiveShadow = true\n this.object3d.add(wallMesh)\n\n // ── Top lip (stackable rim — slightly wider than walls) ──────────\n const lipGeos: THREE.BufferGeometry[] = []\n const lipY = baseY + depth - lipH / 2\n // Long sides\n for (const zSign of [-1, 1]) {\n const lip = new THREE.BoxGeometry(width * 1.02, lipH, wallThickness * 1.5)\n lip.translate(0, lipY, zSign * (height / 2 - wallThickness * 0.75))\n lipGeos.push(lip)\n }\n // Short sides\n for (const xSign of [-1, 1]) {\n const lip = new THREE.BoxGeometry(wallThickness * 1.5, lipH, height * 1.02 - 2 * wallThickness * 1.5)\n lip.translate(xSign * (width / 2 - wallThickness * 0.75), lipY, 0)\n lipGeos.push(lip)\n }\n const lipMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(lipGeos), lipMaterial)\n lipMesh.castShadow = true\n this.object3d.add(lipMesh)\n\n // ── Floor (solid bottom) ─────────────────────────────────────────\n const floorGeo = new THREE.BoxGeometry(width, floorH, height)\n const floorMesh = new THREE.Mesh(floorGeo, totMaterial)\n floorMesh.position.set(0, baseY + floorH / 2, 0)\n floorMesh.receiveShadow = true\n this.object3d.add(floorMesh)\n }\n\n updateDimension() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if (\n 'material' in after ||\n 'bodyColor' 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":"box-3d.js","sourceRoot":"","sources":["../src/box-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Box 3D — wood crate and plastic tote variants.\n *\n * Wood crate: 4 vertical corner posts + horizontal slats with gaps → the\n * typical industrial wooden crate look. Forklift-friendly.\n * Plastic tote: solid 4 walls + visible top lip / handle cutouts. Stackable.\n *\n * Both have a defined floor (so they look like containers, not just walls)\n * and an opening at top — as you'd expect from a real crate / tote that's\n * open or has a removable lid.\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\n// ── Material cache by bodyColor (shared across all Box3D instances) ──\nconst woodBodyMaterials = new Map<string, THREE.MeshStandardMaterial>()\nconst woodPostMaterials = new Map<string, THREE.MeshStandardMaterial>()\nconst plasticToteMaterials = new Map<string, THREE.MeshStandardMaterial>()\nconst plasticLipMaterials = new Map<string, THREE.MeshStandardMaterial>()\n\nfunction getWoodBodyMaterial(bodyColor: string): THREE.MeshStandardMaterial {\n let m = woodBodyMaterials.get(bodyColor)\n if (!m) {\n m = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0, roughness: 0.85 })\n woodBodyMaterials.set(bodyColor, m)\n }\n return m\n}\n\nfunction getWoodPostMaterial(bodyColor: string): THREE.MeshStandardMaterial {\n let m = woodPostMaterials.get(bodyColor)\n if (!m) {\n const tint = new THREE.Color(bodyColor).multiplyScalar(0.8)\n m = new THREE.MeshStandardMaterial({ color: tint, metalness: 0, roughness: 0.9 })\n woodPostMaterials.set(bodyColor, m)\n }\n return m\n}\n\nfunction getPlasticToteMaterial(bodyColor: string): THREE.MeshStandardMaterial {\n let m = plasticToteMaterials.get(bodyColor)\n if (!m) {\n m = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.05, roughness: 0.55 })\n plasticToteMaterials.set(bodyColor, m)\n }\n return m\n}\n\nfunction getPlasticLipMaterial(bodyColor: string): THREE.MeshStandardMaterial {\n let m = plasticLipMaterials.get(bodyColor)\n if (!m) {\n const tint = new THREE.Color(bodyColor).multiplyScalar(0.85)\n m = new THREE.MeshStandardMaterial({ color: tint, metalness: 0.05, roughness: 0.55 })\n plasticLipMaterials.set(bodyColor, m)\n }\n return m\n}\n\n// ── Geometry cache by size (shared across all Box3D instances) ──\n// Floor geo는 local 좌표 + mesh position 으로 두어 mesh 별 position 만 다름.\nconst woodGeoCache = new Map<string, { posts: THREE.BufferGeometry; slats: THREE.BufferGeometry; floor: THREE.BufferGeometry; floorY: number }>()\nconst plasticGeoCache = new Map<string, { walls: THREE.BufferGeometry; lip: THREE.BufferGeometry; floor: THREE.BufferGeometry; floorY: number }>()\n\nexport class Box3D extends RealObjectGroup {\n build() {\n super.build()\n\n const { width, height, depth = 300 } = this.component.state\n const material = (this.component.state.material as string) || 'wood'\n const bodyColor = (this.component.state.bodyColor as string) || '#a87644'\n\n if (material === 'plastic') {\n this.buildPlasticTote(width, height, depth, bodyColor)\n } else {\n this.buildWoodCrate(width, height, depth, bodyColor)\n }\n }\n\n /** Wood crate — visible slats, 4 corner posts, open top. */\n private buildWoodCrate(width: number, height: number, depth: number, bodyColor: string) {\n const { posts, slats, floor, floorY } = this.getWoodGeometries(width, height, depth)\n\n const woodMaterial = getWoodBodyMaterial(bodyColor)\n const postMaterial = getWoodPostMaterial(bodyColor)\n\n const postMesh = new THREE.Mesh(posts, postMaterial)\n postMesh.castShadow = true\n this.object3d.add(postMesh)\n\n const slatMesh = new THREE.Mesh(slats, woodMaterial)\n slatMesh.castShadow = true\n slatMesh.receiveShadow = true\n this.object3d.add(slatMesh)\n\n const floorMesh = new THREE.Mesh(floor, woodMaterial)\n floorMesh.position.set(0, floorY, 0)\n floorMesh.receiveShadow = true\n this.object3d.add(floorMesh)\n }\n\n private getWoodGeometries(width: number, height: number, depth: number) {\n const key = `${width}|${height}|${depth}`\n let cached = woodGeoCache.get(key)\n if (cached) return cached\n\n const baseY = -depth / 2\n const wallThickness = Math.min(width, height) * 0.04\n const postW = wallThickness * 1.6\n const slatH = depth * 0.10\n const slatGap = slatH * 0.6\n const floorH = depth * 0.05\n\n const postGeos: THREE.BufferGeometry[] = []\n for (const xSign of [-1, 1]) {\n for (const zSign of [-1, 1]) {\n const post = new THREE.BoxGeometry(postW, depth, postW)\n post.translate(\n xSign * (width / 2 - postW / 2),\n baseY + depth / 2,\n zSign * (height / 2 - postW / 2)\n )\n postGeos.push(post)\n }\n }\n const posts = BufferGeometryUtils.mergeGeometries(postGeos)\n\n const slatRowCount = Math.max(2, Math.floor((depth - floorH) / (slatH + slatGap)))\n const slatGeos: THREE.BufferGeometry[] = []\n for (let row = 0; row < slatRowCount; row++) {\n const y = baseY + floorH + slatGap + row * (slatH + slatGap) + slatH / 2\n for (const zSign of [-1, 1]) {\n const slat = new THREE.BoxGeometry(width - postW * 2, slatH, wallThickness)\n slat.translate(0, y, zSign * (height / 2 - wallThickness / 2))\n slatGeos.push(slat)\n }\n for (const xSign of [-1, 1]) {\n const slat = new THREE.BoxGeometry(wallThickness, slatH, height - postW * 2)\n slat.translate(xSign * (width / 2 - wallThickness / 2), y, 0)\n slatGeos.push(slat)\n }\n }\n const slats = BufferGeometryUtils.mergeGeometries(slatGeos)\n\n const floor = new THREE.BoxGeometry(width - postW * 2, floorH, height - postW * 2)\n const floorY = baseY + floorH / 2\n\n cached = { posts, slats, floor, floorY }\n woodGeoCache.set(key, cached)\n return cached\n }\n\n /** Plastic tote — solid molded walls + top stackable lip. */\n private buildPlasticTote(width: number, height: number, depth: number, bodyColor: string) {\n const { walls, lip, floor, floorY } = this.getPlasticGeometries(width, height, depth)\n\n const totMaterial = getPlasticToteMaterial(bodyColor)\n const lipMaterial = getPlasticLipMaterial(bodyColor)\n\n const wallMesh = new THREE.Mesh(walls, totMaterial)\n wallMesh.castShadow = true\n wallMesh.receiveShadow = true\n this.object3d.add(wallMesh)\n\n const lipMesh = new THREE.Mesh(lip, lipMaterial)\n lipMesh.castShadow = true\n this.object3d.add(lipMesh)\n\n const floorMesh = new THREE.Mesh(floor, totMaterial)\n floorMesh.position.set(0, floorY, 0)\n floorMesh.receiveShadow = true\n this.object3d.add(floorMesh)\n }\n\n private getPlasticGeometries(width: number, height: number, depth: number) {\n const key = `${width}|${height}|${depth}`\n let cached = plasticGeoCache.get(key)\n if (cached) return cached\n\n const baseY = -depth / 2\n const wallThickness = Math.min(width, height) * 0.05\n const lipH = depth * 0.06\n const floorH = depth * 0.06\n\n const wallGeos: THREE.BufferGeometry[] = []\n const wallH = depth - lipH - floorH\n const wallY = baseY + floorH + wallH / 2\n for (const zSign of [-1, 1]) {\n const wall = new THREE.BoxGeometry(width, wallH, wallThickness)\n wall.translate(0, wallY, zSign * (height / 2 - wallThickness / 2))\n wallGeos.push(wall)\n }\n for (const xSign of [-1, 1]) {\n const wall = new THREE.BoxGeometry(wallThickness, wallH, height - 2 * wallThickness)\n wall.translate(xSign * (width / 2 - wallThickness / 2), wallY, 0)\n wallGeos.push(wall)\n }\n const walls = BufferGeometryUtils.mergeGeometries(wallGeos)\n\n const lipGeos: THREE.BufferGeometry[] = []\n const lipY = baseY + depth - lipH / 2\n for (const zSign of [-1, 1]) {\n const lipG = new THREE.BoxGeometry(width * 1.02, lipH, wallThickness * 1.5)\n lipG.translate(0, lipY, zSign * (height / 2 - wallThickness * 0.75))\n lipGeos.push(lipG)\n }\n for (const xSign of [-1, 1]) {\n const lipG = new THREE.BoxGeometry(wallThickness * 1.5, lipH, height * 1.02 - 2 * wallThickness * 1.5)\n lipG.translate(xSign * (width / 2 - wallThickness * 0.75), lipY, 0)\n lipGeos.push(lipG)\n }\n const lip = BufferGeometryUtils.mergeGeometries(lipGeos)\n\n const floor = new THREE.BoxGeometry(width, floorH, height)\n const floorY = baseY + floorH / 2\n\n cached = { walls, lip, floor, floorY }\n plasticGeoCache.set(key, cached)\n return cached\n }\n\n updateDimension() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if (\n 'material' in after ||\n 'bodyColor' 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,6 +1,16 @@
1
1
  import * as THREE from 'three';
2
2
  import { RealObjectGroup } from '@hatiolab/things-scene';
3
3
  export declare class Crane3D extends RealObjectGroup {
4
+ /**
5
+ * carriageHeight 의 실제 최댓값 — *mast 의 carriage 이동 가능 범위* 기반.
6
+ * mastH = D - railH * 2 - baseH - topFrameH - topGuideH (mast 의 실제 길이).
7
+ * carriage 의 *top edge 가 mast top 안에 머물도록* carriageH/2 추가 차감.
8
+ *
9
+ * 이전 D * 0.85 magic 대체 — base/top frame 의 실제 차지 비율과 무관한 추정값
10
+ * 이라 crane.depth = rack.depth 동일 모델에서도 carriage 가 rack 상위 cell
11
+ * 도달 못 함. 정확한 mastH 사용으로 *동일 size 모델에서 정상 도달* 보장.
12
+ */
13
+ static _maxCarriageHeight(D: number, width: number, height: number): number;
4
14
  private _forkGroup?;
5
15
  private _carrierBaseY;
6
16
  private _bladeMidZ;
package/dist/crane-3d.js CHANGED
@@ -30,7 +30,7 @@
30
30
  * carriageHeight, forkExtension (±), forkLift
31
31
  */
32
32
  import * as THREE from 'three';
33
- import { RealObject, RealObjectGroup } from '@hatiolab/things-scene';
33
+ import { RealObjectGroup } from '@hatiolab/things-scene';
34
34
  const MAST_COLOR = 0xff7a00; // mast — orange
35
35
  const TROLLEY_COLOR = 0x3a4048; // base / top — dark charcoal
36
36
  const CARRIAGE_COLOR = 0xffcc00; // carriage (shuttle) — yellow
@@ -39,6 +39,25 @@ const FORK_COLOR = 0xffcc00; // fork — yellow (same as carriage)
39
39
  const RAIL_COLOR = 0x1a1f24; // rail — dark steel
40
40
  const LAMP_OFF = 0x222222;
41
41
  export class Crane3D extends RealObjectGroup {
42
+ /**
43
+ * carriageHeight 의 실제 최댓값 — *mast 의 carriage 이동 가능 범위* 기반.
44
+ * mastH = D - railH * 2 - baseH - topFrameH - topGuideH (mast 의 실제 길이).
45
+ * carriage 의 *top edge 가 mast top 안에 머물도록* carriageH/2 추가 차감.
46
+ *
47
+ * 이전 D * 0.85 magic 대체 — base/top frame 의 실제 차지 비율과 무관한 추정값
48
+ * 이라 crane.depth = rack.depth 동일 모델에서도 carriage 가 rack 상위 cell
49
+ * 도달 못 함. 정확한 mastH 사용으로 *동일 size 모델에서 정상 도달* 보장.
50
+ */
51
+ static _maxCarriageHeight(D, width, height) {
52
+ const S = Math.min(width, height);
53
+ const railH = S * 0.04;
54
+ const baseH = S * 0.18;
55
+ const topFrameH = S * 0.1;
56
+ const topGuideH = S * 0.1;
57
+ const carriageH = S * 0.12;
58
+ const mastH = Math.max(D - railH * 2 - baseH - topFrameH - topGuideH, S * 0.5);
59
+ return Math.max(0, mastH - carriageH / 2);
60
+ }
42
61
  _forkGroup;
43
62
  _carrierBaseY = 0;
44
63
  _bladeMidZ = 0;
@@ -72,7 +91,12 @@ export class Crane3D extends RealObjectGroup {
72
91
  // Actuators
73
92
  const D = numOr(depth, Math.max(width, height) * 4);
74
93
  const carriageRaw = numOr(this.component.state.carriageHeight, this.component._canonicalDefault?.('carriageHeight') ?? D * 0.4);
75
- const carriageHeight = Math.max(0, Math.min(carriageRaw, D * 0.85));
94
+ // carriageHeight clamp 실제 mast carriage 이동 가능 범위 기반. helper 사용
95
+ // (render + onchange partial update 둘 다 동일 식). 이전엔 D * 0.85 magic 사용 —
96
+ // base/top frame 의 실제 차지 비율과 무관한 추정. crane.depth = rack.depth 동일
97
+ // 모델에서도 carriage 가 rack 상위 cell 도달 불가 → 14 levels rack 의 *마지막
98
+ // shelf 만 한 층 아래에서 포킹* 증상의 root cause 였음.
99
+ const carriageHeight = Math.max(0, Math.min(carriageRaw, Crane3D._maxCarriageHeight(D, width, height)));
76
100
  const forkLength = numOr(this.component.state.forkLength, height * 0.6);
77
101
  const forkExtensionRaw = numOr(this.component.state.forkExtension, 0);
78
102
  const forkExtension = Math.max(-forkLength, Math.min(forkLength, forkExtensionRaw));
@@ -322,9 +346,11 @@ export class Crane3D extends RealObjectGroup {
322
346
  let meshUpdated = false;
323
347
  if (('carriageHeight' in after || 'forkLiftRT' in after) && this._carriageLiftGroup) {
324
348
  const state = this.component.state;
325
- const D = numOr(state.depth, Math.max(numOr(state.width, 100), numOr(state.height, 100)) * 4);
349
+ const W = numOr(state.width, 100);
350
+ const H = numOr(state.height, 100);
351
+ const D = numOr(state.depth, Math.max(W, H) * 4);
326
352
  const carriageRaw = numOr(state.carriageHeight, D * 0.4);
327
- const carriageHeight = Math.max(0, Math.min(carriageRaw, D * 0.85));
353
+ const carriageHeight = Math.max(0, Math.min(carriageRaw, Crane3D._maxCarriageHeight(D, W, H)));
328
354
  const forkLift = numOr(state.forkLiftRT, 0);
329
355
  this._carriageLiftGroup.position.y = this._computeLiftGroupY(carriageHeight, forkLift);
330
356
  meshUpdated = true;
@@ -415,7 +441,10 @@ export class Crane3D extends RealObjectGroup {
415
441
  if (this._forkGroup) {
416
442
  for (const child of this._forkGroup.children) {
417
443
  const ctx = child.userData?.context;
418
- if (ctx && ctx !== this && ctx instanceof RealObject) {
444
+ // `ctx instanceof RealObject` *cross-module identity 실패* 가능
445
+ // (Parcel3D 가 다른 things-scene 인스턴스의 RealObject 를 extend 한 경우
446
+ // instanceof 가 false 반환). duck-type 으로 완화 — *crane 자신만 제외*.
447
+ if (ctx && ctx !== this) {
419
448
  child.position.z = this._bladeMidZ;
420
449
  }
421
450
  }