@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,288 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCollisionHeightfield3D,
|
|
3
|
+
createCollisionTriangleMesh3D,
|
|
4
|
+
registerBuiltInCollisionFaceQueries3D,
|
|
5
|
+
registerBuiltInCollisionSupports3D,
|
|
6
|
+
} from '@flighthq/collision/contract';
|
|
7
|
+
import type { CollisionBuiltInShape3D, Physics3DWorld, RigidBody3D } from '@flighthq/types/contract';
|
|
8
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import { buildPhysics3DContacts } from './contactIntake';
|
|
11
|
+
import { createPhysics3DDebugGeometry, writePhysics3DDebugGeometry } from './debugGeometry';
|
|
12
|
+
import { createPhysics3DBallAndSocketJoint } from './jointFactories';
|
|
13
|
+
import { addPhysics3DJoint } from './jointRegistry';
|
|
14
|
+
import { registerBuiltInPhysics3DJointSolvers } from './registerBuiltInPhysics3DJointSolvers';
|
|
15
|
+
import {
|
|
16
|
+
addPhysics3DBody,
|
|
17
|
+
addPhysics3DCollider,
|
|
18
|
+
createPhysics3DCollider,
|
|
19
|
+
createPhysics3DWorld,
|
|
20
|
+
createRigidBody3D,
|
|
21
|
+
} from './world';
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
registerBuiltInCollisionSupports3D();
|
|
25
|
+
registerBuiltInCollisionFaceQueries3D();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function addBody(world: Physics3DWorld, local: CollisionBuiltInShape3D, y = 0): RigidBody3D {
|
|
29
|
+
const body = createRigidBody3D('dynamic');
|
|
30
|
+
body.y = y;
|
|
31
|
+
addPhysics3DBody(world, body);
|
|
32
|
+
addPhysics3DCollider(world, body, createPhysics3DCollider(local));
|
|
33
|
+
return body;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const unitBox: CollisionBuiltInShape3D = {
|
|
37
|
+
kind: 'aabb',
|
|
38
|
+
minX: -0.5,
|
|
39
|
+
minY: -0.5,
|
|
40
|
+
minZ: -0.5,
|
|
41
|
+
maxX: 0.5,
|
|
42
|
+
maxY: 0.5,
|
|
43
|
+
maxZ: 0.5,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function liveLines(out: ReturnType<typeof createPhysics3DDebugGeometry>) {
|
|
47
|
+
return out.lines.slice(0, out.lineCount);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function liveSpheres(out: ReturnType<typeof createPhysics3DDebugGeometry>) {
|
|
51
|
+
return out.spheres.slice(0, out.sphereCount);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('createPhysics3DDebugGeometry', () => {
|
|
55
|
+
it('starts empty', () => {
|
|
56
|
+
expect(createPhysics3DDebugGeometry()).toEqual({ lines: [], lineCount: 0, spheres: [], sphereCount: 0 });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('writePhysics3DDebugGeometry', () => {
|
|
61
|
+
it('draws explicit mesh triangles and heightfield grid edges', () => {
|
|
62
|
+
const world = createPhysics3DWorld();
|
|
63
|
+
const mesh = createRigidBody3D('static');
|
|
64
|
+
mesh.colliders.push(createPhysics3DCollider(createCollisionTriangleMesh3D([0, 0, 0, 1, 0, 0, 0, 0, 1], [0, 1, 2])));
|
|
65
|
+
addPhysics3DBody(world, mesh);
|
|
66
|
+
const heightfield = createRigidBody3D('static');
|
|
67
|
+
heightfield.colliders.push(createPhysics3DCollider(createCollisionHeightfield3D(2, 2, [0, 0, 0, 0])));
|
|
68
|
+
addPhysics3DBody(world, heightfield);
|
|
69
|
+
const out = createPhysics3DDebugGeometry();
|
|
70
|
+
|
|
71
|
+
writePhysics3DDebugGeometry(world, out, { drawColliders: true, drawCentersOfMass: false });
|
|
72
|
+
|
|
73
|
+
expect(liveLines(out).filter((line) => line.feature === 'collider')).toHaveLength(8);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('draws a box as its twelve edges', () => {
|
|
77
|
+
const world = createPhysics3DWorld();
|
|
78
|
+
addBody(world, unitBox);
|
|
79
|
+
const out = createPhysics3DDebugGeometry();
|
|
80
|
+
|
|
81
|
+
writePhysics3DDebugGeometry(world, out, { drawColliders: true, drawCentersOfMass: false });
|
|
82
|
+
|
|
83
|
+
expect(liveLines(out).filter((line) => line.feature === 'collider')).toHaveLength(12);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('gives every box edge unit length for a unit cube', () => {
|
|
87
|
+
// Twelve lines that are not all edges — a diagonal, a repeat — would still count twelve.
|
|
88
|
+
const world = createPhysics3DWorld();
|
|
89
|
+
addBody(world, unitBox);
|
|
90
|
+
const out = createPhysics3DDebugGeometry();
|
|
91
|
+
|
|
92
|
+
writePhysics3DDebugGeometry(world, out, { drawCentersOfMass: false, drawContacts: false });
|
|
93
|
+
|
|
94
|
+
for (const line of liveLines(out)) {
|
|
95
|
+
expect(Math.hypot(line.x1 - line.x0, line.y1 - line.y0, line.z1 - line.z0)).toBeCloseTo(1, 9);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('follows the body pose rather than drawing at the origin', () => {
|
|
100
|
+
const world = createPhysics3DWorld();
|
|
101
|
+
const body = addBody(world, unitBox);
|
|
102
|
+
const out = createPhysics3DDebugGeometry();
|
|
103
|
+
|
|
104
|
+
body.x = 100;
|
|
105
|
+
writePhysics3DDebugGeometry(world, out, { drawCentersOfMass: false });
|
|
106
|
+
|
|
107
|
+
for (const line of liveLines(out)) expect(line.x0).toBeGreaterThan(99);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('turns a spun box into edges that are no longer axis-aligned', () => {
|
|
111
|
+
const world = createPhysics3DWorld();
|
|
112
|
+
const body = addBody(world, unitBox);
|
|
113
|
+
body.orientationY = Math.sin(Math.PI / 8);
|
|
114
|
+
body.orientationW = Math.cos(Math.PI / 8);
|
|
115
|
+
const out = createPhysics3DDebugGeometry();
|
|
116
|
+
|
|
117
|
+
writePhysics3DDebugGeometry(world, out, { drawCentersOfMass: false });
|
|
118
|
+
|
|
119
|
+
// An eighth turn about Y leaves no edge lying along a world axis in both x and z.
|
|
120
|
+
const axisAligned = liveLines(out).filter(
|
|
121
|
+
(line) => Math.abs(line.x1 - line.x0) < 1e-9 && Math.abs(line.z1 - line.z0) < 1e-9,
|
|
122
|
+
);
|
|
123
|
+
// Only the four vertical edges stay axis-aligned under a Y rotation.
|
|
124
|
+
expect(axisAligned).toHaveLength(4);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('draws a sphere collider as one sphere', () => {
|
|
128
|
+
const world = createPhysics3DWorld();
|
|
129
|
+
addBody(world, { kind: 'sphere', x: 0, y: 0, z: 0, radius: 2 });
|
|
130
|
+
const out = createPhysics3DDebugGeometry();
|
|
131
|
+
|
|
132
|
+
writePhysics3DDebugGeometry(world, out, { drawCentersOfMass: false });
|
|
133
|
+
|
|
134
|
+
const spheres = liveSpheres(out).filter((sphere) => sphere.feature === 'collider');
|
|
135
|
+
expect(spheres).toHaveLength(1);
|
|
136
|
+
expect(spheres[0].radius).toBe(2);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('draws a capsule as two caps joined by its axis', () => {
|
|
140
|
+
const world = createPhysics3DWorld();
|
|
141
|
+
addBody(world, { kind: 'capsule', x0: -2, y0: 0, z0: 0, x1: 2, y1: 0, z1: 0, radius: 0.5 });
|
|
142
|
+
const out = createPhysics3DDebugGeometry();
|
|
143
|
+
|
|
144
|
+
writePhysics3DDebugGeometry(world, out, { drawCentersOfMass: false });
|
|
145
|
+
|
|
146
|
+
expect(liveSpheres(out).filter((sphere) => sphere.feature === 'collider')).toHaveLength(2);
|
|
147
|
+
const axis = liveLines(out).filter((line) => line.feature === 'collider');
|
|
148
|
+
expect(axis).toHaveLength(1);
|
|
149
|
+
expect(Math.hypot(axis[0].x1 - axis[0].x0, axis[0].y1 - axis[0].y0, axis[0].z1 - axis[0].z0)).toBeCloseTo(4, 9);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('draws a hull as the wireframe of its own triangulation', () => {
|
|
153
|
+
const world = createPhysics3DWorld();
|
|
154
|
+
addBody(world, { kind: 'convex', points: [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] });
|
|
155
|
+
const out = createPhysics3DDebugGeometry();
|
|
156
|
+
|
|
157
|
+
writePhysics3DDebugGeometry(world, out, { drawCentersOfMass: false });
|
|
158
|
+
|
|
159
|
+
// A tetrahedron triangulates to four faces, three edges each.
|
|
160
|
+
expect(liveLines(out).filter((line) => line.feature === 'collider')).toHaveLength(12);
|
|
161
|
+
expect(liveSpheres(out).filter((sphere) => sphere.feature === 'collider')).toHaveLength(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('falls back to vertices for a DEGENERATE hull, which has no surface to wire', () => {
|
|
165
|
+
// Coplanar: there is no solid, so any edge drawn would bound a face the simulation does not have.
|
|
166
|
+
const world = createPhysics3DWorld();
|
|
167
|
+
addBody(world, { kind: 'convex', points: [0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0] });
|
|
168
|
+
const out = createPhysics3DDebugGeometry();
|
|
169
|
+
|
|
170
|
+
writePhysics3DDebugGeometry(world, out, { drawCentersOfMass: false });
|
|
171
|
+
|
|
172
|
+
expect(liveLines(out).filter((line) => line.feature === 'collider')).toHaveLength(0);
|
|
173
|
+
expect(liveSpheres(out).filter((sphere) => sphere.feature === 'collider')).toHaveLength(4);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('draws a contact normal from each contact point', () => {
|
|
177
|
+
const world = createPhysics3DWorld();
|
|
178
|
+
const floor = createRigidBody3D('static');
|
|
179
|
+
floor.y = -1;
|
|
180
|
+
addPhysics3DBody(world, floor);
|
|
181
|
+
addPhysics3DCollider(
|
|
182
|
+
world,
|
|
183
|
+
floor,
|
|
184
|
+
createPhysics3DCollider({ kind: 'aabb', minX: -10, minY: -1, minZ: -10, maxX: 10, maxY: 1, maxZ: 10 }),
|
|
185
|
+
);
|
|
186
|
+
addBody(world, unitBox, -0.05);
|
|
187
|
+
buildPhysics3DContacts(world);
|
|
188
|
+
const out = createPhysics3DDebugGeometry();
|
|
189
|
+
|
|
190
|
+
writePhysics3DDebugGeometry(world, out, {
|
|
191
|
+
drawColliders: false,
|
|
192
|
+
drawCentersOfMass: false,
|
|
193
|
+
contactNormalLength: 2,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const normals = liveLines(out).filter((line) => line.feature === 'contact-normal');
|
|
197
|
+
expect(normals).toHaveLength(world.contacts[0].pointCount);
|
|
198
|
+
expect(
|
|
199
|
+
Math.hypot(normals[0].x1 - normals[0].x0, normals[0].y1 - normals[0].y0, normals[0].z1 - normals[0].z0),
|
|
200
|
+
).toBeCloseTo(2, 6);
|
|
201
|
+
expect(normals[0].bodyB).not.toBe(-1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('draws a joint as the gap between its two anchors', () => {
|
|
205
|
+
const world = createPhysics3DWorld();
|
|
206
|
+
registerBuiltInPhysics3DJointSolvers(world);
|
|
207
|
+
const first = addBody(world, unitBox);
|
|
208
|
+
const second = addBody(world, unitBox);
|
|
209
|
+
second.x = 5;
|
|
210
|
+
addPhysics3DJoint(world, createPhysics3DBallAndSocketJoint({ bodyA: first.index, bodyB: second.index }));
|
|
211
|
+
const out = createPhysics3DDebugGeometry();
|
|
212
|
+
|
|
213
|
+
writePhysics3DDebugGeometry(world, out, { drawColliders: false, drawCentersOfMass: false });
|
|
214
|
+
|
|
215
|
+
const joints = liveLines(out).filter((line) => line.feature === 'joint');
|
|
216
|
+
expect(joints).toHaveLength(1);
|
|
217
|
+
// The two anchors have drifted 5 apart, and the line shows that rather than collapsing to a point.
|
|
218
|
+
expect(
|
|
219
|
+
Math.hypot(joints[0].x1 - joints[0].x0, joints[0].y1 - joints[0].y0, joints[0].z1 - joints[0].z0),
|
|
220
|
+
).toBeCloseTo(5, 9);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('draws the centre of mass where the body actually balances', () => {
|
|
224
|
+
const world = createPhysics3DWorld();
|
|
225
|
+
const body = createRigidBody3D('dynamic');
|
|
226
|
+
addPhysics3DBody(world, body);
|
|
227
|
+
addPhysics3DCollider(
|
|
228
|
+
world,
|
|
229
|
+
body,
|
|
230
|
+
createPhysics3DCollider({ kind: 'aabb', minX: 1.5, minY: -0.5, minZ: -0.5, maxX: 2.5, maxY: 0.5, maxZ: 0.5 }),
|
|
231
|
+
);
|
|
232
|
+
const out = createPhysics3DDebugGeometry();
|
|
233
|
+
|
|
234
|
+
writePhysics3DDebugGeometry(world, out, { drawColliders: false });
|
|
235
|
+
|
|
236
|
+
const centers = liveSpheres(out).filter((sphere) => sphere.feature === 'center-of-mass');
|
|
237
|
+
expect(centers).toHaveLength(1);
|
|
238
|
+
// The collider sits a full 2 units along +x, so the centre of mass is there and not at the origin.
|
|
239
|
+
expect(centers[0].x).toBeCloseTo(2, 9);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('honours each feature toggle independently', () => {
|
|
243
|
+
const world = createPhysics3DWorld();
|
|
244
|
+
addBody(world, unitBox);
|
|
245
|
+
const out = createPhysics3DDebugGeometry();
|
|
246
|
+
|
|
247
|
+
writePhysics3DDebugGeometry(world, out, {
|
|
248
|
+
drawColliders: false,
|
|
249
|
+
drawCentersOfMass: false,
|
|
250
|
+
drawContacts: false,
|
|
251
|
+
drawJoints: false,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(out.lineCount).toBe(0);
|
|
255
|
+
expect(out.sphereCount).toBe(0);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('reuses its records, so a per-frame debug draw allocates nothing after the first', () => {
|
|
259
|
+
const world = createPhysics3DWorld();
|
|
260
|
+
addBody(world, unitBox);
|
|
261
|
+
const out = createPhysics3DDebugGeometry();
|
|
262
|
+
|
|
263
|
+
writePhysics3DDebugGeometry(world, out);
|
|
264
|
+
const line = out.lines[0];
|
|
265
|
+
const sphere = out.spheres[0];
|
|
266
|
+
const lineCapacity = out.lines.length;
|
|
267
|
+
writePhysics3DDebugGeometry(world, out);
|
|
268
|
+
|
|
269
|
+
expect(out.lines[0]).toBe(line);
|
|
270
|
+
expect(out.spheres[0]).toBe(sphere);
|
|
271
|
+
expect(out.lines.length).toBe(lineCapacity);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('retains capacity above the live prefix when the scene shrinks', () => {
|
|
275
|
+
const world = createPhysics3DWorld();
|
|
276
|
+
const body = addBody(world, unitBox);
|
|
277
|
+
const out = createPhysics3DDebugGeometry();
|
|
278
|
+
writePhysics3DDebugGeometry(world, out, { drawCentersOfMass: false });
|
|
279
|
+
const capacity = out.lines.length;
|
|
280
|
+
expect(capacity).toBe(12);
|
|
281
|
+
|
|
282
|
+
body.colliders.length = 0;
|
|
283
|
+
writePhysics3DDebugGeometry(world, out, { drawCentersOfMass: false });
|
|
284
|
+
|
|
285
|
+
expect(out.lineCount).toBe(0);
|
|
286
|
+
expect(out.lines.length).toBe(capacity);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import type { LogEntry, Physics3DWorld } from '@flighthq/types/contract';
|
|
3
|
+
|
|
4
|
+
import { synchronizePhysics3DBroadphase } from './broadphase';
|
|
5
|
+
import { arePhysics3DGuardsEnabled, disablePhysics3DGuards, enablePhysics3DGuards } from './enablePhysics3DGuards';
|
|
6
|
+
import { createPhysics3DHingeJoint } from './jointFactories';
|
|
7
|
+
import { addPhysics3DJoint } from './jointRegistry';
|
|
8
|
+
import { registerBuiltInPhysics3DJointSolvers } from './registerBuiltInPhysics3DJointSolvers';
|
|
9
|
+
import { stepPhysics3D } from './step';
|
|
10
|
+
import {
|
|
11
|
+
addPhysics3DBody,
|
|
12
|
+
addPhysics3DCollider,
|
|
13
|
+
createPhysics3DCollider,
|
|
14
|
+
createPhysics3DWorld,
|
|
15
|
+
createRigidBody3D,
|
|
16
|
+
} from './world';
|
|
17
|
+
|
|
18
|
+
function captureLog(run: () => void): readonly LogEntry[] {
|
|
19
|
+
const sink = createMemoryLogSink(8);
|
|
20
|
+
addLogSink(sink.sink);
|
|
21
|
+
try {
|
|
22
|
+
run();
|
|
23
|
+
return getMemoryLogSinkEntries(sink);
|
|
24
|
+
} finally {
|
|
25
|
+
removeLogSink(sink.sink);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
disablePhysics3DGuards();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('arePhysics3DGuardsEnabled', () => {
|
|
34
|
+
it('reports the current module guard state', () => {
|
|
35
|
+
expect(arePhysics3DGuardsEnabled()).toBe(false);
|
|
36
|
+
enablePhysics3DGuards();
|
|
37
|
+
expect(arePhysics3DGuardsEnabled()).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('disablePhysics3DGuards', () => {
|
|
42
|
+
it('removes the seam so a declined step is silent again', () => {
|
|
43
|
+
enablePhysics3DGuards();
|
|
44
|
+
disablePhysics3DGuards();
|
|
45
|
+
const world = createTestWorld();
|
|
46
|
+
world.config.sequentialImpulse.positionIterations = -3;
|
|
47
|
+
|
|
48
|
+
const entries = captureLog(() => stepPhysics3D(world, 1 / 60));
|
|
49
|
+
|
|
50
|
+
expect(entries).toHaveLength(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('removes the unresolved-joint seam', () => {
|
|
54
|
+
enablePhysics3DGuards();
|
|
55
|
+
disablePhysics3DGuards();
|
|
56
|
+
const world = createTestWorld();
|
|
57
|
+
addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
58
|
+
|
|
59
|
+
const entries = captureLog(() => stepPhysics3D(world, 1 / 60));
|
|
60
|
+
|
|
61
|
+
expect(entries).toHaveLength(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('removes the spatial-indexing seam', () => {
|
|
65
|
+
enablePhysics3DGuards();
|
|
66
|
+
disablePhysics3DGuards();
|
|
67
|
+
|
|
68
|
+
const entries = captureLog(() => addPhysics3DBody(createPhysics3DWorld(), createOverflowBody()));
|
|
69
|
+
|
|
70
|
+
expect(entries).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('enablePhysics3DGuards', () => {
|
|
75
|
+
it('says nothing while the world steps normally', () => {
|
|
76
|
+
enablePhysics3DGuards();
|
|
77
|
+
|
|
78
|
+
const entries = captureLog(() => stepPhysics3D(createTestWorld(), 1 / 60));
|
|
79
|
+
|
|
80
|
+
// A guard that fired on a healthy step would be worse than none: the message is supposed to mean the
|
|
81
|
+
// simulation has stopped advancing.
|
|
82
|
+
expect(entries).toHaveLength(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('names every failing precondition at once, not only the first', () => {
|
|
86
|
+
enablePhysics3DGuards();
|
|
87
|
+
const world = createTestWorld();
|
|
88
|
+
world.gravityY = Number.NaN;
|
|
89
|
+
world.config.substeps = 0;
|
|
90
|
+
|
|
91
|
+
const entries = captureLog(() => stepPhysics3D(world, 0));
|
|
92
|
+
|
|
93
|
+
expect(entries).toHaveLength(1);
|
|
94
|
+
const failing = readFailing(entries[0]);
|
|
95
|
+
// The step's own condition short-circuits, so one fault per frame would make a caller repair a
|
|
96
|
+
// three-fault world across three frames, each fix revealing the next.
|
|
97
|
+
expect([...failing].sort()).toEqual(['gravityValid', 'substepsValid', 'timestepValid']);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('logs once for a repeating fault and again when a different one joins it', () => {
|
|
101
|
+
enablePhysics3DGuards();
|
|
102
|
+
const world = createTestWorld();
|
|
103
|
+
world.config.sequentialImpulse.velocityIterations = -1;
|
|
104
|
+
|
|
105
|
+
const repeated = captureLog(() => {
|
|
106
|
+
for (let frame = 0; frame < 5; frame += 1) stepPhysics3D(world, 1 / 60);
|
|
107
|
+
});
|
|
108
|
+
expect(repeated).toHaveLength(1);
|
|
109
|
+
|
|
110
|
+
// Keyed on the failing SET rather than on the world, so a frame loop costs one message while a world
|
|
111
|
+
// that develops a SECOND fault still reports it instead of being swallowed by the first key.
|
|
112
|
+
const widened = captureLog(() => {
|
|
113
|
+
world.gravityZ = Number.NaN;
|
|
114
|
+
stepPhysics3D(world, 1 / 60);
|
|
115
|
+
});
|
|
116
|
+
expect(widened).toHaveLength(1);
|
|
117
|
+
expect(readFailing(widened[0])).toContain('gravityValid');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('names invalid collider state before contact intake can consume it', () => {
|
|
121
|
+
enablePhysics3DGuards();
|
|
122
|
+
const world = createTestWorld();
|
|
123
|
+
const collider = createPhysics3DCollider({ kind: 'sphere', x: 0, y: 0, z: 0, radius: 1 });
|
|
124
|
+
addPhysics3DCollider(world, world.bodies[0], collider);
|
|
125
|
+
collider.material.restitution = Number.NaN;
|
|
126
|
+
|
|
127
|
+
const entries = captureLog(() => stepPhysics3D(world, 1 / 60));
|
|
128
|
+
|
|
129
|
+
expect(entries).toHaveLength(1);
|
|
130
|
+
expect(readFailing(entries[0])).toEqual(['colliderStateValid']);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('names every joint the solve skips and distinguishes why', () => {
|
|
134
|
+
enablePhysics3DGuards();
|
|
135
|
+
const world = createTestWorld();
|
|
136
|
+
const unregistered = createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 });
|
|
137
|
+
unregistered.kind = 'guard.unregistered';
|
|
138
|
+
addPhysics3DJoint(world, unregistered);
|
|
139
|
+
registerBuiltInPhysics3DJointSolvers(world);
|
|
140
|
+
const invalidBodies = addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
141
|
+
invalidBodies.bodyB = 99;
|
|
142
|
+
|
|
143
|
+
const entries = captureLog(() => {
|
|
144
|
+
stepPhysics3D(world, 1 / 60);
|
|
145
|
+
stepPhysics3D(world, 1 / 60);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(entries).toHaveLength(1);
|
|
149
|
+
expect(readJointIssues(entries[0])).toEqual([
|
|
150
|
+
{ index: 0, kind: 'guard.unregistered', status: 'unregistered-kind' },
|
|
151
|
+
{ index: 1, kind: 'Hinge', status: 'invalid-bodies' },
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('reports a body routed through spatial overflow once and stays silent for ordinary indexing', () => {
|
|
156
|
+
enablePhysics3DGuards();
|
|
157
|
+
const world = createPhysics3DWorld();
|
|
158
|
+
|
|
159
|
+
const entries = captureLog(() => {
|
|
160
|
+
const ordinary = createRigidBody3D('dynamic');
|
|
161
|
+
ordinary.colliders.push(createPhysics3DCollider({ kind: 'sphere', x: 0, y: 0, z: 0, radius: 0.5 }));
|
|
162
|
+
addPhysics3DBody(world, ordinary);
|
|
163
|
+
addPhysics3DBody(world, createOverflowBody());
|
|
164
|
+
addPhysics3DBody(world, createOverflowBody());
|
|
165
|
+
synchronizePhysics3DBroadphase(world);
|
|
166
|
+
synchronizePhysics3DBroadphase(world);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(entries).toHaveLength(1);
|
|
170
|
+
expect(entries[0]).toMatchObject({
|
|
171
|
+
channel: 'physics3d',
|
|
172
|
+
data: { bodyIndices: [1], status: 'spatial-overflow' },
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// `LogData` is deliberately `string | Record<string, unknown>`, so a structured entry's fields are only
|
|
178
|
+
// reachable through a narrowing read rather than a property access.
|
|
179
|
+
function readFailing(entry: Readonly<LogEntry>): readonly string[] {
|
|
180
|
+
const data = entry.data;
|
|
181
|
+
if (typeof data === 'string' || data === undefined) return [];
|
|
182
|
+
return (data.failing as readonly string[] | undefined) ?? [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function readJointIssues(entry: Readonly<LogEntry>): readonly Record<string, unknown>[] {
|
|
186
|
+
const data = entry.data;
|
|
187
|
+
if (typeof data === 'string' || data === undefined) return [];
|
|
188
|
+
return (data.joints as readonly Record<string, unknown>[] | undefined) ?? [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Steppable as built: every fault in these tests is one the test introduces, so a case that expects
|
|
192
|
+
// silence is asserting the guard's judgement rather than a world that happens to be fine.
|
|
193
|
+
function createTestWorld(): Physics3DWorld {
|
|
194
|
+
const world = createPhysics3DWorld();
|
|
195
|
+
addPhysics3DBody(world, createRigidBody3D('dynamic'));
|
|
196
|
+
addPhysics3DBody(world, createRigidBody3D('dynamic'));
|
|
197
|
+
return world;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function createOverflowBody() {
|
|
201
|
+
const body = createRigidBody3D('static');
|
|
202
|
+
body.colliders.push(
|
|
203
|
+
createPhysics3DCollider({ kind: 'aabb', minX: -16, minY: -16, minZ: -16, maxX: 16, maxY: 16, maxZ: 16 }),
|
|
204
|
+
);
|
|
205
|
+
return body;
|
|
206
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { registerBuiltInCollisionSupports3D } from '@flighthq/collision/contract';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { explainPhysics3DCollision } from './explainPhysics3DCollision';
|
|
5
|
+
import {
|
|
6
|
+
addPhysics3DBody,
|
|
7
|
+
addPhysics3DCollider,
|
|
8
|
+
createPhysics3DCollider,
|
|
9
|
+
createPhysics3DWorld,
|
|
10
|
+
createRigidBody3D,
|
|
11
|
+
} from './world';
|
|
12
|
+
|
|
13
|
+
// This file asserts the state of the collision support registry BEFORE anything registers, which is the
|
|
14
|
+
// exact state a caller who forgot to register is in — and the state that presents as bodies falling
|
|
15
|
+
// through floors with no error anywhere. That claim only holds in a process nobody else has touched, so
|
|
16
|
+
// the file runs isolated; see `scripts/registryIsolatedTests.ts`. The registering tests come last on
|
|
17
|
+
// purpose: registration is process-global and cannot be undone.
|
|
18
|
+
|
|
19
|
+
function addSphereBody(world: ReturnType<typeof createPhysics3DWorld>, radius = 1): void {
|
|
20
|
+
const body = createRigidBody3D('dynamic');
|
|
21
|
+
addPhysics3DBody(world, body);
|
|
22
|
+
addPhysics3DCollider(world, body, createPhysics3DCollider({ kind: 'sphere', x: 0, y: 0, z: 0, radius }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('explainPhysics3DCollision', () => {
|
|
26
|
+
it('names the kinds with no registered support', () => {
|
|
27
|
+
const world = createPhysics3DWorld();
|
|
28
|
+
addSphereBody(world);
|
|
29
|
+
|
|
30
|
+
const explanation = explainPhysics3DCollision(world);
|
|
31
|
+
expect(explanation.status).toBe('missing-support');
|
|
32
|
+
expect(explanation.unsupportedKinds).toEqual(['sphere']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('reports ready for an empty world, which cannot fail to detect anything', () => {
|
|
36
|
+
expect(explainPhysics3DCollision(createPhysics3DWorld())).toEqual({ unsupportedKinds: [], status: 'ready' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('deduplicates and sorts, so the value is stable however the bodies were inserted', () => {
|
|
40
|
+
const world = createPhysics3DWorld();
|
|
41
|
+
addSphereBody(world, 1);
|
|
42
|
+
const capsule = createRigidBody3D('dynamic');
|
|
43
|
+
addPhysics3DBody(world, capsule);
|
|
44
|
+
addPhysics3DCollider(
|
|
45
|
+
world,
|
|
46
|
+
capsule,
|
|
47
|
+
createPhysics3DCollider({ kind: 'capsule', x0: 0, y0: 0, z0: 0, x1: 0, y1: 1, z1: 0, radius: 1 }),
|
|
48
|
+
);
|
|
49
|
+
addSphereBody(world, 2);
|
|
50
|
+
|
|
51
|
+
expect(explainPhysics3DCollision(world).unsupportedKinds).toEqual(['capsule', 'sphere']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('reads the WORLD kind, which is what the narrow phase is actually handed', () => {
|
|
55
|
+
// An `aabb` local shape carries a `box` world shape, so a world of aabb colliders needs the BOX
|
|
56
|
+
// support registered. Asking about the local kind would report a world ready that is not.
|
|
57
|
+
const world = createPhysics3DWorld();
|
|
58
|
+
const body = createRigidBody3D('dynamic');
|
|
59
|
+
addPhysics3DBody(world, body);
|
|
60
|
+
addPhysics3DCollider(
|
|
61
|
+
world,
|
|
62
|
+
body,
|
|
63
|
+
createPhysics3DCollider({ kind: 'aabb', minX: -1, minY: -1, minZ: -1, maxX: 1, maxY: 1, maxZ: 1 }),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(explainPhysics3DCollision(world).unsupportedKinds).toEqual(['box']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('reports ready once the built-in supports are registered', () => {
|
|
70
|
+
const world = createPhysics3DWorld();
|
|
71
|
+
addSphereBody(world);
|
|
72
|
+
expect(explainPhysics3DCollision(world).status).toBe('missing-support');
|
|
73
|
+
|
|
74
|
+
registerBuiltInCollisionSupports3D();
|
|
75
|
+
|
|
76
|
+
expect(explainPhysics3DCollision(world)).toEqual({ unsupportedKinds: [], status: 'ready' });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Physics3DJointSolver, Physics3DWorld } from '@flighthq/types/contract';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { explainPhysics3DJoints } from './explainPhysics3DJoints';
|
|
5
|
+
import { createPhysics3DBallAndSocketJoint, createPhysics3DHingeJoint } from './jointFactories';
|
|
6
|
+
import { addPhysics3DJoint, registerPhysics3DJointSolver } from './jointRegistry';
|
|
7
|
+
import { Physics3DBallAndSocketJointKind } from './joints';
|
|
8
|
+
import { registerBuiltInPhysics3DJointSolvers } from './registerBuiltInPhysics3DJointSolvers';
|
|
9
|
+
import { addPhysics3DBody, createPhysics3DWorld, createRigidBody3D } from './world';
|
|
10
|
+
|
|
11
|
+
describe('explainPhysics3DJoints', () => {
|
|
12
|
+
it('reports nothing for a world with no joints', () => {
|
|
13
|
+
expect(explainPhysics3DJoints(createTestWorld())).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('calls a fully wired joint solvable', () => {
|
|
17
|
+
const world = createTestWorld();
|
|
18
|
+
registerBuiltInPhysics3DJointSolvers(world);
|
|
19
|
+
addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
20
|
+
|
|
21
|
+
const [explanation] = explainPhysics3DJoints(world);
|
|
22
|
+
|
|
23
|
+
expect(explanation.status).toBe('solvable');
|
|
24
|
+
expect(explanation.hasSolver).toBe(true);
|
|
25
|
+
expect(explanation.bodiesResolvable).toBe(true);
|
|
26
|
+
expect(explanation.index).toBe(0);
|
|
27
|
+
expect(explanation.kind).toBe('Hinge');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('separates an unregistered kind from a broken one', () => {
|
|
31
|
+
const world = createTestWorld();
|
|
32
|
+
addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
33
|
+
|
|
34
|
+
const [explanation] = explainPhysics3DJoints(world);
|
|
35
|
+
|
|
36
|
+
// Not a fault: a scene deserialized ahead of the code that solves it is explicitly supported, and the
|
|
37
|
+
// joint is meant to sit inert until someone registers its solver.
|
|
38
|
+
expect(explanation.status).toBe('unregistered-kind');
|
|
39
|
+
expect(explanation.hasSolver).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reports a joint outliving one of its bodies', () => {
|
|
43
|
+
const world = createTestWorld();
|
|
44
|
+
registerBuiltInPhysics3DJointSolvers(world);
|
|
45
|
+
const joint = addPhysics3DJoint(world, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 }));
|
|
46
|
+
// Removing a body drops the joints that named it, so this is the state a world reconstructed by a
|
|
47
|
+
// format layer reaches rather than one the lifecycle produces: an endpoint that resolves to nothing.
|
|
48
|
+
joint.bodyB = 99;
|
|
49
|
+
|
|
50
|
+
const [explanation] = explainPhysics3DJoints(world);
|
|
51
|
+
|
|
52
|
+
expect(explanation.status).toBe('invalid-bodies');
|
|
53
|
+
expect(explanation.hasSolver).toBe(true);
|
|
54
|
+
expect(explanation.bodiesResolvable).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does not fault a one-body kind for its unused first end', () => {
|
|
58
|
+
const world = createTestWorld();
|
|
59
|
+
const oneBody: Physics3DJointSolver = { prepare(): void {}, solve(): void {}, usesBodyA: false };
|
|
60
|
+
registerPhysics3DJointSolver(world, Physics3DBallAndSocketJointKind, oneBody);
|
|
61
|
+
const joint = addPhysics3DJoint(world, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 }));
|
|
62
|
+
joint.bodyA = 404;
|
|
63
|
+
|
|
64
|
+
const [explanation] = explainPhysics3DJoints(world);
|
|
65
|
+
|
|
66
|
+
// The solver never reads bodyA, so a placeholder index there is not a fault. Asking the kind rather
|
|
67
|
+
// than assuming two endpoints is what keeps this honest for a kind the package does not own.
|
|
68
|
+
expect(explanation.status).toBe('solvable');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('reports one entry per joint, indexed back into the world list', () => {
|
|
72
|
+
const world = createTestWorld();
|
|
73
|
+
registerBuiltInPhysics3DJointSolvers(world);
|
|
74
|
+
addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
75
|
+
addPhysics3DJoint(world, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 }));
|
|
76
|
+
|
|
77
|
+
const explanations = explainPhysics3DJoints(world);
|
|
78
|
+
|
|
79
|
+
expect(explanations.map((entry) => entry.index)).toEqual([0, 1]);
|
|
80
|
+
expect(world.joints[explanations[1].index].kind).toBe(explanations[1].kind);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('changes nothing it looks at', () => {
|
|
84
|
+
const world = createTestWorld();
|
|
85
|
+
registerBuiltInPhysics3DJointSolvers(world);
|
|
86
|
+
const joint = addPhysics3DJoint(world, createPhysics3DHingeJoint({ bodyA: 0, bodyB: 1 }));
|
|
87
|
+
joint.impulse0 = 7;
|
|
88
|
+
|
|
89
|
+
explainPhysics3DJoints(world);
|
|
90
|
+
|
|
91
|
+
expect(joint.impulse0).toBe(7);
|
|
92
|
+
expect(world.joints.length).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
function createTestWorld(): Physics3DWorld {
|
|
97
|
+
const world = createPhysics3DWorld();
|
|
98
|
+
addPhysics3DBody(world, createRigidBody3D('dynamic'));
|
|
99
|
+
addPhysics3DBody(world, createRigidBody3D('dynamic'));
|
|
100
|
+
return world;
|
|
101
|
+
}
|