1
Handoff
Jaroslav Beneš edited this page 2026-08-08 18:11:58 +02:00
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 Roadmap; project rules are Working-notes; 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 Scripting.

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 Physics.

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 LayerMasks.
  • 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 Roadmap, the repository's README.md and this wiki current alongside the code. Physics is on Physics.

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 → the Roadmap backlog, not the current piece.

4. Key architecture facts

// 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: Scripting. 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 Scripting ("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

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.mdcomponent-multiplicity-decision.mdrun-on-target-pc.md (headless-vs-eye-check rule) → workflow-auto-continue.md (don't pause between pure-logic pieces).