export interface PhysicsBounds { min: readonly [number, number, number]; max: readonly [number, number, number]; } /** * Simulation fidelity intent: `low` (cheap — many bodies, loose stacks, forgiving sleep), `standard` * (default), `high` (tight stacks, accurate joints, worth the extra solver work). Sets the solver * knobs below to a matched preset; an individual knob left explicit always wins over the preset. */ export type PhysicsPrecision = "low" | "standard" | "high"; export interface PhysicsWorldConfig { /** Maximum bodies the SoA buffers can hold. Fixed at construction; no reallocation later. */ capacity: number; /** AABB container; every body is constrained inside it (floor included). */ bounds: PhysicsBounds; /** Downward acceleration applied to awake bodies each substep. Default -20. */ gravity?: number; /** Simulation fidelity intent. Default "standard" — see {@link PhysicsPrecision}. */ precision?: PhysicsPrecision; /** Fixed substep length in seconds. Default 1/60. */ fixedDt?: number; /** Cap on substeps per `step()` — bounds catch-up work, drops the rest. Default 4. */ maxSubsteps?: number; /** Bounce factor on contacts and walls, 0..1. Default 0.2. */ restitution?: number; /** Approach speed below which restitution is ignored (contacts rest instead of jittering). Default 1. */ restitutionThreshold?: number; /** Coulomb friction coefficient on contacts and the floor, 0..1. Default 0.4. */ friction?: number; /** Per-second linear velocity damping (physical energy bleed so piles settle and sleep). Default 0.2. */ linearDamping?: number; /** Speed at or below which a body counts as still. Default 0.08. */ sleepLinearVelocity?: number; /** Maximum simultaneous joints the constraint buffers can hold. Default 256. */ jointCapacity?: number; /** * @deprecated Advanced escape hatch — tune only when profiling shows the broadphase grid is the * bottleneck; most games never set this. Broadphase cell edge; ideally ≈ small-body diameter. * Default 1. */ cellSize?: number; /** @deprecated Advanced escape hatch — prefer `precision`. Sequential-impulse velocity iterations per substep (higher = more stable stacks). */ solverIterations?: number; /** @deprecated Advanced escape hatch — prefer `precision`. Baumgarte positional-correction fraction per substep, 0..1. */ correctionFactor?: number; /** @deprecated Advanced escape hatch — prefer `precision`. Penetration tolerated before correction kicks in. Default 0.005. */ slop?: number; /** @deprecated Advanced escape hatch — prefer `precision`. Consecutive still substeps before a body sleeps. */ sleepThresholdSteps?: number; /** @deprecated Advanced escape hatch — prefer `precision`. Approach speed a contact must exceed to wake a sleeping body (resting contacts don't). */ wakeThreshold?: number; /** @deprecated Advanced escape hatch — prefer `precision`. Baumgarte fraction applied to hard-joint positional error per iteration, 0..1. */ jointCorrection?: number; } export interface PhysicsStats { count: number; awake: number; sleeping: number; contacts: number; pairs: number; substeps: number; stepMs: number; } export interface BodyCommonOptions { position: readonly [number, number, number]; velocity?: readonly [number, number, number]; /** ≤ 0 or omitted with `static` → infinite mass (immovable collider). Default 1. */ mass?: number; /** Immovable collider: never integrates, never sleeps-out, invMass 0. */ static?: boolean; /** Seed the body asleep (a settled bed pays no integration cost until woken). */ asleep?: boolean; } export interface BoxBodyOptions extends BodyCommonOptions { shape?: "box"; halfExtents: readonly [number, number, number]; } /** The radius fills all three half-extent columns, so broadphase and bounds see the sphere's enclosing AABB. */ export interface SphereBodyOptions extends BodyCommonOptions { shape: "sphere"; radius: number; } export type AddBodyOptions = BoxBodyOptions | SphereBodyOptions; export declare const SHAPE_BOX = 0; export declare const SHAPE_SPHERE = 1; /** * Hard bilateral: `fixed` pins the shared anchor (3-DOF), `hinge` pins only the plane * perpendicular to `axis` (free along the axis — translational cylindrical), `distance` * holds a fixed separation. `spring` is a soft PD drive toward `restLength`. */ export type JointKind = "hinge" | "distance" | "spring" | "fixed"; export declare const JOINT_FIXED = 0; export declare const JOINT_HINGE = 1; export declare const JOINT_DISTANCE = 2; export declare const JOINT_SPRING = 3; export interface JointOptions { /** First body index. */ bodyA: number; /** Second body index, or omit / < 0 to anchor A to a fixed world point (`anchorB`). */ bodyB?: number; /** Local offset of the anchor on A, in world axes (no rotation). Default [0,0,0]. */ anchorA?: readonly [number, number, number]; /** Local offset on B, or — when B is a world anchor — the world point itself. Default [0,0,0]. */ anchorB?: readonly [number, number, number]; /** Target separation for `distance`/`spring`. Defaults to the anchors' current distance. */ restLength?: number; /** Spring restoring rate. Default 40. */ stiffness?: number; /** Spring damping (relative-velocity bleed). Default 6. */ damping?: number; /** Clamp on the per-substep spring impulse (a follow-point that never yanks). Default unbounded. */ maxImpulse?: number; /** Unit hinge axis (world). Required semantics for `hingeJoint`; free motion along this axis. Default [0,1,0]. */ axis?: readonly [number, number, number]; } /** A contact reported to `onCollision`. The object is reused each call — read/copy, never retain. */ export interface CollisionEvent { a: number; b: number; nx: number; ny: number; nz: number; approachSpeed: number; impulse: number; } /** Cap on `nx*ny*nz` broadphase cells — guards a huge-bounds/tiny-cellSize config from hanging `step()`. */ export declare const MAX_BROADPHASE_CELLS = 1000000; /** Grid cell containing a point, clamped into the grid. Pure — exported for tests. */ export declare function cellCoord(value: number, min: number, cellSize: number, cells: number): number; /** Linear cell index from clamped 3D coords. Pure — exported for tests. */ export declare function cellIndex(cx: number, cy: number, cz: number, nx: number, ny: number): number; export declare class PhysicsWorld { readonly capacity: number; readonly bounds: PhysicsBounds; readonly gravity: number; readonly cellSize: number; readonly fixedDt: number; readonly maxSubsteps: number; readonly restitution: number; readonly restitutionThreshold: number; readonly friction: number; readonly linearDamping: number; readonly solverIterations: number; readonly correctionFactor: number; readonly slop: number; readonly sleepMove2: number; readonly sleepSteps: number; readonly wakeThreshold: number; readonly jointCapacity: number; readonly jointCorrection: number; readonly posX: Float32Array; readonly posY: Float32Array; readonly posZ: Float32Array; readonly velX: Float32Array; readonly velY: Float32Array; readonly velZ: Float32Array; readonly halfX: Float32Array; readonly halfY: Float32Array; readonly halfZ: Float32Array; readonly invMass: Float32Array; readonly flags: Uint8Array; /** `SHAPE_BOX` (0) or `SHAPE_SPHERE` (1) per body; spheres keep their radius in halfX/halfY/halfZ. */ readonly shape: Uint8Array; /** 1 for a body that took part in a contact this frame; reset each `step()`. Render tint reads this. */ readonly contact: Uint8Array; private readonly sleepTimer; private readonly prevX; private readonly prevY; private readonly prevZ; private readonly nx; private readonly ny; private readonly nz; private readonly numCells; private readonly cellStart; private readonly cursor; private sorted; private readonly bodyCell; private readonly awakeList; private readonly awakeSlot; private awakeCount; private readonly jKind; private readonly jActive; private readonly jBodyA; private readonly jBodyB; private readonly jAnchorAX; private readonly jAnchorAY; private readonly jAnchorAZ; private readonly jAnchorBX; private readonly jAnchorBY; private readonly jAnchorBZ; private readonly jAxisX; private readonly jAxisY; private readonly jAxisZ; private readonly jRest; private readonly jStiffness; private readonly jDamping; private readonly jMaxImpulse; private readonly jFree; private jFreeCount; private jointHigh; private jointActiveCount; private readonly anchorScratch; private readonly bodyFree; private bodyFreeCount; private bodyHigh; private collisionListener; private collisionMinSpeed; private readonly collisionEvent; private bodyCount; private accumulator; private readonly stats; constructor(config: PhysicsWorldConfig); private addAwake; private removeAwake; get count(): number; /** One past the highest slot `addBody` has ever handed out — the range `removeBody` leaves holes in. */ get highWater(): number; get cells(): { nx: number; ny: number; nz: number; total: number; }; get jointCount(): number; /** * Deliver every impacting contact (approach speed ≥ `minApproachSpeed`) to `listener` during * `step`. The event object is reused — read/copy it, never retain. Pass `null` to detach. This is * the seam crash-damage and destruction read; contacts otherwise stay inside the sim. */ onCollision(listener: ((e: CollisionEvent) => void) | null, minApproachSpeed?: number): void; hingeJoint(options: JointOptions): number; fixedJoint(options: JointOptions): number; distanceJoint(options: JointOptions): number; springJoint(options: JointOptions): number; private addJoint; removeJoint(id: number): void; /** Move a joint's world anchor point (only meaningful when `bodyB` is a world anchor). */ setJointAnchor(id: number, x: number, y: number, z: number): void; /** Move a joint's local anchor offset on A (world axes, no rotation applied by the solver). */ setJointAnchorA(id: number, x: number, y: number, z: number): void; setJointRest(id: number, restLength: number): void; /** * Gather joint `id`'s two world anchor points into {@link anchorScratch} as [ax,ay,az,bx,by,bz] * (A's local offset added to body A's centre, or world-origin when A is unset; B likewise, treating * `anchorB` as a world point when B is unset). The single definition shared by {@link jointDistance}, * {@link readJointSegments}, and {@link solveJoints}. */ private anchorWorld; private jointDistance; /** Write [ax,ay,az,bx,by,bz] world endpoints per active joint into `out`; returns the joint count. */ readJointSegments(out: Float32Array): number; addBody(options: AddBodyOptions): number; /** * Remove a body: it drops out of integration, the solver, and the broadphase, and its slot is * queued for the next `addBody` to reuse. `id` and every other body's `id` stay valid — a dead * slot is tombstoned (`FLAG_DEAD`), never swapped, since ids are handed out raw and stored as-is * in joints and game state. There is no persistent contact set to consult, so — conservatively — * any sleeping body whose AABB touches the removed body's (padded by `slop`) is woken; a body * that was resting on it always qualifies. */ removeBody(id: number): void; private wakeTouching; isSleeping(i: number): boolean; isAlive(i: number): boolean; wake(i: number): void; /** Overwrite a body's velocity directly, waking it if asleep. */ setVelocity(id: number, x: number, y: number, z: number): void; /** Move a body, keeping its current velocity, and wake it if asleep. */ setPosition(id: number, x: number, y: number, z: number): void; /** Move a body and zero its velocity — a hard reset (respawn, teleporter) instead of a slide. */ teleport(id: number, x: number, y: number, z: number): void; wakeAll(): void; clear(): void; /** Advance by a real frame delta; runs whole fixed substeps and carries the remainder. */ step(frameDt: number): PhysicsStats; getStats(): PhysicsStats; private substep; private constrainBounds; /** * Walk every broadphase cell body `i`'s AABB overlaps, invoking `visit(cell)` once per cell. The * cell-range math (six `cellCoord` calls plus the row-major triple loop) lives here so the count * and scatter passes of {@link buildGrid} share one definition. `visit` is a caller-hoisted closure * (created once per `buildGrid`, not per body) so the hot loop stays allocation-free. */ private forEachBodyCell; private buildGrid; private cellOf; private solve; private resolve; /** * Apply an axial velocity impulse `jn` along unit axis (nX,nY,nZ) to a joint's two bodies: body A * recoils (−) and body B advances (+), each scaled by its inverse mass. A world anchor (body index * < 0) is skipped. Scalar args only — no allocation on the joint hot path. */ private applyAxialImpulse; /** * Positional (Baumgarte) twin of {@link applyAxialImpulse}: nudge the joint's two bodies along * (nX,nY,nZ) by `corr` — body A advances (+), body B recoils (−), each scaled by its inverse mass. */ private applyAxialCorrection; private solveJoints; private updateSleep; }