mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
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:
@@ -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(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -256,13 +251,13 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
|
||||
match item {
|
||||
ClipboardItem::Layer(mut entry) => {
|
||||
for (_, template) in &mut entry.nodes {
|
||||
template.document_node.normalize_stored_types();
|
||||
template.normalize_stored_types();
|
||||
}
|
||||
layers.push(entry);
|
||||
}
|
||||
ClipboardItem::Nodes(mut nodes) => {
|
||||
for (_, template) in &mut nodes {
|
||||
template.document_node.normalize_stored_types();
|
||||
template.normalize_stored_types();
|
||||
}
|
||||
node_groups.push(nodes);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
};
|
||||
@@ -72,29 +71,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::from(context_features.as_slice()),
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
// TODO: Store information for input overrides in the node macro
|
||||
input_metadata: fields
|
||||
.iter()
|
||||
.map(|f| match f.widget_override {
|
||||
RegistryWidgetOverride::None => (f.name, f.description).into(),
|
||||
RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden),
|
||||
RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())),
|
||||
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
|
||||
})
|
||||
.collect(),
|
||||
locked: false,
|
||||
..Default::default()
|
||||
},
|
||||
inputs,
|
||||
call_argument: input_type.clone(),
|
||||
implementation: NodeTemplateImplementation::ProtoNode(id.clone()),
|
||||
context_features: ContextDependencies::from(context_features.as_slice()),
|
||||
// TODO: Store information for input overrides in the node macro
|
||||
input_metadata: fields
|
||||
.iter()
|
||||
.map(|f| match f.widget_override {
|
||||
RegistryWidgetOverride::None => (f.name, f.description).into(),
|
||||
RegistryWidgetOverride::Hidden => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Hidden),
|
||||
RegistryWidgetOverride::String(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::String(str.to_string())),
|
||||
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
category,
|
||||
description: Cow::Borrowed(description),
|
||||
@@ -105,13 +96,14 @@ pub(super) fn post_process_nodes(custom: Vec<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);
|
||||
@@ -120,27 +112,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 => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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(¤t_node) {
|
||||
is_sole_dependent = false;
|
||||
break;
|
||||
}
|
||||
if !sole_dependents.contains(¤t_node) {
|
||||
let mut has_outward_wire = false;
|
||||
for output_index in 0..self.number_of_outputs(¤t_node, network_path) {
|
||||
let Some(outward_wires) = self.outward_wires(network_path) else {
|
||||
log::error!("Cannot load outward wires in load_stack_dependents");
|
||||
continue;
|
||||
};
|
||||
let Some(outward_wires) = outward_wires.get(&OutputConnector::node(current_node, output_index)) else {
|
||||
log::error!("Cannot load outward wires in load_stack_dependents");
|
||||
continue;
|
||||
};
|
||||
for downstream_input in outward_wires {
|
||||
has_outward_wire = true;
|
||||
match downstream_input {
|
||||
InputConnector::Node { node_id, .. } => stack.push(*node_id),
|
||||
InputConnector::Export(_) => is_sole_dependent = false,
|
||||
}
|
||||
}
|
||||
}
|
||||
if !has_outward_wire {
|
||||
is_sole_dependent = false;
|
||||
}
|
||||
}
|
||||
if !is_sole_dependent {
|
||||
break;
|
||||
}
|
||||
if sole_dependents.contains(&upstream_node) || not_sole_dependents.contains(&upstream_node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A path terminates at an already-verified sole dependent, and fails fast through a known non-sole node
|
||||
let is_sole_dependent = self.is_sole_dependent(upstream_node, network_path, |downstream_node, _| {
|
||||
if not_sole_dependents.contains(&downstream_node) {
|
||||
SoleDependentStep::Escape
|
||||
} else if sole_dependents.contains(&downstream_node) {
|
||||
SoleDependentStep::Terminate
|
||||
} else {
|
||||
SoleDependentStep::Continue
|
||||
}
|
||||
});
|
||||
|
||||
if is_sole_dependent {
|
||||
sole_dependents.insert(upstream_node);
|
||||
} else {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -557,14 +557,13 @@ impl NodeNetworkInterface {
|
||||
log::error!("Could not get node in set_implementation");
|
||||
return;
|
||||
};
|
||||
let new_implementation = std::mem::take(&mut new_template.document_node.implementation);
|
||||
let _ = std::mem::replace(&mut node.implementation, new_implementation);
|
||||
let (new_implementation, new_network_metadata) = std::mem::take(&mut new_template.implementation).into_parts();
|
||||
node.implementation = new_implementation;
|
||||
let Some(metadata) = self.node_metadata_mut(node_id, network_path) else {
|
||||
log::error!("Could not get metadata in set_implementation");
|
||||
return;
|
||||
};
|
||||
let new_metadata = std::mem::take(&mut new_template.persistent_node_metadata.network_metadata);
|
||||
let _ = std::mem::replace(&mut metadata.persistent_metadata.network_metadata, new_metadata);
|
||||
metadata.persistent_metadata.network_metadata = new_network_metadata;
|
||||
}
|
||||
|
||||
/// Replaces the inputs and corresponding metadata.
|
||||
@@ -577,13 +576,13 @@ impl NodeNetworkInterface {
|
||||
log::error!("Could not get node in set_implementation");
|
||||
return None;
|
||||
};
|
||||
let new_inputs = std::mem::take(&mut new_template.document_node.inputs);
|
||||
let new_inputs = std::mem::take(&mut new_template.inputs);
|
||||
let old_inputs = std::mem::replace(&mut node.inputs, new_inputs);
|
||||
let Some(metadata) = self.node_metadata_mut(node_id, network_path) else {
|
||||
log::error!("Could not get metadata in set_implementation");
|
||||
return None;
|
||||
};
|
||||
let new_metadata = std::mem::take(&mut new_template.persistent_node_metadata.input_metadata);
|
||||
let new_metadata = std::mem::take(&mut new_template.input_metadata);
|
||||
let _ = std::mem::replace(&mut metadata.persistent_metadata.input_metadata, new_metadata);
|
||||
Some(old_inputs)
|
||||
}
|
||||
@@ -597,7 +596,7 @@ impl NodeNetworkInterface {
|
||||
.reference(node_id, network_path)
|
||||
.as_ref()
|
||||
.and_then(resolve_document_node_type)
|
||||
.and_then(|definition| definition.node_template.persistent_node_metadata.input_metadata.get(added_input_index))
|
||||
.and_then(|definition| definition.node_template.input_metadata.get(added_input_index))
|
||||
.cloned();
|
||||
let Some(metadata) = self.node_metadata_mut(node_id, network_path) else { return };
|
||||
metadata.persistent_metadata.input_metadata.push(input_metadata.unwrap_or_default());
|
||||
@@ -1035,12 +1034,13 @@ impl NodeNetworkInterface {
|
||||
node_template = self.map_ids(node_template, &old_node_id, &new_ids, network_path);
|
||||
// Insert node into network
|
||||
let node_id = *new_ids.get(&old_node_id).unwrap();
|
||||
let (document_node, persistent_metadata) = node_template.into_parts();
|
||||
let Some(network) = self.network_mut(network_path) else {
|
||||
log::error!("Network not found in insert_node");
|
||||
return;
|
||||
};
|
||||
|
||||
network.nodes.insert(node_id, node_template.document_node);
|
||||
network.nodes.insert(node_id, document_node);
|
||||
self.transaction_modified();
|
||||
|
||||
let Some(network_metadata) = self.network_metadata_mut(network_path) else {
|
||||
@@ -1048,7 +1048,7 @@ impl NodeNetworkInterface {
|
||||
return;
|
||||
};
|
||||
let node_metadata = DocumentNodeMetadata {
|
||||
persistent_metadata: node_template.persistent_node_metadata,
|
||||
persistent_metadata,
|
||||
transient_metadata: DocumentNodeTransientMetadata::default(),
|
||||
};
|
||||
network_metadata.persistent_metadata.node_metadata.insert(node_id, node_metadata);
|
||||
@@ -1063,17 +1063,17 @@ impl NodeNetworkInterface {
|
||||
/// Used to insert a node template with no node/network inputs into the network and returns the a NodeTemplate with information from the previous node, if it existed.
|
||||
pub fn insert_node(&mut self, node_id: NodeId, node_template: NodeTemplate, network_path: &[NodeId]) -> Option<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 {
|
||||
@@ -1081,7 +1081,7 @@ impl NodeNetworkInterface {
|
||||
return None;
|
||||
};
|
||||
let node_metadata = DocumentNodeMetadata {
|
||||
persistent_metadata: node_template.persistent_node_metadata,
|
||||
persistent_metadata,
|
||||
transient_metadata: DocumentNodeTransientMetadata::default(),
|
||||
};
|
||||
let previous_metadata = network_metadata.persistent_metadata.node_metadata.insert(node_id, node_metadata);
|
||||
@@ -1089,18 +1089,26 @@ impl NodeNetworkInterface {
|
||||
self.unload_all_nodes_bounding_box(network_path);
|
||||
self.unload_node_click_targets(&node_id, network_path);
|
||||
|
||||
previous_node.zip(previous_metadata).map(|(document_node, node_metadata)| NodeTemplate {
|
||||
document_node,
|
||||
persistent_node_metadata: node_metadata.persistent_metadata,
|
||||
})
|
||||
previous_node
|
||||
.zip(previous_metadata)
|
||||
.map(|(document_node, node_metadata)| NodeTemplate::from_parts(document_node, node_metadata.persistent_metadata))
|
||||
}
|
||||
|
||||
/// Deletes all nodes in `node_ids` and any sole dependents in the horizontal chain if the node to delete is a layer node.
|
||||
pub fn delete_nodes(&mut self, nodes_to_delete: Vec<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 {
|
||||
@@ -1121,45 +1129,18 @@ impl NodeNetworkInterface {
|
||||
upstream_nodes.push(upstream_node);
|
||||
}
|
||||
}
|
||||
// For each potential child perform a complete downstream traversal, ending at either a node in the `delete_nodes` set (excluding layer bottom inputs), the output, or a dead end.
|
||||
// If the output node is eventually reached, then it is not a sole dependent and will not be deleted
|
||||
let mut stack = vec![upstream_node];
|
||||
let mut can_delete = true;
|
||||
while let Some(current_node) = stack.pop() {
|
||||
let mut is_dead_end = true;
|
||||
for output_connector in (0..self.number_of_outputs(¤t_node, network_path)).map(|output_index| OutputConnector::node(current_node, output_index)) {
|
||||
let Some(downstream_nodes) = outward_wires.get(&output_connector) else { continue };
|
||||
if !downstream_nodes.is_empty() {
|
||||
is_dead_end = false
|
||||
}
|
||||
for downstream_node in downstream_nodes {
|
||||
if let InputConnector::Node { node_id: downstream_id, input_index } = downstream_node {
|
||||
// If the downstream node is not in the delete nodes set, then continue iterating
|
||||
// If the downstream node is the bottom input of a layer then continue iterating
|
||||
if !delete_nodes.contains(downstream_id) || (*input_index == 0 && self.is_layer(downstream_id, network_path)) {
|
||||
stack.push(*downstream_id);
|
||||
}
|
||||
// If the traversal reaches the primary input of the node to delete then do not delete it
|
||||
if node_id == downstream_id && *input_index == 0 {
|
||||
can_delete = false;
|
||||
stack = Vec::new();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If the traversal reaches the export, then the current node is not a sole dependent and cannot be deleted
|
||||
else {
|
||||
can_delete = false;
|
||||
stack = Vec::new();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -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(¤t_node, network_path);
|
||||
let Some(outward_wires) = self.outward_wires(network_path) else {
|
||||
log::error!("Could not get outward wires in is_sole_dependent");
|
||||
return false;
|
||||
};
|
||||
|
||||
// Classify every downstream connection of this node, collecting the ones to keep walking through
|
||||
let mut has_downstream_connections = false;
|
||||
let mut nodes_to_walk_through = Vec::new();
|
||||
for output_index in 0..number_of_outputs {
|
||||
let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(current_node, output_index)) else {
|
||||
continue;
|
||||
};
|
||||
for downstream_connection in downstream_connections {
|
||||
has_downstream_connections = true;
|
||||
let InputConnector::Node {
|
||||
node_id: downstream_node,
|
||||
input_index,
|
||||
} = downstream_connection
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
match classify(*downstream_node, *input_index) {
|
||||
SoleDependentStep::Terminate => {}
|
||||
SoleDependentStep::Continue => nodes_to_walk_through.push(*downstream_node),
|
||||
SoleDependentStep::Escape => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !has_downstream_connections {
|
||||
return false;
|
||||
}
|
||||
stack.extend(nodes_to_walk_through);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
// All chain nodes and branches from the chain which are sole dependents of the layer
|
||||
pub fn upstream_nodes_below_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> HashSet<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 ¤t_downstream_connection {
|
||||
InputConnector::Node {
|
||||
node_id: downstream_node_id,
|
||||
input_index,
|
||||
} => {
|
||||
// Stop iterating once the downstream node is the left input to the chain or a sole dependent
|
||||
if !(sole_dependents.contains(downstream_node_id) || downstream_node_id == node_id && *input_index == 1) {
|
||||
// Continue iterating downstream for the downstream node
|
||||
let number_of_outputs = self.number_of_outputs(downstream_node_id, network_path);
|
||||
let Some(outward_wires) = self.outward_wires(network_path) else {
|
||||
log::error!("Could not get outward wires in upstream_nodes_below_layer");
|
||||
continue;
|
||||
};
|
||||
let mut has_downstream_connections = false;
|
||||
for output_index in 0..number_of_outputs {
|
||||
let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(*downstream_node_id, output_index)) else {
|
||||
log::error!("Could not get outward wires in upstream_nodes_below_layer");
|
||||
continue;
|
||||
};
|
||||
if !downstream_connections.is_empty() {
|
||||
has_downstream_connections = true;
|
||||
}
|
||||
stack.extend(downstream_connections.clone());
|
||||
}
|
||||
if !has_downstream_connections {
|
||||
dead_ends += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
InputConnector::Export(_) => current_output_is_sole_dependent = false,
|
||||
}
|
||||
// A path terminates at an already-verified sole dependent or at the left input to the chain
|
||||
let is_sole_dependent = self.is_sole_dependent(upstream_node_from_input, network_path, |downstream_node, input_index| {
|
||||
if sole_dependents.contains(&downstream_node) || downstream_node == *node_id && input_index == 1 {
|
||||
SoleDependentStep::Terminate
|
||||
} else {
|
||||
SoleDependentStep::Continue
|
||||
}
|
||||
if !current_output_is_sole_dependent || dead_ends != 0 {
|
||||
all_outputs_are_sole_dependents = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if all_outputs_are_sole_dependents && dead_ends == 0 {
|
||||
});
|
||||
|
||||
if is_sole_dependent {
|
||||
sole_dependents.insert(upstream_node_from_input);
|
||||
} else {
|
||||
upstream_chain_can_be_added = false;
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
use super::*;
|
||||
use graph_craft::ProtoNodeIdentifier;
|
||||
use graph_craft::concrete;
|
||||
use graphene_std::Context;
|
||||
|
||||
// PartialEq required by message handlers
|
||||
/// All persistent editor and Graphene data for a node, unified into a single flat shape.
|
||||
/// Used to author node definitions, pass nodes through the editor, and serialize them for the clipboard.
|
||||
///
|
||||
/// [`Self::into_parts`] and [`Self::from_parts`] are the only places this is split into (or joined from) the
|
||||
/// [`DocumentNode`] and [`DocumentNodePersistentMetadata`] halves stored by the network interface, so the parallel-tree
|
||||
/// storage invariants hold by construction for every node built from a template.
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(from = "NodeTemplateParts", into = "NodeTemplateParts")]
|
||||
pub struct NodeTemplate {
|
||||
/// The graph data inputs to the node. Kept the same length as `input_metadata` by [`Self::normalize_input_metadata`] and [`Self::into_parts`].
|
||||
pub inputs: Vec<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalizes the stored types at every nesting level via [`DocumentNode::normalize_stored_types`], round-tripping through the split halves to reuse its logic.
|
||||
pub fn normalize_stored_types(&mut self) {
|
||||
let (mut document_node, persistent_node_metadata) = std::mem::take(self).into_parts();
|
||||
document_node.normalize_stored_types();
|
||||
*self = NodeTemplate::from_parts(document_node, persistent_node_metadata);
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeTemplateImplementation {
|
||||
/// Splits into the [`DocumentNodeImplementation`] and the nested network metadata stored alongside it.
|
||||
pub fn into_parts(self) -> (DocumentNodeImplementation, Option<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,
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1151,10 +1151,12 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
||||
if let DocumentNodeImplementation::ProtoNode(protonode_id) = &node.implementation {
|
||||
let node_path_without_type_args = protonode_id.as_str().split('<').next();
|
||||
if let Some(new) = node_path_without_type_args.and_then(|node_path| replacements.get(node_path)) {
|
||||
let mut default_template = NodeTemplate::default();
|
||||
default_template.document_node.implementation = DocumentNodeImplementation::ProtoNode(new.clone());
|
||||
let mut default_template = NodeTemplate {
|
||||
implementation: NodeTemplateImplementation::ProtoNode(new.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
document.network_interface.replace_implementation(node_id, &network_path, &mut default_template);
|
||||
document.network_interface.set_call_argument(node_id, &network_path, default_template.document_node.call_argument);
|
||||
document.network_interface.set_call_argument(node_id, &network_path, default_template.call_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1224,7 +1226,7 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
||||
// (which represented radians in the legacy format) reaches the now-degrees Rotation input correctly.
|
||||
if let Some(multiply_node) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::multiply::IDENTIFIER)) {
|
||||
let mut multiply_template = multiply_node.default_node_template();
|
||||
multiply_template.document_node.inputs[1] = NodeInput::value(TaggedValue::F64(180. / PI), false);
|
||||
multiply_template.inputs[1] = NodeInput::value(TaggedValue::F64(180. / PI), false);
|
||||
let multiply_node_id = NodeId::new();
|
||||
if let Some(transform_position) = document.network_interface.position_from_downstream_node(node_id, network_path) {
|
||||
let multiply_position = transform_position + IVec2::new(-7, 1);
|
||||
@@ -2172,7 +2174,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
// A brush node saved before `Item<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);
|
||||
}
|
||||
|
||||
@@ -2423,7 +2425,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
return None;
|
||||
};
|
||||
let mut subtract_template = subtract_def.default_node_template();
|
||||
subtract_template.document_node.inputs[1] = NodeInput::value(TaggedValue::F64(1.), false);
|
||||
subtract_template.inputs[1] = NodeInput::value(TaggedValue::F64(1.), false);
|
||||
let subtract_id = NodeId::new();
|
||||
|
||||
// Create Divide node: old_progression / (N-1) → new progression
|
||||
@@ -2518,7 +2520,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
return None;
|
||||
};
|
||||
let mut transform_template = transform_node_type.default_node_template();
|
||||
transform_template.document_node.inputs[1] = NodeInput::value(TaggedValue::DVec2(start), false);
|
||||
transform_template.inputs[1] = NodeInput::value(TaggedValue::DVec2(start), false);
|
||||
|
||||
let transform_id = NodeId::new();
|
||||
|
||||
@@ -2579,7 +2581,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
return None;
|
||||
};
|
||||
let mut transform_template = transform_node_type.default_node_template();
|
||||
transform_template.document_node.inputs[1] = NodeInput::value(TaggedValue::DVec2(start), false);
|
||||
transform_template.inputs[1] = NodeInput::value(TaggedValue::DVec2(start), false);
|
||||
|
||||
let transform_id = NodeId::new();
|
||||
|
||||
@@ -2596,7 +2598,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
&& let Some(reference) = document.network_interface.reference(node_id, network_path).clone()
|
||||
&& let Some(node_definition) = resolve_document_node_type(&reference)
|
||||
{
|
||||
let context_features = node_definition.node_template.document_node.context_features;
|
||||
let context_features = node_definition.node_template.context_features;
|
||||
document.network_interface.set_context_features(node_id, network_path, context_features);
|
||||
}
|
||||
|
||||
@@ -2671,7 +2673,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
// A value input stored as a List-form TypeDefault adopts the definition's current default when the connector's declared default has since changed (e.g. the connector was ranked down to Item).
|
||||
// The red-slash no-paint choice shares that stored form but is a deliberate value, not a stale disconnect default, so it is exempt.
|
||||
if let Some(definition) = resolve_document_node_type(&reference) {
|
||||
let definition_inputs = definition.node_template.document_node.inputs.clone();
|
||||
let definition_inputs = definition.node_template.inputs.clone();
|
||||
for (index, definition_input) in definition_inputs.iter().enumerate() {
|
||||
if !matches!(definition_input, NodeInput::Value { .. }) {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user