Blacksite/docs/editor/architecture.md
Rbanh 0798aa5d57 Build renderer and material component foundations
Add dedicated skinned rendering, pose restoration, shared Material and Material Instance slots, registry-driven components, Surface/Solari integration, transactional schema upgrades, navigation authoring, documentation, and evaluation evidence.
2026-07-12 00:24:06 -04:00

269 lines
20 KiB
Markdown

# Editor Architecture
High-level layout of the in-process editor binary (`crates/editor`). For mission and phased work see [roadmap.md](roadmap.md) and [mission.md](../mission.md).
## Crate boundaries
```
editor/ egui UI, PIE, BRP, scene I/O, editor-only cameras, hot-reload (dev)
└─ uses ──► game/ GamePlugin shell, re-exports
game_hot/ Hot-reloadable gameplay systems (cdylib; dev)
settings/ assets/project.ron, SimTuning, camera FX profile
shared/ Authoring components, hydration types
sim/ Components + resources (TypeIds stay static)
protocol/ Input intent (network-ready)
```
Native-only dependencies (`rfd`, BRP HTTP) stay in `editor`. `settings`, `shared`, `sim`, and `protocol` must remain usable from a future WASM client.
## Plugin graph (startup order matters)
| Plugin | Role |
|--------|------|
| `GamePlugin` | Shared runtime: rendering, player, sim schedule; standalone default-level bootstrap |
| `ProjectSettingsPlugin` | Load `assets/project.ron`, emit changes, sync `SimTuning` |
| `ProjectIoPlugin` | User prefs, workspace metadata, window title inputs |
| `EditorStatePlugin` | `EditorMode` (Edit/Play), `PlayPossession`, camera/input gating |
| `ViewportPlugin` | Grid, focus, bookmarks, snap settings |
| `EditorCameraPlugin` | Fly camera; FX applied via `render_view` + `viewport_camera` sync |
| `ActorIconsPlugin` | Selectable actor root billboard icons |
| `EditorVisualizersPlugin` | Collider/light/spawn/prefab/runtime visualizers and selectable proxies |
| `SceneIoPlugin` | Level New/Open/Save, recent paths |
| `OperatorPlugin` | Active operator lifecycle/status for commands and modal tools |
| `SettingsUiPlugin` | egui Project Settings panel |
| `EditorUiPlugin` | Dock, themed egui shell, viewport overlays, menus, status bar |
| `BrpPlugin` | Bevy Remote Protocol HTTP (editor-only) |
| `HotReloadPlugin` | Dev-only: `game_hot` dylib watch + status bar (`--features dev`) |
## Hot reload (dev)
Gameplay system bodies live in [`game_hot`](../../crates/game_hot) as a `cdylib`. Stable component/resource TypeIds remain in `sim` / `protocol` / `shared` / `settings` so ECS inserts stay valid after reload (see [ADR 0004](../adr/0004-editor-hot-reload.md)).
| Piece | Location |
|-------|----------|
| Hot system implementations | `crates/game_hot/src/` |
| Schedule registration + hot wrappers | `crates/game/src/lib.rs` (`hot-reload` feature) |
| Dylib poll, notify watcher, Ctrl+Shift+R | `crates/editor/src/hot_reload.rs` |
| Status bar label | `ui/status_bar.rs` |
Workflow: run `cargo watch … build -p game_hot` in one terminal, `cargo run -p editor --features dev` in another. Logic-only edits apply without restarting the editor; component layout changes still need a full restart.
## Settings split (maintainability)
| Concern | Location |
|---------|----------|
| Serializable project data | `crates/settings``ProjectSettings`, save/load |
| Path + dirty flag | `ProjectSettingsIo` resource |
| Panel open/close | `ProjectSettingsPanel` in `settings_ui.rs` |
| Live apply to sim/rendering | `ProjectSettingsChanged` message (draft edits commit on Apply) |
Do not add egui or editor-only state to the `settings` crate.
## Scene Persistence And Recovery
`scene/scene_io.rs` owns authored scene extraction and user-facing save/load requests;
`scene/recovery.rs` owns transactional byte persistence and machine-local recovery generations.
The editor sets `GameSceneBootstrap::defer_default_level`, so `SceneIoPlugin` alone materializes
the startup document. A standalone `GamePlugin` instead attaches a schema-aware `DynamicWorldRoot`
for `ProjectSettings.default_level`; it never runs this automatic load inside the editor.
Serialization always rebuilds stripped hydration state before returning, including error paths.
Manual writes use same-directory temporary files plus sync/rename, while recovery snapshots live
under the user state directory and never clear scene dirty state. Newer recovery is discovered on
load and exposed through File menu restore/keep-copy/discard actions; it is never restored
automatically. Keeping a copy writes a separate transactional scene without changing the active
scene or retiring recovery.
The bounded in-session scene I/O event log feeds the Diagnostics window so a later status update
does not erase a save or recovery failure.
See [ADR 0023](../adr/0023-transactional-scene-persistence-and-recovery.md).
`project/session.rs` owns the independent versioned restart document under the user state
directory. It snapshots allowlisted paths, panel visibility, dock/hierarchy metadata, and camera
bookmarks, writes a running marker at startup, and records clean `AppExit`. Abnormal sessions do
not auto-open the prior scene; the explicit safe-resume prompt is the only path back. See
[ADR 0024](../adr/0024-versioned-editor-session-state.md).
`project/diagnostics_bundle.rs` exports a separate, transactional support report from an explicit
metadata allowlist. It summarizes build/renderer identity, project paths, dirty state, aggregate
validation, the prior crash boundary, and the bounded Scene I/O log without serializing ECS data,
asset contents, environment values, host identity, credentials, or operator state.
The active project root is a startup boundary because `AssetPlugin.file_path`, project settings,
the asset catalog, and the initial level must agree before editor systems initialize.
`project/launcher.rs` validates and activates explicit roots before the app exists, filters
user-local recents, and scaffolds only missing/empty sandbox destinations. The graphical BS-JD-001
chooser builds on this contract. Do not reintroduce in-process project switching that changes only
settings metadata while leaving `AssetServer` on the old root; see
[ADR 0025](../adr/0025-project-root-is-a-startup-boundary.md).
## Camera and viewport model
See [ADR 0014](../adr/0014-unified-viewport-model.md) for the unified viewport decision.
Three camera roles coexist:
1. **Editor fly camera** (`EditorCamera`) - active while editing or ejected from PIE.
2. **Player camera** - active while playing + possessed.
3. **egui overlay camera** (`PrimaryEguiContext`) - full-window `Camera2d`; never viewport-cropped. Attaching egui to a cropped 3D camera causes NaN layout panics.
**Unified viewport:** one docked **Viewport** tab owns the primary offscreen HDR target (`ViewportRenderTarget`). Edit mode and Play-ejected bind the editor fly camera to that target; Play-possessed binds the player camera to the same target. Saved layouts with the old Game View tab are migrated to one Viewport tab.
**WYSIWYG rendering:** `render_view.rs` is the only editor system that mutates viewport post-FX. It calls `game_hot::sync_viewport_camera_stack` on the active camera (editor fly or possessed player), driven by `ActiveCameraRenderProfile` from project settings and volumes plus `EffectiveRenderStack` for requested/effective GI fallback. `scene_view` and `play/session` set HDR render targets and `is_active` only—they do not strip FX. Only one camera carries the stack at a time (mesh-view bind group limits). See [ADR 0015](../adr/0015-viewport-camera-stack-ownership.md) and [ADR 0016](../adr/0016-unified-rendering-contract.md).
The viewport uses **render-to-texture**: the active camera renders to an offscreen HDR target sized to the dock panel (`panel_physical_size()`), then egui displays that texture. Cameras use `viewport = None` on image targets so the scissor always matches the texture; panel size only drives texture allocation, not a sub-viewport on the image.
**HDR invariant:** Atmosphere, authored HDR, and effective Solari all require an `Hdr`
camera target; they must never render to the primary window swapchain
(`Rgba8UnormSrgb`). When the HDR offscreen target is missing, `scene_view` / `play`
deactivate cameras; `render_view` skips stack sync until a target exists.
Render-target changes and Solari/Forward fallback changes force a full camera-stack rebuild so
stale sky or post-FX pipelines cannot survive across texture-format transitions.
**Viewport picking:** LMB selection maps the egui panel UV through `scene_view_ray` and raycasts with `MeshRayCast`. Object pick runs in `Last` after `transform_gizmo_bevy` so gizmo drags take priority. `GizmoOptions.viewport_rect` is synced from the viewport panel rect so gizmo hit-testing matches render-to-texture layout. Actor root icon and visualizer proxy hits resolve to their source entity, so clicking an icon or light/spawn/collider marker selects the authored or runtime object, not the helper. Actor icon hits are ordered ahead of ordinary mesh hits when both are under the cursor. In Edit mode, selecting the runtime Player redirects to an authored `PlayerSpawn` (`Player Start`), creating one if needed.
**Selection overlay:** `selection_outline.rs` draws a shared unit-box shell per selected `LevelObject` (bounds from `Primitive`, physics collider, or mesh AABB), scaled to fit. Custom shader (`assets/shaders/selection_outline.wgsl`) uses rim-only fresnel with two cheap depth passes: `Greater` (soft cyan x-ray) and `LessEqual` (amber edge). Sync runs only when selection changes. Editor-only.
**Clean game-view overlay:** `ViewportDisplayMode.clean_game_view` hides editor chrome, grid, selection outlines, transform gizmos, actor root icons, visualizers, and selectable proxy meshes without changing camera ownership. `G` toggles this mode; `Ctrl+G` toggles the grid.
**Actor root icons:** `actor_icons.rs` registers the editor PNG icon set as internal binary image assets and spawns screen-stable, camera-facing editor-only billboards at `LevelObject` roots. A custom overlay material draws them unlit with always-pass depth so opaque meshes do not obstruct them. The billboards are `RaytracingExcluded`, hidden with clean game-view, filtered from hierarchy and scene serialization, and expose per-category viewport options for actors, meshes, lights, physics, gameplay, volumes, prefab/model anchors, warnings, and icon size.
**Scene visualizers:** `visualizers.rs` draws transient gizmo lines for authoring components without obvious meshes: colliders, lights, `PlayerSpawn`, prefab/model anchors, Project Sun, and runtime player/camera markers. Small editor-only proxy meshes make those markers selectable but are filtered from hierarchy and scene serialization. Player/start markers use the runtime capsule constants from `sim::tuning`.
**World lighting:** Project settings provide default ambient light and a runtime `ProjectSun`. A scene-authored directional `LightDesc` is treated as a scene sun override; when present, `sync_project_sun_visibility` hides/disables the project sun to avoid duplicate directional lighting.
## Editor UI shell
The egui layer lives under `crates/editor/src/ui/`:
| Module | Role |
|--------|------|
| `mod.rs` | `EditorUiPlugin`, `UiState`, dock host, tab titles |
| `theme.rs` | Unity-like dark pro palette + `egui_dock` chrome |
| `fonts.rs` | Phosphor icon font on `PrimaryEguiContext` |
| `widgets.rs` | Icon buttons, tool toggles, menu helpers |
| `asset_browser.rs` | Project/file browser with tree, breadcrumb, filters, list/grid modes, embedded model shelves, details/import settings |
| `menu.rs` | Top menu bar with shortcuts |
| `viewport_chrome.rs` | Unified viewport RTT + overlay toolbars |
| `actor_icons.rs` | Screen-stable actor root icons and selectable icon proxies |
| `visualizers.rs` | Scene visualizer settings, gizmo drawing, selectable proxies |
| `toolbar.rs` | Bottom dock icon toolbar |
| `status_bar.rs` | Fixed bottom status strip |
| `hierarchy.rs` / `asset_browser.rs` / `inspector.rs` | Panel bodies |
| `diagnostics.rs` | Detailed stats (Window → Diagnostics, Asset Browser footer) |
| `layout.rs` | Dock layout RON persistence in `editor_prefs.ron` |
The Viewport fills the tab (no inline help text). Gizmo/grid/play controls sit on a near-black semi-transparent overlay, and `G` hides editor-only overlays for a clean game view. The fixed dark status strip prioritizes `SceneIo.status` feedback and also shows scene path/dirty state, mode, selection count, history, and the active operator. Long feedback is truncated responsively and available in a hover tooltip. Dock layout restores from `~/.config/bevy-fps/editor_prefs.ron` on startup; **View → Reset Layout** restores defaults.
**Follow-up (Phase 4b):** inspector search and richer per-asset previews.
## PIE state machine
```mermaid
stateDiagram-v2
[*] --> Editing
Editing --> Playing_Possessed: F5 enter play
Playing_Possessed --> Playing_Ejected: F8 eject
Playing_Ejected --> Playing_Possessed: F8 possess
Playing_Possessed --> Editing: F5 stop
Playing_Ejected --> Editing: F5 stop
```
| Resource | Editing | Playing possessed | Playing ejected |
|----------|---------|-------------------|-----------------|
| `SimEnabled` | false | true | true |
| `GameInputEnabled` | false | true | false |
| Editor camera active | true | false | true |
| Player camera active | false | true | false |
On play enter/exit the editor snapshots **player sim state only** (transform, velocity, jump timers). Authored scene entities are not rolled back so in-play edits persist when stopping PIE.
## Scene authoring flow
1. User edits entities with `LevelObject` + reflectable components from `shared`.
2. `EditorOnly` entities (cameras, helpers) are filtered from hierarchy and save; visualizer proxies can be picked but resolve back to source entities.
3. Player placement is stored as `PlayerSpawn`; the runtime `Player` is never serialized.
4. `SceneIo` writes native Bevy dynamic scenes under `assets/levels/`, routed through `scene::document::SceneDocument`. The active tab is materialized in the ECS world; inactive tabs retain normalized authored snapshots and independent dirty/recovery state. `SceneComposition` resources reference validated project-relative subscenes by stable IDs, and runtime-only `ComposedSceneMember` ownership prevents child actors from being flattened into the owner save. See [multi-scene-composition.md](multi-scene-composition.md) and ADR 0026.
5. Hydration systems in `game` spawn meshes, colliders, lights, static mesh renderer parts, and
dedicated skinned-model hierarchies at runtime. Level-object roots are initialized with Bevy
visibility hierarchy components before generated children are attached, so authoring
`EditorVisibility` participates in normal visibility propagation. `ActorId` and
`ComponentInstanceId` are the persisted identities; raw Bevy `Entity` IDs are runtime-only and
must not become tool-facing document IDs.
6. Brush actors store authored convex geometry as `BrushDesc`; hydration rebuilds generated mesh children from those faces and strips the children before save.
## Model import (glTF + FBX)
- **Import:** File → Import Assets copies glTF/GLB/FBX into `assets/models/`. FBX imports also copy a sibling `{name}.fbm/` folder when present (embedded textures).
- **Processing:** the asset registry generates normalized model manifests under
`assets/meshes/generated/`. The manifests store stable part IDs, glTF/FBX mesh/material subasset
labels, whether each part is skin-bound, source metadata, dependencies, and import settings.
These artifacts are hidden from the Asset Browser catalog.
- **Browser subassets:** model rows can expand into a content shelf backed by the generated
manifest. Unrigged mesh subassets place as independent `StaticMeshRenderer` actors. A skinned
subasset places its owning source through `SkinnedMeshRenderer`, retaining joints and inverse bind
poses. Material subassets expose source defaults, and texture dependencies can be applied to
selected actors. The thumbnail studio renders each generated model/subasset/material preview into
its own render target before registering it in the cache so later thumbnails cannot overwrite
earlier cache entries.
- **Default placement:** **Renderable Asset (Auto)** creates
`ActorKind::StaticMesh + StaticMeshRenderer` for unrigged, non-animated models and
`ActorKind::SkinnedMesh + SkinnedMeshRenderer` when any part is skin-bound or the source contains
animation. Static renderer slots
reference imported `EditorAssetRef` values and exclude skinned parts. Static `SingleActor` mode
stores all parts in one renderer; `SourceHierarchy` creates a root with child static mesh actors.
Skinned placement always preserves the imported source hierarchy.
- **Collision:** when collider generation is enabled, static placement adds `RigidBodyDesc` and
`ColliderDesc::StaticMesh` using the same imported mesh refs. Skinned render surfaces do not
receive automatic triangle colliders; author stable gameplay collision explicitly.
- **Scene-instance placement:** asset details can switch placement to `SceneInstance`; that keeps
`ImportedModel + ModelRef` for a generic source scene and hydrates to `WorldAssetRoot`. It is not
the animation renderer. glTF uses `GltfAssetLabel::Scene`; FBX uses `bevy_ufbx` (`path#SceneN`).
`FbxPlugin` is registered in `GamePlugin`.
- **Limitations:** Binary FBX only (not ASCII). Animated or skinned FBX remains blocked until its
loader can construct the same hierarchy and clips as the glTF skinned path. See ADRs 0031 and
0033.
## Undo / history
Structural edits (spawn, delete, transform, rename) go through `EditorHistory` command objects in `history.rs`.
User-facing actions route through the operator lifecycle in `operators.rs`: begin, preview,
commit, cancel, disabled reason, and status text. The first bridge wraps command-palette and queued
editor commands as immediate operators, while existing `EditorHistory` commands remain the undo
payload. Asset placement, mesh-subasset placement, texture apply, and material apply actions now
use immediate operators from `assets::operators`; texture/material changes across a selection use
one `SetMaterialGroup` command. Draw Brush, brush CSG, clip, and element gizmo paths publish the
same preview/commit/cancel phases. `operators/test_harness.rs` verifies authored deltas, dirty
state, helper cleanup, undo grouping, and undo/redo restoration; see
[operator-regression-testing.md](operator-regression-testing.md).
PIE stop restores player simulation state only; authored `LevelObject` edits made during PIE remain in the scene.
## Editor crate layout (ADR 0008)
`crates/editor` is a **library + binary**. `EditorPluginGroup` in `lib.rs` registers plugins in a fixed order. Domain modules:
| Directory | Responsibility |
|-----------|----------------|
| `scene/` | Level I/O, schema plugin, viewport RTT setup |
| `viewport/` | Camera, selection, gizmos, render views, panel settings |
| `play/` | PIE session, editor mode, net editor profiles |
| `assets/` | Catalog, asset DB, static mesh artifacts, prefab overrides |
| `project/` | Project I/O and settings UI |
| `ext/` | Command palette, BRP, game panel adapters |
| `history/` | Undo commands + plugin |
| `ui/` | egui dock shell |
Shared scene schema lives in the Bevy-free `crates/scene` crate (stamp/migrate/validate on save,
load, and CI). `game::schema_world_loader` is the runtime adapter that validates and unwraps the
schema envelope before Bevy deserializes versioned prefab assets; both game and editor install it
through `GamePlugin`.
## HDR / swapchain invariant
The unified viewport renders through an HDR offscreen target (`render_target.rs`). When the panel target is missing (egui reflow), cameras must strip atmosphere/post-FX before targeting the swapchain. User-authored HDR may be disabled, but atmosphere and effective Solari still force `Hdr` on the active viewport camera because Bevy sky/Solari pipelines are not safe to carry across SDR targets. `render_view::sync_project_render_view` runs in **PostUpdate** after RTT target assignment and reapplies the project FX stack whenever the active panel target handle changes (startup, resize), not only on settings Apply. Project Settings uses a **draft buffer**; **Apply** commits after render targets resync.
Bevy 0.19 removed the workspace's prior local `bevy_render` swapchain-timeout patch. The only active Bevy-adjacent patches are compatibility shims for ecosystem crates: `transform-gizmo-bevy` and `bevy_ufbx` under `third_party/`.
## Extensibility (phase 6)
`extensibility.rs` provides `EditorCommand`, `EditorPlugin`, and a working command palette (Ctrl+P). Game crates register panel hooks via `game::editor_ext::editor_panel_setups()`; the editor binary wraps them without editing `ui/mod.rs` (ADR 0007, dogfood: FPS Demo panel, Ctrl+Shift+G).