From 59fe147543b9d9d45e1bb953d90ab0f1cdc6ef2c Mon Sep 17 00:00:00 2001 From: Chetan Sahney Date: Sat, 11 Jul 2026 02:24:21 +0530 Subject: [PATCH 1/2] Add Shift to lock segment molding to horizontal/vertical When molding a path segment with the Path tool, holding Shift now constrains the drag to the dominant screen axis (purely horizontal or vertical), mirroring the direction-lock behavior of other drag operations. A "Lock Direction" hint is shown while molding. Closes #3760 --- .../messages/tool/common_functionality/shape_editor.rs | 8 ++++++++ editor/src/messages/tool/tool_messages/path_tool.rs | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 4224693583..14306db7d6 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -354,6 +354,7 @@ impl ClosestSegment { (c1, c2): (DVec2, DVec2), new_b: DVec2, break_colinear_molding: bool, + lock_direction: bool, temporary_adjacent_handles_while_molding: Option<[Option; 2]>, ) -> Option<[Option; 2]> { let transform = document.metadata().transform_to_viewport_if_feeds(self.layer, &document.network_interface); @@ -363,6 +364,13 @@ impl ClosestSegment { // Apply the drag delta to the segment's handles let b = self.bezier_point_to_viewport; + // When the lock direction modifier is held, constrain the drag to the dominant screen axis (purely horizontal or vertical). + let new_b = if lock_direction { + let drag = new_b - b; + if drag.x.abs() >= drag.y.abs() { DVec2::new(new_b.x, b.y) } else { DVec2::new(b.x, new_b.y) } + } else { + new_b + }; let delta = transform.inverse().transform_vector2(new_b - b); let (nc1, nc2) = (c1 + delta, c2 + delta); diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 319cf68f03..4165c593d0 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -2125,6 +2125,7 @@ impl Fsm for PathToolFsmState { } let break_molding = input.keyboard.get(break_colinear_molding as usize); + let lock_direction = input.keyboard.get(snap_angle as usize); // Logic for molding segment if let Some(segment) = &mut tool_data.segment @@ -2136,6 +2137,7 @@ impl Fsm for PathToolFsmState { molding_segment_handles, input.mouse.position, break_molding, + lock_direction, tool_data.temporary_adjacent_handles_while_molding, ); @@ -3639,6 +3641,8 @@ fn update_dynamic_hints( let mut molding_hints = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; + molding_hints.push(HintGroup(vec![HintInfo::keys([Key::Shift], "Lock Direction")])); + if molding_disable_possible { molding_hints.push(HintGroup(vec![HintInfo::keys([Key::Alt], "Break Colinear Handles")])); } From 01993397484fbac49422650c0566ff35e023316e Mon Sep 17 00:00:00 2001 From: Chetan Sahney Date: Sat, 18 Jul 2026 13:06:12 +0530 Subject: [PATCH 2/2] Draw axis guides while molding and lock relative to the grab point Address review feedback on the molding direction lock: - Reuse the existing X/Y snapping-line overlay instead of leaving the user to guess the alignment. Setting `snapping_axis` during a molding drag makes the drag overlay draw the same red/green guide lines it already draws when dragging points, so no new drawing code or state is needed. - Constrain the pointer relative to `drag_start_pos` rather than the segment's bezier point. Snapping against the bezier point offset the cursor by the distance between the grab location and that point, so the segment jumped when the modifier was pressed. - Move the constraint out of `mold_handle_positions` and into the path tool, where the drag start is known. This drops the `lock_direction` parameter from the shape editor API. - Clear `snapping_axis` on the escape/abort path and on the early return in drag stop, so a locked axis cannot leak into the next drag. Co-Authored-By: Claude Fable 5 --- .../tool/common_functionality/shape_editor.rs | 8 ------ .../messages/tool/tool_messages/path_tool.rs | 25 +++++++++++++++++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 14306db7d6..4224693583 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -354,7 +354,6 @@ impl ClosestSegment { (c1, c2): (DVec2, DVec2), new_b: DVec2, break_colinear_molding: bool, - lock_direction: bool, temporary_adjacent_handles_while_molding: Option<[Option; 2]>, ) -> Option<[Option; 2]> { let transform = document.metadata().transform_to_viewport_if_feeds(self.layer, &document.network_interface); @@ -364,13 +363,6 @@ impl ClosestSegment { // Apply the drag delta to the segment's handles let b = self.bezier_point_to_viewport; - // When the lock direction modifier is held, constrain the drag to the dominant screen axis (purely horizontal or vertical). - let new_b = if lock_direction { - let drag = new_b - b; - if drag.x.abs() >= drag.y.abs() { DVec2::new(new_b.x, b.y) } else { DVec2::new(b.x, new_b.y) } - } else { - new_b - }; let delta = transform.inverse().transform_vector2(new_b - b); let (nc1, nc2) = (c1 + delta, c2 + delta); diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 4165c593d0..99fe793d52 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -2131,13 +2131,32 @@ impl Fsm for PathToolFsmState { if let Some(segment) = &mut tool_data.segment && let Some(molding_segment_handles) = tool_data.molding_info { + // While the lock direction modifier is held, constrain the drag to whichever screen axis it has travelled + // further along. The axis is stored in `snapping_axis` so the drag overlay draws the same X/Y guide lines + // through the grab point as it does when dragging points. + let mouse_position = match lock_direction { + true => { + let delta = input.mouse.position - tool_data.drag_start_pos; + let axis = if delta.x.abs() >= delta.y.abs() { Axis::X } else { Axis::Y }; + tool_data.snapping_axis = Some(axis); + + match axis { + Axis::Y => DVec2::new(tool_data.drag_start_pos.x, input.mouse.position.y), + Axis::X | Axis::Both => DVec2::new(input.mouse.position.x, tool_data.drag_start_pos.y), + } + } + false => { + tool_data.snapping_axis = None; + input.mouse.position + } + }; + tool_data.temporary_adjacent_handles_while_molding = segment.mold_handle_positions( document, responses, molding_segment_handles, - input.mouse.position, + mouse_position, break_molding, - lock_direction, tool_data.temporary_adjacent_handles_while_molding, ); @@ -2391,6 +2410,7 @@ impl Fsm for PathToolFsmState { tool_data.molding_segment = false; tool_data.temporary_adjacent_handles_while_molding = None; tool_data.angle_locked = false; + tool_data.snapping_axis = None; responses.add(DocumentMessage::AbortTransaction); tool_data.snap_manager.cleanup(responses); PathToolFsmState::Ready @@ -2513,6 +2533,7 @@ impl Fsm for PathToolFsmState { tool_data.molding_info = None; tool_data.molding_segment = false; tool_data.temporary_adjacent_handles_while_molding = None; + tool_data.snapping_axis = None; if segment_dissolved || point_inserted { responses.add(DocumentMessage::EndTransaction);