@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/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@operato/scene-transport",
|
|
3
|
+
"description": "Transport-domain components for things-scene (smart factory / logistics) — forklift, worker, AGV.",
|
|
4
|
+
"author": "heartyoh",
|
|
5
|
+
"version": "10.0.0-beta.22",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.js",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"things-scene": true,
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"@operato:registry": "https://registry.npmjs.org"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/things-scene/operato-scene.git",
|
|
18
|
+
"directory": "packages/transport"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"prepublishOnly": "tsc",
|
|
23
|
+
"lint": "eslint src/ && prettier \"src/**/*.ts\" --check",
|
|
24
|
+
"format": "eslint src/ --fix && prettier \"src/**/*.ts\" --write"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@hatiolab/things-scene": "^10.0.0-beta.1",
|
|
28
|
+
"@operato/scene-base": "^10.0.0-beta.22",
|
|
29
|
+
"three": "^0.183.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@hatiolab/prettier-config": "^1.0.0",
|
|
33
|
+
"@types/three": "^0.183.0",
|
|
34
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
35
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
36
|
+
"eslint": "^9.18.0",
|
|
37
|
+
"eslint-config-prettier": "^10.0.1",
|
|
38
|
+
"prettier": "^3.2.5",
|
|
39
|
+
"tslib": "^2.3.1",
|
|
40
|
+
"typescript": "^5.0.4"
|
|
41
|
+
},
|
|
42
|
+
"prettier": "@hatiolab/prettier-config",
|
|
43
|
+
"gitHead": "f48e52f4f5fdc30ec06af9da7cf253f6e29cfb0e"
|
|
44
|
+
}
|
package/src/agv-3d.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Agv 3D — payload (Kiva-style) automated guided vehicle.
|
|
5
|
+
*
|
|
6
|
+
* LO-POLY but with **rounded volume** — the Kiva AGV's signature is a
|
|
7
|
+
* pill-shaped, low-slung chassis with rounded corners, not a flat box.
|
|
8
|
+
*
|
|
9
|
+
* Structure:
|
|
10
|
+
* - rounded-box chassis (volumetric, with bevels — the visual identity)
|
|
11
|
+
* - top deck with raised lift pad at center (Kiva-style under-shelf lift)
|
|
12
|
+
* - LED strip running around the perimeter
|
|
13
|
+
* - safety bumpers (front + rear, hi-vis with black corners)
|
|
14
|
+
* - LiDAR sensor: cylindrical body + transparent dome top
|
|
15
|
+
* - 4 wheel covers (fender bumps integrated into chassis sides)
|
|
16
|
+
* - charging contacts at rear
|
|
17
|
+
* - corner indicator lamps on deck
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as THREE from 'three'
|
|
21
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
22
|
+
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'
|
|
23
|
+
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
24
|
+
|
|
25
|
+
const CHASSIS_COLOR_DARK = 0x444444
|
|
26
|
+
const DECK_COLOR = 0x666677
|
|
27
|
+
const LIFT_PAD_COLOR = 0x4a4a55
|
|
28
|
+
const TIRE_COLOR = 0x202020
|
|
29
|
+
const WHEEL_COVER_COLOR = 0x3a3a3a
|
|
30
|
+
const LIDAR_BODY = 0x222233
|
|
31
|
+
const LIDAR_DOME = 0x445566
|
|
32
|
+
const BUMPER_COLOR = 0xeeaa00
|
|
33
|
+
const BUMPER_BLACK = 0x111111
|
|
34
|
+
const CHARGE_COPPER = 0xc77a00
|
|
35
|
+
|
|
36
|
+
export class Agv3D extends RealObjectGroup {
|
|
37
|
+
build() {
|
|
38
|
+
super.build()
|
|
39
|
+
|
|
40
|
+
const { width, height, depth = 0 } = this.component.state
|
|
41
|
+
const bodyColor = (this.component.state.bodyColor as string) || '#999'
|
|
42
|
+
const emissiveColor = (this.component.state.lampEmissive as string) || '#222222'
|
|
43
|
+
const status = this.component.state.status
|
|
44
|
+
const lampIntensity = status && status !== 'idle' ? 1.5 : 0.2
|
|
45
|
+
|
|
46
|
+
const chassisH = depth * 0.85
|
|
47
|
+
const deckH = depth * 0.05
|
|
48
|
+
const wheelR = chassisH * 0.30
|
|
49
|
+
const baseY = -depth / 2
|
|
50
|
+
|
|
51
|
+
// ── Rounded chassis (volumetric — the Kiva AGV signature) ────────
|
|
52
|
+
const chassisRadius = Math.min(width, height) * 0.10
|
|
53
|
+
const chassisGeo = new RoundedBoxGeometry(width * 0.95, chassisH, height * 0.95, 4, chassisRadius)
|
|
54
|
+
const chassisMesh = new THREE.Mesh(
|
|
55
|
+
chassisGeo,
|
|
56
|
+
new THREE.MeshStandardMaterial({
|
|
57
|
+
color: CHASSIS_COLOR_DARK,
|
|
58
|
+
metalness: 0.4,
|
|
59
|
+
roughness: 0.5
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
chassisMesh.position.set(0, baseY + chassisH / 2, 0)
|
|
63
|
+
chassisMesh.castShadow = true
|
|
64
|
+
chassisMesh.receiveShadow = true
|
|
65
|
+
this.object3d.add(chassisMesh)
|
|
66
|
+
|
|
67
|
+
// ── Top deck (rounded slab) ───────────────────────────────────────
|
|
68
|
+
const deckGeo = new RoundedBoxGeometry(width * 0.95, deckH, height * 0.95, 2, chassisRadius * 0.5)
|
|
69
|
+
const deckMesh = new THREE.Mesh(
|
|
70
|
+
deckGeo,
|
|
71
|
+
new THREE.MeshStandardMaterial({ color: DECK_COLOR, metalness: 0.5, roughness: 0.4 })
|
|
72
|
+
)
|
|
73
|
+
deckMesh.position.set(0, baseY + chassisH + deckH / 2, 0)
|
|
74
|
+
deckMesh.castShadow = true
|
|
75
|
+
deckMesh.receiveShadow = true
|
|
76
|
+
this.object3d.add(deckMesh)
|
|
77
|
+
|
|
78
|
+
// ── Lift mechanism (round pad — Kiva-style under-shelf lift) ─────
|
|
79
|
+
const liftR = Math.min(width, height) * 0.30
|
|
80
|
+
const liftH = deckH * 1.5
|
|
81
|
+
const liftGeo = new THREE.CylinderGeometry(liftR, liftR * 1.05, liftH, 24)
|
|
82
|
+
const liftMesh = new THREE.Mesh(
|
|
83
|
+
liftGeo,
|
|
84
|
+
new THREE.MeshStandardMaterial({ color: LIFT_PAD_COLOR, metalness: 0.6, roughness: 0.35 })
|
|
85
|
+
)
|
|
86
|
+
liftMesh.position.set(0, baseY + chassisH + deckH + liftH / 2, 0)
|
|
87
|
+
liftMesh.castShadow = true
|
|
88
|
+
this.object3d.add(liftMesh)
|
|
89
|
+
|
|
90
|
+
// Body color band on lift rim (status hint)
|
|
91
|
+
const tintH = liftH * 0.3
|
|
92
|
+
const tintGeo = new THREE.CylinderGeometry(liftR * 1.03, liftR * 1.03, tintH, 24, 1, true)
|
|
93
|
+
const tintMesh = new THREE.Mesh(
|
|
94
|
+
tintGeo,
|
|
95
|
+
new THREE.MeshStandardMaterial({
|
|
96
|
+
color: bodyColor,
|
|
97
|
+
side: THREE.DoubleSide,
|
|
98
|
+
transparent: true,
|
|
99
|
+
opacity: 0.7,
|
|
100
|
+
metalness: 0.1,
|
|
101
|
+
roughness: 0.6
|
|
102
|
+
})
|
|
103
|
+
)
|
|
104
|
+
tintMesh.position.set(0, baseY + chassisH + deckH + tintH / 2, 0)
|
|
105
|
+
this.object3d.add(tintMesh)
|
|
106
|
+
|
|
107
|
+
// ── LED strip (4 sides, rounded corners follow chassis bevel) ────
|
|
108
|
+
const stripH = chassisH * 0.10
|
|
109
|
+
const stripT = Math.min(width, height) * 0.015
|
|
110
|
+
const stripY = baseY + chassisH * 0.55
|
|
111
|
+
const stripMaterial = new THREE.MeshStandardMaterial({
|
|
112
|
+
color: emissiveColor,
|
|
113
|
+
emissive: emissiveColor,
|
|
114
|
+
emissiveIntensity: lampIntensity,
|
|
115
|
+
metalness: 0.1,
|
|
116
|
+
roughness: 0.4
|
|
117
|
+
})
|
|
118
|
+
for (const zSign of [-1, 1]) {
|
|
119
|
+
const stripGeo = new THREE.BoxGeometry(width * 0.85, stripH, stripT)
|
|
120
|
+
const stripMesh = new THREE.Mesh(stripGeo, stripMaterial)
|
|
121
|
+
stripMesh.position.set(0, stripY, zSign * (height * 0.475 - stripT / 2))
|
|
122
|
+
this.object3d.add(stripMesh)
|
|
123
|
+
}
|
|
124
|
+
for (const xSign of [-1, 1]) {
|
|
125
|
+
const stripGeo = new THREE.BoxGeometry(stripT, stripH, height * 0.85)
|
|
126
|
+
const stripMesh = new THREE.Mesh(stripGeo, stripMaterial)
|
|
127
|
+
stripMesh.position.set(xSign * (width * 0.475 - stripT / 2), stripY, 0)
|
|
128
|
+
this.object3d.add(stripMesh)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Safety bumpers (front + rear) ────────────────────────────────
|
|
132
|
+
const bumperH = chassisH * 0.18
|
|
133
|
+
const bumperT = Math.min(width, height) * 0.025
|
|
134
|
+
const bumperY = baseY + bumperH / 2 + wheelR * 0.5
|
|
135
|
+
const bumperMaterial = new THREE.MeshStandardMaterial({
|
|
136
|
+
color: BUMPER_COLOR,
|
|
137
|
+
metalness: 0.1,
|
|
138
|
+
roughness: 0.6
|
|
139
|
+
})
|
|
140
|
+
const bumperBlackMaterial = new THREE.MeshStandardMaterial({
|
|
141
|
+
color: BUMPER_BLACK,
|
|
142
|
+
metalness: 0.2,
|
|
143
|
+
roughness: 0.7
|
|
144
|
+
})
|
|
145
|
+
for (const zSign of [-1, 1]) {
|
|
146
|
+
const yellow = new RoundedBoxGeometry(width * 0.7, bumperH, bumperT, 2, bumperT * 0.4)
|
|
147
|
+
const yMesh = new THREE.Mesh(yellow, bumperMaterial)
|
|
148
|
+
yMesh.position.set(0, bumperY, zSign * (height * 0.475 + bumperT / 2))
|
|
149
|
+
this.object3d.add(yMesh)
|
|
150
|
+
for (const xSign of [-1, 1]) {
|
|
151
|
+
const black = new RoundedBoxGeometry(width * 0.13, bumperH, bumperT, 2, bumperT * 0.4)
|
|
152
|
+
const bMesh = new THREE.Mesh(black, bumperBlackMaterial)
|
|
153
|
+
bMesh.position.set(xSign * (width * 0.7 / 2 + width * 0.065), bumperY, zSign * (height * 0.475 + bumperT / 2))
|
|
154
|
+
this.object3d.add(bMesh)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── LiDAR sensor (cylinder body + transparent dome) ──────────────
|
|
159
|
+
const lidarR = Math.min(width, height) * 0.10
|
|
160
|
+
const lidarBodyH = lidarR * 0.7
|
|
161
|
+
const lidarBodyGeo = new THREE.CylinderGeometry(lidarR, lidarR * 1.1, lidarBodyH, 16)
|
|
162
|
+
const lidarBodyMesh = new THREE.Mesh(
|
|
163
|
+
lidarBodyGeo,
|
|
164
|
+
new THREE.MeshStandardMaterial({ color: LIDAR_BODY, metalness: 0.6, roughness: 0.4 })
|
|
165
|
+
)
|
|
166
|
+
lidarBodyMesh.position.set(0, baseY + chassisH + lidarBodyH / 2, height * 0.35)
|
|
167
|
+
lidarBodyMesh.castShadow = true
|
|
168
|
+
this.object3d.add(lidarBodyMesh)
|
|
169
|
+
|
|
170
|
+
const domeGeo = new THREE.SphereGeometry(lidarR * 0.95, 24, 16, 0, Math.PI * 2, 0, Math.PI / 2)
|
|
171
|
+
const domeMesh = new THREE.Mesh(
|
|
172
|
+
domeGeo,
|
|
173
|
+
new THREE.MeshStandardMaterial({
|
|
174
|
+
color: LIDAR_DOME,
|
|
175
|
+
metalness: 0.3,
|
|
176
|
+
roughness: 0.2,
|
|
177
|
+
transparent: true,
|
|
178
|
+
opacity: 0.8
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
domeMesh.position.set(0, baseY + chassisH + lidarBodyH, height * 0.35)
|
|
182
|
+
this.object3d.add(domeMesh)
|
|
183
|
+
|
|
184
|
+
// Status hint on LiDAR top
|
|
185
|
+
const hintR = lidarR * 0.25
|
|
186
|
+
const hintMesh = new THREE.Mesh(
|
|
187
|
+
new THREE.SphereGeometry(hintR, 24, 8),
|
|
188
|
+
new THREE.MeshStandardMaterial({
|
|
189
|
+
color: emissiveColor,
|
|
190
|
+
emissive: emissiveColor,
|
|
191
|
+
emissiveIntensity: lampIntensity,
|
|
192
|
+
roughness: 0.3
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
hintMesh.position.set(0, baseY + chassisH + lidarBodyH + lidarR * 0.85, height * 0.35)
|
|
196
|
+
this.object3d.add(hintMesh)
|
|
197
|
+
|
|
198
|
+
// ── Wheels with covers ───────────────────────────────────────────
|
|
199
|
+
const wheelW = width * 0.07
|
|
200
|
+
const tireMaterial = new THREE.MeshStandardMaterial({ color: TIRE_COLOR, roughness: 0.95 })
|
|
201
|
+
const coverMaterial = new THREE.MeshStandardMaterial({
|
|
202
|
+
color: WHEEL_COVER_COLOR,
|
|
203
|
+
metalness: 0.5,
|
|
204
|
+
roughness: 0.6
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const tireGeos: THREE.BufferGeometry[] = []
|
|
208
|
+
for (const xSign of [-1, 1]) {
|
|
209
|
+
for (const zSign of [-1, 1]) {
|
|
210
|
+
const tire = new THREE.CylinderGeometry(wheelR, wheelR, wheelW, 24)
|
|
211
|
+
tire.rotateZ(Math.PI / 2)
|
|
212
|
+
tire.translate(xSign * width * 0.42, baseY + wheelR, zSign * height * 0.32)
|
|
213
|
+
tireGeos.push(tire)
|
|
214
|
+
// Rounded fender cover
|
|
215
|
+
const cover = new RoundedBoxGeometry(wheelW * 1.5, wheelR * 1.4, wheelR * 1.6, 2, wheelR * 0.25)
|
|
216
|
+
cover.translate(xSign * (width * 0.42 + wheelW * 0.4), baseY + wheelR * 1.2, zSign * height * 0.32)
|
|
217
|
+
const coverMesh = new THREE.Mesh(cover, coverMaterial)
|
|
218
|
+
coverMesh.castShadow = true
|
|
219
|
+
this.object3d.add(coverMesh)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const tireMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(tireGeos), tireMaterial)
|
|
223
|
+
tireMesh.castShadow = true
|
|
224
|
+
this.object3d.add(tireMesh)
|
|
225
|
+
|
|
226
|
+
// ── Charging contacts (rear) ─────────────────────────────────────
|
|
227
|
+
const chargeMaterial = new THREE.MeshStandardMaterial({
|
|
228
|
+
color: CHARGE_COPPER,
|
|
229
|
+
metalness: 0.9,
|
|
230
|
+
roughness: 0.2
|
|
231
|
+
})
|
|
232
|
+
const chargeT = wheelR * 0.15
|
|
233
|
+
const chargeH = wheelR * 0.6
|
|
234
|
+
const chargeW = width * 0.08
|
|
235
|
+
for (const xSign of [-1, 1]) {
|
|
236
|
+
const chargeGeo = new RoundedBoxGeometry(chargeW, chargeH, chargeT, 1, chargeT * 0.3)
|
|
237
|
+
const chargeMesh = new THREE.Mesh(chargeGeo, chargeMaterial)
|
|
238
|
+
chargeMesh.position.set(xSign * width * 0.10, baseY + chargeH / 2 + wheelR * 0.2, -height * 0.475 - chargeT / 2)
|
|
239
|
+
this.object3d.add(chargeMesh)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Corner indicator lamps ───────────────────────────────────────
|
|
243
|
+
const cornerR = Math.min(width, height) * 0.022
|
|
244
|
+
for (const xSign of [-1, 1]) {
|
|
245
|
+
for (const zSign of [-1, 1]) {
|
|
246
|
+
const cornerMesh = new THREE.Mesh(
|
|
247
|
+
new THREE.SphereGeometry(cornerR, 24, 6),
|
|
248
|
+
new THREE.MeshStandardMaterial({
|
|
249
|
+
color: emissiveColor,
|
|
250
|
+
emissive: emissiveColor,
|
|
251
|
+
emissiveIntensity: lampIntensity * 0.6,
|
|
252
|
+
roughness: 0.3
|
|
253
|
+
})
|
|
254
|
+
)
|
|
255
|
+
cornerMesh.position.set(
|
|
256
|
+
xSign * (width * 0.42),
|
|
257
|
+
baseY + chassisH + deckH + cornerR * 0.6,
|
|
258
|
+
zSign * (height * 0.42)
|
|
259
|
+
)
|
|
260
|
+
this.object3d.add(cornerMesh)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
updateDimension() {}
|
|
266
|
+
|
|
267
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
268
|
+
if (
|
|
269
|
+
'status' in after ||
|
|
270
|
+
'bodyColor' in after ||
|
|
271
|
+
'lampEmissive' in after ||
|
|
272
|
+
'width' in after ||
|
|
273
|
+
'height' in after ||
|
|
274
|
+
'depth' in after
|
|
275
|
+
) {
|
|
276
|
+
this.update()
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
super.onchange(after, before)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
updateAlpha() {}
|
|
283
|
+
}
|
package/src/agv.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { Component, ComponentNature, Container, RealObject, 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 { Agv3D } from './agv-3d.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Agv status — common to both payload and towing AGVs (kept narrow on purpose).
|
|
18
|
+
*
|
|
19
|
+
* - `idle` — parked, awaiting task
|
|
20
|
+
* - `moving` — driving along a path / executing transport
|
|
21
|
+
* - `charging` — at a charging dock / battery station
|
|
22
|
+
* - `error` — fault / blocked / e-stop
|
|
23
|
+
*
|
|
24
|
+
* Loaded-vs-empty distinction is *not* in the status enum because for a
|
|
25
|
+
* Kiva-style payload AGV it's already obvious from the children (cargo
|
|
26
|
+
* components) parented to it — duplicating that as a status flag would
|
|
27
|
+
* invite drift.
|
|
28
|
+
*/
|
|
29
|
+
export type AgvStatus = 'idle' | 'moving' | 'charging' | 'error'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Body color — neutral industrial gray base, slightly modulated by status.
|
|
33
|
+
* AGVs typically have status-color LED strips rather than full body color
|
|
34
|
+
* change; the body legend stays subtle so the LED strip + lamp do the
|
|
35
|
+
* communicating.
|
|
36
|
+
*/
|
|
37
|
+
const BODY_LEGEND = {
|
|
38
|
+
idle: '#999',
|
|
39
|
+
moving: '#aaa',
|
|
40
|
+
charging: '#aaa',
|
|
41
|
+
error: '#c66',
|
|
42
|
+
default: '#999'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* LED strip emissive — the dominant status indicator for AGVs (typically a
|
|
47
|
+
* color-band running around the chassis perimeter).
|
|
48
|
+
*/
|
|
49
|
+
const LAMP_EMISSIVE_LEGEND = {
|
|
50
|
+
idle: '#222222',
|
|
51
|
+
moving: '#44ff44', // green (operating)
|
|
52
|
+
charging: '#ffaa00', // amber (charging)
|
|
53
|
+
error: '#ff3333', // red
|
|
54
|
+
default: '#222222'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const NATURE: ComponentNature = {
|
|
58
|
+
mutable: false,
|
|
59
|
+
resizable: true,
|
|
60
|
+
rotatable: true,
|
|
61
|
+
properties: [
|
|
62
|
+
{
|
|
63
|
+
type: 'select',
|
|
64
|
+
label: 'status',
|
|
65
|
+
name: 'status',
|
|
66
|
+
property: {
|
|
67
|
+
options: [
|
|
68
|
+
{ display: 'Idle', value: 'idle' },
|
|
69
|
+
{ display: 'Moving', value: 'moving' },
|
|
70
|
+
{ display: 'Charging', value: 'charging' },
|
|
71
|
+
{ display: 'Error', value: 'error' }
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: 'number',
|
|
77
|
+
label: 'battery',
|
|
78
|
+
name: 'battery',
|
|
79
|
+
placeholder: '0..1'
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
help: 'scene/component/agv'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const Base = Legendable(Placeable(Container)) as unknown as typeof Component
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Agv — payload (unit-load) automated guided vehicle. The Kiva-style flat-deck
|
|
89
|
+
* AGV that drives under or carries cargo to/from operation surfaces.
|
|
90
|
+
*
|
|
91
|
+
* **Container-based for cargo containment.** A payload Agv has a flat top
|
|
92
|
+
* deck whose surface is at the scene's operation height. Boxes, parcels,
|
|
93
|
+
* loaded pallets (anything with `placement: 'operation'`) can be added as
|
|
94
|
+
* children — when they are, their natural archetype-derived zPos puts them
|
|
95
|
+
* exactly on the AGV's deck (since AGV depth = operation - floor).
|
|
96
|
+
*
|
|
97
|
+
* For the towing variant (no cargo deck, pulls trailers behind), see Tugger.
|
|
98
|
+
*/
|
|
99
|
+
@sceneComponent('agv')
|
|
100
|
+
export default class Agv extends Base {
|
|
101
|
+
static legends: Record<string, LegendBinding> = {
|
|
102
|
+
bodyColor: { from: 'status', legend: BODY_LEGEND },
|
|
103
|
+
lampEmissive: { from: 'status', legend: LAMP_EMISSIVE_LEGEND }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* AGV sits on its wheels — `floor` archetype. Default depth = operation,
|
|
108
|
+
* so the top deck lands at the scene's operation height. This is the
|
|
109
|
+
* design point of payload AGVs: the deck height matches conveyor belt
|
|
110
|
+
* height, equipment ports, and forklift fork height — cargo transfers
|
|
111
|
+
* across all of them at the same level.
|
|
112
|
+
*/
|
|
113
|
+
static placement: PlacementArchetype = 'floor'
|
|
114
|
+
static align: Alignment = 'bottom'
|
|
115
|
+
static defaultDepth = (h: Heights) => h.operation - h.floor
|
|
116
|
+
|
|
117
|
+
get nature() {
|
|
118
|
+
return NATURE
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get anchors() {
|
|
122
|
+
return []
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Accept logistics packages (placement='operation') as deck cargo. */
|
|
126
|
+
containable(component: Component) {
|
|
127
|
+
const archetype = (component.constructor as any).placement
|
|
128
|
+
if (archetype === 'operation') return true
|
|
129
|
+
return component.isDescendible(this as any)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 2D — render() sets up the rounded chassis path; the framework fills it
|
|
134
|
+
* with `fillStyle` (= bodyColor from Legendable) and strokes with
|
|
135
|
+
* `strokeStyle`. Additional top-down details (bumpers, LiDAR, lift pad,
|
|
136
|
+
* direction triangle) are drawn in `postrender()`.
|
|
137
|
+
*/
|
|
138
|
+
render(ctx: CanvasRenderingContext2D) {
|
|
139
|
+
const { width, height, left, top } = this.state
|
|
140
|
+
const radius = Math.min(width, height) * 0.12
|
|
141
|
+
ctx.beginPath()
|
|
142
|
+
ctx.roundRect(left, top, width, height, radius)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Top-view accent details — bumpers, lift pad, LiDAR, direction triangle. */
|
|
146
|
+
postrender(ctx: CanvasRenderingContext2D) {
|
|
147
|
+
super.postrender?.(ctx)
|
|
148
|
+
|
|
149
|
+
const { width, height, left, top } = this.state
|
|
150
|
+
const cx = left + width / 2
|
|
151
|
+
const cy = top + height / 2
|
|
152
|
+
const accentColor = (this.state.lampEmissive as string) || '#44ff44'
|
|
153
|
+
|
|
154
|
+
ctx.save()
|
|
155
|
+
|
|
156
|
+
// Hi-vis bumper strips (front + rear)
|
|
157
|
+
const bumperT = Math.min(width, height) * 0.06
|
|
158
|
+
ctx.fillStyle = '#eeaa00'
|
|
159
|
+
ctx.fillRect(left + width * 0.15, top, width * 0.7, bumperT)
|
|
160
|
+
ctx.fillRect(left + width * 0.15, top + height - bumperT, width * 0.7, bumperT)
|
|
161
|
+
ctx.fillStyle = '#111'
|
|
162
|
+
ctx.fillRect(left + width * 0.02, top, width * 0.13, bumperT)
|
|
163
|
+
ctx.fillRect(left + width * 0.85, top, width * 0.13, bumperT)
|
|
164
|
+
ctx.fillRect(left + width * 0.02, top + height - bumperT, width * 0.13, bumperT)
|
|
165
|
+
ctx.fillRect(left + width * 0.85, top + height - bumperT, width * 0.13, bumperT)
|
|
166
|
+
|
|
167
|
+
// Lift pad (center circle — Kiva-style under-shelf lift)
|
|
168
|
+
const padR = Math.min(width, height) * 0.22
|
|
169
|
+
ctx.fillStyle = '#4a4a55'
|
|
170
|
+
ctx.strokeStyle = '#222'
|
|
171
|
+
ctx.lineWidth = 1
|
|
172
|
+
ctx.beginPath()
|
|
173
|
+
ctx.ellipse(cx, cy, padR, padR, 0, 0, Math.PI * 2)
|
|
174
|
+
ctx.fill()
|
|
175
|
+
ctx.stroke()
|
|
176
|
+
|
|
177
|
+
// LiDAR sensor (small filled circle near front)
|
|
178
|
+
const lidarR = Math.min(width, height) * 0.07
|
|
179
|
+
ctx.fillStyle = '#222233'
|
|
180
|
+
ctx.beginPath()
|
|
181
|
+
ctx.ellipse(cx, top + height * 0.20, lidarR, lidarR, 0, 0, Math.PI * 2)
|
|
182
|
+
ctx.fill()
|
|
183
|
+
ctx.stroke()
|
|
184
|
+
// Status hint on LiDAR
|
|
185
|
+
ctx.fillStyle = accentColor
|
|
186
|
+
ctx.beginPath()
|
|
187
|
+
ctx.ellipse(cx, top + height * 0.20, lidarR * 0.4, lidarR * 0.4, 0, 0, Math.PI * 2)
|
|
188
|
+
ctx.fill()
|
|
189
|
+
|
|
190
|
+
// Direction-of-travel triangle
|
|
191
|
+
ctx.fillStyle = accentColor
|
|
192
|
+
ctx.strokeStyle = '#111'
|
|
193
|
+
ctx.beginPath()
|
|
194
|
+
ctx.moveTo(cx, top + height * 0.06)
|
|
195
|
+
ctx.lineTo(cx - width * 0.06, top + height * 0.13)
|
|
196
|
+
ctx.lineTo(cx + width * 0.06, top + height * 0.13)
|
|
197
|
+
ctx.closePath()
|
|
198
|
+
ctx.fill()
|
|
199
|
+
ctx.stroke()
|
|
200
|
+
|
|
201
|
+
ctx.restore()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
buildRealObject(): RealObject | undefined {
|
|
205
|
+
return new Agv3D(this as any)
|
|
206
|
+
}
|
|
207
|
+
}
|