Skip to content

Commit 4f288e1

Browse files
committed
fix(path-tool): correct Shift constraint origin when snapping (#2753)
When constraining movement with Shift during a drag, the constraint now starts from the point’s snapped position rather than the original drag start. This makes constrained movement behave more intuitively when snapping is active. The fix keeps the constraint origin stable on the first Shift press and uses the snapped mouse position so the origin doesn’t change during axis switches.
1 parent 84e9d8c commit 4f288e1

1 file changed

Lines changed: 41 additions & 11 deletions

File tree

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

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,10 @@ struct PathToolData {
590590
last_clicked_point_was_selected: bool,
591591
last_clicked_segment_was_selected: bool,
592592
snapping_axis: Option<Axis>,
593+
/// The origin point for horizontal/vertical constraints when Shift is pressed.
594+
/// When `None`, defaults to `drag_start_pos`. When `Some`, uses the snapped position
595+
/// if Shift was pressed while the point was snapped to another point.
596+
constraint_origin: Option<DVec2>,
593597
alt_clicked_on_anchor: bool,
594598
alt_dragging_from_anchor: bool,
595599
angle_locked: bool,
@@ -1149,15 +1153,28 @@ impl PathToolData {
11491153
}
11501154

11511155
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+
// Fix for GitHub issue #2753: When Shift is pressed during drag,
1157+
// constraint should originate from the snapped position, not the original drag start.
1158+
//
1159+
// Only set constraint origin if not already set (freeze on first Shift press)
1160+
if self.constraint_origin.is_none() {
1161+
// Use the point's current position (after snapping) as the constraint origin.
1162+
// previous_mouse_position tracks the point's position in document space,
1163+
// including snapping and multi-select offsets (see line 956).
1164+
let document_to_viewport = document.metadata().document_to_viewport;
1165+
let point_pos_viewport = document_to_viewport.transform_point2(self.previous_mouse_position);
1166+
1167+
self.constraint_origin = Some(point_pos_viewport);
1168+
}
1169+
1170+
// Find the negative delta to take the point to the constraint origin
1171+
let origin = self.constraint_origin.unwrap_or(self.drag_start_pos);
1172+
let opposite_delta = origin - input.mouse.position;
11561173

11571174
shape_editor.move_selected_points_and_segments(None, document, opposite_delta, false, true, false, None, false, responses);
11581175

11591176
// Calculate the projected delta and shift the points along that delta
1160-
let delta = current_mouse - drag_start;
1177+
let delta = input.mouse.position - origin;
11611178
let axis = if delta.x.abs() >= delta.y.abs() { Axis::X } else { Axis::Y };
11621179
self.snapping_axis = Some(axis);
11631180
let projected_delta = match axis {
@@ -1170,11 +1187,14 @@ impl PathToolData {
11701187
}
11711188

11721189
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
11741190
let current_mouse = input.mouse.position;
1175-
let drag_start = self.drag_start_pos;
11761191

1177-
let opposite_delta = drag_start - current_mouse;
1192+
// Use the stored constraint origin, or fall back to drag_start_pos
1193+
let origin = self.constraint_origin.unwrap_or(self.drag_start_pos);
1194+
1195+
trace!("Shift released: constraint_origin={:?}, current_mouse={:?}", self.constraint_origin, current_mouse);
1196+
1197+
let opposite_delta = origin - current_mouse;
11781198
let Some(axis) = self.snapping_axis else { return };
11791199
let opposite_projected_delta = match axis {
11801200
Axis::X => DVec2::new(opposite_delta.x, 0.),
@@ -1185,11 +1205,12 @@ impl PathToolData {
11851205
shape_editor.move_selected_points_and_segments(None, document, opposite_projected_delta, false, true, false, None, false, responses);
11861206

11871207
// Calculate what actually would have been the original delta for the point, and apply that
1188-
let delta = current_mouse - drag_start;
1208+
let delta = current_mouse - origin;
11891209

11901210
shape_editor.move_selected_points_and_segments(None, document, delta, false, true, false, None, false, responses);
11911211

11921212
self.snapping_axis = None;
1213+
self.constraint_origin = None;
11931214
}
11941215

11951216
fn get_normalized_tangent(&mut self, point: PointId, segment: SegmentId, vector: &Vector) -> Option<DVec2> {
@@ -1410,8 +1431,13 @@ impl PathToolData {
14101431

14111432
// This is where it starts snapping along axis
14121433
if snap_axis && self.snapping_axis.is_none() && !single_handle_selected {
1434+
trace!(
1435+
"Axis snapping activated: snap_axis={}, snapping_axis={:?}, single_handle={}",
1436+
snap_axis, self.snapping_axis, single_handle_selected
1437+
);
14131438
self.start_snap_along_axis(shape_editor, document, input, responses);
14141439
} else if !snap_axis && self.snapping_axis.is_some() {
1440+
trace!("Axis snapping deactivated: snap_axis={}, snapping_axis={:?}", snap_axis, self.snapping_axis);
14151441
self.stop_snap_along_axis(shape_editor, document, input, responses);
14161442
}
14171443

@@ -1526,7 +1552,10 @@ impl PathToolData {
15261552
// Constantly checking and changing the snapping axis based on current mouse position
15271553
if snap_axis && self.snapping_axis.is_some() {
15281554
let Some(current_axis) = self.snapping_axis else { return };
1529-
let total_delta = self.drag_start_pos - input.mouse.position;
1555+
1556+
// Use the stored constraint origin
1557+
let origin = self.constraint_origin.unwrap_or(self.drag_start_pos);
1558+
let total_delta = origin - input.mouse.position;
15301559

15311560
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) {
15321561
self.stop_snap_along_axis(shape_editor, document, input, responses);
@@ -1974,7 +2003,8 @@ impl Fsm for PathToolFsmState {
19742003
// Draw the snapping axis lines
19752004
if tool_data.snapping_axis.is_some() {
19762005
let Some(axis) = tool_data.snapping_axis else { return self };
1977-
let origin = tool_data.drag_start_pos;
2006+
// Use the stored constraint origin for overlay rendering
2007+
let origin = tool_data.constraint_origin.unwrap_or(tool_data.drag_start_pos);
19782008
let viewport_diagonal = viewport.size().into_dvec2().length();
19792009

19802010
let faded = |color: &str| {

0 commit comments

Comments
 (0)