1pub 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
76pub use inspector::Inspector;
78pub use hierarchy::HierarchyPanel;
79pub use console::DevConsole;
80pub use gizmos::GizmoRenderer;
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
88pub struct EntityId(pub u32);
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
92pub struct GlyphId(pub u32);
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum EditorMode {
101 Play,
103 Edit,
105 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum EditorTheme {
162 Dark,
163 Light,
164 HighContrast,
165 Solarized,
166}
167
168impl EditorTheme {
169 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#[derive(Debug, Clone)]
203pub struct EditorCamera {
204 pub position: Vec3,
205 pub yaw: f32, pub pitch: f32, pub move_speed: f32,
208 pub look_sensitivity: f32,
209 pub fov_degrees: f32,
210 pub near: f32,
211 pub far: f32,
212 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 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 pub fn right(&self) -> Vec3 {
249 self.forward().cross(Vec3::Y).normalize()
250 }
251
252 pub fn up(&self) -> Vec3 {
254 self.right().cross(self.forward()).normalize()
255 }
256
257 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 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 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 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 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 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#[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 pub fn select_entity(&mut self, id: EntityId) {
336 self.clear();
337 self.entities.push(id);
338 }
339
340 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 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 pub fn select_glyph(&mut self, id: GlyphId) {
360 self.clear();
361 self.glyphs.push(id);
362 }
363
364 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 pub fn primary_entity(&self) -> Option<EntityId> {
375 self.entities.first().copied()
376 }
377
378 pub fn primary_glyph(&self) -> Option<GlyphId> {
380 self.glyphs.first().copied()
381 }
382
383 pub fn count(&self) -> usize {
385 self.entities.len() + self.glyphs.len()
386 }
387}
388
389pub trait EditorCommand: std::fmt::Debug + Send + Sync {
395 fn name(&self) -> &str;
397 fn execute(&mut self, state: &mut EditorState);
399 fn undo(&mut self, state: &mut EditorState);
401}
402
403#[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#[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#[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#[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#[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
542pub struct UndoHistory {
546 commands: Vec<Box<dyn EditorCommand>>,
547 cursor: usize, 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 pub fn execute(&mut self, mut cmd: Box<dyn EditorCommand>, state: &mut EditorState) {
571 if self.cursor < self.commands.len() {
573 self.commands.truncate(self.cursor);
574 }
575 cmd.execute(state);
576 self.commands.push(cmd);
577 if self.commands.len() > self.max_size {
579 self.commands.remove(0);
580 } else {
581 self.cursor += 1;
582 }
583 }
584
585 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 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 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#[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 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 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#[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#[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#[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); 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 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#[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 pub frame_index: u64,
865 pub elapsed_secs: f32,
867 fps_samples: Vec<f32>,
869 fps_idx: usize,
870}
871
872impl EditorStats {
873 pub fn new() -> Self {
874 Self::default()
875 }
876
877 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 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#[derive(Debug, Clone)]
911pub struct GridRenderer {
912 pub enabled: bool,
913 pub cell_size: f32,
914 pub major_every: u32, pub color_minor: Vec4,
916 pub color_major: Vec4,
917 pub color_origin: Vec4,
918 pub fade_start: f32, pub fade_end: f32, pub y_plane: f32, }
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 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 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 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
1048pub struct EditorState {
1054 pub mode: EditorMode,
1056 pub config: EditorConfig,
1057
1058 pub camera: EditorCamera,
1060
1061 pub selection: SelectionSet,
1063
1064 pub undo_history: UndoHistory,
1066
1067 pub layout: EditorLayout,
1069 pub shortcuts: ShortcutRegistry,
1070
1071 pub stats: EditorStats,
1073
1074 pub grid: GridRenderer,
1076
1077 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 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 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 pub fn set_mode(&mut self, mode: EditorMode) {
1130 self.mode = mode;
1131 }
1132
1133 pub fn tick(&mut self, dt: f32) {
1135 self.stats.update(dt);
1136 }
1137
1138 pub fn do_command(&mut self, cmd: Box<dyn EditorCommand>) {
1140 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 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 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#[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 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}