From e4447853011bdc2b2557921088a091ad9dbfc574 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 <142654792+0SlowPoke0@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:11:55 +0530 Subject: [PATCH] Make the Pen tool use Ctrl to lock the angle of handles such that they maintain colinearity (#2284) * pen tool feature and other minor fixes * ctrl done,need to improve handle_modifications * completes collinear and ctrl for all cases,need to refactor * more bug fix need to refactor * fixed minor issues and refactor done * Code review * minor bug fixes --------- Co-authored-by: Keavon Chambers --- .../document/utility_types/transformation.rs | 13 +- .../tool/common_functionality/shape_editor.rs | 9 +- .../messages/tool/tool_messages/path_tool.rs | 25 +- .../messages/tool/tool_messages/pen_tool.rs | 325 ++++++++++++++---- 4 files changed, 291 insertions(+), 81 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index 8eafbe75..63b9dbdd 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -591,11 +591,14 @@ impl<'a> Selected<'a> { responses.add(GraphOperationMessage::Vector { layer, modification_type }); if let Some((id, initial)) = handle.mirror { - let direction = viewspace.transform_vector2(new_pos_viewport - relative).try_normalize(); - let length = viewspace.transform_vector2(initial - relative).length(); - let new_relative = direction.map_or(initial - relative, |direction| viewspace.inverse().transform_vector2(-direction * length)); - let modification_type = id.set_relative_position(new_relative); - responses.add(GraphOperationMessage::Vector { layer, modification_type }); + // When the handle is scaled to zero, don't update the mirror handle + if (new_pos_viewport - relative).length_squared() > f64::EPSILON { + let direction = viewspace.transform_vector2(new_pos_viewport - relative).try_normalize(); + let length = viewspace.transform_vector2(initial - relative).length(); + let new_relative = direction.map_or(initial - relative, |direction| viewspace.inverse().transform_vector2(-direction * length)); + let modification_type = id.set_relative_position(new_relative); + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } } } } diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 64766c6f..60142adb 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -733,14 +733,16 @@ impl ShapeState { } /// Move the selected points by dragging the mouse. + #[allow(clippy::too_many_arguments)] pub fn move_selected_points( &self, handle_lengths: Option, document: &DocumentMessageHandler, delta: DVec2, equidistant: bool, - responses: &mut VecDeque, in_viewport_space: bool, + opposite_handle_position: Option, + responses: &mut VecDeque, ) { for (&layer, state) in &self.selected_shape_state { let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; @@ -791,6 +793,11 @@ impl ShapeState { let new_relative = if equidistant { -(handle_position - anchor_position) + } + // If the handle is very close to the anchor, return the original position + else if (handle_position - anchor_position).length_squared() < f64::EPSILON * 1e5 { + let Some(opposite_handle_position) = opposite_handle_position else { continue }; + opposite_handle_position - anchor_position } else { // TODO: Is this equivalent to `transform_to_document_space`? If changed, the before and after should be tested. let transform = document.metadata().document_to_viewport.inverse() * transform_to_viewport_space; diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index a31e119b..9552697f 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -371,6 +371,7 @@ struct PathToolData { dragging_state: DraggingState, current_selected_handle_id: Option, angle: f64, + opposite_handle_position: Option, } impl PathToolData { @@ -551,6 +552,16 @@ impl PathToolData { for point in state.selected() { let Some(anchor) = point.get_anchor(&vector_data) else { continue }; layer_manipulators.insert(anchor); + let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) else { continue }; + let Some(handle) = point.as_handle() else { continue }; + // Check which handle is selected and which is opposite + let opposite = if handle == handle1 { handle2 } else { handle1 }; + + self.opposite_handle_position = if self.opposite_handle_position.is_none() { + opposite.to_manipulator_point().get_position(&vector_data) + } else { + self.opposite_handle_position + }; } for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) { if layer_manipulators.contains(&id) { @@ -742,7 +753,8 @@ impl PathToolData { }; let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() }; - shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, responses, true); + let opposite = if lock_angle { None } else { self.opposite_handle_position }; + shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, opposite, responses); self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta); } } @@ -1199,6 +1211,7 @@ impl Fsm for PathToolFsmState { responses.add(DocumentMessage::EndTransaction); responses.add(PathToolMessage::SelectedPointUpdated); tool_data.snap_manager.cleanup(responses); + tool_data.opposite_handle_position = None; PathToolFsmState::Ready } @@ -1249,7 +1262,15 @@ impl Fsm for PathToolFsmState { } (_, PathToolMessage::PointerMove { .. }) => self, (_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => { - shape_editor.move_selected_points(tool_data.opposing_handle_lengths.take(), document, (delta_x, delta_y).into(), true, responses, false); + shape_editor.move_selected_points( + tool_data.opposing_handle_lengths.take(), + document, + (delta_x, delta_y).into(), + true, + false, + tool_data.opposite_handle_position, + responses, + ); PathToolFsmState::Ready } diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 6d772236..570ac521 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -8,7 +8,7 @@ use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils::{self, merge_layers}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; -use crate::messages::tool::common_functionality::utility_functions::should_extend; +use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend}; use bezier_rs::{Bezier, BezierHandles}; use graph_craft::document::NodeId; @@ -243,6 +243,14 @@ struct LastPoint { in_segment: Option, handle_start: DVec2, } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +enum DrawMode { + #[default] + /// Modifies the clicked endpoint segment, once you go to the ready mode you need to modify the handles of the next clicked endpoint segment + BreakPath, + /// Modifies the handle_end + ContinuePath, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] enum HandleMode { @@ -267,8 +275,6 @@ struct PenToolData { g1_continuous: bool, toggle_colinear_debounce: bool, - segment_end_before_bent: Option, - angle: f64, auto_panning: AutoPanning, modifiers: ModifierState, @@ -280,6 +286,10 @@ struct PenToolData { alt_press: bool, handle_mode: HandleMode, + /// The point that is being dragged + end_point: Option, + end_point_segment: Option, + draw_mode: DrawMode, } impl PenToolData { fn latest_point(&self) -> Option<&LastPoint> { @@ -315,25 +325,35 @@ 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, layer: LayerNodeIdentifier) { + fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2, layer: LayerNodeIdentifier, preferences: &PreferencesMessageHandler) { 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 Some((last_pos, id)) = self.latest_point().map(|point| (point.pos, point.id)) 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 { + self.handle_end = None; + self.handle_mode = HandleMode::Free; + + // Update `end_point_segment` that was clicked on + self.store_clicked_endpoint(document, snap_data.input, preferences); + + if self.modifiers.lock_angle { + self.set_lock_angle(&vector_data, id, self.end_point_segment); + let last_segment = self.end_point_segment; + let Some(point) = self.latest_point_mut() else { return }; + point.in_segment = last_segment; + return; + } + 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; } } @@ -379,9 +399,11 @@ impl PenToolData { end }); - let points = [start, end]; + // Store the segment let id = SegmentId::generate(); - self.segment_end_before_bent = Some(id); + self.end_point_segment = Some(id); + + let points = [start, end]; let modification_type = VectorModificationType::InsertSegment { id, points, handles }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); @@ -413,12 +435,17 @@ impl PenToolData { self.next_handle_start = self.compute_snapped_angle(snap_data, transform, colinear, mouse, Some(self.next_point), false); let Some(layer) = layer else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)) }; let vector_data = document.network_interface.compute_modified_vector(layer)?; + // Check if the handle is the start of the segment + let mut is_start = false; + if let Some((anchor, segment)) = self.end_point.zip(self.end_point_segment) { + is_start = vector_data.segment_start_from_id(segment) == Some(anchor); + } 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); + self.colinear(responses, layer, self.next_handle_start, self.next_point, &vector_data, is_start); + self.adjust_handle_length(responses, layer, &vector_data, is_start); } HandleMode::Free => { self.g1_continuous = false; @@ -431,83 +458,122 @@ impl PenToolData { } /// Makes the opposite handle equidistant or locks its length. - fn adjust_handle_length(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector_data: &VectorData) { + fn adjust_handle_length(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector_data: &VectorData, is_start: bool) { 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::ColinearEquidistant => self.adjust_equidistant_handle(anchor_pos, responses, layer, vector_data, is_start), + HandleMode::ColinearLocked => self.adjust_locked_length_handle(anchor_pos, responses, layer, is_start), 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 { + fn colinear(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, handle_start: DVec2, anchor_pos: DVec2, vector_data: &VectorData, is_start: bool) { + let Some(direction) = (anchor_pos - 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); + let Some(handle_offset) = self.get_handle_offset(anchor_pos, vector_data, is_start) else { return }; + let new_handle_position = anchor_pos + handle_offset * direction; + + self.update_handle_position(new_handle_position, anchor_pos, responses, layer, is_start); } - fn adjust_equidistant_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector_data: &VectorData) { + fn get_handle_offset(&self, anchor_pos: DVec2, vector_data: &VectorData, is_start: bool) -> Option { + if is_start { + let segment = self.end_point_segment?; + let handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data)?; + return Some((handle - anchor_pos).length()); + } + + if self.draw_mode == DrawMode::ContinuePath { + return self.handle_end.map(|handle| (handle - anchor_pos).length()).or_else(|| { + self.end_point_segment + .and_then(|segment| Some((ManipulatorPointId::EndHandle(segment).get_position(vector_data)? - anchor_pos).length())) + }); + } + + let handle = ManipulatorPointId::EndHandle(self.end_point_segment?).get_position(vector_data); + if let Some(handle) = handle { + return Some((handle - anchor_pos).length()); + } + None + } + + fn adjust_equidistant_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector_data: &VectorData, is_start: bool) { if self.modifiers.break_handle { - self.store_handle(vector_data); + self.store_handle(vector_data, is_start); 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); + self.update_handle_position(new_position, anchor_pos, responses, layer, is_start); } else { - self.restore_previous_handle(anchor_pos, responses, layer); + self.restore_previous_handle(anchor_pos, responses, layer, is_start); } } - fn adjust_locked_length_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier) { + fn adjust_locked_length_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier, is_start: bool) { 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); + self.update_handle_position(new_position, anchor_pos, responses, layer, is_start); } } /// Temporarily stores the opposite handle position to revert back when Alt is released in equidistant mode. - fn store_handle(&mut self, vector_data: &VectorData) { + fn store_handle(&mut self, vector_data: &VectorData, is_start: bool) { 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) - }); + self.previous_handle_end_pos = if is_start { + let Some(segment) = self.end_point_segment else { return }; + ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data) + } else if self.draw_mode == DrawMode::ContinuePath { + self.handle_end.or_else(|| { + let segment = self.end_point_segment?; + ManipulatorPointId::EndHandle(segment).get_position(vector_data) + }) + } else { + let Some(segment) = self.end_point_segment else { return }; + let end_handle = ManipulatorPointId::EndHandle(segment); + end_handle.get_position(vector_data) + }; } } - fn restore_previous_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier) { + fn restore_previous_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier, is_start: bool) { 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.update_handle_position(previous_handle, anchor_pos, responses, layer, is_start); } 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 }; + fn update_handle_position(&mut self, new_position: DVec2, anchor_pos: DVec2, responses: &mut VecDeque, layer: LayerNodeIdentifier, is_start: bool) { + let relative_position = new_position - anchor_pos; + if self.draw_mode == DrawMode::ContinuePath { + if let Some(handle) = self.handle_end.as_mut() { + *handle = new_position; + return; + } + + let Some(segment) = self.end_point_segment else { return }; + let modification_type = VectorModificationType::SetEndHandle { segment, relative_position }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); + return; } + + let Some(segment) = self.end_point_segment else { return }; + + if is_start { + let modification_type = VectorModificationType::SetPrimaryHandle { segment, relative_position }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + return; + } + + 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 { @@ -626,20 +692,36 @@ impl PenToolData { let tolerance = crate::consts::SNAP_POINT_TOLERANCE; if let Some((layer, point, position)) = should_extend(document, viewport, tolerance, selected_nodes.selected_layers(document.metadata()), preferences) { // Perform extension of an existing path + let in_segment = if self.modifiers.lock_angle { self.end_point_segment } else { None }; self.add_point(LastPoint { id: point, pos: position, - in_segment: None, + in_segment, handle_start: position, }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); self.next_point = position; self.next_handle_start = position; + let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); + let segment = vector_data.all_connected(point).collect::>().first().map(|s| s.segment); + + if self.modifiers.lock_angle { + self.set_lock_angle(&vector_data, point, segment); + } return; } if append { + if let Some((layer, point, _)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) { + let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); + let segment = vector_data.all_connected(point).collect::>().first().map(|s| s.segment); + + if self.modifiers.lock_angle { + self.set_lock_angle(&vector_data, point, segment); + } + } + self.end_point_segment = None; let mut selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&document.network_interface); let existing_layer = selected_layers_except_artboards.next().filter(|_| selected_layers_except_artboards.next().is_none()); if let Some(layer) = existing_layer { @@ -649,6 +731,16 @@ impl PenToolData { } } + if let Some((layer, point, _position)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) { + let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); + let segment = vector_data.all_connected(point).collect::>().first().map(|s| s.segment); + + if self.modifiers.lock_angle { + self.set_lock_angle(&vector_data, point, segment); + self.handle_mode = HandleMode::Free; + } + } + // New path layer let node_type = resolve_document_node_type("Path").expect("Path node does not exist"); let nodes = vec![(NodeId(0), node_type.default_node_template())]; @@ -657,6 +749,8 @@ impl PenToolData { let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); tool_options.fill.apply_fill(layer, responses); tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); + self.end_point_segment = None; + self.draw_mode = DrawMode::ContinuePath; responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); // This causes the following message to be run only after the next graph evaluation runs and the transforms are updated @@ -665,6 +759,61 @@ impl PenToolData { responses.add(PenToolMessage::AddPointLayerPosition { layer, viewport }); } + // Stores the segment and point ID of the clicked endpoint + fn store_clicked_endpoint(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, preferences: &PreferencesMessageHandler) { + let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); + + let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); + let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); + + let tolerance = crate::consts::SNAP_POINT_TOLERANCE; + + if let Some((layer, point, _position)) = closest_point(document, viewport, tolerance, document.metadata().all_layers(), |_| false, preferences) { + self.end_point = Some(point); + let vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); + let segment = vector_data.all_connected(point).collect::>().first().map(|s| s.segment); + self.end_point_segment = segment; + } + } + + fn set_lock_angle(&mut self, vector_data: &VectorData, anchor: PointId, segment: Option) { + let anchor_position = vector_data.point_domain.position_from_id(anchor); + + let Some((anchor_position, segment)) = anchor_position.zip(segment) else { + self.handle_mode = HandleMode::Free; + return; + }; + + // Closure to check if a point is the start or end of a segment + let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point); + + let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data); + let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data); + + let start_point = if is_start(anchor, segment) { + vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id)) + } else { + vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id)) + }; + + let required_handle = if is_start(anchor, segment) { + start_handle + .filter(|&handle| handle != anchor_position) + .or(end_handle.filter(|&handle| Some(handle) != start_point)) + .or(start_point) + } else { + end_handle + .filter(|&handle| handle != anchor_position) + .or(start_handle.filter(|&handle| Some(handle) != start_point)) + .or(start_point) + }; + + if let Some(required_handle) = required_handle { + self.angle = -(required_handle - anchor_position).angle_to(DVec2::X); + self.handle_mode = HandleMode::ColinearEquidistant; + } + } + fn add_point_layer_position(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque, layer: LayerNodeIdentifier, viewport: DVec2) { // Add the first point let id = PointId::generate(); @@ -737,7 +886,7 @@ impl Fsm for PenToolFsmState { tool_data.previous_handle_start_pos = latest.handle_start; // Store the handle_end position - let segment = tool_data.segment_end_before_bent; + let segment = tool_data.end_point_segment; 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); @@ -759,7 +908,7 @@ impl Fsm for PenToolFsmState { 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) { + if let Some((latest, segment)) = tool_data.latest_point().zip(tool_data.end_point_segment) { let handle = ManipulatorPointId::EndHandle(segment).get_position(&vector_data); let Some(handle) = handle else { return PenToolFsmState::GRSHandle }; @@ -814,7 +963,7 @@ impl Fsm for PenToolFsmState { }); // 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) { + if let Some(((latest, segment), handle_end)) = tool_data.latest_point().zip(tool_data.end_point_segment).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 }); @@ -866,6 +1015,19 @@ impl Fsm for PenToolFsmState { // Draw the line between the currently-being-placed anchor and its currently-being-dragged-out outgoing handle (opposite the one currently being dragged out) overlay_context.line(next_anchor, next_handle_start, None); + match tool_options.pen_overlay_mode { + PenOverlayMode::AllHandles => { + path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); + } + PenOverlayMode::FrontierHandles => { + if let Some(latest_segment) = tool_data.end_point_segment { + path_overlays(document, DrawHandles::SelectedAnchors(vec![latest_segment]), shape_editor, &mut overlay_context); + } else { + path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context); + }; + } + } + if let (Some(anchor_start), Some(handle_start), Some(handle_end)) = (anchor_start, handle_start, handle_end) { // Draw the line between the most recently placed anchor and its outgoing handle (which is currently influencing the currently-being-placed segment) overlay_context.line(anchor_start, handle_start, None); @@ -878,18 +1040,9 @@ impl Fsm for PenToolFsmState { overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5)); } - match tool_options.pen_overlay_mode { - PenOverlayMode::AllHandles => { - path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); - } - PenOverlayMode::FrontierHandles => { - // Find the last segment ID to have its handles drawn - if let Some(latest_segment) = tool_data.latest_point().and_then(|point| point.in_segment) { - path_overlays(document, DrawHandles::SelectedAnchors(vec![latest_segment]), shape_editor, &mut overlay_context); - } else { - path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context); - }; - } + // Draw the line between the currently-being-placed anchor and last-placed point (Lock angle bent overlays) + if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.snap_angle { + overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5)); } if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, handle_end) { @@ -938,6 +1091,9 @@ impl Fsm for PenToolFsmState { (PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => { responses.add(DocumentMessage::StartTransaction); tool_data.handle_mode = HandleMode::Free; + + // Get the closest point and the segment it is on + tool_data.store_clicked_endpoint(document, input, preferences); 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 @@ -959,11 +1115,13 @@ impl Fsm for PenToolFsmState { // 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.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; + if let Some(layer) = layer { + tool_data.buffering_merged_vector = false; + tool_data.handle_mode = HandleMode::ColinearLocked; + tool_data.bend_from_previous_point(SnapData::new(document, input), transform, layer, preferences); + tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses); + tool_data.buffering_merged_vector = false; + } PenToolFsmState::DraggingHandle(tool_data.handle_mode) } else { if tool_data.handle_end.is_some() { @@ -982,6 +1140,7 @@ impl Fsm for PenToolFsmState { merge_layers(document, current_layer, other_layer, responses); } } + // Even if no buffer was started, the message still has to be run again in order to call bend_from_previous_point tool_data.buffering_merged_vector = true; responses.add(PenToolMessage::DragStart { append_to_selected }); @@ -997,9 +1156,13 @@ impl Fsm for PenToolFsmState { } self } - (PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => tool_data - .finish_placing_handle(SnapData::new(document, input), transform, preferences, responses) - .unwrap_or(PenToolFsmState::PlacingAnchor), + (PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => { + tool_data.end_point = None; + tool_data.draw_mode = DrawMode::ContinuePath; + tool_data + .finish_placing_handle(SnapData::new(document, input), transform, preferences, responses) + .unwrap_or(PenToolFsmState::PlacingAnchor) + } ( PenToolFsmState::DraggingHandle(_), PenToolMessage::PointerMove { @@ -1093,7 +1256,21 @@ impl Fsm for PenToolFsmState { state } - (PenToolFsmState::Ready, PenToolMessage::PointerMove { .. }) => { + ( + PenToolFsmState::Ready, + 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), + }; tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position); responses.add(OverlaysMessage::Draw); self @@ -1143,6 +1320,7 @@ impl Fsm for PenToolFsmState { (PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => { responses.add(DocumentMessage::EndTransaction); tool_data.handle_end = None; + tool_data.draw_mode = DrawMode::BreakPath; tool_data.latest_points.clear(); tool_data.point_index = 0; tool_data.snap_manager.cleanup(responses); @@ -1154,6 +1332,7 @@ impl Fsm for PenToolFsmState { tool_data.handle_end = None; tool_data.latest_points.clear(); tool_data.point_index = 0; + tool_data.draw_mode = DrawMode::BreakPath; tool_data.snap_manager.cleanup(responses); responses.add(OverlaysMessage::Draw);