11use super :: DocumentNodeDefinition ;
22use 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 ;
48use graph_craft:: document:: * ;
59use graphene_std:: registry:: * ;
610use 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) ;
0 commit comments