@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sweep.js","names":[],"sources":["../../../src/collision/sweep.ts"],"sourcesContent":["import type { PointLike } from '@codexo/exojs';\n\nimport type { ShapeType } from '../shapes/Shape';\nimport { chainEdgeAdmits } from './chainAdjacency';\nimport type { CollisionProxy } from './CollisionProxy';\nimport type { PairTable } from './dispatch';\nimport { orderedEntry, orderedTable } from './dispatch';\nimport { pointSegmentDistanceSquared } from './segments';\n\n/**\n * First time of impact of a translation-only shape cast, written by\n * {@link sweepProxies} (allocation-free: the caller owns the struct).\n */\nexport interface SweepHit {\n /** Fraction of the motion at first contact, in `(0, 1]`. */\n t: number;\n /** Unit surface normal on the target at the hit, pointing back toward the moving shape (opposing the motion). */\n normalX: number;\n /** See {@link SweepHit.normalX}. */\n normalY: number;\n}\n\nconst eps = 1e-9;\n\n// Module-local scratch shared by the casts below. Like the narrow-phase clip\n// scratch, the sweep is single-threaded and non-reentrant - the world's CCD pass\n// calls `sweepProxies` strictly sequentially - so module-global scratch is safe.\nconst _pointScratch: PointLike = { x: 0, y: 0 };\n\n/**\n * Translation-only shape cast of `moving` against a static `target`: `moving`\n * is given at its **end-of-motion** pose (its cached world geometry) and is\n * swept from `pose − (dx, dy)` to `pose`. Rotation over the motion is not swept\n * - the moving shape keeps its end orientation for the whole cast, matching the\n * bullet CCD model (the step's rotation is applied before the sweep).\n *\n * Returns `true` with the first time of impact and the target-surface normal\n * written into `out`. Pairs already overlapping (or exactly touching) at the\n * start of the motion return `false` - they cannot tunnel within this motion\n * and the discrete solver owns them. Allocation-free.\n *\n * Boundary geometry (a segment, a chain edge) is a target, never the moving\n * operand: a chain edge blocks a bullet under the same one-sided adjacency rule\n * the discrete narrow phase applies, while a fast segment or chain is not swept\n * at all.\n */\nexport const sweepProxies = (moving: CollisionProxy, dx: number, dy: number, target: CollisionProxy, out: SweepHit): boolean => {\n const cast = orderedEntry(sweepTable, moving.shape.type, target.shape.type);\n\n // Unlike the narrow phase, the sweep distinguishes its operands, so this table\n // is filled for both orders. A missing entry reports no impact - the pair is\n // not swept at all rather than swept approximately.\n if (cast === undefined) {\n return false;\n }\n\n if (!cast(moving, dx, dy, target, out)) {\n return false;\n }\n\n // `out` holds the outward normal on the target, which is the direction a chain\n // edge has to admit for the hit to be its own.\n return chainEdgeAdmits(target, out.normalX, out.normalY);\n};\n\n/** `true` when {@link sweepProxies} has an implementation for this ordered pair. */\nexport const canSweep = (moving: ShapeType, target: ShapeType): boolean => orderedEntry(sweepTable, moving, target) !== undefined;\n\n/**\n * Vertex count of a proxy's **core**: the geometry left once its radius is taken\n * off. A circle is a point (no core ring), a capsule and a segment are both a\n * two-vertex ring - with and without a radius - and a polygon is its own ring.\n */\nconst coreCount = (proxy: CollisionProxy): number => {\n switch (proxy.shape.type) {\n case 'polygon':\n return proxy.shape.count;\n case 'capsule':\n case 'segment':\n return 2;\n case 'circle':\n case 'chain':\n // A chain is never an operand here - it is swept through its edge proxies.\n return 0;\n }\n};\n\nconst radiusOf = (proxy: CollisionProxy): number => (proxy.shape.type === 'circle' || proxy.shape.type === 'capsule' ? proxy.shape.radius : 0);\n\n/** Moving circle vs static circle: a ray against the Minkowski-summed circle. */\nconst sweepCircleCircle = (moving: CollisionProxy, dx: number, dy: number, target: CollisionProxy, out: SweepHit): boolean => {\n const rsum = radiusOf(moving) + radiusOf(target);\n const cx = target.worldCenter.x;\n const cy = target.worldCenter.y;\n // Start-of-motion centre.\n const ox = moving.worldCenter.x - dx;\n const oy = moving.worldCenter.y - dy;\n const mx = ox - cx;\n const my = oy - cy;\n\n // Already overlapping/touching at the start: the discrete solver owns it.\n if (mx * mx + my * my <= rsum * rsum) {\n return false;\n }\n\n const a = dx * dx + dy * dy;\n const b = mx * dx + my * dy;\n const c = mx * mx + my * my - rsum * rsum;\n\n if (b >= 0) {\n // Moving away (or tangential) - no entry within the motion.\n return false;\n }\n\n const disc = b * b - a * c;\n\n if (disc < 0) {\n return false;\n }\n\n const t = (-b - Math.sqrt(disc)) / a;\n\n if (t < 0 || t > 1) {\n return false;\n }\n\n out.t = t;\n out.normalX = (ox + dx * t - cx) / rsum;\n out.normalY = (oy + dy * t - cy) / rsum;\n\n return true;\n};\n\n/**\n * Moving circle vs a static core ring (polygon, capsule or segment): a ray\n * against that ring inflated by both radii - offset faces plus vertex arcs, the\n * exact swept geometry.\n */\nconst sweepCircleRing = (moving: CollisionProxy, dx: number, dy: number, target: CollisionProxy, out: SweepHit): boolean => {\n const rsum = radiusOf(moving) + radiusOf(target);\n const ox = moving.worldCenter.x - dx;\n const oy = moving.worldCenter.y - dy;\n const verts = target.worldVertices;\n const normals = target.worldNormals;\n const count = coreCount(target);\n\n // Already overlapping/touching at the start: the discrete solver owns it.\n if (pointRingDistanceSquared(ox, oy, verts, normals, count) <= rsum * rsum) {\n return false;\n }\n\n return castPointAtRing(ox, oy, dx, dy, rsum, verts, normals, count, out);\n};\n\n/**\n * Moving core ring (polygon or capsule) vs static circle, solved in the moving\n * shape's frame: the circle sweeps backward (`−d`) from a start displaced by\n * `+d`, against the ring's end-pose geometry - the same relative motion, so the\n * same time of impact.\n */\nconst sweepRingCircle = (moving: CollisionProxy, dx: number, dy: number, target: CollisionProxy, out: SweepHit): boolean => {\n const rsum = radiusOf(moving) + radiusOf(target);\n const ox = target.worldCenter.x + dx;\n const oy = target.worldCenter.y + dy;\n const verts = moving.worldVertices;\n const normals = moving.worldNormals;\n const count = coreCount(moving);\n\n // Already overlapping/touching at the start: the discrete solver owns it.\n if (pointRingDistanceSquared(ox, oy, verts, normals, count) <= rsum * rsum) {\n return false;\n }\n\n if (!castPointAtRing(ox, oy, -dx, -dy, rsum, verts, normals, count, out)) {\n return false;\n }\n\n // The cast reports the normal on the moving shape toward the circle; the\n // caller wants the obstacle-surface normal, so flip it.\n out.normalX = -out.normalX;\n out.normalY = -out.normalY;\n\n return true;\n};\n\n/**\n * Two core rings of which at least one carries a radius (any pair involving a\n * capsule): built as one configuration-space ring and cast as a point, which is\n * exact where a radius-inflated SAT would square off the rounded corners.\n */\nconst sweepRoundedRings = (moving: CollisionProxy, dx: number, dy: number, target: CollisionProxy, out: SweepHit): boolean => {\n const rsum = radiusOf(moving) + radiusOf(target);\n\n if (!buildConfigurationRing(moving, dx, dy, target)) {\n return false;\n }\n\n const verts = _configurationRing.vertices;\n const normals = _configurationRing.normals;\n const count = _configurationRing.count;\n\n // Already overlapping/touching at the start: the discrete solver owns it.\n if (pointRingDistanceSquared(0, 0, verts, normals, count) <= rsum * rsum) {\n return false;\n }\n\n return castPointAtRing(0, 0, dx, dy, rsum, verts, normals, count, out);\n};\n\n/**\n * Cast the point `(ox, oy)` along `(dx, dy)` (t ∈ [0, 1]) against `count`-vertex\n * convex ring inflated by `r`: offset faces plus vertex arcs - the exact swept\n * geometry. Writes the earliest hit into `out`; the normal points from the ring\n * surface toward the point.\n */\nconst castPointAtRing = (\n ox: number,\n oy: number,\n dx: number,\n dy: number,\n r: number,\n verts: readonly number[],\n normals: readonly number[],\n count: number,\n out: SweepHit,\n): boolean => {\n // Both passes only improve `out` when they beat the current `out.t`.\n out.t = Infinity;\n castAtOffsetFaces(ox, oy, dx, dy, r, verts, normals, count, out);\n castAtVertexArcs(ox, oy, dx, dy, r, verts, count, out);\n\n return out.t !== Infinity;\n};\n\n/** Offset-face pass of {@link castPointAtRing}: the face plane pushed out by `r`, hits valid within the edge span. */\nconst castAtOffsetFaces = (\n ox: number,\n oy: number,\n dx: number,\n dy: number,\n r: number,\n verts: readonly number[],\n normals: readonly number[],\n count: number,\n out: SweepHit,\n): void => {\n for (let i = 0; i < count; i++) {\n // Loop indices are provably in-bounds (0..count-1); the `!` is zero-cost.\n const nx = normals[i * 2]!;\n const ny = normals[i * 2 + 1]!;\n const denom = nx * dx + ny * dy;\n\n if (denom >= 0) {\n continue; // Not entering through this face.\n }\n\n const v1x = verts[i * 2]!;\n const v1y = verts[i * 2 + 1]!;\n const t = (nx * (v1x - ox) + ny * (v1y - oy) + r) / denom;\n\n if (t < 0 || t > 1 || t >= out.t) {\n continue;\n }\n\n const j = (i + 1) % count;\n const v2x = verts[j * 2]!;\n const v2y = verts[j * 2 + 1]!;\n const ex = v2x - v1x;\n const ey = v2y - v1y;\n const px = ox + dx * t;\n const py = oy + dy * t;\n const u = (px - v1x) * ex + (py - v1y) * ey;\n\n if (u < 0 || u > ex * ex + ey * ey) {\n continue; // Outside the edge span — the vertex arcs own the corners.\n }\n\n out.t = t;\n out.normalX = nx;\n out.normalY = ny;\n }\n};\n\n/** Vertex-arc pass of {@link castPointAtRing}: a ray against the circle of radius `r` around each vertex. */\nconst castAtVertexArcs = (ox: number, oy: number, dx: number, dy: number, r: number, verts: readonly number[], count: number, out: SweepHit): void => {\n if (r <= 0) {\n return; // A radius-free ring has no arcs; its faces meet at the vertices.\n }\n\n const a = dx * dx + dy * dy;\n\n for (let i = 0; i < count; i++) {\n const vx = verts[i * 2]!;\n const vy = verts[i * 2 + 1]!;\n const mx = ox - vx;\n const my = oy - vy;\n const b = mx * dx + my * dy;\n\n if (b >= 0) {\n continue; // Moving away from this vertex.\n }\n\n const c = mx * mx + my * my - r * r;\n\n if (c < 0) {\n continue; // Start inside the arc — covered by the start-overlap pre-check.\n }\n\n const disc = b * b - a * c;\n\n if (disc < 0) {\n continue;\n }\n\n const t = (-b - Math.sqrt(disc)) / a;\n\n if (t < 0 || t > 1 || t >= out.t) {\n continue;\n }\n\n out.t = t;\n out.normalX = (ox + dx * t - vx) / r;\n out.normalY = (oy + dy * t - vy) / r;\n }\n};\n\n/**\n * Squared distance from `(px, py)` to a convex core ring, `0` inside it. A\n * two-vertex ring has no interior, so it is always the distance to the segment.\n */\nconst pointRingDistanceSquared = (px: number, py: number, verts: readonly number[], normals: readonly number[], count: number): number => {\n if (count >= 3) {\n let inside = true;\n\n for (let i = 0; i < count && inside; i++) {\n inside = normals[i * 2]! * (px - verts[i * 2]!) + normals[i * 2 + 1]! * (py - verts[i * 2 + 1]!) <= 0;\n }\n\n if (inside) {\n return 0;\n }\n }\n\n let best = Infinity;\n\n for (let i = 0; i < count; i++) {\n const j = (i + 1) % count;\n const distance = pointSegmentDistanceSquared(px, py, verts[i * 2]!, verts[i * 2 + 1]!, verts[j * 2]!, verts[j * 2 + 1]!, _pointScratch);\n\n best = distance < best ? distance : best;\n }\n\n return best;\n};\n\n/** A convex ring in world space: the only geometry the casts above work on. */\ninterface Ring {\n vertices: number[];\n normals: number[];\n count: number;\n}\n\n// Scratch for the configuration-space ring and its hull pass. The arrays grow\n// to the largest pair seen and are then written in place, so a steady-state\n// bullet against known geometry allocates nothing.\nconst _configurationRing: Ring = { vertices: [], normals: [], count: 0 };\nconst _sumPoints: number[] = [];\nconst _sumOrder: number[] = [];\nconst _hull: number[] = [];\n\n/**\n * Build the pair's configuration-space obstacle into {@link _configurationRing}:\n * the Minkowski sum of the target's core and the negated start-pose core of the\n * moving shape. A translational cast of the pair is then a cast of the origin\n * point against that single ring, inflated by the two radii - which is why every\n * rounded pair reduces to {@link castPointAtRing} instead of its own algorithm.\n */\nconst buildConfigurationRing = (moving: CollisionProxy, dx: number, dy: number, target: CollisionProxy): boolean => {\n const movingCount = coreCount(moving);\n const targetCount = coreCount(target);\n\n if (movingCount < 2 || targetCount < 2) {\n return false;\n }\n\n const movingVerts = moving.worldVertices;\n const targetVerts = target.worldVertices;\n const total = movingCount * targetCount;\n\n grow(_sumPoints, total * 2);\n grow(_sumOrder, total);\n\n for (let j = 0, k = 0; j < targetCount; j++) {\n for (let i = 0; i < movingCount; i++, k++) {\n // The moving core at the start of the motion is its end pose minus (dx, dy).\n _sumPoints[k * 2] = targetVerts[j * 2]! - (movingVerts[i * 2]! - dx);\n _sumPoints[k * 2 + 1] = targetVerts[j * 2 + 1]! - (movingVerts[i * 2 + 1]! - dy);\n _sumOrder[k] = k;\n }\n }\n\n const count = convexHull(total);\n\n if (count < 2) {\n return false;\n }\n\n grow(_configurationRing.vertices, count * 2);\n grow(_configurationRing.normals, count * 2);\n _configurationRing.count = count;\n\n const verts = _configurationRing.vertices;\n const normals = _configurationRing.normals;\n\n for (let i = 0; i < count; i++) {\n const point = _hull[i]!;\n\n verts[i * 2] = _sumPoints[point * 2]!;\n verts[i * 2 + 1] = _sumPoints[point * 2 + 1]!;\n }\n\n // Same convention as every authored ring: the outward normal of the\n // counter-clockwise edge i → i+1 is (eY, -eX). A two-vertex ring gets the two\n // opposite normals out of the same loop.\n for (let i = 0; i < count; i++) {\n const j = (i + 1) % count;\n const ex = verts[j * 2]! - verts[i * 2]!;\n const ey = verts[j * 2 + 1]! - verts[i * 2 + 1]!;\n const length = Math.sqrt(ex * ex + ey * ey) || 1;\n\n normals[i * 2] = ey / length;\n normals[i * 2 + 1] = -ex / length;\n }\n\n return true;\n};\n\n/**\n * Counter-clockwise convex hull of the first `total` points in\n * {@link _sumPoints} (Andrew's monotone chain), written as point indices into\n * {@link _hull}; returns the hull size. Collinear points are dropped, so a\n * degenerate sum - two parallel spines - comes out as a two-vertex ring rather\n * than a zero-area polygon.\n */\nconst convexHull = (total: number): number => {\n // Insertion sort by (x, y): the point sets here are a handful of vertices, and\n // sorting in place over the index scratch keeps the pass allocation-free.\n for (let i = 1; i < total; i++) {\n const value = _sumOrder[i]!;\n let j = i - 1;\n\n while (j >= 0 && comparePoints(_sumOrder[j]!, value) > 0) {\n _sumOrder[j + 1] = _sumOrder[j]!;\n j--;\n }\n\n _sumOrder[j + 1] = value;\n }\n\n grow(_hull, total * 2 + 1);\n\n let size = 0;\n\n for (let i = 0; i < total; i++) {\n size = pushHullPoint(_sumOrder[i]!, size, 2);\n }\n\n // The upper chain may not eat into the lower one, so it keeps one more point\n // than the lower chain left behind.\n const upperFloor = size + 1;\n\n for (let i = total - 2; i >= 0; i--) {\n size = pushHullPoint(_sumOrder[i]!, size, upperFloor);\n }\n\n // The chain closes on its starting point, which is already the first entry.\n return size > 1 ? size - 1 : size;\n};\n\n/** One monotone-chain step: drop hull points that a left turn onto `point` makes interior. */\nconst pushHullPoint = (point: number, size: number, floor: number): number => {\n while (size >= floor && cross(_hull[size - 2]!, _hull[size - 1]!, point) <= 0) {\n size--;\n }\n\n _hull[size] = point;\n\n return size + 1;\n};\n\nconst comparePoints = (a: number, b: number): number => {\n const ax = _sumPoints[a * 2]!;\n const bx = _sumPoints[b * 2]!;\n\n if (ax !== bx) {\n return ax < bx ? -1 : 1;\n }\n\n const ay = _sumPoints[a * 2 + 1]!;\n const by = _sumPoints[b * 2 + 1]!;\n\n if (ay === by) {\n return 0;\n }\n\n return ay < by ? -1 : 1;\n};\n\n/** Cross product of `o → a` and `o → b`; positive for a counter-clockwise turn. */\nconst cross = (o: number, a: number, b: number): number => {\n const ox = _sumPoints[o * 2]!;\n const oy = _sumPoints[o * 2 + 1]!;\n\n return (_sumPoints[a * 2]! - ox) * (_sumPoints[b * 2 + 1]! - oy) - (_sumPoints[a * 2 + 1]! - oy) * (_sumPoints[b * 2]! - ox);\n};\n\n/** Extend a scratch array to `length`, keeping it a plain packed number array. */\nconst grow = (array: number[], length: number): void => {\n while (array.length < length) {\n array.push(0);\n }\n};\n\n// Swept-SAT accumulator shared by sweepRings's two axis passes.\n// Module-local scratch under the same single-threaded/non-reentrant contract\n// as the rest of this file.\ninterface SweptSatState {\n tEnter: number;\n tLeave: number;\n normalX: number;\n normalY: number;\n}\n\nconst _satState: SweptSatState = { tEnter: 0, tLeave: 0, normalX: 0, normalY: 0 };\n\n/**\n * Two radius-free core rings (polygon or segment) via swept SAT: for every\n * candidate axis (both rings' face normals) intersect the moving projection\n * interval with the static one over t ∈ [0, 1]; the latest entry over all axes\n * is the time of impact, and its axis (oriented against the motion) the normal.\n * Exact for linear motion of convex shapes.\n */\nconst sweepRings = (moving: CollisionProxy, dx: number, dy: number, target: CollisionProxy, out: SweepHit): boolean => {\n const state = _satState;\n\n state.tEnter = -Infinity;\n state.tLeave = Infinity;\n\n // Two passes: axes from the target, then from the moving ring. Either pass may\n // prove the pair never meets within the motion.\n if (!sweptSatAxes(target, moving, dx, dy, target, state) || !sweptSatAxes(moving, moving, dx, dy, target, state)) {\n return false;\n }\n\n // tEnter ≤ 0 means every axis already overlapped at the start of the motion\n // (i.e. the shapes were overlapping/touching): the discrete solver owns it.\n if (state.tEnter <= 0 || state.tEnter > 1) {\n return false;\n }\n\n out.t = state.tEnter;\n out.normalX = state.normalX;\n out.normalY = state.normalY;\n\n return true;\n};\n\n/**\n * One swept-SAT axis pass over `axisOwner`'s face normals, tightening the\n * `state` entry/exit window of `moving` (translated by `(dx, dy)`, given at its\n * end pose) against the static `target`. Returns `false` as soon as an axis\n * proves the shapes never meet within the motion.\n */\nconst sweptSatAxes = (axisOwner: CollisionProxy, moving: CollisionProxy, dx: number, dy: number, target: CollisionProxy, state: SweptSatState): boolean => {\n const axes = axisOwner.worldNormals;\n const axisCount = coreCount(axisOwner);\n const mv = moving.worldVertices;\n const mc = coreCount(moving);\n const tv = target.worldVertices;\n const tc = coreCount(target);\n\n for (let i = 0; i < axisCount; i++) {\n // Loop indices are provably in-bounds; the `!` is zero-cost.\n const nx = axes[i * 2]!;\n const ny = axes[i * 2 + 1]!;\n const dn = nx * dx + ny * dy;\n\n // Project the moving ring (end pose), then shift to its start pose.\n let minA = Infinity;\n let maxA = -Infinity;\n\n for (let k = 0; k < mc; k++) {\n const p = nx * mv[k * 2]! + ny * mv[k * 2 + 1]!;\n\n minA = p < minA ? p : minA;\n maxA = p > maxA ? p : maxA;\n }\n\n minA -= dn;\n maxA -= dn;\n\n let minB = Infinity;\n let maxB = -Infinity;\n\n for (let k = 0; k < tc; k++) {\n const p = nx * tv[k * 2]! + ny * tv[k * 2 + 1]!;\n\n minB = p < minB ? p : minB;\n maxB = p > maxB ? p : maxB;\n }\n\n if (dn > -eps && dn < eps) {\n // No relative motion along this axis: separated - or exactly touching,\n // which the discrete narrow phase already treats as no contact - here\n // means never in contact.\n if (maxA <= minB || maxB <= minA) {\n return false;\n }\n\n continue;\n }\n\n // Entry/exit times of the moving interval against the static one.\n const t0 = (minB - maxA) / dn;\n const t1 = (maxB - minA) / dn;\n const entry = t0 < t1 ? t0 : t1;\n const exit = t0 < t1 ? t1 : t0;\n\n if (entry > state.tEnter) {\n state.tEnter = entry;\n // Orient the axis against the motion (from the target toward the mover).\n state.normalX = dn < 0 ? nx : -nx;\n state.normalY = dn < 0 ? ny : -ny;\n }\n\n if (exit < state.tLeave) {\n state.tLeave = exit;\n }\n\n if (state.tEnter >= state.tLeave) {\n // Separated on one axis before overlapping on another. Equality is an\n // exact graze (separation touches 0 but never goes negative), which the\n // discrete narrow phase classifies as no contact.\n return false;\n }\n }\n\n return true;\n};\n\n/** One ordered `(moving, target)` shape cast. */\ntype SweepPair = (moving: CollisionProxy, dx: number, dy: number, target: CollisionProxy, out: SweepHit) => boolean;\n\n// Every ordered pair a mass-bearing shape can move as. Boundary geometry\n// (segment, chain) appears only as a target: level structure is not swept, and\n// a fast body that needs protection carries a solid shape.\nconst sweepTable: PairTable<SweepPair> = orderedTable<SweepPair>([\n ['circle', 'circle', sweepCircleCircle],\n ['circle', 'capsule', sweepCircleRing],\n ['circle', 'segment', sweepCircleRing],\n ['circle', 'polygon', sweepCircleRing],\n ['capsule', 'circle', sweepRingCircle],\n ['capsule', 'capsule', sweepRoundedRings],\n ['capsule', 'segment', sweepRoundedRings],\n ['capsule', 'polygon', sweepRoundedRings],\n ['polygon', 'circle', sweepRingCircle],\n ['polygon', 'capsule', sweepRoundedRings],\n ['polygon', 'segment', sweepRings],\n ['polygon', 'polygon', sweepRings],\n]);\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ import type { PhysicsWorld } from '../PhysicsWorld';
7
7
  export interface PhysicsDebugDrawOptions {
8
8
  /** Collider shape outlines, coloured by body type. Default `true`. */
9
9
  drawShapes?: boolean;
10
- /** Collider world AABBs. Default `false`. */
10
+ /** World AABBs of the geometry the broad phase holds - a chain as its per-edge leaves. Default `false`. */
11
11
  drawAabb?: boolean;
12
12
  /** Contact points. Default `false`. */
13
13
  drawContacts?: boolean;
@@ -15,7 +15,7 @@ export interface PhysicsDebugDrawOptions {
15
15
  drawNormals?: boolean;
16
16
  /** Body origins (centre markers). Default `false`. */
17
17
  drawCenters?: boolean;
18
- /** Broad-phase candidate links between AABB-overlapping colliders. Default `false`. */
18
+ /** Broad-phase candidate links between AABB-overlapping leaves - a chain links per edge. Default `false`. */
19
19
  drawBroadphase?: boolean;
20
20
  /** Tint sleeping bodies distinctly (applies to the shape outline). Default `false`. */
21
21
  drawSleeping?: boolean;
@@ -23,7 +23,7 @@ export interface PhysicsDebugDrawOptions {
23
23
  drawJoints?: boolean;
24
24
  }
25
25
  /**
26
- * `DebugLayer` that visualises a {@link PhysicsWorld} shapes, AABBs, contacts,
26
+ * `DebugLayer` that visualises a {@link PhysicsWorld} - shapes, AABBs, contacts,
27
27
  * normals, centres and broad-phase links. It reads the world's public state and
28
28
  * recomputes contacts/broad-phase locally only while visible, so it never
29
29
  * perturbs the simulation and works over any backend. Lives in the `./debug`
@@ -34,7 +34,6 @@ export declare class PhysicsDebugDraw extends DebugLayer {
34
34
  readonly options: Required<PhysicsDebugDrawOptions>;
35
35
  private readonly _world;
36
36
  private _graphics;
37
- private readonly _broadPhase;
38
37
  private readonly _manifold;
39
38
  private readonly _pairs;
40
39
  constructor(app: Application, world: PhysicsWorld, options?: PhysicsDebugDrawOptions);
@@ -45,9 +44,29 @@ export declare class PhysicsDebugDraw extends DebugLayer {
45
44
  destroy(): void;
46
45
  private _outlineColor;
47
46
  private _strokeShape;
47
+ /**
48
+ * Capsule outline: the two straight sides offset from the spine, joined by a
49
+ * half-circle at each end. The arcs are approximated - a visualisation may
50
+ * tessellate, the solver never does.
51
+ */
52
+ private _strokeCapsule;
48
53
  private _strokeAabb;
49
54
  private _strokeCircle;
50
55
  private _strokeCross;
56
+ /**
57
+ * Recomputes candidate pairs from scratch for the current frame via a plain
58
+ * brute-force AABB-overlap scan - deliberately NOT `AabbTreeBroadPhase`.
59
+ * That class relies on `Collider._treeProxy`, a field shared on the
60
+ * Collider object and valid only for whichever ONE broad phase currently
61
+ * owns it (the world's own `NativePhysicsBackend`); a second instance here
62
+ * would silently corrupt that ownership every time debug draw and a
63
+ * physics step interleave. Debug-only/opt-in (tree-shaken from production
64
+ * bundles), recomputed from scratch every visible frame regardless, so an
65
+ * O(n^2) scan is an appropriate, fully self-contained trade for
66
+ * correctness - it touches no state outside this method.
67
+ */
68
+ private _collectPairs;
51
69
  private _renderBroadphase;
52
70
  private _renderContacts;
53
71
  }
72
+ //# sourceMappingURL=PhysicsDebugDraw.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PhysicsDebugDraw.d.ts","sourceRoot":"","sources":["../../../src/debug/PhysicsDebugDraw.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,6CAA6C;AAC7C,MAAM,WAAW,uBAAuB;IACtC,sEAAsE;IACtE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2GAA2G;IAC3G,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wCAAwC;IACxC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sDAAsD;IACtD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6GAA6G;IAC7G,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,uFAAuF;IACvF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kEAAkE;IAClE,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAgBD;;;;;;GAMG;AACH,qBAAa,gBAAiB,SAAQ,UAAU;IAC9C,8CAA8C;IAC9C,SAAgB,OAAO,EAAE,QAAQ,CAAC,uBAAuB,CAAC,CAAC;IAE3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;gBAE3B,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,GAAE,uBAA4B;IAgB/F,IAAoB,QAAQ,IAAI,kBAAkB,CAEjD;IAEe,MAAM,IAAI,IAAI;IAId,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IA4CpD,OAAO,CAAC,aAAa;IASL,OAAO,IAAI,IAAI;IAS/B,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,YAAY;IAwDpB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAwBtB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,YAAY;IAOpB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IAqBrB,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,eAAe;CAkCxB"}
@@ -1,195 +1,237 @@
1
- import { Color, Graphics } from '@codexo/exojs';
2
- import { DebugLayer } from '@codexo/exojs/debug';
3
- import { SweepAndPrune } from '../broadphase/SweepAndPrune.js';
4
- import { Manifold } from '../collision/Manifold.js';
5
- import { collide } from '../collision/narrowphase.js';
1
+ import { aabbOverlap } from "../Aabb.js";
2
+ import { authoredCollider } from "../Collider.js";
3
+ import { Manifold } from "../collision/Manifold.js";
4
+ import { collide } from "../collision/narrowphase.js";
5
+ import { Color, Graphics } from "@codexo/exojs";
6
+ import { DebugLayer } from "@codexo/exojs/debug";
6
7
 
8
+ //#region src/debug/PhysicsDebugDraw.ts
7
9
  const segments = 24;
8
- const colorStatic = new Color(0.3, 0.9, 0.4, 0.9);
9
- const colorKinematic = new Color(0.4, 0.7, 1, 0.9);
10
- const colorDynamic = new Color(1, 0.85, 0.3, 0.9);
11
- const colorSensor = new Color(1, 0.3, 1, 0.9);
12
- const colorAabb = new Color(0.5, 0.5, 0.5, 0.5);
13
- const colorContact = new Color(1, 0.2, 0.2, 1);
14
- const colorNormal = new Color(1, 0.6, 0.1, 1);
15
- const colorCenter = new Color(1, 1, 1, 0.9);
16
- const colorBroadphase = new Color(0.2, 0.8, 0.8, 0.5);
17
- const colorSleeping = new Color(0.45, 0.45, 0.5, 0.7);
18
- const colorJoint = new Color(0.9, 0.5, 1, 0.8);
10
+ const colorStatic = new Color(.3, .9, .4, .9);
11
+ const colorKinematic = new Color(.4, .7, 1, .9);
12
+ const colorDynamic = new Color(1, .85, .3, .9);
13
+ const colorSensor = new Color(1, .3, 1, .9);
14
+ const colorAabb = new Color(.5, .5, .5, .5);
15
+ const colorContact = new Color(1, .2, .2, 1);
16
+ const colorNormal = new Color(1, .6, .1, 1);
17
+ const colorCenter = new Color(1, 1, 1, .9);
18
+ const colorBroadphase = new Color(.2, .8, .8, .5);
19
+ const colorSleeping = new Color(.45, .45, .5, .7);
20
+ const colorJoint = new Color(.9, .5, 1, .8);
19
21
  /**
20
- * `DebugLayer` that visualises a {@link PhysicsWorld} shapes, AABBs, contacts,
21
- * normals, centres and broad-phase links. It reads the world's public state and
22
- * recomputes contacts/broad-phase locally only while visible, so it never
23
- * perturbs the simulation and works over any backend. Lives in the `./debug`
24
- * subpath so it tree-shakes out of production bundles.
25
- */
26
- class PhysicsDebugDraw extends DebugLayer {
27
- /** Mutable option flags (read each frame). */
28
- options;
29
- _world;
30
- _graphics = null;
31
- _broadPhase = new SweepAndPrune();
32
- _manifold = new Manifold();
33
- _pairs = [];
34
- constructor(app, world, options = {}) {
35
- super(app);
36
- this._world = world;
37
- this.options = {
38
- drawShapes: options.drawShapes ?? true,
39
- drawAabb: options.drawAabb ?? false,
40
- drawContacts: options.drawContacts ?? false,
41
- drawNormals: options.drawNormals ?? false,
42
- drawCenters: options.drawCenters ?? false,
43
- drawBroadphase: options.drawBroadphase ?? false,
44
- drawSleeping: options.drawSleeping ?? false,
45
- drawJoints: options.drawJoints ?? false,
46
- };
47
- }
48
- get viewMode() {
49
- return 'world';
50
- }
51
- update() {
52
- // All state is read from the world in render().
53
- }
54
- render(backend) {
55
- const gfx = (this._graphics ??= new Graphics());
56
- const options = this.options;
57
- gfx.clear();
58
- gfx.lineWidth = 1;
59
- if (options.drawBroadphase) {
60
- this._renderBroadphase(gfx);
61
- }
62
- if (options.drawAabb) {
63
- for (const collider of this._world.colliders) {
64
- this._strokeAabb(gfx, collider);
65
- }
66
- }
67
- if (options.drawShapes) {
68
- for (const collider of this._world.colliders) {
69
- this._strokeShape(gfx, collider);
70
- }
71
- }
72
- if (options.drawCenters) {
73
- gfx.lineColor = colorCenter;
74
- for (const body of this._world.bodies) {
75
- this._strokeCross(gfx, body.x, body.y, 4);
76
- }
77
- }
78
- if (options.drawContacts || options.drawNormals) {
79
- this._renderContacts(gfx);
80
- }
81
- if (options.drawJoints) {
82
- this._renderJoints(gfx);
83
- }
84
- gfx.render(backend);
85
- }
86
- _renderJoints(gfx) {
87
- gfx.lineColor = colorJoint;
88
- for (const joint of this._world.joints) {
89
- gfx.moveTo(joint.bodyA.x, joint.bodyA.y);
90
- gfx.lineTo(joint.bodyB.x, joint.bodyB.y);
91
- }
92
- }
93
- destroy() {
94
- if (this._graphics !== null) {
95
- this._graphics.destroy();
96
- this._graphics = null;
97
- }
98
- }
99
- // ── helpers ────────────────────────────────────────────────────────────
100
- _outlineColor(collider) {
101
- if (collider.isSensor) {
102
- return colorSensor;
103
- }
104
- if (this.options.drawSleeping && collider.body.isSleeping) {
105
- return colorSleeping;
106
- }
107
- return colorForType(collider.body.type);
108
- }
109
- _strokeShape(gfx, collider) {
110
- gfx.lineColor = this._outlineColor(collider);
111
- if (collider.shape.type === 'circle') {
112
- const c = collider.worldCenter;
113
- const r = collider.shape.radius;
114
- this._strokeCircle(gfx, c.x, c.y, r);
115
- // A spoke to the surface conveys orientation once bodies rotate.
116
- gfx.moveTo(c.x, c.y);
117
- gfx.lineTo(c.x + collider.worldTransform.cos * r, c.y + collider.worldTransform.sin * r);
118
- return;
119
- }
120
- const verts = collider.worldVertices;
121
- const count = collider.shape.count;
122
- gfx.moveTo(verts[0], verts[1]);
123
- for (let i = 1; i <= count; i++) {
124
- const j = i % count;
125
- gfx.lineTo(verts[j * 2], verts[j * 2 + 1]);
126
- }
127
- }
128
- _strokeAabb(gfx, collider) {
129
- const box = collider.aabb;
130
- gfx.lineColor = colorAabb;
131
- gfx.moveTo(box.minX, box.minY);
132
- gfx.lineTo(box.maxX, box.minY);
133
- gfx.lineTo(box.maxX, box.maxY);
134
- gfx.lineTo(box.minX, box.maxY);
135
- gfx.lineTo(box.minX, box.minY);
136
- }
137
- _strokeCircle(gfx, cx, cy, r) {
138
- gfx.moveTo(cx + r, cy);
139
- for (let i = 1; i <= segments; i++) {
140
- const a = (i / segments) * Math.PI * 2;
141
- gfx.lineTo(cx + Math.cos(a) * r, cy + Math.sin(a) * r);
142
- }
143
- }
144
- _strokeCross(gfx, x, y, size) {
145
- gfx.moveTo(x - size, y);
146
- gfx.lineTo(x + size, y);
147
- gfx.moveTo(x, y - size);
148
- gfx.lineTo(x, y + size);
149
- }
150
- _renderBroadphase(gfx) {
151
- this._broadPhase.computePairs(this._world.colliders, this._pairs);
152
- gfx.lineColor = colorBroadphase;
153
- for (const pair of this._pairs) {
154
- const a = pair.a.aabb;
155
- const b = pair.b.aabb;
156
- gfx.moveTo((a.minX + a.maxX) * 0.5, (a.minY + a.maxY) * 0.5);
157
- gfx.lineTo((b.minX + b.maxX) * 0.5, (b.minY + b.maxY) * 0.5);
158
- }
159
- }
160
- _renderContacts(gfx) {
161
- this._broadPhase.computePairs(this._world.colliders, this._pairs);
162
- for (const pair of this._pairs) {
163
- const a = pair.a;
164
- const b = pair.b;
165
- if (a.isSensor || b.isSensor) {
166
- continue;
167
- }
168
- if (!collide(a, b, this._manifold)) {
169
- continue;
170
- }
171
- for (let i = 0; i < this._manifold.pointCount; i++) {
172
- // i in 0..pointCount-1 and pointCount 2, so the point always exists.
173
- const point = i === 0 ? this._manifold.points[0] : this._manifold.points[1];
174
- if (this.options.drawContacts) {
175
- gfx.lineColor = colorContact;
176
- this._strokeCross(gfx, point.x, point.y, 3);
177
- }
178
- if (this.options.drawNormals) {
179
- gfx.lineColor = colorNormal;
180
- gfx.moveTo(point.x, point.y);
181
- gfx.lineTo(point.x + this._manifold.normalX * 12, point.y + this._manifold.normalY * 12);
182
- }
183
- }
184
- }
185
- }
186
- }
22
+ * `DebugLayer` that visualises a {@link PhysicsWorld} - shapes, AABBs, contacts,
23
+ * normals, centres and broad-phase links. It reads the world's public state and
24
+ * recomputes contacts/broad-phase locally only while visible, so it never
25
+ * perturbs the simulation and works over any backend. Lives in the `./debug`
26
+ * subpath so it tree-shakes out of production bundles.
27
+ */
28
+ var PhysicsDebugDraw = class extends DebugLayer {
29
+ /** Mutable option flags (read each frame). */
30
+ options;
31
+ _world;
32
+ _graphics = null;
33
+ _manifold = new Manifold();
34
+ _pairs = [];
35
+ constructor(app, world, options = {}) {
36
+ super(app);
37
+ this._world = world;
38
+ this.options = {
39
+ drawShapes: options.drawShapes ?? true,
40
+ drawAabb: options.drawAabb ?? false,
41
+ drawContacts: options.drawContacts ?? false,
42
+ drawNormals: options.drawNormals ?? false,
43
+ drawCenters: options.drawCenters ?? false,
44
+ drawBroadphase: options.drawBroadphase ?? false,
45
+ drawSleeping: options.drawSleeping ?? false,
46
+ drawJoints: options.drawJoints ?? false
47
+ };
48
+ }
49
+ get viewMode() {
50
+ return "world";
51
+ }
52
+ update() {}
53
+ render(backend) {
54
+ const gfx = this._graphics ??= new Graphics();
55
+ const options = this.options;
56
+ gfx.clear();
57
+ gfx.lineWidth = 1;
58
+ if (options.drawBroadphase) this._renderBroadphase(gfx);
59
+ if (options.drawAabb) for (const collider of this._world.detectionGeometry) this._strokeAabb(gfx, collider);
60
+ if (options.drawShapes) for (const collider of this._world.colliders) this._strokeShape(gfx, collider);
61
+ if (options.drawCenters) {
62
+ gfx.lineColor = colorCenter;
63
+ for (const body of this._world.bodies) this._strokeCross(gfx, body.x, body.y, 4);
64
+ }
65
+ if (options.drawContacts || options.drawNormals) this._renderContacts(gfx);
66
+ if (options.drawJoints) this._renderJoints(gfx);
67
+ gfx.render(backend);
68
+ }
69
+ _renderJoints(gfx) {
70
+ gfx.lineColor = colorJoint;
71
+ for (const joint of this._world.joints) {
72
+ gfx.moveTo(joint.bodyA.x, joint.bodyA.y);
73
+ gfx.lineTo(joint.bodyB.x, joint.bodyB.y);
74
+ }
75
+ }
76
+ destroy() {
77
+ if (this._graphics !== null) {
78
+ this._graphics.destroy();
79
+ this._graphics = null;
80
+ }
81
+ }
82
+ _outlineColor(collider) {
83
+ if (collider.isSensor) return colorSensor;
84
+ if (this.options.drawSleeping && collider.body.isSleeping) return colorSleeping;
85
+ return colorForType(collider.body.type);
86
+ }
87
+ _strokeShape(gfx, collider) {
88
+ gfx.lineColor = this._outlineColor(collider);
89
+ if (collider.shape.type === "circle") {
90
+ const c = collider.worldCenter;
91
+ const r = collider.shape.radius;
92
+ this._strokeCircle(gfx, c.x, c.y, r);
93
+ gfx.moveTo(c.x, c.y);
94
+ gfx.lineTo(c.x + collider.worldTransform.cos * r, c.y + collider.worldTransform.sin * r);
95
+ return;
96
+ }
97
+ if (collider.shape.type === "capsule") {
98
+ this._strokeCapsule(gfx, collider.worldVertices, collider.shape.radius);
99
+ return;
100
+ }
101
+ if (collider.shape.type === "segment") {
102
+ const ends = collider.worldVertices;
103
+ gfx.moveTo(ends[0], ends[1]);
104
+ gfx.lineTo(ends[2], ends[3]);
105
+ return;
106
+ }
107
+ if (collider.shape.type === "chain") {
108
+ for (const edge of collider.chainEdges ?? []) {
109
+ const ends = edge.worldVertices;
110
+ gfx.moveTo(ends[0], ends[1]);
111
+ gfx.lineTo(ends[2], ends[3]);
112
+ }
113
+ return;
114
+ }
115
+ const verts = collider.worldVertices;
116
+ const count = collider.shape.count;
117
+ gfx.moveTo(verts[0], verts[1]);
118
+ for (let i = 1; i <= count; i++) {
119
+ const j = i % count;
120
+ gfx.lineTo(verts[j * 2], verts[j * 2 + 1]);
121
+ }
122
+ }
123
+ /**
124
+ * Capsule outline: the two straight sides offset from the spine, joined by a
125
+ * half-circle at each end. The arcs are approximated - a visualisation may
126
+ * tessellate, the solver never does.
127
+ */
128
+ _strokeCapsule(gfx, spine, radius) {
129
+ const ax = spine[0];
130
+ const ay = spine[1];
131
+ const bx = spine[2];
132
+ const by = spine[3];
133
+ const along = Math.atan2(by - ay, bx - ax);
134
+ const arcSegments = 12;
135
+ gfx.moveTo(ax + Math.cos(along + Math.PI / 2) * radius, ay + Math.sin(along + Math.PI / 2) * radius);
136
+ gfx.lineTo(bx + Math.cos(along + Math.PI / 2) * radius, by + Math.sin(along + Math.PI / 2) * radius);
137
+ for (let i = 1; i <= arcSegments; i++) {
138
+ const angle = along + Math.PI / 2 - Math.PI * i / arcSegments;
139
+ gfx.lineTo(bx + Math.cos(angle) * radius, by + Math.sin(angle) * radius);
140
+ }
141
+ for (let i = 1; i <= arcSegments; i++) {
142
+ const angle = along - Math.PI / 2 - Math.PI * i / arcSegments;
143
+ gfx.lineTo(ax + Math.cos(angle) * radius, ay + Math.sin(angle) * radius);
144
+ }
145
+ }
146
+ _strokeAabb(gfx, collider) {
147
+ const box = collider.aabb;
148
+ gfx.lineColor = colorAabb;
149
+ gfx.moveTo(box.minX, box.minY);
150
+ gfx.lineTo(box.maxX, box.minY);
151
+ gfx.lineTo(box.maxX, box.maxY);
152
+ gfx.lineTo(box.minX, box.maxY);
153
+ gfx.lineTo(box.minX, box.minY);
154
+ }
155
+ _strokeCircle(gfx, cx, cy, r) {
156
+ gfx.moveTo(cx + r, cy);
157
+ for (let i = 1; i <= segments; i++) {
158
+ const a = i / segments * Math.PI * 2;
159
+ gfx.lineTo(cx + Math.cos(a) * r, cy + Math.sin(a) * r);
160
+ }
161
+ }
162
+ _strokeCross(gfx, x, y, size) {
163
+ gfx.moveTo(x - size, y);
164
+ gfx.lineTo(x + size, y);
165
+ gfx.moveTo(x, y - size);
166
+ gfx.lineTo(x, y + size);
167
+ }
168
+ /**
169
+ * Recomputes candidate pairs from scratch for the current frame via a plain
170
+ * brute-force AABB-overlap scan - deliberately NOT `AabbTreeBroadPhase`.
171
+ * That class relies on `Collider._treeProxy`, a field shared on the
172
+ * Collider object and valid only for whichever ONE broad phase currently
173
+ * owns it (the world's own `NativePhysicsBackend`); a second instance here
174
+ * would silently corrupt that ownership every time debug draw and a
175
+ * physics step interleave. Debug-only/opt-in (tree-shaken from production
176
+ * bundles), recomputed from scratch every visible frame regardless, so an
177
+ * O(n^2) scan is an appropriate, fully self-contained trade for
178
+ * correctness - it touches no state outside this method.
179
+ */
180
+ _collectPairs() {
181
+ const colliders = this._world.detectionGeometry;
182
+ const pairs = this._pairs;
183
+ pairs.length = 0;
184
+ for (let i = 0; i < colliders.length; i++) {
185
+ const a = colliders[i];
186
+ for (let j = i + 1; j < colliders.length; j++) {
187
+ const b = colliders[j];
188
+ if (a.body !== b.body && aabbOverlap(a.aabb, b.aabb)) pairs.push(a.id < b.id ? {
189
+ a,
190
+ b
191
+ } : {
192
+ a: b,
193
+ b: a
194
+ });
195
+ }
196
+ }
197
+ }
198
+ _renderBroadphase(gfx) {
199
+ this._collectPairs();
200
+ gfx.lineColor = colorBroadphase;
201
+ for (const pair of this._pairs) {
202
+ const a = pair.a.aabb;
203
+ const b = pair.b.aabb;
204
+ gfx.moveTo((a.minX + a.maxX) * .5, (a.minY + a.maxY) * .5);
205
+ gfx.lineTo((b.minX + b.maxX) * .5, (b.minY + b.maxY) * .5);
206
+ }
207
+ }
208
+ _renderContacts(gfx) {
209
+ this._collectPairs();
210
+ for (const pair of this._pairs) {
211
+ const a = pair.a;
212
+ const b = pair.b;
213
+ if (authoredCollider(a).isSensor || authoredCollider(b).isSensor) continue;
214
+ if (!collide(a, b, this._manifold)) continue;
215
+ for (let i = 0; i < this._manifold.pointCount; i++) {
216
+ const point = i === 0 ? this._manifold.points[0] : this._manifold.points[1];
217
+ if (this.options.drawContacts) {
218
+ gfx.lineColor = colorContact;
219
+ this._strokeCross(gfx, point.x, point.y, 3);
220
+ }
221
+ if (this.options.drawNormals) {
222
+ gfx.lineColor = colorNormal;
223
+ gfx.moveTo(point.x, point.y);
224
+ gfx.lineTo(point.x + this._manifold.normalX * 12, point.y + this._manifold.normalY * 12);
225
+ }
226
+ }
227
+ }
228
+ }
229
+ };
187
230
  const colorForType = (type) => {
188
- if (type === 'static') {
189
- return colorStatic;
190
- }
191
- return type === 'kinematic' ? colorKinematic : colorDynamic;
231
+ if (type === "static") return colorStatic;
232
+ return type === "kinematic" ? colorKinematic : colorDynamic;
192
233
  };
193
234
 
235
+ //#endregion
194
236
  export { PhysicsDebugDraw };
195
- //# sourceMappingURL=PhysicsDebugDraw.js.map
237
+ //# sourceMappingURL=PhysicsDebugDraw.js.map