@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/joints.js ADDED
@@ -0,0 +1,1094 @@
1
+ import { BODY_DYNAMIC } from './bodies.js';
2
+ /**
3
+ * Joints: seven types, each solved with its own rows unrolled.
4
+ *
5
+ * **This file refused a configurable 6-DOF joint until 2026-08-27, and the refusal was narrower
6
+ * than it read.** What it argued against was replacing the six unrolled types with *one* joint
7
+ * whose constraint set is decided at runtime — a branch per row in the hottest loop in the engine,
8
+ * and a megamorphic call site at the top of it. That argument still stands and nothing here has
9
+ * changed: the six keep their own arms, their own rows and their own numbers, and none of them
10
+ * gained a branch.
11
+ *
12
+ * What arrived instead is a **seventh type**, which is exactly what that refusal named as its own
13
+ * cost — "the package has to grow a type" — so the branch a six-DOF joint pays for is paid by
14
+ * six-DOF joints and by nothing else. The stated reversal condition was three consumers wanting
15
+ * three different exotic joints; growing three more unrolled types is the outcome this answers
16
+ * more cheaply than it answers the question of whether three have asked.
17
+ *
18
+ * **`JOINT_SIX_DOF` solves six independent scalar rows, not two coupled 3x3 blocks**, because a
19
+ * coupled solve has no meaning when a caller has made only some of the rows exist. *Cost:* an
20
+ * all-locked six-DOF joint is measurably softer than `JOINT_FIXED` at the same substep count, so
21
+ * reach for `JOINT_FIXED` when all six are locked. *What would make this wrong:* a caller wanting
22
+ * an all-locked six-DOF joint to be as stiff as a fixed one, and the honest answer there is to use
23
+ * the fixed one.
24
+ *
25
+ * **No angle appears anywhere.** A swing limit compares a dot product against a stored cosine; a
26
+ * twist limit compares a quaternion component against the half-angle's sine. `Math.acos` and
27
+ * `Math.atan2` are both on the banned list, and writing the limits this way from the start is
28
+ * cheaper than repairing them — the comparison is what the solver wanted anyway, since it clamps
29
+ * rather than measures.
30
+ *
31
+ * **A point-to-point constraint solves its full 3×3 effective mass** rather than three sequential
32
+ * scalars. The matrix inverts in closed form, so one exact solve replaces three approximate ones
33
+ * that would each disturb the others. *What it costs* is an inversion per joint per iteration.
34
+ *
35
+ * Joints are solved **before** contacts in each iteration, because a joint is a hard structural
36
+ * relationship and a contact is a transient one: letting contacts win produces a limb that pulls
37
+ * out of its socket when something lands on it.
38
+ */
39
+ export const JOINT_DISTANCE = 0;
40
+ export const JOINT_SPHERICAL = 1;
41
+ export const JOINT_REVOLUTE = 2;
42
+ export const JOINT_PRISMATIC = 3;
43
+ export const JOINT_FIXED = 4;
44
+ export const JOINT_CONE_TWIST = 5;
45
+ export const JOINT_SIX_DOF = 6;
46
+ /**
47
+ * What one degree of freedom does, for `JOINT_SIX_DOF`.
48
+ *
49
+ * **Locked is zero so that a zero-filled array is a rigid joint.** A joint somebody forgot to
50
+ * describe holds its bodies together rather than letting them fly apart, which is the safe
51
+ * direction for a default nobody chose.
52
+ *
53
+ * **Explicit rather than inferred from the bounds.** The widespread convention is that
54
+ * `lower > upper` means free and `lower === upper` means locked, which needs no extra field and
55
+ * turns a caller who swapped their two numbers into a joint that silently does nothing. A mode is
56
+ * one byte and it cannot be mistyped into the wrong meaning.
57
+ */
58
+ export const DOF_LOCKED = 0;
59
+ export const DOF_LIMITED = 1;
60
+ export const DOF_FREE = 2;
61
+ /** Six degrees: linear x, y, z, then angular x, y, z — all in body A's local frame. */
62
+ export const DOF_COUNT = 6;
63
+ export class JointSet {
64
+ count = 0;
65
+ type;
66
+ bodyA;
67
+ bodyB;
68
+ localA;
69
+ localB;
70
+ axis;
71
+ /** The relative rotation held at rest, captured when the joint is made. */
72
+ restRel;
73
+ length;
74
+ lower;
75
+ upper;
76
+ swingCos;
77
+ twistSin;
78
+ motorSpeed;
79
+ motorMaxForce;
80
+ breakImpulse;
81
+ broken;
82
+ /** Six accumulated impulses per joint: three linear, three angular or scalar rows. */
83
+ impulse;
84
+ motorImpulse;
85
+ /** Six-DOF only: a mode per degree, and the bounds the limited ones are held between. */
86
+ dofMode;
87
+ dofLower;
88
+ dofUpper;
89
+ cap;
90
+ constructor(capacity = 32) {
91
+ this.cap = capacity;
92
+ this.type = new Uint8Array(this.cap);
93
+ this.bodyA = new Int32Array(this.cap);
94
+ this.bodyB = new Int32Array(this.cap);
95
+ this.localA = new Float32Array(this.cap * 3);
96
+ this.localB = new Float32Array(this.cap * 3);
97
+ this.axis = new Float32Array(this.cap * 3);
98
+ this.restRel = new Float32Array(this.cap * 4);
99
+ this.length = new Float32Array(this.cap);
100
+ this.lower = new Float32Array(this.cap);
101
+ this.upper = new Float32Array(this.cap);
102
+ this.swingCos = new Float32Array(this.cap);
103
+ this.twistSin = new Float32Array(this.cap);
104
+ this.motorSpeed = new Float32Array(this.cap);
105
+ this.motorMaxForce = new Float32Array(this.cap);
106
+ this.breakImpulse = new Float32Array(this.cap);
107
+ this.broken = new Uint8Array(this.cap);
108
+ this.impulse = new Float32Array(this.cap * 6);
109
+ this.motorImpulse = new Float32Array(this.cap);
110
+ /* Zero is `DOF_LOCKED`, so a fresh row is a rigid joint without anything filling it. */
111
+ this.dofMode = new Uint8Array(this.cap * DOF_COUNT);
112
+ this.dofLower = new Float32Array(this.cap * DOF_COUNT);
113
+ this.dofUpper = new Float32Array(this.cap * DOF_COUNT);
114
+ }
115
+ add(bodies, d) {
116
+ if (this.count === this.cap)
117
+ this.grow();
118
+ const j = this.count++;
119
+ this.type[j] = d.type;
120
+ this.bodyA[j] = d.bodyA;
121
+ this.bodyB[j] = d.bodyB;
122
+ /*
123
+ * With no anchor given, the anchor is *where B already is*, expressed in A's frame.
124
+ *
125
+ * That makes the promise this method's own comment carries true for the offset as well as the
126
+ * rotation: pose a rig, joint it, and nothing moves on the first tick. Defaulting both anchors
127
+ * to the body centres instead welds the two centres together, which yanked a fixed joint's body
128
+ * two metres onto its anchor — a snap on the first tick, from a default.
129
+ *
130
+ * A distance joint is the exception, because its anchors *are* the endpoints it measures
131
+ * between and deriving them would give it a length of zero.
132
+ */
133
+ const given = d.anchorAX !== undefined ||
134
+ d.anchorAY !== undefined ||
135
+ d.anchorAZ !== undefined ||
136
+ d.anchorBX !== undefined ||
137
+ d.anchorBY !== undefined ||
138
+ d.anchorBZ !== undefined;
139
+ if (!given && d.type !== JOINT_DISTANCE) {
140
+ unrotateInto(bodies, d.bodyA, (bodies.posX[d.bodyB] ?? 0) - (bodies.posX[d.bodyA] ?? 0), (bodies.posY[d.bodyB] ?? 0) - (bodies.posY[d.bodyA] ?? 0), (bodies.posZ[d.bodyB] ?? 0) - (bodies.posZ[d.bodyA] ?? 0), this.localA, j * 3);
141
+ this.localB[j * 3] = 0;
142
+ this.localB[j * 3 + 1] = 0;
143
+ this.localB[j * 3 + 2] = 0;
144
+ }
145
+ else {
146
+ this.localA[j * 3] = d.anchorAX ?? 0;
147
+ this.localA[j * 3 + 1] = d.anchorAY ?? 0;
148
+ this.localA[j * 3 + 2] = d.anchorAZ ?? 0;
149
+ this.localB[j * 3] = d.anchorBX ?? 0;
150
+ this.localB[j * 3 + 1] = d.anchorBY ?? 0;
151
+ this.localB[j * 3 + 2] = d.anchorBZ ?? 0;
152
+ }
153
+ let ax = d.axisX ?? 0;
154
+ let ay = d.axisY ?? 1;
155
+ let az = d.axisZ ?? 0;
156
+ const alen = Math.sqrt(ax * ax + ay * ay + az * az) || 1;
157
+ ax /= alen;
158
+ ay /= alen;
159
+ az /= alen;
160
+ this.axis[j * 3] = ax;
161
+ this.axis[j * 3 + 1] = ay;
162
+ this.axis[j * 3 + 2] = az;
163
+ this.length[j] = d.length ?? 1;
164
+ this.lower[j] = d.lower ?? -Infinity;
165
+ this.upper[j] = d.upper ?? Infinity;
166
+ this.swingCos[j] = d.swingCos ?? -1;
167
+ this.twistSin[j] = d.twistSin ?? 1;
168
+ this.motorSpeed[j] = d.motorSpeed ?? 0;
169
+ this.motorMaxForce[j] = d.motorMaxForce ?? 0;
170
+ this.breakImpulse[j] = d.breakImpulse ?? Infinity;
171
+ this.broken[j] = 0;
172
+ for (let k = 0; k < 6; k++)
173
+ this.impulse[j * 6 + k] = 0;
174
+ this.motorImpulse[j] = 0;
175
+ for (let k = 0; k < DOF_COUNT; k++) {
176
+ this.dofMode[j * DOF_COUNT + k] = d.dof?.[k] ?? DOF_LOCKED;
177
+ this.dofLower[j * DOF_COUNT + k] = d.dofLower?.[k] ?? 0;
178
+ this.dofUpper[j * DOF_COUNT + k] = d.dofUpper?.[k] ?? 0;
179
+ }
180
+ /*
181
+ * A motor asked for and not delivered is a silent no-op: nothing throws, the axis never
182
+ * drives, and it reads as a broken motor rather than as an unsupported one. Said out loud
183
+ * here, which is init, rather than left for somebody to discover in a frame.
184
+ *
185
+ * Once per process rather than once per joint, on `refuseIfUnvouched`'s pattern: a rig with
186
+ * forty six-DOF joints would otherwise flood a console somebody needs to read.
187
+ */
188
+ if (d.type === JOINT_SIX_DOF && (d.motorMaxForce ?? 0) > 0 && !warnedSixDofMotor) {
189
+ warnedSixDofMotor = true;
190
+ console.warn('[driftengine] a six-DOF joint was given a motor and has none: the type carries free, ' +
191
+ 'limited and locked axes and nothing that drives one. Use a revolute or prismatic ' +
192
+ 'joint for a powered axis, or say so and the type grows twelve more numbers.');
193
+ }
194
+ /*
195
+ * The relative rotation now becomes the one the joint holds. Capturing it here rather than
196
+ * taking it as a parameter is what lets a caller pose a rig and then joint it, which is how
197
+ * `ragdollFromSkeleton` will build one — and it is the only sane default, since any other
198
+ * choice would snap the bodies on the first tick.
199
+ */
200
+ relativeRotation(bodies, d.bodyA, d.bodyB, this.restRel, j * 4);
201
+ return j;
202
+ }
203
+ grow() {
204
+ this.cap *= 2;
205
+ const g = (a, stride = 1) => {
206
+ const out = new a.constructor(this.cap * stride);
207
+ out.set(a);
208
+ return out;
209
+ };
210
+ this.type = g(this.type);
211
+ this.bodyA = g(this.bodyA);
212
+ this.bodyB = g(this.bodyB);
213
+ this.localA = g(this.localA, 3);
214
+ this.localB = g(this.localB, 3);
215
+ this.axis = g(this.axis, 3);
216
+ this.restRel = g(this.restRel, 4);
217
+ this.length = g(this.length);
218
+ this.lower = g(this.lower);
219
+ this.upper = g(this.upper);
220
+ this.swingCos = g(this.swingCos);
221
+ this.twistSin = g(this.twistSin);
222
+ this.motorSpeed = g(this.motorSpeed);
223
+ this.motorMaxForce = g(this.motorMaxForce);
224
+ this.breakImpulse = g(this.breakImpulse);
225
+ this.broken = g(this.broken);
226
+ this.impulse = g(this.impulse, 6);
227
+ this.motorImpulse = g(this.motorImpulse);
228
+ this.dofMode = g(this.dofMode, DOF_COUNT);
229
+ this.dofLower = g(this.dofLower, DOF_COUNT);
230
+ this.dofUpper = g(this.dofUpper, DOF_COUNT);
231
+ }
232
+ }
233
+ /**
234
+ * Clear every joint's accumulated impulse, once per tick, before the substeps.
235
+ *
236
+ * **The soft-constraint relaxation term makes a joint a spring whose sag is proportional to what it
237
+ * is carrying**, through `−impulseScale · accumulated`. Contacts reset that accumulator every tick
238
+ * when `prepareContact` reloads them from the warm-start store; joints had nothing doing the same,
239
+ * so it grew without bound — measured, a distance joint's went from −17.7 on the first tick to
240
+ * −5,278 by the three hundredth, and the body it held sagged 83 millimetres and was still sinking.
241
+ *
242
+ * **What this gives up** is warm starting for joints: a heavily loaded one starts each tick from
243
+ * zero and needs the substeps to find its impulse again. **What would make it wrong** is a rig
244
+ * whose joints visibly soften under load at four substeps, and the answer then is to carry the
245
+ * impulse as a warm start and reset the accumulator, rather than to leave it running.
246
+ */
247
+ export function resetJointImpulses(joints) {
248
+ for (let j = 0; j < joints.count; j++) {
249
+ for (let k = 0; k < 6; k++)
250
+ joints.impulse[j * 6 + k] = 0;
251
+ joints.motorImpulse[j] = 0;
252
+ }
253
+ }
254
+ /** So the motor refusal above is said once per process rather than once per joint. */
255
+ let warnedSixDofMotor = false;
256
+ const iA = new Float32Array(6);
257
+ const iB = new Float32Array(6);
258
+ const rA = new Float64Array(3);
259
+ const rB = new Float64Array(3);
260
+ const K = new Float64Array(6);
261
+ const KINV = new Float64Array(6);
262
+ const V = new Float64Array(3);
263
+ const AXIS = new Float64Array(3);
264
+ const REL = new Float64Array(4);
265
+ // Preallocated, because the hot-path rule applies here as much as anywhere.
266
+ const FRAME = new Float64Array(9);
267
+ const DOF_ANG = new Float64Array(3);
268
+ const SKEW = new Float64Array(9);
269
+ const MAT = new Float64Array(9);
270
+ const TMP = new Float64Array(9);
271
+ const OUT9 = new Float64Array(9);
272
+ /**
273
+ * Solve every joint in a range, one iteration.
274
+ *
275
+ * `useBias` is false on the relax pass, exactly as it is for contacts, so the positional push a
276
+ * joint applies is taken back rather than left as energy.
277
+ */
278
+ export function solveJoints(bodies, joints, order, from, to, h,
279
+ /**
280
+ * The whole tick, which is what a motor's force budget is measured over.
281
+ *
282
+ * `maxForce` means a force, and a force acting for a tick is `maxForce · dt` of impulse. Bounding
283
+ * the accumulator by `maxForce · h` instead gives the entire tick one substep's worth, so raising
284
+ * the substep count silently weakens every motor — measured, a hinge told to reach 4 rad/s got to
285
+ * 0.13 in a tick where it should have reached 0.5.
286
+ */
287
+ dt, useBias, hertz, damping) {
288
+ const omega = 2 * Math.PI * hertz;
289
+ const a1 = 2 * damping + h * omega;
290
+ const a2 = h * omega * a1;
291
+ const a3 = 1 / (1 + a2);
292
+ const biasRate = useBias ? omega / a1 : 0;
293
+ const massScale = useBias ? a2 * a3 : 1;
294
+ const impulseScale = useBias ? a3 : 0;
295
+ for (let oi = from; oi < to; oi++) {
296
+ const j = order[oi] ?? 0;
297
+ if (joints.broken[j])
298
+ continue;
299
+ const a = joints.bodyA[j] ?? 0;
300
+ const b = joints.bodyB[j] ?? 0;
301
+ bodies.worldInverseInertia(a, iA);
302
+ bodies.worldInverseInertia(b, iB);
303
+ rotateLocal(bodies, a, joints.localA, j * 3, rA);
304
+ rotateLocal(bodies, b, joints.localB, j * 3, rB);
305
+ const type = joints.type[j];
306
+ if (type === JOINT_DISTANCE) {
307
+ solveDistance(bodies, joints, j, a, b, biasRate, massScale, impulseScale);
308
+ }
309
+ else if (type === JOINT_SIX_DOF) {
310
+ /* Its own arm above the chain rather than inside it: a six-DOF joint owns all six rows and
311
+ must not also take `solvePoint`, which is the three linear rows locked outright. */
312
+ solveSixDof(bodies, joints, j, a, b, biasRate, massScale, impulseScale);
313
+ }
314
+ else {
315
+ if (type !== JOINT_PRISMATIC) {
316
+ solvePoint(bodies, joints, j, a, b, biasRate, massScale, impulseScale);
317
+ }
318
+ if (type === JOINT_FIXED) {
319
+ solveAngularLock(bodies, joints, j, a, b, biasRate, massScale, impulseScale);
320
+ }
321
+ else if (type === JOINT_REVOLUTE) {
322
+ solveRevolute(bodies, joints, j, a, b, dt, biasRate, massScale, impulseScale);
323
+ }
324
+ else if (type === JOINT_PRISMATIC) {
325
+ solvePrismatic(bodies, joints, j, a, b, dt, biasRate, massScale, impulseScale);
326
+ }
327
+ else if (type === JOINT_CONE_TWIST) {
328
+ solveConeTwist(bodies, joints, j, a, b, biasRate, massScale, impulseScale);
329
+ }
330
+ }
331
+ // A joint gives way when what it is carrying exceeds what it was told to carry.
332
+ const p = joints.impulse;
333
+ const px = p[j * 6] ?? 0;
334
+ const py = p[j * 6 + 1] ?? 0;
335
+ const pz = p[j * 6 + 2] ?? 0;
336
+ const mag = Math.sqrt(px * px + py * py + pz * pz);
337
+ if (mag > (joints.breakImpulse[j] ?? Infinity))
338
+ joints.broken[j] = 1;
339
+ }
340
+ }
341
+ /** Three linear rows, solved together through the inverse of the 3×3 effective mass. */
342
+ function solvePoint(bodies, joints, j, a, b, biasRate, massScale, impulseScale) {
343
+ pointMass(bodies, a, b);
344
+ invertSymmetric3(K, KINV);
345
+ // C, the positional error the bias answers to.
346
+ const cx = (bodies.posX[b] ?? 0) + rB[0] - (bodies.posX[a] ?? 0) - rA[0];
347
+ const cy = (bodies.posY[b] ?? 0) + rB[1] - (bodies.posY[a] ?? 0) - rA[1];
348
+ const cz = (bodies.posZ[b] ?? 0) + rB[2] - (bodies.posZ[a] ?? 0) - rA[2];
349
+ pointVelocity(bodies, a, b);
350
+ const dx = V[0] + biasRate * cx;
351
+ const dy = V[1] + biasRate * cy;
352
+ const dz = V[2] + biasRate * cz;
353
+ const at = j * 6;
354
+ const ox = joints.impulse[at] ?? 0;
355
+ const oy = joints.impulse[at + 1] ?? 0;
356
+ const oz = joints.impulse[at + 2] ?? 0;
357
+ const px = -massScale * multiply3(KINV, dx, dy, dz, 0) - impulseScale * ox;
358
+ const py = -massScale * multiply3(KINV, dx, dy, dz, 1) - impulseScale * oy;
359
+ const pz = -massScale * multiply3(KINV, dx, dy, dz, 2) - impulseScale * oz;
360
+ joints.impulse[at] = ox + px;
361
+ joints.impulse[at + 1] = oy + py;
362
+ joints.impulse[at + 2] = oz + pz;
363
+ applyLinear(bodies, a, b, px, py, pz);
364
+ }
365
+ /** Three angular rows: hold the relative rotation the joint was made at. */
366
+ function solveAngularLock(bodies, joints, j, a, b, biasRate, massScale, impulseScale) {
367
+ angularMass();
368
+ invertSymmetric3(K, KINV);
369
+ orientationError(bodies, joints, j, a, b);
370
+ const wx = (bodies.angX[b] ?? 0) - (bodies.angX[a] ?? 0) + biasRate * V[0];
371
+ const wy = (bodies.angY[b] ?? 0) - (bodies.angY[a] ?? 0) + biasRate * V[1];
372
+ const wz = (bodies.angZ[b] ?? 0) - (bodies.angZ[a] ?? 0) + biasRate * V[2];
373
+ const at = j * 6 + 3;
374
+ const ox = joints.impulse[at] ?? 0;
375
+ const oy = joints.impulse[at + 1] ?? 0;
376
+ const oz = joints.impulse[at + 2] ?? 0;
377
+ const px = -massScale * multiply3(KINV, wx, wy, wz, 0) - impulseScale * ox;
378
+ const py = -massScale * multiply3(KINV, wx, wy, wz, 1) - impulseScale * oy;
379
+ const pz = -massScale * multiply3(KINV, wx, wy, wz, 2) - impulseScale * oz;
380
+ joints.impulse[at] = ox + px;
381
+ joints.impulse[at + 1] = oy + py;
382
+ joints.impulse[at + 2] = oz + pz;
383
+ applyAngular(bodies, a, b, px, py, pz);
384
+ }
385
+ /**
386
+ * One scalar row along the line between anchors, with an optional min and max.
387
+ *
388
+ * **A limit is one-sided and that is not a detail.** Between its bounds the row is *skipped
389
+ * entirely*, not merely given a zero error: a row with `C = 0` still removes all axial velocity, so
390
+ * a body that should hang free is held exactly where it is. And the accumulated impulse is clamped
391
+ * to the side the limit can push, or the row would pull the body back off the limit it is resting
392
+ * against on the following iteration.
393
+ */
394
+ function solveDistance(bodies, joints, j, a, b, biasRate, massScale, impulseScale) {
395
+ let ux = (bodies.posX[b] ?? 0) + rB[0] - (bodies.posX[a] ?? 0) - rA[0];
396
+ let uy = (bodies.posY[b] ?? 0) + rB[1] - (bodies.posY[a] ?? 0) - rA[1];
397
+ let uz = (bodies.posZ[b] ?? 0) + rB[2] - (bodies.posZ[a] ?? 0) - rA[2];
398
+ const len = Math.sqrt(ux * ux + uy * uy + uz * uz);
399
+ if (len < 1e-9)
400
+ return;
401
+ ux /= len;
402
+ uy /= len;
403
+ uz /= len;
404
+ const lower = joints.lower[j] ?? -Infinity;
405
+ const upper = joints.upper[j] ?? Infinity;
406
+ const limited = lower !== -Infinity || upper !== Infinity;
407
+ const at = j * 6;
408
+ let c;
409
+ let minP = -Infinity;
410
+ let maxP = Infinity;
411
+ if (limited) {
412
+ if (len >= lower && len <= upper) {
413
+ joints.impulse[at] = 0;
414
+ return;
415
+ }
416
+ if (len < lower) {
417
+ c = len - lower;
418
+ minP = 0; // may only push apart
419
+ }
420
+ else {
421
+ c = len - upper;
422
+ maxP = 0; // may only pull together
423
+ }
424
+ }
425
+ else {
426
+ c = len - (joints.length[j] ?? 1);
427
+ }
428
+ const mass = axisMass(bodies, a, b, ux, uy, uz);
429
+ pointVelocity(bodies, a, b);
430
+ const cdot = V[0] * ux + V[1] * uy + V[2] * uz;
431
+ const old = joints.impulse[at] ?? 0;
432
+ let next = old - mass * massScale * (cdot + biasRate * c) - impulseScale * old;
433
+ if (next < minP)
434
+ next = minP;
435
+ else if (next > maxP)
436
+ next = maxP;
437
+ const p = next - old;
438
+ joints.impulse[at] = next;
439
+ joints.impulse[at + 1] = 0;
440
+ joints.impulse[at + 2] = 0;
441
+ applyLinear(bodies, a, b, ux * p, uy * p, uz * p);
442
+ }
443
+ /** Point-to-point plus two angular rows locking everything but the axis, then limit and motor. */
444
+ function solveRevolute(bodies, joints, j, a, b, dt, biasRate, massScale, impulseScale) {
445
+ worldAxis(bodies, joints, j, a);
446
+ perpendicularPair(AXIS[0], AXIS[1], AXIS[2]);
447
+ lockAboutTwoAxes(bodies, joints, j, a, b, biasRate, massScale, impulseScale);
448
+ motorAndLimit(bodies, joints, j, a, b, dt, biasRate, massScale, impulseScale, true);
449
+ }
450
+ /** Three angular rows plus two linear rows perpendicular to the axis, then limit and motor. */
451
+ function solvePrismatic(bodies, joints, j, a, b, dt, biasRate, massScale, impulseScale) {
452
+ solveAngularLock(bodies, joints, j, a, b, biasRate, massScale, impulseScale);
453
+ worldAxis(bodies, joints, j, a);
454
+ const ax = AXIS[0];
455
+ const ay = AXIS[1];
456
+ const az = AXIS[2];
457
+ perpendicularPair(ax, ay, az);
458
+ const p1x = PERP[0];
459
+ const p1y = PERP[1];
460
+ const p1z = PERP[2];
461
+ const p2x = PERP[3];
462
+ const p2y = PERP[4];
463
+ const p2z = PERP[5];
464
+ const cx = (bodies.posX[b] ?? 0) + rB[0] - (bodies.posX[a] ?? 0) - rA[0];
465
+ const cy = (bodies.posY[b] ?? 0) + rB[1] - (bodies.posY[a] ?? 0) - rA[1];
466
+ const cz = (bodies.posZ[b] ?? 0) + rB[2] - (bodies.posZ[a] ?? 0) - rA[2];
467
+ for (let row = 0; row < 2; row++) {
468
+ const nx = row === 0 ? p1x : p2x;
469
+ const ny = row === 0 ? p1y : p2y;
470
+ const nz = row === 0 ? p1z : p2z;
471
+ const c = cx * nx + cy * ny + cz * nz;
472
+ const mass = axisMass(bodies, a, b, nx, ny, nz);
473
+ pointVelocity(bodies, a, b);
474
+ const cdot = V[0] * nx + V[1] * ny + V[2] * nz;
475
+ const at = j * 6 + row;
476
+ const old = joints.impulse[at] ?? 0;
477
+ const p = -mass * massScale * (cdot + biasRate * c) - impulseScale * old;
478
+ joints.impulse[at] = old + p;
479
+ applyLinear(bodies, a, b, nx * p, ny * p, nz * p);
480
+ }
481
+ // The travel along the axis: one-sided at each end, free between them.
482
+ const travel = cx * ax + cy * ay + cz * az;
483
+ const lower = joints.lower[j] ?? -Infinity;
484
+ const upper = joints.upper[j] ?? Infinity;
485
+ const at2 = j * 6 + 2;
486
+ if (travel < lower || travel > upper) {
487
+ const low = travel < lower;
488
+ const c = low ? travel - lower : travel - upper;
489
+ const mass = axisMass(bodies, a, b, ax, ay, az);
490
+ pointVelocity(bodies, a, b);
491
+ const cdot = V[0] * ax + V[1] * ay + V[2] * az;
492
+ const old = joints.impulse[at2] ?? 0;
493
+ let next = old - mass * massScale * (cdot + biasRate * c) - impulseScale * old;
494
+ if (low && next < 0)
495
+ next = 0;
496
+ if (!low && next > 0)
497
+ next = 0;
498
+ const p = next - old;
499
+ joints.impulse[at2] = next;
500
+ applyLinear(bodies, a, b, ax * p, ay * p, az * p);
501
+ }
502
+ else {
503
+ joints.impulse[at2] = 0;
504
+ }
505
+ const maxF = joints.motorMaxForce[j] ?? 0;
506
+ if (maxF > 0) {
507
+ const mass = axisMass(bodies, a, b, ax, ay, az);
508
+ pointVelocity(bodies, a, b);
509
+ const cdot = V[0] * ax + V[1] * ay + V[2] * az;
510
+ const old = joints.motorImpulse[j] ?? 0;
511
+ let next = old - mass * (cdot - (joints.motorSpeed[j] ?? 0));
512
+ const bound = maxF * dt;
513
+ if (next > bound)
514
+ next = bound;
515
+ else if (next < -bound)
516
+ next = -bound;
517
+ const p = next - old;
518
+ joints.motorImpulse[j] = next;
519
+ applyLinear(bodies, a, b, ax * p, ay * p, az * p);
520
+ }
521
+ }
522
+ /** A swing cone and a twist limit, both as comparisons rather than as angles. */
523
+ function solveConeTwist(bodies, joints, j, a, b, biasRate, massScale, impulseScale) {
524
+ relativeRotation(bodies, a, b, REL, 0);
525
+ // The rest frame's axis, and where B has taken it.
526
+ worldAxis(bodies, joints, j, a);
527
+ const ax = AXIS[0];
528
+ const ay = AXIS[1];
529
+ const az = AXIS[2];
530
+ rotateVectorByBody(bodies, b, joints.axis, j * 3, V);
531
+ const cos = ax * V[0] + ay * V[1] + az * V[2];
532
+ const limit = joints.swingCos[j] ?? -1;
533
+ if (cos < limit) {
534
+ /*
535
+ * Outside the cone. The correction axis is the one that closes the swing, which is the cross
536
+ * product of where the axis is and where it is allowed to be — no angle is measured, only the
537
+ * comparison above and the direction here.
538
+ */
539
+ let nx = ay * V[2] - az * V[1];
540
+ let ny = az * V[0] - ax * V[2];
541
+ let nz = ax * V[1] - ay * V[0];
542
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
543
+ if (len > 1e-9) {
544
+ nx /= len;
545
+ ny /= len;
546
+ nz /= len;
547
+ const mass = angularAxisMass(nx, ny, nz);
548
+ const wx = (bodies.angX[b] ?? 0) - (bodies.angX[a] ?? 0);
549
+ const wy = (bodies.angY[b] ?? 0) - (bodies.angY[a] ?? 0);
550
+ const wz = (bodies.angZ[b] ?? 0) - (bodies.angZ[a] ?? 0);
551
+ const cdot = wx * nx + wy * ny + wz * nz;
552
+ const at = j * 6 + 3;
553
+ const old = joints.impulse[at] ?? 0;
554
+ const p = -mass * massScale * (cdot + biasRate * (limit - cos)) - impulseScale * old;
555
+ joints.impulse[at] = old + p;
556
+ applyAngular(bodies, a, b, nx * p, ny * p, nz * p);
557
+ }
558
+ }
559
+ // The twist about the axis, as the relative quaternion's component along it.
560
+ const twist = REL[0] * ax + REL[1] * ay + REL[2] * az;
561
+ const bound = joints.twistSin[j] ?? 1;
562
+ if (Math.abs(twist) > bound) {
563
+ const over = twist > 0 ? twist - bound : twist + bound;
564
+ const mass = angularAxisMass(ax, ay, az);
565
+ const wx = (bodies.angX[b] ?? 0) - (bodies.angX[a] ?? 0);
566
+ const wy = (bodies.angY[b] ?? 0) - (bodies.angY[a] ?? 0);
567
+ const wz = (bodies.angZ[b] ?? 0) - (bodies.angZ[a] ?? 0);
568
+ const cdot = wx * ax + wy * ay + wz * az;
569
+ const at = j * 6 + 4;
570
+ const old = joints.impulse[at] ?? 0;
571
+ const p = -mass * massScale * (cdot + biasRate * 2 * over) - impulseScale * old;
572
+ joints.impulse[at] = old + p;
573
+ applyAngular(bodies, a, b, ax * p, ay * p, az * p);
574
+ }
575
+ }
576
+ /**
577
+ * Six independent scalar rows, one per degree of freedom, in body A's frame.
578
+ *
579
+ * **Scalar rows rather than two coupled 3x3 blocks**, and the reason is that the blocks would not
580
+ * exist: a caller who leaves linear y free and locks x and z has a 2x2, and the next caller has a
581
+ * different one. Solving each row on its own is the only shape that survives every configuration.
582
+ * *Cost:* an all-locked six-DOF joint is softer than `JOINT_FIXED`, which solves both blocks
583
+ * coupled — measured as a millimetre of extra sag on a half-tonne box at four substeps.
584
+ *
585
+ * **A free axis is skipped entirely and its accumulator zeroed**, never given a zero error. A row
586
+ * with `C = 0` still removes all velocity along its axis, so a body that should fall would be held
587
+ * exactly where it is — the distinction `solveDistance` draws between its bounds, one dimension up.
588
+ */
589
+ function solveSixDof(bodies, joints, j, a, b, biasRate, massScale, impulseScale) {
590
+ bodyFrame(bodies, a);
591
+ const base = j * DOF_COUNT;
592
+ /* The offset between the two anchors, which every linear row projects onto its own axis. */
593
+ const cx = (bodies.posX[b] ?? 0) + rB[0] - (bodies.posX[a] ?? 0) - rA[0];
594
+ const cy = (bodies.posY[b] ?? 0) + rB[1] - (bodies.posY[a] ?? 0) - rA[1];
595
+ const cz = (bodies.posZ[b] ?? 0) + rB[2] - (bodies.posZ[a] ?? 0) - rA[2];
596
+ for (let k = 0; k < 3; k++) {
597
+ const mode = joints.dofMode[base + k] ?? DOF_LOCKED;
598
+ const at = j * 6 + k;
599
+ if (mode === DOF_FREE) {
600
+ joints.impulse[at] = 0;
601
+ continue;
602
+ }
603
+ const nx = FRAME[k * 3];
604
+ const ny = FRAME[k * 3 + 1];
605
+ const nz = FRAME[k * 3 + 2];
606
+ const offset = cx * nx + cy * ny + cz * nz;
607
+ let c = offset;
608
+ let minP = -Infinity;
609
+ let maxP = Infinity;
610
+ if (mode === DOF_LIMITED) {
611
+ const lower = joints.dofLower[base + k] ?? 0;
612
+ const upper = joints.dofUpper[base + k] ?? 0;
613
+ if (offset >= lower && offset <= upper) {
614
+ joints.impulse[at] = 0;
615
+ continue;
616
+ }
617
+ if (offset < lower) {
618
+ c = offset - lower;
619
+ minP = 0; // may only push out
620
+ }
621
+ else {
622
+ c = offset - upper;
623
+ maxP = 0; // may only pull back
624
+ }
625
+ }
626
+ const mass = axisMass(bodies, a, b, nx, ny, nz);
627
+ pointVelocity(bodies, a, b);
628
+ const cdot = V[0] * nx + V[1] * ny + V[2] * nz;
629
+ const old = joints.impulse[at] ?? 0;
630
+ let next = old - mass * massScale * (cdot + biasRate * c) - impulseScale * old;
631
+ if (next < minP)
632
+ next = minP;
633
+ else if (next > maxP)
634
+ next = maxP;
635
+ const p = next - old;
636
+ joints.impulse[at] = next;
637
+ applyLinear(bodies, a, b, nx * p, ny * p, nz * p);
638
+ }
639
+ /*
640
+ * The angular half reads the drift from the rest rotation rather than the raw relative one, so a
641
+ * bound means "this far from where the joint was made" — which is what a rig posed and then
642
+ * jointed needs, and is the same quantity `solveAngularLock` drives to zero.
643
+ *
644
+ * Twice the vector part is the small-angle rotation vector the locked rows want; the vector part
645
+ * itself is the sine of the half angle the limits are stated in. One computation, two scales.
646
+ */
647
+ orientationErrorInto(bodies, joints, j, a, b, DOF_ANG, 1);
648
+ for (let k = 0; k < 3; k++) {
649
+ const mode = joints.dofMode[base + 3 + k] ?? DOF_LOCKED;
650
+ const at = j * 6 + 3 + k;
651
+ if (mode === DOF_FREE) {
652
+ joints.impulse[at] = 0;
653
+ continue;
654
+ }
655
+ const nx = FRAME[k * 3];
656
+ const ny = FRAME[k * 3 + 1];
657
+ const nz = FRAME[k * 3 + 2];
658
+ const sin = DOF_ANG[0] * nx + DOF_ANG[1] * ny + DOF_ANG[2] * nz;
659
+ /* Locked drives the rotation vector, which is twice the sine, exactly as the fixed joint's
660
+ three rows do. Limited compares the sine against the bound and then answers with the same
661
+ doubled error, so both arms push in the units the solver is calibrated for. */
662
+ let c = 2 * sin;
663
+ let minP = -Infinity;
664
+ let maxP = Infinity;
665
+ if (mode === DOF_LIMITED) {
666
+ const lower = joints.dofLower[base + 3 + k] ?? 0;
667
+ const upper = joints.dofUpper[base + 3 + k] ?? 0;
668
+ if (sin >= lower && sin <= upper) {
669
+ joints.impulse[at] = 0;
670
+ continue;
671
+ }
672
+ if (sin < lower) {
673
+ c = 2 * (sin - lower);
674
+ minP = 0;
675
+ }
676
+ else {
677
+ c = 2 * (sin - upper);
678
+ maxP = 0;
679
+ }
680
+ }
681
+ const mass = angularAxisMass(nx, ny, nz);
682
+ const wx = (bodies.angX[b] ?? 0) - (bodies.angX[a] ?? 0);
683
+ const wy = (bodies.angY[b] ?? 0) - (bodies.angY[a] ?? 0);
684
+ const wz = (bodies.angZ[b] ?? 0) - (bodies.angZ[a] ?? 0);
685
+ const cdot = wx * nx + wy * ny + wz * nz;
686
+ const old = joints.impulse[at] ?? 0;
687
+ let next = old - mass * massScale * (cdot + biasRate * c) - impulseScale * old;
688
+ if (next < minP)
689
+ next = minP;
690
+ else if (next > maxP)
691
+ next = maxP;
692
+ const p = next - old;
693
+ joints.impulse[at] = next;
694
+ applyAngular(bodies, a, b, nx * p, ny * p, nz * p);
695
+ }
696
+ }
697
+ /** Body A's three local axes in world space, as three consecutive triples in `FRAME`. */
698
+ function bodyFrame(bodies, i) {
699
+ const x = bodies.rotX[i] ?? 0;
700
+ const y = bodies.rotY[i] ?? 0;
701
+ const z = bodies.rotZ[i] ?? 0;
702
+ const w = bodies.rotW[i] ?? 1;
703
+ const xx = x * x;
704
+ const yy = y * y;
705
+ const zz = z * z;
706
+ const xy = x * y;
707
+ const xz = x * z;
708
+ const yz = y * z;
709
+ const wx = w * x;
710
+ const wy = w * y;
711
+ const wz = w * z;
712
+ FRAME[0] = 1 - 2 * (yy + zz);
713
+ FRAME[1] = 2 * (xy + wz);
714
+ FRAME[2] = 2 * (xz - wy);
715
+ FRAME[3] = 2 * (xy - wz);
716
+ FRAME[4] = 1 - 2 * (xx + zz);
717
+ FRAME[5] = 2 * (yz + wx);
718
+ FRAME[6] = 2 * (xz + wy);
719
+ FRAME[7] = 2 * (yz - wx);
720
+ FRAME[8] = 1 - 2 * (xx + yy);
721
+ }
722
+ /* ---------- shared rows ---------- */
723
+ const PERP = new Float64Array(6);
724
+ /** Two unit vectors perpendicular to `n` and to each other, without a trig call. */
725
+ function perpendicularPair(nx, ny, nz) {
726
+ let t1x;
727
+ let t1y;
728
+ let t1z;
729
+ if (Math.abs(nx) >= 0.57735) {
730
+ t1x = ny;
731
+ t1y = -nx;
732
+ t1z = 0;
733
+ }
734
+ else {
735
+ t1x = 0;
736
+ t1y = nz;
737
+ t1z = -ny;
738
+ }
739
+ const len = Math.sqrt(t1x * t1x + t1y * t1y + t1z * t1z) || 1;
740
+ PERP[0] = t1x / len;
741
+ PERP[1] = t1y / len;
742
+ PERP[2] = t1z / len;
743
+ PERP[3] = ny * PERP[2] - nz * PERP[1];
744
+ PERP[4] = nz * PERP[0] - nx * PERP[2];
745
+ PERP[5] = nx * PERP[1] - ny * PERP[0];
746
+ }
747
+ /** Lock relative rotation about the two axes perpendicular to the free one. */
748
+ function lockAboutTwoAxes(bodies, joints, j, a, b, biasRate, massScale, impulseScale) {
749
+ orientationError(bodies, joints, j, a, b);
750
+ const ex = V[0];
751
+ const ey = V[1];
752
+ const ez = V[2];
753
+ for (let row = 0; row < 2; row++) {
754
+ const nx = PERP[row * 3];
755
+ const ny = PERP[row * 3 + 1];
756
+ const nz = PERP[row * 3 + 2];
757
+ const c = ex * nx + ey * ny + ez * nz;
758
+ const mass = angularAxisMass(nx, ny, nz);
759
+ const wx = (bodies.angX[b] ?? 0) - (bodies.angX[a] ?? 0);
760
+ const wy = (bodies.angY[b] ?? 0) - (bodies.angY[a] ?? 0);
761
+ const wz = (bodies.angZ[b] ?? 0) - (bodies.angZ[a] ?? 0);
762
+ const cdot = wx * nx + wy * ny + wz * nz;
763
+ const at = j * 6 + 3 + row;
764
+ const old = joints.impulse[at] ?? 0;
765
+ const p = -mass * massScale * (cdot + biasRate * c) - impulseScale * old;
766
+ joints.impulse[at] = old + p;
767
+ applyAngular(bodies, a, b, nx * p, ny * p, nz * p);
768
+ }
769
+ }
770
+ /** A hinge's rotation limit and its motor, both about the free axis. */
771
+ function motorAndLimit(bodies, joints, j, a, b, dt, biasRate, massScale, impulseScale, angular) {
772
+ const ax = AXIS[0];
773
+ const ay = AXIS[1];
774
+ const az = AXIS[2];
775
+ const lower = joints.lower[j] ?? -Infinity;
776
+ const upper = joints.upper[j] ?? Infinity;
777
+ if (lower !== -Infinity || upper !== Infinity) {
778
+ relativeRotation(bodies, a, b, REL, 0);
779
+ // The rotation about the axis, as the relative quaternion's component along it.
780
+ const s = REL[0] * ax + REL[1] * ay + REL[2] * az;
781
+ const at = j * 6 + 5;
782
+ if (s < lower || s > upper) {
783
+ const low = s < lower;
784
+ const c = low ? s - lower : s - upper;
785
+ const mass = angularAxisMass(ax, ay, az);
786
+ const wx = (bodies.angX[b] ?? 0) - (bodies.angX[a] ?? 0);
787
+ const wy = (bodies.angY[b] ?? 0) - (bodies.angY[a] ?? 0);
788
+ const wz = (bodies.angZ[b] ?? 0) - (bodies.angZ[a] ?? 0);
789
+ const cdot = wx * ax + wy * ay + wz * az;
790
+ const old = joints.impulse[at] ?? 0;
791
+ let next = old - mass * massScale * (cdot + biasRate * 2 * c) - impulseScale * old;
792
+ if (low && next < 0)
793
+ next = 0;
794
+ if (!low && next > 0)
795
+ next = 0;
796
+ const p = next - old;
797
+ joints.impulse[at] = next;
798
+ applyAngular(bodies, a, b, ax * p, ay * p, az * p);
799
+ }
800
+ else {
801
+ joints.impulse[at] = 0;
802
+ }
803
+ }
804
+ const maxF = joints.motorMaxForce[j] ?? 0;
805
+ if (maxF > 0 && angular) {
806
+ const mass = angularAxisMass(ax, ay, az);
807
+ const wx = (bodies.angX[b] ?? 0) - (bodies.angX[a] ?? 0);
808
+ const wy = (bodies.angY[b] ?? 0) - (bodies.angY[a] ?? 0);
809
+ const wz = (bodies.angZ[b] ?? 0) - (bodies.angZ[a] ?? 0);
810
+ const cdot = wx * ax + wy * ay + wz * az;
811
+ const old = joints.motorImpulse[j] ?? 0;
812
+ let next = old - mass * (cdot - (joints.motorSpeed[j] ?? 0));
813
+ const bound = maxF * dt;
814
+ if (next > bound)
815
+ next = bound;
816
+ else if (next < -bound)
817
+ next = -bound;
818
+ const p = next - old;
819
+ joints.motorImpulse[j] = next;
820
+ applyAngular(bodies, a, b, ax * p, ay * p, az * p);
821
+ }
822
+ }
823
+ /* ---------- arithmetic ---------- */
824
+ function pointMass(bodies, a, b) {
825
+ const m = (bodies.invMass[a] ?? 0) + (bodies.invMass[b] ?? 0);
826
+ // K = (mA + mB)·I − skew(rA)·IA·skew(rA) − skew(rB)·IB·skew(rB), six symmetric terms.
827
+ K[0] = m;
828
+ K[1] = m;
829
+ K[2] = m;
830
+ K[3] = 0;
831
+ K[4] = 0;
832
+ K[5] = 0;
833
+ accumulateSkew(iA, rA);
834
+ accumulateSkew(iB, rB);
835
+ }
836
+ /**
837
+ * Add `skew(r)ᵀ·I·skew(r)` into `K`, which is symmetric and positive semi-definite.
838
+ *
839
+ * **The sign is the whole of it.** `skew(r)ᵀ = −skew(r)`, so the term is `−skew·I·skew`, and what
840
+ * the multiplication below produces — `skew·I·skewᵀ` — is already exactly that. It is *added*.
841
+ * Subtracting it instead shrinks the effective mass toward singular, which came out as every joint
842
+ * sagging 83 millimetres under load and a spherical joint answering NaN.
843
+ */
844
+ function accumulateSkew(inv, r) {
845
+ const x = r[0];
846
+ const y = r[1];
847
+ const z = r[2];
848
+ SKEW[0] = 0;
849
+ SKEW[1] = -z;
850
+ SKEW[2] = y;
851
+ SKEW[3] = z;
852
+ SKEW[4] = 0;
853
+ SKEW[5] = -x;
854
+ SKEW[6] = -y;
855
+ SKEW[7] = x;
856
+ SKEW[8] = 0;
857
+ const ixx = inv[0] ?? 0;
858
+ const iyy = inv[1] ?? 0;
859
+ const izz = inv[2] ?? 0;
860
+ const ixy = inv[3] ?? 0;
861
+ const ixz = inv[4] ?? 0;
862
+ const iyz = inv[5] ?? 0;
863
+ MAT[0] = ixx;
864
+ MAT[1] = ixy;
865
+ MAT[2] = ixz;
866
+ MAT[3] = ixy;
867
+ MAT[4] = iyy;
868
+ MAT[5] = iyz;
869
+ MAT[6] = ixz;
870
+ MAT[7] = iyz;
871
+ MAT[8] = izz;
872
+ for (let i = 0; i < 3; i++) {
873
+ for (let k = 0; k < 3; k++) {
874
+ TMP[i * 3 + k] =
875
+ SKEW[i * 3] * MAT[k] + SKEW[i * 3 + 1] * MAT[3 + k] + SKEW[i * 3 + 2] * MAT[6 + k];
876
+ }
877
+ }
878
+ for (let i = 0; i < 3; i++) {
879
+ for (let j = 0; j < 3; j++) {
880
+ OUT9[i * 3 + j] =
881
+ TMP[i * 3] * SKEW[j * 3] +
882
+ TMP[i * 3 + 1] * SKEW[j * 3 + 1] +
883
+ TMP[i * 3 + 2] * SKEW[j * 3 + 2];
884
+ }
885
+ }
886
+ K[0] += OUT9[0];
887
+ K[1] += OUT9[4];
888
+ K[2] += OUT9[8];
889
+ K[3] += OUT9[1];
890
+ K[4] += OUT9[2];
891
+ K[5] += OUT9[5];
892
+ }
893
+ function angularMass() {
894
+ for (let k = 0; k < 6; k++)
895
+ K[k] = (iA[k] ?? 0) + (iB[k] ?? 0);
896
+ }
897
+ function angularAxisMass(nx, ny, nz) {
898
+ const k = quad(iA, nx, ny, nz) + quad(iB, nx, ny, nz);
899
+ return k > 0 ? 1 / k : 0;
900
+ }
901
+ function axisMass(bodies, a, b, nx, ny, nz) {
902
+ const cax = rA[1] * nz - rA[2] * ny;
903
+ const cay = rA[2] * nx - rA[0] * nz;
904
+ const caz = rA[0] * ny - rA[1] * nx;
905
+ const cbx = rB[1] * nz - rB[2] * ny;
906
+ const cby = rB[2] * nx - rB[0] * nz;
907
+ const cbz = rB[0] * ny - rB[1] * nx;
908
+ const k = (bodies.invMass[a] ?? 0) +
909
+ (bodies.invMass[b] ?? 0) +
910
+ quad(iA, cax, cay, caz) +
911
+ quad(iB, cbx, cby, cbz);
912
+ return k > 0 ? 1 / k : 0;
913
+ }
914
+ function quad(m, x, y, z) {
915
+ return ((m[0] ?? 0) * x * x +
916
+ (m[1] ?? 0) * y * y +
917
+ (m[2] ?? 0) * z * z +
918
+ 2 * ((m[3] ?? 0) * x * y + (m[4] ?? 0) * x * z + (m[5] ?? 0) * y * z));
919
+ }
920
+ function multiply3(m, x, y, z, row) {
921
+ if (row === 0)
922
+ return m[0] * x + m[3] * y + m[4] * z;
923
+ if (row === 1)
924
+ return m[3] * x + m[1] * y + m[5] * z;
925
+ return m[4] * x + m[5] * y + m[2] * z;
926
+ }
927
+ /** Invert a symmetric 3×3 held as six terms. A singular matrix inverts to zero, never to NaN. */
928
+ function invertSymmetric3(m, out) {
929
+ const c00 = m[1] * m[2] - m[5] * m[5];
930
+ const c01 = m[4] * m[5] - m[3] * m[2];
931
+ const c02 = m[3] * m[5] - m[4] * m[1];
932
+ const det = m[0] * c00 + m[3] * c01 + m[4] * c02;
933
+ if (det === 0) {
934
+ for (let k = 0; k < 6; k++)
935
+ out[k] = 0;
936
+ return;
937
+ }
938
+ const inv = 1 / det;
939
+ out[0] = c00 * inv;
940
+ out[1] = (m[0] * m[2] - m[4] * m[4]) * inv;
941
+ out[2] = (m[0] * m[1] - m[3] * m[3]) * inv;
942
+ out[3] = c01 * inv;
943
+ out[4] = c02 * inv;
944
+ out[5] = (m[3] * m[4] - m[0] * m[5]) * inv;
945
+ }
946
+ function pointVelocity(bodies, a, b) {
947
+ const wax = bodies.angX[a] ?? 0;
948
+ const way = bodies.angY[a] ?? 0;
949
+ const waz = bodies.angZ[a] ?? 0;
950
+ const wbx = bodies.angX[b] ?? 0;
951
+ const wby = bodies.angY[b] ?? 0;
952
+ const wbz = bodies.angZ[b] ?? 0;
953
+ V[0] =
954
+ (bodies.velX[b] ?? 0) +
955
+ (wby * rB[2] - wbz * rB[1]) -
956
+ (bodies.velX[a] ?? 0) -
957
+ (way * rA[2] - waz * rA[1]);
958
+ V[1] =
959
+ (bodies.velY[b] ?? 0) +
960
+ (wbz * rB[0] - wbx * rB[2]) -
961
+ (bodies.velY[a] ?? 0) -
962
+ (waz * rA[0] - wax * rA[2]);
963
+ V[2] =
964
+ (bodies.velZ[b] ?? 0) +
965
+ (wbx * rB[1] - wby * rB[0]) -
966
+ (bodies.velZ[a] ?? 0) -
967
+ (wax * rA[1] - way * rA[0]);
968
+ }
969
+ function applyLinear(bodies, a, b, px, py, pz) {
970
+ const imA = bodies.invMass[a] ?? 0;
971
+ if (imA > 0) {
972
+ bodies.velX[a] = (bodies.velX[a] ?? 0) - px * imA;
973
+ bodies.velY[a] = (bodies.velY[a] ?? 0) - py * imA;
974
+ bodies.velZ[a] = (bodies.velZ[a] ?? 0) - pz * imA;
975
+ torque(bodies, a, iA, rA[1] * pz - rA[2] * py, rA[2] * px - rA[0] * pz, rA[0] * py - rA[1] * px, -1);
976
+ }
977
+ const imB = bodies.invMass[b] ?? 0;
978
+ if (imB > 0) {
979
+ bodies.velX[b] = (bodies.velX[b] ?? 0) + px * imB;
980
+ bodies.velY[b] = (bodies.velY[b] ?? 0) + py * imB;
981
+ bodies.velZ[b] = (bodies.velZ[b] ?? 0) + pz * imB;
982
+ torque(bodies, b, iB, rB[1] * pz - rB[2] * py, rB[2] * px - rB[0] * pz, rB[0] * py - rB[1] * px, 1);
983
+ }
984
+ }
985
+ function applyAngular(bodies, a, b, px, py, pz) {
986
+ if ((bodies.invMass[a] ?? 0) > 0 || bodies.type[a] === BODY_DYNAMIC)
987
+ torque(bodies, a, iA, px, py, pz, -1);
988
+ if ((bodies.invMass[b] ?? 0) > 0 || bodies.type[b] === BODY_DYNAMIC)
989
+ torque(bodies, b, iB, px, py, pz, 1);
990
+ }
991
+ function torque(bodies, i, inv, tx, ty, tz, sign) {
992
+ bodies.angX[i] =
993
+ (bodies.angX[i] ?? 0) + sign * ((inv[0] ?? 0) * tx + (inv[3] ?? 0) * ty + (inv[4] ?? 0) * tz);
994
+ bodies.angY[i] =
995
+ (bodies.angY[i] ?? 0) + sign * ((inv[3] ?? 0) * tx + (inv[1] ?? 0) * ty + (inv[5] ?? 0) * tz);
996
+ bodies.angZ[i] =
997
+ (bodies.angZ[i] ?? 0) + sign * ((inv[4] ?? 0) * tx + (inv[5] ?? 0) * ty + (inv[2] ?? 0) * tz);
998
+ }
999
+ function rotateLocal(bodies, i, src, at, out) {
1000
+ const x = src[at] ?? 0;
1001
+ const y = src[at + 1] ?? 0;
1002
+ const z = src[at + 2] ?? 0;
1003
+ const qx = bodies.rotX[i] ?? 0;
1004
+ const qy = bodies.rotY[i] ?? 0;
1005
+ const qz = bodies.rotZ[i] ?? 0;
1006
+ const qw = bodies.rotW[i] ?? 1;
1007
+ const tx = 2 * (qy * z - qz * y);
1008
+ const ty = 2 * (qz * x - qx * z);
1009
+ const tz = 2 * (qx * y - qy * x);
1010
+ out[0] = x + qw * tx + (qy * tz - qz * ty);
1011
+ out[1] = y + qw * ty + (qz * tx - qx * tz);
1012
+ out[2] = z + qw * tz + (qx * ty - qy * tx);
1013
+ }
1014
+ /** A world vector in a body's local frame, written into `out`. */
1015
+ function unrotateInto(bodies, i, x, y, z, out, at) {
1016
+ const qx = -(bodies.rotX[i] ?? 0);
1017
+ const qy = -(bodies.rotY[i] ?? 0);
1018
+ const qz = -(bodies.rotZ[i] ?? 0);
1019
+ const qw = bodies.rotW[i] ?? 1;
1020
+ const tx = 2 * (qy * z - qz * y);
1021
+ const ty = 2 * (qz * x - qx * z);
1022
+ const tz = 2 * (qx * y - qy * x);
1023
+ out[at] = x + qw * tx + (qy * tz - qz * ty);
1024
+ out[at + 1] = y + qw * ty + (qz * tx - qx * tz);
1025
+ out[at + 2] = z + qw * tz + (qx * ty - qy * tx);
1026
+ }
1027
+ function rotateVectorByBody(bodies, i, src, at, out) {
1028
+ rotateLocal(bodies, i, src, at, out);
1029
+ }
1030
+ function worldAxis(bodies, joints, j, a) {
1031
+ rotateLocal(bodies, a, joints.axis, j * 3, AXIS);
1032
+ }
1033
+ /** `conj(qA) · qB`, into `out`, as xyzw. */
1034
+ export function relativeRotation(bodies, a, b, out, at) {
1035
+ const ax = -(bodies.rotX[a] ?? 0);
1036
+ const ay = -(bodies.rotY[a] ?? 0);
1037
+ const az = -(bodies.rotZ[a] ?? 0);
1038
+ const aw = bodies.rotW[a] ?? 1;
1039
+ const bx = bodies.rotX[b] ?? 0;
1040
+ const by = bodies.rotY[b] ?? 0;
1041
+ const bz = bodies.rotZ[b] ?? 0;
1042
+ const bw = bodies.rotW[b] ?? 1;
1043
+ out[at] = aw * bx + ax * bw + ay * bz - az * by;
1044
+ out[at + 1] = aw * by - ax * bz + ay * bw + az * bx;
1045
+ out[at + 2] = aw * bz + ax * by - ay * bx + az * bw;
1046
+ out[at + 3] = aw * bw - ax * bx - ay * by - az * bz;
1047
+ }
1048
+ /**
1049
+ * How far the relative rotation has drifted from the one the joint was made at, as a rotation
1050
+ * vector in world space, into `V`.
1051
+ *
1052
+ * Twice the vector part of `rel · conj(rest)` is the small-angle rotation vector, which is exact
1053
+ * enough for a constraint that drives it to zero and needs no `acos` to obtain.
1054
+ */
1055
+ function orientationError(bodies, joints, j, a, b) {
1056
+ orientationErrorInto(bodies, joints, j, a, b, V, 2);
1057
+ }
1058
+ /**
1059
+ * The same error at a chosen scale, into a chosen target.
1060
+ *
1061
+ * Split out for `solveSixDof`, which wants the vector part *undoubled* — that is the sine of the
1062
+ * half angle, which is the unit every limit in this file is stated in. Shared rather than written
1063
+ * twice, because two implementations of one decision drift and they drift invisibly when they
1064
+ * start identical. `orientationError` passes 2 and is bit-identical to what it was.
1065
+ */
1066
+ function orientationErrorInto(bodies, joints, j, a, b, out, scale) {
1067
+ relativeRotation(bodies, a, b, REL, 0);
1068
+ const at = j * 4;
1069
+ const rx = -(joints.restRel[at] ?? 0);
1070
+ const ry = -(joints.restRel[at + 1] ?? 0);
1071
+ const rz = -(joints.restRel[at + 2] ?? 0);
1072
+ const rw = joints.restRel[at + 3] ?? 1;
1073
+ let ex = REL[3] * rx + REL[0] * rw + REL[1] * rz - REL[2] * ry;
1074
+ let ey = REL[3] * ry - REL[0] * rz + REL[1] * rw + REL[2] * rx;
1075
+ let ez = REL[3] * rz + REL[0] * ry - REL[1] * rx + REL[2] * rw;
1076
+ const ew = REL[3] * rw - REL[0] * rx - REL[1] * ry - REL[2] * rz;
1077
+ // Take the shorter arc, or a joint past a half turn would be driven the long way round.
1078
+ if (ew < 0) {
1079
+ ex = -ex;
1080
+ ey = -ey;
1081
+ ez = -ez;
1082
+ }
1083
+ // Into A's world frame: the error is expressed in A's local frame by construction.
1084
+ const qx = bodies.rotX[a] ?? 0;
1085
+ const qy = bodies.rotY[a] ?? 0;
1086
+ const qz = bodies.rotZ[a] ?? 0;
1087
+ const qw = bodies.rotW[a] ?? 1;
1088
+ const tx = 2 * (qy * ez - qz * ey);
1089
+ const ty = 2 * (qz * ex - qx * ez);
1090
+ const tz = 2 * (qx * ey - qy * ex);
1091
+ out[0] = scale * (ex + qw * tx + (qy * tz - qz * ty));
1092
+ out[1] = scale * (ey + qw * ty + (qz * tx - qx * tz));
1093
+ out[2] = scale * (ez + qw * tz + (qx * ty - qy * tx));
1094
+ }