Files
Oxide/HANDOFF.md
T
Homer Simpson 9eead719b0 Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:41:02 +02:00

258 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 58.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 18c 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).