@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,265 @@
|
|
|
1
|
+
import { setSpatialIndexingGuard } from '@flighthq/spatial/contract';
|
|
2
|
+
import type {
|
|
3
|
+
CollisionBuiltInShape3D,
|
|
4
|
+
Physics3DWorld,
|
|
5
|
+
RigidBody3D,
|
|
6
|
+
SpatialIndexingNotice,
|
|
7
|
+
SpatialPair,
|
|
8
|
+
} from '@flighthq/types/contract';
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import { synchronizePhysics3DBroadphase, synchronizePhysics3DSweptBroadphase } from './broadphase';
|
|
12
|
+
import {
|
|
13
|
+
addPhysics3DBody,
|
|
14
|
+
addPhysics3DCollider,
|
|
15
|
+
createPhysics3DCollider,
|
|
16
|
+
createPhysics3DWorld,
|
|
17
|
+
createRigidBody3D,
|
|
18
|
+
removePhysics3DBody,
|
|
19
|
+
} from './world';
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
setSpatialIndexingGuard(null);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function unitBox(): CollisionBuiltInShape3D {
|
|
26
|
+
return { kind: 'aabb', minX: -0.5, minY: -0.5, minZ: -0.5, maxX: 0.5, maxY: 0.5, maxZ: 0.5 };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function addBoxBody(world: Physics3DWorld, x: number, y: number, z: number): RigidBody3D {
|
|
30
|
+
const body = createRigidBody3D('dynamic');
|
|
31
|
+
body.x = x;
|
|
32
|
+
body.y = y;
|
|
33
|
+
body.z = z;
|
|
34
|
+
addPhysics3DBody(world, body);
|
|
35
|
+
addPhysics3DCollider(world, body, createPhysics3DCollider(unitBox()));
|
|
36
|
+
return body;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pairs(world: Readonly<Physics3DWorld>): SpatialPair[] {
|
|
40
|
+
const out: SpatialPair[] = [];
|
|
41
|
+
world.index.querySpatialPairs(out);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('synchronizePhysics3DBroadphase', () => {
|
|
46
|
+
it('uses insert, update, and remove without false missing-id lifecycle notices', () => {
|
|
47
|
+
const notices: SpatialIndexingNotice[] = [];
|
|
48
|
+
setSpatialIndexingGuard((notice) => notices.push({ ...notice }));
|
|
49
|
+
const world = createPhysics3DWorld();
|
|
50
|
+
const body = createRigidBody3D('dynamic');
|
|
51
|
+
|
|
52
|
+
addPhysics3DBody(world, body);
|
|
53
|
+
for (let step = 0; step < 100; step += 1) synchronizePhysics3DBroadphase(world);
|
|
54
|
+
addPhysics3DCollider(world, body, createPhysics3DCollider(unitBox()));
|
|
55
|
+
synchronizePhysics3DBroadphase(world);
|
|
56
|
+
body.colliders.length = 0;
|
|
57
|
+
synchronizePhysics3DBroadphase(world);
|
|
58
|
+
synchronizePhysics3DBroadphase(world);
|
|
59
|
+
removePhysics3DBody(world, body);
|
|
60
|
+
|
|
61
|
+
expect(notices.filter((notice) => notice.reason === 'missing-id')).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('publishes a body so an overlapping neighbour becomes a candidate pair', () => {
|
|
65
|
+
const world = createPhysics3DWorld();
|
|
66
|
+
addBoxBody(world, 0, 0, 0);
|
|
67
|
+
addBoxBody(world, 0.25, 0, 0);
|
|
68
|
+
|
|
69
|
+
synchronizePhysics3DBroadphase(world);
|
|
70
|
+
|
|
71
|
+
expect(pairs(world)).toHaveLength(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('reports no pair for bodies that are far apart', () => {
|
|
75
|
+
const world = createPhysics3DWorld();
|
|
76
|
+
addBoxBody(world, 0, 0, 0);
|
|
77
|
+
addBoxBody(world, 500, 0, 0);
|
|
78
|
+
|
|
79
|
+
synchronizePhysics3DBroadphase(world);
|
|
80
|
+
|
|
81
|
+
expect(pairs(world)).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('separates bodies along Z, not only along X and Y', () => {
|
|
85
|
+
// The dimension the 2D index cannot see. A seam that quietly dropped Z would report this pair.
|
|
86
|
+
const world = createPhysics3DWorld();
|
|
87
|
+
addBoxBody(world, 0, 0, 0);
|
|
88
|
+
addBoxBody(world, 0, 0, 500);
|
|
89
|
+
|
|
90
|
+
synchronizePhysics3DBroadphase(world);
|
|
91
|
+
|
|
92
|
+
expect(pairs(world)).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('follows a body that moves, rather than holding its bounds at insertion', () => {
|
|
96
|
+
const world = createPhysics3DWorld();
|
|
97
|
+
addBoxBody(world, 0, 0, 0);
|
|
98
|
+
const mover = addBoxBody(world, 500, 0, 0);
|
|
99
|
+
synchronizePhysics3DBroadphase(world);
|
|
100
|
+
expect(pairs(world)).toHaveLength(0);
|
|
101
|
+
|
|
102
|
+
mover.x = 0.25;
|
|
103
|
+
synchronizePhysics3DBroadphase(world);
|
|
104
|
+
|
|
105
|
+
expect(pairs(world)).toHaveLength(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('withdraws a body that has no colliders rather than leaving stale bounds', () => {
|
|
109
|
+
const world = createPhysics3DWorld();
|
|
110
|
+
addBoxBody(world, 0, 0, 0);
|
|
111
|
+
const ghost = addBoxBody(world, 0.25, 0, 0);
|
|
112
|
+
synchronizePhysics3DBroadphase(world);
|
|
113
|
+
expect(pairs(world)).toHaveLength(1);
|
|
114
|
+
|
|
115
|
+
// Emptied directly rather than through `removePhysics3DCollider`, so this tests the sync's own
|
|
116
|
+
// withdrawal rather than the lifecycle helper's.
|
|
117
|
+
ghost.colliders.length = 0;
|
|
118
|
+
synchronizePhysics3DBroadphase(world);
|
|
119
|
+
|
|
120
|
+
expect(pairs(world)).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('withdraws a body that has diverged to a non-finite position', () => {
|
|
124
|
+
const world = createPhysics3DWorld();
|
|
125
|
+
addBoxBody(world, 0, 0, 0);
|
|
126
|
+
const diverged = addBoxBody(world, 0.25, 0, 0);
|
|
127
|
+
|
|
128
|
+
diverged.x = Number.NaN;
|
|
129
|
+
synchronizePhysics3DBroadphase(world);
|
|
130
|
+
|
|
131
|
+
expect(pairs(world)).toHaveLength(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('withdraws a body whose bounds exceed the simulated extent', () => {
|
|
135
|
+
const world = createPhysics3DWorld();
|
|
136
|
+
addBoxBody(world, 0, 0, 0);
|
|
137
|
+
const huge = createRigidBody3D('dynamic');
|
|
138
|
+
addPhysics3DBody(world, huge);
|
|
139
|
+
addPhysics3DCollider(
|
|
140
|
+
world,
|
|
141
|
+
huge,
|
|
142
|
+
createPhysics3DCollider({ kind: 'aabb', minX: -1e9, minY: -1, minZ: -1, maxX: 1e9, maxY: 1, maxZ: 1 }),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
synchronizePhysics3DBroadphase(world);
|
|
146
|
+
|
|
147
|
+
expect(pairs(world)).toHaveLength(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('refreshes each collider world shape as a side effect, so the narrow phase reads current poses', () => {
|
|
151
|
+
const world = createPhysics3DWorld();
|
|
152
|
+
const body = addBoxBody(world, 0, 0, 0);
|
|
153
|
+
|
|
154
|
+
body.x = 7;
|
|
155
|
+
synchronizePhysics3DBroadphase(world);
|
|
156
|
+
|
|
157
|
+
expect(body.colliders[0].world.kind === 'box' && body.colliders[0].world.x).toBeCloseTo(7, 9);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('unions a compound body into one indexed volume', () => {
|
|
161
|
+
const world = createPhysics3DWorld();
|
|
162
|
+
const compound = createRigidBody3D('dynamic');
|
|
163
|
+
addPhysics3DBody(world, compound);
|
|
164
|
+
addPhysics3DCollider(world, compound, createPhysics3DCollider(unitBox()));
|
|
165
|
+
addPhysics3DCollider(
|
|
166
|
+
world,
|
|
167
|
+
compound,
|
|
168
|
+
createPhysics3DCollider({ kind: 'aabb', minX: 9.5, minY: -0.5, minZ: -0.5, maxX: 10.5, maxY: 0.5, maxZ: 0.5 }),
|
|
169
|
+
);
|
|
170
|
+
// Sits beside the FAR collider, which is only a candidate if the body's bounds covered both.
|
|
171
|
+
addBoxBody(world, 10, 0, 0);
|
|
172
|
+
|
|
173
|
+
synchronizePhysics3DBroadphase(world);
|
|
174
|
+
|
|
175
|
+
expect(pairs(world)).toHaveLength(1);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('synchronizePhysics3DSweptBroadphase', () => {
|
|
180
|
+
it('pairs bodies that will only meet PART WAY through the interval', () => {
|
|
181
|
+
// The whole reason a swept publication exists: at the current pose these two are 20 apart, so an
|
|
182
|
+
// ordinary sync reports no candidate and a continuous pass would never even test the pair.
|
|
183
|
+
const world = createPhysics3DWorld();
|
|
184
|
+
addBoxBody(world, 0, 0, 0);
|
|
185
|
+
const mover = addBoxBody(world, 20, 0, 0);
|
|
186
|
+
mover.velocityX = -1200;
|
|
187
|
+
synchronizePhysics3DBroadphase(world);
|
|
188
|
+
expect(pairs(world)).toHaveLength(0);
|
|
189
|
+
|
|
190
|
+
synchronizePhysics3DSweptBroadphase(world, 1 / 60);
|
|
191
|
+
|
|
192
|
+
expect(pairs(world)).toHaveLength(1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('leaves a body that is not moving where it was', () => {
|
|
196
|
+
const world = createPhysics3DWorld();
|
|
197
|
+
addBoxBody(world, 0, 0, 0);
|
|
198
|
+
addBoxBody(world, 20, 0, 0);
|
|
199
|
+
|
|
200
|
+
synchronizePhysics3DSweptBroadphase(world, 1 / 60);
|
|
201
|
+
|
|
202
|
+
expect(pairs(world)).toHaveLength(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('does not widen a STATIC body, which cannot move however fast its velocity field reads', () => {
|
|
206
|
+
const world = createPhysics3DWorld();
|
|
207
|
+
addBoxBody(world, 0, 0, 0);
|
|
208
|
+
const stuck = createRigidBody3D('static');
|
|
209
|
+
stuck.x = 20;
|
|
210
|
+
stuck.velocityX = -1200;
|
|
211
|
+
addPhysics3DBody(world, stuck);
|
|
212
|
+
addPhysics3DCollider(world, stuck, createPhysics3DCollider(unitBox()));
|
|
213
|
+
|
|
214
|
+
synchronizePhysics3DSweptBroadphase(world, 1 / 60);
|
|
215
|
+
|
|
216
|
+
expect(pairs(world)).toHaveLength(0);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('does not widen a SLEEPING body', () => {
|
|
220
|
+
const world = createPhysics3DWorld();
|
|
221
|
+
addBoxBody(world, 0, 0, 0);
|
|
222
|
+
const asleep = addBoxBody(world, 20, 0, 0);
|
|
223
|
+
asleep.velocityX = -1200;
|
|
224
|
+
asleep.sleeping = true;
|
|
225
|
+
|
|
226
|
+
synchronizePhysics3DSweptBroadphase(world, 1 / 60);
|
|
227
|
+
|
|
228
|
+
expect(pairs(world)).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('covers every orientation a SPINNING body could reach, not just its translation', () => {
|
|
232
|
+
// A rotating body's colliders swing through space its straight-line sweep never enters, so the
|
|
233
|
+
// widened volume has to be the sphere its geometry can reach rather than the box it currently fills.
|
|
234
|
+
const world = createPhysics3DWorld();
|
|
235
|
+
const spinner = createRigidBody3D('dynamic');
|
|
236
|
+
addPhysics3DBody(world, spinner);
|
|
237
|
+
// A long arm reaching out along x; spun about y its far end sweeps to z.
|
|
238
|
+
addPhysics3DCollider(
|
|
239
|
+
world,
|
|
240
|
+
spinner,
|
|
241
|
+
createPhysics3DCollider({ kind: 'aabb', minX: -0.25, minY: -0.25, minZ: -0.25, maxX: 4, maxY: 0.25, maxZ: 0.25 }),
|
|
242
|
+
);
|
|
243
|
+
spinner.angularVelocityY = 30;
|
|
244
|
+
addBoxBody(world, 0, 0, 3.5);
|
|
245
|
+
|
|
246
|
+
synchronizePhysics3DBroadphase(world);
|
|
247
|
+
expect(pairs(world)).toHaveLength(0);
|
|
248
|
+
synchronizePhysics3DSweptBroadphase(world, 1 / 60);
|
|
249
|
+
|
|
250
|
+
expect(pairs(world)).toHaveLength(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('restores the ordinary bounds when the plain sync runs after it', () => {
|
|
254
|
+
const world = createPhysics3DWorld();
|
|
255
|
+
addBoxBody(world, 0, 0, 0);
|
|
256
|
+
const mover = addBoxBody(world, 20, 0, 0);
|
|
257
|
+
mover.velocityX = -1200;
|
|
258
|
+
|
|
259
|
+
synchronizePhysics3DSweptBroadphase(world, 1 / 60);
|
|
260
|
+
expect(pairs(world)).toHaveLength(1);
|
|
261
|
+
synchronizePhysics3DBroadphase(world);
|
|
262
|
+
|
|
263
|
+
expect(pairs(world)).toHaveLength(0);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCollisionContactManifold3D,
|
|
3
|
+
createCollisionRaycastHit3D,
|
|
4
|
+
createCollisionTimeOfImpact3D,
|
|
5
|
+
createCollisionTriangleMesh3D,
|
|
6
|
+
registerBuiltInCollisionFaceQueries3D,
|
|
7
|
+
registerBuiltInCollisionSupports3D,
|
|
8
|
+
} from '@flighthq/collision/contract';
|
|
9
|
+
import type { CollisionShape3D } from '@flighthq/types/contract';
|
|
10
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
collidePhysics3DColliderShapes,
|
|
14
|
+
raycastPhysics3DColliderShape,
|
|
15
|
+
sweepPhysics3DColliderShapes,
|
|
16
|
+
} from './colliderCollision';
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
registerBuiltInCollisionSupports3D();
|
|
20
|
+
registerBuiltInCollisionFaceQueries3D();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('collidePhysics3DColliderShapes', () => {
|
|
24
|
+
it('preserves argument-oriented normals when the mesh is first', () => {
|
|
25
|
+
const mesh = createFloorMesh();
|
|
26
|
+
const sphere: CollisionShape3D = { kind: 'sphere', x: 0, y: 0.4, z: 0, radius: 0.5 };
|
|
27
|
+
const manifold = createCollisionContactManifold3D();
|
|
28
|
+
|
|
29
|
+
expect(collidePhysics3DColliderShapes(mesh, sphere, manifold)).toBe(true);
|
|
30
|
+
expect(manifold.normalY).toBeLessThan(-0.99);
|
|
31
|
+
expect(collidePhysics3DColliderShapes(sphere, mesh, manifold)).toBe(true);
|
|
32
|
+
expect(manifold.normalY).toBeGreaterThan(0.99);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('raycastPhysics3DColliderShape', () => {
|
|
37
|
+
it('routes concave surfaces through their triangle query', () => {
|
|
38
|
+
const hit = createCollisionRaycastHit3D();
|
|
39
|
+
expect(raycastPhysics3DColliderShape(createFloorMesh(), 0, 2, 0, 0, -1, 0, hit, 10)).toBe(true);
|
|
40
|
+
expect(hit.fraction).toBeCloseTo(2, 12);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('sweepPhysics3DColliderShapes', () => {
|
|
45
|
+
it('preserves argument-oriented normals when the mesh is first', () => {
|
|
46
|
+
const mesh = createFloorMesh();
|
|
47
|
+
const sphere: CollisionShape3D = { kind: 'sphere', x: 0, y: 2, z: 0, radius: 0.25 };
|
|
48
|
+
const hit = createCollisionTimeOfImpact3D();
|
|
49
|
+
|
|
50
|
+
expect(sweepPhysics3DColliderShapes(mesh, 0, 0, 0, sphere, 0, -4, 0, hit, 1)).toBe(true);
|
|
51
|
+
expect(hit.normalY).toBeLessThan(-0.99);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function createFloorMesh() {
|
|
56
|
+
return createCollisionTriangleMesh3D([-5, 0, -5, 5, 0, -5, 5, 0, 5, -5, 0, 5], [0, 2, 1, 0, 3, 2]);
|
|
57
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { CollisionBuiltInShape3D, Physics3DCollider, RigidBody3D, SpatialAabb3D } from '@flighthq/types/contract';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createPhysics3DColliderWorldShape,
|
|
6
|
+
updatePhysics3DColliderWorldShape,
|
|
7
|
+
writePhysics3DColliderBounds,
|
|
8
|
+
} from './colliderTransform';
|
|
9
|
+
import { createPhysics3DCollider, createRigidBody3D } from './world';
|
|
10
|
+
|
|
11
|
+
function bounds(): SpatialAabb3D {
|
|
12
|
+
return { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// A body spun a quarter turn about +Y, which maps local +x onto world -z.
|
|
16
|
+
function spunBody(): RigidBody3D {
|
|
17
|
+
const body = createRigidBody3D('dynamic');
|
|
18
|
+
body.orientationY = Math.sin(Math.PI / 4);
|
|
19
|
+
body.orientationW = Math.cos(Math.PI / 4);
|
|
20
|
+
return body;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function collider(local: CollisionBuiltInShape3D): Physics3DCollider {
|
|
24
|
+
return createPhysics3DCollider(local);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('createPhysics3DColliderWorldShape', () => {
|
|
28
|
+
it('keeps the kind for shapes a rigid transform cannot change', () => {
|
|
29
|
+
expect(createPhysics3DColliderWorldShape({ kind: 'sphere', x: 1, y: 2, z: 3, radius: 4 }).kind).toBe('sphere');
|
|
30
|
+
expect(
|
|
31
|
+
createPhysics3DColliderWorldShape({ kind: 'capsule', x0: 0, y0: 0, z0: 0, x1: 0, y1: 1, z1: 0, radius: 1 }).kind,
|
|
32
|
+
).toBe('capsule');
|
|
33
|
+
expect(createPhysics3DColliderWorldShape({ kind: 'convex', points: [0, 0, 0] }).kind).toBe('convex');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('promotes an axis-aligned box to an ORIENTED box', () => {
|
|
37
|
+
// Rotate an aabb and it is no longer axis-aligned. Keeping the kind would silently grow the box to its
|
|
38
|
+
// bounding extent the first time the body turned.
|
|
39
|
+
const world = createPhysics3DColliderWorldShape({
|
|
40
|
+
kind: 'aabb',
|
|
41
|
+
minX: -1,
|
|
42
|
+
minY: -1,
|
|
43
|
+
minZ: -1,
|
|
44
|
+
maxX: 1,
|
|
45
|
+
maxY: 1,
|
|
46
|
+
maxZ: 1,
|
|
47
|
+
});
|
|
48
|
+
expect(world.kind).toBe('box');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('sizes hull storage from the local shape so the transform can write in place', () => {
|
|
52
|
+
const local: CollisionBuiltInShape3D = { kind: 'convex', points: [1, 2, 3, 4, 5, 6] };
|
|
53
|
+
const world = createPhysics3DColliderWorldShape(local);
|
|
54
|
+
expect(world.kind === 'convex' && world.points).toHaveLength(6);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does not alias the local hull points', () => {
|
|
58
|
+
const local: CollisionBuiltInShape3D = { kind: 'convex', points: [1, 2, 3] };
|
|
59
|
+
const world = createPhysics3DColliderWorldShape(local);
|
|
60
|
+
expect(world.kind === 'convex' && world.points).not.toBe(local.points);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('updatePhysics3DColliderWorldShape', () => {
|
|
65
|
+
it('translates a sphere by the body position', () => {
|
|
66
|
+
const body = createRigidBody3D('dynamic');
|
|
67
|
+
body.x = 10;
|
|
68
|
+
body.y = 20;
|
|
69
|
+
body.z = 30;
|
|
70
|
+
const held = collider({ kind: 'sphere', x: 1, y: 0, z: 0, radius: 2 });
|
|
71
|
+
|
|
72
|
+
updatePhysics3DColliderWorldShape(held, body);
|
|
73
|
+
|
|
74
|
+
expect(held.world).toMatchObject({ kind: 'sphere', x: 11, y: 20, z: 30, radius: 2 });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('rotates a sphere offset about the body origin', () => {
|
|
78
|
+
const held = collider({ kind: 'sphere', x: 1, y: 0, z: 0, radius: 1 });
|
|
79
|
+
|
|
80
|
+
updatePhysics3DColliderWorldShape(held, spunBody());
|
|
81
|
+
|
|
82
|
+
// A quarter turn about +Y takes local +x to world -z.
|
|
83
|
+
expect(held.world.kind === 'sphere' && held.world.x).toBeCloseTo(0, 9);
|
|
84
|
+
expect(held.world.kind === 'sphere' && held.world.z).toBeCloseTo(-1, 9);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('carries the body orientation onto a promoted aabb', () => {
|
|
88
|
+
const body = spunBody();
|
|
89
|
+
const held = collider({ kind: 'aabb', minX: -1, minY: -2, minZ: -3, maxX: 1, maxY: 2, maxZ: 3 });
|
|
90
|
+
|
|
91
|
+
updatePhysics3DColliderWorldShape(held, body);
|
|
92
|
+
|
|
93
|
+
expect(held.world.kind === 'box' && held.world.halfX).toBeCloseTo(1, 9);
|
|
94
|
+
expect(held.world.kind === 'box' && held.world.halfZ).toBeCloseTo(3, 9);
|
|
95
|
+
expect(held.world.kind === 'box' && held.world.rotationY).toBeCloseTo(body.orientationY, 9);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('composes body and local rotations rather than replacing one with the other', () => {
|
|
99
|
+
// Both a quarter turn about +Y: composed they are a half turn, whose quaternion is (0,1,0,0).
|
|
100
|
+
const held = collider({
|
|
101
|
+
kind: 'box',
|
|
102
|
+
x: 0,
|
|
103
|
+
y: 0,
|
|
104
|
+
z: 0,
|
|
105
|
+
halfX: 1,
|
|
106
|
+
halfY: 1,
|
|
107
|
+
halfZ: 1,
|
|
108
|
+
rotationX: 0,
|
|
109
|
+
rotationY: Math.sin(Math.PI / 4),
|
|
110
|
+
rotationZ: 0,
|
|
111
|
+
rotationW: Math.cos(Math.PI / 4),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
updatePhysics3DColliderWorldShape(held, spunBody());
|
|
115
|
+
|
|
116
|
+
expect(held.world.kind === 'box' && held.world.rotationY).toBeCloseTo(1, 9);
|
|
117
|
+
expect(held.world.kind === 'box' && held.world.rotationW).toBeCloseTo(0, 9);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('moves both capsule endpoints', () => {
|
|
121
|
+
const body = createRigidBody3D('dynamic');
|
|
122
|
+
body.y = 5;
|
|
123
|
+
const held = collider({ kind: 'capsule', x0: 0, y0: -1, z0: 0, x1: 0, y1: 1, z1: 0, radius: 0.5 });
|
|
124
|
+
|
|
125
|
+
updatePhysics3DColliderWorldShape(held, body);
|
|
126
|
+
|
|
127
|
+
expect(held.world.kind === 'capsule' && held.world.y0).toBeCloseTo(4, 9);
|
|
128
|
+
expect(held.world.kind === 'capsule' && held.world.y1).toBeCloseTo(6, 9);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('moves every hull point', () => {
|
|
132
|
+
const body = createRigidBody3D('dynamic');
|
|
133
|
+
body.x = 100;
|
|
134
|
+
const held = collider({ kind: 'convex', points: [0, 0, 0, 1, 0, 0, 0, 1, 0] });
|
|
135
|
+
|
|
136
|
+
updatePhysics3DColliderWorldShape(held, body);
|
|
137
|
+
|
|
138
|
+
expect(held.world.kind === 'convex' && held.world.points).toEqual([100, 0, 0, 101, 0, 0, 100, 1, 0]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('writes in place, so a step over many colliders allocates nothing', () => {
|
|
142
|
+
const body = createRigidBody3D('dynamic');
|
|
143
|
+
const held = collider({ kind: 'sphere', x: 0, y: 0, z: 0, radius: 1 });
|
|
144
|
+
const before = held.world;
|
|
145
|
+
|
|
146
|
+
body.x = 1;
|
|
147
|
+
updatePhysics3DColliderWorldShape(held, body);
|
|
148
|
+
body.x = 2;
|
|
149
|
+
updatePhysics3DColliderWorldShape(held, body);
|
|
150
|
+
|
|
151
|
+
expect(held.world).toBe(before);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('writePhysics3DColliderBounds', () => {
|
|
156
|
+
it('bounds a sphere by its radius', () => {
|
|
157
|
+
const out = bounds();
|
|
158
|
+
const held = collider({ kind: 'sphere', x: 1, y: 2, z: 3, radius: 4 });
|
|
159
|
+
updatePhysics3DColliderWorldShape(held, createRigidBody3D('dynamic'));
|
|
160
|
+
|
|
161
|
+
writePhysics3DColliderBounds(held, out);
|
|
162
|
+
|
|
163
|
+
expect(out).toEqual({ minX: -3, minY: -2, minZ: -1, maxX: 5, maxY: 6, maxZ: 7 });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('grows an oriented box to the extent its rotation sweeps', () => {
|
|
167
|
+
const out = bounds();
|
|
168
|
+
const held = collider({ kind: 'aabb', minX: -1, minY: -1, minZ: -1, maxX: 1, maxY: 1, maxZ: 1 });
|
|
169
|
+
const body = createRigidBody3D('dynamic');
|
|
170
|
+
// An eighth turn about +Y, where a unit box's x/z extent grows to sqrt(2).
|
|
171
|
+
body.orientationY = Math.sin(Math.PI / 8);
|
|
172
|
+
body.orientationW = Math.cos(Math.PI / 8);
|
|
173
|
+
updatePhysics3DColliderWorldShape(held, body);
|
|
174
|
+
|
|
175
|
+
writePhysics3DColliderBounds(held, out);
|
|
176
|
+
|
|
177
|
+
expect(out.maxX).toBeCloseTo(Math.SQRT2, 9);
|
|
178
|
+
expect(out.maxZ).toBeCloseTo(Math.SQRT2, 9);
|
|
179
|
+
// The rotation axis is unaffected.
|
|
180
|
+
expect(out.maxY).toBeCloseTo(1, 9);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('leaves an unrotated box at its own extent rather than inflating it', () => {
|
|
184
|
+
const out = bounds();
|
|
185
|
+
const held = collider({ kind: 'aabb', minX: -1, minY: -2, minZ: -3, maxX: 1, maxY: 2, maxZ: 3 });
|
|
186
|
+
updatePhysics3DColliderWorldShape(held, createRigidBody3D('dynamic'));
|
|
187
|
+
|
|
188
|
+
writePhysics3DColliderBounds(held, out);
|
|
189
|
+
|
|
190
|
+
expect(out).toEqual({ minX: -1, minY: -2, minZ: -3, maxX: 1, maxY: 2, maxZ: 3 });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('bounds a capsule by both endpoints plus the radius', () => {
|
|
194
|
+
const out = bounds();
|
|
195
|
+
const held = collider({ kind: 'capsule', x0: -2, y0: 0, z0: 0, x1: 2, y1: 0, z1: 0, radius: 0.5 });
|
|
196
|
+
updatePhysics3DColliderWorldShape(held, createRigidBody3D('dynamic'));
|
|
197
|
+
|
|
198
|
+
writePhysics3DColliderBounds(held, out);
|
|
199
|
+
|
|
200
|
+
expect(out).toEqual({ minX: -2.5, minY: -0.5, minZ: -0.5, maxX: 2.5, maxY: 0.5, maxZ: 0.5 });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('bounds a hull by its extreme points', () => {
|
|
204
|
+
const out = bounds();
|
|
205
|
+
const held = collider({ kind: 'convex', points: [-1, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, -2] });
|
|
206
|
+
updatePhysics3DColliderWorldShape(held, createRigidBody3D('dynamic'));
|
|
207
|
+
|
|
208
|
+
writePhysics3DColliderBounds(held, out);
|
|
209
|
+
|
|
210
|
+
expect(out).toEqual({ minX: -1, minY: 0, minZ: -2, maxX: 3, maxY: 7, maxZ: 0 });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('zeroes the bounds of an empty hull rather than reporting infinities', () => {
|
|
214
|
+
const out = bounds();
|
|
215
|
+
const held = collider({ kind: 'convex', points: [] });
|
|
216
|
+
|
|
217
|
+
writePhysics3DColliderBounds(held, out);
|
|
218
|
+
|
|
219
|
+
expect(out).toEqual({ minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 });
|
|
220
|
+
});
|
|
221
|
+
});
|