# Physics and character movement

Sindri AI knowledge pack, file `07-physics-and-movement.md`. 2D and 3D physics: bodies, colliders, sensors, layers and masks, joints, materials, collision events, and character controllers.

Start with `00-start-here.md` if you have not read it. Every fact here is copied from the Sindri repository; nothing in this file is a guess.

## Contents of this file

- **Physics architecture** (from `docs/physics.md`): Goals; Crate boundary; Public model; Transform ownership; Reusable collision materials; Scene-authored distance constraints; Reversible authored joint suspension and distance tuning; Scene-authored hinges and motors; Scene-authored sliders and springs; Runtime ownership and stepping; Events and queries; Editor; Decay; Gather proof; 3D engine foundation and remaining integration; Live 3D collision coefficients; Replaceable static collider groups; Authored voxel colliders; Resident voxel snapshot adapter; Voxel collision geometry foundation; Feature-track slices; Non-goals for the first 2D slice; Geometric movement foundation; 3D character query foundation
- **Character movement** (from `docs/character-movement.md`): Ownership; Implemented geometric primitive; Implemented ground classification; Implemented grounded movement and optional snapping; Implemented slope movement limits; Implemented steps and clearance; Implemented moving-platform carry; Implemented one-way controller policy; Implemented scene ownership; Typed Decay requests and motion snapshots; Browser goal proof; Remaining limitations; Foundation evidence; 3D character movement foundation; Classified 3D ground queries; Read-only 3D platform carry; 3D scene ownership; Typed 3D Decay requests and results; 3D controller feature proof; 3D editor footing guides

---

Source: `docs/physics.md`

## Physics architecture

Status: accepted implementation direction for the physics feature track.

Sindri exposes Sindri physics. Rapier is a backend implementation detail and is
never part of the scene format, editor contract, Decay language surface, or game
API.

### Goals

- Support both 2D and 3D without designing a 2D API that later has to be broken.
- Keep Rapier out of `sindri-core`, serialized scene data, the editor, Decay, and
  Gather.
- Run physics on the engine's fixed simulation step, never on render delta.
- Keep runtime `EntityId` ownership authoritative; physics handles are private
  implementation details.
- Give 2D and 3D parallel concepts without pretending dimension-specific values
  are interchangeable.
- Keep native and `wasm32-unknown-unknown` on the same gameplay semantics.
- Evolve Gather with the 2D implementation so the subsystem is proven by a real
  game rather than only fixtures.

### Crate boundary

Create one `sindri-physics` crate at a real dependency boundary:

```text
sindri-core                 sindri-grid
     ^                           ^
     |                           |
sindri-physics -----------------+
     |
     +-- rapier2d
     +-- rapier3d

sindri-scene -> sindri-core + sindri-grid + sindri-render + sindri-physics
sindri-decay -> sindri-core + sindri-grid + sindri-platform + sindri-physics
editor       -> ... + sindri-physics
Gather       -> ... + sindri-physics
```

`sindri-physics` owns the public physics model and the Rapier adapters. A second
`*-rapier` crate is not justified while there is one backend. Rapier types may
appear inside private implementation modules only. If another backend ever
becomes real, the adapter can be split without changing the public contract.

The crate depends on `sindri-core` because bodies are synchronized to runtime
entities, and on `sindri-grid` only if a concrete physics/navigation integration
requires it. Do not add the grid dependency speculatively. It has no renderer,
window, browser, editor, or Decay-language dependency.

Both `rapier2d` and `rapier3d` must satisfy the repository MSRV, licence policy,
`cargo deny`, native builds, and `wasm32-unknown-unknown` before either is merged.

The adapters use Rapier 0.36. The 2D overlap adapter reads the backend
intersection flag while keeping sub-shape IDs private. The event collector
also owns a soft-body tear channel; Sindri currently authors only rigid bodies
and continues to publish collision and sensor events only.

### Public model

The concepts are intentionally parallel rather than generic over dimensionality.
Dimension-neutral configuration can be shared internally, but public APIs use
ordinary explicit types.

#### Bodies

`RigidBodyKind` is shared:

- `Static`
- `Dynamic`
- `KinematicPosition`
- `KinematicVelocity`

The scene components are distinct:

- `sindri.physics2d.rigid_body`
- `sindri.physics3d.rigid_body`

They carry Sindri-owned configuration such as body kind, gravity scale, damping,
and whether rotation is locked. They never serialize Rapier handles, activation
state, solver state, or backend-specific flags.

#### Continuous collision

`RigidBody2d.continuous_collision` defaults to false, including old payloads
that omit it. It opts a dynamic body into swept solid collision. The live
`continuous_collision` getter and `set_continuous_collision` setter preserve
velocity, attached joints and body identity. The setter rejects non-dynamic or
missing bodies before mutation.

The backend automatically sweeps dynamic bodies against fixed geometry; the
opt-in regression therefore uses a thin velocity-kinematic wall. Sensors remain
discrete, and bullet-versus-bullet collision is not guaranteed. The generic command-backed inspector authors the flag on the body. CCD-only
payload edits toggle the live backend without rebuilding its body, contacts or
joints; ordinary collider/kind edits retain their existing rebuild lifecycle.
`Physics.continuous_collision(entity)` and
`Physics.set_continuous_collision(entity, enabled)` expose the same control in
Decay. The setter requires an authored dynamic body, updates both the live
backend and runtime payload, and works before a spawned body is synchronized.
The getter uses live state when present and authored state during that window.
Missing authored bodies and hosts without physics report errors. The platformer
hero opts in, with its existing run-to-flag regression exercising the setting.

#### One-way platforms

`OneWay2d` is a validated support-side policy: local `normal` defaults to
`[0, 1]`, `angle` defaults to pi/4 radians and is bounded to 0..=pi/2. Normals
must have finite, nonzero length. `sindri.physics2d.one_way` applies it to every
solid piece on the entity, including generated tilemap boxes. The normal follows
each piece's rotation and the body's rotation. Sensors retain ordinary overlap
and enter/exit behavior. Omitting the component means ordinary solid collision.

The backend filters contact pairs, including CCD sweeps, to reject relative
ascent above 0.1 units/s and bodies not yet above the support plane. Supporting contacts must
face the configured cone. Projection uses each convex piece's actual geometry;
a 0.05 world-unit penetration allowance stabilizes resting contacts. A body
starting deeply inside must clear the supporting face before it can land.
Fast descent still requires CCD to avoid discrete tunneling. Policies are live
controls: editing or undoing only the policy preserves velocity and joints.

`PhysicsWorld2d::drop_through(entity, seconds)` and typed
`Physics.drop_through(entity, seconds)` ignore only one-way solid contacts for
a dynamic body. Durations are finite and nonnegative: zero cancels, a new
request replaces the remaining time. Timers advance after each fixed step,
so any positive remainder covers the whole next step. Pause does not expire
them. Removal and structural body/collider rebuilds clear policy/timer state. The host validates the authored body;
requests before synchronization start when the spawned body materializes.
Abandoned requests are discarded at the end of synchronization.

Geometric rays/overlaps/casts still see the pieces from either side and during
drop-through. They do not themselves grant grounding. The platformer uses a
downward support normal, clearance and descending velocity in Decay, suppresses
jump permission while dropping, and keeps sensors for coins/the flag. Its
visible plank tilemap exercises ascent, landing, Down/S/d-pad drop to an ordinary
floor, and another landing after expiration. Engine regressions also exercise
rotated support normals, kinematic platforms, fast CCD ascent and cancellation.

#### Forces and rotation

`PhysicsWorld2d` adds world-space force and torque, angular velocity in radians
per second, angular impulses and impulses at world points. Positive torque and
angular velocity turn counterclockwise. Forces and torques accumulate for the
next valid fixed step and are cleared immediately after it, even when a host
runs several fixed steps per frame. Invalid timesteps do not consume them.
Pause keeps them pending. Impulses change velocity immediately, without a dt
factor; off-centre impulses also turn the body according to its mass/inertia.

Forces and impulses require dynamic bodies. Velocity setters also support
velocity-kinematic bodies. Every request validates finite values and body kind
before mutation; rotation locks prevent angular motion while retaining linear
impulses. Collider mass properties are available immediately after insertion,
so impulses work before the first step. Existing velocity/impulse APIs remain.

Typed Decay adds `Physics.apply_force(entity, Vec2)`, `apply_torque(entity, number)`,
`angular_velocity(entity)`, `set_angular_velocity(entity, number)`,
`apply_angular_impulse(entity, number)` and
`apply_impulse_at_point(entity, Vec2, Vec2)`. The point is in world space.
An authored body can receive these requests between spawning and synchronization:
requests replay in call order when the body materializes, after its collider
mass is known. The angular getter in that window returns the latest queued
setter or the authored initial value; queued impulses affect it at materialization.
Removal, abandoned synchronization and structural rebuilds discard pending work.
A host with its own physics driver can control live bodies without scene body
components. An authored body is required only to validate requests before
materialization. Missing bodies and hosts without physics fail explicitly.

The platformer's wooden crate is pushed by wind and tossed by K or the west
controller button. Decay reapplies force each update, applies torque while it is
airborne, bounds its angular velocity and gives kicks an off-centre and angular
impulse. The native input regression checks its launch and scene rotation
writeback. Browser interaction and visual inspector review remain in final
integration. Native, WASM and browser CI passed on the forces/rotation head.

#### Contact snapshots

`PhysicsWorld2d::contacts(entity)` returns copied `Contact2d` values from the
last valid fixed step. Each value names the other entity and a world-space
surface-anchor midpoint, with a unit normal pointing towards the queried body
(the direction it is pushed). `normal_impulse` is nonnegative; signed
`tangent_impulse` acts along `[-normal.y, normal.x]`. `force` is their total
world impulse divided by that step's seconds. The adapter reads the manifolds
actually solved rather than cached geometric-point impulses. Rapier currently
disables clustering in 2D; the adapter uses its solver-manifold accessor, which
also selects clusters if the backend enables them in the future. Predictive
contacts within the solver's contact reach may be present.

Contacts are ordered by other entity, point, normal, normal impulse and tangent
impulse using total float ordering. Multiple points/pieces can name the same
entity; they are not deduplicated. Sensors are absent. Sleeping support remains
present with zero new impulses/force, so resting geometry can grant grounding
without replaying an old impact. Snapshots are empty before the first step;
missing bodies fail. Invalid steps retain the previous snapshot. Teleports,
removal and structural rebuilds invalidate affected contacts immediately.
Setting a kinematic target takes effect at the next step. Pausing retains the
last snapshot; copies held by a caller survive later changes.

Typed `Physics.contacts(entity) -> List<Contact2d>` exposes the same values in
Decay and omits inactive/despawned other entities immediately. Inactive queried
entities and authored colliders awaiting spawn synchronization return an empty
list. Active entities with no collider/body and hosts without physics fail.
Hosts driving their own physics need no authored component for live bodies.
Script updates observe the preceding fixed solve, as they do collision events.
Scene transforms synchronize at the next step, so a script teleport is reflected
in snapshots after synchronization, not midway through a script pass.

This capability is added for the platformer genre showcase: the wind crate
flashes amber on a hard landing based on normal impulse. The hero now uses
controller support for jump permission and a geometric ray for HUD clearance.
Gameplay policy and thresholds remain in Decay. Browser interaction and editor Play
inspection remain in final integration.

#### Colliders

The scene components are distinct:

- `sindri.physics2d.collider`
- `sindri.physics3d.collider`

The first 2D shapes are box, circle, and capsule. The first 3D shapes are box,
sphere, and capsule. Shape enums are Sindri types. Dimensions are expressed in
scene/world units and validated as finite and positive.

A collider carries:

- shape
- local offset
- sensor/trigger flag
- collision membership mask
- collision filter mask
- friction
- restitution

Collision masks are Sindri bit masks from the first implementation. They are not
Rapier `Group` values in public APIs or serialized JSON.

A collider may exist without a rigid-body component; it is treated as a static
collider owned by the entity.

A 2D collider may be authored in **several pieces**, because one shape is often a
poor description of a thing: a character is a capsule with a circle at each side,
a ship a box and two pods. The pieces belong to the one entity and move as one
object — a compound is one collider made of parts, not several colliders — and
they need no child entities to say so, because a piece already carries its own
offset and rotation.

```jsonc
"sindri.physics2d.collider": {
  "pieces": [
    { "shape": { "shape": "capsule", "half_height": 0.4, "radius": 0.22 },
      "offset": [0.0, 0.05], "rotation": 0.0, /* … */ },
    { "shape": { "shape": "circle", "radius": 0.18 },
      "offset": [-0.38, -0.1], "rotation": 0.0, /* … */ }
  ]
}
```

The single form — a collider written directly, with no `pieces` — still means
exactly what it always did, and is kept rather than migrated: a scene is a file
someone wrote, and a format that can only be read after a rewrite is one that
breaks their project on upgrade.

Two consequences worth stating, because both are derived rather than authored.
Mass properties come from **every** piece and sum, so a body's centre of mass is
decided by the compound and not by its first piece; `PhysicsWorld2d::mass`
exposes the total so that claim can be checked. And validation is **per piece and
names the index** — a compound is a list, so "restitution must be between 0 and
1" without one is a needle in it. Nothing is inserted unless every piece passes,
because a half-built body is worse than none.

The standalone 3D engine world accepts a slice of collider pieces, validates all
of them before insertion and sums their mass. The 3D scene driver now consumes
authored compounds; shared game sessions and native editor Play step both
drivers. This general piece boundary is a prerequisite for voxel collision
geometry; it does not claim voxel/game proof.

An entity may not participate in both the 2D and 3D physics worlds at once.
Validation reports that as an authored configuration error instead of choosing a
world silently.

#### Tilemap collision

`sindri.physics2d.tilemap_collider` makes the tilemap on the same entity solid,
so a painted level collides where it was painted and a wall is repainted rather
than re-placed:

```jsonc
"sindri.physics2d.tilemap_collider": {
  "passable": ["grass-tuft", "vine"],   // palette sprites that do not collide
  "layers": { "memberships": 1, "filter": 4294967295 },
  "friction": 0.5,
  "restitution": 0.0
}
```

Every painted tile is solid unless its sprite is passable. The solid cells are
merged, greedily in reading order, into rectangles that cover each once: a floor
becomes one box, not one per tile, because a character sliding along separate
boxes catches on the seams between them. The rectangles are ordinary collider
pieces, scaled with the entity, and join any pieces an authored collider on the
same entity has, so a tilemap on a kinematic body is a moving platform. Only an
orthogonal map can be solid; an isometric map is a floor seen at an angle and is
refused by name.

#### Gravity

`sindri.physics2d.world` is the scene's say over the world as a whole:

```jsonc
"sindri.physics2d.world": { "gravity": [0.0, -9.81] }
```

A scene carries it rather than the host deciding, because a platformer falls and
a game seen from above does not, and the editor's Play must run each the way its
build will. One per scene, like the Environment. A scene without one keeps the
gravity its host constructed `ScenePhysics2d` with.

The settings that count are the first *active* ones, for gravity and for the
layer names alike. A script reads and turns gravity with `Physics.gravity()`
and `Physics.set_gravity(v)`, which change this component rather than the
solver behind it. A change of gravity wakes every dynamic body in both
dimensions, because a body asleep at rest would otherwise ignore it while
everything in motion fell the new way.

### Transform ownership

`Transform3D` remains the authored and visible transform for both dimensions.
Physics does not introduce a second scene transform.

For 2D, physics reads/writes X, Y, and rotation about Z. Existing Z and 3D scale
are preserved. For 3D it reads/writes XYZ and quaternion-equivalent scene
rotation through the existing transform representation.

Synchronization rules are explicit:

- static body: world transform seeds physics; checked authored changes update the
  backend body/collider before the next step;
- dynamic body: physics owns position/rotation while simulation runs and writes
  the resulting transform back after each fixed step;
- any body whose transform was moved by something other than physics since the
  last write-back (a script respawning the player, clamping it to the screen)
  is moved to the new transform before the next step: a teleport, keeping its
  velocity, as setting a Rigidbody2D's position does in Unity. A
  position-kinematic body takes the move as its next target instead, so a
  platform moved this way carries what stands on it;
- kinematic-position body: gameplay/editor supplies the target transform before
  the fixed step; physics resolves contacts from that motion;
- kinematic-velocity body: gameplay supplies velocity; physics owns the resulting
  transform for the step.

Scale is not simulated. Collider dimensions are authored explicitly. Changing an
entity's visual scale must not secretly mutate collision geometry. The one
exception is said out loud: a tilemap collider's rectangles are derived from
where its tiles are drawn, so they follow the map's scale.

Friction combines as the smaller of the two colliders' values, so a collider
authored frictionless is frictionless against everything: a platformer's hero
pressed into a wall slides down it rather than clinging with half the wall's
friction. Two equal frictions combine to that value, as an average would.

Parented 2D bodies are supported. Synchronization reads the composed world pose
and converts the simulated answer back into the parent's local space. Moving a
parent carries its bodies through the authored-pose change rule. This is exercised
by scene hierarchy regressions and Low Tide's moving deck; it does not create a
physical joint between parent and child.

### Reusable collision materials

Physics coefficients can be reused as project `.profile` assets with type
`physics_material`. They use the existing asynchronous profile pipeline, not a
new asset kind or backend resource. For example:

```json
{
  "format_version": 1,
  "name": "Wood",
  "type": "physics_material",
  "values": { "friction": 0.3, "restitution": 0.1 }
}
```

Both coefficients are required numbers. Friction must be finite and non-negative;
restitution must be finite in `[0, 1]`. Unknown coefficient keys are rejected.
Other profile types remain unrestricted game data. `physics_material_profile`
at the scene boundary uses the engine-owned `PhysicsMaterial::validate` rules;
asset decoders and core profile documents stay independent of physics.

An entity's `sindri.physics2d.material` names `profile` and applies it to every
ordinary and generated tilemap collider piece, sensors included. Its 3D counterpart,
`sindri.physics3d.material`, uses exactly the same fields and asset format for
primitive compounds, characters, static model triangles, convex model hulls and
resident voxel sections. Without a
material component, existing per-piece literals are unchanged. An empty profile
keeps those literals. A non-empty reference must resolve to a loaded physics
profile; a missing or differently typed profile fails the fixed step. The profile
replaces both piece coefficients, then explicit `override_friction` and
`override_restitution` flags select the component's local `friction` and
`restitution` values. Disabled override values are ignored. A missing reference
is still an error even when both overrides are enabled. Friction combines by
minimum; restitution retains the existing average rule.

Hosts build `PhysicsMaterialSources` from their loaded profile documents before
stepping. Reloaded coefficients update registered colliders in place, preserving
velocity, forces, mass and joints, and waking affected contacts. Changes to shapes
or body kinds still use the normal rebuild path. Invalid source sets cannot
partly replace a valid source set; backend piece updates validate all values
before changing any collider.

The editor offers “New physics material here”, a structured profile editor,
profile selection and explicit override controls through checked component
commands and undo/redo. Its asynchronous loader watches valid and invalid files;
a failed edit reports the asset and retains the previous valid profile. Invalid
coefficient edits cannot overwrite the file through the profile editor. Play
waits for initial delivery before physics runs. Export walks scene and prefab
material references, including inactive entities, and validates physics profiles
before producing output; native and browser hosts use the same scene resolver.

Added for the platformer: its wind crate and one-way planks share
`materials/wood.profile`, with an explicit zero-bounce override on the planks.
Native and browser delivery expand placed prefabs during scene entry from the
original library, retaining root aliases for authored references. Native regressions verify sharing, precedence and a changed crate rebound when
the shared restitution changes. Editor loader/reload tests exercise delivery and
last-valid retention. Visual inspector review and full browser gameplay remain
part of final physics integration.

### Scene-authored distance constraints

`sindri.physics2d.distance_joint` lives on a separate joint entity:

```json
{ "first": "lantern-anchor", "second": "wind-lantern", "max_distance": 2.0 }
```

The endpoint strings are stable scene entity IDs, not runtime handles. Resolution
uses an exact qualified scene ID (a string containing `/`) before relative
namespace lookup when that ID exists inside the loaded scene (its namespace is
applied first). Otherwise it tries the owner's namespace
before the containing scene. It never binds
across loaded scene roots. An inactive local match does not fall through to an
external entity with the same name. Placed-prefab sibling IDs resolve within their
instance namespace. Original top-level/nested root aliases survive library-based
scene loading, namespacing, editor reload, duplication and undo. Canonical paths
precede aliases; inactive matches suspend without rebinding.
Runtime-spawned prefabs resolve through `World::prefab_entity` using runtime-only
instance identity. Local sibling paths and original top-level and nested root IDs
resolve within that spawn; missing or inactive targets never bind another instance or a
scene entity. Reparenting does not change ownership. All four authored joint
kinds share this path. After stable IDs are assigned,
`World::to_scene_with_references` remaps their registered endpoint fields for
save/reopen, preserving settings and unknown fields without editing the live world.
All four kinds exercise reopened endpoint isolation, inactivity and removal;
platformer proves motor reversal and a fixed axle after reopening its Decay spawn.
Flattening into a plain expanded document discards aliases; see `prefabs.md`.

Constraints synchronize after all bodies, so endpoint collider/body rebuilds
reconnect the owned joint in the same fixed step. Missing, empty, inactive or
collider-less endpoints suspend it without failing a step; they reconnect when
available. This also means a misspelled but valid ID waits rather than producing
a runtime diagnostic. Removing/disabling the joint entity or its component drops
its constraint. Removing an endpoint clears backend ownership; reappearance can
reconnect the authored constraint. Several joint entities may connect one body.

`PhysicsWorld2d::set_distance_joint(owner, DistanceJoint2d)` validates before
replacing an owned constraint; unchanged requests retain the solver joint.
Changing endpoints/distance replaces only that constraint, preserving body
motion. `remove_owned_joint(owner)` releases it independently of legacy
`connect_distance` connections. Both distance paths use the existing positive,
finite maximum centre distance, free rotation/closer motion and disabled mutual
endpoint contacts. Runtime owner IDs are never serialized.

The checked component inspector authors endpoint IDs with a typeable entity
picker, including names and an explicit None choice. Missing references and
inactive targets are marked; unresolved text remains editable. Choices obey the
same scene/prefab scope as runtime resolution. Real picker-click regressions
retarget/clear all four joint kinds through commands, then undo restores each
constraint and its unknown fields. Visual editor review remains in final integration.
This foundation is added for the platformer: a physical lantern hangs from an
anchor while Decay wind drives it and Decay stretches/turns its visible cord.
The game regression observes movement, bounded distance and tether removal.
Additional typed owned-joint controls and complete prefab references remain incomplete;
the existing typed `Physics.connect_distance` keeps its previous semantics.

### Reversible authored joint suspension and distance tuning

All four joint components accept `enabled`, defaulting to true when omitted.
False releases the solver constraint at the next fixed synchronization while
retaining its owner, endpoint references and settings. Re-enabling reconnects
available active endpoints; missing endpoints continue to suspend the constraint.
Bodies are not rebuilt, and their motion is not reset. Reconnection and restoring
mutual endpoint collision can change motion through the ordinary physics solver.
Multiple joint kinds on one active owner remain an error even when suspended.
All active owners' settings are validated before ownership changes, including
suspended constraints and distance settings whose endpoints are unavailable.

The generic checked inspector exposes the flag. Command suspension, undo/redo,
endpoint rebuilds and all four constraint kinds are exercised.
`Physics.joint_enabled(owner)` reads the authored flag, including before the next
synchronization; it does not report whether a live constraint currently exists.
`Physics.set_joint_enabled(owner, enabled)` patches that flag.
`Physics.set_distance(owner, max_distance)` tunes an owned maximum-distance
constraint, including while suspended or before endpoints materialize.
These typed calls require physics and exactly one valid authored joint;
distance tuning requires the distance kind and a finite positive maximum.
Invalid, missing, conflicting or wrong-kind calls leave payloads unchanged.
Unknown fields are preserved. Legacy `Physics.connect_distance` is unchanged.

Platformer adds these capabilities to let the player reel the hanging lantern
in/out with T and release/reconnect it with L. Decay hides the cord while
suspended or after its owner is removed. Runtime regressions observe length,
free fall, reconnection and return to the original length.
Typed `Physics.set_joint_endpoints(owner, first, second)` retargets every joint
kind through scoped authored references at the next fixed synchronization. Null
clears either endpoint. Both handles validate before mutation; stale, unstable,
out-of-scope or identical endpoints fail. Inactive references remain authored,
with the constraint suspended until active. Settings, enabled flags, unknown
fields and body motion are preserved. No runtime handles are serialized.
Platformer switches its lantern between two hooks with R, including while released.
`Physics.remove_joint(owner)` removes the authored joint component and releases
its solver constraint at the next fixed synchronization. The owner, other
components and endpoint bodies/motion remain; legacy `connect_distance`
constraints are unaffected. Exactly one valid authored joint and a physics host
are required, including while suspended or before initial body synchronization.
Subsequent joint controls fail until a joint is authored again. Platformer Z cuts
its lantern cord; Decay hides the cord and ignores tether controls until repaired.

`Physics.create_distance_joint(owner, first, second, max_distance)` authors an
enabled maximum-distance component on an existing owner carrying no authored
2D joint of any kind. Validation and scoped endpoint rules match retargeting;
null leaves an endpoint unbound, and inactive targets suspend until active.
The maximum length must be finite and positive. Physics is required, but endpoint
bodies need not be synchronized yet. Next fixed synchronization creates the
constraint, preserving other owner components, bodies/motion and legacy connections.
Existing joints, invalid handles/references and invalid lengths fail before mutation.
Platformer C repairs the cut cord using its selected hook and length.
Script-triggered world snapshots remain absent.

Editor scene Save/Save As and subtree prefab authoring now use the active
component registry for stable-reference remapping automatically. All entities
must already have stable IDs. Invalid local targets/types or unstable endpoints
fail before writing/adopting a path, without changing the live world. Tests reopen
two isolated instances of all four joint kinds and reuse saved subtrees. The
platformer's real Decay-spawned nested windmill reopens through editor Save As,
retaining its fixed axle and motor reversal. Save is refused during Play; Decay's
number/flag progress store does not save world snapshots. Script-triggered world
save/load remains a separate absent capability.

### Scene-authored hinges and motors

`sindri.physics2d.hinge_joint` uses the same separate owner entity, stable endpoint
reference resolution and suspend/remove/rebuild lifecycle as distance joints.
One entity owns one kind of constraint; active distance and hinge components on
one owner fail synchronization rather than silently replacing each other.

```json
{
  "first": "windmill-anchor", "second": "windmill-rotor",
  "first_anchor": [0.0, 0.0], "second_anchor": [0.0, 0.0],
  "limits_enabled": false, "lower_angle": 0.0, "upper_angle": 0.0,
  "motor_enabled": true, "motor_velocity": 2.0, "motor_max_torque": 1.0
}
```

Each anchor is measured in world units in its body's translated/rotated local
frame, without transform scaling. Anchors coincide while the bodies may rotate
relative to one another. The default anchors are the centres; default limits
and motor are disabled. Connected bodies do not collide with each other.
Enabled limits bound the second body's angle relative to the first, in radians;
finite bounds must satisfy `-pi <= lower_angle <= upper_angle <= pi`.
They describe relative orientation, not accumulated revolutions.

A velocity motor targets relative angular speed in radians/second; positive
turns the second body counterclockwise relative to the first. Its force-based
velocity drive has a finite non-negative torque cap, so target speed is not a
promise under load. A disabled motor coasts; an enabled zero-speed motor brakes
within its torque cap. All numeric fields, including disabled settings, must be
finite. Negative torque and invalid enabled limits fail before backend mutation.
Settings edits between the same endpoints update the existing solver constraint
and wake both bodies without resetting their velocity. Unchanged synchronization
retains it; ordinary low-motion sleeping behavior remains applicable.

`motor_mode` defaults to `"velocity"`, including old payloads that omit it.
`"position"` selects a force-based damped angular target: `motor_target_angle`
is the second body's relative orientation in radians within `[-pi, pi]`,
`motor_stiffness` is torque per radian of error and `motor_damping` is torque
per radian/second. Gains default to zero and must be finite and non-negative,
even when disabled. The existing torque cap and enabled angular limits apply;
a target beyond enabled limits cannot override them. Targets represent principal
relative angles, not accumulated revolutions. Edits retain body motion and
solver ownership; suspension and endpoint rebuilds retain the authored mode.

`Physics.set_hinge_position_motor(joint, target_angle, stiffness, damping, max_torque)`
selects position mode, validates the complete settings before mutation and
preserves unknown fields. Zero torque disables drive. The next synchronization
applies it, including calls before endpoint bodies exist. `set_hinge_motor`
explicitly selects velocity mode again. Platformer P switches both placed and
spawned windmills between holding 0.6 radians and their reversing drive; H
recreates the placed hinge and asks its script to restore its current mode.
Checked command edits, undo/redo and scene serialization exercise the new fields;
native backend tests prove holding, retargeting, caps, limits and atomic rejection.
Editor save-path integration and native visual review are exercised.

Typed `Physics.set_hinge_motor(joint, velocity, max_torque)` controls the hinge
**owner**, not a body. It requires a hinge component and a physics host, validates
before changing its runtime component, preserves unknown fields and applies at
the next fixed synchronization. Its values persist through endpoint rebuilds and
the valid spawn-to-synchronization window. Zero torque disables the motor rather
than braking; to brake, set zero velocity with positive torque. Typed
`Physics.remove_joint` releases the authored hinge at next synchronization.

`Physics.create_hinge_joint(owner, first, second, first_anchor, second_anchor)`
authors an enabled hinge on an existing owner with no authored 2D joint of any
kind. The scoped endpoint/physics contract matches distance creation, including
null/unbound or inactive targets and pre-body synchronization. Finite `Vec2`
anchors use body-local world units without transform scale; angular limits and
motor start disabled. All validation precedes mutation. The next fixed step
creates the owned constraint while retaining other owner components and bodies.
`set_hinge_motor` can configure drive immediately after creation. Platformer H
rebuilds its placed windmill hinge and restores its selected drive while the spawned
windmill remains independently owned.

The generic checked inspector authors hinge settings and command undo reverses
motor edits. Native visual review is exercised; script-triggered world snapshots
remain absent. This general capability is added for the platformer: Decay reverses
its powered windmill axle every two seconds while physics keeps the rotor on its
anchor. Native tests exercise offset anchors, limits, torque caps, coast/reverse,
invalid atomic edits, command undo, endpoint rebuilds and gameplay removal.
Script-triggered world snapshots remain a separate absent capability.

### Scene-authored sliders and springs

`sindri.physics2d.slider_joint` constrains perpendicular translation and aligns
two body-local axes; it permits signed translation along the first axis. Anchors
use the same unscaled local world units as hinges. `first_axis` and `second_axis`
are finite unit vectors (length tolerance `1e-4`), defaulting to `[1, 0]`. They
set the relative orientation the slider maintains, so a rotated first body makes
a rotated rail. The defaults have zero anchors, disabled limits and disabled drive.
Enabled travel limits require finite `lower_distance <= upper_distance`; negative
travel is allowed. `motor_velocity` is relative speed in world units/second along
the first axis; `motor_max_force` is a finite non-negative force cap. The drive is
force-based: it approaches its target subject to the cap and load. Disabling it
coasts; enabled zero speed brakes within the cap.

```json
{
  "first": "trolley-rail", "second": "trolley-body",
  "first_axis": [1.0, 0.0], "second_axis": [1.0, 0.0],
  "limits_enabled": true, "lower_distance": -1.0, "upper_distance": 1.0,
  "motor_enabled": true, "motor_velocity": 0.8, "motor_max_force": 4.0
}
```

Slider `motor_mode` defaults to `"velocity"`, including old payloads. `"position"`
uses `motor_target_distance`: finite signed separation of the two local anchors
along the first body's axis, in world units without transform scale. Stiffness
is force per unit of position error; damping is force per unit/second. Both
fields default to zero and must be finite and non-negative, even when disabled.
The existing force cap and enabled travel limits apply. A target outside enabled
limits remains valid but cannot override them. Settings preserve body motion and
solver ownership, including suspension/rebuild.

`Physics.set_slider_position_motor(owner, target_distance, stiffness, damping, max_force)`
selects position mode and validates before patching the runtime component,
retaining unknown fields. Zero force disables drive. It applies at next fixed
synchronization, including before bodies exist. `set_slider_motor` explicitly
returns to velocity mode. Platformer O parks its trolley at signed distance 0.5
or resumes reversal; J recreation restores the selected drive and direction.
This general capability is added for the platformer. Tests exercise rotated rails
with offset anchors, signed retargeting, caps, enabled limits, coasting, atomic
rejection, lifecycle, checked command undo/redo and canonical save/reopen.

`sindri.physics2d.spring_joint` applies radial, force-based spring and damping
between two freely rotating local anchors. It pulls extended anchors together
and pushes compressed anchors apart; it does not enforce a hard maximum length.
`rest_length` is finite and strictly positive; `stiffness` and `damping` are finite
and non-negative. Defaults are length `1`, stiffness `10`, damping `1` and zero
anchors. Stiffness is force per unit of extension; damping is force per unit
of relative radial speed. Zero stiffness permits damping alone; zero damping
permits oscillation.
Under gravity the supported weight stretches the spring beyond its rest length;
a regression measures this against `mass * gravity / stiffness`.

```json
{
  "first": "trolley-body", "second": "spring-lantern",
  "rest_length": 1.0, "stiffness": 20.0, "damping": 1.2
}
```

Both kinds share the joint owner, stable reference resolver and post-body
synchronization lifecycle. Connected endpoints do not collide with each other.
Invalid settings fail before backend edits; unchanged synchronization retains
one constraint. Settings edits on unchanged endpoints update it in place and
wake the bodies without resetting their motion. Owner/component removal drops
it; inactive or missing endpoints suspend it; endpoint rebuilds reconnect in the
same fixed step. The generic checked inspector and command undo author these
settings. Native visual review is exercised; script-triggered world snapshots
remain absent. All numeric fields are validated even when their toggle is disabled.

Typed `Physics.set_slider_motor(owner, velocity, max_force)` and
`Physics.set_spring(owner, rest_length, stiffness, damping)` validate and patch
only their fields in the runtime component, preserving unknown payload fields.
They require the appropriate component and a physics host. Values apply at the
next fixed synchronization, including before endpoints are built, and survive
rebuilds. Zero force in the slider call disables its motor rather than braking.

`Physics.create_spring_joint(owner, first, second, first_anchor, second_anchor,
rest_length, stiffness, damping)` authors an enabled spring on an empty
owner under the same scoped endpoint and physics contract as distance/hinge creation. Finite
`Vec2` anchors use body-local world units without transform scale; rest length
must be finite and positive, stiffness/damping finite and non-negative (zero is
valid). All validation precedes mutation. Next fixed synchronization creates the
owned constraint, preserving other components, body motion and legacy connections.
`set_spring` can retune the new component before synchronization. Platformer B
rebuilds its light's spring at its current rest length and continues tuning while
the trolley remains on its independent rail.

`Physics.create_slider_joint(owner, first, second, first_anchor, second_anchor,
first_axis, second_axis, limits_enabled, lower_distance, upper_distance)` authors
an enabled slider on an existing empty joint owner, sharing the scoped endpoint
and physics contract. Finite anchors are body-local world units without transform
scale, and finite axes must be unit vectors. Bounds validate even when disabled;
enabled lower distance cannot exceed upper distance. Its motor starts disabled;
`set_slider_motor` can configure the new component before synchronization.
Invalid settings fail before mutation. Next fixed synchronization creates the
owned constraint while retaining other components, body motion and legacy
connections. Platformer J rebuilds its trolley slider, restores its selected
drive and keeps the separately owned spring attached. Native visual review is
exercised; script-triggered world snapshots remain absent.

These general capabilities are added for the platformer: Decay reverses a
powered lantern trolley near its rail ends, retunes its suspended light's spring
and draws the cord from solved positions. Its regression observes both travel
directions, bounded rail motion, changed light height and independent removal.
Runtime regressions exercise rotated rails, travel limits, force caps, coast/brake,
spring extension/compression, damping, weight support and atomic invalid edits.
Prefab endpoint references, velocity/position motors, editor save paths and
native editor interaction are now exercised. The native joint review (`docs/physics-joint-editor-review.md` in the Sindri repository)
records numeric Save/reopen, Play controls and Stop restoration. The 2D joint
acceptance slice is complete; 3D joints, gameplay world snapshots and final
workspace integration remain separate work.

### Runtime ownership and stepping

`PhysicsWorld2d` and `PhysicsWorld3d` are separate runtime state beside `World`,
not serialized state inside it. Each owns its private backend and maps `EntityId`
to private handles. Shared native/browser game sessions and native editor Play
synchronize and step 2D, then 3D, with the same fixed duration before scripts.
Each dimension validates its own batch; this is not a transaction across both
worlds. Editor fresh Play, Stop and scene replacement discard both solvers;
pause/resume retains them, and Stop restores the authored scene snapshot.
Despawning an entity removes its body/collider before the next
step; generation-checked IDs prevent a reused slot from inheriting old physics
state.

The host advances physics exactly once for each engine fixed update using that
fixed step duration. Render frames never step Rapier directly. Pause therefore
pauses physics automatically, and time-scale/fixed-step semantics remain engine
semantics rather than backend semantics.

The fixed-update order for the first slice is:

1. apply deferred/checkable gameplay writes;
2. synchronize static/kinematic authored state into physics;
3. step physics with the engine fixed `dt`;
4. apply queued controller sweep/slide against current solved poses;
5. synchronize dynamic/velocity-driven/controller results back to `World`;
6. publish normalized collision/sensor events;
7. run consumers that are defined to observe post-physics events.

Controller velocity is held at zero and fresh support is seeded after
synchronization, before the solve. Controller movement updates its collider
pose after the solve, so solid response and discrete sensor events observe that
move at the following solve; same-step response and swept controller triggers
remain absent. Scenes without controllers retain their existing behavior.

The exact system-ordering API is still an open roadmap item, so physics must not
quietly invent a general scheduler. Its ordering is documented as part of the
engine fixed-step contract until that scheduler exists.

### Events and queries

Backend events are normalized to Sindri events containing `EntityId`s and Sindri
collider semantics only:

- collision started / stopped
- sensor entered / exited

Contact manifolds and raw solver data are deliberately not first-slice public
API. Add them only for a demonstrated gameplay requirement.

The implemented query is `PhysicsWorld2d::raycast(origin, direction,
max_distance, RaycastFilter2d) -> Result<Option<RayHit2d>, PhysicsError>`.
`raycast_where` additionally accepts an entity predicate for a scene host to
exclude inactive/despawned entities immediately. A query-only per-piece BVH
selects candidates at current body poses, including offsets and rotations. It
updates immediately on insertion, removal and teleports, and after each completed
solver step; it does not depend on the solver broad phase being current.

Direction is normalized, distance is non-negative and inclusive, and geometry
inputs must be finite. Zero direction is rejected. Normalization accepts even
extreme finite directions; an overflowing segment endpoint is rejected. The
filter mask matches collider memberships independently of collision pair
filters, sensors are excluded by default, and excluding an entity excludes its
whole compound. Mask 0 misses. Exact ties prefer the smaller entity handle,
then authored piece order. Inside/on hits report distance 0, the origin point,
and normal (0, 0). Other hits report the world-space surface normal.

`RayHit2d` is a snapshot. Scene transform writes and new colliders take effect
at the next synchronization. Decay skips inactive/despawned entities even during
a script pass. The platformer proves ground clearance against tilemap geometry;
the Physics Playground's probe visualizes filtering, hits and normals.

2D overlaps and shape casts are implemented for circles, boxes and capsules.
Decay exposes `Physics.overlap_circle`, `overlap_box`, `cast_circle` and
`cast_box`; Orbital's mine blast and Physics Playground exercise them.
2D rays, overlaps, shape casts and controller penetration/sweep/ground phases
use the query index. Previous platform
support is reconstructed directly from that known entity's pieces.
The incremental physics update is tracked in `docs/physics-update.md`.

The 2D index uses one leaf per registered collider piece. Ray traversal tests bounds
against the finite segment; overlaps use probe bounds; casts use the union of
start/end bounds at fixed orientation. Candidates are sorted by entity handle and
piece order before the unchanged exact geometry/filtering phase. Predicates are
called once per candidate entity, rather than for every registered entity; callers
must not depend on visits to remote geometry. Masks, sensors, exclusions, inside
hits and exact ties retain their previous results. Controller skin support
expands probe bounds by contact prediction; controller casts expand start/end
bounds by the target separation plus contact allowance. Initial penetration
uses the probe bounds. Step, snap and carry collision phases share these paths.
Previous-platform verification reads only the known support entity, reconstructing
its historical piece poses regardless of where its current index bounds lie.

Only non-static bodies need post-step refresh. Position-kinematic targets remain
pending until the solver step, while other body teleports update immediately.
Scene collider edits retain their existing remove/reinsert synchronization path.
Outward bounds padding protects touching geometry; overflowing bounds take a
conservative exact-scan fallback instead of introducing new validation failures.
Work is output-dependent: a dense/long query may still select every piece, and
sorting costs O(k log k) for k candidates. Refresh is proportional to registered
non-static collider pieces, including sleeping bodies. No backend types escape
and no new dependency is introduced.

Results contain Sindri entity IDs, hit position/normal in the appropriate vector
dimension, and distance. They never expose Rapier collider handles.

The standalone 3D world exposes `raycast`/`raycast_where`, `overlap`/
`overlap_where` and `shape_cast`/`shape_cast_where` using `RaycastFilter3d`,
`RayHit3d`, `ShapeHit3d` and quaternion `PhysicsPose3d` values. Queries reconstruct
current body/local poses, independently of the solver cache: inserts and ordinary
teleports are visible before stepping; position-kinematic targets remain pending
until the solve. A query-only per-piece BVH selects finite ray segments, overlap
probe bounds and the union of start/end sweep bounds at fixed orientation.
Candidates are sorted by entity handle and authored piece order before exact
geometry/filtering. Stable predicates run once per non-excluded candidate entity;
hosts must not rely on visits to remote geometry.

The 3D index updates on insertion, removal and ordinary teleports, then refreshes
non-static pieces after each completed step, including sleeping bodies. Static
terrain needs no post-step refresh. Outward padding protects touching bounds;
overflowing piece bounds stay in a separate conservative candidate set and
non-finite probe bounds select every piece. Dense/long queries remain output-
dependent, and candidate sorting costs O(k log k). No new dependency is needed.
The fallback preserves candidates, not numerical stability of backend geometry:
shape casts at near-maximum finite coordinates/extents can return non-finite
results. Numerical failure reporting for those extreme 3D queries remains a gap.

Membership masks, sensor opt-in and whole-entity exclusion match the 2D contract.
Overlaps return sorted unique entity handles. Rays/sweeps normalize finite XYZ
directions in f64, accept extreme finite magnitudes, reject zero directions,
negative/non-finite distance and overflowing endpoints. Rays include the segment
endpoint. Ties prefer entity handle then authored piece order. Inside/on rays
return the origin with zero distance/normal; penetrating shape casts return the
probe origin with zero distance/normal. Non-penetrating casts report the touched
piece's world-space witness and outward normal. Sweeps keep probe rotation fixed;
box/sphere dimensions must be positive, and query capsules allow zero half-height
with positive radius. Pose validation uses the body's quaternion tolerance.

Native tests exercise these engine APIs and the scene driver exposes its world.
Typed Decay queries, shared sessions, editor Play and the Causeway browser
fixture exercise them against real voxel geometry (see below).

### Editor

The inspector authors the Sindri body/collider components through the existing
checked command and undo/redo path. It must provide:

- body kind and shared physical properties;
- dimension-appropriate shape and dimensions;
- sensor toggle;
- collision membership/filter masks;
- friction and restitution;
- validation errors in the inspector rather than backend panics.

The Scene view draws every 2D collider's outline from the pieces the physics
world is given, tilemap rectangles included, and the selected collider's pieces
carry drag handles (a box's edges, a circle's radius, a capsule's radius and
height), each drag one undo step. The 3D shapes have no gizmos yet.

The editor never links against or imports Rapier types directly; it consumes the
public `sindri-physics` model/schema.

### Decay

Decay exposes typed Sindri physics operations, not Rapier terminology. The 2D
surface remains `Physics`; the independent 3D surface is `Physics3d`.

`Physics3d.velocity(entity)` and `angular_velocity(entity)` return copied Vec3
values. `set_velocity(entity, velocity: Vec3)`,
`set_angular_velocity(entity, velocity: Vec3)` and
`apply_impulse(entity, impulse: Vec3)` act on active 3D bodies.
Vectors must be finite and fit engine f32 values. Velocity controls accept dynamic
and velocity-kinematic bodies; impulses require dynamic bodies. Rotation locking
keeps angular velocity zero. Invalid arguments/body kinds fail before mutation.
Controls change live simulation, leaving authored starting motion unchanged;
structural body/collider rebuilds therefore restore authored settings. Missing
3D host context and stale/inactive handles fail explicitly. Before synchronization,
controls require a valid authored 3D body with nonempty primitive colliders or
a convex-model reference, compatible ownership and no moving depth lock. Convex
schema/material fields and positive composed scale validate before queuing; hull
geometry and mass validate at synchronization. The composed transform supplies
pose, as it does for scene insertion. Requests replay in call order before the
first solve, after actual compound mass is known. Reads return copied last queued
setters or authored starting velocities (locked angular velocity is zero);
impulses cannot affect reads until materialization. Unconsumed requests expire
after a successful synchronization. Live controls retain their runtime semantics
without reparsing authored data.

The standalone engine now supplies `BodyControl3d` linear/angular velocity
setters and impulses, `apply_control` for live bodies and `remember_control`
for unregistered bodies whose authored kind the caller has validated. Requests
validate finite values and body-kind rules before queuing, replay in call order
only after actual compound mass is known, and never change authored settings.
`pending_linear_velocity`/`pending_angular_velocity` return the last matching
queued setter; impulses cannot affect them until materialization. Ordinary
velocity reads still require a live body. `validate_insertion` checks body, colliders and all pending
requests without mutation, so scene reconciliation rejects a conflicting kind
across the whole batch before gravity, removal or rebuilding. Failed validation
retains requests for a corrected retry. Rotation locking applies at replay.
Removal cancels pending requests even when no body exists. `finish_synchronize`
discards requests not materialized by a successful batch; `ScenePhysics3d` calls
it before solving. Standalone hosts must call it after their own validated
insertion batch. Four engine and three scene regressions exercise ordering/mass,
kind and input rejection, lock/retry, pre-solve replay and expiry. This is the
engine foundation toward Causeway. Four typed bridge regressions additionally
exercise real prefab spawning, copied pending reads, kinds/locks and malformed
authoring. A shared native-session regression and exported Chromium fixture verify actual
spawn replay and movement. Game/voxel integration and native editor proof remain
open.

`Physics3d.collision_started()`, `collision_stopped()`, `sensor_entered()` and
`sensor_exited()` return sorted unique lists of other active entities whose contact with this
script's entity began or ended in the last successful step. Removed/inactive handles are
filtered. Lists are copied and non-draining: every script sees the same completed
snapshot. Sensor events are discrete, without fast sweep guarantees. Shared
native/browser sessions and editor Play supply independent 2D and 3D context.
Six native bridge regressions exercise live/persistent controls, kinds, validation,
lifecycle and all four event queries; two scripts exercise copied sensor lists.
A shared-session Decay driver and Chromium fixture exercise Vec3 controls,
rotation locking, copied values and an actual solid landing event. This is host
contract evidence toward Causeway, not voxel/game proof.

`Physics3d.raycast(origin: Vec3, direction: Vec3, max_distance, mask,
include_sensors, exclude: Entity?)` and `cast_sphere(origin: Vec3, radius,
direction: Vec3, max_distance, mask, include_sensors, exclude: Entity?)` return
copied `RayHit3d?` snapshots (entity, world Vec3 point/normal, distance).
`overlap_sphere(centre: Vec3, radius, mask, include_sensors, exclude: Entity?)`
returns a copied sorted unique entity list. Queries use the engine's indexed
current synchronized geometry and active-entity predicate without stepping.
Finite f32-range inputs, positive radii, non-negative distance, normalized nonzero
direction and a finite endpoint are required. Masks select memberships as whole
u32 values, sensors opt in and exclusion skips all pieces of one entity.
Inside/on and initial overlaps return zero distance/normal; a sphere's initial
point is its origin. Exact ties prefer entity then authored piece order.
Six bridge regressions exercise queries, masks/sensors/exclusion, unsynchronized
inactivity/despawn, compound uniqueness, boundary/inside hits, invalid input and
optional/copied script snapshots. The shared-session driver and Chromium landing
fixture query actual floor geometry. Typed box/capsule overlaps and casts also
use indexed geometry, the same filters and copied results. Boxes have positive
Vec3 half-extents. Capsules lie along local Y before rotation, with non-negative
straight-segment half-height and positive radius; zero half-height is a sphere.
Orientation is a finite nonzero Vec3 axis (normalized) and finite angle in radians
using the right-hand rule; identity uses angle zero. Axes normalize in f64 before
unit-quaternion conversion, avoiding length overflow/underflow. Casts keep this
orientation fixed during travel. All inputs fit f32 range. Five bridge regressions
exercise rotated misses/hits, sensor/exclusion filtering, normalized sweep travel,
initial overlaps, invalid inputs and zero-height/extreme-axis validity. Typed and
shared-session/browser scripts exercise all four probes on actual geometry.
Existing extreme geometry numerical limitations still apply.

`Physics3d.layer(name)` and `mask(names)` read the active authored 3D world's
labels independently of 2D. The first 32 labels map to mask bits; duplicate
labels choose the first nonempty matching name. Empty labels cannot be selected,
repeated requests combine with OR and an empty list yields zero. Inactive
settings are ignored; current authored edits are visible before synchronization.
Unknown names, wrong types/arity, malformed settings, multiple active 3D worlds
and absent 3D context fail explicitly. Bridge regressions cover the high u32 bit,
duplicates/empty names, invalid inputs, active authoring and real query filtering;
the typed query and shared-session/browser fixtures use named masks. This is
scripting integration toward Causeway, whose voxel/game proof remains open.

Initial 2D gameplay operations should cover:

- get/set linear velocity;
- apply impulse;
- sensor/collision event observation;
- 2D overlap/shape casts when a real game needs them; closest-hit raycasts are implemented and proven by platformer clearance.

Dimension is explicit in names/types where ambiguity would otherwise exist. The
language workspace remains independent: `decay/` gains no Sindri dependency;
`sindri-decay` performs all conversion and host calls.

### Gather proof

The first 2D physics track is incomplete until Gather visibly depends on it.
Gather should evolve so that:

- authored walls/obstacles have static colliders;
- the player has a collision-constrained body/collider rather than walking
  through those obstacles;
- pickups use sensor enter events instead of a hand-written distance check;
- at least one hazard interaction uses collision or sensor events;
- native and browser Gather exercise the same physics semantics.

Pathfinding remains navigation, not physics. The Wisp may plan through the grid,
but collision remains authoritative for physical overlap. Do not couple Rapier
into `sindri-grid` to make those systems agree implicitly.

### 3D engine foundation and remaining integration

`PhysicsWorld3d` now implements standalone fixed-step simulation over Sindri-owned
`RigidBody3d`, `Collider3d`, `PhysicsPose3d` and `PhysicsEvent3d` values. Vectors are
XYZ; body and local collider rotations are unit quaternions in `[x, y, z, w]`
order. Quaternions must be finite with squared norm within 0.0001 of one; zero or
non-unit rotations outside that tolerance fail explicitly; accepted near-unit
values normalize at the backend boundary. Shapes are boxes, spheres and Y-axis
capsules with finite positive dimensions. Friction is non-negative, restitution is in [0, 1], and damping is non-negative.

Bodies support static, dynamic, position-kinematic and velocity-kinematic kinds.
Insertion validates the whole body and every piece before mutating the backend;
errors identify the failing piece. Compounds use local offsets/rotations and sum
mass. Gravity scale, damping and all-axis rotation locking are engine policy.
Locked rotation clears authored initial angular velocity and angular setters.
Velocity/angular-velocity setters accept dynamic or velocity-kinematic bodies;
impulses require dynamic bodies. All controls reject invalid values before
mutation. Teleports preserve velocity; position-kinematic moves/targets take effect
at the next fixed step. Missing handles fail instead of queuing future spawns.

One positive finite engine `Duration` advances exactly one step. The engine
returns solid/sensor start/stop events with sorted entity pairs and dimension-
specific event values; event stream order remains backend-owned. Membership/filter
masks use the same mutual-pair rules as 2D. Removing an entity removes all of its
pieces and drops events whose handles no longer map to live entities, preventing
reused backend slots from inheriting old identity. No backend types escape.

Native tests exercise XYZ integration, gravity scale, all shapes landing on solid
geometry, rotated/offset collision, quaternion round-trip, masks/sensor enter/exit,
kinematic target timing, compound mass/impulse, teleport velocity, rotation lock,
atomic rejection and removal/reuse. WASM compilation is a separate gate; no 3D
voxel/game proof is claimed. A scratch exported host fixture separately exercises
XYZ motion and solid-box landing in Chromium through a read-only Decay observer.

The scene now registers `sindri.physics3d.rigid_body`, `sindri.physics3d.collider`
and `sindri.physics3d.world`. Body payloads use the engine's XYZ fields (including
`position` and `rotation`), but an entity's composed `Transform3D` is authoritative
when present. Without a transform, the body pose is the fallback. Colliders accept
one piece or `{ "pieces": [...] }`; a collider without a body creates a static
body, while a body without a collider takes no part. Shape dimensions and
offsets are local units. The scene applies the entity's
composed world scale (including parents) once before solver insertion and voxel
reach planning. Without a transform, scale is one. Positive finite uniform scale
works for all pieces; boxes also support nonuniform scale if their local rotated
axes remain orthogonal. Axis-permuting rotations work; sheared boxes, nonuniform
spheres/capsules, zero/negative scale and overflowing dimensions/offsets fail
explicitly, naming the entity and piece when applicable. The scene does not
approximate ellipsoids or stretched capsule caps. Collider wireframes use the
same resolved geometry and omit unsupported geometry. Standalone solver values
remain world units, and 2D collider semantics are unchanged. Existing scenes
authored with world-unit dimensions must convert them to local units; an
axis-aligned box's old half extents and offsets divide by its composed scale.
Orbit Camera Lab is converted, while the quarry's unit-scale pieces need no edit.
Orbit Lab's keyboard shrink/restore controls exercise live scaling through
Session and real Chromium: center and edge rays verify synchronized geometry,
including reactivating a removed barrier. Native scale review (`docs/collider-scale-editor-review.md` in the Sindri repository)
exercises inspector edits, matching wireframes, Undo/Redo, persistence, Play and
Stop restoration. Explorer game adoption remains open.
World settings accept `gravity: [x, y, z]` and optional
layer labels; removing/disabling the settings restores the host's gravity.
Multiple active 3D world settings fail explicitly.

`ScenePhysics3d` validates the complete active authored batch before changing
runtime gravity, lifecycle or bodies. Both scene drivers reject an active entity
claiming 2D and 3D body/collider/controller/tilemap ownership. Moving 3D bodies
reject a Z-locked local transform rather than allowing simulation and rendering
to disagree. The driver removes inactive/despawned entities and removed colliders,
including inherited inactivity, and re-registers them on reactivation. Unchanged
components preserve live velocity; transform/ancestor moves teleport or set a
position-kinematic target. Any body/collider payload change or resolved geometry
change caused by entity
or ancestor scale rebuilds that body and resets its motion to authored values.
Unchanged scale does not rebuild; scale writes become effective at the next
fixed step. Authored collider payloads are never rewritten by scaling. Live
coefficient/control patching is not a
3D scene capability yet.

After solving, all non-static body kinds write XYZ/quaternion poses back through
parent space, preserving scale and component payloads. Parent bodies are written
before children regardless of allocation order; world transforms are snapshotted
before writes. Equivalent quaternion signs do not count as external movement.
Position-kinematic targets set through the runtime world write back after solving.
Events describe the last successful step; invalid input retains prior runtime
state/events. Registered defaults and body-kind choices are available to generic
checked authoring. Tests exercise commands/undo/redo and stable-ID save/reopen,
not native inspector or Play interaction. The Physics menu includes all three
3D components, and its registered-component coverage test passes.

This is the engine prerequisite for the physics-update voxel-world proof in
Causeway. Two shared-session regressions exercise XYZ motion, solid landing and
disabling/reactivating bodies; browser host execution exercises the same motion
and landing. Native editor interaction is exercised in
the native review (`docs/physics-voxel-editor-review.md` in the Sindri repository), and authored voxel
collision (below) supplies the occupied/resident/edited world proof. Typed
rotated probes are exercised as described above. CCD controls,
contacts, force/torque, materials, joints and character controllers also have no
3D authoring/Decay surface yet. The 2D API remains unchanged.

### Live 3D collision coefficients

`PhysicsWorld3d::materials(entity)` returns copied `PhysicsMaterial` values.
`set_materials(entity, values)` updates existing colliders without rebuilding
their body or geometry. The order is the original primitive pieces followed by
convex hull pieces, then static groups in ascending key and piece order. Static
triangle meshes use their original piece order. An existing static owner whose
groups have all been removed accepts an empty list; an absent owner fails.

The complete piece count and all coefficients validate before any mutation.
Friction is finite and nonnegative; restitution is finite in `[0, 1]`, using
the same asset-free `PhysicsMaterial` rules as 2D. Updates preserve collider
and body handles, pose, mass, linear/angular velocity, forces, position-kinematic
targets and checkpoint replay. Changed values refresh contact processing and
wake affected bodies, including sleeping neighbors of a changed static floor.
Identical values leave sleeping bodies asleep. Friction retains the minimum
combine rule and restitution the average rule.

Rapier can reuse a resting contact's cached coefficients even after a pose
refresh. A changed batch therefore requests one full contact update on the next
successful step, temporarily bypassing contact recycling for that step only.
Normal recycling resumes afterward. Checkpoints retain the pending refresh;
an invalid timestep does not consume it. Geometry and contact identities remain
intact. A profile reload may consequently cost one full world contact pass.

Native backend regressions exercise mixed primitive/hull validation and state,
static triangle/group order and identity, empty owners, sleeping bodies,
kinematic targets, friction changes on existing contacts without new start
events, sleeping-neighbor wakeup and restitution/replay.

Scene synchronization resolves profiles before committing any 3D body or voxel
batch. Missing/wrongly typed profiles and invalid enabled overrides leave live
3D geometry, motion and events intact. Cached model plans start from authored
literal coefficients every step, so removing a profile restores those values.
Coefficient-only edits (including literal model coefficients) preserve body
motion, character support state and existing contacts. Structural geometry,
layers, sensor, scale or body edits retain the normal rebuild behavior.

Voxel coefficient edits retain compiled boxes, section keys and solver groups,
consume no geometry rebuild budget and apply coefficients in solver group order.
Session builds one source set for both dimensions before stepping; restoring a
checkpoint keeps the latest profile assets delivered by the host. Export discovers
3D profile references in inactive entities and spawned prefab contents as well as
ordinary scenes. The editor registers Physics Material 3D in its Physics family,
with a profile asset field and command-backed generic override controls.

Model Collision Lab's floor and spawned convex prop share `materials/slide.profile`
and explicitly override restitution to zero. Native scene/runtime/export
regressions exercise sharing, precedence, invalid-batch retention, reload,
profile removal, structural rebuilds and checkpoint replay. This capability is
added for Explorer, with feature-lab proof; Explorer adoption remains pending.
The rebuilt Chromium smoke fetches the profile and both GLBs, reaches both goals,
checks rendered colors and reports no runtime/GPU errors. The native editor
review (`docs/model-collision-editor-review.md` in the Sindri repository) covers
profile selection, overrides, Undo/Redo, save/reopen, valid/invalid asset reload,
prop Play goal and Stop restoration. Native visual evidence is X11/software
Vulkan; broader platform/GPU coverage remains open.

### Replaceable static collider groups

`PhysicsWorld3d::replace_static_group(entity, key, pose, pieces)` changes one
static geometry group under a real engine entity, retaining that body's handle,
all other group's collider handles and any originally inserted pieces. A first
nonempty group creates a static owner; group keys are caller-owned runtime u64
identities, not scene or entity handles. This is a general streamed/editable
geometry primitive toward Causeway, with no voxel dependency in the solver.

Pose belongs to the whole owner, so changing it moves every group together.
Each piece retains its local offset/rotation, sensor, masks and coefficients.
Immediate indexed queries see the new geometry and return the owner's entity;
exact ties use original pieces, then ascending group key and piece order.
Replacement rebuilds only that group's backend colliders, although query-index
ordering/pose refresh visits the owner's retained pieces as well. Hosts must
bound total owned pieces and per-step edits; the API imposes no residency budget.

`validate_static_group` checks pose, all pieces, static kind and (for a new
nonempty owner) pending-control compatibility without mutation, allowing hosts
to prevalidate their whole edit batch. Invalid replacements preserve geometry.
`remove_static_group` releases one group, preserving body and all other pieces;
an unknown group returns false on an existing static owner. Missing/nonstatic
owners fail. Replacing with an empty list removes that group and applies the
owner pose; it never creates an absent owner. An empty existing owner remains
registered until ordinary `remove`, which cancels every group and allows clean
owner reuse. Changed/removed pieces follow ordinary structural collider event
semantics; retained pieces are not rebuilt.

Six native regressions exercise immediate replacement/removal queries,
original-piece preservation, actual body/unaffected collider handles, canonical
ordering, invalid inputs/nonstatic owners/pending conflicts, sensor/mask/local
pose filtering, empty/reused owners and a dynamic body's landing/contact event
against its real owner followed by falling after removal. All 187 physics tests,
warning-denied native checks/Clippy and all-target/all-feature WASM checks pass.
This is an engine prerequisite. The scene snapshot adapter below now provides
revision caches and bounded reconciliation; authored policy/residency production
and real Causeway/Decay/editor/browser streamed collision remain absent.

### Authored voxel colliders

`sindri.physics3d.voxel_collider` opts the voxel world on the same entity into
3D collision. It carries friction, restitution, layers and a residency margin
in voxels (default two). The entity must carry `sindri.voxel_world` and must
not also carry a 3D body or collider: the collision cache owns that entity's
static solver body exclusively, and a conflict fails the step before either
batch changes the solver.

What collides is a block set's answer, not navigation's. `TileDefinition`
gains `collides` (default true): a colliding block contributes its full
`bounds()` box (height or extent, in sixteenths), and a block with
`"collides": false` -- Causeway's water -- is passed through. `supports` and
`walkable` are not read: a railing post stops a crate and holds no walker up.
Worlds of numbered materials collide as whole cubes. The policy revision is a
hash of every block's answer; each section's occupancy revision is a hash of its
voxels after edits, so an edit, a different generator or a rebound block set
all change the revision and equal content reuses cached geometry.

Residency follows what can hit voxels. Scene characters add their scaled probe
extent, requested travel and skin/snap/step margins to the same residency plan,
so characters alone retain collision terrain. Each step, every planned dynamic body
contributes its farthest piece extent plus twice the travel its velocity and
gravity allow in that step, transformed into the world's voxel space (the
entity's composed pose and scale, then the grid's cell size and centring from
`voxel_space`) and widened by the margin. The union of sections those reaches
touch is the complete snapshot handed to `SceneVoxelCollision3d`; terrain no
body is near is not resident, wherever the camera is. A single reach wider than
the section budget fails the step whole rather than publishing a partial window.
Ordinary authored kinematic and static bodies do not drive residency; scene
characters do. Queries see only resident sections -- a script asking about distant terrain uses `Grid`/`Aim`.

`ScenePhysics3d::step_with_tile_sets` resolves these worlds through
`VoxelGround` (generator, palette and edits), plans the voxel snapshot against
the same solver state as the body batch, validates both, then commits bodies
and voxels before solving. The cache lives inside `ScenePhysics3d`, so a fresh
session or the editor's Play/Stop reset clears both together. `step` is the
same call without block sets, for hosts whose voxel colliders use materials.
Shared native/browser game sessions and editor Play pass their bound block sets.

Causeway exercises it: taking a laid block back spawns a loose-block prefab
whose dynamic body pops out of the emptied cell, falls onto the world's own
voxels and returns to the stock only when `Physics3d.collision_started` reports
the Floor world (a three-second fallback covers water and edges). Scene
regressions cover landing and the owner contact event, raycasts against the
owner, falling down an edited shaft after the body has slept, far-from-origin
residency, block-set non-colliding water and half-height slabs, releasing
geometry when the component is removed, and atomic rejection of missing worlds,
conflicting owners, unbound block sets and over-budget reaches.

### Resident voxel snapshot adapter

`SceneVoxelCollision3d` connects resolved resident `VoxelSection` snapshots to
keyed static solver groups. It owns each supplied real scene entity's solver
body exclusively; it creates no artificial section entities and never steps
physics. Hosts provide the complete set of active collision worlds/sections on
every `synchronize` call, independently of render visibility. The adapter takes
`World` only to validate owner lifetime/activity; authored component resolution,
composed pose/scale and residency selection remain the host's responsibility.

A `VoxelCollisionWorld3d` supplies owner, world pose, positive finite XYZ scale,
shared layers/friction/restitution/sensor settings, a collision shape policy and
its revision. Each `VoxelCollisionSection3d` supplies coordinate, voxel snapshot
and occupancy revision. Revisions must change whenever the corresponding data
changes: replacing a generator with sections whose local revision is again zero
must **not** reuse the old occupancy revision. Likewise changing a callback's
answer requires a new policy revision. Solidity is explicit, not guessed from
render occlusion or navigation walkability. All blocks in a snapshot share the
world's physical settings; distinct block coefficients need separate groups or
a later richer policy. Reflected/nonpositive scale is rejected. Geometry stays
in the engine's f32 coordinate domain, with its existing extreme-value limits.

Compiled boxes and collider arrays are retained by occupancy/policy revision,
scale and physical settings. Unchanged sections keep their geometry; scale or
coefficient changes rebuild collider pieces from cached boxes without recompiling
occupancy. Pose-only changes move the owner without replacing sections. New keys
are allocated in section-coordinate order and retained while resident. Departed
sections release their groups; omitted, inactive or despawned owners release the
owned body. A supplied stale owner is an error. Removing the last section keeps
an empty owned body until its world is omitted. Reset the cache together with
the solver; unrelated/lost owned bodies are rejected. Dropping the cache alone
does not clean up the solver: synchronize an empty snapshot first unless both
objects are being reset.

`VoxelCollisionBudget3d` defaults to four active worlds, 64 retained sections,
eight rebuilt sections per call and 8,192 total retained collider pieces.
Each compilation visits at most 4,096 cells. Query-index refresh still visits
retained owner pieces. Removal work is bounded by previously retained sections/
pieces. Over-budget inputs fail before changing solver geometry or cached
revisions; the adapter does not publish a partial window or silently defer a
hole. Hosts must choose a bounded complete collision window or deliberately
configure larger limits. Snapshot validation and every changed collider group
complete before any insertion, removal or pose update; failed input retains the
previous snapshot for a corrected retry. `VoxelCollisionReport3d` reports active
world/section/piece counts and compiled/rebuilt/removed section counts.

Seven native scene regressions exercise unchanged/edited/policy cache reuse,
retained collider arrays, transformed slab bounds, scale/settings/pose updates,
all four budget failures, late invalid inputs and retry, duplicates/ownership,
negative-section departure/reentry, inactivity/despawn/reset and actual landing/
contact on occupied voxel geometry followed by falling through an edited hole.
All 497 scene tests, warning-denied native checks/Clippy and all-target/all-feature
WASM checks pass. This is the scene/solver seam; authored voxel colliders
(above) produce its snapshots from real worlds.

### Voxel collision geometry foundation

`compile_section_collision` in `sindri-voxel` accepts a `VoxelSection` and a
caller-owned `VoxelId -> Option<VoxelShape>` collision policy. Air is always
empty; `None` makes other blocks noncolliding. Solidity is independent of render
transparency/occlusion, so a host must supply its actual block policy rather than
infer collision from what hides a face. The function never samples a generator
or a neighbouring/unresident section and never mutates occupancy.

`SectionCollisionBox` uses exact section-local integer min/max bounds in
sixteenths of a voxel, within 0..=256. Full cubes merge greedily in X, then Z,
then Y, across material IDs that the policy classifies as full. Partial slabs,
posts and other box shapes retain their individual exact bounds. Output is
deterministic for a stable policy, disjoint and covers precisely the collidable
occupancy; the box count is at most 4,096, not necessarily the minimum possible.
Invalid zero-thickness, inverted or out-of-voxel shapes return a typed
`VoxelCollisionError`. Physics coefficients and per-block collision layers are
not represented in this geometry compiler; hosts needing distinct coefficients
must partition policy/output rather than silently merge incompatible settings.

Five native regressions exercise air/full sections, merging across materials,
24 varied occupancy patterns with exact cover/no-overlap/no-air assertions,
partial shapes/noncollision, worst-case output bounds, typed errors and edited
negative sections across residency unload/reload. The crate also checks for
WASM. This is a renderer/physics-independent engine prerequisite toward Causeway,
not solver or game proof. Scene integration must retain compiled geometry by
occupancy **and policy** revision, rebuild only entering/edited sections, release
leaving/disabled worlds, apply composed transforms and bound per-step work.
The native scene adapter now exercises resident snapshot collision and owner
queries/events. Authored host/game/editor/Decay/browser integration remains
absent. No component or host surface changes here.

### Feature-track slices

Implement this in reviewable PRs rather than one giant patch:

1. **Physics foundation.** Add `sindri-physics`, dependency-policy changes,
   Sindri-owned 2D/3D public model, backend masking, fixed-step 2D runtime,
   collision layers/events, native + WASM tests. Do not mark Editor or Script
   complete.
2. **2D authoring.** Register/serialize the 2D scene components, inspector
   controls, checked edits, undo/redo, and collider gizmos.
3. **Decay 2D.** Add typed velocity/impulse/event access through `sindri-decay`
   with language/host contract tests.
4. **Gather physics.** Convert obstacles, pickups, player movement, and one hazard
   interaction to the real physics path; prove native/browser behavior and update
   capability docs/matrices.
5. **3D physics.** Establish the standalone runtime first, then integrate
   scene/editor/Decay surfaces with a real 3D proof that exercises them end to end.
   The engine foundation alone does not complete this track.

Each PR runs all checks relevant to its dependency and target surface. A slice
that introduces Rapier must run `cargo deny`; a slice touching browser-reachable
physics must compile WASM; the Gather slice must run the real browser smoke test.

### Non-goals for the first 2D slice

- richer joints (a runtime maximum-distance joint now exists)
- continuous-character-controller abstraction
- further collider shapes (authored 2D compound colliders now exist)
- persistent contact IDs and mutable contact-manifold scripting
- physics-driven visual scale
- platformer navigation
- backend selection/plugin API
- deterministic cross-platform floating-point lockstep

Those are not forbidden forever. They are deliberately absent until a real game
needs them.

#### Joint motor authoring choices

Hinge and slider `motor_mode` fields are registered choices derived from
`MotorMode2d`: `velocity` and `position`. The generic inspector offers these
choices through its existing checked-command path, including the default
velocity mode for old payloads that omit the field. Selecting a mode preserves
other settings; it does not enable the motor or reset its target or gains.
This closes an authoring gap found while reviewing the platformer mechanisms.

### Geometric movement foundation

`PhysicsWorld2d::move_and_slide` computes read-only 2D swept displacement with
positive skin, bounded iterations and explicit penetration/budget outcomes.
It uses current poses and existing query filtering; ordinary shape casts retain
their zero-normal initial-overlap contract. See character movement (in `07-physics-and-movement.md`)
for the result semantics, validation, ownership and remaining vertical slices.
This is not yet the full character controller; one-way geometry is two-sided
in this primitive. No editor, Decay or platformer integration is claimed.

Ground probes now report nearest support and classify world-space normals
against configurable up and slope limits, including zero-travel contacts at the
skin. Steep surfaces are reported, not skipped; initial penetration is unwalkable.
The character movement contract (in `07-physics-and-movement.md`) defines tolerances
and remaining controller movement policy. Ordinary geometric
queries are unchanged; this is not yet controller or game proof.

`move_and_slide_grounded` composes sliding with post-move support and optional
snapping using one shared skin. Snap defaults off; positive downward travel is
accepted only for walkable support. Upward requests and initial penetration
suppress snapping and grounded state. The result separates slide motion, support,
snap and total translation; gameplay applies total translation once and owns
prior support state. Native tests exercise landing, ledges, blocked ascent,
repeated support and downhill snapping.
One-way controller policy and scene ownership are now implemented; checked
editor/Decay/platformer proof remain open.

Grounded sliding also enforces `max_slope_angle`: upward-facing steep contacts
cannot create rise beyond the positive remaining request. Horizontal approaches
stop at steep slopes; explicit jumps can slide with bounded rise and steep descent
remains possible without support or snap. Walkable slopes, vertical walls and
ceilings keep geometric projection. Motion and support share the slope tolerance;
ordinary `move_and_slide` remains unrestricted. No gameplay gravity or constant
slope speed is supplied. See the character contract and native slope regressions.

Optional grounded steps use `step_height` (default zero) and require initial
walkable support, no ascent, clear full-height lift, improved forward progress,
a walkable landing within the height cap and final non-penetrating support.
Ceilings, overhangs, tall obstacles and absent/steep support retain the ordinary
path. The result separates step lift, selected forward slide and downward landing;
apply their total translation once. Full-height clearance and skin-sized minimum
progress are conservative limits. Impact contact geometry now refines movement
cast normals, preventing large artificial hops on flat box faces. Ordinary
ray/overlap/shape-cast contracts are unchanged; see the character contract/tests.

Opt-in `platform_support` captures the previous synchronized support entity/pose.
Grounded movement verifies old walkable contact using current local pieces, derives
the origin's displacement through previous/current body poses and sweeps carry
before character motion. The support alone is excluded during carry, then restored
for ordinary slide/step/ground queries. `platform` reports requested and actual
carry plus collisions/current pose; total translation includes it once. No velocity
inheritance or persistent snapshot state is supplied. Hosts advance/clear snapshots
for grounding changes, teleports or structural edits and avoid additional parent/
solver motion. Wall clipping and ceiling crush remain explicit. Rotation follows
a chord with fixed probe orientation, not an arc/orientation sweep. See the
character contract for ordering, limitations and native kinematic/geometry tests.


Grounded controller phases now respect per-piece one-way support sides/cones;
request-scoped `drop_through` ignores only one-way solids while ordinary geometric
queries remain two-sided. Scene-authored `sindri.physics2d.character` owns one
solid collider probe (additional sensors allowed) and derives a stationary
kinematic body, rejecting simultaneous rigid-body ownership. Runtime
`CharacterRequests2d` queues displacement/snap permission and timed drop-through;
`ScenePhysics2d` seeds fresh support, captures synchronized poses, applies movement
once and exposes cached results through `character_motion`/`CharacterMotions2d`.
Settings, parenting, teleports, removal and structural edits clear old state;
positive drop remainders cover a whole pass and decrement afterward. Saved
component data contains settings, never input queues or runtime support handles.
No game rules are supplied. See the character contract for frame ordering,
query masks, transform semantics, sensor timing and remaining editor/Decay/game
integration. Compound character probes, same-step solver response and swept
controller triggers remain absent.

### 3D character query foundation

`PhysicsWorld3d::move_character` and `move_character_where` wrap Rapier's
kinematic character controller in engine-owned `CharacterOptions3d`,
`CharacterMotion3d` and `CharacterCollision3d` values. They are read-only
world-displacement queries with current-pose geometry, slope/step/snap options
and copied hit results; applying motion and gameplay policy are separate.
Scene ownership is described below; there is no Decay surface yet. This is added
generally for Explorer's planned movement, with native engine regression evidence and
honestly pending game/editor/browser proof. See
Character movement (in `07-physics-and-movement.md`)
for validation, backend flag semantics, numerical limits and remaining slices.

#### Scene-owned 3D character movement

`sindri.physics3d.character` derives a stationary kinematic body from one scaled
solid Collider 3D piece, with optional sensor pieces and no authored rigid body.
`ScenePhysics3d` consumes queued world displacement after the solve, applies it
once and writes the result through the existing parent-space transform path.
Input/solver reach participates in voxel residency before any bodies change.
Actual solved carry expands residency before rider queries; a carry budget failure
retains queued input and leaves riders unmoved but does not roll back the solve.
Session uses this same driver, including checkpoints; no host assembles another
movement loop. Backend limits, request/result lifetime, next-solve sensor timing
and remaining Decay/game proof are specified in
the character contract (in `07-physics-and-movement.md`).

#### Classified 3D ground queries

`PhysicsWorld3d::probe_ground` and `probe_ground_where` expose read-only nearest
support geometry through `GroundOptions3d` and copied `GroundProbe3d` values.
They classify skin/touching contacts and downward casts against a slope limit,
retain steep nearest surfaces and reject initial penetration as support. Filters,
stable whole-entity predicate reuse, deterministic ties and current poses use the
existing query index. No snap, carry or gameplay standing policy is applied.
Native geometry/filter/lifecycle/invalid-input regressions exercise this general
Explorer prerequisite; browser/editor/Decay/game proof
remain pending. See the contract (in `07-physics-and-movement.md`).

#### Read-only 3D platform carry

`move_character_grounded` / `move_character_grounded_where` combine optional
`PlatformSupport3d`, collision-limited carry, ordinary Rapier movement and
classified final support. Current compound geometry verifies the previous pose;
translation and rotation-point travel sweep before input movement, with separate
copied carry/movement hits and one total translation. Pending targets contribute
nothing until solved. Native regressions cover geometry, filtering, wall clipping,
vertical platforms, jumps and validation. Hosts still own snapshot invalidation,
once-only application and terrain reach; scene ownership is described above. See
the contract (in `07-physics-and-movement.md`) for chord,
fixed-orientation and backend correction/iteration limits.

Scene Character 3D enables platform carry by default, with a flat
`carry_platforms` opt-out. Support is classified before solving; solved translation
and rotation-point carry precede input movement and are included once in the
cached `GroundedCharacterMotion3d`. Dependent rider caches invalidate with support
lifecycle changes; fresh snapshots prevent replay after clipped travel. Native
scene and Session regressions exercise parenting, jumps, lifecycle, replay and
terrain budgets. Browser/controller, editor interaction and Explorer proof remain
pending; no controller-on-controller carry is claimed.

#### Imported-model collision preparation

Assets now prepare bounded model-space triangles with accumulated selected-node
transforms, reflected winding and primitive-instance ranges. This is the CPU
geometry prerequisite for model-derived static meshes and convex hulls, added
for the planned Explorer showcase. Static triangle geometry and standalone-world
queries are now implemented; scene asset bindings remain absent. Standalone
convex hulls are available below.
No runtime body is created by asset preparation. See the asset contract (in `09-3d.md`).

#### Static triangle geometry

`TriangleMesh3d::new(vertices, triangles)` builds an immutable, clone-shared
physics resource without an asset, renderer or scene dependency. Its private
backend acceleration structure is shared across instances and world snapshots.
It accepts open, disconnected and nonmanifold surfaces without welding or repair;
rays see both sides and gaps remain empty. A mesh is a surface, not a solid
volume or a convex approximation. Authored winding remains meaningful for normals.

Construction rejects empty geometry, more than 1,000,000 vertices or triangles,
nonfinite coordinates or coordinates outside +/-100,000,000 units, out-of-range
indices and degenerate triangles. Triangle cross-product squared length must be
positive and finite in backend f32 arithmetic; extremely small triangles can
therefore fail even after CPU model preparation succeeds. These bounds keep
construction arithmetic representable. `TriangleMeshError3d` identifies a failing
vertex or triangle; failed construction returns no partial resource.

`TriangleMeshCollider3d` adds local pose, sensor flag, collision layers and
`PhysicsMaterial` coefficients. `PhysicsWorld3d::validate_static_mesh_group` and
`replace_static_mesh_group` validate the owner pose, every piece (with its index),
composed mesh bounds, static ownership and incompatible pending controls before
mutation. Static meshes share keys, body identity, ordering, replacement and
removal with existing primitive groups. A group can replace either kind. The
owner pose moves all groups; piece poses remain local. An empty replacement
removes a group and never creates an absent owner. Queries see changes before
a solver step; simulation contacts update at the next solve.

Eight native regressions exercise geometry errors/budgets, shared resources,
open gaps and two-sided rays, immediate ray/overlap/cast/ground/character queries,
local rotation and filtering, replacement/removal, failed late-piece edits and
pending controls, original body insertion/duplicate ownership, dynamic sphere
landing and identical cloned-world replay.
This is standalone physics proof for the planned Explorer showcase. Primitive
`ColliderShape3d` remains unchanged. Explicit static scene asset bindings are
described below; automatic host loading and native/browser/editor/Explorer
project proof remain pending;
WebAssembly compilation alone does not prove browser behavior.

#### Convex hull geometry

`ConvexHull3d::new(points)` builds an immutable, clone-shared volumetric hull
without asset, renderer or scene dependencies. It fills concavities rather than
performing convex decomposition. Duplicate and interior points are allowed;
scale is baked into input positions. Construction accepts 4..=4,096 points in
+/-10,000 local units. Too few/too many points, nonfinite/out-of-range points,
flat/collinear/identical input and unusable backend mass properties return
`ConvexHullError3d`. Unit-density mass, principal inertia and their inverses must
be positive and finite, with finite center of mass and inertia-frame rotation.
Tiny, thin or numerically ill-conditioned hulls may therefore be rejected.

`ConvexHullCollider3d` carries the shared geometry, local pose, sensor flag,
layers and existing `PhysicsMaterial`. `PhysicsWorld3d::validate_hull_body` and
`insert_hull_body` take a rigid body, primitive pieces and hull pieces. Diagnostics
number primitives first, then hulls. Compounds require 1..=1,024 total pieces.
The body, all pieces, composed bounds, individual/combined mass properties,
entity ownership and queued controls are checked before insertion. Failure keeps
pending controls and leaves the world untouched. Geometry has default unit
density; sensor pieces contribute mass, matching existing primitive behavior.

Hull bodies use the ordinary insertion, coefficients, immediate queries,
teleports/kinematic targets, velocity/impulse controls, events, removal and world
snapshot lifecycle. All four body kinds work; a hull may join primitives in a
compound. Primitive-only compounds also pass through this API, subject to its
stricter count and mass checks. Static hulls remain original body pieces when
keyed static groups are added later.

Eight native regressions exercise invalid/degenerate construction, shared
geometry and exact tetrahedron rays/interior overlap, mixed-compound mass and
local rotation/offset, pending impulses, dynamic landing and cloned replay,
atomic invalid edits/budgets/duplicate ownership, both kinematic kinds and sensor
filtering/enter/exit events. This foundation is added for the planned Explorer
showcase. Model-derived scene bindings now have explicit native proof below; automatic
asset loading and native/browser/editor/game project proof remain pending. Serialized primitive shapes and Decay APIs do not
change in this slice; WASM compilation is not browser behavior proof.

#### Static model scene ownership

`sindri.physics3d.model_collider` requires a logical `asset` ID and optionally
accepts `friction` (default 0.5), `restitution` (0), `layers` (all) and `sensor`
(false). It owns one static body and cannot coexist with a 3D rigid body,
primitive collider, character or voxel collider. Mixed 2D/3D ownership is rejected.
A render model is optional and can name a different asset. There is no invented
blank asset or engine add-component default. The editor completes the component
from a real project GLB and offers a model asset picker through declared field
metadata. It withholds competing body/collider/character components in either
order. Render and collision asset choices remain independent and undo separately;
see the native authoring review (`docs/model-collision-editor-review.md` in the Sindri repository).

Hosts explicitly prepare and deliver resources with
`Session::bind_model_collision(id, &ModelAsset)` after asynchronous decoding.
The runtime calls bounded CPU model preparation, then scene-owned
`ModelCollisionGeometry3d` validation; neither scene nor physics depends on
assets. Failed preparation keeps the previous binding. Removing a binding makes
active references fail before the next 3D solve, retaining live geometry until
corrected. Shared model asset discovery and export include collision-only
references from scenes and prefabs, including inactive entities. Automatic
native/browser player collision binding now runs after asset loading completes
and before Session starts. It expands all scene/prefab references, including
inactive entities and overrides, and prepares only authored collision assets.
Missing or invalid geometry names the logical asset ID. Native headless
`ProjectRun` waits for its worker queue and installs authored collision before
stepping. Editor Play uses its asynchronous model queue, including unsaved world
references and retained later-scene/prefab assets; unavailable or invalid assets
keep Play stopped with a named diagnostic. Successful model reloads replace
collision before the next step; failures retain the last valid binding. Stop
releases Play-only retention. References introduced during Play require restarting
the run. Model Collision Lab's editor/headless parity regression reaches the goal
with identical state, then restores and replays wall obstruction.

The scene driver prevalidates the entire authored plan before changing its
solver. Composed positive finite scale is baked into model-space triangles;
nonuniform scale is supported, invalid/overflowing/degenerate results fail.
Identity geometry shares its immutable resource. A cache retains at most 64
scaled variants; active colliders retain their resources independently. Preparation
may populate this derived cache even when another entity invalidates the plan.
Unchanged source/properties/scale keeps the registration; changed resources or
scale rebuild once. Translation/rotation uses existing static teleports. Disable,
removal and primitive/model transitions use the same ownership lifecycle.

Checkpoints copy the solver and share immutable geometry. Restoring keeps the
latest host-delivered bindings, like script sources; the next step reconciles
against those resources. Native scene tests exercise dynamic landing, inherited
scale/rotation, invalid updates, lifecycle and ownership transitions. Native
Session tests decode the actual GLB fixture, block character motion, replay the
same checkpoint and retain replacements across restore and failed preparation.
This slice is foundation for Explorer; native/browser/editor project and game
proof remain pending, and no surface earns a completion checkmark from compiling.


#### Convex model scene ownership

`sindri.physics3d.model_convex_collider` uses the static model collider's required
`asset` and optional friction/restitution/layers/sensor fields. It supplies one
volumetric hull to an optional `sindri.physics3d.rigid_body`; without that body it
is static. All four body kinds are supported. Primitive collider pieces may
share this body, while characters, static model collision, voxel collision and
mixed 2D/3D owners are rejected before solver mutation. Moving Z-locked owners
are rejected. Rendering remains independent.

Hosts explicitly call `Session::bind_model_convex_collision(id, &ModelAsset)`
after decoding. CPU preparation expands the selected hierarchy with a 4,096
expanded-vertex budget, including duplicate vertices and repeated mesh instances.
`ModelConvexGeometry3d` computes one hull of the entire hierarchy; it fills gaps
and concavities and performs no decomposition, welding or simplification. A flat
model fails with a typed error. Hull point, volume and mass limits are those of
`ConvexHull3d`; asset preparation retains its other default node/triangle limits.
Failed preparation leaves the previous binding intact.

Positive finite composed nonuniform scale is baked into points. Unit-scale
instances share immutable geometry; at most 64 scaled variants are cached,
with active instances retaining their geometry independently. Source, scale or
collider/body edits rebuild; unchanged frames and transform teleports preserve
velocity. Dynamic hull extent contributes to voxel residency. Disable, removal
and primitive transitions use the shared scene lifecycle. Checkpoints share
immutable hulls and restoring preserves the latest host bindings.

Native scene regressions exercise landing, scaling, inherited placement, mixed
pieces, live velocity, replay, residency extent, invalid scale/material/ownership,
Z-lock and unbinding retention. A Session regression uses the decoded GLB fixture,
reorients a node to make it volumetric, moves and replays its body, and verifies
replacement across restore and rejected planar replacement. Hull rebuild
prevalidation ignores existing registration; insertion still rejects duplicate
ownership.

Shared discovery now retains separate triangle/convex uses for each logical
asset across scenes, expanded prefabs/overrides and inactive entities. Export
includes convex-only references and original verified GLB bytes. The native/browser
player, headless `ProjectRun` and editor Play prepare only authored representations
before gameplay. Editor startup merges unsaved and later-scene uses without losing
either representation, and keeps their assets retained through Play.

`Session::bind_model_collisions` expands CPU geometry once, validates every
requested representation, then replaces bindings. A failed hull preparation leaves
both previous bindings intact, including an asset used for triangles elsewhere.
Shared triangle/convex use inherits the 4,096 expanded-vertex convex budget;
triangle-only assets keep their larger budget. Editor model reload uses this same
atomic operation and retains the previous rendered model on collision failure.
References introduced during Play still require stopping and restarting.

Add Convex Model Collider uses a real project GLB and the model asset picker.
Rigid bodies and primitive pieces may share it; static model/voxel collision,
characters and 2D collision (including tilemaps) are withheld in either order.
Model Collision Lab now has a volumetric GLB prop that falls onto the imported
floor, receives a Decay impulse after landing, and reaches a travel goal. This is
native player/Session, rebuilt browser and native editor feature proof for Explorer;
see the editor review (`docs/model-collision-editor-review.md` in the Sindri repository).
Explorer game adoption remains open. Static triangle
and convex bindings remain independent, even when they name the same asset.


#### Convex-model spawn-window controls

Existing `Physics3d` motion reads, setters and impulses now accept an authored
convex-only body before its first synchronization, and hull/primitive compounds.
`PhysicsWorld3d::validate_body_settings` checks the real authored/composed body
without requiring or inventing primitive geometry. The host also validates
collider schemas, nonempty primitive pieces when present, convex material,
positive finite composed hull scale, dimension/owner conflicts and moving Z locks.
Invalid authoring or input never replaces an earlier valid queued request.

No asset loading or hull construction runs in the scripting host. Geometry,
bounds, volume, scaled mass and pending kind compatibility validate together
before scene solver mutation. Missing/invalid hull bindings fail synchronization
and retain queued controls for a corrected retry. Reads return copied authored
starts/last queued setters; impulses wait for the actual combined mass. Rotation
locks still return zero angular velocity. Successful materialization replays once
in call order, retains authored motion fields and participates in checkpoints.

Five native host regressions cover scaled hull mass/ordered controls/replay,
invalid authoring preserving the last request, kinematic impulse rejection and
rotation locks, mixed-compound mass, and missing binding/corrected retry.
Model Collision Lab now spawns its prop from a typed prefab, queues velocity,
impulse and angular motion immediately, and reports pending-read verification.
A project Session regression checks the first materialization step and checkpoint
replay, then reaches the prop's contact-start impulse/travel goal. Rebuilt Chromium verifies the pending-read message, both fetched GLBs and both
lab goals. Native editor Play/restart reaches the prop goal and Stop removes
the spawned body; see the review (`docs/model-collision-editor-review.md` in the Sindri repository).
Explorer adoption remains open.

---

Source: `docs/character-movement.md`

## Character movement

2D status: geometric sweep/slide, ground probing, grounded snapping, slope
limits, optional steps, synchronized platform carry, one-way controller policy
and scene runtime ownership implemented; 2D acceptance is complete in
`physics-update.md`. Added for the platformer genre showcase, whose Hero script
now uses the controller. Authored low steps and an inclined boardwalk exercise
step selection, walkable ascent and downhill snap. A one-way plank ferry across
the first gap exercises solved translation carry, reversal, boarding, jump/landing
and drop-through. Native editor authoring/undo/save/reopen/Play is exercised in
the editor review (`docs/physics-character-editor-review.md` in the Sindri repository). The exported platformer
reaches its flag through real Chromium keyboard input with five coins and no falls.

### Ownership

`sindri-physics` owns geometric movement and collision results, independently of
scene JSON, Decay, the editor and gameplay. No new crate or dependency is needed.
The engine computes a proposed displacement; gameplay chooses speed, gravity,
jumping and whether to apply that proposal. Future scene/host integration must
use this same primitive rather than implement another collision algorithm.

### Implemented geometric primitive

`PhysicsWorld2d::move_and_slide(shape, pose, displacement, options, filter)` and
`move_and_slide_where(..., include)` are read-only queries. The probe can be a
box, circle or capsule; rotation stays fixed during a move. The caller supplies
world-space displacement, not velocity or a timestep. The result changes no
body pose, velocity, force, joint, contact or event state.

`SlideOptions2d` carries a positive finite `skin` distance in world units
(default 0.01) and an iteration budget in 1..=32 (default 8). Each sweep travels
until the probe reaches the skin around a surface, then removes the untravelled
motion into its outward normal and sweeps the remaining tangent. Rotated
surfaces use world normals; impact contact queries refine skin-cast geometry to
avoid false upward motion from tilted cast normals on flat box faces. Contact
prediction uses the same skin-relative allowance as support probing; when no
contact is available the cast geometry is retained. Small numerical normal error
can still require more than one hit on the same surface. Sub-epsilon remaining
movement is discarded.

`SlideMotion2d` returns actual translation, remaining sliding displacement,
ordered collisions and two outcome flags. Collision distances are relative to
each sweep start, not accumulated path length. Blocked normal movement is
removed, so `remaining` is not the original displacement minus translation.
`iteration_limit_reached` means unapplied tangent movement remains after the
budget. A head-on wall can block all movement without exhausting that budget.

Strict initial penetration blocks the whole move: translation is zero,
`remaining` is the requested displacement and `started_penetrating` is true.
Its collision has zero distance/normal and the probe origin as its point.
This includes zero-motion requests; the caller chooses recovery or respawn.
Exact touching and separation inside the skin permit tangent/escape movement,
but approach is blocked. This slice does not perform depenetration or push the
probe out to the skin when it starts closer than that.

Validation precedes queries: shape dimensions, pose, displacement, destination,
movement length and options must be valid/finite. Overflowing destinations or
lengths fail instead of returning non-finite results. Query filters match the
existing mask, sensor inclusion and whole-entity exclusion contract. The host
predicate can remove inactive entities and must be stable across the call.
Equal-distance contacts prefer entity handle, then original collider-piece
order. Inserts, teleports and removals update the query-only per-piece BVH before
a physics step. Penetration uses probe bounds; skin support inflates those bounds
by contact prediction; sweeps use start/end bounds expanded by target separation.
The exact geometry and one-way policy are unchanged. Previous-platform support
reads only that entity's pieces at their historical poses, so current bounds
cannot discard support that has moved away. Predicates visit spatial candidates
and must not depend on visits to remote entities.

Ordinary ray/overlap/shape-cast contracts are unchanged. In particular, ordinary
shape casts still return a zero normal at zero-distance overlap. The movement
sweep privately requests impact geometry at the skin and ignores tangent or
separating contacts so they cannot hide a blocking obstacle farther ahead.

### Implemented ground classification

`PhysicsWorld2d::probe_ground(shape, pose, options, filter)` and
`probe_ground_where(..., include)` share the movement narrow-phase helpers and
current-pose filtering. They are read-only and cast along negative world-space
up without changing probe rotation.

`GroundOptions2d` defaults to up `[0, 1]`, maximum slope angle pi/4 radians,
maximum travel 0.1 and skin 0.01 world units. Up must be finite and within 0.0001
of unit length; accepted up vectors are normalized before use. The angle must
be finite and within 0..=pi/2, travel finite/non-negative and skin finite/positive.
An overflowing probe destination fails before any query.

`GroundProbe2d` reports the nearest hit, `walkable` classification and initial
penetration. Distance is downward travel to the skin, not to the bare surface.
Steep hits are returned as unwalkable, never skipped in favor of a floor below.
A surface is walkable when its world normal is within the configured angle of
up, with a 0.0001 cosine tolerance for narrow-phase approximation. A nearby
walkable hit does not itself mean the caller is standing on it.

Initial penetration returns the usual zero-normal hit and is never walkable.
Exact touching/inside-skin support can be detected with zero travel: a contact
query, allowing one percent of skin plus one f32 epsilon for numerical error,
precedes the sweep. Rotated/curved sweeps can stop slightly outside the nominal
skin; this tolerance keeps snapped endpoints supported without moving them again.
Prediction is capped at the largest finite float. Blocking contacts at zero
travel tie by entity then original piece order. A surface tangent to the probe direction is not support. No hit
means unwalkable, not penetrating.

This API does not snap, choose jump behavior, carry platforms, limit uphill
movement or implement one-way support/drop-through. The composed grounded
movement API below adds support state and optional snapping; remaining movement
policy belongs to following controller slices. One-way geometry still has the
ordinary two-sided geometric query semantics here.

### Implemented grounded movement and optional snapping

`PhysicsWorld2d::move_and_slide_grounded(shape, pose, displacement, options, filter)`
and `move_and_slide_grounded_where(..., include)` compose the existing slide and
support queries. They remain read-only and do not retain state between calls.
The same filter and stable entity predicate apply to both phases.

`GroundedSlideOptions2d` supplies slide settings, up, maximum slope angle and
non-negative finite `snap_distance` and `step_height`. Both default to zero.
Slide skin is also the support skin, so movement and grounding cannot disagree about that separation. Up and angle use
the ground-probe validation contract. Snap defaults to zero: support is then
recognized only within skin plus its numerical tolerance after sliding. A floor near
the starting pose cannot keep the character grounded after it walks off a ledge.

A positive snap distance allows a downward probe at the slide endpoint. Only
walkable support accepts the downward translation to the skin. A steep surface
remains visible in the ground result but never snaps; it is not skipped for a
floor below. Requested motion with any positive component along up disables
snap and reports ungrounded, even if a ceiling blocks ascent or touching support
is still visible. Initial penetration likewise blocks snapping and grounding.

`GroundedSlideMotion2d.translation` is the full proposal to apply once.
`slide` retains the selected slope-limited sweep result, including its unsatisfied
movement and budget flags. It starts after any carry, otherwise at the supplied pose;
for an accepted step it is the forward sweep from the lifted pose.
`step_translation` is the accepted lift (zero for ordinary movement), and
`snap_translation` is the additional downward landing/snap motion. Their sum
with `slide.translation` and any `platform.motion.translation` is total translation. `ground` is the support query at
the selected slide endpoint before landing/snapping; its distance describes that
extra downward travel. `grounded` means the proposed endpoint
has walkable support and the request was not ascending or initially penetrating.
Snapping is separate from slide collisions and does not alter slide remaining.
An exhausted slide budget still permits support probing at its actual endpoint.

Gameplay keeps prior support state and chooses when to enable snap: typically
while following ground, with zero snap in air or while jumping. Speed, gravity,
jump timing and support transitions remain Decay decisions. The engine does not
supply coyote time, jump buffering or persistent state. Arbitrary up and fixed
probe rotation are supported. Ordinary shape casts and one-way geometry retain
their existing contracts; this is not yet the full controller.

### Implemented slope movement limits

Grounded movement uses `max_slope_angle` for both support and sliding. The
geometric `move_and_slide` query retains its unrestricted tangent projection;
both paths share one sweep implementation. Slope classification shares the same
0.0001 cosine tolerance with ground probing, including the configured boundary.

On an upward-facing, unwalkable contact, the remaining tangent cannot gain rise
beyond the positive upward component of the remaining request. A horizontal or
downward request cannot turn into an uphill climb. An explicit jump can still
slide along the incline, with its rise capped to what was requested. Scaling the
tangent preserves separation from the surface; removing just its upward component
would point back into the slope. Motion blocked by this policy is discarded, like
blocked normal motion, and does not count as exhausted iterations.

Walkable inclines use the geometric projection, which reduces displacement when
approaching a surface; this API does not maintain constant speed along slopes.
Downward sliding on a steep incline remains possible, but the surface cannot grant
grounding or snap. Downhill following of walkable support uses the caller's
optional snap distance; requests that leave its reach become airborne. Vertical
walls and ceilings retain ordinary sliding. No autonomous gravity or downhill
acceleration is applied. Up is world-space and independent of probe rotation.

This general policy is exercised by the platformer's inclined boardwalk through
its real Hero script. Native tests climb and snap down it without jumping, then
lower the slope limit and disable steps to show the same incline blocks walking.
Native editor authoring/undo/save/reopen/Play is exercised in
the editor review (`docs/physics-character-editor-review.md` in the Sindri repository). The exported platformer
reaches its flag through real Chromium keyboard input with five coins and no falls.

### Implemented steps and clearance

Positive `step_height` enables a conservative alternate lift/forward/landing
path. It requires walkable support at the starting pose, no initial penetration,
a non-ascending request and a non-walkable collision opposing its horizontal
component. Horizontal travel must exceed one skin. Unobstructed motion and jumps
retain ordinary sliding without adding a lift.

The full configured lift must be clear at the skin: ceilings and overhangs cannot
redirect it sideways. A second slope-limited sweep moves the horizontal component
from that lifted pose. The candidate must improve horizontal progress by more
than one skin over the ordinary result and finish its forward iteration budget.
A downward probe travels at most the step height, accepting only walkable support;
it never skips a steep surface for the floor below. Final rise cannot exceed the
configured height, and a zero-travel probe confirms non-penetrating support at
the proposed endpoint. Probe rotation stays fixed through every phase.

On acceptance, step lift, forward motion and downward landing replace the
ordinary path. On rejection, the ordinary result is retained exactly. Downward
requested motion is replaced by the landing sweep, so an accepted step follows
support rather than applying gravity displacement again. Landing is independent
of ordinary `snap_distance`, so stepping can work with snap disabled. It does not
retain an airborne step or bridge a gap without reachable support. Each slide
phase has its own bounded iteration budget; an attempted step adds at most a
lift, a forward sweep and support probes to the original query.

The full-height clearance requirement is deliberately conservative: a lower
obstacle beneath a ceiling that blocks the configured lift falls back to ordinary
sliding rather than searching smaller heights. Minimum progress is tied to skin;
very small movement requests do not step. These limitations remain visible for
future game integration. Stepping, like slope limits, is added generally for
platformer adoption. Its two authored 0.25-unit stone risers now exercise accepted
step lifts through the Hero script; disabling steps makes the first riser block.
Intended horizontal speed remains script-owned at obstacles, so clipping one
request does not prevent the next request from advancing over a low step.

### Implemented moving-platform carry

`GroundedSlideOptions2d.platform_support` defaults to `None`. A supplied
`PlatformSupport2d` identifies the previous ground body's runtime entity and its
previous synchronized pose. Before carrying, the engine verifies walkable contact
at that pose with the current local collider shapes/offsets/rotations and the same
filter/predicate. Removed, filtered, unsupported, previously penetrating or steep
support is ignored. Previous pose and character request validate before queries.
Support verification uses skin contact tolerance and authored compound order.

The previous inverse pose maps the probe origin into the support body's local
coordinates. Its current synchronized pose maps that point back into world space;
the difference is carry displacement, including translation and rotation about
the body origin. This displacement is swept first, against all filtered entities
except the carrying body. The character retains its own rotation. Grounded
movement, steps and snapping then run against the whole current world from the
carried pose; the support body is included again. A platform moving upward is not
mistaken for a character jump. Explicit jump requests still lose grounding after
carry. Carry does not add platform velocity or choose jump velocity inheritance.

`GroundedSlideMotion2d.platform` is a `PlatformCarry2d` when old support verifies.
It reports the entity, current pose, requested point displacement and
`motion` clipped by collisions. Carry collisions, penetration and exhausted iterations are
separate from the selected character slide. The actual carry translation is
included once in total translation. Wall clipping may leave support under the
character; a rising platform obstructed by a ceiling can leave initial penetration
in the following slide/ground result. Recovery, crush damage and respawn are
ordinary Decay gameplay decisions; the query never pushes through obstacles.

Hosts capture support from the final grounded hit, synchronize bodies, and supply
its prior pose on the next movement pass. Pending kinematic targets or velocities
produce no carry until the actual physics pose advances. Advance the snapshot on
every pass, even when carry is clipped, so old motion is not retried. Reuse the
returned current pose only if the final ground hit still identifies that entity;
otherwise capture the new support or clear the snapshot when airborne. Clear it
on teleport, parenting changes or collider/body rebuilds. Current local shapes
are used for verification, so the query cannot reconstruct previous geometry
across structural edits.

Do not also add support displacement via transform parenting, solver movement,
velocity or the character's requested displacement. This read-only API does not
store or consume snapshots; submitting the same old pose again can repeat motion.
The scene/host integration must enforce one application per movement pass.

Rotation carry sweeps a straight chord between the old and new point positions;
it does not sweep the circular arc or rotate the probe. Large rotations can miss
obstacles on that arc or leave a non-circular probe intersecting the support.
Use smaller synchronized steps and inspect the returned penetration/support
state. Continuous arc carry and rotating-probe sweeps remain absent and have
explicit parity gaps. The platformer's authored kinematic ferry now proves
translation carry through its real Ferry and Hero scripts: a stationary rider
follows each solved displacement once across the first gap and back, a running
jump boards it from the starting field, jump/landing clears and resumes support,
and drop-through leaves carry and falls into the normal respawn path. Native
regressions assert relative position, carry/total displacement, both directions
and airborne null support. Chromium runs the same game assets with the hero
initially aboard, showing stationary riding, reversal and a keyboard jump.
Rotation/collision-clipped carry still has engine/scene regressions rather than
an authored game encounter. Native editor proof is recorded in
the editor review (`docs/physics-character-editor-review.md` in the Sindri repository).

### Implemented one-way controller policy

Grounded movement respects `OneWay2d` on each enabled solid piece by default.
The policy's normalized local normal follows both body and piece rotation.
Approach must not move along that normal, the contact normal must lie within
its support cone (one f32 epsilon cosine tolerance), and the probe's rear
support point must start on the front support plane within 0.05 world units.
That last allowance matches solver support slop: shallow front penetration
still reports initial penetration; a probe inside/from below passes until it
clears the plane. Movement uses displacement, not the solver's velocity threshold.
Ordinary solids retain strict initial-penetration behavior.

`GroundedSlideOptions2d.drop_through` defaults false. True ignores only one-way
solid pieces during this entire request: slide, initial penetration, support,
snap, step clearance/landing and previous-platform verification/carry sweeps.
A dropped one-way support cannot carry or ground the probe; an ordinary support
can. Sensors retain their ordinary geometric behavior when explicitly included.
Masks, whole-entity exclusion, host predicates and deterministic ties still
apply. A rejected one-way hit cannot hide an ordinary floor farther below.
The public geometric slide/ground/ray/overlap/shape-cast queries remain two-sided.

The query has no body identity or timestep and stores no drop timer. The host
owns duration and cancellation, supplying the flag for each simulation request;
false restores policy immediately. This does not read or modify the separate
dynamic-body `drop_through(entity, seconds)` solver timer. Scene/Decay integration
must choose one movement owner and maintain its timed drop state explicitly.

Native regressions exercise all probe shapes, ascent/descent, underside/deep
penetration, shallow front penetration, cone rejection, piece/body rotation,
snap/drop to an ordinary floor, request cancellation, mixed solid/sensor pieces,
step landing and carry interactions. This remains engine evidence for platformer
adoption, with no editor/Decay/game proof yet.

### Implemented scene ownership

`sindri.physics2d.character` configures skin, iteration budget, up, slope angle,
snap/step distances and `carry_platforms` (default true). Omitted settings use
engine defaults and deserialize through the same validation. Add an ordinary
Collider 2D with exactly one solid box/circle/capsule piece; additional sensors
are allowed. The controller uses that piece's offset, rotation and filter mask,
excludes its own whole entity and ignores inactive entities. Shape dimensions
retain existing world-unit collider semantics; transform scale is not a second
shape-size authoring path. Compound solid probes remain absent.

The scene derives a stationary velocity-kinematic body for the controller.
An authored rigid body on the same entity is a configuration error, as are a
missing transform or an absent/multiple solid probe. Gameplay owns displacement,
velocity, gravity and jumps; solver linear/angular velocity is zeroed before
stepping so it cannot add a second movement. Input and runtime state are held
beside `World`, never in serialized component fields. Saved settings and unknown
payload fields survive reopening; pending motion, support handles, drop timers
and result caches do not.

`ScenePhysics2d::character_requests()` returns `CharacterRequests2d`:

- `move_character(entity, displacement, snap)` queues world displacement for the
  next fixed update; a later request replaces it. `snap` permits authored snap
  for this request, while upward displacement always suppresses it.
- `drop_through(entity, seconds)` replaces the controller's remaining simulation
  duration; zero cancels. Any positive remainder covers the entire movement
  pass and decrements afterward. Pause retains requests/timers; spawning starts
  them when synchronized. Nonfinite, overflowing or negative request values are rejected
  before replacing valid input. Missing, inactive and non-controller requests
  are discarded at synchronization. An invalid timestep consumes nothing.

No-request frames use zero displacement and enable snap only if the last result
was grounded. Carry and support queries still run each fixed step. Settings,
parenting, collider/body rebuilds and actor teleports invalidate cached state;
support rebuilds/teleports also clear riders' old snapshots. Fresh support is
probed at zero travel before the solve, so a newly spawned/rebuilt actor can ride
that step's moving platform without inheriting older motion. Drop requests are
consumed after synchronization, keeping same-step input valid across rebuilds.

The scene then advances the physics solve, runs controllers against those current
poses in entity order, applies total motion once to each controller's backend
body and writes world poses into local scene transforms. Z, scale and the probe's
fixed rotation use the existing physics writeback contract. Final walkable
support captures the current platform pose for the next pass; departures/jumps
clear it. Parenting to a simulated support does not add a second carry.

`character_motion(entity)` exposes the last applied `GroundedSlideMotion2d`.
`for_scripts_with_characters()` returns disjoint physics/event/request/result
borrows; `CharacterMotions2d::get(entity)` reads a shared cached result. Scripts
must check activity when reading during a pass, just as with other snapshots.
Existing `for_scripts()` remains available for hosts without controller APIs.
The shared game runtime, editor Play and platformer test host now supply these
borrows to Decay alongside the existing physics context. Physics-only hosts
retain their independent driver and existing APIs.

**Solver timing limit:** controller motion occurs after the solve so carry uses
actual current platform motion. Queries and render transforms see the applied
pose immediately; solid response and discrete sensor enter/exit processing see
it at the following solve. Moving a controller invalidates its prior solved
contact snapshot, so grounding must use controller results. Controllers do not
apply physical push impulses or swept trigger events. A fast controller can
cross a small sensor between solves without entering it. These limits are
explicit parity gaps; this is not a same-step solver-response implementation.

### Typed Decay requests and motion snapshots

`Physics.move_character(entity, displacement, snap)` queues world-space `Vec2`
displacement for the next fixed scene step; later input replaces earlier input.
The actor must be active, have valid Character 2D settings and a transform, and
have no competing authored rigid body. Probe assembly (including tilemap pieces)
is validated by scene synchronization before queued movement applies. Finite
components and finite displacement length are required; invalid arguments leave
pending input intact. Calls before a spawned actor's first synchronization are
valid. A host without scene controller context reports an error.

`Physics.character_motion(entity)` returns a copied optional `CharacterMotion2d`
from the last completed fixed pass. Check for null before reading: there is no
result before first movement, for an inactive actor or without its character
component. Stale entity handles are errors. All scripts in a pass read the same
cached results while queuing future input. Settings, parenting and teleports
invalidate caches at synchronization, not by changing an already returned copy.

The snapshot contains total `translation`, selected `slide_translation`,
`remaining`, `step_translation`, `snap_translation`, `grounded`, optional
`ground` (`RayHit2d`), `ground_walkable`, `ground_started_penetrating`, slide
`started_penetrating`/`iteration_limit_reached` and ordered slide `collisions`.
Carry has nullable `platform`, `carry_requested`, `carry_translation`, ordered
`carry_collisions` and separate `carry_started_penetrating`/
`carry_iteration_limit_reached`. Without carry its vectors, lists and flags are
zero/empty/false. Hits copy world point, normal and phase-relative distance.
Inactive/despawned hit/platform references are filtered during each call;
filtered ground also clears grounded/walkable in that copy. Applied displacement
and other flags remain historical. Editing fields, nested hits or lists cannot
change the engine cache or future movement.

`Physics.drop_through(entity, seconds)` dispatches authored characters to the
scene-controller timer; dynamic bodies retain the existing solver timer and
physics-only host compatibility. A finite non-negative duration replaces pending
time, zero cancels and invalid values preserve the queue. Timers begin at the
next valid fixed pass, cover its whole movement and decrement afterward; pause
retains them. Controller drop suppresses only one-way solids across slide,
support, snap, steps and carry. Neither request changes saved component settings.

Native regressions exercise previous-pass reads, input replacement, copies,
carry/support filtering, invalid input and controller timer dispatch. A shared
session regression exercises the same context the exported game uses. A rebuilt
Chromium export at `/examples/character-api/` runs that fixture script, observes
queued movement and grounded results, verifies snapshot edits leave the cache
intact and renders the controller above its solid floor. This API
is added generally for platformer adoption. Its Hero script now integrates
gravity and velocity, queues movement and grounds from cached support. One
ground-filtered solid capsule supplies the probe, while separate body/foot
sensors preserve pickups. Native runs reach the flag with coins and no falls,
traverse/drop through one-way planks, and check variable jump height, braking
and respawn. The dynamic crate retains CCD and contact-impulse proof.

### Browser goal proof

The CI browser job copies the shipped platformer with
`scripts/browser/prepare-platformer.py`, exports it through `sindri-export`, then
runs `scripts/browser/smoke.mjs` with `SINDRI_PLATFORMER_GOAL=1`,
`SINDRI_EXPECT_ASSETS=1` and `SINDRI_BASE_PATH=/examples/platformer/`.
The copy adds only `tests/browser_goal.decay`, a read-only observer. The normal
starting pose, terrain and gameplay scripts remain intact. The observer reports
completed movement, coins, falls, win state and held jump input; Playwright sends
ordinary ArrowRight/Space events using the native goal test's terrain decisions.
It waits for gameplay to observe a jump release before pressing again, so two
input edges cannot collapse into one browser frame.

The goal gate requires at least three coins, no falls and the flag within a
90-second wall-clock budget. It also retains the smoke checks for fetched assets,
WebGPU, rendered pixels and script/GPU errors, and captures the win screen.
Local repeated Chromium runs reached the flag with five coins and eight jumps;
no game-state writes or browser-only gameplay rules were added.

### Remaining limitations

The 2D controller slice is exercised in the engine, native editor, typed Decay
hosts and the platformer on native and browser targets. Gravity, coyote time,
jump buffering and player input remain Decay policy. Compound solid probes,
same-step solver response, swept trigger events and curved/rotating-probe carry
remain explicit gaps in parity. The 3D scene controller below is a foundation;
Scene platform carry and typed Decay access are covered below; browser, editor
interaction and Explorer game proof remain pending.

### Foundation evidence

Native tests cover all probe shapes in empty space, zero motion, wall tangents,
skin, escape/approach at touching surfaces, initial overlap, bounded corner
iterations, rotated surfaces, filter/predicate behavior, immediate teleports and
removal, deterministic ties, one-way geometry's two-sided query behavior and
invalid input rejection. These geometric tests complement the typed host and
platformer gameplay and native editor evidence above.

Ground-probe tests cover separation/touching and zero travel, box/circle/rotated
capsule extents, slope boundaries and steep obstruction, arbitrary up, ceiling
orientation, initial penetration, masks/sensors/whole-entity exclusion, host
predicates, current poses/removal, deterministic ties and invalid options.

Grounded movement tests cover contact-only support, snapping on/off and distance
limits, leaving a ledge, landing and repeated grounded movement, jump and blocked
ceiling requests, descending walkable slopes, steep rejection, initial overlap,
arbitrary up with a rotated capsule, shared filters/predicates, immediate pose
changes/removal, budget exhaustion, read-only results and invalid/overflowing
inputs. Zero-travel support tests bound the numerical skin allowance.

Slope movement tests cover walkable and steep ascent, the configured angle
boundary and changed limits, explicit jump rise, negative vertical requests,
steep descent and walkable downhill snap, walls/ceilings, mirrored capsule and
rotated box/up, filtering, read-only state and unchanged geometric sliding.

Step tests cover opt-in low obstacles, the height boundary, tall walls, ceilings
and forward overhangs, airborne/jump/overlap rejection, no or steep landing,
box/circle/capsule extents, rotated up/probe, filters/predicates, exact fallback,
read-only state, motion accounting and invalid heights. A repeated flat-floor
regression covers contact-normal refinement for all three probe shapes.

Platform tests cover translation for all probe shapes, upward/downward support,
rotation-point displacement, snapshot advancement, wall clipping and ceiling
crush reporting, jumps/ledges, filters/sensors/host predicates/removal, stale or
steep support, compound offsets/rotation, carry plus step accounting, invalid
input and actual position/velocity-kinematic simulation ordering.

### 3D character movement foundation

Added generally for the planned Explorer genre showcase in PR #507. It is an
engine capability with scene ownership and typed Decay access described below.
Explorer adoption and full surface proof remain pending.
It uses Rapier's kinematic character controller behind Sindri-owned types;
2D movement and its platformer contract remain unchanged.

`PhysicsWorld3d::move_character(shape, pose, displacement, options, filter)` and
`move_character_where(..., include)` return a copied `CharacterMotion3d` without
changing any body pose, velocity, event state or scene. Shapes are the existing
box, sphere and Y-axis capsule probes (including a zero-height query capsule).
The world-space displacement is not velocity; probe orientation stays fixed.
The predicate is stable for the call and may run repeatedly across movement,
step and support probes. Membership masks, sensor policy and whole-entity
exclusion use the existing `RaycastFilter3d` contract, independently of the
obstacle's physical pair filter. Exact ties and traversal order remain
backend-owned, unlike the deterministic ordinary Sindri query tie policy.

The existing query-only per-piece BVH supplies Rapier's query pipeline. Its
collider world poses and bounds update at insertion/teleport, after solving and
when static groups change, so movement sees the same current geometry as rays
without waiting for a physics step or rebuilding a second spatial index.

`CharacterOptions3d` uses absolute world-unit distances: positive `skin`
(default 0.01), optional `snap_distance`/`step_height` (both default zero),
`step_min_width` (0.1) and opt-in `step_dynamic_bodies` (false). It also carries
`slide` (true), unit world-space `up` ([0, 1, 0]), `max_slope_angle` and
`min_slide_angle` (both pi/4 radians). Angles accept 0..=pi/2; distances must be
finite/non-negative and skin positive. Up must be within 0.0001 of unit norm
and normalizes before use. Scene integration must decide when to enable steps
and snap; gravity, jumps and acceleration belong in game scripts.

The result contains total `translation`, `grounded`, `sliding_down_slope` and
ordered movement-phase `CharacterCollision3d` values. A collision copies a
`ShapeHit3d` (entity, world point/normal, distance from that sweep's start), plus
`translation_applied` and `translation_remaining` at impact. Internal stair,
snap and support probes do not populate that collision list. Editing a copied
result cannot affect later movement.

Backend limits are explicit: `grounded` means Rapier found a nearby upward-facing
contact, not that it passed the configured walkable slope test. Prediction
includes skin plus 0.05 world units. `sliding_down_slope` preserves Rapier's flag,
which may also be set during uphill slope handling. Rapier's movement loop has
its own fixed 20-iteration budget, without a public exhaustion flag. Approximate
curved shape-cast normals can
introduce small normal nudges or lateral travel loss, including on flat faces;
native slope/capsule checks use explicit 0.01–0.02 world-unit tolerances.
Stationary near-zero requests can propose bounded depenetration (four passes,
total at most one quarter of the backend's local-bounds up extent); moving
penetration uses Rapier's shape-cast policy.
There is no Sindri penetration flag or recovery policy yet. Sensor-inclusive
queries can block movement, but Rapier's stationary correction ignores sensors.
Steps/snap and these flags need scene/game policy before they become gameplay
contracts. This foundation supplies no push impulses, swept trigger events or
rotating probe casts.

A displacement query passes zero timestep to the backend, disabling automatic
velocity-based platform friction/carry. Future scene ownership must add solved
translation/rotation-point carry exactly once and invalidate support when its
owner rebuilds, teleports or disappears. No moving-platform feature is claimed.

Shape, pose, settings, displacement, destination and expanded probe bounds
validate before queries. Non-finite copied backend results fail explicitly.
If any obstacle has overflowing spatial bounds, this controller query fails
instead of silently dropping the ordinary index's conservative fallback pieces.
Ordinary Sindri queries retain their existing overflow fallback behavior.

Native regressions cover free XYZ movement for every shape, wall slide/stop,
solid landing and stationary support, snap on/off and upward suppression,
low/tall steps and headroom, slope limits, rotated capsules/arbitrary up,
mask/sensor/exclusion/predicate filtering, immediate offset teleports and handle
reuse, copied/read-only state, cloned solve replay, no implicit platform carry,
bounded stationary correction and invalid/extreme input. All 206 physics tests,
26 existing 3D scene regressions, warning-denied checks/Clippy and Rust 1.95 WASM
checks/build pass. The rebuilt Orbit Lab passes its real WebGPU Chromium
capture/denial/goal regression with an inspected screenshot, checking existing
scene behavior after pose synchronization changed. The new movement query is
not yet exercised in a browser. Scene carry and typed Decay proof are described
below; editor interaction and Explorer goal proof remain pending.

### Classified 3D ground queries

`PhysicsWorld3d::probe_ground(shape, pose, options, filter)` and
`probe_ground_where(..., include)` return copied `GroundProbe3d` values without
moving bodies, snapping, changing velocity or choosing gameplay support.
This is a general prerequisite for Explorer's moving platforms, used by the
scene carry and typed Decay result surface described below.

`GroundOptions3d` defaults to world up [0, 1, 0], slope limit pi/4, travel 0.1
and skin 0.01. Up uses the movement query's squared-unit-norm tolerance of
0.0001 and normalizes before use. Slope accepts 0..=pi/2, travel must be finite
and non-negative, and skin finite and positive. The fixed-orientation box,
sphere or capsule travels downward to the skin, rather than physical contact.
Zero travel can classify touching or existing skin contacts, allowing numerical
slack of one percent of skin plus `f32::EPSILON`.

The hit copies obstacle entity, world point/normal and travel distance. The
nearest surface is retained even when too steep; the query does not look through
it for a walkable floor. Exact distance ties use entity-handle order, then piece
order within an entity. A normal must face up and its normalized alignment plus
1e-6 must meet the cosine of the slope limit. Horizontal walls and ceilings
remain unwalkable even at pi/2. Any initial contact deeper than `f32::EPSILON`
blocks support: `started_penetrating` is true and the first penetrating entity
in stable traversal supplies a zero-distance, zero-normal hit at the probe
origin. This flag is independent of the movement wrapper's raw Rapier flags.

The same current-pose per-piece index, masks, sensor policy and whole-entity
exclusion apply. The optional predicate runs once per spatial candidate entity;
all its pieces reuse that answer. Invalid shapes, poses, settings, overflowing
probe bounds/extents or nonfinite selected hit geometry fail explicitly. Any
unbounded obstacle fails the query even when excluded, matching the movement
wrapper's conservative validation rather than ordinary queries' fallback.

Native regressions cover every probe shape, skin and touching contacts, travel,
steep nearest support, penetration, arbitrary up, rotated compound offsets,
filters, predicate reuse, ties, immediate teleports/removal, copies/clone replay
and invalid/extreme input. They distinguish Rapier's broad grounded prediction
from classified support and verify support at a movement query's landed endpoint.
Scene and typed Decay result proof are described below. Browser, editor
interaction and Explorer proof of this primitive remain pending.

### Read-only 3D platform carry

`PhysicsWorld3d::move_character_grounded` and `move_character_grounded_where`
compose opt-in platform carry, the existing Rapier movement query and classified
zero-travel support at the final endpoint. `GroundedCharacterOptions3d` contains
`movement: CharacterOptions3d` and nullable `platform_support`. This is general
Explorer foundation work; scene ownership is described below.

`PlatformSupport3d` names a runtime entity and its previous synchronized pose.
Before carrying, current compound geometry is checked at that previous body pose,
including local offsets/rotations, masks, sensor policy and the host predicate.
Previous bounds may lie outside the current spatial index, so verification visits
only that body's pieces directly without rebuilding or cloning the world.
Missing, excluded, filtered, detached, steep or initially penetrating support
produces no carry. Invalid previous poses fail even for missing/excluded supports.
Unbounded current obstacles and overflowing previous support bounds fail explicitly.

The previous inverse pose maps the probe origin into support-local coordinates;
the current solved pose maps it back into world space. Their difference is the
requested translation, including rotation about the body origin. Pending targets
produce no carry until the actual pose advances. Carry sweeps a straight chord
with fixed probe orientation and sliding, steps and snap disabled, excluding only
the carrying entity. The ordinary movement query then runs from the carried pose
with that entity included again. A zero carry request skips its sweep and returns
zero translation, false raw flags and no hits, avoiding a second stationary
correction. Nonzero carry retains Rapier's movement/correction limits.

`GroundedCharacterMotion3d.translation` includes actual carry exactly once.
`movement` contains the separate raw backend result after carry. `platform`, when
verified, copies entity, current pose, requested displacement and collision-limited
`motion`, including its separate hits. `ground` copies classified final support.
The composed `grounded` flag requires walkable final support and no upward input
request; it does not inherit the raw backend flag. Rising/falling support motion
is not an input jump. Jump requests execute after carry; no platform velocity
inheritance, impulses, crush damage or recovery gameplay is introduced.

This query does not consume snapshots or apply movement. Hosts must apply total
translation once and advance snapshots even when carry is clipped. Retain/capture
support only from the final classified grounded hit. Clear it on teleport,
reparenting, rebuild, inactivity, removal and handle reuse, and do not also add
support motion through parenting, solver velocity or requested displacement.
Scene integration accounts for input residency before solving and actual solved
carry residency before querying movement, as described below.

Large rotations can miss obstacles along the circular arc, and a rising support
clipped by a ceiling may leave penetration visible in the final ground probe.
The probe does not rotate with the support. Rapier's bounded correction and
unreported iteration exhaustion remain limits; there is no continuous arc sweep,
rotating probe sweep or crush/recovery policy.

Eight native regressions cover pending versus solved poses, translation/vertical
motion, rotation, wall clipping, restored support collision, input jumps, masks,
predicates, sensors, missing/detached/steep/penetrating support, compound offsets
outside current bounds, clone/copy stability, raw versus classified grounding and
invalid/extreme input. Scene and typed Decay proof are described below; editor
interaction, browser and game proof remain pending.

### 3D scene ownership

`sindri.physics3d.character` authors the flat `CharacterOptions3d` fields above
and `carry_platforms` (default true).
Defaults and edited payloads use the same validation. A controller requires a
transform and Collider 3D with exactly one solid box/sphere/capsule; extra sensors
are allowed. It uses the composed-scale solid piece, including its local offset,
rotation and filter mask, excludes its whole entity and ignores inactive obstacles.
Authored rigid bodies, mixed 2D/3D ownership and moving Z locks are rejected.
The scene derives a stationary velocity-kinematic body. Gameplay still supplies
input displacement, including gravity and jumps; solver velocities are zeroed before
solving so they cannot become a second movement owner.

`ScenePhysics3d::character_requests()` returns `CharacterRequests3d`.
`move_character(entity, displacement, snap)` queues world-space XYZ travel for the
next fixed pass. The last valid request wins; nonfinite vectors and overflowing
lengths are rejected before replacing input. Missing/inactive/non-controller
requests are discarded at synchronization. Pause retains input; an invalid
step or authored batch consumes none. A body rebuild or teleport preserves valid
same-step input. No-request frames query zero displacement with snap disabled:
gameplay must explicitly permit snap each step. Raw backend grounded status does
not automatically authorize snapping or become a walkable-support decision.

Controller settings and scaled geometry validate beside the ordinary body batch
before lifecycle changes. Voxel residency includes each character's geometry,
requested travel and skin/snap/step margins even without dynamic bodies. An
oversized input residency window fails before any body changes. Characters are
held stationary during the solve. Enabled carry captures zero-travel walkable
support before the solve for fresh or previously classified grounded characters;
raw Rapier flags never seed it. Other character controllers cannot supply carry
support in this slice. After solving, actual support translation/rotation-point
travel expands the same voxel residency, retaining the original input/solver
window, before any character movement query. Unchanged support poses skip this
extra plan. This uses actual solved travel rather than predicted platform velocity.
Characters are then queried against current solved poses in entity order. Each
successful result applies once; parent-space writeback retains
rotation and scale. Queries see the applied pose immediately. Physical response
and discrete sensor events observe it at the following solve, with no push
impulses or swept trigger events. Stationary bounded correction and all backend
flag limits from the query contract still apply.

`character_motion(entity)` returns the last applied `GroundedCharacterMotion3d`:
one total translation, separate `movement` and nullable `platform` carry hits,
classified `ground` and `grounded`. The raw backend flags remain under `movement`.
Carry executes before input movement and advances from the current pre-solve
snapshot each pass, even when clipped. A jump leaves classified grounding and
prevents support capture on the following pass. Setting `carry_platforms` false
uses ordinary movement/classification without carry. Parent writeback applies
world carry once, retaining the rider's world orientation and authored scale;
parenting does not add a second displacement.
`for_scripts_with_characters()` supplies disjoint solver/event/request/result
borrows with `CharacterMotions3d::get(entity)`. Results are immutable snapshots;
activity must still be checked while reading during a script pass. Settings,
parenting, scaled geometry, body rebuilds and teleports invalidate caches during
synchronization; removal/inactivity discard them. Support-owner changes also
invalidate dependent cached rider results. Each pre-solve pass clears transient
support snapshots and verifies current contact, so stale travel is never retried.
Cloning the driver or a Session
checkpoint includes pending input and cached results, without changing saved
component payloads. `Session::character_requests3d()` exposes the same queue to
Rust hosts and Decay through the scene context described below.

As with backend solver failures, an actual-carry residency failure or movement-query
error after solving is not a transactional rollback of the fixed pass. Carry-window
budget/geometry failure leaves all riders unmoved and requests queued, although
the solver and its event state have already advanced. The next successful pass
captures fresh current support rather than retrying old carry. Requests already
applied successfully
are consumed; a failed request and later requests remain queued. Hosts must report
the error rather than treating an incomplete pass as a successful gameplay frame.

Native scene regressions cover request replacement, invalid input/batch/step,
cloned replay, no solver gravity or velocity, masks/sensors, scaled offset probes
under rotated parents, lifecycle/rebuilds, current solved platform poses,
immediate query visibility and far-away voxel residency/budget rejection. A
shared Session regression verifies once-only movement and checkpoint replay.
Additional native regressions cover fresh boarding, solved translation/reversal,
vertical travel, jump departure, parented rotation without double carry, wall
clipping without replay, lifecycle invalidation, carry opt-out and flat authoring,
raw-ground rejection, terrain expansion and post-solve budget failure/input
retention. Session exercises first-solve carry and checkpoint replay through the
shared runtime. Stationary rotated support returns exactly zero carry rather than
accumulating inverse/forward transform rounding. Typed Decay access is covered
below; controller browser verification, editor interaction and Explorer adoption
remain pending.

Scoped physics/runtime/scene preflight, warning-denied Clippy (also editor and
capabilities), catalogue/generated-document checks and Rust 1.95 scene/runtime
WASM checks pass. The rebuilt generic browser player passes Orbit Camera Lab's
real WebGPU Chromium capture/denial/goal regression with an inspected screenshot.
That verifies existing host behavior after this change; the new controller's
browser/editor interaction and in-tree game proof remain open.

### Typed 3D Decay requests and results

`Physics3d.move_character(entity, displacement: Vec3, snap: bool)` queues world
XYZ displacement for the next fixed step. The last valid request wins, including
calls before first synchronization; invalid vectors, settings, inactive targets,
missing transforms, competing bodies/controllers or absent scene context fail
without replacing it. Each component must be finite and fit f32, and the vector
length must fit the movement backend. Scaled single-solid probe assembly and
terrain budgets remain synchronization checks. Snap enables only the authored
snap distance; gameplay supplies gravity, jump velocity and its snap decision.

`Physics3d.character_motion(entity)` returns a copied `CharacterMotion3d?` from
the last completed controller step, or null before a result exists or when the
target is inactive or no longer a controller. A stale actor handle is a host error,
as with other entity-taking physics calls. A physics-only host without scene
character context rejects both calls. Every shared Session supplies independent
2D/3D request queues and borrowed results to each script pass; reads never drain
results and scripts run after physics, so queued movement applies next step.

The copied records follow the language's ordinary mutable value semantics:

| Record | Fields |
| --- | --- |
| `CharacterMotion3d` | `translation: Vec3`, `movement: CharacterMovement3d`, `grounded: bool`, `ground: RayHit3d?`, `ground_walkable: bool`, `ground_started_penetrating: bool`, `platform: CharacterCarry3d?` |
| `CharacterMovement3d` | `translation: Vec3`, `grounded: bool`, `sliding_down_slope: bool`, `collisions: List<CharacterCollision3d>` |
| `CharacterCollision3d` | `hit: RayHit3d`, `translation_applied: Vec3`, `translation_remaining: Vec3` |
| `CharacterCarry3d` | `entity: Entity`, `requested: Vec3`, `motion: CharacterMovement3d` |

Top-level translation includes actual carry exactly once. Movement excludes
carry; `platform.motion` reports its separate phase. Top-level grounding uses
classified walkable endpoint support, while `movement.grounded` and
`movement.sliding_down_slope` preserve the documented raw Rapier limitations.
No unavailable iteration-limit, penetration or separate step/snap flag is
invented for raw movement. The endpoint ground probe reports its own penetration.
Inactive or removed ground/collision entities are filtered; platform becomes
null when its entity is inactive or removed. Historical total translation and
raw backend flags stay unchanged. Editing any nested record cannot change the
cached result, simulation or input queue. Platform quaternions are not exposed:
Decay currently has no quaternion value type.

Six native host/script regressions exercise startup queueing, last-valid input,
invalid values/context/ownership, typed support/carry, collision filtering and
nested copy mutation and raw versus classified grounding. A shared Session regression observes next-step movement
and checkpoint replay through a real Decay script. Compiler/LSP checks exercise
signatures, optional nested result fields, completion and hover. This general
capability is added for the planned Explorer showcase; controller browser,
editor interaction and Explorer goal proof remain pending.

### 3D controller feature proof

Character Lab (`examples/character3d/README.md` in the Sindri repository) is a small project added for
the planned Explorer capability, with a gray floor, gold ferry and goal, blue
box controller and perspective follow camera. Decay owns gravity, keyboard
movement, jump permission, platform reversal and goal rules. Native Session
tests open/compile the project, verify idle carry through reversal without
double translation, and use Space/D input to jump, release airborne carry,
land on the floor and reach the goal. The jump step consumes final pre-solve
support carry; the following airborne step has none.

The exported shared WebGPU player runs the same scripts/assets in Chromium.
`scripts/browser/character3d.mjs` uses ordinary keyboard input and waits for
carry, jump, floor landing and goal milestones; it also verifies fetched assets,
changed rendered pixels and absence of GPU/runtime errors. CI runs the check
and uploads its goal capture. This is feature-example proof. Native editor
controller interaction is now recorded in
the review (`docs/physics-character3d-editor-review.md` in the Sindri repository); Explorer game adoption remains
pending. Primitive engine tests remain the evidence for slopes, steps and
collision edge cases.

### 3D editor footing guides

The existing selected-character orange overlay draws 3D slope rays and a ring,
a step-height ring when positive, and a snap-distance ring when positive. Its
foot is the collider center projected down by the primitive's exact extent
along world-space `up`. The scene controller plan supplies validated settings
and the same scaled solid probe used by movement; sensors do not set its foot.
Box, sphere and Y-capsule projections account for composed parent/entity scale,
rotation and collider offsets. Step/snap distances stay in world units.

These are authoring guides, not predicted support contacts, accepted steps or
snap results. The ring width uses the larger extent on two perpendicular axes
in the footing plane. Invalid controller plans hide all 3D footing guides,
matching the runtime's batch rejection; non-finite derived points are omitted.
The existing selection filter hides footing on unselected entities.
