# 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` not `Handle` in components**: a component stores a serializable `AssetRef` (= `Option`); `AssetRef::resolve(db, server)` yields the process-local `Handle` 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 ` - **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::` (needs `Default`); enums via `register_enum::`. - 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"); // 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` 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 ` 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).