Builds a body from descriptor, which is validated against §22, §23, §31,
and §85 (dimension-independently — see the module header).
Vectors and the inertia tensor are copied, so the caller keeps ownership of whatever it passes in.
ReadonlyangularAngular velocity in radians per second (§23), as an axis-angle rate vector;
in a "2d" world only z is meaningful (§21).
ReadonlycenterCentre of mass in the body's local frame (§23). Owned by the component and never replaced — write into it. The origin by default.
§23 makes this always present, which is not the same as always authored: a body that never mentioned a centre of mass is asking the solver to derive one from its colliders (§23, §25), and a body that placed one is overriding that derivation. RigidBody.centerOfMassAuthored is that distinction and RigidBody.toDescriptor emits the field only when it holds.
Optional ReadonlyderivedThe last mass the solver derived for this body, or undefined before
one was read (§23, §25).
Read-only mirror of solver state, written by PhysicsWorld.addBody's mass
refresh. It exists so that a derived mass can be reported — RigidBody.mass falls back to it, so inverseMass is a number rather than
NaN for a density-derived body — without being authored: only an
authored mass is re-emitted by RigidBody.toDescriptor. See
RigidBody.massAuthored.
The node this component is attached to, or null. Written by the
ComponentRegistry alone (§6a); never assign it.
OptionalinertiaRotational inertia about the centre of mass (§23), or undefined to let
the solver derive it from the colliders and the mass.
§23 declares this non-optional, but §23 also derives mass (and with it
inertia) from collider density when it is not authored — and "derive it"
and "the identity tensor" are different instructions. The component keeps
them distinguishable: undefined is this field's authored flag, which
is why it needs no companion to
RigidBody.centerOfMassAuthored. In a "2d" world only the Z
diagonal entry is used (§23).
For the same reason a future syncSolverToScene must not publish a
solver-derived tensor back into this field (WP-5.2-fix1): doing so would
turn a derivation into an authored mass distribution, which is exactly the
defect RigidBody.centerOfMassAuthored exists to prevent. A derived
tensor belongs in a separate read-only field.
Optional ReadonlyinitialThe initial world position a descriptor carried, or undefined (§37).
The live pose is the node's Transform (§7, §42); this is only what the
descriptor asked for, kept so a descriptor round-trips through the
component unchanged. A Vector2 has already been widened to z = 0.
Optional ReadonlyinitialThe initial world rotation a descriptor carried, or undefined. A scalar
angle has already been resolved to its quaternion about +Z (§7a).
ReadonlylinearLinear velocity in metres per second (§23). Authored state going in, solved state coming back — see the module header.
ReadonlyliveHow many of those worlds can carry a §37 property change into their solver
— the subset of RigidBody.registeredWorldCount whose adapter
implements SolverBodyTuningAccess (PH-1 stage 2, 2026-08-07).
Read-only state, written by the same addBody / removeBody pair. It is
what decides whether a setter in the module header's table warns or
merely marks the property dirty: a write is on the record as
unreachable exactly when some world holding this body cannot deliver it, so
the two counts differ. Registering a body with a tuning adapter and a
plain one leaves the warning on, which is the truth — one of the two
simulations will not see the change.
ReadonlypendingWhich §37 property writes are waiting for the next fixed step, as a bit set
of the RIGID_BODY_*_DIRTY flags (PH-1 stage 2, 2026-08-07).
Read-only state like RigidBody.sleeping: the setters in the module
header's table set bits, and PhysicsWorld clears them at the top of
step after pushing each one through SolverBodyTuningAccess. 0 — the
value for a body nobody wrote to — is what the world tests to decide that
this body needs no solver call at all.
Bits are set whether or not any world can carry them, because the cost is
one |= and an unregistered body that is later added rebuilds itself from
toDescriptor() anyway: a stale bit is at worst one redundant solver call
with the value the solver already has, never a wrong one.
ReadonlyregisteredHow many PhysicsWorlds currently hold this component (§37), normally 0
or 1.
Read-only state like RigidBody.sleeping, written by
PhysicsWorld.addBody and removeBody. A count rather than a flag because
nothing forbids registering one component with two worlds, and because the
body stops being registered only when the last of them lets go. It is
what turns this class's "that write reaches no solver" warnings on — see
the module header's table.
ReadonlysleepingWhether the solver has put this body to sleep (§23, §32).
Read-only state, exactly as §23 requires: RigidBody.wake and
RigidBody.sleep are the commands, and the flag changes only when
the solver reports the transition after a step — at which point the body
also emits "sleep" or "wake" (WP-5.3).
The §8 space this body is solved in (PH-12, 2026-08-09).
DEFAULT_SPACE_MODE ("world") unless the descriptor said otherwise, so
every body written before PH-12 means exactly what it meant.
§8 gives physics two legal frames — "physics normally operates in world or
local-plane space" — and forbids a third case outright: "screen-space UI
should not automatically participate in physical simulation unless
explicitly mapped to a simulation plane". PhysicsWorld.addBody
therefore accepts "world" and "local-plane" and refuses the rest:
"screen", "viewport", "camera", "billboard" — refused because
§8 says so. The sentence's escape hatch is a mapping the author
performs: simulate a body on a node in the simulated frame and drive the
presentation node from it."local-plane" — legal under §8 and accepted by PhysicsWorld.addBody:
the body's 2D pose lives in PhysicsWorld.localPlane's frame and
feed/publish maps through that basis (§21). Presentation frames stay
refused.Plain and mutable, with no change hook and no validation: PhysicsWorld is
the only reader and it reads at registration, so writing it on an
already-registered body does nothing until the body is registered again.
@fourjs/core's isSimulationSpaceMode answers §8's own question (world
and local-plane pass; presentation frames fail).
Static ReadonlytypeComponent key (plan D2) and §79 serialization name.
Angular damping coefficient, >= 0 (§23). See RigidBody.linearDamping.
How much of a "blended" node's final pose comes from the animated target
pose — the PoseTarget on the node (§19, P7-1) — relative to
RigidBody.physicsWeight. See that property for the whole contract.
Defaults to DEFAULT_ANIMATION_WEIGHT: a body nobody has blended is
fully physical.
Which §31 method sweeps this body, "disabled" when it is not swept.
This is the authoritative field; RigidBody.continuousCollisionDetection is §23's on/off view of it.
Live on a world whose adapter implements SolverBodyTuningAccess: an
assignment takes effect at the next fixed step, carrying
RigidBody.ccdPredictionDistance with it for "speculative". On an
adapter without that seam it warns once, like the rest of the module
header's table (PH-1 stage 2, 2026-08-07 — before which this setter did not
even warn).
§31's "speculative" prediction distance, in world units, when authored —
undefined means the adapter's documented default (1 m on both Rapier
adapters). Carried by the component so world.addBody's rebuilt
descriptor delivers it to the solver (2026-08-05 review fix: the
constructor validated and accepted the field, then toDescriptor()
silently dropped it, so the authored distance never reached the adapter
on the component path).
Read-only, like the shape of a collider: the distance is applied at
createBody and Rapier offers no post-creation re-tune through the seam.
Whether RigidBody.centerOfMass is an authored mass distribution rather than the always-present default §23 gives every body (WP-5.2-fix1).
§23 derives a body's mass from collider density times volume whenever
mass is not authored, and a solver derives the centre of mass and the
rotational inertia from the same geometry in the same breath — mass,
centre, and inertia are one triple. Naming any part of that triple means
"do not derive it", so a component that reported its default origin as an
authored centre would make the density-derived mass unreachable: an adapter
asked for a distribution with no mass to distribute has nothing to do but
refuse.
A centre of mass counts as authored when either holds:
centerOfMass (whatever its value,
including the origin), or RigidBody.markCenterOfMassAuthored
has been called — the sticky half;body.centerOfMass.set(…) after construction
without asking the caller to announce it.The union is what makes the omission safe. Rule 1 keeps an explicitly
authored origin — the one value rule 2 cannot see — and rule 2 keeps every
post-construction edit. The only state that is omitted is a centre that is
at the origin and was never mentioned, which is precisely the state that
carries no instruction. -0 reads as the origin (-0 !== 0 is false),
and a non-finite component reads as authored, so §85 validation rejects it
rather than the descriptor quietly losing it.
The one case rule 2 alone would get wrong is "pin the centre to the origin
even though the colliders sit off it", which is a real instruction that
looks identical to the default. Authoring centerOfMass in the descriptor
states it; RigidBody.markCenterOfMassAuthored states it after the
fact.
What this body has been asked to do since the world last drained it (§26, §32). A live read-only view — see RigidBodyCommands.
§23's switch, expressed over RigidBody.ccdMode so the two can never
disagree: reading it asks whether a method is selected, and
body.continuousCollisionDetection = true (§31's own example) selects
DEFAULT_ENABLED_CCD_MODE unless a method is already set. Assigning
false clears the method.
Whether RigidBody.dispose has run. Disposal is terminal (§83): a
disposed component refuses its §26/§32 commands and world registration with
INVALID_APPLICATION_STATE (§89) rather than silently working.
Multiplier on world gravity for this body (§23). 1 leaves gravity as-is,
0 makes the body weightless, a negative value inverts it.
Applied at createBody; see RigidBody.linearDamping for what an
assignment to a registered body does and does not do.
Derived reciprocal mass (§23), read-only:
0 for "static" and both kinematic types — they do not respond to
forces (§22), which is what an infinite effective mass means;1 / mass for a dynamic body with an authored mass;NaN for a dynamic body whose mass has not been authored and has not yet
been derived from collider density (§23). NaN and not 0: an
unknown mass is not an infinite one, and a value that silently reads as
"immovable" would turn a missing collider into a body that quietly
refuses to move.Linear damping coefficient, >= 0 (§23).
Applied to the solver body at createBody and, on a world whose adapter
implements SolverBodyTuningAccess, at the next fixed step after every
assignment (PH-1 stage 2). Assigning to a registered dynamic body held by
an adapter without that seam warns once instead — see the module header.
Mass in kilograms (§23): the authored mass when there is one, otherwise the
last mass the solver derived from collider density times volume (§23, §24,
§25), or undefined when neither exists.
Authoritative when authored and re-validated on assignment: a dynamic
body's mass must be positive, and non-simulated mass is expressed through
the body type, never mass = 0 (§23, §85). Assigning here authors the
mass — see RigidBody.massAuthored — and queues §23's whole triple
(mass, centre of mass, rotational inertia) for the next fixed step, which a
world whose adapter implements SolverBodyTuningAccess pushes into the
solver (PH-1 stage 2). See the module header's table for what happens when
it does not, and for the one assignment that still reaches nothing:
mass = undefined.
Whether RigidBody.mass is an authored mass rather than one the solver derived (§23, §25).
PhysicsWorld.addBody reads the solver's mass back onto the component so
that a density-derived body stops reporting inverseMass === NaN. Before
2026-08-06 that read went through the mass setter, which made the
derived number indistinguishable from an authored one — so
RigidBody.toDescriptor re-emitted it as mass, and re-registering
the body (or serializing and reloading it) turned "derive my mass from my
colliders" into "my mass is 3.0141594". Scaling a collider afterwards then
changed nothing, silently.
The two masses are therefore kept apart: #mass for what the author said,
RigidBody.derivedMass for what the solver answered, and
toDescriptor emits the field only when this holds. It is the same
rule RigidBody.centerOfMassAuthored states for the centre, with a
simpler test: no default value competes with undefined here, so an
authored mass is exactly a defined #mass — which is what this getter
derives rather than mirroring in a second field.
How much of a "blended" node's final pose comes from the solver (§19),
relative to RigidBody.animationWeight.
§19's sketch verbatim:
body.transformAuthority = "blended";
body.physicsWeight = 0.35;
body.animationWeight = 0.65;
The two weights are independent settables, normalized at use — they are
not required to sum to 1 and setting one never rewrites the other, so
0.35/0.65 and 35/65 describe the same blend and a fade can drive
either slider alone. RigidBody.normalizedWeights is where the ratio
becomes a pair of fractions.
Defaults to DEFAULT_PHYSICS_WEIGHT; must be finite and >= 0
(§85), checked on assignment. Inert under every authority except
"blended" (§42).
Which §22 simulation model this body follows (§23). Settable; the §23 mass
rule is re-checked against the new type, so promoting a zero-mass body to
"dynamic" fails here rather than in the solver.
This setter changes the component. Once the body is registered with a
PhysicsWorld, the solver is running a body of the type it was created
with, and writing here alone would leave the two disagreeing about what is
being simulated — the fixed-step pipeline would keep feeding the old model.
world.setBodyControlMode(node, type, options?) is the call that changes
both: it re-types the solver body in place (the id, the checksum order,
the colliders, and the pose all survive), moves this property with it, and
can seed the new velocities from an animated PoseTarget — §19's
animated → kinematic → dynamic transitions. WP-5.3's rule that a re-typed
body had to be removed and added again is superseded by it.
Assigning here while the body is registered is not silently accepted: the
setter warns once per body (2026-08-06) naming setBodyControlMode, which
is the call that moves the solver too. The assignment still happens — the
component is the caller's — but the divergence is on the record instead of
showing up later as a body that refuses to fall.
Accumulates an angular impulse in newton-metre-seconds (§26). A number is
the scalar about +Z (plan P5-3).
Accumulates a force in newtons, applied at the centre of mass (§26).
Acts for one fixed step; call it every step to push continuously. Applying a force does not implicitly wake a sleeping body — call RigidBody.wake as well, so that what wakes a body is always something the caller asked for (decision, WP-5.2).
Per-step path: the components are widened but not range-checked, matching §85's allowance that per-step validation may be skipped. A non-finite force is caught by the solver, not here.
Accumulates a force in newtons applied at worldPoint, a world-space
position (§26).
Kept as a point load rather than being decomposed into a force and a torque
at the call site: the lever arm is measured from the world-space centre
of mass, which the component does not know — §23's centerOfMass is in the
body's local frame and the pose belongs to the node and the solver. The
adapter, which has both, does the decomposition.
Accumulates a linear impulse in newton-seconds, applied at the centre of mass (§26).
One-shot: the world applies it at the next fixed step and clears the buffer, so a single call changes momentum exactly once.
Accumulates an impulse in newton-seconds applied at worldPoint (§26).
See RigidBody.applyForceAtPoint for why the point is kept.
Accumulates a torque in newton-metres (§26). A number is the scalar about
+Z (plan P5-3). Acts for one fixed step, like RigidBody.applyForce.
Drops every listener and every queued command (§6a, §83).
Detaching a component does not dispose it (§6a), so this runs only from
an explicit dispose() or ComponentRegistry.disposeAll. Removing the
body from its world is the world's business, not the component's.
Delivers event to the listeners registered for type in registration
order. Re-entrant emission of type is deferred (rule 4).
Number of live registrations for type (diagnostics, §83).
Number of live listener registrations across every event type (§83).
Used by teardown diagnostics — a detached node that still holds listeners pins its subtree until those listeners are removed.
Declares RigidBody.centerOfMass an authored mass distribution from now on, so RigidBody.toDescriptor emits it even at the origin.
The escape hatch for the one case the live half of
RigidBody.centerOfMassAuthored cannot infer: a centre deliberately
pinned to the body origin while the colliders sit elsewhere. One-way by
design — nothing un-authors a distribution, because "forget what I asked
for" is the silent data loss this whole mechanism exists to avoid. To go
back to a derived centre, build a body whose descriptor omits
centerOfMass.
Remember that an authored distribution needs an authored RigidBody.mass as well: solvers set mass, centre, and inertia as one triple (§23), and an adapter is entitled to reject half of it.
Queues §23's mass triple for the next fixed step after RigidBody.centerOfMass or RigidBody.inertiaTensor was edited in place (PH-1 stage 2, 2026-08-07).
centerOfMass is a readonly vector mutated with set/copy and
inertiaTensor is a plain field, so neither edit passes through a setter
and nothing can observe it. The alternatives were to compare both against a
shadow copy on every body on every step — real per-step cost for a value
that changes approximately never — or to say so and give the caller one
call. This is that call, and it is the same bargain
RigidBody.markCenterOfMassAuthored strikes for the same field.
The whole triple, because a solver sets the three together (§23) — so it also picks up an authored RigidBody.mass that has not changed. Calling it on a body whose mass was never authored queues nothing and warns once: §23 derives centre and inertia from geometry precisely when no mass was named, and an adapter asked for a distribution with no mass to distribute has nothing to do but refuse (the rule RigidBody.centerOfMassAuthored states at length). Author a mass first.
Free to call on an unregistered body, and free to call twice: the bit is already set.
The two §19 weights as fractions that sum to 1, written into out and
returned.
const split = body.normalizedWeights(scratch); // per fixed step
pose.position
.copy(solverPosition)
.scale(split.physics)
.add(targetScaled);
Pass an out on a per-step path and the call allocates nothing (§7b, plan
D7); omit it and one record is allocated per call.
A body with physicsWeight === 0 and animationWeight === 0 has asked for
a pose made of nothing, which has no answer. Rather than produce NaN — or
a zero pose, which would teleport the node to the origin — this falls
back to fully physical ({ physics: 1, animation: 0 }, the constructed
default) and warns once per body via console.warn, the same development
semantics warnAuthorityConflict uses in @fourjs/scene: the first
occurrence names the mistake and every repeat is suppressed, because a
misconfigured body would otherwise print once per fixed step forever. The
suppression is sticky for the life of the body — fixing the weights and
zeroing them again warns no further.
It is a warning and not a validation error because the state is reachable
one legal assignment at a time (physicsWeight = 0 while animationWeight
is still 0 is the first half of every "fade to fully animated"), so
rejecting it at the setter would reject the sequence rather than the
mistake.
Finiteness is guaranteed by the setters, so the only other arithmetic edge
is two weights whose sum overflows to infinity; that case is rescaled by
the larger weight so the ratio survives instead of collapsing to 0/0.
Optionalout: BlendWeightsRemoves the earliest still-registered registration of listener for
type. Removing a listener that is not registered is a no-op.
Registers listener for type. Returns a function that removes it.
The same listener may be registered more than once; each registration is independent and is delivered once per dispatch.
Registers listener for a single delivery. Returns an unsubscriber.
Removes every listener, or every listener of one type. Used by teardown paths that must not leave detached nodes retaining listeners (§83).
Optionaltype: KRequests that the solver put this body to sleep (§23, §32). See RigidBody.wake.
The descriptor an adapter would be handed for this body (§37 createBody).
A fresh record whose vectors and tensor are references to this
component's live state, not copies: the adapter reads them during
createBody and must not retain them. mass, centerOfMass,
inertiaTensor, position, and rotation are present only when authored,
so a descriptor that omitted them round-trips unchanged.
centerOfMass is the subtle one: §23 keeps it always present on the
component, so emitting it unconditionally would tell every adapter that
every body carries an authored mass distribution and make §23's
density-derived mass unreachable (WP-5.2-fix1). It is emitted exactly when
RigidBody.centerOfMassAuthored holds, which that property documents.
mass follows the same rule against the same defect (2026-08-06): only an
authored mass is emitted, so a body whose mass a solver derived at
registration still asks the next solver to derive it rather than presenting
the first solver's answer as an instruction. See
RigidBody.massAuthored.
§19's RigidBody.physicsWeight and RigidBody.animationWeight are deliberately absent: a solver simulates a body, it does not blend one, and the blend happens entirely above the adapter (§37) after the solve (§19 step 5). Putting a weight in the descriptor would tell every adapter it had a say in something it never sees.
Re-validates this body against a world of dimension (§21, §85).
Called when the body is registered with a PhysicsWorld (WP-5.3), which is
the first moment the dimension is known: a "2d" world rejects an
out-of-plane centre of mass, initial position, or angular velocity here
rather than projecting it away. Not a per-step path — it builds a
descriptor.
Requests that the solver wake this body at the next fixed step (§23, §32).
Queued, not immediate: RigidBody.sleeping changes when the solver
reports the transition. Calling wake() then sleep() in the same step
leaves the later command standing.
A simulated body attached to a node (§23), created from a RigidBodyDescriptor.
One per node (§6a). See the module header for the pose, command, event, and dimension contracts.