Add the compass rose translation gizmo to the transform cage (#2277)

* Rotate pivot and squares to orient along quad

* Add compass rose UI

* Add compass rose functionality

* Refactor code and polish things

* Fix UI

* Fix crash

* More polish

* Rework arrow to use different selection method

* Adjust for rotated layer and show when within cage

* Don't show when other modes are possible

* Fix glitchy compass

* fixes

* fixes

* WIP separate pivot and compass rose (not compiling)

* Complete file moving fixes

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
mTvare 2025-02-15 11:59:32 +05:30 committed by GitHub
parent e44c460cf8
commit 70b4beab49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 343 additions and 40 deletions

View File

@ -2,7 +2,7 @@
name = "graphite-editor"
publish = false
version = "0.0.0"
rust-version = "1.79"
rust-version = "1.82"
authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021"
readme = "../README.md"

View File

@ -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";

View File

@ -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<bool>) {
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");

View File

@ -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<Axis> {
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)
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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<LayerNodeIdentifier>,
select_single_layer: Option<LayerNodeIdentifier>,
has_dragged: bool,
axis_align: bool,
non_duplicated_layers: Option<Vec<LayerNodeIdentifier>>,
bounding_box_manager: Option<BoundingBoxManager>,
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()]),