diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 5b2c759aeb..a3c50173e5 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -210,7 +210,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }), entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }), entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }), - entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PathToolMessage::PointerMove { alt: Alt, shift: Shift }), + entry!(PointerMove; refresh_keys=[Alt, Shift, Space], action_dispatch=PathToolMessage::PointerMove { alt: Alt, shift: Shift, move_anchor_and_handles: Space}), entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete), entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors), entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints), diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index dd521f14ea..796c141297 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -6,6 +6,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node use crate::messages::prelude::*; use bezier_rs::{Bezier, BezierHandles, TValue}; +use dyn_any::DynAny; use graphene_core::transform::Transform; use graphene_core::vector::{ManipulatorPointId, PointId, VectorData, VectorModificationType}; @@ -1061,6 +1062,52 @@ impl ShapeState { } } + /// Selects handles and anchor connected to current handle + pub fn select_handles_and_anchor_connected_to_current_handle(&mut self, network_interface: &NodeNetworkInterface) { + let mut points_to_select: Vec<(LayerNodeIdentifier, Option, Option)> = Vec::new(); + + for &layer in self.selected_shape_state.keys() { + let Some(vector_data) = network_interface.compute_modified_vector(layer) else { + continue; + }; + + for point in self.selected_points() { + if let Some(_) = point.as_anchor() { + continue; + } + + let anchor = point.get_anchor(&vector_data); + if let Some(handles) = point.get_handle_pair(&vector_data) { + points_to_select.push((layer, anchor, Some(handles[1].to_manipulator_point()))); + } else { + points_to_select.push((layer, anchor, None)); + } + } + } + + for (layer, anchor, handle) in points_to_select { + if let Some(state) = self.selected_shape_state.get_mut(&layer) { + if let Some(anchor) = anchor { + state.select_point(ManipulatorPointId::Anchor(anchor)); + } + if let Some(handle) = handle { + state.select_point(handle); + } + } + } + } + + pub fn select_points_by_manipulator_id(&mut self, points: &Vec) { + let layers_to_modify: Vec<_> = self.selected_shape_state.keys().cloned().collect(); + + for layer in layers_to_modify { + if let Some(state) = self.selected_shape_state.get_mut(&layer) { + for point in points { + state.select_point(*point); + } + } + } + } /// Converts a nearby clicked anchor point's handles between sharp (zero-length handles) and smooth (pulled-apart handle(s)). /// If both handles aren't zero-length, they are set that. If both are zero-length, they are stretched apart by a reasonable amount. /// This can can be activated by double clicking on an anchor with the Path tool. diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index c27114307b..e759447050 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -58,10 +58,12 @@ pub enum PathToolMessage { PointerMove { alt: Key, shift: Key, + move_anchor_and_handles: Key, }, PointerOutsideViewport { alt: Key, shift: Key, + move_anchor_and_handles: Key, }, RightClick, SelectAllAnchors, @@ -262,9 +264,20 @@ struct PathToolData { snap_cache: SnapCache, double_click_handled: bool, auto_panning: AutoPanning, + saved_points_before_anchor_select_toggle: Vec, + select_anchor_toggled: bool, } impl PathToolData { + fn save_points_before_anchor_toggle(&mut self, points: Vec) -> PathToolFsmState { + self.saved_points_before_anchor_select_toggle = points; + PathToolFsmState::Dragging + } + + fn remove_saved_points(&mut self) { + self.saved_points_before_anchor_select_toggle.clear(); + } + fn start_insertion(&mut self, responses: &mut VecDeque, segment: ClosestSegment) -> PathToolFsmState { if self.segment.is_some() { warn!("Segment was `Some(..)` before `start_insertion`") @@ -521,17 +534,37 @@ impl Fsm for PathToolFsmState { let direct_insert_without_sliding = input.keyboard.get(ctrl as usize); tool_data.mouse_down(shape_editor, document, input, responses, add_to_selection, direct_insert_without_sliding) } - (PathToolFsmState::DrawingBox, PathToolMessage::PointerMove { alt, shift }) => { + (PathToolFsmState::DrawingBox, PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }) => { tool_data.previous_mouse_position = input.mouse.position; responses.add(OverlaysMessage::Draw); // Auto-panning - let messages = [PathToolMessage::PointerOutsideViewport { alt, shift }.into(), PathToolMessage::PointerMove { alt, shift }.into()]; + let messages = [ + PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }.into(), + PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }.into(), + ]; tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); PathToolFsmState::DrawingBox } - (PathToolFsmState::Dragging, PathToolMessage::PointerMove { alt, shift }) => { + (PathToolFsmState::Dragging, PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }) => { + let anchor_and_handle_toggled = input.keyboard.get(move_anchor_and_handles as usize); + let initial_press = anchor_and_handle_toggled && !tool_data.select_anchor_toggled; + let released_from_toggle = tool_data.select_anchor_toggled && !anchor_and_handle_toggled; + + if initial_press { + responses.add(PathToolMessage::SelectedPointUpdated); + tool_data.select_anchor_toggled = true; + tool_data.save_points_before_anchor_toggle(shape_editor.selected_points().cloned().collect()); + shape_editor.select_handles_and_anchor_connected_to_current_handle(&document.network_interface); + } else if released_from_toggle { + responses.add(PathToolMessage::SelectedPointUpdated); + tool_data.select_anchor_toggled = false; + shape_editor.deselect_all_points(); + shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle); + tool_data.remove_saved_points(); + } + let alt_state = input.keyboard.get(alt as usize); let shift_state = input.keyboard.get(shift as usize); if !tool_data.update_colinear(shift_state, alt_state, shape_editor, document, responses) { @@ -539,7 +572,10 @@ impl Fsm for PathToolFsmState { } // Auto-panning - let messages = [PathToolMessage::PointerOutsideViewport { alt, shift }.into(), PathToolMessage::PointerMove { alt, shift }.into()]; + let messages = [ + PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }.into(), + PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }.into(), + ]; tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); PathToolFsmState::Dragging @@ -561,9 +597,12 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Dragging } - (state, PathToolMessage::PointerOutsideViewport { alt, shift }) => { + (state, PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }) => { // Auto-panning - let messages = [PathToolMessage::PointerOutsideViewport { alt, shift }.into(), PathToolMessage::PointerMove { alt, shift }.into()]; + let messages = [ + PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }.into(), + PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }.into(), + ]; tool_data.auto_panning.stop(&messages, responses); state @@ -605,6 +644,13 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } (_, PathToolMessage::DragStop { equidistant }) => { + if tool_data.select_anchor_toggled { + shape_editor.deselect_all_points(); + shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle); + tool_data.remove_saved_points(); + tool_data.select_anchor_toggled = false; + } + let equidistant = input.keyboard.get(equidistant as usize); let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD); @@ -731,6 +777,7 @@ impl Fsm for PathToolFsmState { HintInfo::keys([Key::Shift], "Equidistant Handles"), // TODO: Add "Snap 15°" modifier with the "Shift" key (only when a handle is being dragged). // TODO: Add "Lock Angle" modifier with the "Ctrl" key (only when a handle is being dragged). + HintInfo::keys([Key::Space], "Drag anchor"), ]), ]), PathToolFsmState::DrawingBox => HintData(vec![