nightshade-editor 0.13.4

An interactive editor for the Nightshade game engine
pub(crate) mod assets;
mod load;
mod save;
pub(crate) mod startup;
mod types;

pub use types::{EditorProjectFile, project_data};

#[derive(Default)]
pub struct ProjectState {
    pub is_open: bool,
    pub is_modified: bool,
}

impl ProjectState {
    pub fn mark_modified(&mut self) {
        self.is_modified = true;
    }

    pub fn clear_modified(&mut self) {
        self.is_modified = false;
    }
}
pub(crate) use types::{new_editor_project, project_data_mut};

use crate::Editor;
use crate::app_context::ProjectEditState;
use crate::engine_editor::EditorContext;
#[cfg(not(target_arch = "wasm32"))]
use crate::mosaic::ToastKind;
use crate::mosaic::WindowLayout;
use crate::widgets::{CameraWidget, EditorWidget};
use nightshade::prelude::*;

type Pane = crate::mosaic::Pane<EditorWidget>;

fn world_to_scene(world: &World) -> nightshade::ecs::scene::Scene {
    nightshade::ecs::scene::world_to_scene(world, "Untitled")
}

pub fn create_default_tile_tree() -> egui_tiles::Tree<Pane> {
    let mut tiles = egui_tiles::Tiles::default();
    let viewport = tiles.insert_pane(crate::mosaic::Pane::new(EditorWidget::Camera(
        CameraWidget { camera_index: 0 },
    )));
    egui_tiles::Tree::new("tile_tree", viewport, tiles)
}

pub fn clear_scene(context: &mut EditorContext, world: &mut World) {
    use nightshade::ecs::gizmos;

    if let Some(old_gizmo) = context.gizmo_interaction.active.take() {
        gizmos::destroy_gizmo(world, &old_gizmo);
    }

    let camera_entity = world.resources.active_camera;
    let all_entities: Vec<Entity> = world.core.get_all_entities();

    for entity in all_entities {
        if Some(entity) == camera_entity {
            continue;
        }

        despawn_recursive_immediate(world, entity);
    }

    context.selection.clear();
    context.undo_history.clear();
    context.gizmo_interaction.hover_axis = None;
    context.gizmo_interaction.drag_mode = nightshade::ecs::gizmos::GizmoDragMode::None;

    world.resources.graphics.atmosphere = Atmosphere::Sky;
    spawn_sun(world);
}

fn update_hdr_skybox_from_scene(
    project_edit: &mut ProjectEditState,
    scene: &nightshade::ecs::scene::Scene,
) {
    project_edit.hdr_skybox_path = None;
    if let Some(nightshade::ecs::scene::SceneHdrSkybox::Reference { path }) = &scene.hdr_skybox {
        project_edit.hdr_skybox_path = Some(std::path::PathBuf::from(path));
    }
}

impl Editor {
    pub fn ensure_project_exists(&mut self, world: &World) {
        if self.project.is_none() {
            let mut new_project =
                new_editor_project(self.context.project_edit.current_name.clone());
            project_data_mut(&mut new_project)
                .add_scene("Untitled".to_string(), world_to_scene(world));
            if let Some(tree) = self.mosaic.current_tree().cloned() {
                new_project.windows.push(WindowLayout::new(tree, "Default"));
            }
            self.project = Some(new_project);
            self.scene_browser_dirty = true;
        }
    }

    pub fn new_project(&mut self, world: &mut World) {
        clear_scene(&mut self.context.editor, world);
        self.context.project_edit.current_path = None;
        self.context.project_edit.current_name = None;
        self.context.editor.selection.clear();
        self.context.project_edit.hdr_skybox_path = None;
        self.context.project_edit.editing_name = false;
        self.context.project_edit.name_edit_buffer.clear();

        let default_tree = create_default_tile_tree();
        let layouts = vec![WindowLayout::new(default_tree, "Default")];
        self.mosaic.load_layouts(layouts.clone(), 0);
        self.scene_browser_dirty = true;

        #[cfg(not(target_arch = "wasm32"))]
        {
            let mut project = new_editor_project(None);
            project_data_mut(&mut project).add_scene("Untitled".to_string(), world_to_scene(world));
            project.windows = layouts;
            self.project = Some(project);
            self.project_state = ProjectState::default();
            self.toasts
                .push(ToastKind::Success, "Created new project", 3.0);
        }
    }

    pub fn update_project_from_current_state(&mut self, world: &World) {
        let scene = world_to_scene(world);
        let (layouts, active_index) = self.mosaic.save_layouts();
        if let Some(ref mut project) = self.project {
            let data = project_data_mut(project);
            if let Some(active_scene_name) = &data.active_scene_name.clone() {
                data.scenes.insert(active_scene_name.clone(), scene);
            }
            data.active_layout_index = active_index;
            project.windows = layouts;
        }
    }

    fn spawn_scene_with_feedback(
        &mut self,
        world: &mut World,
        scene: &nightshade::ecs::scene::Scene,
    ) -> bool {
        match nightshade::ecs::scene::spawn_scene(world, scene, None) {
            Ok(result) => {
                for warning in result.warnings {
                    tracing::warn!("{}", warning);
                    #[cfg(not(target_arch = "wasm32"))]
                    self.toasts.push(ToastKind::Warning, warning, 3.0);
                }
                crate::engine_editor::recreate_gizmo_for_mode(&mut self.context.editor, world);
                true
            }
            Err(error) => {
                tracing::error!("Failed to spawn scene: {}", error);
                #[cfg(not(target_arch = "wasm32"))]
                self.toasts
                    .push(ToastKind::Error, format!("Failed to spawn: {}", error), 3.0);
                false
            }
        }
    }

    pub fn switch_to_map(&mut self, world: &mut World, map_name: &str) {
        self.update_project_from_current_state(world);
        let map_to_spawn = if let Some(ref mut project) = self.project {
            let data = project_data_mut(project);
            if data.set_active_scene(map_name) {
                data.active_scene().cloned()
            } else {
                None
            }
        } else {
            None
        };
        self.scene_browser_dirty = true;
        if let Some(map) = map_to_spawn {
            update_hdr_skybox_from_scene(&mut self.context.project_edit, &map);
            clear_scene(&mut self.context.editor, world);
            if self.spawn_scene_with_feedback(world, &map) {
                #[cfg(not(target_arch = "wasm32"))]
                self.toasts.push(
                    ToastKind::Success,
                    format!("Switched to map: {}", map_name),
                    3.0,
                );
            }
        }
    }

    pub fn create_new_map(&mut self, world: &mut World, map_name: String) {
        self.update_project_from_current_state(world);
        clear_scene(&mut self.context.editor, world);
        world.resources.graphics.atmosphere = Atmosphere::Sky;
        spawn_sun(world);
        let map = world_to_scene(world);
        if let Some(ref mut project) = self.project {
            let data = project_data_mut(project);
            data.add_scene(map_name.clone(), map);
            data.set_active_scene(&map_name);
        }
        self.project_state.mark_modified();
        self.scene_browser_dirty = true;
        #[cfg(not(target_arch = "wasm32"))]
        self.toasts.push(
            ToastKind::Success,
            format!("Created map: {}", map_name),
            3.0,
        );
    }

    pub fn delete_current_map(&mut self, world: &mut World) {
        let can_delete = self
            .project
            .as_ref()
            .map(|p| project_data(p).map(|d| d.scenes.len() > 1).unwrap_or(false))
            .unwrap_or(false);
        if !can_delete {
            #[cfg(not(target_arch = "wasm32"))]
            self.toasts
                .push(ToastKind::Warning, "Cannot delete the last map", 3.0);
            #[cfg(target_arch = "wasm32")]
            tracing::warn!("Cannot delete the last map");
            return;
        }
        let (deleted_name, map_to_spawn) = if let Some(ref mut project) = self.project {
            let data = project_data_mut(project);
            if let Some(map_name) = data.active_scene_name.clone() {
                data.remove_scene(&map_name);
                (Some(map_name), data.active_scene().cloned())
            } else {
                (None, None)
            }
        } else {
            (None, None)
        };
        if let Some(map_name) = deleted_name {
            self.project_state.mark_modified();
            self.scene_browser_dirty = true;
            clear_scene(&mut self.context.editor, world);
            if let Some(map) = map_to_spawn {
                update_hdr_skybox_from_scene(&mut self.context.project_edit, &map);
                self.spawn_scene_with_feedback(world, &map);
            } else {
                self.context.project_edit.hdr_skybox_path = None;
            }
            #[cfg(not(target_arch = "wasm32"))]
            self.toasts.push(
                ToastKind::Success,
                format!("Deleted map: {}", map_name),
                3.0,
            );
            #[cfg(target_arch = "wasm32")]
            let _ = map_name;
        }
    }
}