1use std::collections::{HashMap, HashSet, VecDeque};
2use std::path::{Path, PathBuf};
3use std::time::{Duration, Instant};
4
5use libdqg::camera::Camera;
6use libdqg::ecs::Entity;
7use libdqg::glam;
8use libdqg::input::{InputState, MouseState};
9use libdqg::renderer::{DrawPass, Renderer};
10use libdqg::scene::{Scene, SceneTransition};
11use libdqg::scripting::{ScriptError, ScriptRuntime};
12use libdqg::types::Color;
13use libdqg::world::{CameraComponent, Renderable, Transform, World};
14
15use crate::editor_settings::EditorSettings;
16use crate::egui_layer::EguiLayer;
17use crate::fly_camera::FlyCamera;
18use crate::picking;
19use crate::project::{EntityRecord, Project, RenderableAsset, RenderableKind};
20use crate::recent_projects::RecentProjects;
21use crate::ui;
22
23const LOAD_BUDGET_PER_FRAME: Duration = Duration::from_millis(8);
27
28const SCRIPT_ERROR_DISPLAY: Duration = Duration::from_secs(6);
31
32const EXPORT_STATUS_DISPLAY: Duration = Duration::from_secs(6);
35
36enum EditorMode {
45 Edit,
46 Playing { world_snapshot: World, runtime: ScriptRuntime },
47}
48
49pub enum PendingAction {
53 New(PathBuf),
54 Open(PathBuf),
55}
56
57enum LoadState {
60 Pending(PendingAction),
62 LoadingProject {
67 project: Project,
68 path: PathBuf,
69 world: World,
70 entity_assets: HashMap<Entity, RenderableAsset>,
71 remaining: VecDeque<EntityRecord>,
72 total: usize,
73 },
74 LoadingScene {
78 path: PathBuf,
79 world: World,
80 entity_assets: HashMap<Entity, RenderableAsset>,
81 remaining: VecDeque<EntityRecord>,
82 total: usize,
83 },
84}
85
86fn spawn_budgeted(
90 world: &mut World,
91 entity_assets: &mut HashMap<Entity, RenderableAsset>,
92 remaining: &mut VecDeque<EntityRecord>,
93 renderer: &Renderer,
94 project_root: &Path,
95) -> bool {
96 let deadline = Instant::now() + LOAD_BUDGET_PER_FRAME;
97 while Instant::now() < deadline {
98 let Some(record) = remaining.pop_front() else { break };
99 let (entity, asset) = record.spawn_into(world, Some(renderer), project_root);
100 if let Some(asset) = asset {
101 entity_assets.insert(entity, asset);
102 }
103 }
104 remaining.is_empty()
105}
106
107struct OpenScene {
114 path: PathBuf,
117 world: World,
118 entity_assets: HashMap<Entity, RenderableAsset>,
119 selected: Option<Entity>,
120 hovered: Option<Entity>,
124 renaming: Option<Entity>,
125 rename_buffer: String,
126 fly_camera: FlyCamera,
129 dirty: bool,
138}
139
140impl OpenScene {
141 fn new(path: PathBuf, world: World, entity_assets: HashMap<Entity, RenderableAsset>) -> Self {
142 Self {
143 path,
144 world,
145 entity_assets,
146 selected: None,
147 hovered: None,
148 renaming: None,
149 rename_buffer: String::new(),
150 fly_camera: FlyCamera::new(6.0),
151 dirty: false,
152 }
153 }
154}
155
156fn default_camera() -> Camera {
159 let mut camera = Camera {
160 position: glam::Vec3::new(0.0, 1.5, 4.0),
161 yaw: 0.0,
162 pitch: 0.0,
163 aspect: 1.0,
164 fov: 45.0,
165 znear: 0.1,
166 zfar: 100.0,
167 };
168 camera.look_at(glam::Vec3::ZERO);
169 camera
170}
171
172pub struct EditorScene {
173 open_scenes: Vec<OpenScene>,
177 active_scene: usize,
178 hover_blink_time: f32,
181 project: Option<Project>,
182 last_mouse_pos: (f32, f32),
183 recent: RecentProjects,
184 load: Option<LoadState>,
185 egui: EguiLayer,
186 assets_expanded: bool,
187 texture_previews: HashMap<PathBuf, egui::TextureHandle>,
191 mode: EditorMode,
192 script_errors: Vec<(String, Instant)>,
196 export_status: Option<(String, Instant)>,
200 settings: EditorSettings,
203 renaming_script: Option<PathBuf>,
207 script_rename_buffer: String,
208 renaming_scene: Option<PathBuf>,
211 scene_rename_buffer: String,
212 play_confirmation: Option<Vec<PathBuf>>,
217}
218
219impl EditorScene {
220 pub fn opening(action: PendingAction) -> Self {
222 Self {
223 open_scenes: vec![OpenScene::new(PathBuf::new(), World::new(default_camera()), HashMap::new())],
224 active_scene: 0,
225 hover_blink_time: 0.0,
226 project: None,
227 last_mouse_pos: (0.0, 0.0),
228 recent: RecentProjects::load(),
229 load: Some(LoadState::Pending(action)),
230 egui: EguiLayer::new(),
231 assets_expanded: true,
232 texture_previews: HashMap::new(),
233 mode: EditorMode::Edit,
234 script_errors: Vec::new(),
235 export_status: None,
236 settings: EditorSettings::load(),
237 renaming_script: None,
238 script_rename_buffer: String::new(),
239 renaming_scene: None,
240 scene_rename_buffer: String::new(),
241 play_confirmation: None,
242 }
243 }
244
245 fn queue_action(&mut self, action: PendingAction) {
247 self.load = Some(LoadState::Pending(action));
248 }
249
250 fn start_load(&mut self, action: PendingAction) {
257 let (root, create) = match action {
258 PendingAction::New(root) => (root, true),
259 PendingAction::Open(root) => (root, false),
260 };
261
262 let result = if create { Project::create(root.clone()) } else { Project::open(root.clone()) };
263 let project = match result {
264 Ok(project) => project,
265 Err(e) => {
266 eprintln!("Failed to {} project: {e}", if create { "create" } else { "open" });
267 return;
268 }
269 };
270
271 let scene_path = project.manifest.start_scene.clone();
272 let entities: VecDeque<EntityRecord> = if create {
273 VecDeque::new()
274 } else {
275 match project.load_scene(&scene_path) {
276 Ok(scene_file) => scene_file.entities.into(),
277 Err(e) => {
278 eprintln!("Failed to load scene: {e}");
284 return;
285 }
286 }
287 };
288
289 self.recent.add(root);
290 let _ = self.recent.save();
291
292 self.mode = EditorMode::Edit;
295 self.script_errors.clear();
296 self.texture_previews.clear();
297
298 let world = World::new(default_camera());
299 if entities.is_empty() {
300 self.open_scenes = vec![OpenScene::new(scene_path, world, HashMap::new())];
301 self.active_scene = 0;
302 self.project = Some(project);
303 } else {
304 let total = entities.len();
305 self.load = Some(LoadState::LoadingProject { project, path: scene_path, world, entity_assets: HashMap::new(), remaining: entities, total });
306 }
307 }
308
309 fn switch_or_open_scene(&mut self, path: PathBuf) {
318 if matches!(self.mode, EditorMode::Playing { .. }) {
319 self.stop_play();
320 }
321
322 if let Some(index) = self.open_scenes.iter().position(|scene| scene.path == path) {
323 self.active_scene = index;
324 return;
325 }
326
327 let Some(project) = self.project.as_ref() else { return };
328 let scene_file = match project.load_scene(&path) {
329 Ok(scene_file) => scene_file,
330 Err(e) => {
331 eprintln!("Failed to load scene {}: {e}", path.display());
332 return;
333 }
334 };
335
336 let entities: VecDeque<EntityRecord> = scene_file.entities.into();
337 let world = World::new(default_camera());
338 if entities.is_empty() {
339 self.open_scenes.push(OpenScene::new(path, world, HashMap::new()));
340 self.active_scene = self.open_scenes.len() - 1;
341 } else {
342 let total = entities.len();
343 self.load = Some(LoadState::LoadingScene { path, world, entity_assets: HashMap::new(), remaining: entities, total });
344 }
345 }
346
347 fn close_scene_tab(&mut self, path: PathBuf) {
351 if self.open_scenes.len() <= 1 {
352 return;
353 }
354 let Some(index) = self.open_scenes.iter().position(|scene| scene.path == path) else { return };
355
356 if index == self.active_scene && matches!(self.mode, EditorMode::Playing { .. }) {
357 self.stop_play();
358 }
359
360 if let Some(project) = self.project.as_ref() {
361 let scene = &self.open_scenes[index];
362 if let Err(e) = project.save_scene(&scene.path, &scene.world, &scene.entity_assets) {
363 eprintln!("Failed to save {}: {e}", scene.path.display());
368 return;
369 }
370 }
371
372 self.open_scenes.remove(index);
373 if index < self.active_scene {
374 self.active_scene -= 1;
375 } else if self.active_scene >= self.open_scenes.len() {
376 self.active_scene = self.open_scenes.len() - 1;
377 }
378 }
379
380 fn rename_scene(&mut self, old_path: PathBuf, new_stem: String) {
387 let new_stem = new_stem.trim();
388 if !is_valid_rename_stem(new_stem) {
389 eprintln!("Failed to rename scene: \"{new_stem}\" is not a valid file name");
390 return;
391 }
392 let extension = old_path.extension().map(|ext| ext.to_string_lossy().into_owned()).unwrap_or_default();
393 let new_path = old_path.with_file_name(format!("{new_stem}.{extension}"));
394 if new_path == old_path {
395 return;
396 }
397
398 let Some(project) = self.project.as_mut() else { return };
399 let old_absolute = project.root.join(&old_path);
400 let new_absolute = project.root.join(&new_path);
401 if new_absolute.exists() {
402 eprintln!("Failed to rename scene: {} already exists", new_path.display());
403 return;
404 }
405 if let Err(e) = std::fs::rename(&old_absolute, &new_absolute) {
406 eprintln!("Failed to rename scene: {e}");
407 return;
408 }
409
410 if project.manifest.start_scene == old_path {
411 if let Err(e) = project.set_start_scene(new_path.clone()) {
412 eprintln!("Failed to update start scene after rename: {e}");
421 if let Err(rollback_err) = std::fs::rename(&new_absolute, &old_absolute) {
422 eprintln!(
423 "Failed to restore scene after manifest error: {rollback_err} — the file remains at {}",
424 new_path.display()
425 );
426 project.manifest.start_scene = new_path.clone();
427 }
428 return;
429 }
430 }
431
432 for scene in &mut self.open_scenes {
433 if scene.path == old_path {
434 scene.path = new_path.clone();
435 }
436 }
437 }
438
439 fn rename_script(&mut self, old_path: PathBuf, new_stem: String) {
457 let new_stem = new_stem.trim();
458 if !is_valid_rename_stem(new_stem) {
459 eprintln!("Failed to rename script: \"{new_stem}\" is not a valid file name");
460 return;
461 }
462 let extension = old_path.extension().map(|ext| ext.to_string_lossy().into_owned()).unwrap_or_default();
463 let new_path = old_path.with_file_name(format!("{new_stem}.{extension}"));
464 if new_path == old_path {
465 return;
466 }
467
468 let old_absolute = {
469 let Some(project) = self.project.as_ref() else { return };
470 let old_absolute = project.root.join(&old_path);
471 let new_absolute = project.root.join(&new_path);
472 if new_absolute.exists() {
473 eprintln!("Failed to rename script: {} already exists", new_path.display());
474 return;
475 }
476 if let Err(e) = std::fs::copy(&old_absolute, &new_absolute) {
477 eprintln!("Failed to rename script: {e}");
478 return;
479 }
480 old_absolute
481 };
482
483 let mut all_migrated = true;
484
485 let open_paths: HashSet<PathBuf> = self.open_scenes.iter().map(|scene| scene.path.clone()).collect();
491 if let Some(project) = self.project.as_ref() {
492 for scene in self.open_scenes.iter_mut() {
493 let mut changed = false;
494 for (_, list) in scene.world.scripts.iter_mut() {
495 for attachment in list.0.iter_mut() {
496 if attachment.path == old_path {
497 attachment.path = new_path.clone();
498 changed = true;
499 }
500 }
501 }
502 if changed {
503 match project.save_scene(&scene.path, &scene.world, &scene.entity_assets) {
504 Ok(()) => scene.dirty = false,
505 Err(e) => {
506 eprintln!("Renamed script but failed to save {}: {e}", scene.path.display());
507 scene.dirty = true;
508 all_migrated = false;
509 }
510 }
511 }
512 }
513 }
514
515 if let Some(project) = self.project.as_ref() {
518 for scene_path in project.list_scenes() {
519 if open_paths.contains(&scene_path) {
520 continue;
521 }
522 let mut scene_file = match project.load_scene(&scene_path) {
523 Ok(scene_file) => scene_file,
524 Err(e) => {
525 eprintln!("Failed to check {} for script references: {e}", scene_path.display());
526 all_migrated = false;
527 continue;
528 }
529 };
530 let mut changed = false;
531 for record in scene_file.entities.iter_mut() {
532 for attachment in record.scripts.iter_mut() {
533 if attachment.path == old_path {
534 attachment.path = new_path.clone();
535 changed = true;
536 }
537 }
538 }
539 if changed {
540 if let Err(e) = scene_file.save(&project.root.join(&scene_path)) {
541 eprintln!("Failed to update script reference in {}: {e}", scene_path.display());
542 all_migrated = false;
543 }
544 }
545 }
546 }
547
548 if all_migrated {
549 if let Err(e) = std::fs::remove_file(&old_absolute) {
550 eprintln!("Renamed script to {}, but failed to remove the old file: {e}", new_path.display());
551 }
552 } else {
553 eprintln!(
554 "Some scenes still reference {} — left it alongside {} on disk until they're fixed up \
555 (open the affected scene and re-save, or reattach {})",
556 old_path.display(),
557 new_path.display(),
558 new_path.display()
559 );
560 }
561 }
562
563 fn start_play(&mut self, renderer: &Renderer) {
568 let Some(project) = self.project.as_ref() else { return };
569 let scene = &mut self.open_scenes[self.active_scene];
570
571 let world_snapshot = scene.world.clone();
572 let mut runtime = ScriptRuntime::new(project.root.clone());
573 runtime.begin_frame(&scene.world);
578
579 for error in runtime.start_all_scripts(&mut scene.world, &project.root, Some(renderer)) {
580 self.script_errors.push((format_script_error(&error), Instant::now()));
581 }
582
583 self.mode = EditorMode::Playing { world_snapshot, runtime };
584 }
585
586 fn stop_play(&mut self) {
590 if let EditorMode::Playing { world_snapshot, .. } = std::mem::replace(&mut self.mode, EditorMode::Edit) {
591 self.open_scenes[self.active_scene].world = world_snapshot;
592 }
593 self.script_errors.clear();
594 }
595
596 fn request_play(&mut self, renderer: &Renderer) {
602 let dirty_paths: Vec<PathBuf> = self.open_scenes.iter().filter(|scene| scene.dirty).map(|scene| scene.path.clone()).collect();
603 if dirty_paths.is_empty() {
604 self.start_play(renderer);
605 } else {
606 self.play_confirmation = Some(dirty_paths);
607 }
608 }
609
610 fn save_all_dirty(&mut self) -> bool {
619 let Some(project) = self.project.as_ref() else { return true };
620 let mut all_saved = true;
621 for scene in self.open_scenes.iter_mut() {
622 if !scene.dirty {
623 continue;
624 }
625 match project.save_scene(&scene.path, &scene.world, &scene.entity_assets) {
626 Ok(()) => scene.dirty = false,
627 Err(e) => {
628 eprintln!("Failed to save {}: {e}", scene.path.display());
629 all_saved = false;
630 }
631 }
632 }
633 all_saved
634 }
635}
636
637enum PlayConfirmAction {
639 SaveAndPlay,
640 PlayWithoutSaving,
641 Cancel,
642}
643
644fn draw_play_confirmation(ui: &mut egui::Ui, dirty_paths: &[PathBuf]) -> Option<PlayConfirmAction> {
650 let mut action = None;
651 egui::CentralPanel::default().show(ui, |ui| {
652 ui.centered_and_justified(|ui| {
653 ui.vertical_centered(|ui| {
654 ui.label("Unsaved changes in:");
655 for path in dirty_paths {
656 ui.label(path.display().to_string());
657 }
658 ui.add_space(8.0);
659 ui.label("A script that switches to one of these during Play would load its last-saved contents, not what's shown here.");
660 ui.add_space(8.0);
661 ui.horizontal(|ui| {
662 if ui.button("Save & Play").clicked() {
663 action = Some(PlayConfirmAction::SaveAndPlay);
664 }
665 if ui.button("Play Without Saving").clicked() {
666 action = Some(PlayConfirmAction::PlayWithoutSaving);
667 }
668 if ui.button("Cancel").clicked() {
669 action = Some(PlayConfirmAction::Cancel);
670 }
671 });
672 });
673 });
674 });
675 action
676}
677
678fn format_script_error(error: &ScriptError) -> String {
679 format!("{}: {}", error.script.display(), error.message)
680}
681
682fn is_valid_rename_stem(stem: &str) -> bool {
690 if stem.is_empty() || stem.contains('/') || stem.contains('\\') {
696 return false;
697 }
698 let mut components = Path::new(stem).components();
699 matches!(components.next(), Some(std::path::Component::Normal(_))) && components.next().is_none()
700}
701
702const CAMERA_GIZMO_DISTANCE: f32 = 0.6;
706const CAMERA_GIZMO_ASPECT: f32 = 16.0 / 9.0;
710
711fn draw_camera_gizmo(pass: &mut DrawPass, view_camera: &Camera, transform: &Transform, component: &CameraComponent, color: Color) {
715 let forward = transform.rotation * glam::Vec3::NEG_Z;
716 let up = transform.rotation * glam::Vec3::Y;
717 let right = transform.rotation * glam::Vec3::X;
718
719 let half_height = (component.fov.to_radians() * 0.5).tan() * CAMERA_GIZMO_DISTANCE;
720 let half_width = half_height * CAMERA_GIZMO_ASPECT;
721
722 let eye = transform.position;
723 let far_center = eye + forward * CAMERA_GIZMO_DISTANCE;
724 let corners = [
725 far_center + up * half_height + right * half_width,
726 far_center + up * half_height - right * half_width,
727 far_center - up * half_height - right * half_width,
728 far_center - up * half_height + right * half_width,
729 ];
730
731 for corner in corners {
732 pass.draw_world_line(view_camera, eye, corner, 1.5, color);
733 }
734 for i in 0..4 {
735 pass.draw_world_line(view_camera, corners[i], corners[(i + 1) % 4], 1.5, color);
736 }
737}
738
739impl Scene for EditorScene {
740 fn update(
741 &mut self,
742 delta_time: f32,
743 input_state: &InputState,
744 mouse_state: &MouseState,
745 renderer: Option<&mut Renderer>,
746 ) -> SceneTransition {
747 let Some(renderer) = renderer else { return SceneTransition::None };
748
749 if let Some(state) = self.load.take() {
750 match state {
751 LoadState::Pending(action) => self.start_load(action),
752 LoadState::LoadingProject { project, path, mut world, mut entity_assets, mut remaining, total } => {
753 let done = spawn_budgeted(&mut world, &mut entity_assets, &mut remaining, renderer, &project.root);
754 if done {
755 self.open_scenes = vec![OpenScene::new(path, world, entity_assets)];
756 self.active_scene = 0;
757 self.project = Some(project);
758 } else {
759 self.load = Some(LoadState::LoadingProject { project, path, world, entity_assets, remaining, total });
760 }
761 }
762 LoadState::LoadingScene { path, mut world, mut entity_assets, mut remaining, total } => {
763 if let Some(project) = self.project.as_ref() {
764 let done = spawn_budgeted(&mut world, &mut entity_assets, &mut remaining, renderer, &project.root);
765 if done {
766 self.open_scenes.push(OpenScene::new(path, world, entity_assets));
767 self.active_scene = self.open_scenes.len() - 1;
768 } else {
769 self.load = Some(LoadState::LoadingScene { path, world, entity_assets, remaining, total });
770 }
771 }
772 }
773 }
774
775 if let Some(load_state) = &self.load {
776 let (loaded, total) = match load_state {
777 LoadState::LoadingProject { remaining, total, .. } => (*total - remaining.len(), *total),
778 LoadState::LoadingScene { remaining, total, .. } => (*total - remaining.len(), *total),
779 LoadState::Pending(_) => (0, 0),
780 };
781 self.egui.run(renderer, |ui| draw_loading_screen(ui, loaded, total));
782 return SceneTransition::None;
783 }
784 }
785
786 if let Some(dirty_paths) = self.play_confirmation.clone() {
787 let mut action = None;
788 self.egui.run(renderer, |ui| action = draw_play_confirmation(ui, &dirty_paths));
789 match action {
790 Some(PlayConfirmAction::SaveAndPlay) => {
791 self.save_all_dirty();
792 let still_dirty: Vec<PathBuf> = self.open_scenes.iter().filter(|scene| scene.dirty).map(|scene| scene.path.clone()).collect();
798 if still_dirty.is_empty() {
799 self.play_confirmation = None;
800 self.start_play(renderer);
801 } else {
802 self.play_confirmation = Some(still_dirty);
803 }
804 }
805 Some(PlayConfirmAction::PlayWithoutSaving) => {
806 self.play_confirmation = None;
807 self.start_play(renderer);
808 }
809 Some(PlayConfirmAction::Cancel) => {
810 self.play_confirmation = None;
811 }
812 None => {}
813 }
814 return SceneTransition::None;
819 }
820
821 let mouse_pos = mouse_state.position();
822 let mouse_delta = (mouse_pos.0 - self.last_mouse_pos.0, mouse_pos.1 - self.last_mouse_pos.1);
823 self.last_mouse_pos = mouse_pos;
824
825 self.script_errors.retain(|(_, at)| at.elapsed() < SCRIPT_ERROR_DISPLAY);
826 if self.export_status.as_ref().is_some_and(|(_, at)| at.elapsed() >= EXPORT_STATUS_DISPLAY) {
827 self.export_status = None;
828 }
829 let is_playing = matches!(self.mode, EditorMode::Playing { .. });
830 let script_error_messages: Vec<String> = self.script_errors.iter().map(|(message, _)| message.clone()).collect();
831 let export_status_message = self.export_status.as_ref().map(|(message, _)| message.as_str());
832 let open_scene_tabs: Vec<(PathBuf, bool)> = self.open_scenes.iter().map(|scene| (scene.path.clone(), scene.dirty)).collect();
833 let active_scene = self.active_scene;
834
835 let active_index = self.active_scene;
836 let (world, selected, renaming, rename_buffer, entity_assets, current_scene, current_scene_dirty) = {
837 let scene = &mut self.open_scenes[active_index];
838 (&mut scene.world, &mut scene.selected, &mut scene.renaming, &mut scene.rename_buffer, &mut scene.entity_assets, scene.path.clone(), scene.dirty)
839 };
840 let project = self.project.as_ref();
841 let assets_expanded = &mut self.assets_expanded;
842 let texture_previews = &mut self.texture_previews;
843 let settings = &mut self.settings;
844 let renaming_script = &mut self.renaming_script;
845 let script_rename_buffer = &mut self.script_rename_buffer;
846 let renaming_scene = &mut self.renaming_scene;
847 let scene_rename_buffer = &mut self.scene_rename_buffer;
848 let mut requests = ui::UiRequests::default();
849 self.egui.run(renderer, |ui| {
850 ui::draw(
851 ui,
852 world,
853 selected,
854 renaming,
855 rename_buffer,
856 project,
857 entity_assets,
858 assets_expanded,
859 texture_previews,
860 is_playing,
861 &script_error_messages,
862 settings,
863 renaming_script,
864 script_rename_buffer,
865 renaming_scene,
866 scene_rename_buffer,
867 export_status_message,
868 ¤t_scene,
869 current_scene_dirty,
870 &open_scene_tabs,
871 active_scene,
872 &mut requests,
873 );
874 });
875
876 if requests.edited {
877 self.open_scenes[active_index].dirty = true;
878 }
879
880 if requests.save_scene {
881 if let Some(project) = self.project.as_ref() {
882 let scene = &self.open_scenes[active_index];
883 match project.save_scene(&scene.path, &scene.world, &scene.entity_assets) {
884 Ok(()) => self.open_scenes[active_index].dirty = false,
885 Err(e) => eprintln!("Failed to save project: {e}"),
886 }
887 }
888 }
889
890 if let Some(root) = requests.new_project.take() {
891 self.queue_action(PendingAction::New(root));
892 }
893
894 if let Some(output_dir) = requests.export_project.take() {
895 if self.project.is_some() {
896 let saved = self.save_all_dirty();
904 if let Some(project) = self.project.as_ref() {
905 if saved {
906 let result = crate::export::export_project(project, &output_dir);
907 let message = match &result {
908 Ok(()) => format!("Exported to {}", output_dir.display()),
909 Err(e) => format!("Export failed: {e}"),
910 };
911 if let Err(e) = &result {
912 eprintln!("Export failed: {e}");
913 }
914 self.export_status = Some((message, Instant::now()));
915 } else {
916 self.export_status = Some((
917 "Export aborted: one or more open scenes failed to save".to_string(),
918 Instant::now(),
919 ));
920 }
921 }
922 }
923 }
924
925 if let Some(root) = requests.open_project.take() {
926 self.queue_action(PendingAction::Open(root));
927 }
928
929 if let Some(path) = requests.switch_scene.take() {
930 self.switch_or_open_scene(path);
931 }
932
933 if let Some(path) = requests.close_scene_tab.take() {
934 self.close_scene_tab(path);
935 }
936
937 if let Some((old_path, new_stem)) = requests.rename_scene.take() {
938 self.rename_scene(old_path, new_stem);
939 }
940
941 if let Some((old_path, new_stem)) = requests.rename_script.take() {
942 self.rename_script(old_path, new_stem);
943 }
944
945 if let Some(path) = requests.set_start_scene.take() {
946 if let Some(project) = self.project.as_mut() {
947 if let Err(e) = project.set_start_scene(path) {
948 eprintln!("Failed to set start scene: {e}");
949 }
950 }
951 }
952
953 if requests.toggle_play {
954 if is_playing {
955 self.stop_play();
956 } else {
957 self.request_play(renderer);
958 }
959 }
960
961 if let Some((entity, kind, path)) = requests.attach_renderable.take() {
962 if let Some(project) = self.project.as_ref() {
963 let probe_asset = match kind {
964 RenderableKind::Sprite => {
965 RenderableAsset::Sprite { texture_path: path.clone(), width: 0.0, height: 0.0 }
966 }
967 RenderableKind::Model => RenderableAsset::Model { model_path: path.clone() },
968 };
969 match probe_asset.load(renderer, &project.root) {
970 Ok(renderable) => {
971 let asset = match &renderable {
974 Renderable::Sprite(sprite) => {
975 RenderableAsset::Sprite { texture_path: path, width: sprite.width, height: sprite.height }
976 }
977 Renderable::Model(_) => RenderableAsset::Model { model_path: path },
978 };
979 let scene = &mut self.open_scenes[self.active_scene];
980 scene.world.set_renderable(entity, renderable);
981 scene.entity_assets.insert(entity, asset);
982 scene.dirty = true;
983 }
984 Err(e) => eprintln!("Failed to attach renderable: {e}"),
985 }
986 }
987 }
988
989 self.hover_blink_time += delta_time;
990
991 let active_index = self.active_scene;
992 match &mut self.mode {
993 EditorMode::Edit => {
994 if !self.egui.wants_pointer_input() {
995 let scene = &mut self.open_scenes[active_index];
996 scene.fly_camera.update(delta_time, input_state, mouse_state, &mut scene.world.camera, mouse_delta);
997
998 let size = renderer.size();
999 let ndc_x = (mouse_pos.0 / (size.width.max(1) as f32)) * 2.0 - 1.0;
1000 let ndc_y = 1.0 - (mouse_pos.1 / (size.height.max(1) as f32)) * 2.0;
1001 scene.hovered = picking::pick(&scene.world, &scene.world.camera, ndc_x, ndc_y);
1002
1003 if mouse_state.is_button_pressed(winit::event::MouseButton::Left) {
1004 scene.selected = scene.hovered;
1005 }
1006 } else {
1007 self.open_scenes[active_index].hovered = None;
1008 }
1009 }
1010 EditorMode::Playing { runtime, .. } => {
1011 let scene = &mut self.open_scenes[active_index];
1015 scene.hovered = None;
1016 runtime.begin_frame(&scene.world);
1017 for entity in scene.world.iter_entities().collect::<Vec<_>>() {
1018 for error in runtime.update_entity(&mut scene.world, entity, delta_time, input_state, mouse_state, mouse_delta, Some(&*renderer)) {
1019 self.script_errors.push((format_script_error(&error), Instant::now()));
1020 }
1021 }
1022 }
1023 }
1024
1025 let scene = &mut self.open_scenes[active_index];
1026 scene.world.sync_transforms();
1027 let size = renderer.size();
1028 let aspect = size.width as f32 / (size.height.max(1) as f32);
1029 scene.world.camera.aspect = aspect;
1030
1031 let push_camera = if is_playing {
1032 scene.world.active_camera(aspect).unwrap_or(scene.world.camera)
1033 } else {
1034 scene.world.camera
1035 };
1036 *renderer.camera_mut() = push_camera;
1037
1038 SceneTransition::None
1039 }
1040
1041 fn render(&mut self, pass: &mut DrawPass) {
1042 let blink_alpha = 0.15 + 0.35 * (self.hover_blink_time * 6.0).sin().abs();
1043 let hover_highlight = Color::new(1.0, 1.0, 1.0, blink_alpha as f64);
1044 let no_highlight = Color::new(1.0, 1.0, 1.0, 0.0);
1045 let is_edit_mode = matches!(self.mode, EditorMode::Edit);
1046 let scene = &mut self.open_scenes[self.active_scene];
1047
1048 for (entity, renderable) in scene.world.renderables.iter_mut() {
1049 let highlight = if scene.hovered == Some(entity) { hover_highlight } else { no_highlight };
1050 let is_selected = scene.selected == Some(entity);
1051
1052 match renderable {
1053 Renderable::Sprite(sprite) => {
1054 sprite.highlight = highlight;
1055 if is_selected {
1059 pass.draw_world_sprite_outline(sprite.x, sprite.y, sprite.z, sprite.width, sprite.height, sprite.transform);
1060 }
1061 sprite.draw_world(pass);
1062 }
1063 Renderable::Model(model) => {
1064 model.highlight = highlight;
1065 pass.draw_model(model);
1066 if is_selected {
1067 pass.draw_model_outline(model);
1068 }
1069 }
1070 }
1071 }
1072
1073 if is_edit_mode {
1074 for (entity, component) in scene.world.cameras.iter() {
1075 let Some(transform) = scene.world.transforms.get(entity) else { continue };
1076 let color = if scene.selected == Some(entity) {
1077 Color::new(0.3, 0.7, 1.0, 1.0)
1078 } else {
1079 Color::new(0.6, 0.6, 0.6, 1.0)
1080 };
1081 draw_camera_gizmo(pass, &scene.world.camera, transform, component, color);
1082 }
1083 }
1084 }
1085
1086 fn on_window_event(&mut self, event: &winit::event::WindowEvent) {
1087 self.egui.on_window_event(event);
1088 }
1089
1090 fn render_overlay(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
1091 self.egui.render_overlay(device, queue, encoder, view);
1092 }
1093}
1094
1095fn draw_loading_screen(ui: &mut egui::Ui, loaded: usize, total: usize) {
1096 egui::CentralPanel::default().show(ui, |ui| {
1097 ui.centered_and_justified(|ui| {
1098 ui.vertical_centered(|ui| {
1099 ui.spinner();
1100 ui.add_space(8.0);
1101 ui.label(format!("Loading scene... ({loaded}/{total})"));
1102 });
1103 });
1104 });
1105}