@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,370 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerBuiltInCollisionFaceQueries3D,
|
|
3
|
+
registerBuiltInCollisionSupports3D,
|
|
4
|
+
} from '@flighthq/collision/contract';
|
|
5
|
+
import type { CollisionBuiltInShape3D, Physics3DWorld, RigidBody3D } from '@flighthq/types/contract';
|
|
6
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { buildPhysics3DContacts, refreshPhysics3DContacts, setPhysics3DContactIntakeGuard } from './contactIntake';
|
|
9
|
+
import { stepPhysics3D } from './step';
|
|
10
|
+
import {
|
|
11
|
+
addPhysics3DBody,
|
|
12
|
+
addPhysics3DCollider,
|
|
13
|
+
createPhysics3DCollider,
|
|
14
|
+
createPhysics3DWorld,
|
|
15
|
+
createRigidBody3D,
|
|
16
|
+
} from './world';
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
registerBuiltInCollisionSupports3D();
|
|
20
|
+
registerBuiltInCollisionFaceQueries3D();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function box(halfX: number, halfY: number, halfZ: number): CollisionBuiltInShape3D {
|
|
24
|
+
return { kind: 'aabb', minX: -halfX, minY: -halfY, minZ: -halfZ, maxX: halfX, maxY: halfY, maxZ: halfZ };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A floor at y=0 spanning enough ground that nothing in these tests walks off it.
|
|
28
|
+
function addFloor(world: Physics3DWorld): RigidBody3D {
|
|
29
|
+
const floor = createRigidBody3D('static');
|
|
30
|
+
floor.y = -1;
|
|
31
|
+
addPhysics3DBody(world, floor);
|
|
32
|
+
addPhysics3DCollider(world, floor, createPhysics3DCollider(box(20, 1, 20)));
|
|
33
|
+
return floor;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addCrate(world: Physics3DWorld, x: number, y: number, sensor = false): RigidBody3D {
|
|
37
|
+
const crate = createRigidBody3D('dynamic');
|
|
38
|
+
crate.x = x;
|
|
39
|
+
crate.y = y;
|
|
40
|
+
addPhysics3DBody(world, crate);
|
|
41
|
+
addPhysics3DCollider(world, crate, createPhysics3DCollider(box(0.5, 0.5, 0.5), undefined, undefined, sensor));
|
|
42
|
+
return crate;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('buildPhysics3DContacts', () => {
|
|
46
|
+
it('generates a contact for an overlapping pair with no caller-supplied contact', () => {
|
|
47
|
+
// The whole point of the intake: the world was GIVEN no contacts and finds one itself.
|
|
48
|
+
const world = createPhysics3DWorld();
|
|
49
|
+
addFloor(world);
|
|
50
|
+
const crate = addCrate(world, 0, 0.4);
|
|
51
|
+
expect(world.contacts).toHaveLength(0);
|
|
52
|
+
|
|
53
|
+
buildPhysics3DContacts(world);
|
|
54
|
+
|
|
55
|
+
expect(world.contacts).toHaveLength(1);
|
|
56
|
+
expect(world.contacts[0].pointCount).toBeGreaterThan(0);
|
|
57
|
+
expect(crate.index).toBeGreaterThanOrEqual(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('gives a resting box four points, so it does not topple off a single one', () => {
|
|
61
|
+
const world = createPhysics3DWorld();
|
|
62
|
+
addFloor(world);
|
|
63
|
+
addCrate(world, 0, 0.45);
|
|
64
|
+
|
|
65
|
+
buildPhysics3DContacts(world);
|
|
66
|
+
|
|
67
|
+
expect(world.contacts[0].pointCount).toBe(4);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('orients the normal to push A out of B', () => {
|
|
71
|
+
const world = createPhysics3DWorld();
|
|
72
|
+
const floor = addFloor(world);
|
|
73
|
+
const crate = addCrate(world, 0, 0.45);
|
|
74
|
+
|
|
75
|
+
buildPhysics3DContacts(world);
|
|
76
|
+
|
|
77
|
+
// The floor was added first, so it holds the lower index and is body A. Resolving must push the
|
|
78
|
+
// FLOOR out of the crate, which is downward.
|
|
79
|
+
const contact = world.contacts[0];
|
|
80
|
+
expect(contact.bodyA).toBe(floor.index);
|
|
81
|
+
expect(contact.bodyB).toBe(crate.index);
|
|
82
|
+
expect(contact.normalY).toBeCloseTo(-1, 5);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('finds no contact for a pair that is clear of each other', () => {
|
|
86
|
+
const world = createPhysics3DWorld();
|
|
87
|
+
addFloor(world);
|
|
88
|
+
addCrate(world, 0, 3);
|
|
89
|
+
|
|
90
|
+
buildPhysics3DContacts(world);
|
|
91
|
+
|
|
92
|
+
expect(world.contacts).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('reports a begin the step a pair starts touching, and only that step', () => {
|
|
96
|
+
const world = createPhysics3DWorld();
|
|
97
|
+
addFloor(world);
|
|
98
|
+
const crate = addCrate(world, 0, 3);
|
|
99
|
+
|
|
100
|
+
buildPhysics3DContacts(world);
|
|
101
|
+
expect(world.events.began).toHaveLength(0);
|
|
102
|
+
|
|
103
|
+
crate.y = 0.45;
|
|
104
|
+
buildPhysics3DContacts(world);
|
|
105
|
+
expect(world.events.began).toHaveLength(1);
|
|
106
|
+
|
|
107
|
+
buildPhysics3DContacts(world);
|
|
108
|
+
expect(world.events.began).toHaveLength(0);
|
|
109
|
+
expect(world.contacts).toHaveLength(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('reports an end the step a pair stops touching, and retires the contact', () => {
|
|
113
|
+
const world = createPhysics3DWorld();
|
|
114
|
+
addFloor(world);
|
|
115
|
+
const crate = addCrate(world, 0, 0.45);
|
|
116
|
+
|
|
117
|
+
buildPhysics3DContacts(world);
|
|
118
|
+
expect(world.contacts).toHaveLength(1);
|
|
119
|
+
|
|
120
|
+
crate.y = 5;
|
|
121
|
+
buildPhysics3DContacts(world);
|
|
122
|
+
|
|
123
|
+
expect(world.events.ended).toHaveLength(1);
|
|
124
|
+
expect(world.contacts).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('keeps one contact object across steps, so a warm-start cache has something to match', () => {
|
|
128
|
+
const world = createPhysics3DWorld();
|
|
129
|
+
addFloor(world);
|
|
130
|
+
addCrate(world, 0, 0.45);
|
|
131
|
+
|
|
132
|
+
buildPhysics3DContacts(world);
|
|
133
|
+
const first = world.contacts[0];
|
|
134
|
+
buildPhysics3DContacts(world);
|
|
135
|
+
|
|
136
|
+
expect(world.contacts[0]).toBe(first);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('reports a sensor overlap without making it resolvable', () => {
|
|
140
|
+
const world = createPhysics3DWorld();
|
|
141
|
+
addFloor(world);
|
|
142
|
+
addCrate(world, 0, 0.45, true);
|
|
143
|
+
|
|
144
|
+
buildPhysics3DContacts(world);
|
|
145
|
+
|
|
146
|
+
expect(world.contacts).toHaveLength(1);
|
|
147
|
+
expect(world.contacts[0].sensor).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('skips a pair whose filters exclude each other', () => {
|
|
151
|
+
const world = createPhysics3DWorld();
|
|
152
|
+
const floor = createRigidBody3D('static');
|
|
153
|
+
floor.y = -1;
|
|
154
|
+
addPhysics3DBody(world, floor);
|
|
155
|
+
addPhysics3DCollider(
|
|
156
|
+
world,
|
|
157
|
+
floor,
|
|
158
|
+
createPhysics3DCollider(box(20, 1, 20), undefined, { categoryBits: 1, maskBits: 1, groupIndex: 0 }),
|
|
159
|
+
);
|
|
160
|
+
const crate = createRigidBody3D('dynamic');
|
|
161
|
+
crate.y = 0.45;
|
|
162
|
+
addPhysics3DBody(world, crate);
|
|
163
|
+
addPhysics3DCollider(
|
|
164
|
+
world,
|
|
165
|
+
crate,
|
|
166
|
+
createPhysics3DCollider(box(0.5, 0.5, 0.5), undefined, { categoryBits: 2, maskBits: 2, groupIndex: 0 }),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
buildPhysics3DContacts(world);
|
|
170
|
+
|
|
171
|
+
expect(world.contacts).toHaveLength(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('generates no contact between two static bodies, which nothing could resolve', () => {
|
|
175
|
+
const world = createPhysics3DWorld();
|
|
176
|
+
addFloor(world);
|
|
177
|
+
const wall = createRigidBody3D('static');
|
|
178
|
+
wall.y = 0.45;
|
|
179
|
+
addPhysics3DBody(world, wall);
|
|
180
|
+
addPhysics3DCollider(world, wall, createPhysics3DCollider(box(0.5, 0.5, 0.5)));
|
|
181
|
+
|
|
182
|
+
buildPhysics3DContacts(world);
|
|
183
|
+
|
|
184
|
+
expect(world.contacts).toHaveLength(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('gives a compound body one contact per touching collider pair', () => {
|
|
188
|
+
const world = createPhysics3DWorld();
|
|
189
|
+
addFloor(world);
|
|
190
|
+
const barbell = createRigidBody3D('dynamic');
|
|
191
|
+
barbell.y = 0.45;
|
|
192
|
+
addPhysics3DBody(world, barbell);
|
|
193
|
+
// Two feet, far enough apart that each touches the floor as its own contact.
|
|
194
|
+
addPhysics3DCollider(
|
|
195
|
+
world,
|
|
196
|
+
barbell,
|
|
197
|
+
createPhysics3DCollider({ kind: 'aabb', minX: -3.5, minY: -0.5, minZ: -0.5, maxX: -2.5, maxY: 0.5, maxZ: 0.5 }),
|
|
198
|
+
);
|
|
199
|
+
addPhysics3DCollider(
|
|
200
|
+
world,
|
|
201
|
+
barbell,
|
|
202
|
+
createPhysics3DCollider({ kind: 'aabb', minX: 2.5, minY: -0.5, minZ: -0.5, maxX: 3.5, maxY: 0.5, maxZ: 0.5 }),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
buildPhysics3DContacts(world);
|
|
206
|
+
|
|
207
|
+
expect(world.contacts).toHaveLength(2);
|
|
208
|
+
expect(world.contacts.map((contact) => contact.colliderB)).toEqual([0, 1]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('sorts contacts into a deterministic order regardless of insertion history', () => {
|
|
212
|
+
const world = createPhysics3DWorld();
|
|
213
|
+
addFloor(world);
|
|
214
|
+
addCrate(world, 5, 0.45);
|
|
215
|
+
addCrate(world, -5, 0.45);
|
|
216
|
+
addCrate(world, 0, 0.45);
|
|
217
|
+
|
|
218
|
+
buildPhysics3DContacts(world);
|
|
219
|
+
|
|
220
|
+
const pairs = world.contacts.map((contact) => `${contact.bodyA}:${contact.bodyB}`);
|
|
221
|
+
expect(pairs).toEqual([...pairs].sort());
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('measures the lever arms from the centre of mass rather than the body origin', () => {
|
|
225
|
+
const world = createPhysics3DWorld();
|
|
226
|
+
addFloor(world);
|
|
227
|
+
// One collider, offset a whole unit along +x from the body origin, so the centre of mass is there
|
|
228
|
+
// and not at the origin.
|
|
229
|
+
const crate = createRigidBody3D('dynamic');
|
|
230
|
+
crate.y = 0.45;
|
|
231
|
+
addPhysics3DBody(world, crate);
|
|
232
|
+
addPhysics3DCollider(
|
|
233
|
+
world,
|
|
234
|
+
crate,
|
|
235
|
+
createPhysics3DCollider({ kind: 'aabb', minX: 0.5, minY: -0.5, minZ: -0.5, maxX: 1.5, maxY: 0.5, maxZ: 0.5 }),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(crate.centerX).toBeCloseTo(1, 9);
|
|
239
|
+
buildPhysics3DContacts(world);
|
|
240
|
+
|
|
241
|
+
// Every contact point sits under the collider around x=1, so an arm measured from the origin would
|
|
242
|
+
// average about +1 and one measured from the centre of mass about 0.
|
|
243
|
+
const contact = world.contacts[0];
|
|
244
|
+
let sum = 0;
|
|
245
|
+
for (let i = 0; i < contact.pointCount; i += 1) sum += contact.points[i].rBX;
|
|
246
|
+
expect(sum / contact.pointCount).toBeCloseTo(0, 6);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('refreshPhysics3DContacts', () => {
|
|
251
|
+
it('appends transitions and preserves the pre-solve fields on an existing contact', () => {
|
|
252
|
+
const world = createPhysics3DWorld();
|
|
253
|
+
addFloor(world);
|
|
254
|
+
const crate = addCrate(world, 0, 3);
|
|
255
|
+
buildPhysics3DContacts(world);
|
|
256
|
+
|
|
257
|
+
crate.y = 0.45;
|
|
258
|
+
refreshPhysics3DContacts(world);
|
|
259
|
+
expect(world.events.began).toHaveLength(1);
|
|
260
|
+
const contact = world.contacts[0];
|
|
261
|
+
contact.friction = 0.9;
|
|
262
|
+
contact.restitution = 0.8;
|
|
263
|
+
contact.enabled = false;
|
|
264
|
+
contact.sensor = true;
|
|
265
|
+
|
|
266
|
+
refreshPhysics3DContacts(world);
|
|
267
|
+
|
|
268
|
+
expect(world.events.began).toHaveLength(1);
|
|
269
|
+
expect(world.contacts[0]).toBe(contact);
|
|
270
|
+
expect(contact.friction).toBe(0.9);
|
|
271
|
+
expect(contact.restitution).toBe(0.8);
|
|
272
|
+
expect(contact.enabled).toBe(false);
|
|
273
|
+
expect(contact.sensor).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('setPhysics3DContactIntakeGuard', () => {
|
|
278
|
+
it('is consulted on every rebuild, and only while installed', () => {
|
|
279
|
+
const seen: number[] = [];
|
|
280
|
+
setPhysics3DContactIntakeGuard((world) => seen.push(world.bodies.length));
|
|
281
|
+
try {
|
|
282
|
+
const world = createPhysics3DWorld();
|
|
283
|
+
addFloor(world);
|
|
284
|
+
addCrate(world, 0, 3);
|
|
285
|
+
|
|
286
|
+
buildPhysics3DContacts(world);
|
|
287
|
+
expect(seen).toEqual([2]);
|
|
288
|
+
|
|
289
|
+
// Unlike the step guard, this one fires on a HEALTHY rebuild too: the failure it reports — no
|
|
290
|
+
// registered support, so nothing can ever touch — is invisible in the output of a successful call.
|
|
291
|
+
buildPhysics3DContacts(world);
|
|
292
|
+
expect(seen).toEqual([2, 2]);
|
|
293
|
+
|
|
294
|
+
setPhysics3DContactIntakeGuard(null);
|
|
295
|
+
buildPhysics3DContacts(world);
|
|
296
|
+
expect(seen).toEqual([2, 2]);
|
|
297
|
+
} finally {
|
|
298
|
+
setPhysics3DContactIntakeGuard(null);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('stepPhysics3D contact generation', () => {
|
|
304
|
+
it('lands a falling box on a floor and settles it there', () => {
|
|
305
|
+
// The end-to-end claim the whole arc exists for: a world given NO contacts simulates a solid.
|
|
306
|
+
const world = createPhysics3DWorld();
|
|
307
|
+
addFloor(world);
|
|
308
|
+
const crate = addCrate(world, 0, 3);
|
|
309
|
+
|
|
310
|
+
for (let i = 0; i < 240; i += 1) stepPhysics3D(world, 1 / 60);
|
|
311
|
+
|
|
312
|
+
// Resting on the floor means the crate's underside is at y=0, so its centre is at its half extent.
|
|
313
|
+
expect(crate.y).toBeGreaterThan(0.4);
|
|
314
|
+
expect(crate.y).toBeLessThan(0.6);
|
|
315
|
+
expect(Math.abs(crate.velocityY)).toBeLessThan(0.1);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('does not let a box fall through a floor it started above', () => {
|
|
319
|
+
const world = createPhysics3DWorld();
|
|
320
|
+
addFloor(world);
|
|
321
|
+
const crate = addCrate(world, 0, 1.5);
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < 600; i += 1) stepPhysics3D(world, 1 / 60);
|
|
324
|
+
|
|
325
|
+
expect(crate.y).toBeGreaterThan(0);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('stacks two boxes without either sinking into the other', () => {
|
|
329
|
+
const world = createPhysics3DWorld();
|
|
330
|
+
addFloor(world);
|
|
331
|
+
const lower = addCrate(world, 0, 0.5);
|
|
332
|
+
const upper = addCrate(world, 0, 1.5);
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < 300; i += 1) stepPhysics3D(world, 1 / 60);
|
|
335
|
+
|
|
336
|
+
expect(lower.y).toBeGreaterThan(0.4);
|
|
337
|
+
// The upper box rests on the lower one, a full box height above it.
|
|
338
|
+
expect(upper.y - lower.y).toBeGreaterThan(0.9);
|
|
339
|
+
expect(upper.y - lower.y).toBeLessThan(1.1);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('leaves world.events filled by the step rather than empty', () => {
|
|
343
|
+
const world = createPhysics3DWorld();
|
|
344
|
+
addFloor(world);
|
|
345
|
+
addCrate(world, 0, 0.6);
|
|
346
|
+
|
|
347
|
+
let began = 0;
|
|
348
|
+
for (let i = 0; i < 60; i += 1) {
|
|
349
|
+
stepPhysics3D(world, 1 / 60);
|
|
350
|
+
began += world.events.began.length;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
expect(began).toBe(1);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('runs identically twice from the same initial state', () => {
|
|
357
|
+
const trace = (): number[] => {
|
|
358
|
+
const world = createPhysics3DWorld();
|
|
359
|
+
addFloor(world);
|
|
360
|
+
const crate = addCrate(world, 0.1, 2);
|
|
361
|
+
const samples: number[] = [];
|
|
362
|
+
for (let i = 0; i < 180; i += 1) {
|
|
363
|
+
stepPhysics3D(world, 1 / 60);
|
|
364
|
+
samples.push(crate.x, crate.y, crate.z);
|
|
365
|
+
}
|
|
366
|
+
return samples;
|
|
367
|
+
};
|
|
368
|
+
expect(trace()).toEqual(trace());
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createPhysics3DContact, createPhysics3DContactPoint } from './contacts';
|
|
4
|
+
|
|
5
|
+
describe('createPhysics3DContact', () => {
|
|
6
|
+
it('orders the pair by index whichever way round it is given', () => {
|
|
7
|
+
// The order is an invariant of creation rather than a convention a caller is asked to respect: a
|
|
8
|
+
// narrow phase ties its contact points toward its first argument, so the reversed pair would move the
|
|
9
|
+
// points and renumber their feature ids — and the warm-start cache is keyed to those ids.
|
|
10
|
+
expect(createPhysics3DContact(7, 3).bodyA).toBe(3);
|
|
11
|
+
expect(createPhysics3DContact(7, 3).bodyB).toBe(7);
|
|
12
|
+
expect(createPhysics3DContact(3, 7).bodyA).toBe(3);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('starts inert, with no points and nothing touching', () => {
|
|
16
|
+
const contact = createPhysics3DContact(0, 1);
|
|
17
|
+
|
|
18
|
+
expect(contact.pointCount).toBe(0);
|
|
19
|
+
expect(contact.points).toEqual([]);
|
|
20
|
+
expect(contact.touching).toBe(false);
|
|
21
|
+
expect([contact.normalX, contact.normalY, contact.normalZ]).toEqual([0, 0, 0]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('is enabled and solid by default', () => {
|
|
25
|
+
const contact = createPhysics3DContact(0, 1);
|
|
26
|
+
|
|
27
|
+
expect(contact.enabled).toBe(true);
|
|
28
|
+
expect(contact.sensor).toBe(false);
|
|
29
|
+
expect(contact.friction).toBe(0);
|
|
30
|
+
expect(contact.restitution).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('allocates a distinct record each time', () => {
|
|
34
|
+
expect(createPhysics3DContact(0, 1).points).not.toBe(createPhysics3DContact(0, 1).points);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('createPhysics3DContactPoint', () => {
|
|
39
|
+
it('zeroes the geometry and both lever arms', () => {
|
|
40
|
+
const point = createPhysics3DContactPoint();
|
|
41
|
+
|
|
42
|
+
expect([point.x, point.y, point.z, point.depth, point.featureId]).toEqual([0, 0, 0, 0, 0]);
|
|
43
|
+
expect([point.rAX, point.rAY, point.rAZ]).toEqual([0, 0, 0]);
|
|
44
|
+
expect([point.rBX, point.rBY, point.rBZ]).toEqual([0, 0, 0]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('carries no solver state', () => {
|
|
48
|
+
// Solver accumulators live in `Physics3DContactConstraintPoint`, which the solver owns. A contact
|
|
49
|
+
// point carries geometry and identity only, so a second solver inherits no dead fields.
|
|
50
|
+
expect(Object.keys(createPhysics3DContactPoint()).sort()).toEqual([
|
|
51
|
+
'depth',
|
|
52
|
+
'featureId',
|
|
53
|
+
'rAX',
|
|
54
|
+
'rAY',
|
|
55
|
+
'rAZ',
|
|
56
|
+
'rBX',
|
|
57
|
+
'rBY',
|
|
58
|
+
'rBZ',
|
|
59
|
+
'x',
|
|
60
|
+
'y',
|
|
61
|
+
'z',
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
});
|