@operato/scene-transport 10.0.0-beta.22

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +55 -0
  3. package/dist/agv-3d.d.ts +7 -0
  4. package/dist/agv-3d.js +233 -0
  5. package/dist/agv-3d.js.map +1 -0
  6. package/dist/agv.d.ts +57 -0
  7. package/dist/agv.js +171 -0
  8. package/dist/agv.js.map +1 -0
  9. package/dist/forklift-3d.d.ts +15 -0
  10. package/dist/forklift-3d.js +518 -0
  11. package/dist/forklift-3d.js.map +1 -0
  12. package/dist/forklift.d.ts +58 -0
  13. package/dist/forklift.js +163 -0
  14. package/dist/forklift.js.map +1 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.js +8 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/templates/index.d.ts +47 -0
  19. package/dist/templates/index.js +73 -0
  20. package/dist/templates/index.js.map +1 -0
  21. package/dist/tugger-3d.d.ts +7 -0
  22. package/dist/tugger-3d.js +140 -0
  23. package/dist/tugger-3d.js.map +1 -0
  24. package/dist/tugger.d.ts +40 -0
  25. package/dist/tugger.js +135 -0
  26. package/dist/tugger.js.map +1 -0
  27. package/dist/worker-3d.d.ts +7 -0
  28. package/dist/worker-3d.js +199 -0
  29. package/dist/worker-3d.js.map +1 -0
  30. package/dist/worker.d.ts +44 -0
  31. package/dist/worker.js +130 -0
  32. package/dist/worker.js.map +1 -0
  33. package/icons/agv.png +0 -0
  34. package/icons/forklift.png +0 -0
  35. package/icons/tugger.png +0 -0
  36. package/icons/worker.png +0 -0
  37. package/package.json +44 -0
  38. package/src/agv-3d.ts +283 -0
  39. package/src/agv.ts +207 -0
  40. package/src/forklift-3d.ts +591 -0
  41. package/src/forklift.ts +200 -0
  42. package/src/index.ts +14 -0
  43. package/src/templates/index.ts +73 -0
  44. package/src/tugger-3d.ts +169 -0
  45. package/src/tugger.ts +169 -0
  46. package/src/worker-3d.ts +232 -0
  47. package/src/worker.ts +164 -0
  48. package/things-scene.config.js +5 -0
  49. package/translations/en.json +9 -0
  50. package/translations/ja.json +9 -0
  51. package/translations/ko.json +9 -0
  52. package/translations/ms.json +9 -0
  53. package/translations/zh.json +9 -0
  54. package/tsconfig.json +23 -0
  55. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,58 @@
1
+ import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
2
+ import { type Alignment, type Heights, type LegendBinding, type PlacementArchetype } from '@operato/scene-base';
3
+ /**
4
+ * Forklift status — the operating state of a forklift truck.
5
+ *
6
+ * - `idle` — parked, engine off
7
+ * - `running` — driving (no load)
8
+ * - `lifting` — actively lifting / lowering forks
9
+ * - `loaded` — driving with cargo on forks
10
+ * - `error` — fault / emergency stop
11
+ *
12
+ * The 5-state set differs from Conveyor's because forklifts have a distinct
13
+ * loaded-vs-empty distinction relevant for fleet visualization.
14
+ */
15
+ export type ForkliftStatus = 'idle' | 'running' | 'lifting' | 'loaded' | 'error';
16
+ declare const Base: typeof Component;
17
+ /**
18
+ * Forklift — a powered industrial truck used to lift and transport material
19
+ * over short distances.
20
+ *
21
+ * **Container-based for cargo containment.** A Forklift extends `Container` so
22
+ * that loaded items (boxes, parcels, pallets) can be added as children. The
23
+ * intended visual semantics: the children's 3D representations should appear
24
+ * sitting on the forks, at a height determined by `state.forkHeight`. The
25
+ * actual *positioning* of cargo on the forks is a v2 concern — for now,
26
+ * children render with their declared archetype's zPos (operation level by
27
+ * default), which approximates the fork-down position but won't track
28
+ * `forkHeight` dynamically. See `forklift-3d.ts` for the fork attach point.
29
+ */
30
+ export default class Forklift extends Base {
31
+ static legends: Record<string, LegendBinding>;
32
+ /**
33
+ * Forklift sits on its wheels — `floor` archetype. Default depth is the
34
+ * forklift's overall envelope with mast collapsed (~= operation level).
35
+ */
36
+ static placement: PlacementArchetype;
37
+ static align: Alignment;
38
+ static defaultDepth: (h: Heights) => number;
39
+ get nature(): ComponentNature;
40
+ get anchors(): never[];
41
+ /**
42
+ * Allow items that flow at operation level (boxes, cartons, parcels, loaded
43
+ * pallets) to be added as cargo. Restricting by archetype rather than type
44
+ * means new package components are auto-permitted as they're added to the
45
+ * scene-base ecosystem.
46
+ */
47
+ containable(component: Component): boolean;
48
+ /**
49
+ * 2D — render() sets the body silhouette path (auto-filled with bodyColor
50
+ * via the Legendable→fillStyle chain). postrender() draws structural
51
+ * details (forks, mast bar, counterweight tail-light hint, status lamp).
52
+ */
53
+ render(ctx: CanvasRenderingContext2D): void;
54
+ postrender(ctx: CanvasRenderingContext2D): void;
55
+ get fillStyle(): string;
56
+ buildRealObject(): RealObject | undefined;
57
+ }
58
+ export {};
@@ -0,0 +1,163 @@
1
+ import { __decorate } from "tslib";
2
+ /*
3
+ * Copyright © HatioLab Inc. All rights reserved.
4
+ */
5
+ import { Container, sceneComponent } from '@hatiolab/things-scene';
6
+ import { Legendable, Placeable } from '@operato/scene-base';
7
+ import { Forklift3D } from './forklift-3d.js';
8
+ /** Body color — yellow base hue, modulated slightly by status for at-a-glance state. */
9
+ const BODY_LEGEND = {
10
+ idle: '#d4a017', // muted yellow (parked)
11
+ running: '#FFD700', // bright yellow (active)
12
+ lifting: '#FFD700',
13
+ loaded: '#FFA500', // orange tint (carrying)
14
+ error: '#e9746b', // red
15
+ default: '#d4a017'
16
+ };
17
+ /** Status lamp emissive — saturated for at-a-glance status from a distance. */
18
+ const LAMP_EMISSIVE_LEGEND = {
19
+ idle: '#333333',
20
+ running: '#44ff44', // green (operating)
21
+ lifting: '#44aaff', // blue (lifting)
22
+ loaded: '#ffaa00', // amber (loaded)
23
+ error: '#ff3333', // red
24
+ default: '#333333'
25
+ };
26
+ const NATURE = {
27
+ mutable: false,
28
+ resizable: true,
29
+ rotatable: true,
30
+ properties: [
31
+ {
32
+ type: 'select',
33
+ label: 'status',
34
+ name: 'status',
35
+ property: {
36
+ options: [
37
+ { display: 'Idle', value: 'idle' },
38
+ { display: 'Running', value: 'running' },
39
+ { display: 'Lifting', value: 'lifting' },
40
+ { display: 'Loaded', value: 'loaded' },
41
+ { display: 'Error', value: 'error' }
42
+ ]
43
+ }
44
+ },
45
+ {
46
+ type: 'number',
47
+ label: 'fork-height',
48
+ name: 'forkHeight',
49
+ placeholder: '0..2000mm'
50
+ }
51
+ ],
52
+ help: 'scene/component/forklift'
53
+ };
54
+ const Base = Legendable(Placeable(Container));
55
+ /**
56
+ * Forklift — a powered industrial truck used to lift and transport material
57
+ * over short distances.
58
+ *
59
+ * **Container-based for cargo containment.** A Forklift extends `Container` so
60
+ * that loaded items (boxes, parcels, pallets) can be added as children. The
61
+ * intended visual semantics: the children's 3D representations should appear
62
+ * sitting on the forks, at a height determined by `state.forkHeight`. The
63
+ * actual *positioning* of cargo on the forks is a v2 concern — for now,
64
+ * children render with their declared archetype's zPos (operation level by
65
+ * default), which approximates the fork-down position but won't track
66
+ * `forkHeight` dynamically. See `forklift-3d.ts` for the fork attach point.
67
+ */
68
+ let Forklift = class Forklift extends Base {
69
+ static legends = {
70
+ bodyColor: { from: 'status', legend: BODY_LEGEND },
71
+ lampEmissive: { from: 'status', legend: LAMP_EMISSIVE_LEGEND }
72
+ };
73
+ /**
74
+ * Forklift sits on its wheels — `floor` archetype. Default depth is the
75
+ * forklift's overall envelope with mast collapsed (~= operation level).
76
+ */
77
+ static placement = 'floor';
78
+ static align = 'bottom';
79
+ static defaultDepth = (h) => h.operation - h.floor;
80
+ get nature() {
81
+ return NATURE;
82
+ }
83
+ get anchors() {
84
+ return [];
85
+ }
86
+ /**
87
+ * Allow items that flow at operation level (boxes, cartons, parcels, loaded
88
+ * pallets) to be added as cargo. Restricting by archetype rather than type
89
+ * means new package components are auto-permitted as they're added to the
90
+ * scene-base ecosystem.
91
+ */
92
+ containable(component) {
93
+ const archetype = component.constructor.placement;
94
+ if (archetype === 'operation')
95
+ return true;
96
+ // Container's default — accept anything descendible (default things-scene policy).
97
+ return component.isDescendible(this);
98
+ }
99
+ /**
100
+ * 2D — render() sets the body silhouette path (auto-filled with bodyColor
101
+ * via the Legendable→fillStyle chain). postrender() draws structural
102
+ * details (forks, mast bar, counterweight tail-light hint, status lamp).
103
+ */
104
+ render(ctx) {
105
+ const { width, height, left, top } = this.state;
106
+ const forkLen = height * 0.30;
107
+ const cwBulge = height * 0.10;
108
+ const radius = Math.min(width, height) * 0.10;
109
+ ctx.beginPath();
110
+ // Body with rounded corners + rear counterweight bulge (slight extension at top
111
+ // edge in top-view convention: top = back of forklift)
112
+ ctx.roundRect(left, top - cwBulge, width, height - forkLen + cwBulge, radius);
113
+ }
114
+ postrender(ctx) {
115
+ super.postrender?.(ctx);
116
+ const { width, height, left, top } = this.state;
117
+ const forkLen = height * 0.30;
118
+ const accentColor = this.state.lampEmissive || '#44ff44';
119
+ ctx.save();
120
+ // Mast (thin horizontal bar at the front of the body, before the forks)
121
+ const mastY = top + height - forkLen;
122
+ ctx.fillStyle = '#444455';
123
+ ctx.fillRect(left + width * 0.10, mastY - height * 0.025, width * 0.80, height * 0.025);
124
+ // Forks (two prongs extending forward — bottom of the top-down silhouette)
125
+ const forkW = width * 0.10;
126
+ ctx.fillStyle = '#222233';
127
+ ctx.strokeStyle = '#111';
128
+ ctx.lineWidth = 1;
129
+ for (const xFrac of [0.20, 0.70]) {
130
+ ctx.beginPath();
131
+ ctx.rect(left + width * xFrac, mastY, forkW, forkLen);
132
+ ctx.fill();
133
+ ctx.stroke();
134
+ }
135
+ // Tail lights on counterweight rear (top edge in top-down view)
136
+ ctx.fillStyle = '#ff2222';
137
+ const tailLightW = width * 0.08;
138
+ const tailLightH = height * 0.025;
139
+ const cwBulge = height * 0.10;
140
+ ctx.fillRect(left + width * 0.15, top - cwBulge + tailLightH * 0.5, tailLightW, tailLightH);
141
+ ctx.fillRect(left + width - width * 0.15 - tailLightW, top - cwBulge + tailLightH * 0.5, tailLightW, tailLightH);
142
+ // Status lamp on top of overhead guard (center)
143
+ const lampR = Math.min(width, height) * 0.06;
144
+ ctx.fillStyle = accentColor;
145
+ ctx.strokeStyle = '#111';
146
+ ctx.beginPath();
147
+ ctx.ellipse(left + width / 2, top + height * 0.30, lampR, lampR, 0, 0, Math.PI * 2);
148
+ ctx.fill();
149
+ ctx.stroke();
150
+ ctx.restore();
151
+ }
152
+ get fillStyle() {
153
+ return this.state.bodyColor || '#d4a017';
154
+ }
155
+ buildRealObject() {
156
+ return new Forklift3D(this);
157
+ }
158
+ };
159
+ Forklift = __decorate([
160
+ sceneComponent('forklift')
161
+ ], Forklift);
162
+ export default Forklift;
163
+ //# sourceMappingURL=forklift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forklift.js","sourceRoot":"","sources":["../src/forklift.ts"],"names":[],"mappings":";AAAA;;GAEG;AACH,OAAO,EAA8B,SAAS,EAAc,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC1G,OAAO,EACL,UAAU,EACV,SAAS,EAKV,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAgB7C,wFAAwF;AACxF,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,SAAS,EAAM,wBAAwB;IAC7C,OAAO,EAAE,SAAS,EAAG,yBAAyB;IAC9C,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,SAAS,EAAI,yBAAyB;IAC9C,KAAK,EAAE,SAAS,EAAK,MAAM;IAC3B,OAAO,EAAE,SAAS;CACnB,CAAA;AAED,+EAA+E;AAC/E,MAAM,oBAAoB,GAAG;IAC3B,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,SAAS,EAAG,oBAAoB;IACzC,OAAO,EAAE,SAAS,EAAG,iBAAiB;IACtC,MAAM,EAAE,SAAS,EAAI,iBAAiB;IACtC,KAAK,EAAE,SAAS,EAAK,MAAM;IAC3B,OAAO,EAAE,SAAS;CACnB,CAAA;AAED,MAAM,MAAM,GAAoB;IAC9B,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,IAAI;IACf,UAAU,EAAE;QACV;YACE,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,QAAQ;YACf,IAAI,EAAE,QAAQ;YACd,QAAQ,EAAE;gBACR,OAAO,EAAE;oBACP,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;oBAClC,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;oBACxC,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;oBACxC,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE;oBACtC,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;iBACrC;aACF;SACF;QACD;YACE,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,aAAa;YACpB,IAAI,EAAE,YAAY;YAClB,WAAW,EAAE,WAAW;SACzB;KACF;IACD,IAAI,EAAE,0BAA0B;CACjC,CAAA;AAED,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,SAAS,CAAC,CAAgC,CAAA;AAE5E;;;;;;;;;;;;GAYG;AAEY,IAAM,QAAQ,GAAd,MAAM,QAAS,SAAQ,IAAI;IACxC,MAAM,CAAC,OAAO,GAAkC;QAC9C,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE;QAClD,YAAY,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,oBAAoB,EAAE;KAC/D,CAAA;IAED;;;OAGG;IACH,MAAM,CAAC,SAAS,GAAuB,OAAO,CAAA;IAC9C,MAAM,CAAC,KAAK,GAAc,QAAQ,CAAA;IAClC,MAAM,CAAC,YAAY,GAAG,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,KAAK,CAAA;IAE3D,IAAI,MAAM;QACR,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,OAAO;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED;;;;;OAKG;IACH,WAAW,CAAC,SAAoB;QAC9B,MAAM,SAAS,GAAI,SAAS,CAAC,WAAmB,CAAC,SAAS,CAAA;QAC1D,IAAI,SAAS,KAAK,WAAW;YAAE,OAAO,IAAI,CAAA;QAC1C,mFAAmF;QACnF,OAAO,SAAS,CAAC,aAAa,CAAC,IAAW,CAAC,CAAA;IAC7C,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAA6B;QAClC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAA;QAC/C,MAAM,OAAO,GAAG,MAAM,GAAG,IAAI,CAAA;QAC7B,MAAM,OAAO,GAAG,MAAM,GAAG,IAAI,CAAA;QAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;QAE7C,GAAG,CAAC,SAAS,EAAE,CAAA;QACf,gFAAgF;QAChF,uDAAuD;QACvD,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EAAE,MAAM,CAAC,CAAA;IAC/E,CAAC;IAED,UAAU,CAAC,GAA6B;QACtC,KAAK,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAA;QAEvB,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAA;QAC/C,MAAM,OAAO,GAAG,MAAM,GAAG,IAAI,CAAA;QAC7B,MAAM,WAAW,GAAI,IAAI,CAAC,KAAK,CAAC,YAAuB,IAAI,SAAS,CAAA;QAEpE,GAAG,CAAC,IAAI,EAAE,CAAA;QAEV,wEAAwE;QACxE,MAAM,KAAK,GAAG,GAAG,GAAG,MAAM,GAAG,OAAO,CAAA;QACpC,GAAG,CAAC,SAAS,GAAG,SAAS,CAAA;QACzB,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,CAAA;QAEvF,2EAA2E;QAC3E,MAAM,KAAK,GAAG,KAAK,GAAG,IAAI,CAAA;QAC1B,GAAG,CAAC,SAAS,GAAG,SAAS,CAAA;QACzB,GAAG,CAAC,WAAW,GAAG,MAAM,CAAA;QACxB,GAAG,CAAC,SAAS,GAAG,CAAC,CAAA;QACjB,KAAK,MAAM,KAAK,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YACjC,GAAG,CAAC,SAAS,EAAE,CAAA;YACf,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,KAAK,GAAG,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAA;YACrD,GAAG,CAAC,IAAI,EAAE,CAAA;YACV,GAAG,CAAC,MAAM,EAAE,CAAA;QACd,CAAC;QAED,gEAAgE;QAChE,GAAG,CAAC,SAAS,GAAG,SAAS,CAAA;QACzB,MAAM,UAAU,GAAG,KAAK,GAAG,IAAI,CAAA;QAC/B,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,CAAA;QACjC,MAAM,OAAO,GAAG,MAAM,GAAG,IAAI,CAAA;QAC7B,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,KAAK,GAAG,IAAI,EAAE,GAAG,GAAG,OAAO,GAAG,UAAU,GAAG,GAAG,EAAE,UAAU,EAAE,UAAU,CAAC,CAAA;QAC3F,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,GAAG,UAAU,EAAE,GAAG,GAAG,OAAO,GAAG,UAAU,GAAG,GAAG,EAAE,UAAU,EAAE,UAAU,CAAC,CAAA;QAEhH,gDAAgD;QAChD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;QAC5C,GAAG,CAAC,SAAS,GAAG,WAAW,CAAA;QAC3B,GAAG,CAAC,WAAW,GAAG,MAAM,CAAA;QACxB,GAAG,CAAC,SAAS,EAAE,CAAA;QACf,GAAG,CAAC,OAAO,CAAC,IAAI,GAAG,KAAK,GAAG,CAAC,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;QACnF,GAAG,CAAC,IAAI,EAAE,CAAA;QACV,GAAG,CAAC,MAAM,EAAE,CAAA;QAEZ,GAAG,CAAC,OAAO,EAAE,CAAA;IACf,CAAC;IAED,IAAI,SAAS;QACX,OAAQ,IAAI,CAAC,KAAK,CAAC,SAAoB,IAAI,SAAS,CAAA;IACtD,CAAC;IAED,eAAe;QACb,OAAO,IAAI,UAAU,CAAC,IAAW,CAAC,CAAA;IACpC,CAAC;;AAxGkB,QAAQ;IAD5B,cAAc,CAAC,UAAU,CAAC;GACN,QAAQ,CAyG5B;eAzGoB,QAAQ","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport { Component, ComponentNature, Container, RealObject, sceneComponent } from '@hatiolab/things-scene'\nimport {\n Legendable,\n Placeable,\n type Alignment,\n type Heights,\n type LegendBinding,\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 help: 'scene/component/forklift'\n}\n\nconst Base = Legendable(Placeable(Container)) as unknown as typeof Component\n\n/**\n * Forklift — a powered industrial truck used to lift and transport material\n * over short distances.\n *\n * **Container-based for cargo containment.** A Forklift extends `Container` so\n * that loaded items (boxes, parcels, pallets) can be added as children. The\n * intended visual semantics: the children's 3D representations should appear\n * sitting on the forks, at a height determined by `state.forkHeight`. The\n * actual *positioning* of cargo on the forks is a v2 concern — for now,\n * children render with their declared archetype's zPos (operation level by\n * default), which approximates the fork-down position but won't track\n * `forkHeight` dynamically. See `forklift-3d.ts` for the fork attach point.\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 get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\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 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 * 2D — render() sets the body silhouette path (auto-filled with bodyColor\n * via the Legendable→fillStyle chain). postrender() draws structural\n * details (forks, mast bar, counterweight tail-light hint, status lamp).\n */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n const forkLen = height * 0.30\n const cwBulge = height * 0.10\n const radius = Math.min(width, height) * 0.10\n\n ctx.beginPath()\n // Body with rounded corners + rear counterweight bulge (slight extension at top\n // edge in top-view convention: top = back of forklift)\n ctx.roundRect(left, top - cwBulge, 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.30\n const accentColor = (this.state.lampEmissive as string) || '#44ff44'\n\n ctx.save()\n\n // Mast (thin horizontal bar at the front of the body, before the forks)\n const mastY = top + height - forkLen\n ctx.fillStyle = '#444455'\n ctx.fillRect(left + width * 0.10, mastY - height * 0.025, width * 0.80, height * 0.025)\n\n // Forks (two prongs extending forward — bottom of the top-down silhouette)\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, mastY, forkW, forkLen)\n ctx.fill()\n ctx.stroke()\n }\n\n // Tail lights on counterweight rear (top edge in top-down view)\n ctx.fillStyle = '#ff2222'\n const tailLightW = width * 0.08\n const tailLightH = height * 0.025\n const cwBulge = height * 0.10\n ctx.fillRect(left + width * 0.15, top - cwBulge + tailLightH * 0.5, tailLightW, tailLightH)\n ctx.fillRect(left + width - width * 0.15 - tailLightW, top - cwBulge + tailLightH * 0.5, tailLightW, tailLightH)\n\n // Status lamp on top of overhead guard (center)\n const lampR = Math.min(width, height) * 0.06\n ctx.fillStyle = accentColor\n ctx.strokeStyle = '#111'\n ctx.beginPath()\n ctx.ellipse(left + width / 2, top + height * 0.30, lampR, lampR, 0, 0, Math.PI * 2)\n ctx.fill()\n ctx.stroke()\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"]}
@@ -0,0 +1,8 @@
1
+ export { default as Forklift } from './forklift.js';
2
+ export type { ForkliftStatus } from './forklift.js';
3
+ export { default as Worker } from './worker.js';
4
+ export type { WorkerStatus } from './worker.js';
5
+ export { default as Agv } from './agv.js';
6
+ export type { AgvStatus } from './agv.js';
7
+ export { default as Tugger } from './tugger.js';
8
+ export type { TuggerStatus } from './tugger.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ export { default as Forklift } from './forklift.js';
5
+ export { default as Worker } from './worker.js';
6
+ export { default as Agv } from './agv.js';
7
+ export { default as Tugger } from './tugger.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAA;AAGnD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAA;AAG/C,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,UAAU,CAAA;AAGzC,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAA","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nexport { default as Forklift } from './forklift.js'\nexport type { ForkliftStatus } from './forklift.js'\n\nexport { default as Worker } from './worker.js'\nexport type { WorkerStatus } from './worker.js'\n\nexport { default as Agv } from './agv.js'\nexport type { AgvStatus } from './agv.js'\n\nexport { default as Tugger } from './tugger.js'\nexport type { TuggerStatus } from './tugger.js'\n"]}
@@ -0,0 +1,47 @@
1
+ declare const _default: ({
2
+ type: string;
3
+ description: string;
4
+ group: string;
5
+ icon: string;
6
+ model: {
7
+ type: string;
8
+ top: number;
9
+ left: number;
10
+ width: number;
11
+ height: number;
12
+ status: string;
13
+ forkHeight: number;
14
+ battery?: undefined;
15
+ };
16
+ } | {
17
+ type: string;
18
+ description: string;
19
+ group: string;
20
+ icon: string;
21
+ model: {
22
+ type: string;
23
+ top: number;
24
+ left: number;
25
+ width: number;
26
+ height: number;
27
+ status: string;
28
+ forkHeight?: undefined;
29
+ battery?: undefined;
30
+ };
31
+ } | {
32
+ type: string;
33
+ description: string;
34
+ group: string;
35
+ icon: string;
36
+ model: {
37
+ type: string;
38
+ top: number;
39
+ left: number;
40
+ width: number;
41
+ height: number;
42
+ status: string;
43
+ battery: number;
44
+ forkHeight?: undefined;
45
+ };
46
+ })[];
47
+ export default _default;
@@ -0,0 +1,73 @@
1
+ /*
2
+ * things-scene catalog templates for the transport domain.
3
+ *
4
+ * Icons reference PNG assets in `packages/transport/icons/`. They are not yet
5
+ * shipped — the catalog will fall back to the default rendering if icons are
6
+ * missing. Add icon files before publishing the catalog publicly.
7
+ */
8
+ const forklift = new URL('../../icons/forklift.png', import.meta.url).href;
9
+ const worker = new URL('../../icons/worker.png', import.meta.url).href;
10
+ const agv = new URL('../../icons/agv.png', import.meta.url).href;
11
+ const tugger = new URL('../../icons/tugger.png', import.meta.url).href;
12
+ export default [
13
+ {
14
+ type: 'forklift',
15
+ description: 'forklift truck',
16
+ group: 'transport',
17
+ icon: forklift,
18
+ model: {
19
+ type: 'forklift',
20
+ top: 100,
21
+ left: 100,
22
+ width: 120,
23
+ height: 200,
24
+ status: 'idle',
25
+ forkHeight: 0
26
+ }
27
+ },
28
+ {
29
+ type: 'worker',
30
+ description: 'human worker',
31
+ group: 'transport',
32
+ icon: worker,
33
+ model: {
34
+ type: 'worker',
35
+ top: 100,
36
+ left: 300,
37
+ width: 50,
38
+ height: 50,
39
+ status: 'idle'
40
+ }
41
+ },
42
+ {
43
+ type: 'agv',
44
+ description: 'payload AGV (unit-load, Kiva-style)',
45
+ group: 'transport',
46
+ icon: agv,
47
+ model: {
48
+ type: 'agv',
49
+ top: 100,
50
+ left: 500,
51
+ width: 200,
52
+ height: 200,
53
+ status: 'idle',
54
+ battery: 1
55
+ }
56
+ },
57
+ {
58
+ type: 'tugger',
59
+ description: 'tugger AGV (towing tractor)',
60
+ group: 'transport',
61
+ icon: tugger,
62
+ model: {
63
+ type: 'tugger',
64
+ top: 100,
65
+ left: 800,
66
+ width: 150,
67
+ height: 250,
68
+ status: 'idle',
69
+ battery: 1
70
+ }
71
+ }
72
+ ];
73
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,0BAA0B,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAC1E,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,wBAAwB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AACtE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,qBAAqB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAChE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,wBAAwB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAEtE,eAAe;IACb;QACE,IAAI,EAAE,UAAU;QAChB,WAAW,EAAE,gBAAgB;QAC7B,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE;YACL,IAAI,EAAE,UAAU;YAChB,GAAG,EAAE,GAAG;YACR,IAAI,EAAE,GAAG;YACT,KAAK,EAAE,GAAG;YACV,MAAM,EAAE,GAAG;YACX,MAAM,EAAE,MAAM;YACd,UAAU,EAAE,CAAC;SACd;KACF;IACD;QACE,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,cAAc;QAC3B,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE;YACL,IAAI,EAAE,QAAQ;YACd,GAAG,EAAE,GAAG;YACR,IAAI,EAAE,GAAG;YACT,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,EAAE;YACV,MAAM,EAAE,MAAM;SACf;KACF;IACD;QACE,IAAI,EAAE,KAAK;QACX,WAAW,EAAE,qCAAqC;QAClD,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,GAAG;QACT,KAAK,EAAE;YACL,IAAI,EAAE,KAAK;YACX,GAAG,EAAE,GAAG;YACR,IAAI,EAAE,GAAG;YACT,KAAK,EAAE,GAAG;YACV,MAAM,EAAE,GAAG;YACX,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,CAAC;SACX;KACF;IACD;QACE,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,6BAA6B;QAC1C,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE;YACL,IAAI,EAAE,QAAQ;YACd,GAAG,EAAE,GAAG;YACR,IAAI,EAAE,GAAG;YACT,KAAK,EAAE,GAAG;YACV,MAAM,EAAE,GAAG;YACX,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,CAAC;SACX;KACF;CACF,CAAA","sourcesContent":["/*\n * things-scene catalog templates for the transport domain.\n *\n * Icons reference PNG assets in `packages/transport/icons/`. They are not yet\n * shipped — the catalog will fall back to the default rendering if icons are\n * missing. Add icon files before publishing the catalog publicly.\n */\nconst forklift = new URL('../../icons/forklift.png', import.meta.url).href\nconst worker = new URL('../../icons/worker.png', import.meta.url).href\nconst agv = new URL('../../icons/agv.png', import.meta.url).href\nconst tugger = new URL('../../icons/tugger.png', import.meta.url).href\n\nexport default [\n {\n type: 'forklift',\n description: 'forklift truck',\n group: 'transport',\n icon: forklift,\n model: {\n type: 'forklift',\n top: 100,\n left: 100,\n width: 120,\n height: 200,\n status: 'idle',\n forkHeight: 0\n }\n },\n {\n type: 'worker',\n description: 'human worker',\n group: 'transport',\n icon: worker,\n model: {\n type: 'worker',\n top: 100,\n left: 300,\n width: 50,\n height: 50,\n status: 'idle'\n }\n },\n {\n type: 'agv',\n description: 'payload AGV (unit-load, Kiva-style)',\n group: 'transport',\n icon: agv,\n model: {\n type: 'agv',\n top: 100,\n left: 500,\n width: 200,\n height: 200,\n status: 'idle',\n battery: 1\n }\n },\n {\n type: 'tugger',\n description: 'tugger AGV (towing tractor)',\n group: 'transport',\n icon: tugger,\n model: {\n type: 'tugger',\n top: 100,\n left: 800,\n width: 150,\n height: 250,\n status: 'idle',\n battery: 1\n }\n }\n]\n"]}
@@ -0,0 +1,7 @@
1
+ import { RealObjectGroup } from '@hatiolab/things-scene';
2
+ export declare class Tugger3D extends RealObjectGroup {
3
+ build(): void;
4
+ updateDimension(): void;
5
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>): void;
6
+ updateAlpha(): void;
7
+ }
@@ -0,0 +1,140 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * Tugger 3D — towing AGV.
5
+ *
6
+ * LO-POLY but structurally complete:
7
+ *
8
+ * - chassis (compact body, ~600mm tall — no cargo deck)
9
+ * - control panel hump on top (operator-less but houses electronics)
10
+ * - 4 wheels (front pair = drive/steer, larger; rear pair = caster, smaller)
11
+ * - LiDAR sensor at front (forward-facing — detects obstacles)
12
+ * - hitch at rear (the defining feature — what distinguishes a tugger from
13
+ * a payload AGV)
14
+ * - status lamp on top of control panel
15
+ *
16
+ * Design rule: the hitch is the visual signature. Without it, this looks like
17
+ * a tall payload AGV. Make sure the hitch is unambiguously visible.
18
+ */
19
+ import * as THREE from 'three';
20
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
21
+ import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
22
+ import { RealObjectGroup } from '@hatiolab/things-scene';
23
+ const TIRE_COLOR = 0x202020;
24
+ const HITCH_COLOR = 0x222233;
25
+ const PANEL_COLOR = 0x444455;
26
+ const LIDAR_COLOR = 0x222233;
27
+ export class Tugger3D extends RealObjectGroup {
28
+ build() {
29
+ super.build();
30
+ const { width, height, depth = 600 } = this.component.state;
31
+ const bodyColor = this.component.state.bodyColor || '#666';
32
+ const emissiveColor = this.component.state.lampEmissive || '#222222';
33
+ const status = this.component.state.status;
34
+ const lampIntensity = status && status !== 'idle' ? 1.5 : 0.2;
35
+ // Proportions
36
+ const chassisH = depth * 0.55;
37
+ const panelH = depth * 0.30;
38
+ const wheelR = chassisH * 0.45;
39
+ const baseY = -depth / 2;
40
+ const bodyMaterial = new THREE.MeshStandardMaterial({
41
+ color: bodyColor,
42
+ metalness: 0.4,
43
+ roughness: 0.5
44
+ });
45
+ // ── Chassis (rounded body) ────────────────────────────────────
46
+ const chassisRadius = Math.min(width, height) * 0.10;
47
+ const chassisGeo = new RoundedBoxGeometry(width * 0.9, chassisH, height * 0.85, 4, chassisRadius);
48
+ const chassisMesh = new THREE.Mesh(chassisGeo, bodyMaterial);
49
+ chassisMesh.position.set(0, baseY + chassisH / 2 + wheelR * 0.3, 0);
50
+ chassisMesh.castShadow = true;
51
+ chassisMesh.receiveShadow = true;
52
+ this.object3d.add(chassisMesh);
53
+ // ── Control panel (rounded hump on top) ───────────────────────
54
+ const panelGeo = new RoundedBoxGeometry(width * 0.55, panelH, height * 0.5, 3, chassisRadius * 0.6);
55
+ const panelMesh = new THREE.Mesh(panelGeo, new THREE.MeshStandardMaterial({ color: PANEL_COLOR, metalness: 0.5, roughness: 0.4 }));
56
+ panelMesh.position.set(0, baseY + chassisH + panelH / 2 + wheelR * 0.3, 0);
57
+ panelMesh.castShadow = true;
58
+ this.object3d.add(panelMesh);
59
+ // ── LiDAR sensor at front ─────────────────────────────────────
60
+ const lidarR = Math.min(width, height) * 0.07;
61
+ const lidarH = lidarR * 0.7;
62
+ const lidarGeo = new THREE.CylinderGeometry(lidarR, lidarR * 1.1, lidarH, 24);
63
+ const lidarMesh = new THREE.Mesh(lidarGeo, new THREE.MeshStandardMaterial({ color: LIDAR_COLOR, metalness: 0.6, roughness: 0.4 }));
64
+ lidarMesh.position.set(0, baseY + chassisH + lidarH / 2 + wheelR * 0.3, height * 0.40);
65
+ lidarMesh.castShadow = true;
66
+ this.object3d.add(lidarMesh);
67
+ // ── Hitch at rear (the tugger's signature) ────────────────────
68
+ const hitchW = width * 0.15;
69
+ const hitchH = chassisH * 0.30;
70
+ const hitchD = height * 0.18;
71
+ const hitchMaterial = new THREE.MeshStandardMaterial({
72
+ color: HITCH_COLOR,
73
+ metalness: 0.85,
74
+ roughness: 0.4
75
+ });
76
+ // Hitch arm (horizontal bar extending rearward)
77
+ const hitchArmGeo = new THREE.BoxGeometry(hitchW * 0.4, hitchH * 0.4, hitchD);
78
+ const hitchArmMesh = new THREE.Mesh(hitchArmGeo, hitchMaterial);
79
+ hitchArmMesh.position.set(0, baseY + wheelR * 0.6, -height * 0.5 - hitchD / 2);
80
+ hitchArmMesh.castShadow = true;
81
+ this.object3d.add(hitchArmMesh);
82
+ // Hitch coupler (the ball/pin where a trailer attaches)
83
+ const couplerR = hitchH * 0.5;
84
+ const couplerGeo = new THREE.SphereGeometry(couplerR, 20, 8);
85
+ const couplerMesh = new THREE.Mesh(couplerGeo, hitchMaterial);
86
+ couplerMesh.position.set(0, baseY + wheelR * 0.6 + couplerR * 0.5, -height * 0.5 - hitchD - couplerR * 0.2);
87
+ couplerMesh.castShadow = true;
88
+ this.object3d.add(couplerMesh);
89
+ // ── Wheels — front (larger, drive/steer) and rear (smaller, caster) ─
90
+ const frontWheelR = wheelR;
91
+ const rearWheelR = wheelR * 0.7;
92
+ const wheelGeometries = [];
93
+ // Front pair
94
+ for (const xSign of [-1, 1]) {
95
+ const wheel = new THREE.CylinderGeometry(frontWheelR, frontWheelR, width * 0.08, 20);
96
+ wheel.rotateZ(Math.PI / 2);
97
+ wheel.translate(xSign * width * 0.4, baseY + frontWheelR, height * 0.30);
98
+ wheelGeometries.push(wheel);
99
+ }
100
+ // Rear pair (smaller)
101
+ for (const xSign of [-1, 1]) {
102
+ const wheel = new THREE.CylinderGeometry(rearWheelR, rearWheelR, width * 0.07, 20);
103
+ wheel.rotateZ(Math.PI / 2);
104
+ wheel.translate(xSign * width * 0.35, baseY + rearWheelR, -height * 0.32);
105
+ wheelGeometries.push(wheel);
106
+ }
107
+ const wheelMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wheelGeometries), new THREE.MeshStandardMaterial({ color: TIRE_COLOR, roughness: 0.95 }));
108
+ wheelMesh.castShadow = true;
109
+ this.object3d.add(wheelMesh);
110
+ // ── Status lamp on control panel top ──────────────────────────
111
+ const lampR = Math.min(width, height) * 0.05;
112
+ const lampH = lampR * 1.3;
113
+ const lampMaterial = new THREE.MeshStandardMaterial({
114
+ color: emissiveColor,
115
+ emissive: emissiveColor,
116
+ emissiveIntensity: lampIntensity,
117
+ metalness: 0,
118
+ roughness: 0.3
119
+ });
120
+ const lampGeo = new THREE.CylinderGeometry(lampR, lampR * 0.85, lampH, 20);
121
+ const lampMesh = new THREE.Mesh(lampGeo, lampMaterial);
122
+ lampMesh.position.set(0, baseY + chassisH + panelH + lampH / 2 + wheelR * 0.3, 0);
123
+ this.object3d.add(lampMesh);
124
+ }
125
+ updateDimension() { }
126
+ onchange(after, before) {
127
+ if ('status' in after ||
128
+ 'bodyColor' in after ||
129
+ 'lampEmissive' in after ||
130
+ 'width' in after ||
131
+ 'height' in after ||
132
+ 'depth' in after) {
133
+ this.update();
134
+ return;
135
+ }
136
+ super.onchange(after, before);
137
+ }
138
+ updateAlpha() { }
139
+ }
140
+ //# sourceMappingURL=tugger-3d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tugger-3d.js","sourceRoot":"","sources":["../src/tugger-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Tugger 3D — towing AGV.\n *\n * LO-POLY but structurally complete:\n *\n * - chassis (compact body, ~600mm tall — no cargo deck)\n * - control panel hump on top (operator-less but houses electronics)\n * - 4 wheels (front pair = drive/steer, larger; rear pair = caster, smaller)\n * - LiDAR sensor at front (forward-facing — detects obstacles)\n * - hitch at rear (the defining feature — what distinguishes a tugger from\n * a payload AGV)\n * - status lamp on top of control panel\n *\n * Design rule: the hitch is the visual signature. Without it, this looks like\n * a tall payload AGV. Make sure the hitch is unambiguously visible.\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\nconst TIRE_COLOR = 0x202020\nconst HITCH_COLOR = 0x222233\nconst PANEL_COLOR = 0x444455\nconst LIDAR_COLOR = 0x222233\n\nexport class Tugger3D extends RealObjectGroup {\n build() {\n super.build()\n\n const { width, height, depth = 600 } = this.component.state\n const bodyColor = (this.component.state.bodyColor as string) || '#666'\n const emissiveColor = (this.component.state.lampEmissive as string) || '#222222'\n const status = this.component.state.status\n const lampIntensity = status && status !== 'idle' ? 1.5 : 0.2\n\n // Proportions\n const chassisH = depth * 0.55\n const panelH = depth * 0.30\n const wheelR = chassisH * 0.45\n const baseY = -depth / 2\n\n const bodyMaterial = new THREE.MeshStandardMaterial({\n color: bodyColor,\n metalness: 0.4,\n roughness: 0.5\n })\n\n // ── Chassis (rounded body) ────────────────────────────────────\n const chassisRadius = Math.min(width, height) * 0.10\n const chassisGeo = new RoundedBoxGeometry(width * 0.9, chassisH, height * 0.85, 4, chassisRadius)\n const chassisMesh = new THREE.Mesh(chassisGeo, bodyMaterial)\n chassisMesh.position.set(0, baseY + chassisH / 2 + wheelR * 0.3, 0)\n chassisMesh.castShadow = true\n chassisMesh.receiveShadow = true\n this.object3d.add(chassisMesh)\n\n // ── Control panel (rounded hump on top) ───────────────────────\n const panelGeo = new RoundedBoxGeometry(width * 0.55, panelH, height * 0.5, 3, chassisRadius * 0.6)\n const panelMesh = new THREE.Mesh(\n panelGeo,\n new THREE.MeshStandardMaterial({ color: PANEL_COLOR, metalness: 0.5, roughness: 0.4 })\n )\n panelMesh.position.set(0, baseY + chassisH + panelH / 2 + wheelR * 0.3, 0)\n panelMesh.castShadow = true\n this.object3d.add(panelMesh)\n\n // ── LiDAR sensor at front ─────────────────────────────────────\n const lidarR = Math.min(width, height) * 0.07\n const lidarH = lidarR * 0.7\n const lidarGeo = new THREE.CylinderGeometry(lidarR, lidarR * 1.1, lidarH, 24)\n const lidarMesh = new THREE.Mesh(\n lidarGeo,\n new THREE.MeshStandardMaterial({ color: LIDAR_COLOR, metalness: 0.6, roughness: 0.4 })\n )\n lidarMesh.position.set(0, baseY + chassisH + lidarH / 2 + wheelR * 0.3, height * 0.40)\n lidarMesh.castShadow = true\n this.object3d.add(lidarMesh)\n\n // ── Hitch at rear (the tugger's signature) ────────────────────\n const hitchW = width * 0.15\n const hitchH = chassisH * 0.30\n const hitchD = height * 0.18\n const hitchMaterial = new THREE.MeshStandardMaterial({\n color: HITCH_COLOR,\n metalness: 0.85,\n roughness: 0.4\n })\n\n // Hitch arm (horizontal bar extending rearward)\n const hitchArmGeo = new THREE.BoxGeometry(hitchW * 0.4, hitchH * 0.4, hitchD)\n const hitchArmMesh = new THREE.Mesh(hitchArmGeo, hitchMaterial)\n hitchArmMesh.position.set(0, baseY + wheelR * 0.6, -height * 0.5 - hitchD / 2)\n hitchArmMesh.castShadow = true\n this.object3d.add(hitchArmMesh)\n\n // Hitch coupler (the ball/pin where a trailer attaches)\n const couplerR = hitchH * 0.5\n const couplerGeo = new THREE.SphereGeometry(couplerR, 20, 8)\n const couplerMesh = new THREE.Mesh(couplerGeo, hitchMaterial)\n couplerMesh.position.set(0, baseY + wheelR * 0.6 + couplerR * 0.5, -height * 0.5 - hitchD - couplerR * 0.2)\n couplerMesh.castShadow = true\n this.object3d.add(couplerMesh)\n\n // ── Wheels — front (larger, drive/steer) and rear (smaller, caster) ─\n const frontWheelR = wheelR\n const rearWheelR = wheelR * 0.7\n const wheelGeometries: THREE.BufferGeometry[] = []\n\n // Front pair\n for (const xSign of [-1, 1]) {\n const wheel = new THREE.CylinderGeometry(frontWheelR, frontWheelR, width * 0.08, 20)\n wheel.rotateZ(Math.PI / 2)\n wheel.translate(xSign * width * 0.4, baseY + frontWheelR, height * 0.30)\n wheelGeometries.push(wheel)\n }\n // Rear pair (smaller)\n for (const xSign of [-1, 1]) {\n const wheel = new THREE.CylinderGeometry(rearWheelR, rearWheelR, width * 0.07, 20)\n wheel.rotateZ(Math.PI / 2)\n wheel.translate(xSign * width * 0.35, baseY + rearWheelR, -height * 0.32)\n wheelGeometries.push(wheel)\n }\n\n const wheelMesh = new THREE.Mesh(\n BufferGeometryUtils.mergeGeometries(wheelGeometries),\n new THREE.MeshStandardMaterial({ color: TIRE_COLOR, roughness: 0.95 })\n )\n wheelMesh.castShadow = true\n this.object3d.add(wheelMesh)\n\n // ── Status lamp on control panel top ──────────────────────────\n const lampR = Math.min(width, height) * 0.05\n const lampH = lampR * 1.3\n const lampMaterial = new THREE.MeshStandardMaterial({\n color: emissiveColor,\n emissive: emissiveColor,\n emissiveIntensity: lampIntensity,\n metalness: 0,\n roughness: 0.3\n })\n const lampGeo = new THREE.CylinderGeometry(lampR, lampR * 0.85, lampH, 20)\n const lampMesh = new THREE.Mesh(lampGeo, lampMaterial)\n lampMesh.position.set(0, baseY + chassisH + panelH + lampH / 2 + wheelR * 0.3, 0)\n this.object3d.add(lampMesh)\n }\n\n updateDimension() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if (\n 'status' in after ||\n 'bodyColor' in after ||\n 'lampEmissive' in after ||\n 'width' in after ||\n 'height' in after ||\n 'depth' in after\n ) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n\n updateAlpha() {}\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
2
+ import { type Alignment, type LegendBinding, type PlacementArchetype } from '@operato/scene-base';
3
+ import type { AgvStatus } from './agv.js';
4
+ /**
5
+ * Tugger — a towing AGV. Pulls trailers / carts behind it; has no cargo deck
6
+ * of its own. Common in mail sorting, automotive line-side delivery, hospital
7
+ * supply transport.
8
+ *
9
+ * Status enum is reused from {@link AgvStatus} — the tugger and the unit-load
10
+ * AGV share the same operating-state semantics. The structural difference
11
+ * (cargo deck vs hitch) is in geometry, not state.
12
+ */
13
+ export type TuggerStatus = AgvStatus;
14
+ declare const Base: typeof Component;
15
+ /**
16
+ * Tugger has no cargo deck — just a powered tractor with a hitch. Trailers
17
+ * are separate scene components and are linked by data binding (a
18
+ * `currentTrailer` data field) rather than scene-tree containment, since
19
+ * trailers swap dynamically and don't share the tractor's transform.
20
+ *
21
+ * Default depth is shorter than a payload AGV — there's no operation surface
22
+ * to align to. ~600mm is enough for the motor housing + control panel.
23
+ */
24
+ export default class Tugger extends Base {
25
+ static legends: Record<string, LegendBinding>;
26
+ static placement: PlacementArchetype;
27
+ static align: Alignment;
28
+ static defaultDepth: number;
29
+ get nature(): ComponentNature;
30
+ get anchors(): never[];
31
+ /**
32
+ * 2D — render() draws the rounded body silhouette (auto-filled with
33
+ * bodyColor); postrender() adds the hitch arm + LiDAR + status lamp.
34
+ */
35
+ render(ctx: CanvasRenderingContext2D): void;
36
+ postrender(ctx: CanvasRenderingContext2D): void;
37
+ get fillStyle(): string;
38
+ buildRealObject(): RealObject | undefined;
39
+ }
40
+ export {};