From 41ee1cf8bcf3c77a46b2e2abf050708c489b9790 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 <142654792+0SlowPoke0@users.noreply.github.com> Date: Wed, 5 Feb 2025 08:45:43 +0530 Subject: [PATCH] Improve the Pen tool's colinearity and equidistance controls (#2242) * basic implementation done now refactor * fixed overlays refactoring need to fix colinear(update it) * more_refactoring ,only toggle C for grs to be done(if required) * cleanup * cleanup * more formatting checks * refactoring alt fixed hints fixed * code-review-changes * path-tool-tab-fix * fixed bugs * some refactor * fixed ctrl_snap * added lock-overlays and fixed grs bug * Code review --------- Co-authored-by: Keavon Chambers --- .../messages/input_mapper/input_mappings.rs | 4 +- .../messages/tool/tool_messages/path_tool.rs | 39 +- .../messages/tool/tool_messages/pen_tool.rs | 387 +++++++++++++++--- 3 files changed, 353 insertions(+), 77 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index afd581873f..c87ec64027 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -212,7 +212,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath), entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath), entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath), - entry!(KeyDown(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles), + entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles), entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift, lasso_select: Control }), entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick), entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape), @@ -254,7 +254,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=ToolMessage::Path(PathToolMessage::ClosePath)), // // PenToolMessage - entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=PenToolMessage::PointerMove { snap_angle: Shift, break_handle: Alt, lock_angle: Control}), + entry!(PointerMove; refresh_keys=[Control, Alt, Shift, KeyC], action_dispatch=PenToolMessage::PointerMove { snap_angle: Shift, break_handle: Alt, lock_angle: Control, colinear: KeyC }), entry!(KeyDown(MouseLeft); action_dispatch=PenToolMessage::DragStart { append_to_selected: Shift }), entry!(KeyUp(MouseLeft); action_dispatch=PenToolMessage::DragStop), entry!(KeyDown(MouseRight); action_dispatch=PenToolMessage::Confirm), diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 70948a0bb1..275c7e6c2b 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1185,16 +1185,9 @@ impl Fsm for PathToolFsmState { .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 => { - let mut hints = vec![drag_anchor]; - hints.push(HintInfo::keys([Key::Shift], "Snap 15°")); - hints.push(HintInfo::keys([Key::Control], "Lock Angle")); - hints - } - PointSelectState::HandleWithPair => { - let mut hints = vec![drag_anchor]; - hints.push(HintInfo::keys([Key::Tab], "Swap Selected Handles")); + let toggle_group = match dragging_state.point_select_state { + PointSelectState::HandleNoPair | PointSelectState::HandleWithPair => { + let mut hints = vec![HintInfo::keys([Key::Tab], "Swap Selected Handles")]; hints.push(HintInfo::keys( [Key::KeyC], if colinear == ManipulatorAngle::Colinear { @@ -1203,18 +1196,40 @@ impl Fsm for PathToolFsmState { "Make Handles Colinear" }, )); + hints + } + PointSelectState::Anchor => Vec::new(), + }; + let hold_group = match dragging_state.point_select_state { + PointSelectState::HandleNoPair => { + let mut hints = vec![]; if colinear != ManipulatorAngle::Free { hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); } hints.push(HintInfo::keys([Key::Shift], "Snap 15°")); hints.push(HintInfo::keys([Key::Control], "Lock Angle")); + hints.push(drag_anchor); + hints + } + PointSelectState::HandleWithPair => { + let mut hints = vec![]; + if colinear != ManipulatorAngle::Free { + hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); + } + hints.push(HintInfo::keys([Key::Shift], "Snap 15°")); + hints.push(HintInfo::keys([Key::Control], "Lock Angle")); + hints.push(drag_anchor); hints } PointSelectState::Anchor => Vec::new(), }; - if !point_select_state_hint_group.is_empty() { - dragging_hint_data.0.push(HintGroup(point_select_state_hint_group)); + if !toggle_group.is_empty() { + dragging_hint_data.0.push(HintGroup(toggle_group)); + } + + if !hold_group.is_empty() { + dragging_hint_data.0.push(HintGroup(hold_group)); } dragging_hint_data diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 689eac307a..960f9804a4 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -15,7 +15,7 @@ use bezier_rs::{Bezier, BezierHandles}; use graph_craft::document::NodeId; use graphene_core::vector::{PointId, VectorModificationType}; use graphene_core::Color; -use graphene_std::vector::{HandleId, SegmentId}; +use graphene_std::vector::{HandleId, ManipulatorPointId, SegmentId, VectorData}; #[derive(Default)] pub struct PenTool { @@ -56,8 +56,8 @@ pub enum PenToolMessage { Confirm, DragStart { append_to_selected: Key }, DragStop, - PointerMove { snap_angle: Key, break_handle: Key, lock_angle: Key }, - PointerOutsideViewport { snap_angle: Key, break_handle: Key, lock_angle: Key }, + PointerMove { snap_angle: Key, break_handle: Key, lock_angle: Key, colinear: Key }, + PointerOutsideViewport { snap_angle: Key, break_handle: Key, lock_angle: Key, colinear: Key }, Redo, Undo, UpdateOptions(PenOptionsUpdate), @@ -71,7 +71,7 @@ pub enum PenToolMessage { enum PenToolFsmState { #[default] Ready, - DraggingHandle, + DraggingHandle(HandleMode), PlacingAnchor, GRSHandle, } @@ -174,7 +174,7 @@ impl<'a> MessageHandler> for PenTool PointerMove, FinalPosition ), - PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant; + PenToolFsmState::DraggingHandle(_) | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant; DragStart, DragStop, PointerMove, @@ -203,6 +203,7 @@ struct ModifierState { snap_angle: bool, lock_angle: bool, break_handle: bool, + colinear: bool, } #[derive(Clone, Debug)] struct LastPoint { @@ -212,6 +213,17 @@ struct LastPoint { handle_start: DVec2, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +enum HandleMode { + /// Pressing 'C' breaks colinearity + Free, + /// Pressing 'Alt': Handle length is locked + #[default] + ColinearLocked, + /// Pressing 'Alt': Handles are equidistant + ColinearEquidistant, +} + #[derive(Clone, Debug, Default)] struct PenToolData { snap_manager: SnapManager, @@ -222,6 +234,9 @@ struct PenToolData { next_handle_start: DVec2, g1_continuous: bool, + toggle_colinear_debounce: bool, + + segment_end_before_bent: Option, angle: f64, auto_panning: AutoPanning, @@ -229,7 +244,11 @@ struct PenToolData { buffering_merged_vector: bool, - before_grs_pos: DVec2, + previous_handle_start_pos: DVec2, + previous_handle_end_pos: Option, + alt_press: bool, + + handle_mode: HandleMode, } impl PenToolData { fn latest_point(&self) -> Option<&LastPoint> { @@ -265,19 +284,24 @@ impl PenToolData { } /// If the user places the anchor on top of the previous anchor, it becomes sharp and the outgoing handle may be dragged. - fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2) { + fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2, layer: LayerNodeIdentifier) { self.g1_continuous = true; let document = snap_data.document; self.next_handle_start = self.next_point; + let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); // Break the control let Some(last_pos) = self.latest_point().map(|point| point.pos) else { return }; + let transform = document.metadata().document_to_viewport * transform; let on_top = transform.transform_point2(self.next_point).distance_squared(transform.transform_point2(last_pos)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2); if on_top { if let Some(point) = self.latest_point_mut() { point.in_segment = None; } + + self.segment_end_before_bent = vector_data.segment_domain.ids().last().copied(); + self.handle_mode = HandleMode::Free; self.handle_end = None; } } @@ -326,6 +350,7 @@ impl PenToolData { let points = [start, end]; let id = SegmentId::generate(); + self.segment_end_before_bent = Some(id); let modification_type = VectorModificationType::InsertSegment { id, points, handles }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); @@ -351,19 +376,107 @@ impl PenToolData { Some(if close_subpath { PenToolFsmState::Ready } else { PenToolFsmState::PlacingAnchor }) } - fn drag_handle(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque) -> Option { - let colinear = !self.modifiers.break_handle && self.handle_end.is_some(); + fn drag_handle(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque, layer: Option) -> Option { + let colinear = (self.handle_mode == HandleMode::ColinearEquidistant && self.modifiers.break_handle) || (self.handle_mode == HandleMode::ColinearLocked && !self.modifiers.break_handle); + let document = snap_data.document; self.next_handle_start = self.compute_snapped_angle(snap_data, transform, colinear, mouse, Some(self.next_point), false); - if let Some(handle_end) = self.handle_end.as_mut().filter(|_| colinear) { - *handle_end = self.next_point * 2. - self.next_handle_start; - self.g1_continuous = true; - } else { - self.g1_continuous = false; + let Some(layer) = layer else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)) }; + let vector_data = document.network_interface.compute_modified_vector(layer)?; + + match self.handle_mode { + HandleMode::ColinearLocked | HandleMode::ColinearEquidistant => { + self.g1_continuous = true; + self.colinear(responses, layer, self.next_handle_start, self.next_point, &vector_data); + self.adjust_handle_length(responses, layer, &vector_data); + } + HandleMode::Free => { + self.g1_continuous = false; + } } responses.add(OverlaysMessage::Draw); - Some(PenToolFsmState::DraggingHandle) + Some(PenToolFsmState::DraggingHandle(self.handle_mode)) + } + + /// Makes the opposite handle equidistant or locks its length. + fn adjust_handle_length(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector_data: &VectorData) { + let Some(latest) = self.latest_point() else { return }; + let anchor_pos = latest.pos; + + match self.handle_mode { + HandleMode::ColinearEquidistant => self.adjust_equidistant_handle(anchor_pos, responses, layer, vector_data), + HandleMode::ColinearLocked => self.adjust_locked_length_handle(anchor_pos, responses, layer), + HandleMode::Free => {} // No adjustments needed in free mode + } + } + + fn colinear(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, handle_start: DVec2, anchor_point: DVec2, vector_data: &VectorData) { + let Some(direction) = (anchor_point - handle_start).try_normalize() else { + log::trace!("Skipping colinear adjustment: handle_start and anchor_point are too close!"); + return; + }; + + let handle_offset = if let Some(handle_end) = self.handle_end { + (handle_end - anchor_point).length() + } else { + let Some(segment) = self.segment_end_before_bent else { return }; + let end_handle = ManipulatorPointId::EndHandle(segment); + let Some(end_handle) = end_handle.get_position(vector_data) else { return }; + (end_handle - anchor_point).length() + }; + let new_handle_position = anchor_point + handle_offset * direction; + self.update_handle_position(new_handle_position, anchor_point, responses, layer); + } + + fn adjust_equidistant_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector_data: &VectorData) { + if self.modifiers.break_handle { + self.store_handle(vector_data); + self.alt_press = true; + let new_position = self.next_point * 2. - self.next_handle_start; + self.update_handle_position(new_position, anchor_pos, responses, layer); + } else { + self.restore_previous_handle(anchor_pos, responses, layer); + } + } + + fn adjust_locked_length_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier) { + if !self.modifiers.break_handle { + let new_position = self.next_point * 2. - self.next_handle_start; + self.update_handle_position(new_position, anchor_pos, responses, layer); + } + } + + /// Temporarily stores the opposite handle position to revert back when Alt is released in equidistant mode. + fn store_handle(&mut self, vector_data: &VectorData) { + if !self.alt_press { + self.previous_handle_end_pos = self.handle_end.or_else(|| { + let segment = self.segment_end_before_bent?; + ManipulatorPointId::EndHandle(segment).get_position(vector_data) + }); + } + } + + fn restore_previous_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier) { + if self.alt_press { + self.alt_press = false; + if let Some(previous_handle) = self.previous_handle_end_pos { + self.update_handle_position(previous_handle, anchor_pos, responses, layer); + } + self.previous_handle_end_pos = None; // Reset storage + } + } + + fn update_handle_position(&mut self, new_position: DVec2, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier) { + if let Some(handle) = self.handle_end.as_mut() { + *handle = new_position; + } else { + let Some(segment) = self.segment_end_before_bent else { return }; + let relative_position = new_position - anchor_pos; + let modification_type = VectorModificationType::SetEndHandle { segment, relative_position }; + + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } } fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque) -> Option { @@ -455,7 +568,7 @@ impl PenToolData { } if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) { - if (relative - document_pos) != DVec2::ZERO { + if (relative - document_pos) != DVec2::ZERO && (relative - document_pos).length_squared() > f64::EPSILON * 100. { self.angle = -(relative - document_pos).angle_to(DVec2::X) } } @@ -571,36 +684,67 @@ impl Fsm for PenToolFsmState { let ToolMessage::Pen(event) = event else { return self }; match (self, event) { (PenToolFsmState::PlacingAnchor | PenToolFsmState::GRSHandle, PenToolMessage::GRS { grab, rotate, scale }) => { - let Some(latest) = tool_data.latest_point_mut() else { return PenToolFsmState::PlacingAnchor }; let Some(layer) = layer else { return PenToolFsmState::PlacingAnchor }; - if latest.handle_start != latest.pos { - let viewport = document.metadata().transform_to_viewport(layer); - let last_point = viewport.transform_point2(latest.pos); - let handle = viewport.transform_point2(latest.handle_start); - - if input.keyboard.key(grab) { - responses.add(TransformLayerMessage::BeginGrabPen { last_point, handle }); - } else if input.keyboard.key(rotate) { - responses.add(TransformLayerMessage::BeginRotatePen { last_point, handle }); - } else if input.keyboard.key(scale) { - responses.add(TransformLayerMessage::BeginScalePen { last_point, handle }); - } + let Some(latest) = tool_data.latest_point() else { return PenToolFsmState::PlacingAnchor }; + if latest.handle_start == latest.pos { + return PenToolFsmState::PlacingAnchor; + } + + let viewport = document.metadata().transform_to_viewport(layer); + let last_point = viewport.transform_point2(latest.pos); + let handle = viewport.transform_point2(latest.handle_start); - tool_data.before_grs_pos = latest.handle_start; + if input.keyboard.key(grab) { + responses.add(TransformLayerMessage::BeginGrabPen { last_point, handle }); + } else if input.keyboard.key(rotate) { + responses.add(TransformLayerMessage::BeginRotatePen { last_point, handle }); + } else if input.keyboard.key(scale) { + responses.add(TransformLayerMessage::BeginScalePen { last_point, handle }); } + tool_data.previous_handle_start_pos = latest.handle_start; + + // Store the handle_end position + let segment = tool_data.segment_end_before_bent; + if let Some(segment) = segment { + let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); + tool_data.previous_handle_end_pos = ManipulatorPointId::EndHandle(segment).get_position(&vector_data); + } PenToolFsmState::GRSHandle } - (PenToolFsmState::GRSHandle, PenToolMessage::FinalPosition { final_position: final_pos }) => { + (PenToolFsmState::GRSHandle, PenToolMessage::FinalPosition { final_position }) => { let Some(layer) = layer else { return PenToolFsmState::GRSHandle }; + let vector_data = document.network_interface.compute_modified_vector(layer); + let Some(vector_data) = vector_data else { return PenToolFsmState::GRSHandle }; if let Some(latest_pt) = tool_data.latest_point_mut() { let layer_space_to_viewport = document.metadata().transform_to_viewport(layer); - let final_pos = layer_space_to_viewport.inverse().transform_point2(final_pos); + let final_pos = layer_space_to_viewport.inverse().transform_point2(final_position); latest_pt.handle_start = final_pos; } + // Making the end handle colinear + match tool_data.handle_mode { + HandleMode::Free => {} + HandleMode::ColinearEquidistant | HandleMode::ColinearLocked => { + if let Some((latest, segment)) = tool_data.latest_point().zip(tool_data.segment_end_before_bent) { + let handle = ManipulatorPointId::EndHandle(segment).get_position(&vector_data); + let Some(handle) = handle else { return PenToolFsmState::GRSHandle }; + + let Some(direction) = (latest.pos - latest.handle_start).try_normalize() else { + log::trace!("Skipping handle adjustment: latest.pos and latest.handle_start are too close!"); + return PenToolFsmState::GRSHandle; + }; + + let relative_distance = (handle - latest.pos).length(); + let relative_position = relative_distance * direction; + let modification_type = VectorModificationType::SetEndHandle { segment, relative_position }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + } + } + responses.add(OverlaysMessage::Draw); PenToolFsmState::GRSHandle @@ -614,6 +758,7 @@ impl Fsm for PenToolFsmState { snap_angle: Key::Control, break_handle: Key::Alt, lock_angle: Key::Shift, + colinear: Key::KeyC, }); PenToolFsmState::PlacingAnchor @@ -622,7 +767,9 @@ impl Fsm for PenToolFsmState { tool_data.next_point = input.mouse.position; tool_data.next_handle_start = input.mouse.position; - let previous = tool_data.before_grs_pos; + let Some(layer) = layer else { return PenToolFsmState::GRSHandle }; + + let previous = tool_data.previous_handle_start_pos; if let Some(latest) = tool_data.latest_point_mut() { latest.handle_start = previous; } @@ -632,8 +779,16 @@ impl Fsm for PenToolFsmState { snap_angle: Key::Control, break_handle: Key::Alt, lock_angle: Key::Shift, + colinear: Key::KeyC, }); + // Set the handle-end back to original position + if let Some(((latest, segment), handle_end)) = tool_data.latest_point().zip(tool_data.segment_end_before_bent).zip(tool_data.previous_handle_end_pos) { + let relative = handle_end - latest.pos; + let modification_type = VectorModificationType::SetEndHandle { segment, relative_position: relative }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + PenToolFsmState::PlacingAnchor } (_, PenToolMessage::SelectionChanged) => { @@ -680,9 +835,14 @@ impl Fsm for PenToolFsmState { // Draw the line between the currently-being-placed anchor and its incoming handle (opposite the one currently being dragged out) overlay_context.line(next_anchor, handle_end, None); + if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.lock_angle { + // Draw the line between the currently-being-placed anchor and last-placed point (Lock angle bent overlays) + overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5)); + } + path_overlays(document, shape_editor, &mut overlay_context); - if self == PenToolFsmState::DraggingHandle && valid(next_anchor, handle_end) { + if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, handle_end) { // Draw the handle circle for the currently-being-dragged-out incoming handle (opposite the one currently being dragged out) overlay_context.manipulator_handle(handle_end, false, None); } @@ -696,12 +856,12 @@ impl Fsm for PenToolFsmState { path_overlays(document, shape_editor, &mut overlay_context); } - if self == PenToolFsmState::DraggingHandle && valid(next_anchor, next_handle_start) { + if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, next_handle_start) { // Draw the handle circle for the currently-being-dragged-out outgoing handle (the one currently being dragged out, under the user's cursor) overlay_context.manipulator_handle(next_handle_start, false, None); } - if self == PenToolFsmState::DraggingHandle { + if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) { // Draw the anchor square for the most recently placed anchor overlay_context.manipulator_anchor(next_anchor, false, None); } @@ -720,11 +880,11 @@ impl Fsm for PenToolFsmState { } (PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => { responses.add(DocumentMessage::StartTransaction); - + tool_data.handle_mode = HandleMode::Free; tool_data.create_initial_point(document, input, responses, tool_options, input.keyboard.key(append_to_selected), preferences); // Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle - PenToolFsmState::DraggingHandle + PenToolFsmState::DraggingHandle(tool_data.handle_mode) } (_, PenToolMessage::AddPointLayerPosition { layer, viewport }) => { tool_data.add_point_layer_position(document, responses, layer, viewport); @@ -739,13 +899,15 @@ impl Fsm for PenToolFsmState { let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); + // Early return if the buffer was started and this message is being run again after the buffer (so that place_anchor updates the state with the newly merged vector) if tool_data.buffering_merged_vector { tool_data.buffering_merged_vector = false; - tool_data.bend_from_previous_point(SnapData::new(document, input), transform); + tool_data.handle_mode = HandleMode::ColinearLocked; + tool_data.bend_from_previous_point(SnapData::new(document, input), transform, layer.unwrap()); tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses); tool_data.buffering_merged_vector = false; - PenToolFsmState::DraggingHandle + PenToolFsmState::DraggingHandle(tool_data.handle_mode) } else { if tool_data.handle_end.is_some() { responses.add(DocumentMessage::StartTransaction); @@ -774,37 +936,80 @@ impl Fsm for PenToolFsmState { last_point.handle_start = last_point.pos; responses.add(OverlaysMessage::Draw); } else { - log::warn!("No latest point available to modify handle_start."); + log::trace!("No latest point available to modify handle_start."); } self } - (PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data + (PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => tool_data .finish_placing_handle(SnapData::new(document, input), transform, preferences, responses) .unwrap_or(PenToolFsmState::PlacingAnchor), - (PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => { + ( + PenToolFsmState::DraggingHandle(_), + PenToolMessage::PointerMove { + snap_angle, + break_handle, + lock_angle, + colinear, + }, + ) => { tool_data.modifiers = ModifierState { snap_angle: input.keyboard.key(snap_angle), lock_angle: input.keyboard.key(lock_angle), break_handle: input.keyboard.key(break_handle), + colinear: input.keyboard.key(colinear), }; let snap_data = SnapData::new(document, input); - let state = tool_data.drag_handle(snap_data, transform, input.mouse.position, responses).unwrap_or(PenToolFsmState::Ready); + if tool_data.modifiers.colinear && !tool_data.toggle_colinear_debounce { + tool_data.handle_mode = match tool_data.handle_mode { + HandleMode::Free => HandleMode::ColinearEquidistant, + HandleMode::ColinearEquidistant | HandleMode::ColinearLocked => HandleMode::Free, + }; + tool_data.toggle_colinear_debounce = true; + } + + if !tool_data.modifiers.colinear { + tool_data.toggle_colinear_debounce = false; + } + + let state = tool_data.drag_handle(snap_data, transform, input.mouse.position, responses, layer).unwrap_or(PenToolFsmState::Ready); // Auto-panning let messages = [ - PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(), - PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(), + PenToolMessage::PointerOutsideViewport { + snap_angle, + break_handle, + lock_angle, + colinear, + } + .into(), + PenToolMessage::PointerMove { + snap_angle, + break_handle, + lock_angle, + colinear, + } + .into(), ]; tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); state } - (PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => { + ( + PenToolFsmState::PlacingAnchor, + PenToolMessage::PointerMove { + snap_angle, + break_handle, + lock_angle, + colinear, + }, + ) => { + tool_data.alt_press = false; tool_data.modifiers = ModifierState { snap_angle: input.keyboard.key(snap_angle), lock_angle: input.keyboard.key(lock_angle), break_handle: input.keyboard.key(break_handle), + colinear: input.keyboard.key(colinear), }; let state = tool_data .place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses) @@ -812,8 +1017,20 @@ impl Fsm for PenToolFsmState { // Auto-panning let messages = [ - PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(), - PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(), + PenToolMessage::PointerOutsideViewport { + snap_angle, + break_handle, + lock_angle, + colinear, + } + .into(), + PenToolMessage::PointerMove { + snap_angle, + break_handle, + lock_angle, + colinear, + } + .into(), ]; tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); @@ -824,11 +1041,11 @@ impl Fsm for PenToolFsmState { responses.add(OverlaysMessage::Draw); self } - (PenToolFsmState::DraggingHandle, PenToolMessage::PointerOutsideViewport { .. }) => { + (PenToolFsmState::DraggingHandle(mode), PenToolMessage::PointerOutsideViewport { .. }) => { // Auto-panning let _ = tool_data.auto_panning.shift_viewport(input, responses); - PenToolFsmState::DraggingHandle + PenToolFsmState::DraggingHandle(mode) } (PenToolFsmState::PlacingAnchor, PenToolMessage::PointerOutsideViewport { .. }) => { // Auto-panning @@ -836,17 +1053,37 @@ impl Fsm for PenToolFsmState { PenToolFsmState::PlacingAnchor } - (state, PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }) => { + ( + state, + PenToolMessage::PointerOutsideViewport { + snap_angle, + break_handle, + lock_angle, + colinear, + }, + ) => { // Auto-panning let messages = [ - PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(), - PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(), + PenToolMessage::PointerOutsideViewport { + snap_angle, + break_handle, + lock_angle, + colinear, + } + .into(), + PenToolMessage::PointerMove { + snap_angle, + break_handle, + lock_angle, + colinear, + } + .into(), ]; tool_data.auto_panning.stop(&messages, responses); state } - (PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => { + (PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => { responses.add(DocumentMessage::EndTransaction); tool_data.handle_end = None; tool_data.latest_points.clear(); @@ -866,7 +1103,7 @@ impl Fsm for PenToolFsmState { PenToolFsmState::Ready } - (PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => { + (PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => { if tool_data.point_index > 0 { tool_data.point_index -= 1; tool_data @@ -906,16 +1143,40 @@ impl Fsm for PenToolFsmState { HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Add Sharp Point"), HintInfo::mouse(MouseMotion::LmbDrag, "Add Smooth Point")]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, ""), HintInfo::mouse(MouseMotion::LmbDrag, "Bend Prev. Point").prepend_slash()]), ]), - PenToolFsmState::DraggingHandle => HintData(vec![ - HintGroup(vec![ + PenToolFsmState::DraggingHandle(mode) => { + let mut dragging_hint_data = HintData(Vec::new()); + dragging_hint_data.0.push(HintGroup(vec![ HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "").prepend_slash(), HintInfo::keys([Key::Enter], "End Path").prepend_slash(), - ]), - HintGroup(vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")]), - // TODO: Only show this if the handle being dragged is colinear, so don't show this when bending from the previous point (by clicking and dragging from the previously placed anchor) - HintGroup(vec![HintInfo::keys([Key::Alt], "Bend Handle")]), - ]), + ])); + + let toggle_group = match mode { + HandleMode::Free => { + vec![HintInfo::keys([Key::KeyC], "Make Handles Colinear")] + } + HandleMode::ColinearLocked | HandleMode::ColinearEquidistant => { + vec![HintInfo::keys([Key::KeyC], "Break Colinear Handles")] + } + }; + + let mut common_hints = vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")]; + let hold_group = match mode { + HandleMode::Free => common_hints, + HandleMode::ColinearLocked => { + common_hints.push(HintInfo::keys([Key::Alt], "Non-Equidistant Handles")); + common_hints + } + HandleMode::ColinearEquidistant => { + common_hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); + common_hints + } + }; + + dragging_hint_data.0.push(HintGroup(toggle_group)); + dragging_hint_data.0.push(HintGroup(hold_group)); + dragging_hint_data + } }; responses.add(FrontendMessage::UpdateInputHints { hint_data });