@operato/scene-transport 10.0.0-beta.30 → 10.0.0-beta.32

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,44 @@
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.32](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.31...v10.0.0-beta.32) (2026-05-09)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * **mover/transport/manufacturing:** Phase H — Mover.pick 의 pickup compatibility 검사 ([d269366](https://github.com/things-scene/operato-scene/commit/d2693662f40f0fb6de1879b86c79f360348caaf9))
12
+ * **transport:** Phase H — Vehicle 별 toolType 추가 (Agv/Tugger/Worker) ([989596c](https://github.com/things-scene/operato-scene/commit/989596c04e29c43ec2406472ca5b3dd1d25a8bb4))
13
+
14
+
15
+ ### :bug: Bug Fix
16
+
17
+ * **manufacturing/transport:** Phase G6 — sim-time aware sleep + forklift settle ([864888f](https://github.com/things-scene/operato-scene/commit/864888fdcfdff628d19de99728e4748af6fcad08))
18
+
19
+
20
+
21
+ ## [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)
22
+
23
+
24
+ ### :rocket: New Features
25
+
26
+ * **conveyance+transport+manufacturing:** induct→sorter→chute 자세 보존 + AGV/Forklift pick 보완 ([28c2ed7](https://github.com/things-scene/operato-scene/commit/28c2ed7b64d87b27e72e8cbd36a48a5d26d6cf8a))
27
+ * **transport,conveyance:** Phase F Step 4-6 — carryPolicy 명시적 선언 ([522e003](https://github.com/things-scene/operato-scene/commit/522e0035ddf9357fb385183d03a54609b9e20ee1))
28
+
29
+
30
+ ### :bug: Bug Fix
31
+
32
+ * **transport/visualizer:** children → components — 잠재 버그 수정 (Phase C Round 4) ([8131166](https://github.com/things-scene/operato-scene/commit/813116650dd846becfbf4f9571893cff25f85e62))
33
+
34
+
35
+ ### :house: Code Refactoring
36
+
37
+ * _realObject 캐스트 16개 제거 (Phase C Round 2 후속) ([8616c6c](https://github.com/things-scene/operato-scene/commit/8616c6c45e38a9bbcc244a77cee53479a756df85))
38
+ * 39개 buildRealObject 캐스트 제거 (Phase D Round 4 후속) ([b86681c](https://github.com/things-scene/operato-scene/commit/b86681c07d7bf41cb93ade9855afb21902ed8186))
39
+ * **mfg/transport/storage:** typed state Round 2 — 14개 컴포넌트 적용 ([f1765f6](https://github.com/things-scene/operato-scene/commit/f1765f62d12cf4c20ea26a2297e98069b9621a45))
40
+ * 함수 인자 (this as any) 13개 제거 (Phase D Round 4 후속) ([b0a400c](https://github.com/things-scene/operato-scene/commit/b0a400c9716fdec22518cc6ad0bd1c1e69bda89d))
41
+
42
+
43
+
6
44
  ## [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)
7
45
 
8
46
 
package/dist/agv.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
2
- import type { SlotDef } from '@hatiolab/things-scene';
2
+ import type { SlotDef, State, Material3D } from '@hatiolab/things-scene';
3
3
  import { type Alignment, type CarrierAttachPoint, type Heights, type LegendBinding, type MoveOptions, type PlacementArchetype } from '@operato/scene-base';
4
4
  /**
5
5
  * Agv status — common to both payload and towing AGVs (kept narrow on purpose).
@@ -15,6 +15,13 @@ import { type Alignment, type CarrierAttachPoint, type Heights, type LegendBindi
15
15
  * invite drift.
16
16
  */
17
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
+ }
18
25
  declare const Base: typeof Component & {
19
26
  new (...args: any[]): Component & {
20
27
  isCarrierHolder: boolean;
@@ -38,6 +45,16 @@ declare const Base: typeof Component & {
38
45
  * For the towing variant (no cargo deck, pulls trailers behind), see Tugger.
39
46
  */
40
47
  export default class Agv extends Base {
48
+ get state(): AgvState;
49
+ /**
50
+ * Phase H — pickup contract 호환성 type. AGV 는 carrier 를 deck 에 올리는
51
+ * 식이라 carrier 가 'agv-deck' 진입을 노출하면 매칭. (예: pallet 위 AGV 가
52
+ * 자체 deck 으로 pallet 들어올리는 시나리오)
53
+ *
54
+ * (override 키워드 미사용 — Base 캐스트가 Mover 의 toolType 을 노출하지 않아
55
+ * TS4113 회피.)
56
+ */
57
+ get toolType(): string;
41
58
  static legends: Record<string, LegendBinding>;
42
59
  /**
43
60
  * AGV sits on its wheels — `floor` archetype. Default depth = operation,
@@ -60,8 +77,8 @@ export default class Agv extends Base {
60
77
  get nature(): ComponentNature;
61
78
  get anchors(): never[];
62
79
  /**
63
- * Flat deck holds up to 2 stacked load units.
64
- * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx).
80
+ * Single-cargo policy: AGV 번에 1개의 carrier 만 운반.
81
+ * 이미 1개를 보유한 상태에서는 `canReceive()` false `pick()` 거부.
65
82
  */
66
83
  get slots(): SlotDef[];
67
84
  /** Accept logistics packages (placement='operation') as deck cargo. */
@@ -82,6 +99,17 @@ export default class Agv extends Base {
82
99
  * children order is preserved; no persisted state needed.
83
100
  */
84
101
  private _slotIndexOf;
102
+ /**
103
+ * pick 보완: super.pick 후 carrier 의 자세 정렬 + attachPointFor 강제 재적용.
104
+ *
105
+ * 흐름의 마지막에 ContainerCapacity.receive 의 setState({left, top}) 가 표준
106
+ * pipeline 을 트리거해 obj3d.position 이 attachPointFor 의 localPosition 을 덮어씀.
107
+ * (= 첫번째 carrier 가 데크 위가 아니라 잘못된 위치/공중에 잡히는 원인)
108
+ * super.pick 종료 후 attachPointFor 를 다시 적용하고 suppressTransform=true 로
109
+ * pipeline 의 추가 override 차단. AGV 가 움직이면 carrier 는 scene-graph 통해 자동 추종.
110
+ */
111
+ pick(carrier: Component, options?: any): Promise<void>;
112
+ private _snapToAttachPoint;
85
113
  /**
86
114
  * 2D — render() sets up the rounded chassis path; the framework fills it
87
115
  * with `fillStyle` (= bodyColor from Legendable) and strokes with
package/dist/agv.js CHANGED
@@ -90,6 +90,23 @@ const Base = FloorBound(Mover(CarrierHolder(ContainerCapacity(Legendable(Placeab
90
90
  * For the towing variant (no cargo deck, pulls trailers behind), see Tugger.
91
91
  */
92
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
+ }
99
+ /**
100
+ * Phase H — pickup contract 호환성 type. AGV 는 carrier 를 deck 에 올리는
101
+ * 식이라 carrier 가 'agv-deck' 진입을 노출하면 매칭. (예: pallet 위 AGV 가
102
+ * 자체 deck 으로 pallet 들어올리는 시나리오)
103
+ *
104
+ * (override 키워드 미사용 — Base 캐스트가 Mover 의 toolType 을 노출하지 않아
105
+ * TS4113 회피.)
106
+ */
107
+ get toolType() {
108
+ return 'agv-deck';
109
+ }
93
110
  static legends = {
94
111
  bodyColor: { from: 'status', legend: BODY_LEGEND },
95
112
  lampEmissive: { from: 'status', legend: LAMP_EMISSIVE_LEGEND }
@@ -120,11 +137,11 @@ let Agv = class Agv extends Base {
120
137
  }
121
138
  // ── ContainerCapacity ─────────────────────────────────────────────────────
122
139
  /**
123
- * Flat deck holds up to 2 stacked load units.
124
- * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx).
140
+ * Single-cargo policy: AGV 번에 1개의 carrier 만 운반.
141
+ * 이미 1개를 보유한 상태에서는 `canReceive()` false `pick()` 거부.
125
142
  */
126
143
  get slots() {
127
- return [{ id: 'deck', maxCount: 2 }];
144
+ return [{ id: 'deck', maxCount: 1 }];
128
145
  }
129
146
  /** Accept logistics packages (placement='operation') as deck cargo. */
130
147
  containable(component) {
@@ -150,14 +167,19 @@ let Agv = class Agv extends Base {
150
167
  const depth = this.state.depth ?? 0;
151
168
  const slotIdx = this._slotIndexOf(carrier);
152
169
  const cargoDepth = carrier.state.depth ?? 100;
170
+ // 3D origin convention (beta.66+): carrier 의 origin = center.
171
+ // 데크 표면 (= AGV 상단) y = depth/2. carrier 의 bottom 을 데크 표면에 두려면
172
+ // carrier center y = deckY + cargoDepth/2. 이후 slot 별로 cargoDepth 씩 stack.
153
173
  const deckY = depth / 2;
154
174
  return {
155
175
  attach: ro.object3d,
156
176
  localPosition: {
157
177
  x: 0,
158
- y: deckY + slotIdx * cargoDepth,
178
+ y: deckY + cargoDepth / 2 + slotIdx * cargoDepth,
159
179
  z: 0
160
- }
180
+ },
181
+ // Phase F: AGV 가 회전하면 cargo 도 같이 회전 (deck 위 고정 lock).
182
+ carryPolicy: 'follow-holder'
161
183
  };
162
184
  }
163
185
  /**
@@ -165,10 +187,51 @@ let Agv = class Agv extends Base {
165
187
  * children order is preserved; no persisted state needed.
166
188
  */
167
189
  _slotIndexOf(carrier) {
168
- const children = this.children ?? [];
190
+ const children = this.components ?? [];
169
191
  const idx = children.indexOf(carrier);
170
192
  return idx < 0 ? children.length : idx;
171
193
  }
194
+ /**
195
+ * pick 보완: super.pick 후 carrier 의 자세 정렬 + attachPointFor 강제 재적용.
196
+ *
197
+ * 흐름의 마지막에 ContainerCapacity.receive 의 setState({left, top}) 가 표준
198
+ * pipeline 을 트리거해 obj3d.position 이 attachPointFor 의 localPosition 을 덮어씀.
199
+ * (= 첫번째 carrier 가 데크 위가 아니라 잘못된 위치/공중에 잡히는 원인)
200
+ * super.pick 종료 후 attachPointFor 를 다시 적용하고 suppressTransform=true 로
201
+ * pipeline 의 추가 override 차단. AGV 가 움직이면 carrier 는 scene-graph 통해 자동 추종.
202
+ */
203
+ async pick(carrier, options = {}) {
204
+ // 1-capacity 정책: 이미 1개 보유 시 pick 거부 (moveTo / engage 낭비 방지).
205
+ if (!this.canReceive(carrier)) {
206
+ this.trigger('transfer-rejected', {
207
+ type: 'transfer-rejected',
208
+ component: carrier,
209
+ container: this,
210
+ reason: 'agv-at-capacity'
211
+ });
212
+ return;
213
+ }
214
+ await super.pick(carrier, options);
215
+ carrier.setState?.({ rotation: 0, rotationX: 0, rotationY: 0 });
216
+ this._snapToAttachPoint(carrier);
217
+ }
218
+ _snapToAttachPoint(carrier) {
219
+ const point = this.attachPointFor(carrier);
220
+ const ro = carrier._realObject;
221
+ const obj3d = ro?.object3d;
222
+ if (!obj3d || !point?.localPosition)
223
+ return;
224
+ const lp = point.localPosition;
225
+ obj3d.position.set(lp.x, lp.y, lp.z);
226
+ if (point.localRotation) {
227
+ obj3d.rotation.set(point.localRotation.x, point.localRotation.y, point.localRotation.z);
228
+ }
229
+ else {
230
+ obj3d.quaternion.identity();
231
+ }
232
+ if (ro)
233
+ ro.suppressTransform = true;
234
+ }
172
235
  /**
173
236
  * 2D — render() sets up the rounded chassis path; the framework fills it
174
237
  * 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, ContainerCapacity, RealObject, sceneComponent } from '@hatiolab/things-scene'\nimport type { SlotDef } 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:\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 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 * Flat deck holds up to 2 stacked load units.\n * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx).\n */\n get slots(): SlotDef[] {\n return [{ id: 'deck', maxCount: 2 }]\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 /**\n * Phase H — pickup contract 호환성 type. AGV 는 carrier 를 deck 에 올리는\n * 식이라 carrier 가 'agv-deck' 진입을 노출하면 매칭. (예: pallet 위 AGV 가\n * 자체 deck 으로 pallet 들어올리는 시나리오)\n *\n * (override 키워드 미사용 — Base 캐스트가 Mover 의 toolType 을 노출하지 않아\n * TS4113 회피.)\n */\n get toolType(): string {\n return 'agv-deck'\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,6 +1,7 @@
1
1
  import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
2
- import type { SlotDef } from '@hatiolab/things-scene';
2
+ import type { SlotDef, State, Material3D } from '@hatiolab/things-scene';
3
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';
4
5
  /**
5
6
  * Forklift status — the operating state of a forklift truck.
6
7
  *
@@ -14,6 +15,13 @@ import { type Alignment, type CarrierAttachPoint, type Heights, type LegendBindi
14
15
  * loaded-vs-empty distinction relevant for fleet visualization.
15
16
  */
16
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
+ }
17
25
  declare const Base: typeof Component & {
18
26
  new (...args: any[]): Component & {
19
27
  isCarrierHolder: boolean;
@@ -38,6 +46,18 @@ declare const Base: typeof Component & {
38
46
  * change the rendering path, only how children's 3D objects are reparented.
39
47
  */
40
48
  export default class Forklift extends Base {
49
+ get state(): ForkliftState;
50
+ _realObject: Forklift3D | undefined;
51
+ /**
52
+ * Phase H — pickup contract 호환성 검사용. carrier 의 pickupFramesFor 가
53
+ * 'forklift-fork' toolType 으로 query 받았을 때 매칭되는 entry 만 통과한다.
54
+ * Pallet 처럼 fork 진입 가능한 carrier 는 이 type 에 호환되는 entry 를 선언하고,
55
+ * 호환 안 되는 carrier (예: drum 의 magnet-only) 는 자동으로 reject 된다.
56
+ *
57
+ * (override 키워드 미사용 — Base 캐스트가 Mover 의 toolType 을 노출하지 않아
58
+ * TS4113 회피.)
59
+ */
60
+ get toolType(): string;
41
61
  static legends: Record<string, LegendBinding>;
42
62
  /**
43
63
  * Forklift sits on its wheels — `floor` archetype. Default depth is the
@@ -64,14 +84,9 @@ export default class Forklift extends Base {
64
84
  */
65
85
  get slots(): SlotDef[];
66
86
  /**
67
- * Allow items that flow at operation level (boxes, cartons, parcels, loaded
68
- * pallets) to be added as cargo. Restricting by archetype rather than type
69
- * means new package components are auto-permitted as they're added to the
70
- * scene-base ecosystem.
71
- *
72
- * Note: this overrides CarrierHolder's default `containable` (which restricts
73
- * to `isCarriable === true`). Forklift accepts any operation-archetype
74
- * component so cargo authored before adopting Carriable still drops in.
87
+ * Forklift 정책: **파레트만** 자식으로 받음. 박스, 카트론, 파셀 등은 받지 않음.
88
+ * 실제 forklift fork pocket 통해 들기 때문에 pocket 구조가 있는 파레트만
89
+ * 있음. 박스 등은 AGV 같은 평탄 데크 차량이 운반.
75
90
  */
76
91
  containable(component: Component): boolean;
77
92
  /**
@@ -103,6 +118,29 @@ export default class Forklift extends Base {
103
118
  * once the add settles.
104
119
  */
105
120
  private _slotIndexOf;
121
+ /**
122
+ * engage 보완: pick 시 fork blade top 의 world Y 가 carrier (파레트) 의
123
+ * world center Y 와 일치하도록 forkHeight 를 들어올림. → fork 가 pallet 의
124
+ * 높이 중간 (= pocket 공간) 에 정확히 위치.
125
+ *
126
+ * forklift-3d 의 cargoMountLocal 공식:
127
+ * mount.y = -forklift.depth/2 + wheelR + forkHeight + forkH
128
+ * fork blade top 의 world Y = forklift.world.y + mount.y (origin=center 가정).
129
+ *
130
+ * 목표: fork blade top world Y = pallet world center Y
131
+ * ⟹ forkHeight = pallet_worldY - forklift_worldY + forklift.depth/2 - wheelR - forkH
132
+ *
133
+ * forkHeight clamp: [0, forklift.depth * 0.85] (cargoMountLocal 의 hard cap).
134
+ */
135
+ engage(target: Component, kind: 'pick' | 'place', _options?: any): Promise<void>;
136
+ /**
137
+ * pick 보완: super.pick 후 자세 정렬 + attachPointFor 강제 재적용.
138
+ * 흐름의 마지막에 ContainerCapacity.receive 의 setState({left, top}) pipeline 이
139
+ * obj3d.position 을 덮는 것을 다시 잡음. suppressTransform=true 로 향후 차단.
140
+ * forklift 가 움직이면 carrier 는 scene-graph 통해 자동 추종.
141
+ */
142
+ pick(carrier: Component, options?: any): Promise<void>;
143
+ private _snapToAttachPoint;
106
144
  /**
107
145
  * 2D — top-down silhouette of a forklift. Layout (top-down view):
108
146
  *
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, ContainerCapacity, sceneComponent } from '@hatiolab/things-scene';
5
+ import * as THREE from 'three';
6
+ import { ContainerAbstract, ContainerCapacity, frameClock, 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. */
@@ -86,6 +87,24 @@ const Base = FloorBound(Mover(CarrierHolder(ContainerCapacity(Legendable(Placeab
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
+ }
96
+ /**
97
+ * Phase H — pickup contract 호환성 검사용. carrier 의 pickupFramesFor 가
98
+ * 'forklift-fork' toolType 으로 query 받았을 때 매칭되는 entry 만 통과한다.
99
+ * Pallet 처럼 fork 진입 가능한 carrier 는 이 type 에 호환되는 entry 를 선언하고,
100
+ * 호환 안 되는 carrier (예: drum 의 magnet-only) 는 자동으로 reject 된다.
101
+ *
102
+ * (override 키워드 미사용 — Base 캐스트가 Mover 의 toolType 을 노출하지 않아
103
+ * TS4113 회피.)
104
+ */
105
+ get toolType() {
106
+ return 'forklift-fork';
107
+ }
89
108
  static legends = {
90
109
  bodyColor: { from: 'status', legend: BODY_LEGEND },
91
110
  lampEmissive: { from: 'status', legend: LAMP_EMISSIVE_LEGEND }
@@ -119,24 +138,16 @@ let Forklift = class Forklift extends Base {
119
138
  * the slot maxCount here is the hard cap for `canReceive()`.
120
139
  */
121
140
  get slots() {
122
- return [{ id: 'forks', maxCount: 3 }];
141
+ // Single-cargo 정책: 이미 1개 보유 시 `canReceive()` → false → `pick()` 거부.
142
+ return [{ id: 'forks', maxCount: 1 }];
123
143
  }
124
144
  /**
125
- * Allow items that flow at operation level (boxes, cartons, parcels, loaded
126
- * pallets) to be added as cargo. Restricting by archetype rather than type
127
- * means new package components are auto-permitted as they're added to the
128
- * scene-base ecosystem.
129
- *
130
- * Note: this overrides CarrierHolder's default `containable` (which restricts
131
- * to `isCarriable === true`). Forklift accepts any operation-archetype
132
- * component so cargo authored before adopting Carriable still drops in.
145
+ * Forklift 정책: **파레트만** 자식으로 받음. 박스, 카트론, 파셀 등은 받지 않음.
146
+ * 실제 forklift fork pocket 통해 들기 때문에 pocket 구조가 있는 파레트만
147
+ * 있음. 박스 등은 AGV 같은 평탄 데크 차량이 운반.
133
148
  */
134
149
  containable(component) {
135
- const archetype = component.constructor.placement;
136
- if (archetype === 'operation')
137
- return true;
138
- // Container's default — accept anything descendible (default things-scene policy).
139
- return component.isDescendible(this);
150
+ return component?.model?.type === 'pallet';
140
151
  }
141
152
  /**
142
153
  * Forklift has a single mount frame: the **forks**. Multiple cargo items
@@ -166,16 +177,31 @@ let Forklift = class Forklift extends Base {
166
177
  const attach = ro.modelGroup ?? ro.object3d;
167
178
  if (!attach)
168
179
  return undefined;
180
+ // mount: { x, y (fork blade top), z (mid-fork), width, depth (= forkLen) }
169
181
  const mount = ro.cargoMount;
170
182
  const slotIdx = this._slotIndexOf(carrier);
171
183
  const cargoDepth = carrier.state.depth ?? 100;
184
+ // carrier 의 carrier-local 의 fork 방향 길이 (height = -Z forward 방향).
185
+ const carrierLenAlongFork = carrier.state.height ?? cargoDepth;
186
+ // Fork-in-pocket 정렬: forklift 의 fork blade 가 파레트의 fork pocket 공간 안으로
187
+ // 들어가도록 carrier center y = mount.y (= fork blade top). carrier 의 하반부가 fork
188
+ // 아래로 내려와 fork 를 감쌈 (pocket 공간). carrier 의 상반부는 fork 위쪽 = 적재 영역.
189
+ // 이게 실제 forklift-pallet 결합 시각과 일치.
190
+ // (single-cargo 정책이라 slotIdx 는 항상 0; 멀티 stack 시 cargoDepth 로 stack)
191
+ const y = mount.y + slotIdx * cargoDepth;
192
+ // Z (fork 방향): carrier 의 forklift-쪽 면(= +Z, 본체에 가까운 쪽)을 **fork heel
193
+ // (= 포크의 시작부, 본체와 만나는 지점)** 에 정렬. carrier 본체는 fork tip 쪽으로
194
+ // 뻗어나가며 forklift 본체 안으로는 절대 파고들지 않음. 큰 파레트는 fork tip 너머로
195
+ // overhang.
196
+ // mount.z 는 fork mid, mount.depth 는 fork length. fork heel = mount.z + forkLen/2
197
+ // (heel 이 본체 쪽 = +Z, less negative).
198
+ const forkHeelZ = mount.z + mount.depth / 2;
199
+ const z = forkHeelZ - carrierLenAlongFork / 2;
172
200
  return {
173
201
  attach,
174
- localPosition: {
175
- x: mount.x,
176
- y: mount.y + slotIdx * cargoDepth,
177
- z: mount.z
178
- }
202
+ localPosition: { x: mount.x, y, z },
203
+ // Phase F: forklift 가 회전하면 fork 의 cargo 도 함께 회전 (fork 위 lock).
204
+ carryPolicy: 'follow-holder'
179
205
  };
180
206
  }
181
207
  /**
@@ -190,10 +216,95 @@ let Forklift = class Forklift extends Base {
190
216
  * once the add settles.
191
217
  */
192
218
  _slotIndexOf(carrier) {
193
- const children = this.children ?? [];
219
+ const children = this.components ?? [];
194
220
  const idx = children.indexOf(carrier);
195
221
  return idx < 0 ? children.length : idx;
196
222
  }
223
+ /**
224
+ * engage 보완: pick 시 fork blade top 의 world Y 가 carrier (파레트) 의
225
+ * world center Y 와 일치하도록 forkHeight 를 들어올림. → fork 가 pallet 의
226
+ * 높이 중간 (= pocket 공간) 에 정확히 위치.
227
+ *
228
+ * forklift-3d 의 cargoMountLocal 공식:
229
+ * mount.y = -forklift.depth/2 + wheelR + forkHeight + forkH
230
+ * fork blade top 의 world Y = forklift.world.y + mount.y (origin=center 가정).
231
+ *
232
+ * 목표: fork blade top world Y = pallet world center Y
233
+ * ⟹ forkHeight = pallet_worldY - forklift_worldY + forklift.depth/2 - wheelR - forkH
234
+ *
235
+ * forkHeight clamp: [0, forklift.depth * 0.85] (cargoMountLocal 의 hard cap).
236
+ */
237
+ async engage(target, kind, _options = {}) {
238
+ if (kind !== 'pick')
239
+ return;
240
+ const myObj3d = this._realObject?.object3d;
241
+ const targetObj3d = target._realObject?.object3d;
242
+ if (!myObj3d || !targetObj3d)
243
+ return;
244
+ myObj3d.updateWorldMatrix(true, false);
245
+ targetObj3d.updateWorldMatrix(true, false);
246
+ const myPos = new THREE.Vector3();
247
+ const targetPos = new THREE.Vector3();
248
+ myObj3d.getWorldPosition(myPos);
249
+ targetObj3d.getWorldPosition(targetPos);
250
+ const forkliftDepth = this.state.depth ?? 200;
251
+ const wheelR = forkliftDepth * 0.07;
252
+ const forkH = forkliftDepth * 0.018;
253
+ const desired = targetPos.y - myPos.y + forkliftDepth / 2 - wheelR - forkH;
254
+ const targetForkHeight = Math.max(0, Math.min(desired, forkliftDepth * 0.85));
255
+ this.setState?.({ forkHeight: targetForkHeight });
256
+ // mast 재구성 + attach point 재계산 안정화 시간 — sim-time 기준 (frameClock).
257
+ // speed 변경 시에도 sim animation 과 같은 시간축 유지.
258
+ await new Promise(resolve => {
259
+ let elapsed = 0;
260
+ let unsub = null;
261
+ unsub = frameClock.subscribe(simDt => {
262
+ elapsed += simDt;
263
+ if (elapsed >= 200) {
264
+ unsub?.();
265
+ unsub = null;
266
+ resolve();
267
+ }
268
+ }, { description: 'forklift mast settle' });
269
+ });
270
+ }
271
+ /**
272
+ * pick 보완: super.pick 후 자세 정렬 + attachPointFor 강제 재적용.
273
+ * 흐름의 마지막에 ContainerCapacity.receive 의 setState({left, top}) pipeline 이
274
+ * obj3d.position 을 덮는 것을 다시 잡음. suppressTransform=true 로 향후 차단.
275
+ * forklift 가 움직이면 carrier 는 scene-graph 통해 자동 추종.
276
+ */
277
+ async pick(carrier, options = {}) {
278
+ if (!this.canReceive(carrier)) {
279
+ this.trigger('transfer-rejected', {
280
+ type: 'transfer-rejected',
281
+ component: carrier,
282
+ container: this,
283
+ reason: 'forklift-at-capacity'
284
+ });
285
+ return;
286
+ }
287
+ await super.pick(carrier, options);
288
+ carrier.setState?.({ rotation: 0, rotationX: 0, rotationY: 0 });
289
+ this._snapToAttachPoint(carrier);
290
+ }
291
+ _snapToAttachPoint(carrier) {
292
+ const point = this.attachPointFor(carrier);
293
+ const ro = carrier._realObject;
294
+ const obj3d = ro?.object3d;
295
+ if (!obj3d || !point?.localPosition)
296
+ return;
297
+ const lp = point.localPosition;
298
+ obj3d.position.set(lp.x, lp.y, lp.z);
299
+ if (point.localRotation) {
300
+ obj3d.rotation.set(point.localRotation.x, point.localRotation.y, point.localRotation.z);
301
+ }
302
+ else {
303
+ obj3d.quaternion.identity();
304
+ }
305
+ if (ro)
306
+ ro.suppressTransform = true;
307
+ }
197
308
  /**
198
309
  * 2D — top-down silhouette of a forklift. Layout (top-down view):
199
310
  *
@@ -317,7 +428,7 @@ let Forklift = class Forklift extends Base {
317
428
  super.onchange?.(after, before);
318
429
  if (!('forkHeight' in (after ?? {})))
319
430
  return;
320
- const children = this.children ?? [];
431
+ const children = this.components ?? [];
321
432
  for (const child of children) {
322
433
  if (child.applyHolderAttachPoint) {
323
434
  ;