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:
parent
e44c460cf8
commit
70b4beab49
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()]),
|
||||
|
|
|
|||
Loading…
Reference in New Issue