Compose and test 2D physics
Use PhysicsWorld2D around normal Pibbl components with physics attachments.
Pibbl supplies the clock and component lifecycle; the bundled Rapier engine solves
solid contacts. Physics and geometry subpaths are included in @pibbl/core;
Rapier requires no separate installation. Core drawing and pure geometry
imports do not load or initialize the engine. Do not start another animation loop or call the engine directly.
import { Circle, Group, Rectangle } from "@pibbl/core";import { PhysicsWorld2D, applyImpulse2D, collider2D, pibblPhysicsBody2D, dynamicBody2D, staticBody2D,} from "@pibbl/core/physics/2d";
function Ball() { const body = pibblPhysicsBody2D(); return ( <Group physics={dynamicBody2D({ handle: body, mass: 1, collider: false })} style={{ translateX: 80, translateY: 30 }} > <Circle physics={collider2D({ restitution: 0.2, friction: 0.5 })} style={{ radius: 12, fill: "#2563eb" }} onPointerDown={() => applyImpulse2D(body, [30, -180])} /> </Group> );}
function PhysicsPanel() { return ( <PhysicsWorld2D space="local" gravity={[0, 600]}> <Rectangle physics={staticBody2D()} style={{ left: 0, top: 180, width: 240, height: 16, fill: "#334155" }} /> <Ball /> </PhysicsWorld2D> );}Render two keyed PhysicsPanel instances to get independent worlds. Move or
scale their parents without resetting either simulation. Try the
resizable panels example and the
drag-and-throw example.
Coordinates and one pose owner
Section titled “Coordinates and one pose owner”space="local" makes geometry, gravity, body poses, and queries relative to the
world component. space="root" is the compatibility default. space is creation
only; change the component key to choose a different space. A local viewport
must have a finite invertible transform. Body transforms inside that viewport
still need to be rigid; put decorative scaling outside the world boundary or
inside the body’s visual children with explicit collision geometry.
Pointer events always report root-logical coordinates. Convert them with
toPhysicsPoint2D(world, [event.x, event.y]) before using local queries or targets;
fromPhysicsPoint2D converts back.
Dynamic bodies own their pose after mounting. Initial linearVelocity and
angularVelocityDegreesPerSecond seed motion; later commands set velocity,
apply an impulse, apply one fixed-tick force/torque, or teleport. Changing the
initial drawing transform is not a dynamic-body motion command. Static bodies
follow authored geometry. Kinematic bodies follow target signals and transfer
motion through contacts. Use one of these owners at a time when implementing
dragging or animation handoff. Keep the handle stable and use its enabled and
generation signals when checking lifecycle state.
A compound body is a Group with collider: false and child collider2D
attachments. Children may be solids or sensors; custom geometry comes from the
pure @pibbl/core/physics/2d/geometry factories. mass: "auto" derives mass and center
of mass from contributing colliders; explicit mass gives a predictable impulse
response. Public body position is the authored origin, not necessarily its COM.
Timing, sleeping, and collision limits
Section titled “Timing, sleeping, and collision limits”Distances use logical Canvas units, velocities use units/second, angles use
degrees, and the world fixed step uses milliseconds. Gravity defaults to zero;
[0, 600] is a practical screen-space starting point. The default is 60 fixed
ticks/second with at most four catch-up ticks per frame. Sleeping bodies and
stationary kinematic worlds stop requesting frames. Commands and target changes
wake them through the same Pibbl scheduler.
The engine internally uses 100 logical units per meter and single-precision
numbers. Use tolerances in assertions. velocityIterations (default 4) selects
Rapier solver iterations; positionIterations (default 1) selects internal PGS
iterations. These retained option names describe budgets, not exact trajectories.
Set ccd: true for fast dynamic solids. Pibbl adds bounded substeps for opposing
fast bodies; a step needing more than 64 rejects and rolls back with an error.
Reduce speed or fixed-step length, or enlarge the collision geometry. Sensors
observe discrete overlaps, so fast triggers should use explicit ray/shape casts.
Ellipses and rounded corners use convex approximations with at most 0.01-unit
chord error and a 4096-vertex safety limit. Pure queries retain the mathematical
shape. This is not a claim of exact arbitrary-speed collision detection.
World settings are reactive except space. Attachment material, density,
filtering, enabled state and damping changes reconcile on a successful render.
Hooks and keyed component identity determine ownership; unmounting invalidates
handles and removes the body. Commands on stale handles cannot affect a later
body generation. Complete-phase callbacks can queue commands for the next frame.
Observe before judging pixels
Section titled “Observe before judging pixels”Capture a world handle with pibblPhysicsWorld2D() inside the world, then call
inspectPhysics2D(world) outside render to get frozen JSON-safe observations.
It includes body identity, pose, presentation pose, velocity, sleep state,
collider bounds, contacts and failed-frame diagnostics. String collider tags
become labels; arbitrary tag objects are not serialized. Inspection does not
advance the simulation and is not an engine save/restore format.
<PhysicsDebug2D world={world} /> draws authoritative bounds, centers of mass and
contact normals without intercepting pointer events. Between fixed ticks, the
interpolated drawing may lag the authoritative overlay slightly; that difference
is expected and separately exposed by inspection.
Reproduce a failure with the supported clock
Section titled “Reproduce a failure with the supported clock”Run this in an isolated browser test. This complete fixture owns and attaches its Canvas and uses the same public components as a manual scene.
import { expect } from "vitest";import { Circle, Rectangle } from "@pibbl/core";import { createPhysicsTestHarness2D } from "@pibbl/core/physics/2d/testing";import { PhysicsWorld2D, pibblPhysicsWorld2D, dynamicBody2D, staticBody2D, inspectPhysics2D, type PibblPhysicsWorldHandle2D,} from "@pibbl/core/physics/2d";
const canvas = document.createElement("canvas");canvas.width = 240;canvas.height = 220;document.body.append(canvas);let world!: PibblPhysicsWorldHandle2D;function Contents() { world = pibblPhysicsWorld2D(); return ( <> <Circle physics={dynamicBody2D({ mass: 1 })} style={{ cx: 80, cy: 30, radius: 12 }} /> <Rectangle physics={staticBody2D()} style={{ left: 0, top: 180, width: 240, height: 16 }} /> </> );}function Scene() { return ( <PhysicsWorld2D gravity={[0, 600]}> <Contents /> </PhysicsWorld2D> );}
const harness = createPhysicsTestHarness2D({ canvas, root: () => <Scene />, observe: () => inspectPhysics2D(world),});try { harness.flush(); // render the scene harness.flush(); // accept topology and anchor physics time harness.advanceFrames(300, 1000 / 60); const state = harness.observe(); expect( state.bodies.every((body) => Number.isFinite(body.pose.position[1])), ).toBe(true); expect(state.bodies.find((body) => body.type === "dynamic")?.sleeping).toBe( true, );} finally { harness.dispose(); canvas.remove();}advanceFrames advances pending host frames, not guaranteed fixed ticks. It
stops when the realm becomes idle. act(callback) runs an action and flushes one
frame at the current time; subsequent physics may require another frame.
reset() remounts the root factory while keeping host time monotonic. It does
not reset caller-owned signals. A harness rejects an already-mounted Pibbl realm
so it cannot take over a production clock. Dispose it before mounting another.
For an agent-written regression, save the scene/seed, engine version, world settings, exact frame times, actions, and the failed observation. Assert bounds, contact events, sleep, identity and cleanup before using screenshots. Test the same public scene used for manual review; do not force sleep or teleport bodies to make a settling or collision claim pass.
Loading and cost
Section titled “Loading and cost”The runtime entry initializes embedded WebAssembly using top-level await. Use an ES-module-capable bundler; no separate WASM URL or setup hook is necessary. The pure geometry entries avoid engine initialization. Startup and bundle size are real costs; dynamically import the runtime when physics is optional.
One world is generally cheaper than many tiny worlds when objects must collide. Keep diagnostics opt-in, reuse immutable shape descriptions, and let sleeping stop work. Pibbl performs transaction checkpoints so failed frames can roll back; raw engine benchmarks do not include that cost or Canvas repainting.
Implementation guidance for agents
Section titled “Implementation guidance for agents”Read the Physics and geometry companion for ownership, adaptation, failure modes, and verification. Agent start provides the version-selection workflow.
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision c321a83. ALPHA — NOT FOR PRODUCTION USE.