@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,565 @@
|
|
|
1
|
+
import { createCollisionTriangleMesh3D, registerBuiltInCollisionSupports3D } from '@flighthq/collision/contract';
|
|
2
|
+
import type { CollisionBuiltInShape3D, Physics3DWorld, RigidBody3D } from '@flighthq/types/contract';
|
|
3
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
addPhysics3DBody,
|
|
7
|
+
addPhysics3DCollider,
|
|
8
|
+
createPhysics3DCollider,
|
|
9
|
+
createPhysics3DWorld,
|
|
10
|
+
createRigidBody3D,
|
|
11
|
+
} from './world';
|
|
12
|
+
import {
|
|
13
|
+
createPhysics3DQueryFilter,
|
|
14
|
+
createPhysics3DQueryResult,
|
|
15
|
+
createPhysics3DRayResult,
|
|
16
|
+
createPhysics3DShapeCastResult,
|
|
17
|
+
queryPhysics3DPoint,
|
|
18
|
+
queryPhysics3DRay,
|
|
19
|
+
queryPhysics3DRayClosest,
|
|
20
|
+
queryPhysics3DRegion,
|
|
21
|
+
queryPhysics3DShapeCast,
|
|
22
|
+
} from './worldQueries';
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
registerBuiltInCollisionSupports3D();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function unitBox(): CollisionBuiltInShape3D {
|
|
29
|
+
return { kind: 'aabb', minX: -0.5, minY: -0.5, minZ: -0.5, maxX: 0.5, maxY: 0.5, maxZ: 0.5 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function addBoxBody(
|
|
33
|
+
world: Physics3DWorld,
|
|
34
|
+
x: number,
|
|
35
|
+
y: number,
|
|
36
|
+
z: number,
|
|
37
|
+
type: RigidBody3D['type'] = 'dynamic',
|
|
38
|
+
sensor = false,
|
|
39
|
+
): RigidBody3D {
|
|
40
|
+
const body = createRigidBody3D(type);
|
|
41
|
+
body.x = x;
|
|
42
|
+
body.y = y;
|
|
43
|
+
body.z = z;
|
|
44
|
+
addPhysics3DBody(world, body);
|
|
45
|
+
addPhysics3DCollider(world, body, createPhysics3DCollider(unitBox(), undefined, undefined, sensor));
|
|
46
|
+
return body;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('createPhysics3DQueryFilter', () => {
|
|
50
|
+
it('admits everything by default, so a filter is opt-out rather than opt-in', () => {
|
|
51
|
+
expect(createPhysics3DQueryFilter()).toEqual({
|
|
52
|
+
categoryBits: 0xffffffff,
|
|
53
|
+
maskBits: 0xffffffff,
|
|
54
|
+
includeSensors: true,
|
|
55
|
+
includeDynamic: true,
|
|
56
|
+
includeKinematic: true,
|
|
57
|
+
includeStatic: true,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('createPhysics3DQueryResult', () => {
|
|
63
|
+
it('starts empty', () => {
|
|
64
|
+
expect(createPhysics3DQueryResult()).toEqual({ hits: [], hitCount: 0 });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('createPhysics3DRayResult', () => {
|
|
69
|
+
it('starts empty', () => {
|
|
70
|
+
expect(createPhysics3DRayResult()).toEqual({ hits: [], hitCount: 0 });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('createPhysics3DShapeCastResult', () => {
|
|
75
|
+
it('starts as a miss with no body', () => {
|
|
76
|
+
const out = createPhysics3DShapeCastResult();
|
|
77
|
+
expect(out.hit).toBe(false);
|
|
78
|
+
expect(out.body).toBeNull();
|
|
79
|
+
expect(out.collider).toBeNull();
|
|
80
|
+
expect(out.colliderIndex).toBe(-1);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('queryPhysics3DPoint', () => {
|
|
85
|
+
it('finds the collider containing the point', () => {
|
|
86
|
+
const world = createPhysics3DWorld();
|
|
87
|
+
const body = addBoxBody(world, 0, 0, 0);
|
|
88
|
+
const out = createPhysics3DQueryResult();
|
|
89
|
+
|
|
90
|
+
queryPhysics3DPoint(world, 0.1, 0.1, 0.1, out);
|
|
91
|
+
|
|
92
|
+
expect(out.hitCount).toBe(1);
|
|
93
|
+
expect(out.hits[0].body).toBe(body);
|
|
94
|
+
expect(out.hits[0].colliderIndex).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('finds nothing at a point in empty space', () => {
|
|
98
|
+
const world = createPhysics3DWorld();
|
|
99
|
+
addBoxBody(world, 0, 0, 0);
|
|
100
|
+
const out = createPhysics3DQueryResult();
|
|
101
|
+
|
|
102
|
+
queryPhysics3DPoint(world, 5, 5, 5, out);
|
|
103
|
+
|
|
104
|
+
expect(out.hitCount).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('separates on Z, so a point out of plane does not hit', () => {
|
|
108
|
+
const world = createPhysics3DWorld();
|
|
109
|
+
addBoxBody(world, 0, 0, 0);
|
|
110
|
+
const out = createPhysics3DQueryResult();
|
|
111
|
+
|
|
112
|
+
queryPhysics3DPoint(world, 0, 0, 5, out);
|
|
113
|
+
|
|
114
|
+
expect(out.hitCount).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('sees the CURRENT pose without needing a step first', () => {
|
|
118
|
+
// The synchronize-first rule. A caller that moves a body and picks immediately must not query the
|
|
119
|
+
// pose the last step published.
|
|
120
|
+
const world = createPhysics3DWorld();
|
|
121
|
+
const body = addBoxBody(world, 0, 0, 0);
|
|
122
|
+
const out = createPhysics3DQueryResult();
|
|
123
|
+
|
|
124
|
+
body.x = 10;
|
|
125
|
+
queryPhysics3DPoint(world, 10, 0, 0, out);
|
|
126
|
+
|
|
127
|
+
expect(out.hitCount).toBe(1);
|
|
128
|
+
queryPhysics3DPoint(world, 0, 0, 0, out);
|
|
129
|
+
expect(out.hitCount).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('excludes a body type the filter turned off', () => {
|
|
133
|
+
const world = createPhysics3DWorld();
|
|
134
|
+
addBoxBody(world, 0, 0, 0, 'static');
|
|
135
|
+
const filter = createPhysics3DQueryFilter();
|
|
136
|
+
filter.includeStatic = false;
|
|
137
|
+
const out = createPhysics3DQueryResult();
|
|
138
|
+
|
|
139
|
+
queryPhysics3DPoint(world, 0, 0, 0, out, filter);
|
|
140
|
+
|
|
141
|
+
expect(out.hitCount).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('excludes sensors when the filter says so', () => {
|
|
145
|
+
const world = createPhysics3DWorld();
|
|
146
|
+
addBoxBody(world, 0, 0, 0, 'dynamic', true);
|
|
147
|
+
const filter = createPhysics3DQueryFilter();
|
|
148
|
+
filter.includeSensors = false;
|
|
149
|
+
const out = createPhysics3DQueryResult();
|
|
150
|
+
|
|
151
|
+
queryPhysics3DPoint(world, 0, 0, 0, out, filter);
|
|
152
|
+
|
|
153
|
+
expect(out.hitCount).toBe(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('excludes a collider whose category the filter mask does not select', () => {
|
|
157
|
+
const world = createPhysics3DWorld();
|
|
158
|
+
const body = createRigidBody3D('dynamic');
|
|
159
|
+
addPhysics3DBody(world, body);
|
|
160
|
+
addPhysics3DCollider(
|
|
161
|
+
world,
|
|
162
|
+
body,
|
|
163
|
+
createPhysics3DCollider(unitBox(), undefined, { categoryBits: 2, maskBits: 2, groupIndex: 0 }),
|
|
164
|
+
);
|
|
165
|
+
const filter = createPhysics3DQueryFilter();
|
|
166
|
+
filter.categoryBits = 1;
|
|
167
|
+
const out = createPhysics3DQueryResult();
|
|
168
|
+
|
|
169
|
+
queryPhysics3DPoint(world, 0, 0, 0, out, filter);
|
|
170
|
+
|
|
171
|
+
expect(out.hitCount).toBe(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('orders hits by body index rather than by broadphase history', () => {
|
|
175
|
+
const world = createPhysics3DWorld();
|
|
176
|
+
const first = addBoxBody(world, 0, 0, 0);
|
|
177
|
+
const second = addBoxBody(world, 0, 0, 0);
|
|
178
|
+
const out = createPhysics3DQueryResult();
|
|
179
|
+
|
|
180
|
+
queryPhysics3DPoint(world, 0, 0, 0, out);
|
|
181
|
+
|
|
182
|
+
expect(out.hitCount).toBe(2);
|
|
183
|
+
expect(out.hits[0].body.index).toBeLessThan(out.hits[1].body.index);
|
|
184
|
+
expect([first.index, second.index]).toContain(out.hits[0].body.index);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('declines a non-finite point rather than reporting a hit', () => {
|
|
188
|
+
const world = createPhysics3DWorld();
|
|
189
|
+
addBoxBody(world, 0, 0, 0);
|
|
190
|
+
const out = createPhysics3DQueryResult();
|
|
191
|
+
|
|
192
|
+
queryPhysics3DPoint(world, Number.NaN, 0, 0, out);
|
|
193
|
+
|
|
194
|
+
expect(out.hitCount).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('reuses hit records across calls, so a picking loop allocates nothing after its first frame', () => {
|
|
198
|
+
const world = createPhysics3DWorld();
|
|
199
|
+
addBoxBody(world, 0, 0, 0);
|
|
200
|
+
const out = createPhysics3DQueryResult();
|
|
201
|
+
|
|
202
|
+
queryPhysics3DPoint(world, 0, 0, 0, out);
|
|
203
|
+
const record = out.hits[0];
|
|
204
|
+
queryPhysics3DPoint(world, 5, 5, 5, out);
|
|
205
|
+
expect(out.hitCount).toBe(0);
|
|
206
|
+
queryPhysics3DPoint(world, 0, 0, 0, out);
|
|
207
|
+
|
|
208
|
+
expect(out.hits[0]).toBe(record);
|
|
209
|
+
// The stale record above the live prefix is retained on purpose and must not be published.
|
|
210
|
+
expect(out.hits.length).toBeGreaterThanOrEqual(out.hitCount);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('queryPhysics3DRay', () => {
|
|
215
|
+
it('raycasts static triangle mesh surfaces', () => {
|
|
216
|
+
const world = createPhysics3DWorld();
|
|
217
|
+
const terrain = createRigidBody3D('static');
|
|
218
|
+
terrain.colliders.push(
|
|
219
|
+
createPhysics3DCollider(createCollisionTriangleMesh3D([-5, 0, -5, 5, 0, -5, 0, 0, 5], [0, 1, 2])),
|
|
220
|
+
);
|
|
221
|
+
addPhysics3DBody(world, terrain);
|
|
222
|
+
const out = createPhysics3DRayResult();
|
|
223
|
+
|
|
224
|
+
queryPhysics3DRay(world, 0, 2, 0, 0, -1, 0, out);
|
|
225
|
+
|
|
226
|
+
expect(out.hitCount).toBe(1);
|
|
227
|
+
expect(out.hits[0].body).toBe(terrain);
|
|
228
|
+
expect(out.hits[0].fraction).toBeCloseTo(2, 12);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('finds every collider along the ray, nearest first', () => {
|
|
232
|
+
const world = createPhysics3DWorld();
|
|
233
|
+
const far = addBoxBody(world, 10, 0, 0);
|
|
234
|
+
const near = addBoxBody(world, 5, 0, 0);
|
|
235
|
+
const out = createPhysics3DRayResult();
|
|
236
|
+
|
|
237
|
+
queryPhysics3DRay(world, -10, 0, 0, 1, 0, 0, out);
|
|
238
|
+
|
|
239
|
+
expect(out.hitCount).toBe(2);
|
|
240
|
+
expect(out.hits[0].body).toBe(near);
|
|
241
|
+
expect(out.hits[1].body).toBe(far);
|
|
242
|
+
expect(out.hits[0].fraction).toBeLessThan(out.hits[1].fraction);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('reports the hit point on the ray in the caller parameterization', () => {
|
|
246
|
+
const world = createPhysics3DWorld();
|
|
247
|
+
addBoxBody(world, 5, 0, 0);
|
|
248
|
+
const out = createPhysics3DRayResult();
|
|
249
|
+
|
|
250
|
+
queryPhysics3DRay(world, -10, 0, 0, 2, 0, 0, out);
|
|
251
|
+
|
|
252
|
+
const hit = out.hits[0];
|
|
253
|
+
expect(hit.x).toBeCloseTo(-10 + 2 * hit.fraction, 9);
|
|
254
|
+
expect(hit.normalX).toBeCloseTo(-1, 9);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('bounds the ray with maxFraction', () => {
|
|
258
|
+
const world = createPhysics3DWorld();
|
|
259
|
+
addBoxBody(world, 5, 0, 0);
|
|
260
|
+
const out = createPhysics3DRayResult();
|
|
261
|
+
|
|
262
|
+
queryPhysics3DRay(world, 0, 0, 0, 1, 0, 0, out, 4);
|
|
263
|
+
expect(out.hitCount).toBe(0);
|
|
264
|
+
queryPhysics3DRay(world, 0, 0, 0, 1, 0, 0, out, 5);
|
|
265
|
+
expect(out.hitCount).toBe(1);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('finds nothing when the ray points away', () => {
|
|
269
|
+
const world = createPhysics3DWorld();
|
|
270
|
+
addBoxBody(world, 5, 0, 0);
|
|
271
|
+
const out = createPhysics3DRayResult();
|
|
272
|
+
|
|
273
|
+
queryPhysics3DRay(world, 0, 0, 0, -1, 0, 0, out);
|
|
274
|
+
|
|
275
|
+
expect(out.hitCount).toBe(0);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('respects the filter', () => {
|
|
279
|
+
const world = createPhysics3DWorld();
|
|
280
|
+
addBoxBody(world, 5, 0, 0, 'static');
|
|
281
|
+
const filter = createPhysics3DQueryFilter();
|
|
282
|
+
filter.includeStatic = false;
|
|
283
|
+
const out = createPhysics3DRayResult();
|
|
284
|
+
|
|
285
|
+
queryPhysics3DRay(world, 0, 0, 0, 1, 0, 0, out, Number.POSITIVE_INFINITY, filter);
|
|
286
|
+
|
|
287
|
+
expect(out.hitCount).toBe(0);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('declines a non-finite ray', () => {
|
|
291
|
+
const world = createPhysics3DWorld();
|
|
292
|
+
addBoxBody(world, 5, 0, 0);
|
|
293
|
+
const out = createPhysics3DRayResult();
|
|
294
|
+
|
|
295
|
+
queryPhysics3DRay(world, 0, 0, 0, Number.NaN, 0, 0, out);
|
|
296
|
+
|
|
297
|
+
expect(out.hitCount).toBe(0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('sorts only the live prefix, leaving stale records above hitCount unpublished', () => {
|
|
301
|
+
const world = createPhysics3DWorld();
|
|
302
|
+
addBoxBody(world, 5, 0, 0);
|
|
303
|
+
addBoxBody(world, 10, 0, 0);
|
|
304
|
+
const out = createPhysics3DRayResult();
|
|
305
|
+
|
|
306
|
+
queryPhysics3DRay(world, 0, 0, 0, 1, 0, 0, out);
|
|
307
|
+
expect(out.hitCount).toBe(2);
|
|
308
|
+
queryPhysics3DRay(world, 0, 0, 0, 1, 0, 0, out, 6);
|
|
309
|
+
|
|
310
|
+
expect(out.hitCount).toBe(1);
|
|
311
|
+
expect(out.hits[0].fraction).toBeCloseTo(4.5, 6);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('queryPhysics3DRayClosest', () => {
|
|
316
|
+
it('writes only the nearest hit', () => {
|
|
317
|
+
const world = createPhysics3DWorld();
|
|
318
|
+
addBoxBody(world, 10, 0, 0);
|
|
319
|
+
const near = addBoxBody(world, 5, 0, 0);
|
|
320
|
+
const out = createPhysics3DRayResult();
|
|
321
|
+
|
|
322
|
+
queryPhysics3DRayClosest(world, -10, 0, 0, 1, 0, 0, out);
|
|
323
|
+
|
|
324
|
+
expect(out.hitCount).toBe(1);
|
|
325
|
+
expect(out.hits[0].body).toBe(near);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('breaks an exact tie by body index, not by traversal order', () => {
|
|
329
|
+
const world = createPhysics3DWorld();
|
|
330
|
+
// Two identical boxes at the same place: the fractions are exactly equal.
|
|
331
|
+
const first = addBoxBody(world, 5, 0, 0);
|
|
332
|
+
addBoxBody(world, 5, 0, 0);
|
|
333
|
+
const out = createPhysics3DRayResult();
|
|
334
|
+
|
|
335
|
+
queryPhysics3DRayClosest(world, 0, 0, 0, 1, 0, 0, out);
|
|
336
|
+
|
|
337
|
+
expect(out.hitCount).toBe(1);
|
|
338
|
+
expect(out.hits[0].body).toBe(first);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('finds nothing when the ray misses everything', () => {
|
|
342
|
+
const world = createPhysics3DWorld();
|
|
343
|
+
addBoxBody(world, 5, 0, 0);
|
|
344
|
+
const out = createPhysics3DRayResult();
|
|
345
|
+
|
|
346
|
+
queryPhysics3DRayClosest(world, 0, 50, 0, 1, 0, 0, out);
|
|
347
|
+
|
|
348
|
+
expect(out.hitCount).toBe(0);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('queryPhysics3DRegion', () => {
|
|
353
|
+
it('finds colliders overlapping the region', () => {
|
|
354
|
+
const world = createPhysics3DWorld();
|
|
355
|
+
const inside = addBoxBody(world, 0, 0, 0);
|
|
356
|
+
addBoxBody(world, 50, 0, 0);
|
|
357
|
+
const out = createPhysics3DQueryResult();
|
|
358
|
+
|
|
359
|
+
queryPhysics3DRegion(world, { minX: -1, minY: -1, minZ: -1, maxX: 1, maxY: 1, maxZ: 1 }, out);
|
|
360
|
+
|
|
361
|
+
expect(out.hitCount).toBe(1);
|
|
362
|
+
expect(out.hits[0].body).toBe(inside);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('refines a compound body per collider, not by its aggregate bounds', () => {
|
|
366
|
+
// The index holds one volume for the whole body, so a region in the empty gap between two colliders
|
|
367
|
+
// is a broadphase candidate. Publishing it would report geometry that is not there.
|
|
368
|
+
const world = createPhysics3DWorld();
|
|
369
|
+
const body = createRigidBody3D('dynamic');
|
|
370
|
+
addPhysics3DBody(world, body);
|
|
371
|
+
addPhysics3DCollider(
|
|
372
|
+
world,
|
|
373
|
+
body,
|
|
374
|
+
createPhysics3DCollider({ kind: 'aabb', minX: -10.5, minY: -0.5, minZ: -0.5, maxX: -9.5, maxY: 0.5, maxZ: 0.5 }),
|
|
375
|
+
);
|
|
376
|
+
addPhysics3DCollider(
|
|
377
|
+
world,
|
|
378
|
+
body,
|
|
379
|
+
createPhysics3DCollider({ kind: 'aabb', minX: 9.5, minY: -0.5, minZ: -0.5, maxX: 10.5, maxY: 0.5, maxZ: 0.5 }),
|
|
380
|
+
);
|
|
381
|
+
const out = createPhysics3DQueryResult();
|
|
382
|
+
|
|
383
|
+
queryPhysics3DRegion(world, { minX: -1, minY: -1, minZ: -1, maxX: 1, maxY: 1, maxZ: 1 }, out);
|
|
384
|
+
|
|
385
|
+
expect(out.hitCount).toBe(0);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('finds both colliders of a compound body when the region covers both', () => {
|
|
389
|
+
const world = createPhysics3DWorld();
|
|
390
|
+
const body = createRigidBody3D('dynamic');
|
|
391
|
+
addPhysics3DBody(world, body);
|
|
392
|
+
addPhysics3DCollider(
|
|
393
|
+
world,
|
|
394
|
+
body,
|
|
395
|
+
createPhysics3DCollider({ kind: 'aabb', minX: -2.5, minY: -0.5, minZ: -0.5, maxX: -1.5, maxY: 0.5, maxZ: 0.5 }),
|
|
396
|
+
);
|
|
397
|
+
addPhysics3DCollider(
|
|
398
|
+
world,
|
|
399
|
+
body,
|
|
400
|
+
createPhysics3DCollider({ kind: 'aabb', minX: 1.5, minY: -0.5, minZ: -0.5, maxX: 2.5, maxY: 0.5, maxZ: 0.5 }),
|
|
401
|
+
);
|
|
402
|
+
const out = createPhysics3DQueryResult();
|
|
403
|
+
|
|
404
|
+
queryPhysics3DRegion(world, { minX: -5, minY: -5, minZ: -5, maxX: 5, maxY: 5, maxZ: 5 }, out);
|
|
405
|
+
|
|
406
|
+
expect(out.hitCount).toBe(2);
|
|
407
|
+
expect(out.hits.slice(0, 2).map((h) => h.colliderIndex)).toEqual([0, 1]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('declines an inverted or non-finite region', () => {
|
|
411
|
+
const world = createPhysics3DWorld();
|
|
412
|
+
addBoxBody(world, 0, 0, 0);
|
|
413
|
+
const out = createPhysics3DQueryResult();
|
|
414
|
+
|
|
415
|
+
queryPhysics3DRegion(world, { minX: 1, minY: -1, minZ: -1, maxX: -1, maxY: 1, maxZ: 1 }, out);
|
|
416
|
+
expect(out.hitCount).toBe(0);
|
|
417
|
+
queryPhysics3DRegion(world, { minX: Number.NaN, minY: -1, minZ: -1, maxX: 1, maxY: 1, maxZ: 1 }, out);
|
|
418
|
+
expect(out.hitCount).toBe(0);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('respects the filter', () => {
|
|
422
|
+
const world = createPhysics3DWorld();
|
|
423
|
+
addBoxBody(world, 0, 0, 0, 'kinematic');
|
|
424
|
+
const filter = createPhysics3DQueryFilter();
|
|
425
|
+
filter.includeKinematic = false;
|
|
426
|
+
const out = createPhysics3DQueryResult();
|
|
427
|
+
|
|
428
|
+
queryPhysics3DRegion(world, { minX: -1, minY: -1, minZ: -1, maxX: 1, maxY: 1, maxZ: 1 }, out);
|
|
429
|
+
expect(out.hitCount).toBe(1);
|
|
430
|
+
queryPhysics3DRegion(world, { minX: -1, minY: -1, minZ: -1, maxX: 1, maxY: 1, maxZ: 1 }, out, filter);
|
|
431
|
+
expect(out.hitCount).toBe(0);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe('queryPhysics3DShapeCast', () => {
|
|
436
|
+
it('casts convex shapes against static triangle mesh surfaces', () => {
|
|
437
|
+
const world = createPhysics3DWorld();
|
|
438
|
+
const terrain = createRigidBody3D('static');
|
|
439
|
+
terrain.colliders.push(
|
|
440
|
+
createPhysics3DCollider(createCollisionTriangleMesh3D([-5, 0, -5, 5, 0, -5, 0, 0, 5], [0, 1, 2])),
|
|
441
|
+
);
|
|
442
|
+
addPhysics3DBody(world, terrain);
|
|
443
|
+
const out = createPhysics3DShapeCastResult();
|
|
444
|
+
const sphere: CollisionBuiltInShape3D = { kind: 'sphere', x: 0, y: 2, z: 0, radius: 0.25 };
|
|
445
|
+
|
|
446
|
+
queryPhysics3DShapeCast(world, sphere, 0, -4, 0, out);
|
|
447
|
+
|
|
448
|
+
expect(out.hit).toBe(true);
|
|
449
|
+
expect(out.body).toBe(terrain);
|
|
450
|
+
expect(out.fraction).toBeCloseTo(0.4375, 5);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('stops a swept shape at the first collider and reports where', () => {
|
|
454
|
+
const world = createPhysics3DWorld();
|
|
455
|
+
addBoxBody(world, 10, 0, 0, 'static');
|
|
456
|
+
const out = createPhysics3DShapeCastResult();
|
|
457
|
+
|
|
458
|
+
// A unit box at the origin swept 20 along +x. The target's near face is at 9.5 and the caster's own
|
|
459
|
+
// half-extent is 0.5, so contact is at 9 of 20.
|
|
460
|
+
queryPhysics3DShapeCast(world, unitBox(), 20, 0, 0, out);
|
|
461
|
+
|
|
462
|
+
expect(out.hit).toBe(true);
|
|
463
|
+
expect(out.fraction).toBeCloseTo(9 / 20, 4);
|
|
464
|
+
expect(out.body?.x).toBe(10);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('reports a clean miss when nothing is in the swept path', () => {
|
|
468
|
+
const world = createPhysics3DWorld();
|
|
469
|
+
addBoxBody(world, 0, 50, 0, 'static');
|
|
470
|
+
const out = createPhysics3DShapeCastResult();
|
|
471
|
+
|
|
472
|
+
queryPhysics3DShapeCast(world, unitBox(), 20, 0, 0, out);
|
|
473
|
+
|
|
474
|
+
expect(out.hit).toBe(false);
|
|
475
|
+
expect(out.body).toBeNull();
|
|
476
|
+
expect(out.fraction).toBe(0);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('finds a collider the sweep reaches but the start position does not touch', () => {
|
|
480
|
+
// The whole reason the broadphase is asked for the SWEPT bounds. Querying the start box alone would
|
|
481
|
+
// return only what the shape is already touching — nothing — and report a miss.
|
|
482
|
+
const world = createPhysics3DWorld();
|
|
483
|
+
addBoxBody(world, 30, 0, 0, 'static');
|
|
484
|
+
const out = createPhysics3DShapeCastResult();
|
|
485
|
+
|
|
486
|
+
queryPhysics3DShapeCast(world, unitBox(), 100, 0, 0, out);
|
|
487
|
+
|
|
488
|
+
expect(out.hit).toBe(true);
|
|
489
|
+
expect(out.body?.x).toBe(30);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('reports the NEAREST of several colliders along the sweep', () => {
|
|
493
|
+
const world = createPhysics3DWorld();
|
|
494
|
+
addBoxBody(world, 30, 0, 0, 'static');
|
|
495
|
+
const near = addBoxBody(world, 10, 0, 0, 'static');
|
|
496
|
+
addBoxBody(world, 20, 0, 0, 'static');
|
|
497
|
+
const out = createPhysics3DShapeCastResult();
|
|
498
|
+
|
|
499
|
+
queryPhysics3DShapeCast(world, unitBox(), 100, 0, 0, out);
|
|
500
|
+
|
|
501
|
+
// Added out of order on purpose: the answer must come from the fraction, not from insertion order.
|
|
502
|
+
expect(out.body).toBe(near);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('respects maxFraction, stopping short of a collider beyond it', () => {
|
|
506
|
+
const world = createPhysics3DWorld();
|
|
507
|
+
addBoxBody(world, 10, 0, 0, 'static');
|
|
508
|
+
const out = createPhysics3DShapeCastResult();
|
|
509
|
+
|
|
510
|
+
// Contact would be at 0.45 of the full displacement; bounding the sweep at 0.2 must not reach it.
|
|
511
|
+
queryPhysics3DShapeCast(world, unitBox(), 20, 0, 0, out, 0.2);
|
|
512
|
+
expect(out.hit).toBe(false);
|
|
513
|
+
|
|
514
|
+
queryPhysics3DShapeCast(world, unitBox(), 20, 0, 0, out, 0.5);
|
|
515
|
+
expect(out.hit).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('honours the query filter', () => {
|
|
519
|
+
const world = createPhysics3DWorld();
|
|
520
|
+
addBoxBody(world, 10, 0, 0, 'static');
|
|
521
|
+
const out = createPhysics3DShapeCastResult();
|
|
522
|
+
const filter = createPhysics3DQueryFilter();
|
|
523
|
+
filter.includeStatic = false;
|
|
524
|
+
|
|
525
|
+
queryPhysics3DShapeCast(world, unitBox(), 20, 0, 0, out, 1, filter);
|
|
526
|
+
expect(out.hit).toBe(false);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('returns a miss for a non-finite displacement rather than throwing', () => {
|
|
530
|
+
const world = createPhysics3DWorld();
|
|
531
|
+
addBoxBody(world, 10, 0, 0, 'static');
|
|
532
|
+
const out = createPhysics3DShapeCastResult();
|
|
533
|
+
|
|
534
|
+
queryPhysics3DShapeCast(world, unitBox(), NaN, 0, 0, out);
|
|
535
|
+
expect(out.hit).toBe(false);
|
|
536
|
+
queryPhysics3DShapeCast(world, unitBox(), 20, 0, 0, out, Number.POSITIVE_INFINITY);
|
|
537
|
+
expect(out.hit).toBe(false);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('finds a gap a raycast would pass straight through', () => {
|
|
541
|
+
// The case that justifies the query existing. Two walls leave a 0.8-tall gap that the centre LINE
|
|
542
|
+
// clears; the swept box is 1.0 tall and does not fit. The gap has to be narrower than the caster or
|
|
543
|
+
// the correct answer is a miss — which is what a first version of this test actually asserted
|
|
544
|
+
// against, with a 1.4 gap and a 1.0 box.
|
|
545
|
+
const world = createPhysics3DWorld();
|
|
546
|
+
const low = createRigidBody3D('static');
|
|
547
|
+
low.x = 10;
|
|
548
|
+
low.y = -0.9;
|
|
549
|
+
addPhysics3DBody(world, low);
|
|
550
|
+
addPhysics3DCollider(world, low, createPhysics3DCollider(unitBox()));
|
|
551
|
+
const high = createRigidBody3D('static');
|
|
552
|
+
high.x = 10;
|
|
553
|
+
high.y = 0.9;
|
|
554
|
+
addPhysics3DBody(world, high);
|
|
555
|
+
addPhysics3DCollider(world, high, createPhysics3DCollider(unitBox()));
|
|
556
|
+
|
|
557
|
+
const rayOut = createPhysics3DRayResult();
|
|
558
|
+
queryPhysics3DRay(world, 0, 0, 0, 20, 0, 0, rayOut);
|
|
559
|
+
expect(rayOut.hitCount).toBe(0);
|
|
560
|
+
|
|
561
|
+
const out = createPhysics3DShapeCastResult();
|
|
562
|
+
queryPhysics3DShapeCast(world, unitBox(), 20, 0, 0, out);
|
|
563
|
+
expect(out.hit).toBe(true);
|
|
564
|
+
});
|
|
565
|
+
});
|