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