mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 06:38:11 +08:00
Add multi-output nodes with struct returns destructured by #[node_macro::destructure]
A #[node_macro::node] function returning a struct tagged with
field is a named output connector (title-cased from the field name,
renamed with #[name("...")], described by its doc comment). By default
the node has a hidden primary output carrying the whole struct with the
fields as secondary outputs; marking at most one field #[primary] makes
that field the primary output instead.
The macro generates one hidden extractor proto node per field plus a
registration keyed by the struct's TypeId. The Graphene preprocessor
recognizes nodes returning a registered struct and substitutes them, in
the transient runtime copy of the network only, with a generated network
exporting each field through its extractor. The destructuring machinery
therefore never appears when drilling into a node, in copied clipboard
content, or in saved documents. When a Memoize implementation is
registered for the struct type, the struct is computed once and shared
across all outputs rather than re-evaluated per output.
The editor derives output counts, names, and types for such nodes from
the registry. The old hand-authored "Split Vec2" and "Split Channels"
wrapper-network definitions are replaced by multi-output split_vec2 and
split_channels proto nodes, with document migrations that keep existing
wires valid since the output indices are unchanged.
The "Position on Path" and "Tangent on Path" nodes are combined into a
single multi-output "Evaluate Path" node whose primary output is the
position and whose secondary output is the tangent angle. A migration
converts old instances, forwarding the shared inputs and remapping the
tangent nodes' downstream connections to the new tangent output index.
The now-redundant "Extract XY" node is removed (its role is subsumed by
Split Vec2's destructuring), and "Extract Channel" becomes a plain helper
used by Split Channels rather than a standalone node.
This commit is contained in:
@@ -15,9 +15,7 @@ use graph_craft::ProtoNodeIdentifier;
|
||||
use graph_craft::document::value::*;
|
||||
use graph_craft::document::*;
|
||||
use graph_craft::{concrete, list};
|
||||
use graphene_std::extract_xy::XY;
|
||||
use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha};
|
||||
use graphene_std::raster_types::{CPU, Raster};
|
||||
use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType};
|
||||
#[allow(unused_imports)]
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::vector::Vector;
|
||||
@@ -1059,185 +1057,6 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
description: Cow::Borrowed("TODO"),
|
||||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Split Channels",
|
||||
category: "Raster: Channels",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
exports: vec![
|
||||
NodeInput::value(TaggedValue::None, false),
|
||||
NodeInput::node(NodeId(0), 0),
|
||||
NodeInput::node(NodeId(1), 0),
|
||||
NodeInput::node(NodeId(2), 0),
|
||||
NodeInput::node(NodeId(3), 0),
|
||||
],
|
||||
nodes: [
|
||||
DocumentNode {
|
||||
inputs: vec![
|
||||
NodeInput::import(list!(Raster<CPU>), 0),
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Red), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![
|
||||
NodeInput::import(list!(Raster<CPU>), 0),
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Green), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![
|
||||
NodeInput::import(list!(Raster<CPU>), 0),
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Blue), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![
|
||||
NodeInput::import(list!(Raster<CPU>), 0),
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Alpha), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(id, node)| (NodeId(id as u64), node))
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}),
|
||||
inputs: vec![NodeInput::type_default(list!(Raster<CPU>), true)],
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
input_metadata: vec![("Image", "TODO").into()],
|
||||
output_names: vec!["".to_string(), "Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()],
|
||||
network_metadata: Some(NodeNetworkMetadata {
|
||||
persistent_metadata: NodeNetworkPersistentMetadata {
|
||||
node_metadata: [
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 2)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 4)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 6)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(id, node)| (NodeId(id as u64), node))
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
description: Cow::Borrowed("TODO"),
|
||||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Split Vec2",
|
||||
category: "Math: Vec2",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
exports: vec![NodeInput::value(TaggedValue::None, false), NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
|
||||
nodes: [
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::import(item!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::import(item!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(id, node)| (NodeId(id as u64), node))
|
||||
.collect(),
|
||||
|
||||
..Default::default()
|
||||
}),
|
||||
inputs: vec![NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), true)],
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
input_metadata: vec![("Vec2", "TODO").into()],
|
||||
output_names: vec!["".to_string(), "X".to_string(), "Y".to_string()],
|
||||
network_metadata: Some(NodeNetworkMetadata {
|
||||
persistent_metadata: NodeNetworkPersistentMetadata {
|
||||
node_metadata: [
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 2)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(id, node)| (NodeId(id as u64), node))
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
description: Cow::Borrowed(
|
||||
"Decomposes the X and Y components of a vec2.\n\
|
||||
\n\
|
||||
The inverse of this node is **Combine Vec2**, which composes a vec2 from its X and Y components.",
|
||||
),
|
||||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Extract",
|
||||
category: "",
|
||||
|
||||
+14
@@ -28,6 +28,8 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
||||
// `interpreted_executor::node_registry::NODE_REGISTRY` via `async_node!`. We consult that extended registry as a
|
||||
// fallback when deriving `call_argument` so it reflects the impls actually registered, which will usually be `Context`.
|
||||
let extended_node_registry = &*interpreted_executor::node_registry::NODE_REGISTRY;
|
||||
// Pre-initialize the multi-output node map here since its initializer locks NODE_REGISTRY, which would deadlock once we hold the lock below
|
||||
let multi_output_nodes = &*MULTI_OUTPUT_NODES;
|
||||
let node_registry = NODE_REGISTRY.lock().unwrap();
|
||||
let empty_implementations: Vec<(NodeConstructor, NodeIOTypes)> = Vec::new();
|
||||
let context_type = concrete!(Context);
|
||||
@@ -67,6 +69,17 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
||||
};
|
||||
|
||||
let inputs = preprocessor::node_inputs(fields, first_node_io);
|
||||
|
||||
// A multi-output node (returning a `#[node_macro::destructure]` struct) names each output after its struct field,
|
||||
// preceded by an unnamed entry for the hidden primary output unless one field is marked `#[primary]`
|
||||
let output_names = multi_output_nodes
|
||||
.get(id)
|
||||
.map(|metadata| {
|
||||
let hidden_primary_name = (!metadata.has_primary).then(String::new);
|
||||
hidden_primary_name.into_iter().chain(metadata.fields.iter().map(|field| field.name.to_string())).collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
definitions_map.insert(
|
||||
identifier,
|
||||
DocumentNodeDefinition {
|
||||
@@ -92,6 +105,7 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
||||
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
|
||||
})
|
||||
.collect(),
|
||||
output_names,
|
||||
locked: false,
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ use graphene_std::ContextDependencies;
|
||||
use graphene_std::Graphic;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::registry::MULTI_OUTPUT_NODES;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType, FreePoint};
|
||||
@@ -365,7 +366,12 @@ impl NodeNetworkInterface {
|
||||
return 0;
|
||||
};
|
||||
match &implementation {
|
||||
DocumentNodeImplementation::ProtoNode(_) => 1,
|
||||
// A multi-output proto node (returning a `#[node_macro::destructure]` struct) has one output per field,
|
||||
// preceded by a hidden primary output carrying the struct itself unless one field is marked `#[primary]`
|
||||
DocumentNodeImplementation::ProtoNode(identifier) => match MULTI_OUTPUT_NODES.get(identifier) {
|
||||
Some(metadata) => metadata.fields.len() + if metadata.has_primary { 0 } else { 1 },
|
||||
None => 1,
|
||||
},
|
||||
DocumentNodeImplementation::Network(nested_network) => nested_network.exports.len(),
|
||||
DocumentNodeImplementation::Extract => 1,
|
||||
}
|
||||
@@ -1140,6 +1146,9 @@ impl NodeNetworkInterface {
|
||||
pub fn hidden_primary_output(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
|
||||
match self.implementation(node_id, network_path) {
|
||||
Some(DocumentNodeImplementation::Network(network)) => network.exports.first().is_none_or(|input| !input.is_exposed()),
|
||||
// A multi-output proto node's primary output carries the whole struct, hidden so only the destructured field
|
||||
// outputs are shown, unless a field marked `#[primary]` takes its place as the primary output
|
||||
Some(DocumentNodeImplementation::ProtoNode(identifier)) => MULTI_OUTPUT_NODES.get(identifier).is_some_and(|metadata| !metadata.has_primary),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -2618,10 +2627,7 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
}
|
||||
|
||||
let number_of_outputs = match &document_node.implementation {
|
||||
DocumentNodeImplementation::Network(network) => network.exports.len(),
|
||||
_ => 1,
|
||||
};
|
||||
let number_of_outputs = self.number_of_outputs(node_id, network_path);
|
||||
// If the node has a hidden primary output, do not display the first output
|
||||
let start_index = if self.hidden_primary_output(node_id, network_path) { 1 } else { 0 };
|
||||
for output_index in start_index..number_of_outputs {
|
||||
@@ -4791,6 +4797,20 @@ impl NodeNetworkInterface {
|
||||
self.unload_node_click_targets(node_id, network_path);
|
||||
}
|
||||
|
||||
/// Replaces the full list of output port names for a node. Used by document migrations that turn a single-output node
|
||||
/// into a multi-output one, since the port labels are otherwise unnamed and fall back to the type name.
|
||||
pub fn set_output_names(&mut self, node_id: &NodeId, output_names: Vec<String>, network_path: &[NodeId]) {
|
||||
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {
|
||||
log::error!("Could not get node {node_id} in set_output_names");
|
||||
return;
|
||||
};
|
||||
if node_metadata.persistent_metadata.output_names == output_names {
|
||||
return;
|
||||
}
|
||||
node_metadata.persistent_metadata.output_names = output_names;
|
||||
self.transaction_modified();
|
||||
}
|
||||
|
||||
pub fn set_import_export_name(&mut self, mut name: String, index: ImportOrExport, network_path: &[NodeId]) {
|
||||
let Some(encapsulating_node) = self.encapsulating_node_metadata_mut(network_path) else {
|
||||
log::error!("Could not get encapsulating network in set_import_export_name");
|
||||
|
||||
+31
-4
@@ -355,10 +355,37 @@ impl NodeNetworkInterface {
|
||||
DocumentNodeImplementation::Network(_) => self.input_type(&InputConnector::Export(*output_index), &[network_path, &[*node_id]].concat()),
|
||||
// The compiler removes passthrough nodes so they resolve no type of their own, but their output carries their primary input's type
|
||||
DocumentNodeImplementation::ProtoNode(identifier) if *identifier == graphene_std::ops::passthrough::IDENTIFIER => self.input_type(&InputConnector::node(*node_id, 0), network_path),
|
||||
DocumentNodeImplementation::ProtoNode(_) => match self.resolved_types.types.get(&[network_path, &[*node_id]].concat()) {
|
||||
Some(resolved_type) => TypeSource::Compiled(resolved_type.output.clone()),
|
||||
None => TypeSource::Unknown,
|
||||
},
|
||||
DocumentNodeImplementation::ProtoNode(identifier) => {
|
||||
// The field outputs of a multi-output proto node have their element types recorded in the registry, and
|
||||
// ride the struct's resolved rank since a framed multi-output node emits one list per field. Without a
|
||||
// `#[primary]` field, output 0 is the hidden struct output, which falls through to the compiled types below.
|
||||
if let Some(metadata) = graphene_std::registry::MULTI_OUTPUT_NODES.get(identifier) {
|
||||
let field_index = if metadata.has_primary { Some(*output_index) } else { output_index.checked_sub(1) };
|
||||
if let Some(field_index) = field_index {
|
||||
return match metadata.fields.get(field_index) {
|
||||
Some(field) => {
|
||||
let struct_is_rank_1 = self
|
||||
.resolved_types
|
||||
.types
|
||||
.get(&[network_path, &[*node_id]].concat())
|
||||
.is_some_and(|resolved_type| matches!(resolved_type.output.nested_type(), Type::List(_)));
|
||||
let field_wire = if struct_is_rank_1 {
|
||||
Type::List(Box::new(field.ty.clone()))
|
||||
} else {
|
||||
Type::Item(Box::new(field.ty.clone()))
|
||||
};
|
||||
TypeSource::Compiled(field_wire)
|
||||
}
|
||||
None => TypeSource::Error("Output index out of range for proto node"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
match self.resolved_types.types.get(&[network_path, &[*node_id]].concat()) {
|
||||
Some(resolved_type) => TypeSource::Compiled(resolved_type.output.clone()),
|
||||
None => TypeSource::Unknown,
|
||||
}
|
||||
}
|
||||
DocumentNodeImplementation::Extract => TypeSource::Compiled(concrete!(())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,10 +93,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
node: graphene_std::animation::animation_time::IDENTIFIER,
|
||||
aliases: &["graphene_core::animation::AnimationTimeNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::extract_xy::extract_xy::IDENTIFIER,
|
||||
aliases: &["graphene_core::ops::ExtractXyNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::ops::passthrough::IDENTIFIER,
|
||||
aliases: &[
|
||||
@@ -519,14 +515,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
node: graphene_std::raster_nodes::std_nodes::extend_image_to_bounds::IDENTIFIER,
|
||||
aliases: &["graphene_raster_nodes::std_nodes::ExtendImageToBoundsNode", "graphene_std::raster::ExtendImageToBoundsNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::raster_nodes::adjustments::extract_channel::IDENTIFIER,
|
||||
aliases: &[
|
||||
"graphene_raster_nodes::adjustments::ExtractChannelNode",
|
||||
"graphene_core::raster::adjustments::ExtractChannelNode",
|
||||
"graphene_core::raster::ExtractChannelNode",
|
||||
],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::raster_nodes::adjustments::gamma_correction::IDENTIFIER,
|
||||
aliases: &["graphene_raster_nodes::adjustments::GammaCorrectionNode", "graphene_core::raster::adjustments::GammaCorrectionNode"],
|
||||
@@ -928,10 +916,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
node: graphene_std::vector::scatter_points::IDENTIFIER,
|
||||
aliases: &["graphene_core::vector::PoissonDiskPointsNode", "core_types::vector::PoissonDiskPointsNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::vector::position_on_path::IDENTIFIER,
|
||||
aliases: &["graphene_core::vector::PositionOnPathNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::vector::round_corners::IDENTIFIER,
|
||||
aliases: &["graphene_core::vector::RoundCornersNode"],
|
||||
@@ -964,10 +948,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
node: graphene_std::vector::stroke::IDENTIFIER,
|
||||
aliases: &["graphene_core::vector::StrokeNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::vector::tangent_on_path::IDENTIFIER,
|
||||
aliases: &["graphene_core::vector::TangentOnPathNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::vector::as_vector::IDENTIFIER,
|
||||
aliases: &[
|
||||
@@ -1272,6 +1252,96 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
||||
}
|
||||
}
|
||||
|
||||
// The "Split Vec2" and "Split Channels" wrapper networks were replaced with the multi-output `split_vec2` and `split_channels` proto nodes.
|
||||
// Convert old instances to the proto node, forwarding the single input. Output indices are unchanged: the hidden primary output stays at
|
||||
// index 0 (now carrying the destructured struct) and the field outputs keep their old indices, so downstream wires stay connected.
|
||||
// Pre-pass for the same reason as the Brush, Transform, and Image migrations above: replacing the outer network impl orphans its child paths.
|
||||
let split_wrapper_replacements = [
|
||||
("Split Vec2", graphene_std::extract_xy::split_vec_2::IDENTIFIER),
|
||||
("Split Channels", graphene_std::raster_nodes::adjustments::split_channels::IDENTIFIER),
|
||||
];
|
||||
for (old_reference, new_identifier) in split_wrapper_replacements {
|
||||
let split_nodes: Vec<(NodeId, Vec<NodeId>)> = document
|
||||
.network_interface
|
||||
.document_network()
|
||||
.recursive_nodes()
|
||||
.filter_map(|(node_id, _, path)| (document.network_interface.reference(node_id, &path) == Some(DefinitionIdentifier::Network(old_reference.into()))).then_some((*node_id, path)))
|
||||
.collect();
|
||||
for (node_id, network_path) in &split_nodes {
|
||||
// Pre-load `outward_wires` so the chain-break check inside `set_input` resolves the original upstream→node wire from cache
|
||||
// rather than triggering a fresh rebuild from the (already-mutated) post-`replace_inputs` state, which would orphan wires.
|
||||
let _ = document.network_interface.outward_wires(network_path);
|
||||
let new_reference = DefinitionIdentifier::ProtoNode(new_identifier.clone());
|
||||
let Some(definition) = resolve_document_node_type(&new_reference) else { continue };
|
||||
let mut node_template = definition.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
let Some(old_inputs) = document.network_interface.replace_inputs(node_id, network_path, &mut node_template) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(input) = old_inputs.first() {
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), input.clone(), network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The "Position on Path" and "Tangent on Path" nodes were combined into a single multi-output "Evaluate Path" node whose primary
|
||||
// output (index 0) is the position and whose secondary output (index 1) is the tangent angle. Convert old instances to the new node,
|
||||
// forwarding the shared inputs. Tangent instances additionally have their downstream connections remapped from output 0 to output 1,
|
||||
// and default their radians input to true to match the old "Tangent on Path" behavior where radians was the only option.
|
||||
const POSITION_ON_PATH: &str = "graphene_core::vector::PositionOnPathNode";
|
||||
const TANGENT_ON_PATH: &str = "graphene_core::vector::TangentOnPathNode";
|
||||
let evaluate_path_nodes: Vec<(NodeId, Vec<NodeId>, bool)> = document
|
||||
.network_interface
|
||||
.document_network()
|
||||
.recursive_nodes()
|
||||
.filter_map(|(node_id, node, path)| {
|
||||
let DocumentNodeImplementation::ProtoNode(identifier) = &node.implementation else { return None };
|
||||
match identifier.as_str() {
|
||||
POSITION_ON_PATH => Some((*node_id, path, false)),
|
||||
TANGENT_ON_PATH => Some((*node_id, path, true)),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for (node_id, network_path, is_tangent) in &evaluate_path_nodes {
|
||||
// Capture the old output's downstream connections before mutating, so a tangent node's wires can be remapped to output index 1
|
||||
let _ = document.network_interface.outward_wires(network_path);
|
||||
let downstream_from_output = document
|
||||
.network_interface
|
||||
.outward_wires(network_path)
|
||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(*node_id, 0)))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let new_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::evaluate_path::IDENTIFIER);
|
||||
let Some(definition) = resolve_document_node_type(&new_reference) else { continue };
|
||||
let mut node_template = definition.default_node_template();
|
||||
let output_names = definition.node_template.persistent_node_metadata.output_names.clone();
|
||||
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
let Some(old_inputs) = document.network_interface.replace_inputs(node_id, network_path, &mut node_template) else {
|
||||
continue;
|
||||
};
|
||||
// The old single-output nodes have no output names, so set them to match the new multi-output node's "Position" and "Tangent" ports
|
||||
document.network_interface.set_output_names(node_id, output_names, network_path);
|
||||
|
||||
// Forward the shared inputs: content, progression, reverse, and parameterized distance
|
||||
for (index, input) in old_inputs.iter().take(4).enumerate() {
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, index), input.clone(), network_path);
|
||||
}
|
||||
|
||||
if *is_tangent {
|
||||
// Forward the radians input if the tangent node already had it, otherwise default it to true to preserve the old behavior
|
||||
let radians = old_inputs.get(4).cloned().unwrap_or_else(|| NodeInput::value(TaggedValue::Bool(true), false));
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 4), radians, network_path);
|
||||
|
||||
// Remap the tangent node's downstream connections from the old single output to the new tangent output at index 1
|
||||
for input_connector in &downstream_from_output {
|
||||
document.network_interface.set_input(input_connector, NodeInput::node(*node_id, 1), network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Record which old text nodes are chain-positioned now, before `migrate_node`'s staged input-count migrations run, since those set
|
||||
// the upstream chain to absolute; the split below re-chains exactly the nodes that were originally part of a layer chain.
|
||||
let text_nodes_in_chain: std::collections::HashSet<NodeId> = document
|
||||
@@ -1993,22 +2063,6 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||
}
|
||||
|
||||
// Upgrade the 'Tangent on Path' node to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::tangent_on_path::IDENTIFIER) && inputs_count == 4 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||
}
|
||||
|
||||
// Upgrade the Modulo node to include a boolean input for whether the output should be always positive, which was previously not an option
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::modulo::IDENTIFIER) && inputs_count == 2 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
|
||||
Reference in New Issue
Block a user