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

17 KiB
Raw Blame History

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

// 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

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