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>
17 KiB
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, onmain). One follow-up remains (the editor-UX batch — GUI, needs eye-check). Piece table + what's next in §5; details indocs/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-valuedTagscomponent + a project-levelGroupRegistry. Do NOT reintroduce multi-valuedLayermembership. PhysicsCollider.membership/filterareLayerMasks. - Component multiplicity = hybrid (component-multiplicity-decision):
one component of a type per entity (
hecsarchetypal). 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>notHandle<T>in components: a component stores a serializableAssetRef<T>(=Option<AssetUid>);AssetRef::resolve(db, server)yields the process-localHandle<T>at runtime. The inspector recognises both spellings viaasset_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_argumentsfires at >7 params; bundle args into tuples/structs.) - Branch promotion rule (from CLAUDE.md):
- Pure-logic, fully proven by automated tests → commit to
devand fast-forwardmain. 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
devonly, 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 atdev.
- Pure-logic, fully proven by automated tests → commit to
- 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 indocs/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 testsat the bottom of each file; integration tests intests/src/lib.rs. Even for visual pieces, factor the math into pure helpers and unit-test those (e.g.collider_wire_segments/push_arcin 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
thiserrorenums with specific variants. - Reflection: public fields only;
#[reflect(skip)]drops a public field;#[reflect(min=, max=)]on an f32 → slider. Register editor-visible types inoxide_editor::state::register_builtin_types; addable ones viaregister_addable::<T>(needsDefault); enums viaregister_enum::<E>. - New deferred ideas →
PLAN.mdbacklog, 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_addableadds an "Add Component" constructor;register_enumlists variants for combo widgets.oxide_engine::prefab:Prefab { name, components },ComponentSpec { type_name, ron }; editor seeds built-ins (Empty/Cube/…).oxide_engine::layer: single-valuedLayer { 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_widgetdispatches onFieldInfo.type_name. - Viewport overlay (
editor/src/shell.rs): the host feeds aViewportOverlay { view_proj, gizmo_size }each frame; the Viewport tab paints 2D over the 3D scene viaproject(world, &view_proj, tab_rect) -> Pos2. Transform gizmo handles, the play-state border, and the new collider wireframes all paint this way. Reuseproject+painter.line_segmentfor 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 playApp(DefaultModules + PhysicsModule + ScriptModule;app.assetsis set to a clone of the editor'sAssetServerand the projectAssetDatabaseis inserted as a resource so scripts resolve + live-reload reaches a playing scene), built on Play / dropped on Stop, swapsstate.scenein/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 oncursor_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 onmain):
| 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
Scriptinspector — write a.rhaitemplate intoassets/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
.rhaiin an in-editor text view or launch$EDITOR/ a configured external editor; edits flow back through live reload (you can also just run$EDITOR filein 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.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).