@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.
- package/CHANGELOG.md +26 -0
- package/README.md +55 -0
- package/dist/agv-3d.d.ts +7 -0
- package/dist/agv-3d.js +233 -0
- package/dist/agv-3d.js.map +1 -0
- package/dist/agv.d.ts +57 -0
- package/dist/agv.js +171 -0
- package/dist/agv.js.map +1 -0
- package/dist/forklift-3d.d.ts +15 -0
- package/dist/forklift-3d.js +518 -0
- package/dist/forklift-3d.js.map +1 -0
- package/dist/forklift.d.ts +58 -0
- package/dist/forklift.js +163 -0
- package/dist/forklift.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/index.d.ts +47 -0
- package/dist/templates/index.js +73 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/tugger-3d.d.ts +7 -0
- package/dist/tugger-3d.js +140 -0
- package/dist/tugger-3d.js.map +1 -0
- package/dist/tugger.d.ts +40 -0
- package/dist/tugger.js +135 -0
- package/dist/tugger.js.map +1 -0
- package/dist/worker-3d.d.ts +7 -0
- package/dist/worker-3d.js +199 -0
- package/dist/worker-3d.js.map +1 -0
- package/dist/worker.d.ts +44 -0
- package/dist/worker.js +130 -0
- package/dist/worker.js.map +1 -0
- package/icons/agv.png +0 -0
- package/icons/forklift.png +0 -0
- package/icons/tugger.png +0 -0
- package/icons/worker.png +0 -0
- package/package.json +44 -0
- package/src/agv-3d.ts +283 -0
- package/src/agv.ts +207 -0
- package/src/forklift-3d.ts +591 -0
- package/src/forklift.ts +200 -0
- package/src/index.ts +14 -0
- package/src/templates/index.ts +73 -0
- package/src/tugger-3d.ts +169 -0
- package/src/tugger.ts +169 -0
- package/src/worker-3d.ts +232 -0
- package/src/worker.ts +164 -0
- package/things-scene.config.js +5 -0
- package/translations/en.json +9 -0
- package/translations/ja.json +9 -0
- package/translations/ko.json +9 -0
- package/translations/ms.json +9 -0
- package/translations/zh.json +9 -0
- package/tsconfig.json +23 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/worker-3d.ts
ADDED
|
@@ -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
|
+
}
|
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
|
+
}
|