From 2e97e81290da3bc8ea0a388a20736a104b67eb22 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 14 Apr 2025 08:15:26 +0200 Subject: [PATCH 1/6] Add `Destruct` macro and trait for automatically acessing struct fields --- .../document_node_derive.rs | 1 + .../libraries/core-types/src/registry.rs | 21 +++++++ node-graph/node-macro/src/codegen.rs | 6 ++ node-graph/node-macro/src/destruct.rs | 62 +++++++++++++++++++ node-graph/node-macro/src/lib.rs | 14 +++++ node-graph/node-macro/src/parsing.rs | 23 ++++++- 6 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 node-graph/node-macro/src/destruct.rs diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs index 72a4558a67..80d9d02ed9 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs @@ -36,6 +36,7 @@ pub(super) fn post_process_nodes(custom: Vec) -> HashMap description, properties, context_features, + output_fields: _, } = metadata; let Some(implementations) = &node_registry.get(id) else { continue }; diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index a472ef45b2..2e7253a790 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -16,6 +16,7 @@ pub struct NodeMetadata { pub description: &'static str, pub properties: Option<&'static str>, pub context_features: Vec, + pub output_fields: &'static [StructField], } // Translation struct between macro and definition @@ -36,6 +37,26 @@ pub struct FieldMetadata { pub unit: Option<&'static str>, } +#[derive(Clone, Debug)] +pub struct StructField { + pub name: &'static str, + pub node_path: &'static str, + pub ty: Type, +} + +pub trait Destruct { + fn fields(&self) -> &'static [StructField]; +} + +impl Destruct for &T +where + T: Default, +{ + fn fields(&self) -> &'static [StructField] { + &[] + } +} + #[derive(Clone, Debug)] pub enum RegistryWidgetOverride { None, diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 8fbfe88815..8cc2d2fd26 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -402,6 +402,11 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None)); + let output_fields = match attributes.deconstruct_output { + false => quote!(&[]), + true => quote!(#output_type::fields), + }; + let cfg = crate::shader_nodes::modify_cfg(attributes); let node_input_accessor = generate_node_input_references(parsed, fn_generics, &field_idents, core_types, &identifier, &cfg); let ShaderTokens { shader_entry_point, gpu_node } = attributes.shader_node.as_ref().map(|n| n.codegen(crate_ident, parsed)).unwrap_or(Ok(ShaderTokens::default()))?; @@ -474,6 +479,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn description: #description, properties: #properties, context_features: vec![#(ContextFeature::#context_features,)*], + output_fields: #output_fields, fields: vec![ #( FieldMetadata { diff --git a/node-graph/node-macro/src/destruct.rs b/node-graph/node-macro/src/destruct.rs new file mode 100644 index 0000000000..b787c6069e --- /dev/null +++ b/node-graph/node-macro/src/destruct.rs @@ -0,0 +1,62 @@ +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use syn::{Error, Ident, spanned::Spanned}; + +pub fn derive(struct_name: Ident, data: syn::Data) -> syn::Result { + let syn::Data::Struct(data_struct) = data else { + return Err(Error::new(proc_macro2::Span::call_site(), String::from("Deriving `Destruct` is currently only supported for structs"))); + }; + + let found_crate = proc_macro_crate::crate_name("graphene-core").map_err(|e| { + Error::new( + proc_macro2::Span::call_site(), + format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e), + ) + })?; + + let crate_name = match found_crate { + proc_macro_crate::FoundCrate::Itself => quote!(crate), + proc_macro_crate::FoundCrate::Name(name) => { + let ident = format_ident!("{}", name); + quote!(#ident) + } + }; + + let path = quote!(std::module_path!().rsplit_once("::").unwrap().0); + + let mut node_implementations = Vec::with_capacity(data_struct.fields.len()); + let mut field_structs = Vec::with_capacity(data_struct.fields.len()); + + for field in data_struct.fields { + let Some(field_name) = field.ident else { + return Err(Error::new(field.span(), String::from("Destruct cant be used on tuple structs"))); + }; + let ty = field.ty; + let fn_name = quote::format_ident!("extract_ {field_name}"); + node_implementations.push(quote! { + #[node_macro(category(""))] + fn #fn_name(_: impl Ctx, data: #struct_name) -> #ty { + data.#field_name + } + }); + + field_structs.push(quote! { + #crate_name::registry::FieldStruct { + name: stringify!(#field_name), + node_path: concat!() + + } + }) + } + + Ok(quote! { + impl graphene_core::registry::Destruct for #struct_name { + fn fields() -> &[graphene_core::registry::FieldStruct] { + &[ + + ] + } + } + + }) +} diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 35fe604a01..b2a55e13b8 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -7,6 +7,7 @@ mod buffer_struct; mod codegen; mod crate_ident; mod derive_choice_type; +mod destruct; mod parsing; mod shader_nodes; mod validation; @@ -39,3 +40,16 @@ pub fn derive_buffer_struct(input_item: TokenStream) -> TokenStream { let crate_ident = CrateIdent::default(); TokenStream::from(buffer_struct::derive_buffer_struct(&crate_ident, input_item).unwrap_or_else(|err| err.to_compile_error())) } + +#[proc_macro_error] +#[proc_macro_derive(Destruct)] +/// Derives the `Destruct` trait for structs and creates accessor node implementations. +pub fn derive_destruct(item: TokenStream) -> TokenStream { + let s = syn::parse_macro_input!(item as syn::DeriveInput); + let parse_result = destruct::derive(s.ident, s.data).into(); + let Ok(parsed_node) = parse_result else { + let e = parse_result.unwrap_err(); + return syn::Error::new(e.span(), format!("Failed to parse node function: {e}")).to_compile_error().into(); + }; + parsed_node.into() +} diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 0a8367f3af..01df7446da 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -52,6 +52,7 @@ pub(crate) struct NodeFnAttributes { pub(crate) shader_node: Option, /// Custom serialization function path (e.g., "my_module::custom_serialize") pub(crate) serialize: Option, + pub(crate) deconstruct_output: bool, // Add more attributes as needed } @@ -201,6 +202,7 @@ impl Parse for NodeFnAttributes { let mut display_name = None; let mut path = None; let mut skip_impl = false; + let mut deconstruct_output = false; let mut properties_string = None; let mut cfg = None; let mut shader_node = None; @@ -271,6 +273,17 @@ impl Parse for NodeFnAttributes { } skip_impl = true; } + // Indicator that the node output should be deconstructed into its fields. + // + // Example usage: + // #[node_macro::node(..., deconstruct_output, ...)] + "deconstruct_output" => { + let path = meta.require_path_only()?; + if deconstruct_output { + return Err(Error::new_spanned(path, "Multiple 'deconstruct_output' attributes are not allowed")); + } + deconstruct_output = true; + } // Override UI layout generator function name defined in `node_properties.rs` that returns a custom Properties panel layout for this node. // This is used to create custom UI for the input parameters of the node in cases where the defaults generated from the type and attributes are insufficient. // @@ -329,7 +342,7 @@ impl Parse for NodeFnAttributes { indoc!( r#" Unsupported attribute in `node`. - Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', and 'serialize'. + Supported attributes are 'category', 'name', 'path', 'skip_impl', 'deconstruct_output', 'properties', 'cfg', 'shader_node', and 'serialize'. Example usage: #[node_macro::node(..., name("Test Node"), ...)] "# @@ -361,6 +374,7 @@ impl Parse for NodeFnAttributes { cfg, shader_node, serialize, + deconstruct_output, }) } } @@ -934,6 +948,7 @@ mod tests { cfg: None, shader_node: None, serialize: None, + deconstruct_output: false, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1002,6 +1017,7 @@ mod tests { cfg: None, shader_node: None, serialize: None, + deconstruct_output: false, }, fn_name: Ident::new("transform", Span::call_site()), struct_name: Ident::new("Transform", Span::call_site()), @@ -1084,6 +1100,7 @@ mod tests { cfg: None, shader_node: None, serialize: None, + deconstruct_output: false, }, fn_name: Ident::new("circle", Span::call_site()), struct_name: Ident::new("Circle", Span::call_site()), @@ -1148,6 +1165,7 @@ mod tests { cfg: None, shader_node: None, serialize: None, + deconstruct_output: false, }, fn_name: Ident::new("levels", Span::call_site()), struct_name: Ident::new("Levels", Span::call_site()), @@ -1224,6 +1242,7 @@ mod tests { cfg: None, shader_node: None, serialize: None, + deconstruct_output: false, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1288,6 +1307,7 @@ mod tests { cfg: None, shader_node: None, serialize: None, + deconstruct_output: false, }, fn_name: Ident::new("load_image", Span::call_site()), struct_name: Ident::new("LoadImage", Span::call_site()), @@ -1352,6 +1372,7 @@ mod tests { cfg: None, shader_node: None, serialize: None, + deconstruct_output: false, }, fn_name: Ident::new("custom_node", Span::call_site()), struct_name: Ident::new("CustomNode", Span::call_site()), From e21592764dfeeaba2bc1bfe3a9cf94b9e940c135 Mon Sep 17 00:00:00 2001 From: JustJ01 Date: Mon, 23 Feb 2026 02:25:14 +0530 Subject: [PATCH 2/6] Complete node output destructuring implementation --- .../libraries/core-types/src/registry.rs | 9 +- node-graph/node-macro/src/codegen.rs | 52 +++---- node-graph/node-macro/src/destruct.rs | 131 +++++++++++++----- node-graph/node-macro/src/lib.rs | 12 +- node-graph/node-macro/src/parsing.rs | 7 +- 5 files changed, 133 insertions(+), 78 deletions(-) diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 2e7253a790..7cc6d8e135 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -45,14 +45,7 @@ pub struct StructField { } pub trait Destruct { - fn fields(&self) -> &'static [StructField]; -} - -impl Destruct for &T -where - T: Default, -{ - fn fields(&self) -> &'static [StructField] { + fn fields() -> &'static [StructField] { &[] } } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 8cc2d2fd26..3ddd8c5080 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -403,8 +403,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None)); let output_fields = match attributes.deconstruct_output { - false => quote!(&[]), - true => quote!(#output_type::fields), + false => quote!(&[] as &[#graphene_core::registry::StructField]), + true => quote!(<#output_type as #graphene_core::registry::Destruct>::fields()), }; let cfg = crate::shader_nodes::modify_cfg(attributes); @@ -538,28 +538,27 @@ fn generate_node_input_references( let struct_name = format_ident!("{}Input", input_ident.ident.to_string().to_case(Case::Pascal)); let (fn_generic_params, phantom_data_declerations) = generate_phantom_data(used.iter()); - // Only create structs with phantom data where necessary. - generated_input_accessor.push(if phantom_data_declerations.is_empty() { - quote! { - pub struct #struct_name; - } - } else { - quote! { - pub struct #struct_name <#(#used),*>{ - #(#phantom_data_declerations,)* - } + // Only create structs with phantom data where necessary. + generated_input_accessor.push(if phantom_data_declerations.is_empty() { + quote! { + pub struct #struct_name; + } + } else { + quote! { + pub struct #struct_name <#(#used),*>{ + #(#phantom_data_declerations,)* } - }); - generated_input_accessor.push(quote! { - impl <#(#used),*> #core_types::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> { - const INDEX: usize = #input_index; - fn identifier() -> #core_types::ProtoNodeIdentifier { - #inputs_module_name::IDENTIFIER.clone() - } - type Result = #ty; + } + }); + generated_input_accessor.push(quote! { + impl <#(#used),*> #graphene_core::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> { + const INDEX: usize = #input_index; + fn identifier() -> &'static str { + protonode_identifier() } - }) - } + type Result = #ty; + } + }) } quote! { @@ -567,8 +566,13 @@ fn generate_node_input_references( pub mod #inputs_module_name { use super::*; - /// The `ProtoNodeIdentifier` of this node without any generics attached to it - pub const IDENTIFIER: #core_types::ProtoNodeIdentifier = #identifier(); + pub fn protonode_identifier() -> &'static str { + static NODE_NAME: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new(); + NODE_NAME.get_or_init(|| { + let name = #identifier; + Box::leak(name.into_boxed_str()) + }) + } #(#generated_input_accessor)* } } diff --git a/node-graph/node-macro/src/destruct.rs b/node-graph/node-macro/src/destruct.rs index b787c6069e..a03adf541e 100644 --- a/node-graph/node-macro/src/destruct.rs +++ b/node-graph/node-macro/src/destruct.rs @@ -1,62 +1,127 @@ +use convert_case::{Case, Casing}; +use proc_macro_crate::FoundCrate; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote}; -use syn::{Error, Ident, spanned::Spanned}; +use syn::{Data, DeriveInput, Error, LitStr, Meta, Type, spanned::Spanned}; -pub fn derive(struct_name: Ident, data: syn::Data) -> syn::Result { - let syn::Data::Struct(data_struct) = data else { - return Err(Error::new(proc_macro2::Span::call_site(), String::from("Deriving `Destruct` is currently only supported for structs"))); +pub fn derive(input: DeriveInput) -> syn::Result { + let struct_name = input.ident; + let generics = input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let Data::Struct(data_struct) = input.data else { + return Err(Error::new( + Span::call_site(), + "Deriving `Destruct` is currently only supported for structs", + )); }; - let found_crate = proc_macro_crate::crate_name("graphene-core").map_err(|e| { + let graphene_core = match proc_macro_crate::crate_name("graphene-core").map_err(|e| { Error::new( - proc_macro2::Span::call_site(), - format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e), + Span::call_site(), + format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {e}"), ) - })?; - - let crate_name = match found_crate { - proc_macro_crate::FoundCrate::Itself => quote!(crate), - proc_macro_crate::FoundCrate::Name(name) => { - let ident = format_ident!("{}", name); + })? { + FoundCrate::Itself => quote!(crate), + FoundCrate::Name(name) => { + let ident = syn::Ident::new(&name, Span::call_site()); quote!(#ident) } }; - let path = quote!(std::module_path!().rsplit_once("::").unwrap().0); - let mut node_implementations = Vec::with_capacity(data_struct.fields.len()); - let mut field_structs = Vec::with_capacity(data_struct.fields.len()); + let mut output_fields = Vec::with_capacity(data_struct.fields.len()); for field in data_struct.fields { let Some(field_name) = field.ident else { - return Err(Error::new(field.span(), String::from("Destruct cant be used on tuple structs"))); + return Err(Error::new(field.span(), "Destruct cannot be used on tuple structs")); }; + let ty = field.ty; - let fn_name = quote::format_ident!("extract_ {field_name}"); - node_implementations.push(quote! { - #[node_macro(category(""))] - fn #fn_name(_: impl Ctx, data: #struct_name) -> #ty { - data.#field_name - } - }); + let output_name = parse_output_name(&field.attrs)?.unwrap_or_else(|| field_name.to_string().to_case(Case::Title)); + let output_name_lit = LitStr::new(&output_name, field_name.span()); - field_structs.push(quote! { - #crate_name::registry::FieldStruct { - name: stringify!(#field_name), - node_path: concat!() + let fn_name = format_ident!("extract_{}_{}", struct_name.to_string().to_case(Case::Snake), field_name); + let node_struct_name = format_ident!("{}Node", fn_name.to_string().to_case(Case::Pascal)); + node_implementations.push(generate_extractor_node(&graphene_core, &fn_name, &struct_name, &field_name, &ty, &output_name_lit)); + output_fields.push(quote! { + #graphene_core::registry::StructField { + name: #output_name_lit, + node_path: concat!(std::module_path!().rsplit_once("::").unwrap().0, "::", stringify!(#node_struct_name)), + ty: #graphene_core::concrete!(#ty), } - }) + }); } Ok(quote! { - impl graphene_core::registry::Destruct for #struct_name { - fn fields() -> &[graphene_core::registry::FieldStruct] { - &[ + #(#node_implementations)* + impl #impl_generics #graphene_core::registry::Destruct for #struct_name #ty_generics #where_clause { + fn fields() -> &'static [#graphene_core::registry::StructField] { + &[ + #(#output_fields,)* ] } } - }) } + +fn generate_extractor_node( + graphene_core: &TokenStream2, + fn_name: &syn::Ident, + struct_name: &syn::Ident, + field_name: &syn::Ident, + ty: &Type, + output_name: &LitStr, +) -> TokenStream2 { + quote! { + #[node_macro::node(category(""), name(#output_name))] + fn #fn_name(_: impl #graphene_core::Ctx, data: #struct_name) -> #ty { + data.#field_name + } + } +} + +fn parse_output_name(attrs: &[syn::Attribute]) -> syn::Result> { + let mut output_name = None; + + for attr in attrs { + if !attr.path().is_ident("output") { + continue; + } + + let mut this_output_name = None; + match &attr.meta { + Meta::Path(_) => { + return Err(Error::new_spanned(attr, "Expected output metadata like #[output(name = \"Result\")]")); + } + Meta::NameValue(_) => { + return Err(Error::new_spanned(attr, "Expected output metadata like #[output(name = \"Result\")]")); + } + Meta::List(_) => { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("name") { + if this_output_name.is_some() { + return Err(meta.error("Multiple output names provided for one field")); + } + let value = meta.value()?; + let lit: LitStr = value.parse()?; + this_output_name = Some(lit.value()); + Ok(()) + } else { + Err(meta.error("Unsupported output metadata. Supported syntax is #[output(name = \"...\")]")) + } + })?; + } + } + + let this_output_name = this_output_name.ok_or_else(|| Error::new_spanned(attr, "Missing output name. Use #[output(name = \"...\")]"))?; + if output_name.is_some() { + return Err(Error::new_spanned(attr, "Multiple #[output(...)] attributes are not allowed on one field")); + } + output_name = Some(this_output_name); + } + + Ok(output_name) +} diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index b2a55e13b8..3116f1bc4b 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -45,11 +45,9 @@ pub fn derive_buffer_struct(input_item: TokenStream) -> TokenStream { #[proc_macro_derive(Destruct)] /// Derives the `Destruct` trait for structs and creates accessor node implementations. pub fn derive_destruct(item: TokenStream) -> TokenStream { - let s = syn::parse_macro_input!(item as syn::DeriveInput); - let parse_result = destruct::derive(s.ident, s.data).into(); - let Ok(parsed_node) = parse_result else { - let e = parse_result.unwrap_err(); - return syn::Error::new(e.span(), format!("Failed to parse node function: {e}")).to_compile_error().into(); - }; - parsed_node.into() + let input = syn::parse_macro_input!(item as syn::DeriveInput); + match destruct::derive(input) { + Ok(tokens) => tokens.into(), + Err(error) => syn::Error::new(error.span(), format!("Failed to derive Destruct: {error}")).to_compile_error().into(), + } } diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 01df7446da..de84627266 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -273,12 +273,7 @@ impl Parse for NodeFnAttributes { } skip_impl = true; } - // Indicator that the node output should be deconstructed into its fields. - // - // Example usage: - // #[node_macro::node(..., deconstruct_output, ...)] - "deconstruct_output" => { - let path = meta.require_path_only()?; + Meta::Path(path) if path.is_ident("deconstruct_output") => { if deconstruct_output { return Err(Error::new_spanned(path, "Multiple 'deconstruct_output' attributes are not allowed")); } From f05970c703c226f3a80dc8ba97c307c9a6f1e2a2 Mon Sep 17 00:00:00 2001 From: JustJ01 Date: Mon, 23 Feb 2026 06:12:13 +0530 Subject: [PATCH 3/6] implemenet preprocessing expansion --- node-graph/node-macro/src/codegen.rs | 52 ++++++++--------- node-graph/node-macro/src/parsing.rs | 3 +- node-graph/preprocessor/src/lib.rs | 85 +++++++++++++++++++++++++--- 3 files changed, 105 insertions(+), 35 deletions(-) diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 3ddd8c5080..89b92e78d4 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -403,8 +403,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None)); let output_fields = match attributes.deconstruct_output { - false => quote!(&[] as &[#graphene_core::registry::StructField]), - true => quote!(<#output_type as #graphene_core::registry::Destruct>::fields()), + false => quote!(&[] as &[#core_types::registry::StructField]), + true => quote!(<#output_type as #core_types::registry::Destruct>::fields()), }; let cfg = crate::shader_nodes::modify_cfg(attributes); @@ -538,40 +538,38 @@ fn generate_node_input_references( let struct_name = format_ident!("{}Input", input_ident.ident.to_string().to_case(Case::Pascal)); let (fn_generic_params, phantom_data_declerations) = generate_phantom_data(used.iter()); - // Only create structs with phantom data where necessary. - generated_input_accessor.push(if phantom_data_declerations.is_empty() { - quote! { - pub struct #struct_name; - } - } else { - quote! { - pub struct #struct_name <#(#used),*>{ - #(#phantom_data_declerations,)* + // Only create structs with phantom data where necessary. + generated_input_accessor.push(if phantom_data_declerations.is_empty() { + quote! { + pub struct #struct_name; } - } - }); - generated_input_accessor.push(quote! { - impl <#(#used),*> #graphene_core::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> { - const INDEX: usize = #input_index; - fn identifier() -> &'static str { - protonode_identifier() + } else { + quote! { + pub struct #struct_name <#(#used),*>{ + #(#phantom_data_declerations,)* + } } - type Result = #ty; - } - }) + }); + generated_input_accessor.push(quote! { + impl <#(#used),*> #core_types::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> { + const INDEX: usize = #input_index; + fn identifier() -> #core_types::ProtoNodeIdentifier { + protonode_identifier() + } + type Result = #ty; + } + }); + } } quote! { #cfg pub mod #inputs_module_name { use super::*; + pub const IDENTIFIER: #core_types::ProtoNodeIdentifier = #identifier(); - pub fn protonode_identifier() -> &'static str { - static NODE_NAME: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new(); - NODE_NAME.get_or_init(|| { - let name = #identifier; - Box::leak(name.into_boxed_str()) - }) + pub fn protonode_identifier() -> #core_types::ProtoNodeIdentifier { + IDENTIFIER } #(#generated_input_accessor)* } diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index de84627266..460c159d66 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -273,7 +273,8 @@ impl Parse for NodeFnAttributes { } skip_impl = true; } - Meta::Path(path) if path.is_ident("deconstruct_output") => { + "deconstruct_output" => { + let path = meta.require_path_only()?; if deconstruct_output { return Err(Error::new_spanned(path, "Multiple 'deconstruct_output' attributes are not allowed")); } diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index 0f9bf398e4..2b476b707d 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -35,7 +35,7 @@ pub fn generate_node_substitutions() -> HashMap = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect(); let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() }); @@ -55,6 +55,7 @@ pub fn generate_node_substitutions() -> HashMap = node_io_types @@ -103,10 +104,27 @@ pub fn generate_node_substitutions() -> HashMap = output_fields + .iter() + .filter_map(|field| { + let identifier = ProtoNodeIdentifier::with_owned_string(field.node_path.to_string()); + if node_registry.contains_key(&identifier) { + Some((field, identifier)) + } else { + warn!("Failed to find output field extractor node '{}' for '{}'", field.node_path, id.as_str()); + None + } + }) + .collect(); + + if generated_nodes == 0 && available_output_fields.is_empty() { continue; } + let source_node_id = NodeId(input_count as u64); + let mut generated_node_count = 1; + let mut output_source_node = source_node_id; + let document_node = DocumentNode { inputs: network_inputs, call_argument: input_type.clone(), @@ -117,16 +135,69 @@ pub fn generate_node_substitutions() -> HashMap Date: Mon, 23 Feb 2026 17:35:22 +0530 Subject: [PATCH 4/6] formatted files --- node-graph/node-macro/src/destruct.rs | 23 +++++------------------ node-graph/preprocessor/src/lib.rs | 9 ++++++--- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/node-graph/node-macro/src/destruct.rs b/node-graph/node-macro/src/destruct.rs index a03adf541e..7bbae9a969 100644 --- a/node-graph/node-macro/src/destruct.rs +++ b/node-graph/node-macro/src/destruct.rs @@ -10,18 +10,12 @@ pub fn derive(input: DeriveInput) -> syn::Result { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let Data::Struct(data_struct) = input.data else { - return Err(Error::new( - Span::call_site(), - "Deriving `Destruct` is currently only supported for structs", - )); + return Err(Error::new(Span::call_site(), "Deriving `Destruct` is currently only supported for structs")); }; - let graphene_core = match proc_macro_crate::crate_name("graphene-core").map_err(|e| { - Error::new( - Span::call_site(), - format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {e}"), - ) - })? { + let graphene_core = match proc_macro_crate::crate_name("graphene-core") + .map_err(|e| Error::new(Span::call_site(), format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {e}")))? + { FoundCrate::Itself => quote!(crate), FoundCrate::Name(name) => { let ident = syn::Ident::new(&name, Span::call_site()); @@ -67,14 +61,7 @@ pub fn derive(input: DeriveInput) -> syn::Result { }) } -fn generate_extractor_node( - graphene_core: &TokenStream2, - fn_name: &syn::Ident, - struct_name: &syn::Ident, - field_name: &syn::Ident, - ty: &Type, - output_name: &LitStr, -) -> TokenStream2 { +fn generate_extractor_node(graphene_core: &TokenStream2, fn_name: &syn::Ident, struct_name: &syn::Ident, field_name: &syn::Ident, ty: &Type, output_name: &LitStr) -> TokenStream2 { quote! { #[node_macro::node(category(""), name(#output_name))] fn #fn_name(_: impl #graphene_core::Ctx, data: #struct_name) -> #ty { diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index 2b476b707d..8934205c21 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -137,9 +137,12 @@ pub fn generate_node_substitutions() -> HashMap Date: Mon, 23 Feb 2026 23:32:25 +0530 Subject: [PATCH 5/6] convert Split Vec2 and Split Channels to proto nodes --- .../node_graph/document_node_definitions.rs | 156 ++---------------- node-graph/node-macro/src/destruct.rs | 25 +-- node-graph/node-macro/src/lib.rs | 2 +- node-graph/nodes/gcore/src/extract_xy.rs | 14 ++ node-graph/nodes/raster/src/adjustments.rs | 24 +++ 5 files changed, 62 insertions(+), 159 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index ed7f440839..53dd534503 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -17,8 +17,7 @@ use graph_craft::concrete; use graph_craft::document::value::*; use graph_craft::document::*; use graphene_std::brush::brush_cache::BrushCache; -use graphene_std::extract_xy::XY; -use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha}; +use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType}; use graphene_std::raster_types::{CPU, Raster}; use graphene_std::table::Table; #[allow(unused_imports)] @@ -1201,104 +1200,15 @@ fn document_node_definitions() -> HashMap>), 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(concrete!(Table>), 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(concrete!(Table>), 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(concrete!(Table>), 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() - }), + implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::split_channels::IDENTIFIER), inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)], + call_argument: generic!(T), ..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() - }), + output_names: vec!["Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()], + network_metadata: None, ..Default::default() }, }, @@ -1309,62 +1219,16 @@ fn document_node_definitions() -> HashMap syn::Result { return Err(Error::new(Span::call_site(), "Deriving `Destruct` is currently only supported for structs")); }; - let graphene_core = match proc_macro_crate::crate_name("graphene-core") - .map_err(|e| Error::new(Span::call_site(), format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {e}")))? + let core_types = match proc_macro_crate::crate_name("core-types") + .map_err(|e| Error::new(Span::call_site(), format!("Failed to find location of core_types. Make sure it is imported as a dependency: {e}")))? { FoundCrate::Itself => quote!(crate), FoundCrate::Name(name) => { @@ -38,12 +38,12 @@ pub fn derive(input: DeriveInput) -> syn::Result { let fn_name = format_ident!("extract_{}_{}", struct_name.to_string().to_case(Case::Snake), field_name); let node_struct_name = format_ident!("{}Node", fn_name.to_string().to_case(Case::Pascal)); - node_implementations.push(generate_extractor_node(&graphene_core, &fn_name, &struct_name, &field_name, &ty, &output_name_lit)); + node_implementations.push(generate_extractor_node(&core_types, &fn_name, &struct_name, &field_name, &ty, &output_name_lit)); output_fields.push(quote! { - #graphene_core::registry::StructField { + #core_types::registry::StructField { name: #output_name_lit, - node_path: concat!(std::module_path!().rsplit_once("::").unwrap().0, "::", stringify!(#node_struct_name)), - ty: #graphene_core::concrete!(#ty), + node_path: concat!(std::module_path!(), "::", stringify!(#node_struct_name)), + ty: #core_types::concrete!(#ty), } }); } @@ -51,20 +51,21 @@ pub fn derive(input: DeriveInput) -> syn::Result { Ok(quote! { #(#node_implementations)* - impl #impl_generics #graphene_core::registry::Destruct for #struct_name #ty_generics #where_clause { - fn fields() -> &'static [#graphene_core::registry::StructField] { - &[ + impl #impl_generics #core_types::registry::Destruct for #struct_name #ty_generics #where_clause { + fn fields() -> &'static [#core_types::registry::StructField] { + static FIELDS: std::sync::OnceLock> = std::sync::OnceLock::new(); + FIELDS.get_or_init(|| vec![ #(#output_fields,)* - ] + ]).as_slice() } } }) } -fn generate_extractor_node(graphene_core: &TokenStream2, fn_name: &syn::Ident, struct_name: &syn::Ident, field_name: &syn::Ident, ty: &Type, output_name: &LitStr) -> TokenStream2 { +fn generate_extractor_node(core_types: &TokenStream2, fn_name: &syn::Ident, struct_name: &syn::Ident, field_name: &syn::Ident, ty: &Type, output_name: &LitStr) -> TokenStream2 { quote! { #[node_macro::node(category(""), name(#output_name))] - fn #fn_name(_: impl #graphene_core::Ctx, data: #struct_name) -> #ty { + fn #fn_name(_: impl #core_types::Ctx, data: #struct_name) -> #ty { data.#field_name } } diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 3116f1bc4b..8751ce2da7 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -42,7 +42,7 @@ pub fn derive_buffer_struct(input_item: TokenStream) -> TokenStream { } #[proc_macro_error] -#[proc_macro_derive(Destruct)] +#[proc_macro_derive(Destruct, attributes(output))] /// Derives the `Destruct` trait for structs and creates accessor node implementations. pub fn derive_destruct(item: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(item as syn::DeriveInput); diff --git a/node-graph/nodes/gcore/src/extract_xy.rs b/node-graph/nodes/gcore/src/extract_xy.rs index 63d4922ce0..d59b64687f 100644 --- a/node-graph/nodes/gcore/src/extract_xy.rs +++ b/node-graph/nodes/gcore/src/extract_xy.rs @@ -2,6 +2,14 @@ use core_types::Ctx; use dyn_any::DynAny; use glam::{DVec2, IVec2, UVec2}; +#[derive(Debug, Clone, DynAny, node_macro::Destruct)] +pub struct SplitVec2Output { + #[output(name = "X")] + pub x: f64, + #[output(name = "Y")] + pub y: f64, +} + /// Obtains the X or Y component of a vec2. /// /// The inverse of this node is "Vec2 Value", which can have either or both its X and Y parameters exposed as graph inputs. @@ -13,6 +21,12 @@ fn extract_xy>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2 } } +#[node_macro::node(name("Split Vec2"), category("Math: Vector"), deconstruct_output)] +fn split_vec2>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T) -> SplitVec2Output { + let vector = vector.into(); + SplitVec2Output { x: vector.x, y: vector.y } +} + /// The X or Y component of a vec2. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)] #[widget(Radio)] diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index 0b3a0b0af8..bc9d988083 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -18,6 +18,19 @@ use raster_types::{CPU, Raster}; #[cfg(feature = "std")] use vector_types::GradientStops; +#[cfg(feature = "std")] +#[derive(Debug, Clone, dyn_any::DynAny, node_macro::Destruct)] +pub struct SplitChannelsOutput { + #[output(name = "Red")] + pub red: Table>, + #[output(name = "Green")] + pub green: Table>, + #[output(name = "Blue")] + pub blue: Table>, + #[output(name = "Alpha")] + pub alpha: Table>, +} + // TODO: Implement the following: // Color Balance // Aims for interoperable compatibility with: @@ -120,6 +133,17 @@ fn extract_channel>( input } +#[cfg(feature = "std")] +#[node_macro::node(name("Split Channels"), category("Raster: Channels"), deconstruct_output)] +fn split_channels(_: impl Ctx, input: Table>) -> SplitChannelsOutput { + SplitChannelsOutput { + red: extract_channel((), input.clone(), RedGreenBlueAlpha::Red), + green: extract_channel((), input.clone(), RedGreenBlueAlpha::Green), + blue: extract_channel((), input.clone(), RedGreenBlueAlpha::Blue), + alpha: extract_channel((), input, RedGreenBlueAlpha::Alpha), + } +} + #[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))] fn make_opaque>( _: impl Ctx, From 051b6906c88acf6b08a489970fd8a320aa5eb3cf Mon Sep 17 00:00:00 2001 From: JustJ01 Date: Tue, 24 Feb 2026 04:31:31 +0530 Subject: [PATCH 6/6] removed dead code --- .../node_graph/document_node_definitions.rs | 24 +++---------------- node-graph/node-macro/src/destruct.rs | 17 +++++++------ node-graph/nodes/gcore/src/extract_xy.rs | 2 -- node-graph/nodes/raster/src/adjustments.rs | 4 ---- 4 files changed, 11 insertions(+), 36 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 53dd534503..a341d966f5 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -1207,7 +1207,6 @@ fn document_node_definitions() -> HashMap HashMap HashMap HashMap>), 0), - NodeInput::import(concrete!(Vec), 1), - NodeInput::import(concrete!(BrushCache), 2), - ], - implementation: DocumentNodeImplementation::ProtoNode(brush::brush::brush::IDENTIFIER), - ..Default::default() - }] - .into_iter() - .enumerate() - .map(|(id, node)| (NodeId(id as u64), node)) - .collect(), - ..Default::default() - }), + implementation: DocumentNodeImplementation::ProtoNode(brush::brush::brush::IDENTIFIER), inputs: vec![ NodeInput::value(TaggedValue::Raster(Default::default()), true), NodeInput::value(TaggedValue::BrushStrokes(Vec::new()), false), diff --git a/node-graph/node-macro/src/destruct.rs b/node-graph/node-macro/src/destruct.rs index 4e04252649..1eee997181 100644 --- a/node-graph/node-macro/src/destruct.rs +++ b/node-graph/node-macro/src/destruct.rs @@ -13,15 +13,14 @@ pub fn derive(input: DeriveInput) -> syn::Result { return Err(Error::new(Span::call_site(), "Deriving `Destruct` is currently only supported for structs")); }; - let core_types = match proc_macro_crate::crate_name("core-types") - .map_err(|e| Error::new(Span::call_site(), format!("Failed to find location of core_types. Make sure it is imported as a dependency: {e}")))? - { - FoundCrate::Itself => quote!(crate), - FoundCrate::Name(name) => { - let ident = syn::Ident::new(&name, Span::call_site()); - quote!(#ident) - } - }; + let core_types = + match proc_macro_crate::crate_name("core-types").map_err(|e| Error::new(Span::call_site(), format!("Failed to find location of core_types. Make sure it is imported as a dependency: {e}")))? { + FoundCrate::Itself => quote!(crate), + FoundCrate::Name(name) => { + let ident = syn::Ident::new(&name, Span::call_site()); + quote!(#ident) + } + }; let mut node_implementations = Vec::with_capacity(data_struct.fields.len()); let mut output_fields = Vec::with_capacity(data_struct.fields.len()); diff --git a/node-graph/nodes/gcore/src/extract_xy.rs b/node-graph/nodes/gcore/src/extract_xy.rs index d59b64687f..c8e5e087d9 100644 --- a/node-graph/nodes/gcore/src/extract_xy.rs +++ b/node-graph/nodes/gcore/src/extract_xy.rs @@ -4,9 +4,7 @@ use glam::{DVec2, IVec2, UVec2}; #[derive(Debug, Clone, DynAny, node_macro::Destruct)] pub struct SplitVec2Output { - #[output(name = "X")] pub x: f64, - #[output(name = "Y")] pub y: f64, } diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index bc9d988083..6b937e36bd 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -21,13 +21,9 @@ use vector_types::GradientStops; #[cfg(feature = "std")] #[derive(Debug, Clone, dyn_any::DynAny, node_macro::Destruct)] pub struct SplitChannelsOutput { - #[output(name = "Red")] pub red: Table>, - #[output(name = "Green")] pub green: Table>, - #[output(name = "Blue")] pub blue: Table>, - #[output(name = "Alpha")] pub alpha: Table>, }