lecodes-sdk 0.20.0 → 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.
Files changed (114) hide show
  1. package/dist/global.d.ts +48 -5
  2. package/dist/inject.js +361 -260
  3. package/dist/types/audio/Bus.d.ts +45 -0
  4. package/dist/types/audio/Sound.d.ts +28 -0
  5. package/dist/types/audio/Voice.d.ts +27 -0
  6. package/dist/types/audio/audio.d.ts +83 -0
  7. package/dist/types/audio/support.d.ts +1 -0
  8. package/dist/types/canvas/Canvas.d.ts +2 -0
  9. package/dist/types/gl/DecalSet.d.ts +148 -0
  10. package/dist/types/gl/Geometry.d.ts +17 -0
  11. package/dist/types/gl/Light.d.ts +7 -0
  12. package/dist/types/gl/Lightmap.d.ts +9 -0
  13. package/dist/types/gl/Material.d.ts +90 -2
  14. package/dist/types/gl/Mesh.d.ts +18 -1
  15. package/dist/types/gl/Model.d.ts +34 -0
  16. package/dist/types/gl/Particles.d.ts +13 -0
  17. package/dist/types/gl/Scene.d.ts +23 -0
  18. package/dist/types/gl/Texture.d.ts +29 -1
  19. package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
  20. package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
  21. package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
  22. package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
  23. package/dist/types/gl/animation/core.d.ts +15 -10
  24. package/dist/types/gl/audio/AudioSource.d.ts +60 -0
  25. package/dist/types/gl/audio/AudioZone.d.ts +32 -0
  26. package/dist/types/gl/audio/SceneAudio.d.ts +11 -0
  27. package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
  28. package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
  29. package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
  30. package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
  31. package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
  32. package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
  33. package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
  34. package/dist/types/gl/state.d.ts +0 -1
  35. package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
  36. package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
  37. package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
  38. package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
  39. package/dist/types/inject.d.ts +33 -21
  40. package/dist/types/runtime/files.d.ts +24 -1
  41. package/dist/types/runtime/input.d.ts +11 -0
  42. package/dist/types/scene/defineScene.d.ts +10 -3
  43. package/dist/types/ui/UIImage.d.ts +15 -5
  44. package/dist/types.json +1 -1
  45. package/package.json +1 -1
  46. package/prompts/README.md +142 -142
  47. package/prompts/dist/2d-game.md +408 -197
  48. package/prompts/dist/3d-app.md +491 -166
  49. package/prompts/dist/ar-app.md +373 -163
  50. package/prompts/dist/design.md +83 -87
  51. package/prompts/dist/ui-app.md +325 -136
  52. package/src/audio/Bus.ts +102 -0
  53. package/src/audio/Sound.ts +96 -0
  54. package/src/audio/Voice.ts +102 -0
  55. package/src/audio/audio.ts +161 -0
  56. package/src/audio/support.ts +6 -0
  57. package/src/bridges.d.ts +279 -32
  58. package/src/canvas/Canvas.ts +21 -0
  59. package/src/compile/__tests__/compile.test.ts +11 -0
  60. package/src/compile/compileProject.ts +30 -15
  61. package/src/compile/header.ts +6 -3
  62. package/src/compile/index.ts +4 -0
  63. package/src/compile/sceneEditor.ts +42 -1
  64. package/src/core/Aspect.ts +33 -8
  65. package/src/g2/Scene2D.ts +7 -0
  66. package/src/gl/CameraPlace.ts +52 -52
  67. package/src/gl/DecalSet.ts +360 -0
  68. package/src/gl/Geometry.ts +348 -279
  69. package/src/gl/Light.ts +16 -0
  70. package/src/gl/Lightmap.ts +35 -7
  71. package/src/gl/Material.ts +173 -4
  72. package/src/gl/Mesh.ts +120 -83
  73. package/src/gl/Model.ts +33 -1
  74. package/src/gl/Node.ts +1 -1
  75. package/src/gl/Particles.ts +21 -3
  76. package/src/gl/Scene.ts +41 -7
  77. package/src/gl/Texture.ts +43 -3
  78. package/src/gl/animation/AnimationClip.ts +43 -20
  79. package/src/gl/animation/Animator.ts +4 -3
  80. package/src/gl/animation/DynamicBone.ts +459 -0
  81. package/src/gl/{IK.ts → animation/IK.ts} +4 -4
  82. package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
  83. package/src/gl/animation/core.ts +20 -15
  84. package/src/gl/audio/AudioSource.ts +113 -0
  85. package/src/gl/audio/AudioZone.ts +75 -0
  86. package/src/gl/audio/SceneAudio.ts +26 -0
  87. package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
  88. package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
  89. package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
  90. package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
  91. package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +272 -270
  92. package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
  93. package/src/gl/{Trigger.ts → physics/Trigger.ts} +2 -2
  94. package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
  95. package/src/gl/scenarios.ts +291 -291
  96. package/src/gl/state.ts +1 -1
  97. package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
  98. package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
  99. package/src/gl/vehicle/Vehicle.ts +666 -0
  100. package/src/gl/vehicle/Wheel.ts +290 -0
  101. package/src/inject.ts +226 -212
  102. package/src/runtime/files.ts +32 -2
  103. package/src/runtime/input.ts +6 -1
  104. package/src/scene/defineScene.ts +26 -10
  105. package/src/scene/gizmos.ts +148 -148
  106. package/src/scene/level.ts +2 -2
  107. package/src/ui/UIImage.ts +21 -7
  108. package/dist/types/gl/Gearbox.d.ts +0 -86
  109. package/dist/types/gl/Vehicle.d.ts +0 -191
  110. package/dist/types/gl/Wheel.d.ts +0 -95
  111. package/src/gl/Gearbox.ts +0 -212
  112. package/src/gl/Vehicle.ts +0 -473
  113. package/src/gl/Wheel.ts +0 -240
  114. /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
+ }