Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -663,114 +661,6 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
identifier: "Split Channels",
category: "Raster: Channels",
node_template: NodeTemplate {
implementation: NodeTemplateImplementation::Network(NodeNetworkTemplate {
exports: vec![
NodeInput::value(TaggedValue::None, false),
NodeInput::node(NodeId(0), 0),
NodeInput::node(NodeId(1), 0),
NodeInput::node(NodeId(2), 0),
NodeInput::node(NodeId(3), 0),
],
nodes: [
NodeTemplate {
inputs: vec![
NodeInput::import(list!(Raster<CPU>), 0),
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Red), false),
],
implementation: NodeTemplateImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
call_argument: generic!(T),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
..Default::default()
},
NodeTemplate {
inputs: vec![
NodeInput::import(list!(Raster<CPU>), 0),
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Green), false),
],
implementation: NodeTemplateImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
call_argument: generic!(T),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 2)),
..Default::default()
},
NodeTemplate {
inputs: vec![
NodeInput::import(list!(Raster<CPU>), 0),
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Blue), false),
],
implementation: NodeTemplateImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
call_argument: generic!(T),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 4)),
..Default::default()
},
NodeTemplate {
inputs: vec![
NodeInput::import(list!(Raster<CPU>), 0),
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Alpha), false),
],
implementation: NodeTemplateImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
call_argument: generic!(T),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 6)),
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![NodeInput::type_default(list!(Raster<CPU>), true)],
input_metadata: vec![("Image", "TODO").into()],
output_names: vec!["".to_string(), "Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()],
..Default::default()
},
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
identifier: "Split Vec2",
category: "Math: Vec2",
node_template: NodeTemplate {
implementation: NodeTemplateImplementation::Network(NodeNetworkTemplate {
exports: vec![NodeInput::value(TaggedValue::None, false), NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
nodes: [
NodeTemplate {
inputs: vec![NodeInput::import(item!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
implementation: NodeTemplateImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
call_argument: generic!(T),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
..Default::default()
},
NodeTemplate {
inputs: vec![NodeInput::import(item!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
implementation: NodeTemplateImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
call_argument: generic!(T),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 2)),
..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)],
input_metadata: vec![("Vec2", "TODO").into()],
output_names: vec!["".to_string(), "X".to_string(), "Y".to_string()],
..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: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
description,
properties,
context_features,
output_fields,
..
} = metadata;

Expand All @@ -66,6 +67,17 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
};

let inputs = preprocessor::node_inputs(fields, first_node_io);

// A multi-output node (declared `destructure_output`) names each output after a field of its returned struct,
// preceded by an unnamed entry for the hidden primary output unless one field is marked `#[primary]`
let output_names = output_fields
.as_ref()
.map(|destructure| {
let hidden_primary_name = (!destructure.has_primary).then(String::new);
hidden_primary_name.into_iter().chain(destructure.fields.iter().map(|field| field.name.to_string())).collect()
})
.unwrap_or_default();

definitions_map.insert(
identifier,
DocumentNodeDefinition {
Expand All @@ -85,6 +97,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,
..Default::default()
},
category,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, No
use graphene_std::Appearance;
use graphene_std::ContextDependencies;
use graphene_std::math::quad::Quad;
use graphene_std::registry::MULTI_OUTPUT_NODES;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType, FreePoint};
use graphene_std::vector::{Vector, VectorModificationType};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -984,10 +984,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,32 @@ impl NodeNetworkInterface {
DocumentNodeImplementation::ProtoNode(identifier) if *identifier == graphene_std::ops::passthrough::IDENTIFIER => {
self.input_type(&InputConnector::primary_input(*node_id), 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 wire types recorded in the registry. When the
// node resolved to its mapped variant it returns the struct's rank-lifted twin, whose fields are all lists.
// 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 is_mapped = self
.resolved_types
.types
.get(&[network_path, &[*node_id]].concat())
.is_some_and(|resolved_type| *resolved_type.output.nested_type() == metadata.mapped_type);
TypeSource::Compiled(if is_mapped { field.mapped_ty.clone() } else { field.ty.clone() })
}
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!(())),
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ impl<'a, 'p> NetworkView<'a, 'p> {
pub fn number_of_outputs(&self, node_id: &NodeId) -> Result<usize, NetworkError> {
Ok(match self.implementation(node_id)? {
DocumentNodeImplementation::Network(nested_network) => nested_network.exports.len(),
// A multi-output proto node (declared `destructure_output`) has one output per field of the struct it returns,
// 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The multi-output layout invariant is reimplemented in three places: output_names in document_node_derive.rs ((!has_primary).then(String::new).chain(fields…)), number_of_outputs/hidden_primary_output here in view.rs, and the field_index computation in resolved_types.rs. Each copy encodes fields.len() + (has_primary ? 0 : 1) and the 0/1-based output↔field mapping separately, linked only by comments. A future change to one site (e.g., reordering the hidden primary, or changing the #[primary] rule) silently breaks port counts and wire-type lookup in the others. Add number_of_outputs(), is_primary_hidden(), and field_index_for_output(output_index) methods on DestructureMetadata in core-types/src/registry.rs and call them from all three sites so the layout has a single source of truth.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/utility_types/network_interface/view.rs, line 233:

<comment>The multi-output layout invariant is reimplemented in three places: `output_names` in document_node_derive.rs (`(!has_primary).then(String::new).chain(fields…)`), `number_of_outputs`/`hidden_primary_output` here in view.rs, and the `field_index` computation in resolved_types.rs. Each copy encodes `fields.len() + (has_primary ? 0 : 1)` and the 0/1-based output↔field mapping separately, linked only by comments. A future change to one site (e.g., reordering the hidden primary, or changing the `#[primary]` rule) silently breaks port counts and wire-type lookup in the others. Add `number_of_outputs()`, `is_primary_hidden()`, and `field_index_for_output(output_index)` methods on `DestructureMetadata` in core-types/src/registry.rs and call them from all three sites so the layout has a single source of truth.</comment>

<file context>
@@ -228,6 +228,12 @@ impl<'a, 'p> NetworkView<'a, 'p> {
 			DocumentNodeImplementation::Network(nested_network) => nested_network.exports.len(),
+			// A multi-output proto node (declared `destructure_output`) has one output per field of the struct it returns,
+			// 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,
</file context>

Some(metadata) => metadata.fields.len() + if metadata.has_primary { 0 } else { 1 },
None => 1,
},
_ => 1,
})
}
Expand All @@ -248,6 +254,9 @@ impl<'a, 'p> NetworkView<'a, 'p> {
pub fn hidden_primary_output(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
Ok(match self.implementation(node_id)? {
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
DocumentNodeImplementation::ProtoNode(identifier) => MULTI_OUTPUT_NODES.get(identifier).is_some_and(|metadata| !metadata.has_primary),
_ => false,
})
}
Expand Down
Loading
Loading