From 92e10b66106371596c74c1e9eb61752920a2133d Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sun, 2 Jul 2023 13:03:51 +0530 Subject: [PATCH] Add Box Selection to Path Tool (#1316) * [WIP]feat: add bounding box to path tool * feat: draw bounding box for path tool - register Enter key for finalizing bounding box position - add (WIP)func to select all in bounding box area - add hint data for new state * fix: re-render shape overlays after selection * fix: maintain existing point selection with Shift key * feat: add Shift key support for Enter Key state * fix: set apt name for Enter state's keybind * refactor: remove unnecessary code * refactor: correct hints and remove unneeded code - correct DrawingBox state's footer hints - remove PathToolData's quad and bbox methods * refactor: remove duplicate mouse position vectors --- .../messages/input_mapper/default_mapping.rs | 3 + .../tool/common_functionality/shape_editor.rs | 31 +++++- .../messages/tool/tool_messages/path_tool.rs | 101 +++++++++++++++--- 3 files changed, 119 insertions(+), 16 deletions(-) diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index d3cfccdf..d8892b28 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -179,6 +179,9 @@ pub fn default_mapping() -> Mapping { entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete), entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete), entry!(KeyUp(Lmb); action_dispatch=PathToolMessage::DragStop { shift_mirror_distance: Shift }), + entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { + add_to_selection: Shift + }), entry!(DoubleClick; action_dispatch=PathToolMessage::InsertPoint), entry!(KeyDown(ArrowRight); action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: 0. }), entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }), diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 977d7c12..3adcc8e2 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -24,6 +24,9 @@ impl SelectedLayerState { pub fn deselect_point(&mut self, point: ManipulatorPointId) { self.selected_points.remove(&point); } + pub fn clear_points(&mut self) { + self.selected_points.clear(); + } } pub type SelectedShapeState = HashMap, SelectedLayerState>; #[derive(Debug, Default)] @@ -97,10 +100,6 @@ impl ShapeState { return None; } } - - // Deselect all points if no nearby point - self.deselect_all(); - None } @@ -621,4 +620,28 @@ impl ShapeState { } false } + + pub fn select_all_in_quad(&mut self, document: &Document, quad: [DVec2; 2], clear_selection: bool) { + for (layer_path, state) in &mut self.selected_shape_state { + if clear_selection { + state.clear_points() + } + + let Ok(layer) = document.layer(&layer_path) else {continue}; + let Some(vector_data) = layer.as_vector_data() else {continue}; + + let transform = document.multiply_transforms(layer_path).unwrap_or_default(); + + for manipulator_group in vector_data.manipulator_groups() { + for selected_type in [SelectedType::Anchor, SelectedType::InHandle, SelectedType::OutHandle] { + let Some(position) = selected_type.get_position(manipulator_group) else {continue}; + let transformed_position = transform.transform_point2(position); + + if quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all() { + state.select_point(ManipulatorPointId::new(manipulator_group.id, selected_type)); + } + } + } + } + } } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 26bd5ea1..da97f309 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1,3 +1,5 @@ +use std::vec; + use crate::consts::{DRAG_THRESHOLD, SELECTION_THRESHOLD, SELECTION_TOLERANCE}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion}; @@ -6,12 +8,14 @@ use crate::messages::prelude::*; use crate::messages::tool::common_functionality::overlay_renderer::OverlayRenderer; use crate::messages::tool::common_functionality::shape_editor::{ManipulatorPointInfo, OpposingHandleLengths, ShapeState}; use crate::messages::tool::common_functionality::snapping::SnapManager; +use crate::messages::tool::common_functionality::transformation_cage::{add_bounding_box, transform_from_box}; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, HintData, HintGroup, HintInfo, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use document_legacy::intersection::Quad; +use document_legacy::{LayerId, Operation}; use graphene_core::vector::{ManipulatorPointId, SelectedType}; -use glam::DVec2; +use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; #[derive(Default)] @@ -40,6 +44,9 @@ pub enum PathToolMessage { DragStop { shift_mirror_distance: Key, }, + Enter { + add_to_selection: Key, + }, InsertPoint, NudgeSelectedPoints { delta_x: f64, @@ -80,6 +87,7 @@ impl<'a> MessageHandler> for PathToo DragStart, Delete, NudgeSelectedPoints, + Enter, ), Dragging => actions!(PathToolMessageDiscriminant; InsertPoint, @@ -87,6 +95,13 @@ impl<'a> MessageHandler> for PathToo PointerMove, Delete, ), + DrawingBox => actions!(PathToolMessageDiscriminant; + InsertPoint, + DragStop, + PointerMove, + Delete, + Enter + ), } } } @@ -107,6 +122,7 @@ enum PathToolFsmState { #[default] Ready, Dragging, + DrawingBox, } #[derive(Default)] @@ -116,6 +132,7 @@ struct PathToolData { previous_mouse_position: DVec2, alt_debounce: bool, opposing_handle_lengths: Option, + drag_box_overlay_layer: Option>, } impl PathToolData { @@ -237,15 +254,29 @@ impl Fsm for PathToolFsmState { return PathToolFsmState::Dragging; } } else { - // Clear the previous selection if we didn't find anything - if !input.keyboard.get(shift_pressed as usize) { - responses.add(DocumentMessage::DeselectAllLayers); - } + // an empty intersection means that the user is drawing a box + tool_data.drag_start_pos = input.mouse.position; + tool_data.previous_mouse_position = input.mouse.position; + + tool_data.drag_box_overlay_layer = Some(add_bounding_box(responses)); + return PathToolFsmState::DrawingBox; } PathToolFsmState::Ready } } + (PathToolFsmState::DrawingBox, PathToolMessage::PointerMove { .. }) => { + tool_data.previous_mouse_position = input.mouse.position; + + responses.add_front(DocumentMessage::Overlays( + Operation::SetLayerTransformInViewport { + path: tool_data.drag_box_overlay_layer.clone().unwrap(), + transform: transform_from_box(tool_data.drag_start_pos, tool_data.previous_mouse_position, DAffine2::IDENTITY).to_cols_array(), + } + .into(), + )); + PathToolFsmState::DrawingBox + } // Dragging ( @@ -284,12 +315,50 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Dragging } + (PathToolFsmState::DrawingBox, PathToolMessage::Enter { add_to_selection }) => { + let shift_pressed = input.keyboard.get(add_to_selection as usize); + + if tool_data.drag_start_pos == tool_data.previous_mouse_position { + responses.add(DocumentMessage::DeselectAllLayers); + } else { + shape_editor.select_all_in_quad(&document.document_legacy, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed); + tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses); + }; + + responses.add_front(DocumentMessage::Overlays( + Operation::DeleteLayer { + path: tool_data.drag_box_overlay_layer.take().unwrap(), + } + .into(), + )); + PathToolFsmState::Ready + } + // Mouse up + (PathToolFsmState::DrawingBox, PathToolMessage::DragStop { shift_mirror_distance }) => { + let shift_pressed = input.keyboard.get(shift_mirror_distance as usize); + + if tool_data.drag_start_pos == tool_data.previous_mouse_position { + responses.add(DocumentMessage::DeselectAllLayers); + } else { + shape_editor.select_all_in_quad(&document.document_legacy, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed); + tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses); + }; + + responses.add_front(DocumentMessage::Overlays( + Operation::DeleteLayer { + path: tool_data.drag_box_overlay_layer.take().unwrap(), + } + .into(), + )); + return PathToolFsmState::Ready; + } (_, PathToolMessage::DragStop { shift_mirror_distance }) => { + let shift_pressed = input.keyboard.get(shift_mirror_distance as usize); + let nearest_point = shape_editor .find_nearest_point_indices(&document.document_legacy, input.mouse.position, SELECTION_THRESHOLD) .map(|(_, nearest_point)| nearest_point); - let shift_pressed = input.keyboard.get(shift_mirror_distance as usize); shape_editor.delete_selected_handles_with_zero_length(&document.document_legacy, &tool_data.opposing_handle_lengths, responses); @@ -304,6 +373,7 @@ impl Fsm for PathToolFsmState { tool_data.snap_manager.cleanup(responses); PathToolFsmState::Ready } + // Delete key (_, PathToolMessage::Delete) => { // Delete the selected points and clean up overlays @@ -342,6 +412,7 @@ impl Fsm for PathToolFsmState { shape_editor.move_selected_points(&document.document_legacy, (delta_x, delta_y).into(), true, responses); PathToolFsmState::Ready } + (_, _) => PathToolFsmState::Ready, } } else { self @@ -349,17 +420,23 @@ impl Fsm for PathToolFsmState { } fn update_hints(&self, responses: &mut VecDeque) { + let general_hint_data = HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus()]), + HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]), + HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]), + HintGroup(vec![HintInfo::keys([Key::KeyG, Key::KeyR, Key::KeyS], "Grab/Rotate/Scale Selected")]), + ]); + let hint_data = match self { - PathToolFsmState::Ready => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus()]), - HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]), - HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]), - HintGroup(vec![HintInfo::keys([Key::KeyG, Key::KeyR, Key::KeyS], "Grab/Rotate/Scale Selected")]), - ]), + PathToolFsmState::Ready => general_hint_data, PathToolFsmState::Dragging => HintData(vec![HintGroup(vec![ HintInfo::keys([Key::Alt], "Split/Align Handles (Toggle)"), HintInfo::keys([Key::Shift], "Share Lengths of Aligned Handles"), ])]), + PathToolFsmState::DrawingBox => HintData(vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), + HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus(), + ])]), }; responses.add(FrontendMessage::UpdateInputHints { hint_data });