# What works today, and what does not

Sindri AI knowledge pack, file `11-what-works.md`. The capability inventory: for each feature, whether the engine, editor and Decay support it, and its known limitations. Check here before building on a feature.

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

- **Engine core** (from `docs/capabilities/core.md`): Lifecycle and time; World and entities; Scenes; Commands and undo; Not yet
- **Effects, saves and randomness** (from `docs/capabilities/runtime.md`): Effects; Game saves; Randomness; Not yet
- **Games, labs and tooling** (from `docs/capabilities/projects.md`): Games; Labs and feature examples; Tooling
- **Assets** (from `docs/capabilities/assets.md`): Loading
- **Decay** (from `docs/capabilities/decay.md`): Scripts and the host; Language; Tooling; Not yet; 3D transform values
- **Input and audio** (from `docs/capabilities/input-and-audio.md`): Input; Audio; Not yet; 3D foundation
- **Physics** (from `docs/capabilities/physics.md`): 2D bodies and colliders; 2D character controller; 2D queries; 2D joints; 3D; 3D physics foundation; Not yet; 3D foundation
- **Hosts and rendering** (from `docs/capabilities/rendering.md`): Hosts and GPU; Frame and scene extraction; Sprites and shapes; Sprite animation; Text; 2D lighting; Tilemaps and tile volumes; World presentation; 3D models; Not yet; 3D foundation; 3D rendering
- **Screen UI and Weave** (from `docs/capabilities/ui.md`): Screen UI; Weave; Not yet
- **Grids and voxels** (from `docs/capabilities/grids-and-voxels.md`): Grid geometry; Voxel world; Not yet

---

Source: `docs/capabilities/core.md`

## Engine core

The engine loop, the world and its entities, prefabs, scenes on disk, and the
command layer every edit goes through.

Each entry links its tests and the projects that exercise it. Surface marks match the row in parity (`docs/parity.md` in the Sindri repository) where one is named: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — not applicable.

| Capability | Engine | Editor | Decay | Proof |
| --- | :-: | :-: | :-: | :-: |
| **Lifecycle and time** | | | | |
| Engine lifecycle and fixed-step time | ✅ | — | — | ✅ |
| **World and entities** | | | | |
| Entities and hierarchy | ✅ | ✅ | ✅ | ✅ |
| Child transforms follow their parent | ✅ | ✅ | ✅ | ✅ |
| Prefabs | ✅ | ✅ | ✅ | ✅ |
| Nested prefabs | ✅ | ✅ | ✅ | ✅ |
| **Scenes** | | | | |
| Scene save / load | ✅ | ✅ | — | ✅ |
| **Commands and undo** | | | | |
| Undo / redo | ✅ | ✅ | — | — |
| **Not yet** | | | | |
| Extensible system scheduler | ❌ | — | — | — |

### Lifecycle and time

<a id="lifecycle-and-time"></a>

#### Engine lifecycle and fixed-step time

Enforced lifecycle transitions, capped frame time, bounded fixed-step catch-up and rational time scale.

Engine ✅ · Editor — · Decay — · Proof ✅

**Tested by:** `crates/sindri-core/src/lifecycle.rs` (`crates/sindri-core/src/lifecycle.rs` in the Sindri repository), `crates/sindri-core/src/engine.rs` (`crates/sindri-core/src/engine.rs` in the Sindri repository), `crates/sindri-platform/src/clock.rs` (`crates/sindri-platform/src/clock.rs` in the Sindri repository), `crates/sindri-platform/tests/game_loop.rs` (`crates/sindri-platform/tests/game_loop.rs` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Legal state transitions are enforced rather than assumed: an engine can be
created, started, paused, resumed, and stopped, and an illegal transition is a
`LifecycleError` rather than a silently ignored call. Frame time is capped so a
long stall cannot produce one enormous step, and fixed-step accumulation runs
simulation at a steady rate with a bound on catch-up steps that prevents a
spiral of death. Time scale is rational rather than floating-point, so slow
motion does not accumulate drift. `ManualClock` drives the whole loop from a
test with no window and no sleeping.

</details>

### World and entities

<a id="entities-and-hierarchy"></a>

#### Entities and hierarchy

Generation-checked entity handles, safe spawning and destruction, and reparenting with cycle prevention.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Entities and hierarchy*

**Tested by:** `crates/sindri-core/src/world/tests/mod.rs` (`crates/sindri-core/src/world/tests/mod.rs` in the Sindri repository), `crates/sindri-core/src/world/tests/hierarchy.rs` (`crates/sindri-core/src/world/tests/hierarchy.rs` in the Sindri repository)

**See:** `docs/2d-model.md` (in `06-2d.md`), `docs/entity-scaling.md` (`docs/entity-scaling.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Entities are generation-checked slot handles, so a handle to a despawned entity
is detected rather than silently addressing whatever took its place. Spawning,
access, recursive destruction, and slot reuse are all safe. Hierarchies support
reparenting with cycle prevention, and `World::check_set_parent` answers whether
a move is legal without making it. Each entity carries a name, a parent,
children, an optional `Transform3D`, arbitrary JSON components, and an
editor-only section the runtime never interprets. There is one transform, with
2D-shaped accessors that read and write X and Y and cannot express a change to
Z, and a Z lock a transform can declare that the command layer refuses to write
past: a 2D entity is one that keeps to a plane, not one with a different
transform type — see `docs/2d-model.md`.

Entity storage was measured at 1k, 10k, and 100k entities before considering an
archetype ECS; `docs/entity-scaling.md` records why one is not warranted.

</details>

<a id="child-transforms"></a>

#### Child transforms follow their parent

A child's transform is local to its parent everywhere something is placed in the world.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Child transforms follow their parent*

**Tested by:** `crates/sindri-core/src/world/space_tests.rs` (`crates/sindri-core/src/world/space_tests.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_reads_where_a_child_is.rs` (`crates/sindri-decay/tests/a_script_reads_where_a_child_is.rs` in the Sindri repository)

**Exercised in:** `games/scorchball` (`games/scorchball` in the Sindri repository), `games/low-tide` (`games/low-tide` in the Sindri repository)

**Limits:**

- Scale composes per axis, which is exact unless a rotated parent is scaled unevenly.

<details markdown="1">
<summary>Evidence</summary>

Moving, turning or growing a parent carries its children, and
`World::world_transform` is where an entity ends up with every parent folded
in. Everything that places something in the world reads that: sprites, shapes,
meshes, tilemaps, tile volumes, voxel worlds, cameras and their follow targets,
lights, grid placement, particle bursts, and 2D physics, which starts a child
body at its world pose and writes its answer back as the local transform that
puts it there. The editor draws and drags a child's handles in the world and
stores the local result, and moving an entity to a new parent in the hierarchy
keeps it where it was on screen. Scripts' `transform.position` is the stored
local value and `world_position` the world one; `World.set_parent` keeps the
local transform. Screen UI keeps its own layout, which already placed an
element inside its parent's box.

</details>

<a id="prefabs"></a>

#### Prefabs

Single-root reusable entity definitions in the scene document shape, spawned whole or not at all.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Prefabs*

**Tested by:** `crates/sindri-core/src/prefab/tests.rs` (`crates/sindri-core/src/prefab/tests.rs` in the Sindri repository), `crates/sindri-core/src/world/tests/prefab_identity.rs` (`crates/sindri-core/src/world/tests/prefab_identity.rs` in the Sindri repository), `games/platformer/tests/coins_are_one_prefab.rs` (`games/platformer/tests/coins_are_one_prefab.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/prefabs.md` (in `05-projects-scenes-prefabs.md`)

**Limits:**

- An entity spawned at runtime is not linked to its prefab, by design; only a scene's instances are.

<details markdown="1">
<summary>Evidence</summary>

A **prefab** is an authored reusable entity definition: a single-root scene
fragment in the same document shape a scene uses, sharing its entity rules,
component payloads, versioning, and canonical serialization.
`World::spawn_prefab` creates the whole subtree or none of it, and answers with
the root, every entity made, and which authored identity each became.
Instances carry no `source_id`, because a prefab's identities name entities
inside the prefab and two instances would collide on every one.

</details>

<a id="prefab-instances"></a>

#### Nested prefabs

Scenes place prefab instances with per-component overrides; prefabs nest, and edits reach every instance.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Nested prefabs*

**Tested by:** `crates/sindri-core/tests/prefab_instances.rs` (`crates/sindri-core/tests/prefab_instances.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/prefabs.md` (in `05-projects-scenes-prefabs.md`)

<details markdown="1">
<summary>Evidence</summary>

A scene places a prefab as an **instance**: an entity naming the prefab, with
its own ID, parent, name and transform and nothing else but its overrides — a
JSON merge patch per component, keyed by the prefab's entity IDs. Loading
expands it into the prefab's entities (`coin-3`, `coin-3/sparkle`), each
carrying a `PrefabLink`; saving writes it back as the reference, working the
overrides out as the difference from the prefab. Prefabs nest, and an
override reaches inside by path. The editor keeps instances linked: an edit to
the prefab reaches each one as a single undoable reconciliation that keeps its
overrides, and the inspector reverts, applies and unpacks them. Hosts that play
a scene expand it once with `SceneDocument::expanded`; the exporter ships every
prefab a scene places. The platformer's ten coins are instances of
`prefabs/coin.prefab`. An instance can do without some of its prefab's
entities (`removed`), patch a list by index, and keep its inner entities'
editor state.

</details>

### Scenes

<a id="scene-documents"></a>

#### Scene save / load

Versioned JSON scenes with stable authored IDs, validated on load and saved canonically byte for byte.

Engine ✅ · Editor ✅ · Decay — · Proof ✅ — parity row *Scene save / load*

**Tested by:** `crates/sindri-core/src/scene/tests/canonical.rs` (`crates/sindri-core/src/scene/tests/canonical.rs` in the Sindri repository), `crates/sindri-core/src/scene/tests/validation.rs` (`crates/sindri-core/src/scene/tests/validation.rs` in the Sindri repository), `crates/sindri-core/tests/scene_round_trip.rs` (`crates/sindri-core/tests/scene_round_trip.rs` in the Sindri repository), `crates/sindri-core/tests/baked_documents_are_canonical.rs` (`crates/sindri-core/tests/baked_documents_are_canonical.rs` in the Sindri repository), `crates/sindri-core/src/migration/tests/chain.rs` (`crates/sindri-core/src/migration/tests/chain.rs` in the Sindri repository)

**See:** `docs/scene-serialization.md` (in `05-projects-scenes-prefabs.md`), `docs/versioning.md` (`docs/versioning.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Scenes are versioned JSON documents with stable authored IDs kept separate from
runtime handles, so saving a loaded scene reproduces the authored identities
rather than inventing new ones. Loading validates duplicate IDs, missing
parents, and hierarchy cycles. Unknown component payloads are preserved by
default rather than dropped, which is what makes a scene written by a newer
editor survive an older runtime.

Serialization is canonical and a fixed point: entities and keys are sorted,
empty sections omitted, short scalar arrays kept on one line. Saving an unedited
scene reproduces the file byte for byte, which is what makes saving safe to
offer at all. Golden fixtures enforce it, and legacy fixtures migrate forward
through the migration chain.

</details>

### Commands and undo

<a id="commands-and-undo"></a>

#### Undo / redo

Every world edit is an invertible command; transactions are all-or-nothing and drags merge into one step.

Engine ✅ · Editor ✅ · Decay — · Proof — — parity row *Undo / redo*

**Tested by:** `crates/sindri-core/src/command/tests/undo.rs` (`crates/sindri-core/src/command/tests/undo.rs` in the Sindri repository), `crates/sindri-core/src/command/tests/transaction.rs` (`crates/sindri-core/src/command/tests/transaction.rs` in the Sindri repository), `crates/sindri-core/src/command/tests/merge.rs` (`crates/sindri-core/src/command/tests/merge.rs` in the Sindri repository), `crates/sindri-core/src/command/tests/locked.rs` (`crates/sindri-core/src/command/tests/locked.rs` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Every world edit can go through a `WorldCommand` that produces its own inverse:
set name, set the transform, set parent, set or remove a component.
`Transaction` is all-or-nothing, and `CommandHistory` gives bounded undo and
redo with labelled steps and merge runs, so a continuous drag collapses into one
undoable step rather than several hundred. The history also numbers the state
the world is in, so a tool that remembers the number it last saved can tell
whether the world and the file still agree — including after undoing back to it.

</details>

### Not yet

<a id="system-scheduler"></a>

#### Extensible system scheduler

The fixed session pipeline has an explicit order, but no scheduler or dependency graph for third-party systems.

Engine ❌ · Editor — · Decay — · Proof —

---

Source: `docs/capabilities/runtime.md`

## Effects, saves and randomness

Short-lived visual effects kept out of the entity world, persistent game saves,
and the seeded random stream every script shares.

Each entry links its tests and the projects that exercise it. Surface marks match the row in parity (`docs/parity.md` in the Sindri repository) where one is named: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — not applicable.

| Capability | Engine | Editor | Decay | Proof |
| --- | :-: | :-: | :-: | :-: |
| **Effects** | | | | |
| Pooled fleck bursts, batched with sprites | ✅ | 🟡 | ✅ | ✅ |
| Particle system (emitters, curves, shapes) | ✅ | 🟡 | ✅ | ✅ |
| **Game saves** | | | | |
| Game saves | ✅ | ✅ | ✅ | ✅ |
| **Randomness** | | | | |
| Seeded, portable randomness | ✅ | ✅ | ✅ | ✅ |
| **Not yet** | | | | |
| Authored parallax | ❌ | ❌ | ❌ | — |

### Effects

<a id="fleck-bursts"></a>

#### Pooled fleck bursts, batched with sprites

Short-lived flecks live as plain values in one bounded pool, drawn in the same batches as sprites.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Pooled fleck bursts, batched with sprites*

**Tested by:** `crates/sindri-scene/src/effects/pool.rs` (`crates/sindri-scene/src/effects/pool.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_throws_flecks.rs` (`crates/sindri-decay/tests/a_script_throws_flecks.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**See:** `docs/effect-scaling.md` (`docs/effect-scaling.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`Effects2d` holds short-lived visual flecks as plain values in one array. A
fleck has no identity a script can hold, no components, no place in the
hierarchy, and nothing can collide with it — everything an entity is *for* is
given up, and `docs/effect-scaling.md` is why: 8,000 flecks-as-entities cost
5.25 ms a frame, a third of a 60 Hz budget, against 0.018 ms pooled. Over half
the entity cost was `serde_json` re-reading each one's payload, every entity,
every frame.

What a burst looks like is authored on the entity as `sindri.effect.burst` —
count, speed, spread, lifetime, size, tint, fade, drag — because those are a
designer's numbers and a call naming all of them would be unreadable. Flecks
draw their directions from a stream of their own, never the run's, so turning
an explosion up cannot change which enemies spawn. The pool is bounded, the
oldest fleck makes way for the newest, and overflow is counted rather than
hidden.

They batch with ordinary sprites by layer and texture, so a burst is one draw
call rather than a second rendering path.

</details>

<a id="particle-emitters"></a>

#### Particle system (emitters, curves, shapes)

`sindri.particles` emitters throw into the fleck pool at an exact rate, with shapes, ramps and world or local simulation.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Particle system (emitters, curves, shapes)*

**Tested by:** `crates/sindri-scene/src/effects/emitter_tests.rs` (`crates/sindri-scene/src/effects/emitter_tests.rs` in the Sindri repository), `crates/sindri-scene/src/effects/particles.rs` (`crates/sindri-scene/src/effects/particles.rs` in the Sindri repository), `tools/sindri-particle-lab/src/preview.rs` (`tools/sindri-particle-lab/src/preview.rs` in the Sindri repository), `editor/src/native/particle_lab.rs` (`editor/src/native/particle_lab.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_drives_particles.rs` (`crates/sindri-decay/tests/a_script_drives_particles.rs` in the Sindri repository), `games/platformer/tests/sparkle_and_dust.rs` (`games/platformer/tests/sparkle_and_dust.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `games/low-tide` (`games/low-tide` in the Sindri repository)

**See:** `docs/particles.md` (in `06-2d.md`)

<details markdown="1">
<summary>Evidence</summary>

Particle Lab exposes the emitter in the editor and browser with presets,
a shared engine-simulation motion preview and JSON export. Its initial preview
accepts a dropped preview texture, but does not reproduce scene lighting.

`sindri.particles` is an emitter that keeps throwing into the same pool while
it plays: a rate and a cap, lifetime, speed, direction and spread with
variation, a point, circle, box or ring shape at an offset, gravity, drag, size
over life, a color ramp, rotation and spin, and world or local simulation. It
owes its rate times each step and carries the remainder, so a fractional rate
is exact at any frame rate, and it draws from the pool's stream. Decay plays,
stops and emits a handful at once (`Effects.play`, `Effects.stop`,
`Effects.emit`); the editor runs the selected emitter in the Scene view while
nothing plays. The platformer's coins sparkle and its hero trails dust; Low
Tide's treads kick up dust and its engine smokes while it drives.

</details>

### Game saves

<a id="game-saves"></a>

#### Game saves

A flat, versioned key/value save written atomically to disk or `localStorage` by the host, never by a script.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Game progress saves (numbers, flags and text)*

**Tested by:** `crates/sindri-core/src/save.rs` (`crates/sindri-core/src/save.rs` in the Sindri repository), `crates/sindri-platform/src/saves/file.rs` (`crates/sindri-platform/src/saves/file.rs` in the Sindri repository), `crates/sindri-platform/src/saves/mod.rs` (`crates/sindri-platform/src/saves/mod.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_remembers.rs` (`crates/sindri-decay/tests/a_script_remembers.rs` in the Sindri repository), `games/orbital-baked/tests/the_mixer.rs` (`games/orbital-baked/tests/the_mixer.rs` in the Sindri repository), `games/orbital-baked/tests/the_complete_game.rs` (`games/orbital-baked/tests/the_complete_game.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**Limits:**

- Decay's store holds numbers, truths and text; it has no world snapshot operation.

<details markdown="1">
<summary>Evidence</summary>

`sindri_core::SaveStore` is a flat, versioned key/value document. Flat because
Decay holds numbers, truths and text and nothing else: a structure a script
could not build is a structure nothing could write, and the file stays
something a person can read and repair. Ordered keys, so the same saved state
is the same bytes and a save can be diffed.

Three absences are distinguished, because they call for different things: a
first run starts cheerfully, a **damaged** save is worth telling someone about
before their progress is written over, and one written by a **newer** build is
reported without being read — a reader that guessed at a format it does not
know would corrupt it the moment it wrote back.

`sindri_platform::SaveBackend` is where the bytes go. `MemorySaves` is the
default so a headless run and a test have somewhere to write without choosing a
path; `DamagedSaves` exists so the unreadable path can be proven without
corrupting a real file; `FileSaves` writes beside the target and renames over
it, because a save half written is a save destroyed at the exact moment
someone's machine lost power mid-run; `BrowserSaves` uses `localStorage`,
chosen over every larger browser store because the alternatives are
asynchronous and a game should not have to ask whether its progress has landed
yet.

Decay reaches Unicode text through typed `Save.text` and `Save.set_text`, with
explicit wrong-type/missing-key fallbacks. Orbital uses those calls for pilot
names and a bounded flight log alongside its numeric career and flag unlocks.

Nothing in Decay writes to storage. The store is in memory, marked dirty on
change, and the host writes it out on a cadence and before it stops — how often
someone's disk is touched is a decision about their machine. Writing the same
value again is not a change, so a game storing its volume every frame does not
keep a disk busy.

Editor Play keeps a save in memory for the editor's lifetime and never writes
it to disk: `Save.*` calls work and round-trip inside a session so persistence
can be play-tested, but putting a file into someone's project because they
pressed Play would be a side effect they did not ask for. Editor preferences
are a separate thing and stay separate.

</details>

### Randomness

<a id="seeded-randomness"></a>

#### Seeded, portable randomness

A written-out PCG-XSH-RR generator: the same seed is the same run on every host, and Play restarts it.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅

**Tested by:** `crates/sindri-core/src/random.rs` (`crates/sindri-core/src/random.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**Limits:**

- One stream is shared by every script, so a number drawn early shifts every number after it.
- Not a source of secrets: a handful of outputs reveals the state.

<details markdown="1">
<summary>Evidence</summary>

`sindri_core::Rng` is a **PCG-XSH-RR 64/32** generator written out rather than
depended on. Every general-purpose crate reaches the operating system for a
seed, which on `wasm32-unknown-unknown` means `getrandom` and a target that
refuses to compile without an opt-in — and more to the point, entropy is the
opposite of what this is for. A run that cannot be replayed from its seed is
not seeded at all.

Everything in it is integer arithmetic and the one division is by a power of
two, which is what makes a seed mean the same thing on every host. Fractions
are built from the top 24 bits, so `[0, 1)` is never `1.0`; bounded integers
reject the draws that would make the low values slightly more likely, because
modulo bias on a drop table over a long run is exactly the kind of wrongness
that gets blamed on the game design. Seeding takes two steps around the state,
so neighbouring seeds do not produce neighbouring first outputs.

The engine never asks the platform for entropy and does not pretend to: a host
that seeds nothing gets a fixed stream. The editor puts the stream back to its
seed on every fresh Play, so pressing Play twice gives the same run twice and a
bug found once can be found again; resuming from a pause deliberately does
not, since that would replay numbers the scene has already acted on. A game
that wants variety calls `Random.seed` with something it knows. A run's seed is
worth storing while a frame's numbers are not.

</details>

### Not yet

<a id="parallax"></a>

#### Authored parallax

There is no authored parallax system.

Engine ❌ · Editor ❌ · Decay ❌ · Proof —

---

Source: `docs/capabilities/projects.md`

## Games, labs and tooling

The projects in this repository that exercise the engine, and the offline tools
beside it. A capability elsewhere names these as its proof; this page says what
each project is and what it has found. The tooling at the end changes nothing
about what the engine can do and is listed so that finding it does not read as
evidence of a capability.

Each entry links its tests and the projects that exercise it. Surface marks match the row in parity (`docs/parity.md` in the Sindri repository) where one is named: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — not applicable.

| Capability | Engine | Editor | Decay | Proof |
| --- | :-: | :-: | :-: | :-: |
| **Games** | | | | |
| Causeway | — | — | ✅ | ✅ |
| Orbital Last Stand | — | — | ✅ | ✅ |
| The platformer | — | — | ✅ | ✅ |
| Scorchball | — | — | ✅ | ✅ |
| Low Tide | — | — | ✅ | ✅ |
| **Labs and feature examples** | | | | |
| Spatial Query Lab | — | — | ✅ | ✅ |
| Camera Lab | — | — | ✅ | ✅ |
| **Tooling** | | | | |
| The isometric baker | — | — | — | ✅ |
| Sheet-declared ground anchor | ✅ | ✅ | — | ✅ |

### Games

<a id="causeway"></a>

#### Causeway

The flagship showcase: a tactics game of three wardens holding islands against the Hollow until the tide closes the causeway.

Engine — · Editor — · Decay ✅ · Proof ✅

**Tested by:** `game/tests/the_game_is_played/main.rs` (`game/tests/the_game_is_played/main.rs` in the Sindri repository), `game/tests/the_game_holds_together.rs` (`game/tests/the_game_holds_together.rs` in the Sindri repository), `game/tests/a_crossing_is_played.rs` (`game/tests/a_crossing_is_played.rs` in the Sindri repository)

**Exercised in:** `game` (`game` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Causeway is a turn-based tactics game. Wardens hold an eight-by-eight
island while the Hollow cross the causeway from the mainland at low tide; each
Hollow shows on the ground what it will strike, and a shove that moves it moves
its strike, so a turn is read and answered rather than raced. The tide comes in
on a schedule shown in advance, drowning any Hollow it reaches, and high tide
closes the causeway and wins the island. Three wardens of six are mustered
for a crossing, a road of six islands that forks three times, with a boon
chosen between islands and the Hearth carried from one to the next. Past the
Lighthouse it divides between two worlds, where a fourth warden joins. In the
Cinderfast lava rises instead of the tide: it burns what it reaches, vents
spill it across the board, and water a warden throws on it cools into new
ground. In the Rime anything shoved onto ice skids on until something stops
it, and thrown water freezes. Some forks offer an uncharted island, generated
from the crossing's seed to its world's rules and built at runtime.

The rules are one value and pure shared functions in `rules.decay`, so aiming
previews a skill by running it on a copy of the board; the director plays back
the beats each change leaves. The islands are text in `islands.decay`, read by
the rules and by `tools/causeway_art.py`, which builds every island, figure and
building as a low-poly GLB. It uses imported models with point lights, the
environment's grade and bloom in the shipped player, Weave screens, the
pointer's world ray for picking cells, camera shake, and Sound Lab effects and
an Ogg song. `a_crossing_is_played.rs` plays a whole crossing with a bot
through the real rules; `the_game_is_played` plays it by clicks and keys.
Four 300 ms quarter views use J/L, compact touch buttons and sea swipes.
View-relative keys and guarded ray picking preserve movement and combat;
muted model silhouettes reveal units behind raised terrain.

</details>

<a id="orbital-last-stand"></a>

#### Orbital Last Stand

The recreated game that forces new capabilities into existence: waves, bosses and a growing arsenal.

Engine — · Editor — · Decay ✅ · Proof ✅

**Tested by:** `games/orbital-baked/tests/it_compiles_and_runs.rs` (`games/orbital-baked/tests/it_compiles_and_runs.rs` in the Sindri repository), `games/orbital-baked/tests/boss_rush.rs` (`games/orbital-baked/tests/boss_rush.rs` in the Sindri repository), `games/orbital-baked/tests/the_complete_game.rs` (`games/orbital-baked/tests/the_complete_game.rs` in the Sindri repository), `games/orbital-baked/tests/the_build_has_teeth.rs` (`games/orbital-baked/tests/the_build_has_teeth.rs` in the Sindri repository), `scripts/browser/orbital-complete.mjs` (`scripts/browser/orbital-complete.mjs` in the Sindri repository), `games/orbital-baked/tests/combat_lab.rs` (`games/orbital-baked/tests/combat_lab.rs` in the Sindri repository), `games/orbital-baked/tests/the_ui_fits_every_screen.rs` (`games/orbital-baked/tests/the_ui_fits_every_screen.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**See:** `docs/orbital-last-stand-evidence.md` (`docs/orbital-last-stand-evidence.md` in the Sindri repository), `docs/orbital-combat-lab.md` (`docs/orbital-combat-lab.md` in the Sindri repository), `docs/orbital-release.md` (`docs/orbital-release.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Orbital Last Stand is the proof named by many entries on the other pages:
masks and sensor events, overlap queries, typed script references, profiles,
tags and spatial queries, tweening, the mixer and save, the full form-widget
set and composed Weave styling. Its own evidence and plan documents record what
it exercises in detail. Its complete-game edition adds a fleet hangar with four
sidegrade hulls, three difficulty settings, a finite twelve-boss rush, evasive
burst, career salvage/unlocks, commendations, the full module/threat archive,
recent sortie records and accessibility settings. All nineteen synergy recipes
grant explicit build bonuses, catalog descriptions match their combat effects,
and three Sound Lab compositions crossfade through named audio buses. Game logic remains Decay;
Weave owns responsive screens. Unicode text saves were the general engine gap
found by the callsign and flight log.

</details>

<a id="platformer"></a>

#### The platformer

The first genre showcase: a painted, solid tilemap level, a controller-driven hero, ten coins and a flag, with no Rust of its own.

Engine — · Editor — · Decay ✅ · Proof ✅

**Tested by:** `games/platformer/tests/a_run_reaches_the_flag.rs` (`games/platformer/tests/a_run_reaches_the_flag.rs` in the Sindri repository), `games/platformer/tests/painted_ground.rs` (`games/platformer/tests/painted_ground.rs` in the Sindri repository), `games/platformer/tests/character_gameplay.rs` (`games/platformer/tests/character_gameplay.rs` in the Sindri repository), `scripts/browser/platformer-goal.mjs` (`scripts/browser/platformer-goal.mjs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**Limits:**

- No enemies or hazards.
- No level after the first, no pause and no restart without Stop.

<details markdown="1">
<summary>Evidence</summary>

A side-view level painted as a tilemap and made solid by a Tilemap Collider 2D,
a hero who runs and jumps, ten coins and a flag, a HUD, and a camera that
follows. The scene and its Decay scripts are the game, and it exports to the
site.

Its hero uses the scene-owned Character 2D controller. Decay integrates
velocity and gravity, queues displacement and reads cached walkable support.
One capsule probe checks ground obstacles; body and foot sensors preserve
pickups independently. Walls, ceilings and one-way undersides never grant
standing. It gives the jump a buffer for a press just before landing and coyote
time for one just after running off a ledge. Letting go early cuts the rise
short.

It found three gaps, all closed as general capabilities: a script moving a
body's transform was undone by the next physics step, so a respawn did nothing;
a frictionless hero clung to walls because friction averaged; and camera
behaviors ran only in the camera example. Its tests stand the hero on the
painted ground, have a scripted player reach the flag without falling, and hold
the camera to following it inside the level. Its joints, materials, one-way
planks, ferry, forces and lighting are listed with those capabilities.

</details>

<a id="scorchball"></a>

#### Scorchball

The local-multiplayer genre showcase: couch football for two to four pads, ported from Unity with no Rust of its own.

Engine — · Editor — · Decay ✅ · Proof ✅

**Tested by:** `games/scorchball/tests/a_match_is_played.rs` (`games/scorchball/tests/a_match_is_played.rs` in the Sindri repository), `games/scorchball/tests/a_bot_plays.rs` (`games/scorchball/tests/a_bot_plays.rs` in the Sindri repository)

**Exercised in:** `games/scorchball` (`games/scorchball` in the Sindri repository)

**Limits:**

- No music: the original tracks' terms are unchecked, and MP3 is an exception the dependency policy would rather not grow.
- No match end, pause or restart without Stop.
- Bots do not pass, go for power-ups on purpose, or come in difficulties.
- Played with real pads by nobody yet: CI has none.

<details markdown="1">
<summary>Evidence</summary>

A scene, four prefabs and four Decay scripts are the game, and it exports to
the site. Gamepads were the capability it needed and were added for it as a
general one.

**Players are made when a pad joins.** The match reads `Gamepad.joined()`,
spawns a player prefab, and authors its slot with `World.set_property`; odd
slots play for Blue and even for Red. Unplugging a pad takes its player off
through `Gamepad.left()`. Each player readies with North, picks one of two
characters with the d-pad, walks with the left stick and kicks with the right
bumper, aimed with the right stick, all read through its own slot.

**Scripts talk by type and by event.** A player kicks the ball with a typed
message (`this.ball.kick(velocity, slot)`) and bends the shot with `nudge`; the
match counts players whose live `ready` field is set; a power-up sends
`power_up` to the player who took it, or emits `WindPicked` or `FirePicked`;
the ball sends `catch_fire` to an opponent it touches while alight, and emits
`GoalScored` when it crosses a goal line. Possession, pickups and reach are
distance checks, because a collider does not scale with its entity and the
Enlarger makes a player bigger.

**It found two gaps, both closed.** A child sprite was drawn from its own
transform alone; a child's transform is now local to its parent everywhere. And
the test harness did not lay out screen text, so a misspelled text anchor
passed every test; the harness now lays it out each step.

**It is checked, not just run.** The tests press pads: two join on their own
sides, ready up and kick off; Blue takes the ball, dribbles past Red and
scores; a sign cannot be taken until it lands; unplugging a pad takes its
player off; Blue collects all four powers; and a Fireball sets an opponent
running wild until it burns out. Select in the lobby adds a bot, which attacks,
supports, defends goal-side of the ball and goes for loose balls it can reach
first; the tests have one score on a player standing still, and two play each
other until one scores.

</details>

<a id="low-tide"></a>

#### Low Tide

The moving-home genre showcase: drive a crawler across a drained voxel sea, gather cargo, and escape the returning tide.

Engine — · Editor — · Decay ✅ · Proof ✅

**Tested by:** `games/low-tide/tests/a_voyage_is_played.rs` (`games/low-tide/tests/a_voyage_is_played.rs` in the Sindri repository), `games/low-tide/tests/gathering_fills_the_hold.rs` (`games/low-tide/tests/gathering_fills_the_hold.rs` in the Sindri repository), `games/low-tide/tests/a_home_grows.rs` (`games/low-tide/tests/a_home_grows.rs` in the Sindri repository), `games/low-tide/tests/a_flood_season.rs` (`games/low-tide/tests/a_flood_season.rs` in the Sindri repository), `games/low-tide/tests/a_high_tide_dive.rs` (`games/low-tide/tests/a_high_tide_dive.rs` in the Sindri repository), `games/low-tide/tests/played_on_a_phone.rs` (`games/low-tide/tests/played_on_a_phone.rs` in the Sindri repository)

**Exercised in:** `games/low-tide` (`games/low-tide` in the Sindri repository)

**Limits:**

- There is no session persistence on desktop or web; history is session-only.
- The dive interior is an authored activity, not a generated terrain cross-section.

<details markdown="1">
<summary>Evidence</summary>

Low Tide's Basin is a voxel world viewed as a map. It exercises sparse
`Grid.set_block` edits for biome harvesting, typed shared state and deck tiles
for its six-resource hold (four slots on the small starter crawler, growing to
sixteen through three resource-funded stern extensions), and prefab spawning
for deterministic wrecks across the Basin. Keyboard and phone-touch runtime
tests harvest, carry, stow and jettison actual blocks; the workbench consumes
physical cargo for deck expansion, a bunk and an engine upgrade, preserving the
crew and deck origin while driving. Its viewport-sized workbench exposes three
directly selectable blueprints, current cargo versus cost, installed and
chassis-limit states and explicit actions.

Its wrecks spawn six resource-specific cargo prefabs as hull children; Decay
selects the nearby physical crate and retains a per-slot mask across wreck
streaming. Carried cargo can be set down as a world prefab and picked up again
before stowing. Its instrument HUD uses authored atlas icons, real hold
segments and speed/phase-time meters, and discard confirmation is tied to a
cell and cancels on movement, Escape or timeout.

A side-view high-tide dive frames a wreck tilemap with a second authored
camera; Decay swims against hull cells, manages air and a heavy cache, then
deposits ore and scrap in the original hold only after returning. The map tide
continues; recall, empty air and ebb recover crew without loot. It found the
reversible map flood capability; its season, cargo loss, waiting and escape
rules are Decay gameplay. These are Decay gameplay rules on existing engine
APIs.

</details>

### Labs and feature examples

<a id="spatial-query-lab"></a>

#### Spatial Query Lab

`examples/spatial` makes `World.nearest` and `World.within_radius` observable, on desktop and phone.

Engine — · Editor — · Decay ✅ · Proof ✅

**Tested by:** `crates/sindri-decay/tests/spatial_demo.rs` (`crates/sindri-decay/tests/spatial_demo.rs` in the Sindri repository)

**Exercised in:** `examples/spatial` (`examples/spatial` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

A movable origin and radius guide accompany the actual nearest result and
ordered radius snapshot. Tag selection, equal-distance samples, an exact
boundary target, active/parent toggles, rotating and scaled parent transforms,
a transformless tagged entity and persistent gray markers make the query
contract observable. Native regressions exercise these cases and phone touch;
browser smoke exercises visible controls and captures desktop/phone
project-subpath and custom-domain exports. Orbital remains the gameplay proof
of the APIs.

</details>

<a id="camera-lab"></a>

#### Camera Lab

`examples/camera` makes camera follow, confinement and shake observable with Decay movement and runtime controls.

Engine — · Editor — · Decay ✅ · Proof ✅

**Tested by:** `examples/camera/src/tests.rs` (`examples/camera/src/tests.rs` in the Sindri repository)

**Exercised in:** `examples/camera` (`examples/camera` in the Sindri repository)

**See:** `docs/cameras.md` (in `06-2d.md`)

**Limits:**

- Dedicated editor behavior controls and gizmos are absent.

<details markdown="1">
<summary>Evidence</summary>

Camera-relative dead-zone and world-space camera-center bounds guides, mode
toggles, sampled coordinates, an automatic tour, impact button and touch
movement pad make follow, confinement and shake observable. The native harness
uses the same scene/scripts, binds the packaged font and supplies UI
hit-testing; Pages uses the existing shared browser host. Runtime regressions
exercise modes and phone touch; browser smoke covers desktop and phone controls
and project/custom-domain routes.

</details>

### Tooling

<a id="isometric-baker"></a>

#### The isometric baker

`tools/isometric-baker` bakes a 3D model into an ordinary sprite sheet and its `.sheet`, offline, in isometric, top-down or side views.

Engine — · Editor — · Decay — · Proof ✅

**Tested by:** `tools/isometric-baker/test/bake.test.ts` (`tools/isometric-baker/test/bake.test.ts` in the Sindri repository), `tools/isometric-baker/test/anchor.test.ts` (`tools/isometric-baker/test/anchor.test.ts` in the Sindri repository), `tools/isometric-baker/test/grain.test.ts` (`tools/isometric-baker/test/grain.test.ts` in the Sindri repository), `tools/isometric-baker/test/tile-overhang.test.ts` (`tools/isometric-baker/test/tile-overhang.test.ts` in the Sindri repository), `tools/isometric-baker/test/web.test.ts` (`tools/isometric-baker/test/web.test.ts` in the Sindri repository), `tools/isometric-baker/test/imported.test.ts` (`tools/isometric-baker/test/imported.test.ts` in the Sindri repository), `tools/isometric-baker/test/models.test.ts` (`tools/isometric-baker/test/models.test.ts` in the Sindri repository), `tools/isometric-baker/test/quantize.test.ts` (`tools/isometric-baker/test/quantize.test.ts` in the Sindri repository), `tools/isometric-baker/test/unzip.test.ts` (`tools/isometric-baker/test/unzip.test.ts` in the Sindri repository)

**Exercised in:** `games/orbital-baked/assets/textures/detonation.isobake` (`games/orbital-baked/assets/textures/detonation.isobake` in the Sindri repository)

**See:** `docs/isometric-baker.md` (`docs/isometric-baker.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Nothing the engine, editor, native game or browser export builds depends on the
tool: the assets it produces are ordinary PNGs and sheet documents, and by the
time a game loads one there is nothing left to say it was baked.

It bakes three views. Isometric is the default; **top-down** and **side** were
added because the isometric camera cannot reach them by any choice of tile.
The flat views state `pixels_per_unit` rather than deriving a scale from a
diamond they do not draw, and each view refuses the other's fields. A `grain`
option shifts texels along a material's ramp, hashed from the texel's position
on the model, so the same texel is the same shade in every bake. Height is
baked rather than scaled, so a slab is its own tile. The baker refuses art that
overhangs its own footprint, and a sheet declares where its sprites meet the
ground.

The same bake runs in a page on the site (`web/`), built from the tool's own
sources with their types stripped. `web.test.ts` asserts its output matches the
command line's pixel for pixel; a browser smoke test opens it in CI. The page
also bakes Blender models: a recipe may name a `.glb`, `.gltf` or `.obj`
(`"kind": "file"`), which Three.js reads into triangles that are fitted, centred
on the footprint and banded like a hand-written model (`imported.test.ts`,
`models.test.ts`). A model that comes as several files opens whole, from a zip, a
folder or the files chosen together, and a textured model's colors are cut to a
few (`quantize.test.ts`, `unzip.test.ts`). The command line cannot read model
files yet.

Orbital Last Stand uses it for one thing: `textures/detonation.isobake` bakes a
five-frame blast, top-down, as `variants` of one sheet, and
`scripts/detonation.decay` plays the clip and despawns when
`Animation.is_finished` says it ended.

</details>

<a id="ground-anchor"></a>

#### Sheet-declared ground anchor

A sheet says where its sprites meet the ground, so sorting and placement are authored rather than guessed.

Engine ✅ · Editor ✅ · Decay — · Proof ✅ — parity row *Sheet-declared ground anchor*

**Tested by:** `crates/sindri-scene/tests/extraction/standing_on_ground.rs` (`crates/sindri-scene/tests/extraction/standing_on_ground.rs` in the Sindri repository), `tools/isometric-baker/test/anchor.test.ts` (`tools/isometric-baker/test/anchor.test.ts` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

A quad is drawn centred on its entity, so without an anchor the middle of the
picture is what lands on the tile. A sheet declares where its sprites meet the
ground, and `sindri.tilemap` has `tile_overhang`, how far below its cell a
tile's art may reach: the cell is unchanged and still what the grid, picking,
occupancy and gameplay measure in; only the drawn quad grows, downward.

</details>

---

Source: `docs/capabilities/assets.md`

## Assets

Logical asset IDs, the asynchronous load queue, typed decoders, manifests and
hot reload. `docs/asset-foundation.md` is the contract.

Each entry links its tests and the projects that exercise it. Surface marks match the row in parity (`docs/parity.md` in the Sindri repository) where one is named: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — not applicable.

| Capability | Engine | Editor | Decay | Proof |
| --- | :-: | :-: | :-: | :-: |
| **Loading** | | | | |
| Asset IDs, manifests, async load | ✅ | ✅ | 🟡 | ✅ |
| Identical image decoding on every target | ✅ | — | — | — |
| Project fonts | ✅ | ✅ | — | ✅ |
| Reusable data profiles | ✅ | ✅ | ✅ | ✅ |
| Hot reload | ✅ | 🟡 | 🟡 | ✅ |

### Loading

<a id="asset-ids-and-loading"></a>

#### Asset IDs, manifests, async load

Validated logical IDs, a bounded generation-checked load queue, typed decoders and hash-checked manifests.

Engine ✅ · Editor ✅ · Decay 🟡 · Proof ✅ — parity row *Asset IDs, manifests, async load*

**Tested by:** `crates/sindri-core/src/asset/tests/id.rs` (`crates/sindri-core/src/asset/tests/id.rs` in the Sindri repository), `crates/sindri-core/src/asset/tests/store.rs` (`crates/sindri-core/src/asset/tests/store.rs` in the Sindri repository), `crates/sindri-assets/src/queue/tests.rs` (`crates/sindri-assets/src/queue/tests.rs` in the Sindri repository), `crates/sindri-assets/src/loader/tests.rs` (`crates/sindri-assets/src/loader/tests.rs` in the Sindri repository), `crates/sindri-assets/src/manifest/tests.rs` (`crates/sindri-assets/src/manifest/tests.rs` in the Sindri repository), `crates/sindri-gpu/tests/texture_registry.rs` (`crates/sindri-gpu/tests/texture_registry.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**See:** `docs/asset-foundation.md` (in `05-projects-scenes-prefabs.md`)

<details markdown="1">
<summary>Evidence</summary>

`AssetId` is a validated relative logical ID, never a path or URL. Sources
resolve it against a root: the filesystem source canonicalises to catch symlink
escapes, and `UrlRoot` percent-encodes and normalises bases with rules
exercised on every target rather than only in a browser. The load queue is
bounded, rejects duplicates, and carries a generation token so a late
completion cannot overwrite a replacement. Textures, fonts, sheets, scripts,
and scene JSON decode through typed decoders. Nothing pretends browser I/O is
synchronous.

A `TextureId` is a generation-checked slot handle, so the renderer's texture
registry can release one texture and reuse its slot without a handle nobody
updated drawing whatever lands there next.

`AssetLoader` drives all of that in one place — request, enqueue, drain,
decode, apply — so a caller does not have to know the order. Requesting is
idempotent, a failure is reported once rather than retried forever, and
`retain` releases what is no longer wanted and says which IDs went, so a host
can drop whatever it built from them. GPU upload stays with the host, which is
the only thing that owns a device.

`AssetManifest` records what a project ships — each asset's length and the
SHA-256 of its stored bytes — as a versioned, ID-ordered file. A loader given
one holds arriving bytes to it, so a truncated response or a stale cache entry
is an error naming the asset rather than a picture from last week; an asset the
manifest does not list still loads. The editor picks one up from the directory
a scene lives in. The generic browser host sizes each kind's bounded request
queue from that manifest, so projects with more than the asset pipeline's
default 64 textures or other same-kind assets still load without increasing
fetch concurrency.

</details>

<a id="image-decoding"></a>

#### Identical image decoding on every target

Every PNG colour type, 16-bit, interlaced and JPEG images decode pixel-identically natively and on wasm32.

Engine ✅ · Editor — · Decay — · Proof —

**Tested by:** `crates/sindri-assets/tests/decode_compatibility.rs` (`crates/sindri-assets/tests/decode_compatibility.rs` in the Sindri repository), `crates/sindri-assets/src/decode/tests.rs` (`crates/sindri-assets/src/decode/tests.rs` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

A corpus of deliberately awkward images — every PNG colour type, sixteen bits
per channel, an interlaced encoding, and a JPEG — is decoded and checked pixel
by pixel on both native and `wasm32-unknown-unknown`, so a texture cannot
decode one way in the editor and another in the browser.

</details>

<a id="project-fonts"></a>

#### Project fonts

OpenType faces are validated and bound under the scene's logical reference, so no build substitutes an installed font.

Engine ✅ · Editor ✅ · Decay — · Proof ✅ — parity row *Project fonts*

**Tested by:** `crates/sindri-assets/src/decode/tests.rs` (`crates/sindri-assets/src/decode/tests.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`FontAssetDecoder` validates OpenType bytes and records their declared family;
text binds that project-owned face under the scene's logical asset reference,
so native and browser builds never substitute different installed fonts.

</details>

<a id="profiles"></a>

#### Reusable data profiles

Versioned `.profile` assets hold nested data that many script components read without creating entities.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Reusable data profiles*

**Tested by:** `crates/sindri-decay/tests/a_script_reads_a_profile.rs` (`crates/sindri-decay/tests/a_script_reads_a_profile.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/profiles.md` (in `05-projects-scenes-prefabs.md`)

<details markdown="1">
<summary>Evidence</summary>

`ProfileDocument` is the reusable-data counterpart to a prefab: a versioned
`.profile` asset with an author label, an optional game-defined type, and a
nested JSON-shaped value payload. `ProfileAssetDecoder` validates it through
the same native/browser loading path, and the manifest records it as
`profile`. Unlike a prefab it creates no entity; many script components can
hold the same logical profile reference and read one loaded document.

</details>

<a id="hot-reload"></a>

#### Hot reload

On native, `AssetWatch` notices a changed file behind a loaded asset and `AssetLoader::reload` loads it again.

Engine ✅ · Editor 🟡 · Decay 🟡 · Proof ✅ — parity row *Hot reload*

**Tested by:** `crates/sindri-assets/src/loader/tests.rs` (`crates/sindri-assets/src/loader/tests.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

On native, `AssetWatch` notices when the file behind a loaded asset changes and
`AssetLoader::reload` loads it again, by polling modification time and length
rather than subscribing to filesystem events.

</details>

---

Source: `docs/capabilities/decay.md`

## Decay

The gameplay language, in `decay/` — a **separate Cargo workspace**, not a
member of this one. Nothing under `decay/` depends on a `sindri-*` crate and no
engine crate depends on Decay. `sindri-decay` binds it to a world. The whole
host table is `docs/scripting.md`; the language is `decay/LANGUAGE.md`; the
generated host surface is `docs/generated/decay-api.md`.

Host calls that belong to one subsystem — physics, audio, UI, grids, effects,
saves, randomness — are listed with that subsystem's capability, not here.

Each entry links its tests and the projects that exercise it. Surface marks match the row in parity (`docs/parity.md` in the Sindri repository) where one is named: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — not applicable.

| Capability | Engine | Editor | Decay | Proof |
| --- | :-: | :-: | :-: | :-: |
| **Scripts and the host** | | | | |
| Typed host, compile / run / reload | ✅ | 🟡 | ✅ | ✅ |
| Safe entity, prefab, profile references | ✅ | ✅ | ✅ | ✅ |
| Scripts reach each other by type | ✅ | ✅ | ✅ | ✅ |
| Events (emit, handle anywhere) | ✅ | ✅ | ✅ | ✅ |
| Shared game state, declared and typed | 🟡 | ✅ | 🟡 | 🟡 |
| Tags and queries | ✅ | ✅ | ✅ | ✅ |
| Deterministic gameplay nearest / radius queries | ✅ | — | ✅ | ✅ |
| Camera impacts are gameplay | ✅ | — | ✅ | ✅ |
| Tweening / easing | ✅ | 🟡 | ✅ | ✅ |
| **Language** | | | | |
| Vectors as values (`Vec2`, `Vec3`) | ✅ | 🟡 | ✅ | ✅ |
| Timers as values | ✅ | 🟡 | ✅ | ✅ |
| Shared helper functions across files | ✅ | ✅ | ✅ | ✅ |
| Constants | ✅ | ✅ | ✅ | ✅ |
| Structs | ✅ | ✅ | ✅ | ✅ |
| Lists and ranges | ✅ | ✅ | ✅ | ✅ |
| Maps | ✅ | ✅ | ✅ | ✅ |
| Text: joining and methods | ✅ | ✅ | ✅ | ✅ |
| Enums and exhaustive `match` | ✅ | ✅ | ✅ | ✅ |
| Optional values | ✅ | ✅ | ✅ | ✅ |
| Colours | ✅ | ✅ | ✅ | ✅ |
| Parser, analyzer, IR and runtime | ✅ | — | ✅ | ✅ |
| **Tooling** | | | | |
| Language server: highlight, complete, hover, definitions, diagnostics | — | 🟡 | ✅ | ✅ |
| **Not yet** | | | | |
| Formatter | ❌ | ❌ | ❌ | — |
| Debugger (breakpoints, stepping) | ❌ | ❌ | ❌ | — |
| Coroutines / sequencing | ❌ | — | ❌ | — |
| Script unit tests | ❌ | ❌ | ❌ | ❌ |
| Sets, closures and first-class functions | — | — | ❌ | — |
| TypeScript embedding SDK | ❌ | — | — | — |
| Fractional component writes from integer-authored numbers | ❌ | — | ❌ | ❌ |
| Quaternion composition and interpolation | ❌ | — | ❌ | ❌ |
| **3D transform values** | | | | |
| Rotations as values | 🟡 | — | 🟡 | 🟡 |

### Scripts and the host

<a id="typed-host"></a>

#### Typed host, compile / run / reload

`sindri.script` binds a source to an entity; paths are typed against one description shared by analyzer and host, and sources hot-reload.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Typed host, compile / run / reload*

**Tested by:** `crates/sindri-decay/src/surface/tests.rs` (`crates/sindri-decay/src/surface/tests.rs` in the Sindri repository), `crates/sindri-decay/tests/documented_surface.rs` (`crates/sindri-decay/tests/documented_surface.rs` in the Sindri repository), `crates/sindri-runtime/tests/a_session_steps_a_scene.rs` (`crates/sindri-runtime/tests/a_session_steps_a_scene.rs` in the Sindri repository), `crates/sindri-decay/tests/spatial_transforms.rs` (`crates/sindri-decay/tests/spatial_transforms.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository), `games/scorchball` (`games/scorchball` in the Sindri repository), `games/platformer` (`games/platformer` in the Sindri repository), `examples/transform` (`examples/transform` in the Sindri repository)

**See:** `docs/scripting.md` (in `03-scripting-guide.md`), `docs/generated/decay-api.md` (in `02-decay-api.md`)

**Limits:**

- No script state migration across a reload: a changed file recompiles, and the running instance keeps whatever fields it had.
- There is no general dynamic component API; purpose-built typed paths cover each subsystem.
- Despawning is not undoable — no script write is; play mode restores from a snapshot.

<details markdown="1">
<summary>Evidence</summary>

`sindri-decay` binds the language to a world: a `sindri.script` component names
a source and a container, `WorldHost` gives Decay's symbolic paths a meaning in
terms of one entity's transform, and the editor runs every script once a frame
with whatever the transport says a frame is worth. Authored `@export`
properties reach the script before its first line. Sources load through
`sindri-assets` and hot-reload from the same `AssetWatch` the textures use.

A script reaches its own transform's position, scale and Z rotation, its
sprite's tint and layer, the keyboard, the frame's delta and its own elapsed
time, logical grid position through an explicit tilemap entity, maths
functions (`abs`, `sqrt`, `sin`, `cos`, `atan2`, `exp`, `min`, `max`, `floor`,
`ceil`, `round`, `sign`, `clamp`, `lerp`, `PI` and `TAU`), and `print`.
**Those paths are typed**, so `this.transfrom.position.x` is a compile error
with a line number rather than a first-frame failure, and reaching for a method
on a container says what to write instead. The analyzer's view and the host's
accessors are derived from one description, and a test walks every path the
analyzer accepts to assert the host answers it.

A failing script reports itself and does not stop the others.


The 3D transform surface additionally offers read-only world-space unit
`forward`, `right`, and `up`, and checked `look_at(target)` and
`rotate_around(pivot, axis, radians)` methods on own and referenced entities.
The script regressions cover rotated/scaled parents, normalized directions,
copy-versus-write semantics, and atomic errors including the Z lock. The
Transform Lab (`examples/transform/README.md` in the Sindri repository) opens a perspective scene
and runs to its goal.
This is slice 6 of `docs/3d-update.md`, a foundation for Explorer rather than
completed game proof: no in-tree game uses these new methods yet. Quaternion
composition/interpolation remains pending; character and orbit-camera feature
proof is documented below.

`transform.rotation` is a copied parent-relative `Quat` x/y/z/w value, using
Decay's existing struct constructor and copy semantics. Whole writes normalize
finite nonzero values atomically; zero/non-finite writes retain the transform.
Stored components are readable; edit a copied local value and assign it back.
Script regressions cover huge/tiny magnitudes, invalid input, parented rotation,
copy independence and runtime rejection of direct component writes. Transform
Lab saves its rotation, transfers its facing to the goal and restores its original
on arrival through Session and exported Chromium. Generated host metadata exposes
the type to the checker and LSP. This is a general transform capability added to
finish the planned 3D scripting surface; Explorer game proof remains pending.

</details>

<a id="references"></a>

#### Safe entity, prefab, profile references

Opaque references a script holds, passes and reaches through, checked at compile time and reported when stale.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Safe entity, prefab, profile references*

**Tested by:** `crates/sindri-decay/tests/finding_an_entity_by_name.rs` (`crates/sindri-decay/tests/finding_an_entity_by_name.rs` in the Sindri repository), `crates/sindri-decay/tests/one_script_reaches_another_entity.rs` (`crates/sindri-decay/tests/one_script_reaches_another_entity.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_makes_an_entity.rs` (`crates/sindri-decay/tests/a_script_makes_an_entity.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_reads_a_profile.rs` (`crates/sindri-decay/tests/a_script_reads_a_profile.rs` in the Sindri repository), `crates/sindri-decay/tests/a_struct_holds_an_entity.rs` (`crates/sindri-decay/tests/a_struct_holds_an_entity.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`Value::Reference` is a value Decay can hold, pass, compare and store but
cannot construct or look inside; the engine packs a runtime handle into it.
`World.find` looks one up by the name a scene gave it, `World.exists` asks
whether it still names anything, `World.despawn` removes it, and reaching
through one gets the same transform and sprite paths a script reaches on
itself — checked at compile time. Reaching through a stale or null reference is
reported rather than silently ignored.

`World.spawn` takes a typed `Prefab` — an asset reference the scene authored
into an `@export` field, not a string in the source, which is what lets the
editor resolve it and load the document before the frame that needs it — and
answers with a generation-checked reference to the new root. Overrides are the
ordinary writes through that reference; `World.set_parent` moves it, and
`World.set_property` authors a per-instance starting value that reaches the
spawned script before its first callback. A second entity can read a numeric
authored value through `World.property_number`, and a running entity receives
addressed numeric events through `World.send_signal` and `World.take_signal`.
A spawned script starts within the same pass, so a bullet fired during an
update moves during that update. Both the cascade that allows and the number of
entities one pass may create are bounded and reported rather than run.

An opaque exported `Profile` is resolved and loaded from the project in the
same pass. `Profiles.number`/`text`/`flag` read scalar values;
`Profiles.count` and the typed `*_at` calls read object records from top-level
lists, always with explicit fallbacks. Orbital Last Stand drives its 160-entry
module registry, weighted pools, requirements, display copy, and generic
effects from one profile rather than ID switchboards.

`World.set_shape_point(index, x, y)` writes one of at most eight authored
points on the current entity's `sindri.shape`; non-integer and out-of-range
indices are refused. Orbital Last Stand uses it for the exact six-point Strider
hull.

</details>

<a id="scripts-by-type"></a>

#### Scripts reach each other by type

Every `script` is a type: `Bolt.on(entity)` finds one, its fields read and write live, and its functions are messages.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Scripts reach each other by type*

**Tested by:** `crates/sindri-decay/tests/scripts_reach_each_other_by_type.rs` (`crates/sindri-decay/tests/scripts_reach_each_other_by_type.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Every `script` in a project is a type any other script can name:
`Bolt.on(entity)` finds one (or `null`), its fields read and write live — or
set what a just-spawned one starts with — and calling one of its functions
sends a message delivered after the pass, in order, in bounded rounds. A script
type is also an `Entity`. An unknown script, a misspelt field or message, or a
wrong argument is a compile error. The editor and the exporter load every
project script rather than only referenced ones, and the editor's preview and
`decay-lsp` check against the whole project. Orbital Last Stand's player sets
up each bullet it fires this way.

</details>

<a id="events"></a>

#### Events (emit, handle anywhere)

A project declares events any script emits and any script handles, delivered after the pass and checked across files.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Events (emit, handle anywhere)*

**Tested by:** `crates/sindri-decay/tests/scripts_hear_events.rs` (`crates/sindri-decay/tests/scripts_hear_events.rs` in the Sindri repository), `decay/crates/decay-semantic/src/tests/events.rs` (`decay/crates/decay-semantic/src/tests/events.rs` in the Sindri repository), `decay/crates/decay-runtime/src/tests/events.rs` (`decay/crates/decay-runtime/src/tests/events.rs` in the Sindri repository)

**Exercised in:** `games/scorchball` (`games/scorchball` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

A project declares events (`event GoalScored(team: number);`) that any script
emits and any script handles with `on GoalScored(team) { }`, delivered after
the pass and checked across files.

</details>

<a id="shared-state"></a>

#### Shared game state, declared and typed

`state Game { var score: number = 0.0; }` makes a checked, typed name on the game's board.

Engine 🟡 · Editor ✅ · Decay 🟡 · Proof 🟡 — parity row *Shared game state, declared and typed*

**Tested by:** `crates/sindri-decay/tests/scripts_share_state.rs` (`crates/sindri-decay/tests/scripts_share_state.rs` in the Sindri repository), `decay/crates/decay-semantic/src/tests/states.rs` (`decay/crates/decay-semantic/src/tests/states.rs` in the Sindri repository)

**Exercised in:** `games/scorchball` (`games/scorchball` in the Sindri repository), `games/platformer` (`games/platformer` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

The board earns its place for facts that belong to the game rather than to an
entity — the score, whether the game is won. A project can declare what goes on
it: `state Game { var score: number = 0.0; }` in any file makes `Game.score` a
checked name every script reads and writes, with one type and one starting
value, stored under the same board name so `Game.get("score", 0.0)` still sees
it. A misspelt name, a wrong type and a write to a `let` are compile errors
across files.

</details>

<a id="tag-queries"></a>

#### Tags and queries

`World.with_tag` and `World.has_tag` answer by authored `sindri.tags`, in deterministic order, bounded and refused past it.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Tags and queries*

**Tested by:** `crates/sindri-decay/tests/a_script_asks_for_a_group.rs` (`crates/sindri-decay/tests/a_script_asks_for_a_group.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**Limits:**

- No query by more than one tag at a time, and no measured cost for a query at combat density.

<details markdown="1">
<summary>Evidence</summary>

`World.with_tag` answers with a `List<Entity>` — every active entity carrying
an authored `sindri.tags` tag, in deterministic world order, bounded at 8192
and refused rather than truncated past it. A tag says what an entity *is*,
which is the question a game that makes its enemies as it goes actually has.
The answer is a snapshot of handles, so an entity despawned mid-walk leaves one
that `World.exists` answers false for. `World.has_tag` answers the same
question for one entity without walking the world; Orbital Last Stand uses it
on collision handles so dense projectile traffic does not become one
full-world query per impact.

</details>

<a id="spatial-queries"></a>

#### Deterministic gameplay nearest / radius queries

`World.nearest` and `World.within_radius` find tagged entities by world position, deterministically ordered.

Engine ✅ · Editor — · Decay ✅ · Proof ✅ — parity row *Deterministic gameplay nearest / radius queries*

**Tested by:** `crates/sindri-decay/src/host/query/tests.rs` (`crates/sindri-decay/src/host/query/tests.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_queries_spatial_entities.rs` (`crates/sindri-decay/tests/a_script_queries_spatial_entities.rs` in the Sindri repository), `crates/sindri-decay/tests/spatial_demo.rs` (`crates/sindri-decay/tests/spatial_demo.rs` in the Sindri repository), `games/orbital-baked/tests/spatial_targeting.rs` (`games/orbital-baked/tests/spatial_targeting.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository), `examples/spatial` (`examples/spatial` in the Sindri repository)

**Limits:**

- These are O(n) scans without an index; cone and box queries are deferred.

<details markdown="1">
<summary>Evidence</summary>

`World.nearest(tag, position)` returns the closest active tagged entity to a
world-space `Vec3`, or `null`. `World.within_radius(tag, position, radius)`
returns an inclusive-radius snapshot sorted nearest first, with stable world
order breaking ties. Both use `World::world_transform`, skip
missing/non-finite world positions, and share `with_tag`'s authored-tag and
inherited-active semantics. Radius results are refused above 8192; negative and
NaN radii are errors, zero includes coincident entities, and positive infinity
searches all spatial matches. Orbital Last Stand's player uses `nearest` when
the closest enemy is visible, with a sorted unbounded-radius fallback
otherwise; Arc selects from sorted radius results with its impact-point
exclusion.

</details>

<a id="camera-trauma"></a>

#### Camera impacts are gameplay

`Camera.add_trauma` lets Decay decide when an impact happens while the engine owns the shake.

Engine ✅ · Editor — · Decay ✅ · Proof ✅

**Tested by:** `crates/sindri-decay/tests/a_script_controls_camera.rs` (`crates/sindri-decay/tests/a_script_controls_camera.rs` in the Sindri repository)

**Exercised in:** `examples/camera` (`examples/camera` in the Sindri repository)

**See:** `docs/camera-runtime-scripting.md` (in `03-scripting-guide.md`)

<details markdown="1">
<summary>Evidence</summary>

Decay exposes `Camera.add_trauma(amount)`, which adds impact trauma to the one
authored world camera carrying `sindri.camera.behavior`. Decay decides when the
impact occurs; the engine-owned camera behavior remains responsible for shake
strength, waveform, frequency, decay, and the resulting transform. The camera
acceptance demo uses this call directly.

</details>

<a id="tweening"></a>

#### Tweening / easing

Typed `Tween` handles for numbers, vectors and colours with delay, loops, yoyo and sequencing, sharing easing with Weave.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Tweening / easing*

**Tested by:** `crates/sindri-decay/tests/scripts_tween.rs` (`crates/sindri-decay/tests/scripts_tween.rs` in the Sindri repository), `crates/sindri-decay/tests/tween_demo.rs` (`crates/sindri-decay/tests/tween_demo.rs` in the Sindri repository), `games/orbital-baked/tests/pickup_tween.rs` (`games/orbital-baked/tests/pickup_tween.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository), `examples/tween` (`examples/tween` in the Sindri repository)

**See:** `docs/scripting.md` (in `03-scripting-guide.md`)

**Limits:**

- No editor timeline, arbitrary component property binding, completion callback or CSS `@keyframes` surface.

<details markdown="1">
<summary>Evidence</summary>

`Tween.number`, `vec2`, `vec3` and `color` create typed handles, advanced once
before their owner's subsequent script updates. Typed value reads, pause,
resume, cancel, restart, progress/completion and disposal are exercised;
ownership, reclamation and the 8192 retained-handle bound are covered by the
runtime store tests. A tween composes on its handle: `Tween.set_delay`,
`Tween.set_loops` (0 for ever), `Tween.set_yoyo`, and `Tween.after` to hold one
until another has finished. Named easing lives in `sindri-core` and is also
used by Weave's CSS transitions.

Orbital's powerups appear with a short eased scale animation, pausing with the
game's menu state, and breathe with an endless yoyo that starts after their
appearance. The Pages feature example at `examples/tween` compares the five
curves on movement, scale, rotation and colour, with mouse/touch buttons and
keyboard playback controls; its real browser smoke observes movement and pause
on desktop and portrait viewports, and Tween Lab's sixth row crosses, waits and
returns as a sequence while it breathes.

</details>

### Language

<a id="vectors"></a>

#### Vectors as values (`Vec2`, `Vec3`)

The language's own vector types, handed out and taken back whole by the engine, with mistakes as compile errors.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Vectors as values (`Vec2`, `Vec3`)*

**Tested by:** `decay/crates/decay-runtime/src/tests/vectors.rs` (`decay/crates/decay-runtime/src/tests/vectors.rs` in the Sindri repository)

**Exercised in:** `games/scorchball` (`games/scorchball` in the Sindri repository), `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Built with `Vec2(x, y)` and `Vec3(x, y, z)`, added, subtracted, scaled, negated
and compared, with `x`/`y`/`z` components that read and assign, and `length`,
`normalized` (zero stays zero rather than NaN), `dot`, `distance` and `lerp`.
The engine hands them out and takes them back whole —
`this.transform.{position,world_position,scale}`, the same through a reference,
`Pointer.position`, `Pointer.overlay` and `Stick.direction` — through the same
per-component accessors the `.x` paths use. A vector `@export` is stored as
`[x, y]` or `[x, y, z]`. Mistakes are compile errors: a `Vec2` plus a `Vec3`, a
vector times a vector (pointing at `dot`), `length()` called, a component of a
`let`. Scorchball's ball velocity, kick, aftertouch and bot aim and Orbital
Last Stand's volley spread and bullet homing use them.

</details>

<a id="timers"></a>

#### Timers as values

`Timer` values in a script's fields run down before its `update`, so an early return cannot stop the clock.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Timers as values*

**Tested by:** `crates/sindri-decay/tests/scripts_keep_time.rs` (`crates/sindri-decay/tests/scripts_keep_time.rs` in the Sindri repository), `decay/crates/decay-runtime/src/tests/timers.rs` (`decay/crates/decay-runtime/src/tests/timers.rs` in the Sindri repository)

**Exercised in:** `games/scorchball` (`games/scorchball` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`var cooldown = Timer(0.0);`, restarted with `Timer(2.4)` and read through
`done`, `left`, `duration` and `progress`. Every timer a script's fields hold
runs down by each frame before its `update`. Scorchball's banners, power-ups,
burning, aftertouch and bot reaction times use them.

</details>

<a id="shared-functions"></a>

#### Shared helper functions across files

`shared fn` belongs to the project; every script calls it by name, checked across files.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Shared helper functions across files*

**Tested by:** `crates/sindri-decay/tests/scripts_share_functions.rs` (`crates/sindri-decay/tests/scripts_share_functions.rs` in the Sindri repository), `decay/crates/decay-semantic/src/tests/functions.rs` (`decay/crates/decay-semantic/src/tests/functions.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<a id="constants"></a>

#### Constants

`const` values are worked out at compile time; `shared const` makes one every file's.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Constants*

**Tested by:** `crates/sindri-decay/tests/scripts_share_constants.rs` (`crates/sindri-decay/tests/scripts_share_constants.rs` in the Sindri repository), `decay/crates/decay-runtime/src/tests/constants.rs` (`decay/crates/decay-runtime/src/tests/constants.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<a id="structs"></a>

#### Structs

Value types with named fields and methods, copied on assignment and authored in scenes as JSON.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Structs*

**Tested by:** `crates/sindri-decay/tests/scripts_use_structs.rs` (`crates/sindri-decay/tests/scripts_use_structs.rs` in the Sindri repository), `crates/sindri-decay/tests/scripts_use_struct_methods.rs` (`crates/sindri-decay/tests/scripts_use_struct_methods.rs` in the Sindri repository), `crates/sindri-decay/tests/scenes_author_lists_and_structs.rs` (`crates/sindri-decay/tests/scenes_author_lists_and_structs.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

A `struct` in any file is a value type every script builds with named fields
(`OfferCard(card: c, name: n, blurb: b)`), reads, writes through the `var`
holding it, and keeps in lists; it is copied where it is assigned. Functions
written after its fields are its methods, asked of one value with `this` a
read-only copy. A list or struct `@export` is authored in the scene as JSON
shaped like its type and drawn by the inspector as rows per item and per field.

</details>

<a id="lists"></a>

#### Lists and ranges

Lists change in place, are asked and walked; `for i in 0..n` walks a range; `List<T>` is what host queries return.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Lists and ranges*

**Tested by:** `decay/crates/decay-runtime/src/tests/lists.rs` (`decay/crates/decay-runtime/src/tests/lists.rs` in the Sindri repository), `decay/crates/decay-runtime/src/tests/loops.rs` (`decay/crates/decay-runtime/src/tests/loops.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**Limits:**

- A script's list is capped at 10,000 elements; loops are bounded by the operation budget.

<a id="maps"></a>

#### Maps

Maps keyed by text, numbers, flags, variants or entities, in first-set order.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Maps*

**Tested by:** `decay/crates/decay-runtime/src/tests/maps.rs` (`decay/crates/decay-runtime/src/tests/maps.rs` in the Sindri repository), `decay/crates/decay-semantic/src/tests/maps.rs` (`decay/crates/decay-semantic/src/tests/maps.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Maps are written (`["a": 1.0]`, `[:]`), read by key (`m[k]`, `get`,
`contains`, `keys()`, `values()`, `length`) and changed in place where a `var`
or field holds them (`m[k] = v`, `remove`, `clear`).

</details>

<a id="decay-text"></a>

#### Text: joining and methods

Text joins with `+` and has search, slicing, replacement, case and number formatting, capped at 64 KiB.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Text: joining and methods*

**Tested by:** `crates/sindri-decay/tests/scripts_use_text.rs` (`crates/sindri-decay/tests/scripts_use_text.rs` in the Sindri repository), `decay/crates/decay-runtime/src/tests/text.rs` (`decay/crates/decay-runtime/src/tests/text.rs` in the Sindri repository)

**Exercised in:** `games/scorchball` (`games/scorchball` in the Sindri repository), `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<a id="enums"></a>

#### Enums and exhaustive `match`

Enums are shared types, `match` must be exhaustive, and an enum `@export` is a dropdown in the inspector.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Enums and exhaustive `match`*

**Tested by:** `crates/sindri-decay/tests/scripts_use_enums.rs` (`crates/sindri-decay/tests/scripts_use_enums.rs` in the Sindri repository), `decay/crates/decay-semantic/src/tests/enums.rs` (`decay/crates/decay-semantic/src/tests/enums.rs` in the Sindri repository)

**Exercised in:** `games/scorchball` (`games/scorchball` in the Sindri repository)

<a id="optionals"></a>

#### Optional values

Nullable types such as `number?` and `Entity?` are checked before use.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Optional values*

**Tested by:** `decay/crates/decay-runtime/src/tests/optionals.rs` (`decay/crates/decay-runtime/src/tests/optionals.rs` in the Sindri repository), `decay/crates/decay-semantic/src/tests/optionals.rs` (`decay/crates/decay-semantic/src/tests/optionals.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<a id="colours"></a>

#### Colours

Colours are language values.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Colours*

**Tested by:** `decay/crates/decay-runtime/src/tests/colors.rs` (`decay/crates/decay-runtime/src/tests/colors.rs` in the Sindri repository), `decay/crates/decay-semantic/src/tests/colors.rs` (`decay/crates/decay-semantic/src/tests/colors.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<a id="language-pipeline"></a>

#### Parser, analyzer, IR and runtime

A recovering parser, a checking analyzer, a symbolic IR and a runtime behind a three-method `Host` trait, all on wasm32.

Engine ✅ · Editor — · Decay ✅ · Proof ✅

**Tested by:** `decay/crates/decay-syntax/src/tests.rs` (`decay/crates/decay-syntax/src/tests.rs` in the Sindri repository), `decay/crates/decay-semantic/src/tests/language.rs` (`decay/crates/decay-semantic/src/tests/language.rs` in the Sindri repository), `decay/crates/decay-ir/tests/language_reference.rs` (`decay/crates/decay-ir/tests/language_reference.rs` in the Sindri repository), `decay/crates/decay-runtime/tests/example_script.rs` (`decay/crates/decay-runtime/tests/example_script.rs` in the Sindri repository)

**Exercised in:** `decay/examples/player.decay` (`decay/examples/player.decay` in the Sindri repository)

**See:** `decay/LANGUAGE.md` (in `01-decay-language.md`)

**Limits:**

- The only numeric type is `number`, a 64-bit float; `f32`, its first spelling, still works.

<details markdown="1">
<summary>Evidence</summary>

`decay-syntax` lexes and parses a script, reporting diagnostics with a span,
line, and column; the parser recovers rather than stopping at the first error,
and survives two hundred thousand random token sequences without panicking or
hanging. `decay-semantic` resolves names through block scopes, checks the type
model, enforces `let` against `var`, and rejects duplicate members and locals.
Host globals such as `Input` enter through an `Environment` rather than being
builtins.

`decay-ir` lowers a checked program to a symbolic instruction list, with member
chains becoming paths such as `this.transform.position.x` rather than anything
the IR interprets. `decay-runtime` executes it. Everything external crosses a
three-method `Host` trait — load a path, store a path, call a path — each
taking a subject: `None` for a path rooted at something the host owns, and the
reference for one rooted at a value the script is holding. The whole workspace
compiles for `wasm32-unknown-unknown`, which its CI checks.
`decay/examples/player.decay` is executed by a test.

</details>

### Tooling

<a id="language-server"></a>

#### Language server: highlight, complete, hover, definitions, diagnostics

The VS Code extension starts the real `decay-lsp` at the project root, with project-aware diagnostics, completion and hover.

Engine — · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Language server: highlight, complete, hover, definitions, diagnostics*

**Tested by:** `tools/decay-lsp/src/lifecycle_tests.rs` (`tools/decay-lsp/src/lifecycle_tests.rs` in the Sindri repository), `tools/decay-lsp/src/protocol.rs` (`tools/decay-lsp/src/protocol.rs` in the Sindri repository)

**Exercised in:** `editors/vscode-decay` (`editors/vscode-decay` in the Sindri repository)

**See:** `docs/decay-lsp-modernization.md` (`docs/decay-lsp-modernization.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

The official VS Code extension registers `.decay`, highlights the current
language, and supplies comments, pairs and indentation. It starts the real
`decay-lsp` at the nearest Sindri project root, so diagnostics, completion,
hover and document symbols use the project's script types, events and shared
state. The Sindri project browser opens scripts in VS Code with that root;
saving uses the editor's existing watched compile/reload path.
`docs/decay-lsp-modernization.md` is the active checklist for what remains.

</details>

### Not yet

<a id="formatter"></a>

#### Formatter

There is no Decay formatter.

Engine ❌ · Editor ❌ · Decay ❌ · Proof — — parity row *Formatter*

<a id="debugger"></a>

#### Debugger (breakpoints, stepping)

There is no Decay debugger.

Engine ❌ · Editor ❌ · Decay ❌ · Proof — — parity row *Debugger (breakpoints, stepping)*

<a id="coroutines"></a>

#### Coroutines / sequencing

Scripts cannot suspend; sequencing is written with timers and state.

Engine ❌ · Editor — · Decay ❌ · Proof — — parity row *Coroutines / sequencing*

<a id="script-unit-tests"></a>

#### Script unit tests

There is no way to write a test in Decay itself.

Engine ❌ · Editor ❌ · Decay ❌ · Proof ❌ — parity row *Script unit tests*

<a id="closures"></a>

#### Sets, closures and first-class functions

The language has no sets, closures or first-class functions.

Engine — · Editor — · Decay ❌ · Proof —

<a id="typescript-sdk"></a>

#### TypeScript embedding SDK

Browser games expose narrow application entry points and run their gameplay in Decay; there is no optional TypeScript SDK.

Engine ❌ · Editor — · Decay — · Proof —

<a id="generic-fractional-component-writes"></a>

#### Fractional component writes from integer-authored numbers

A generic numeric leaf the scene wrote as an integer rounds a script's write, so a fractional 2D light intensity can be lost; color and shader-parameter channels no longer round

Engine ❌ · Editor — · Decay ❌ · Proof ❌ — parity row *Fractional component writes from integer-authored numbers*

<details markdown="1">
<summary>Evidence</summary>

A generic numeric leaf the scene wrote as an integer rounds a script's write, so a fractional 2D light intensity can be lost. A channel reached by index -- a color's `a`, a shader parameter -- is always a fraction and never rounds, so a tint authored as `[1, 1, 1, 0]` fades. Checked 3D light/environment setters use dedicated floating-point storage; other leaves still need declared storage semantics (`sindri-decay/host/access.rs`)

</details>

<a id="quaternion-operations"></a>

#### Quaternion composition and interpolation

No quaternion multiplication, axis-angle value constructor or slerp API; existing Euler and world-space aim/orbit controls remain available

Engine ❌ · Editor — · Decay ❌ · Proof ❌ — parity row *Quaternion composition and interpolation*

**See:** `docs/scripting.md` (in `03-scripting-guide.md`)

<details markdown="1">
<summary>Evidence</summary>

No quaternion multiplication, axis-angle value constructor or slerp API; existing Euler and world-space aim/orbit controls remain available

</details>

### 3D transform values

<a id="rotations"></a>

#### Rotations as values

Copied parent-relative Quat values support normalized atomic whole writes; composition and interpolation remain absent.

Engine 🟡 · Editor — · Decay 🟡 · Proof 🟡 — parity row *Rotations as values*

**Tested by:** `crates/sindri-decay/tests/spatial_transforms/quaternion.rs` (`crates/sindri-decay/tests/spatial_transforms/quaternion.rs` in the Sindri repository)

**Exercised in:** `examples/transform` (`examples/transform` in the Sindri repository)

**See:** `docs/scripting.md` (in `03-scripting-guide.md`)

<details markdown="1">
<summary>Evidence</summary>

parent-relative `Quat` x/y/z/w value, using
Decay's existing struct constructor and copy semantics. Whole writes normalize
finite nonzero values atomically; zero/non-finite writes retain the transform.
Stored components are readable; edit a copied local value and assign it back.
Script regressions cover huge/tiny magnitudes, invalid input, parented rotation,
copy independence and runtime rejection of direct component writes. Transform
Lab saves its rotation, transfers its facing to the goal and restores its original
on arrival through Session and exported Chromium. Generated host metadata exposes
the type to the checker and LSP. This is a general transform capability added to
finish the planned 3D scripting surface; Explorer game proof remains pending.

</details>

---

Source: `docs/capabilities/input-and-audio.md`

## Input and audio

Platform-independent input — keyboard, mouse, touch, gamepads and named
actions — and audio playback through a mixer of named buses.

Each entry links its tests and the projects that exercise it. Surface marks match the row in parity (`docs/parity.md` in the Sindri repository) where one is named: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — not applicable.

| Capability | Engine | Editor | Decay | Proof |
| --- | :-: | :-: | :-: | :-: |
| **Input** | | | | |
| Keyboard, mouse, touch behind the platform boundary | ✅ | ✅ | ✅ | ✅ |
| Unified pointer, bounded fingers | ✅ | ✅ | ✅ | ✅ |
| Gamepad, by player slot | ✅ | 🟡 | ✅ | ✅ |
| Action mapping (named actions, rebindable) | ✅ | 🟡 | ✅ | ✅ |
| **Audio** | | | | |
| WAV / Ogg / MP3, native + browser + silent backends | ✅ | ✅ | ✅ | ✅ |
| Buses / mixer / master volume | ✅ | 🟡 | ✅ | ✅ |
| Sound effects made in the Sound Lab | — | ❌ | — | ✅ |
| **Not yet** | | | | |
| `referenced_audio` gather | ❌ | — | — | — |
| Procedural music authoring and MIDI note editing | ❌ | ❌ | ❌ | ❌ |
| **3D foundation** | | | | |
| Mouse displacement for 3D look | 🟡 | 🟡 | 🟡 | 🟡 |
| Pointer lock | 🟡 | 🟡 | 🟡 | 🟡 |

### Input

<a id="keyboard-mouse-touch"></a>

#### Keyboard, mouse, touch behind the platform boundary

Platform-independent events accumulate into an `InputState` of held, pressed and released keys and buttons.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Keyboard, mouse, touch behind the platform boundary*

**Tested by:** `crates/sindri-platform/src/input/state_tests.rs` (`crates/sindri-platform/src/input/state_tests.rs` in the Sindri repository), `crates/sindri-decay/tests/input_gathers_every_way_in.rs` (`crates/sindri-decay/tests/input_gathers_every_way_in.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Keyboard, mouse, and touch input arrive as platform-independent events and
accumulate into an `InputState` that answers held, pressed-this-frame, and
released-this-frame for keys and mouse buttons, plus pointer position, pointer
delta, scroll delta, and window focus. `axis(negative, positive)` returns -1,
0, or 1 and gives zero when both are held, so opposed movement keys cannot
resolve by event order.

The editor routes all of it through the Game view during Play, in **that
view's own pixels**, so a script reads the same position there as in the real
build. A pointer outside the view is reported as gone rather than clamped to
its edge, because a game told the person is pointing at somewhere they are not
is worse than a game told they are not pointing at all.

</details>

<a id="unified-pointer"></a>

#### Unified pointer, bounded fingers

Touch is kept beside the mouse; `pointer_position` and `pointer_down` unify them where a game reads them.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Unified pointer, bounded fingers*

**Tested by:** `crates/sindri-platform/src/input/state_tests.rs` (`crates/sindri-platform/src/input/state_tests.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_reads_the_pointer.rs` (`crates/sindri-decay/tests/a_script_reads_the_pointer.rs` in the Sindri repository), `game/tests/the_game_is_played/rotation.rs` (`game/tests/the_game_is_played/rotation.rs` in the Sindri repository), `scripts/browser/causeway-rotation.mjs` (`scripts/browser/causeway-rotation.mjs` in the Sindri repository)

**Exercised in:** `examples/physics` (`examples/physics` in the Sindri repository), `game` (`game` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Touch is held beside the mouse rather than folded into it, because they are
different facts: a mouse has one position and is always somewhere, while
fingers arrive and leave and there may be several. Fingers are bounded at ten,
ordered by the id their host gave them so one keeps its place while it stays
down, and let go of when a window loses focus — a finger cannot be reported as
lifted once the window has stopped hearing about it. What *unifies* the two is
a separate question and is answered where a game reads it: `pointer_position`
follows the first active finger, retains its final position through the release
frame, and otherwise follows the mouse. A parked mouse cannot mask touch on a
hybrid device, and
`pointer_down(Left)` is the left button or any finger.

</details>

<a id="gamepads"></a>

#### Gamepad, by player slot

Desktop and browser gamepads, read by player slot with join/leave edges, positional buttons and round dead zones.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Gamepad, by player slot*

**Tested by:** `crates/sindri-platform/src/input/gamepad_tests.rs` (`crates/sindri-platform/src/input/gamepad_tests.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_reads_a_gamepad.rs` (`crates/sindri-decay/tests/a_script_reads_a_gamepad.rs` in the Sindri repository)

**Exercised in:** `games/scorchball` (`games/scorchball` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Gamepads are read on desktop and in the browser (`GamepadReader`, over
`gilrs`, behind the platform's default `gamepad` feature, which links libudev
on Linux the way audio links ALSA). Every host that draws a window asks for
them once a frame, and so does the editor's Play. A game reads pads by **player
slot** rather than by device: a face button or Start on an unclaimed pad claims
the first free slot, a slot stays claimed while its pad is connected and is
given back when it goes, and joins and leaves are one-frame edges that never
name two slots at once, so a second pad pressing in the same frame joins the
next. The press that claims a slot is not reported through it, so joining does
not also jump. Slot 0 reads every pad, for a game with one player. Buttons are
named by position (`south`, `right_bumper`, `dpad_up`), sticks read in screen
axes with a round dead zone taken out, and focus loss lets go of every button
without dropping a player. Scripts read `Gamepad`; an action binding reads any
pad as `gamepad.south` or `gamepad.axis.left_x`.

</details>

<a id="input-actions"></a>

#### Action mapping (named actions, rebindable)

A scene declares named actions; scripts read them by name and rebind them while the game runs.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Action mapping (named actions, rebindable)*

**Tested by:** `crates/sindri-platform/src/input/action/map.rs` (`crates/sindri-platform/src/input/action/map.rs` in the Sindri repository), `crates/sindri-platform/src/input/action/state.rs` (`crates/sindri-platform/src/input/action/state.rs` in the Sindri repository), `games/platformer/tests/a_run_reaches_the_flag.rs` (`games/platformer/tests/a_run_reaches_the_flag.rs` in the Sindri repository), `game/tests/the_mixer_and_input_demos_work.rs` (`game/tests/the_mixer_and_input_demos_work.rs` in the Sindri repository), `games/orbital-baked/tests/the_complete_game.rs` (`games/orbital-baked/tests/the_complete_game.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `examples/input` (`examples/input` in the Sindri repository), `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**Limits:**

- Actions are per scene, not per project or per player.
- No automatic binding persistence; a game can store and restore a binding with Save.text.

<details markdown="1">
<summary>Evidence</summary>

Input actions are declared by a scene's `sindri.input.actions` component, in
the actions-document shape `ActionMap` reads. The script runner reads the
declaration when it changes and every action's value from the step's input
before any script runs; `Action.held`, `pressed`, `released`, `axis` and
`vector` answer by name, and `Action.bindings`, `Action.rebind` and
`Action.last_pressed` rebind one while the game runs, written back into the
component. The platformer's hero runs and jumps by actions, and
`examples/input` rebinds its boost (also covered by the browser smoke).
Orbital Last Stand rebinds burst in its settings and saves the chosen binding
through `Save.text`, restoring it when the next session opens.

</details>

### Audio

<a id="audio-backends"></a>

#### WAV / Ogg / MP3, native + browser + silent backends

Validated audio assets play through Rodio/CPAL natively, Web Audio in the browser, and a recording silent backend in tests.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *WAV / Ogg / MP3, native + browser + silent backends*

**Tested by:** `crates/sindri-platform/src/audio/tests.rs` (`crates/sindri-platform/src/audio/tests.rs` in the Sindri repository), `editor/src/play_audio/tests.rs` (`editor/src/play_audio/tests.rs` in the Sindri repository), `scripts/browser/smoke.mjs` (`scripts/browser/smoke.mjs` in the Sindri repository)

**Exercised in:** `examples/audio` (`examples/audio` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Audio is a platform service rather than simulation state. Encoded WAV, Ogg, and
MP3 assets are identified and validated by the asset layer, then registered
with an `AudioBackend`. Native hosts use Rodio/CPAL; browser hosts decode each
clip once into a Web Audio buffer and play it through one `AudioContext`,
unlocked by the first key, click or touch — resumed inside the gesture's own
handler, which iOS Safari requires, and on a finger lifting as well as landing,
so a phone player holding the platformer's stick still hears it. Headless tests
use a silent backend that records every request without needing a sound device. The
shared boundary supports one-shot and looping playback plus per-voice stop and
global pause, resume, and stop.

Scenes can author `sindri.audio.source` with a logical clip ID, autoplay,
looping, and a normalized volume. The generic component inspector can add and
edit it, and the project browser recognises WAV/Ogg/MP3 files as audio assets.
Decay exposes typed `Audio.play`, `loop`, `stop_all`, `pause_all`, and
`resume_all` calls while only emitting intent, so the language itself retains
its no-I/O boundary. Gather exercises the path end to end with background,
pickup, and victory sounds. The looping background music has been observed
playing in a real browser through `scripts/browser/smoke.mjs`, which fails if a
clip a page asked for did not play. The editor can audition a selected clip
through its own audio backend.

</details>

<a id="audio-buses"></a>

#### Buses / mixer / master volume

Every voice plays through a named bus under `master`; scripts move and read bus volumes.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Buses / mixer / master volume*

**Tested by:** `crates/sindri-platform/src/audio/mixer.rs` (`crates/sindri-platform/src/audio/mixer.rs` in the Sindri repository), `games/orbital-baked/tests/the_mixer.rs` (`games/orbital-baked/tests/the_mixer.rs` in the Sindri repository), `game/tests/the_mixer_and_input_demos_work.rs` (`game/tests/the_mixer_and_input_demos_work.rs` in the Sindri repository), `games/orbital-baked/tests/the_complete_game.rs` (`games/orbital-baked/tests/the_complete_game.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository), `examples/audio` (`examples/audio` in the Sindri repository)

**Limits:**

- There are no bus effects, ducking or snapshots.

<details markdown="1">
<summary>Evidence</summary>

`AudioMixer` in `sindri-platform` sits in front of any backend: it starts a
voice at its own volume times its bus's times the master's, and
`set_bus_volume` re-applies the gain to every live voice the bus reaches, which
each backend supports (`set_volume`, `is_active`). Decay's `Audio.play` routes
to `effects`, `Audio.loop` to `music`, `play_on`/`loop_on` to any bus, and
`Audio.set_volume` and `Audio.volume` move and read a bus; an authored source
names its `bus` or follows its looping. The game session routes scripts'
requests and autoplay through the mixer. Orbital's pause screen has Master,
Music and Effects sliders, saved between sessions, and `examples/audio` is the
Sound Mixer feature example.

</details>

<a id="sound-lab"></a>

#### Sound effects made in the Sound Lab

A standalone tool designs seeded game sound effects and delivers each into a project, listed for export, with the line that plays it.

Engine — · Editor ❌ · Decay — · Proof ✅ — parity row *Sound effect authoring (Sound Lab)*

**Tested by:** `tools/sindri-chiptune/src/lab/sfx/tests.rs` (`tools/sindri-chiptune/src/lab/sfx/tests.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/project.rs` (`tools/sindri-chiptune/src/lab/project.rs` in the Sindri repository), `games/platformer/tests/a_run_reaches_the_flag.rs` (`games/platformer/tests/a_run_reaches_the_flag.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `tools/sindri-chiptune/README.md` (`tools/sindri-chiptune/README.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`tools/sindri-chiptune` (the Sindri Sound Lab) has a Sound FX designer beside
its music workstation. Eight kinds — coin, jump, laser, explosion, power-up,
hurt, blip and thud — are seeded regions of one synth voice: a wave, an
attack/sustain/punch/decay envelope, a pitch path with slide, curve, vibrato,
arpeggio and repeat, a square duty sweep, low- and high-pass filters and a
sample-and-hold crush. The same kind and seed always render the same samples.
Every parameter is a slider with a unit, a change replays the sound, and
**Mutate** nudges each by a few percent.

**Send to project** writes `assets/audio/<name>.wav` and the editable
`<name>.sindrisfx` beside it, adds the clip to `sindri.toml`'s
`[assets] include` (editing the manifest in place, comments kept), and offers
`Audio.play("audio/<name>.wav", 0.5);` to paste. Without that include line a
clip a script plays is found by the editor and silently left out of an
export. The same delivery runs headless:
`sindri-chiptune sfx coin --seed 5 --project games/platformer --name coin`.

The platformer's hero plays four sounds made this way — jump, landing, coin
and the flag — and its scripted run to the flag asserts it heard each one and
that each is listed for export. The tool's own tests re-render every committed
`.sindrisfx` and compare it with the WAV beside it, so reopening one edits
what ships.

The same Lab runs in a browser at `/examples/sound-lab/`, built by Pages with
wasm-pack and drawn with WebGL2. There **Send** becomes **Download** of the
same two files with the lines to add, and under 720 points wide the window
folds into one column for a phone. `scripts/browser/sound-lab.mjs` opens it at
desktop and phone sizes in both modes on every Pages build. The tool is not
part of the editor.

</details>

### Not yet

<a id="referenced-audio"></a>

#### `referenced_audio` gather

Nothing gathers the clips a scene names, so dynamically named clips must be listed in `[assets].include`.

Engine ❌ · Editor — · Decay — · Proof — — parity row *`referenced_audio` gather*

**Limits:**

- The editor does not preview a scene's audio source in place.

<a id="procedural-music-authoring"></a>

#### Procedural music authoring and MIDI note editing

Available in the standalone Sound Lab, with no engine, editor panel, or Decay composition API.

Engine ❌ · Editor ❌ · Decay ❌ · Proof ❌ — parity row *Procedural music authoring / MIDI note editing*

**Tested by:** `tools/sindri-chiptune/src/lab/composer/tests.rs` (`tools/sindri-chiptune/src/lab/composer/tests.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/composer/musical/tests.rs` (`tools/sindri-chiptune/src/lab/composer/musical/tests.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/synth/harmony.rs` (`tools/sindri-chiptune/src/lab/synth/harmony.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/song.rs` (`tools/sindri-chiptune/src/lab/song.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/gui/seed.rs` (`tools/sindri-chiptune/src/lab/gui/seed.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/ensemble/tests.rs` (`tools/sindri-chiptune/src/lab/ensemble/tests.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/seed.rs` (`tools/sindri-chiptune/src/lab/seed.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/synth/participation.rs` (`tools/sindri-chiptune/src/lab/synth/participation.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/gui/tests.rs` (`tools/sindri-chiptune/src/lab/gui/tests.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/gui/tools/tests.rs` (`tools/sindri-chiptune/src/lab/gui/tools/tests.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/gui/tools/harmony.rs` (`tools/sindri-chiptune/src/lab/gui/tools/harmony.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/phrases.rs` (`tools/sindri-chiptune/src/lab/phrases.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/development/tests.rs` (`tools/sindri-chiptune/src/lab/development/tests.rs` in the Sindri repository), `tools/sindri-chiptune/src/lab/gui/development/tests.rs` (`tools/sindri-chiptune/src/lab/gui/development/tests.rs` in the Sindri repository)

**See:** `tools/sindri-chiptune/README.md` (`tools/sindri-chiptune/README.md` in the Sindri repository)

**Limits:**

- Standalone authoring only; no in-engine or Decay composition API.
- MIDI exports the three monophonic note parts, not Sound Lab synth patches or the generated drum/chord layers.

<details markdown="1">
<summary>Evidence</summary>

New Sound Lab sessions use versioned Musical composition: a theme-derived Rise,
modal eight-bar preparation/resolution, register-aware arpeggios that simplify
under busy leads, and individual-voice chord inversion selection. Pad harmony
follows bar roots instead of passing mid-bar bass tones. Preview and playback
share the same deterministic voicing path. Saved sources and snapshots retain
the composer version; missing fields select Legacy, which retains previous note
and half-bar pad rules. The Song selector and CLI --generation legacy expose
that path. Unknown versions are refused. Existing note locks preserve the
composer version along with seed, key and mood. These rules have seed-sweep
and compatibility tests, not a claim that music quality is objectively solved.

Sound Lab composes and edits a deterministic 32-bar score. Ensemble version 1
uses a separate seeded stream to choose coordinated instrument presets. All layers
are enabled in every section by default, preserving the note composer's natural
phrases. Section rests are manual choices; masks are saved explicitly, while
legacy sources without masks retain their full-layer mix. Smooth gain changes preserve effect tails, and MIDI clips melodic notes at
section rests without altering the editable source.

The workstation preserves notes, sound presets and layer participation
independently. A/B candidates retain their own mixes; manual mixing and tuning
survive generation. Song seeds accept the existing numeric values or UTF-8
phrases via fixed FNV-1a/64 hashing. Saved candidate A phrases are checked against
their numeric seed; candidate B saves its exact incremented numeric seed. The
random button uses OS entropy natively and browser Crypto on the web. The CLI
also supports --random-seed and --classic, and --open preserves saved choices.

The standalone workstation also regenerates a selected bar range without
changing surrounding notes, auditions coordinated ensembles while preserving
mixes and section choices, inspects selected notes against the written harmony,
and stores up to 16 named snapshots with the song. Snapshot restore and sound
changes use the same undo/redo history as score edits. Phrase replacements
follow edited bass roots; harmony inspection describes written notes rather
than diagnosing the rendered audio.

Explicit musical development actions copy, sequence, invert or stretch motifs;
arrange Arpeggio/Lead call-and-response pairs while preserving Bass; and shape
seeded velocity, phrase dynamics and note lengths without moving onsets. Track
locks and Undo/Redo protect edits. Source rhythm and melodic contours also seed
related Exploration, Danger and Victory snapshots with adapted scales, tempo,
dynamics and coordinated instruments. Family creation is atomic, uses existing
snapshot persistence, and leaves the current candidates unchanged. Expression
is quantized to the sixteenth-note grid; separate cues do not provide an
adaptive music playback API.

These are standalone-tool capabilities. The parity row remains absent in the
engine and editor until composition is available on those surfaces.

</details>

### 3D foundation

<a id="mouse-look-delta"></a>

#### Mouse displacement for 3D look

Read-only `Pointer.delta` / `Input.Pointer.delta` returns accumulated viewport-pixel mouse movement once per fixed step, positive right/down

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *Mouse displacement for 3D look*

**Tested by:** `crates/sindri-decay/tests/a_script_reads_the_pointer.rs` (`crates/sindri-decay/tests/a_script_reads_the_pointer.rs` in the Sindri repository)

**Exercised in:** `examples/orbit` (`examples/orbit` in the Sindri repository)

**See:** `docs/scripting.md` (in `03-scripting-guide.md`)

<details markdown="1">
<summary>Evidence</summary>

`Pointer.delta` and `Input.Pointer.delta` expose the existing platform mouse
movement accumulator as a read-only `Vec2`, including component reads. Movement
is in viewport pixels, positive right/down, spent once per fixed step; a first
arrival/re-entry adds no jump. It is displacement rather than velocity, and
independent of touch (`Gesture` remains the touch drag surface). Compiler and
runtime reject writes, while a copied vector stays mutable. Bridge regressions
exercise accumulated movement, reset, re-entry and both namespaces; LSP tests
check completion/hover from the same host metadata.

Orbit Camera Lab uses it for right-button drag-to-look, with sensitivity and
pitch limits in Decay. Its Session regression checks yaw/pitch changes, no
repeated displacement on the next step and pitch clamping. Real WebGPU
Chromium drove a right-button drag through the exported generic player; a
console observer in a disposable copy recorded exactly 60 pixels right and 40
down, once. Assets loaded, the walker reached its goal and inspected captures
showed the camera response without runtime/GPU errors. The flat-color lab used
the same local eight-color smoke threshold as its earlier capture (fourteen
colors drawn); the repository browser gate is unchanged. This is feature
example evidence; native editor Play also exercises captured delta. Explorer
game proof remains pending.
Native/browser capture is described below.

</details>

<a id="pointer-lock"></a>

#### Pointer lock

`Pointer.lock()` / `unlock()` and read-only `locked`, also under `Input.Pointer`, connect Session to native/browser and native editor Play capture

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *Pointer lock*

**Tested by:** `crates/sindri-platform/tests/pointer_motion.rs` (`crates/sindri-platform/tests/pointer_motion.rs` in the Sindri repository), `scripts/browser/pointer-lock.mjs` (`scripts/browser/pointer-lock.mjs` in the Sindri repository)

**Exercised in:** `examples/orbit` (`examples/orbit` in the Sindri repository)

**See:** `docs/platform-host.md` (`docs/platform-host.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

The shared platform input state accepts `PointerLockChanged` as actual host
capture feedback and `PointerMotion` as relative mouse displacement. These are
platform events, not Decay APIs or capture requests. Relative motion is accepted
only while captured and focused; cursor warps and leave events do not add motion
or move/cancel UI presses during capture. Changing capture mode discards pending
motion and rebases the next absolute position. Focus loss clears capture, pending
motion and the absolute mouse position; focus regain does not recapture.
Nonfinite motion and overflowing sums are rejected atomically.

`crates/sindri-platform/tests/pointer_motion.rs` exercises these transitions,
invalid input and once-only consumption through the real `EngineHost` fixed-step
loop, including frames with no step and catchup steps. Native/browser player
hosts now emit this feedback and motion, as described below.

`Pointer.lock()` / `Input.Pointer.lock()` request capture; `unlock()` requests
release and read-only `locked` reports actual state. No request changes state
within an invocation. Successful invocation outputs collect requests in execution
order (last request wins), Session queues the latest across fixed steps, and
`DesktopApp` drains it once after update. Failed invocations do not forward their
capture intent; checkpoint restore does not replay window commands.

The windowed host uses native Locked, with X11 confinement plus raw device
motion as a fallback. Browser capture calls are caught at the JavaScript boundary
and Promise rejection is consumed; actual document lock is polled before gameplay
and relative motion. Delayed grants after explicit release are released again.
Escape, focus loss, suspension, hidden pages, failure and normal exit release
capture. Returning focus does not recapture. Motion uses native raw device counts
or browser physical pixels, positive right/down, through the existing delta API.

Bridge regressions cover actual-state reads, both namespaces, read-only writes,
invalid call signatures and failed-invocation intent. Orbit Camera Lab's Session
regression covers request draining, relative camera motion and checkpoint restore.
LSP tests cover read-only boolean metadata and capture-call completion; generated
reference/metadata share the same typed host surface.

`scripts/browser/pointer-lock.mjs` runs the exported lab in real WebGPU Chromium:
a trusted click captures, movement beyond the viewport reaches Decay as exactly
1,420 horizontal/60 vertical units once, U and Escape release, and a sandboxed
frame denies capture without false success or interrupting the goal run. The
captured-look screenshot was inspected. CI exports the same project and runs
this regression, retaining its screenshot. Its flat-color fixture uses an
eight-color minimum; the general browser smoke threshold remains unchanged.
Native X11 verification runs the same generic player in an isolated Xvfb
window on Vulkan, reaches the goal, and drives capture/raw motion/U/Escape/focus
changes with XTest. Decay observes 4,000 horizontal/120 vertical raw units in
one step, releases on each requested/focus transition and does not recapture
on focus regain. The native window screenshot was inspected alongside Chromium.
Native platforms other than this X11 run were not visually exercised here.
Native editor Play also drains Session requests after stepping and editor
transport/layout changes. Native window calls report success before input sees
capture; missing windows cannot report false success. egui raw mouse events
feed native counts without display scaling; the shared state ignores absolute
warps. Unit regressions cover accumulation across frames, once-only consumption,
release rebasing, stopping and absent windows. The actual X11 editor window runs
Orbit Camera Lab to its goal, reports 6,000 horizontal/120 vertical raw units
from an unbounded XTest move, and releases on U/Escape, pause/stop, focus loss
and hiding the Game view. An external pointer-grab probe verifies actual capture
and release; resume/focus regain/new Play do not automatically recapture. The
editor screenshot with Decay motion and goal output was inspected. Other native
editor platforms and Explorer game proof remain pending.

</details>

---

Source: `docs/capabilities/physics.md`

## Physics

`sindri-physics` is the masked physics boundary. Rapier2D and Rapier3D are
private implementation dependencies; public code speaks only in Sindri body,
collider, shape, layer, pose, error, and event types. Runtime entities remain
the identity exposed by the subsystem, so no Rapier handle can leak into a
scene, script, editor, or game contract.

The exercised runtime is 2D. A parallel 3D world, scene driver and typed Decay
surface exist and are exercised by fixtures, but no shipped game yet makes a
gameplay decision with them. The contract is `docs/physics.md`.

Each entry links its tests and the projects that exercise it. Surface marks match the row in parity (`docs/parity.md` in the Sindri repository) where one is named: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — not applicable.

| Capability | Engine | Editor | Decay | Proof |
| --- | :-: | :-: | :-: | :-: |
| **2D bodies and colliders** | | | | |
| Bodies, fixed-step stepping, velocity, impulse | ✅ | 🟡 | ✅ | ✅ |
| Masks, sensors, collision events | ✅ | 🟡 | ✅ | ✅ |
| Named collision layers | ✅ | ✅ | ✅ | ✅ |
| Compound colliders | ✅ | ✅ | — | ✅ |
| Tilemap collision | ✅ | 🟡 | — | ✅ |
| Scene gravity | ✅ | ✅ | — | ✅ |
| Moving a body by its transform | ✅ | — | ✅ | ✅ |
| Forces and rotation (2D) | 🟡 | 🟡 | 🟡 | 🟡 |
| Solid contact snapshots (2D) | ✅ | 🟡 | ✅ | ✅ |
| One-way platforms | ✅ | 🟡 | ✅ | ✅ |
| Physics materials as assets (2D) | ✅ | 🟡 | — | ✅ |
| Collider gizmos in the Scene view | — | ✅ | — | — |
| Continuous collision (CCD) | ✅ | 🟡 | ✅ | ✅ |
| **2D character controller** | | | | |
| Sweep, slide, ground, step and carry queries | ✅ | — | — | ✅ |
| Character controller (2D) | ✅ | ✅ | ✅ | ✅ |
| **2D queries** | | | | |
| 2D closest-hit raycasts | ✅ | — | ✅ | ✅ |
| Overlap and shape casts (2D) | ✅ | — | ✅ | ✅ |
| Accelerated 2D physics queries | ✅ | — | ✅ | ✅ |
| **2D joints** | | | | |
| Joints and constraints | 🟡 | 🟡 | 🟡 | ✅ |
| Typed joint controls from Decay | ✅ | — | ✅ | ✅ |
| Hinge and slider position motors | ✅ | ✅ | ✅ | ✅ |
| Authored joint references in prefabs | 🟡 | — | 🟡 | ✅ |
| Saved spawned-prefab reference remapping | ✅ | ✅ | — | ✅ |
| **3D** | | | | |
| Standalone 3D physics world | ✅ | — | — | ❌ |
| 3D physics in scenes | ✅ | 🟡 | 🟡 | ❌ |
| 3D spawn-window controls | 🟡 | ❌ | 🟡 | ❌ |
| 3D physics queries | ✅ | ✅ | ✅ | 🟡 |
| 3D replaceable static geometry groups | 🟡 | ❌ | ❌ | ❌ |
| **3D physics foundation** | | | | |
| Automatic 3D collider scaling | 🟡 | 🟡 | 🟡 | 🟡 |
| Character controller (3D) | 🟡 | ✅ | 🟡 | 🟡 |
| **Not yet** | | | | |
| Extreme 3D query numerical failure reporting | ❌ | — | — | ❌ |
| Script-triggered world snapshot save/load | ❌ | — | ❌ | ❌ |
| Compound solid character probes | ❌ | — | ❌ | ❌ |
| Same-step solver response to controller motion | ❌ | — | ❌ | ❌ |
| Swept controller trigger events | ❌ | — | ❌ | ❌ |
| Sweeping rotating platform carry along its arc | ❌ | — | ❌ | ❌ |
| Sweeping a rotating character probe | ❌ | — | ❌ | ❌ |
| Carry on a scene-owned 3D character | ❌ | — | ❌ | ❌ |
| **3D foundation** | | | | |
| 3D physics | ✅ | ✅ | ✅ | ✅ |
| Physics materials as assets (3D) | 🟡 | 🟡 | — | 🟡 |
| Static triangle mesh colliders (3D) | 🟡 | 🟡 | ❌ | 🟡 |
| Convex hull colliders (3D) | 🟡 | ❌ | ❌ | 🟡 |
| Convex model collision project loading | 🟡 | 🟡 | 🟡 | 🟡 |
| Pre-synchronization controls for convex-only bodies | 🟡 | 🟡 | 🟡 | 🟡 |

### 2D bodies and colliders

<a id="bodies-2d"></a>

#### Bodies, fixed-step stepping, velocity, impulse

Fixed, dynamic and two kinds of kinematic body, stepped on the engine's fixed update with validated velocity and impulse controls.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Bodies, fixed-step stepping, velocity, impulse*

**Tested by:** `crates/sindri-decay/tests/a_script_drives_a_body.rs` (`crates/sindri-decay/tests/a_script_drives_a_body.rs` in the Sindri repository), `crates/sindri-physics/tests/copies_go_on_as_the_original.rs` (`crates/sindri-physics/tests/copies_go_on_as_the_original.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository), `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/physics.md` (in `07-physics-and-movement.md`)

<details markdown="1">
<summary>Evidence</summary>

`PhysicsWorld2d` owns fixed, dynamic, position-kinematic, and
velocity-kinematic bodies; box, circle, and capsule colliders; friction and
restitution; and checked velocity/impulse/kinematic operations. Values are
validated before they reach the backend, and stepping takes an explicit engine
fixed-step duration rather than a render delta. Tests cover gravity, body
operations, and removal/reuse through generation-checked `EntityId`s, and the
crate passes the workspace's native and WASM checks.

`sindri.physics2d.rigid_body` and `sindri.physics2d.collider` are registered
scene components with defaults the engine accepts, so a scene authors bodies
and colliders and the editor's generic component inspector adds and edits them.

</details>

<a id="masks-sensors-events"></a>

#### Masks, sensors, collision events

Independent membership and filter masks, sensor colliders, and normalized collision/sensor start and stop events.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Masks, sensors, collision events*

**Tested by:** `games/orbital-baked/tests/asteroid_physics.rs` (`games/orbital-baked/tests/asteroid_physics.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`games/orbital-baked` is the end-to-end proof: player, enemies, projectiles,
pickups, and effects use distinct masks and collision or sensor events
continuously.


Physics Playground checks existence before consuming 2D last-solve references in shell/pane/backstop/bumper scripts. A reset-after-contact project regression and desktop/phone browser smoke exercise cleanup; copied event references can outlive their entities

</details>

<a id="named-layers"></a>

#### Named collision layers

A scene names its collision layers bit by bit; scripts and the inspector use the names.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Named collision layers*

**Tested by:** `crates/sindri-decay/tests/a_script_names_its_layers.rs` (`crates/sindri-decay/tests/a_script_names_its_layers.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `examples/physics` (`examples/physics` in the Sindri repository)

**Limits:**

- Named per scene rather than in project settings; colliders still store masks.

<details markdown="1">
<summary>Evidence</summary>

A scene's `sindri.physics2d.world` names its collision layers bit by bit.
`Physics.layer` and `Physics.mask` turn names into masks and refuse an unknown
name, and the editor's inspector draws every collision mask as a menu of the
named layers, keeping unnamed bits. The platformer names ground, hero and
pickups; its hero's ground probe asks for `ground`.

</details>

<a id="compound-colliders"></a>

#### Compound colliders

One body made of several collider pieces, each fully editable in the inspector.

Engine ✅ · Editor ✅ · Decay — · Proof ✅ — parity row *Compound colliders (several pieces, one body)*

**Tested by:** `editor/tests/inspector_field_meanings.rs` (`editor/tests/inspector_field_meanings.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

The generic inspector exposes the body and collider payloads — a compound's
pieces are added, removed, reordered, and edited down to each piece's shape.
The platformer's hero is one body of two pieces, a capsule and a box.

</details>

<a id="tilemap-collider"></a>

#### Tilemap collision

Painted tiles become solid, merged into as few rectangles as cover them so nothing catches on tile seams.

Engine ✅ · Editor 🟡 · Decay — · Proof ✅ — parity row *Tilemap collision*

**Tested by:** `crates/sindri-scene/src/tilemap_collision.rs` (`crates/sindri-scene/src/tilemap_collision.rs` in the Sindri repository), `games/platformer/tests/painted_ground.rs` (`games/platformer/tests/painted_ground.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**Limits:**

- An isometric map is refused by name.
- Passable sprites are typed as names rather than picked from the palette.

<details markdown="1">
<summary>Evidence</summary>

`sindri.physics2d.tilemap_collider` makes the orthogonal tilemap on the same
entity solid: every painted tile collides unless its palette sprite is listed
as `passable`. Solid cells are merged into as few rectangles as cover them, so
a floor is one box and a character cannot catch on the seams between tiles;
the rectangles follow the entity's scale, and join any collider pieces the
entity also carries.

</details>

<a id="scene-gravity"></a>

#### Scene gravity

`sindri.physics2d.world` sets the scene's gravity, so editor Play runs a scene as its build will.

Engine ✅ · Editor ✅ · Decay — · Proof ✅ — parity row *Scene gravity*

**Tested by:** `crates/sindri-physics/tests/gravity_changes.rs` (`crates/sindri-physics/tests/gravity_changes.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_turns_gravity.rs` (`crates/sindri-decay/tests/a_script_turns_gravity.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `games/scorchball` (`games/scorchball` in the Sindri repository), `examples/physics` (`examples/physics` in the Sindri repository)

**Limits:**

- One vector; no per-area gravity.

<details markdown="1">
<summary>Evidence</summary>

`sindri.physics2d.world` carries the scene's gravity (straight down at 9.81 by
default); a scene without one keeps its host's, which for the editor and
Orbital is none. Decay reads and turns it with `Physics.gravity` and
`Physics.set_gravity` (the Physics Playground's GRAVITY button), and a change
wakes resting bodies.

</details>

<a id="scene-sync-2d"></a>

#### Moving a body by its transform

`ScenePhysics2d` keeps the simulation in step with the scene and writes physics results back into transforms.

Engine ✅ · Editor — · Decay ✅ · Proof ✅ — parity row *Moving a body by its transform*

**Tested by:** `crates/sindri-scene/src/physics_sync/tests.rs` (`crates/sindri-scene/src/physics_sync/tests.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`ScenePhysics2d` builds the simulation from the scene components, keeps it in
step as entities are spawned, switched off, and despawned — a body outliving
its entity would collide on behalf of nothing — and writes what physics decided
back into the transforms the renderer reads, leaving the authored Z and the 3D
scale alone. A transform moved by something other than physics since then, a
script respawning the player, moves the body with it before the next step
rather than being put back. Friction combines as the smaller of two colliders'
values, so a frictionless hero slides down walls. A body whose authored values
have not changed is left as it is rather than rebuilt, because rebuilding one
discards the velocity and contacts the simulation owns. Editor Play and the
game session both step it once per fixed update, before scripts run, so a
script observes the events of the step that just happened.

</details>

<a id="forces-rotation-2d"></a>

#### Forces and rotation (2D)

Additive world force and torque, immediate and off-centre impulses, angular velocity and rotation locks.

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *Forces and rotation (2D)*

**Tested by:** `crates/sindri-physics/tests/forces_and_rotation.rs` (`crates/sindri-physics/tests/forces_and_rotation.rs` in the Sindri repository), `crates/sindri-decay/tests/forces_and_rotation.rs` (`crates/sindri-decay/tests/forces_and_rotation.rs` in the Sindri repository), `games/platformer/tests/forces_and_rotation.rs` (`games/platformer/tests/forces_and_rotation.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/physics.md` (in `07-physics-and-movement.md`)

**Limits:**

- Browser input and visual inspector review remain in final integration.

<details markdown="1">
<summary>Evidence</summary>

Additive world force and torque last one fixed step, impulses act immediately
and off-centre kicks turn a body. Dynamic/velocity-kinematic rules, finite
values and rotation locks are validated; spawn-window requests replay after
collider mass is known and expire if never materialized. The platformer's wind
crate exercises force, torque, angular velocity and impulses through its real
kick input, with scene rotation writeback. Native, WASM and browser CI passed
on the forces/rotation head.

</details>

<a id="contacts-2d"></a>

#### Solid contact snapshots (2D)

Copied solver contacts: other entity, world point, push normal, normal and friction impulses, and force over the last step.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Solid contact snapshots (2D)*

**Tested by:** `crates/sindri-physics/tests/contacts.rs` (`crates/sindri-physics/tests/contacts.rs` in the Sindri repository), `crates/sindri-decay/tests/contact_snapshots.rs` (`crates/sindri-decay/tests/contact_snapshots.rs` in the Sindri repository), `games/platformer/tests/contact_impacts.rs` (`games/platformer/tests/contact_impacts.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**Limits:**

- Sensors are excluded.
- Browser interaction and editor Play inspection remain in final integration.

<details markdown="1">
<summary>Evidence</summary>

`PhysicsWorld2d::contacts` and typed `Physics.contacts(entity)` expose copied
solid solver snapshots: other entity, world point, push normal towards the
queried body, normal and signed friction impulses, and world force over the
last fixed dt. Solver manifolds supply solved impulses. Ordering is
deterministic by entity, point, normal and impulses. Sleeping support remains
with zero new impulse/force. Runtime teleports and removals invalidate
contacts, and Decay filters inactive/despawned others. Spawn-window queries
are empty; missing bodies/physics fail.

The platformer's crate flashes on a hard landing; its hero grounds from
controller results while keeping its ray for clearance. This is a general
capability added for that genre showcase. Native regressions cover
momentum/force balance, compound pieces, snapshot copies, lifecycle, ordering
and sleeping support.

</details>

<a id="one-way-platforms"></a>

#### One-way platforms

A local support normal and contact cone on solid pieces, with timed drop-through from Decay.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *One-way platforms*

**Tested by:** `crates/sindri-physics/tests/one_way.rs` (`crates/sindri-physics/tests/one_way.rs` in the Sindri repository), `crates/sindri-physics/tests/one_way_movement.rs` (`crates/sindri-physics/tests/one_way_movement.rs` in the Sindri repository), `games/platformer/tests/one_way_platforms.rs` (`games/platformer/tests/one_way_platforms.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**Limits:**

- Geometric queries still see both sides, so Decay grounding uses solved support contacts and descent rather than foot-sensor overlap.
- Visual inspector and full browser interaction remain in final integration.

<details markdown="1">
<summary>Evidence</summary>

One-way support is authored with `sindri.physics2d.one_way`: a validated local
normal and contact cone, applied to solid collider and tilemap pieces. Pair
filtering also covers CCD ascent. `Physics.drop_through(entity, seconds)` uses
fixed simulation time, preserves ordinary floors/sensors and works in the spawn
window. Policy edits and undo keep live velocity/joints. The platformer's
visible planks prove ascent, landing, input-driven dropping to painted ground
and landing again; native regressions cover rotated normals, kinematic geometry
and timer cancellation. Chromium export/load smoke passed.

</details>

<a id="materials-2d"></a>

#### Physics materials as assets (2D)

Reusable `physics_material` profile assets applied to every collider piece of an entity, with explicit overrides.

Engine ✅ · Editor 🟡 · Decay — · Proof ✅ — parity row *Physics materials as assets (2D)*

**Tested by:** `crates/sindri-scene/src/physics_material/tests.rs` (`crates/sindri-scene/src/physics_material/tests.rs` in the Sindri repository), `crates/sindri-export/tests/physics_materials.rs` (`crates/sindri-export/tests/physics_materials.rs` in the Sindri repository), `games/platformer/tests/physics_materials.rs` (`games/platformer/tests/physics_materials.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**Limits:**

- Visual inspector review and browser gameplay interaction remain in final physics integration.
- 3D waits for its runtime.

<details markdown="1">
<summary>Evidence</summary>

Reusable physics coefficients are project `.profile` assets with type
`physics_material`, resolved at the scene boundary using shared validation.
`sindri.physics2d.material` applies them to all an entity's collider pieces,
including tilemap collision; explicit override flags win over profile values,
and entities without the component retain their literals. Editor creation,
profile selection, asynchronous delivery and hot reload use the existing
profile pipeline. Invalid reloads retain the previous valid edit; coefficient
changes preserve solver bodies, velocities and joints. Export discovers
material references and validates profiles, and the shared native/browser host
resolves them before stepping. The platformer reuses wood for its wind crate
and planks, explicitly disables plank bounce, and tests the effect of changed
restitution.

</details>

<a id="collider-gizmos"></a>

#### Collider gizmos in the Scene view

Every collider is outlined from the pieces physics is given; the selected one's edges and radii drag.

Engine — · Editor ✅ · Decay — · Proof — — parity row *Collider gizmos in the Scene view*

**Tested by:** `editor/src/native/collider_gizmo.rs` (`editor/src/native/collider_gizmo.rs` in the Sindri repository)

**Limits:**

- Offsets and rotations are still typed.
- 3D pieces are drawn as wireframes and have no handles.

<details markdown="1">
<summary>Evidence</summary>

The Scene view draws every collider's outline, from the very pieces the
physics world is given (a tilemap's merged rectangles included), brighter for
the selected entity and fainter for static geometry. The selected collider's
pieces carry handles: a box's four edges, a circle's radius, a capsule's radius
and height. Dragging an edge keeps the opposite edge where it was, along the
piece's own rotated axis, and a whole drag is one undo step.

</details>

<a id="ccd-2d"></a>

#### Continuous collision (CCD)

An opt-in 2D body flag that sweeps fast bodies so they cannot tunnel through thin geometry.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Continuous collision (CCD)*

**Tested by:** `crates/sindri-physics/tests/continuous_collision.rs` (`crates/sindri-physics/tests/continuous_collision.rs` in the Sindri repository), `crates/sindri-decay/tests/continuous_collision.rs` (`crates/sindri-decay/tests/continuous_collision.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**Limits:**

- Sensors stay discrete; bullet-versus-bullet sweeping is not guaranteed.
- Visual inspector and browser verification remain in final integration.

<details markdown="1">
<summary>Evidence</summary>

An opt-in 2D body flag, scene synchronization and typed Decay controls, with
thin kinematic-wall and spawn-window/undo lifecycle regressions. The
platformer's hero opts in; command-backed edits/undo preserve solver state.
Native and WASM checks passed. This entry was missing from the hand-written
inventory and is taken from its parity row.

</details>

### 2D character controller

<a id="character-movement-2d"></a>

#### Sweep, slide, ground, step and carry queries

Read-only 2D movement queries: collision-limited slide, ground probes with slope limits, steps, platform carry and one-way rules.

Engine ✅ · Editor — · Decay — · Proof ✅

**Tested by:** `crates/sindri-physics/tests/move_and_slide.rs` (`crates/sindri-physics/tests/move_and_slide.rs` in the Sindri repository), `crates/sindri-physics/tests/ground_probe.rs` (`crates/sindri-physics/tests/ground_probe.rs` in the Sindri repository), `crates/sindri-physics/tests/grounded_slide.rs` (`crates/sindri-physics/tests/grounded_slide.rs` in the Sindri repository), `crates/sindri-physics/tests/slope_movement.rs` (`crates/sindri-physics/tests/slope_movement.rs` in the Sindri repository), `crates/sindri-physics/tests/step_movement.rs` (`crates/sindri-physics/tests/step_movement.rs` in the Sindri repository), `crates/sindri-physics/tests/platform_movement.rs` (`crates/sindri-physics/tests/platform_movement.rs` in the Sindri repository), `crates/sindri-physics/tests/one_way_movement.rs` (`crates/sindri-physics/tests/one_way_movement.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/character-movement.md` (in `07-physics-and-movement.md`)

**Limits:**

- Platform rotation follows a straight chord with fixed probe orientation; continuous arc and rotating-probe sweeps are absent.

<details markdown="1">
<summary>Evidence</summary>

A read-only 2D sweep/slide movement foundation proposes displacement against
current collider poses without mutating bodies. Positive skin, bounded
iterations, initial penetration, filters and deterministic ties have native
engine tests.

Read-only 2D ground probing adds configurable unit up, bounded slope angles,
travel and skin. Tests exercise zero-travel support, rotated shapes, slope
boundaries, steep obstruction, penetration, filters, current poses and invalid
inputs. The nearest hit remains visible when unwalkable. The composed grounded
movement query adds post-slide support state and optional downward snapping,
default off. Ascending requests and initial penetration never snap or report
grounded; steep support does not snap. Grounded motion enforces its support
slope limit while sliding: steep contacts cannot introduce unrequested rise,
explicit jumps retain their requested rise, and steep descent remains
ungrounded. Ordinary geometric projection remains unrestricted.

Optional steps require starting support, full lift clearance, better forward
progress and a walkable landing within the height cap. Rejected candidates
preserve the ordinary result. Native tests cover all three probe shapes,
ceilings/overhangs, height limits, absent/steep landings, rotated up and exact
fallback. Contact-normal refinement prevents flat box-face casts from producing
large artificial hops.

Opt-in platform snapshots verify old support and sweep synchronized
translation/rotation-point carry before the character move, retaining wall
clipping and crush/penetration outcomes. Native tests cover all shapes,
snapshot advancement, simulated kinematic ordering, rotation, filtering, jumps
and step accounting. Grounded movement respects per-piece one-way support
sides/cones across sliding, support, steps and carry. Request-scoped
drop-through ignores only one-way solids, preserving ordinary floors and sensor
filtering; hosts own duration and cancellation.

</details>

<a id="character-controller-2d"></a>

#### Character controller (2D)

`sindri.physics2d.character` owns a kinematic probe moved by queued Decay requests, adopted by the platformer's hero.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Character controller (2D)*

**Tested by:** `crates/sindri-scene/src/characters/tests.rs` (`crates/sindri-scene/src/characters/tests.rs` in the Sindri repository), `crates/sindri-decay/tests/character_controllers.rs` (`crates/sindri-decay/tests/character_controllers.rs` in the Sindri repository), `crates/sindri-decay/tests/character_controllers/motions.rs` (`crates/sindri-decay/tests/character_controllers/motions.rs` in the Sindri repository), `crates/sindri-decay/tests/character_controllers/requests.rs` (`crates/sindri-decay/tests/character_controllers/requests.rs` in the Sindri repository), `game/tests/the_character_controller_works.rs` (`game/tests/the_character_controller_works.rs` in the Sindri repository), `games/platformer/tests/character_gameplay.rs` (`games/platformer/tests/character_gameplay.rs` in the Sindri repository), `games/platformer/tests/controller_terrain.rs` (`games/platformer/tests/controller_terrain.rs` in the Sindri repository), `games/platformer/tests/moving_platform.rs` (`games/platformer/tests/moving_platform.rs` in the Sindri repository), `scripts/browser/platformer-goal.mjs` (`scripts/browser/platformer-goal.mjs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/character-movement.md` (in `07-physics-and-movement.md`), `docs/physics-character-editor-review.md` (`docs/physics-character-editor-review.md` in the Sindri repository)

**Limits:**

- Coyote time, jump buffering, gravity and velocity remain Decay gameplay policy.
- Solid response and discrete sensor events see controller motion at the following solve.

<details markdown="1">
<summary>Evidence</summary>

Scene controller ownership registers validated `sindri.physics2d.character`
settings beside the existing Collider 2D. It derives a stationary kinematic
body, requires one solid probe and rejects a simultaneous rigid body; extra
sensors, probe offsets/rotation and query masks are exercised. Runtime movement
requests replace per-step input and specify snap permission; timed controller
drop-through uses fixed simulation duration. Fresh support is seeded before the
solve and current platform motion is carried afterward, with motion applied
once and parent-relative writeback. Scene native tests cover spawning,
first-step vertical carry, rotation, parented riders,
teleports/rebuilds/inactivity/slot reuse, timers, request validation, cached
script borrows and saved authored data.

Typed Decay offers queued `Physics.move_character` requests and copied optional
`Physics.character_motion` results; `Physics.drop_through` selects controller
or dynamic-body timers by authored movement ownership. Motion/support/carry
fields and ordered hit lists filter inactive/despawned references without
rewriting historical displacement. Missing controller context and invalid
inputs fail explicitly; invalid requests preserve previous queued input. Shared
game runtime, editor Play and platformer harness offer the same scene context.
A rebuilt Chromium export of the shared-session fixture runs queued movement,
observes grounded/copied results and renders the controller above its floor.

The platformer adopts the controller through its real Hero script: native
regressions reach the flag with coins and no falls, traverse/drop through
planks, and exercise variable jump height, acceleration/braking and respawn.
Two authored stone risers and an inclined boardwalk exercise accepted steps,
walkable ascent and downhill snap without jumping; control tests disable
stepping or lower the slope limit to show the same terrain blocks walking.

The platformer's one-way plank ferry crosses the first gap using ordinary
kinematic velocity set by Decay. Hero adds no platform motion: scene support
snapshots supply each actual solved displacement once. Native game regressions
ride across and back with fixed relative position, board through a running
jump, leave/reacquire carry on jump/landing, and drop through into the
pit/normal respawn path. Chromium exports the same assets with the hero
initially aboard and renders riding in both directions and a keyboard jump.

Native Character 2D editor review exercises every setting, snap undo/redo,
save/reopen, add/remove through checked commands and restoring custom settings.
The reopened platformer runs/jumps in Play with a read-only inspector; Save
refuses during Play and file hashes confirm Stop restores the document.

The exported platformer has a CI browser goal regression. A scratch copy adds
one read-only Decay observer and retains the shipped starting pose and scripts.
Playwright drives ordinary movement/jump keyboard events, waiting for an
observed release between jumps. The gate asserts the flag, at least three coins
and zero falls, and preserves asset-fetch, WebGPU, pixel and script-error
checks.

</details>

### 2D queries

<a id="raycasts-2d"></a>

#### 2D closest-hit raycasts

Finite world-space casts return entity, point, normal and distance, or null on a miss.

Engine ✅ · Editor — · Decay ✅ · Proof ✅ — parity row *2D closest-hit raycasts*

**Tested by:** `crates/sindri-physics/tests/raycast.rs` (`crates/sindri-physics/tests/raycast.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_casts_a_ray.rs` (`crates/sindri-decay/tests/a_script_casts_a_ray.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `examples/physics` (`examples/physics` in the Sindri repository)

**See:** `docs/physics-playground.md` (`docs/physics-playground.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Closest-hit 2D raycasts return Sindri `RayHit2d` snapshots (entity, world-space
point/normal, distance). Directions are normalized; maximum distance is
inclusive; invalid finite/range inputs fail. Membership masks, sensor opt-in
and whole-entity exclusion select pieces before choosing a hit. Inside hits
have zero distance/normal; exact ties prefer entity handle then piece order.
Decay returns `null` on a miss and skips inactive/despawned entities
immediately, while scene geometry changes are synchronized at the next step.

The platformer displays ground clearance through jumps against painted tilemap
colliders. `examples/physics`, the Physics Playground, has a probe that drags
rays, circle and box casts and area queries through a layer mask and reads out
what they hit, how far and the surface normal, with desktop/touch controls.
Pages exports it under `examples/physics/`; native project regressions and
desktop/phone browser smoke checks exercise its real Decay controls.

</details>

<a id="overlap-cast-2d"></a>

#### Overlap and shape casts (2D)

Circle, box and capsule overlaps and sweeps, with the raycast's mask, sensor and exclusion filter.

Engine ✅ · Editor — · Decay ✅ · Proof ✅ — parity row *Overlap and shape casts (2D)*

**Tested by:** `crates/sindri-physics/tests/overlap_and_shape_cast.rs` (`crates/sindri-physics/tests/overlap_and_shape_cast.rs` in the Sindri repository), `games/orbital-baked/tests/combat_lab.rs` (`games/orbital-baked/tests/combat_lab.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository), `examples/physics` (`examples/physics` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`PhysicsWorld2d::overlap` lists every entity with a piece overlapping a circle,
box or capsule placed in the world, and `shape_cast` sweeps one along a line to
the first piece it touches, both from current body poses with the raycast's
filter. Decay's `Physics.overlap_circle`, `overlap_box`, `cast_circle` and
`cast_box` reach them. Orbital's hostile mine damages what its blast circle
overlaps (`combat_lab.rs` proves a target caught by its edge), and the Physics
Playground's probe casts a ray, a circle or a box, or counts an area.

</details>

<a id="accelerated-queries-2d"></a>

#### Accelerated 2D physics queries

A query-only per-piece BVH selects candidates for rays, overlaps, shape casts and controller probes.

Engine ✅ · Editor — · Decay ✅ · Proof ✅ — parity row *Accelerated 2D physics queries*

**Tested by:** `crates/sindri-physics/src/world2d/spatial/tests.rs` (`crates/sindri-physics/src/world2d/spatial/tests.rs` in the Sindri repository), `crates/sindri-physics/src/world2d/spatial/tests/controllers.rs` (`crates/sindri-physics/src/world2d/spatial/tests/controllers.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**Limits:**

- Dense or long queries can still visit every piece.
- Non-static pieces need a post-step refresh.

<details markdown="1">
<summary>Evidence</summary>

A query-only per-piece BVH works before the first step and after a body move.
Rays, overlaps and shape casts use the synchronized per-piece index, as do
controller penetration, slide and ground probes. Historical platform support
reads only the known support entity's pieces; current bounds cannot discard
its previous pose.

</details>

### 2D joints

<a id="joints-2d"></a>

#### Joints and constraints

Scene-authored distance, hinge, slider and spring joints, each owned by its own entity, with typed Decay controls.

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof ✅ — parity row *Joints and constraints*

**Tested by:** `crates/sindri-physics/tests/distance_joint.rs` (`crates/sindri-physics/tests/distance_joint.rs` in the Sindri repository), `crates/sindri-physics/tests/hinges.rs` (`crates/sindri-physics/tests/hinges.rs` in the Sindri repository), `crates/sindri-physics/tests/sliders.rs` (`crates/sindri-physics/tests/sliders.rs` in the Sindri repository), `crates/sindri-physics/tests/springs.rs` (`crates/sindri-physics/tests/springs.rs` in the Sindri repository), `crates/sindri-scene/src/physics_joints/tests.rs` (`crates/sindri-scene/src/physics_joints/tests.rs` in the Sindri repository), `games/platformer/tests/distance_joints.rs` (`games/platformer/tests/distance_joints.rs` in the Sindri repository), `games/platformer/tests/hinge_motor.rs` (`games/platformer/tests/hinge_motor.rs` in the Sindri repository), `games/platformer/tests/slider_spring.rs` (`games/platformer/tests/slider_spring.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/physics-joint-editor-review.md` (`docs/physics-joint-editor-review.md` in the Sindri repository)

**Limits:**

- The 2D slice is complete; 3D joints are absent, which is why the family is 🟡.

<details markdown="1">
<summary>Evidence</summary>

`sindri.physics2d.distance_joint` adds a scene-authored maximum-distance
constraint owned by a separate entity. Endpoint strings resolve to stable IDs
inside the containing scene; unchanged frames retain one constraint, edits/undo
preserve body motion, and endpoint rebuilds reconnect after body
synchronization. Inactive/missing endpoints suspend the constraint; removing
its entity/component releases it independently of legacy script-created joints.
The platformer's lantern sways under Decay wind within its authored tether and
renders its cord in Decay.

`sindri.physics2d.hinge_joint` adds body-local anchors, bounded relative angles
and torque-capped velocity motors. Unchanged frames keep the constraint;
setting edits wake its endpoints without resetting body motion. The
platformer's Decay-driven windmill reverses its axle motor; native tests
exercise limits, caps, coasting, atomic validation, undo and rebuilds.

`sindri.physics2d.slider_joint` adds aligned local axes, signed travel limits
and force-capped velocity drive; `sindri.physics2d.spring_joint` adds radial
force-based stiffness/damping. The platformer's powered lantern trolley
reverses along its rail while Decay retunes the hanging light's rest length.
Native tests measure rotated rail motion/limits, force caps, coasting/braking,
spring damping and weight support; scene edits/undo, suspension, rebuilds and
game removal are exercised. Editor save integration and native visual review
are exercised.

All four authored constraints accept an enabled flag, defaulting true for old
payloads. Hinge and slider motor authoring uses registered velocity/position
choices from the engine enum rather than a free-text mode field.

The authored 2D joint slice has completed native visual review: all four
numeric payloads reopen, nested hinge overrides persist, editor Play switches
and rebuilds modes, and Stop restores the document. Play Save refuses to write,
confirmed by an unchanged file hash.

</details>

<a id="joint-controls"></a>

#### Typed joint controls from Decay

Scripts tune, enable, retarget, remove and create all four 2D joint kinds, validated atomically before mutation.

Engine ✅ · Editor — · Decay ✅ · Proof ✅

**Tested by:** `crates/sindri-decay/tests/hinge_motor.rs` (`crates/sindri-decay/tests/hinge_motor.rs` in the Sindri repository), `crates/sindri-decay/tests/linear_joint_controls.rs` (`crates/sindri-decay/tests/linear_joint_controls.rs` in the Sindri repository), `crates/sindri-decay/tests/joint_state_controls.rs` (`crates/sindri-decay/tests/joint_state_controls.rs` in the Sindri repository), `crates/sindri-decay/tests/joint_state_controls/endpoints.rs` (`crates/sindri-decay/tests/joint_state_controls/endpoints.rs` in the Sindri repository), `crates/sindri-decay/tests/joint_state_controls/removal.rs` (`crates/sindri-decay/tests/joint_state_controls/removal.rs` in the Sindri repository), `crates/sindri-decay/tests/joint_state_controls/creation.rs` (`crates/sindri-decay/tests/joint_state_controls/creation.rs` in the Sindri repository), `crates/sindri-decay/tests/joint_state_controls/hinge_creation.rs` (`crates/sindri-decay/tests/joint_state_controls/hinge_creation.rs` in the Sindri repository), `crates/sindri-decay/tests/joint_state_controls/spring_creation.rs` (`crates/sindri-decay/tests/joint_state_controls/spring_creation.rs` in the Sindri repository), `crates/sindri-decay/tests/joint_state_controls/slider_creation.rs` (`crates/sindri-decay/tests/joint_state_controls/slider_creation.rs` in the Sindri repository), `games/platformer/tests/joint_controls.rs` (`games/platformer/tests/joint_controls.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Typed `Physics.set_hinge_motor`, `Physics.set_slider_motor` and
`Physics.set_spring` validate atomic runtime component edits for the next
synchronization, preserving unknown fields and rebuild behavior.
`Physics.joint_enabled` reads the authored flag and `Physics.set_joint_enabled`
suspends/reconnects the constraint at next fixed synchronization without
rebuilding endpoint bodies. `Physics.set_distance` tunes owned maximum-distance
constraints, including before initial sync and while suspended. Validation
rejects missing/conflicting/invalid owners, wrong kinds/types, missing physics
and non-positive/non-finite lengths before mutation. The platformer reels its
lantern tether with T and releases/reconnects it with L.

`Physics.set_joint_endpoints` retargets any of the four kinds. It accepts
scoped entity handles or null to clear an endpoint, storing stable scene
IDs/canonical prefab paths. Invalid scope, stale/unstable handles and identical
endpoints fail before mutation; inactive targets suspend until active.
Settings, enabled state, unknown payloads and body motion are retained. The
platformer switches its lantern between two hooks with R, including while
released.

`Physics.remove_joint` removes exactly one valid authored joint component,
releasing its owned constraint at the next fixed synchronization; the owner,
other components and bodies/motion survive. The platformer's Z cuts its lantern
cord.

`Physics.create_distance_joint`, `create_hinge_joint`, `create_spring_joint`
and `create_slider_joint` author an enabled component on an existing owner with
no joint of any kind. Scoped stable references and finite local
anchors/axes/lengths validate before mutation; null/inactive endpoints suspend.
Tests cover pre-sync creation, repeated removal/recreation, invalid arguments,
inactive/unbound targets and isolated runtime prefabs. The platformer repairs
its cut cord with C, rebuilds its windmill hinge with H, its lantern spring
with B and its trolley slider with J.

</details>

<a id="joint-position-motors"></a>

#### Hinge and slider position motors

Damped force-based drives that hold a hinge angle or a slider separation, switchable with the velocity drive.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅

**Tested by:** `crates/sindri-decay/tests/joint_state_controls/hinge_position.rs` (`crates/sindri-decay/tests/joint_state_controls/hinge_position.rs` in the Sindri repository), `crates/sindri-decay/tests/joint_state_controls/slider_position.rs` (`crates/sindri-decay/tests/joint_state_controls/slider_position.rs` in the Sindri repository), `crates/sindri-scene/src/physics_joints/position_tests.rs` (`crates/sindri-scene/src/physics_joints/position_tests.rs` in the Sindri repository), `crates/sindri-scene/src/physics_joints/slider_position_tests.rs` (`crates/sindri-scene/src/physics_joints/slider_position_tests.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/physics-joint-editor-review.md` (`docs/physics-joint-editor-review.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Old payloads default to velocity mode. Typed `Physics.set_hinge_position_motor`
selects a relative angle within `[-pi, pi]`, finite non-negative
stiffness/damping and a torque cap; `Physics.set_slider_position_motor` sets a
finite signed anchor separation along the first local axis with a force cap,
and travel limits still apply. The velocity setters switch back explicitly.
Native tests exercise holding, retargeting, caps, limits, atomic rejection,
command undo/redo, scene round trips, suspension and collider rebuilds. The
platformer's P holds both windmills and resumes their reversal; O parks and
releases its trolley. Added for that showcase.

</details>

<a id="joint-prefab-references"></a>

#### Authored joint references in prefabs

Joint endpoints inside prefabs resolve to their own instance, across spawning, placing, reloading and undo.

Engine 🟡 · Editor — · Decay 🟡 · Proof ✅ — parity row *Authored joint references in prefabs*

**Tested by:** `crates/sindri-scene/src/physics_joints/prefab_tests.rs` (`crates/sindri-scene/src/physics_joints/prefab_tests.rs` in the Sindri repository), `crates/sindri-scene/src/physics_joints/placed_tests.rs` (`crates/sindri-scene/src/physics_joints/placed_tests.rs` in the Sindri repository), `games/platformer/tests/spawned_joints.rs` (`games/platformer/tests/spawned_joints.rs` in the Sindri repository), `editor/src/prefab/joint_tests.rs` (`editor/src/prefab/joint_tests.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**Limits:**

- Flattened plain documents discard the alias metadata.

<details markdown="1">
<summary>Evidence</summary>

Runtime-spawned prefab references use a separate runtime identity, retaining
local sibling paths and the original top-level root ID without assigning saved
scene IDs. The Decay spawn host passes the original prefab library into
expansion; all four joint kinds reconnect only their own nested endpoints
across repeated spawns, inactivity and removal. Canonical paths shadow
aliases; ambiguous aliases fail before spawning. Placed instances retain
aliases through library-aware scene entry on native/browser hosts, scene
namespaces, editor reload/duplication and command undo. Qualified scene IDs
take precedence over relative namespace lookup when an exact match exists.

The platformer's reusable windmill nests its powered mechanism in
`windmill-kit.prefab`; Decay spawns and removes it, and the level places the
same nested assembly.

Registered entity fields have scoped native inspector choices, explicit
clearing and visible missing/inactive diagnostics through the runtime's shared
resolver. Real picker-click edits retarget/clear all four constraint kinds, and
command undo restores their constraints/unknown payloads.

</details>

<a id="saved-reference-remapping"></a>

#### Saved spawned-prefab reference remapping

Saving a world remaps registered entity references to stable IDs, so spawned joint assemblies reopen intact.

Engine ✅ · Editor ✅ · Decay — · Proof ✅ — parity row *Saved spawned-prefab reference remapping*

**Tested by:** `crates/sindri-core/tests/saved_prefab_references.rs` (`crates/sindri-core/tests/saved_prefab_references.rs` in the Sindri repository), `crates/sindri-scene/src/physics_joints/saved_tests.rs` (`crates/sindri-scene/src/physics_joints/saved_tests.rs` in the Sindri repository), `editor/tests/scene_joint_saves.rs` (`editor/tests/scene_joint_saves.rs` in the Sindri repository), `games/platformer/tests/saved_joints.rs` (`games/platformer/tests/saved_joints.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**Limits:**

- Stable IDs remain a prerequisite.
- Save is unavailable during Play.

<details markdown="1">
<summary>Evidence</summary>

Opt-in `World::to_scene_with_references` remaps registered entity fields to
assigned stable IDs, including nested lists, without live edits. Unknown fields
remain unchanged, empty references stay unbound and
malformed/unresolved/unstable targets fail serialization.

Editor `SceneFile::save`/`save_as` and subtree prefab authoring use the active
registry automatically. Invalid references fail before file writes/path
adoption while unknown fields and the live world remain intact. Existing placed
instances still save as prefab references. Native editor tests reopen two
isolated instances of every joint kind, reuse saved subtrees and reject invalid
saves without changing disk or the agreed document. The platformer's real Decay
setup script spawns a nested windmill that editor Save As reopens with its
fixed axle and reversing motor.

</details>

### 3D

<a id="world-3d"></a>

#### Standalone 3D physics world

`PhysicsWorld3d` runs the same Sindri-owned body and collider model in XYZ.

Engine ✅ · Editor — · Decay — · Proof ❌

**Tested by:** `crates/sindri-physics/tests/world3d.rs` (`crates/sindri-physics/tests/world3d.rs` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Native tests cover XYZ gravity, box/sphere/capsule solids, quaternion/local
offsets, compound mass, masks and collision/sensor events, kinematic targets,
velocity/impulse/teleport controls, rotation locking and atomic
validation/removal. The engine rejects quaternions outside the unit-norm
tolerance; all poses use `[x, y, z, w]`.

</details>

<a id="scene-physics-3d"></a>

#### 3D physics in scenes

`ScenePhysics3d` reconciles 3D body, collider and world components with scene entities, and editor Play steps it.

Engine ✅ · Editor 🟡 · Decay 🟡 · Proof ❌

**Tested by:** `crates/sindri-scene/src/physics_sync3d/tests.rs` (`crates/sindri-scene/src/physics_sync3d/tests.rs` in the Sindri repository), `crates/sindri-scene/src/physics_sync3d/authoring_tests.rs` (`crates/sindri-scene/src/physics_sync3d/authoring_tests.rs` in the Sindri repository), `crates/sindri-scene/src/physics_sync3d/hierarchy_tests.rs` (`crates/sindri-scene/src/physics_sync3d/hierarchy_tests.rs` in the Sindri repository), `crates/sindri-decay/tests/physics3d_controls.rs` (`crates/sindri-decay/tests/physics3d_controls.rs` in the Sindri repository)

**See:** `docs/physics-voxel-editor-review.md` (`docs/physics-voxel-editor-review.md` in the Sindri repository)

**Limits:**

- Composed scale edits rebuild geometry and reset body motion.
- Structural payload edits reset the rebuilt body's motion.
- Model/character capabilities have feature-lab proof; Explorer game adoption remains open.

<details markdown="1">
<summary>Evidence</summary>

`ScenePhysics3d` reconciles registered body/collider/world components with
active scene entities, composed XYZ/quaternion transforms, gravity settings and
parent-space write-back. Twelve regressions cover lifecycle/edits, batch
rejection, solid/sensor events, targets, newer rotated/scaled parents and
checked command undo/redo/save/reopen. This is the prerequisite for Causeway
voxel collision proof.

Shared native/browser sessions and native editor Play step both dimensions
before scripts; fresh Play, Stop and scene replacement reset both editor
solvers. An exported scratch fixture exercises XYZ motion/landing in Chromium
with a read-only Decay observer and visible textured geometry. All three
components appear in the Physics menu.

Typed `Physics3d` adds copied Vec3 linear/angular velocities, setters and
dynamic impulses, plus all four sorted unique non-draining collision/sensor
event queries. Controls require an active body and leave authored motion
unchanged.

</details>

<a id="spawn-window-3d"></a>

#### 3D spawn-window controls

Velocity and impulse requests on a body that has not materialized yet queue, replay after mass is known, and expire.

Engine 🟡 · Editor ❌ · Decay 🟡 · Proof ❌ — parity row *3D spawn-window controls*

**Tested by:** `crates/sindri-physics/tests/pending3d.rs` (`crates/sindri-physics/tests/pending3d.rs` in the Sindri repository), `crates/sindri-scene/src/physics_sync3d/pending_tests.rs` (`crates/sindri-scene/src/physics_sync3d/pending_tests.rs` in the Sindri repository), `crates/sindri-decay/tests/physics3d_spawn.rs` (`crates/sindri-decay/tests/physics3d_spawn.rs` in the Sindri repository)

**Limits:**

- Game, voxel and editor proof remain open.

<details markdown="1">
<summary>Evidence</summary>

The standalone `BodyControl3d` queue validates setters/impulses before
insertion, replays them in order after compound mass is known and expires
unresolved requests at a successful synchronization. Complete-batch validation
includes queued kind conflicts and retains requests after failure.
`ScenePhysics3d` replays before solving. Typed bridge regressions exercise real
prefab spawning, copied pending reads, locks/kinds and invalid authoring. A
shared native-session regression and exported Chromium fixture verify actual
spawn replay and movement.

</details>

<a id="queries-3d"></a>

#### 3D physics queries

Rays, overlaps and fixed-orientation shape casts over a per-piece BVH, reachable from typed `Physics3d`.

Engine ✅ · Editor ✅ · Decay ✅ · Proof 🟡 — parity row *3D physics queries*

**Tested by:** `crates/sindri-physics/tests/queries3d.rs` (`crates/sindri-physics/tests/queries3d.rs` in the Sindri repository), `crates/sindri-physics/src/world3d/spatial/tests.rs` (`crates/sindri-physics/src/world3d/spatial/tests.rs` in the Sindri repository), `crates/sindri-decay/tests/physics3d_queries.rs` (`crates/sindri-decay/tests/physics3d_queries.rs` in the Sindri repository), `crates/sindri-decay/tests/physics3d_probes.rs` (`crates/sindri-decay/tests/physics3d_probes.rs` in the Sindri repository), `crates/sindri-decay/tests/physics3d_layers.rs` (`crates/sindri-decay/tests/physics3d_layers.rs` in the Sindri repository)

**Limits:**

- No shipped gameplay decision uses a 3D query yet.
- Backend shape casts at near-maximum finite coordinates/extents can return non-finite results.

<details markdown="1">
<summary>Evidence</summary>

Standalone 3D rays, overlaps and fixed-orientation shape casts use current
body/local poses even before the first step or immediately after teleports.
Native regressions cover all shapes, XYZ hits, rotated probes, filtering,
whole-entity predicates, sorted unique results, exact ties, initial overlap,
extreme finite directions and pending/solved poses. A query-only per-piece BVH
selects finite ray segments and probe/swept bounds, preserving entity/piece
order; differential regressions compare exhaustive candidates, and sparse XYZ
rays, overlaps and casts select one piece in 100/1,000/10,000-piece worlds.

Typed 3D rays, sphere overlaps and sphere casts use indexed synchronized
geometry, active-entity filtering, masks/sensors/whole-entity exclusion and
copied optional `RayHit3d`/sorted overlap lists. Typed `Physics3d.layer`/`mask`
read only the active authored 3D world's first 32 labels. Typed rotated
box/capsule overlaps and casts accept an arbitrary nonzero rotation axis and
radians, with f64 normalization before quaternion conversion.

</details>

<a id="static-groups-3d"></a>

#### 3D replaceable static geometry groups

Keyed static collider groups under one real owner, replaced or removed without disturbing other handles.

Engine 🟡 · Editor ❌ · Decay ❌ · Proof ❌ — parity row *3D replaceable static geometry groups*

**Tested by:** `crates/sindri-physics/tests/static_groups3d.rs` (`crates/sindri-physics/tests/static_groups3d.rs` in the Sindri repository), `crates/sindri-voxel/src/collision_tests.rs` (`crates/sindri-voxel/src/collision_tests.rs` in the Sindri repository)

**Limits:**

- Authored Causeway, editor, Decay and browser group integration is absent; voxel collision reaches the solver through the scene adapter instead.

<details markdown="1">
<summary>Evidence</summary>

The standalone 3D solver owns keyed static collider groups under one real
entity. Replacement/removal retains other collider handles and the body handle;
queries update immediately with owner identity and canonical key/piece
ordering. A read-only validator supports complete scene-batch preflight.
Invalid geometry, nonstatic owners or incompatible pending controls leave the
world unchanged. Empty owners retain identity until full removal. Native
regressions include actual landing/events, deletion/falling, filters and
internal handle retention.

A renderer/physics-independent `compile_section_collision` derives exact
section-local collision boxes from occupied `VoxelSection` cells and a supplied
solidity/shape policy. Full cubes merge deterministically across material IDs;
partial slabs/posts retain exact bounds, air and policy-excluded blocks are
empty. Integer sixteenth-voxel bounds avoid world-coordinate precision loss;
invalid shapes return typed errors and output is bounded by 4,096 boxes per
section. Regressions prove exact cover without holes, overlaps or air, partial
shapes, worst-case bounds and edited negative sections across residency
unload/reload.

</details>

### 3D physics foundation

<a id="collider-scaling-3d"></a>

#### Automatic 3D collider scaling

Composed positive entity/parent scale resizes dimensions and offsets with atomic validation.

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *Automatic 3D collider scaling*

**Tested by:** `crates/sindri-scene/src/physics_sync3d/scale_tests.rs` (`crates/sindri-scene/src/physics_sync3d/scale_tests.rs` in the Sindri repository)

**Exercised in:** `examples/orbit` (`examples/orbit` in the Sindri repository)

**See:** `docs/collider-scale-editor-review.md` (`docs/collider-scale-editor-review.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`ScenePhysics3d` reconciles registered body/collider/world components with
active scene entities, composed XYZ/quaternion transforms, gravity settings and
parent-space write-back. Twelve regressions cover lifecycle/edits, batch rejection,
solid/sensor events, targets, newer rotated/scaled parents and checked command
undo/redo/save/reopen. Composed positive transform scale now resizes 3D collider
dimensions and offsets, including inherited scale. Boxes support nonuniform
scale with orthogonal local axes; spheres/capsules require uniform scale. Shear,
nonpositive/nonfinite scale and overflow fail before batch mutation. The shared
editor wireframe path resolves the same geometry. Native scene regressions
exercise scaled solid landing, queries, parent offsets, wireframes, rebuild timing
and atomic rejection. A shared Session regression narrows/restores Orbit Camera
Lab's barrier and verifies recovery/immediate pull-in. The converted lab reaches
its goal in real WebGPU Chromium with fetched assets, rendered pixels and no
runtime errors; captured input and denial regressions also pass. Native editor
scale authoring is exercised below; Explorer adoption remains pending. Structural payload or resolved scale edits
reset the rebuilt body's motion. This is the prerequisite

Orbit Lab keyboard shrink/restore and native editor scale authoring, Undo/Redo, save/reopen, Play query/goal and Stop restoration are exercised. Explorer adoption remains open.

</details>

<a id="character-controller-3d"></a>

#### Character controller (3D)

Checked scene-owned 3D movement, grounding and solved platform carry have typed Decay and native/browser/editor feature proof.

Engine 🟡 · Editor ✅ · Decay 🟡 · Proof 🟡 — parity row *Character controller (3D)*

**Tested by:** `crates/sindri-physics/tests/character3d.rs` (`crates/sindri-physics/tests/character3d.rs` in the Sindri repository), `crates/sindri-decay/tests/character_controllers3d.rs` (`crates/sindri-decay/tests/character_controllers3d.rs` in the Sindri repository)

**Exercised in:** `examples/character3d` (`examples/character3d` in the Sindri repository)

**See:** `docs/character-movement.md` (in `07-physics-and-movement.md`), `docs/physics-character3d-editor-review.md` (`docs/physics-character3d-editor-review.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`PhysicsWorld3d::move_character` wraps Rapier's kinematic character controller
in engine-owned read-only movement queries. Absolute separation, slope limits,
optional steps/snap, stationary correction and copied collision values use the
existing current-pose query index. Native engine tests exercise XYZ motion,
solid landing, wall slide, steps/headroom, snap, slopes, arbitrary up, filtering,
teleports, copies, clone replay and invalid input. Platform carry is intentionally
separate from this displacement query. Rapier contact/flag semantics and bounded
iteration/correction limits are documented in Character movement (in `07-physics-and-movement.md`).
This is a foundation added for Explorer, with scene ownership described below;
Scene/Decay integration, moving-platform carry and browser feature proof are
implemented below; native editor proof is recorded in
the review (`docs/physics-character3d-editor-review.md` in the Sindri repository). Explorer game goal proof remains pending.

Scene ownership through
`sindri.physics3d.character` provides stationary kinematic bodies, next-fixed-step XYZ
requests, scaled single-solid probes, cached results and voxel residency covering
requested movement. Native scene and shared Session tests exercise filtering,
parent writeback, invalid input, rebuilds, terrain budgets and checkpoint replay.
Read-only `probe_ground` queries now classify nearest skin/touching or downward
support against a slope limit, retain steep hits and flag initial penetration.
Native tests cover shapes, compound offsets, filters, ties, current poses and
invalid inputs; the query contract (in `07-physics-and-movement.md`)
records numerical limits. `move_character_grounded` now composes verified
previous support, solved translation/rotation-point carry, ordinary movement and
classified final support. Eight native regressions cover clipping, vertical
platforms, jumps, compound offsets, filtering, copies and invalid input; the
carry contract (in `07-physics-and-movement.md`) records chord
and backend limits. Scene carry now uses the composition with classified pre-solve support, a flat
`carry_platforms` opt-out, dependent cache invalidation and once-only parent-space
writeback. Terrain keeps its input/solver window before the solve and expands for
actual carry before rider queries. A post-solve carry budget failure retains input
and leaves riders unmoved; it does not roll back solver state. Native scene and
Session tests cover boarding, jumps, parented rotation, wall clipping, lifecycle,
checkpoint replay and terrain budgets.
Typed `Physics3d.move_character` now queues Vec3 displacement and
`Physics3d.character_motion` copies optional classified support, raw movement,
carry and detailed collisions. Native host/script tests exercise next-step and
last-valid-input semantics, invalid context/values/ownership, stale-reference
filtering and nested copy mutation; shared Session verifies real-script timing
and checkpoint replay. Compiler/LSP and generated API metadata use the same
record types. Gameplay supplies gravity/jumps. The new
Character Lab (`examples/character3d/README.md` in the Sindri repository) opens and compiles through
native Session, verifies idle carry/reversal, and reaches its goal through keyboard
jump, airborne carry release and floor landing. Exported WebGPU Chromium plays
the same project with ordinary keys, verifies each milestone, fetched assets and
no runtime/GPU errors, and captures the blue rider on the gold goal pad. CI runs
both project tests and the browser regression. Selected 3D characters now show
orange slope, step and snap guides from the runtime's validated scaled probe,
with exact box/sphere/capsule projection extents and world-space up/reach. Four
scene regressions cover parenting, scaling, sensors, invalid geometry and disabled
controllers. Native editor review (`docs/physics-character3d-editor-review.md` in the Sindri repository) exercises
snap undo/redo, step editing, invalid skin rejection, save/reopen, keyboard Play
to the goal, read-only controls, Save refusal and Stop restoration. Explorer
adoption remains pending; see
the contract (in `07-physics-and-movement.md`). This is a general
capability added for the planned Explorer showcase, not completed game proof.

</details>

### Not yet

<a id="query-numerics-3d"></a>

#### Extreme 3D query numerical failure reporting

Near-maximum finite coordinates can produce non-finite shape-cast results, and the engine has no contract for reporting it.

Engine ❌ · Editor — · Decay — · Proof ❌ — parity row *Extreme 3D query numerical failure reporting*

<a id="world-snapshots"></a>

#### Script-triggered world snapshot save/load

Decay's `Save` stores numbers and flags; there is no operation to persist and reopen a running world.

Engine ❌ · Editor — · Decay ❌ · Proof ❌ — parity row *Script-triggered world snapshot save/load*

<a id="compound-character-probes"></a>

#### Compound solid character probes

Scene controllers require one solid box, circle or capsule piece, because the movement primitive sweeps one convex shape.

Engine ❌ · Editor — · Decay ❌ · Proof ❌ — parity row *Compound solid character probes*

<details markdown="1">
<summary>Evidence</summary>

Scene controllers require one solid primitive piece; extra sensors are allowed. Multiple solid probes are rejected because the movement primitive sweeps one convex shape

</details>

<a id="same-step-controller-response"></a>

#### Same-step solver response to controller motion

Controller moves happen after the solve; dynamic response and sensors see them at the following solve, and no push impulse is applied.

Engine ❌ · Editor — · Decay ❌ · Proof ❌ — parity row *Same-step solver response to controller motion*

<a id="swept-controller-triggers"></a>

#### Swept controller trigger events

Controller sweeps exclude sensors, so crossing a small sensor between solves can produce no event.

Engine ❌ · Editor — · Decay ❌ · Proof ❌ — parity row *Swept controller trigger events*

<a id="rotating-carry-arc"></a>

#### Sweeping rotating platform carry along its arc

Platform carry sweeps the straight chord between support points; large rotations can miss obstacles on the arc.

Engine ❌ · Editor — · Decay ❌ · Proof ❌ — parity row *Sweeping rotating platform carry along its arc*

<a id="rotating-character-probe"></a>

#### Sweeping a rotating character probe

Movement and carry keep the probe's rotation; there is no swept orientation change.

Engine ❌ · Editor — · Decay ❌ · Proof ❌ — parity row *Sweeping a rotating character probe*

<a id="controller-on-controller-carry"></a>

#### Carry on a scene-owned 3D character

Controllers collide geometrically, but carry support excludes other controllers: they remain stationary during the solve and apply input afterward, so controller-on-controller carry would otherwise depend on entity processing order

Engine ❌ · Editor — · Decay ❌ · Proof ❌ — parity row *Carry on a scene-owned 3D character*

**See:** `docs/character-movement.md` (in `07-physics-and-movement.md`)

<details markdown="1">
<summary>Evidence</summary>

Controllers collide geometrically, but carry support excludes other controllers: they remain stationary during the solve and apply input afterward, so controller-on-controller carry would otherwise depend on entity processing order

</details>

### 3D foundation

<a id="physics-3d-integration"></a>

#### 3D physics

`PhysicsWorld3d` exercises fixed steps, all body kinds, box/sphere/capsule compounds, quaternion/local offsets, compound mass, collision/sensor events, masks, controls, keyed static groups and atomic validation

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *3D physics*

**Tested by:** `game/tests/the_3d_physics_works.rs` (`game/tests/the_3d_physics_works.rs` in the Sindri repository)

**Exercised in:** `game` (`game` in the Sindri repository)

**See:** `docs/physics.md` (in `07-physics-and-movement.md`)

<details markdown="1">
<summary>Evidence</summary>

`PhysicsWorld3d` exercises fixed steps, all body kinds, box/sphere/capsule compounds, quaternion/local offsets, compound mass, collision/sensor events, masks, controls, keyed static groups and atomic validation. `ScenePhysics3d` owns authored bodies, colliders and voxel colliders, validates each batch before committing it and writes back parent-space poses. Shared native/browser sessions and editor Play step it before scripts. Typed `Physics3d` controls, ordered spawn queues and copied collision/sensor events are exercised in bridge/session tests and Chromium. Causeway uses it: a block taken back is a dynamic body that lands on the voxel world before it counts. The native review (`docs/physics-voxel-editor-review.md` in the Sindri repository) authors Rigid Body 3D/Collider 3D in the inspector and lands the body on voxels in Play, with Stop and replay. Behind because joints, CCD and one-way support remain 2D-only; 3D profiles and characters still await their game proof, and the 3D scene/controller foundation is tracked below

</details>

<a id="physics-materials-3d"></a>

#### Physics materials as assets (3D)

Shared `.profile` format and explicit overrides resolve on primitives, characters, static model triangles, convex hulls and resident voxels

Engine 🟡 · Editor 🟡 · Decay — · Proof 🟡 — parity row *Physics materials as assets (3D)*

**Tested by:** `crates/sindri-physics/src/world3d/materials_tests.rs` (`crates/sindri-physics/src/world3d/materials_tests.rs` in the Sindri repository), `crates/sindri-scene/src/physics_sync3d/material_tests.rs` (`crates/sindri-scene/src/physics_sync3d/material_tests.rs` in the Sindri repository), `crates/sindri-runtime/tests/physics_material3d.rs` (`crates/sindri-runtime/tests/physics_material3d.rs` in the Sindri repository)

**Exercised in:** `examples/model-collision` (`examples/model-collision` in the Sindri repository)

**See:** `docs/physics.md` (in `07-physics-and-movement.md`), `docs/model-collision-editor-review.md` (`docs/model-collision-editor-review.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

The standalone `PhysicsWorld3d` now reads copied `PhysicsMaterial` values and
updates complete coefficient batches on primitive, convex, triangle and keyed
static colliders. Invalid values or a piece count mismatch leave the batch
unchanged. Existing geometry/body handles, mass, motion, forces and kinematic
targets survive; changed values wake contact processing while identical values
leave sleeping bodies asleep. Native regressions verify friction changes on
existing contacts, static-floor wakeup, restitution response and checkpoint
replay. See the physics contract (in `07-physics-and-movement.md`).
The shared scene and project integration is described below; Explorer game
proof remains pending.

`sindri.physics3d.material` reuses the 2D physics profile schema and explicit
override fields. Scene preparation resolves coefficients on primitive/model/hull
bodies and voxel sections before solver mutation. Reloads and literal coefficient
edits retain body motion, geometry, character support and contacts; voxel edits
keep section groups and consume no geometry rebuild budget. Cached model plans
restore authored coefficients before resolving each step, including profile removal.
Session validates one source set for both dimensions and keeps the latest host
profiles on checkpoint restore. Export includes inactive and prefab references.
The editor's registered default, profile metadata and Physics family support
checked generic authoring. Model Collision Lab shares a slide profile between
its floor and spawned convex prop, with a zero-restitution override. Native
scene, runtime and export regressions exercise resolution, atomic invalid edits,
retained motion, latest-asset replay and original-byte deduplication. This is
feature proof for Explorer; gameplay adoption remains open. See the
physics contract (in `07-physics-and-movement.md`).

Rebuilt Chromium fetches the shared physics profile and both GLBs, reaches the
walker/prop goals and passes rendered-color/error checks. Native editor review
exercises the profile picker, override Undo/Redo and save/reopen, valid reload
during Play, invalid-reload last-valid retention, prop goal and Stop restoration.
See the review (`docs/model-collision-editor-review.md` in the Sindri repository).
Native visual evidence uses isolated X11/software Vulkan; broader coverage and
Explorer game proof remain open.

</details>

<a id="static-model-collision-3d"></a>

#### Static triangle mesh colliders (3D)

Assets prepare bounded GLB triangles; immutable physics meshes share acceleration structures across keyed static groups

Engine 🟡 · Editor 🟡 · Decay ❌ · Proof 🟡 — parity row *Static triangle mesh colliders (3D)*

**Tested by:** `crates/sindri-assets/src/decode/model/collision/tests.rs` (`crates/sindri-assets/src/decode/model/collision/tests.rs` in the Sindri repository), `crates/sindri-physics/src/world3d/triangle_mesh_tests.rs` (`crates/sindri-physics/src/world3d/triangle_mesh_tests.rs` in the Sindri repository), `crates/sindri-runtime/tests/model_collision_steps.rs` (`crates/sindri-runtime/tests/model_collision_steps.rs` in the Sindri repository)

**Exercised in:** `examples/model-collision` (`examples/model-collision` in the Sindri repository)

**See:** `docs/model-collision-editor-review.md` (`docs/model-collision-editor-review.md` in the Sindri repository), `docs/physics.md` (in `07-physics-and-movement.md`)

<details markdown="1">
<summary>Evidence</summary>

Model assets expose bounded CPU `collision_mesh` preparation: selected-root
node matrices are accumulated, reused mesh primitives expand per instance,
reflections correct index winding, and node/mesh/primitive ranges are retained.
Six native GLB regressions cover order, copied data, both index widths,
non-indexed geometry, mirror transforms, exact budgets, deep iterative traversal
and malformed data. The optional `sindri-assets` model inspector's `--collision`
flag reports prepared counts without creating physics bodies. This is a
foundation added for the planned Explorer showcase. Standalone static triangle
physics is available below; scene bindings and imported-model project collision now have the feature proof below. See
the contract (in `09-3d.md`).

Static physics now provides clone-shared `TriangleMesh3d` resources and
`TriangleMeshCollider3d` properties through keyed static groups. Eight native
regressions exercise immediate ray/overlap/cast/ground/character queries, open
surface gaps, filtering and local rotation, resource sharing, atomic invalid
edits, replacement/removal, dynamic landing and cloned-world replay. This is
foundation for Explorer. Static scene model components and explicit Session GLB
bindings now exercise character collision, scale/lifecycle, asset replacement and
checkpoint replay in native regressions. Shared discovery and export now cover
collision-only scene/prefab references, inactive entities, deduplicated assets and
original verified GLB bytes. The shared native/browser player now installs
collision before gameplay, expanding scenes/prefabs/overrides and preparing only
authored collider assets. Model Collision Lab opens/compiles and walks into an
imported wall, around it and to its goal through the player's Session regression
and exported browser smoke. Native headless loading and editor Play now install
collision before gameplay; the lab's editor/headless parity regression compares
every step through wall obstruction and the goal and restores/replays collision.
Editor startup checks its asynchronous queue, honors unsaved references and keeps
later-scene assets alive; model reload failures retain previous collision.
Native editor review (`docs/model-collision-editor-review.md` in the Sindri repository) verifies the goal,
moved-wall reload, failed-replacement retention, Save refusal and Stop restoration
on X11/software Vulkan. The same review now exercises model asset picking,
independent Undo/Redo, project-backed Add Model and save/reopen followed by Play.
Picker metadata is reflected in the generated catalogue; uppercase GLB native
loading is covered without changing IDs. Explorer game proof remains pending. See
the contract and numeric limits (in `07-physics-and-movement.md`).

</details>

<a id="convex-hull-collision-3d"></a>

#### Convex hull colliders (3D)

Clone-shared volumetric hull resources and mixed primitive/hull bodies support all four body kinds

Engine 🟡 · Editor ❌ · Decay ❌ · Proof 🟡 — parity row *Convex hull colliders (3D)*

**Tested by:** `crates/sindri-physics/src/world3d/convex_hull_tests.rs` (`crates/sindri-physics/src/world3d/convex_hull_tests.rs` in the Sindri repository), `crates/sindri-scene/src/physics_sync3d/convex_model_tests.rs` (`crates/sindri-scene/src/physics_sync3d/convex_model_tests.rs` in the Sindri repository)

**Exercised in:** `examples/model-collision` (`examples/model-collision` in the Sindri repository)

**See:** `docs/physics.md` (in `07-physics-and-movement.md`)

<details markdown="1">
<summary>Evidence</summary>

`ConvexHull3d` constructs bounded, immutable volumetric resources, shared by
`ConvexHullCollider3d` pieces in mixed primitive/hull rigid bodies. Native tests
exercise exact tetrahedron geometry, duplicates/invalid/degenerate points,
shared resources, compound mass and local transforms, pending impulses, dynamic
landing/replay, atomic failures, both kinematic kinds and sensor masks/events.
The same insertion and control/query lifecycle supports all four body kinds.
This is foundation added for Explorer; explicit convex-model scene and Session
bindings now exercise scaled dynamic landing, mixed pieces, retained velocity,
lifecycle, atomic failures and checkpoint-aware GLB replacement. Shared convex discovery/export and player/headless/editor Play loading now
retain both representations per logical asset, with atomic shared reload.
Collision-only headless layouts, inactive/prefab export, merged unsaved/later
references and authoring compatibility have regressions. Model Collision Lab
adds a falling GLB prop with a Decay contact-start/impulse/travel goal through
native player and editor/headless parity tests, rebuilt Chromium and native
editor Play. The editor review (`docs/model-collision-editor-review.md` in the Sindri repository)
checks valid/invalid reload, Add Component, independent picking/Undo/Redo, save
and reopen. Explorer game
proof remains pending. See
the hull contract and numeric limits (in `07-physics-and-movement.md`).

</details>

<a id="convex-model-project-loading"></a>

#### Convex model collision project loading

Shared discovery/export and player/headless/editor Play installation retain both collision representations per logical GLB

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *Convex model collision project loading*

**Tested by:** `crates/sindri-runtime/tests/model_project_loading.rs` (`crates/sindri-runtime/tests/model_project_loading.rs` in the Sindri repository), `crates/sindri-runtime/tests/model_convex_steps.rs` (`crates/sindri-runtime/tests/model_convex_steps.rs` in the Sindri repository)

**Exercised in:** `examples/model-collision` (`examples/model-collision` in the Sindri repository)

**See:** `docs/model-collision-editor-review.md` (`docs/model-collision-editor-review.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`ConvexHull3d` constructs bounded, immutable volumetric resources, shared by
`ConvexHullCollider3d` pieces in mixed primitive/hull rigid bodies. Native tests
exercise exact tetrahedron geometry, duplicates/invalid/degenerate points,
shared resources, compound mass and local transforms, pending impulses, dynamic
landing/replay, atomic failures, both kinematic kinds and sensor masks/events.
The same insertion and control/query lifecycle supports all four body kinds.
This is foundation added for Explorer; explicit convex-model scene and Session
bindings now exercise scaled dynamic landing, mixed pieces, retained velocity,
lifecycle, atomic failures and checkpoint-aware GLB replacement. Shared convex discovery/export and player/headless/editor Play loading now
retain both representations per logical asset, with atomic shared reload.
Collision-only headless layouts, inactive/prefab export, merged unsaved/later
references and authoring compatibility have regressions. Model Collision Lab
adds a falling GLB prop with a Decay contact-start/impulse/travel goal through
native player and editor/headless parity tests, rebuilt Chromium and native
editor Play. The editor review (`docs/model-collision-editor-review.md` in the Sindri repository)
checks valid/invalid reload, Add Component, independent picking/Undo/Redo, save
and reopen. Explorer game
proof remains pending. See
the hull contract and numeric limits (in `07-physics-and-movement.md`).

</details>

<a id="convex-spawn-controls"></a>

#### Pre-synchronization controls for convex-only bodies

Convex-only and mixed bodies accept existing typed motion reads/setters/impulses before materialization

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *Pre-synchronization controls for convex-only bodies*

**Tested by:** `crates/sindri-decay/tests/physics3d_convex_spawn.rs` (`crates/sindri-decay/tests/physics3d_convex_spawn.rs` in the Sindri repository)

**Exercised in:** `examples/model-collision` (`examples/model-collision` in the Sindri repository)

**See:** `docs/model-collision-editor-review.md` (`docs/model-collision-editor-review.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Convex-model spawn-window motionnow shares the existing `Physics3d` control
queue. The host validates real body settings and primitive/convex authoring
without creating stand-in geometry; full model bindings, hull volume/bounds and
combined mass validate at scene synchronization. Five native host regressions
cover scaled/mixed mass, pending reads, rotation locks, invalid authoring,
geometry retry and once-only replay. Model Collision Lab now spawns a typed
convex GLB prefab, queues controls immediately, and reaches its landing/impulse
travel goal. The project Session regression checks first materialization and
checkpoint replay. Rebuilt Chromium reaches both goals, and native editor
Play/restart/Stop is recorded in the review (`docs/model-collision-editor-review.md` in the Sindri repository).
This is feature proof for Explorer; game adoption remains open.

</details>

---

Source: `docs/capabilities/rendering.md`

## Hosts and rendering

How a game reaches a window or a browser, how the GPU is negotiated, and what
the renderer can actually draw: sprites, shapes, text, 2D lighting, tilemaps
and tile volumes, 3D meshes and models, and the world presentation stack.
`docs/rendering-frame-pipeline.md`, `docs/rendering-surface.md` and
`docs/rendering-color.md` are the contracts.

Each entry links its tests and the projects that exercise it. Surface marks match the row in parity (`docs/parity.md` in the Sindri repository) where one is named: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — not applicable.

| Capability | Engine | Editor | Decay | Proof |
| --- | :-: | :-: | :-: | :-: |
| **Hosts and GPU** | | | | |
| Native desktop | ✅ | ✅ | — | ✅ |
| Browser / WASM | ✅ | — | — | ✅ |
| GPU negotiation and swapchain policy | ✅ | — | — | ✅ |
| **Frame and scene extraction** | | | | |
| Extraction, preparation and ordered passes | ✅ | ✅ | — | ✅ |
| Cameras, depth, cube primitive, inline textured surface mesh | 🟡 | 🟡 | ❌ | 🟡 |
| **Sprites and shapes** | | | | |
| Sprites, sheets, UVs, layers, blending | ✅ | ✅ | 🟡 | ✅ |
| Advanced sprite colour transform | ✅ | ✅ | ✅ | ✅ |
| Procedural shapes | ✅ | 🟡 | ✅ | ✅ |
| Custom shaders / materials | 🟡 | ✅ | ✅ | 🟡 |
| Sprite masking / stencil | ✅ | ✅ | ✅ | ✅ |
| Nine-slice sprites | ✅ | ✅ | ✅ | ✅ |
| **Sprite animation** | | | | |
| Sprite frame clips, timing, loop state | ✅ | ✅ | ✅ | ✅ |
| Clip authoring and preview | — | ✅ | — | ✅ |
| **Text** | | | | |
| World-space text | ✅ | ✅ | ✅ | ✅ |
| Rich text | ✅ | ✅ | ✅ | ✅ |
| **2D lighting** | | | | |
| 2D lights and shadows | ✅ | 🟡 | ✅ | ✅ |
| Normal map studio | ✅ | ✅ | — | ✅ |
| **Tilemaps and tile volumes** | | | | |
| Tilemaps (ortho + iso, overhang) | ✅ | ✅ | ✅ | ✅ |
| Autotiling / rule tiles | ✅ | 🟡 | ✅ | ✅ |
| Stackable tile volumes | ✅ | 🟡 | 🟡 | ❌ |
| Navigation over tile volume surfaces | ✅ | — | ✅ | ❌ |
| **World presentation** | | | | |
| Camera follow / confine / shake | ✅ | 🟡 | 🟡 | ✅ |
| 3D directional + ambient lighting | 🟡 | 🟡 | 🟡 | 🟡 |
| Voxel ambient occlusion / contact depth | 🟡 | 🟡 | ❌ | 🟡 |
| Post-processing stack | 🟡 | 🟡 | ✅ | ✅ |
| Authored fog/AO/post stack in shipped project hosts | ✅ | — | ✅ | ✅ |
| **3D models** | | | | |
| glTF / model import | 🟡 | ❌ | ❌ | ✅ |
| **Not yet** | | | | |
| Font fallback and richer spans | ❌ | ❌ | — | — |
| Skeletal animation | ❌ | ❌ | ❌ | ❌ |
| Tile volume terrain tools | ❌ | ❌ | — | — |
| **3D foundation** | | | | |
| 3D orbit / third-person camera | 🟡 | 🟡 | 🟡 | 🟡 |
| Shared standard material factors | 🟡 | 🟡 | ❌ | 🟡 |
| Authored textured geometry normals | 🟡 | ❌ | ❌ | 🟡 |
| Lighting | 🟡 | 🟡 | 🟡 | 🟡 |
| 3D point and spot lights | 🟡 | 🟡 | 🟡 | 🟡 |
| Runtime ambient and background controls | 🟡 | 🟡 | 🟡 | 🟡 |
| Scripted day/night cycle | 🟡 | 🟡 | 🟡 | 🟡 |
| Retained inline mesh geometry | 🟡 | 🟡 | — | 🟡 |
| Authored material sidedness | 🟡 | 🟡 | — | 🟡 |
| Shared render material profiles | 🟡 | 🟡 | — | 🟡 |
| **3D rendering** | | | | |
| Imported-model occlusion silhouettes | ✅ | ❌ | — | ✅ |

### Hosts and GPU

<a id="hosts"></a>

#### Native desktop

One `Game` loop runs windowed on desktop, in a browser and headless, with failures reaching the host whole.

Engine ✅ · Editor ✅ · Decay — · Proof ✅ — parity row *Native desktop*

**Tested by:** `crates/sindri-desktop/src/host/app.rs` (`crates/sindri-desktop/src/host/app.rs` in the Sindri repository), `crates/sindri-platform/tests/game_loop.rs` (`crates/sindri-platform/tests/game_loop.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**See:** `docs/platform-host.md` (`docs/platform-host.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`Game` receives `start`, `fixed_update`, `update`, and `stop`, each with a
`FrameContext` carrying the world, the input state, and frame time, and each
able to fail in a way that reaches the host rather than being swallowed. The
same loop runs windowed on a desktop, in a browser through the same `winit`
event loop and canvas attachment, and headless in a test.

`DesktopApp` supplies the window, event loop, and input translation once, so
an application does not rewrite them. Target-specific code is confined to the
crates that must have it. Application failures retain their complete
`std::error::Error` source chain when logged natively or announced through the
browser's `sindri:failed` event, rather than exposing only the outer host
error.

</details>

<a id="browser"></a>

#### Browser / WASM

The engine runs in Chromium over WebGPU with the same pictures and the same Decay behaviour as native.

Engine ✅ · Editor — · Decay — · Proof ✅ — parity row *Browser / WASM*

**Tested by:** `scripts/browser/smoke.mjs` (`scripts/browser/smoke.mjs` in the Sindri repository), `crates/sindri-export/tests/exports_a_project.rs` (`crates/sindri-export/tests/exports_a_project.rs` in the Sindri repository)

**Exercised in:** `examples/cube` (`examples/cube` in the Sindri repository), `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/browser.md` (in `10-export-and-web.md`)

<details markdown="1">
<summary>Evidence</summary>

The cube example draws the same picture in Chromium as it does natively, in
the same colours, over WebGPU, and games are playable there with Decay
executing on the browser target. `scripts/browser/smoke.mjs` checks that a page
starts the engine and fails when it does not.

</details>

<a id="gpu-negotiation"></a>

#### GPU negotiation and swapchain policy

Shared adapter/device negotiation with conservative limits, one policy for every swapchain outcome, and enforced sRGB.

Engine ✅ · Editor — · Decay — · Proof ✅

**Tested by:** `crates/sindri-gpu/src/surface/tests.rs` (`crates/sindri-gpu/src/surface/tests.rs` in the Sindri repository)

**Exercised in:** `examples/triangle` (`examples/triangle` in the Sindri repository), `examples/cube` (`examples/cube` in the Sindri repository)

**See:** `docs/rendering-surface.md` (`docs/rendering-surface.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Adapter, device, and queue negotiation is shared, with conservative
cross-target limits, resource labels, and errors that name what failed.
Swapchain acquisition has seven outcomes and one policy applied to all of
them, so hosts do not re-derive when to skip, reconfigure, or recreate.
Surfaces must negotiate an sRGB format or fail loudly. Headless adapter
initialisation is proven in CI on software Vulkan.

</details>

### Frame and scene extraction

<a id="frame-pipeline"></a>

#### Extraction, preparation and ordered passes

Frames are extracted from the world, validated and ordered deterministically by stage, layer and insertion.

Engine ✅ · Editor ✅ · Decay — · Proof ✅

**Tested by:** `crates/sindri-render/src/frame.rs` (`crates/sindri-render/src/frame.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/passes.rs` (`crates/sindri-scene/tests/extraction/passes.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/textures.rs` (`crates/sindri-scene/tests/extraction/textures.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/assets.rs` (`crates/sindri-scene/tests/extraction/assets.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/rendering-frame-pipeline.md` (`docs/rendering-frame-pipeline.md` in the Sindri repository), `docs/scene-extraction.md` (`docs/scene-extraction.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

A frame goes through extraction, preparation, and rendering. Preparation
validates and orders passes deterministically by stage, then layer, then
insertion order; stage order is `Opaque3d`, `Transparent2d`, `Overlay`.

Gameplay writes to the world and nothing tells the renderer. `SceneExtractor`
derives an ordered frame from whatever the world currently holds, using the
built-in camera, mesh, sprite, tilemap, animation, and text schemas. No scene
needs hand-written extraction code.

Textures bind by reference: a scene names `textures/badge.png`, the renderer
knows only a `TextureId`, and one place knows both. An unbound reference draws
a magenta checker and is reported by name rather than failing the frame or
silently reusing the last texture.

</details>

<a id="cameras-and-capture"></a>

#### Cameras, depth, cube primitive, inline textured surface mesh

Perspective and orthographic cameras with tested projection maths, depth, and deterministic offscreen PNG capture.

Engine 🟡 · Editor 🟡 · Decay ❌ · Proof 🟡 — parity row *Cameras, depth, cube primitive, inline textured surface mesh*

**Tested by:** `crates/sindri-render/src/camera.rs` (`crates/sindri-render/src/camera.rs` in the Sindri repository), `crates/sindri-render/src/offscreen.rs` (`crates/sindri-render/src/offscreen.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/cameras.rs` (`crates/sindri-scene/tests/extraction/cameras.rs` in the Sindri repository)

**Exercised in:** `examples/cube` (`examples/cube` in the Sindri repository)

**See:** `docs/cameras.md` (in `06-2d.md`), `docs/rendering-color.md` (`docs/rendering-color.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

What can be drawn includes a triangle, a coloured cube, and a textured cube
with depth testing. The frame clears once before anything draws, so a scene of
only sprites has a depth buffer to test against. Perspective and orthographic
cameras exist with tested projection maths, and the zero-to-one depth range and
Y-up convention are chosen in one place rather than at each call site. Colour
space is enforced by a shared constant, and the offscreen capture verifies
authored colours actually survive to the pixels. Offscreen rendering produces a
deterministic PNG, which CI uploads.

</details>

### Sprites and shapes

<a id="sprites"></a>

#### Sprites, sheets, UVs, layers, blending

Textured sprites in screen or world space, batched per space, layer and texture, cut by named sheet parts.

Engine ✅ · Editor ✅ · Decay 🟡 · Proof ✅ — parity row *Sprites, sheets, UVs, layers, blending*

**Tested by:** `crates/sindri-gpu/tests/sprite_batches_are_independent.rs` (`crates/sindri-gpu/tests/sprite_batches_are_independent.rs` in the Sindri repository), `crates/sindri-gpu/tests/sprite_uv_rect.rs` (`crates/sindri-gpu/tests/sprite_uv_rect.rs` in the Sindri repository), `crates/sindri-gpu/tests/world_sprites.rs` (`crates/sindri-gpu/tests/world_sprites.rs` in the Sindri repository), `crates/sindri-gpu/tests/sprite_textures_follow_their_registry.rs` (`crates/sindri-gpu/tests/sprite_textures_follow_their_registry.rs` in the Sindri repository), `crates/sindri-render/src/sprite_batch/tests.rs` (`crates/sindri-render/src/sprite_batch/tests.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/sprites.rs` (`crates/sindri-scene/tests/extraction/sprites.rs` in the Sindri repository), `crates/sindri-core/src/sheet/tests.rs` (`crates/sindri-core/src/sheet/tests.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Textured sprites carry tint, anchor, and layer. A sprite is either
screen-anchored, which is the default and cannot be occluded by the world, or
in the world, drawn through the world camera by its full transform and hidden
by opaque geometry in front of it. Sprites batch per space, layer, and texture,
with a measured five-to-one draw-call reduction. Each batch draws with its own
camera and its own instances, from buffers of its own; a GPU test draws two
batches at two scales into one frame and reads back the pixels that only come
out right when they stayed separate.

**A sliced image says how it is cut, once.** A sheet document beside a
texture — `textures/tiles.png` is sliced by `textures/tiles.sheet`, at a
derived ID nothing has to declare — names the parts of it, either as a grid or
as explicit rects. A scene then writes `textures/tiles.png#floor`, and the `#`
that had always been rejected inside an asset ID is what carries the name,
because it was reserved so a fragment could not leak into a path. Names rather
than indices, so a re-slice that moves a cell does not silently change what a
scene draws. Rects are checked once, where a sheet binds, and ride on the
instance so every part of one sheet stays in one draw call — a GPU test reads
the pixels back to prove the shader honours it. A name no loaded sheet places
draws the whole image and is reported by `unresolved_sprites`, the same rule an
unbound texture follows.

</details>

<a id="sprite-colour-transform"></a>

#### Advanced sprite colour transform

Per-instance RGBA multipliers and offsets, `sample * tint * multiply + offset`, without breaking batches.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Advanced sprite colour transform*

**Tested by:** `crates/sindri-scene/tests/extraction/sprites.rs` (`crates/sindri-scene/tests/extraction/sprites.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**See:** `docs/color-transform-design.md` (`docs/color-transform-design.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

A sprite may carry independent RGBA multipliers and offsets evaluated as
`sample * tint * multiply + offset`, travelling per instance so two sprites
recoloured differently still share a draw call. The identity is
`multiply = [1, 1, 1, 1]` and `offset = [0, 0, 0, 0]`, so a sprite authored
before the transform existed draws exactly as it did. A transform that is not
finite is refused at extraction rather than drawn — the reachable case is
narrowing, since `1e39` is an ordinary `f64` and an infinity once it is an
`f32` — while a finite value outside zero to one is left alone for the render
target to clip. The editor draws the two quadruples as labelled RGBA rows inside
an advanced section, collapsed until the scene has authored one and resettable
to the identity. Orbital's mine blast flashes white-hot through the colour
offset, which a tint cannot do, and cools within a few frames.

</details>

<a id="procedural-shapes"></a>

#### Procedural shapes

Rectangles, ellipses, grids, regular polygons and up-to-eight-point hulls evaluated crisply in the shape shader.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Procedural shapes*

**Tested by:** `crates/sindri-render/src/shape/mod.rs` (`crates/sindri-render/src/shape/mod.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_authors_shape.rs` (`crates/sindri-decay/tests/a_script_authors_shape.rs` in the Sindri repository), `games/orbital-baked/tests/every_boss_is_its_own.rs` (`games/orbital-baked/tests/every_boss_is_its_own.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Rectangles, ellipses, grids, and regular polygons are evaluated in the shape
shader. A polygon may also carry up to eight explicit 2D vertices; those points
travel in the same instanced payload and the shader measures against the
actual authored edges, so an irregular hull remains crisp without becoming a
texture. When no points are authored, regular polygons keep their existing
path. The eight-point cap keeps the per-instance vertex attributes inside
Sindri's conservative WebGPU limits. Orbital Last Stand exercises the
capability with the Strider's exact six-point hull.

</details>

<a id="sprite-shaders"></a>

#### Custom shaders / materials

A sprite can be drawn through a project `.wgsl` function, validated with naga by every host and hot-reloaded in the editor.

Engine 🟡 · Editor ✅ · Decay ✅ · Proof 🟡 — parity row *Custom shaders / materials*

**Tested by:** `crates/sindri-render/src/sprite_shader/tests.rs` (`crates/sindri-render/src/sprite_shader/tests.rs` in the Sindri repository), `crates/sindri-gpu/tests/sprite_shaders.rs` (`crates/sindri-gpu/tests/sprite_shaders.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/sprite_shaders.rs` (`crates/sindri-scene/tests/extraction/sprite_shaders.rs` in the Sindri repository), `game/tests/the_sprite_shader_demo_works.rs` (`game/tests/the_sprite_shader_demo_works.rs` in the Sindri repository)

**Exercised in:** `examples/sprite-shaders` (`examples/sprite-shaders` in the Sindri repository)

**See:** `docs/sprite-shaders.md` (in `06-2d.md`)

**Limits:**

- Shaders reach `sindri.sprite` only, with one texture and no material asset.

<details markdown="1">
<summary>Evidence</summary>

`sindri.sprite` names a `.wgsl` asset in `shader` and carries four
`shader_params`. The file defines
`fn sprite_shader(input: SpriteShaderInput) -> vec4<f32>`, handed the pixel as
it would draw unshaded, its UVs and the sprite's UV rect, its world position,
the four parameters and the scene's clock, with `sample_sprite` and
`sprite_texel_size` to read the texture again; the engine supplies the vertex
stage and pipeline around it. Parameters ride on the instance, so sprites
sharing a texture and a shader share a draw. Every host validates a shader with
naga as it binds it: the editor loads, watches and hot-reloads the ones a scene
names, `sindri-player` reads them natively and from the export manifest, and
`sindri-export` refuses one that does not compile. A failure names the file and
the line in the project's own numbering and the sprite falls back to the
built-in look. Decay writes `this.sprite.shader_params.{x,y,z,w}`.
`examples/sprite-shaders` draws an outline, a dissolve, a hit flash and a wave.

</details>

<a id="sprite-masks"></a>

#### Sprite masking / stencil

`sindri.sprite_mask` lets sprites draw only inside or outside a soft alpha shape, without a stencil or broken batches.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Sprite masking / stencil*

**Tested by:** `crates/sindri-gpu/tests/sprite_masks.rs` (`crates/sindri-gpu/tests/sprite_masks.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/sprite_masks.rs` (`crates/sindri-scene/tests/extraction/sprite_masks.rs` in the Sindri repository), `game/tests/the_sprite_mask_demo_works.rs` (`game/tests/the_sprite_mask_demo_works.rs` in the Sindri repository)

**Exercised in:** `examples/sprite-masks` (`examples/sprite-masks` in the Sindri repository)

**See:** `docs/sprite-masks.md` (in `06-2d.md`)

**Limits:**

- World sprites only.

<details markdown="1">
<summary>Evidence</summary>

`sindri.sprite_mask` places a picture whose alpha is a shape, and a sprite with
`mask: inside` or `outside` and the mask's name draws only within or beyond it,
softly where the mask is soft. The mask is batch state read in the fragment
stage, so masked sprites still share draws and nothing needs a stencil.
`examples/sprite-masks` reveals a meadow and a hidden gem through a wandering
keyhole.

</details>

<a id="nine-slice"></a>

#### Nine-slice sprites

A `sliced` sprite or UI image keeps its sheet-recorded border and stretches only the middle, in one draw.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Nine-slice sprites*

**Tested by:** `crates/sindri-scene/tests/extraction/nine_slice.rs` (`crates/sindri-scene/tests/extraction/nine_slice.rs` in the Sindri repository), `games/platformer/tests/sliced_panels.rs` (`games/platformer/tests/sliced_panels.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/nine-slice.md` (in `06-2d.md`)

<details markdown="1">
<summary>Evidence</summary>

A sprite or UI image drawn `sliced` keeps the border its sheet records, in
pixels by sprite name, and stretches only the middle: nine quads in the image's
own space, sharing one draw. `border_scale` sets how big a border pixel is
drawn. The sheet slicer drags the border's guides on the image. The
platformer's HUD frame and signboard are one 24-pixel panel.

</details>

### Sprite animation

<a id="sprite-animation"></a>

#### Sprite frame clips, timing, loop state

Clips of named sheet parts, played as runtime state and chosen by scripts without dirtying the scene.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Sprite frame clips, timing, loop state*

**Tested by:** `crates/sindri-scene/src/animation/tests.rs` (`crates/sindri-scene/src/animation/tests.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/animation.rs` (`crates/sindri-scene/tests/extraction/animation.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_plays_a_clip.rs` (`crates/sindri-decay/tests/a_script_plays_a_clip.rs` in the Sindri repository), `games/low-tide/tests/a_crawler_looks_alive.rs` (`games/low-tide/tests/a_crawler_looks_alive.rs` in the Sindri repository)

**Exercised in:** `games/low-tide` (`games/low-tide` in the Sindri repository), `games/platformer` (`games/platformer` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`sindri.animation.sprite` holds clips of sprite names and which one plays,
`SpriteAnimations` holds where each sprite has got to, and the cursor holds a
name rather than a rect so playback does not depend on where anything sits in
an image. Playback is runtime state, so watching an animation run does not
rewrite the scene it came from.

`Animation.play`, `stop` and `restart` name one of the clips the scene
authored; `is_finished`, `frame` and `clip` read where playback has got to;
`set_speed` scales it. Choosing is authored state and writes the component, so
it lives in the world and undoes and saves like anything else, while the cursor
is derived and is only read — which is what keeps a script driving an
animation from dirtying the scene. `play` is idempotent so that the natural way
to write it (`if moving { play("walk") }`, named on every frame) runs the clip
instead of pinning it to frame one; `restart` is the separate thing to want.

An animated sprite whose reference names no part of its sheet draws the first
frame of the clip it is playing rather than the whole sheet, so a scene shows a
pose before anything has run it — in a game's opening frame, in an offscreen
capture, and in the editor outside play mode. Once a clip is running it decides
alone: a frame that resolves to nothing draws the whole image rather than the
resting pose, because a plausible picture of the wrong moment is the failure
that hides.

Low Tide exercises speed-controlled clips on children of a moving deck: its two
tread belts roll with actual movement, pivot in opposite directions, hold their
frame while flooded and resume after the ebb.

</details>

<a id="sheet-slicer"></a>

#### Clip authoring and preview

The Sprite sheet tab slices an image on the canvas, names cells, and the animation inspector builds and previews clips.

Engine — · Editor ✅ · Decay — · Proof ✅ — parity row *Clip authoring and preview*

**Tested by:** `editor/src/slicer/tests.rs` (`editor/src/slicer/tests.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Selecting a texture opens it in the Sprite sheet tab, a top-row tab beside the
Scene and Game views, where the image fills the canvas pixel-sharp on a
checkerboard, zooms about the pointer with the wheel (5% to 6400%), pans with
the middle or right button, and fits back with Fit. Each cell is outlined on
the picture, named cells are labelled once there is room, and with nothing open
the tab lists every image in the project. The slice itself is the inspector's
while an image is open: columns, rows, margin and spacing are drags, so a
packed sheet with gutters can be cut and not only one that divides edge to
edge. A cell is named by clicking it, and the panel lists the cells that have
names rather than a field per cell, so a 16x16 atlas is as workable as a
four-frame strip. Saving writes the sidecar, and the browser then lists the
sprites underneath the image, collapsed until asked for. A sprite's animation
inspector creates, renames, and removes clips from those named cells, orders
frames, edits timing and looping, chooses the runtime clip, and previews it
without changing scene state.

</details>

### Text

<a id="world-text"></a>

#### World-space text

`sindri.text` places a label in world units over the sprites of its layer.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *World-space text*

**Tested by:** `crates/sindri-scene/tests/extraction/world_text.rs` (`crates/sindri-scene/tests/extraction/world_text.rs` in the Sindri repository), `crates/sindri-render/tests/text_measurement.rs` (`crates/sindri-render/tests/text_measurement.rs` in the Sindri repository), `games/orbital-baked/tests/damage_numbers.rs` (`games/orbital-baked/tests/damage_numbers.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**See:** `docs/world-text.md` (in `06-2d.md`)

<details markdown="1">
<summary>Evidence</summary>

`sindri.text` is the UI label's fields in world units, placed at its entity and
drawn over the sprites of its layer. Decay writes the words with `Ui.set_text`
and `Ui.set_number` and reaches `this.world_text.color`. Orbital's landed shots
float their damage.

</details>

<a id="rich-text"></a>

#### Rich text

With `rich: true`, `[b]`, `[i]` and `[color=#rrggbb]` tags in UI or world text are spans.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Rich text*

**Tested by:** `crates/sindri-render/src/text/rich.rs` (`crates/sindri-render/src/text/rich.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/text.rs` (`crates/sindri-scene/tests/extraction/text.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

With `rich: true`, `[b]`, `[i]` and `[color=#rrggbb]` tags in either kind of
text are spans. Orbital's critical hits float their damage in bold gold.

</details>

### 2D lighting

<a id="lights-2d"></a>

#### 2D lights and shadows

Ambient plus point lights, normal maps and polar-depth shadows from occluders and tilemaps, for sprites, tiles and maps.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *2D lights and shadows*

**Tested by:** `crates/sindri-render/src/light2d/tests.rs` (`crates/sindri-render/src/light2d/tests.rs` in the Sindri repository), `crates/sindri-gpu/tests/sprite_lights.rs` (`crates/sindri-gpu/tests/sprite_lights.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/lights_2d.rs` (`crates/sindri-scene/tests/extraction/lights_2d.rs` in the Sindri repository), `games/platformer/tests/cave_lights.rs` (`games/platformer/tests/cave_lights.rs` in the Sindri repository), `games/low-tide/tests/the_basin_is_lit.rs` (`games/low-tide/tests/the_basin_is_lit.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `games/low-tide` (`games/low-tide` in the Sindri repository)

**See:** `docs/2d-lights.md` (in `06-2d.md`)

**Limits:**

- A frame carries the 16 lights nearest the camera.

<details markdown="1">
<summary>Evidence</summary>

With `lighting_2d` on in `sindri.environment`, world sprites, tilemaps, flecks
and world shapes are multiplied by the light reaching them: an ambient color
plus every `sindri.light2d` point light within its radius, faded by its
falloff. A sprite opts out with `unlit`, additive shapes are light themselves,
and the UI is never lit. A sprite or tilemap may name a `normal_map` laid out
like its texture; a light's `height` then decides how squarely it meets each
texel, so a low light rakes across the relief. A light with `shadows` on is
blocked by `sindri.light2d.occluder` boxes, polygons and the solid cells of a
tilemap, through one-dimensional polar depth maps built on the CPU each frame. An optional five-sample area light produces approximate soft shadows; Lamplighter uses it for its lanterns.
Decay writes a light's color, intensity, radius, falloff and height. The Scene
view draws light radii and occluder outlines with handles. The platformer's
cave is the proof. A voxel world viewed as a map is lit too, and its
`normal_maps` name a normal map for each texture its squares draw from: Low
Tide's Basin is lit by a sun that follows the tide from noon through a golden
hour to a moonlit flood, and its crawler carries lamps and shadow-casting
headlights.

</details>

<a id="normal-map-studio"></a>

#### Normal map studio

The Normal map tab derives a sprite's normal map and previews it under the game's own 2D lighting.

Engine ✅ · Editor ✅ · Decay — · Proof ✅

**Tested by:** `crates/sindri-assets/src/normal_map/tests.rs` (`crates/sindri-assets/src/normal_map/tests.rs` in the Sindri repository), `tools/normal-map-web/src/lib.rs` (`tools/normal-map-web/src/lib.rs` in the Sindri repository), `editor/src/normal_studio/tests.rs` (`editor/src/normal_studio/tests.rs` in the Sindri repository), `editor/src/native/normal_studio/scene/tests.rs` (`editor/src/native/normal_studio/scene/tests.rs` in the Sindri repository), `games/platformer/tests/studio_normal_map.rs` (`games/platformer/tests/studio_normal_map.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/normal-map-studio.md` (`docs/normal-map-studio.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

The responsive browser studio uses the same generation library and saves PNG
and settings files; its lighting preview is approximate. The editor Normal map
tab derives a sprite's `normal_map` from its brightness or a
painted height map, keeping its size, alpha and pixel edges, with each sheet
cell kept apart, and material presets as starting values. The preview is the
game's own 2D lighting rather than an imitation: the studio's surface, light
and ambient are a `sindri.sprite`, a `sindri.light2d` and a
`sindri.environment` in a world of its own, extracted and drawn as the Scene
view draws, and a test requires the frame to equal the one a scene using the
saved map extracts. It shows the map per channel, splits it against the sprite
lit without a map, and orbits a plane in 3D. Saving writes the map and a
`.normalmap` of the settings, so the map is made again from changed art; the
platformer's hero is lit through one made this way.

</details>

### Tilemaps and tile volumes

<a id="tilemaps"></a>

#### Tilemaps (ortho + iso, overhang)

One entity draws a grid of sheet tiles into the same batches as loose sprites, painted in the Scene view.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Tilemaps (ortho + iso, overhang)*

**Tested by:** `crates/sindri-scene/tests/extraction/tilemap.rs` (`crates/sindri-scene/tests/extraction/tilemap.rs` in the Sindri repository), `editor/src/tilemap/tests.rs` (`editor/src/tilemap/tests.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `games/low-tide` (`games/low-tide` in the Sindri repository)

**Limits:**

- Screen-space tilemaps render and edit as data, but direct painting is limited to world space.

<details markdown="1">
<summary>Evidence</summary>

A tilemap is a grid of tiles drawn from one entity: `sindri.tilemap` carries
the map's grid, a palette of sprite names, and a flat array of cells indexing
that palette, with `null` where the map is empty. Its cells become instances in
the same batches loose sprites use, so a prop sorts among the floor rather than
behind it. Columns and rows lay out orthogonally or isometrically, and placing a
tile and finding the tile under a point are inverses on every cell of both.

Selecting a world-space tilemap shows the slices from its texture's sheet as a
visual palette, lets its grid be resized while preserving the overlap, and
turns primary drag in the Scene view into an undoable paint or erase stroke.
The hovered cell is found by inverting the exact camera matrix used to render
that viewport, so an orthographic or isometric map does not need separate
picking approximations. The ordinary component fields still edit projection,
tile size, tint, render layer, and space.

</details>

<a id="autotiling"></a>

#### Autotiling / rule tiles

A sheet declares 16-edge, 47-blob or 16-corner terrains; painting one redraws every tile that depends on the cell or corner.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Autotiling / rule tiles*

**Tested by:** `crates/sindri-core/src/terrain.rs` (`crates/sindri-core/src/terrain.rs` in the Sindri repository), `crates/sindri-scene/src/terrain_paint_tests.rs` (`crates/sindri-scene/src/terrain_paint_tests.rs` in the Sindri repository), `crates/sindri-scene/src/terrain_corners_tests.rs` (`crates/sindri-scene/src/terrain_corners_tests.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_paints_terrain.rs` (`crates/sindri-decay/tests/a_script_paints_terrain.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_paints_corners.rs` (`crates/sindri-decay/tests/a_script_paints_corners.rs` in the Sindri repository), `games/lamplighter/tests/the_ground_is_painted.rs` (`games/lamplighter/tests/the_ground_is_painted.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `games/lamplighter` (`games/lamplighter` in the Sindri repository)

**See:** `docs/terrains.md` (in `06-2d.md`)

**Limits:**

- Corners terrains are proven only by Lamplighter's one ground terrain; the platformer proves edge and blob terrains.
- The editor brush does not offer a corners terrain.
- No random variants, probabilities or custom rules, and no Tiled map import.

<details markdown="1">
<summary>Evidence</summary>

A sheet can declare terrains: the sixteen `edges` or forty-seven `blob`
sprites a cell draws for each pattern of same-terrain neighbors. The tilemap
brush and `Grid.set_terrain` paint a terrain and redraw every neighbor whose
picture depends on the cell, erasing included. The platformer's level is
painted with its ground terrain.

A `corners` terrain is sixteen sprites chosen by which of a tile's four corners
are the terrain, the layout of a Wang or Tiled corner set. It is painted by
corner: `Grid.set_terrain_corner` sets or clears the corner at a grid line's
crossing and rewrites the up to four tiles that meet there, each reading its
own corners back from the sprite it already shows. This was found by trying to
use the first tileset Pixel Lab generated for Lamplighter, which is laid out
this way. Lamplighter's ground is that tileset: `scripts/ground.decay` lays its
dirt path corner by corner, and a test reads the tiles back.

</details>

<a id="tile-volumes"></a>

#### Stackable tile volumes

Sparse `(column, row, level)` cells from a `.tileset`, resolved into only the faces nothing covers, in either projection.

Engine ✅ · Editor 🟡 · Decay 🟡 · Proof ❌ — parity row *Stackable tile volumes*

**Tested by:** `crates/sindri-core/src/tileset/tests.rs` (`crates/sindri-core/src/tileset/tests.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/tile_volume.rs` (`crates/sindri-scene/tests/extraction/tile_volume.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/baked_volume.rs` (`crates/sindri-scene/tests/extraction/baked_volume.rs` in the Sindri repository), `crates/sindri-scene/src/tile_chunk.rs` (`crates/sindri-scene/src/tile_chunk.rs` in the Sindri repository), `editor/src/tile_volume/tests.rs` (`editor/src/tile_volume/tests.rs` in the Sindri repository)

**Limits:**

- No game in the repository uses a tile volume since Causeway moved onto the voxel world.

<details markdown="1">
<summary>Evidence</summary>

`sindri.tile_grid` carries the geometry — columns, rows, cell size,
projection, and the screen step one level costs — and `sindri.tile_volume`
carries sparse cells at integer `(column, row, level)` coordinates naming tiles
in a reusable `.tileset` asset. Empty is absence rather than a reserved ID, so
an empty sky costs nothing to store. A tile says what its faces look like,
whether it hides a neighbour's shared face, whether it is solid, and how much of
its cell it fills; the renderer resolves each occupied cell into only the faces
nothing covers, so a column two blocks taller than its neighbour exposes two
side faces without a cliff case and removing a lower block opens a real hole.

**A tile can be shorter than its cell.** `height` is a fraction of a cell
measured from its floor, defaulting to one. A half is a slab. The value is
logical: it decides what the tile hides and how high something standing on it
stands, and says nothing about the art. Anything taller than one cell is
refused when the asset decodes, because occlusion and occupancy both assume a
cell's contents stay inside it. A neighbour hides a side face only when it is at
least as tall as the face it abuts, and a top face survives a block placed
above it unless the tile fills its cell.

**Both projections are real.** `orthogonal` or `isometric` decides where a cell
sits, which of its faces a view can see, and what "behind" means. One tile set
serves both: faces a projection cannot see are left undrawn. Depth belongs to a
cell's column rather than to each drawn face, so a tower keeps the depth of the
column it stands in.

**Runtime tile volumes have one engine-owned chunk unit.** A chunk is 16×16
columns, addressed with Euclidean coordinates and held sparsely by
`TileChunkStore`; materializing it produces the existing scene component.

**A volume is resolved once, not every frame.** It is resolved into faces once
and replayed, with only the depth each cell sorts at measured again. `World`
gives each entity a revision, bumped whenever anything takes a mutable borrow
of it, and `TextureBindings` and `TileSetBindings` each carry a generation,
handed out process-wide; a volume keeps the ones it was built from, so editing
its cells, moving it, changing its grid, rebinding its tile set and re-cutting
its atlas each show up on the next frame. `baked_volume.rs` is one test per
input: each edits exactly one of them and fails if the volume goes on drawing
what it drew before.

**A script reaches one cell at a time.** `Grid.block(volume, column, row,
level)` answers with the tile a cell holds and `Grid.set_block` writes one,
with the empty string meaning absence in both directions. A write naming a tile
the volume's own tile set does not define is refused at the call.

</details>

<a id="tile-volume-navigation"></a>

#### Navigation over tile volume surfaces

A volume's walkable surface becomes grid walls by `max_step`, and `Grid.walkable` asks the same question from a script.

Engine ✅ · Editor — · Decay ✅ · Proof ❌

**Tested by:** `crates/sindri-scene/tests/navigation/volume.rs` (`crates/sindri-scene/tests/navigation/volume.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_asks_what_is_walkable.rs` (`crates/sindri-decay/tests/a_script_asks_what_is_walkable.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/standing_on_ground.rs` (`crates/sindri-scene/tests/extraction/standing_on_ground.rs` in the Sindri repository)

**Limits:**

- `Grid.walkable` is about a cell, not an edge, so a script-driven walker does not honour `max_step`.
- Separate climb and drop limits wait on directional edges.

<details markdown="1">
<summary>Evidence</summary>

A volume is three-dimensional and grid navigation is not, so the bridge is a
*surface*: for each column, the height of the ground something walking across
it stands on — the top of its highest solid cell, which is a fraction rather
than a level because a slab's top genuinely sits half a cell lower than a
block's. A column holding nothing solid has no surface at all: it is a hole.
Every edge between two columns is then either a step something can take or one
it cannot, and `sindri-grid` already refuses to cross a blocked edge — so the
volume becomes walls rather than the pathfinder learning about levels.
`sindri.grid.navigation` carries `max_step`, one cell by default.

A grid still carrying `sindri.tilemap` navigates by that flat map alone, so
dropping a volume beside an existing floor never closes ground the game already
walks on. Only `WorldGridNavigation::from_world_with_tile_sets` can answer at
all, because which cells are solid and how tall they are is a question only the
tile set answers.

`Grid.walkable(floor, x, y)` answers whether a walker can stand where a point
falls, from the walkable surface the pathfinder walks, choosing the cell by
`sindri_scene::nearest_cell`, the same rule a walker's own placement reads
from. Off the grid, water, decoration and empty columns all answer the same
way. An authored occupant rests on whatever *supports* it; a walker stands only
on what is *walkable*.

</details>

### World presentation

<a id="camera-view-bounds"></a>

#### Camera follow / confine / shake

Orthographic 2D camera confinement keeps the view within level edges and can look ahead in portrait as the target moves.

Engine ✅ · Editor 🟡 · Decay 🟡 · Proof ✅ — parity row *Camera follow / confine / shake*

**Tested by:** `crates/sindri-scene/src/components/camera_confine.rs` (`crates/sindri-scene/src/components/camera_confine.rs` in the Sindri repository), `games/platformer/tests/the_shell.rs` (`games/platformer/tests/the_shell.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository)

**See:** `docs/cameras.md` (in `06-2d.md`)

**Limits:**

- Viewport confinement assumes an unrotated, unscaled 2D camera and an untransformed scene root; shake can cross the bounds.

<a id="environment-lighting"></a>

#### 3D directional + ambient lighting

`sindri.environment` and a directional `sindri.light` shade textured 3D geometry, with one bounded shadow map.

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *3D directional + ambient lighting*

**Tested by:** `crates/sindri-gpu/tests/sunlight_faces.rs` (`crates/sindri-gpu/tests/sunlight_faces.rs` in the Sindri repository), `crates/sindri-decay/tests/lights3d.rs` (`crates/sindri-decay/tests/lights3d.rs` in the Sindri repository)

**Exercised in:** `games/voxel-lab` (`games/voxel-lab` in the Sindri repository), `examples/lighting3d` (`examples/lighting3d` in the Sindri repository), `examples/daynight` (`examples/daynight` in the Sindri repository)

**See:** `docs/world-presentation-plan.md` (`docs/world-presentation-plan.md` in the Sindri repository)

**Limits:**

- Sky, environment profiles/volumes, weather, water, full material assets and cascaded shadows remain future slices; authored fog and post effects reach the shared project host.

<details markdown="1">
<summary>Evidence</summary>

`sindri.environment` is authored scene state for background, ambient fill,
directional shadows, voxel contact-depth ambient occlusion, and an ordered
world post stack. The sun is a `sindri.light` entity (kind `directional`) aimed
by its rotation along local -Z and drawn and aimed in the Scene view;
`SceneExtractor::lighting` combines it with the environment's ambient into
renderer-owned `WorldLighting`, tolerating an invalid light in the editor;
textured cubes, authored meshes, and cached voxel meshes share that model in
editor and browser rendering. The default renderer lighting preserves the
previous unlit appearance for scenes without an environment. Voxel Lab is the
acceptance surface. Directional shadows use one bounded depth map with authored
coverage, 256–2048 resolution, and bias controls; both transient world meshes
and persistent voxel meshes cast and receive them.


`this.light` and `Entity.light` expose color, intensity, range, spotlight
half-angles and independent boolean enabled state from the shared typed surface.
Setters validate full candidates before mutation, including f32 narrowing and
cross-field cone bounds; failed writes retain payloads and whole colors are
atomic. Reads honor omitted defaults; Color alpha stays one. The enabled flag
changes illumination without deactivating the entity or stopping its scripts.
`examples/lighting3d` is feature proof for Explorer's planned lighting track:
its checked script animates parented points/spots and the sun, reaches lit/dark
Session goals and exercises cube, inline and imported geometry. Completion,
hover, generated docs and typed batch checks share the same metadata. Native,
real WebGPU and editor Play controls are recorded in `3d-lights.md`. In-tree
game adoption and local shadows remain
open; no new uniform or pass cost is introduced.

</details>

<a id="voxel-ao"></a>

#### Voxel ambient occlusion / contact depth

Voxel meshes carry deterministic corner AO controlled by the environment's contact-depth strength.

Engine 🟡 · Editor 🟡 · Decay ❌ · Proof 🟡 — parity row *Voxel ambient occlusion / contact depth*

**Tested by:** `crates/sindri-voxel/src/mesh_tests.rs` (`crates/sindri-voxel/src/mesh_tests.rs` in the Sindri repository)

**Exercised in:** `games/voxel-lab` (`games/voxel-lab` in the Sindri repository)

**Limits:**

- Screen-space AO for arbitrary geometry is absent.

<a id="post-processing"></a>

#### Post-processing stack

Exposure, tone mapping, contrast, saturation, bloom and vignette in one ordered world post path; overlay/UI stays crisp.

Engine 🟡 · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Post-processing stack*

**Tested by:** `crates/sindri-render/src/bloom/mod.rs` (`crates/sindri-render/src/bloom/mod.rs` in the Sindri repository)

**Exercised in:** `games/voxel-lab` (`games/voxel-lab` in the Sindri repository), `game` (`game` in the Sindri repository)

**Limits:**

- LUT grading and advanced cinematic effects remain future slices.

<a id="project-host-fog-post"></a>

#### Authored fog/AO/post stack in shipped project hosts

The shared native/browser player and the offscreen project renderer apply an environment's fog, occlusion, grade and bloom as the editor does

Engine ✅ · Editor — · Decay ✅ · Proof ✅ — parity row *Authored fog/AO/post stack in shipped project hosts*

**Tested by:** `game/tests/the_game_holds_together.rs` (`game/tests/the_game_holds_together.rs` in the Sindri repository), `crates/sindri-decay/tests/environment.rs` (`crates/sindri-decay/tests/environment.rs` in the Sindri repository)

**Exercised in:** `game` (`game` in the Sindri repository)

**See:** `docs/3d-foundation-review.md` (`docs/3d-foundation-review.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`SceneExtractor::world_settings` gathers an environment's light, shadows, fog,
occlusion strength and grade into one `WorldSettings`, and
`TexturedCubeRenderer::configure` applies them, so the shared native/browser
player and the offscreen project renderer light a frame as the editor does. A
frame with post-processing goes through `encode_lit_frame` and its bloom.
Causeway ships tone mapping, grade, vignette and bloom on its lanterns and
beacon this way. Decay reaches the same settings while a game runs:
`this.environment.fog`, `.post_process` and `.bloom` read and write each field,
checked as the scene's own values are, and Causeway eases every island's fog,
grade and bloom from one mood to the next, flashing them for lightning.

</details>

### 3D models

<a id="glb-models"></a>

#### glTF / model import

Self-contained binary GLB decodes to reusable geometry, hierarchy, materials and textures, and draws natively and in the browser.

Engine 🟡 · Editor ❌ · Decay ❌ · Proof ✅ — parity row *glTF / model import*

**Tested by:** `game/tests/the_game_holds_together.rs` (`game/tests/the_game_holds_together.rs` in the Sindri repository), `crates/sindri-assets/src/decode/model/tests.rs` (`crates/sindri-assets/src/decode/model/tests.rs` in the Sindri repository), `crates/sindri-render/src/model/tests.rs` (`crates/sindri-render/src/model/tests.rs` in the Sindri repository), `crates/sindri-gpu/tests/imported_models.rs` (`crates/sindri-gpu/tests/imported_models.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/models.rs` (`crates/sindri-scene/tests/extraction/models.rs` in the Sindri repository), `crates/sindri-export/tests/imported_models.rs` (`crates/sindri-export/tests/imported_models.rs` in the Sindri repository), `crates/sindri/tests/imported_model.rs` (`crates/sindri/tests/imported_model.rs` in the Sindri repository)

**Exercised in:** `game` (`game` in the Sindri repository), `examples/model-collision` (`examples/model-collision` in the Sindri repository)

**See:** `docs/imported-models.md` (in `09-3d.md`)

**Limits:**

- Full model authoring, animation and skinning remain deferred.
- Causeway draws only static, generated models; no flagship imports artist-authored animated GLB yet.

<details markdown="1">
<summary>Evidence</summary>

`ModelAssetDecoder` parses self-contained binary GLB into reusable CPU geometry,
node local matrices/children, materials and embedded base-color textures. The
tiny deterministic fixture exercises both index widths, multiple primitives,
mesh reuse, non-indexed triangles and malformed/unsupported diagnostics.
`AssetLoader` reuse is covered. A separate renderer resource and GPU path retain
normal/UV geometry, hierarchy, textures and materials, composing node and entity
matrices with inverse-transpose normals. GPU readback coverage checks colors,
depth and upload reuse; renderer resource tests check malformed data. Scene
`ModelComponent` references validate asset paths and extract shared resources
with entity world transforms. Disabled models are discovered without drawing;
missing bindings name the asset. The public facade converts decoded models
without cross-coupling foundational crates. The editor catalogue assigns the
registered component an existing icon/family. Render and static-collision asset
fields now declare model metadata, use a GLB-only project picker and complete
Add Component with an actual model. Independent widget/component history scopes
keep the two selections separate; native review verifies Undo/Redo, save/reopen
and Play with an alternate wall. Broader model authoring remains deferred. Model manifests and export retain original GLB bytes and discover
scene/prefab
references, including inactive entities. Native project capture and the WebGPU
project host prepare/bind models once through the existing asset queue and
apply scene lighting. The exact Low Tide cutaway crawler was visually inspected
in both runtimes at 1200 × 1000, correctly oriented and visibly recognizable.
Exported bytes match the source hash. Full editor authoring,
animation and skinning remain deferred. This external proof does not claim
the repository's in-tree game completion surface. Model Collision Lab now also
has native player-window visual proof on X11/software Vulkan, after editor asset
selection and save/reopen; the imported wall blocks keyboard movement and the
walker renders on the goal pad.
Causeway draws every island, figure and building as a generated GLB with
emissive materials, lit by the sun and point lights.
See imported models (in `09-3d.md`).

</details>

### Not yet

<a id="font-fallback"></a>

#### Font fallback and richer spans

There is no font fallback, and rich text has colour, bold and italic spans only.

Engine ❌ · Editor ❌ · Decay — · Proof —

<a id="skeletal-animation"></a>

#### Skeletal animation

Imported models are static: glTF animation and skinning, and further generated primitives, are open.

Engine ❌ · Editor ❌ · Decay ❌ · Proof ❌ — parity row *Skeletal animation*

<a id="tile-volume-terrain"></a>

#### Tile volume terrain tools

Terrain-name painting, automatic supporting strata, side-face picking and slice views for tile volumes remain; see `docs/tile-system-2.md`.

Engine ❌ · Editor ❌ · Decay — · Proof —

### 3D foundation

<a id="orbit-camera-3d"></a>

#### 3D orbit / third-person camera

`sindri.camera.orbit` and explicit `Camera.orbit` controls provide world-space follow, aiming, smoothing and 3D ray pull-in; `Camera.perspective_fov` changes FOV

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *3D orbit / third-person camera*

**Tested by:** `crates/sindri-scene/tests/orbit_cameras.rs` (`crates/sindri-scene/tests/orbit_cameras.rs` in the Sindri repository), `crates/sindri-runtime/tests/an_orbit_camera_project_plays.rs` (`crates/sindri-runtime/tests/an_orbit_camera_project_plays.rs` in the Sindri repository)

**Exercised in:** `examples/orbit` (`examples/orbit` in the Sindri repository)

**See:** `docs/cameras.md` (in `06-2d.md`)

<details markdown="1">
<summary>Evidence</summary>

`examples/orbit` is an authored feature project with a walker, raised goal and
blocking wall. Decay changes the perspective FOV and orbit angles; the shared
runtime follows, aims, pulls in against the wall and recovers after the wall
is disabled. Its project test opens, compiles and runs to the goal. Native and
exported Chromium captures were visually inspected before and after recovery;
the browser fetched the exported assets and reported the scripted goal without
runtime or GPU errors. The flat-color example produces fourteen screenshot
colors, so its local smoke used an eight-color minimum plus blue-walker and
gold-goal pixel checks instead of the generic sixteen-color blank-frame gate. Scene
regressions cover immediate pull-in, smoothed recovery, sensor/layer/self
filtering, rotated and scaled parents, Z locks, invalid settings and payload
preservation. Editor Add Component now creates Camera Orbit from a real active
spatial target with a stable ID; a Camera is required. Invalid numeric or target
edits are refused before commands, retaining the previous component. Unit tests
exercise defaults, self/child/inactive/UI/unidentified exclusions, validation,
Undo/Redo and save/reopen. Native review (`docs/camera-orbit-editor-review.md` in the Sindri repository) removes
the camera script from a scratch Orbit Camera Lab, authors orbit in the inspector
and reaches the walker goal with engine-owned pull-in/recovery. Selected Scene
view guides show the world-space focus, requested position/yaw circle and
actual camera pose during pull-in/recovery, with near-plane clipping and no
picking or world mutations. Parented target/owner, live poses, invalid targets,
overflow and a camera behind the Scene eye have regressions. Collision uses one
sight-line ray and synchronized obstacle poses; camera-volume sweeps and
Explorer's game/browser proof remain pending. Pointer lock has separate native,
browser and editor feature-lab evidence. This is new general camera
capability for Explorer in slice 6 of `docs/3d-update.md`.

Orbit Lab also exercises existing transform scale through normal keyboard
controls: 1 narrows the wall, 2 restores it and R resumes automatic removal.
Manual edits reactivate and retain the barrier. A synchronized center ray must
hit it before an outside ray can verify the narrow/wide state, preventing an
inactive/missing collider from counting as scale proof. Session regression
observes reactivation, repeated edits, orbit recovery/pull-in and the goal;
Chromium drives the same controls and captures both widths. Native
scale review (`docs/collider-scale-editor-review.md` in the Sindri repository) verifies matching mesh/collider
guides, Undo/Redo, save/reopen, Play queries/goal and Stop restoring the saved
scale. This is feature adoption of the existing general 3D scale capability;
Explorer game proof remains open.

</details>

<a id="standard-material-factors"></a>

#### Shared standard material factors

One numeric `MaterialFactors` type and GGX shader serve opt-in cubes, inline/cached geometry, voxel owners and imported primitives

Engine 🟡 · Editor 🟡 · Decay ❌ · Proof 🟡 — parity row *Shared standard material factors*

**Tested by:** `crates/sindri-gpu/tests/standard_materials.rs` (`crates/sindri-gpu/tests/standard_materials.rs` in the Sindri repository)

**Exercised in:** `examples/lighting3d` (`examples/lighting3d` in the Sindri repository)

**See:** `docs/rendering-materials.md` (in `09-3d.md`)

<details markdown="1">
<summary>Evidence</summary>

Shared `MaterialFactors` and direct GGX now shade opt-in cubes, inline/cached
meshes, voxel roots and imported primitives. `sindri.material` authors linear
base color, metallic/roughness and emission, with checked ranges and per-entity
factor replacement. GPU comparisons, zero-light emission, mask-alpha and cache
reuse, scene undo and voxel residency regressions, native/browser swatches and
native editor edit/Undo/Redo/save/reopen review exercise the numeric foundation.
Core GLB emissive factors survive decoding/preparation and render without scene
overrides. Decoder/facade/resource tests, zero-light GPU readback, override
restoration, masked-shadow invariance and native/browser imported GLB controls
exercise this addition. Absent overrides preserve legacy/imported looks. Material
texture-bearing assets, unified alpha, emissive maps and Explorer game adoption remain
open; see the material contract (in `09-3d.md`).

</details>

<a id="textured-geometry-normals"></a>

#### Authored textured geometry normals

Cubes have outward face normals; inline surfaces accept validated optional XYZ normals and cached render vertices carry them

Engine 🟡 · Editor ❌ · Decay ❌ · Proof 🟡 — parity row *Authored textured geometry normals*

**Tested by:** `crates/sindri-gpu/tests/standard_materials/normals.rs` (`crates/sindri-gpu/tests/standard_materials/normals.rs` in the Sindri repository)

**See:** `docs/rendering-materials.md` (in `09-3d.md`)

<details markdown="1">
<summary>Evidence</summary>

Cubes carry outward face normals, while inline mesh documents and cached
render vertices can supply object-space normals for smooth lighting. Checked
components reject malformed vectors/counts; absent normals retain flat shading.
GPU comparisons with imported geometry exercise nonuniform scale, rotation and
reflection; world color/shadow pipelines correct reflected front-face selection.
Scene edit/history tests and native/browser smooth/flat ellipsoid controls prove
the feature. Textured vertices grow from 24 to 36 bytes; per-draw uniforms and
cache residency are unchanged. Editor authoring review, Decay access and Explorer
adoption remain open. See normal semantics (in `09-3d.md`).

</details>

<a id="shared-lighting-integration"></a>

#### Lighting

One authored ambient contribution plus one directional world light shade textured 3D geometry and voxel terrain in editor viewports and Voxel Lab, with one bounded directional shadow map

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *Lighting*

**Tested by:** `game/tests/a_3d_lighting_project_plays.rs` (`game/tests/a_3d_lighting_project_plays.rs` in the Sindri repository)

**Exercised in:** `examples/lighting3d` (`examples/lighting3d` in the Sindri repository)

**See:** `docs/3d-lights.md` (in `09-3d.md`)

<details markdown="1">
<summary>Evidence</summary>

`this.light` and `Entity.light` expose color, intensity, range, spotlight
half-angles and independent boolean enabled state from the shared typed surface.
Setters validate full candidates before mutation, including f32 narrowing and
cross-field cone bounds; failed writes retain payloads and whole colors are
atomic. Reads honor omitted defaults; Color alpha stays one. The enabled flag
changes illumination without deactivating the entity or stopping its scripts.
`examples/lighting3d` is feature proof for Explorer's planned lighting track:
its checked script animates parented points/spots and the sun, reaches lit/dark
Session goals and exercises cube, inline and imported geometry. Completion,
hover, generated docs and typed batch checks share the same metadata. Native,
real WebGPU and editor Play controls are recorded in `3d-lights.md`. In-tree
game adoption and local shadows remain
open; no new uniform or pass cost is introduced.

</details>

<a id="local-lights-3d"></a>

#### 3D point and spot lights

Strongest-eight active points/spots, world-unit range/falloff, feathered half-angle cones and composed parent positions/directions share Lambert/GGX across meshes, voxels and imported models

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *3D point and spot lights*

**Tested by:** `crates/sindri-gpu/tests/standard_materials/points.rs` (`crates/sindri-gpu/tests/standard_materials/points.rs` in the Sindri repository), `crates/sindri-gpu/tests/standard_materials/spots.rs` (`crates/sindri-gpu/tests/standard_materials/spots.rs` in the Sindri repository)

**Exercised in:** `examples/lighting3d` (`examples/lighting3d` in the Sindri repository)

**See:** `docs/3d-lights.md` (in `09-3d.md`)

<details markdown="1">
<summary>Evidence</summary>

`sindri.light` now supports point lights with world-unit range. The strongest
eight active points shade primitive, inline/cached/voxel and imported geometry
through shared Lambert/GGX point lighting. Positions follow composed live
transforms and inactive ancestors suppress the light. Editor creation uses one
checked transaction and selected world-range circles; studio lighting omits
authored points. Native/browser feature controls and GPU/scene regressions are
described in the 3D light contract (in `09-3d.md`). This is Explorer feature
proof, with no in-tree game adoption yet; local shadows are absent.
Typed runtime light properties are described below. Points now share the
spotlight records
described below. The fixed uniform cost is 512 additional
bytes per color draw; shared buffers and spatial light assignment remain open.

Explorer's planned lighting track now has unshadowed spotlights as feature proof;
no in-tree game uses them yet. `sindri.light.kind = "spot"` uses composed world
position/forward with validated inner/outer half-angles in radians. Points and
spots share eight strongest-intensity slots, range falloff and Lambert/GGX
shading across world/voxel meshes and imported models. Cone strength feathers
with smoothstep between angle cosines. Color uniforms are now 880/944 bytes for
textured/model draws (eight 64-byte light records, plus the model silhouette
color); shadow uniforms, passes, writes
and persistent geometry uploads are unchanged. The editor creates down-facing
spots in one undo step and paints clipped selected inner/outer range cones.
Native/browser aimed/away controls and Scene/GPU/guide regressions provide
feature evidence. Local shadows and Explorer adoption remain open; runtime light
controls and mounted-light feature evidence are described below and in `3d-lights.md`.

</details>

<a id="runtime-environment"></a>

#### Runtime ambient and background controls

Typed entity environment access shares authored defaults and validates full candidates atomically

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *Runtime ambient and background controls*

**Tested by:** `crates/sindri-decay/tests/environment.rs` (`crates/sindri-decay/tests/environment.rs` in the Sindri repository)

**Exercised in:** `examples/lighting3d` (`examples/lighting3d` in the Sindri repository)

**See:** `docs/runtime-environment.md` (in `05-projects-scenes-prefabs.md`)

<details markdown="1">
<summary>Evidence</summary>

Typed `this.environment` / `entity.environment` address an existing authored
environment. Ambient intensity, RGB Color and RGBA background reads honor
defaults; writes validate the full payload before mutation and preserve unrelated
settings. Whole Color writes are atomic, fractional numbers stay fractional,
ambient alpha stays one, and stale/missing targets are runtime errors.
Lighting Lab extends its first two phases with cool/warm ambient and clear-color
changes, restoring original settings at the ready goal. Shared Session tests
observe the resulting lighting and frame clear; native/browser captures and
editor Play exercise rendering. This is feature proof for Explorer/Low Tide 3D's
planned lighting track, with in-tree game adoption and fog/post controls open.
Day/Night Lab below exercises the project clock and mounted-light integration. No renderer layout, resource lifetime or dependency changes.
See `runtime-environment.md` for the contract and validation evidence.

</details>

<a id="daynight-cycle"></a>

#### Scripted day/night cycle

Day/Night Lab opens, compiles and plays two Decay clock configurations through sunrise/midday/sunset/night and a continuing goal

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *Scripted day/night cycle*

**Tested by:** `game/tests/a_daynight_project_plays.rs` (`game/tests/a_daynight_project_plays.rs` in the Sindri repository)

**Exercised in:** `examples/daynight` (`examples/daynight` in the Sindri repository)

**See:** `docs/daynight.md` (in `09-3d.md`)

<details markdown="1">
<summary>Evidence</summary>

`examples/daynight` is feature proof for Explorer and external Low Tide 3D's
planned lighting track. Its Decay-owned clock smoothly changes sun brightness,
color and rotation, ambient light and background through four phases. A moving
GLB carries a point and spotlight; night illumination works while the sun is
disabled. Real-project Session tests exercise two clock configurations, all
phases, composed night positions, continued motion after the goal, frozen hours
and independent night controls. Native/browser images isolate moving sun
shadows on imported ground with fixed model poses, and local illumination at
midnight. Native editor Play runs the same scene. See `daynight.md` for measured
evidence. No new host API, renderer path, dependency or engine time service;
in-tree game adoption and fog/post controls remain open.

</details>

<a id="retained-inline-meshes"></a>

#### Retained inline mesh geometry

Immutable u32 resources share shadow/color buffers and survive independent draw-state changes

Engine 🟡 · Editor 🟡 · Decay — · Proof 🟡 — parity row *Retained inline mesh geometry*

**Tested by:** `crates/sindri-core/src/component/decoded.rs` (`crates/sindri-core/src/component/decoded.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/retained_meshes.rs` (`crates/sindri-scene/tests/extraction/retained_meshes.rs` in the Sindri repository), `crates/sindri-gpu/tests/standard_materials/retained.rs` (`crates/sindri-gpu/tests/standard_materials/retained.rs` in the Sindri repository), `crates/sindri-gpu/tests/standard_materials/normals.rs` (`crates/sindri-gpu/tests/standard_materials/normals.rs` in the Sindri repository), `game/tests/a_3d_benchmark_project_plays.rs` (`game/tests/a_3d_benchmark_project_plays.rs` in the Sindri repository)

**Exercised in:** `examples/benchmark3d` (`examples/benchmark3d` in the Sindri repository), `examples/lighting3d` (`examples/lighting3d` in the Sindri repository)

**See:** `docs/retained-meshes.md` (`docs/retained-meshes.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Scene extraction reuses immutable geometry until its positions, UVs, normals,
indices or atlas rectangle change. Per-renderer weak caches upload once across
shadow/color draws and retire unused resources; older frames own their revision.
Checked edits/undo, lifecycle, real u32 drawing and shared shading have regressions.
Native/editor benchmark reports record zero settled geometry uploads with
unchanged draws/passes. Release browser exports exercise real u32 geometry,
shadows, smooth normals and live lighting. This is feature proof for the planned
Explorer/Low Tide 3D track; in-tree game adoption,
instancing, deduplication and culling remain open.

</details>

<a id="material-sidedness"></a>

#### Authored material sidedness

Inherit, single and double face visibility share color/shadow policy across world geometry

Engine 🟡 · Editor 🟡 · Decay — · Proof 🟡 — parity row *Authored material sidedness*

**Tested by:** `crates/sindri-gpu/tests/standard_materials/sidedness.rs` (`crates/sindri-gpu/tests/standard_materials/sidedness.rs` in the Sindri repository), `crates/sindri-gpu/tests/imported_models/sidedness.rs` (`crates/sindri-gpu/tests/imported_models/sidedness.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/material.rs` (`crates/sindri-scene/tests/extraction/material.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/voxel_world.rs` (`crates/sindri-scene/tests/extraction/voxel_world.rs` in the Sindri repository), `game/tests/a_3d_lighting_project_plays.rs` (`game/tests/a_3d_lighting_project_plays.rs` in the Sindri repository)

**Exercised in:** `examples/lighting3d` (`examples/lighting3d` in the Sindri repository)

**See:** `docs/rendering-materials.md` (in `09-3d.md`)

<details markdown="1">
<summary>Evidence</summary>

The material component carries a checked sidedness choice independently of numeric
factors. Inherit preserves imported primitive settings; other geometry is single-sided.
Explicit single/double choices govern both color and shadows, including reflected
transforms and lighting on visible backs. Policy edits retain geometry and voxel meshes.
Lighting Lab carries a back-facing orange panel and a single-sided native/WebGPU
control. Shared texture bindings, alpha unification and Explorer game adoption remain open.

</details>

<a id="render-material-profiles"></a>

#### Shared render material profiles

Typed profile assets share numeric material factors and face visibility with explicit local overrides

Engine 🟡 · Editor 🟡 · Decay — · Proof 🟡 — parity row *Shared render material profiles*

**Tested by:** `crates/sindri-scene/src/render_material/tests.rs` (`crates/sindri-scene/src/render_material/tests.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/material_profiles.rs` (`crates/sindri-scene/tests/extraction/material_profiles.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/voxel_world.rs` (`crates/sindri-scene/tests/extraction/voxel_world.rs` in the Sindri repository), `crates/sindri-export/tests/render_materials.rs` (`crates/sindri-export/tests/render_materials.rs` in the Sindri repository), `editor/src/scripts/tests.rs` (`editor/src/scripts/tests.rs` in the Sindri repository), `editor/src/profile.rs` (`editor/src/profile.rs` in the Sindri repository), `game/tests/a_3d_lighting_project_plays.rs` (`game/tests/a_3d_lighting_project_plays.rs` in the Sindri repository)

**Exercised in:** `examples/lighting3d` (`examples/lighting3d` in the Sindri repository)

**See:** `docs/rendering-materials.md` (in `09-3d.md`), `docs/profiles.md` (in `05-projects-scenes-prefabs.md`)

<details markdown="1">
<summary>Evidence</summary>

Engine-owned render_material profiles carry checked numeric factors and sidedness.
References use the existing asynchronous profile pipeline and export dependency
walk, including inactive entities, later scenes and expanded prefabs. Explicit
local overrides are independent of profile values. Invalid editor reloads retain
last-valid values; extraction snapshots values without replacing geometry.
Lighting Lab shares three profiles across six objects and carries inline-equivalence
and shared-edit controls, exercised in native and real Chromium/WebGPU captures.
Native editor review covers creation, selection/history, validated Save/correction,
reload, reopen and Play. Material texture ownership, unified alpha and in-tree
game adoption remain open.

</details>

### 3D rendering

<a id="model-occlusion-silhouettes"></a>

#### Imported-model occlusion silhouettes

Optional muted model silhouettes keep Causeway units visible behind elevated terrain.

Engine ✅ · Editor ❌ · Decay — · Proof ✅ — parity row *Imported-model occlusion silhouettes*

**Tested by:** `crates/sindri-scene/tests/extraction/models.rs` (`crates/sindri-scene/tests/extraction/models.rs` in the Sindri repository), `crates/sindri-gpu/tests/imported_models/occlusion.rs` (`crates/sindri-gpu/tests/imported_models/occlusion.rs` in the Sindri repository)

**Exercised in:** `game` (`game` in the Sindri repository)

**See:** `docs/model-occlusion.md` (`docs/model-occlusion.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`sindri.model.occluded_color` opts into a depth-tested flat RGBA pass. Opaque
non-subject geometry fills depth first, then silhouettes, then normally lit
subjects. Shared geometry, material facing and cutouts are preserved. No
subject self-occlusion; opted-in subjects do not reveal each other. Filled
silhouettes may accumulate alpha where primitives overlap; transparency can
cover them. JSON authoring only; editor color interaction is not proven.

</details>

---

Source: `docs/capabilities/ui.md`

## Screen UI and Weave

The screen overlay — images, text, buttons and widgets laid out against the
viewport — and Weave, the CSS-shaped stylesheet language that presents it.
`docs/weave.md` and `docs/weave-reference.md` record the supported Weave
surface; `docs/ui-direction.md` is the plan beyond it.

Each entry links its tests and the projects that exercise it. Surface marks match the row in parity (`docs/parity.md` in the Sindri repository) where one is named: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — not applicable.

| Capability | Engine | Editor | Decay | Proof |
| --- | :-: | :-: | :-: | :-: |
| **Screen UI** | | | | |
| Anchored image and text, fill bars, buttons, row/column, safe area | ✅ | ✅ | ✅ | ✅ |
| Pointer hit-testing, hover/press/held | ✅ | ✅ | ✅ | ✅ |
| Slider | ✅ | ✅ | ✅ | ✅ |
| Toggles, text fields, scroll regions, radio groups and dropdowns | ✅ | 🟡 | ✅ | ✅ |
| **Weave** | | | | |
| Weave responsive stylesheets | 🟡 | 🟡 | — | ✅ |
| Flexbox, grid, box model and content-sized text | ✅ | ✅ | — | ✅ |
| UI devtools (computed style, which rule won) | — | ✅ | — | — |
| **Not yet** | | | | |
| Accessibility labels | ❌ | ❌ | ❌ | ❌ |

### Screen UI

<a id="screen-ui-basics"></a>

#### Anchored image and text, fill bars, buttons, row/column, safe area

Viewport-space images, templated text, fill bars and pressable buttons, in normalized units with a safe area.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Anchored image and text, fill bars, buttons, row/column, safe area*

**Tested by:** `crates/sindri-scene/tests/screen_ui.rs` (`crates/sindri-scene/tests/screen_ui.rs` in the Sindri repository), `crates/sindri-scene/src/screen_ui/hierarchy/tests.rs` (`crates/sindri-scene/src/screen_ui/hierarchy/tests.rs` in the Sindri repository), `games/orbital-baked/tests/the_ui_fits_every_screen.rs` (`games/orbital-baked/tests/the_ui_fits_every_screen.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`sindri.ui.image` and `sindri.ui.text` draw against the viewport. Text is a
**template**: the words carry `{}` slots and a script supplies the numbers, so
a HUD's words stay in the scene, and a script that does need to build text
joins it with `+`. An image carries a **fill** fraction and the edge it empties
from, which is what makes a bar a bar rather than a picture of one.

`sindri.ui.layout` places a parent's active children in a row or column. Three
buttons could be authored as three offsets; what cannot be authored is a menu
closing up around an entry that was switched off. `sindri.ui.box` gives any
element CSS's padding and margin per side: a layout's children flow inside its
padding and keep their own margins free, without collapsing, as in flexbox. A
`sindri.ui.shape` can cast a soft `shadow` (colour, offset, blur, spread).

The overlay is authored in normalized units — two tall, centred, running out to
the aspect ratio — so one authored scene is responsive across a portrait phone
and a wide desktop window without a breakpoint. A **safe area** takes a notch
or a home indicator off the edges, moving anchored elements in while leaving
centred ones where they are; the editor reports none, because a desktop window
has no notch. Decay reads that same screen shape through `Viewport.aspect`,
which lets a game combine the viewport with its authored camera framing for
responsive world-space rules without exposing pixel dimensions or asking the
host to pick a gameplay camera.

</details>

<a id="ui-hit-testing"></a>

#### Pointer hit-testing, hover/press/held

`ScreenUi` hit-tests the pointer by layer and answers hover, click and hold; a disabled subtree is a hidden screen.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Pointer hit-testing, hover/press/held*

**Tested by:** `crates/sindri-scene/tests/screen_ui.rs` (`crates/sindri-scene/tests/screen_ui.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_reads_a_button.rs` (`crates/sindri-decay/tests/a_script_reads_a_button.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`sindri.ui.button` makes an element pressable, its rect being the entity's own
transform. `ScreenUi` is the runtime beside the world: it lays every element
out, hit-tests the pointer, and answers hover, click and hold. A click is a
press and a release on the same element, so sliding off before letting go
changes a person's mind. Overlapping elements are resolved by layer, so a modal
is a modal because it is on top. A disabled entity — or one under a disabled
parent — is not hit-tested, which is also what a *screen* is: a menu is an
entity with children, and showing it is switching it on. No screen stack was
added, because the engine already had the mechanism.

Nothing is silently withheld from gameplay while a menu is up: which scripts
are gameplay is not something a host can know. `Pointer.over_ui` is the one
line a gameplay script writes instead.

</details>

<a id="ui-slider"></a>

#### Slider

A dragged value whose pointer and script writes pass through one clamp-and-quantize check.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Slider*

**Tested by:** `crates/sindri-scene/tests/slider_interaction.rs` (`crates/sindri-scene/tests/slider_interaction.rs` in the Sindri repository), `crates/sindri-scene/tests/sliders_by_keyboard.rs` (`crates/sindri-scene/tests/sliders_by_keyboard.rs` in the Sindri repository), `crates/sindri-weave/tests/slider_presentation.rs` (`crates/sindri-weave/tests/slider_presentation.rs` in the Sindri repository), `games/orbital-baked/tests/the_mixer.rs` (`games/orbital-baked/tests/the_mixer.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository), `games/weave-poc` (`games/weave-poc` in the Sindri repository)

**Limits:**

- `value` is a plain number box in the inspector; no field carries a bounded-range meaning tying it to `min` and `max`.

<details markdown="1">
<summary>Evidence</summary>

`sindri.ui.slider` is the first element a person drags rather than presses. It
carries `min`, `max`, `step`, a `value`, an orientation and a `disabled` flag,
and its point is that the two ways of *setting* it agree: a pointer drag and
`Ui.set_slider_value` from a script both pass through one `coerce_value`, which
clamps to the range and quantizes to the step. A value therefore cannot enter
the component off-step from either direction. An authored value is not coerced
on load — a scene may hold anything — so the read side stays defensive
instead: a degenerate range, or a value that is not finite, answers `min`
rather than letting a NaN spread into the layout. The drag tracks the pointer
along the element's own rect, so an element moved or resized by Weave keeps its
slider correct without the slider being told. Decay reads `Ui.slider_value`,
writes `Ui.set_slider_value`, and asks `Ui.slider_changed` whether the person
moved it during the step. The editor offers it as **UI Slider** in Add
Component with `orientation` drawn as a declared choice. Orbital's pause screen
uses three for its mixer.

</details>

<a id="ui-widgets"></a>

#### Toggles, text fields, scroll regions, radio groups and dropdowns

The remaining form widgets, each with one check shared by a person's change and a script's write, and keyboard/pad focus.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅

**Tested by:** `crates/sindri-scene/tests/widgets.rs` (`crates/sindri-scene/tests/widgets.rs` in the Sindri repository), `crates/sindri-scene/tests/choices_and_editing.rs` (`crates/sindri-scene/tests/choices_and_editing.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_reads_widgets.rs` (`crates/sindri-decay/tests/a_script_reads_widgets.rs` in the Sindri repository), `game/tests/the_ui_demo_works.rs` (`game/tests/the_ui_demo_works.rs` in the Sindri repository), `scripts/browser/ui-demo.mjs` (`scripts/browser/ui-demo.mjs` in the Sindri repository), `games/orbital-baked/tests/boss_rush.rs` (`games/orbital-baked/tests/boss_rush.rs` in the Sindri repository)

**Exercised in:** `examples/ui` (`examples/ui` in the Sindri repository), `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`sindri.ui.toggle` is a boolean that a click, tap, Space or Enter flips;
whether it looks like a switch or a checkbox is styling, through Weave's
`:checked`. `sindri.ui.text_input` is a single-line field with a placeholder
and a limit counted in characters: it takes committed Unicode text, Backspace,
Enter to submit and Escape to let go, and while it has the keyboard its keys
are held back from `Input`, so typing does not steer. `sindri.ui.scroll`
scrolls its children vertically by wheel, drag or finger, clamped to how tall
they are as laid out, and clips their drawing and hit-testing to itself; a drag
that starts on a row never becomes a click on it. Tab and Shift+Tab walk every
focusable element in the order the scene is written. Decay reads and writes
them with `Ui.is_checked`, `Ui.input_text`, `Ui.scroll_offset` and their
setters, and `Ui.changed`, `Ui.submitted` and `Ui.is_focused` answer for the
step. Toggles that share a `group` are a radio group, and `sindri.ui.dropdown`
opens a popup of authored `sindri.ui.option` rows that the engine shows only
while it is open (Weave's `:open`), read with `Ui.selected`. A field edits at a
caret with a selection and the system clipboard, and a script draws the caret
from `Ui.caret` and the `Ui.selection_*` pair. In a browser a field being
edited gives the page's focus to a hidden textarea, which is how IME
composition, a paste and a phone's keyboard reach it. The arrows and any pad's
d-pad move focus toward the nearest control, preferring those in line, and a
row reached inside a scroll region is scrolled into view. `examples/ui` is the
feature example; Orbital Last Stand proves them in a game with a saved
compact-HUD switch, a pilot callsign carried onto the HUD and results, a
scrolling dropdown of its twelve bosses, and a scrolling field manual on its
pause screen.

</details>

### Weave

<a id="weave-stylesheets"></a>

#### Weave responsive stylesheets

CSS selectors, cascade, custom properties, media queries and transitions, applied by every host through one presenter.

Engine 🟡 · Editor 🟡 · Decay — · Proof ✅ — parity row *Weave responsive stylesheets*

**Tested by:** `crates/sindri-weave/src/tests.rs` (`crates/sindri-weave/src/tests.rs` in the Sindri repository), `crates/sindri-weave/tests/web_style.rs` (`crates/sindri-weave/tests/web_style.rs` in the Sindri repository), `crates/sindri-weave/tests/structural.rs` (`crates/sindri-weave/tests/structural.rs` in the Sindri repository), `crates/sindri-export/tests/weave_assets.rs` (`crates/sindri-export/tests/weave_assets.rs` in the Sindri repository), `weave/crates/weave/tests/smoke.rs` (`weave/crates/weave/tests/smoke.rs` in the Sindri repository)

**Exercised in:** `games/weave-poc` (`games/weave-poc` in the Sindri repository), `games/orbital-baked` (`games/orbital-baked` in the Sindri repository)

**See:** `docs/weave.md` (in `08-weave-ui.md`), `docs/weave-reference.md` (in `08-weave-ui.md`)

**Limits:**

- Not a general CSS implementation: `@keyframes`, borders per side, grid's `minmax()` and named areas, and accessibility mapping are absent.

<details markdown="1">
<summary>Evidence</summary>

Weave resolves responsive presentation into a disposable clone of an authored
world. Its selectors are CSS's: element names (`text`, `button`, with the long
`sindri.ui.text` still accepted), IDs, classes, `*`, compounds such as
`button.primary:hover`, descendant and child combinators, and lists, with CSS
specificity and source order deciding conflicts. Selectors include the
structural pseudo-classes (`:first-child`, `:nth-child(2n+1)`, `:root` and the
rest), `:not()`, `:is()`, `:where()`, and the `+` and `~` sibling combinators,
matched in scene order; lengths take `calc()`. The cascade is CSS's too: text
properties inherit from the containing entity, `inherit`, `initial` and `unset`
do what they do in CSS, and custom properties (`--accent`) inherit and
substitute through `var()` with fallbacks, so a theme's tokens live in one
place. Media queries test orientation and minimum or maximum width and height,
combined with `and`, commas and nesting. `:disabled` and `:checked` follow the
entity's own component data everywhere.

Every running host (the editor's Play, native games, browser exports) styles
through a `sindri_weave::Presenter`: a game's world is settled with the
stylesheet's rules at start and on resize, and each frame only what the
pointer's states and running transitions change is laid over it, in place, for
the draw, and taken off again. So `:hover` (which, as in CSS, also matches the
hovered element's containers) and `:active` follow the pointer, a script's own
writes are not styled back, and hit-testing uses what was drawn. CSS
`transition` eases colours, lengths and numbers between states with named or
`cubic-bezier` easings, delays and `all`.

Weave is kept in its own workspace, with its own CI workflow, while
`sindri-weave` is the one-way bridge that knows the engine. Stylesheets compose
through `@use`; the exporter follows that graph, and the browser host applies
independent roots in deterministic asset order. `games/weave-poc` is the
focused responsive showcase, while Orbital Last Stand uses five composed Weave
files — a shared theme, then its HUD, overlays and screens — for its
production-oriented screen UI.

</details>

<a id="weave-layout"></a>

#### Flexbox, grid, box model and content-sized text

CSS flexbox and grid layout in the engine, margin/padding/box-shadow, and text that sizes what holds it.

Engine ✅ · Editor ✅ · Decay — · Proof ✅

**Tested by:** `crates/sindri-weave/tests/flex.rs` (`crates/sindri-weave/tests/flex.rs` in the Sindri repository), `crates/sindri-weave/tests/grid.rs` (`crates/sindri-weave/tests/grid.rs` in the Sindri repository), `crates/sindri-weave/tests/padding.rs` (`crates/sindri-weave/tests/padding.rs` in the Sindri repository), `crates/sindri-scene/src/screen_ui/flex/tests.rs` (`crates/sindri-scene/src/screen_ui/flex/tests.rs` in the Sindri repository), `crates/sindri-scene/src/screen_ui/grid/tests.rs` (`crates/sindri-scene/src/screen_ui/grid/tests.rs` in the Sindri repository), `crates/sindri-scene/tests/text_sizing.rs` (`crates/sindri-scene/tests/text_sizing.rs` in the Sindri repository), `crates/sindri-scene/tests/screen_ui.rs` (`crates/sindri-scene/tests/screen_ui.rs` in the Sindri repository)

**Exercised in:** `games/orbital-baked` (`games/orbital-baked` in the Sindri repository), `games/weave-poc` (`games/weave-poc` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

Weave's `margin` and `padding` (one to four values, per-side longhands, CSS
percentages) write that box, and `box-shadow` writes the shape's shadow.
Percentage sizing, min/max constraints, gaps, wrapping, alignment, and text
wrapping cover the responsive composition used by the shipped examples.

Layouts are CSS flexbox, in the engine: `sindri.ui.layout` wraps and fits its
content, and each child's `sindri.ui.box` grows, shrinks, takes a basis, orders
and aligns itself within min/max limits. Layout decides sizes as well as
places, and drawing, hit-testing and the editor's picking use them. Weave's
`flex`, its longhands, `flex-wrap`, `order`, `align-self`, `width: auto` and
the rest write the same data. `position: absolute` takes an element out of
its parent's line or grid, which closes up as if it were not there, and places
it by its own `x` and `y` from the parent's middle; Causeway lays its island
banner's cloud under the words that way.

Text sizes what holds it: a text element whose `sindri.ui.box` fits its content
(Weave's `width: auto`) is measured by its font each draw, so a label sizes a
button, and a label is the least a shrinking flex item keeps. Hosts measure
through `sindri_scene::measure_ui_text` and pass the sizes to extraction and
hit-testing.

`sindri.ui.grid` lays children out in CSS grid's columns and rows: fixed,
`auto` and `fr` tracks, explicit or automatic placement with spans, implicit
tracks, gaps and cell alignment. Weave's `display: grid` and the
`grid-template-*`, `grid-column`/`grid-row`, gap and alignment properties
write it.

</details>

<a id="weave-devtools"></a>

#### UI devtools (computed style, which rule won)

The inspector's Styles section shows the box model, every matched rule by `file:line`, and edits values back into the stylesheet.

Engine — · Editor ✅ · Decay — · Proof — — parity row *UI devtools (computed style, which rule won)*

**Tested by:** `crates/sindri-weave/tests/inspect.rs` (`crates/sindri-weave/tests/inspect.rs` in the Sindri repository), `crates/sindri-weave/tests/undo.rs` (`crates/sindri-weave/tests/undo.rs` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

The editor treats manifest-listed `.weave` roots as project assets, previews
their source, authors reusable classes in a dedicated inspector section, and
applies the composed presentation to both Scene and Game views. It watches the
manifest and complete `@use` graph for saved changes, keeps the last good
presentation after a broken save, and reports composition failures with their
source path, line, and column. The inspector's Styles section is Weave's
devtools: the box model as drawn, every matched rule with its `file:line` and
the overridden declarations struck through, and the computed values, at the
Game view's size. Clicking a value there edits it in place and writes it into
the stylesheet at the rule's line, which then reloads; the Game view's Pick
toggle selects an element by clicking it in the running game.

</details>

### Not yet

<a id="accessibility-labels"></a>

#### Accessibility labels

A button carries a `label`, but the web export renders through canvas/WebGPU with no semantic DOM bridge to expose it.

Engine ❌ · Editor ❌ · Decay ❌ · Proof ❌ — parity row *Accessibility labels*

---

Source: `docs/capabilities/grids-and-voxels.md`

## Grids and voxels

Renderer-independent grid geometry and navigation, and the engine-owned voxel
world: storage, generation, residency, meshing, collision, and the map view.
`docs/grid.md`, `docs/voxel-terrain.md` and `docs/voxel-world-plan.md` are the
contracts.

Each entry links its tests and the projects that exercise it. Surface marks match the row in parity (`docs/parity.md` in the Sindri repository) where one is named: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — not applicable.

| Capability | Engine | Editor | Decay | Proof |
| --- | :-: | :-: | :-: | :-: |
| **Grid geometry** | | | | |
| Orthogonal and isometric grids | ✅ | ✅ | ✅ | ✅ |
| Footprints, occupancy, walls | ✅ | ✅ | ✅ | ✅ |
| **Voxel world** | | | | |
| Engine-owned voxel coordinates and 16³ sections | 🟡 | 🟡 | 🟡 | 🟡 |
| Bounded voxel residency | 🟡 | 🟡 | ❌ | 🟡 |
| Neighbour-aware block meshing | 🟡 | 🟡 | ❌ | 🟡 |
| Persistent voxel compiled/GPU mesh cache | 🟡 | 🟡 | ❌ | 🟡 |
| Voxel collision | ✅ | ✅ | ✅ | 🟡 |
| Voxel ground for gameplay | ✅ | — | ✅ | ✅ |
| Voxel world viewed as a map | ✅ | 🟡 | ✅ | ✅ |
| Reversible voxel map flooding | ✅ | 🟡 | ✅ | ✅ |
| **Not yet** | | | | |
| Voxel persistence and digging | ❌ | ❌ | ❌ | ❌ |
| Reusable voxel asset authoring | ❌ | ❌ | ❌ | ❌ |
| Per-path costs and policies | ❌ | ❌ | ❌ | — |

### Grid geometry

<a id="grid-geometry"></a>

#### Orthogonal and isometric grids

Signed cell coordinates, bounds, neighbour queries and validated orthogonal/isometric projection, shared by drawing and picking.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Orthogonal and isometric grids*

**Tested by:** `crates/sindri-grid/src/geometry/tests.rs` (`crates/sindri-grid/src/geometry/tests.rs` in the Sindri repository), `crates/sindri-decay/tests/grid_position.rs` (`crates/sindri-decay/tests/grid_position.rs` in the Sindri repository)

**Exercised in:** `games/platformer` (`games/platformer` in the Sindri repository), `games/low-tide` (`games/low-tide` in the Sindri repository)

**See:** `docs/grid.md` (in `06-2d.md`)

<details markdown="1">
<summary>Evidence</summary>

`sindri-grid` provides renderer-independent signed cell coordinates,
continuous grid and plane points, finite rectangular bounds, deterministic
cardinal and eight-way neighbour queries, and validated orthogonal/isometric
projection. Cell centres and arbitrary fractional points round-trip through
both projections across negative and positive space. Upward/downward plane Y
adapts world and screen coordinate conventions without introducing a renderer
dependency.

`sindri.tilemap` exposes the exact `GridSpace` and `GridBounds` its rendering
and editor picking use. A map's complete transform is composed around that
local grid, so moving, rotating, or scaling a map keeps drawn and picked cells
in agreement. Decay can read an entity's continuous logical X/Y relative to a
tilemap and place it back through the same projection and map transform.

</details>

<a id="grid-navigation"></a>

#### Footprints, occupancy, walls

Authored wall edges and multi-cell occupants resolved into one validated navigation snapshot.

Engine ✅ · Editor ✅ · Decay ✅ · Proof ✅ — parity row *Footprints, occupancy, walls*

**Tested by:** `crates/sindri-scene/tests/navigation/authored.rs` (`crates/sindri-scene/tests/navigation/authored.rs` in the Sindri repository), `crates/sindri-scene/tests/navigation/volume.rs` (`crates/sindri-scene/tests/navigation/volume.rs` in the Sindri repository), `crates/sindri-grid/src/pathfinding/tests.rs` (`crates/sindri-grid/src/pathfinding/tests.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_asks_what_is_walkable.rs` (`crates/sindri-decay/tests/a_script_asks_what_is_walkable.rs` in the Sindri repository)

**Exercised in:** `games/low-tide` (`games/low-tide` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`sindri.grid.navigation` adds authored internal wall edges without duplicating
the map's bounds or projection settings, while `sindri.grid.occupant` names its
grid by stable scene ID and carries a relative multi-cell footprint.
`WorldGridNavigation` resolves those references, derives anchors from current
world transforms, rejects conflicting or invalid placements as one incomplete
snapshot, and exposes validated placement and wall-aware path queries.

</details>

### Voxel world

<a id="voxel-storage"></a>

#### Engine-owned voxel coordinates and 16³ sections

Signed voxel and section coordinates, palette-backed sections, deterministic generation, sparse edits and work queues.

Engine 🟡 · Editor 🟡 · Decay 🟡 · Proof 🟡 — parity row *Engine-owned voxel coordinates and 16³ sections*

**Tested by:** `crates/sindri-scene/src/voxel/tests.rs` (`crates/sindri-scene/src/voxel/tests.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/voxel_world.rs` (`crates/sindri-scene/tests/extraction/voxel_world.rs` in the Sindri repository), `crates/sindri-voxel/src/terrain/tests.rs` (`crates/sindri-voxel/src/terrain/tests.rs` in the Sindri repository)

**Exercised in:** `games/voxel-lab` (`games/voxel-lab` in the Sindri repository)

**See:** `docs/voxel-terrain.md` (`docs/voxel-terrain.md` in the Sindri repository), `docs/voxel-world-plan.md` (`docs/voxel-world-plan.md` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`sindri-voxel` owns signed voxel and 16³ section coordinates, palette-backed
section storage, deterministic random-access generation, bounded 3D residency,
sparse edits, dirty tracking, and deduplicated generation/meshing work queues.
Mesh jobs carry a monotonic revision and meshing profile, and boundary changes
give the neighbour a new mesh revision even when its own stored voxels did not
change.

Scenes author this as `sindri.voxel_world`, with either a deterministic layered
generator or a natural terrain generator (continents, ranges, rivers, biomes,
caves, overhangs, water, snow and trees from one seed), per-face voxel material
textures, bounded horizontal/vertical residency, and a section focus. The same
scene path renders in games and in the editor Scene view. A world may instead
name a block set in `blocks` (the engine ships `builtin:blocks`, and new worlds
use it), and then its generator names blocks by name, drawn with each block's
per-face art; the generator's fields are `FieldMeaning::Block`, which the
editor offers as a menu of the set's blocks drawn as cubes. Without a block
set, material IDs are declared keys (`FieldMeaning::Key`) and the generator's
surface, subsurface, and deep layers reference them (`FieldMeaning::Block`
falling back to `KeyOf`, including the natural terrain's biome and optional
palette voxels, which also offer *None*), so the editor numbers an added
material past every ID in use, refuses to remove one a layer still names, and
offers the layers only the IDs that exist.

The editor's extractor tolerates an invalid voxel world or environment
(`SceneExtractor::tolerate_invalid_components`): the last valid terrain and
lighting keep drawing, and the failure is reported against its entity, naming
the environment field and the values it accepts. Games and capture tools stay
strict and fail the frame.

A world's `edits` list what has been changed from what was generated, cell by
cell; only changes are stored, an edit back to the generated block is dropped,
and an edit remeshes only the sections it touches.

</details>

<a id="voxel-residency"></a>

#### Bounded voxel residency

A bounded resident window, optionally following the world camera, loads the sections it reaches and releases those it leaves.

Engine 🟡 · Editor 🟡 · Decay ❌ · Proof 🟡 — parity row *Bounded voxel residency*

**Tested by:** `crates/sindri-scene/src/voxel/tests.rs` (`crates/sindri-scene/src/voxel/tests.rs` in the Sindri repository)

**Exercised in:** `games/voxel-lab` (`games/voxel-lab` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

A world may keep its resident window under the world camera (`follow_camera`):
the window centres on where the middle of the picture meets the height of the
`focus` section, and moving it loads the sections it reaches and releases those
it leaves. Cached section bounds are tested conservatively against the current
camera after the voxel world's entity transform, so offscreen resident sections
retain their CPU cache without submitting draws or consuming their pending GPU
upload.

</details>

<a id="voxel-meshing"></a>

#### Neighbour-aware block meshing

Blocks mesh as the boxes their tiles describe, dropping only fully covered faces, with a one-voxel halo across sections.

Engine 🟡 · Editor 🟡 · Decay ❌ · Proof 🟡 — parity row *Neighbour-aware block meshing*

**Tested by:** `crates/sindri-voxel/src/mesh_tests.rs` (`crates/sindri-voxel/src/mesh_tests.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/voxel_blocks.rs` (`crates/sindri-scene/tests/extraction/voxel_blocks.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/voxel_surfaces.rs` (`crates/sindri-scene/tests/extraction/voxel_surfaces.rs` in the Sindri repository)

**Exercised in:** `games/voxel-lab` (`games/voxel-lab` in the Sindri repository)

**Limits:**

- Only opaque block batches are bridged to the GPU; cutout/transparent pipelines and editor voxel picking/painting remain.

<details markdown="1">
<summary>Evidence</summary>

The block mesher samples a one-voxel halo through `VoxelSource`, so adjacent
solid sections omit their shared faces without requiring both to be resident.

Compiled block output is CPU-side indexed geometry in section-local
coordinates. It carries unit-square UV corners plus semantic voxel and face
identity, splits opaque, cutout, and transparent passes, and reports
world-space section bounds. Games provide `VoxelMaterialSource` policy for
render class and whether a voxel occludes every neighbour, only a matching
voxel, or none. Atlas lookup, texture bindings, and GPU upload deliberately
remain outside this crate.

A block from a block set is meshed as the box its tile describes — a slab's top
half way up, a post a thin column — and a neighbour's face is dropped only
where a block covers all of it. Each cell wears one of its block's variants,
chosen by its position and the world's `variant_seed` exactly as a tile volume
chooses them, and a block with something standing on it wears its `covered`
look. A block's faces may animate through frames and a block may glow (drawn
per batch through a `MeshSurface` UV offset and glow, with no remeshing);
blocks that do not hide their neighbours are cut out, and `liquid` ones hide
only their own inner faces.

</details>

<a id="voxel-mesh-cache"></a>

#### Persistent voxel compiled/GPU mesh cache

CPU and GPU caches keyed by section and revision keep current geometry while replacements are pending.

Engine 🟡 · Editor 🟡 · Decay ❌ · Proof 🟡 — parity row *Persistent voxel compiled/GPU mesh cache*

**Tested by:** `crates/sindri-scene/src/voxel_render/tests.rs` (`crates/sindri-scene/src/voxel_render/tests.rs` in the Sindri repository)

**Exercised in:** `games/voxel-lab` (`games/voxel-lab` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

A generic `SectionMeshCache<T>` owns the renderer-independent lifetime
contract: block, smooth, hybrid, and custom representations are separate;
current geometry stays available while a replacement is pending; superseded
worker results are rejected; and one removal releases every profile of a
leaving section. `sindri-render` has the matching GPU-side contract for
textured meshes: opaque cache identities and monotonic revisions reuse
unchanged vertex/index buffers, retain the last buffers while replacement
geometry is unavailable, accept 32-bit indices, release departed entries
explicitly, and expose cumulative cache counters. `sindri-scene` is the
texture-aware seam between the two: it maps semantic voxel/face identities to
atlas regions, groups one section into deterministic texture batches, keeps
stable GPU identities across newer section revisions, ignores superseded
results, and releases every batch when a section leaves residency.

A resident voxel world keeps its compiled sections until its definition or the
textures its own faces resolve to change; binding an unrelated texture does not
rebuild it. Voxel Lab uses that component, while its browser route exposes the
persistent-cache and settled-remesh counters.

</details>

<a id="voxel-collision"></a>

#### Voxel collision

`sindri.physics3d.voxel_collider` makes an authored voxel world solid near the bodies that can reach it.

Engine ✅ · Editor ✅ · Decay ✅ · Proof 🟡 — parity row *Voxel collision*

**Tested by:** `crates/sindri-scene/src/physics_sync3d/voxel_tests.rs` (`crates/sindri-scene/src/physics_sync3d/voxel_tests.rs` in the Sindri repository), `crates/sindri-scene/src/voxel_collision3d/tests.rs` (`crates/sindri-scene/src/voxel_collision3d/tests.rs` in the Sindri repository), `crates/sindri-scene/src/voxel_collision3d/lifecycle_tests.rs` (`crates/sindri-scene/src/voxel_collision3d/lifecycle_tests.rs` in the Sindri repository), `game/tests/a_block_lands_on_voxel_lab.rs` (`game/tests/a_block_lands_on_voxel_lab.rs` in the Sindri repository), `scripts/browser/voxel-landing/observer.decay` (`scripts/browser/voxel-landing/observer.decay` in the Sindri repository)

**Exercised in:** `games/voxel-lab` (`games/voxel-lab` in the Sindri repository)

**See:** `docs/physics.md` (in `07-physics-and-movement.md`), `docs/physics-voxel-editor-review.md` (`docs/physics-voxel-editor-review.md` in the Sindri repository)

**Limits:**

- No game uses voxel collision since Causeway became a tactics game on model islands; the proof is a fixture over Voxel Lab.

<details markdown="1">
<summary>Evidence</summary>

`sindri.physics3d.voxel_collider` on a `sindri.voxel_world` entity resolves the
world's generator, palette and edits through `VoxelGround`, takes each block's
collision box from its block set (`collides`, default true, with the tile's own
height/extent bounds; water is usually `false`), and keeps resident only
the sections dynamic bodies can reach this step plus a margin.
`ScenePhysics3d::step_with_tile_sets` validates the voxel snapshot and the body
batch before committing either, and the shared game session and editor Play
pass their block sets. Causeway used it until it became a tactics game played
on model islands; now a fixture over Voxel Lab gives the world collision and
drops a block onto it, natively in `a_block_lands_on_voxel_lab.rs` and in an
exported build in a CI Chromium run. Scene regressions exercise landing, owner
events/queries, edited shafts under a sleeping body, distant residency,
non-colliding and slab blocks, release and atomic rejection, and the native
editor review authors the voxel collider, block-set
Collides and 3D bodies and lands them in Play.

`SceneVoxelCollision3d` reconciles explicit complete resident voxel snapshots
into static owner groups. Occupancy/policy revisions retain compiled geometry;
scale/settings changes reuse boxes, pose-only changes move the body, and
section exit or world inactivity/removal releases owned geometry. Complete
prevalidation and world/section/rebuild/piece limits reject invalid or
over-budget snapshots without publishing partial collision.

</details>

<a id="voxel-gameplay-ground"></a>

#### Voxel ground for gameplay

`VoxelGround` answers gameplay's block, surface and ray questions; scripts build, dig, place and path on a voxel grid.

Engine ✅ · Editor — · Decay ✅ · Proof ✅

**Tested by:** `crates/sindri-scene/src/extract/voxel_ground_tests.rs` (`crates/sindri-scene/src/extract/voxel_ground_tests.rs` in the Sindri repository), `crates/sindri-scene/tests/placement/on_voxel_ground.rs` (`crates/sindri-scene/tests/placement/on_voxel_ground.rs` in the Sindri repository), `crates/sindri-decay/tests/a_script_walks_and_builds_on_voxels.rs` (`crates/sindri-decay/tests/a_script_walks_and_builds_on_voxels.rs` in the Sindri repository), `games/voxel-lab/tests/a_click_lands_on_what_it_points_at.rs` (`games/voxel-lab/tests/a_click_lands_on_what_it_points_at.rs` in the Sindri repository)

**Exercised in:** `games/voxel-lab` (`games/voxel-lab` in the Sindri repository)

<details markdown="1">
<summary>Evidence</summary>

`VoxelGround` answers gameplay's questions from the same generator and edits —
the block in a cell, a column's surface, the first block a ray meets —
remembering generated sections between questions so edits never regenerate
terrain. A voxel world on a grid of boxes (`sindri.tile_grid` with `space:
"solid"`) is that grid's ground: its voxels are the grid's cells, centred on
them as a volume's blocks are, and the grid's calls answer from it. Scripts
read, build and remove blocks with `Grid.block`, `Grid.set_block` and
`Grid.tagged`; `sindri.grid.placement` stands things on its surface;
`Grid.can_reach` and `Grid.step_toward` path across it within the grid's
`max_step`, reading the ground around the walker and its goal; and
`voxel::aim_at_with` says which block and face the pointer is on.
`a_script_walks_and_builds_on_voxels.rs` builds, digs and walks round a wall on
a generated world, and Voxel Lab picks its blocks with it.

</details>

<a id="voxel-map-view"></a>

#### Voxel world viewed as a map

`"view": "map"` draws a voxel world from straight above as height-lit squares in the ordinary sprite batches.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Voxel world viewed as a map*

**Tested by:** `crates/sindri-scene/src/extract/voxel_map_tests.rs` (`crates/sindri-scene/src/extract/voxel_map_tests.rs` in the Sindri repository), `crates/sindri-scene/tests/extraction/voxel_map.rs` (`crates/sindri-scene/tests/extraction/voxel_map.rs` in the Sindri repository), `games/low-tide/tests/the_basin_is_lit.rs` (`games/low-tide/tests/the_basin_is_lit.rs` in the Sindri repository)

**Exercised in:** `games/low-tide` (`games/low-tide` in the Sindri repository)

**Limits:**

- Not drawn: decoration that holds nothing up, and a face's animation or glow.

<details markdown="1">
<summary>Evidence</summary>

A world with `"view": "map"` is drawn instead as a flat map from straight
above (`extract/voxel_map.rs`): each column one unit square wearing the top
face of its highest supporting block, read through the same `VoxelGround`
scripts use, so edits show and agree with gameplay. Squares are lit by height
(a slope against a north-western sun, paler high ground, water darker by
depth) and go into the ordinary sprite batches, so a map sorts and shares draws
with sprites and tilemaps. Only the columns under the world camera's four
corners are worked out, sixteen square at a time, capped at 48 chunks a side,
and remembered until the entity, its textures or its block set change;
switching a world to a map releases its meshed sections. `Grid.surface` and
`Grid.height` read a column's top from Decay (the latter `number?`, `null` where
nothing stands). Extraction tests cover the squares, row direction, relief,
edits and the absence of meshes; Low Tide's Basin is the proof in a game.

</details>

<a id="voxel-map-flooding"></a>

#### Reversible voxel map flooding

A map-only flood overlay with one continuous water height, set and read from Decay, that never edits terrain.

Engine ✅ · Editor 🟡 · Decay ✅ · Proof ✅ — parity row *Reversible voxel map flooding*

**Tested by:** `crates/sindri-decay/tests/a_script_floods_a_map.rs` (`crates/sindri-decay/tests/a_script_floods_a_map.rs` in the Sindri repository), `games/low-tide/tests/a_flood_season.rs` (`games/low-tide/tests/a_flood_season.rs` in the Sindri repository)

**Exercised in:** `games/low-tide` (`games/low-tide` in the Sindri repository)

**See:** `docs/voxel-terrain.md` (`docs/voxel-terrain.md` in the Sindri repository)

**Limits:**

- The editor Scene view is unverified.
- No fluid simulation, 3D flood mesh, decoration or animated/glowing map face.

<details markdown="1">
<summary>Evidence</summary>

A map-only `map_flood` overlay with `Grid.set_flood` and `Grid.flooded`: one
continuous water height, strict submersion against the column's existing
surface, a block palette top face and depth shading. Flood changes reuse cached
map columns; terrain edits still invalidate them. Terrain, biomes, 3D block
meshes and sparse edits are untouched, including builtin natural overland
terrain. This capability was found by the moving-home genre showcase, Low Tide,
whose season, cargo loss, waiting and escape rules are Decay gameplay. Native
headless tests exercise keys and phone touch; extraction tests exercise the
rendered faces and cache retention.

</details>

### Not yet

<a id="voxel-persistence"></a>

#### Voxel persistence and digging

Sparse overrides survive residency changes in memory, but there is no save format or persisted tunnel proof.

Engine ❌ · Editor ❌ · Decay ❌ · Proof ❌ — parity row *Voxel persistence and digging*

<a id="reusable-voxel-assets"></a>

#### Reusable voxel asset authoring

No project asset format or dedicated editor exists for bounded reusable voxel models such as trees, rocks or props.

Engine ❌ · Editor ❌ · Decay ❌ · Proof ❌ — parity row *Reusable voxel asset authoring*

<a id="path-costs"></a>

#### Per-path costs and policies

A* treats every passable edge alike; there are no per-path costs or policies.

Engine ❌ · Editor ❌ · Decay ❌ · Proof — — parity row *Per-path costs and policies*
