From e39843690d9750082afb0e95716620cbf9dce722 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Mon, 1 Apr 2024 12:56:53 -0400 Subject: [PATCH] Replace `bevy_mod_picking` selection --- studio/Cargo.toml | 1 - studio/src/camera_controller.rs | 60 ++--- studio/src/debug.rs | 14 +- studio/src/input.rs | 392 +++++++++++++++++++--------- studio/src/interaction/selection.rs | 85 ++++-- 5 files changed, 364 insertions(+), 188 deletions(-) diff --git a/studio/Cargo.toml b/studio/Cargo.toml index b5d9bc2..b95d6d8 100644 --- a/studio/Cargo.toml +++ b/studio/Cargo.toml @@ -83,5 +83,4 @@ features = [ "backend_raycast", "backend_sprite", "debug", - "selection", ] diff --git a/studio/src/camera_controller.rs b/studio/src/camera_controller.rs index bee5881..b0445d2 100644 --- a/studio/src/camera_controller.rs +++ b/studio/src/camera_controller.rs @@ -6,7 +6,7 @@ use bevy::{ }; use crate::{ - input::{self, Action, CameraMotion, InputAction, Lifecycle}, + input::{self, CameraAction, CameraMotion, Lifecycle, SelectionAction}, interaction::Selection, math::{ Easing, InterpPosition, InterpVelocity, Interpolation, InterpolationSystem, NearlyEq, @@ -71,10 +71,10 @@ impl CameraControllerBundle { fn camera_control_kinematic( mut q_window: Query<&mut Window, With>, - mut input: EventReader, + mut er_camera: EventReader, mut q_camera: Query<(&mut CameraController, &mut Transform, &Projection)>, ) { - if input.is_empty() { + if er_camera.is_empty() { return; } @@ -82,30 +82,28 @@ fn camera_control_kinematic( let mut any_started = false; let mut any_stopped = false; - for action in input.read().filter(|action| { - !matches!( - action, - InputAction(_, Action::Camera(CameraMotion::Translate(_))) - ) - }) { - if let InputAction(lifecycle, Action::Camera(motion)) = action { - let (mut controller, mut xform, proj) = q_camera.single_mut(); - let window = q_window.single(); + for action in er_camera + .read() + .filter(|action| !matches!(action, CameraAction(_, CameraMotion::Translate(_)))) + { + let CameraAction(lifecycle, motion) = action; + let (mut controller, mut xform, proj) = q_camera.single_mut(); + let window = q_window.single(); - match lifecycle { - Lifecycle::Start => any_started = true, - Lifecycle::Ongoing => any_ongoing = true, - Lifecycle::Stop => any_stopped = true, - } + match lifecycle { + Lifecycle::Start => any_started = true, + Lifecycle::Ongoing => any_ongoing = true, + Lifecycle::Stop => any_stopped = true, + _ => {} + } - use CameraMotion::*; - match motion { - Pan(delta) => do_pan(proj, window, *delta, &mut xform, &mut controller), - Zoom(delta) => do_zoom(window, *delta, &mut xform, &mut controller), - Orbit(delta) => do_orbit(window, *delta, *lifecycle, &mut xform, &mut controller), - Rotate(delta) => do_rotate(window, *delta, &mut xform, &mut controller), - _ => {} - } + use CameraMotion::*; + match motion { + Pan(delta) => do_pan(proj, window, *delta, &mut xform, &mut controller), + Zoom(delta) => do_zoom(window, *delta, &mut xform, &mut controller), + Orbit(delta) => do_orbit(window, *delta, *lifecycle, &mut xform, &mut controller), + Rotate(delta) => do_rotate(window, *delta, &mut xform, &mut controller), + _ => {} } } @@ -118,12 +116,12 @@ fn camera_control_kinematic( fn process_physical_inputs( mut cmd: Commands, - mut input: EventReader, + mut er_camera: EventReader, q_camera: Query<(Entity, &Velocity), With>, mut q_interp: Query<&mut InterpVelocity>, ) { - for action in input.read() { - let &InputAction(_, Action::Camera(CameraMotion::Translate(delta))) = action else { + for action in er_camera.read() { + let &CameraAction(_, CameraMotion::Translate(delta)) = action else { continue; }; @@ -156,7 +154,7 @@ fn process_physical_inputs( fn process_focus_inputs( mut cmd: Commands, r_selection: Option>, - mut input: EventReader, + mut er_selection: EventReader, mut q_camera: Query<(Entity, &CameraController, &mut Velocity)>, q_interp_vel: Query<&InterpVelocity>, mut q_interp_pos: Query<&mut InterpPosition>, @@ -166,10 +164,10 @@ fn process_focus_inputs( return; }; - if input.read().any(|action| { + if er_selection.read().any(|action| { matches!( action, - InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Focus)) + SelectionAction(Lifecycle::Stop, input::Selection::Focus) ) }) { let Ok(target) = q_global_xforms diff --git a/studio/src/debug.rs b/studio/src/debug.rs index 117a1bd..8c77a33 100644 --- a/studio/src/debug.rs +++ b/studio/src/debug.rs @@ -15,7 +15,7 @@ use space_editor::{ use crate::{ anim::SkeletalJoint, camera_controller::{CameraController, CameraMotionSystem}, - input::{Action, InputAction, Lifecycle}, + input::{self, EditorAction, Lifecycle}, is_editor, MainCamera, }; @@ -83,11 +83,13 @@ impl Plugin for DebugPlugin { } } -fn toggle_debug_mode(mut input: EventReader, mut r_debug: ResMut) { - if input - .read() - .any(|action| matches!(action, InputAction(Lifecycle::Stop, Action::ToggleDebug))) - { +fn toggle_debug_mode(mut er_editor: EventReader, mut r_debug: ResMut) { + if er_editor.read().any(|action| { + matches!( + action, + EditorAction(Lifecycle::Stop, input::Editor::ToggleDebug) + ) + }) { **r_debug = !**r_debug; } } diff --git a/studio/src/input.rs b/studio/src/input.rs index 332c7a8..0b06a61 100644 --- a/studio/src/input.rs +++ b/studio/src/input.rs @@ -2,18 +2,30 @@ use bevy::{ input::mouse::{MouseMotion, MouseWheel}, prelude::*, utils::HashSet, + window::PrimaryWindow, }; use space_editor::prelude::space_undo::UndoRedo; -use crate::is_editor; +use crate::{is_editor, math::NearlyZero}; #[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)] pub struct StudioInputSystem; +#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)] +pub struct CameraInputSystem; + +#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)] +pub struct EditorInputSystem; + +#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)] +pub struct SelectionInputSystem; + pub struct StudioInputPlugin; impl Plugin for StudioInputPlugin { fn build(&self, app: &mut App) { - app.add_event::(); + app.add_event::() + .add_event::() + .add_event::(); // Reading each frame of the mouse delta allows // to get rid of the bug that delta is counted from the last reading, @@ -28,18 +40,32 @@ impl Plugin for StudioInputPlugin { app.add_systems( PreUpdate, - ( - translate_selection_events, - translate_debug_toggle, - translate_dcc_camera_events, - translate_fps_camera_events, - send_undo_event.run_if(is_editor), - ) + (translate_dcc_camera_events, translate_fps_camera_events) .in_set(StudioInputSystem) + .in_set(CameraInputSystem) .after(bevy::input::InputSystem) .after(read_mouse_delta), ); + app.add_systems( + PreUpdate, + (translate_debug_toggle, send_undo_event.run_if(is_editor)) + .in_set(StudioInputSystem) + .in_set(EditorInputSystem) + .after(bevy::input::InputSystem) + .after(read_mouse_delta), + ); + + app.add_systems( + PreUpdate, + (apply_deferred, translate_selection_events) + .in_set(StudioInputSystem) + .in_set(SelectionInputSystem) + .after(bevy::input::InputSystem) + .after(CameraInputSystem) + .after(read_mouse_delta), + ); + app.add_systems( PostUpdate, (apply_deferred, log_input_actions).after(StudioInputSystem), @@ -47,78 +73,26 @@ impl Plugin for StudioInputPlugin { } } -/// Logs all `InputAction`s emitted, along with the input state at the time they -/// were fired, with `debug` verbosity. -fn log_input_actions( - ri_keyboard: Res>, - ri_mouse_buttons: Res>, - mut er_input_actions: EventReader, -) { - if er_input_actions.is_empty() { - return; - } - - let lmb = if ri_mouse_buttons.pressed(MouseButton::Left) { - 'X' - } else { - '-' - }; - let mmb = if ri_mouse_buttons.pressed(MouseButton::Middle) { - 'X' - } else { - '-' - }; - let rmb = if ri_mouse_buttons.pressed(MouseButton::Right) { - 'X' - } else { - '-' - }; - - use KeyCode::*; - let mod_keys = [AltLeft, AltRight, ShiftLeft, ControlLeft, ControlRight] - .into_iter() - .collect::>(); - let action_keys = [W, A, S, D, E, Q, Escape, Grave, Delete, F] - .into_iter() - .collect::>(); - - let keys_pressed = ri_keyboard.get_pressed().copied().collect::>(); - let keys_display = keys_pressed - .intersection(&mod_keys) - .map(|key| format!("{key:?}")) - .chain( - keys_pressed - .intersection(&action_keys) - .map(|key| format!("{key:?}")), - ) - .collect::>() - .join("+"); - - for action in er_input_actions.read() { - debug!( - "Mouse: [{lmb}{mmb}{rmb}] \ - Keyboard: [{keys_display}] \ - Action: [{action:.2?}]" - ); - } -} - -#[derive(Event, Clone, Copy, Debug)] -pub struct InputAction(pub Lifecycle, pub Action); +// #[derive(Event, Clone, Copy, Debug)] +// pub struct InputAction(pub Lifecycle, pub Action); #[derive(Clone, Copy, Debug)] pub enum Lifecycle { Start, Ongoing, Stop, + Cancel, } -#[derive(Clone, Copy, Debug)] -pub enum Action { - Camera(CameraMotion), - Selection(Selection), - ToggleDebug, -} +// #[derive(Clone, Copy, Debug)] +// pub enum Action { +// Camera(CameraMotion), +// Selection(Selection), +// ToggleDebug, +// } + +#[derive(Event, Clone, Copy, Debug)] +pub struct CameraAction(pub Lifecycle, pub CameraMotion); #[derive(Clone, Copy, Debug)] pub enum CameraMotion { @@ -128,11 +102,9 @@ pub enum CameraMotion { Orbit(Vec2), Rotate(Vec2), } -impl From for Action { - fn from(value: CameraMotion) -> Self { - Action::Camera(value) - } -} + +#[derive(Event, Clone, Copy, Debug)] +pub struct SelectionAction(pub Lifecycle, pub Selection); #[derive(Clone, Copy, Debug)] pub enum Selection { @@ -142,41 +114,137 @@ pub enum Selection { Focus, /// Delete the currently selected entity and its descendants Delete, + /// Select the single target under the cursor position + Single(Vec2), + /// Add or remove the target under the cursor position to the current + /// selection + Multi(Vec2), + /// Select all targets that overlap the marquee selection + Marquee(Rect), } -impl From for Action { - fn from(value: Selection) -> Self { - Action::Selection(value) - } + +#[derive(Event, Clone, Copy, Debug)] +pub struct EditorAction(pub Lifecycle, pub Editor); + +#[derive(Clone, Copy, Debug)] +pub enum Editor { + ToggleDebug, } // TODO: Make the bindings of raw device inputs to InputActions configurable -fn translate_selection_events(keyboard: Res>, mut input: EventWriter) { - if keyboard.just_pressed(KeyCode::Escape) { - input.send(InputAction(Lifecycle::Start, Selection::Clear.into())); - } else if keyboard.just_released(KeyCode::Escape) { - input.send(InputAction(Lifecycle::Stop, Selection::Clear.into())); +fn translate_selection_events( + r_mouse_motion: Res, + ri_mouse_buttons: Res>, + ri_keyboard: Res>, + mut l_pointer_drag_start: Local>, + mut l_drag_dist2: Local, + q_window: Query<&Window, With>, + mut er_camera_actions: EventReader, + mut ew_actions: EventWriter, +) { + // Keyboard actions -- clear, focus, etc. + if ri_keyboard.just_pressed(KeyCode::Escape) { + ew_actions.send(SelectionAction(Lifecycle::Start, Selection::Clear)); + } else if ri_keyboard.just_released(KeyCode::Escape) { + ew_actions.send(SelectionAction(Lifecycle::Stop, Selection::Clear)); } - if keyboard.just_pressed(KeyCode::F) { - input.send(InputAction(Lifecycle::Start, Selection::Focus.into())); - } else if keyboard.just_released(KeyCode::F) { - input.send(InputAction(Lifecycle::Stop, Selection::Focus.into())); + if ri_keyboard.just_pressed(KeyCode::F) { + ew_actions.send(SelectionAction(Lifecycle::Start, Selection::Focus)); + } else if ri_keyboard.just_released(KeyCode::F) { + ew_actions.send(SelectionAction(Lifecycle::Stop, Selection::Focus)); } - if keyboard.just_pressed(KeyCode::Delete) { - input.send(InputAction(Lifecycle::Start, Selection::Delete.into())); - } else if keyboard.just_released(KeyCode::Delete) { - input.send(InputAction(Lifecycle::Stop, Selection::Delete.into())); + if ri_keyboard.just_pressed(KeyCode::Delete) { + ew_actions.send(SelectionAction(Lifecycle::Start, Selection::Delete)); + } else if ri_keyboard.just_released(KeyCode::Delete) { + ew_actions.send(SelectionAction(Lifecycle::Stop, Selection::Delete)); + } + + // Mouse-based actions + if !er_camera_actions.is_empty() { + // We don't want to emit any mouse-triggered selection actions if + // we've already emitted some kind of camera motion this frame. This + // avoids UX jank where releasing the LMB at the end of e.g. + // orbiting the camera causes the object under the cursor to become + // selected as an unintended side-effect. + er_camera_actions.clear(); + return; + } + + let Some(mouse_pos) = q_window + .get_single() + .ok() + .and_then(|window| window.cursor_position()) + else { + return; + }; + + if ri_mouse_buttons.just_pressed(MouseButton::Left) { + *l_drag_dist2 = 0.; + *l_pointer_drag_start = Some(mouse_pos); + + ew_actions.send(SelectionAction( + Lifecycle::Start, + Selection::Marquee(Rect::from_corners(mouse_pos, mouse_pos)), + )); + } else if ri_mouse_buttons.pressed(MouseButton::Left) { + *l_drag_dist2 += r_mouse_motion.0.length_squared(); + + let Some(drag_start) = *l_pointer_drag_start else { + return; + }; + + ew_actions.send(SelectionAction( + Lifecycle::Ongoing, + Selection::Marquee(Rect::from_corners(drag_start, mouse_pos)), + )); + } else if ri_mouse_buttons.just_released(MouseButton::Left) { + *l_drag_dist2 += r_mouse_motion.0.length_squared(); + + let Some(drag_start) = l_pointer_drag_start.take() else { + return; + }; + + // If the total drag distance is nearly zero, treat this as a single-click + if l_drag_dist2.nearly_zero_within(1e-3) { + ew_actions.send(SelectionAction( + Lifecycle::Cancel, + Selection::Marquee(Rect::from_corners(drag_start, mouse_pos)), + )); + + if ri_keyboard.any_pressed([ + KeyCode::ShiftLeft, + KeyCode::ShiftRight, + KeyCode::ControlLeft, + KeyCode::ControlRight, + ]) { + ew_actions.send(SelectionAction( + Lifecycle::Stop, + Selection::Multi(mouse_pos), + )); + } else { + ew_actions.send(SelectionAction( + Lifecycle::Stop, + Selection::Single(mouse_pos), + )); + } + } else { + ew_actions.send(SelectionAction( + Lifecycle::Stop, + Selection::Marquee(Rect::from_corners(drag_start, mouse_pos)), + )); + } } } -fn translate_debug_toggle(keyboard: Res>, mut input: EventWriter) { +fn translate_debug_toggle(keyboard: Res>, mut input: EventWriter) { if keyboard.pressed(KeyCode::ControlLeft) || keyboard.pressed(KeyCode::ControlRight) { if keyboard.just_pressed(KeyCode::Grave) { - input.send(InputAction(Lifecycle::Start, Action::ToggleDebug)); + input.send(EditorAction(Lifecycle::Start, Editor::ToggleDebug)); } else if keyboard.just_released(KeyCode::Grave) { - input.send(InputAction(Lifecycle::Stop, Action::ToggleDebug)); + input.send(EditorAction(Lifecycle::Stop, Editor::ToggleDebug)); } } } @@ -185,7 +253,7 @@ fn translate_dcc_camera_events( mouse_motion: Res, mouse_buttons: Res>, keyboard: Res>, - mut input: EventWriter, + mut input: EventWriter, ) { let delta = mouse_motion.get(); // Camera Pan @@ -195,14 +263,11 @@ fn translate_dcc_camera_events( } else { Lifecycle::Ongoing }; - input.send(InputAction(lifecycle, CameraMotion::Pan(delta).into())); + input.send(CameraAction(lifecycle, CameraMotion::Pan(delta))); } // Stop Camera Panning else if mouse_buttons.just_released(MouseButton::Middle) { - input.send(InputAction( - Lifecycle::Stop, - CameraMotion::Pan(Vec2::ZERO).into(), - )); + input.send(CameraAction(Lifecycle::Stop, CameraMotion::Pan(Vec2::ZERO))); } // Camera Orbit/Zoom else if keyboard.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]) { @@ -220,18 +285,18 @@ fn translate_dcc_camera_events( CameraMotion::Zoom(delta.y) }; - input.send(InputAction(lifecycle, motion.into())); + input.send(CameraAction(lifecycle, motion)); } // Stop Camera Orbit else if mouse_buttons.just_released(MouseButton::Left) { - input.send(InputAction( + input.send(CameraAction( Lifecycle::Stop, - CameraMotion::Orbit(Vec2::ZERO).into(), + CameraMotion::Orbit(Vec2::ZERO), )); } // Stop Camera Zoom else if mouse_buttons.just_released(MouseButton::Right) { - input.send(InputAction(Lifecycle::Stop, CameraMotion::Zoom(0.).into())); + input.send(CameraAction(Lifecycle::Stop, CameraMotion::Zoom(0.))); } } // Stop Camera Orbit & Zoom @@ -240,14 +305,14 @@ fn translate_dcc_camera_events( { // Stop Camera Orbit if mouse_buttons.pressed(MouseButton::Left) { - input.send(InputAction( + input.send(CameraAction( Lifecycle::Stop, - CameraMotion::Orbit(Vec2::ZERO).into(), + CameraMotion::Orbit(Vec2::ZERO), )); } // Stop Camera Zoom else if mouse_buttons.pressed(MouseButton::Right) { - input.send(InputAction(Lifecycle::Stop, CameraMotion::Zoom(0.).into())); + input.send(CameraAction(Lifecycle::Stop, CameraMotion::Zoom(0.))); } } // Camera Rotation @@ -258,13 +323,13 @@ fn translate_dcc_camera_events( Lifecycle::Ongoing }; - input.send(InputAction(lifecycle, CameraMotion::Rotate(delta).into())); + input.send(CameraAction(lifecycle, CameraMotion::Rotate(delta))); } // Stop Camera Rotation else if mouse_buttons.just_released(MouseButton::Right) { - input.send(InputAction( + input.send(CameraAction( Lifecycle::Stop, - CameraMotion::Rotate(Vec2::ZERO).into(), + CameraMotion::Rotate(Vec2::ZERO), )); } // Alternative Camera Rotation (designed for laptop) @@ -275,13 +340,13 @@ fn translate_dcc_camera_events( Lifecycle::Ongoing }; - input.send(InputAction(lifecycle, CameraMotion::Rotate(delta).into())); + input.send(CameraAction(lifecycle, CameraMotion::Rotate(delta))); } // Stop Camera Rotation else if keyboard.just_released(KeyCode::ShiftLeft) { - input.send(InputAction( + input.send(CameraAction( Lifecycle::Stop, - CameraMotion::Rotate(Vec2::ZERO).into(), + CameraMotion::Rotate(Vec2::ZERO), )); } } @@ -289,7 +354,7 @@ fn translate_dcc_camera_events( fn translate_fps_camera_events( mut mouse_wheel: EventReader, keyboard: Res>, - mut input: EventWriter, + mut input: EventWriter, ) { // Camera Translation (FPS-like) use KeyCode::*; @@ -330,13 +395,13 @@ fn translate_fps_camera_events( Lifecycle::Ongoing }; - input.send(InputAction(lifecycle, CameraMotion::Translate(mvmt).into())); + input.send(CameraAction(lifecycle, CameraMotion::Translate(mvmt))); } // Stop Translation else if keyboard.any_just_released(wasd_keys) { - input.send(InputAction( + input.send(CameraAction( Lifecycle::Stop, - CameraMotion::Translate(Vec3::ZERO).into(), + CameraMotion::Translate(Vec3::ZERO), )); } @@ -347,10 +412,7 @@ fn translate_fps_camera_events( // let rem_y = (100. / y).abs() % 1.; // if (rem_x.is_nan() || rem_x < 1e-5_f32) && (rem_y.is_nan() || rem_y < 1e-5_f32) { - input.send(InputAction( - Lifecycle::Ongoing, - CameraMotion::Zoom(y).into(), - )); + input.send(CameraAction(Lifecycle::Ongoing, CameraMotion::Zoom(y))); // } else { // FIXME: I had high hopes for this, but the input translation from // Bevy / winit is herky-jerky-janky af. It seems to be @@ -411,3 +473,83 @@ fn send_undo_event(mut events: EventWriter, keyboard: Res>, + ri_mouse_buttons: Res>, + mut er_camera_actions: EventReader, + mut er_selection_actions: EventReader, + mut er_editor_actions: EventReader, +) { + #[derive(Clone, Copy, Debug)] + enum InputAction { + Camera(CameraAction), + Selection(SelectionAction), + Editor(EditorAction), + } + + let input_actions = er_camera_actions + .read() + .map(|&action| InputAction::Camera(action)) + .chain( + er_selection_actions + .read() + .map(|&action| InputAction::Selection(action)), + ) + .chain( + er_editor_actions + .read() + .map(|&action| InputAction::Editor(action)), + ) + .collect::>(); + + if input_actions.is_empty() { + return; + } + + let lmb = if ri_mouse_buttons.pressed(MouseButton::Left) { + 'X' + } else { + '-' + }; + let mmb = if ri_mouse_buttons.pressed(MouseButton::Middle) { + 'X' + } else { + '-' + }; + let rmb = if ri_mouse_buttons.pressed(MouseButton::Right) { + 'X' + } else { + '-' + }; + + use KeyCode::*; + let mod_keys = [AltLeft, AltRight, ShiftLeft, ControlLeft, ControlRight] + .into_iter() + .collect::>(); + let action_keys = [W, A, S, D, E, Q, Escape, Grave, Delete, F] + .into_iter() + .collect::>(); + + let keys_pressed = ri_keyboard.get_pressed().copied().collect::>(); + let keys_display = keys_pressed + .intersection(&mod_keys) + .map(|key| format!("{key:?}")) + .chain( + keys_pressed + .intersection(&action_keys) + .map(|key| format!("{key:?}")), + ) + .collect::>() + .join("+"); + + for action in input_actions { + info!( + "Mouse: [{lmb}{mmb}{rmb}] \ + Keyboard: [{keys_display}] \ + Action: [{action:.2?}]" + ); + } +} diff --git a/studio/src/interaction/selection.rs b/studio/src/interaction/selection.rs index 405eb4c..7fa7ee5 100644 --- a/studio/src/interaction/selection.rs +++ b/studio/src/interaction/selection.rs @@ -4,12 +4,14 @@ use bevy::{prelude::*, render::mesh::skinning::SkinnedMesh}; use bevy_mod_outline::{ AutoGenerateOutlineNormalsPlugin, OutlineBundle, OutlinePlugin, OutlineVolume, }; -use bevy_mod_picking::{prelude::Pointer, selection::Select, PickableBundle}; +use bevy_mod_picking::PickableBundle; +use bevy_mod_raycast::prelude::{Raycast, RaycastSettings}; use space_editor::prelude::space_undo::{self, NewChange, RemovedEntity}; use crate::{ - input::{self, Action, InputAction, Lifecycle}, + input::{self, Lifecycle, SelectionAction}, scene::{SceneElement, SceneSpawnerSystem, SceneState}, + MainCamera, }; pub struct SelectionPlugin; @@ -122,29 +124,62 @@ fn tag_scene_elements( fn update_selection( mut cmd: Commands, - // FIXME: These events aren't aware of our input actions, so we receive a - // selection event when LMB is released after orbiting the camera. - // Should drop bevy_mod_picking's Selection plugin/feature and just - // use our own conditional click listener. - mut ev_select: EventReader>, - mut ev_input: EventReader, + mut er_selection: EventReader, + mut raycast: Raycast, q_parents: Query<&Parent>, - q_selectable: Query<&Selectable>, + q_selectables: Query<&Selectable>, + q_pickables: Query<&bevy_mod_picking::prelude::Pickable>, + q_camera: Query<(&Camera, &GlobalTransform), With>, ) { - if let Some(event) = ev_select.read().last() { - // Find the nearest Selectable parent of the target entity - if let Some(ent) = nearest_selectable_parent(event.target, &q_parents, &q_selectable) { - cmd.insert_resource(Selection(ent)); - } else { - cmd.remove_resource::(); + for SelectionAction(lifecycle, action) in er_selection.read() { + match (lifecycle, action) { + (_, input::Selection::Clear) => { + cmd.remove_resource::(); + } + (_, input::Selection::Single(mouse_pos)) | (_, input::Selection::Multi(mouse_pos)) => { + let is_multi = matches!(action, input::Selection::Multi(_)); + + // Raycast to find the pickable entity under the cursor + let (cam, cam_xform) = q_camera.single(); + let ray = cam.viewport_to_world(cam_xform, *mouse_pos).unwrap(); + let hits = raycast.cast_ray( + ray.into(), + &RaycastSettings { + filter: &|ent: Entity| q_pickables.contains(ent), + ..default() + }, + ); + + if hits.is_empty() && !is_multi { + cmd.remove_resource::(); + continue; + } + + let (ent, _) = &hits[0]; + if let Some(ent) = nearest_selectable_parent(*ent, &q_parents, &q_selectables) { + cmd.insert_resource(Selection(ent)); + } else { + cmd.remove_resource::(); + } + } + (Lifecycle::Ongoing, input::Selection::Marquee(Rect { min, max })) => { + // TODO + // let center = (*max - *min) / 2.; + // let width = max.x - min.x; + // let height = max.y - min.y; + + // gizmos.rect_2d( + // center, + // 0., + // vec2(width, height), + // Color::rgba(1., 1., 0., 0.4), + // ); + } + (Lifecycle::Stop, input::Selection::Marquee(Rect { min, max })) => { + // TODO + } + _ => {} } - } else if ev_input.read().any(|action| { - matches!( - action, - InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Clear)) - ) - }) { - cmd.remove_resource::(); } } @@ -171,14 +206,14 @@ pub struct MarkedForDeletion; fn process_delete_inputs( mut cmd: Commands, - mut er_input: EventReader, + mut er_selection: EventReader, mut ew_changes: EventWriter, r_selection: Res, ) { - if er_input.read().any(|action| { + if er_selection.read().any(|action| { matches!( action, - InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Delete)) + SelectionAction(Lifecycle::Stop, input::Selection::Delete) ) }) { let entity = **r_selection;