Skip to content

Commit 958810d

Browse files
committed
Potentially solve outstanding issues
1 parent e5e4936 commit 958810d

File tree

8 files changed

+235
-83
lines changed

8 files changed

+235
-83
lines changed

editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs

Lines changed: 18 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,55 +1196,29 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
11961196
description: Cow::Borrowed("Generates customizable procedural noise patterns."),
11971197
properties: None,
11981198
},
1199-
DocumentNodeDefinition {
1200-
identifier: "Split Channels",
1201-
category: "Raster: Channels",
1202-
node_template: NodeTemplate {
1203-
document_node: DocumentNode {
1204-
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::split_channels::IDENTIFIER),
1205-
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
1206-
call_argument: generic!(T),
1207-
..Default::default()
1208-
},
1209-
persistent_node_metadata: DocumentNodePersistentMetadata {
1210-
input_metadata: vec![("Image", "TODO").into()],
1211-
network_metadata: None,
1212-
..Default::default()
1213-
},
1214-
},
1215-
description: Cow::Borrowed("TODO"),
1216-
properties: None,
1217-
},
1218-
DocumentNodeDefinition {
1219-
identifier: "Split Vec2",
1220-
category: "Math: Vector",
1221-
node_template: NodeTemplate {
1222-
document_node: DocumentNode {
1223-
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::split_vec_2::IDENTIFIER),
1224-
inputs: vec![NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), true)],
1225-
call_argument: generic!(T),
1226-
..Default::default()
1227-
},
1228-
persistent_node_metadata: DocumentNodePersistentMetadata {
1229-
input_metadata: vec![("Vec2", "TODO").into()],
1230-
network_metadata: None,
1231-
..Default::default()
1232-
},
1233-
},
1234-
description: Cow::Borrowed(
1235-
"Decomposes the X and Y components of a vec2.\n\
1236-
\n\
1237-
The inverse of this node is \"Vec2 Value\", which can have either or both its X and Y parameters exposed as graph inputs.",
1238-
),
1239-
properties: None,
1240-
},
1241-
//TODO: Remove this and just use the proto node definition directly
1199+
// TODO: Remove this and just use the proto node definition directly
12421200
DocumentNodeDefinition {
12431201
identifier: "Brush",
12441202
category: "Raster",
12451203
node_template: NodeTemplate {
12461204
document_node: DocumentNode {
1247-
implementation: DocumentNodeImplementation::ProtoNode(brush::brush::brush::IDENTIFIER),
1205+
implementation: DocumentNodeImplementation::Network(NodeNetwork {
1206+
exports: vec![NodeInput::node(NodeId(0), 0)],
1207+
nodes: vec![DocumentNode {
1208+
inputs: vec![
1209+
NodeInput::import(concrete!(Table<Raster<CPU>>), 0),
1210+
NodeInput::import(concrete!(Vec<brush::brush_stroke::BrushStroke>), 1),
1211+
NodeInput::import(concrete!(BrushCache), 2),
1212+
],
1213+
implementation: DocumentNodeImplementation::ProtoNode(brush::brush::brush::IDENTIFIER),
1214+
..Default::default()
1215+
}]
1216+
.into_iter()
1217+
.enumerate()
1218+
.map(|(id, node)| (NodeId(id as u64), node))
1219+
.collect(),
1220+
..Default::default()
1221+
}),
12481222
inputs: vec![
12491223
NodeInput::value(TaggedValue::Raster(Default::default()), true),
12501224
NodeInput::value(TaggedValue::BrushStrokes(Vec::new()), false),

editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use super::DocumentNodeDefinition;
22
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
3-
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, InputMetadata, NodeTemplate, WidgetOverride};
3+
use crate::messages::portfolio::document::utility_types::network_interface::{
4+
DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata, WidgetOverride,
5+
};
6+
use graph_craft::ProtoNodeIdentifier;
7+
use graph_craft::document::value::TaggedValue;
48
use graph_craft::document::*;
59
use graphene_std::registry::*;
610
use graphene_std::*;
@@ -36,7 +40,7 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
3640
description,
3741
properties,
3842
context_features,
39-
output_fields: _,
43+
output_fields,
4044
} = metadata;
4145

4246
let Some(implementations) = &node_registry.get(id) else { continue };
@@ -72,6 +76,12 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
7276
RegistryWidgetOverride::Custom(str) => InputMetadata::with_name_description_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
7377
})
7478
.collect(),
79+
output_names: if output_fields.is_empty() {
80+
Vec::new()
81+
} else {
82+
// A leading field with empty node_path is a hidden primary output placeholder, included as ""
83+
output_fields.iter().map(|field| field.name.to_string()).collect()
84+
},
7585
locked: false,
7686
..Default::default()
7787
},
@@ -82,6 +92,120 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
8292
},
8393
);
8494
}
95+
drop(node_registry);
96+
97+
// For nodes with destructured outputs, build a Network implementation so the UI knows about
98+
// the multiple output connectors. The network contains the source proto node plus one extractor
99+
// proto node per output field, wired directly (no identity pass-through or memo nodes).
100+
let destructured_info = {
101+
let node_metadata_registry = NODE_METADATA.lock().unwrap();
102+
let node_registry = NODE_REGISTRY.lock().unwrap();
103+
definitions_map
104+
.iter()
105+
.filter_map(|(_, def)| {
106+
let DocumentNodeImplementation::ProtoNode(id) = &def.node_template.document_node.implementation else {
107+
return None;
108+
};
109+
let meta = node_metadata_registry.get(id)?;
110+
if meta.output_fields.is_empty() {
111+
return None;
112+
}
113+
114+
let has_hidden_primary = meta.output_fields.first().is_some_and(|f| f.node_path.is_empty());
115+
let real_fields = if has_hidden_primary { &meta.output_fields[1..] } else { meta.output_fields };
116+
117+
let extractors: Vec<_> = real_fields
118+
.iter()
119+
.filter_map(|field| {
120+
let extractor_id = ProtoNodeIdentifier::with_owned_string(field.node_path.to_string());
121+
let call_arg = node_registry.get(&extractor_id).and_then(|impls| impls.first().map(|(_, node_io)| node_io.call_argument.clone()))?;
122+
Some((extractor_id, call_arg))
123+
})
124+
.collect();
125+
126+
if extractors.is_empty() {
127+
return None;
128+
}
129+
130+
Some((id.clone(), has_hidden_primary, extractors))
131+
})
132+
.collect::<Vec<_>>()
133+
};
134+
135+
// Build the definition-time network for each destructured node
136+
for (id, has_hidden_primary, extractors) in &destructured_info {
137+
let identifier = DefinitionIdentifier::ProtoNode(id.clone());
138+
let Some(definition) = definitions_map.get_mut(&identifier) else { continue };
139+
140+
// The source node runs the original proto node (e.g. split_vec2)
141+
let source_node_id = NodeId(0);
142+
let source_node = DocumentNode {
143+
inputs: definition
144+
.node_template
145+
.document_node
146+
.inputs
147+
.iter()
148+
.enumerate()
149+
.map(|(i, input)| NodeInput::import(input.ty(), i))
150+
.collect(),
151+
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
152+
call_argument: definition.node_template.document_node.call_argument.clone(),
153+
..Default::default()
154+
};
155+
156+
// One extractor node per output field, each taking input from the source node
157+
let mut inner_nodes: Vec<(NodeId, DocumentNode)> = vec![(source_node_id, source_node)];
158+
159+
let mut exports = if *has_hidden_primary { vec![NodeInput::value(TaggedValue::None, false)] } else { Vec::new() };
160+
161+
for (i, (extractor_id, call_arg)) in extractors.iter().enumerate() {
162+
let extractor_node_id = NodeId((i + 1) as u64);
163+
inner_nodes.push((
164+
extractor_node_id,
165+
DocumentNode {
166+
inputs: vec![NodeInput::node(source_node_id, 0)],
167+
implementation: DocumentNodeImplementation::ProtoNode(extractor_id.clone()),
168+
call_argument: call_arg.clone(),
169+
..Default::default()
170+
},
171+
));
172+
exports.push(NodeInput::node(extractor_node_id, 0));
173+
}
174+
175+
// Generate network metadata for each inner node
176+
let node_metadata: HashMap<_, _> = inner_nodes
177+
.iter()
178+
.map(|(nid, _)| {
179+
(
180+
*nid,
181+
DocumentNodeMetadata {
182+
persistent_metadata: DocumentNodePersistentMetadata {
183+
node_type_metadata: NodeTypePersistentMetadata::node(glam::IVec2::ZERO),
184+
..Default::default()
185+
},
186+
..Default::default()
187+
},
188+
)
189+
})
190+
.collect();
191+
192+
definition.node_template.persistent_node_metadata.network_metadata = Some(NodeNetworkMetadata {
193+
persistent_metadata: NodeNetworkPersistentMetadata {
194+
reference: Some(definition.identifier.to_string()),
195+
node_metadata,
196+
..Default::default()
197+
},
198+
..Default::default()
199+
});
200+
definition.node_template.persistent_node_metadata.display_name = definition.identifier.to_string();
201+
202+
definition.node_template.document_node.implementation = DocumentNodeImplementation::Network(NodeNetwork {
203+
exports,
204+
nodes: inner_nodes.into_iter().collect(),
205+
scope_injections: Default::default(),
206+
generated: false,
207+
});
208+
}
85209

86210
// If any protonode does not have metadata then set its display name to its identifier string
87211
for definition in definitions_map.values_mut() {
@@ -93,6 +217,17 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
93217
}
94218
}
95219

220+
// Fill in inner proto node metadata for the destructured output network definitions
221+
for (id, _, _) in destructured_info {
222+
let identifier = DefinitionIdentifier::ProtoNode(id);
223+
// We need to split the borrow: extract the node and metadata, traverse, then put metadata back
224+
let Some(definition) = definitions_map.get(&identifier) else { continue };
225+
let document_node = definition.node_template.document_node.clone();
226+
let mut persistent_metadata = definition.node_template.persistent_node_metadata.clone();
227+
traverse_node(&document_node, &mut persistent_metadata, &definitions_map);
228+
definitions_map.get_mut(&identifier).unwrap().node_template.persistent_node_metadata = persistent_metadata;
229+
}
230+
96231
// Add the rest of the network nodes to the map and add the metadata for their internal protonodes
97232
for mut network_node in network_nodes {
98233
traverse_node(&network_node.node_template.document_node, &mut network_node.node_template.persistent_node_metadata, &definitions_map);

editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
405405
{
406406
return;
407407
};
408-
if let Some(DocumentNodeImplementation::Network(_)) = network_interface.implementation(&node_id, selection_network_path) {
408+
if let Some(DocumentNodeImplementation::Network(network)) = network_interface.implementation(&node_id, selection_network_path)
409+
&& !network.generated
410+
{
409411
responses.add(DocumentMessage::EnterNestedNetwork { node_id });
410412
}
411413
}

editor/src/node_graph_executor/runtime.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,14 @@ impl NodeRuntime {
140140
node_graph_errors: Vec::new(),
141141
monitor_nodes: Vec::new(),
142142

143-
substitutions: preprocessor::generate_node_substitutions(),
143+
substitutions: {
144+
// Exclude nodes with destructured outputs since their definition networks already contain the complete structure
145+
let mut substitutions = preprocessor::generate_node_substitutions();
146+
let node_metadata = graphene_std::registry::NODE_METADATA.lock().unwrap();
147+
substitutions.retain(|id, _| node_metadata.get(id).is_none_or(|meta| meta.output_fields.is_empty()));
148+
drop(node_metadata);
149+
substitutions
150+
},
144151

145152
thumbnail_renders: Default::default(),
146153
vector_modify: Default::default(),

node-graph/node-macro/src/destruct.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ pub fn derive(input: DeriveInput) -> syn::Result<TokenStream2> {
3131
};
3232

3333
let ty = field.ty;
34+
35+
// A `()` field serves as a hidden primary output placeholder (analogous to `_: ()` for inputs)
36+
if is_unit_type(&ty) {
37+
output_fields.push(quote! {
38+
#core_types::registry::StructField {
39+
name: "",
40+
node_path: "",
41+
ty: #core_types::concrete!(()),
42+
}
43+
});
44+
continue;
45+
}
46+
3447
let output_name = parse_output_name(&field.attrs)?.unwrap_or_else(|| field_name.to_string().to_case(Case::Title));
3548
let output_name_lit = LitStr::new(&output_name, field_name.span());
3649

@@ -70,6 +83,10 @@ fn generate_extractor_node(core_types: &TokenStream2, fn_name: &syn::Ident, stru
7083
}
7184
}
7285

86+
fn is_unit_type(ty: &Type) -> bool {
87+
matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())
88+
}
89+
7390
fn parse_output_name(attrs: &[syn::Attribute]) -> syn::Result<Option<String>> {
7491
let mut output_name = None;
7592

node-graph/nodes/gcore/src/extract_xy.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@ use core_types::Ctx;
22
use dyn_any::DynAny;
33
use glam::{DVec2, IVec2, UVec2};
44

5-
#[derive(Debug, Clone, DynAny, node_macro::Destruct)]
6-
pub struct SplitVec2Output {
7-
pub x: f64,
8-
pub y: f64,
9-
}
10-
5+
// TODO: Remove `extract_xy` and `XY` once a migration converts old documents' Split Vec2 network nodes (which used Extract XY internally) to the new proto node form
116
/// Obtains the X or Y component of a vec2.
127
///
138
/// The inverse of this node is "Vec2 Value", which can have either or both its X and Y parameters exposed as graph inputs.
@@ -19,10 +14,24 @@ fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2
1914
}
2015
}
2116

17+
#[derive(Debug, Clone, DynAny, node_macro::Destruct)]
18+
pub struct SplitVec2Output {
19+
pub primary: (),
20+
pub x: f64,
21+
pub y: f64,
22+
}
23+
24+
/// Decomposes the X and Y components of a vec2.
25+
///
26+
/// The inverse of this node is "Vec2 Value", which can have either or both its X and Y parameters exposed as graph inputs.
2227
#[node_macro::node(name("Split Vec2"), category("Math: Vector"), deconstruct_output)]
2328
fn split_vec2<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T) -> SplitVec2Output {
2429
let vector = vector.into();
25-
SplitVec2Output { x: vector.x, y: vector.y }
30+
SplitVec2Output {
31+
primary: (),
32+
x: vector.x,
33+
y: vector.y,
34+
}
2635
}
2736

2837
/// The X or Y component of a vec2.

node-graph/nodes/raster/src/adjustments.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use vector_types::GradientStops;
2121
#[cfg(feature = "std")]
2222
#[derive(Debug, Clone, dyn_any::DynAny, node_macro::Destruct)]
2323
pub struct SplitChannelsOutput {
24+
pub primary: (),
2425
pub red: Table<Raster<CPU>>,
2526
pub green: Table<Raster<CPU>>,
2627
pub blue: Table<Raster<CPU>>,
@@ -103,6 +104,7 @@ fn gamma_correction<T: Adjust<Color>>(
103104
input
104105
}
105106

107+
// TODO: Remove `extract_channel` and `RedGreenBlueAlpha` once a migration converts old documents' Split Channels network nodes (which used Extract Channel internally) to the new proto node form
106108
#[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))]
107109
fn extract_channel<T: Adjust<Color>>(
108110
_: impl Ctx,
@@ -131,6 +133,7 @@ fn extract_channel<T: Adjust<Color>>(
131133
#[node_macro::node(name("Split Channels"), category("Raster: Channels"), deconstruct_output)]
132134
fn split_channels(_: impl Ctx, input: Table<Raster<CPU>>) -> SplitChannelsOutput {
133135
SplitChannelsOutput {
136+
primary: (),
134137
red: extract_channel((), input.clone(), RedGreenBlueAlpha::Red),
135138
green: extract_channel((), input.clone(), RedGreenBlueAlpha::Green),
136139
blue: extract_channel((), input.clone(), RedGreenBlueAlpha::Blue),

0 commit comments

Comments
 (0)