Repository: GraphiteEditor/Graphite (language: rust)

Issue:
G/R/S visualization overlays
G/R/S, used with both the Select and Path tools, should display an X or Y line when grabbing or scaling in the X or Y axis. (The line should be aligned with the common center, i.e. the pivot used for scaling or rotating.) It should go from one end of the viewport to the other. It should also display a dot around the pivot while rotating or scaling. Reference how other overlays are drawn (like those visualizing snapping relationships) for how to draw overlay shapes like lines and dots.

Also, text should be drawn next to those lines visualizing the numerical offsets. Currently these values are shown in the hints bar, which is a temporary workaround until this is implemented.

Also: a line should be drawn when dragging with the Select tool while holding <kbd>Shift</kbd> to constrain it to the X or Y axis. (That behavior already exists, but the line should be added to visualize it.)

- [x] #2195
- [x] #2229
- [x] #2243
- [x] #2285


Discussion / hints:
Commenting so you can assign.

Fix patch:
diff --git a/editor/src/messages/input_mapper/utility_types/input_mouse.rs b/editor/src/messages/input_mapper/utility_types/input_mouse.rs
index 1bebca528d..8302345fad 100644
--- a/editor/src/messages/input_mapper/utility_types/input_mouse.rs
+++ b/editor/src/messages/input_mapper/utility_types/input_mouse.rs
@@ -6,6 +6,7 @@ use glam::DVec2;
 use std::collections::VecDeque;
 
 // Origin is top left
+pub type DocumentPosition = DVec2;
 pub type ViewportPosition = DVec2;
 pub type EditorPosition = DVec2;
 
diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs
index 590bb1b34c..02f26d51f7 100644
--- a/editor/src/messages/portfolio/document/utility_types/misc.rs
+++ b/editor/src/messages/portfolio/document/utility_types/misc.rs
@@ -653,6 +653,10 @@ impl PTZ {
 		(((self.tilt + std::f64::consts::PI) % std::f64::consts::TAU) + std::f64::consts::TAU) % std::f64::consts::TAU - std::f64::consts::PI
 	}
 
+	pub fn unmodified_tilt(&self) -> f64 {
+		self.tilt
+	}
+
 	/// Set a new tilt angle in radians.
 	pub fn set_tilt(&mut self, tilt: f64) {
 		self.tilt = tilt;
diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs
index 69d4fee996..8eafbe758b 100644
--- a/editor/src/messages/portfolio/document/utility_types/transformation.rs
+++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs
@@ -14,6 +14,7 @@ use graphene_std::vector::{HandleId, PointId};
 
 use glam::{DAffine2, DMat2, DVec2};
 use std::collections::{HashMap, VecDeque};
+use std::f64::consts::PI;
 
 #[derive(Debug, PartialEq, Clone, Copy)]
 struct AnchorPoint {
@@ -305,17 +306,17 @@ pub enum TransformOperation {
 }
 
 impl TransformOperation {
-	pub fn apply_transform_operation(&self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2) {
+	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();
 		if self != &TransformOperation::None {
 			let transformation = match self {
 				TransformOperation::Grabbing(translation) => {
-					let translate = DAffine2::from_translation(transform.transform_vector2(translation.to_dvec(transform, increment_mode)));
+					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 - std::f64::consts::PI
-						} else {
 							local_axis_transform_angle
+						} else {
+							local_axis_transform_angle - PI
 						};
 						DAffine2::from_angle(resolved_angle) * translate * DAffine2::from_angle(-resolved_angle)
 					} else {
@@ -333,7 +334,7 @@ impl TransformOperation {
 				TransformOperation::None => unreachable!(),
 			};
 
-			selected.update_transforms(transformation);
+			selected.update_transforms(transformation, Some(pivot));
 			self.hints(selected.responses, local);
 		}
 	}
@@ -350,7 +351,7 @@ impl TransformOperation {
 		self.is_constraint_to_axis() || !matches!(self, TransformOperation::Grabbing(_))
 	}
 
-	pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, increment_mode: bool, mut local: bool, quad: Quad, transform: DAffine2) -> bool {
+	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 {
 			TransformOperation::Grabbing(translation) => {
 				let (translation, local) = translation.with_constraint(axis, local);
@@ -362,11 +363,11 @@ impl TransformOperation {
 			}
 			_ => (*self, false),
 		};
-		self.apply_transform_operation(selected, increment_mode, local, quad, transform);
+		self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform);
 		local
 	}
 
-	pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2) {
+	pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
 		match self {
 			TransformOperation::None => (),
 			TransformOperation::Grabbing(translation) => translation.typed_distance = typed,
@@ -374,7 +375,7 @@ impl TransformOperation {
 			TransformOperation::Scaling(scale) => scale.typed_factor = typed,
 		};
 
-		self.apply_transform_operation(selected, increment_mode, local, quad, transform);
+		self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform);
 	}
 
 	pub fn hints(&self, responses: &mut VecDeque<Message>, local: bool) {
@@ -456,7 +457,7 @@ impl TransformOperation {
 		}
 	}
 
-	pub fn negate(&mut self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2) {
+	pub fn negate(&mut self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
 		if *self != TransformOperation::None {
 			*self = match self {
 				TransformOperation::Scaling(scale) => TransformOperation::Scaling(scale.negate()),
@@ -464,7 +465,7 @@ impl TransformOperation {
 				TransformOperation::Grabbing(translation) => TransformOperation::Grabbing(translation.negate()),
 				_ => *self,
 			};
-			self.apply_transform_operation(selected, increment_mode, local, quad, transform);
+			self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform);
 		}
 	}
 }
@@ -624,8 +625,8 @@ impl<'a> Selected<'a> {
 		}
 	}
 
-	pub fn update_transforms(&mut self, delta: DAffine2) {
-		let pivot = DAffine2::from_translation(*self.pivot);
+	pub fn update_transforms(&mut self, delta: DAffine2, pivot: Option<DVec2>) {
+		let pivot = DAffine2::from_translation(pivot.unwrap_or_else(|| *self.pivot));
 		let transformation = pivot * delta * pivot.inverse();
 		match self.tool_type {
 			ToolType::Pen => self.apply_transform_pen(transformation),
diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs
index a2befd59c1..4b8559659b 100644
--- a/editor/src/messages/tool/tool_messages/select_tool.rs
+++ b/editor/src/messages/tool/tool_messages/select_tool.rs
@@ -1085,7 +1085,7 @@ impl Fsm for SelectToolFsmState {
 						None,
 					);
 
-					selected.update_transforms(delta);
+					selected.update_transforms(delta, None);
 				}
 
 				SelectToolFsmState::RotatingBounds
@@ -1189,6 +1189,7 @@ impl Fsm for SelectToolFsmState {
 					true => DocumentMessage::AbortTransaction,
 					false => DocumentMessage::EndTransaction,
 				};
+				tool_data.axis_align = false;
 				tool_data.snap_manager.cleanup(responses);
 				responses.add_front(response);
 
@@ -1380,6 +1381,7 @@ impl Fsm for SelectToolFsmState {
 			(SelectToolFsmState::Dragging { .. }, SelectToolMessage::Abort) => {
 				responses.add(DocumentMessage::AbortTransaction);
 				tool_data.snap_manager.cleanup(responses);
+				tool_data.axis_align = false;
 				responses.add(OverlaysMessage::Draw);
 
 				let selection = tool_data.nested_selection_behavior;
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 317b671587..d2aa875f95 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
@@ -1,6 +1,7 @@
 use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, SLOWING_DIVISOR};
-use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
+use crate::messages::input_mapper::utility_types::input_mouse::{DocumentPosition, ViewportPosition};
 use crate::messages::portfolio::document::overlays::utility_types::{OverlayProvider, Pivot};
+use crate::messages::portfolio::document::utility_types::misc::PTZ;
 use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
 use crate::messages::prelude::*;
 use crate::messages::tool::common_functionality::shape_editor::ShapeState;
@@ -34,8 +35,12 @@ pub struct TransformLayerMessageHandler {
 	start_mouse: ViewportPosition,
 
 	original_transforms: OriginalTransforms,
-	pivot: DVec2,
-	grab_target: DVec2,
+	pivot: ViewportPosition,
+	local_pivot: DocumentPosition,
+	local_mouse_start: DocumentPosition,
+	grab_target: DocumentPosition,
+	ptz: PTZ,
+	initial_transform: DAffine2,
 
 	// Pen tool (outgoing handle GRS manipulation)
 	handle: DVec2,
@@ -125,7 +130,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 			Some(&mut self.handle),
 		);
 
-		let mut begin_operation = |operation: TransformOperation, typing: &mut Typing, mouse_position: &mut DVec2, start_mouse: &mut DVec2| {
+		let document_to_viewport = document.metadata().document_to_viewport;
+		let mut begin_operation = |operation: TransformOperation, typing: &mut Typing, mouse_position: &mut DVec2, start_mouse: &mut DVec2, transform: &mut DAffine2| {
 			if operation != TransformOperation::None {
 				selected.revert_operation();
 				typing.clear();
@@ -142,7 +148,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 
 			if !using_path_tool {
 				*selected.pivot = selected.mean_average_of_pivots();
-				self.grab_target = selected.mean_average_of_pivots();
+				self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
+				self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(selected.mean_average_of_pivots());
 			} else if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
 				*selected.original_transforms = OriginalTransforms::default();
 
@@ -153,7 +160,11 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				if let Some((new_pivot, grab_target)) = calculate_pivot(&selected_points, &vector_data, viewspace, |point: &ManipulatorPointId| get_location(&point)) {
 					*selected.pivot = new_pivot;
 
+					self.local_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot);
+
 					self.grab_target = grab_target;
+
+					self.grab_target = document_to_viewport.inverse().transform_point2(grab_target);
 				} else {
 					log::warn!("Failed to calculate pivot.");
 				}
@@ -161,11 +172,13 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 
 			*mouse_position = input.mouse.position;
 			*start_mouse = input.mouse.position;
+			*transform = document_to_viewport;
+			self.local_mouse_start = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
+
 			selected.original_transforms.clear();
 
 			selected.responses.add(DocumentMessage::StartTransaction);
 		};
-		let document_to_viewport = document.metadata().document_to_viewport;
 
 		match message {
 			// Overlays
@@ -186,12 +199,19 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 						}
 					};
 
+					// TODO: Ensure removing this and adding this doesn't change the position of layers under PTZ ops
+					// responses.add(TransformLayerMessage::PointerMove {
+					// 	slow_key: SLOW_KEY,
+					// 	increments_key: INCREMENTS_KEY,
+					// });
+
 					match self.transform_operation {
 						TransformOperation::None => (),
 						TransformOperation::Grabbing(translation) => {
-							let translation = translation.to_dvec(document_to_viewport, self.increments);
+							let translation = translation.to_dvec(self.initial_transform, self.increments);
 							let viewport_translate = document_to_viewport.transform_vector2(translation);
-							let quad = Quad::from_box([self.grab_target, self.grab_target + viewport_translate]).0;
+							let pivot = document_to_viewport.transform_point2(self.grab_target);
+							let quad = Quad::from_box([pivot, pivot + viewport_translate]).0;
 							let e1 = (self.layer_bounding_box.0[1] - self.layer_bounding_box.0[0]).normalize_or(DVec2::X);
 
 							if matches!(axis_constraint, Axis::Both | Axis::X) && translation.x != 0. {
@@ -215,7 +235,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 								overlay_context.line(quad[0], end, None);
 								let x_parameter = viewport_translate.x.clamp(-1., 1.);
 								let y_transform = DAffine2::from_translation((quad[0] + end) / 2. + x_parameter * DVec2::X * 0.);
-								let pivot_selection = if x_parameter >= 0. { Pivot::Start } else { Pivot::End };
+								let pivot_selection = if x_parameter >= -1e-3 { Pivot::Start } else { Pivot::End };
 								if axis_constraint != Axis::Both || self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
 									overlay_context.text(&format_rounded(translation.y, 2), COLOR_OVERLAY_BLUE, None, y_transform, 3., [pivot_selection, Pivot::Middle]);
 								}
@@ -228,31 +248,36 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 						TransformOperation::Scaling(scale) => {
 							let scale = scale.to_f64(self.increments);
 							let text = format!("{}x", format_rounded(scale, 3));
-							let local_edge = self.start_mouse - self.pivot;
+							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 = self.pivot + local_edge * scale.min(1.);
-							let end_point = self.pivot + local_edge * scale.max(1.);
+							let boundary_point = pivot + local_edge * scale.min(1.);
+							let end_point = pivot + local_edge * scale.max(1.);
 
 							if scale > 0. {
-								overlay_context.dashed_line(self.pivot, boundary_point, None, Some(4.), Some(4.), Some(0.5));
+								overlay_context.dashed_line(pivot, boundary_point, None, Some(4.), Some(4.), Some(0.5));
 							}
 							overlay_context.line(boundary_point, end_point, None);
 
-							let transform = DAffine2::from_translation(boundary_point.midpoint(self.pivot) + local_edge.perp().normalize_or(DVec2::X) * local_edge.element_product().signum() * 24.);
+							let transform = DAffine2::from_translation(boundary_point.midpoint(pivot) + 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 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 {
-								self.start_mouse - self.pivot
+								start_mouse - pivot
 							} else {
 								self.layer_bounding_box.top_right() - self.layer_bounding_box.top_right()
 							};
-							let offset_angle = offset_angle.to_angle();
+							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 = self.start_mouse.distance(self.pivot);
+							let radius = start_mouse.distance(pivot);
 							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 text = format!("{}°", format_rounded(angle.to_degrees(), 2));
@@ -263,8 +288,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 								(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 + self.pivot);
-							overlay_context.draw_angle(self.pivot, radius, arc_radius, offset_angle, angle);
+							let transform = DAffine2::from_translation(text_texture_position + pivot);
+							overlay_context.draw_angle(pivot, radius, arc_radius, offset_angle, angle);
 							overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
 						}
 					}
@@ -305,8 +330,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				let bottom_right = DVec2::new(handle.x, last_point.y);
 				self.local = false;
 				self.layer_bounding_box = Quad::from_box([top_left, bottom_right]);
-				self.grab_target = handle;
+				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);
+				self.local_mouse_start = document.metadata().document_to_viewport.inverse().transform_point2(self.start_mouse);
 				self.handle = handle;
 
 				// Operation-specific logic
@@ -335,7 +362,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 					return;
 				}
 
-				begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
+				begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse, &mut self.initial_transform);
 
 				self.transform_operation = TransformOperation::Grabbing(Default::default());
 				self.local = false;
@@ -388,7 +415,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 					}
 				}
 
-				begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
+				begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse, &mut self.initial_transform);
 
 				self.transform_operation = TransformOperation::Rotating(Default::default());
 
@@ -441,7 +468,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 					}
 				}
 
-				begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse);
+				begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse, &mut self.initial_transform);
 
 				self.transform_operation = TransformOperation::Scaling(Default::default());
 
@@ -479,56 +506,110 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
 			}
 			TransformLayerMessage::ConstrainX => {
-				self.local = self
-					.transform_operation
-					.constrain_axis(Axis::X, &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
-				self.transform_operation
-					.grs_typed(self.typing.evaluate(), &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
+				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,
+				);
 			}
 			TransformLayerMessage::ConstrainY => {
-				self.local = self
-					.transform_operation
-					.constrain_axis(Axis::Y, &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
-				self.transform_operation
-					.grs_typed(self.typing.evaluate(), &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
+				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,
+				);
 			}
 			TransformLayerMessage::PointerMove { slow_key, increments_key } => {
 				self.slow = input.keyboard.get(slow_key as usize);
+				let old_ptz = self.ptz;
+				self.ptz = document.document_ptz;
+				if old_ptz != self.ptz {
+					self.mouse_position = input.mouse.position;
+					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);
+						.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport, pivot, self.initial_transform);
 				}
 
 				if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
-					let delta_pos = input.mouse.position - self.mouse_position;
-
 					match self.transform_operation {
 						TransformOperation::None => unreachable!(),
 						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));
-							self.transform_operation
-								.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
+							self.transform_operation.apply_transform_operation(
+								&mut selected,
+								self.increments,
+								self.local,
+								self.layer_bounding_box,
+								document_to_viewport,
+								pivot,
+								self.initial_transform,
+							);
 						}
 						TransformOperation::Rotating(rotation) => {
-							let start_offset = *selected.pivot - self.mouse_position;
-							let end_offset = *selected.pivot - input.mouse.position;
+							let start_offset = pivot - self.mouse_position;
+							let end_offset = pivot - 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);
+							self.transform_operation.apply_transform_operation(
+								&mut selected,
+								self.increments,
+								self.local,
+								self.layer_bounding_box,
+								document_to_viewport,
+								pivot,
+								self.initial_transform,
+							);
 						}
 						TransformOperation::Scaling(mut scale) => {
 							let axis_constraint = scale.constraint;
-							let to_mouse_final = self.mouse_position - *selected.pivot;
-							let to_mouse_final_old = input.mouse.position - *selected.pivot;
-							let to_mouse_start = self.start_mouse - *selected.pivot;
+							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 = 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);
@@ -545,8 +626,15 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 
 							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);
+							self.transform_operation.apply_transform_operation(
+								&mut selected,
+								self.increments,
+								self.local,
+								self.layer_bounding_box,
+								document_to_viewport,
+								pivot,
+								self.initial_transform,
+							);
 						}
 					};
 				}
@@ -558,15 +646,25 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				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);
+						.negate(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport, pivot, self.initial_transform);
 					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);
+				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,
+				);
 			}
 			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(),
@@ -575,11 +673,14 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 						self.local,
 						self.layer_bounding_box,
 						document_to_viewport,
+						pivot,
+						self.initial_transform,
 					)
 				}
 			}
 			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,
@@ -587,16 +688,27 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 						self.local,
 						self.layer_bounding_box,
 						document_to_viewport,
+						pivot,
+						self.initial_transform,
 					)
 				}
 			}
 			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);
+						.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.increments, self.local, self.layer_bounding_box, document_to_viewport)
+				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,
+				)
 			}
 		}
 	}


Test patch: