diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 37e33b32..6e0a3f56 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -93,7 +93,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(Minus); action_dispatch=TransformLayerMessage::TypeNegate), entry!(KeyDown(Comma); action_dispatch=TransformLayerMessage::TypeDecimalPoint), entry!(KeyDown(Period); action_dispatch=TransformLayerMessage::TypeDecimalPoint), - entry!(PointerMove; refresh_keys=[Control, Shift], action_dispatch=TransformLayerMessage::PointerMove { slow_key: Shift, snap_key: Control }), + entry!(PointerMove; refresh_keys=[Control, Shift], action_dispatch=TransformLayerMessage::PointerMove { slow_key: Shift, increments_key: Control }), // // SelectToolMessage entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Control, center: Alt, duplicate: Alt })), diff --git a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs index 625c33d4..00242efd 100644 --- a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs +++ b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs @@ -295,6 +295,7 @@ impl fmt::Display for Key { Self::MouseMiddle => "MMB", Self::MouseBack => "Mouse Back", Self::MouseForward => "Mouse Fwd", + Self::NumKeys => "0–9", _ => key_name.as_str(), }; @@ -317,20 +318,6 @@ struct LayoutKey { key: String, label: String, } -/* -impl Serialize for Key { - fn serialize(&self, serializer: S) -> Result { - let key = format!("{:?}", self.0); - let label = self.0.to_string(); - - assert_eq!(serde_json::to_string(Key::KeyEscape), {"key": KeyEscape, "label": "Esc"}); - - let mut state = serializer.serialize_struct("KeyWithLabel", 2)?; - state.serialize_field("key", &key)?; - state.serialize_field("label", &label)?; - state.end() - } -}*/ pub const NUMBER_OF_KEYS: usize = Key::NumKeys as usize; diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index b540b928..315a91a9 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -143,8 +143,8 @@ pub struct Translation { } impl Translation { - pub fn to_dvec(self, transform: DAffine2) -> DVec2 { - if let Some(value) = self.typed_distance { + pub fn to_dvec(self, transform: DAffine2, snap: 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) } else { @@ -153,6 +153,12 @@ impl Translation { Axis::X => DVec2::new(self.dragged_distance.x, 0.), Axis::Y => DVec2::new(0., self.dragged_distance.y), } + }; + let displacement = transform.inverse().transform_vector2(displacement); + if snap { + displacement.round() + } else { + displacement } } @@ -172,6 +178,11 @@ impl Translation { } } + pub fn negate(self) -> Self { + let dragged_distance = -self.dragged_distance; + Self { dragged_distance, ..self } + } + pub fn with_constraint(self, target: Axis, local: bool) -> (Self, bool) { let (constraint, local) = self.constraint.contrainted_to_axis(target, local); (Self { constraint, ..self }, local) @@ -297,10 +308,11 @@ impl TransformOperation { if self != &TransformOperation::None { let transformation = match self { TransformOperation::Grabbing(translation) => { + let translate = DAffine2::from_translation(transform.transform_vector2(translation.to_dvec(transform, snapping))); if local { - DAffine2::from_angle(edge.to_angle()) * DAffine2::from_translation(translation.to_dvec(transform)) * DAffine2::from_angle(-edge.to_angle()) + DAffine2::from_angle(edge.to_angle()) * translate * DAffine2::from_angle(-edge.to_angle()) } else { - DAffine2::from_translation(translation.to_dvec(transform)) + translate } } TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(snapping)), @@ -315,7 +327,16 @@ impl TransformOperation { }; selected.update_transforms(transformation); - self.hints(selected.responses); + self.hints(selected.responses, local); + } + } + + 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(), } } @@ -346,21 +367,61 @@ impl TransformOperation { self.apply_transform_operation(selected, snapping, local, quad, transform); } - pub fn hints(&self, responses: &mut VecDeque) { + pub fn hints(&self, responses: &mut VecDeque, local: bool) { use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; let mut input_hints = Vec::new(); - input_hints.push(HintInfo::keys([Key::Shift], "Slow Mode")); - if matches!(self, TransformOperation::Rotating(_) | TransformOperation::Scaling(_)) { - input_hints.push(HintInfo::keys([Key::Control], "Snap")); - } - if matches!(self, TransformOperation::Grabbing(_) | TransformOperation::Scaling(_)) { - input_hints.push(HintInfo::keys([Key::KeyX], "Along X Axis")); - input_hints.push(HintInfo::keys([Key::KeyY], "Along Y Axis")); + 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)); + } + } + 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)); + } + } + } } - let hint_data = HintData(vec![HintGroup(input_hints)]); + let grs_hint_group = match self { + TransformOperation::None => unreachable!(), + TransformOperation::Scaling(_) => HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR]], "Grab/Rotate Selected")]), + TransformOperation::Grabbing(_) => HintGroup(vec![HintInfo::multi_keys([[Key::KeyR], [Key::KeyS]], "Rotate/Scale Selected")]), + TransformOperation::Rotating(_) => HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyS]], "Grab/Scale Selected")]), + }; + + let mut hint_groups = vec![grs_hint_group]; + if !self.is_typing() { + let modifiers = vec![HintInfo::keys([Key::Shift], "Slow"), HintInfo::keys([Key::Control], "Increments")]; + hint_groups.push(HintGroup(modifiers)); + } + hint_groups.push(HintGroup(input_hints)); + + let hint_data = HintData(hint_groups); responses.add(FrontendMessage::UpdateInputHints { hint_data }); } @@ -369,6 +430,7 @@ impl TransformOperation { *self = match self { TransformOperation::Scaling(scale) => TransformOperation::Scaling(scale.negate()), TransformOperation::Rotating(rotation) => TransformOperation::Rotating(rotation.negate()), + TransformOperation::Grabbing(translation) => TransformOperation::Grabbing(translation.negate()), _ => *self, }; self.apply_transform_operation(selected, snapping, local, quad, transform); @@ -580,6 +642,7 @@ impl<'a> Selected<'a> { #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Typing { pub digits: Vec, + pub string: String, pub contains_decimal: bool, pub negative: bool, } @@ -589,6 +652,7 @@ const DECIMAL_POINT: u8 = 10; impl Typing { pub fn type_number(&mut self, number: u8) -> Option { self.digits.push(number); + self.string.push((b'0' + number) as char); self.evaluate() } @@ -603,7 +667,7 @@ impl Typing { Some(_) => (), None => self.negative = false, } - + self.string.pop(); self.evaluate() } @@ -611,6 +675,7 @@ impl Typing { if !self.contains_decimal { self.contains_decimal = true; self.digits.push(DECIMAL_POINT); + self.string.push('.'); } self.evaluate() @@ -618,6 +683,11 @@ impl Typing { pub fn type_negate(&mut self) -> Option { self.negative = !self.negative; + if self.negative { + self.string.insert(0, '-'); + } else { + self.string.remove(0); + } self.evaluate() } @@ -653,6 +723,7 @@ impl Typing { pub fn clear(&mut self) { self.digits.clear(); + self.string.clear(); self.contains_decimal = false; self.negative = false; } diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index 20dba81a..3bc6d909 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -429,7 +429,7 @@ impl Fsm for BrushToolFsmState { let hint_data = match self { BrushToolFsmState::Ready => HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw")]), - HintGroup(vec![HintInfo::keys([Key::BracketLeft, Key::BracketRight], "Shrink/Grow Brush")]), + HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Shrink/Grow Brush")]), ]), BrushToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]), }; diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 029c5b1d..e0252218 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1055,7 +1055,7 @@ impl Fsm for PathToolFsmState { HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDouble, "Make Anchor Smooth/Sharp")]), // TODO: Only show the following hints if at least one point is selected HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]), - HintGroup(vec![HintInfo::keys([Key::KeyG, Key::KeyR, Key::KeyS], "Grab/Rotate/Scale Selected")]), + HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]), HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]), HintGroup(vec![ HintInfo::keys([Key::Delete], "Delete Selected"), diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 677acfae..c8fc8bf4 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1198,7 +1198,7 @@ impl Fsm for SelectToolFsmState { HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus(), ]), - HintGroup(vec![HintInfo::keys([Key::KeyG, Key::KeyR, Key::KeyS], "Grab/Rotate/Scale Selected")]), + HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]), HintGroup(vec![ HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus(), diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message.rs b/editor/src/messages/tool/transform_layer/transform_layer_message.rs index 6163bf56..864eeb17 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message.rs @@ -7,6 +7,9 @@ use glam::DVec2; #[impl_message(Message, ToolMessage, TransformLayer)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum TransformLayerMessage { + // Overlays + Overlays(OverlayContext), + // Messages ApplyTransformOperation, BeginGrab, @@ -18,8 +21,7 @@ pub enum TransformLayerMessage { CancelTransformOperation, ConstrainX, ConstrainY, - Overlays(OverlayContext), - PointerMove { slow_key: Key, snap_key: Key }, + PointerMove { slow_key: Key, increments_key: Key }, SelectionChanged, TypeBackspace, TypeDecimalPoint, 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 8f616006..64c8590a 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,4 +1,4 @@ -use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_LABEL_BACKGROUND, COLOR_OVERLAY_WHITE, SLOWING_DIVISOR}; +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::portfolio::document::overlays::utility_types::{OverlayProvider, Pivot}; use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing}; @@ -16,12 +16,16 @@ use std::f64::consts::TAU; const TRANSFORM_GRS_OVERLAY_PROVIDER: OverlayProvider = |context| TransformLayerMessage::Overlays(context).into(); +// TODO: Get these from the input mapper +const SLOW_KEY: Key = Key::Shift; +const INCREMENTS_KEY: Key = Key::Control; + #[derive(Debug, Clone, Default)] pub struct TransformLayerMessageHandler { pub transform_operation: TransformOperation, slow: bool, - snap: bool, + increments: bool, local: bool, fixed_bbox: Quad, typing: Typing, @@ -45,7 +49,7 @@ impl TransformLayerMessageHandler { } pub fn hints(&self, responses: &mut VecDeque) { - self.transform_operation.hints(responses); + self.transform_operation.hints(responses, self.local); } } @@ -74,6 +78,27 @@ 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]) + } else { + edge.with_y(0.) + } + } + Axis::Y => { + if local { + edge.project_onto(quad[3] - quad[0]) + } else { + edge.with_x(0.) + } + } + _ => edge, + } +} + type TransformData<'a> = (&'a DocumentMessageHandler, &'a InputPreprocessorMessageHandler, &'a ToolData, &'a mut ShapeState); impl MessageHandler> for TransformLayerMessageHandler { fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque, (document, input, tool_data, shape_editor): TransformData) { @@ -144,6 +169,121 @@ impl MessageHandler> for TransformLayer let document_to_viewport = document.metadata().document_to_viewport; match message { + // Overlays + TransformLayerMessage::Overlays(mut overlay_context) => { + for layer in document.metadata().all_layers() { + if !document.network_interface.is_artboard(&layer.to_node(), &[]) { + continue; + }; + + 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 format_rounded = |value: f64, precision: usize| { + if self.typing.digits.is_empty() { + format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string() + } else { + self.typing.string.clone() + } + }; + + match self.transform_operation { + TransformOperation::None => (), + TransformOperation::Grabbing(translation) => { + 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(); + + if matches!(axis_constraint, Axis::Both | Axis::X) && translation.x != 0. { + let end = if self.local { + (quad[1] - quad[0]).length() * e1 * e1.dot(quad[1] - quad[0]).signum() + quad[0] + } else { + quad[1] + }; + overlay_context.line(quad[0], end, None); + + let x_transform = DAffine2::from_translation((quad[0] + end) / 2.); + overlay_context.text(&format_rounded(translation.x, 3), COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]); + } + + if matches!(axis_constraint, Axis::Both | Axis::Y) && translation.y != 0. { + let end = if self.local { + (quad[3] - quad[0]).length() * e1.perp() * e1.perp().dot(quad[3] - quad[0]).signum() + quad[0] + } else { + quad[3] + }; + 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 }; + if axis_constraint != Axis::Both || self.typing.digits.is_empty() { + overlay_context.text(&format_rounded(translation.y, 2), COLOR_OVERLAY_BLUE, None, y_transform, 3., [pivot_selection, Pivot::Middle]); + } + } + if matches!(axis_constraint, Axis::Both) && translation.x != 0. && translation.y != 0. { + overlay_context.dashed_line(quad[1], quad[2], None, Some(2.), Some(2.), Some(0.5)); + overlay_context.dashed_line(quad[3], quad[2], None, Some(2.), Some(2.), Some(0.5)); + } + } + 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 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 boundary_point = self.pivot + local_edge * scale.min(1.); + let end_point = self.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.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.); + 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] + }; + let offset_angle = offset_angle.to_angle(); + let width = viewport_box.max_element(); + let radius = self.start_mouse.distance(self.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)); + let text_texture_width = overlay_context.get_width(&text) / 2.; + let text_texture_height = 12.; + let text_angle_on_unit_circle = DVec2::from_angle((angle % TAU) / 2. + offset_angle); + let text_texture_position = DVec2::new( + (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); + overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]); + } + } + } + } + + // Messages TransformLayerMessage::ApplyTransformOperation => { selected.original_transforms.clear(); self.typing.clear(); @@ -190,6 +330,11 @@ impl MessageHandler> for TransformLayer }; responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER)); + // Find a way better than this hack + responses.add(TransformLayerMessage::PointerMove { + slow_key: SLOW_KEY, + increments_key: INCREMENTS_KEY, + }); } TransformLayerMessage::BeginGrab => { if (!using_path_tool && !using_select_tool && !using_pen_tool) @@ -211,6 +356,10 @@ impl MessageHandler> for TransformLayer selected.original_transforms.clear(); responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER)); + responses.add(TransformLayerMessage::PointerMove { + slow_key: SLOW_KEY, + increments_key: INCREMENTS_KEY, + }); } TransformLayerMessage::BeginRotate => { let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect(); @@ -261,6 +410,10 @@ impl MessageHandler> for TransformLayer selected.original_transforms.clear(); responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER)); + responses.add(TransformLayerMessage::PointerMove { + slow_key: SLOW_KEY, + increments_key: INCREMENTS_KEY, + }); } TransformLayerMessage::BeginScale => { let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect(); @@ -310,6 +463,10 @@ impl MessageHandler> for TransformLayer selected.original_transforms.clear(); responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER)); + responses.add(TransformLayerMessage::PointerMove { + slow_key: SLOW_KEY, + increments_key: INCREMENTS_KEY, + }); } TransformLayerMessage::CancelTransformOperation => { if using_pen_tool { @@ -335,159 +492,21 @@ impl MessageHandler> for TransformLayer TransformLayerMessage::ConstrainX => { self.local = self .transform_operation - .constrain_axis(Axis::X, &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport) + .constrain_axis(Axis::X, &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport) } TransformLayerMessage::ConstrainY => { self.local = self .transform_operation - .constrain_axis(Axis::Y, &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport) + .constrain_axis(Axis::Y, &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport) } - TransformLayerMessage::Overlays(mut overlay_context) => { - for layer in document.metadata().all_layers() { - if !document.network_interface.is_artboard(&layer.to_node(), &[]) { - continue; - }; - - let viewport_box = input.viewport_bounds.size(); - let transform = DAffine2::from_translation(DVec2::new(0., viewport_box.y)) * DAffine2::from_scale(DVec2::splat(1.2)); - - let axis_constraint = match self.transform_operation { - TransformOperation::Grabbing(grabbing) => grabbing.constraint, - TransformOperation::Scaling(scaling) => scaling.constraint, - _ => Axis::Both, - }; - - let format_rounded = |value: f64, precision: usize| format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string(); - - let axis_text = |vector: DVec2, separate: bool| match (axis_constraint, separate) { - (Axis::Both, false) => format!("by {}", format_rounded(vector.x, 3)), - (Axis::Both, true) => format!("by ({}, {})", format_rounded(vector.x, 3), format_rounded(vector.y, 3)), - (Axis::X, _) => format!("X by {}", format_rounded(vector.x, 3)), - (Axis::Y, _) => format!("Y by {}", format_rounded(vector.y, 3)), - }; - - let grs_value_text = match self.transform_operation { - TransformOperation::None => String::new(), - TransformOperation::Grabbing(translation) => format!( - "Translating {}", - axis_text(document_to_viewport.inverse().transform_vector2(translation.to_dvec(document_to_viewport)), true) - ), - TransformOperation::Rotating(rotation) => format!("Rotating by {}°", format_rounded(rotation.to_f64(self.snap).to_degrees(), 3)), - TransformOperation::Scaling(scale) => format!("Scaling {}", axis_text(scale.to_dvec(self.snap), false)), - }; - - match self.transform_operation { - TransformOperation::None => (), - TransformOperation::Grabbing(translation) => { - let translation = document_to_viewport.inverse().transform_vector2(translation.to_dvec(document_to_viewport)); - let vec_to_end = self.mouse_position - self.start_mouse; - let quad = Quad::from_box([self.grab_target, self.grab_target + vec_to_end]).0; - let e1 = (self.fixed_bbox.0[1] - self.fixed_bbox.0[0]).normalize(); - - if matches!(axis_constraint, Axis::Both | Axis::X) { - let end = if self.local { - (quad[1] - quad[0]).length() * e1 * e1.dot(quad[1] - quad[0]).signum() + quad[0] - } else { - quad[1] - }; - overlay_context.line(quad[0], end, None); - - let x_transform = DAffine2::from_translation((quad[0] + end) / 2.); - overlay_context.text(&format_rounded(translation.x, 3), COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]); - } - - if matches!(axis_constraint, Axis::Both | Axis::Y) { - let end = if self.local { - (quad[3] - quad[0]).length() * e1.perp() * e1.perp().dot(quad[3] - quad[0]).signum() + quad[0] - } else { - quad[3] - }; - overlay_context.line(quad[0], end, None); - let x_parameter = vec_to_end.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 if x_parameter == 0. { - Pivot::Middle - } else { - Pivot::End - }; - overlay_context.text(&format_rounded(translation.y, 2), COLOR_OVERLAY_BLUE, None, y_transform, 3., [pivot_selection, Pivot::Middle]); - } - if matches!(axis_constraint, Axis::Both) { - overlay_context.dashed_line(quad[1], quad[2], None, Some(2.), Some(2.), Some(0.5)); - overlay_context.dashed_line(quad[3], quad[2], None, Some(2.), Some(2.), Some(0.5)); - } - } - TransformOperation::Scaling(scale) => { - let scale = scale.to_f64(self.snap); - let text = format!("{}x", format_rounded(scale, 3)); - let extension_vector = self.mouse_position - self.start_mouse; - let local_edge = self.start_mouse - self.pivot; - let quad = self.fixed_bbox.0; - let local_edge = match axis_constraint { - Axis::X => { - if self.local { - local_edge.project_onto(quad[1] - quad[0]) - } else { - local_edge.with_y(0.) - } - } - Axis::Y => { - if self.local { - local_edge.project_onto(quad[3] - quad[0]) - } else { - local_edge.with_x(0.) - } - } - _ => local_edge, - }; - let boundary_point = local_edge + self.pivot; - let projected_pointer = extension_vector.project_onto(local_edge); - let dashed_till = if extension_vector.dot(local_edge) < 0. { local_edge + projected_pointer } else { local_edge }; - let lined_till = projected_pointer + boundary_point; - if dashed_till.dot(local_edge) > 0. { - overlay_context.dashed_line(self.pivot, self.pivot + dashed_till, None, Some(4.), Some(4.), Some(0.5)); - } - overlay_context.line(boundary_point, lined_till, None); - - let transform = DAffine2::from_translation(boundary_point.midpoint(self.pivot) + local_edge.perp().normalize() * 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.snap); - let quad = self.fixed_bbox.0; - let offset_angle = if self.grs_pen_handle { self.handle - self.last_point } else { quad[1] - quad[0] }; - let offset_angle = offset_angle.to_angle(); - let width = viewport_box.max_element(); - let radius = self.start_mouse.distance(self.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)); - let text_texture_width = overlay_context.get_width(&text) / 2.; - let text_texture_height = 12.; - let text_angle_on_unit_circle = DVec2::from_angle((angle % TAU) / 2. + offset_angle); - let text_texture_position = DVec2::new( - (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); - overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]); - } - } - - overlay_context.text(&grs_value_text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_LABEL_BACKGROUND), transform, 4., [Pivot::Start, Pivot::End]); - } - } - TransformLayerMessage::PointerMove { slow_key, snap_key } => { + TransformLayerMessage::PointerMove { slow_key, increments_key } => { self.slow = input.keyboard.get(slow_key as usize); - let new_snap = input.keyboard.get(snap_key as usize); - if new_snap != self.snap { - self.snap = new_snap; + 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.snap, self.local, self.fixed_bbox, document_to_viewport); + .apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport); } if self.typing.digits.is_empty() { @@ -499,7 +518,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.snap, self.local, self.fixed_bbox, document_to_viewport); + .apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport); } TransformOperation::Rotating(rotation) => { let start_offset = *selected.pivot - self.mouse_position; @@ -510,22 +529,33 @@ impl MessageHandler> for TransformLayer self.transform_operation = TransformOperation::Rotating(rotation.increment_amount(change)); self.transform_operation - .apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport); + .apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport); } TransformOperation::Scaling(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 change = { - let previous_frame_dist = (self.mouse_position - *selected.pivot).length(); - let current_frame_dist = (input.mouse.position - *selected.pivot).length(); - let start_transform_dist = (self.start_mouse - *selected.pivot).length(); + 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(); (current_frame_dist - previous_frame_dist) / start_transform_dist }; let change = if self.slow { change / SLOWING_DIVISOR } else { change }; - self.transform_operation = TransformOperation::Scaling(scale.increment_amount(change)); + let sign = to_mouse_final.dot(to_mouse_start).signum(); + let scale = scale.increment_amount(change * scale.to_f64(self.increments).signum()); + self.transform_operation = TransformOperation::Scaling(scale); - self.transform_operation - .apply_transform_operation(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport); + 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); } }; } @@ -536,23 +566,32 @@ impl MessageHandler> for TransformLayer let target_layers = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect(); shape_editor.set_selected_layers(target_layers); } - TransformLayerMessage::TypeBackspace => self - .transform_operation - .grs_typed(self.typing.type_backspace(), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport), - TransformLayerMessage::TypeDecimalPoint => { + 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.typing.type_negate(); + } self.transform_operation - .grs_typed(self.typing.type_decimal_point(), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport) + .grs_typed(self.typing.type_backspace(), &mut selected, self.increments, self.local, self.fixed_bbox, 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) + } } TransformLayerMessage::TypeDigit { digit } => { self.transform_operation - .grs_typed(self.typing.type_number(digit), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport) + .grs_typed(self.typing.type_number(digit), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport) } TransformLayerMessage::TypeNegate => { if self.typing.digits.is_empty() { - self.transform_operation.negate(&mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport); + self.transform_operation.negate(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport); } self.transform_operation - .grs_typed(self.typing.type_negate(), &mut selected, self.snap, self.local, self.fixed_bbox, document_to_viewport) + .grs_typed(self.typing.type_negate(), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport) } } } diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index 37d3e219..7d4567d7 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -482,8 +482,12 @@ pub struct HintInfo { } impl HintInfo { + /// Used for a hint where a single key or key stroke is used to perform one action. + /// Examples: + /// - The Escape key can be used to cancel an action + /// - The Ctrl+C key stroke can be used to copy pub fn keys(keys: impl IntoIterator, label: impl Into>) -> Self { - let keys: Vec<_> = keys.into_iter().collect(); + let keys = keys.into_iter().collect(); Self { key_groups: vec![KeysGroup(keys).into()], key_groups_mac: None, @@ -494,6 +498,10 @@ impl HintInfo { } } + /// Used for a hint where multiple different individual keys can be used to perform variations of the same action. These keys are represented with a slight separation between them compared to [`Self::keys`]. + /// Examples: + /// - The four arrow keys can be used to nudge a layer in different directions + /// - The G, R, and S keys can be used to enter GRS transformation mode pub fn multi_keys(multi_keys: impl IntoIterator>, label: impl Into>) -> Self { let key_groups = multi_keys.into_iter().map(|keys| KeysGroup(keys.into_iter().collect()).into()).collect(); Self { @@ -529,7 +537,7 @@ impl HintInfo { } pub fn keys_and_mouse(keys: impl IntoIterator, mouse_motion: MouseMotion, label: impl Into>) -> Self { - let keys: Vec<_> = keys.into_iter().collect(); + let keys = keys.into_iter().collect(); Self { key_groups: vec![KeysGroup(keys).into()], key_groups_mac: None, @@ -568,7 +576,7 @@ impl HintInfo { } pub fn add_mac_keys(mut self, keys: impl IntoIterator) -> Self { - let mac_keys: Vec<_> = keys.into_iter().collect(); + let mac_keys = keys.into_iter().collect(); self.key_groups_mac = Some(vec![KeysGroup(mac_keys).into()]); self }