Documentation
    Preparing search index...

    A PhysicsSolverAdapter (§37) backed by Rapier 2D.

    const adapter = new Rapier2dAdapter();
    await adapter.initialize({ dimension: "2d", gravity: new Vector2(0, -9.81) });
    const body = adapter.createBody({ type: "dynamic", position: new Vector2(0, 5) });
    adapter.createCollider({ body, shape: { type: "circle", radius: 0.5 } });
    adapter.step(1 / 60);
    const events = adapter.drainEvents();
    adapter.dispose();

    One instance owns exactly one Rapier World. initialize may be called once; dispose is idempotent and terminal.

    Implements

    Index
    capabilities: PhysicsCapabilities = RAPIER_2D_CAPABILITIES

    §37 capabilities. Readable before initialize, and frozen.

    name: string = ADAPTER_NAME

    §37 name.

    reportsJointReactions: false

    false: Rapier 0.19.3 cannot report a joint's reaction (SolverJointAccess, plan P6-2; verified 2026-08-01 for WP-6.2 against the installed typings and the wasm binary — see init.ts's module header for the exact evidence).

    The consequence is deliberate and visible: PhysicsWorld.addJoint refuses a joint carrying a breakForce or a breakTorque on this adapter, with an error saying so, instead of accepting a threshold that could never fire. Nothing here estimates a reaction from the bodies' momentum change — that number would be indistinguishable from a real one and wrong.

    This flips to true, with no other API change, if a later Rapier exposes ImpulseJoint::impulses() through its JavaScript bindings.

    • get version(): string

      §37 version — Rapier's own version string, once it is known.

      Empty until Rapier2dAdapter.initialize resolves, because the value lives inside the wasm module: calling version() before init() throws a TypeError from the bindings (verified at 0.19.3). This adapter would rather report "not known yet" than invent a number that §34 will bake into a snapshot's validity key.

      Returns string

    • Creates a body (§37, §22, §23).

      §22's four types map to Rapier's Fixed, Dynamic, KinematicPositionBased, and KinematicVelocityBased. Position, rotation, both velocities, both damping coefficients, and gravityScale are applied from the descriptor; §31's CCD mode is resolved from the §23 boolean and the §31 mode together, exactly as RigidBodyDescriptor.ccdMode documents.

      Rapier composes a body's mass additively, so an explicit mass cannot simply be handed to RigidBodyDesc: setAdditionalMass(5) on a body with a density-1 collider yields mass 6, and on a body with only zero-density colliders yields mass 5 with zero rotational inertia (both measured at 0.19.3). §23 says mass is authoritative, so the adapter resolves one of three MassModes here and every createCollider on this body honours it. In particular, an explicit mass with no inertia tensor is carried by the body's first collider through ColliderDesc.setMass, which gives the requested mass and a geometry-derived inertia.

      A centerOfMass or inertiaTensor without a mass is rejected: Rapier can only set the three together, and silently dropping the one the caller did supply is the kind of quiet substitution §85 exists to prevent.

      Parameters

      Returns PhysicsBodyHandle

    • Creates a collider on desc.body (§37, §24, §25).

      The full §24 2D shape list — circle, rectangle, capsule, convex polygon, polyline, chain — maps to ColliderDesc.ball, .cuboid, .capsule, .convexHull, and .polyline (twice, with different index buffers); see conversions2d.ts for the capsule axis, the polygon winding rule, and the open-versus-closed segment run. §24's compound is several colliders on one body, which needs nothing here (PH-22a).

      Friction and restitution come from the explicit fields, then the material, then @fourjs/physics's defaults. Their combine rules are set to Appendix A's — friction Average, restitution Max — because Rapier's own default for restitution is Average, which would quietly contradict §25 for every contact in the world. §25's rollingFriction and spinningFriction have no Rapier 2D binding at 0.19.3 and cannot be applied here — which this adapter declares, in capabilities.tuning, so that a PhysicsWorld registering a collider that carries one warns instead of dropping it in silence (2026-08-06).

      collisionGroups/collisionMask are packed into Rapier's single InteractionGroups word; sensor sets the sensor flag and widens ActiveCollisionTypes to ALL, because Rapier's default excludes kinematic-vs-fixed and fixed-vs-fixed pairs — a static trigger volume would otherwise never notice a kinematic character walking through it (verified, and a §29 requirement rather than a preference). Solid colliders keep Rapier's default, where those pairs are genuinely uninteresting.

      ActiveEvents.COLLISION_EVENTS is enabled on every collider: §29's five event names are part of the engine's contract, not an opt-in. CONTACT_FORCE_EVENTS is deliberately not enabled — the contact manifolds read in step carry per-contact impulses, which is strictly more information than a contact-force event's summed force and needs no threshold to be configured.

      Parameters

      Returns PhysicsColliderHandle

    • Creates a joint (§37, §28, plan P6-1; WP-6.2).

      The descriptor's anchors are body-local by the time they arrive here — PhysicsWorld.addJoint converts the user's world-space anchors once, at registration — which is exactly the space Rapier's JointData factories want, so no frame conversion happens in this method.

      §28 type Rapier 2D call
      fixed JointData.fixed(a1, rotB − rotA, a2, 0)
      revolute JointData.revolute(a1, a2) — a plane has no axis (§21)
      prismatic JointData.prismatic(a1, a2, axis)
      rope JointData.rope(maxLength, a1, a2)
      spring JointData.spring(restLength, stiffness, damping, a1, a2)

      Two of those need explaining:

      • The fixed joint's frames. Rapier's fixed joint holds rotA + frame1 == rotB + frame2, so passing two zeroes would snap the bodies to a common orientation the moment the joint is created (measured: a body at 0.7 rad is dragged to 0). §28 says the bodies keep the relative pose they had, so frame1 is the relative angle read out of the solver right now and frame2 is zero — which holds the pose instead of changing it (measured: the same body stays at 0.7 rad).
      • The revolute joint's axis. Rapier 2D's revolute factory takes none, because §21 leaves one rotational degree of freedom. A descriptor may still name −Z, and the sign is kept: it mirrors the joint's limits and its motor rate rather than being dropped (see revoluteAxisSignZ).

      collisionEnabled maps to Rapier's setContactsEnabled, which is a real per-joint switch (measured: two overlapping boxes tied by a slack rope push each other apart with it on and stay overlapped with it off). Rapier's default is contacts on; §28's default is off, so this method always sets it explicitly.

      Limits and a motor are applied immediately after creation, through the same code path Rapier2dAdapter.setJointLimits and Rapier2dAdapter.setJointMotor use, so a joint created with a motor and a joint reconfigured into one cannot drift apart.

      Parameters

      Returns PhysicsJointHandle

    • Serializes the world (§34).

      Rapier's World.takeSnapshot() captures the solver — bodies, colliders, contacts, integration parameters, gravity — and preserves its own handles across World.restoreSnapshot (verified). It does not know about this adapter's monotonic ids, mass modes, or collision-group values, all of which §33's checksum and §30's filters depend on. The buffer this method returns is therefore an envelope:

      offset  0  u32  magic            0x32523446 ("F4R2", little-endian)
      offset  4  u32  format version   2  (joint registry added, WP-6.2)
      offset  8  u32  metadata length  in bytes
      offset 12  u32  Rapier length    in bytes
      offset 16  …    metadata         UTF-8 JSON (SnapshotMeta)
      offset 16+m …   Rapier snapshot  World.takeSnapshot() bytes, verbatim
      

      The envelope is opaque adapter data in §37's sense: valid only for this adapter, this Rapier version, and this world configuration.

      Returns ArrayBuffer

    • Destroys a body and everything attached to it (§37).

      Rapier removes the body's colliders and its joints with it (verified: impulseJoints.len() drops and the joint's handle stops resolving), so their records are dropped here too, along with any contact pair those colliders were part of. The monotonic id is not reused, which is what keeps Rapier2dAdapter.forEachBody's order stable under destruction (§33).

      PhysicsWorld destroys a body's joints itself before it removes the body (§83), so this path normally finds none; it exists because an adapter used directly must not be left holding handles to constraints Rapier has already freed.

      Parameters

      Returns void

    • Destroys a collider (§37). Any contact pair it was part of is forgotten.

      A collider is a §23 mass contributor, so removing one changes the body's mass properties and the adapter's own BodyRecord.colliderIds — the list Rapier2dAdapter.createCollider reads to decide whether the next collider carries an authored mass. Until this fix neither was touched, and both failures were silent:

      • a "first-collider" body (an authored mass with no authored centre or tensor) whose mass-bearing collider was destroyed dropped to zero mass — Rapier leaves such a body motionless — and
      • because the list still held one id, its replacement collider was created with density: 0, so the mass never came back.

      What happens now, in order: the id is dropped (in #forgetCollider, which is also destroyBody's path), then, for a surviving "first-collider" body that still has colliders, the authored mass is re-applied to the surviving collider with the lowest monotonic id (Collider.setMass, §33-stable — creation order, unaffected by which collider died), and finally the body's mass properties are recomputed so getBodyMass is truthful before the next step, exactly as createCollider does.

      Re-applying rather than refusing the destruction is the honest option here: §23 makes an authored mass authoritative for the body, not for whichever collider happens to be holding it, and throwing would make "remove a collider" fail on a body that has a perfectly good place to put its mass. A body left with no collider keeps nothing — there is nowhere to hold it — and gets its mass back from the next collider created on it, which is what the corrected list now guarantees.

      Not called when the body itself is going away (2026-08-07): PhysicsWorld tears a registration down with a single destroyBody, which §37 defines as taking everything attached with it, so none of this runs for a body one line from destruction — see that method's note.

      Parameters

      Returns void

    • Creates the Rapier world (§37).

      Loads the wasm module (once per process — see init.ts), then builds a World with the §21-resolved gravity. Options are validated with @fourjs/physics's own validators, so this adapter accepts exactly what the engine accepts and no more.

      SleepingConfig.enabled maps to RigidBodyDesc.setCanSleep, applied to every body this adapter creates. linearThreshold, angularThreshold, and timeThreshold do not map: IntegrationParameters in 0.19.3 exposes dt, contact_erp, lengthUnit, normalizedAllowedLinearError, normalizedPredictionDistance, numSolverIterations, numInternalPgsIterations, minIslandSize, maxCcdSubsteps, and contact_natural_frequency — and no sleeping thresholds at all (verified by enumerating the prototype). Rapier's thresholds are compiled into the wasm. The adapter therefore honours the on/off switch and leaves the three thresholds unimplemented rather than pretending; a world that depends on them is depending on something no Rapier 0.19.3 binding can deliver.

      That last sentence is now machine-readable: capabilities.tuning .sleepThresholds is false, and PhysicsWorld warns once per world when a threshold departs from Appendix A against an adapter that declares it (2026-08-06). The resolved configuration still reaches this method in full.

      Parameters

      Returns Promise<void>

    • Tests a point (§30) — every collider that contains it.

      Each hit's point and distance come from Rapier's projectPoint(point, solid: true), which returns the query point itself for a point inside a plain shape. Since a point query only ever reports containing colliders, distance is 0 on every hit; that is PointHit's documented meaning of "the point is inside", not a field this adapter failed to fill.

      Parameters

      Returns PointHit[]

    • Casts a ray (§30).

      The direction is normalized before the cast, so Rapier's time of impact — which is measured in units of the ray direction's length — equals the distance in world units that RaycastHit.distance promises.

      Filtering is done entirely by @fourjs/physics's passesQueryFilter rather than by Rapier's own group words: one implementation of §30's filter semantics means Rapier and every future adapter answer a query identically, which is exactly the portability the §37 seam exists for.

      mode: "first" uses Rapier's nearest-hit entry point; "all" enumerates and then applies maxHits in traversal order and sorted afterwards.

      Note the module header's warning: a collider created since the last step is not yet in the broad phase and cannot be hit.

      Parameters

      Returns RaycastHit[]

    • Restores a world previously captured by Rapier2dAdapter.createSnapshot (§34).

      The Rapier world is replaced wholesale — World.restoreSnapshot builds a new one — and the old one is freed. The adapter's registry is then rebuilt from the envelope's metadata, reusing the existing record objects wherever an id survives: that is what makes handles minted before the snapshot keep working afterwards, so "body identity and ordering survive the round trip" is true in the sense §33's checksum needs. A record whose id is not in the snapshot is marked dead, and its handle is rejected from then on.

      The touching-pair set is rebuilt from the restored narrow phase, so collisionstay and collisionend keep working for pairs that were already in contact when the snapshot was taken — Rapier will not re-report those as started, so without this they would go silent.

      Joints are restored the same way as bodies (WP-6.2): Rapier's snapshot carries the constraints themselves — anchors, limits, motors, and the contact flag all come back with it, and a run continued from a restore matches the original exactly (measured) — while the envelope carries the monotonic ids, −Z axis signs, and body links that only this adapter knows.

      Parameters

      • snapshot: ArrayBuffer

      Returns void

    • Replaces §23's mass triple on a live body — SolverBodyTuningAccess.setBodyMassProperties (PH-1 stage 2, 2026-08-07).

      createBody puts an authored mass in one of two places depending on what else was authored (resolveMassMode): a bare mass goes onto the body's first collider so Rapier still derives the centre and the inertia from the geometry, while a mass with a centre or a tensor goes onto the body as additional mass properties, with every collider zeroed out of the derivation. This method makes exactly the same choice from the same inputs and rewrites BodyRecord.massMode, so that body.mass = 5 and removeBody + addBody with mass: 5 leave Rapier in the same state — and so that a later createCollider or destroyCollider on this body keeps making the right call about who carries the mass.

      A body with no colliders takes the body branch whatever was authored: the first-collider branch would have nowhere to put the mass, and losing it silently is what §23 forbids.

      recomputeMassPropertiesFromColliders() runs last for the reason Rapier2dAdapter.createBody records: Rapier does not surface a mass change in mass() until the next step otherwise, which would make getBodyMass disagree with what was just written.

      Parameters

      Returns void

    • Re-types a live body in place — SolverBodyAccess.setBodyType's contract, §22/plan P7-3 (a documented own summary rather than @inheritDoc, which cannot carry additional paragraphs).

      Rapier's setBodyType(type, wakeUp) takes wakeUp as a required argument, so the default is applied here rather than passed through.

      Nothing else is touched: measured at 0.19.3 (WP-7.2), the retype leaves the body's Rapier handle, its colliders, its pose, and its mass properties alone, which is what makes Rapier2dAdapter.getBodyId and Rapier2dAdapter.forEachBody — and with them §33's checksum order — survive a control-mode switch untouched. record.sleeping is deliberately not refreshed here: it is the previous-step snapshot §32's sleep/wake events are diffed against, so a wake caused by this call is reported by the next step like any other.

      Parameters

      Returns void

    • Replaces a collider's §24 participation — SolverBodyTuningAccess.setColliderFilter.

      ActiveCollisionTypes.ALL is set when a collider becomes a sensor, for the reason createCollider records: Rapier's DEFAULT excludes kinematic-versus-fixed, so a fixed sensor would see nothing from a kinematic body. It is not cleared again when a sensor becomes a solid collider — widening which pairs are considered changes no §29 event that a narrower set would have produced, while narrowing it mid-run could drop a pair the engine is already tracking.

      A pair that is already touching when the sensor flag flips keeps the classification it was opened with until the two separate: §29's collisionend and triggerexit must name the same event the matching …start did, and re-labelling an open pair would emit one without the other.

      Parameters

      Returns void

    • Replaces a collider's §25 surface properties — SolverBodyTuningAccess.setColliderMaterial.

      friction and restitution arrive already resolved by the engine (the collider's own field beats its PhysicsMaterial), so this is two Rapier setters and no precedence logic. The combine rules set at createCollider are left alone: they are §25 engine policy, not a per-collider property.

      density is undefined for a body whose mass was authored, and then the collider's mass contribution is left exactly as createCollider and Rapier2dAdapter.setBodyMassProperties arranged it — writing a density there would silently un-author the body's mass. When it is defined the body's derived mass is recomputed straight away, so getBodyMass never reports a figure that is one step stale.

      Parameters

      Returns void

    • Turns contact generation between the two joined bodies on or off, live (§28 collisionEnabled; PH-22f, 2026-08-08).

      ImpulseJoint.setContactsEnabled is on Rapier's base joint class, so unlike setLimits and the motor configuration it works for every §28 type this adapter builds — which is why this is the one §28 property that could stop being frozen. It is the same call createJoint makes, so a joint created with contacts on and a joint switched to contacts on cannot drift apart.

      Parameters

      Returns void

    • Reconfigures a revolute or prismatic joint's travel limits (§28 "limits"), in radians or metres.

      A −Z revolute joint's range is mirrored to [−max, −min] before it reaches Rapier, which is the same range expressed about +Z — the only axis a plane has (§21).

      A fixed, rope, or spring joint has no limited degree of freedom to move: Rapier's FixedImpulseJoint, RopeImpulseJoint, and SpringImpulseJoint carry no setLimits at all, and this method throws rather than accepting a command it would have to drop.

      Parameters

      Returns void

    • Reconfigures a revolute or prismatic joint's velocity motor (§28 "motors"), and the one place this adapter's honest limitation lives.

      Rapier 0.19.3's JavaScript bindings expose no motor force limit: the whole motor surface is configureMotorModel, configureMotorVelocity, configureMotorPosition, and configureMotor, and none of them takes a maximum (verified against the wasm exports, not only the typings). What they do take is a factor, which a measurement pins down exactly: in Rapier's ForceBased model the motor exerts

      effort = factor · (targetVelocity − currentVelocity)
      

      so factor is a damping coefficient in N·m·s/rad for a hinge and N·s/m for a slider. This adapter passes §28's maxTorque/maxForce as that factor, with ForceBased selected because §28 states the motor's strength as a torque and a force (the AccelerationBased model divides the driven body's inertia out, which would make the number mean something else again).

      The consequence, stated plainly: a bigger maxTorque is a stronger motor — the monotone relationship §28 intends, and enough for the motor to reach its commanded velocity — but it is not the hard ceiling §28 describes. The motor's effort equals maxTorque at one rad/s (or one m/s) of velocity error and exceeds it beyond that. No JavaScript-reachable Rapier API can enforce the ceiling, and this adapter will not simulate one it does not have.

      Rapier cannot remove a motor once one has been configured, and configureMotorVelocity(0, 0) is not the way to switch one off: a zero gain is Rapier's rigid zero-velocity constraint, a brake rather than a release (measured: a bar turning at 3 rad/s falls to 0.294 rad/s in one step and to 5.9e-44 rad/s within a second, where the same bar with no motor keeps turning). A motor that arrives disabled — or whose maxEffort is 0, which asks for a drive that exerts nothing — is therefore configured with INERT_MOTOR_GAIN and a zero target, the setting measured to be bit-identical to a joint that never had a motor at all: max |Δ| = 0 over 3600 steps on a hinge and a slider, and over 600 steps continuing from a joint that had just been running a strong motor. Rapier3dAdapter does the same thing with the same constant, so a disabled motor means one thing across this package.

      What that buys the caller: a running motor can be released, and the axis is then as free as an unmotorized one. What it does not do is restore the joint's pre-motor history — the body keeps whatever velocity the motor gave it and coasts from there, which is what releasing a drive means.

      Parameters

      Returns void

    • Sweeps a shape (§30).

      Multiplicity limit, declared rather than hidden: Rapier 0.19.3's 2D query surface exposes castShape, which returns the first impact and nothing else — there is no "all shape-cast hits" entry point. This method therefore returns at most one hit whatever mode, maxHits, or sorted ask for. capabilities.queries.shapeCast stays true because the query is implemented; the cap is a property of the solver, and a caller that needs every crossing should step the sweep or use overlap.

      ShapeCastHit.point is Rapier's witness1, which is world-space on the hit collider, and normal is witness1's normal, pointing out of it (both verified at 0.19.3 — witness2 is in the cast shape's local frame and is not used).

      Parameters

      Returns ShapeCastHit[]

    • Advances the solver by exactly delta seconds (§37, §10) and collects the step's events.

      The events are gathered here rather than in drainEvents for two reasons, both Rapier's: the EventQueue is auto-drained at the start of the next step, and contact manifolds only carry the impulses the solver just applied for as long as no further stepping has happened.

      Rapier reports only started and stopped. §29 names three collision phases, so somebody has to derive the middle one, and this adapter does it rather than leaving each caller to reinvent the bookkeeping (decision, WP-5.4 — the alternative, "the adapter emits start/end only and @fourjs/physics derives stay", was rejected because the contact manifold a collisionstay payload needs is only readable here, immediately after the solve).

      The rule: a pair that Rapier reported as started is remembered; on every later step where it has not been reported as stopped, a collisionstay carrying a fresh manifold is emitted; when Rapier reports it stopped, a collisionend is emitted and the pair is forgotten. Trigger (sensor) pairs are tracked the same way but produce no stay, because §29 defines only triggerenter and triggerexit.

      Deterministic and documented: all collisionstart/triggerenter in Rapier's queue order, then every collisionstay in pair-creation order, then all collisionend/triggerexit in queue order, then every §32 sleep/wake in body-creation order.

      Parameters

      • delta: number

      Returns void