@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.
Files changed (180) hide show
  1. package/dist/broadphase.d.ts +4 -0
  2. package/dist/broadphase.d.ts.map +1 -0
  3. package/dist/broadphase.js +158 -0
  4. package/dist/broadphase.js.map +1 -0
  5. package/dist/colliderCollision.d.ts +5 -0
  6. package/dist/colliderCollision.d.ts.map +1 -0
  7. package/dist/colliderCollision.js +55 -0
  8. package/dist/colliderCollision.js.map +1 -0
  9. package/dist/colliderTransform.d.ts +5 -0
  10. package/dist/colliderTransform.d.ts.map +1 -0
  11. package/dist/colliderTransform.js +424 -0
  12. package/dist/colliderTransform.js.map +1 -0
  13. package/dist/contactIntake.d.ts +5 -0
  14. package/dist/contactIntake.d.ts.map +1 -0
  15. package/dist/contactIntake.js +237 -0
  16. package/dist/contactIntake.js.map +1 -0
  17. package/dist/contacts.d.ts +4 -0
  18. package/dist/contacts.d.ts.map +1 -0
  19. package/dist/contacts.js +64 -0
  20. package/dist/contacts.js.map +1 -0
  21. package/dist/continuous.d.ts +5 -0
  22. package/dist/continuous.d.ts.map +1 -0
  23. package/dist/continuous.js +829 -0
  24. package/dist/continuous.js.map +1 -0
  25. package/dist/contract.d.ts +28 -0
  26. package/dist/contract.d.ts.map +1 -0
  27. package/dist/contract.js +28 -0
  28. package/dist/contract.js.map +1 -0
  29. package/dist/debugGeometry.d.ts +4 -0
  30. package/dist/debugGeometry.d.ts.map +1 -0
  31. package/dist/debugGeometry.js +333 -0
  32. package/dist/debugGeometry.js.map +1 -0
  33. package/dist/enablePhysics3DGuards.d.ts +4 -0
  34. package/dist/enablePhysics3DGuards.d.ts.map +1 -0
  35. package/dist/enablePhysics3DGuards.js +155 -0
  36. package/dist/enablePhysics3DGuards.js.map +1 -0
  37. package/dist/explainPhysics3DCollision.d.ts +3 -0
  38. package/dist/explainPhysics3DCollision.d.ts.map +1 -0
  39. package/dist/explainPhysics3DCollision.js +30 -0
  40. package/dist/explainPhysics3DCollision.js.map +1 -0
  41. package/dist/explainPhysics3DJoints.d.ts +3 -0
  42. package/dist/explainPhysics3DJoints.d.ts.map +1 -0
  43. package/dist/explainPhysics3DJoints.js +32 -0
  44. package/dist/explainPhysics3DJoints.js.map +1 -0
  45. package/dist/explainPhysics3DStep.d.ts +3 -0
  46. package/dist/explainPhysics3DStep.d.ts.map +1 -0
  47. package/dist/explainPhysics3DStep.js +51 -0
  48. package/dist/explainPhysics3DStep.js.map +1 -0
  49. package/dist/index.d.ts +22 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +22 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/integrate.d.ts +6 -0
  54. package/dist/integrate.d.ts.map +1 -0
  55. package/dist/integrate.js +184 -0
  56. package/dist/integrate.js.map +1 -0
  57. package/dist/islands.d.ts +6 -0
  58. package/dist/islands.d.ts.map +1 -0
  59. package/dist/islands.js +333 -0
  60. package/dist/islands.js.map +1 -0
  61. package/dist/jointBreakage.d.ts +5 -0
  62. package/dist/jointBreakage.d.ts.map +1 -0
  63. package/dist/jointBreakage.js +44 -0
  64. package/dist/jointBreakage.js.map +1 -0
  65. package/dist/jointCollisionSuppression.d.ts +4 -0
  66. package/dist/jointCollisionSuppression.d.ts.map +1 -0
  67. package/dist/jointCollisionSuppression.js +40 -0
  68. package/dist/jointCollisionSuppression.js.map +1 -0
  69. package/dist/jointFactories.d.ts +9 -0
  70. package/dist/jointFactories.d.ts.map +1 -0
  71. package/dist/jointFactories.js +159 -0
  72. package/dist/jointFactories.js.map +1 -0
  73. package/dist/jointMath.d.ts +17 -0
  74. package/dist/jointMath.d.ts.map +1 -0
  75. package/dist/jointMath.js +361 -0
  76. package/dist/jointMath.js.map +1 -0
  77. package/dist/jointReaction.d.ts +8 -0
  78. package/dist/jointReaction.d.ts.map +1 -0
  79. package/dist/jointReaction.js +65 -0
  80. package/dist/jointReaction.js.map +1 -0
  81. package/dist/jointRegistry.d.ts +8 -0
  82. package/dist/jointRegistry.d.ts.map +1 -0
  83. package/dist/jointRegistry.js +144 -0
  84. package/dist/jointRegistry.js.map +1 -0
  85. package/dist/jointRows.d.ts +33 -0
  86. package/dist/jointRows.d.ts.map +1 -0
  87. package/dist/jointRows.js +352 -0
  88. package/dist/jointRows.js.map +1 -0
  89. package/dist/joints.d.ts +16 -0
  90. package/dist/joints.d.ts.map +1 -0
  91. package/dist/joints.js +1060 -0
  92. package/dist/joints.js.map +1 -0
  93. package/dist/massProperties.d.ts +13 -0
  94. package/dist/massProperties.d.ts.map +1 -0
  95. package/dist/massProperties.js +462 -0
  96. package/dist/massProperties.js.map +1 -0
  97. package/dist/material.d.ts +3 -0
  98. package/dist/material.d.ts.map +1 -0
  99. package/dist/material.js +13 -0
  100. package/dist/material.js.map +1 -0
  101. package/dist/nodeSync.d.ts +3 -0
  102. package/dist/nodeSync.d.ts.map +1 -0
  103. package/dist/nodeSync.js +16 -0
  104. package/dist/nodeSync.js.map +1 -0
  105. package/dist/ownership.d.ts +8 -0
  106. package/dist/ownership.d.ts.map +1 -0
  107. package/dist/ownership.js +26 -0
  108. package/dist/ownership.js.map +1 -0
  109. package/dist/physics3DBroadphasePublication.d.ts +5 -0
  110. package/dist/physics3DBroadphasePublication.d.ts.map +1 -0
  111. package/dist/physics3DBroadphasePublication.js +49 -0
  112. package/dist/physics3DBroadphasePublication.js.map +1 -0
  113. package/dist/physics3DSpatialIndexingGuards.d.ts +4 -0
  114. package/dist/physics3DSpatialIndexingGuards.d.ts.map +1 -0
  115. package/dist/physics3DSpatialIndexingGuards.js +11 -0
  116. package/dist/physics3DSpatialIndexingGuards.js.map +1 -0
  117. package/dist/registerBuiltInPhysics3DJointSolvers.d.ts +3 -0
  118. package/dist/registerBuiltInPhysics3DJointSolvers.d.ts.map +1 -0
  119. package/dist/registerBuiltInPhysics3DJointSolvers.js +15 -0
  120. package/dist/registerBuiltInPhysics3DJointSolvers.js.map +1 -0
  121. package/dist/solver.d.ts +8 -0
  122. package/dist/solver.d.ts.map +1 -0
  123. package/dist/solver.js +460 -0
  124. package/dist/solver.js.map +1 -0
  125. package/dist/step.d.ts +5 -0
  126. package/dist/step.d.ts.map +1 -0
  127. package/dist/step.js +302 -0
  128. package/dist/step.js.map +1 -0
  129. package/dist/stepValidation.d.ts +13 -0
  130. package/dist/stepValidation.d.ts.map +1 -0
  131. package/dist/stepValidation.js +284 -0
  132. package/dist/stepValidation.js.map +1 -0
  133. package/dist/symmetricTensor.d.ts +11 -0
  134. package/dist/symmetricTensor.d.ts.map +1 -0
  135. package/dist/symmetricTensor.js +150 -0
  136. package/dist/symmetricTensor.js.map +1 -0
  137. package/dist/world.d.ts +27 -0
  138. package/dist/world.d.ts.map +1 -0
  139. package/dist/world.js +762 -0
  140. package/dist/world.js.map +1 -0
  141. package/dist/worldQueries.d.ts +11 -0
  142. package/dist/worldQueries.d.ts.map +1 -0
  143. package/dist/worldQueries.js +409 -0
  144. package/dist/worldQueries.js.map +1 -0
  145. package/package.json +55 -0
  146. package/src/broadphase.test.ts +265 -0
  147. package/src/colliderCollision.test.ts +57 -0
  148. package/src/colliderTransform.test.ts +221 -0
  149. package/src/contactIntake.test.ts +370 -0
  150. package/src/contacts.test.ts +64 -0
  151. package/src/continuous.test.ts +679 -0
  152. package/src/debugGeometry.test.ts +288 -0
  153. package/src/enablePhysics3DGuards.test.ts +206 -0
  154. package/src/explainPhysics3DCollision.test.ts +78 -0
  155. package/src/explainPhysics3DJoints.test.ts +101 -0
  156. package/src/explainPhysics3DStep.test.ts +142 -0
  157. package/src/integrate.test.ts +279 -0
  158. package/src/islands.test.ts +384 -0
  159. package/src/jointBreakage.test.ts +143 -0
  160. package/src/jointCollisionSuppression.test.ts +94 -0
  161. package/src/jointFactories.test.ts +244 -0
  162. package/src/jointMath.test.ts +646 -0
  163. package/src/jointReaction.test.ts +241 -0
  164. package/src/jointRegistry.test.ts +229 -0
  165. package/src/jointRows.test.ts +548 -0
  166. package/src/joints.test.ts +1066 -0
  167. package/src/massProperties.test.ts +611 -0
  168. package/src/material.test.ts +40 -0
  169. package/src/nodeSync.test.ts +50 -0
  170. package/src/ownership.test.ts +61 -0
  171. package/src/physics3DBroadphasePublication.test.ts +63 -0
  172. package/src/physics3DSpatialIndexingGuards.test.ts +43 -0
  173. package/src/registerBuiltInPhysics3DJointSolvers.test.ts +51 -0
  174. package/src/solver.test.ts +567 -0
  175. package/src/step.test.ts +490 -0
  176. package/src/stepValidation.test.ts +346 -0
  177. package/src/stress.test.ts +526 -0
  178. package/src/symmetricTensor.test.ts +166 -0
  179. package/src/world.test.ts +1127 -0
  180. package/src/worldQueries.test.ts +565 -0
@@ -0,0 +1,346 @@
1
+ import type { Physics3DWorld } from '@flighthq/types/contract';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createPhysics3DContact, createPhysics3DContactPoint } from './contacts';
5
+ import { createPhysics3DBallAndSocketJoint, createPhysics3DDistanceJoint } from './jointFactories';
6
+ import { addPhysics3DJoint } from './jointRegistry';
7
+ import {
8
+ isPhysics3DBodyStateValid,
9
+ isPhysics3DColliderStateValid,
10
+ isPhysics3DContactStateValid,
11
+ isPhysics3DContactValid,
12
+ isPhysics3DGravityValid,
13
+ isPhysics3DJointStateValid,
14
+ isPhysics3DPositionIterationsValid,
15
+ isPhysics3DSolverConfigValid,
16
+ isPhysics3DSubstepsValid,
17
+ isPhysics3DTimestepValid,
18
+ isPhysics3DVelocityIterationsValid,
19
+ } from './stepValidation';
20
+ import {
21
+ addPhysics3DBody,
22
+ addPhysics3DCollider,
23
+ createPhysics3DCollider,
24
+ createPhysics3DWorld,
25
+ createRigidBody3D,
26
+ } from './world';
27
+
28
+ describe('isPhysics3DBodyStateValid', () => {
29
+ it('accepts a freshly built world', () => {
30
+ expect(isPhysics3DBodyStateValid(createTestWorld())).toBe(true);
31
+ });
32
+
33
+ it('rejects a non-finite pose, velocity, or accumulator', () => {
34
+ for (const key of ['z', 'orientationW', 'angularVelocityY', 'torqueZ', 'centerX'] as const) {
35
+ const world = createTestWorld();
36
+ world.bodies[0][key] = Number.NaN;
37
+ expect(isPhysics3DBodyStateValid(world)).toBe(false);
38
+ }
39
+ });
40
+
41
+ it('rejects a non-finite WORLD inertia tensor', () => {
42
+ const world = createTestWorld();
43
+ world.bodies[0].inverseInertiaWorldXY = Number.POSITIVE_INFINITY;
44
+
45
+ // Derived, and included anyway: it is what every constraint row multiplies by, so a NaN there reaches
46
+ // both bodies of every contact and joint within one sub-interval.
47
+ expect(isPhysics3DBodyStateValid(world)).toBe(false);
48
+ });
49
+
50
+ it('rejects a zero or materially non-unit orientation before rotation math consumes it', () => {
51
+ const zero = createTestWorld();
52
+ zero.bodies[0].orientationW = 0;
53
+ expect(isPhysics3DBodyStateValid(zero)).toBe(false);
54
+
55
+ const scaled = createTestWorld();
56
+ scaled.bodies[0].orientationZ = 1;
57
+ expect(isPhysics3DBodyStateValid(scaled)).toBe(false);
58
+
59
+ const rounded = createTestWorld();
60
+ rounded.bodies[0].orientationZ = 0.70710678;
61
+ rounded.bodies[0].orientationW = 0.70710678;
62
+ expect(isPhysics3DBodyStateValid(rounded)).toBe(true);
63
+ });
64
+
65
+ it('rejects a body the index map has lost track of', () => {
66
+ const world = createTestWorld();
67
+ world.bodyByIndex.delete(world.bodies[0].index);
68
+
69
+ expect(isPhysics3DBodyStateValid(world)).toBe(false);
70
+ });
71
+
72
+ it('rejects an unknown body type and a negative mass', () => {
73
+ const type = createTestWorld();
74
+ (type.bodies[0] as { type: string }).type = 'ghost';
75
+ expect(isPhysics3DBodyStateValid(type)).toBe(false);
76
+
77
+ const mass = createTestWorld();
78
+ mass.bodies[0].mass = -1;
79
+ expect(isPhysics3DBodyStateValid(mass)).toBe(false);
80
+ });
81
+ });
82
+
83
+ describe('isPhysics3DColliderStateValid', () => {
84
+ it('accepts valid collider geometry, material, filter, and derived shape', () => {
85
+ const world = createTestWorld();
86
+ addPhysics3DCollider(
87
+ world,
88
+ world.bodies[0],
89
+ createPhysics3DCollider({ kind: 'sphere', x: 0, y: 0, z: 0, radius: 1 }),
90
+ );
91
+
92
+ expect(isPhysics3DColliderStateValid(world)).toBe(true);
93
+ });
94
+
95
+ it('rejects degenerate authored geometry and a poisoned material', () => {
96
+ const shape = createTestWorld();
97
+ const shapeCollider = createPhysics3DCollider({ kind: 'sphere', x: 0, y: 0, z: 0, radius: 1 });
98
+ addPhysics3DCollider(shape, shape.bodies[0], shapeCollider);
99
+ if (shapeCollider.local.kind === 'sphere') shapeCollider.local.radius = 0;
100
+ expect(isPhysics3DColliderStateValid(shape)).toBe(false);
101
+
102
+ const material = createTestWorld();
103
+ const materialCollider = createPhysics3DCollider({ kind: 'sphere', x: 0, y: 0, z: 0, radius: 1 });
104
+ addPhysics3DCollider(material, material.bodies[0], materialCollider);
105
+ materialCollider.material.friction = Number.NaN;
106
+ expect(isPhysics3DColliderStateValid(material)).toBe(false);
107
+ });
108
+
109
+ it('rejects an incompatible derived kind and a fractional filter bitfield', () => {
110
+ const world = createTestWorld();
111
+ const collider = createPhysics3DCollider({ kind: 'sphere', x: 0, y: 0, z: 0, radius: 1 });
112
+ addPhysics3DCollider(world, world.bodies[0], collider);
113
+ collider.world = { kind: 'aabb', minX: -1, minY: -1, minZ: -1, maxX: 1, maxY: 1, maxZ: 1 };
114
+ expect(isPhysics3DColliderStateValid(world)).toBe(false);
115
+
116
+ collider.world = { kind: 'sphere', x: 0, y: 0, z: 0, radius: 1 };
117
+ collider.filter.maskBits = 1.5;
118
+ expect(isPhysics3DColliderStateValid(world)).toBe(false);
119
+ });
120
+ });
121
+
122
+ describe('isPhysics3DContactStateValid', () => {
123
+ it('accepts a world with no contacts', () => {
124
+ expect(isPhysics3DContactStateValid(createTestWorld())).toBe(true);
125
+ });
126
+
127
+ it('rejects the world when any one contact is unusable', () => {
128
+ const world = createTestWorld();
129
+ world.contacts.push(createPhysics3DContact(0, 1));
130
+ world.contacts.push(createPhysics3DContact(0, 1));
131
+ world.contacts[1].friction = Number.NaN;
132
+
133
+ expect(isPhysics3DContactStateValid(world)).toBe(false);
134
+ });
135
+ });
136
+
137
+ describe('isPhysics3DContactValid', () => {
138
+ it('accepts a freshly allocated contact', () => {
139
+ expect(isPhysics3DContactValid(createPhysics3DContact(0, 1))).toBe(true);
140
+ });
141
+
142
+ it('rejects a point count that runs past the points it has', () => {
143
+ const contact = createPhysics3DContact(0, 1);
144
+ contact.pointCount = 2;
145
+
146
+ expect(isPhysics3DContactValid(contact)).toBe(false);
147
+ });
148
+
149
+ it('rejects invalid collider indices before the solver indexes a body', () => {
150
+ const contact = createPhysics3DContact(0, 1);
151
+ contact.colliderA = -1;
152
+ expect(isPhysics3DContactValid(contact)).toBe(false);
153
+
154
+ contact.colliderA = 0;
155
+ contact.colliderB = 1.5;
156
+ expect(isPhysics3DContactValid(contact)).toBe(false);
157
+ });
158
+
159
+ it('rejects a negative friction or restitution', () => {
160
+ const friction = createPhysics3DContact(0, 1);
161
+ friction.friction = -0.1;
162
+ expect(isPhysics3DContactValid(friction)).toBe(false);
163
+
164
+ const restitution = createPhysics3DContact(0, 1);
165
+ restitution.restitution = -1;
166
+ expect(isPhysics3DContactValid(restitution)).toBe(false);
167
+ });
168
+
169
+ it('rejects a non-finite field on a point it will actually read', () => {
170
+ const contact = createPhysics3DContact(0, 1);
171
+ const point = createPhysics3DContactPoint();
172
+ point.rBZ = Number.NaN;
173
+ contact.points.push(point);
174
+ contact.pointCount = 1;
175
+
176
+ expect(isPhysics3DContactValid(contact)).toBe(false);
177
+ });
178
+
179
+ it('ignores a point beyond the count, which the solver never reads', () => {
180
+ const contact = createPhysics3DContact(0, 1);
181
+ const point = createPhysics3DContactPoint();
182
+ point.depth = Number.NaN;
183
+ contact.points.push(point);
184
+
185
+ expect(isPhysics3DContactValid(contact)).toBe(true);
186
+ });
187
+ });
188
+
189
+ describe('isPhysics3DGravityValid', () => {
190
+ it('accepts finite gravity on every axis and rejects any non-finite one', () => {
191
+ expect(isPhysics3DGravityValid(createTestWorld())).toBe(true);
192
+
193
+ for (const axis of ['gravityX', 'gravityY', 'gravityZ'] as const) {
194
+ const world = createTestWorld();
195
+ world[axis] = Number.NaN;
196
+ expect(isPhysics3DGravityValid(world)).toBe(false);
197
+ }
198
+ });
199
+ });
200
+
201
+ describe('isPhysics3DJointStateValid', () => {
202
+ it('ACCEPTS THE INFINITE DEFAULTS every joint is born with', () => {
203
+ // `breakForce`/`breakTorque` default to infinity (never breaks) and a distance joint's `maxLength` to
204
+ // infinity (no far stop). A blanket finiteness walk rejects all three, which invalidates every
205
+ // ordinary joint and makes the world decline to step — silently, because an invalid step is skipped
206
+ // rather than thrown. The symptom is a motor that will not turn and contacts that never fire, with
207
+ // nothing naming the joint. This is the test that catches it.
208
+ const world = createPhysics3DWorld();
209
+ addPhysics3DJoint(world, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 }));
210
+ addPhysics3DJoint(world, createPhysics3DDistanceJoint({ bodyA: 0, bodyB: 1, enableLimit: true }));
211
+
212
+ expect(isPhysics3DJointStateValid(world)).toBe(true);
213
+ });
214
+
215
+ it('still rejects a non-finite field that is NOT a declared bound', () => {
216
+ const world = createPhysics3DWorld();
217
+ const joint = createPhysics3DDistanceJoint({ bodyA: 0, bodyB: 1 });
218
+ addPhysics3DJoint(world, joint);
219
+ joint.length = Number.NaN;
220
+
221
+ expect(isPhysics3DJointStateValid(world)).toBe(false);
222
+ });
223
+
224
+ it('accepts a world with no joints', () => {
225
+ expect(isPhysics3DJointStateValid(createTestWorld())).toBe(true);
226
+ });
227
+
228
+ it('rejects a joint with an empty kind or a non-integer endpoint', () => {
229
+ const kind = createTestWorld();
230
+ addPhysics3DJoint(kind, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 })).kind = '';
231
+ expect(isPhysics3DJointStateValid(kind)).toBe(false);
232
+
233
+ const endpoint = createTestWorld();
234
+ addPhysics3DJoint(endpoint, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 })).bodyB = 1.5;
235
+ expect(isPhysics3DJointStateValid(endpoint)).toBe(false);
236
+ });
237
+
238
+ it('rejects a non-finite field a kind added, not only the common ones', () => {
239
+ const world = createTestWorld();
240
+ const joint = addPhysics3DJoint(world, createPhysics3DBallAndSocketJoint({ bodyA: 0, bodyB: 1 }));
241
+ (joint as unknown as Record<string, number>).acmeSuspensionTravel = Number.NaN;
242
+
243
+ // Walking the object rather than a fixed key list is what makes this cover a user's own joint kind,
244
+ // which is the point of an open registry.
245
+ expect(isPhysics3DJointStateValid(world)).toBe(false);
246
+ });
247
+ });
248
+
249
+ describe('isPhysics3DPositionIterationsValid', () => {
250
+ it('accepts zero and rejects a negative or fractional count', () => {
251
+ const world = createTestWorld();
252
+ world.config.sequentialImpulse.positionIterations = 0;
253
+ expect(isPhysics3DPositionIterationsValid(world.config)).toBe(true);
254
+
255
+ world.config.sequentialImpulse.positionIterations = -1;
256
+ expect(isPhysics3DPositionIterationsValid(world.config)).toBe(false);
257
+
258
+ world.config.sequentialImpulse.positionIterations = 2.5;
259
+ expect(isPhysics3DPositionIterationsValid(world.config)).toBe(false);
260
+ });
261
+ });
262
+
263
+ describe('isPhysics3DSolverConfigValid', () => {
264
+ it('accepts the default tuning', () => {
265
+ expect(isPhysics3DSolverConfigValid(createTestWorld().config)).toBe(true);
266
+ });
267
+
268
+ it('rejects a negative or fractional angular CCD budget', () => {
269
+ const world = createTestWorld();
270
+ world.config.maxCcdRotationSubsteps = -1;
271
+ expect(isPhysics3DSolverConfigValid(world.config)).toBe(false);
272
+
273
+ world.config.maxCcdRotationSubsteps = 1.5;
274
+ expect(isPhysics3DSolverConfigValid(world.config)).toBe(false);
275
+ });
276
+
277
+ it('rejects a position correction outside zero to one', () => {
278
+ const world = createTestWorld();
279
+ world.config.sequentialImpulse.positionCorrection = 1.5;
280
+
281
+ expect(isPhysics3DSolverConfigValid(world.config)).toBe(false);
282
+ });
283
+
284
+ it('rejects negative sleep thresholds and a negative slop', () => {
285
+ const threshold = createTestWorld();
286
+ threshold.config.sleepAngularThreshold = -1;
287
+ expect(isPhysics3DSolverConfigValid(threshold.config)).toBe(false);
288
+
289
+ const slop = createTestWorld();
290
+ slop.config.sequentialImpulse.penetrationSlop = -0.01;
291
+ expect(isPhysics3DSolverConfigValid(slop.config)).toBe(false);
292
+ });
293
+
294
+ it('does not speak for the iteration counts or substeps', () => {
295
+ const world = createTestWorld();
296
+ world.config.substeps = 0;
297
+ world.config.sequentialImpulse.velocityIterations = -4;
298
+
299
+ // Those three are asked separately, because they are the ones a caller is most likely to have tuned by
300
+ // hand and the explanation names each of them.
301
+ expect(isPhysics3DSolverConfigValid(world.config)).toBe(true);
302
+ });
303
+ });
304
+
305
+ describe('isPhysics3DSubstepsValid', () => {
306
+ it('requires at least one whole sub-interval', () => {
307
+ const world = createTestWorld();
308
+ expect(isPhysics3DSubstepsValid(world.config)).toBe(true);
309
+
310
+ for (const substeps of [0, -1, 1.5, Number.NaN]) {
311
+ world.config.substeps = substeps;
312
+ expect(isPhysics3DSubstepsValid(world.config)).toBe(false);
313
+ }
314
+ });
315
+ });
316
+
317
+ describe('isPhysics3DTimestepValid', () => {
318
+ it('requires a finite positive interval', () => {
319
+ expect(isPhysics3DTimestepValid(1 / 60)).toBe(true);
320
+ expect(isPhysics3DTimestepValid(0)).toBe(false);
321
+ expect(isPhysics3DTimestepValid(-1)).toBe(false);
322
+ expect(isPhysics3DTimestepValid(Number.NaN)).toBe(false);
323
+ expect(isPhysics3DTimestepValid(Number.POSITIVE_INFINITY)).toBe(false);
324
+ });
325
+ });
326
+
327
+ describe('isPhysics3DVelocityIterationsValid', () => {
328
+ it('accepts zero and rejects a negative or fractional count', () => {
329
+ const world = createTestWorld();
330
+ world.config.sequentialImpulse.velocityIterations = 0;
331
+ expect(isPhysics3DVelocityIterationsValid(world.config)).toBe(true);
332
+
333
+ world.config.sequentialImpulse.velocityIterations = -1;
334
+ expect(isPhysics3DVelocityIterationsValid(world.config)).toBe(false);
335
+
336
+ world.config.sequentialImpulse.velocityIterations = 0.5;
337
+ expect(isPhysics3DVelocityIterationsValid(world.config)).toBe(false);
338
+ });
339
+ });
340
+
341
+ function createTestWorld(): Physics3DWorld {
342
+ const world = createPhysics3DWorld();
343
+ addPhysics3DBody(world, createRigidBody3D('dynamic'));
344
+ addPhysics3DBody(world, createRigidBody3D('dynamic'));
345
+ return world;
346
+ }