Skip to content

Commit 70b4bea

Browse files
mTvare6Keavon
andauthored
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 <[email protected]>
1 parent e44c460 commit 70b4bea

File tree

8 files changed

+343
-40
lines changed

8 files changed

+343
-40
lines changed

editor/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "graphite-editor"
33
publish = false
44
version = "0.0.0"
5-
rust-version = "1.79"
5+
rust-version = "1.82"
66
authors = ["Graphite Authors <[email protected]>"]
77
edition = "2021"
88
readme = "../README.md"

editor/src/consts.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,20 @@ pub const DEFAULT_STROKE_WIDTH: f64 = 2.;
5454
pub const SELECTION_TOLERANCE: f64 = 5.;
5555
pub const DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD: f64 = 15.;
5656
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
57+
58+
// PIVOT
5759
pub const PIVOT_CROSSHAIR_THICKNESS: f64 = 1.;
5860
pub const PIVOT_CROSSHAIR_LENGTH: f64 = 9.;
5961
pub const PIVOT_DIAMETER: f64 = 5.;
6062

63+
// COMPASS ROSE
64+
pub const COMPASS_ROSE_RING_INNER_DIAMETER: f64 = 13.;
65+
pub const COMPASS_ROSE_MAIN_RING_DIAMETER: f64 = 15.;
66+
pub const COMPASS_ROSE_HOVER_RING_DIAMETER: f64 = 23.;
67+
pub const COMPASS_ROSE_ARROW_SIZE: f64 = 5.;
68+
// Angle to either side of the compass arrows where they are targetted by the cursor (in degrees, must be less than 45°)
69+
pub const COMPASS_ROSE_ARROW_CLICK_TARGET_ANGLE: f64 = 20.;
70+
6171
// TRANSFORM OVERLAY
6272
pub const ANGLE_MEASURE_RADIUS_FACTOR: f64 = 0.04;
6373
pub const ARC_MEASURE_RADIUS_FACTOR_RANGE: (f64, f64) = (0.05, 0.15);
@@ -108,7 +118,6 @@ pub const COLOR_OVERLAY_RED: &str = "#ef5454";
108118
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
109119
pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
110120
pub const COLOR_OVERLAY_LABEL_BACKGROUND: &str = "#000000cc";
111-
pub const COLOR_OVERLAY_TRANSPARENT: &str = "#ffffff00";
112121

113122
// DOCUMENT
114123
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";

editor/src/messages/portfolio/document/overlays/utility_types.rs

+87-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::utility_functions::overlay_canvas_context;
22
use crate::consts::{
3-
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_TRANSPARENT, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
3+
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,
4+
COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
45
};
56
use crate::messages::prelude::Message;
67

@@ -9,7 +10,7 @@ use graphene_core::renderer::Quad;
910
use graphene_std::vector::{PointId, SegmentId, VectorData};
1011

1112
use core::borrow::Borrow;
12-
use core::f64::consts::TAU;
13+
use core::f64::consts::{FRAC_PI_2, TAU};
1314
use glam::{DAffine2, DVec2};
1415
use std::collections::HashMap;
1516
use wasm_bindgen::JsValue;
@@ -294,9 +295,15 @@ impl OverlayContext {
294295

295296
pub fn draw_scale(&mut self, start: DVec2, scale: f64, radius: f64, text: &str) {
296297
let sign = scale.signum();
298+
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap())
299+
.unwrap()
300+
.with_alpha(0.05)
301+
.rgba_hex();
302+
fill_color.insert(0, '#');
303+
let fill_color = Some(fill_color.as_str());
297304
self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None);
298-
self.circle(start, radius, Some(COLOR_OVERLAY_TRANSPARENT), None);
299-
self.circle(start, radius * scale.abs(), Some(COLOR_OVERLAY_TRANSPARENT), None);
305+
self.circle(start, radius, fill_color, None);
306+
self.circle(start, radius * scale.abs(), fill_color, None);
300307
self.text(
301308
text,
302309
COLOR_OVERLAY_BLUE,
@@ -307,7 +314,77 @@ impl OverlayContext {
307314
)
308315
}
309316

310-
pub fn pivot(&mut self, position: DVec2) {
317+
pub fn compass_rose(&mut self, compass_center: DVec2, angle: f64, show_compass_with_hover_ring: Option<bool>) {
318+
const HOVER_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_HOVER_RING_DIAMETER / 2.;
319+
const MAIN_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_MAIN_RING_DIAMETER / 2.;
320+
const MAIN_RING_INNER_RADIUS: f64 = COMPASS_ROSE_RING_INNER_DIAMETER / 2.;
321+
const ARROW_RADIUS: f64 = COMPASS_ROSE_ARROW_SIZE / 2.;
322+
const HOVER_RING_STROKE_WIDTH: f64 = HOVER_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS;
323+
const HOVER_RING_CENTERLINE_RADIUS: f64 = (HOVER_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.;
324+
const MAIN_RING_STROKE_WIDTH: f64 = MAIN_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS;
325+
const MAIN_RING_CENTERLINE_RADIUS: f64 = (MAIN_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.;
326+
327+
let Some(show_hover_ring) = show_compass_with_hover_ring else { return };
328+
329+
self.start_dpi_aware_transform();
330+
331+
let center = compass_center.round() - DVec2::splat(0.5);
332+
333+
// Save the old line width to restore it later
334+
let old_line_width = self.render_context.line_width();
335+
336+
// Hover ring
337+
if show_hover_ring {
338+
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.5).rgba_hex();
339+
fill_color.insert(0, '#');
340+
341+
self.render_context.set_line_width(HOVER_RING_STROKE_WIDTH);
342+
self.render_context.begin_path();
343+
self.render_context.arc(center.x, center.y, HOVER_RING_CENTERLINE_RADIUS, 0., TAU).expect("Failed to draw hover ring");
344+
self.render_context.set_stroke_style_str(&fill_color);
345+
self.render_context.stroke();
346+
}
347+
348+
// Arrows
349+
self.render_context.set_line_width(0.01);
350+
for i in 0..4 {
351+
let direction = DVec2::from_angle(i as f64 * FRAC_PI_2 + angle);
352+
let color = if i % 2 == 0 { COLOR_OVERLAY_RED } else { COLOR_OVERLAY_GREEN };
353+
354+
let tip = center + direction * HOVER_RING_OUTER_RADIUS;
355+
let base = center + direction * (MAIN_RING_INNER_RADIUS + MAIN_RING_OUTER_RADIUS) / 2.;
356+
357+
let r = (ARROW_RADIUS.powi(2) + MAIN_RING_INNER_RADIUS.powi(2)).sqrt();
358+
let (cos, sin) = (MAIN_RING_INNER_RADIUS / r, ARROW_RADIUS / r);
359+
let side1 = center + r * DVec2::new(cos * direction.x - sin * direction.y, sin * direction.x + direction.y * cos);
360+
let side2 = center + r * DVec2::new(cos * direction.x + sin * direction.y, -sin * direction.x + direction.y * cos);
361+
362+
self.render_context.begin_path();
363+
self.render_context.move_to(tip.x, tip.y);
364+
self.render_context.line_to(side1.x, side1.y);
365+
self.render_context.line_to(base.x, base.y);
366+
self.render_context.line_to(side2.x, side2.y);
367+
self.render_context.close_path();
368+
369+
self.render_context.set_fill_style_str(color);
370+
self.render_context.fill();
371+
self.render_context.set_stroke_style_str(color);
372+
self.render_context.stroke();
373+
}
374+
375+
// Main ring
376+
self.render_context.set_line_width(MAIN_RING_STROKE_WIDTH);
377+
self.render_context.begin_path();
378+
self.render_context.arc(center.x, center.y, MAIN_RING_CENTERLINE_RADIUS, 0., TAU).expect("Failed to draw main ring");
379+
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
380+
self.render_context.stroke();
381+
382+
// Restore the old line width
383+
self.render_context.set_line_width(old_line_width);
384+
}
385+
386+
pub fn pivot(&mut self, position: DVec2, angle: f64) {
387+
let uv = DVec2::from_angle(angle);
311388
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
312389

313390
self.start_dpi_aware_transform();
@@ -322,19 +399,19 @@ impl OverlayContext {
322399
// Crosshair
323400

324401
// 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
325-
let crosshair_radius = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.;
402+
const CROSSHAIR_RADIUS: f64 = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.;
326403

327404
self.render_context.set_stroke_style_str(COLOR_OVERLAY_YELLOW);
328405
self.render_context.set_line_cap("round");
329406

330407
self.render_context.begin_path();
331-
self.render_context.move_to(x - crosshair_radius, y);
332-
self.render_context.line_to(x + crosshair_radius, y);
408+
self.render_context.move_to(x + CROSSHAIR_RADIUS * uv.x, y + CROSSHAIR_RADIUS * uv.y);
409+
self.render_context.line_to(x - CROSSHAIR_RADIUS * uv.x, y - CROSSHAIR_RADIUS * uv.y);
333410
self.render_context.stroke();
334411

335412
self.render_context.begin_path();
336-
self.render_context.move_to(x, y - crosshair_radius);
337-
self.render_context.line_to(x, y + crosshair_radius);
413+
self.render_context.move_to(x - CROSSHAIR_RADIUS * uv.y, y + CROSSHAIR_RADIUS * uv.x);
414+
self.render_context.line_to(x + CROSSHAIR_RADIUS * uv.y, y - CROSSHAIR_RADIUS * uv.x);
338415
self.render_context.stroke();
339416

340417
self.render_context.set_line_cap("butt");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use crate::consts::{COMPASS_ROSE_ARROW_CLICK_TARGET_ANGLE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER};
2+
use crate::messages::prelude::DocumentMessageHandler;
3+
4+
use glam::{DAffine2, DVec2};
5+
use std::f64::consts::FRAC_PI_2;
6+
7+
#[derive(Clone, Default, Debug)]
8+
pub struct CompassRose {
9+
compass_center: DVec2,
10+
}
11+
12+
impl CompassRose {
13+
pub fn refresh_transform(&mut self, document: &DocumentMessageHandler) {
14+
let [min, max] = document.selected_visible_and_unlock_layers_bounding_box_viewport().unwrap_or([DVec2::ZERO, DVec2::ONE]);
15+
self.compass_center = (DAffine2::from_translation(min) * DAffine2::from_scale(max - min)).transform_point2(DVec2::splat(0.5));
16+
}
17+
18+
pub fn compass_rose_position(&self) -> DVec2 {
19+
self.compass_center
20+
}
21+
22+
pub fn compass_rose_state(&self, mouse: DVec2, angle: f64) -> CompassRoseState {
23+
const COMPASS_ROSE_RING_INNER_RADIUS_SQUARED: f64 = (COMPASS_ROSE_RING_INNER_DIAMETER / 2.) * (COMPASS_ROSE_RING_INNER_DIAMETER / 2.);
24+
const COMPASS_ROSE_HOVER_RING_RADIUS_SQUARED: f64 = (COMPASS_ROSE_HOVER_RING_DIAMETER / 2.) * (COMPASS_ROSE_HOVER_RING_DIAMETER / 2.);
25+
26+
let compass_distance_squared = mouse.distance_squared(self.compass_center);
27+
28+
if !(COMPASS_ROSE_RING_INNER_RADIUS_SQUARED..COMPASS_ROSE_HOVER_RING_RADIUS_SQUARED).contains(&compass_distance_squared) {
29+
return CompassRoseState::None;
30+
}
31+
32+
let angle = (mouse - self.compass_center).angle_to(DVec2::from_angle(angle)).abs();
33+
let resolved_angle = (FRAC_PI_2 - angle).abs();
34+
let angular_width = COMPASS_ROSE_ARROW_CLICK_TARGET_ANGLE.to_radians();
35+
36+
if resolved_angle < angular_width {
37+
CompassRoseState::AxisY
38+
} else if resolved_angle > (FRAC_PI_2 - angular_width) {
39+
CompassRoseState::AxisX
40+
} else {
41+
CompassRoseState::Ring
42+
}
43+
}
44+
}
45+
46+
#[derive(Clone, Debug, PartialEq)]
47+
pub enum CompassRoseState {
48+
Ring,
49+
AxisX,
50+
AxisY,
51+
None,
52+
}
53+
54+
impl CompassRoseState {
55+
pub fn can_grab(&self) -> bool {
56+
matches!(self, Self::Ring | Self::AxisX | Self::AxisY)
57+
}
58+
59+
pub fn is_ring(&self) -> bool {
60+
matches!(self, Self::Ring)
61+
}
62+
63+
pub fn axis_type(&self) -> Option<Axis> {
64+
match self {
65+
CompassRoseState::AxisX => Some(Axis::X),
66+
CompassRoseState::AxisY => Some(Axis::Y),
67+
CompassRoseState::Ring => Some(Axis::None),
68+
_ => None,
69+
}
70+
}
71+
}
72+
73+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
74+
pub enum Axis {
75+
#[default]
76+
None,
77+
X,
78+
Y,
79+
}
80+
81+
impl Axis {
82+
pub fn is_constraint(&self) -> bool {
83+
matches!(self, Self::X | Self::Y)
84+
}
85+
}

editor/src/messages/tool/common_functionality/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod auto_panning;
22
pub mod color_selector;
3+
pub mod compass_rose;
34
pub mod graph_modification_utils;
45
pub mod measure;
56
pub mod pivot;

editor/src/messages/tool/common_functionality/pivot.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ impl Pivot {
8383
}
8484
}
8585

86-
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
86+
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, angle: f64) {
8787
self.recalculate_pivot(document);
8888
if let Some(pivot) = self.pivot {
89-
overlay_context.pivot(pivot);
89+
overlay_context.pivot(pivot, angle);
9090
}
9191
}
9292

editor/src/messages/tool/common_functionality/transformation_cage.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::consts::{
2-
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,
3-
SELECTION_DRAG_ANGLE,
2+
BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_OVERLAY_WHITE, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY, MIN_LENGTH_FOR_MIDPOINT_VISIBILITY,
3+
MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, SELECTION_DRAG_ANGLE,
44
};
55
use crate::messages::frontend::utility_types::MouseCursorIcon;
66
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -379,7 +379,10 @@ impl BoundingBoxManager {
379379
// Draw the bounding box rectangle
380380
overlay_context.quad(quad, None);
381381

382-
let mut draw_handle = |point: DVec2| overlay_context.square(point, Some(6.), None, None);
382+
let mut draw_handle = |point: DVec2| {
383+
let quad = DAffine2::from_angle_translation((quad.top_left() - quad.top_right()).to_angle(), point) * Quad::from_box([DVec2::splat(-3.), DVec2::splat(3.)]);
384+
overlay_context.quad(quad, Some(COLOR_OVERLAY_WHITE));
385+
};
383386

384387
// Draw the horizontal midpoint drag handles
385388
if matches!(category, HandleDisplayCategory::Full | HandleDisplayCategory::Narrow | HandleDisplayCategory::ReducedLandscape) {

0 commit comments

Comments
 (0)