@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
@@ -1 +1 @@
1
- {"version":3,"file":"parcel-3d.js","sourceRoot":"","sources":["../src/parcel-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Parcel 3D — a cardboard package.\n *\n * Structure:\n * - main body box (cardboard color)\n * - tape line running across the top (the visual signature — what makes\n * this read as a \"shipping parcel\" rather than a generic box)\n * - small label area on top (white rectangle suggesting a shipping label)\n *\n * Kept very simple — parcels in a logistics scene are typically present in\n * large numbers (sortation lines, fulfillment bays), so polygon count\n * matters more than it does for one-off equipment.\n */\n\nimport * as THREE from 'three'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\nconst CARDBOARD_COLOR = 0xc8a878\nconst TAPE_COLOR = 0xddc899\nconst LABEL_COLOR = 0xeeeeee\n\nexport class Parcel3D extends RealObjectGroup {\n build() {\n super.build()\n\n const { width, height, depth = 150 } = this.component.state\n const baseY = -depth / 2\n\n // ── Main body ────────────────────────────────────────────────────\n const bodyGeo = new THREE.BoxGeometry(width, depth, height)\n const bodyMaterial = new THREE.MeshStandardMaterial({\n color: CARDBOARD_COLOR,\n metalness: 0,\n roughness: 0.9\n })\n const bodyMesh = new THREE.Mesh(bodyGeo, bodyMaterial)\n bodyMesh.position.set(0, 0, 0)\n bodyMesh.castShadow = true\n bodyMesh.receiveShadow = true\n this.object3d.add(bodyMesh)\n\n // ── Tape line on top (running along the long axis) ───────────────\n const tapeW = Math.min(width, height) * 0.10\n const tapeT = depth * 0.02\n const tapeAlongLong = width >= height\n const tapeGeo = tapeAlongLong\n ? new THREE.BoxGeometry(width * 1.005, tapeT, tapeW)\n : new THREE.BoxGeometry(tapeW, tapeT, height * 1.005)\n const tapeMaterial = new THREE.MeshStandardMaterial({\n color: TAPE_COLOR,\n metalness: 0.05,\n roughness: 0.5\n })\n const tapeMesh = new THREE.Mesh(tapeGeo, tapeMaterial)\n tapeMesh.position.set(0, baseY + depth + tapeT / 2 - 0.01, 0)\n this.object3d.add(tapeMesh)\n\n // ── Shipping label (small white rectangle on top) ────────────────\n const labelW = Math.min(width, height) * 0.35\n const labelH = labelW * 0.6\n const labelGeo = new THREE.BoxGeometry(labelW, depth * 0.005, labelH)\n const labelMaterial = new THREE.MeshStandardMaterial({\n color: LABEL_COLOR,\n metalness: 0,\n roughness: 0.4\n })\n const labelMesh = new THREE.Mesh(labelGeo, labelMaterial)\n // Position on top, off-center by ~25% of long axis\n if (tapeAlongLong) {\n labelMesh.position.set(width * 0.2, baseY + depth + depth * 0.0025, -height * 0.15)\n } else {\n labelMesh.position.set(width * 0.15, baseY + depth + depth * 0.0025, height * 0.2)\n }\n this.object3d.add(labelMesh)\n }\n\n updateDimension() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if ('width' in after || 'height' in after || 'depth' in after) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n\n updateAlpha() {}\n}\n"]}
1
+ {"version":3,"file":"parcel-3d.js","sourceRoot":"","sources":["../src/parcel-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Parcel 3D — a cardboard package.\n *\n * Structure:\n * - main body box (cardboard color)\n * - tape line running across the top (the visual signature — what makes\n * this read as a \"shipping parcel\" rather than a generic box)\n * - small label area on top (white rectangle suggesting a shipping label)\n *\n * Kept very simple — parcels in a logistics scene are typically present in\n * large numbers (sortation lines, fulfillment bays), so polygon count\n * matters more than it does for one-off equipment.\n */\n\nimport * as THREE from 'three'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\nconst CARDBOARD_COLOR = 0xc8a878\nconst TAPE_COLOR = 0xddc899\nconst LABEL_COLOR = 0xeeeeee\n\n// ── Module-level shared materials ───────────────────────────────────────────\n// Parcel 인스턴스가 수백~수천 가능. instance 별 new MeshStandardMaterial 시\n// material 개수 폭증 → GPU memory + draw call 비효율. static color 라 단일\n// instance 공유. 색 변경 시 모든 Parcel 에 자동 반영 (의도).\nconst PARCEL_BODY_MATERIAL = new THREE.MeshStandardMaterial({\n color: CARDBOARD_COLOR,\n metalness: 0,\n roughness: 0.9\n})\nconst PARCEL_TAPE_MATERIAL = new THREE.MeshStandardMaterial({\n color: TAPE_COLOR,\n metalness: 0.05,\n roughness: 0.5\n})\nconst PARCEL_LABEL_MATERIAL = new THREE.MeshStandardMaterial({\n color: LABEL_COLOR,\n metalness: 0,\n roughness: 0.4\n})\n\nexport class Parcel3D extends RealObjectGroup {\n build() {\n super.build()\n\n const { width, height, depth = 150 } = this.component.state\n const baseY = -depth / 2\n\n // ── Main body ────────────────────────────────────────────────────\n const bodyGeo = new THREE.BoxGeometry(width, depth, height)\n const bodyMesh = new THREE.Mesh(bodyGeo, PARCEL_BODY_MATERIAL)\n bodyMesh.position.set(0, 0, 0)\n bodyMesh.castShadow = true\n bodyMesh.receiveShadow = true\n this.object3d.add(bodyMesh)\n\n // ── Tape line on top (running along the long axis) ───────────────\n const tapeW = Math.min(width, height) * 0.10\n const tapeT = depth * 0.02\n const tapeAlongLong = width >= height\n const tapeGeo = tapeAlongLong\n ? new THREE.BoxGeometry(width * 1.005, tapeT, tapeW)\n : new THREE.BoxGeometry(tapeW, tapeT, height * 1.005)\n const tapeMesh = new THREE.Mesh(tapeGeo, PARCEL_TAPE_MATERIAL)\n tapeMesh.position.set(0, baseY + depth + tapeT / 2 - 0.01, 0)\n this.object3d.add(tapeMesh)\n\n // ── Shipping label (small white rectangle on top) ────────────────\n const labelW = Math.min(width, height) * 0.35\n const labelH = labelW * 0.6\n const labelGeo = new THREE.BoxGeometry(labelW, depth * 0.005, labelH)\n const labelMesh = new THREE.Mesh(labelGeo, PARCEL_LABEL_MATERIAL)\n // Position on top, off-center by ~25% of long axis\n if (tapeAlongLong) {\n labelMesh.position.set(width * 0.2, baseY + depth + depth * 0.0025, -height * 0.15)\n } else {\n labelMesh.position.set(width * 0.15, baseY + depth + depth * 0.0025, height * 0.2)\n }\n this.object3d.add(labelMesh)\n }\n\n updateDimension() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if ('width' in after || 'height' in after || 'depth' in after) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n\n updateAlpha() {}\n}\n"]}
package/dist/parcel.d.ts CHANGED
@@ -34,9 +34,10 @@ export default class Parcel extends Parcel_base {
34
34
  get fillStyle(): string;
35
35
  buildRealObject(): RealObject | undefined;
36
36
  /**
37
- * Phase H — pickup contract. Parcel 위에서 vacuum gripper / suction cup 으로
38
- * 집기 Box 동일한 패턴이지만 cardboard 표면이라 더 큰 흡착 필요.
39
- * tolerance 약간 완화 (cardboard 변형 가능성).
37
+ * Phase H — pickup contract. Parcel pickup 방식:
38
+ * - gripper (vacuum / suction): 위에서 흡착 RobotArm
39
+ * - agv-deck: AGV/Forklift deck 위에 위에서 얹기 — 같은 top approach 지만
40
+ * deck 자체가 운반체라 tolerance 더 완화
40
41
  */
41
42
  pickupFrames(): PickupFrame[];
42
43
  }
package/dist/parcel.js CHANGED
@@ -59,9 +59,10 @@ let Parcel = class Parcel extends Carriable(Placeable(RectPath(Shape))) {
59
59
  return new Parcel3D(this);
60
60
  }
61
61
  /**
62
- * Phase H — pickup contract. Parcel 위에서 vacuum gripper / suction cup 으로
63
- * 집기 Box 동일한 패턴이지만 cardboard 표면이라 더 큰 흡착 필요.
64
- * tolerance 약간 완화 (cardboard 변형 가능성).
62
+ * Phase H — pickup contract. Parcel pickup 방식:
63
+ * - gripper (vacuum / suction): 위에서 흡착 RobotArm
64
+ * - agv-deck: AGV/Forklift deck 위에 위에서 얹기 — 같은 top approach 지만
65
+ * deck 자체가 운반체라 tolerance 더 완화
65
66
  */
66
67
  pickupFrames() {
67
68
  const wp = getWorldPose(this);
@@ -74,11 +75,29 @@ let Parcel = class Parcel extends Carriable(Placeable(RectPath(Shape))) {
74
75
  topApproachFrame({
75
76
  carrierWorld: me,
76
77
  topY: parcelDepth,
77
- approachDistance: 80, // gripper hover 거리 (Box 보다 더 — vacuum 펼침)
78
+ approachDistance: 80,
78
79
  toolType: 'gripper',
79
- tolerance: { positionMm: 10, angleDeg: 2 }, // cardboard 변형 감안
80
+ tolerance: { positionMm: 10, angleDeg: 2 },
80
81
  priority: 0,
81
82
  id: 'top-suction'
83
+ }),
84
+ topApproachFrame({
85
+ carrierWorld: me,
86
+ topY: parcelDepth,
87
+ approachDistance: 60,
88
+ toolType: 'agv-deck',
89
+ tolerance: { positionMm: 20, angleDeg: 5 },
90
+ priority: 1,
91
+ id: 'top-deck'
92
+ }),
93
+ topApproachFrame({
94
+ carrierWorld: me,
95
+ topY: parcelDepth,
96
+ approachDistance: 100, // crane fork 가 cell 진입 hover
97
+ toolType: 'forklift-fork',
98
+ tolerance: { positionMm: 30, angleDeg: 5 }, // fork 적재 tolerance
99
+ priority: 2, // gripper/deck 다음
100
+ id: 'top-fork'
82
101
  })
83
102
  ];
84
103
  }
@@ -1 +1 @@
1
- {"version":3,"file":"parcel.js","sourceRoot":"","sources":["../src/parcel.ts"],"names":[],"mappings":";AAAA;;GAEG;AACH,OAAO,EAGL,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACf,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EACL,SAAS,EACT,SAAS,EAGV,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAWzC,MAAM,MAAM,GAAoB;IAC9B,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,IAAI;IACf,UAAU,EAAE;QACV;YACE,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,aAAa;YACpB,IAAI,EAAE,YAAY;SACnB;KACF;IACD,IAAI,EAAE,wBAAwB;CAC/B,CAAA;AAED,yEAAyE;AACzE,uEAAuE;AACvE,oDAAoD;AACpD;;;;;;;;;;;;;;GAcG;AAEY,IAAM,MAAM,GAAZ,MAAM,MAAO,SAAQ,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAGvE,MAAM,CAAC,SAAS,GAAuB,WAAW,CAAA;IAClD,MAAM,CAAC,KAAK,GAAc,QAAQ,CAAA;IAClC,MAAM,CAAC,YAAY,GAAG,GAAG,CAAA;IAEzB,IAAI,MAAM;QACR,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,OAAO;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED,gDAAgD;IAChD,MAAM,CAAC,GAA6B;QAClC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAA;QAC/C,GAAG,CAAC,SAAS,EAAE,CAAA;QACf,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;IACpC,CAAC;IAED,IAAI,SAAS;QACX,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,eAAe;QACb,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC;IAED;;;;OAIG;IACH,YAAY;QACV,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;QAC7B,MAAM,EAAE,GAAmB;YACzB,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE;YAClE,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE;SACrF,CAAA;QACD,MAAM,WAAW,GAAI,IAAI,CAAC,WAAmB,CAAC,YAAY,IAAI,GAAG,CAAA;QAEjE,OAAO;YACL,gBAAgB,CAAC;gBACf,YAAY,EAAE,EAAE;gBAChB,IAAI,EAAE,WAAW;gBACjB,gBAAgB,EAAE,EAAE,EAAa,0CAA0C;gBAC3E,QAAQ,EAAE,SAAS;gBACnB,SAAS,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAG,kBAAkB;gBAC/D,QAAQ,EAAE,CAAC;gBACX,EAAE,EAAE,aAAa;aAClB,CAAC;SACH,CAAA;IACH,CAAC;;AAtDkB,MAAM;IAD1B,cAAc,CAAC,QAAQ,CAAC;GACJ,MAAM,CAuD1B;eAvDoB,MAAM","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport {\n ComponentNature,\n RealObject,\n RectPath,\n Shape,\n topApproachFrame,\n getWorldPose,\n sceneComponent\n} from '@hatiolab/things-scene'\nimport type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'\nimport {\n Carriable,\n Placeable,\n type Alignment,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { Parcel3D } from './parcel-3d.js'\n\n/** Parcel 컴포넌트 state */\nexport interface ParcelState extends State {\n // ── 정체 ──\n trackingId?: string\n\n // ── 3D 재질 ──\n material3d?: Material3D\n}\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n {\n type: 'string',\n label: 'tracking-id',\n name: 'trackingId'\n }\n ],\n help: 'scene/component/parcel'\n}\n\n// Carriable: parcel can be a child of any CarrierHolder (Spot, robot-arm\n// gripper, AGV deck, …). Mixin wraps add() so the parcel's 3D object3d\n// is reattached to the holder's chosen mount frame.\n/**\n * Parcel — a cardboard package, the typical e-commerce / parcel-sortation unit.\n *\n * Distinct from `Box` because parcels have:\n * - cardboard appearance (tan/brown corrugate, not wood / plastic)\n * - tape line down the center (the visual signature that says \"package\")\n * - typically a label on top (where shipping info goes)\n * - flatter / more elongated proportions in real-world parcel networks\n *\n * No `material` prop — parcels are always cardboard. If a future shipping\n * domain needs metal cases or polybags, those become separate components.\n *\n * No Legendable for v1 — parcel color is fixed cardboard. Future damaged /\n * inspected indicators would add a status legend then.\n */\n@sceneComponent('parcel')\nexport default class Parcel extends Carriable(Placeable(RectPath(Shape))) {\n declare state: ParcelState\n\n static placement: PlacementArchetype = 'operation'\n static align: Alignment = 'bottom'\n static defaultDepth = 150\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /** 2D — top-down rectangle in cardboard tan. */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n ctx.beginPath()\n ctx.rect(left, top, width, height)\n }\n\n get fillStyle() {\n return '#c8a878'\n }\n\n buildRealObject(): RealObject | undefined {\n return new Parcel3D(this)\n }\n\n /**\n * Phase H — pickup contract. Parcel 위에서 vacuum gripper / suction cup 으로\n * 집기 Box 동일한 패턴이지만 cardboard 표면이라 흡착 면 필요.\n * tolerance 약간 완화 (cardboard 변형 가능성).\n */\n pickupFrames(): PickupFrame[] {\n const wp = getWorldPose(this)\n const me: PoseSerialized = {\n position: { x: wp.position.x, y: wp.position.y, z: wp.position.z },\n rotation: { x: wp.rotation.x, y: wp.rotation.y, z: wp.rotation.z, w: wp.rotation.w }\n }\n const parcelDepth = (this.constructor as any).defaultDepth ?? 150\n\n return [\n topApproachFrame({\n carrierWorld: me,\n topY: parcelDepth,\n approachDistance: 80, // gripper hover 거리 (Box 보다 vacuum 펼침)\n toolType: 'gripper',\n tolerance: { positionMm: 10, angleDeg: 2 }, // cardboard 변형 감안\n priority: 0,\n id: 'top-suction'\n })\n ]\n }\n}\n"]}
1
+ {"version":3,"file":"parcel.js","sourceRoot":"","sources":["../src/parcel.ts"],"names":[],"mappings":";AAAA;;GAEG;AACH,OAAO,EAGL,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACf,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EACL,SAAS,EACT,SAAS,EAGV,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAWzC,MAAM,MAAM,GAAoB;IAC9B,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,IAAI;IACf,UAAU,EAAE;QACV;YACE,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,aAAa;YACpB,IAAI,EAAE,YAAY;SACnB;KACF;IACD,IAAI,EAAE,wBAAwB;CAC/B,CAAA;AAED,yEAAyE;AACzE,uEAAuE;AACvE,oDAAoD;AACpD;;;;;;;;;;;;;;GAcG;AAEY,IAAM,MAAM,GAAZ,MAAM,MAAO,SAAQ,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAGvE,MAAM,CAAC,SAAS,GAAuB,WAAW,CAAA;IAClD,MAAM,CAAC,KAAK,GAAc,QAAQ,CAAA;IAClC,MAAM,CAAC,YAAY,GAAG,GAAG,CAAA;IAEzB,IAAI,MAAM;QACR,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,OAAO;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED,gDAAgD;IAChD,MAAM,CAAC,GAA6B;QAClC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAA;QAC/C,GAAG,CAAC,SAAS,EAAE,CAAA;QACf,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;IACpC,CAAC;IAED,IAAI,SAAS;QACX,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,eAAe;QACb,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC;IAED;;;;;OAKG;IACH,YAAY;QACV,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;QAC7B,MAAM,EAAE,GAAmB;YACzB,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE;YAClE,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE;SACrF,CAAA;QACD,MAAM,WAAW,GAAI,IAAI,CAAC,WAAmB,CAAC,YAAY,IAAI,GAAG,CAAA;QAEjE,OAAO;YACL,gBAAgB,CAAC;gBACf,YAAY,EAAE,EAAE;gBAChB,IAAI,EAAE,WAAW;gBACjB,gBAAgB,EAAE,EAAE;gBACpB,QAAQ,EAAE,SAAS;gBACnB,SAAS,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE;gBAC1C,QAAQ,EAAE,CAAC;gBACX,EAAE,EAAE,aAAa;aAClB,CAAC;YACF,gBAAgB,CAAC;gBACf,YAAY,EAAE,EAAE;gBAChB,IAAI,EAAE,WAAW;gBACjB,gBAAgB,EAAE,EAAE;gBACpB,QAAQ,EAAE,UAAU;gBACpB,SAAS,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE;gBAC1C,QAAQ,EAAE,CAAC;gBACX,EAAE,EAAE,UAAU;aACf,CAAC;YACF,gBAAgB,CAAC;gBACf,YAAY,EAAE,EAAE;gBAChB,IAAI,EAAE,WAAW;gBACjB,gBAAgB,EAAE,GAAG,EAAY,6BAA6B;gBAC9D,QAAQ,EAAE,eAAe;gBACzB,SAAS,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAG,oBAAoB;gBACjE,QAAQ,EAAE,CAAC,EAAsB,kBAAkB;gBACnD,EAAE,EAAE,UAAU;aACf,CAAC;SACH,CAAA;IACH,CAAC;;AAzEkB,MAAM;IAD1B,cAAc,CAAC,QAAQ,CAAC;GACJ,MAAM,CA0E1B;eA1EoB,MAAM","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport {\n ComponentNature,\n RealObject,\n RectPath,\n Shape,\n topApproachFrame,\n getWorldPose,\n sceneComponent\n} from '@hatiolab/things-scene'\nimport type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'\nimport {\n Carriable,\n Placeable,\n type Alignment,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { Parcel3D } from './parcel-3d.js'\n\n/** Parcel 컴포넌트 state */\nexport interface ParcelState extends State {\n // ── 정체 ──\n trackingId?: string\n\n // ── 3D 재질 ──\n material3d?: Material3D\n}\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n {\n type: 'string',\n label: 'tracking-id',\n name: 'trackingId'\n }\n ],\n help: 'scene/component/parcel'\n}\n\n// Carriable: parcel can be a child of any CarrierHolder (Spot, robot-arm\n// gripper, AGV deck, …). Mixin wraps add() so the parcel's 3D object3d\n// is reattached to the holder's chosen mount frame.\n/**\n * Parcel — a cardboard package, the typical e-commerce / parcel-sortation unit.\n *\n * Distinct from `Box` because parcels have:\n * - cardboard appearance (tan/brown corrugate, not wood / plastic)\n * - tape line down the center (the visual signature that says \"package\")\n * - typically a label on top (where shipping info goes)\n * - flatter / more elongated proportions in real-world parcel networks\n *\n * No `material` prop — parcels are always cardboard. If a future shipping\n * domain needs metal cases or polybags, those become separate components.\n *\n * No Legendable for v1 — parcel color is fixed cardboard. Future damaged /\n * inspected indicators would add a status legend then.\n */\n@sceneComponent('parcel')\nexport default class Parcel extends Carriable(Placeable(RectPath(Shape))) {\n declare state: ParcelState\n\n static placement: PlacementArchetype = 'operation'\n static align: Alignment = 'bottom'\n static defaultDepth = 150\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /** 2D — top-down rectangle in cardboard tan. */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n ctx.beginPath()\n ctx.rect(left, top, width, height)\n }\n\n get fillStyle() {\n return '#c8a878'\n }\n\n buildRealObject(): RealObject | undefined {\n return new Parcel3D(this)\n }\n\n /**\n * Phase H — pickup contract. Parcel pickup 방식:\n * - gripper (vacuum / suction): 위에서 흡착 — RobotArm\n * - agv-deck: AGV/Forklift deck 위에 위에서 얹기 같은 top approach 지만\n * deck 자체가 운반체라 tolerance 완화\n */\n pickupFrames(): PickupFrame[] {\n const wp = getWorldPose(this)\n const me: PoseSerialized = {\n position: { x: wp.position.x, y: wp.position.y, z: wp.position.z },\n rotation: { x: wp.rotation.x, y: wp.rotation.y, z: wp.rotation.z, w: wp.rotation.w }\n }\n const parcelDepth = (this.constructor as any).defaultDepth ?? 150\n\n return [\n topApproachFrame({\n carrierWorld: me,\n topY: parcelDepth,\n approachDistance: 80,\n toolType: 'gripper',\n tolerance: { positionMm: 10, angleDeg: 2 },\n priority: 0,\n id: 'top-suction'\n }),\n topApproachFrame({\n carrierWorld: me,\n topY: parcelDepth,\n approachDistance: 60,\n toolType: 'agv-deck',\n tolerance: { positionMm: 20, angleDeg: 5 },\n priority: 1,\n id: 'top-deck'\n }),\n topApproachFrame({\n carrierWorld: me,\n topY: parcelDepth,\n approachDistance: 100, // crane fork 가 cell 진입 hover\n toolType: 'forklift-fork',\n tolerance: { positionMm: 30, angleDeg: 5 }, // fork 적재 tolerance\n priority: 2, // gripper/deck 다음\n id: 'top-fork'\n })\n ]\n }\n}\n"]}
@@ -1,13 +1,24 @@
1
+ import * as THREE from 'three';
1
2
  import { RealObjectGroup } from '@hatiolab/things-scene';
2
- import { Rack } from './rack-column.js';
3
3
  export declare class RackGrid3D extends RealObjectGroup {
4
- private _frameMaterial?;
4
+ private _frameGroup?;
5
+ private _beamGroup?;
5
6
  build(): void;
6
- protected get syncZPosOffset(): number;
7
- get geometricOffsetY(): number;
8
- private createFrameMaterial;
9
- createRacks(): void;
10
- mergeAndAddRackCommonObjects(racks: Rack[]): void;
7
+ /** hideRackFrame / hideHorizontalFrame 변경 시 visibility 즉시 반영. */
8
+ applyFrameVisibility(): void;
9
+ private _applyFrameVisibility;
10
+ private _buildFrames;
11
+ private _stockMesh?;
12
+ private _emptyStockMesh?;
13
+ /** Public — 후속 click 핸들러 사용. */
14
+ get stockMesh(): THREE.InstancedMesh | undefined;
15
+ /**
16
+ * Stock 시각화 (Plan A — InstancedMesh batched).
17
+ * - hideEmptyStock=true : state.data 의 record 있는 cell 만 instance
18
+ * - hideEmptyStock=false : *모든 (non-isEmpty bay) cell × shelf* 에 instance.
19
+ * record 있으면 default 색, 없으면 *백색 반투명*.
20
+ */
21
+ rebuildStockMesh(): void;
11
22
  dispose(): void;
12
23
  updateAlpha(): void;
13
24
  }
@@ -1,92 +1,395 @@
1
1
  /*
2
2
  * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * RackGrid3D — *공유 corner posts + bay 별 beams + shelf planes*.
5
+ *
6
+ * Post (수직 기둥):
7
+ * - (cols+1) × (rows+1) 의 grid corner 위치마다 *공유 post* 1 개.
8
+ * - 인접 4 bay 중 *최소 1개 non-empty* 면 post 만듦. 모두 empty 면 skip.
9
+ * - 옆 bay 와 *공유* → 두 post 겹침 X.
10
+ *
11
+ * Beam (수평 부재):
12
+ * - non-empty bay 마다 front + back beam (각 shelf level).
13
+ *
14
+ * Shelf (반투명 판):
15
+ * - non-empty bay 의 각 level 의 frame 안쪽.
16
+ *
17
+ * isEmpty source of truth: RackGrid.isBayEmpty(col, row) — cell.state.isEmpty 우선.
3
18
  */
4
19
  import * as THREE from 'three';
5
20
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
6
21
  import { RealObjectGroup } from '@hatiolab/things-scene';
7
- import { Rack } from './rack-column.js';
8
- const DEFAULT_FRAME_COLOR = 0x8a8a8a;
22
+ import { POST_MATERIAL, BEAM_MATERIAL, SHELF_MATERIAL, STOCK_MATERIAL, EMPTY_STOCK_MATERIAL } from './rack-materials.js';
9
23
  export class RackGrid3D extends RealObjectGroup {
10
- _frameMaterial;
24
+ _frameGroup; // post + beam 묶음 (hideRackFrame 시 hidden)
25
+ _beamGroup; // beam 만 (hideHorizontalFrame 시 hidden)
11
26
  build() {
12
27
  super.build();
13
- this.createRacks();
28
+ this._buildFrames();
29
+ this._applyFrameVisibility();
14
30
  }
15
- // bottom origin: object3d가 zPos(바닥)에 위치 (내부 mesh는 center-local 좌표로 쌓임)
16
- get syncZPosOffset() {
17
- return 0;
31
+ /** hideRackFrame / hideHorizontalFrame 변경 visibility 즉시 반영. */
32
+ applyFrameVisibility() {
33
+ this._applyFrameVisibility();
18
34
  }
19
- get geometricOffsetY() {
20
- return 0;
35
+ _applyFrameVisibility() {
36
+ const rs = this.component.state;
37
+ const hideFrame = !!rs?.hideRackFrame;
38
+ const hideBeams = !!rs?.hideHorizontalFrame;
39
+ if (this._frameGroup)
40
+ this._frameGroup.visible = !hideFrame;
41
+ // beam group 은 frame group 안에 nested — frame 이 hidden 일 때는 beam 도 자연히 hidden.
42
+ // frame visible + beam toggle 따로
43
+ if (this._beamGroup)
44
+ this._beamGroup.visible = !hideBeams;
21
45
  }
22
- createFrameMaterial() {
23
- this._frameMaterial?.dispose();
24
- const { strokeStyle } = this.component.state;
25
- const color = strokeStyle && typeof strokeStyle === 'string' ? strokeStyle : DEFAULT_FRAME_COLOR;
26
- this._frameMaterial = new THREE.MeshStandardMaterial({
27
- color,
28
- roughness: 0.35,
29
- metalness: 0.85
30
- });
31
- return this._frameMaterial;
32
- }
33
- createRacks() {
34
- const { rotation = 0, shelfLocations, shelves = 1 } = this.component.state;
35
- this.object3d.rotation.y = -rotation;
36
- const racks = this.component.components
37
- .map((cell) => {
38
- const { shelfLocations: shelfLoc = shelfLocations, isEmpty } = cell.state;
39
- if (!isEmpty) {
40
- cell.setState('shelfLocations', shelfLoc);
41
- const rack = new Rack(cell);
42
- cell._realObject = rack; // 중복 생성 방지: addObject 재귀에서 skip
43
- rack.update();
44
- this.object3d.add(rack.object3d);
45
- return rack;
46
+ _buildFrames() {
47
+ const comp = this.component;
48
+ const rs = comp.state;
49
+ // frame group post + beamGroup 담음. hideRackFrame frame group.visible=false.
50
+ // beam group — 가로 frame 만 (hideHorizontalFrame 시 따로 토글).
51
+ this._frameGroup = new THREE.Group();
52
+ this._beamGroup = new THREE.Group();
53
+ this._frameGroup.add(this._beamGroup);
54
+ this.object3d.add(this._frameGroup);
55
+ const width = rs?.width ?? 400; // 3D X
56
+ const height = rs?.depth ?? 2000; // 3D Y (floor → ceiling)
57
+ const depth = rs?.height ?? 200; // 3D Z (front → back)
58
+ const cols = comp.columns;
59
+ const rows = comp.rackRows;
60
+ const shelves = comp.shelves;
61
+ const shelfBase = Math.max(0, Math.min(rs?.shelfBaseHeight || 0, height * 0.9));
62
+ const shelfZone = height - shelfBase;
63
+ const bayW = width / cols;
64
+ const bayD = depth / rows;
65
+ const baseY = -height / 2;
66
+ const shelfBaseY = baseY + shelfBase;
67
+ // Frame 굵기 — storage-rack 과 동일 비율 (공유 post / 공유 beam 적용 후엔
68
+ // 겹침 없으므로 같은 비율 사용 가능).
69
+ const postW = Math.min(bayW, bayD) * 0.06;
70
+ const beamH = postW * 1.2;
71
+ const isEmpty = (col, row) => {
72
+ if (col < 0 || col >= cols || row < 0 || row >= rows)
73
+ return true;
74
+ return comp.isBayEmpty(col, row);
75
+ };
76
+ // ── 1. 공유 corner posts (hideRackFrame 면 skip) ───────
77
+ //
78
+ // (cols+1) × (rows+1) 의 모든 corner 위치. 4 인접 bay 중 *하나라도 non-empty*
79
+ // 이면 post 생성. 인접 bay 가 모두 empty → post skip.
80
+ const postGeos = [];
81
+ for (let c = 0; c <= cols; c++) {
82
+ for (let r = 0; r <= rows; r++) {
83
+ // 이 corner 에 인접한 4 bay (없는 위치는 isEmpty 처리)
84
+ const anyActive = !isEmpty(c - 1, r - 1) ||
85
+ !isEmpty(c, r - 1) ||
86
+ !isEmpty(c - 1, r) ||
87
+ !isEmpty(c, r);
88
+ if (!anyActive)
89
+ continue;
90
+ const x = (c - cols / 2) * bayW;
91
+ const z = (r - rows / 2) * bayD;
92
+ const post = new THREE.BoxGeometry(postW, height, postW);
93
+ post.translate(x, 0, z);
94
+ postGeos.push(post);
46
95
  }
47
- return;
48
- })
49
- .filter((rack) => !!rack);
50
- this.mergeAndAddRackCommonObjects(racks);
51
- }
52
- mergeAndAddRackCommonObjects(racks) {
53
- const framesGeometries = [];
54
- const boardsGeometries = [];
55
- if (racks.length > 0) {
56
- racks.forEach(rack => {
57
- const geometry = rack.frame;
58
- if (!geometry) {
59
- return;
96
+ }
97
+ // ── 2. Horizontal beams (X 축 외곽 wall 만 — storage-rack 일관) ────
98
+ //
99
+ // storage-rack 처럼 *front + back 의 X 축 beam* 만. 내부 행 경계 beam +
100
+ // Z 축 (좌우 side) beam 모두 제거 — *깔끔한 wall-frame 시각*.
101
+ // 각 level 마다 front (zEdge=0) + back (zEdge=rows) 두 줄. 각 줄은 *연속
102
+ // non-empty col 구간* 공유 통합.
103
+ const beamGeos = [];
104
+ // 외곽 wall 가로 frame — *연속 non-empty bay 구간*만. isEmpty bay 영역엔
105
+ // frame 없음. *내부 cross frame 은 항상 제외* — 깔끔함 유지.
106
+ // X beam (front + back, 각 level) — non-empty col segment 별 단일 beam
107
+ for (const zEdge of [0, rows]) {
108
+ const zPos = (zEdge - rows / 2) * bayD;
109
+ const adjacentRow = zEdge === 0 ? 0 : rows - 1;
110
+ for (let lv = 0; lv <= shelves; lv++) {
111
+ const yFrac = lv / shelves;
112
+ const y = shelfBaseY + yFrac * shelfZone - beamH / 2 + (lv === 0 ? beamH : 0);
113
+ let segStart = -1;
114
+ for (let col = 0; col <= cols; col++) {
115
+ const active = col < cols && !isEmpty(col, adjacentRow);
116
+ if (active && segStart === -1)
117
+ segStart = col;
118
+ if (!active && segStart !== -1) {
119
+ const startX = (segStart - cols / 2) * bayW;
120
+ const endX = (col - cols / 2) * bayW;
121
+ const beamLen = endX - startX;
122
+ const beamCenterX = (startX + endX) / 2;
123
+ const beam = new THREE.BoxGeometry(beamLen, beamH, beamH);
124
+ beam.translate(beamCenterX, y, zPos);
125
+ beamGeos.push(beam);
126
+ segStart = -1;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ // Z 축 beam (좌우 side) — *모든 level*, isEmpty row 제외 segment.
132
+ for (const xEdge of [0, cols]) {
133
+ const xPos = (xEdge - cols / 2) * bayW;
134
+ const adjacentCol = xEdge === 0 ? 0 : cols - 1;
135
+ for (let lv = 0; lv <= shelves; lv++) {
136
+ const yFrac = lv / shelves;
137
+ const y = shelfBaseY + yFrac * shelfZone - beamH / 2 + (lv === 0 ? beamH : 0);
138
+ let segStart = -1;
139
+ for (let row = 0; row <= rows; row++) {
140
+ const active = row < rows && !isEmpty(adjacentCol, row);
141
+ if (active && segStart === -1)
142
+ segStart = row;
143
+ if (!active && segStart !== -1) {
144
+ const startZ = (segStart - rows / 2) * bayD;
145
+ const endZ = (row - rows / 2) * bayD;
146
+ const beamLen = endZ - startZ;
147
+ const beamCenterZ = (startZ + endZ) / 2;
148
+ const beam = new THREE.BoxGeometry(beamH, beamH, beamLen);
149
+ beam.translate(xPos, y, beamCenterZ);
150
+ beamGeos.push(beam);
151
+ segStart = -1;
152
+ }
153
+ }
154
+ }
155
+ }
156
+ // 천장 (lv=shelves) 내부 cross beam — *isEmpty 영역 제외 segment*.
157
+ {
158
+ const lv = shelves;
159
+ const yFrac = lv / shelves;
160
+ const y = shelfBaseY + yFrac * shelfZone - beamH / 2 + (lv === 0 ? beamH : 0);
161
+ // 내부 col 경계 의 Z 축 beam (col 사이, Z 방향) — 인접 2 col 중 *해당 row* 가 non-empty
162
+ for (let xEdge = 1; xEdge < cols; xEdge++) {
163
+ const xPos = (xEdge - cols / 2) * bayW;
164
+ let segStart = -1;
165
+ for (let row = 0; row <= rows; row++) {
166
+ const active = row < rows && (!isEmpty(xEdge - 1, row) || !isEmpty(xEdge, row));
167
+ if (active && segStart === -1)
168
+ segStart = row;
169
+ if (!active && segStart !== -1) {
170
+ const startZ = (segStart - rows / 2) * bayD;
171
+ const endZ = (row - rows / 2) * bayD;
172
+ const beamLen = endZ - startZ;
173
+ const beamCenterZ = (startZ + endZ) / 2;
174
+ const beam = new THREE.BoxGeometry(beamH, beamH, beamLen);
175
+ beam.translate(xPos, y, beamCenterZ);
176
+ beamGeos.push(beam);
177
+ segStart = -1;
178
+ }
179
+ }
180
+ }
181
+ // 내부 row 경계 의 X 축 beam (row 사이, X 방향) — 인접 2 row 중 *해당 col* 가 non-empty
182
+ for (let zEdge = 1; zEdge < rows; zEdge++) {
183
+ const zPos = (zEdge - rows / 2) * bayD;
184
+ let segStart = -1;
185
+ for (let col = 0; col <= cols; col++) {
186
+ const active = col < cols && (!isEmpty(col, zEdge - 1) || !isEmpty(col, zEdge));
187
+ if (active && segStart === -1)
188
+ segStart = col;
189
+ if (!active && segStart !== -1) {
190
+ const startX = (segStart - cols / 2) * bayW;
191
+ const endX = (col - cols / 2) * bayW;
192
+ const beamLen = endX - startX;
193
+ const beamCenterX = (startX + endX) / 2;
194
+ const beam = new THREE.BoxGeometry(beamLen, beamH, beamH);
195
+ beam.translate(beamCenterX, y, zPos);
196
+ beamGeos.push(beam);
197
+ segStart = -1;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ // ── 3. Shelf planes — *단일 InstancedMesh* (성능). 이전엔 cols × rows × shelves
203
+ // 개별 Mesh (각 draw call) — 큰 grid 에서 수천 draw call. 이제 1 mesh / 1 draw call.
204
+ const shelfW = Math.max(0, bayW - 2 * postW);
205
+ const shelfDD = Math.max(0, bayD - 2 * beamH);
206
+ if (shelfW > 0 && shelfDD > 0) {
207
+ const positions = [];
208
+ for (let col = 0; col < cols; col++) {
209
+ for (let row = 0; row < rows; row++) {
210
+ if (isEmpty(col, row))
211
+ continue;
212
+ const bayCenterX = (col - cols / 2 + 0.5) * bayW;
213
+ const bayCenterZ = (row - rows / 2 + 0.5) * bayD;
214
+ for (let lv = 0; lv < shelves; lv++) {
215
+ const yFrac = lv / shelves;
216
+ const y = shelfBaseY + yFrac * shelfZone + (lv === 0 ? beamH : 0);
217
+ positions.push({ x: bayCenterX, y, z: bayCenterZ });
218
+ }
219
+ }
220
+ }
221
+ if (positions.length > 0) {
222
+ const shelfGeo = new THREE.PlaneGeometry(shelfW, shelfDD);
223
+ shelfGeo.rotateX(-Math.PI / 2);
224
+ const shelfMesh = new THREE.InstancedMesh(shelfGeo, SHELF_MATERIAL, positions.length);
225
+ shelfMesh.receiveShadow = true;
226
+ shelfMesh.frustumCulled = false;
227
+ const m = new THREE.Matrix4();
228
+ const pos = new THREE.Vector3();
229
+ const q = new THREE.Quaternion();
230
+ const s = new THREE.Vector3(1, 1, 1);
231
+ for (let i = 0; i < positions.length; i++) {
232
+ pos.set(positions[i].x, positions[i].y, positions[i].z);
233
+ m.compose(pos, q, s);
234
+ shelfMesh.setMatrixAt(i, m);
60
235
  }
61
- geometry.translate(rack.position.x, rack.position.y, rack.position.z);
62
- geometry.scale(rack.scale.x, rack.scale.y, rack.scale.z);
63
- framesGeometries.push(geometry);
64
- });
65
- if (framesGeometries.length > 0) {
66
- const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(framesGeometries), this.createFrameMaterial());
67
- this.object3d.add(frameMesh);
236
+ shelfMesh.instanceMatrix.needsUpdate = true;
237
+ shelfMesh.computeBoundingSphere();
238
+ this.object3d.add(shelfMesh);
68
239
  }
69
- racks.forEach(rack => {
70
- const geometry = rack.board;
71
- if (!geometry) {
72
- return;
240
+ }
241
+ // ── Merge — post + beam ─────────────────────────────────
242
+ if (postGeos.length > 0) {
243
+ const merged = BufferGeometryUtils.mergeGeometries(postGeos);
244
+ const mesh = new THREE.Mesh(merged, POST_MATERIAL);
245
+ mesh.castShadow = true;
246
+ mesh.receiveShadow = true;
247
+ this._frameGroup.add(mesh);
248
+ }
249
+ if (beamGeos.length > 0) {
250
+ const merged = BufferGeometryUtils.mergeGeometries(beamGeos);
251
+ const mesh = new THREE.Mesh(merged, BEAM_MATERIAL);
252
+ mesh.castShadow = true;
253
+ mesh.receiveShadow = true;
254
+ this._beamGroup.add(mesh);
255
+ }
256
+ // ── 4. Stock InstancedMesh — state.data 의 record + hideEmptyStock 분기 ────
257
+ this.rebuildStockMesh();
258
+ }
259
+ // ── Stock visualization ─────────────────────────────────
260
+ _stockMesh; // record 있는 stock (불투명, 색)
261
+ _emptyStockMesh; // record 없는 stock (반투명 회색)
262
+ /** Public — 후속 click 핸들러 사용. */
263
+ get stockMesh() {
264
+ return this._stockMesh;
265
+ }
266
+ /**
267
+ * Stock 시각화 (Plan A — InstancedMesh batched).
268
+ * - hideEmptyStock=true : state.data 의 record 있는 cell 만 instance
269
+ * - hideEmptyStock=false : *모든 (non-isEmpty bay) cell × shelf* 에 instance.
270
+ * record 있으면 default 색, 없으면 *백색 반투명*.
271
+ */
272
+ rebuildStockMesh() {
273
+ // 기존 두 mesh 모두 제거
274
+ if (this._stockMesh) {
275
+ this.object3d.remove(this._stockMesh);
276
+ this._stockMesh.dispose?.();
277
+ this._stockMesh = undefined;
278
+ }
279
+ if (this._emptyStockMesh) {
280
+ this.object3d.remove(this._emptyStockMesh);
281
+ this._emptyStockMesh.dispose?.();
282
+ this._emptyStockMesh = undefined;
283
+ }
284
+ const comp = this.component;
285
+ const rs = comp.state;
286
+ const cols = comp.columns;
287
+ const rows = comp.rackRows;
288
+ const shelves = comp.shelves;
289
+ const width = rs?.width ?? 400;
290
+ const height = rs?.depth ?? 2000;
291
+ const depth = rs?.height ?? 200;
292
+ const shelfBase = Math.max(0, Math.min(rs?.shelfBaseHeight || 0, height * 0.9));
293
+ const shelfZone = height - shelfBase;
294
+ const bayW = width / cols;
295
+ const bayD = depth / rows;
296
+ const cellY = shelfZone / shelves;
297
+ const baseY = -height / 2;
298
+ const shelfBaseY = baseY + shelfBase;
299
+ const stockW = bayW * 0.85;
300
+ const stockD = cellY * 0.7;
301
+ const stockH = bayD * 0.85;
302
+ const records = comp.records;
303
+ const recordsByCell = new Map();
304
+ for (const r of records) {
305
+ if (r?.cellId)
306
+ recordsByCell.set(r.cellId, r);
307
+ }
308
+ const hideEmpty = !!rs?.hideEmptyStock;
309
+ // 두 그룹 분리: record 있는 stock (불투명, 색) vs empty stock (반투명 회색)
310
+ const filled = [];
311
+ const empties = [];
312
+ for (let col = 0; col < cols; col++) {
313
+ for (let row = 0; row < rows; row++) {
314
+ if (comp.isBayEmpty(col, row))
315
+ continue;
316
+ for (let shelf = 0; shelf < shelves; shelf++) {
317
+ const cellId = `${col}-${row}-${shelf}`;
318
+ const record = recordsByCell.get(cellId);
319
+ if (record) {
320
+ filled.push({ col, row, shelf, record });
321
+ }
322
+ else if (!hideEmpty) {
323
+ empties.push({ col, row, shelf });
324
+ }
73
325
  }
74
- geometry.translate(rack.position.x, rack.position.y, rack.position.z);
75
- geometry.scale(rack.scale.x, rack.scale.y, rack.scale.z);
76
- boardsGeometries.push(geometry);
77
- });
78
- if (boardsGeometries.length > 0) {
79
- const material = Rack.boardMaterial;
80
- material.opacity = 0.5;
81
- material.transparent = true;
82
- const boardMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(boardsGeometries), material);
83
- this.object3d.add(boardMesh);
84
326
  }
85
327
  }
328
+ const matrixFor = (col, row, shelf, target) => {
329
+ const cx = (col - cols / 2 + 0.5) * bayW;
330
+ const cellBottomY = shelfBaseY + shelf * cellY;
331
+ const cy = cellBottomY + stockD / 2;
332
+ const cz = (row - rows / 2 + 0.5) * bayD;
333
+ target.compose(new THREE.Vector3(cx, cy, cz), new THREE.Quaternion(), new THREE.Vector3(1, 1, 1));
334
+ };
335
+ // ── 1. Filled stock — 불투명, legend/default 색 ──────
336
+ if (filled.length > 0) {
337
+ const STOCK_COLOR_DEFAULT = '#c8a878'; // cardboard
338
+ const geo = new THREE.BoxGeometry(stockW, stockD, stockH);
339
+ const mesh = new THREE.InstancedMesh(geo, STOCK_MATERIAL, filled.length);
340
+ mesh.frustumCulled = false;
341
+ mesh.userData.context = this;
342
+ mesh.userData._records = filled.map(i => i.record);
343
+ const m = new THREE.Matrix4();
344
+ const c = new THREE.Color();
345
+ for (let i = 0; i < filled.length; i++) {
346
+ const { col, row, shelf, record } = filled[i];
347
+ matrixFor(col, row, shelf, m);
348
+ mesh.setMatrixAt(i, m);
349
+ const resolved = comp.resolveLegendColor?.(record) ?? STOCK_COLOR_DEFAULT;
350
+ c.set(resolved);
351
+ mesh.setColorAt(i, c);
352
+ }
353
+ mesh.instanceMatrix.needsUpdate = true;
354
+ if (mesh.instanceColor)
355
+ mesh.instanceColor.needsUpdate = true;
356
+ mesh.computeBoundingSphere();
357
+ mesh.computeBoundingBox?.();
358
+ this.object3d.add(mesh);
359
+ this._stockMesh = mesh;
360
+ }
361
+ // ── 2. Empty stock — 반투명 회색 (hideEmptyStock=off 시만) ────
362
+ if (empties.length > 0) {
363
+ const geo = new THREE.BoxGeometry(stockW, stockD, stockH);
364
+ const mesh = new THREE.InstancedMesh(geo, EMPTY_STOCK_MATERIAL, empties.length);
365
+ mesh.frustumCulled = false;
366
+ mesh.userData.context = this;
367
+ const m = new THREE.Matrix4();
368
+ for (let i = 0; i < empties.length; i++) {
369
+ const { col, row, shelf } = empties[i];
370
+ matrixFor(col, row, shelf, m);
371
+ mesh.setMatrixAt(i, m);
372
+ }
373
+ mesh.instanceMatrix.needsUpdate = true;
374
+ mesh.computeBoundingSphere();
375
+ mesh.computeBoundingBox?.();
376
+ this.object3d.add(mesh);
377
+ this._emptyStockMesh = mesh;
378
+ }
86
379
  }
87
380
  dispose() {
88
- this._frameMaterial?.dispose();
89
- this._frameMaterial = undefined;
381
+ // Material 은 module-level singleton 이라 dispose 안 함 (전체 application
382
+ // lifecycle 동안 살아있음). InstancedMesh / Group 의 geometry 만 정리.
383
+ if (this._stockMesh) {
384
+ this.object3d.remove(this._stockMesh);
385
+ this._stockMesh.dispose?.();
386
+ this._stockMesh = undefined;
387
+ }
388
+ if (this._emptyStockMesh) {
389
+ this.object3d.remove(this._emptyStockMesh);
390
+ this._emptyStockMesh.dispose?.();
391
+ this._emptyStockMesh = undefined;
392
+ }
90
393
  super.dispose();
91
394
  }
92
395
  updateAlpha() { }