Update node graph guide readme with new syntax (#1061)

This commit is contained in:
Dennis Kobert 2023-03-01 15:13:51 +01:00 committed by Keavon Chambers
parent 8bc290fde9
commit 02d4565b0c

View file

@ -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<LayoutGroup> {
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<LayoutGroup> {
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: Node<(), Output = f64>>(N);
pub struct OpacityNode<O> {
opacity_multiplier: O,
}
impl<N: Node<(), Output = f64>> Node<Image> for GammaNode<N> {
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<N> {
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<N: Node<(), Output = f64> + Copy> Node<Image> for &GammaNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
image_gamma(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> GammaNode<N> {
impl<N> OpacityNode<N> {
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<G> {
gamma: G,
pub struct OpacityNode<O> {
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<dyn DynAny>` as input and output another `Box<dyn DynAny>`, 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<dyn DynAny>` as input and output another `Box<dyn DynAny>`, 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