Readonlycapabilities§37 capabilities. Readable before initialize, and frozen.
Readonlyname§37 name.
Readonlyreportsfalse: 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.
§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.
§26 body.applyAngularImpulse(angularImpulse) about +Z.
§26 body.applyForce(force) — newtons, at the centre of mass.
§26 body.applyForceAtPoint(force, worldPoint).
§26 body.applyImpulse(impulse) — newton-seconds at the centre of mass.
§26 body.applyImpulseAtPoint(impulse, worldPoint).
§26 body.applyTorque(torque) — newton-metres about +Z.
Number of contact points across collider manifolds in the current solver world.
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.
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.
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:
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).−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.
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.
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.
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:
"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 — anddensity: 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.
Destroys a joint (§37, §28). The handle is rejected from then on.
The two bodies are woken, because a constraint disappearing is a change they have to react to — a sleeping body still hanging from a joint that no longer exists would stay frozen in mid-air until something else touched it.
Releases the Rapier world and event queue (§37, §83). Idempotent and terminal: every handle this adapter minted is invalid afterwards, and every other method throws.
Returns and clears the events accumulated during the preceding step
(§37).
Order must be deterministic for a given step. Returning an empty array is normal. The physics package normalizes these into node-facing events (§101) and dispatches them after the step (§6b, §29) — an adapter never calls user code itself.
Visits every live body in creation order, which is ascending RapierBodyAccess.getBodyId because ids are monotonic and never reused. This is the iteration §33 requires: destroying a body removes it from the sequence and creating one appends, so no destruction can permute the order a checksum is taken in.
Visits every live collider in creation order.
Visits every live joint in creation order, which is ascending Rapier2dAdapter.getJointId (§28, §33) — the joint counterpart of Rapier2dAdapter.forEachBody, and for the same reason: Rapier's own joint handles are reused after a removal and cannot order anything.
The §31 CCD mode the solver is actually running for this body — read back from Rapier rather than remembered, so it reports what the solver does and not what the descriptor asked for.
World-space centre of mass into out (§25); out.z is 0 in this planar
build. Served by Rapier's RigidBody.worldCom().
The body's mass in kilograms, as the solver computed it (§23, §25).
Reads a body's world transform (§42's "physics" authority reads this).
outRotation receives the pure Z quaternion for Rapier's scalar angle.
Reads linear (m/s) and angular (rad/s) velocity into out parameters.
The body a collider belongs to (§24, §37).
The collider's monotonic id, ordered like RapierBodyAccess.getBodyId.
The monotonic id this joint is registered and iterated under (§33).
Throws NOT_IMPLEMENTED — Rapier 0.19.3 reports no joint reaction
(plan P6-2; verified 2026-08-01 for WP-6.2, in the typings and in the wasm
exports alike, see init.ts).
PhysicsWorld never calls this: it checks
Rapier2dAdapter.reportsJointReactions first and refuses to register
a breakable joint on an adapter that answers false. The method exists so
the SolverJointAccess seam is structurally complete — and so a
caller that reaches past the world gets an explanation rather than a
plausible-looking zero vector.
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.
§32 sleep state. The transitions are also reported as drainEvents events.
Tests a static shape for overlaps (§30).
§21's planar forms fall out of the shape union: an overlap with a
"circle" is §30's overlapSphere in a 2D world, and one with a
"rectangle" is overlapBox. Hits carry no distance, so sorted does
nothing here — OverlapHit has nothing to sort by, which QueryOptions
already says.
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.
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.
Clears the accumulated forces and torques (not impulses, which are applied immediately). Rapier keeps user forces across steps until reset.
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.
Selects §31's method on a live body —
SolverBodyTuningAccess.setBodyCcdMode.
The two Rapier switches are independent (enableCcd for swept CCD,
setSoftCcdPrediction for the speculative kind), so each mode sets one and
clears the other; a prediction distance of 0 is how Rapier expresses "no
soft CCD", which is what Rapier2dAdapter.getBodyCcdMode reads back.
That round trip is what makes "disabled" genuinely reversible rather than
leaving a body speculatively swept forever.
OptionalpredictionDistance: numberReplaces §23's two damping coefficients, both >= 0.
Replaces §23's gravity multiplier: 1 leaves gravity as-is, 0 is weightless.
Replaces §23's mass triple on a live body —
SolverBodyTuningAccess.setBodyMassProperties (PH-1 stage 2, 2026-08-07).
createBody's mass-mode decision, deliberatelycreateBody 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.
Teleports a body (§37 "teleports"). Use RapierBodyAccess.setNextKinematicTransform for a kinematic body that should push dynamic bodies on the way.
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.
Writes both velocities (§23).
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.
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.
Returns and clears the events collected during the preceding step (§37).
The array is handed over, not shared: the adapter starts a fresh buffer, so a caller may keep, sort, or splice the result. Contact vectors are freshly allocated per event and are never pooled, so they stay valid for as long as the caller holds them.
Replaces both body-local anchors, live (§28, PH-22f).
ImpulseJoint.setAnchor1 / setAnchor2 are on Rapier's base joint
class, so unlike setLimits this works for every §28 type this adapter
builds. The vectors are already body-local — the same space createJoint
received — and are not re-measured against any pose. A "2d" world still
rejects a non-zero z through toRapierVector2.
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.
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.
Reconfigures a revolute or prismatic joint's velocity motor (§28 "motors"), and the one place this adapter's honest limitation lives.
maxEffort is a gain here, not a clamp (decision, WP-6.2)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.
Sets a "kinematic-position" body's target pose for the next step (§22).
Rapier derives an artificial velocity from it so dynamic bodies react.
Optionalrotation: RotationInputSweeps 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).
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.
collisionstay, which Rapier does not haveRapier 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.
§37's pre-step hook. A documented no-op for this adapter.
Rapier applies every write the moment it is made — there is no staging
buffer for this call to flush. Scene-authored state reaches the solver
through RapierBodyAccess, which @fourjs/physics calls from inside
its own syncSceneToSolver phase; see the module header.
§37's post-step hook. A documented no-op for this adapter, for the same reason as Rapier2dAdapter.syncSceneToSolver: solved transforms are read per body through RapierBodyAccess.getBodyTransform, which is the only way an adapter that cannot see the scene graph could publish them.
A
PhysicsSolverAdapter(§37) backed by Rapier 2D.One instance owns exactly one Rapier
World.initializemay be called once;disposeis idempotent and terminal.