@codexo/exojs-physics 0.13.0 → 0.14.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 (48) 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 +113 -15
  9. package/dist/esm/PhysicsBody.js +224 -21
  10. package/dist/esm/PhysicsBody.js.map +1 -1
  11. package/dist/esm/PhysicsWorld.d.ts +107 -35
  12. package/dist/esm/PhysicsWorld.js +136 -31
  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/PhysicsBinding.d.ts +6 -6
  19. package/dist/esm/binding/PhysicsBinding.js +8 -5
  20. package/dist/esm/binding/PhysicsBinding.js.map +1 -1
  21. package/dist/esm/broadphase/SweepAndPrune.d.ts +1 -0
  22. package/dist/esm/broadphase/SweepAndPrune.js +32 -3
  23. package/dist/esm/broadphase/SweepAndPrune.js.map +1 -1
  24. package/dist/esm/collision/CollisionProxy.d.ts +2 -2
  25. package/dist/esm/collision/narrowphase.js +91 -38
  26. package/dist/esm/collision/narrowphase.js.map +1 -1
  27. package/dist/esm/debug/PhysicsDebugDraw.d.ts +2 -1
  28. package/dist/esm/debug/PhysicsDebugDraw.js +2 -1
  29. package/dist/esm/debug/PhysicsDebugDraw.js.map +1 -1
  30. package/dist/esm/physicsBuildInfo.js +2 -2
  31. package/dist/esm/public.d.ts +2 -1
  32. package/dist/esm/query/QueryEngine.d.ts +2 -2
  33. package/dist/esm/query/QueryEngine.js +13 -4
  34. package/dist/esm/query/QueryEngine.js.map +1 -1
  35. package/dist/esm/shapes/AnyShape.d.ts +9 -0
  36. package/dist/esm/shapes/CircleShape.d.ts +1 -2
  37. package/dist/esm/shapes/CircleShape.js.map +1 -1
  38. package/dist/esm/shapes/PolygonShape.d.ts +1 -2
  39. package/dist/esm/shapes/PolygonShape.js +45 -17
  40. package/dist/esm/shapes/PolygonShape.js.map +1 -1
  41. package/dist/esm/shapes/index.d.ts +1 -0
  42. package/dist/esm/solver/ContactSolver.d.ts +87 -0
  43. package/dist/esm/solver/ContactSolver.js +483 -0
  44. package/dist/esm/solver/ContactSolver.js.map +1 -0
  45. package/dist/esm/sort.d.ts +17 -0
  46. package/dist/esm/sort.js +54 -0
  47. package/dist/esm/sort.js.map +1 -0
  48. package/package.json +4 -4
@@ -3,47 +3,102 @@ import { Signal, Vector } from '@codexo/exojs';
3
3
  import type { Aabb } from './Aabb';
4
4
  import type { PhysicsBackend } from './backend/PhysicsBackend';
5
5
  import type { BindingOptions, PhysicsBinding } from './binding/PhysicsBinding';
6
- import type { Collider, ColliderOptions } from './Collider';
6
+ import { Collider } from './Collider';
7
7
  import type { CollisionEvent, SensorEvent } from './events';
8
- import type { BodyOptions, BodyOwner } from './PhysicsBody';
8
+ import type { BodyOwner } from './PhysicsBody';
9
9
  import { PhysicsBody } from './PhysicsBody';
10
10
  import type { QueryFilter, RayHit } from './query/QueryEngine';
11
- import type { Shape } from './shapes/Shape';
11
+ import type { AnyShape } from './shapes/AnyShape';
12
12
  import { TimeStepper } from './TimeStepper';
13
- import type { VectorLike } from './types';
13
+ import type { BodyType, CollisionFilter, VectorLike } from './types';
14
14
  /** Construction options for a {@link PhysicsWorld}. */
15
15
  export interface PhysicsWorldOptions {
16
- /** Gravity in px/s² (+Y down). Stored; applied once the dynamics solver ships. Default `(0, 0)`. */
16
+ /** Gravity in px/s² (+Y down). Integrated each sub-step. Default `(0, 0)`. */
17
17
  gravity?: VectorLike;
18
18
  /** Fixed timestep in seconds. Default `1 / 60`. */
19
19
  fixedDelta?: number;
20
- /** Maximum sub-steps per `step` (spiral-of-death guard). Default `8`. */
20
+ /** Maximum fixed steps per `step` call (spiral-of-death guard). Default `8`. */
21
21
  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`. */
22
+ /**
23
+ * TGS-Soft sub-steps per fixed step (the solver's stiffness scales with this,
24
+ * not iteration count). Default `4`. Must be ≥ 1. Values below `2` visibly
25
+ * degrade tall-stack stability (a 10-box tower jitters at `1`), so the default
26
+ * is load-bearing do not lower it for performance.
27
+ */
28
+ subStepCount?: number;
29
+ /** Soft-contact stiffness in Hz (the contact behaves as a damped spring at this frequency). Default `30`. */
30
+ contactHertz?: number;
31
+ /** Soft-contact damping ratio (≥ 1 keeps contacts from oscillating). Default `10`. */
32
+ dampingRatio?: number;
33
+ /** Interpolate bound nodes between fixed steps (reserved; no effect yet). Default `true`. */
27
34
  interpolation?: boolean;
28
35
  }
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)`. */
36
+ /**
37
+ * {@link PhysicsWorld.attach} convenience options: a body type plus a single
38
+ * collider, attached to a scene node in one call.
39
+ */
40
+ export interface AttachOptions {
41
+ /** Simulation role of the created body. Default `'dynamic'`. */
42
+ type?: BodyType;
43
+ /** Initial world position of the body. Default the node's position is left untouched and `(0, 0)` is used. */
32
44
  position?: VectorLike;
33
- /** Rotation (radians) of the implicit static body. Default `0`. */
45
+ /** Initial rotation (radians) of the body. Default `0`. */
34
46
  angle?: number;
47
+ /** Per-body multiplier on world gravity. Default `1`. */
48
+ gravityScale?: number;
49
+ /** When `true`, the body never rotates under contacts. Default `false`. */
50
+ fixedRotation?: boolean;
51
+ /** The collider geometry. */
52
+ shape: AnyShape;
53
+ /** Body-local offset of the collider. Default `(0, 0)`. */
54
+ offset?: VectorLike;
55
+ /** Body-local rotation of the collider (radians). Default `0`. */
56
+ rotation?: number;
57
+ /** Collider density (mass per px²). Default `1`. */
58
+ density?: number;
59
+ /** Coulomb friction coefficient. Default `0.2`. */
60
+ friction?: number;
61
+ /** Restitution / bounciness in `[0, 1]`. Default `0`. */
62
+ restitution?: number;
63
+ /** When `true`, the collider generates overlap events but no contact response. Default `false`. */
64
+ isSensor?: boolean;
65
+ /** Category/mask/group collision filter; partials merge over the defaults. */
66
+ filter?: Partial<CollisionFilter>;
67
+ /** Binding options forwarded to {@link PhysicsWorld.bind}. */
68
+ binding?: BindingOptions;
35
69
  }
36
70
  /**
37
71
  * The collision/query world: owns bodies, colliders, the detection backend,
38
72
  * 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).
73
+ * caller (commonly from a `Scene.update`), each fixed sub-step it integrates
74
+ * body velocities, runs broad- and narrow-phase detection, solves contacts and
75
+ * integrates positions, then fires immutable contact/sensor events and writes
76
+ * bound node transforms. It holds **no module-level state**, so any number of
77
+ * worlds run in isolation (gate I-1).
78
+ *
79
+ * The dynamics are a native, warm-started **TGS-Soft** solver (Box2D-v3 "soft
80
+ * step"): each fixed step runs detection once, then several sub-steps, each
81
+ * integrating gravity over the sub-step and solving contacts with a soft
82
+ * position bias plus a bias-free relax pass; a 2-point block normal solve
83
+ * propagates stack loads, and restitution is a separate final pass. Decoupling
84
+ * stiffness from the iteration count keeps tall towers stable. The detection
85
+ * backend sits behind an internal seam, so the solver is swappable without
86
+ * touching this public surface.
43
87
  *
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.
88
+ * **Operating envelope.** The soft solver trades a little accuracy for
89
+ * robustness, so it has a few documented limits — each stays finite/stable and
90
+ * each is pinned by a gate in `dynamics.test.ts`:
91
+ * - **Mass ratio** — resting stacks are slop-accurate up to ~100:1. Beyond that
92
+ * the velocity-capped soft push-out (`maxBiasVelocity`) lets the lighter body
93
+ * settle progressively deeper (≈6px at 500:1, fully through a thin floor by
94
+ * ~5000:1) — always finite, never exploding (SG-MR3).
95
+ * - **No CCD** — detection runs once per fixed step with no swept test, so a
96
+ * body that travels farther than an obstacle's thickness in one step tunnels
97
+ * straight through it (it stays finite). Reliably stopping fast projectiles is
98
+ * a future bullet-mode feature (SG-X5).
99
+ * - **{@link PhysicsWorldOptions.subStepCount}** — the default `4` is
100
+ * load-bearing for tall-stack stability; lowering it below `2` visibly
101
+ * degrades stacking, so do not reduce it for performance.
47
102
  */
48
103
  export declare class PhysicsWorld implements BodyOwner {
49
104
  /** Fires when two solid colliders begin touching. Argument is an immutable snapshot. */
@@ -54,16 +109,18 @@ export declare class PhysicsWorld implements BodyOwner {
54
109
  readonly onSensorEnter: Signal<[SensorEvent]>;
55
110
  /** Fires when a collider leaves a sensor. */
56
111
  readonly onSensorExit: Signal<[SensorEvent]>;
57
- /** World gravity (px/s², +Y down). Stored until dynamics ship. */
112
+ /** World gravity (px/s², +Y down). Integrated each sub-step. */
58
113
  readonly gravity: Vector;
59
114
  /** The fixed-step accumulator. */
60
115
  readonly timeStepper: TimeStepper;
61
- /** Whether bound nodes interpolate between sub-steps (no effect until dynamics). */
116
+ /** Whether bound nodes interpolate between fixed steps (reserved; no effect yet). */
62
117
  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;
118
+ /** TGS-Soft sub-steps per fixed step. */
119
+ readonly subStepCount: number;
120
+ /** Soft-contact stiffness in Hz. */
121
+ readonly contactHertz: number;
122
+ /** Soft-contact damping ratio. */
123
+ readonly dampingRatio: number;
67
124
  private readonly _backend;
68
125
  private readonly _bodies;
69
126
  private readonly _colliders;
@@ -79,17 +136,32 @@ export declare class PhysicsWorld implements BodyOwner {
79
136
  get bodies(): readonly PhysicsBody[];
80
137
  /** Live colliders (read-only view). */
81
138
  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;
139
+ /**
140
+ * Add a body to the world: allocates the body and its collider ids, registers
141
+ * the colliders, computes the mass model and tracks the body for stepping.
142
+ * Construct the body freely first (`new PhysicsBody({ … })`), then add it.
143
+ * Safe to call inside an event callback — the body push is deferred to the end
144
+ * of the step, exactly like collider registration. Returns the body.
145
+ *
146
+ * @throws if the body has already been added to a world.
147
+ */
148
+ add(body: PhysicsBody): PhysicsBody;
149
+ /**
150
+ * Convenience: create a body carrying a single collider, add it to the world
151
+ * and bind it to `node` in one call. The node tracks `body.position` after each
152
+ * step. Returns the body. Equivalent to `new PhysicsBody(...)` + `add` + `bind`.
153
+ */
154
+ attach(node: SceneNode, options: AttachOptions): PhysicsBody;
86
155
  /** Destroy a body and its colliders. Deferred when called inside a callback. */
87
156
  destroyBody(body: PhysicsBody): void;
88
157
  /** Destroy a single collider, recomputing its body's mass. Deferred when called inside a callback. */
89
158
  destroyCollider(collider: Collider): void;
90
159
  /**
91
- * Advance the world by `frameDeltaSeconds`. Accumulates into fixed sub-steps,
92
- * runs detection, dispatches events, then writes bound node transforms.
160
+ * Advance the world by `frameDeltaSeconds`. Accumulates into fixed steps; each
161
+ * fixed step runs detection once, then a TGS-Soft sub-step loop (integrate
162
+ * gravity, solve contacts with a soft bias, integrate positions, relax) and a
163
+ * restitution pass, then writes the accumulated motion into each body. Finally
164
+ * dispatches events and writes bound node transforms.
93
165
  */
94
166
  step(frameDeltaSeconds: number): void;
95
167
  /** Link a body to a scene node; the node tracks the body after each step. */
@@ -107,7 +179,7 @@ export declare class PhysicsWorld implements BodyOwner {
107
179
  /** All collider hits along the ray, sorted by distance. Writes into `out` (cleared) if given. */
108
180
  rayCastAll(origin: VectorLike, direction: VectorLike, filter?: QueryFilter, out?: RayHit[], maxDistance?: number): RayHit[];
109
181
  /** Colliders overlapping `shape` placed at `position`/`angle`. Fresh array. */
110
- overlapShape(shape: Shape, position: VectorLike, filter?: QueryFilter, angle?: number): Collider[];
182
+ overlapShape(shape: AnyShape, position: VectorLike, filter?: QueryFilter, angle?: number): Collider[];
111
183
  /** Release every body, collider, binding and backend resource. */
112
184
  destroy(): void;
113
185
  _allocateColliderId(): number;
@@ -1,6 +1,7 @@
1
1
  import { Signal, Vector } from '@codexo/exojs';
2
2
  import { NativePhysicsBackend } from './backend/NativePhysicsBackend.js';
3
3
  import { BindingRegistry } from './binding/BindingRegistry.js';
4
+ import { Collider } from './Collider.js';
4
5
  import { PhysicsBody } from './PhysicsBody.js';
5
6
  import { QueryEngine } from './query/QueryEngine.js';
6
7
  import { TimeStepper } from './TimeStepper.js';
@@ -8,14 +9,35 @@ import { TimeStepper } from './TimeStepper.js';
8
9
  /**
9
10
  * The collision/query world: owns bodies, colliders, the detection backend,
10
11
  * bindings, the query engine and the fixed-step accumulator. Stepped by the
11
- * caller (commonly from a `Scene.update`), it runs broad- and narrow-phase
12
- * detection, fires immutable contact/sensor events, and writes bound node
13
- * transforms. It holds **no module-level state**, so any number of worlds run
14
- * in isolation (gate I-1).
12
+ * caller (commonly from a `Scene.update`), each fixed sub-step it integrates
13
+ * body velocities, runs broad- and narrow-phase detection, solves contacts and
14
+ * integrates positions, then fires immutable contact/sensor events and writes
15
+ * bound node transforms. It holds **no module-level state**, so any number of
16
+ * worlds run in isolation (gate I-1).
15
17
  *
16
- * This release performs collision detection, sensors, events, queries and
17
- * binding; bodies move only via {@link PhysicsBody.setTransform}. Gravity,
18
- * forces and impulse integration arrive with the dynamics solver.
18
+ * The dynamics are a native, warm-started **TGS-Soft** solver (Box2D-v3 "soft
19
+ * step"): each fixed step runs detection once, then several sub-steps, each
20
+ * integrating gravity over the sub-step and solving contacts with a soft
21
+ * position bias plus a bias-free relax pass; a 2-point block normal solve
22
+ * propagates stack loads, and restitution is a separate final pass. Decoupling
23
+ * stiffness from the iteration count keeps tall towers stable. The detection
24
+ * backend sits behind an internal seam, so the solver is swappable without
25
+ * touching this public surface.
26
+ *
27
+ * **Operating envelope.** The soft solver trades a little accuracy for
28
+ * robustness, so it has a few documented limits — each stays finite/stable and
29
+ * each is pinned by a gate in `dynamics.test.ts`:
30
+ * - **Mass ratio** — resting stacks are slop-accurate up to ~100:1. Beyond that
31
+ * the velocity-capped soft push-out (`maxBiasVelocity`) lets the lighter body
32
+ * settle progressively deeper (≈6px at 500:1, fully through a thin floor by
33
+ * ~5000:1) — always finite, never exploding (SG-MR3).
34
+ * - **No CCD** — detection runs once per fixed step with no swept test, so a
35
+ * body that travels farther than an obstacle's thickness in one step tunnels
36
+ * straight through it (it stays finite). Reliably stopping fast projectiles is
37
+ * a future bullet-mode feature (SG-X5).
38
+ * - **{@link PhysicsWorldOptions.subStepCount}** — the default `4` is
39
+ * load-bearing for tall-stack stability; lowering it below `2` visibly
40
+ * degrades stacking, so do not reduce it for performance.
19
41
  */
20
42
  class PhysicsWorld {
21
43
  /** Fires when two solid colliders begin touching. Argument is an immutable snapshot. */
@@ -26,16 +48,18 @@ class PhysicsWorld {
26
48
  onSensorEnter = new Signal();
27
49
  /** Fires when a collider leaves a sensor. */
28
50
  onSensorExit = new Signal();
29
- /** World gravity (px/s², +Y down). Stored until dynamics ship. */
51
+ /** World gravity (px/s², +Y down). Integrated each sub-step. */
30
52
  gravity;
31
53
  /** The fixed-step accumulator. */
32
54
  timeStepper;
33
- /** Whether bound nodes interpolate between sub-steps (no effect until dynamics). */
55
+ /** Whether bound nodes interpolate between fixed steps (reserved; no effect yet). */
34
56
  interpolation;
35
- /** Solver velocity iterations (stored for forward-compat). */
36
- velocityIterations;
37
- /** Solver position iterations (stored for forward-compat). */
38
- positionIterations;
57
+ /** TGS-Soft sub-steps per fixed step. */
58
+ subStepCount;
59
+ /** Soft-contact stiffness in Hz. */
60
+ contactHertz;
61
+ /** Soft-contact damping ratio. */
62
+ dampingRatio;
39
63
  _backend = new NativePhysicsBackend();
40
64
  _bodies = [];
41
65
  _colliders = [];
@@ -48,10 +72,18 @@ class PhysicsWorld {
48
72
  _destroyed = false;
49
73
  constructor(options = {}) {
50
74
  this.gravity = new Vector(options.gravity?.x ?? 0, options.gravity?.y ?? 0);
51
- this.timeStepper = new TimeStepper({ fixedDelta: options.fixedDelta, maxSubSteps: options.maxSubSteps });
75
+ this.timeStepper = new TimeStepper({
76
+ ...(options.fixedDelta !== undefined && { fixedDelta: options.fixedDelta }),
77
+ ...(options.maxSubSteps !== undefined && { maxSubSteps: options.maxSubSteps }),
78
+ });
52
79
  this.interpolation = options.interpolation ?? true;
53
- this.velocityIterations = options.velocityIterations ?? 8;
54
- this.positionIterations = options.positionIterations ?? 3;
80
+ const subStepCount = options.subStepCount ?? 4;
81
+ if (!Number.isInteger(subStepCount) || subStepCount < 1) {
82
+ throw new RangeError(`PhysicsWorld: subStepCount must be an integer ≥ 1, received ${subStepCount}.`);
83
+ }
84
+ this.subStepCount = subStepCount;
85
+ this.contactHertz = options.contactHertz ?? 30;
86
+ this.dampingRatio = options.dampingRatio ?? 10;
55
87
  this._query = new QueryEngine(this._colliders);
56
88
  }
57
89
  /** Live bodies (read-only view). */
@@ -63,10 +95,24 @@ class PhysicsWorld {
63
95
  return this._colliders;
64
96
  }
65
97
  // ── lifecycle ──────────────────────────────────────────────────────────
66
- /** Create a body. Safe to call inside an event callback (deferred to end of step). */
67
- createBody(options) {
98
+ /**
99
+ * Add a body to the world: allocates the body and its collider ids, registers
100
+ * the colliders, computes the mass model and tracks the body for stepping.
101
+ * Construct the body freely first (`new PhysicsBody({ … })`), then add it.
102
+ * Safe to call inside an event callback — the body push is deferred to the end
103
+ * of the step, exactly like collider registration. Returns the body.
104
+ *
105
+ * @throws if the body has already been added to a world.
106
+ */
107
+ add(body) {
68
108
  this._assertAlive();
69
- const body = new PhysicsBody(this, this._nextBodyId++, options);
109
+ if (body.attached) {
110
+ throw new Error('PhysicsWorld.add: this body has already been added to a world.');
111
+ }
112
+ // Allocate the id + link/register colliders + aggregate mass now (matches the
113
+ // old createBody, which allocated the id synchronously); only the body-list
114
+ // push is deferred so it is safe inside an event dispatch.
115
+ body._attachToWorld(this, this._nextBodyId++);
70
116
  this._defer(() => {
71
117
  if (!body.destroyed) {
72
118
  this._bodies.push(body);
@@ -74,10 +120,34 @@ class PhysicsWorld {
74
120
  });
75
121
  return body;
76
122
  }
77
- /** Sugar: an explicit static body carrying a single collider. The body is addressable via `collider.body`. */
78
- createStaticCollider(options) {
79
- const body = this.createBody({ type: 'static', position: options.position, angle: options.angle });
80
- return body.createCollider(options);
123
+ /**
124
+ * Convenience: create a body carrying a single collider, add it to the world
125
+ * and bind it to `node` in one call. The node tracks `body.position` after each
126
+ * step. Returns the body. Equivalent to `new PhysicsBody(...)` + `add` + `bind`.
127
+ */
128
+ attach(node, options) {
129
+ const body = new PhysicsBody({
130
+ ...(options.type !== undefined && { type: options.type }),
131
+ ...(options.position !== undefined && { position: options.position }),
132
+ ...(options.angle !== undefined && { angle: options.angle }),
133
+ ...(options.gravityScale !== undefined && { gravityScale: options.gravityScale }),
134
+ ...(options.fixedRotation !== undefined && { fixedRotation: options.fixedRotation }),
135
+ colliders: [
136
+ new Collider({
137
+ shape: options.shape,
138
+ ...(options.offset !== undefined && { offset: options.offset }),
139
+ ...(options.rotation !== undefined && { rotation: options.rotation }),
140
+ ...(options.density !== undefined && { density: options.density }),
141
+ ...(options.friction !== undefined && { friction: options.friction }),
142
+ ...(options.restitution !== undefined && { restitution: options.restitution }),
143
+ ...(options.isSensor !== undefined && { isSensor: options.isSensor }),
144
+ ...(options.filter !== undefined && { filter: options.filter }),
145
+ }),
146
+ ],
147
+ });
148
+ this.add(body);
149
+ this.bind(body, node, options.binding);
150
+ return body;
81
151
  }
82
152
  /** Destroy a body and its colliders. Deferred when called inside a callback. */
83
153
  destroyBody(body) {
@@ -89,20 +159,55 @@ class PhysicsWorld {
89
159
  }
90
160
  // ── stepping ───────────────────────────────────────────────────────────
91
161
  /**
92
- * Advance the world by `frameDeltaSeconds`. Accumulates into fixed sub-steps,
93
- * runs detection, dispatches events, then writes bound node transforms.
162
+ * Advance the world by `frameDeltaSeconds`. Accumulates into fixed steps; each
163
+ * fixed step runs detection once, then a TGS-Soft sub-step loop (integrate
164
+ * gravity, solve contacts with a soft bias, integrate positions, relax) and a
165
+ * restitution pass, then writes the accumulated motion into each body. Finally
166
+ * dispatches events and writes bound node transforms.
94
167
  */
95
168
  step(frameDeltaSeconds) {
96
169
  this._assertAlive();
97
170
  const steps = this.timeStepper.advance(frameDeltaSeconds);
98
171
  if (steps > 0) {
99
- // Without dynamics, geometry is unchanged across sub-steps, so a single
100
- // detection pass per frame suffices; the dynamics solver will run per
101
- // sub-step here.
102
- for (const body of this._bodies) {
103
- body.synchronizeColliders();
172
+ const subStepCount = this.subStepCount;
173
+ const h = this.timeStepper.fixedDelta / subStepCount;
174
+ const gravityX = this.gravity.x;
175
+ const gravityY = this.gravity.y;
176
+ const contactHertz = this.contactHertz;
177
+ const dampingRatio = this.dampingRatio;
178
+ for (let step = 0; step < steps; step++) {
179
+ // Detection runs once per fixed step (collider geometry is already current
180
+ // from the previous frame's finalize / attach / setTransform). TGS-Soft
181
+ // reuses the manifolds across the sub-steps below.
182
+ this._backend.detect(this._colliders);
183
+ this._backend.prepareSolve(h, contactHertz, dampingRatio);
184
+ for (let subStep = 0; subStep < subStepCount; subStep++) {
185
+ // Integrate gravity/forces over the sub-step (forces persist across
186
+ // sub-steps; cleared once per frame by `_finalizePosition`).
187
+ for (const body of this._bodies) {
188
+ body._integrateVelocity(h, gravityX, gravityY);
189
+ }
190
+ // Warm-start every sub-step (Box2D-v3 soft step): the relax pass leaves
191
+ // each contact's normal velocity at zero, so re-applying the
192
+ // accumulated impulse re-balances exactly this sub-step's gravity — the
193
+ // impulse converges to the per-sub-step load (m·h·g), not the per-frame
194
+ // load, which is what keeps tall stacks from pumping energy.
195
+ this._backend.warmStart();
196
+ // Main soft-bias velocity solve, integrate positions (accumulating
197
+ // per-body delta), then the bias-free relax pass.
198
+ this._backend.solveVelocities(true);
199
+ for (const body of this._bodies) {
200
+ body._integratePosition(h);
201
+ }
202
+ this._backend.solveVelocities(false);
203
+ }
204
+ // Separate restitution pass, then write the accumulated delta into each
205
+ // body's transform and re-sync collider geometry.
206
+ this._backend.applyRestitution();
207
+ for (const body of this._bodies) {
208
+ body._finalizePosition();
209
+ }
104
210
  }
105
- this._backend.detect(this._colliders);
106
211
  this._dispatchEvents();
107
212
  }
108
213
  this._bindings.sync();
@@ -1 +1 @@
1
- {"version":3,"file":"PhysicsWorld.js","sources":["../../../src/PhysicsWorld.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PhysicsWorld.js","sources":["../../../src/PhysicsWorld.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
@@ -11,9 +11,14 @@ import type { PhysicsBackend } from './PhysicsBackend';
11
11
  export declare class NativePhysicsBackend implements PhysicsBackend {
12
12
  readonly contactGraph: ContactGraph;
13
13
  private readonly _broadPhase;
14
+ private readonly _solver;
14
15
  private readonly _pairs;
15
16
  get candidatePairs(): readonly CandidatePair[];
16
17
  detect(colliders: readonly Collider[]): void;
18
+ prepareSolve(h: number, contactHertz: number, dampingRatio: number): void;
19
+ warmStart(): void;
20
+ solveVelocities(useBias: boolean): void;
21
+ applyRestitution(): void;
17
22
  removeCollider(collider: Collider): void;
18
23
  destroy(): void;
19
24
  }
@@ -1,5 +1,6 @@
1
1
  import { SweepAndPrune } from '../broadphase/SweepAndPrune.js';
2
2
  import { ContactGraph } from '../ContactGraph.js';
3
+ import { ContactSolver } from '../solver/ContactSolver.js';
3
4
 
4
5
  /**
5
6
  * The native, dependency-free backend: a {@link BroadPhase} (sweep-and-prune in
@@ -10,6 +11,7 @@ import { ContactGraph } from '../ContactGraph.js';
10
11
  class NativePhysicsBackend {
11
12
  contactGraph = new ContactGraph();
12
13
  _broadPhase = new SweepAndPrune();
14
+ _solver = new ContactSolver();
13
15
  _pairs = [];
14
16
  get candidatePairs() {
15
17
  return this._pairs;
@@ -18,6 +20,18 @@ class NativePhysicsBackend {
18
20
  this._broadPhase.computePairs(colliders, this._pairs);
19
21
  this.contactGraph.update(this._pairs);
20
22
  }
23
+ prepareSolve(h, contactHertz, dampingRatio) {
24
+ this._solver.prepare(this.contactGraph.solidContacts, h, contactHertz, dampingRatio);
25
+ }
26
+ warmStart() {
27
+ this._solver.warmStart();
28
+ }
29
+ solveVelocities(useBias) {
30
+ this._solver.solveVelocities(useBias);
31
+ }
32
+ applyRestitution() {
33
+ this._solver.applyRestitution();
34
+ }
21
35
  removeCollider(collider) {
22
36
  this.contactGraph.removeCollider(collider);
23
37
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NativePhysicsBackend.js","sources":["../../../../src/backend/NativePhysicsBackend.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AAMA;;;;;AAKG;MACU,oBAAoB,CAAA;AACf,IAAA,YAAY,GAAG,IAAI,YAAY,EAAE;AAEhC,IAAA,WAAW,GAAe,IAAI,aAAa,EAAE;IAC7C,MAAM,GAAoB,EAAE;AAE7C,IAAA,IAAW,cAAc,GAAA;QACvB,OAAO,IAAI,CAAC,MAAM;IACpB;AAEO,IAAA,MAAM,CAAC,SAA8B,EAAA;QAC1C,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC;QACrD,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;IACvC;AAEO,IAAA,cAAc,CAAC,QAAkB,EAAA;AACtC,QAAA,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,QAAQ,CAAC;IAC5C;IAEO,OAAO,GAAA;AACZ,QAAA,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE;AACzB,QAAA,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;IACxB;AACD;;;;"}
1
+ {"version":3,"file":"NativePhysicsBackend.js","sources":["../../../../src/backend/NativePhysicsBackend.ts"],"sourcesContent":[null],"names":[],"mappings":";;;;AAOA;;;;;AAKG;MACU,oBAAoB,CAAA;AACf,IAAA,YAAY,GAAG,IAAI,YAAY,EAAE;AAEhC,IAAA,WAAW,GAAe,IAAI,aAAa,EAAE;AAC7C,IAAA,OAAO,GAAG,IAAI,aAAa,EAAE;IAC7B,MAAM,GAAoB,EAAE;AAE7C,IAAA,IAAW,cAAc,GAAA;QACvB,OAAO,IAAI,CAAC,MAAM;IACpB;AAEO,IAAA,MAAM,CAAC,SAA8B,EAAA;QAC1C,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC;QACrD,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;IACvC;AAEO,IAAA,YAAY,CAAC,CAAS,EAAE,YAAoB,EAAE,YAAoB,EAAA;AACvE,QAAA,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC,EAAE,YAAY,EAAE,YAAY,CAAC;IACtF;IAEO,SAAS,GAAA;AACd,QAAA,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE;IAC1B;AAEO,IAAA,eAAe,CAAC,OAAgB,EAAA;AACrC,QAAA,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC;IACvC;IAEO,gBAAgB,GAAA;AACrB,QAAA,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE;IACjC;AAEO,IAAA,cAAc,CAAC,QAAkB,EAAA;AACtC,QAAA,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,QAAQ,CAAC;IAC5C;IAEO,OAAO,GAAA;AACZ,QAAA,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE;AACzB,QAAA,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;IACxB;AACD;;;;"}
@@ -14,8 +14,16 @@ export interface PhysicsBackend {
14
14
  readonly contactGraph: ContactGraph;
15
15
  /** The latest broad-phase candidate pairs (read-only; for debug draw). */
16
16
  readonly candidatePairs: readonly CandidatePair[];
17
- /** Run one detection pass over `colliders`, refreshing the contact graph. */
17
+ /** Run one detection pass over `colliders`, refreshing the contact graph. Once per frame (TGS reuses the manifolds across sub-steps). */
18
18
  detect(colliders: readonly Collider[]): void;
19
+ /** Build the per-frame contact constraints from the solid contacts. `h` is the sub-step duration; the soft factors derive from it plus `contactHertz`/`dampingRatio`. Call once per frame after {@link detect}. */
20
+ prepareSolve(h: number, contactHertz: number, dampingRatio: number): void;
21
+ /** Re-apply the cached warm-start impulses to the contacting bodies (first sub-step only). */
22
+ warmStart(): void;
23
+ /** One velocity pass over the solid contacts. `useBias` selects the main soft-bias pass; `false` is the relax pass. */
24
+ solveVelocities(useBias: boolean): void;
25
+ /** Restitution pass, run once per frame after the sub-step loop. */
26
+ applyRestitution(): void;
19
27
  /** Forget any state referencing `collider` (called on destruction). */
20
28
  removeCollider(collider: Collider): void;
21
29
  /** Release all backend state. */
@@ -1,4 +1,4 @@
1
- import type { SceneNode } from '@codexo/exojs';
1
+ import { type SceneNode } from '@codexo/exojs';
2
2
  import type { PhysicsBody } from '../PhysicsBody';
3
3
  /** Options for {@link PhysicsWorld.bind}. */
4
4
  export interface BindingOptions {
@@ -12,16 +12,16 @@ export interface BindingOptions {
12
12
  }
13
13
  /**
14
14
  * A link between a {@link PhysicsBody} and a {@link SceneNode}. After each
15
- * {@link PhysicsWorld.step}, the body's world position is written onto the node.
16
- * Rotation is written once dynamics ship (a body cannot rotate under contacts in
17
- * this collision/query release). The node must be world-space-rooted; runtime
18
- * scale is ignored and non-zero skew is rejected at bind time.
15
+ * {@link PhysicsWorld.step}, the body's world position **and rotation** are
16
+ * written onto the node (the body's angle is radians; the node's rotation is
17
+ * degrees). The node must be world-space-rooted; runtime scale is ignored and
18
+ * non-zero skew is rejected at bind time.
19
19
  */
20
20
  export declare class PhysicsBinding {
21
21
  readonly body: PhysicsBody;
22
22
  readonly node: SceneNode;
23
23
  readonly drive: 'body-to-node' | 'node-to-body';
24
24
  constructor(body: PhysicsBody, node: SceneNode, drive?: 'body-to-node' | 'node-to-body');
25
- /** Write the body's current transform onto the bound node. */
25
+ /** Write the body's current transform (position + rotation) onto the bound node. */
26
26
  sync(): void;
27
27
  }
@@ -1,9 +1,11 @@
1
+ import { MathUtils } from '@codexo/exojs';
2
+
1
3
  /**
2
4
  * A link between a {@link PhysicsBody} and a {@link SceneNode}. After each
3
- * {@link PhysicsWorld.step}, the body's world position is written onto the node.
4
- * Rotation is written once dynamics ship (a body cannot rotate under contacts in
5
- * this collision/query release). The node must be world-space-rooted; runtime
6
- * scale is ignored and non-zero skew is rejected at bind time.
5
+ * {@link PhysicsWorld.step}, the body's world position **and rotation** are
6
+ * written onto the node (the body's angle is radians; the node's rotation is
7
+ * degrees). The node must be world-space-rooted; runtime scale is ignored and
8
+ * non-zero skew is rejected at bind time.
7
9
  */
8
10
  class PhysicsBinding {
9
11
  body;
@@ -14,9 +16,10 @@ class PhysicsBinding {
14
16
  this.node = node;
15
17
  this.drive = drive;
16
18
  }
17
- /** Write the body's current transform onto the bound node. */
19
+ /** Write the body's current transform (position + rotation) onto the bound node. */
18
20
  sync() {
19
21
  this.node.setPosition(this.body.x, this.body.y);
22
+ this.node.setRotation(MathUtils.radiansToDegrees(this.body.angle));
20
23
  }
21
24
  }
22
25
 
@@ -1 +1 @@
1
- {"version":3,"file":"PhysicsBinding.js","sources":["../../../../src/binding/PhysicsBinding.ts"],"sourcesContent":[null],"names":[],"mappings":"AAeA;;;;;;AAMG;MACU,cAAc,CAAA;AAEP,IAAA,IAAA;AACA,IAAA,IAAA;AACA,IAAA,KAAA;AAHlB,IAAA,WAAA,CACkB,IAAiB,EACjB,IAAe,EACf,QAAyC,cAAc,EAAA;QAFvD,IAAA,CAAA,IAAI,GAAJ,IAAI;QACJ,IAAA,CAAA,IAAI,GAAJ,IAAI;QACJ,IAAA,CAAA,KAAK,GAAL,KAAK;IACpB;;IAGI,IAAI,GAAA;AACT,QAAA,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACjD;AACD;;;;"}
1
+ {"version":3,"file":"PhysicsBinding.js","sources":["../../../../src/binding/PhysicsBinding.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAeA;;;;;;AAMG;MACU,cAAc,CAAA;AAEP,IAAA,IAAA;AACA,IAAA,IAAA;AACA,IAAA,KAAA;AAHlB,IAAA,WAAA,CACkB,IAAiB,EACjB,IAAe,EACf,QAAyC,cAAc,EAAA;QAFvD,IAAA,CAAA,IAAI,GAAJ,IAAI;QACJ,IAAA,CAAA,IAAI,GAAJ,IAAI;QACJ,IAAA,CAAA,KAAK,GAAL,KAAK;IACpB;;IAGI,IAAI,GAAA;AACT,QAAA,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC/C,QAAA,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpE;AACD;;;;"}
@@ -10,5 +10,6 @@ import type { BroadPhase, CandidatePair } from './BroadPhase';
10
10
  */
11
11
  export declare class SweepAndPrune implements BroadPhase {
12
12
  private readonly _sorted;
13
+ private readonly _pairPool;
13
14
  computePairs(colliders: readonly Collider[], out: CandidatePair[]): CandidatePair[];
14
15
  }