Skip to main content

proof_engine/editor/
mod.rs

1//! In-Engine Editor — master state, camera, undo/redo, grid, shortcuts.
2//!
3//! This module is the entry point for the editor subsystem.  It owns the
4//! top-level `EditorState` and wires together all sub-panels.
5
6pub mod inspector;
7pub mod hierarchy;
8pub mod console;
9pub mod gizmos;
10pub mod sdf_node_editor;
11pub mod material_painter;
12pub mod bone_rigger;
13pub mod timeline;
14pub mod kit_panel;
15pub mod asset_browser;
16pub mod camera_controller;
17pub mod perf_overlay;
18pub mod scene_io;
19pub mod shader_graph;
20pub mod viewport;
21pub mod curve_editor;
22pub mod physics_debug;
23pub mod localization;
24pub mod scripting;
25pub mod profiler;
26pub mod prefab;
27pub mod terrain;
28pub mod audio_editor;
29pub mod post_fx;
30pub mod light_probe;
31pub mod particle_editor;
32pub mod color_grading;
33pub mod render_pipeline;
34pub mod anim_retarget;
35pub mod lod_manager;
36pub mod material_editor;
37pub mod node_editor;
38pub mod animation_state_machine;
39pub mod scene_editor;
40pub mod cinematic_editor;
41pub mod nav_mesh;
42pub mod font_editor;
43pub mod ui_canvas;
44pub mod vfx_graph;
45pub mod gpu_profiler;
46pub mod network_editor;
47pub mod input_editor;
48pub mod build_system;
49pub mod event_editor;
50pub mod world_editor;
51pub mod ai_behavior_editor;
52pub mod physics_editor;
53pub mod render_graph_editor;
54pub mod dialogue_editor;
55pub mod quest_editor;
56pub mod spline_editor;
57pub mod cinematic_sequencer;
58pub mod inventory_editor;
59pub mod ability_editor;
60pub mod level_streaming_editor;
61pub mod audio_mixer_editor;
62pub mod modeling_editor;
63pub mod animation_compression;
64pub mod voxel_editor;
65pub mod terrain_road_tool;
66pub mod cutscene_importer;
67pub mod map_editor;
68pub mod particle_system_editor;
69pub mod shader_compiler;
70pub mod loot_editor;
71pub mod world_streaming_editor;
72
73use glam::{Vec2, Vec3, Vec4};
74use std::collections::HashMap;
75
76// ─── re-export sub-types so callers can `use proof_engine::editor::*` ────────
77pub use inspector::Inspector;
78pub use hierarchy::HierarchyPanel;
79pub use console::DevConsole;
80pub use gizmos::GizmoRenderer;
81
82// ─────────────────────────────────────────────────────────────────────────────
83// Primitive identifiers (mirrored here so editor doesn't depend on internals)
84// ─────────────────────────────────────────────────────────────────────────────
85
86/// Opaque handle to a scene entity.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
88pub struct EntityId(pub u32);
89
90/// Opaque handle to a glyph.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
92pub struct GlyphId(pub u32);
93
94// ─────────────────────────────────────────────────────────────────────────────
95// EditorMode
96// ─────────────────────────────────────────────────────────────────────────────
97
98/// The current operational mode of the editor.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum EditorMode {
101    /// Scene is running at full speed.
102    Play,
103    /// Scene is frozen; user edits geometry / properties.
104    Edit,
105    /// Scene is running at reduced speed or frame-by-frame.
106    Pause,
107}
108
109impl Default for EditorMode {
110    fn default() -> Self {
111        EditorMode::Edit
112    }
113}
114
115impl std::fmt::Display for EditorMode {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        match self {
118            EditorMode::Play => write!(f, "PLAY"),
119            EditorMode::Edit => write!(f, "EDIT"),
120            EditorMode::Pause => write!(f, "PAUSE"),
121        }
122    }
123}
124
125// ─────────────────────────────────────────────────────────────────────────────
126// EditorConfig
127// ─────────────────────────────────────────────────────────────────────────────
128
129/// Persistent editor preferences.
130#[derive(Debug, Clone)]
131pub struct EditorConfig {
132    pub font_size: f32,
133    pub theme: EditorTheme,
134    pub show_gizmos: bool,
135    pub show_grid: bool,
136    pub snap_enabled: bool,
137    pub snap_size: f32,
138    pub auto_save_interval_secs: f32,
139    pub max_undo_history: usize,
140    pub panel_opacity: f32,
141}
142
143impl Default for EditorConfig {
144    fn default() -> Self {
145        Self {
146            font_size: 14.0,
147            theme: EditorTheme::Dark,
148            show_gizmos: true,
149            show_grid: true,
150            snap_enabled: false,
151            snap_size: 0.5,
152            auto_save_interval_secs: 60.0,
153            max_undo_history: 200,
154            panel_opacity: 0.92,
155        }
156    }
157}
158
159/// Visual colour scheme for the editor UI.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum EditorTheme {
162    Dark,
163    Light,
164    HighContrast,
165    Solarized,
166}
167
168impl EditorTheme {
169    /// Returns (background, foreground, accent) as Vec4 RGBA.
170    pub fn colors(self) -> (Vec4, Vec4, Vec4) {
171        match self {
172            EditorTheme::Dark => (
173                Vec4::new(0.10, 0.10, 0.12, 1.0),
174                Vec4::new(0.90, 0.90, 0.90, 1.0),
175                Vec4::new(0.25, 0.55, 1.00, 1.0),
176            ),
177            EditorTheme::Light => (
178                Vec4::new(0.95, 0.95, 0.95, 1.0),
179                Vec4::new(0.05, 0.05, 0.05, 1.0),
180                Vec4::new(0.10, 0.40, 0.85, 1.0),
181            ),
182            EditorTheme::HighContrast => (
183                Vec4::new(0.00, 0.00, 0.00, 1.0),
184                Vec4::new(1.00, 1.00, 1.00, 1.0),
185                Vec4::new(1.00, 1.00, 0.00, 1.0),
186            ),
187            EditorTheme::Solarized => (
188                Vec4::new(0.00, 0.17, 0.21, 1.0),
189                Vec4::new(0.63, 0.63, 0.60, 1.0),
190                Vec4::new(0.52, 0.60, 0.00, 1.0),
191            ),
192        }
193    }
194}
195
196// ─────────────────────────────────────────────────────────────────────────────
197// EditorCamera
198// ─────────────────────────────────────────────────────────────────────────────
199
200/// Free-fly camera that is completely independent of the game camera.
201/// Controlled by WASD + mouse look in the editor viewport.
202#[derive(Debug, Clone)]
203pub struct EditorCamera {
204    pub position: Vec3,
205    pub yaw: f32,   // radians, horizontal rotation
206    pub pitch: f32, // radians, vertical rotation
207    pub move_speed: f32,
208    pub look_sensitivity: f32,
209    pub fov_degrees: f32,
210    pub near: f32,
211    pub far: f32,
212    /// Whether the camera is currently being piloted (right-mouse held).
213    pub active: bool,
214}
215
216impl Default for EditorCamera {
217    fn default() -> Self {
218        Self {
219            position: Vec3::new(0.0, 5.0, 10.0),
220            yaw: 0.0,
221            pitch: -0.3,
222            move_speed: 5.0,
223            look_sensitivity: 0.003,
224            fov_degrees: 60.0,
225            near: 0.1,
226            far: 1000.0,
227            active: false,
228        }
229    }
230}
231
232impl EditorCamera {
233    pub fn new() -> Self {
234        Self::default()
235    }
236
237    /// Direction the camera is currently facing.
238    pub fn forward(&self) -> Vec3 {
239        Vec3::new(
240            self.yaw.cos() * self.pitch.cos(),
241            self.pitch.sin(),
242            self.yaw.sin() * self.pitch.cos(),
243        )
244        .normalize()
245    }
246
247    /// Right vector perpendicular to forward and world-up.
248    pub fn right(&self) -> Vec3 {
249        self.forward().cross(Vec3::Y).normalize()
250    }
251
252    /// Up vector.
253    pub fn up(&self) -> Vec3 {
254        self.right().cross(self.forward()).normalize()
255    }
256
257    /// Apply mouse delta to yaw/pitch.
258    pub fn rotate(&mut self, delta_x: f32, delta_y: f32) {
259        self.yaw += delta_x * self.look_sensitivity;
260        self.pitch -= delta_y * self.look_sensitivity;
261        self.pitch = self.pitch.clamp(-1.5, 1.5);
262    }
263
264    /// Move the camera in local space.  `input` is (right, up, forward) signed.
265    pub fn translate(&mut self, input: Vec3, dt: f32) {
266        let fwd = self.forward();
267        let right = self.right();
268        let up = self.up();
269        let speed = self.move_speed * dt;
270        self.position += right * input.x * speed;
271        self.position += up * input.y * speed;
272        self.position += fwd * input.z * speed;
273    }
274
275    /// Build a view matrix (row-major, compatible with glam).
276    pub fn view_matrix(&self) -> glam::Mat4 {
277        let target = self.position + self.forward();
278        glam::Mat4::look_at_rh(self.position, target, Vec3::Y)
279    }
280
281    /// Build a projection matrix.
282    pub fn projection_matrix(&self, aspect: f32) -> glam::Mat4 {
283        glam::Mat4::perspective_rh_gl(
284            self.fov_degrees.to_radians(),
285            aspect,
286            self.near,
287            self.far,
288        )
289    }
290
291    /// Focus the camera on a world-space point.
292    pub fn focus_on(&mut self, target: Vec3) {
293        let offset = Vec3::new(3.0, 3.0, 5.0);
294        self.position = target + offset;
295        let dir = (target - self.position).normalize();
296        self.pitch = dir.y.asin();
297        self.yaw = dir.z.atan2(dir.x);
298    }
299
300    /// Orbit around a pivot point by delta angles.
301    pub fn orbit(&mut self, pivot: Vec3, d_yaw: f32, d_pitch: f32) {
302        let dist = (self.position - pivot).length();
303        self.yaw += d_yaw;
304        self.pitch = (self.pitch + d_pitch).clamp(-1.5, 1.5);
305        self.position = pivot - self.forward() * dist;
306    }
307}
308
309// ─────────────────────────────────────────────────────────────────────────────
310// SelectionSet
311// ─────────────────────────────────────────────────────────────────────────────
312
313/// Tracks which entities / glyphs are currently selected in the editor.
314#[derive(Debug, Clone, Default)]
315pub struct SelectionSet {
316    pub entities: Vec<EntityId>,
317    pub glyphs: Vec<GlyphId>,
318}
319
320impl SelectionSet {
321    pub fn new() -> Self {
322        Self::default()
323    }
324
325    pub fn is_empty(&self) -> bool {
326        self.entities.is_empty() && self.glyphs.is_empty()
327    }
328
329    pub fn clear(&mut self) {
330        self.entities.clear();
331        self.glyphs.clear();
332    }
333
334    /// Select a single entity (replaces current selection).
335    pub fn select_entity(&mut self, id: EntityId) {
336        self.clear();
337        self.entities.push(id);
338    }
339
340    /// Toggle an entity in/out of the selection (Ctrl+click).
341    pub fn toggle_entity(&mut self, id: EntityId) {
342        if let Some(pos) = self.entities.iter().position(|&e| e == id) {
343            self.entities.remove(pos);
344        } else {
345            self.entities.push(id);
346        }
347    }
348
349    /// Add a range of entity ids (Shift+click).
350    pub fn add_entity_range(&mut self, ids: &[EntityId]) {
351        for &id in ids {
352            if !self.entities.contains(&id) {
353                self.entities.push(id);
354            }
355        }
356    }
357
358    /// Select a single glyph.
359    pub fn select_glyph(&mut self, id: GlyphId) {
360        self.clear();
361        self.glyphs.push(id);
362    }
363
364    /// Toggle a glyph.
365    pub fn toggle_glyph(&mut self, id: GlyphId) {
366        if let Some(pos) = self.glyphs.iter().position(|&g| g == id) {
367            self.glyphs.remove(pos);
368        } else {
369            self.glyphs.push(id);
370        }
371    }
372
373    /// Returns the primary selected entity (first in list), if any.
374    pub fn primary_entity(&self) -> Option<EntityId> {
375        self.entities.first().copied()
376    }
377
378    /// Returns the primary selected glyph (first in list), if any.
379    pub fn primary_glyph(&self) -> Option<GlyphId> {
380        self.glyphs.first().copied()
381    }
382
383    /// Total number of selected items.
384    pub fn count(&self) -> usize {
385        self.entities.len() + self.glyphs.len()
386    }
387}
388
389// ─────────────────────────────────────────────────────────────────────────────
390// UndoHistory — command pattern
391// ─────────────────────────────────────────────────────────────────────────────
392
393/// Trait that every undoable editor command must implement.
394pub trait EditorCommand: std::fmt::Debug + Send + Sync {
395    /// Human-readable name for display in the undo stack.
396    fn name(&self) -> &str;
397    /// Apply / re-apply the command.
398    fn execute(&mut self, state: &mut EditorState);
399    /// Reverse the command.
400    fn undo(&mut self, state: &mut EditorState);
401}
402
403// ── Concrete commands ─────────────────────────────────────────────────────────
404
405/// Move one or more entities by a delta vector.
406#[derive(Debug)]
407pub struct MoveEntityCommand {
408    pub entity_ids: Vec<EntityId>,
409    pub delta: Vec3,
410}
411
412impl EditorCommand for MoveEntityCommand {
413    fn name(&self) -> &str {
414        "Move Entity"
415    }
416    fn execute(&mut self, state: &mut EditorState) {
417        for &id in &self.entity_ids {
418            if let Some(pos) = state.entity_positions.get_mut(&id) {
419                *pos += self.delta;
420            }
421        }
422    }
423    fn undo(&mut self, state: &mut EditorState) {
424        for &id in &self.entity_ids {
425            if let Some(pos) = state.entity_positions.get_mut(&id) {
426                *pos -= self.delta;
427            }
428        }
429    }
430}
431
432/// Spawn an entity at a given position.
433#[derive(Debug)]
434pub struct SpawnEntityCommand {
435    pub id: EntityId,
436    pub position: Vec3,
437    pub name: String,
438    pub executed: bool,
439}
440
441impl EditorCommand for SpawnEntityCommand {
442    fn name(&self) -> &str {
443        "Spawn Entity"
444    }
445    fn execute(&mut self, state: &mut EditorState) {
446        state.entity_positions.insert(self.id, self.position);
447        state.entity_names.insert(self.id, self.name.clone());
448        self.executed = true;
449    }
450    fn undo(&mut self, state: &mut EditorState) {
451        state.entity_positions.remove(&self.id);
452        state.entity_names.remove(&self.id);
453        self.executed = false;
454    }
455}
456
457/// Delete an entity from the scene.
458#[derive(Debug)]
459pub struct DeleteEntityCommand {
460    pub id: EntityId,
461    pub saved_position: Option<Vec3>,
462    pub saved_name: Option<String>,
463}
464
465impl EditorCommand for DeleteEntityCommand {
466    fn name(&self) -> &str {
467        "Delete Entity"
468    }
469    fn execute(&mut self, state: &mut EditorState) {
470        self.saved_position = state.entity_positions.remove(&self.id);
471        self.saved_name = state.entity_names.remove(&self.id);
472        state.selection.entities.retain(|&e| e != self.id);
473    }
474    fn undo(&mut self, state: &mut EditorState) {
475        if let Some(pos) = self.saved_position {
476            state.entity_positions.insert(self.id, pos);
477        }
478        if let Some(ref name) = self.saved_name {
479            state.entity_names.insert(self.id, name.clone());
480        }
481    }
482}
483
484/// Set a named string property on an entity.
485#[derive(Debug)]
486pub struct SetPropertyCommand {
487    pub entity_id: EntityId,
488    pub property: String,
489    pub old_value: String,
490    pub new_value: String,
491}
492
493impl EditorCommand for SetPropertyCommand {
494    fn name(&self) -> &str {
495        "Set Property"
496    }
497    fn execute(&mut self, state: &mut EditorState) {
498        state
499            .entity_properties
500            .entry(self.entity_id)
501            .or_default()
502            .insert(self.property.clone(), self.new_value.clone());
503    }
504    fn undo(&mut self, state: &mut EditorState) {
505        state
506            .entity_properties
507            .entry(self.entity_id)
508            .or_default()
509            .insert(self.property.clone(), self.old_value.clone());
510    }
511}
512
513/// Group multiple entities together under a shared label.
514#[derive(Debug)]
515pub struct GroupSelectionCommand {
516    pub group_id: EntityId,
517    pub members: Vec<EntityId>,
518    pub group_name: String,
519    pub executed: bool,
520}
521
522impl EditorCommand for GroupSelectionCommand {
523    fn name(&self) -> &str {
524        "Group Selection"
525    }
526    fn execute(&mut self, state: &mut EditorState) {
527        state
528            .entity_names
529            .insert(self.group_id, self.group_name.clone());
530        state
531            .entity_groups
532            .insert(self.group_id, self.members.clone());
533        self.executed = true;
534    }
535    fn undo(&mut self, state: &mut EditorState) {
536        state.entity_names.remove(&self.group_id);
537        state.entity_groups.remove(&self.group_id);
538        self.executed = false;
539    }
540}
541
542// ── UndoHistory ───────────────────────────────────────────────────────────────
543
544/// Ring-buffer undo/redo stack (max 200 entries).
545pub struct UndoHistory {
546    commands: Vec<Box<dyn EditorCommand>>,
547    cursor: usize, // points one past the last executed command
548    max_size: usize,
549}
550
551impl std::fmt::Debug for UndoHistory {
552    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
553        f.debug_struct("UndoHistory")
554            .field("cursor", &self.cursor)
555            .field("len", &self.commands.len())
556            .finish()
557    }
558}
559
560impl UndoHistory {
561    pub fn new(max_size: usize) -> Self {
562        Self {
563            commands: Vec::with_capacity(max_size),
564            cursor: 0,
565            max_size,
566        }
567    }
568
569    /// Execute a command and push it onto the history.
570    pub fn execute(&mut self, mut cmd: Box<dyn EditorCommand>, state: &mut EditorState) {
571        // Drop any redo-able future if we branch from the middle.
572        if self.cursor < self.commands.len() {
573            self.commands.truncate(self.cursor);
574        }
575        cmd.execute(state);
576        self.commands.push(cmd);
577        // Evict oldest if over capacity.
578        if self.commands.len() > self.max_size {
579            self.commands.remove(0);
580        } else {
581            self.cursor += 1;
582        }
583    }
584
585    /// Undo the last command.
586    pub fn undo(&mut self, state: &mut EditorState) -> bool {
587        if self.cursor == 0 {
588            return false;
589        }
590        self.cursor -= 1;
591        self.commands[self.cursor].undo(state);
592        true
593    }
594
595    /// Redo the next undone command.
596    pub fn redo(&mut self, state: &mut EditorState) -> bool {
597        if self.cursor >= self.commands.len() {
598            return false;
599        }
600        self.commands[self.cursor].execute(state);
601        self.cursor += 1;
602        true
603    }
604
605    pub fn can_undo(&self) -> bool {
606        self.cursor > 0
607    }
608
609    pub fn can_redo(&self) -> bool {
610        self.cursor < self.commands.len()
611    }
612
613    pub fn clear(&mut self) {
614        self.commands.clear();
615        self.cursor = 0;
616    }
617
618    pub fn len(&self) -> usize {
619        self.commands.len()
620    }
621
622    pub fn is_empty(&self) -> bool {
623        self.commands.is_empty()
624    }
625
626    /// Returns the names of the last N commands (most recent first).
627    pub fn recent_names(&self, n: usize) -> Vec<&str> {
628        let start = if self.cursor > n { self.cursor - n } else { 0 };
629        self.commands[start..self.cursor]
630            .iter()
631            .rev()
632            .map(|c| c.name())
633            .collect()
634    }
635}
636
637// ─────────────────────────────────────────────────────────────────────────────
638// EditorLayout — dockable panels
639// ─────────────────────────────────────────────────────────────────────────────
640
641/// Which editor panels are currently visible and their pixel bounds.
642#[derive(Debug, Clone)]
643pub struct PanelRect {
644    pub x: f32,
645    pub y: f32,
646    pub width: f32,
647    pub height: f32,
648    pub visible: bool,
649    pub collapsed: bool,
650}
651
652impl PanelRect {
653    pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
654        Self { x, y, width, height, visible: true, collapsed: false }
655    }
656    pub fn contains(&self, px: f32, py: f32) -> bool {
657        px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
658    }
659}
660
661#[derive(Debug, Clone)]
662pub struct EditorLayout {
663    pub hierarchy: PanelRect,
664    pub inspector: PanelRect,
665    pub console: PanelRect,
666    pub viewport: PanelRect,
667    pub toolbar: PanelRect,
668    pub asset_browser: PanelRect,
669}
670
671impl Default for EditorLayout {
672    fn default() -> Self {
673        Self {
674            hierarchy:     PanelRect::new(0.0,   20.0, 200.0, 600.0),
675            inspector:     PanelRect::new(1080.0, 20.0, 220.0, 700.0),
676            console:       PanelRect::new(0.0,   620.0, 1300.0, 200.0),
677            viewport:      PanelRect::new(200.0,  20.0, 880.0, 600.0),
678            toolbar:       PanelRect::new(0.0,   0.0,  1300.0,  20.0),
679            asset_browser: PanelRect::new(200.0, 620.0, 880.0, 200.0),
680        }
681    }
682}
683
684impl EditorLayout {
685    pub fn toggle_hierarchy(&mut self) {
686        self.hierarchy.visible = !self.hierarchy.visible;
687    }
688    pub fn toggle_inspector(&mut self) {
689        self.inspector.visible = !self.inspector.visible;
690    }
691    pub fn toggle_console(&mut self) {
692        self.console.visible = !self.console.visible;
693    }
694    pub fn reset_to_default(&mut self) {
695        *self = Self::default();
696    }
697    /// Move a panel by delta pixels (drag).
698    pub fn drag_panel(&mut self, panel: PanelId, dx: f32, dy: f32) {
699        let rect = match panel {
700            PanelId::Hierarchy => &mut self.hierarchy,
701            PanelId::Inspector => &mut self.inspector,
702            PanelId::Console   => &mut self.console,
703            PanelId::Viewport  => &mut self.viewport,
704            PanelId::Toolbar   => &mut self.toolbar,
705            PanelId::AssetBrowser => &mut self.asset_browser,
706        };
707        rect.x += dx;
708        rect.y += dy;
709    }
710    /// Resize a panel.
711    pub fn resize_panel(&mut self, panel: PanelId, dw: f32, dh: f32) {
712        let rect = match panel {
713            PanelId::Hierarchy => &mut self.hierarchy,
714            PanelId::Inspector => &mut self.inspector,
715            PanelId::Console   => &mut self.console,
716            PanelId::Viewport  => &mut self.viewport,
717            PanelId::Toolbar   => &mut self.toolbar,
718            PanelId::AssetBrowser => &mut self.asset_browser,
719        };
720        rect.width  = (rect.width  + dw).max(50.0);
721        rect.height = (rect.height + dh).max(30.0);
722    }
723}
724
725#[derive(Debug, Clone, Copy, PartialEq, Eq)]
726pub enum PanelId {
727    Hierarchy,
728    Inspector,
729    Console,
730    Viewport,
731    Toolbar,
732    AssetBrowser,
733}
734
735// ─────────────────────────────────────────────────────────────────────────────
736// Shortcut registry
737// ─────────────────────────────────────────────────────────────────────────────
738
739/// A keyboard shortcut (key + modifiers).
740#[derive(Debug, Clone, PartialEq, Eq, Hash)]
741pub struct Shortcut {
742    pub key: char,
743    pub ctrl: bool,
744    pub shift: bool,
745    pub alt: bool,
746}
747
748impl Shortcut {
749    pub fn key(key: char) -> Self {
750        Self { key, ctrl: false, shift: false, alt: false }
751    }
752    pub fn ctrl(key: char) -> Self {
753        Self { key, ctrl: true, shift: false, alt: false }
754    }
755    pub fn ctrl_shift(key: char) -> Self {
756        Self { key, ctrl: true, shift: true, alt: false }
757    }
758}
759
760/// Action the shortcut should trigger.
761#[derive(Debug, Clone, PartialEq, Eq, Hash)]
762pub enum EditorAction {
763    Undo,
764    Redo,
765    Save,
766    SaveAs,
767    Open,
768    New,
769    Delete,
770    Duplicate,
771    SelectAll,
772    DeselectAll,
773    TogglePlay,
774    TogglePause,
775    FocusSelected,
776    ToggleHierarchy,
777    ToggleInspector,
778    ToggleConsole,
779    GizmoTranslate,
780    GizmoRotate,
781    GizmoScale,
782    GizmoUniversal,
783    SnapToggle,
784    AxisX,
785    AxisY,
786    AxisZ,
787    Screenshot,
788    Custom(String),
789}
790
791/// Maps shortcuts to editor actions.
792#[derive(Debug, Clone, Default)]
793pub struct ShortcutRegistry {
794    bindings: HashMap<Shortcut, EditorAction>,
795}
796
797impl ShortcutRegistry {
798    pub fn new() -> Self {
799        let mut reg = Self::default();
800        reg.register_defaults();
801        reg
802    }
803
804    fn register_defaults(&mut self) {
805        self.bind(Shortcut::ctrl('z'), EditorAction::Undo);
806        self.bind(Shortcut::ctrl('y'), EditorAction::Redo);
807        self.bind(Shortcut::ctrl('s'), EditorAction::Save);
808        self.bind(Shortcut::ctrl_shift('s'), EditorAction::SaveAs);
809        self.bind(Shortcut::ctrl('o'), EditorAction::Open);
810        self.bind(Shortcut::ctrl('n'), EditorAction::New);
811        self.bind(Shortcut::key('\x7f'), EditorAction::Delete); // Delete key
812        self.bind(Shortcut::ctrl('d'), EditorAction::Duplicate);
813        self.bind(Shortcut::ctrl('a'), EditorAction::SelectAll);
814        self.bind(Shortcut::key('g'), EditorAction::GizmoTranslate);
815        self.bind(Shortcut::key('r'), EditorAction::GizmoRotate);
816        self.bind(Shortcut::key('s'), EditorAction::GizmoScale);
817        self.bind(Shortcut::key('x'), EditorAction::AxisX);
818        self.bind(Shortcut::key('y'), EditorAction::AxisY);
819        self.bind(Shortcut::key('z'), EditorAction::AxisZ);
820        self.bind(Shortcut::key('f'), EditorAction::FocusSelected);
821        self.bind(Shortcut::key(' '), EditorAction::TogglePlay);
822    }
823
824    pub fn bind(&mut self, shortcut: Shortcut, action: EditorAction) {
825        self.bindings.insert(shortcut, action);
826    }
827
828    pub fn unbind(&mut self, shortcut: &Shortcut) {
829        self.bindings.remove(shortcut);
830    }
831
832    pub fn resolve(&self, shortcut: &Shortcut) -> Option<&EditorAction> {
833        self.bindings.get(shortcut)
834    }
835
836    pub fn all_bindings(&self) -> &HashMap<Shortcut, EditorAction> {
837        &self.bindings
838    }
839
840    /// Find the shortcut bound to a given action (first match).
841    pub fn shortcut_for(&self, action: &EditorAction) -> Option<&Shortcut> {
842        self.bindings.iter().find_map(|(k, v)| if v == action { Some(k) } else { None })
843    }
844}
845
846// ─────────────────────────────────────────────────────────────────────────────
847// EditorStats
848// ─────────────────────────────────────────────────────────────────────────────
849
850/// Live performance / scene statistics displayed in the editor overlay.
851#[derive(Debug, Clone, Default)]
852pub struct EditorStats {
853    pub fps: f32,
854    pub frame_time_ms: f32,
855    pub draw_calls: u32,
856    pub entities: u32,
857    pub glyphs: u32,
858    pub particles: u32,
859    pub force_fields: u32,
860    pub memory_mb: f32,
861    pub gpu_memory_mb: f32,
862    pub triangles: u64,
863    /// Frame counter since engine start.
864    pub frame_index: u64,
865    /// Accumulated time since engine start.
866    pub elapsed_secs: f32,
867    /// Rolling average fps over last N frames.
868    fps_samples: Vec<f32>,
869    fps_idx: usize,
870}
871
872impl EditorStats {
873    pub fn new() -> Self {
874        Self::default()
875    }
876
877    /// Update with a new frame's delta time.
878    pub fn update(&mut self, dt: f32) {
879        let fps = if dt > 0.0 { 1.0 / dt } else { 0.0 };
880        let idx = self.fps_idx % 64;
881        if self.fps_samples.len() <= idx { self.fps_samples.resize(idx + 1, 0.0); }
882        self.fps_samples[idx] = fps;
883        self.fps_idx = self.fps_idx.wrapping_add(1);
884        let n = self.fps_samples.len() as f32;
885        self.fps = self.fps_samples.iter().sum::<f32>() / n.max(1.0);
886        self.frame_time_ms = dt * 1000.0;
887        self.frame_index += 1;
888        self.elapsed_secs += dt;
889    }
890
891    /// Render a compact stats string for the toolbar.
892    pub fn summary(&self) -> String {
893        format!(
894            "FPS:{:.0} | Frame:{:.2}ms | Entities:{} | Particles:{} | DC:{} | Mem:{:.1}MB",
895            self.fps,
896            self.frame_time_ms,
897            self.entities,
898            self.particles,
899            self.draw_calls,
900            self.memory_mb,
901        )
902    }
903}
904
905// ─────────────────────────────────────────────────────────────────────────────
906// GridRenderer
907// ─────────────────────────────────────────────────────────────────────────────
908
909/// Configuration and rendering logic for the infinite editor grid.
910#[derive(Debug, Clone)]
911pub struct GridRenderer {
912    pub enabled: bool,
913    pub cell_size: f32,
914    pub major_every: u32,   // draw a thicker line every N cells
915    pub color_minor: Vec4,
916    pub color_major: Vec4,
917    pub color_origin: Vec4,
918    pub fade_start: f32,    // distance from camera at which grid begins to fade
919    pub fade_end: f32,      // distance at which grid is fully transparent
920    pub y_plane: f32,       // world-space Y position of the ground plane
921}
922
923impl Default for GridRenderer {
924    fn default() -> Self {
925        Self {
926            enabled: true,
927            cell_size: 1.0,
928            major_every: 5,
929            color_minor:  Vec4::new(0.4, 0.4, 0.4, 0.35),
930            color_major:  Vec4::new(0.6, 0.6, 0.6, 0.60),
931            color_origin: Vec4::new(0.8, 0.8, 0.8, 0.90),
932            fade_start: 20.0,
933            fade_end: 60.0,
934            y_plane: 0.0,
935        }
936    }
937}
938
939impl GridRenderer {
940    pub fn new() -> Self {
941        Self::default()
942    }
943
944    /// Compute alpha fade factor at a given distance from the camera.
945    pub fn fade_alpha(&self, distance: f32) -> f32 {
946        if distance <= self.fade_start {
947            1.0
948        } else if distance >= self.fade_end {
949            0.0
950        } else {
951            1.0 - (distance - self.fade_start) / (self.fade_end - self.fade_start)
952        }
953    }
954
955    /// Returns a list of grid lines within the visible region.
956    /// Each line is (start: Vec3, end: Vec3, color: Vec4).
957    pub fn build_lines(
958        &self,
959        camera_pos: Vec3,
960        half_extent: f32,
961    ) -> Vec<(Vec3, Vec3, Vec4)> {
962        if !self.enabled {
963            return Vec::new();
964        }
965        let cs = self.cell_size;
966        let cx = (camera_pos.x / cs).floor() as i32;
967        let cz = (camera_pos.z / cs).floor() as i32;
968        let extent = (half_extent / cs).ceil() as i32 + 1;
969
970        let mut lines = Vec::new();
971        let y = self.y_plane;
972
973        for i in -extent..=extent {
974            let lx = (cx + i) as f32 * cs;
975            let lz = (cz + i) as f32 * cs;
976
977            let is_major_x = (cx + i).unsigned_abs() % self.major_every == 0;
978            let is_major_z = (cz + i).unsigned_abs() % self.major_every == 0;
979            let is_origin_x = cx + i == 0;
980            let is_origin_z = cz + i == 0;
981
982            let col_x = if is_origin_x {
983                self.color_origin
984            } else if is_major_x {
985                self.color_major
986            } else {
987                self.color_minor
988            };
989
990            let col_z = if is_origin_z {
991                self.color_origin
992            } else if is_major_z {
993                self.color_major
994            } else {
995                self.color_minor
996            };
997
998            let z0 = (cz - extent) as f32 * cs;
999            let z1 = (cz + extent) as f32 * cs;
1000            let x0 = (cx - extent) as f32 * cs;
1001            let x1 = (cx + extent) as f32 * cs;
1002
1003            let dist_x = (Vec2::new(lx, y) - Vec2::new(camera_pos.x, camera_pos.y)).length();
1004            let dist_z = (Vec2::new(lz, y) - Vec2::new(camera_pos.z, camera_pos.y)).length();
1005
1006            let alpha_x = self.fade_alpha(dist_x);
1007            let alpha_z = self.fade_alpha(dist_z);
1008
1009            let mut c_x = col_x;
1010            c_x.w *= alpha_x;
1011            let mut c_z = col_z;
1012            c_z.w *= alpha_z;
1013
1014            if c_x.w > 0.01 {
1015                lines.push((Vec3::new(lx, y, z0), Vec3::new(lx, y, z1), c_x));
1016            }
1017            if c_z.w > 0.01 {
1018                lines.push((Vec3::new(x0, y, lz), Vec3::new(x1, y, lz), c_z));
1019            }
1020        }
1021        lines
1022    }
1023
1024    /// Render grid as ASCII art for debug/console output.
1025    pub fn render_ascii(&self, width: usize, height: usize) -> String {
1026        let mut out = String::with_capacity(width * height);
1027        for row in 0..height {
1028            for col in 0..width {
1029                let on_major_h = row % (self.major_every as usize) == 0;
1030                let on_major_v = col % (self.major_every as usize) == 0;
1031                let ch = if on_major_h && on_major_v {
1032                    '+'
1033                } else if on_major_h {
1034                    '-'
1035                } else if on_major_v {
1036                    '|'
1037                } else {
1038                    ' '
1039                };
1040                out.push(ch);
1041            }
1042            out.push('\n');
1043        }
1044        out
1045    }
1046}
1047
1048// ─────────────────────────────────────────────────────────────────────────────
1049// EditorState — master struct
1050// ─────────────────────────────────────────────────────────────────────────────
1051
1052/// The top-level editor state.  Owns everything the editor needs to function.
1053pub struct EditorState {
1054    // ── Mode & config ─────────────────────────────────────────────────────────
1055    pub mode: EditorMode,
1056    pub config: EditorConfig,
1057
1058    // ── Camera ────────────────────────────────────────────────────────────────
1059    pub camera: EditorCamera,
1060
1061    // ── Selection ─────────────────────────────────────────────────────────────
1062    pub selection: SelectionSet,
1063
1064    // ── Undo/redo ─────────────────────────────────────────────────────────────
1065    pub undo_history: UndoHistory,
1066
1067    // ── Layout & panels ───────────────────────────────────────────────────────
1068    pub layout: EditorLayout,
1069    pub shortcuts: ShortcutRegistry,
1070
1071    // ── Stats ─────────────────────────────────────────────────────────────────
1072    pub stats: EditorStats,
1073
1074    // ── Grid ─────────────────────────────────────────────────────────────────
1075    pub grid: GridRenderer,
1076
1077    // ── Scene mirror (lightweight copies for editor logic) ────────────────────
1078    pub entity_positions:  HashMap<EntityId, Vec3>,
1079    pub entity_names:      HashMap<EntityId, String>,
1080    pub entity_properties: HashMap<EntityId, HashMap<String, String>>,
1081    pub entity_groups:     HashMap<EntityId, Vec<EntityId>>,
1082
1083    // ── Dirty flag ───────────────────────────────────────────────────────────
1084    pub dirty: bool,
1085    pub scene_path: Option<String>,
1086    pub next_entity_id: u32,
1087}
1088
1089impl std::fmt::Debug for EditorState {
1090    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1091        f.debug_struct("EditorState")
1092            .field("mode", &self.mode)
1093            .field("dirty", &self.dirty)
1094            .finish()
1095    }
1096}
1097
1098impl EditorState {
1099    pub fn new(config: EditorConfig) -> Self {
1100        let max_undo = config.max_undo_history;
1101        Self {
1102            mode: EditorMode::Edit,
1103            config,
1104            camera: EditorCamera::new(),
1105            selection: SelectionSet::new(),
1106            undo_history: UndoHistory::new(max_undo),
1107            layout: EditorLayout::default(),
1108            shortcuts: ShortcutRegistry::new(),
1109            stats: EditorStats::new(),
1110            grid: GridRenderer::new(),
1111            entity_positions:  HashMap::new(),
1112            entity_names:      HashMap::new(),
1113            entity_properties: HashMap::new(),
1114            entity_groups:     HashMap::new(),
1115            dirty: false,
1116            scene_path: None,
1117            next_entity_id: 1,
1118        }
1119    }
1120
1121    /// Allocate a fresh EntityId.
1122    pub fn alloc_entity_id(&mut self) -> EntityId {
1123        let id = EntityId(self.next_entity_id);
1124        self.next_entity_id += 1;
1125        id
1126    }
1127
1128    /// Switch the editor mode.
1129    pub fn set_mode(&mut self, mode: EditorMode) {
1130        self.mode = mode;
1131    }
1132
1133    /// Tick the editor (update stats, process any deferred logic).
1134    pub fn tick(&mut self, dt: f32) {
1135        self.stats.update(dt);
1136    }
1137
1138    /// Execute an undoable command.
1139    pub fn do_command(&mut self, cmd: Box<dyn EditorCommand>) {
1140        // We need to temporarily separate self from undo_history to satisfy borrow rules.
1141        // Use an unsafe swap approach via pointer — safe here because EditorState is not
1142        // re-entrant and we own both fields.
1143        let mut history = std::mem::replace(
1144            &mut self.undo_history,
1145            UndoHistory::new(0),
1146        );
1147        history.execute(cmd, self);
1148        self.undo_history = history;
1149        self.dirty = true;
1150    }
1151
1152    pub fn undo(&mut self) -> bool {
1153        let mut history = std::mem::replace(&mut self.undo_history, UndoHistory::new(0));
1154        let result = history.undo(self);
1155        self.undo_history = history;
1156        result
1157    }
1158
1159    pub fn redo(&mut self) -> bool {
1160        let mut history = std::mem::replace(&mut self.undo_history, UndoHistory::new(0));
1161        let result = history.redo(self);
1162        self.undo_history = history;
1163        result
1164    }
1165
1166    /// Process a keyboard shortcut.
1167    pub fn handle_shortcut(&mut self, shortcut: &Shortcut) -> Option<EditorAction> {
1168        let action = self.shortcuts.resolve(shortcut).cloned()?;
1169        match &action {
1170            EditorAction::Undo => { self.undo(); }
1171            EditorAction::Redo => { self.redo(); }
1172            EditorAction::TogglePlay => {
1173                self.mode = match self.mode {
1174                    EditorMode::Play  => EditorMode::Edit,
1175                    EditorMode::Edit  => EditorMode::Play,
1176                    EditorMode::Pause => EditorMode::Play,
1177                };
1178            }
1179            EditorAction::TogglePause => {
1180                self.mode = match self.mode {
1181                    EditorMode::Play  => EditorMode::Pause,
1182                    EditorMode::Pause => EditorMode::Play,
1183                    EditorMode::Edit  => EditorMode::Edit,
1184                };
1185            }
1186            EditorAction::ToggleHierarchy => self.layout.toggle_hierarchy(),
1187            EditorAction::ToggleInspector => self.layout.toggle_inspector(),
1188            EditorAction::ToggleConsole   => self.layout.toggle_console(),
1189            EditorAction::FocusSelected => {
1190                if let Some(id) = self.selection.primary_entity() {
1191                    if let Some(&pos) = self.entity_positions.get(&id) {
1192                        self.camera.focus_on(pos);
1193                    }
1194                }
1195            }
1196            EditorAction::SnapToggle => {
1197                self.config.snap_enabled = !self.config.snap_enabled;
1198            }
1199            _ => {}
1200        }
1201        Some(action)
1202    }
1203
1204    /// Snap a world-space position to the snap grid.
1205    pub fn snap_position(&self, pos: Vec3) -> Vec3 {
1206        if !self.config.snap_enabled {
1207            return pos;
1208        }
1209        let s = self.config.snap_size;
1210        Vec3::new(
1211            (pos.x / s).round() * s,
1212            (pos.y / s).round() * s,
1213            (pos.z / s).round() * s,
1214        )
1215    }
1216}
1217
1218// ─────────────────────────────────────────────────────────────────────────────
1219// Tests
1220// ─────────────────────────────────────────────────────────────────────────────
1221
1222#[cfg(test)]
1223mod tests {
1224    use super::*;
1225
1226    fn make_state() -> EditorState {
1227        EditorState::new(EditorConfig::default())
1228    }
1229
1230    #[test]
1231    fn test_editor_mode_default() {
1232        let state = make_state();
1233        assert_eq!(state.mode, EditorMode::Edit);
1234    }
1235
1236    #[test]
1237    fn test_alloc_entity_id() {
1238        let mut state = make_state();
1239        let a = state.alloc_entity_id();
1240        let b = state.alloc_entity_id();
1241        assert_ne!(a, b);
1242    }
1243
1244    #[test]
1245    fn test_undo_redo() {
1246        let mut state = make_state();
1247        let id = state.alloc_entity_id();
1248        let cmd = Box::new(SpawnEntityCommand {
1249            id,
1250            position: Vec3::ZERO,
1251            name: "test".into(),
1252            executed: false,
1253        });
1254        state.do_command(cmd);
1255        assert!(state.entity_positions.contains_key(&id));
1256        state.undo();
1257        assert!(!state.entity_positions.contains_key(&id));
1258        state.redo();
1259        assert!(state.entity_positions.contains_key(&id));
1260    }
1261
1262    #[test]
1263    fn test_move_entity_undo() {
1264        let mut state = make_state();
1265        let id = state.alloc_entity_id();
1266        state.entity_positions.insert(id, Vec3::ZERO);
1267        let cmd = Box::new(MoveEntityCommand {
1268            entity_ids: vec![id],
1269            delta: Vec3::new(1.0, 0.0, 0.0),
1270        });
1271        state.do_command(cmd);
1272        assert_eq!(state.entity_positions[&id], Vec3::new(1.0, 0.0, 0.0));
1273        state.undo();
1274        assert_eq!(state.entity_positions[&id], Vec3::ZERO);
1275    }
1276
1277    #[test]
1278    fn test_selection_set() {
1279        let mut sel = SelectionSet::new();
1280        let a = EntityId(1);
1281        let b = EntityId(2);
1282        sel.select_entity(a);
1283        assert_eq!(sel.count(), 1);
1284        sel.toggle_entity(b);
1285        assert_eq!(sel.count(), 2);
1286        sel.toggle_entity(a);
1287        assert_eq!(sel.count(), 1);
1288        assert_eq!(sel.primary_entity(), Some(b));
1289    }
1290
1291    #[test]
1292    fn test_camera_forward() {
1293        let cam = EditorCamera::default();
1294        let fwd = cam.forward();
1295        assert!((fwd.length() - 1.0).abs() < 1e-5);
1296    }
1297
1298    #[test]
1299    fn test_camera_focus_on() {
1300        let mut cam = EditorCamera::default();
1301        let target = Vec3::new(5.0, 0.0, 5.0);
1302        cam.focus_on(target);
1303        let d = (cam.position - target).length();
1304        assert!(d > 0.5, "camera should not be at target");
1305    }
1306
1307    #[test]
1308    fn test_grid_fade_alpha() {
1309        let grid = GridRenderer::default();
1310        assert!((grid.fade_alpha(0.0) - 1.0).abs() < 1e-6);
1311        assert!((grid.fade_alpha(100.0)).abs() < 1e-6);
1312        let mid = grid.fade_alpha((grid.fade_start + grid.fade_end) / 2.0);
1313        assert!(mid > 0.0 && mid < 1.0);
1314    }
1315
1316    #[test]
1317    fn test_grid_build_lines() {
1318        let grid = GridRenderer::default();
1319        let lines = grid.build_lines(Vec3::ZERO, 10.0);
1320        assert!(!lines.is_empty());
1321    }
1322
1323    #[test]
1324    fn test_shortcut_registry() {
1325        let reg = ShortcutRegistry::new();
1326        let sc = Shortcut::ctrl('z');
1327        assert_eq!(reg.resolve(&sc), Some(&EditorAction::Undo));
1328    }
1329
1330    #[test]
1331    fn test_snap_position() {
1332        let mut state = make_state();
1333        state.config.snap_enabled = true;
1334        state.config.snap_size = 1.0;
1335        let snapped = state.snap_position(Vec3::new(0.7, 1.3, -0.4));
1336        assert_eq!(snapped, Vec3::new(1.0, 1.0, 0.0));
1337    }
1338
1339    #[test]
1340    fn test_stats_update() {
1341        let mut stats = EditorStats::new();
1342        stats.update(0.016);
1343        assert!(stats.fps > 0.0);
1344        assert!(stats.frame_index == 1);
1345    }
1346
1347    #[test]
1348    fn test_undo_history_ring_buffer() {
1349        let mut state = make_state();
1350        // Overflow the ring buffer
1351        for i in 0..210u32 {
1352            let id = EntityId(i);
1353            state.entity_positions.insert(id, Vec3::ZERO);
1354            let cmd = Box::new(MoveEntityCommand {
1355                entity_ids: vec![id],
1356                delta: Vec3::new(1.0, 0.0, 0.0),
1357            });
1358            state.do_command(cmd);
1359        }
1360        assert!(state.undo_history.len() <= state.config.max_undo_history);
1361    }
1362
1363    #[test]
1364    fn test_delete_entity_undo() {
1365        let mut state = make_state();
1366        let id = state.alloc_entity_id();
1367        state.entity_positions.insert(id, Vec3::new(1.0, 2.0, 3.0));
1368        state.entity_names.insert(id, "hero".into());
1369        let cmd = Box::new(DeleteEntityCommand {
1370            id,
1371            saved_position: None,
1372            saved_name: None,
1373        });
1374        state.do_command(cmd);
1375        assert!(!state.entity_positions.contains_key(&id));
1376        state.undo();
1377        assert!(state.entity_positions.contains_key(&id));
1378    }
1379
1380    #[test]
1381    fn test_editor_layout_toggle() {
1382        let mut layout = EditorLayout::default();
1383        assert!(layout.hierarchy.visible);
1384        layout.toggle_hierarchy();
1385        assert!(!layout.hierarchy.visible);
1386        layout.toggle_hierarchy();
1387        assert!(layout.hierarchy.visible);
1388    }
1389}