@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,447 +1,465 @@
1
- import { Vector } from '@codexo/exojs';
2
- import { Collider } from './Collider.js';
3
- import { createTransform, setTransform, applyTransform } from './math.js';
1
+ import { applyTransform, createTransform, setTransform } from "./math.js";
2
+ import { Collider } from "./Collider.js";
3
+ import { Vector } from "@codexo/exojs";
4
4
 
5
+ //#region src/PhysicsBody.ts
5
6
  /**
6
- * A rigid body: a world transform plus mass properties aggregated from its
7
- * colliders. Dynamic bodies integrate under gravity, accumulated forces/torque
8
- * and contact impulses each fixed sub-step; static bodies never move; kinematic
9
- * bodies move by their velocity only and stay immovable under contacts. Drive a
10
- * body with {@link applyForce}, {@link applyTorque} and {@link applyImpulse}, or
11
- * set {@link linearVelocityX}/{@link linearVelocityY}/{@link angularVelocity}
12
- * directly; reposition it (teleport, no velocity) with {@link setTransform}.
13
- */
14
- class PhysicsBody {
15
- type;
16
- /** Linear damping applied to the velocity each sub-step. */
17
- linearDamping;
18
- /** Angular damping applied to the angular velocity each sub-step. */
19
- angularDamping;
20
- /** Per-body multiplier on world gravity (e.g. `0` to ignore gravity). */
21
- gravityScale;
22
- /** When `true`, rotational inertia is treated as infinite. */
23
- fixedRotation;
24
- /** When `true`, the body is swept against static geometry each step (CCD) so it cannot tunnel through thin walls. */
25
- isBullet;
26
- /** Total mass (0 for static/kinematic). */
27
- mass = 0;
28
- /** Inverse mass (`0` = immovable). */
29
- invMass = 0;
30
- /** Rotational inertia about the centre of mass (0 for static/kinematic or fixed rotation). */
31
- inertia = 0;
32
- /** Inverse rotational inertia (`0` = no angular response). */
33
- invInertia = 0;
34
- /** Linear velocity X in px/s. */
35
- linearVelocityX = 0;
36
- /** Linear velocity Y in px/s. */
37
- linearVelocityY = 0;
38
- /** Angular velocity in rad/s. */
39
- angularVelocity = 0;
40
- /** When `false`, this body is never put to sleep. Default `true`. */
41
- allowSleep = true;
42
- /** @internal Delta position X accumulated across the frame's sub-steps by the TGS integrator; written into the transform once per frame by {@link _finalizePosition}. */
43
- _deltaPosX = 0;
44
- /** @internal Delta position Y accumulated across the frame's sub-steps by the TGS integrator. */
45
- _deltaPosY = 0;
46
- /** @internal Delta rotation (radians) accumulated across the frame's sub-steps by the TGS integrator. */
47
- _deltaAngle = 0;
48
- /** @internal `cos(_deltaAngle)`, cached so the solver can rotate the contact anchors by the live sub-step rotation without a per-contact trig call. */
49
- _deltaCos = 1;
50
- /** @internal `sin(_deltaAngle)`, cached alongside {@link _deltaCos}. */
51
- _deltaSin = 0;
52
- _forceX = 0;
53
- _forceY = 0;
54
- _torque = 0;
55
- /** @internal seconds the body has stayed below the sleep thresholds (frozen while asleep). Read by the world's island pass. */
56
- _sleepTime = 0;
57
- /** @internal dense union-find index assigned by the world's island pass each step. */
58
- _islandIndex = 0;
59
- /** @internal — world-space centre of mass at the start of the current fixed step (CCD swept-test origin). */
60
- _ccdPrevX = 0;
61
- /** @internal see {@link _ccdPrevX}. */
62
- _ccdPrevY = 0;
63
- _sleeping = false;
64
- _id = -1;
65
- _owner = null;
66
- _attached = false;
67
- _transform;
68
- _colliders = [];
69
- _comX = 0;
70
- _comY = 0;
71
- _massReady = false;
72
- _destroyed = false;
73
- constructor(options = {}) {
74
- this.type = options.type ?? 'dynamic';
75
- this._transform = createTransform(options.position?.x ?? 0, options.position?.y ?? 0, options.angle ?? 0);
76
- this.linearDamping = options.linearDamping ?? 0;
77
- this.angularDamping = options.angularDamping ?? 0;
78
- this.gravityScale = options.gravityScale ?? 1;
79
- this.fixedRotation = options.fixedRotation ?? false;
80
- this.isBullet = options.isBullet ?? false;
81
- // Build up-front colliders now (no id/world registration until the body
82
- // joins a world via `world.add()`). They sit in `_colliders` unsynchronised;
83
- // `_attachToWorld` assigns ids, registers them and computes the mass model.
84
- if (options.colliders) {
85
- for (const entry of options.colliders) {
86
- this._colliders.push(entry instanceof Collider ? entry : new Collider(entry));
87
- }
88
- }
89
- }
90
- /** Stable id, assigned when the body joins a world via `world.add()`; `-1` until then. */
91
- get id() {
92
- return this._id;
93
- }
94
- /** `true` once the body has been added to a world (guards against double-add). */
95
- get attached() {
96
- return this._attached;
97
- }
98
- /** World X. */
99
- get x() {
100
- return this._transform.x;
101
- }
102
- /** World Y. */
103
- get y() {
104
- return this._transform.y;
105
- }
106
- /** Rotation in radians. */
107
- get angle() {
108
- return this._transform.angle;
109
- }
110
- /** A fresh `Vector` copy of the body's world position. */
111
- get position() {
112
- return new Vector(this._transform.x, this._transform.y);
113
- }
114
- /** The body's world transform (read-only; mutate via {@link setTransform}). */
115
- get transform() {
116
- return this._transform;
117
- }
118
- /** The body's colliders (read-only view). */
119
- get colliders() {
120
- return this._colliders;
121
- }
122
- /** Centre of mass in body-local space (relative to the body origin). */
123
- get centerOfMassX() {
124
- return this._comX;
125
- }
126
- /** Centre of mass in body-local space (relative to the body origin). */
127
- get centerOfMassY() {
128
- return this._comY;
129
- }
130
- /**
131
- * `true` when the body is ready to simulate. Static/kinematic bodies are
132
- * always ready (infinite-mass semantics); a dynamic body is ready only once
133
- * it has at least one positive-density collider.
134
- */
135
- get isMassReady() {
136
- return this.type !== 'dynamic' || this._massReady;
137
- }
138
- /** `true` after the owning world has destroyed this body. */
139
- get destroyed() {
140
- return this._destroyed;
141
- }
142
- /** `true` when the body is asleep — skipped by the integrator and solver until woken. */
143
- get isSleeping() {
144
- return this._sleeping;
145
- }
146
- /**
147
- * Attach a collider to this body. Accepts a {@link Collider} instance or its
148
- * {@link ColliderOptions} (a convenience that constructs the collider for you).
149
- * When the body is already in a world, the collider is id-allocated, registered,
150
- * the mass model is recomputed and the world geometry synchronised immediately;
151
- * on a free-standing body the collider is simply held until `world.add()`.
152
- * Returns the collider.
153
- */
154
- addCollider(collider) {
155
- if (this._destroyed) {
156
- throw new Error('PhysicsBody: cannot add a collider to a destroyed body.');
157
- }
158
- // Collider imports PhysicsBody type-only (erased), so this value import is
159
- // one-directional no runtime cycle.
160
- const instance = collider instanceof Collider ? collider : new Collider(collider);
161
- this._colliders.push(instance);
162
- // Before the body joins a world there is no owner to allocate ids / register
163
- // with; `_attachToWorld` does that (and the mass/sync pass) for every held
164
- // collider. Once attached, mirror the world's create order exactly.
165
- if (this._attached && this._owner) {
166
- instance._attach(this, this._owner._allocateColliderId());
167
- this._recomputeMass();
168
- instance.synchronize(this._transform);
169
- this._owner._registerCollider(instance);
170
- }
171
- return instance;
172
- }
173
- /**
174
- * Set the body's world transform. Resets the body's cached collider geometry
175
- * immediately so subsequent queries see the new placement. Returns `this`.
176
- */
177
- setTransform(position, angle = this._transform.angle) {
178
- if (this._destroyed) {
179
- throw new Error('PhysicsBody: cannot move a destroyed body.');
180
- }
181
- this.wake();
182
- setTransform(this._transform, position.x, position.y, angle);
183
- for (const collider of this._colliders) {
184
- collider.synchronize(this._transform);
185
- }
186
- return this;
187
- }
188
- /** Refresh every collider's world geometry from the current transform. */
189
- synchronizeColliders() {
190
- for (const collider of this._colliders) {
191
- collider.synchronize(this._transform);
192
- }
193
- }
194
- /** World-space centre of mass X (body origin + rotated local centre of mass). */
195
- get worldCenterOfMassX() {
196
- return this._transform.x + this._transform.cos * this._comX - this._transform.sin * this._comY;
197
- }
198
- /** World-space centre of mass Y. */
199
- get worldCenterOfMassY() {
200
- return this._transform.y + this._transform.sin * this._comX + this._transform.cos * this._comY;
201
- }
202
- /**
203
- * Accumulate a world-space force at the centre of mass; integrated on the next
204
- * sub-step and then cleared. No-op on static/kinematic bodies. Returns `this`.
205
- */
206
- applyForce(forceX, forceY) {
207
- this.wake();
208
- this._forceX += forceX;
209
- this._forceY += forceY;
210
- return this;
211
- }
212
- /**
213
- * Accumulate a torque; integrated on the next sub-step and then cleared. No-op
214
- * on static/kinematic bodies (and fixed-rotation bodies). Returns `this`.
215
- */
216
- applyTorque(torque) {
217
- this.wake();
218
- this._torque += torque;
219
- return this;
220
- }
221
- /**
222
- * Apply an instantaneous world-space impulse at `(pointX, pointY)` (world space;
223
- * defaults to the centre of mass), changing velocity immediately. No-op on
224
- * static/kinematic bodies (infinite mass). Returns `this`.
225
- */
226
- applyImpulse(impulseX, impulseY, pointX, pointY) {
227
- if (this.invMass === 0) {
228
- return this;
229
- }
230
- this.wake();
231
- this.linearVelocityX += impulseX * this.invMass;
232
- this.linearVelocityY += impulseY * this.invMass;
233
- if (pointX !== undefined && pointY !== undefined && this.invInertia !== 0) {
234
- const rx = pointX - this.worldCenterOfMassX;
235
- const ry = pointY - this.worldCenterOfMassY;
236
- this.angularVelocity += (rx * impulseY - ry * impulseX) * this.invInertia;
237
- }
238
- return this;
239
- }
240
- /**
241
- * @internal integrate velocity from gravity, accumulated forces/torque and
242
- * damping over the sub-step `h`. No-op for static/kinematic (infinite mass
243
- * keeps their velocity solver-driven only). Called once per TGS sub-step, so
244
- * gravity/forces are applied with `h` each sub-step (`N·h = dt`, same total
245
- * impulse, but the small-step position error scales with `h²`). The
246
- * force/torque accumulators are **not** cleared here — they are consumed by
247
- * every sub-step and cleared once per frame by {@link _finalizePosition}.
248
- */
249
- _integrateVelocity(h, gravityX, gravityY) {
250
- if (this.invMass === 0 || this._sleeping) {
251
- return;
252
- }
253
- this.linearVelocityX += (gravityX * this.gravityScale + this._forceX * this.invMass) * h;
254
- this.linearVelocityY += (gravityY * this.gravityScale + this._forceY * this.invMass) * h;
255
- this.angularVelocity += this._torque * this.invInertia * h;
256
- // Exponential damping (no-op when damping is 0).
257
- this.linearVelocityX /= 1 + h * this.linearDamping;
258
- this.linearVelocityY /= 1 + h * this.linearDamping;
259
- this.angularVelocity /= 1 + h * this.angularDamping;
260
- }
261
- /**
262
- * @internal accumulate this sub-step's velocity into the per-frame delta
263
- * position/rotation (TGS sub-stepping). The transform itself is **not** moved
264
- * here; {@link _finalizePosition} writes the accumulated delta once per frame.
265
- * Tracking the delta (rather than moving the transform each sub-step) lets the
266
- * solver recompute contact separation from it without re-running detection or
267
- * re-syncing collider geometry per sub-step. Static bodies never move.
268
- */
269
- _integratePosition(h) {
270
- if (this.type === 'static' || this._sleeping) {
271
- return;
272
- }
273
- this._deltaPosX += this.linearVelocityX * h;
274
- this._deltaPosY += this.linearVelocityY * h;
275
- this._deltaAngle += this.angularVelocity * h;
276
- // Cache the rotation so the solver can rotate the contact anchors by the
277
- // live sub-step rotation (TGS): without it the anchors stay frozen at frame
278
- // start and a tilting tower's restoring torque is mis-computed, so the tilt
279
- // grows instead of being corrected. Only non-zero rotation pays the trig.
280
- if (this._deltaAngle !== 0) {
281
- this._deltaCos = Math.cos(this._deltaAngle);
282
- this._deltaSin = Math.sin(this._deltaAngle);
283
- }
284
- }
285
- /**
286
- * @internal advance the sleep timer over one fixed step `dt`. A body below
287
- * both velocity thresholds accumulates time; a too-fast body, or one that opts
288
- * out via {@link allowSleep}, resets it. The sleep/wake decision is made per
289
- * island by the world (so a stack sleeps as a unit) from {@link _sleepTime}.
290
- */
291
- _accumulateSleepTime(dt, linearThreshold, angularThreshold) {
292
- const tooFast = this.linearVelocityX * this.linearVelocityX + this.linearVelocityY * this.linearVelocityY > linearThreshold * linearThreshold ||
293
- Math.abs(this.angularVelocity) > angularThreshold;
294
- this._sleepTime = !this.allowSleep || tooFast ? 0 : this._sleepTime + dt;
295
- }
296
- /** @internal set the sleep state. Sleeping zeroes the velocity; waking resets the sleep timer. */
297
- _setSleeping(sleeping) {
298
- if (this._sleeping === sleeping) {
299
- return;
300
- }
301
- this._sleeping = sleeping;
302
- if (sleeping) {
303
- this.linearVelocityX = 0;
304
- this.linearVelocityY = 0;
305
- this.angularVelocity = 0;
306
- }
307
- else {
308
- this._sleepTime = 0;
309
- }
310
- }
311
- /**
312
- * Wake the body if it is asleep, resetting its sleep timer. The rest of its
313
- * island wakes with it on the next step (a contact to an awake body keeps the
314
- * whole island awake). Returns `this`.
315
- */
316
- wake() {
317
- this._setSleeping(false);
318
- this._sleepTime = 0;
319
- return this;
320
- }
321
- /**
322
- * @internal — apply the frame's accumulated delta position/rotation to the
323
- * transform (rotating about the centre of mass), re-sync collider geometry and
324
- * clear the force/torque accumulators. Called once per frame after the
325
- * sub-step loop. Static bodies never move; the force clear still runs (forces
326
- * are a no-op on infinite mass but the accumulator is reset for consistency).
327
- */
328
- _finalizePosition() {
329
- this._forceX = 0;
330
- this._forceY = 0;
331
- this._torque = 0;
332
- if (this.type === 'static' || (this._deltaPosX === 0 && this._deltaPosY === 0 && this._deltaAngle === 0)) {
333
- this._resetDelta();
334
- return;
335
- }
336
- const newComX = this.worldCenterOfMassX + this._deltaPosX;
337
- const newComY = this.worldCenterOfMassY + this._deltaPosY;
338
- const newAngle = this._transform.angle + this._deltaAngle;
339
- const cos = Math.cos(newAngle);
340
- const sin = Math.sin(newAngle);
341
- // origin = newCoM R(newAngle)·comLocal (so rotation pivots about the CoM).
342
- setTransform(this._transform, newComX - (cos * this._comX - sin * this._comY), newComY - (sin * this._comX + cos * this._comY), newAngle);
343
- this._resetDelta();
344
- for (const collider of this._colliders) {
345
- collider.synchronize(this._transform);
346
- }
347
- }
348
- /** Clear the per-frame TGS delta accumulators (position, rotation and its cached cos/sin). */
349
- _resetDelta() {
350
- this._deltaPosX = 0;
351
- this._deltaPosY = 0;
352
- this._deltaAngle = 0;
353
- this._deltaCos = 1;
354
- this._deltaSin = 0;
355
- }
356
- /** Internal: detach a collider (called by the world during destruction). */
357
- _removeCollider(collider) {
358
- const index = this._colliders.indexOf(collider);
359
- if (index !== -1) {
360
- this._colliders.splice(index, 1);
361
- this._recomputeMass();
362
- }
363
- }
364
- /**
365
- * @internal — join `owner`'s world with the allocated `id`. Allocates ids for
366
- * and registers every held collider, then aggregates the mass model and
367
- * synchronises world geometry. Called once by {@link PhysicsWorld.add}.
368
- *
369
- * Ordering matters and mirrors the per-collider create path exactly: ids and
370
- * the body link go on first, then a single mass recompute over the full
371
- * collider set (so the centre of mass / inertia aggregate is correct), then
372
- * `synchronize` over every collider (the cached world geometry the broad/narrow
373
- * phase reads), then world registration last. Recomputing mass before all
374
- * colliders are linked, or syncing before the mass model exists, would feed the
375
- * solver a stale CoM and produce subtle integration bugs.
376
- */
377
- _attachToWorld(owner, id) {
378
- if (this._destroyed) {
379
- throw new Error('PhysicsBody: cannot add a destroyed body to a world.');
380
- }
381
- this._owner = owner;
382
- this._id = id;
383
- this._attached = true;
384
- for (const collider of this._colliders) {
385
- collider._attach(this, owner._allocateColliderId());
386
- }
387
- this._recomputeMass();
388
- for (const collider of this._colliders) {
389
- collider.synchronize(this._transform);
390
- }
391
- for (const collider of this._colliders) {
392
- owner._registerCollider(collider);
393
- }
394
- }
395
- /** Internal: mark destroyed (called by the world). */
396
- _markDestroyed() {
397
- this._destroyed = true;
398
- }
399
- /** Recompute mass, centre of mass and rotational inertia from the colliders. */
400
- _recomputeMass() {
401
- if (this.type !== 'dynamic') {
402
- this.mass = 0;
403
- this.invMass = 0;
404
- this.inertia = 0;
405
- this.invInertia = 0;
406
- this._comX = 0;
407
- this._comY = 0;
408
- this._massReady = false;
409
- return;
410
- }
411
- let mass = 0;
412
- let cx = 0;
413
- let cy = 0;
414
- let inertiaOrigin = 0;
415
- const point = { x: 0, y: 0 };
416
- for (const collider of this._colliders) {
417
- const shape = collider.shape;
418
- const m = collider.density * shape.area;
419
- if (m <= 0) {
420
- continue;
421
- }
422
- // Shape centroid expressed in body-local space (offset + local rotation).
423
- applyTransform(collider.localTransform, shape.centroidX, shape.centroidY, point);
424
- mass += m;
425
- cx += m * point.x;
426
- cy += m * point.y;
427
- // Parallel-axis to the body origin: I_origin += I_centroid + m·d².
428
- inertiaOrigin += collider.density * shape.unitInertia + m * (point.x * point.x + point.y * point.y);
429
- }
430
- if (mass > 0) {
431
- cx /= mass;
432
- cy /= mass;
433
- }
434
- this.mass = mass;
435
- this.invMass = mass > 0 ? 1 / mass : 0;
436
- this._comX = cx;
437
- this._comY = cy;
438
- // Shift inertia from the body origin to the centre of mass.
439
- const inertiaCom = inertiaOrigin - mass * (cx * cx + cy * cy);
440
- this.inertia = this.fixedRotation ? 0 : inertiaCom;
441
- this.invInertia = this.fixedRotation || inertiaCom <= 0 ? 0 : 1 / inertiaCom;
442
- this._massReady = mass > 0;
443
- }
444
- }
7
+ * A rigid body: a world transform plus mass properties aggregated from its
8
+ * colliders. Dynamic bodies integrate under gravity, accumulated forces/torque
9
+ * and contact impulses each fixed sub-step; static bodies never move; kinematic
10
+ * bodies move by their velocity only and stay immovable under contacts. Drive a
11
+ * body with {@link applyForce}, {@link applyTorque} and {@link applyImpulse}, or
12
+ * set {@link linearVelocityX}/{@link linearVelocityY}/{@link angularVelocity}
13
+ * directly; reposition it (teleport, no velocity) with {@link setTransform}.
14
+ */
15
+ var PhysicsBody = class {
16
+ type;
17
+ /** Linear damping applied to the velocity each sub-step. */
18
+ linearDamping;
19
+ /** Angular damping applied to the angular velocity each sub-step. */
20
+ angularDamping;
21
+ /** Per-body multiplier on world gravity (e.g. `0` to ignore gravity). */
22
+ gravityScale;
23
+ /** When `true`, rotational inertia is treated as infinite. */
24
+ fixedRotation;
25
+ /** When `true`, the body's colliders are shape-cast along each step's motion (CCD) so it cannot tunnel through thin geometry. */
26
+ isBullet;
27
+ /** Total mass (0 for static/kinematic). */
28
+ mass = 0;
29
+ /** Inverse mass (`0` = immovable). */
30
+ invMass = 0;
31
+ /** Rotational inertia about the centre of mass (0 for static/kinematic or fixed rotation). */
32
+ inertia = 0;
33
+ /** Inverse rotational inertia (`0` = no angular response). */
34
+ invInertia = 0;
35
+ /** Linear velocity X in px/s. */
36
+ linearVelocityX = 0;
37
+ /** Linear velocity Y in px/s. */
38
+ linearVelocityY = 0;
39
+ /** Angular velocity in rad/s. */
40
+ angularVelocity = 0;
41
+ /** When `false`, this body is never put to sleep. Default `true`. */
42
+ allowSleep = true;
43
+ /** @internal - Delta position X accumulated across the frame's sub-steps by the TGS integrator; written into the transform once per frame by {@link _finalizePosition}. */
44
+ _deltaPosX = 0;
45
+ /** @internal - Delta position Y accumulated across the frame's sub-steps by the TGS integrator. */
46
+ _deltaPosY = 0;
47
+ /** @internal - Delta rotation (radians) accumulated across the frame's sub-steps by the TGS integrator. */
48
+ _deltaAngle = 0;
49
+ /** @internal - `cos(_deltaAngle)`, cached so the solver can rotate the contact anchors by the live sub-step rotation without a per-contact trig call. */
50
+ _deltaCos = 1;
51
+ /** @internal - `sin(_deltaAngle)`, cached alongside {@link _deltaCos}. */
52
+ _deltaSin = 0;
53
+ _forceX = 0;
54
+ _forceY = 0;
55
+ _torque = 0;
56
+ /** @internal - seconds the body has stayed below the sleep thresholds (frozen while asleep). Read by the world's island pass. */
57
+ _sleepTime = 0;
58
+ /** @internal - dense union-find index assigned by the world's island pass each step. */
59
+ _islandIndex = 0;
60
+ /**
61
+ * @internal - raised by {@link setTransform}, cleared once the fixed step that
62
+ * saw it completes. A static/kinematic body driven by teleporting carries no
63
+ * velocity, so this is the world's only signal that such a body moved and its
64
+ * sleeping contact partners must be woken.
65
+ */
66
+ _teleported = false;
67
+ /** @internal - world-space centre of mass at the start of the current fixed step (CCD swept-test origin). */
68
+ _ccdPrevX = 0;
69
+ /** @internal - see {@link _ccdPrevX}. */
70
+ _ccdPrevY = 0;
71
+ _sleeping = false;
72
+ _id = -1;
73
+ _owner = null;
74
+ _attached = false;
75
+ _transform;
76
+ _previousX;
77
+ _previousY;
78
+ _previousAngle;
79
+ _colliders = [];
80
+ _comX = 0;
81
+ _comY = 0;
82
+ _massReady = false;
83
+ _destroyed = false;
84
+ constructor(options = {}) {
85
+ this.type = options.type ?? "dynamic";
86
+ this._transform = createTransform(options.position?.x ?? 0, options.position?.y ?? 0, options.angle ?? 0);
87
+ this._previousX = this._transform.x;
88
+ this._previousY = this._transform.y;
89
+ this._previousAngle = this._transform.angle;
90
+ this.linearDamping = options.linearDamping ?? 0;
91
+ this.angularDamping = options.angularDamping ?? 0;
92
+ this.gravityScale = options.gravityScale ?? 1;
93
+ this.fixedRotation = options.fixedRotation ?? false;
94
+ this.isBullet = options.isBullet ?? false;
95
+ if (options.colliders) for (const entry of options.colliders) this._colliders.push(entry instanceof Collider ? entry : new Collider(entry));
96
+ }
97
+ /** Stable id, assigned when the body joins a world via `world.add()`; `-1` until then. */
98
+ get id() {
99
+ return this._id;
100
+ }
101
+ /** `true` once the body has been added to a world (guards against double-add). */
102
+ get attached() {
103
+ return this._attached;
104
+ }
105
+ /** World X. */
106
+ get x() {
107
+ return this._transform.x;
108
+ }
109
+ /** World Y. */
110
+ get y() {
111
+ return this._transform.y;
112
+ }
113
+ /** Rotation in radians. */
114
+ get angle() {
115
+ return this._transform.angle;
116
+ }
117
+ /**
118
+ * World X at the start of the most recent fixed step. With {@link x} it
119
+ * brackets the last step, which is what an interpolating presentation blends
120
+ * between; a teleport collapses it onto {@link x}. Never read by the solver.
121
+ */
122
+ get previousX() {
123
+ return this._previousX;
124
+ }
125
+ /** World Y at the start of the most recent fixed step; see {@link previousX}. */
126
+ get previousY() {
127
+ return this._previousY;
128
+ }
129
+ /**
130
+ * Rotation in radians at the start of the most recent fixed step; see
131
+ * {@link previousX}. Body angles are continuous and unbounded, never wrapped
132
+ * to a range, so this and {@link angle} can be blended with a plain lerp.
133
+ */
134
+ get previousAngle() {
135
+ return this._previousAngle;
136
+ }
137
+ /** A fresh `Vector` copy of the body's world position. */
138
+ get position() {
139
+ return new Vector(this._transform.x, this._transform.y);
140
+ }
141
+ /** The body's world transform (read-only; mutate via {@link setTransform}). */
142
+ get transform() {
143
+ return this._transform;
144
+ }
145
+ /** The body's colliders (read-only view). */
146
+ get colliders() {
147
+ return this._colliders;
148
+ }
149
+ /** Centre of mass in body-local space (relative to the body origin). */
150
+ get centerOfMassX() {
151
+ return this._comX;
152
+ }
153
+ /** Centre of mass in body-local space (relative to the body origin). */
154
+ get centerOfMassY() {
155
+ return this._comY;
156
+ }
157
+ /**
158
+ * `true` when the body is ready to simulate. Static/kinematic bodies are
159
+ * always ready (infinite-mass semantics); a dynamic body is ready only once
160
+ * it has at least one positive-density collider.
161
+ */
162
+ get isMassReady() {
163
+ return this.type !== "dynamic" || this._massReady;
164
+ }
165
+ /** `true` after the owning world has destroyed this body. */
166
+ get destroyed() {
167
+ return this._destroyed;
168
+ }
169
+ /** `true` when the body is asleep - skipped by the integrator and solver until woken. */
170
+ get isSleeping() {
171
+ return this._sleeping;
172
+ }
173
+ /**
174
+ * Attach a collider to this body. Accepts a {@link Collider} instance or its
175
+ * {@link ColliderOptions} (a convenience that constructs the collider for you).
176
+ * When the body is already in a world, the collider is id-allocated, registered,
177
+ * the mass model is recomputed and the world geometry synchronised immediately;
178
+ * on a free-standing body the collider is simply held until `world.add()`.
179
+ * Returns the collider.
180
+ */
181
+ addCollider(collider) {
182
+ if (this._destroyed) throw new Error("PhysicsBody: cannot add a collider to a destroyed body.");
183
+ const instance = collider instanceof Collider ? collider : new Collider(collider);
184
+ this._colliders.push(instance);
185
+ if (this._attached && this._owner) {
186
+ attachWithChainEdges(instance, this, this._owner);
187
+ this._recomputeMass();
188
+ this._assertDynamicCarriesMass();
189
+ instance.synchronize(this._transform);
190
+ this._owner._registerCollider(instance);
191
+ }
192
+ return instance;
193
+ }
194
+ /**
195
+ * Set the body's world transform. Resets the body's cached collider geometry
196
+ * immediately so subsequent queries see the new placement. Returns `this`.
197
+ */
198
+ setTransform(position, angle = this._transform.angle) {
199
+ if (this._destroyed) throw new Error("PhysicsBody: cannot move a destroyed body.");
200
+ this.wake();
201
+ this._teleported = true;
202
+ setTransform(this._transform, position.x, position.y, angle);
203
+ this._previousX = this._transform.x;
204
+ this._previousY = this._transform.y;
205
+ this._previousAngle = this._transform.angle;
206
+ for (const collider of this._colliders) collider.synchronize(this._transform);
207
+ return this;
208
+ }
209
+ /** Refresh every collider's world geometry from the current transform. */
210
+ synchronizeColliders() {
211
+ for (const collider of this._colliders) collider.synchronize(this._transform);
212
+ }
213
+ /** World-space centre of mass X (body origin + rotated local centre of mass). */
214
+ get worldCenterOfMassX() {
215
+ return this._transform.x + this._transform.cos * this._comX - this._transform.sin * this._comY;
216
+ }
217
+ /** World-space centre of mass Y. */
218
+ get worldCenterOfMassY() {
219
+ return this._transform.y + this._transform.sin * this._comX + this._transform.cos * this._comY;
220
+ }
221
+ /**
222
+ * Accumulate a world-space force at the centre of mass; integrated on the next
223
+ * sub-step and then cleared. No-op on static/kinematic bodies. Returns `this`.
224
+ */
225
+ applyForce(forceX, forceY) {
226
+ this.wake();
227
+ this._forceX += forceX;
228
+ this._forceY += forceY;
229
+ return this;
230
+ }
231
+ /**
232
+ * Accumulate a torque; integrated on the next sub-step and then cleared. No-op
233
+ * on static/kinematic bodies (and fixed-rotation bodies). Returns `this`.
234
+ */
235
+ applyTorque(torque) {
236
+ this.wake();
237
+ this._torque += torque;
238
+ return this;
239
+ }
240
+ /**
241
+ * Apply an instantaneous world-space impulse at `(pointX, pointY)` (world space;
242
+ * defaults to the centre of mass), changing velocity immediately. No-op on
243
+ * static/kinematic bodies (infinite mass). Returns `this`.
244
+ */
245
+ applyImpulse(impulseX, impulseY, pointX, pointY) {
246
+ if (this.invMass === 0) return this;
247
+ this.wake();
248
+ this.linearVelocityX += impulseX * this.invMass;
249
+ this.linearVelocityY += impulseY * this.invMass;
250
+ if (pointX !== void 0 && pointY !== void 0 && this.invInertia !== 0) {
251
+ const rx = pointX - this.worldCenterOfMassX;
252
+ const ry = pointY - this.worldCenterOfMassY;
253
+ this.angularVelocity += (rx * impulseY - ry * impulseX) * this.invInertia;
254
+ }
255
+ return this;
256
+ }
257
+ /**
258
+ * @internal - integrate velocity from gravity, accumulated forces/torque and
259
+ * damping over the sub-step `h`. No-op for static/kinematic (infinite mass
260
+ * keeps their velocity solver-driven only). Called once per TGS sub-step, so
261
+ * gravity/forces are applied with `h` each sub-step (`N·h = dt`, same total
262
+ * impulse, but the small-step position error scales with `h²`). The
263
+ * force/torque accumulators are **not** cleared here - they are consumed by
264
+ * every sub-step and cleared once per frame by {@link _finalizePosition}.
265
+ */
266
+ _integrateVelocity(h, gravityX, gravityY) {
267
+ if (this.invMass === 0 || this._sleeping) return;
268
+ this.linearVelocityX += (gravityX * this.gravityScale + this._forceX * this.invMass) * h;
269
+ this.linearVelocityY += (gravityY * this.gravityScale + this._forceY * this.invMass) * h;
270
+ this.angularVelocity += this._torque * this.invInertia * h;
271
+ this.linearVelocityX /= 1 + h * this.linearDamping;
272
+ this.linearVelocityY /= 1 + h * this.linearDamping;
273
+ this.angularVelocity /= 1 + h * this.angularDamping;
274
+ }
275
+ /**
276
+ * @internal - accumulate this sub-step's velocity into the per-frame delta
277
+ * position/rotation (TGS sub-stepping). The transform itself is **not** moved
278
+ * here; {@link _finalizePosition} writes the accumulated delta once per frame.
279
+ * Tracking the delta (rather than moving the transform each sub-step) lets the
280
+ * solver recompute contact separation from it without re-running detection or
281
+ * re-syncing collider geometry per sub-step. Static bodies never move.
282
+ */
283
+ _integratePosition(h) {
284
+ if (this.type === "static" || this._sleeping) return;
285
+ this._deltaPosX += this.linearVelocityX * h;
286
+ this._deltaPosY += this.linearVelocityY * h;
287
+ this._deltaAngle += this.angularVelocity * h;
288
+ if (this._deltaAngle !== 0) {
289
+ this._deltaCos = Math.cos(this._deltaAngle);
290
+ this._deltaSin = Math.sin(this._deltaAngle);
291
+ }
292
+ }
293
+ /**
294
+ * @internal - advance the sleep timer over one fixed step `dt`. A body below
295
+ * both velocity thresholds accumulates time; a too-fast body, or one that opts
296
+ * out via {@link allowSleep}, resets it. The sleep/wake decision is made per
297
+ * island by the world (so a stack sleeps as a unit) from {@link _sleepTime}.
298
+ */
299
+ _accumulateSleepTime(dt, linearThreshold, angularThreshold) {
300
+ const tooFast = this.linearVelocityX * this.linearVelocityX + this.linearVelocityY * this.linearVelocityY > linearThreshold * linearThreshold || Math.abs(this.angularVelocity) > angularThreshold;
301
+ this._sleepTime = !this.allowSleep || tooFast ? 0 : this._sleepTime + dt;
302
+ }
303
+ /** @internal - set the sleep state. Sleeping zeroes the velocity; waking resets the sleep timer. */
304
+ _setSleeping(sleeping) {
305
+ if (this._sleeping === sleeping) return;
306
+ this._sleeping = sleeping;
307
+ if (sleeping) {
308
+ this.linearVelocityX = 0;
309
+ this.linearVelocityY = 0;
310
+ this.angularVelocity = 0;
311
+ } else this._sleepTime = 0;
312
+ }
313
+ /**
314
+ * Wake the body if it is asleep, resetting its sleep timer. The rest of its
315
+ * island wakes with it on the next step (a contact to an awake body keeps the
316
+ * whole island awake). Returns `this`.
317
+ */
318
+ wake() {
319
+ this._setSleeping(false);
320
+ this._sleepTime = 0;
321
+ return this;
322
+ }
323
+ /**
324
+ * @internal - apply the frame's accumulated delta position/rotation to the
325
+ * transform (rotating about the centre of mass), re-sync collider geometry and
326
+ * clear the force/torque accumulators. Called once per frame after the
327
+ * sub-step loop. Static bodies never move; the force clear still runs (forces
328
+ * are a no-op on infinite mass but the accumulator is reset for consistency).
329
+ */
330
+ _finalizePosition() {
331
+ this._forceX = 0;
332
+ this._forceY = 0;
333
+ this._torque = 0;
334
+ this._teleported = false;
335
+ this._previousX = this._transform.x;
336
+ this._previousY = this._transform.y;
337
+ this._previousAngle = this._transform.angle;
338
+ if (this.type === "static" || this._deltaPosX === 0 && this._deltaPosY === 0 && this._deltaAngle === 0) {
339
+ this._resetDelta();
340
+ return;
341
+ }
342
+ const newComX = this.worldCenterOfMassX + this._deltaPosX;
343
+ const newComY = this.worldCenterOfMassY + this._deltaPosY;
344
+ const newAngle = this._transform.angle + this._deltaAngle;
345
+ const cos = Math.cos(newAngle);
346
+ const sin = Math.sin(newAngle);
347
+ setTransform(this._transform, newComX - (cos * this._comX - sin * this._comY), newComY - (sin * this._comX + cos * this._comY), newAngle);
348
+ this._resetDelta();
349
+ for (const collider of this._colliders) collider.synchronize(this._transform);
350
+ }
351
+ /** Clear the per-frame TGS delta accumulators (position, rotation and its cached cos/sin). */
352
+ _resetDelta() {
353
+ this._deltaPosX = 0;
354
+ this._deltaPosY = 0;
355
+ this._deltaAngle = 0;
356
+ this._deltaCos = 1;
357
+ this._deltaSin = 0;
358
+ }
359
+ /** Internal: detach a collider (called by the world during destruction). */
360
+ _removeCollider(collider) {
361
+ const index = this._colliders.indexOf(collider);
362
+ if (index !== -1) {
363
+ this._colliders.splice(index, 1);
364
+ this._recomputeMass();
365
+ }
366
+ }
367
+ /**
368
+ * @internal - join `owner`'s world with the allocated `id`. Allocates ids for
369
+ * and registers every held collider, then aggregates the mass model and
370
+ * synchronises world geometry. Called once by {@link PhysicsWorld.add}.
371
+ *
372
+ * Ordering matters and mirrors the per-collider create path exactly: ids and
373
+ * the body link go on first, then a single mass recompute over the full
374
+ * collider set (so the centre of mass / inertia aggregate is correct), then
375
+ * `synchronize` over every collider (the cached world geometry the broad/narrow
376
+ * phase reads), then world registration last. Recomputing mass before all
377
+ * colliders are linked, or syncing before the mass model exists, would feed the
378
+ * solver a stale CoM and produce subtle integration bugs.
379
+ */
380
+ _attachToWorld(owner, id) {
381
+ if (this._destroyed) throw new Error("PhysicsBody: cannot add a destroyed body to a world.");
382
+ this._owner = owner;
383
+ this._id = id;
384
+ this._attached = true;
385
+ for (const collider of this._colliders) attachWithChainEdges(collider, this, owner);
386
+ this._recomputeMass();
387
+ this._assertDynamicCarriesMass();
388
+ for (const collider of this._colliders) collider.synchronize(this._transform);
389
+ for (const collider of this._colliders) owner._registerCollider(collider);
390
+ }
391
+ /** Internal: mark destroyed (called by the world). */
392
+ _markDestroyed() {
393
+ this._destroyed = true;
394
+ }
395
+ /**
396
+ * A dynamic body has to carry mass. Boundary geometry (a segment, a chain)
397
+ * reports no mass properties, and a collider may also be given zero density,
398
+ * so a dynamic body can end up with none at all - which the solver cannot tell
399
+ * apart from a static body, since both have `invMass === 0`. Rejecting it is
400
+ * the difference between a clear error and a body that silently stops moving.
401
+ */
402
+ _assertDynamicCarriesMass() {
403
+ if (this.type !== "dynamic" || this._massReady || this._colliders.length === 0) return;
404
+ throw new Error("PhysicsBody: a dynamic body must carry at least one collider with mass — every collider it holds is either boundary geometry (a segment or chain, which has no interior) or has zero density. Add a solid collider, or make the body static or kinematic.");
405
+ }
406
+ /** Recompute mass, centre of mass and rotational inertia from the colliders. */
407
+ _recomputeMass() {
408
+ if (this.type !== "dynamic") {
409
+ this.mass = 0;
410
+ this.invMass = 0;
411
+ this.inertia = 0;
412
+ this.invInertia = 0;
413
+ this._comX = 0;
414
+ this._comY = 0;
415
+ this._massReady = false;
416
+ return;
417
+ }
418
+ let mass = 0;
419
+ let cx = 0;
420
+ let cy = 0;
421
+ let inertiaOrigin = 0;
422
+ const point = {
423
+ x: 0,
424
+ y: 0
425
+ };
426
+ for (const collider of this._colliders) {
427
+ const massProperties = collider.shape.massProperties;
428
+ if (massProperties === null) continue;
429
+ const m = collider.density * massProperties.area;
430
+ if (m <= 0) continue;
431
+ applyTransform(collider.localTransform, massProperties.centroidX, massProperties.centroidY, point);
432
+ mass += m;
433
+ cx += m * point.x;
434
+ cy += m * point.y;
435
+ inertiaOrigin += collider.density * massProperties.unitInertia + m * (point.x * point.x + point.y * point.y);
436
+ }
437
+ if (mass > 0) {
438
+ cx /= mass;
439
+ cy /= mass;
440
+ }
441
+ this.mass = mass;
442
+ this.invMass = mass > 0 ? 1 / mass : 0;
443
+ this._comX = cx;
444
+ this._comY = cy;
445
+ const inertiaCom = inertiaOrigin - mass * (cx * cx + cy * cy);
446
+ this.inertia = this.fixedRotation ? 0 : inertiaCom;
447
+ this.invInertia = this.fixedRotation || inertiaCom <= 0 ? 0 : 1 / inertiaCom;
448
+ this._massReady = mass > 0;
449
+ }
450
+ };
451
+ /**
452
+ * Give a collider its id, and a chain collider's edge proxies theirs. The
453
+ * proxies are ids in the same space as authored colliders - the contact graph
454
+ * keys pairs on them - but they never appear in `body.colliders`.
455
+ */
456
+ const attachWithChainEdges = (collider, body, owner) => {
457
+ collider._attach(body, owner._allocateColliderId());
458
+ const edges = collider.chainEdges;
459
+ if (edges === null) return;
460
+ for (const edge of edges) edge._attach(body, owner._allocateColliderId());
461
+ };
445
462
 
463
+ //#endregion
446
464
  export { PhysicsBody };
447
- //# sourceMappingURL=PhysicsBody.js.map
465
+ //# sourceMappingURL=PhysicsBody.js.map