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

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,15 @@
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.30](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.29...v10.0.0-beta.30) (2026-05-07)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * **conveyance:** InductStation + CrossBeltLine + Chute 3D 시뮬레이션 전면 개선 ([e729b9d](https://github.com/things-scene/operato-scene/commit/e729b9dbb736ce1f12a4de789738ec1e58bf1dbc))
12
+
13
+
14
+
6
15
  ## [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
16
 
8
17
 
package/dist/agv.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
2
+ import type { SlotDef } 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).
@@ -58,6 +59,11 @@ export default class Agv extends Base {
58
59
  static yawOffset: number;
59
60
  get nature(): ComponentNature;
60
61
  get anchors(): never[];
62
+ /**
63
+ * Flat deck holds up to 2 stacked load units.
64
+ * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx).
65
+ */
66
+ get slots(): SlotDef[];
61
67
  /** Accept logistics packages (placement='operation') as deck cargo. */
62
68
  containable(component: Component): boolean;
63
69
  /**
package/dist/agv.js CHANGED
@@ -2,7 +2,7 @@ import { __decorate } from "tslib";
2
2
  /*
3
3
  * Copyright © HatioLab Inc. All rights reserved.
4
4
  */
5
- import { ContainerAbstract, sceneComponent } from '@hatiolab/things-scene';
5
+ import { ContainerAbstract, ContainerCapacity, sceneComponent } from '@hatiolab/things-scene';
6
6
  import { CarrierHolder, FloorBound, Legendable, Mover, Placeable } from '@operato/scene-base';
7
7
  import { Agv3D } from './agv-3d.js';
8
8
  /**
@@ -64,16 +64,19 @@ const NATURE = {
64
64
  ],
65
65
  help: 'scene/component/agv'
66
66
  };
67
- // Composition: ContainerAbstract → Placeable → Legendable → CarrierHolder → Mover → FloorBound.
68
- // Same chain as Forklift the AGV is a wheeled, payload-carrying vehicle
69
- // with the same pick/place semantics; differences (deck-stack instead of
70
- // fork-mount) are encoded in `attachPointFor`. FloorBound is outermost so
71
- // its rotation guard sees state changes last.
67
+ // Composition:
68
+ // FloorBound Mover CarrierHolder ContainerCapacity Legendable Placeable ContainerAbstract
72
69
  //
73
- // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
74
- // which forces `isHTMLElement(): true` and trips the 3D pipeline's
75
- // addObject DOM-skip gate. AGV lives only in the 3D scene graph; no DOM.
76
- const Base = FloorBound(Mover(CarrierHolder(Legendable(Placeable(ContainerAbstract)))));
70
+ // ContainerCapacity: receive() / dispatch() / canReceive() / slots Mover.pick uses
71
+ // receive() for slot-tracking and event emission on every cargo transfer.
72
+ // CarrierHolder: attachPointFor() deck-top 3D mount frame; Carriable.added()
73
+ // calls applyHolderAttachPoint() which uses our override for 3D positioning.
74
+ // Mover: moveTo / pick / place / pickAndPlace / executeMission.
75
+ // FloorBound: outermost rotation guard.
76
+ //
77
+ // `ContainerAbstract` (not `Container`) — avoids isHTMLElement()=true that would
78
+ // trip the 3D pipeline's addObject DOM-skip gate.
79
+ const Base = FloorBound(Mover(CarrierHolder(ContainerCapacity(Legendable(Placeable(ContainerAbstract))))));
77
80
  /**
78
81
  * Agv — payload (unit-load) automated guided vehicle. The Kiva-style flat-deck
79
82
  * AGV that drives under or carries cargo to/from operation surfaces.
@@ -115,6 +118,14 @@ let Agv = class Agv extends Base {
115
118
  get anchors() {
116
119
  return [];
117
120
  }
121
+ // ── ContainerCapacity ─────────────────────────────────────────────────────
122
+ /**
123
+ * Flat deck holds up to 2 stacked load units.
124
+ * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx).
125
+ */
126
+ get slots() {
127
+ return [{ id: 'deck', maxCount: 2 }];
128
+ }
118
129
  /** Accept logistics packages (placement='operation') as deck cargo. */
119
130
  containable(component) {
120
131
  const archetype = component.constructor.placement;
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 } 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,4 +1,5 @@
1
1
  import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
2
+ import type { SlotDef } 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
  * Forklift status — the operating state of a forklift truck.
@@ -56,6 +57,12 @@ export default class Forklift extends Base {
56
57
  static yawOffset: number;
57
58
  get nature(): ComponentNature;
58
59
  get anchors(): never[];
60
+ /**
61
+ * Forks hold up to 3 stacked load units (pallet + boxes).
62
+ * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx) —
63
+ * the slot maxCount here is the hard cap for `canReceive()`.
64
+ */
65
+ get slots(): SlotDef[];
59
66
  /**
60
67
  * Allow items that flow at operation level (boxes, cartons, parcels, loaded
61
68
  * pallets) to be added as cargo. Restricting by archetype rather than type
package/dist/forklift.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 { Forklift3D } from './forklift-3d.js';
8
8
  /** Body color — yellow base hue, modulated slightly by status for at-a-glance state. */
@@ -61,17 +61,17 @@ const NATURE = {
61
61
  ],
62
62
  help: 'scene/component/forklift'
63
63
  };
64
- // Composition order: Container → Placeable → Legendable → CarrierHolder → Mover.
65
- // Mover sits OUTSIDE CarrierHolder because Mover's defaults (`pick = this.reparent`,
66
- // `place = holder.reparent`) rely on CarrierHolder's reparent override being
67
- // already on the prototype chain. Default impls are instant reparent — Forklift
68
- // does not yet implement navigation+forkLift motion, so `await forklift.pick(box)`
69
- // just attaches the box to the forks immediately. Real motion follows in a
70
- // later commit (path planning, fork actuation animation).
71
- // FloorBound is the outermost wrap so its rotation guard sees state changes
72
- // last, after Mover's heading update has run any ±π corruption (saved-JSON
73
- // or editor gizmo) is clamped before the next 3D frame applies it.
74
- const Base = FloorBound(Mover(CarrierHolder(Legendable(Placeable(ContainerAbstract)))));
64
+ // Composition order:
65
+ // FloorBound Mover CarrierHolder ContainerCapacity Legendable Placeable → ContainerAbstract
66
+ //
67
+ // ContainerCapacity: receive() / dispatch() / canReceive() / slots Mover.pick uses
68
+ // receive() (slot-tracking path) instead of the reparent() fallback. Event emission
69
+ // ('transfer-received', 'transfer-dispatched') fires on every cargo transfer.
70
+ // CarrierHolder: attachPointFor() fork-tip 3D mount frame; Carriable.added()
71
+ // calls applyHolderAttachPoint() which uses our override for 3D positioning.
72
+ // Mover: moveTo / pick / place / pickAndPlace / executeMission.
73
+ // FloorBound: rotation guard outermost, sees state changes last.
74
+ const Base = FloorBound(Mover(CarrierHolder(ContainerCapacity(Legendable(Placeable(ContainerAbstract))))));
75
75
  /**
76
76
  * Forklift — a powered industrial truck used to lift and transport material
77
77
  * over short distances.
@@ -112,6 +112,15 @@ let Forklift = class Forklift extends Base {
112
112
  get anchors() {
113
113
  return [];
114
114
  }
115
+ // ── ContainerCapacity ─────────────────────────────────────────────────────
116
+ /**
117
+ * Forks hold up to 3 stacked load units (pallet + boxes).
118
+ * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx) —
119
+ * the slot maxCount here is the hard cap for `canReceive()`.
120
+ */
121
+ get slots() {
122
+ return [{ id: 'forks', maxCount: 3 }];
123
+ }
115
124
  /**
116
125
  * Allow items that flow at operation level (boxes, cartons, parcels, loaded
117
126
  * pallets) to be added as cargo. Restricting by archetype rather than type
@@ -1 +1 @@
1
- {"version":3,"file":"forklift.js","sourceRoot":"","sources":["../src/forklift.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 { Forklift3D } from './forklift-3d.js'\n\n/**\n * Forklift status — the operating state of a forklift truck.\n *\n * - `idle` — parked, engine off\n * - `running` — driving (no load)\n * - `lifting` — actively lifting / lowering forks\n * - `loaded` — driving with cargo on forks\n * - `error` — fault / emergency stop\n *\n * The 5-state set differs from Conveyor's because forklifts have a distinct\n * loaded-vs-empty distinction relevant for fleet visualization.\n */\nexport type ForkliftStatus = 'idle' | 'running' | 'lifting' | 'loaded' | 'error'\n\n/** Body color — yellow base hue, modulated slightly by status for at-a-glance state. */\nconst BODY_LEGEND = {\n idle: '#d4a017', // muted yellow (parked)\n running: '#FFD700', // bright yellow (active)\n lifting: '#FFD700',\n loaded: '#FFA500', // orange tint (carrying)\n error: '#e9746b', // red\n default: '#d4a017'\n}\n\n/** Status lamp emissive — saturated for at-a-glance status from a distance. */\nconst LAMP_EMISSIVE_LEGEND = {\n idle: '#333333',\n running: '#44ff44', // green (operating)\n lifting: '#44aaff', // blue (lifting)\n loaded: '#ffaa00', // amber (loaded)\n error: '#ff3333', // red\n default: '#333333'\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: 'Running', value: 'running' },\n { display: 'Lifting', value: 'lifting' },\n { display: 'Loaded', value: 'loaded' },\n { display: 'Error', value: 'error' }\n ]\n }\n },\n {\n type: 'number',\n label: 'fork-height',\n name: 'forkHeight',\n placeholder: '0..2000mm'\n },\n {\n // Forklift travel speed in scene units / second. Used by `Mover.moveTo`\n // to derive motion duration as `distance / speed × 1000`. Set to a value\n // that gives the look you want on this board's scale — defaults are\n // intentionally unguessed (board scale varies).\n type: 'number',\n label: 'speed',\n name: 'speed',\n placeholder: 'units/sec'\n }\n ],\n help: 'scene/component/forklift'\n}\n\n// Composition order: Container → Placeable → Legendable → CarrierHolder → Mover.\n// Mover sits OUTSIDE CarrierHolder because Mover's defaults (`pick = this.reparent`,\n// `place = holder.reparent`) rely on CarrierHolder's reparent override being\n// already on the prototype chain. Default impls are instant reparent — Forklift\n// does not yet implement navigation+forkLift motion, so `await forklift.pick(box)`\n// just attaches the box to the forks immediately. Real motion follows in a\n// later commit (path planning, fork actuation animation).\n// FloorBound is the outermost wrap so its rotation guard sees state changes\n// last, after Mover's heading update has run — any ±π corruption (saved-JSON\n// or editor gizmo) is clamped before the next 3D frame applies it.\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 * Forklift — a powered industrial truck used to lift and transport material\n * over short distances.\n *\n * **Container + CarrierHolder for cargo carrying.** Children (boxes, parcels,\n * pallets) attach to the fork-tip frame published by `attachPointFor`, so\n * their 3D pose follows `state.forkHeight` automatically and they read as\n * \"sitting on the forks\". Multiple cargo items stack vertically on the forks.\n *\n * Procedural (non-GLB) build — keeps the hand-tuned silhouette in\n * `forklift-3d.ts`. The transportable layer is purely additive: it does not\n * change the rendering path, only how children's 3D objects are reparented.\n */\n@sceneComponent('forklift')\nexport default class Forklift 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 * Forklift sits on its wheels — `floor` archetype. Default depth is the\n * forklift's overall envelope with mast collapsed (~= operation 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 (in radians) applied by `Mover.moveTo` when feeding\n * direction-of-travel to the Waypoint animation. Forklift uses\n * things-scene's standard vehicle convention (forward = -Z, Three.js\n * camera convention) — same as the framework default. Stated explicitly\n * here so the model's forward axis is documented at the class level\n * rather than being a silent dependency 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\n /**\n * Allow items that flow at operation level (boxes, cartons, parcels, loaded\n * pallets) to be added as cargo. Restricting by archetype rather than type\n * means new package components are auto-permitted as they're added to the\n * scene-base ecosystem.\n *\n * Note: this overrides CarrierHolder's default `containable` (which restricts\n * to `isCarriable === true`). Forklift accepts any operation-archetype\n * component so cargo authored before adopting Carriable still drops in.\n */\n containable(component: Component) {\n const archetype = (component.constructor as any).placement\n if (archetype === 'operation') return true\n // Container's default — accept anything descendible (default things-scene policy).\n return component.isDescendible(this as any)\n }\n\n /**\n * Forklift has a single mount frame: the **forks**. Multiple cargo items\n * stack vertically on the forks (slotIdx → +Y by cargo depth).\n *\n * Returns:\n * - `attach`: the Forklift's RealObjectGroup root (`object3d`). The\n * fork-tip *position* is supplied via `localPosition` rather than a\n * dedicated 3D mount frame, because Forklift3D rebuilds on\n * forkHeight changes and any nested mount node would be re-created\n * on each rebuild (losing scene-graph identity for cargo).\n * - `localPosition`: `Forklift3D.cargoMount.{x,y,z}` (already includes\n * forkHeight) plus a per-slot Y offset for stacking.\n *\n * On forkHeight changes, `onchange` below re-runs this for each child so\n * cargo tracks the lift.\n */\n attachPointFor(carrier: Component): CarrierAttachPoint | undefined {\n const ro = (this as any)._realObject as\n | { object3d?: any; modelGroup?: any; cargoMount?: any }\n | undefined\n if (!ro?.cargoMount) return undefined\n\n // Attach onto modelGroup (rotated to match things-scene -Z forward\n // convention) so the carrier follows the visible mesh's rotation. The\n // cargoMount coordinates are authored in the model's natural +Z-forward\n // frame, which IS the modelGroup's local frame — so localPosition\n // applies directly without an extra rotation.\n const attach = ro.modelGroup ?? ro.object3d\n if (!attach) return undefined\n\n const mount = ro.cargoMount as { x: number; y: number; z: number }\n const slotIdx = this._slotIndexOf(carrier)\n const cargoDepth = (carrier.state as any).depth ?? 100\n\n return {\n attach,\n localPosition: {\n x: mount.x,\n y: mount.y + slotIdx * cargoDepth,\n z: mount.z\n }\n }\n }\n\n /**\n * Stack slot = the carrier's index among its sibling children. Stable\n * because things-scene preserves children order, so re-calling\n * `attachPointFor` after a forkHeight change keeps each cargo at its\n * original stack position. No persisted state needed.\n *\n * If the carrier isn't a child yet (mid-reparent — `attachPointFor` is\n * invoked from CarrierHolder.reparent before/around the add), default\n * to \"next free slot\" = current child count, which becomes its index\n * once the add settles.\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 — top-down silhouette of a forklift. Layout (top-down view):\n *\n * ┌──────────────────────┐ ← canvas top edge (forks point here, -Z forward)\n * │ ▒ forks ▒ │\n * │ ▒ ▒ │\n * │═════ mast bar ═══════│\n * │ ┌─────cab──────┐ │\n * │ │ ▢ seat │ │ ← driver in cab (faces forks/top edge)\n * │ └───────────────┘ │\n * │ ● ● rear-w │\n * ╰──────────────────────╯ ← counterweight bulge at bottom edge\n * ● ●\n * tail lights\n *\n * `forks` extend toward `top` (canvas y minus). Counterweight bulge\n * extends past `top + height` (canvas y plus). Matches things-scene's\n * vehicle convention (rotation=0 → vehicle points to canvas top edge).\n */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n const forkLen = height * 0.28\n const cwBulge = height * 0.08\n const radius = Math.min(width, height) * 0.08\n\n // Body silhouette: from (top + forkLen) past (top + height) by cwBulge.\n // `top..top+forkLen` is reserved for the fork prongs (drawn in postrender).\n ctx.beginPath()\n ctx.roundRect(left, top + forkLen, width, height - forkLen + cwBulge, radius)\n }\n\n postrender(ctx: CanvasRenderingContext2D) {\n super.postrender?.(ctx)\n\n const { width, height, left, top } = this.state\n const forkLen = height * 0.28\n const cwBulge = height * 0.08\n const accentColor = (this.state.lampEmissive as string) || '#44ff44'\n const mastY = top + forkLen\n const bodyBottom = top + height + cwBulge\n\n ctx.save()\n\n // Mast bar — horizontal across the front of the body, where forks attach\n ctx.fillStyle = '#444455'\n ctx.fillRect(left + width * 0.10, mastY - height * 0.022, width * 0.80, height * 0.025)\n\n // Forks — two prongs extending toward canvas top (forward = -Z)\n const forkW = width * 0.10\n ctx.fillStyle = '#222233'\n ctx.strokeStyle = '#111'\n ctx.lineWidth = 1\n for (const xFrac of [0.20, 0.70]) {\n ctx.beginPath()\n ctx.rect(left + width * xFrac, top, forkW, forkLen)\n ctx.fill()\n ctx.stroke()\n }\n\n // Cab outline (centered, just behind the mast)\n const cabL = left + width * 0.18\n const cabT = mastY + height * 0.05\n const cabW = width * 0.64\n const cabH = height * 0.38\n ctx.fillStyle = 'rgba(0,0,0,0.06)'\n ctx.strokeStyle = '#333'\n ctx.lineWidth = 1\n ctx.fillRect(cabL, cabT, cabW, cabH)\n ctx.strokeRect(cabL, cabT, cabW, cabH)\n\n // Driver seat hint (rear half of cab — driver faces forks)\n ctx.fillStyle = '#1a1a22'\n ctx.fillRect(\n left + width * 0.40,\n cabT + cabH * 0.50,\n width * 0.20,\n cabH * 0.35\n )\n\n // Steering wheel hint (front of cab)\n ctx.fillStyle = '#222'\n ctx.beginPath()\n ctx.arc(left + width / 2, cabT + cabH * 0.30, Math.min(width, height) * 0.05, 0, Math.PI * 2)\n ctx.stroke()\n\n // Wheels — 4 corners. Front (drive) wheels behind mast; rear (caster) at bottom.\n ctx.fillStyle = '#1a1a1a'\n const wheelW = width * 0.07\n const wheelH = height * 0.10\n // Front wheels — just below mast, sides of body\n ctx.fillRect(left + width * 0.04, mastY + height * 0.02, wheelW, wheelH)\n ctx.fillRect(left + width - width * 0.04 - wheelW, mastY + height * 0.02, wheelW, wheelH)\n // Rear wheels — near body bottom (excluding cwBulge)\n const rearWheelY = top + height - wheelH * 1.1\n ctx.fillRect(left + width * 0.10, rearWheelY, wheelW * 0.85, wheelH * 0.85)\n ctx.fillRect(left + width - width * 0.10 - wheelW * 0.85, rearWheelY, wheelW * 0.85, wheelH * 0.85)\n\n // Status lamp on overhead guard top (over cab, near front)\n const lampR = Math.min(width, height) * 0.045\n ctx.fillStyle = accentColor\n ctx.strokeStyle = '#111'\n ctx.beginPath()\n ctx.ellipse(left + width / 2, cabT + cabH * 0.15, lampR, lampR, 0, 0, Math.PI * 2)\n ctx.fill()\n ctx.stroke()\n\n // Tail lights — bottom of counterweight bulge\n ctx.fillStyle = '#ff2222'\n const tailLightW = width * 0.08\n const tailLightH = height * 0.022\n const tailY = bodyBottom - tailLightH * 1.5\n ctx.fillRect(left + width * 0.15, tailY, tailLightW, tailLightH)\n ctx.fillRect(left + width - width * 0.15 - tailLightW, tailY, tailLightW, tailLightH)\n\n ctx.restore()\n }\n\n get fillStyle() {\n return (this.state.bodyColor as string) || '#d4a017'\n }\n\n buildRealObject(): RealObject | undefined {\n return new Forklift3D(this as any)\n }\n\n /**\n * forkHeight change → mast & forks rebuild AND cargo Y must follow.\n *\n * Forklift3D.onchange already handles the mast/fork rebuild via update().\n * What's missing is moving each carrier's `object3d` to the new fork-tip\n * position. We do that here at the component level (not in Forklift3D)\n * because the attach-point policy lives on the holder component, and\n * Carriable's `applyHolderAttachPoint` is the established protocol.\n *\n * Carriable children expose `applyHolderAttachPoint` — calling it makes\n * them re-fetch our `attachPointFor` and snap to the new localPosition.\n * For non-Carriable children we fall through to direct object3d update\n * so legacy cargo (operation-archetype components without the Carriable\n * mixin) still tracks the lift.\n */\n onchange(after: any, before: any) {\n super.onchange?.(after, before)\n\n if (!('forkHeight' in (after ?? {}))) return\n\n const children = ((this as any).children as Component[] | undefined) ?? []\n for (const child of children) {\n if ((child as any).applyHolderAttachPoint) {\n ;(child as any).applyHolderAttachPoint()\n continue\n }\n // Non-Carriable cargo: snap object3d directly using our attach policy.\n const point = this.attachPointFor(child)\n const obj3d = (child as any)._realObject?.object3d\n if (point?.localPosition && obj3d?.position) {\n obj3d.position.set(point.localPosition.x, point.localPosition.y, point.localPosition.z)\n }\n }\n }\n\n}\n"]}
1
+ {"version":3,"file":"forklift.js","sourceRoot":"","sources":["../src/forklift.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 { Forklift3D } from './forklift-3d.js'\n\n/**\n * Forklift status — the operating state of a forklift truck.\n *\n * - `idle` — parked, engine off\n * - `running` — driving (no load)\n * - `lifting` — actively lifting / lowering forks\n * - `loaded` — driving with cargo on forks\n * - `error` — fault / emergency stop\n *\n * The 5-state set differs from Conveyor's because forklifts have a distinct\n * loaded-vs-empty distinction relevant for fleet visualization.\n */\nexport type ForkliftStatus = 'idle' | 'running' | 'lifting' | 'loaded' | 'error'\n\n/** Body color — yellow base hue, modulated slightly by status for at-a-glance state. */\nconst BODY_LEGEND = {\n idle: '#d4a017', // muted yellow (parked)\n running: '#FFD700', // bright yellow (active)\n lifting: '#FFD700',\n loaded: '#FFA500', // orange tint (carrying)\n error: '#e9746b', // red\n default: '#d4a017'\n}\n\n/** Status lamp emissive — saturated for at-a-glance status from a distance. */\nconst LAMP_EMISSIVE_LEGEND = {\n idle: '#333333',\n running: '#44ff44', // green (operating)\n lifting: '#44aaff', // blue (lifting)\n loaded: '#ffaa00', // amber (loaded)\n error: '#ff3333', // red\n default: '#333333'\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: 'Running', value: 'running' },\n { display: 'Lifting', value: 'lifting' },\n { display: 'Loaded', value: 'loaded' },\n { display: 'Error', value: 'error' }\n ]\n }\n },\n {\n type: 'number',\n label: 'fork-height',\n name: 'forkHeight',\n placeholder: '0..2000mm'\n },\n {\n // Forklift travel speed in scene units / second. Used by `Mover.moveTo`\n // to derive motion duration as `distance / speed × 1000`. Set to a value\n // that gives the look you want on this board's scale — defaults are\n // intentionally unguessed (board scale varies).\n type: 'number',\n label: 'speed',\n name: 'speed',\n placeholder: 'units/sec'\n }\n ],\n help: 'scene/component/forklift'\n}\n\n// Composition order:\n// FloorBound → Mover → CarrierHolder → ContainerCapacity → Legendable → Placeable → ContainerAbstract\n//\n// ContainerCapacity: receive() / dispatch() / canReceive() / slots — Mover.pick uses\n// receive() (slot-tracking path) instead of the reparent() fallback. Event emission\n// ('transfer-received', 'transfer-dispatched') fires on every cargo transfer.\n// CarrierHolder: attachPointFor() — fork-tip 3D mount frame; Carriable.added()\n// calls applyHolderAttachPoint() which uses our override for 3D positioning.\n// Mover: moveTo / pick / place / pickAndPlace / executeMission.\n// FloorBound: rotation guard — outermost, sees state changes last.\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 * Forklift — a powered industrial truck used to lift and transport material\n * over short distances.\n *\n * **Container + CarrierHolder for cargo carrying.** Children (boxes, parcels,\n * pallets) attach to the fork-tip frame published by `attachPointFor`, so\n * their 3D pose follows `state.forkHeight` automatically and they read as\n * \"sitting on the forks\". Multiple cargo items stack vertically on the forks.\n *\n * Procedural (non-GLB) build — keeps the hand-tuned silhouette in\n * `forklift-3d.ts`. The transportable layer is purely additive: it does not\n * change the rendering path, only how children's 3D objects are reparented.\n */\n@sceneComponent('forklift')\nexport default class Forklift 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 * Forklift sits on its wheels — `floor` archetype. Default depth is the\n * forklift's overall envelope with mast collapsed (~= operation 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 (in radians) applied by `Mover.moveTo` when feeding\n * direction-of-travel to the Waypoint animation. Forklift uses\n * things-scene's standard vehicle convention (forward = -Z, Three.js\n * camera convention) — same as the framework default. Stated explicitly\n * here so the model's forward axis is documented at the class level\n * rather than being a silent dependency 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 * Forks hold up to 3 stacked load units (pallet + boxes).\n * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx) —\n * the slot maxCount here is the hard cap for `canReceive()`.\n */\n get slots(): SlotDef[] {\n return [{ id: 'forks', maxCount: 3 }]\n }\n\n /**\n * Allow items that flow at operation level (boxes, cartons, parcels, loaded\n * pallets) to be added as cargo. Restricting by archetype rather than type\n * means new package components are auto-permitted as they're added to the\n * scene-base ecosystem.\n *\n * Note: this overrides CarrierHolder's default `containable` (which restricts\n * to `isCarriable === true`). Forklift accepts any operation-archetype\n * component so cargo authored before adopting Carriable still drops in.\n */\n containable(component: Component) {\n const archetype = (component.constructor as any).placement\n if (archetype === 'operation') return true\n // Container's default — accept anything descendible (default things-scene policy).\n return component.isDescendible(this as any)\n }\n\n /**\n * Forklift has a single mount frame: the **forks**. Multiple cargo items\n * stack vertically on the forks (slotIdx → +Y by cargo depth).\n *\n * Returns:\n * - `attach`: the Forklift's RealObjectGroup root (`object3d`). The\n * fork-tip *position* is supplied via `localPosition` rather than a\n * dedicated 3D mount frame, because Forklift3D rebuilds on\n * forkHeight changes and any nested mount node would be re-created\n * on each rebuild (losing scene-graph identity for cargo).\n * - `localPosition`: `Forklift3D.cargoMount.{x,y,z}` (already includes\n * forkHeight) plus a per-slot Y offset for stacking.\n *\n * On forkHeight changes, `onchange` below re-runs this for each child so\n * cargo tracks the lift.\n */\n attachPointFor(carrier: Component): CarrierAttachPoint | undefined {\n const ro = (this as any)._realObject as\n | { object3d?: any; modelGroup?: any; cargoMount?: any }\n | undefined\n if (!ro?.cargoMount) return undefined\n\n // Attach onto modelGroup (rotated to match things-scene -Z forward\n // convention) so the carrier follows the visible mesh's rotation. The\n // cargoMount coordinates are authored in the model's natural +Z-forward\n // frame, which IS the modelGroup's local frame — so localPosition\n // applies directly without an extra rotation.\n const attach = ro.modelGroup ?? ro.object3d\n if (!attach) return undefined\n\n const mount = ro.cargoMount as { x: number; y: number; z: number }\n const slotIdx = this._slotIndexOf(carrier)\n const cargoDepth = (carrier.state as any).depth ?? 100\n\n return {\n attach,\n localPosition: {\n x: mount.x,\n y: mount.y + slotIdx * cargoDepth,\n z: mount.z\n }\n }\n }\n\n /**\n * Stack slot = the carrier's index among its sibling children. Stable\n * because things-scene preserves children order, so re-calling\n * `attachPointFor` after a forkHeight change keeps each cargo at its\n * original stack position. No persisted state needed.\n *\n * If the carrier isn't a child yet (mid-reparent — `attachPointFor` is\n * invoked from CarrierHolder.reparent before/around the add), default\n * to \"next free slot\" = current child count, which becomes its index\n * once the add settles.\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 — top-down silhouette of a forklift. Layout (top-down view):\n *\n * ┌──────────────────────┐ ← canvas top edge (forks point here, -Z forward)\n * │ ▒ forks ▒ │\n * │ ▒ ▒ │\n * │═════ mast bar ═══════│\n * │ ┌─────cab──────┐ │\n * │ │ ▢ seat │ │ ← driver in cab (faces forks/top edge)\n * │ └───────────────┘ │\n * │ ● ● rear-w │\n * ╰──────────────────────╯ ← counterweight bulge at bottom edge\n * ● ●\n * tail lights\n *\n * `forks` extend toward `top` (canvas y minus). Counterweight bulge\n * extends past `top + height` (canvas y plus). Matches things-scene's\n * vehicle convention (rotation=0 → vehicle points to canvas top edge).\n */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n const forkLen = height * 0.28\n const cwBulge = height * 0.08\n const radius = Math.min(width, height) * 0.08\n\n // Body silhouette: from (top + forkLen) past (top + height) by cwBulge.\n // `top..top+forkLen` is reserved for the fork prongs (drawn in postrender).\n ctx.beginPath()\n ctx.roundRect(left, top + forkLen, width, height - forkLen + cwBulge, radius)\n }\n\n postrender(ctx: CanvasRenderingContext2D) {\n super.postrender?.(ctx)\n\n const { width, height, left, top } = this.state\n const forkLen = height * 0.28\n const cwBulge = height * 0.08\n const accentColor = (this.state.lampEmissive as string) || '#44ff44'\n const mastY = top + forkLen\n const bodyBottom = top + height + cwBulge\n\n ctx.save()\n\n // Mast bar — horizontal across the front of the body, where forks attach\n ctx.fillStyle = '#444455'\n ctx.fillRect(left + width * 0.10, mastY - height * 0.022, width * 0.80, height * 0.025)\n\n // Forks — two prongs extending toward canvas top (forward = -Z)\n const forkW = width * 0.10\n ctx.fillStyle = '#222233'\n ctx.strokeStyle = '#111'\n ctx.lineWidth = 1\n for (const xFrac of [0.20, 0.70]) {\n ctx.beginPath()\n ctx.rect(left + width * xFrac, top, forkW, forkLen)\n ctx.fill()\n ctx.stroke()\n }\n\n // Cab outline (centered, just behind the mast)\n const cabL = left + width * 0.18\n const cabT = mastY + height * 0.05\n const cabW = width * 0.64\n const cabH = height * 0.38\n ctx.fillStyle = 'rgba(0,0,0,0.06)'\n ctx.strokeStyle = '#333'\n ctx.lineWidth = 1\n ctx.fillRect(cabL, cabT, cabW, cabH)\n ctx.strokeRect(cabL, cabT, cabW, cabH)\n\n // Driver seat hint (rear half of cab — driver faces forks)\n ctx.fillStyle = '#1a1a22'\n ctx.fillRect(\n left + width * 0.40,\n cabT + cabH * 0.50,\n width * 0.20,\n cabH * 0.35\n )\n\n // Steering wheel hint (front of cab)\n ctx.fillStyle = '#222'\n ctx.beginPath()\n ctx.arc(left + width / 2, cabT + cabH * 0.30, Math.min(width, height) * 0.05, 0, Math.PI * 2)\n ctx.stroke()\n\n // Wheels — 4 corners. Front (drive) wheels behind mast; rear (caster) at bottom.\n ctx.fillStyle = '#1a1a1a'\n const wheelW = width * 0.07\n const wheelH = height * 0.10\n // Front wheels — just below mast, sides of body\n ctx.fillRect(left + width * 0.04, mastY + height * 0.02, wheelW, wheelH)\n ctx.fillRect(left + width - width * 0.04 - wheelW, mastY + height * 0.02, wheelW, wheelH)\n // Rear wheels — near body bottom (excluding cwBulge)\n const rearWheelY = top + height - wheelH * 1.1\n ctx.fillRect(left + width * 0.10, rearWheelY, wheelW * 0.85, wheelH * 0.85)\n ctx.fillRect(left + width - width * 0.10 - wheelW * 0.85, rearWheelY, wheelW * 0.85, wheelH * 0.85)\n\n // Status lamp on overhead guard top (over cab, near front)\n const lampR = Math.min(width, height) * 0.045\n ctx.fillStyle = accentColor\n ctx.strokeStyle = '#111'\n ctx.beginPath()\n ctx.ellipse(left + width / 2, cabT + cabH * 0.15, lampR, lampR, 0, 0, Math.PI * 2)\n ctx.fill()\n ctx.stroke()\n\n // Tail lights — bottom of counterweight bulge\n ctx.fillStyle = '#ff2222'\n const tailLightW = width * 0.08\n const tailLightH = height * 0.022\n const tailY = bodyBottom - tailLightH * 1.5\n ctx.fillRect(left + width * 0.15, tailY, tailLightW, tailLightH)\n ctx.fillRect(left + width - width * 0.15 - tailLightW, tailY, tailLightW, tailLightH)\n\n ctx.restore()\n }\n\n get fillStyle() {\n return (this.state.bodyColor as string) || '#d4a017'\n }\n\n buildRealObject(): RealObject | undefined {\n return new Forklift3D(this as any)\n }\n\n /**\n * forkHeight change → mast & forks rebuild AND cargo Y must follow.\n *\n * Forklift3D.onchange already handles the mast/fork rebuild via update().\n * What's missing is moving each carrier's `object3d` to the new fork-tip\n * position. We do that here at the component level (not in Forklift3D)\n * because the attach-point policy lives on the holder component, and\n * Carriable's `applyHolderAttachPoint` is the established protocol.\n *\n * Carriable children expose `applyHolderAttachPoint` — calling it makes\n * them re-fetch our `attachPointFor` and snap to the new localPosition.\n * For non-Carriable children we fall through to direct object3d update\n * so legacy cargo (operation-archetype components without the Carriable\n * mixin) still tracks the lift.\n */\n onchange(after: any, before: any) {\n super.onchange?.(after, before)\n\n if (!('forkHeight' in (after ?? {}))) return\n\n const children = ((this as any).children as Component[] | undefined) ?? []\n for (const child of children) {\n if ((child as any).applyHolderAttachPoint) {\n ;(child as any).applyHolderAttachPoint()\n continue\n }\n // Non-Carriable cargo: snap object3d directly using our attach policy.\n const point = this.attachPointFor(child)\n const obj3d = (child as any)._realObject?.object3d\n if (point?.localPosition && obj3d?.position) {\n obj3d.position.set(point.localPosition.x, point.localPosition.y, point.localPosition.z)\n }\n }\n }\n\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { Component, ComponentNature } from '@hatiolab/things-scene';
2
+ import type { SlotDef } 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
  import { GenericTransport3D } from './generic-transport-3d.js';
4
5
  import type { TransportMount, TransportMaterialOverride } from './generic-transport-types.js';
@@ -23,6 +24,11 @@ export default class GenericTransport extends Base {
23
24
  get anchors(): never[];
24
25
  /** state 의 mounts 배열 (없으면 빈 배열). */
25
26
  get mounts(): TransportMount[];
27
+ /**
28
+ * Derive SlotDef[] from state.mounts — each mount becomes one slot.
29
+ * Fallback: single 'cargo' slot (capacity 1) when no mounts are configured.
30
+ */
31
+ get slots(): SlotDef[];
26
32
  get actuators(): Record<string, ActuatorDef>;
27
33
  get actuatorValues(): Record<string, number>;
28
34
  get materialOverrides(): TransportMaterialOverride[];
@@ -20,7 +20,7 @@
20
20
  * 각 차량별로 다른 GLB + 다른 mount/actuator 매핑만 다름.
21
21
  */
22
22
  import { __decorate } from "tslib";
23
- import { ContainerAbstract, sceneComponent, GltfComponent, gltfNatureProperties } from '@hatiolab/things-scene';
23
+ import { ContainerAbstract, ContainerCapacity, sceneComponent, GltfComponent, gltfNatureProperties } from '@hatiolab/things-scene';
24
24
  import { Legendable, Placeable, CarrierHolder, Mover } from '@operato/scene-base';
25
25
  import { GenericTransport3D } from './generic-transport-3d.js';
26
26
  const BODY_LEGEND = {
@@ -64,17 +64,12 @@ const NATURE = {
64
64
  ],
65
65
  help: 'scene/component/generic-transport'
66
66
  };
67
- // 합성 순서: 안쪽부터 → ContainerAbstract → Placeable → Legendable → CarrierHolder → Mover → GltfComponent
68
- // GltfComponent 가장 바깥 (render/onchange 같은 핵심 메서드를 override 하므로
69
- // CarrierHolder 등의 영향을 마지막에 받지 않도록).
70
- // Mover CarrierHolder 와 GltfComponent 사이 — pick/place 디폴트가 this.reparent
71
- // 의존하므로 CarrierHolder.reparent prototype 체인에 먼저 있어야 함. GltfComponent
72
- // lifecycle 메서드만 override, pick/place 는 안 건드리므로 Mover 를 감싸도 무해.
73
- //
74
- // ContainerAbstract (not Container) — Container = MixinHTMLElement(ContainerAbstract),
75
- // which forces `isHTMLElement(): true` and trips the 3D addObject DOM-skip
76
- // gate. Generic transport lives only in the 3D scene graph (GLB-based 3D mesh).
77
- const Base = GltfComponent(Mover(CarrierHolder(Legendable(Placeable(ContainerAbstract)))));
67
+ // 합성 순서: 안쪽부터 → ContainerAbstract → Placeable → Legendable → ContainerCapacity → CarrierHolder → Mover → GltfComponent
68
+ // ContainerCapacity: state.mounts slots 파생; receive() / dispatch() / canReceive()
69
+ // CarrierHolder: attachPointFor() GLB 노드 기반 3D TCP mount
70
+ // Mover: pick / place / pickAndPlace (ContainerCapacity.receive() 경로 사용)
71
+ // GltfComponent: 가장 바깥 lifecycle + render override
72
+ const Base = GltfComponent(Mover(CarrierHolder(ContainerCapacity(Legendable(Placeable(ContainerAbstract))))));
78
73
  let GenericTransport = class GenericTransport extends Base {
79
74
  static legends = {
80
75
  bodyColor: { from: 'status', legend: BODY_LEGEND },
@@ -93,6 +88,22 @@ let GenericTransport = class GenericTransport extends Base {
93
88
  get mounts() {
94
89
  return this.state.mounts ?? [];
95
90
  }
91
+ // ── ContainerCapacity ─────────────────────────────────────────────────────
92
+ /**
93
+ * Derive SlotDef[] from state.mounts — each mount becomes one slot.
94
+ * Fallback: single 'cargo' slot (capacity 1) when no mounts are configured.
95
+ */
96
+ get slots() {
97
+ const mounts = this.mounts;
98
+ if (mounts.length === 0)
99
+ return [{ id: 'cargo', maxCount: 1 }];
100
+ return mounts.map((m, i) => ({
101
+ id: m.node || `mount-${i}`,
102
+ maxCount: m.capacity ?? 1,
103
+ localPosition: m.offset,
104
+ allowedTypes: m.acceptTypes
105
+ }));
106
+ }
96
107
  get actuators() {
97
108
  return this.state.actuators ?? {};
98
109
  }
@@ -1 +1 @@
1
- {"version":3,"file":"generic-transport.js","sourceRoot":"","sources":["../src/generic-transport.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * GenericTransport — GLB 모델 기반 범용 운송 차량 컴포넌트.\n *\n * 합성 구조:\n * GltfComponent (things-scene) — GLB 컨벤션 (src 상태, 2D 스냅샷,\n * gltf-* 에디터, ratioLock, RealObjectGLTF)\n * ⊕ ContainerAbstract (things-scene) — cargo 자식 보유 (DOM-free 컨테이너)\n * ⊕ Placeable (scene-base) — floor placement, depth=operation\n * ⊕ Legendable (scene-base) — status 별 bodyColor/lampEmissive\n * ⊕ CarrierHolder(scene-base) — cargo 부착 hook\n *\n * 추가 (이 클래스 고유):\n * - state.mounts: cargo 부착 지점 배열 (GLB 노드 + arrangement)\n * - state.actuators / actuatorValues: 동적 노드 transform (forkHeight 등)\n * - resolveMountSlot / attachPointFor: cargo 자동 슬롯 배정 + GLB 노드 reparent\n *\n * 동일 컴포넌트 하나로 forklift / AGV / tugger / crane 모두 표현 가능 —\n * 각 차량별로 다른 GLB + 다른 mount/actuator 매핑만 다름.\n */\n\nimport { Component, ComponentNature, ContainerAbstract, sceneComponent, GltfComponent, gltfNatureProperties } from '@hatiolab/things-scene'\nimport {\n Legendable,\n Placeable,\n CarrierHolder,\n Mover,\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 { GenericTransport3D } from './generic-transport-3d.js'\nimport type { TransportMount, TransportMaterialOverride } from './generic-transport-types.js'\nimport type { ActuatorDef } from '@operato/scene-base'\n\nexport type TransportStatus = 'idle' | 'running' | 'lifting' | 'loaded' | 'error'\n\nconst BODY_LEGEND = {\n idle: '#d4a017',\n running: '#FFD700',\n lifting: '#FFD700',\n loaded: '#FFA500',\n error: '#e9746b',\n default: '#d4a017'\n}\n\nconst LAMP_EMISSIVE_LEGEND = {\n idle: '#333333',\n running: '#44ff44',\n lifting: '#44aaff',\n loaded: '#ffaa00',\n error: '#ff3333',\n default: '#333333'\n}\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n // 공통 GLB 속성 (gltf-selector / info / fill-targets / play-targets / preset)\n ...gltfNatureProperties,\n // transport 고유: status (Legendable 가 bodyColor/lampEmissive 자동 derivation)\n {\n type: 'select',\n label: 'status',\n name: 'status',\n property: {\n options: [\n { display: 'Idle', value: 'idle' },\n { display: 'Running', value: 'running' },\n { display: 'Lifting', value: 'lifting' },\n { display: 'Loaded', value: 'loaded' },\n { display: 'Error', value: 'error' }\n ]\n }\n }\n ],\n help: 'scene/component/generic-transport'\n}\n\n// 합성 순서: 안쪽부터 → ContainerAbstract → Placeable → Legendable → CarrierHolder → Mover → GltfComponent\n// GltfComponent 는 가장 바깥 (render/onchange 같은 핵심 메서드를 override 하므로\n// CarrierHolder 등의 영향을 마지막에 받지 않도록).\n// Mover 는 CarrierHolder 와 GltfComponent 사이 — pick/place 디폴트가 this.reparent 에\n// 의존하므로 CarrierHolder.reparent 가 prototype 체인에 먼저 있어야 함. GltfComponent\n// 는 lifecycle 메서드만 override, pick/place 는 안 건드리므로 Mover 를 감싸도 무해.\n//\n// ContainerAbstract (not Container) — Container = MixinHTMLElement(ContainerAbstract),\n// which forces `isHTMLElement(): true` and trips the 3D addObject DOM-skip\n// gate. Generic transport lives only in the 3D scene graph (GLB-based 3D mesh).\nconst Base = GltfComponent(\n Mover(CarrierHolder(Legendable(Placeable(ContainerAbstract))))\n) 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@sceneComponent('transport')\nexport default class GenericTransport 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 static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (h: Heights) => h.operation - h.floor\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /** state 의 mounts 배열 (없으면 빈 배열). */\n get mounts(): TransportMount[] {\n return ((this.state as any).mounts as TransportMount[] | undefined) ?? []\n }\n\n get actuators(): Record<string, ActuatorDef> {\n return ((this.state as any).actuators as Record<string, ActuatorDef> | undefined) ?? {}\n }\n\n get actuatorValues(): Record<string, number> {\n return ((this.state as any).actuatorValues as Record<string, number> | undefined) ?? {}\n }\n\n get materialOverrides(): TransportMaterialOverride[] {\n return ((this.state as any).materials as TransportMaterialOverride[] | undefined) ?? []\n }\n\n /** 'operation' archetype (cargo) 만 받아들임. */\n containable(component: Component): boolean {\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 * cargo 가 들어갈 mount 와 그 안에서의 slot 을 자동 배정.\n * acceptTypes 일치 + capacity 여유 있는 첫 mount.\n */\n resolveMountSlot(carrier: Component): { mountIdx: number; slotIdx: number } | undefined {\n const cargoType = (carrier as any).type as string\n for (let mi = 0; mi < this.mounts.length; mi++) {\n const m = this.mounts[mi]\n if (m.acceptTypes && !m.acceptTypes.includes(cargoType)) continue\n const cap = m.capacity ?? 1\n const used = this._countCargoInMount(mi, carrier)\n if (used < cap) return { mountIdx: mi, slotIdx: used }\n }\n return undefined\n }\n\n private _countCargoInMount(mountIdx: number, excluding?: Component): number {\n const cs = (this as any).children as Component[] | undefined\n if (!cs) return 0\n let n = 0\n for (const c of cs) {\n if (c === excluding) continue\n const idx = (c.state as any)._mountIdx\n if (idx === mountIdx) n++\n }\n return n\n }\n\n /**\n * CarrierHolder mixin override — cargo 가 attach 될 GLB 노드 + 로컬\n * 위치 반환. 3D 레이어 (GenericTransport3D) 가 노드 노출.\n */\n attachPointFor(carrier: Component): CarrierAttachPoint | undefined {\n const ro = (this as any)._realObject as GenericTransport3D | undefined\n if (!ro) return undefined\n\n const slot = this.resolveMountSlot(carrier)\n if (!slot) return undefined\n\n const mount = this.mounts[slot.mountIdx]\n const mountNode = ro.getMountNode(mount.node)\n if (!mountNode) return undefined\n\n const cargoDepth = (carrier.state as any).depth ?? 100\n const off = mount.offset ?? { x: 0, y: 0, z: 0 }\n const localPosition = this._computeLocalPosition(mount, slot.slotIdx, cargoDepth, off)\n\n ;(carrier.state as any)._mountIdx = slot.mountIdx\n ;(carrier.state as any)._slotIdx = slot.slotIdx\n\n return { attach: mountNode, localPosition }\n }\n\n private _computeLocalPosition(\n mount: TransportMount,\n slotIdx: number,\n cargoDepth: number,\n base: { x: number; y: number; z: number }\n ): { x: number; y: number; z: number } {\n switch (mount.arrangement ?? 'single') {\n case 'stack':\n return { x: base.x, y: base.y + slotIdx * cargoDepth, z: base.z }\n case 'queue-z':\n return { x: base.x, y: base.y, z: base.z + slotIdx * cargoDepth }\n case 'grid-xz': {\n const cols = mount.gridShape?.cols ?? 1\n const sx = mount.gridShape?.spacingX ?? cargoDepth\n const sz = mount.gridShape?.spacingZ ?? cargoDepth\n const ix = slotIdx % cols\n const iz = Math.floor(slotIdx / cols)\n return { x: base.x + ix * sx, y: base.y, z: base.z + iz * sz }\n }\n case 'single':\n default:\n return base\n }\n }\n\n /** 3D 레이어를 GenericTransport3D 로 (GltfComponent 의 RealObjectGLTF 디폴트 override). */\n buildRealObject() {\n return new GenericTransport3D(this as any)\n }\n}\n"]}
1
+ {"version":3,"file":"generic-transport.js","sourceRoot":"","sources":["../src/generic-transport.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * GenericTransport — GLB 모델 기반 범용 운송 차량 컴포넌트.\n *\n * 합성 구조:\n * GltfComponent (things-scene) — GLB 컨벤션 (src 상태, 2D 스냅샷,\n * gltf-* 에디터, ratioLock, RealObjectGLTF)\n * ⊕ ContainerAbstract (things-scene) — cargo 자식 보유 (DOM-free 컨테이너)\n * ⊕ Placeable (scene-base) — floor placement, depth=operation\n * ⊕ Legendable (scene-base) — status 별 bodyColor/lampEmissive\n * ⊕ CarrierHolder(scene-base) — cargo 부착 hook\n *\n * 추가 (이 클래스 고유):\n * - state.mounts: cargo 부착 지점 배열 (GLB 노드 + arrangement)\n * - state.actuators / actuatorValues: 동적 노드 transform (forkHeight 등)\n * - resolveMountSlot / attachPointFor: cargo 자동 슬롯 배정 + GLB 노드 reparent\n *\n * 동일 컴포넌트 하나로 forklift / AGV / tugger / crane 모두 표현 가능 —\n * 각 차량별로 다른 GLB + 다른 mount/actuator 매핑만 다름.\n */\n\nimport { Component, ComponentNature, ContainerAbstract, ContainerCapacity, sceneComponent, GltfComponent, gltfNatureProperties } from '@hatiolab/things-scene'\nimport type { SlotDef } from '@hatiolab/things-scene'\nimport {\n Legendable,\n Placeable,\n CarrierHolder,\n Mover,\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 { GenericTransport3D } from './generic-transport-3d.js'\nimport type { TransportMount, TransportMaterialOverride } from './generic-transport-types.js'\nimport type { ActuatorDef } from '@operato/scene-base'\n\nexport type TransportStatus = 'idle' | 'running' | 'lifting' | 'loaded' | 'error'\n\nconst BODY_LEGEND = {\n idle: '#d4a017',\n running: '#FFD700',\n lifting: '#FFD700',\n loaded: '#FFA500',\n error: '#e9746b',\n default: '#d4a017'\n}\n\nconst LAMP_EMISSIVE_LEGEND = {\n idle: '#333333',\n running: '#44ff44',\n lifting: '#44aaff',\n loaded: '#ffaa00',\n error: '#ff3333',\n default: '#333333'\n}\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n // 공통 GLB 속성 (gltf-selector / info / fill-targets / play-targets / preset)\n ...gltfNatureProperties,\n // transport 고유: status (Legendable 가 bodyColor/lampEmissive 자동 derivation)\n {\n type: 'select',\n label: 'status',\n name: 'status',\n property: {\n options: [\n { display: 'Idle', value: 'idle' },\n { display: 'Running', value: 'running' },\n { display: 'Lifting', value: 'lifting' },\n { display: 'Loaded', value: 'loaded' },\n { display: 'Error', value: 'error' }\n ]\n }\n }\n ],\n help: 'scene/component/generic-transport'\n}\n\n// 합성 순서: 안쪽부터 → ContainerAbstract → Placeable → Legendable → ContainerCapacity → CarrierHolder → Mover → GltfComponent\n// ContainerCapacity: state.mounts → slots 파생; receive() / dispatch() / canReceive()\n// CarrierHolder: attachPointFor() — GLB 노드 기반 3D TCP mount\n// Mover: pick / place / pickAndPlace (ContainerCapacity.receive() 경로 사용)\n// GltfComponent: 가장 바깥 — lifecycle + render override\nconst Base = GltfComponent(\n Mover(CarrierHolder(ContainerCapacity(Legendable(Placeable(ContainerAbstract)))))\n) 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@sceneComponent('transport')\nexport default class GenericTransport 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 static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (h: Heights) => h.operation - h.floor\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /** state 의 mounts 배열 (없으면 빈 배열). */\n get mounts(): TransportMount[] {\n return ((this.state as any).mounts as TransportMount[] | undefined) ?? []\n }\n\n // ── ContainerCapacity ─────────────────────────────────────────────────────\n\n /**\n * Derive SlotDef[] from state.mounts — each mount becomes one slot.\n * Fallback: single 'cargo' slot (capacity 1) when no mounts are configured.\n */\n get slots(): SlotDef[] {\n const mounts = this.mounts\n if (mounts.length === 0) return [{ id: 'cargo', maxCount: 1 }]\n return mounts.map((m, i) => ({\n id: m.node || `mount-${i}`,\n maxCount: m.capacity ?? 1,\n localPosition: m.offset,\n allowedTypes: m.acceptTypes\n }))\n }\n\n get actuators(): Record<string, ActuatorDef> {\n return ((this.state as any).actuators as Record<string, ActuatorDef> | undefined) ?? {}\n }\n\n get actuatorValues(): Record<string, number> {\n return ((this.state as any).actuatorValues as Record<string, number> | undefined) ?? {}\n }\n\n get materialOverrides(): TransportMaterialOverride[] {\n return ((this.state as any).materials as TransportMaterialOverride[] | undefined) ?? []\n }\n\n /** 'operation' archetype (cargo) 만 받아들임. */\n containable(component: Component): boolean {\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 * cargo 가 들어갈 mount 와 그 안에서의 slot 을 자동 배정.\n * acceptTypes 일치 + capacity 여유 있는 첫 mount.\n */\n resolveMountSlot(carrier: Component): { mountIdx: number; slotIdx: number } | undefined {\n const cargoType = (carrier as any).type as string\n for (let mi = 0; mi < this.mounts.length; mi++) {\n const m = this.mounts[mi]\n if (m.acceptTypes && !m.acceptTypes.includes(cargoType)) continue\n const cap = m.capacity ?? 1\n const used = this._countCargoInMount(mi, carrier)\n if (used < cap) return { mountIdx: mi, slotIdx: used }\n }\n return undefined\n }\n\n private _countCargoInMount(mountIdx: number, excluding?: Component): number {\n const cs = (this as any).children as Component[] | undefined\n if (!cs) return 0\n let n = 0\n for (const c of cs) {\n if (c === excluding) continue\n const idx = (c.state as any)._mountIdx\n if (idx === mountIdx) n++\n }\n return n\n }\n\n /**\n * CarrierHolder mixin override — cargo 가 attach 될 GLB 노드 + 로컬\n * 위치 반환. 3D 레이어 (GenericTransport3D) 가 노드 노출.\n */\n attachPointFor(carrier: Component): CarrierAttachPoint | undefined {\n const ro = (this as any)._realObject as GenericTransport3D | undefined\n if (!ro) return undefined\n\n const slot = this.resolveMountSlot(carrier)\n if (!slot) return undefined\n\n const mount = this.mounts[slot.mountIdx]\n const mountNode = ro.getMountNode(mount.node)\n if (!mountNode) return undefined\n\n const cargoDepth = (carrier.state as any).depth ?? 100\n const off = mount.offset ?? { x: 0, y: 0, z: 0 }\n const localPosition = this._computeLocalPosition(mount, slot.slotIdx, cargoDepth, off)\n\n ;(carrier.state as any)._mountIdx = slot.mountIdx\n ;(carrier.state as any)._slotIdx = slot.slotIdx\n\n return { attach: mountNode, localPosition }\n }\n\n private _computeLocalPosition(\n mount: TransportMount,\n slotIdx: number,\n cargoDepth: number,\n base: { x: number; y: number; z: number }\n ): { x: number; y: number; z: number } {\n switch (mount.arrangement ?? 'single') {\n case 'stack':\n return { x: base.x, y: base.y + slotIdx * cargoDepth, z: base.z }\n case 'queue-z':\n return { x: base.x, y: base.y, z: base.z + slotIdx * cargoDepth }\n case 'grid-xz': {\n const cols = mount.gridShape?.cols ?? 1\n const sx = mount.gridShape?.spacingX ?? cargoDepth\n const sz = mount.gridShape?.spacingZ ?? cargoDepth\n const ix = slotIdx % cols\n const iz = Math.floor(slotIdx / cols)\n return { x: base.x + ix * sx, y: base.y, z: base.z + iz * sz }\n }\n case 'single':\n default:\n return base\n }\n }\n\n /** 3D 레이어를 GenericTransport3D 로 (GltfComponent 의 RealObjectGLTF 디폴트 override). */\n buildRealObject() {\n return new GenericTransport3D(this as any)\n }\n}\n"]}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@operato/scene-transport",
3
3
  "description": "Transport-domain components for things-scene (smart factory / logistics) — forklift, worker, AGV.",
4
4
  "author": "heartyoh",
5
- "version": "10.0.0-beta.28",
5
+ "version": "10.0.0-beta.30",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "module": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@hatiolab/things-scene": "^10.0.0-beta.1",
29
- "@operato/scene-base": "^10.0.0-beta.24",
29
+ "@operato/scene-base": "^10.0.0-beta.30",
30
30
  "three": "^0.183.0"
31
31
  },
32
32
  "devDependencies": {
@@ -45,5 +45,5 @@
45
45
  "typescript": "^5.0.4"
46
46
  },
47
47
  "prettier": "@hatiolab/prettier-config",
48
- "gitHead": "40634f9afc681d852d6028a329cedb51cc4fb94f"
48
+ "gitHead": "06b35b1726ec4f27ee76657ce341c6c6f3ba1b3a"
49
49
  }
package/src/agv.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /*
2
2
  * Copyright © HatioLab Inc. All rights reserved.
3
3
  */
4
- import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
4
+ import { Component, ComponentNature, ContainerAbstract, ContainerCapacity, RealObject, sceneComponent } from '@hatiolab/things-scene'
5
+ import type { SlotDef } from '@hatiolab/things-scene'
5
6
  import {
6
7
  CarrierHolder,
7
8
  FloorBound,
@@ -95,16 +96,19 @@ const NATURE: ComponentNature = {
95
96
  help: 'scene/component/agv'
96
97
  }
97
98
 
98
- // Composition: ContainerAbstract → Placeable → Legendable → CarrierHolder → Mover → FloorBound.
99
- // Same chain as Forklift the AGV is a wheeled, payload-carrying vehicle
100
- // with the same pick/place semantics; differences (deck-stack instead of
101
- // fork-mount) are encoded in `attachPointFor`. FloorBound is outermost so
102
- // its rotation guard sees state changes last.
99
+ // Composition:
100
+ // FloorBound Mover CarrierHolder ContainerCapacity Legendable Placeable ContainerAbstract
103
101
  //
104
- // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
105
- // which forces `isHTMLElement(): true` and trips the 3D pipeline's
106
- // addObject DOM-skip gate. AGV lives only in the 3D scene graph; no DOM.
107
- const Base = FloorBound(Mover(CarrierHolder(Legendable(Placeable(ContainerAbstract))))) as unknown as typeof Component & {
102
+ // ContainerCapacity: receive() / dispatch() / canReceive() / slots Mover.pick uses
103
+ // receive() for slot-tracking and event emission on every cargo transfer.
104
+ // CarrierHolder: attachPointFor() deck-top 3D mount frame; Carriable.added()
105
+ // calls applyHolderAttachPoint() which uses our override for 3D positioning.
106
+ // Mover: moveTo / pick / place / pickAndPlace / executeMission.
107
+ // FloorBound: outermost rotation guard.
108
+ //
109
+ // `ContainerAbstract` (not `Container`) — avoids isHTMLElement()=true that would
110
+ // trip the 3D pipeline's addObject DOM-skip gate.
111
+ const Base = FloorBound(Mover(CarrierHolder(ContainerCapacity(Legendable(Placeable(ContainerAbstract)))))) as unknown as typeof Component & {
108
112
  new (...args: any[]): Component & {
109
113
  isCarrierHolder: boolean
110
114
  isMover: boolean
@@ -162,6 +166,16 @@ export default class Agv extends Base {
162
166
  return []
163
167
  }
164
168
 
169
+ // ── ContainerCapacity ─────────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Flat deck holds up to 2 stacked load units.
173
+ * 3D stacking is handled by `attachPointFor()` (Y offset per slotIdx).
174
+ */
175
+ get slots(): SlotDef[] {
176
+ return [{ id: 'deck', maxCount: 2 }]
177
+ }
178
+
165
179
  /** Accept logistics packages (placement='operation') as deck cargo. */
166
180
  containable(component: Component) {
167
181
  const archetype = (component.constructor as any).placement