@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
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Forklift 3D — designed from a real forklift's proportions, not stacked from
|
|
5
|
+
* primitive geometry. Procedural lo-poly with deliberate aesthetic choices.
|
|
6
|
+
*
|
|
7
|
+
* Design priorities (in order):
|
|
8
|
+
*
|
|
9
|
+
* 1. **Silhouette reads** — at any camera angle the mast + counterweight +
|
|
10
|
+
* forks combo identifies this as an industrial forklift.
|
|
11
|
+
* 2. **Proportions** — chassis ~35% of envelope, cab ~25%, overhead guard
|
|
12
|
+
* ~30%. Forks extend forward roughly the full chassis length.
|
|
13
|
+
* Counterweight is the heaviest visual mass, set rearward.
|
|
14
|
+
* 3. **Sculpted, not stacked** — the chassis side profile is a single
|
|
15
|
+
* `ExtrudeGeometry` (sloped hood / cab indent / counterweight rise),
|
|
16
|
+
* not three separate boxes glued together. Mast and forks are
|
|
17
|
+
* L-shaped extrusions, not rectangular prisms.
|
|
18
|
+
* 4. **Material palette** — yellow body with subtle shaded variant for
|
|
19
|
+
* shape-reading; dark counterweight; cool gray mast; chrome hydraulic.
|
|
20
|
+
* Status emissive only on the lamp + accent stripe.
|
|
21
|
+
* 5. **Sub-assemblies as groups** — chassis / cab / guard / mast / wheels
|
|
22
|
+
* / accents are organized as scene-graph children. Adjustable mass
|
|
23
|
+
* (mast group, forks) tracks `state.forkHeight` cleanly.
|
|
24
|
+
*
|
|
25
|
+
* Coordinate convention (component-local):
|
|
26
|
+
* - Origin at the geometric center of the component bounds.
|
|
27
|
+
* - X-axis: vehicle width (left/right).
|
|
28
|
+
* - Y-axis: vertical. Floor at `-depth/2`, ceiling at `+depth/2`.
|
|
29
|
+
* - Z-axis: vehicle length, +Z = forward (forks point this way).
|
|
30
|
+
*
|
|
31
|
+
* Defaults (with the scene-unit StandardHeights — operation=50, ceiling=200):
|
|
32
|
+
* - depth = 200 (full envelope; mast collapsed reaches ~80%, mast up to top)
|
|
33
|
+
* - wheelR = depth * 0.07 (~14)
|
|
34
|
+
* - chassis section length ~0.65 of width footprint
|
|
35
|
+
* - cab seat height ~operation level (matches the design intuition that
|
|
36
|
+
* the operator's seat sits at the standard operation surface)
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import * as THREE from 'three'
|
|
40
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
41
|
+
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'
|
|
42
|
+
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
43
|
+
|
|
44
|
+
// ─── Color palette ─────────────────────────────────────────────────────────
|
|
45
|
+
const COLOR_COUNTERWEIGHT = 0x1c1c22
|
|
46
|
+
const COLOR_MAST = 0x36363f
|
|
47
|
+
const COLOR_FORK = 0x14141a
|
|
48
|
+
const COLOR_TIRE = 0x171719
|
|
49
|
+
const COLOR_RIM = 0x707080
|
|
50
|
+
const COLOR_HUB = 0xb8b8c0
|
|
51
|
+
const COLOR_SEAT = 0x14141c
|
|
52
|
+
const COLOR_STEERING = 0x202028
|
|
53
|
+
const COLOR_GUARD = 0x252530
|
|
54
|
+
const COLOR_HYDRAULIC = 0xc8ccd8
|
|
55
|
+
const COLOR_CHAIN = 0x4a4a55
|
|
56
|
+
const COLOR_WINDOW = 0x88aacc
|
|
57
|
+
const COLOR_TAIL = 0xff2626
|
|
58
|
+
const COLOR_HEADLIGHT = 0xfff7d4
|
|
59
|
+
|
|
60
|
+
export class Forklift3D extends RealObjectGroup {
|
|
61
|
+
/** Local-frame position of the fork-tip top surface — where cargo sits. */
|
|
62
|
+
get cargoMount(): { x: number; y: number; z: number; width: number; depth: number } {
|
|
63
|
+
const { width, height, depth = 200 } = this.component.state
|
|
64
|
+
const raw = (this.component.state.forkHeight as number) ?? 0
|
|
65
|
+
const forkHeight = Math.max(0, Math.min(raw, depth * 0.85))
|
|
66
|
+
const wheelR = depth * 0.07
|
|
67
|
+
const forkH = depth * 0.018
|
|
68
|
+
const forkLen = height * 0.42
|
|
69
|
+
const y = -depth / 2 + wheelR + forkHeight + forkH
|
|
70
|
+
const z = height * 0.42 + forkLen / 2
|
|
71
|
+
return { x: 0, y, z, width: width * 0.4, depth: forkLen }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
build() {
|
|
75
|
+
super.build()
|
|
76
|
+
|
|
77
|
+
// ── State + clamping ─────────────────────────────────────────────
|
|
78
|
+
const { width, height, depth = 200 } = this.component.state
|
|
79
|
+
const bodyColor = (this.component.state.bodyColor as string) || '#ffc107'
|
|
80
|
+
const lampEmissive = (this.component.state.lampEmissive as string) || '#333333'
|
|
81
|
+
const status = this.component.state.status
|
|
82
|
+
const lampIntensity = status && status !== 'idle' ? 1.5 : 0.2
|
|
83
|
+
const forkHeightRaw = (this.component.state.forkHeight as number) ?? 0
|
|
84
|
+
const forkHeight = Math.max(0, Math.min(forkHeightRaw, depth * 0.85))
|
|
85
|
+
|
|
86
|
+
// ── Proportions ──────────────────────────────────────────────────
|
|
87
|
+
const baseY = -depth / 2
|
|
88
|
+
const wheelR = depth * 0.07
|
|
89
|
+
const chassisH = depth * 0.30 // hood height + step up
|
|
90
|
+
const cabH = depth * 0.22
|
|
91
|
+
const guardH = depth * 0.30 // overhead guard above cab
|
|
92
|
+
const cwH = depth * 0.50 // counterweight tall block
|
|
93
|
+
const cwD = height * 0.22 // counterweight depth (along z)
|
|
94
|
+
const chassisLen = height * 0.55 // length along z (vehicle direction)
|
|
95
|
+
const cabFloorY = baseY + wheelR + chassisH * 0.85 // cab seat near operation level
|
|
96
|
+
|
|
97
|
+
// ── Materials ─────────────────────────────────────────────────────
|
|
98
|
+
const bodyMat = new THREE.MeshStandardMaterial({
|
|
99
|
+
color: bodyColor,
|
|
100
|
+
metalness: 0.30,
|
|
101
|
+
roughness: 0.45
|
|
102
|
+
})
|
|
103
|
+
const bodyShadeMat = new THREE.MeshStandardMaterial({
|
|
104
|
+
color: new THREE.Color(bodyColor).multiplyScalar(0.78),
|
|
105
|
+
metalness: 0.28,
|
|
106
|
+
roughness: 0.50
|
|
107
|
+
})
|
|
108
|
+
const cwMat = new THREE.MeshStandardMaterial({
|
|
109
|
+
color: COLOR_COUNTERWEIGHT,
|
|
110
|
+
metalness: 0.55,
|
|
111
|
+
roughness: 0.65
|
|
112
|
+
})
|
|
113
|
+
const mastMat = new THREE.MeshStandardMaterial({
|
|
114
|
+
color: COLOR_MAST,
|
|
115
|
+
metalness: 0.85,
|
|
116
|
+
roughness: 0.30
|
|
117
|
+
})
|
|
118
|
+
const forkMat = new THREE.MeshStandardMaterial({
|
|
119
|
+
color: COLOR_FORK,
|
|
120
|
+
metalness: 0.85,
|
|
121
|
+
roughness: 0.35
|
|
122
|
+
})
|
|
123
|
+
const tireMat = new THREE.MeshStandardMaterial({ color: COLOR_TIRE, roughness: 0.95 })
|
|
124
|
+
const rimMat = new THREE.MeshStandardMaterial({ color: COLOR_RIM, metalness: 0.80, roughness: 0.30 })
|
|
125
|
+
const hubMat = new THREE.MeshStandardMaterial({ color: COLOR_HUB, metalness: 0.85, roughness: 0.25 })
|
|
126
|
+
const seatMat = new THREE.MeshStandardMaterial({ color: COLOR_SEAT, roughness: 0.85 })
|
|
127
|
+
const guardMat = new THREE.MeshStandardMaterial({ color: COLOR_GUARD, metalness: 0.75, roughness: 0.40 })
|
|
128
|
+
const hydraulicMat = new THREE.MeshStandardMaterial({ color: COLOR_HYDRAULIC, metalness: 0.95, roughness: 0.10 })
|
|
129
|
+
const chainMat = new THREE.MeshStandardMaterial({ color: COLOR_CHAIN, metalness: 0.80, roughness: 0.40 })
|
|
130
|
+
const windowMat = new THREE.MeshStandardMaterial({
|
|
131
|
+
color: COLOR_WINDOW, metalness: 0.20, roughness: 0.05,
|
|
132
|
+
transparent: true, opacity: 0.55
|
|
133
|
+
})
|
|
134
|
+
const tailMat = new THREE.MeshStandardMaterial({
|
|
135
|
+
color: COLOR_TAIL, emissive: COLOR_TAIL, emissiveIntensity: 0.6,
|
|
136
|
+
metalness: 0.10, roughness: 0.40
|
|
137
|
+
})
|
|
138
|
+
const headlightMat = new THREE.MeshStandardMaterial({
|
|
139
|
+
color: COLOR_HEADLIGHT, emissive: COLOR_HEADLIGHT, emissiveIntensity: 0.4,
|
|
140
|
+
metalness: 0.10, roughness: 0.30
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// ────────────────────────────────────────────────────────────────
|
|
144
|
+
// 1. CHASSIS BODY — sculpted side profile + width via ExtrudeGeometry
|
|
145
|
+
// ────────────────────────────────────────────────────────────────
|
|
146
|
+
const chassisGroup = new THREE.Group()
|
|
147
|
+
chassisGroup.position.set(0, baseY + wheelR, 0) // wheels rest on floor
|
|
148
|
+
|
|
149
|
+
// Side profile (looking from +X, +Z forward, +Y up).
|
|
150
|
+
// Coordinates start at (z, y) = (chassisLen/2, 0) — front bottom corner.
|
|
151
|
+
const zFront = chassisLen * 0.5
|
|
152
|
+
const zBack = -chassisLen * 0.5
|
|
153
|
+
const hoodH = chassisH * 0.55
|
|
154
|
+
const stepZ = chassisLen * 0.10
|
|
155
|
+
const seatH = chassisH * 0.85
|
|
156
|
+
|
|
157
|
+
const sideShape = new THREE.Shape()
|
|
158
|
+
sideShape.moveTo(zFront, 0)
|
|
159
|
+
sideShape.lineTo(zFront, hoodH) // up to hood top
|
|
160
|
+
sideShape.lineTo(stepZ * 1.2, hoodH) // forward hood flat
|
|
161
|
+
sideShape.lineTo(stepZ * 0.4, seatH) // sloped step up to seat platform
|
|
162
|
+
sideShape.lineTo(zBack + stepZ, seatH) // platform flat to rear
|
|
163
|
+
sideShape.lineTo(zBack, seatH * 1.05) // tiny lip into counterweight
|
|
164
|
+
sideShape.lineTo(zBack, 0) // down to ground
|
|
165
|
+
sideShape.lineTo(zFront, 0)
|
|
166
|
+
|
|
167
|
+
const chassisGeo = new THREE.ExtrudeGeometry(sideShape, {
|
|
168
|
+
depth: width * 0.78,
|
|
169
|
+
bevelEnabled: true,
|
|
170
|
+
bevelSegments: 2,
|
|
171
|
+
bevelSize: chassisH * 0.04,
|
|
172
|
+
bevelThickness: chassisH * 0.04,
|
|
173
|
+
curveSegments: 6
|
|
174
|
+
})
|
|
175
|
+
chassisGeo.rotateY(Math.PI / 2)
|
|
176
|
+
chassisGeo.translate(width * 0.39, 0, 0)
|
|
177
|
+
const chassisMesh = new THREE.Mesh(chassisGeo, bodyMat)
|
|
178
|
+
chassisMesh.castShadow = true
|
|
179
|
+
chassisMesh.receiveShadow = true
|
|
180
|
+
chassisGroup.add(chassisMesh)
|
|
181
|
+
|
|
182
|
+
// Side accent panel (slightly recessed darker panel along the hood)
|
|
183
|
+
for (const xSign of [-1, 1]) {
|
|
184
|
+
const panelGeo = new RoundedBoxGeometry(width * 0.005, hoodH * 0.55, chassisLen * 0.6, 1, hoodH * 0.06)
|
|
185
|
+
const panelMesh = new THREE.Mesh(panelGeo, bodyShadeMat)
|
|
186
|
+
panelMesh.position.set(xSign * width * 0.395, hoodH * 0.45, stepZ * 0.4)
|
|
187
|
+
chassisGroup.add(panelMesh)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Status pinstripe — emissive thin band above the panel, status color
|
|
191
|
+
const pinMat = new THREE.MeshStandardMaterial({
|
|
192
|
+
color: lampEmissive, emissive: lampEmissive,
|
|
193
|
+
emissiveIntensity: Math.max(lampIntensity * 0.3, 0.08),
|
|
194
|
+
metalness: 0.20, roughness: 0.40
|
|
195
|
+
})
|
|
196
|
+
for (const xSign of [-1, 1]) {
|
|
197
|
+
const pinGeo = new THREE.BoxGeometry(width * 0.006, hoodH * 0.04, chassisLen * 0.55)
|
|
198
|
+
const pinMesh = new THREE.Mesh(pinGeo, pinMat)
|
|
199
|
+
pinMesh.position.set(xSign * width * 0.40, hoodH * 0.78, stepZ * 0.4)
|
|
200
|
+
chassisGroup.add(pinMesh)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Headlights (front face of hood, two small emissive blocks)
|
|
204
|
+
for (const xSign of [-1, 1]) {
|
|
205
|
+
const hlGeo = new RoundedBoxGeometry(width * 0.10, hoodH * 0.18, chassisH * 0.05, 2, hoodH * 0.04)
|
|
206
|
+
const hlMesh = new THREE.Mesh(hlGeo, headlightMat)
|
|
207
|
+
hlMesh.position.set(xSign * width * 0.21, hoodH * 0.60, zFront + chassisH * 0.025)
|
|
208
|
+
chassisGroup.add(hlMesh)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.object3d.add(chassisGroup)
|
|
212
|
+
|
|
213
|
+
// ────────────────────────────────────────────────────────────────
|
|
214
|
+
// 2. COUNTERWEIGHT — sculpted rear mass (tapered, distinct from chassis)
|
|
215
|
+
// ────────────────────────────────────────────────────────────────
|
|
216
|
+
const cwGroup = new THREE.Group()
|
|
217
|
+
const cwZ = zBack - cwD * 0.5
|
|
218
|
+
|
|
219
|
+
// Base — wide, rectangular with rounded corners, sits low
|
|
220
|
+
const cwBaseGeo = new RoundedBoxGeometry(width * 0.96, cwH * 0.70, cwD, 3, cwH * 0.05)
|
|
221
|
+
const cwBaseMesh = new THREE.Mesh(cwBaseGeo, cwMat)
|
|
222
|
+
cwBaseMesh.position.set(0, baseY + wheelR + cwH * 0.35, cwZ)
|
|
223
|
+
cwBaseMesh.castShadow = true
|
|
224
|
+
cwGroup.add(cwBaseMesh)
|
|
225
|
+
|
|
226
|
+
// Top — narrower, slightly forward — gives the chamfered cast-iron look
|
|
227
|
+
const cwTopGeo = new RoundedBoxGeometry(width * 0.82, cwH * 0.32, cwD * 0.85, 3, cwH * 0.04)
|
|
228
|
+
const cwTopMat = new THREE.MeshStandardMaterial({
|
|
229
|
+
color: new THREE.Color(COLOR_COUNTERWEIGHT).multiplyScalar(1.20),
|
|
230
|
+
metalness: 0.55, roughness: 0.65
|
|
231
|
+
})
|
|
232
|
+
const cwTopMesh = new THREE.Mesh(cwTopGeo, cwTopMat)
|
|
233
|
+
cwTopMesh.position.set(0, baseY + wheelR + cwH * 0.70 + cwH * 0.16, cwZ + cwD * 0.05)
|
|
234
|
+
cwTopMesh.castShadow = true
|
|
235
|
+
cwGroup.add(cwTopMesh)
|
|
236
|
+
|
|
237
|
+
// Tail lights (rear face of cw base)
|
|
238
|
+
for (const xSign of [-1, 1]) {
|
|
239
|
+
const tailGeo = new RoundedBoxGeometry(width * 0.10, cwH * 0.10, cwD * 0.04, 1, cwD * 0.02)
|
|
240
|
+
const tailMesh = new THREE.Mesh(tailGeo, tailMat)
|
|
241
|
+
tailMesh.position.set(xSign * width * 0.36, baseY + wheelR + cwH * 0.50, cwZ - cwD * 0.50)
|
|
242
|
+
cwGroup.add(tailMesh)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.object3d.add(cwGroup)
|
|
246
|
+
|
|
247
|
+
// ────────────────────────────────────────────────────────────────
|
|
248
|
+
// 3. CAB — open frame: pillars + roof + seat + steering
|
|
249
|
+
// ────────────────────────────────────────────────────────────────
|
|
250
|
+
const cabGroup = new THREE.Group()
|
|
251
|
+
const cabRoofY = cabFloorY + cabH
|
|
252
|
+
const pillarT = width * 0.025
|
|
253
|
+
const pillarPositions: [number, number][] = [
|
|
254
|
+
[width * 0.27, height * 0.05], // front-right
|
|
255
|
+
[width * 0.27, -height * 0.28], // rear-right
|
|
256
|
+
[-width * 0.27, height * 0.05],
|
|
257
|
+
[-width * 0.27, -height * 0.28]
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
const pillarGeos: THREE.BufferGeometry[] = []
|
|
261
|
+
for (const [x, z] of pillarPositions) {
|
|
262
|
+
const g = new RoundedBoxGeometry(pillarT, cabH, pillarT, 1, pillarT * 0.2)
|
|
263
|
+
g.translate(x, cabFloorY + cabH / 2, z)
|
|
264
|
+
pillarGeos.push(g)
|
|
265
|
+
}
|
|
266
|
+
const pillarsMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(pillarGeos), bodyShadeMat)
|
|
267
|
+
pillarsMesh.castShadow = true
|
|
268
|
+
cabGroup.add(pillarsMesh)
|
|
269
|
+
|
|
270
|
+
// Roof panel with overhang
|
|
271
|
+
const cabRoofGeo = new RoundedBoxGeometry(width * 0.62, depth * 0.020, height * 0.40, 2, width * 0.02)
|
|
272
|
+
const cabRoofMesh = new THREE.Mesh(cabRoofGeo, bodyShadeMat)
|
|
273
|
+
cabRoofMesh.position.set(0, cabRoofY + depth * 0.010, -height * 0.115)
|
|
274
|
+
cabRoofMesh.castShadow = true
|
|
275
|
+
cabGroup.add(cabRoofMesh)
|
|
276
|
+
|
|
277
|
+
// Operator seat
|
|
278
|
+
const seatGeo = new RoundedBoxGeometry(width * 0.28, cabH * 0.40, height * 0.20, 2, width * 0.03)
|
|
279
|
+
const seatMesh = new THREE.Mesh(seatGeo, seatMat)
|
|
280
|
+
seatMesh.position.set(0, cabFloorY + cabH * 0.22, -height * 0.18)
|
|
281
|
+
cabGroup.add(seatMesh)
|
|
282
|
+
|
|
283
|
+
// Backrest
|
|
284
|
+
const backGeo = new RoundedBoxGeometry(width * 0.28, cabH * 0.65, height * 0.04, 2, width * 0.02)
|
|
285
|
+
const backMesh = new THREE.Mesh(backGeo, seatMat)
|
|
286
|
+
backMesh.position.set(0, cabFloorY + cabH * 0.55, -height * 0.27)
|
|
287
|
+
cabGroup.add(backMesh)
|
|
288
|
+
|
|
289
|
+
// Steering column + wheel
|
|
290
|
+
const steeringMat = new THREE.MeshStandardMaterial({
|
|
291
|
+
color: COLOR_STEERING, metalness: 0.4, roughness: 0.55
|
|
292
|
+
})
|
|
293
|
+
const colGeo = new THREE.CylinderGeometry(width * 0.014, width * 0.020, cabH * 0.55, 16)
|
|
294
|
+
colGeo.rotateX(-Math.PI * 0.12)
|
|
295
|
+
const colMesh = new THREE.Mesh(colGeo, steeringMat)
|
|
296
|
+
colMesh.position.set(0, cabFloorY + cabH * 0.30, -height * 0.05)
|
|
297
|
+
cabGroup.add(colMesh)
|
|
298
|
+
const swGeo = new THREE.TorusGeometry(width * 0.10, width * 0.013, 8, 24)
|
|
299
|
+
swGeo.rotateX(Math.PI / 2 - Math.PI * 0.12)
|
|
300
|
+
const swMesh = new THREE.Mesh(swGeo, steeringMat)
|
|
301
|
+
swMesh.position.set(0, cabFloorY + cabH * 0.55, height * 0.02)
|
|
302
|
+
cabGroup.add(swMesh)
|
|
303
|
+
|
|
304
|
+
this.object3d.add(cabGroup)
|
|
305
|
+
|
|
306
|
+
// ────────────────────────────────────────────────────────────────
|
|
307
|
+
// 4. OVERHEAD GUARD — ROPS structure above cab
|
|
308
|
+
// ────────────────────────────────────────────────────────────────
|
|
309
|
+
const guardGroup = new THREE.Group()
|
|
310
|
+
const guardPostT = width * 0.024
|
|
311
|
+
const guardX = width * 0.27
|
|
312
|
+
const guardZF = height * 0.05
|
|
313
|
+
const guardZB = -height * 0.38
|
|
314
|
+
const guardPostY = cabRoofY + guardH / 2
|
|
315
|
+
|
|
316
|
+
const guardGeos: THREE.BufferGeometry[] = []
|
|
317
|
+
for (const xSign of [-1, 1]) {
|
|
318
|
+
for (const z of [guardZF, guardZB]) {
|
|
319
|
+
const g = new THREE.BoxGeometry(guardPostT, guardH, guardPostT)
|
|
320
|
+
g.translate(xSign * guardX, guardPostY, z)
|
|
321
|
+
guardGeos.push(g)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Roof grid (3 lateral + 3 longitudinal bars)
|
|
325
|
+
const roofY = guardPostY + guardH / 2 + guardPostT / 2
|
|
326
|
+
const roofZSpan = guardZF - guardZB
|
|
327
|
+
for (const xFrac of [-1, 0, 1]) {
|
|
328
|
+
const g = new THREE.BoxGeometry(guardPostT * 0.7, guardPostT * 0.7, roofZSpan + guardPostT)
|
|
329
|
+
g.translate(xFrac * guardX, roofY, (guardZF + guardZB) / 2)
|
|
330
|
+
guardGeos.push(g)
|
|
331
|
+
}
|
|
332
|
+
for (const zFrac of [-1, 0, 1]) {
|
|
333
|
+
const g = new THREE.BoxGeometry(2 * guardX + guardPostT, guardPostT * 0.7, guardPostT * 0.7)
|
|
334
|
+
g.translate(0, roofY, (guardZF + guardZB) / 2 + zFrac * roofZSpan * 0.5)
|
|
335
|
+
guardGeos.push(g)
|
|
336
|
+
}
|
|
337
|
+
const guardMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(guardGeos), guardMat)
|
|
338
|
+
guardMesh.castShadow = true
|
|
339
|
+
guardGroup.add(guardMesh)
|
|
340
|
+
|
|
341
|
+
// Side mirrors (small disks on bracket arms from guard front posts)
|
|
342
|
+
const mirrorMat = new THREE.MeshStandardMaterial({
|
|
343
|
+
color: COLOR_WINDOW, metalness: 0.85, roughness: 0.10
|
|
344
|
+
})
|
|
345
|
+
for (const xSign of [-1, 1]) {
|
|
346
|
+
const bracketGeo = new THREE.BoxGeometry(width * 0.025, guardPostT * 0.5, guardPostT * 0.5)
|
|
347
|
+
const bracketMesh = new THREE.Mesh(bracketGeo, guardMat)
|
|
348
|
+
bracketMesh.position.set(xSign * (guardX + width * 0.0125), cabRoofY + guardH * 0.65, guardZF)
|
|
349
|
+
guardGroup.add(bracketMesh)
|
|
350
|
+
|
|
351
|
+
const mirGeo = new THREE.CylinderGeometry(width * 0.04, width * 0.04, width * 0.008, 16)
|
|
352
|
+
mirGeo.rotateZ(Math.PI / 2)
|
|
353
|
+
const mirMesh = new THREE.Mesh(mirGeo, mirrorMat)
|
|
354
|
+
mirMesh.position.set(xSign * (guardX + width * 0.05), cabRoofY + guardH * 0.65, guardZF)
|
|
355
|
+
guardGroup.add(mirMesh)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this.object3d.add(guardGroup)
|
|
359
|
+
|
|
360
|
+
// ────────────────────────────────────────────────────────────────
|
|
361
|
+
// 5. MAST ASSEMBLY — rails + crossbar + hydraulic + chains + carriage + forks
|
|
362
|
+
// ────────────────────────────────────────────────────────────────
|
|
363
|
+
const mastGroup = new THREE.Group()
|
|
364
|
+
const mastH = depth * 0.95
|
|
365
|
+
const mastRailW = width * 0.06
|
|
366
|
+
const mastRailD = height * 0.03
|
|
367
|
+
const mastSpacing = width * 0.45
|
|
368
|
+
const mastZ = height * 0.42
|
|
369
|
+
const mastY = baseY + wheelR + mastH / 2
|
|
370
|
+
|
|
371
|
+
// Two main rails (rounded for visual softness)
|
|
372
|
+
for (const xSign of [-1, 1]) {
|
|
373
|
+
const railGeo = new RoundedBoxGeometry(mastRailW, mastH, mastRailD, 2, mastRailW * 0.20)
|
|
374
|
+
const railMesh = new THREE.Mesh(railGeo, mastMat)
|
|
375
|
+
railMesh.position.set((xSign * mastSpacing) / 2, mastY, mastZ)
|
|
376
|
+
railMesh.castShadow = true
|
|
377
|
+
mastGroup.add(railMesh)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Crossbar at top
|
|
381
|
+
const crossbarGeo = new RoundedBoxGeometry(
|
|
382
|
+
mastSpacing + mastRailW, mastRailD * 1.3, mastRailD * 1.1, 2, mastRailW * 0.15
|
|
383
|
+
)
|
|
384
|
+
const crossbarMesh = new THREE.Mesh(crossbarGeo, mastMat)
|
|
385
|
+
crossbarMesh.position.set(0, baseY + wheelR + mastH, mastZ)
|
|
386
|
+
crossbarMesh.castShadow = true
|
|
387
|
+
mastGroup.add(crossbarMesh)
|
|
388
|
+
|
|
389
|
+
// Hydraulic cylinder (center, between rails)
|
|
390
|
+
const hydCylR = mastRailW * 0.42
|
|
391
|
+
const hydCylH = mastH * 0.78
|
|
392
|
+
const hydCylGeo = new THREE.CylinderGeometry(hydCylR, hydCylR * 1.05, hydCylH, 20)
|
|
393
|
+
const hydCylMesh = new THREE.Mesh(hydCylGeo, hydraulicMat)
|
|
394
|
+
hydCylMesh.position.set(0, baseY + wheelR + hydCylH / 2 + mastRailD * 0.5, mastZ - mastRailD * 0.15)
|
|
395
|
+
hydCylMesh.castShadow = true
|
|
396
|
+
mastGroup.add(hydCylMesh)
|
|
397
|
+
|
|
398
|
+
// Piston rod (length tracks forkHeight)
|
|
399
|
+
const pistonR = hydCylR * 0.55
|
|
400
|
+
const pistonH = hydCylH * 0.45 + forkHeight * 0.7
|
|
401
|
+
const pistonGeo = new THREE.CylinderGeometry(pistonR, pistonR, pistonH, 16)
|
|
402
|
+
const pistonMesh = new THREE.Mesh(pistonGeo, hydraulicMat)
|
|
403
|
+
pistonMesh.position.set(
|
|
404
|
+
0,
|
|
405
|
+
baseY + wheelR + hydCylH + pistonH / 2 + mastRailD * 0.5,
|
|
406
|
+
mastZ - mastRailD * 0.15
|
|
407
|
+
)
|
|
408
|
+
mastGroup.add(pistonMesh)
|
|
409
|
+
|
|
410
|
+
// Lift chains (parallel link bars between rails)
|
|
411
|
+
for (const xSign of [-1, 1]) {
|
|
412
|
+
const chW = mastRailW * 0.18
|
|
413
|
+
const chH = mastH * 0.55
|
|
414
|
+
const chGeo = new THREE.BoxGeometry(chW, chH, mastRailD * 0.3)
|
|
415
|
+
const chMesh = new THREE.Mesh(chGeo, chainMat)
|
|
416
|
+
chMesh.position.set(xSign * mastSpacing * 0.30, baseY + wheelR + chH / 2 + mastRailD * 0.5, mastZ - mastRailD * 0.4)
|
|
417
|
+
mastGroup.add(chMesh)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Carriage (sliding plate)
|
|
421
|
+
const carriageY = baseY + wheelR + forkHeight + cwH * 0.05
|
|
422
|
+
const carriageW = mastSpacing + mastRailW
|
|
423
|
+
const carriageH = chassisH * 0.40
|
|
424
|
+
const carriageD = mastRailD * 0.7
|
|
425
|
+
const carriageGeo = new RoundedBoxGeometry(carriageW, carriageH, carriageD, 2, carriageH * 0.10)
|
|
426
|
+
const carriageMesh = new THREE.Mesh(carriageGeo, forkMat)
|
|
427
|
+
carriageMesh.position.set(0, carriageY + carriageH / 2, mastZ + mastRailD)
|
|
428
|
+
carriageMesh.castShadow = true
|
|
429
|
+
mastGroup.add(carriageMesh)
|
|
430
|
+
|
|
431
|
+
// Forks (L-shape via ExtrudeGeometry — heel + horizontal blade)
|
|
432
|
+
const forkLen = height * 0.42
|
|
433
|
+
const forkW = width * 0.10
|
|
434
|
+
const forkH = depth * 0.018
|
|
435
|
+
const forkBladeY = carriageY + forkH / 2
|
|
436
|
+
for (const xSign of [-1, 1]) {
|
|
437
|
+
const heelH = carriageH * 0.85
|
|
438
|
+
const fs = new THREE.Shape()
|
|
439
|
+
fs.moveTo(0, 0)
|
|
440
|
+
fs.lineTo(forkLen, 0)
|
|
441
|
+
fs.lineTo(forkLen, forkH * 0.5)
|
|
442
|
+
fs.lineTo(forkH, forkH * 1.0)
|
|
443
|
+
fs.lineTo(forkH, heelH)
|
|
444
|
+
fs.lineTo(0, heelH)
|
|
445
|
+
fs.lineTo(0, 0)
|
|
446
|
+
const forkGeo = new THREE.ExtrudeGeometry(fs, {
|
|
447
|
+
depth: forkW,
|
|
448
|
+
bevelEnabled: true,
|
|
449
|
+
bevelSegments: 1,
|
|
450
|
+
bevelSize: forkH * 0.18,
|
|
451
|
+
bevelThickness: forkH * 0.18
|
|
452
|
+
})
|
|
453
|
+
forkGeo.rotateY(-Math.PI / 2)
|
|
454
|
+
forkGeo.translate(
|
|
455
|
+
xSign * mastSpacing * 0.45 + forkW / 2,
|
|
456
|
+
forkBladeY,
|
|
457
|
+
mastZ + mastRailD + carriageD
|
|
458
|
+
)
|
|
459
|
+
const forkMesh = new THREE.Mesh(forkGeo, forkMat)
|
|
460
|
+
forkMesh.castShadow = true
|
|
461
|
+
mastGroup.add(forkMesh)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
this.object3d.add(mastGroup)
|
|
465
|
+
|
|
466
|
+
// ────────────────────────────────────────────────────────────────
|
|
467
|
+
// 6. WHEELS — tire + rim disc + spokes (drive front, caster rear)
|
|
468
|
+
// ────────────────────────────────────────────────────────────────
|
|
469
|
+
const wheelW = depth * 0.04
|
|
470
|
+
const wheelGroup = new THREE.Group()
|
|
471
|
+
|
|
472
|
+
const tireGeos: THREE.BufferGeometry[] = []
|
|
473
|
+
const rimGeos: THREE.BufferGeometry[] = []
|
|
474
|
+
const hubGeos: THREE.BufferGeometry[] = []
|
|
475
|
+
|
|
476
|
+
// Front (drive) wheels — slightly larger
|
|
477
|
+
for (const xSign of [-1, 1]) {
|
|
478
|
+
const cx = xSign * width * 0.42
|
|
479
|
+
const cz = height * 0.30
|
|
480
|
+
const r = wheelR * 1.05
|
|
481
|
+
|
|
482
|
+
const tire = new THREE.CylinderGeometry(r, r, wheelW, 24)
|
|
483
|
+
tire.rotateZ(Math.PI / 2)
|
|
484
|
+
tire.translate(cx, baseY + r, cz)
|
|
485
|
+
tireGeos.push(tire)
|
|
486
|
+
|
|
487
|
+
const rim = new THREE.CylinderGeometry(r * 0.55, r * 0.55, wheelW * 0.85, 18)
|
|
488
|
+
rim.rotateZ(Math.PI / 2)
|
|
489
|
+
rim.translate(cx + xSign * wheelW * 0.10, baseY + r, cz)
|
|
490
|
+
rimGeos.push(rim)
|
|
491
|
+
|
|
492
|
+
// Spokes (5 cross-bars on the rim front face)
|
|
493
|
+
for (let i = 0; i < 5; i++) {
|
|
494
|
+
const angle = (i / 5) * Math.PI * 2
|
|
495
|
+
const sp = new THREE.BoxGeometry(wheelW * 0.10, r * 0.10, r * 0.95)
|
|
496
|
+
sp.rotateX(angle)
|
|
497
|
+
sp.translate(cx + xSign * wheelW * 0.50, baseY + r, cz)
|
|
498
|
+
hubGeos.push(sp)
|
|
499
|
+
}
|
|
500
|
+
// Center hub cap
|
|
501
|
+
const cap = new THREE.SphereGeometry(r * 0.22, 14, 10, 0, Math.PI * 2, 0, Math.PI / 2)
|
|
502
|
+
cap.rotateZ(Math.PI / 2 * xSign)
|
|
503
|
+
cap.translate(cx + xSign * wheelW * 0.55, baseY + r, cz)
|
|
504
|
+
hubGeos.push(cap)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Rear (caster) wheels — smaller
|
|
508
|
+
for (const xSign of [-1, 1]) {
|
|
509
|
+
const cx = xSign * width * 0.36
|
|
510
|
+
const cz = -height * 0.32
|
|
511
|
+
const r = wheelR * 0.78
|
|
512
|
+
|
|
513
|
+
const tire = new THREE.CylinderGeometry(r, r, wheelW * 0.85, 20)
|
|
514
|
+
tire.rotateZ(Math.PI / 2)
|
|
515
|
+
tire.translate(cx, baseY + r, cz)
|
|
516
|
+
tireGeos.push(tire)
|
|
517
|
+
|
|
518
|
+
const rim = new THREE.CylinderGeometry(r * 0.55, r * 0.55, wheelW * 0.7, 16)
|
|
519
|
+
rim.rotateZ(Math.PI / 2)
|
|
520
|
+
rim.translate(cx + xSign * wheelW * 0.08, baseY + r, cz)
|
|
521
|
+
rimGeos.push(rim)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const tireMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(tireGeos), tireMat)
|
|
525
|
+
tireMesh.castShadow = true
|
|
526
|
+
wheelGroup.add(tireMesh)
|
|
527
|
+
|
|
528
|
+
const rimMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(rimGeos), rimMat)
|
|
529
|
+
rimMesh.castShadow = true
|
|
530
|
+
wheelGroup.add(rimMesh)
|
|
531
|
+
|
|
532
|
+
if (hubGeos.length > 0) {
|
|
533
|
+
const hubMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(hubGeos), hubMat)
|
|
534
|
+
wheelGroup.add(hubMesh)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
this.object3d.add(wheelGroup)
|
|
538
|
+
|
|
539
|
+
// ────────────────────────────────────────────────────────────────
|
|
540
|
+
// 7. STATUS LAMP — sculpted (base + body + dome) on guard top
|
|
541
|
+
// ────────────────────────────────────────────────────────────────
|
|
542
|
+
const lampGroup = new THREE.Group()
|
|
543
|
+
const lampR = Math.min(width, height) * 0.04
|
|
544
|
+
const lampH = lampR * 1.5
|
|
545
|
+
const lampMat = new THREE.MeshStandardMaterial({
|
|
546
|
+
color: lampEmissive, emissive: lampEmissive,
|
|
547
|
+
emissiveIntensity: lampIntensity,
|
|
548
|
+
metalness: 0.0, roughness: 0.30
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
// Base disc
|
|
552
|
+
const lbGeo = new THREE.CylinderGeometry(lampR * 1.15, lampR * 1.15, lampR * 0.25, 16)
|
|
553
|
+
const lbMesh = new THREE.Mesh(lbGeo, guardMat)
|
|
554
|
+
lbMesh.position.set(0, roofY + guardPostT * 0.5 + lampR * 0.125, (guardZF + guardZB) / 2)
|
|
555
|
+
lampGroup.add(lbMesh)
|
|
556
|
+
|
|
557
|
+
// Body
|
|
558
|
+
const lyGeo = new THREE.CylinderGeometry(lampR, lampR * 0.95, lampH, 16)
|
|
559
|
+
const lyMesh = new THREE.Mesh(lyGeo, lampMat)
|
|
560
|
+
lyMesh.position.set(0, roofY + guardPostT * 0.5 + lampR * 0.25 + lampH / 2, (guardZF + guardZB) / 2)
|
|
561
|
+
lampGroup.add(lyMesh)
|
|
562
|
+
|
|
563
|
+
// Dome
|
|
564
|
+
const ldGeo = new THREE.SphereGeometry(lampR, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2)
|
|
565
|
+
const ldMesh = new THREE.Mesh(ldGeo, lampMat)
|
|
566
|
+
ldMesh.position.set(0, roofY + guardPostT * 0.5 + lampR * 0.25 + lampH, (guardZF + guardZB) / 2)
|
|
567
|
+
lampGroup.add(ldMesh)
|
|
568
|
+
|
|
569
|
+
this.object3d.add(lampGroup)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
updateDimension() {}
|
|
573
|
+
|
|
574
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
575
|
+
if (
|
|
576
|
+
'status' in after ||
|
|
577
|
+
'bodyColor' in after ||
|
|
578
|
+
'lampEmissive' in after ||
|
|
579
|
+
'forkHeight' in after ||
|
|
580
|
+
'width' in after ||
|
|
581
|
+
'height' in after ||
|
|
582
|
+
'depth' in after
|
|
583
|
+
) {
|
|
584
|
+
this.update()
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
super.onchange(after, before)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
updateAlpha() {}
|
|
591
|
+
}
|