Skip to content

Commit 72d26fb

Browse files
authored
Draw the outlines of shapes on hover and selection (#609)
* Add hover outline overlay * Increase selection tolerance * Increase weight * Only check if top intersection is selected * Outline selected paths * Reduce outline weight * Increase path tool outline thickness to match hover
1 parent a9f1794 commit 72d26fb

File tree

5 files changed

+157
-8
lines changed

5 files changed

+157
-8
lines changed

editor/src/consts.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ pub const SNAP_OVERLAY_UNSNAPPED_OPACITY: f64 = 0.4;
2121

2222
pub const DRAG_THRESHOLD: f64 = 1.;
2323

24+
pub const PATH_OUTLINE_WEIGHT: f64 = 2.;
25+
2426
// Transforming layer
2527
pub const ROTATE_SNAP_ANGLE: f64 = 15.;
2628
pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
2729
pub const SLOWING_DIVISOR: f64 = 10.;
2830

2931
// Select tool
30-
pub const SELECTION_TOLERANCE: f64 = 1.;
32+
pub const SELECTION_TOLERANCE: f64 = 5.;
3133
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
3234

3335
// Transformation cage

editor/src/viewport_tools/tools/select_tool.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use graphene::intersection::Quad;
1717
use graphene::layers::layer_info::LayerDataType;
1818
use graphene::Operation;
1919

20+
use super::shared::path_outline::*;
2021
use super::shared::transformation_cage::*;
2122

2223
use glam::{DAffine2, DVec2};
@@ -253,7 +254,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for SelectTool {
253254
use SelectToolFsmState::*;
254255

255256
match self.fsm_state {
256-
Ready => actions!(SelectToolMessageDiscriminant; DragStart, PointerMove, EditLayer),
257+
Ready => actions!(SelectToolMessageDiscriminant; DragStart, PointerMove, Abort, EditLayer),
257258
_ => actions!(SelectToolMessageDiscriminant; DragStop, PointerMove, Abort, EditLayer),
258259
}
259260
}
@@ -280,6 +281,7 @@ struct SelectToolData {
280281
drag_current: ViewportPosition,
281282
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
282283
drag_box_overlay_layer: Option<Vec<LayerId>>,
284+
path_outlines: PathOutline,
283285
bounding_box_overlays: Option<BoundingBoxOverlays>,
284286
snap_handler: SnapHandler,
285287
cursor: MouseCursorIcon,
@@ -337,6 +339,9 @@ impl Fsm for SelectToolFsmState {
337339
(_, _) => {}
338340
};
339341
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
342+
343+
data.path_outlines.update_selected(document.selected_visible_layers(), document, responses);
344+
340345
self
341346
}
342347
(_, EditLayer) => {
@@ -360,6 +365,8 @@ impl Fsm for SelectToolFsmState {
360365
self
361366
}
362367
(Ready, DragStart { add_to_selection }) => {
368+
data.path_outlines.clear_hovered(responses);
369+
363370
data.drag_start = input.mouse.position;
364371
data.drag_current = input.mouse.position;
365372
let mut buffer = Vec::new();
@@ -536,6 +543,27 @@ impl Fsm for SelectToolFsmState {
536543
(Ready, PointerMove { .. }) => {
537544
let cursor = data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
538545

546+
// Generate the select outline (but not if the user is going to use the bound overlays)
547+
if cursor == MouseCursorIcon::Default {
548+
// Get the layer the user is hovering over
549+
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
550+
let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]);
551+
let mut intersection = document.graphene_document.intersects_quad_root(quad);
552+
553+
// If the user is hovering over a layer they have not already selected, then update outline
554+
if let Some(path) = intersection.pop() {
555+
if !document.selected_visible_layers().any(|visible| visible == path.as_slice()) {
556+
data.path_outlines.update_hovered(path, document, responses)
557+
} else {
558+
data.path_outlines.clear_hovered(responses);
559+
}
560+
} else {
561+
data.path_outlines.clear_hovered(responses);
562+
}
563+
} else {
564+
data.path_outlines.clear_hovered(responses);
565+
}
566+
539567
if data.cursor != cursor {
540568
data.cursor = cursor;
541569
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
@@ -590,6 +618,9 @@ impl Fsm for SelectToolFsmState {
590618
(Dragging, Abort) => {
591619
data.snap_handler.cleanup(responses);
592620
responses.push_back(DocumentMessage::Undo.into());
621+
622+
data.path_outlines.clear_selected(responses);
623+
593624
Ready
594625
}
595626
(_, Abort) => {
@@ -611,6 +642,9 @@ impl Fsm for SelectToolFsmState {
611642
bounding_box_overlays.delete(responses);
612643
}
613644

645+
data.path_outlines.clear_hovered(responses);
646+
data.path_outlines.clear_selected(responses);
647+
614648
data.snap_handler.cleanup(responses);
615649
Ready
616650
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
pub mod path_outline;
12
pub mod resize;
23
pub mod transformation_cage;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT};
2+
use crate::document::DocumentMessageHandler;
3+
use crate::message_prelude::*;
4+
5+
use graphene::layers::layer_info::LayerDataType;
6+
use graphene::layers::style::{self, Fill, Stroke};
7+
use graphene::{LayerId, Operation};
8+
9+
use glam::DAffine2;
10+
use kurbo::{BezPath, Shape};
11+
use std::collections::VecDeque;
12+
13+
/// Manages the overlay used by the select tool for outlining selected shapes and when hovering over a non selected shape.
14+
#[derive(Clone, Debug, Default)]
15+
pub struct PathOutline {
16+
hovered_layer_path: Option<Vec<LayerId>>,
17+
hovered_overlay_path: Option<Vec<LayerId>>,
18+
selected_overlay_paths: Vec<Vec<LayerId>>,
19+
}
20+
21+
impl PathOutline {
22+
/// Creates an outline of a layer either with a pre-existing overlay or by generating a new one
23+
fn create_outline(document_layer_path: Vec<LayerId>, overlay_path: Option<Vec<LayerId>>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
24+
// Get layer data
25+
let document_layer = document.graphene_document.layer(&document_layer_path).ok()?;
26+
27+
// Get the bezpath from the shape or text
28+
let path = match &document_layer.data {
29+
LayerDataType::Shape(shape) => Some(shape.path.clone()),
30+
LayerDataType::Text(text) => Some(text.to_bez_path_nonmut(&document.graphene_document.font_cache)),
31+
_ => document_layer
32+
.aabounding_box_for_transform(DAffine2::IDENTITY, &document.graphene_document.font_cache)
33+
.map(|bounds| kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y).to_path(0.)),
34+
}?;
35+
36+
// Generate a new overlay layer if necessary
37+
let overlay = match overlay_path {
38+
Some(path) => path,
39+
None => {
40+
let overlay_path = vec![generate_uuid()];
41+
let operation = Operation::AddOverlayShape {
42+
path: overlay_path.clone(),
43+
bez_path: BezPath::new(),
44+
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
45+
closed: false,
46+
};
47+
48+
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
49+
50+
overlay_path
51+
}
52+
};
53+
54+
// Update the shape bezpath
55+
let operation = Operation::SetShapePath {
56+
path: overlay.clone(),
57+
bez_path: path,
58+
};
59+
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
60+
61+
// Update the transform to match the document
62+
let operation = Operation::SetLayerTransform {
63+
path: overlay.clone(),
64+
transform: document.graphene_document.multiply_transforms(&document_layer_path).unwrap().to_cols_array(),
65+
};
66+
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
67+
68+
Some(overlay)
69+
}
70+
71+
/// Removes the hovered overlay and deletes path references
72+
pub fn clear_hovered(&mut self, responses: &mut VecDeque<Message>) {
73+
if let Some(path) = self.hovered_overlay_path.take() {
74+
let operation = Operation::DeleteLayer { path };
75+
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
76+
}
77+
self.hovered_layer_path = None;
78+
}
79+
80+
/// Updates the overlay, generating a new one if necessary
81+
pub fn update_hovered(&mut self, new_layer_path: Vec<LayerId>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
82+
// Check if we are hovering over a different layer than before
83+
if self.hovered_layer_path.as_ref().map_or(true, |old| &new_layer_path != old) {
84+
self.hovered_overlay_path = Self::create_outline(new_layer_path.clone(), self.hovered_overlay_path.take(), document, responses);
85+
if self.hovered_overlay_path.is_none() {
86+
self.clear_hovered(responses);
87+
}
88+
}
89+
self.hovered_layer_path = Some(new_layer_path);
90+
}
91+
92+
/// Clears overlays for the seleted paths and removes references
93+
pub fn clear_selected(&mut self, responses: &mut VecDeque<Message>) {
94+
if let Some(path) = self.selected_overlay_paths.pop() {
95+
let operation = Operation::DeleteLayer { path };
96+
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
97+
}
98+
}
99+
100+
/// Updates the selected overlays, generating or removing overlays if necessary
101+
pub fn update_selected<'a>(&mut self, selected: impl Iterator<Item = &'a [LayerId]>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
102+
let mut old_overlay_paths = std::mem::take(&mut self.selected_overlay_paths);
103+
104+
for document_layer_path in selected {
105+
if let Some(overlay_path) = Self::create_outline(document_layer_path.to_vec(), old_overlay_paths.pop(), document, responses) {
106+
self.selected_overlay_paths.push(overlay_path);
107+
}
108+
}
109+
for path in old_overlay_paths {
110+
let operation = Operation::DeleteLayer { path };
111+
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
112+
}
113+
}
114+
}

editor/src/viewport_tools/vector_editor/vector_shape.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
use super::{constants::ControlPointType, vector_anchor::VectorAnchor, vector_control_point::VectorControlPoint};
2-
use crate::{
3-
consts::COLOR_ACCENT,
4-
document::DocumentMessageHandler,
5-
message_prelude::{generate_uuid, DocumentMessage, Message},
6-
};
2+
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT};
3+
use crate::document::DocumentMessageHandler;
4+
use crate::message_prelude::*;
75

86
use graphene::{
97
color::Color,
@@ -355,7 +353,7 @@ impl VectorShape {
355353
let operation = Operation::AddOverlayShape {
356354
path: layer_path.clone(),
357355
bez_path: self.bez_path.clone(),
358-
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
356+
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
359357
closed: false,
360358
};
361359
responses.push_back(DocumentMessage::Overlays(operation.into()).into());

0 commit comments

Comments
 (0)