From 02d4565b0c78ce32d3fbf62297c26534fdbec4ca Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Wed, 1 Mar 2023 15:13:51 +0100 Subject: [PATCH] Update node graph guide readme with new syntax (#1061) --- node-graph/README.md | 91 +++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 56 deletions(-) diff --git a/node-graph/README.md b/node-graph/README.md index 3ee924dd..a8169a32 100644 --- a/node-graph/README.md +++ b/node-graph/README.md @@ -21,110 +21,89 @@ pub struct DocumentNode { } ``` -You can define your own type of document node in `editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs`. We currently just store document node types in a static slice but this will become dynamic in future. A sample document node type definition for the gamma node is shown: +You can define your own type of document node in `editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs`. We currently just store document node types in a static slice but this will become dynamic in future. A sample document node type definition for the opacity node is shown: ```rs DocumentNodeType { - name: "Gamma", + name: "Opacity", category: "Image Adjustments", - identifier: NodeIdentifier::new("graphene_std::raster::GammaNode", &[concrete!("&TypeErasedNode")]), - inputs: &[ - DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true), - DocumentInputType::new("Gamma", TaggedValue::F64(1.), false), + identifier: NodeImplementation::proto("graphene_core::raster::OpacityNode<_>"), + inputs: vec![ + DocumentInputType::value("Image", TaggedValue::Image(Image::empty()), true), + DocumentInputType::value("Factor", TaggedValue::F64(100.), false), ], - outputs: &[FrontendGraphDataType::Raster], - properties: node_properties::adjust_gamma_properties, + outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], + properties: node_properties::multiply_opacity, }, ``` The identifier here must be the same as that of the proto-node which will be discussed soon and is usually the path to the node implementation. -The input names are shown in the graph when an input is exposed (with a dot in the properties panel). The default input is used when a node is first created or when a link is disconnected. An input is comprised from a `TaggedValue` (allowing serialisation of a dynamic type with serde) in addition to an exposed boolean, which defines if the input is shown as a dot in the node graph UI by default. In the gamma node, the "Image" input is shown but the "Gamma" input is hidden from the graph by default, allowing for a less cluttered graph. +The input names are shown in the graph when an input is exposed (with a dot in the properties panel). The default input is used when a node is first created or when a link is disconnected. An input is comprised from a `TaggedValue` (allowing serialisation of a dynamic type with serde) in addition to an exposed boolean, which defines if the input is shown as a dot in the node graph UI by default. In the opacity node, the "Color" input is shown but the "Factor" input is hidden from the graph by default, allowing for a less cluttered graph. The properties field is a function that defines a number input, which can be seen by selecting the gamma node in the graph. The code for this property is shown below: ```rs -pub fn adjust_gamma_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { - let gamma = number_range_widget(document_node, node_id, 1, "Gamma", Some(0.01), None, "".into(), false); +pub fn multiply_opacity(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let factor = number_widget(document_node, node_id, 1, "Factor", NumberInput::default().min(0.).max(100.).unit("%"), true); - vec![LayoutGroup::Row { widgets: gamma }] + vec![LayoutGroup::Row { widgets: factor }] } ``` ## Node Implementation -Defining the actual implementation for a node is done by implementing the `Node` trait. The `Node` trait has one function called `eval` that takes one generic input and consumes the struct by value, however the `Node` trait can be implemented on a reference, meaning that the eval function consumes a pointer. A node implementation for the gamma node is seen below: +Defining the actual implementation for a node is done by implementing the `Node` trait. The `Node` trait has one function called `eval` that takes one generic input. A node implementation for the opacity node is seen below: ```rs #[derive(Debug, Clone, Copy)] -pub struct GammaNode>(N); +pub struct OpacityNode { + opacity_multiplier: O, +} -impl> Node for GammaNode { - type Output = Image; - fn eval(self, image: Image) -> Image { - image_gamma(image, self.0.eval(()) as f32) +impl<'i, N: Node<'i, (), Output = f64> + 'i> Node<'i, Color> for OpacityNode { + type Output = Color; + fn eval<'s: 'i>(&'s self, color: Color) -> Color { + let opacity_multiplier = self.opacity_multiplier.eval(()) as f32 / 100.; + Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier) } } -impl + Copy> Node for &GammaNode { - type Output = Image; - fn eval(self, image: Image) -> Image { - image_gamma(image, self.0.eval(()) as f32) - } -} - -impl + Copy> GammaNode { +impl OpacityNode { pub fn new(node: N) -> Self { - Self(node) + Self { opacity_multiplier: node } } } ``` The `eval` function can only take one input. To support more than one input, the node struct can contain references to other nodes (it is the references that implement the `Node` trait). If the input is a value, then a node that simply evaluates to its field will be referenced. If the input is a node, then the relevant proto-node will be referenced. To use these secondary inputs in the implementation, they are evaluated with the input of `()` to give an output an `f64` as specified by the generics. A helper function to create a new node struct is also defined here. -This process can be made more concise using the `node_macro` macro, which can be applied to a function like `image_gamma` with an attribute of the name of the node: +This process can be made more concise using the `node_macro` macro, which can be applied to a function like `image_opacity` with an attribute of the name of the node: ```rs #[derive(Debug, Clone, Copy)] -pub struct GammaNode { - gamma: G, +pub struct OpacityNode { + opacity_multiplier: O, } -#[node_macro::node_fn(GammaNode)] -fn image_gamma(mut image: Image, gamma: f64) -> Image { - let inverse_gamma = 1. / gamma; - let channel = |channel: f32| channel.powf(inverse_gamma as f32); - for pixel in &mut image.data { - *pixel = Color::from_rgbaf32_unchecked(channel(pixel.r()), channel(pixel.g()), channel(pixel.b()), pixel.a()) - } - image +#[node_macro::node_fn(OpacityNode)] +fn image_opacity(color: Color, opacity_multiplier: f64) -> Color { + let opacity_multiplier = opacity_multiplier as f32 / 100.; + Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier) } ``` ## Inserting the Proto-Node -When the document graph is executed, it is first converted to a proto-graph, which has all of the nested node graphs flattened as well as separating out the primary input from the secondary inputs. The secondary inputs are stored as a list of node ids in the construction arguments field of the `ProtoNode`. The newly created `ProtoNode`s are then converted into the corresponding dynamic rust functions using the mapping defined in `node-graph/interpreted-executor/src/node_registry.rs`. The resolved functions are then stored in a `BorrowStack`, which allows previous proto-nodes to be referenced as inputs by later nodes. To avoid invalid references, items can only be added or removed from the end of the stack. +When the document graph is executed, it is first converted to a proto-graph, which has all of the nested node graphs flattened as well as separating out the primary input from the secondary inputs. The secondary inputs are stored as a list of node ids in the construction arguments field of the `ProtoNode`. The newly created `ProtoNode`s are then converted into the corresponding dynamic rust functions using the mapping defined in `node-graph/interpreted-executor/src/node_registry.rs`. The resolved functions are then stored in a `BorrowTree`, which allows previous proto-nodes to be referenced as inputs by later nodes. The `BorrowTree` ensures nodes can't be removed while being referenced by other nodes. ```rs -(NodeIdentifier::new("graphene_std::raster::GammaNode", &[concrete!("&TypeErasedNode")]), |proto_node, stack| { - stack.push_fn(move |nodes| { - let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("GammaNode Node constructed without inputs") }; - let gamma: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[0] as usize).unwrap()); - let node = DynAnyNode::new(graphene_std::raster::GammaNode::new(gamma)); - - if let ProtoNodeInput::Node(node_id) = proto_node.input { - let pre_node = nodes.get(node_id as usize).unwrap(); - (pre_node).then(node).into_type_erased() - } else { - node.into_type_erased() - } - }) -}), +raster_node!(graphene_core::raster::OpacityNode<_>, params: [f64]), ``` -Nodes in the borrow stack take a `Box` as input and output another `Box`, to allow for any type. To use this as the field for our `GammaNode`, we must downcast these types so the input is a `()` and the output is a `f64`. This can be achieved by the `DowncastBothNode`. -The new `GammaNode` that has been constructed must then be made to have a dynamic input and output using the `DynAnyNode`. -If the primary input to the node comes from the output of another node, then the `ProtoNodeInput` will be set to a node id. This node is found and then chained with the gamma node using the `.then()` function. If there is no primary input then we simply return the node. +Nodes in the borrow stack take a `Box` as input and output another `Box`, to allow for any type. To use this as the field for our `OpacityNode`, we must downcast these types so the input is a `()` and the output is a `f64`. This can be achieved by the `DowncastBothNode`. +The new `OpacityNode` that has been constructed must then be made to have a dynamic input and output using the `DynAnyNode`. +If the primary input to the node comes from the output of another node, then the `ProtoNodeInput` will be set to a node id. This node is found and then chained with the opacity node using the `.then()` function. If there is no primary input then we simply return the node. Finally we call `.into_type_erased()` on the result and that is inserted into the borrow stack. ## Conclusion