# Oxide — Session Handoff

Drop this file into a new session and say "continue from HANDOFF.md". It
captures where we are, how we work, and exactly what to do next. Authoritative
roadmap is `PLAN.md`; project rules are `CLAUDE.md`; this is the "current
state + how to continue" snapshot.

_Last updated: 2026-06-17. **`main` and `dev` are aligned.** **Stage 9 — Physics
is ✅ COMPLETE on `main`.** **Stage 10 — Scripting, Live Reload & Editor Terminal:
core ✅ on `main`** (all eye-checked & approved 2026-06-17 in the `untitled`
project): the `oxide-script` crate (`Script` + `.rhai` loader + sandboxed
`ScriptEngine`), the `init`/`update(dt)` lifecycle via `ScriptHost`, the
`Vec3`/transform engine API, live reload (`examples/script_spin`), editor
integration (`Script` addable; play-loop shares the editor
`AssetServer`/`AssetDatabase` so a live `.rhai` edit updates a *playing* scene),
the **Console** (captures the `log` stream — script `print`/errors), a **command
terminal** (`$` → `sh -c`), and an **interactive PTY terminal** (`portable-pty` +
`vt100`; tabbed, auto-closes a tab when its program exits, Tab/arrows/Esc routed
to the program — runs shells / TUIs / `claude`), and the **richer script API**
(scripts now `spawn_entity`/`despawn` and `add_component`/`set_component`/
`remove_component` on any entity via RON through the reflection registry —
closing the last PLAN round-trip criterion; pure-logic + headless tests → on
`main`). **What's left in Stage 10:** the maintainer's editor-UX batch —
Unity-style file explorer, native New/Open-Project dialog (typing the path by
hand is too hard), a **New Script** button on the `Script` inspector,
open-script-in-editor. See §5 to start. Parked, not blockers: editor-authored
joint **components** need a serializable entity-reference type._

---

## 1. Where we are

- **Phase 1 / Stages 5–8.7: ✅ on `main`.** Engine core, editor framework,
  input + gizmos, comprehensive UI, Reflection v2 + asset database (Stage 8.5),
  and editor Play Mode (Stage 8.7).
- **Phase 1 / Stage 9 — Physics: ✅ complete on `main`.** All pieces 1–8c done
  and promoted (eye-checked & approved 2026-06-17).
- **Phase 1 / Stage 10 — Scripting: 🚧 core done on `main`** (crate, lifecycle,
  live reload, editor integration, Console, command terminal, interactive PTY
  terminal — all eye-checked & approved; **richer script API**
  spawn/despawn/component-edit — pure-logic, on `main`). One follow-up remains
  (the editor-UX batch — GUI, needs eye-check). Piece table + what's next in §5;
  details in `docs/scripting.md`.

### Stage 9 — piece status (all on `main`)

| # | Piece | Where |
|---|-------|-------|
| 1 | Component data model + module wiring: `RigidBody`/`RigidBodyKind`, `Collider`/`ColliderShape`, `PhysicsModule`, `PhysicsSettings` | `physics/` (the `oxide-physics` crate) |
| 2 | Rapier-backed sim: build the world from components, step on `FixedUpdate`, write transforms back; by-entity forces/velocities/sleep | `physics/src/world.rs` |
| 3 | Collision groups/masks via `LayerMask`, sensors, collision/trigger events (enter/stay/exit) | `physics/src/world.rs` |
| 4 | Scene queries: raycast, sphere-cast, point/overlap with `LayerMask` filtering | `physics/src/world.rs` |
| 5 | Joints/constraints: fixed, spherical, revolute, prismatic (programmatic API) | `physics/src/world.rs` |
| 6 | Kinematic capsule character controller: move-and-slide, step offset, slope limit, grounded | `physics/src/{character,world}.rs` |
| 7 | `examples/physics_stack` + `examples/character_capsule` (headless console demos) | `examples/` |
| 8a | Editor integration: register RigidBody/Collider/CharacterController (addable, reflected) + enums; wire `PhysicsModule` into the play `App` (physics = first real consumer of Play; Stop reverts via snapshot) | `editor/src/state.rs`, `editor/src/main.rs` |
| 8b | **Collider wireframe gizmos**: box/sphere/capsule/cylinder outlines over the viewport; green=solid, amber=sensor, selected drawn thicker; matches the sim (ignores `Transform::scale`); **View ▸ Show Colliders** toggle (on by default) | `editor/src/shell.rs` |
| 8c | **Raycast debug probe**: `PhysicsWorld::sync_to_scene` (query the edited scene with no Play) + **View ▸ Raycast Probe** — click freezes a camera→cursor ray into the world (orbit to see it as a 3D line), cyan ray + magenta hit dot + normal whisker, status hint on cast. Overlay `view_proj` decoupled from selection (colliders/probe show with nothing selected) | `physics/src/world.rs`, `editor/src/{shell,main}.rs` |
| — | Bug fix: namespace every inspector field widget per component (`ui.push_id(component_name)`) so a field name shared by two components (e.g. both `MeshRenderer` and `Collider` have `shape`) can't collide egui ids and block edits | `editor/src/shell.rs` |

The `oxide-physics` crate: the **ECS is the source of truth**; the rapier world
is a transient resource rebuilt from components each step, so play-mode
snapshot/restore works for free. Full usage + internals in `docs/physics.md`.

### Key design decisions (also in memory + PLAN.md)

- **Layers = Unity "Model A"** ([[layer-group-model-decision]]): an entity is on
  **one** `Layer` (single index, render/physics filter slot); multi-category
  needs are served by **Groups** = the multi-valued `Tags` component + a
  project-level `GroupRegistry`. **Do NOT reintroduce multi-valued `Layer`
  membership.** Physics `Collider.membership`/`filter` are `LayerMask`s.
- **Component multiplicity = hybrid** ([[component-multiplicity-decision]]):
  one component of a type per entity (`hecs` archetypal). For "multiple things on
  one node": a **local-offset field** for naturally-single positioned things, and
  **child entities** for genuine multiples. NOT internal multi-instance lists.
- **`AssetRef<T>` not `Handle<T>` in components**: a component stores a
  serializable `AssetRef<T>` (= `Option<AssetUid>`); `AssetRef::resolve(db,
  server)` yields the process-local `Handle<T>` at runtime. The inspector
  recognises both spellings via `asset_ref_target`.

---

## 2. How we work (follow this exactly)

- **Build each stage as small, independently-tested pieces.** One piece = one
  green commit. Split a piece into a pure-logic part (→ `main`) and a GUI part
  (→ `dev`) when cleaner.
- **Local gate before every commit:**
  `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test`.
  (`#![deny(warnings)]` is on in the editor — clippy lints are hard errors, e.g.
  `too_many_arguments` fires at >7 params; bundle args into tuples/structs.)
- **Branch promotion rule (from CLAUDE.md):**
  - Pure-logic, fully proven by automated tests → commit to `dev` **and**
    fast-forward `main`. Continue without asking. (Headless GPU pixel tests count
    as proof — run them yourself.)
  - Interactive GUI / rendering-feel / shell polish (anything a human must
    **look at**) → push to `dev` only, **ask the maintainer to run + eye-check**,
    promote after explicit approval.
  - Decision rule: "Can an automated test prove this without a human looking?"
    Yes → `main`-eligible. No → stop at `dev`.
- **Promotion commands** (from `dev`, after committing + pushing dev):
  `git checkout main -q && git merge --ff-only dev -q && git push origin main && git checkout dev -q`
- **Commit messages** end with: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`
- **Document as you build:** keep `PLAN.md`/`README.md`/`docs/` current in the
  **same** change. Physics docs live in `docs/physics.md`.

### egui gotcha
egui's bundled font has a **limited glyph set**. Confirmed-rendering: `🗑 ➕ ⚙ ✏
▾ … ↺`. Confirmed-tofu (avoid): `✕` (U+2715), `⧉`, `⣿` (braille). Reuse an icon
already in the codebase or test it before shipping; prefer plain text if unsure.
Also: egui assigns widget ids from a hash of the call path + a salt — when the
same logical widget appears twice (two components with a same-named field), scope
it under `ui.push_id(unique_key)` or it errors and edits silently break.

---

## 3. Conventions (match the existing code)

- Heavy rustdoc: each public type/fn gets a one-line summary + the "why".
  Module-level `//!` docs explain the piece's role.
- Unit tests in `#[cfg(test)] mod tests` at the bottom of each file; integration
  tests in `tests/src/lib.rs`. Even for visual pieces, factor the math into pure
  helpers and unit-test those (e.g. `collider_wire_segments`/`push_arc` in 8b),
  so only the painting itself needs the eye-check.
- Serialization is `serde` + RON throughout. New optional fields use
  `#[serde(default, skip_serializing_if = ...)]` so older documents still parse.
- Errors are `thiserror` enums with specific variants.
- Reflection: **public fields only**; `#[reflect(skip)]` drops a public field;
  `#[reflect(min=, max=)]` on an f32 → slider. Register editor-visible types in
  `oxide_editor::state::register_builtin_types`; addable ones via
  `register_addable::<T>` (needs `Default`); enums via `register_enum::<E>`.
- New deferred ideas → `PLAN.md` backlog, not the current piece.

---

## 4. Key architecture facts

```rust
// Per-field reflection on any component (zero per-type editor code):
#[derive(Reflect, Serialize, Deserialize)]
struct Timer { pub repeating: bool, pub duration: f32, #[reflect(skip)] pub elapsed: f32 }
registry.register_reflected::<Timer>("Timer");   // editor + scripts

// Inspector renders generically: components_on -> field_infos -> get_field/set_field,
// edits routed through SetFieldCmd (undoable, drag-coalesced).
```

- **`oxide_engine::reflect`**: whole-value (`get_ron`/`set_ron`) + per-field
  (`Reflect` + `register_reflected`) layers. `register_addable` adds an "Add
  Component" constructor; `register_enum` lists variants for combo widgets.
- **`oxide_engine::prefab`**: `Prefab { name, components }`,
  `ComponentSpec { type_name, ron }`; editor seeds built-ins (Empty/Cube/…).
- **`oxide_engine::layer`**: single-valued `Layer { index }`; `LayerMask`
  (filter); `LayerRegistry` (names); `Tags` + `GroupRegistry` (groups).
- **Editor inspector** (`oxide_editor::shell::ShellTabViewer`): node-baked
  section (Layer/Groups/Transform) then modular components (drag-reorder,
  enable/disable, remove, Add Component menu). `field_widget` dispatches on
  `FieldInfo.type_name`.
- **Viewport overlay** (`editor/src/shell.rs`): the host feeds a
  `ViewportOverlay { view_proj, gizmo_size }` each frame; the Viewport tab paints
  2D over the 3D scene via `project(world, &view_proj, tab_rect) -> Pos2`.
  Transform gizmo handles, the play-state border, **and the new collider
  wireframes** all paint this way. Reuse `project` + `painter.line_segment` for
  any new world-space overlay (e.g. piece 8c's raycast viz).
- **Play loop** (`oxide_editor::main::drive_play` + `oxide_engine::scene::
  SceneSnapshot`): owns a play `App` (DefaultModules + PhysicsModule +
  ScriptModule; **`app.assets` is set to a clone of the editor's `AssetServer`**
  and the project `AssetDatabase` is inserted as a resource so scripts resolve +
  live-reload reaches a playing scene), built on Play / dropped on Stop, **swaps
  `state.scene` in/out per tick** so the editor scene stays the single source of
  truth. Snapshot covers reflected components + `Tags`/`DisabledComponents`. Undo
  cleared on Play/Stop.
- **Host input** (`oxide_editor::main`): viewport orbit/pan/zoom + WASD gated on
  `cursor_over_viewport && !pointer_over_floating`.

---

## 5. Exactly what to do next — finish Stage 10 (Scripting)

**Stage 10's core is done and on `main`** (all eye-checked & approved). The
`oxide-script` crate is the worked-out twin of `oxide-physics` (same
"ECS-as-truth, transient runtime resource, snapshot-for-free" shape). Full usage
+ internals: `docs/scripting.md`. What shipped (all on `main`):

| Piece | Where |
|-------|-------|
| `oxide-script` crate: `Script`, `ScriptAsset` + `.rhai` loader (`AssetKind::Script`), sandboxed `ScriptEngine` (compile/run, op cap, `ScriptError`), `ScriptModule` | `script/` |
| Lifecycle: `ScriptHost` + `run_scripts` (Update schedule); `init`/`update(dt)`; **engine API** (`bridge.rs`) — scripts read/write their `Transform` via a staged shared context (`position`/`translate`/`rotate_*`/`Vec3`), `rhai` `f32_float` | `script/src/{host,engine,bridge}.rs` |
| **Live reload**: `ScriptHost` holds each script's `Handle<ScriptAsset>` alive → watcher's in-place reload recompiles (no restart); `examples/script_spin` | `script/src/host.rs`, `examples/` |
| **Editor integration**: `Script` addable in `register_builtin_types`; `drive_play` adds `ScriptModule` + **shares the editor `AssetServer`** + a db snapshot → live `.rhai` edits update a *playing* scene | `editor/src/{state,main}.rs` |
| **Console**: capturing logger mirrors the `log` stream into a ring buffer the panel renders (script `print` + paused-script errors via `target:"oxide_script"`) | `editor/src/console.rs`, `shell.rs::console` |
| **Command terminal**: `$` prompt → `sh -c <cmd>` in the project root, streams stdout/stderr into the Console | `editor/src/terminal.rs` |
| **Interactive PTY terminal**: `PanelKind::Terminal` — tabbed, auto-closes a tab when its program exits, Tab/arrows/Esc routed to the program; runs shells / TUIs / `claude` | `editor/src/pty.rs`, `shell.rs::terminal_panel` |

### Remaining for Stage 10

**1. Richer script API — ✅ DONE, on `main`.** Scripts now `spawn_entity()` /
`spawn_entity(name)`, `despawn(e)`, and `add_component`/`set_component`/
`remove_component` on any entity, plus `entity()` for their own. Implemented as a
**deferred command buffer** in `bridge.rs` (`EntityHandle` + `ScriptCommand`):
the `rhai` functions can't borrow the ECS (must be `Send + Sync`), so each call
buffers a command the host drains in `ScriptHost::apply_commands` and applies via
the reflection registry (`app.types`, `set_ron`/`add_default`/`remove`). `spawn_entity`
returns a **provisional** handle resolvable in the same frame (`spawn` is a rhai
reserved word, hence the longer name). Component edits go through RON, closing the
last PLAN round-trip criterion. Headless tests in `host.rs`/`bridge.rs`; docs in
`docs/scripting.md` ("Spawning entities and editing components").

**2. Maintainer's editor-UX batch (GUI → `dev` + eye-check).** Details in
PLAN.md "Editor-UX follow-ups" + [[editor-file-explorer-preference]]. The
maintainer asked for these after the terminal work:
- **"New Script" button** on the `Script` inspector — write a `.rhai` template
  into `assets/scripts/`, register it in the db, auto-assign it to the component.
  There is **no in-editor script creation today** (scripts are authored as
  files). Good small first one to pull forward.
- **Open a script in an editor** — double-click / button to open a `.rhai` in an
  in-editor text view or launch `$EDITOR` / a configured external editor; edits
  flow back through live reload (you can also just run `$EDITOR file` in the new
  PTY terminal).
- **Unity-style file explorer** in the Project panel (create/rename/move folders,
  drag-in OS import, context menus) over the existing `AssetDatabase`.
- **Native New/Open-Project dialog** (`rfd`, must work on Wayland + X11) —
  typing the project path by hand "is very hard to use".

**Reuse, don't reinvent:** `oxide-physics`/`oxide-script` are the template for
"new crate → `Module` + schedule → reflected/addable components → editor
registration → play-loop integration". The play loop
(`oxide_editor::main::drive_play`) runs scripts alongside physics and shares the
editor's asset server, so live reload reaches a playing scene.

**Terminal design note:** the embedded terminal is a general PTY widget
(`portable-pty` + `vt100`); run any program (incl. `claude`) from a *Shell* tab.
A "one-app launcher" was considered and rejected — it needs the same
PTY+VT+render+input stack, so the general widget is strictly better.

**Don't do these unless asked (parked in PLAN.md):** editor authoring of joint
**components** (needs a serializable entity-reference type); standalone game
"Launch" (deferred to Stage 16).

---

## 6. Quick orientation commands

```sh
cargo test                                           # full suite (all green at handoff)
cargo test -p oxide-script                           # the scripting crate (lifecycle, live reload)
cargo test -p oxide-editor --lib pty                 # PTY terminal pure helpers
cargo run  -p oxide-examples --bin script_spin       # headless live-reload demo
cargo run  -p oxide-editor                           # the editor (Console + Terminal panels)
git log --oneline -15                                # recent per-piece commits
sed -n '/## Stage 10/,/## Stage 11/p' PLAN.md        # Stage 10 spec + status
```

Memory files (auto-loaded each session) track decisions: `MEMORY.md` index →
`layer-group-model-decision.md` → `component-multiplicity-decision.md` →
`run-on-target-pc.md` (headless-vs-eye-check rule) →
`workflow-auto-continue.md` (don't pause between pure-logic pieces).
