From 5c393a148dcd5e2048f8092869bf986e44a07c35 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Thu, 29 Jan 2026 15:47:55 +0530 Subject: [PATCH 1/2] aligned locked axis with canvas --- .../document/utility_types/transformation.rs | 8 +++--- .../transform_layer_message_handler.rs | 27 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index 6ccd9f83f0..92c2985d6f 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -161,15 +161,15 @@ impl Translation { let document_to_viewport = document.metadata().document_to_viewport; let displacement = if let Some(value) = self.typed_distance { match self.constraint { - Axis::X => DVec2::X * value, - Axis::Y => DVec2::Y * value, + Axis::X => state.constraint_axis(self.constraint, document).unwrap_or(DVec2::X) * value, + Axis::Y => state.constraint_axis(self.constraint, document).unwrap_or(DVec2::Y) * value, Axis::Both => self.dragged_distance, } } else { match self.constraint { Axis::Both => self.dragged_distance, - Axis::X => DVec2::X * self.dragged_distance.dot(state.constraint_axis(self.constraint).unwrap_or_default()), - Axis::Y => DVec2::Y * self.dragged_distance.dot(state.constraint_axis(self.constraint).unwrap_or_default()), + Axis::X => state.constraint_axis(self.constraint, document).unwrap_or(DVec2::X) * self.dragged_distance.dot(state.constraint_axis(self.constraint, document).unwrap_or_default()), + Axis::Y => state.constraint_axis(self.constraint, document).unwrap_or(DVec2::Y) * self.dragged_distance.dot(state.constraint_axis(self.constraint, document).unwrap_or_default()), } }; let displacement_viewport = displacement * document_to_viewport.matrix2.y_axis.length(); // Values are local to the viewport but scaled so values are relative to the current scale. diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index 5c784b556e..3a6a7831a6 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -46,16 +46,25 @@ impl TransformationState { document.metadata().document_to_viewport.transform_point2(self.document_space_pivot) } - pub fn constraint_axis(&self, axis_constraint: Axis) -> Option { + pub fn constraint_axis(&self, axis_constraint: Axis, document: &DocumentMessageHandler) -> Option { match axis_constraint { - Axis::X => Some(if self.is_transforming_in_local_space { self.local_transform_axes[0] } else { DVec2::X }), - Axis::Y => Some(if self.is_transforming_in_local_space { self.local_transform_axes[1] } else { DVec2::Y }), + Axis::X => Some(if self.is_transforming_in_local_space { + self.local_transform_axes[0] + } else { + document.metadata().document_to_viewport.transform_vector2(DVec2::X).normalize() + }), + Axis::Y => Some(if self.is_transforming_in_local_space { + self.local_transform_axes[1] + } else { + document.metadata().document_to_viewport.transform_vector2(DVec2::Y).normalize() + }), _ => None, } } - pub fn project_onto_constrained(&self, vector: DVec2, axis_constraint: Axis) -> DVec2 { - self.constraint_axis(axis_constraint).map_or(vector, |direction| vector.project_onto_normalized(direction)) + pub fn project_onto_constrained(&self, vector: DVec2, axis_constraint: Axis, document: &DocumentMessageHandler) -> DVec2 { + self.constraint_axis(axis_constraint, document) + .map_or(vector, |direction| vector.project_onto_normalized(direction)) } pub fn local_to_viewport_transform(&self) -> DAffine2 { @@ -249,7 +258,7 @@ impl MessageHandler> for let text = format!("{}x", format_rounded(scale, 3)); let start_mouse = document_to_viewport.transform_point2(self.local_mouse_start); let local_edge = start_mouse - self.state.pivot_viewport(document); - let local_edge = self.state.project_onto_constrained(local_edge, axis_constraint); + let local_edge = self.state.project_onto_constrained(local_edge, axis_constraint, document); let boundary_point = self.state.pivot_viewport(document) + local_edge * scale.min(1.); let end_point = self.state.pivot_viewport(document) + local_edge * scale.max(1.); @@ -560,9 +569,9 @@ impl MessageHandler> for let to_mouse_final_old = input.mouse.position - self.state.pivot_viewport(document); let to_mouse_start = self.start_mouse - self.state.pivot_viewport(document); - let to_mouse_final = self.state.project_onto_constrained(to_mouse_final, axis_constraint); - let to_mouse_final_old = self.state.project_onto_constrained(to_mouse_final_old, axis_constraint); - let to_mouse_start = self.state.project_onto_constrained(to_mouse_start, axis_constraint); + let to_mouse_final = self.state.project_onto_constrained(to_mouse_final, axis_constraint, document); + let to_mouse_final_old = self.state.project_onto_constrained(to_mouse_final_old, axis_constraint, document); + let to_mouse_start = self.state.project_onto_constrained(to_mouse_start, axis_constraint, document); let change = { let previous_frame_dist = to_mouse_final.dot(to_mouse_start); From d53cedba8db4ab6f1a45a59ff1504899d7edfb69 Mon Sep 17 00:00:00 2001 From: krVatsal Date: Thu, 29 Jan 2026 23:41:23 +0530 Subject: [PATCH 2/2] aligned Axis Locking with Canvas Pan/Tilt --- .../document/utility_types/transformation.rs | 8 +++--- .../transformation_cage.rs | 18 ++++++++----- .../tool/tool_messages/select_tool.rs | 19 +++++++++---- .../transform_layer_message_handler.rs | 27 +++++++------------ 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index 92c2985d6f..6ccd9f83f0 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -161,15 +161,15 @@ impl Translation { let document_to_viewport = document.metadata().document_to_viewport; let displacement = if let Some(value) = self.typed_distance { match self.constraint { - Axis::X => state.constraint_axis(self.constraint, document).unwrap_or(DVec2::X) * value, - Axis::Y => state.constraint_axis(self.constraint, document).unwrap_or(DVec2::Y) * value, + Axis::X => DVec2::X * value, + Axis::Y => DVec2::Y * value, Axis::Both => self.dragged_distance, } } else { match self.constraint { Axis::Both => self.dragged_distance, - Axis::X => state.constraint_axis(self.constraint, document).unwrap_or(DVec2::X) * self.dragged_distance.dot(state.constraint_axis(self.constraint, document).unwrap_or_default()), - Axis::Y => state.constraint_axis(self.constraint, document).unwrap_or(DVec2::Y) * self.dragged_distance.dot(state.constraint_axis(self.constraint, document).unwrap_or_default()), + Axis::X => DVec2::X * self.dragged_distance.dot(state.constraint_axis(self.constraint).unwrap_or_default()), + Axis::Y => DVec2::Y * self.dragged_distance.dot(state.constraint_axis(self.constraint).unwrap_or_default()), } }; let displacement_viewport = displacement * document_to_viewport.matrix2.y_axis.length(); // Values are local to the viewport but scaled so values are relative to the current scale. diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index dba65b4080..cdbbf723d4 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -303,15 +303,19 @@ impl SelectedEdges { } } -/// Aligns the mouse position to the closest axis -pub fn axis_align_drag(axis_align: bool, axis: Axis, position: DVec2, start: DVec2) -> DVec2 { +/// Aligns the mouse position to the closest axis, accounting for canvas rotation. +/// `canvas_rotation` is the angle in radians by which the canvas is tilted. +pub fn axis_align_drag(axis_align: bool, axis: Axis, position: DVec2, start: DVec2, canvas_rotation: f64) -> DVec2 { if axis_align { let mouse_position = position - start; let snap_resolution = SELECTION_DRAG_ANGLE.to_radians(); - let angle = -mouse_position.angle_to(DVec2::X); + // Subtract canvas rotation to work in canvas-local space + let angle = -mouse_position.angle_to(DVec2::X) - canvas_rotation; let snapped_angle = (angle / snap_resolution).round() * snap_resolution; - let axis_vector = DVec2::from_angle(snapped_angle); - if snapped_angle.is_finite() { + // Add canvas rotation back to get the final screen-space angle + let final_angle = snapped_angle + canvas_rotation; + let axis_vector = DVec2::from_angle(final_angle); + if final_angle.is_finite() { start + axis_vector * mouse_position.dot(axis_vector).abs() } else { start @@ -327,8 +331,10 @@ pub fn axis_align_drag(axis_align: bool, axis: Axis, position: DVec2, start: DVe /// Snaps a dragging event from the artboard or select tool pub fn snap_drag(start: DVec2, current: DVec2, snap_to_axis: bool, axis: Axis, snap_data: SnapData, snap_manager: &mut SnapManager, candidates: &[SnapCandidatePoint]) -> DVec2 { - let mouse_position = axis_align_drag(snap_to_axis, axis, snap_data.input.mouse.position, start); let document = snap_data.document; + // Extract canvas rotation from the document_to_viewport transform + let canvas_rotation = document.metadata().document_to_viewport.matrix2.y_axis.to_angle() - std::f64::consts::FRAC_PI_2; + let mouse_position = axis_align_drag(snap_to_axis, axis, snap_data.input.mouse.position, start, canvas_rotation); let total_mouse_delta_document = document.metadata().document_to_viewport.inverse().transform_vector2(mouse_position - start); let mouse_delta_document = document.metadata().document_to_viewport.inverse().transform_vector2(mouse_position - current); let mut offset = mouse_delta_document; diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index e5738e7770..08fb5e2b81 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -402,6 +402,8 @@ struct SelectToolData { snap_candidates: Vec, auto_panning: AutoPanning, drag_start_center: ViewportPosition, + /// Drag start position in document coordinates, used for axis-aligned snapping that follows canvas pan/tilt + drag_start_document: DVec2, } impl SelectToolData { @@ -911,12 +913,16 @@ impl Fsm for SelectToolFsmState { } if axis_state.is_none_or(|(axis, _)| !axis.is_constraint()) && tool_data.axis_align { - let mouse_position = mouse_position - tool_data.drag_start; + // Convert document-space origin to current viewport (follows pan/tilt) + let viewport_origin = document.metadata().document_to_viewport.transform_point2(tool_data.drag_start_document); + let mouse_position = mouse_position - viewport_origin; let snap_resolution = SELECTION_DRAG_ANGLE.to_radians(); - let angle = -mouse_position.angle_to(DVec2::X); - let snapped_angle = (angle / snap_resolution).round() * snap_resolution; + // Account for canvas rotation + let canvas_rotation = document.metadata().document_to_viewport.matrix2.y_axis.to_angle() - std::f64::consts::FRAC_PI_2; + let angle = -mouse_position.angle_to(DVec2::X) - canvas_rotation; + let snapped_angle = (angle / snap_resolution).round() * snap_resolution + canvas_rotation; - let origin = tool_data.drag_start_center; + let origin = viewport_origin; let viewport_diagonal = viewport.size().into_dvec2().length(); let edge = DVec2::from_angle(snapped_angle).normalize_or(DVec2::X); @@ -1037,6 +1043,7 @@ impl Fsm for SelectToolFsmState { let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging, Some(position)); tool_data.drag_start_center = position; + tool_data.drag_start_document = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); // If the user is dragging the bounding box bounds, go into ResizingBounds mode. // If the user is dragging the rotate trigger, go into RotatingBounds mode. @@ -1179,7 +1186,9 @@ impl Fsm for SelectToolFsmState { let ignore = tool_data.non_duplicated_layers.as_ref().filter(|_| !layers_exist).unwrap_or(&tool_data.layers_dragging); let snap_data = SnapData::ignore(document, input, viewport, ignore); - let (start, current) = (tool_data.drag_start, tool_data.drag_current); + // Convert document-space origin to current viewport (follows pan/tilt) + let start = document.metadata().document_to_viewport.transform_point2(tool_data.drag_start_document); + let current = tool_data.drag_current; let e0 = tool_data .bounding_box_manager .as_ref() diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index 3a6a7831a6..5c784b556e 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -46,25 +46,16 @@ impl TransformationState { document.metadata().document_to_viewport.transform_point2(self.document_space_pivot) } - pub fn constraint_axis(&self, axis_constraint: Axis, document: &DocumentMessageHandler) -> Option { + pub fn constraint_axis(&self, axis_constraint: Axis) -> Option { match axis_constraint { - Axis::X => Some(if self.is_transforming_in_local_space { - self.local_transform_axes[0] - } else { - document.metadata().document_to_viewport.transform_vector2(DVec2::X).normalize() - }), - Axis::Y => Some(if self.is_transforming_in_local_space { - self.local_transform_axes[1] - } else { - document.metadata().document_to_viewport.transform_vector2(DVec2::Y).normalize() - }), + Axis::X => Some(if self.is_transforming_in_local_space { self.local_transform_axes[0] } else { DVec2::X }), + Axis::Y => Some(if self.is_transforming_in_local_space { self.local_transform_axes[1] } else { DVec2::Y }), _ => None, } } - pub fn project_onto_constrained(&self, vector: DVec2, axis_constraint: Axis, document: &DocumentMessageHandler) -> DVec2 { - self.constraint_axis(axis_constraint, document) - .map_or(vector, |direction| vector.project_onto_normalized(direction)) + pub fn project_onto_constrained(&self, vector: DVec2, axis_constraint: Axis) -> DVec2 { + self.constraint_axis(axis_constraint).map_or(vector, |direction| vector.project_onto_normalized(direction)) } pub fn local_to_viewport_transform(&self) -> DAffine2 { @@ -258,7 +249,7 @@ impl MessageHandler> for let text = format!("{}x", format_rounded(scale, 3)); let start_mouse = document_to_viewport.transform_point2(self.local_mouse_start); let local_edge = start_mouse - self.state.pivot_viewport(document); - let local_edge = self.state.project_onto_constrained(local_edge, axis_constraint, document); + let local_edge = self.state.project_onto_constrained(local_edge, axis_constraint); let boundary_point = self.state.pivot_viewport(document) + local_edge * scale.min(1.); let end_point = self.state.pivot_viewport(document) + local_edge * scale.max(1.); @@ -569,9 +560,9 @@ impl MessageHandler> for let to_mouse_final_old = input.mouse.position - self.state.pivot_viewport(document); let to_mouse_start = self.start_mouse - self.state.pivot_viewport(document); - let to_mouse_final = self.state.project_onto_constrained(to_mouse_final, axis_constraint, document); - let to_mouse_final_old = self.state.project_onto_constrained(to_mouse_final_old, axis_constraint, document); - let to_mouse_start = self.state.project_onto_constrained(to_mouse_start, axis_constraint, document); + let to_mouse_final = self.state.project_onto_constrained(to_mouse_final, axis_constraint); + let to_mouse_final_old = self.state.project_onto_constrained(to_mouse_final_old, axis_constraint); + let to_mouse_start = self.state.project_onto_constrained(to_mouse_start, axis_constraint); let change = { let previous_frame_dist = to_mouse_final.dot(to_mouse_start);