From 018e9839f800bedafcaab7a2b2b8f83dec8bea6e Mon Sep 17 00:00:00 2001 From: Daragh Date: Wed, 30 Oct 2024 09:48:20 +0000 Subject: [PATCH] Add Path tool support for the Tab key swapping to dragging the opposite handle (#2058) * feat: tab alternates between handles * fix: handle hints, remove anchor to handle switch Added specific handle hints, Can no longer switch to handle if just anchor is selected typo fix * fix: no longer deselect on esc/rclick * feat: hides cursor when switching A pointerlock implementation would be ideal in the future to keep the screen from panning, * fix: tidy up dynamic tool hints switch colinear to V * fix: can no longer hide cursor if anchor selected remove debug statement * fix: clippy * Solve some issues and remap V to C to toggle colinear * Cleanup + change equidistant key from Shift to Alt --------- Co-authored-by: Keavon Chambers --- .../messages/input_mapper/input_mappings.rs | 11 +- .../tool/common_functionality/shape_editor.rs | 83 +++++- .../messages/tool/tool_messages/path_tool.rs | 279 ++++++++++++------ .../tool/tool_messages/select_tool.rs | 6 +- node-graph/gcore/src/vector/vector_data.rs | 2 +- 5 files changed, 280 insertions(+), 101 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index a3c50173..654310be 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -93,7 +93,7 @@ pub fn input_mappings() -> Mapping { // // SelectToolMessage entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Control, center: Alt, duplicate: Alt })), - entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { add_to_selection: Shift, select_deepest: Accel }), + entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { extend_selection: Shift, select_deepest: Accel }), entry!(KeyUp(MouseLeft); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Shift }), entry!(KeyDown(Enter); action_dispatch=SelectToolMessage::Enter), entry!(DoubleClick(MouseButton::Left); action_dispatch=SelectToolMessage::EditLayer), @@ -204,19 +204,20 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath), entry!(KeyDown(Delete); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath), entry!(KeyDown(Backspace); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath), - entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { ctrl: Control, shift: Shift }), + entry!(KeyDown(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles), + entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift }), entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick), entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape), 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, Space], action_dispatch=PathToolMessage::PointerMove { alt: Alt, shift: Shift, move_anchor_and_handles: Space}), + entry!(PointerMove; refresh_keys=[KeyC, Shift, Alt, Space], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_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), entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete), - entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { equidistant: Shift }), - entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { add_to_selection: Shift }), + entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { extend_selection: Shift }), + entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { extend_selection: Shift }), entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSmoothSharp), 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 8dd0087f..774dff58 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -4,6 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc use crate::messages::portfolio::document::utility_types::misc::{GeometrySnapSource, SnapSource}; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::prelude::*; +use crate::messages::tool::tool_messages::path_tool::PointSelectState; use bezier_rs::{Bezier, BezierHandles, TValue}; use graphene_core::transform::Transform; @@ -12,8 +13,9 @@ use graphene_core::vector::{ManipulatorPointId, PointId, VectorData, VectorModif use glam::DVec2; use graphene_std::vector::{HandleId, SegmentId}; -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)] pub enum ManipulatorAngle { + #[default] Colinear, Free, Mixed, @@ -161,9 +163,9 @@ impl ClosestSegment { midpoint } - pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque, add_to_selection: bool) { + pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque, extend_selection: bool) { let id = self.adjusted_insert(responses); - shape_editor.select_anchor_point_by_id(self.layer, id, add_to_selection) + shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection) } } @@ -221,7 +223,7 @@ impl ShapeState { /// Select/deselect the first point within the selection threshold. /// Returns a tuple of the points if found and the offset, or `None` otherwise. - pub fn change_point_selection(&mut self, network_interface: &NodeNetworkInterface, mouse_position: DVec2, select_threshold: f64, add_to_selection: bool) -> Option> { + pub fn change_point_selection(&mut self, network_interface: &NodeNetworkInterface, mouse_position: DVec2, select_threshold: f64, extend_selection: bool) -> Option> { if self.selected_shape_state.is_empty() { return None; } @@ -234,14 +236,14 @@ impl ShapeState { let already_selected = selected_shape_state.is_selected(manipulator_point_id); // Should we select or deselect the point? - let new_selected = if already_selected { !add_to_selection } else { true }; + let new_selected = if already_selected { !extend_selection } else { true }; // Offset to snap the selected point to the cursor let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position); // This is selecting the manipulator only for now, next to generalize to points if new_selected { - let retain_existing_selection = add_to_selection || already_selected; + let retain_existing_selection = extend_selection || already_selected; if !retain_existing_selection { self.deselect_all_points(); } @@ -267,8 +269,8 @@ impl ShapeState { None } - pub fn select_anchor_point_by_id(&mut self, layer: LayerNodeIdentifier, id: PointId, add_to_selection: bool) { - if !add_to_selection { + pub fn select_anchor_point_by_id(&mut self, layer: LayerNodeIdentifier, id: PointId, extend_selection: bool) { + if !extend_selection { self.deselect_all_points(); } let point = ManipulatorPointId::Anchor(id); @@ -1060,6 +1062,71 @@ impl ShapeState { _ => self.sorted_selected_layers(network_interface.document_metadata()).find_map(closest_seg), } } + pub fn get_dragging_state(&self, network_interface: &NodeNetworkInterface) -> PointSelectState { + 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 point.as_anchor().is_some() { + return PointSelectState::Anchor; + } + if point.get_handle_pair(&vector_data).is_some() { + return PointSelectState::HandleWithPair; + } + } + } + PointSelectState::HandleNoPair + } + + /// Returns true if at least one handle with pair is selected + pub fn handle_with_pair_selected(&mut self, network_interface: &NodeNetworkInterface) -> bool { + 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 point.as_anchor().is_some() { + return false; + } + if point.get_handle_pair(&vector_data).is_some() { + return true; + } + } + } + + false + } + + /// Alternate selected handles to mirrors + pub fn alternate_selected_handles(&mut self, network_interface: &NodeNetworkInterface) { + let mut handles_to_update = 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 point.as_anchor().is_some() { + continue; + } + if let Some(handles) = point.get_handle_pair(&vector_data) { + // handle[0] is selected, handle[1] is opposite / mirror handle + handles_to_update.push((layer, handles[0].to_manipulator_point(), handles[1].to_manipulator_point())); + } + } + } + + for (layer, handle_to_deselect, handle_to_select) in handles_to_update { + if let Some(state) = self.selected_shape_state.get_mut(&layer) { + let points = &state.selected_points; + let both_selected = points.contains(&handle_to_deselect) && points.contains(&handle_to_select); + if both_selected { + continue; + } + + state.deselect_point(handle_to_deselect); + state.select_point(handle_to_select); + } + } + } /// Selects handles and anchor connected to current handle pub fn select_handles_and_anchor_connected_to_current_handle(&mut self, network_interface: &NodeNetworkInterface) { diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index e7594470..074af713 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -34,10 +34,10 @@ pub enum PathToolMessage { Delete, DeleteAndBreakPath, DragStop { - equidistant: Key, + extend_selection: Key, }, Enter { - add_to_selection: Key, + extend_selection: Key, }, Escape, FlipSmoothSharp, @@ -48,22 +48,22 @@ pub enum PathToolMessage { ManipulatorMakeHandlesFree, ManipulatorMakeHandlesColinear, MouseDown { - ctrl: Key, - shift: Key, + direct_insert_without_sliding: Key, + extend_selection: Key, }, NudgeSelectedPoints { delta_x: f64, delta_y: f64, }, PointerMove { - alt: Key, - shift: Key, - move_anchor_and_handles: Key, + equidistant: Key, + toggle_colinear: Key, + move_anchor_with_handles: Key, }, PointerOutsideViewport { - alt: Key, - shift: Key, - move_anchor_and_handles: Key, + equidistant: Key, + toggle_colinear: Key, + move_anchor_with_handles: Key, }, RightClick, SelectAllAnchors, @@ -74,6 +74,7 @@ pub enum PathToolMessage { SelectedPointYChanged { new_y: f64, }, + SwapSelectedHandles, } impl ToolMetadata for PathTool { @@ -170,7 +171,19 @@ impl<'a> MessageHandler> for PathToo fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated); - self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true); + match message { + ToolMessage::Path(PathToolMessage::SwapSelectedHandles) => { + if tool_data.shape_editor.handle_with_pair_selected(&tool_data.document.network_interface) { + tool_data.shape_editor.alternate_selected_handles(&tool_data.document.network_interface); + responses.add(PathToolMessage::SelectedPointUpdated); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::None }); + responses.add(OverlaysMessage::Draw); + } + } + _ => { + self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true); + } + } if updating_point { self.send_layout(responses, LayoutTarget::ToolOptions); @@ -191,7 +204,7 @@ impl<'a> MessageHandler> for PathToo BreakPath, DeleteAndBreakPath, ), - PathToolFsmState::Dragging => actions!(PathToolMessageDiscriminant; + PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant; Escape, RightClick, FlipSmoothSharp, @@ -200,6 +213,7 @@ impl<'a> MessageHandler> for PathToo Delete, BreakPath, DeleteAndBreakPath, + SwapSelectedHandles, ), PathToolFsmState::DrawingBox => actions!(PathToolMessageDiscriminant; FlipSmoothSharp, @@ -235,19 +249,32 @@ impl ToolTransition for PathTool { } } } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct DraggingState { + point_select_state: PointSelectState, + colinear: ManipulatorAngle, +} + +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] +pub enum PointSelectState { + HandleWithPair, + #[default] + HandleNoPair, + Anchor, +} #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum PathToolFsmState { #[default] Ready, - Dragging, + Dragging(DraggingState), DrawingBox, InsertPoint, } enum InsertEndKind { Abort, - Add { shift: bool }, + Add { extend_selection: bool }, } #[derive(Default)] @@ -255,7 +282,7 @@ struct PathToolData { snap_manager: SnapManager, drag_start_pos: DVec2, previous_mouse_position: DVec2, - alt_debounce: bool, + toggle_colinear_debounce: bool, opposing_handle_lengths: Option, /// Describes information about the selected point(s), if any, across one or multiple shapes and manipulator point types (anchor or handle). /// The available information varies depending on whether `None`, `One`, or `Multiple` points are currently selected. @@ -266,12 +293,13 @@ struct PathToolData { auto_panning: AutoPanning, saved_points_before_anchor_select_toggle: Vec, select_anchor_toggled: bool, + dragging_state: DraggingState, } impl PathToolData { fn save_points_before_anchor_toggle(&mut self, points: Vec) -> PathToolFsmState { self.saved_points_before_anchor_select_toggle = points; - PathToolFsmState::Dragging + PathToolFsmState::Dragging(self.dragging_state) } fn remove_saved_points(&mut self) { @@ -308,8 +336,8 @@ impl PathToolData { warn!("Segment was `None` before `end_insertion`") } Some(closed_segment) => { - if let InsertEndKind::Add { shift } = kind { - closed_segment.adjusted_insert_and_select(shape_editor, responses, shift); + if let InsertEndKind::Add { extend_selection } = kind { + closed_segment.adjusted_insert_and_select(shape_editor, responses, extend_selection); commit_transaction = true; } } @@ -331,7 +359,7 @@ impl PathToolData { document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, - add_to_selection: bool, + extend_selection: bool, direct_insert_without_sliding: bool, ) -> PathToolFsmState { self.double_click_handled = false; @@ -340,7 +368,7 @@ impl PathToolData { self.drag_start_pos = input.mouse.position; // Select the first point within the threshold (in pixels) - if let Some(selected_points) = shape_editor.change_point_selection(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD, add_to_selection) { + if let Some(selected_points) = shape_editor.change_point_selection(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD, extend_selection) { responses.add(DocumentMessage::StartTransaction); if let Some(selected_points) = selected_points { @@ -348,21 +376,21 @@ impl PathToolData { self.start_dragging_point(selected_points, input, document, shape_editor); responses.add(OverlaysMessage::Draw); } - PathToolFsmState::Dragging + PathToolFsmState::Dragging(self.dragging_state) } // We didn't find a point nearby, so now we'll try to add a point into the closest path segment else if let Some(closed_segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE) { responses.add(DocumentMessage::StartTransaction); if direct_insert_without_sliding { self.start_insertion(responses, closed_segment); - self.end_insertion(shape_editor, responses, InsertEndKind::Add { shift: add_to_selection }) + self.end_insertion(shape_editor, responses, InsertEndKind::Add { extend_selection }) } else { self.start_insertion(responses, closed_segment) } } // We didn't find a segment path, so consider selecting the nearest shape instead else if let Some(layer) = document.click(input) { - if add_to_selection { + if extend_selection { responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] }); } else { responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); @@ -372,7 +400,8 @@ impl PathToolData { shape_editor.select_connected_anchors(document, layer, input.mouse.position); responses.add(DocumentMessage::StartTransaction); - PathToolFsmState::Dragging + + PathToolFsmState::Dragging(self.dragging_state) } // Start drawing a box else { @@ -413,9 +442,9 @@ impl PathToolData { self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position - selected_points.offset); } - fn update_colinear(&mut self, shift: bool, alt: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque) -> bool { - // Check if the alt key has just been pressed - if alt && !self.alt_debounce { + fn update_colinear(&mut self, equidistant: bool, toggle_colinear: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque) -> bool { + // Check if the toggle_colinear key has just been pressed + if toggle_colinear && !self.toggle_colinear_debounce { self.opposing_handle_lengths = None; let colinear = self.selection_status.angle().map_or(false, |angle| match angle { ManipulatorAngle::Colinear => true, @@ -427,12 +456,12 @@ impl PathToolData { } else { shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document); } - self.alt_debounce = true; + self.toggle_colinear_debounce = true; return true; } - self.alt_debounce = alt; + self.toggle_colinear_debounce = toggle_colinear; - if shift && self.opposing_handle_lengths.is_none() { + if equidistant && self.opposing_handle_lengths.is_none() { self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document)); } false @@ -481,7 +510,7 @@ impl Fsm for PathToolFsmState { overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position]), Some(&("#".to_string() + &fill_color))); } - Self::Dragging => { + Self::Dragging(_) => { tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); } Self::InsertPoint => { @@ -502,11 +531,10 @@ impl Fsm for PathToolFsmState { } // `Self::InsertPoint` case: - (Self::InsertPoint, PathToolMessage::MouseDown { .. } | PathToolMessage::Enter { .. }) => { + (Self::InsertPoint, PathToolMessage::MouseDown { extend_selection, .. } | PathToolMessage::Enter { extend_selection }) => { tool_data.double_click_handled = true; - // TODO: Don't use `Key::Shift` directly, instead take it as a variable from the input mappings list like in all other places - let shift = input.keyboard.get(Key::Shift as usize); - tool_data.end_insertion(shape_editor, responses, InsertEndKind::Add { shift }) + let extend_selection = input.keyboard.get(extend_selection as usize); + tool_data.end_insertion(shape_editor, responses, InsertEndKind::Add { extend_selection }) } (Self::InsertPoint, PathToolMessage::PointerMove { .. }) => { responses.add(OverlaysMessage::Draw); @@ -529,26 +557,56 @@ impl Fsm for PathToolFsmState { tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort) } // Mouse down - (_, PathToolMessage::MouseDown { ctrl, shift }) => { - let add_to_selection = input.keyboard.get(shift as usize); - 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) + ( + _, + PathToolMessage::MouseDown { + direct_insert_without_sliding, + extend_selection, + }, + ) => { + let extend_selection = input.keyboard.get(extend_selection as usize); + let direct_insert_without_sliding = input.keyboard.get(direct_insert_without_sliding as usize); + tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, direct_insert_without_sliding) } - (PathToolFsmState::DrawingBox, PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }) => { + ( + PathToolFsmState::DrawingBox, + PathToolMessage::PointerMove { + equidistant, + toggle_colinear, + move_anchor_with_handles, + }, + ) => { tool_data.previous_mouse_position = input.mouse.position; responses.add(OverlaysMessage::Draw); // Auto-panning let messages = [ - PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }.into(), - PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }.into(), + PathToolMessage::PointerOutsideViewport { + equidistant, + toggle_colinear, + move_anchor_with_handles, + } + .into(), + PathToolMessage::PointerMove { + equidistant, + toggle_colinear, + move_anchor_with_handles, + } + .into(), ]; tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); PathToolFsmState::DrawingBox } - (PathToolFsmState::Dragging, PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }) => { - let anchor_and_handle_toggled = input.keyboard.get(move_anchor_and_handles as usize); + ( + PathToolFsmState::Dragging(_), + PathToolMessage::PointerMove { + equidistant, + toggle_colinear, + move_anchor_with_handles, + }, + ) => { + let anchor_and_handle_toggled = input.keyboard.get(move_anchor_with_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; @@ -565,63 +623,89 @@ impl Fsm for PathToolFsmState { 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) { - tool_data.drag(shift_state, shape_editor, document, input, responses); + let toggle_colinear_state = input.keyboard.get(toggle_colinear as usize); + let equidistant_state = input.keyboard.get(equidistant as usize); + if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, shape_editor, document, responses) { + tool_data.drag(equidistant_state, shape_editor, document, input, responses); } // Auto-panning let messages = [ - PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }.into(), - PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }.into(), + PathToolMessage::PointerOutsideViewport { + toggle_colinear, + equidistant, + move_anchor_with_handles, + } + .into(), + PathToolMessage::PointerMove { + toggle_colinear, + equidistant, + move_anchor_with_handles, + } + .into(), ]; tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); - PathToolFsmState::Dragging + PathToolFsmState::Dragging(tool_data.dragging_state) } (PathToolFsmState::DrawingBox, PathToolMessage::PointerOutsideViewport { .. }) => { // Auto-panning - if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { - tool_data.drag_start_pos += shift; + if let Some(offset) = tool_data.auto_panning.shift_viewport(input, responses) { + tool_data.drag_start_pos += offset; } PathToolFsmState::DrawingBox } - (PathToolFsmState::Dragging, PathToolMessage::PointerOutsideViewport { shift, .. }) => { + (PathToolFsmState::Dragging(dragging_state), PathToolMessage::PointerOutsideViewport { equidistant, .. }) => { // Auto-panning if tool_data.auto_panning.shift_viewport(input, responses).is_some() { - let shift_state = input.keyboard.get(shift as usize); - tool_data.drag(shift_state, shape_editor, document, input, responses); + let equidistant = input.keyboard.get(equidistant as usize); + tool_data.drag(equidistant, shape_editor, document, input, responses); } - PathToolFsmState::Dragging + PathToolFsmState::Dragging(dragging_state) } - (state, PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }) => { + ( + state, + PathToolMessage::PointerOutsideViewport { + equidistant, + toggle_colinear, + move_anchor_with_handles, + }, + ) => { // Auto-panning let messages = [ - PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }.into(), - PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }.into(), + PathToolMessage::PointerOutsideViewport { + equidistant, + toggle_colinear, + move_anchor_with_handles, + } + .into(), + PathToolMessage::PointerMove { + equidistant, + toggle_colinear, + move_anchor_with_handles, + } + .into(), ]; tool_data.auto_panning.stop(&messages, responses); state } - (PathToolFsmState::DrawingBox, PathToolMessage::Enter { add_to_selection }) => { - let shift_pressed = input.keyboard.get(add_to_selection as usize); + (PathToolFsmState::DrawingBox, PathToolMessage::Enter { extend_selection }) => { + let extend_selection = input.keyboard.get(extend_selection as usize); if tool_data.drag_start_pos == tool_data.previous_mouse_position { responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); } else { - shape_editor.select_all_in_quad(&document.network_interface, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed); + shape_editor.select_all_in_quad(&document.network_interface, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !extend_selection); } responses.add(OverlaysMessage::Draw); PathToolFsmState::Ready } - (PathToolFsmState::Dragging, PathToolMessage::Escape | PathToolMessage::RightClick) => { + (PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => { responses.add(DocumentMessage::AbortTransaction); - shape_editor.deselect_all_points(); tool_data.snap_manager.cleanup(responses); PathToolFsmState::Ready } @@ -630,20 +714,20 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } // Mouse up - (PathToolFsmState::DrawingBox, PathToolMessage::DragStop { equidistant }) => { - let equidistant = input.keyboard.get(equidistant as usize); + (PathToolFsmState::DrawingBox, PathToolMessage::DragStop { extend_selection }) => { + let extend_selection = input.keyboard.get(extend_selection as usize); if tool_data.drag_start_pos == tool_data.previous_mouse_position { responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); } else { - shape_editor.select_all_in_quad(&document.network_interface, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !equidistant); + shape_editor.select_all_in_quad(&document.network_interface, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !extend_selection); } responses.add(OverlaysMessage::Draw); responses.add(PathToolMessage::SelectedPointUpdated); PathToolFsmState::Ready } - (_, PathToolMessage::DragStop { equidistant }) => { + (_, PathToolMessage::DragStop { extend_selection }) => { 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); @@ -651,12 +735,12 @@ impl Fsm for PathToolFsmState { tool_data.select_anchor_toggled = false; } - let equidistant = input.keyboard.get(equidistant as usize); + let extend_selection = input.keyboard.get(extend_selection as usize); let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD); if let Some((layer, nearest_point)) = nearest_point { - if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !equidistant { + if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !extend_selection { let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point); if clicked_selected { shape_editor.deselect_all_points(); @@ -729,6 +813,11 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } (_, PathToolMessage::SelectedPointUpdated) => { + let colinear = shape_editor.selected_manipulator_angles(&document.network_interface); + tool_data.dragging_state = DraggingState { + point_select_state: shape_editor.get_dragging_state(&document.network_interface), + colinear, + }; tool_data.selection_status = get_selection_status(&document.network_interface, shape_editor); self } @@ -767,19 +856,41 @@ impl Fsm for PathToolFsmState { HintInfo::keys([Key::Shift], "Break Anchor").prepend_plus(), ]), ]), - PathToolFsmState::Dragging => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![ - // TODO: Switch this to the "S" key. Also, make the hint dynamically say "Make Colinear" or "Make Not Colinear" based on its current state. And only - // TODO: show this hint if a handle (not an anchor) is being dragged, and disable that shortcut so it can't be pressed even with the hint not shown. - HintInfo::keys([Key::Alt], "Toggle Colinear Handles"), - // TODO: Switch this to the "Alt" key (since it's equivalent to the "From Center" modifier when drawing a line). And show this only when a handle is being dragged. - 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::Dragging(dragging_state) => { + let colinear = dragging_state.colinear; + let mut dragging_hint_data = HintData(Vec::new()); + dragging_hint_data + .0 + .push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])); + + let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor"); + let point_select_state_hint_group = match dragging_state.point_select_state { + PointSelectState::HandleNoPair => vec![drag_anchor], + PointSelectState::HandleWithPair => { + let mut hints = vec![drag_anchor]; + hints.push(HintInfo::keys([Key::Tab], "Swap Selected Handles")); + hints.push(HintInfo::keys( + [Key::KeyC], + if colinear == ManipulatorAngle::Colinear { + "Break Colinear Handles" + } else { + "Make Handles Colinear" + }, + )); + if colinear != ManipulatorAngle::Free { + hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); + } + hints + } + PointSelectState::Anchor => Vec::new(), + }; + + if !point_select_state_hint_group.is_empty() { + dragging_hint_data.0.push(HintGroup(point_select_state_hint_group)); + } + + dragging_hint_data + } PathToolFsmState::DrawingBox => HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), HintGroup(vec![ diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index b975d8e3..ca6a9acc 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -71,7 +71,7 @@ pub enum SelectToolMessage { Overlays(OverlayContext), // Tool-specific messages - DragStart { add_to_selection: Key, select_deepest: Key }, + DragStart { extend_selection: Key, select_deepest: Key }, DragStop { remove_from_selection: Key }, EditLayer, Enter, @@ -517,7 +517,7 @@ impl Fsm for SelectToolFsmState { self } - (SelectToolFsmState::Ready { .. }, SelectToolMessage::DragStart { add_to_selection, select_deepest }) => { + (SelectToolFsmState::Ready { .. }, SelectToolMessage::DragStart { extend_selection, select_deepest }) => { tool_data.drag_start = input.mouse.position; tool_data.drag_current = input.mouse.position; @@ -647,7 +647,7 @@ impl Fsm for SelectToolFsmState { else { tool_data.layers_dragging = selected; - if !input.keyboard.key(add_to_selection) { + if !input.keyboard.key(extend_selection) { responses.add(DocumentMessage::DeselectAllLayers); tool_data.layers_dragging.clear(); } diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index f6d847ce..35fc1db0 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -306,7 +306,7 @@ impl ManipulatorPointId { } } - /// Attempt to get a pair of handles. For an anchor this is the first to handles connected. For a handle it is self and the first opposing handle. + /// Attempt to get a pair of handles. For an anchor this is the first two handles connected. For a handle it is self and the first opposing handle. #[must_use] pub fn get_handle_pair(self, vector_data: &VectorData) -> Option<[HandleId; 2]> { match self {