@series-inc/rundot-syncplay 5.23.0 → 5.24.0-beta.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.
- package/README.md +20 -0
- package/dist/browser.d.ts +4 -1
- package/dist/browser.js +1 -0
- package/dist/checksum-basis.d.ts +4 -0
- package/dist/checksum-basis.js +17 -0
- package/dist/cjs/browser.js +4 -2
- package/dist/cjs/checksum-basis.js +22 -0
- package/dist/cjs/collider-cooking.js +834 -0
- package/dist/cjs/deterministic-session.js +2 -0
- package/dist/cjs/engine-parity-matrix.js +32 -9
- package/dist/cjs/engine-parity.js +1 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/instant-replay.js +4 -9
- package/dist/cjs/movement3d.js +17 -1
- package/dist/cjs/networked-client.js +11 -9
- package/dist/cjs/node.js +5 -1
- package/dist/cjs/offline-session.js +52 -24
- package/dist/cjs/physics-cert.js +7 -2
- package/dist/cjs/physics3d-hull.js +381 -0
- package/dist/cjs/physics3d-joints.js +163 -31
- package/dist/cjs/physics3d-recipes.js +197 -0
- package/dist/cjs/physics3d-shared.js +6 -0
- package/dist/cjs/physics3d-solver.js +746 -89
- package/dist/cjs/physics3d-stacking.js +154 -0
- package/dist/cjs/physics3d-toi.js +251 -0
- package/dist/cjs/physics3d-vehicle.js +35 -10
- package/dist/cjs/physics3d.js +401 -20
- package/dist/cjs/runtime-support.js +71 -0
- package/dist/cjs/session-promotion.js +5 -5
- package/dist/cjs/synctest.js +49 -0
- package/dist/cjs/testing.js +4 -1
- package/dist/cli.js +163 -41
- package/dist/collider-cooking.d.ts +141 -0
- package/dist/collider-cooking.js +829 -0
- package/dist/deterministic-session.js +2 -0
- package/dist/engine-parity-matrix.js +32 -9
- package/dist/engine-parity.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/instant-replay.d.ts +2 -2
- package/dist/instant-replay.js +4 -9
- package/dist/movement3d.js +18 -2
- package/dist/networked-client.js +11 -9
- package/dist/node.d.ts +2 -0
- package/dist/node.js +2 -0
- package/dist/offline-session.js +53 -25
- package/dist/physics-cert.js +7 -2
- package/dist/physics3d-hull.d.ts +53 -0
- package/dist/physics3d-hull.js +376 -0
- package/dist/physics3d-joints.d.ts +7 -0
- package/dist/physics3d-joints.js +163 -31
- package/dist/physics3d-recipes.d.ts +29 -0
- package/dist/physics3d-recipes.js +193 -0
- package/dist/physics3d-shared.js +6 -0
- package/dist/physics3d-solver.d.ts +65 -1
- package/dist/physics3d-solver.js +744 -89
- package/dist/physics3d-stacking.d.ts +26 -0
- package/dist/physics3d-stacking.js +147 -0
- package/dist/physics3d-toi.d.ts +70 -0
- package/dist/physics3d-toi.js +245 -0
- package/dist/physics3d-vehicle.js +35 -10
- package/dist/physics3d.d.ts +20 -2
- package/dist/physics3d.js +400 -21
- package/dist/runtime-support.d.ts +60 -0
- package/dist/runtime-support.js +66 -0
- package/dist/session-promotion.js +5 -5
- package/dist/synctest.d.ts +1 -1
- package/dist/synctest.js +49 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +1 -0
- package/dist/types.d.ts +21 -2
- package/package.json +8 -3
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.createSolverJoints = createSolverJoints;
|
|
16
16
|
const physics3d_shared_js_1 = require("./physics3d-shared.js");
|
|
17
|
+
const math_js_1 = require("./math.js");
|
|
17
18
|
const JOINT_BAUMGARTE = 0.2;
|
|
19
|
+
const TWO_PI = 6.283185307179586;
|
|
20
|
+
// Module-level deterministic math for cone-twist cosine (vehicle precedent); the
|
|
21
|
+
// generator is pure and the tables are read-only, so no cross-frame state leaks.
|
|
22
|
+
const jointMath = (0, math_js_1.createDeterministicMath)();
|
|
18
23
|
function requireIndex(id, jointId, indexById) {
|
|
19
24
|
const index = indexById.get(id);
|
|
20
25
|
if (index === undefined) {
|
|
@@ -42,13 +47,37 @@ function ballJoint(joint, aIndex, bIndex, warm, weld) {
|
|
|
42
47
|
const state = {
|
|
43
48
|
impulse: warm ? { x: warm.impulseX, y: warm.impulseY, z: warm.impulseZ } : { x: 0, y: 0, z: 0 },
|
|
44
49
|
axialImpulse: 0,
|
|
45
|
-
hingeAngle: 0,
|
|
50
|
+
hingeAngle: warm?.hingeAngle ?? 0,
|
|
51
|
+
stepImpulse: { x: 0, y: 0, z: 0 },
|
|
52
|
+
stepAxial: 0,
|
|
46
53
|
};
|
|
54
|
+
// Cone-twist limits (ball only). coneCos is the cosine of the swing half-angle;
|
|
55
|
+
// twist limits are stored in radians. axisA/axisB are the local twist axes.
|
|
56
|
+
const coneTwist = !weld && joint.type === 'ball'
|
|
57
|
+
&& (joint.coneLimitTurns !== undefined || joint.twistMinTurns !== undefined || joint.twistMaxTurns !== undefined)
|
|
58
|
+
? {
|
|
59
|
+
localAxisA: joint.axisA,
|
|
60
|
+
localAxisB: (joint.axisB ?? joint.axisA),
|
|
61
|
+
coneCos: joint.coneLimitTurns !== undefined
|
|
62
|
+
? jointMath.cosTurns(Math.round(joint.coneLimitTurns * jointMath.fixedScale)) / jointMath.fixedScale
|
|
63
|
+
: undefined,
|
|
64
|
+
twistMin: joint.twistMinTurns !== undefined ? joint.twistMinTurns * TWO_PI : undefined,
|
|
65
|
+
twistMax: joint.twistMaxTurns !== undefined ? joint.twistMaxTurns * TWO_PI : undefined,
|
|
66
|
+
}
|
|
67
|
+
: undefined;
|
|
47
68
|
let rA = { x: 0, y: 0, z: 0 };
|
|
48
69
|
let rB = { x: 0, y: 0, z: 0 };
|
|
49
70
|
let kInv = [0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
50
71
|
let biasTarget = { x: 0, y: 0, z: 0 };
|
|
51
72
|
let angKInv = [0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
73
|
+
let twistAxis = { x: 0, y: 1, z: 0 };
|
|
74
|
+
let swingV = { x: 0, y: 1, z: 0 };
|
|
75
|
+
let coneImpulse = 0;
|
|
76
|
+
let twistImpulse = 0;
|
|
77
|
+
const angularMassAlong = (ctx, axis) => {
|
|
78
|
+
const denom = (0, physics3d_shared_js_1.dotVec3)(axis, (0, physics3d_shared_js_1.mulMat3Vec)(ctx.invInertia[aIndex], axis)) + (0, physics3d_shared_js_1.dotVec3)(axis, (0, physics3d_shared_js_1.mulMat3Vec)(ctx.invInertia[bIndex], axis));
|
|
79
|
+
return denom === 0 ? 0 : 1 / denom;
|
|
80
|
+
};
|
|
52
81
|
return {
|
|
53
82
|
bodyIndices: [aIndex, bIndex],
|
|
54
83
|
prepare(ctx) {
|
|
@@ -64,6 +93,12 @@ function ballJoint(joint, aIndex, bIndex, warm, weld) {
|
|
|
64
93
|
if (weld) {
|
|
65
94
|
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
95
|
}
|
|
96
|
+
if (coneTwist !== undefined) {
|
|
97
|
+
twistAxis = unit((0, physics3d_shared_js_1.rotatePointByQuat)(A.q, coneTwist.localAxisA));
|
|
98
|
+
swingV = unit((0, physics3d_shared_js_1.rotatePointByQuat)(B.q, coneTwist.localAxisB));
|
|
99
|
+
coneImpulse = 0;
|
|
100
|
+
twistImpulse = 0;
|
|
101
|
+
}
|
|
67
102
|
ctx.apply(aIndex, bIndex, rA, rB, state.impulse);
|
|
68
103
|
},
|
|
69
104
|
iterate(ctx) {
|
|
@@ -78,16 +113,63 @@ function ballJoint(joint, aIndex, bIndex, warm, weld) {
|
|
|
78
113
|
const angImpulse = (0, physics3d_shared_js_1.scaleVec3)((0, physics3d_shared_js_1.mulMat3Vec)(angKInv, relOmega), -1);
|
|
79
114
|
ctx.applyAngular(aIndex, bIndex, angImpulse);
|
|
80
115
|
}
|
|
116
|
+
if (coneTwist !== undefined) {
|
|
117
|
+
// Swing cone: one-sided limit on the angle between the two twist axes.
|
|
118
|
+
if (coneTwist.coneCos !== undefined) {
|
|
119
|
+
const cosAngle = (0, physics3d_shared_js_1.dotVec3)(twistAxis, swingV);
|
|
120
|
+
if (cosAngle < coneTwist.coneCos) {
|
|
121
|
+
const rawAxis = (0, physics3d_shared_js_1.crossVec3)(swingV, twistAxis);
|
|
122
|
+
const len = (0, physics3d_shared_js_1.solverSqrt)((0, physics3d_shared_js_1.dotVec3)(rawAxis, rawAxis));
|
|
123
|
+
if (len > 1e-9) {
|
|
124
|
+
const axis = (0, physics3d_shared_js_1.scaleVec3)(rawAxis, 1 / len);
|
|
125
|
+
const relW = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(B.w, A.w), axis);
|
|
126
|
+
const mass = angularMassAlong(ctx, axis);
|
|
127
|
+
let dCone = mass * (-relW + (JOINT_BAUMGARTE / ctx.dt) * (coneTwist.coneCos - cosAngle));
|
|
128
|
+
const newAcc = Math.max(0, coneImpulse + dCone);
|
|
129
|
+
dCone = newAcc - coneImpulse;
|
|
130
|
+
coneImpulse = newAcc;
|
|
131
|
+
ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)(axis, dCone));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Twist: min/max inequality rows about the shared twist axis (mirrors hinge).
|
|
136
|
+
if (coneTwist.twistMin !== undefined && state.hingeAngle <= coneTwist.twistMin) {
|
|
137
|
+
const overshoot = coneTwist.twistMin - state.hingeAngle;
|
|
138
|
+
const relW = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(B.w, A.w), twistAxis);
|
|
139
|
+
const mass = angularMassAlong(ctx, twistAxis);
|
|
140
|
+
let dTwist = mass * (-relW + (JOINT_BAUMGARTE / ctx.dt) * overshoot);
|
|
141
|
+
const newAcc = Math.max(0, twistImpulse + dTwist);
|
|
142
|
+
dTwist = newAcc - twistImpulse;
|
|
143
|
+
twistImpulse = newAcc;
|
|
144
|
+
ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)(twistAxis, dTwist));
|
|
145
|
+
}
|
|
146
|
+
else if (coneTwist.twistMax !== undefined && state.hingeAngle >= coneTwist.twistMax) {
|
|
147
|
+
const overshoot = state.hingeAngle - coneTwist.twistMax;
|
|
148
|
+
const relW = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(B.w, A.w), twistAxis);
|
|
149
|
+
const mass = angularMassAlong(ctx, twistAxis);
|
|
150
|
+
let dTwist = mass * (-relW - (JOINT_BAUMGARTE / ctx.dt) * overshoot);
|
|
151
|
+
const newAcc = Math.min(0, twistImpulse + dTwist);
|
|
152
|
+
dTwist = newAcc - twistImpulse;
|
|
153
|
+
twistImpulse = newAcc;
|
|
154
|
+
ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)(twistAxis, dTwist));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
81
157
|
// position correction on pseudo-velocities
|
|
82
158
|
const relBias = ctx.biasRelativeVelocity(aIndex, bIndex, rA, rB);
|
|
83
159
|
const dBias = (0, physics3d_shared_js_1.mulMat3Vec)(kInv, (0, physics3d_shared_js_1.subVec3)(biasTarget, relBias));
|
|
84
160
|
ctx.applyBias(aIndex, bIndex, rA, rB, dBias);
|
|
85
161
|
},
|
|
162
|
+
postSubstep(ctx) {
|
|
163
|
+
state.stepImpulse = (0, physics3d_shared_js_1.addVec3)(state.stepImpulse, state.impulse);
|
|
164
|
+
if (coneTwist !== undefined) {
|
|
165
|
+
state.hingeAngle += (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(ctx.bodies[bIndex].w, ctx.bodies[aIndex].w), twistAxis) * ctx.dt;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
86
168
|
emitCache() {
|
|
87
|
-
const reaction2 = (0, physics3d_shared_js_1.dotVec3)(state.
|
|
169
|
+
const reaction2 = (0, physics3d_shared_js_1.dotVec3)(state.stepImpulse, state.stepImpulse);
|
|
88
170
|
const broken = joint.breakImpulse !== undefined && reaction2 > joint.breakImpulse * joint.breakImpulse;
|
|
89
171
|
return {
|
|
90
|
-
jointId: joint.id, impulseX: state.impulse.x, impulseY: state.impulse.y, impulseZ: state.impulse.z, axialImpulse: 0, hingeAngle: 0,
|
|
172
|
+
jointId: joint.id, impulseX: state.impulse.x, impulseY: state.impulse.y, impulseZ: state.impulse.z, axialImpulse: 0, hingeAngle: coneTwist !== undefined ? state.hingeAngle : 0,
|
|
91
173
|
...(broken ? { broken: true } : {}),
|
|
92
174
|
};
|
|
93
175
|
},
|
|
@@ -101,6 +183,8 @@ function hingeJoint(joint, aIndex, bIndex, warm) {
|
|
|
101
183
|
impulse: warm ? { x: warm.impulseX, y: warm.impulseY, z: warm.impulseZ } : { x: 0, y: 0, z: 0 },
|
|
102
184
|
axialImpulse: warm?.axialImpulse ?? 0,
|
|
103
185
|
hingeAngle: warm?.hingeAngle ?? 0,
|
|
186
|
+
stepImpulse: { x: 0, y: 0, z: 0 },
|
|
187
|
+
stepAxial: 0,
|
|
104
188
|
};
|
|
105
189
|
let rA = { x: 0, y: 0, z: 0 };
|
|
106
190
|
let rB = { x: 0, y: 0, z: 0 };
|
|
@@ -113,7 +197,6 @@ function hingeJoint(joint, aIndex, bIndex, warm) {
|
|
|
113
197
|
let angMassT1 = 0;
|
|
114
198
|
let angMassT2 = 0;
|
|
115
199
|
let limitImpulse = 0;
|
|
116
|
-
let ctxRef;
|
|
117
200
|
const angularMass = (ctx, t) => {
|
|
118
201
|
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
202
|
/* istanbul ignore next -- a hinge with a real dynamic body has non-zero inverse inertia along the axis; the zero guard is defensive. */
|
|
@@ -139,7 +222,6 @@ function hingeJoint(joint, aIndex, bIndex, warm) {
|
|
|
139
222
|
angMassT1 = angularMass(ctx, t1);
|
|
140
223
|
angMassT2 = angularMass(ctx, t2);
|
|
141
224
|
limitImpulse = 0;
|
|
142
|
-
ctxRef = ctx;
|
|
143
225
|
ctx.apply(aIndex, bIndex, rA, rB, state.impulse);
|
|
144
226
|
if (state.axialImpulse !== 0) {
|
|
145
227
|
ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)(axis, state.axialImpulse));
|
|
@@ -193,14 +275,14 @@ function hingeJoint(joint, aIndex, bIndex, warm) {
|
|
|
193
275
|
ctx.applyAngular(aIndex, bIndex, (0, physics3d_shared_js_1.scaleVec3)(axis, dImpulse));
|
|
194
276
|
}
|
|
195
277
|
},
|
|
196
|
-
|
|
278
|
+
postSubstep(ctx) {
|
|
197
279
|
// integrate the hinge angle from the post-solve relative axial velocity
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const reaction2 = (0, physics3d_shared_js_1.dotVec3)(state.
|
|
280
|
+
const relAxial = (0, physics3d_shared_js_1.dotVec3)((0, physics3d_shared_js_1.subVec3)(ctx.bodies[bIndex].w, ctx.bodies[aIndex].w), axis);
|
|
281
|
+
state.hingeAngle += relAxial * ctx.dt;
|
|
282
|
+
state.stepImpulse = (0, physics3d_shared_js_1.addVec3)(state.stepImpulse, state.impulse);
|
|
283
|
+
},
|
|
284
|
+
emitCache() {
|
|
285
|
+
const reaction2 = (0, physics3d_shared_js_1.dotVec3)(state.stepImpulse, state.stepImpulse);
|
|
204
286
|
const broken = joint.breakImpulse !== undefined && reaction2 > joint.breakImpulse * joint.breakImpulse;
|
|
205
287
|
return {
|
|
206
288
|
jointId: joint.id,
|
|
@@ -224,6 +306,8 @@ function distanceJoint(joint, aIndex, bIndex, warm) {
|
|
|
224
306
|
impulse: { x: warm?.axialImpulse ?? 0, y: 0, z: 0 },
|
|
225
307
|
axialImpulse: warm?.axialImpulse ?? 0,
|
|
226
308
|
hingeAngle: 0,
|
|
309
|
+
stepImpulse: { x: 0, y: 0, z: 0 },
|
|
310
|
+
stepAxial: 0,
|
|
227
311
|
};
|
|
228
312
|
let rA = { x: 0, y: 0, z: 0 };
|
|
229
313
|
let rB = { x: 0, y: 0, z: 0 };
|
|
@@ -248,7 +332,10 @@ function distanceJoint(joint, aIndex, bIndex, warm) {
|
|
|
248
332
|
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
333
|
/* istanbul ignore next -- a distance joint with a real dynamic body has non-zero effective mass; the zero guard is defensive. */
|
|
250
334
|
mass = denom === 0 ? 0 : 1 / denom;
|
|
251
|
-
|
|
335
|
+
// Warm-start the persisted axial impulse (it accumulates across substeps and
|
|
336
|
+
// is zeroed by iterate when the rope goes slack). Re-reading the frame's warm
|
|
337
|
+
// value here would drop intra-step buildup and undo the slack-rope zeroing on
|
|
338
|
+
// later substeps — mirror the spring joint, which persists axialImpulse too.
|
|
252
339
|
ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(n, state.axialImpulse));
|
|
253
340
|
},
|
|
254
341
|
iterate(ctx) {
|
|
@@ -281,8 +368,11 @@ function distanceJoint(joint, aIndex, bIndex, warm) {
|
|
|
281
368
|
state.axialImpulse = 0;
|
|
282
369
|
}
|
|
283
370
|
},
|
|
371
|
+
postSubstep() {
|
|
372
|
+
state.stepAxial += state.axialImpulse;
|
|
373
|
+
},
|
|
284
374
|
emitCache() {
|
|
285
|
-
const reaction2 = state.
|
|
375
|
+
const reaction2 = state.stepAxial * state.stepAxial;
|
|
286
376
|
const broken = joint.breakImpulse !== undefined && reaction2 > joint.breakImpulse * joint.breakImpulse;
|
|
287
377
|
return {
|
|
288
378
|
jointId: joint.id, impulseX: 0, impulseY: 0, impulseZ: 0, axialImpulse: state.axialImpulse, hingeAngle: 0,
|
|
@@ -296,6 +386,8 @@ function prismaticJoint(joint, aIndex, bIndex, warm) {
|
|
|
296
386
|
impulse: warm ? { x: warm.impulseX, y: warm.impulseY, z: warm.impulseZ } : { x: 0, y: 0, z: 0 },
|
|
297
387
|
axialImpulse: warm?.axialImpulse ?? 0,
|
|
298
388
|
hingeAngle: 0,
|
|
389
|
+
stepImpulse: { x: 0, y: 0, z: 0 },
|
|
390
|
+
stepAxial: 0,
|
|
299
391
|
};
|
|
300
392
|
let rA = { x: 0, y: 0, z: 0 };
|
|
301
393
|
let rB = { x: 0, y: 0, z: 0 };
|
|
@@ -398,10 +490,13 @@ function prismaticJoint(joint, aIndex, bIndex, warm) {
|
|
|
398
490
|
ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(axis, dLambda));
|
|
399
491
|
}
|
|
400
492
|
},
|
|
493
|
+
postSubstep() {
|
|
494
|
+
state.stepImpulse = (0, physics3d_shared_js_1.addVec3)(state.stepImpulse, state.impulse);
|
|
495
|
+
},
|
|
401
496
|
emitCache() {
|
|
402
497
|
// perpendicular reaction only — the axialImpulse is the motor accumulator,
|
|
403
498
|
// and motors/limits are excluded from breaking.
|
|
404
|
-
const reaction2 = (0, physics3d_shared_js_1.dotVec3)(state.
|
|
499
|
+
const reaction2 = (0, physics3d_shared_js_1.dotVec3)(state.stepImpulse, state.stepImpulse);
|
|
405
500
|
const broken = joint.breakImpulse !== undefined && reaction2 > joint.breakImpulse * joint.breakImpulse;
|
|
406
501
|
return {
|
|
407
502
|
jointId: joint.id,
|
|
@@ -420,16 +515,18 @@ function springJoint(joint, aIndex, bIndex, warm) {
|
|
|
420
515
|
impulse: { x: 0, y: 0, z: 0 },
|
|
421
516
|
axialImpulse: warm?.axialImpulse ?? 0,
|
|
422
517
|
hingeAngle: 0,
|
|
518
|
+
stepImpulse: { x: 0, y: 0, z: 0 },
|
|
519
|
+
stepAxial: 0,
|
|
423
520
|
};
|
|
424
521
|
const omega = 6.283185307179586 * joint.frequencyHz;
|
|
425
522
|
let rA = { x: 0, y: 0, z: 0 };
|
|
426
523
|
let rB = { x: 0, y: 0, z: 0 };
|
|
427
524
|
let n = { x: 0, y: 1, z: 0 };
|
|
428
|
-
let
|
|
525
|
+
let massEff = 0;
|
|
429
526
|
let length = 0;
|
|
430
|
-
let
|
|
431
|
-
let
|
|
432
|
-
let
|
|
527
|
+
let biasRate = 0;
|
|
528
|
+
let massScale = 1;
|
|
529
|
+
let impulseScale = 0;
|
|
433
530
|
return {
|
|
434
531
|
bodyIndices: [aIndex, bIndex],
|
|
435
532
|
prepare(ctx) {
|
|
@@ -446,16 +543,19 @@ function springJoint(joint, aIndex, bIndex, warm) {
|
|
|
446
543
|
const imB = ctx.invMass[bIndex];
|
|
447
544
|
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
545
|
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
|
-
|
|
450
|
-
// Box2D soft-constraint coefficients
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
546
|
+
massEff = denom === 0 ? 0 : 1 / denom;
|
|
547
|
+
// Box2D-v3 soft-constraint coefficients: substep-invariant (biasRate stays
|
|
548
|
+
// bounded as h -> 0), unlike the gamma/beta form whose 1/dt bias diverges
|
|
549
|
+
// under sub-stepping. hertz = frequencyHz, zeta = dampingRatio, h = ctx.dt.
|
|
550
|
+
const h = ctx.dt;
|
|
551
|
+
const a1 = 2 * joint.dampingRatio + omega * h;
|
|
552
|
+
const a2 = h * omega * a1;
|
|
553
|
+
const a3 = 1 / (1 + a2);
|
|
554
|
+
/* istanbul ignore next -- frequencyHz > 0 is validated and dampingRatio >= 0, so a1 = 2*zeta + omega*h is always positive. */
|
|
555
|
+
biasRate = a1 === 0 ? 0 : omega / a1;
|
|
556
|
+
massScale = a2 * a3;
|
|
557
|
+
impulseScale = a3;
|
|
558
|
+
// warm start: axialImpulse persists across substeps (per-substep magnitude).
|
|
459
559
|
ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(n, state.axialImpulse));
|
|
460
560
|
},
|
|
461
561
|
iterate(ctx) {
|
|
@@ -463,12 +563,20 @@ function springJoint(joint, aIndex, bIndex, warm) {
|
|
|
463
563
|
const B = ctx.bodies[bIndex];
|
|
464
564
|
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
565
|
const cdot = (0, physics3d_shared_js_1.dotVec3)(relVel, n);
|
|
466
|
-
const dLambda = -
|
|
566
|
+
const dLambda = -massScale * massEff * (cdot + biasRate * (length - joint.restLength)) - impulseScale * state.axialImpulse;
|
|
467
567
|
state.axialImpulse += dLambda;
|
|
468
568
|
ctx.apply(aIndex, bIndex, rA, rB, (0, physics3d_shared_js_1.scaleVec3)(n, dLambda));
|
|
469
569
|
},
|
|
570
|
+
postSubstep() {
|
|
571
|
+
state.stepAxial += state.axialImpulse;
|
|
572
|
+
},
|
|
470
573
|
emitCache() {
|
|
471
|
-
|
|
574
|
+
const reaction2 = state.stepAxial * state.stepAxial;
|
|
575
|
+
const broken = joint.breakImpulse !== undefined && reaction2 > joint.breakImpulse * joint.breakImpulse;
|
|
576
|
+
return {
|
|
577
|
+
jointId: joint.id, impulseX: 0, impulseY: 0, impulseZ: 0, axialImpulse: state.axialImpulse, hingeAngle: 0,
|
|
578
|
+
...(broken ? { broken: true } : {}),
|
|
579
|
+
};
|
|
472
580
|
},
|
|
473
581
|
};
|
|
474
582
|
}
|
|
@@ -477,6 +585,8 @@ function grabJoint(joint, bodyIndex, warm) {
|
|
|
477
585
|
impulse: warm ? { x: warm.impulseX, y: warm.impulseY, z: warm.impulseZ } : { x: 0, y: 0, z: 0 },
|
|
478
586
|
axialImpulse: 0,
|
|
479
587
|
hingeAngle: 0,
|
|
588
|
+
stepImpulse: { x: 0, y: 0, z: 0 },
|
|
589
|
+
stepAxial: 0,
|
|
480
590
|
};
|
|
481
591
|
let r = { x: 0, y: 0, z: 0 };
|
|
482
592
|
let kInv = [0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
@@ -538,6 +648,28 @@ function validateJoint(joint) {
|
|
|
538
648
|
if ('breakImpulse' in joint && joint.breakImpulse !== undefined && (!Number.isFinite(joint.breakImpulse) || joint.breakImpulse <= 0)) {
|
|
539
649
|
throw new Error(`physics3d joint ${joint.id}: breakImpulse must be positive`);
|
|
540
650
|
}
|
|
651
|
+
if (joint.type === 'ball') {
|
|
652
|
+
const hasLimit = joint.coneLimitTurns !== undefined || joint.twistMinTurns !== undefined || joint.twistMaxTurns !== undefined;
|
|
653
|
+
if (hasLimit) {
|
|
654
|
+
if (joint.axisA === undefined || !Number.isFinite(joint.axisA.x) || !Number.isFinite(joint.axisA.y) || !Number.isFinite(joint.axisA.z) || (0, physics3d_shared_js_1.dotVec3)(joint.axisA, joint.axisA) === 0) {
|
|
655
|
+
throw new Error(`physics3d joint ${joint.id}: cone-twist limits require a non-zero finite axisA`);
|
|
656
|
+
}
|
|
657
|
+
if (joint.axisB !== undefined && (!Number.isFinite(joint.axisB.x) || !Number.isFinite(joint.axisB.y) || !Number.isFinite(joint.axisB.z) || (0, physics3d_shared_js_1.dotVec3)(joint.axisB, joint.axisB) === 0)) {
|
|
658
|
+
throw new Error(`physics3d joint ${joint.id}: cone-twist axisB must be non-zero finite`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (joint.coneLimitTurns !== undefined && (!Number.isFinite(joint.coneLimitTurns) || joint.coneLimitTurns <= 0 || joint.coneLimitTurns > 0.5)) {
|
|
662
|
+
throw new Error(`physics3d joint ${joint.id}: coneLimitTurns must be in (0, 0.5]`);
|
|
663
|
+
}
|
|
664
|
+
for (const [name, value] of [['twistMinTurns', joint.twistMinTurns], ['twistMaxTurns', joint.twistMaxTurns]]) {
|
|
665
|
+
if (value !== undefined && (!Number.isFinite(value) || value < -0.5 || value > 0.5)) {
|
|
666
|
+
throw new Error(`physics3d joint ${joint.id}: ${name} must be in [-0.5, 0.5]`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (joint.twistMinTurns !== undefined && joint.twistMaxTurns !== undefined && joint.twistMinTurns > joint.twistMaxTurns) {
|
|
670
|
+
throw new Error(`physics3d joint ${joint.id}: twistMinTurns must not exceed twistMaxTurns`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
541
673
|
if (joint.type === 'hinge') {
|
|
542
674
|
if ((0, physics3d_shared_js_1.dotVec3)(joint.axisA, joint.axisA) === 0) {
|
|
543
675
|
throw new Error(`physics3d joint ${joint.id}: hinge axisA must be non-zero`);
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runDeterministicRagdoll3DFixture = runDeterministicRagdoll3DFixture;
|
|
4
|
+
exports.runDeterministicRigidWheelVehicle3DFixture = runDeterministicRigidWheelVehicle3DFixture;
|
|
5
|
+
const canonical_js_1 = require("./canonical.js");
|
|
6
|
+
const physics3d_js_1 = require("./physics3d.js");
|
|
7
|
+
const physics3d_shared_js_1 = require("./physics3d-shared.js");
|
|
8
|
+
const math_js_1 = require("./math.js");
|
|
9
|
+
const RAGDOLL_GRAVITY_Y = -0.02;
|
|
10
|
+
const VEHICLE_GRAVITY_Y = -0.02;
|
|
11
|
+
const IDENT = { x: 0, y: 0, z: 0, w: 1 };
|
|
12
|
+
// Deterministic fixed-point trig for per-tick wheel steering (vehicle precedent).
|
|
13
|
+
const recipeMath = (0, math_js_1.createDeterministicMath)();
|
|
14
|
+
function upDot(orientation) {
|
|
15
|
+
/* istanbul ignore next -- fixture bodies always carry an orientation. */
|
|
16
|
+
const q = orientation ?? IDENT;
|
|
17
|
+
return 1 - 2 * (q.x * q.x + q.z * q.z);
|
|
18
|
+
}
|
|
19
|
+
function ragdollBodies() {
|
|
20
|
+
const box = (id, x, y, z, half, mass) => ({
|
|
21
|
+
id, kind: 'dynamic', x, y, z, vx: 0, vy: 0, vz: 0, orientation: IDENT, angularVel: { x: 0, y: 0, z: 0 },
|
|
22
|
+
shape: { type: 'box', halfX: half.x, halfY: half.y, halfZ: half.z }, layer: 1, mask: 1, material: { friction: 6, restitution: 0 }, mass,
|
|
23
|
+
});
|
|
24
|
+
const capsule = (id, x, y, z, radius, halfHeight, mass) => ({
|
|
25
|
+
id, kind: 'dynamic', x, y, z, vx: 0, vy: 0, vz: 0, orientation: IDENT, angularVel: { x: 0, y: 0, z: 0 },
|
|
26
|
+
shape: { type: 'capsule', radius, halfHeight }, layer: 1, mask: 1, material: { friction: 6, restitution: 0 }, mass,
|
|
27
|
+
});
|
|
28
|
+
return [
|
|
29
|
+
{ id: 'floor', kind: 'static', x: 0, y: -1, z: 0, vx: 0, vy: 0, vz: 0, shape: { type: 'box', halfX: 40, halfY: 1, halfZ: 40 }, layer: 1, mask: 1, material: { friction: 10, restitution: 0 } },
|
|
30
|
+
box('torso', 0, 4, 0, { x: 0.4, y: 0.6, z: 0.25 }, 4),
|
|
31
|
+
box('head', 0, 4.85, 0, { x: 0.25, y: 0.25, z: 0.25 }, 1),
|
|
32
|
+
capsule('arm-l', -0.4, 4.1, 0, 0.15, 0.5, 1),
|
|
33
|
+
capsule('arm-r', 0.4, 4.1, 0, 0.15, 0.5, 1),
|
|
34
|
+
capsule('leg-l', -0.2, 2.8, 0, 0.18, 0.6, 1.5),
|
|
35
|
+
capsule('leg-r', 0.2, 2.8, 0, 0.18, 0.6, 1.5),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
function ragdollJoints() {
|
|
39
|
+
return [
|
|
40
|
+
{ id: 'neck', type: 'ball', a: 'torso', b: 'head', anchorA: { x: 0, y: 0.6, z: 0 }, anchorB: { x: 0, y: -0.25, z: 0 }, axisA: { x: 0, y: 1, z: 0 }, coneLimitTurns: 0.1, twistMinTurns: -0.05, twistMaxTurns: 0.05, disableCollision: true },
|
|
41
|
+
{ id: 'shoulder-l', type: 'ball', a: 'torso', b: 'arm-l', anchorA: { x: -0.4, y: 0.6, z: 0 }, anchorB: { x: 0, y: 0.5, z: 0 }, axisA: { x: 0, y: 1, z: 0 }, coneLimitTurns: 0.2, disableCollision: true },
|
|
42
|
+
{ id: 'shoulder-r', type: 'ball', a: 'torso', b: 'arm-r', anchorA: { x: 0.4, y: 0.6, z: 0 }, anchorB: { x: 0, y: 0.5, z: 0 }, axisA: { x: 0, y: 1, z: 0 }, coneLimitTurns: 0.2, disableCollision: true },
|
|
43
|
+
{ id: 'hip-l', type: 'hinge', a: 'torso', b: 'leg-l', anchorA: { x: -0.2, y: -0.6, z: 0 }, anchorB: { x: 0, y: 0.6, z: 0 }, axisA: { x: 1, y: 0, z: 0 }, minAngle: -0.3, maxAngle: 2.0, disableCollision: true },
|
|
44
|
+
{ id: 'hip-r', type: 'hinge', a: 'torso', b: 'leg-r', anchorA: { x: 0.2, y: -0.6, z: 0 }, anchorB: { x: 0, y: 0.6, z: 0 }, axisA: { x: 1, y: 0, z: 0 }, minAngle: -0.3, maxAngle: 2.0, disableCollision: true },
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
function worldAnchor(world, id, local) {
|
|
48
|
+
const body = world.bodies.find((candidate) => candidate.id === id);
|
|
49
|
+
/* istanbul ignore next -- fixture joints only ever name bodies the fixture authored. */
|
|
50
|
+
if (body === undefined) {
|
|
51
|
+
throw new Error(`ragdoll fixture: unknown body ${id}`);
|
|
52
|
+
}
|
|
53
|
+
/* istanbul ignore next -- every fixture body is authored with an orientation. */
|
|
54
|
+
const rotated = (0, physics3d_shared_js_1.rotatePointByQuat)(body.orientation ?? IDENT, local);
|
|
55
|
+
return { x: body.x + rotated.x, y: body.y + rotated.y, z: body.z + rotated.z };
|
|
56
|
+
}
|
|
57
|
+
function runRagdollLoop(bodies, joints, frames) {
|
|
58
|
+
let world = (0, physics3d_js_1.createDeterministicPhysicsWorld3D)(bodies);
|
|
59
|
+
const snapshotFrame = Math.floor(frames / 2);
|
|
60
|
+
let snapshot = world;
|
|
61
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
62
|
+
if (frame === snapshotFrame) {
|
|
63
|
+
snapshot = (0, canonical_js_1.cloneCanonical)(world);
|
|
64
|
+
}
|
|
65
|
+
world = (0, physics3d_js_1.stepDeterministicPhysicsWorld3D)(world, { gravityY: RAGDOLL_GRAVITY_Y, joints, computeChecksum: false }).world;
|
|
66
|
+
}
|
|
67
|
+
return { world, snapshot, snapshotFrame };
|
|
68
|
+
}
|
|
69
|
+
function runDeterministicRagdoll3DFixture(frames = 600) {
|
|
70
|
+
const bodies = ragdollBodies();
|
|
71
|
+
const joints = ragdollJoints();
|
|
72
|
+
const main = runRagdollLoop(bodies, joints, frames);
|
|
73
|
+
let restored = main.snapshot;
|
|
74
|
+
for (let frame = main.snapshotFrame; frame < frames; frame += 1) {
|
|
75
|
+
restored = (0, physics3d_js_1.stepDeterministicPhysicsWorld3D)(restored, { gravityY: RAGDOLL_GRAVITY_Y, joints, computeChecksum: false }).world;
|
|
76
|
+
}
|
|
77
|
+
const permuted = runRagdollLoop([...bodies].reverse(), [...joints].reverse(), frames);
|
|
78
|
+
const dyn = main.world.bodies.filter((body) => body.kind === 'dynamic');
|
|
79
|
+
let maxFinalSpeed = 0;
|
|
80
|
+
let maxFinalSpin = 0;
|
|
81
|
+
for (const body of dyn) {
|
|
82
|
+
maxFinalSpeed = Math.max(maxFinalSpeed, Math.abs(body.vx), Math.abs(body.vy), Math.abs(body.vz));
|
|
83
|
+
/* istanbul ignore next -- every fixture body is authored with an angularVel. */
|
|
84
|
+
const av = body.angularVel ?? { x: 0, y: 0, z: 0 };
|
|
85
|
+
maxFinalSpin = Math.max(maxFinalSpin, Math.abs(av.x), Math.abs(av.y), Math.abs(av.z));
|
|
86
|
+
}
|
|
87
|
+
let maxJointGap = 0;
|
|
88
|
+
for (const joint of joints) {
|
|
89
|
+
/* istanbul ignore next -- the ragdoll authors only ball/hinge joints; grab joints carry no anchor pair. */
|
|
90
|
+
if (joint.type === 'grab') {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const anchorA = worldAnchor(main.world, joint.a, joint.anchorA);
|
|
94
|
+
const anchorB = worldAnchor(main.world, joint.b, joint.anchorB);
|
|
95
|
+
const gap = Math.hypot(anchorA.x - anchorB.x, anchorA.y - anchorB.y, anchorA.z - anchorB.z);
|
|
96
|
+
maxJointGap = Math.max(maxJointGap, gap);
|
|
97
|
+
}
|
|
98
|
+
const brokenJoints = main.world.jointCache.filter((entry) => entry.broken === true).length;
|
|
99
|
+
const checksum = (0, canonical_js_1.defaultChecksum)(main.world);
|
|
100
|
+
return {
|
|
101
|
+
checksum,
|
|
102
|
+
restoredChecksum: (0, canonical_js_1.defaultChecksum)(restored),
|
|
103
|
+
permutedChecksum: (0, canonical_js_1.defaultChecksum)(permuted.world),
|
|
104
|
+
rollbackEquivalent: checksum === (0, canonical_js_1.defaultChecksum)(restored),
|
|
105
|
+
stableUnderAuthoringOrderPermutation: checksum === (0, canonical_js_1.defaultChecksum)(permuted.world),
|
|
106
|
+
maxFinalSpeed,
|
|
107
|
+
maxFinalSpin,
|
|
108
|
+
bodies: dyn.length,
|
|
109
|
+
maxJointGap,
|
|
110
|
+
brokenJoints,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Rigid-wheel car: a chassis with four sphere wheels driven by hinge motors —
|
|
114
|
+
// no raycast controller, just the rigid solver. The `angularDamping: 1` on the
|
|
115
|
+
// wheels is load-bearing: the default 0.98 bleeds wheel spin and the car crawls.
|
|
116
|
+
const WHEEL_OFFSETS = [
|
|
117
|
+
{ id: 'wheel-fl', x: -0.9, z: 1.6, steered: true },
|
|
118
|
+
{ id: 'wheel-fr', x: 0.9, z: 1.6, steered: true },
|
|
119
|
+
{ id: 'wheel-rl', x: -0.9, z: -1.6, steered: false },
|
|
120
|
+
{ id: 'wheel-rr', x: 0.9, z: -1.6, steered: false },
|
|
121
|
+
];
|
|
122
|
+
function rigidWheelBodies() {
|
|
123
|
+
const bodies = [
|
|
124
|
+
{ id: 'floor', kind: 'static', x: 0, y: -1, z: 0, vx: 0, vy: 0, vz: 0, shape: { type: 'box', halfX: 60, halfY: 1, halfZ: 60 }, layer: 1, mask: 1, material: { friction: 10, restitution: 0 } },
|
|
125
|
+
{ id: 'chassis', kind: 'dynamic', x: 0, y: 1, z: 0, vx: 0, vy: 0, vz: 0, orientation: IDENT, angularVel: { x: 0, y: 0, z: 0 }, shape: { type: 'box', halfX: 0.9, halfY: 0.25, halfZ: 1.6 }, layer: 1, mask: 1, material: { friction: 4, restitution: 0 }, mass: 8 },
|
|
126
|
+
];
|
|
127
|
+
for (const wheel of WHEEL_OFFSETS) {
|
|
128
|
+
bodies.push({ id: wheel.id, kind: 'dynamic', x: wheel.x, y: 0.5, z: wheel.z, vx: 0, vy: 0, vz: 0, orientation: IDENT, angularVel: { x: 0, y: 0, z: 0 }, shape: { type: 'sphere', radius: 0.5 }, layer: 1, mask: 1, material: { friction: 10, restitution: 0 }, mass: 0.5, angularDamping: 1 });
|
|
129
|
+
}
|
|
130
|
+
return bodies;
|
|
131
|
+
}
|
|
132
|
+
// Rotate the spin axis (world +x) about world +y by `steerTurns` so a steered
|
|
133
|
+
// front wheel rolls at an angle. Deterministic fixed-point trig, quantized.
|
|
134
|
+
function steerAxis(steerTurns) {
|
|
135
|
+
const fixed = Math.round(steerTurns * recipeMath.fixedScale);
|
|
136
|
+
const cos = recipeMath.cosTurns(fixed) / recipeMath.fixedScale;
|
|
137
|
+
const sin = recipeMath.sinTurns(fixed) / recipeMath.fixedScale;
|
|
138
|
+
return { x: cos, y: 0, z: -sin };
|
|
139
|
+
}
|
|
140
|
+
function rigidWheelJoints(steerTurns) {
|
|
141
|
+
return WHEEL_OFFSETS.map((wheel) => ({
|
|
142
|
+
id: `axle-${wheel.id}`,
|
|
143
|
+
type: 'hinge',
|
|
144
|
+
a: 'chassis',
|
|
145
|
+
b: wheel.id,
|
|
146
|
+
anchorA: { x: wheel.x, y: -0.5, z: wheel.z },
|
|
147
|
+
anchorB: { x: 0, y: 0, z: 0 },
|
|
148
|
+
axisA: wheel.steered ? steerAxis(steerTurns) : { x: 1, y: 0, z: 0 },
|
|
149
|
+
motorSpeed: -0.15,
|
|
150
|
+
maxMotorTorque: 0.4,
|
|
151
|
+
disableCollision: true,
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
// Straight until frame 300, steer left through frame 450, then straight again.
|
|
155
|
+
function steerForFrame(frame) {
|
|
156
|
+
return frame >= 300 && frame < 450 ? 0.03 : 0;
|
|
157
|
+
}
|
|
158
|
+
function runRigidWheelLoop(bodies, frames) {
|
|
159
|
+
let world = (0, physics3d_js_1.createDeterministicPhysicsWorld3D)(bodies);
|
|
160
|
+
const snapshotFrame = Math.floor(frames / 2);
|
|
161
|
+
let snapshot = world;
|
|
162
|
+
let minUpDot = 1;
|
|
163
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
164
|
+
if (frame === snapshotFrame) {
|
|
165
|
+
snapshot = (0, canonical_js_1.cloneCanonical)(world);
|
|
166
|
+
}
|
|
167
|
+
world = (0, physics3d_js_1.stepDeterministicPhysicsWorld3D)(world, { gravityY: VEHICLE_GRAVITY_Y, joints: rigidWheelJoints(steerForFrame(frame)), computeChecksum: false }).world;
|
|
168
|
+
const chassis = world.bodies.find((body) => body.id === 'chassis');
|
|
169
|
+
/* istanbul ignore else -- the chassis is authored by the fixture and never removed. */
|
|
170
|
+
if (chassis !== undefined) {
|
|
171
|
+
minUpDot = Math.min(minUpDot, upDot(chassis.orientation));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { world, snapshot, snapshotFrame, minUpDot };
|
|
175
|
+
}
|
|
176
|
+
function runDeterministicRigidWheelVehicle3DFixture(frames = 600) {
|
|
177
|
+
const bodies = rigidWheelBodies();
|
|
178
|
+
const main = runRigidWheelLoop(bodies, frames);
|
|
179
|
+
let restored = main.snapshot;
|
|
180
|
+
for (let frame = main.snapshotFrame; frame < frames; frame += 1) {
|
|
181
|
+
restored = (0, physics3d_js_1.stepDeterministicPhysicsWorld3D)(restored, { gravityY: VEHICLE_GRAVITY_Y, joints: rigidWheelJoints(steerForFrame(frame)), computeChecksum: false }).world;
|
|
182
|
+
}
|
|
183
|
+
const permuted = runRigidWheelLoop([...bodies].reverse(), frames);
|
|
184
|
+
const chassis = main.world.bodies.find((body) => body.id === 'chassis');
|
|
185
|
+
const checksum = (0, canonical_js_1.defaultChecksum)(main.world);
|
|
186
|
+
return {
|
|
187
|
+
checksum,
|
|
188
|
+
restoredChecksum: (0, canonical_js_1.defaultChecksum)(restored),
|
|
189
|
+
permutedChecksum: (0, canonical_js_1.defaultChecksum)(permuted.world),
|
|
190
|
+
rollbackEquivalent: checksum === (0, canonical_js_1.defaultChecksum)(restored),
|
|
191
|
+
stableUnderAuthoringOrderPermutation: checksum === (0, canonical_js_1.defaultChecksum)(permuted.world),
|
|
192
|
+
forwardDistance: chassis.z,
|
|
193
|
+
lateralDistance: Math.abs(chassis.x),
|
|
194
|
+
minChassisUpDot: main.minUpDot,
|
|
195
|
+
bodies: main.world.bodies.filter((body) => body.kind === 'dynamic').length,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -81,6 +81,12 @@ function solverSqrt(value) {
|
|
|
81
81
|
if (value <= 0) {
|
|
82
82
|
return 0;
|
|
83
83
|
}
|
|
84
|
+
// Non-finite guard: `while (magnitude >= 4) magnitude /= 4` never terminates for
|
|
85
|
+
// Infinity (Inf/4 === Inf). Returning value keeps sqrt(Inf) = Inf / sqrt(NaN) = NaN
|
|
86
|
+
// without spinning, so a diverged body surfaces as a bad checksum, not a hang.
|
|
87
|
+
if (!Number.isFinite(value)) {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
84
90
|
let estimate = 1;
|
|
85
91
|
let magnitude = value;
|
|
86
92
|
while (magnitude >= 4) {
|