From 4de65c292a60e2e8dff886568f8a8eccc5ae055e Mon Sep 17 00:00:00 2001 From: mTvare Date: Wed, 5 Feb 2025 16:27:47 +0530 Subject: [PATCH] Further polishing of G/R/S visualization and features (#2243) * Further polishing of G/R/S visualisation and features Followup to #2229. * Begin typing only if constrained or not in G * Prevent adding empty group in R mode. Order fn alphabetically as was before * Always show typing hints unless can't begin typing * Fix one frame bug * Add cancel and confirm groups for GRS hints * Fix inconsistency in call increments, snaps * Use top/bottom left/right methods with quads where more readable * Fix inconsistent use of narrow/flat * Add hints to transform cage Fixes https://discord.com/channels/731730685944922173/881073965047636018/939265895509925898. * Rename some hints * Fix scale radial behaviour, grab constraints and local edge orientation * Fix not being able to remove the whole selection with delete modifier Fixes https://discord.com/channels/731730685944922173/881073965047636018/1336221441716391937. * Fix compiling * Fix crash when single point bbox Fixes #2267 * Fix the same crash in scale and use better name for bbox * cargo fmt --------- Co-authored-by: Keavon Chambers --- editor/src/consts.rs | 4 +- .../document/overlays/utility_types.rs | 2 +- .../document/utility_types/transformation.rs | 175 +++++++++++------- .../transformation_cage.rs | 4 +- .../tool/tool_messages/select_tool.rs | 63 ++++--- .../transform_layer_message_handler.rs | 117 ++++++------ 6 files changed, 211 insertions(+), 154 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index ec247089..e4bb2cc1 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 7c1f7bf1..6773f135 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, dash_gap_width: Option, dash_offset: Option) { diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index 315a91a9..69d4fee9 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, selected: &mut Selected, snapping: bool, local: bool, quad: Quad, transform: DAffine2) { + pub fn grs_typed(&mut self, typed: Option, 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, 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")); + 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::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)); - } - } - 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 42d932ef..811b1c2d 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 40e6d329..835ace5c 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 64c8590a..317b6715 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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) } } }