1pub mod inspector;
7pub mod hierarchy;
8pub mod console;
9pub mod gizmos;
10
11use glam::{Vec2, Vec3, Vec4};
12use std::collections::HashMap;
13
14pub use inspector::Inspector;
16pub use hierarchy::HierarchyPanel;
17pub use console::DevConsole;
18pub use gizmos::GizmoRenderer;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
26pub struct EntityId(pub u32);
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
30pub struct GlyphId(pub u32);
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum EditorMode {
39 Play,
41 Edit,
43 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum EditorTheme {
100 Dark,
101 Light,
102 HighContrast,
103 Solarized,
104}
105
106impl EditorTheme {
107 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#[derive(Debug, Clone)]
141pub struct EditorCamera {
142 pub position: Vec3,
143 pub yaw: f32, pub pitch: f32, pub move_speed: f32,
146 pub look_sensitivity: f32,
147 pub fov_degrees: f32,
148 pub near: f32,
149 pub far: f32,
150 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 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 pub fn right(&self) -> Vec3 {
187 self.forward().cross(Vec3::Y).normalize()
188 }
189
190 pub fn up(&self) -> Vec3 {
192 self.right().cross(self.forward()).normalize()
193 }
194
195 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 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 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 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 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 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#[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 pub fn select_entity(&mut self, id: EntityId) {
274 self.clear();
275 self.entities.push(id);
276 }
277
278 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 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 pub fn select_glyph(&mut self, id: GlyphId) {
298 self.clear();
299 self.glyphs.push(id);
300 }
301
302 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 pub fn primary_entity(&self) -> Option<EntityId> {
313 self.entities.first().copied()
314 }
315
316 pub fn primary_glyph(&self) -> Option<GlyphId> {
318 self.glyphs.first().copied()
319 }
320
321 pub fn count(&self) -> usize {
323 self.entities.len() + self.glyphs.len()
324 }
325}
326
327pub trait EditorCommand: std::fmt::Debug + Send + Sync {
333 fn name(&self) -> &str;
335 fn execute(&mut self, state: &mut EditorState);
337 fn undo(&mut self, state: &mut EditorState);
339}
340
341#[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#[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#[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#[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#[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
480pub struct UndoHistory {
484 commands: Vec<Box<dyn EditorCommand>>,
485 cursor: usize, 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 pub fn execute(&mut self, mut cmd: Box<dyn EditorCommand>, state: &mut EditorState) {
509 if self.cursor < self.commands.len() {
511 self.commands.truncate(self.cursor);
512 }
513 cmd.execute(state);
514 self.commands.push(cmd);
515 if self.commands.len() > self.max_size {
517 self.commands.remove(0);
518 } else {
519 self.cursor += 1;
520 }
521 }
522
523 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 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 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#[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 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 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#[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#[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#[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); 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 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#[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 pub frame_index: u64,
803 pub elapsed_secs: f32,
805 fps_samples: Vec<f32>,
807 fps_idx: usize,
808}
809
810impl EditorStats {
811 pub fn new() -> Self {
812 Self::default()
813 }
814
815 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 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#[derive(Debug, Clone)]
849pub struct GridRenderer {
850 pub enabled: bool,
851 pub cell_size: f32,
852 pub major_every: u32, pub color_minor: Vec4,
854 pub color_major: Vec4,
855 pub color_origin: Vec4,
856 pub fade_start: f32, pub fade_end: f32, pub y_plane: f32, }
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 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 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 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
986pub struct EditorState {
992 pub mode: EditorMode,
994 pub config: EditorConfig,
995
996 pub camera: EditorCamera,
998
999 pub selection: SelectionSet,
1001
1002 pub undo_history: UndoHistory,
1004
1005 pub layout: EditorLayout,
1007 pub shortcuts: ShortcutRegistry,
1008
1009 pub stats: EditorStats,
1011
1012 pub grid: GridRenderer,
1014
1015 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 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 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 pub fn set_mode(&mut self, mode: EditorMode) {
1068 self.mode = mode;
1069 }
1070
1071 pub fn tick(&mut self, dt: f32) {
1073 self.stats.update(dt);
1074 }
1075
1076 pub fn do_command(&mut self, cmd: Box<dyn EditorCommand>) {
1078 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 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 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#[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 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}