@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,232 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * Worker 3D — a stylized humanoid figure built from primitives.
5
+ *
6
+ * LO-POLY but recognizably industrial-worker. The signature parts:
7
+ *
8
+ * - boots (two flat-top boxes — workboots, not sneakers)
9
+ * - legs (two cylinders, slight stance — not perfectly parallel)
10
+ * - belt with tool pouch
11
+ * - torso + hi-vis vest (cylinder, vest color from legend)
12
+ * - reflective stripes on vest (two horizontal bands — ANSI 107 hi-vis)
13
+ * - shoulders + slight A-pose arms (arms angled slightly away from body)
14
+ * - hands (small spheres at arm ends)
15
+ * - badge / nametag hint on chest
16
+ * - neck (small cylinder between torso and head)
17
+ * - head (skin-tone sphere, smaller than minimal anatomy)
18
+ * - full helmet (not just top hemisphere — covers the whole upper head)
19
+ * - chinstrap hint
20
+ *
21
+ * Heights are proportioned against `depth` (= worker height ~1700mm).
22
+ */
23
+
24
+ import * as THREE from 'three'
25
+ import { RealObjectGroup } from '@hatiolab/things-scene'
26
+
27
+ const SKIN_COLOR = 0xc89878
28
+ const PANTS_COLOR = 0x2a3a55
29
+ const BOOT_COLOR = 0x141414
30
+ const BELT_COLOR = 0x1a1a1a
31
+ const POUCH_COLOR = 0x2c1810
32
+ const REFLECTIVE_COLOR = 0xeaeaea
33
+ const BADGE_COLOR = 0xeeeeee
34
+
35
+ export class Worker3D extends RealObjectGroup {
36
+ build() {
37
+ super.build()
38
+
39
+ const { width, height, depth = 1700 } = this.component.state
40
+ const vestColor = (this.component.state.vestColor as string) || '#FFD700'
41
+ const helmetEmissive = (this.component.state.helmetEmissive as string) || '#222222'
42
+ const status = this.component.state.status
43
+ const helmetIntensity = status && status !== 'idle' ? 1.2 : 0.0
44
+
45
+ // Body part heights (proportional to total height = depth)
46
+ const bootH = depth * 0.04
47
+ const legH = depth * 0.42
48
+ const beltH = depth * 0.025
49
+ const torsoH = depth * 0.30
50
+ const neckH = depth * 0.025
51
+ const headR = depth * 0.06
52
+ const helmetR = headR * 1.10
53
+ const helmetH = headR * 1.4
54
+
55
+ let yCursor = -depth / 2
56
+
57
+ // ── Boots (two flat-top boxes — work boots) ──────────────────────
58
+ const bootW = depth * 0.06
59
+ const bootD = depth * 0.13
60
+ const bootMaterial = new THREE.MeshStandardMaterial({ color: BOOT_COLOR, roughness: 0.85 })
61
+ for (const xSign of [-1, 1]) {
62
+ const bootGeo = new THREE.BoxGeometry(bootW, bootH, bootD)
63
+ const bootMesh = new THREE.Mesh(bootGeo, bootMaterial)
64
+ bootMesh.position.set(xSign * bootW * 0.7, yCursor + bootH / 2, bootD * 0.10)
65
+ bootMesh.castShadow = true
66
+ this.object3d.add(bootMesh)
67
+ }
68
+ yCursor += bootH
69
+
70
+ // ── Legs (two cylinders, slight stance) ──────────────────────────
71
+ const legR = depth * 0.038
72
+ const legSpacing = legR * 1.3
73
+ const pantsMaterial = new THREE.MeshStandardMaterial({ color: PANTS_COLOR, roughness: 0.7 })
74
+ for (const xSign of [-1, 1]) {
75
+ const legGeo = new THREE.CylinderGeometry(legR, legR * 1.05, legH, 20)
76
+ const legMesh = new THREE.Mesh(legGeo, pantsMaterial)
77
+ legMesh.position.set(xSign * legSpacing, yCursor + legH / 2, 0)
78
+ legMesh.castShadow = true
79
+ this.object3d.add(legMesh)
80
+ }
81
+ yCursor += legH
82
+
83
+ // ── Belt + tool pouch ────────────────────────────────────────────
84
+ const torsoR = depth * 0.085
85
+ const beltGeo = new THREE.CylinderGeometry(torsoR * 1.05, torsoR * 1.05, beltH, 24)
86
+ const beltMaterial = new THREE.MeshStandardMaterial({ color: BELT_COLOR, roughness: 0.6 })
87
+ const beltMesh = new THREE.Mesh(beltGeo, beltMaterial)
88
+ beltMesh.position.set(0, yCursor + beltH / 2, 0)
89
+ this.object3d.add(beltMesh)
90
+
91
+ // Tool pouch on the right side
92
+ const pouchW = torsoR * 0.7
93
+ const pouchH = beltH * 2.5
94
+ const pouchD = torsoR * 0.5
95
+ const pouchGeo = new THREE.BoxGeometry(pouchW, pouchH, pouchD)
96
+ const pouchMesh = new THREE.Mesh(
97
+ pouchGeo,
98
+ new THREE.MeshStandardMaterial({ color: POUCH_COLOR, roughness: 0.8 })
99
+ )
100
+ pouchMesh.position.set(torsoR * 1.0, yCursor + beltH / 2 - pouchH * 0.2, 0)
101
+ this.object3d.add(pouchMesh)
102
+ yCursor += beltH
103
+
104
+ // ── Torso + hi-vis vest ──────────────────────────────────────────
105
+ const vestMaterial = new THREE.MeshStandardMaterial({
106
+ color: vestColor,
107
+ metalness: 0.05,
108
+ roughness: 0.6
109
+ })
110
+ const torsoGeo = new THREE.CylinderGeometry(torsoR, torsoR * 0.92, torsoH, 24)
111
+ const torsoMesh = new THREE.Mesh(torsoGeo, vestMaterial)
112
+ torsoMesh.position.set(0, yCursor + torsoH / 2, 0)
113
+ torsoMesh.castShadow = true
114
+ this.object3d.add(torsoMesh)
115
+
116
+ // Reflective stripes on vest (ANSI 107 — two horizontal bands)
117
+ const stripeMaterial = new THREE.MeshStandardMaterial({
118
+ color: REFLECTIVE_COLOR,
119
+ metalness: 0.4,
120
+ roughness: 0.2
121
+ })
122
+ const stripeH = torsoH * 0.07
123
+ for (const yFrac of [0.30, 0.55]) {
124
+ const stripeGeo = new THREE.CylinderGeometry(torsoR * 1.005, torsoR * 0.93, stripeH, 24)
125
+ const stripeMesh = new THREE.Mesh(stripeGeo, stripeMaterial)
126
+ stripeMesh.position.set(0, yCursor + torsoH * yFrac, 0)
127
+ this.object3d.add(stripeMesh)
128
+ }
129
+
130
+ // Badge / nametag on chest
131
+ const badgeGeo = new THREE.BoxGeometry(torsoR * 0.55, torsoR * 0.35, torsoR * 0.05)
132
+ const badgeMesh = new THREE.Mesh(
133
+ badgeGeo,
134
+ new THREE.MeshStandardMaterial({ color: BADGE_COLOR, roughness: 0.5 })
135
+ )
136
+ badgeMesh.position.set(-torsoR * 0.45, yCursor + torsoH * 0.78, torsoR * 0.95)
137
+ this.object3d.add(badgeMesh)
138
+
139
+ // ── Arms + hands (slight A-pose, hands at sides) ─────────────────
140
+ const skinMaterial = new THREE.MeshStandardMaterial({ color: SKIN_COLOR, roughness: 0.7 })
141
+ const armR = depth * 0.026
142
+ const armH = torsoH * 1.05
143
+ // Arms angled outward by ~10 degrees
144
+ const armAngle = Math.PI * 0.05
145
+ for (const xSign of [-1, 1]) {
146
+ const armGeo = new THREE.CylinderGeometry(armR, armR, armH, 16)
147
+ armGeo.rotateZ(xSign * -armAngle)
148
+ // After rotation, the arm's center is offset slightly outward and down
149
+ const armMesh = new THREE.Mesh(armGeo, skinMaterial)
150
+ const armCx = xSign * (torsoR + armR + armH * 0.5 * Math.sin(armAngle))
151
+ const armCy = yCursor + torsoH * 0.85 - armH / 2
152
+ armMesh.position.set(armCx, armCy, 0)
153
+ armMesh.castShadow = true
154
+ this.object3d.add(armMesh)
155
+
156
+ // Hand (small sphere at arm end)
157
+ const handR = armR * 1.2
158
+ const handGeo = new THREE.SphereGeometry(handR, 24, 8)
159
+ const handMesh = new THREE.Mesh(handGeo, skinMaterial)
160
+ const handCx = armCx + xSign * (armH / 2) * Math.sin(armAngle)
161
+ const handCy = armCy - armH / 2 - handR * 0.5
162
+ handMesh.position.set(handCx, handCy, 0)
163
+ this.object3d.add(handMesh)
164
+ }
165
+ yCursor += torsoH
166
+
167
+ // ── Neck ─────────────────────────────────────────────────────────
168
+ const neckR = headR * 0.55
169
+ const neckGeo = new THREE.CylinderGeometry(neckR, neckR * 1.15, neckH, 16)
170
+ const neckMesh = new THREE.Mesh(neckGeo, skinMaterial)
171
+ neckMesh.position.set(0, yCursor + neckH / 2, 0)
172
+ this.object3d.add(neckMesh)
173
+ yCursor += neckH
174
+
175
+ // ── Head ─────────────────────────────────────────────────────────
176
+ const headGeo = new THREE.SphereGeometry(headR, 20, 12)
177
+ const headMesh = new THREE.Mesh(headGeo, skinMaterial)
178
+ headMesh.position.set(0, yCursor + headR, 0)
179
+ headMesh.castShadow = true
180
+ this.object3d.add(headMesh)
181
+
182
+ // ── Helmet (full hard hat — top sphere + brim) ───────────────────
183
+ // Top: half-sphere covering upper head (3/5 of sphere)
184
+ const helmetMaterial = new THREE.MeshStandardMaterial({
185
+ color: vestColor, // matches vest base
186
+ emissive: helmetEmissive,
187
+ emissiveIntensity: helmetIntensity,
188
+ metalness: 0.1,
189
+ roughness: 0.45
190
+ })
191
+ const helmetTopGeo = new THREE.SphereGeometry(helmetR, 24, 16, 0, Math.PI * 2, 0, Math.PI * 0.6)
192
+ const helmetTopMesh = new THREE.Mesh(helmetTopGeo, helmetMaterial)
193
+ helmetTopMesh.position.set(0, yCursor + headR * 0.7, 0)
194
+ helmetTopMesh.castShadow = true
195
+ this.object3d.add(helmetTopMesh)
196
+
197
+ // Brim: thin disk in front
198
+ const brimGeo = new THREE.CylinderGeometry(helmetR * 1.05, helmetR * 1.05, helmetR * 0.12, 16, 1, false, -Math.PI * 0.6, Math.PI * 1.2)
199
+ const brimMesh = new THREE.Mesh(brimGeo, helmetMaterial)
200
+ brimMesh.position.set(0, yCursor + headR * 0.85, headR * 0.1)
201
+ this.object3d.add(brimMesh)
202
+
203
+ // Chinstrap hint (thin ring around lower head)
204
+ const strapGeo = new THREE.TorusGeometry(headR * 0.95, headR * 0.03, 6, 16)
205
+ strapGeo.rotateX(Math.PI / 2)
206
+ const strapMesh = new THREE.Mesh(
207
+ strapGeo,
208
+ new THREE.MeshStandardMaterial({ color: BELT_COLOR, roughness: 0.6 })
209
+ )
210
+ strapMesh.position.set(0, yCursor + headR * 0.6, 0)
211
+ this.object3d.add(strapMesh)
212
+ }
213
+
214
+ updateDimension() {}
215
+
216
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
217
+ if (
218
+ 'status' in after ||
219
+ 'vestColor' in after ||
220
+ 'helmetEmissive' in after ||
221
+ 'width' in after ||
222
+ 'height' in after ||
223
+ 'depth' in after
224
+ ) {
225
+ this.update()
226
+ return
227
+ }
228
+ super.onchange(after, before)
229
+ }
230
+
231
+ updateAlpha() {}
232
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,164 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ import { Component, ComponentNature, RealObject, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene'
5
+ import {
6
+ Legendable,
7
+ Placeable,
8
+ type Alignment,
9
+ type Heights,
10
+ type LegendBinding,
11
+ type PlacementArchetype
12
+ } from '@operato/scene-base'
13
+
14
+ import { Worker3D } from './worker-3d.js'
15
+
16
+ /**
17
+ * Worker status — what a human operator is currently doing.
18
+ *
19
+ * - `idle` — present at station but not actively working
20
+ * - `walking` — moving between stations
21
+ * - `working` — actively performing a task
22
+ * - `alarm` — emergency / call-for-help (e.g. line-stop pull-cord)
23
+ *
24
+ * Notably no `loaded` or `running` — workers don't carry the same kind of
25
+ * payload semantics as a forklift, and "running" is ambiguous for a person.
26
+ */
27
+ export type WorkerStatus = 'idle' | 'walking' | 'working' | 'alarm'
28
+
29
+ /**
30
+ * Vest color — the hi-vis safety vest is the most visible status indicator
31
+ * for human workers in a smart factory floor view.
32
+ */
33
+ const VEST_LEGEND = {
34
+ idle: '#FFD700', // standard hi-vis yellow
35
+ walking: '#FFD700',
36
+ working: '#FF8C00', // orange tint (busy)
37
+ alarm: '#FF1744', // bright red (emergency)
38
+ default: '#FFD700'
39
+ }
40
+
41
+ /** Helmet emissive — small accent indicator visible from above (camera view). */
42
+ const HELMET_EMISSIVE_LEGEND = {
43
+ idle: '#222222',
44
+ walking: '#44ff44',
45
+ working: '#44aaff',
46
+ alarm: '#ff3333',
47
+ default: '#222222'
48
+ }
49
+
50
+ const NATURE: ComponentNature = {
51
+ mutable: false,
52
+ resizable: true,
53
+ rotatable: true,
54
+ properties: [
55
+ {
56
+ type: 'select',
57
+ label: 'status',
58
+ name: 'status',
59
+ property: {
60
+ options: [
61
+ { display: 'Idle', value: 'idle' },
62
+ { display: 'Walking', value: 'walking' },
63
+ { display: 'Working', value: 'working' },
64
+ { display: 'Alarm', value: 'alarm' }
65
+ ]
66
+ }
67
+ },
68
+ {
69
+ type: 'string',
70
+ label: 'name',
71
+ name: 'workerName'
72
+ }
73
+ ],
74
+ help: 'scene/component/worker'
75
+ }
76
+
77
+ const Base = Legendable(Placeable(RectPath(Shape))) as unknown as typeof Component
78
+
79
+ @sceneComponent('worker')
80
+ export default class Worker extends Base {
81
+ static legends: Record<string, LegendBinding> = {
82
+ vestColor: { from: 'status', legend: VEST_LEGEND },
83
+ helmetEmissive: { from: 'status', legend: HELMET_EMISSIVE_LEGEND }
84
+ }
85
+
86
+ /**
87
+ * Worker stands on the floor — `floor` archetype with `bottom` alignment.
88
+ * Default depth is ~1700mm (typical adult height); the head reaches just
89
+ * above the operation surface, which makes a worker visually adjacent to a
90
+ * conveyor in a side view but clearly above it in a front view.
91
+ *
92
+ * Note this is human height, not "operation - floor" — the worker is taller
93
+ * than the conveyor by design. Use the explicit number form rather than the
94
+ * heights-derived function form.
95
+ */
96
+ static placement: PlacementArchetype = 'floor'
97
+ static align: Alignment = 'bottom'
98
+ static defaultDepth = 1700
99
+
100
+ get nature() {
101
+ return NATURE
102
+ }
103
+
104
+ get anchors() {
105
+ return []
106
+ }
107
+
108
+ /**
109
+ * 2D — top-down view of a person from above: shoulders / vest as the main
110
+ * filled silhouette (auto-filled with vestColor), helmet circle drawn over
111
+ * it in postrender(). The helmet sits at the *facing* direction (top of
112
+ * the bounds), so the orientation is readable.
113
+ */
114
+ render(ctx: CanvasRenderingContext2D) {
115
+ const { width, height, left, top } = this.state
116
+ // Shoulders: a rounded rect, wider than tall (chest seen from above)
117
+ const shoulderH = height * 0.55
118
+ const shoulderY = top + height * 0.30
119
+ const radius = Math.min(width, shoulderH) * 0.30
120
+ ctx.beginPath()
121
+ ctx.roundRect(left, shoulderY, width, shoulderH, radius)
122
+ }
123
+
124
+ postrender(ctx: CanvasRenderingContext2D) {
125
+ super.postrender?.(ctx)
126
+
127
+ const { width, height, left, top } = this.state
128
+ const cx = left + width / 2
129
+ const helmetEmissive = (this.state.helmetEmissive as string) || '#222'
130
+
131
+ ctx.save()
132
+
133
+ // Reflective stripes across the vest (two horizontal bands)
134
+ ctx.fillStyle = '#eaeaea'
135
+ ctx.fillRect(left + width * 0.08, top + height * 0.45, width * 0.84, height * 0.04)
136
+ ctx.fillRect(left + width * 0.08, top + height * 0.65, width * 0.84, height * 0.04)
137
+
138
+ // Helmet circle at the top (facing direction)
139
+ const helmetR = Math.min(width, height * 0.4) * 0.45
140
+ ctx.fillStyle = (this.state.vestColor as string) || '#FFD700'
141
+ ctx.strokeStyle = '#222'
142
+ ctx.lineWidth = 1
143
+ ctx.beginPath()
144
+ ctx.ellipse(cx, top + helmetR + height * 0.02, helmetR, helmetR, 0, 0, Math.PI * 2)
145
+ ctx.fill()
146
+ ctx.stroke()
147
+
148
+ // Emissive helmet status accent (small dot on helmet)
149
+ ctx.fillStyle = helmetEmissive
150
+ ctx.beginPath()
151
+ ctx.ellipse(cx, top + helmetR + height * 0.02, helmetR * 0.4, helmetR * 0.4, 0, 0, Math.PI * 2)
152
+ ctx.fill()
153
+
154
+ ctx.restore()
155
+ }
156
+
157
+ get fillStyle() {
158
+ return (this.state.vestColor as string) || '#FFD700'
159
+ }
160
+
161
+ buildRealObject(): RealObject | undefined {
162
+ return new Worker3D(this as any)
163
+ }
164
+ }
@@ -0,0 +1,5 @@
1
+ import templates from './dist/templates/index.js'
2
+
3
+ export default {
4
+ templates
5
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "component.forklift": "forklift",
3
+ "component.worker": "worker",
4
+ "component.agv": "AGV",
5
+ "component.tugger": "tugger",
6
+ "label.fork-height": "Fork Height",
7
+ "label.battery": "Battery",
8
+ "label.name": "Name"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "component.forklift": "フォークリフト",
3
+ "component.worker": "作業者",
4
+ "component.agv": "AGV",
5
+ "component.tugger": "タガー",
6
+ "label.fork-height": "フォーク高さ",
7
+ "label.battery": "バッテリー",
8
+ "label.name": "名前"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "component.forklift": "지게차",
3
+ "component.worker": "작업자",
4
+ "component.agv": "AGV",
5
+ "component.tugger": "터거",
6
+ "label.fork-height": "포크 높이",
7
+ "label.battery": "배터리",
8
+ "label.name": "이름"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "component.forklift": "forklift",
3
+ "component.worker": "pekerja",
4
+ "component.agv": "AGV",
5
+ "component.tugger": "tugger",
6
+ "label.fork-height": "Tinggi Garpu",
7
+ "label.battery": "Bateri",
8
+ "label.name": "Nama"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "component.forklift": "叉车",
3
+ "component.worker": "工人",
4
+ "component.agv": "AGV",
5
+ "component.tugger": "牵引车",
6
+ "label.fork-height": "叉高",
7
+ "label.battery": "电池",
8
+ "label.name": "名称"
9
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "noEmitOnError": true,
7
+ "lib": ["es2022", "dom"],
8
+ "strict": true,
9
+ "esModuleInterop": false,
10
+ "allowJs": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "experimentalDecorators": true,
13
+ "importHelpers": true,
14
+ "outDir": "dist",
15
+ "sourceMap": true,
16
+ "inlineSources": true,
17
+ "rootDir": "src",
18
+ "declaration": true,
19
+ "incremental": true,
20
+ "skipLibCheck": true
21
+ },
22
+ "include": ["**/*.ts", "*.d.ts"]
23
+ }