@driftengine/physics 3.61.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 (136) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +503 -0
  4. package/dist/bodies.d.ts +121 -0
  5. package/dist/bodies.js +298 -0
  6. package/dist/buoyancy.d.ts +46 -0
  7. package/dist/buoyancy.js +45 -0
  8. package/dist/cloth.d.ts +226 -0
  9. package/dist/cloth.js +587 -0
  10. package/dist/collide/aabb.d.ts +10 -0
  11. package/dist/collide/aabb.js +11 -0
  12. package/dist/collide/body.d.ts +202 -0
  13. package/dist/collide/body.js +142 -0
  14. package/dist/collide/eject.d.ts +39 -0
  15. package/dist/collide/eject.js +142 -0
  16. package/dist/collide/index.d.ts +18 -0
  17. package/dist/collide/index.js +5 -0
  18. package/dist/collide/scratch.d.ts +8 -0
  19. package/dist/collide/scratch.js +9 -0
  20. package/dist/collide/segment.d.ts +12 -0
  21. package/dist/collide/segment.js +109 -0
  22. package/dist/collide/sweep.d.ts +3 -0
  23. package/dist/collide/sweep.js +1036 -0
  24. package/dist/colliderSet.d.ts +239 -0
  25. package/dist/colliderSet.js +546 -0
  26. package/dist/compoundContact.d.ts +70 -0
  27. package/dist/compoundContact.js +221 -0
  28. package/dist/controller.d.ts +351 -0
  29. package/dist/controller.js +641 -0
  30. package/dist/cylinder.d.ts +78 -0
  31. package/dist/cylinder.js +294 -0
  32. package/dist/cylinderContact.d.ts +7 -0
  33. package/dist/cylinderContact.js +704 -0
  34. package/dist/decompose.d.ts +105 -0
  35. package/dist/decompose.js +979 -0
  36. package/dist/events.d.ts +43 -0
  37. package/dist/events.js +95 -0
  38. package/dist/executor.d.ts +147 -0
  39. package/dist/executor.js +173 -0
  40. package/dist/faces.d.ts +26 -0
  41. package/dist/faces.js +34 -0
  42. package/dist/fingerprint.d.ts +61 -0
  43. package/dist/fingerprint.js +139 -0
  44. package/dist/groundMotor.d.ts +34 -0
  45. package/dist/groundMotor.js +41 -0
  46. package/dist/heightfieldShape.d.ts +128 -0
  47. package/dist/heightfieldShape.js +328 -0
  48. package/dist/index.d.ts +73 -0
  49. package/dist/index.js +47 -0
  50. package/dist/island.d.ts +55 -0
  51. package/dist/island.js +227 -0
  52. package/dist/islandSolve.d.ts +70 -0
  53. package/dist/islandSolve.js +171 -0
  54. package/dist/islandWorker.d.ts +18 -0
  55. package/dist/islandWorker.js +125 -0
  56. package/dist/joints.d.ts +180 -0
  57. package/dist/joints.js +1094 -0
  58. package/dist/manifold.d.ts +89 -0
  59. package/dist/manifold.js +608 -0
  60. package/dist/mass.d.ts +53 -0
  61. package/dist/mass.js +353 -0
  62. package/dist/meshContact.d.ts +57 -0
  63. package/dist/meshContact.js +393 -0
  64. package/dist/meshShape.d.ts +76 -0
  65. package/dist/meshShape.js +175 -0
  66. package/dist/pairs.d.ts +62 -0
  67. package/dist/pairs.js +154 -0
  68. package/dist/query.d.ts +71 -0
  69. package/dist/query.js +683 -0
  70. package/dist/ragdoll.d.ts +135 -0
  71. package/dist/ragdoll.js +489 -0
  72. package/dist/shape.d.ts +177 -0
  73. package/dist/shape.js +533 -0
  74. package/dist/solver.d.ts +136 -0
  75. package/dist/solver.js +601 -0
  76. package/dist/stage.d.ts +167 -0
  77. package/dist/stage.js +350 -0
  78. package/dist/stallEscape.d.ts +141 -0
  79. package/dist/stallEscape.js +254 -0
  80. package/dist/sweptStep.d.ts +173 -0
  81. package/dist/sweptStep.js +472 -0
  82. package/dist/tree.d.ts +61 -0
  83. package/dist/tree.js +348 -0
  84. package/dist/vehicle.d.ts +154 -0
  85. package/dist/vehicle.js +343 -0
  86. package/dist/workerPool.d.ts +107 -0
  87. package/dist/workerPool.js +239 -0
  88. package/dist/workers.d.ts +37 -0
  89. package/dist/workers.js +35 -0
  90. package/dist/world.d.ts +298 -0
  91. package/dist/world.js +712 -0
  92. package/package.json +55 -0
  93. package/src/bodies.ts +369 -0
  94. package/src/buoyancy.ts +78 -0
  95. package/src/cloth.ts +734 -0
  96. package/src/collide/aabb.ts +28 -0
  97. package/src/collide/body.ts +336 -0
  98. package/src/collide/eject.ts +156 -0
  99. package/src/collide/index.ts +26 -0
  100. package/src/collide/scratch.ts +10 -0
  101. package/src/collide/segment.ts +127 -0
  102. package/src/collide/sweep.ts +1132 -0
  103. package/src/colliderSet.ts +637 -0
  104. package/src/compoundContact.ts +284 -0
  105. package/src/controller.ts +817 -0
  106. package/src/cylinder.ts +340 -0
  107. package/src/cylinderContact.ts +825 -0
  108. package/src/decompose.ts +1198 -0
  109. package/src/events.ts +108 -0
  110. package/src/executor.ts +224 -0
  111. package/src/faces.ts +37 -0
  112. package/src/fingerprint.ts +146 -0
  113. package/src/groundMotor.ts +53 -0
  114. package/src/heightfieldShape.ts +395 -0
  115. package/src/index.ts +164 -0
  116. package/src/island.ts +219 -0
  117. package/src/islandSolve.ts +218 -0
  118. package/src/islandWorker.ts +141 -0
  119. package/src/joints.ts +1367 -0
  120. package/src/manifold.ts +798 -0
  121. package/src/mass.ts +424 -0
  122. package/src/meshContact.ts +443 -0
  123. package/src/meshShape.ts +261 -0
  124. package/src/pairs.ts +169 -0
  125. package/src/query.ts +844 -0
  126. package/src/ragdoll.ts +662 -0
  127. package/src/shape.ts +656 -0
  128. package/src/solver.ts +873 -0
  129. package/src/stage.ts +406 -0
  130. package/src/stallEscape.ts +362 -0
  131. package/src/sweptStep.ts +609 -0
  132. package/src/tree.ts +366 -0
  133. package/src/vehicle.ts +489 -0
  134. package/src/workerPool.ts +283 -0
  135. package/src/workers.ts +38 -0
  136. package/src/world.ts +961 -0
package/src/vehicle.ts ADDED
@@ -0,0 +1,489 @@
1
+ import { createRayHit } from './query.ts';
2
+ import type { QueryFilter, RayHit } from './query.ts';
3
+ import type { PhysicsWorld } from './world.ts';
4
+ import type { GroundProbe, GroundProbeHit } from './controller.ts';
5
+
6
+ /**
7
+ * A raycast vehicle: a chassis body, and wheels that are rays rather than bodies.
8
+ *
9
+ * **Rigid wheels on joints are the obvious alternative and are unstable in practice.** They need
10
+ * joint stiffness and substep counts that nobody wants to pay, which is why Bullet, Godot and Unity
11
+ * all raycast. *What that costs, said where the decision is:* a raycast wheel cannot wedge in
12
+ * geometry, cannot collide with another wheel, and a vehicle on its side behaves as a box rather
13
+ * than as a car. *What would reverse it* is a game about vehicles interlocking with terrain.
14
+ *
15
+ * **The tyre curve is a table the caller supplies.** Pacejka's magic formula is `sin(atan(…))` and
16
+ * both are banned from the tick, so grip is sampled from a curve and linearly interpolated. This is
17
+ * the one place in Track B where the arithmetic contract produced a *better* API than the closed
18
+ * form it replaced: a curve is edited by moving a point, and a magic formula is edited by guessing
19
+ * at a coefficient nobody can picture.
20
+ *
21
+ * Everything here is applied as an impulse at a point on the chassis, so the vehicle's roll, pitch
22
+ * and weight transfer fall out of the solver rather than being modelled separately.
23
+ */
24
+
25
+ /**
26
+ * Grip against slip, as a lookup table.
27
+ *
28
+ * `force[i]` is the fraction of the load available as lateral or longitudinal force at
29
+ * `slip[i]`. Both arrays are the same length and `slip` must ascend. Beyond the last entry the
30
+ * final value is held, which is what a tyre past its peak does.
31
+ */
32
+ export interface TyreCurve {
33
+ readonly slip: Float32Array;
34
+ readonly force: Float32Array;
35
+ }
36
+
37
+ /** A curve that rises to a peak and falls away, which is what a tyre does. */
38
+ export function defaultTyreCurve(peak = 1): TyreCurve {
39
+ return {
40
+ slip: new Float32Array([0, 0.1, 0.2, 0.4, 0.8, 1.5]),
41
+ force: new Float32Array([0, peak * 0.75, peak, peak * 0.9, peak * 0.75, peak * 0.7]),
42
+ };
43
+ }
44
+
45
+ export interface WheelOptions {
46
+ /** Where the suspension is anchored, in the chassis's local frame. */
47
+ x: number;
48
+ y: number;
49
+ z: number;
50
+ radius?: number;
51
+ /** How far the wheel may travel below its anchor. */
52
+ suspensionTravel?: number;
53
+ /** Spring rate, in newtons per metre of compression. */
54
+ stiffness?: number;
55
+ /** Damping, in newtons per metre per second. */
56
+ damping?: number;
57
+ /** Whether steering input turns this wheel. */
58
+ steers?: boolean;
59
+ /** Whether engine and brake force reach it. */
60
+ driven?: boolean;
61
+ }
62
+
63
+ export interface VehicleOptions {
64
+ wheels: readonly WheelOptions[];
65
+ /** Maximum steering angle, as the tangent of the angle rather than the angle. */
66
+ maxSteerTangent?: number;
67
+ /** Newtons at full throttle, shared across driven wheels. */
68
+ engineForce?: number;
69
+ /** Newtons at full brake. */
70
+ brakeForce?: number;
71
+ longitudinal?: TyreCurve;
72
+ lateral?: TyreCurve;
73
+ filter?: QueryFilter;
74
+ /**
75
+ * A surface the wheels can rest on that is not a body.
76
+ *
77
+ * The same seam `CharacterController` takes, and here for the same reason plus one: a consumer
78
+ * whose ground is a height function had a character that stood on it and a car that fell through
79
+ * it, so the world's floor was two things that had to be kept in step by hand. It was reported
80
+ * that way — a hundred lines of ground query written consumer-side, and then written again for
81
+ * everything else that stands on it.
82
+ *
83
+ * Consulted only where the wheel's ray found no body, on the same ruling the controller carries:
84
+ * a body is a thing that is there and a surface is a description of where the ground is.
85
+ */
86
+ ground?: GroundProbe;
87
+ }
88
+
89
+ export interface VehicleInput {
90
+ /** −1 to 1. */
91
+ throttle: number;
92
+ /** 0 to 1. */
93
+ brake: number;
94
+ /** −1 to 1, scaled by `maxSteerTangent`. */
95
+ steer: number;
96
+ }
97
+
98
+ export class Vehicle {
99
+ readonly chassis: number;
100
+ readonly wheelCount: number;
101
+ /** Whether each wheel found ground on the last update. */
102
+ readonly grounded: Uint8Array;
103
+ /** How far each wheel's suspension is compressed, in metres. */
104
+ readonly compression: Float32Array;
105
+ /** Where each wheel's contact is, xyz-packed. Meaningful where `grounded`. */
106
+ readonly contact: Float32Array;
107
+
108
+ private readonly local: Float32Array;
109
+ private readonly radius: Float32Array;
110
+ private readonly travel: Float32Array;
111
+ private readonly stiffness: Float32Array;
112
+ private readonly damping: Float32Array;
113
+ private readonly steers: Uint8Array;
114
+ private readonly driven: Uint8Array;
115
+ private readonly maxSteerTangent: number;
116
+ private readonly engineForce: number;
117
+ private readonly brakeForce: number;
118
+ private readonly longitudinal: TyreCurve;
119
+ private readonly lateral: TyreCurve;
120
+ /**
121
+ * The suspension ray must ignore the car it holds up.
122
+ *
123
+ * A wheel anchor sits *inside* the chassis by construction — that is what a suspension mount is —
124
+ * and a ray starting inside a body reports that body at fraction zero, which is the right answer
125
+ * to the question a line-of-sight test asks and the wrong one here. Left unfiltered, every wheel
126
+ * found the chassis at zero distance, read the suspension as fully compressed, and pushed the car
127
+ * against itself: it sank onto its own chassis and sat there.
128
+ */
129
+ private readonly filter: QueryFilter;
130
+ private readonly hit: RayHit = createRayHit();
131
+ private readonly probe: GroundProbe | null;
132
+ /** Scratch for the probe, claimed once: every wheel asks it every tick. */
133
+ private readonly probed: GroundProbeHit = { y: 0, normalX: 0, normalY: 1, normalZ: 0 };
134
+ /**
135
+ * Every wheel's impulse, gathered before any is applied: `rx ry rz px py pz` per wheel.
136
+ *
137
+ * **Applying them one at a time makes the car curve.** Each wheel then sees a velocity the
138
+ * previous wheel has already changed, so the left front's lateral impulse biases what the right
139
+ * front computes, and the asymmetry accumulates into a yaw rather than cancelling. Measured on a
140
+ * symmetric car under straight throttle: 0.011 metres of drift after 2 metres, 0.64 after 39, and
141
+ * **5.49 after 155** — a curve, not a wobble. Forces at an instant are simultaneous, and gathering
142
+ * them is both the correct physics and the cheaper fix.
143
+ */
144
+ private readonly pending: Float64Array;
145
+
146
+ constructor(chassis: number, options: VehicleOptions) {
147
+ this.chassis = chassis;
148
+ this.wheelCount = options.wheels.length;
149
+ const n = this.wheelCount;
150
+ this.local = new Float32Array(n * 3);
151
+ this.radius = new Float32Array(n);
152
+ this.travel = new Float32Array(n);
153
+ this.stiffness = new Float32Array(n);
154
+ this.damping = new Float32Array(n);
155
+ this.steers = new Uint8Array(n);
156
+ this.driven = new Uint8Array(n);
157
+ this.grounded = new Uint8Array(n);
158
+ this.compression = new Float32Array(n);
159
+ this.contact = new Float32Array(n * 3);
160
+ options.wheels.forEach((w, i) => {
161
+ this.local[i * 3] = w.x;
162
+ this.local[i * 3 + 1] = w.y;
163
+ this.local[i * 3 + 2] = w.z;
164
+ this.radius[i] = w.radius ?? 0.35;
165
+ this.travel[i] = w.suspensionTravel ?? 0.3;
166
+ this.stiffness[i] = w.stiffness ?? 40000;
167
+ this.damping[i] = w.damping ?? 4000;
168
+ this.steers[i] = w.steers ? 1 : 0;
169
+ this.driven[i] = w.driven ? 1 : 0;
170
+ });
171
+ this.maxSteerTangent = options.maxSteerTangent ?? 0.6;
172
+ this.engineForce = options.engineForce ?? 6000;
173
+ this.brakeForce = options.brakeForce ?? 9000;
174
+ this.longitudinal = options.longitudinal ?? defaultTyreCurve(1.2);
175
+ this.lateral = options.lateral ?? defaultTyreCurve(1.4);
176
+ this.filter = { ...options.filter, ignore: chassis };
177
+ this.probe = options.ground ?? null;
178
+ this.pending = new Float64Array(n * 6);
179
+ }
180
+
181
+ /** Advance the vehicle one fixed tick. Call before `world.step`. */
182
+ update(world: PhysicsWorld, dt: number, input: VehicleInput): void {
183
+ const body = this.chassis;
184
+ const bodies = world.bodies;
185
+ this.pending.fill(0);
186
+ let drivenCount = 0;
187
+ for (let i = 0; i < this.wheelCount; i++) if (this.driven[i]) drivenCount++;
188
+ const perWheelDrive = drivenCount > 0 ? (input.throttle * this.engineForce) / drivenCount : 0;
189
+
190
+ for (let i = 0; i < this.wheelCount; i++) {
191
+ rotate(bodies, body, this.local, i * 3, ANCHOR);
192
+ const ax = (bodies.posX[body] ?? 0) + ANCHOR[0];
193
+ const ay = (bodies.posY[body] ?? 0) + ANCHOR[1];
194
+ const az = (bodies.posZ[body] ?? 0) + ANCHOR[2];
195
+
196
+ // The suspension looks along the chassis's own down, not world down.
197
+ axis(bodies, body, 0, -1, 0, DOWN);
198
+ const reach = (this.travel[i] ?? 0) + (this.radius[i] ?? 0);
199
+ if (
200
+ !world.raycast(ax, ay, az, DOWN[0], DOWN[1], DOWN[2], reach, this.hit, this.filter) &&
201
+ !this.probeUnder(ax, ay, az, reach)
202
+ ) {
203
+ this.grounded[i] = 0;
204
+ this.compression[i] = 0;
205
+ continue;
206
+ }
207
+ this.grounded[i] = 1;
208
+ this.contact[i * 3] = this.hit.x;
209
+ this.contact[i * 3 + 1] = this.hit.y;
210
+ this.contact[i * 3 + 2] = this.hit.z;
211
+
212
+ const distance = this.hit.fraction * reach;
213
+ /*
214
+ * Compression is capped at the travel. Past that the wheel is bottomed out and the *chassis*
215
+ * takes the load through its own collider, which is what a bump stop is.
216
+ *
217
+ * **No test here reaches it, and that is worth saying rather than leaving somebody to look.**
218
+ * With wheels mounted inside the chassis — the ordinary arrangement — the chassis collider
219
+ * meets the ground before the suspension can bottom out, so removing this line changes a drop
220
+ * from six metres by a centimetre and nothing else. It stays because a vehicle with outboard
221
+ * mounts *can* reach it, and an uncapped spring then answers a hard landing with several
222
+ * times the car's weight.
223
+ */
224
+ let compression = reach - distance;
225
+ const bumpStop = this.travel[i] ?? 0;
226
+ if (compression > bumpStop) compression = bumpStop;
227
+ this.compression[i] = compression;
228
+
229
+ // The anchor's velocity, which is what the damper answers to.
230
+ pointVelocity(bodies, body, ANCHOR, VEL);
231
+ const alongDown = VEL[0] * DOWN[0] + VEL[1] * DOWN[1] + VEL[2] * DOWN[2];
232
+ /*
233
+ * **The damper adds to the load while the spring is compressing.** `alongDown` is positive
234
+ * when the anchor is moving toward the ground, which is exactly when compression is
235
+ * increasing, so a damper opposing that motion pushes *up*. Subtracting it instead — which is
236
+ * what `k·x − c·v` reads like if `v` is taken as the wrong sign — leaves the spring undamped
237
+ * and clamps the load to zero on the way down: the car fell, bounced on a pure spring, and
238
+ * launched itself upside down.
239
+ */
240
+ let load = (this.stiffness[i] ?? 0) * compression + (this.damping[i] ?? 0) * alongDown;
241
+ if (load < 0) load = 0;
242
+ // Along the surface normal rather than along the ray, so a slope pushes sideways too.
243
+ this.gather(
244
+ i,
245
+ ANCHOR,
246
+ this.hit.nx * load * dt,
247
+ this.hit.ny * load * dt,
248
+ this.hit.nz * load * dt,
249
+ );
250
+
251
+ // The wheel's own frame: forward turned by steering, and lateral across it.
252
+ const steer = this.steers[i] ? input.steer * this.maxSteerTangent : 0;
253
+ axis(bodies, body, steer, 0, 1, FORWARD);
254
+ normalise(FORWARD);
255
+ axis(bodies, body, 1, 0, -steer, SIDE);
256
+ normalise(SIDE);
257
+ // Flattened onto the surface, so grip acts along the ground rather than into it.
258
+ project(FORWARD, this.hit.nx, this.hit.ny, this.hit.nz);
259
+ project(SIDE, this.hit.nx, this.hit.ny, this.hit.nz);
260
+ normalise(FORWARD);
261
+ normalise(SIDE);
262
+
263
+ pointVelocity(bodies, body, ANCHOR, VEL);
264
+ const vForward = VEL[0] * FORWARD[0] + VEL[1] * FORWARD[1] + VEL[2] * FORWARD[2];
265
+ const vSide = VEL[0] * SIDE[0] + VEL[1] * SIDE[1] + VEL[2] * SIDE[2];
266
+ const speed = Math.sqrt(vForward * vForward + vSide * vSide);
267
+
268
+ /*
269
+ * Slip is the sideways or braking velocity against the speed the wheel is going, which is what
270
+ * a tyre curve is indexed by. At a standstill the ratio is undefined, so the slip is taken as
271
+ * the velocity itself — a stationary car with a sideways nudge should still resist it.
272
+ */
273
+ const denom = speed > 1 ? speed : 1;
274
+ const lateralGrip = sampleTyreCurve(this.lateral, Math.abs(vSide) / denom) * load;
275
+ const sideImpulse = clampMagnitude(-vSide * massAt(bodies, body), lateralGrip * dt);
276
+ this.gather(i, ANCHOR, SIDE[0] * sideImpulse, SIDE[1] * sideImpulse, SIDE[2] * sideImpulse);
277
+
278
+ let forwardForce = this.driven[i] ? perWheelDrive : 0;
279
+ if (input.brake > 0) {
280
+ const brake = input.brake * this.brakeForce;
281
+ forwardForce += vForward > 0 ? -brake : brake;
282
+ }
283
+ const traction = sampleTyreCurve(this.longitudinal, Math.abs(vForward) / denom) * load;
284
+ const limit = traction > 0 ? traction : load;
285
+ const drive = clampMagnitude(forwardForce, limit);
286
+ this.gather(
287
+ i,
288
+ ANCHOR,
289
+ FORWARD[0] * drive * dt,
290
+ FORWARD[1] * drive * dt,
291
+ FORWARD[2] * drive * dt,
292
+ );
293
+ }
294
+
295
+ // Now apply, all from the state every wheel was measured against.
296
+ for (let i = 0; i < this.wheelCount; i++) {
297
+ const at = i * 6;
298
+ ANCHOR[0] = this.pending[at];
299
+ ANCHOR[1] = this.pending[at + 1];
300
+ ANCHOR[2] = this.pending[at + 2];
301
+ applyImpulse(
302
+ bodies,
303
+ body,
304
+ ANCHOR,
305
+ this.pending[at + 3],
306
+ this.pending[at + 4],
307
+ this.pending[at + 5],
308
+ );
309
+ }
310
+ world.wakeIsland(body);
311
+ }
312
+
313
+ /** Add an impulse at a wheel's anchor to what will be applied at the end of the pass. */
314
+ /**
315
+ * Rest a wheel on the analytic surface, filling `hit` as a ray against a body would have.
316
+ *
317
+ * Everything downstream — the spring, the damper, the tyre frame flattened onto the ground —
318
+ * reads `hit`, so filling it is the whole of the wiring and no grip code learns that a surface
319
+ * exists.
320
+ *
321
+ * **The column is the anchor's own and the drop is measured along the chassis's down**, which is
322
+ * exact while the chassis is upright and off by its tilt when it is not: the true contact sits a
323
+ * little to the side of the column that was sampled, and on a slope that column is a little
324
+ * higher or lower than the contact. At the lean angles a car spends its life at the error is
325
+ * millimetres, and the alternative is solving a ray against an arbitrary height function, which
326
+ * is an iteration this package does not allow itself. A vehicle on its roof finds no ground here
327
+ * at all, because there is no downward intersection to find, which is the honest answer.
328
+ */
329
+ private probeUnder(ax: number, ay: number, az: number, reach: number): boolean {
330
+ if (this.probe === null || DOWN[1] >= 0) return false;
331
+ if (!this.probe.sample(ax, az, this.probed, ay)) return false;
332
+ const distance = (this.probed.y - ay) / DOWN[1];
333
+ if (!(distance >= 0) || distance > reach) return false;
334
+ this.hit.body = -1;
335
+ this.hit.fraction = distance / reach;
336
+ this.hit.x = ax + DOWN[0] * distance;
337
+ this.hit.y = ay + DOWN[1] * distance;
338
+ this.hit.z = az + DOWN[2] * distance;
339
+ this.hit.nx = this.probed.normalX;
340
+ this.hit.ny = this.probed.normalY;
341
+ this.hit.nz = this.probed.normalZ;
342
+ return true;
343
+ }
344
+
345
+ private gather(i: number, r: Float64Array, px: number, py: number, pz: number): void {
346
+ const at = i * 6;
347
+ this.pending[at] = r[0];
348
+ this.pending[at + 1] = r[1];
349
+ this.pending[at + 2] = r[2];
350
+ this.pending[at + 3] += px;
351
+ this.pending[at + 4] += py;
352
+ this.pending[at + 5] += pz;
353
+ }
354
+ }
355
+
356
+ /** Sample a curve at `at`, holding the last value beyond its end. */
357
+ export function sampleTyreCurve(curve: TyreCurve, at: number): number {
358
+ const n = curve.slip.length;
359
+ if (n === 0) return 0;
360
+ if (at <= (curve.slip[0] ?? 0)) return curve.force[0] ?? 0;
361
+ for (let i = 1; i < n; i++) {
362
+ const s1 = curve.slip[i] ?? 0;
363
+ if (at <= s1) {
364
+ const s0 = curve.slip[i - 1] ?? 0;
365
+ const span = s1 - s0;
366
+ const t = span > 0 ? (at - s0) / span : 0;
367
+ const f0 = curve.force[i - 1] ?? 0;
368
+ return f0 + ((curve.force[i] ?? 0) - f0) * t;
369
+ }
370
+ }
371
+ return curve.force[n - 1] ?? 0;
372
+ }
373
+
374
+ const ANCHOR = new Float64Array(3);
375
+ const DOWN = new Float64Array(3);
376
+ const FORWARD = new Float64Array(3);
377
+ const SIDE = new Float64Array(3);
378
+ const VEL = new Float64Array(3);
379
+ const INV_I = new Float32Array(6);
380
+
381
+ function clampMagnitude(v: number, limit: number): number {
382
+ if (v > limit) return limit;
383
+ if (v < -limit) return -limit;
384
+ return v;
385
+ }
386
+
387
+ /** A representative mass at the anchor, for turning a wanted velocity change into an impulse. */
388
+ function massAt(bodies: PhysicsWorld['bodies'], body: number): number {
389
+ const im = bodies.invMass[body] ?? 0;
390
+ return im > 0 ? 1 / im : 0;
391
+ }
392
+
393
+ function normalise(v: Float64Array): void {
394
+ const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
395
+ if (len === 0) return;
396
+ v[0] /= len;
397
+ v[1] /= len;
398
+ v[2] /= len;
399
+ }
400
+
401
+ /** Remove the component of `v` along a normal, leaving it in the surface. */
402
+ function project(v: Float64Array, nx: number, ny: number, nz: number): void {
403
+ const d = v[0] * nx + v[1] * ny + v[2] * nz;
404
+ v[0] -= nx * d;
405
+ v[1] -= ny * d;
406
+ v[2] -= nz * d;
407
+ }
408
+
409
+ function rotate(
410
+ bodies: PhysicsWorld['bodies'],
411
+ i: number,
412
+ src: Float32Array,
413
+ at: number,
414
+ out: Float64Array,
415
+ ): void {
416
+ const x = src[at] ?? 0;
417
+ const y = src[at + 1] ?? 0;
418
+ const z = src[at + 2] ?? 0;
419
+ const qx = bodies.rotX[i] ?? 0;
420
+ const qy = bodies.rotY[i] ?? 0;
421
+ const qz = bodies.rotZ[i] ?? 0;
422
+ const qw = bodies.rotW[i] ?? 1;
423
+ const tx = 2 * (qy * z - qz * y);
424
+ const ty = 2 * (qz * x - qx * z);
425
+ const tz = 2 * (qx * y - qy * x);
426
+ out[0] = x + qw * tx + (qy * tz - qz * ty);
427
+ out[1] = y + qw * ty + (qz * tx - qx * tz);
428
+ out[2] = z + qw * tz + (qx * ty - qy * tx);
429
+ }
430
+
431
+ /** A direction in the body's frame, rotated into world. */
432
+ function axis(
433
+ bodies: PhysicsWorld['bodies'],
434
+ i: number,
435
+ x: number,
436
+ y: number,
437
+ z: number,
438
+ out: Float64Array,
439
+ ): void {
440
+ const qx = bodies.rotX[i] ?? 0;
441
+ const qy = bodies.rotY[i] ?? 0;
442
+ const qz = bodies.rotZ[i] ?? 0;
443
+ const qw = bodies.rotW[i] ?? 1;
444
+ const tx = 2 * (qy * z - qz * y);
445
+ const ty = 2 * (qz * x - qx * z);
446
+ const tz = 2 * (qx * y - qy * x);
447
+ out[0] = x + qw * tx + (qy * tz - qz * ty);
448
+ out[1] = y + qw * ty + (qz * tx - qx * tz);
449
+ out[2] = z + qw * tz + (qx * ty - qy * tx);
450
+ }
451
+
452
+ function pointVelocity(
453
+ bodies: PhysicsWorld['bodies'],
454
+ i: number,
455
+ r: Float64Array,
456
+ out: Float64Array,
457
+ ): void {
458
+ const wx = bodies.angX[i] ?? 0;
459
+ const wy = bodies.angY[i] ?? 0;
460
+ const wz = bodies.angZ[i] ?? 0;
461
+ out[0] = (bodies.velX[i] ?? 0) + (wy * r[2] - wz * r[1]);
462
+ out[1] = (bodies.velY[i] ?? 0) + (wz * r[0] - wx * r[2]);
463
+ out[2] = (bodies.velZ[i] ?? 0) + (wx * r[1] - wy * r[0]);
464
+ }
465
+
466
+ function applyImpulse(
467
+ bodies: PhysicsWorld['bodies'],
468
+ i: number,
469
+ r: Float64Array,
470
+ px: number,
471
+ py: number,
472
+ pz: number,
473
+ ): void {
474
+ const im = bodies.invMass[i] ?? 0;
475
+ if (im === 0) return;
476
+ bodies.velX[i] = (bodies.velX[i] ?? 0) + px * im;
477
+ bodies.velY[i] = (bodies.velY[i] ?? 0) + py * im;
478
+ bodies.velZ[i] = (bodies.velZ[i] ?? 0) + pz * im;
479
+ bodies.worldInverseInertia(i, INV_I);
480
+ const tx = r[1] * pz - r[2] * py;
481
+ const ty = r[2] * px - r[0] * pz;
482
+ const tz = r[0] * py - r[1] * px;
483
+ bodies.angX[i] =
484
+ (bodies.angX[i] ?? 0) + (INV_I[0] ?? 0) * tx + (INV_I[3] ?? 0) * ty + (INV_I[4] ?? 0) * tz;
485
+ bodies.angY[i] =
486
+ (bodies.angY[i] ?? 0) + (INV_I[3] ?? 0) * tx + (INV_I[1] ?? 0) * ty + (INV_I[5] ?? 0) * tz;
487
+ bodies.angZ[i] =
488
+ (bodies.angZ[i] ?? 0) + (INV_I[4] ?? 0) * tx + (INV_I[5] ?? 0) * ty + (INV_I[2] ?? 0) * tz;
489
+ }