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:
parent
a4a2680ac4
commit
a412a77062
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue