mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 13:58:12 +08:00
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
This commit is contained in:
committed by
Keavon Chambers
parent
ec43b7945e
commit
265cc0fe32
@@ -21,13 +21,15 @@ pub const SNAP_OVERLAY_UNSNAPPED_OPACITY: f64 = 0.4;
|
|||||||
|
|
||||||
pub const DRAG_THRESHOLD: f64 = 1.;
|
pub const DRAG_THRESHOLD: f64 = 1.;
|
||||||
|
|
||||||
|
pub const PATH_OUTLINE_WEIGHT: f64 = 2.;
|
||||||
|
|
||||||
// Transforming layer
|
// Transforming layer
|
||||||
pub const ROTATE_SNAP_ANGLE: f64 = 15.;
|
pub const ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||||
pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
|
pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
|
||||||
pub const SLOWING_DIVISOR: f64 = 10.;
|
pub const SLOWING_DIVISOR: f64 = 10.;
|
||||||
|
|
||||||
// Select tool
|
// Select tool
|
||||||
pub const SELECTION_TOLERANCE: f64 = 1.;
|
pub const SELECTION_TOLERANCE: f64 = 5.;
|
||||||
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
|
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
|
||||||
|
|
||||||
// Transformation cage
|
// Transformation cage
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use graphene::intersection::Quad;
|
|||||||
use graphene::layers::layer_info::LayerDataType;
|
use graphene::layers::layer_info::LayerDataType;
|
||||||
use graphene::Operation;
|
use graphene::Operation;
|
||||||
|
|
||||||
|
use super::shared::path_outline::*;
|
||||||
use super::shared::transformation_cage::*;
|
use super::shared::transformation_cage::*;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
@@ -253,7 +254,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for SelectTool {
|
|||||||
use SelectToolFsmState::*;
|
use SelectToolFsmState::*;
|
||||||
|
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
Ready => actions!(SelectToolMessageDiscriminant; DragStart, PointerMove, EditLayer),
|
Ready => actions!(SelectToolMessageDiscriminant; DragStart, PointerMove, Abort, EditLayer),
|
||||||
_ => actions!(SelectToolMessageDiscriminant; DragStop, PointerMove, Abort, EditLayer),
|
_ => actions!(SelectToolMessageDiscriminant; DragStop, PointerMove, Abort, EditLayer),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,6 +281,7 @@ struct SelectToolData {
|
|||||||
drag_current: ViewportPosition,
|
drag_current: ViewportPosition,
|
||||||
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
|
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
|
||||||
drag_box_overlay_layer: Option<Vec<LayerId>>,
|
drag_box_overlay_layer: Option<Vec<LayerId>>,
|
||||||
|
path_outlines: PathOutline,
|
||||||
bounding_box_overlays: Option<BoundingBoxOverlays>,
|
bounding_box_overlays: Option<BoundingBoxOverlays>,
|
||||||
snap_handler: SnapHandler,
|
snap_handler: SnapHandler,
|
||||||
cursor: MouseCursorIcon,
|
cursor: MouseCursorIcon,
|
||||||
@@ -337,6 +339,9 @@ impl Fsm for SelectToolFsmState {
|
|||||||
(_, _) => {}
|
(_, _) => {}
|
||||||
};
|
};
|
||||||
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
|
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
|
||||||
|
|
||||||
|
data.path_outlines.update_selected(document.selected_visible_layers(), document, responses);
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
(_, EditLayer) => {
|
(_, EditLayer) => {
|
||||||
@@ -360,6 +365,8 @@ impl Fsm for SelectToolFsmState {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
(Ready, DragStart { add_to_selection }) => {
|
(Ready, DragStart { add_to_selection }) => {
|
||||||
|
data.path_outlines.clear_hovered(responses);
|
||||||
|
|
||||||
data.drag_start = input.mouse.position;
|
data.drag_start = input.mouse.position;
|
||||||
data.drag_current = input.mouse.position;
|
data.drag_current = input.mouse.position;
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
@@ -536,6 +543,27 @@ impl Fsm for SelectToolFsmState {
|
|||||||
(Ready, PointerMove { .. }) => {
|
(Ready, PointerMove { .. }) => {
|
||||||
let cursor = data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
|
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 {
|
if data.cursor != cursor {
|
||||||
data.cursor = cursor;
|
data.cursor = cursor;
|
||||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
|
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
|
||||||
@@ -590,6 +618,9 @@ impl Fsm for SelectToolFsmState {
|
|||||||
(Dragging, Abort) => {
|
(Dragging, Abort) => {
|
||||||
data.snap_handler.cleanup(responses);
|
data.snap_handler.cleanup(responses);
|
||||||
responses.push_back(DocumentMessage::Undo.into());
|
responses.push_back(DocumentMessage::Undo.into());
|
||||||
|
|
||||||
|
data.path_outlines.clear_selected(responses);
|
||||||
|
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
(_, Abort) => {
|
(_, Abort) => {
|
||||||
@@ -611,6 +642,9 @@ impl Fsm for SelectToolFsmState {
|
|||||||
bounding_box_overlays.delete(responses);
|
bounding_box_overlays.delete(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.path_outlines.clear_hovered(responses);
|
||||||
|
data.path_outlines.clear_selected(responses);
|
||||||
|
|
||||||
data.snap_handler.cleanup(responses);
|
data.snap_handler.cleanup(responses);
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
pub mod path_outline;
|
||||||
pub mod resize;
|
pub mod resize;
|
||||||
pub mod transformation_cage;
|
pub mod transformation_cage;
|
||||||
|
|||||||
@@ -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<Vec<LayerId>>,
|
||||||
|
hovered_overlay_path: Option<Vec<LayerId>>,
|
||||||
|
selected_overlay_paths: Vec<Vec<LayerId>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<LayerId>, overlay_path: Option<Vec<LayerId>>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
|
||||||
|
// 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<Message>) {
|
||||||
|
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<LayerId>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
// 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<Message>) {
|
||||||
|
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<Item = &'a [LayerId]>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
use super::{constants::ControlPointType, vector_anchor::VectorAnchor, vector_control_point::VectorControlPoint};
|
use super::{constants::ControlPointType, vector_anchor::VectorAnchor, vector_control_point::VectorControlPoint};
|
||||||
use crate::{
|
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT};
|
||||||
consts::COLOR_ACCENT,
|
use crate::document::DocumentMessageHandler;
|
||||||
document::DocumentMessageHandler,
|
use crate::message_prelude::*;
|
||||||
message_prelude::{generate_uuid, DocumentMessage, Message},
|
|
||||||
};
|
|
||||||
|
|
||||||
use graphene::{
|
use graphene::{
|
||||||
color::Color,
|
color::Color,
|
||||||
@@ -355,7 +353,7 @@ impl VectorShape {
|
|||||||
let operation = Operation::AddOverlayShape {
|
let operation = Operation::AddOverlayShape {
|
||||||
path: layer_path.clone(),
|
path: layer_path.clone(),
|
||||||
bez_path: self.bez_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,
|
closed: false,
|
||||||
};
|
};
|
||||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||||
|
|||||||
Reference in New Issue
Block a user