@codexo/exojs-physics 0.15.2 → 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,490 +1,444 @@
1
- /** Relative normal speed (px/s) below which a contact is treated as resting (no restitution). */
2
- /**
3
- * Penetration allowance (px) the soft bias leaves uncorrected. The narrow phase
4
- * only produces a manifold while the colliders overlap, so pushing penetration
5
- * fully to zero lets a resting contact wink out for a frame (free-fall, then
6
- * re-detect) — a periodic energy spike. Leaving a small slop keeps the contact
7
- * persistently overlapping and the warm-start cache alive.
8
- */
9
- const slop = 0.25;
1
+ import { contactSlop } from "./tolerances.js";
2
+
3
+ //#region src/solver/ContactSolver.ts
10
4
  /**
11
- * Cap on the soft-constraint push-out velocity (px/s). Bounds how fast the bias
12
- * resolves deep penetration so a large overlap cannot fling bodies apart; the
13
- * Box2D-v3 analogue is `contactSpeed = 3·lengthUnit`, retuned for ExoJS pixels.
14
- */
5
+ * Cap on the soft-constraint push-out velocity (px/s). Bounds how fast the bias
6
+ * resolves deep penetration so a large overlap cannot fling bodies apart; the
7
+ * Box2D-v3 analogue is `contactSpeed = 3·lengthUnit`, retuned for ExoJS pixels.
8
+ */
15
9
  const maxBiasVelocity = 4;
16
10
  /** Reject the 2-point block solve when the contact matrix is worse-conditioned than this (fall back to sequential). */
17
- const maxConditionNumber = 1000;
11
+ const maxConditionNumber = 1e3;
18
12
  /**
19
- * Per-contact velocity constraint (12 points), computed once per **frame** in
20
- * {@link ContactSolver.prepare} and reused across all sub-steps and passes.
21
- * Pooled and reused across frames, so steady-state stepping allocates nothing.
22
- */
23
- class ContactConstraint {
24
- record;
25
- bodyA;
26
- bodyB;
27
- nx = 0;
28
- ny = 0;
29
- tx = 0;
30
- ty = 0;
31
- friction = 0;
32
- restitution = 0;
33
- pointCount = 0;
34
- // Soft-constraint factors (Box2D-v3 "soft step"), derived once per frame from
35
- // contactHertz/dampingRatio and the sub-step `h`. Shared by both points.
36
- biasRate = 0;
37
- massScale = 1;
38
- impulseScale = 0;
39
- // Per-point contact arms at frame start (relative to each body's centre of mass).
40
- rAx = [0, 0];
41
- rAy = [0, 0];
42
- rBx = [0, 0];
43
- rBy = [0, 0];
44
- // Per-point arms rotated by the live sub-step rotation (refreshed each pass by
45
- // {@link ContactSolver._updateArms}); the solve reads these so a rotating body's
46
- // contact torque tracks its current orientation.
47
- rotAx = [0, 0];
48
- rotAy = [0, 0];
49
- rotBx = [0, 0];
50
- rotBy = [0, 0];
51
- normalMass = [0, 0];
52
- tangentMass = [0, 0];
53
- /** Contact separation at frame start (= −penetration; negative when overlapping). */
54
- baseSeparation = [0, 0];
55
- /** Relative normal velocity at frame start (the impact speed the restitution pass keys off). */
56
- relativeVelocity = [0, 0];
57
- /** Push-out target normal velocity for the current pass (set per pass from the live separation). */
58
- velocityBias = [0, 0];
59
- // 2-point block: the contact matrix `K` and its inverse (set only when the
60
- // manifold has two points and `K` is well-conditioned; else sequential).
61
- block = false;
62
- k11 = 0;
63
- k12 = 0;
64
- k22 = 0;
65
- invK11 = 0;
66
- invK12 = 0;
67
- invK22 = 0;
68
- }
13
+ * Per-contact velocity constraint (1-2 points), computed once per **frame** in
14
+ * {@link ContactSolver.prepare} and reused across all sub-steps and passes.
15
+ * Pooled and reused across frames, so a steady-state step constructs no
16
+ * constraint of its own. It is not allocation-free in the wider sense: a double
17
+ * handed to a function the optimiser declines to inline is boxed on the heap for
18
+ * the duration of that call, which is why the hot paths below apply their
19
+ * results in place instead of through helpers.
20
+ */
21
+ var ContactConstraint = class {
22
+ record;
23
+ bodyA;
24
+ bodyB;
25
+ nx = 0;
26
+ ny = 0;
27
+ tx = 0;
28
+ ty = 0;
29
+ friction = 0;
30
+ restitution = 0;
31
+ pointCount = 0;
32
+ biasRate = 0;
33
+ massScale = 1;
34
+ impulseScale = 0;
35
+ rAx = [0, 0];
36
+ rAy = [0, 0];
37
+ rBx = [0, 0];
38
+ rBy = [0, 0];
39
+ rotAx = [0, 0];
40
+ rotAy = [0, 0];
41
+ rotBx = [0, 0];
42
+ rotBy = [0, 0];
43
+ normalMass = [0, 0];
44
+ tangentMass = [0, 0];
45
+ /** Contact separation at frame start (= −penetration; negative when overlapping). */
46
+ baseSeparation = [0, 0];
47
+ /** Relative normal velocity at frame start (the impact speed the restitution pass keys off). */
48
+ relativeVelocity = [0, 0];
49
+ /** Push-out target normal velocity for the current pass (set per pass from the live separation). */
50
+ velocityBias = [0, 0];
51
+ /**
52
+ * Whether the point's push-out target was clamped to `maxBiasVelocity` this
53
+ * pass. A clamped target is no longer the spring response the soft scaling
54
+ * models, so the single-point solve drops that scaling while it holds.
55
+ */
56
+ biasClamped = [false, false];
57
+ block = false;
58
+ k11 = 0;
59
+ k12 = 0;
60
+ k22 = 0;
61
+ invK11 = 0;
62
+ invK12 = 0;
63
+ invK22 = 0;
64
+ };
69
65
  /**
70
- * Native warm-started **TGS-Soft** contact solver (Box2D-v3 "soft step"). Driven
71
- * by {@link PhysicsWorld} as a sub-stepping loop: detection runs once per frame,
72
- * then {@link prepare} builds the per-contact constraints (effective masses,
73
- * frame-start separation + impact velocity, soft factors, and a 2×2 block matrix
74
- * for two-point manifolds) **once**, and each sub-step runs
75
- * {@link warmStart} → {@link solveVelocities}(useBias=true) → integrate positions
76
- * → {@link solveVelocities}(useBias=false) relax. A final {@link applyRestitution}
77
- * pass adds bounce above the threshold.
78
- *
79
- * The position constraint is folded into the velocity solve as a **soft bias**
80
- * (a damped-spring push-out whose stiffness is decoupled from the iteration
81
- * count), recomputed each pass from the live per-body delta position/rotation
82
- * so there is no separate NGS geometric pass. The contact anchors are rotated by
83
- * the live sub-step rotation each pass, so a tilting stack's restoring torque is
84
- * computed against its current orientation (without this the tilt grows instead
85
- * of being corrected). Two-point manifolds still solve the normal impulses **as a
86
- * 2×2 block LCP** (Box2D-style), which propagates stack loads far better than
87
- * solving the points sequentially; the block path carries the soft push-out as
88
- * its velocity target (hard mass scale), while single-point contacts use the full
89
- * soft mass/impulse scaling. Accumulated impulses are written back to each
90
- * {@link ContactRecord} for warm-starting the next frame.
91
- *
92
- * Internal to {@link NativePhysicsBackend}; not part of the public surface.
93
- */
94
- class ContactSolver {
95
- _constraints = [];
96
- _count = 0;
97
- /**
98
- * Build the per-contact constraints for this frame from the touching solid
99
- * contacts. `h` is the sub-step duration (`fixedDelta / subStepCount`); the
100
- * soft factors are derived from it together with `contactHertz`/`dampingRatio`.
101
- * Runs once per frame (detection is not repeated per sub-step).
102
- */
103
- prepare(contacts, h, contactHertz, dampingRatio) {
104
- this._count = 0;
105
- // Box2D-v3 soft-constraint factors from a damped spring (hertz, zeta) at the
106
- // sub-step `h`: omega = 2πf; a1 = + hω; a2 = hω·a1; a3 = 1/(1+a2).
107
- const omega = 2 * Math.PI * contactHertz;
108
- const a1 = 2 * dampingRatio + h * omega;
109
- const a2 = h * omega * a1;
110
- const a3 = 1 / (1 + a2);
111
- const biasRate = contactHertz > 0 ? omega / a1 : 0;
112
- const massScale = contactHertz > 0 ? a2 * a3 : 1;
113
- const impulseScale = contactHertz > 0 ? a3 : 0;
114
- for (const contact of contacts) {
115
- const manifold = contact.manifold;
116
- const pointCount = manifold.pointCount;
117
- if (pointCount === 0) {
118
- continue;
119
- }
120
- const bodyA = contact.a.body;
121
- const bodyB = contact.b.body;
122
- // Skip contacts where either body is asleep: a sleeping island's solid
123
- // contacts are all sleeping↔sleeping or sleeping↔static (both at rest), so
124
- // there is nothing to solve. A sleeping body touched by an awake one is
125
- // woken (island merge) before this runs, so it is never skipped wrongly.
126
- if (bodyA.isSleeping || bodyB.isSleeping) {
127
- continue;
128
- }
129
- const constraint = this._acquire();
130
- const nx = manifold.normalX;
131
- const ny = manifold.normalY;
132
- const tx = -ny;
133
- const ty = nx;
134
- constraint.record = contact;
135
- constraint.bodyA = bodyA;
136
- constraint.bodyB = bodyB;
137
- constraint.nx = nx;
138
- constraint.ny = ny;
139
- constraint.tx = tx;
140
- constraint.ty = ty;
141
- constraint.friction = Math.sqrt(contact.a.friction * contact.b.friction);
142
- constraint.restitution = Math.max(contact.a.restitution, contact.b.restitution);
143
- constraint.pointCount = pointCount;
144
- constraint.block = false;
145
- constraint.biasRate = biasRate;
146
- constraint.massScale = massScale;
147
- constraint.impulseScale = impulseScale;
148
- const mA = bodyA.invMass;
149
- const mB = bodyB.invMass;
150
- const iA = bodyA.invInertia;
151
- const iB = bodyB.invInertia;
152
- const comAx = bodyA.worldCenterOfMassX;
153
- const comAy = bodyA.worldCenterOfMassY;
154
- const comBx = bodyB.worldCenterOfMassX;
155
- const comBy = bodyB.worldCenterOfMassY;
156
- for (let i = 0; i < pointCount; i++) {
157
- // i in 0..pointCount-1 and pointCount ≤ 2, so the manifold point exists.
158
- const point = i === 0 ? manifold.points[0] : manifold.points[1];
159
- const rAx = point.x - comAx;
160
- const rAy = point.y - comAy;
161
- const rBx = point.x - comBx;
162
- const rBy = point.y - comBy;
163
- constraint.rAx[i] = rAx;
164
- constraint.rAy[i] = rAy;
165
- constraint.rBx[i] = rBx;
166
- constraint.rBy[i] = rBy;
167
- // Frame start: no accumulated rotation yet, so the live arms equal the base arms.
168
- constraint.rotAx[i] = rAx;
169
- constraint.rotAy[i] = rAy;
170
- constraint.rotBx[i] = rBx;
171
- constraint.rotBy[i] = rBy;
172
- const rnA = rAx * ny - rAy * nx;
173
- const rnB = rBx * ny - rBy * nx;
174
- const kn = mA + mB + iA * rnA * rnA + iB * rnB * rnB;
175
- const rtA = rAx * ty - rAy * tx;
176
- const rtB = rBx * ty - rBy * tx;
177
- const kt = mA + mB + iA * rtA * rtA + iB * rtB * rtB;
178
- constraint.normalMass[i] = kn > 0 ? 1 / kn : 0;
179
- constraint.tangentMass[i] = kt > 0 ? 1 / kt : 0;
180
- constraint.baseSeparation[i] = -point.penetration;
181
- constraint.velocityBias[i] = 0;
182
- // Frame-start relative normal velocity (the true impact speed, captured
183
- // before this frame's gravity since prepare runs ahead of the sub-step
184
- // loop) — keys the restitution pass so a resting contact does not
185
- // perpetually micro-bounce off the per-step gravity increment.
186
- constraint.relativeVelocity[i] = normalVelocity(bodyA, bodyB, rAx, rAy, rBx, rBy, nx, ny);
187
- }
188
- if (pointCount === 2) {
189
- const rn1A = n(constraint.rAx, 0) * ny - n(constraint.rAy, 0) * nx;
190
- const rn1B = n(constraint.rBx, 0) * ny - n(constraint.rBy, 0) * nx;
191
- const rn2A = n(constraint.rAx, 1) * ny - n(constraint.rAy, 1) * nx;
192
- const rn2B = n(constraint.rBx, 1) * ny - n(constraint.rBy, 1) * nx;
193
- const k11 = mA + mB + iA * rn1A * rn1A + iB * rn1B * rn1B;
194
- const k22 = mA + mB + iA * rn2A * rn2A + iB * rn2B * rn2B;
195
- const k12 = mA + mB + iA * rn1A * rn2A + iB * rn1B * rn2B;
196
- const det = k11 * k22 - k12 * k12;
197
- // Only block-solve when `K` is well-conditioned (parallel-ish contacts
198
- // produce a near-singular matrix → fall back to sequential).
199
- if (det > 0 && k11 * k11 < maxConditionNumber * det) {
200
- const invDet = 1 / det;
201
- constraint.k11 = k11;
202
- constraint.k12 = k12;
203
- constraint.k22 = k22;
204
- constraint.invK11 = invDet * k22;
205
- constraint.invK12 = -invDet * k12;
206
- constraint.invK22 = invDet * k11;
207
- constraint.block = true;
208
- }
209
- }
210
- }
211
- }
212
- /** Re-apply the cached impulses from the previous frame (warm-starting); runs each sub-step. */
213
- warmStart() {
214
- for (let ci = 0; ci < this._count; ci++) {
215
- const constraint = this._at(ci);
216
- const bodyA = constraint.bodyA;
217
- const bodyB = constraint.bodyB;
218
- this._updateArms(constraint);
219
- for (let i = 0; i < constraint.pointCount; i++) {
220
- const normalImpulse = n(constraint.record.normalImpulse, i);
221
- const tangentImpulse = n(constraint.record.tangentImpulse, i);
222
- const jx = normalImpulse * constraint.nx + tangentImpulse * constraint.tx;
223
- const jy = normalImpulse * constraint.ny + tangentImpulse * constraint.ty;
224
- applyImpulse(bodyA, bodyB, n(constraint.rotAx, i), n(constraint.rotAy, i), n(constraint.rotBx, i), n(constraint.rotBy, i), jx, jy);
225
- }
226
- }
227
- }
228
- /**
229
- * One velocity pass over every contact: the normal constraint (block when
230
- * possible) then friction (Coulomb cone). With `useBias` the normal solve
231
- * carries the soft push-out bias (the main pass, recomputed from the live
232
- * separation); without it the relax pass drives the normal velocity to zero so
233
- * the bias velocity does not remain in the bodies as injected energy.
234
- */
235
- solveVelocities(useBias) {
236
- for (let ci = 0; ci < this._count; ci++) {
237
- this._solveVelocityContact(this._at(ci), useBias);
238
- }
239
- }
240
- /**
241
- * Restitution pass, run once after the sub-step loop. For each loaded point
242
- * whose frame-start impact speed exceeded the threshold, add the impulse that
243
- * brings the post-solve normal velocity to `−restitution·impactSpeed`. Kept
244
- * separate from the main solve so resting contacts (impact below threshold) do
245
- * not micro-bounce.
246
- */
247
- applyRestitution() {
248
- for (let ci = 0; ci < this._count; ci++) {
249
- const constraint = this._at(ci);
250
- if (constraint.restitution === 0) {
251
- continue;
252
- }
253
- const bodyA = constraint.bodyA;
254
- const bodyB = constraint.bodyB;
255
- const nx = constraint.nx;
256
- const ny = constraint.ny;
257
- this._updateArms(constraint);
258
- for (let i = 0; i < constraint.pointCount; i++) {
259
- const relativeVelocity = n(constraint.relativeVelocity, i);
260
- // Only points that carried load (normalImpulse > 0) and were struck
261
- // above the resting threshold bounce.
262
- if (relativeVelocity > -1 || n(constraint.record.normalImpulse, i) <= 0) {
263
- continue;
264
- }
265
- const rAx = n(constraint.rotAx, i);
266
- const rAy = n(constraint.rotAy, i);
267
- const rBx = n(constraint.rotBx, i);
268
- const rBy = n(constraint.rotBy, i);
269
- const vn = normalVelocity(bodyA, bodyB, rAx, rAy, rBx, rBy, nx, ny);
270
- const oldNormal = n(constraint.record.normalImpulse, i);
271
- const newNormal = Math.max(0, oldNormal - n(constraint.normalMass, i) * (vn + constraint.restitution * relativeVelocity));
272
- const deltaNormal = newNormal - oldNormal;
273
- constraint.record.normalImpulse[i] = newNormal;
274
- applyImpulse(bodyA, bodyB, rAx, rAy, rBx, rBy, deltaNormal * nx, deltaNormal * ny);
275
- }
276
- }
277
- }
278
- /** Refresh the live (rotated) contact arms from each body's accumulated sub-step rotation. */
279
- _updateArms(constraint) {
280
- const cosA = constraint.bodyA._deltaCos;
281
- const sinA = constraint.bodyA._deltaSin;
282
- const cosB = constraint.bodyB._deltaCos;
283
- const sinB = constraint.bodyB._deltaSin;
284
- for (let i = 0; i < constraint.pointCount; i++) {
285
- const rAx = n(constraint.rAx, i);
286
- const rAy = n(constraint.rAy, i);
287
- const rBx = n(constraint.rBx, i);
288
- const rBy = n(constraint.rBy, i);
289
- constraint.rotAx[i] = rAx * cosA - rAy * sinA;
290
- constraint.rotAy[i] = rAx * sinA + rAy * cosA;
291
- constraint.rotBx[i] = rBx * cosB - rBy * sinB;
292
- constraint.rotBy[i] = rBx * sinB + rBy * cosB;
293
- }
294
- }
295
- _solveVelocityContact(constraint, useBias) {
296
- this._updateArms(constraint);
297
- // Recompute the per-point push-out target from the live separation (folds the
298
- // position constraint into the velocity solve as a soft bias). With useBias
299
- // off (relax pass) the target is zero the plain hard normal solve.
300
- for (let i = 0; i < constraint.pointCount; i++) {
301
- // Push out only the penetration beyond the slop, capped — leaving the slop
302
- // keeps the contact overlapping so the narrow phase does not drop it.
303
- const excess = -currentSeparation(constraint, i) - slop;
304
- constraint.velocityBias[i] = useBias && excess > 0 ? Math.min(constraint.biasRate * excess, maxBiasVelocity) : 0;
305
- }
306
- // Normal before friction (Box2D-v3 order): friction's Coulomb cone clamps to
307
- // `μ·normalImpulse`, so it must see this pass's freshly-solved normal impulse
308
- // solving friction first uses last pass's cone and lets stacks creep laterally.
309
- if (constraint.block) {
310
- this._solveNormalBlock(constraint);
311
- }
312
- else {
313
- this._solveNormalSequential(constraint, useBias);
314
- }
315
- this._solveFriction(constraint);
316
- }
317
- /** Per-point friction: clamp the accumulated tangent impulse to the Coulomb cone `±μ·normalImpulse`. */
318
- _solveFriction(constraint) {
319
- const bodyA = constraint.bodyA;
320
- const bodyB = constraint.bodyB;
321
- for (let i = 0; i < constraint.pointCount; i++) {
322
- const rAx = n(constraint.rotAx, i);
323
- const rAy = n(constraint.rotAy, i);
324
- const rBx = n(constraint.rotBx, i);
325
- const rBy = n(constraint.rotBy, i);
326
- const vtX = bodyB.linearVelocityX - bodyB.angularVelocity * rBy - (bodyA.linearVelocityX - bodyA.angularVelocity * rAy);
327
- const vtY = bodyB.linearVelocityY + bodyB.angularVelocity * rBx - (bodyA.linearVelocityY + bodyA.angularVelocity * rAx);
328
- const vt = vtX * constraint.tx + vtY * constraint.ty;
329
- const maxFriction = constraint.friction * n(constraint.record.normalImpulse, i);
330
- const oldTangent = n(constraint.record.tangentImpulse, i);
331
- const newTangent = clamp(oldTangent - n(constraint.tangentMass, i) * vt, -maxFriction, maxFriction);
332
- const deltaTangent = newTangent - oldTangent;
333
- constraint.record.tangentImpulse[i] = newTangent;
334
- applyImpulse(bodyA, bodyB, rAx, rAy, rBx, rBy, deltaTangent * constraint.tx, deltaTangent * constraint.ty);
335
- }
336
- }
337
- /**
338
- * Single-point (or ill-conditioned) soft normal solve, accumulated and clamped
339
- * ≥ 0. The Box2D-v3 incremental form: with the soft bias active the impulse is
340
- * `−normalMass·massScale·(vn − bias) impulseScale·accumulated`, which both
341
- * pushes out the penetration and bleeds a little stored impulse (the damped
342
- * spring). The relax pass passes `useBias=false`, collapsing it to the hard
343
- * `−normalMass·vn` (no bias, full mass, no impulse decay).
344
- */
345
- _solveNormalSequential(constraint, useBias) {
346
- const bodyA = constraint.bodyA;
347
- const bodyB = constraint.bodyB;
348
- const nx = constraint.nx;
349
- const ny = constraint.ny;
350
- const massScale = useBias ? constraint.massScale : 1;
351
- const impulseScale = useBias ? constraint.impulseScale : 0;
352
- for (let i = 0; i < constraint.pointCount; i++) {
353
- const rAx = n(constraint.rotAx, i);
354
- const rAy = n(constraint.rotAy, i);
355
- const rBx = n(constraint.rotBx, i);
356
- const rBy = n(constraint.rotBy, i);
357
- const vn = normalVelocity(bodyA, bodyB, rAx, rAy, rBx, rBy, nx, ny);
358
- const oldNormal = n(constraint.record.normalImpulse, i);
359
- const impulse = -n(constraint.normalMass, i) * massScale * (vn - n(constraint.velocityBias, i)) - impulseScale * oldNormal;
360
- const newNormal = Math.max(0, oldNormal + impulse);
361
- const deltaNormal = newNormal - oldNormal;
362
- constraint.record.normalImpulse[i] = newNormal;
363
- applyImpulse(bodyA, bodyB, rAx, rAy, rBx, rBy, deltaNormal * nx, deltaNormal * ny);
364
- }
365
- }
366
- /**
367
- * Two-point block normal solve (Box2D LCP): find both new normal impulses
368
- * `x 0` so the post-solve normal velocities equal the push-out targets
369
- * (`velocityBias`, zero in the relax pass) and are complementary, trying the
370
- * four corners (both active, one active, none). The block path carries the
371
- * soft push-out as its target but solves with a hard mass scale.
372
- */
373
- _solveNormalBlock(constraint) {
374
- const bodyA = constraint.bodyA;
375
- const bodyB = constraint.bodyB;
376
- const nx = constraint.nx;
377
- const ny = constraint.ny;
378
- const a1 = n(constraint.record.normalImpulse, 0);
379
- const a2 = n(constraint.record.normalImpulse, 1);
380
- const vn1 = normalVelocity(bodyA, bodyB, n(constraint.rotAx, 0), n(constraint.rotAy, 0), n(constraint.rotBx, 0), n(constraint.rotBy, 0), nx, ny);
381
- const vn2 = normalVelocity(bodyA, bodyB, n(constraint.rotAx, 1), n(constraint.rotAy, 1), n(constraint.rotBx, 1), n(constraint.rotBy, 1), nx, ny);
382
- // Residual at the current impulse: b = (vn − bias) − K·a.
383
- const bx = vn1 - n(constraint.velocityBias, 0) - (constraint.k11 * a1 + constraint.k12 * a2);
384
- const by = vn2 - n(constraint.velocityBias, 1) - (constraint.k12 * a1 + constraint.k22 * a2);
385
- // Case 1 both points active: x = −K⁻¹·b.
386
- let x1 = -(constraint.invK11 * bx + constraint.invK12 * by);
387
- let x2 = -(constraint.invK12 * bx + constraint.invK22 * by);
388
- if (x1 >= 0 && x2 >= 0) {
389
- this._applyBlock(constraint, x1 - a1, x2 - a2);
390
- constraint.record.normalImpulse[0] = x1;
391
- constraint.record.normalImpulse[1] = x2;
392
- return;
393
- }
394
- // Case 2 — only point 1 active (x2 = 0).
395
- x1 = -n(constraint.normalMass, 0) * bx;
396
- x2 = 0;
397
- if (x1 >= 0 && constraint.k12 * x1 + by >= 0) {
398
- this._applyBlock(constraint, x1 - a1, x2 - a2);
399
- constraint.record.normalImpulse[0] = x1;
400
- constraint.record.normalImpulse[1] = x2;
401
- return;
402
- }
403
- // Case 3 — only point 2 active (x1 = 0).
404
- x1 = 0;
405
- x2 = -n(constraint.normalMass, 1) * by;
406
- if (x2 >= 0 && constraint.k12 * x2 + bx >= 0) {
407
- this._applyBlock(constraint, x1 - a1, x2 - a2);
408
- constraint.record.normalImpulse[0] = x1;
409
- constraint.record.normalImpulse[1] = x2;
410
- return;
411
- }
412
- // Case 4 — neither active (separating). Only valid when both residuals are non-negative.
413
- if (bx >= 0 && by >= 0) {
414
- this._applyBlock(constraint, -a1, -a2);
415
- constraint.record.normalImpulse[0] = 0;
416
- constraint.record.normalImpulse[1] = 0;
417
- }
418
- }
419
- /** Apply the two-point block impulse deltas `(d1, d2)` along the normal at both contact points. */
420
- _applyBlock(constraint, d1, d2) {
421
- const bodyA = constraint.bodyA;
422
- const bodyB = constraint.bodyB;
423
- const nx = constraint.nx;
424
- const ny = constraint.ny;
425
- applyImpulse(bodyA, bodyB, n(constraint.rotAx, 0), n(constraint.rotAy, 0), n(constraint.rotBx, 0), n(constraint.rotBy, 0), d1 * nx, d1 * ny);
426
- applyImpulse(bodyA, bodyB, n(constraint.rotAx, 1), n(constraint.rotAy, 1), n(constraint.rotBx, 1), n(constraint.rotBy, 1), d2 * nx, d2 * ny);
427
- }
428
- _acquire() {
429
- if (this._count === this._constraints.length) {
430
- this._constraints.push(new ContactConstraint());
431
- }
432
- return this._at(this._count++);
433
- }
434
- /**
435
- * Pooled constraint at `ci`. Every caller indexes within `0..this._count-1`
436
- * and `_count` never exceeds the pool length, so the entry always exists; the
437
- * throw is unreachable and only discharges `noUncheckedIndexedAccess` without a
438
- * cast or non-null assertion.
439
- */
440
- _at(ci) {
441
- const constraint = this._constraints[ci];
442
- if (constraint === undefined) {
443
- throw new RangeError(`ContactSolver: constraint pool index ${ci} out of range.`);
444
- }
445
- return constraint;
446
- }
447
- }
66
+ * Native warm-started **TGS-Soft** contact solver (Box2D-v3 "soft step"). Driven
67
+ * by {@link PhysicsWorld} as a sub-stepping loop: detection runs once per frame,
68
+ * then {@link prepare} builds the per-contact constraints (effective masses,
69
+ * frame-start separation + impact velocity, soft factors, and a 2×2 block matrix
70
+ * for two-point manifolds) **once**, and each sub-step runs
71
+ * {@link warmStart} → {@link solveVelocities}(useBias=true) → integrate positions
72
+ * → {@link solveVelocities}(useBias=false) relax. A final {@link applyRestitution}
73
+ * pass adds bounce above the threshold.
74
+ *
75
+ * The position constraint is folded into the velocity solve as a **soft bias**
76
+ * (a damped-spring push-out whose stiffness is decoupled from the iteration
77
+ * count), recomputed each pass from the live per-body delta position/rotation -
78
+ * so there is no separate NGS geometric pass. The contact anchors are rotated by
79
+ * the live sub-step rotation each pass, so a tilting stack's restoring torque is
80
+ * computed against its current orientation (without this the tilt grows instead
81
+ * of being corrected). Two-point manifolds still solve the normal impulses **as a
82
+ * 2×2 block LCP** (Box2D-style), which propagates stack loads far better than
83
+ * solving the points sequentially; the block path carries the soft push-out as
84
+ * its velocity target (hard mass scale), while single-point contacts use the full
85
+ * soft mass/impulse scaling for as long as their push-out target stays inside
86
+ * the cap, and the hard scale once it is clamped. Accumulated impulses are
87
+ * written back to each {@link ContactRecord} for warm-starting the next frame.
88
+ *
89
+ * Internal to {@link NativePhysicsBackend}; not part of the public surface.
90
+ */
91
+ var ContactSolver = class {
92
+ _constraints = [];
93
+ _count = 0;
94
+ /**
95
+ * Build the per-contact constraints for this frame from the touching solid
96
+ * contacts. `h` is the sub-step duration (`fixedDelta / subStepCount`); the
97
+ * soft factors are derived from it together with `contactHertz`/`dampingRatio`.
98
+ * Runs once per frame (detection is not repeated per sub-step).
99
+ */
100
+ prepare(contacts, h, contactHertz, dampingRatio) {
101
+ this._count = 0;
102
+ const omega = 2 * Math.PI * contactHertz;
103
+ const a1 = 2 * dampingRatio + h * omega;
104
+ const a2 = h * omega * a1;
105
+ const a3 = 1 / (1 + a2);
106
+ const biasRate = contactHertz > 0 ? omega / a1 : 0;
107
+ const massScale = contactHertz > 0 ? a2 * a3 : 1;
108
+ const impulseScale = contactHertz > 0 ? a3 : 0;
109
+ for (const contact of contacts) {
110
+ const manifold = contact.manifold;
111
+ const pointCount = manifold.pointCount;
112
+ if (pointCount === 0 || !contact.enabled) continue;
113
+ const bodyA = contact.a.body;
114
+ const bodyB = contact.b.body;
115
+ if (bodyA.isSleeping || bodyB.isSleeping) continue;
116
+ const constraint = this._acquire();
117
+ const nx = manifold.normalX;
118
+ const ny = manifold.normalY;
119
+ const tx = -ny;
120
+ const ty = nx;
121
+ constraint.record = contact;
122
+ constraint.bodyA = bodyA;
123
+ constraint.bodyB = bodyB;
124
+ constraint.nx = nx;
125
+ constraint.ny = ny;
126
+ constraint.tx = tx;
127
+ constraint.ty = ty;
128
+ constraint.friction = contact.friction;
129
+ constraint.restitution = contact.restitution;
130
+ constraint.pointCount = pointCount;
131
+ constraint.block = false;
132
+ constraint.biasRate = biasRate;
133
+ constraint.massScale = massScale;
134
+ constraint.impulseScale = impulseScale;
135
+ const mA = bodyA.invMass;
136
+ const mB = bodyB.invMass;
137
+ const iA = bodyA.invInertia;
138
+ const iB = bodyB.invInertia;
139
+ const comAx = bodyA.worldCenterOfMassX;
140
+ const comAy = bodyA.worldCenterOfMassY;
141
+ const comBx = bodyB.worldCenterOfMassX;
142
+ const comBy = bodyB.worldCenterOfMassY;
143
+ for (let i = 0; i < pointCount; i++) {
144
+ const point = i === 0 ? manifold.points[0] : manifold.points[1];
145
+ const rAx = point.x - comAx;
146
+ const rAy = point.y - comAy;
147
+ const rBx = point.x - comBx;
148
+ const rBy = point.y - comBy;
149
+ constraint.rAx[i] = rAx;
150
+ constraint.rAy[i] = rAy;
151
+ constraint.rBx[i] = rBx;
152
+ constraint.rBy[i] = rBy;
153
+ constraint.rotAx[i] = rAx;
154
+ constraint.rotAy[i] = rAy;
155
+ constraint.rotBx[i] = rBx;
156
+ constraint.rotBy[i] = rBy;
157
+ const rnA = rAx * ny - rAy * nx;
158
+ const rnB = rBx * ny - rBy * nx;
159
+ const kn = mA + mB + iA * rnA * rnA + iB * rnB * rnB;
160
+ const rtA = rAx * ty - rAy * tx;
161
+ const rtB = rBx * ty - rBy * tx;
162
+ const kt = mA + mB + iA * rtA * rtA + iB * rtB * rtB;
163
+ constraint.normalMass[i] = kn > 0 ? 1 / kn : 0;
164
+ constraint.tangentMass[i] = kt > 0 ? 1 / kt : 0;
165
+ constraint.baseSeparation[i] = -point.penetration;
166
+ constraint.velocityBias[i] = 0;
167
+ constraint.relativeVelocity[i] = normalVelocity(bodyA, bodyB, rAx, rAy, rBx, rBy, nx, ny);
168
+ }
169
+ if (pointCount === 2) {
170
+ const rn1A = n(constraint.rAx, 0) * ny - n(constraint.rAy, 0) * nx;
171
+ const rn1B = n(constraint.rBx, 0) * ny - n(constraint.rBy, 0) * nx;
172
+ const rn2A = n(constraint.rAx, 1) * ny - n(constraint.rAy, 1) * nx;
173
+ const rn2B = n(constraint.rBx, 1) * ny - n(constraint.rBy, 1) * nx;
174
+ const k11 = mA + mB + iA * rn1A * rn1A + iB * rn1B * rn1B;
175
+ const k22 = mA + mB + iA * rn2A * rn2A + iB * rn2B * rn2B;
176
+ const k12 = mA + mB + iA * rn1A * rn2A + iB * rn1B * rn2B;
177
+ const det = k11 * k22 - k12 * k12;
178
+ if (det > 0 && k11 * k11 < maxConditionNumber * det) {
179
+ const invDet = 1 / det;
180
+ constraint.k11 = k11;
181
+ constraint.k12 = k12;
182
+ constraint.k22 = k22;
183
+ constraint.invK11 = invDet * k22;
184
+ constraint.invK12 = -invDet * k12;
185
+ constraint.invK22 = invDet * k11;
186
+ constraint.block = true;
187
+ }
188
+ }
189
+ }
190
+ }
191
+ /** Re-apply the cached impulses from the previous frame (warm-starting); runs each sub-step. */
192
+ warmStart() {
193
+ for (let ci = 0; ci < this._count; ci++) {
194
+ const constraint = this._at(ci);
195
+ const bodyA = constraint.bodyA;
196
+ const bodyB = constraint.bodyB;
197
+ this._updateArms(constraint);
198
+ for (let i = 0; i < constraint.pointCount; i++) {
199
+ const normalImpulse = n(constraint.record.normalImpulse, i);
200
+ const tangentImpulse = n(constraint.record.tangentImpulse, i);
201
+ const jx = normalImpulse * constraint.nx + tangentImpulse * constraint.tx;
202
+ const jy = normalImpulse * constraint.ny + tangentImpulse * constraint.ty;
203
+ applyImpulse(bodyA, bodyB, n(constraint.rotAx, i), n(constraint.rotAy, i), n(constraint.rotBx, i), n(constraint.rotBy, i), jx, jy);
204
+ }
205
+ }
206
+ }
207
+ /**
208
+ * One velocity pass over every contact: the normal constraint (block when
209
+ * possible) then friction (Coulomb cone). With `useBias` the normal solve
210
+ * carries the soft push-out bias (the main pass, recomputed from the live
211
+ * separation); without it the relax pass drives the normal velocity to zero so
212
+ * the bias velocity does not remain in the bodies as injected energy.
213
+ */
214
+ solveVelocities(useBias) {
215
+ for (let ci = 0; ci < this._count; ci++) this._solveVelocityContact(this._at(ci), useBias);
216
+ }
217
+ /**
218
+ * Restitution pass, run once after the sub-step loop. For each loaded point
219
+ * whose frame-start impact speed exceeded the threshold, add the impulse that
220
+ * brings the post-solve normal velocity to `−restitution·impactSpeed`. Kept
221
+ * separate from the main solve so resting contacts (impact below threshold) do
222
+ * not micro-bounce.
223
+ */
224
+ applyRestitution() {
225
+ for (let ci = 0; ci < this._count; ci++) {
226
+ const constraint = this._at(ci);
227
+ if (constraint.restitution === 0) continue;
228
+ const bodyA = constraint.bodyA;
229
+ const bodyB = constraint.bodyB;
230
+ const nx = constraint.nx;
231
+ const ny = constraint.ny;
232
+ this._updateArms(constraint);
233
+ for (let i = 0; i < constraint.pointCount; i++) {
234
+ const relativeVelocity = n(constraint.relativeVelocity, i);
235
+ if (relativeVelocity > -1 || n(constraint.record.normalImpulse, i) <= 0) continue;
236
+ const rAx = n(constraint.rotAx, i);
237
+ const rAy = n(constraint.rotAy, i);
238
+ const rBx = n(constraint.rotBx, i);
239
+ const rBy = n(constraint.rotBy, i);
240
+ const vn = normalVelocity(bodyA, bodyB, rAx, rAy, rBx, rBy, nx, ny);
241
+ const oldNormal = n(constraint.record.normalImpulse, i);
242
+ const newNormal = Math.max(0, oldNormal - n(constraint.normalMass, i) * (vn + constraint.restitution * relativeVelocity));
243
+ const deltaNormal = newNormal - oldNormal;
244
+ constraint.record.normalImpulse[i] = newNormal;
245
+ applyImpulse(bodyA, bodyB, rAx, rAy, rBx, rBy, deltaNormal * nx, deltaNormal * ny);
246
+ }
247
+ }
248
+ }
249
+ /** Refresh the live (rotated) contact arms from each body's accumulated sub-step rotation. */
250
+ _updateArms(constraint) {
251
+ const cosA = constraint.bodyA._deltaCos;
252
+ const sinA = constraint.bodyA._deltaSin;
253
+ const cosB = constraint.bodyB._deltaCos;
254
+ const sinB = constraint.bodyB._deltaSin;
255
+ for (let i = 0; i < constraint.pointCount; i++) {
256
+ const rAx = n(constraint.rAx, i);
257
+ const rAy = n(constraint.rAy, i);
258
+ const rBx = n(constraint.rBx, i);
259
+ const rBy = n(constraint.rBy, i);
260
+ constraint.rotAx[i] = rAx * cosA - rAy * sinA;
261
+ constraint.rotAy[i] = rAx * sinA + rAy * cosA;
262
+ constraint.rotBx[i] = rBx * cosB - rBy * sinB;
263
+ constraint.rotBy[i] = rBx * sinB + rBy * cosB;
264
+ }
265
+ }
266
+ _solveVelocityContact(constraint, useBias) {
267
+ this._updateArms(constraint);
268
+ for (let i = 0; i < constraint.pointCount; i++) {
269
+ const excess = -currentSeparation(constraint, i) - contactSlop;
270
+ const target = useBias && excess > 0 ? constraint.biasRate * excess : 0;
271
+ constraint.velocityBias[i] = Math.min(target, maxBiasVelocity);
272
+ constraint.biasClamped[i] = target > maxBiasVelocity;
273
+ }
274
+ if (constraint.block) this._solveNormalBlock(constraint);
275
+ else this._solveNormalSequential(constraint, useBias);
276
+ this._solveFriction(constraint);
277
+ }
278
+ /** Per-point friction: clamp the accumulated tangent impulse to the Coulomb cone `±μ·normalImpulse`. */
279
+ _solveFriction(constraint) {
280
+ const bodyA = constraint.bodyA;
281
+ const bodyB = constraint.bodyB;
282
+ for (let i = 0; i < constraint.pointCount; i++) {
283
+ const rAx = n(constraint.rotAx, i);
284
+ const rAy = n(constraint.rotAy, i);
285
+ const rBx = n(constraint.rotBx, i);
286
+ const rBy = n(constraint.rotBy, i);
287
+ const vtX = bodyB.linearVelocityX - bodyB.angularVelocity * rBy - (bodyA.linearVelocityX - bodyA.angularVelocity * rAy);
288
+ const vtY = bodyB.linearVelocityY + bodyB.angularVelocity * rBx - (bodyA.linearVelocityY + bodyA.angularVelocity * rAx);
289
+ const vt = vtX * constraint.tx + vtY * constraint.ty;
290
+ const maxFriction = constraint.friction * n(constraint.record.normalImpulse, i);
291
+ const oldTangent = n(constraint.record.tangentImpulse, i);
292
+ const newTangent = clamp(oldTangent - n(constraint.tangentMass, i) * vt, -maxFriction, maxFriction);
293
+ const deltaTangent = newTangent - oldTangent;
294
+ constraint.record.tangentImpulse[i] = newTangent;
295
+ applyImpulse(bodyA, bodyB, rAx, rAy, rBx, rBy, deltaTangent * constraint.tx, deltaTangent * constraint.ty);
296
+ }
297
+ }
298
+ /**
299
+ * Single-point (or ill-conditioned) soft normal solve, accumulated and clamped
300
+ * ≥ 0. The Box2D-v3 incremental form: with the soft bias active the impulse is
301
+ * `−normalMass·massScale·(vn − bias) − impulseScale·accumulated`, which both
302
+ * pushes out the penetration and bleeds a little stored impulse (the damped
303
+ * spring). The relax pass passes `useBias=false`, collapsing it to the hard
304
+ * `−normalMass·vn` (no bias, full mass, no impulse decay).
305
+ *
306
+ * A point whose push-out target was clamped to `maxBiasVelocity` is solved
307
+ * with the hard scale as well: the soft scaling models a spring, and once the
308
+ * target is capped the point would otherwise reach only a fraction of an
309
+ * already limited push-out. That deficit grows with the load, so past the
310
+ * acceleration at which the cap binds the contact would sink without bound
311
+ * instead of settling into a slightly deeper limit cycle.
312
+ */
313
+ _solveNormalSequential(constraint, useBias) {
314
+ const bodyA = constraint.bodyA;
315
+ const bodyB = constraint.bodyB;
316
+ const nx = constraint.nx;
317
+ const ny = constraint.ny;
318
+ for (let i = 0; i < constraint.pointCount; i++) {
319
+ const rAx = n(constraint.rotAx, i);
320
+ const rAy = n(constraint.rotAy, i);
321
+ const rBx = n(constraint.rotBx, i);
322
+ const rBy = n(constraint.rotBy, i);
323
+ const vn = normalVelocity(bodyA, bodyB, rAx, rAy, rBx, rBy, nx, ny);
324
+ const oldNormal = n(constraint.record.normalImpulse, i);
325
+ const clamped = constraint.biasClamped[i] ?? false;
326
+ const massScale = useBias && !clamped ? constraint.massScale : 1;
327
+ const impulseScale = useBias && !clamped ? constraint.impulseScale : 0;
328
+ const impulse = -n(constraint.normalMass, i) * massScale * (vn - n(constraint.velocityBias, i)) - impulseScale * oldNormal;
329
+ const newNormal = Math.max(0, oldNormal + impulse);
330
+ const deltaNormal = newNormal - oldNormal;
331
+ constraint.record.normalImpulse[i] = newNormal;
332
+ applyImpulse(bodyA, bodyB, rAx, rAy, rBx, rBy, deltaNormal * nx, deltaNormal * ny);
333
+ }
334
+ }
335
+ /**
336
+ * Two-point block normal solve (Box2D LCP): find both new normal impulses
337
+ * `x 0` so the post-solve normal velocities equal the push-out targets
338
+ * (`velocityBias`, zero in the relax pass) and are complementary, trying the
339
+ * four corners (both active, one active, none). The block path carries the
340
+ * soft push-out as its target but solves with a hard mass scale.
341
+ */
342
+ _solveNormalBlock(constraint) {
343
+ const bodyA = constraint.bodyA;
344
+ const bodyB = constraint.bodyB;
345
+ const nx = constraint.nx;
346
+ const ny = constraint.ny;
347
+ const a1 = n(constraint.record.normalImpulse, 0);
348
+ const a2 = n(constraint.record.normalImpulse, 1);
349
+ const vn1 = normalVelocity(bodyA, bodyB, n(constraint.rotAx, 0), n(constraint.rotAy, 0), n(constraint.rotBx, 0), n(constraint.rotBy, 0), nx, ny);
350
+ const vn2 = normalVelocity(bodyA, bodyB, n(constraint.rotAx, 1), n(constraint.rotAy, 1), n(constraint.rotBx, 1), n(constraint.rotBy, 1), nx, ny);
351
+ const bx = vn1 - n(constraint.velocityBias, 0) - (constraint.k11 * a1 + constraint.k12 * a2);
352
+ const by = vn2 - n(constraint.velocityBias, 1) - (constraint.k12 * a1 + constraint.k22 * a2);
353
+ let x1 = -(constraint.invK11 * bx + constraint.invK12 * by);
354
+ let x2 = -(constraint.invK12 * bx + constraint.invK22 * by);
355
+ let solved = x1 >= 0 && x2 >= 0;
356
+ if (!solved) {
357
+ x1 = -n(constraint.normalMass, 0) * bx;
358
+ x2 = 0;
359
+ solved = x1 >= 0 && constraint.k12 * x1 + by >= 0;
360
+ }
361
+ if (!solved) {
362
+ x1 = 0;
363
+ x2 = -n(constraint.normalMass, 1) * by;
364
+ solved = x2 >= 0 && constraint.k12 * x2 + bx >= 0;
365
+ }
366
+ if (!solved) {
367
+ if (bx < 0 || by < 0) return;
368
+ x1 = 0;
369
+ x2 = 0;
370
+ }
371
+ const d1 = x1 - a1;
372
+ const d2 = x2 - a2;
373
+ const jx = (d1 + d2) * nx;
374
+ const jy = (d1 + d2) * ny;
375
+ const torqueA = (n(constraint.rotAx, 0) * ny - n(constraint.rotAy, 0) * nx) * d1 + (n(constraint.rotAx, 1) * ny - n(constraint.rotAy, 1) * nx) * d2;
376
+ const torqueB = (n(constraint.rotBx, 0) * ny - n(constraint.rotBy, 0) * nx) * d1 + (n(constraint.rotBx, 1) * ny - n(constraint.rotBy, 1) * nx) * d2;
377
+ bodyA.linearVelocityX -= jx * bodyA.invMass;
378
+ bodyA.linearVelocityY -= jy * bodyA.invMass;
379
+ bodyA.angularVelocity -= torqueA * bodyA.invInertia;
380
+ bodyB.linearVelocityX += jx * bodyB.invMass;
381
+ bodyB.linearVelocityY += jy * bodyB.invMass;
382
+ bodyB.angularVelocity += torqueB * bodyB.invInertia;
383
+ constraint.record.normalImpulse[0] = x1;
384
+ constraint.record.normalImpulse[1] = x2;
385
+ }
386
+ _acquire() {
387
+ if (this._count === this._constraints.length) this._constraints.push(new ContactConstraint());
388
+ return this._at(this._count++);
389
+ }
390
+ /**
391
+ * Pooled constraint at `ci`. Every caller indexes within `0..this._count-1`
392
+ * and `_count` never exceeds the pool length, so the entry always exists; the
393
+ * throw is unreachable and only discharges `noUncheckedIndexedAccess` without a
394
+ * cast or non-null assertion.
395
+ */
396
+ _at(ci) {
397
+ const constraint = this._constraints[ci];
398
+ if (constraint === void 0) throw new RangeError(`ContactSolver: constraint pool index ${ci} out of range.`);
399
+ return constraint;
400
+ }
401
+ };
448
402
  /**
449
- * Live contact separation at point `i`, recomputed from each body's accumulated
450
- * delta position plus the rotation-induced shift of the (rotated) anchor since
451
- * frame start, projected onto the normal. Negative when overlapping. This is what
452
- * folds the position constraint into the velocity solve without a separate
453
- * geometric pass or re-running detection per sub-step.
454
- */
403
+ * Live contact separation at point `i`, recomputed from each body's accumulated
404
+ * delta position plus the rotation-induced shift of the (rotated) anchor since
405
+ * frame start, projected onto the normal. Negative when overlapping. This is what
406
+ * folds the position constraint into the velocity solve without a separate
407
+ * geometric pass or re-running detection per sub-step.
408
+ */
455
409
  const currentSeparation = (constraint, i) => {
456
- const bodyA = constraint.bodyA;
457
- const bodyB = constraint.bodyB;
458
- // Anchor displacement since frame start = linear delta + (rotated arm base arm).
459
- const dispAx = bodyA._deltaPosX + n(constraint.rotAx, i) - n(constraint.rAx, i);
460
- const dispAy = bodyA._deltaPosY + n(constraint.rotAy, i) - n(constraint.rAy, i);
461
- const dispBx = bodyB._deltaPosX + n(constraint.rotBx, i) - n(constraint.rBx, i);
462
- const dispBy = bodyB._deltaPosY + n(constraint.rotBy, i) - n(constraint.rBy, i);
463
- return n(constraint.baseSeparation, i) + (dispBx - dispAx) * constraint.nx + (dispBy - dispAy) * constraint.ny;
410
+ const bodyA = constraint.bodyA;
411
+ const bodyB = constraint.bodyB;
412
+ const dispAx = bodyA._deltaPosX + n(constraint.rotAx, i) - n(constraint.rAx, i);
413
+ const dispAy = bodyA._deltaPosY + n(constraint.rotAy, i) - n(constraint.rAy, i);
414
+ const dispBx = bodyB._deltaPosX + n(constraint.rotBx, i) - n(constraint.rBx, i);
415
+ const dispBy = bodyB._deltaPosY + n(constraint.rotBy, i) - n(constraint.rBy, i);
416
+ return n(constraint.baseSeparation, i) + (dispBx - dispAx) * constraint.nx + (dispBy - dispAy) * constraint.ny;
464
417
  };
465
418
  /** Relative normal velocity at a contact point: `dot(vB + ωB×rB − vA − ωA×rA, n)`. */
466
419
  const normalVelocity = (bodyA, bodyB, rAx, rAy, rBx, rBy, nx, ny) => {
467
- const dvx = bodyB.linearVelocityX - bodyB.angularVelocity * rBy - (bodyA.linearVelocityX - bodyA.angularVelocity * rAy);
468
- const dvy = bodyB.linearVelocityY + bodyB.angularVelocity * rBx - (bodyA.linearVelocityY + bodyA.angularVelocity * rAx);
469
- return dvx * nx + dvy * ny;
420
+ const dvx = bodyB.linearVelocityX - bodyB.angularVelocity * rBy - (bodyA.linearVelocityX - bodyA.angularVelocity * rAy);
421
+ const dvy = bodyB.linearVelocityY + bodyB.angularVelocity * rBx - (bodyA.linearVelocityY + bodyA.angularVelocity * rAx);
422
+ return dvx * nx + dvy * ny;
470
423
  };
471
424
  /** Apply impulse `(jx, jy)` to B and its negation to A about their contact arms. */
472
425
  const applyImpulse = (bodyA, bodyB, rAx, rAy, rBx, rBy, jx, jy) => {
473
- bodyA.linearVelocityX -= jx * bodyA.invMass;
474
- bodyA.linearVelocityY -= jy * bodyA.invMass;
475
- bodyA.angularVelocity -= (rAx * jy - rAy * jx) * bodyA.invInertia;
476
- bodyB.linearVelocityX += jx * bodyB.invMass;
477
- bodyB.linearVelocityY += jy * bodyB.invMass;
478
- bodyB.angularVelocity += (rBx * jy - rBy * jx) * bodyB.invInertia;
426
+ bodyA.linearVelocityX -= jx * bodyA.invMass;
427
+ bodyA.linearVelocityY -= jy * bodyA.invMass;
428
+ bodyA.angularVelocity -= (rAx * jy - rAy * jx) * bodyA.invInertia;
429
+ bodyB.linearVelocityX += jx * bodyB.invMass;
430
+ bodyB.linearVelocityY += jy * bodyB.invMass;
431
+ bodyB.angularVelocity += (rBx * jy - rBy * jx) * bodyB.invInertia;
479
432
  };
480
433
  const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
481
434
  /**
482
- * In-bounds read of a fixed-size (length-2) per-point scratch array. Every
483
- * caller indexes within `0..pointCount-1` (pointCount ≤ 2), so the element
484
- * always exists; the `0` fallback only discharges `noUncheckedIndexedAccess`
485
- * for the unreachable case and keeps the `??` out of caller complexity.
486
- */
435
+ * In-bounds read of a fixed-size (length-2) per-point scratch array. Every
436
+ * caller indexes within `0..pointCount-1` (pointCount ≤ 2), so the element
437
+ * always exists; the `0` fallback only discharges `noUncheckedIndexedAccess`
438
+ * for the unreachable case and keeps the `??` out of caller complexity.
439
+ */
487
440
  const n = (arr, i) => arr[i] ?? 0;
488
441
 
442
+ //#endregion
489
443
  export { ContactSolver };
490
- //# sourceMappingURL=ContactSolver.js.map
444
+ //# sourceMappingURL=ContactSolver.js.map