API reference
Every public class, method, and event in @codexo/exojs. Generated from source.
classPhysicsWorld
The collision/query world: owns bodies, colliders, the detection backend, bindings, the query engine and the fixed-step accumulator. Stepped by the caller (commonly from a `Scene.update`), each fixed sub-step it integrates body velocities, runs broad- and narrow-phase detection, solves contacts and integrates positions, then fires immutable contact/sensor events and writes bound node transforms. It holds **no module-level state**, so any number of worlds run in isolation. The dynamics are a native, warm-started **TGS-Soft** solver (Box2D-v3 "soft step"): each fixed step runs detection once, then several sub-steps, each integrating gravity over the sub-step and solving contacts with a soft position bias plus a bias-free relax pass; a 2-point block normal solve propagates stack loads, and restitution is a separate final pass. Decoupling stiffness from the iteration count keeps tall towers stable. The detection backend sits behind an internal seam, so the solver is swappable without touching this public surface. **Operating envelope.** The soft solver trades a little accuracy for robustness, so it has a few documented limits - each stays finite and stable, and each is pinned by a gate in `dynamics.test.ts` or `contact-push-out.test.ts`: - **Resting contacts** settle within a small, fixed tolerance. A face contact settles at exactly that tolerance; a single-point contact settles slightly deeper, and that gap grows with acceleration. Very high accelerations eventually exceed what the push-out can resolve within a step, at which point the contact holds a slightly deeper resting depth instead of the tolerance - keep gravity in the range ordinary 2D games use. - **Mass ratio** - contacts between bodies of very different mass degrade gradually rather than at a fixed ratio. What sets the practical limit is how thick the supporting geometry is relative to the lighter body, not the ratio alone: a light body squeezed against a boundary thinner than itself is the case that fails first, and it fails by sinking through. - **Scale-relative behaviour** - the solver's tolerances are absolute lengths, so a very small shape sees them as a large fraction of itself, and a very large one as a negligible one. Keep the shapes of one world within a couple of orders of magnitude of each other. - **CCD is opt-in and translation-only** - detection runs once per fixed step, so an ordinary body that crosses more than roughly half a barrier's total thickness within one step may end up on the wrong side of it, either passing through or being resolved out of the far face. Flag fast projectiles with PhysicsBody.isBullet: each of the body's colliders is then shape-cast along the step's motion (an exact translation-only sweep of the full shape, not just the centre) and clamped at the first impact. Rotation over the step is not swept - a long body spinning fast enough to sweep past an obstacle within one step can still miss it. - **PhysicsWorldOptions.subStepCount** - the default `4` is load-bearing for tall-stack stability; lowering it below `2` visibly degrades stacking, so do not reduce it for performance. - **Broad phase is a dynamic AABB tree** (`AabbTreeBroadPhase`), stateful across fixed steps: a collider whose tight AABB stays inside its stored fat AABB costs nothing to re-sync, and only colliders that actually move outside their margin trigger a tree update and a local re-query for new neighbours. Detection still walks every live collider once per step (a cheap containment check for each), so there is a small linear floor, but the dominant cost - reinsertion and neighbour discovery - is driven by how much actually moved, not by the total live collider count; sleeping bodies skip that dominant cost entirely. Scales to tens of thousands of simultaneously-live colliders, including dense clusters that would degrade a sort-and-sweep broad phase; very large or highly dynamic worlds may still benefit from splitting into several smaller `PhysicsWorld` instances (e.g. per room/chunk).
import { PhysicsWorld } from '@codexo/exojs-physics'The collision/query world: owns bodies, colliders, the detection backend, bindings, the query engine and the fixed-step accumulator. Stepped by the caller (commonly from a `Scene.update`), each fixed sub-step it integrates body velocities, runs broad- and narrow-phase detection, solves contacts and integrates positions, then fires immutable contact/sensor events and writes bound node transforms. It holds **no module-level state**, so any number of worlds run in isolation.
The dynamics are a native, warm-started **TGS-Soft** solver (Box2D-v3 "soft step"): each fixed step runs detection once, then several sub-steps, each integrating gravity over the sub-step and solving contacts with a soft position bias plus a bias-free relax pass; a 2-point block normal solve propagates stack loads, and restitution is a separate final pass. Decoupling stiffness from the iteration count keeps tall towers stable. The detection backend sits behind an internal seam, so the solver is swappable without touching this public surface.
**Operating envelope.** The soft solver trades a little accuracy for robustness, so it has a few documented limits - each stays finite and stable, and each is pinned by a gate in `dynamics.test.ts` or `contact-push-out.test.ts`: - **Resting contacts** settle within a small, fixed tolerance. A face contact settles at exactly that tolerance; a single-point contact settles slightly deeper, and that gap grows with acceleration. Very high accelerations eventually exceed what the push-out can resolve within a step, at which point the contact holds a slightly deeper resting depth instead of the tolerance - keep gravity in the range ordinary 2D games use. - **Mass ratio** - contacts between bodies of very different mass degrade gradually rather than at a fixed ratio. What sets the practical limit is how thick the supporting geometry is relative to the lighter body, not the ratio alone: a light body squeezed against a boundary thinner than itself is the case that fails first, and it fails by sinking through. - **Scale-relative behaviour** - the solver's tolerances are absolute lengths, so a very small shape sees them as a large fraction of itself, and a very large one as a negligible one. Keep the shapes of one world within a couple of orders of magnitude of each other. - **CCD is opt-in and translation-only** - detection runs once per fixed step, so an ordinary body that crosses more than roughly half a barrier's total thickness within one step may end up on the wrong side of it, either passing through or being resolved out of the far face. Flag fast projectiles with PhysicsBody.isBullet: each of the body's colliders is then shape-cast along the step's motion (an exact translation-only sweep of the full shape, not just the centre) and clamped at the first impact. Rotation over the step is not swept - a long body spinning fast enough to sweep past an obstacle within one step can still miss it. - **PhysicsWorldOptions.subStepCount** - the default `4` is load-bearing for tall-stack stability; lowering it below `2` visibly degrades stacking, so do not reduce it for performance. - **Broad phase is a dynamic AABB tree** (`AabbTreeBroadPhase`), stateful across fixed steps: a collider whose tight AABB stays inside its stored fat AABB costs nothing to re-sync, and only colliders that actually move outside their margin trigger a tree update and a local re-query for new neighbours. Detection still walks every live collider once per step (a cheap containment check for each), so there is a small linear floor, but the dominant cost - reinsertion and neighbour discovery - is driven by how much actually moved, not by the total live collider count; sleeping bodies skip that dominant cost entirely. Scales to tens of thousands of simultaneously-live colliders, including dense clusters that would degrade a sort-and-sweep broad phase; very large or highly dynamic worlds may still benefit from splitting into several smaller `PhysicsWorld` instances (e.g. per room/chunk).
new(options: PhysicsWorldOptionsConstruction options for a PhysicsWorld.): PhysicsWorldadd(body: PhysicsBodyA rigid body: a world transform plus mass properties aggregated from its colliders. Dynamic bodies integrate under gravity, accumulated forces/torque and conta…): PhysicsBodyA rigid body: a world transform plus mass properties aggregated from its colliders. Dynamic bodies integrate under gravity, accumulated forces/torque and conta…addJoint(joint: T): Tattach(node: SceneNodeTransform-bearing leaf in the scene-graph hierarchy. Carries position, rotation, scale, skew and origin. Implements Collidable so any node can participate dire…, options: AttachOptionsPhysicsWorld.attach convenience options: a body type plus a single collider, attached to a scene node in one call.): PhysicsBodyA rigid body: a world transform plus mass properties aggregated from its colliders. Dynamic bodies integrate under gravity, accumulated forces/torque and conta…bind(body: PhysicsBodyA rigid body: a world transform plus mass properties aggregated from its colliders. Dynamic bodies integrate under gravity, accumulated forces/torque and conta…, node: SceneNodeTransform-bearing leaf in the scene-graph hierarchy. Carries position, rotation, scale, skew and origin. Implements Collidable so any node can participate dire…): PhysicsBindingA link between a PhysicsBody and a SceneNode. The body's world position **and rotation** are written onto the node (the body's angle is radians; the node's rot…destroy(): voidforEachAabbHit(bounds: AabbLikeStructural type for an axis-aligned bounding box given as min/max extents. This is the canonical AABB contract across the engine and its packages - broad phase…, filter: Partial<{ category: number; group: number; mask: number }> | undefined, callback: (collider: ColliderGeometry attached to a PhysicsBody: a Shape plus a body-local offset/rotation, material (friction/restitution/density) and a collision filter. A body may own s…) => void): voidoverlapShape(shape: AnyShapeDiscriminated union of the concrete shape kinds. Narrow via the literal `shape.type` discriminant (`'circle'` → CircleShape, `'capsule'` → CapsuleShape, `'poly…, position: Readonly<PointLikeStructural type for any object with a 2D `x`/`y` position.>, filter?: Partial<{ category: number; group: number; mask: number }>, angle?: number): ColliderGeometry attached to a PhysicsBody: a Shape plus a body-local offset/rotation, material (friction/restitution/density) and a collision filter. A body may own s…[]queryAabb(bounds: AabbLikeStructural type for an axis-aligned bounding box given as min/max extents. This is the canonical AABB contract across the engine and its packages - broad phase…, filter?: Partial<{ category: number; group: number; mask: number }>, out?: ColliderGeometry attached to a PhysicsBody: a Shape plus a body-local offset/rotation, material (friction/restitution/density) and a collision filter. A body may own s…[]): ColliderGeometry attached to a PhysicsBody: a Shape plus a body-local offset/rotation, material (friction/restitution/density) and a collision filter. A body may own s…[]queryPoint(point: Readonly<PointLikeStructural type for any object with a 2D `x`/`y` position.>, filter?: Partial<{ category: number; group: number; mask: number }>): ColliderGeometry attached to a PhysicsBody: a Shape plus a body-local offset/rotation, material (friction/restitution/density) and a collision filter. A body may own s…[]rayCast(origin: Readonly<PointLikeStructural type for any object with a 2D `x`/`y` position.>, direction: Readonly<PointLikeStructural type for any object with a 2D `x`/`y` position.>, filter?: Partial<{ category: number; group: number; mask: number }>, maxDistance?: number): RayHitA single ray-cast intersection. | nullrayCastAll(origin: Readonly<PointLikeStructural type for any object with a 2D `x`/`y` position.>, direction: Readonly<PointLikeStructural type for any object with a 2D `x`/`y` position.>, filter?: Partial<{ category: number; group: number; mask: number }>, out?: RayHitA single ray-cast intersection.[], maxDistance?: number): RayHitA single ray-cast intersection.[]step(frameDeltaSeconds: number): voidcontactHertz: numberdampingRatio: numberenableSleeping: booleanframeAlphaSource: () => numberinterpolation: booleansleepAngularVelocity: numbersleepLinearVelocity: numbersubStepCount: numbertimeToSleep: numberbackend: PhysicsBackendonCollisionEnd: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[CollisionEventImmutable per-dispatch snapshot describing a solid (non-sensor) contact. Frozen: it is safe to read during the callback and to copy fields out, but it is never…]>onCollisionStart: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[CollisionEventImmutable per-dispatch snapshot describing a solid (non-sensor) contact. Frozen: it is safe to read during the callback and to copy fields out, but it is never…]>onSensorEnter: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[SensorEventImmutable per-dispatch snapshot for a sensor overlap. `sensor` is the sensor-flagged collider; `other` is the collider that entered/left it. When both collider…]>onSensorExit: SignalLightweight typed event emitter. Each `Signal` represents one named notification channel (e.g. `onResize`, `onFrame`). Listeners are added with Signal.add or S…<[SensorEventImmutable per-dispatch snapshot for a sensor overlap. `sensor` is the sensor-flagged collider; `other` is the collider that entered/left it. When both collider…]>