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