@codexo/exojs-physics 0.15.3 → 0.16.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 (201) hide show
  1. package/README.md +51 -23
  2. package/dist/esm/Aabb.d.ts +6 -10
  3. package/dist/esm/Aabb.d.ts.map +1 -0
  4. package/dist/esm/Aabb.js +9 -5
  5. package/dist/esm/Aabb.js.map +1 -1
  6. package/dist/esm/Collider.d.ts +62 -10
  7. package/dist/esm/Collider.d.ts.map +1 -0
  8. package/dist/esm/Collider.js +263 -157
  9. package/dist/esm/Collider.js.map +1 -1
  10. package/dist/esm/ContactGraph.d.ts +51 -5
  11. package/dist/esm/ContactGraph.d.ts.map +1 -0
  12. package/dist/esm/ContactGraph.js +282 -223
  13. package/dist/esm/ContactGraph.js.map +1 -1
  14. package/dist/esm/ContactModifier.d.ts +85 -0
  15. package/dist/esm/ContactModifier.d.ts.map +1 -0
  16. package/dist/esm/ContactModifier.js +49 -0
  17. package/dist/esm/ContactModifier.js.map +1 -0
  18. package/dist/esm/PhysicsBody.d.ts +56 -22
  19. package/dist/esm/PhysicsBody.d.ts.map +1 -0
  20. package/dist/esm/PhysicsBody.js +461 -443
  21. package/dist/esm/PhysicsBody.js.map +1 -1
  22. package/dist/esm/PhysicsWorld.d.ts +276 -43
  23. package/dist/esm/PhysicsWorld.d.ts.map +1 -0
  24. package/dist/esm/PhysicsWorld.js +890 -626
  25. package/dist/esm/PhysicsWorld.js.map +1 -1
  26. package/dist/esm/TimeStepper.d.ts +3 -2
  27. package/dist/esm/TimeStepper.d.ts.map +1 -0
  28. package/dist/esm/TimeStepper.js +56 -67
  29. package/dist/esm/TimeStepper.js.map +1 -1
  30. package/dist/esm/backend/NativePhysicsBackend.d.ts +8 -4
  31. package/dist/esm/backend/NativePhysicsBackend.d.ts.map +1 -0
  32. package/dist/esm/backend/NativePhysicsBackend.js +48 -41
  33. package/dist/esm/backend/NativePhysicsBackend.js.map +1 -1
  34. package/dist/esm/backend/PhysicsBackend.d.ts +9 -2
  35. package/dist/esm/backend/PhysicsBackend.d.ts.map +1 -0
  36. package/dist/esm/binding/BindingRegistry.d.ts +25 -4
  37. package/dist/esm/binding/BindingRegistry.d.ts.map +1 -0
  38. package/dist/esm/binding/BindingRegistry.js +70 -36
  39. package/dist/esm/binding/BindingRegistry.js.map +1 -1
  40. package/dist/esm/binding/PhysicsBinding.d.ts +22 -5
  41. package/dist/esm/binding/PhysicsBinding.d.ts.map +1 -0
  42. package/dist/esm/binding/PhysicsBinding.js +46 -21
  43. package/dist/esm/binding/PhysicsBinding.js.map +1 -1
  44. package/dist/esm/broadphase/AabbTreeBroadPhase.d.ts +68 -0
  45. package/dist/esm/broadphase/AabbTreeBroadPhase.d.ts.map +1 -0
  46. package/dist/esm/broadphase/AabbTreeBroadPhase.js +149 -0
  47. package/dist/esm/broadphase/AabbTreeBroadPhase.js.map +1 -0
  48. package/dist/esm/broadphase/BroadPhase.d.ts +7 -4
  49. package/dist/esm/broadphase/BroadPhase.d.ts.map +1 -0
  50. package/dist/esm/broadphase/index.d.ts +2 -1
  51. package/dist/esm/broadphase/index.d.ts.map +1 -0
  52. package/dist/esm/collision/CollisionProxy.d.ts +3 -2
  53. package/dist/esm/collision/CollisionProxy.d.ts.map +1 -0
  54. package/dist/esm/collision/Manifold.d.ts +5 -4
  55. package/dist/esm/collision/Manifold.d.ts.map +1 -0
  56. package/dist/esm/collision/Manifold.js +34 -25
  57. package/dist/esm/collision/Manifold.js.map +1 -1
  58. package/dist/esm/collision/chainAdjacency.d.ts +29 -0
  59. package/dist/esm/collision/chainAdjacency.d.ts.map +1 -0
  60. package/dist/esm/collision/chainAdjacency.js +44 -0
  61. package/dist/esm/collision/chainAdjacency.js.map +1 -0
  62. package/dist/esm/collision/dispatch.d.ts +35 -0
  63. package/dist/esm/collision/dispatch.d.ts.map +1 -0
  64. package/dist/esm/collision/dispatch.js +58 -0
  65. package/dist/esm/collision/dispatch.js.map +1 -0
  66. package/dist/esm/collision/index.d.ts +2 -0
  67. package/dist/esm/collision/index.d.ts.map +1 -0
  68. package/dist/esm/collision/narrowphase.d.ts +5 -2
  69. package/dist/esm/collision/narrowphase.d.ts.map +1 -0
  70. package/dist/esm/collision/narrowphase.js +524 -399
  71. package/dist/esm/collision/narrowphase.js.map +1 -1
  72. package/dist/esm/collision/segments.d.ts +19 -0
  73. package/dist/esm/collision/segments.d.ts.map +1 -0
  74. package/dist/esm/collision/segments.js +36 -0
  75. package/dist/esm/collision/segments.js.map +1 -0
  76. package/dist/esm/collision/sweep.d.ts +35 -0
  77. package/dist/esm/collision/sweep.d.ts.map +1 -0
  78. package/dist/esm/collision/sweep.js +437 -0
  79. package/dist/esm/collision/sweep.js.map +1 -0
  80. package/dist/esm/debug/PhysicsDebugDraw.d.ts +23 -4
  81. package/dist/esm/debug/PhysicsDebugDraw.d.ts.map +1 -0
  82. package/dist/esm/debug/PhysicsDebugDraw.js +230 -188
  83. package/dist/esm/debug/PhysicsDebugDraw.js.map +1 -1
  84. package/dist/esm/debug/index.d.ts +1 -0
  85. package/dist/esm/debug/index.d.ts.map +1 -0
  86. package/dist/esm/debug/index.js +3 -2
  87. package/dist/esm/events.d.ts +1 -0
  88. package/dist/esm/events.d.ts.map +1 -0
  89. package/dist/esm/index.d.ts +1 -0
  90. package/dist/esm/index.d.ts.map +1 -0
  91. package/dist/esm/index.js +24 -19
  92. package/dist/esm/joints/DistanceJoint.d.ts +4 -3
  93. package/dist/esm/joints/DistanceJoint.d.ts.map +1 -0
  94. package/dist/esm/joints/DistanceJoint.js +160 -172
  95. package/dist/esm/joints/DistanceJoint.js.map +1 -1
  96. package/dist/esm/joints/Joint.d.ts +5 -4
  97. package/dist/esm/joints/Joint.d.ts.map +1 -0
  98. package/dist/esm/joints/Joint.js +23 -21
  99. package/dist/esm/joints/Joint.js.map +1 -1
  100. package/dist/esm/joints/MouseJoint.d.ts +8 -7
  101. package/dist/esm/joints/MouseJoint.d.ts.map +1 -0
  102. package/dist/esm/joints/MouseJoint.js +132 -133
  103. package/dist/esm/joints/MouseJoint.js.map +1 -1
  104. package/dist/esm/joints/PrismaticJoint.d.ts +5 -4
  105. package/dist/esm/joints/PrismaticJoint.d.ts.map +1 -0
  106. package/dist/esm/joints/PrismaticJoint.js +211 -240
  107. package/dist/esm/joints/PrismaticJoint.js.map +1 -1
  108. package/dist/esm/joints/RevoluteJoint.d.ts +4 -3
  109. package/dist/esm/joints/RevoluteJoint.d.ts.map +1 -0
  110. package/dist/esm/joints/RevoluteJoint.js +191 -213
  111. package/dist/esm/joints/RevoluteJoint.js.map +1 -1
  112. package/dist/esm/joints/WeldJoint.d.ts +3 -2
  113. package/dist/esm/joints/WeldJoint.d.ts.map +1 -0
  114. package/dist/esm/joints/WeldJoint.js +156 -152
  115. package/dist/esm/joints/WeldJoint.js.map +1 -1
  116. package/dist/esm/joints/WheelJoint.d.ts +4 -3
  117. package/dist/esm/joints/WheelJoint.d.ts.map +1 -0
  118. package/dist/esm/joints/WheelJoint.js +228 -255
  119. package/dist/esm/joints/WheelJoint.js.map +1 -1
  120. package/dist/esm/math.d.ts +6 -9
  121. package/dist/esm/math.d.ts.map +1 -0
  122. package/dist/esm/math.js +37 -39
  123. package/dist/esm/math.js.map +1 -1
  124. package/dist/esm/physicsBuildInfo.d.ts +1 -0
  125. package/dist/esm/physicsBuildInfo.d.ts.map +1 -0
  126. package/dist/esm/physicsBuildInfo.js +6 -4
  127. package/dist/esm/physicsBuildInfo.js.map +1 -1
  128. package/dist/esm/public.d.ts +8 -3
  129. package/dist/esm/public.d.ts.map +1 -0
  130. package/dist/esm/query/QueryEngine.d.ts +56 -12
  131. package/dist/esm/query/QueryEngine.d.ts.map +1 -0
  132. package/dist/esm/query/QueryEngine.js +494 -237
  133. package/dist/esm/query/QueryEngine.js.map +1 -1
  134. package/dist/esm/query/SpatialIndex.d.ts +34 -0
  135. package/dist/esm/query/SpatialIndex.d.ts.map +1 -0
  136. package/dist/esm/shapes/AnyShape.d.ts +9 -4
  137. package/dist/esm/shapes/AnyShape.d.ts.map +1 -0
  138. package/dist/esm/shapes/BoxShape.d.ts +2 -1
  139. package/dist/esm/shapes/BoxShape.d.ts.map +1 -0
  140. package/dist/esm/shapes/BoxShape.js +37 -25
  141. package/dist/esm/shapes/BoxShape.js.map +1 -1
  142. package/dist/esm/shapes/CapsuleShape.d.ts +30 -0
  143. package/dist/esm/shapes/CapsuleShape.d.ts.map +1 -0
  144. package/dist/esm/shapes/CapsuleShape.js +88 -0
  145. package/dist/esm/shapes/CapsuleShape.js.map +1 -0
  146. package/dist/esm/shapes/ChainEdgeShape.d.ts +30 -0
  147. package/dist/esm/shapes/ChainEdgeShape.d.ts.map +1 -0
  148. package/dist/esm/shapes/ChainEdgeShape.js +66 -0
  149. package/dist/esm/shapes/ChainEdgeShape.js.map +1 -0
  150. package/dist/esm/shapes/ChainShape.d.ts +52 -0
  151. package/dist/esm/shapes/ChainShape.d.ts.map +1 -0
  152. package/dist/esm/shapes/ChainShape.js +85 -0
  153. package/dist/esm/shapes/ChainShape.js.map +1 -0
  154. package/dist/esm/shapes/CircleShape.d.ts +3 -4
  155. package/dist/esm/shapes/CircleShape.d.ts.map +1 -0
  156. package/dist/esm/shapes/CircleShape.js +27 -26
  157. package/dist/esm/shapes/CircleShape.js.map +1 -1
  158. package/dist/esm/shapes/PolygonShape.d.ts +7 -8
  159. package/dist/esm/shapes/PolygonShape.d.ts.map +1 -0
  160. package/dist/esm/shapes/PolygonShape.js +131 -162
  161. package/dist/esm/shapes/PolygonShape.js.map +1 -1
  162. package/dist/esm/shapes/SegmentShape.d.ts +31 -0
  163. package/dist/esm/shapes/SegmentShape.d.ts.map +1 -0
  164. package/dist/esm/shapes/SegmentShape.js +62 -0
  165. package/dist/esm/shapes/SegmentShape.js.map +1 -0
  166. package/dist/esm/shapes/Shape.d.ts +28 -17
  167. package/dist/esm/shapes/Shape.d.ts.map +1 -0
  168. package/dist/esm/shapes/Shape.js +12 -13
  169. package/dist/esm/shapes/Shape.js.map +1 -1
  170. package/dist/esm/shapes/convexParts.d.ts +21 -0
  171. package/dist/esm/shapes/convexParts.d.ts.map +1 -0
  172. package/dist/esm/shapes/convexParts.js +26 -0
  173. package/dist/esm/shapes/convexParts.js.map +1 -0
  174. package/dist/esm/shapes/decompose.d.ts +24 -0
  175. package/dist/esm/shapes/decompose.d.ts.map +1 -0
  176. package/dist/esm/shapes/decompose.js +272 -0
  177. package/dist/esm/shapes/decompose.js.map +1 -0
  178. package/dist/esm/shapes/index.d.ts +4 -0
  179. package/dist/esm/shapes/index.d.ts.map +1 -0
  180. package/dist/esm/solver/ContactSolver.d.ts +12 -5
  181. package/dist/esm/solver/ContactSolver.d.ts.map +1 -0
  182. package/dist/esm/solver/ContactSolver.js +425 -471
  183. package/dist/esm/solver/ContactSolver.js.map +1 -1
  184. package/dist/esm/solver/tolerances.d.ts +34 -0
  185. package/dist/esm/solver/tolerances.d.ts.map +1 -0
  186. package/dist/esm/solver/tolerances.js +38 -0
  187. package/dist/esm/solver/tolerances.js.map +1 -0
  188. package/dist/esm/sort.d.ts +5 -4
  189. package/dist/esm/sort.d.ts.map +1 -0
  190. package/dist/esm/sort.js +41 -51
  191. package/dist/esm/sort.js.map +1 -1
  192. package/dist/esm/types.d.ts +4 -8
  193. package/dist/esm/types.d.ts.map +1 -0
  194. package/dist/esm/types.js +15 -17
  195. package/dist/esm/types.js.map +1 -1
  196. package/package.json +6 -6
  197. package/dist/esm/broadphase/SweepAndPrune.d.ts +0 -15
  198. package/dist/esm/broadphase/SweepAndPrune.js +0 -77
  199. package/dist/esm/broadphase/SweepAndPrune.js.map +0 -1
  200. package/dist/esm/debug/index.js.map +0 -1
  201. package/dist/esm/index.js.map +0 -1
@@ -1,255 +1,512 @@
1
- import { aabbOverlap, aabbContainsPoint } from '../Aabb.js';
2
- import { testOverlap } from '../collision/narrowphase.js';
3
- import { applyTransform, applyRotation, createTransform } from '../math.js';
4
- import { resolveFilter, shouldCollide } from '../types.js';
1
+ import { aabbContainsPoint, aabbOverlap, createAabb } from "../Aabb.js";
2
+ import { applyRotation, applyTransform, createTransform } from "../math.js";
3
+ import { resolveFilter, shouldCollide } from "../types.js";
4
+ import { authoredCollider } from "../Collider.js";
5
+ import { closestPointOnSegment, pointSegmentDistanceSquared } from "../collision/segments.js";
6
+ import { testOverlap } from "../collision/narrowphase.js";
5
7
 
8
+ //#region src/query/QueryEngine.ts
9
+ /** Reused sink for the closest-point primitives; queries run sequentially. */
10
+ const _pointScratch = {
11
+ x: 0,
12
+ y: 0
13
+ };
6
14
  /**
7
- * Spatial queries over the world's live collider set. The engine holds a
8
- * reference to the world's collider array (kept world-synchronised on every
9
- * body move), so queries always see current placements. Array-returning queries
10
- * follow the three explicit allocation forms: fresh array, caller-owned `out`,
11
- * or an allocation-free callback never a hidden shared buffer.
12
- */
13
- class QueryEngine {
14
- _colliders;
15
- constructor(colliders) {
16
- this._colliders = colliders;
17
- }
18
- /** Colliders containing `point`. Allocates a fresh array. */
19
- queryPoint(point, filter) {
20
- const out = [];
21
- const resolved = filter ? resolveFilter(filter) : null;
22
- for (const collider of this._colliders) {
23
- if (resolved && !shouldCollide(resolved, collider.filter)) {
24
- continue;
25
- }
26
- if (pointInCollider(collider, point.x, point.y)) {
27
- out.push(collider);
28
- }
29
- }
30
- return out;
31
- }
32
- /** Colliders whose AABB overlaps `bounds`. Writes into `out` (cleared first) when given, else a fresh array. */
33
- queryAabb(bounds, filter, out) {
34
- const result = out ?? [];
35
- result.length = 0;
36
- const resolved = filter ? resolveFilter(filter) : null;
37
- for (const collider of this._colliders) {
38
- if (resolved && !shouldCollide(resolved, collider.filter)) {
39
- continue;
40
- }
41
- if (aabbOverlap(collider.aabb, bounds)) {
42
- result.push(collider);
43
- }
44
- }
45
- return result;
46
- }
47
- /** Invoke `callback` for each collider whose AABB overlaps `bounds`. Allocation-free. */
48
- forEachAabbHit(bounds, filter, callback) {
49
- const resolved = filter ? resolveFilter(filter) : null;
50
- for (const collider of this._colliders) {
51
- if (resolved && !shouldCollide(resolved, collider.filter)) {
52
- continue;
53
- }
54
- if (aabbOverlap(collider.aabb, bounds)) {
55
- callback(collider);
56
- }
57
- }
58
- }
59
- /** Nearest collider hit by the ray from `origin` along `direction`, or `null`. */
60
- rayCast(origin, direction, filter, maxDistance = Infinity) {
61
- const length = Math.hypot(direction.x, direction.y);
62
- if (length < 1e-9) {
63
- throw new RangeError('QueryEngine.rayCast: direction must be non-zero.');
64
- }
65
- const dx = direction.x / length;
66
- const dy = direction.y / length;
67
- const resolved = filter ? resolveFilter(filter) : null;
68
- let best = null;
69
- for (const collider of this._colliders) {
70
- if (resolved && !shouldCollide(resolved, collider.filter)) {
71
- continue;
72
- }
73
- const hit = rayCastCollider(collider, origin.x, origin.y, dx, dy, best ? best.distance : maxDistance);
74
- if (hit && (best === null || hit.distance < best.distance)) {
75
- best = hit;
76
- }
77
- }
78
- return best;
79
- }
80
- /** All collider hits along the ray, sorted by distance. Writes into `out` (cleared first) when given. */
81
- rayCastAll(origin, direction, filter, out, maxDistance = Infinity) {
82
- const length = Math.hypot(direction.x, direction.y);
83
- if (length < 1e-9) {
84
- throw new RangeError('QueryEngine.rayCastAll: direction must be non-zero.');
85
- }
86
- const dx = direction.x / length;
87
- const dy = direction.y / length;
88
- const resolved = filter ? resolveFilter(filter) : null;
89
- const result = out ?? [];
90
- result.length = 0;
91
- for (const collider of this._colliders) {
92
- if (resolved && !shouldCollide(resolved, collider.filter)) {
93
- continue;
94
- }
95
- const hit = rayCastCollider(collider, origin.x, origin.y, dx, dy, maxDistance);
96
- if (hit) {
97
- result.push(hit);
98
- }
99
- }
100
- result.sort((a, b) => a.distance - b.distance);
101
- return result;
102
- }
103
- /** Colliders overlapping `shape` placed at `position`/`angle`. Allocates a fresh array. */
104
- overlapShape(shape, position, filter, angle = 0) {
105
- const proxy = makeProxy(shape, position.x, position.y, angle);
106
- const out = [];
107
- const resolved = filter ? resolveFilter(filter) : null;
108
- for (const collider of this._colliders) {
109
- if (resolved && !shouldCollide(resolved, collider.filter)) {
110
- continue;
111
- }
112
- if (testOverlap(proxy, collider)) {
113
- out.push(collider);
114
- }
115
- }
116
- return out;
117
- }
118
- }
15
+ * Spatial queries over the world's live collider set. The engine holds a
16
+ * reference to the world's collider array (kept world-synchronised on every
17
+ * body move), so queries always see current placements. Array-returning queries
18
+ * follow the three explicit allocation forms: fresh array, caller-owned `out`,
19
+ * or an allocation-free callback - never a hidden shared buffer.
20
+ *
21
+ * The engine scans the geometry the broad phase holds, which for a chain
22
+ * collider is its per-edge proxies. Every result is reported as the authored
23
+ * collider, and an overlap query reports a chain once however many of its edges
24
+ * it touches. A ray still reports one hit per edge it crosses, because those are
25
+ * distinct intersections.
26
+ */
27
+ var QueryEngine = class {
28
+ _colliders;
29
+ _spatialIndex;
30
+ _scratchHits = [];
31
+ _scratchHitsForEach = [];
32
+ /** Chains already reported by the running `forEachAabbHit`, so one chain is one callback. */
33
+ _scratchSeenForEach = [];
34
+ _scratchAabb = createAabb();
35
+ constructor(colliders, spatialIndex) {
36
+ this._colliders = colliders;
37
+ this._spatialIndex = spatialIndex;
38
+ }
39
+ /**
40
+ * Colliders to scan for a query over `bounds` - the spatial index's narrowed
41
+ * result if one is wired, else every live collider. `sync` is called first so
42
+ * colliders added/moved since the index's last detection pass (including
43
+ * before the world's very first `step()`) are still found.
44
+ *
45
+ * `buffer` is the scratch array the spatial index writes candidates into
46
+ * (cleared and refilled in place). Callers whose per-candidate loop can
47
+ * re-enter `_candidatesFor` - directly or via caller-supplied callback code -
48
+ * must pass a buffer dedicated to them, so a nested call refilling its own
49
+ * buffer can't truncate/corrupt an outer call's still-live iteration.
50
+ */
51
+ _candidatesFor(bounds, buffer) {
52
+ if (this._spatialIndex === void 0) return this._colliders;
53
+ this._spatialIndex.sync(this._colliders);
54
+ return this._spatialIndex.queryAabb(bounds, buffer);
55
+ }
56
+ /** Colliders containing `point`. Allocates a fresh array. */
57
+ queryPoint(point, filter) {
58
+ const out = [];
59
+ const resolved = filter ? resolveFilter(filter) : null;
60
+ const bounds = this._scratchAabb;
61
+ bounds.minX = point.x;
62
+ bounds.minY = point.y;
63
+ bounds.maxX = point.x;
64
+ bounds.maxY = point.y;
65
+ for (const collider of this._candidatesFor(bounds, this._scratchHits)) {
66
+ if (resolved && !shouldCollide(resolved, collider.filter)) continue;
67
+ if (pointInCollider(collider, point.x, point.y)) out.push(collider);
68
+ }
69
+ return out;
70
+ }
71
+ /** Colliders whose AABB overlaps `bounds`. Writes into `out` (cleared first) when given, else a fresh array. */
72
+ queryAabb(bounds, filter, out) {
73
+ const result = out ?? [];
74
+ result.length = 0;
75
+ const resolved = filter ? resolveFilter(filter) : null;
76
+ for (const collider of this._candidatesFor(bounds, this._scratchHits)) {
77
+ const authored = authoredCollider(collider);
78
+ if (resolved && !shouldCollide(resolved, authored.filter)) continue;
79
+ if (aabbOverlap(collider.aabb, bounds) && !alreadyReported(collider, authored, result)) result.push(authored);
80
+ }
81
+ return result;
82
+ }
83
+ /**
84
+ * Invoke `callback` for each collider whose AABB overlaps `bounds`. Allocation-free.
85
+ *
86
+ * NOT re-entrant on itself: `callback` runs mid-traversal over a scratch
87
+ * buffer dedicated to this method, so it may safely call `queryPoint`,
88
+ * `queryAabb`, or `overlapShape` on this same `PhysicsWorld`/`QueryEngine`
89
+ * (they use a separate buffer and never invoke caller code mid-loop). It
90
+ * must NOT call `forEachAabbHit` again - directly or indirectly - on the
91
+ * same instance: the nested call would refill the same shared buffer this
92
+ * traversal is still iterating, silently truncating/corrupting it. This
93
+ * mirrors `DynamicAabbTree.query()`'s own non-reentrancy contract.
94
+ */
95
+ forEachAabbHit(bounds, filter, callback) {
96
+ const resolved = filter ? resolveFilter(filter) : null;
97
+ const reported = this._scratchSeenForEach;
98
+ reported.length = 0;
99
+ for (const collider of this._candidatesFor(bounds, this._scratchHitsForEach)) {
100
+ const authored = authoredCollider(collider);
101
+ if (resolved && !shouldCollide(resolved, authored.filter)) continue;
102
+ if (!aabbOverlap(collider.aabb, bounds) || alreadyReported(collider, authored, reported)) continue;
103
+ if (collider !== authored) reported.push(authored);
104
+ callback(authored);
105
+ }
106
+ }
107
+ /** Nearest collider hit by the ray from `origin` along `direction`, or `null`. */
108
+ rayCast(origin, direction, filter, maxDistance = Infinity) {
109
+ const length = Math.hypot(direction.x, direction.y);
110
+ if (length < 1e-9) throw new RangeError("QueryEngine.rayCast: direction must be non-zero.");
111
+ const dx = direction.x / length;
112
+ const dy = direction.y / length;
113
+ const resolved = filter ? resolveFilter(filter) : null;
114
+ let best = null;
115
+ const consider = (collider) => {
116
+ const authored = authoredCollider(collider);
117
+ if (resolved && !shouldCollide(resolved, authored.filter)) return;
118
+ const hit = rayCastCollider(collider, origin.x, origin.y, dx, dy, best ? best.distance : maxDistance);
119
+ if (hit && (best === null || hit.distance < best.distance)) {
120
+ hit.collider = authored;
121
+ best = hit;
122
+ }
123
+ };
124
+ if (this._spatialIndex === void 0) for (const collider of this._colliders) consider(collider);
125
+ else {
126
+ this._spatialIndex.sync(this._colliders);
127
+ this._spatialIndex.rayCast(origin.x, origin.y, dx, dy, maxDistance, consider);
128
+ }
129
+ return best;
130
+ }
131
+ /** All collider hits along the ray, sorted by distance. Writes into `out` (cleared first) when given. */
132
+ rayCastAll(origin, direction, filter, out, maxDistance = Infinity) {
133
+ const length = Math.hypot(direction.x, direction.y);
134
+ if (length < 1e-9) throw new RangeError("QueryEngine.rayCastAll: direction must be non-zero.");
135
+ const dx = direction.x / length;
136
+ const dy = direction.y / length;
137
+ const resolved = filter ? resolveFilter(filter) : null;
138
+ const result = out ?? [];
139
+ result.length = 0;
140
+ const consider = (collider) => {
141
+ const authored = authoredCollider(collider);
142
+ if (resolved && !shouldCollide(resolved, authored.filter)) return;
143
+ const hit = rayCastCollider(collider, origin.x, origin.y, dx, dy, maxDistance);
144
+ if (hit) {
145
+ hit.collider = authored;
146
+ result.push(hit);
147
+ }
148
+ };
149
+ if (this._spatialIndex === void 0) for (const collider of this._colliders) consider(collider);
150
+ else {
151
+ this._spatialIndex.sync(this._colliders);
152
+ this._spatialIndex.rayCast(origin.x, origin.y, dx, dy, maxDistance, consider);
153
+ }
154
+ result.sort((a, b) => a.distance - b.distance);
155
+ return result;
156
+ }
157
+ /**
158
+ * Colliders overlapping `shape` placed at `position`/`angle`. Allocates a
159
+ * fresh array.
160
+ *
161
+ * A chain query shape is tested edge by edge, exactly as a chain collider is
162
+ * solved. Two boundaries have no overlap volume, so a chain - like a segment -
163
+ * never reports another chain or segment.
164
+ */
165
+ overlapShape(shape, position, filter, angle = 0) {
166
+ const proxies = queryProxies(shape, position.x, position.y, angle);
167
+ const out = [];
168
+ const resolved = filter ? resolveFilter(filter) : null;
169
+ const bounds = proxiesAabb(shape, proxies, this._scratchAabb);
170
+ for (const collider of this._candidatesFor(bounds, this._scratchHits)) {
171
+ const authored = authoredCollider(collider);
172
+ if (resolved && !shouldCollide(resolved, authored.filter)) continue;
173
+ if (alreadyReported(collider, authored, out)) continue;
174
+ for (const proxy of proxies) if (testOverlap(proxy, collider)) {
175
+ out.push(authored);
176
+ break;
177
+ }
178
+ }
179
+ return out;
180
+ }
181
+ };
119
182
  /** Read a flat vertex/normal buffer in-bounds: callers index within 0..count-1. */
120
183
  /** `true` when world point `(px, py)` lies inside `collider`'s shape. */
121
184
  const pointInCollider = (collider, px, py) => {
122
- if (!aabbContainsPoint(collider.aabb, px, py)) {
123
- return false;
124
- }
125
- if (collider.shape.type === 'circle') {
126
- const c = collider.worldCenter;
127
- const r = collider.shape.radius;
128
- const dx = px - c.x;
129
- const dy = py - c.y;
130
- return dx * dx + dy * dy <= r * r;
131
- }
132
- const verts = collider.worldVertices;
133
- const normals = collider.worldNormals;
134
- const count = collider.shape.count;
135
- for (let i = 0; i < count; i++) {
136
- const nx = normals[i * 2];
137
- const ny = normals[i * 2 + 1];
138
- const vx = verts[i * 2];
139
- const vy = verts[i * 2 + 1];
140
- if (nx * (px - vx) + ny * (py - vy) > 0) {
141
- return false;
142
- }
143
- }
144
- return true;
185
+ if (!aabbContainsPoint(collider.aabb, px, py)) return false;
186
+ if (collider.shape.type === "circle") {
187
+ const c = collider.worldCenter;
188
+ const r = collider.shape.radius;
189
+ const dx = px - c.x;
190
+ const dy = py - c.y;
191
+ return dx * dx + dy * dy <= r * r;
192
+ }
193
+ if (collider.shape.type === "chain") return false;
194
+ if (collider.shape.type === "segment") return false;
195
+ if (collider.shape.type === "capsule") {
196
+ const spine = collider.worldVertices;
197
+ return pointSegmentDistanceSquared(px, py, spine[0], spine[1], spine[2], spine[3], _pointScratch) <= collider.shape.radius * collider.shape.radius;
198
+ }
199
+ const verts = collider.worldVertices;
200
+ const normals = collider.worldNormals;
201
+ const count = collider.shape.count;
202
+ for (let i = 0; i < count; i++) {
203
+ const nx = normals[i * 2];
204
+ const ny = normals[i * 2 + 1];
205
+ const vx = verts[i * 2];
206
+ const vy = verts[i * 2 + 1];
207
+ if (nx * (px - vx) + ny * (py - vy) > 0) return false;
208
+ }
209
+ return true;
145
210
  };
146
211
  /** Cast the (normalised) ray against one collider, returning the entry hit or `null`. */
147
212
  const rayCastCollider = (collider, ox, oy, dx, dy, maxDistance) => {
148
- if (collider.shape.type === 'circle') {
149
- return rayCastCircle(collider, ox, oy, dx, dy, maxDistance);
150
- }
151
- return rayCastPolygon(collider, ox, oy, dx, dy, maxDistance);
213
+ switch (collider.shape.type) {
214
+ case "circle": return rayCastCircle(collider, ox, oy, dx, dy, maxDistance);
215
+ case "capsule": return rayCastCapsule(collider, collider.shape.radius, ox, oy, dx, dy, maxDistance);
216
+ case "segment": return rayCastSegment(collider, ox, oy, dx, dy, maxDistance);
217
+ case "polygon": return rayCastPolygon(collider, ox, oy, dx, dy, maxDistance);
218
+ case "chain": return null;
219
+ }
220
+ };
221
+ /**
222
+ * Ray against a segment: a plain segment/segment crossing. The reported normal is
223
+ * the segment's own side normal, flipped to face the incoming ray - a boundary
224
+ * blocks from both sides, so which of its two normals applies is decided by where
225
+ * the ray comes from.
226
+ */
227
+ const rayCastSegment = (collider, ox, oy, dx, dy, maxDistance) => {
228
+ const ends = collider.worldVertices;
229
+ const ax = ends[0];
230
+ const ay = ends[1];
231
+ const ex = ends[2] - ax;
232
+ const ey = ends[3] - ay;
233
+ const denominator = dx * ey - dy * ex;
234
+ if (Math.abs(denominator) < 1e-12) return null;
235
+ const rx = ax - ox;
236
+ const ry = ay - oy;
237
+ const distance = (rx * ey - ry * ex) / denominator;
238
+ const along = (rx * dy - ry * dx) / denominator;
239
+ if (distance < 0 || distance > maxDistance || along < 0 || along > 1) return null;
240
+ const nx = collider.worldNormals[0];
241
+ const ny = collider.worldNormals[1];
242
+ const facing = nx * dx + ny * dy > 0 ? -1 : 1;
243
+ return {
244
+ collider,
245
+ body: collider.body,
246
+ point: {
247
+ x: ox + dx * distance,
248
+ y: oy + dy * distance
249
+ },
250
+ normal: {
251
+ x: nx * facing,
252
+ y: ny * facing
253
+ },
254
+ distance
255
+ };
256
+ };
257
+ /**
258
+ * Ray against a capsule: the first point along the ray whose distance to the
259
+ * spine equals the radius.
260
+ *
261
+ * Found by scanning for the first sample inside and bisecting the bracket that
262
+ * produced it, rather than by three closed-form feature tests (two caps and the
263
+ * side slab). The distance-to-spine function is continuous and, approaching the
264
+ * capsule from outside, monotone up to the first touch, so the bracket is valid;
265
+ * a fixed 40 bisections put the result far below the engine's contact slop. The
266
+ * cost is a query-path concern only - the solver never runs this.
267
+ */
268
+ const rayCastCapsule = (collider, radius, ox, oy, dx, dy, maxDistance) => {
269
+ const spine = collider.worldVertices;
270
+ const ax = spine[0];
271
+ const ay = spine[1];
272
+ const bx = spine[2];
273
+ const by = spine[3];
274
+ const radiusSquared = radius * radius;
275
+ if (pointSegmentDistanceSquared(ox, oy, ax, ay, bx, by, _pointScratch) <= radiusSquared) return null;
276
+ const box = collider.aabb;
277
+ let near = 0;
278
+ let far = maxDistance;
279
+ for (let axis = 0; axis < 2; axis++) {
280
+ const origin = axis === 0 ? ox : oy;
281
+ const direction = axis === 0 ? dx : dy;
282
+ const lo = axis === 0 ? box.minX : box.minY;
283
+ const hi = axis === 0 ? box.maxX : box.maxY;
284
+ if (Math.abs(direction) < 1e-12) {
285
+ if (origin < lo || origin > hi) return null;
286
+ continue;
287
+ }
288
+ const t0 = (lo - origin) / direction;
289
+ const t1 = (hi - origin) / direction;
290
+ near = Math.max(near, Math.min(t0, t1));
291
+ far = Math.min(far, Math.max(t0, t1));
292
+ }
293
+ if (near > far) return null;
294
+ const steps = 32;
295
+ const stepLength = (far - near) / steps;
296
+ let entered = -1;
297
+ for (let i = 1; i <= steps; i++) {
298
+ const t = near + i * stepLength;
299
+ if (pointSegmentDistanceSquared(ox + dx * t, oy + dy * t, ax, ay, bx, by, _pointScratch) <= radiusSquared) {
300
+ entered = t;
301
+ break;
302
+ }
303
+ }
304
+ if (entered < 0) return null;
305
+ let lo = entered - stepLength;
306
+ let hi = entered;
307
+ for (let i = 0; i < 40; i++) {
308
+ const mid = (lo + hi) / 2;
309
+ if (pointSegmentDistanceSquared(ox + dx * mid, oy + dy * mid, ax, ay, bx, by, _pointScratch) <= radiusSquared) hi = mid;
310
+ else lo = mid;
311
+ }
312
+ const hitX = ox + dx * hi;
313
+ const hitY = oy + dy * hi;
314
+ closestPointOnSegment(hitX, hitY, ax, ay, bx, by, _pointScratch);
315
+ const nx = hitX - _pointScratch.x;
316
+ const ny = hitY - _pointScratch.y;
317
+ const length = Math.hypot(nx, ny) || 1;
318
+ return {
319
+ collider,
320
+ body: collider.body,
321
+ point: {
322
+ x: hitX,
323
+ y: hitY
324
+ },
325
+ normal: {
326
+ x: nx / length,
327
+ y: ny / length
328
+ },
329
+ distance: hi
330
+ };
152
331
  };
153
332
  const rayCastCircle = (collider, ox, oy, dx, dy, maxDistance) => {
154
- const c = collider.worldCenter;
155
- // Dispatched only for circle colliders; the fallback is unreachable.
156
- const r = collider.shape.type === 'circle' ? collider.shape.radius : 0;
157
- const mx = ox - c.x;
158
- const my = oy - c.y;
159
- const b = mx * dx + my * dy;
160
- const cc = mx * mx + my * my - r * r;
161
- // Origin outside and pointing away.
162
- if (cc > 0 && b > 0) {
163
- return null;
164
- }
165
- const disc = b * b - cc;
166
- if (disc < 0) {
167
- return null;
168
- }
169
- let t = -b - Math.sqrt(disc);
170
- if (t < 0) {
171
- t = 0;
172
- }
173
- if (t > maxDistance) {
174
- return null;
175
- }
176
- const px = ox + dx * t;
177
- const py = oy + dy * t;
178
- const nx = (px - c.x) / r;
179
- const ny = (py - c.y) / r;
180
- return { collider, body: collider.body, point: { x: px, y: py }, normal: { x: nx, y: ny }, distance: t };
333
+ const c = collider.worldCenter;
334
+ const r = collider.shape.type === "circle" ? collider.shape.radius : 0;
335
+ const mx = ox - c.x;
336
+ const my = oy - c.y;
337
+ const b = mx * dx + my * dy;
338
+ const cc = mx * mx + my * my - r * r;
339
+ if (cc > 0 && b > 0) return null;
340
+ const disc = b * b - cc;
341
+ if (disc < 0) return null;
342
+ let t = -b - Math.sqrt(disc);
343
+ if (t < 0) t = 0;
344
+ if (t > maxDistance) return null;
345
+ const px = ox + dx * t;
346
+ const py = oy + dy * t;
347
+ const nx = (px - c.x) / r;
348
+ const ny = (py - c.y) / r;
349
+ return {
350
+ collider,
351
+ body: collider.body,
352
+ point: {
353
+ x: px,
354
+ y: py
355
+ },
356
+ normal: {
357
+ x: nx,
358
+ y: ny
359
+ },
360
+ distance: t
361
+ };
181
362
  };
182
363
  const rayCastPolygon = (collider, ox, oy, dx, dy, maxDistance) => {
183
- const verts = collider.worldVertices;
184
- const normals = collider.worldNormals;
185
- // Dispatched only for polygon colliders; the fallback is unreachable.
186
- const count = collider.shape.type === 'polygon' ? collider.shape.count : 0;
187
- let tmin = 0;
188
- let tmax = maxDistance;
189
- let enterNx = 0;
190
- let enterNy = 0;
191
- let entered = false;
192
- for (let i = 0; i < count; i++) {
193
- const nx = normals[i * 2];
194
- const ny = normals[i * 2 + 1];
195
- const vx = verts[i * 2];
196
- const vy = verts[i * 2 + 1];
197
- const numerator = nx * (vx - ox) + ny * (vy - oy);
198
- const denominator = nx * dx + ny * dy;
199
- if (denominator === 0) {
200
- // Parallel to this face: if the origin is outside it, the ray misses.
201
- if (numerator < 0) {
202
- return null;
203
- }
204
- continue;
205
- }
206
- const t = numerator / denominator;
207
- if (denominator < 0) {
208
- // Entering this half-plane.
209
- if (t > tmin) {
210
- tmin = t;
211
- enterNx = nx;
212
- enterNy = ny;
213
- entered = true;
214
- }
215
- }
216
- else if (t < tmax) {
217
- // Leaving this half-plane.
218
- tmax = t;
219
- }
220
- if (tmin > tmax) {
221
- return null;
222
- }
223
- }
224
- if (!entered || tmin < 0 || tmin > maxDistance) {
225
- return null;
226
- }
227
- return {
228
- collider,
229
- body: collider.body,
230
- point: { x: ox + dx * tmin, y: oy + dy * tmin },
231
- normal: { x: enterNx, y: enterNy },
232
- distance: tmin,
233
- };
364
+ const verts = collider.worldVertices;
365
+ const normals = collider.worldNormals;
366
+ const count = collider.shape.type === "polygon" ? collider.shape.count : 0;
367
+ let tmin = 0;
368
+ let tmax = maxDistance;
369
+ let enterNx = 0;
370
+ let enterNy = 0;
371
+ let entered = false;
372
+ for (let i = 0; i < count; i++) {
373
+ const nx = normals[i * 2];
374
+ const ny = normals[i * 2 + 1];
375
+ const vx = verts[i * 2];
376
+ const vy = verts[i * 2 + 1];
377
+ const numerator = nx * (vx - ox) + ny * (vy - oy);
378
+ const denominator = nx * dx + ny * dy;
379
+ if (denominator === 0) {
380
+ if (numerator < 0) return null;
381
+ continue;
382
+ }
383
+ const t = numerator / denominator;
384
+ if (denominator < 0) {
385
+ if (t > tmin) {
386
+ tmin = t;
387
+ enterNx = nx;
388
+ enterNy = ny;
389
+ entered = true;
390
+ }
391
+ } else if (t < tmax) tmax = t;
392
+ if (tmin > tmax) return null;
393
+ }
394
+ if (!entered || tmin < 0 || tmin > maxDistance) return null;
395
+ return {
396
+ collider,
397
+ body: collider.body,
398
+ point: {
399
+ x: ox + dx * tmin,
400
+ y: oy + dy * tmin
401
+ },
402
+ normal: {
403
+ x: enterNx,
404
+ y: enterNy
405
+ },
406
+ distance: tmin
407
+ };
234
408
  };
409
+ /**
410
+ * Throwaway collision proxies for a query shape placed at `(x, y)` with `angle`:
411
+ * one, or one per edge for a chain.
412
+ */
413
+ const queryProxies = (shape, x, y, angle) => {
414
+ if (shape.type !== "chain") return [makeProxy(shape, x, y, angle)];
415
+ return shape.edges.map((edge) => makeProxy(edge, x, y, angle));
416
+ };
417
+ /** World AABB covering every proxy of a query shape. */
418
+ const proxiesAabb = (shape, proxies, out) => {
419
+ if (shape.type !== "chain") return proxyAabb(shape, proxies[0], out);
420
+ let minX = Infinity;
421
+ let minY = Infinity;
422
+ let maxX = -Infinity;
423
+ let maxY = -Infinity;
424
+ for (const proxy of proxies) {
425
+ const edgeBounds = proxyAabb(proxy.shape, proxy, _scratchEdgeAabb);
426
+ minX = edgeBounds.minX < minX ? edgeBounds.minX : minX;
427
+ minY = edgeBounds.minY < minY ? edgeBounds.minY : minY;
428
+ maxX = edgeBounds.maxX > maxX ? edgeBounds.maxX : maxX;
429
+ maxY = edgeBounds.maxY > maxY ? edgeBounds.maxY : maxY;
430
+ }
431
+ out.minX = minX;
432
+ out.minY = minY;
433
+ out.maxX = maxX;
434
+ out.maxY = maxY;
435
+ return out;
436
+ };
437
+ /** Scratch for the per-edge bounds a chain query shape unions. */
438
+ const _scratchEdgeAabb = createAabb();
439
+ /**
440
+ * `true` when `authored` is already in `reported`. Only a chain can be reached
441
+ * twice in one query (once per edge proxy), so nothing else pays for the scan.
442
+ */
443
+ const alreadyReported = (candidate, authored, reported) => candidate !== authored && reported.includes(authored);
235
444
  /** Build a throwaway collision proxy for a shape placed at `(x, y)` with `angle`. */
236
445
  const makeProxy = (shape, x, y, angle) => {
237
- if (shape.type === 'circle') {
238
- return { shape, worldCenter: { x, y }, worldVertices: [], worldNormals: [] };
239
- }
240
- const polygon = shape;
241
- const transform = createTransform(x, y, angle);
242
- const worldVertices = [];
243
- const worldNormals = [];
244
- const out = { x: 0, y: 0 };
245
- for (let i = 0; i < polygon.count; i++) {
246
- applyTransform(transform, polygon.vertices[i * 2], polygon.vertices[i * 2 + 1], out);
247
- worldVertices.push(out.x, out.y);
248
- applyRotation(transform, polygon.normals[i * 2], polygon.normals[i * 2 + 1], out);
249
- worldNormals.push(out.x, out.y);
250
- }
251
- return { shape, worldCenter: { x, y }, worldVertices, worldNormals };
446
+ if (shape.type === "circle") return {
447
+ shape,
448
+ worldCenter: {
449
+ x,
450
+ y
451
+ },
452
+ worldVertices: [],
453
+ worldNormals: []
454
+ };
455
+ const count = shape.type === "polygon" ? shape.count : 2;
456
+ const transform = createTransform(x, y, angle);
457
+ const worldVertices = [];
458
+ const worldNormals = [];
459
+ const out = {
460
+ x: 0,
461
+ y: 0
462
+ };
463
+ for (let i = 0; i < count; i++) {
464
+ applyTransform(transform, shape.vertices[i * 2], shape.vertices[i * 2 + 1], out);
465
+ worldVertices.push(out.x, out.y);
466
+ applyRotation(transform, shape.normals[i * 2], shape.normals[i * 2 + 1], out);
467
+ worldNormals.push(out.x, out.y);
468
+ }
469
+ return {
470
+ shape,
471
+ worldCenter: {
472
+ x,
473
+ y
474
+ },
475
+ worldVertices,
476
+ worldNormals
477
+ };
478
+ };
479
+ /** Compute the world AABB of an already-built collision proxy (reuses its cached vertices/centre - no extra transform work). */
480
+ const proxyAabb = (shape, proxy, out) => {
481
+ if (shape.type === "circle") {
482
+ const r = shape.radius;
483
+ out.minX = proxy.worldCenter.x - r;
484
+ out.minY = proxy.worldCenter.y - r;
485
+ out.maxX = proxy.worldCenter.x + r;
486
+ out.maxY = proxy.worldCenter.y + r;
487
+ return out;
488
+ }
489
+ const verts = proxy.worldVertices;
490
+ let minX = Infinity;
491
+ let minY = Infinity;
492
+ let maxX = -Infinity;
493
+ let maxY = -Infinity;
494
+ for (let i = 0; i < verts.length; i += 2) {
495
+ const x = verts[i];
496
+ const y = verts[i + 1];
497
+ minX = x < minX ? x : minX;
498
+ minY = y < minY ? y : minY;
499
+ maxX = x > maxX ? x : maxX;
500
+ maxY = y > maxY ? y : maxY;
501
+ }
502
+ const radius = shape.type === "capsule" ? shape.radius : 0;
503
+ out.minX = minX - radius;
504
+ out.minY = minY - radius;
505
+ out.maxX = maxX + radius;
506
+ out.maxY = maxY + radius;
507
+ return out;
252
508
  };
253
509
 
510
+ //#endregion
254
511
  export { QueryEngine };
255
- //# sourceMappingURL=QueryEngine.js.map
512
+ //# sourceMappingURL=QueryEngine.js.map