Add joining of path endpoints with Ctrl+J in the Path tool (#2227)
* feat(path-tool): ctrlJ to join endpoints * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
86f09be0ee
commit
f462963a36
|
|
@ -251,6 +251,7 @@ pub fn input_mappings() -> Mapping {
|
||||||
entry!(KeyDown(ArrowDown); modifiers=[ArrowRight], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }),
|
entry!(KeyDown(ArrowDown); modifiers=[ArrowRight], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }),
|
||||||
entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowLeft], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
|
entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowLeft], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
|
||||||
entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowRight], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
|
entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowRight], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
|
||||||
|
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=ToolMessage::Path(PathToolMessage::ClosePath)),
|
||||||
//
|
//
|
||||||
// PenToolMessage
|
// PenToolMessage
|
||||||
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=PenToolMessage::PointerMove { snap_angle: Shift, break_handle: Alt, lock_angle: Control}),
|
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=PenToolMessage::PointerMove { snap_angle: Shift, break_handle: Alt, lock_angle: Control}),
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,117 @@ impl ClosestSegment {
|
||||||
|
|
||||||
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
|
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
|
||||||
impl ShapeState {
|
impl ShapeState {
|
||||||
|
pub fn close_selected_path(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
// First collect all selected anchor points across all layers
|
||||||
|
let all_selected_points: Vec<(LayerNodeIdentifier, PointId)> = self
|
||||||
|
.selected_shape_state
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(&layer, state)| {
|
||||||
|
if document.network_interface.compute_modified_vector(layer).is_none() {
|
||||||
|
return Vec::new().into_iter();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collect selected anchor points from this layer
|
||||||
|
state
|
||||||
|
.selected_points
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&point| if let ManipulatorPointId::Anchor(id) = point { Some((layer, id)) } else { None })
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// If exactly two points are selected (regardless of layer), connect them
|
||||||
|
if all_selected_points.len() == 2 {
|
||||||
|
let (layer1, start_point) = all_selected_points[0];
|
||||||
|
let (layer2, end_point) = all_selected_points[1];
|
||||||
|
|
||||||
|
let Some(vector_data1) = document.network_interface.compute_modified_vector(layer1) else { return };
|
||||||
|
let Some(vector_data2) = document.network_interface.compute_modified_vector(layer2) else { return };
|
||||||
|
|
||||||
|
if vector_data1.all_connected(start_point).count() != 1 || vector_data2.all_connected(end_point).count() != 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if layer1 == layer2 {
|
||||||
|
if start_point == end_point {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let segment_id = SegmentId::generate();
|
||||||
|
let modification_type = VectorModificationType::InsertSegment {
|
||||||
|
id: segment_id,
|
||||||
|
points: [end_point, start_point],
|
||||||
|
handles: [None, None],
|
||||||
|
};
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer: layer1, modification_type });
|
||||||
|
}
|
||||||
|
// TODO: Fix the implementation of this case so it actually connects the separate layers, see:
|
||||||
|
// TODO: <https://github.com/GraphiteEditor/Graphite/pull/2227#issuecomment-2626342475>
|
||||||
|
else {
|
||||||
|
// Points are in different layers - find the topmost layer
|
||||||
|
let top_layer = document.metadata().all_layers().find(|&layer| layer == layer1 || layer == layer2).unwrap_or(layer1);
|
||||||
|
|
||||||
|
let bottom_layer = if top_layer == layer1 { layer2 } else { layer1 };
|
||||||
|
let bottom_point = if top_layer == layer1 { end_point } else { start_point };
|
||||||
|
|
||||||
|
// Get position of point in bottom layer
|
||||||
|
let Some(bottom_vector_data) = document.network_interface.compute_modified_vector(bottom_layer) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(point_pos) = bottom_vector_data.point_domain.position_from_id(bottom_point) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create new point in top layer
|
||||||
|
let new_point_id = PointId::generate();
|
||||||
|
let modification_type = VectorModificationType::InsertPoint {
|
||||||
|
id: new_point_id,
|
||||||
|
position: point_pos,
|
||||||
|
};
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer: top_layer, modification_type });
|
||||||
|
|
||||||
|
// Create segment between points in top layer
|
||||||
|
let segment_id = SegmentId::generate();
|
||||||
|
let points = if top_layer == layer1 { [start_point, new_point_id] } else { [new_point_id, end_point] };
|
||||||
|
|
||||||
|
let modification_type = VectorModificationType::InsertSegment {
|
||||||
|
id: segment_id,
|
||||||
|
points,
|
||||||
|
handles: [None, None],
|
||||||
|
};
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer: top_layer, modification_type });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no points are selected, try to find a single continuous subpath in each layer to connect the endpoints of
|
||||||
|
for &layer in self.selected_shape_state.keys() {
|
||||||
|
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||||
|
|
||||||
|
let endpoints: Vec<PointId> = vector_data
|
||||||
|
.point_domain
|
||||||
|
.ids()
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|&point_id| vector_data.all_connected(point_id).count() == 1)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if endpoints.len() == 2 {
|
||||||
|
let start_point = endpoints[0];
|
||||||
|
let end_point = endpoints[1];
|
||||||
|
|
||||||
|
let segment_id = SegmentId::generate();
|
||||||
|
let modification_type = VectorModificationType::InsertSegment {
|
||||||
|
id: segment_id,
|
||||||
|
points: [end_point, start_point],
|
||||||
|
handles: [None, None],
|
||||||
|
};
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Snap, returning a viewport delta
|
// Snap, returning a viewport delta
|
||||||
pub fn snap(&self, snap_manager: &mut SnapManager, snap_cache: &SnapCache, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, previous_mouse: DVec2) -> DVec2 {
|
pub fn snap(&self, snap_manager: &mut SnapManager, snap_cache: &SnapCache, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, previous_mouse: DVec2) -> DVec2 {
|
||||||
let snap_data = SnapData::new_snap_cache(document, input, snap_cache);
|
let snap_data = SnapData::new_snap_cache(document, input, snap_cache);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ pub enum PathToolMessage {
|
||||||
extend_selection: Key,
|
extend_selection: Key,
|
||||||
},
|
},
|
||||||
Escape,
|
Escape,
|
||||||
|
ClosePath,
|
||||||
FlipSmoothSharp,
|
FlipSmoothSharp,
|
||||||
GRS {
|
GRS {
|
||||||
// Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale)
|
// Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale)
|
||||||
|
|
@ -179,6 +180,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
||||||
let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated);
|
let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated);
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
|
ToolMessage::Path(PathToolMessage::ClosePath) => {
|
||||||
|
responses.add(DocumentMessage::AddTransaction);
|
||||||
|
tool_data.shape_editor.close_selected_path(tool_data.document, responses);
|
||||||
|
responses.add(DocumentMessage::EndTransaction);
|
||||||
|
responses.add(OverlaysMessage::Draw);
|
||||||
|
}
|
||||||
ToolMessage::Path(PathToolMessage::SwapSelectedHandles) => {
|
ToolMessage::Path(PathToolMessage::SwapSelectedHandles) => {
|
||||||
if tool_data.shape_editor.handle_with_pair_selected(&tool_data.document.network_interface) {
|
if tool_data.shape_editor.handle_with_pair_selected(&tool_data.document.network_interface) {
|
||||||
tool_data.shape_editor.alternate_selected_handles(&tool_data.document.network_interface);
|
tool_data.shape_editor.alternate_selected_handles(&tool_data.document.network_interface);
|
||||||
|
|
@ -210,6 +217,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
||||||
DeselectAllPoints,
|
DeselectAllPoints,
|
||||||
BreakPath,
|
BreakPath,
|
||||||
DeleteAndBreakPath,
|
DeleteAndBreakPath,
|
||||||
|
ClosePath,
|
||||||
),
|
),
|
||||||
PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant;
|
PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant;
|
||||||
Escape,
|
Escape,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue