Replace NodeTemplate's parallel node and metadata trees with a single flat recursive shape (#4377)

* Unify NodeTemplate into a single flat recursive shape with from_parts/into_parts as the storage split points

* Unify the three sole-dependent traversals onto one is_sole_dependent kernel

* Scan network exports for resource values when collecting used resources
This commit is contained in:
Keavon Chambers
2026-09-15 14:37:22 +02:00
committed by Dennis Kobert
parent 343969561e
commit 7ec885e7c7
13 changed files with 1195 additions and 1524 deletions
@@ -182,13 +182,8 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
let mut resource_ids = HashSet::new(); let mut resource_ids = HashSet::new();
for item in &items { for item in &items {
match item { match item {
ClipboardItem::Layer(entry) => entry ClipboardItem::Layer(entry) => entry.nodes.iter().for_each(|(_, template)| network_interface::collect_template_resources(template, &mut resource_ids)),
.nodes ClipboardItem::Nodes(nodes) => nodes.iter().for_each(|(_, template)| network_interface::collect_template_resources(template, &mut resource_ids)),
.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::Vector(_) | ClipboardItem::Resource(_) => {} ClipboardItem::Vector(_) | ClipboardItem::Resource(_) => {}
} }
} }
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,6 @@
use super::DocumentNodeDefinition; use super::DocumentNodeDefinition;
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; 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 crate::messages::portfolio::document::utility_types::network_interface::{InputMetadata, NodeTemplate, NodeTemplateImplementation, WidgetOverride};
use graph_craft::document::*;
use graphene_std::registry::*; use graphene_std::registry::*;
use graphene_std::*; use graphene_std::*;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@@ -14,7 +13,7 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
let network_nodes = custom let network_nodes = custom
.into_iter() .into_iter()
.filter_map(|definition| { .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); definitions_map.insert(DefinitionIdentifier::ProtoNode(proto_node_identifier.clone()), definition);
return None; return None;
}; };
@@ -71,29 +70,21 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
DocumentNodeDefinition { DocumentNodeDefinition {
identifier: display_name, identifier: display_name,
node_template: NodeTemplate { node_template: NodeTemplate {
document_node: DocumentNode { inputs,
inputs, call_argument: input_type.clone(),
call_argument: input_type.clone(), implementation: NodeTemplateImplementation::ProtoNode(id.clone()),
implementation: DocumentNodeImplementation::ProtoNode(id.clone()), context_features: ContextDependencies::default(),
visible: true, // TODO: Store information for input overrides in the node macro
skip_deduplication: false, input_metadata: fields
context_features: ContextDependencies::default(), .iter()
..Default::default() .map(|f| match f.widget_override {
}, RegistryWidgetOverride::None => (f.name, f.description).into(),
persistent_node_metadata: DocumentNodePersistentMetadata { RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden),
// TODO: Store information for input overrides in the node macro RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())),
input_metadata: fields RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
.iter() })
.map(|f| match f.widget_override { .collect(),
RegistryWidgetOverride::None => (f.name, f.description).into(), ..Default::default()
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()
},
}, },
category, category,
description: Cow::Borrowed(description), description: Cow::Borrowed(description),
@@ -104,13 +95,14 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
// Add the rest of the network nodes to the map and add the metadata for their internal protonodes // 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 { 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 // Set the reference to the node identifier
if let Some(nested_metadata) = network_node.node_template.persistent_node_metadata.network_metadata.as_mut() { if let NodeTemplateImplementation::Network(network_template) = &mut network_node.node_template.implementation {
nested_metadata.persistent_metadata.reference = Some(network_node.identifier.to_string()); 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 it is not a merge node, then set the display name to the identifier/reference
if network_node.identifier != "Merge" { 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); definitions_map.insert(DefinitionIdentifier::Network(network_node.identifier.to_string()), network_node);
@@ -119,27 +111,27 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
definitions_map definitions_map
} }
/// Traverses a document node template and metadata in parallel to add metadata to the protonodes /// Recursively fills each nested proto node's editor metadata from its definition, preserving only the authored position.
fn traverse_node(node: &DocumentNode, node_metadata: &mut DocumentNodePersistentMetadata, definitions_map: &HashMap<DefinitionIdentifier, DocumentNodeDefinition>) { fn fill_proto_node_metadata(node_template: &mut NodeTemplate, definitions_map: &HashMap<DefinitionIdentifier, DocumentNodeDefinition>) {
match &node.implementation { match &mut node_template.implementation {
DocumentNodeImplementation::Network(node_network) => { NodeTemplateImplementation::Network(network_template) => {
for (nested_node_id, nested_node) in node_network.nodes.iter() { for nested_template in network_template.nodes.values_mut() {
let nested_metadata = node_metadata.network_metadata.as_mut().unwrap().persistent_metadata.node_metadata.get_mut(nested_node_id).unwrap(); fill_proto_node_metadata(nested_template, definitions_map);
traverse_node(nested_node, &mut nested_metadata.persistent_metadata, definitions_map);
} }
} }
DocumentNodeImplementation::ProtoNode(id) => { NodeTemplateImplementation::ProtoNode(id) => {
// Set all the metadata except the position to the proto node information from the macro // If this lookup fails then the proto node id in the definition doesn't match what is generated by 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
let Some(definition) = definitions_map.get(&DefinitionIdentifier::ProtoNode(id.clone())) else { 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; return;
}; };
let mut new_metadata = definition.node_template.persistent_node_metadata.clone();
new_metadata.node_type_metadata = node_metadata.node_type_metadata.clone(); let definition_template = &definition.node_template;
*node_metadata = new_metadata 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 => {}
} }
} }
@@ -301,7 +301,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
// A freshly added Text node carries no font, so give it the default font (registered like the Text tool does) // 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) { if node_type == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) {
let font_resource_id = graph_craft::application_io::resource::ResourceId::new(); 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); *font_input = NodeInput::value(TaggedValue::Resource(font_resource_id), false);
} }
responses.add(DocumentMessage::Resource(ResourceMessage::AddFont { responses.add(DocumentMessage::Resource(ResourceMessage::AddFont {
@@ -336,7 +336,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}; };
// Ensure connection is to correct input of new node. If it does not have an input then do not connect // 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 { responses.add(NodeGraphMessage::CreateWire {
output_connector: *output_connector, output_connector: *output_connector,
input_connector: InputConnector::node(node_id, input_index), input_connector: InputConnector::node(node_id, input_index),
@@ -693,7 +693,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return; 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); 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(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::InsertNode { responses.add(NodeGraphMessage::InsertNode {
node_id: encapsulating_node_id, node_id: encapsulating_node_id,
@@ -10,11 +10,13 @@ mod queries;
mod resolved_types; mod resolved_types;
pub mod storage_metadata; pub mod storage_metadata;
mod structure; mod structure;
mod template;
mod types; mod types;
#[cfg(test)] #[cfg(test)]
mod validation; mod validation;
mod view; mod view;
pub use template::*;
pub use types::*; pub use types::*;
pub use view::{NetworkError, NetworkView}; pub use view::{NetworkError, NetworkView};
@@ -193,41 +193,21 @@ impl NodeNetworkInterface {
let mut not_sole_dependents = HashSet::new(); let mut not_sole_dependents = HashSet::new();
sole_dependents.insert(*stack_top); sole_dependents.insert(*stack_top);
for upstream_node in self.upstream_flow_back_from_nodes(vec![*stack_top], network_path, FlowType::UpstreamFlow).collect::<Vec<_>>() { for upstream_node in self.upstream_flow_back_from_nodes(vec![*stack_top], network_path, FlowType::UpstreamFlow).collect::<Vec<_>>() {
let mut stack = vec![upstream_node]; if sole_dependents.contains(&upstream_node) || not_sole_dependents.contains(&upstream_node) {
let mut is_sole_dependent = true; continue;
while let Some(current_node) = stack.pop() {
if not_sole_dependents.contains(&current_node) {
is_sole_dependent = false;
break;
}
if !sole_dependents.contains(&current_node) {
let mut has_outward_wire = false;
for output_index in 0..self.number_of_outputs(&current_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;
}
} }
// 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 { if is_sole_dependent {
sole_dependents.insert(upstream_node); sole_dependents.insert(upstream_node);
} else { } else {
@@ -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"); 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] #[tokio::test]
async fn cyclic_connection_is_rejected_without_side_effects() { async fn cyclic_connection_is_rejected_without_side_effects() {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
@@ -557,14 +557,13 @@ impl NodeNetworkInterface {
log::error!("Could not get node in set_implementation"); log::error!("Could not get node in set_implementation");
return; return;
}; };
let new_implementation = std::mem::take(&mut new_template.document_node.implementation); let (new_implementation, new_network_metadata) = std::mem::take(&mut new_template.implementation).into_parts();
let _ = std::mem::replace(&mut node.implementation, new_implementation); node.implementation = new_implementation;
let Some(metadata) = self.node_metadata_mut(node_id, network_path) else { let Some(metadata) = self.node_metadata_mut(node_id, network_path) else {
log::error!("Could not get metadata in set_implementation"); log::error!("Could not get metadata in set_implementation");
return; return;
}; };
let new_metadata = std::mem::take(&mut new_template.persistent_node_metadata.network_metadata); metadata.persistent_metadata.network_metadata = new_network_metadata;
let _ = std::mem::replace(&mut metadata.persistent_metadata.network_metadata, new_metadata);
} }
/// Replaces the inputs and corresponding metadata. /// Replaces the inputs and corresponding metadata.
@@ -577,13 +576,13 @@ impl NodeNetworkInterface {
log::error!("Could not get node in set_implementation"); log::error!("Could not get node in set_implementation");
return None; 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 old_inputs = std::mem::replace(&mut node.inputs, new_inputs);
let Some(metadata) = self.node_metadata_mut(node_id, network_path) else { let Some(metadata) = self.node_metadata_mut(node_id, network_path) else {
log::error!("Could not get metadata in set_implementation"); log::error!("Could not get metadata in set_implementation");
return None; 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); let _ = std::mem::replace(&mut metadata.persistent_metadata.input_metadata, new_metadata);
Some(old_inputs) Some(old_inputs)
} }
@@ -597,7 +596,7 @@ impl NodeNetworkInterface {
.reference(node_id, network_path) .reference(node_id, network_path)
.as_ref() .as_ref()
.and_then(resolve_document_node_type) .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(); .cloned();
let Some(metadata) = self.node_metadata_mut(node_id, network_path) else { return }; let Some(metadata) = self.node_metadata_mut(node_id, network_path) else { return };
metadata.persistent_metadata.input_metadata.push(input_metadata.unwrap_or_default()); metadata.persistent_metadata.input_metadata.push(input_metadata.unwrap_or_default());
@@ -1023,12 +1022,13 @@ impl NodeNetworkInterface {
node_template = self.map_ids(node_template, &old_node_id, &new_ids, network_path); node_template = self.map_ids(node_template, &old_node_id, &new_ids, network_path);
// Insert node into network // Insert node into network
let node_id = *new_ids.get(&old_node_id).unwrap(); 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 { let Some(network) = self.network_mut(network_path) else {
log::error!("Network not found in insert_node"); log::error!("Network not found in insert_node");
return; return;
}; };
network.nodes.insert(node_id, node_template.document_node); network.nodes.insert(node_id, document_node);
self.transaction_modified(); self.transaction_modified();
let Some(network_metadata) = self.network_metadata_mut(network_path) else { let Some(network_metadata) = self.network_metadata_mut(network_path) else {
@@ -1036,7 +1036,7 @@ impl NodeNetworkInterface {
return; return;
}; };
let node_metadata = DocumentNodeMetadata { let node_metadata = DocumentNodeMetadata {
persistent_metadata: node_template.persistent_node_metadata, persistent_metadata,
transient_metadata: DocumentNodeTransientMetadata::default(), transient_metadata: DocumentNodeTransientMetadata::default(),
}; };
network_metadata.persistent_metadata.node_metadata.insert(node_id, node_metadata); network_metadata.persistent_metadata.node_metadata.insert(node_id, node_metadata);
@@ -1051,17 +1051,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. /// 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<NodeTemplate> { pub fn insert_node(&mut self, node_id: NodeId, node_template: NodeTemplate, network_path: &[NodeId]) -> Option<NodeTemplate> {
let has_node_or_network_input = node_template let has_node_or_network_input = node_template
.document_node
.inputs .inputs
.iter() .iter()
.all(|input| !(matches!(input, NodeInput::Node { .. }) || matches!(input, NodeInput::Import { .. }))); .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"); 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 { let Some(network) = self.network_mut(network_path) else {
log::error!("Network not found in insert_node"); log::error!("Network not found in insert_node");
return None; 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(); self.transaction_modified();
let Some(network_metadata) = self.network_metadata_mut(network_path) else { let Some(network_metadata) = self.network_metadata_mut(network_path) else {
@@ -1069,7 +1069,7 @@ impl NodeNetworkInterface {
return None; return None;
}; };
let node_metadata = DocumentNodeMetadata { let node_metadata = DocumentNodeMetadata {
persistent_metadata: node_template.persistent_node_metadata, persistent_metadata,
transient_metadata: DocumentNodeTransientMetadata::default(), transient_metadata: DocumentNodeTransientMetadata::default(),
}; };
let previous_metadata = network_metadata.persistent_metadata.node_metadata.insert(node_id, node_metadata); let previous_metadata = network_metadata.persistent_metadata.node_metadata.insert(node_id, node_metadata);
@@ -1077,18 +1077,26 @@ impl NodeNetworkInterface {
self.unload_all_nodes_bounding_box(network_path); self.unload_all_nodes_bounding_box(network_path);
self.unload_node_click_targets(&node_id, network_path); self.unload_node_click_targets(&node_id, network_path);
previous_node.zip(previous_metadata).map(|(document_node, node_metadata)| NodeTemplate { previous_node
document_node, .zip(previous_metadata)
persistent_node_metadata: node_metadata.persistent_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. /// 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<NodeId>, delete_children: bool, network_path: &[NodeId]) { pub fn delete_nodes(&mut self, nodes_to_delete: Vec<NodeId>, 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"); log::error!("Could not get outward wires in delete_nodes");
return; 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::<Vec<_>>())
.unwrap_or_default()
.into_iter()
.filter(|candidate| self.is_layer(candidate, network_path))
.collect::<HashSet<_>>();
let mut delete_nodes = HashSet::new(); let mut delete_nodes = HashSet::new();
for node_id in &nodes_to_delete { for node_id in &nodes_to_delete {
@@ -1109,45 +1117,18 @@ impl NodeNetworkInterface {
upstream_nodes.push(upstream_node); 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 // 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.
let mut stack = vec![upstream_node]; // Reaching the primary input of the node being deleted means this is the stack continuation rather than a child, so it must survive.
let mut can_delete = true; let can_delete = self.is_sole_dependent(upstream_node, network_path, |downstream_node, input_index| {
while let Some(current_node) = stack.pop() { if downstream_node == *node_id && input_index == 0 {
let mut is_dead_end = true; SoleDependentStep::Escape
for output_connector in (0..self.number_of_outputs(&current_node, network_path)).map(|output_index| OutputConnector::node(current_node, output_index)) { } else if delete_nodes.contains(&downstream_node) && !(input_index == 0 && layer_nodes.contains(&downstream_node)) {
let Some(downstream_nodes) = outward_wires.get(&output_connector) else { continue }; SoleDependentStep::Terminate
if !downstream_nodes.is_empty() { } else {
is_dead_end = false SoleDependentStep::Continue
}
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();
}
}
if can_delete { if can_delete {
delete_nodes.insert(upstream_node); delete_nodes.insert(upstream_node);
@@ -185,7 +185,7 @@ impl NodeNetworkInterface {
log::error!("Could not get position in create_node_template"); log::error!("Could not get position in create_node_template");
return None; 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::Layer(layer_metadata) => layer_metadata.position = LayerPosition::Absolute(position),
NodeTypePersistentMetadata::Node(node_metadata) => node_metadata.position = NodePosition::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"); log::error!("Could not get position in create_node_template");
return None; 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), position: NodePosition::Absolute(position),
}); });
} }
// Shift all absolute nodes 2 to the right and 2 down // 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 // 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) => { NodeTypePersistentMetadata::Layer(layer_metadata) => {
if let LayerPosition::Absolute(position) = &mut layer_metadata.position { if let LayerPosition::Absolute(position) = &mut layer_metadata.position {
*position += IVec2::new(2, 2) *position += IVec2::new(2, 2)
@@ -228,7 +228,7 @@ impl NodeNetworkInterface {
if self.is_layer(&old_id, network_path) { 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) { 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)) { 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::Node(node_metadata) => node_metadata.position = NodePosition::Chain,
NodeTypePersistentMetadata::Layer(_) => log::error!("Node cannot be a layer"), 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 /// 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<NodeId, NodeId>, network_path: &[NodeId]) -> NodeTemplate { pub fn map_ids(&mut self, mut node_template: NodeTemplate, node_id: &NodeId, new_ids: &HashMap<NodeId, NodeId>, 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 &mut NodeInput::Node { node_id: id, output_index } = input {
if let Some(&new_id) = new_ids.get(&id) { if let Some(&new_id) = new_ids.get(&id) {
*input = NodeInput::Node { node_id: new_id, output_index }; *input = NodeInput::Node { node_id: new_id, output_index };
@@ -550,6 +550,56 @@ impl NodeNetworkInterface {
node_height 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(&current_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 // 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<NodeId> { pub fn upstream_nodes_below_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> HashSet<NodeId> {
// Every upstream node below layer must be a sole dependent // 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) .upstream_flow_back_from_nodes(vec![potential_upstream_node], network_path, FlowType::UpstreamFlow)
.collect::<Vec<_>>() .collect::<Vec<_>>()
{ {
let number_of_outputs = self.number_of_outputs(&upstream_node_from_input, network_path); // 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| {
// A node is a sole dependent if all outputs are sole dependents, and there are no dead ends if sole_dependents.contains(&downstream_node) || downstream_node == *node_id && input_index == 1 {
let mut all_outputs_are_sole_dependents = true; SoleDependentStep::Terminate
let mut dead_ends = 0; } else {
SoleDependentStep::Continue
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 &current_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,
}
} }
if !current_output_is_sole_dependent || dead_ends != 0 { });
all_outputs_are_sole_dependents = false;
break; if is_sole_dependent {
}
}
if all_outputs_are_sole_dependents && dead_ends == 0 {
sole_dependents.insert(upstream_node_from_input); sole_dependents.insert(upstream_node_from_input);
} else { } else {
upstream_chain_can_be_added = false; upstream_chain_can_be_added = false;
@@ -0,0 +1,307 @@
use super::*;
use graph_craft::ProtoNodeIdentifier;
use graph_craft::concrete;
use graphene_std::Context;
use graphene_std::ContextDependencies;
// 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<NodeInput>,
/// 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<InputMetadata>,
pub output_names: Vec<String>,
/// 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<NodeInput>,
pub nodes: HashMap<NodeId, NodeTemplate>,
pub scope_injections: HashMap<String, (NodeId, Type)>,
/// 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<String>,
/// The display order of pinned nodes in the Properties panel.
pub pinned_node_order: Vec<NodeId>,
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();
}
}
}
}
impl NodeTemplateImplementation {
/// Splits into the [`DocumentNodeImplementation`] and the nested network metadata stored alongside it.
pub fn into_parts(self) -> (DocumentNodeImplementation, Option<NodeNetworkMetadata>) {
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<ResourceId>) {
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<NodeTemplateParts> for NodeTemplate {
fn from(parts: NodeTemplateParts) -> Self {
NodeTemplate::from_parts(parts.document_node, parts.persistent_node_metadata)
}
}
impl From<NodeTemplate> for NodeTemplateParts {
fn from(template: NodeTemplate) -> Self {
let (document_node, persistent_node_metadata) = template.into_parts();
NodeTemplateParts {
document_node,
persistent_node_metadata,
}
}
}
@@ -814,14 +814,6 @@ pub struct NavigationMetadata {
pub node_graph_width: f64, 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)] #[derive(Clone, Copy, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum TransactionStatus { pub enum TransactionStatus {
Started, Started,
@@ -830,7 +822,21 @@ pub enum TransactionStatus {
Finished, 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<ResourceId>) { pub(crate) fn collect_network_resources(network: &NodeNetwork, out: &mut HashSet<ResourceId>) {
for export in &network.exports {
collect_input_resource(export, out);
}
for node in network.nodes.values() { for node in network.nodes.values() {
collect_node_resources(node, out); 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. /// Collects resource IDs referenced by a node and its nested networks.
pub fn collect_node_resources(node: &DocumentNode, out: &mut HashSet<ResourceId>) { pub fn collect_node_resources(node: &DocumentNode, out: &mut HashSet<ResourceId>) {
for input in &node.inputs { for input in &node.inputs {
if let NodeInput::Value { tagged_value, .. } = input collect_input_resource(input, out);
&& let TaggedValue::Resource(id) = &**tagged_value
{
out.insert(*id);
}
} }
if let DocumentNodeImplementation::Network(nested) = &node.implementation { if let DocumentNodeImplementation::Network(nested) = &node.implementation {
collect_network_resources(nested, out); 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<ResourceId>) {
if let NodeInput::Value { tagged_value, .. } = input
&& let TaggedValue::Resource(id) = &**tagged_value
{
out.insert(*id);
}
}
@@ -418,9 +418,6 @@ impl<'a, 'p> NetworkView<'a, 'p> {
let node = self.node(node_id)?; let node = self.node(node_id)?;
let node_metadata = self.node_metadata(node_id)?; let node_metadata = self.node_metadata(node_id)?;
Ok(NodeTemplate { Ok(NodeTemplate::from_parts(node.clone(), node_metadata.persistent_metadata.clone()))
persistent_node_metadata: node_metadata.persistent_metadata.clone(),
document_node: node.clone(),
})
} }
} }
@@ -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::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::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 crate::messages::prelude::DocumentMessageHandler;
use glam::{DVec2, IVec2}; use glam::{DVec2, IVec2};
use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId}; use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId};
@@ -1001,9 +1001,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. /// so the staged input-count migrations can still upgrade old text nodes before the split.
fn legacy_text_node_template() -> Option<NodeTemplate> { fn legacy_text_node_template() -> Option<NodeTemplate> {
let mut template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))?.default_node_template(); 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.implementation = NodeTemplateImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode"));
template.document_node.inputs.push(NodeInput::value(TaggedValue::Bool(false), false)); template.inputs.push(NodeInput::value(TaggedValue::Bool(false), false));
template.persistent_node_metadata.input_metadata.push(Default::default()); template.input_metadata.push(Default::default());
Some(template) Some(template)
} }
@@ -1158,10 +1158,12 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
if let DocumentNodeImplementation::ProtoNode(protonode_id) = &node.implementation { if let DocumentNodeImplementation::ProtoNode(protonode_id) = &node.implementation {
let node_path_without_type_args = protonode_id.as_str().split('<').next(); 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)) { if let Some(new) = node_path_without_type_args.and_then(|node_path| replacements.get(node_path)) {
let mut default_template = NodeTemplate::default(); let mut default_template = NodeTemplate {
default_template.document_node.implementation = DocumentNodeImplementation::ProtoNode(new.clone()); implementation: NodeTemplateImplementation::ProtoNode(new.clone()),
..Default::default()
};
document.network_interface.replace_implementation(node_id, &network_path, &mut default_template); 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);
} }
} }
} }
@@ -1231,7 +1233,7 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
// (which represented radians in the legacy format) reaches the now-degrees Rotation input correctly. // (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)) { 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(); 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(); let multiply_node_id = NodeId::new();
if let Some(transform_position) = document.network_interface.position_from_downstream_node(node_id, network_path) { 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); let multiply_position = transform_position + IVec2::new(-7, 1);
@@ -2219,7 +2221,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
// A brush node saved before `Item<Raster<CPU>>` had a default stored its unconnected background as the invalid `()`, // A brush node saved before `Item<Raster<CPU>>` 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. // 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)) { 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); document.network_interface.set_input(&InputConnector::node(*node_id, 0), default_background, network_path);
} }
@@ -2470,7 +2472,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
return None; return None;
}; };
let mut subtract_template = subtract_def.default_node_template(); 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(); let subtract_id = NodeId::new();
// Create Divide node: old_progression / (N-1) → new progression // Create Divide node: old_progression / (N-1) → new progression
@@ -2565,7 +2567,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
return None; return None;
}; };
let mut transform_template = transform_node_type.default_node_template(); 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(); let transform_id = NodeId::new();
@@ -2626,7 +2628,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
return None; return None;
}; };
let mut transform_template = transform_node_type.default_node_template(); 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(); let transform_id = NodeId::new();
@@ -2711,7 +2713,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). // 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. // 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) { 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() { for (index, definition_input) in definition_inputs.iter().enumerate() {
if !matches!(definition_input, NodeInput::Value { .. }) { if !matches!(definition_input, NodeInput::Value { .. }) {
continue; continue;