From a412a770620c50b23b4c0ded7875cd49b8968169 Mon Sep 17 00:00:00 2001 From: zhiyuan <32867472+zhiyuang@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:02:09 +0800 Subject: [PATCH] Support for deleting points to break path (#1593) * feat: break closed curve * feat: update hotkeys and handles * feat: break an open path * feat: elegantly handle breaking at multi points in a subpath * feat: handle break at end points * feat: ctrl+delete to remove segments and break path * fix: rm unused * First code review pass * fix: closed eclipse handles after breaking path --------- Co-authored-by: Keavon Chambers --- .../messages/input_mapper/default_mapping.rs | 6 +- .../tool/common_functionality/shape_editor.rs | 141 ++++++++++++++++++ .../messages/tool/tool_messages/path_tool.rs | 16 ++ 3 files changed, 162 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index 3c6f47f6..2aed6d66 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -174,9 +174,13 @@ pub fn default_mapping() -> Mapping { entry!(KeyUp(Lmb); action_dispatch=LineToolMessage::DragStop), entry!(KeyDown(Rmb); action_dispatch=LineToolMessage::Abort), entry!(KeyDown(Escape); action_dispatch=LineToolMessage::Abort), - entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=LineToolMessage::PointerMove { center: Alt, lock_angle: Control, snap_angle: Shift }), + entry!(PointerMove; refresh_keys=[Alt, Control, Shift], action_dispatch=LineToolMessage::PointerMove { center: Alt, lock_angle: Control, snap_angle: Shift }), // // PathToolMessage + entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath), + 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(Lmb); action_dispatch=PathToolMessage::DragStart { add_to_selection: Shift }), entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PathToolMessage::PointerMove { alt: Alt, shift: Shift }), entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete), diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index cd123ef5..a2f37c6a 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -675,6 +675,147 @@ impl ShapeState { } } + pub fn break_path_at_selected_point(&self, document_network: &NodeNetwork, responses: &mut VecDeque) { + for (&layer, state) in &self.selected_shape_state { + let Some(subpaths) = get_subpaths(layer, document_network) else { + continue; + }; + + let mut broken_subpaths = Vec::>::new(); + + for subpath in subpaths { + let mut points: Vec<_> = state + .selected_points + .iter() + .filter_map(|&point| { + let Some(manipulator_index) = subpath.manipulator_index_from_id(point.group) else { + return None; + }; + let Some(manipulator) = subpath.manipulator_from_id(point.group) else { + return None; + }; + Some((manipulator_index, manipulator)) + }) + .collect(); + + if points.is_empty() { + broken_subpaths.push(subpath.clone()); + continue; + } + + points.sort_by(|&a, &b| match a.0 > b.0 { + true => std::cmp::Ordering::Greater, + false => std::cmp::Ordering::Less, + }); + + let mut last_manipulator_index = 0; + let mut to_extend_with_last_group: Option>> = None; + let mut last_manipulator_group: Option<&ManipulatorGroup> = None; + for (i, &(manipulator_index, group)) in points.iter().enumerate() { + if manipulator_index == 0 && !subpath.closed { + last_manipulator_index = manipulator_index + 1; + last_manipulator_group = Some(group); + continue; + } + + let mut segment = subpath.manipulator_groups()[last_manipulator_index..manipulator_index].to_vec(); + if i != 0 { + segment.insert(0, ManipulatorGroup::new(last_manipulator_group.unwrap().anchor, None, last_manipulator_group.unwrap().out_handle)); + } + + segment.push(ManipulatorGroup::new(group.anchor, group.in_handle, None)); + + if subpath.closed && i == 0 { + to_extend_with_last_group = Some(segment); + } else { + broken_subpaths.push(bezier_rs::Subpath::new(segment, false)); + } + + last_manipulator_index = manipulator_index + 1; + last_manipulator_group = Some(group); + } + + if last_manipulator_index == subpath.len() && !subpath.closed { + continue; + } + + let mut final_segment = subpath.manipulator_groups()[last_manipulator_index..].to_vec(); + final_segment.insert(0, ManipulatorGroup::new(last_manipulator_group.unwrap().anchor, None, last_manipulator_group.unwrap().out_handle)); + + if let Some(group) = to_extend_with_last_group { + final_segment.extend(group); + } + + broken_subpaths.push(bezier_rs::Subpath::new(final_segment, false)); + } + + responses.add(GraphOperationMessage::Vector { + layer, + modification: VectorDataModification::UpdateSubpaths { subpaths: broken_subpaths }, + }); + } + } + + /// Delete point(s) and adjacent segments, which breaks a closed path as open, or an open path into multiple. + pub fn delete_point_and_break_path(&self, document_network: &NodeNetwork, responses: &mut VecDeque) { + for (&layer, state) in &self.selected_shape_state { + let Some(subpaths) = get_subpaths(layer, document_network) else { + continue; + }; + + let mut broken_subpaths = Vec::>::with_capacity(subpaths.len()); + + for subpath in subpaths { + let mut selected_points: Vec<_> = state.selected_points.iter().filter_map(|&point| subpath.manipulator_index_from_id(point.group)).collect(); + + if selected_points.is_empty() { + broken_subpaths.push(subpath.clone()); + continue; + } + + selected_points.sort_by(|&a, &b| match a > b { + true => std::cmp::Ordering::Greater, + false => std::cmp::Ordering::Less, + }); + + let mut last_manipulator_index = 0; + let mut to_extend_with_last_group: Option>> = None; + for (i, &manipulator_index) in selected_points.iter().enumerate() { + if (manipulator_index == 0 || manipulator_index == 1) && !subpath.closed { + last_manipulator_index = manipulator_index + 1; + continue; + } + + let segment = subpath.manipulator_groups()[last_manipulator_index..manipulator_index].to_vec(); + if subpath.closed && i == 0 { + to_extend_with_last_group = Some(segment); + } else { + broken_subpaths.push(bezier_rs::Subpath::new(segment, false)); + } + + last_manipulator_index = manipulator_index + 1; + } + + if (last_manipulator_index == subpath.len() || last_manipulator_index == subpath.len() - 1) && !subpath.closed { + continue; + } + + let mut final_segment = subpath.manipulator_groups()[last_manipulator_index..].to_vec(); + + if let Some(group) = to_extend_with_last_group { + final_segment.extend(group); + } + + broken_subpaths.push(bezier_rs::Subpath::new(final_segment, false)); + } + + responses.add(GraphOperationMessage::Vector { + layer, + modification: VectorDataModification::UpdateSubpaths { subpaths: broken_subpaths }, + }); + } + } + /// Toggle if the handles should mirror angle across the anchor position. pub fn toggle_handle_mirroring_on_selected(&self, responses: &mut VecDeque) { for (&layer, state) in &self.selected_shape_state { diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 220c97cf..069cc27e 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -32,7 +32,9 @@ pub enum PathToolMessage { SelectionChanged, // Tool-specific messages + BreakPath, Delete, + DeleteAndBreakPath, DragStart { add_to_selection: Key, }, @@ -159,6 +161,8 @@ impl<'a> MessageHandler> for PathToo NudgeSelectedPoints, Enter, SelectAllPoints, + BreakPath, + DeleteAndBreakPath, ), Dragging => actions!(PathToolMessageDiscriminant; InsertPoint, @@ -166,6 +170,8 @@ impl<'a> MessageHandler> for PathToo PointerMove, Delete, SelectAllPoints, + BreakPath, + DeleteAndBreakPath, ), DrawingBox => actions!(PathToolMessageDiscriminant; InsertPoint, @@ -174,6 +180,8 @@ impl<'a> MessageHandler> for PathToo Delete, Enter, SelectAllPoints, + BreakPath, + DeleteAndBreakPath, ), } } @@ -418,6 +426,14 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } + (_, PathToolMessage::BreakPath) => { + shape_editor.break_path_at_selected_point(&document.network, responses); + PathToolFsmState::Ready + } + (_, PathToolMessage::DeleteAndBreakPath) => { + shape_editor.delete_point_and_break_path(&document.network, responses); + PathToolFsmState::Ready + } (_, PathToolMessage::InsertPoint) => { // First we try and flip the sharpness (if they have clicked on an anchor) if !shape_editor.flip_sharp(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses) {