Blacksite/docs/editor/architecture.md
Rbanh f29712d158
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Initial project import
2026-06-05 21:44:45 -04:00

184 lines
14 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 |
| `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 |
| `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.
## 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:** The atmosphere/HDR stack (`Atmosphere`, `Hdr`, etc.) 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.
**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. 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. 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, and warnings.
**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, 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 semi-transparent overlay, and `G` hides editor-only overlays for a clean game view. A blue status bar shows scene path, mode, selection count, and history. Dock layout restores from `~/.config/bevy-fps/editor_prefs.ron` on startup; **View → Reset Layout** restores defaults.
**Follow-up (Phase 4b):** hierarchy expand/collapse persistence, 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/`.
5. Hydration systems in `game` spawn meshes, colliders, lights, and static mesh renderer parts at runtime. `ActorId` and `ComponentInstanceId` are the persisted identities; raw Bevy `Entity` IDs are runtime-only.
## 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 static mesh manifests under `assets/meshes/generated/`. The manifests store stable part IDs, glTF/FBX mesh/material subasset labels, source metadata, dependencies, and import settings. These artifacts are hidden from the Asset Browser catalog.
- **Default placement:** model drag/drop creates `ActorKind::StaticMesh + StaticMeshRenderer`. Renderer slots reference imported `EditorAssetRef` values (`asset_id` + `sub_asset_id`) and hydration resolves those through the generated artifact. `SingleActor` hierarchy mode stores all parts in one renderer; `SourceHierarchy` creates a root with child static mesh actors.
- **Collision:** when collider generation is enabled, placement adds `RigidBodyDesc` and `ColliderDesc::StaticMesh` using the same imported mesh refs. Renderer slots no longer own collider state.
- **Scene-instance placement:** asset details can switch placement to `SceneInstance`; that keeps `ImportedModel + ModelRef`, which hydrates to `SceneRoot`. glTF uses `GltfAssetLabel::Scene`; FBX uses `bevy_ufbx` (`path#SceneN`). `FbxPlugin` is registered in `GamePlugin`.
- **Limitations:** Binary FBX only (not ASCII). Static mesh placement stores animation/skinning/camera/light metadata and warnings but does not play those features; use Scene Instance for full-scene playback.
## Undo / history
Structural edits (spawn, delete, transform, rename) go through `EditorHistory` command objects in `history.rs`.
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/` | Workspace, project I/O, settings UI |
| `ext/` | Command palette, BRP, game panel adapters |
| `history/` | Undo commands + plugin |
| `ui/` | egui dock shell |
Shared scene schema lives in `crates/scene` (stamp/migrate/validate on save, load, and CI).
## HDR / swapchain invariant
The unified viewport renders through an HDR offscreen target (`render_target.rs`). When the panel target is missing (egui reflow) or settings disable HDR, cameras must strip atmosphere/post-FX before targeting the swapchain. `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.
## 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).