diff --git a/editor/src/messages/clipboard/clipboard_message_handler.rs b/editor/src/messages/clipboard/clipboard_message_handler.rs index a5bbbd9e5f..65122d80b3 100644 --- a/editor/src/messages/clipboard/clipboard_message_handler.rs +++ b/editor/src/messages/clipboard/clipboard_message_handler.rs @@ -182,13 +182,8 @@ impl MessageHandler> for Clipboard let mut resource_ids = HashSet::new(); for item in &items { match item { - ClipboardItem::Layer(entry) => entry - .nodes - .iter() - .for_each(|(_, template)| network_interface::collect_node_resources(&template.document_node, &mut resource_ids)), - ClipboardItem::Nodes(nodes) => nodes - .iter() - .for_each(|(_, template)| network_interface::collect_node_resources(&template.document_node, &mut resource_ids)), + ClipboardItem::Layer(entry) => entry.nodes.iter().for_each(|(_, template)| network_interface::collect_template_resources(template, &mut resource_ids)), + ClipboardItem::Nodes(nodes) => nodes.iter().for_each(|(_, template)| network_interface::collect_template_resources(template, &mut resource_ids)), ClipboardItem::Vector(_) | ClipboardItem::Resource(_) => {} } } @@ -256,13 +251,13 @@ impl MessageHandler> for Clipboard match item { ClipboardItem::Layer(mut entry) => { for (_, template) in &mut entry.nodes { - template.document_node.normalize_stored_types(); + template.normalize_stored_types(); } layers.push(entry); } ClipboardItem::Nodes(mut nodes) => { for (_, template) in &mut nodes { - template.document_node.normalize_stored_types(); + template.normalize_stored_types(); } node_groups.push(nodes); } diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index cc53b14997..00351bf6b9 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -5,8 +5,7 @@ use super::node_properties::{self, ParameterWidgetsInfo}; use super::utility_types::FrontendNodeType; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::utility_types::network_interface::{ - DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata, - Vec2InputSettings, WidgetOverride, + InputMetadata, NodeNetworkInterface, NodeNetworkTemplate, NodeTemplate, NodeTemplateImplementation, NodeTypePersistentMetadata, Vec2InputSettings, WidgetOverride, }; use crate::messages::prelude::{FontsMessage, FontsMessageHandler, Message, ResourceMessageHandler, Responses}; use crate::node_graph_executor::NodeGraphExecutor; @@ -132,14 +131,8 @@ fn document_node_definitions() -> HashMap HashMap HashMap HashMap HashMap) - DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(vector::as_vector::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(2), 0)], - ..Default::default() - }, - // 4: Flatten Vector - DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(graphic_nodes::graphic::flatten_vector::IDENTIFIER), - inputs: vec![NodeInput::import(generic!(T), 0)], - ..Default::default() - }, - // 5: Map - DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(graphic::map::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(4), 0), NodeInput::node(NodeId(3), 0)], - ..Default::default() - }, - // 6: Combine Paths - DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(vector::combine_paths::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(5), 0)], - ..Default::default() - }, - // 7: Points to Polyline - DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(vector::points_to_polyline::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(6), 0), NodeInput::value(TaggedValue::Bool(false), false)], - ..Default::default() - }, - ] - .into_iter() - .enumerate() - .map(|(id, node)| (NodeId(id as u64), node)) - .collect(), - ..Default::default() - }), - inputs: vec![NodeInput::type_default(list!(Vector), true)], - ..Default::default() - }, - persistent_node_metadata: DocumentNodePersistentMetadata { - input_metadata: vec![("Vector", "TODO").into()], - output_names: vec!["Vector".to_string()], - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), - network_metadata: Some(NodeNetworkMetadata { - persistent_metadata: NodeNetworkPersistentMetadata { - node_metadata: [ - // 0: Read Vector - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 1)), - ..Default::default() - }, - ..Default::default() - }, - // 1: Extract Transform - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 1)), - ..Default::default() - }, - ..Default::default() - }, - // 2: Decompose Transform - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 1)), - ..Default::default() - }, - ..Default::default() - }, - // 3: As Vector - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 1)), - ..Default::default() - }, - ..Default::default() - }, - // 4: Flatten Vector - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 0)), - ..Default::default() - }, - ..Default::default() - }, - // 5: Map - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 0)), - ..Default::default() - }, - ..Default::default() - }, - // 6: Combine Paths - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(35, 0)), - ..Default::default() - }, - ..Default::default() - }, - // 7: Points to Polyline - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(42, 0)), - ..Default::default() - }, - ..Default::default() - }, - ] - .into_iter() - .enumerate() - .map(|(id, node)| (NodeId(id as u64), node)) - .collect(), + implementation: NodeTemplateImplementation::Network(NodeNetworkTemplate { + exports: vec![NodeInput::node(NodeId(7), 0)], + nodes: [ + // 0: Read Vector + NodeTemplate { + implementation: NodeTemplateImplementation::ProtoNode(context::read_vector::IDENTIFIER), + inputs: vec![NodeInput::value(TaggedValue::None, false)], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 1)), + ..Default::default() + }, + // 1: Extract Transform + NodeTemplate { + implementation: NodeTemplateImplementation::ProtoNode(transform_nodes::extract_transform::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(0), 0)], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 1)), + ..Default::default() + }, + // 2: Decompose Translation + NodeTemplate { + implementation: NodeTemplateImplementation::ProtoNode(transform_nodes::decompose_translation::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(1), 0)], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 1)), + ..Default::default() + }, + // 3: As Vector (auto-converts the decomposed translation into a single-anchor List) + NodeTemplate { + implementation: NodeTemplateImplementation::ProtoNode(vector::as_vector::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(2), 0)], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 1)), + ..Default::default() + }, + // 4: Flatten Vector + NodeTemplate { + implementation: NodeTemplateImplementation::ProtoNode(graphic_nodes::graphic::flatten_vector::IDENTIFIER), + inputs: vec![NodeInput::import(generic!(T), 0)], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 0)), + ..Default::default() + }, + // 5: Map + NodeTemplate { + implementation: NodeTemplateImplementation::ProtoNode(graphic::map::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(4), 0), NodeInput::node(NodeId(3), 0)], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 0)), + ..Default::default() + }, + // 6: Combine Paths + NodeTemplate { + implementation: NodeTemplateImplementation::ProtoNode(vector::combine_paths::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(5), 0)], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(35, 0)), + ..Default::default() + }, + // 7: Points to Polyline + NodeTemplate { + implementation: NodeTemplateImplementation::ProtoNode(vector::points_to_polyline::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(6), 0), NodeInput::value(TaggedValue::Bool(false), false)], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(42, 0)), ..Default::default() }, - ..Default::default() - }), + ] + .into_iter() + .enumerate() + .map(|(id, node)| (NodeId(id as u64), node)) + .collect(), ..Default::default() - }, + }), + inputs: vec![NodeInput::type_default(list!(Vector), true)], + input_metadata: vec![("Vector", "TODO").into()], + output_names: vec!["Vector".to_string()], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), + ..Default::default() }, description: Cow::Borrowed("TODO"), properties: None, @@ -913,61 +572,32 @@ fn document_node_definitions() -> HashMap HashMap HashMap), 0), - NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Red), false), - ], - implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER), - call_argument: generic!(T), - ..Default::default() - }, - DocumentNode { - inputs: vec![ - NodeInput::import(list!(Raster), 0), - NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Green), false), - ], - implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER), - call_argument: generic!(T), - ..Default::default() - }, - DocumentNode { - inputs: vec![ - NodeInput::import(list!(Raster), 0), - NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Blue), false), - ], - implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER), - call_argument: generic!(T), - ..Default::default() - }, - DocumentNode { - inputs: vec![ - NodeInput::import(list!(Raster), 0), - NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Alpha), false), - ], - implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER), - call_argument: generic!(T), - ..Default::default() - }, - ] - .into_iter() - .enumerate() - .map(|(id, node)| (NodeId(id as u64), node)) - .collect(), - ..Default::default() - }), - inputs: vec![NodeInput::type_default(list!(Raster), true)], - ..Default::default() - }, - persistent_node_metadata: DocumentNodePersistentMetadata { - input_metadata: vec![("Image", "TODO").into()], - output_names: vec!["".to_string(), "Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()], - network_metadata: Some(NodeNetworkMetadata { - persistent_metadata: NodeNetworkPersistentMetadata { - node_metadata: [ - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), - ..Default::default() - }, - ..Default::default() - }, - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 2)), - ..Default::default() - }, - ..Default::default() - }, - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 4)), - ..Default::default() - }, - ..Default::default() - }, - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 6)), - ..Default::default() - }, - ..Default::default() - }, - ] - .into_iter() - .enumerate() - .map(|(id, node)| (NodeId(id as u64), node)) - .collect(), + implementation: NodeTemplateImplementation::Network(NodeNetworkTemplate { + exports: vec![ + NodeInput::value(TaggedValue::None, false), + NodeInput::node(NodeId(0), 0), + NodeInput::node(NodeId(1), 0), + NodeInput::node(NodeId(2), 0), + NodeInput::node(NodeId(3), 0), + ], + nodes: [ + NodeTemplate { + inputs: vec![ + NodeInput::import(list!(Raster), 0), + NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Red), false), + ], + implementation: NodeTemplateImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER), + call_argument: generic!(T), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), + ..Default::default() + }, + NodeTemplate { + inputs: vec![ + NodeInput::import(list!(Raster), 0), + NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Green), false), + ], + implementation: NodeTemplateImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER), + call_argument: generic!(T), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 2)), + ..Default::default() + }, + NodeTemplate { + inputs: vec![ + NodeInput::import(list!(Raster), 0), + NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Blue), false), + ], + implementation: NodeTemplateImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER), + call_argument: generic!(T), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 4)), + ..Default::default() + }, + NodeTemplate { + inputs: vec![ + NodeInput::import(list!(Raster), 0), + NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Alpha), false), + ], + implementation: NodeTemplateImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER), + call_argument: generic!(T), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 6)), ..Default::default() }, - ..Default::default() - }), + ] + .into_iter() + .enumerate() + .map(|(id, node)| (NodeId(id as u64), node)) + .collect(), ..Default::default() - }, + }), + inputs: vec![NodeInput::type_default(list!(Raster), true)], + input_metadata: vec![("Image", "TODO").into()], + output_names: vec!["".to_string(), "Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()], + ..Default::default() }, description: Cow::Borrowed("TODO"), properties: None, @@ -1172,64 +726,34 @@ fn document_node_definitions() -> HashMap HashMap HashMap), each item carries `start`/`end`/`name` attributes from `regex_find` - NodeInput::node(NodeId(2), 0), - ], - nodes: [ - // Node 0: regex_find proto node — returns List of [whole_match, ...capture_groups] - DocumentNode { - inputs: vec![ - NodeInput::import(item!(String), 0), - NodeInput::import(item!(String), 1), - NodeInput::import(item!(f64), 2), - NodeInput::import(item!(bool), 3), - NodeInput::import(item!(bool), 4), - ], - implementation: DocumentNodeImplementation::ProtoNode(text_nodes::regex::regex_find::IDENTIFIER), - ..Default::default() - }, - // Node 1: item_at_index at index 0, extracts the whole match as a bare String (drops the item's start/end/name attributes since the unwrapped String can't carry them) - DocumentNode { - inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)], - implementation: DocumentNodeImplementation::ProtoNode(graphic::item_at_index::IDENTIFIER), - ..Default::default() - }, - // Node 2: remove_at_index at index 0, returns the capture group items as a List, preserving each item's start/end/name attributes - DocumentNode { - inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)], - implementation: DocumentNodeImplementation::ProtoNode(graphic::remove_at_index::IDENTIFIER), - ..Default::default() - }, - ] - .into_iter() - .enumerate() - .map(|(id, node)| (NodeId(id as u64), node)) - .collect(), - ..Default::default() - }), - inputs: vec![ - NodeInput::value(TaggedValue::String(String::new()), true), - NodeInput::value(TaggedValue::String(String::new()), false), - NodeInput::value(TaggedValue::F64(0.), false), - NodeInput::value(TaggedValue::Bool(false), false), - NodeInput::value(TaggedValue::Bool(false), false), + implementation: NodeTemplateImplementation::Network(NodeNetworkTemplate { + exports: vec![ + // Primary output: the whole match (String) + NodeInput::node(NodeId(1), 0), + // Secondary output: capture groups (List), each item carries `start`/`end`/`name` attributes from `regex_find` + NodeInput::node(NodeId(2), 0), ], - ..Default::default() - }, - persistent_node_metadata: DocumentNodePersistentMetadata { - input_metadata: vec![ - ("String", "The string to search within.").into(), - ("Pattern", "The regular expression pattern to search for.").into(), - ( - "Match Index", - "Which non-overlapping occurrence of the pattern to return, starting from 0 for the first match. Negative indices count backwards from the last match.", - ) - .into(), - ("Case Insensitive", "Match letters regardless of case.").into(), - ("Multiline", "Make `^` and `$` match the start and end of each line, not just the whole string.").into(), - ], - output_names: vec!["Match".to_string(), "Captures".to_string()], - network_metadata: Some(NodeNetworkMetadata { - persistent_metadata: NodeNetworkPersistentMetadata { - node_metadata: [ - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), - ..Default::default() - }, - ..Default::default() - }, - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(8, 0)), - ..Default::default() - }, - ..Default::default() - }, - DocumentNodeMetadata { - persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(8, 2)), - ..Default::default() - }, - ..Default::default() - }, - ] - .into_iter() - .enumerate() - .map(|(id, node)| (NodeId(id as u64), node)) - .collect(), + nodes: [ + // Node 0: regex_find proto node — returns List of [whole_match, ...capture_groups] + NodeTemplate { + inputs: vec![ + NodeInput::import(item!(String), 0), + NodeInput::import(item!(String), 1), + NodeInput::import(item!(f64), 2), + NodeInput::import(item!(bool), 3), + NodeInput::import(item!(bool), 4), + ], + implementation: NodeTemplateImplementation::ProtoNode(text_nodes::regex::regex_find::IDENTIFIER), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), + ..Default::default() + }, + // Node 1: item_at_index at index 0, extracts the whole match as a bare String (drops the item's start/end/name attributes since the unwrapped String can't carry them) + NodeTemplate { + inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)], + implementation: NodeTemplateImplementation::ProtoNode(graphic::item_at_index::IDENTIFIER), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(8, 0)), ..Default::default() }, - ..Default::default() - }), + // Node 2: remove_at_index at index 0, returns the capture group items as a List, preserving each item's start/end/name attributes + NodeTemplate { + inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)], + implementation: NodeTemplateImplementation::ProtoNode(graphic::remove_at_index::IDENTIFIER), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(8, 2)), + ..Default::default() + }, + ] + .into_iter() + .enumerate() + .map(|(id, node)| (NodeId(id as u64), node)) + .collect(), ..Default::default() - }, + }), + inputs: vec![ + NodeInput::value(TaggedValue::String(String::new()), true), + NodeInput::value(TaggedValue::String(String::new()), false), + NodeInput::value(TaggedValue::F64(0.), false), + NodeInput::value(TaggedValue::Bool(false), false), + NodeInput::value(TaggedValue::Bool(false), false), + ], + input_metadata: vec![ + ("String", "The string to search within.").into(), + ("Pattern", "The regular expression pattern to search for.").into(), + ( + "Match Index", + "Which non-overlapping occurrence of the pattern to return, starting from 0 for the first match. Negative indices count backwards from the last match.", + ) + .into(), + ("Case Insensitive", "Match letters regardless of case.").into(), + ("Multiline", "Make `^` and `$` match the start and end of each line, not just the whole string.").into(), + ], + output_names: vec!["Match".to_string(), "Captures".to_string()], + ..Default::default() }, description: Cow::Borrowed( r#"Finds a portion of the string matching a regular expression pattern. With "Match Index" at its default 0, it selects the first non-overlapping occurrence, but others may be selected. Capture groups, if any, are produced as a list in the "Captures" output."#, @@ -1367,71 +851,42 @@ fn document_node_definitions() -> HashMap Vec { .map(|(identifier, definition)| { let input_types = definition .node_template - .document_node .inputs .iter() .map(|node_input| node_input.as_value().map(|node_value| node_value.ty().nested_type().to_string()).unwrap_or_default()) .collect::>(); - let mut name = definition.node_template.persistent_node_metadata.display_name.clone(); + let mut name = definition.node_template.display_name.clone(); if name.is_empty() { name = identifier.implementation_name_from_identifier() } @@ -2061,72 +1515,19 @@ impl DocumentNodeDefinition { // Only value inputs should be overridden, since node inputs change graph structure and must be handled by the network interface. // However, this would require changing some tooling which creates multiple nodes at once, before they are inserted into the network. // debug_assert!(input_override.as_node().is_none(), "Node inputs are not supported in input overrides"); - template.document_node.inputs[index] = input_override; + template.inputs[index] = input_override; } }); - // Ensure that the input properties are initialized for every Document Node input for every node - fn populate_input_properties(node_template: &mut NodeTemplate, mut path: Vec) { - if let Some(current_node) = path.pop() { - let DocumentNodeImplementation::Network(template_network) = &node_template.document_node.implementation else { - log::error!("Template network should always exist"); - return; - }; - let Some(nested_network) = template_network.nested_network(&path) else { - log::error!("Nested network should exist for path"); - return; - }; - let Some(input_length) = nested_network.nodes.get(¤t_node).map(|node| node.inputs.len()) else { - log::error!("Could not get current node in nested network"); - return; - }; - let Some(template_network_metadata) = &mut node_template.persistent_node_metadata.network_metadata else { - log::error!("Template should have metadata if it has network implementation"); - return; - }; - let Some(nested_network_metadata) = template_network_metadata.nested_metadata_mut(&path) else { - log::error!("Path is not valid for network"); - return; - }; - let Some(nested_node_metadata) = nested_network_metadata.persistent_metadata.node_metadata.get_mut(¤t_node) else { - log::error!("Path is not valid for network"); - return; - }; - nested_node_metadata.persistent_metadata.input_metadata.resize_with(input_length, InputMetadata::default); - - // Recurse over all sub-nodes if the current node is a network implementation - let mut current_path = path.clone(); - current_path.push(current_node); - let DocumentNodeImplementation::Network(template_network) = &node_template.document_node.implementation else { - log::error!("Template network should always exist"); - return; - }; - if let Some(current_nested_network) = template_network.nested_network(¤t_path) { - for sub_node_id in current_nested_network.nodes.keys().cloned().collect::>() { - let mut sub_path = current_path.clone(); - sub_path.push(sub_node_id); - populate_input_properties(node_template, sub_path); - } - }; - } else { - // Base case - let input_len = node_template.document_node.inputs.len(); - node_template.persistent_node_metadata.input_metadata.resize_with(input_len, InputMetadata::default); - if let DocumentNodeImplementation::Network(node_template_network) = &node_template.document_node.implementation { - for sub_node_id in node_template_network.nodes.keys().cloned().collect::>() { - populate_input_properties(node_template, vec![sub_node_id]); - } - } - } - } - populate_input_properties(&mut template, Vec::new()); + // Ensure that the input properties are initialized for every input of every node + template.normalize_input_metadata(); template } /// Converts the [DocumentNodeDefinition] type to a [NodeTemplate], completely default. pub fn default_node_template(&self) -> NodeTemplate { - self.node_template_input_override(self.node_template.document_node.inputs.clone().into_iter().map(Some)) + self.node_template_input_override(self.node_template.inputs.clone().into_iter().map(Some)) } } diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs index 78098dc950..f8a96b0c8c 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs @@ -1,7 +1,6 @@ use super::DocumentNodeDefinition; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, InputMetadata, NodeTemplate, WidgetOverride}; -use graph_craft::document::*; +use crate::messages::portfolio::document::utility_types::network_interface::{InputMetadata, NodeTemplate, NodeTemplateImplementation, WidgetOverride}; use graphene_std::registry::*; use graphene_std::*; use std::collections::{HashMap, HashSet}; @@ -14,7 +13,7 @@ pub(super) fn post_process_nodes(custom: Vec) -> HashMap let network_nodes = custom .into_iter() .filter_map(|definition| { - if let DocumentNodeImplementation::ProtoNode(proto_node_identifier) = &definition.node_template.document_node.implementation { + if let NodeTemplateImplementation::ProtoNode(proto_node_identifier) = &definition.node_template.implementation { definitions_map.insert(DefinitionIdentifier::ProtoNode(proto_node_identifier.clone()), definition); return None; }; @@ -72,29 +71,21 @@ pub(super) fn post_process_nodes(custom: Vec) -> HashMap DocumentNodeDefinition { identifier: display_name, node_template: NodeTemplate { - document_node: DocumentNode { - inputs, - call_argument: input_type.clone(), - implementation: DocumentNodeImplementation::ProtoNode(id.clone()), - visible: true, - skip_deduplication: false, - context_features: ContextDependencies::from(context_features.as_slice()), - ..Default::default() - }, - persistent_node_metadata: DocumentNodePersistentMetadata { - // TODO: Store information for input overrides in the node macro - input_metadata: fields - .iter() - .map(|f| match f.widget_override { - RegistryWidgetOverride::None => (f.name, f.description).into(), - RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden), - RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())), - RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())), - }) - .collect(), - locked: false, - ..Default::default() - }, + inputs, + call_argument: input_type.clone(), + implementation: NodeTemplateImplementation::ProtoNode(id.clone()), + context_features: ContextDependencies::from(context_features.as_slice()), + // TODO: Store information for input overrides in the node macro + input_metadata: fields + .iter() + .map(|f| match f.widget_override { + RegistryWidgetOverride::None => (f.name, f.description).into(), + RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden), + RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())), + RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())), + }) + .collect(), + ..Default::default() }, category, description: Cow::Borrowed(description), @@ -105,13 +96,14 @@ pub(super) fn post_process_nodes(custom: Vec) -> HashMap // Add the rest of the network nodes to the map and add the metadata for their internal protonodes for mut network_node in network_nodes { - traverse_node(&network_node.node_template.document_node, &mut network_node.node_template.persistent_node_metadata, &definitions_map); + fill_proto_node_metadata(&mut network_node.node_template, &definitions_map); + // Set the reference to the node identifier - if let Some(nested_metadata) = network_node.node_template.persistent_node_metadata.network_metadata.as_mut() { - nested_metadata.persistent_metadata.reference = Some(network_node.identifier.to_string()); + if let NodeTemplateImplementation::Network(network_template) = &mut network_node.node_template.implementation { + network_template.reference = Some(network_node.identifier.to_string()); // If it is not a merge node, then set the display name to the identifier/reference if network_node.identifier != "Merge" { - network_node.node_template.persistent_node_metadata.display_name = network_node.identifier.to_string(); + network_node.node_template.display_name = network_node.identifier.to_string(); } } definitions_map.insert(DefinitionIdentifier::Network(network_node.identifier.to_string()), network_node); @@ -120,27 +112,27 @@ pub(super) fn post_process_nodes(custom: Vec) -> HashMap definitions_map } -/// Traverses a document node template and metadata in parallel to add metadata to the protonodes -fn traverse_node(node: &DocumentNode, node_metadata: &mut DocumentNodePersistentMetadata, definitions_map: &HashMap) { - match &node.implementation { - DocumentNodeImplementation::Network(node_network) => { - for (nested_node_id, nested_node) in node_network.nodes.iter() { - let nested_metadata = node_metadata.network_metadata.as_mut().unwrap().persistent_metadata.node_metadata.get_mut(nested_node_id).unwrap(); - traverse_node(nested_node, &mut nested_metadata.persistent_metadata, definitions_map); +/// Recursively fills each nested proto node's editor metadata from its definition, preserving only the authored position. +fn fill_proto_node_metadata(node_template: &mut NodeTemplate, definitions_map: &HashMap) { + match &mut node_template.implementation { + NodeTemplateImplementation::Network(network_template) => { + for nested_template in network_template.nodes.values_mut() { + fill_proto_node_metadata(nested_template, definitions_map); } } - DocumentNodeImplementation::ProtoNode(id) => { - // Set all the metadata except the position to the proto node information from the macro - // TODO: Use options in the template to specify what you want to default and what you want to override - // If this fails then the proto node id in the definition doesn't match what is generated by the macro + NodeTemplateImplementation::ProtoNode(id) => { + // If this lookup fails then the proto node id in the definition doesn't match what is generated by the macro let Some(definition) = definitions_map.get(&DefinitionIdentifier::ProtoNode(id.clone())) else { - // log::error!("Could not get definition for id {} when filling in protonode metadata for a custom node", id.clone()); return; }; - let mut new_metadata = definition.node_template.persistent_node_metadata.clone(); - new_metadata.node_type_metadata = node_metadata.node_type_metadata.clone(); - *node_metadata = new_metadata + + let definition_template = &definition.node_template; + node_template.display_name = definition_template.display_name.clone(); + node_template.input_metadata = definition_template.input_metadata.clone(); + node_template.output_names = definition_template.output_names.clone(); + node_template.locked = definition_template.locked; + node_template.pinned = definition_template.pinned; } - DocumentNodeImplementation::Extract => {} + NodeTemplateImplementation::Extract => {} } } 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 239d87cfb0..430787802e 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 @@ -301,7 +301,7 @@ impl<'a> MessageHandler> for NodeG // A freshly added Text node carries no font, so give it the default font (registered like the Text tool does) if node_type == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) { let font_resource_id = graph_craft::application_io::resource::ResourceId::new(); - if let Some(font_input) = node_template.document_node.inputs.get_mut(graphene_std::text::text::FontInput::INDEX) { + if let Some(font_input) = node_template.inputs.get_mut(graphene_std::text::text::FontInput::INDEX) { *font_input = NodeInput::value(TaggedValue::Resource(font_resource_id), false); } responses.add(DocumentMessage::Resource(ResourceMessage::AddFont { @@ -336,7 +336,7 @@ impl<'a> MessageHandler> for NodeG }; // Ensure connection is to correct input of new node. If it does not have an input then do not connect - if let Some((input_index, _)) = node_template.document_node.inputs.iter().enumerate().find(|(_, input)| input.is_exposed()) { + if let Some((input_index, _)) = node_template.inputs.iter().enumerate().find(|(_, input)| input.is_exposed()) { responses.add(NodeGraphMessage::CreateWire { output_connector: *output_connector, input_connector: InputConnector::node(node_id, input_index), @@ -693,7 +693,7 @@ impl<'a> MessageHandler> for NodeG return; }; let center_of_selected_nodes_grid_space = IVec2::new((center_of_selected_nodes.x / 24. + 0.5).floor() as i32, (center_of_selected_nodes.y / 24. + 0.5).floor() as i32); - default_node_template.persistent_node_metadata.node_type_metadata = NodeTypePersistentMetadata::node(center_of_selected_nodes_grid_space - IVec2::new(3, 1)); + default_node_template.node_type_metadata = NodeTypePersistentMetadata::node(center_of_selected_nodes_grid_space - IVec2::new(3, 1)); responses.add(DocumentMessage::AddTransaction); responses.add(NodeGraphMessage::InsertNode { node_id: encapsulating_node_id, diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index f1800fcb8d..e7d2a7bba3 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -10,11 +10,13 @@ mod queries; mod resolved_types; pub mod storage_metadata; mod structure; +mod template; mod types; #[cfg(test)] mod validation; mod view; +pub use template::*; pub use types::*; pub use view::{NetworkError, NetworkView}; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/caches.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/caches.rs index ac65d06b40..794e15558e 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/caches.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/caches.rs @@ -193,41 +193,21 @@ impl NodeNetworkInterface { let mut not_sole_dependents = HashSet::new(); sole_dependents.insert(*stack_top); for upstream_node in self.upstream_flow_back_from_nodes(vec![*stack_top], network_path, FlowType::UpstreamFlow).collect::>() { - let mut stack = vec![upstream_node]; - let mut is_sole_dependent = true; - while let Some(current_node) = stack.pop() { - if not_sole_dependents.contains(¤t_node) { - is_sole_dependent = false; - break; - } - if !sole_dependents.contains(¤t_node) { - let mut has_outward_wire = false; - for output_index in 0..self.number_of_outputs(¤t_node, network_path) { - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Cannot load outward wires in load_stack_dependents"); - continue; - }; - let Some(outward_wires) = outward_wires.get(&OutputConnector::node(current_node, output_index)) else { - log::error!("Cannot load outward wires in load_stack_dependents"); - continue; - }; - for downstream_input in outward_wires { - has_outward_wire = true; - match downstream_input { - InputConnector::Node { node_id, .. } => stack.push(*node_id), - InputConnector::Export(_) => is_sole_dependent = false, - } - } - } - if !has_outward_wire { - is_sole_dependent = false; - } - } - if !is_sole_dependent { - break; - } + if sole_dependents.contains(&upstream_node) || not_sole_dependents.contains(&upstream_node) { + continue; } + // A path terminates at an already-verified sole dependent, and fails fast through a known non-sole node + let is_sole_dependent = self.is_sole_dependent(upstream_node, network_path, |downstream_node, _| { + if not_sole_dependents.contains(&downstream_node) { + SoleDependentStep::Escape + } else if sole_dependents.contains(&downstream_node) { + SoleDependentStep::Terminate + } else { + SoleDependentStep::Continue + } + }); + if is_sole_dependent { sole_dependents.insert(upstream_node); } else { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/characterization_tests.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/characterization_tests.rs index 40ef78ad4f..9f62742b7f 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/characterization_tests.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/characterization_tests.rs @@ -73,6 +73,30 @@ async fn deleting_a_node_with_children_prunes_them_from_the_selection() { assert_invariants(&editor, "after deleting a node with children"); } +#[tokio::test] +async fn deleting_a_node_keeps_children_shared_with_other_nodes() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + let parent = editor.create_node_by_name(rectangle_definition()).await; + let sibling = editor.create_node_by_name(rectangle_definition()).await; + let shared_child = editor.create_node_by_name(rectangle_definition()).await; + + let network_interface = &mut editor.active_document_mut().network_interface; + + // Wire the same child into the secondary inputs of both nodes, then delete only the parent along with its children + network_interface.set_input(&InputConnector::node(parent, 1), NodeInput::node(shared_child, 0), &[]); + network_interface.set_input(&InputConnector::node(sibling, 1), NodeInput::node(shared_child, 0), &[]); + network_interface.delete_nodes(vec![parent], true, &[]); + + let nodes = &network_interface.document_network().nodes; + assert!(!nodes.contains_key(&parent), "The deleted node itself should be gone"); + assert!(nodes.contains_key(&shared_child), "A child shared with another node is not a sole dependent and should survive"); + assert!(nodes.contains_key(&sibling), "The unrelated sibling should survive"); + + assert_invariants(&editor, "after deleting a node with a shared child"); +} + #[tokio::test] async fn cyclic_connection_is_rejected_without_side_effects() { let mut editor = EditorTestUtils::create(); diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs index 354a26a277..9f8b5bae99 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs @@ -557,14 +557,13 @@ impl NodeNetworkInterface { log::error!("Could not get node in set_implementation"); return; }; - let new_implementation = std::mem::take(&mut new_template.document_node.implementation); - let _ = std::mem::replace(&mut node.implementation, new_implementation); + let (new_implementation, new_network_metadata) = std::mem::take(&mut new_template.implementation).into_parts(); + node.implementation = new_implementation; let Some(metadata) = self.node_metadata_mut(node_id, network_path) else { log::error!("Could not get metadata in set_implementation"); return; }; - let new_metadata = std::mem::take(&mut new_template.persistent_node_metadata.network_metadata); - let _ = std::mem::replace(&mut metadata.persistent_metadata.network_metadata, new_metadata); + metadata.persistent_metadata.network_metadata = new_network_metadata; } /// Replaces the inputs and corresponding metadata. @@ -577,13 +576,13 @@ impl NodeNetworkInterface { log::error!("Could not get node in set_implementation"); return None; }; - let new_inputs = std::mem::take(&mut new_template.document_node.inputs); + let new_inputs = std::mem::take(&mut new_template.inputs); let old_inputs = std::mem::replace(&mut node.inputs, new_inputs); let Some(metadata) = self.node_metadata_mut(node_id, network_path) else { log::error!("Could not get metadata in set_implementation"); return None; }; - let new_metadata = std::mem::take(&mut new_template.persistent_node_metadata.input_metadata); + let new_metadata = std::mem::take(&mut new_template.input_metadata); let _ = std::mem::replace(&mut metadata.persistent_metadata.input_metadata, new_metadata); Some(old_inputs) } @@ -597,7 +596,7 @@ impl NodeNetworkInterface { .reference(node_id, network_path) .as_ref() .and_then(resolve_document_node_type) - .and_then(|definition| definition.node_template.persistent_node_metadata.input_metadata.get(added_input_index)) + .and_then(|definition| definition.node_template.input_metadata.get(added_input_index)) .cloned(); let Some(metadata) = self.node_metadata_mut(node_id, network_path) else { return }; metadata.persistent_metadata.input_metadata.push(input_metadata.unwrap_or_default()); @@ -1035,12 +1034,13 @@ impl NodeNetworkInterface { node_template = self.map_ids(node_template, &old_node_id, &new_ids, network_path); // Insert node into network let node_id = *new_ids.get(&old_node_id).unwrap(); + let (document_node, persistent_metadata) = node_template.into_parts(); let Some(network) = self.network_mut(network_path) else { log::error!("Network not found in insert_node"); return; }; - network.nodes.insert(node_id, node_template.document_node); + network.nodes.insert(node_id, document_node); self.transaction_modified(); let Some(network_metadata) = self.network_metadata_mut(network_path) else { @@ -1048,7 +1048,7 @@ impl NodeNetworkInterface { return; }; let node_metadata = DocumentNodeMetadata { - persistent_metadata: node_template.persistent_node_metadata, + persistent_metadata, transient_metadata: DocumentNodeTransientMetadata::default(), }; network_metadata.persistent_metadata.node_metadata.insert(node_id, node_metadata); @@ -1063,17 +1063,17 @@ impl NodeNetworkInterface { /// Used to insert a node template with no node/network inputs into the network and returns the a NodeTemplate with information from the previous node, if it existed. pub fn insert_node(&mut self, node_id: NodeId, node_template: NodeTemplate, network_path: &[NodeId]) -> Option { let has_node_or_network_input = node_template - .document_node .inputs .iter() .all(|input| !(matches!(input, NodeInput::Node { .. }) || matches!(input, NodeInput::Import { .. }))); assert!(has_node_or_network_input, "Cannot insert node with node or network inputs. Use insert_node_group instead"); + let (document_node, persistent_metadata) = node_template.into_parts(); let Some(network) = self.network_mut(network_path) else { log::error!("Network not found in insert_node"); return None; }; - let previous_node = network.nodes.insert(node_id, node_template.document_node); + let previous_node = network.nodes.insert(node_id, document_node); self.transaction_modified(); let Some(network_metadata) = self.network_metadata_mut(network_path) else { @@ -1081,7 +1081,7 @@ impl NodeNetworkInterface { return None; }; let node_metadata = DocumentNodeMetadata { - persistent_metadata: node_template.persistent_node_metadata, + persistent_metadata, transient_metadata: DocumentNodeTransientMetadata::default(), }; let previous_metadata = network_metadata.persistent_metadata.node_metadata.insert(node_id, node_metadata); @@ -1089,18 +1089,26 @@ impl NodeNetworkInterface { self.unload_all_nodes_bounding_box(network_path); self.unload_node_click_targets(&node_id, network_path); - previous_node.zip(previous_metadata).map(|(document_node, node_metadata)| NodeTemplate { - document_node, - persistent_node_metadata: node_metadata.persistent_metadata, - }) + previous_node + .zip(previous_metadata) + .map(|(document_node, node_metadata)| NodeTemplate::from_parts(document_node, node_metadata.persistent_metadata)) } /// Deletes all nodes in `node_ids` and any sole dependents in the horizontal chain if the node to delete is a layer node. pub fn delete_nodes(&mut self, nodes_to_delete: Vec, delete_children: bool, network_path: &[NodeId]) { - let Some(outward_wires) = self.outward_wires(network_path).cloned() else { + if self.outward_wires(network_path).is_none() { log::error!("Could not get outward wires in delete_nodes"); return; - }; + } + + // Layer membership is fixed during the expansion phase, so gather it once for the sole-dependent closure + let layer_nodes = self + .nested_network(network_path) + .map(|network| network.nodes.keys().copied().collect::>()) + .unwrap_or_default() + .into_iter() + .filter(|candidate| self.is_layer(candidate, network_path)) + .collect::>(); let mut delete_nodes = HashSet::new(); for node_id in &nodes_to_delete { @@ -1121,45 +1129,18 @@ impl NodeNetworkInterface { upstream_nodes.push(upstream_node); } } - // For each potential child perform a complete downstream traversal, ending at either a node in the `delete_nodes` set (excluding layer bottom inputs), the output, or a dead end. - // If the output node is eventually reached, then it is not a sole dependent and will not be deleted - let mut stack = vec![upstream_node]; - let mut can_delete = true; - while let Some(current_node) = stack.pop() { - let mut is_dead_end = true; - for output_connector in (0..self.number_of_outputs(¤t_node, network_path)).map(|output_index| OutputConnector::node(current_node, output_index)) { - let Some(downstream_nodes) = outward_wires.get(&output_connector) else { continue }; - if !downstream_nodes.is_empty() { - is_dead_end = false - } - for downstream_node in downstream_nodes { - if let InputConnector::Node { node_id: downstream_id, input_index } = downstream_node { - // If the downstream node is not in the delete nodes set, then continue iterating - // If the downstream node is the bottom input of a layer then continue iterating - if !delete_nodes.contains(downstream_id) || (*input_index == 0 && self.is_layer(downstream_id, network_path)) { - stack.push(*downstream_id); - } - // If the traversal reaches the primary input of the node to delete then do not delete it - if node_id == downstream_id && *input_index == 0 { - can_delete = false; - stack = Vec::new(); - break; - } - } - // If the traversal reaches the export, then the current node is not a sole dependent and cannot be deleted - else { - can_delete = false; - stack = Vec::new(); - break; - } - } - } - // If there are no outward wires, then we have reached a dead end, and the node cannot be deleted - if is_dead_end { - can_delete = false; - stack = Vec::new(); + + // A path terminates when absorbed by another node marked for deletion, except through a layer's bottom input, which is stack flow to walk through. + // Reaching the primary input of the node being deleted means this is the stack continuation rather than a child, so it must survive. + let can_delete = self.is_sole_dependent(upstream_node, network_path, |downstream_node, input_index| { + if downstream_node == *node_id && input_index == 0 { + SoleDependentStep::Escape + } else if delete_nodes.contains(&downstream_node) && !(input_index == 0 && layer_nodes.contains(&downstream_node)) { + SoleDependentStep::Terminate + } else { + SoleDependentStep::Continue } - } + }); if can_delete { delete_nodes.insert(upstream_node); diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/queries.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/queries.rs index ef73cd3da8..c64b25bde2 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/queries.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/queries.rs @@ -185,7 +185,7 @@ impl NodeNetworkInterface { log::error!("Could not get position in create_node_template"); return None; }; - match &mut node_template.persistent_node_metadata.node_type_metadata { + match &mut node_template.node_type_metadata { NodeTypePersistentMetadata::Layer(layer_metadata) => layer_metadata.position = LayerPosition::Absolute(position), NodeTypePersistentMetadata::Node(node_metadata) => node_metadata.position = NodePosition::Absolute(position), }; @@ -198,14 +198,14 @@ impl NodeNetworkInterface { log::error!("Could not get position in create_node_template"); return None; }; - node_template.persistent_node_metadata.node_type_metadata = NodeTypePersistentMetadata::Node(NodePersistentMetadata { + node_template.node_type_metadata = NodeTypePersistentMetadata::Node(NodePersistentMetadata { position: NodePosition::Absolute(position), }); } // Shift all absolute nodes 2 to the right and 2 down // TODO: Remove 2x2 offset and replace with layout system to find space for new node - match &mut node_template.persistent_node_metadata.node_type_metadata { + match &mut node_template.node_type_metadata { NodeTypePersistentMetadata::Layer(layer_metadata) => { if let LayerPosition::Absolute(position) = &mut layer_metadata.position { *position += IVec2::new(2, 2) @@ -228,7 +228,7 @@ impl NodeNetworkInterface { if self.is_layer(&old_id, network_path) { for valid_upstream_chain_node in self.valid_upstream_chain_nodes(&InputConnector::node(old_id, 1), network_path) { if let Some(node_template) = new_nodes.iter_mut().find_map(|(_, old_id, template)| (*old_id == valid_upstream_chain_node).then_some(template)) { - match &mut node_template.persistent_node_metadata.node_type_metadata { + match &mut node_template.node_type_metadata { NodeTypePersistentMetadata::Node(node_metadata) => node_metadata.position = NodePosition::Chain, NodeTypePersistentMetadata::Layer(_) => log::error!("Node cannot be a layer"), }; @@ -248,7 +248,7 @@ impl NodeNetworkInterface { /// /// If the node is not in the hashmap then a default input is found based on the compiled network, using the node_id passed as a parameter pub fn map_ids(&mut self, mut node_template: NodeTemplate, node_id: &NodeId, new_ids: &HashMap, network_path: &[NodeId]) -> NodeTemplate { - for (input_index, input) in node_template.document_node.inputs.iter_mut().enumerate() { + for (input_index, input) in node_template.inputs.iter_mut().enumerate() { if let &mut NodeInput::Node { node_id: id, output_index } = input { if let Some(&new_id) = new_ids.get(&id) { *input = NodeInput::Node { node_id: new_id, output_index }; @@ -550,6 +550,56 @@ impl NodeNetworkInterface { node_height } + /// Returns whether every downstream path from the node's outputs stays within the dependent set defined by `classify`, meaning nothing else in the graph depends on this node. + /// Reaching an export or a dead end (a walked node with no outward wires) always escapes. O(nodes + wires) per call. + pub(crate) fn is_sole_dependent(&mut self, node_id: NodeId, network_path: &[NodeId], classify: impl Fn(NodeId, usize) -> SoleDependentStep) -> bool { + let mut visited = HashSet::new(); + let mut stack = vec![node_id]; + + while let Some(current_node) = stack.pop() { + if !visited.insert(current_node) { + continue; + } + + let number_of_outputs = self.number_of_outputs(¤t_node, network_path); + let Some(outward_wires) = self.outward_wires(network_path) else { + log::error!("Could not get outward wires in is_sole_dependent"); + return false; + }; + + // Classify every downstream connection of this node, collecting the ones to keep walking through + let mut has_downstream_connections = false; + let mut nodes_to_walk_through = Vec::new(); + for output_index in 0..number_of_outputs { + let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(current_node, output_index)) else { + continue; + }; + for downstream_connection in downstream_connections { + has_downstream_connections = true; + let InputConnector::Node { + node_id: downstream_node, + input_index, + } = downstream_connection + else { + return false; + }; + match classify(*downstream_node, *input_index) { + SoleDependentStep::Terminate => {} + SoleDependentStep::Continue => nodes_to_walk_through.push(*downstream_node), + SoleDependentStep::Escape => return false, + } + } + } + + if !has_downstream_connections { + return false; + } + stack.extend(nodes_to_walk_through); + } + + true + } + // All chain nodes and branches from the chain which are sole dependents of the layer pub fn upstream_nodes_below_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> HashSet { // Every upstream node below layer must be a sole dependent @@ -605,66 +655,16 @@ impl NodeNetworkInterface { .upstream_flow_back_from_nodes(vec![potential_upstream_node], network_path, FlowType::UpstreamFlow) .collect::>() { - let number_of_outputs = self.number_of_outputs(&upstream_node_from_input, network_path); - - // A node is a sole dependent if all outputs are sole dependents, and there are no dead ends - let mut all_outputs_are_sole_dependents = true; - let mut dead_ends = 0; - - for output_index in 0..number_of_outputs { - let downstream_connections = { - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Could not get outward wires in upstream_nodes_below_layer"); - continue; - }; - outward_wires.get(&OutputConnector::node(upstream_node_from_input, output_index)).cloned() - }; - let Some(downstream_connections) = downstream_connections else { - log::error!("Could not get outward wires in upstream_nodes_below_layer"); - continue; - }; - let mut current_output_is_sole_dependent = true; - let mut stack = downstream_connections; - while let Some(current_downstream_connection) = stack.pop() { - // Iterate downstream. If a sole dependent or chain_node_id is reached, then stop the iteration. If the exports is eventually reached, then it is not a sole dependent - match ¤t_downstream_connection { - InputConnector::Node { - node_id: downstream_node_id, - input_index, - } => { - // Stop iterating once the downstream node is the left input to the chain or a sole dependent - if !(sole_dependents.contains(downstream_node_id) || downstream_node_id == node_id && *input_index == 1) { - // Continue iterating downstream for the downstream node - let number_of_outputs = self.number_of_outputs(downstream_node_id, network_path); - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Could not get outward wires in upstream_nodes_below_layer"); - continue; - }; - let mut has_downstream_connections = false; - for output_index in 0..number_of_outputs { - let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(*downstream_node_id, output_index)) else { - log::error!("Could not get outward wires in upstream_nodes_below_layer"); - continue; - }; - if !downstream_connections.is_empty() { - has_downstream_connections = true; - } - stack.extend(downstream_connections.clone()); - } - if !has_downstream_connections { - dead_ends += 1; - } - } - } - InputConnector::Export(_) => current_output_is_sole_dependent = false, - } + // A path terminates at an already-verified sole dependent or at the left input to the chain + let is_sole_dependent = self.is_sole_dependent(upstream_node_from_input, network_path, |downstream_node, input_index| { + if sole_dependents.contains(&downstream_node) || downstream_node == *node_id && input_index == 1 { + SoleDependentStep::Terminate + } else { + SoleDependentStep::Continue } - if !current_output_is_sole_dependent || dead_ends != 0 { - all_outputs_are_sole_dependents = false; - break; - } - } - if all_outputs_are_sole_dependents && dead_ends == 0 { + }); + + if is_sole_dependent { sole_dependents.insert(upstream_node_from_input); } else { upstream_chain_can_be_added = false; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/template.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/template.rs new file mode 100644 index 0000000000..3d13f7858b --- /dev/null +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/template.rs @@ -0,0 +1,313 @@ +use super::*; +use graph_craft::ProtoNodeIdentifier; +use graph_craft::concrete; +use graphene_std::Context; + +// PartialEq required by message handlers +/// All persistent editor and Graphene data for a node, unified into a single flat shape. +/// Used to author node definitions, pass nodes through the editor, and serialize them for the clipboard. +/// +/// [`Self::into_parts`] and [`Self::from_parts`] are the only places this is split into (or joined from) the +/// [`DocumentNode`] and [`DocumentNodePersistentMetadata`] halves stored by the network interface, so the parallel-tree +/// storage invariants hold by construction for every node built from a template. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(from = "NodeTemplateParts", into = "NodeTemplateParts")] +pub struct NodeTemplate { + /// The graph data inputs to the node. Kept the same length as `input_metadata` by [`Self::normalize_input_metadata`] and [`Self::into_parts`]. + pub inputs: Vec, + /// Type of the argument which this node can be evaluated with. + pub call_argument: Type, + /// A nested network of templates, a proto node identifier, or the Extract tag. + pub implementation: NodeTemplateImplementation, + /// Represents the eye icon for hiding/showing the node in the graph UI. + pub visible: bool, + /// Prevents identical proto nodes from being deduplicated during compilation, e.g. for monitor nodes. + pub skip_deduplication: bool, + /// List of Extract and Inject annotations for the Context. + pub context_features: ContextDependencies, + /// A name chosen by the user for this instance of the node. Empty indicates no given name, in which case the implementation name is displayed in italics. + pub display_name: String, + /// Metadata to override the properties panel widgets for each input. Kept the same length as `inputs`. + pub input_metadata: Vec, + pub output_names: Vec, + /// Represents the lock icon for locking/unlocking the node in the graph UI. + pub locked: bool, + /// Indicates that the node will be shown in the Properties panel when it would otherwise be empty. + pub pinned: bool, + /// Whether the node is displayed as a left-to-right node or bottom-to-top layer, along with its position. + pub node_type_metadata: NodeTypePersistentMetadata, +} + +impl Default for NodeTemplate { + fn default() -> Self { + Self { + inputs: Vec::new(), + call_argument: concrete!(Context), + implementation: NodeTemplateImplementation::default(), + visible: true, + skip_deduplication: false, + context_features: ContextDependencies::default(), + display_name: String::new(), + input_metadata: Vec::new(), + output_names: Vec::new(), + locked: false, + pinned: false, + node_type_metadata: NodeTypePersistentMetadata::default(), + } + } +} + +/// The implementation of a [`NodeTemplate`], mirroring [`DocumentNodeImplementation`] but carrying unified templates for nested network nodes. +// Templates are transient authoring objects, so the Network variant's size is not worth the authoring noise of boxing +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq)] +pub enum NodeTemplateImplementation { + Network(NodeNetworkTemplate), + ProtoNode(ProtoNodeIdentifier), + Extract, +} + +impl Default for NodeTemplateImplementation { + fn default() -> Self { + NodeTemplateImplementation::ProtoNode(graphene_std::ops::passthrough::IDENTIFIER) + } +} + +/// A nested network within a [`NodeTemplate`], holding each nested node as a unified template alongside the network-level persistent metadata. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct NodeNetworkTemplate { + pub exports: Vec, + pub nodes: HashMap, + pub scope_injections: HashMap, + /// The identifier of the [`DocumentNodeDefinition`] this network was instantiated from, if unmodified. + /// + /// [`DocumentNodeDefinition`]: crate::messages::portfolio::document::node_graph::document_node_definitions::DocumentNodeDefinition + pub reference: Option, + /// The display order of pinned nodes in the Properties panel. + pub pinned_node_order: Vec, + pub previewing: Previewing, + pub navigation_metadata: NavigationMetadata, +} + +impl NodeTemplate { + /// Joins a [`DocumentNode`] and its persistent metadata into the unified template shape. Missing nested metadata is filled with defaults. + pub fn from_parts(document_node: DocumentNode, persistent_node_metadata: DocumentNodePersistentMetadata) -> Self { + let DocumentNode { + inputs, + call_argument, + implementation, + visible, + skip_deduplication, + context_features, + original_location: _, + } = document_node; + let DocumentNodePersistentMetadata { + display_name, + input_metadata, + output_names, + locked, + pinned, + node_type_metadata, + network_metadata, + } = persistent_node_metadata; + + let implementation = match implementation { + DocumentNodeImplementation::Network(network) => { + let mut nested_persistent = network_metadata.map(|metadata| metadata.persistent_metadata).unwrap_or_default(); + + // Pair each nested node with its metadata by ID, recursively + let mut nodes = HashMap::with_capacity(network.nodes.len()); + for (node_id, node) in network.nodes { + let node_persistent_metadata = nested_persistent.node_metadata.remove(&node_id).map(|metadata| metadata.persistent_metadata).unwrap_or_default(); + nodes.insert(node_id, NodeTemplate::from_parts(node, node_persistent_metadata)); + } + + NodeTemplateImplementation::Network(NodeNetworkTemplate { + exports: network.exports, + nodes, + scope_injections: network.scope_injections.into_iter().collect(), + reference: nested_persistent.reference, + pinned_node_order: nested_persistent.pinned_node_order, + previewing: nested_persistent.previewing, + navigation_metadata: nested_persistent.navigation_metadata, + }) + } + DocumentNodeImplementation::ProtoNode(identifier) => NodeTemplateImplementation::ProtoNode(identifier), + DocumentNodeImplementation::Extract => NodeTemplateImplementation::Extract, + }; + + NodeTemplate { + inputs, + call_argument, + implementation, + visible, + skip_deduplication, + context_features, + display_name, + input_metadata, + output_names, + locked, + pinned, + node_type_metadata, + } + } + + /// Splits the template into the [`DocumentNode`] and [`DocumentNodePersistentMetadata`] halves stored by the network interface. + pub fn into_parts(self) -> (DocumentNode, DocumentNodePersistentMetadata) { + let NodeTemplate { + inputs, + call_argument, + implementation, + visible, + skip_deduplication, + context_features, + display_name, + mut input_metadata, + output_names, + locked, + pinned, + node_type_metadata, + } = self; + + let (implementation, network_metadata) = implementation.into_parts(); + + // The stored metadata invariant requires exactly one input metadata entry per input + input_metadata.resize_with(inputs.len(), InputMetadata::default); + + let document_node = DocumentNode { + inputs, + call_argument, + implementation, + visible, + skip_deduplication, + context_features, + original_location: Default::default(), + }; + let persistent_node_metadata = DocumentNodePersistentMetadata { + display_name, + input_metadata, + output_names, + locked, + pinned, + node_type_metadata, + network_metadata, + }; + (document_node, persistent_node_metadata) + } + + /// The [`DocumentNode`] half alone, for callers performing raw network surgery. + pub fn into_document_node(self) -> DocumentNode { + self.into_parts().0 + } + + /// Resizes `input_metadata` to match `inputs` at every nesting level, filling gaps with defaults. + pub fn normalize_input_metadata(&mut self) { + self.input_metadata.resize_with(self.inputs.len(), InputMetadata::default); + + if let NodeTemplateImplementation::Network(network_template) = &mut self.implementation { + for nested_template in network_template.nodes.values_mut() { + nested_template.normalize_input_metadata(); + } + } + } + + /// Normalizes the stored types at every nesting level via [`DocumentNode::normalize_stored_types`], round-tripping through the split halves to reuse its logic. + pub fn normalize_stored_types(&mut self) { + let (mut document_node, persistent_node_metadata) = std::mem::take(self).into_parts(); + document_node.normalize_stored_types(); + *self = NodeTemplate::from_parts(document_node, persistent_node_metadata); + } +} + +impl NodeTemplateImplementation { + /// Splits into the [`DocumentNodeImplementation`] and the nested network metadata stored alongside it. + pub fn into_parts(self) -> (DocumentNodeImplementation, Option) { + match self { + NodeTemplateImplementation::Network(network_template) => { + let NodeNetworkTemplate { + exports, + nodes, + scope_injections, + reference, + pinned_node_order, + previewing, + navigation_metadata, + } = network_template; + + // Split each nested template into its two halves, recursively + let mut network = NodeNetwork { + exports, + scope_injections: scope_injections.into_iter().collect(), + ..Default::default() + }; + let mut node_metadata = HashMap::with_capacity(nodes.len()); + for (node_id, node_template) in nodes { + let (document_node, persistent_metadata) = node_template.into_parts(); + network.nodes.insert(node_id, document_node); + node_metadata.insert( + node_id, + DocumentNodeMetadata { + persistent_metadata, + transient_metadata: Default::default(), + }, + ); + } + + let network_metadata = NodeNetworkMetadata { + persistent_metadata: NodeNetworkPersistentMetadata { + reference, + node_metadata, + pinned_node_order, + previewing, + navigation_metadata, + selection_undo_history: Default::default(), + selection_redo_history: Default::default(), + }, + transient_metadata: Default::default(), + }; + (DocumentNodeImplementation::Network(network), Some(network_metadata)) + } + NodeTemplateImplementation::ProtoNode(identifier) => (DocumentNodeImplementation::ProtoNode(identifier), None), + NodeTemplateImplementation::Extract => (DocumentNodeImplementation::Extract, None), + } + } +} + +/// Collects resource IDs referenced by a template and its nested networks. +pub fn collect_template_resources(template: &NodeTemplate, out: &mut HashSet) { + for input in &template.inputs { + collect_input_resource(input, out); + } + + if let NodeTemplateImplementation::Network(network_template) = &template.implementation { + for export in &network_template.exports { + collect_input_resource(export, out); + } + for nested_template in network_template.nodes.values() { + collect_template_resources(nested_template, out); + } + } +} + +/// The legacy two-tree shape of [`NodeTemplate`], kept as its serde representation so serialized clipboard data round-trips across versions. +#[derive(serde::Serialize, serde::Deserialize)] +struct NodeTemplateParts { + document_node: DocumentNode, + persistent_node_metadata: DocumentNodePersistentMetadata, +} + +impl From for NodeTemplate { + fn from(parts: NodeTemplateParts) -> Self { + NodeTemplate::from_parts(parts.document_node, parts.persistent_node_metadata) + } +} + +impl From for NodeTemplateParts { + fn from(template: NodeTemplate) -> Self { + let (document_node, persistent_node_metadata) = template.into_parts(); + NodeTemplateParts { + document_node, + persistent_node_metadata, + } + } +} diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/types.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/types.rs index 601a910921..8d16ff30dc 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/types.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/types.rs @@ -814,14 +814,6 @@ pub struct NavigationMetadata { pub node_graph_width: f64, } -// PartialEq required by message handlers -/// All persistent editor and Graphene data for a node. Used to serialize and deserialize a node, pass it through the editor, and create definitions. -#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct NodeTemplate { - pub document_node: DocumentNode, - pub persistent_node_metadata: DocumentNodePersistentMetadata, -} - #[derive(Clone, Copy, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)] pub enum TransactionStatus { Started, @@ -830,7 +822,21 @@ pub enum TransactionStatus { Finished, } +/// How [`NodeNetworkInterface::is_sole_dependent`] should treat a downstream connector it encounters. +#[derive(Clone, Copy, Debug)] +pub(crate) enum SoleDependentStep { + /// The downstream path ends here, inside the dependent set. + Terminate, + /// Keep walking downstream through this node. + Continue, + /// The path leaves the dependent set, so the candidate is not a sole dependent. + Escape, +} + pub(crate) fn collect_network_resources(network: &NodeNetwork, out: &mut HashSet) { + for export in &network.exports { + collect_input_resource(export, out); + } for node in network.nodes.values() { collect_node_resources(node, out); } @@ -839,13 +845,18 @@ pub(crate) fn collect_network_resources(network: &NodeNetwork, out: &mut HashSet /// Collects resource IDs referenced by a node and its nested networks. pub fn collect_node_resources(node: &DocumentNode, out: &mut HashSet) { for input in &node.inputs { - if let NodeInput::Value { tagged_value, .. } = input - && let TaggedValue::Resource(id) = &**tagged_value - { - out.insert(*id); - } + collect_input_resource(input, out); } if let DocumentNodeImplementation::Network(nested) = &node.implementation { collect_network_resources(nested, out); } } + +/// Records the resource ID held by a value input, covering node inputs and export slots alike. +pub(crate) fn collect_input_resource(input: &NodeInput, out: &mut HashSet) { + if let NodeInput::Value { tagged_value, .. } = input + && let TaggedValue::Resource(id) = &**tagged_value + { + out.insert(*id); + } +} diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/view.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/view.rs index f66c85d689..82ad13a0e7 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/view.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/view.rs @@ -418,9 +418,6 @@ impl<'a, 'p> NetworkView<'a, 'p> { let node = self.node(node_id)?; let node_metadata = self.node_metadata(node_id)?; - Ok(NodeTemplate { - persistent_node_metadata: node_metadata.persistent_metadata.clone(), - document_node: node.clone(), - }) + Ok(NodeTemplate::from_parts(node.clone(), node_metadata.persistent_metadata.clone())) } } diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 43ee80be2c..ed64588bef 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -3,7 +3,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, NodeTemplateImplementation, OutputConnector}; use crate::messages::prelude::DocumentMessageHandler; use glam::{DVec2, IVec2}; use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId}; @@ -993,9 +993,9 @@ pub fn document_migration_string_preprocessing(document_serialized_content: Stri /// so the staged input-count migrations can still upgrade old text nodes before the split. fn legacy_text_node_template() -> Option { let mut template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))?.default_node_template(); - template.document_node.implementation = DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")); - template.document_node.inputs.push(NodeInput::value(TaggedValue::Bool(false), false)); - template.persistent_node_metadata.input_metadata.push(Default::default()); + template.implementation = NodeTemplateImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")); + template.inputs.push(NodeInput::value(TaggedValue::Bool(false), false)); + template.input_metadata.push(Default::default()); Some(template) } @@ -1151,10 +1151,12 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_ if let DocumentNodeImplementation::ProtoNode(protonode_id) = &node.implementation { let node_path_without_type_args = protonode_id.as_str().split('<').next(); if let Some(new) = node_path_without_type_args.and_then(|node_path| replacements.get(node_path)) { - let mut default_template = NodeTemplate::default(); - default_template.document_node.implementation = DocumentNodeImplementation::ProtoNode(new.clone()); + let mut default_template = NodeTemplate { + implementation: NodeTemplateImplementation::ProtoNode(new.clone()), + ..Default::default() + }; document.network_interface.replace_implementation(node_id, &network_path, &mut default_template); - document.network_interface.set_call_argument(node_id, &network_path, default_template.document_node.call_argument); + document.network_interface.set_call_argument(node_id, &network_path, default_template.call_argument); } } } @@ -1224,7 +1226,7 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_ // (which represented radians in the legacy format) reaches the now-degrees Rotation input correctly. if let Some(multiply_node) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::multiply::IDENTIFIER)) { let mut multiply_template = multiply_node.default_node_template(); - multiply_template.document_node.inputs[1] = NodeInput::value(TaggedValue::F64(180. / PI), false); + multiply_template.inputs[1] = NodeInput::value(TaggedValue::F64(180. / PI), false); let multiply_node_id = NodeId::new(); if let Some(transform_position) = document.network_interface.position_from_downstream_node(node_id, network_path) { let multiply_position = transform_position + IVec2::new(-7, 1); @@ -2172,7 +2174,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], // A brush node saved before `Item>` had a default stored its unconnected background as the invalid `()`, // which fails type resolution against the raster primary; adopt the definition's empty-raster default instead. if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && matches!(node.inputs.first().and_then(|input| input.as_value()), Some(TaggedValue::None)) { - let default_background = resolve_document_node_type(&reference)?.node_template.document_node.inputs.first()?.clone(); + let default_background = resolve_document_node_type(&reference)?.node_template.inputs.first()?.clone(); document.network_interface.set_input(&InputConnector::node(*node_id, 0), default_background, network_path); } @@ -2423,7 +2425,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], return None; }; let mut subtract_template = subtract_def.default_node_template(); - subtract_template.document_node.inputs[1] = NodeInput::value(TaggedValue::F64(1.), false); + subtract_template.inputs[1] = NodeInput::value(TaggedValue::F64(1.), false); let subtract_id = NodeId::new(); // Create Divide node: old_progression / (N-1) → new progression @@ -2518,7 +2520,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], return None; }; let mut transform_template = transform_node_type.default_node_template(); - transform_template.document_node.inputs[1] = NodeInput::value(TaggedValue::DVec2(start), false); + transform_template.inputs[1] = NodeInput::value(TaggedValue::DVec2(start), false); let transform_id = NodeId::new(); @@ -2579,7 +2581,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], return None; }; let mut transform_template = transform_node_type.default_node_template(); - transform_template.document_node.inputs[1] = NodeInput::value(TaggedValue::DVec2(start), false); + transform_template.inputs[1] = NodeInput::value(TaggedValue::DVec2(start), false); let transform_id = NodeId::new(); @@ -2596,7 +2598,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], && let Some(reference) = document.network_interface.reference(node_id, network_path).clone() && let Some(node_definition) = resolve_document_node_type(&reference) { - let context_features = node_definition.node_template.document_node.context_features; + let context_features = node_definition.node_template.context_features; document.network_interface.set_context_features(node_id, network_path, context_features); } @@ -2671,7 +2673,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], // A value input stored as a List-form TypeDefault adopts the definition's current default when the connector's declared default has since changed (e.g. the connector was ranked down to Item). // The red-slash no-paint choice shares that stored form but is a deliberate value, not a stale disconnect default, so it is exempt. if let Some(definition) = resolve_document_node_type(&reference) { - let definition_inputs = definition.node_template.document_node.inputs.clone(); + let definition_inputs = definition.node_template.inputs.clone(); for (index, definition_input) in definition_inputs.iter().enumerate() { if !matches!(definition_input, NodeInput::Value { .. }) { continue;