diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 499703b2be..042d9eeeb1 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -2,7 +2,7 @@ name = "graphite-editor" publish = false version = "0.0.0" -rust-version = "1.79" +rust-version = "1.82" authors = ["Graphite Authors "] edition = "2021" readme = "../README.md" diff --git a/editor/src/consts.rs b/editor/src/consts.rs index e4bb2cc126..c10a8d8615 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -54,10 +54,20 @@ pub const DEFAULT_STROKE_WIDTH: f64 = 2.; pub const SELECTION_TOLERANCE: f64 = 5.; pub const DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD: f64 = 15.; pub const SELECTION_DRAG_ANGLE: f64 = 90.; + +// PIVOT pub const PIVOT_CROSSHAIR_THICKNESS: f64 = 1.; pub const PIVOT_CROSSHAIR_LENGTH: f64 = 9.; pub const PIVOT_DIAMETER: f64 = 5.; +// COMPASS ROSE +pub const COMPASS_ROSE_RING_INNER_DIAMETER: f64 = 13.; +pub const COMPASS_ROSE_MAIN_RING_DIAMETER: f64 = 15.; +pub const COMPASS_ROSE_HOVER_RING_DIAMETER: f64 = 23.; +pub const COMPASS_ROSE_ARROW_SIZE: f64 = 5.; +// Angle to either side of the compass arrows where they are targetted by the cursor (in degrees, must be less than 45°) +pub const COMPASS_ROSE_ARROW_CLICK_TARGET_ANGLE: f64 = 20.; + // TRANSFORM OVERLAY pub const ANGLE_MEASURE_RADIUS_FACTOR: f64 = 0.04; pub const ARC_MEASURE_RADIUS_FACTOR_RANGE: (f64, f64) = (0.05, 0.15); @@ -108,7 +118,6 @@ pub const COLOR_OVERLAY_RED: &str = "#ef5454"; pub const COLOR_OVERLAY_GRAY: &str = "#cccccc"; pub const COLOR_OVERLAY_WHITE: &str = "#ffffff"; pub const COLOR_OVERLAY_LABEL_BACKGROUND: &str = "#000000cc"; -pub const COLOR_OVERLAY_TRANSPARENT: &str = "#ffffff00"; // DOCUMENT pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index a16eda48c4..125a1ed091 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -1,6 +1,7 @@ use super::utility_functions::overlay_canvas_context; use crate::consts::{ - COLOR_OVERLAY_BLUE, COLOR_OVERLAY_TRANSPARENT, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, + COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, + COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, }; use crate::messages::prelude::Message; @@ -9,7 +10,7 @@ use graphene_core::renderer::Quad; use graphene_std::vector::{PointId, SegmentId, VectorData}; use core::borrow::Borrow; -use core::f64::consts::TAU; +use core::f64::consts::{FRAC_PI_2, TAU}; use glam::{DAffine2, DVec2}; use std::collections::HashMap; use wasm_bindgen::JsValue; @@ -294,9 +295,15 @@ impl OverlayContext { pub fn draw_scale(&mut self, start: DVec2, scale: f64, radius: f64, text: &str) { let sign = scale.signum(); + let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap()) + .unwrap() + .with_alpha(0.05) + .rgba_hex(); + fill_color.insert(0, '#'); + let fill_color = Some(fill_color.as_str()); self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None); - self.circle(start, radius, Some(COLOR_OVERLAY_TRANSPARENT), None); - self.circle(start, radius * scale.abs(), Some(COLOR_OVERLAY_TRANSPARENT), None); + self.circle(start, radius, fill_color, None); + self.circle(start, radius * scale.abs(), fill_color, None); self.text( text, COLOR_OVERLAY_BLUE, @@ -307,7 +314,77 @@ impl OverlayContext { ) } - pub fn pivot(&mut self, position: DVec2) { + pub fn compass_rose(&mut self, compass_center: DVec2, angle: f64, show_compass_with_hover_ring: Option) { + const HOVER_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_HOVER_RING_DIAMETER / 2.; + const MAIN_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_MAIN_RING_DIAMETER / 2.; + const MAIN_RING_INNER_RADIUS: f64 = COMPASS_ROSE_RING_INNER_DIAMETER / 2.; + const ARROW_RADIUS: f64 = COMPASS_ROSE_ARROW_SIZE / 2.; + const HOVER_RING_STROKE_WIDTH: f64 = HOVER_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS; + const HOVER_RING_CENTERLINE_RADIUS: f64 = (HOVER_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.; + const MAIN_RING_STROKE_WIDTH: f64 = MAIN_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS; + const MAIN_RING_CENTERLINE_RADIUS: f64 = (MAIN_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.; + + let Some(show_hover_ring) = show_compass_with_hover_ring else { return }; + + self.start_dpi_aware_transform(); + + let center = compass_center.round() - DVec2::splat(0.5); + + // Save the old line width to restore it later + let old_line_width = self.render_context.line_width(); + + // Hover ring + if show_hover_ring { + let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.5).rgba_hex(); + fill_color.insert(0, '#'); + + self.render_context.set_line_width(HOVER_RING_STROKE_WIDTH); + self.render_context.begin_path(); + self.render_context.arc(center.x, center.y, HOVER_RING_CENTERLINE_RADIUS, 0., TAU).expect("Failed to draw hover ring"); + self.render_context.set_stroke_style_str(&fill_color); + self.render_context.stroke(); + } + + // Arrows + self.render_context.set_line_width(0.01); + for i in 0..4 { + let direction = DVec2::from_angle(i as f64 * FRAC_PI_2 + angle); + let color = if i % 2 == 0 { COLOR_OVERLAY_RED } else { COLOR_OVERLAY_GREEN }; + + let tip = center + direction * HOVER_RING_OUTER_RADIUS; + let base = center + direction * (MAIN_RING_INNER_RADIUS + MAIN_RING_OUTER_RADIUS) / 2.; + + let r = (ARROW_RADIUS.powi(2) + MAIN_RING_INNER_RADIUS.powi(2)).sqrt(); + let (cos, sin) = (MAIN_RING_INNER_RADIUS / r, ARROW_RADIUS / r); + let side1 = center + r * DVec2::new(cos * direction.x - sin * direction.y, sin * direction.x + direction.y * cos); + let side2 = center + r * DVec2::new(cos * direction.x + sin * direction.y, -sin * direction.x + direction.y * cos); + + self.render_context.begin_path(); + self.render_context.move_to(tip.x, tip.y); + self.render_context.line_to(side1.x, side1.y); + self.render_context.line_to(base.x, base.y); + self.render_context.line_to(side2.x, side2.y); + self.render_context.close_path(); + + self.render_context.set_fill_style_str(color); + self.render_context.fill(); + self.render_context.set_stroke_style_str(color); + self.render_context.stroke(); + } + + // Main ring + self.render_context.set_line_width(MAIN_RING_STROKE_WIDTH); + self.render_context.begin_path(); + self.render_context.arc(center.x, center.y, MAIN_RING_CENTERLINE_RADIUS, 0., TAU).expect("Failed to draw main ring"); + self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); + self.render_context.stroke(); + + // Restore the old line width + self.render_context.set_line_width(old_line_width); + } + + pub fn pivot(&mut self, position: DVec2, angle: f64) { + let uv = DVec2::from_angle(angle); let (x, y) = (position.round() - DVec2::splat(0.5)).into(); self.start_dpi_aware_transform(); @@ -322,19 +399,19 @@ impl OverlayContext { // Crosshair // Round line caps add half the stroke width to the length on each end, so we subtract that here before halving to get the radius - let crosshair_radius = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.; + const CROSSHAIR_RADIUS: f64 = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.; self.render_context.set_stroke_style_str(COLOR_OVERLAY_YELLOW); self.render_context.set_line_cap("round"); self.render_context.begin_path(); - self.render_context.move_to(x - crosshair_radius, y); - self.render_context.line_to(x + crosshair_radius, y); + self.render_context.move_to(x + CROSSHAIR_RADIUS * uv.x, y + CROSSHAIR_RADIUS * uv.y); + self.render_context.line_to(x - CROSSHAIR_RADIUS * uv.x, y - CROSSHAIR_RADIUS * uv.y); self.render_context.stroke(); self.render_context.begin_path(); - self.render_context.move_to(x, y - crosshair_radius); - self.render_context.line_to(x, y + crosshair_radius); + self.render_context.move_to(x - CROSSHAIR_RADIUS * uv.y, y + CROSSHAIR_RADIUS * uv.x); + self.render_context.line_to(x + CROSSHAIR_RADIUS * uv.y, y - CROSSHAIR_RADIUS * uv.x); self.render_context.stroke(); self.render_context.set_line_cap("butt"); diff --git a/editor/src/messages/tool/common_functionality/compass_rose.rs b/editor/src/messages/tool/common_functionality/compass_rose.rs new file mode 100644 index 0000000000..be7af7ca79 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/compass_rose.rs @@ -0,0 +1,85 @@ +use crate::consts::{COMPASS_ROSE_ARROW_CLICK_TARGET_ANGLE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER}; +use crate::messages::prelude::DocumentMessageHandler; + +use glam::{DAffine2, DVec2}; +use std::f64::consts::FRAC_PI_2; + +#[derive(Clone, Default, Debug)] +pub struct CompassRose { + compass_center: DVec2, +} + +impl CompassRose { + pub fn refresh_transform(&mut self, document: &DocumentMessageHandler) { + let [min, max] = document.selected_visible_and_unlock_layers_bounding_box_viewport().unwrap_or([DVec2::ZERO, DVec2::ONE]); + self.compass_center = (DAffine2::from_translation(min) * DAffine2::from_scale(max - min)).transform_point2(DVec2::splat(0.5)); + } + + pub fn compass_rose_position(&self) -> DVec2 { + self.compass_center + } + + pub fn compass_rose_state(&self, mouse: DVec2, angle: f64) -> CompassRoseState { + const COMPASS_ROSE_RING_INNER_RADIUS_SQUARED: f64 = (COMPASS_ROSE_RING_INNER_DIAMETER / 2.) * (COMPASS_ROSE_RING_INNER_DIAMETER / 2.); + const COMPASS_ROSE_HOVER_RING_RADIUS_SQUARED: f64 = (COMPASS_ROSE_HOVER_RING_DIAMETER / 2.) * (COMPASS_ROSE_HOVER_RING_DIAMETER / 2.); + + let compass_distance_squared = mouse.distance_squared(self.compass_center); + + if !(COMPASS_ROSE_RING_INNER_RADIUS_SQUARED..COMPASS_ROSE_HOVER_RING_RADIUS_SQUARED).contains(&compass_distance_squared) { + return CompassRoseState::None; + } + + let angle = (mouse - self.compass_center).angle_to(DVec2::from_angle(angle)).abs(); + let resolved_angle = (FRAC_PI_2 - angle).abs(); + let angular_width = COMPASS_ROSE_ARROW_CLICK_TARGET_ANGLE.to_radians(); + + if resolved_angle < angular_width { + CompassRoseState::AxisY + } else if resolved_angle > (FRAC_PI_2 - angular_width) { + CompassRoseState::AxisX + } else { + CompassRoseState::Ring + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum CompassRoseState { + Ring, + AxisX, + AxisY, + None, +} + +impl CompassRoseState { + pub fn can_grab(&self) -> bool { + matches!(self, Self::Ring | Self::AxisX | Self::AxisY) + } + + pub fn is_ring(&self) -> bool { + matches!(self, Self::Ring) + } + + pub fn axis_type(&self) -> Option { + match self { + CompassRoseState::AxisX => Some(Axis::X), + CompassRoseState::AxisY => Some(Axis::Y), + CompassRoseState::Ring => Some(Axis::None), + _ => None, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] +pub enum Axis { + #[default] + None, + X, + Y, +} + +impl Axis { + pub fn is_constraint(&self) -> bool { + matches!(self, Self::X | Self::Y) + } +} diff --git a/editor/src/messages/tool/common_functionality/mod.rs b/editor/src/messages/tool/common_functionality/mod.rs index e3e273fa2e..9bc2236061 100644 --- a/editor/src/messages/tool/common_functionality/mod.rs +++ b/editor/src/messages/tool/common_functionality/mod.rs @@ -1,5 +1,6 @@ pub mod auto_panning; pub mod color_selector; +pub mod compass_rose; pub mod graph_modification_utils; pub mod measure; pub mod pivot; diff --git a/editor/src/messages/tool/common_functionality/pivot.rs b/editor/src/messages/tool/common_functionality/pivot.rs index c56f12127e..2c924602fb 100644 --- a/editor/src/messages/tool/common_functionality/pivot.rs +++ b/editor/src/messages/tool/common_functionality/pivot.rs @@ -83,10 +83,10 @@ impl Pivot { } } - pub fn update_pivot(&mut self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { + pub fn update_pivot(&mut self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, angle: f64) { self.recalculate_pivot(document); if let Some(pivot) = self.pivot { - overlay_context.pivot(pivot); + overlay_context.pivot(pivot, angle); } } diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index 811b1c2df4..da129c9b9a 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -1,6 +1,6 @@ use crate::consts::{ - BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY, MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, - SELECTION_DRAG_ANGLE, + BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_OVERLAY_WHITE, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY, MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, + MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, SELECTION_DRAG_ANGLE, }; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; @@ -379,7 +379,10 @@ impl BoundingBoxManager { // Draw the bounding box rectangle overlay_context.quad(quad, None); - let mut draw_handle = |point: DVec2| overlay_context.square(point, Some(6.), None, None); + let mut draw_handle = |point: DVec2| { + let quad = DAffine2::from_angle_translation((quad.top_left() - quad.top_right()).to_angle(), point) * Quad::from_box([DVec2::splat(-3.), DVec2::splat(3.)]); + overlay_context.quad(quad, Some(COLOR_OVERLAY_WHITE)); + }; // Draw the horizontal midpoint drag handles if matches!(category, HandleDisplayCategory::Full | HandleDisplayCategory::Narrow | HandleDisplayCategory::ReducedLandscape) { diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 3c95b59f02..a2befd59c1 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_INCREMENT, SELECTION_TOLERANCE}; +use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, ROTATE_INCREMENT, SELECTION_DRAG_ANGLE, 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; @@ -11,6 +11,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Flo use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::preferences::SelectionMode; +use crate::messages::tool::common_functionality::compass_rose::{Axis, CompassRose}; use crate::messages::tool::common_functionality::graph_modification_utils::{get_text, is_layer_fed_by_node_of_name}; use crate::messages::tool::common_functionality::pivot::Pivot; use crate::messages::tool::common_functionality::shape_editor::SelectionShapeType; @@ -274,7 +275,7 @@ impl ToolTransition for SelectTool { enum SelectToolFsmState { Ready { selection: NestedSelectionBehavior }, Drawing { selection_shape: SelectionShapeType }, - Dragging, + Dragging { axis: Axis, using_compass: bool }, ResizingBounds, SkewingBounds, RotatingBounds, @@ -298,11 +299,13 @@ struct SelectToolData { layer_selected_on_start: Option, select_single_layer: Option, has_dragged: bool, + axis_align: bool, non_duplicated_layers: Option>, bounding_box_manager: Option, snap_manager: SnapManager, cursor: MouseCursorIcon, pivot: Pivot, + compass_rose: CompassRose, nested_selection_behavior: NestedSelectionBehavior, selected_layers_count: usize, selected_layers_changed: bool, @@ -543,8 +546,106 @@ impl Fsm for SelectToolFsmState { tool_data.bounding_box_manager.take(); } + let angle = bounds + .map(|bounds| transform * Quad::from_box(bounds)) + .map_or(0., |quad| (quad.top_left() - quad.top_right()).to_angle()); + + let mouse_position = input.mouse.position; + let compass_rose_state = tool_data.compass_rose.compass_rose_state(mouse_position, angle); + + let show_hover_ring = if let SelectToolFsmState::Dragging { axis, using_compass } = self { + using_compass && !axis.is_constraint() + } else { + compass_rose_state.is_ring() + }; + + let dragging_bounds = tool_data + .bounding_box_manager + .as_mut() + .and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position)) + .is_some(); + + let rotating_bounds = tool_data + .bounding_box_manager + .as_ref() + .map(|bounding_box| bounding_box.check_rotate(input.mouse.position)) + .unwrap_or_default(); + + let might_resize_or_rotate = dragging_bounds || rotating_bounds; + let is_resizing_or_rotating = matches!(self, SelectToolFsmState::ResizingBounds { .. } | SelectToolFsmState::SkewingBounds | SelectToolFsmState::RotatingBounds); + let can_get_into_other_states = might_resize_or_rotate && !matches!(self, SelectToolFsmState::Dragging { .. }); + + let show_compass = !(can_get_into_other_states || is_resizing_or_rotating); + let show_compass_with_ring = bounds.map(|bounds| transform * Quad::from_box(bounds)).and_then(|quad| { + show_compass + .then_some( + matches!(self, SelectToolFsmState::Dragging { .. }) + .then_some(show_hover_ring) + .or(quad.contains(mouse_position).then_some(show_hover_ring)), + ) + .flatten() + }); + // Update pivot - tool_data.pivot.update_pivot(document, &mut overlay_context); + tool_data.pivot.update_pivot(document, &mut overlay_context, angle); + + // Update compass rose + tool_data.compass_rose.refresh_transform(document); + let compass_center = tool_data.compass_rose.compass_rose_position(); + overlay_context.compass_rose(compass_center, angle, show_compass_with_ring); + + let axis_state = if let SelectToolFsmState::Dragging { axis, .. } = self { + Some((axis, false)) + } else { + compass_rose_state.axis_type().and_then(|axis| axis.is_constraint().then_some((axis, true))) + }; + + if let Some((axis, hover)) = axis_state { + if axis.is_constraint() { + let e0 = tool_data + .bounding_box_manager + .as_ref() + .map(|man| man.transform * Quad::from_box(man.bounds)) + .map_or(DVec2::X, |quad| (quad.top_left() - quad.top_right()).normalize_or(DVec2::X)); + + let (direction, color) = match axis { + Axis::X => (e0, COLOR_OVERLAY_RED), + Axis::Y => (e0.perp(), COLOR_OVERLAY_GREEN), + _ => unreachable!(), + }; + + let viewport_diagonal = input.viewport_bounds.size().length(); + + let color = if !hover { + color + } else { + let color_string = &graphene_std::Color::from_rgb_str(color.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).rgba_hex(); + &format!("#{}", color_string) + }; + overlay_context.line(compass_center - direction * viewport_diagonal, compass_center + direction * viewport_diagonal, Some(color)); + } + } + + if axis_state.is_none_or(|(axis, _)| !axis.is_constraint()) && tool_data.axis_align { + let mouse_position = mouse_position - tool_data.drag_start; + let snap_resolution = SELECTION_DRAG_ANGLE.to_radians(); + let angle = -mouse_position.angle_to(DVec2::X); + let snapped_angle = (angle / snap_resolution).round() * snap_resolution; + + let mut other = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).rgba_hex(); + other.insert(0, '#'); + let other = other.as_str(); + + let extension = tool_data.drag_current - tool_data.drag_start; + let origin = compass_center - extension; + let viewport_diagonal = input.viewport_bounds.size().length(); + + let edge = DVec2::from_angle(snapped_angle) * viewport_diagonal; + let perp = edge.perp(); + + overlay_context.line(origin - edge * viewport_diagonal, origin + edge * viewport_diagonal, Some(COLOR_OVERLAY_BLUE)); + overlay_context.line(origin - perp * viewport_diagonal, origin + perp * viewport_diagonal, Some(other)); + } // Check if the tool is in selection mode if let Self::Drawing { selection_shape } = self { @@ -676,9 +777,18 @@ impl Fsm for SelectToolFsmState { // If the user clicks on new shape, make that layer their new selection. // Otherwise enter the box select mode + let angle = tool_data + .bounding_box_manager + .as_ref() + .map(|man| man.transform * Quad::from_box(man.bounds)) + .map_or(0., |quad| (quad.top_left() - quad.top_right()).to_angle()); + let mouse_position = input.mouse.position; + let compass_rose_state = tool_data.compass_rose.compass_rose_state(mouse_position, angle); + let is_over_pivot = tool_data.pivot.is_over(mouse_position); + let state = // Dragging the pivot - if tool_data.pivot.is_over(input.mouse.position) { + if is_over_pivot { responses.add(DocumentMessage::StartTransaction); // tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true); @@ -687,7 +797,7 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::DraggingPivot } // Dragging one (or two, forming a corner) of the transform cage bounding box edges - else if let Some(_selected_edges) = dragging_bounds { + else if dragging_bounds.is_some() { responses.add(DocumentMessage::StartTransaction); tool_data.layers_dragging = selected; @@ -756,7 +866,7 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::RotatingBounds } // Dragging the selected layers around to transform them - else if intersection.is_some_and(|intersection| selected.iter().any(|selected_layer| intersection.starts_with(*selected_layer, document.metadata()))) { + else if compass_rose_state.can_grab() || intersection.is_some_and(|intersection| selected.iter().any(|selected_layer| intersection.starts_with(*selected_layer, document.metadata()))) { responses.add(DocumentMessage::StartTransaction); if input.keyboard.key(select_deepest) || tool_data.nested_selection_behavior == NestedSelectionBehavior::Deepest { @@ -768,8 +878,11 @@ impl Fsm for SelectToolFsmState { tool_data.layers_dragging = selected; tool_data.get_snap_candidates(document, input); - - SelectToolFsmState::Dragging + let axis = compass_rose_state.axis_type(); + match axis { + Some(axis) => SelectToolFsmState::Dragging { axis, using_compass: true }, + None => SelectToolFsmState::Dragging { axis: Axis::None, using_compass: false } + } } // Dragging a selection box else { @@ -790,7 +903,7 @@ impl Fsm for SelectToolFsmState { tool_data.get_snap_candidates(document, input); responses.add(DocumentMessage::StartTransaction); - SelectToolFsmState::Dragging + SelectToolFsmState::Dragging { axis: Axis::None, using_compass: false } } else { let selection_shape = if input.keyboard.key(lasso_select) { SelectionShapeType::Lasso } else { SelectionShapeType::Box }; SelectToolFsmState::Drawing { selection_shape } @@ -806,7 +919,7 @@ impl Fsm for SelectToolFsmState { let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } - (SelectToolFsmState::Dragging, SelectToolMessage::PointerMove(modifier_keys)) => { + (SelectToolFsmState::Dragging { axis, using_compass }, SelectToolMessage::PointerMove(modifier_keys)) => { tool_data.has_dragged = true; if input.keyboard.key(modifier_keys.duplicate) && tool_data.non_duplicated_layers.is_none() { @@ -815,7 +928,7 @@ impl Fsm for SelectToolFsmState { tool_data.stop_duplicates(document, responses); } - let axis_align = input.keyboard.key(modifier_keys.axis_align); + tool_data.axis_align = input.keyboard.key(modifier_keys.axis_align) && !axis.is_constraint(); // Ignore the non duplicated layers if the current layers have not spawned yet. let layers_exist = tool_data.layers_dragging.iter().all(|&layer| document.metadata().click_targets(layer).is_some()); @@ -823,7 +936,17 @@ impl Fsm for SelectToolFsmState { let snap_data = SnapData::ignore(document, input, ignore); let (start, current) = (tool_data.drag_start, tool_data.drag_current); - let mouse_delta = snap_drag(start, current, axis_align, snap_data, &mut tool_data.snap_manager, &tool_data.snap_candidates); + let mouse_delta = snap_drag(start, current, tool_data.axis_align, snap_data, &mut tool_data.snap_manager, &tool_data.snap_candidates); + let e0 = tool_data + .bounding_box_manager + .as_ref() + .map(|man| man.transform * Quad::from_box(man.bounds)) + .map_or(DVec2::X, |quad| (quad.top_left() - quad.top_right()).normalize_or(DVec2::X)); + let mouse_delta = match axis { + Axis::X => mouse_delta.project_onto(e0), + Axis::Y => mouse_delta.project_onto(e0.perp()), + Axis::None => mouse_delta, + }; // TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481 for layer in document.network_interface.shallowest_unique_layers(&[]) { @@ -843,7 +966,7 @@ impl Fsm for SelectToolFsmState { ]; tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); - SelectToolFsmState::Dragging + SelectToolFsmState::Dragging { axis, using_compass } } (SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove(modifier_keys)) => { if let Some(ref mut bounds) = &mut tool_data.bounding_box_manager { @@ -1017,14 +1140,14 @@ impl Fsm for SelectToolFsmState { let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } - (SelectToolFsmState::Dragging, SelectToolMessage::PointerOutsideViewport(_)) => { + (SelectToolFsmState::Dragging { axis, using_compass }, SelectToolMessage::PointerOutsideViewport(_)) => { // AutoPanning if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { tool_data.drag_current += shift; tool_data.drag_start += shift; } - SelectToolFsmState::Dragging + SelectToolFsmState::Dragging { axis, using_compass } } (SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds, SelectToolMessage::PointerOutsideViewport(_)) => { // AutoPanning @@ -1061,7 +1184,7 @@ impl Fsm for SelectToolFsmState { state } - (SelectToolFsmState::Dragging, SelectToolMessage::Enter) => { + (SelectToolFsmState::Dragging { .. }, SelectToolMessage::Enter) => { let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON { true => DocumentMessage::AbortTransaction, false => DocumentMessage::EndTransaction, @@ -1072,9 +1195,10 @@ impl Fsm for SelectToolFsmState { let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } - (SelectToolFsmState::Dragging, SelectToolMessage::DragStop { remove_from_selection }) => { + (SelectToolFsmState::Dragging { .. }, SelectToolMessage::DragStop { remove_from_selection }) => { // Deselect layer if not snap dragging responses.add(DocumentMessage::EndTransaction); + tool_data.axis_align = false; if !tool_data.has_dragged && input.keyboard.key(remove_from_selection) && tool_data.layer_selected_on_start.is_none() { // When you click on the layer with remove from selection key (shift) pressed, we deselect all nodes that are children. @@ -1253,7 +1377,7 @@ impl Fsm for SelectToolFsmState { let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } - (SelectToolFsmState::Dragging, SelectToolMessage::Abort) => { + (SelectToolFsmState::Dragging { .. }, SelectToolMessage::Abort) => { responses.add(DocumentMessage::AbortTransaction); tool_data.snap_manager.cleanup(responses); responses.add(OverlaysMessage::Draw); @@ -1336,15 +1460,19 @@ impl Fsm for SelectToolFsmState { ]); responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - SelectToolFsmState::Dragging if tool_data.has_dragged => { - let hint_data = HintData(vec![ + SelectToolFsmState::Dragging { axis, using_compass } if tool_data.has_dragged => { + let mut hint_data = vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain to Axis")]), HintGroup(vec![ HintInfo::keys([Key::Alt], "Move Duplicate"), HintInfo::keys([Key::Control, Key::KeyD], "Place Duplicate").add_mac_keys([Key::Command, Key::KeyD]), ]), - ]); + ]; + + if !(*using_compass && axis.is_constraint()) { + hint_data.push(HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain to Axis")])); + }; + let hint_data = HintData(hint_data); responses.add(FrontendMessage::UpdateInputHints { hint_data }); } SelectToolFsmState::Drawing { .. } if tool_data.drag_start != tool_data.drag_current => { @@ -1357,7 +1485,7 @@ impl Fsm for SelectToolFsmState { ]); responses.add(FrontendMessage::UpdateInputHints { hint_data }); } - SelectToolFsmState::Drawing { .. } | SelectToolFsmState::Dragging => {} + SelectToolFsmState::Drawing { .. } | SelectToolFsmState::Dragging { .. } => {} SelectToolFsmState::ResizingBounds => { let hint_data = HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),