@operato/scene-transport 10.0.0-beta.28 → 10.0.0-beta.31

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 CHANGED
@@ -3,6 +3,38 @@
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.31](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.30...v10.0.0-beta.31) (2026-05-08)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * **conveyance+transport+manufacturing:** induct→sorter→chute 자세 보존 + AGV/Forklift pick 보완 ([28c2ed7](https://github.com/things-scene/operato-scene/commit/28c2ed7b64d87b27e72e8cbd36a48a5d26d6cf8a))
12
+ * **transport,conveyance:** Phase F Step 4-6 — carryPolicy 명시적 선언 ([522e003](https://github.com/things-scene/operato-scene/commit/522e0035ddf9357fb385183d03a54609b9e20ee1))
13
+
14
+
15
+ ### :bug: Bug Fix
16
+
17
+ * **transport/visualizer:** children → components — 잠재 버그 수정 (Phase C Round 4) ([8131166](https://github.com/things-scene/operato-scene/commit/813116650dd846becfbf4f9571893cff25f85e62))
18
+
19
+
20
+ ### :house: Code Refactoring
21
+
22
+ * _realObject 캐스트 16개 제거 (Phase C Round 2 후속) ([8616c6c](https://github.com/things-scene/operato-scene/commit/8616c6c45e38a9bbcc244a77cee53479a756df85))
23
+ * 39개 buildRealObject 캐스트 제거 (Phase D Round 4 후속) ([b86681c](https://github.com/things-scene/operato-scene/commit/b86681c07d7bf41cb93ade9855afb21902ed8186))
24
+ * **mfg/transport/storage:** typed state Round 2 — 14개 컴포넌트 적용 ([f1765f6](https://github.com/things-scene/operato-scene/commit/f1765f62d12cf4c20ea26a2297e98069b9621a45))
25
+ * 함수 인자 (this as any) 13개 제거 (Phase D Round 4 후속) ([b0a400c](https://github.com/things-scene/operato-scene/commit/b0a400c9716fdec22518cc6ad0bd1c1e69bda89d))
26
+
27
+
28
+
29
+ ## [10.0.0-beta.30](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.29...v10.0.0-beta.30) (2026-05-07)
30
+
31
+
32
+ ### :rocket: New Features
33
+
34
+ * **conveyance:** InductStation + CrossBeltLine + Chute 3D 시뮬레이션 전면 개선 ([e729b9d](https://github.com/things-scene/operato-scene/commit/e729b9dbb736ce1f12a4de789738ec1e58bf1dbc))
35
+
36
+
37
+
6
38
  ## [10.0.0-beta.28](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.27...v10.0.0-beta.28) (2026-05-05)
7
39
 
8
40
 
package/dist/agv.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
2
+ import type { SlotDef, State, Material3D } from '@hatiolab/things-scene';
2
3
  import { type Alignment, type CarrierAttachPoint, type Heights, type LegendBinding, type MoveOptions, type PlacementArchetype } from '@operato/scene-base';
3
4
  /**
4
5
  * Agv status — common to both payload and towing AGVs (kept narrow on purpose).
@@ -14,6 +15,13 @@ import { type Alignment, type CarrierAttachPoint, type Heights, type LegendBindi
14
15
  * invite drift.
15
16
  */
16
17
  export type AgvStatus = 'idle' | 'moving' | 'charging' | 'error';
18
+ /** Agv 컴포넌트 state */
19
+ export interface AgvState extends State {
20
+ status?: AgvStatus;
21
+ battery?: number;
22
+ speed?: number;
23
+ material3d?: Material3D;
24
+ }
17
25
  declare const Base: typeof Component & {
18
26
  new (...args: any[]): Component & {
19
27
  isCarrierHolder: boolean;
@@ -37,6 +45,7 @@ declare const Base: typeof Component & {
37
45
  * For the towing variant (no cargo deck, pulls trailers behind), see Tugger.
38
46
  */
39
47
  export default class Agv extends Base {
48
+ get state(): AgvState;
40
49
  static legends: Record<string, LegendBinding>;
41
50
  /**
42
51
  * AGV sits on its wheels — `floor` archetype. Default depth = operation,
@@ -58,6 +67,11 @@ export default class Agv extends Base {
58
67
  static yawOffset: number;
59
68
  get nature(): ComponentNature;
60
69
  get anchors(): never[];
70
+ /**
71
+ * Single-cargo policy: AGV 는 한 번에 1개의 carrier 만 운반.
72
+ * 이미 1개를 보유한 상태에서는 `canReceive()` → false → `pick()` 거부.
73
+ */
74
+ get slots(): SlotDef[];
61
75
  /** Accept logistics packages (placement='operation') as deck cargo. */
62
76
  containable(component: Component): boolean;
63
77
  /**
@@ -76,6 +90,17 @@ export default class Agv extends Base {
76
90
  * children order is preserved; no persisted state needed.
77
91
  */
78
92
  private _slotIndexOf;
93
+ /**
94
+ * pick 보완: super.pick 후 carrier 의 자세 정렬 + attachPointFor 강제 재적용.
95
+ *
96
+ * 흐름의 마지막에 ContainerCapacity.receive 의 setState({left, top}) 가 표준
97
+ * pipeline 을 트리거해 obj3d.position 이 attachPointFor 의 localPosition 을 덮어씀.
98
+ * (= 첫번째 carrier 가 데크 위가 아니라 잘못된 위치/공중에 잡히는 원인)
99
+ * super.pick 종료 후 attachPointFor 를 다시 적용하고 suppressTransform=true 로
100
+ * pipeline 의 추가 override 차단. AGV 가 움직이면 carrier 는 scene-graph 통해 자동 추종.
101
+ */
102
+ pick(carrier: Component, options?: any): Promise<void>;
103
+ private _snapToAttachPoint;
79
104
  /**
80
105
  * 2D — render() sets up the rounded chassis path; the framework fills it
81
106
  * with `fillStyle` (= bodyColor from Legendable) and strokes with
package/dist/agv.js CHANGED
@@ -2,7 +2,7 @@ import { __decorate } from "tslib";
2
2
  /*
3
3
  * Copyright © HatioLab Inc. All rights reserved.
4
4
  */
5
- import { ContainerAbstract, sceneComponent } from '@hatiolab/things-scene';
5
+ import { ContainerAbstract, ContainerCapacity, sceneComponent } from '@hatiolab/things-scene';
6
6
  import { CarrierHolder, FloorBound, Legendable, Mover, Placeable } from '@operato/scene-base';
7
7
  import { Agv3D } from './agv-3d.js';
8
8
  /**
@@ -64,16 +64,19 @@ const NATURE = {
64
64
  ],
65
65
  help: 'scene/component/agv'
66
66
  };
67
- // Composition: ContainerAbstract → Placeable → Legendable → CarrierHolder → Mover → FloorBound.
68
- // Same chain as Forklift the AGV is a wheeled, payload-carrying vehicle
69
- // with the same pick/place semantics; differences (deck-stack instead of
70
- // fork-mount) are encoded in `attachPointFor`. FloorBound is outermost so
71
- // its rotation guard sees state changes last.
67
+ // Composition:
68
+ // FloorBound Mover CarrierHolder ContainerCapacity Legendable Placeable ContainerAbstract
72
69
  //
73
- // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
74
- // which forces `isHTMLElement(): true` and trips the 3D pipeline's
75
- // addObject DOM-skip gate. AGV lives only in the 3D scene graph; no DOM.
76
- const Base = FloorBound(Mover(CarrierHolder(Legendable(Placeable(ContainerAbstract)))));
70
+ // ContainerCapacity: receive() / dispatch() / canReceive() / slots Mover.pick uses
71
+ // receive() for slot-tracking and event emission on every cargo transfer.
72
+ // CarrierHolder: attachPointFor() deck-top 3D mount frame; Carriable.added()
73
+ // calls applyHolderAttachPoint() which uses our override for 3D positioning.
74
+ // Mover: moveTo / pick / place / pickAndPlace / executeMission.
75
+ // FloorBound: outermost rotation guard.
76
+ //
77
+ // `ContainerAbstract` (not `Container`) — avoids isHTMLElement()=true that would
78
+ // trip the 3D pipeline's addObject DOM-skip gate.
79
+ const Base = FloorBound(Mover(CarrierHolder(ContainerCapacity(Legendable(Placeable(ContainerAbstract))))));
77
80
  /**
78
81
  * Agv — payload (unit-load) automated guided vehicle. The Kiva-style flat-deck
79
82
  * AGV that drives under or carries cargo to/from operation surfaces.
@@ -87,6 +90,12 @@ const Base = FloorBound(Mover(CarrierHolder(Legendable(Placeable(ContainerAbstra
87
90
  * For the towing variant (no cargo deck, pulls trailers behind), see Tugger.
88
91
  */
89
92
  let Agv = class Agv extends Base {
93
+ // `Base` is cast through `typeof Component` so TS sees `state` as an
94
+ // accessor; `declare state: …` would conflict with TS2610. Override the
95
+ // getter instead — runtime behavior is identical (just delegates to super).
96
+ get state() {
97
+ return super.state;
98
+ }
90
99
  static legends = {
91
100
  bodyColor: { from: 'status', legend: BODY_LEGEND },
92
101
  lampEmissive: { from: 'status', legend: LAMP_EMISSIVE_LEGEND }
@@ -115,6 +124,14 @@ let Agv = class Agv extends Base {
115
124
  get anchors() {
116
125
  return [];
117
126
  }
127
+ // ── ContainerCapacity ─────────────────────────────────────────────────────
128
+ /**
129
+ * Single-cargo policy: AGV 는 한 번에 1개의 carrier 만 운반.
130
+ * 이미 1개를 보유한 상태에서는 `canReceive()` → false → `pick()` 거부.
131
+ */
132
+ get slots() {
133
+ return [{ id: 'deck', maxCount: 1 }];
134
+ }
118
135
  /** Accept logistics packages (placement='operation') as deck cargo. */
119
136
  containable(component) {
120
137
  const archetype = component.constructor.placement;
@@ -139,14 +156,19 @@ let Agv = class Agv extends Base {
139
156
  const depth = this.state.depth ?? 0;
140
157
  const slotIdx = this._slotIndexOf(carrier);
141
158
  const cargoDepth = carrier.state.depth ?? 100;
159
+ // 3D origin convention (beta.66+): carrier 의 origin = center.
160
+ // 데크 표면 (= AGV 상단) y = depth/2. carrier 의 bottom 을 데크 표면에 두려면
161
+ // carrier center y = deckY + cargoDepth/2. 이후 slot 별로 cargoDepth 씩 stack.
142
162
  const deckY = depth / 2;
143
163
  return {
144
164
  attach: ro.object3d,
145
165
  localPosition: {
146
166
  x: 0,
147
- y: deckY + slotIdx * cargoDepth,
167
+ y: deckY + cargoDepth / 2 + slotIdx * cargoDepth,
148
168
  z: 0
149
- }
169
+ },
170
+ // Phase F: AGV 가 회전하면 cargo 도 같이 회전 (deck 위 고정 lock).
171
+ carryPolicy: 'follow-holder'
150
172
  };
151
173
  }
152
174
  /**
@@ -154,10 +176,51 @@ let Agv = class Agv extends Base {
154
176
  * children order is preserved; no persisted state needed.
155
177
  */
156
178
  _slotIndexOf(carrier) {
157
- const children = this.children ?? [];
179
+ const children = this.components ?? [];
158
180
  const idx = children.indexOf(carrier);
159
181
  return idx < 0 ? children.length : idx;
160
182
  }
183
+ /**
184
+ * pick 보완: super.pick 후 carrier 의 자세 정렬 + attachPointFor 강제 재적용.
185
+ *
186
+ * 흐름의 마지막에 ContainerCapacity.receive 의 setState({left, top}) 가 표준
187
+ * pipeline 을 트리거해 obj3d.position 이 attachPointFor 의 localPosition 을 덮어씀.
188
+ * (= 첫번째 carrier 가 데크 위가 아니라 잘못된 위치/공중에 잡히는 원인)
189
+ * super.pick 종료 후 attachPointFor 를 다시 적용하고 suppressTransform=true 로
190
+ * pipeline 의 추가 override 차단. AGV 가 움직이면 carrier 는 scene-graph 통해 자동 추종.
191
+ */
192
+ async pick(carrier, options = {}) {
193
+ // 1-capacity 정책: 이미 1개 보유 시 pick 거부 (moveTo / engage 낭비 방지).
194
+ if (!this.canReceive(carrier)) {
195
+ this.trigger('transfer-rejected', {
196
+ type: 'transfer-rejected',
197
+ component: carrier,
198
+ container: this,
199
+ reason: 'agv-at-capacity'
200
+ });
201
+ return;
202
+ }
203
+ await super.pick(carrier, options);
204
+ carrier.setState?.({ rotation: 0, rotationX: 0, rotationY: 0 });
205
+ this._snapToAttachPoint(carrier);
206
+ }
207
+ _snapToAttachPoint(carrier) {
208
+ const point = this.attachPointFor(carrier);
209
+ const ro = carrier._realObject;
210
+ const obj3d = ro?.object3d;
211
+ if (!obj3d || !point?.localPosition)
212
+ return;
213
+ const lp = point.localPosition;
214
+ obj3d.position.set(lp.x, lp.y, lp.z);
215
+ if (point.localRotation) {
216
+ obj3d.rotation.set(point.localRotation.x, point.localRotation.y, point.localRotation.z);
217
+ }
218
+ else {
219
+ obj3d.quaternion.identity();
220
+ }
221
+ if (ro)
222
+ ro.suppressTransform = true;
223
+ }
161
224
  /**
162
225
  * 2D — render() sets up the rounded chassis path; the framework fills it
163
226
  * with `fillStyle` (= bodyColor from Legendable) and strokes with
package/dist/agv.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"agv.js","sourceRoot":"","sources":["../src/agv.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 {\n CarrierHolder,\n FloorBound,\n Legendable,\n Mover,\n Placeable,\n type Alignment,\n type CarrierAttachPoint,\n type Heights,\n type LegendBinding,\n type MoveOptions,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { Agv3D } from './agv-3d.js'\n\n/**\n * Agv status — common to both payload and towing AGVs (kept narrow on purpose).\n *\n * - `idle` — parked, awaiting task\n * - `moving` — driving along a path / executing transport\n * - `charging` — at a charging dock / battery station\n * - `error` — fault / blocked / e-stop\n *\n * Loaded-vs-empty distinction is *not* in the status enum because for a\n * Kiva-style payload AGV it's already obvious from the children (cargo\n * components) parented to it — duplicating that as a status flag would\n * invite drift.\n */\nexport type AgvStatus = 'idle' | 'moving' | 'charging' | 'error'\n\n/**\n * Body color — neutral industrial gray base, slightly modulated by status.\n * AGVs typically have status-color LED strips rather than full body color\n * change; the body legend stays subtle so the LED strip + lamp do the\n * communicating.\n */\nconst BODY_LEGEND = {\n idle: '#999',\n moving: '#aaa',\n charging: '#aaa',\n error: '#c66',\n default: '#999'\n}\n\n/**\n * LED strip emissive — the dominant status indicator for AGVs (typically a\n * color-band running around the chassis perimeter).\n */\nconst LAMP_EMISSIVE_LEGEND = {\n idle: '#222222',\n moving: '#44ff44', // green (operating)\n charging: '#ffaa00', // amber (charging)\n error: '#ff3333', // red\n default: '#222222'\n}\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n {\n type: 'select',\n label: 'status',\n name: 'status',\n property: {\n options: [\n { display: 'Idle', value: 'idle' },\n { display: 'Moving', value: 'moving' },\n { display: 'Charging', value: 'charging' },\n { display: 'Error', value: 'error' }\n ]\n }\n },\n {\n type: 'number',\n label: 'battery',\n name: 'battery',\n placeholder: '0..1'\n },\n {\n // AGV travel speed in scene units / second. Used by Mover.moveTo\n // to derive motion duration. Tune per board scale; forklift convention.\n type: 'number',\n label: 'speed',\n name: 'speed',\n placeholder: 'units/sec'\n }\n ],\n help: 'scene/component/agv'\n}\n\n// Composition: ContainerAbstract → Placeable → Legendable → CarrierHolder → Mover → FloorBound.\n// Same chain as Forklift — the AGV is a wheeled, payload-carrying vehicle\n// with the same pick/place semantics; differences (deck-stack instead of\n// fork-mount) are encoded in `attachPointFor`. FloorBound is outermost so\n// its rotation guard sees state changes last.\n//\n// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),\n// which forces `isHTMLElement(): true` and trips the 3D pipeline's\n// addObject DOM-skip gate. AGV lives only in the 3D scene graph; no DOM.\nconst Base = FloorBound(Mover(CarrierHolder(Legendable(Placeable(ContainerAbstract))))) as unknown as typeof Component & {\n new (...args: any[]): Component & {\n isCarrierHolder: boolean\n isMover: boolean\n attachPointFor(carrier: Component): CarrierAttachPoint | undefined\n pick(carrier: Component, options?: MoveOptions): Promise<void>\n place(carrier: Component, holder: Component, options?: MoveOptions): Promise<void>\n pickAndPlace(carrier: Component, holder: Component, options?: MoveOptions): Promise<void>\n }\n}\n\n/**\n * Agv — payload (unit-load) automated guided vehicle. The Kiva-style flat-deck\n * AGV that drives under or carries cargo to/from operation surfaces.\n *\n * **Container-based for cargo containment.** A payload Agv has a flat top\n * deck whose surface is at the scene's operation height. Boxes, parcels,\n * loaded pallets (anything with `placement: 'operation'`) can be added as\n * children — when they are, their natural archetype-derived zPos puts them\n * exactly on the AGV's deck (since AGV depth = operation - floor).\n *\n * For the towing variant (no cargo deck, pulls trailers behind), see Tugger.\n */\n@sceneComponent('agv')\nexport default class Agv extends Base {\n static legends: Record<string, LegendBinding> = {\n bodyColor: { from: 'status', legend: BODY_LEGEND },\n lampEmissive: { from: 'status', legend: LAMP_EMISSIVE_LEGEND }\n }\n\n /**\n * AGV sits on its wheels — `floor` archetype. Default depth = operation,\n * so the top deck lands at the scene's operation height. This is the\n * design point of payload AGVs: the deck height matches conveyor belt\n * height, equipment ports, and forklift fork height — cargo transfers\n * across all of them at the same level.\n */\n static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (h: Heights) => h.operation - h.floor\n\n /**\n * Heading yaw offset (rad). things-scene's vehicle convention is\n * `vehicle forward = component-local -Z` (= \"rotation=0 → toward canvas\n * top edge\"); the framework default `Math.PI / 2` aligns with that.\n * Stated explicitly so the model's forward axis is documented at the\n * class level rather than relying silently on the framework default.\n */\n static yawOffset = Math.PI / 2\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /** Accept logistics packages (placement='operation') as deck cargo. */\n containable(component: Component) {\n const archetype = (component.constructor as any).placement\n if (archetype === 'operation') return true\n return component.isDescendible(this as any)\n }\n\n /**\n * AGV attach point — flat top deck. Cargo lands on the deck surface\n * (top of the AGV envelope, which is `+depth/2` in component-local Y).\n * Multiple cargo items stack vertically by their own depth.\n *\n * `attach` is the AGV's `_realObject.object3d` directly — there is no\n * inner sub-frame the way Forklift has a fork-tip frame, because the\n * deck moves rigidly with the chassis. Cargo Y reflects the cargoDepth\n * × slot index stack offset.\n */\n attachPointFor(carrier: Component): CarrierAttachPoint | undefined {\n const ro = (this as any)._realObject as { object3d?: any } | undefined\n if (!ro?.object3d) return undefined\n\n const depth = (this.state.depth as number) ?? 0\n const slotIdx = this._slotIndexOf(carrier)\n const cargoDepth = (carrier.state as any).depth ?? 100\n const deckY = depth / 2\n\n return {\n attach: ro.object3d,\n localPosition: {\n x: 0,\n y: deckY + slotIdx * cargoDepth,\n z: 0\n }\n }\n }\n\n /**\n * Stack slot = the carrier's index among siblings. Stable because\n * children order is preserved; no persisted state needed.\n */\n private _slotIndexOf(carrier: Component): number {\n const children = ((this as any).children as Component[] | undefined) ?? []\n const idx = children.indexOf(carrier)\n return idx < 0 ? children.length : idx\n }\n\n /**\n * 2D — render() sets up the rounded chassis path; the framework fills it\n * with `fillStyle` (= bodyColor from Legendable) and strokes with\n * `strokeStyle`. Additional top-down details (bumpers, LiDAR, lift pad,\n * direction triangle) are drawn in `postrender()`.\n */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n const radius = Math.min(width, height) * 0.12\n ctx.beginPath()\n ctx.roundRect(left, top, width, height, radius)\n }\n\n /** Top-view accent details — bumpers, lift pad, LiDAR, direction triangle. */\n postrender(ctx: CanvasRenderingContext2D) {\n super.postrender?.(ctx)\n\n const { width, height, left, top } = this.state\n const cx = left + width / 2\n const cy = top + height / 2\n const accentColor = (this.state.lampEmissive as string) || '#44ff44'\n\n ctx.save()\n\n // Hi-vis bumper strips (front + rear)\n const bumperT = Math.min(width, height) * 0.06\n ctx.fillStyle = '#eeaa00'\n ctx.fillRect(left + width * 0.15, top, width * 0.7, bumperT)\n ctx.fillRect(left + width * 0.15, top + height - bumperT, width * 0.7, bumperT)\n ctx.fillStyle = '#111'\n ctx.fillRect(left + width * 0.02, top, width * 0.13, bumperT)\n ctx.fillRect(left + width * 0.85, top, width * 0.13, bumperT)\n ctx.fillRect(left + width * 0.02, top + height - bumperT, width * 0.13, bumperT)\n ctx.fillRect(left + width * 0.85, top + height - bumperT, width * 0.13, bumperT)\n\n // Lift pad (center circle — Kiva-style under-shelf lift)\n const padR = Math.min(width, height) * 0.22\n ctx.fillStyle = '#4a4a55'\n ctx.strokeStyle = '#222'\n ctx.lineWidth = 1\n ctx.beginPath()\n ctx.ellipse(cx, cy, padR, padR, 0, 0, Math.PI * 2)\n ctx.fill()\n ctx.stroke()\n\n // LiDAR sensor (small filled circle near front)\n const lidarR = Math.min(width, height) * 0.07\n ctx.fillStyle = '#222233'\n ctx.beginPath()\n ctx.ellipse(cx, top + height * 0.20, lidarR, lidarR, 0, 0, Math.PI * 2)\n ctx.fill()\n ctx.stroke()\n // Status hint on LiDAR\n ctx.fillStyle = accentColor\n ctx.beginPath()\n ctx.ellipse(cx, top + height * 0.20, lidarR * 0.4, lidarR * 0.4, 0, 0, Math.PI * 2)\n ctx.fill()\n\n // Direction-of-travel triangle\n ctx.fillStyle = accentColor\n ctx.strokeStyle = '#111'\n ctx.beginPath()\n ctx.moveTo(cx, top + height * 0.06)\n ctx.lineTo(cx - width * 0.06, top + height * 0.13)\n ctx.lineTo(cx + width * 0.06, top + height * 0.13)\n ctx.closePath()\n ctx.fill()\n ctx.stroke()\n\n ctx.restore()\n }\n\n buildRealObject(): RealObject | undefined {\n return new Agv3D(this as any)\n }\n}\n"]}
1
+ {"version":3,"file":"agv.js","sourceRoot":"","sources":["../src/agv.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport { Component, ComponentNature, ContainerAbstract, ContainerCapacity, RealObject, sceneComponent } from '@hatiolab/things-scene'\nimport type { SlotDef, State, Material3D } from '@hatiolab/things-scene'\nimport {\n CarrierHolder,\n FloorBound,\n Legendable,\n Mover,\n Placeable,\n type Alignment,\n type CarrierAttachPoint,\n type Heights,\n type LegendBinding,\n type MoveOptions,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { Agv3D } from './agv-3d.js'\n\n/**\n * Agv status — common to both payload and towing AGVs (kept narrow on purpose).\n *\n * - `idle` — parked, awaiting task\n * - `moving` — driving along a path / executing transport\n * - `charging` — at a charging dock / battery station\n * - `error` — fault / blocked / e-stop\n *\n * Loaded-vs-empty distinction is *not* in the status enum because for a\n * Kiva-style payload AGV it's already obvious from the children (cargo\n * components) parented to it — duplicating that as a status flag would\n * invite drift.\n */\nexport type AgvStatus = 'idle' | 'moving' | 'charging' | 'error'\n\n/** Agv 컴포넌트 state */\nexport interface AgvState extends State {\n // ── 운영 상태 ──\n status?: AgvStatus\n\n // ── 시뮬레이션/모니터링 ──\n battery?: number\n speed?: number\n\n // ── 3D 재질 ──\n material3d?: Material3D\n}\n\n/**\n * Body color — neutral industrial gray base, slightly modulated by status.\n * AGVs typically have status-color LED strips rather than full body color\n * change; the body legend stays subtle so the LED strip + lamp do the\n * communicating.\n */\nconst BODY_LEGEND = {\n idle: '#999',\n moving: '#aaa',\n charging: '#aaa',\n error: '#c66',\n default: '#999'\n}\n\n/**\n * LED strip emissive — the dominant status indicator for AGVs (typically a\n * color-band running around the chassis perimeter).\n */\nconst LAMP_EMISSIVE_LEGEND = {\n idle: '#222222',\n moving: '#44ff44', // green (operating)\n charging: '#ffaa00', // amber (charging)\n error: '#ff3333', // red\n default: '#222222'\n}\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n {\n type: 'select',\n label: 'status',\n name: 'status',\n property: {\n options: [\n { display: 'Idle', value: 'idle' },\n { display: 'Moving', value: 'moving' },\n { display: 'Charging', value: 'charging' },\n { display: 'Error', value: 'error' }\n ]\n }\n },\n {\n type: 'number',\n label: 'battery',\n name: 'battery',\n placeholder: '0..1'\n },\n {\n // AGV travel speed in scene units / second. Used by Mover.moveTo\n // to derive motion duration. Tune per board scale; forklift convention.\n type: 'number',\n label: 'speed',\n name: 'speed',\n placeholder: 'units/sec'\n }\n ],\n help: 'scene/component/agv'\n}\n\n// Composition:\n// FloorBound → Mover → CarrierHolder → ContainerCapacity → Legendable → Placeable → ContainerAbstract\n//\n// ContainerCapacity: receive() / dispatch() / canReceive() / slots — Mover.pick uses\n// receive() for slot-tracking and event emission on every cargo transfer.\n// CarrierHolder: attachPointFor() — deck-top 3D mount frame; Carriable.added()\n// calls applyHolderAttachPoint() which uses our override for 3D positioning.\n// Mover: moveTo / pick / place / pickAndPlace / executeMission.\n// FloorBound: outermost rotation guard.\n//\n// `ContainerAbstract` (not `Container`) — avoids isHTMLElement()=true that would\n// trip the 3D pipeline's addObject DOM-skip gate.\nconst Base = FloorBound(Mover(CarrierHolder(ContainerCapacity(Legendable(Placeable(ContainerAbstract)))))) as unknown as typeof Component & {\n new (...args: any[]): Component & {\n isCarrierHolder: boolean\n isMover: boolean\n attachPointFor(carrier: Component): CarrierAttachPoint | undefined\n pick(carrier: Component, options?: MoveOptions): Promise<void>\n place(carrier: Component, holder: Component, options?: MoveOptions): Promise<void>\n pickAndPlace(carrier: Component, holder: Component, options?: MoveOptions): Promise<void>\n }\n}\n\n/**\n * Agv — payload (unit-load) automated guided vehicle. The Kiva-style flat-deck\n * AGV that drives under or carries cargo to/from operation surfaces.\n *\n * **Container-based for cargo containment.** A payload Agv has a flat top\n * deck whose surface is at the scene's operation height. Boxes, parcels,\n * loaded pallets (anything with `placement: 'operation'`) can be added as\n * children — when they are, their natural archetype-derived zPos puts them\n * exactly on the AGV's deck (since AGV depth = operation - floor).\n *\n * For the towing variant (no cargo deck, pulls trailers behind), see Tugger.\n */\n@sceneComponent('agv')\nexport default class Agv extends Base {\n // `Base` is cast through `typeof Component` so TS sees `state` as an\n // accessor; `declare state: …` would conflict with TS2610. Override the\n // getter instead — runtime behavior is identical (just delegates to super).\n override get state(): AgvState {\n return super.state as AgvState\n }\n\n static legends: Record<string, LegendBinding> = {\n bodyColor: { from: 'status', legend: BODY_LEGEND },\n lampEmissive: { from: 'status', legend: LAMP_EMISSIVE_LEGEND }\n }\n\n /**\n * AGV sits on its wheels — `floor` archetype. Default depth = operation,\n * so the top deck lands at the scene's operation height. This is the\n * design point of payload AGVs: the deck height matches conveyor belt\n * height, equipment ports, and forklift fork height — cargo transfers\n * across all of them at the same level.\n */\n static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (h: Heights) => h.operation - h.floor\n\n /**\n * Heading yaw offset (rad). things-scene's vehicle convention is\n * `vehicle forward = component-local -Z` (= \"rotation=0 → toward canvas\n * top edge\"); the framework default `Math.PI / 2` aligns with that.\n * Stated explicitly so the model's forward axis is documented at the\n * class level rather than relying silently on the framework default.\n */\n static yawOffset = Math.PI / 2\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n // ── ContainerCapacity ─────────────────────────────────────────────────────\n\n /**\n * Single-cargo policy: AGV 는 한 번에 1개의 carrier 만 운반.\n * 이미 1개를 보유한 상태에서는 `canReceive()` → false → `pick()` 거부.\n */\n get slots(): SlotDef[] {\n return [{ id: 'deck', maxCount: 1 }]\n }\n\n /** Accept logistics packages (placement='operation') as deck cargo. */\n containable(component: Component) {\n const archetype = (component.constructor as any).placement\n if (archetype === 'operation') return true\n return component.isDescendible(this)\n }\n\n /**\n * AGV attach point — flat top deck. Cargo lands on the deck surface\n * (top of the AGV envelope, which is `+depth/2` in component-local Y).\n * Multiple cargo items stack vertically by their own depth.\n *\n * `attach` is the AGV's `_realObject.object3d` directly — there is no\n * inner sub-frame the way Forklift has a fork-tip frame, because the\n * deck moves rigidly with the chassis. Cargo Y reflects the cargoDepth\n * × slot index stack offset.\n */\n attachPointFor(carrier: Component): CarrierAttachPoint | undefined {\n const ro = this._realObject\n if (!ro?.object3d) return undefined\n\n const depth = (this.state.depth as number) ?? 0\n const slotIdx = this._slotIndexOf(carrier)\n const cargoDepth = (carrier.state as any).depth ?? 100\n // 3D origin convention (beta.66+): carrier 의 origin = center.\n // 데크 표면 (= AGV 상단) y = depth/2. carrier 의 bottom 을 데크 표면에 두려면\n // carrier center y = deckY + cargoDepth/2. 이후 slot 별로 cargoDepth 씩 stack.\n const deckY = depth / 2\n\n return {\n attach: ro.object3d,\n localPosition: {\n x: 0,\n y: deckY + cargoDepth / 2 + slotIdx * cargoDepth,\n z: 0\n },\n // Phase F: AGV 가 회전하면 cargo 도 같이 회전 (deck 위 고정 lock).\n carryPolicy: 'follow-holder'\n }\n }\n\n /**\n * Stack slot = the carrier's index among siblings. Stable because\n * children order is preserved; no persisted state needed.\n */\n private _slotIndexOf(carrier: Component): number {\n const children = this.components ?? []\n const idx = children.indexOf(carrier)\n return idx < 0 ? children.length : idx\n }\n\n /**\n * pick 보완: super.pick 후 carrier 의 자세 정렬 + attachPointFor 강제 재적용.\n *\n * 흐름의 마지막에 ContainerCapacity.receive 의 setState({left, top}) 가 표준\n * pipeline 을 트리거해 obj3d.position 이 attachPointFor 의 localPosition 을 덮어씀.\n * (= 첫번째 carrier 가 데크 위가 아니라 잘못된 위치/공중에 잡히는 원인)\n * super.pick 종료 후 attachPointFor 를 다시 적용하고 suppressTransform=true 로\n * pipeline 의 추가 override 차단. AGV 가 움직이면 carrier 는 scene-graph 통해 자동 추종.\n */\n override async pick(carrier: Component, options: any = {}): Promise<void> {\n // 1-capacity 정책: 이미 1개 보유 시 pick 거부 (moveTo / engage 낭비 방지).\n if (!(this as any).canReceive(carrier)) {\n this.trigger('transfer-rejected', {\n type: 'transfer-rejected',\n component: carrier,\n container: this,\n reason: 'agv-at-capacity'\n })\n return\n }\n await super.pick(carrier, options)\n carrier.setState?.({ rotation: 0, rotationX: 0, rotationY: 0 })\n this._snapToAttachPoint(carrier)\n }\n\n private _snapToAttachPoint(carrier: Component): void {\n const point = this.attachPointFor(carrier)\n const ro = (carrier as any)._realObject\n const obj3d = ro?.object3d\n if (!obj3d || !point?.localPosition) return\n const lp = point.localPosition\n obj3d.position.set(lp.x, lp.y, lp.z)\n if (point.localRotation) {\n obj3d.rotation.set(point.localRotation.x, point.localRotation.y, point.localRotation.z)\n } else {\n obj3d.quaternion.identity()\n }\n if (ro) ro.suppressTransform = true\n }\n\n /**\n * 2D — render() sets up the rounded chassis path; the framework fills it\n * with `fillStyle` (= bodyColor from Legendable) and strokes with\n * `strokeStyle`. Additional top-down details (bumpers, LiDAR, lift pad,\n * direction triangle) are drawn in `postrender()`.\n */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n const radius = Math.min(width, height) * 0.12\n ctx.beginPath()\n ctx.roundRect(left, top, width, height, radius)\n }\n\n /** Top-view accent details — bumpers, lift pad, LiDAR, direction triangle. */\n postrender(ctx: CanvasRenderingContext2D) {\n super.postrender?.(ctx)\n\n const { width, height, left, top } = this.state\n const cx = left + width / 2\n const cy = top + height / 2\n const accentColor = (this.state.lampEmissive as string) || '#44ff44'\n\n ctx.save()\n\n // Hi-vis bumper strips (front + rear)\n const bumperT = Math.min(width, height) * 0.06\n ctx.fillStyle = '#eeaa00'\n ctx.fillRect(left + width * 0.15, top, width * 0.7, bumperT)\n ctx.fillRect(left + width * 0.15, top + height - bumperT, width * 0.7, bumperT)\n ctx.fillStyle = '#111'\n ctx.fillRect(left + width * 0.02, top, width * 0.13, bumperT)\n ctx.fillRect(left + width * 0.85, top, width * 0.13, bumperT)\n ctx.fillRect(left + width * 0.02, top + height - bumperT, width * 0.13, bumperT)\n ctx.fillRect(left + width * 0.85, top + height - bumperT, width * 0.13, bumperT)\n\n // Lift pad (center circle — Kiva-style under-shelf lift)\n const padR = Math.min(width, height) * 0.22\n ctx.fillStyle = '#4a4a55'\n ctx.strokeStyle = '#222'\n ctx.lineWidth = 1\n ctx.beginPath()\n ctx.ellipse(cx, cy, padR, padR, 0, 0, Math.PI * 2)\n ctx.fill()\n ctx.stroke()\n\n // LiDAR sensor (small filled circle near front)\n const lidarR = Math.min(width, height) * 0.07\n ctx.fillStyle = '#222233'\n ctx.beginPath()\n ctx.ellipse(cx, top + height * 0.20, lidarR, lidarR, 0, 0, Math.PI * 2)\n ctx.fill()\n ctx.stroke()\n // Status hint on LiDAR\n ctx.fillStyle = accentColor\n ctx.beginPath()\n ctx.ellipse(cx, top + height * 0.20, lidarR * 0.4, lidarR * 0.4, 0, 0, Math.PI * 2)\n ctx.fill()\n\n // Direction-of-travel triangle\n ctx.fillStyle = accentColor\n ctx.strokeStyle = '#111'\n ctx.beginPath()\n ctx.moveTo(cx, top + height * 0.06)\n ctx.lineTo(cx - width * 0.06, top + height * 0.13)\n ctx.lineTo(cx + width * 0.06, top + height * 0.13)\n ctx.closePath()\n ctx.fill()\n ctx.stroke()\n\n ctx.restore()\n }\n\n buildRealObject(): RealObject | undefined {\n return new Agv3D(this)\n }\n}\n"]}
@@ -1,5 +1,7 @@
1
1
  import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
2
+ import type { SlotDef, State, Material3D } from '@hatiolab/things-scene';
2
3
  import { type Alignment, type CarrierAttachPoint, type Heights, type LegendBinding, type MoveOptions, type PlacementArchetype } from '@operato/scene-base';
4
+ import { Forklift3D } from './forklift-3d.js';
3
5
  /**
4
6
  * Forklift status — the operating state of a forklift truck.
5
7
  *
@@ -13,6 +15,13 @@ import { type Alignment, type CarrierAttachPoint, type Heights, type LegendBindi
13
15
  * loaded-vs-empty distinction relevant for fleet visualization.
14
16
  */
15
17
  export type ForkliftStatus = 'idle' | 'running' | 'lifting' | 'loaded' | 'error';
18
+ /** Forklift 컴포넌트 state */
19
+ export interface ForkliftState extends State {
20
+ status?: ForkliftStatus;
21
+ forkHeight?: number;
22
+ speed?: number;
23
+ material3d?: Material3D;
24
+ }
16
25
  declare const Base: typeof Component & {
17
26
  new (...args: any[]): Component & {
18
27
  isCarrierHolder: boolean;
@@ -37,6 +46,8 @@ declare const Base: typeof Component & {
37
46
  * change the rendering path, only how children's 3D objects are reparented.
38
47
  */
39
48
  export default class Forklift extends Base {
49
+ get state(): ForkliftState;
50
+ _realObject: Forklift3D | undefined;
40
51
  static legends: Record<string, LegendBinding>;
41
52
  /**
42
53
  * Forklift sits on its wheels — `floor` archetype. Default depth is the
@@ -57,14 +68,15 @@ export default class Forklift extends Base {
57
68
  get nature(): ComponentNature;
58
69
  get anchors(): never[];
59
70
  /**
60
- * Allow items that flow at operation level (boxes, cartons, parcels, loaded
61
- * pallets) to be added as cargo. Restricting by archetype rather than type
62
- * means new package components are auto-permitted as they're added to the
63
- * scene-base ecosystem.
64
- *
65
- * Note: this overrides CarrierHolder's default `containable` (which restricts
66
- * to `isCarriable === true`). Forklift accepts any operation-archetype
67
- * component so cargo authored before adopting Carriable still drops in.
71
+ * Forks hold up to 3 stacked load units (pallet + boxes).
72
+ * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx)
73
+ * the slot maxCount here is the hard cap for `canReceive()`.
74
+ */
75
+ get slots(): SlotDef[];
76
+ /**
77
+ * Forklift 정책: **파레트만** 자식으로 받음. 박스, 카트론, 파셀 등은 받지 않음.
78
+ * 실제 forklift fork pocket 통해 들기 때문에 pocket 구조가 있는 파레트만
79
+ * 들 수 있음. 박스 등은 AGV 같은 평탄 데크 차량이 운반.
68
80
  */
69
81
  containable(component: Component): boolean;
70
82
  /**
@@ -96,6 +108,29 @@ export default class Forklift extends Base {
96
108
  * once the add settles.
97
109
  */
98
110
  private _slotIndexOf;
111
+ /**
112
+ * engage 보완: pick 시 fork blade top 의 world Y 가 carrier (파레트) 의
113
+ * world center Y 와 일치하도록 forkHeight 를 들어올림. → fork 가 pallet 의
114
+ * 높이 중간 (= pocket 공간) 에 정확히 위치.
115
+ *
116
+ * forklift-3d 의 cargoMountLocal 공식:
117
+ * mount.y = -forklift.depth/2 + wheelR + forkHeight + forkH
118
+ * fork blade top 의 world Y = forklift.world.y + mount.y (origin=center 가정).
119
+ *
120
+ * 목표: fork blade top world Y = pallet world center Y
121
+ * ⟹ forkHeight = pallet_worldY - forklift_worldY + forklift.depth/2 - wheelR - forkH
122
+ *
123
+ * forkHeight clamp: [0, forklift.depth * 0.85] (cargoMountLocal 의 hard cap).
124
+ */
125
+ engage(target: Component, kind: 'pick' | 'place', _options?: any): Promise<void>;
126
+ /**
127
+ * pick 보완: super.pick 후 자세 정렬 + attachPointFor 강제 재적용.
128
+ * 흐름의 마지막에 ContainerCapacity.receive 의 setState({left, top}) pipeline 이
129
+ * obj3d.position 을 덮는 것을 다시 잡음. suppressTransform=true 로 향후 차단.
130
+ * forklift 가 움직이면 carrier 는 scene-graph 통해 자동 추종.
131
+ */
132
+ pick(carrier: Component, options?: any): Promise<void>;
133
+ private _snapToAttachPoint;
99
134
  /**
100
135
  * 2D — top-down silhouette of a forklift. Layout (top-down view):
101
136
  *
package/dist/forklift.js CHANGED
@@ -2,7 +2,8 @@ import { __decorate } from "tslib";
2
2
  /*
3
3
  * Copyright © HatioLab Inc. All rights reserved.
4
4
  */
5
- import { ContainerAbstract, sceneComponent } from '@hatiolab/things-scene';
5
+ import * as THREE from 'three';
6
+ import { ContainerAbstract, ContainerCapacity, sceneComponent } from '@hatiolab/things-scene';
6
7
  import { CarrierHolder, FloorBound, Legendable, Mover, Placeable } from '@operato/scene-base';
7
8
  import { Forklift3D } from './forklift-3d.js';
8
9
  /** Body color — yellow base hue, modulated slightly by status for at-a-glance state. */
@@ -61,17 +62,17 @@ const NATURE = {
61
62
  ],
62
63
  help: 'scene/component/forklift'
63
64
  };
64
- // Composition order: Container → Placeable → Legendable → CarrierHolder → Mover.
65
- // Mover sits OUTSIDE CarrierHolder because Mover's defaults (`pick = this.reparent`,
66
- // `place = holder.reparent`) rely on CarrierHolder's reparent override being
67
- // already on the prototype chain. Default impls are instant reparent — Forklift
68
- // does not yet implement navigation+forkLift motion, so `await forklift.pick(box)`
69
- // just attaches the box to the forks immediately. Real motion follows in a
70
- // later commit (path planning, fork actuation animation).
71
- // FloorBound is the outermost wrap so its rotation guard sees state changes
72
- // last, after Mover's heading update has run any ±π corruption (saved-JSON
73
- // or editor gizmo) is clamped before the next 3D frame applies it.
74
- const Base = FloorBound(Mover(CarrierHolder(Legendable(Placeable(ContainerAbstract)))));
65
+ // Composition order:
66
+ // FloorBound Mover CarrierHolder ContainerCapacity Legendable Placeable → ContainerAbstract
67
+ //
68
+ // ContainerCapacity: receive() / dispatch() / canReceive() / slots Mover.pick uses
69
+ // receive() (slot-tracking path) instead of the reparent() fallback. Event emission
70
+ // ('transfer-received', 'transfer-dispatched') fires on every cargo transfer.
71
+ // CarrierHolder: attachPointFor() fork-tip 3D mount frame; Carriable.added()
72
+ // calls applyHolderAttachPoint() which uses our override for 3D positioning.
73
+ // Mover: moveTo / pick / place / pickAndPlace / executeMission.
74
+ // FloorBound: rotation guard outermost, sees state changes last.
75
+ const Base = FloorBound(Mover(CarrierHolder(ContainerCapacity(Legendable(Placeable(ContainerAbstract))))));
75
76
  /**
76
77
  * Forklift — a powered industrial truck used to lift and transport material
77
78
  * over short distances.
@@ -86,6 +87,12 @@ const Base = FloorBound(Mover(CarrierHolder(Legendable(Placeable(ContainerAbstra
86
87
  * change the rendering path, only how children's 3D objects are reparented.
87
88
  */
88
89
  let Forklift = class Forklift extends Base {
90
+ // `Base` is cast through `typeof Component` so TS sees `state` as an
91
+ // accessor; `declare state: …` would conflict with TS2610. Override the
92
+ // getter instead — runtime behavior is identical (just delegates to super).
93
+ get state() {
94
+ return super.state;
95
+ }
89
96
  static legends = {
90
97
  bodyColor: { from: 'status', legend: BODY_LEGEND },
91
98
  lampEmissive: { from: 'status', legend: LAMP_EMISSIVE_LEGEND }
@@ -112,22 +119,23 @@ let Forklift = class Forklift extends Base {
112
119
  get anchors() {
113
120
  return [];
114
121
  }
122
+ // ── ContainerCapacity ─────────────────────────────────────────────────────
115
123
  /**
116
- * Allow items that flow at operation level (boxes, cartons, parcels, loaded
117
- * pallets) to be added as cargo. Restricting by archetype rather than type
118
- * means new package components are auto-permitted as they're added to the
119
- * scene-base ecosystem.
120
- *
121
- * Note: this overrides CarrierHolder's default `containable` (which restricts
122
- * to `isCarriable === true`). Forklift accepts any operation-archetype
123
- * component so cargo authored before adopting Carriable still drops in.
124
+ * Forks hold up to 3 stacked load units (pallet + boxes).
125
+ * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx)
126
+ * the slot maxCount here is the hard cap for `canReceive()`.
127
+ */
128
+ get slots() {
129
+ // Single-cargo 정책: 이미 1개 보유 `canReceive()` → false → `pick()` 거부.
130
+ return [{ id: 'forks', maxCount: 1 }];
131
+ }
132
+ /**
133
+ * Forklift 정책: **파레트만** 자식으로 받음. 박스, 카트론, 파셀 등은 받지 않음.
134
+ * 실제 forklift 가 fork pocket 을 통해 들기 때문에 pocket 구조가 있는 파레트만
135
+ * 들 수 있음. 박스 등은 AGV 같은 평탄 데크 차량이 운반.
124
136
  */
125
137
  containable(component) {
126
- const archetype = component.constructor.placement;
127
- if (archetype === 'operation')
128
- return true;
129
- // Container's default — accept anything descendible (default things-scene policy).
130
- return component.isDescendible(this);
138
+ return component?.model?.type === 'pallet';
131
139
  }
132
140
  /**
133
141
  * Forklift has a single mount frame: the **forks**. Multiple cargo items
@@ -157,16 +165,31 @@ let Forklift = class Forklift extends Base {
157
165
  const attach = ro.modelGroup ?? ro.object3d;
158
166
  if (!attach)
159
167
  return undefined;
168
+ // mount: { x, y (fork blade top), z (mid-fork), width, depth (= forkLen) }
160
169
  const mount = ro.cargoMount;
161
170
  const slotIdx = this._slotIndexOf(carrier);
162
171
  const cargoDepth = carrier.state.depth ?? 100;
172
+ // carrier 의 carrier-local 의 fork 방향 길이 (height = -Z forward 방향).
173
+ const carrierLenAlongFork = carrier.state.height ?? cargoDepth;
174
+ // Fork-in-pocket 정렬: forklift 의 fork blade 가 파레트의 fork pocket 공간 안으로
175
+ // 들어가도록 carrier center y = mount.y (= fork blade top). carrier 의 하반부가 fork
176
+ // 아래로 내려와 fork 를 감쌈 (pocket 공간). carrier 의 상반부는 fork 위쪽 = 적재 영역.
177
+ // 이게 실제 forklift-pallet 결합 시각과 일치.
178
+ // (single-cargo 정책이라 slotIdx 는 항상 0; 멀티 stack 시 cargoDepth 로 stack)
179
+ const y = mount.y + slotIdx * cargoDepth;
180
+ // Z (fork 방향): carrier 의 forklift-쪽 면(= +Z, 본체에 가까운 쪽)을 **fork heel
181
+ // (= 포크의 시작부, 본체와 만나는 지점)** 에 정렬. carrier 본체는 fork tip 쪽으로
182
+ // 뻗어나가며 forklift 본체 안으로는 절대 파고들지 않음. 큰 파레트는 fork tip 너머로
183
+ // overhang.
184
+ // mount.z 는 fork mid, mount.depth 는 fork length. fork heel = mount.z + forkLen/2
185
+ // (heel 이 본체 쪽 = +Z, less negative).
186
+ const forkHeelZ = mount.z + mount.depth / 2;
187
+ const z = forkHeelZ - carrierLenAlongFork / 2;
163
188
  return {
164
189
  attach,
165
- localPosition: {
166
- x: mount.x,
167
- y: mount.y + slotIdx * cargoDepth,
168
- z: mount.z
169
- }
190
+ localPosition: { x: mount.x, y, z },
191
+ // Phase F: forklift 가 회전하면 fork 의 cargo 도 함께 회전 (fork 위 lock).
192
+ carryPolicy: 'follow-holder'
170
193
  };
171
194
  }
172
195
  /**
@@ -181,10 +204,83 @@ let Forklift = class Forklift extends Base {
181
204
  * once the add settles.
182
205
  */
183
206
  _slotIndexOf(carrier) {
184
- const children = this.children ?? [];
207
+ const children = this.components ?? [];
185
208
  const idx = children.indexOf(carrier);
186
209
  return idx < 0 ? children.length : idx;
187
210
  }
211
+ /**
212
+ * engage 보완: pick 시 fork blade top 의 world Y 가 carrier (파레트) 의
213
+ * world center Y 와 일치하도록 forkHeight 를 들어올림. → fork 가 pallet 의
214
+ * 높이 중간 (= pocket 공간) 에 정확히 위치.
215
+ *
216
+ * forklift-3d 의 cargoMountLocal 공식:
217
+ * mount.y = -forklift.depth/2 + wheelR + forkHeight + forkH
218
+ * fork blade top 의 world Y = forklift.world.y + mount.y (origin=center 가정).
219
+ *
220
+ * 목표: fork blade top world Y = pallet world center Y
221
+ * ⟹ forkHeight = pallet_worldY - forklift_worldY + forklift.depth/2 - wheelR - forkH
222
+ *
223
+ * forkHeight clamp: [0, forklift.depth * 0.85] (cargoMountLocal 의 hard cap).
224
+ */
225
+ async engage(target, kind, _options = {}) {
226
+ if (kind !== 'pick')
227
+ return;
228
+ const myObj3d = this._realObject?.object3d;
229
+ const targetObj3d = target._realObject?.object3d;
230
+ if (!myObj3d || !targetObj3d)
231
+ return;
232
+ myObj3d.updateWorldMatrix(true, false);
233
+ targetObj3d.updateWorldMatrix(true, false);
234
+ const myPos = new THREE.Vector3();
235
+ const targetPos = new THREE.Vector3();
236
+ myObj3d.getWorldPosition(myPos);
237
+ targetObj3d.getWorldPosition(targetPos);
238
+ const forkliftDepth = this.state.depth ?? 200;
239
+ const wheelR = forkliftDepth * 0.07;
240
+ const forkH = forkliftDepth * 0.018;
241
+ const desired = targetPos.y - myPos.y + forkliftDepth / 2 - wheelR - forkH;
242
+ const targetForkHeight = Math.max(0, Math.min(desired, forkliftDepth * 0.85));
243
+ this.setState?.({ forkHeight: targetForkHeight });
244
+ // mast 재구성 + attach point 재계산 안정화 시간.
245
+ await new Promise(resolve => setTimeout(resolve, 200));
246
+ }
247
+ /**
248
+ * pick 보완: super.pick 후 자세 정렬 + attachPointFor 강제 재적용.
249
+ * 흐름의 마지막에 ContainerCapacity.receive 의 setState({left, top}) pipeline 이
250
+ * obj3d.position 을 덮는 것을 다시 잡음. suppressTransform=true 로 향후 차단.
251
+ * forklift 가 움직이면 carrier 는 scene-graph 통해 자동 추종.
252
+ */
253
+ async pick(carrier, options = {}) {
254
+ if (!this.canReceive(carrier)) {
255
+ this.trigger('transfer-rejected', {
256
+ type: 'transfer-rejected',
257
+ component: carrier,
258
+ container: this,
259
+ reason: 'forklift-at-capacity'
260
+ });
261
+ return;
262
+ }
263
+ await super.pick(carrier, options);
264
+ carrier.setState?.({ rotation: 0, rotationX: 0, rotationY: 0 });
265
+ this._snapToAttachPoint(carrier);
266
+ }
267
+ _snapToAttachPoint(carrier) {
268
+ const point = this.attachPointFor(carrier);
269
+ const ro = carrier._realObject;
270
+ const obj3d = ro?.object3d;
271
+ if (!obj3d || !point?.localPosition)
272
+ return;
273
+ const lp = point.localPosition;
274
+ obj3d.position.set(lp.x, lp.y, lp.z);
275
+ if (point.localRotation) {
276
+ obj3d.rotation.set(point.localRotation.x, point.localRotation.y, point.localRotation.z);
277
+ }
278
+ else {
279
+ obj3d.quaternion.identity();
280
+ }
281
+ if (ro)
282
+ ro.suppressTransform = true;
283
+ }
188
284
  /**
189
285
  * 2D — top-down silhouette of a forklift. Layout (top-down view):
190
286
  *
@@ -308,7 +404,7 @@ let Forklift = class Forklift extends Base {
308
404
  super.onchange?.(after, before);
309
405
  if (!('forkHeight' in (after ?? {})))
310
406
  return;
311
- const children = this.children ?? [];
407
+ const children = this.components ?? [];
312
408
  for (const child of children) {
313
409
  if (child.applyHolderAttachPoint) {
314
410
  ;