Skip to content

Commit

Permalink
Add joining of path endpoints with Ctrl+J in the Path tool (#2227)
Browse files Browse the repository at this point in the history
* feat(path-tool): ctrlJ to join endpoints

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
  • Loading branch information
Sidharth-Singh10 and Keavon authored Jan 31, 2025
1 parent 86f09be commit f462963
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 0 deletions.
1 change: 1 addition & 0 deletions editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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=[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(KeyJ); modifiers=[Accel], action_dispatch=ToolMessage::Path(PathToolMessage::ClosePath)),
//
// PenToolMessage
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=PenToolMessage::PointerMove { snap_angle: Shift, break_handle: Alt, lock_angle: Control}),
Expand Down
111 changes: 111 additions & 0 deletions editor/src/messages/tool/common_functionality/shape_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,117 @@ impl ClosestSegment {

// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
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
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);
Expand Down
8 changes: 8 additions & 0 deletions editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub enum PathToolMessage {
extend_selection: Key,
},
Escape,
ClosePath,
FlipSmoothSharp,
GRS {
// Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale)
Expand Down Expand Up @@ -179,6 +180,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated);

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) => {
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);
Expand Down Expand Up @@ -210,6 +217,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
DeselectAllPoints,
BreakPath,
DeleteAndBreakPath,
ClosePath,
),
PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant;
Escape,
Expand Down

0 comments on commit f462963

Please sign in to comment.