nightshade-editor 0.14.2

Interactive map editor for the Nightshade game engine
use crate::ecs::{EditorMode, EditorWorld};
use crate::systems::input;
use crate::systems::mode::is_descendant_of;
use nightshade::ecs::input::resources::MouseState;
use nightshade::ecs::transform::queries::{chain_from_root_to_leaf, find_group_root};
use nightshade::prelude::*;

const CLICK_DRAG_THRESHOLD: f32 = 4.0;

pub fn update(editor_world: &mut EditorWorld, world: &mut World) {
    let mouse = *nightshade::ecs::input::access::mouse_for_active(world);
    let just_pressed = mouse.state.contains(MouseState::LEFT_JUST_PRESSED);
    let just_released = mouse.state.contains(MouseState::LEFT_JUST_RELEASED);
    let held = mouse.state.contains(MouseState::LEFT_CLICKED);
    let in_viewport = input::mouse_in_active_viewport(world);
    let ui_block = input::ui_capturing(world);
    let hud_block = world.resources.user_interface.hud_wants_pointer;

    if just_pressed {
        editor_world.resources.picking.press_position = if in_viewport && !ui_block && !hud_block {
            Some(mouse.position)
        } else {
            None
        };
        editor_world.resources.picking.is_dragging = false;
    }

    if held
        && let Some(start) = editor_world.resources.picking.press_position
        && nalgebra_glm::distance(&start, &mouse.position) > CLICK_DRAG_THRESHOLD
    {
        editor_world.resources.picking.is_dragging = true;
    }

    let mut pending_alt = false;
    let mut pending_shift = false;
    let mut requested_pick = false;
    if just_released
        && let Some(start) = editor_world.resources.picking.press_position.take()
        && !editor_world.resources.picking.is_dragging
        && nalgebra_glm::distance(&start, &mouse.position) <= CLICK_DRAG_THRESHOLD
    {
        let render_scale = world.resources.graphics.render_scale.clamp(0.25, 4.0);
        let pick_pos = world
            .resources
            .window
            .active_viewport_rect
            .and_then(|rect| {
                let texture_width = ((rect.width * render_scale).round() as u32).max(1);
                let texture_height = ((rect.height * render_scale).round() as u32).max(1);
                surface_pick_coords(mouse.position, Some(rect), (texture_width, texture_height))
            });
        if let Some((x, y)) = pick_pos {
            world.resources.gpu_picking.request_pick(x, y);
            let keyboard = &world.resources.input.keyboard;
            pending_alt = keyboard.is_key_pressed(KeyCode::AltLeft)
                || keyboard.is_key_pressed(KeyCode::AltRight);
            pending_shift = keyboard.is_key_pressed(KeyCode::ShiftLeft)
                || keyboard.is_key_pressed(KeyCode::ShiftRight);
            requested_pick = true;
        }
    }

    if just_released {
        editor_world.resources.picking.is_dragging = false;
    }

    if requested_pick {
        editor_world.resources.picking.pending_alt = pending_alt;
        editor_world.resources.picking.pending_shift = pending_shift;
    }

    if let Some(result) = world.resources.gpu_picking.take_result() {
        let alt_held = std::mem::take(&mut editor_world.resources.picking.pending_alt);
        let shift_held = std::mem::take(&mut editor_world.resources.picking.pending_shift);
        let picked = result.entity_id.and_then(|id| {
            world
                .core
                .query_entities(nightshade::ecs::world::RENDER_MESH)
                .find(|entity| entity.id == id)
        });
        let (new_selection, expand_tree) = match picked {
            None => {
                reset_cycle(editor_world);
                (None, false)
            }
            Some(leaf) => resolve_selection(editor_world, world, leaf, alt_held),
        };
        if shift_held {
            if let Some(entity) = new_selection {
                crate::systems::selection::toggle(editor_world, entity);
            }
        } else {
            crate::systems::selection::set_primary(editor_world, new_selection);
        }
        if expand_tree {
            let focus_target = editor_world.resources.ui.selected_entity;
            focus_tree_on(editor_world, world, focus_target);
        }
    }

    sync_outline(editor_world, world);
}

pub fn reset_cycle(editor_world: &mut EditorWorld) {
    editor_world.resources.picking.last_pick_leaf = None;
    editor_world.resources.picking.last_pick_root = None;
    editor_world.resources.picking.cycle_depth = 0;
}

fn resolve_selection(
    editor_world: &mut EditorWorld,
    world: &World,
    leaf: Entity,
    alt_held: bool,
) -> (Option<Entity>, bool) {
    if alt_held {
        editor_world.resources.picking.last_pick_leaf = Some(leaf);
        editor_world.resources.picking.last_pick_root = Some(leaf);
        editor_world.resources.picking.cycle_depth = 0;
        return (Some(leaf), true);
    }
    match editor_world.resources.mode.mode {
        EditorMode::Edit { target } => {
            reset_cycle(editor_world);
            if is_descendant_of(world, leaf, target) {
                (Some(leaf), true)
            } else {
                (None, false)
            }
        }
        EditorMode::Object => {
            let root = find_group_root(world, leaf);
            let chain = chain_from_root_to_leaf(world, root, leaf);
            let last_root = editor_world.resources.picking.last_pick_root;
            let last_leaf = editor_world.resources.picking.last_pick_leaf;
            let current_selection = editor_world.resources.ui.selected_entity;
            let on_same_chain = current_selection.is_some_and(|selected| chain.contains(&selected));
            let cycling = last_leaf == Some(leaf) && last_root == Some(root) && on_same_chain;
            let depth = if cycling {
                let next = editor_world.resources.picking.cycle_depth + 1;
                if next >= chain.len() { 0 } else { next }
            } else {
                0
            };
            editor_world.resources.picking.last_pick_leaf = Some(leaf);
            editor_world.resources.picking.last_pick_root = Some(root);
            editor_world.resources.picking.cycle_depth = depth;
            (Some(chain[depth]), !cycling)
        }
    }
}

fn focus_tree_on(editor_world: &mut EditorWorld, world: &mut World, selection: Option<Entity>) {
    let Some(entity) = selection else {
        return;
    };
    let tree = &mut editor_world.resources.ui_handles.tree;
    let row_index = tree.all_rows.iter().position(|row| row.entity == entity);
    let Some(row_index) = row_index else {
        return;
    };
    let target_depth = tree.all_rows[row_index].depth;
    if target_depth > 0 {
        let mut depth_to_find = target_depth;
        for row in tree.all_rows[..row_index].iter().rev() {
            if row.depth < depth_to_find && row.has_children {
                tree.expanded.insert(row.entity.id);
                if depth_to_find == 1 {
                    break;
                }
                depth_to_find = row.depth;
            }
        }
    }
    tree.last_visible_start = usize::MAX;

    let scroll_entity = world
        .ui
        .get_ui_virtual_list(tree.list)
        .map(|d| d.scroll_entity);
    if let Some(scroll_entity) = scroll_entity {
        let list_height = world
            .ui
            .get_ui_layout_node(tree.list)
            .map(|n| n.computed_rect.height())
            .unwrap_or(0.0);
        let dpi = world.resources.window.cached_scale_factor.max(1.0);
        let logical_height = list_height / dpi;
        let mut visible_position = 0usize;
        let mut skip_until_depth: Option<usize> = None;
        let filter_active = !tree.filter_lower.is_empty();
        for (idx, row) in tree.all_rows.iter().enumerate() {
            if let Some(depth) = skip_until_depth {
                if row.depth > depth {
                    continue;
                } else {
                    skip_until_depth = None;
                }
            }
            let collapsed =
                !tree.cameras_only && row.has_children && !tree.expanded.contains(&row.entity.id);
            let matches_filter =
                !filter_active || row.label.to_lowercase().contains(&tree.filter_lower);
            if matches_filter {
                if idx == row_index {
                    break;
                }
                visible_position += 1;
            }
            if collapsed && !filter_active {
                skip_until_depth = Some(row.depth);
            }
        }
        let row_top = visible_position as f32 * 22.0;
        let row_bottom = row_top + 22.0;
        let current_offset = world
            .ui
            .get_ui_scroll_area(scroll_entity)
            .map(|d| d.scroll_offset)
            .unwrap_or(0.0);
        let mut new_offset = current_offset;
        if row_top < current_offset {
            new_offset = row_top;
        } else if row_bottom > current_offset + logical_height && logical_height > 0.0 {
            new_offset = row_bottom - logical_height;
        }
        if (new_offset - current_offset).abs() > 0.01 {
            ui_scroll_area_set_offset(world, scroll_entity, new_offset);
        }
    }
}

fn sync_outline(editor_world: &EditorWorld, world: &mut World) {
    crate::systems::selection::sync_to_engine(editor_world, world);
}