From 75af331ffcdf7976e92a1b6ab8b941e79e4550fb Mon Sep 17 00:00:00 2001 From: hypercube <0hypercube@gmail.com> Date: Sun, 9 Nov 2025 13:09:45 +0000 Subject: [PATCH 1/4] Better local grab --- .../document/utility_types/transformation.rs | 75 ++--- .../tool/tool_messages/select_tool.rs | 22 +- .../transform_layer_message_handler.rs | 271 ++++++------------ 3 files changed, 132 insertions(+), 236 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index bd9d30dea2..bfb604c85e 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -5,13 +5,13 @@ use crate::messages::portfolio::document::graph_operation::utility_types::{Modif use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::transform_layer::transform_layer_message_handler::OtherUsefulParameters; use crate::messages::tool::utility_types::ToolType; use glam::{DAffine2, DMat2, DVec2}; use graphene_std::renderer::Quad; use graphene_std::vector::misc::{HandleId, ManipulatorPointId}; use graphene_std::vector::{HandleExt, PointId, VectorModificationType}; use std::collections::{HashMap, VecDeque}; -use std::f64::consts::PI; #[derive(Debug, PartialEq, Clone, Copy)] struct AnchorPoint { @@ -156,22 +156,21 @@ pub struct Translation { } impl Translation { - pub fn to_dvec(self, transform: DAffine2, increment_mode: bool) -> DVec2 { + pub fn to_dvec(self, state: &OtherUsefulParameters) -> DVec2 { let displacement = if let Some(value) = self.typed_distance { match self.constraint { - Axis::X => transform.transform_vector2(DVec2::new(value, 0.)), - Axis::Y => transform.transform_vector2(DVec2::new(0., 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 => DVec2::new(self.dragged_distance.x, 0.), - Axis::Y => DVec2::new(0., self.dragged_distance.y), + 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 = transform.inverse().transform_vector2(displacement); - if increment_mode { displacement.round() } else { displacement } + if state.is_rounded_to_intervals { displacement.round() } else { displacement } } #[must_use] @@ -327,36 +326,19 @@ impl TransformType { impl TransformOperation { #[allow(clippy::too_many_arguments)] - pub fn apply_transform_operation(&self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) { - let local_axis_transform_angle = (quad.top_left() - quad.top_right()).to_angle(); + pub fn apply_transform_operation(&self, selected: &mut Selected, state: &OtherUsefulParameters, document: &DocumentMessageHandler) { if self != &TransformOperation::None { - let transformation = match self { - TransformOperation::Grabbing(translation) => { - let translate = DAffine2::from_translation(transform.transform_vector2(translation.to_dvec(local_transform, increment_mode))); - if local { - let resolved_angle = if local_axis_transform_angle > 0. { - local_axis_transform_angle - } else { - local_axis_transform_angle - PI - }; - DAffine2::from_angle(resolved_angle) * translate * DAffine2::from_angle(-resolved_angle) - } else { - translate - } - } - TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(increment_mode)), - TransformOperation::Scaling(scale) => { - if local { - DAffine2::from_angle(local_axis_transform_angle) * DAffine2::from_scale(scale.to_dvec(increment_mode)) * DAffine2::from_angle(-local_axis_transform_angle) - } else { - DAffine2::from_scale(scale.to_dvec(increment_mode)) - } - } + let mut transformation = match self { + TransformOperation::Grabbing(translation) => DAffine2::from_translation(translation.to_dvec(state)), + TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(state.is_rounded_to_intervals)), + TransformOperation::Scaling(scale) => DAffine2::from_scale(scale.to_dvec(state.is_rounded_to_intervals)), TransformOperation::None => unreachable!(), }; + let normalized_transform = state.local_to_viewport_transform(); + transformation = normalized_transform * transformation * normalized_transform.inverse(); - selected.update_transforms(transformation, Some(pivot), Some(*self)); - self.hints(selected.responses, local); + selected.update_transforms(transformation, Some(state.pivot_viewport(document)), Some(*self)); + self.hints(selected.responses, state.is_transforming_in_local_space); } } @@ -373,24 +355,25 @@ impl TransformOperation { } #[allow(clippy::too_many_arguments)] - pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, increment_mode: bool, mut local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) -> bool { - (*self, local) = match self { + pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, state: &OtherUsefulParameters, document: &DocumentMessageHandler) -> bool { + let resulting_local; + (*self, resulting_local) = match self { TransformOperation::Grabbing(translation) => { - let (translation, local) = translation.with_constraint(axis, local); - (TransformOperation::Grabbing(translation), local) + let (translation, resulting_local) = translation.with_constraint(axis, state.is_transforming_in_local_space); + (TransformOperation::Grabbing(translation), resulting_local) } TransformOperation::Scaling(scale) => { - let (scale, local) = scale.with_constraint(axis, local); - (TransformOperation::Scaling(scale), local) + let (scale, resulting_local) = scale.with_constraint(axis, state.is_transforming_in_local_space); + (TransformOperation::Scaling(scale), resulting_local) } _ => (*self, false), }; - self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform); - local + self.apply_transform_operation(selected, state, document); + resulting_local } #[allow(clippy::too_many_arguments)] - pub fn grs_typed(&mut self, typed: Option, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) { + pub fn grs_typed(&mut self, typed: Option, selected: &mut Selected, state: &OtherUsefulParameters, document: &DocumentMessageHandler) { match self { TransformOperation::None => (), TransformOperation::Grabbing(translation) => translation.typed_distance = typed, @@ -398,7 +381,7 @@ impl TransformOperation { TransformOperation::Scaling(scale) => scale.typed_factor = typed, }; - self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform); + self.apply_transform_operation(selected, state, document); } pub fn hints(&self, responses: &mut VecDeque, local: bool) { @@ -481,7 +464,7 @@ impl TransformOperation { } #[allow(clippy::too_many_arguments)] - pub fn negate(&mut self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) { + pub fn negate(&mut self, selected: &mut Selected, state: &OtherUsefulParameters, document: &DocumentMessageHandler) { if *self != TransformOperation::None { *self = match self { TransformOperation::Scaling(scale) => TransformOperation::Scaling(scale.negate()), @@ -489,7 +472,7 @@ impl TransformOperation { TransformOperation::Grabbing(translation) => TransformOperation::Grabbing(translation.negate()), _ => *self, }; - self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform); + self.apply_transform_operation(selected, state, document); } } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index be36c1e166..751bda584e 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -593,6 +593,19 @@ impl SelectToolData { } } +/// Bounding boxes are unfortunately not axis aligned. The bounding boxes are found after a transformation is applied to all of the layers. +/// This uses some rather confusing logic to determine what transform that should be. +pub fn create_bounding_box_transform(document: &DocumentMessageHandler) -> DAffine2 { + // Update bounds + document + .network_interface + .selected_nodes() + .selected_visible_and_unlocked_layers(&document.network_interface) + .find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])) + .map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface)) + .unwrap_or_default() +} + impl Fsm for SelectToolFsmState { type ToolData = SelectToolData; type ToolOptions = (); @@ -633,14 +646,7 @@ impl Fsm for SelectToolFsmState { } } - // Update bounds - let mut transform = document - .network_interface - .selected_nodes() - .selected_visible_and_unlocked_layers(&document.network_interface) - .find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])) - .map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface)) - .unwrap_or_default(); + let mut transform = create_bounding_box_transform(&document); // Check if the matrix is not invertible let mut transform_tampered = false; 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 a39c2b4e9f..ef4d141f91 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 @@ -31,13 +31,41 @@ pub struct TransformLayerMessageContext<'a> { pub viewport: &'a ViewportMessageHandler, } +#[derive(Debug, Clone, Default, ExtractField)] +pub struct OtherUsefulParameters { + pub is_rounded_to_intervals: bool, + pub is_transforming_in_local_space: bool, + pub local_transform_axes: [DVec2; 2], + + pub document_space_pivot: DocumentPosition, +} + +impl OtherUsefulParameters { + pub fn pivot_viewport(&self, document: &DocumentMessageHandler) -> DVec2 { + document.metadata().document_to_viewport.transform_point2(self.document_space_pivot) + } + 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 { 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) -> DVec2 { + self.constraint_axis(axis_constraint).map_or(vector, |direction| vector.project_onto_normalized(direction)) + } + pub fn local_to_viewport_transform(&self) -> DAffine2 { + self.is_transforming_in_local_space + .then(|| DAffine2::from_cols(self.local_transform_axes[0], self.local_transform_axes[1], DVec2::ZERO)) + .unwrap_or_default() + } +} + #[derive(Debug, Clone, Default, ExtractField)] pub struct TransformLayerMessageHandler { pub transform_operation: TransformOperation, slow: bool, - increments: bool, - local: bool, layer_bounding_box: Quad, typing: Typing, @@ -50,7 +78,6 @@ pub struct TransformLayerMessageHandler { path_bounds: Option<[DVec2; 2]>, - local_pivot: DocumentPosition, local_mouse_start: DocumentPosition, grab_target: DocumentPosition, @@ -67,6 +94,8 @@ pub struct TransformLayerMessageHandler { // Ghost outlines for Path Tool ghost_outline: Vec<(Vec, DAffine2)>, + state: OtherUsefulParameters, + was_grabbing: bool, } @@ -124,8 +153,8 @@ impl MessageHandler> for if !using_path_tool { self.pivot_gizmo.recalculate_transform(document); *selected.pivot = self.pivot_gizmo.position(document); - self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot); - self.grab_target = self.local_pivot; + self.state.document_space_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot); + self.grab_target = self.state.document_space_pivot; } // TODO: Here vector data from all layers is not considered which can be a problem in pivot calculation else if let Some(vector) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) { @@ -156,7 +185,7 @@ impl MessageHandler> for *selected.pivot = new_pivot; self.path_bounds = bounds; - self.local_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot); + self.state.document_space_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot); self.grab_target = document_to_viewport.inverse().transform_point2(grab_target); } else { log::warn!("Failed to calculate pivot."); @@ -206,52 +235,52 @@ impl MessageHandler> for match self.transform_operation { TransformOperation::None => (), TransformOperation::Grabbing(translation) => { - let translation = translation.to_dvec(self.initial_transform, self.increments); - let viewport_translate = document_to_viewport.transform_vector2(translation); + let translation_local = translation.to_dvec(&self.state); + let translation_viewport = (self.state.local_to_viewport_transform() * document_to_viewport).transform_vector2(translation_local); let pivot = document_to_viewport.transform_point2(self.grab_target); - let quad = Quad::from_box([pivot, pivot + viewport_translate]); + let quad = Quad::from_box([pivot, pivot + translation_viewport]); responses.add(SelectToolMessage::PivotShift { - offset: Some(viewport_translate), + offset: Some(translation_viewport), flush: false, }); let typed_string = (!self.typing.digits.is_empty() && self.transform_operation.can_begin_typing()).then(|| self.typing.string.clone()); - overlay_context.translation_box(translation, quad, typed_string); + overlay_context.translation_box(document_to_viewport.inverse().transform_vector2(translation_viewport), quad, typed_string); } TransformOperation::Scaling(scale) => { - let scale = scale.to_f64(self.increments); + let scale = scale.to_f64(self.state.is_rounded_to_intervals); let text = format!("{}x", format_rounded(scale, 3)); - let pivot = document_to_viewport.transform_point2(self.local_pivot); let start_mouse = document_to_viewport.transform_point2(self.local_mouse_start); - let local_edge = start_mouse - pivot; - let local_edge = project_edge_to_quad(local_edge, &self.layer_bounding_box, self.local, axis_constraint); - let boundary_point = pivot + local_edge * scale.min(1.); - let end_point = pivot + local_edge * scale.max(1.); + let local_edge = start_mouse - self.state.pivot_viewport(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.); if scale > 0. { - overlay_context.dashed_line(pivot, boundary_point, None, None, Some(2.), Some(2.), Some(0.5)); + overlay_context.dashed_line(self.state.pivot_viewport(document), boundary_point, None, None, Some(2.), Some(2.), Some(0.5)); } overlay_context.line(boundary_point, end_point, None, None); - let transform = DAffine2::from_translation(boundary_point.midpoint(pivot) + local_edge.perp().normalize_or(DVec2::X) * local_edge.element_product().signum() * 24.); + let transform = DAffine2::from_translation( + boundary_point.midpoint(self.state.pivot_viewport(document)) + local_edge.perp().normalize_or(DVec2::X) * local_edge.element_product().signum() * 24., + ); overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]); } TransformOperation::Rotating(rotation) => { - let angle = rotation.to_f64(self.increments); - let pivot = document_to_viewport.transform_point2(self.local_pivot); + let angle = rotation.to_f64(self.state.is_rounded_to_intervals); let start_mouse = document_to_viewport.transform_point2(self.local_mouse_start); let offset_angle = if self.grs_pen_handle { self.handle - self.last_point } else if using_path_tool { - start_mouse - pivot + start_mouse - self.state.pivot_viewport(document) } else { - self.layer_bounding_box.top_right() - self.layer_bounding_box.top_right() + self.layer_bounding_box.top_right() - self.layer_bounding_box.top_right() // TODO: This is always zero breaking the to_angle below???????? }; let tilt_offset = document.document_ptz.unmodified_tilt(); let offset_angle = offset_angle.to_angle() + tilt_offset; let width = viewport_box.max_element(); - let radius = start_mouse.distance(pivot); + let radius = start_mouse.distance(self.state.pivot_viewport(document)); let arc_radius = ANGLE_MEASURE_RADIUS_FACTOR * width; let radius = radius.clamp(ARC_MEASURE_RADIUS_FACTOR_RANGE.0 * width, ARC_MEASURE_RADIUS_FACTOR_RANGE.1 * width); let angle_in_degrees = angle.to_degrees(); @@ -270,8 +299,8 @@ impl MessageHandler> for (arc_radius + 4. + text_texture_width) * text_angle_on_unit_circle.x, (arc_radius + text_texture_height) * text_angle_on_unit_circle.y, ); - let transform = DAffine2::from_translation(text_texture_position + pivot); - overlay_context.draw_angle(pivot, radius, arc_radius, offset_angle, angle); + let transform = DAffine2::from_translation(text_texture_position + self.state.pivot_viewport(document)); + overlay_context.draw_angle(self.state.pivot_viewport(document), radius, arc_radius, offset_angle, angle); overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]); } } @@ -320,6 +349,9 @@ impl MessageHandler> for TransformType::Scale => TransformOperation::Scaling(Default::default()), }; self.layer_bounding_box = selected.bounding_box(); + let bounding_box = crate::messages::tool::tool_messages::select_tool::create_bounding_box_transform(document); + self.state.local_transform_axes = [bounding_box.x_axis, bounding_box.y_axis].map(|axis| axis.normalize_or_zero()); + info!("{:?}", self.state.local_transform_axes); } TransformLayerMessage::BeginGrabPen { last_point, handle } | TransformLayerMessage::BeginRotatePen { last_point, handle } | TransformLayerMessage::BeginScalePen { last_point, handle } => { self.typing.clear(); @@ -332,11 +364,13 @@ impl MessageHandler> for let top_left = DVec2::new(last_point.x, handle.y); let bottom_right = DVec2::new(handle.x, last_point.y); - self.local = false; + self.state.is_transforming_in_local_space = false; self.layer_bounding_box = Quad::from_box([top_left, bottom_right]); + let normalized_along = (handle - last_point).normalize_or_zero(); + self.state.local_transform_axes = [normalized_along, normalized_along.perp()]; self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(handle); - self.pivot = last_point; - self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(self.pivot); + let pivot = last_point; + self.state.document_space_pivot = document.metadata().document_to_viewport.inverse().transform_point2(pivot); self.local_mouse_start = document.metadata().document_to_viewport.inverse().transform_point2(self.start_mouse); self.handle = handle; @@ -428,7 +462,7 @@ impl MessageHandler> for } } - self.local = false; + self.state.is_transforming_in_local_space = false; self.operation_count += 1; let chain_operation = self.transform_operation != TransformOperation::None; @@ -479,50 +513,12 @@ impl MessageHandler> for }); } TransformLayerMessage::ConstrainX => { - let pivot = document_to_viewport.transform_point2(self.local_pivot); - self.local = self.transform_operation.constrain_axis( - Axis::X, - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ); - self.transform_operation.grs_typed( - self.typing.evaluate(), - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ); + self.state.is_transforming_in_local_space = self.transform_operation.constrain_axis(Axis::X, &mut selected, &self.state, document); + self.transform_operation.grs_typed(self.typing.evaluate(), &mut selected, &self.state, document); } TransformLayerMessage::ConstrainY => { - let pivot = document_to_viewport.transform_point2(self.local_pivot); - self.local = self.transform_operation.constrain_axis( - Axis::Y, - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ); - self.transform_operation.grs_typed( - self.typing.evaluate(), - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ); + self.state.is_transforming_in_local_space = self.transform_operation.constrain_axis(Axis::Y, &mut selected, &self.state, document); + self.transform_operation.grs_typed(self.typing.evaluate(), &mut selected, &self.state, document); } TransformLayerMessage::PointerMove { slow_key, increments_key } => { self.slow = input.keyboard.get(slow_key as usize); @@ -533,13 +529,10 @@ impl MessageHandler> for return; } - let pivot = document_to_viewport.transform_point2(self.local_pivot); - let new_increments = input.keyboard.get(increments_key as usize); - if new_increments != self.increments { - self.increments = new_increments; - self.transform_operation - .apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport, pivot, self.initial_transform); + if new_increments != self.state.is_rounded_to_intervals { + self.state.is_rounded_to_intervals = new_increments; + self.transform_operation.apply_transform_operation(&mut selected, &self.state, document); } if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() { @@ -550,43 +543,27 @@ impl MessageHandler> for let delta_pos = (self.initial_transform * document_to_viewport.inverse()).transform_vector2(delta_pos); let change = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos }; self.transform_operation = TransformOperation::Grabbing(translation.increment_amount(change)); - self.transform_operation.apply_transform_operation( - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ); + self.transform_operation.apply_transform_operation(&mut selected, &self.state, document); } TransformOperation::Rotating(rotation) => { - let start_offset = pivot - self.mouse_position; - let end_offset = pivot - input.mouse.position; + let start_offset = self.state.pivot_viewport(document) - self.mouse_position; + let end_offset = self.state.pivot_viewport(document) - input.mouse.position; let angle = start_offset.angle_to(end_offset); let change = if self.slow { angle / SLOWING_DIVISOR } else { angle }; self.transform_operation = TransformOperation::Rotating(rotation.increment_amount(change)); - self.transform_operation.apply_transform_operation( - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ); + self.transform_operation.apply_transform_operation(&mut selected, &self.state, document); } TransformOperation::Scaling(mut scale) => { let axis_constraint = scale.constraint; - let to_mouse_final = self.mouse_position - pivot; - let to_mouse_final_old = input.mouse.position - pivot; - let to_mouse_start = self.start_mouse - pivot; + let to_mouse_final = self.mouse_position - self.state.pivot_viewport(document); + 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 = project_edge_to_quad(to_mouse_final, &self.layer_bounding_box, self.local, axis_constraint); - let to_mouse_final_old = project_edge_to_quad(to_mouse_final_old, &self.layer_bounding_box, self.local, axis_constraint); - let to_mouse_start = project_edge_to_quad(to_mouse_start, &self.layer_bounding_box, self.local, axis_constraint); + 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); @@ -599,15 +576,7 @@ impl MessageHandler> for scale = scale.increment_amount(change); self.transform_operation = TransformOperation::Scaling(scale); - self.transform_operation.apply_transform_operation( - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ); + self.transform_operation.apply_transform_operation(&mut selected, &self.state, document); } }; } @@ -619,69 +588,27 @@ impl MessageHandler> for shape_editor.set_selected_layers(target_layers); } TransformLayerMessage::TypeBackspace => { - let pivot = document_to_viewport.transform_point2(self.local_pivot); if self.typing.digits.is_empty() && self.typing.negative { - self.transform_operation - .negate(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport, pivot, self.initial_transform); + self.transform_operation.negate(&mut selected, &self.state, document); self.typing.type_negate(); } - self.transform_operation.grs_typed( - self.typing.type_backspace(), - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ); + self.transform_operation.grs_typed(self.typing.type_backspace(), &mut selected, &self.state, document); } TransformLayerMessage::TypeDecimalPoint => { - let pivot = document_to_viewport.transform_point2(self.local_pivot); if self.transform_operation.can_begin_typing() { - self.transform_operation.grs_typed( - self.typing.type_decimal_point(), - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ) + self.transform_operation.grs_typed(self.typing.type_decimal_point(), &mut selected, &self.state, document) } } TransformLayerMessage::TypeDigit { digit } => { if self.transform_operation.can_begin_typing() { - let pivot = document_to_viewport.transform_point2(self.local_pivot); - self.transform_operation.grs_typed( - self.typing.type_number(digit), - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ) + self.transform_operation.grs_typed(self.typing.type_number(digit), &mut selected, &self.state, document) } } TransformLayerMessage::TypeNegate => { - let pivot = document_to_viewport.transform_point2(self.local_pivot); if self.typing.digits.is_empty() { - self.transform_operation - .negate(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport, pivot, self.initial_transform); + self.transform_operation.negate(&mut selected, &self.state, document); } - self.transform_operation.grs_typed( - self.typing.type_negate(), - &mut selected, - self.increments, - self.local, - self.layer_bounding_box, - document_to_viewport, - pivot, - self.initial_transform, - ) + self.transform_operation.grs_typed(self.typing.type_negate(), &mut selected, &self.state, document) } TransformLayerMessage::SetPivotGizmo { pivot_gizmo } => { self.pivot_gizmo = pivot_gizmo; @@ -721,7 +648,7 @@ impl TransformLayerMessageHandler { } pub fn hints(&self, responses: &mut VecDeque) { - self.transform_operation.hints(responses, self.local); + self.transform_operation.hints(responses, self.state.is_transforming_in_local_space); } fn set_ghost_outline(ghost_outline: &mut Vec<(Vec, DAffine2)>, shape_editor: &ShapeState, document: &DocumentMessageHandler) { @@ -797,26 +724,6 @@ fn calculate_pivot( } } -fn project_edge_to_quad(edge: DVec2, quad: &Quad, local: bool, axis_constraint: Axis) -> DVec2 { - match axis_constraint { - Axis::X => { - if local { - edge.project_onto(quad.top_right() - quad.top_left()) - } else { - edge.with_y(0.) - } - } - Axis::Y => { - if local { - edge.project_onto(quad.bottom_left() - quad.top_left()) - } else { - edge.with_x(0.) - } - } - _ => edge, - } -} - fn update_colinear_handles(selected_layers: &[LayerNodeIdentifier], document: &DocumentMessageHandler, responses: &mut VecDeque) { for &layer in selected_layers { let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; From c6797122ce68a10ff6cf3bcb0a0ffa9fbd0c131d Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 10 Nov 2025 21:11:55 -0800 Subject: [PATCH 2/4] Formatting --- .../document/utility_types/transformation.rs | 15 +++++--- .../tool/tool_messages/select_tool.rs | 2 +- .../transform_layer_message_handler.rs | 38 +++++++++---------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index bfb604c85e..f0f20e2da9 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -5,7 +5,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::{Modif use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::shape_editor::ShapeState; -use crate::messages::tool::transform_layer::transform_layer_message_handler::OtherUsefulParameters; +use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformationState; use crate::messages::tool::utility_types::ToolType; use glam::{DAffine2, DMat2, DVec2}; use graphene_std::renderer::Quad; @@ -156,7 +156,7 @@ pub struct Translation { } impl Translation { - pub fn to_dvec(self, state: &OtherUsefulParameters) -> DVec2 { + pub fn to_dvec(self, state: &TransformationState) -> DVec2 { let displacement = if let Some(value) = self.typed_distance { match self.constraint { Axis::X => DVec2::X * value, @@ -326,7 +326,7 @@ impl TransformType { impl TransformOperation { #[allow(clippy::too_many_arguments)] - pub fn apply_transform_operation(&self, selected: &mut Selected, state: &OtherUsefulParameters, document: &DocumentMessageHandler) { + pub fn apply_transform_operation(&self, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) { if self != &TransformOperation::None { let mut transformation = match self { TransformOperation::Grabbing(translation) => DAffine2::from_translation(translation.to_dvec(state)), @@ -355,7 +355,7 @@ impl TransformOperation { } #[allow(clippy::too_many_arguments)] - pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, state: &OtherUsefulParameters, document: &DocumentMessageHandler) -> bool { + pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) -> bool { let resulting_local; (*self, resulting_local) = match self { TransformOperation::Grabbing(translation) => { @@ -368,12 +368,14 @@ impl TransformOperation { } _ => (*self, false), }; + self.apply_transform_operation(selected, state, document); + resulting_local } #[allow(clippy::too_many_arguments)] - pub fn grs_typed(&mut self, typed: Option, selected: &mut Selected, state: &OtherUsefulParameters, document: &DocumentMessageHandler) { + pub fn grs_typed(&mut self, typed: Option, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) { match self { TransformOperation::None => (), TransformOperation::Grabbing(translation) => translation.typed_distance = typed, @@ -464,7 +466,7 @@ impl TransformOperation { } #[allow(clippy::too_many_arguments)] - pub fn negate(&mut self, selected: &mut Selected, state: &OtherUsefulParameters, document: &DocumentMessageHandler) { + pub fn negate(&mut self, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) { if *self != TransformOperation::None { *self = match self { TransformOperation::Scaling(scale) => TransformOperation::Scaling(scale.negate()), @@ -472,6 +474,7 @@ impl TransformOperation { TransformOperation::Grabbing(translation) => TransformOperation::Grabbing(translation.negate()), _ => *self, }; + self.apply_transform_operation(selected, state, document); } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 751bda584e..938b5ff6f9 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -646,7 +646,7 @@ impl Fsm for SelectToolFsmState { } } - let mut transform = create_bounding_box_transform(&document); + let mut transform = create_bounding_box_transform(document); // Check if the matrix is not invertible let mut transform_tampered = false; 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 ef4d141f91..16b432d3b9 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 @@ -7,6 +7,7 @@ use crate::messages::portfolio::document::utility_types::transformation::{Axis, use crate::messages::prelude::*; use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType}; use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::tool_messages::select_tool; use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::utility_types::{ToolData, ToolType}; use glam::{DAffine2, DVec2}; @@ -32,18 +33,18 @@ pub struct TransformLayerMessageContext<'a> { } #[derive(Debug, Clone, Default, ExtractField)] -pub struct OtherUsefulParameters { +pub struct TransformationState { pub is_rounded_to_intervals: bool, pub is_transforming_in_local_space: bool, pub local_transform_axes: [DVec2; 2], - pub document_space_pivot: DocumentPosition, } -impl OtherUsefulParameters { +impl TransformationState { pub fn pivot_viewport(&self, document: &DocumentMessageHandler) -> DVec2 { document.metadata().document_to_viewport.transform_point2(self.document_space_pivot) } + 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 { DVec2::X }), @@ -51,52 +52,47 @@ impl OtherUsefulParameters { _ => 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 local_to_viewport_transform(&self) -> DAffine2 { - self.is_transforming_in_local_space - .then(|| DAffine2::from_cols(self.local_transform_axes[0], self.local_transform_axes[1], DVec2::ZERO)) - .unwrap_or_default() + if self.is_transforming_in_local_space { + DAffine2::from_cols(self.local_transform_axes[0], self.local_transform_axes[1], DVec2::ZERO) + } else { + DAffine2::IDENTITY + } } } #[derive(Debug, Clone, Default, ExtractField)] pub struct TransformLayerMessageHandler { pub transform_operation: TransformOperation, - + state: TransformationState, slow: bool, layer_bounding_box: Quad, typing: Typing, - mouse_position: ViewportPosition, start_mouse: ViewportPosition, - original_transforms: OriginalTransforms, pivot_gizmo: PivotGizmo, pivot: ViewportPosition, - path_bounds: Option<[DVec2; 2]>, - local_mouse_start: DocumentPosition, grab_target: DocumentPosition, - ptz: PTZ, initial_transform: DAffine2, - operation_count: usize, + was_grabbing: bool, // Pen tool (outgoing handle GRS manipulation) handle: DVec2, last_point: DVec2, grs_pen_handle: bool, - // Ghost outlines for Path Tool + // Path tool (ghost outlines showing pre-transform geometry) ghost_outline: Vec<(Vec, DAffine2)>, - - state: OtherUsefulParameters, - - was_grabbing: bool, } #[message_handler_data] @@ -275,7 +271,8 @@ impl MessageHandler> for } else if using_path_tool { start_mouse - self.state.pivot_viewport(document) } else { - self.layer_bounding_box.top_right() - self.layer_bounding_box.top_right() // TODO: This is always zero breaking the to_angle below???????? + // TODO: This is always zero breaking the `.to_angle()` below? + self.layer_bounding_box.top_right() - self.layer_bounding_box.top_right() }; let tilt_offset = document.document_ptz.unmodified_tilt(); let offset_angle = offset_angle.to_angle() + tilt_offset; @@ -349,9 +346,8 @@ impl MessageHandler> for TransformType::Scale => TransformOperation::Scaling(Default::default()), }; self.layer_bounding_box = selected.bounding_box(); - let bounding_box = crate::messages::tool::tool_messages::select_tool::create_bounding_box_transform(document); + let bounding_box = select_tool::create_bounding_box_transform(document); self.state.local_transform_axes = [bounding_box.x_axis, bounding_box.y_axis].map(|axis| axis.normalize_or_zero()); - info!("{:?}", self.state.local_transform_axes); } TransformLayerMessage::BeginGrabPen { last_point, handle } | TransformLayerMessage::BeginRotatePen { last_point, handle } | TransformLayerMessage::BeginScalePen { last_point, handle } => { self.typing.clear(); From 629ee23444b53b2104ba9049976984ae44d3bb80 Mon Sep 17 00:00:00 2001 From: hypercube <0hypercube@gmail.com> Date: Mon, 24 Nov 2025 22:50:59 +0000 Subject: [PATCH 3/4] Multiply by document_to_viewport.matrix2.y_axis.length() --- .../portfolio/document/utility_types/transformation.rs | 6 ++++-- .../transform_layer/transform_layer_message_handler.rs | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index f0f20e2da9..8824e4df82 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -156,7 +156,8 @@ pub struct Translation { } impl Translation { - pub fn to_dvec(self, state: &TransformationState) -> DVec2 { + pub fn to_dvec(self, state: &TransformationState, document: &DocumentMessageHandler) -> DVec2 { + 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, @@ -170,6 +171,7 @@ impl Translation { Axis::Y => DVec2::Y * self.dragged_distance.dot(state.constraint_axis(self.constraint).unwrap_or_default()), } }; + let displacement = displacement * document_to_viewport.matrix2.y_axis.length(); // Values are local to the viewport but scaled so values are relative to the current scale. if state.is_rounded_to_intervals { displacement.round() } else { displacement } } @@ -329,7 +331,7 @@ impl TransformOperation { pub fn apply_transform_operation(&self, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) { if self != &TransformOperation::None { let mut transformation = match self { - TransformOperation::Grabbing(translation) => DAffine2::from_translation(translation.to_dvec(state)), + TransformOperation::Grabbing(translation) => DAffine2::from_translation(translation.to_dvec(state, document)), TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(state.is_rounded_to_intervals)), TransformOperation::Scaling(scale) => DAffine2::from_scale(scale.to_dvec(state.is_rounded_to_intervals)), TransformOperation::None => unreachable!(), 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 16b432d3b9..c642225083 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 @@ -231,8 +231,7 @@ impl MessageHandler> for match self.transform_operation { TransformOperation::None => (), TransformOperation::Grabbing(translation) => { - let translation_local = translation.to_dvec(&self.state); - let translation_viewport = (self.state.local_to_viewport_transform() * document_to_viewport).transform_vector2(translation_local); + let translation_viewport = self.state.local_to_viewport_transform().matrix2 * translation.to_dvec(&self.state, document); let pivot = document_to_viewport.transform_point2(self.grab_target); let quad = Quad::from_box([pivot, pivot + translation_viewport]); @@ -242,7 +241,7 @@ impl MessageHandler> for }); let typed_string = (!self.typing.digits.is_empty() && self.transform_operation.can_begin_typing()).then(|| self.typing.string.clone()); - overlay_context.translation_box(document_to_viewport.inverse().transform_vector2(translation_viewport), quad, typed_string); + overlay_context.translation_box(translation_viewport, quad, typed_string); } TransformOperation::Scaling(scale) => { let scale = scale.to_f64(self.state.is_rounded_to_intervals); @@ -537,8 +536,9 @@ impl MessageHandler> for TransformOperation::Grabbing(translation) => { let delta_pos = input.mouse.position - self.mouse_position; let delta_pos = (self.initial_transform * document_to_viewport.inverse()).transform_vector2(delta_pos); - let change = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos }; - self.transform_operation = TransformOperation::Grabbing(translation.increment_amount(change)); + let delta_viewport = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos }; + let delta_scaled = delta_viewport / document_to_viewport.y_axis.length(); // Values are local to the viewport but scaled so values are relative to the current scale. + self.transform_operation = TransformOperation::Grabbing(translation.increment_amount(delta_scaled)); self.transform_operation.apply_transform_operation(&mut selected, &self.state, document); } TransformOperation::Rotating(rotation) => { From f9885a79778b9e513d402c599add384c27a1536f Mon Sep 17 00:00:00 2001 From: hypercube <0hypercube@gmail.com> Date: Mon, 24 Nov 2025 23:10:31 +0000 Subject: [PATCH 4/4] Fix logic errors --- .../portfolio/document/utility_types/transformation.rs | 6 ++++-- .../tool/transform_layer/transform_layer_message_handler.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index 8824e4df82..47bc1eed2e 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -171,8 +171,10 @@ impl Translation { Axis::Y => DVec2::Y * self.dragged_distance.dot(state.constraint_axis(self.constraint).unwrap_or_default()), } }; - let displacement = displacement * document_to_viewport.matrix2.y_axis.length(); // Values are local to the viewport but scaled so values are relative to the current scale. - if state.is_rounded_to_intervals { displacement.round() } else { displacement } + 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. + let displacement_document = document_to_viewport.inverse().transform_vector2(displacement_viewport); + let displacement_document = if state.is_rounded_to_intervals { displacement_document.round() } else { displacement_document }; // It rounds in document space? + document_to_viewport.transform_vector2(displacement_document) } #[must_use] 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 c642225083..488542c6ee 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 @@ -241,7 +241,7 @@ impl MessageHandler> for }); let typed_string = (!self.typing.digits.is_empty() && self.transform_operation.can_begin_typing()).then(|| self.typing.string.clone()); - overlay_context.translation_box(translation_viewport, quad, typed_string); + overlay_context.translation_box(translation_viewport / document_to_viewport.matrix2.y_axis.length(), quad, typed_string); } TransformOperation::Scaling(scale) => { let scale = scale.to_f64(self.state.is_rounded_to_intervals);