@operato/scene-storage 10.0.0-beta.40 → 10.0.0-beta.41
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.
- package/CHANGELOG.md +17 -0
- package/dist/box.js +18 -0
- package/dist/box.js.map +1 -1
- package/dist/crane-3d.d.ts +47 -2
- package/dist/crane-3d.js +246 -89
- package/dist/crane-3d.js.map +1 -1
- package/dist/crane.d.ts +96 -12
- package/dist/crane.js +395 -100
- package/dist/crane.js.map +1 -1
- package/dist/pallet.d.ts +15 -0
- package/dist/pallet.js +38 -2
- package/dist/pallet.js.map +1 -1
- package/dist/parcel-3d.js +22 -18
- package/dist/parcel-3d.js.map +1 -1
- package/dist/parcel.d.ts +4 -3
- package/dist/parcel.js +24 -5
- package/dist/parcel.js.map +1 -1
- package/dist/storage-cell.d.ts +5 -2
- package/dist/storage-cell.js +21 -3
- package/dist/storage-cell.js.map +1 -1
- package/dist/storage-rack-3d.js +42 -7
- package/dist/storage-rack-3d.js.map +1 -1
- package/dist/storage-rack.d.ts +26 -2
- package/dist/storage-rack.js +92 -10
- package/dist/storage-rack.js.map +1 -1
- package/package.json +3 -3
- package/src/box.ts +18 -0
- package/src/crane-3d.ts +258 -93
- package/src/crane.ts +445 -110
- package/src/pallet.ts +50 -1
- package/src/parcel-3d.ts +23 -18
- package/src/parcel.ts +24 -5
- package/src/storage-cell.ts +23 -3
- package/src/storage-rack-3d.ts +47 -8
- package/src/storage-rack.ts +110 -10
- package/test/test-cell-position.ts +105 -0
- package/test/test-crane-geometry.ts +167 -0
- package/test/test-phase-h-carrier-pickable.ts +4 -3
- package/translations/en.json +5 -1
- package/translations/ja.json +5 -1
- package/translations/ko.json +5 -1
- package/translations/ms.json +5 -1
- package/translations/zh.json +5 -1
- package/tsconfig.tsbuildinfo +1 -1
package/dist/storage-rack.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-rack.js","sourceRoot":"","sources":["../src/storage-rack.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'\nimport type { State, Material3D } from '@hatiolab/things-scene'\nimport {\n CellContainer,\n CellMap,\n CarrierHolder,\n Placeable,\n type AttachFrame,\n type Alignment,\n type Heights,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { StorageRack3D } from './storage-rack-3d.js'\n\n/** Rack 컴포넌트 state */\nexport interface StorageRackState extends State {\n // ── 토폴로지 ──\n bays?: number\n levels?: number\n\n // ── 디버그 ──\n debugCells?: boolean\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: 'number',\n label: 'levels',\n name: 'levels',\n placeholder: '# of vertical levels (default 4)'\n },\n {\n type: 'number',\n label: 'bays',\n name: 'bays',\n placeholder: '# of horizontal bays (default 5)'\n }\n ],\n help: 'scene/component/rack'\n}\n\n// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),\n// which forces `isHTMLElement(): true` and trips the 3D pipeline's\n// addObject DOM-skip gate. Rack lives only in the 3D scene graph.\n//\n// Mixin chain: CellContainer → CarrierHolder → Placeable → ContainerAbstract\n// CellContainer: cell topology (cellMap, cell(), findAvailableCell(), occupiedCellIds())\n// CarrierHolder: 3D attach-point protocol (attachPointFor, containable gates)\n// Placeable: floor-archetype positioning\n// ContainerAbstract: child component management\n/**\n * Rack — a multi-level storage shelf system. A *Storage* whose role is to hold\n * carriers in a (bay × level) grid of cells.\n *\n * `levels` × `bays` cells form a vertical grid. Each cell holds one logistics\n * package (Pallet / Box / Parcel). A picker (Crane / Forklift / robot arm)\n * accesses individual cells via Phase G/H Pickable contract — the picker is\n * Rack-agnostic, knowing only how to interact with a cell.\n *\n * **Monitoring mode** (default): carriers are direct children of the rack,\n * placed by external data binding. No RackCell children are created.\n *\n * **Simulation mode**: call `rack._buildCells()` after placing the rack on the\n * scene. This creates RackCell children at the correct 3D positions. A picker\n * (Crane / Forklift / ...) then navigates to individual RackCells for\n * pick-and-place.\n *\n * **Placement**: `floor` archetype, full ceiling depth by default.\n *\n * **Mobility**: this Rack is stationary. A `MobileRack = Mover(Rack)` mixin\n * extension can be added later for AGV-mounted or cart-mounted variants —\n * the cell topology and pickable contract stay the same.\n */\n@sceneComponent('storage-rack')\nexport default class Rack extends CellContainer(CarrierHolder(Placeable(ContainerAbstract))) {\n declare state: StorageRackState\n\n static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (h: Heights) => h.ceiling - h.floor\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n // ── CellContainer ─────────────────────────────────────────────────────────\n\n /**\n * Derive the cell topology from the rack's current dimensions and bay/level\n * counts. The CellMap is rebuilt fresh each time (state changes trigger\n * re-reads via things-scene's invalidation pipeline).\n *\n * Coordinate convention (matches things-scene 3D):\n * X = bay axis (left → right)\n * Y = level axis (floor → ceiling, the rack's `depth` state property)\n * Z = row axis (front → back, the rack's `height` state property)\n */\n get cellMap(): CellMap {\n const bays = Math.max(1, Math.floor((this.state.bays as number) || 5))\n const levels = Math.max(1, Math.floor((this.state.levels as number) || 4))\n const width = (this.state.width as number) || 1000\n const rackDepth = (this.state.depth as number) || 3000 // Y: floor→ceiling\n const rackHeight = (this.state.height as number) || 600 // Z: front→back\n\n return CellMap.grid({\n bays,\n rows: 1,\n levels,\n bayWidth: width / bays,\n rowDepth: rackHeight,\n levelHeight: rackDepth / levels\n })\n }\n\n /**\n * Create RackCell child components for each cell in the CellMap.\n *\n * Called explicitly to enter simulation mode — monitoring-mode racks\n * never call this (carriers are direct children, no explicit cells).\n *\n * Idempotent: removes existing rack-cell children first.\n */\n _buildCells(): void {\n // Remove existing rack-cell children\n const existing = (this.components as Component[] | undefined) ?? []\n for (const child of [...existing]) {\n if ((child as any).state?.type === 'storage-cell') {\n this.removeComponent(child)\n }\n }\n\n // Create a RackCell for each cell in the map\n const RackCellClass = (Component as any).register('storage-cell') as (new (...args: any[]) => Component) | undefined\n if (!RackCellClass) {\n console.warn('Rack._buildCells: rack-cell type not registered. Import rack-cell.ts first.')\n return\n }\n\n const context = this._app\n for (const cell of this.cellMap.cells) {\n const model = {\n type: 'storage-cell',\n cellId: cell.id,\n width: cell.size.width,\n height: cell.size.depth, // 2D height = 3D Z depth\n depth: cell.size.height // 3D Y = level height\n }\n const rackCell = new RackCellClass(model, context)\n this.addComponent(rackCell)\n }\n }\n\n // ── Container gates ───────────────────────────────────────────────────────\n\n /**\n * Allow:\n * - Carriable components (pallets, boxes, parcels) — direct children in monitoring mode.\n * - RackCell — created by _buildCells() in simulation mode.\n *\n * Block:\n * - Everything else (sensors, labels, etc. can be siblings of the rack, not children).\n */\n containable(component: Component): boolean {\n if ((component as any).state?.type === 'storage-cell') return true\n const archetype = (component.constructor as any).placement\n if (archetype === 'operation') return true\n return component.isDescendible(this)\n }\n\n // ── CarrierHolder — attach frame for direct carrier children ─────────────\n\n /**\n * Attach frame for carriers that are DIRECT children of the rack\n * (monitoring mode, where carriers go directly into the rack without\n * explicit RackCell components).\n *\n * In simulation mode, carriers become children of their RackCell,\n * and each RackCell provides its own attachPointFor(). So this method\n * is only invoked on direct-child carriers in monitoring mode — it\n * returns the rack's own object3d as the attach frame (default behavior).\n */\n attachPointFor(_carrier: Component): AttachFrame | null {\n const root = this._realObject?.object3d\n if (!root) return null\n return { attach: root }\n }\n\n // ── 2D rendering ─────────────────────────────────────────────────────────\n\n /**\n * 2D — top-down rectangle showing the rack footprint, with subdivisions\n * suggesting the bay layout.\n */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n const bays = Math.max(1, Math.floor((this.state.bays as number) || 5))\n\n ctx.beginPath()\n // Outer rectangle\n ctx.rect(left, top, width, height)\n // Bay subdivisions (vertical lines)\n for (let i = 1; i < bays; i++) {\n const x = left + (width * i) / bays\n ctx.moveTo(x, top)\n ctx.lineTo(x, top + height)\n }\n }\n\n get fillStyle() {\n return '#a0a0a8'\n }\n\n // ── 3D ───────────────────────────────────────────────────────────────────\n\n buildRealObject(): RealObject | undefined {\n return new StorageRack3D(this)\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"storage-rack.js","sourceRoot":"","sources":["../src/storage-rack.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'\nimport type { State, Material3D } from '@hatiolab/things-scene'\nimport {\n CellContainer,\n CellMap,\n CarrierHolder,\n Placeable,\n type AttachFrame,\n type Alignment,\n type Heights,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { StorageRack3D } from './storage-rack-3d.js'\n\n/** Rack 컴포넌트 state */\nexport interface StorageRackState extends State {\n // ── 토폴로지 ──\n bays?: number\n levels?: number\n\n /**\n * Level 1 (첫 shelf) 의 *시작 높이* (mm, rack 의 3D Y 축, 바닥부터). 미명시 0\n * (바닥 = 첫 shelf). 양수 시 그만큼 위로 올라가 stocker port / conveyor 같은\n * 컴포넌트가 들어갈 *빈 공간* 확보. Frame uprights 는 바닥 ~ 천장 그대로.\n */\n shelfBaseHeight?: number\n\n // ── 디버그 ──\n debugCells?: boolean\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: 'number',\n label: 'levels',\n name: 'levels',\n placeholder: '# of vertical levels (default 4)'\n },\n {\n type: 'number',\n label: 'bays',\n name: 'bays',\n placeholder: '# of horizontal bays (default 5)'\n },\n {\n type: 'number',\n label: 'shelf-base-height',\n name: 'shelfBaseHeight',\n placeholder: 'mm — level 1 시작 높이 (바닥부터). stocker port / conveyor 공간.'\n }\n ],\n help: 'scene/component/rack'\n}\n\n// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),\n// which forces `isHTMLElement(): true` and trips the 3D pipeline's\n// addObject DOM-skip gate. Rack lives only in the 3D scene graph.\n//\n// Mixin chain: CellContainer → CarrierHolder → Placeable → ContainerAbstract\n// CellContainer: cell topology (cellMap, cell(), findAvailableCell(), occupiedCellIds())\n// CarrierHolder: 3D attach-point protocol (attachPointFor, containable gates)\n// Placeable: floor-archetype positioning\n// ContainerAbstract: child component management\n/**\n * Rack — a multi-level storage shelf system. A *Storage* whose role is to hold\n * carriers in a (bay × level) grid of cells.\n *\n * `levels` × `bays` cells form a vertical grid. Each cell holds one logistics\n * package (Pallet / Box / Parcel). A picker (Crane / Forklift / robot arm)\n * accesses individual cells via Phase G/H Pickable contract — the picker is\n * Rack-agnostic, knowing only how to interact with a cell.\n *\n * **Monitoring mode** (default): carriers are direct children of the rack,\n * placed by external data binding. No RackCell children are created.\n *\n * **Simulation mode**: call `rack._buildCells()` after placing the rack on the\n * scene. This creates RackCell children at the correct 3D positions. A picker\n * (Crane / Forklift / ...) then navigates to individual RackCells for\n * pick-and-place.\n *\n * **Placement**: `floor` archetype, full ceiling depth by default.\n *\n * **Mobility**: this Rack is stationary. A `MobileRack = Mover(Rack)` mixin\n * extension can be added later for AGV-mounted or cart-mounted variants —\n * the cell topology and pickable contract stay the same.\n */\n@sceneComponent('storage-rack')\nexport default class Rack extends CellContainer(CarrierHolder(Placeable(ContainerAbstract))) {\n declare state: StorageRackState\n\n static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (h: Heights) => h.ceiling - h.floor\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /**\n * Model serialization — storage-cell 자식 자동 제외. cells 는 _buildCells() 가\n * runtime 재생성 (added() 호출 시점). 저장하면 *redundant 모델 크기 폭증* +\n * load 시 _buildCells 와 중복. rack 의 bays/levels/shelfBaseHeight 만 저장,\n * cells 는 derive.\n */\n get hierarchy(): Record<string, any> {\n const base = super.hierarchy as Record<string, any>\n if (base?.components && Array.isArray(base.components)) {\n base.components = base.components.filter(\n (c: any) => c?.type !== 'storage-cell'\n )\n if (base.components.length === 0) delete base.components\n }\n return base\n }\n\n /**\n * Lifecycle — RackCell child 자동 build. Rack 은 항상 cells 가짐.\n */\n added(parent: any): void {\n super.added?.(parent)\n this._buildCells()\n }\n\n /**\n * Runtime — bays / levels 변경 시 RackCell child 재구성. _buildCells() 는\n * 기존 cell 제거 후 재생성 (idempotent), 단 carrier 보유 시 결함 위험 —\n * application 책임.\n */\n onchange(after: Record<string, unknown>, before: Record<string, unknown>): void {\n super.onchange?.(after, before)\n if (\n 'bays' in after ||\n 'levels' in after ||\n 'shelfBaseHeight' in after ||\n 'width' in after ||\n 'height' in after ||\n 'depth' in after\n ) {\n this._buildCells()\n }\n }\n\n // ── CellContainer ─────────────────────────────────────────────────────────\n\n /**\n * Derive the cell topology from the rack's current dimensions and bay/level\n * counts. The CellMap is rebuilt fresh each time (state changes trigger\n * re-reads via things-scene's invalidation pipeline).\n *\n * Coordinate convention (matches things-scene 3D):\n * X = bay axis (left → right)\n * Y = level axis (floor → ceiling, the rack's `depth` state property)\n * Z = row axis (front → back, the rack's `height` state property)\n */\n get cellMap(): CellMap {\n const bays = Math.max(1, Math.floor((this.state.bays as number) || 5))\n const levels = Math.max(1, Math.floor((this.state.levels as number) || 4))\n const width = (this.state.width as number) || 1000\n const rackDepth = (this.state.depth as number) || 3000 // Y: floor→ceiling\n const rackHeight = (this.state.height as number) || 600 // Z: front→back\n const shelfBase = Math.max(0, Math.min(\n (this.state.shelfBaseHeight as number) || 0,\n rackDepth * 0.9 // clamp ≤ 90% — 최소 shelf zone\n ))\n const shelfZone = rackDepth - shelfBase // 실제 shelf 가 차지하는 Y 영역\n\n return CellMap.grid({\n bays,\n rows: 1,\n levels,\n bayWidth: width / bays,\n rowDepth: rackHeight,\n levelHeight: shelfZone / levels,\n origin: { x: 0, y: shelfBase, z: 0 } // 첫 cell 의 Y = shelfBase\n })\n }\n\n /**\n * Create RackCell child components for each cell in the CellMap.\n *\n * Called explicitly to enter simulation mode — monitoring-mode racks\n * never call this (carriers are direct children, no explicit cells).\n *\n * Idempotent: removes existing rack-cell children first.\n */\n _buildCells(): void {\n // Remove existing rack-cell children\n const existing = (this.components as Component[] | undefined) ?? []\n for (const child of [...existing]) {\n if ((child as any).state?.type === 'storage-cell') {\n this.removeComponent(child)\n }\n }\n\n // Create a RackCell for each cell in the map\n const RackCellClass = (Component as any).register('storage-cell') as (new (...args: any[]) => Component) | undefined\n if (!RackCellClass) {\n console.warn('Rack._buildCells: rack-cell type not registered. Import rack-cell.ts first.')\n return\n }\n\n // cell 의 state.left/top 는 *rack-local* (= parent-relative). things-scene\n // 의 toScene() 이 parent chain 따라 board-absolute 변환 자동. 이전 board-\n // absolute 설정은 *이중 변환* 결함 (rack.left 와 cell.left 둘 다 rack.transform\n // 적용 받아 dx 폭증 → carriagePos clamp → carriage 안 움직임).\n const rackWidth = (this.state.width as number) ?? 1000\n const rackHeight = (this.state.height as number) ?? 100\n const bays = Math.max(1, Math.floor((this.state.bays as number) || 5))\n const bayWidth = rackWidth / bays\n\n const context = this._app\n for (const cell of this.cellMap.cells) {\n // cell.bay / row 는 1-based. storage-rack 은 rows=1 라 모든 cell 이 같은 2D\n // top. level (수직) 은 2D 표현 안 함 — Crane.engage 의 carriageHeight 가 처리.\n const bayIdx = cell.bay - 1 // 0-based\n const cellW = cell.size.width\n const cellH = cell.size.depth // 2D height = 3D Z (rack depth axis)\n // rack-local 좌표 (rack 의 origin = rack.left/top, things-scene 의 자식 좌표)\n const cellLeft = bayIdx * bayWidth + (bayWidth - cellW) / 2\n const cellTop = (rackHeight - cellH) / 2\n\n const model = {\n type: 'storage-cell',\n cellId: cell.id,\n left: cellLeft,\n top: cellTop,\n width: cellW,\n height: cellH,\n depth: cell.size.height, // 3D Y = level height\n zPos: cell.localPosition.y // ← 3D Y 위치 (level 따라 다름)\n }\n const rackCell = new RackCellClass(model, context)\n this.addComponent(rackCell)\n }\n }\n\n // ── Container gates ───────────────────────────────────────────────────────\n\n /**\n * Allow:\n * - Carriable components (pallets, boxes, parcels) — direct children in monitoring mode.\n * - RackCell — created by _buildCells() in simulation mode.\n *\n * Block:\n * - Everything else (sensors, labels, etc. can be siblings of the rack, not children).\n */\n containable(component: Component): boolean {\n if ((component as any).state?.type === 'storage-cell') return true\n const archetype = (component.constructor as any).placement\n if (archetype === 'operation') return true\n return component.isDescendible(this)\n }\n\n // ── CarrierHolder — attach frame for direct carrier children ─────────────\n\n /**\n * Attach frame for carriers that are DIRECT children of the rack\n * (monitoring mode, where carriers go directly into the rack without\n * explicit RackCell components).\n *\n * In simulation mode, carriers become children of their RackCell,\n * and each RackCell provides its own attachPointFor(). So this method\n * is only invoked on direct-child carriers in monitoring mode — it\n * returns the rack's own object3d as the attach frame (default behavior).\n */\n attachPointFor(_carrier: Component): AttachFrame | null {\n const root = this._realObject?.object3d\n if (!root) return null\n return { attach: root }\n }\n\n // ── 2D rendering ─────────────────────────────────────────────────────────\n\n /**\n * 2D — top-down rectangle showing the rack footprint with bay subdivisions.\n * 편집/배치 가 가능하도록 *명시 fill + stroke* — pipeline 분기 무관하게 항상\n * 보임. fill 은 반투명 (carrier / cell 위 overlay).\n */\n render(ctx: CanvasRenderingContext2D) {\n const left = (this.state.left as number) ?? 0\n const top = (this.state.top as number) ?? 0\n const width = (this.state.width as number) ?? 400\n const height = (this.state.height as number) ?? 100\n const bays = Math.max(1, Math.floor((this.state.bays as number) || 5))\n const fill = (this.state.fillStyle as string) || '#a0a0a8'\n const stroke = (this.state.strokeStyle as string) || '#555'\n const lineWidth = (this.state.lineWidth as number) || 1\n\n // Fill (반투명)\n ctx.save()\n ctx.fillStyle = fill\n ctx.globalAlpha = 0.2\n ctx.fillRect(left, top, width, height)\n ctx.restore()\n\n // Stroke — outer + bay subdivisions\n ctx.strokeStyle = stroke\n ctx.lineWidth = lineWidth\n ctx.strokeRect(left, top, width, height)\n ctx.beginPath()\n for (let i = 1; i < bays; i++) {\n const x = left + (width * i) / bays\n ctx.moveTo(x, top)\n ctx.lineTo(x, top + height)\n }\n ctx.stroke()\n }\n\n get fillStyle() {\n return '#a0a0a8'\n }\n\n // ── 3D ───────────────────────────────────────────────────────────────────\n\n buildRealObject(): RealObject | undefined {\n return new StorageRack3D(this)\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@operato/scene-storage",
|
|
3
3
|
"description": "Storage-domain components for things-scene (smart factory / logistics) — pallet, box, parcel; AS/RS and shelves planned.",
|
|
4
4
|
"author": "heartyoh",
|
|
5
|
-
"version": "10.0.0-beta.
|
|
5
|
+
"version": "10.0.0-beta.41",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"module": "dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@hatiolab/things-scene": "^10.0.0-beta.1",
|
|
29
|
-
"@operato/scene-base": "^10.0.0-beta.
|
|
29
|
+
"@operato/scene-base": "^10.0.0-beta.41",
|
|
30
30
|
"three": "^0.183.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"typescript": "^5.0.4"
|
|
46
46
|
},
|
|
47
47
|
"prettier": "@hatiolab/prettier-config",
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "946396e9ccdc598af48129d9d62d36332a906581"
|
|
49
49
|
}
|
package/src/box.ts
CHANGED
|
@@ -140,6 +140,24 @@ export default class Box extends Carriable(Legendable(Placeable(RectPath(Shape))
|
|
|
140
140
|
tolerance: { positionMm: 5, angleDeg: 1 },
|
|
141
141
|
priority: 0,
|
|
142
142
|
id: 'top-gripper'
|
|
143
|
+
}),
|
|
144
|
+
topApproachFrame({
|
|
145
|
+
carrierWorld: me,
|
|
146
|
+
topY: boxDepth,
|
|
147
|
+
approachDistance: 40,
|
|
148
|
+
toolType: 'agv-deck',
|
|
149
|
+
tolerance: { positionMm: 15, angleDeg: 3 },
|
|
150
|
+
priority: 1,
|
|
151
|
+
id: 'top-deck'
|
|
152
|
+
}),
|
|
153
|
+
topApproachFrame({
|
|
154
|
+
carrierWorld: me,
|
|
155
|
+
topY: boxDepth,
|
|
156
|
+
approachDistance: 80, // crane fork hover
|
|
157
|
+
toolType: 'forklift-fork',
|
|
158
|
+
tolerance: { positionMm: 25, angleDeg: 4 },
|
|
159
|
+
priority: 2,
|
|
160
|
+
id: 'top-fork'
|
|
143
161
|
})
|
|
144
162
|
]
|
|
145
163
|
}
|
package/src/crane-3d.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import * as THREE from 'three'
|
|
34
|
-
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
34
|
+
import { RealObject, RealObjectGroup } from '@hatiolab/things-scene'
|
|
35
35
|
|
|
36
36
|
const MAST_COLOR = 0xff7a00 // mast — orange
|
|
37
37
|
const TROLLEY_COLOR = 0x3a4048 // base / top — dark charcoal
|
|
@@ -43,15 +43,33 @@ const LAMP_OFF = 0x222222
|
|
|
43
43
|
|
|
44
44
|
export class Crane3D extends RealObjectGroup {
|
|
45
45
|
private _forkGroup?: THREE.Group
|
|
46
|
-
private
|
|
46
|
+
private _carrierBaseY: number = 0
|
|
47
47
|
private _bladeMidZ: number = 0
|
|
48
|
+
/** floor rail 만 제외한 나머지 (trolley + masts + carriage + fork) 의 movable parent. */
|
|
49
|
+
private _trolleyGroup?: THREE.Group
|
|
50
|
+
/** carriage + fork 의 lift parent (carriageHeight + forkLiftRT 변경 시 Y 만 update). */
|
|
51
|
+
private _carriageLiftGroup?: THREE.Group
|
|
52
|
+
/** Fork active extension mesh — scale.z 와 position.z 로 lerp (rebuild 없이). */
|
|
53
|
+
private _extLeftMesh?: THREE.Mesh
|
|
54
|
+
private _extRightMesh?: THREE.Mesh
|
|
55
|
+
private _extBaseParams?: { bladeSpacing: number; carriageZ: number; stubL: number }
|
|
56
|
+
/** Fork mesh 의 group-local Y center (carriage 위 + bladeH/2). _applyForkExtensionMeshes 가 ext mesh Y 결정. */
|
|
57
|
+
private _forkOffsetY: number = 0
|
|
58
|
+
/** liftGroup.position.y 재계산용 base parameters. */
|
|
59
|
+
private _liftBaseParams?: { baseTrolleyY: number; baseH: number; carriageH: number; bladeH: number }
|
|
48
60
|
|
|
49
61
|
build() {
|
|
50
62
|
super.build()
|
|
51
63
|
|
|
52
64
|
this._forkGroup = undefined
|
|
53
|
-
this.
|
|
65
|
+
this._carrierBaseY = 0
|
|
54
66
|
this._bladeMidZ = 0
|
|
67
|
+
this._trolleyGroup = undefined
|
|
68
|
+
this._carriageLiftGroup = undefined
|
|
69
|
+
this._extLeftMesh = undefined
|
|
70
|
+
this._extRightMesh = undefined
|
|
71
|
+
this._extBaseParams = undefined
|
|
72
|
+
this._liftBaseParams = undefined
|
|
55
73
|
|
|
56
74
|
const { width, height, depth } = this.component.state
|
|
57
75
|
const emissiveColor = (this.component.state.lampEmissive as string) || '#222222'
|
|
@@ -60,13 +78,15 @@ export class Crane3D extends RealObjectGroup {
|
|
|
60
78
|
|
|
61
79
|
// Actuators
|
|
62
80
|
const D = numOr(depth, Math.max(width, height) * 4)
|
|
63
|
-
const carriageRaw = numOr((this.component.state as any).carriageHeight, D * 0.4)
|
|
81
|
+
const carriageRaw = numOr((this.component.state as any).carriageHeight, (this.component as any)._canonicalDefault?.('carriageHeight') ?? D * 0.4)
|
|
64
82
|
const carriageHeight = Math.max(0, Math.min(carriageRaw, D * 0.85))
|
|
65
83
|
|
|
66
84
|
const forkLength = numOr((this.component.state as any).forkLength, height * 0.6)
|
|
67
85
|
const forkExtensionRaw = numOr((this.component.state as any).forkExtension, 0)
|
|
68
86
|
const forkExtension = Math.max(-forkLength, Math.min(forkLength, forkExtensionRaw))
|
|
69
|
-
|
|
87
|
+
// forkLiftRT — 시뮬 runtime current 들림. state.forkLift 는 *configured 진폭*
|
|
88
|
+
// (사용자 설정) 라 안 건드림. 3D carriage Y 는 runtime 값 사용.
|
|
89
|
+
const forkLift = numOr((this.component.state as any).forkLiftRT, 0)
|
|
70
90
|
|
|
71
91
|
// ── Axis convention (FIXED): ─────────────────────────────────────
|
|
72
92
|
// Rail = X (state.left = 2D X = 3D X). Crane 좌우 이동.
|
|
@@ -80,18 +100,24 @@ export class Crane3D extends RealObjectGroup {
|
|
|
80
100
|
const topFrameH = S * 0.1
|
|
81
101
|
const topGuideH = S * 0.1
|
|
82
102
|
const carriageH = S * 0.12
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
103
|
+
// Carriage assembly 크기 — state.carriageWidth 기반 (rail 길이 = crane.width
|
|
104
|
+
// 와 독립). 미명시 시 rail 의 10%.
|
|
105
|
+
const carriageAssemblyW = numOr((this.component.state as any).carriageWidth, width * 0.1)
|
|
106
|
+
const mastW = carriageAssemblyW * 0.15 // mast X 단면 (along rail)
|
|
107
|
+
const mastD = height * 0.25 // mast Z 단면 (cross-rail)
|
|
108
|
+
const mastSpacing = carriageAssemblyW * 0.85 // 두 mast X 간격
|
|
86
109
|
const bladeW = S * 0.1
|
|
87
|
-
|
|
110
|
+
// bladeH — fork 두께. carriage 보다 얇게 (실 fork prong 의 가는 모양).
|
|
111
|
+
// carriage 와 *같은 Y center (0)* 이고 두께만 carriageH * 0.35.
|
|
112
|
+
const bladeH = carriageH * 0.35
|
|
88
113
|
const bladeL = forkLength
|
|
89
114
|
const bladeSpacing = mastSpacing * 0.45
|
|
90
115
|
const carriageW = mastSpacing - mastW * 0.2
|
|
91
116
|
const carriageZ = height * 0.55
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const
|
|
117
|
+
// Cabinet — 존재감만. 작게.
|
|
118
|
+
const cabW = S * 0.18
|
|
119
|
+
const cabH = S * 0.18
|
|
120
|
+
const cabD = S * 0.15
|
|
95
121
|
|
|
96
122
|
const baseY = -D / 2
|
|
97
123
|
const mastH = Math.max(D - railH * 2 - baseH - topFrameH - topGuideH, S * 0.5)
|
|
@@ -104,37 +130,52 @@ export class Crane3D extends RealObjectGroup {
|
|
|
104
130
|
const forkMat = new THREE.MeshStandardMaterial({ color: FORK_COLOR, metalness: 0.85, roughness: 0.3 })
|
|
105
131
|
const railMat = new THREE.MeshStandardMaterial({ color: RAIL_COLOR, metalness: 0.9, roughness: 0.3 })
|
|
106
132
|
|
|
107
|
-
// ── Floor rail
|
|
133
|
+
// ── Floor rail (고정 — crane 본체 안 움직임). 폭 = crane.width (overhang 제거).
|
|
108
134
|
{
|
|
109
|
-
const
|
|
135
|
+
const railThin = railH * 0.5
|
|
136
|
+
const geo = new THREE.BoxGeometry(width, railThin, height * 0.15)
|
|
110
137
|
const mesh = new THREE.Mesh(geo, railMat)
|
|
111
|
-
mesh.position.set(0, baseY +
|
|
138
|
+
mesh.position.set(0, baseY + railThin / 2, 0)
|
|
112
139
|
mesh.receiveShadow = true
|
|
113
140
|
this.object3d.add(mesh)
|
|
114
141
|
}
|
|
115
142
|
|
|
143
|
+
// ── Trolley group — carriage assembly (rail 위 X 만 이동) ─────────
|
|
144
|
+
// 모든 movable 부품 (base trolley, masts, carriage, fork, cabinet, lamp)
|
|
145
|
+
// 의 parent. carriagePosition 변경 시 group 의 local X 만 변경.
|
|
146
|
+
const trolleyGroup = new THREE.Group()
|
|
147
|
+
this._trolleyGroup = trolleyGroup
|
|
148
|
+
this.object3d.add(trolleyGroup)
|
|
149
|
+
// 초기 carriagePosition 적용 — rail-local X (0 ~ width) → object3d-local X (-W/2 ~ +W/2)
|
|
150
|
+
const carriagePos = numOr((this.component.state as any).carriagePosition, (this.component as any)._canonicalDefault?.('carriagePosition') ?? width / 2)
|
|
151
|
+
trolleyGroup.position.x = carriagePos - width / 2
|
|
152
|
+
|
|
116
153
|
// ── Base trolley ──────────────────────────────────────────────────
|
|
154
|
+
// Cabinet 이 mast 바깥쪽에 자연스럽게 놓이도록 *trolley 폭을 mast + cabinet
|
|
155
|
+
// padding 까지 확장*. carriage assembly width 보다 양 옆으로 (cabW + gap) × 2.
|
|
156
|
+
const trolleyPad = cabW + S * 0.04
|
|
157
|
+
const baseTrolleyW = carriageAssemblyW + trolleyPad * 2
|
|
117
158
|
const baseTrolleyY = baseY + railH + baseH / 2
|
|
118
159
|
{
|
|
119
|
-
const geo = new THREE.BoxGeometry(
|
|
160
|
+
const geo = new THREE.BoxGeometry(baseTrolleyW, baseH, height * 0.7)
|
|
120
161
|
const mesh = new THREE.Mesh(geo, trolleyMat)
|
|
121
162
|
mesh.position.set(0, baseTrolleyY, 0)
|
|
122
163
|
mesh.castShadow = true
|
|
123
164
|
mesh.receiveShadow = true
|
|
124
|
-
|
|
165
|
+
trolleyGroup.add(mesh)
|
|
125
166
|
}
|
|
126
167
|
|
|
127
|
-
// ── Control cabinet
|
|
168
|
+
// ── Control cabinet — mast 바깥쪽 (trolley 의 *확장 padding 영역* 위) ─────
|
|
128
169
|
{
|
|
129
170
|
const geo = new THREE.BoxGeometry(cabW, cabH, cabD)
|
|
130
171
|
const cab = new THREE.Mesh(geo, cabinetMat)
|
|
131
172
|
cab.position.set(
|
|
132
|
-
-
|
|
173
|
+
-(carriageAssemblyW / 2 + cabW / 2 + S * 0.02), // mast 왼쪽 바깥
|
|
133
174
|
baseTrolleyY + baseH / 2 + cabH / 2,
|
|
134
175
|
-height * 0.25 + cabD / 2
|
|
135
176
|
)
|
|
136
177
|
cab.castShadow = true
|
|
137
|
-
|
|
178
|
+
trolleyGroup.add(cab)
|
|
138
179
|
}
|
|
139
180
|
|
|
140
181
|
// ── Status lamp ───────────────────────────────────────────────────
|
|
@@ -150,8 +191,8 @@ export class Crane3D extends RealObjectGroup {
|
|
|
150
191
|
})
|
|
151
192
|
const geo = new THREE.CylinderGeometry(lampR, lampR * 0.8, lampH, 12)
|
|
152
193
|
const lamp = new THREE.Mesh(geo, lampMat)
|
|
153
|
-
lamp.position.set(
|
|
154
|
-
|
|
194
|
+
lamp.position.set(carriageAssemblyW / 2 + lampR * 1.5 + S * 0.02, baseTrolleyY + baseH / 2 + lampH / 2, 0)
|
|
195
|
+
trolleyGroup.add(lamp)
|
|
155
196
|
}
|
|
156
197
|
|
|
157
198
|
// ── Twin masts ────────────────────────────────────────────────────
|
|
@@ -162,118 +203,131 @@ export class Crane3D extends RealObjectGroup {
|
|
|
162
203
|
mesh.position.set(xOff, mastY, 0)
|
|
163
204
|
mesh.castShadow = true
|
|
164
205
|
mesh.receiveShadow = true
|
|
165
|
-
|
|
206
|
+
trolleyGroup.add(mesh)
|
|
166
207
|
}
|
|
167
208
|
|
|
168
|
-
// ── Carriage + Fork
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
const
|
|
209
|
+
// ── Carriage + Fork lift group (carriageHeight + forkLiftRT 따라 Y 이동) ─
|
|
210
|
+
// _carriageLiftGroup 안에 carriage + forkGroup. forkLiftRT / carriageHeight
|
|
211
|
+
// 변경 시 *그룹 Y 만 update* (mesh rebuild X). _forkGroup 의 child carrier 가
|
|
212
|
+
// *dispose 없이 그대로* 함께 따라 움직임.
|
|
213
|
+
const stubL = Math.min(carriageZ * 0.2, Math.max(bladeL * 0.05, 6))
|
|
214
|
+
const liftGroup = new THREE.Group()
|
|
215
|
+
this._carriageLiftGroup = liftGroup
|
|
216
|
+
this._liftBaseParams = { baseTrolleyY, baseH, carriageH, bladeH }
|
|
217
|
+
liftGroup.position.set(0, this._computeLiftGroupY(carriageHeight, forkLift), 0)
|
|
218
|
+
trolleyGroup.add(liftGroup)
|
|
219
|
+
|
|
220
|
+
// Carriage — liftGroup local center
|
|
173
221
|
{
|
|
174
222
|
const geo = new THREE.BoxGeometry(carriageW, carriageH, carriageZ)
|
|
175
223
|
const mesh = new THREE.Mesh(geo, carriageMat)
|
|
176
|
-
mesh.position.set(0,
|
|
224
|
+
mesh.position.set(0, 0, 0)
|
|
177
225
|
mesh.castShadow = true
|
|
178
226
|
mesh.receiveShadow = true
|
|
179
|
-
|
|
227
|
+
liftGroup.add(mesh)
|
|
180
228
|
}
|
|
181
229
|
|
|
182
|
-
// ── Two-prong forks
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
const forkY = carriageY // carriage 중심 Y (embed)
|
|
187
|
-
const stubL = Math.min(carriageZ * 0.2, Math.max(bladeL * 0.05, 6))
|
|
230
|
+
// ── Two-prong forks ───────────────────────────────────────────────
|
|
231
|
+
// stub (4 box, fixed) + active extension (2 box, scale.z 로 lerp).
|
|
232
|
+
// active mesh 는 *unit-length* 로 생성. _applyForkExtension 이 scale.z 와
|
|
233
|
+
// position.z 로 길이/방향 update — rebuild 없이 매 frame 부드러운 변형.
|
|
188
234
|
const absExt = Math.abs(forkExtension)
|
|
189
235
|
const sign = forkExtension >= 0 ? 1 : -1
|
|
190
236
|
{
|
|
191
237
|
const group = new THREE.Group()
|
|
192
|
-
|
|
238
|
+
// _forkGroup 은 liftGroup-local center (0,0,0) — frame 일치 단순화.
|
|
239
|
+
// attach localPosition (carrier 자식) 도 *group-local = liftGroup-local* 동일 frame.
|
|
240
|
+
// Fork mesh 자체가 group-local 안에서 carriage *위* 로 (mesh.position.y).
|
|
241
|
+
group.position.set(0, 0, 0)
|
|
242
|
+
|
|
243
|
+
// Fork mesh 가 carriage 와 *수평 (같은 Y 평면)* — carriage 안에 embed.
|
|
244
|
+
// fork mesh group-local Y center = 0 (carriage center 와 같음).
|
|
245
|
+
// fork blade *bottom* 면 group-local Y = -bladeH/2.
|
|
246
|
+
// fork blade *top* 면 group-local Y = +bladeH/2.
|
|
247
|
+
const forkOffsetY = 0
|
|
193
248
|
|
|
194
249
|
// 양옆 stub — 두 prong × 두 측면 = 4 box
|
|
195
250
|
const stubGeo = new THREE.BoxGeometry(bladeW, bladeH, stubL)
|
|
196
251
|
for (const xOff of [-bladeSpacing / 2, +bladeSpacing / 2]) {
|
|
197
252
|
for (const zSide of [-1, +1]) {
|
|
198
253
|
const mesh = new THREE.Mesh(stubGeo, forkMat)
|
|
199
|
-
mesh.position.set(xOff,
|
|
254
|
+
mesh.position.set(xOff, forkOffsetY, zSide * (carriageZ / 2 + stubL / 2))
|
|
200
255
|
mesh.castShadow = true
|
|
201
256
|
mesh.receiveShadow = true
|
|
202
257
|
group.add(mesh)
|
|
203
258
|
}
|
|
204
259
|
}
|
|
205
260
|
|
|
206
|
-
// Active
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const palletL = Math.max(bladeL * 0.3, carriageZ * 0.6)
|
|
227
|
-
const geo = new THREE.BoxGeometry(palletW, palletH, palletL)
|
|
228
|
-
const pallet = new THREE.Mesh(geo, palletMat)
|
|
229
|
-
const palletZ = absExt < 0.5 ? 0 : sign * (carriageZ / 2 + absExt / 2)
|
|
230
|
-
pallet.position.set(0, carriageH / 2 + palletH / 2, palletZ)
|
|
231
|
-
pallet.castShadow = true
|
|
232
|
-
pallet.receiveShadow = true
|
|
233
|
-
group.add(pallet)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
this.object3d.add(group)
|
|
261
|
+
// Active extension — unit length, scale.z + position.z 로 변형
|
|
262
|
+
const extGeo = new THREE.BoxGeometry(bladeW, bladeH, 1)
|
|
263
|
+
const extLeft = new THREE.Mesh(extGeo, forkMat)
|
|
264
|
+
const extRight = new THREE.Mesh(extGeo, forkMat)
|
|
265
|
+
this._forkOffsetY = forkOffsetY
|
|
266
|
+
extLeft.castShadow = true
|
|
267
|
+
extLeft.receiveShadow = true
|
|
268
|
+
extRight.castShadow = true
|
|
269
|
+
extRight.receiveShadow = true
|
|
270
|
+
group.add(extLeft)
|
|
271
|
+
group.add(extRight)
|
|
272
|
+
this._extLeftMesh = extLeft
|
|
273
|
+
this._extRightMesh = extRight
|
|
274
|
+
this._extBaseParams = { bladeSpacing, carriageZ, stubL }
|
|
275
|
+
this._applyForkExtensionMeshes(absExt, sign)
|
|
276
|
+
|
|
277
|
+
// carrier 의 초기 Z = sign * absExt (= _applyForkExtensionMeshes 의 공식 동일).
|
|
278
|
+
const carrierZ = sign * absExt
|
|
279
|
+
|
|
280
|
+
liftGroup.add(group)
|
|
237
281
|
this._forkGroup = group
|
|
238
|
-
|
|
239
|
-
|
|
282
|
+
// Carrier 외부 bottom 정렬점 (liftGroup-local Y) = fork blade *bottom* =
|
|
283
|
+
// -bladeH/2 (fork mesh group-local center = 0, 두께 bladeH).
|
|
284
|
+
// 사용자 모델: "fork 의 아랫면 ≈ carrier 의 아랫면" — fork blade 가 carrier
|
|
285
|
+
// 의 bottom 부분 안으로 *찔러 들어감* (겹친 자세).
|
|
286
|
+
this._carrierBaseY = -bladeH / 2
|
|
287
|
+
this._bladeMidZ = carrierZ
|
|
240
288
|
}
|
|
241
289
|
|
|
242
|
-
// ── Top frame (connects mast tops)
|
|
290
|
+
// ── Top frame (connects mast tops) — trolley 함께 이동 ─────────────
|
|
243
291
|
const topFrameY = mastY + mastH / 2 + topFrameH / 2
|
|
244
292
|
{
|
|
245
293
|
const geo = new THREE.BoxGeometry(mastSpacing + mastW, topFrameH, height * 0.35)
|
|
246
294
|
const mesh = new THREE.Mesh(geo, trolleyMat)
|
|
247
295
|
mesh.position.set(0, topFrameY, 0)
|
|
248
296
|
mesh.castShadow = true
|
|
249
|
-
|
|
297
|
+
trolleyGroup.add(mesh)
|
|
250
298
|
}
|
|
251
299
|
|
|
252
|
-
// ── Top guide trolley
|
|
300
|
+
// ── Top guide trolley — trolley 함께 이동 (ceiling rail 위 미끄러짐) ─
|
|
253
301
|
const topGuideY = topFrameY + topFrameH / 2 + topGuideH / 2
|
|
254
302
|
{
|
|
255
303
|
const geo = new THREE.BoxGeometry(mastSpacing + mastW * 2, topGuideH, height * 0.3)
|
|
256
304
|
const mesh = new THREE.Mesh(geo, trolleyMat)
|
|
257
305
|
mesh.position.set(0, topGuideY, 0)
|
|
258
306
|
mesh.castShadow = true
|
|
259
|
-
|
|
307
|
+
trolleyGroup.add(mesh)
|
|
260
308
|
}
|
|
261
309
|
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
const geo = new THREE.BoxGeometry(width * 1.1, railH, height * 0.3)
|
|
265
|
-
const mesh = new THREE.Mesh(geo, railMat)
|
|
266
|
-
mesh.position.set(0, topGuideY + topGuideH / 2 + railH / 2, 0)
|
|
267
|
-
this.object3d.add(mesh)
|
|
268
|
-
}
|
|
310
|
+
// Ceiling rail 생략 — 상단은 top guide trolley 만으로 충분. 사용자 의도.
|
|
311
|
+
|
|
269
312
|
}
|
|
270
313
|
|
|
271
314
|
getCarriageFrame(): THREE.Object3D | undefined {
|
|
272
|
-
|
|
315
|
+
// Fallback chain — carrier 가 *carriage transform (X 이동, Y lift)* 따라오도록.
|
|
316
|
+
// _forkGroup 미존재 시 _carriageLiftGroup (X+Y 따라옴) → _trolleyGroup (X 만) →
|
|
317
|
+
// root (no follow). 절대 root 로 떨어지지 않도록 lift/trolley 우선.
|
|
318
|
+
return this._forkGroup ?? this._carriageLiftGroup ?? this._trolleyGroup ?? this.object3d
|
|
273
319
|
}
|
|
274
320
|
|
|
275
|
-
|
|
276
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Fork blade *bottom* 의 liftGroup-local Y. *carrier 외부 bottom 정렬점*.
|
|
323
|
+
*
|
|
324
|
+
* 모델: carrier 의 외부 bottom 과 fork blade 의 bottom 이 *거의 일치*. fork 가
|
|
325
|
+
* carrier 의 bottom 부분을 *찔러 들어가* carrier 와 *겹친 자세* (pallet pocket
|
|
326
|
+
* 안 fork 진입). attachPointFor 가 `carrierBaseY + carrier.depth/2` 로 carrier
|
|
327
|
+
* center 를 정렬 → carrier bottom = fork blade bottom.
|
|
328
|
+
*/
|
|
329
|
+
get carrierBaseY(): number {
|
|
330
|
+
return this._carrierBaseY
|
|
277
331
|
}
|
|
278
332
|
|
|
279
333
|
get bladeMidZ(): number {
|
|
@@ -283,24 +337,135 @@ export class Crane3D extends RealObjectGroup {
|
|
|
283
337
|
updateDimension() {}
|
|
284
338
|
|
|
285
339
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
286
|
-
|
|
340
|
+
// carriagePosition — trolleyGroup.position.x (mesh-level update).
|
|
341
|
+
if ('carriagePosition' in after && this._trolleyGroup) {
|
|
342
|
+
const W = numOr(this.component.state.width, 100)
|
|
343
|
+
const pos = numOr(after.carriagePosition, W / 2)
|
|
344
|
+
this._trolleyGroup.position.x = pos - W / 2
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Mesh-level updates — fork extension / lift / carriage height. *rebuild 없이*
|
|
348
|
+
// mesh 의 scale / position 만 변경. _forkGroup 의 child carrier 가 dispose
|
|
349
|
+
// 없이 그대로 따라 움직임 (fork 작업 시 시각 자연스러움).
|
|
350
|
+
//
|
|
351
|
+
// status / bodyColor / lampEmissive 는 *cosmetic* — full rebuild 회피. 별도
|
|
352
|
+
// 처리 없음 시 status 변경 시 carrier dispose → 사라짐 결함의 원인. 향후
|
|
353
|
+
// material color 만 update 하는 path 추가 가능.
|
|
354
|
+
const needsFullRebuild =
|
|
287
355
|
'width' in after ||
|
|
288
356
|
'height' in after ||
|
|
289
357
|
'depth' in after ||
|
|
290
|
-
'carriageHeight' in after ||
|
|
291
358
|
'forkLength' in after ||
|
|
292
|
-
'
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
'
|
|
297
|
-
|
|
359
|
+
'carriageWidth' in after
|
|
360
|
+
|
|
361
|
+
if (!needsFullRebuild) {
|
|
362
|
+
let meshUpdated = false
|
|
363
|
+
if (('carriageHeight' in after || 'forkLiftRT' in after) && this._carriageLiftGroup) {
|
|
364
|
+
const state = this.component.state as any
|
|
365
|
+
const D = numOr(state.depth, Math.max(numOr(state.width, 100), numOr(state.height, 100)) * 4)
|
|
366
|
+
const carriageRaw = numOr(state.carriageHeight, D * 0.4)
|
|
367
|
+
const carriageHeight = Math.max(0, Math.min(carriageRaw, D * 0.85))
|
|
368
|
+
const forkLift = numOr(state.forkLiftRT, 0)
|
|
369
|
+
this._carriageLiftGroup.position.y = this._computeLiftGroupY(carriageHeight, forkLift)
|
|
370
|
+
meshUpdated = true
|
|
371
|
+
}
|
|
372
|
+
if ('forkExtension' in after && this._extLeftMesh && this._extRightMesh) {
|
|
373
|
+
const state = this.component.state as any
|
|
374
|
+
const forkLength = numOr(state.forkLength, numOr(state.height, 100) * 0.6)
|
|
375
|
+
const forkExtensionRaw = numOr(state.forkExtension, 0)
|
|
376
|
+
const forkExtension = Math.max(-forkLength, Math.min(forkLength, forkExtensionRaw))
|
|
377
|
+
const absExt = Math.abs(forkExtension)
|
|
378
|
+
const sign = forkExtension >= 0 ? 1 : -1
|
|
379
|
+
this._applyForkExtensionMeshes(absExt, sign)
|
|
380
|
+
meshUpdated = true
|
|
381
|
+
}
|
|
382
|
+
if (meshUpdated) return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (needsFullRebuild) {
|
|
298
386
|
this.update()
|
|
299
387
|
return
|
|
300
388
|
}
|
|
301
389
|
super.onchange(after, before)
|
|
302
390
|
}
|
|
303
391
|
|
|
392
|
+
/** carriageHeight + forkLiftRT 를 liftGroup.position.y 로 변환. */
|
|
393
|
+
private _computeLiftGroupY(carriageHeight: number, forkLift: number): number {
|
|
394
|
+
const p = this._liftBaseParams
|
|
395
|
+
if (!p) return 0
|
|
396
|
+
return p.baseTrolleyY + p.baseH / 2 + carriageHeight + forkLift + p.carriageH / 2
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Carrier 외부 bottom 의 world Y → carriageHeight state 값 inverse-solve.
|
|
401
|
+
*
|
|
402
|
+
* Forward 공식 (build):
|
|
403
|
+
* liftGroup.y crane-local = baseTrolleyY + baseH/2 + carriageHeight + forkLift + carriageH/2
|
|
404
|
+
* carrier 외부 bottom crane-local = liftGroup.y + carrierBaseY (= -bladeH/2)
|
|
405
|
+
* = baseTrolleyY + baseH/2 + carriageH/2 - bladeH/2 + carriageHeight + forkLift
|
|
406
|
+
*
|
|
407
|
+
* Inverse:
|
|
408
|
+
* carriageHeight = worldY − craneCenterY − (baseTrolleyY + baseH/2 + carriageH/2 − bladeH/2) − forkLift
|
|
409
|
+
*/
|
|
410
|
+
solveCarriageHeightForCarrierBaseWorldY(worldY: number, forkLift: number = 0): number {
|
|
411
|
+
const p = this._liftBaseParams
|
|
412
|
+
if (!p) return 0
|
|
413
|
+
this.object3d.updateWorldMatrix(true, false)
|
|
414
|
+
const v = new THREE.Vector3()
|
|
415
|
+
this.object3d.matrixWorld.decompose(v, new THREE.Quaternion(), new THREE.Vector3())
|
|
416
|
+
const craneCenterWorldY = v.y
|
|
417
|
+
return worldY - craneCenterWorldY - (p.baseTrolleyY + p.baseH / 2 + p.carriageH / 2 - p.bladeH / 2) - forkLift
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* target 의 *crane-local Z* → fork extension 값 inverse-solve.
|
|
422
|
+
*
|
|
423
|
+
* Forward (_applyForkExtensionMeshes): `_bladeMidZ = sign * absExt`
|
|
424
|
+
* (carrier 가 ext 만큼 fork 따라 진출). Inverse: `ext = |localZ|, sign = sign(localZ)`.
|
|
425
|
+
*
|
|
426
|
+
* forkLength 로 clamp — localZ 가 forkLength 보다 멀면 carrier 가 fork tip 까지만.
|
|
427
|
+
*/
|
|
428
|
+
solveForkExtensionForLocalZ(localZ: number): number {
|
|
429
|
+
const sign = localZ >= 0 ? 1 : -1
|
|
430
|
+
const state = this.component.state as any
|
|
431
|
+
const forkLen = numOr(state.forkLength, numOr(state.height, 100) * 0.6)
|
|
432
|
+
const clamped = Math.max(0, Math.min(forkLen, Math.abs(localZ)))
|
|
433
|
+
return sign * clamped
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Fork active extension mesh 의 scale.z + position.z + visibility update. */
|
|
437
|
+
private _applyForkExtensionMeshes(absExt: number, sign: number) {
|
|
438
|
+
if (!this._extLeftMesh || !this._extRightMesh || !this._extBaseParams) return
|
|
439
|
+
const { bladeSpacing, carriageZ, stubL } = this._extBaseParams
|
|
440
|
+
const visible = absExt > 0.5
|
|
441
|
+
const len = Math.max(0.001, absExt)
|
|
442
|
+
const posZ = sign * (carriageZ / 2 + stubL + absExt / 2)
|
|
443
|
+
this._extLeftMesh.scale.z = len
|
|
444
|
+
this._extRightMesh.scale.z = len
|
|
445
|
+
this._extLeftMesh.position.set(-bladeSpacing / 2, this._forkOffsetY, posZ)
|
|
446
|
+
this._extRightMesh.position.set(+bladeSpacing / 2, this._forkOffsetY, posZ)
|
|
447
|
+
this._extLeftMesh.visible = visible
|
|
448
|
+
this._extRightMesh.visible = visible
|
|
449
|
+
|
|
450
|
+
// _bladeMidZ = carrier 의 Z 위치 = sign * absExt (= fork extension 만큼 직접).
|
|
451
|
+
// ext=0 → 0 (carriage 정중앙 — retract 끝 자세)
|
|
452
|
+
// ext=L → ±L (fork 가 L 만큼 진출한 위치 = carrier 도 그 위치)
|
|
453
|
+
// 단순 linear — solveForkExtensionForLocalZ 의 inverse 도 단순 (ext = |localZ|).
|
|
454
|
+
this._bladeMidZ = sign * absExt
|
|
455
|
+
|
|
456
|
+
// _forkGroup 의 child carrier 의 Z 도 동기 — fork tip 위치 따라 carrier 가
|
|
457
|
+
// 함께 끌려와야 retract 시각 자연.
|
|
458
|
+
if (this._forkGroup) {
|
|
459
|
+
for (const child of this._forkGroup.children) {
|
|
460
|
+
const ctx = (child as any).userData?.context
|
|
461
|
+
if (ctx && ctx !== this && ctx instanceof RealObject) {
|
|
462
|
+
child.position.z = this._bladeMidZ
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
}
|
|
468
|
+
|
|
304
469
|
updateAlpha() {}
|
|
305
470
|
}
|
|
306
471
|
|