@flighthq/physics3d 0.4.0-next.1694.598ef6f

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 (109) hide show
  1. package/dist/contacts.d.ts +4 -0
  2. package/dist/contacts.d.ts.map +1 -0
  3. package/dist/contacts.js +55 -0
  4. package/dist/contacts.js.map +1 -0
  5. package/dist/contract.d.ts +17 -0
  6. package/dist/contract.d.ts.map +1 -0
  7. package/dist/contract.js +17 -0
  8. package/dist/contract.js.map +1 -0
  9. package/dist/enablePhysics3DGuards.d.ts +4 -0
  10. package/dist/enablePhysics3DGuards.d.ts.map +1 -0
  11. package/dist/enablePhysics3DGuards.js +68 -0
  12. package/dist/enablePhysics3DGuards.js.map +1 -0
  13. package/dist/explainPhysics3DJoints.d.ts +3 -0
  14. package/dist/explainPhysics3DJoints.d.ts.map +1 -0
  15. package/dist/explainPhysics3DJoints.js +32 -0
  16. package/dist/explainPhysics3DJoints.js.map +1 -0
  17. package/dist/explainPhysics3DStep.d.ts +3 -0
  18. package/dist/explainPhysics3DStep.d.ts.map +1 -0
  19. package/dist/explainPhysics3DStep.js +48 -0
  20. package/dist/explainPhysics3DStep.js.map +1 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +14 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/integrate.d.ts +6 -0
  26. package/dist/integrate.d.ts.map +1 -0
  27. package/dist/integrate.js +154 -0
  28. package/dist/integrate.js.map +1 -0
  29. package/dist/islands.d.ts +5 -0
  30. package/dist/islands.d.ts.map +1 -0
  31. package/dist/islands.js +316 -0
  32. package/dist/islands.js.map +1 -0
  33. package/dist/jointCollisionSuppression.d.ts +4 -0
  34. package/dist/jointCollisionSuppression.d.ts.map +1 -0
  35. package/dist/jointCollisionSuppression.js +40 -0
  36. package/dist/jointCollisionSuppression.js.map +1 -0
  37. package/dist/jointFactories.d.ts +8 -0
  38. package/dist/jointFactories.d.ts.map +1 -0
  39. package/dist/jointFactories.js +126 -0
  40. package/dist/jointFactories.js.map +1 -0
  41. package/dist/jointMath.d.ts +17 -0
  42. package/dist/jointMath.d.ts.map +1 -0
  43. package/dist/jointMath.js +361 -0
  44. package/dist/jointMath.js.map +1 -0
  45. package/dist/jointRegistry.d.ts +8 -0
  46. package/dist/jointRegistry.d.ts.map +1 -0
  47. package/dist/jointRegistry.js +144 -0
  48. package/dist/jointRegistry.js.map +1 -0
  49. package/dist/jointRows.d.ts +32 -0
  50. package/dist/jointRows.d.ts.map +1 -0
  51. package/dist/jointRows.js +308 -0
  52. package/dist/jointRows.js.map +1 -0
  53. package/dist/joints.d.ts +14 -0
  54. package/dist/joints.d.ts.map +1 -0
  55. package/dist/joints.js +758 -0
  56. package/dist/joints.js.map +1 -0
  57. package/dist/massProperties.d.ts +8 -0
  58. package/dist/massProperties.d.ts.map +1 -0
  59. package/dist/massProperties.js +169 -0
  60. package/dist/massProperties.js.map +1 -0
  61. package/dist/ownership.d.ts +5 -0
  62. package/dist/ownership.d.ts.map +1 -0
  63. package/dist/ownership.js +16 -0
  64. package/dist/ownership.js.map +1 -0
  65. package/dist/registerBuiltInPhysics3DJointSolvers.d.ts +3 -0
  66. package/dist/registerBuiltInPhysics3DJointSolvers.d.ts.map +1 -0
  67. package/dist/registerBuiltInPhysics3DJointSolvers.js +14 -0
  68. package/dist/registerBuiltInPhysics3DJointSolvers.js.map +1 -0
  69. package/dist/solver.d.ts +8 -0
  70. package/dist/solver.d.ts.map +1 -0
  71. package/dist/solver.js +383 -0
  72. package/dist/solver.js.map +1 -0
  73. package/dist/step.d.ts +5 -0
  74. package/dist/step.d.ts.map +1 -0
  75. package/dist/step.js +250 -0
  76. package/dist/step.js.map +1 -0
  77. package/dist/stepValidation.d.ts +12 -0
  78. package/dist/stepValidation.d.ts.map +1 -0
  79. package/dist/stepValidation.js +201 -0
  80. package/dist/stepValidation.js.map +1 -0
  81. package/dist/symmetricTensor.d.ts +11 -0
  82. package/dist/symmetricTensor.d.ts.map +1 -0
  83. package/dist/symmetricTensor.js +150 -0
  84. package/dist/symmetricTensor.js.map +1 -0
  85. package/dist/world.d.ts +19 -0
  86. package/dist/world.d.ts.map +1 -0
  87. package/dist/world.js +370 -0
  88. package/dist/world.js.map +1 -0
  89. package/package.json +49 -0
  90. package/src/contacts.test.ts +64 -0
  91. package/src/enablePhysics3DGuards.test.ts +105 -0
  92. package/src/explainPhysics3DJoints.test.ts +101 -0
  93. package/src/explainPhysics3DStep.test.ts +123 -0
  94. package/src/integrate.test.ts +249 -0
  95. package/src/islands.test.ts +357 -0
  96. package/src/jointCollisionSuppression.test.ts +94 -0
  97. package/src/jointFactories.test.ts +195 -0
  98. package/src/jointMath.test.ts +643 -0
  99. package/src/jointRegistry.test.ts +229 -0
  100. package/src/jointRows.test.ts +484 -0
  101. package/src/joints.test.ts +677 -0
  102. package/src/massProperties.test.ts +305 -0
  103. package/src/ownership.test.ts +36 -0
  104. package/src/registerBuiltInPhysics3DJointSolvers.test.ts +48 -0
  105. package/src/solver.test.ts +481 -0
  106. package/src/step.test.ts +390 -0
  107. package/src/stepValidation.test.ts +244 -0
  108. package/src/symmetricTensor.test.ts +166 -0
  109. package/src/world.test.ts +512 -0
package/dist/joints.js ADDED
@@ -0,0 +1,758 @@
1
+ import { swapPhysics3DJointFrames, writePhysics3DJointAnchors, writePhysics3DJointRelativeRotation, writePhysics3DJointRotationError, writePhysics3DJointSeparation, } from './jointMath';
2
+ import { applyRow, beginJointSolve, clearJointSolve, frameABasis, frameARotation, frameBBasis, frameBRotation, getJointSolveState, getRowMass, POINT_LENGTH, prepareAngularBlock, preparePointBlock, readFrameBases, readFrameRotations, ROW_LENGTH, solveAngularBlock, solveEqualityRow, solveLowerLimitRow, solveMotorRow, solvePointBlock, solveUpperLimitRow, readJointImpulse, warmStartAngularBlock, warmStartPointBlock, writeAngularRow, writeRow, } from './jointRows';
3
+ import { findPhysics3DBody } from './world';
4
+ // The built-in joint kinds. Bare names are reserved for these; a user's own joint takes a vendor prefix, and
5
+ // that convention rather than a registration guard is what keeps the two from colliding.
6
+ export const Physics3DBallAndSocketJointKind = 'BallAndSocket';
7
+ export const Physics3DConeTwistJointKind = 'ConeTwist';
8
+ export const Physics3DFixedJointKind = 'Fixed';
9
+ export const Physics3DGeneric6DofJointKind = 'Generic6Dof';
10
+ export const Physics3DHingeJointKind = 'Hinge';
11
+ export const Physics3DSliderJointKind = 'Slider';
12
+ // Pins two anchors together, leaving rotation free. Three coupled rows and nothing else — the simplest kind,
13
+ // and the one every other kind's point half is the same block of.
14
+ export const physics3DBallAndSocketJointSolver = {
15
+ clearAccumulatedImpulses(joint) {
16
+ joint.impulse0 = 0;
17
+ joint.impulse1 = 0;
18
+ joint.impulse2 = 0;
19
+ },
20
+ prepare(world, joint, dt) {
21
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
22
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
23
+ if (bodyA === null || bodyB === null) {
24
+ clearJointSolve(joint);
25
+ return;
26
+ }
27
+ preparePointBlock(bodyA, bodyB, joint, beginJointSolve(joint, POINT_LENGTH), dt);
28
+ },
29
+ solve(world, joint) {
30
+ const state = getJointSolveState(joint);
31
+ if (state === undefined)
32
+ return;
33
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
34
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
35
+ if (bodyA === null || bodyB === null)
36
+ return;
37
+ solvePointBlock(bodyA, bodyB, joint, state);
38
+ },
39
+ warmStart(world, joint) {
40
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
41
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
42
+ if (bodyA === null || bodyB === null)
43
+ return;
44
+ warmStartPointBlock(bodyA, bodyB, joint);
45
+ },
46
+ };
47
+ // Pins two anchors together and locks the relative orientation. The point block plus the angular block, with
48
+ // no parameters of its own: a fixed joint is defined entirely by the pose it was authored in.
49
+ export const physics3DFixedJointSolver = {
50
+ clearAccumulatedImpulses(joint) {
51
+ joint.impulse0 = 0;
52
+ joint.impulse1 = 0;
53
+ joint.impulse2 = 0;
54
+ joint.impulse3 = 0;
55
+ joint.impulse4 = 0;
56
+ joint.impulse5 = 0;
57
+ },
58
+ // The frames travel with their bodies and nothing about them reverses: "these two frames coincide" reads
59
+ // the same from either end. The generic swap moves the bodies and anchors; this moves the frames with them.
60
+ swapEnds(joint) {
61
+ swapPhysics3DJointFrames(joint);
62
+ return true;
63
+ },
64
+ prepare(world, joint, dt) {
65
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
66
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
67
+ if (bodyA === null || bodyB === null) {
68
+ clearJointSolve(joint);
69
+ return;
70
+ }
71
+ const state = beginJointSolve(joint, FIXED_LENGTH);
72
+ preparePointBlock(bodyA, bodyB, joint, state, dt);
73
+ readFrameRotations(bodyA, bodyB, joint);
74
+ prepareAngularBlock(bodyA, bodyB, state, FIXED_ANGULAR_MASS, FIXED_ANGULAR_BIAS, dt);
75
+ },
76
+ solve(world, joint) {
77
+ const state = getJointSolveState(joint);
78
+ if (state === undefined)
79
+ return;
80
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
81
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
82
+ if (bodyA === null || bodyB === null)
83
+ return;
84
+ solveAngularBlock(bodyA, bodyB, joint, state, FIXED_ANGULAR_MASS, FIXED_ANGULAR_BIAS);
85
+ solvePointBlock(bodyA, bodyB, joint, state);
86
+ },
87
+ warmStart(world, joint) {
88
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
89
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
90
+ if (bodyA === null || bodyB === null)
91
+ return;
92
+ warmStartPointBlock(bodyA, bodyB, joint);
93
+ warmStartAngularBlock(bodyA, bodyB, joint);
94
+ },
95
+ };
96
+ // Pins two anchors together and confines rotation to the frame's X axis, optionally motorized and limited.
97
+ //
98
+ // The point block, then two angular rows holding frame B's X onto frame A's, then a motor and a pair of
99
+ // limits sharing the axis row. The two lock rows are solved sequentially rather than as a coupled 2x2: they
100
+ // turn about perpendicular axes, so they couple only through an anisotropic inertia tensor, and the residual
101
+ // is what the remaining velocity iterations absorb.
102
+ export const physics3DHingeJointSolver = {
103
+ clearAccumulatedImpulses(joint) {
104
+ joint.impulse0 = 0;
105
+ joint.impulse1 = 0;
106
+ joint.impulse2 = 0;
107
+ joint.impulse3 = 0;
108
+ joint.impulse4 = 0;
109
+ const hinge = joint;
110
+ hinge.motorImpulse = 0;
111
+ hinge.lowerLimitImpulse = 0;
112
+ hinge.upperLimitImpulse = 0;
113
+ },
114
+ scaleAccumulatedImpulses(joint, timestepRatio) {
115
+ const hinge = joint;
116
+ hinge.motorImpulse = (hinge.motorImpulse ?? 0) * timestepRatio;
117
+ hinge.lowerLimitImpulse = (hinge.lowerLimitImpulse ?? 0) * timestepRatio;
118
+ hinge.upperLimitImpulse = (hinge.upperLimitImpulse ?? 0) * timestepRatio;
119
+ },
120
+ // The angle is measured from frame A's Y axis to frame B's about the shared X axis, so exchanging the ends
121
+ // reverses it. The limit interval negates AND its ends exchange — the old lower bound becomes the new
122
+ // upper — and the motor's target relative velocity reverses with it.
123
+ //
124
+ // Deriving the interval: the constraint is lower <= angle <= upper. After the swap the same physical
125
+ // interval requires lower' = -upper and upper' = -lower.
126
+ swapEnds(joint) {
127
+ const hinge = joint;
128
+ swapPhysics3DJointFrames(hinge);
129
+ const lower = hinge.lowerAngle;
130
+ hinge.lowerAngle = -hinge.upperAngle;
131
+ hinge.upperAngle = -lower;
132
+ hinge.motorSpeed = -hinge.motorSpeed;
133
+ // Defaulted BEFORE the negation. This runs when the joint is added, ahead of the first prepare, so the
134
+ // field may still be absent — and `-undefined` is NaN, which is not nullish, so a later `??` would accept
135
+ // the poison rather than replace it.
136
+ hinge.motorImpulse = -(hinge.motorImpulse ?? 0);
137
+ // Exchanged, NOT negated. Each is a non-negative magnitude whose direction is carried by the row that
138
+ // owns it, so the push that used to hold the lower bound is the one that now holds the upper.
139
+ const lowerImpulse = hinge.lowerLimitImpulse ?? 0;
140
+ hinge.lowerLimitImpulse = hinge.upperLimitImpulse ?? 0;
141
+ hinge.upperLimitImpulse = lowerImpulse;
142
+ return true;
143
+ },
144
+ prepare(world, joint, dt) {
145
+ const hinge = joint;
146
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
147
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
148
+ if (bodyA === null || bodyB === null) {
149
+ clearJointSolve(joint);
150
+ return;
151
+ }
152
+ const state = beginJointSolve(joint, HINGE_LENGTH);
153
+ preparePointBlock(bodyA, bodyB, joint, state, dt);
154
+ readFrameRotations(bodyA, bodyB, hinge);
155
+ readFrameBases(bodyA, bodyB, hinge);
156
+ writeAngularRow(state, HINGE_AXIS_ROW, frameABasis[0], frameABasis[1], frameABasis[2]);
157
+ writeAngularRow(state, HINGE_LOCK_ROW0, frameABasis[3], frameABasis[4], frameABasis[5]);
158
+ writeAngularRow(state, HINGE_LOCK_ROW1, frameABasis[6], frameABasis[7], frameABasis[8]);
159
+ state[HINGE_AXIS_MASS] = getRowMass(bodyA, bodyB, state, HINGE_AXIS_ROW);
160
+ state[HINGE_LOCK_MASS0] = getRowMass(bodyA, bodyB, state, HINGE_LOCK_ROW0);
161
+ state[HINGE_LOCK_MASS1] = getRowMass(bodyA, bodyB, state, HINGE_LOCK_ROW1);
162
+ // The two lock rows' positional error is the misalignment rotation projected onto the axes they turn
163
+ // about. Its third component, about the hinge axis itself, is the one rotation the joint permits and is
164
+ // deliberately not read here — the limits read the exact angle instead.
165
+ writePhysics3DJointRotationError(frameARotation[0], frameARotation[1], frameARotation[2], frameARotation[3], frameBRotation[0], frameBRotation[1], frameBRotation[2], frameBRotation[3], rotationError);
166
+ state[HINGE_LOCK_ERROR0] = dot(rotationError, 0, frameABasis, 3);
167
+ state[HINGE_LOCK_ERROR1] = dot(rotationError, 0, frameABasis, 6);
168
+ state[HINGE_ANGLE] = getSignedAxisAngle(frameABasis, frameBBasis);
169
+ state[HINGE_LOCK_BIAS] = BAUMGARTE / dt;
170
+ state[HINGE_LIMIT_BIAS] = 1 / dt;
171
+ state[HINGE_MAX_MOTOR] = Math.max(0, dt * hinge.maxMotorTorque);
172
+ // Seeded from the carried accumulators rather than zeroed, which is what makes the limit rows warm
173
+ // start. Gated on the CURRENT `enableLimit` for the same reason the motor is: a cached impulse is only
174
+ // valid while the row that produced it is still being solved.
175
+ hinge.lowerLimitImpulse ??= 0;
176
+ hinge.upperLimitImpulse ??= 0;
177
+ if (!hinge.enableLimit) {
178
+ hinge.lowerLimitImpulse = 0;
179
+ hinge.upperLimitImpulse = 0;
180
+ }
181
+ state[HINGE_LOWER_IMPULSE] = hinge.lowerLimitImpulse;
182
+ state[HINGE_UPPER_IMPULSE] = hinge.upperLimitImpulse;
183
+ hinge.motorImpulse ??= 0;
184
+ if (hinge.enableMotor) {
185
+ hinge.motorImpulse = clamp(hinge.motorImpulse, -state[HINGE_MAX_MOTOR], state[HINGE_MAX_MOTOR]);
186
+ }
187
+ else {
188
+ hinge.motorImpulse = 0;
189
+ }
190
+ },
191
+ solve(world, joint) {
192
+ const hinge = joint;
193
+ const state = getJointSolveState(joint);
194
+ if (state === undefined)
195
+ return;
196
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
197
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
198
+ if (bodyA === null || bodyB === null)
199
+ return;
200
+ // Motor first: it is a soft target the limits are allowed to override, so solving it before them lets a
201
+ // limit's impulse win within the same iteration when the two disagree.
202
+ if (hinge.enableMotor && state[HINGE_AXIS_MASS] > 0) {
203
+ hinge.motorImpulse = solveMotorRow(bodyA, bodyB, state, HINGE_AXIS_ROW, state[HINGE_AXIS_MASS], hinge.motorSpeed, state[HINGE_MAX_MOTOR], hinge.motorImpulse);
204
+ }
205
+ if (hinge.enableLimit && state[HINGE_AXIS_MASS] > 0) {
206
+ const angle = state[HINGE_ANGLE];
207
+ solveLowerLimitRow(bodyA, bodyB, state, HINGE_AXIS_ROW, state[HINGE_AXIS_MASS], angle - hinge.lowerAngle, state[HINGE_LIMIT_BIAS], HINGE_LOWER_IMPULSE);
208
+ solveUpperLimitRow(bodyA, bodyB, state, HINGE_AXIS_ROW, state[HINGE_AXIS_MASS], hinge.upperAngle - angle, state[HINGE_LIMIT_BIAS], HINGE_UPPER_IMPULSE);
209
+ // Written back every iteration, not once at the end of the step: the solve state is per-sub-interval
210
+ // scratch that `beginJointSolve` reallocates, so the joint field is the only place a value survives to
211
+ // the next one.
212
+ hinge.lowerLimitImpulse = state[HINGE_LOWER_IMPULSE];
213
+ hinge.upperLimitImpulse = state[HINGE_UPPER_IMPULSE];
214
+ }
215
+ solveEqualityRow(bodyA, bodyB, joint, 3, state, HINGE_LOCK_ROW0, state[HINGE_LOCK_MASS0], state[HINGE_LOCK_ERROR0], state[HINGE_LOCK_BIAS]);
216
+ solveEqualityRow(bodyA, bodyB, joint, 4, state, HINGE_LOCK_ROW1, state[HINGE_LOCK_MASS1], state[HINGE_LOCK_ERROR1], state[HINGE_LOCK_BIAS]);
217
+ solvePointBlock(bodyA, bodyB, joint, state);
218
+ },
219
+ warmStart(world, joint) {
220
+ const hinge = joint;
221
+ const state = getJointSolveState(joint);
222
+ if (state === undefined)
223
+ return;
224
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
225
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
226
+ if (bodyA === null || bodyB === null)
227
+ return;
228
+ warmStartPointBlock(bodyA, bodyB, joint);
229
+ applyRow(bodyA, bodyB, state, HINGE_LOCK_ROW0, joint.impulse3);
230
+ applyRow(bodyA, bodyB, state, HINGE_LOCK_ROW1, joint.impulse4);
231
+ // Gated on the CURRENT `enableMotor`, not on the value being non-zero. A cached impulse is valid only
232
+ // while the thing that produced it is still running: `solve` skips a disabled motor, so reapplying its
233
+ // last impulse here would exert a torque no solver ever asks for and nothing ever cancels — a disabled
234
+ // motor that keeps turning the hinge forever.
235
+ if (hinge.enableMotor)
236
+ applyRow(bodyA, bodyB, state, HINGE_AXIS_ROW, hinge.motorImpulse);
237
+ // The lower row pushes along the axis and the upper row against it, which is the sign each carries in
238
+ // `solveLowerLimitRow`/`solveUpperLimitRow`. Reapplying them with the wrong signs would drive the joint
239
+ // toward the stop it is resting against instead of holding it off.
240
+ if (hinge.enableLimit) {
241
+ applyRow(bodyA, bodyB, state, HINGE_AXIS_ROW, hinge.lowerLimitImpulse - hinge.upperLimitImpulse);
242
+ }
243
+ },
244
+ };
245
+ // Pins two anchors together and bounds relative rotation as a cone plus a twist — the ragdoll joint.
246
+ //
247
+ // The point block, one swing limit, and a pair of twist limits. The swing limit is a single one-sided row
248
+ // about the axis the tilt is happening in, not two per-axis bounds: a cone is not a box, and a shoulder
249
+ // bounded by three independent intervals reaches poses along the diagonal that the anatomy does not have.
250
+ export const physics3DConeTwistJointSolver = {
251
+ clearAccumulatedImpulses(joint) {
252
+ joint.impulse0 = 0;
253
+ joint.impulse1 = 0;
254
+ joint.impulse2 = 0;
255
+ const cone = joint;
256
+ cone.swingLimitImpulse = 0;
257
+ cone.lowerTwistImpulse = 0;
258
+ cone.upperTwistImpulse = 0;
259
+ },
260
+ scaleAccumulatedImpulses(joint, timestepRatio) {
261
+ const cone = joint;
262
+ cone.swingLimitImpulse = (cone.swingLimitImpulse ?? 0) * timestepRatio;
263
+ cone.lowerTwistImpulse = (cone.lowerTwistImpulse ?? 0) * timestepRatio;
264
+ cone.upperTwistImpulse = (cone.upperTwistImpulse ?? 0) * timestepRatio;
265
+ },
266
+ // Vetoed, and the reason is exact rather than cautious. A kind may exchange its ends only when its own
267
+ // constraint keeps the two frames aligned on the axes its parameters are measured against — a hinge and a
268
+ // slider both lock frame B's X onto frame A's, so their axis survives the exchange. A cone-twist
269
+ // deliberately leaves that alignment FREE within the cone, so after the exchange the cone would be
270
+ // measured against the other body's frame: a different constraint wearing the same numbers.
271
+ swapEnds() {
272
+ return false;
273
+ },
274
+ prepare(world, joint, dt) {
275
+ const cone = joint;
276
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
277
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
278
+ if (bodyA === null || bodyB === null) {
279
+ clearJointSolve(joint);
280
+ return;
281
+ }
282
+ const state = beginJointSolve(joint, CONE_LENGTH);
283
+ preparePointBlock(bodyA, bodyB, joint, state, dt);
284
+ readFrameRotations(bodyA, bodyB, cone);
285
+ readFrameBases(bodyA, bodyB, cone);
286
+ // The swing axis is perpendicular to both twist axes, so turning B about it is exactly what opens and
287
+ // closes the cone. It vanishes when the two are parallel, which is the joint sitting at the centre of its
288
+ // cone — no direction to limit, and nothing to limit it against.
289
+ const swingX = frameABasis[1] * frameBBasis[2] - frameABasis[2] * frameBBasis[1];
290
+ const swingY = frameABasis[2] * frameBBasis[0] - frameABasis[0] * frameBBasis[2];
291
+ const swingZ = frameABasis[0] * frameBBasis[1] - frameABasis[1] * frameBBasis[0];
292
+ const swingLength = Math.sqrt(swingX * swingX + swingY * swingY + swingZ * swingZ);
293
+ const active = swingLength > AXIS_EPSILON;
294
+ state[CONE_SWING_ACTIVE] = active ? 1 : 0;
295
+ if (active) {
296
+ writeAngularRow(state, CONE_SWING_ROW, swingX / swingLength, swingY / swingLength, swingZ / swingLength);
297
+ state[CONE_SWING_MASS] = getRowMass(bodyA, bodyB, state, CONE_SWING_ROW);
298
+ const swing = Math.acos(clamp(dot(frameABasis, 0, frameBBasis, 0), -1, 1));
299
+ state[CONE_SWING_ERROR] = getConeSwingLimit(cone) - swing;
300
+ }
301
+ writeAngularRow(state, CONE_TWIST_ROW, frameABasis[0], frameABasis[1], frameABasis[2]);
302
+ state[CONE_TWIST_MASS] = getRowMass(bodyA, bodyB, state, CONE_TWIST_ROW);
303
+ // Twist is the X component of the relative rotation taken in A's own frame — the rotation left over once
304
+ // the swing is removed. Reading it here rather than from the world-space error vector is what keeps it
305
+ // independent of how far the joint has swung.
306
+ writePhysics3DJointRelativeRotation(frameARotation[0], frameARotation[1], frameARotation[2], frameARotation[3], frameBRotation[0], frameBRotation[1], frameBRotation[2], frameBRotation[3], relativeRotation);
307
+ state[CONE_TWIST_ANGLE] = 2 * Math.atan2(relativeRotation[0], relativeRotation[3]);
308
+ state[CONE_LIMIT_BIAS] = 1 / dt;
309
+ // The swing row's AXIS moves with the tilt direction, so its carried impulse is reapplied along a
310
+ // slightly different axis than the one that earned it — the same approximation contact warm starting
311
+ // makes when a normal rotates, and it converges for the same reason: one iteration corrects it.
312
+ cone.swingLimitImpulse ??= 0;
313
+ cone.lowerTwistImpulse ??= 0;
314
+ cone.upperTwistImpulse ??= 0;
315
+ if (!cone.enableSwingLimit)
316
+ cone.swingLimitImpulse = 0;
317
+ if (!cone.enableTwistLimit) {
318
+ cone.lowerTwistImpulse = 0;
319
+ cone.upperTwistImpulse = 0;
320
+ }
321
+ state[CONE_SWING_IMPULSE] = cone.swingLimitImpulse;
322
+ state[CONE_LOWER_TWIST_IMPULSE] = cone.lowerTwistImpulse;
323
+ state[CONE_UPPER_TWIST_IMPULSE] = cone.upperTwistImpulse;
324
+ },
325
+ solve(world, joint) {
326
+ const cone = joint;
327
+ const state = getJointSolveState(joint);
328
+ if (state === undefined)
329
+ return;
330
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
331
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
332
+ if (bodyA === null || bodyB === null)
333
+ return;
334
+ if (cone.enableSwingLimit && state[CONE_SWING_ACTIVE] === 1 && state[CONE_SWING_MASS] > 0) {
335
+ solveUpperLimitRow(bodyA, bodyB, state, CONE_SWING_ROW, state[CONE_SWING_MASS], state[CONE_SWING_ERROR], state[CONE_LIMIT_BIAS], CONE_SWING_IMPULSE);
336
+ cone.swingLimitImpulse = state[CONE_SWING_IMPULSE];
337
+ }
338
+ if (cone.enableTwistLimit && state[CONE_TWIST_MASS] > 0) {
339
+ const twist = state[CONE_TWIST_ANGLE];
340
+ solveLowerLimitRow(bodyA, bodyB, state, CONE_TWIST_ROW, state[CONE_TWIST_MASS], twist - cone.lowerTwistAngle, state[CONE_LIMIT_BIAS], CONE_LOWER_TWIST_IMPULSE);
341
+ solveUpperLimitRow(bodyA, bodyB, state, CONE_TWIST_ROW, state[CONE_TWIST_MASS], cone.upperTwistAngle - twist, state[CONE_LIMIT_BIAS], CONE_UPPER_TWIST_IMPULSE);
342
+ cone.lowerTwistImpulse = state[CONE_LOWER_TWIST_IMPULSE];
343
+ cone.upperTwistImpulse = state[CONE_UPPER_TWIST_IMPULSE];
344
+ }
345
+ solvePointBlock(bodyA, bodyB, joint, state);
346
+ },
347
+ warmStart(world, joint) {
348
+ const cone = joint;
349
+ const state = getJointSolveState(joint);
350
+ if (state === undefined)
351
+ return;
352
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
353
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
354
+ if (bodyA === null || bodyB === null)
355
+ return;
356
+ warmStartPointBlock(bodyA, bodyB, joint);
357
+ // The swing row is reapplied only while it is the ACTIVE side of the cone. Outside that, the row it was
358
+ // measured against does not exist this sub-interval and its axis is stale.
359
+ if (cone.enableSwingLimit && state[CONE_SWING_ACTIVE] === 1) {
360
+ applyRow(bodyA, bodyB, state, CONE_SWING_ROW, -cone.swingLimitImpulse);
361
+ }
362
+ if (cone.enableTwistLimit) {
363
+ applyRow(bodyA, bodyB, state, CONE_TWIST_ROW, cone.lowerTwistImpulse - cone.upperTwistImpulse);
364
+ }
365
+ },
366
+ };
367
+ // Bounds each of the six degrees of freedom independently — the configurable joint every other kind is a
368
+ // preset of.
369
+ //
370
+ // Each axis reads its state from its bounds rather than from a mode flag: `lower > upper` is free,
371
+ // `lower === upper` is locked, and anything else is limited. A separate flag would be a second source that
372
+ // can disagree with the numbers it describes.
373
+ export const physics3DGeneric6DofJointSolver = {
374
+ clearAccumulatedImpulses(joint) {
375
+ const dof = joint;
376
+ for (let axis = 0; axis < 6; axis += 1) {
377
+ dof.lowerLimitImpulses[axis] = 0;
378
+ dof.upperLimitImpulses[axis] = 0;
379
+ }
380
+ joint.impulse0 = 0;
381
+ joint.impulse1 = 0;
382
+ joint.impulse2 = 0;
383
+ joint.impulse3 = 0;
384
+ joint.impulse4 = 0;
385
+ joint.impulse5 = 0;
386
+ },
387
+ scaleAccumulatedImpulses(joint, timestepRatio) {
388
+ const dof = joint;
389
+ for (let axis = 0; axis < 6; axis += 1) {
390
+ dof.lowerLimitImpulses[axis] *= timestepRatio;
391
+ dof.upperLimitImpulses[axis] *= timestepRatio;
392
+ }
393
+ },
394
+ // Vetoed for the same reason as a cone-twist, and for a kind whose entire purpose is per-axis freedom the
395
+ // reason is unavoidable: any axis left free lets the two frames diverge about it, so the bounds — which are
396
+ // measured against frame A — would after the exchange be measured against a frame the joint never held
397
+ // still. An all-locked 6-DOF could be exchanged safely, but a kind cannot answer this per instance without
398
+ // its ends already meaning two different things depending on how it was configured.
399
+ swapEnds() {
400
+ return false;
401
+ },
402
+ prepare(world, joint, dt) {
403
+ const dof = joint;
404
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
405
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
406
+ if (bodyA === null || bodyB === null) {
407
+ clearJointSolve(joint);
408
+ return;
409
+ }
410
+ const state = beginJointSolve(joint, DOF_LENGTH);
411
+ writePhysics3DJointAnchors(bodyA, bodyB, joint);
412
+ writePhysics3DJointSeparation(bodyA, bodyB, joint, separation);
413
+ readFrameRotations(bodyA, bodyB, dof);
414
+ readFrameBases(bodyA, bodyB, dof);
415
+ writePhysics3DJointRotationError(frameARotation[0], frameARotation[1], frameARotation[2], frameARotation[3], frameBRotation[0], frameBRotation[1], frameBRotation[2], frameBRotation[3], rotationError);
416
+ readDofBounds(dof);
417
+ for (let axis = 0; axis < 6; axis += 1) {
418
+ const rowOffset = DOF_ROWS + axis * ROW_LENGTH;
419
+ const basisOffset = (axis % 3) * 3;
420
+ const lower = dofLower[axis];
421
+ const upper = dofUpper[axis];
422
+ state[DOF_MODE + axis] = lower > upper ? DOF_FREE : lower === upper ? DOF_LOCKED : DOF_LIMITED;
423
+ // Carried only while the axis is still LIMITED. An axis whose bounds changed it to free or locked has
424
+ // an accumulator describing a constraint that no longer exists, and reapplying it would push against a
425
+ // bound the joint may no longer be anywhere near.
426
+ if (state[DOF_MODE + axis] !== DOF_LIMITED) {
427
+ dof.lowerLimitImpulses[axis] = 0;
428
+ dof.upperLimitImpulses[axis] = 0;
429
+ }
430
+ state[DOF_LOWER_IMPULSE + axis] = dof.lowerLimitImpulses[axis];
431
+ state[DOF_UPPER_IMPULSE + axis] = dof.upperLimitImpulses[axis];
432
+ if (state[DOF_MODE + axis] === DOF_FREE)
433
+ continue;
434
+ if (axis < 3) {
435
+ writeLinearAxisRow(state, rowOffset, joint, basisOffset);
436
+ state[DOF_VALUE + axis] = dot(separation, 0, frameABasis, basisOffset);
437
+ }
438
+ else {
439
+ writeAngularRow(state, rowOffset, frameABasis[basisOffset], frameABasis[basisOffset + 1], frameABasis[basisOffset + 2]);
440
+ state[DOF_VALUE + axis] = dot(rotationError, 0, frameABasis, basisOffset);
441
+ }
442
+ state[DOF_MASS + axis] = getRowMass(bodyA, bodyB, state, rowOffset);
443
+ }
444
+ state[DOF_LOCK_BIAS] = BAUMGARTE / dt;
445
+ state[DOF_LIMIT_BIAS] = 1 / dt;
446
+ },
447
+ solve(world, joint) {
448
+ const dof = joint;
449
+ const state = getJointSolveState(joint);
450
+ if (state === undefined)
451
+ return;
452
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
453
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
454
+ if (bodyA === null || bodyB === null)
455
+ return;
456
+ readDofBounds(dof);
457
+ for (let axis = 0; axis < 6; axis += 1) {
458
+ const mode = state[DOF_MODE + axis];
459
+ if (mode === DOF_FREE || state[DOF_MASS + axis] <= 0)
460
+ continue;
461
+ const rowOffset = DOF_ROWS + axis * ROW_LENGTH;
462
+ const value = state[DOF_VALUE + axis];
463
+ if (mode === DOF_LOCKED) {
464
+ solveEqualityRow(bodyA, bodyB, joint, axis, state, rowOffset, state[DOF_MASS + axis], value - dofLower[axis], state[DOF_LOCK_BIAS]);
465
+ continue;
466
+ }
467
+ solveLowerLimitRow(bodyA, bodyB, state, rowOffset, state[DOF_MASS + axis], value - dofLower[axis], state[DOF_LIMIT_BIAS], DOF_LOWER_IMPULSE + axis);
468
+ solveUpperLimitRow(bodyA, bodyB, state, rowOffset, state[DOF_MASS + axis], dofUpper[axis] - value, state[DOF_LIMIT_BIAS], DOF_UPPER_IMPULSE + axis);
469
+ dof.lowerLimitImpulses[axis] = state[DOF_LOWER_IMPULSE + axis];
470
+ dof.upperLimitImpulses[axis] = state[DOF_UPPER_IMPULSE + axis];
471
+ }
472
+ },
473
+ // Locked axes reapply the equality block's slot; limited axes reapply their own pair of one-sided
474
+ // accumulators. The two come from different places because a locked axis is one signed row while a limited
475
+ // axis is two rows that may only push, and neither can be read out of the other's storage.
476
+ warmStart(world, joint) {
477
+ const dof = joint;
478
+ const state = getJointSolveState(joint);
479
+ if (state === undefined)
480
+ return;
481
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
482
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
483
+ if (bodyA === null || bodyB === null)
484
+ return;
485
+ for (let axis = 0; axis < 6; axis += 1) {
486
+ const mode = state[DOF_MODE + axis];
487
+ const rowOffset = DOF_ROWS + axis * ROW_LENGTH;
488
+ if (mode === DOF_LOCKED) {
489
+ applyRow(bodyA, bodyB, state, rowOffset, readJointImpulse(joint, axis));
490
+ }
491
+ else if (mode === DOF_LIMITED) {
492
+ applyRow(bodyA, bodyB, state, rowOffset, dof.lowerLimitImpulses[axis] - dof.upperLimitImpulses[axis]);
493
+ }
494
+ }
495
+ },
496
+ };
497
+ // Confines relative motion to translation along the frame's X axis, optionally motorized and travel-limited.
498
+ //
499
+ // Two perpendicular linear rows hold the anchors on the rail, three angular rows lock the orientation, and
500
+ // the axis row carries the motor and the limits. The two perpendicular rows are solved sequentially rather
501
+ // than as a coupled 2x2: their linear directions are orthogonal, so they couple only through an anisotropic
502
+ // inertia tensor, and the residual is what the remaining velocity iterations absorb.
503
+ export const physics3DSliderJointSolver = {
504
+ clearAccumulatedImpulses(joint) {
505
+ joint.impulse0 = 0;
506
+ joint.impulse1 = 0;
507
+ joint.impulse3 = 0;
508
+ joint.impulse4 = 0;
509
+ joint.impulse5 = 0;
510
+ const slider = joint;
511
+ slider.motorImpulse = 0;
512
+ slider.lowerLimitImpulse = 0;
513
+ slider.upperLimitImpulse = 0;
514
+ },
515
+ scaleAccumulatedImpulses(joint, timestepRatio) {
516
+ const slider = joint;
517
+ slider.motorImpulse = (slider.motorImpulse ?? 0) * timestepRatio;
518
+ slider.lowerLimitImpulse = (slider.lowerLimitImpulse ?? 0) * timestepRatio;
519
+ slider.upperLimitImpulse = (slider.upperLimitImpulse ?? 0) * timestepRatio;
520
+ },
521
+ // The translation is measured from A's anchor to B's along the shared axis, so exchanging the ends reverses
522
+ // it: the interval negates AND its ends exchange, and the motor's target velocity reverses with them. The
523
+ // exchange is exact because the slider's own angular lock holds frame B's X onto frame A's, so the axis the
524
+ // bounds are measured against is the same line from either end.
525
+ swapEnds(joint) {
526
+ const slider = joint;
527
+ swapPhysics3DJointFrames(slider);
528
+ const lower = slider.lowerTranslation;
529
+ slider.lowerTranslation = -slider.upperTranslation;
530
+ slider.upperTranslation = -lower;
531
+ slider.motorSpeed = -slider.motorSpeed;
532
+ slider.motorImpulse = -(slider.motorImpulse ?? 0);
533
+ // Exchanged, not negated — see the hinge's swapEnds for why a magnitude behaves differently here from
534
+ // the bound it holds.
535
+ const lowerImpulse = slider.lowerLimitImpulse ?? 0;
536
+ slider.lowerLimitImpulse = slider.upperLimitImpulse ?? 0;
537
+ slider.upperLimitImpulse = lowerImpulse;
538
+ return true;
539
+ },
540
+ prepare(world, joint, dt) {
541
+ const slider = joint;
542
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
543
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
544
+ if (bodyA === null || bodyB === null) {
545
+ clearJointSolve(joint);
546
+ return;
547
+ }
548
+ const state = beginJointSolve(joint, SLIDER_LENGTH);
549
+ writePhysics3DJointAnchors(bodyA, bodyB, joint);
550
+ writePhysics3DJointSeparation(bodyA, bodyB, joint, separation);
551
+ readFrameRotations(bodyA, bodyB, slider);
552
+ readFrameBases(bodyA, bodyB, slider);
553
+ writeLinearAxisRow(state, SLIDER_AXIS_ROW, joint, 0);
554
+ writeLinearAxisRow(state, SLIDER_PERP_ROW0, joint, 3);
555
+ writeLinearAxisRow(state, SLIDER_PERP_ROW1, joint, 6);
556
+ state[SLIDER_AXIS_MASS] = getRowMass(bodyA, bodyB, state, SLIDER_AXIS_ROW);
557
+ state[SLIDER_PERP_MASS0] = getRowMass(bodyA, bodyB, state, SLIDER_PERP_ROW0);
558
+ state[SLIDER_PERP_MASS1] = getRowMass(bodyA, bodyB, state, SLIDER_PERP_ROW1);
559
+ state[SLIDER_TRANSLATION] = dot(separation, 0, frameABasis, 0);
560
+ state[SLIDER_PERP_ERROR0] = dot(separation, 0, frameABasis, 3);
561
+ state[SLIDER_PERP_ERROR1] = dot(separation, 0, frameABasis, 6);
562
+ prepareAngularBlock(bodyA, bodyB, state, SLIDER_ANGULAR_MASS, SLIDER_ANGULAR_BIAS, dt);
563
+ state[SLIDER_LOCK_BIAS] = BAUMGARTE / dt;
564
+ state[SLIDER_LIMIT_BIAS] = 1 / dt;
565
+ state[SLIDER_MAX_MOTOR] = Math.max(0, dt * slider.maxMotorForce);
566
+ slider.lowerLimitImpulse ??= 0;
567
+ slider.upperLimitImpulse ??= 0;
568
+ if (!slider.enableLimit) {
569
+ slider.lowerLimitImpulse = 0;
570
+ slider.upperLimitImpulse = 0;
571
+ }
572
+ state[SLIDER_LOWER_IMPULSE] = slider.lowerLimitImpulse;
573
+ state[SLIDER_UPPER_IMPULSE] = slider.upperLimitImpulse;
574
+ slider.motorImpulse ??= 0;
575
+ if (slider.enableMotor) {
576
+ slider.motorImpulse = clamp(slider.motorImpulse, -state[SLIDER_MAX_MOTOR], state[SLIDER_MAX_MOTOR]);
577
+ }
578
+ else {
579
+ slider.motorImpulse = 0;
580
+ }
581
+ },
582
+ solve(world, joint) {
583
+ const slider = joint;
584
+ const state = getJointSolveState(joint);
585
+ if (state === undefined)
586
+ return;
587
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
588
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
589
+ if (bodyA === null || bodyB === null)
590
+ return;
591
+ if (slider.enableMotor && state[SLIDER_AXIS_MASS] > 0) {
592
+ slider.motorImpulse = solveMotorRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, state[SLIDER_AXIS_MASS], slider.motorSpeed, state[SLIDER_MAX_MOTOR], slider.motorImpulse);
593
+ }
594
+ if (slider.enableLimit && state[SLIDER_AXIS_MASS] > 0) {
595
+ const translation = state[SLIDER_TRANSLATION];
596
+ solveLowerLimitRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, state[SLIDER_AXIS_MASS], translation - slider.lowerTranslation, state[SLIDER_LIMIT_BIAS], SLIDER_LOWER_IMPULSE);
597
+ solveUpperLimitRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, state[SLIDER_AXIS_MASS], slider.upperTranslation - translation, state[SLIDER_LIMIT_BIAS], SLIDER_UPPER_IMPULSE);
598
+ slider.lowerLimitImpulse = state[SLIDER_LOWER_IMPULSE];
599
+ slider.upperLimitImpulse = state[SLIDER_UPPER_IMPULSE];
600
+ }
601
+ solveAngularBlock(bodyA, bodyB, joint, state, SLIDER_ANGULAR_MASS, SLIDER_ANGULAR_BIAS);
602
+ solveEqualityRow(bodyA, bodyB, joint, 0, state, SLIDER_PERP_ROW0, state[SLIDER_PERP_MASS0], state[SLIDER_PERP_ERROR0], state[SLIDER_LOCK_BIAS]);
603
+ solveEqualityRow(bodyA, bodyB, joint, 1, state, SLIDER_PERP_ROW1, state[SLIDER_PERP_MASS1], state[SLIDER_PERP_ERROR1], state[SLIDER_LOCK_BIAS]);
604
+ },
605
+ warmStart(world, joint) {
606
+ const slider = joint;
607
+ const state = getJointSolveState(joint);
608
+ if (state === undefined)
609
+ return;
610
+ const bodyA = findPhysics3DBody(world, joint.bodyA);
611
+ const bodyB = findPhysics3DBody(world, joint.bodyB);
612
+ if (bodyA === null || bodyB === null)
613
+ return;
614
+ warmStartAngularBlock(bodyA, bodyB, joint);
615
+ applyRow(bodyA, bodyB, state, SLIDER_PERP_ROW0, joint.impulse0);
616
+ applyRow(bodyA, bodyB, state, SLIDER_PERP_ROW1, joint.impulse1);
617
+ if (slider.enableMotor)
618
+ applyRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, slider.motorImpulse);
619
+ if (slider.enableLimit) {
620
+ applyRow(bodyA, bodyB, state, SLIDER_AXIS_ROW, slider.lowerLimitImpulse - slider.upperLimitImpulse);
621
+ }
622
+ },
623
+ };
624
+ function clamp(value, low, high) {
625
+ return Math.min(Math.max(value, low), high);
626
+ }
627
+ function dot(a, aOffset, b, bOffset) {
628
+ return a[aOffset] * b[bOffset] + a[aOffset + 1] * b[bOffset + 1] + a[aOffset + 2] * b[bOffset + 2];
629
+ }
630
+ // The signed angle from frame A's Y axis to frame B's, measured about frame A's X axis. Exact for any pose,
631
+ // which is why the limits read this rather than the axis component of the rotation error: that component is
632
+ // the true angle only while the two lock rows are converged, so a hinge under load would find its stops
633
+ // drifting with how hard it was being pushed.
634
+ function getSignedAxisAngle(basisA, basisB) {
635
+ const crossX = basisA[4] * basisB[5] - basisA[5] * basisB[4];
636
+ const crossY = basisA[5] * basisB[3] - basisA[3] * basisB[5];
637
+ const crossZ = basisA[3] * basisB[4] - basisA[4] * basisB[3];
638
+ const sine = crossX * basisA[0] + crossY * basisA[1] + crossZ * basisA[2];
639
+ return Math.atan2(sine, dot(basisA, 3, basisB, 3));
640
+ }
641
+ const BAUMGARTE = 0.2;
642
+ const FIXED_ANGULAR_MASS = POINT_LENGTH;
643
+ const FIXED_ANGULAR_BIAS = FIXED_ANGULAR_MASS + 6;
644
+ const FIXED_LENGTH = FIXED_ANGULAR_BIAS + 3;
645
+ const HINGE_AXIS_ROW = POINT_LENGTH;
646
+ const HINGE_LOCK_ROW0 = HINGE_AXIS_ROW + ROW_LENGTH;
647
+ const HINGE_LOCK_ROW1 = HINGE_LOCK_ROW0 + ROW_LENGTH;
648
+ const HINGE_AXIS_MASS = HINGE_LOCK_ROW1 + ROW_LENGTH;
649
+ const HINGE_LOCK_MASS0 = HINGE_AXIS_MASS + 1;
650
+ const HINGE_LOCK_MASS1 = HINGE_LOCK_MASS0 + 1;
651
+ const HINGE_LOCK_ERROR0 = HINGE_LOCK_MASS1 + 1;
652
+ const HINGE_LOCK_ERROR1 = HINGE_LOCK_ERROR0 + 1;
653
+ const HINGE_ANGLE = HINGE_LOCK_ERROR1 + 1;
654
+ const HINGE_LOCK_BIAS = HINGE_ANGLE + 1;
655
+ const HINGE_LIMIT_BIAS = HINGE_LOCK_BIAS + 1;
656
+ const HINGE_MAX_MOTOR = HINGE_LIMIT_BIAS + 1;
657
+ const HINGE_LOWER_IMPULSE = HINGE_MAX_MOTOR + 1;
658
+ const HINGE_UPPER_IMPULSE = HINGE_LOWER_IMPULSE + 1;
659
+ const HINGE_LENGTH = HINGE_UPPER_IMPULSE + 1;
660
+ // The effective cone half-angle in the direction the twist axis has actually tilted. Equal limits give a
661
+ // circular cone; unequal ones give an ellipse, and the blend below is the ellipse in polar form — it returns
662
+ // `swingLimitY` for a tilt straight toward frame A's Y and `swingLimitZ` for one straight toward Z.
663
+ //
664
+ // A non-positive limit is a cone with no opening, and the smaller of the two is the honest reading of it: an
665
+ // ellipse with a zero semi-axis is a line segment, not an ellipse.
666
+ function getConeSwingLimit(cone) {
667
+ if (cone.swingLimitY <= 0 || cone.swingLimitZ <= 0)
668
+ return Math.min(cone.swingLimitY, cone.swingLimitZ);
669
+ const towardY = dot(frameBBasis, 0, frameABasis, 3);
670
+ const towardZ = dot(frameBBasis, 0, frameABasis, 6);
671
+ const tilt = Math.sqrt(towardY * towardY + towardZ * towardZ);
672
+ if (tilt <= AXIS_EPSILON)
673
+ return Math.min(cone.swingLimitY, cone.swingLimitZ);
674
+ const unitY = towardY / tilt / cone.swingLimitY;
675
+ const unitZ = towardZ / tilt / cone.swingLimitZ;
676
+ return 1 / Math.sqrt(unitY * unitY + unitZ * unitZ);
677
+ }
678
+ // Copies a 6-DOF joint's twelve bounds into the two scratch arrays, ordered linear X, Y, Z then angular X, Y,
679
+ // Z — the axis numbering every loop over the six degrees of freedom uses.
680
+ function readDofBounds(dof) {
681
+ dofLower[0] = dof.lowerLinearX;
682
+ dofLower[1] = dof.lowerLinearY;
683
+ dofLower[2] = dof.lowerLinearZ;
684
+ dofLower[3] = dof.lowerAngularX;
685
+ dofLower[4] = dof.lowerAngularY;
686
+ dofLower[5] = dof.lowerAngularZ;
687
+ dofUpper[0] = dof.upperLinearX;
688
+ dofUpper[1] = dof.upperLinearY;
689
+ dofUpper[2] = dof.upperLinearZ;
690
+ dofUpper[3] = dof.upperAngularX;
691
+ dofUpper[4] = dof.upperAngularY;
692
+ dofUpper[5] = dof.upperAngularZ;
693
+ }
694
+ // Writes the row for a linear coordinate measured along one of frame A's axes, reading the separation left by
695
+ // `writePhysics3DJointSeparation`.
696
+ //
697
+ // A's arm is `(rA + separation) x direction`, NOT `rA x direction`. The direction is carried by A and rotates
698
+ // with it, so the coordinate changes when A turns even if neither anchor moves; an arm taken from A's bare
699
+ // lever arm drops that term and leaks motion sideways whenever the rail body rotates.
700
+ function writeLinearAxisRow(state, offset, joint, basisOffset) {
701
+ const directionX = frameABasis[basisOffset];
702
+ const directionY = frameABasis[basisOffset + 1];
703
+ const directionZ = frameABasis[basisOffset + 2];
704
+ const armAX = joint.rAX + separation[0];
705
+ const armAY = joint.rAY + separation[1];
706
+ const armAZ = joint.rAZ + separation[2];
707
+ writeRow(state, offset, directionX, directionY, directionZ, armAY * directionZ - armAZ * directionY, armAZ * directionX - armAX * directionZ, armAX * directionY - armAY * directionX, joint.rBY * directionZ - joint.rBZ * directionY, joint.rBZ * directionX - joint.rBX * directionZ, joint.rBX * directionY - joint.rBY * directionX);
708
+ }
709
+ // Below this the swing axis is degenerate: the two twist axes are parallel, the joint sits at the centre of
710
+ // its cone, and there is no direction for a limit to act along.
711
+ const AXIS_EPSILON = 1e-9;
712
+ const CONE_SWING_ROW = POINT_LENGTH;
713
+ const CONE_TWIST_ROW = CONE_SWING_ROW + ROW_LENGTH;
714
+ const CONE_SWING_MASS = CONE_TWIST_ROW + ROW_LENGTH;
715
+ const CONE_SWING_ERROR = CONE_SWING_MASS + 1;
716
+ const CONE_SWING_ACTIVE = CONE_SWING_ERROR + 1;
717
+ const CONE_TWIST_MASS = CONE_SWING_ACTIVE + 1;
718
+ const CONE_TWIST_ANGLE = CONE_TWIST_MASS + 1;
719
+ const CONE_LIMIT_BIAS = CONE_TWIST_ANGLE + 1;
720
+ const CONE_SWING_IMPULSE = CONE_LIMIT_BIAS + 1;
721
+ const CONE_LOWER_TWIST_IMPULSE = CONE_SWING_IMPULSE + 1;
722
+ const CONE_UPPER_TWIST_IMPULSE = CONE_LOWER_TWIST_IMPULSE + 1;
723
+ const CONE_LENGTH = CONE_UPPER_TWIST_IMPULSE + 1;
724
+ const DOF_FREE = 0;
725
+ const DOF_LOCKED = 1;
726
+ const DOF_LIMITED = 2;
727
+ const DOF_ROWS = 0;
728
+ const DOF_MASS = DOF_ROWS + 6 * ROW_LENGTH;
729
+ const DOF_VALUE = DOF_MASS + 6;
730
+ const DOF_MODE = DOF_VALUE + 6;
731
+ const DOF_LOWER_IMPULSE = DOF_MODE + 6;
732
+ const DOF_UPPER_IMPULSE = DOF_LOWER_IMPULSE + 6;
733
+ const DOF_LOCK_BIAS = DOF_UPPER_IMPULSE + 6;
734
+ const DOF_LIMIT_BIAS = DOF_LOCK_BIAS + 1;
735
+ const DOF_LENGTH = DOF_LIMIT_BIAS + 1;
736
+ const SLIDER_PERP_ROW0 = 0;
737
+ const SLIDER_PERP_ROW1 = SLIDER_PERP_ROW0 + ROW_LENGTH;
738
+ const SLIDER_AXIS_ROW = SLIDER_PERP_ROW1 + ROW_LENGTH;
739
+ const SLIDER_ANGULAR_MASS = SLIDER_AXIS_ROW + ROW_LENGTH;
740
+ const SLIDER_ANGULAR_BIAS = SLIDER_ANGULAR_MASS + 6;
741
+ const SLIDER_PERP_MASS0 = SLIDER_ANGULAR_BIAS + 3;
742
+ const SLIDER_PERP_MASS1 = SLIDER_PERP_MASS0 + 1;
743
+ const SLIDER_PERP_ERROR0 = SLIDER_PERP_MASS1 + 1;
744
+ const SLIDER_PERP_ERROR1 = SLIDER_PERP_ERROR0 + 1;
745
+ const SLIDER_AXIS_MASS = SLIDER_PERP_ERROR1 + 1;
746
+ const SLIDER_TRANSLATION = SLIDER_AXIS_MASS + 1;
747
+ const SLIDER_LOCK_BIAS = SLIDER_TRANSLATION + 1;
748
+ const SLIDER_LIMIT_BIAS = SLIDER_LOCK_BIAS + 1;
749
+ const SLIDER_MAX_MOTOR = SLIDER_LIMIT_BIAS + 1;
750
+ const SLIDER_LOWER_IMPULSE = SLIDER_MAX_MOTOR + 1;
751
+ const SLIDER_UPPER_IMPULSE = SLIDER_LOWER_IMPULSE + 1;
752
+ const SLIDER_LENGTH = SLIDER_UPPER_IMPULSE + 1;
753
+ const dofLower = [0, 0, 0, 0, 0, 0];
754
+ const dofUpper = [0, 0, 0, 0, 0, 0];
755
+ const relativeRotation = [0, 0, 0, 1];
756
+ const rotationError = [0, 0, 0];
757
+ const separation = [0, 0, 0];
758
+ //# sourceMappingURL=joints.js.map