Repository: GraphiteEditor/Graphite (language: rust)

Issue:
G/R/S on a layer composed of a single anchor point crashes in dev mode
Use the Pen tool to draw a new layer with a single point, but don't draw any segments before finishing. With that new point layer selected, in the Select tool, use `G`, `R`, or `S` which crashes the editor (however, no crash dialog appears for some reason, which should also be investigated as to why not). But the editor stops working and the panic is shown in the browser console. This happens only in debug mode when running from localhost.


Discussion / hints:

Fix patch:
diff --git a/editor/src/consts.rs b/editor/src/consts.rs
index ec247089c9..e4bb2cc126 100644
--- a/editor/src/consts.rs
+++ b/editor/src/consts.rs
@@ -41,8 +41,8 @@ pub const MAX_LAYER_SNAP_POINTS: usize = 100;
 pub const DRAG_THRESHOLD: f64 = 1.;
 
 // TRANSFORMING LAYER
-pub const ROTATE_SNAP_ANGLE: f64 = 15.;
-pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
+pub const ROTATE_INCREMENT: f64 = 15.;
+pub const SCALE_INCREMENT: f64 = 0.1;
 pub const SLOWING_DIVISOR: f64 = 10.;
 pub const NUDGE_AMOUNT: f64 = 1.;
 pub const BIG_NUDGE_AMOUNT: f64 = 10.;
diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs
index 7c1f7bf12d..6773f13548 100644
--- a/editor/src/messages/portfolio/document/overlays/utility_types.rs
+++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs
@@ -45,7 +45,7 @@ impl OverlayContext {
 	}
 
 	pub fn polygon(&mut self, polygon: &[DVec2], color_fill: Option<&str>) {
-		self.dashed_polygon(&polygon, color_fill, None, None, None);
+		self.dashed_polygon(polygon, color_fill, None, None, None);
 	}
 
 	pub fn dashed_polygon(&mut self, polygon: &[DVec2], color_fill: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs
index 315a91a983..69d4fee996 100644
--- a/editor/src/messages/portfolio/document/utility_types/transformation.rs
+++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs
@@ -1,5 +1,5 @@
 use super::network_interface::NodeNetworkInterface;
-use crate::consts::{ROTATE_SNAP_ANGLE, SCALE_SNAP_INTERVAL};
+use crate::consts::{ROTATE_INCREMENT, SCALE_INCREMENT};
 use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
 use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
 use crate::messages::prelude::*;
@@ -12,7 +12,7 @@ use graphene_core::vector::ManipulatorPointId;
 use graphene_core::vector::VectorModificationType;
 use graphene_std::vector::{HandleId, PointId};
 
-use glam::{DAffine2, DVec2};
+use glam::{DAffine2, DMat2, DVec2};
 use std::collections::{HashMap, VecDeque};
 
 #[derive(Debug, PartialEq, Clone, Copy)]
@@ -143,10 +143,13 @@ pub struct Translation {
 }
 
 impl Translation {
-	pub fn to_dvec(self, transform: DAffine2, snap: bool) -> DVec2 {
+	pub fn to_dvec(self, transform: DAffine2, increment_mode: bool) -> DVec2 {
 		let displacement = if let Some(value) = self.typed_distance {
-			let document_displacement = if self.constraint == Axis::Y { DVec2::new(0., value) } else { DVec2::new(value, 0.) };
-			transform.transform_vector2(document_displacement)
+			match self.constraint {
+				Axis::X => transform.transform_vector2(DVec2::new(value, 0.)),
+				Axis::Y => transform.transform_vector2(DVec2::new(0., value)),
+				Axis::Both => self.dragged_distance,
+			}
 		} else {
 			match self.constraint {
 				Axis::Both => self.dragged_distance,
@@ -155,7 +158,7 @@ impl Translation {
 			}
 		};
 		let displacement = transform.inverse().transform_vector2(displacement);
-		if snap {
+		if increment_mode {
 			displacement.round()
 		} else {
 			displacement
@@ -196,12 +199,12 @@ pub struct Rotation {
 }
 
 impl Rotation {
-	pub fn to_f64(self, snap: bool) -> f64 {
+	pub fn to_f64(self, increment_mode: bool) -> f64 {
 		if let Some(value) = self.typed_angle {
 			value.to_radians()
-		} else if snap {
-			let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
-			(self.dragged_angle / snap_resolution).round() * snap_resolution
+		} else if increment_mode {
+			let increment_resolution = ROTATE_INCREMENT.to_radians();
+			(self.dragged_angle / increment_resolution).round() * increment_resolution
 		} else {
 			self.dragged_angle
 		}
@@ -245,17 +248,17 @@ impl Default for Scale {
 }
 
 impl Scale {
-	pub fn to_f64(self, snap: bool) -> f64 {
+	pub fn to_f64(self, increment: bool) -> f64 {
 		let factor = if let Some(value) = self.typed_factor { value } else { self.dragged_factor };
-		if snap {
-			(factor / SCALE_SNAP_INTERVAL).round() * SCALE_SNAP_INTERVAL
+		if increment {
+			(factor / SCALE_INCREMENT).round() * SCALE_INCREMENT
 		} else {
 			factor
 		}
 	}
 
-	pub fn to_dvec(self, snap: bool) -> DVec2 {
-		let factor = self.to_f64(snap);
+	pub fn to_dvec(self, increment_mode: bool) -> DVec2 {
+		let factor = self.to_f64(increment_mode);
 
 		match self.constraint {
 			Axis::Both => DVec2::splat(factor),
@@ -272,7 +275,7 @@ impl Scale {
 	#[must_use]
 	pub fn increment_amount(self, delta: f64) -> Self {
 		Self {
-			dragged_factor: self.dragged_factor + delta,
+			dragged_factor: (self.dragged_factor + delta),
 			typed_factor: None,
 			constraint: self.constraint,
 		}
@@ -302,25 +305,29 @@ pub enum TransformOperation {
 }
 
 impl TransformOperation {
-	pub fn apply_transform_operation(&self, selected: &mut Selected, snapping: bool, local: bool, quad: Quad, transform: DAffine2) {
-		let quad = quad.0;
-		let edge = quad[1] - quad[0];
+	pub fn apply_transform_operation(&self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, 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, snapping)));
+					let translate = DAffine2::from_translation(transform.transform_vector2(translation.to_dvec(transform, increment_mode)));
 					if local {
-						DAffine2::from_angle(edge.to_angle()) * translate * DAffine2::from_angle(-edge.to_angle())
+						let resolved_angle = if local_axis_transform_angle > 0. {
+							local_axis_transform_angle - std::f64::consts::PI
+						} else {
+							local_axis_transform_angle
+						};
+						DAffine2::from_angle(resolved_angle) * translate * DAffine2::from_angle(-resolved_angle)
 					} else {
 						translate
 					}
 				}
-				TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(snapping)),
+				TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(increment_mode)),
 				TransformOperation::Scaling(scale) => {
 					if local {
-						DAffine2::from_angle(edge.to_angle()) * DAffine2::from_scale(scale.to_dvec(snapping)) * DAffine2::from_angle(-edge.to_angle())
+						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(snapping))
+						DAffine2::from_scale(scale.to_dvec(increment_mode))
 					}
 				}
 				TransformOperation::None => unreachable!(),
@@ -331,16 +338,19 @@ impl TransformOperation {
 		}
 	}
 
-	pub fn is_typing(&self) -> bool {
+	pub fn axis_constraint(&self) -> Axis {
 		match self {
-			TransformOperation::None => false,
-			TransformOperation::Grabbing(translation) => translation.typed_distance.is_some(),
-			TransformOperation::Rotating(rotation) => rotation.typed_angle.is_some(),
-			TransformOperation::Scaling(scale) => scale.typed_factor.is_some(),
+			TransformOperation::Grabbing(grabbing) => grabbing.constraint,
+			TransformOperation::Scaling(scaling) => scaling.constraint,
+			_ => Axis::Both,
 		}
 	}
 
-	pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, snapping: bool, mut local: bool, quad: Quad, transform: DAffine2) -> bool {
+	pub fn can_begin_typing(&self) -> bool {
+		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 {
 		(*self, local) = match self {
 			TransformOperation::Grabbing(translation) => {
 				let (translation, local) = translation.with_constraint(axis, local);
@@ -352,11 +362,11 @@ impl TransformOperation {
 			}
 			_ => (*self, false),
 		};
-		self.apply_transform_operation(selected, snapping, local, quad, transform);
+		self.apply_transform_operation(selected, increment_mode, local, quad, transform);
 		local
 	}
 
-	pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, snapping: 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) {
 		match self {
 			TransformOperation::None => (),
 			TransformOperation::Grabbing(translation) => translation.typed_distance = typed,
@@ -364,45 +374,34 @@ impl TransformOperation {
 			TransformOperation::Scaling(scale) => scale.typed_factor = typed,
 		};
 
-		self.apply_transform_operation(selected, snapping, local, quad, transform);
+		self.apply_transform_operation(selected, increment_mode, local, quad, transform);
 	}
 
 	pub fn hints(&self, responses: &mut VecDeque<Message>, local: bool) {
-		use crate::messages::input_mapper::utility_types::input_keyboard::Key;
+		use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion};
 		use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
 
 		let mut input_hints = Vec::new();
-		if self.is_typing() {
-			input_hints.push(HintInfo::keys([Key::Minus], "Negate Direction"));
-			input_hints.push(HintInfo::keys([Key::Backspace], "Delete Digit"));
-			input_hints.push(HintInfo::keys([Key::NumKeys], "Enter Number"));
-		} else if matches!(self, TransformOperation::Grabbing(_) | TransformOperation::Scaling(_)) {
-			let axis_constraint = match self {
-				TransformOperation::Grabbing(grabbing) => grabbing.constraint,
-				TransformOperation::Scaling(scaling) => scaling.constraint,
-				_ => Axis::Both,
-			};
-			let clear_constraint = "Clear Constraint";
-			match axis_constraint {
-				Axis::Both => {
-					input_hints.push(HintInfo::keys([Key::KeyX], "Along X Axis"));
-					input_hints.push(HintInfo::keys([Key::KeyY], "Along Y Axis"));
-				}
-				Axis::X => {
-					let x_label = if local { clear_constraint } else { "Along Local X Axis" };
-					input_hints.push(HintInfo::keys([Key::KeyX], x_label));
-					input_hints.push(HintInfo::keys([Key::KeyY], "Along Y Axis"));
-					if !local {
-						input_hints.push(HintInfo::keys([Key::KeyX, Key::KeyX], clear_constraint));
-					}
+		let clear_constraint = "Clear Constraint";
+		match self.axis_constraint() {
+			Axis::Both => {
+				input_hints.push(HintInfo::keys([Key::KeyX], "X-Axis Constraint"));
+				input_hints.push(HintInfo::keys([Key::KeyY], "Y-Axis Constraint"));
+			}
+			Axis::X => {
+				let x_label = if local { clear_constraint } else { "Local X-Axis Constraint" };
+				input_hints.push(HintInfo::keys([Key::KeyX], x_label));
+				input_hints.push(HintInfo::keys([Key::KeyY], "Y-Axis Constraint"));
+				if !local {
+					input_hints.push(HintInfo::keys([Key::KeyX, Key::KeyX], clear_constraint));
 				}
-				Axis::Y => {
-					let y_label = if local { clear_constraint } else { "Along Local Y Axis" };
-					input_hints.push(HintInfo::keys([Key::KeyX], "Along X Axis"));
-					input_hints.push(HintInfo::keys([Key::KeyY], y_label));
-					if !local {
-						input_hints.push(HintInfo::keys([Key::KeyY, Key::KeyY], clear_constraint));
-					}
+			}
+			Axis::Y => {
+				let y_label = if local { clear_constraint } else { "Local Y-Axis Constraint" };
+				input_hints.push(HintInfo::keys([Key::KeyX], "X-Axis Constraint"));
+				input_hints.push(HintInfo::keys([Key::KeyY], y_label));
+				if !local {
+					input_hints.push(HintInfo::keys([Key::KeyY, Key::KeyY], clear_constraint));
 				}
 			}
 		}
@@ -414,18 +413,50 @@ impl TransformOperation {
 			TransformOperation::Rotating(_) => HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyS]], "Grab/Scale Selected")]),
 		};
 
-		let mut hint_groups = vec![grs_hint_group];
+		let confirm_and_cancel_group = HintGroup(vec![
+			HintInfo::mouse(MouseMotion::Lmb, ""),
+			HintInfo::keys([Key::Enter], "Confirm").prepend_slash(),
+			HintInfo::mouse(MouseMotion::Rmb, ""),
+			HintInfo::keys([Key::Escape], "Cancel").prepend_slash(),
+		]);
+		let mut hint_groups = vec![confirm_and_cancel_group, grs_hint_group];
 		if !self.is_typing() {
-			let modifiers = vec![HintInfo::keys([Key::Shift], "Slow"), HintInfo::keys([Key::Control], "Increments")];
+			let modifiers = vec![
+				HintInfo::keys([Key::Shift], "Slow"),
+				HintInfo::keys([Key::Control], if matches!(self, TransformOperation::Rotating(_)) { "15° Increments" } else { "Increments" }),
+			];
 			hint_groups.push(HintGroup(modifiers));
 		}
-		hint_groups.push(HintGroup(input_hints));
+		if !matches!(self, TransformOperation::Rotating(_)) {
+			hint_groups.push(HintGroup(input_hints));
+		}
+		let mut typing_hints = vec![HintInfo::keys([Key::Minus], "Negate Direction")];
+		if self.can_begin_typing() {
+			typing_hints.push(HintInfo::keys([Key::NumKeys], "Enter Number"));
+			if self.is_typing() {
+				typing_hints.push(HintInfo::keys([Key::Backspace], "Delete Digit"));
+			}
+		}
+		hint_groups.push(HintGroup(typing_hints));
 
 		let hint_data = HintData(hint_groups);
 		responses.add(FrontendMessage::UpdateInputHints { hint_data });
 	}
 
-	pub fn negate(&mut self, selected: &mut Selected, snapping: bool, local: bool, quad: Quad, transform: DAffine2) {
+	pub fn is_constraint_to_axis(&self) -> bool {
+		self.axis_constraint() != Axis::Both
+	}
+
+	pub fn is_typing(&self) -> bool {
+		match self {
+			TransformOperation::None => false,
+			TransformOperation::Grabbing(translation) => translation.typed_distance.is_some(),
+			TransformOperation::Rotating(rotation) => rotation.typed_angle.is_some(),
+			TransformOperation::Scaling(scale) => scale.typed_factor.is_some(),
+		}
+	}
+
+	pub fn negate(&mut self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2) {
 		if *self != TransformOperation::None {
 			*self = match self {
 				TransformOperation::Scaling(scale) => TransformOperation::Scaling(scale.negate()),
@@ -433,7 +464,7 @@ impl TransformOperation {
 				TransformOperation::Grabbing(translation) => TransformOperation::Grabbing(translation.negate()),
 				_ => *self,
 			};
-			self.apply_transform_operation(selected, snapping, local, quad, transform);
+			self.apply_transform_operation(selected, increment_mode, local, quad, transform);
 		}
 	}
 }
@@ -505,7 +536,7 @@ impl<'a> Selected<'a> {
 	pub fn bounding_box(&mut self) -> Quad {
 		let metadata = self.network_interface.document_metadata();
 
-		let transform = self
+		let mut transform = self
 			.network_interface
 			.selected_nodes(&[])
 			.unwrap()
@@ -514,8 +545,8 @@ impl<'a> Selected<'a> {
 			.map(|layer| metadata.transform_to_viewport(layer))
 			.unwrap_or(DAffine2::IDENTITY);
 
-		if transform.matrix2.determinant() == 0. {
-			return Default::default();
+		if transform.matrix2.determinant().abs() <= f64::EPSILON {
+			transform.matrix2 += DMat2::IDENTITY * 1e-4;
 		}
 
 		let bounds = self
diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs
index 42d932ef76..811b1c2df4 100644
--- a/editor/src/messages/tool/common_functionality/transformation_cage.rs
+++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs
@@ -507,9 +507,9 @@ impl BoundingBoxManager {
 		let cursor = self.transform.inverse().transform_point2(cursor);
 		let [threshold_x, threshold_y] = self.compute_viewport_threshold(BOUNDS_ROTATE_THRESHOLD);
 
-		let narrow = (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any();
+		let flat = (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any();
 		let within_square_bounds = |center: &DVec2| center.x - threshold_x < cursor.x && cursor.x < center.x + threshold_x && center.y - threshold_y < cursor.y && cursor.y < center.y + threshold_y;
-		if narrow {
+		if flat {
 			[self.bounds[0], self.bounds[1]].iter().any(within_square_bounds)
 		} else {
 			self.evaluate_transform_handle_positions().iter().any(within_square_bounds)
diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs
index 40e6d3294e..835ace5cb7 100644
--- a/editor/src/messages/tool/tool_messages/select_tool.rs
+++ b/editor/src/messages/tool/tool_messages/select_tool.rs
@@ -1,7 +1,7 @@
 #![allow(clippy::too_many_arguments)]
 
 use super::tool_prelude::*;
-use crate::consts::{DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
+use crate::consts::{DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, ROTATE_INCREMENT, SELECTION_TOLERANCE};
 use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
 use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
 use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -935,7 +935,7 @@ impl Fsm for SelectToolFsmState {
 					};
 
 					let snapped_angle = if input.keyboard.key(modifier_keys.snap_angle) {
-						let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
+						let snap_resolution = ROTATE_INCREMENT.to_radians();
 						(angle / snap_resolution).round() * snap_resolution
 					} else {
 						angle
@@ -1194,24 +1194,27 @@ impl Fsm for SelectToolFsmState {
 				};
 
 				let current_selected: HashSet<_> = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect();
-				if new_selected != current_selected {
-					// Negative selection when both Shift and Ctrl are pressed
-					if input.keyboard.key(remove_from_selection) {
-						let updated_selection = current_selected
-							.into_iter()
-							.filter(|layer| !new_selected.iter().any(|selected| layer.starts_with(*selected, document.metadata())))
-							.collect();
-						tool_data.layers_dragging = updated_selection;
-					} else {
-						let parent_selected: HashSet<_> = new_selected
-							.into_iter()
-							.map(|layer| {
-								// Find the parent node
-								layer.ancestors(document.metadata()).filter(not_artboard(document)).last().unwrap_or(layer)
-							})
-							.collect();
-						tool_data.layers_dragging.extend(parent_selected.iter().copied());
-					}
+				let negative_selection = input.keyboard.key(remove_from_selection);
+				let selection_modified = new_selected != current_selected;
+				// Negative selection when both Shift and Ctrl are pressed
+				if negative_selection {
+					let updated_selection = current_selected
+						.into_iter()
+						.filter(|layer| !new_selected.iter().any(|selected| layer.starts_with(*selected, document.metadata())))
+						.collect();
+					tool_data.layers_dragging = updated_selection;
+				} else if selection_modified {
+					let parent_selected: HashSet<_> = new_selected
+						.into_iter()
+						.map(|layer| {
+							// Find the parent node
+							layer.ancestors(document.metadata()).filter(not_artboard(document)).last().unwrap_or(layer)
+						})
+						.collect();
+					tool_data.layers_dragging.extend(parent_selected.iter().copied());
+				}
+
+				if negative_selection || selection_modified {
 					responses.add(NodeGraphMessage::SelectedNodesSet {
 						nodes: tool_data
 							.layers_dragging
@@ -1354,7 +1357,25 @@ impl Fsm for SelectToolFsmState {
 				]);
 				responses.add(FrontendMessage::UpdateInputHints { hint_data });
 			}
-			_ => {}
+			SelectToolFsmState::Drawing { .. } | SelectToolFsmState::Dragging => {}
+			SelectToolFsmState::ResizingBounds => {
+				let hint_data = HintData(vec![
+					HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
+					HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]),
+				]);
+				responses.add(FrontendMessage::UpdateInputHints { hint_data });
+			}
+			SelectToolFsmState::RotatingBounds => {
+				let hint_data = HintData(vec![
+					HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
+					HintGroup(vec![HintInfo::keys([Key::Control], "Snap")]),
+				]);
+				responses.add(FrontendMessage::UpdateInputHints { hint_data });
+			}
+			SelectToolFsmState::DraggingPivot | SelectToolFsmState::SkewingBounds => {
+				let hint_data = HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]);
+				responses.add(FrontendMessage::UpdateInputHints { hint_data });
+			}
 		}
 	}
 
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 64c8590a5f..317b671587 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
@@ -27,7 +27,7 @@ pub struct TransformLayerMessageHandler {
 	slow: bool,
 	increments: bool,
 	local: bool,
-	fixed_bbox: Quad,
+	layer_bounding_box: Quad,
 	typing: Typing,
 
 	mouse_position: ViewportPosition,
@@ -79,18 +79,17 @@ fn calculate_pivot(selected_points: &Vec<&ManipulatorPointId>, vector_data: &Vec
 }
 
 fn project_edge_to_quad(edge: DVec2, quad: &Quad, local: bool, axis_constraint: Axis) -> DVec2 {
-	let quad = quad.0;
 	match axis_constraint {
 		Axis::X => {
 			if local {
-				edge.project_onto(quad[1] - quad[0])
+				edge.project_onto(quad.top_right() - quad.top_left())
 			} else {
 				edge.with_y(0.)
 			}
 		}
 		Axis::Y => {
 			if local {
-				edge.project_onto(quad[3] - quad[0])
+				edge.project_onto(quad.bottom_left() - quad.top_left())
 			} else {
 				edge.with_x(0.)
 			}
@@ -177,14 +176,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 					};
 
 					let viewport_box = input.viewport_bounds.size();
-					let axis_constraint = match self.transform_operation {
-						TransformOperation::Grabbing(grabbing) => grabbing.constraint,
-						TransformOperation::Scaling(scaling) => scaling.constraint,
-						_ => Axis::Both,
-					};
+					let axis_constraint = self.transform_operation.axis_constraint();
 
 					let format_rounded = |value: f64, precision: usize| {
-						if self.typing.digits.is_empty() {
+						if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
 							format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string()
 						} else {
 							self.typing.string.clone()
@@ -197,7 +192,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 							let translation = translation.to_dvec(document_to_viewport, 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 e1 = (self.fixed_bbox.0[1] - self.fixed_bbox.0[0]).normalize();
+							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. {
 								let end = if self.local {
@@ -221,7 +216,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 								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 };
-								if axis_constraint != Axis::Both || self.typing.digits.is_empty() {
+								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]);
 								}
 							}
@@ -231,16 +226,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 							}
 						}
 						TransformOperation::Scaling(scale) => {
-							let to_mouse_final = self.mouse_position - self.pivot;
-							let to_mouse_start = self.start_mouse - self.pivot;
-
-							let to_mouse_final = project_edge_to_quad(to_mouse_final, &self.fixed_bbox, self.local, axis_constraint);
-							let to_mouse_start = project_edge_to_quad(to_mouse_start, &self.fixed_bbox, self.local, axis_constraint);
-
-							let scale = scale.to_f64(self.increments) * to_mouse_final.dot(to_mouse_start).signum();
+							let scale = scale.to_f64(self.increments);
 							let text = format!("{}x", format_rounded(scale, 3));
 							let local_edge = self.start_mouse - self.pivot;
-							let local_edge = project_edge_to_quad(local_edge, &self.fixed_bbox, self.local, axis_constraint);
+							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.);
 
@@ -249,18 +238,17 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 							}
 							overlay_context.line(boundary_point, end_point, None);
 
-							let transform = DAffine2::from_translation(boundary_point.midpoint(self.pivot) + local_edge.perp().normalize() * local_edge.element_product().signum() * 24.);
+							let transform = DAffine2::from_translation(boundary_point.midpoint(self.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 quad = self.fixed_bbox.0;
 							let offset_angle = if self.grs_pen_handle {
 								self.handle - self.last_point
 							} else if using_path_tool {
 								self.start_mouse - self.pivot
 							} else {
-								quad[1] - quad[0]
+								self.layer_bounding_box.top_right() - self.layer_bounding_box.top_right()
 							};
 							let offset_angle = offset_angle.to_angle();
 							let width = viewport_box.max_element();
@@ -316,7 +304,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				let top_left = DVec2::new(last_point.x, handle.y);
 				let bottom_right = DVec2::new(handle.x, last_point.y);
 				self.local = false;
-				self.fixed_bbox = Quad::from_box([top_left, bottom_right]);
+				self.layer_bounding_box = Quad::from_box([top_left, bottom_right]);
 				self.grab_target = handle;
 				self.pivot = last_point;
 				self.handle = handle;
@@ -351,7 +339,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 
 				self.transform_operation = TransformOperation::Grabbing(Default::default());
 				self.local = false;
-				self.fixed_bbox = selected.bounding_box();
+				self.layer_bounding_box = selected.bounding_box();
 
 				selected.original_transforms.clear();
 
@@ -405,7 +393,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				self.transform_operation = TransformOperation::Rotating(Default::default());
 
 				self.local = false;
-				self.fixed_bbox = selected.bounding_box();
+				self.layer_bounding_box = selected.bounding_box();
 
 				selected.original_transforms.clear();
 
@@ -458,7 +446,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				self.transform_operation = TransformOperation::Scaling(Default::default());
 
 				self.local = false;
-				self.fixed_bbox = selected.bounding_box();
+				self.layer_bounding_box = selected.bounding_box();
 
 				selected.original_transforms.clear();
 
@@ -477,6 +465,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 					self.handle = DVec2::ZERO;
 
 					responses.add(PenToolMessage::Abort);
+					responses.add(ToolMessage::UpdateHints);
 				} else {
 					selected.revert_operation();
 					selected.original_transforms.clear();
@@ -492,12 +481,16 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 			TransformLayerMessage::ConstrainX => {
 				self.local = self
 					.transform_operation
-					.constrain_axis(Axis::X, &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport)
+					.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);
 			}
 			TransformLayerMessage::ConstrainY => {
 				self.local = self
 					.transform_operation
-					.constrain_axis(Axis::Y, &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport)
+					.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);
 			}
 			TransformLayerMessage::PointerMove { slow_key, increments_key } => {
 				self.slow = input.keyboard.get(slow_key as usize);
@@ -506,10 +499,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 				if new_increments != self.increments {
 					self.increments = new_increments;
 					self.transform_operation
-						.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
+						.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
 				}
 
-				if self.typing.digits.is_empty() {
+				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 {
@@ -518,7 +511,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 							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.fixed_bbox, document_to_viewport);
+								.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
 						}
 						TransformOperation::Rotating(rotation) => {
 							let start_offset = *selected.pivot - self.mouse_position;
@@ -529,33 +522,31 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 
 							self.transform_operation = TransformOperation::Rotating(rotation.increment_amount(change));
 							self.transform_operation
-								.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
+								.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
 						}
-						TransformOperation::Scaling(scale) => {
+						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 = project_edge_to_quad(to_mouse_final, &self.fixed_bbox, self.local, axis_constraint);
-							let to_mouse_final_old = project_edge_to_quad(to_mouse_final_old, &self.fixed_bbox, self.local, axis_constraint);
-							let to_mouse_start = project_edge_to_quad(to_mouse_start, &self.fixed_bbox, self.local, axis_constraint);
+							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 change = {
-								let previous_frame_dist = to_mouse_final.length();
-								let current_frame_dist = to_mouse_final_old.length();
-								let start_transform_dist = to_mouse_start.length();
+								let previous_frame_dist = to_mouse_final.dot(to_mouse_start);
+								let current_frame_dist = to_mouse_final_old.dot(to_mouse_start);
+								let start_transform_dist = to_mouse_start.length_squared();
 
 								(current_frame_dist - previous_frame_dist) / start_transform_dist
 							};
 							let change = if self.slow { change / SLOWING_DIVISOR } else { change };
 
-							let sign = to_mouse_final.dot(to_mouse_start).signum();
-							let scale = scale.increment_amount(change * scale.to_f64(self.increments).signum());
+							scale = scale.increment_amount(change);
 							self.transform_operation = TransformOperation::Scaling(scale);
-
-							let op = TransformOperation::Scaling(if sign > 0. { scale } else { scale.negate() });
-							op.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
+							self.transform_operation
+								.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
 						}
 					};
 				}
@@ -568,30 +559,44 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
 			}
 			TransformLayerMessage::TypeBackspace => {
 				if self.typing.digits.is_empty() && self.typing.negative {
-					self.transform_operation.negate(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
+					self.transform_operation
+						.negate(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
 					self.typing.type_negate();
 				}
 				self.transform_operation
-					.grs_typed(self.typing.type_backspace(), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
+					.grs_typed(self.typing.type_backspace(), &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
 			}
 			TransformLayerMessage::TypeDecimalPoint => {
-				if self.typing.digits.is_empty() {
-					self.typing.negative = false;
-				} else {
-					self.transform_operation
-						.grs_typed(self.typing.type_decimal_point(), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport)
+				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,
+					)
 				}
 			}
 			TransformLayerMessage::TypeDigit { digit } => {
-				self.transform_operation
-					.grs_typed(self.typing.type_number(digit), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport)
+				if self.transform_operation.can_begin_typing() {
+					self.transform_operation.grs_typed(
+						self.typing.type_number(digit),
+						&mut selected,
+						self.increments,
+						self.local,
+						self.layer_bounding_box,
+						document_to_viewport,
+					)
+				}
 			}
 			TransformLayerMessage::TypeNegate => {
 				if self.typing.digits.is_empty() {
-					self.transform_operation.negate(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
+					self.transform_operation
+						.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.fixed_bbox, document_to_viewport)
+					.grs_typed(self.typing.type_negate(), &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport)
 			}
 		}
 	}


Test patch: