@@ -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