Skip to content

Commit d4c2277

Browse files
committed
Fixes broken multi handle snap for path tool (#2969)
1 parent 57055e3 commit d4c2277

File tree

2 files changed

+96
-115
lines changed

2 files changed

+96
-115
lines changed

editor/src/messages/tool/common_functionality/shape_editor.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::graph_modification_utils::merge_layers;
2-
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
2+
use super::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint};
33
use super::utility_functions::{adjust_handle_colinearity, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position};
44
use crate::consts::HANDLE_LENGTH_FACTOR;
55
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments_for_layer;
@@ -502,7 +502,6 @@ impl ShapeState {
502502
}
503503
}
504504

505-
// Snap, returning a viewport delta
506505
pub fn snap(
507506
&self,
508507
snap_manager: &mut SnapManager,
@@ -511,6 +510,8 @@ impl ShapeState {
511510
input: &InputPreprocessorMessageHandler,
512511
viewport: &ViewportMessageHandler,
513512
previous_mouse: DVec2,
513+
constraint: SnapConstraint,
514+
accumulated_offset: DVec2,
514515
) -> DVec2 {
515516
let snap_data = SnapData::new_snap_cache(document, input, viewport, snap_cache);
516517

@@ -536,7 +537,8 @@ impl ShapeState {
536537
};
537538

538539
let Some(position) = selected.get_position(&vector) else { continue };
539-
let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source);
540+
let point_position = to_document.transform_point2(position);
541+
let mut point = SnapCandidatePoint::new_source(point_position + mouse_delta, source);
540542

541543
if let Some(id) = selected.as_anchor() {
542544
for neighbor in vector.connected_points(id) {
@@ -548,14 +550,42 @@ impl ShapeState {
548550
}
549551
}
550552

551-
let snapped = snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default());
553+
let snapped = match constraint {
554+
SnapConstraint::Line { origin, direction } => {
555+
let projected_origin = origin + (point_position - origin).project_onto(direction);
556+
let constraint = SnapConstraint::Line { origin: projected_origin, direction };
557+
snap_manager.constrained_snap(&snap_data, &point, constraint, SnapTypeConfiguration::default())
558+
}
559+
SnapConstraint::Direction(direction) => {
560+
let origin = point_position - accumulated_offset;
561+
let constraint = SnapConstraint::Line { origin, direction };
562+
snap_manager.constrained_snap(&snap_data, &point, constraint, SnapTypeConfiguration::default())
563+
}
564+
_ => snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default()),
565+
};
566+
552567
if best_snapped.other_snap_better(&snapped) {
553568
offset = snapped.snapped_point_document - point.document_point + mouse_delta;
554569
best_snapped = snapped;
555570
}
556571
}
557572
}
573+
let is_snapped = best_snapped.is_snapped();
558574
snap_manager.update_indicator(best_snapped);
575+
576+
// If no magnetic snap occurred but we have a constraint, force the constraint
577+
if !is_snapped {
578+
match constraint {
579+
SnapConstraint::Direction(direction) => {
580+
let constrained_total_offset = (accumulated_offset + mouse_delta).project_onto(direction);
581+
offset = constrained_total_offset - accumulated_offset;
582+
}
583+
SnapConstraint::Line { direction, .. } if direction != DVec2::ZERO => {
584+
offset = mouse_delta.project_onto(direction);
585+
}
586+
_ => {}
587+
}
588+
}
559589
document.metadata().document_to_viewport.transform_vector2(offset)
560590
}
561591

editor/src/messages/tool/tool_messages/path_tool.rs

Lines changed: 62 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,8 @@ struct PathToolData {
598598
started_drawing_from_inside: bool,
599599
first_selected_with_single_click: bool,
600600
stored_selection: Option<HashMap<LayerNodeIdentifier, SelectedLayerState>>,
601+
snap_offset: DVec2,
602+
601603
last_drill_through_click_position: Option<DVec2>,
602604
drill_through_cycle_index: usize,
603605
drill_through_cycle_count: usize,
@@ -730,6 +732,7 @@ impl PathToolData {
730732
self.opposing_handle_lengths = None;
731733

732734
self.drag_start_pos = input.mouse.position;
735+
self.snap_offset = DVec2::ZERO;
733736

734737
if input.time - self.last_click_time > DOUBLE_CLICK_MILLISECONDS {
735738
self.saved_points_before_anchor_convert_smooth_sharp.clear();
@@ -1143,50 +1146,6 @@ impl PathToolData {
11431146
document.metadata().document_to_viewport.transform_vector2(snap_result.snapped_point_document - handle_position)
11441147
}
11451148

1146-
fn start_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
1147-
// Find the negative delta to take the point to the drag start position
1148-
let current_mouse = input.mouse.position;
1149-
let drag_start = self.drag_start_pos;
1150-
let opposite_delta = drag_start - current_mouse;
1151-
1152-
shape_editor.move_selected_points_and_segments(None, document, opposite_delta, false, true, false, None, false, responses);
1153-
1154-
// Calculate the projected delta and shift the points along that delta
1155-
let delta = current_mouse - drag_start;
1156-
let axis = if delta.x.abs() >= delta.y.abs() { Axis::X } else { Axis::Y };
1157-
self.snapping_axis = Some(axis);
1158-
let projected_delta = match axis {
1159-
Axis::X => DVec2::new(delta.x, 0.),
1160-
Axis::Y => DVec2::new(0., delta.y),
1161-
_ => DVec2::new(delta.x, 0.),
1162-
};
1163-
1164-
shape_editor.move_selected_points_and_segments(None, document, projected_delta, false, true, false, None, false, responses);
1165-
}
1166-
1167-
fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
1168-
// Calculate the negative delta of the selection and move it back to the drag start
1169-
let current_mouse = input.mouse.position;
1170-
let drag_start = self.drag_start_pos;
1171-
1172-
let opposite_delta = drag_start - current_mouse;
1173-
let Some(axis) = self.snapping_axis else { return };
1174-
let opposite_projected_delta = match axis {
1175-
Axis::X => DVec2::new(opposite_delta.x, 0.),
1176-
Axis::Y => DVec2::new(0., opposite_delta.y),
1177-
_ => DVec2::new(opposite_delta.x, 0.),
1178-
};
1179-
1180-
shape_editor.move_selected_points_and_segments(None, document, opposite_projected_delta, false, true, false, None, false, responses);
1181-
1182-
// Calculate what actually would have been the original delta for the point, and apply that
1183-
let delta = current_mouse - drag_start;
1184-
1185-
shape_editor.move_selected_points_and_segments(None, document, delta, false, true, false, None, false, responses);
1186-
1187-
self.snapping_axis = None;
1188-
}
1189-
11901149
fn get_normalized_tangent(&mut self, point: PointId, segment: SegmentId, vector: &Vector) -> Option<DVec2> {
11911150
let other_point = vector.other_point(segment, point)?;
11921151
let position = ManipulatorPointId::Anchor(point).get_position(vector)?;
@@ -1404,10 +1363,15 @@ impl PathToolData {
14041363
.any(|point| matches!(point, ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_)));
14051364

14061365
// This is where it starts snapping along axis
1407-
if snap_axis && self.snapping_axis.is_none() && !single_handle_selected {
1408-
self.start_snap_along_axis(shape_editor, document, input, responses);
1409-
} else if !snap_axis && self.snapping_axis.is_some() {
1410-
self.stop_snap_along_axis(shape_editor, document, input, responses);
1366+
if snap_axis && !single_handle_selected {
1367+
let total_delta = self.drag_start_pos - input.mouse.position;
1368+
if total_delta.x.abs() > total_delta.y.abs() {
1369+
self.snapping_axis = Some(Axis::X);
1370+
} else {
1371+
self.snapping_axis = Some(Axis::Y);
1372+
}
1373+
} else {
1374+
self.snapping_axis = None;
14111375
}
14121376

14131377
let document_to_viewport = document.metadata().document_to_viewport;
@@ -1447,87 +1411,74 @@ impl PathToolData {
14471411
viewport,
14481412
)
14491413
} else {
1450-
shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, viewport, previous_mouse)
1414+
let constraint = if let Some(axis) = self.snapping_axis {
1415+
match axis {
1416+
Axis::X => SnapConstraint::Direction(DVec2::X),
1417+
Axis::Y => SnapConstraint::Direction(DVec2::Y),
1418+
_ => SnapConstraint::None,
1419+
}
1420+
} else {
1421+
SnapConstraint::None
1422+
};
1423+
shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, viewport, previous_mouse, constraint, self.snap_offset)
14511424
};
14521425

14531426
let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() };
14541427
let opposite = if lock_angle { None } else { self.opposite_handle_position };
1455-
let unsnapped_delta = current_mouse - previous_mouse;
14561428
let mut was_alt_dragging = false;
14571429

1458-
if self.snapping_axis.is_none() {
1459-
if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
1460-
// Checking which direction the dragging begins
1461-
self.alt_dragging_from_anchor = true;
1462-
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else {
1430+
if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
1431+
// Checking which direction the dragging begins
1432+
self.alt_dragging_from_anchor = true;
1433+
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else {
1434+
return;
1435+
};
1436+
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return };
1437+
let Some(point_id) = shape_editor.selected_points().next().unwrap().get_anchor(&vector) else {
1438+
return;
1439+
};
1440+
1441+
if vector.connected_count(point_id) == 2 {
1442+
let connected_segments: Vec<HandleId> = vector.all_connected(point_id).collect();
1443+
let segment1 = connected_segments[0];
1444+
let Some(tangent1) = self.get_normalized_tangent(point_id, segment1.segment, &vector) else {
14631445
return;
14641446
};
1465-
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return };
1466-
let Some(point_id) = shape_editor.selected_points().next().unwrap().get_anchor(&vector) else {
1447+
let segment2 = connected_segments[1];
1448+
let Some(tangent2) = self.get_normalized_tangent(point_id, segment2.segment, &vector) else {
14671449
return;
14681450
};
14691451

1470-
if vector.connected_count(point_id) == 2 {
1471-
let connected_segments: Vec<HandleId> = vector.all_connected(point_id).collect();
1472-
let segment1 = connected_segments[0];
1473-
let Some(tangent1) = self.get_normalized_tangent(point_id, segment1.segment, &vector) else {
1474-
return;
1475-
};
1476-
let segment2 = connected_segments[1];
1477-
let Some(tangent2) = self.get_normalized_tangent(point_id, segment2.segment, &vector) else {
1478-
return;
1479-
};
1480-
1481-
let delta = input.mouse.position - self.drag_start_pos;
1482-
let handle = if delta.dot(tangent1) >= delta.dot(tangent2) {
1483-
segment1.to_manipulator_point()
1484-
} else {
1485-
segment2.to_manipulator_point()
1486-
};
1487-
1488-
// Now change the selection to this handle
1489-
shape_editor.deselect_all_points();
1490-
shape_editor.select_point_by_layer_and_id(handle, layer);
1491-
1492-
responses.add(PathToolMessage::SelectionChanged);
1493-
}
1494-
}
1452+
let delta = input.mouse.position - self.drag_start_pos;
1453+
let handle = if delta.dot(tangent1) >= delta.dot(tangent2) {
1454+
segment1.to_manipulator_point()
1455+
} else {
1456+
segment2.to_manipulator_point()
1457+
};
14951458

1496-
if self.alt_dragging_from_anchor && !equidistant && self.alt_clicked_on_anchor {
1497-
was_alt_dragging = true;
1498-
self.alt_dragging_from_anchor = false;
1499-
self.alt_clicked_on_anchor = false;
1500-
}
1459+
// Now change the selection to this handle
1460+
shape_editor.deselect_all_points();
1461+
shape_editor.select_point_by_layer_and_id(handle, layer);
15011462

1502-
let mut skip_opposite = false;
1503-
if self.temporary_colinear_handles && !lock_angle {
1504-
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses);
1505-
self.temporary_colinear_handles = false;
1506-
skip_opposite = true;
1463+
responses.add(PathToolMessage::SelectionChanged);
15071464
}
1508-
shape_editor.move_selected_points_and_segments(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
1509-
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
1510-
} else {
1511-
let Some(axis) = self.snapping_axis else { return };
1512-
let projected_delta = match axis {
1513-
Axis::X => DVec2::new(unsnapped_delta.x, 0.),
1514-
Axis::Y => DVec2::new(0., unsnapped_delta.y),
1515-
_ => DVec2::new(unsnapped_delta.x, 0.),
1516-
};
1517-
shape_editor.move_selected_points_and_segments(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses);
1518-
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
15191465
}
15201466

1521-
// Constantly checking and changing the snapping axis based on current mouse position
1522-
if snap_axis && self.snapping_axis.is_some() {
1523-
let Some(current_axis) = self.snapping_axis else { return };
1524-
let total_delta = self.drag_start_pos - input.mouse.position;
1467+
if self.alt_dragging_from_anchor && !equidistant && self.alt_clicked_on_anchor {
1468+
was_alt_dragging = true;
1469+
self.alt_dragging_from_anchor = false;
1470+
self.alt_clicked_on_anchor = false;
1471+
}
15251472

1526-
if (total_delta.x.abs() > total_delta.y.abs() && current_axis == Axis::Y) || (total_delta.y.abs() > total_delta.x.abs() && current_axis == Axis::X) {
1527-
self.stop_snap_along_axis(shape_editor, document, input, responses);
1528-
self.start_snap_along_axis(shape_editor, document, input, responses);
1529-
}
1473+
let mut skip_opposite = false;
1474+
if self.temporary_colinear_handles && !lock_angle {
1475+
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses);
1476+
self.temporary_colinear_handles = false;
1477+
skip_opposite = true;
15301478
}
1479+
shape_editor.move_selected_points_and_segments(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
1480+
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
1481+
self.snap_offset += document_to_viewport.inverse().transform_vector2(snapped_delta);
15311482
}
15321483

15331484
fn pivot_gizmo(&self) -> PivotGizmo {

0 commit comments

Comments
 (0)