From b8906f344e8ed8f4afb7aeca4412e005a42717e4 Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Tue, 24 Oct 2023 20:55:13 +0100 Subject: [PATCH] Migrate the Text tool to the document graph (#1435) * Update text tool to document graph * Fix selection issue * Log graph reruns and text node evals * Hash to set node * Fix let node crash * Fix loading document with fonts * Allow pressing enter to edit * Cleanup * Code review nits --------- Co-authored-by: Keavon Chambers --- .../document/document_message_handler.rs | 37 ++--- .../node_graph/graph_operation_message.rs | 7 + .../graph_operation_message_handler.rs | 35 ++++ .../document/node_graph/node_graph_message.rs | 7 +- .../node_graph/node_graph_message_handler.rs | 38 ++--- .../document_node_types.rs | 15 +- .../portfolio/portfolio_message_handler.rs | 42 +---- .../graph_modification_utils.rs | 56 ++++++- .../messages/tool/tool_messages/path_tool.rs | 6 +- .../tool/tool_messages/select_tool.rs | 28 ++-- .../messages/tool/tool_messages/text_tool.rs | 152 ++++++------------ editor/src/node_graph_executor.rs | 45 ++++-- frontend/wasm/src/editor_api.rs | 2 +- node-graph/graph-craft/src/document.rs | 4 + node-graph/graph-craft/src/proto.rs | 4 + .../src/dynamic_executor.rs | 1 + .../interpreted-executor/src/node_registry.rs | 42 ++++- 17 files changed, 283 insertions(+), 238 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index eafbf11c23..faac9f30ec 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1430,37 +1430,18 @@ impl DocumentMessageHandler { /// Loads layer resources such as creating the blob URLs for the images and loading all of the fonts in the document pub fn load_layer_resources(&self, responses: &mut VecDeque, root: &LayerDataType, mut path: Vec, _document_id: u64) { - fn walk_layers(data: &LayerDataType, path: &mut Vec, responses: &mut VecDeque, fonts: &mut HashSet) { - match data { - LayerDataType::Folder(folder) => { - for (id, layer) in folder.layer_ids.iter().zip(folder.layers().iter()) { - path.push(*id); - walk_layers(&layer.data, path, responses, fonts); - path.pop(); - } + let mut fonts = HashSet::new(); + for (_node_id, node) in self.document_legacy.document_network.recursive_nodes() { + for input in &node.inputs { + if let NodeInput::Value { + tagged_value: TaggedValue::Font(font), + .. + } = input + { + fonts.insert(font.clone()); } - LayerDataType::Layer(layer) => { - if layer.cached_output_data == CachedOutputData::None { - responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path: path.clone() }); - } - for node in layer.network.nodes.values() { - for input in &node.inputs { - if let NodeInput::Value { - tagged_value: TaggedValue::Font(font), - .. - } = input - { - fonts.insert(font.clone()); - } - } - } - } - _ => {} } } - - let mut fonts = HashSet::new(); - walk_layers(root, &mut path, responses, &mut fonts); for font in fonts { responses.add_front(FrontendMessage::TriggerFontLoad { font, is_default: false }); } diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs index d5186b3f47..c24a0961e8 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs @@ -3,6 +3,7 @@ use crate::messages::prelude::*; use bezier_rs::Subpath; use graph_craft::document::NodeId; use graphene_core::raster::ImageFrame; +use graphene_core::text::Font; use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::style::{Fill, Stroke}; @@ -68,6 +69,12 @@ pub enum GraphOperationMessage { id: NodeId, subpaths: Vec>, }, + NewTextLayer { + id: NodeId, + text: String, + font: Font, + size: f64, + }, ResizeArtboard { id: NodeId, location: IVec2, diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs index 3ca46a0473..350f44796c 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs @@ -8,6 +8,7 @@ use document_legacy::{LayerId, Operation}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{generate_uuid, DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput}; use graphene_core::raster::ImageFrame; +use graphene_core::text::Font; use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::style::{Fill, FillType, Stroke}; @@ -178,6 +179,34 @@ impl<'a> ModifyInputsContext<'a> { self.responses.add(NodeGraphMessage::SendGraph { should_rerender: true }); } + fn insert_text(&mut self, text: String, font: Font, size: f64, layer: NodeId) { + let text = resolve_document_node_type("Text").expect("Text node does not exist").to_document_node( + [ + NodeInput::Network(graph_craft::concrete!(graphene_std::wasm_application_io::WasmEditorApi)), + NodeInput::value(TaggedValue::String(text), false), + NodeInput::value(TaggedValue::Font(font), false), + NodeInput::value(TaggedValue::F64(size), false), + ], + Default::default(), + ); + let cull = resolve_document_node_type("Cull").expect("Cull node does not exist").default_document_node(); + let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_document_node(); + let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_document_node(); + let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_document_node(); + + let stroke_id = generate_uuid(); + self.insert_node_before(stroke_id, layer, 0, stroke, IVec2::new(-8, 0)); + let fill_id = generate_uuid(); + self.insert_node_before(fill_id, stroke_id, 0, fill, IVec2::new(-8, 0)); + let transform_id = generate_uuid(); + self.insert_node_before(transform_id, fill_id, 0, transform, IVec2::new(-8, 0)); + let cull_id = generate_uuid(); + self.insert_node_before(cull_id, transform_id, 0, cull, IVec2::new(-8, 0)); + let text_id = generate_uuid(); + self.insert_node_before(text_id, cull_id, 0, text, IVec2::new(-8, 0)); + self.responses.add(NodeGraphMessage::SendGraph { should_rerender: true }); + } + fn insert_image_data(&mut self, image_frame: ImageFrame, layer: NodeId) { let image = { let node_type = resolve_document_node_type("Image").expect("Image node does not exist"); @@ -560,6 +589,12 @@ impl MessageHandler { + let mut modify_inputs = ModifyInputsContext::new(document, node_graph, responses); + if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.network.outputs[0].node_id, 0) { + modify_inputs.insert_text(text, font, size, layer); + } + } GraphOperationMessage::ResizeArtboard { id, location, dimensions } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_layer(&[id], document, node_graph, responses) { modify_inputs.resize_artboard(location, dimensions); diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index a62cb8d26d..7f516adbbc 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -86,14 +86,17 @@ pub enum NodeGraphMessage { input_index: usize, value: TaggedValue, }, - SetSelectNodes { + SetSelectedNodes { nodes: Vec, }, ShiftNode { node_id: NodeId, }, ToggleHidden, - ToggleHiddenImpl, + SetHidden { + node_id: NodeId, + hidden: bool, + }, TogglePreview { node_id: NodeId, }, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index a6cda71f35..77d3d562b5 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -746,7 +746,7 @@ impl<'a> MessageHandler> for NodeGrap } let nodes = new_ids.values().copied().collect(); - responses.add(NodeGraphMessage::SetSelectNodes { nodes }); + responses.add(NodeGraphMessage::SetSelectedNodes { nodes }); responses.add(NodeGraphMessage::SendGraph { should_rerender: false }); } @@ -817,7 +817,7 @@ impl<'a> MessageHandler> for NodeGrap } } } - NodeGraphMessage::SetSelectNodes { nodes } => { + NodeGraphMessage::SetSelectedNodes { nodes } => { responses.add(document.metadata.set_selected_nodes(nodes)); responses.add(PropertiesPanelMessage::ResendActiveProperties); } @@ -870,31 +870,27 @@ impl<'a> MessageHandler> for NodeGrap responses.add(NodeGraphMessage::SendGraph { should_rerender: false }); } NodeGraphMessage::ToggleHidden => { - responses.add(DocumentMessage::StartTransaction); - responses.add(NodeGraphMessage::ToggleHiddenImpl); + if let Some(network) = document.document_network.nested_network(&self.network) { + responses.add(DocumentMessage::StartTransaction); + + let new_hidden = !document.metadata.selected_nodes().any(|id| network.disabled.contains(id)); + for &node_id in document.metadata.selected_nodes() { + responses.add(NodeGraphMessage::SetHidden { node_id, hidden: new_hidden }); + } + } } - NodeGraphMessage::ToggleHiddenImpl => { + NodeGraphMessage::SetHidden { node_id, hidden } => { if let Some(network) = document.document_network.nested_network_mut(&self.network) { - // Check if any of the selected nodes are hidden - if document.metadata.selected_nodes().any(|id| network.disabled.contains(id)) { - // Remove all selected nodes from the disabled list - network.disabled.retain(|id| !document.metadata.selected_nodes_ref().contains(id)); - } else { - let original_outputs = network.original_outputs().iter().map(|output| output.node_id).collect::>(); - // Add all selected nodes to the disabled list (excluding input or output nodes) - network - .disabled - .extend(document.metadata.selected_nodes().filter(|&id| !network.inputs.contains(id) && !original_outputs.contains(id))); + if !hidden { + network.disabled.retain(|&id| node_id != id); + } else if !network.inputs.contains(&node_id) && !network.original_outputs().iter().any(|output| output.node_id == node_id) { + network.disabled.push(node_id); } Self::send_graph(network, &self.layer_path, responses); // Only generate node graph if one of the selected nodes is connected to the output - if document.metadata.selected_nodes().any(|&node_id| network.connected_to_output(node_id)) { - if let Some(layer_path) = self.layer_path.clone() { - responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path }); - } else { - responses.add(NodeGraphMessage::RunDocumentGraph); - } + if network.connected_to_output(node_id) { + responses.add(NodeGraphMessage::RunDocumentGraph); } } self.update_selection_action_buttons(document, responses); diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index 660d8c8972..bee1a86621 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -2483,7 +2483,7 @@ impl DocumentNodeType { } } -pub fn wrap_network_in_scope(mut network: NodeNetwork) -> NodeNetwork { +pub fn wrap_network_in_scope(mut network: NodeNetwork, hash: u64) -> NodeNetwork { network.generate_node_paths(&[]); let node_ids = network.nodes.keys().copied().collect::>(); @@ -2520,11 +2520,18 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork) -> NodeNetwork { ..Default::default() }; + let mut begin_scope = resolve_document_node_type("Begin Scope") + .expect("Begin Scope node type not found") + .to_document_node(vec![input_type.unwrap()], DocumentNodeMetadata::default()); + if let DocumentNodeImplementation::Network(g) = &mut begin_scope.implementation { + if let Some(node) = g.nodes.get_mut(&0) { + node.hash = hash; + } + } + // wrap the inner network in a scope let nodes = vec![ - resolve_document_node_type("Begin Scope") - .expect("Begin Scope node type not found") - .to_document_node(vec![input_type.unwrap()], DocumentNodeMetadata::default()), + begin_scope, inner_network, resolve_document_node_type("End Scope") .expect("End Scope node type not found") diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index a8d0bbb205..2228685f54 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -219,14 +219,13 @@ impl MessageHandler { let font = Font::new(font_family, font_style); - if let Some(document) = self.active_document_mut() { - Self::uploaded_new_font(document, &font, responses); - responses.add(DocumentMessage::RenderDocument); - responses.add(BroadcastEvent::DocumentIsDirty); - } - self.persistent_data.font_cache.insert(font, preview_url, data, is_default); self.executor.update_font_cache(self.persistent_data.font_cache.clone()); + + if let Some(document) = self.active_document_mut() { + responses.add(NodeGraphMessage::RunDocumentGraph); + responses.add(BroadcastEvent::DocumentIsDirty); + } } PortfolioMessage::GraphViewOverlay { open } => { self.graph_view_overlay_open = open; @@ -675,37 +674,6 @@ impl PortfolioMessageHandler { self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids") } - fn uploaded_new_font(document: &mut DocumentMessageHandler, target_font: &Font, responses: &mut VecDeque) { - let mut stack = vec![(&document.document_legacy.root, Vec::new())]; - - while let Some((layer, layer_path)) = stack.pop() { - match &layer.data { - LayerDataType::Folder(folder) => stack.extend(folder.layers.iter().zip(folder.layer_ids.iter().map(|id| { - let mut x = layer_path.clone(); - x.push(*id); - x - }))), - LayerDataType::Layer(layer) => { - let input_is_font = |input: &NodeInput| { - let NodeInput::Value { - tagged_value: TaggedValue::Font(font), - .. - } = input - else { - return false; - }; - font == target_font - }; - let should_rerender = layer.network.nodes.values().any(|node| node.inputs.iter().any(input_is_font)); - if should_rerender { - responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path }); - } - } - _ => {} - } - } - } - pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) { let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) else { warn!("Polling node graph with no document"); diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 33e75b5bba..6528858bbe 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -5,6 +5,7 @@ use bezier_rs::{ManipulatorGroup, Subpath}; use document_legacy::{document::Document, document_metadata::LayerNodeIdentifier, LayerId, Operation}; use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput, NodeNetwork}; use graphene_core::raster::ImageFrame; +use graphene_core::text::Font; use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::style::{FillType, Gradient}; use graphene_core::Color; @@ -16,10 +17,10 @@ use std::collections::VecDeque; pub fn new_vector_layer(subpaths: Vec>, layer_path: Vec, responses: &mut VecDeque) { let id = *layer_path.last().unwrap(); responses.add(GraphOperationMessage::NewVectorLayer { id, subpaths }); - responses.add(NodeGraphMessage::SetSelectNodes { nodes: vec![id] }) + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: vec![id] }) } -/// Creat a new bitmap layer from an [`graphene_core::raster::ImageFrame`] +/// Create a new bitmap layer from an [`graphene_core::raster::ImageFrame`] pub fn new_image_layer(image_frame: ImageFrame, layer_path: Vec, responses: &mut VecDeque) { responses.add(GraphOperationMessage::NewBitmapLayer { id: *layer_path.last().unwrap(), @@ -119,6 +120,52 @@ pub fn get_gradient(layer: LayerNodeIdentifier, document: &Document) -> Option Option { + let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Fill")?; + let TaggedValue::Color(color) = inputs.get(2)?.as_value()? else { + return None; + }; + Some(*color) +} + +pub fn get_text_id(layer: LayerNodeIdentifier, document: &Document) -> Option { + NodeGraphLayer::new(layer, document)?.node_id("Text") +} +pub fn get_fill_id(layer: LayerNodeIdentifier, document: &Document) -> Option { + NodeGraphLayer::new(layer, document)?.node_id("Fill") +} + +/// Gets properties from the text node +pub fn get_text(layer: LayerNodeIdentifier, document: &Document) -> Option<(&String, &Font, f64)> { + let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Text")?; + let NodeInput::Value { + tagged_value: TaggedValue::String(text), + .. + } = &inputs[1] + else { + return None; + }; + + let NodeInput::Value { + tagged_value: TaggedValue::Font(font), + .. + } = &inputs[2] + else { + return None; + }; + + let NodeInput::Value { + tagged_value: TaggedValue::F64(font_size), + .. + } = inputs[3] + else { + return None; + }; + + Some((text, font, font_size)) +} + /// Is a specified layer an artboard? pub fn is_artboard(layer: LayerNodeIdentifier, document: &Document) -> bool { NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.uses_node("Artboard")) @@ -193,6 +240,11 @@ impl<'a> NodeGraphLayer<'a> { self.primary_layer_flow().any(|(node, _id)| node.name == node_name) } + /// Node id of a node if it exists in the layer's primary flow + pub fn node_id(&self, node_name: &str) -> Option { + self.primary_layer_flow().find(|(node, _id)| node.name == node_name).map(|(_node, id)| id) + } + /// Find all of the inputs of a specific node within the layer's primary flow pub fn find_node_inputs(&self, node_name: &str) -> Option<&'a Vec> { self.primary_layer_flow().find(|(node, _id)| node.name == node_name).map(|(node, _id)| &node.inputs) diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 6d5b4661ba..f807f3dd0c 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -246,7 +246,7 @@ impl PathToolData { if shift { responses.add(NodeGraphMessage::AddSelectNodes { nodes: vec![layer.to_node()] }); } else { - responses.add(NodeGraphMessage::SetSelectNodes { nodes: vec![layer.to_node()] }); + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: vec![layer.to_node()] }); } self.drag_start_pos = input.mouse.position; self.previous_mouse_position = input.mouse.position; @@ -382,7 +382,7 @@ impl Fsm for PathToolFsmState { let shift_pressed = input.keyboard.get(add_to_selection as usize); if tool_data.drag_start_pos == tool_data.previous_mouse_position { - responses.add(NodeGraphMessage::SetSelectNodes { nodes: vec![] }); + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: vec![] }); } else { shape_editor.select_all_in_quad(&document.document_legacy, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed); tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses); @@ -397,7 +397,7 @@ impl Fsm for PathToolFsmState { let shift_pressed = input.keyboard.get(shift_mirror_distance as usize); if tool_data.drag_start_pos == tool_data.previous_mouse_position { - responses.add(NodeGraphMessage::SetSelectNodes { nodes: vec![] }); + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: vec![] }); } else { shape_editor.select_all_in_quad(&document.document_legacy, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed); tool_data.refresh_overlays(document, shape_editor, shape_overlay, responses); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 3e2e8b4237..b68db2bf5c 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -369,7 +369,7 @@ impl SelectToolData { } // Select the originals - responses.add(NodeGraphMessage::SetSelectNodes { + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: originals.iter().map(|layer| layer.to_node()).collect::>(), }); @@ -719,13 +719,13 @@ impl Fsm for SelectToolFsmState { tool_data.layers_dragging.clear(); tool_data.layers_dragging.extend(replacement_selected_layers.iter()); - responses.add(NodeGraphMessage::SetSelectNodes { + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: replacement_selected_layers.iter().map(|layer| layer.to_node()).collect(), }); } } else if let Some(selecting_layer) = tool_data.select_single_layer.take() { if !tool_data.has_dragged { - responses.add(NodeGraphMessage::SetSelectNodes { + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: vec![selecting_layer.to_node()], }); } @@ -787,7 +787,7 @@ impl Fsm for SelectToolFsmState { let quad = tool_data.selection_quad(); // For shallow select we don't update dragging layers until inside drag_start_shallowest_manipulation() tool_data.layers_dragging = document.metadata().intersect_quad(quad, &document.document_legacy.document_network).collect(); - responses.add_front(NodeGraphMessage::SetSelectNodes { + responses.add_front(NodeGraphMessage::SetSelectedNodes { nodes: tool_data.layers_dragging.iter().map(|layer| layer.to_node()).collect(), }); responses.add_front(DocumentMessage::Overlays( @@ -799,18 +799,14 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::Ready } (SelectToolFsmState::Ready, SelectToolMessage::Enter) => { - let mut selected_layers = document.selected_layers(); + let mut selected_layers = document.metadata().selected_layers(); - if let Some(layer_path) = selected_layers.next() { + if let Some(layer) = selected_layers.next() { // Check that only one layer is selected if selected_layers.next().is_none() { - if let Ok(layer) = document.document_legacy.layer(layer_path) { - if let Ok(network) = layer.as_layer_network() { - if network.nodes.values().any(|node| node.name == "Text") { - responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }); - responses.add(TextToolMessage::EditSelected); - } - } + if is_text_layer(layer, &document.document_legacy) { + responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }); + responses.add(TextToolMessage::EditSelected); } } } @@ -928,7 +924,7 @@ fn drag_shallowest_manipulation(responses: &mut VecDeque, mut selected: let new_selected = ancestor.unwrap_or_else(|| layer.child_of_root(document.metadata())); tool_data.layers_dragging = vec![new_selected]; - responses.add(NodeGraphMessage::SetSelectNodes { + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: tool_data.layers_dragging.iter().map(|layer| layer.to_node()).collect(), }); // tool_data @@ -938,7 +934,7 @@ fn drag_shallowest_manipulation(responses: &mut VecDeque, mut selected: fn drag_deepest_manipulation(responses: &mut VecDeque, mut selected: Vec, tool_data: &mut SelectToolData) { tool_data.layers_dragging.append(&mut selected); - responses.add(NodeGraphMessage::SetSelectNodes { + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: tool_data.layers_dragging.iter().map(|layer| layer.to_node()).collect(), }); // tool_data @@ -961,7 +957,7 @@ fn edit_layer_shallowest_manipulation(document: &DocumentMessageHandler, layer: return; }; - responses.add(NodeGraphMessage::SetSelectNodes { nodes: vec![new_selected.to_node()] }); + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: vec![new_selected.to_node()] }); } fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, document: &Document, responses: &mut VecDeque) { diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index cc28096e8a..d081d5a455 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -4,7 +4,9 @@ use crate::application::generate_uuid; use crate::consts::{COLOR_ACCENT, SELECTION_TOLERANCE}; use crate::messages::portfolio::document::node_graph::new_text_network; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_text_layer}; +use document_legacy::document_metadata::LayerNodeIdentifier; use document_legacy::intersection::Quad; use document_legacy::layers::layer_info::Layer; use document_legacy::layers::style::{self, Fill, RenderData, Stroke}; @@ -216,7 +218,7 @@ pub struct EditingText { #[derive(Clone, Debug, Default)] struct TextToolData { - layer_path: Vec, + layer: LayerNodeIdentifier, overlays: Vec>, editing_text: Option, new_text: String, @@ -224,9 +226,10 @@ struct TextToolData { impl TextToolData { /// Set the editing state of the currently modifying layer - fn set_editing(&self, editable: bool, render_data: &RenderData, responses: &mut VecDeque) { - let path = self.layer_path.clone(); - responses.add(Operation::SetLayerVisibility { path, visible: !editable }); + fn set_editing(&self, editable: bool, render_data: &RenderData, document: &DocumentMessageHandler, responses: &mut VecDeque) { + if let Some(node_id) = graph_modification_utils::get_fill_id(self.layer, &document.document_legacy) { + responses.add(NodeGraphMessage::SetHidden { node_id, hidden: editable }); + } if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) { responses.add(FrontendMessage::DisplayEditableTextbox { @@ -243,18 +246,9 @@ impl TextToolData { } fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> { - let transform = document.document_legacy.multiply_transforms(&self.layer_path).ok()?; - let layer = document.document_legacy.layer(&self.layer_path).ok()?; - let color = layer - .style() - .ok() - .map_or(Color::BLACK, |style| if let Fill::Solid(solid_color) = style.fill() { *solid_color } else { Color::BLACK }); - - let network = get_network(&self.layer_path, document)?; - let node_id = get_text_node_id(network)?; - let node = network.nodes.get(&node_id)?; - - let (text, font, font_size) = Self::extract_text_node_inputs(node)?; + let transform = document.metadata().transform_to_viewport(self.layer); + let color = graph_modification_utils::get_fill_color(self.layer, &document.document_legacy).unwrap_or(Color::BLACK); + let (text, font, font_size) = graph_modification_utils::get_text(self.layer, &document.document_legacy)?; self.editing_text = Some(EditingText { text: text.clone(), font: font.clone(), @@ -266,53 +260,24 @@ impl TextToolData { Some(()) } - fn start_editing_layer(&mut self, layer_path: &[LayerId], tool_state: TextToolFsmState, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque) { + fn start_editing_layer(&mut self, layer: LayerNodeIdentifier, tool_state: TextToolFsmState, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque) { if tool_state == TextToolFsmState::Editing { - self.set_editing(false, render_data, responses); + self.set_editing(false, render_data, document, responses); } - self.layer_path = layer_path.into(); + self.layer = layer; self.load_layer_text_node(document); responses.add(DocumentMessage::StartTransaction); - self.set_editing(true, render_data, responses); + self.set_editing(true, render_data, document, responses); - let replacement_selected_layers = vec![self.layer_path.clone()]; - responses.add(DocumentMessage::SetSelectedLayers { replacement_selected_layers }); - } - - fn extract_text_node_inputs(node: &DocumentNode) -> Option<(&String, &Font, f64)> { - let NodeInput::Value { - tagged_value: TaggedValue::String(text), - .. - } = &node.inputs[1] - else { - return None; - }; - let NodeInput::Value { - tagged_value: TaggedValue::Font(font), - .. - } = &node.inputs[2] - else { - return None; - }; - let NodeInput::Value { - tagged_value: TaggedValue::F64(font_size), - .. - } = &node.inputs[3] - else { - return None; - }; - Some((text, font, *font_size)) + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: vec![self.layer.to_node()] }); } fn interact(&mut self, state: TextToolFsmState, mouse: DVec2, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque) -> TextToolFsmState { - let tolerance = DVec2::splat(SELECTION_TOLERANCE); - let quad = Quad::from_box([mouse - tolerance, mouse + tolerance]); - // Check if the user has selected an existing text layer - if let Some(clicked_text_layer_path) = document.document_legacy.intersects_quad_root(quad, render_data).last().filter(|l| is_text_layer(document, l)) { + if let Some(clicked_text_layer_path) = document.metadata().click(mouse, &document.network()).filter(|&layer| is_text_layer(layer, &document.document_legacy)) { self.start_editing_layer(clicked_text_layer_path, state, document, render_data, responses); TextToolFsmState::Editing @@ -321,35 +286,33 @@ impl TextToolData { else if let Some(editing_text) = self.editing_text.as_ref().filter(|_| state == TextToolFsmState::Ready) { responses.add(DocumentMessage::StartTransaction); - let network = new_text_network(String::new(), editing_text.font.clone(), editing_text.font_size); + self.layer = LayerNodeIdentifier::new_unchecked(generate_uuid()); - responses.add(Operation::AddFrame { - path: self.layer_path.clone(), - insert_index: -1, - transform: DAffine2::ZERO.to_cols_array(), - network, + responses.add(GraphOperationMessage::NewTextLayer { + id: self.layer.to_node(), + text: String::new(), + font: editing_text.font.clone(), + size: editing_text.font_size, }); responses.add(GraphOperationMessage::FillSet { - layer: self.layer_path.clone(), + layer: self.layer.to_path(), fill: if editing_text.color.is_some() { Fill::Solid(editing_text.color.unwrap()) } else { Fill::None }, }); responses.add(GraphOperationMessage::TransformSet { - layer: self.layer_path.clone(), + layer: self.layer.to_path(), transform: editing_text.transform, transform_in: TransformIn::Viewport, skip_rerender: true, }); - self.set_editing(true, render_data, responses); + self.set_editing(true, render_data, document, responses); - let replacement_selected_layers = vec![self.layer_path.clone()]; - - responses.add(DocumentMessage::SetSelectedLayers { replacement_selected_layers }); + responses.add(NodeGraphMessage::SetSelectedNodes { nodes: self.layer.to_path() }); TextToolFsmState::Editing } else { // Removing old text as editable - self.set_editing(false, render_data, responses); + self.set_editing(false, render_data, document, responses); resize_overlays(&mut self.overlays, responses, 0); @@ -365,7 +328,7 @@ impl TextToolData { let far = graphene_core::text::bounding_box(&self.new_text, buzz_face, editing_text.font_size, None); let quad = Quad::from_box([DVec2::ZERO, far]); - let transformed_quad = document.document_legacy.multiply_transforms(&self.layer_path).ok()? * quad; + let transformed_quad = document.metadata().transform_to_viewport(self.layer) * quad; let bounds = transformed_quad.bounding_box(); let operation = Operation::SetLayerTransformInViewport { @@ -386,7 +349,7 @@ impl TextToolData { } fn fix_text_bounds(&self, new_text: &str, _document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque) -> Option<()> { - let layer = self.layer_path.clone(); + let layer = self.layer.to_path(); let old_bounds = self.get_bounds(&self.editing_text.as_ref()?.text, render_data)?; let new_bounds = self.get_bounds(new_text, render_data)?; responses.add(GraphOperationMessage::UpdateBounds { layer, old_bounds, new_bounds }); @@ -419,21 +382,15 @@ fn resize_overlays(overlays: &mut Vec>, responses: &mut VecDeque, render_data: &RenderData) { - let get_bounds = |layer: &Layer, path: &[LayerId], document: &DocumentMessageHandler, render_data: &RenderData| { - let node_graph = layer.as_layer_network().ok()?; - let node_id = get_text_node_id(node_graph)?; - let document_node = node_graph.nodes.get(&node_id)?; - let (text, font, font_size) = TextToolData::extract_text_node_inputs(document_node)?; + let get_bounds = |layer: LayerNodeIdentifier, document: &DocumentMessageHandler, render_data: &RenderData| { + let (text, font, font_size) = graph_modification_utils::get_text(layer, &document.document_legacy)?; let buzz_face = render_data.font_cache.get(font).map(|data| load_face(data)); let far = graphene_core::text::bounding_box(text, buzz_face, font_size, None); let quad = Quad::from_box([DVec2::ZERO, far]); - let multiplied = document.document_legacy.multiply_transforms(path).ok()? * quad; + let multiplied = document.metadata().transform_to_viewport(layer) * quad; Some(multiplied.bounding_box()) }; - let bounds = document.selected_layers().filter_map(|path| match document.document_legacy.layer(path) { - Ok(layer) => get_bounds(layer, path, document, render_data), - Err(_) => None, - }); + let bounds = document.metadata().selected_layers().filter_map(|layer| get_bounds(layer, document, render_data)); let bounds = bounds.collect::>(); let new_len = bounds.len(); @@ -448,36 +405,20 @@ fn update_overlays(document: &DocumentMessageHandler, tool_data: &mut TextToolDa resize_overlays(&mut tool_data.overlays, responses, new_len); } -fn get_network<'a>(layer_path: &[LayerId], document: &'a DocumentMessageHandler) -> Option<&'a NodeNetwork> { - let layer = document.document_legacy.layer(layer_path).ok()?; - layer.as_layer_network().ok() -} +fn can_edit_selected(document: &DocumentMessageHandler) -> Option { + let mut selected_layers = document.metadata().selected_layers(); -fn get_text_node_id(network: &NodeNetwork) -> Option { - network.nodes.iter().find(|(_, node)| node.name == "Text").map(|(&id, _)| id) -} - -fn is_text_layer(document: &DocumentMessageHandler, layer_path: &[LayerId]) -> bool { - let Some(network) = get_network(layer_path, document) else { - return false; - }; - get_text_node_id(network).is_some() -} - -fn can_edit_selected(document: &DocumentMessageHandler) -> Option> { - let mut selected_layers = document.selected_layers(); - - let layer_path = selected_layers.next()?.to_vec(); + let layer = selected_layers.next()?; // Check that only one layer is selected if selected_layers.next().is_some() { return None; } - if !is_text_layer(document, &layer_path) { + if !is_text_layer(layer, &document.document_legacy) { return None; } - Some(layer_path) + Some(layer) } impl Fsm for TextToolFsmState { @@ -498,7 +439,7 @@ impl Fsm for TextToolFsmState { match (self, event) { (TextToolFsmState::Editing, TextToolMessage::DocumentIsDirty) => { responses.add(FrontendMessage::DisplayEditableTextboxTransform { - transform: document.document_legacy.multiply_transforms(&tool_data.layer_path).ok().unwrap_or_default().to_cols_array(), + transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(), }); tool_data.update_bounds_overlay(document, render_data, responses); TextToolFsmState::Editing @@ -517,13 +458,12 @@ impl Fsm for TextToolFsmState { color: tool_options.fill.active_color(), }); tool_data.new_text = String::new(); - tool_data.layer_path = document.get_path_for_new_layer(); tool_data.interact(state, input.mouse.position, document, render_data, responses) } (state, TextToolMessage::EditSelected) => { - if let Some(layer_path) = can_edit_selected(document) { - tool_data.start_editing_layer(&layer_path, state, document, render_data, responses); + if let Some(layer) = can_edit_selected(document) { + tool_data.start_editing_layer(layer, state, document, render_data, responses); return TextToolFsmState::Editing; } @@ -531,7 +471,7 @@ impl Fsm for TextToolFsmState { } (state, TextToolMessage::Abort) => { if state == TextToolFsmState::Editing { - tool_data.set_editing(false, render_data, responses); + tool_data.set_editing(false, render_data, document, responses); } resize_overlays(&mut tool_data.overlays, responses, 0); @@ -544,17 +484,15 @@ impl Fsm for TextToolFsmState { TextToolFsmState::Editing } (TextToolFsmState::Editing, TextToolMessage::TextChange { new_text }) => { - let layer_path = tool_data.layer_path.clone(); - let network = get_network(&layer_path, document).unwrap(); tool_data.fix_text_bounds(&new_text, document, render_data, responses); responses.add(NodeGraphMessage::SetQualifiedInputValue { - layer_path, - node_path: vec![get_text_node_id(network).unwrap()], + layer_path: Vec::new(), + node_path: vec![graph_modification_utils::get_text_id(tool_data.layer, &document.document_legacy).unwrap()], input_index: 1, value: TaggedValue::String(new_text), }); - tool_data.set_editing(false, render_data, responses); + tool_data.set_editing(false, render_data, document, responses); resize_overlays(&mut tool_data.overlays, responses, 0); diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index fefb9749b4..421acfea70 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -107,6 +107,8 @@ thread_local! { pub(crate) static NODE_RUNTIME: Rc>> = Rc::new(RefCell::new(None)); } +type MonitorNodes = Vec>; + impl NodeRuntime { fn new(receiver: Receiver, sender: Sender) -> Self { let executor = DynamicExecutor::default(); @@ -146,15 +148,7 @@ impl NodeRuntime { viewport_resolution, .. }) => { - let network = wrap_network_in_scope(graph); - - let monitor_nodes = network - .recursive_nodes() - .filter(|(_, node)| node.implementation == DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode<_>")) - .map(|(_, node)| node.path.clone().unwrap_or_default()) - .collect::>(); - - let result = self.execute_network(&path, network, transform, viewport_resolution).await; + let (result, monitor_nodes) = self.execute_network(&path, graph, transform, viewport_resolution).await; let mut responses = VecDeque::new(); self.update_thumbnails(&path, &monitor_nodes, &mut responses); self.update_upstream_transforms(&path, &monitor_nodes, &mut responses); @@ -173,11 +167,10 @@ impl NodeRuntime { } } - async fn execute_network<'a>(&'a mut self, path: &[LayerId], scoped_network: NodeNetwork, transform: DAffine2, viewport_resolution: UVec2) -> Result { + async fn execute_network<'a>(&'a mut self, path: &[LayerId], graph: NodeNetwork, transform: DAffine2, viewport_resolution: UVec2) -> (Result, MonitorNodes) { if self.wasm_io.is_none() { self.wasm_io = Some(WasmApplicationIo::new().await); } - let editor_api = WasmEditorApi { font_cache: &self.font_cache, application_io: self.wasm_io.as_ref().unwrap(), @@ -197,15 +190,33 @@ impl NodeRuntime { image_frame: None, }; + use std::collections::hash_map::DefaultHasher; + use std::hash::Hash; + use std::hash::Hasher; + // Required to ensure that the appropriate protonodes are reinserted when the Editor API changes. + let mut graph_input_hash = DefaultHasher::new(); + editor_api.font_cache.hash(&mut graph_input_hash); + + let scoped_network = wrap_network_in_scope(graph, graph_input_hash.finish()); + + let monitor_nodes = scoped_network + .recursive_nodes() + .filter(|(_, node)| node.implementation == DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode<_>")) + .map(|(_, node)| node.path.clone().unwrap_or_default()) + .collect::>(); + // We assume only one output assert_eq!(scoped_network.outputs.len(), 1, "Graph with multiple outputs not yet handled"); let c = Compiler {}; - let proto_network = c.compile_single(scoped_network)?; + let proto_network = match c.compile_single(scoped_network) { + Ok(network) => network, + Err(e) => return (Err(e), monitor_nodes), + }; assert_ne!(proto_network.nodes.len(), 0, "No protonodes exist?"); if let Err(e) = self.executor.update(proto_network).await { error!("Failed to update executor:\n{e}"); - return Err(e); + return (Err(e), monitor_nodes); } use graph_craft::graphene_compiler::Executor; @@ -215,7 +226,11 @@ impl NodeRuntime { Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()), Some(t) => Err(format!("Invalid input type {t:?}")), _ => Err("No input type".to_string()), - }?; + }; + let result = match result { + Ok(value) => value, + Err(e) => return (Err(e), monitor_nodes), + }; if let TaggedValue::SurfaceFrame(SurfaceFrame { surface_id, transform: _ }) = result { let old_id = self.canvas_cache.insert(path.to_vec(), surface_id); @@ -227,7 +242,7 @@ impl NodeRuntime { } } } - Ok(result) + (Ok((result)), monitor_nodes) } /// Recomputes the thumbnails for the layers in the graph, modifying the state and updating the UI. diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 6c056951a8..9f8d46c4ee 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -652,7 +652,7 @@ impl JsEditorHandle { #[wasm_bindgen(js_name = selectNodes)] pub fn select_nodes(&self, nodes: Option>) { let nodes = nodes.unwrap_or_default(); - let message = NodeGraphMessage::SetSelectNodes { nodes }; + let message = NodeGraphMessage::SetSelectedNodes { nodes }; self.dispatch(message); } diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index f2fb9665e3..7832515ebe 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -41,6 +41,9 @@ pub struct DocumentNode { pub metadata: DocumentNodeMetadata, #[serde(default)] pub skip_deduplication: bool, + /// Used as a hash of the graph input where applicable. This ensures that protonodes that depend on the graph's input are always regenerated. + #[serde(default)] + pub hash: u64, pub path: Option>, } @@ -103,6 +106,7 @@ impl DocumentNode { construction_args: args, document_node_path: self.path.unwrap_or(Vec::new()), skip_deduplication: self.skip_deduplication, + hash: self.hash, } } diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index ec99e259ff..a7548d04f4 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -197,6 +197,7 @@ pub struct ProtoNode { pub identifier: NodeIdentifier, pub document_node_path: Vec, pub skip_deduplication: bool, + pub hash: u64, } /// A ProtoNodeInput represents the input of a node in a ProtoNetwork. @@ -235,6 +236,7 @@ impl ProtoNode { if self.skip_deduplication { self.document_node_path.hash(&mut hasher); } + self.hash.hash(&mut hasher); std::mem::discriminant(&self.input).hash(&mut hasher); match self.input { ProtoNodeInput::None => (), @@ -256,6 +258,7 @@ impl ProtoNode { input: ProtoNodeInput::None, document_node_path: path, skip_deduplication: false, + hash: 0, } } @@ -361,6 +364,7 @@ impl ProtoNetwork { input, document_node_path: path, skip_deduplication: false, + hash: 0, }, )); diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index a324830a59..82b1a47d8b 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -102,6 +102,7 @@ impl BorrowTree { old_nodes.remove(&id); } self.source_map.retain(|_, nid| !old_nodes.contains(nid)); + self.nodes.retain(|nid, _| !old_nodes.contains(nid)); Ok(old_nodes.into_iter().collect()) } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index fea973ae64..9d2f69cb37 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -730,9 +730,47 @@ fn node_registry() -> HashMap, input: VectorData, params: [DVec2, u32]), register_node!(graphene_core::vector::BoundingBoxNode, input: VectorData, params: []), register_node!(graphene_core::vector::CircularRepeatNode<_, _, _>, input: VectorData, params: [f32, f32, u32]), - register_node!(graphene_core::transform::CullNode<_>, input: Footprint, params: [VectorData]), + vec![( + NodeIdentifier::new("graphene_core::transform::CullNode<_>"), + |args| { + Box::pin(async move { + let mut args = args.clone(); + args.reverse(); + let node = >::new(graphene_std::any::input_node::(args.pop().expect("Not enough arguments provided to construct node"))); + let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); + Box::new(any) as Box NodeIO<'i, graph_craft::proto::Any<'i>, Output = (core::pin::Pin> + 'i>>)> + '_> + }) + }, + { + let node = >::new((graphene_std::any::PanicNode::<(), VectorData>::new())); + let params = vec![fn_type!((), VectorData)]; + let mut node_io = as NodeIO<'_, Footprint>>::to_node_io(&node, params); + node_io.input = concrete!(::Static); + node_io + }, + )], register_node!(graphene_core::transform::CullNode<_>, input: Footprint, params: [graphene_core::Artboard]), - register_node!(graphene_core::transform::CullNode<_>, input: Footprint, params: [graphene_core::GraphicGroup]), + vec![( + NodeIdentifier::new("graphene_core::transform::CullNode<_>"), + |args| { + Box::pin(async move { + let mut args = args.clone(); + args.reverse(); + let node = >::new(graphene_std::any::input_node::( + args.pop().expect("Not enough arguments provided to construct node"), + )); + let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); + Box::new(any) as Box NodeIO<'i, graph_craft::proto::Any<'i>, Output = (core::pin::Pin> + 'i>>)> + '_> + }) + }, + { + let node = >::new((graphene_std::any::PanicNode::<(), graphene_core::GraphicGroup>::new())); + let params = vec![fn_type!((), graphene_core::GraphicGroup)]; + let mut node_io = as NodeIO<'_, Footprint>>::to_node_io(&node, params); + node_io.input = concrete!(::Static); + node_io + }, + )], register_node!(graphene_std::raster::SampleNode<_>, input: Footprint, params: [ImageFrame]), register_node!(graphene_std::raster::MandelbrotNode, input: Footprint, params: []), register_node!(graphene_core::vector::ResamplePoints<_>, input: VectorData, params: [f64]),