diff --git a/editor/src/consts.rs b/editor/src/consts.rs index eea97a4faa..c0d693fdff 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -21,13 +21,15 @@ pub const SNAP_OVERLAY_UNSNAPPED_OPACITY: f64 = 0.4; pub const DRAG_THRESHOLD: f64 = 1.; +pub const PATH_OUTLINE_WEIGHT: f64 = 2.; + // Transforming layer pub const ROTATE_SNAP_ANGLE: f64 = 15.; pub const SCALE_SNAP_INTERVAL: f64 = 0.1; pub const SLOWING_DIVISOR: f64 = 10.; // Select tool -pub const SELECTION_TOLERANCE: f64 = 1.; +pub const SELECTION_TOLERANCE: f64 = 5.; pub const SELECTION_DRAG_ANGLE: f64 = 90.; // Transformation cage diff --git a/editor/src/viewport_tools/tools/select_tool.rs b/editor/src/viewport_tools/tools/select_tool.rs index ceb6f59818..294f342599 100644 --- a/editor/src/viewport_tools/tools/select_tool.rs +++ b/editor/src/viewport_tools/tools/select_tool.rs @@ -17,6 +17,7 @@ use graphene::intersection::Quad; use graphene::layers::layer_info::LayerDataType; use graphene::Operation; +use super::shared::path_outline::*; use super::shared::transformation_cage::*; use glam::{DAffine2, DVec2}; @@ -253,7 +254,7 @@ impl<'a> MessageHandler> for SelectTool { use SelectToolFsmState::*; match self.fsm_state { - Ready => actions!(SelectToolMessageDiscriminant; DragStart, PointerMove, EditLayer), + Ready => actions!(SelectToolMessageDiscriminant; DragStart, PointerMove, Abort, EditLayer), _ => actions!(SelectToolMessageDiscriminant; DragStop, PointerMove, Abort, EditLayer), } } @@ -280,6 +281,7 @@ struct SelectToolData { drag_current: ViewportPosition, layers_dragging: Vec>, // Paths and offsets drag_box_overlay_layer: Option>, + path_outlines: PathOutline, bounding_box_overlays: Option, snap_handler: SnapHandler, cursor: MouseCursorIcon, @@ -337,6 +339,9 @@ impl Fsm for SelectToolFsmState { (_, _) => {} }; buffer.into_iter().rev().for_each(|message| responses.push_front(message)); + + data.path_outlines.update_selected(document.selected_visible_layers(), document, responses); + self } (_, EditLayer) => { @@ -360,6 +365,8 @@ impl Fsm for SelectToolFsmState { self } (Ready, DragStart { add_to_selection }) => { + data.path_outlines.clear_hovered(responses); + data.drag_start = input.mouse.position; data.drag_current = input.mouse.position; let mut buffer = Vec::new(); @@ -536,6 +543,27 @@ impl Fsm for SelectToolFsmState { (Ready, PointerMove { .. }) => { let cursor = data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true)); + // Generate the select outline (but not if the user is going to use the bound overlays) + if cursor == MouseCursorIcon::Default { + // Get the layer the user is hovering over + let tolerance = DVec2::splat(SELECTION_TOLERANCE); + let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]); + let mut intersection = document.graphene_document.intersects_quad_root(quad); + + // If the user is hovering over a layer they have not already selected, then update outline + if let Some(path) = intersection.pop() { + if !document.selected_visible_layers().any(|visible| visible == path.as_slice()) { + data.path_outlines.update_hovered(path, document, responses) + } else { + data.path_outlines.clear_hovered(responses); + } + } else { + data.path_outlines.clear_hovered(responses); + } + } else { + data.path_outlines.clear_hovered(responses); + } + if data.cursor != cursor { data.cursor = cursor; responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into()); @@ -590,6 +618,9 @@ impl Fsm for SelectToolFsmState { (Dragging, Abort) => { data.snap_handler.cleanup(responses); responses.push_back(DocumentMessage::Undo.into()); + + data.path_outlines.clear_selected(responses); + Ready } (_, Abort) => { @@ -611,6 +642,9 @@ impl Fsm for SelectToolFsmState { bounding_box_overlays.delete(responses); } + data.path_outlines.clear_hovered(responses); + data.path_outlines.clear_selected(responses); + data.snap_handler.cleanup(responses); Ready } diff --git a/editor/src/viewport_tools/tools/shared/mod.rs b/editor/src/viewport_tools/tools/shared/mod.rs index b7d6c44ce9..59a94bc880 100644 --- a/editor/src/viewport_tools/tools/shared/mod.rs +++ b/editor/src/viewport_tools/tools/shared/mod.rs @@ -1,2 +1,3 @@ +pub mod path_outline; pub mod resize; pub mod transformation_cage; diff --git a/editor/src/viewport_tools/tools/shared/path_outline.rs b/editor/src/viewport_tools/tools/shared/path_outline.rs new file mode 100644 index 0000000000..44754ecbb3 --- /dev/null +++ b/editor/src/viewport_tools/tools/shared/path_outline.rs @@ -0,0 +1,114 @@ +use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT}; +use crate::document::DocumentMessageHandler; +use crate::message_prelude::*; + +use graphene::layers::layer_info::LayerDataType; +use graphene::layers::style::{self, Fill, Stroke}; +use graphene::{LayerId, Operation}; + +use glam::DAffine2; +use kurbo::{BezPath, Shape}; +use std::collections::VecDeque; + +/// Manages the overlay used by the select tool for outlining selected shapes and when hovering over a non selected shape. +#[derive(Clone, Debug, Default)] +pub struct PathOutline { + hovered_layer_path: Option>, + hovered_overlay_path: Option>, + selected_overlay_paths: Vec>, +} + +impl PathOutline { + /// Creates an outline of a layer either with a pre-existing overlay or by generating a new one + fn create_outline(document_layer_path: Vec, overlay_path: Option>, document: &DocumentMessageHandler, responses: &mut VecDeque) -> Option> { + // Get layer data + let document_layer = document.graphene_document.layer(&document_layer_path).ok()?; + + // Get the bezpath from the shape or text + let path = match &document_layer.data { + LayerDataType::Shape(shape) => Some(shape.path.clone()), + LayerDataType::Text(text) => Some(text.to_bez_path_nonmut(&document.graphene_document.font_cache)), + _ => document_layer + .aabounding_box_for_transform(DAffine2::IDENTITY, &document.graphene_document.font_cache) + .map(|bounds| kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y).to_path(0.)), + }?; + + // Generate a new overlay layer if necessary + let overlay = match overlay_path { + Some(path) => path, + None => { + let overlay_path = vec![generate_uuid()]; + let operation = Operation::AddOverlayShape { + path: overlay_path.clone(), + bez_path: BezPath::new(), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None), + closed: false, + }; + + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + + overlay_path + } + }; + + // Update the shape bezpath + let operation = Operation::SetShapePath { + path: overlay.clone(), + bez_path: path, + }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + + // Update the transform to match the document + let operation = Operation::SetLayerTransform { + path: overlay.clone(), + transform: document.graphene_document.multiply_transforms(&document_layer_path).unwrap().to_cols_array(), + }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + + Some(overlay) + } + + /// Removes the hovered overlay and deletes path references + pub fn clear_hovered(&mut self, responses: &mut VecDeque) { + if let Some(path) = self.hovered_overlay_path.take() { + let operation = Operation::DeleteLayer { path }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + } + self.hovered_layer_path = None; + } + + /// Updates the overlay, generating a new one if necessary + pub fn update_hovered(&mut self, new_layer_path: Vec, document: &DocumentMessageHandler, responses: &mut VecDeque) { + // Check if we are hovering over a different layer than before + if self.hovered_layer_path.as_ref().map_or(true, |old| &new_layer_path != old) { + self.hovered_overlay_path = Self::create_outline(new_layer_path.clone(), self.hovered_overlay_path.take(), document, responses); + if self.hovered_overlay_path.is_none() { + self.clear_hovered(responses); + } + } + self.hovered_layer_path = Some(new_layer_path); + } + + /// Clears overlays for the seleted paths and removes references + pub fn clear_selected(&mut self, responses: &mut VecDeque) { + if let Some(path) = self.selected_overlay_paths.pop() { + let operation = Operation::DeleteLayer { path }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + } + } + + /// Updates the selected overlays, generating or removing overlays if necessary + pub fn update_selected<'a>(&mut self, selected: impl Iterator, document: &DocumentMessageHandler, responses: &mut VecDeque) { + let mut old_overlay_paths = std::mem::take(&mut self.selected_overlay_paths); + + for document_layer_path in selected { + if let Some(overlay_path) = Self::create_outline(document_layer_path.to_vec(), old_overlay_paths.pop(), document, responses) { + self.selected_overlay_paths.push(overlay_path); + } + } + for path in old_overlay_paths { + let operation = Operation::DeleteLayer { path }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + } + } +} diff --git a/editor/src/viewport_tools/vector_editor/vector_shape.rs b/editor/src/viewport_tools/vector_editor/vector_shape.rs index fd7d69569d..15b037efed 100644 --- a/editor/src/viewport_tools/vector_editor/vector_shape.rs +++ b/editor/src/viewport_tools/vector_editor/vector_shape.rs @@ -1,9 +1,7 @@ use super::{constants::ControlPointType, vector_anchor::VectorAnchor, vector_control_point::VectorControlPoint}; -use crate::{ - consts::COLOR_ACCENT, - document::DocumentMessageHandler, - message_prelude::{generate_uuid, DocumentMessage, Message}, -}; +use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT}; +use crate::document::DocumentMessageHandler; +use crate::message_prelude::*; use graphene::{ color::Color, @@ -355,7 +353,7 @@ impl VectorShape { let operation = Operation::AddOverlayShape { path: layer_path.clone(), bez_path: self.bez_path.clone(), - style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None), closed: false, }; responses.push_back(DocumentMessage::Overlays(operation.into()).into());