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:
parent
9c2520111d
commit
92e10b6610
|
|
@ -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. }),
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue