Migrate the Select tool to the document graph (#1433)

* function for accessing document metadata

* Better select tool

* Fix render

* Fix transforms

* Fix loading saved documents

* Populate graph UI when loading autosave

* Multiple transform nodes

* Fix deep select

* Graph tooltips

* Fix flip axis icon

* Show disabled widgets

* Stop select tool from selecting artboards

* Disable (not hide) the pivot widget; remove Deep/Shallow select for now

* Code review changes

* Fix pivot position with select tool

* Fix incorrectly selected layers when shift clicking

---------

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2023-10-17 11:09:32 -07:00
committed by Keavon Chambers
co-authored by Dennis Kobert Keavon Chambers
parent e1cdb2242d
commit 5827e989dc
46 changed files with 1041 additions and 1215 deletions
@@ -9,15 +9,14 @@ use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::style::{FillType, Gradient};
use graphene_core::Color;
use glam::DAffine2;
use glam::{DAffine2, DVec2};
use std::collections::VecDeque;
/// Create a new vector layer from a vector of [`bezier_rs::Subpath`].
pub fn new_vector_layer(subpaths: Vec<Subpath<ManipulatorGroupId>>, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
responses.add(GraphOperationMessage::NewVectorLayer {
id: *layer_path.last().unwrap(),
subpaths,
});
let id = *layer_path.last().unwrap();
responses.add(GraphOperationMessage::NewVectorLayer { id, subpaths });
responses.add(NodeGraphMessage::SetSelectNodes { nodes: vec![id] })
}
/// Creat a new bitmap layer from an [`graphene_core::raster::ImageFrame<Color>`]
@@ -62,6 +61,25 @@ pub fn get_subpaths(layer: LayerNodeIdentifier, document: &Document) -> Option<&
}
}
/// Locate the final pivot from the transform (TODO: decide how the pivot should actually work)
pub fn get_pivot(layer: LayerNodeIdentifier, document: &Document) -> Option<DVec2> {
if let TaggedValue::DVec2(pivot) = NodeGraphLayer::new(layer, document)?.find_input("Transform", 5)? {
Some(*pivot)
} else {
None
}
}
pub fn get_document_pivot(layer: LayerNodeIdentifier, document: &Document) -> Option<DVec2> {
let [min, max] = document.metadata.nonzero_bounding_box(layer);
get_pivot(layer, document).map(|pivot| document.metadata.transform_to_document(layer).transform_point2(min + (max - min) * pivot))
}
pub fn get_viewport_pivot(layer: LayerNodeIdentifier, document: &Document) -> Option<DVec2> {
let [min, max] = document.metadata.nonzero_bounding_box(layer);
get_pivot(layer, document).map(|pivot| document.metadata.transform_to_viewport(layer).transform_point2(min + (max - min) * pivot))
}
/// Get the currently mirrored handles for a particular layer from the shape node
pub fn get_mirror_handles(layer: LayerNodeIdentifier, document: &Document) -> Option<&Vec<ManipulatorGroupId>> {
if let TaggedValue::ManipulatorGroupIds(mirror_handles) = NodeGraphLayer::new(layer, document)?.find_input("Shape", 1)? {
@@ -106,6 +124,16 @@ pub fn is_artboard(layer: LayerNodeIdentifier, document: &Document) -> bool {
NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.uses_node("Artboard"))
}
/// Is a specified layer a shape?
pub fn is_shape_layer(layer: LayerNodeIdentifier, document: &Document) -> bool {
NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.uses_node("Shape"))
}
/// Is a specified layer text?
pub fn is_text_layer(layer: LayerNodeIdentifier, document: &Document) -> bool {
NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.uses_node("Text"))
}
/// Convert subpaths to an iterator of manipulator groups
pub fn get_manipulator_groups(subpaths: &[Subpath<ManipulatorGroupId>]) -> impl Iterator<Item = &bezier_rs::ManipulatorGroup<ManipulatorGroupId>> + DoubleEndedIterator {
subpaths.iter().flat_map(|subpath| subpath.manipulator_groups())
@@ -51,7 +51,7 @@ impl OverlayRenderer {
}
pub fn render_subpath_overlays(&mut self, selected_shape_state: &SelectedShapeState, document: &Document, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
let transform = document.metadata.transform_from_viewport(layer);
let transform = document.metadata.transform_to_viewport(layer);
let Some(subpaths) = get_subpaths(layer, document) else {
return;
@@ -1,19 +1,17 @@
use crate::application::generate_uuid;
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT, SELECTION_TOLERANCE};
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT};
use crate::messages::prelude::*;
use document_legacy::intersection::Quad;
use document_legacy::layers::layer_info::LayerDataType;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::style::{self, Fill, RenderData, Stroke};
use document_legacy::{LayerId, Operation};
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DVec2};
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<Vec<LayerId>>,
hovered_layer_path: Option<LayerNodeIdentifier>,
hovered_overlay_path: Option<Vec<LayerId>>,
selected_overlay_paths: Vec<Vec<LayerId>>,
}
@@ -21,29 +19,14 @@ pub struct PathOutline {
impl PathOutline {
/// Creates an outline of a layer either with a pre-existing overlay or by generating a new one
fn try_create_outline(
document_layer_path: Vec<LayerId>,
layer: LayerNodeIdentifier,
overlay_path: Option<Vec<LayerId>>,
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
render_data: &RenderData,
) -> Option<Vec<LayerId>> {
// Get layer data
let document_layer = document.document_legacy.layer(&document_layer_path).ok()?;
// Get the subpath from the shape
let subpath = match &document_layer.data {
LayerDataType::Shape(shape) => Some(shape.shape.clone()),
LayerDataType::Layer(layer) => {
if let Some(vector_data) = layer.as_vector_data() {
// Vector graph output
Some(Subpath::from_bezier_rs(&vector_data.subpaths))
} else {
// Frame graph output
Some(Subpath::new_rect(DVec2::new(0., 0.), DVec2::new(1., 1.)))
}
}
_ => document_layer.aabb_for_transform(DAffine2::IDENTITY, render_data).map(|[p1, p2]| Subpath::new_rect(p1, p2)),
}?;
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(|| {
@@ -70,7 +53,7 @@ impl PathOutline {
responses.add(DocumentMessage::Overlays(
(Operation::SetLayerTransform {
path: overlay.clone(),
transform: document.document_legacy.multiply_transforms(&document_layer_path).unwrap().to_cols_array(),
transform: transform.to_cols_array(),
})
.into(),
));
@@ -82,14 +65,14 @@ impl PathOutline {
///
/// Creates an outline, discarding the overlay on failure.
fn create_outline(
document_layer_path: Vec<LayerId>,
layer: LayerNodeIdentifier,
overlay_path: Option<Vec<LayerId>>,
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
render_data: &RenderData,
) -> Option<Vec<LayerId>> {
let copied_overlay_path = overlay_path.clone();
let result = Self::try_create_outline(document_layer_path, overlay_path, document, responses, render_data);
let result = Self::try_create_outline(layer, overlay_path, document, responses, render_data);
if result.is_none() {
// Discard the overlay layer if it exists
if let Some(overlay_path) = copied_overlay_path {
@@ -112,26 +95,25 @@ impl PathOutline {
/// 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>, render_data: &RenderData) {
// 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.document_legacy.intersects_quad_root(quad, render_data);
let intersection = document.metadata().click(input.mouse.position, &document.document_legacy.document_network);
// 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()) {
// Updates the overlay, generating a new one if necessary
self.hovered_overlay_path = Self::create_outline(path.clone(), self.hovered_overlay_path.take(), document, responses, render_data);
if self.hovered_overlay_path.is_none() {
self.clear_hovered(responses);
}
let Some(hovered_layer) = intersection else {
self.clear_hovered(responses);
return;
};
self.hovered_layer_path = Some(path);
} else {
self.clear_hovered(responses);
}
} else {
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, render_data);
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
@@ -143,11 +125,11 @@ impl PathOutline {
}
/// 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>, render_data: &RenderData) {
pub fn update_selected<'a>(&mut self, selected: impl Iterator<Item = LayerNodeIdentifier>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, render_data: &RenderData) {
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, render_data) {
for layer_identifier in selected {
if let Some(overlay_path) = Self::create_outline(layer_identifier, old_overlay_paths.pop(), document, responses, render_data) {
self.selected_overlay_paths.push(overlay_path);
}
}
@@ -5,12 +5,15 @@ use crate::consts::{COLOR_ACCENT, PIVOT_INNER, PIVOT_OUTER, PIVOT_OUTER_OUTLINE_
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
use document_legacy::document_metadata::LayerNodeIdentifier;
use document_legacy::layers::style::{self, RenderData};
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)
@@ -39,54 +42,49 @@ impl Default for Pivot {
impl Pivot {
/// Calculates the transform that gets from normalized pivot to viewspace.
fn get_layer_pivot_transform(layer_path: &[LayerId], layer: &document_legacy::layers::layer_info::Layer, document: &DocumentMessageHandler, render_data: &RenderData) -> DAffine2 {
let [mut min, max] = layer.aabb_for_transform(DAffine2::IDENTITY, render_data).unwrap_or([DVec2::ZERO, DVec2::ONE]);
fn get_layer_pivot_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 {
let [min, max] = document.metadata().nonzero_bounding_box(layer);
// If the layer bounds are 0 in either axis then set them to one (to avoid div 0)
if (max.x - min.x) < f64::EPSILON * 1000. {
min.x = max.x - 1.;
}
if (max.y - min.y) < f64::EPSILON * 1000. {
min.y = max.y - 1.;
}
let bounds_transform = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
let layer_transform = document.document_legacy.multiply_transforms(layer_path).unwrap_or(DAffine2::IDENTITY);
let layer_transform = document.metadata().transform_to_viewport(layer);
layer_transform * bounds_transform
}
/// Recomputes the pivot position and transform.
fn recalculate_pivot(&mut self, document: &DocumentMessageHandler, render_data: &RenderData) {
let mut layers = document.selected_visible_layers();
if let Some(first) = layers.next() {
// Add one because the first item is consumed above.
let selected_layers_count = layers.count() + 1;
// If just one layer is selected we can use its inner transform
if selected_layers_count == 1 {
if let Ok(layer) = document.document_legacy.layer(first) {
self.normalized_pivot = layer.pivot;
self.transform_from_normalized = Self::get_layer_pivot_transform(first, layer, document, render_data);
self.pivot = Some(self.transform_from_normalized.transform_point2(layer.pivot));
}
} else {
// If more than one layer is selected we use the AABB with the mean of the pivots
let xy_summation = document
.selected_visible_layers()
.filter_map(|path| document.document_legacy.pivot(path, render_data))
.reduce(|a, b| a + b)
.unwrap_or_default();
let pivot = xy_summation / selected_layers_count as f64;
self.pivot = Some(pivot);
let [min, max] = document.selected_visible_layers_bounding_box(render_data).unwrap_or([DVec2::ZERO, DVec2::ONE]);
self.normalized_pivot = (pivot - min) / (max - min);
self.transform_from_normalized = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
}
} else {
let mut layers = document.metadata().selected_visible_layers();
let Some(first) = layers.next() else {
// If no layers are selected then we revert things back to default
self.normalized_pivot = DVec2::splat(0.5);
self.pivot = None;
return;
};
// Add one because the first item is consumed above.
let selected_layers_count = layers.count() + 1;
// If just one layer is selected we can use its inner transform (as it accounts for rotation)
if selected_layers_count == 1 {
if let Some(normalized_pivot) = graph_modification_utils::get_pivot(first, &document.document_legacy) {
self.normalized_pivot = normalized_pivot;
self.transform_from_normalized = Self::get_layer_pivot_transform(first, document);
self.pivot = Some(self.transform_from_normalized.transform_point2(normalized_pivot));
}
} else {
// If more than one layer is selected we use the AABB with the mean of the pivots
let xy_summation = document
.metadata()
.selected_visible_layers()
.filter_map(|layer| graph_modification_utils::get_viewport_pivot(layer, &document.document_legacy))
.reduce(|a, b| a + b)
.unwrap_or_default();
let pivot = xy_summation / selected_layers_count as f64;
self.pivot = Some(pivot);
let [min, max] = document.metadata().selected_visible_layers_bounding_box_viewport().unwrap_or([DVec2::ZERO, DVec2::ONE]);
self.normalized_pivot = (pivot - min) / (max - min);
self.transform_from_normalized = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
}
}
@@ -158,23 +156,21 @@ impl Pivot {
}
/// Sets the viewport position of the pivot for all selected layers.
pub fn set_viewport_position(&self, position: DVec2, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque<Message>) {
for layer_path in document.selected_visible_layers() {
if let Ok(layer) = document.document_legacy.layer(layer_path) {
let transform = Self::get_layer_pivot_transform(layer_path, layer, document, render_data);
let pivot = transform.inverse().transform_point2(position);
// Only update the pivot when computed position is finite. Infinite can happen when scale is 0.
if pivot.is_finite() {
let layer = layer_path.to_owned();
responses.add(GraphOperationMessage::TransformSetPivot { layer, pivot });
}
pub fn set_viewport_position(&self, position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
for layer in document.metadata().selected_visible_layers() {
let transform = Self::get_layer_pivot_transform(layer, document);
let pivot = transform.inverse().transform_point2(position);
// Only update the pivot when computed position is finite. Infinite can happen when scale is 0.
if pivot.is_finite() {
let layer = layer.to_path();
responses.add(GraphOperationMessage::TransformSetPivot { layer, pivot });
}
}
}
/// Set the pivot using the normalized transform that is set above.
pub fn set_normalized_position(&self, position: DVec2, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque<Message>) {
self.set_viewport_position(self.transform_from_normalized.transform_point2(position), document, render_data, responses);
pub fn set_normalized_position(&self, position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.set_viewport_position(self.transform_from_normalized.transform_point2(position), document, responses);
}
/// Answers if the pointer is currently positioned over the pivot.
@@ -20,7 +20,7 @@ impl Resize {
pub fn start(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, render_data: &RenderData) {
self.snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
self.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
let root_transform = document.document_legacy.metadata.document_to_viewport;
let root_transform = document.metadata().document_to_viewport;
self.drag_start = root_transform.inverse().transform_point2(self.snap_manager.snap_position(responses, document, input.mouse.position));
}
@@ -32,7 +32,7 @@ impl Resize {
/// Calculate the drag start position in viewport space.
pub fn viewport_drag_start(&self, document: &DocumentMessageHandler) -> DVec2 {
let root_transform = document.document_legacy.metadata.document_to_viewport;
let root_transform = document.metadata().document_to_viewport;
root_transform.transform_point2(self.drag_start)
}
@@ -95,7 +95,7 @@ impl ShapeState {
selected_shape_state.select_point(manipulator_point_id);
// Offset to snap the selected point to the cursor
let offset = mouse_position - document.metadata.transform_from_viewport(layer).transform_point2(point_position);
let offset = mouse_position - document.metadata.transform_to_viewport(layer).transform_point2(point_position);
let points = self
.selected_shape_state
@@ -116,7 +116,7 @@ impl ShapeState {
pub fn select_all_points(&mut self, document: &Document) {
for (layer, state) in self.selected_shape_state.iter_mut() {
let Some(subpaths) = get_subpaths(*layer, document) else { return };
let Some(subpaths) = get_subpaths(*layer, document) else { return };
for manipulator in get_manipulator_groups(subpaths) {
state.select_point(ManipulatorPointId::new(manipulator.id, SelectedType::Anchor));
for selected_type in &[SelectedType::InHandle, SelectedType::OutHandle] {
@@ -367,7 +367,7 @@ impl ShapeState {
let Some(subpaths) = get_subpaths(layer, document) else { continue };
let Some(mirror_angle) = get_mirror_handles(layer, document) else { continue };
let transform = document.metadata.transform_from_viewport(layer);
let transform = document.metadata.transform_to_viewport(layer);
let delta = transform.inverse().transform_vector2(delta);
for &point in state.selected_points.iter() {
@@ -435,7 +435,7 @@ impl ShapeState {
let opposing_handle_lengths = opposing_handle_lengths.as_ref().and_then(|lengths| lengths.get(&layer));
let transform = document.metadata.transform_from_viewport(layer);
let transform = document.metadata.transform_to_viewport(layer);
for &point in state.selected_points.iter() {
let anchor = ManipulatorPointId::new(point.group, SelectedType::Anchor);
@@ -649,7 +649,7 @@ impl ShapeState {
let mut result = None;
let subpaths = get_subpaths(layer, document)?;
let viewspace = document.metadata.transform_from_viewport(layer);
let viewspace = document.metadata.transform_to_viewport(layer);
for manipulator in get_manipulator_groups(subpaths) {
let (selected, distance_squared) = SelectedType::closest_widget(manipulator, viewspace, pos, crate::consts::HIDE_HANDLE_DISTANCE);
@@ -664,7 +664,7 @@ impl ShapeState {
/// Find the `t` value along the path segment we have clicked upon, together with that segment ID.
fn closest_segment(&self, document: &Document, layer: LayerNodeIdentifier, position: glam::DVec2, tolerance: f64) -> Option<(ManipulatorGroupId, ManipulatorGroupId, Bezier, f64)> {
let transform = document.metadata.transform_from_viewport(layer);
let transform = document.metadata.transform_to_viewport(layer);
let layer_pos = transform.inverse().transform_point2(position);
let projection_options = bezier_rs::ProjectionOptions { lut_size: 5, ..Default::default() };
@@ -735,7 +735,7 @@ impl ShapeState {
let mut process_layer = |layer| {
let subpaths = get_subpaths(layer, document)?;
let transform_to_screenspace = document.metadata.transform_from_viewport(layer);
let transform_to_screenspace = document.metadata.transform_to_viewport(layer);
let mut result = None;
let mut closest_distance_squared = tolerance * tolerance;
@@ -803,7 +803,7 @@ impl ShapeState {
let Some(subpaths) = get_subpaths(layer, document) else { continue };
let transform = document.metadata.transform_from_viewport(layer);
let transform = document.metadata.transform_to_viewport(layer);
for manipulator_group in get_manipulator_groups(subpaths) {
for selected_type in [SelectedType::Anchor, SelectedType::InHandle, SelectedType::OutHandle] {