@codexo/exojs-physics 0.13.0 → 0.15.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 (76) hide show
  1. package/README.md +17 -10
  2. package/dist/esm/Collider.d.ts +17 -6
  3. package/dist/esm/Collider.js +28 -6
  4. package/dist/esm/Collider.js.map +1 -1
  5. package/dist/esm/ContactGraph.d.ts +49 -3
  6. package/dist/esm/ContactGraph.js +132 -44
  7. package/dist/esm/ContactGraph.js.map +1 -1
  8. package/dist/esm/PhysicsBody.d.ts +145 -15
  9. package/dist/esm/PhysicsBody.js +282 -21
  10. package/dist/esm/PhysicsBody.js.map +1 -1
  11. package/dist/esm/PhysicsWorld.d.ts +177 -39
  12. package/dist/esm/PhysicsWorld.js +412 -35
  13. package/dist/esm/PhysicsWorld.js.map +1 -1
  14. package/dist/esm/backend/NativePhysicsBackend.d.ts +5 -0
  15. package/dist/esm/backend/NativePhysicsBackend.js +14 -0
  16. package/dist/esm/backend/NativePhysicsBackend.js.map +1 -1
  17. package/dist/esm/backend/PhysicsBackend.d.ts +9 -1
  18. package/dist/esm/binding/BindingRegistry.d.ts +1 -2
  19. package/dist/esm/binding/BindingRegistry.js +2 -2
  20. package/dist/esm/binding/BindingRegistry.js.map +1 -1
  21. package/dist/esm/binding/PhysicsBinding.d.ts +7 -18
  22. package/dist/esm/binding/PhysicsBinding.js +9 -8
  23. package/dist/esm/binding/PhysicsBinding.js.map +1 -1
  24. package/dist/esm/broadphase/SweepAndPrune.d.ts +1 -0
  25. package/dist/esm/broadphase/SweepAndPrune.js +32 -3
  26. package/dist/esm/broadphase/SweepAndPrune.js.map +1 -1
  27. package/dist/esm/collision/CollisionProxy.d.ts +2 -2
  28. package/dist/esm/collision/narrowphase.js +91 -38
  29. package/dist/esm/collision/narrowphase.js.map +1 -1
  30. package/dist/esm/debug/PhysicsDebugDraw.d.ts +8 -1
  31. package/dist/esm/debug/PhysicsDebugDraw.js +26 -2
  32. package/dist/esm/debug/PhysicsDebugDraw.js.map +1 -1
  33. package/dist/esm/index.js +7 -0
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/joints/DistanceJoint.d.ts +71 -0
  36. package/dist/esm/joints/DistanceJoint.js +176 -0
  37. package/dist/esm/joints/DistanceJoint.js.map +1 -0
  38. package/dist/esm/joints/Joint.d.ts +25 -0
  39. package/dist/esm/joints/Joint.js +24 -0
  40. package/dist/esm/joints/Joint.js.map +1 -0
  41. package/dist/esm/joints/MouseJoint.d.ts +57 -0
  42. package/dist/esm/joints/MouseJoint.js +137 -0
  43. package/dist/esm/joints/MouseJoint.js.map +1 -0
  44. package/dist/esm/joints/PrismaticJoint.d.ts +85 -0
  45. package/dist/esm/joints/PrismaticJoint.js +241 -0
  46. package/dist/esm/joints/PrismaticJoint.js.map +1 -0
  47. package/dist/esm/joints/RevoluteJoint.d.ts +81 -0
  48. package/dist/esm/joints/RevoluteJoint.js +217 -0
  49. package/dist/esm/joints/RevoluteJoint.js.map +1 -0
  50. package/dist/esm/joints/WeldJoint.d.ts +61 -0
  51. package/dist/esm/joints/WeldJoint.js +159 -0
  52. package/dist/esm/joints/WeldJoint.js.map +1 -0
  53. package/dist/esm/joints/WheelJoint.d.ts +92 -0
  54. package/dist/esm/joints/WheelJoint.js +256 -0
  55. package/dist/esm/joints/WheelJoint.js.map +1 -0
  56. package/dist/esm/math.js +15 -1
  57. package/dist/esm/math.js.map +1 -1
  58. package/dist/esm/physicsBuildInfo.js +2 -2
  59. package/dist/esm/public.d.ts +9 -2
  60. package/dist/esm/query/QueryEngine.d.ts +2 -2
  61. package/dist/esm/query/QueryEngine.js +13 -4
  62. package/dist/esm/query/QueryEngine.js.map +1 -1
  63. package/dist/esm/shapes/AnyShape.d.ts +9 -0
  64. package/dist/esm/shapes/CircleShape.d.ts +1 -2
  65. package/dist/esm/shapes/CircleShape.js.map +1 -1
  66. package/dist/esm/shapes/PolygonShape.d.ts +1 -2
  67. package/dist/esm/shapes/PolygonShape.js +45 -17
  68. package/dist/esm/shapes/PolygonShape.js.map +1 -1
  69. package/dist/esm/shapes/index.d.ts +1 -0
  70. package/dist/esm/solver/ContactSolver.d.ts +87 -0
  71. package/dist/esm/solver/ContactSolver.js +490 -0
  72. package/dist/esm/solver/ContactSolver.js.map +1 -0
  73. package/dist/esm/sort.d.ts +17 -0
  74. package/dist/esm/sort.js +54 -0
  75. package/dist/esm/sort.js.map +1 -0
  76. package/package.json +3 -3
@@ -2,48 +2,108 @@ import type { SceneNode } from '@codexo/exojs';
2
2
  import { Signal, Vector } from '@codexo/exojs';
3
3
  import type { Aabb } from './Aabb';
4
4
  import type { PhysicsBackend } from './backend/PhysicsBackend';
5
- import type { BindingOptions, PhysicsBinding } from './binding/PhysicsBinding';
6
- import type { Collider, ColliderOptions } from './Collider';
5
+ import type { PhysicsBinding } from './binding/PhysicsBinding';
6
+ import { Collider } from './Collider';
7
7
  import type { CollisionEvent, SensorEvent } from './events';
8
- import type { BodyOptions, BodyOwner } from './PhysicsBody';
8
+ import type { Joint } from './joints/Joint';
9
+ import type { BodyOwner } from './PhysicsBody';
9
10
  import { PhysicsBody } from './PhysicsBody';
10
11
  import type { QueryFilter, RayHit } from './query/QueryEngine';
11
- import type { Shape } from './shapes/Shape';
12
+ import type { AnyShape } from './shapes/AnyShape';
12
13
  import { TimeStepper } from './TimeStepper';
13
- import type { VectorLike } from './types';
14
+ import type { BodyType, CollisionFilter, VectorLike } from './types';
14
15
  /** Construction options for a {@link PhysicsWorld}. */
15
16
  export interface PhysicsWorldOptions {
16
- /** Gravity in px/s² (+Y down). Stored; applied once the dynamics solver ships. Default `(0, 0)`. */
17
+ /** Gravity in px/s² (+Y down). Integrated each sub-step. Default `(0, 0)`. */
17
18
  gravity?: VectorLike;
18
19
  /** Fixed timestep in seconds. Default `1 / 60`. */
19
20
  fixedDelta?: number;
20
- /** Maximum sub-steps per `step` (spiral-of-death guard). Default `8`. */
21
+ /** Maximum fixed steps per `step` call (spiral-of-death guard). Default `8`. */
21
22
  maxSubSteps?: number;
22
- /** Solver velocity iterations (stored for forward-compat). Default `8`. */
23
- velocityIterations?: number;
24
- /** Solver position iterations (stored for forward-compat). Default `3`. */
25
- positionIterations?: number;
26
- /** Interpolate bound nodes between sub-steps (no effect until dynamics integrate). Default `true`. */
27
- interpolation?: boolean;
23
+ /**
24
+ * TGS-Soft sub-steps per fixed step (the solver's stiffness scales with this,
25
+ * not iteration count). Default `4`. Must be ≥ 1. Values below `2` visibly
26
+ * degrade tall-stack stability (a 10-box tower jitters at `1`), so the default
27
+ * is load-bearing do not lower it for performance.
28
+ */
29
+ subStepCount?: number;
30
+ /** Soft-contact stiffness in Hz (the contact behaves as a damped spring at this frequency). Default `30`. */
31
+ contactHertz?: number;
32
+ /** Soft-contact damping ratio (≥ 1 keeps contacts from oscillating). Default `10`. */
33
+ dampingRatio?: number;
34
+ /** Put resting bodies to sleep so they skip integration and solving. Default `true`. */
35
+ enableSleeping?: boolean;
36
+ /** Linear speed at or below which a body is a sleep candidate, px/s. Default `5`. */
37
+ sleepLinearVelocity?: number;
38
+ /** Angular speed at or below which a body is a sleep candidate, rad/s. Default `0.06`. */
39
+ sleepAngularVelocity?: number;
40
+ /** Seconds a body must stay below the sleep thresholds before it sleeps. Default `0.5`. */
41
+ timeToSleep?: number;
28
42
  }
29
- /** {@link PhysicsWorld.createStaticCollider} options: a collider plus its static body placement. */
30
- export interface StaticColliderOptions extends ColliderOptions {
31
- /** World position of the implicit static body. Default `(0, 0)`. */
43
+ /**
44
+ * {@link PhysicsWorld.attach} convenience options: a body type plus a single
45
+ * collider, attached to a scene node in one call.
46
+ */
47
+ export interface AttachOptions {
48
+ /** Simulation role of the created body. Default `'dynamic'`. */
49
+ type?: BodyType;
50
+ /** Initial world position of the body. Default the node's position is left untouched and `(0, 0)` is used. */
32
51
  position?: VectorLike;
33
- /** Rotation (radians) of the implicit static body. Default `0`. */
52
+ /** Initial rotation (radians) of the body. Default `0`. */
34
53
  angle?: number;
54
+ /** Per-body multiplier on world gravity. Default `1`. */
55
+ gravityScale?: number;
56
+ /** When `true`, the body never rotates under contacts. Default `false`. */
57
+ fixedRotation?: boolean;
58
+ /** The collider geometry. */
59
+ shape: AnyShape;
60
+ /** Body-local offset of the collider. Default `(0, 0)`. */
61
+ offset?: VectorLike;
62
+ /** Body-local rotation of the collider (radians). Default `0`. */
63
+ rotation?: number;
64
+ /** Collider density (mass per px²). Default `1`. */
65
+ density?: number;
66
+ /** Coulomb friction coefficient. Default `0.2`. */
67
+ friction?: number;
68
+ /** Restitution / bounciness in `[0, 1]`. Default `0`. */
69
+ restitution?: number;
70
+ /** When `true`, the collider generates overlap events but no contact response. Default `false`. */
71
+ isSensor?: boolean;
72
+ /** Category/mask/group collision filter; partials merge over the defaults. */
73
+ filter?: Partial<CollisionFilter>;
35
74
  }
36
75
  /**
37
76
  * The collision/query world: owns bodies, colliders, the detection backend,
38
77
  * bindings, the query engine and the fixed-step accumulator. Stepped by the
39
- * caller (commonly from a `Scene.update`), it runs broad- and narrow-phase
40
- * detection, fires immutable contact/sensor events, and writes bound node
41
- * transforms. It holds **no module-level state**, so any number of worlds run
42
- * in isolation (gate I-1).
78
+ * caller (commonly from a `Scene.update`), each fixed sub-step it integrates
79
+ * body velocities, runs broad- and narrow-phase detection, solves contacts and
80
+ * integrates positions, then fires immutable contact/sensor events and writes
81
+ * bound node transforms. It holds **no module-level state**, so any number of
82
+ * worlds run in isolation (gate I-1).
43
83
  *
44
- * This release performs collision detection, sensors, events, queries and
45
- * binding; bodies move only via {@link PhysicsBody.setTransform}. Gravity,
46
- * forces and impulse integration arrive with the dynamics solver.
84
+ * The dynamics are a native, warm-started **TGS-Soft** solver (Box2D-v3 "soft
85
+ * step"): each fixed step runs detection once, then several sub-steps, each
86
+ * integrating gravity over the sub-step and solving contacts with a soft
87
+ * position bias plus a bias-free relax pass; a 2-point block normal solve
88
+ * propagates stack loads, and restitution is a separate final pass. Decoupling
89
+ * stiffness from the iteration count keeps tall towers stable. The detection
90
+ * backend sits behind an internal seam, so the solver is swappable without
91
+ * touching this public surface.
92
+ *
93
+ * **Operating envelope.** The soft solver trades a little accuracy for
94
+ * robustness, so it has a few documented limits — each stays finite/stable and
95
+ * each is pinned by a gate in `dynamics.test.ts`:
96
+ * - **Mass ratio** — resting stacks are slop-accurate up to ~100:1. Beyond that
97
+ * the velocity-capped soft push-out (`maxBiasVelocity`) lets the lighter body
98
+ * settle progressively deeper (≈6px at 500:1, fully through a thin floor by
99
+ * ~5000:1) — always finite, never exploding (SG-MR3).
100
+ * - **No CCD** — detection runs once per fixed step with no swept test, so a
101
+ * body that travels farther than an obstacle's thickness in one step tunnels
102
+ * straight through it (it stays finite). Reliably stopping fast projectiles is
103
+ * a future bullet-mode feature (SG-X5).
104
+ * - **{@link PhysicsWorldOptions.subStepCount}** — the default `4` is
105
+ * load-bearing for tall-stack stability; lowering it below `2` visibly
106
+ * degrades stacking, so do not reduce it for performance.
47
107
  */
48
108
  export declare class PhysicsWorld implements BodyOwner {
49
109
  /** Fires when two solid colliders begin touching. Argument is an immutable snapshot. */
@@ -54,22 +114,39 @@ export declare class PhysicsWorld implements BodyOwner {
54
114
  readonly onSensorEnter: Signal<[SensorEvent]>;
55
115
  /** Fires when a collider leaves a sensor. */
56
116
  readonly onSensorExit: Signal<[SensorEvent]>;
57
- /** World gravity (px/s², +Y down). Stored until dynamics ship. */
117
+ /** World gravity (px/s², +Y down). Integrated each sub-step. */
58
118
  readonly gravity: Vector;
59
119
  /** The fixed-step accumulator. */
60
120
  readonly timeStepper: TimeStepper;
61
- /** Whether bound nodes interpolate between sub-steps (no effect until dynamics). */
62
- readonly interpolation: boolean;
63
- /** Solver velocity iterations (stored for forward-compat). */
64
- readonly velocityIterations: number;
65
- /** Solver position iterations (stored for forward-compat). */
66
- readonly positionIterations: number;
121
+ /** TGS-Soft sub-steps per fixed step. */
122
+ readonly subStepCount: number;
123
+ /** Soft-contact stiffness in Hz. */
124
+ readonly contactHertz: number;
125
+ /** Soft-contact damping ratio. */
126
+ readonly dampingRatio: number;
127
+ /** Whether resting bodies are put to sleep. */
128
+ readonly enableSleeping: boolean;
129
+ /** Linear sleep threshold (px/s). */
130
+ readonly sleepLinearVelocity: number;
131
+ /** Angular sleep threshold (rad/s). */
132
+ readonly sleepAngularVelocity: number;
133
+ /** Seconds below the thresholds before a body sleeps. */
134
+ readonly timeToSleep: number;
67
135
  private readonly _backend;
68
136
  private readonly _bodies;
69
137
  private readonly _colliders;
138
+ private readonly _joints;
70
139
  private readonly _bindings;
71
140
  private readonly _query;
72
141
  private readonly _commands;
142
+ /** Pooled union-find parent array for the per-step island pass (reused; sized to the body count). */
143
+ private readonly _islandParent;
144
+ /** Pooled per-island minimum sleep time, indexed by union-find root. */
145
+ private readonly _islandMinSleep;
146
+ /** Pooled ray-hit buffer + origin/direction for the CCD swept test. */
147
+ private readonly _ccdHits;
148
+ private readonly _ccdOrigin;
149
+ private readonly _ccdDir;
73
150
  private _nextBodyId;
74
151
  private _nextColliderId;
75
152
  private _dispatching;
@@ -79,21 +156,46 @@ export declare class PhysicsWorld implements BodyOwner {
79
156
  get bodies(): readonly PhysicsBody[];
80
157
  /** Live colliders (read-only view). */
81
158
  get colliders(): readonly Collider[];
82
- /** Create a body. Safe to call inside an event callback (deferred to end of step). */
83
- createBody(options?: BodyOptions): PhysicsBody;
84
- /** Sugar: an explicit static body carrying a single collider. The body is addressable via `collider.body`. */
85
- createStaticCollider(options: StaticColliderOptions): Collider;
159
+ /**
160
+ * Add a body to the world: allocates the body and its collider ids, registers
161
+ * the colliders, computes the mass model and tracks the body for stepping.
162
+ * Construct the body freely first (`new PhysicsBody({ … })`), then add it.
163
+ * Safe to call inside an event callback — the body push is deferred to the end
164
+ * of the step, exactly like collider registration. Returns the body.
165
+ *
166
+ * @throws if the body has already been added to a world.
167
+ */
168
+ add(body: PhysicsBody): PhysicsBody;
169
+ /**
170
+ * Convenience: create a body carrying a single collider, add it to the world
171
+ * and bind it to `node` in one call. The node tracks `body.position` after each
172
+ * step. Returns the body. Equivalent to `new PhysicsBody(...)` + `add` + `bind`.
173
+ */
174
+ attach(node: SceneNode, options: AttachOptions): PhysicsBody;
86
175
  /** Destroy a body and its colliders. Deferred when called inside a callback. */
87
176
  destroyBody(body: PhysicsBody): void;
88
177
  /** Destroy a single collider, recomputing its body's mass. Deferred when called inside a callback. */
89
178
  destroyCollider(collider: Collider): void;
179
+ /** Live joints (read-only view). */
180
+ get joints(): readonly Joint[];
181
+ /**
182
+ * Add a constraint joint. Construct it first (`new DistanceJoint({ … })`),
183
+ * then add it. Wakes both bodies; safe inside a callback (registration is
184
+ * deferred). Returns the joint.
185
+ */
186
+ addJoint<T extends Joint>(joint: T): T;
187
+ /** Remove a joint, waking both bodies so they respond to the lost constraint. Deferred when called inside a callback. */
188
+ removeJoint(joint: Joint): void;
90
189
  /**
91
- * Advance the world by `frameDeltaSeconds`. Accumulates into fixed sub-steps,
92
- * runs detection, dispatches events, then writes bound node transforms.
190
+ * Advance the world by `frameDeltaSeconds`. Accumulates into fixed steps; each
191
+ * fixed step runs detection once, then a TGS-Soft sub-step loop (integrate
192
+ * gravity, solve contacts with a soft bias, integrate positions, relax) and a
193
+ * restitution pass, then writes the accumulated motion into each body. Finally
194
+ * dispatches events and writes bound node transforms.
93
195
  */
94
196
  step(frameDeltaSeconds: number): void;
95
197
  /** Link a body to a scene node; the node tracks the body after each step. */
96
- bind(body: PhysicsBody, node: SceneNode, options?: BindingOptions): PhysicsBinding;
198
+ bind(body: PhysicsBody, node: SceneNode): PhysicsBinding;
97
199
  /** Remove a body↔node link. */
98
200
  unbind(body: PhysicsBody): void;
99
201
  /** Colliders containing `point`. Fresh array. */
@@ -107,7 +209,7 @@ export declare class PhysicsWorld implements BodyOwner {
107
209
  /** All collider hits along the ray, sorted by distance. Writes into `out` (cleared) if given. */
108
210
  rayCastAll(origin: VectorLike, direction: VectorLike, filter?: QueryFilter, out?: RayHit[], maxDistance?: number): RayHit[];
109
211
  /** Colliders overlapping `shape` placed at `position`/`angle`. Fresh array. */
110
- overlapShape(shape: Shape, position: VectorLike, filter?: QueryFilter, angle?: number): Collider[];
212
+ overlapShape(shape: AnyShape, position: VectorLike, filter?: QueryFilter, angle?: number): Collider[];
111
213
  /** Release every body, collider, binding and backend resource. */
112
214
  destroy(): void;
113
215
  _allocateColliderId(): number;
@@ -115,6 +217,42 @@ export declare class PhysicsWorld implements BodyOwner {
115
217
  /** The detection backend (internal; consumed by the debug draw layer). */
116
218
  get backend(): PhysicsBackend;
117
219
  private _dispatchEvents;
220
+ /**
221
+ * Accumulate per-body sleep timers and put/keep islands of resting bodies
222
+ * asleep so a stack sleeps and wakes as one unit. An island is a connected
223
+ * component of dynamic bodies joined by touching solid contacts (static and
224
+ * kinematic bodies are boundaries, not nodes); it sleeps once every member has
225
+ * stayed below the sleep thresholds for `timeToSleep`, and wakes the instant
226
+ * any member does (e.g. an awake body merges into it via a new contact).
227
+ * Deterministic: union-find roots break ties by lower index and the contact
228
+ * set is id-sorted.
229
+ */
230
+ private _updateSleeping;
231
+ /** Union-find union by lower index (deterministic roots). */
232
+ private _union;
233
+ /** Union-find find with path halving. */
234
+ private _find;
235
+ /** Build each joint's per-frame constraint data (once per fixed step). */
236
+ private _prepareJoints;
237
+ /** Re-apply each joint's accumulated impulse (each sub-step). */
238
+ private _warmStartJoints;
239
+ /** One joint velocity pass (each sub-step, after the contacts). */
240
+ private _solveJoints;
241
+ /** Whether any dynamic body is flagged for continuous collision (bullet mode). */
242
+ private _hasBullets;
243
+ /** Snapshot each bullet's centre of mass at the start of the fixed step (the swept-test origin). */
244
+ private _recordBulletPositions;
245
+ /**
246
+ * Sweep each bullet's centre of mass along this fixed step's motion against every
247
+ * other body's colliders; if it would cross one, clamp the body just short of the
248
+ * surface and resolve the impact about the surface normal (a slide for a non-bouncy
249
+ * body, an elastic reflection as restitution → 1) so it cannot tunnel. Sweeps the
250
+ * centre point — good for small/point-like projectiles; a full swept-shape TOI for
251
+ * large fast bodies is backlog (raise sub-steps or thicken geometry meanwhile).
252
+ */
253
+ private _advanceBullets;
254
+ /** The highest restitution among a body's colliders (its CCD bounce factor). */
255
+ private _bulletRestitution;
118
256
  /** Run `command` now, or queue it when inside an event dispatch (deferred to end of step). */
119
257
  private _defer;
120
258
  private _drainCommands;