@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 +32 -0
- package/dist/agv.d.ts +25 -0
- package/dist/agv.js +76 -13
- package/dist/agv.js.map +1 -1
- package/dist/forklift.d.ts +43 -8
- package/dist/forklift.js +128 -32
- package/dist/forklift.js.map +1 -1
- package/dist/generic-transport-3d.js.map +1 -1
- package/dist/generic-transport.d.ts +17 -0
- package/dist/generic-transport.js +30 -13
- package/dist/generic-transport.js.map +1 -1
- package/dist/tugger.d.ts +9 -0
- package/dist/tugger.js +6 -0
- package/dist/tugger.js.map +1 -1
- package/dist/worker.d.ts +9 -0
- package/dist/worker.js +6 -0
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/src/agv.ts +95 -16
- package/src/forklift.ts +152 -36
- package/src/generic-transport-3d.ts +1 -1
- package/src/generic-transport.ts +57 -20
- package/src/tugger.ts +23 -2
- package/src/worker.ts +25 -2
- package/test/test-phase-f-policies.ts +192 -0
- package/tsconfig.tsbuildinfo +1 -1
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:
|
|
68
|
-
//
|
|
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
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
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.
|
|
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"]}
|
package/dist/forklift.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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
|
|
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:
|
|
65
|
-
// Mover
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
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
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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.
|
|
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.
|
|
407
|
+
const children = this.components ?? [];
|
|
312
408
|
for (const child of children) {
|
|
313
409
|
if (child.applyHolderAttachPoint) {
|
|
314
410
|
;
|