Skip to content

Commit d5fd55f

Browse files
committed
Fixes broken multi handle snap for path tool (#2969)
1 parent 5a1503f commit d5fd55f

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;
@@ -562,7 +562,6 @@ impl ShapeState {
562562
}
563563
}
564564

565-
// Snap, returning a viewport delta
566565
pub fn snap(
567566
&self,
568567
snap_manager: &mut SnapManager,
@@ -571,6 +570,8 @@ impl ShapeState {
571570
input: &InputPreprocessorMessageHandler,
572571
viewport: &ViewportMessageHandler,
573572
previous_mouse: DVec2,
573+
constraint: SnapConstraint,
574+
accumulated_offset: DVec2,
574575
) -> DVec2 {
575576
let snap_data = SnapData::new_snap_cache(document, input, viewport, snap_cache);
576577

@@ -596,7 +597,8 @@ impl ShapeState {
596597
};
597598

598599
let Some(position) = selected.get_position(&vector) else { continue };
599-
let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source);
600+
let point_position = to_document.transform_point2(position);
601+
let mut point = SnapCandidatePoint::new_source(point_position + mouse_delta, source);
600602

601603
if let Some(id) = selected.as_anchor() {
602604
for neighbor in vector.connected_points(id) {
@@ -608,14 +610,42 @@ impl ShapeState {
608610
}
609611
}
610612

611-
let snapped = snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default());
613+
let snapped = match constraint {
614+
SnapConstraint::Line { origin, direction } => {
615+
let projected_origin = origin + (point_position - origin).project_onto(direction);
616+
let constraint = SnapConstraint::Line { origin: projected_origin, direction };
617+
snap_manager.constrained_snap(&snap_data, &point, constraint, SnapTypeConfiguration::default())
618+
}
619+
SnapConstraint::Direction(direction) => {
620+
let origin = point_position - accumulated_offset;
621+
let constraint = SnapConstraint::Line { origin, direction };
622+
snap_manager.constrained_snap(&snap_data, &point, constraint, SnapTypeConfiguration::default())
623+
}
624+
_ => snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default()),
625+
};
626+
612627
if best_snapped.other_snap_better(&snapped) {
613628
offset = snapped.snapped_point_document - point.document_point + mouse_delta;
614629
best_snapped = snapped;
615630
}
616631
}
617632
}
633+
let is_snapped = best_snapped.is_snapped();
618634
snap_manager.update_indicator(best_snapped);
635+
636+
// If no magnetic snap occurred but we have a constraint, force the constraint
637+
if !is_snapped {
638+
match constraint {
639+
SnapConstraint::Direction(direction) => {
640+
let constrained_total_offset = (accumulated_offset + mouse_delta).project_onto(direction);
641+
offset = constrained_total_offset - accumulated_offset;
642+
}
643+
SnapConstraint::Line { direction, .. } if direction != DVec2::ZERO => {
644+
offset = mouse_delta.project_onto(direction);
645+
}
646+
_ => {}
647+
}
648+
}
619649
document.metadata().document_to_viewport.transform_vector2(offset)
620650
}
621651

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

Lines changed: 62 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,8 @@ struct PathToolData {
603603
started_drawing_from_inside: bool,
604604
first_selected_with_single_click: bool,
605605
stored_selection: Option<HashMap<LayerNodeIdentifier, SelectedLayerState>>,
606+
snap_offset: DVec2,
607+
606608
last_drill_through_click_position: Option<DVec2>,
607609
drill_through_cycle_index: usize,
608610
drill_through_cycle_count: usize,
@@ -735,6 +737,7 @@ impl PathToolData {
735737
self.opposing_handle_lengths = None;
736738

737739
self.drag_start_pos = input.mouse.position;
740+
self.snap_offset = DVec2::ZERO;
738741

739742
if input.time - self.last_click_time > DOUBLE_CLICK_MILLISECONDS {
740743
self.saved_points_before_anchor_convert_smooth_sharp.clear();
@@ -1148,50 +1151,6 @@ impl PathToolData {
11481151
document.metadata().document_to_viewport.transform_vector2(snap_result.snapped_point_document - handle_position)
11491152
}
11501153

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

14111370
// This is where it starts snapping along axis
1412-
if snap_axis && self.snapping_axis.is_none() && !single_handle_selected {
1413-
self.start_snap_along_axis(shape_editor, document, input, responses);
1414-
} else if !snap_axis && self.snapping_axis.is_some() {
1415-
self.stop_snap_along_axis(shape_editor, document, input, responses);
1371+
if snap_axis && !single_handle_selected {
1372+
let total_delta = self.drag_start_pos - input.mouse.position;
1373+
if total_delta.x.abs() > total_delta.y.abs() {
1374+
self.snapping_axis = Some(Axis::X);
1375+
} else {
1376+
self.snapping_axis = Some(Axis::Y);
1377+
}
1378+
} else {
1379+
self.snapping_axis = None;
14161380
}
14171381

14181382
let document_to_viewport = document.metadata().document_to_viewport;
@@ -1452,87 +1416,74 @@ impl PathToolData {
14521416
viewport,
14531417
)
14541418
} else {
1455-
shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, viewport, previous_mouse)
1419+
let constraint = if let Some(axis) = self.snapping_axis {
1420+
match axis {
1421+
Axis::X => SnapConstraint::Direction(DVec2::X),
1422+
Axis::Y => SnapConstraint::Direction(DVec2::Y),
1423+
_ => SnapConstraint::None,
1424+
}
1425+
} else {
1426+
SnapConstraint::None
1427+
};
1428+
shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, viewport, previous_mouse, constraint, self.snap_offset)
14561429
};
14571430

14581431
let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() };
14591432
let opposite = if lock_angle { None } else { self.opposite_handle_position };
1460-
let unsnapped_delta = current_mouse - previous_mouse;
14611433
let mut was_alt_dragging = false;
14621434

1463-
if self.snapping_axis.is_none() {
1464-
if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
1465-
// Checking which direction the dragging begins
1466-
self.alt_dragging_from_anchor = true;
1467-
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else {
1435+
if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
1436+
// Checking which direction the dragging begins
1437+
self.alt_dragging_from_anchor = true;
1438+
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else {
1439+
return;
1440+
};
1441+
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return };
1442+
let Some(point_id) = shape_editor.selected_points().next().unwrap().get_anchor(&vector) else {
1443+
return;
1444+
};
1445+
1446+
if vector.connected_count(point_id) == 2 {
1447+
let connected_segments: Vec<HandleId> = vector.all_connected(point_id).collect();
1448+
let segment1 = connected_segments[0];
1449+
let Some(tangent1) = self.get_normalized_tangent(point_id, segment1.segment, &vector) else {
14681450
return;
14691451
};
1470-
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return };
1471-
let Some(point_id) = shape_editor.selected_points().next().unwrap().get_anchor(&vector) else {
1452+
let segment2 = connected_segments[1];
1453+
let Some(tangent2) = self.get_normalized_tangent(point_id, segment2.segment, &vector) else {
14721454
return;
14731455
};
14741456

1475-
if vector.connected_count(point_id) == 2 {
1476-
let connected_segments: Vec<HandleId> = vector.all_connected(point_id).collect();
1477-
let segment1 = connected_segments[0];
1478-
let Some(tangent1) = self.get_normalized_tangent(point_id, segment1.segment, &vector) else {
1479-
return;
1480-
};
1481-
let segment2 = connected_segments[1];
1482-
let Some(tangent2) = self.get_normalized_tangent(point_id, segment2.segment, &vector) else {
1483-
return;
1484-
};
1485-
1486-
let delta = input.mouse.position - self.drag_start_pos;
1487-
let handle = if delta.dot(tangent1) >= delta.dot(tangent2) {
1488-
segment1.to_manipulator_point()
1489-
} else {
1490-
segment2.to_manipulator_point()
1491-
};
1492-
1493-
// Now change the selection to this handle
1494-
shape_editor.deselect_all_points();
1495-
shape_editor.select_point_by_layer_and_id(handle, layer);
1496-
1497-
responses.add(PathToolMessage::SelectionChanged);
1498-
}
1499-
}
1457+
let delta = input.mouse.position - self.drag_start_pos;
1458+
let handle = if delta.dot(tangent1) >= delta.dot(tangent2) {
1459+
segment1.to_manipulator_point()
1460+
} else {
1461+
segment2.to_manipulator_point()
1462+
};
15001463

1501-
if self.alt_dragging_from_anchor && !equidistant && self.alt_clicked_on_anchor {
1502-
was_alt_dragging = true;
1503-
self.alt_dragging_from_anchor = false;
1504-
self.alt_clicked_on_anchor = false;
1505-
}
1464+
// Now change the selection to this handle
1465+
shape_editor.deselect_all_points();
1466+
shape_editor.select_point_by_layer_and_id(handle, layer);
15061467

1507-
let mut skip_opposite = false;
1508-
if self.temporary_colinear_handles && !lock_angle {
1509-
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses);
1510-
self.temporary_colinear_handles = false;
1511-
skip_opposite = true;
1468+
responses.add(PathToolMessage::SelectionChanged);
15121469
}
1513-
shape_editor.move_selected_points_and_segments(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
1514-
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
1515-
} else {
1516-
let Some(axis) = self.snapping_axis else { return };
1517-
let projected_delta = match axis {
1518-
Axis::X => DVec2::new(unsnapped_delta.x, 0.),
1519-
Axis::Y => DVec2::new(0., unsnapped_delta.y),
1520-
_ => DVec2::new(unsnapped_delta.x, 0.),
1521-
};
1522-
shape_editor.move_selected_points_and_segments(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses);
1523-
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
15241470
}
15251471

1526-
// Constantly checking and changing the snapping axis based on current mouse position
1527-
if snap_axis && self.snapping_axis.is_some() {
1528-
let Some(current_axis) = self.snapping_axis else { return };
1529-
let total_delta = self.drag_start_pos - input.mouse.position;
1472+
if self.alt_dragging_from_anchor && !equidistant && self.alt_clicked_on_anchor {
1473+
was_alt_dragging = true;
1474+
self.alt_dragging_from_anchor = false;
1475+
self.alt_clicked_on_anchor = false;
1476+
}
15301477

1531-
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) {
1532-
self.stop_snap_along_axis(shape_editor, document, input, responses);
1533-
self.start_snap_along_axis(shape_editor, document, input, responses);
1534-
}
1478+
let mut skip_opposite = false;
1479+
if self.temporary_colinear_handles && !lock_angle {
1480+
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses);
1481+
self.temporary_colinear_handles = false;
1482+
skip_opposite = true;
15351483
}
1484+
shape_editor.move_selected_points_and_segments(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
1485+
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
1486+
self.snap_offset += document_to_viewport.inverse().transform_vector2(snapped_delta);
15361487
}
15371488

15381489
fn pivot_gizmo(&self) -> PivotGizmo {

0 commit comments

Comments
 (0)