Skip to content

Draw the outlines of shapes on hover and selection #609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 35 additions & 1 deletion editor/src/viewport_tools/tools/select_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -253,7 +254,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> 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),
}
}
Expand All @@ -280,6 +281,7 @@ struct SelectToolData {
drag_current: ViewportPosition,
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
drag_box_overlay_layer: Option<Vec<LayerId>>,
path_outlines: PathOutline,
bounding_box_overlays: Option<BoundingBoxOverlays>,
snap_handler: SnapHandler,
cursor: MouseCursorIcon,
Expand Down Expand Up @@ -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) => {
Expand All @@ -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();
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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) => {
Expand All @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions editor/src/viewport_tools/tools/shared/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod path_outline;
pub mod resize;
pub mod transformation_cage;
114 changes: 114 additions & 0 deletions editor/src/viewport_tools/tools/shared/path_outline.rs
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
10 changes: 4 additions & 6 deletions editor/src/viewport_tools/vector_editor/vector_shape.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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());
Expand Down