nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use super::context::EditorContext;
use super::input::{is_ctrl_pressed, is_shift_pressed};
use crate::prelude::*;

pub fn is_editor_utility_entity(
    world: &World,
    entity: Entity,
    camera_entity: Option<Entity>,
    gizmo: Option<&crate::ecs::gizmos::GizmoState>,
) -> bool {
    if Some(entity) == camera_entity {
        return true;
    }
    if let Some(gizmo) = gizmo
        && crate::ecs::gizmos::is_gizmo_entity(world, gizmo, entity)
    {
        return true;
    }
    false
}

pub fn draw_marquee_selection(marquee: &super::context::MarqueeState, ui_context: &egui::Context) {
    if let Some(rect) = marquee.rect() {
        let painter = ui_context.layer_painter(egui::LayerId::new(
            egui::Order::Foreground,
            egui::Id::new("marquee_selection"),
        ));

        painter.rect_filled(
            rect,
            0.0,
            egui::Color32::from_rgba_unmultiplied(100, 150, 250, 50),
        );
        painter.rect_stroke(
            rect,
            0.0,
            egui::Stroke::new(1.0, egui::Color32::from_rgb(100, 150, 250)),
            egui::StrokeKind::Inside,
        );
    }
}

pub fn update_marquee_selection(context: &mut EditorContext, world: &mut World) {
    if !super::camera_controls::is_mouse_in_selected_viewport(world) {
        context.marquee.cancel();
        return;
    }

    if context.gizmo_interaction.is_dragging() {
        context.marquee.cancel();
        return;
    }

    let in_modal_transform =
        context.transform_edit.modal.operation != super::context::TransformOperation::None;
    if in_modal_transform {
        context.marquee.cancel();
        return;
    }

    if world.resources.user_interface.hud_wants_pointer {
        context.marquee.cancel();
        return;
    }

    let mouse = &world.resources.input.mouse;
    let mouse_pos = Vec2::new(mouse.position.x, mouse.position.y);

    if mouse.state.contains(MouseState::LEFT_JUST_PRESSED)
        && context.gizmo_interaction.hover_axis.is_none()
    {
        context.marquee.start(egui::pos2(mouse_pos.x, mouse_pos.y));
    }

    if context.marquee.active {
        context.marquee.update(egui::pos2(mouse_pos.x, mouse_pos.y));

        if mouse.state.contains(MouseState::LEFT_JUST_RELEASED)
            && let Some(rect) = context.marquee.finish()
        {
            let min = Vec2::new(rect.min.x, rect.min.y);
            let max = Vec2::new(rect.max.x, rect.max.y);
            let min_threshold = 5.0;
            if (max.x - min.x).abs() > min_threshold && (max.y - min.y).abs() > min_threshold {
                select_entities_in_marquee(context, world, min, max);
            }
        }
    }
}

fn select_entities_in_marquee(
    context: &mut EditorContext,
    world: &mut World,
    screen_min: Vec2,
    screen_max: Vec2,
) {
    use crate::ecs::camera::queries::query_active_camera_matrices;

    let Some(camera_matrices) = query_active_camera_matrices(world) else {
        return;
    };

    let viewport_size = match world.resources.window.cached_viewport_size {
        Some((width, height)) => (width as f32, height as f32),
        None => return,
    };

    let viewport_rect = world.resources.window.active_viewport_rect.as_ref();

    if !is_shift_pressed(world) {
        context.selection.clear();
    }

    let view_projection = camera_matrices.projection * camera_matrices.view;
    let camera_entity = world.resources.active_camera;
    let gizmo = context.gizmo_interaction.active.as_ref();

    let entities: Vec<Entity> = world
        .query_entities(crate::ecs::GLOBAL_TRANSFORM)
        .filter(|entity| !is_editor_utility_entity(world, *entity, camera_entity, gizmo))
        .collect();

    for entity in entities {
        if let Some(global_transform) = world.get_global_transform(entity) {
            let world_pos = global_transform.translation();
            let clip_pos =
                view_projection * nalgebra_glm::vec4(world_pos.x, world_pos.y, world_pos.z, 1.0);

            if clip_pos.w <= 0.0 {
                continue;
            }

            let ndc = nalgebra_glm::vec3(
                clip_pos.x / clip_pos.w,
                clip_pos.y / clip_pos.w,
                clip_pos.z / clip_pos.w,
            );

            if ndc.x < -1.0
                || ndc.x > 1.0
                || ndc.y < -1.0
                || ndc.y > 1.0
                || ndc.z < 0.0
                || ndc.z > 1.0
            {
                continue;
            }

            let mut screen_x = (ndc.x + 1.0) * 0.5 * viewport_size.0;
            let mut screen_y = (1.0 - ndc.y) * 0.5 * viewport_size.1;

            if let Some(rect) = viewport_rect {
                screen_x = rect.x + screen_x * (rect.width / viewport_size.0);
                screen_y = rect.y + screen_y * (rect.height / viewport_size.1);
            }

            if screen_x >= screen_min.x
                && screen_x <= screen_max.x
                && screen_y >= screen_min.y
                && screen_y <= screen_max.y
            {
                context.selection.add(entity);
            }
        }
    }

    world.resources.graphics.bounding_volume_selected_entity = context.selection.primary();
}

pub fn update_picking(context: &mut EditorContext, world: &mut World) {
    if !super::camera_controls::is_mouse_in_selected_viewport(world) {
        return;
    }

    if context.gizmo_interaction.is_dragging() {
        return;
    }

    let mouse = &world.resources.input.mouse;
    let mouse_pos = mouse.position;

    if mouse.state.contains(MouseState::LEFT_JUST_PRESSED)
        && !world.resources.user_interface.hud_wants_pointer
        && context.gizmo_interaction.hover_axis.is_none()
    {
        let pick_pos = if let Some(viewport_rect) = &world.resources.window.active_viewport_rect
            && let Some((window_width, window_height)) = world.resources.window.cached_viewport_size
        {
            let local = viewport_rect.to_local(mouse_pos);
            let scale_x = window_width as f32 / viewport_rect.width;
            let scale_y = window_height as f32 / viewport_rect.height;
            ((local.x * scale_x) as u32, (local.y * scale_y) as u32)
        } else {
            (mouse_pos.x as u32, mouse_pos.y as u32)
        };
        world
            .resources
            .gpu_picking
            .request_pick(pick_pos.0, pick_pos.1);
    }

    if let Some(result) = world.resources.gpu_picking.take_result() {
        let shift_pressed = is_shift_pressed(world);
        let ctrl_pressed = is_ctrl_pressed(world);

        if let Some(entity_id) = result.entity_id {
            let camera_entity = world.resources.active_camera;
            let gizmo = context.gizmo_interaction.active.as_ref();

            let found_entity = world
                .query_entities(crate::ecs::RENDER_MESH | crate::ecs::GLOBAL_TRANSFORM)
                .find(|entity| {
                    entity.id == entity_id
                        && !is_editor_utility_entity(world, *entity, camera_entity, gizmo)
                });

            if let Some(entity) = found_entity {
                if ctrl_pressed {
                    context.selection.toggle(entity);
                } else if shift_pressed {
                    context.selection.add(entity);
                } else {
                    context.selection.set_single(entity);
                }
            } else if !shift_pressed && !ctrl_pressed {
                context.selection.clear();
            }
            world.resources.graphics.bounding_volume_selected_entity = context.selection.primary();
        } else if !shift_pressed && !ctrl_pressed {
            context.selection.clear();
            world.resources.graphics.bounding_volume_selected_entity = None;
        }
    }
}

pub fn check_context_menu_trigger(context: &EditorContext, world: &World) -> Option<Vec2> {
    if !super::camera_controls::is_mouse_in_selected_viewport(world) {
        return None;
    }

    if context.selection.is_empty() {
        return None;
    }

    let in_modal_transform =
        context.transform_edit.modal.operation != super::context::TransformOperation::None;
    if in_modal_transform {
        return None;
    }

    if context.gizmo_interaction.is_dragging() {
        return None;
    }

    let mouse = &world.resources.input.mouse;
    if mouse
        .state
        .contains(crate::prelude::MouseState::RIGHT_JUST_PRESSED)
    {
        return Some(mouse.position);
    }

    None
}