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
This commit is contained in:
Dhruv 2023-07-02 13:03:51 +05:30 committed by Keavon Chambers
parent 9c2520111d
commit 92e10b6610
3 changed files with 119 additions and 16 deletions

View File

@ -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. }),

View File

@ -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<Vec<LayerId>, 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));
}
}
}
}
}
}

View File

@ -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<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
DragStart,
Delete,
NudgeSelectedPoints,
Enter,
),
Dragging => actions!(PathToolMessageDiscriminant;
InsertPoint,
@ -87,6 +95,13 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> 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<OpposingHandleLengths>,
drag_box_overlay_layer: Option<Vec<LayerId>>,
}
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<Message>) {
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 });