@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,241 @@
|
|
|
1
|
+
import type { Physics3DJointReaction, Physics3DWorld, RigidBody3D } from '@flighthq/types/contract';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { createPhysics3DBallAndSocketJoint, createPhysics3DDistanceJoint } from './jointFactories';
|
|
5
|
+
import {
|
|
6
|
+
accumulatePhysics3DJointRowReaction,
|
|
7
|
+
clearPhysics3DJointReaction,
|
|
8
|
+
createPhysics3DJointReaction,
|
|
9
|
+
getPhysics3DJointReactionForce,
|
|
10
|
+
getPhysics3DJointReactionTorque,
|
|
11
|
+
writePhysics3DJointReaction,
|
|
12
|
+
} from './jointReaction';
|
|
13
|
+
import { addPhysics3DJoint } from './jointRegistry';
|
|
14
|
+
import { createPhysics3DMassData, setRigidBody3DMassData } from './massProperties';
|
|
15
|
+
import { registerBuiltInPhysics3DJointSolvers } from './registerBuiltInPhysics3DJointSolvers';
|
|
16
|
+
import { stepPhysics3D } from './step';
|
|
17
|
+
import { addPhysics3DBody, createPhysics3DWorld, createRigidBody3D } from './world';
|
|
18
|
+
|
|
19
|
+
describe('accumulatePhysics3DJointRowReaction', () => {
|
|
20
|
+
it('reads a LINEAR row as pure force, with no torque at the anchor', () => {
|
|
21
|
+
// The subtraction that makes the two readings independent. This row acts along +x through an anchor
|
|
22
|
+
// offset in +y, so it exerts a real torque about the centre of mass — but none about the ANCHOR, and
|
|
23
|
+
// a breakTorque must not fire on it.
|
|
24
|
+
const joint = createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 });
|
|
25
|
+
joint.rBX = 0;
|
|
26
|
+
joint.rBY = 2;
|
|
27
|
+
joint.rBZ = 0;
|
|
28
|
+
// direction +x, armB = rB x direction = (2,0,0) x ... = (0*0-0*0, 0*1-0*0, 0*0-2*1) = (0,0,-2)
|
|
29
|
+
const state = [1, 0, 0, 0, 0, 0, 0, 0, -2];
|
|
30
|
+
const out = createPhysics3DJointReaction();
|
|
31
|
+
|
|
32
|
+
accumulatePhysics3DJointRowReaction(joint, state, 0, 5, out);
|
|
33
|
+
|
|
34
|
+
expect(out.forceX).toBeCloseTo(5, 9);
|
|
35
|
+
expect(getPhysics3DJointReactionTorque(out)).toBeCloseTo(0, 9);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('reads a purely ANGULAR row as pure torque', () => {
|
|
39
|
+
const joint = createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 });
|
|
40
|
+
joint.rBX = 3;
|
|
41
|
+
joint.rBY = 1;
|
|
42
|
+
joint.rBZ = -2;
|
|
43
|
+
// A zero direction leaves the lever term zero however the anchor is placed.
|
|
44
|
+
const state = [0, 0, 0, 0, 1, 0, 0, 1, 0];
|
|
45
|
+
const out = createPhysics3DJointReaction();
|
|
46
|
+
|
|
47
|
+
accumulatePhysics3DJointRowReaction(joint, state, 0, 4, out);
|
|
48
|
+
|
|
49
|
+
expect(getPhysics3DJointReactionForce(out)).toBeCloseTo(0, 9);
|
|
50
|
+
expect(out.torqueY).toBeCloseTo(4, 9);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('adds rather than overwrites, so several rows sum into one reaction', () => {
|
|
54
|
+
const joint = createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 });
|
|
55
|
+
const out = createPhysics3DJointReaction();
|
|
56
|
+
const state = [1, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
57
|
+
|
|
58
|
+
accumulatePhysics3DJointRowReaction(joint, state, 0, 2, out);
|
|
59
|
+
accumulatePhysics3DJointRowReaction(joint, state, 0, 3, out);
|
|
60
|
+
|
|
61
|
+
expect(out.forceX).toBeCloseTo(5, 9);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('ignores a zero impulse', () => {
|
|
65
|
+
const joint = createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 });
|
|
66
|
+
const out = createPhysics3DJointReaction();
|
|
67
|
+
out.forceX = 9;
|
|
68
|
+
|
|
69
|
+
accumulatePhysics3DJointRowReaction(joint, [1, 0, 0, 0, 0, 0, 0, 0, 0], 0, 0, out);
|
|
70
|
+
|
|
71
|
+
expect(out.forceX).toBe(9);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('clearPhysics3DJointReaction', () => {
|
|
76
|
+
it('zeroes every component', () => {
|
|
77
|
+
const out = createPhysics3DJointReaction();
|
|
78
|
+
out.forceX = 1;
|
|
79
|
+
out.torqueZ = 2;
|
|
80
|
+
|
|
81
|
+
clearPhysics3DJointReaction(out);
|
|
82
|
+
|
|
83
|
+
expect(out).toEqual({ forceX: 0, forceY: 0, forceZ: 0, torqueX: 0, torqueY: 0, torqueZ: 0 });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('createPhysics3DJointReaction', () => {
|
|
88
|
+
it('starts zeroed', () => {
|
|
89
|
+
expect(createPhysics3DJointReaction()).toEqual({
|
|
90
|
+
forceX: 0,
|
|
91
|
+
forceY: 0,
|
|
92
|
+
forceZ: 0,
|
|
93
|
+
torqueX: 0,
|
|
94
|
+
torqueY: 0,
|
|
95
|
+
torqueZ: 0,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('getPhysics3DJointReactionForce', () => {
|
|
101
|
+
it('is the magnitude of the force triple', () => {
|
|
102
|
+
const out = createPhysics3DJointReaction();
|
|
103
|
+
out.forceX = 3;
|
|
104
|
+
out.forceY = 4;
|
|
105
|
+
|
|
106
|
+
expect(getPhysics3DJointReactionForce(out)).toBeCloseTo(5, 9);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('getPhysics3DJointReactionTorque', () => {
|
|
111
|
+
it('is the magnitude of the torque triple', () => {
|
|
112
|
+
const out = createPhysics3DJointReaction();
|
|
113
|
+
out.torqueY = 6;
|
|
114
|
+
out.torqueZ = 8;
|
|
115
|
+
|
|
116
|
+
expect(getPhysics3DJointReactionTorque(out)).toBeCloseTo(10, 9);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('writePhysics3DJointReaction', () => {
|
|
121
|
+
it('MEASURES THE WEIGHT A JOINT IS HOLDING UP', () => {
|
|
122
|
+
// The reading that makes the API worth having, checked against a number derived from outside the
|
|
123
|
+
// solver: a 3 kg body hanging under gravity 10 loads its joint with 30 newtons and nothing else.
|
|
124
|
+
const scene = createHangingScene(3, 10);
|
|
125
|
+
|
|
126
|
+
stepPhysics3D(scene.world, 1 / 60);
|
|
127
|
+
|
|
128
|
+
const out = createPhysics3DJointReaction();
|
|
129
|
+
expect(writePhysics3DJointReaction(scene.world, scene.joint, 1 / 60, out)).toBe(true);
|
|
130
|
+
expect(getPhysics3DJointReactionForce(out)).toBeCloseTo(30, 0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('scales with the hanging mass', () => {
|
|
134
|
+
const light = createHangingScene(1, 10);
|
|
135
|
+
const heavy = createHangingScene(4, 10);
|
|
136
|
+
stepPhysics3D(light.world, 1 / 60);
|
|
137
|
+
stepPhysics3D(heavy.world, 1 / 60);
|
|
138
|
+
|
|
139
|
+
const out = createPhysics3DJointReaction();
|
|
140
|
+
writePhysics3DJointReaction(light.world, light.joint, 1 / 60, out);
|
|
141
|
+
const lightForce = getPhysics3DJointReactionForce(out);
|
|
142
|
+
writePhysics3DJointReaction(heavy.world, heavy.joint, 1 / 60, out);
|
|
143
|
+
|
|
144
|
+
expect(getPhysics3DJointReactionForce(out) / lightForce).toBeCloseTo(4, 0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('reports a FORCE, so the reading does not move with the timestep', () => {
|
|
148
|
+
// The reason this divides by dt at all. An impulse would read twice as large at half the rate, and a
|
|
149
|
+
// breakForce tuned at 60Hz would behave differently at 120.
|
|
150
|
+
const coarse = createHangingScene(3, 10);
|
|
151
|
+
const fine = createHangingScene(3, 10);
|
|
152
|
+
stepPhysics3D(coarse.world, 1 / 60);
|
|
153
|
+
stepPhysics3D(fine.world, 1 / 120);
|
|
154
|
+
|
|
155
|
+
const out = createPhysics3DJointReaction();
|
|
156
|
+
writePhysics3DJointReaction(coarse.world, coarse.joint, 1 / 60, out);
|
|
157
|
+
const coarseForce = getPhysics3DJointReactionForce(out);
|
|
158
|
+
writePhysics3DJointReaction(fine.world, fine.joint, 1 / 120, out);
|
|
159
|
+
|
|
160
|
+
expect(getPhysics3DJointReactionForce(out)).toBeCloseTo(coarseForce, 0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('declines a joint whose kind has no registered solver', () => {
|
|
164
|
+
const scene = createHangingScene(3, 10);
|
|
165
|
+
scene.world.jointSolvers.clear();
|
|
166
|
+
const out = createPhysics3DJointReaction();
|
|
167
|
+
|
|
168
|
+
expect(writePhysics3DJointReaction(scene.world, scene.joint, 1 / 60, out)).toBe(false);
|
|
169
|
+
expect(getPhysics3DJointReactionForce(out)).toBe(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('declines a joint that has not been solved yet', () => {
|
|
173
|
+
const scene = createHangingScene(3, 10);
|
|
174
|
+
const out = createPhysics3DJointReaction();
|
|
175
|
+
|
|
176
|
+
expect(writePhysics3DJointReaction(scene.world, scene.joint, 1 / 60, out)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('declines a non-positive timestep rather than dividing by it', () => {
|
|
180
|
+
const scene = createHangingScene(3, 10);
|
|
181
|
+
stepPhysics3D(scene.world, 1 / 60);
|
|
182
|
+
const out = createPhysics3DJointReaction();
|
|
183
|
+
|
|
184
|
+
expect(writePhysics3DJointReaction(scene.world, scene.joint, 0, out)).toBe(false);
|
|
185
|
+
expect(Number.isFinite(getPhysics3DJointReactionForce(out))).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('reads a DISTANCE joint, whose one slot is an axial scalar rather than a world triple', () => {
|
|
189
|
+
// The kind that proves the reaction has to be the solver's to report: reading `impulse0..2` off this
|
|
190
|
+
// one generically would take a scalar along an axis for an x component.
|
|
191
|
+
const world = createPhysics3DWorld();
|
|
192
|
+
world.gravityY = -10;
|
|
193
|
+
registerBuiltInPhysics3DJointSolvers(world);
|
|
194
|
+
const anchor = createRigidBody3D('static');
|
|
195
|
+
addPhysics3DBody(world, anchor);
|
|
196
|
+
const hanging = createUnitBody(2);
|
|
197
|
+
hanging.y = -3;
|
|
198
|
+
addPhysics3DBody(world, hanging);
|
|
199
|
+
const joint = createPhysics3DDistanceJoint({ bodyA: 0, bodyB: 1, length: 3 });
|
|
200
|
+
addPhysics3DJoint(world, joint);
|
|
201
|
+
|
|
202
|
+
stepPhysics3D(world, 1 / 60);
|
|
203
|
+
|
|
204
|
+
const out = createPhysics3DJointReaction();
|
|
205
|
+
expect(writePhysics3DJointReaction(world, joint, 1 / 60, out)).toBe(true);
|
|
206
|
+
// The rope hangs straight down, so the load is along y and nowhere else.
|
|
207
|
+
expect(getPhysics3DJointReactionForce(out)).toBeCloseTo(20, 0);
|
|
208
|
+
expect(Math.abs(out.forceY)).toBeCloseTo(20, 0);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
interface HangingScene {
|
|
213
|
+
world: Physics3DWorld;
|
|
214
|
+
joint: ReturnType<typeof createPhysics3DBallAndSocketJoint>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// One body hanging from a static anchor by a ball-and-socket, which is the simplest arrangement whose
|
|
218
|
+
// reaction has a value known without reference to the solver: it is the hanging weight.
|
|
219
|
+
function createHangingScene(mass: number, gravity: number): HangingScene {
|
|
220
|
+
const world = createPhysics3DWorld();
|
|
221
|
+
world.gravityY = -gravity;
|
|
222
|
+
registerBuiltInPhysics3DJointSolvers(world);
|
|
223
|
+
const anchor = createRigidBody3D('static');
|
|
224
|
+
addPhysics3DBody(world, anchor);
|
|
225
|
+
const hanging = createUnitBody(mass);
|
|
226
|
+
addPhysics3DBody(world, hanging);
|
|
227
|
+
const joint = createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 });
|
|
228
|
+
addPhysics3DJoint(world, joint);
|
|
229
|
+
return { world, joint };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function createUnitBody(mass: number): RigidBody3D {
|
|
233
|
+
const body = createRigidBody3D('dynamic');
|
|
234
|
+
const data = createPhysics3DMassData();
|
|
235
|
+
data.mass = mass;
|
|
236
|
+
data.inertiaXX = mass;
|
|
237
|
+
data.inertiaYY = mass;
|
|
238
|
+
data.inertiaZZ = mass;
|
|
239
|
+
setRigidBody3DMassData(body, data);
|
|
240
|
+
return body;
|
|
241
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { Physics3DJointSolver, Physics3DWorld, RigidBody3D } from '@flighthq/types/contract';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { createPhysics3DBallAndSocketJoint, createPhysics3DHingeJoint } from './jointFactories';
|
|
5
|
+
import {
|
|
6
|
+
addPhysics3DJoint,
|
|
7
|
+
getPhysics3DJointSolver,
|
|
8
|
+
invalidatePhysics3DJoint,
|
|
9
|
+
isPhysics3DPairOrdered,
|
|
10
|
+
registerPhysics3DJointSolver,
|
|
11
|
+
removePhysics3DJoint,
|
|
12
|
+
} from './jointRegistry';
|
|
13
|
+
import { physics3DBallAndSocketJointSolver, physics3DHingeJointSolver, Physics3DHingeJointKind } from './joints';
|
|
14
|
+
import { steppingPhysics3DWorlds } from './ownership';
|
|
15
|
+
import { addPhysics3DBody, createPhysics3DWorld, createRigidBody3D } from './world';
|
|
16
|
+
|
|
17
|
+
describe('addPhysics3DJoint', () => {
|
|
18
|
+
it('returns the joint it was given and records it on the world', () => {
|
|
19
|
+
const world = createTestWorld();
|
|
20
|
+
const joint = createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 });
|
|
21
|
+
|
|
22
|
+
expect(addPhysics3DJoint(world, joint)).toBe(joint);
|
|
23
|
+
expect(world.joints).toEqual([joint]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('refuses a joint that already belongs to a world', () => {
|
|
27
|
+
const world = createTestWorld();
|
|
28
|
+
const joint = createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 });
|
|
29
|
+
addPhysics3DJoint(world, joint);
|
|
30
|
+
|
|
31
|
+
expect(() => addPhysics3DJoint(world, joint)).toThrow();
|
|
32
|
+
expect(() => addPhysics3DJoint(createTestWorld(), joint)).toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('exchanges out-of-order ends when the kind has a solver to consult', () => {
|
|
36
|
+
const world = createTestWorld();
|
|
37
|
+
registerPhysics3DJointSolver(world, Physics3DHingeJointKind, physics3DHingeJointSolver);
|
|
38
|
+
const joint = createPhysics3DHingeJoint({
|
|
39
|
+
bodyA: 1,
|
|
40
|
+
bodyB: 0,
|
|
41
|
+
localAnchorAX: 5,
|
|
42
|
+
localAnchorBX: 7,
|
|
43
|
+
lowerAngle: -0.25,
|
|
44
|
+
upperAngle: 1.5,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
addPhysics3DJoint(world, joint);
|
|
48
|
+
|
|
49
|
+
expect(joint.bodyA).toBe(0);
|
|
50
|
+
expect(joint.bodyB).toBe(1);
|
|
51
|
+
expect(joint.localAnchorAX).toBe(7);
|
|
52
|
+
expect(joint.localAnchorBX).toBe(5);
|
|
53
|
+
expect(joint.lowerAngle).toBe(-1.5);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('leaves an unknown kind in its authored order', () => {
|
|
57
|
+
const world = createTestWorld();
|
|
58
|
+
const joint = createPhysics3DHingeJoint({ bodyA: 1, bodyB: 0, lowerAngle: -0.25, upperAngle: 1.5 });
|
|
59
|
+
|
|
60
|
+
addPhysics3DJoint(world, joint);
|
|
61
|
+
|
|
62
|
+
// Exchanging the ends without the kind's consent would move the bodies and anchors while the limit
|
|
63
|
+
// interval stayed as authored — the generic half of a swap with the kind's half missing, which no later
|
|
64
|
+
// registration repairs.
|
|
65
|
+
expect(joint.bodyA).toBe(1);
|
|
66
|
+
expect(joint.lowerAngle).toBe(-0.25);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('wakes both bodies when the constraint is live', () => {
|
|
70
|
+
const world = createTestWorld();
|
|
71
|
+
registerPhysics3DJointSolver(world, Physics3DHingeJointKind, physics3DHingeJointSolver);
|
|
72
|
+
world.bodies[0].sleeping = true;
|
|
73
|
+
world.bodies[1].sleeping = true;
|
|
74
|
+
|
|
75
|
+
addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
76
|
+
|
|
77
|
+
expect(world.bodies[0].sleeping).toBe(false);
|
|
78
|
+
expect(world.bodies[1].sleeping).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('leaves a sleeping pair asleep for a kind that constrains nothing yet', () => {
|
|
82
|
+
const world = createTestWorld();
|
|
83
|
+
world.bodies[0].sleeping = true;
|
|
84
|
+
|
|
85
|
+
addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
86
|
+
|
|
87
|
+
expect(world.bodies[0].sleeping).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('rejects a mutation while the world is stepping', () => {
|
|
91
|
+
const world = createTestWorld();
|
|
92
|
+
steppingPhysics3DWorlds.add(world);
|
|
93
|
+
|
|
94
|
+
expect(() => addPhysics3DJoint(world, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 }))).toThrow();
|
|
95
|
+
|
|
96
|
+
steppingPhysics3DWorlds.delete(world);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('getPhysics3DJointSolver', () => {
|
|
101
|
+
it('returns null for a kind nobody registered', () => {
|
|
102
|
+
expect(getPhysics3DJointSolver(createTestWorld(), 'acme.Suspension')).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns the registered solver', () => {
|
|
106
|
+
const world = createTestWorld();
|
|
107
|
+
registerPhysics3DJointSolver(world, Physics3DHingeJointKind, physics3DHingeJointSolver);
|
|
108
|
+
|
|
109
|
+
expect(getPhysics3DJointSolver(world, Physics3DHingeJointKind)).toBe(physics3DHingeJointSolver);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('invalidatePhysics3DJoint', () => {
|
|
114
|
+
it('clears every common accumulator and the kind-specific ones', () => {
|
|
115
|
+
const world = createTestWorld();
|
|
116
|
+
registerPhysics3DJointSolver(world, Physics3DHingeJointKind, physics3DHingeJointSolver);
|
|
117
|
+
const joint = addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
118
|
+
joint.impulse0 = 1;
|
|
119
|
+
joint.impulse5 = 2;
|
|
120
|
+
(joint as ReturnType<typeof createPhysics3DHingeJoint>).motorImpulse = 3;
|
|
121
|
+
|
|
122
|
+
expect(invalidatePhysics3DJoint(world, joint)).toBe(true);
|
|
123
|
+
expect(joint.impulse0).toBe(0);
|
|
124
|
+
expect(joint.impulse5).toBe(0);
|
|
125
|
+
expect((joint as ReturnType<typeof createPhysics3DHingeJoint>).motorImpulse).toBe(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('returns false for a joint this world does not hold', () => {
|
|
129
|
+
const world = createTestWorld();
|
|
130
|
+
const joint = createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 });
|
|
131
|
+
|
|
132
|
+
expect(invalidatePhysics3DJoint(world, joint)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('isPhysics3DPairOrdered', () => {
|
|
137
|
+
it('orders by persistent index and admits a pair already in order', () => {
|
|
138
|
+
expect(isPhysics3DPairOrdered(0, 1)).toBe(true);
|
|
139
|
+
expect(isPhysics3DPairOrdered(1, 1)).toBe(true);
|
|
140
|
+
expect(isPhysics3DPairOrdered(1, 0)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('registerPhysics3DJointSolver', () => {
|
|
145
|
+
it('canonicalizes joints that were added before their solver existed', () => {
|
|
146
|
+
const world = createTestWorld();
|
|
147
|
+
const joint = addPhysics3DJoint(
|
|
148
|
+
world,
|
|
149
|
+
createPhysics3DHingeJoint({ bodyA: 1, bodyB: 0, lowerAngle: -0.25, upperAngle: 1.5 }),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
registerPhysics3DJointSolver(world, Physics3DHingeJointKind, physics3DHingeJointSolver);
|
|
153
|
+
|
|
154
|
+
// The outcome no longer depends on whether the scene or the solver arrived first.
|
|
155
|
+
expect(joint.bodyA).toBe(0);
|
|
156
|
+
expect(joint.lowerAngle).toBe(-1.5);
|
|
157
|
+
expect(joint.upperAngle).toBe(0.25);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('lets a later registration replace an earlier one', () => {
|
|
161
|
+
const world = createTestWorld();
|
|
162
|
+
const replacement: Physics3DJointSolver = { prepare(): void {}, solve(): void {} };
|
|
163
|
+
registerPhysics3DJointSolver(world, Physics3DHingeJointKind, physics3DHingeJointSolver);
|
|
164
|
+
registerPhysics3DJointSolver(world, Physics3DHingeJointKind, replacement);
|
|
165
|
+
|
|
166
|
+
expect(getPhysics3DJointSolver(world, Physics3DHingeJointKind)).toBe(replacement);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('wakes the bodies of a joint the registration just brought to life', () => {
|
|
170
|
+
const world = createTestWorld();
|
|
171
|
+
addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
172
|
+
world.bodies[0].sleeping = true;
|
|
173
|
+
world.bodies[1].sleeping = true;
|
|
174
|
+
|
|
175
|
+
registerPhysics3DJointSolver(world, Physics3DHingeJointKind, physics3DHingeJointSolver);
|
|
176
|
+
|
|
177
|
+
expect(world.bodies[0].sleeping).toBe(false);
|
|
178
|
+
expect(world.bodies[1].sleeping).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('removePhysics3DJoint', () => {
|
|
183
|
+
it('drops the joint and reports it', () => {
|
|
184
|
+
const world = createTestWorld();
|
|
185
|
+
const joint = addPhysics3DJoint(world, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 }));
|
|
186
|
+
|
|
187
|
+
expect(removePhysics3DJoint(world, joint)).toBe(true);
|
|
188
|
+
expect(world.joints).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('returns false for a joint it never held', () => {
|
|
192
|
+
const world = createTestWorld();
|
|
193
|
+
|
|
194
|
+
expect(removePhysics3DJoint(world, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 }))).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('releases ownership so the joint can be added again', () => {
|
|
198
|
+
const world = createTestWorld();
|
|
199
|
+
const joint = addPhysics3DJoint(world, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 }));
|
|
200
|
+
removePhysics3DJoint(world, joint);
|
|
201
|
+
|
|
202
|
+
expect(() => addPhysics3DJoint(world, joint)).not.toThrow();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('wakes both bodies when a live constraint is taken away', () => {
|
|
206
|
+
const world = createTestWorld();
|
|
207
|
+
registerPhysics3DJointSolver(world, Physics3DHingeJointKind, physics3DHingeJointSolver);
|
|
208
|
+
const joint = addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
209
|
+
world.bodies[0].sleeping = true;
|
|
210
|
+
|
|
211
|
+
removePhysics3DJoint(world, joint);
|
|
212
|
+
|
|
213
|
+
expect(world.bodies[0].sleeping).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
function createTestWorld(): Physics3DWorld {
|
|
218
|
+
const world = createPhysics3DWorld();
|
|
219
|
+
addPhysics3DBody(world, createBody());
|
|
220
|
+
addPhysics3DBody(world, createBody());
|
|
221
|
+
// The ball-and-socket solver is imported so this file exercises a kind with no `swapEnds` of its own; the
|
|
222
|
+
// registry has to reach the generic swap for it without consulting one.
|
|
223
|
+
expect(physics3DBallAndSocketJointSolver.swapEnds).toBeUndefined();
|
|
224
|
+
return world;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function createBody(): RigidBody3D {
|
|
228
|
+
return createRigidBody3D('dynamic');
|
|
229
|
+
}
|