mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 18:08:12 +08:00
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:
committed by
Keavon Chambers
co-authored by
Dennis Kobert
Keavon Chambers
parent
e1cdb2242d
commit
5827e989dc
@@ -3,15 +3,17 @@ use graphene_core::renderer::ClickTarget;
|
||||
use std::collections::HashMap;
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use graph_craft::document::{NodeId, NodeNetwork};
|
||||
use graph_craft::document::{DocumentNode, NodeId, NodeNetwork};
|
||||
|
||||
use graphene_core::renderer::Quad;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DocumentMetadata {
|
||||
transforms: HashMap<LayerNodeIdentifier, DAffine2>,
|
||||
upstream_transforms: HashMap<NodeId, DAffine2>,
|
||||
structure: HashMap<LayerNodeIdentifier, NodeRelations>,
|
||||
click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
|
||||
selected_nodes: Vec<NodeId>,
|
||||
/// Transform from document space to viewport space.
|
||||
pub document_to_viewport: DAffine2,
|
||||
}
|
||||
@@ -20,13 +22,17 @@ impl Default for DocumentMetadata {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
transforms: HashMap::new(),
|
||||
upstream_transforms: HashMap::new(),
|
||||
click_targets: HashMap::new(),
|
||||
structure: HashMap::from_iter([(LayerNodeIdentifier::ROOT, NodeRelations::default())]),
|
||||
selected_nodes: Vec::new(),
|
||||
document_to_viewport: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct SelectionChanged;
|
||||
|
||||
// layer iters
|
||||
impl DocumentMetadata {
|
||||
/// Get the root layer from the document
|
||||
pub const fn root(&self) -> LayerNodeIdentifier {
|
||||
@@ -38,7 +44,7 @@ impl DocumentMetadata {
|
||||
}
|
||||
|
||||
pub fn selected_layers(&self) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
|
||||
self.all_layers()
|
||||
self.all_layers().filter(|layer| self.selected_nodes.contains(&layer.to_node()))
|
||||
}
|
||||
|
||||
pub fn selected_layers_contains(&self, layer: LayerNodeIdentifier) -> bool {
|
||||
@@ -46,7 +52,19 @@ impl DocumentMetadata {
|
||||
}
|
||||
|
||||
pub fn selected_visible_layers(&self) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
|
||||
self.all_layers()
|
||||
self.selected_layers()
|
||||
}
|
||||
|
||||
pub fn selected_nodes(&self) -> core::slice::Iter<'_, NodeId> {
|
||||
self.selected_nodes.iter()
|
||||
}
|
||||
|
||||
pub fn selected_nodes_ref(&self) -> &Vec<NodeId> {
|
||||
&self.selected_nodes
|
||||
}
|
||||
|
||||
pub fn has_selected_nodes(&self) -> bool {
|
||||
!self.selected_nodes.is_empty()
|
||||
}
|
||||
|
||||
/// Access the [`NodeRelations`] of a layer
|
||||
@@ -59,35 +77,125 @@ impl DocumentMetadata {
|
||||
self.structure.entry(node_identifier).or_default()
|
||||
}
|
||||
|
||||
/// Update the cached transforms of the layers
|
||||
pub fn update_transforms(&mut self, new_transforms: HashMap<LayerNodeIdentifier, DAffine2>) {
|
||||
self.transforms = new_transforms;
|
||||
pub fn shallowest_unique_layers<'a>(&self, layers: impl Iterator<Item = &'a LayerNodeIdentifier>) -> Vec<Vec<LayerNodeIdentifier>> {
|
||||
let mut sorted_layers = layers
|
||||
.map(|layer| {
|
||||
let mut layer_path = layer.ancestors(self).collect::<Vec<_>>();
|
||||
layer_path.reverse();
|
||||
layer_path
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
sorted_layers.sort();
|
||||
// Sorting here creates groups of similar UUID paths
|
||||
sorted_layers.dedup_by(|a, b| a.starts_with(b));
|
||||
sorted_layers
|
||||
}
|
||||
}
|
||||
|
||||
// selected layer modifications
|
||||
impl DocumentMetadata {
|
||||
#[must_use]
|
||||
pub fn retain_selected_nodes(&mut self, f: impl FnMut(&NodeId) -> bool) -> SelectionChanged {
|
||||
self.selected_nodes.retain(f);
|
||||
SelectionChanged
|
||||
}
|
||||
#[must_use]
|
||||
pub fn set_selected_nodes(&mut self, new: Vec<NodeId>) -> SelectionChanged {
|
||||
self.selected_nodes = new;
|
||||
SelectionChanged
|
||||
}
|
||||
#[must_use]
|
||||
pub fn add_selected_nodes(&mut self, iter: impl IntoIterator<Item = NodeId>) -> SelectionChanged {
|
||||
self.selected_nodes.extend(iter);
|
||||
SelectionChanged
|
||||
}
|
||||
#[must_use]
|
||||
pub fn clear_selected_nodes(&mut self) -> SelectionChanged {
|
||||
self.set_selected_nodes(Vec::new())
|
||||
}
|
||||
|
||||
/// Loads the structure of layer nodes from a node graph.
|
||||
pub fn load_structure(&mut self, graph: &NodeNetwork) {
|
||||
self.structure = HashMap::from_iter([(LayerNodeIdentifier::ROOT, NodeRelations::default())]);
|
||||
|
||||
let id = graph.outputs[0].node_id;
|
||||
let Some(output_node) = graph.nodes.get(&id) else {
|
||||
return;
|
||||
};
|
||||
let Some((layer_node, node_id)) = first_child_layer(graph, output_node, id) else {
|
||||
return;
|
||||
};
|
||||
let parent = LayerNodeIdentifier::ROOT;
|
||||
let mut stack = vec![(layer_node, node_id, parent)];
|
||||
while let Some((node, id, parent)) = stack.pop() {
|
||||
let mut current = Some((node, id));
|
||||
while let Some(&(current_node, current_id)) = current.as_ref() {
|
||||
let current_identifier = LayerNodeIdentifier::new_unchecked(current_id);
|
||||
if !self.structure.contains_key(¤t_identifier) {
|
||||
parent.push_child(self, current_identifier);
|
||||
|
||||
if let Some((child_node, child_id)) = first_child_layer(graph, current_node, current_id) {
|
||||
stack.push((child_node, child_id, current_identifier));
|
||||
}
|
||||
}
|
||||
|
||||
current = sibling_below(graph, current_node, current_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn first_child_layer<'a>(graph: &'a NodeNetwork, node: &DocumentNode, id: NodeId) -> Option<(&'a DocumentNode, NodeId)> {
|
||||
graph.primary_flow_from_opt(Some(node.inputs[0].as_node()?)).find(|(node, _)| node.name == "Layer")
|
||||
}
|
||||
fn sibling_below<'a>(graph: &'a NodeNetwork, node: &DocumentNode, id: NodeId) -> Option<(&'a DocumentNode, NodeId)> {
|
||||
node.inputs[7].as_node().and_then(|id| graph.nodes.get(&id).filter(|node| node.name == "Layer").map(|node| (node, id)))
|
||||
}
|
||||
|
||||
// transforms
|
||||
impl DocumentMetadata {
|
||||
/// Update the cached transforms of the layers
|
||||
pub fn update_transforms(&mut self, new_transforms: HashMap<LayerNodeIdentifier, DAffine2>, new_upstream_transforms: HashMap<NodeId, DAffine2>) {
|
||||
self.transforms = new_transforms;
|
||||
self.upstream_transforms = new_upstream_transforms;
|
||||
}
|
||||
|
||||
/// Access the cached transformation to document space from layer space
|
||||
pub fn transform_to_document(&self, layer: LayerNodeIdentifier) -> DAffine2 {
|
||||
self.transforms.get(&layer).copied().unwrap_or_else(|| {
|
||||
warn!("Tried to access transform of bad layer {layer:?}");
|
||||
DAffine2::IDENTITY
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transform_to_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 {
|
||||
self.document_to_viewport * self.transform_to_document(layer)
|
||||
}
|
||||
|
||||
pub fn upstream_transform(&self, node_id: NodeId) -> DAffine2 {
|
||||
self.upstream_transforms.get(&node_id).copied().unwrap_or(DAffine2::IDENTITY)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_artboard(layer: LayerNodeIdentifier, network: &NodeNetwork) -> bool {
|
||||
network.primary_flow_from_opt(Some(layer.to_node())).any(|(node, _)| node.name == "Artboard")
|
||||
}
|
||||
|
||||
// click targets
|
||||
impl DocumentMetadata {
|
||||
/// Update the cached click targets of the layers
|
||||
pub fn update_click_targets(&mut self, new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>) {
|
||||
self.click_targets = new_click_targets;
|
||||
}
|
||||
|
||||
/// Access the cached transformation from document space to layer space
|
||||
pub fn transform_from_document(&self, layer: LayerNodeIdentifier) -> DAffine2 {
|
||||
self.transforms.get(&layer).copied().unwrap_or_else(|| {
|
||||
warn!("Tried to access transform of bad layer");
|
||||
DAffine2::IDENTITY
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transform_from_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 {
|
||||
self.document_to_viewport * self.transform_from_document(layer)
|
||||
}
|
||||
|
||||
/// Runs an intersection test with all layers and a viewport space quad
|
||||
pub fn intersect_quad(&self, viewport_quad: Quad) -> Option<LayerNodeIdentifier> {
|
||||
pub fn intersect_quad<'a>(&'a self, viewport_quad: Quad, network: &'a NodeNetwork) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
|
||||
let document_quad = self.document_to_viewport.inverse() * viewport_quad;
|
||||
self.root()
|
||||
.decendants(self)
|
||||
.filter(|&layer| !is_artboard(layer, network))
|
||||
.filter_map(|layer| self.click_targets.get(&layer).map(|targets| (layer, targets)))
|
||||
.find(|(layer, target)| target.iter().any(|target| target.intersect_rectangle(document_quad, self.transform_from_document(*layer))))
|
||||
.filter(move |(layer, target)| target.iter().any(move |target| target.intersect_rectangle(document_quad, self.transform_to_document(*layer))))
|
||||
.map(|(layer, _)| layer)
|
||||
}
|
||||
|
||||
@@ -97,13 +205,13 @@ impl DocumentMetadata {
|
||||
self.root()
|
||||
.decendants(self)
|
||||
.filter_map(|layer| self.click_targets.get(&layer).map(|targets| (layer, targets)))
|
||||
.filter(move |(layer, target)| target.iter().any(|target: &ClickTarget| target.intersect_point(point, self.transform_from_document(*layer))))
|
||||
.filter(move |(layer, target)| target.iter().any(|target: &ClickTarget| target.intersect_point(point, self.transform_to_document(*layer))))
|
||||
.map(|(layer, _)| layer)
|
||||
}
|
||||
|
||||
/// Find the layer that has been clicked on from a viewport space location
|
||||
pub fn click(&self, viewport_location: DVec2) -> Option<LayerNodeIdentifier> {
|
||||
self.click_xray(viewport_location).next()
|
||||
pub fn click(&self, viewport_location: DVec2, network: &NodeNetwork) -> Option<LayerNodeIdentifier> {
|
||||
self.click_xray(viewport_location).filter(|&layer| !is_artboard(layer, network)).next()
|
||||
}
|
||||
|
||||
/// Get the bounding box of the click target of the specified layer in the specified transform space
|
||||
@@ -115,14 +223,47 @@ impl DocumentMetadata {
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
/// Calculate the corners of the bounding box but with a nonzero size.
|
||||
///
|
||||
/// If the layer bounds are `0` in either axis then they are changed to be `1`.
|
||||
pub fn nonzero_bounding_box(&self, layer: LayerNodeIdentifier) -> [DVec2; 2] {
|
||||
let [bounds_min, mut bounds_max] = self.bounding_box_with_transform(layer, DAffine2::IDENTITY).unwrap_or_default();
|
||||
|
||||
let bounds_size = bounds_max - bounds_min;
|
||||
if bounds_size.x < 1e-10 {
|
||||
bounds_max.x = bounds_min.x + 1.;
|
||||
}
|
||||
if bounds_size.y < 1e-10 {
|
||||
bounds_max.y = bounds_min.y + 1.;
|
||||
}
|
||||
|
||||
[bounds_min, bounds_max]
|
||||
}
|
||||
|
||||
/// Get the bounding box of the click target of the specified layer in document space
|
||||
pub fn bounding_box_document(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box_with_transform(layer, self.transform_from_document(layer))
|
||||
self.bounding_box_with_transform(layer, self.transform_to_document(layer))
|
||||
}
|
||||
|
||||
/// Get the bounding box of the click target of the specified layer in viewport space
|
||||
pub fn bounding_box_viewport(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box_with_transform(layer, self.transform_from_viewport(layer))
|
||||
self.bounding_box_with_transform(layer, self.transform_to_viewport(layer))
|
||||
}
|
||||
|
||||
pub fn selected_visible_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
|
||||
self.selected_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
/// Calculates the document bounds used for scrolling and centring (the layer bounds or the artboard (if applicable))
|
||||
pub fn document_bounds(&self) -> Option<[DVec2; 2]> {
|
||||
self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> graphene_core::vector::Subpath {
|
||||
let Some(click_targets) = self.click_targets.get(&layer) else {
|
||||
return graphene_core::vector::Subpath::new();
|
||||
};
|
||||
graphene_core::vector::Subpath::from_bezier_rs(click_targets.iter().map(|click_target| &click_target.subpath))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +383,7 @@ impl LayerNodeIdentifier {
|
||||
pub fn decendants(self, document_metadata: &DocumentMetadata) -> DecendantsIter {
|
||||
DecendantsIter {
|
||||
front: self.first_child(document_metadata),
|
||||
back: self.last_child(document_metadata),
|
||||
back: self.last_child(document_metadata).and_then(|child| child.last_children(document_metadata).last()),
|
||||
document_metadata,
|
||||
}
|
||||
}
|
||||
@@ -339,6 +480,17 @@ impl LayerNodeIdentifier {
|
||||
pub fn exists(&self, document_metadata: &DocumentMetadata) -> bool {
|
||||
document_metadata.get_relations(*self).is_some()
|
||||
}
|
||||
|
||||
pub fn starts_with(&self, other: Self, document_metadata: &DocumentMetadata) -> bool {
|
||||
self.ancestors(document_metadata).any(|parent| parent == other)
|
||||
}
|
||||
|
||||
pub fn child_of_root(&self, document_metadata: &DocumentMetadata) -> Self {
|
||||
self.ancestors(document_metadata)
|
||||
.filter(|&layer| layer != LayerNodeIdentifier::ROOT)
|
||||
.last()
|
||||
.expect("There should be a layer before the root")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NodeId> for LayerNodeIdentifier {
|
||||
@@ -457,7 +609,8 @@ fn test_tree() {
|
||||
assert!(root.children(document_metadata).all(|child| child.parent(document_metadata) == Some(root)));
|
||||
LayerNodeIdentifier::new_unchecked(6).delete(document_metadata);
|
||||
LayerNodeIdentifier::new_unchecked(1).delete(document_metadata);
|
||||
LayerNodeIdentifier::new_unchecked(9).push_child(document_metadata, LayerNodeIdentifier::new_unchecked(10));
|
||||
assert_eq!(root.children(document_metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(), vec![2, 3, 4, 5, 9]);
|
||||
assert_eq!(root.decendants(document_metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(), vec![2, 3, 4, 5, 9]);
|
||||
assert_eq!(root.decendants(document_metadata).map(LayerNodeIdentifier::to_node).rev().collect::<Vec<_>>(), vec![9, 5, 4, 3, 2]);
|
||||
assert_eq!(root.decendants(document_metadata).map(LayerNodeIdentifier::to_node).collect::<Vec<_>>(), vec![2, 3, 4, 5, 9, 10]);
|
||||
assert_eq!(root.decendants(document_metadata).map(LayerNodeIdentifier::to_node).rev().collect::<Vec<_>>(), vec![10, 9, 5, 4, 3, 2]);
|
||||
}
|
||||
|
||||
@@ -32,11 +32,12 @@ impl LayerData for ShapeLayer {
|
||||
let layer_bounds = subpath.bounding_box().unwrap_or_default();
|
||||
|
||||
let transform = self.transform(transforms, render_data.view_mode);
|
||||
let inverse = transform.inverse();
|
||||
if !inverse.is_finite() {
|
||||
if !transform.is_finite() || transform.matrix2.determinant() == 0. {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return false;
|
||||
}
|
||||
let inverse = transform.inverse();
|
||||
|
||||
subpath.apply_affine(transform);
|
||||
|
||||
let transformed_bounds = subpath.bounding_box().unwrap_or_default();
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
extern crate log;
|
||||
|
||||
pub mod boolean_ops;
|
||||
/// Contains constant values used by this crate.
|
||||
pub mod consts;
|
||||
pub mod document;
|
||||
pub mod document_metadata;
|
||||
/// Defines errors that can occur when using this crate.
|
||||
pub mod error;
|
||||
/// Utilities for computing intersections.
|
||||
pub mod intersection;
|
||||
pub mod layers;
|
||||
pub mod operation;
|
||||
|
||||
@@ -11,9 +11,6 @@ pub enum DocumentResponse {
|
||||
FolderChanged {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
AddSelectedLayer {
|
||||
additional_layers: Vec<Vec<LayerId>>,
|
||||
},
|
||||
CreatedLayer {
|
||||
path: Vec<LayerId>,
|
||||
is_selected: bool,
|
||||
@@ -38,7 +35,6 @@ impl fmt::Display for DocumentResponse {
|
||||
match self {
|
||||
DocumentResponse::DocumentChanged { .. } => write!(f, "DocumentChanged"),
|
||||
DocumentResponse::FolderChanged { .. } => write!(f, "FolderChanged"),
|
||||
DocumentResponse::AddSelectedLayer { .. } => write!(f, "AddSelectedLayer"),
|
||||
DocumentResponse::CreatedLayer { .. } => write!(f, "CreatedLayer"),
|
||||
DocumentResponse::LayerChanged { .. } => write!(f, "LayerChanged"),
|
||||
DocumentResponse::DeletedLayer { .. } => write!(f, "DeleteLayer"),
|
||||
|
||||
Reference in New Issue
Block a user