@flighthq/physics3d 0.4.0-edge.1908.751021c
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/dist/broadphase.d.ts +4 -0
- package/dist/broadphase.d.ts.map +1 -0
- package/dist/broadphase.js +158 -0
- package/dist/broadphase.js.map +1 -0
- package/dist/colliderCollision.d.ts +5 -0
- package/dist/colliderCollision.d.ts.map +1 -0
- package/dist/colliderCollision.js +55 -0
- package/dist/colliderCollision.js.map +1 -0
- package/dist/colliderTransform.d.ts +5 -0
- package/dist/colliderTransform.d.ts.map +1 -0
- package/dist/colliderTransform.js +424 -0
- package/dist/colliderTransform.js.map +1 -0
- package/dist/contactIntake.d.ts +5 -0
- package/dist/contactIntake.d.ts.map +1 -0
- package/dist/contactIntake.js +237 -0
- package/dist/contactIntake.js.map +1 -0
- package/dist/contacts.d.ts +4 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +64 -0
- package/dist/contacts.js.map +1 -0
- package/dist/continuous.d.ts +5 -0
- package/dist/continuous.d.ts.map +1 -0
- package/dist/continuous.js +829 -0
- package/dist/continuous.js.map +1 -0
- package/dist/contract.d.ts +28 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +28 -0
- package/dist/contract.js.map +1 -0
- package/dist/debugGeometry.d.ts +4 -0
- package/dist/debugGeometry.d.ts.map +1 -0
- package/dist/debugGeometry.js +333 -0
- package/dist/debugGeometry.js.map +1 -0
- package/dist/enablePhysics3DGuards.d.ts +4 -0
- package/dist/enablePhysics3DGuards.d.ts.map +1 -0
- package/dist/enablePhysics3DGuards.js +155 -0
- package/dist/enablePhysics3DGuards.js.map +1 -0
- package/dist/explainPhysics3DCollision.d.ts +3 -0
- package/dist/explainPhysics3DCollision.d.ts.map +1 -0
- package/dist/explainPhysics3DCollision.js +30 -0
- package/dist/explainPhysics3DCollision.js.map +1 -0
- package/dist/explainPhysics3DJoints.d.ts +3 -0
- package/dist/explainPhysics3DJoints.d.ts.map +1 -0
- package/dist/explainPhysics3DJoints.js +32 -0
- package/dist/explainPhysics3DJoints.js.map +1 -0
- package/dist/explainPhysics3DStep.d.ts +3 -0
- package/dist/explainPhysics3DStep.d.ts.map +1 -0
- package/dist/explainPhysics3DStep.js +51 -0
- package/dist/explainPhysics3DStep.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/integrate.d.ts +6 -0
- package/dist/integrate.d.ts.map +1 -0
- package/dist/integrate.js +184 -0
- package/dist/integrate.js.map +1 -0
- package/dist/islands.d.ts +6 -0
- package/dist/islands.d.ts.map +1 -0
- package/dist/islands.js +333 -0
- package/dist/islands.js.map +1 -0
- package/dist/jointBreakage.d.ts +5 -0
- package/dist/jointBreakage.d.ts.map +1 -0
- package/dist/jointBreakage.js +44 -0
- package/dist/jointBreakage.js.map +1 -0
- package/dist/jointCollisionSuppression.d.ts +4 -0
- package/dist/jointCollisionSuppression.d.ts.map +1 -0
- package/dist/jointCollisionSuppression.js +40 -0
- package/dist/jointCollisionSuppression.js.map +1 -0
- package/dist/jointFactories.d.ts +9 -0
- package/dist/jointFactories.d.ts.map +1 -0
- package/dist/jointFactories.js +159 -0
- package/dist/jointFactories.js.map +1 -0
- package/dist/jointMath.d.ts +17 -0
- package/dist/jointMath.d.ts.map +1 -0
- package/dist/jointMath.js +361 -0
- package/dist/jointMath.js.map +1 -0
- package/dist/jointReaction.d.ts +8 -0
- package/dist/jointReaction.d.ts.map +1 -0
- package/dist/jointReaction.js +65 -0
- package/dist/jointReaction.js.map +1 -0
- package/dist/jointRegistry.d.ts +8 -0
- package/dist/jointRegistry.d.ts.map +1 -0
- package/dist/jointRegistry.js +144 -0
- package/dist/jointRegistry.js.map +1 -0
- package/dist/jointRows.d.ts +33 -0
- package/dist/jointRows.d.ts.map +1 -0
- package/dist/jointRows.js +352 -0
- package/dist/jointRows.js.map +1 -0
- package/dist/joints.d.ts +16 -0
- package/dist/joints.d.ts.map +1 -0
- package/dist/joints.js +1060 -0
- package/dist/joints.js.map +1 -0
- package/dist/massProperties.d.ts +13 -0
- package/dist/massProperties.d.ts.map +1 -0
- package/dist/massProperties.js +462 -0
- package/dist/massProperties.js.map +1 -0
- package/dist/material.d.ts +3 -0
- package/dist/material.d.ts.map +1 -0
- package/dist/material.js +13 -0
- package/dist/material.js.map +1 -0
- package/dist/nodeSync.d.ts +3 -0
- package/dist/nodeSync.d.ts.map +1 -0
- package/dist/nodeSync.js +16 -0
- package/dist/nodeSync.js.map +1 -0
- package/dist/ownership.d.ts +8 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/ownership.js +26 -0
- package/dist/ownership.js.map +1 -0
- package/dist/physics3DBroadphasePublication.d.ts +5 -0
- package/dist/physics3DBroadphasePublication.d.ts.map +1 -0
- package/dist/physics3DBroadphasePublication.js +49 -0
- package/dist/physics3DBroadphasePublication.js.map +1 -0
- package/dist/physics3DSpatialIndexingGuards.d.ts +4 -0
- package/dist/physics3DSpatialIndexingGuards.d.ts.map +1 -0
- package/dist/physics3DSpatialIndexingGuards.js +11 -0
- package/dist/physics3DSpatialIndexingGuards.js.map +1 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.d.ts +3 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.d.ts.map +1 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.js +15 -0
- package/dist/registerBuiltInPhysics3DJointSolvers.js.map +1 -0
- package/dist/solver.d.ts +8 -0
- package/dist/solver.d.ts.map +1 -0
- package/dist/solver.js +460 -0
- package/dist/solver.js.map +1 -0
- package/dist/step.d.ts +5 -0
- package/dist/step.d.ts.map +1 -0
- package/dist/step.js +302 -0
- package/dist/step.js.map +1 -0
- package/dist/stepValidation.d.ts +13 -0
- package/dist/stepValidation.d.ts.map +1 -0
- package/dist/stepValidation.js +284 -0
- package/dist/stepValidation.js.map +1 -0
- package/dist/symmetricTensor.d.ts +11 -0
- package/dist/symmetricTensor.d.ts.map +1 -0
- package/dist/symmetricTensor.js +150 -0
- package/dist/symmetricTensor.js.map +1 -0
- package/dist/world.d.ts +27 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +762 -0
- package/dist/world.js.map +1 -0
- package/dist/worldQueries.d.ts +11 -0
- package/dist/worldQueries.d.ts.map +1 -0
- package/dist/worldQueries.js +409 -0
- package/dist/worldQueries.js.map +1 -0
- package/package.json +55 -0
- package/src/broadphase.test.ts +265 -0
- package/src/colliderCollision.test.ts +57 -0
- package/src/colliderTransform.test.ts +221 -0
- package/src/contactIntake.test.ts +370 -0
- package/src/contacts.test.ts +64 -0
- package/src/continuous.test.ts +679 -0
- package/src/debugGeometry.test.ts +288 -0
- package/src/enablePhysics3DGuards.test.ts +206 -0
- package/src/explainPhysics3DCollision.test.ts +78 -0
- package/src/explainPhysics3DJoints.test.ts +101 -0
- package/src/explainPhysics3DStep.test.ts +142 -0
- package/src/integrate.test.ts +279 -0
- package/src/islands.test.ts +384 -0
- package/src/jointBreakage.test.ts +143 -0
- package/src/jointCollisionSuppression.test.ts +94 -0
- package/src/jointFactories.test.ts +244 -0
- package/src/jointMath.test.ts +646 -0
- package/src/jointReaction.test.ts +241 -0
- package/src/jointRegistry.test.ts +229 -0
- package/src/jointRows.test.ts +548 -0
- package/src/joints.test.ts +1066 -0
- package/src/massProperties.test.ts +611 -0
- package/src/material.test.ts +40 -0
- package/src/nodeSync.test.ts +50 -0
- package/src/ownership.test.ts +61 -0
- package/src/physics3DBroadphasePublication.test.ts +63 -0
- package/src/physics3DSpatialIndexingGuards.test.ts +43 -0
- package/src/registerBuiltInPhysics3DJointSolvers.test.ts +51 -0
- package/src/solver.test.ts +567 -0
- package/src/step.test.ts +490 -0
- package/src/stepValidation.test.ts +346 -0
- package/src/stress.test.ts +526 -0
- package/src/symmetricTensor.test.ts +166 -0
- package/src/world.test.ts +1127 -0
- package/src/worldQueries.test.ts +565 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import type { Physics3DFixedJoint, Physics3DJoint, RigidBody3D } from '@flighthq/types/contract';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { refreshRigidBody3DWorldInertia } from './integrate';
|
|
5
|
+
import { createPhysics3DFixedJoint } from './jointFactories';
|
|
6
|
+
import {
|
|
7
|
+
applyRow,
|
|
8
|
+
beginJointSolve,
|
|
9
|
+
clearJointSolve,
|
|
10
|
+
frameABasis,
|
|
11
|
+
frameARotation,
|
|
12
|
+
frameBBasis,
|
|
13
|
+
frameBRotation,
|
|
14
|
+
getJointSolveState,
|
|
15
|
+
getRowMass,
|
|
16
|
+
getRowVelocity,
|
|
17
|
+
POINT_BIAS,
|
|
18
|
+
POINT_LENGTH,
|
|
19
|
+
POINT_MASS,
|
|
20
|
+
prepareAngularBlock,
|
|
21
|
+
preparePointBlock,
|
|
22
|
+
readFrameBases,
|
|
23
|
+
readFrameRotations,
|
|
24
|
+
readJointImpulse,
|
|
25
|
+
ROW_LENGTH,
|
|
26
|
+
solveAngularBlock,
|
|
27
|
+
solveEqualityRow,
|
|
28
|
+
solveLowerLimitRow,
|
|
29
|
+
solveMotorRow,
|
|
30
|
+
solvePointBlock,
|
|
31
|
+
solveUpperLimitRow,
|
|
32
|
+
warmStartAngularBlock,
|
|
33
|
+
warmStartPointBlock,
|
|
34
|
+
writeAngularRow,
|
|
35
|
+
writeJointImpulse,
|
|
36
|
+
writeRow,
|
|
37
|
+
writePhysics3DSoftRowParameters,
|
|
38
|
+
} from './jointRows';
|
|
39
|
+
import { createPhysics3DMassData, setRigidBody3DMassData } from './massProperties';
|
|
40
|
+
import { createRigidBody3D } from './world';
|
|
41
|
+
|
|
42
|
+
describe('applyRow', () => {
|
|
43
|
+
it('drives a row toward rest when scaled by the row mass', () => {
|
|
44
|
+
const bodyA = createUnitBody();
|
|
45
|
+
const bodyB = createUnitBody();
|
|
46
|
+
bodyB.velocityX = 2;
|
|
47
|
+
const state = newState();
|
|
48
|
+
writeRow(state, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
49
|
+
|
|
50
|
+
applyRow(bodyA, bodyB, state, 0, -getRowVelocity(bodyA, bodyB, state, 0) * getRowMass(bodyA, bodyB, state, 0));
|
|
51
|
+
|
|
52
|
+
expect(getRowVelocity(bodyA, bodyB, state, 0)).toBeCloseTo(0, 12);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('does nothing for a zero impulse', () => {
|
|
56
|
+
const bodyA = createUnitBody();
|
|
57
|
+
const bodyB = createUnitBody();
|
|
58
|
+
const state = newState();
|
|
59
|
+
writeRow(state, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
60
|
+
|
|
61
|
+
applyRow(bodyA, bodyB, state, 0, 0);
|
|
62
|
+
|
|
63
|
+
expect(bodyB.velocityX).toBe(0);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('beginJointSolve', () => {
|
|
68
|
+
it('sizes the scratch and hands the same array back next time', () => {
|
|
69
|
+
const joint = createTestJoint();
|
|
70
|
+
|
|
71
|
+
const first = beginJointSolve(joint, 5);
|
|
72
|
+
first[0] = 9;
|
|
73
|
+
const second = beginJointSolve(joint, 8);
|
|
74
|
+
|
|
75
|
+
expect(second).toBe(first);
|
|
76
|
+
expect(second.length).toBe(8);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('establishes accumulators a reconstructed joint reached the solver without', () => {
|
|
80
|
+
const joint = createTestJoint();
|
|
81
|
+
// A joint deserialized from a saved world satisfies the required type at compile time only; at runtime the
|
|
82
|
+
// fields are simply absent, and the first `undefined + x` would send NaN out through both bodies.
|
|
83
|
+
(joint as Partial<Physics3DJoint>).impulse0 = undefined;
|
|
84
|
+
(joint as Partial<Physics3DJoint>).impulse5 = undefined;
|
|
85
|
+
|
|
86
|
+
beginJointSolve(joint, 4);
|
|
87
|
+
|
|
88
|
+
expect(joint.impulse0).toBe(0);
|
|
89
|
+
expect(joint.impulse5).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('clearJointSolve', () => {
|
|
94
|
+
it('leaves a solve with nothing to read', () => {
|
|
95
|
+
const joint = createTestJoint();
|
|
96
|
+
beginJointSolve(joint, 4);
|
|
97
|
+
|
|
98
|
+
clearJointSolve(joint);
|
|
99
|
+
|
|
100
|
+
expect(getJointSolveState(joint)).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('frameABasis', () => {
|
|
105
|
+
it('holds nine numbers, three per axis', () => {
|
|
106
|
+
expect(frameABasis.length).toBe(9);
|
|
107
|
+
expect(frameBBasis.length).toBe(9);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('frameARotation', () => {
|
|
112
|
+
it('holds a quaternion, and starts as the identity', () => {
|
|
113
|
+
expect(frameARotation.length).toBe(4);
|
|
114
|
+
expect(frameBRotation.length).toBe(4);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('frameBBasis', () => {
|
|
119
|
+
it('is a distinct array from the frame A basis', () => {
|
|
120
|
+
expect(frameBBasis).not.toBe(frameABasis);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('frameBRotation', () => {
|
|
125
|
+
it('is a distinct array from the frame A rotation', () => {
|
|
126
|
+
expect(frameBRotation).not.toBe(frameARotation);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('getJointSolveState', () => {
|
|
131
|
+
it('is undefined until a prepare has run', () => {
|
|
132
|
+
expect(getJointSolveState(createTestJoint())).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('getRowMass', () => {
|
|
137
|
+
it('reads the row at the given offset rather than the head of the state', () => {
|
|
138
|
+
const bodyA = createUnitBody();
|
|
139
|
+
const bodyB = createUnitBody();
|
|
140
|
+
const state = newState();
|
|
141
|
+
writeRow(state, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
142
|
+
writeRow(state, ROW_LENGTH, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
143
|
+
|
|
144
|
+
expect(getRowMass(bodyA, bodyB, state, 0)).toBe(0);
|
|
145
|
+
expect(getRowMass(bodyA, bodyB, state, ROW_LENGTH)).toBeCloseTo(0.5, 12);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('getRowVelocity', () => {
|
|
150
|
+
it('measures B relative to A along the row', () => {
|
|
151
|
+
const bodyA = createUnitBody();
|
|
152
|
+
const bodyB = createUnitBody();
|
|
153
|
+
bodyB.velocityY = 3;
|
|
154
|
+
const state = newState();
|
|
155
|
+
writeRow(state, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0);
|
|
156
|
+
|
|
157
|
+
expect(getRowVelocity(bodyA, bodyB, state, 0)).toBeCloseTo(3, 12);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('POINT_BIAS', () => {
|
|
162
|
+
it('follows the six-number mass block', () => {
|
|
163
|
+
expect(POINT_BIAS).toBe(POINT_MASS + 6);
|
|
164
|
+
expect(POINT_LENGTH).toBe(POINT_BIAS + 3);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('POINT_LENGTH', () => {
|
|
169
|
+
it('covers the whole point block', () => {
|
|
170
|
+
expect(POINT_LENGTH).toBe(9);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('POINT_MASS', () => {
|
|
175
|
+
it('sits at the head of the per-step state', () => {
|
|
176
|
+
expect(POINT_MASS).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('prepareAngularBlock', () => {
|
|
181
|
+
it('reads the frames left by readFrameRotations', () => {
|
|
182
|
+
const bodyA = createUnitBody();
|
|
183
|
+
const bodyB = createUnitBody();
|
|
184
|
+
setAxisAngle(bodyB, 0, 0, 1, 0.4);
|
|
185
|
+
const joint = createPhysics3DFixedJoint({ bodyA: 0, bodyB: 1 });
|
|
186
|
+
const state = beginJointSolve(joint, 32);
|
|
187
|
+
|
|
188
|
+
readFrameRotations(bodyA, bodyB, joint);
|
|
189
|
+
prepareAngularBlock(bodyA, bodyB, state, 0, 6, 1 / 60);
|
|
190
|
+
|
|
191
|
+
// The bias is the rotation error scaled by BAUMGARTE / dt: 0.4 * 0.2 * 60.
|
|
192
|
+
expect(state[8]).toBeCloseTo(0.4 * 0.2 * 60, 9);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('preparePointBlock', () => {
|
|
197
|
+
it('writes the lever arms and the inverted mass block', () => {
|
|
198
|
+
const bodyA = createUnitBody();
|
|
199
|
+
const bodyB = createUnitBody();
|
|
200
|
+
bodyB.x = 3;
|
|
201
|
+
const joint = createTestJoint();
|
|
202
|
+
joint.localAnchorAX = 1;
|
|
203
|
+
const state = newState();
|
|
204
|
+
|
|
205
|
+
preparePointBlock(bodyA, bodyB, joint, state, 1 / 60);
|
|
206
|
+
|
|
207
|
+
expect(joint.rAX).toBeCloseTo(1, 12);
|
|
208
|
+
// Both anchors are on their centres for B and one unit out on A, and the inverse of the diagonal 2 that a
|
|
209
|
+
// zero-arm pair would give is 0.5.
|
|
210
|
+
expect(state[POINT_MASS]).toBeCloseTo(0.5, 12);
|
|
211
|
+
expect(state[POINT_BIAS]).toBeCloseTo(2 * 0.2 * 60, 9);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('readFrameBases', () => {
|
|
216
|
+
it('expands both frames into world axes', () => {
|
|
217
|
+
const bodyA = createUnitBody();
|
|
218
|
+
setAxisAngle(bodyA, 0, 0, 1, Math.PI / 2);
|
|
219
|
+
const bodyB = createUnitBody();
|
|
220
|
+
|
|
221
|
+
readFrameBases(bodyA, bodyB, createPhysics3DFixedJoint({ bodyA: 0, bodyB: 1 }));
|
|
222
|
+
|
|
223
|
+
expect(frameABasis[0]).toBeCloseTo(0, 12);
|
|
224
|
+
expect(frameABasis[1]).toBeCloseTo(1, 12);
|
|
225
|
+
expect(frameBBasis[0]).toBeCloseTo(1, 12);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('readFrameRotations', () => {
|
|
230
|
+
it('establishes an absent frame as the identity', () => {
|
|
231
|
+
const bodyA = createUnitBody();
|
|
232
|
+
const bodyB = createUnitBody();
|
|
233
|
+
const joint = createPhysics3DFixedJoint({ bodyA: 0, bodyB: 1 });
|
|
234
|
+
(joint as Partial<Physics3DFixedJoint>).localRotationAW = undefined;
|
|
235
|
+
(joint as Partial<Physics3DFixedJoint>).localRotationAX = undefined;
|
|
236
|
+
|
|
237
|
+
readFrameRotations(bodyA, bodyB, joint);
|
|
238
|
+
|
|
239
|
+
expect(joint.localRotationAW).toBe(1);
|
|
240
|
+
expect(frameARotation[3]).toBeCloseTo(1, 12);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('readJointImpulse', () => {
|
|
245
|
+
it('reads each of the six slots', () => {
|
|
246
|
+
const joint = createTestJoint();
|
|
247
|
+
joint.impulse0 = 10;
|
|
248
|
+
joint.impulse3 = 13;
|
|
249
|
+
joint.impulse5 = 15;
|
|
250
|
+
|
|
251
|
+
expect(readJointImpulse(joint, 0)).toBe(10);
|
|
252
|
+
expect(readJointImpulse(joint, 3)).toBe(13);
|
|
253
|
+
expect(readJointImpulse(joint, 5)).toBe(15);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('ROW_LENGTH', () => {
|
|
258
|
+
it('is a direction plus two angular arms', () => {
|
|
259
|
+
expect(ROW_LENGTH).toBe(9);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('solveAngularBlock', () => {
|
|
264
|
+
it('cancels relative rotation and accumulates into the upper three slots', () => {
|
|
265
|
+
const bodyA = createUnitBody();
|
|
266
|
+
const bodyB = createUnitBody();
|
|
267
|
+
bodyB.angularVelocityY = 2;
|
|
268
|
+
const joint = createPhysics3DFixedJoint({ bodyA: 0, bodyB: 1 });
|
|
269
|
+
const state = beginJointSolve(joint, 32);
|
|
270
|
+
readFrameRotations(bodyA, bodyB, joint);
|
|
271
|
+
prepareAngularBlock(bodyA, bodyB, state, 0, 6, 1 / 60);
|
|
272
|
+
|
|
273
|
+
solveAngularBlock(bodyA, bodyB, joint, state, 0, 6);
|
|
274
|
+
|
|
275
|
+
expect(bodyB.angularVelocityY - bodyA.angularVelocityY).toBeCloseTo(0, 12);
|
|
276
|
+
expect(joint.impulse4).not.toBe(0);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('solveEqualityRow', () => {
|
|
281
|
+
it('drives the row to the bias velocity and accumulates into its slot', () => {
|
|
282
|
+
const bodyA = createUnitBody();
|
|
283
|
+
const bodyB = createUnitBody();
|
|
284
|
+
bodyB.velocityX = 2;
|
|
285
|
+
const joint = createTestJoint();
|
|
286
|
+
const state = newState();
|
|
287
|
+
writeRow(state, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
288
|
+
|
|
289
|
+
solveEqualityRow(bodyA, bodyB, joint, 2, state, 0, getRowMass(bodyA, bodyB, state, 0), 0, 0);
|
|
290
|
+
|
|
291
|
+
expect(getRowVelocity(bodyA, bodyB, state, 0)).toBeCloseTo(0, 12);
|
|
292
|
+
expect(joint.impulse2).toBeCloseTo(-1, 12);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('solveLowerLimitRow', () => {
|
|
297
|
+
it('pushes a crossed bound back up', () => {
|
|
298
|
+
const bodyA = createUnitBody();
|
|
299
|
+
const bodyB = createUnitBody();
|
|
300
|
+
const state = newState();
|
|
301
|
+
writeRow(state, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
302
|
+
|
|
303
|
+
solveLowerLimitRow(bodyA, bodyB, state, 0, getRowMass(bodyA, bodyB, state, 0), -1, 60, ROW_LENGTH);
|
|
304
|
+
|
|
305
|
+
expect(getRowVelocity(bodyA, bodyB, state, 0)).toBeGreaterThan(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('never pulls a coordinate down through its bound', () => {
|
|
309
|
+
const bodyA = createUnitBody();
|
|
310
|
+
const bodyB = createUnitBody();
|
|
311
|
+
bodyB.velocityX = 5;
|
|
312
|
+
const state = newState();
|
|
313
|
+
writeRow(state, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
314
|
+
|
|
315
|
+
solveLowerLimitRow(bodyA, bodyB, state, 0, getRowMass(bodyA, bodyB, state, 0), 3, 60, ROW_LENGTH);
|
|
316
|
+
|
|
317
|
+
expect(bodyB.velocityX).toBe(5);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('solveMotorRow', () => {
|
|
322
|
+
it('returns the accumulated impulse clamped to its budget', () => {
|
|
323
|
+
const bodyA = createUnitBody();
|
|
324
|
+
const bodyB = createUnitBody();
|
|
325
|
+
const state = newState();
|
|
326
|
+
writeRow(state, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
327
|
+
|
|
328
|
+
const total = solveMotorRow(bodyA, bodyB, state, 0, getRowMass(bodyA, bodyB, state, 0), 100, 0.25, 0);
|
|
329
|
+
|
|
330
|
+
expect(total).toBeCloseTo(0.25, 12);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('reaches its target speed when the budget allows', () => {
|
|
334
|
+
const bodyA = createUnitBody();
|
|
335
|
+
const bodyB = createUnitBody();
|
|
336
|
+
const state = newState();
|
|
337
|
+
writeRow(state, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
338
|
+
|
|
339
|
+
solveMotorRow(bodyA, bodyB, state, 0, getRowMass(bodyA, bodyB, state, 0), 4, 1000, 0);
|
|
340
|
+
|
|
341
|
+
expect(getRowVelocity(bodyA, bodyB, state, 0)).toBeCloseTo(4, 12);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('solvePointBlock', () => {
|
|
346
|
+
it('cancels the velocity separating the anchors', () => {
|
|
347
|
+
const bodyA = createUnitBody();
|
|
348
|
+
const bodyB = createUnitBody();
|
|
349
|
+
bodyB.velocityX = 2;
|
|
350
|
+
bodyB.velocityZ = -1;
|
|
351
|
+
const joint = createTestJoint();
|
|
352
|
+
const state = newState();
|
|
353
|
+
preparePointBlock(bodyA, bodyB, joint, state, 1 / 60);
|
|
354
|
+
|
|
355
|
+
solvePointBlock(bodyA, bodyB, joint, state);
|
|
356
|
+
|
|
357
|
+
expect(bodyB.velocityX - bodyA.velocityX).toBeCloseTo(0, 12);
|
|
358
|
+
expect(bodyB.velocityZ - bodyA.velocityZ).toBeCloseTo(0, 12);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe('solveUpperLimitRow', () => {
|
|
363
|
+
it('pushes a crossed bound back down', () => {
|
|
364
|
+
const bodyA = createUnitBody();
|
|
365
|
+
const bodyB = createUnitBody();
|
|
366
|
+
const state = newState();
|
|
367
|
+
writeRow(state, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
368
|
+
|
|
369
|
+
solveUpperLimitRow(bodyA, bodyB, state, 0, getRowMass(bodyA, bodyB, state, 0), -1, 60, ROW_LENGTH);
|
|
370
|
+
|
|
371
|
+
expect(getRowVelocity(bodyA, bodyB, state, 0)).toBeLessThan(0);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('never pushes a coordinate up through its bound', () => {
|
|
375
|
+
const bodyA = createUnitBody();
|
|
376
|
+
const bodyB = createUnitBody();
|
|
377
|
+
bodyB.velocityX = -5;
|
|
378
|
+
const state = newState();
|
|
379
|
+
writeRow(state, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
380
|
+
|
|
381
|
+
solveUpperLimitRow(bodyA, bodyB, state, 0, getRowMass(bodyA, bodyB, state, 0), 3, 60, ROW_LENGTH);
|
|
382
|
+
|
|
383
|
+
expect(bodyB.velocityX).toBe(-5);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('warmStartAngularBlock', () => {
|
|
388
|
+
it('reapplies the angular accumulators', () => {
|
|
389
|
+
const bodyA = createUnitBody();
|
|
390
|
+
const bodyB = createUnitBody();
|
|
391
|
+
const joint = createTestJoint();
|
|
392
|
+
joint.impulse4 = 2;
|
|
393
|
+
|
|
394
|
+
warmStartAngularBlock(bodyA, bodyB, joint);
|
|
395
|
+
|
|
396
|
+
expect(bodyB.angularVelocityY).toBeCloseTo(2, 12);
|
|
397
|
+
expect(bodyA.angularVelocityY).toBeCloseTo(-2, 12);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe('warmStartPointBlock', () => {
|
|
402
|
+
it('reapplies the point accumulators', () => {
|
|
403
|
+
const bodyA = createUnitBody();
|
|
404
|
+
const bodyB = createUnitBody();
|
|
405
|
+
const joint = createTestJoint();
|
|
406
|
+
joint.impulse0 = 3;
|
|
407
|
+
|
|
408
|
+
warmStartPointBlock(bodyA, bodyB, joint);
|
|
409
|
+
|
|
410
|
+
expect(bodyB.velocityX).toBeCloseTo(3, 12);
|
|
411
|
+
expect(bodyA.velocityX).toBeCloseTo(-3, 12);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('writeAngularRow', () => {
|
|
416
|
+
it('writes a row with no linear direction and the axis as both arms', () => {
|
|
417
|
+
const state = newState();
|
|
418
|
+
|
|
419
|
+
writeAngularRow(state, 0, 0, 1, 0);
|
|
420
|
+
|
|
421
|
+
expect(state.slice(0, 9)).toEqual([0, 0, 0, 0, 1, 0, 0, 1, 0]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('measures the relative spin about its axis', () => {
|
|
425
|
+
const bodyA = createUnitBody();
|
|
426
|
+
const bodyB = createUnitBody();
|
|
427
|
+
bodyB.angularVelocityZ = 1.5;
|
|
428
|
+
const state = newState();
|
|
429
|
+
|
|
430
|
+
writeAngularRow(state, 0, 0, 0, 1);
|
|
431
|
+
|
|
432
|
+
expect(getRowVelocity(bodyA, bodyB, state, 0)).toBeCloseTo(1.5, 12);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('writeJointImpulse', () => {
|
|
437
|
+
it('writes each of the six slots', () => {
|
|
438
|
+
const joint = createTestJoint();
|
|
439
|
+
|
|
440
|
+
for (let slot = 0; slot < 6; slot += 1) writeJointImpulse(joint, slot, slot + 1);
|
|
441
|
+
|
|
442
|
+
expect([joint.impulse0, joint.impulse1, joint.impulse2, joint.impulse3, joint.impulse4, joint.impulse5]).toEqual([
|
|
443
|
+
1, 2, 3, 4, 5, 6,
|
|
444
|
+
]);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe('writePhysics3DSoftRowParameters', () => {
|
|
449
|
+
it('returns the hard parameters unchanged for a non-positive frequency', () => {
|
|
450
|
+
// "Spring enabled but never configured" must not be a third behaviour. This is what makes an
|
|
451
|
+
// unconfigured spring degrade to exactly the stop it replaced.
|
|
452
|
+
const out = [0, 0, 0];
|
|
453
|
+
writePhysics3DSoftRowParameters(4, 0, 0.7, 1 / 60, 60, out);
|
|
454
|
+
expect(out).toEqual([4, 60, 0]);
|
|
455
|
+
writePhysics3DSoftRowParameters(4, -1, 0.7, 1 / 60, 60, out);
|
|
456
|
+
expect(out).toEqual([4, 60, 0]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('uses the caller HARD bias factor rather than a baked-in constant', () => {
|
|
460
|
+
// The two callers legitimately disagree: a two-sided rest row corrects at BAUMGARTE/dt, a one-sided
|
|
461
|
+
// limit row fully at 1/dt. Baking either in would silently change the other's hard behaviour.
|
|
462
|
+
const out = [0, 0, 0];
|
|
463
|
+
writePhysics3DSoftRowParameters(1, 0, 0, 1 / 60, 12, out);
|
|
464
|
+
expect(out[1]).toBe(12);
|
|
465
|
+
writePhysics3DSoftRowParameters(1, 0, 0, 1 / 60, 60, out);
|
|
466
|
+
expect(out[1]).toBe(60);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('softens the mass, which is an addition on the RECIPROCAL side', () => {
|
|
470
|
+
// Compliance makes a constraint easier to violate, so it adds to the inverse mass. The softened mass
|
|
471
|
+
// is therefore always below the rigid one, never a scaled-up version of it.
|
|
472
|
+
const out = [0, 0, 0];
|
|
473
|
+
const mass = 5;
|
|
474
|
+
writePhysics3DSoftRowParameters(mass, 4, 0.7, 1 / 60, 60, out);
|
|
475
|
+
expect(out[0]).toBeGreaterThan(0);
|
|
476
|
+
expect(out[0]).toBeLessThan(mass);
|
|
477
|
+
const gamma = out[2];
|
|
478
|
+
expect(out[0]).toBeCloseTo(1 / (1 / mass + gamma), 12);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('produces a bias factor INDEPENDENT of mass', () => {
|
|
482
|
+
// The property the cone-twist and 6-DOF solvers rely on when several rows of different masses share
|
|
483
|
+
// one bias slot. If this were false, the last row prepared would corrupt the others.
|
|
484
|
+
const light = [0, 0, 0];
|
|
485
|
+
const heavy = [0, 0, 0];
|
|
486
|
+
writePhysics3DSoftRowParameters(0.25, 4, 0.7, 1 / 60, 60, light);
|
|
487
|
+
writePhysics3DSoftRowParameters(400, 4, 0.7, 1 / 60, 60, heavy);
|
|
488
|
+
expect(heavy[1]).toBeCloseTo(light[1], 12);
|
|
489
|
+
// Gamma is NOT mass-independent — it scales as the reciprocal — which is why it gets a slot per row.
|
|
490
|
+
expect(heavy[2]).toBeLessThan(light[2]);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('stiffens toward the rigid mass as frequency rises', () => {
|
|
494
|
+
const slack = [0, 0, 0];
|
|
495
|
+
const stiff = [0, 0, 0];
|
|
496
|
+
writePhysics3DSoftRowParameters(2, 1, 0.7, 1 / 60, 60, slack);
|
|
497
|
+
writePhysics3DSoftRowParameters(2, 200, 0.7, 1 / 60, 60, stiff);
|
|
498
|
+
expect(stiff[0]).toBeGreaterThan(slack[0]);
|
|
499
|
+
expect(stiff[0]).toBeLessThanOrEqual(2);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('stays finite for a zero mass rather than dividing by it', () => {
|
|
503
|
+
const out = [0, 0, 0];
|
|
504
|
+
writePhysics3DSoftRowParameters(0, 4, 0.7, 1 / 60, 60, out);
|
|
505
|
+
expect(Number.isFinite(out[0])).toBe(true);
|
|
506
|
+
expect(Number.isFinite(out[1])).toBe(true);
|
|
507
|
+
expect(Number.isFinite(out[2])).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
function createTestJoint(): Physics3DJoint {
|
|
512
|
+
return createPhysics3DFixedJoint({ bodyA: 0, bodyB: 1 });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function createUnitBody(): RigidBody3D {
|
|
516
|
+
const body = createRigidBody3D('dynamic');
|
|
517
|
+
const data = createPhysics3DMassData();
|
|
518
|
+
data.mass = 1;
|
|
519
|
+
data.inertiaXX = 1;
|
|
520
|
+
data.inertiaYY = 1;
|
|
521
|
+
data.inertiaZZ = 1;
|
|
522
|
+
setRigidBody3DMassData(body, data);
|
|
523
|
+
refreshRigidBody3DWorldInertia(body);
|
|
524
|
+
return body;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function newState(): number[] {
|
|
528
|
+
return new Array<number>(32).fill(0);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function setAxisAngle(body: RigidBody3D, x: number, y: number, z: number, angle: number): void {
|
|
532
|
+
const sine = Math.sin(angle / 2);
|
|
533
|
+
body.orientationX = x * sine;
|
|
534
|
+
body.orientationY = y * sine;
|
|
535
|
+
body.orientationZ = z * sine;
|
|
536
|
+
body.orientationW = Math.cos(angle / 2);
|
|
537
|
+
refreshRigidBody3DWorldInertia(body);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
describe('writeRow', () => {
|
|
541
|
+
it('lays the direction and the two arms out in one block', () => {
|
|
542
|
+
const state = newState();
|
|
543
|
+
|
|
544
|
+
writeRow(state, ROW_LENGTH, 1, 2, 3, 4, 5, 6, 7, 8, 9);
|
|
545
|
+
|
|
546
|
+
expect(state.slice(ROW_LENGTH, ROW_LENGTH * 2)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
547
|
+
});
|
|
548
|
+
});
|