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()),