From 0553cc51003bd047cd0e5697ef8c00d44fd53e76 Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Mon, 24 Oct 2022 21:55:31 +0100 Subject: [PATCH] Double click to toggle sharpness of an anchor's handles with the Path tool (#815) * Double click to flip sharpness of anchor's handles * Fix bad length * Convert mirror distance to toggle * Revert "Convert mirror distance to toggle" This reverts commit 76b565002265feb29c899840c4b4696b80a220b2. * Fix mirror_distance_between_handles being inverted --- .../tool/common_functionality/shape_editor.rs | 107 ++++++++++++++++++ .../messages/tool/tool_messages/path_tool.rs | 6 +- .../src/layers/vector/manipulator_group.rs | 5 +- 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 910d22bb..07a830a2 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -323,6 +323,113 @@ impl ShapeEditor { } } + /// Handles the flipping between sharp corner and smooth (which can be activated by double clicking on an anchor with the Path tool). + pub fn flip_sharp(&self, document: &Document, position: glam::DVec2, tolerance: f64, responses: &mut VecDeque) -> bool { + let mut process_layer = |layer_path| { + let manipulator_groups = document.layer(layer_path).ok()?.as_subpath()?.manipulator_groups(); + + let transform_to_screenspace = document.generate_transform_relative_to_viewport(layer_path).ok()?; + let mut result = None; + let mut closest_distance_squared = tolerance * tolerance; + + // Find the closest anchor point on the current layer + for (index, (&bezier_id, group)) in manipulator_groups.enumerate().enumerate() { + if let Some(anchor) = &group.points[ManipulatorType::Anchor as usize] { + let screenspace = transform_to_screenspace.transform_point2(anchor.position); + let distance_squared = screenspace.distance_squared(position); + + if distance_squared < closest_distance_squared { + closest_distance_squared = distance_squared; + result = Some((anchor.position, index, bezier_id, group)); + } + } + } + let (anchor_position, index, bezier_id, group) = result?; + + // Check by comparing the handle positions to the anchor if this maniuplator group is a point + let already_sharp = match &group.points { + [_, Some(in_handle), Some(out_handle)] => anchor_position.abs_diff_eq(in_handle.position, f64::EPSILON * 100.) && anchor_position.abs_diff_eq(out_handle.position, f64::EPSILON * 100.), + [_, Some(handle), None] | [_, None, Some(handle)] => anchor_position.abs_diff_eq(handle.position, f64::EPSILON * 100.), + [_, None, None] => true, + }; + + let (in_handle, out_handle) = if already_sharp { + // Grab the next and previous manipulator groups by simply looking at the next / previous index + // TODO: Wrapping around on a closed path + let previous_position = index.checked_sub(1).and_then(|index| manipulator_groups.by_index(index)).and_then(|group| group.points[0].as_ref()); + let next_position = manipulator_groups.by_index(index + 1).and_then(|group| group.points[0].as_ref()); + + // To find the length of the new tangent we just take the distance to the anchor and divide by 3 (pretty arbitrary) + let length_previous = previous_position.map(|point| (point.position - anchor_position).length() / 3.); + let length_next = next_position.map(|point| (point.position - anchor_position).length() / 3.); + + // Use the position relative to the anchor + let relative_previous_normalised = previous_position.map(|point| (point.position - anchor_position).normalize()); + let relative_next_normalised = next_position.map(|point| (point.position - anchor_position).normalize()); + + // The direction of the handles is either the perpendicular vector to the sum of the anchors' positions or just the anchor's position (if only one) + let handle_direction = match (relative_previous_normalised, relative_next_normalised) { + (Some(previous), Some(next)) => DVec2::new(previous.y + next.y, -(previous.x + next.x)), + (None, Some(val)) => -val, + (Some(val), None) => val, + (None, None) => return None, + }; + + // Mirror the angle but not the distance + responses.push_back( + Operation::SetManipulatorHandleMirroring { + layer_path: layer_path.to_vec(), + id: bezier_id, + mirror_distance: false, + mirror_angle: true, + } + .into(), + ); + + let mut handle_vector = handle_direction.normalize(); + + // Flip the vector if it is not facing towards the same direction as the anchor + if relative_previous_normalised.filter(|pos| pos.dot(handle_vector) < 0.).is_some() || relative_next_normalised.filter(|pos| pos.dot(handle_vector) > 0.).is_some() { + handle_vector = -handle_vector; + } + + ( + length_previous.map(|length| anchor_position + handle_vector * length), + length_next.map(|length| anchor_position - handle_vector * length), + ) + } else { + (Some(anchor_position), Some(anchor_position)) + }; + + // Push both in and out handles into the correct position + if let Some(in_handle) = in_handle { + let in_handle = Operation::SetManipulatorPoints { + layer_path: layer_path.to_vec(), + id: bezier_id, + manipulator_type: ManipulatorType::InHandle, + position: Some(in_handle.into()), + }; + responses.push_back(in_handle.into()); + } + if let Some(out_handle) = out_handle { + let out_handle = Operation::SetManipulatorPoints { + layer_path: layer_path.to_vec(), + id: bezier_id, + manipulator_type: ManipulatorType::OutHandle, + position: Some(out_handle.into()), + }; + responses.push_back(out_handle.into()); + } + Some(true) + }; + for layer_path in &self.selected_layers { + if let Some(result) = process_layer(layer_path) { + return result; + } + } + false + } + fn shape<'a>(&'a self, document: &'a Document, layer_id: &[u64]) -> Option<&'a Subpath> { document.layer(layer_id).ok()?.as_subpath() } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 2f72f120..14b368d7 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -281,7 +281,11 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } (_, PathToolMessage::InsertPoint) => { - tool_data.shape_editor.split(&document.graphene_document, input.mouse.position, SELECTION_TOLERANCE, responses); + // First we try and flip the sharpness (if they have clicked on an anchor) + if !tool_data.shape_editor.flip_sharp(&document.graphene_document, input.mouse.position, SELECTION_TOLERANCE, responses) { + // If not, then we try and split the path that may have been clicked upon + tool_data.shape_editor.split(&document.graphene_document, input.mouse.position, SELECTION_TOLERANCE, responses); + } self } diff --git a/graphene/src/layers/vector/manipulator_group.rs b/graphene/src/layers/vector/manipulator_group.rs index 85a9d095..43eacd94 100644 --- a/graphene/src/layers/vector/manipulator_group.rs +++ b/graphene/src/layers/vector/manipulator_group.rs @@ -96,8 +96,7 @@ impl ManipulatorGroup { /// Move the selected points by the provided transform. pub fn move_selected_points(&mut self, delta: DVec2) { let mirror_angle = self.editor_state.mirror_angle_between_handles; - // Invert distance since we want it to start disabled - let mirror_distance = !self.editor_state.mirror_distance_between_handles; + let mirror_distance = self.editor_state.mirror_distance_between_handles; // Move the point absolutely or relatively depending on if the point is under the cursor (the last selected point) let move_point = |point: &mut ManipulatorPoint, delta: DVec2| { @@ -299,7 +298,7 @@ impl Default for ManipulatorGroupEditorState { fn default() -> Self { Self { mirror_angle_between_handles: true, - mirror_distance_between_handles: true, + mirror_distance_between_handles: false, } } }