New overlay system (#1516)

* Port gradient tool overlays

* Fix tests

* Text tool

* Artboard tool and some of select tool

* Port select tool drawing box

* Pen and path tool

* Remove overlays document

* Show the overlay refactor as done on the website roadmap

* Select tool bounds in layer space (first layer)

* Code review and fixes

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2023-12-18 11:17:43 +00:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent 9e06e70aa2
commit c42d030f18
36 changed files with 552 additions and 1425 deletions
@@ -1,7 +1,5 @@
pub mod color_selector;
pub mod graph_modification_utils;
pub mod overlay_renderer;
pub mod path_outline;
pub mod pivot;
pub mod resize;
pub mod shape_editor;
@@ -1,359 +0,0 @@
use super::shape_editor::SelectedShapeState;
use crate::application::generate_uuid;
use crate::consts::VIEWPORT_GRID_ROUNDING_BIAS;
use crate::consts::{COLOR_ACCENT, HIDE_HANDLE_DISTANCE, MANIPULATOR_GROUP_MARKER_SIZE, PATH_OUTLINE_WEIGHT};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_groups, get_subpaths};
use bezier_rs::ManipulatorGroup;
use document_legacy::document::Document;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::style::{self, Fill, Stroke};
use document_legacy::{LayerId, Operation};
use graphene_core::raster::color::Color;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use glam::{DAffine2, DVec2};
/// [ManipulatorGroupOverlay]s is the collection of overlays that make up an [ManipulatorGroup] visible in the editor.
#[derive(Clone, Debug, Default)]
struct ManipulatorGroupOverlays {
pub anchor: Option<Vec<LayerId>>,
pub in_handle: Option<Vec<LayerId>>,
pub in_line: Option<Vec<LayerId>>,
pub out_handle: Option<Vec<LayerId>>,
pub out_line: Option<Vec<LayerId>>,
}
impl ManipulatorGroupOverlays {
pub fn iter(&self) -> impl Iterator<Item = &'_ Option<Vec<LayerId>>> {
[&self.anchor, &self.in_handle, &self.in_line, &self.out_handle, &self.out_line].into_iter()
}
}
type GraphiteManipulatorGroup = ManipulatorGroup<ManipulatorGroupId>;
const POINT_STROKE_WEIGHT: f64 = 2.;
#[derive(Clone, Debug, Default)]
pub struct OverlayRenderer {
shape_overlay_cache: HashMap<LayerNodeIdentifier, Vec<LayerId>>,
manipulator_group_overlay_cache: HashMap<LayerNodeIdentifier, HashMap<ManipulatorGroupId, ManipulatorGroupOverlays>>,
}
impl OverlayRenderer {
pub fn new() -> Self {
Self::default()
}
pub fn query_cache(&self, layer: &LayerNodeIdentifier) -> Option<&Vec<LayerId>> {
self.shape_overlay_cache.get(layer)
}
pub fn render_subpath_overlays(&mut self, selected_shape_state: &SelectedShapeState, document: &Document, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
let transform = document.metadata.transform_to_viewport(layer);
let Some(subpaths) = get_subpaths(layer, document) else {
return;
};
self.layer_overlay_visibility(document, layer, true, responses);
let outline_cache = self.shape_overlay_cache.get(&layer);
trace!("Overlay: Outline cache {outline_cache:?}");
// Create an outline if we do not have a cached one
if outline_cache.is_none() {
let outline_path = self.create_shape_outline_overlay(graphene_core::vector::Subpath::from_bezier_rs(subpaths), responses);
self.shape_overlay_cache.insert(layer, outline_path.clone());
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
trace!("Overlay: Creating new outline {outline_path:?}");
} else if let Some(outline_path) = outline_cache {
trace!("Overlay: Updating overlays for {outline_path:?} owning layer: {layer:?}");
Self::modify_outline_overlays(outline_path.clone(), graphene_core::vector::Subpath::from_bezier_rs(subpaths), responses);
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
}
// Create, place, and style the manipulator overlays
for manipulator_group in get_manipulator_groups(subpaths) {
let manipulator_group_cache = self.manipulator_group_overlay_cache.entry(layer).or_default().entry(manipulator_group.id).or_default();
// Only view in and out handles if they are not on top of the anchor
let [in_handle, out_handle] = {
let anchor = manipulator_group.anchor;
let anchor_position = transform.transform_point2(anchor);
let not_under_anchor = |&position: &DVec2| transform.transform_point2(position).distance_squared(anchor_position) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
let filter_handle = |manipulator: Option<DVec2>| manipulator.filter(not_under_anchor);
[filter_handle(manipulator_group.in_handle), filter_handle(manipulator_group.out_handle)]
};
// Create anchor
manipulator_group_cache.anchor = manipulator_group_cache.anchor.take().or_else(|| Some(Self::create_anchor_overlay(responses)));
// Create or delete in handle
if in_handle.is_none() {
Self::remove_overlay(manipulator_group_cache.in_handle.take(), responses);
Self::remove_overlay(manipulator_group_cache.in_line.take(), responses);
} else {
manipulator_group_cache.in_handle = manipulator_group_cache.in_handle.take().or_else(|| Self::create_handle_overlay_if_exists(in_handle, responses));
manipulator_group_cache.in_line = manipulator_group_cache.in_line.take().or_else(|| Self::create_handle_line_overlay_if_exists(in_handle, responses));
}
// Create or delete out handle
if out_handle.is_none() {
Self::remove_overlay(manipulator_group_cache.out_handle.take(), responses);
Self::remove_overlay(manipulator_group_cache.out_line.take(), responses);
} else {
manipulator_group_cache.out_handle = manipulator_group_cache.out_handle.take().or_else(|| Self::create_handle_overlay_if_exists(out_handle, responses));
manipulator_group_cache.out_line = manipulator_group_cache.out_line.take().or_else(|| Self::create_handle_line_overlay_if_exists(out_handle, responses));
}
// Update placement and style
Self::place_manipulator_group_overlays(manipulator_group, manipulator_group_cache, &transform, responses);
Self::style_overlays(selected_shape_state, layer, manipulator_group, manipulator_group_cache, responses);
}
if let Some(layer_overlays) = self.manipulator_group_overlay_cache.get_mut(&layer) {
if layer_overlays.len() > subpaths.iter().map(|subpath| subpath.len()).sum() {
layer_overlays.retain(|manipulator, manipulator_group_overlays| {
if get_manipulator_groups(subpaths).any(|current_manipulator| current_manipulator.id == *manipulator) {
true
} else {
Self::remove_manipulator_group_overlays(manipulator_group_overlays, responses);
false
}
});
}
}
// TODO Handle removing shapes from cache so we don't memory leak
// Eventually will get replaced with am immediate mode renderer for overlays
responses.add(OverlaysMessage::Rerender);
}
/// Delete all cached overlays
pub fn clear_all_overlays(&mut self, responses: &mut VecDeque<Message>) {
for (_, overlay_path) in self.shape_overlay_cache.drain() {
Self::remove_outline_overlays(overlay_path, responses)
}
for (_, layer_cache) in self.manipulator_group_overlay_cache.drain() {
for manipulator_group_overlays in layer_cache.values() {
Self::remove_manipulator_group_overlays(manipulator_group_overlays, responses);
}
}
}
pub fn layer_overlay_visibility(&mut self, document: &Document, layer: LayerNodeIdentifier, visibility: bool, responses: &mut VecDeque<Message>) {
// Hide the shape outline overlays
if let Some(overlay_path) = self.shape_overlay_cache.get(&layer) {
Self::set_outline_overlay_visibility(overlay_path.clone(), visibility, responses);
}
// Hide the manipulator group overlays
let Some(manipulator_groups) = self.manipulator_group_overlay_cache.get(&layer) else { return };
if visibility {
let Some(subpaths) = get_subpaths(layer, document) else { return };
for manipulator_group in get_manipulator_groups(subpaths) {
let id = manipulator_group.id;
if let Some(manipulator_group_overlays) = manipulator_groups.get(&id) {
Self::set_manipulator_group_overlay_visibility(manipulator_group_overlays, visibility, responses);
}
}
} else {
for manipulator_group_overlays in manipulator_groups.values() {
Self::set_manipulator_group_overlay_visibility(manipulator_group_overlays, visibility, responses);
}
}
}
/// Create the kurbo shape that matches the selected viewport shape.
fn create_shape_outline_overlay(&self, subpath: graphene_core::vector::Subpath, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddShape {
path: layer_path.clone(),
subpath,
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), PATH_OUTLINE_WEIGHT)), Fill::None),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
};
responses.add(DocumentMessage::Overlays(operation.into()));
layer_path
}
/// Create a single anchor overlay and return its layer ID.
fn create_anchor_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddRect {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
layer_path
}
/// Create a single handle overlay and return its layer ID.
fn create_handle_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddEllipse {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
layer_path
}
/// Create a single handle overlay and return its layer id if it exists.
fn create_handle_overlay_if_exists(handle: Option<DVec2>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
handle.map(|_| Self::create_handle_overlay(responses))
}
/// Remove an overlay at the specified path
fn remove_overlay(path: Option<Vec<LayerId>>, responses: &mut VecDeque<Message>) {
if let Some(path) = path {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()));
}
}
/// Create the shape outline overlay and return its layer ID.
fn create_handle_line_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddLine {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), Fill::None),
insert_index: -1,
};
responses.add_front(DocumentMessage::Overlays(operation.into()));
layer_path
}
/// Create the shape outline overlay and return its layer ID.
fn create_handle_line_overlay_if_exists(handle: Option<DVec2>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
handle.as_ref().map(|_| Self::create_handle_line_overlay(responses))
}
fn place_outline_overlays(outline_path: Vec<LayerId>, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
let transform_message = Self::overlay_transform_message(outline_path, parent_transform.to_cols_array());
responses.add(transform_message);
}
fn modify_outline_overlays(outline_path: Vec<LayerId>, subpath: graphene_core::vector::Subpath, responses: &mut VecDeque<Message>) {
let outline_modify_message = Self::overlay_modify_message(outline_path, subpath);
responses.add(outline_modify_message);
}
/// Updates the position of the overlays based on the [Subpath] points.
fn place_manipulator_group_overlays(manipulator_group: &GraphiteManipulatorGroup, overlays: &mut ManipulatorGroupOverlays, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
let anchor = manipulator_group.anchor;
let mut place_handle_and_line = |handle_position: DVec2, line_overlay: &[LayerId], marker_source: &mut Option<Vec<LayerId>>| {
let line_vector = parent_transform.transform_point2(anchor) - parent_transform.transform_point2(handle_position);
let scale = DVec2::splat(line_vector.length());
let angle = -line_vector.angle_between(DVec2::X);
let translation = (parent_transform.transform_point2(handle_position) + VIEWPORT_GRID_ROUNDING_BIAS).round() + DVec2::splat(0.5);
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.add(Self::overlay_transform_message(line_overlay.to_vec(), transform));
let marker_overlay = marker_source.take().unwrap_or_else(|| Self::create_handle_overlay(responses));
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let angle = 0.;
let translation = (parent_transform.transform_point2(handle_position) - (scale / 2.) + VIEWPORT_GRID_ROUNDING_BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.add(Self::overlay_transform_message(marker_overlay.clone(), transform));
*marker_source = Some(marker_overlay);
};
// Place the handle overlays
if let (Some(handle_position), Some(line_overlay)) = (manipulator_group.in_handle, overlays.in_line.as_mut()) {
place_handle_and_line(handle_position, line_overlay, &mut overlays.in_handle);
}
if let (Some(handle_position), Some(line_overlay)) = (manipulator_group.out_handle, overlays.out_line.as_ref()) {
place_handle_and_line(handle_position, line_overlay, &mut overlays.out_handle);
}
// Place the anchor point overlay
if let Some(anchor_overlay) = &overlays.anchor {
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let angle = 0.;
let translation = (parent_transform.transform_point2(anchor) - (scale / 2.) + VIEWPORT_GRID_ROUNDING_BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let message = Self::overlay_transform_message(anchor_overlay.clone(), transform);
responses.add(message);
}
}
/// Removes the manipulator overlays from the overlay document.
fn remove_manipulator_group_overlays(overlay_paths: &ManipulatorGroupOverlays, responses: &mut VecDeque<Message>) {
overlay_paths.iter().flatten().for_each(|layer_id| {
trace!("Overlay: Sending delete message for: {layer_id:?}");
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer_id.clone() }.into()));
});
}
fn remove_outline_overlays(overlay_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_path }.into()));
}
/// Sets the visibility of the handles overlay.
fn set_manipulator_group_overlay_visibility(manipulator_group_overlays: &ManipulatorGroupOverlays, visibility: bool, responses: &mut VecDeque<Message>) {
manipulator_group_overlays.iter().flatten().for_each(|layer_id| {
responses.add(Self::overlay_visibility_message(layer_id.clone(), visibility));
});
}
fn set_outline_overlay_visibility(overlay_path: Vec<LayerId>, visibility: bool, responses: &mut VecDeque<Message>) {
responses.add(Self::overlay_visibility_message(overlay_path, visibility));
}
/// Create a visibility message for an overlay.
fn overlay_visibility_message(layer_path: Vec<LayerId>, visibility: bool) -> Message {
DocumentMessage::Overlays(
Operation::SetLayerVisibility {
path: layer_path,
visible: visibility,
}
.into(),
)
.into()
}
/// Create a transform message for an overlay.
fn overlay_transform_message(layer_path: Vec<LayerId>, transform: [f64; 6]) -> Message {
DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path: layer_path, transform }.into()).into()
}
/// Create an update message for an overlay.
fn overlay_modify_message(layer_path: Vec<LayerId>, subpath: graphene_core::vector::Subpath) -> Message {
DocumentMessage::Overlays(Operation::SetShapePath { path: layer_path, subpath }.into()).into()
}
/// Sets the overlay style for this point.
fn style_overlays(state: &SelectedShapeState, layer: LayerNodeIdentifier, manipulator_group: &GraphiteManipulatorGroup, overlays: &ManipulatorGroupOverlays, responses: &mut VecDeque<Message>) {
// TODO Move the style definitions out of the Subpath, should be looked up from a stylesheet or similar
let selected_style = style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), POINT_STROKE_WEIGHT + 1.0)), Fill::solid(COLOR_ACCENT));
let deselected_style = style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), POINT_STROKE_WEIGHT)), Fill::solid(Color::WHITE));
let selected_shape_state = state.get(&layer);
// Update if the manipulator points are shown as selected
// Here the index is important, even though overlays has five elements we only care about the first three
for (index, overlay) in [&overlays.in_handle, &overlays.out_handle, &overlays.anchor].into_iter().enumerate() {
let selected_type = [SelectedType::InHandle, SelectedType::OutHandle, SelectedType::Anchor][index];
if let Some(overlay_path) = overlay {
let selected = selected_shape_state
.filter(|state| state.is_selected(ManipulatorPointId::new(manipulator_group.id, selected_type)))
.is_some();
let style = if selected { selected_style.clone() } else { deselected_style.clone() };
responses.add(DocumentMessage::Overlays(Operation::SetLayerStyle { path: overlay_path.clone(), style }.into()));
}
}
}
}
@@ -1,129 +0,0 @@
use crate::application::generate_uuid;
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT};
use crate::messages::prelude::*;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::style::{self, Fill, Stroke};
use document_legacy::{LayerId, Operation};
use glam::DAffine2;
/// 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<LayerNodeIdentifier>,
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 try_create_outline(layer: LayerNodeIdentifier, overlay_path: Option<Vec<LayerId>>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
let subpath = document.metadata().layer_outline(layer);
let transform = document.metadata().transform_to_viewport(layer);
// Generate a new overlay layer if necessary
let overlay = overlay_path.unwrap_or_else(|| {
let overlay_path = vec![generate_uuid()];
responses.add(DocumentMessage::Overlays(
(Operation::AddShape {
path: overlay_path.clone(),
subpath: Default::default(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), PATH_OUTLINE_WEIGHT)), Fill::None),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
})
.into(),
));
overlay_path
});
// Update the shape subpath
responses.add(DocumentMessage::Overlays((Operation::SetShapePath { path: overlay.clone(), subpath }).into()));
// Update the transform to match the document
responses.add(DocumentMessage::Overlays(
(Operation::SetLayerTransform {
path: overlay.clone(),
transform: transform.to_cols_array(),
})
.into(),
));
Some(overlay)
}
/// Creates an outline of a layer either with a pre-existing overlay or by generating a new one.
///
/// Creates an outline, discarding the overlay on failure.
fn create_outline(layer: LayerNodeIdentifier, overlay_path: Option<Vec<LayerId>>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
let copied_overlay_path = overlay_path.clone();
let result = Self::try_create_outline(layer, overlay_path, document, responses);
if result.is_none() {
// Discard the overlay layer if it exists
if let Some(overlay_path) = copied_overlay_path {
let operation = Operation::DeleteLayer { path: overlay_path };
responses.add(DocumentMessage::Overlays(operation.into()));
}
}
result
}
/// 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.add(DocumentMessage::Overlays(operation.into()));
}
self.hovered_layer_path = None;
}
/// Performs an intersect test and generates a hovered overlay if necessary
pub fn intersect_test_hovered(&mut self, input: &InputPreprocessorMessageHandler, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
// Get the layer the user is hovering over
let intersection = document.document_legacy.click(input.mouse.position, &document.document_legacy.document_network);
let Some(hovered_layer) = intersection else {
self.clear_hovered(responses);
return;
};
if document.metadata().selected_layers_contains(hovered_layer) {
self.clear_hovered(responses);
return;
}
// Updates the overlay, generating a new one if necessary
self.hovered_overlay_path = Self::create_outline(hovered_layer, self.hovered_overlay_path.take(), document, responses);
if self.hovered_overlay_path.is_none() {
self.clear_hovered(responses);
}
self.hovered_layer_path = Some(hovered_layer);
}
/// Clears overlays for the selected paths and removes references
pub fn clear_selected(&mut self, responses: &mut VecDeque<Message>) {
while let Some(path) = self.selected_overlay_paths.pop() {
let operation = Operation::DeleteLayer { path };
responses.add(DocumentMessage::Overlays(operation.into()));
}
}
/// Updates the selected overlays, generating or removing overlays if necessary
pub fn update_selected(&mut self, selected: impl Iterator<Item = LayerNodeIdentifier>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let mut old_overlay_paths = std::mem::take(&mut self.selected_overlay_paths);
for layer_identifier in selected {
if let Some(overlay_path) = Self::create_outline(layer_identifier, 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.add(DocumentMessage::Overlays(operation.into()));
}
}
}
@@ -1,19 +1,16 @@
//! Handler for the pivot overlay visible on the selected layer(s) whilst using the Select tool which controls the center of rotation/scale and origin of the layer.
use crate::application::generate_uuid;
use crate::consts::{COLOR_ACCENT, PIVOT_INNER, PIVOT_OUTER, PIVOT_OUTER_OUTLINE_THICKNESS};
use super::graph_modification_utils;
use crate::consts::PIVOT_OUTER;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::prelude::*;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::style;
use document_legacy::{LayerId, Operation};
use glam::{DAffine2, DVec2};
use std::collections::VecDeque;
use super::graph_modification_utils;
#[derive(Clone, Debug)]
pub struct Pivot {
/// Pivot between (0,0) and (1,1)
@@ -22,8 +19,6 @@ pub struct Pivot {
transform_from_normalized: DAffine2,
/// The viewspace pivot position (if applicable)
pivot: Option<DVec2>,
/// A reference to the previous overlays so we can destroy them
pivot_overlay_circles: Option<[Vec<LayerId>; 2]>,
/// The old pivot position in the GUI, used to reduce refreshes of the document bar
old_pivot_position: PivotPosition,
}
@@ -34,7 +29,6 @@ impl Default for Pivot {
normalized_pivot: DVec2::splat(0.5),
transform_from_normalized: Default::default(),
pivot: Default::default(),
pivot_overlay_circles: Default::default(),
old_pivot_position: PivotPosition::Center,
}
}
@@ -87,59 +81,11 @@ impl Pivot {
}
}
pub fn clear_overlays(&mut self, responses: &mut VecDeque<Message>) {
if let Some(overlays) = self.pivot_overlay_circles.take() {
for path in overlays {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()));
}
}
}
fn redraw_pivot(&mut self, responses: &mut VecDeque<Message>) {
self.clear_overlays(responses);
let pivot = match self.pivot {
Some(pivot) => pivot,
None => return,
};
let layer_paths = [vec![generate_uuid()], vec![generate_uuid()]];
responses.add(DocumentMessage::Overlays(
Operation::AddEllipse {
path: layer_paths[0].clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(
Some(style::Stroke::new(Some(COLOR_ACCENT), PIVOT_OUTER_OUTLINE_THICKNESS)),
style::Fill::Solid(graphene_core::raster::color::Color::WHITE),
),
insert_index: -1,
}
.into(),
));
responses.add(DocumentMessage::Overlays(
Operation::AddEllipse {
path: layer_paths[1].clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)),
insert_index: -1,
}
.into(),
));
self.pivot_overlay_circles = Some(layer_paths.clone());
let [outer, inner] = layer_paths;
let pivot_diameter_without_outline = PIVOT_OUTER - PIVOT_OUTER_OUTLINE_THICKNESS;
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(pivot_diameter_without_outline), 0., pivot - DVec2::splat(pivot_diameter_without_outline / 2.)).to_cols_array();
responses.add(DocumentMessage::Overlays(Operation::TransformLayerInViewport { path: outer, transform }.into()));
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(PIVOT_INNER), 0., pivot - DVec2::splat(PIVOT_INNER / 2.)).to_cols_array();
responses.add(DocumentMessage::Overlays(Operation::TransformLayerInViewport { path: inner, transform }.into()));
}
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
self.recalculate_pivot(document);
self.redraw_pivot(responses);
if let Some(pivot) = self.pivot {
overlay_context.pivot(pivot);
}
}
/// Answers if the pivot widget has changed (so we should refresh the tool bar at the top of the canvas).
@@ -1,169 +1,19 @@
use super::shape_editor::ManipulatorPointInfo;
use crate::application::generate_uuid;
use crate::consts::{
COLOR_ACCENT, SNAP_AXIS_OVERLAY_FADE_DISTANCE, SNAP_AXIS_TOLERANCE, SNAP_AXIS_UNSNAPPED_OPACITY, SNAP_POINT_OVERLAY_FADE_FAR, SNAP_POINT_OVERLAY_FADE_NEAR, SNAP_POINT_SIZE, SNAP_POINT_TOLERANCE,
SNAP_POINT_UNSNAPPED_OPACITY,
};
use crate::consts::{SNAP_AXIS_TOLERANCE, SNAP_POINT_TOLERANCE};
use crate::messages::prelude::*;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::layer_info::LegacyLayer;
use document_legacy::layers::style::{self, Stroke};
use document_legacy::{LayerId, Operation};
use document_legacy::LayerId;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use glam::{DAffine2, DVec2};
use std::f64::consts::PI;
// Handles snap overlays
#[derive(Debug, Clone, Default)]
struct SnapOverlays {
axis_overlay_paths: Vec<Vec<LayerId>>,
point_overlay_paths: Vec<Vec<LayerId>>,
axis_index: usize,
point_index: usize,
}
impl SnapOverlays {
/// Draws an overlay (axis or point) with the correct transform and fade opacity, reusing lines from the pool if available.
fn add_overlay(is_axis: bool, responses: &mut VecDeque<Message>, transform: [f64; 6], opacity: Option<f64>, index: usize, overlay_paths: &mut Vec<Vec<LayerId>>) {
// If there isn't one in the pool to ruse, add a new alignment line to the pool with the intended transform
let layer_path = if index >= overlay_paths.len() {
let layer_path = vec![generate_uuid()];
responses.add(DocumentMessage::Overlays(
if is_axis {
Operation::AddLine {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), style::Fill::None),
insert_index: -1,
}
} else {
Operation::AddEllipse {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)),
insert_index: -1,
}
}
.into(),
));
overlay_paths.push(layer_path.clone());
layer_path
}
// Otherwise, reuse an overlay from the pool and update its new transform
else {
let layer_path = overlay_paths[index].clone();
responses.add(DocumentMessage::Overlays(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into()));
layer_path
};
// Then set its opacity to the fade amount
if let Some(opacity) = opacity {
responses.add(DocumentMessage::Overlays(Operation::SetLayerOpacity { path: layer_path, opacity }.into()));
}
}
/// Draw the alignment lines for an axis
/// Note: horizontal refers to the overlay line being horizontal and the snap being along the Y axis
fn draw_alignment_lines(&mut self, is_horizontal: bool, distances: impl Iterator<Item = (DVec2, DVec2, f64)>, responses: &mut VecDeque<Message>, closest_distance: DVec2) {
for (target, goal, distance) in distances.filter(|(_target, _pos, dist)| dist.abs() < SNAP_AXIS_OVERLAY_FADE_DISTANCE) {
let offset = if is_horizontal { target.y } else { target.x }.round() - 0.5;
let offset_other = if is_horizontal { target.x } else { target.y }.round() - 0.5;
let goal_axis = if is_horizontal { goal.x } else { goal.y }.round() - 0.5;
let scale = DVec2::new(offset_other - goal_axis, 1.);
let angle = if is_horizontal { 0. } else { PI / 2. };
let translation = if is_horizontal { DVec2::new(goal_axis, offset) } else { DVec2::new(offset, goal_axis) };
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let closest = if is_horizontal { closest_distance.y } else { closest_distance.x };
let opacity = if (closest - distance).abs() < 1. {
1.
} else {
SNAP_AXIS_UNSNAPPED_OPACITY - distance.abs() / (SNAP_AXIS_OVERLAY_FADE_DISTANCE / SNAP_AXIS_UNSNAPPED_OPACITY)
};
// Add line
Self::add_overlay(true, responses, transform, Some(opacity), self.axis_index, &mut self.axis_overlay_paths);
self.axis_index += 1;
let size = DVec2::splat(SNAP_POINT_SIZE);
// Add point at target
let transform = DAffine2::from_scale_angle_translation(size, 0., target - size / 2.).to_cols_array();
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
self.point_index += 1;
// Add point along line but towards goal
let translation = if is_horizontal { DVec2::new(goal.x, target.y) } else { DVec2::new(target.x, goal.y) };
let transform = DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array();
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
self.point_index += 1
}
}
/// Draw the snap points
fn draw_snap_points(&mut self, distances: impl Iterator<Item = (DVec2, DVec2, f64)>, responses: &mut VecDeque<Message>, closest_distance: DVec2) {
for (target, offset, distance) in distances.filter(|(_pos, _offset, dist)| dist.abs() < SNAP_POINT_OVERLAY_FADE_FAR) {
let active = (closest_distance - offset).length_squared() < 1.;
if active {
continue;
}
let opacity = (1. - (distance - SNAP_POINT_OVERLAY_FADE_NEAR) / (SNAP_POINT_OVERLAY_FADE_FAR - SNAP_POINT_OVERLAY_FADE_NEAR)).min(1.) / SNAP_POINT_UNSNAPPED_OPACITY;
let size = DVec2::splat(SNAP_POINT_SIZE);
let transform = DAffine2::from_scale_angle_translation(size, 0., target - size / 2.).to_cols_array();
Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths);
self.point_index += 1
}
}
/// Updates the snapping overlays with the specified distances.
/// `positions_and_distances` is a tuple of `x`, `y` & `point` iterators,, each with `(position, goal, distance)` values.
fn update_overlays<X, Y, P>(&mut self, responses: &mut VecDeque<Message>, positions_and_distances: (X, Y, P), closest_distance: DVec2, snapped_to_point: bool)
where
X: Iterator<Item = (DVec2, DVec2, f64)>,
Y: Iterator<Item = (DVec2, DVec2, f64)>,
P: Iterator<Item = (DVec2, DVec2, f64)>,
{
self.axis_index = 0;
self.point_index = 0;
let (x, y, points) = positions_and_distances;
if !snapped_to_point {
self.draw_alignment_lines(true, y, responses, closest_distance);
self.draw_alignment_lines(false, x, responses, closest_distance);
self.draw_snap_points(points, responses, closest_distance);
}
Self::remove_unused_overlays(&mut self.axis_overlay_paths, responses, self.axis_index);
Self::remove_unused_overlays(&mut self.point_overlay_paths, responses, self.point_index);
}
/// Remove overlays from the pool beyond a given index. Pool entries up through that index will be kept.
fn remove_unused_overlays(overlay_paths: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Message>, remove_after_index: usize) {
while overlay_paths.len() > remove_after_index {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_paths.pop().unwrap() }.into()));
}
}
/// Deletes all overlays
fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
Self::remove_unused_overlays(&mut self.axis_overlay_paths, responses, 0);
Self::remove_unused_overlays(&mut self.point_overlay_paths, responses, 0);
}
}
use glam::DVec2;
/// Handles snapping and snap overlays
#[derive(Debug, Clone, Default)]
pub struct SnapManager {
point_targets: Option<Vec<DVec2>>,
bound_targets: Option<Vec<DVec2>>,
snap_overlays: SnapOverlays,
snap_x: bool,
snap_y: bool,
}
@@ -193,7 +43,7 @@ impl SnapManager {
let min_points = points.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
// Snap to a point if possible
let (clamped_closest_distance, snapped_to_point) = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) {
let (clamped_closest_distance, _snapped_to_point) = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) {
(min_points.1, true)
} else {
// Do not move if over snap tolerance
@@ -206,8 +56,7 @@ impl SnapManager {
false,
)
};
self.snap_overlays.update_overlays(responses, (x_axis, y_axis, points), clamped_closest_distance, snapped_to_point);
responses.add(OverlaysMessage::Draw);
clamped_closest_distance
}
@@ -336,9 +185,9 @@ impl SnapManager {
/// Removes snap target data and overlays. Call this when snapping is done.
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
self.snap_overlays.cleanup(responses);
self.bound_targets = None;
self.point_targets = None;
responses.add(OverlaysMessage::Draw);
}
}
@@ -1,13 +1,10 @@
use crate::application::generate_uuid;
use crate::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_ACCENT, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_DRAG_ANGLE};
use crate::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, SELECTION_DRAG_ANGLE};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::transformation::OriginalTransforms;
use crate::messages::prelude::*;
use document_legacy::layers::style::{self, Fill, Stroke};
use document_legacy::LayerId;
use document_legacy::Operation;
use graphene_core::raster::color::Color;
use graphene_core::renderer::Quad;
use glam::{DAffine2, DVec2};
@@ -149,71 +146,6 @@ impl SelectedEdges {
}
}
/// Create a viewport relative bounding box overlay with no transform handles
pub fn add_bounding_box(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let path = vec![generate_uuid()];
let operation = Operation::AddRect {
path: path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 1.0)), Fill::None),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
path
}
/// Update the location of a bounding box with no handles
pub fn update_bounding_box(pos1: DVec2, pos2: DVec2, layer: &Option<Vec<LayerId>>, responses: &mut VecDeque<Message>) {
if let Some(path) = layer.as_ref().cloned() {
let transform = transform_from_box(pos1, pos2, DAffine2::IDENTITY).to_cols_array();
let operation = Operation::SetLayerTransformInViewport { path, transform };
responses.add_front(DocumentMessage::Overlays(operation.into()));
}
}
/// Removes the bounding box overlay with no transform handles
pub fn remove_bounding_box(layer_path: Option<Vec<LayerId>>, responses: &mut VecDeque<Message>) {
if let Some(path) = layer_path {
let operation = Operation::DeleteLayer { path };
responses.add(DocumentMessage::Overlays(operation.into()));
}
}
/// Add the transform handle overlay
fn add_transform_handles(responses: &mut VecDeque<Message>) -> [Vec<LayerId>; 8] {
const EMPTY_VEC: Vec<LayerId> = Vec::new();
let mut transform_handle_paths = [EMPTY_VEC; 8];
for item in &mut transform_handle_paths {
let current_path = vec![generate_uuid()];
let operation = Operation::AddRect {
path: current_path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Some(COLOR_ACCENT), 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.add(DocumentMessage::Overlays(operation.into()));
*item = current_path;
}
transform_handle_paths
}
/// Converts a bounding box to a rounded transform (with translation and scale)
pub fn transform_from_box(pos1: DVec2, pos2: DVec2, transform: DAffine2) -> DAffine2 {
let inverse = transform.inverse();
transform
* DAffine2::from_scale_angle_translation(
inverse.transform_vector2(transform.transform_vector2(pos2 - pos1).round()),
0.,
inverse.transform_point2(transform.transform_point2(pos1).round() - DVec2::splat(0.5)),
)
}
/// Aligns the mouse position to the closest axis
pub fn axis_align_drag(axis_align: bool, position: DVec2, start: DVec2) -> DVec2 {
if axis_align {
@@ -229,27 +161,17 @@ pub fn axis_align_drag(axis_align: bool, position: DVec2, start: DVec2) -> DVec2
/// Contains info on the overlays for the bounding box and transform handles
#[derive(Clone, Debug, Default)]
pub struct BoundingBoxOverlays {
pub bounding_box: Vec<LayerId>,
pub transform_handles: [Vec<LayerId>; 8],
pub struct BoundingBoxManager {
pub bounds: [DVec2; 2],
pub transform: DAffine2,
pub original_bound_transform: DAffine2,
pub selected_edges: Option<SelectedEdges>,
pub original_transforms: OriginalTransforms,
pub opposite_pivot: DVec2,
pub center_of_transformation: DVec2,
}
impl BoundingBoxOverlays {
#[must_use]
pub fn new(responses: &mut VecDeque<Message>) -> Self {
Self {
bounding_box: add_bounding_box(responses),
transform_handles: add_transform_handles(responses),
..Default::default()
}
}
impl BoundingBoxManager {
/// Calculates the transformed handle positions based on the bounding box and the transform
pub fn evaluate_transform_handle_positions(&self) -> [DVec2; 8] {
let (left, top): (f64, f64) = self.bounds[0].into();
@@ -267,20 +189,11 @@ impl BoundingBoxOverlays {
}
/// Update the position of the bounding box and transform handles
pub fn transform(&mut self, responses: &mut VecDeque<Message>) {
let transform = transform_from_box(self.bounds[0], self.bounds[1], self.transform).to_cols_array();
let path = self.bounding_box.clone();
responses.add(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()));
pub fn render_overlays(&mut self, overlay_context: &mut OverlayContext) {
overlay_context.quad(self.transform * Quad::from_box(self.bounds));
// Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function
const BIAS: f64 = 0.0001;
for (position, path) in self.evaluate_transform_handle_positions().into_iter().zip(&self.transform_handles) {
let scale = DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE);
let translation = (position - (scale / 2.) - 0.5 + BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, 0., translation).to_cols_array();
let path = path.clone();
responses.add(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()));
for position in self.evaluate_transform_handle_positions() {
overlay_context.square(position, false);
}
}
@@ -355,14 +268,4 @@ impl BoundingBoxOverlays {
MouseCursorIcon::Default
}
}
/// Removes the overlays
pub fn delete(self, responses: &mut VecDeque<Message>) {
responses.add(DocumentMessage::Overlays(Operation::DeleteLayer { path: self.bounding_box }.into()));
responses.extend(
self.transform_handles
.iter()
.map(|path| DocumentMessage::Overlays(Operation::DeleteLayer { path: path.clone() }.into()).into()),
);
}
}