@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,611 @@
|
|
|
1
|
+
import type { CollisionBuiltInShape3D, Physics3DCollider, RigidBody3D } from '@flighthq/types/contract';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
combinePhysics3DMassData,
|
|
6
|
+
computePhysics3DBoxMassData,
|
|
7
|
+
computePhysics3DColliderMassData,
|
|
8
|
+
computePhysics3DConeMassData,
|
|
9
|
+
computePhysics3DCylinderMassData,
|
|
10
|
+
computePhysics3DConvexHullMassData,
|
|
11
|
+
computePhysics3DCapsuleMassData,
|
|
12
|
+
computePhysics3DSphereMassData,
|
|
13
|
+
createPhysics3DMassData,
|
|
14
|
+
updateRigidBody3DMassData,
|
|
15
|
+
setRigidBody3DMassData,
|
|
16
|
+
} from './massProperties';
|
|
17
|
+
import { addPhysics3DBody, createPhysics3DCollider, createPhysics3DWorld, createRigidBody3D } from './world';
|
|
18
|
+
|
|
19
|
+
describe('combinePhysics3DMassData', () => {
|
|
20
|
+
it('starts from the zero identity, adopting the addend outright', () => {
|
|
21
|
+
const target = createPhysics3DMassData();
|
|
22
|
+
const sphere = createPhysics3DMassData();
|
|
23
|
+
computePhysics3DSphereMassData(1, 1, sphere);
|
|
24
|
+
|
|
25
|
+
combinePhysics3DMassData(target, sphere);
|
|
26
|
+
|
|
27
|
+
expect(target.mass).toBeCloseTo(sphere.mass, 12);
|
|
28
|
+
expect(target.inertiaXX).toBeCloseTo(sphere.inertiaXX, 12);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('puts the combined centre between two equal masses', () => {
|
|
32
|
+
const target = createPhysics3DMassData();
|
|
33
|
+
const left = createPhysics3DMassData();
|
|
34
|
+
const right = createPhysics3DMassData();
|
|
35
|
+
computePhysics3DSphereMassData(1, 1, left);
|
|
36
|
+
computePhysics3DSphereMassData(1, 1, right);
|
|
37
|
+
left.centerX = -2;
|
|
38
|
+
right.centerX = 2;
|
|
39
|
+
|
|
40
|
+
combinePhysics3DMassData(target, left);
|
|
41
|
+
combinePhysics3DMassData(target, right);
|
|
42
|
+
|
|
43
|
+
expect(target.centerX).toBeCloseTo(0, 12);
|
|
44
|
+
expect(target.mass).toBeCloseTo(left.mass + right.mass, 12);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('shifts the ALREADY-ACCUMULATED tensor too, not only the addend', () => {
|
|
48
|
+
// Two unit spheres offset along x. About the combined centre, each contributes its own moment plus
|
|
49
|
+
// mass * d^2 for the y and z axes. A version that shifted only the addend would report half of it.
|
|
50
|
+
const target = createPhysics3DMassData();
|
|
51
|
+
const left = createPhysics3DMassData();
|
|
52
|
+
const right = createPhysics3DMassData();
|
|
53
|
+
computePhysics3DSphereMassData(1, 1, left);
|
|
54
|
+
computePhysics3DSphereMassData(1, 1, right);
|
|
55
|
+
left.centerX = -2;
|
|
56
|
+
right.centerX = 2;
|
|
57
|
+
|
|
58
|
+
combinePhysics3DMassData(target, left);
|
|
59
|
+
combinePhysics3DMassData(target, right);
|
|
60
|
+
|
|
61
|
+
const each = left.mass;
|
|
62
|
+
const expectedYY = 2 * (0.4 * each * 1 + each * 4);
|
|
63
|
+
expect(target.inertiaYY).toBeCloseTo(expectedYY, 8);
|
|
64
|
+
// The x axis runs through both centres, so it gains no parallel-axis term.
|
|
65
|
+
expect(target.inertiaXX).toBeCloseTo(2 * 0.4 * each, 8);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('is order-independent for the combined mass and centre', () => {
|
|
69
|
+
const forward = createPhysics3DMassData();
|
|
70
|
+
const backward = createPhysics3DMassData();
|
|
71
|
+
const a = createPhysics3DMassData();
|
|
72
|
+
const b = createPhysics3DMassData();
|
|
73
|
+
computePhysics3DSphereMassData(1, 1, a);
|
|
74
|
+
computePhysics3DBoxMassData(1, 2, 3, 2, b);
|
|
75
|
+
a.centerY = 1;
|
|
76
|
+
b.centerY = -3;
|
|
77
|
+
|
|
78
|
+
combinePhysics3DMassData(forward, a);
|
|
79
|
+
combinePhysics3DMassData(forward, b);
|
|
80
|
+
combinePhysics3DMassData(backward, b);
|
|
81
|
+
combinePhysics3DMassData(backward, a);
|
|
82
|
+
|
|
83
|
+
expect(backward.mass).toBeCloseTo(forward.mass, 10);
|
|
84
|
+
expect(backward.centerY).toBeCloseTo(forward.centerY, 10);
|
|
85
|
+
expect(backward.inertiaXX).toBeCloseTo(forward.inertiaXX, 8);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('zeroes the result when both sides are massless rather than dividing by zero', () => {
|
|
89
|
+
const target = createPhysics3DMassData();
|
|
90
|
+
combinePhysics3DMassData(target, createPhysics3DMassData());
|
|
91
|
+
expect(target.mass).toBe(0);
|
|
92
|
+
expect(Number.isNaN(target.centerX)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('computePhysics3DBoxMassData', () => {
|
|
97
|
+
it('derives mass from volume and density', () => {
|
|
98
|
+
const out = createPhysics3DMassData();
|
|
99
|
+
computePhysics3DBoxMassData(1, 2, 3, 5, out);
|
|
100
|
+
expect(out.mass).toBeCloseTo(8 * 1 * 2 * 3 * 5, 10);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('gives a cube one moment on every axis', () => {
|
|
104
|
+
const out = createPhysics3DMassData();
|
|
105
|
+
computePhysics3DBoxMassData(2, 2, 2, 1, out);
|
|
106
|
+
expect(out.inertiaYY).toBeCloseTo(out.inertiaXX, 10);
|
|
107
|
+
expect(out.inertiaZZ).toBeCloseTo(out.inertiaXX, 10);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('resists rotation least about its longest axis', () => {
|
|
111
|
+
const out = createPhysics3DMassData();
|
|
112
|
+
computePhysics3DBoxMassData(4, 1, 1, 1, out);
|
|
113
|
+
expect(out.inertiaXX).toBeLessThan(out.inertiaYY);
|
|
114
|
+
expect(out.inertiaYY).toBeCloseTo(out.inertiaZZ, 10);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('leaves the off-diagonal terms zero for an axis-aligned box', () => {
|
|
118
|
+
const out = createPhysics3DMassData();
|
|
119
|
+
computePhysics3DBoxMassData(1, 2, 3, 1, out);
|
|
120
|
+
expect(out.inertiaXY).toBe(0);
|
|
121
|
+
expect(out.inertiaXZ).toBe(0);
|
|
122
|
+
expect(out.inertiaYZ).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('computePhysics3DCapsuleMassData', () => {
|
|
127
|
+
it('degenerates exactly to a sphere at zero half-height', () => {
|
|
128
|
+
const capsule = createPhysics3DMassData();
|
|
129
|
+
const sphere = createPhysics3DMassData();
|
|
130
|
+
computePhysics3DCapsuleMassData(2, 0, 3, capsule);
|
|
131
|
+
computePhysics3DSphereMassData(2, 3, sphere);
|
|
132
|
+
|
|
133
|
+
expect(capsule.mass).toBeCloseTo(sphere.mass, 10);
|
|
134
|
+
expect(capsule.inertiaXX).toBeCloseTo(sphere.inertiaXX, 8);
|
|
135
|
+
expect(capsule.inertiaYY).toBeCloseTo(sphere.inertiaYY, 8);
|
|
136
|
+
expect(capsule.inertiaZZ).toBeCloseTo(sphere.inertiaZZ, 8);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('derives mass as cylinder plus two hemispheres', () => {
|
|
140
|
+
const out = createPhysics3DMassData();
|
|
141
|
+
computePhysics3DCapsuleMassData(1, 2, 1, out);
|
|
142
|
+
const expected = Math.PI * 1 * 4 + (4 / 3) * Math.PI * 1;
|
|
143
|
+
expect(out.mass).toBeCloseTo(expected, 10);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('resists rotation least about its own axis', () => {
|
|
147
|
+
const out = createPhysics3DMassData();
|
|
148
|
+
computePhysics3DCapsuleMassData(1, 4, 1, out);
|
|
149
|
+
expect(out.inertiaYY).toBeLessThan(out.inertiaXX);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('is symmetric across the two axes orthogonal to its own', () => {
|
|
153
|
+
const out = createPhysics3DMassData();
|
|
154
|
+
computePhysics3DCapsuleMassData(1.5, 3, 2, out);
|
|
155
|
+
expect(out.inertiaZZ).toBeCloseTo(out.inertiaXX, 10);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('computePhysics3DColliderMassData', () => {
|
|
160
|
+
function colliderOf(local: CollisionBuiltInShape3D, density = 1): Physics3DCollider {
|
|
161
|
+
return createPhysics3DCollider(local, { density, friction: 0.2, restitution: 0 });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
it('places the centroid at an offset collider rather than at the body origin', () => {
|
|
165
|
+
const out = createPhysics3DMassData();
|
|
166
|
+
computePhysics3DColliderMassData(colliderOf({ kind: 'sphere', x: 3, y: -2, z: 1, radius: 1 }), out);
|
|
167
|
+
expect(out.centerX).toBe(3);
|
|
168
|
+
expect(out.centerY).toBe(-2);
|
|
169
|
+
expect(out.centerZ).toBe(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('gives an aabb the same tensor as the equivalent box', () => {
|
|
173
|
+
const collider = createPhysics3DMassData();
|
|
174
|
+
const box = createPhysics3DMassData();
|
|
175
|
+
computePhysics3DColliderMassData(
|
|
176
|
+
colliderOf({ kind: 'aabb', minX: -1, minY: -2, minZ: -3, maxX: 1, maxY: 2, maxZ: 3 }),
|
|
177
|
+
collider,
|
|
178
|
+
);
|
|
179
|
+
computePhysics3DBoxMassData(1, 2, 3, 1, box);
|
|
180
|
+
expect(collider.mass).toBeCloseTo(box.mass, 9);
|
|
181
|
+
expect(collider.inertiaXX).toBeCloseTo(box.inertiaXX, 9);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('makes a LOCALLY ROTATED box non-diagonal in the body frame', () => {
|
|
185
|
+
// The silent-failure case the tensor representation exists for: skipping this rotation leaves the
|
|
186
|
+
// mass and the diagonal both plausible and only the off-axis swing wrong.
|
|
187
|
+
const out = createPhysics3DMassData();
|
|
188
|
+
computePhysics3DColliderMassData(
|
|
189
|
+
colliderOf({
|
|
190
|
+
kind: 'box',
|
|
191
|
+
x: 0,
|
|
192
|
+
y: 0,
|
|
193
|
+
z: 0,
|
|
194
|
+
halfX: 3,
|
|
195
|
+
halfY: 1,
|
|
196
|
+
halfZ: 1,
|
|
197
|
+
rotationX: 0,
|
|
198
|
+
rotationY: 0,
|
|
199
|
+
rotationZ: Math.sin(Math.PI / 8),
|
|
200
|
+
rotationW: Math.cos(Math.PI / 8),
|
|
201
|
+
}),
|
|
202
|
+
out,
|
|
203
|
+
);
|
|
204
|
+
expect(Math.abs(out.inertiaXY)).toBeGreaterThan(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('realigns a capsule that is not on the Y axis', () => {
|
|
208
|
+
// The closed form is written for a Y-axis capsule. One lying along X must end up with its SMALL
|
|
209
|
+
// moment on x, not on y.
|
|
210
|
+
const out = createPhysics3DMassData();
|
|
211
|
+
computePhysics3DColliderMassData(
|
|
212
|
+
colliderOf({ kind: 'capsule', x0: -2, y0: 0, z0: 0, x1: 2, y1: 0, z1: 0, radius: 0.5 }),
|
|
213
|
+
out,
|
|
214
|
+
);
|
|
215
|
+
expect(out.inertiaXX).toBeLessThan(out.inertiaYY);
|
|
216
|
+
expect(out.inertiaYY).toBeCloseTo(out.inertiaZZ, 9);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('integrates a convex hull rather than zeroing it', () => {
|
|
220
|
+
const out = createPhysics3DMassData();
|
|
221
|
+
computePhysics3DColliderMassData(colliderOf({ kind: 'convex', points: [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] }), out);
|
|
222
|
+
expect(out.mass).toBeCloseTo(1 / 6, 9);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('computePhysics3DConeMassData', () => {
|
|
227
|
+
it('has exactly one third the mass of the cylinder that circumscribes it', () => {
|
|
228
|
+
// The cleanest independent check on the volume term: a cone is a third of its bounding cylinder, a
|
|
229
|
+
// relation that holds for every radius, height, and density.
|
|
230
|
+
const cone = createPhysics3DMassData();
|
|
231
|
+
const cylinder = createPhysics3DMassData();
|
|
232
|
+
computePhysics3DConeMassData(1.7, 4, 2.5, cone);
|
|
233
|
+
computePhysics3DCylinderMassData(1.7, 2, 2.5, cylinder);
|
|
234
|
+
expect(cone.mass).toBeCloseTo(cylinder.mass / 3, 10);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('matches a numerically integrated cone about its own centroid', () => {
|
|
238
|
+
// The strong test, and the one worth the runtime. The transverse moment is the term most easily got
|
|
239
|
+
// wrong — the textbook value is usually quoted about the APEX, and using it here would inflate a tall
|
|
240
|
+
// cone's inertia by a factor of sixteen while still looking like a plausible number. This integrates
|
|
241
|
+
// the solid on a grid instead, sharing no algebra with the closed form.
|
|
242
|
+
const radius = 1.2;
|
|
243
|
+
const height = 3;
|
|
244
|
+
const density = 2;
|
|
245
|
+
const out = createPhysics3DMassData();
|
|
246
|
+
computePhysics3DConeMassData(radius, height, density, out);
|
|
247
|
+
|
|
248
|
+
// Centroid at the origin, symmetry axis along +Y, so apex sits at 3h/4 and the base at -h/4 — the
|
|
249
|
+
// frame the closed form reports in.
|
|
250
|
+
const apexY = 0.75 * height;
|
|
251
|
+
const baseY = -0.25 * height;
|
|
252
|
+
const steps = 90;
|
|
253
|
+
const step = (2 * radius) / steps;
|
|
254
|
+
const stepY = height / steps;
|
|
255
|
+
let mass = 0;
|
|
256
|
+
let axial = 0;
|
|
257
|
+
let transverse = 0;
|
|
258
|
+
for (let iy = 0; iy < steps; iy += 1) {
|
|
259
|
+
const y = baseY + (iy + 0.5) * stepY;
|
|
260
|
+
// The permitted radius tapers linearly from `radius` at the base to zero at the apex.
|
|
261
|
+
const permitted = radius * ((apexY - y) / height);
|
|
262
|
+
for (let ix = 0; ix < steps; ix += 1) {
|
|
263
|
+
const x = -radius + (ix + 0.5) * step;
|
|
264
|
+
for (let iz = 0; iz < steps; iz += 1) {
|
|
265
|
+
const z = -radius + (iz + 0.5) * step;
|
|
266
|
+
if (x * x + z * z > permitted * permitted) continue;
|
|
267
|
+
const cell = step * step * stepY * density;
|
|
268
|
+
mass += cell;
|
|
269
|
+
axial += cell * (x * x + z * z);
|
|
270
|
+
transverse += cell * (x * x + y * y);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
expect(out.mass).toBeCloseTo(mass, 1);
|
|
276
|
+
expect(Math.abs(out.inertiaYY - axial) / axial).toBeLessThan(0.02);
|
|
277
|
+
expect(Math.abs(out.inertiaXX - transverse) / transverse).toBeLessThan(0.02);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('is symmetric across the two axes orthogonal to its own', () => {
|
|
281
|
+
const out = createPhysics3DMassData();
|
|
282
|
+
computePhysics3DConeMassData(1.5, 3, 2, out);
|
|
283
|
+
expect(out.inertiaZZ).toBeCloseTo(out.inertiaXX, 10);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('computePhysics3DConvexHullMassData', () => {
|
|
288
|
+
function boxPoints(halfX: number, halfY: number, halfZ: number, offsetX = 0): number[] {
|
|
289
|
+
const points: number[] = [];
|
|
290
|
+
for (const x of [-halfX, halfX]) {
|
|
291
|
+
for (const y of [-halfY, halfY]) {
|
|
292
|
+
for (const z of [-halfZ, halfZ]) points.push(x + offsetX, y, z);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return points;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
it('reproduces the closed-form box tensor exactly for a box-shaped hull', () => {
|
|
299
|
+
// The strongest check available: two completely independent routes to the same six numbers. The
|
|
300
|
+
// closed form is algebra on the extents; the hull integrates over a triangulation it derived itself.
|
|
301
|
+
const hull = createPhysics3DMassData();
|
|
302
|
+
const box = createPhysics3DMassData();
|
|
303
|
+
computePhysics3DConvexHullMassData(boxPoints(1.5, 0.75, 2.25), 3, hull);
|
|
304
|
+
computePhysics3DBoxMassData(1.5, 0.75, 2.25, 3, box);
|
|
305
|
+
|
|
306
|
+
expect(hull.mass).toBeCloseTo(box.mass, 9);
|
|
307
|
+
expect(hull.inertiaXX).toBeCloseTo(box.inertiaXX, 9);
|
|
308
|
+
expect(hull.inertiaYY).toBeCloseTo(box.inertiaYY, 9);
|
|
309
|
+
expect(hull.inertiaZZ).toBeCloseTo(box.inertiaZZ, 9);
|
|
310
|
+
expect(hull.inertiaXY).toBeCloseTo(0, 9);
|
|
311
|
+
expect(hull.inertiaXZ).toBeCloseTo(0, 9);
|
|
312
|
+
expect(hull.inertiaYZ).toBeCloseTo(0, 9);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('reports the tensor about the CENTRE OF MASS for an offset hull', () => {
|
|
316
|
+
// The case a centred hull cannot test, and the one that caught the parallel-axis shift being applied
|
|
317
|
+
// in the wrong direction: the tensor came out with twice the offset term rather than none of it,
|
|
318
|
+
// which is invisible at an offset of zero.
|
|
319
|
+
const hull = createPhysics3DMassData();
|
|
320
|
+
const reference = createPhysics3DMassData();
|
|
321
|
+
computePhysics3DConvexHullMassData(boxPoints(1, 1, 1, 5), 1, hull);
|
|
322
|
+
computePhysics3DBoxMassData(1, 1, 1, 1, reference);
|
|
323
|
+
|
|
324
|
+
expect(hull.centerX).toBeCloseTo(5, 9);
|
|
325
|
+
expect(hull.centerY).toBeCloseTo(0, 9);
|
|
326
|
+
// A cube's tensor about its own centre does not depend on where that centre is.
|
|
327
|
+
expect(hull.inertiaXX).toBeCloseTo(reference.inertiaXX, 9);
|
|
328
|
+
expect(hull.inertiaYY).toBeCloseTo(reference.inertiaYY, 9);
|
|
329
|
+
expect(hull.inertiaZZ).toBeCloseTo(reference.inertiaZZ, 9);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('approaches the closed-form sphere from below as the sample gets denser', () => {
|
|
333
|
+
// An inscribed hull is strictly smaller than the sphere it samples, so this is a bound with a
|
|
334
|
+
// direction rather than a tolerance — a formula that were merely close could sit on either side.
|
|
335
|
+
const hull = createPhysics3DMassData();
|
|
336
|
+
const sphere = createPhysics3DMassData();
|
|
337
|
+
const points: number[] = [];
|
|
338
|
+
for (let i = 0; i < 400; i += 1) {
|
|
339
|
+
const z = 1 - (2 * i) / 399;
|
|
340
|
+
const radius = Math.sqrt(Math.max(0, 1 - z * z));
|
|
341
|
+
const theta = i * 2.399963;
|
|
342
|
+
points.push(radius * Math.cos(theta), radius * Math.sin(theta), z);
|
|
343
|
+
}
|
|
344
|
+
computePhysics3DConvexHullMassData(points, 1, hull);
|
|
345
|
+
computePhysics3DSphereMassData(1, 1, sphere);
|
|
346
|
+
|
|
347
|
+
expect(hull.mass).toBeLessThan(sphere.mass);
|
|
348
|
+
expect(hull.mass).toBeGreaterThan(sphere.mass * 0.98);
|
|
349
|
+
expect(hull.inertiaXX).toBeLessThan(sphere.inertiaXX);
|
|
350
|
+
expect(hull.inertiaXX).toBeGreaterThan(sphere.inertiaXX * 0.96);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('scales linearly with density', () => {
|
|
354
|
+
const single = createPhysics3DMassData();
|
|
355
|
+
const triple = createPhysics3DMassData();
|
|
356
|
+
computePhysics3DConvexHullMassData(boxPoints(1, 1, 1), 1, single);
|
|
357
|
+
computePhysics3DConvexHullMassData(boxPoints(1, 1, 1), 3, triple);
|
|
358
|
+
|
|
359
|
+
expect(triple.mass).toBeCloseTo(single.mass * 3, 9);
|
|
360
|
+
expect(triple.inertiaXX).toBeCloseTo(single.inertiaXX * 3, 9);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('gives a degenerate hull no mass rather than a plausible wrong one', () => {
|
|
364
|
+
const out = createPhysics3DMassData();
|
|
365
|
+
// Coplanar, so there is no solid.
|
|
366
|
+
computePhysics3DConvexHullMassData([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], 1, out);
|
|
367
|
+
expect(out.mass).toBe(0);
|
|
368
|
+
computePhysics3DConvexHullMassData([], 1, out);
|
|
369
|
+
expect(out.mass).toBe(0);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('computePhysics3DCylinderMassData', () => {
|
|
374
|
+
it('is the capsule less its two hemispheres', () => {
|
|
375
|
+
const cylinder = createPhysics3DMassData();
|
|
376
|
+
const capsule = createPhysics3DMassData();
|
|
377
|
+
computePhysics3DCylinderMassData(2, 3, 1.5, cylinder);
|
|
378
|
+
computePhysics3DCapsuleMassData(2, 3, 1.5, capsule);
|
|
379
|
+
const hemispheres = (4 / 3) * Math.PI * 8 * 1.5;
|
|
380
|
+
expect(cylinder.mass).toBeCloseTo(capsule.mass - hemispheres, 10);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('resists rotation least about its own axis', () => {
|
|
384
|
+
const out = createPhysics3DMassData();
|
|
385
|
+
computePhysics3DCylinderMassData(1, 4, 1, out);
|
|
386
|
+
expect(out.inertiaYY).toBeLessThan(out.inertiaXX);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('matches the closed form for a solid cylinder', () => {
|
|
390
|
+
const out = createPhysics3DMassData();
|
|
391
|
+
computePhysics3DCylinderMassData(2, 1.5, 1, out);
|
|
392
|
+
const mass = Math.PI * 4 * 3;
|
|
393
|
+
expect(out.mass).toBeCloseTo(mass, 10);
|
|
394
|
+
expect(out.inertiaYY).toBeCloseTo(0.5 * mass * 4, 10);
|
|
395
|
+
expect(out.inertiaXX).toBeCloseTo(mass * (9 / 12 + 1), 10);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('computePhysics3DSphereMassData', () => {
|
|
400
|
+
it('derives mass from volume and density', () => {
|
|
401
|
+
const out = createPhysics3DMassData();
|
|
402
|
+
computePhysics3DSphereMassData(2, 3, out);
|
|
403
|
+
expect(out.mass).toBeCloseTo((4 / 3) * Math.PI * 8 * 3, 10);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('uses the two-fifths moment on every axis', () => {
|
|
407
|
+
const out = createPhysics3DMassData();
|
|
408
|
+
computePhysics3DSphereMassData(2, 3, out);
|
|
409
|
+
expect(out.inertiaXX).toBeCloseTo(0.4 * out.mass * 4, 10);
|
|
410
|
+
expect(out.inertiaYY).toBeCloseTo(out.inertiaXX, 12);
|
|
411
|
+
expect(out.inertiaZZ).toBeCloseTo(out.inertiaXX, 12);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
function testBody(type: RigidBody3D['type']): RigidBody3D {
|
|
416
|
+
return {
|
|
417
|
+
index: 0,
|
|
418
|
+
type,
|
|
419
|
+
x: 0,
|
|
420
|
+
y: 0,
|
|
421
|
+
z: 0,
|
|
422
|
+
orientationX: 0,
|
|
423
|
+
orientationY: 0,
|
|
424
|
+
orientationZ: 0,
|
|
425
|
+
orientationW: 1,
|
|
426
|
+
velocityX: 0,
|
|
427
|
+
velocityY: 0,
|
|
428
|
+
velocityZ: 0,
|
|
429
|
+
angularVelocityX: 0,
|
|
430
|
+
angularVelocityY: 0,
|
|
431
|
+
angularVelocityZ: 0,
|
|
432
|
+
forceX: 0,
|
|
433
|
+
forceY: 0,
|
|
434
|
+
forceZ: 0,
|
|
435
|
+
torqueX: 0,
|
|
436
|
+
torqueY: 0,
|
|
437
|
+
torqueZ: 0,
|
|
438
|
+
mass: 0,
|
|
439
|
+
inverseMass: 0,
|
|
440
|
+
inertiaXX: 0,
|
|
441
|
+
inertiaYY: 0,
|
|
442
|
+
inertiaZZ: 0,
|
|
443
|
+
inertiaXY: 0,
|
|
444
|
+
inertiaXZ: 0,
|
|
445
|
+
inertiaYZ: 0,
|
|
446
|
+
inverseInertiaXX: 0,
|
|
447
|
+
inverseInertiaYY: 0,
|
|
448
|
+
inverseInertiaZZ: 0,
|
|
449
|
+
inverseInertiaXY: 0,
|
|
450
|
+
inverseInertiaXZ: 0,
|
|
451
|
+
inverseInertiaYZ: 0,
|
|
452
|
+
inverseInertiaWorldXX: 0,
|
|
453
|
+
inverseInertiaWorldYY: 0,
|
|
454
|
+
inverseInertiaWorldZZ: 0,
|
|
455
|
+
inverseInertiaWorldXY: 0,
|
|
456
|
+
inverseInertiaWorldXZ: 0,
|
|
457
|
+
inverseInertiaWorldYZ: 0,
|
|
458
|
+
centerX: 0,
|
|
459
|
+
centerY: 0,
|
|
460
|
+
centerZ: 0,
|
|
461
|
+
linearDamping: 0,
|
|
462
|
+
angularDamping: 0,
|
|
463
|
+
gravityScale: 1,
|
|
464
|
+
fixedRotation: false,
|
|
465
|
+
bullet: false,
|
|
466
|
+
sleeping: false,
|
|
467
|
+
sleepEnabled: true,
|
|
468
|
+
sleepTimer: 0,
|
|
469
|
+
colliders: [],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
describe('createPhysics3DMassData', () => {
|
|
474
|
+
it('is the additive identity — all zero', () => {
|
|
475
|
+
const out = createPhysics3DMassData();
|
|
476
|
+
expect(out.mass).toBe(0);
|
|
477
|
+
expect(out.inertiaXX).toBe(0);
|
|
478
|
+
expect(out.centerZ).toBe(0);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe('setRigidBody3DMassData', () => {
|
|
483
|
+
it('derives the inverse mass and a non-zero inverse tensor for a dynamic body', () => {
|
|
484
|
+
const body = testBody('dynamic');
|
|
485
|
+
const data = createPhysics3DMassData();
|
|
486
|
+
computePhysics3DSphereMassData(1, 1, data);
|
|
487
|
+
|
|
488
|
+
setRigidBody3DMassData(body, data);
|
|
489
|
+
|
|
490
|
+
expect(body.mass).toBeCloseTo(data.mass, 12);
|
|
491
|
+
expect(body.inverseMass).toBeCloseTo(1 / data.mass, 10);
|
|
492
|
+
expect(body.inverseInertiaXX).toBeCloseTo(1 / data.inertiaXX, 8);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('gives a static body zero inverse mass and zero inverse inertia while keeping the forward values', () => {
|
|
496
|
+
const body = testBody('static');
|
|
497
|
+
const data = createPhysics3DMassData();
|
|
498
|
+
computePhysics3DSphereMassData(1, 1, data);
|
|
499
|
+
|
|
500
|
+
setRigidBody3DMassData(body, data);
|
|
501
|
+
|
|
502
|
+
expect(body.mass).toBeCloseTo(data.mass, 12);
|
|
503
|
+
expect(body.inverseMass).toBe(0);
|
|
504
|
+
expect(body.inverseInertiaXX).toBe(0);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('gives a kinematic body the same zero inverses as a static one', () => {
|
|
508
|
+
const body = testBody('kinematic');
|
|
509
|
+
const data = createPhysics3DMassData();
|
|
510
|
+
computePhysics3DSphereMassData(1, 1, data);
|
|
511
|
+
|
|
512
|
+
setRigidBody3DMassData(body, data);
|
|
513
|
+
|
|
514
|
+
expect(body.inverseMass).toBe(0);
|
|
515
|
+
expect(body.inverseInertiaZZ).toBe(0);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('zeroes only the inverse inertia for a fixed-rotation dynamic body, keeping its inverse mass', () => {
|
|
519
|
+
const body = testBody('dynamic');
|
|
520
|
+
body.fixedRotation = true;
|
|
521
|
+
const data = createPhysics3DMassData();
|
|
522
|
+
computePhysics3DSphereMassData(1, 1, data);
|
|
523
|
+
|
|
524
|
+
setRigidBody3DMassData(body, data);
|
|
525
|
+
|
|
526
|
+
expect(body.inverseMass).toBeGreaterThan(0);
|
|
527
|
+
expect(body.inverseInertiaXX).toBe(0);
|
|
528
|
+
expect(body.inverseInertiaYY).toBe(0);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('treats a zero-mass dynamic body as immovable rather than dividing by zero', () => {
|
|
532
|
+
const body = testBody('dynamic');
|
|
533
|
+
|
|
534
|
+
setRigidBody3DMassData(body, createPhysics3DMassData());
|
|
535
|
+
|
|
536
|
+
expect(body.inverseMass).toBe(0);
|
|
537
|
+
expect(body.inverseInertiaXX).toBe(0);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('copies the centre of mass onto the body', () => {
|
|
541
|
+
const body = testBody('dynamic');
|
|
542
|
+
const data = createPhysics3DMassData();
|
|
543
|
+
computePhysics3DSphereMassData(1, 1, data);
|
|
544
|
+
data.centerY = 4;
|
|
545
|
+
|
|
546
|
+
setRigidBody3DMassData(body, data);
|
|
547
|
+
|
|
548
|
+
expect(body.centerY).toBe(4);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
describe('updateRigidBody3DMassData', () => {
|
|
553
|
+
it('derives a body mass from its colliders', () => {
|
|
554
|
+
const world = createPhysics3DWorld();
|
|
555
|
+
const body = createRigidBody3D('dynamic');
|
|
556
|
+
addPhysics3DBody(world, body);
|
|
557
|
+
body.colliders.push(
|
|
558
|
+
createPhysics3DCollider({ kind: 'aabb', minX: -0.5, minY: -0.5, minZ: -0.5, maxX: 0.5, maxY: 0.5, maxZ: 0.5 }),
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
updateRigidBody3DMassData(body);
|
|
562
|
+
|
|
563
|
+
expect(body.mass).toBeCloseTo(1, 9);
|
|
564
|
+
expect(body.inverseMass).toBeCloseTo(1, 9);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('balances a body between two colliders', () => {
|
|
568
|
+
const world = createPhysics3DWorld();
|
|
569
|
+
const body = createRigidBody3D('dynamic');
|
|
570
|
+
addPhysics3DBody(world, body);
|
|
571
|
+
for (const offset of [-2, 2]) {
|
|
572
|
+
body.colliders.push(
|
|
573
|
+
createPhysics3DCollider({
|
|
574
|
+
kind: 'aabb',
|
|
575
|
+
minX: offset - 0.5,
|
|
576
|
+
minY: -0.5,
|
|
577
|
+
minZ: -0.5,
|
|
578
|
+
maxX: offset + 0.5,
|
|
579
|
+
maxY: 0.5,
|
|
580
|
+
maxZ: 0.5,
|
|
581
|
+
}),
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
updateRigidBody3DMassData(body);
|
|
586
|
+
|
|
587
|
+
expect(body.mass).toBeCloseTo(2, 9);
|
|
588
|
+
expect(body.centerX).toBeCloseTo(0, 9);
|
|
589
|
+
// Two masses held 2 apart swing far harder about y and z than one box does.
|
|
590
|
+
expect(body.inertiaYY).toBeGreaterThan(body.inertiaXX);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('leaves a body with no colliders massless', () => {
|
|
594
|
+
const body = createRigidBody3D('dynamic');
|
|
595
|
+
updateRigidBody3DMassData(body);
|
|
596
|
+
expect(body.mass).toBe(0);
|
|
597
|
+
expect(body.inverseMass).toBe(0);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('gives a static body zero inverse mass while keeping its forward mass', () => {
|
|
601
|
+
const body = createRigidBody3D('static');
|
|
602
|
+
body.colliders.push(
|
|
603
|
+
createPhysics3DCollider({ kind: 'aabb', minX: -0.5, minY: -0.5, minZ: -0.5, maxX: 0.5, maxY: 0.5, maxZ: 0.5 }),
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
updateRigidBody3DMassData(body);
|
|
607
|
+
|
|
608
|
+
expect(body.mass).toBeCloseTo(1, 9);
|
|
609
|
+
expect(body.inverseMass).toBe(0);
|
|
610
|
+
});
|
|
611
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { mixPhysics3DFriction, mixPhysics3DRestitution } from './material';
|
|
4
|
+
|
|
5
|
+
describe('mixPhysics3DFriction', () => {
|
|
6
|
+
it('keeps a coefficient two equal surfaces share', () => {
|
|
7
|
+
expect(mixPhysics3DFriction(0.3, 0.3)).toBeCloseTo(0.3, 12);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('makes the pair frictionless when either surface is', () => {
|
|
11
|
+
// Ice against anything slides. This is the property that makes the geometric mean the right mix and
|
|
12
|
+
// an average the wrong one.
|
|
13
|
+
expect(mixPhysics3DFriction(0, 0.9)).toBe(0);
|
|
14
|
+
expect(mixPhysics3DFriction(0.9, 0)).toBe(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('is symmetric, so a pair does not depend on which body was added first', () => {
|
|
18
|
+
expect(mixPhysics3DFriction(0.2, 0.8)).toBeCloseTo(mixPhysics3DFriction(0.8, 0.2), 12);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('lands between the two coefficients', () => {
|
|
22
|
+
const mixed = mixPhysics3DFriction(0.1, 0.9);
|
|
23
|
+
expect(mixed).toBeGreaterThan(0.1);
|
|
24
|
+
expect(mixed).toBeLessThan(0.9);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('mixPhysics3DRestitution', () => {
|
|
29
|
+
it('takes the bouncier surface', () => {
|
|
30
|
+
expect(mixPhysics3DRestitution(0.1, 0.8)).toBe(0.8);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('is symmetric', () => {
|
|
34
|
+
expect(mixPhysics3DRestitution(0.8, 0.1)).toBe(mixPhysics3DRestitution(0.1, 0.8));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('keeps a pair of dead surfaces dead', () => {
|
|
38
|
+
expect(mixPhysics3DRestitution(0, 0)).toBe(0);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getNodeLocalMatrix4 } from '@flighthq/node/contract';
|
|
2
|
+
import { createNode3D } from '@flighthq/scene3d/contract';
|
|
3
|
+
import type { RigidBody3D } from '@flighthq/types/contract';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { syncPhysics3DBodyToNode3D } from './nodeSync';
|
|
7
|
+
|
|
8
|
+
function stubBody(x: number, y: number, z: number, qx: number, qy: number, qz: number, qw: number): RigidBody3D {
|
|
9
|
+
return {
|
|
10
|
+
x,
|
|
11
|
+
y,
|
|
12
|
+
z,
|
|
13
|
+
orientationX: qx,
|
|
14
|
+
orientationY: qy,
|
|
15
|
+
orientationZ: qz,
|
|
16
|
+
orientationW: qw,
|
|
17
|
+
} as RigidBody3D;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('syncPhysics3DBodyToNode3D', () => {
|
|
21
|
+
it('copies position from body to node', () => {
|
|
22
|
+
const body = stubBody(3, 7, -2, 0, 0, 0, 1);
|
|
23
|
+
const node = createNode3D();
|
|
24
|
+
syncPhysics3DBodyToNode3D(body, node);
|
|
25
|
+
expect(node.position.x).toBe(3);
|
|
26
|
+
expect(node.position.y).toBe(7);
|
|
27
|
+
expect(node.position.z).toBe(-2);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('copies orientation quaternion from body to node', () => {
|
|
31
|
+
const body = stubBody(0, 0, 0, 0.5, 0.5, 0.5, 0.5);
|
|
32
|
+
const node = createNode3D();
|
|
33
|
+
syncPhysics3DBodyToNode3D(body, node);
|
|
34
|
+
expect(node.rotation.x).toBe(0.5);
|
|
35
|
+
expect(node.rotation.y).toBe(0.5);
|
|
36
|
+
expect(node.rotation.z).toBe(0.5);
|
|
37
|
+
expect(node.rotation.w).toBe(0.5);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('invalidates the local matrix so the next world-transform read recomputes', () => {
|
|
41
|
+
const node = createNode3D();
|
|
42
|
+
const matrixBefore = getNodeLocalMatrix4(node);
|
|
43
|
+
const m12Before = matrixBefore.m[12];
|
|
44
|
+
const body = stubBody(10, 0, 0, 0, 0, 0, 1);
|
|
45
|
+
syncPhysics3DBodyToNode3D(body, node);
|
|
46
|
+
const matrixAfter = getNodeLocalMatrix4(node);
|
|
47
|
+
expect(matrixAfter.m[12]).toBe(10);
|
|
48
|
+
expect(matrixAfter.m[12]).not.toBe(m12Before);
|
|
49
|
+
});
|
|
50
|
+
});
|