@series-inc/rundot-syncplay 5.23.0-beta.7 → 5.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +116 -15
  2. package/dist/browser.d.ts +87 -55
  3. package/dist/browser.js +47 -52
  4. package/dist/certification.d.ts +32 -3
  5. package/dist/certification.js +88 -3
  6. package/dist/cjs/browser.js +259 -87
  7. package/dist/cjs/certification.js +87 -2
  8. package/dist/cjs/collision.js +58 -0
  9. package/dist/cjs/creator.js +2 -1
  10. package/dist/cjs/engine-complete.js +213 -3
  11. package/dist/cjs/engine-parity-matrix.js +19 -4
  12. package/dist/cjs/index.js +199 -132
  13. package/dist/cjs/math.js +342 -15
  14. package/dist/cjs/movement3d.js +230 -1
  15. package/dist/cjs/node.js +5 -1
  16. package/dist/cjs/noise.js +58 -0
  17. package/dist/cjs/physics-cert.js +147 -6
  18. package/dist/cjs/physics2d.js +10 -4
  19. package/dist/cjs/physics3d-joints.js +637 -0
  20. package/dist/cjs/physics3d-shared.js +288 -0
  21. package/dist/cjs/physics3d-solver.js +1351 -0
  22. package/dist/cjs/physics3d-vehicle.js +467 -0
  23. package/dist/cjs/physics3d.js +1057 -223
  24. package/dist/cjs/random.js +49 -0
  25. package/dist/cjs/replay-bundle.js +127 -20
  26. package/dist/cjs/sample-scenes.js +3 -0
  27. package/dist/cjs/sdk-offline.js +6 -32
  28. package/dist/cjs/sdk-session.js +156 -0
  29. package/dist/cjs/session-build.js +42 -0
  30. package/dist/cjs/testing.js +107 -0
  31. package/dist/cli.js +40 -12
  32. package/dist/collision.d.ts +12 -0
  33. package/dist/collision.js +52 -0
  34. package/dist/creator.d.ts +1 -1
  35. package/dist/creator.js +1 -1
  36. package/dist/engine-complete.js +214 -4
  37. package/dist/engine-parity-matrix.js +19 -4
  38. package/dist/index.d.ts +94 -89
  39. package/dist/index.js +45 -56
  40. package/dist/math.d.ts +59 -6
  41. package/dist/math.js +342 -15
  42. package/dist/movement3d.d.ts +73 -0
  43. package/dist/movement3d.js +229 -1
  44. package/dist/node.d.ts +4 -0
  45. package/dist/node.js +2 -0
  46. package/dist/noise.d.ts +7 -0
  47. package/dist/noise.js +51 -0
  48. package/dist/physics-cert.d.ts +1 -1
  49. package/dist/physics-cert.js +147 -6
  50. package/dist/physics2d.js +10 -4
  51. package/dist/physics3d-joints.d.ts +94 -0
  52. package/dist/physics3d-joints.js +634 -0
  53. package/dist/physics3d-shared.d.ts +72 -0
  54. package/dist/physics3d-shared.js +257 -0
  55. package/dist/physics3d-solver.d.ts +197 -0
  56. package/dist/physics3d-solver.js +1346 -0
  57. package/dist/physics3d-vehicle.d.ts +84 -0
  58. package/dist/physics3d-vehicle.js +463 -0
  59. package/dist/physics3d.d.ts +108 -8
  60. package/dist/physics3d.js +1006 -177
  61. package/dist/random.d.ts +7 -0
  62. package/dist/random.js +49 -0
  63. package/dist/replay-bundle.d.ts +40 -1
  64. package/dist/replay-bundle.js +126 -20
  65. package/dist/sample-scenes.js +3 -0
  66. package/dist/sdk-offline.js +6 -32
  67. package/dist/sdk-session.d.ts +47 -0
  68. package/dist/sdk-session.js +153 -0
  69. package/dist/session-build.d.ts +21 -0
  70. package/dist/session-build.js +39 -0
  71. package/dist/testing.d.ts +52 -0
  72. package/dist/testing.js +45 -0
  73. package/dist/tools/vite-plugin.d.ts +8 -0
  74. package/dist/tools/vite-plugin.js +63 -9
  75. package/package.json +11 -3
@@ -0,0 +1,637 @@
1
+ "use strict";
2
+ /**
3
+ * Impulse-level joint constraints for the deterministic rigid-body solver:
4
+ * ball (point-to-point), hinge (axis + limits + motor), weld, distance (rope),
5
+ * and grab (world-target point motor — the gravity-gun/carry primitive). Each
6
+ * joint is solved as accumulated impulses inside the solver's iteration loop
7
+ * via the `SolverJointSolve` interface; hinge angle and warm-start impulses live
8
+ * in the canonical joint cache.
9
+ *
10
+ * Angles (hinge `minAngle`/`maxAngle`/`motorSpeed`) are in radians / radians per
11
+ * tick — natural units for the radian-valued angular velocity the solver
12
+ * integrates.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.createSolverJoints = createSolverJoints;
16
+ const physics3d_shared_js_1 = require("./physics3d-shared.js");
17
+ const JOINT_BAUMGARTE = 0.2;
18
+ function requireIndex(id, jointId, indexById) {
19
+ const index = indexById.get(id);
20
+ if (index === undefined) {
21
+ throw new Error(`physics3d joint ${jointId}: references unknown body ${id}`);
22
+ }
23
+ return index;
24
+ }
25
+ function assertFiniteVec(v, label, jointId) {
26
+ if (!Number.isFinite(v.x) || !Number.isFinite(v.y) || !Number.isFinite(v.z)) {
27
+ throw new Error(`physics3d joint ${jointId}: ${label} must be finite`);
28
+ }
29
+ }
30
+ /** Effective-mass matrix for a point constraint between bodies a and b. */
31
+ function pointConstraintK(ctx, aIndex, bIndex, rA, rB) {
32
+ const imA = ctx.invMass[aIndex];
33
+ const imB = ctx.invMass[bIndex];
34
+ const linear = (0, physics3d_shared_js_1.mat3FromDiag)({ x: imA.x + imB.x, y: imA.y + imB.y, z: imA.z + imB.z });
35
+ const skewA = (0, physics3d_shared_js_1.skewMat3)(rA);
36
+ const skewB = (0, physics3d_shared_js_1.skewMat3)(rB);
37
+ const angA = (0, physics3d_shared_js_1.mat3Multiply)((0, physics3d_shared_js_1.mat3Multiply)(skewA, ctx.invInertia[aIndex]), skewA);
38
+ const angB = (0, physics3d_shared_js_1.mat3Multiply)((0, physics3d_shared_js_1.mat3Multiply)(skewB, ctx.invInertia[bIndex]), skewB);
39
+ return (0, physics3d_shared_js_1.mat3Sub)((0, physics3d_shared_js_1.mat3Sub)(linear, angA), angB);
40
+ }
41
+ function ballJoint(joint, aIndex, bIndex, warm, weld) {
42
+ const state = {
43
+ impulse: warm ? { x: warm.impulseX, y: warm.impulseY, z: warm.impulseZ } : { x: 0, y: 0, z: 0 },
44
+ axialImpulse: 0,
45
+ hingeAngle: 0,
46
+ };
47
+ let rA = { x: 0, y: 0, z: 0 };
48
+ let rB = { x: 0, y: 0, z: 0 };
49
+ let kInv = [0, 0, 0, 0, 0, 0, 0, 0, 0];
50
+ let biasTarget = { x: 0, y: 0, z: 0 };
51
+ let angKInv = [0, 0, 0, 0, 0, 0, 0, 0, 0];
52
+ return {
53
+ bodyIndices: [aIndex, bIndex],
54
+ prepare(ctx) {
55
+ const A = ctx.bodies[aIndex];
56
+ const B = ctx.bodies[bIndex];
57
+ rA = (0, physics3d_shared_js_1.rotatePointByQuat)(A.q, joint.anchorA);
58
+ rB = (0, physics3d_shared_js_1.rotatePointByQuat)(B.q, joint.anchorB);
59
+ const c = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.p, rB), (0, physics3d_shared_js_1.addVec3)(A.p, rA));
60
+ // split-impulse: pseudo-velocity target that closes position error without
61
+ // pumping real momentum (Baumgarte in the real solve pumps energy).
62
+ biasTarget = (0, physics3d_shared_js_1.scaleVec3)(c, -JOINT_BAUMGARTE / ctx.dt);
63
+ kInv = (0, physics3d_shared_js_1.invertMat3)(pointConstraintK(ctx, aIndex, bIndex, rA, rB));
64
+ if (weld) {
65
+ angKInv = (0, physics3d_shared_js_1.invertMat3)((0, physics3d_shared_js_1.mat3Sub)((0, physics3d_shared_js_1.mat3Sub)((0, physics3d_shared_js_1.mat3FromDiag)({ x: 0, y: 0, z: 0 }), scaleMat3(ctx.invInertia[aIndex], -1)), scaleMat3(ctx.invInertia[bIndex], -1)));
66
+ }
67
+ ctx.apply(aIndex, bIndex, rA, rB, state.impulse);
68
+ },
69
+ iterate(ctx) {
70
+ const A = ctx.bodies[aIndex];
71
+ const B = ctx.bodies[bIndex];
72
+ const relVel = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.v, (0, physics3d_shared_js_1.crossVec3)(B.w, rB)), (0, physics3d_shared_js_1.addVec3)(A.v, (0, physics3d_shared_js_1.crossVec3)(A.w, rA)));
73
+ const dImpulse = (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.mulMat3Vec)(kInv, relVel), -1);
74
+ state.impulse = (0, physics3d_shared_js_1.addVec3)(state.impulse, dImpulse);
75
+ ctx.apply(aIndex, bIndex, rA, rB, dImpulse);
76
+ if (weld) {
77
+ const relOmega = (0, physics3d_shared_js_1.subVec3)(B.w, A.w);
78
+ const angImpulse = (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.mulMat3Vec)(angKInv, relOmega), -1);
79
+ ctx.applyAngular(aIndex, bIndex, angImpulse);
80
+ }
81
+ // position correction on pseudo-velocities
82
+ const relBias = ctx.biasRelativeVelocity(aIndex, bIndex, rA, rB);
83
+ const dBias = (0, physics3d_shared_js_1.mulMat3Vec)(kInv, (0, physics3d_shared_js_1.subVec3)(biasTarget, relBias));
84
+ ctx.applyBias(aIndex, bIndex, rA, rB, dBias);
85
+ },
86
+ emitCache() {
87
+ const reaction2 = (0, physics3d_shared_js_1.dotVec3)(state.impulse, state.impulse);
88
+ const broken = joint.breakImpulse !== undefined && reaction2 > joint.breakImpulse * joint.breakImpulse;
89
+ return {
90
+ jointId: joint.id, impulseX: state.impulse.x, impulseY: state.impulse.y, impulseZ: state.impulse.z, axialImpulse: 0, hingeAngle: 0,
91
+ ...(broken ? { broken: true } : {}),
92
+ };
93
+ },
94
+ };
95
+ }
96
+ function scaleMat3(m, s) {
97
+ return m.map((value) => value * s);
98
+ }
99
+ function hingeJoint(joint, aIndex, bIndex, warm) {
100
+ const state = {
101
+ impulse: warm ? { x: warm.impulseX, y: warm.impulseY, z: warm.impulseZ } : { x: 0, y: 0, z: 0 },
102
+ axialImpulse: warm?.axialImpulse ?? 0,
103
+ hingeAngle: warm?.hingeAngle ?? 0,
104
+ };
105
+ let rA = { x: 0, y: 0, z: 0 };
106
+ let rB = { x: 0, y: 0, z: 0 };
107
+ let kInv = [0, 0, 0, 0, 0, 0, 0, 0, 0];
108
+ let biasTarget = { x: 0, y: 0, z: 0 };
109
+ let axis = { x: 0, y: 1, z: 0 };
110
+ let t1 = { x: 1, y: 0, z: 0 };
111
+ let t2 = { x: 0, y: 0, z: 1 };
112
+ let angMassAxis = 0;
113
+ let angMassT1 = 0;
114
+ let angMassT2 = 0;
115
+ let limitImpulse = 0;
116
+ let ctxRef;
117
+ const angularMass = (ctx, t) => {
118
+ const denom = (0, physics3d_shared_js_1.dotVec3)(t, (0, physics3d_shared_js_1.mulMat3Vec)(ctx.invInertia[aIndex], t)) + (0, physics3d_shared_js_1.dotVec3)(t, (0, physics3d_shared_js_1.mulMat3Vec)(ctx.invInertia[bIndex], t));
119
+ /* istanbul ignore next -- a hinge with a real dynamic body has non-zero inverse inertia along the axis; the zero guard is defensive. */
120
+ return denom === 0 ? 0 : 1 / denom;
121
+ };
122
+ return {
123
+ bodyIndices: [aIndex, bIndex],
124
+ prepare(ctx) {
125
+ const A = ctx.bodies[aIndex];
126
+ const B = ctx.bodies[bIndex];
127
+ rA = (0, physics3d_shared_js_1.rotatePointByQuat)(A.q, joint.anchorA);
128
+ rB = (0, physics3d_shared_js_1.rotatePointByQuat)(B.q, joint.anchorB);
129
+ biasTarget = (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.p, rB), (0, physics3d_shared_js_1.addVec3)(A.p, rA)), -JOINT_BAUMGARTE / ctx.dt);
130
+ kInv = (0, physics3d_shared_js_1.invertMat3)(pointConstraintK(ctx, aIndex, bIndex, rA, rB));
131
+ const rawAxis = (0, physics3d_shared_js_1.rotatePointByQuat)(A.q, joint.axisA);
132
+ const len = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(rawAxis, rawAxis));
133
+ /* istanbul ignore next -- axisA is validated non-zero and rotation preserves length, so len is never zero. */
134
+ axis = len === 0 ? { x: 0, y: 1, z: 0 } : (0, physics3d_shared_js_1.scaleVec3)(rawAxis, 1 / len);
135
+ // deterministic perpendicular basis
136
+ t1 = Math.abs(axis.x) >= 0.57735 ? unit({ x: axis.y, y: -axis.x, z: 0 }) : unit({ x: 0, y: axis.z, z: -axis.y });
137
+ t2 = (0, physics3d_shared_js_1.crossVec3)(axis, t1);
138
+ angMassAxis = angularMass(ctx, axis);
139
+ angMassT1 = angularMass(ctx, t1);
140
+ angMassT2 = angularMass(ctx, t2);
141
+ limitImpulse = 0;
142
+ ctxRef = ctx;
143
+ ctx.apply(aIndex, bIndex, rA, rB, state.impulse);
144
+ if (state.axialImpulse !== 0) {
145
+ ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)(axis, state.axialImpulse));
146
+ }
147
+ },
148
+ iterate(ctx) {
149
+ const A = ctx.bodies[aIndex];
150
+ const B = ctx.bodies[bIndex];
151
+ // point constraint first so its r×P perturbation of the axial DOF is
152
+ // corrected by the motor/limit rows that follow (split-impulse: no
153
+ // position Baumgarte in the real solve, so it cannot pump the motor).
154
+ const relVel = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.v, (0, physics3d_shared_js_1.crossVec3)(B.w, rB)), (0, physics3d_shared_js_1.addVec3)(A.v, (0, physics3d_shared_js_1.crossVec3)(A.w, rA)));
155
+ const dPoint = (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.mulMat3Vec)(kInv, relVel), -1);
156
+ state.impulse = (0, physics3d_shared_js_1.addVec3)(state.impulse, dPoint);
157
+ ctx.apply(aIndex, bIndex, rA, rB, dPoint);
158
+ const relBias = ctx.biasRelativeVelocity(aIndex, bIndex, rA, rB);
159
+ ctx.applyBias(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.mulMat3Vec)(kInv, (0, physics3d_shared_js_1.subVec3)(biasTarget, relBias)));
160
+ // perpendicular angular locks
161
+ for (const [t, mass] of [[t1, angMassT1], [t2, angMassT2]]) {
162
+ const relW = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(B.w, A.w), t);
163
+ ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)(t, -mass * relW));
164
+ }
165
+ // motor
166
+ if (joint.motorSpeed !== undefined) {
167
+ const maxTorque = joint.maxMotorTorque ?? 0;
168
+ const relAxial = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(B.w, A.w), axis);
169
+ let dImpulse = angMassAxis * (joint.motorSpeed - relAxial);
170
+ const maxImpulse = maxTorque * ctx.dt;
171
+ const newAcc = (0, physics3d_shared_js_1.clampNumber)(state.axialImpulse + dImpulse, -maxImpulse, maxImpulse);
172
+ dImpulse = newAcc - state.axialImpulse;
173
+ state.axialImpulse = newAcc;
174
+ ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)(axis, dImpulse));
175
+ }
176
+ // limits (inequality, biased by how far past)
177
+ if (joint.minAngle !== undefined && state.hingeAngle <= joint.minAngle) {
178
+ const overshoot = joint.minAngle - state.hingeAngle;
179
+ const relAxial = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(B.w, A.w), axis);
180
+ let dImpulse = angMassAxis * (-relAxial + (JOINT_BAUMGARTE / ctx.dt) * overshoot);
181
+ const newAcc = Math.max(0, limitImpulse + dImpulse);
182
+ dImpulse = newAcc - limitImpulse;
183
+ limitImpulse = newAcc;
184
+ ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)(axis, dImpulse));
185
+ }
186
+ else if (joint.maxAngle !== undefined && state.hingeAngle >= joint.maxAngle) {
187
+ const overshoot = state.hingeAngle - joint.maxAngle;
188
+ const relAxial = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(B.w, A.w), axis);
189
+ let dImpulse = angMassAxis * (-relAxial - (JOINT_BAUMGARTE / ctx.dt) * overshoot);
190
+ const newAcc = Math.min(0, limitImpulse + dImpulse);
191
+ dImpulse = newAcc - limitImpulse;
192
+ limitImpulse = newAcc;
193
+ ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)(axis, dImpulse));
194
+ }
195
+ },
196
+ emitCache() {
197
+ // integrate the hinge angle from the post-solve relative axial velocity
198
+ /* istanbul ignore else -- prepare() always runs before emitCache(), so ctxRef is defined. */
199
+ if (ctxRef !== undefined) {
200
+ const relAxial = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(ctxRef.bodies[bIndex].w, ctxRef.bodies[aIndex].w), axis);
201
+ state.hingeAngle += relAxial * ctxRef.dt;
202
+ }
203
+ const reaction2 = (0, physics3d_shared_js_1.dotVec3)(state.impulse, state.impulse);
204
+ const broken = joint.breakImpulse !== undefined && reaction2 > joint.breakImpulse * joint.breakImpulse;
205
+ return {
206
+ jointId: joint.id,
207
+ impulseX: state.impulse.x,
208
+ impulseY: state.impulse.y,
209
+ impulseZ: state.impulse.z,
210
+ axialImpulse: state.axialImpulse,
211
+ hingeAngle: state.hingeAngle,
212
+ ...(broken ? { broken: true } : {}),
213
+ };
214
+ },
215
+ };
216
+ }
217
+ function unit(v) {
218
+ const l = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(v, v));
219
+ /* istanbul ignore next -- unit() is only applied to non-zero perpendicular basis vectors derived from the hinge axis. */
220
+ return l === 0 ? { x: 0, y: 0, z: 0 } : (0, physics3d_shared_js_1.scaleVec3)(v, 1 / l);
221
+ }
222
+ function distanceJoint(joint, aIndex, bIndex, warm) {
223
+ const state = {
224
+ impulse: { x: warm?.axialImpulse ?? 0, y: 0, z: 0 },
225
+ axialImpulse: warm?.axialImpulse ?? 0,
226
+ hingeAngle: 0,
227
+ };
228
+ let rA = { x: 0, y: 0, z: 0 };
229
+ let rB = { x: 0, y: 0, z: 0 };
230
+ let n = { x: 0, y: 1, z: 0 };
231
+ let mass = 0;
232
+ let length = 0;
233
+ return {
234
+ bodyIndices: [aIndex, bIndex],
235
+ prepare(ctx) {
236
+ const A = ctx.bodies[aIndex];
237
+ const B = ctx.bodies[bIndex];
238
+ rA = (0, physics3d_shared_js_1.rotatePointByQuat)(A.q, joint.anchorA);
239
+ rB = (0, physics3d_shared_js_1.rotatePointByQuat)(B.q, joint.anchorB);
240
+ const d = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.p, rB), (0, physics3d_shared_js_1.addVec3)(A.p, rA));
241
+ length = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(d, d));
242
+ n = length === 0 ? { x: 0, y: 1, z: 0 } : (0, physics3d_shared_js_1.scaleVec3)(d, 1 / length);
243
+ const raxn = (0, physics3d_shared_js_1.crossVec3)(rA, n);
244
+ const rbxn = (0, physics3d_shared_js_1.crossVec3)(rB, n);
245
+ const imA = ctx.invMass[aIndex];
246
+ const imB = ctx.invMass[bIndex];
247
+ const linear = n.x * n.x * (imA.x + imB.x) + n.y * n.y * (imA.y + imB.y) + n.z * n.z * (imA.z + imB.z);
248
+ const denom = linear + (0, physics3d_shared_js_1.dotVec3)(raxn, (0, physics3d_shared_js_1.mulMat3Vec)(ctx.invInertia[aIndex], raxn)) + (0, physics3d_shared_js_1.dotVec3)(rbxn, (0, physics3d_shared_js_1.mulMat3Vec)(ctx.invInertia[bIndex], rbxn));
249
+ /* istanbul ignore next -- a distance joint with a real dynamic body has non-zero effective mass; the zero guard is defensive. */
250
+ mass = denom === 0 ? 0 : 1 / denom;
251
+ state.axialImpulse = warm?.axialImpulse ?? 0;
252
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(n, state.axialImpulse));
253
+ },
254
+ iterate(ctx) {
255
+ const A = ctx.bodies[aIndex];
256
+ const B = ctx.bodies[bIndex];
257
+ const relVel = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.v, (0, physics3d_shared_js_1.crossVec3)(B.w, rB)), (0, physics3d_shared_js_1.addVec3)(A.v, (0, physics3d_shared_js_1.crossVec3)(A.w, rA)));
258
+ const vn = (0, physics3d_shared_js_1.dotVec3)(relVel, n);
259
+ const overMax = length - joint.maxLength;
260
+ const underMin = joint.minLength !== undefined ? joint.minLength - length : -Infinity;
261
+ if (overMax > 0) {
262
+ const targetBias = (JOINT_BAUMGARTE / ctx.dt) * overMax;
263
+ let dLambda = -mass * (vn + targetBias);
264
+ const newAcc = Math.min(0, state.axialImpulse + dLambda);
265
+ dLambda = newAcc - state.axialImpulse;
266
+ state.axialImpulse = newAcc;
267
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(n, dLambda));
268
+ }
269
+ else if (underMin > 0) {
270
+ const targetBias = (JOINT_BAUMGARTE / ctx.dt) * underMin;
271
+ let dLambda = mass * (-vn + targetBias);
272
+ const newAcc = Math.max(0, state.axialImpulse + dLambda);
273
+ dLambda = newAcc - state.axialImpulse;
274
+ state.axialImpulse = newAcc;
275
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(n, dLambda));
276
+ }
277
+ else if (state.axialImpulse !== 0) {
278
+ // rope is slack — remove the warm-started pull applied in prepare(),
279
+ // otherwise it keeps pulling for one frame after going limp.
280
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(n, -state.axialImpulse));
281
+ state.axialImpulse = 0;
282
+ }
283
+ },
284
+ emitCache() {
285
+ const reaction2 = state.axialImpulse * state.axialImpulse;
286
+ const broken = joint.breakImpulse !== undefined && reaction2 > joint.breakImpulse * joint.breakImpulse;
287
+ return {
288
+ jointId: joint.id, impulseX: 0, impulseY: 0, impulseZ: 0, axialImpulse: state.axialImpulse, hingeAngle: 0,
289
+ ...(broken ? { broken: true } : {}),
290
+ };
291
+ },
292
+ };
293
+ }
294
+ function prismaticJoint(joint, aIndex, bIndex, warm) {
295
+ const state = {
296
+ impulse: warm ? { x: warm.impulseX, y: warm.impulseY, z: warm.impulseZ } : { x: 0, y: 0, z: 0 },
297
+ axialImpulse: warm?.axialImpulse ?? 0,
298
+ hingeAngle: 0,
299
+ };
300
+ let rA = { x: 0, y: 0, z: 0 };
301
+ let rB = { x: 0, y: 0, z: 0 };
302
+ let axis = { x: 1, y: 0, z: 0 };
303
+ let t1 = { x: 0, y: 1, z: 0 };
304
+ let t2 = { x: 0, y: 0, z: 1 };
305
+ let angKInv = [0, 0, 0, 0, 0, 0, 0, 0, 0];
306
+ let massT1 = 0;
307
+ let massT2 = 0;
308
+ let massAxial = 0;
309
+ let translation = 0;
310
+ let biasT1 = 0;
311
+ let biasT2 = 0;
312
+ let limitImpulse = 0;
313
+ const rowMass = (ctx, t) => {
314
+ const raxn = (0, physics3d_shared_js_1.crossVec3)(rA, t);
315
+ const rbxn = (0, physics3d_shared_js_1.crossVec3)(rB, t);
316
+ const imA = ctx.invMass[aIndex];
317
+ const imB = ctx.invMass[bIndex];
318
+ const linear = t.x * t.x * (imA.x + imB.x) + t.y * t.y * (imA.y + imB.y) + t.z * t.z * (imA.z + imB.z);
319
+ const denom = linear + (0, physics3d_shared_js_1.dotVec3)(raxn, (0, physics3d_shared_js_1.mulMat3Vec)(ctx.invInertia[aIndex], raxn)) + (0, physics3d_shared_js_1.dotVec3)(rbxn, (0, physics3d_shared_js_1.mulMat3Vec)(ctx.invInertia[bIndex], rbxn));
320
+ /* istanbul ignore next -- a prismatic joint with a real dynamic body has non-zero row mass; the zero guard is defensive. */
321
+ return denom === 0 ? 0 : 1 / denom;
322
+ };
323
+ return {
324
+ bodyIndices: [aIndex, bIndex],
325
+ prepare(ctx) {
326
+ const A = ctx.bodies[aIndex];
327
+ const B = ctx.bodies[bIndex];
328
+ rA = (0, physics3d_shared_js_1.rotatePointByQuat)(A.q, joint.anchorA);
329
+ rB = (0, physics3d_shared_js_1.rotatePointByQuat)(B.q, joint.anchorB);
330
+ const c = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.p, rB), (0, physics3d_shared_js_1.addVec3)(A.p, rA));
331
+ const rawAxis = (0, physics3d_shared_js_1.rotatePointByQuat)(A.q, joint.axisA);
332
+ const len = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(rawAxis, rawAxis));
333
+ /* istanbul ignore next -- axisA is validated non-zero and rotation preserves length, so len is never zero. */
334
+ axis = len === 0 ? { x: 1, y: 0, z: 0 } : (0, physics3d_shared_js_1.scaleVec3)(rawAxis, 1 / len);
335
+ t1 = Math.abs(axis.x) >= 0.57735 ? unit({ x: axis.y, y: -axis.x, z: 0 }) : unit({ x: 0, y: axis.z, z: -axis.y });
336
+ t2 = (0, physics3d_shared_js_1.crossVec3)(axis, t1);
337
+ translation = (0, physics3d_shared_js_1.dotVec3)(c, axis);
338
+ massT1 = rowMass(ctx, t1);
339
+ massT2 = rowMass(ctx, t2);
340
+ massAxial = rowMass(ctx, axis);
341
+ angKInv = (0, physics3d_shared_js_1.invertMat3)((0, physics3d_shared_js_1.mat3Sub)((0, physics3d_shared_js_1.mat3Sub)((0, physics3d_shared_js_1.mat3FromDiag)({ x: 0, y: 0, z: 0 }), scaleMat3(ctx.invInertia[aIndex], -1)), scaleMat3(ctx.invInertia[bIndex], -1)));
342
+ biasT1 = (-JOINT_BAUMGARTE / ctx.dt) * (0, physics3d_shared_js_1.dotVec3)(c, t1);
343
+ biasT2 = (-JOINT_BAUMGARTE / ctx.dt) * (0, physics3d_shared_js_1.dotVec3)(c, t2);
344
+ limitImpulse = 0;
345
+ ctx.apply(aIndex, bIndex, rA, rB, state.impulse);
346
+ if (state.axialImpulse !== 0) {
347
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(axis, state.axialImpulse));
348
+ }
349
+ },
350
+ iterate(ctx) {
351
+ const A = ctx.bodies[aIndex];
352
+ const B = ctx.bodies[bIndex];
353
+ // perpendicular rows: kill anchor drift off the slide axis.
354
+ // Sequential-impulse discipline: re-read the relative velocity after each
355
+ // applied row (one stale snapshot for both rows under-corrects — same GS
356
+ // lesson as the vehicle tire solve).
357
+ for (const [t, mass, biasT] of [[t1, massT1, biasT1], [t2, massT2, biasT2]]) {
358
+ const relVel = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.v, (0, physics3d_shared_js_1.crossVec3)(B.w, rB)), (0, physics3d_shared_js_1.addVec3)(A.v, (0, physics3d_shared_js_1.crossVec3)(A.w, rA)));
359
+ const vn = (0, physics3d_shared_js_1.dotVec3)(relVel, t);
360
+ const dLambda = -mass * vn;
361
+ state.impulse = (0, physics3d_shared_js_1.addVec3)(state.impulse, (0, physics3d_shared_js_1.scaleVec3)(t, dLambda));
362
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(t, dLambda));
363
+ const relBias = ctx.biasRelativeVelocity(aIndex, bIndex, rA, rB);
364
+ ctx.applyBias(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(t, mass * (biasT - (0, physics3d_shared_js_1.dotVec3)(relBias, t))));
365
+ }
366
+ // full angular lock (weld-style)
367
+ const relOmega = (0, physics3d_shared_js_1.subVec3)(B.w, A.w);
368
+ ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.mulMat3Vec)(angKInv, relOmega), -1));
369
+ // linear motor along the axis
370
+ if (joint.motorSpeed !== undefined) {
371
+ const maxForce = joint.maxMotorForce ?? 0;
372
+ const relAfterLock = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.v, (0, physics3d_shared_js_1.crossVec3)(B.w, rB)), (0, physics3d_shared_js_1.addVec3)(A.v, (0, physics3d_shared_js_1.crossVec3)(A.w, rA)));
373
+ const relAxial = (0, physics3d_shared_js_1.dotVec3)(relAfterLock, axis);
374
+ let dImpulse = massAxial * (joint.motorSpeed - relAxial);
375
+ const maxImpulse = maxForce * ctx.dt;
376
+ const newAcc = (0, physics3d_shared_js_1.clampNumber)(state.axialImpulse + dImpulse, -maxImpulse, maxImpulse);
377
+ dImpulse = newAcc - state.axialImpulse;
378
+ state.axialImpulse = newAcc;
379
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(axis, dImpulse));
380
+ }
381
+ // translation limits (inequality, biased by overshoot — mirrors distance)
382
+ const relNow = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.v, (0, physics3d_shared_js_1.crossVec3)(B.w, rB)), (0, physics3d_shared_js_1.addVec3)(A.v, (0, physics3d_shared_js_1.crossVec3)(A.w, rA)));
383
+ const vnAxial = (0, physics3d_shared_js_1.dotVec3)(relNow, axis);
384
+ if (joint.maxTranslation !== undefined && translation > joint.maxTranslation) {
385
+ const overshoot = translation - joint.maxTranslation;
386
+ let dLambda = -massAxial * (vnAxial + (JOINT_BAUMGARTE / ctx.dt) * overshoot);
387
+ const newAcc = Math.min(0, limitImpulse + dLambda);
388
+ dLambda = newAcc - limitImpulse;
389
+ limitImpulse = newAcc;
390
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(axis, dLambda));
391
+ }
392
+ else if (joint.minTranslation !== undefined && translation < joint.minTranslation) {
393
+ const overshoot = joint.minTranslation - translation;
394
+ let dLambda = massAxial * (-vnAxial + (JOINT_BAUMGARTE / ctx.dt) * overshoot);
395
+ const newAcc = Math.max(0, limitImpulse + dLambda);
396
+ dLambda = newAcc - limitImpulse;
397
+ limitImpulse = newAcc;
398
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(axis, dLambda));
399
+ }
400
+ },
401
+ emitCache() {
402
+ // perpendicular reaction only — the axialImpulse is the motor accumulator,
403
+ // and motors/limits are excluded from breaking.
404
+ const reaction2 = (0, physics3d_shared_js_1.dotVec3)(state.impulse, state.impulse);
405
+ const broken = joint.breakImpulse !== undefined && reaction2 > joint.breakImpulse * joint.breakImpulse;
406
+ return {
407
+ jointId: joint.id,
408
+ impulseX: state.impulse.x,
409
+ impulseY: state.impulse.y,
410
+ impulseZ: state.impulse.z,
411
+ axialImpulse: state.axialImpulse,
412
+ hingeAngle: 0,
413
+ ...(broken ? { broken: true } : {}),
414
+ };
415
+ },
416
+ };
417
+ }
418
+ function springJoint(joint, aIndex, bIndex, warm) {
419
+ const state = {
420
+ impulse: { x: 0, y: 0, z: 0 },
421
+ axialImpulse: warm?.axialImpulse ?? 0,
422
+ hingeAngle: 0,
423
+ };
424
+ const omega = 6.283185307179586 * joint.frequencyHz;
425
+ let rA = { x: 0, y: 0, z: 0 };
426
+ let rB = { x: 0, y: 0, z: 0 };
427
+ let n = { x: 0, y: 1, z: 0 };
428
+ let mass = 0;
429
+ let length = 0;
430
+ let gamma = 0;
431
+ let bias = 0;
432
+ let massSoft = 0;
433
+ return {
434
+ bodyIndices: [aIndex, bIndex],
435
+ prepare(ctx) {
436
+ const A = ctx.bodies[aIndex];
437
+ const B = ctx.bodies[bIndex];
438
+ rA = (0, physics3d_shared_js_1.rotatePointByQuat)(A.q, joint.anchorA);
439
+ rB = (0, physics3d_shared_js_1.rotatePointByQuat)(B.q, joint.anchorB);
440
+ const d = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.p, rB), (0, physics3d_shared_js_1.addVec3)(A.p, rA));
441
+ length = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(d, d));
442
+ n = length === 0 ? { x: 0, y: 1, z: 0 } : (0, physics3d_shared_js_1.scaleVec3)(d, 1 / length);
443
+ const raxn = (0, physics3d_shared_js_1.crossVec3)(rA, n);
444
+ const rbxn = (0, physics3d_shared_js_1.crossVec3)(rB, n);
445
+ const imA = ctx.invMass[aIndex];
446
+ const imB = ctx.invMass[bIndex];
447
+ const linear = n.x * n.x * (imA.x + imB.x) + n.y * n.y * (imA.y + imB.y) + n.z * n.z * (imA.z + imB.z);
448
+ const denom = linear + (0, physics3d_shared_js_1.dotVec3)(raxn, (0, physics3d_shared_js_1.mulMat3Vec)(ctx.invInertia[aIndex], raxn)) + (0, physics3d_shared_js_1.dotVec3)(rbxn, (0, physics3d_shared_js_1.mulMat3Vec)(ctx.invInertia[bIndex], rbxn));
449
+ mass = denom === 0 ? 0 : 1 / denom;
450
+ // Box2D soft-constraint coefficients from frequency + damping ratio.
451
+ const c = 2 * mass * joint.dampingRatio * omega;
452
+ const k = mass * omega * omega;
453
+ const softDenom = c + ctx.dt * k;
454
+ gamma = softDenom === 0 ? 0 : 1 / (ctx.dt * softDenom);
455
+ const beta = ctx.dt * k * gamma;
456
+ bias = (beta / ctx.dt) * (length - joint.restLength);
457
+ massSoft = mass === 0 ? 0 : 1 / (1 / mass + gamma);
458
+ state.axialImpulse = warm?.axialImpulse ?? 0;
459
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(n, state.axialImpulse));
460
+ },
461
+ iterate(ctx) {
462
+ const A = ctx.bodies[aIndex];
463
+ const B = ctx.bodies[bIndex];
464
+ const relVel = (0, physics3d_shared_js_1.subVec3)((0, physics3d_shared_js_1.addVec3)(B.v, (0, physics3d_shared_js_1.crossVec3)(B.w, rB)), (0, physics3d_shared_js_1.addVec3)(A.v, (0, physics3d_shared_js_1.crossVec3)(A.w, rA)));
465
+ const cdot = (0, physics3d_shared_js_1.dotVec3)(relVel, n);
466
+ const dLambda = -massSoft * (cdot + bias + gamma * state.axialImpulse);
467
+ state.axialImpulse += dLambda;
468
+ ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(n, dLambda));
469
+ },
470
+ emitCache() {
471
+ return { jointId: joint.id, impulseX: 0, impulseY: 0, impulseZ: 0, axialImpulse: state.axialImpulse, hingeAngle: 0 };
472
+ },
473
+ };
474
+ }
475
+ function grabJoint(joint, bodyIndex, warm) {
476
+ const state = {
477
+ impulse: warm ? { x: warm.impulseX, y: warm.impulseY, z: warm.impulseZ } : { x: 0, y: 0, z: 0 },
478
+ axialImpulse: 0,
479
+ hingeAngle: 0,
480
+ };
481
+ let r = { x: 0, y: 0, z: 0 };
482
+ let kInv = [0, 0, 0, 0, 0, 0, 0, 0, 0];
483
+ let desiredVel = { x: 0, y: 0, z: 0 };
484
+ const target = { x: joint.targetX, y: joint.targetY, z: joint.targetZ };
485
+ const maxImpulse = joint.maxForce * 1;
486
+ return {
487
+ bodyIndices: [bodyIndex],
488
+ prepare(ctx) {
489
+ const body = ctx.bodies[bodyIndex];
490
+ r = (0, physics3d_shared_js_1.rotatePointByQuat)(body.q, joint.anchor);
491
+ const worldAnchor = (0, physics3d_shared_js_1.addVec3)(body.p, r);
492
+ const imB = ctx.invMass[bodyIndex];
493
+ const linear = (0, physics3d_shared_js_1.mat3FromDiag)(imB);
494
+ const skewR = (0, physics3d_shared_js_1.skewMat3)(r);
495
+ const ang = (0, physics3d_shared_js_1.mat3Multiply)((0, physics3d_shared_js_1.mat3Multiply)(skewR, ctx.invInertia[bodyIndex]), skewR);
496
+ kInv = (0, physics3d_shared_js_1.invertMat3)((0, physics3d_shared_js_1.mat3Sub)(linear, ang));
497
+ const damping = joint.damping ?? 1;
498
+ desiredVel = (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.subVec3)(target, worldAnchor), damping / ctx.dt);
499
+ ctx.applyToBody(bodyIndex, r, state.impulse);
500
+ },
501
+ iterate(ctx) {
502
+ const body = ctx.bodies[bodyIndex];
503
+ const currentVel = (0, physics3d_shared_js_1.addVec3)(body.v, (0, physics3d_shared_js_1.crossVec3)(body.w, r));
504
+ const dImpulse = (0, physics3d_shared_js_1.mulMat3Vec)(kInv, (0, physics3d_shared_js_1.subVec3)(desiredVel, currentVel));
505
+ const target2 = (0, physics3d_shared_js_1.addVec3)(state.impulse, dImpulse);
506
+ const clamped = clampMagnitude(target2, maxImpulse * ctx.dt);
507
+ const applied = (0, physics3d_shared_js_1.subVec3)(clamped, state.impulse);
508
+ state.impulse = clamped;
509
+ ctx.applyToBody(bodyIndex, r, applied);
510
+ },
511
+ emitCache() {
512
+ return { jointId: joint.id, impulseX: state.impulse.x, impulseY: state.impulse.y, impulseZ: state.impulse.z, axialImpulse: 0, hingeAngle: 0 };
513
+ },
514
+ };
515
+ }
516
+ function clampMagnitude(v, maxLength) {
517
+ const lenSq = (0, physics3d_shared_js_1.dotVec3)(v, v);
518
+ const maxSq = maxLength * maxLength;
519
+ if (lenSq <= maxSq || lenSq === 0) {
520
+ return v;
521
+ }
522
+ const scale = maxLength / (0, physics3d_shared_js_1.solverSqrt)(lenSq);
523
+ return (0, physics3d_shared_js_1.scaleVec3)(v, scale);
524
+ }
525
+ function validateJoint(joint) {
526
+ if (joint.type === 'grab') {
527
+ assertFiniteVec(joint.anchor, 'anchor', joint.id);
528
+ if (!Number.isFinite(joint.maxForce) || joint.maxForce <= 0) {
529
+ throw new Error(`physics3d joint ${joint.id}: maxForce must be a positive finite number`);
530
+ }
531
+ if (!Number.isFinite(joint.targetX) || !Number.isFinite(joint.targetY) || !Number.isFinite(joint.targetZ)) {
532
+ throw new Error(`physics3d joint ${joint.id}: target must be finite`);
533
+ }
534
+ return;
535
+ }
536
+ assertFiniteVec(joint.anchorA, 'anchorA', joint.id);
537
+ assertFiniteVec(joint.anchorB, 'anchorB', joint.id);
538
+ if ('breakImpulse' in joint && joint.breakImpulse !== undefined && (!Number.isFinite(joint.breakImpulse) || joint.breakImpulse <= 0)) {
539
+ throw new Error(`physics3d joint ${joint.id}: breakImpulse must be positive`);
540
+ }
541
+ if (joint.type === 'hinge') {
542
+ if ((0, physics3d_shared_js_1.dotVec3)(joint.axisA, joint.axisA) === 0) {
543
+ throw new Error(`physics3d joint ${joint.id}: hinge axisA must be non-zero`);
544
+ }
545
+ if (joint.minAngle !== undefined && joint.maxAngle !== undefined && joint.minAngle > joint.maxAngle) {
546
+ throw new Error(`physics3d joint ${joint.id}: minAngle must not exceed maxAngle`);
547
+ }
548
+ if (joint.maxMotorTorque !== undefined && joint.maxMotorTorque <= 0) {
549
+ throw new Error(`physics3d joint ${joint.id}: maxMotorTorque must be positive`);
550
+ }
551
+ }
552
+ if (joint.type === 'distance') {
553
+ if (!Number.isFinite(joint.maxLength) || joint.maxLength <= 0) {
554
+ throw new Error(`physics3d joint ${joint.id}: maxLength must be a positive finite number`);
555
+ }
556
+ if (joint.minLength !== undefined && joint.minLength > joint.maxLength) {
557
+ throw new Error(`physics3d joint ${joint.id}: minLength must not exceed maxLength`);
558
+ }
559
+ }
560
+ if (joint.type === 'prismatic') {
561
+ assertFiniteVec(joint.axisA, 'axisA', joint.id);
562
+ if ((0, physics3d_shared_js_1.dotVec3)(joint.axisA, joint.axisA) === 0) {
563
+ throw new Error(`physics3d joint ${joint.id}: prismatic axisA must be non-zero`);
564
+ }
565
+ for (const [name, value] of [['minTranslation', joint.minTranslation], ['maxTranslation', joint.maxTranslation], ['motorSpeed', joint.motorSpeed]]) {
566
+ if (value !== undefined && !Number.isFinite(value)) {
567
+ throw new Error(`physics3d joint ${joint.id}: ${name} must be a finite number`);
568
+ }
569
+ }
570
+ if (joint.minTranslation !== undefined && joint.maxTranslation !== undefined && joint.minTranslation > joint.maxTranslation) {
571
+ throw new Error(`physics3d joint ${joint.id}: minTranslation must not exceed maxTranslation`);
572
+ }
573
+ if (joint.maxMotorForce !== undefined && (!Number.isFinite(joint.maxMotorForce) || joint.maxMotorForce <= 0)) {
574
+ throw new Error(`physics3d joint ${joint.id}: maxMotorForce must be positive`);
575
+ }
576
+ }
577
+ if (joint.type === 'spring') {
578
+ if (!Number.isFinite(joint.frequencyHz) || joint.frequencyHz <= 0) {
579
+ throw new Error(`physics3d joint ${joint.id}: frequencyHz must be a positive finite number`);
580
+ }
581
+ if (!Number.isFinite(joint.dampingRatio) || joint.dampingRatio < 0) {
582
+ throw new Error(`physics3d joint ${joint.id}: dampingRatio must be a non-negative finite number`);
583
+ }
584
+ if (!Number.isFinite(joint.restLength) || joint.restLength < 0) {
585
+ throw new Error(`physics3d joint ${joint.id}: restLength must be a non-negative finite number`);
586
+ }
587
+ }
588
+ }
589
+ function brokenJoint(jointId) {
590
+ return {
591
+ bodyIndices: [],
592
+ prepare() { },
593
+ iterate() { },
594
+ emitCache() {
595
+ return { jointId, impulseX: 0, impulseY: 0, impulseZ: 0, axialImpulse: 0, hingeAngle: 0, broken: true };
596
+ },
597
+ };
598
+ }
599
+ /** Build solver joint objects from joint definitions, seeded from the joint cache. */
600
+ function createSolverJoints(joints, indexById, jointCache) {
601
+ const cacheById = new Map(jointCache.map((entry) => [entry.jointId, entry]));
602
+ const seenIds = new Set();
603
+ return [...joints]
604
+ .sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0))
605
+ .map((joint) => {
606
+ if (seenIds.has(joint.id)) {
607
+ throw new Error(`physics3d joint ${joint.id}: duplicate joint id`);
608
+ }
609
+ seenIds.add(joint.id);
610
+ validateJoint(joint);
611
+ const warm = cacheById.get(joint.id);
612
+ if (warm?.broken === true) {
613
+ return brokenJoint(joint.id);
614
+ }
615
+ if (joint.type === 'grab') {
616
+ return grabJoint(joint, requireIndex(joint.body, joint.id, indexById), warm);
617
+ }
618
+ const aIndex = requireIndex(joint.a, joint.id, indexById);
619
+ const bIndex = requireIndex(joint.b, joint.id, indexById);
620
+ if (joint.type === 'ball') {
621
+ return ballJoint(joint, aIndex, bIndex, warm, false);
622
+ }
623
+ if (joint.type === 'weld') {
624
+ return ballJoint(joint, aIndex, bIndex, warm, true);
625
+ }
626
+ if (joint.type === 'hinge') {
627
+ return hingeJoint(joint, aIndex, bIndex, warm);
628
+ }
629
+ if (joint.type === 'spring') {
630
+ return springJoint(joint, aIndex, bIndex, warm);
631
+ }
632
+ if (joint.type === 'prismatic') {
633
+ return prismaticJoint(joint, aIndex, bIndex, warm);
634
+ }
635
+ return distanceJoint(joint, aIndex, bIndex, warm);
636
+ });
637
+ }