@series-inc/rundot-syncplay 5.23.0-beta.7 → 5.23.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 (75) hide show
  1. package/README.md +116 -15
  2. package/dist/browser.d.ts +87 -55
  3. package/dist/browser.js +47 -52
  4. package/dist/certification.d.ts +32 -3
  5. package/dist/certification.js +88 -3
  6. package/dist/cjs/browser.js +259 -87
  7. package/dist/cjs/certification.js +87 -2
  8. package/dist/cjs/collision.js +58 -0
  9. package/dist/cjs/creator.js +2 -1
  10. package/dist/cjs/engine-complete.js +213 -3
  11. package/dist/cjs/engine-parity-matrix.js +19 -4
  12. package/dist/cjs/index.js +199 -132
  13. package/dist/cjs/math.js +342 -15
  14. package/dist/cjs/movement3d.js +230 -1
  15. package/dist/cjs/node.js +5 -1
  16. package/dist/cjs/noise.js +58 -0
  17. package/dist/cjs/physics-cert.js +147 -6
  18. package/dist/cjs/physics2d.js +10 -4
  19. package/dist/cjs/physics3d-joints.js +637 -0
  20. package/dist/cjs/physics3d-shared.js +288 -0
  21. package/dist/cjs/physics3d-solver.js +1351 -0
  22. package/dist/cjs/physics3d-vehicle.js +467 -0
  23. package/dist/cjs/physics3d.js +1057 -223
  24. package/dist/cjs/random.js +49 -0
  25. package/dist/cjs/replay-bundle.js +127 -20
  26. package/dist/cjs/sample-scenes.js +3 -0
  27. package/dist/cjs/sdk-offline.js +6 -32
  28. package/dist/cjs/sdk-session.js +156 -0
  29. package/dist/cjs/session-build.js +42 -0
  30. package/dist/cjs/testing.js +107 -0
  31. package/dist/cli.js +40 -12
  32. package/dist/collision.d.ts +12 -0
  33. package/dist/collision.js +52 -0
  34. package/dist/creator.d.ts +1 -1
  35. package/dist/creator.js +1 -1
  36. package/dist/engine-complete.js +214 -4
  37. package/dist/engine-parity-matrix.js +19 -4
  38. package/dist/index.d.ts +94 -89
  39. package/dist/index.js +45 -56
  40. package/dist/math.d.ts +59 -6
  41. package/dist/math.js +342 -15
  42. package/dist/movement3d.d.ts +73 -0
  43. package/dist/movement3d.js +229 -1
  44. package/dist/node.d.ts +4 -0
  45. package/dist/node.js +2 -0
  46. package/dist/noise.d.ts +7 -0
  47. package/dist/noise.js +51 -0
  48. package/dist/physics-cert.d.ts +1 -1
  49. package/dist/physics-cert.js +147 -6
  50. package/dist/physics2d.js +10 -4
  51. package/dist/physics3d-joints.d.ts +94 -0
  52. package/dist/physics3d-joints.js +634 -0
  53. package/dist/physics3d-shared.d.ts +72 -0
  54. package/dist/physics3d-shared.js +257 -0
  55. package/dist/physics3d-solver.d.ts +197 -0
  56. package/dist/physics3d-solver.js +1346 -0
  57. package/dist/physics3d-vehicle.d.ts +84 -0
  58. package/dist/physics3d-vehicle.js +463 -0
  59. package/dist/physics3d.d.ts +108 -8
  60. package/dist/physics3d.js +1006 -177
  61. package/dist/random.d.ts +7 -0
  62. package/dist/random.js +49 -0
  63. package/dist/replay-bundle.d.ts +40 -1
  64. package/dist/replay-bundle.js +126 -20
  65. package/dist/sample-scenes.js +3 -0
  66. package/dist/sdk-offline.js +6 -32
  67. package/dist/sdk-session.d.ts +47 -0
  68. package/dist/sdk-session.js +153 -0
  69. package/dist/session-build.d.ts +21 -0
  70. package/dist/session-build.js +39 -0
  71. package/dist/testing.d.ts +52 -0
  72. package/dist/testing.js +45 -0
  73. package/dist/tools/vite-plugin.d.ts +8 -0
  74. package/dist/tools/vite-plugin.js +63 -9
  75. package/package.json +11 -3
@@ -0,0 +1,1346 @@
1
+ /**
2
+ * Deterministic rigid-body solver core (geometry half): SAT + face-clipping
3
+ * manifolds for boxes, closed-form manifolds for sphere/capsule pairs,
4
+ * one-sided triangle contacts for dynamic-shape-vs-static-mesh, and compound
5
+ * decomposition. The velocity/position solver, contact cache matching, islands
6
+ * and sleeping live alongside in this module (see solveRigidStep).
7
+ *
8
+ * Numeric regime: unquantized solver math via the shared kernel; every constant
9
+ * here is load-bearing (each was earned by a failure mode in the validated
10
+ * spike) — do not "simplify" them without re-running the stack/tumble/
11
+ * determinism scenarios.
12
+ */
13
+ import { addVec3, clampNumber as clamp, crossVec3, dotVec3, integrateQuat, mulMat3Vec, quatMultiply, quatToMat3, rotatePointByQuat, rotatePointByQuatInverse, scaleVec3, solverSqrt, subVec3, worldInvInertia, } from './physics3d-shared.js';
14
+ const ZERO = { x: 0, y: 0, z: 0 };
15
+ /** Expand a compound body into primitive sub-bodies in world space. */
16
+ function primitiveViews(body) {
17
+ const shape = body.shape;
18
+ if (shape.kind === 'compound') {
19
+ return shape.children.map((child) => ({
20
+ index: body.index,
21
+ p: addVec3(body.p, rotatePointByQuat(body.q, child.offset)),
22
+ q: quatMultiply(body.q, child.q),
23
+ shape: child.shape,
24
+ }));
25
+ }
26
+ /* istanbul ignore next -- mesh bodies are routed to meshManifold before primitiveViews is ever reached; this branch only satisfies the SolverShape union. */
27
+ if (shape.kind === 'mesh') {
28
+ return [];
29
+ }
30
+ return [{ index: body.index, p: body.p, q: body.q, shape }];
31
+ }
32
+ // ---------- public entry ----------
33
+ /**
34
+ * Generate a contact manifold between two bodies. Normal points from `a` to `b`.
35
+ * Returns undefined when the shapes are separated. `speculativeMargin` keeps
36
+ * contact points with `penetration >= -speculativeMargin` (CCD, Task 6);
37
+ * default 0 keeps only real (penetrating) points.
38
+ */
39
+ function asPrimitive(shape) {
40
+ /* istanbul ignore next -- callers guard compound, and mesh bodies are routed to meshManifold first; type guard only. */
41
+ if (shape.kind === 'compound' || shape.kind === 'mesh') {
42
+ throw new Error('asPrimitive requires a primitive shape');
43
+ }
44
+ return shape;
45
+ }
46
+ export function generateManifold(a, b, speculativeMargin = 0) {
47
+ // mesh is one-sided static geometry; the other shape's primitives collide
48
+ // against each triangle.
49
+ if (a.shape.kind === 'mesh' || b.shape.kind === 'mesh') {
50
+ return meshManifold(a, b, speculativeMargin);
51
+ }
52
+ if (a.shape.kind !== 'compound' && b.shape.kind !== 'compound') {
53
+ // single primitive pair — no view arrays, no union accumulation
54
+ const sub = collidePrimitives({ index: a.index, p: a.p, q: a.q, shape: asPrimitive(a.shape) }, { index: b.index, p: b.p, q: b.q, shape: asPrimitive(b.shape) }, a.p, b.p, speculativeMargin, 0);
55
+ if (sub === undefined) {
56
+ return undefined;
57
+ }
58
+ return { a: a.index, b: b.index, normal: sub.normal, points: reducePoints([...sub.points]) };
59
+ }
60
+ const primsA = primitiveViews(a);
61
+ const primsB = primitiveViews(b);
62
+ const points = [];
63
+ let normalAccum;
64
+ let deepest = -Infinity;
65
+ let childOffset = 0;
66
+ for (const pa of primsA) {
67
+ for (const pb of primsB) {
68
+ const sub = collidePrimitives(pa, pb, a.p, b.p, speculativeMargin, childOffset);
69
+ childOffset += 64;
70
+ if (sub === undefined) {
71
+ continue;
72
+ }
73
+ for (const point of sub.points) {
74
+ points.push(point);
75
+ }
76
+ // adopt the normal from the deepest sub-manifold for a stable union normal
77
+ const subDeepest = Math.max(...sub.points.map((point) => point.penetration));
78
+ if (subDeepest > deepest) {
79
+ deepest = subDeepest;
80
+ normalAccum = sub.normal;
81
+ }
82
+ }
83
+ }
84
+ if (points.length === 0 || normalAccum === undefined) {
85
+ return undefined;
86
+ }
87
+ const reduced = reducePoints(points);
88
+ return { a: a.index, b: b.index, normal: normalAccum, points: reduced };
89
+ }
90
+ function reducePoints(points) {
91
+ if (points.length <= 4) {
92
+ return [...points].sort((l, r) => l.feature - r.feature);
93
+ }
94
+ const byDepth = [...points].sort((l, r) => r.penetration - l.penetration || l.feature - r.feature);
95
+ const kept = byDepth.slice(0, 4);
96
+ return kept.sort((l, r) => l.feature - r.feature);
97
+ }
98
+ // ---------- primitive dispatch ----------
99
+ function collidePrimitives(a, b, centerA, centerB, speculativeMargin, featureBase) {
100
+ const ka = a.shape.kind;
101
+ const kb = b.shape.kind;
102
+ if (ka === 'box' && kb === 'box') {
103
+ return collideBoxes(a, b, centerA, centerB, speculativeMargin, featureBase);
104
+ }
105
+ if (ka === 'sphere' && kb === 'sphere') {
106
+ return sphereSphere(a, b, centerA, centerB, speculativeMargin, featureBase, false);
107
+ }
108
+ if (ka === 'sphere' && kb === 'box') {
109
+ return sphereBox(a, b, centerA, centerB, speculativeMargin, featureBase, false);
110
+ }
111
+ if (ka === 'box' && kb === 'sphere') {
112
+ return sphereBox(b, a, centerB, centerA, speculativeMargin, featureBase, true);
113
+ }
114
+ if (ka === 'sphere' && kb === 'capsule') {
115
+ return sphereCapsule(a, b, centerA, centerB, speculativeMargin, featureBase, false);
116
+ }
117
+ if (ka === 'capsule' && kb === 'sphere') {
118
+ return sphereCapsule(b, a, centerB, centerA, speculativeMargin, featureBase, true);
119
+ }
120
+ if (ka === 'capsule' && kb === 'capsule') {
121
+ return capsuleCapsule(a, b, centerA, centerB, speculativeMargin, featureBase);
122
+ }
123
+ if (ka === 'capsule' && kb === 'box') {
124
+ return capsuleBox(a, b, centerA, centerB, speculativeMargin, featureBase, false);
125
+ }
126
+ /* istanbul ignore next -- when a box operand reaches this point kb is necessarily 'capsule' (box-box/box-sphere returned earlier), so the second condition is always true. */
127
+ if (ka === 'box' && kb === 'capsule') {
128
+ return capsuleBox(b, a, centerB, centerA, speculativeMargin, featureBase, true);
129
+ }
130
+ /* istanbul ignore next -- every primitive shape pairing is enumerated above; this is an exhaustiveness guard. */
131
+ return undefined;
132
+ }
133
+ // ---------- sphere / capsule closed forms ----------
134
+ function radiusOf(shape) {
135
+ /* istanbul ignore next -- radiusOf is only called for sphere/capsule operands; the box arm is defensive. */
136
+ return shape.kind === 'box' ? 0 : shape.radius;
137
+ }
138
+ /**
139
+ * Build a single contact point from two world points on the surfaces of two
140
+ * spheres of the given radii, with normal from centerA-sphere to centerB-sphere.
141
+ * `flip` swaps which passed body is A vs B relative to the caller's frame.
142
+ */
143
+ function pointContact(posA, posB, radiusA, radiusB, centerA, centerB, speculativeMargin, feature, flip) {
144
+ const delta = subVec3(posB, posA);
145
+ const distSq = dotVec3(delta, delta);
146
+ const dist = solverSqrt(distSq);
147
+ const sumR = radiusA + radiusB;
148
+ const penetration = sumR - dist;
149
+ if (penetration < -speculativeMargin) {
150
+ return undefined;
151
+ }
152
+ let normal;
153
+ if (dist > 1e-9) {
154
+ normal = scaleVec3(delta, 1 / dist);
155
+ }
156
+ else {
157
+ normal = { x: 0, y: 1, z: 0 };
158
+ }
159
+ // contact point midway between the two surfaces
160
+ const surfaceA = addVec3(posA, scaleVec3(normal, radiusA));
161
+ const surfaceB = subVec3(posB, scaleVec3(normal, radiusB));
162
+ const mid = scaleVec3(addVec3(surfaceA, surfaceB), 0.5);
163
+ const outNormal = flip ? scaleVec3(normal, -1) : normal;
164
+ const aCenter = flip ? centerB : centerA;
165
+ const bCenter = flip ? centerA : centerB;
166
+ return {
167
+ a: 0,
168
+ b: 0,
169
+ normal: outNormal,
170
+ points: [{ rA: subVec3(mid, aCenter), rB: subVec3(mid, bCenter), penetration, feature }],
171
+ };
172
+ }
173
+ function sphereSphere(a, b, ca, cb, margin, feature, flip) {
174
+ return pointContact(a.p, b.p, radiusOf(a.shape), radiusOf(b.shape), ca, cb, margin, feature, flip);
175
+ }
176
+ function closestPointOnBox(box, worldPoint) {
177
+ /* istanbul ignore next -- callers always pass a box; type guard only. */
178
+ if (box.shape.kind !== 'box') {
179
+ throw new Error('closestPointOnBox requires a box');
180
+ }
181
+ const half = box.shape.half;
182
+ const local = rotatePointByQuatInverse(box.q, subVec3(worldPoint, box.p));
183
+ const clampedLocal = {
184
+ x: clamp(local.x, -half.x, half.x),
185
+ y: clamp(local.y, -half.y, half.y),
186
+ z: clamp(local.z, -half.z, half.z),
187
+ };
188
+ const inside = clampedLocal.x === local.x && clampedLocal.y === local.y && clampedLocal.z === local.z;
189
+ return { point: addVec3(box.p, rotatePointByQuat(box.q, clampedLocal)), inside, localClamped: clampedLocal };
190
+ }
191
+ function sphereBox(sphere, box, cSphere, cBox, margin, feature, flip) {
192
+ /* istanbul ignore next -- the dispatch only calls sphereBox with a (sphere, box) pair; type guard only. */
193
+ if (box.shape.kind !== 'box' || sphere.shape.kind !== 'sphere') {
194
+ return undefined;
195
+ }
196
+ const radius = sphere.shape.radius;
197
+ const closest = closestPointOnBox(box, sphere.p);
198
+ const half = box.shape.half;
199
+ if (closest.inside) {
200
+ // sphere center inside the box: push out along the least-penetrating local axis
201
+ const local = closest.localClamped;
202
+ const distances = [half.x - Math.abs(local.x), half.y - Math.abs(local.y), half.z - Math.abs(local.z)];
203
+ let axis = 0;
204
+ if (distances[1] < distances[axis])
205
+ axis = 1;
206
+ if (distances[2] < distances[axis])
207
+ axis = 2;
208
+ const localAxes = [{ x: 1, y: 0, z: 0 }, { x: 0, y: 1, z: 0 }, { x: 0, y: 0, z: 1 }];
209
+ const localComponent = axis === 0 ? local.x : axis === 1 ? local.y : local.z;
210
+ const localNormal = scaleVec3(localAxes[axis], localComponent >= 0 ? 1 : -1);
211
+ const worldNormal = rotatePointByQuat(box.q, localNormal); // sphere is A here → normal box->sphere below
212
+ const penetration = distances[axis] + radius;
213
+ // caller frame: sphere==first arg. When !flip, sphere is A: normal A->B = sphere->box = -worldNormal
214
+ const nSphereToBox = scaleVec3(worldNormal, -1);
215
+ const mid = sphere.p;
216
+ return emitSingle(nSphereToBox, mid, penetration, cSphere, cBox, feature, flip);
217
+ }
218
+ const delta = subVec3(sphere.p, closest.point);
219
+ const dist = solverSqrt(dotVec3(delta, delta));
220
+ const penetration = radius - dist;
221
+ if (penetration < -margin) {
222
+ return undefined;
223
+ }
224
+ /* istanbul ignore next -- the fallback fires only when the sphere centre lies exactly on the box surface (dist === 0), which quantised inputs do not produce. */
225
+ const nBoxToSphere = dist > 1e-9 ? scaleVec3(delta, 1 / dist) : { x: 0, y: 1, z: 0 };
226
+ const nSphereToBox = scaleVec3(nBoxToSphere, -1);
227
+ const mid = scaleVec3(addVec3(closest.point, subVec3(sphere.p, scaleVec3(nBoxToSphere, radius))), 0.5);
228
+ return emitSingle(nSphereToBox, mid, penetration, cSphere, cBox, feature, flip);
229
+ }
230
+ /** Emit a single-point manifold with normal (sphere->box) in the caller's A->B frame. */
231
+ function emitSingle(nSphereToBox, mid, penetration, cSphere, cBox, feature, flip) {
232
+ // caller frame: when !flip, first arg (sphere) is A -> normal A->B = sphere->box
233
+ const normal = flip ? scaleVec3(nSphereToBox, -1) : nSphereToBox;
234
+ const aCenter = flip ? cBox : cSphere;
235
+ const bCenter = flip ? cSphere : cBox;
236
+ return {
237
+ a: 0,
238
+ b: 0,
239
+ normal,
240
+ points: [{ rA: subVec3(mid, aCenter), rB: subVec3(mid, bCenter), penetration, feature }],
241
+ };
242
+ }
243
+ function capsuleSegment(capsule) {
244
+ /* istanbul ignore next -- callers always pass a capsule; type guard only. */
245
+ if (capsule.shape.kind !== 'capsule') {
246
+ throw new Error('capsuleSegment requires a capsule');
247
+ }
248
+ const axis = rotatePointByQuat(capsule.q, { x: 0, y: capsule.shape.halfHeight, z: 0 });
249
+ return { start: subVec3(capsule.p, axis), end: addVec3(capsule.p, axis), radius: capsule.shape.radius };
250
+ }
251
+ function closestPointOnSegment(point, start, end) {
252
+ const ab = subVec3(end, start);
253
+ const denom = dotVec3(ab, ab);
254
+ if (denom === 0) {
255
+ return start;
256
+ }
257
+ const t = clamp(dotVec3(subVec3(point, start), ab) / denom, 0, 1);
258
+ return addVec3(start, scaleVec3(ab, t));
259
+ }
260
+ function sphereCapsule(sphere, capsule, cSphere, cCapsule, margin, feature, flip) {
261
+ const seg = capsuleSegment(capsule);
262
+ const closest = closestPointOnSegment(sphere.p, seg.start, seg.end);
263
+ return pointContact(sphere.p, closest, radiusOf(sphere.shape), seg.radius, cSphere, cCapsule, margin, feature, flip);
264
+ }
265
+ function closestSegmentSegment(p1, q1, p2, q2) {
266
+ const d1 = subVec3(q1, p1);
267
+ const d2 = subVec3(q2, p2);
268
+ const r = subVec3(p1, p2);
269
+ const a = dotVec3(d1, d1);
270
+ const e = dotVec3(d2, d2);
271
+ const f = dotVec3(d2, r);
272
+ let s = 0;
273
+ let t = 0;
274
+ if (a <= 1e-12 && e <= 1e-12) {
275
+ return { c1: p1, c2: p2 };
276
+ }
277
+ if (a <= 1e-12) {
278
+ t = clamp(f / e, 0, 1);
279
+ }
280
+ else {
281
+ const c = dotVec3(d1, r);
282
+ if (e <= 1e-12) {
283
+ s = clamp(-c / a, 0, 1);
284
+ }
285
+ else {
286
+ const b = dotVec3(d1, d2);
287
+ const denom = a * e - b * b;
288
+ s = denom !== 0 ? clamp((b * f - c * e) / denom, 0, 1) : 0;
289
+ t = (b * s + f) / e;
290
+ if (t < 0) {
291
+ t = 0;
292
+ s = clamp(-c / a, 0, 1);
293
+ }
294
+ else if (t > 1) {
295
+ t = 1;
296
+ s = clamp((b - c) / a, 0, 1);
297
+ }
298
+ }
299
+ }
300
+ return { c1: addVec3(p1, scaleVec3(d1, s)), c2: addVec3(p2, scaleVec3(d2, t)) };
301
+ }
302
+ function capsuleCapsule(a, b, ca, cb, margin, feature) {
303
+ const sa = capsuleSegment(a);
304
+ const sb = capsuleSegment(b);
305
+ const closest = closestSegmentSegment(sa.start, sa.end, sb.start, sb.end);
306
+ return pointContact(closest.c1, closest.c2, sa.radius, sb.radius, ca, cb, margin, feature, false);
307
+ }
308
+ function capsuleBox(capsule, box, cCapsule, cBox, margin, feature, flip) {
309
+ // Sample the capsule's two endpoints and center as spheres against the box.
310
+ const seg = capsuleSegment(capsule);
311
+ const samples = [seg.start, seg.end, scaleVec3(addVec3(seg.start, seg.end), 0.5)];
312
+ const points = [];
313
+ let normal;
314
+ let deepest = -Infinity;
315
+ samples.forEach((sample, i) => {
316
+ const sphere = { index: capsule.index, p: sample, q: capsule.q, shape: { kind: 'sphere', radius: seg.radius } };
317
+ const sub = sphereBox(sphere, box, cCapsule, cBox, margin, feature + i, flip);
318
+ if (sub === undefined) {
319
+ return;
320
+ }
321
+ for (const point of sub.points) {
322
+ points.push(point);
323
+ }
324
+ const d = sub.points[0].penetration;
325
+ if (d > deepest) {
326
+ deepest = d;
327
+ normal = sub.normal;
328
+ }
329
+ });
330
+ if (points.length === 0 || normal === undefined) {
331
+ return undefined;
332
+ }
333
+ return { a: 0, b: 0, normal, points };
334
+ }
335
+ // ---------- box-box SAT + face clipping (spike port) ----------
336
+ // Rotation matrices are pure functions of the quaternion, and quaternion
337
+ // objects are never mutated in place (integration reassigns b.q), so identity
338
+ // memoization is value-identical; entries die with their quats.
339
+ const quatMat3Cache = new WeakMap();
340
+ function quatToMat3Cached(q) {
341
+ let m = quatMat3Cache.get(q);
342
+ if (m === undefined) {
343
+ m = quatToMat3(q);
344
+ quatMat3Cache.set(q, m);
345
+ }
346
+ return m;
347
+ }
348
+ function boxAxes(rot) {
349
+ return [
350
+ { x: rot[0], y: rot[3], z: rot[6] },
351
+ { x: rot[1], y: rot[4], z: rot[7] },
352
+ { x: rot[2], y: rot[5], z: rot[8] },
353
+ ];
354
+ }
355
+ function halfArr(shape) {
356
+ /* istanbul ignore next -- halfArr is only called for box shapes; type guard only. */
357
+ if (shape.kind !== 'box') {
358
+ throw new Error('halfArr requires a box');
359
+ }
360
+ return [shape.half.x, shape.half.y, shape.half.z];
361
+ }
362
+ function supportPoint(center, axes, half, dir) {
363
+ let p = center;
364
+ p = addVec3(p, scaleVec3(axes[0], dotVec3(axes[0], dir) >= 0 ? half[0] : -half[0]));
365
+ p = addVec3(p, scaleVec3(axes[1], dotVec3(axes[1], dir) >= 0 ? half[1] : -half[1]));
366
+ p = addVec3(p, scaleVec3(axes[2], dotVec3(axes[2], dir) >= 0 ? half[2] : -half[2]));
367
+ return p;
368
+ }
369
+ // Step-scoped ping-pong scratch for face clipping. Slots are fully overwritten
370
+ // before every read (counts are threaded explicitly), so no value survives
371
+ // into any later step's output.
372
+ // rdm-ignore-next-line determinism/no-module-mutable-state: step-scoped clip scratch, fully overwritten before every read
373
+ const clipScratch = { a: [], b: [] };
374
+ // Incident-face corner sign pattern (hoisted — the literal allocated per call).
375
+ const BOX_FACE_CORNERS = [[-1, -1], [-1, 1], [1, 1], [1, -1]];
376
+ function ensureClipSlots(buffer, count) {
377
+ while (buffer.length < count) {
378
+ buffer.push({ x: 0, y: 0, z: 0, id: 0 });
379
+ }
380
+ }
381
+ /**
382
+ * Sutherland–Hodgman half-space clip, scalarized in the exact IEEE operation
383
+ * order of the previous vector-helper form. Reads `input[0..inCount)`, writes
384
+ * `output`, returns the output count.
385
+ */
386
+ function clipPlaneScratch(input, inCount, nx, ny, nz, offset, planeId, output) {
387
+ ensureClipSlots(output, inCount + 1);
388
+ let outCount = 0;
389
+ for (let i = 0; i < inCount; i += 1) {
390
+ const cur = input[i];
391
+ const nxt = input[(i + 1) % inCount];
392
+ const dc = nx * cur.x + ny * cur.y + nz * cur.z - offset;
393
+ const dn = nx * nxt.x + ny * nxt.y + nz * nxt.z - offset;
394
+ if (dc <= 0) {
395
+ const keep = output[outCount];
396
+ keep.x = cur.x;
397
+ keep.y = cur.y;
398
+ keep.z = cur.z;
399
+ keep.id = cur.id;
400
+ outCount += 1;
401
+ }
402
+ if ((dc < 0 && dn > 0) || (dc > 0 && dn < 0)) {
403
+ const t = dc / (dc - dn);
404
+ const cross = output[outCount];
405
+ cross.x = cur.x + (nxt.x - cur.x) * t;
406
+ cross.y = cur.y + (nxt.y - cur.y) * t;
407
+ cross.z = cur.z + (nxt.z - cur.z) * t;
408
+ cross.id = cur.id * 16 + planeId + 4;
409
+ outCount += 1;
410
+ }
411
+ }
412
+ return outCount;
413
+ }
414
+ function collideBoxes(A, B, centerA, centerB, speculativeMargin, featureBase) {
415
+ const rotA = quatToMat3Cached(A.q);
416
+ const rotB = quatToMat3Cached(B.q);
417
+ const halfA = halfArr(A.shape);
418
+ const halfB = halfArr(B.shape);
419
+ const hA0 = halfA[0];
420
+ const hA1 = halfA[1];
421
+ const hA2 = halfA[2];
422
+ const hB0 = halfB[0];
423
+ const hB1 = halfB[1];
424
+ const hB2 = halfB[2];
425
+ const dx = B.p.x - A.p.x;
426
+ const dy = B.p.y - A.p.y;
427
+ const dz = B.p.z - A.p.z;
428
+ // Scalarized SAT: the 15-axis loop is the narrow-phase hot path, so the
429
+ // vector helpers are expanded component-wise in the exact same IEEE
430
+ // operation order (axis i of a rotation matrix is (rot[i], rot[3+i], rot[6+i])).
431
+ let facePen = Infinity;
432
+ let faceIndex = -1;
433
+ let faceFound = false;
434
+ let faceNx = 0;
435
+ let faceNy = 0;
436
+ let faceNz = 0;
437
+ let edgePen = Infinity;
438
+ let edgeIndex = -1;
439
+ let edgeFound = false;
440
+ let edgeNx = 0;
441
+ let edgeNy = 0;
442
+ let edgeNz = 0;
443
+ const tryAxis = (ax, ay, az, index, isEdge) => {
444
+ const l2 = ax * ax + ay * ay + az * az;
445
+ if (l2 < 1e-8) {
446
+ return true;
447
+ }
448
+ const inv = 1 / solverSqrt(l2);
449
+ const nx = ax * inv;
450
+ const ny = ay * inv;
451
+ const nz = az * inv;
452
+ const sep = Math.abs(dx * nx + dy * ny + dz * nz)
453
+ - ((Math.abs(rotA[0] * nx + rotA[3] * ny + rotA[6] * nz) * hA0
454
+ + Math.abs(rotA[1] * nx + rotA[4] * ny + rotA[7] * nz) * hA1
455
+ + Math.abs(rotA[2] * nx + rotA[5] * ny + rotA[8] * nz) * hA2)
456
+ + (Math.abs(rotB[0] * nx + rotB[3] * ny + rotB[6] * nz) * hB0
457
+ + Math.abs(rotB[1] * nx + rotB[4] * ny + rotB[7] * nz) * hB1
458
+ + Math.abs(rotB[2] * nx + rotB[5] * ny + rotB[8] * nz) * hB2));
459
+ if (sep > speculativeMargin) {
460
+ return false;
461
+ }
462
+ const pen = -sep;
463
+ if (isEdge) {
464
+ if (pen < edgePen - 1e-9) {
465
+ edgePen = pen;
466
+ edgeNx = nx;
467
+ edgeNy = ny;
468
+ edgeNz = nz;
469
+ edgeIndex = index;
470
+ edgeFound = true;
471
+ }
472
+ }
473
+ else if (pen < facePen - 1e-9) {
474
+ facePen = pen;
475
+ faceNx = nx;
476
+ faceNy = ny;
477
+ faceNz = nz;
478
+ faceIndex = index;
479
+ faceFound = true;
480
+ }
481
+ return true;
482
+ };
483
+ for (let i = 0; i < 3; i += 1) {
484
+ if (!tryAxis(rotA[i], rotA[3 + i], rotA[6 + i], i, false)) {
485
+ return undefined;
486
+ }
487
+ }
488
+ for (let i = 0; i < 3; i += 1) {
489
+ if (!tryAxis(rotB[i], rotB[3 + i], rotB[6 + i], 3 + i, false)) {
490
+ return undefined;
491
+ }
492
+ }
493
+ for (let i = 0; i < 3; i += 1) {
494
+ const aix = rotA[i];
495
+ const aiy = rotA[3 + i];
496
+ const aiz = rotA[6 + i];
497
+ for (let j = 0; j < 3; j += 1) {
498
+ const bjx = rotB[j];
499
+ const bjy = rotB[3 + j];
500
+ const bjz = rotB[6 + j];
501
+ if (!tryAxis(aiy * bjz - aiz * bjy, aiz * bjx - aix * bjz, aix * bjy - aiy * bjx, 6 + i * 3 + j, true)) {
502
+ return undefined;
503
+ }
504
+ }
505
+ }
506
+ /* istanbul ignore next -- box face axes are unit vectors and never degenerate, so at least one separating axis is always recorded. */
507
+ if (!faceFound && !edgeFound) {
508
+ return undefined;
509
+ }
510
+ // qu3e-style hysteresis: use the edge axis only when decisively shallower.
511
+ // Only for real (positive-penetration) overlaps — for speculative/separated
512
+ // pairs (negative pen) the face manifold gives correct near-body anchors,
513
+ // while the edge path would place a single point far from the bodies.
514
+ const edgePreferred = edgeFound && facePen > 0 && edgePen < facePen * 0.9 - 5e-3;
515
+ /* istanbul ignore next -- a face axis is always recorded here (see the guard above), so the !faceFound arm is unreachable. */
516
+ const bestIsEdge = !faceFound ? edgeFound : edgePreferred;
517
+ const bestIndex = bestIsEdge ? edgeIndex : faceIndex;
518
+ const minPen = bestIsEdge ? edgePen : facePen;
519
+ let normal = bestIsEdge ? { x: edgeNx, y: edgeNy, z: edgeNz } : { x: faceNx, y: faceNy, z: faceNz };
520
+ if (normal.x * dx + normal.y * dy + normal.z * dz < 0) {
521
+ normal = scaleVec3(normal, -1);
522
+ }
523
+ if (bestIsEdge) {
524
+ const axesA = boxAxes(rotA);
525
+ const axesB = boxAxes(rotB);
526
+ const i = Math.trunc((bestIndex - 6) / 3);
527
+ const j = (bestIndex - 6) % 3;
528
+ const pA = supportPoint(A.p, axesA, halfA, normal);
529
+ const pB = supportPoint(B.p, axesB, halfB, scaleVec3(normal, -1));
530
+ const eA = axesA[i];
531
+ const eB = axesB[j];
532
+ const r = subVec3(pB, pA);
533
+ const aa = dotVec3(eA, eA);
534
+ const ee = dotVec3(eB, eB);
535
+ const ff = dotVec3(eB, r);
536
+ const cc = dotVec3(eA, r);
537
+ const bd = dotVec3(eA, eB);
538
+ const denom = aa * ee - bd * bd;
539
+ /* istanbul ignore next -- the edge axis is a normalised cross product of two non-parallel edges, so its denominator is non-zero. */
540
+ const s = denom !== 0 ? clamp((bd * ff - cc * ee) / denom, -halfA[i], halfA[i]) : 0;
541
+ const t = clamp((bd * s + ff) / ee, -halfB[j], halfB[j]);
542
+ const cA = addVec3(pA, scaleVec3(eA, s));
543
+ const cB = addVec3(pB, scaleVec3(eB, t));
544
+ const mid = scaleVec3(addVec3(cA, cB), 0.5);
545
+ return { a: 0, b: 0, normal, points: [{ rA: subVec3(mid, centerA), rB: subVec3(mid, centerB), penetration: minPen, feature: featureBase + 1000 + bestIndex }] };
546
+ }
547
+ const refIsA = bestIndex < 3;
548
+ const refCenter = refIsA ? A.p : B.p;
549
+ const refRot = refIsA ? rotA : rotB;
550
+ const refHalf = refIsA ? halfA : halfB;
551
+ const incCenter = refIsA ? B.p : A.p;
552
+ const incRot = refIsA ? rotB : rotA;
553
+ const incHalf = refIsA ? halfB : halfA;
554
+ const rnx = refIsA ? normal.x : normal.x * -1;
555
+ const rny = refIsA ? normal.y : normal.y * -1;
556
+ const rnz = refIsA ? normal.z : normal.z * -1;
557
+ let incFace = 0;
558
+ let minDot = Infinity;
559
+ for (let i = 0; i < 3; i += 1) {
560
+ const dd = incRot[i] * rnx + incRot[3 + i] * rny + incRot[6 + i] * rnz;
561
+ if (dd < minDot) {
562
+ minDot = dd;
563
+ incFace = i * 2;
564
+ }
565
+ if (-dd < minDot) {
566
+ minDot = -dd;
567
+ incFace = i * 2 + 1;
568
+ }
569
+ }
570
+ // incident-face corners into scratch (scalar form of faceVertices)
571
+ const incAxis = Math.trunc(incFace / 2);
572
+ const incSign = incFace % 2 === 0 ? 1 : -1;
573
+ const u = (incAxis + 1) % 3;
574
+ const w = (incAxis + 2) % 3;
575
+ const sAxis = incSign * incHalf[incAxis];
576
+ const fcx = incCenter.x + incRot[incAxis] * sAxis;
577
+ const fcy = incCenter.y + incRot[3 + incAxis] * sAxis;
578
+ const fcz = incCenter.z + incRot[6 + incAxis] * sAxis;
579
+ ensureClipSlots(clipScratch.a, 4);
580
+ let poly = clipScratch.a;
581
+ let spare = clipScratch.b;
582
+ let polyCount = 0;
583
+ for (const [su, sw] of BOX_FACE_CORNERS) {
584
+ const suh = su * incHalf[u];
585
+ const swh = sw * incHalf[w];
586
+ const slot = poly[polyCount];
587
+ slot.x = fcx + (incRot[u] * suh + incRot[w] * swh);
588
+ slot.y = fcy + (incRot[3 + u] * suh + incRot[3 + w] * swh);
589
+ slot.z = fcz + (incRot[6 + u] * suh + incRot[6 + w] * swh);
590
+ slot.id = featureBase + polyCount + 1;
591
+ polyCount += 1;
592
+ }
593
+ const refFaceAxis = bestIndex % 3;
594
+ let planeIdx = 0;
595
+ for (let i = 0; i < 3; i += 1) {
596
+ if (i === refFaceAxis) {
597
+ continue;
598
+ }
599
+ planeIdx += 1;
600
+ const h = refHalf[i];
601
+ const ax = refRot[i];
602
+ const ay = refRot[3 + i];
603
+ const az = refRot[6 + i];
604
+ const negAx = ax * -1;
605
+ const negAy = ay * -1;
606
+ const negAz = az * -1;
607
+ polyCount = clipPlaneScratch(poly, polyCount, negAx, negAy, negAz, negAx * refCenter.x + negAy * refCenter.y + negAz * refCenter.z + h, planeIdx * 2 - 1, spare);
608
+ let swap = poly;
609
+ poly = spare;
610
+ spare = swap;
611
+ polyCount = clipPlaneScratch(poly, polyCount, ax, ay, az, ax * refCenter.x + ay * refCenter.y + az * refCenter.z + h, planeIdx * 2, spare);
612
+ swap = poly;
613
+ poly = spare;
614
+ spare = swap;
615
+ if (polyCount === 0) {
616
+ return undefined;
617
+ }
618
+ }
619
+ const refH = refHalf[refFaceAxis];
620
+ const refFaceDist = rnx * refCenter.x + rny * refCenter.y + rnz * refCenter.z + refH;
621
+ const points = [];
622
+ for (let i = 0; i < polyCount; i += 1) {
623
+ const vert = poly[i];
624
+ const depth = refFaceDist - (rnx * vert.x + rny * vert.y + rnz * vert.z);
625
+ if (depth >= -speculativeMargin) {
626
+ const onRefX = vert.x + rnx * depth;
627
+ const onRefY = vert.y + rny * depth;
628
+ const onRefZ = vert.z + rnz * depth;
629
+ const midX = (vert.x + onRefX) * 0.5;
630
+ const midY = (vert.y + onRefY) * 0.5;
631
+ const midZ = (vert.z + onRefZ) * 0.5;
632
+ points.push({
633
+ rA: { x: midX - centerA.x, y: midY - centerA.y, z: midZ - centerA.z },
634
+ rB: { x: midX - centerB.x, y: midY - centerB.y, z: midZ - centerB.z },
635
+ penetration: depth,
636
+ feature: vert.id,
637
+ });
638
+ }
639
+ }
640
+ /* istanbul ignore next -- a face manifold that passes the SAT overlap test always retains at least one clipped point above the speculative margin. */
641
+ if (points.length === 0) {
642
+ return undefined;
643
+ }
644
+ return { a: 0, b: 0, normal, points };
645
+ }
646
+ // ---------- mesh (one-sided static triangle) ----------
647
+ function meshManifold(a, b, speculativeMargin) {
648
+ const meshIsA = a.shape.kind === 'mesh';
649
+ const meshBody = meshIsA ? a : b;
650
+ const otherBody = meshIsA ? b : a;
651
+ if (meshBody.shape.kind !== 'mesh' || otherBody.shape.kind === 'mesh') {
652
+ return undefined;
653
+ }
654
+ const prims = primitiveViews(otherBody);
655
+ const points = [];
656
+ let normal;
657
+ let deepest = -Infinity;
658
+ let feature = 0;
659
+ const triangles = meshBody.shape.triangles.map((tri) => [
660
+ addVec3(meshBody.p, rotatePointByQuat(meshBody.q, tri[0])),
661
+ addVec3(meshBody.p, rotatePointByQuat(meshBody.q, tri[1])),
662
+ addVec3(meshBody.p, rotatePointByQuat(meshBody.q, tri[2])),
663
+ ]);
664
+ for (const prim of prims) {
665
+ /* istanbul ignore next -- primitiveViews only yields box/sphere/capsule sub-bodies (compound children are primitives); this skip is unreachable. */
666
+ if (prim.shape.kind !== 'sphere' && prim.shape.kind !== 'capsule' && prim.shape.kind !== 'box') {
667
+ continue;
668
+ }
669
+ const radius = prim.shape.kind === 'box'
670
+ ? 0
671
+ : prim.shape.radius;
672
+ // Represent boxes by their center as a fallback point-sphere against triangles
673
+ // (dynamic box-vs-mesh uses the box's support samples).
674
+ const samplePoints = prim.shape.kind === 'box'
675
+ ? boxSupportSamples(prim)
676
+ : prim.shape.kind === 'capsule'
677
+ ? capsuleSamples(prim)
678
+ : [prim.p];
679
+ for (const triangle of triangles) {
680
+ for (const sample of samplePoints) {
681
+ const closest = closestPointOnTriangle(sample, triangle[0], triangle[1], triangle[2]);
682
+ const delta = subVec3(sample, closest);
683
+ const dist = solverSqrt(dotVec3(delta, delta));
684
+ const triNormal = triangleNormal(triangle[0], triangle[1], triangle[2]);
685
+ const side = dotVec3(delta, triNormal);
686
+ const penetration = radius - dist;
687
+ if (penetration < -speculativeMargin || side < 0) {
688
+ continue;
689
+ }
690
+ const contactNormal = triNormal; // one-sided: mesh -> other
691
+ const mid = closest;
692
+ // caller frame normal A->B
693
+ const nAtoB = meshIsA ? contactNormal : scaleVec3(contactNormal, -1);
694
+ points.push({
695
+ rA: subVec3(mid, a.p),
696
+ rB: subVec3(mid, b.p),
697
+ penetration,
698
+ feature: feature,
699
+ });
700
+ feature += 1;
701
+ if (penetration > deepest) {
702
+ deepest = penetration;
703
+ normal = nAtoB;
704
+ }
705
+ }
706
+ }
707
+ }
708
+ if (points.length === 0 || normal === undefined) {
709
+ return undefined;
710
+ }
711
+ return { a: a.index, b: b.index, normal, points: reducePoints(points) };
712
+ }
713
+ function triangleNormal(a, b, c) {
714
+ const n = crossVec3(subVec3(b, a), subVec3(c, a));
715
+ const len = solverSqrt(dotVec3(n, n));
716
+ return len === 0 ? { x: 0, y: 1, z: 0 } : scaleVec3(n, 1 / len);
717
+ }
718
+ function boxSupportSamples(box) {
719
+ /* istanbul ignore next -- boxSupportSamples is only called for box operands; type guard only. */
720
+ if (box.shape.kind !== 'box') {
721
+ return [box.p];
722
+ }
723
+ const rot = quatToMat3Cached(box.q);
724
+ const axes = boxAxes(rot);
725
+ const h = halfArr(box.shape);
726
+ const out = [];
727
+ for (const sx of [-1, 1]) {
728
+ for (const sy of [-1, 1]) {
729
+ for (const sz of [-1, 1]) {
730
+ out.push(addVec3(box.p, addVec3(scaleVec3(axes[0], sx * h[0]), addVec3(scaleVec3(axes[1], sy * h[1]), scaleVec3(axes[2], sz * h[2])))));
731
+ }
732
+ }
733
+ }
734
+ return out;
735
+ }
736
+ function capsuleSamples(capsule) {
737
+ const seg = capsuleSegment(capsule);
738
+ return [seg.start, seg.end, scaleVec3(addVec3(seg.start, seg.end), 0.5)];
739
+ }
740
+ function closestPointOnTriangle(p, a, b, c) {
741
+ const ab = subVec3(b, a);
742
+ const ac = subVec3(c, a);
743
+ const ap = subVec3(p, a);
744
+ const d1 = dotVec3(ab, ap);
745
+ const d2 = dotVec3(ac, ap);
746
+ if (d1 <= 0 && d2 <= 0) {
747
+ return a;
748
+ }
749
+ const bp = subVec3(p, b);
750
+ const d3 = dotVec3(ab, bp);
751
+ const d4 = dotVec3(ac, bp);
752
+ if (d3 >= 0 && d4 <= d3) {
753
+ return b;
754
+ }
755
+ const vc = d1 * d4 - d3 * d2;
756
+ if (vc <= 0 && d1 >= 0 && d3 <= 0) {
757
+ const v = d1 / (d1 - d3);
758
+ return addVec3(a, scaleVec3(ab, v));
759
+ }
760
+ const cp = subVec3(p, c);
761
+ const d5 = dotVec3(ab, cp);
762
+ const d6 = dotVec3(ac, cp);
763
+ if (d6 >= 0 && d5 <= d6) {
764
+ return c;
765
+ }
766
+ const vb = d5 * d2 - d1 * d6;
767
+ if (vb <= 0 && d2 >= 0 && d6 <= 0) {
768
+ const w = d2 / (d2 - d6);
769
+ return addVec3(a, scaleVec3(ac, w));
770
+ }
771
+ const va = d3 * d6 - d5 * d4;
772
+ if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
773
+ const w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
774
+ return addVec3(b, scaleVec3(subVec3(c, b), w));
775
+ }
776
+ const denom = 1 / (va + vb + vc);
777
+ const v = vb * denom;
778
+ const w = vc * denom;
779
+ return addVec3(a, addVec3(scaleVec3(ab, v), scaleVec3(ac, w)));
780
+ }
781
+ // ============================================================================
782
+ // Sequential-impulse velocity solver + split impulse + islands + sleep
783
+ // ============================================================================
784
+ export const SOLVER_CONSTANTS = {
785
+ BAUMGARTE: 0.2,
786
+ SLOP: 0.005,
787
+ WAKE_VEL2: 0.005,
788
+ SLEEP_VEL2: 0.005,
789
+ SLEEP_FRAMES: 20,
790
+ RESTITUTION_THRESHOLD: 1.0,
791
+ WARM_MATCH_TOL2: 0.01,
792
+ LINEAR_DAMPING: 0.995,
793
+ ANGULAR_DAMPING: 0.98,
794
+ };
795
+ /**
796
+ * Warm-cache maps pack the canonical index pair (a<b) into one exact integer
797
+ * `a * WARM_KEY_BASE + b` — string pair keys were measurable churn on large piles.
798
+ */
799
+ export const WARM_KEY_BASE = 2 ** 20;
800
+ function solid(b) {
801
+ return b.isDynamic && !b.sleeping;
802
+ }
803
+ function worldInvInertiaLocked(b) {
804
+ if (!solid(b)) {
805
+ return [0, 0, 0, 0, 0, 0, 0, 0, 0];
806
+ }
807
+ const base = worldInvInertia(quatToMat3Cached(b.q), b.invInertiaLocal);
808
+ const lock = b.angularLock;
809
+ const lockArr = [lock.x, lock.y, lock.z];
810
+ const out = new Array(9).fill(0);
811
+ for (let row = 0; row < 3; row += 1) {
812
+ for (let col = 0; col < 3; col += 1) {
813
+ out[row * 3 + col] = base[row * 3 + col] * lockArr[row] * lockArr[col];
814
+ }
815
+ }
816
+ return out;
817
+ }
818
+ function invMassOf(b) {
819
+ return solid(b) ? b.invMassVec : ZERO;
820
+ }
821
+ function effectiveMass(axis, rA, rB, imA, imB, iiA, iiB) {
822
+ const linA = axis.x * axis.x * imA.x + axis.y * axis.y * imA.y + axis.z * axis.z * imA.z;
823
+ const linB = axis.x * axis.x * imB.x + axis.y * axis.y * imB.y + axis.z * axis.z * imB.z;
824
+ const raxn = crossVec3(rA, axis);
825
+ const rbxn = crossVec3(rB, axis);
826
+ const denom = linA + linB + dotVec3(raxn, mulMat3Vec(iiA, raxn)) + dotVec3(rbxn, mulMat3Vec(iiB, rbxn));
827
+ return denom === 0 ? 0 : 1 / denom;
828
+ }
829
+ function tangentBasis(n) {
830
+ const t1 = Math.abs(n.x) >= 0.57735
831
+ ? normalizeVec3Local({ x: n.y, y: -n.x, z: 0 })
832
+ : normalizeVec3Local({ x: 0, y: n.z, z: -n.y });
833
+ const t2 = crossVec3(n, t1);
834
+ return [t1, t2];
835
+ }
836
+ function normalizeVec3Local(a) {
837
+ const l = solverSqrt(dotVec3(a, a));
838
+ /* istanbul ignore next -- called only on the tangent-basis vectors of a unit contact normal, which are never zero-length. */
839
+ return l === 0 ? { x: 0, y: 0, z: 0 } : scaleVec3(a, 1 / l);
840
+ }
841
+ function matchWarm(points, rA) {
842
+ if (points === undefined) {
843
+ return undefined;
844
+ }
845
+ let best;
846
+ let bestD = SOLVER_CONSTANTS.WARM_MATCH_TOL2;
847
+ for (const p of points) {
848
+ const dx = p.anchor.x - rA.x;
849
+ const dy = p.anchor.y - rA.y;
850
+ const dz = p.anchor.z - rA.z;
851
+ const d2 = dx * dx + dy * dy + dz * dz;
852
+ if (d2 < bestD) {
853
+ bestD = d2;
854
+ best = p;
855
+ }
856
+ }
857
+ return best;
858
+ }
859
+ // Step-scoped pool: the cursor resets at solveRigidStep entry and every field
860
+ // is overwritten on take, so no value crosses steps. Constraints never escape
861
+ // the step (warm-cache entries copy rA by reference from the manifold points,
862
+ // which are freshly allocated).
863
+ // rdm-ignore-next-line determinism/no-module-mutable-state: step-scoped constraint pool, fully overwritten before every read
864
+ const constraintPool = { slots: [], high: 0 };
865
+ function takeConstraint() {
866
+ if (constraintPool.high === constraintPool.slots.length) {
867
+ constraintPool.slots.push({
868
+ manifoldIndex: 0, a: 0, b: 0,
869
+ normal: ZERO, t1: ZERO, t2: ZERO, rA: ZERO, rB: ZERO,
870
+ penetration: 0, speculative: false,
871
+ massN: 0, massT1: 0, massT2: 0, bias: 0,
872
+ accN: 0, accT1: 0, accT2: 0, accNb: 0,
873
+ mu: 0, restitution: 0,
874
+ });
875
+ }
876
+ const slot = constraintPool.slots[constraintPool.high];
877
+ constraintPool.high += 1;
878
+ return slot;
879
+ }
880
+ /** Runs one deterministic rigid-body step in place over `bodies`. */
881
+ export function solveRigidStep(bodies, options) {
882
+ /* istanbul ignore next -- worlds are validated far below this bound; guards the packed warm-cache key from silent index collisions. */
883
+ if (bodies.length > WARM_KEY_BASE) {
884
+ throw new Error(`solveRigidStep supports at most ${WARM_KEY_BASE} bodies`);
885
+ }
886
+ constraintPool.high = 0;
887
+ const dt = options.dt;
888
+ // 1. reset pseudo-velocities; apply gravity + damping to awake dynamics.
889
+ // v/w are copied so the solver owns them: callers may pass aliased vectors
890
+ // (canonical angularVel, shared zero constants) and the hot scalar impulse
891
+ // path mutates these objects in place.
892
+ for (const b of bodies) {
893
+ b.vb = { x: 0, y: 0, z: 0 };
894
+ b.wb = { x: 0, y: 0, z: 0 };
895
+ b.v = { x: b.v.x, y: b.v.y, z: b.v.z };
896
+ b.w = { x: b.w.x, y: b.w.y, z: b.w.z };
897
+ if (!solid(b)) {
898
+ continue;
899
+ }
900
+ b.v.y = b.v.y + options.gravityY * b.gravityScale * dt;
901
+ }
902
+ // 2. narrow-phase manifolds (canonical order i<j).
903
+ const manifolds = [];
904
+ const emitPair = (i, j) => {
905
+ const A = bodies[i];
906
+ const B = bodies[j];
907
+ if (!A.isDynamic && !B.isDynamic) {
908
+ return;
909
+ }
910
+ if (A.noCollide || B.noCollide) {
911
+ return;
912
+ }
913
+ if (options.noCollidePairs !== undefined && options.noCollidePairs.has(i * WARM_KEY_BASE + j)) {
914
+ return;
915
+ }
916
+ if ((A.mask & B.layer) === 0 || (B.mask & A.layer) === 0) {
917
+ return;
918
+ }
919
+ const margin = A.ccd || B.ccd ? speculativeMarginFor(A, B, dt) : 0;
920
+ const manifold = generateManifold(toGeom(A), toGeom(B), margin);
921
+ if (manifold !== undefined) {
922
+ manifolds.push(manifold);
923
+ }
924
+ };
925
+ if (options.pairs !== undefined) {
926
+ for (const [i, j] of options.pairs) {
927
+ emitPair(Math.min(i, j), Math.max(i, j));
928
+ }
929
+ }
930
+ else {
931
+ // Sort-and-sweep broadphase over swept world AABBs (indexed by canonical
932
+ // body order) — replaces the naive O(n²) pair scan for large piles.
933
+ const aabbs = bodies.map((body) => solverBodyAabb(body, dt));
934
+ const order = bodies.map((_, index) => index).sort((l, r) => aabbs[l].minX - aabbs[r].minX || l - r);
935
+ for (let sweepA = 0; sweepA < order.length; sweepA += 1) {
936
+ const i = order[sweepA];
937
+ const boundsI = aabbs[i];
938
+ for (let sweepB = sweepA + 1; sweepB < order.length; sweepB += 1) {
939
+ const j = order[sweepB];
940
+ const boundsJ = aabbs[j];
941
+ if (boundsJ.minX > boundsI.maxX) {
942
+ break;
943
+ }
944
+ if (boundsJ.minY > boundsI.maxY || boundsJ.maxY < boundsI.minY || boundsJ.minZ > boundsI.maxZ || boundsJ.maxZ < boundsI.minZ) {
945
+ continue;
946
+ }
947
+ emitPair(Math.min(i, j), Math.max(i, j));
948
+ }
949
+ }
950
+ }
951
+ manifolds.sort((l, r) => l.a - r.a || l.b - r.b);
952
+ // Bodies coupled to an active joint this step. Joints are step inputs (not
953
+ // world state), so any joint member — including one that fell asleep before
954
+ // the joint was attached — must wake and stay awake for the joint to drive it.
955
+ const joints = options.joints ?? [];
956
+ const jointBodies = new Set();
957
+ for (const joint of joints) {
958
+ for (const index of joint.bodyIndices) {
959
+ jointBodies.add(index);
960
+ }
961
+ }
962
+ // 3. wake pass: sleeping body forced awake, contacted by an awake mover, or
963
+ // coupled to an active joint.
964
+ for (const b of bodies) {
965
+ if ((b.wakeForced || jointBodies.has(b.index)) && b.sleeping) {
966
+ b.sleeping = false;
967
+ b.lowVelFrames = 0;
968
+ }
969
+ }
970
+ for (const m of manifolds) {
971
+ wakeIfContacted(bodies[m.a], bodies[m.b]);
972
+ wakeIfContacted(bodies[m.b], bodies[m.a]);
973
+ }
974
+ // 4. build contact constraints + warm start.
975
+ const invMass = bodies.map(invMassOf);
976
+ const invInertia = bodies.map(worldInvInertiaLocked);
977
+ const constraints = [];
978
+ manifolds.forEach((m, manifoldIndex) => {
979
+ const A = bodies[m.a];
980
+ const B = bodies[m.b];
981
+ const [t1, t2] = tangentBasis(m.normal);
982
+ const combinedMu = Math.floor((toInt10(A.matFriction) + toInt10(B.matFriction)) / 2) / 10;
983
+ const combinedRest = Math.max(toInt10(A.matRestitution), toInt10(B.matRestitution)) / 10;
984
+ const warmPts = options.warm.get(m.a * WARM_KEY_BASE + m.b);
985
+ for (const pt of m.points) {
986
+ const matched = matchWarm(warmPts, pt.rA);
987
+ const constraint = takeConstraint();
988
+ constraint.manifoldIndex = manifoldIndex;
989
+ constraint.a = m.a;
990
+ constraint.b = m.b;
991
+ constraint.normal = m.normal;
992
+ constraint.t1 = t1;
993
+ constraint.t2 = t2;
994
+ constraint.rA = pt.rA;
995
+ constraint.rB = pt.rB;
996
+ constraint.penetration = pt.penetration;
997
+ constraint.speculative = pt.penetration < 0;
998
+ constraint.massN = effectiveMass(m.normal, pt.rA, pt.rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
999
+ constraint.massT1 = effectiveMass(t1, pt.rA, pt.rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
1000
+ constraint.massT2 = effectiveMass(t2, pt.rA, pt.rB, invMass[m.a], invMass[m.b], invInertia[m.a], invInertia[m.b]);
1001
+ constraint.bias = (SOLVER_CONSTANTS.BAUMGARTE / dt) * Math.max(0, pt.penetration - SOLVER_CONSTANTS.SLOP);
1002
+ constraint.accN = matched?.normalImpulse ?? 0;
1003
+ constraint.accT1 = matched?.tangentImpulse1 ?? 0;
1004
+ constraint.accT2 = matched?.tangentImpulse2 ?? 0;
1005
+ constraint.accNb = 0;
1006
+ constraint.mu = combinedMu;
1007
+ constraint.restitution = combinedRest;
1008
+ // warm-start impulse, scalar form of normal*accN + (t1*accT1 + t2*accT2)
1009
+ applyImpulseScalar(bodies, invMass, invInertia, constraint.a, constraint.b, constraint.rA, constraint.rB, m.normal.x * constraint.accN + (t1.x * constraint.accT1 + t2.x * constraint.accT2), m.normal.y * constraint.accN + (t1.y * constraint.accT1 + t2.y * constraint.accT2), m.normal.z * constraint.accN + (t1.z * constraint.accT1 + t2.z * constraint.accT2), false);
1010
+ constraints.push(constraint);
1011
+ }
1012
+ });
1013
+ // 5. joint warm start + iterations interleaved with contacts.
1014
+ const jointCtx = {
1015
+ bodies,
1016
+ invMass,
1017
+ invInertia,
1018
+ dt,
1019
+ apply: (a, b, rA, rB, impulse) => applyImpulse(bodies, invMass, invInertia, a, b, rA, rB, impulse),
1020
+ applyAngular: (a, b, angularImpulse) => {
1021
+ const A = bodies[a];
1022
+ const B = bodies[b];
1023
+ if (solid(A)) {
1024
+ A.w = subVec3(A.w, mulMat3Vec(invInertia[a], angularImpulse));
1025
+ }
1026
+ if (solid(B)) {
1027
+ B.w = addVec3(B.w, mulMat3Vec(invInertia[b], angularImpulse));
1028
+ }
1029
+ },
1030
+ applyToBody: (index, r, impulse) => {
1031
+ const body = bodies[index];
1032
+ if (!solid(body)) {
1033
+ return;
1034
+ }
1035
+ body.v = addVec3(body.v, mulComponents(impulse, invMass[index]));
1036
+ body.w = addVec3(body.w, mulMat3Vec(invInertia[index], crossVec3(r, impulse)));
1037
+ },
1038
+ applyBias: (a, b, rA, rB, impulse) => applyBiasImpulseAt(bodies, invMass, invInertia, a, b, rA, rB, impulse),
1039
+ biasRelativeVelocity: (a, b, rA, rB) => relativeBiasVelocity(bodies[a], bodies[b], rA, rB),
1040
+ };
1041
+ for (const joint of joints) {
1042
+ joint.prepare(jointCtx);
1043
+ }
1044
+ for (let iter = 0; iter < options.velocityIterations; iter += 1) {
1045
+ for (const joint of joints) {
1046
+ joint.iterate(jointCtx);
1047
+ }
1048
+ for (const c of constraints) {
1049
+ solveContactConstraint(bodies, invMass, invInertia, c, dt);
1050
+ }
1051
+ }
1052
+ // 6. store warm cache (index-pair keyed, deterministic order).
1053
+ const warm = new Map();
1054
+ for (const c of constraints) {
1055
+ const key = c.a * WARM_KEY_BASE + c.b;
1056
+ let list = warm.get(key);
1057
+ if (list === undefined) {
1058
+ list = [];
1059
+ warm.set(key, list);
1060
+ }
1061
+ list.push({ anchor: c.rA, normalImpulse: c.accN, tangentImpulse1: c.accT1, tangentImpulse2: c.accT2 });
1062
+ }
1063
+ // `jointBodies` (built at the wake pass) keeps joint members awake — a motor or
1064
+ // grab can drive them below the sleep velocity threshold, and joints are step
1065
+ // inputs the sleeper can't see.
1066
+ // 7. damping + integrate (real + pseudo velocity).
1067
+ for (const b of bodies) {
1068
+ if (!solid(b)) {
1069
+ continue;
1070
+ }
1071
+ b.v = scaleVec3(b.v, b.linearDamping);
1072
+ b.w = scaleVec3(b.w, b.angularDamping);
1073
+ // Axis locks: a locked linear/angular axis holds zero velocity so the body
1074
+ // can neither be pushed along it nor drift from residual velocity.
1075
+ b.v = {
1076
+ x: b.invMassVec.x === 0 ? 0 : b.v.x,
1077
+ y: b.invMassVec.y === 0 ? 0 : b.v.y,
1078
+ z: b.invMassVec.z === 0 ? 0 : b.v.z,
1079
+ };
1080
+ b.w = {
1081
+ x: b.angularLock.x === 0 ? 0 : b.w.x,
1082
+ y: b.angularLock.y === 0 ? 0 : b.w.y,
1083
+ z: b.angularLock.z === 0 ? 0 : b.w.z,
1084
+ };
1085
+ b.p = addVec3(b.p, scaleVec3(addVec3(b.v, b.vb), dt));
1086
+ if (b.hasOrientation) {
1087
+ b.q = integrateQuat(b.q, addVec3(b.w, b.wb), dt);
1088
+ }
1089
+ const speed2 = dotVec3(b.v, b.v) + dotVec3(b.w, b.w);
1090
+ b.lowVelFrames = jointBodies.has(b.index) || speed2 >= SOLVER_CONSTANTS.SLEEP_VEL2 ? 0 : b.lowVelFrames + 1;
1091
+ }
1092
+ // 8. island sleep over contacts + joints.
1093
+ islandSleep(bodies, manifolds, joints);
1094
+ // 9. per-manifold accumulated normal impulse for contact events.
1095
+ const impulseByManifold = new Array(manifolds.length).fill(0);
1096
+ for (const c of constraints) {
1097
+ impulseByManifold[c.manifoldIndex] += c.accN;
1098
+ }
1099
+ const contactImpulses = manifolds.map((m, index) => ({
1100
+ a: m.a,
1101
+ b: m.b,
1102
+ normalImpulse: impulseByManifold[index],
1103
+ pair: `${bodies[m.a].id}|${bodies[m.b].id}`,
1104
+ }));
1105
+ const jointCache = joints.map((joint) => joint.emitCache());
1106
+ return { manifolds, contactImpulses, warm, jointCache };
1107
+ }
1108
+ function toInt10(unit) {
1109
+ return Math.round(unit * 10);
1110
+ }
1111
+ /** World AABB of a solver body's shape (orientation-aware), swept by v·dt when ccd. */
1112
+ function solverBodyAabb(body, dt) {
1113
+ const half = shapeHalfExtent(body.shape, body.q);
1114
+ let minX = body.p.x - half.x;
1115
+ let maxX = body.p.x + half.x;
1116
+ let minY = body.p.y - half.y;
1117
+ let maxY = body.p.y + half.y;
1118
+ let minZ = body.p.z - half.z;
1119
+ let maxZ = body.p.z + half.z;
1120
+ if (body.ccd) {
1121
+ const dx = body.v.x * dt;
1122
+ const dy = body.v.y * dt;
1123
+ const dz = body.v.z * dt;
1124
+ minX = Math.min(minX, minX + dx);
1125
+ maxX = Math.max(maxX, maxX + dx);
1126
+ minY = Math.min(minY, minY + dy);
1127
+ maxY = Math.max(maxY, maxY + dy);
1128
+ minZ = Math.min(minZ, minZ + dz);
1129
+ maxZ = Math.max(maxZ, maxZ + dz);
1130
+ }
1131
+ return { minX, maxX, minY, maxY, minZ, maxZ };
1132
+ }
1133
+ function shapeHalfExtent(shape, q) {
1134
+ if (shape.kind === 'box') {
1135
+ const rot = quatToMat3Cached(q);
1136
+ return {
1137
+ x: Math.abs(rot[0]) * shape.half.x + Math.abs(rot[1]) * shape.half.y + Math.abs(rot[2]) * shape.half.z,
1138
+ y: Math.abs(rot[3]) * shape.half.x + Math.abs(rot[4]) * shape.half.y + Math.abs(rot[5]) * shape.half.z,
1139
+ z: Math.abs(rot[6]) * shape.half.x + Math.abs(rot[7]) * shape.half.y + Math.abs(rot[8]) * shape.half.z,
1140
+ };
1141
+ }
1142
+ if (shape.kind === 'sphere') {
1143
+ return { x: shape.radius, y: shape.radius, z: shape.radius };
1144
+ }
1145
+ if (shape.kind === 'capsule') {
1146
+ const reach = shape.radius + shape.halfHeight;
1147
+ return { x: reach, y: reach, z: reach };
1148
+ }
1149
+ if (shape.kind === 'mesh') {
1150
+ let ext = { x: 0, y: 0, z: 0 };
1151
+ for (const tri of shape.triangles) {
1152
+ for (const vertex of tri) {
1153
+ const rotated = rotatePointByQuat(q, vertex);
1154
+ ext = { x: Math.max(ext.x, Math.abs(rotated.x)), y: Math.max(ext.y, Math.abs(rotated.y)), z: Math.max(ext.z, Math.abs(rotated.z)) };
1155
+ }
1156
+ }
1157
+ return ext;
1158
+ }
1159
+ // compound: union of child extents about the body center.
1160
+ let ext = { x: 0, y: 0, z: 0 };
1161
+ for (const child of shape.children) {
1162
+ const childCenter = rotatePointByQuat(q, child.offset);
1163
+ const childHalf = shapeHalfExtent(child.shape, quatMultiply(q, child.q));
1164
+ ext = {
1165
+ x: Math.max(ext.x, Math.abs(childCenter.x) + childHalf.x),
1166
+ y: Math.max(ext.y, Math.abs(childCenter.y) + childHalf.y),
1167
+ z: Math.max(ext.z, Math.abs(childCenter.z) + childHalf.z),
1168
+ };
1169
+ }
1170
+ return ext;
1171
+ }
1172
+ function speculativeMarginFor(a, b, dt) {
1173
+ const relSpeed = solverSqrt(Math.max(dotVec3(a.v, a.v), dotVec3(b.v, b.v)));
1174
+ return relSpeed * dt + SOLVER_CONSTANTS.SLOP;
1175
+ }
1176
+ function toGeom(b) {
1177
+ return { index: b.index, p: b.p, q: b.q, shape: b.shape };
1178
+ }
1179
+ function wakeIfContacted(sleeper, other) {
1180
+ if (sleeper.isDynamic && sleeper.sleeping && other.isDynamic && !other.sleeping
1181
+ && dotVec3(other.v, other.v) + dotVec3(other.w, other.w) > SOLVER_CONSTANTS.WAKE_VEL2) {
1182
+ sleeper.sleeping = false;
1183
+ sleeper.lowVelFrames = 0;
1184
+ }
1185
+ }
1186
+ function applyImpulse(bodies, invMass, invInertia, a, b, rA, rB, P) {
1187
+ const A = bodies[a];
1188
+ const B = bodies[b];
1189
+ if (solid(A)) {
1190
+ A.v = subVec3(A.v, mulComponents(P, invMass[a]));
1191
+ A.w = subVec3(A.w, mulMat3Vec(invInertia[a], crossVec3(rA, P)));
1192
+ }
1193
+ if (solid(B)) {
1194
+ B.v = addVec3(B.v, mulComponents(P, invMass[b]));
1195
+ B.w = addVec3(B.w, mulMat3Vec(invInertia[b], crossVec3(rB, P)));
1196
+ }
1197
+ }
1198
+ function applyBiasImpulseAt(bodies, invMass, invInertia, a, b, rA, rB, P) {
1199
+ const A = bodies[a];
1200
+ const B = bodies[b];
1201
+ if (solid(A)) {
1202
+ A.vb = subVec3(A.vb, mulComponents(P, invMass[a]));
1203
+ A.wb = subVec3(A.wb, mulMat3Vec(invInertia[a], crossVec3(rA, P)));
1204
+ }
1205
+ if (solid(B)) {
1206
+ B.vb = addVec3(B.vb, mulComponents(P, invMass[b]));
1207
+ B.wb = addVec3(B.wb, mulMat3Vec(invInertia[b], crossVec3(rB, P)));
1208
+ }
1209
+ }
1210
+ function mulComponents(a, b) {
1211
+ return { x: a.x * b.x, y: a.y * b.y, z: a.z * b.z };
1212
+ }
1213
+ function relativeBiasVelocity(A, B, rA, rB) {
1214
+ return subVec3(addVec3(B.vb, crossVec3(B.wb, rB)), addVec3(A.vb, crossVec3(A.wb, rA)));
1215
+ }
1216
+ /**
1217
+ * Scalar apply of impulse (px,py,pz) at rA/rB — same operations in the same
1218
+ * order as the vector-helper form (bit-identical results), with zero
1219
+ * intermediate allocations. This runs per contact per velocity iteration.
1220
+ */
1221
+ function applyImpulseScalar(bodies, invMass, invInertia, a, b, rA, rB, px, py, pz, bias) {
1222
+ const A = bodies[a];
1223
+ const B = bodies[b];
1224
+ if (solid(A)) {
1225
+ const cx = rA.y * pz - rA.z * py;
1226
+ const cy = rA.z * px - rA.x * pz;
1227
+ const cz = rA.x * py - rA.y * px;
1228
+ const m = invInertia[a];
1229
+ const inv = invMass[a];
1230
+ const lin = bias ? A.vb : A.v;
1231
+ const ang = bias ? A.wb : A.w;
1232
+ lin.x = lin.x - px * inv.x;
1233
+ lin.y = lin.y - py * inv.y;
1234
+ lin.z = lin.z - pz * inv.z;
1235
+ ang.x = ang.x - (m[0] * cx + m[1] * cy + m[2] * cz);
1236
+ ang.y = ang.y - (m[3] * cx + m[4] * cy + m[5] * cz);
1237
+ ang.z = ang.z - (m[6] * cx + m[7] * cy + m[8] * cz);
1238
+ }
1239
+ if (solid(B)) {
1240
+ const cx = rB.y * pz - rB.z * py;
1241
+ const cy = rB.z * px - rB.x * pz;
1242
+ const cz = rB.x * py - rB.y * px;
1243
+ const m = invInertia[b];
1244
+ const inv = invMass[b];
1245
+ const lin = bias ? B.vb : B.v;
1246
+ const ang = bias ? B.wb : B.w;
1247
+ lin.x = lin.x + px * inv.x;
1248
+ lin.y = lin.y + py * inv.y;
1249
+ lin.z = lin.z + pz * inv.z;
1250
+ ang.x = ang.x + (m[0] * cx + m[1] * cy + m[2] * cz);
1251
+ ang.y = ang.y + (m[3] * cx + m[4] * cy + m[5] * cz);
1252
+ ang.z = ang.z + (m[6] * cx + m[7] * cy + m[8] * cz);
1253
+ }
1254
+ }
1255
+ /** Relative velocity at the contact along direction (dx,dy,dz) — scalar form of dot(relativeVelocity, d). */
1256
+ function relativeVelocityAlong(A, B, rA, rB, dx, dy, dz) {
1257
+ const relX = (B.v.x + (B.w.y * rB.z - B.w.z * rB.y)) - (A.v.x + (A.w.y * rA.z - A.w.z * rA.y));
1258
+ const relY = (B.v.y + (B.w.z * rB.x - B.w.x * rB.z)) - (A.v.y + (A.w.z * rA.x - A.w.x * rA.z));
1259
+ const relZ = (B.v.z + (B.w.x * rB.y - B.w.y * rB.x)) - (A.v.z + (A.w.x * rA.y - A.w.y * rA.x));
1260
+ return relX * dx + relY * dy + relZ * dz;
1261
+ }
1262
+ function solveTangentImpulse(bodies, invMass, invInertia, c, A, B, t, mass, acc, maxF) {
1263
+ const vt = relativeVelocityAlong(A, B, c.rA, c.rB, t.x, t.y, t.z);
1264
+ let dF = mass * -vt;
1265
+ const clamped = clamp(acc + dF, -maxF, maxF);
1266
+ dF = clamped - acc;
1267
+ applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, t.x * dF, t.y * dF, t.z * dF, false);
1268
+ return clamped;
1269
+ }
1270
+ function solveContactConstraint(bodies, invMass, invInertia, c, dt) {
1271
+ const A = bodies[c.a];
1272
+ const B = bodies[c.b];
1273
+ // normal (no Baumgarte — position error handled by split impulse)
1274
+ const vn = relativeVelocityAlong(A, B, c.rA, c.rB, c.normal.x, c.normal.y, c.normal.z);
1275
+ const restThreshold = SOLVER_CONSTANTS.RESTITUTION_THRESHOLD;
1276
+ const restBias = vn < -restThreshold ? -c.restitution * vn : 0;
1277
+ // speculative points: allow closing up to the gap, never push apart.
1278
+ const speculativeBias = c.speculative ? c.penetration / dt : 0;
1279
+ let dLambda = c.massN * (-(vn) + restBias + speculativeBias);
1280
+ const newAcc = Math.max(0, c.accN + dLambda);
1281
+ dLambda = newAcc - c.accN;
1282
+ c.accN = newAcc;
1283
+ applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, c.normal.x * dLambda, c.normal.y * dLambda, c.normal.z * dLambda, false);
1284
+ // split-impulse position bias (skip speculative points).
1285
+ if (!c.speculative) {
1286
+ const relbX = (B.vb.x + (B.wb.y * c.rB.z - B.wb.z * c.rB.y)) - (A.vb.x + (A.wb.y * c.rA.z - A.wb.z * c.rA.y));
1287
+ const relbY = (B.vb.y + (B.wb.z * c.rB.x - B.wb.x * c.rB.z)) - (A.vb.y + (A.wb.z * c.rA.x - A.wb.x * c.rA.z));
1288
+ const relbZ = (B.vb.z + (B.wb.x * c.rB.y - B.wb.y * c.rB.x)) - (A.vb.z + (A.wb.x * c.rA.y - A.wb.y * c.rA.x));
1289
+ const vnb = relbX * c.normal.x + relbY * c.normal.y + relbZ * c.normal.z;
1290
+ let dLb = c.massN * (c.bias - vnb);
1291
+ const newAccNb = Math.max(0, c.accNb + dLb);
1292
+ dLb = newAccNb - c.accNb;
1293
+ c.accNb = newAccNb;
1294
+ applyImpulseScalar(bodies, invMass, invInertia, c.a, c.b, c.rA, c.rB, c.normal.x * dLb, c.normal.y * dLb, c.normal.z * dLb, true);
1295
+ }
1296
+ // friction (skip for pure speculative points).
1297
+ if (c.speculative) {
1298
+ return;
1299
+ }
1300
+ const maxF = c.mu * c.accN;
1301
+ c.accT1 = solveTangentImpulse(bodies, invMass, invInertia, c, A, B, c.t1, c.massT1, c.accT1, maxF);
1302
+ c.accT2 = solveTangentImpulse(bodies, invMass, invInertia, c, A, B, c.t2, c.massT2, c.accT2, maxF);
1303
+ }
1304
+ function islandSleep(bodies, manifolds, joints) {
1305
+ const parent = new Map();
1306
+ const find = (i) => {
1307
+ let r = i;
1308
+ while (parent.get(r) !== r) {
1309
+ r = parent.get(r);
1310
+ }
1311
+ return r;
1312
+ };
1313
+ for (let i = 0; i < bodies.length; i += 1) {
1314
+ if (solid(bodies[i])) {
1315
+ parent.set(i, i);
1316
+ }
1317
+ }
1318
+ const link = (a, b) => {
1319
+ if (solid(bodies[a]) && solid(bodies[b])) {
1320
+ parent.set(find(a), find(b));
1321
+ }
1322
+ };
1323
+ for (const m of manifolds) {
1324
+ link(m.a, m.b);
1325
+ }
1326
+ for (const joint of joints) {
1327
+ for (let i = 1; i < joint.bodyIndices.length; i += 1) {
1328
+ link(joint.bodyIndices[0], joint.bodyIndices[i]);
1329
+ }
1330
+ }
1331
+ const islandReady = new Map();
1332
+ for (const i of [...parent.keys()].sort((l, r) => l - r)) {
1333
+ const root = find(i);
1334
+ const ready = bodies[i].lowVelFrames >= SOLVER_CONSTANTS.SLEEP_FRAMES;
1335
+ islandReady.set(root, (islandReady.get(root) ?? true) && ready);
1336
+ }
1337
+ for (const i of [...parent.keys()].sort((l, r) => l - r)) {
1338
+ if (islandReady.get(find(i)) === true) {
1339
+ const b = bodies[i];
1340
+ b.sleeping = true;
1341
+ b.v = { x: 0, y: 0, z: 0 };
1342
+ b.w = { x: 0, y: 0, z: 0 };
1343
+ }
1344
+ }
1345
+ }
1346
+ export const __solverInternals = { closestPointOnSegment, closestSegmentSegment, closestPointOnTriangle, triangleNormal };