nightshade-editor 0.33.0

Interactive map editor for the Nightshade game engine
use crate::HDR_BYTES;
use crate::ecs::EditorWorld;
use crate::systems::retained_ui;
use crate::systems::shell::EditorShellContext;
use crate::systems::{
    atmosphere, camera, camera_gizmos, cutscene_edit, deep_link, grab, graphics, input,
    light_gizmos, loading, mode, model, perf_test, picking, project_io, random, render, shell,
    skeleton_debug, sun,
};
use nightshade::ecs::camera::systems::{
    camera_controllers_system, fly_camera_system, pan_orbit_update_transform,
};
use nightshade::ecs::input::resources::MouseState;
use nightshade::prelude::*;
use nightshade::render::wgpu::rendergraph::RenderGraph;
use nightshade::run::RenderResources;
use nightshade::shell::ShellState;

pub struct Editor {
    pub editor_world: EditorWorld,
    pub shell: ShellState<EditorShellContext>,
    #[cfg(not(target_arch = "wasm32"))]
    pub startup_map: Option<std::path::PathBuf>,
}

impl Default for Editor {
    fn default() -> Self {
        Self {
            editor_world: EditorWorld::default(),
            shell: shell::new_shell(),
            #[cfg(not(target_arch = "wasm32"))]
            startup_map: None,
        }
    }
}

impl State for Editor {
    fn configure_render_graph(
        &mut self,
        graph: &mut RenderGraph<World>,
        device: &wgpu::Device,
        _: wgpu::TextureFormat,
        resources: RenderResources,
    ) {
        render::configure_graph(graph, device, resources);
    }

    fn initialize(&mut self, world: &mut World) {
        world.resources.window.title = "Nightshade Editor".to_string();

        graphics::configure_defaults(world);

        load_hdr_skybox(world, HDR_BYTES.to_vec());

        sun::spawn_with_shadows(&mut self.editor_world, world);
        camera::spawn_default(&mut self.editor_world, world);

        project_io::new_project(&mut self.editor_world, world);

        #[cfg(not(target_arch = "wasm32"))]
        if let Some(path) = self.startup_map.take() {
            match std::fs::read(&path) {
                Ok(bytes) => project_io::load_project_bytes_into_editor(
                    &mut self.editor_world,
                    world,
                    &path.to_string_lossy(),
                    &bytes,
                ),
                Err(error) => {
                    tracing::error!("Failed to read map '{}': {error}", path.display())
                }
            }
        }

        const GLTF_DATA: &[u8] = include_bytes!("../assets/gltf/DamagedHelmet.glb");
        loading::load_gltf_bytes(&mut self.editor_world, "DamagedHelmet.glb", GLTF_DATA);
        loading::preload_browsers(&mut self.editor_world);

        retained_ui::init(&mut self.editor_world, world);
        retained_ui::build::load_palette_textures(&mut self.editor_world, world);
    }

    fn run_systems(&mut self, world: &mut World) {
        let shell_capturing = shell::is_capturing_input(&self.shell);
        if !shell_capturing {
            crate::systems::play::poll_hotkey(&mut self.editor_world, world);
        }
        if self.editor_world.resources.play.active {
            crate::systems::play::tick(&mut self.editor_world, world, shell_capturing);
            shell::run(&mut self.shell, world);
            let events = std::mem::take(&mut world.resources.input.events);
            for event in events {
                if let AppEvent::Keyboard { key, state } = event {
                    shell::handle_key(&mut self.shell, world, key, state);
                }
            }
            crate::systems::repaint::update(&mut self.editor_world, world);
            return;
        }

        camera::handle_fly_toggle(&mut self.editor_world, world);
        let grab_active = grab::is_active(&self.editor_world);
        let marquee_intent = marquee_drag_intent(world);
        if self.editor_world.resources.camera.fly_mode {
            if !shell_capturing {
                set_cursor_locked(world, true);
                set_cursor_visible(world, false);
                fly_camera_system(world);
            } else {
                set_cursor_locked(world, false);
                set_cursor_visible(world, true);
            }
        } else if !shell_capturing && !input::ui_capturing(world) && !grab_active {
            if marquee_intent
                || placement_draw_active(&self.editor_world)
                || crate::systems::push_pull::is_dragging(&self.editor_world)
            {
                pan_orbit_update_transform(world);
            } else {
                camera_controllers_system(world);
            }
        } else {
            pan_orbit_update_transform(world);
        }

        camera::handle_reset_input(&mut self.editor_world, world);
        atmosphere::switch_system(&mut self.editor_world, world);
        let mut shortcut_actions: Vec<crate::systems::retained_ui::Action> = Vec::new();
        if !grab_active {
            input::poll_shortcuts(
                &mut self.editor_world.resources.shortcuts,
                world,
                &mut shortcut_actions,
            );
            if !shortcut_actions.is_empty() {
                self.editor_world
                    .resources
                    .ui_interaction
                    .actions
                    .extend(shortcut_actions);
            }
        }
        random::poll(&mut self.editor_world);
        perf_test::poll(&mut self.editor_world, world);
        deep_link::poll(&mut self.editor_world, world);
        loading::poll_pending_imports(&mut self.editor_world, world);
        loading::poll_browsers(&mut self.editor_world, world);
        loading::poll_thumbnails(&mut self.editor_world, world);
        loading::poll_fit_frames(&mut self.editor_world, world);
        loading::update_top_progress_bar(&self.editor_world, world);
        project_io::poll_pending_loads(&mut self.editor_world, world);
        project_io::poll_thumbnail_isolation(&mut self.editor_world, world);
        let force_resync = std::mem::take(
            &mut self
                .editor_world
                .resources
                .writeback_state
                .needs_full_resync,
        );
        crate::scene_writeback::reconcile(
            &mut self.editor_world.resources.project,
            &mut self.editor_world.resources.editor_scene,
            &mut self.editor_world.resources.writeback_state,
            world,
            force_resync,
        );
        atmosphere::tick_day_night(&mut self.editor_world, world);
        sun::update(&mut self.editor_world, world);
        light_gizmos::update(&mut self.editor_world, world);
        camera_gizmos::update(&mut self.editor_world, world);
        skeleton_debug::update(&mut self.editor_world, world);
        sync_snap_to_engine(&self.editor_world, world);
        model::rotate(&mut self.editor_world, world);

        mode::tick_mode(&mut self.editor_world, world, shell_capturing);
        retained_ui::update(&mut self.editor_world, world);
        grab::tick(&mut self.editor_world, world);
        if !greybox_tab_active(&self.editor_world, world) {
            if self.editor_world.resources.placement.active {
                crate::systems::retained_ui::create_shape::stop(&mut self.editor_world, world);
            }
            self.editor_world.resources.push_pull.active = false;
        }
        if grab::is_active(&self.editor_world) {
            crate::systems::selection::sync_to_engine(&self.editor_world, world);
        } else if self.editor_world.resources.placement.active {
            crate::systems::retained_ui::create_shape::tick(&mut self.editor_world, world);
        } else if self.editor_world.resources.push_pull.active {
            crate::systems::push_pull::tick(&mut self.editor_world, world);
        } else {
            picking::update(&mut self.editor_world, world);
        }
        crate::systems::retained_ui::build::update_grid(&mut self.editor_world, world);
        let undo_count_before = self.editor_world.resources.undo.undo_len();
        crate::undo::poll_gizmo_drag(
            &mut self.editor_world.resources.gizmo_drag,
            &mut self.editor_world.resources.undo,
            &self.editor_world.resources.editor_scene,
            world,
        );
        if self.editor_world.resources.undo.undo_len() > undo_count_before {
            self.editor_world.resources.ui_handles.inspector.dirty = true;
        }
        shell::run(&mut self.shell, world);
        cutscene_edit::apply_commands(&mut self.editor_world, world, &mut self.shell.context);
        cutscene_edit::sync(&mut self.editor_world, world);
        crate::systems::repaint::update(&mut self.editor_world, world);

        let events = std::mem::take(&mut world.resources.input.events);
        let mut dropped_files: Vec<nightshade::ecs::input::resources::DroppedFile> = Vec::new();
        for event in events {
            match event {
                AppEvent::Keyboard { key, state } => {
                    shell::handle_key(&mut self.shell, world, key, state);
                }
                AppEvent::FileDropped(file) => {
                    dropped_files.push(file);
                }
                #[cfg(not(target_arch = "wasm32"))]
                AppEvent::FileDroppedPath(path) => {
                    loading::handle_dropped_path(&mut self.editor_world, world, &path);
                }
                _ => {}
            }
        }
        if !dropped_files.is_empty() {
            loading::handle_dropped_files(&mut self.editor_world, world, &dropped_files);
        }
    }
}

fn greybox_tab_active(editor_world: &EditorWorld, world: &World) -> bool {
    ui_tab_bar_selected(world, editor_world.resources.ui_handles.tree.tab_bar) == Some(1)
}

fn placement_draw_active(editor_world: &EditorWorld) -> bool {
    matches!(
        editor_world.resources.placement.phase,
        crate::ecs::PlacementPhase::Footprint { .. } | crate::ecs::PlacementPhase::Height { .. }
    )
}

fn marquee_drag_intent(world: &World) -> bool {
    let keyboard = &world.resources.input.keyboard;
    let shift =
        keyboard.is_key_pressed(KeyCode::ShiftLeft) || keyboard.is_key_pressed(KeyCode::ShiftRight);
    if !shift {
        return false;
    }
    let left_held = nightshade::ecs::input::access::mouse_for_active(world)
        .state
        .contains(MouseState::LEFT_CLICKED);
    left_held && crate::systems::input::mouse_in_active_viewport(world)
}

fn sync_snap_to_engine(editor_world: &EditorWorld, world: &mut World) {
    let snap = &editor_world.resources.snap;
    let gizmos = &mut world.resources.user_interface.gizmos;
    if snap.enabled {
        gizmos.translation_snap = Some(snap.translation_step);
        gizmos.rotation_snap_radians = Some(snap.rotation_step_degrees.to_radians());
        gizmos.scale_snap = Some(snap.scale_step);
    } else {
        gizmos.translation_snap = None;
        gizmos.rotation_snap_radians = None;
        gizmos.scale_snap = None;
    }
}