Skip to content

Feat: Merge selected near points with path tool #2966

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.;
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
pub const SEGMENT_SELECTED_THICKNESS: f64 = 3.;
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
pub const POINT_MERGE_THRESHOLD: f64 = 10.;

// PEN TOOL
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
Expand Down
160 changes: 159 additions & 1 deletion editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::select_tool::extend_lasso;
use super::tool_prelude::*;
use crate::consts::{
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DEFAULT_STROKE_WIDTH, DOUBLE_CLICK_MILLISECONDS, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD,
DRILL_THROUGH_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE,
DRILL_THROUGH_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, POINT_MERGE_THRESHOLD, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE,
};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
Expand Down Expand Up @@ -125,6 +125,7 @@ pub enum PathToolMessage {
UpdateSelectedPointsStatus {
overlay_context: OverlayContext,
},
MergeSelectedPoints,
Copy {
clipboard: Clipboard,
},
Expand Down Expand Up @@ -293,6 +294,12 @@ impl LayoutHolder for PathTool {
.disabled(!self.tool_data.make_path_editable_is_allowed)
.widget_holder();

let merge_button = IconButton::new("Folder", 24)
.tooltip("Merge selected points")
.on_update(|_| PathToolMessage::MergeSelectedPoints.into())
.disabled(!self.tool_data.merging_points_enabled)
.widget_holder();

let [_checkbox, _dropdown] = {
let pivot_gizmo_type_widget = pivot_gizmo_type_widget(self.tool_data.pivot_gizmo.state, PivotToolSource::Path);
[pivot_gizmo_type_widget[0].clone(), pivot_gizmo_type_widget[2].clone()]
Expand Down Expand Up @@ -324,6 +331,9 @@ impl LayoutHolder for PathTool {
path_overlay_mode_widget,
unrelated_seperator.clone(),
path_node_button,
unrelated_seperator.clone(),
merge_button,
unrelated_seperator.clone(),
// checkbox.clone(),
// related_seperator.clone(),
// dropdown.clone(),
Expand Down Expand Up @@ -418,6 +428,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
DeleteAndBreakPath,
ClosePath,
PointerMove,
MergeSelectedPoints,
Copy,
Cut,
DeleteSelected,
Expand Down Expand Up @@ -571,6 +582,7 @@ struct PathToolData {
hovered_layers: Vec<LayerNodeIdentifier>,
ghost_outline: Vec<(Vec<ClickTargetType>, LayerNodeIdentifier)>,
make_path_editable_is_allowed: bool,
merging_points_enabled: bool,
}

impl PathToolData {
Expand Down Expand Up @@ -632,6 +644,64 @@ impl PathToolData {
self.selection_status = selection_status;
}

fn update_merge_point_toggle(&mut self, shape_editor: &ShapeState, document: &DocumentMessageHandler, vector_meshes: bool) {
let mut non_empty_layers = shape_editor.selected_shape_state.iter().filter(|(_, state)| !state.is_empty());
let Some((layer, _)) = non_empty_layers.next() else {
self.merging_points_enabled = false;
return;
};

if non_empty_layers.next().is_some() {
self.merging_points_enabled = false;
return;
}

if !vector_meshes {
// Check that only two points are selected such that they are endpoints
let all_anchors = shape_editor.selected_points().all(|point| matches!(point, ManipulatorPointId::Anchor(_)));
let points = shape_editor
.selected_points()
.filter_map(|point| if let ManipulatorPointId::Anchor(anchor) = point { Some(anchor) } else { None })
.collect::<Vec<_>>();

if points.len() == 2 && all_anchors {
let Some(layer) = shape_editor.selected_layers().next() else { return };
let Some(vector) = document.network_interface.compute_modified_vector(*layer) else { return };
if points.iter().all(|point| vector.all_connected(**point).count() == 1) {
self.merging_points_enabled = true;
return;
}
}
self.merging_points_enabled = false;
return;
}

let points = shape_editor.selected_points().collect::<Vec<_>>();
let all_anchors = points.iter().all(|point| matches!(point, ManipulatorPointId::Anchor(_)));

if points.len() < 2 || !all_anchors {
self.merging_points_enabled = false;
return;
}

let Some(vector) = document.network_interface.compute_modified_vector(*layer) else { return };
let positions = points.iter().filter_map(|point| point.get_position(&vector)).collect::<Vec<_>>();

let mut sum = DVec2::default();
for position in &positions {
sum += position;
}
let centroid = sum / (positions.len() as f64);

for position in positions {
if position.distance(centroid) > POINT_MERGE_THRESHOLD {
self.merging_points_enabled = false;
return;
}
}
self.merging_points_enabled = true;
}

fn remove_saved_points(&mut self) {
self.saved_points_before_anchor_select_toggle.clear();
}
Expand Down Expand Up @@ -3009,6 +3079,10 @@ impl Fsm for PathToolFsmState {

tool_data.make_path_editable_is_allowed = make_path_editable_is_allowed(&document.network_interface, document.metadata()).is_some();
tool_data.update_selection_status(shape_editor, document);
tool_data.update_merge_point_toggle(shape_editor, document, tool_action_data.preferences.vector_meshes);

// TODO: Here add a toggle for the disable of the merge points button

self
}
(_, PathToolMessage::ManipulatorMakeHandlesColinear) => {
Expand Down Expand Up @@ -3036,6 +3110,90 @@ impl Fsm for PathToolFsmState {

self
}
(_, PathToolMessage::MergeSelectedPoints) => {
// Assuming that all these points are on the same layer
let mut non_empty_layers = shape_editor.selected_shape_state.iter().filter(|(_, state)| !state.is_empty());

// If all layers are empty, or no layer selected
let Some((layer, _)) = non_empty_layers.next() else {
return PathToolFsmState::Ready;
};

// If selected points are of more than one layer
if non_empty_layers.next().is_some() {
return PathToolFsmState::Ready;
}

let points = shape_editor.selected_points().collect::<Vec<_>>();
let all_anchors = points.iter().all(|point| matches!(point, ManipulatorPointId::Anchor(_)));
if points.len() < 2 || !all_anchors {
return PathToolFsmState::Ready;
}

responses.add(DocumentMessage::StartTransaction);
let state = shape_editor.selected_shape_state.get(layer).expect("No state for selected layer");
let points = state
.selected_points()
.filter_map(|point| if let ManipulatorPointId::Anchor(anchor) = point { Some(anchor) } else { None });

// Calculate the centroid
if let Some(vector) = document.network_interface.compute_modified_vector(*layer) {
let positions = points.filter_map(|point| ManipulatorPointId::Anchor(point).get_position(&vector)).collect::<Vec<_>>();
let mut sum = DVec2::default();
for position in &positions {
sum += position;
}
let centroid = sum / (positions.len() as f64);

for position in &positions {
if position.distance(centroid) > POINT_MERGE_THRESHOLD {
return PathToolFsmState::Ready;
}
}

// Add a new point with the new coordinates
let new_id = PointId::generate();
let modification_type = VectorModificationType::InsertPoint { id: new_id, position: centroid };
responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type });

// Remove old points
for point in state
.selected_points()
.filter_map(|point| if let ManipulatorPointId::Anchor(anchor) = point { Some(anchor) } else { None })
{
let modification_type = VectorModificationType::RemovePoint { id: point };
responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type });
}

let handles = |bezier: Bezier| -> [Option<DVec2>; 2] {
match bezier.handles {
BezierHandles::Linear => [None, None],
BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None],
BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)],
}
};

// Find those segments which were connected to just one of the selected points
for (_, bezier, start, end) in vector.segment_bezier_iter() {
let id = SegmentId::generate();

if state.is_point_selected(ManipulatorPointId::Anchor(start)) {
let points = [new_id, end];
let handles = handles(bezier);
let modification_type = VectorModificationType::InsertSegment { id, points, handles };
responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type });
} else if state.is_point_selected(ManipulatorPointId::Anchor(end)) {
let points = [start, new_id];
let handles = handles(bezier);
let modification_type = VectorModificationType::InsertSegment { id, points, handles };
responses.add(GraphOperationMessage::Vector { layer: *layer, modification_type });
}
}
responses.add(DocumentMessage::EndTransaction);
}

PathToolFsmState::Ready
}
(_, _) => PathToolFsmState::Ready,
}
}
Expand Down