lecodes-sdk 0.20.2 → 1.0.0
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/dist/global.d.ts +17 -5
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/gl/DecalSet.d.ts +48 -3
- package/dist/types/gl/Geometry.d.ts +12 -0
- package/dist/types/gl/Lightmap.d.ts +9 -0
- package/dist/types/gl/Material.d.ts +4 -0
- package/dist/types/gl/Mesh.d.ts +7 -1
- package/dist/types/gl/Model.d.ts +34 -0
- package/dist/types/gl/Particles.d.ts +13 -0
- package/dist/types/gl/Scene.d.ts +1 -1
- package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
- package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
- package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
- package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
- package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
- package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
- package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
- package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
- package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
- package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
- package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
- package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
- package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
- package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
- package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
- package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
- package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
- package/dist/types/inject.d.ts +28 -26
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/scene/defineScene.d.ts +10 -3
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/src/bridges.d.ts +1599 -1481
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +1 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/gl/DecalSet.ts +132 -5
- package/src/gl/Geometry.ts +348 -284
- package/src/gl/Lightmap.ts +32 -5
- package/src/gl/Material.ts +21 -0
- package/src/gl/Mesh.ts +120 -102
- package/src/gl/Model.ts +33 -1
- package/src/gl/Node.ts +1 -1
- package/src/gl/Particles.ts +21 -3
- package/src/gl/Scene.ts +1 -1
- package/src/gl/animation/DynamicBone.ts +459 -0
- package/src/gl/{IK.ts → animation/IK.ts} +4 -4
- package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
- package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
- package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
- package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
- package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
- package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
- package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
- package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
- package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +8 -6
- package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
- package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
- package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
- package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
- package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
- package/src/gl/vehicle/Vehicle.ts +666 -0
- package/src/gl/vehicle/Wheel.ts +290 -0
- package/src/inject.ts +226 -224
- package/src/runtime/files.ts +32 -2
- package/src/scene/defineScene.ts +26 -10
- package/src/scene/level.ts +2 -2
- package/dist/types/gl/Gearbox.d.ts +0 -86
- package/dist/types/gl/Vehicle.d.ts +0 -191
- package/dist/types/gl/Wheel.d.ts +0 -95
- package/src/gl/Gearbox.ts +0 -212
- package/src/gl/Vehicle.ts +0 -473
- package/src/gl/Wheel.ts +0 -240
- /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// One wheel of a `Vehicle`, as an aspect on the wheel's own node — a wheel COLLIDER, the way Unity's
|
|
2
|
+
// WheelCollider is one. The node's position IS the wheel's resting position (where its centre sits
|
|
3
|
+
// on flat ground), and everything physical about it lives here: size, suspension, tire, its role.
|
|
4
|
+
//
|
|
5
|
+
// const car = new Node('car')
|
|
6
|
+
// .add(body, hubFL.aspect(Wheel), hubFR.aspect(Wheel), hubRL.aspect(Wheel), hubRR.aspect(Wheel))
|
|
7
|
+
// .aspect(Shape, { box: [0.9, 0.4, 2.1] })
|
|
8
|
+
// .aspect(Vehicle, { mass: 1200 })
|
|
9
|
+
//
|
|
10
|
+
// That is the whole minimum: `Vehicle` collects every `Wheel` under itself, groups them into axles by
|
|
11
|
+
// Z (front first), and the defaults fall out — the front axle steers, the rear one is driven.
|
|
12
|
+
// Override any of it per wheel.
|
|
13
|
+
//
|
|
14
|
+
// The aspect never touches a transform. The hub node stays where the author put it; where the
|
|
15
|
+
// wheel IS right now is a readout — `position` (down the suspension), `steerAngle`, `spin` — and
|
|
16
|
+
// posing a wheel model from them is the game's, a few lines in a late-phase system:
|
|
17
|
+
//
|
|
18
|
+
// angle = (angle + wheel.spin * dt) % (2 * Math.PI)
|
|
19
|
+
// mesh.position = wheel.position
|
|
20
|
+
// mesh.quaternion = Quat.fromAxisAngle([0, 1, 0], -wheel.steerAngle * DEG2RAD)
|
|
21
|
+
// .mul(Quat.fromAxisAngle([1, 0, 0], -angle))
|
|
22
|
+
//
|
|
23
|
+
// (both minus signs: the engine reports the driver's terms — steer right-positive, spin forward-
|
|
24
|
+
// positive — in a right-handed scene whose forward is −Z; a caliper is the same without the spin).
|
|
25
|
+
//
|
|
26
|
+
// The TIRE is numbers too, all the way down: its friction curves go to the engine as points, the way
|
|
27
|
+
// the engine's torque curve does. `Tire.road()` / `arcade()` / `drift()` are point lists of this
|
|
28
|
+
// file, not the engine's — the engine has no presets at all.
|
|
29
|
+
//
|
|
30
|
+
// The brake is an INPUT here (`brake`, N·m this frame), like `throttle` on the car: a foot brake, a
|
|
31
|
+
// handbrake, an ABS decide what each wheel gets, and that is the game's.
|
|
32
|
+
//
|
|
33
|
+
// Chassis space is forward −Z / up +Y, the SDK's own convention (`node.forward`).
|
|
34
|
+
|
|
35
|
+
import { Aspect } from "../../core/Aspect"
|
|
36
|
+
import type { FieldMeta } from "../../core/fields"
|
|
37
|
+
import { Mat4 } from "../../math/mat4"
|
|
38
|
+
import { Vec3, cx, cy, cz } from "../../math/vec"
|
|
39
|
+
import { Gizmos } from "../../scene/gizmos"
|
|
40
|
+
import { Node } from "../Node"
|
|
41
|
+
import { Shape } from "../physics/Shape"
|
|
42
|
+
import type { Vehicle } from "./Vehicle"
|
|
43
|
+
|
|
44
|
+
/** Floats `vehicleGetState` writes before the per-wheel block: speed, rpm, contacts, vx, vy, vz, wx, wy, wz,
|
|
45
|
+
* steerDeg, steerTorque. */
|
|
46
|
+
export const STATE_HEADER = 11
|
|
47
|
+
/** Floats per wheel: contact, slip, slipAngle, suspensionLength, steerDeg, spin, fLat, fLong, vLat, vSlip, load. */
|
|
48
|
+
export const WHEEL_STATE = 11
|
|
49
|
+
/** Fully compressed = this fraction of `travel` (the engine's one suspension constant, mirrored here
|
|
50
|
+
* so `compression` and the resting-height maths read the same number). */
|
|
51
|
+
export const SUSPENSION_MIN_FRACTION = 0.4
|
|
52
|
+
|
|
53
|
+
/** A point on a curve — a tire's friction over slip, an engine's torque over rpm. */
|
|
54
|
+
export type Point = { x: number, y: number }
|
|
55
|
+
|
|
56
|
+
export interface WheelSuspension {
|
|
57
|
+
/** Total travel in metres, full droop to fully compressed (default 0.4). */
|
|
58
|
+
travel?: number
|
|
59
|
+
/** Spring frequency in Hz — bigger is stiffer (default 1.4; a race car ~2.5, a truck ~1). It also
|
|
60
|
+
* sets the ride-height sag, and the sag is how far the wheel can still extend DOWN. */
|
|
61
|
+
stiffness?: number
|
|
62
|
+
/** Damping ratio 0..1 (default 0.5). Low values pogo. */
|
|
63
|
+
damping?: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A tire, as the engine takes it: two friction curves and two numbers. `Tire.road()` and the other
|
|
68
|
+
* presets are starting points — copy one and edit, or `Tire.scale()` it.
|
|
69
|
+
*/
|
|
70
|
+
export interface Tire {
|
|
71
|
+
/** Lateral friction over slip ANGLE: `{ x: degrees, y: friction }`, x ascending. Friction is the
|
|
72
|
+
* coefficient the tire's load is multiplied by; the last value holds to 90°. The grip a wheel
|
|
73
|
+
* actually gets is combined with the ground it stands on as `sqrt(tire × groundFriction)`, and a
|
|
74
|
+
* body's friction defaults to 0.6 — a road a car should really corner on wants `Physics { friction: 1 }`. */
|
|
75
|
+
side: Point[]
|
|
76
|
+
/** Longitudinal friction over slip RATIO: `{ x: ratio, y: friction }` (0 = rolling in sync, 1 =
|
|
77
|
+
* locked or spinning freely). */
|
|
78
|
+
forward: Point[]
|
|
79
|
+
/** The longitudinal impulse clamp as a multiple of `friction × load` — for DRIVE only; a braking
|
|
80
|
+
* wheel is always clamped at the physical limit (or a locked rear would stop the car at a full g
|
|
81
|
+
* and kill every handbrake turn). `1` is the physical tire; `road`'s 10 is Jolt's own sample
|
|
82
|
+
* value (it keeps a driven wheel from ever spinning up). Anything a friction circle should bite
|
|
83
|
+
* on wants this near 1. */
|
|
84
|
+
traction?: number
|
|
85
|
+
/** Friction circle, 0..1: how much of the lateral capacity the longitudinal impulse in use takes
|
|
86
|
+
* away. `0` = the two axes independent (a spinning wheel corners as well as a rolling one);
|
|
87
|
+
* `1` = the full circle — wheelspin or a locked wheel lets go sideways. */
|
|
88
|
+
circle?: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pts = (flat: number[]): Point[] => {
|
|
92
|
+
const out: Point[] = []
|
|
93
|
+
for (let i = 0; i + 1 < flat.length; i += 2) out.push({ x: flat[i]!, y: flat[i + 1]! })
|
|
94
|
+
return out
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Tire presets — fresh point lists every call (editing one never touches another), and `scale`.
|
|
99
|
+
*
|
|
100
|
+
* - `road` — Jolt's stock tire: lateral peak 1.2 at 3° falling to 1.0 by 20° and flat after, the
|
|
101
|
+
* two axes independent, and the longitudinal clamp ×10 (Jolt's own sample runs this; it means a
|
|
102
|
+
* driven wheel practically never spins up). Grippy, glued, hard to unsettle.
|
|
103
|
+
* - `arcade` — the peak held all the way out on both axes: the car does not let go at all.
|
|
104
|
+
* - `drift` — a tire that can be held sideways: lateral grip falls to 0.7 past 25°, the longitudinal
|
|
105
|
+
* clamp is near the physical one (`traction: 2` — a launch at exactly 1 is a 0.4 g crawl; 2 keeps
|
|
106
|
+
* it punchy) so the rear can be spun up, and a friction circle (`circle: 0.85` — most of it, so a
|
|
107
|
+
* spinning rear loses most but not all of its lateral grip and the slide stays progressive):
|
|
108
|
+
* power oversteer, and a slide the throttle keeps alive. A 600 N·m car lights its tires off the
|
|
109
|
+
* line on it; that is the tire, and a traction control is the game's.
|
|
110
|
+
*/
|
|
111
|
+
export const Tire = {
|
|
112
|
+
road: (): Tire => ({ side: pts([ 0, 0, 3, 1.2, 20, 1.0 ]), forward: pts([ 0, 0, 0.06, 1.2, 0.2, 1.0 ]), traction: 10, circle: 0 }),
|
|
113
|
+
arcade: (): Tire => ({ side: pts([ 0, 0, 3, 1.2, 90, 1.2 ]), forward: pts([ 0, 0, 0.06, 1.2, 1.0, 1.2 ]), traction: 10, circle: 0 }),
|
|
114
|
+
drift: (): Tire => ({ side: pts([ 0, 0, 4, 1.2, 25, 0.75, 90, 0.7 ]), forward: pts([ 0, 0, 0.08, 1.2, 0.5, 0.85 ]), traction: 2, circle: 0.85 }),
|
|
115
|
+
/** Both curves multiplied by `k` — a grip slider, a cold tire, a wet road. A new tire; `t` is untouched. */
|
|
116
|
+
scale: (t: Tire, k: number): Tire => ({
|
|
117
|
+
...t,
|
|
118
|
+
side: t.side.map((p) => ({ x: p.x, y: p.y * k })),
|
|
119
|
+
forward: t.forward.map((p) => ({ x: p.x, y: p.y * k })),
|
|
120
|
+
}),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @internal The tire as the blob carries it: every field filled in. */
|
|
124
|
+
export interface ResolvedTire {
|
|
125
|
+
side: Point[]
|
|
126
|
+
forward: Point[]
|
|
127
|
+
traction: number
|
|
128
|
+
circle: number
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function resolveTire(tire: Tire): ResolvedTire {
|
|
132
|
+
const road = Tire.road()
|
|
133
|
+
return {
|
|
134
|
+
side: tire.side.length > 0 ? tire.side : road.side,
|
|
135
|
+
forward: tire.forward.length > 0 ? tire.forward : road.forward,
|
|
136
|
+
traction: tire.traction !== undefined && tire.traction > 0 ? tire.traction : road.traction!,
|
|
137
|
+
circle: Math.max(0, Math.min(1, tire.circle ?? road.circle!)),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class Wheel extends Aspect<"wheel", Node> {
|
|
142
|
+
static readonly aspect = "wheel"
|
|
143
|
+
|
|
144
|
+
/** Wheel radius in metres. Default: measured from the node's rendered bounds, else 0.35. Too small
|
|
145
|
+
* and the car scrapes, too large and it floats. */
|
|
146
|
+
radius?: number
|
|
147
|
+
/** Wheel width in metres. Default: measured, else 0.25. */
|
|
148
|
+
width?: number
|
|
149
|
+
/** Axle group, front to back. Default: derived from Z. Set it for a car whose wheels are not in neat
|
|
150
|
+
* left/right pairs — an axle's wheels are paired for the differential and the anti-roll bar. */
|
|
151
|
+
axle?: number
|
|
152
|
+
/** Does this wheel steer? Default: the wheels on the front axle. */
|
|
153
|
+
steer?: boolean
|
|
154
|
+
/** This wheel's SHARE of the engine's torque, 0 = not driven. Default: from the vehicle's
|
|
155
|
+
* `driveAxle` (1 on that axle). An axle's share is the sum of its two wheels', the left/right
|
|
156
|
+
* split their ratio, so `0.6` / `0.4` front-to-rear is `driven: 0.3` on each front wheel and
|
|
157
|
+
* `0.2` on each rear. Structural: baked at attach. */
|
|
158
|
+
driven?: number
|
|
159
|
+
|
|
160
|
+
suspension: WheelSuspension = { travel: 0.4, stiffness: 1.4, damping: 0.5 }
|
|
161
|
+
/** The tire (default `Tire.road()`). Live: re-assign it or re-configure the aspect and the car
|
|
162
|
+
* retunes in place — a grip slider, a tire that heats up. */
|
|
163
|
+
tire: Tire = Tire.road()
|
|
164
|
+
|
|
165
|
+
/** INPUT: brake torque on this wheel THIS frame, N·m (default 0). Sticky, like `Vehicle.throttle`:
|
|
166
|
+
* write it every frame from a driver system's `updateBefore` — the foot brake's share, the
|
|
167
|
+
* handbrake's 4500 on a rear wheel, an ABS's pulses — and the same physics step applies it. A
|
|
168
|
+
* braking wheel is clamped at the physical tire limit (the `traction` boost is for drive only). */
|
|
169
|
+
brake = 0
|
|
170
|
+
|
|
171
|
+
/** Scene-editor inspector. `steer` / `driven` are deliberately absent: they are tri-state
|
|
172
|
+
* (undefined = derive it from the axle), which a slider cannot express. */
|
|
173
|
+
static fields: FieldMeta<Wheel> = {
|
|
174
|
+
radius: { min: 0.05, step: 0.01 },
|
|
175
|
+
width: { min: 0.02, step: 0.01 },
|
|
176
|
+
axle: { min: 0, step: 1 },
|
|
177
|
+
brake: { hidden: true },
|
|
178
|
+
suspension: { hidden: true },
|
|
179
|
+
tire: { hidden: true },
|
|
180
|
+
}
|
|
181
|
+
static editor = { rebuild: true }
|
|
182
|
+
|
|
183
|
+
/** @internal Set by `Vehicle` at attach — the car this wheel belongs to, and its index in the blob. */
|
|
184
|
+
_vehicle: Vehicle | null = null
|
|
185
|
+
/** @internal */
|
|
186
|
+
_index = -1
|
|
187
|
+
/** @internal Suspension attachment point in chassis space; the wheel hangs below it. */
|
|
188
|
+
_attach = new Vec3()
|
|
189
|
+
/** @internal Resting centre in chassis space (what the author placed). */
|
|
190
|
+
_rest = new Vec3()
|
|
191
|
+
|
|
192
|
+
onDetach(): void {
|
|
193
|
+
this._vehicle = null
|
|
194
|
+
this._index = -1
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Re-configuring a wheel on a live car retunes the car: the tire is live; size, suspension, the
|
|
198
|
+
* driven share and the axle are structural (re-attach the Vehicle). */
|
|
199
|
+
onReconfigure(): void {
|
|
200
|
+
this._vehicle?._retuneSoon()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @internal Called by Vehicle once the car exists, with the geometry it resolved. */
|
|
204
|
+
_bind(vehicle: Vehicle, index: number, rest: Vec3, attach: Vec3): void {
|
|
205
|
+
this._vehicle = vehicle
|
|
206
|
+
this._index = index
|
|
207
|
+
this._rest = rest
|
|
208
|
+
this._attach = attach
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- live state ------------------------------------------------------------------------------
|
|
212
|
+
// Every getter reads the car's ONE shared state buffer, which is refreshed once per phase.
|
|
213
|
+
|
|
214
|
+
private _state(k: number): number {
|
|
215
|
+
const v = this._vehicle
|
|
216
|
+
if (!v || this._index < 0) return 0
|
|
217
|
+
return v._wheelState(this._index, k)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Touching the ground this step. */
|
|
221
|
+
get contact(): boolean { return this._state(0) !== 0 }
|
|
222
|
+
/** Longitudinal slip RATIO, signed: 0 = rolling in sync with the ground, towards +1 = spinning up
|
|
223
|
+
* (wheelspin), towards −1 = locking under braking — whichever way the car travels. Smoke, tire
|
|
224
|
+
* marks, wheelspin sound and traction control key off this. */
|
|
225
|
+
get slip(): number { return this._state(1) }
|
|
226
|
+
/** Slip ANGLE in degrees, signed: how far the tire's travel direction is off from where it points,
|
|
227
|
+
* positive when the contact patch slides to the tire's RIGHT. Past the curve's peak (~3°) the tire
|
|
228
|
+
* is letting go sideways. */
|
|
229
|
+
get slipAngle(): number { return this._state(2) }
|
|
230
|
+
/** This wheel's steer angle in degrees, right-positive. */
|
|
231
|
+
get steerAngle(): number { return this._state(4) }
|
|
232
|
+
/** Wheel spin in rad/s (× radius ≈ the speed the tire is laying down). Forward-positive; a model
|
|
233
|
+
* turns about the right-hand axle by MINUS the integrated angle. */
|
|
234
|
+
get spin(): number { return this._state(5) }
|
|
235
|
+
/** The tire's lateral force this step, N, positive toward the tire's right — the road pushing the
|
|
236
|
+
* patch back against its slide. */
|
|
237
|
+
get sideForce(): number { return this._state(6) }
|
|
238
|
+
/** The tire's longitudinal force this step, N, positive pushing the car forward. */
|
|
239
|
+
get forwardForce(): number { return this._state(7) }
|
|
240
|
+
/** How fast the contact patch slides sideways, m/s, positive to the tire's right. */
|
|
241
|
+
get sideSlipSpeed(): number { return this._state(8) }
|
|
242
|
+
/** How fast the tread runs over the road, m/s, positive spinning up, negative locking. Force ×
|
|
243
|
+
* sliding speed (`|sideForce × sideSlipSpeed| + |forwardForce × slipSpeed|`) is the friction's
|
|
244
|
+
* heat into the tread, W — a tire-temperature model integrates it. */
|
|
245
|
+
get slipSpeed(): number { return this._state(9) }
|
|
246
|
+
/** The suspension's force on the wheel this step, N — the tire's LOAD (0 in the air). What a
|
|
247
|
+
* friction budget divides by: `hypot(sideForce, forwardForce) / (load × μ)`. */
|
|
248
|
+
get load(): number { return this._state(10) }
|
|
249
|
+
/** Suspension compression, 0 = fully extended (drooping) … 1 = fully compressed. */
|
|
250
|
+
get compression(): number {
|
|
251
|
+
const travel = this.suspension.travel ?? 0.4
|
|
252
|
+
const min = travel * SUSPENSION_MIN_FRACTION
|
|
253
|
+
const span = travel - min
|
|
254
|
+
if (span <= 0) return 0
|
|
255
|
+
const length = this._state(3)
|
|
256
|
+
return Math.max(0, Math.min(1, 1 - (length - min) / span))
|
|
257
|
+
}
|
|
258
|
+
/** Where the wheel's centre is right now, in CHASSIS space: the suspension's attachment point,
|
|
259
|
+
* down by however far it hangs this step. Put a wheel model here (see the file header). */
|
|
260
|
+
get position(): Vec3 {
|
|
261
|
+
return new Vec3(this._attach.x, this._attach.y - this._state(3), this._attach.z)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Edit mode: this wheel's own circle at its resting height, its travel range, and a mark when the
|
|
265
|
+
* engine drives it — enough to place a wheel without running the game. */
|
|
266
|
+
rebuild(): void {
|
|
267
|
+
const travel = this.suspension.travel ?? 0.4
|
|
268
|
+
const r = this.radius ?? this._measure().radius
|
|
269
|
+
const world = new Mat4(this.node.worldMatrix)
|
|
270
|
+
const P = (x: number, y: number, z: number): Vec3 => world.transformPoint([ x, y, z ])
|
|
271
|
+
const ring: Vec3[] = []
|
|
272
|
+
for (let i = 0; i < 24; i++) {
|
|
273
|
+
const t = (i / 24) * Math.PI * 2
|
|
274
|
+
ring.push(P(0, Math.sin(t) * r, Math.cos(t) * r))
|
|
275
|
+
}
|
|
276
|
+
Gizmos.polyline(ring, { color: this.steer === false ? "#9aa3b2" : "#5b8ef0", closed: true })
|
|
277
|
+
Gizmos.line(P(0, travel * (1 - SUSPENSION_MIN_FRACTION), 0), P(0, -travel * SUSPENSION_MIN_FRACTION, 0), { color: "#59606d" })
|
|
278
|
+
if (this.driven) Gizmos.cross(P(0, 0, 0), r * 0.35, { color: "#e0b25a" })
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** @internal Radius/width from the node's rendered bounds, so a placed model needs no numbers.
|
|
282
|
+
* The axle is the node's X: a disc's diameter is the larger of the two axes across it. */
|
|
283
|
+
_measure(): { radius: number, width: number } {
|
|
284
|
+
const b = Shape.boundsOf(this.node)
|
|
285
|
+
if (!b) return { radius: 0.35, width: 0.25 }
|
|
286
|
+
const size = new Vec3(cx(b.max) - cx(b.min), cy(b.max) - cy(b.min), cz(b.max) - cz(b.min))
|
|
287
|
+
const across = Math.max(size.z, size.y)
|
|
288
|
+
return { radius: across > 0 ? across * 0.5 : 0.35, width: size.x > 0 ? size.x : 0.25 }
|
|
289
|
+
}
|
|
290
|
+
}
|