nightshade-editor 0.13.3

An interactive editor for the Nightshade game engine
use crate::Editor;
#[cfg(not(target_arch = "wasm32"))]
use crate::mosaic::ToastKind;
#[cfg(not(target_arch = "wasm32"))]
use crate::mosaic::WindowLayout;
use crate::project_io::{EditorProjectFile, project_data};
#[cfg(not(target_arch = "wasm32"))]
use crate::project_io::{new_editor_project, project_data_mut};
use nightshade::prelude::*;

#[cfg(not(target_arch = "wasm32"))]
fn resolve_project_relative_paths(
    project: &mut EditorProjectFile,
    project_dir: Option<&std::path::Path>,
) {
    let Some(base_dir) = project_dir else {
        return;
    };

    for map in project_data_mut(project).scenes.values_mut() {
        if let Some(nightshade::ecs::scene::SceneHdrSkybox::Reference { path }) =
            &mut map.hdr_skybox
        {
            let path_buf = std::path::Path::new(&*path);
            if path_buf.is_relative() {
                let absolute = base_dir.join(path_buf);
                if let Some(absolute_str) = absolute.to_str() {
                    *path = absolute_str.to_string();
                }
            }
        }
    }
}

impl Editor {
    #[cfg(not(target_arch = "wasm32"))]
    pub fn load_project(&mut self, world: &mut World) {
        let filters = [
            nightshade::filesystem::FileFilter {
                name: "JSON Project".to_string(),
                extensions: vec!["project.json".to_string()],
            },
            nightshade::filesystem::FileFilter {
                name: "Binary Project".to_string(),
                extensions: vec!["project.bin".to_string()],
            },
        ];
        if let Some(path) = nightshade::filesystem::pick_file(&filters) {
            self.load_project_from_path(world, &path);
        }
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub fn load_project_from_path(&mut self, world: &mut World, path: &std::path::Path) {
        match EditorProjectFile::load_from_path(path) {
            Ok(mut project) => {
                let project_dir = path.parent();
                resolve_project_relative_paths(&mut project, project_dir);

                super::clear_scene(&mut self.context.editor, world);
                world.resources.physics.paused = true;

                if let Some(active_map) = project_data(&project)
                    .and_then(|d| d.active_scene())
                    .cloned()
                {
                    super::update_hdr_skybox_from_scene(
                        &mut self.context.project_edit,
                        &active_map,
                    );
                    if !self.spawn_scene_with_feedback(world, &active_map) {
                        return;
                    }
                } else {
                    self.context.project_edit.hdr_skybox_path = None;
                }

                if project.windows.is_empty() {
                    let default_tree = crate::project_io::create_default_tile_tree();
                    project
                        .windows
                        .push(WindowLayout::new(default_tree, "Default"));
                }
                let active_index = project_data(&project)
                    .map(|d| d.active_layout_index)
                    .unwrap_or(0);
                self.mosaic
                    .load_layouts(project.windows.clone(), active_index);

                self.context.project_edit.current_name =
                    Some(project.name.clone()).filter(|n| !n.is_empty());
                self.context.editor.selection.clear();
                self.context.project_edit.editing_name = false;
                self.context.project_edit.name_edit_buffer.clear();
                self.project = Some(project);
                self.scene_browser_dirty = true;
                self.set_active_project_path(path);
                self.toasts.push(
                    ToastKind::Success,
                    format!("Loaded from {}", path.display()),
                    3.0,
                );
            }
            Err(error) => {
                self.toasts
                    .push(ToastKind::Error, format!("Failed to load: {}", error), 3.0);
            }
        }
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub fn load_scene_from_path(&mut self, world: &mut World, path: &std::path::Path) {
        match nightshade::ecs::scene::load_scene(path) {
            Ok(mut scene) => {
                scene.rebuild_uuid_index();

                super::clear_scene(&mut self.context.editor, world);
                super::update_hdr_skybox_from_scene(&mut self.context.project_edit, &scene);
                world.resources.physics.paused = true;

                let scene_name = if scene.header.name.is_empty() {
                    path.file_stem()
                        .map(|s| s.to_string_lossy().to_string())
                        .unwrap_or_else(|| "Untitled".to_string())
                } else {
                    scene.header.name.clone()
                };

                if !self.spawn_scene_with_feedback(world, &scene) {
                    return;
                }

                if self.project.is_none() {
                    let mut new_project = new_editor_project(Some(scene_name.clone()));
                    let default_tree = crate::project_io::create_default_tile_tree();
                    new_project
                        .windows
                        .push(WindowLayout::new(default_tree.clone(), "Default"));
                    self.mosaic.set_tree(default_tree);
                    self.project = Some(new_project);
                    self.project_state.is_open = true;
                }

                if let Some(ref mut project) = self.project {
                    let data = project_data_mut(project);
                    data.add_scene(scene_name.clone(), scene);
                    data.set_active_scene(&scene_name);
                }

                self.project_state.mark_modified();
                self.scene_browser_dirty = true;
                self.context.editor.selection.clear();

                self.toasts.push(
                    ToastKind::Success,
                    format!("Loaded scene from {}", path.display()),
                    3.0,
                );
            }
            Err(error) => {
                self.toasts.push(
                    ToastKind::Error,
                    format!("Failed to load scene: {}", error),
                    3.0,
                );
            }
        }
    }

    pub fn load_project_from_bytes(&mut self, world: &mut World, bytes: &[u8]) {
        let parse_result: Result<EditorProjectFile, crate::mosaic::MosaicError> =
            EditorProjectFile::from_bytes(bytes);
        match parse_result {
            Ok(project) => {
                super::clear_scene(&mut self.context.editor, world);
                if let Some(active_map) = project_data(&project)
                    .and_then(|d| d.active_scene())
                    .cloned()
                    && !self.spawn_scene_with_feedback(world, &active_map)
                {
                    return;
                }
                let active_index = project_data(&project)
                    .map(|d| d.active_layout_index)
                    .unwrap_or(0);
                self.mosaic
                    .load_layouts(project.windows.clone(), active_index);
                self.context.project_edit.current_path = None;
                self.context.project_edit.current_name =
                    Some(project.name.clone()).filter(|n| !n.is_empty());
                self.context.editor.selection.clear();
                self.project = Some(project);
                self.scene_browser_dirty = true;
                self.project_state.is_open = true;
                self.project_state.clear_modified();
            }
            Err(error) => {
                tracing::error!("Failed to parse project: {}", error);
            }
        }
    }

    pub fn process_pending_project_load(&mut self, world: &mut World) {
        if let Some(ref pending) = self.context.pending_file_load
            && let Some(loaded_file) = pending.take()
        {
            self.context.pending_file_load = None;
            self.load_project_from_bytes(world, &loaded_file.bytes);
        }
    }

    #[cfg(target_arch = "wasm32")]
    pub fn load_project(&mut self, _world: &mut World) {
        let filters = [nightshade::filesystem::FileFilter {
            name: "JSON Project".to_string(),
            extensions: vec!["project.json".to_string()],
        }];
        self.context.pending_file_load = Some(nightshade::filesystem::request_file_load(&filters));
    }
}