@woosh/meep-engine 2.140.0 → 2.141.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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
  3. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
  4. package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
  5. package/src/engine/physics/PLAN.md +152 -35
  6. package/src/engine/physics/REVIEW_002.md +151 -0
  7. package/src/engine/physics/constraint/DofMode.d.ts +28 -0
  8. package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
  9. package/src/engine/physics/constraint/DofMode.js +35 -0
  10. package/src/engine/physics/constraint/solve_constraints.d.ts +16 -0
  11. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
  12. package/src/engine/physics/constraint/solve_constraints.js +436 -0
  13. package/src/engine/physics/ecs/Joint.d.ts +179 -0
  14. package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
  15. package/src/engine/physics/ecs/Joint.js +234 -0
  16. package/src/engine/physics/ecs/PhysicsSystem.d.ts +52 -0
  17. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  18. package/src/engine/physics/ecs/PhysicsSystem.js +126 -4
  19. package/src/engine/physics/fluid/FluidField.d.ts +14 -10
  20. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  21. package/src/engine/physics/fluid/FluidField.js +14 -10
  22. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  23. package/src/engine/physics/fluid/FluidSimulator.js +0 -1
  24. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
  25. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
  26. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
  27. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
  28. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
  29. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
  30. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
  31. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
  32. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
  33. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +24 -22
  34. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  35. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +26 -22
  36. package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
  37. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  38. package/src/engine/physics/island/IslandBuilder.js +33 -16
  39. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  40. package/src/engine/physics/narrowphase/box_box_manifold.js +27 -1
  41. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +33 -0
  42. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  43. package/src/engine/physics/narrowphase/narrowphase_step.js +75 -0
  44. package/src/engine/physics/solver/solve_contacts.d.ts +28 -0
  45. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  46. package/src/engine/physics/solver/solve_contacts.js +169 -1
@@ -0,0 +1,436 @@
1
+ import { BodyKind } from "../ecs/BodyKind.js";
2
+ import { SleepState } from "../ecs/SleepState.js";
3
+ import { JOINT_WORLD } from "../ecs/Joint.js";
4
+ import { DofMode } from "./DofMode.js";
5
+ import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
6
+ import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
7
+ import { quat3_to_matrix3 } from "../../../core/geom/3d/quaternion/quat3_to_matrix3.js";
8
+ import { quat3_multiply } from "../../../core/geom/3d/quaternion/quat3_multiply.js";
9
+
10
+ /**
11
+ * # 6-DOF constraint (joint) solver
12
+ *
13
+ * Runs alongside the contact solver inside the same TGS substep loop: each
14
+ * substep the joints are warm-started and their velocity rows solved, sharing
15
+ * the contact solver's per-substep / warm-start / SPOOK-bias model so the two
16
+ * constraint families converge together (a body touched by both a contact and
17
+ * a joint sees a single coupled Gauss-Seidel sweep across the substep loop).
18
+ *
19
+ * A joint is a configurable 6-DOF constraint ({@link Joint}); each relative
20
+ * degree of freedom is independently locked / free / limited / springy /
21
+ * motorised. This module implements **LOCKED linear** DOFs first — i.e. the
22
+ * ball-socket (point-to-point) joint, which alone covers chains, ropes, and
23
+ * pendulums. Angular locks, limits, springs and motors slot into the same
24
+ * per-DOF loop as they land.
25
+ *
26
+ * Row math is identical in shape to a contact's normal row: for a unit axis
27
+ * `d` and lever arms `rA = anchorA − comA`, `rB = anchorB − comB`,
28
+ * `K = invMA + invMB + (rA×d)·Iw_A·(rA×d) + (rB×d)·Iw_B·(rB×d)`,
29
+ * `vrel = (vA + ωA×rA − vB − ωB×rB) · d`,
30
+ * `λ = −(1/K)·(vrel + bias)`,
31
+ * applied as `±λ·d` at the anchors. Locked DOFs are bilateral (no clamp); the
32
+ * position error is corrected by a SPOOK velocity bias. Geometry (anchors /
33
+ * lever arms / position error) is recomputed from the current pose every
34
+ * substep, so the joint tracks the bodies as they move.
35
+ *
36
+ * @author Alex Goldring
37
+ * @copyright Company Named Limited (c) 2026
38
+ */
39
+
40
+ /**
41
+ * Maximum SPOOK position-correction bias for a joint row, m/s — belt-and-braces
42
+ * against a wildly-violated constraint (e.g. a freshly-teleported body)
43
+ * yanking the solve. Equality joints sit near zero error at steady state, so
44
+ * this only clamps transients.
45
+ * @type {number}
46
+ */
47
+ const MAX_JOINT_BIAS = 4;
48
+
49
+ /**
50
+ * Baumgarte / SPOOK position-correction gain `β`. The per-substep gain is
51
+ * `β / dt_sub`; with `β = 0.2` this matches the contact solver's position
52
+ * stiffness. Joints are bilateral equality constraints, so this only nudges
53
+ * residual position error toward zero (no clamp interplay to destabilise).
54
+ * @type {number}
55
+ */
56
+ const JOINT_BAUMGARTE = 0.2;
57
+
58
+ const scratch_inertia = new Float64Array(3);
59
+ const scratch_anchor = new Float64Array(3);
60
+ /** Frame-A world basis (rows = A's frame axes in world), from quat3_to_matrix3. */
61
+ const scratch_frame_a = new Float64Array(9);
62
+ /** Scratch quaternion for frame composition / relative rotation. */
63
+ const scratch_q = new Float64Array(4);
64
+
65
+ /**
66
+ * Angular effective-mass contribution of one body about a unit world axis:
67
+ * `a · Iw⁻¹ · a`. Zero for non-dynamic / rotation-locked bodies.
68
+ * @returns {number}
69
+ */
70
+ function angular_axis_effective_mass(rb, transform, ax, ay, az) {
71
+ if (rb.kind !== BodyKind.Dynamic) return 0;
72
+ const ii = rb.inverseInertiaLocal;
73
+ if (ii.x === 0 && ii.y === 0 && ii.z === 0) return 0;
74
+ world_inverse_inertia_apply(scratch_inertia, 0, ii, transform.rotation, ax, ay, az);
75
+ return ax * scratch_inertia[0] + ay * scratch_inertia[1] + az * scratch_inertia[2];
76
+ }
77
+
78
+ /**
79
+ * Apply a pure angular impulse `sign·λ·axis`: Δω = Iw⁻¹·(sign·λ·axis).
80
+ * @param {RigidBody} rb @param {Transform} transform
81
+ * @param {number} ax @param {number} ay @param {number} az
82
+ * @param {number} lambda @param {number} sign
83
+ */
84
+ function apply_angular_impulse(rb, transform, ax, ay, az, lambda, sign) {
85
+ if (rb.kind !== BodyKind.Dynamic) return;
86
+ const ii = rb.inverseInertiaLocal;
87
+ if (ii.x === 0 && ii.y === 0 && ii.z === 0) return;
88
+ const L = sign * lambda;
89
+ world_inverse_inertia_apply(scratch_inertia, 0, ii, transform.rotation, ax * L, ay * L, az * L);
90
+ const av = rb.angularVelocity;
91
+ av[0] += scratch_inertia[0];
92
+ av[1] += scratch_inertia[1];
93
+ av[2] += scratch_inertia[2];
94
+ }
95
+
96
+ /**
97
+ * Effective-mass denominator contribution of one body along a unit axis:
98
+ * `invM + (r×d)·Iw⁻¹·(r×d)`. Static / kinematic bodies (invM === 0) contribute
99
+ * nothing.
100
+ *
101
+ * @param {RigidBody} rb
102
+ * @param {Transform} transform
103
+ * @param {number} invM
104
+ * @param {number} rx @param {number} ry @param {number} rz
105
+ * @param {number} dx @param {number} dy @param {number} dz
106
+ * @returns {number}
107
+ */
108
+ function axis_effective_mass(rb, transform, invM, rx, ry, rz, dx, dy, dz) {
109
+ if (invM === 0) return 0;
110
+ let k = invM;
111
+ const ii = rb.inverseInertiaLocal;
112
+ if (ii.x !== 0 || ii.y !== 0 || ii.z !== 0) {
113
+ const cx = ry * dz - rz * dy;
114
+ const cy = rz * dx - rx * dz;
115
+ const cz = rx * dy - ry * dx;
116
+ world_inverse_inertia_apply(scratch_inertia, 0, ii, transform.rotation, cx, cy, cz);
117
+ k += cx * scratch_inertia[0] + cy * scratch_inertia[1] + cz * scratch_inertia[2];
118
+ }
119
+ return k;
120
+ }
121
+
122
+ /**
123
+ * Apply impulse `P` at body-relative offset `r`: Δv = P·invM, Δω = Iw⁻¹·(r×P).
124
+ * Direct typed-array writes (bypass Vector3#set observers — dead weight here).
125
+ *
126
+ * @param {RigidBody} rb @param {Transform} transform @param {number} invM
127
+ * @param {number} rx @param {number} ry @param {number} rz
128
+ * @param {number} Px @param {number} Py @param {number} Pz
129
+ * @param {number} sign +1 / −1
130
+ */
131
+ function apply_impulse(rb, transform, invM, rx, ry, rz, Px, Py, Pz, sign) {
132
+ if (invM === 0) return;
133
+ const sPx = sign * Px, sPy = sign * Py, sPz = sign * Pz;
134
+ const lv = rb.linearVelocity;
135
+ lv[0] += sPx * invM; lv[1] += sPy * invM; lv[2] += sPz * invM;
136
+
137
+ const tx = ry * sPz - rz * sPy;
138
+ const ty = rz * sPx - rx * sPz;
139
+ const tz = rx * sPy - ry * sPx;
140
+ world_inverse_inertia_apply(scratch_inertia, 0, rb.inverseInertiaLocal, transform.rotation, tx, ty, tz);
141
+ const av = rb.angularVelocity;
142
+ av[0] += scratch_inertia[0]; av[1] += scratch_inertia[1]; av[2] += scratch_inertia[2];
143
+ }
144
+
145
+ /**
146
+ * @param {RigidBody} rb
147
+ * @returns {number}
148
+ */
149
+ function inv_mass_of(rb) {
150
+ if (rb.kind !== BodyKind.Dynamic) return 0;
151
+ return rb.mass > 0 ? 1 / rb.mass : 0;
152
+ }
153
+
154
+ /**
155
+ * Solve every joint for one substep: recompute geometry at the current poses,
156
+ * replay the per-substep warm-start, and run `iters` velocity iterations of
157
+ * the locked linear DOFs.
158
+ *
159
+ * Called once per substep from `PhysicsSystem.fixedUpdate`, after the contact
160
+ * solve so the two share the substep / warm-start cadence.
161
+ *
162
+ * @param {Joint[]} joints live joints (sparse array; holes skipped)
163
+ * @param {PhysicsSystem} system reads `__bodies` / `__transforms` / index map
164
+ * @param {number} dt_sub substep size in seconds (the SPOOK gain is derived
165
+ * from it, matching the contact solver's per-substep position stiffness)
166
+ * @param {number} iters velocity iterations
167
+ */
168
+ export function solve_joints(joints, system, dt_sub, iters) {
169
+ const n = joints.length;
170
+ if (n === 0 || dt_sub <= 0) return;
171
+
172
+ const spook_a = JOINT_BAUMGARTE / dt_sub;
173
+
174
+ const storage = system.storage;
175
+
176
+ for (let ji = 0; ji < n; ji++) {
177
+ const joint = joints[ji];
178
+ if (joint === undefined || joint === null) continue;
179
+
180
+ // Generation-checked validity: if A's body was unlinked (and its slot
181
+ // possibly reused by a different body), the stored packed id no longer
182
+ // validates — skip the stale joint rather than attach to the wrong body.
183
+ if (!storage.is_valid(joint._bodyIdA)) continue;
184
+ const idxA = system.__index_of(joint._bodyIdA);
185
+ const rbA = system.__bodies[idxA];
186
+ if (rbA === undefined) continue;
187
+ const trA = system.__transforms[idxA];
188
+
189
+ const to_world = joint._bodyIdB === JOINT_WORLD;
190
+ let rbB = null, trB = null, idxB = -1;
191
+ if (!to_world) {
192
+ if (!storage.is_valid(joint._bodyIdB)) continue;
193
+ idxB = system.__index_of(joint._bodyIdB);
194
+ rbB = system.__bodies[idxB];
195
+ if (rbB === undefined) continue;
196
+ trB = system.__transforms[idxB];
197
+ }
198
+
199
+ // Skip while a participating dynamic body sleeps — its velocity must
200
+ // stay zero until woken. (Island-aware joint sleep is a follow-up; for
201
+ // now jointed bodies that should stay coupled use DisableSleep.)
202
+ if (rbA.sleepState === SleepState.Sleeping) continue;
203
+ if (!to_world && rbB.sleepState === SleepState.Sleeping) continue;
204
+
205
+ // Both bodies static/kinematic → nothing to solve.
206
+ const invMA = inv_mass_of(rbA);
207
+ const invMB = to_world ? 0 : inv_mass_of(rbB);
208
+ if (invMA === 0 && invMB === 0) continue;
209
+
210
+ // World anchor A and its lever arm rA = R_A · localAnchorA.
211
+ const la = joint.localAnchorA;
212
+ v3_quat3_apply(scratch_anchor, 0, la.x, la.y, la.z,
213
+ trA.rotation[0], trA.rotation[1], trA.rotation[2], trA.rotation[3]);
214
+ const rAx = scratch_anchor[0], rAy = scratch_anchor[1], rAz = scratch_anchor[2];
215
+ const anchorAx = trA.position.x + rAx;
216
+ const anchorAy = trA.position.y + rAy;
217
+ const anchorAz = trA.position.z + rAz;
218
+
219
+ // World anchor B + lever arm rB. For a world anchor, localAnchorB is a
220
+ // fixed world point and there is no body B lever.
221
+ let rBx = 0, rBy = 0, rBz = 0;
222
+ let anchorBx, anchorBy, anchorBz;
223
+ const lb = joint.localAnchorB;
224
+ if (to_world) {
225
+ anchorBx = lb.x; anchorBy = lb.y; anchorBz = lb.z;
226
+ } else {
227
+ v3_quat3_apply(scratch_anchor, 0, lb.x, lb.y, lb.z,
228
+ trB.rotation[0], trB.rotation[1], trB.rotation[2], trB.rotation[3]);
229
+ rBx = scratch_anchor[0]; rBy = scratch_anchor[1]; rBz = scratch_anchor[2];
230
+ anchorBx = trB.position.x + rBx;
231
+ anchorBy = trB.position.y + rBy;
232
+ anchorBz = trB.position.z + rBz;
233
+ }
234
+
235
+ // Position error C = anchorA − anchorB (drive to zero on locked axes).
236
+ const Cx = anchorAx - anchorBx;
237
+ const Cy = anchorAy - anchorBy;
238
+ const Cz = anchorAz - anchorBz;
239
+
240
+ const mode = joint.dofMode;
241
+ const imp = joint.dofImpulse;
242
+ const lvA = rbA.linearVelocity, avA = rbA.angularVelocity;
243
+ const lvB = to_world ? null : rbB.linearVelocity;
244
+ const avB = to_world ? null : rbB.angularVelocity;
245
+
246
+ // === Frame A world axes — shared by the linear AND angular rows ===
247
+ // Every DOF is expressed in the joint's frame on body A. A locked
248
+ // linear DOF pins the anchor offset along a frame axis (all three →
249
+ // ball-socket); a FREE linear DOF is a prismatic slide. A locked
250
+ // angular DOF pins relative rotation about a frame axis; a FREE one is
251
+ // a hinge axis. Frame A = body-A rotation ⊗ localBasisA; the rows of
252
+ // its matrix are the frame axes in world (see quat3_to_matrix3).
253
+ const linLockX = mode[0] === DofMode.LOCKED;
254
+ const linLockY = mode[1] === DofMode.LOCKED;
255
+ const linLockZ = mode[2] === DofMode.LOCKED;
256
+ const angLockX = mode[3] === DofMode.LOCKED;
257
+ const angLockY = mode[4] === DofMode.LOCKED;
258
+ const angLockZ = mode[5] === DofMode.LOCKED;
259
+ if (!(linLockX || linLockY || linLockZ || angLockX || angLockY || angLockZ)) continue;
260
+
261
+ const lba = joint.localBasisA;
262
+ quat3_multiply(scratch_q, 0, trA.rotation[0], trA.rotation[1], trA.rotation[2], trA.rotation[3], lba[0], lba[1], lba[2], lba[3]);
263
+ const qAx = scratch_q[0], qAy = scratch_q[1], qAz = scratch_q[2], qAw = scratch_q[3];
264
+ quat3_to_matrix3(scratch_frame_a, 0, qAx, qAy, qAz, qAw);
265
+ const aXx = scratch_frame_a[0], aXy = scratch_frame_a[1], aXz = scratch_frame_a[2];
266
+ const aYx = scratch_frame_a[3], aYy = scratch_frame_a[4], aYz = scratch_frame_a[5];
267
+ const aZx = scratch_frame_a[6], aZy = scratch_frame_a[7], aZz = scratch_frame_a[8];
268
+
269
+ // --- Linear rows: effective mass + SPOOK bias along each locked frame
270
+ // axis. Error along an axis is `C · axis` (signed anchor offset).
271
+ // Convention is A−B (relative anchor velocity vA−vB, impulse +to A
272
+ // / −to B), matching contacts. ---
273
+ let m_eff_lx = 0, m_eff_ly = 0, m_eff_lz = 0;
274
+ let biasLx = 0, biasLy = 0, biasLz = 0;
275
+ if (linLockX) {
276
+ const k = axis_effective_mass(rbA, trA, invMA, rAx, rAy, rAz, aXx, aXy, aXz)
277
+ + (to_world ? 0 : axis_effective_mass(rbB, trB, invMB, rBx, rBy, rBz, aXx, aXy, aXz));
278
+ m_eff_lx = k > 0 ? 1 / k : 0;
279
+ let b = spook_a * (Cx * aXx + Cy * aXy + Cz * aXz);
280
+ if (b > MAX_JOINT_BIAS) b = MAX_JOINT_BIAS; else if (b < -MAX_JOINT_BIAS) b = -MAX_JOINT_BIAS;
281
+ biasLx = b;
282
+ }
283
+ if (linLockY) {
284
+ const k = axis_effective_mass(rbA, trA, invMA, rAx, rAy, rAz, aYx, aYy, aYz)
285
+ + (to_world ? 0 : axis_effective_mass(rbB, trB, invMB, rBx, rBy, rBz, aYx, aYy, aYz));
286
+ m_eff_ly = k > 0 ? 1 / k : 0;
287
+ let b = spook_a * (Cx * aYx + Cy * aYy + Cz * aYz);
288
+ if (b > MAX_JOINT_BIAS) b = MAX_JOINT_BIAS; else if (b < -MAX_JOINT_BIAS) b = -MAX_JOINT_BIAS;
289
+ biasLy = b;
290
+ }
291
+ if (linLockZ) {
292
+ const k = axis_effective_mass(rbA, trA, invMA, rAx, rAy, rAz, aZx, aZy, aZz)
293
+ + (to_world ? 0 : axis_effective_mass(rbB, trB, invMB, rBx, rBy, rBz, aZx, aZy, aZz));
294
+ m_eff_lz = k > 0 ? 1 / k : 0;
295
+ let b = spook_a * (Cx * aZx + Cy * aZy + Cz * aZz);
296
+ if (b > MAX_JOINT_BIAS) b = MAX_JOINT_BIAS; else if (b < -MAX_JOINT_BIAS) b = -MAX_JOINT_BIAS;
297
+ biasLz = b;
298
+ }
299
+ const doLinX = linLockX && m_eff_lx !== 0;
300
+ const doLinY = linLockY && m_eff_ly !== 0;
301
+ const doLinZ = linLockZ && m_eff_lz !== 0;
302
+
303
+ // --- Angular rows: relative-rotation error + effective mass about each
304
+ // locked frame axis. Convention is B−A (relative angular velocity
305
+ // ωB−ωA, impulse +to B / −to A). ---
306
+ let m_eff_ax = 0, m_eff_ay = 0, m_eff_az = 0;
307
+ let biasAx = 0, biasAy = 0, biasAz = 0;
308
+ let lockAX = false, lockAY = false, lockAZ = false;
309
+ if (angLockX || angLockY || angLockZ) {
310
+ // Frame B world rotation (identity for a world anchor → locks A's
311
+ // frame to world axes).
312
+ let qBx = 0, qBy = 0, qBz = 0, qBw = 1;
313
+ if (!to_world) {
314
+ const lbb = joint.localBasisB;
315
+ quat3_multiply(scratch_q, 0, trB.rotation[0], trB.rotation[1], trB.rotation[2], trB.rotation[3], lbb[0], lbb[1], lbb[2], lbb[3]);
316
+ qBx = scratch_q[0]; qBy = scratch_q[1]; qBz = scratch_q[2]; qBw = scratch_q[3];
317
+ }
318
+ // qD = conj(qA) ⊗ qB; small-angle rotation vector (shortest path via
319
+ // sign(w)) is the per-axis error in frame-A-local coords — pairs
320
+ // with the world frame axes above (axis i ↔ error component i).
321
+ quat3_multiply(scratch_q, 0, -qAx, -qAy, -qAz, qAw, qBx, qBy, qBz, qBw);
322
+ const es = scratch_q[3] < 0 ? -2 : 2;
323
+ const eX = es * scratch_q[0];
324
+ const eY = es * scratch_q[1];
325
+ const eZ = es * scratch_q[2];
326
+
327
+ if (angLockX) {
328
+ const k = angular_axis_effective_mass(rbA, trA, aXx, aXy, aXz)
329
+ + (to_world ? 0 : angular_axis_effective_mass(rbB, trB, aXx, aXy, aXz));
330
+ m_eff_ax = k > 0 ? 1 / k : 0;
331
+ let b = spook_a * eX;
332
+ if (b > MAX_JOINT_BIAS) b = MAX_JOINT_BIAS; else if (b < -MAX_JOINT_BIAS) b = -MAX_JOINT_BIAS;
333
+ biasAx = b;
334
+ lockAX = m_eff_ax !== 0;
335
+ }
336
+ if (angLockY) {
337
+ const k = angular_axis_effective_mass(rbA, trA, aYx, aYy, aYz)
338
+ + (to_world ? 0 : angular_axis_effective_mass(rbB, trB, aYx, aYy, aYz));
339
+ m_eff_ay = k > 0 ? 1 / k : 0;
340
+ let b = spook_a * eY;
341
+ if (b > MAX_JOINT_BIAS) b = MAX_JOINT_BIAS; else if (b < -MAX_JOINT_BIAS) b = -MAX_JOINT_BIAS;
342
+ biasAy = b;
343
+ lockAY = m_eff_ay !== 0;
344
+ }
345
+ if (angLockZ) {
346
+ const k = angular_axis_effective_mass(rbA, trA, aZx, aZy, aZz)
347
+ + (to_world ? 0 : angular_axis_effective_mass(rbB, trB, aZx, aZy, aZz));
348
+ m_eff_az = k > 0 ? 1 / k : 0;
349
+ let b = spook_a * eZ;
350
+ if (b > MAX_JOINT_BIAS) b = MAX_JOINT_BIAS; else if (b < -MAX_JOINT_BIAS) b = -MAX_JOINT_BIAS;
351
+ biasAz = b;
352
+ lockAZ = m_eff_az !== 0;
353
+ }
354
+ }
355
+
356
+ // --- Per-substep warm-start (frame axes): linear impulse `Σ imp[i]·aᵢ`
357
+ // applied at the anchors, angular impulse `Σ imp[3+i]·aᵢ` about the
358
+ // frame axes (+to B / −to A). ---
359
+ {
360
+ const Px = (doLinX ? imp[0] : 0) * aXx + (doLinY ? imp[1] : 0) * aYx + (doLinZ ? imp[2] : 0) * aZx;
361
+ const Py = (doLinX ? imp[0] : 0) * aXy + (doLinY ? imp[1] : 0) * aYy + (doLinZ ? imp[2] : 0) * aZy;
362
+ const Pz = (doLinX ? imp[0] : 0) * aXz + (doLinY ? imp[1] : 0) * aYz + (doLinZ ? imp[2] : 0) * aZz;
363
+ if (Px !== 0 || Py !== 0 || Pz !== 0) {
364
+ apply_impulse(rbA, trA, invMA, rAx, rAy, rAz, Px, Py, Pz, +1);
365
+ if (!to_world) apply_impulse(rbB, trB, invMB, rBx, rBy, rBz, Px, Py, Pz, -1);
366
+ }
367
+ const Lx = (lockAX ? imp[3] : 0) * aXx + (lockAY ? imp[4] : 0) * aYx + (lockAZ ? imp[5] : 0) * aZx;
368
+ const Ly = (lockAX ? imp[3] : 0) * aXy + (lockAY ? imp[4] : 0) * aYy + (lockAZ ? imp[5] : 0) * aZy;
369
+ const Lz = (lockAX ? imp[3] : 0) * aXz + (lockAY ? imp[4] : 0) * aYz + (lockAZ ? imp[5] : 0) * aZz;
370
+ if (Lx !== 0 || Ly !== 0 || Lz !== 0) {
371
+ if (!to_world) apply_angular_impulse(rbB, trB, Lx, Ly, Lz, 1, +1);
372
+ apply_angular_impulse(rbA, trA, Lx, Ly, Lz, 1, -1);
373
+ }
374
+ }
375
+
376
+ for (let it = 0; it < iters; it++) {
377
+ // Linear rows (frame axes). The relative anchor velocity is
378
+ // recomputed before each row so successive rows see the prior
379
+ // impulses (Gauss-Seidel coupling).
380
+ if (doLinX) {
381
+ const rvx = (lvA[0] + avA[1] * rAz - avA[2] * rAy) - (to_world ? 0 : (lvB[0] + avB[1] * rBz - avB[2] * rBy));
382
+ const rvy = (lvA[1] + avA[2] * rAx - avA[0] * rAz) - (to_world ? 0 : (lvB[1] + avB[2] * rBx - avB[0] * rBz));
383
+ const rvz = (lvA[2] + avA[0] * rAy - avA[1] * rAx) - (to_world ? 0 : (lvB[2] + avB[0] * rBy - avB[1] * rBx));
384
+ const lambda = -m_eff_lx * ((rvx * aXx + rvy * aXy + rvz * aXz) + biasLx);
385
+ imp[0] += lambda;
386
+ apply_impulse(rbA, trA, invMA, rAx, rAy, rAz, lambda * aXx, lambda * aXy, lambda * aXz, +1);
387
+ if (!to_world) apply_impulse(rbB, trB, invMB, rBx, rBy, rBz, lambda * aXx, lambda * aXy, lambda * aXz, -1);
388
+ }
389
+ if (doLinY) {
390
+ const rvx = (lvA[0] + avA[1] * rAz - avA[2] * rAy) - (to_world ? 0 : (lvB[0] + avB[1] * rBz - avB[2] * rBy));
391
+ const rvy = (lvA[1] + avA[2] * rAx - avA[0] * rAz) - (to_world ? 0 : (lvB[1] + avB[2] * rBx - avB[0] * rBz));
392
+ const rvz = (lvA[2] + avA[0] * rAy - avA[1] * rAx) - (to_world ? 0 : (lvB[2] + avB[0] * rBy - avB[1] * rBx));
393
+ const lambda = -m_eff_ly * ((rvx * aYx + rvy * aYy + rvz * aYz) + biasLy);
394
+ imp[1] += lambda;
395
+ apply_impulse(rbA, trA, invMA, rAx, rAy, rAz, lambda * aYx, lambda * aYy, lambda * aYz, +1);
396
+ if (!to_world) apply_impulse(rbB, trB, invMB, rBx, rBy, rBz, lambda * aYx, lambda * aYy, lambda * aYz, -1);
397
+ }
398
+ if (doLinZ) {
399
+ const rvx = (lvA[0] + avA[1] * rAz - avA[2] * rAy) - (to_world ? 0 : (lvB[0] + avB[1] * rBz - avB[2] * rBy));
400
+ const rvy = (lvA[1] + avA[2] * rAx - avA[0] * rAz) - (to_world ? 0 : (lvB[1] + avB[2] * rBx - avB[0] * rBz));
401
+ const rvz = (lvA[2] + avA[0] * rAy - avA[1] * rAx) - (to_world ? 0 : (lvB[2] + avB[0] * rBy - avB[1] * rBx));
402
+ const lambda = -m_eff_lz * ((rvx * aZx + rvy * aZy + rvz * aZz) + biasLz);
403
+ imp[2] += lambda;
404
+ apply_impulse(rbA, trA, invMA, rAx, rAy, rAz, lambda * aZx, lambda * aZy, lambda * aZz, +1);
405
+ if (!to_world) apply_impulse(rbB, trB, invMB, rBx, rBy, rBz, lambda * aZx, lambda * aZy, lambda * aZz, -1);
406
+ }
407
+
408
+ // Angular rows (frame axes): drive ωB−ωA about each locked axis to
409
+ // zero with the SPOOK bias.
410
+ if (lockAX) {
411
+ let wrel = -(avA[0] * aXx + avA[1] * aXy + avA[2] * aXz);
412
+ if (!to_world) wrel += avB[0] * aXx + avB[1] * aXy + avB[2] * aXz;
413
+ const lambda = -m_eff_ax * (wrel + biasAx);
414
+ imp[3] += lambda;
415
+ if (!to_world) apply_angular_impulse(rbB, trB, aXx, aXy, aXz, lambda, +1);
416
+ apply_angular_impulse(rbA, trA, aXx, aXy, aXz, lambda, -1);
417
+ }
418
+ if (lockAY) {
419
+ let wrel = -(avA[0] * aYx + avA[1] * aYy + avA[2] * aYz);
420
+ if (!to_world) wrel += avB[0] * aYx + avB[1] * aYy + avB[2] * aYz;
421
+ const lambda = -m_eff_ay * (wrel + biasAy);
422
+ imp[4] += lambda;
423
+ if (!to_world) apply_angular_impulse(rbB, trB, aYx, aYy, aYz, lambda, +1);
424
+ apply_angular_impulse(rbA, trA, aYx, aYy, aYz, lambda, -1);
425
+ }
426
+ if (lockAZ) {
427
+ let wrel = -(avA[0] * aZx + avA[1] * aZy + avA[2] * aZz);
428
+ if (!to_world) wrel += avB[0] * aZx + avB[1] * aZy + avB[2] * aZz;
429
+ const lambda = -m_eff_az * (wrel + biasAz);
430
+ imp[5] += lambda;
431
+ if (!to_world) apply_angular_impulse(rbB, trB, aZx, aZy, aZz, lambda, +1);
432
+ apply_angular_impulse(rbA, trA, aZx, aZy, aZz, lambda, -1);
433
+ }
434
+ }
435
+ }
436
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Sentinel for {@link Joint#_jointId} before the joint is linked into a
3
+ * {@link PhysicsSystem}.
4
+ * @type {number}
5
+ */
6
+ export const JOINT_UNALLOCATED: number;
7
+ /**
8
+ * `entityB` value meaning "the world" — body A is constrained to a fixed
9
+ * world anchor rather than to a second body.
10
+ * @type {number}
11
+ */
12
+ export const JOINT_WORLD: number;
13
+ /**
14
+ * A configurable 6-DOF constraint between two bodies (or a body and the
15
+ * world). Connects body A's anchor frame to body B's; each of the 6 relative
16
+ * degrees of freedom (3 linear, 3 angular) is independently
17
+ * locked / free / limited / springy / motorised ({@link DofMode}). One
18
+ * constraint type expresses the whole joint taxonomy — see DofMode.
19
+ *
20
+ * The default configuration is a **ball-socket** (point-to-point): the three
21
+ * linear DOFs LOCKED (the two anchor points are held coincident), the three
22
+ * angular DOFs FREE. That alone gives chains, ropes, and pendulums.
23
+ *
24
+ * Anchors live in body-local frames. When `entityB === JOINT_WORLD`,
25
+ * `localAnchorB` is interpreted as a fixed **world** point instead of a
26
+ * local anchor on a second body.
27
+ *
28
+ * Solver state ({@link dofImpulse}, the `_*` fields) is owned by the
29
+ * PhysicsSystem; user code must not write it.
30
+ *
31
+ * @author Alex Goldring
32
+ * @copyright Company Named Limited (c) 2026
33
+ */
34
+ export class Joint {
35
+ /**
36
+ * Entity owning body A (the constraint's first body — always a real body).
37
+ * @type {number}
38
+ */
39
+ entityA: number;
40
+ /**
41
+ * Entity owning body B, or {@link JOINT_WORLD} to anchor A to the world.
42
+ * @type {number}
43
+ */
44
+ entityB: number;
45
+ /**
46
+ * Anchor point on body A, in A's local frame.
47
+ * @readonly
48
+ * @type {Vector3}
49
+ */
50
+ readonly localAnchorA: Vector3;
51
+ /**
52
+ * Anchor point on body B in B's local frame — or, when
53
+ * `entityB === JOINT_WORLD`, a fixed world-space anchor point.
54
+ * @readonly
55
+ * @type {Vector3}
56
+ */
57
+ readonly localAnchorB: Vector3;
58
+ /**
59
+ * Orientation of the joint's reference frame on body A, in A's local
60
+ * space (a unit quaternion; identity = the joint frame is the body
61
+ * frame). The 3 linear and 3 angular DOFs are expressed in this frame:
62
+ * locked/limited/etc. axes are the frame's axes, not world axes. For a
63
+ * hinge, the free angular axis is one of these frame axes — orient the
64
+ * frame so that axis is the hinge axis.
65
+ * @readonly
66
+ * @type {Quaternion}
67
+ */
68
+ readonly localBasisA: Quaternion;
69
+ /**
70
+ * Orientation of the joint's reference frame on body B, in B's local
71
+ * space. At rest a fully-locked joint holds frame B aligned to frame A.
72
+ * When `entityB === JOINT_WORLD`, the world frame is treated as identity
73
+ * (angular locks hold A's frame to world axes).
74
+ * @readonly
75
+ * @type {Quaternion}
76
+ */
77
+ readonly localBasisB: Quaternion;
78
+ /**
79
+ * Per-DOF mode, 6 entries: `[lin x, lin y, lin z, ang x, ang y, ang z]`.
80
+ * Default = ball-socket (linear locked, angular free).
81
+ * @readonly
82
+ * @type {Uint8Array}
83
+ */
84
+ readonly dofMode: Uint8Array;
85
+ /**
86
+ * Lower / upper bounds per DOF, used by {@link DofMode.LIMITED}. Linear in
87
+ * metres, angular in radians. (Reserved — consumed once the LIMITED mode
88
+ * lands.)
89
+ * @readonly
90
+ * @type {Float64Array}
91
+ */
92
+ readonly dofLowerLimit: Float64Array;
93
+ /** @readonly @type {Float64Array} */
94
+ readonly dofUpperLimit: Float64Array;
95
+ /**
96
+ * Spring stiffness / damping per DOF, used by {@link DofMode.SPRING}.
97
+ * (Reserved — consumed once the SPRING mode lands.)
98
+ * @readonly
99
+ * @type {Float64Array}
100
+ */
101
+ readonly dofStiffness: Float64Array;
102
+ /** @readonly @type {Float64Array} */
103
+ readonly dofDamping: Float64Array;
104
+ /**
105
+ * Motor target velocity / max force per DOF, used by
106
+ * {@link DofMode.MOTOR}. (Reserved — consumed once the MOTOR mode lands.)
107
+ * @readonly
108
+ * @type {Float64Array}
109
+ */
110
+ readonly dofMotorTargetVelocity: Float64Array;
111
+ /** @readonly @type {Float64Array} */
112
+ readonly dofMotorMaxForce: Float64Array;
113
+ /**
114
+ * Accumulated constraint impulse per DOF — warm-start state carried across
115
+ * frames. System-owned.
116
+ * @readonly
117
+ * @type {Float64Array}
118
+ */
119
+ readonly dofImpulse: Float64Array;
120
+ /**
121
+ * Resolved packed body id of A, set at link time. System-private.
122
+ * @type {number}
123
+ */
124
+ _bodyIdA: number;
125
+ /**
126
+ * Resolved packed body id of B, or {@link JOINT_WORLD}. System-private.
127
+ * @type {number}
128
+ */
129
+ _bodyIdB: number;
130
+ /**
131
+ * Packed joint handle assigned at link. System-private.
132
+ * @type {number}
133
+ */
134
+ _jointId: number;
135
+ /**
136
+ * Configure this joint as a ball-socket (point-to-point) between two
137
+ * anchors. Linear DOFs locked, angular free — the default, provided as a
138
+ * named helper for clarity at call sites.
139
+ * @returns {Joint} this
140
+ */
141
+ asBallSocket(): Joint;
142
+ /**
143
+ * Configure as a weld (fixed) joint: all 6 DOFs locked, holding the two
144
+ * bodies rigidly in their relative pose.
145
+ * @returns {Joint} this
146
+ */
147
+ asWeld(): Joint;
148
+ /**
149
+ * Configure as a hinge (revolute): 3 linear + 2 angular locked, free to
150
+ * rotate about a single frame axis. Orient {@link localBasisA} /
151
+ * {@link localBasisB} so that frame axis is the desired hinge axis.
152
+ *
153
+ * @param {number} [freeAngularAxis] which frame axis is free: 0 = x
154
+ * (default), 1 = y, 2 = z.
155
+ * @returns {Joint} this
156
+ */
157
+ asHinge(freeAngularAxis?: number): Joint;
158
+ /**
159
+ * Configure as a prismatic (slider): 2 linear + 3 angular locked, free to
160
+ * translate along a single frame axis. Orient {@link localBasisA} /
161
+ * {@link localBasisB} so that frame axis is the desired slide direction.
162
+ *
163
+ * @param {number} [freeLinearAxis] which frame axis is free: 0 = x
164
+ * (default), 1 = y, 2 = z.
165
+ * @returns {Joint} this
166
+ */
167
+ asPrismatic(freeLinearAxis?: number): Joint;
168
+ /**
169
+ * @readonly
170
+ * @type {boolean}
171
+ */
172
+ readonly isJoint: boolean;
173
+ }
174
+ export namespace Joint {
175
+ let typeName: string;
176
+ }
177
+ import Vector3 from "../../../core/geom/Vector3.js";
178
+ import Quaternion from "../../../core/geom/Quaternion.js";
179
+ //# sourceMappingURL=Joint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Joint.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/Joint.js"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,gCAFU,MAAM,CAEoB;AAEpC;;;;GAIG;AACH,0BAFU,MAAM,CAEc;AAE9B;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;IAEI;;;OAGG;IACH,SAFU,MAAM,CAEH;IAEb;;;OAGG;IACH,SAFU,MAAM,CAEM;IAEtB;;;;OAIG;IACH,uBAFU,OAAO,CAEmB;IAEpC;;;;;OAKG;IACH,uBAFU,OAAO,CAEmB;IAEpC;;;;;;;;;OASG;IACH,sBAFU,UAAU,CAEW;IAE/B;;;;;;;OAOG;IACH,sBAFU,UAAU,CAEW;IAE/B;;;;;OAKG;IACH,kBAFU,UAAU,CAKjB;IAEH;;;;;;OAMG;IACH,wBAFU,YAAY,CAEc;IACpC,qCAAqC;IACrC,wBADqB,YAAY,CACG;IAEpC;;;;;OAKG;IACH,uBAFU,YAAY,CAEa;IACnC,qCAAqC;IACrC,qBADqB,YAAY,CACA;IAEjC;;;;;OAKG;IACH,iCAFU,YAAY,CAEuB;IAC7C,qCAAqC;IACrC,2BADqB,YAAY,CACM;IAEvC;;;;;OAKG;IACH,qBAFU,YAAY,CAEW;IAEjC;;;OAGG;IACH,UAFU,MAAM,CAEF;IACd;;;OAGG;IACH,UAFU,MAAM,CAEO;IACvB;;;OAGG;IACH,UAFU,MAAM,CAEa;IAE7B;;;;;OAKG;IACH,gBAFa,KAAK,CAUjB;IAED;;;;OAIG;IACH,UAFa,KAAK,CAKjB;IAED;;;;;;;;OAQG;IACH,0BAJW,MAAM,GAEJ,KAAK,CAWjB;IAED;;;;;;;;OAQG;IACH,6BAJW,MAAM,GAEJ,KAAK,CAWjB;IASL;;;OAGG;IACH,kBAFU,OAAO,CAEM;CAZtB;;kBAIS,MAAM;;oBAjOI,+BAA+B;uBAC5B,kCAAkC"}