@flighthq/physics3d 0.4.0-next.1694.598ef6f

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.
Files changed (109) hide show
  1. package/dist/contacts.d.ts +4 -0
  2. package/dist/contacts.d.ts.map +1 -0
  3. package/dist/contacts.js +55 -0
  4. package/dist/contacts.js.map +1 -0
  5. package/dist/contract.d.ts +17 -0
  6. package/dist/contract.d.ts.map +1 -0
  7. package/dist/contract.js +17 -0
  8. package/dist/contract.js.map +1 -0
  9. package/dist/enablePhysics3DGuards.d.ts +4 -0
  10. package/dist/enablePhysics3DGuards.d.ts.map +1 -0
  11. package/dist/enablePhysics3DGuards.js +68 -0
  12. package/dist/enablePhysics3DGuards.js.map +1 -0
  13. package/dist/explainPhysics3DJoints.d.ts +3 -0
  14. package/dist/explainPhysics3DJoints.d.ts.map +1 -0
  15. package/dist/explainPhysics3DJoints.js +32 -0
  16. package/dist/explainPhysics3DJoints.js.map +1 -0
  17. package/dist/explainPhysics3DStep.d.ts +3 -0
  18. package/dist/explainPhysics3DStep.d.ts.map +1 -0
  19. package/dist/explainPhysics3DStep.js +48 -0
  20. package/dist/explainPhysics3DStep.js.map +1 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +14 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/integrate.d.ts +6 -0
  26. package/dist/integrate.d.ts.map +1 -0
  27. package/dist/integrate.js +154 -0
  28. package/dist/integrate.js.map +1 -0
  29. package/dist/islands.d.ts +5 -0
  30. package/dist/islands.d.ts.map +1 -0
  31. package/dist/islands.js +316 -0
  32. package/dist/islands.js.map +1 -0
  33. package/dist/jointCollisionSuppression.d.ts +4 -0
  34. package/dist/jointCollisionSuppression.d.ts.map +1 -0
  35. package/dist/jointCollisionSuppression.js +40 -0
  36. package/dist/jointCollisionSuppression.js.map +1 -0
  37. package/dist/jointFactories.d.ts +8 -0
  38. package/dist/jointFactories.d.ts.map +1 -0
  39. package/dist/jointFactories.js +126 -0
  40. package/dist/jointFactories.js.map +1 -0
  41. package/dist/jointMath.d.ts +17 -0
  42. package/dist/jointMath.d.ts.map +1 -0
  43. package/dist/jointMath.js +361 -0
  44. package/dist/jointMath.js.map +1 -0
  45. package/dist/jointRegistry.d.ts +8 -0
  46. package/dist/jointRegistry.d.ts.map +1 -0
  47. package/dist/jointRegistry.js +144 -0
  48. package/dist/jointRegistry.js.map +1 -0
  49. package/dist/jointRows.d.ts +32 -0
  50. package/dist/jointRows.d.ts.map +1 -0
  51. package/dist/jointRows.js +308 -0
  52. package/dist/jointRows.js.map +1 -0
  53. package/dist/joints.d.ts +14 -0
  54. package/dist/joints.d.ts.map +1 -0
  55. package/dist/joints.js +758 -0
  56. package/dist/joints.js.map +1 -0
  57. package/dist/massProperties.d.ts +8 -0
  58. package/dist/massProperties.d.ts.map +1 -0
  59. package/dist/massProperties.js +169 -0
  60. package/dist/massProperties.js.map +1 -0
  61. package/dist/ownership.d.ts +5 -0
  62. package/dist/ownership.d.ts.map +1 -0
  63. package/dist/ownership.js +16 -0
  64. package/dist/ownership.js.map +1 -0
  65. package/dist/registerBuiltInPhysics3DJointSolvers.d.ts +3 -0
  66. package/dist/registerBuiltInPhysics3DJointSolvers.d.ts.map +1 -0
  67. package/dist/registerBuiltInPhysics3DJointSolvers.js +14 -0
  68. package/dist/registerBuiltInPhysics3DJointSolvers.js.map +1 -0
  69. package/dist/solver.d.ts +8 -0
  70. package/dist/solver.d.ts.map +1 -0
  71. package/dist/solver.js +383 -0
  72. package/dist/solver.js.map +1 -0
  73. package/dist/step.d.ts +5 -0
  74. package/dist/step.d.ts.map +1 -0
  75. package/dist/step.js +250 -0
  76. package/dist/step.js.map +1 -0
  77. package/dist/stepValidation.d.ts +12 -0
  78. package/dist/stepValidation.d.ts.map +1 -0
  79. package/dist/stepValidation.js +201 -0
  80. package/dist/stepValidation.js.map +1 -0
  81. package/dist/symmetricTensor.d.ts +11 -0
  82. package/dist/symmetricTensor.d.ts.map +1 -0
  83. package/dist/symmetricTensor.js +150 -0
  84. package/dist/symmetricTensor.js.map +1 -0
  85. package/dist/world.d.ts +19 -0
  86. package/dist/world.d.ts.map +1 -0
  87. package/dist/world.js +370 -0
  88. package/dist/world.js.map +1 -0
  89. package/package.json +49 -0
  90. package/src/contacts.test.ts +64 -0
  91. package/src/enablePhysics3DGuards.test.ts +105 -0
  92. package/src/explainPhysics3DJoints.test.ts +101 -0
  93. package/src/explainPhysics3DStep.test.ts +123 -0
  94. package/src/integrate.test.ts +249 -0
  95. package/src/islands.test.ts +357 -0
  96. package/src/jointCollisionSuppression.test.ts +94 -0
  97. package/src/jointFactories.test.ts +195 -0
  98. package/src/jointMath.test.ts +643 -0
  99. package/src/jointRegistry.test.ts +229 -0
  100. package/src/jointRows.test.ts +484 -0
  101. package/src/joints.test.ts +677 -0
  102. package/src/massProperties.test.ts +305 -0
  103. package/src/ownership.test.ts +36 -0
  104. package/src/registerBuiltInPhysics3DJointSolvers.test.ts +48 -0
  105. package/src/solver.test.ts +481 -0
  106. package/src/step.test.ts +390 -0
  107. package/src/stepValidation.test.ts +244 -0
  108. package/src/symmetricTensor.test.ts +166 -0
  109. package/src/world.test.ts +512 -0
@@ -0,0 +1,305 @@
1
+ import type { RigidBody3D } from '@flighthq/types/contract';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import {
5
+ combinePhysics3DMassData,
6
+ computePhysics3DBoxMassData,
7
+ computePhysics3DCapsuleMassData,
8
+ computePhysics3DSphereMassData,
9
+ createPhysics3DMassData,
10
+ setRigidBody3DMassData,
11
+ } from './massProperties';
12
+
13
+ describe('combinePhysics3DMassData', () => {
14
+ it('starts from the zero identity, adopting the addend outright', () => {
15
+ const target = createPhysics3DMassData();
16
+ const sphere = createPhysics3DMassData();
17
+ computePhysics3DSphereMassData(1, 1, sphere);
18
+
19
+ combinePhysics3DMassData(target, sphere);
20
+
21
+ expect(target.mass).toBeCloseTo(sphere.mass, 12);
22
+ expect(target.inertiaXX).toBeCloseTo(sphere.inertiaXX, 12);
23
+ });
24
+
25
+ it('puts the combined centre between two equal masses', () => {
26
+ const target = createPhysics3DMassData();
27
+ const left = createPhysics3DMassData();
28
+ const right = createPhysics3DMassData();
29
+ computePhysics3DSphereMassData(1, 1, left);
30
+ computePhysics3DSphereMassData(1, 1, right);
31
+ left.centerX = -2;
32
+ right.centerX = 2;
33
+
34
+ combinePhysics3DMassData(target, left);
35
+ combinePhysics3DMassData(target, right);
36
+
37
+ expect(target.centerX).toBeCloseTo(0, 12);
38
+ expect(target.mass).toBeCloseTo(left.mass + right.mass, 12);
39
+ });
40
+
41
+ it('shifts the ALREADY-ACCUMULATED tensor too, not only the addend', () => {
42
+ // Two unit spheres offset along x. About the combined centre, each contributes its own moment plus
43
+ // mass * d^2 for the y and z axes. A version that shifted only the addend would report half of it.
44
+ const target = createPhysics3DMassData();
45
+ const left = createPhysics3DMassData();
46
+ const right = createPhysics3DMassData();
47
+ computePhysics3DSphereMassData(1, 1, left);
48
+ computePhysics3DSphereMassData(1, 1, right);
49
+ left.centerX = -2;
50
+ right.centerX = 2;
51
+
52
+ combinePhysics3DMassData(target, left);
53
+ combinePhysics3DMassData(target, right);
54
+
55
+ const each = left.mass;
56
+ const expectedYY = 2 * (0.4 * each * 1 + each * 4);
57
+ expect(target.inertiaYY).toBeCloseTo(expectedYY, 8);
58
+ // The x axis runs through both centres, so it gains no parallel-axis term.
59
+ expect(target.inertiaXX).toBeCloseTo(2 * 0.4 * each, 8);
60
+ });
61
+
62
+ it('is order-independent for the combined mass and centre', () => {
63
+ const forward = createPhysics3DMassData();
64
+ const backward = createPhysics3DMassData();
65
+ const a = createPhysics3DMassData();
66
+ const b = createPhysics3DMassData();
67
+ computePhysics3DSphereMassData(1, 1, a);
68
+ computePhysics3DBoxMassData(1, 2, 3, 2, b);
69
+ a.centerY = 1;
70
+ b.centerY = -3;
71
+
72
+ combinePhysics3DMassData(forward, a);
73
+ combinePhysics3DMassData(forward, b);
74
+ combinePhysics3DMassData(backward, b);
75
+ combinePhysics3DMassData(backward, a);
76
+
77
+ expect(backward.mass).toBeCloseTo(forward.mass, 10);
78
+ expect(backward.centerY).toBeCloseTo(forward.centerY, 10);
79
+ expect(backward.inertiaXX).toBeCloseTo(forward.inertiaXX, 8);
80
+ });
81
+
82
+ it('zeroes the result when both sides are massless rather than dividing by zero', () => {
83
+ const target = createPhysics3DMassData();
84
+ combinePhysics3DMassData(target, createPhysics3DMassData());
85
+ expect(target.mass).toBe(0);
86
+ expect(Number.isNaN(target.centerX)).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('computePhysics3DBoxMassData', () => {
91
+ it('derives mass from volume and density', () => {
92
+ const out = createPhysics3DMassData();
93
+ computePhysics3DBoxMassData(1, 2, 3, 5, out);
94
+ expect(out.mass).toBeCloseTo(8 * 1 * 2 * 3 * 5, 10);
95
+ });
96
+
97
+ it('gives a cube one moment on every axis', () => {
98
+ const out = createPhysics3DMassData();
99
+ computePhysics3DBoxMassData(2, 2, 2, 1, out);
100
+ expect(out.inertiaYY).toBeCloseTo(out.inertiaXX, 10);
101
+ expect(out.inertiaZZ).toBeCloseTo(out.inertiaXX, 10);
102
+ });
103
+
104
+ it('resists rotation least about its longest axis', () => {
105
+ const out = createPhysics3DMassData();
106
+ computePhysics3DBoxMassData(4, 1, 1, 1, out);
107
+ expect(out.inertiaXX).toBeLessThan(out.inertiaYY);
108
+ expect(out.inertiaYY).toBeCloseTo(out.inertiaZZ, 10);
109
+ });
110
+
111
+ it('leaves the off-diagonal terms zero for an axis-aligned box', () => {
112
+ const out = createPhysics3DMassData();
113
+ computePhysics3DBoxMassData(1, 2, 3, 1, out);
114
+ expect(out.inertiaXY).toBe(0);
115
+ expect(out.inertiaXZ).toBe(0);
116
+ expect(out.inertiaYZ).toBe(0);
117
+ });
118
+ });
119
+
120
+ describe('computePhysics3DCapsuleMassData', () => {
121
+ it('degenerates exactly to a sphere at zero half-height', () => {
122
+ const capsule = createPhysics3DMassData();
123
+ const sphere = createPhysics3DMassData();
124
+ computePhysics3DCapsuleMassData(2, 0, 3, capsule);
125
+ computePhysics3DSphereMassData(2, 3, sphere);
126
+
127
+ expect(capsule.mass).toBeCloseTo(sphere.mass, 10);
128
+ expect(capsule.inertiaXX).toBeCloseTo(sphere.inertiaXX, 8);
129
+ expect(capsule.inertiaYY).toBeCloseTo(sphere.inertiaYY, 8);
130
+ expect(capsule.inertiaZZ).toBeCloseTo(sphere.inertiaZZ, 8);
131
+ });
132
+
133
+ it('derives mass as cylinder plus two hemispheres', () => {
134
+ const out = createPhysics3DMassData();
135
+ computePhysics3DCapsuleMassData(1, 2, 1, out);
136
+ const expected = Math.PI * 1 * 4 + (4 / 3) * Math.PI * 1;
137
+ expect(out.mass).toBeCloseTo(expected, 10);
138
+ });
139
+
140
+ it('resists rotation least about its own axis', () => {
141
+ const out = createPhysics3DMassData();
142
+ computePhysics3DCapsuleMassData(1, 4, 1, out);
143
+ expect(out.inertiaYY).toBeLessThan(out.inertiaXX);
144
+ });
145
+
146
+ it('is symmetric across the two axes orthogonal to its own', () => {
147
+ const out = createPhysics3DMassData();
148
+ computePhysics3DCapsuleMassData(1.5, 3, 2, out);
149
+ expect(out.inertiaZZ).toBeCloseTo(out.inertiaXX, 10);
150
+ });
151
+ });
152
+
153
+ describe('computePhysics3DSphereMassData', () => {
154
+ it('derives mass from volume and density', () => {
155
+ const out = createPhysics3DMassData();
156
+ computePhysics3DSphereMassData(2, 3, out);
157
+ expect(out.mass).toBeCloseTo((4 / 3) * Math.PI * 8 * 3, 10);
158
+ });
159
+
160
+ it('uses the two-fifths moment on every axis', () => {
161
+ const out = createPhysics3DMassData();
162
+ computePhysics3DSphereMassData(2, 3, out);
163
+ expect(out.inertiaXX).toBeCloseTo(0.4 * out.mass * 4, 10);
164
+ expect(out.inertiaYY).toBeCloseTo(out.inertiaXX, 12);
165
+ expect(out.inertiaZZ).toBeCloseTo(out.inertiaXX, 12);
166
+ });
167
+ });
168
+
169
+ describe('createPhysics3DMassData', () => {
170
+ it('is the additive identity — all zero', () => {
171
+ const out = createPhysics3DMassData();
172
+ expect(out.mass).toBe(0);
173
+ expect(out.inertiaXX).toBe(0);
174
+ expect(out.centerZ).toBe(0);
175
+ });
176
+ });
177
+
178
+ describe('setRigidBody3DMassData', () => {
179
+ it('derives the inverse mass and a non-zero inverse tensor for a dynamic body', () => {
180
+ const body = testBody('dynamic');
181
+ const data = createPhysics3DMassData();
182
+ computePhysics3DSphereMassData(1, 1, data);
183
+
184
+ setRigidBody3DMassData(body, data);
185
+
186
+ expect(body.mass).toBeCloseTo(data.mass, 12);
187
+ expect(body.inverseMass).toBeCloseTo(1 / data.mass, 10);
188
+ expect(body.inverseInertiaXX).toBeCloseTo(1 / data.inertiaXX, 8);
189
+ });
190
+
191
+ it('gives a static body zero inverse mass and zero inverse inertia while keeping the forward values', () => {
192
+ const body = testBody('static');
193
+ const data = createPhysics3DMassData();
194
+ computePhysics3DSphereMassData(1, 1, data);
195
+
196
+ setRigidBody3DMassData(body, data);
197
+
198
+ expect(body.mass).toBeCloseTo(data.mass, 12);
199
+ expect(body.inverseMass).toBe(0);
200
+ expect(body.inverseInertiaXX).toBe(0);
201
+ });
202
+
203
+ it('gives a kinematic body the same zero inverses as a static one', () => {
204
+ const body = testBody('kinematic');
205
+ const data = createPhysics3DMassData();
206
+ computePhysics3DSphereMassData(1, 1, data);
207
+
208
+ setRigidBody3DMassData(body, data);
209
+
210
+ expect(body.inverseMass).toBe(0);
211
+ expect(body.inverseInertiaZZ).toBe(0);
212
+ });
213
+
214
+ it('zeroes only the inverse inertia for a fixed-rotation dynamic body, keeping its inverse mass', () => {
215
+ const body = testBody('dynamic');
216
+ body.fixedRotation = true;
217
+ const data = createPhysics3DMassData();
218
+ computePhysics3DSphereMassData(1, 1, data);
219
+
220
+ setRigidBody3DMassData(body, data);
221
+
222
+ expect(body.inverseMass).toBeGreaterThan(0);
223
+ expect(body.inverseInertiaXX).toBe(0);
224
+ expect(body.inverseInertiaYY).toBe(0);
225
+ });
226
+
227
+ it('treats a zero-mass dynamic body as immovable rather than dividing by zero', () => {
228
+ const body = testBody('dynamic');
229
+
230
+ setRigidBody3DMassData(body, createPhysics3DMassData());
231
+
232
+ expect(body.inverseMass).toBe(0);
233
+ expect(body.inverseInertiaXX).toBe(0);
234
+ });
235
+
236
+ it('copies the centre of mass onto the body', () => {
237
+ const body = testBody('dynamic');
238
+ const data = createPhysics3DMassData();
239
+ computePhysics3DSphereMassData(1, 1, data);
240
+ data.centerY = 4;
241
+
242
+ setRigidBody3DMassData(body, data);
243
+
244
+ expect(body.centerY).toBe(4);
245
+ });
246
+ });
247
+
248
+ function testBody(type: RigidBody3D['type']): RigidBody3D {
249
+ return {
250
+ index: 0,
251
+ type,
252
+ x: 0,
253
+ y: 0,
254
+ z: 0,
255
+ orientationX: 0,
256
+ orientationY: 0,
257
+ orientationZ: 0,
258
+ orientationW: 1,
259
+ velocityX: 0,
260
+ velocityY: 0,
261
+ velocityZ: 0,
262
+ angularVelocityX: 0,
263
+ angularVelocityY: 0,
264
+ angularVelocityZ: 0,
265
+ forceX: 0,
266
+ forceY: 0,
267
+ forceZ: 0,
268
+ torqueX: 0,
269
+ torqueY: 0,
270
+ torqueZ: 0,
271
+ mass: 0,
272
+ inverseMass: 0,
273
+ inertiaXX: 0,
274
+ inertiaYY: 0,
275
+ inertiaZZ: 0,
276
+ inertiaXY: 0,
277
+ inertiaXZ: 0,
278
+ inertiaYZ: 0,
279
+ inverseInertiaXX: 0,
280
+ inverseInertiaYY: 0,
281
+ inverseInertiaZZ: 0,
282
+ inverseInertiaXY: 0,
283
+ inverseInertiaXZ: 0,
284
+ inverseInertiaYZ: 0,
285
+ inverseInertiaWorldXX: 0,
286
+ inverseInertiaWorldYY: 0,
287
+ inverseInertiaWorldZZ: 0,
288
+ inverseInertiaWorldXY: 0,
289
+ inverseInertiaWorldXZ: 0,
290
+ inverseInertiaWorldYZ: 0,
291
+ centerX: 0,
292
+ centerY: 0,
293
+ centerZ: 0,
294
+ linearDamping: 0,
295
+ angularDamping: 0,
296
+ gravityScale: 1,
297
+ fixedRotation: false,
298
+ bullet: false,
299
+ sleeping: false,
300
+ sleepEnabled: true,
301
+ sleepTimer: 0,
302
+ material: { density: 1, friction: 0.2, restitution: 0 },
303
+ filter: { categoryBits: 1, maskBits: 0xffff, groupIndex: 0 },
304
+ };
305
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { assertPhysics3DWorldNotStepping, physics3DJointOwners, steppingPhysics3DWorlds } from './ownership';
4
+ import { createPhysics3DWorld } from './world';
5
+
6
+ describe('assertPhysics3DWorldNotStepping', () => {
7
+ it('permits a mutation outside the step', () => {
8
+ expect(() => assertPhysics3DWorldNotStepping(createPhysics3DWorld())).not.toThrow();
9
+ });
10
+
11
+ it('refuses a mutation while the world is stepping', () => {
12
+ const world = createPhysics3DWorld();
13
+ steppingPhysics3DWorlds.add(world);
14
+
15
+ expect(() => assertPhysics3DWorldNotStepping(world)).toThrow();
16
+
17
+ steppingPhysics3DWorlds.delete(world);
18
+ });
19
+
20
+ it('tracks each world separately', () => {
21
+ const stepping = createPhysics3DWorld();
22
+ const idle = createPhysics3DWorld();
23
+ steppingPhysics3DWorlds.add(stepping);
24
+
25
+ expect(() => assertPhysics3DWorldNotStepping(idle)).not.toThrow();
26
+
27
+ steppingPhysics3DWorlds.delete(stepping);
28
+ });
29
+
30
+ it('keeps joint ownership out of the serializable world record', () => {
31
+ // Ownership is runtime state: a world reconstructed by a format layer carries no owner map, and rebuilding
32
+ // one is the add path's job rather than something the record has to round-trip.
33
+ expect(physics3DJointOwners).toBeInstanceOf(WeakMap);
34
+ expect(Object.keys(createPhysics3DWorld())).not.toContain('jointOwners');
35
+ });
36
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { getPhysics3DJointSolver } from './jointRegistry';
4
+ import {
5
+ physics3DBallAndSocketJointSolver,
6
+ physics3DConeTwistJointSolver,
7
+ physics3DFixedJointSolver,
8
+ physics3DGeneric6DofJointSolver,
9
+ physics3DHingeJointSolver,
10
+ physics3DSliderJointSolver,
11
+ Physics3DBallAndSocketJointKind,
12
+ Physics3DConeTwistJointKind,
13
+ Physics3DFixedJointKind,
14
+ Physics3DGeneric6DofJointKind,
15
+ Physics3DHingeJointKind,
16
+ Physics3DSliderJointKind,
17
+ } from './joints';
18
+ import { registerBuiltInPhysics3DJointSolvers } from './registerBuiltInPhysics3DJointSolvers';
19
+ import { createPhysics3DWorld } from './world';
20
+
21
+ describe('registerBuiltInPhysics3DJointSolvers', () => {
22
+ it('binds every built-in kind to its own solver', () => {
23
+ const world = createPhysics3DWorld();
24
+
25
+ registerBuiltInPhysics3DJointSolvers(world);
26
+
27
+ expect(getPhysics3DJointSolver(world, Physics3DBallAndSocketJointKind)).toBe(physics3DBallAndSocketJointSolver);
28
+ expect(getPhysics3DJointSolver(world, Physics3DConeTwistJointKind)).toBe(physics3DConeTwistJointSolver);
29
+ expect(getPhysics3DJointSolver(world, Physics3DFixedJointKind)).toBe(physics3DFixedJointSolver);
30
+ expect(getPhysics3DJointSolver(world, Physics3DGeneric6DofJointKind)).toBe(physics3DGeneric6DofJointSolver);
31
+ expect(getPhysics3DJointSolver(world, Physics3DHingeJointKind)).toBe(physics3DHingeJointSolver);
32
+ expect(getPhysics3DJointSolver(world, Physics3DSliderJointKind)).toBe(physics3DSliderJointSolver);
33
+ });
34
+
35
+ it('registers the whole bank and nothing else', () => {
36
+ const world = createPhysics3DWorld();
37
+
38
+ registerBuiltInPhysics3DJointSolvers(world);
39
+
40
+ expect(world.jointSolvers.size).toBe(6);
41
+ });
42
+
43
+ it('leaves a world that never calls it with no solvers at all', () => {
44
+ // Registration is explicit rather than part of world construction, so constructing a world links no
45
+ // constraint math and a caller may register only the kinds it uses.
46
+ expect(createPhysics3DWorld().jointSolvers.size).toBe(0);
47
+ });
48
+ });