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::ecs::ui::types::Rect;
use nightshade::prelude::*;
const CLICK_DRAG_THRESHOLD: f32 = 4.0;
pub fn update(editor_world: &mut EditorWorld, world: &mut World) {
handle_selection_shortcuts(editor_world, 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;
world.resources.user_interface.gizmos.selection_marquee = None;
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 shift_held = world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::ShiftLeft)
|| world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::ShiftRight);
if held
&& shift_held
&& editor_world.resources.picking.is_dragging
&& !ui_block
&& !gizmo_drag_active(world)
&& let Some(start) = editor_world.resources.picking.press_position
{
marquee_select(editor_world, world, start, mouse.position);
}
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
.render_settings
.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)
})
.map(|entity| {
editor_world
.resources
.camera_gizmos
.camera_for_proxy(entity)
.unwrap_or(entity)
})
.filter(|entity| !editor_world.resources.editor_scene.is_scaffolding(*entity));
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);
}
fn handle_selection_shortcuts(editor_world: &mut EditorWorld, world: &mut World) {
let keyboard = &world.resources.input.keyboard;
let ctrl = keyboard.is_key_pressed(KeyCode::ControlLeft)
|| keyboard.is_key_pressed(KeyCode::ControlRight);
let typing = world
.resources
.retained_ui
.interaction
.focused_entity
.is_some_and(|entity| {
world.ui.get_ui_text_input(entity).is_some()
|| world.ui.get_ui_text_area(entity).is_some()
});
let select_all = !typing && !ctrl && keyboard.is_key_pressed(KeyCode::KeyA);
let deselect = keyboard.is_key_pressed(KeyCode::Escape);
if select_all && !editor_world.resources.picking.select_all_was_pressed {
let mut roots: Vec<Entity> = Vec::new();
for entity in world
.core
.query_entities(nightshade::ecs::world::RENDER_MESH)
{
if editor_world.resources.editor_scene.is_scaffolding(entity) {
continue;
}
let root = find_group_root(world, entity);
if !roots.contains(&root) {
roots.push(root);
}
}
crate::systems::selection::set_many(editor_world, roots);
}
editor_world.resources.picking.select_all_was_pressed = select_all;
if deselect && !editor_world.resources.picking.deselect_was_pressed {
crate::systems::selection::clear(editor_world);
}
editor_world.resources.picking.deselect_was_pressed = deselect;
}
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);
}
fn gizmo_drag_active(world: &World) -> bool {
let gizmos = &world.resources.user_interface.gizmos;
gizmos.translation_drag.is_some()
|| gizmos.scale_drag.is_some()
|| gizmos.rotation_drag.is_some()
|| gizmos.planar_scale_drag.is_some()
|| gizmos.planar_translation_drag.is_some()
|| gizmos.nav_gizmo_drag.is_some()
}
fn marquee_select(
editor_world: &mut EditorWorld,
world: &mut World,
start: nalgebra_glm::Vec2,
current: nalgebra_glm::Vec2,
) {
let Some(camera_entity) = world.resources.active_camera else {
return;
};
let Some(viewport) = world.resources.window.active_viewport_rect else {
return;
};
let min_x = start.x.min(current.x);
let max_x = start.x.max(current.x);
let min_y = start.y.min(current.y);
let max_y = start.y.max(current.y);
let mut roots: Vec<Entity> = Vec::new();
let candidates: Vec<Entity> = world
.core
.query_entities(
nightshade::ecs::world::RENDER_MESH | nightshade::ecs::world::GLOBAL_TRANSFORM,
)
.collect();
for entity in candidates {
if editor_world.resources.editor_scene.is_scaffolding(entity) {
continue;
}
let Some(global) = world.core.get_global_transform(entity) else {
continue;
};
let world_position = global.translation();
let Some(screen) = project_to_screen(
world,
camera_entity,
world_position,
viewport.x,
viewport.y,
viewport.width,
viewport.height,
) else {
continue;
};
if screen.x >= min_x && screen.x <= max_x && screen.y >= min_y && screen.y <= max_y {
let root = find_group_root(world, entity);
if !roots.contains(&root) {
roots.push(root);
}
}
}
crate::systems::selection::set_many(editor_world, roots);
world.resources.user_interface.gizmos.selection_marquee = Some(Rect {
min: vec2(min_x, min_y),
max: vec2(max_x, max_y),
});
}
fn project_to_screen(
world: &World,
camera_entity: Entity,
world_position: Vec3,
viewport_x: f32,
viewport_y: f32,
viewport_width: f32,
viewport_height: f32,
) -> Option<Vec2> {
if viewport_height <= 0.0 {
return None;
}
let camera = world.core.get_camera(camera_entity).copied()?;
let camera_transform = world.core.get_global_transform(camera_entity)?;
let view = camera_transform
.0
.try_inverse()
.unwrap_or_else(Mat4::identity);
let aspect = viewport_width / viewport_height;
let view_projection = camera.projection.matrix_with_aspect(aspect) * view;
let clip = view_projection
* nalgebra_glm::Vec4::new(world_position.x, world_position.y, world_position.z, 1.0);
if clip.w <= 1.0e-6 {
return None;
}
let ndc_x = clip.x / clip.w;
let ndc_y = clip.y / clip.w;
let screen_x = viewport_x + (ndc_x * 0.5 + 0.5) * viewport_width;
let screen_y = viewport_y + (1.0 - (ndc_y * 0.5 + 0.5)) * viewport_height;
Some(vec2(screen_x, screen_y))
}