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::project_io::project_data_mut;
use nightshade::prelude::*;

impl Editor {
    #[cfg(not(target_arch = "wasm32"))]
    pub(super) fn set_active_project_path(&mut self, path: &std::path::Path) {
        self.context.project_edit.current_path = Some(path.to_path_buf());
        self.project_state.is_open = true;
        self.project_state.clear_modified();
        self.settings
            .data
            .recent_projects
            .add(std::path::PathBuf::from(path.display().to_string()));
        if let Err(error) = self.settings.save() {
            tracing::error!("Failed to save settings: {error}");
        }
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub fn save_project(&mut self, world: &World) {
        if let Some(path) = &self.context.project_edit.current_path.clone() {
            self.update_project_from_current_state(world);
            if let Some(ref project) = self.project {
                match project.save_to_path(path) {
                    Ok(()) => {
                        let path_str = path.display().to_string();
                        self.project_state.clear_modified();
                        self.toasts
                            .push(ToastKind::Success, format!("Saved to {}", path_str), 3.0);
                    }
                    Err(error) => {
                        self.toasts.push(
                            ToastKind::Error,
                            format!("Failed to save: {}", error),
                            3.0,
                        );
                    }
                }
            }
        } else {
            self.save_project_as(world);
        }
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub fn save_project_as(&mut self, world: &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::save_file_dialog(&filters, Some("project.json"))
        {
            self.ensure_project_exists(world);
            self.update_project_from_current_state(world);
            if let Some(ref mut project) = self.project {
                project.name = self
                    .context
                    .project_edit
                    .current_name
                    .clone()
                    .unwrap_or_default();
                match project.save_to_path(&path) {
                    Ok(()) => {
                        self.set_active_project_path(&path);
                        self.toasts.push(
                            ToastKind::Success,
                            format!("Saved to {}", path.display()),
                            3.0,
                        );
                    }
                    Err(error) => {
                        self.toasts.push(
                            ToastKind::Error,
                            format!("Failed to save: {}", error),
                            3.0,
                        );
                    }
                }
            }
        }
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub fn collect_all_and_save(&mut self, world: &World) {
        let Some(save_dir) = nightshade::filesystem::pick_folder() else {
            return;
        };

        self.ensure_project_exists(world);
        self.update_project_from_current_state(world);

        let Some(ref mut project) = self.project else {
            return;
        };

        let assets_dir = save_dir.join("assets");
        if let Err(error) = std::fs::create_dir_all(&assets_dir) {
            self.toasts.push(
                ToastKind::Error,
                format!("Failed to create assets directory: {}", error),
                3.0,
            );
            return;
        }

        let (copied_count, copy_errors) =
            collect_scene_assets(project_data_mut(project), &assets_dir);

        for error in &copy_errors {
            self.toasts.push(ToastKind::Warning, error.clone(), 3.0);
        }

        let project_name = if project.name.is_empty() {
            "project".to_string()
        } else {
            project.name.clone()
        };
        let project_file = save_dir.join(format!("{}.project.json", project_name));

        project.name = self
            .context
            .project_edit
            .current_name
            .clone()
            .unwrap_or_default();
        match project.save_to_path(&project_file) {
            Ok(()) => {
                self.set_active_project_path(&project_file);
                self.toasts.push(
                    ToastKind::Success,
                    format!(
                        "Saved to {} with {} assets collected",
                        project_file.display(),
                        copied_count
                    ),
                    3.0,
                );
            }
            Err(error) => {
                self.toasts
                    .push(ToastKind::Error, format!("Failed to save: {}", error), 3.0);
            }
        }
    }

    #[cfg(not(target_arch = "wasm32"))]
    pub fn export_scene_to_path(&mut self, world: &World, path: &std::path::Path) {
        let mut scene = nightshade::ecs::scene::world_to_scene(world, "Exported Scene");
        match nightshade::ecs::scene::save_scene(&mut scene, path) {
            Ok(()) => {
                self.toasts.push(
                    ToastKind::Success,
                    format!("Exported scene to {}", path.display()),
                    3.0,
                );
            }
            Err(error) => {
                self.toasts.push(
                    ToastKind::Error,
                    format!("Failed to export scene: {}", error),
                    3.0,
                );
            }
        }
    }

    #[cfg(target_arch = "wasm32")]
    pub fn save_project(&mut self, world: &World) {
        self.ensure_project_exists(world);
        self.update_project_from_current_state(world);
        let Some(project) = &self.project else {
            return;
        };
        let bytes = match project.to_json_bytes() {
            Ok(bytes) => bytes,
            Err(error) => {
                tracing::error!("Failed to serialize project: {}", error);
                return;
            }
        };

        let filters = [nightshade::filesystem::FileFilter {
            name: "JSON Project".to_string(),
            extensions: vec!["project.json".to_string()],
        }];
        if let Err(error) = nightshade::filesystem::save_file("project.json", &bytes, &filters) {
            tracing::error!("Failed to save project: {}", error);
            return;
        }

        self.project_state.clear_modified();
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn collect_scene_assets(
    data: &mut super::types::EditorProjectData,
    assets_dir: &std::path::Path,
) -> (usize, Vec<String>) {
    let mut copied_files: std::collections::HashMap<String, String> =
        std::collections::HashMap::new();
    let mut copy_errors: Vec<String> = Vec::new();

    for map in data.scenes.values_mut() {
        if let Some(nightshade::ecs::scene::SceneHdrSkybox::Reference { path }) =
            &mut map.hdr_skybox
        {
            let source_path = std::path::Path::new(&*path);
            if source_path.exists() {
                let file_name = source_path
                    .file_name()
                    .and_then(|name| name.to_str())
                    .unwrap_or("skybox.hdr");

                if let Some(new_relative_path) = copied_files.get(&*path) {
                    *path = new_relative_path.clone();
                } else {
                    let dest_path = assets_dir.join(file_name);
                    let relative_path = format!("assets/{}", file_name);

                    if !dest_path.exists()
                        && let Err(error) = std::fs::copy(source_path, &dest_path)
                    {
                        copy_errors.push(format!("Failed to copy HDR '{}': {}", path, error));
                    }

                    if dest_path.exists() {
                        copied_files.insert(path.clone(), relative_path.clone());
                        *path = relative_path;
                    }
                }
            } else {
                copy_errors.push(format!("HDR skybox file not found: {}", path));
            }
        }
    }

    (copied_files.len(), copy_errors)
}