mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 05:58:12 +08:00
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:
co-authored by
Keavon Chambers
parent
a4a2680ac4
commit
a412a77062
@@ -174,9 +174,13 @@ pub fn default_mapping() -> Mapping {
|
|||||||
entry!(KeyUp(Lmb); action_dispatch=LineToolMessage::DragStop),
|
entry!(KeyUp(Lmb); action_dispatch=LineToolMessage::DragStop),
|
||||||
entry!(KeyDown(Rmb); action_dispatch=LineToolMessage::Abort),
|
entry!(KeyDown(Rmb); action_dispatch=LineToolMessage::Abort),
|
||||||
entry!(KeyDown(Escape); 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
|
// 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!(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!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PathToolMessage::PointerMove { alt: Alt, shift: Shift }),
|
||||||
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
||||||
|
|||||||
@@ -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.
|
/// Toggle if the handles should mirror angle across the anchor position.
|
||||||
pub fn toggle_handle_mirroring_on_selected(&self, responses: &mut VecDeque<Message>) {
|
pub fn toggle_handle_mirroring_on_selected(&self, responses: &mut VecDeque<Message>) {
|
||||||
for (&layer, state) in &self.selected_shape_state {
|
for (&layer, state) in &self.selected_shape_state {
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ pub enum PathToolMessage {
|
|||||||
SelectionChanged,
|
SelectionChanged,
|
||||||
|
|
||||||
// Tool-specific messages
|
// Tool-specific messages
|
||||||
|
BreakPath,
|
||||||
Delete,
|
Delete,
|
||||||
|
DeleteAndBreakPath,
|
||||||
DragStart {
|
DragStart {
|
||||||
add_to_selection: Key,
|
add_to_selection: Key,
|
||||||
},
|
},
|
||||||
@@ -159,6 +161,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||||||
NudgeSelectedPoints,
|
NudgeSelectedPoints,
|
||||||
Enter,
|
Enter,
|
||||||
SelectAllPoints,
|
SelectAllPoints,
|
||||||
|
BreakPath,
|
||||||
|
DeleteAndBreakPath,
|
||||||
),
|
),
|
||||||
Dragging => actions!(PathToolMessageDiscriminant;
|
Dragging => actions!(PathToolMessageDiscriminant;
|
||||||
InsertPoint,
|
InsertPoint,
|
||||||
@@ -166,6 +170,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||||||
PointerMove,
|
PointerMove,
|
||||||
Delete,
|
Delete,
|
||||||
SelectAllPoints,
|
SelectAllPoints,
|
||||||
|
BreakPath,
|
||||||
|
DeleteAndBreakPath,
|
||||||
),
|
),
|
||||||
DrawingBox => actions!(PathToolMessageDiscriminant;
|
DrawingBox => actions!(PathToolMessageDiscriminant;
|
||||||
InsertPoint,
|
InsertPoint,
|
||||||
@@ -174,6 +180,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||||||
Delete,
|
Delete,
|
||||||
Enter,
|
Enter,
|
||||||
SelectAllPoints,
|
SelectAllPoints,
|
||||||
|
BreakPath,
|
||||||
|
DeleteAndBreakPath,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,6 +426,14 @@ impl Fsm for PathToolFsmState {
|
|||||||
|
|
||||||
PathToolFsmState::Ready
|
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) => {
|
(_, PathToolMessage::InsertPoint) => {
|
||||||
// First we try and flip the sharpness (if they have clicked on an anchor)
|
// 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) {
|
if !shape_editor.flip_sharp(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses) {
|
||||||
|
|||||||
Reference in New Issue
Block a user