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

@@ -106,6 +106,34 @@ impl Artboard {
}
}
/// Contains multiple artboards.
#[derive(Clone, Default, Debug, Hash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ArtboardGroup {
pub artboards: Vec<Artboard>,
}
impl ArtboardGroup {
pub const EMPTY: Self = Self { artboards: Vec::new() };
pub fn new() -> Self {
Default::default()
}
fn add_artboard(&mut self, artboard: Artboard) {
self.artboards.push(artboard);
}
pub fn get_graphic_group(&self) -> GraphicGroup {
let mut graphic_group = GraphicGroup::EMPTY;
for artboard in self.artboards.clone() {
let graphic_element: GraphicElement = artboard.into();
graphic_group.push(graphic_element);
}
graphic_group
}
}
pub struct ConstructLayerNode<GraphicElement, Stack> {
graphic_element: GraphicElement,
stack: Stack,
@@ -157,6 +185,24 @@ async fn construct_artboard<Fut: Future<Output = GraphicGroup>>(
clip,
}
}
pub struct AddArtboardNode<Artboard, ArtboardGroup> {
artboard: Artboard,
artboards: ArtboardGroup,
}
#[node_fn(AddArtboardNode)]
async fn add_artboard<Data: Into<Artboard>, Fut1: Future<Output = Data>, Fut2: Future<Output = ArtboardGroup>>(
footprint: Footprint,
artboard: impl Node<Footprint, Output = Fut1>,
mut artboards: impl Node<Footprint, Output = Fut2>,
) -> ArtboardGroup {
let artboard = self.artboard.eval(footprint).await;
let mut artboards = self.artboards.eval(footprint).await;
artboards.add_artboard(artboard.into());
artboards
}
impl From<ImageFrame<Color>> for GraphicElement {
fn from(mut image_frame: ImageFrame<Color>) -> Self {

View File

@@ -447,7 +447,7 @@ impl GraphicElementRendered for Artboard {
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
let subpath = Subpath::new_rect(self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2());
click_targets.push(ClickTarget { stroke_width: 0., subpath });
}
@@ -456,6 +456,23 @@ impl GraphicElementRendered for Artboard {
}
}
impl GraphicElementRendered for crate::ArtboardGroup {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
self.get_graphic_group().render_svg(render, render_params);
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.get_graphic_group().bounding_box(transform)
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
self.get_graphic_group().add_click_targets(click_targets);
}
fn contains_artboard(&self) -> bool {
true
}
}
impl GraphicElementRendered for ImageFrame<Color> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let transform: String = format_transform_matrix(self.transform * render.transform);

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),

View File

@@ -11,7 +11,7 @@ use graphene_core::value::{ClonedNode, CopiedNode, ValueNode};
use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::VectorData;
use graphene_core::{application_io::SurfaceHandle, SurfaceFrame, WasmSurfaceHandleFrame};
use graphene_core::{concrete, generic, Artboard, GraphicGroup};
use graphene_core::{concrete, generic, Artboard, ArtboardGroup, GraphicGroup};
use graphene_core::{fn_type, raster::*};
use graphene_core::{Cow, ProtoNodeIdentifier, Type};
use graphene_core::{Node, NodeIO, NodeIOTypes};
@@ -345,6 +345,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: graphene_core::GraphicGroup, fn_params: [Footprint => graphene_core::GraphicGroup]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: graphene_core::GraphicElement, fn_params: [Footprint => graphene_core::GraphicElement]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: Artboard, fn_params: [Footprint => graphene_core::Artboard]),
async_node!(graphene_std::wasm_application_io::LoadResourceNode<_>, input: WasmEditorApi, output: Arc<[u8]>, params: [String]),
register_node!(graphene_std::wasm_application_io::DecodeImageNode, input: Arc<[u8]>, params: []),
async_node!(graphene_std::wasm_application_io::CreateSurfaceNode, input: WasmEditorApi, output: Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>, params: []),
@@ -678,6 +679,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => VectorData, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => GraphicGroup, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Artboard, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ArtboardGroup, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => ImageFrame<Color>, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => VectorData, () => Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => GraphicGroup, () => Arc<WasmSurfaceHandle>]),
@@ -726,6 +728,25 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
)],
register_node!(graphene_core::transform::CullNode<_>, input: Footprint, params: [Artboard]),
register_node!(graphene_core::transform::CullNode<_>, input: Footprint, params: [ImageFrame<Color>]),
vec![(
ProtoNodeIdentifier::new("graphene_core::transform::CullNode<_>"),
|args| {
Box::pin(async move {
let mut args = args.clone();
args.reverse();
let node = <graphene_core::transform::CullNode<_>>::new(graphene_std::any::input_node::<ArtboardGroup>(args.pop().expect("Not enough arguments provided to construct node")));
let any: DynAnyNode<Footprint, _, _> = graphene_std::any::DynAnyNode::new(node);
Box::new(any) as Box<dyn for<'i> NodeIO<'i, graph_craft::proto::Any<'i>, Output = core::pin::Pin<Box<dyn core::future::Future<Output = graph_craft::proto::Any<'i>> + 'i>>> + '_>
})
},
{
let node = <graphene_core::transform::CullNode<_>>::new(graphene_std::any::PanicNode::<(), ArtboardGroup>::new());
let params = vec![fn_type!((), ArtboardGroup)];
let mut node_io = <graphene_core::transform::CullNode<_> as NodeIO<'_, Footprint>>::to_node_io(&node, params);
node_io.input = concrete!(<Footprint as StaticType>::Static);
node_io
},
)],
vec![(
ProtoNodeIdentifier::new("graphene_core::transform::CullNode<_>"),
|args| {
@@ -776,6 +797,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_core::ToGraphicElementNode, input: GraphicGroup, params: []),
register_node!(graphene_core::ToGraphicElementNode, input: Artboard, params: []),
async_node!(graphene_core::ConstructArtboardNode<_, _, _, _, _>, input: Footprint, output: Artboard, fn_params: [Footprint => GraphicGroup, () => glam::IVec2, () => glam::IVec2, () => Color, () => bool]),
async_node!(graphene_core::AddArtboardNode<_, _>, input: Footprint, output: ArtboardGroup, fn_params: [Footprint => Artboard, Footprint => ArtboardGroup]),
];
let mut map: HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
for (id, c, types) in node_types.into_iter().flatten() {