mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Add Destruct macro and trait for automatically acessing struct fields
This commit is contained in:
@@ -36,6 +36,7 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
|
||||
description,
|
||||
properties,
|
||||
context_features,
|
||||
output_fields: _,
|
||||
} = metadata;
|
||||
|
||||
let Some(implementations) = &node_registry.get(id) else { continue };
|
||||
|
||||
@@ -16,6 +16,7 @@ pub struct NodeMetadata {
|
||||
pub description: &'static str,
|
||||
pub properties: Option<&'static str>,
|
||||
pub context_features: Vec<ContextFeature>,
|
||||
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<T> Destruct for &T
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
fn fields(&self) -> &'static [StructField] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RegistryWidgetOverride {
|
||||
None,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
62
node-graph/node-macro/src/destruct.rs
Normal file
62
node-graph/node-macro/src/destruct.rs
Normal file
@@ -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<TokenStream2> {
|
||||
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] {
|
||||
&[
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ pub(crate) struct NodeFnAttributes {
|
||||
pub(crate) shader_node: Option<ShaderNodeType>,
|
||||
/// Custom serialization function path (e.g., "my_module::custom_serialize")
|
||||
pub(crate) serialize: Option<Path>,
|
||||
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()),
|
||||
|
||||
Reference in New Issue
Block a user