@perplexdotgg/bounce 1.0.0

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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/package.json +32 -0
  4. package/src/builders/ConvexHullBuilder.ts +437 -0
  5. package/src/builders/ConvexHullBuilder2d.ts +344 -0
  6. package/src/builders/ConvexHullBuilder3d.ts +1689 -0
  7. package/src/builders/HeightMapBuilder.ts +414 -0
  8. package/src/builders/TriangleMeshBuilder.ts +92 -0
  9. package/src/collision/CastShapesModule.ts +184 -0
  10. package/src/collision/CollideShapesModule.ts +152 -0
  11. package/src/collision/HeightMapCaster.ts +38 -0
  12. package/src/collision/HeightMapCollider.ts +33 -0
  13. package/src/collision/TriangleCaster.ts +249 -0
  14. package/src/collision/TriangleCollider.ts +308 -0
  15. package/src/collision/TriangleCollider2.ts +379 -0
  16. package/src/collision/activeEdge.ts +146 -0
  17. package/src/collision/cast/cast.ts +139 -0
  18. package/src/collision/cast/castCompoundVsCompound.ts +59 -0
  19. package/src/collision/cast/castCompoundVsConvex.ts +116 -0
  20. package/src/collision/cast/castConvexVsCompound.ts +123 -0
  21. package/src/collision/cast/castConvexVsConvex.ts +213 -0
  22. package/src/collision/cast/castConvexVsHeightMap.ts +73 -0
  23. package/src/collision/cast/castConvexVsTriangleMesh.ts +56 -0
  24. package/src/collision/cast/castRayVsCompound.ts +44 -0
  25. package/src/collision/cast/castRayVsConvex.ts +45 -0
  26. package/src/collision/cast/castRayVsHeightMap.ts +58 -0
  27. package/src/collision/cast/castRayVsTriangleMesh.ts +58 -0
  28. package/src/collision/closestPoints/closestPoints.ts +23 -0
  29. package/src/collision/closestPoints/computeBarycentricCoordinates2d.ts +32 -0
  30. package/src/collision/closestPoints/computeBarycentricCoordinates3d.ts +81 -0
  31. package/src/collision/closestPoints/computeClosestPointOnLine.ts +30 -0
  32. package/src/collision/closestPoints/computeClosestPointOnTetrahedron.ts +96 -0
  33. package/src/collision/closestPoints/computeClosestPointOnTriangle.ts +195 -0
  34. package/src/collision/closestPoints/isOriginOutsideOfPlane.ts +25 -0
  35. package/src/collision/closestPoints/isOriginOutsideOfTrianglePlanes.ts +72 -0
  36. package/src/collision/collide/collide.ts +146 -0
  37. package/src/collision/collide/collideCompoundVsCompound.ts +60 -0
  38. package/src/collision/collide/collideCompoundVsConvex.ts +59 -0
  39. package/src/collision/collide/collideCompoundVsHeightMap.ts +73 -0
  40. package/src/collision/collide/collideCompoundVsTriangleMesh.ts +56 -0
  41. package/src/collision/collide/collideConvexVsCompound.ts +57 -0
  42. package/src/collision/collide/collideConvexVsConvex.ts +225 -0
  43. package/src/collision/collide/collideConvexVsConvexImp.ts +236 -0
  44. package/src/collision/collide/collideConvexVsHeightMap.ts +53 -0
  45. package/src/collision/collide/collideConvexVsTriangleMesh.ts +58 -0
  46. package/src/collision/collide/collideHeightMapVsCompound.ts +69 -0
  47. package/src/collision/collide/collideHeightMapVsConvex.ts +53 -0
  48. package/src/collision/collide/collideSphereVsSphere.ts +81 -0
  49. package/src/collision/collide/collideTriangleMeshVsCompound.ts +58 -0
  50. package/src/collision/collide/collideTriangleMeshVsConvex.ts +58 -0
  51. package/src/collision/epa/EpaConvexHullBuilder.ts +397 -0
  52. package/src/collision/epa/StaticArray.ts +154 -0
  53. package/src/collision/epa/TriangleFactory.ts +32 -0
  54. package/src/collision/epa/arrays.ts +99 -0
  55. package/src/collision/epa/binaryHeap.ts +82 -0
  56. package/src/collision/epa/structs.ts +227 -0
  57. package/src/collision/gjk/GjkModule.ts +864 -0
  58. package/src/collision/gjk/PenetrationDepthModule.ts +493 -0
  59. package/src/collision/gjk/SupportPoints.ts +50 -0
  60. package/src/collision/imp/MinkowskiDifference.ts +36 -0
  61. package/src/collision/imp/computeExploredDistanceLowerUpperBound.ts +40 -0
  62. package/src/collision/imp/finalizeImpResult.ts +69 -0
  63. package/src/collision/imp/findContactImp.ts +196 -0
  64. package/src/collision/imp/imp.ts +28 -0
  65. package/src/collision/imp/incrementalMinimumDistanceExploreDirection.ts +207 -0
  66. package/src/collision/mpr/findPortal.ts +152 -0
  67. package/src/collision/mpr/mpr.ts +29 -0
  68. package/src/collision/mpr/updatePortal.ts +52 -0
  69. package/src/constraints/BaseConstraint.ts +50 -0
  70. package/src/constraints/ConstraintOptions.ts +22 -0
  71. package/src/constraints/ConstraintSolver.ts +119 -0
  72. package/src/constraints/DistanceConstraint.ts +229 -0
  73. package/src/constraints/FixedConstraint.ts +203 -0
  74. package/src/constraints/HingeConstraint.ts +460 -0
  75. package/src/constraints/PointConstraint.ts +108 -0
  76. package/src/constraints/components/AngleComponent.ts +226 -0
  77. package/src/constraints/components/AxisComponent.ts +263 -0
  78. package/src/constraints/components/HingeComponent.ts +215 -0
  79. package/src/constraints/components/Motor.ts +36 -0
  80. package/src/constraints/components/PointConstraintComponent.ts +179 -0
  81. package/src/constraints/components/RotationEulerComponent.ts +139 -0
  82. package/src/constraints/components/Spring.ts +30 -0
  83. package/src/constraints/components/SpringComponent.ts +71 -0
  84. package/src/constraints/types.ts +6 -0
  85. package/src/helpers.ts +147 -0
  86. package/src/index.ts +50 -0
  87. package/src/math/BasicTransform.ts +19 -0
  88. package/src/math/NumberValue.ts +13 -0
  89. package/src/math/isometry.ts +64 -0
  90. package/src/math/mat3.ts +529 -0
  91. package/src/math/mat4.ts +588 -0
  92. package/src/math/quat.ts +193 -0
  93. package/src/math/scalar.ts +81 -0
  94. package/src/math/tensor.ts +17 -0
  95. package/src/math/vec3.ts +589 -0
  96. package/src/math/vec4.ts +10 -0
  97. package/src/physics/Body.ts +581 -0
  98. package/src/physics/CollisionFilter.ts +52 -0
  99. package/src/physics/SleepModule.ts +163 -0
  100. package/src/physics/broadphase/BodyPairsModule.ts +363 -0
  101. package/src/physics/broadphase/BvhModule.ts +237 -0
  102. package/src/physics/broadphase/BvhTree.ts +803 -0
  103. package/src/physics/broadphase/ConstraintPairsModule.ts +385 -0
  104. package/src/physics/broadphase/TriangleMeshBvhTree.ts +379 -0
  105. package/src/physics/manifold/ContactManifold.ts +227 -0
  106. package/src/physics/manifold/ContactManifoldModule.ts +623 -0
  107. package/src/physics/manifold/Face.ts +119 -0
  108. package/src/physics/manifold/ManifoldCache.ts +116 -0
  109. package/src/physics/manifold/clipping/clipPolyVsEdge.ts +131 -0
  110. package/src/physics/manifold/clipping/clipPolyVsPlane.ts +73 -0
  111. package/src/physics/manifold/clipping/clipPolyVsPoly.ts +72 -0
  112. package/src/physics/narrowphase/CollideBodiesModule.ts +755 -0
  113. package/src/physics/solver/ContactConstraintModule.ts +659 -0
  114. package/src/physics/solver/ManifoldConstraint.ts +420 -0
  115. package/src/physics/solver/estimateCollisionResponse.ts +146 -0
  116. package/src/shape/Aabb.ts +400 -0
  117. package/src/shape/Box.ts +231 -0
  118. package/src/shape/Capsule.ts +332 -0
  119. package/src/shape/CompoundShape.ts +288 -0
  120. package/src/shape/Convex.ts +130 -0
  121. package/src/shape/ConvexHull.ts +423 -0
  122. package/src/shape/Cylinder.ts +313 -0
  123. package/src/shape/HeightMap.ts +511 -0
  124. package/src/shape/Line.ts +14 -0
  125. package/src/shape/Plane.ts +116 -0
  126. package/src/shape/Ray.ts +81 -0
  127. package/src/shape/Segment.ts +25 -0
  128. package/src/shape/Shape.ts +77 -0
  129. package/src/shape/Sphere.ts +181 -0
  130. package/src/shape/TransformedShape.ts +51 -0
  131. package/src/shape/Triangle.ts +122 -0
  132. package/src/shape/TriangleMesh.ts +186 -0
  133. package/src/types.ts +1 -0
  134. package/src/world.ts +1335 -0
  135. package/tests/BodyPairsModule.test.ts +71 -0
  136. package/tests/BvhTree.test.ts +406 -0
  137. package/tests/test.md +642 -0
  138. package/tests/vec3.test.ts +12 -0
  139. package/tsconfig.json +20 -0
  140. package/vite.config.js +40 -0
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { Body, BodyPairNode, BodyPairsModule } from "../src/index";
3
+
4
+ describe("BodyPairsModule", () => {
5
+ it("should add body pairs", () => {
6
+ const bodies = [];
7
+ for (let i = 0; i < 5; i++) {
8
+ const body = Body.create({
9
+ position: { x: i, y: 0, z: 0 },
10
+ orientation: { x: 0, y: 0, z: 0 },
11
+ });
12
+ bodies.push(body);
13
+ }
14
+ const pairs = new BodyPairsModule();
15
+ pairs.createPair(bodies[0], bodies[1]); // AB
16
+ pairs.createPair(bodies[1], bodies[2]); // BC
17
+ pairs.createPair(bodies[0], bodies[2]); // AC
18
+ pairs.createPair(bodies[3], bodies[4]); // DE
19
+ pairs.createPair(bodies[0], bodies[3]); // AD
20
+ pairs.createPair(bodies[4], bodies[0]); // AE
21
+ pairs.destroyPair(bodies[0], bodies[3]); // AD
22
+ const collectedPairs: BodyPairNode[] = [];
23
+ collectedPairs.length = 0;
24
+ for (const pair of pairs.iteratePairsOfOne(bodies[0])) {
25
+ collectedPairs.push(pair);
26
+ }
27
+ collectedPairs.reverse();
28
+ expect(collectedPairs.length).toBe(3);
29
+ expect(collectedPairs[0].bodyA! === bodies[0]).toBe(true);
30
+ expect(collectedPairs[0].bodyB! === bodies[1]).toBe(true);
31
+ expect(collectedPairs[1].bodyA! === bodies[0]).toBe(true);
32
+ expect(collectedPairs[1].bodyB! === bodies[2]).toBe(true);
33
+ expect(collectedPairs[2].bodyA! === bodies[4]).toBe(true);
34
+ expect(collectedPairs[2].bodyB! === bodies[0]).toBe(true);
35
+ collectedPairs.length = 0;
36
+ for (const pair of pairs.iteratePairsOfOne(bodies[1])) {
37
+ collectedPairs.push(pair);
38
+ }
39
+ collectedPairs.reverse();
40
+ expect(collectedPairs.length).toBe(2);
41
+ expect(collectedPairs[0].bodyA! === bodies[0]).toBe(true);
42
+ expect(collectedPairs[0].bodyB! === bodies[1]).toBe(true);
43
+ expect(collectedPairs[1].bodyA! === bodies[1]).toBe(true);
44
+ expect(collectedPairs[1].bodyB! === bodies[2]).toBe(true);
45
+ collectedPairs.length = 0;
46
+ for (const pair of pairs.iteratePairs()) {
47
+ collectedPairs.push(pair);
48
+ }
49
+ expect(collectedPairs.length).toBe(5);
50
+ expect(collectedPairs[0].bodyA! === bodies[0]).toBe(true);
51
+ expect(collectedPairs[0].bodyB! === bodies[1]).toBe(true);
52
+ expect(collectedPairs[1].bodyA! === bodies[1]).toBe(true);
53
+ expect(collectedPairs[1].bodyB! === bodies[2]).toBe(true);
54
+ expect(collectedPairs[2].bodyA! === bodies[0]).toBe(true);
55
+ expect(collectedPairs[2].bodyB! === bodies[2]).toBe(true);
56
+ expect(collectedPairs[3].bodyA! === bodies[3]).toBe(true);
57
+ expect(collectedPairs[3].bodyB! === bodies[4]).toBe(true);
58
+ expect(collectedPairs[4].bodyA! === bodies[4]).toBe(true);
59
+ expect(collectedPairs[4].bodyB! === bodies[0]).toBe(true);
60
+ pairs.destroyAllPairsOfOne(bodies[0]);
61
+ collectedPairs.length = 0;
62
+ for (const pair of pairs.iteratePairs()) {
63
+ collectedPairs.push(pair);
64
+ }
65
+ expect(collectedPairs.length).toBe(2);
66
+ expect(collectedPairs[0].bodyA! === bodies[1]);
67
+ expect(collectedPairs[0].bodyB! === bodies[2]);
68
+ expect(collectedPairs[1].bodyA! === bodies[3]);
69
+ expect(collectedPairs[1].bodyB! === bodies[4]);
70
+ });
71
+ });
@@ -0,0 +1,406 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { World, BvhTree, Aabb, Body } from "../src/index";
3
+
4
+ interface Vec3Like {
5
+ x: number;
6
+ y: number;
7
+ z: number;
8
+ }
9
+
10
+ function createTestBody(world: World, x: number, y: number, z: number, radius = 0.5) {
11
+ const shape = world.createSphere({ radius });
12
+ return world.createDynamicBody({
13
+ shape,
14
+ position: { x, y, z },
15
+ orientation: { x: 0, y: 0, z: 0 },
16
+ });
17
+ }
18
+
19
+ function createTestBodyWithExtents(world: World, min: Vec3Like, max: Vec3Like) {
20
+ const shape = world.createSphere({ radius: 1.0 });
21
+ const body = world.createDynamicBody({
22
+ shape,
23
+ position: { x: 0, y: 0, z: 0 },
24
+ orientation: { x: 0, y: 0, z: 0 },
25
+ });
26
+ body.computedBounds.setExtents(min, max);
27
+ return body;
28
+ }
29
+
30
+ function destroyTestBody(world: World, body: Body) {
31
+ world.destroyBody(body);
32
+ }
33
+
34
+ function createSeededRng(seed: number) {
35
+ let state = seed >>> 0;
36
+ return () => {
37
+ state = (1664525 * state + 1013904223) >>> 0;
38
+ return state / 0x100000000;
39
+ };
40
+ }
41
+
42
+ describe("BvhTree", () => {
43
+ it("inserts and intersects one object", () => {
44
+ const world = new World();
45
+ const tree = new BvhTree({ maxDepth: 4, maxObjectsPerLeaf: 2, expansionMargin: 0 });
46
+
47
+ const body = createTestBody(world, 0, 0, 0);
48
+ tree.insert(body);
49
+
50
+ const query = Aabb.create();
51
+ query.setExtents({ x: -1, y: -1, z: -1 }, { x: 1, y: 1, z: 1 });
52
+
53
+ let hitCount = 0;
54
+ tree.intersectAabb(() => {
55
+ hitCount++;
56
+ return false;
57
+ }, query);
58
+
59
+ expect(hitCount).toBe(1);
60
+ });
61
+
62
+ it("removes objects correctly", () => {
63
+ const world = new World();
64
+ const tree = new BvhTree({ maxDepth: 4, maxObjectsPerLeaf: 2, expansionMargin: 0 });
65
+
66
+ const body = createTestBody(world, 0, 0, 0);
67
+ tree.insert(body);
68
+ tree.remove(body);
69
+
70
+ const query = Aabb.create();
71
+ query.setExtents({ x: -1, y: -1, z: -1 }, { x: 1, y: 1, z: 1 });
72
+
73
+ let hitCount = 0;
74
+ tree.intersectAabb(() => {
75
+ hitCount++;
76
+ return false;
77
+ }, query);
78
+
79
+ expect(hitCount).toBe(0);
80
+ });
81
+
82
+ it("updates objects correctly", () => {
83
+ const world = new World();
84
+ const tree = new BvhTree({ maxDepth: 4, maxObjectsPerLeaf: 2, expansionMargin: 0 });
85
+
86
+ const body = createTestBody(world, 0, 0, 0);
87
+ tree.insert(body);
88
+
89
+ body.computedBounds.setExtents({ x: 10, y: 10, z: 10 }, { x: 11, y: 11, z: 11 });
90
+ tree.update(body);
91
+
92
+ const nearQuery = Aabb.create();
93
+ nearQuery.setExtents({ x: -1, y: -1, z: -1 }, { x: 1, y: 1, z: 1 });
94
+
95
+ let nearHits = 0;
96
+ tree.intersectAabb(() => {
97
+ nearHits++;
98
+ return false;
99
+ }, nearQuery);
100
+ expect(nearHits).toBe(0);
101
+
102
+ const farQuery = Aabb.create();
103
+ farQuery.setExtents({ x: 9, y: 9, z: 9 }, { x: 12, y: 12, z: 12 });
104
+
105
+ let farHits = 0;
106
+ tree.intersectAabb(() => {
107
+ farHits++;
108
+ return false;
109
+ }, farQuery);
110
+ expect(farHits).toBe(1);
111
+ });
112
+
113
+ it("inserts a single object into the root", () => {
114
+ const tree = new BvhTree({ maxDepth: 10, maxObjectsPerLeaf: 2, expansionMargin: 0.1 });
115
+
116
+ const object = Body.create(undefined);
117
+ object.computedBounds.setExtents({ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 });
118
+
119
+ tree.insert(object);
120
+
121
+ expect(tree.root).toBeDefined();
122
+ expect(tree.root!.isLeaf()).toBe(true);
123
+ expect(tree.root!.objects).toContain(object);
124
+ expect(object.node).toBe(tree.root);
125
+ });
126
+
127
+ it("inserts two objects into the same leaf", () => {
128
+ const tree = new BvhTree({ maxDepth: 10, maxObjectsPerLeaf: 2, expansionMargin: 0.1 });
129
+
130
+ const a = Body.create(undefined);
131
+ a.computedBounds.setExtents({ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 });
132
+
133
+ const b = Body.create(undefined);
134
+ b.computedBounds.setExtents({ x: 2, y: 2, z: 2 }, { x: 3, y: 3, z: 3 });
135
+
136
+ tree.insert(a);
137
+ tree.insert(b);
138
+
139
+ expect(tree.root).toBeDefined();
140
+ expect(tree.root!.isLeaf()).toBe(true);
141
+ expect(tree.root!.objects).toContain(a);
142
+ expect(tree.root!.objects).toContain(b);
143
+ });
144
+
145
+ it("splits root when inserting third object", () => {
146
+ const tree = new BvhTree({ maxDepth: 10, maxObjectsPerLeaf: 2, expansionMargin: 0.1 });
147
+
148
+ const a = Body.create(undefined);
149
+ a.computedBounds.setExtents({ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 });
150
+ const b = Body.create(undefined);
151
+ b.computedBounds.setExtents({ x: 2, y: 2, z: 2 }, { x: 3, y: 3, z: 3 });
152
+ const c = Body.create(undefined);
153
+ c.computedBounds.setExtents({ x: 4, y: 4, z: 4 }, { x: 5, y: 5, z: 5 });
154
+
155
+ tree.insert(a);
156
+ tree.insert(b);
157
+ tree.insert(c);
158
+
159
+ const root = tree.root!;
160
+ const rootLeft = root.left!;
161
+ const rootRight = root.right!;
162
+ expect(root.isLeaf()).toBe(false);
163
+ expect(root.left).toBeDefined();
164
+ expect(root.right).toBeDefined();
165
+ expect(rootLeft.isLeaf()).toBe(true);
166
+ expect(rootRight.isLeaf()).toBe(true);
167
+
168
+ const all = [...rootLeft.objects, ...rootRight.objects];
169
+ expect(all).toEqual(expect.arrayContaining([a, b, c]));
170
+ });
171
+
172
+ it("respects early exit from intersectAabb", () => {
173
+ const world = new World();
174
+ const tree = new BvhTree({ maxDepth: 4, maxObjectsPerLeaf: 2, expansionMargin: 0 });
175
+
176
+ for (let i = 0; i < 10; i++) {
177
+ const body = createTestBody(world, i * 2, 0, 0);
178
+ tree.insert(body);
179
+ }
180
+
181
+ const query = Aabb.create();
182
+ query.setExtents({ x: 0, y: -1, z: -1 }, { x: 20, y: 1, z: 1 });
183
+
184
+ let called = 0;
185
+ tree.intersectAabb(() => {
186
+ called++;
187
+ return true;
188
+ }, query);
189
+
190
+ expect(called).toBe(1);
191
+ });
192
+
193
+ it("handles multiple inserts and finds all", () => {
194
+ const world = new World();
195
+ const tree = new BvhTree({ maxDepth: 4, maxObjectsPerLeaf: 2, expansionMargin: 0 });
196
+
197
+ const count = 50;
198
+ for (let i = 0; i < count; i++) {
199
+ const body = createTestBody(world, i * 2, 0, 0);
200
+ tree.insert(body);
201
+ }
202
+
203
+ const query = Aabb.create();
204
+ query.setExtents({ x: -1, y: -1, z: -1 }, { x: count * 2, y: 1, z: 1 });
205
+
206
+ let hits = 0;
207
+ tree.intersectAabb(() => {
208
+ hits++;
209
+ return false;
210
+ }, query);
211
+
212
+ expect(hits).toBe(count);
213
+ });
214
+
215
+ it("removes a middle object", () => {
216
+ const tree = new BvhTree({ maxDepth: 10, maxObjectsPerLeaf: 2, expansionMargin: 0 });
217
+
218
+ const objects = [];
219
+ for (let i = 0; i < 6; i++) {
220
+ const obj = Body.create(undefined);
221
+ obj.computedBounds.setExtents({ x: i * 2, y: 0, z: 0 }, { x: i * 2 + 1, y: 1, z: 1 });
222
+ objects.push(obj);
223
+ }
224
+
225
+ for (const obj of objects) {
226
+ tree.insert(obj);
227
+ }
228
+
229
+ // remove a middle object
230
+ tree.remove(objects[2]);
231
+
232
+ const query = Aabb.create();
233
+ query.setExtents({ x: 0, y: 0, z: 0 }, { x: 20, y: 1, z: 1 });
234
+
235
+ let hitCount = 0;
236
+ tree.intersectAabb(() => {
237
+ hitCount++;
238
+ return false;
239
+ }, query);
240
+
241
+ expect(hitCount).toBe(objects.length - 1);
242
+ expect(objects[2].node).toBe(null);
243
+ });
244
+
245
+ it("removes the only object", () => {
246
+ const tree = new BvhTree({ maxDepth: 4, maxObjectsPerLeaf: 1, expansionMargin: 0 });
247
+
248
+ const obj = Body.create(undefined);
249
+ obj.computedBounds.setExtents({ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 });
250
+
251
+ tree.insert(obj);
252
+ tree.remove(obj);
253
+
254
+ expect(tree.root).toBe(null);
255
+ });
256
+
257
+ it("updates an object to move across the tree", () => {
258
+ const tree = new BvhTree({ maxDepth: 10, maxObjectsPerLeaf: 1, expansionMargin: 0 });
259
+
260
+ const objectA = Body.create(undefined);
261
+ objectA.computedBounds.setExtents({ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 });
262
+ objectA.node = null;
263
+
264
+ const objectB = Body.create(undefined);
265
+ objectB.computedBounds.setExtents({ x: 10, y: 0, z: 0 }, { x: 11, y: 1, z: 1 });
266
+ objectB.node = null;
267
+
268
+ tree.insert(objectA);
269
+ tree.insert(objectB);
270
+
271
+ objectA.computedBounds.setExtents({ x: 20, y: 0, z: 0 }, { x: 21, y: 1, z: 1 });
272
+ tree.update(objectA);
273
+
274
+ const query1 = Aabb.create();
275
+ query1.setExtents({ x: 0, y: 0, z: 0 }, { x: 5, y: 1, z: 1 });
276
+
277
+ let nearHits = 0;
278
+ tree.intersectAabb(() => {
279
+ nearHits++;
280
+ return false;
281
+ }, query1);
282
+
283
+ expect(nearHits).toBe(0);
284
+
285
+ const query2 = Aabb.create();
286
+ query2.setExtents({ x: 19, y: 0, z: 0 }, { x: 22, y: 1, z: 1 });
287
+
288
+ let farHits = 0;
289
+ tree.intersectAabb(() => {
290
+ farHits++;
291
+ return false;
292
+ }, query2);
293
+
294
+ expect(farHits).toBe(1);
295
+ expect(objectA.node).not.toBe(null);
296
+ });
297
+
298
+ it("updates object with no bounding box change", () => {
299
+ const tree = new BvhTree({ maxDepth: 4, maxObjectsPerLeaf: 2, expansionMargin: 0 });
300
+
301
+ const object = Body.create(undefined);
302
+ object.computedBounds.setExtents({ x: 5, y: 5, z: 5 }, { x: 6, y: 6, z: 6 });
303
+ object.node = null;
304
+
305
+ tree.insert(object);
306
+ tree.update(object);
307
+
308
+ const query = Aabb.create();
309
+ query.setExtents({ x: 4, y: 4, z: 4 }, { x: 7, y: 7, z: 7 });
310
+
311
+ let hits = 0;
312
+ tree.intersectAabb(() => {
313
+ hits++;
314
+ return false;
315
+ }, query);
316
+
317
+ expect(hits).toBe(1);
318
+ });
319
+
320
+ it("does not refit if update is within expansion margin", () => {
321
+ const expansionMargin = 1.0;
322
+ const tree = new BvhTree({ maxDepth: 4, maxObjectsPerLeaf: 2, expansionMargin });
323
+
324
+ const object = Body.create(undefined);
325
+ object.computedBounds.setExtents({ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 });
326
+ object.node = null;
327
+
328
+ tree.insert(object);
329
+
330
+ // move the object slightly, within the margin
331
+ object.computedBounds.setExtents({ x: 0.2, y: 0.2, z: 0.2 }, { x: 1.2, y: 1.2, z: 1.2 });
332
+
333
+ const didRefit = tree.update(object);
334
+ expect(didRefit).toBe(false);
335
+
336
+ const query = Aabb.create();
337
+ query.setExtents({ x: 0, y: 0, z: 0 }, { x: 2, y: 2, z: 2 });
338
+
339
+ let hits = 0;
340
+ tree.intersectAabb(() => {
341
+ hits++;
342
+ return false;
343
+ }, query);
344
+
345
+ expect(hits).toBe(1);
346
+ });
347
+
348
+ it("handles many dynamic inserts, updates, and removals", () => {
349
+ const tree = new BvhTree({ maxDepth: 8, maxObjectsPerLeaf: 4, expansionMargin: 0 });
350
+ const rng = createSeededRng(42);
351
+
352
+ const objects = [];
353
+ const objectsInTheTree = new Set<Body>();
354
+
355
+ const world = new World();
356
+
357
+ const numObjects = 100;
358
+ const numToMove = Math.floor(numObjects / 2);
359
+ const numToRemove = Math.floor(numObjects / 2);
360
+
361
+ for (let i = 0; i < numObjects; i++) {
362
+ const min = { x: i * 2, y: 0, z: 0 };
363
+ const max = { x: i * 2 + 1, y: 1, z: 1 };
364
+ const obj = createTestBodyWithExtents(world, min, max);
365
+ objects.push(obj);
366
+ objectsInTheTree.add(obj);
367
+ tree.insert(obj);
368
+ }
369
+
370
+ for (let i = 0; i < numToMove; i++) {
371
+ const index = Math.floor(rng() * objects.length);
372
+ const obj = objects[index];
373
+
374
+ if (!obj.node) continue;
375
+
376
+ const offset = rng() * numObjects;
377
+ obj.computedBounds.setExtents({ x: offset, y: 0, z: 0 }, { x: offset + 1, y: 1, z: 1 });
378
+ tree.update(obj);
379
+ }
380
+
381
+ for (let i = 0; i < numToRemove; i++) {
382
+ const index = Math.floor(rng() * objects.length);
383
+ const obj = objects[index];
384
+ if (obj.node) {
385
+ tree.remove(obj);
386
+ objectsInTheTree.delete(obj);
387
+ expect(obj.node).toBe(null);
388
+ }
389
+ }
390
+
391
+ const query = Aabb.create();
392
+ query.setExtents({ x: -10000, y: -10000, z: -10000 }, { x: +10000, y: +10000, z: +10000 });
393
+
394
+ const actual: Array<Body> = [];
395
+ const expected = [...objectsInTheTree];
396
+ tree.intersectAabb(obj => {
397
+ actual.push(obj);
398
+ return false;
399
+ }, query);
400
+
401
+ expect(actual.length).toBe(expected.length);
402
+ const actualSet = new Set(actual);
403
+ const expectedSet = new Set(expected);
404
+ expect(actualSet).toEqual(expectedSet);
405
+ });
406
+ });