@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/dist/solver.js ADDED
@@ -0,0 +1,601 @@
1
+ import { BODY_DYNAMIC } from './bodies.js';
2
+ /**
3
+ * Contact constraints, and the substepped soft solve over them.
4
+ *
5
+ * **Soft rather than Baumgarte, and the difference is a property rather than a preference.** Every
6
+ * constraint carries a *hertz* and a damping ratio, from which the bias rate, mass scale and
7
+ * impulse scale are derived using the substep length. Stating stiffness as a frequency is what
8
+ * makes the substep count a quality dial: turn it up and nothing moves differently. A Baumgarte
9
+ * bias is a fraction of penetration per step, so it means something different at every step length,
10
+ * and the mitigations for that are all tuning constants — which `AGENTS.md` says never to assert.
11
+ *
12
+ * **Nothing here recomputes a manifold.** Anchors are stored in body-local frame at prepare time
13
+ * along with a separation offset, so the current separation is exact at any point inside the tick
14
+ * without re-running narrowphase per substep. `s = sBase + dot((pB + rB') − (pA + rA'), n)` is that
15
+ * identity, and it holds because `sBase` is defined to make it hold at prepare.
16
+ *
17
+ * **Every entry point takes an order array rather than a contiguous range.** Islands are disjoint
18
+ * by construction, so they can be solved in any order or simultaneously — but a constraint list
19
+ * built in pair order is not grouped by island. Indirecting through an order array groups them
20
+ * without permuting eleven parallel arrays, and it is what lets one `solveIsland` serve both
21
+ * executors.
22
+ *
23
+ * **Friction is solved before the normal**, against the normal impulse accumulated so far. Solving
24
+ * it after means the first iteration has no normal impulse to bound against and applies none, so a
25
+ * resting body slides for one iteration every substep.
26
+ */
27
+ /** Contacts start with this much slack allowed before the bias fights them, in metres. */
28
+ export const LINEAR_SLOP = 0.005;
29
+ /** How fast the bias may push overlap out, in metres per second. */
30
+ export const MAX_BIAS_VELOCITY = 4;
31
+ /** Below this approach speed a bounce is not worth having, and jitters if it is applied. */
32
+ export const RESTITUTION_THRESHOLD = 1;
33
+ export class ContactConstraints {
34
+ /** One entry per manifold. */
35
+ count = 0;
36
+ bodyA;
37
+ bodyB;
38
+ nx;
39
+ ny;
40
+ nz;
41
+ t1x;
42
+ t1y;
43
+ t1z;
44
+ t2x;
45
+ t2y;
46
+ t2z;
47
+ friction;
48
+ restitution;
49
+ pointStart;
50
+ pointCount;
51
+ /** One entry per contact point. */
52
+ points = 0;
53
+ localA;
54
+ localB;
55
+ sBase;
56
+ normalMass;
57
+ tangent1Mass;
58
+ tangent2Mass;
59
+ normalImpulse;
60
+ tangent1Impulse;
61
+ tangent2Impulse;
62
+ /** Approach speed when the constraint was prepared, which is what restitution answers to. */
63
+ approach;
64
+ featureId;
65
+ /**
66
+ * Whether each side's anchor sits on a curved surface, one byte per point.
67
+ *
68
+ * A curved anchor is held in world frame rather than in the body's, for the reason `Manifold`
69
+ * states at length: the point where a ball touches the floor does not turn with the ball, and an
70
+ * anchor that turns anyway climbs its side and reports a gap that is not there. Per point rather
71
+ * than per manifold so `worldAnchors` needs only the point index, which is all its callers have.
72
+ */
73
+ curvedA;
74
+ curvedB;
75
+ cap;
76
+ pcap;
77
+ constructor(capacity = 128) {
78
+ this.cap = capacity;
79
+ this.pcap = capacity * 4;
80
+ this.bodyA = new Int32Array(this.cap);
81
+ this.bodyB = new Int32Array(this.cap);
82
+ this.nx = new Float32Array(this.cap);
83
+ this.ny = new Float32Array(this.cap);
84
+ this.nz = new Float32Array(this.cap);
85
+ this.t1x = new Float32Array(this.cap);
86
+ this.t1y = new Float32Array(this.cap);
87
+ this.t1z = new Float32Array(this.cap);
88
+ this.t2x = new Float32Array(this.cap);
89
+ this.t2y = new Float32Array(this.cap);
90
+ this.t2z = new Float32Array(this.cap);
91
+ this.friction = new Float32Array(this.cap);
92
+ this.restitution = new Float32Array(this.cap);
93
+ this.pointStart = new Int32Array(this.cap);
94
+ this.pointCount = new Int32Array(this.cap);
95
+ this.localA = new Float32Array(this.pcap * 3);
96
+ this.localB = new Float32Array(this.pcap * 3);
97
+ this.sBase = new Float32Array(this.pcap);
98
+ this.normalMass = new Float32Array(this.pcap);
99
+ this.tangent1Mass = new Float32Array(this.pcap);
100
+ this.tangent2Mass = new Float32Array(this.pcap);
101
+ this.normalImpulse = new Float32Array(this.pcap);
102
+ this.tangent1Impulse = new Float32Array(this.pcap);
103
+ this.tangent2Impulse = new Float32Array(this.pcap);
104
+ this.approach = new Float32Array(this.pcap);
105
+ this.featureId = new Int32Array(this.pcap);
106
+ this.curvedA = new Uint8Array(this.pcap);
107
+ this.curvedB = new Uint8Array(this.pcap);
108
+ }
109
+ clear() {
110
+ this.count = 0;
111
+ this.points = 0;
112
+ }
113
+ reserve(pointsNeeded) {
114
+ if (this.count === this.cap)
115
+ this.growConstraints();
116
+ while (this.points + pointsNeeded > this.pcap)
117
+ this.growPoints();
118
+ }
119
+ growConstraints() {
120
+ this.cap *= 2;
121
+ const g = (a) => {
122
+ const out = new a.constructor(this.cap);
123
+ out.set(a);
124
+ return out;
125
+ };
126
+ this.bodyA = g(this.bodyA);
127
+ this.bodyB = g(this.bodyB);
128
+ this.nx = g(this.nx);
129
+ this.ny = g(this.ny);
130
+ this.nz = g(this.nz);
131
+ this.t1x = g(this.t1x);
132
+ this.t1y = g(this.t1y);
133
+ this.t1z = g(this.t1z);
134
+ this.t2x = g(this.t2x);
135
+ this.t2y = g(this.t2y);
136
+ this.t2z = g(this.t2z);
137
+ this.friction = g(this.friction);
138
+ this.restitution = g(this.restitution);
139
+ this.pointStart = g(this.pointStart);
140
+ this.pointCount = g(this.pointCount);
141
+ }
142
+ growPoints() {
143
+ this.pcap *= 2;
144
+ const g = (a, stride = 1) => {
145
+ const out = new a.constructor(this.pcap * stride);
146
+ out.set(a);
147
+ return out;
148
+ };
149
+ this.localA = g(this.localA, 3);
150
+ this.localB = g(this.localB, 3);
151
+ this.sBase = g(this.sBase);
152
+ this.normalMass = g(this.normalMass);
153
+ this.tangent1Mass = g(this.tangent1Mass);
154
+ this.tangent2Mass = g(this.tangent2Mass);
155
+ this.normalImpulse = g(this.normalImpulse);
156
+ this.tangent1Impulse = g(this.tangent1Impulse);
157
+ this.tangent2Impulse = g(this.tangent2Impulse);
158
+ this.approach = g(this.approach);
159
+ this.featureId = g(this.featureId);
160
+ this.curvedA = g(this.curvedA);
161
+ this.curvedB = g(this.curvedB);
162
+ }
163
+ }
164
+ const invIA = new Float32Array(6);
165
+ const invIB = new Float32Array(6);
166
+ /**
167
+ * Turn one manifold into a constraint, with its effective masses and its local anchors.
168
+ *
169
+ * `warm` supplies the previous tick's impulses by feature id, or −1 where there is none. Warm
170
+ * starting is what lets a stack converge in a handful of iterations rather than a hundred.
171
+ */
172
+ export function prepareContact(bodies, a, b, m, c, warmNormal, warmT1, warmT2) {
173
+ c.reserve(m.count);
174
+ const k = c.count++;
175
+ c.bodyA[k] = a;
176
+ c.bodyB[k] = b;
177
+ c.nx[k] = m.nx;
178
+ c.ny[k] = m.ny;
179
+ c.nz[k] = m.nz;
180
+ // An orthonormal basis around the normal, built from comparisons and one square root.
181
+ let t1x;
182
+ let t1y;
183
+ let t1z;
184
+ if (Math.abs(m.nx) >= 0.57735) {
185
+ t1x = m.ny;
186
+ t1y = -m.nx;
187
+ t1z = 0;
188
+ }
189
+ else {
190
+ t1x = 0;
191
+ t1y = m.nz;
192
+ t1z = -m.ny;
193
+ }
194
+ const tl = Math.sqrt(t1x * t1x + t1y * t1y + t1z * t1z) || 1;
195
+ t1x /= tl;
196
+ t1y /= tl;
197
+ t1z /= tl;
198
+ c.t1x[k] = t1x;
199
+ c.t1y[k] = t1y;
200
+ c.t1z[k] = t1z;
201
+ c.t2x[k] = m.ny * t1z - m.nz * t1y;
202
+ c.t2y[k] = m.nz * t1x - m.nx * t1z;
203
+ c.t2z[k] = m.nx * t1y - m.ny * t1x;
204
+ // A pair's friction and bounce are the geometric and the larger mean of the two bodies'.
205
+ c.friction[k] = Math.sqrt((bodies.friction[a] ?? 0) * (bodies.friction[b] ?? 0));
206
+ const ra = bodies.restitution[a] ?? 0;
207
+ const rb = bodies.restitution[b] ?? 0;
208
+ c.restitution[k] = ra > rb ? ra : rb;
209
+ bodies.worldInverseInertia(a, invIA);
210
+ bodies.worldInverseInertia(b, invIB);
211
+ const imA = bodies.invMass[a] ?? 0;
212
+ const imB = bodies.invMass[b] ?? 0;
213
+ c.pointStart[k] = c.points;
214
+ c.pointCount[k] = m.count;
215
+ for (let i = 0; i < m.count; i++) {
216
+ const p = c.points++;
217
+ const px = m.points[i * 3] ?? 0;
218
+ const py = m.points[i * 3 + 1] ?? 0;
219
+ const pz = m.points[i * 3 + 2] ?? 0;
220
+ const rax = px - (bodies.posX[a] ?? 0);
221
+ const ray = py - (bodies.posY[a] ?? 0);
222
+ const raz = pz - (bodies.posZ[a] ?? 0);
223
+ const rbx = px - (bodies.posX[b] ?? 0);
224
+ const rby = py - (bodies.posY[b] ?? 0);
225
+ const rbz = pz - (bodies.posZ[b] ?? 0);
226
+ /*
227
+ * A flat anchor is stored in its body's frame and turned forward with it; a curved one is
228
+ * stored in world frame and left alone. Both are read back by `worldAnchors`, which reads the
229
+ * same flag — so the pair of them is one decision written in two places and nowhere else.
230
+ */
231
+ c.curvedA[p] = m.curvedA ? 1 : 0;
232
+ c.curvedB[p] = m.curvedB ? 1 : 0;
233
+ if (m.curvedA) {
234
+ c.localA[p * 3] = rax;
235
+ c.localA[p * 3 + 1] = ray;
236
+ c.localA[p * 3 + 2] = raz;
237
+ }
238
+ else {
239
+ unrotate(bodies, a, rax, ray, raz, c.localA, p * 3);
240
+ }
241
+ if (m.curvedB) {
242
+ c.localB[p * 3] = rbx;
243
+ c.localB[p * 3 + 1] = rby;
244
+ c.localB[p * 3 + 2] = rbz;
245
+ }
246
+ else {
247
+ unrotate(bodies, b, rbx, rby, rbz, c.localB, p * 3);
248
+ }
249
+ /*
250
+ * The offset that makes `s = sBase + dot((pB + rB') − (pA + rA'), n)` reproduce this tick's
251
+ * separation exactly, and every later substep's without a second narrowphase.
252
+ */
253
+ const nx = m.nx;
254
+ const ny = m.ny;
255
+ const nz = m.nz;
256
+ const now = ((bodies.posX[b] ?? 0) + rbx - (bodies.posX[a] ?? 0) - rax) * nx +
257
+ ((bodies.posY[b] ?? 0) + rby - (bodies.posY[a] ?? 0) - ray) * ny +
258
+ ((bodies.posZ[b] ?? 0) + rbz - (bodies.posZ[a] ?? 0) - raz) * nz;
259
+ c.sBase[p] = (m.separations[i] ?? 0) - now;
260
+ c.normalMass[p] = effectiveMass(imA, imB, rax, ray, raz, rbx, rby, rbz, nx, ny, nz);
261
+ c.tangent1Mass[p] = effectiveMass(imA, imB, rax, ray, raz, rbx, rby, rbz, t1x, t1y, t1z);
262
+ c.tangent2Mass[p] = effectiveMass(imA, imB, rax, ray, raz, rbx, rby, rbz, c.t2x[k] ?? 0, c.t2y[k] ?? 0, c.t2z[k] ?? 0);
263
+ relativeVelocity(bodies, a, b, rax, ray, raz, rbx, rby, rbz);
264
+ c.approach[p] = REL[0] * nx + REL[1] * ny + REL[2] * nz;
265
+ c.featureId[p] = m.featureIds[i] ?? 0;
266
+ c.normalImpulse[p] = warmNormal ? (warmNormal[i] ?? 0) : 0;
267
+ c.tangent1Impulse[p] = warmT1 ? (warmT1[i] ?? 0) : 0;
268
+ c.tangent2Impulse[p] = warmT2 ? (warmT2[i] ?? 0) : 0;
269
+ }
270
+ }
271
+ /** Apply the impulses carried over from the previous tick, before the first iteration. */
272
+ export function warmStart(bodies, c, order, from, to) {
273
+ for (let oi = from; oi < to; oi++) {
274
+ const k = order[oi] ?? 0;
275
+ const a = c.bodyA[k] ?? 0;
276
+ const b = c.bodyB[k] ?? 0;
277
+ bodies.worldInverseInertia(a, invIA);
278
+ bodies.worldInverseInertia(b, invIB);
279
+ const start = c.pointStart[k] ?? 0;
280
+ const end = start + (c.pointCount[k] ?? 0);
281
+ for (let p = start; p < end; p++) {
282
+ worldAnchors(bodies, a, b, c, p);
283
+ const px = (c.nx[k] ?? 0) * (c.normalImpulse[p] ?? 0) +
284
+ (c.t1x[k] ?? 0) * (c.tangent1Impulse[p] ?? 0) +
285
+ (c.t2x[k] ?? 0) * (c.tangent2Impulse[p] ?? 0);
286
+ const py = (c.ny[k] ?? 0) * (c.normalImpulse[p] ?? 0) +
287
+ (c.t1y[k] ?? 0) * (c.tangent1Impulse[p] ?? 0) +
288
+ (c.t2y[k] ?? 0) * (c.tangent2Impulse[p] ?? 0);
289
+ const pz = (c.nz[k] ?? 0) * (c.normalImpulse[p] ?? 0) +
290
+ (c.t1z[k] ?? 0) * (c.tangent1Impulse[p] ?? 0) +
291
+ (c.t2z[k] ?? 0) * (c.tangent2Impulse[p] ?? 0);
292
+ applyImpulse(bodies, a, b, ANCHOR, px, py, pz);
293
+ }
294
+ }
295
+ }
296
+ export function solveContacts(bodies, c, order, from, to, h, useBias, hertz, damping,
297
+ /* Defaulted so a consumer calling this directly — it is exported — keeps its old numbers. */
298
+ friction = 'box') {
299
+ // The soft-constraint triple, from a frequency rather than from a per-step fraction.
300
+ const omega = 2 * Math.PI * hertz;
301
+ const a1 = 2 * damping + h * omega;
302
+ const a2 = h * omega * a1;
303
+ const a3 = 1 / (1 + a2);
304
+ const biasRate = omega / a1;
305
+ const massScale = a2 * a3;
306
+ const impulseScale = a3;
307
+ const invH = h > 0 ? 1 / h : 0;
308
+ for (let oi = from; oi < to; oi++) {
309
+ const k = order[oi] ?? 0;
310
+ const a = c.bodyA[k] ?? 0;
311
+ const b = c.bodyB[k] ?? 0;
312
+ bodies.worldInverseInertia(a, invIA);
313
+ bodies.worldInverseInertia(b, invIB);
314
+ const nx = c.nx[k] ?? 0;
315
+ const ny = c.ny[k] ?? 0;
316
+ const nz = c.nz[k] ?? 0;
317
+ const mu = c.friction[k] ?? 0;
318
+ const start = c.pointStart[k] ?? 0;
319
+ const end = start + (c.pointCount[k] ?? 0);
320
+ /*
321
+ * Friction first, bounded by the normal impulse standing from the previous iteration. Solving
322
+ * it after the normal costs the first iteration of every substep its friction entirely, because
323
+ * there is no accumulated normal impulse to bound against yet.
324
+ */
325
+ for (let p = start; p < end; p++) {
326
+ worldAnchors(bodies, a, b, c, p);
327
+ relativeVelocity(bodies, a, b, ANCHOR[0], ANCHOR[1], ANCHOR[2], ANCHOR[3], ANCHOR[4], ANCHOR[5]);
328
+ const maxF = mu * (c.normalImpulse[p] ?? 0);
329
+ if (friction === 'elliptical') {
330
+ solveFrictionCone(bodies, a, b, k, p, c, maxF);
331
+ }
332
+ else {
333
+ solveTangent(bodies, a, b, p, c.t1x[k] ?? 0, c.t1y[k] ?? 0, c.t1z[k] ?? 0, c.tangent1Mass, c.tangent1Impulse, maxF);
334
+ relativeVelocity(bodies, a, b, ANCHOR[0], ANCHOR[1], ANCHOR[2], ANCHOR[3], ANCHOR[4], ANCHOR[5]);
335
+ solveTangent(bodies, a, b, p, c.t2x[k] ?? 0, c.t2y[k] ?? 0, c.t2z[k] ?? 0, c.tangent2Mass, c.tangent2Impulse, maxF);
336
+ }
337
+ }
338
+ for (let p = start; p < end; p++) {
339
+ worldAnchors(bodies, a, b, c, p);
340
+ const s = (c.sBase[p] ?? 0) +
341
+ ((bodies.posX[b] ?? 0) + ANCHOR[3] - (bodies.posX[a] ?? 0) - ANCHOR[0]) * nx +
342
+ ((bodies.posY[b] ?? 0) + ANCHOR[4] - (bodies.posY[a] ?? 0) - ANCHOR[1]) * ny +
343
+ ((bodies.posZ[b] ?? 0) + ANCHOR[5] - (bodies.posZ[a] ?? 0) - ANCHOR[2]) * nz;
344
+ relativeVelocity(bodies, a, b, ANCHOR[0], ANCHOR[1], ANCHOR[2], ANCHOR[3], ANCHOR[4], ANCHOR[5]);
345
+ const vn = REL[0] * nx + REL[1] * ny + REL[2] * nz;
346
+ let bias = 0;
347
+ let ms = 1;
348
+ let is = 0;
349
+ if (s > 0) {
350
+ // Speculative: close exactly the gap this substep and no more.
351
+ bias = s * invH;
352
+ }
353
+ else if (useBias) {
354
+ const want = biasRate * (s + LINEAR_SLOP);
355
+ bias = want < -MAX_BIAS_VELOCITY ? -MAX_BIAS_VELOCITY : want > 0 ? 0 : want;
356
+ ms = massScale;
357
+ is = impulseScale;
358
+ }
359
+ const old = c.normalImpulse[p] ?? 0;
360
+ let impulse = -(c.normalMass[p] ?? 0) * ms * (vn + bias) - is * old;
361
+ const next = old + impulse > 0 ? old + impulse : 0;
362
+ impulse = next - old;
363
+ c.normalImpulse[p] = next;
364
+ applyImpulse(bodies, a, b, ANCHOR, nx * impulse, ny * impulse, nz * impulse);
365
+ }
366
+ }
367
+ }
368
+ /**
369
+ * One pass after every substep, giving back the bounce the solve suppressed.
370
+ *
371
+ * Applied once against the approach speed captured at prepare, so a drop returns to the same height
372
+ * whatever the substep count. A bounce driven from the *current* velocity instead compounds with
373
+ * every substep and turns the coefficient into a function of the dial.
374
+ */
375
+ export function applyRestitution(bodies, c, order, from, to) {
376
+ for (let oi = from; oi < to; oi++) {
377
+ const k = order[oi] ?? 0;
378
+ const e = c.restitution[k] ?? 0;
379
+ if (e === 0)
380
+ continue;
381
+ const a = c.bodyA[k] ?? 0;
382
+ const b = c.bodyB[k] ?? 0;
383
+ bodies.worldInverseInertia(a, invIA);
384
+ bodies.worldInverseInertia(b, invIB);
385
+ const nx = c.nx[k] ?? 0;
386
+ const ny = c.ny[k] ?? 0;
387
+ const nz = c.nz[k] ?? 0;
388
+ const start = c.pointStart[k] ?? 0;
389
+ const end = start + (c.pointCount[k] ?? 0);
390
+ for (let p = start; p < end; p++) {
391
+ const approach = c.approach[p] ?? 0;
392
+ if (approach > -RESTITUTION_THRESHOLD || (c.normalImpulse[p] ?? 0) === 0)
393
+ continue;
394
+ worldAnchors(bodies, a, b, c, p);
395
+ relativeVelocity(bodies, a, b, ANCHOR[0], ANCHOR[1], ANCHOR[2], ANCHOR[3], ANCHOR[4], ANCHOR[5]);
396
+ const vn = REL[0] * nx + REL[1] * ny + REL[2] * nz;
397
+ const old = c.normalImpulse[p] ?? 0;
398
+ let impulse = -(c.normalMass[p] ?? 0) * (vn + e * approach);
399
+ const next = old + impulse > 0 ? old + impulse : 0;
400
+ impulse = next - old;
401
+ c.normalImpulse[p] = next;
402
+ applyImpulse(bodies, a, b, ANCHOR, nx * impulse, ny * impulse, nz * impulse);
403
+ }
404
+ }
405
+ }
406
+ /* ---------- shared scratch, so nothing allocates per contact ---------- */
407
+ const ANCHOR = new Float64Array(6);
408
+ const REL = new Float64Array(3);
409
+ /**
410
+ * One friction axis, solved against the anchors and relative velocity already in scratch.
411
+ *
412
+ * **It takes no `ContactConstraints`, and it used to.** The parameter was dead: everything this
413
+ * needs is either in the module-scope `REL` and `ANCHOR` that the caller filled, or in the two
414
+ * arrays it is handed. A parameter nothing reads is a claim about what a function depends on, and
415
+ * this one said it depended on the whole constraint block.
416
+ *
417
+ * Found by a consumer's typecheck rather than by this repository's: one of them runs
418
+ * `noUnusedParameters` over engine source where the engine's own config does not, on the grounds
419
+ * that the flag costs the engine nothing and catches a real class of mistake. This is it being
420
+ * right, and it arrived as a red CI in a repository that had changed nothing.
421
+ */
422
+ function solveTangent(bodies, a, b, p, tx, ty, tz, massArray, impulseArray, maxF) {
423
+ const vt = REL[0] * tx + REL[1] * ty + REL[2] * tz;
424
+ const old = impulseArray[p] ?? 0;
425
+ let impulse = -(massArray[p] ?? 0) * vt;
426
+ let next = old + impulse;
427
+ if (next > maxF)
428
+ next = maxF;
429
+ else if (next < -maxF)
430
+ next = -maxF;
431
+ impulse = next - old;
432
+ impulseArray[p] = next;
433
+ applyImpulse(bodies, a, b, ANCHOR, tx * impulse, ty * impulse, tz * impulse);
434
+ }
435
+ /**
436
+ * Both friction axes at once, with the pair projected onto a disc of radius `maxF`.
437
+ *
438
+ * **Two differences from the pair of `solveTangent` calls it replaces, and only one of them is the
439
+ * clamp.** The clamp is the point: the accumulated `(P1, P2)` is scaled onto the circle rather than
440
+ * each component being clipped to a square, so the total friction impulse is `mu * N` whatever the
441
+ * heading. The other is that **both rows read one relative velocity** — the box path re-reads it
442
+ * between the two axes, which makes the second row see the first row's push, and there is no
443
+ * meaningful way to keep that while solving them jointly. That is why this is not a change a stored
444
+ * replay survives, and why it is behind an option.
445
+ *
446
+ * The impulse is applied once, as the sum of the two deltas, rather than twice: two applications of
447
+ * a jointly-solved pair would each disturb the other's velocity between them, which is the sequential
448
+ * behaviour this exists to leave behind.
449
+ */
450
+ function solveFrictionCone(bodies, a, b, k, p, c, maxF) {
451
+ const t1x = c.t1x[k] ?? 0;
452
+ const t1y = c.t1y[k] ?? 0;
453
+ const t1z = c.t1z[k] ?? 0;
454
+ const t2x = c.t2x[k] ?? 0;
455
+ const t2y = c.t2y[k] ?? 0;
456
+ const t2z = c.t2z[k] ?? 0;
457
+ const vt1 = REL[0] * t1x + REL[1] * t1y + REL[2] * t1z;
458
+ const vt2 = REL[0] * t2x + REL[1] * t2y + REL[2] * t2z;
459
+ const old1 = c.tangent1Impulse[p] ?? 0;
460
+ const old2 = c.tangent2Impulse[p] ?? 0;
461
+ let next1 = old1 - (c.tangent1Mass[p] ?? 0) * vt1;
462
+ let next2 = old2 - (c.tangent2Mass[p] ?? 0) * vt2;
463
+ const length = Math.sqrt(next1 * next1 + next2 * next2);
464
+ if (length > maxF) {
465
+ /* `length > maxF` implies `length > 0` for a non-negative bound, so the divide is safe: a zero
466
+ normal impulse gives `maxF = 0` and a zero length fails the compare. */
467
+ const scale = maxF / length;
468
+ next1 *= scale;
469
+ next2 *= scale;
470
+ }
471
+ const delta1 = next1 - old1;
472
+ const delta2 = next2 - old2;
473
+ c.tangent1Impulse[p] = next1;
474
+ c.tangent2Impulse[p] = next2;
475
+ applyImpulse(bodies, a, b, ANCHOR, t1x * delta1 + t2x * delta2, t1y * delta1 + t2y * delta2, t1z * delta1 + t2z * delta2);
476
+ }
477
+ /** `1 / (mA + mB + (rA×n)ᵀ IA (rA×n) + (rB×n)ᵀ IB (rB×n))`, or zero where nothing can move. */
478
+ function effectiveMass(imA, imB, rax, ray, raz, rbx, rby, rbz, nx, ny, nz) {
479
+ const ax = ray * nz - raz * ny;
480
+ const ay = raz * nx - rax * nz;
481
+ const az = rax * ny - ray * nx;
482
+ const bx = rby * nz - rbz * ny;
483
+ const by = rbz * nx - rbx * nz;
484
+ const bz = rbx * ny - rby * nx;
485
+ const k = imA + imB + quadratic(invIA, ax, ay, az) + quadratic(invIB, bx, by, bz);
486
+ return k > 0 ? 1 / k : 0;
487
+ }
488
+ /** `vᵀ M v` for a symmetric M stored as six terms. */
489
+ function quadratic(m, x, y, z) {
490
+ return ((m[0] ?? 0) * x * x +
491
+ (m[1] ?? 0) * y * y +
492
+ (m[2] ?? 0) * z * z +
493
+ 2 * ((m[3] ?? 0) * x * y + (m[4] ?? 0) * x * z + (m[5] ?? 0) * y * z));
494
+ }
495
+ /** Rotate the stored local anchors into world, into `ANCHOR[0..5]`. */
496
+ function worldAnchors(bodies, a, b, c, p) {
497
+ if (c.curvedA[p]) {
498
+ ANCHOR[0] = c.localA[p * 3] ?? 0;
499
+ ANCHOR[1] = c.localA[p * 3 + 1] ?? 0;
500
+ ANCHOR[2] = c.localA[p * 3 + 2] ?? 0;
501
+ }
502
+ else {
503
+ rotate(bodies, a, c.localA[p * 3] ?? 0, c.localA[p * 3 + 1] ?? 0, c.localA[p * 3 + 2] ?? 0, ANCHOR, 0);
504
+ }
505
+ if (c.curvedB[p]) {
506
+ ANCHOR[3] = c.localB[p * 3] ?? 0;
507
+ ANCHOR[4] = c.localB[p * 3 + 1] ?? 0;
508
+ ANCHOR[5] = c.localB[p * 3 + 2] ?? 0;
509
+ }
510
+ else {
511
+ rotate(bodies, b, c.localB[p * 3] ?? 0, c.localB[p * 3 + 1] ?? 0, c.localB[p * 3 + 2] ?? 0, ANCHOR, 3);
512
+ }
513
+ }
514
+ /** `(vB + wB × rB) − (vA + wA × rA)`, into `REL`. */
515
+ function relativeVelocity(bodies, a, b, rax, ray, raz, rbx, rby, rbz) {
516
+ const wax = bodies.angX[a] ?? 0;
517
+ const way = bodies.angY[a] ?? 0;
518
+ const waz = bodies.angZ[a] ?? 0;
519
+ const wbx = bodies.angX[b] ?? 0;
520
+ const wby = bodies.angY[b] ?? 0;
521
+ const wbz = bodies.angZ[b] ?? 0;
522
+ REL[0] =
523
+ (bodies.velX[b] ?? 0) +
524
+ (wby * rbz - wbz * rby) -
525
+ (bodies.velX[a] ?? 0) -
526
+ (way * raz - waz * ray);
527
+ REL[1] =
528
+ (bodies.velY[b] ?? 0) +
529
+ (wbz * rbx - wbx * rbz) -
530
+ (bodies.velY[a] ?? 0) -
531
+ (waz * rax - wax * raz);
532
+ REL[2] =
533
+ (bodies.velZ[b] ?? 0) +
534
+ (wbx * rby - wby * rbx) -
535
+ (bodies.velZ[a] ?? 0) -
536
+ (wax * ray - way * rax);
537
+ }
538
+ /** Apply `+P` to B at `rB` and `−P` to A at `rA`. */
539
+ function applyImpulse(bodies, a, b, anchors, px, py, pz) {
540
+ const imA = bodies.invMass[a] ?? 0;
541
+ if (imA > 0) {
542
+ bodies.velX[a] = (bodies.velX[a] ?? 0) - px * imA;
543
+ bodies.velY[a] = (bodies.velY[a] ?? 0) - py * imA;
544
+ bodies.velZ[a] = (bodies.velZ[a] ?? 0) - pz * imA;
545
+ const tx = anchors[1] * pz - anchors[2] * py;
546
+ const ty = anchors[2] * px - anchors[0] * pz;
547
+ const tz = anchors[0] * py - anchors[1] * px;
548
+ bodies.angX[a] = (bodies.angX[a] ?? 0) - multiply(invIA, tx, ty, tz, 0);
549
+ bodies.angY[a] = (bodies.angY[a] ?? 0) - multiply(invIA, tx, ty, tz, 1);
550
+ bodies.angZ[a] = (bodies.angZ[a] ?? 0) - multiply(invIA, tx, ty, tz, 2);
551
+ }
552
+ const imB = bodies.invMass[b] ?? 0;
553
+ if (imB > 0) {
554
+ bodies.velX[b] = (bodies.velX[b] ?? 0) + px * imB;
555
+ bodies.velY[b] = (bodies.velY[b] ?? 0) + py * imB;
556
+ bodies.velZ[b] = (bodies.velZ[b] ?? 0) + pz * imB;
557
+ const tx = anchors[4] * pz - anchors[5] * py;
558
+ const ty = anchors[5] * px - anchors[3] * pz;
559
+ const tz = anchors[3] * py - anchors[4] * px;
560
+ bodies.angX[b] = (bodies.angX[b] ?? 0) + multiply(invIB, tx, ty, tz, 0);
561
+ bodies.angY[b] = (bodies.angY[b] ?? 0) + multiply(invIB, tx, ty, tz, 1);
562
+ bodies.angZ[b] = (bodies.angZ[b] ?? 0) + multiply(invIB, tx, ty, tz, 2);
563
+ }
564
+ }
565
+ /** Row `row` of `M v`, for a symmetric M stored as six terms. */
566
+ function multiply(m, x, y, z, row) {
567
+ if (row === 0)
568
+ return (m[0] ?? 0) * x + (m[3] ?? 0) * y + (m[4] ?? 0) * z;
569
+ if (row === 1)
570
+ return (m[3] ?? 0) * x + (m[1] ?? 0) * y + (m[5] ?? 0) * z;
571
+ return (m[4] ?? 0) * x + (m[5] ?? 0) * y + (m[2] ?? 0) * z;
572
+ }
573
+ function rotate(bodies, i, x, y, z, out, at) {
574
+ const qx = bodies.rotX[i] ?? 0;
575
+ const qy = bodies.rotY[i] ?? 0;
576
+ const qz = bodies.rotZ[i] ?? 0;
577
+ const qw = bodies.rotW[i] ?? 1;
578
+ // v + 2w(q × v) + 2(q × (q × v)), the standard quaternion rotation without a matrix.
579
+ const tx = 2 * (qy * z - qz * y);
580
+ const ty = 2 * (qz * x - qx * z);
581
+ const tz = 2 * (qx * y - qy * x);
582
+ out[at] = x + qw * tx + (qy * tz - qz * ty);
583
+ out[at + 1] = y + qw * ty + (qz * tx - qx * tz);
584
+ out[at + 2] = z + qw * tz + (qx * ty - qy * tx);
585
+ }
586
+ function unrotate(bodies, i, x, y, z, out, at) {
587
+ const qx = -(bodies.rotX[i] ?? 0);
588
+ const qy = -(bodies.rotY[i] ?? 0);
589
+ const qz = -(bodies.rotZ[i] ?? 0);
590
+ const qw = bodies.rotW[i] ?? 1;
591
+ const tx = 2 * (qy * z - qz * y);
592
+ const ty = 2 * (qz * x - qx * z);
593
+ const tz = 2 * (qx * y - qy * x);
594
+ out[at] = x + qw * tx + (qy * tz - qz * ty);
595
+ out[at + 1] = y + qw * ty + (qz * tx - qx * tz);
596
+ out[at + 2] = z + qw * tz + (qx * ty - qy * tx);
597
+ }
598
+ /** Whether a body is one the solver may move. */
599
+ export function isDynamicBody(bodies, i) {
600
+ return bodies.type[i] === BODY_DYNAMIC;
601
+ }