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 <keavon@keavon.com>
This commit is contained in:
zhiyuan 2024-02-05 13:02:09 +08:00 committed by GitHub
parent a4a2680ac4
commit a412a77062
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 162 additions and 1 deletions

View File

@ -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),

View File

@ -675,6 +675,147 @@ impl ShapeState {
}
}
pub fn break_path_at_selected_point(&self, document_network: &NodeNetwork, responses: &mut VecDeque<Message>) {
for (&layer, state) in &self.selected_shape_state {
let Some(subpaths) = get_subpaths(layer, document_network) else {
continue;
};
let mut broken_subpaths = Vec::<bezier_rs::Subpath<ManipulatorGroupId>>::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<Vec<ManipulatorGroup<ManipulatorGroupId>>> = None;
let mut last_manipulator_group: Option<&ManipulatorGroup<ManipulatorGroupId>> = 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<Message>) {
for (&layer, state) in &self.selected_shape_state {
let Some(subpaths) = get_subpaths(layer, document_network) else {
continue;
};
let mut broken_subpaths = Vec::<bezier_rs::Subpath<ManipulatorGroupId>>::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<Vec<ManipulatorGroup<ManipulatorGroupId>>> = 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<Message>) {
for (&layer, state) in &self.selected_shape_state {

View File

@ -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<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
NudgeSelectedPoints,
Enter,
SelectAllPoints,
BreakPath,
DeleteAndBreakPath,
),
Dragging => actions!(PathToolMessageDiscriminant;
InsertPoint,
@ -166,6 +170,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
PointerMove,
Delete,
SelectAllPoints,
BreakPath,
DeleteAndBreakPath,
),
DrawingBox => actions!(PathToolMessageDiscriminant;
InsertPoint,
@ -174,6 +180,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> 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) {