Generalize layers as merge nodes to enable adjustment layers (#1712)

* WIP, backward traversal issues

* Fix some tool issues

* Remove debugging

* Change some indices

* WIP: new artboard node

* WIP: add artboard node

* WIP: Artboard node and create_artboard

* WIP: Artboard node implementation complete

* WIP: Artboards input for output node

* Complete Artboard node

* Generalize LayerNodeIdentifier,  monitor_nodes support for Artboard node, adjust ResizeArtboard/ClearArtboards, move alias validation to Rust

* Fix misaligned artboard click targets

* Generalize/clarify create_layer and insert_between

* non-negative dimensions for resize_artboard

* Show artboards in layer panel

* Generalize create_layer for layer output node

* Generalize delete_layer/delete_artboard to NodeGraphMessage::DeleteNodes. Fixed upstream flow Iter

* remove old primary_input function

* Vertical node visuals, remove is_layer function, rename Layer node to Merge node, toggle display as layer

exposed_value_count type fix

Vertical node visuals, remove is_layer function, rename Layer node to Merge node, toggle display as layer

* Fix demo artwork

* Layer display context menu

* Automatically select artboard, fix warnings

* Improvements to context menu and layer invariant enforcement

* Remove display_as_layer and update load_structure

* Improve load_structure to show more layers, improve FlowIter, improve layer naming, layer rearrangement validation.

* Clean up demo artwork using generalized layers

* Improve design of Layers panel and graph nodes

* MoveSelectedLayersTo rewrite to support generalized layer nodes

* Include artboards in deepest_common_ancestor, fix resize_artboard/delete_artboard, sync artboard tool to layer panel

* MoveSelectedLayersTo adjustments

* Sync non layer node visibility with metadata

* Include non layer nodes when moving/creating layer

* Fix group layers and get_post_node_with_index

* Include non layer nodes in UngroupSelectedLayers

* GroupSelected for all selected nodes, UnGroupSelected position adjustments

* Add grouping for layers in different folders

* Fix hidden layers

* Prevent node from connecting to itself, fix undo automatic node insertion,

* Fix undo CreateEmptyFolder, fix grouping nested layer nodes

* Formatting

* Remove test and check if node is layer from network

* Fix undo group layers

* Check off roadmap

* MoveUpstreamSiblingsToChild adjustments

* Replace tabs with spaces, remove mut from argument

* Final code review pass

---------

Co-authored-by: 0hypercube <0hypercube@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
adamgerhant
2024-04-30 23:03:42 -07:00
committed by GitHub
parent beb88d280c
commit 8d83fa7079
41 changed files with 1712 additions and 825 deletions

View File

@@ -3,7 +3,8 @@ use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, ProtoNodeInput};
use dyn_any::{DynAny, StaticType};
pub use graphene_core::uuid::generate_uuid;
use graphene_core::{GraphicGroup, ProtoNodeIdentifier, Type};
use graphene_core::vector::VectorData;
use graphene_core::{ArtboardGroup, GraphicGroup, ProtoNodeIdentifier, Type};
use glam::IVec2;
use std::collections::hash_map::DefaultHasher;
@@ -76,8 +77,8 @@ pub struct DocumentNode {
/// - Concrete example: a node that takes an image as primary input will get that image data from an upstream node that produces image output data and is evaluated first before being fed downstream.
///
/// This is achieved by automatically inserting `ComposeNode`s, which run the first node with the overall input and then feed the resulting output into the second node.
/// The `ComposeNode` is basically a function composition operator: the parentheses in `F(G(x))` or circle math operator in `(GF)(x)`.
/// For flexability, instead of being a language construct, Graphene splits out composition itself as its own low-level node so that behavior can be overridden.
/// The `ComposeNode` is basically a function composition operator: the parentheses in `F(G(x))` or circle math operator in `(FG)(x)`.
/// For flexibility, instead of being a language construct, Graphene splits out composition itself as its own low-level node so that behavior can be overridden.
/// The `ComposeNode`s are then inserted during the graph rewriting step for nodes that don't opt out with `manual_composition`.
/// Instead of node `G` feeding into node `F` feeding as the result back to the caller,
/// the graph is rewritten so nodes `G` and `F` both feed as lambdas into the parameters of a `ComposeNode` which calls `F(G(input))` and returns the result to the caller.
@@ -159,6 +160,9 @@ pub struct DocumentNode {
pub has_primary_output: bool,
// A nested document network or a proto-node identifier.
pub implementation: DocumentNodeImplementation,
/// User chosen state for displaying this as a left-to-right node or bottom-to-top layer.
#[serde(default)]
pub is_layer: bool,
/// Represents the eye icon for hiding/showing the node in the graph UI. When hidden, a node gets replaced with an identity node during the graph flattening step.
#[serde(default = "return_true")]
pub visible: bool,
@@ -212,6 +216,7 @@ impl Default for DocumentNode {
manual_composition: Default::default(),
has_primary_output: true,
implementation: Default::default(),
is_layer: false,
visible: true,
locked: Default::default(),
metadata: Default::default(),
@@ -348,21 +353,24 @@ impl DocumentNode {
self
}
pub fn is_layer(&self) -> bool {
// TODO: Use something more robust than checking against a string.
// TODO: Or, more fundamentally separate the concept of a layer from a node.
self.name == "Layer"
}
pub fn is_artboard(&self) -> bool {
// TODO: Use something more robust than checking against a string.
// TODO: Or, more fundamentally separate the concept of a layer from a node.
self.name == "Artboard"
}
pub fn is_folder(&self, network: &NodeNetwork) -> bool {
let input_connection = self.inputs.get(0).and_then(|input| input.as_node()).and_then(|node_id| network.nodes.get(&node_id));
input_connection.map(|node| node.is_layer()).unwrap_or(false)
// TODO: Is this redundant with `LayerNodeIdentifier::has_children()`? Consider removing this in favor of that.
/// Determines if a document node acting as a layer has any nested children where its secondary input eventually leads to a layer along horizontal flow.
pub fn layer_has_child_layers(&self, network: &NodeNetwork) -> bool {
if !self.is_layer {
return false;
}
self.inputs.iter().skip(1).any(|input| {
input.as_node().map_or(false, |node_id| {
network.upstream_flow_back_from_nodes(vec![node_id], FlowType::HorizontalFlow).any(|(node, _)| node.is_layer)
})
})
}
}
@@ -538,6 +546,16 @@ pub struct NodeNetwork {
pub previous_outputs: Option<Vec<NodeOutput>>,
}
#[derive(PartialEq)]
pub enum FlowType {
/// Iterate over all upstream nodes from every input (the primary and all secondary).
UpstreamFlow,
/// Iterate over nodes connected to the primary input.
PrimaryFlow,
/// Iterate over the secondary input for layer nodes and primary input for non layer nodes.
HorizontalFlow,
}
impl std::hash::Hash for NodeNetwork {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.imports.hash(state);
@@ -741,18 +759,18 @@ impl NodeNetwork {
self.previous_outputs.as_ref().map(|outputs| outputs.iter().any(|output| output.node_id == node_id))
}
/// Gives an iterator to all nodes connected to the given nodes by all inputs (primary or primary + secondary depending on `only_follow_primary` choice), traversing backwards upstream starting from the given node's inputs.
pub fn upstream_flow_back_from_nodes(&self, node_ids: Vec<NodeId>, only_follow_primary: bool) -> impl Iterator<Item = (&DocumentNode, NodeId)> {
/// Gives an iterator to all nodes connected to the given nodes (inclusive) by all inputs (primary or primary + secondary depending on `only_follow_primary` choice), traversing backwards upstream starting from the given node's inputs.
pub fn upstream_flow_back_from_nodes(&self, node_ids: Vec<NodeId>, flow_type: FlowType) -> impl Iterator<Item = (&DocumentNode, NodeId)> {
FlowIter {
stack: node_ids,
network: self,
only_follow_primary,
flow_type,
}
}
/// In the network `X -> Y -> Z`, `is_node_upstream_of_another_by_primary_flow(Z, X)` returns true.
pub fn is_node_upstream_of_another_by_primary_flow(&self, node: NodeId, potentially_upstream_node: NodeId) -> bool {
self.upstream_flow_back_from_nodes(vec![node], true).any(|(_, id)| id == potentially_upstream_node)
pub fn is_node_upstream_of_another_by_horizontal_flow(&self, node: NodeId, potentially_upstream_node: NodeId) -> bool {
self.upstream_flow_back_from_nodes(vec![node], FlowType::HorizontalFlow).any(|(_, id)| id == potentially_upstream_node)
}
/// Check there are no cycles in the graph (this should never happen).
@@ -789,11 +807,14 @@ impl NodeNetwork {
}
}
/// Iterate over the primary inputs of nodes, so in the case of `a -> b -> c`, this would yield `c, b, a` if we started from `c`.
/// Iterate over upstream nodes. The behavior changes based on the `flow_type` that's set.
/// - [`FlowType::UpstreamFlow`]: iterates over all upstream nodes from every input (the primary and all secondary).
/// - [`FlowType::PrimaryFlow`]: iterates along the horizontal inputs of nodes, so in the case of a node chain `a -> b -> c`, this would yield `c, b, a` if we started from `c`.
/// - [`FlowType::HorizontalFlow`]: iterates over the secondary input for layer nodes and primary input for non layer nodes.
struct FlowIter<'a> {
stack: Vec<NodeId>,
network: &'a NodeNetwork,
only_follow_primary: bool,
flow_type: FlowType,
}
impl<'a> Iterator for FlowIter<'a> {
type Item = (&'a DocumentNode, NodeId);
@@ -802,8 +823,9 @@ impl<'a> Iterator for FlowIter<'a> {
let node_id = self.stack.pop()?;
if let Some(document_node) = self.network.nodes.get(&node_id) {
let take = if self.only_follow_primary { 1 } else { usize::MAX };
let inputs = document_node.inputs.iter().take(take);
let skip = if self.flow_type == FlowType::HorizontalFlow && document_node.is_layer { 1 } else { 0 };
let take = if self.flow_type == FlowType::UpstreamFlow { usize::MAX } else { 1 };
let inputs = document_node.inputs.iter().skip(skip).take(take);
let node_ids = inputs.filter_map(|input| if let NodeInput::Node { node_id, .. } = input { Some(node_id) } else { None });
@@ -940,12 +962,8 @@ impl NodeNetwork {
if !node.visible && node.implementation != identity_node {
node.implementation = identity_node;
if node.is_layer() {
// Connect layer node to the graphic group below
node.inputs.drain(..1);
} else {
node.inputs.drain(1..);
}
// Connect layer node to the graphic group below
node.inputs.drain(1..);
self.nodes.insert(id, node);
return;
@@ -1169,37 +1187,39 @@ impl NodeNetwork {
/// However, in the case of the default input, we must insert a node that takes an input of `Footprint` and returns `GraphicGroup::Empty`, in order to satisfy the type system.
/// This is because the standard value node takes in `()`.
pub fn resolve_empty_stacks(&mut self) {
const EMPTY_STACK: &str = "Empty Stack";
for value in [
TaggedValue::GraphicGroup(GraphicGroup::EMPTY),
TaggedValue::VectorData(VectorData::empty()),
TaggedValue::ArtboardGroup(ArtboardGroup::EMPTY),
] {
const EMPTY_STACK: &str = "Empty Stack";
let new_id = generate_uuid();
let mut used = false;
let new_id = generate_uuid();
let mut used = false;
// We filter out the newly inserted empty stack in case `resolve_empty_stacks` runs multiple times.
for node in self.nodes.values_mut().filter(|node| node.name != EMPTY_STACK) {
for input in &mut node.inputs {
if let NodeInput::Value {
tagged_value: TaggedValue::GraphicGroup(graphic_group),
..
} = input
{
if *graphic_group == GraphicGroup::EMPTY {
*input = NodeInput::node(NodeId(new_id), 0);
used = true;
// We filter out the newly inserted empty stack in case `resolve_empty_stacks` runs multiple times.
for node in self.nodes.values_mut().filter(|node| node.name != EMPTY_STACK) {
for input in &mut node.inputs {
if let NodeInput::Value { tagged_value, .. } = input {
if *tagged_value == value {
*input = NodeInput::node(NodeId(new_id), 0);
used = true;
}
}
}
}
}
// Only insert the node if necessary.
if used {
let new_node = DocumentNode {
name: EMPTY_STACK.to_string(),
implementation: DocumentNodeImplementation::proto("graphene_core::transform::CullNode<_>"),
manual_composition: Some(concrete!(graphene_core::transform::Footprint)),
inputs: vec![NodeInput::value(TaggedValue::GraphicGroup(graphene_core::GraphicGroup::EMPTY), false)],
..Default::default()
};
self.nodes.insert(NodeId(new_id), new_node);
// Only insert the node if necessary.
if used {
let new_node = DocumentNode {
name: EMPTY_STACK.to_string(),
implementation: DocumentNodeImplementation::proto("graphene_core::transform::CullNode<_>"),
manual_composition: Some(concrete!(graphene_core::transform::Footprint)),
inputs: vec![NodeInput::value(value, false)],
..Default::default()
};
self.nodes.insert(NodeId(new_id), new_node);
}
}
}

View File

@@ -69,6 +69,7 @@ pub enum TaggedValue {
DocumentNode(DocumentNode),
GraphicGroup(graphene_core::GraphicGroup),
Artboard(graphene_core::Artboard),
ArtboardGroup(graphene_core::ArtboardGroup),
Curve(graphene_core::raster::curve::Curve),
IVec2(glam::IVec2),
SurfaceFrame(graphene_core::SurfaceFrame),
@@ -148,6 +149,7 @@ impl Hash for TaggedValue {
Self::DocumentNode(x) => x.hash(state),
Self::GraphicGroup(x) => x.hash(state),
Self::Artboard(x) => x.hash(state),
Self::ArtboardGroup(x) => x.hash(state),
Self::Curve(x) => x.hash(state),
Self::IVec2(x) => x.hash(state),
Self::SurfaceFrame(x) => x.hash(state),
@@ -214,6 +216,7 @@ impl<'a> TaggedValue {
TaggedValue::DocumentNode(x) => Box::new(x),
TaggedValue::GraphicGroup(x) => Box::new(x),
TaggedValue::Artboard(x) => Box::new(x),
TaggedValue::ArtboardGroup(x) => Box::new(x),
TaggedValue::Curve(x) => Box::new(x),
TaggedValue::IVec2(x) => Box::new(x),
TaggedValue::SurfaceFrame(x) => Box::new(x),
@@ -292,6 +295,7 @@ impl<'a> TaggedValue {
TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode),
TaggedValue::GraphicGroup(_) => concrete!(graphene_core::GraphicGroup),
TaggedValue::Artboard(_) => concrete!(graphene_core::Artboard),
TaggedValue::ArtboardGroup(_) => concrete!(graphene_core::ArtboardGroup),
TaggedValue::Curve(_) => concrete!(graphene_core::raster::curve::Curve),
TaggedValue::IVec2(_) => concrete!(glam::IVec2),
TaggedValue::SurfaceFrame(_) => concrete!(graphene_core::SurfaceFrame),