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-07-25 20:37:36 -07:00
committed by Dennis Kobert
parent bb447eab1a
commit b694d22fa3
13 changed files with 1195 additions and 1524 deletions

View File

@@ -182,13 +182,8 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> 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(_) => {}
}
}

View File

@@ -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<DocumentNodeDefinition>) -> 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;
};
@@ -71,29 +70,21 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> 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::default(),
..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::default(),
// 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),
@@ -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
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);
@@ -119,27 +111,27 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> 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<DefinitionIdentifier, DocumentNodeDefinition>) {
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<DefinitionIdentifier, DocumentNodeDefinition>) {
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 => {}
}
}

View File

@@ -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)
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<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
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<NodeGraphMessage, NodeGraphMessageContext<'a>> 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,

View File

@@ -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};

View File

@@ -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::<Vec<_>>() {
let mut stack = vec![upstream_node];
let mut is_sole_dependent = true;
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;
}
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 {

View File

@@ -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();

View File

@@ -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());
@@ -1023,12 +1022,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 {
@@ -1036,7 +1036,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);
@@ -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.
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
.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 {
@@ -1069,7 +1069,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);
@@ -1077,18 +1077,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<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");
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();
for node_id in &nodes_to_delete {
@@ -1109,45 +1117,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(&current_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;
}
}
// 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 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 {
delete_nodes.insert(upstream_node);

View File

@@ -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<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 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(&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
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
@@ -605,66 +655,16 @@ impl NodeNetworkInterface {
.upstream_flow_back_from_nodes(vec![potential_upstream_node], network_path, FlowType::UpstreamFlow)
.collect::<Vec<_>>()
{
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 &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,
}
// 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;

View File

@@ -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,
}
}
}

View File

@@ -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<ResourceId>) {
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<ResourceId>) {
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<ResourceId>) {
if let NodeInput::Value { tagged_value, .. } = input
&& let TaggedValue::Resource(id) = &**tagged_value
{
out.insert(*id);
}
}

View File

@@ -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()))
}
}

View File

@@ -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<NodeTemplate> {
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)
}
@@ -1150,10 +1150,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);
}
}
}
@@ -1223,7 +1225,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);
@@ -2211,7 +2213,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 `()`,
// 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);
}
@@ -2462,7 +2464,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
@@ -2557,7 +2559,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();
@@ -2618,7 +2620,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();
@@ -2703,7 +2705,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;