mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-20 03:18:06 +08:00
Migrate memo nodes to node macro and make implementing other persistent nodes easier (#3552)
* Add #[data] and #[serialize] attributes to node macro - Add #[data] attribute for struct fields that aren't node parameters - Data fields are initialized with Default::default() - Passed as references to the underlying function - Excluded from registry metadata (internal state) - Generic types in data fields allowed without #[implementations] - Add #[serialize] attribute for custom Node::serialize() implementation - Receives references to all data fields - Generates serialize() method in Node trait impl - Conditional derives based on data field presence - With data fields: Debug, Clone only - Without data fields: Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash * Refactor Memo and Monitor Node to use node macro * Move Complex type into type alias * Fix format * Update node-graph/nodes/gcore/src/memo.rs Co-authored-by: Keavon Chambers <keavon@keavon.com> * Update node-graph/nodes/gcore/src/memo.rs Co-authored-by: Keavon Chambers <keavon@keavon.com> --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -50,6 +50,8 @@ pub(crate) struct NodeFnAttributes {
|
||||
pub(crate) cfg: Option<TokenStream2>,
|
||||
/// if this node should get a gpu implementation, defaults to None
|
||||
pub(crate) shader_node: Option<ShaderNodeType>,
|
||||
/// Custom serialization function path (e.g., "my_module::custom_serialize")
|
||||
pub(crate) serialize: Option<Path>,
|
||||
// Add more attributes as needed
|
||||
}
|
||||
|
||||
@@ -112,6 +114,7 @@ pub struct ParsedField {
|
||||
pub number_display_decimal_places: Option<LitInt>,
|
||||
pub number_step: Option<LitFloat>,
|
||||
pub unit: Option<LitStr>,
|
||||
pub is_data_field: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -201,6 +204,7 @@ impl Parse for NodeFnAttributes {
|
||||
let mut properties_string = None;
|
||||
let mut cfg = None;
|
||||
let mut shader_node = None;
|
||||
let mut serialize = None;
|
||||
|
||||
let content = input;
|
||||
// let content;
|
||||
@@ -270,13 +274,23 @@ impl Parse for NodeFnAttributes {
|
||||
let meta = meta.require_list()?;
|
||||
shader_node = Some(syn::parse2(meta.tokens.to_token_stream())?);
|
||||
}
|
||||
"serialize" => {
|
||||
let meta = meta.require_list()?;
|
||||
if serialize.is_some() {
|
||||
return Err(Error::new_spanned(meta, "Multiple 'serialize' attributes are not allowed"));
|
||||
}
|
||||
let parsed_path: Path = meta
|
||||
.parse_args()
|
||||
.map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'serialize', e.g., serialize(my_module::custom_serialize)"))?;
|
||||
serialize = Some(parsed_path);
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::new_spanned(
|
||||
meta,
|
||||
indoc!(
|
||||
r#"
|
||||
Unsupported attribute in `node`.
|
||||
Supported attributes are 'category', 'path' 'name', 'skip_impl', 'cfg' and 'properties'.
|
||||
Supported attributes are 'category', 'path', 'name', 'skip_impl', 'cfg', 'properties', 'serialize', and 'shader_node'.
|
||||
|
||||
Example usage:
|
||||
#[node_macro::node(category("Value"), name("Test Node"))]
|
||||
@@ -295,6 +309,7 @@ impl Parse for NodeFnAttributes {
|
||||
properties_string,
|
||||
cfg,
|
||||
shader_node,
|
||||
serialize,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -467,6 +482,9 @@ fn parse_node_implementations<T: Parse>(attr: &Attribute, name: &Ident) -> syn::
|
||||
fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Result<ParsedField> {
|
||||
let ident = &pat_ident.ident;
|
||||
|
||||
// Check if this is a data field (struct field, not a parameter)
|
||||
let is_data_field = extract_attribute(attrs, "data").is_some();
|
||||
|
||||
let default_value = extract_attribute(attrs, "default")
|
||||
.map(|attr| attr.parse_args().map_err(|e| Error::new_spanned(attr, format!("Invalid `default` value for argument '{ident}': {e}"))))
|
||||
.transpose()?;
|
||||
@@ -489,6 +507,25 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
||||
|
||||
let exposed = extract_attribute(attrs, "expose").is_some();
|
||||
|
||||
// Validate data field attributes
|
||||
if is_data_field {
|
||||
if default_value.is_some() {
|
||||
return Err(Error::new_spanned(
|
||||
&pat_ident,
|
||||
"Data fields (#[data]) cannot have #[default] attribute. They are automatically initialized with Default::default()",
|
||||
));
|
||||
}
|
||||
if scope.is_some() {
|
||||
return Err(Error::new_spanned(&pat_ident, "Data fields (#[data]) cannot have #[scope] attribute"));
|
||||
}
|
||||
if exposed {
|
||||
return Err(Error::new_spanned(
|
||||
&pat_ident,
|
||||
"Data fields (#[data]) cannot be exposed (#[expose]). They are internal state, not node parameters",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let value_source = match (default_value, scope) {
|
||||
(Some(_), Some(_)) => return Err(Error::new_spanned(&pat_ident, "Cannot have both `default` and `scope` attributes")),
|
||||
(Some(default_value), _) => ParsedValueSource::Default(default_value),
|
||||
@@ -586,6 +623,14 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
||||
.fold(String::new(), |acc, b| acc + &b + "\n");
|
||||
|
||||
if is_node {
|
||||
// Data fields cannot be impl Node types
|
||||
if is_data_field {
|
||||
return Err(Error::new_spanned(
|
||||
&ty,
|
||||
"Data fields (#[data]) cannot be of type `impl Node`. Data fields must be concrete types that implement Default",
|
||||
));
|
||||
}
|
||||
|
||||
let (input_type, output_type) = node_input_type
|
||||
.zip(node_output_type)
|
||||
.ok_or_else(|| Error::new_spanned(&ty, "Invalid Node type. Expected `impl Node<Input, Output = OutputType>`"))?;
|
||||
@@ -610,6 +655,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
||||
number_display_decimal_places,
|
||||
number_step,
|
||||
unit,
|
||||
is_data_field,
|
||||
})
|
||||
} else {
|
||||
let implementations = extract_attribute(attrs, "implementations")
|
||||
@@ -636,6 +682,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
||||
number_display_decimal_places,
|
||||
number_step,
|
||||
unit,
|
||||
is_data_field,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -826,6 +873,7 @@ mod tests {
|
||||
properties_string: None,
|
||||
cfg: None,
|
||||
shader_node: None,
|
||||
serialize: None,
|
||||
},
|
||||
fn_name: Ident::new("add", Span::call_site()),
|
||||
struct_name: Ident::new("Add", Span::call_site()),
|
||||
@@ -860,6 +908,7 @@ mod tests {
|
||||
number_display_decimal_places: None,
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
}],
|
||||
body: TokenStream2::new(),
|
||||
description: String::from("Multi\nLine\n"),
|
||||
@@ -892,6 +941,7 @@ mod tests {
|
||||
properties_string: None,
|
||||
cfg: None,
|
||||
shader_node: None,
|
||||
serialize: None,
|
||||
},
|
||||
fn_name: Ident::new("transform", Span::call_site()),
|
||||
struct_name: Ident::new("Transform", Span::call_site()),
|
||||
@@ -920,6 +970,7 @@ mod tests {
|
||||
number_display_decimal_places: None,
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
},
|
||||
ParsedField {
|
||||
pat_ident: pat_ident("translate"),
|
||||
@@ -941,6 +992,7 @@ mod tests {
|
||||
number_display_decimal_places: None,
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
},
|
||||
],
|
||||
body: TokenStream2::new(),
|
||||
@@ -971,6 +1023,7 @@ mod tests {
|
||||
properties_string: None,
|
||||
cfg: None,
|
||||
shader_node: None,
|
||||
serialize: None,
|
||||
},
|
||||
fn_name: Ident::new("circle", Span::call_site()),
|
||||
struct_name: Ident::new("Circle", Span::call_site()),
|
||||
@@ -1005,6 +1058,7 @@ mod tests {
|
||||
number_display_decimal_places: None,
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
}],
|
||||
body: TokenStream2::new(),
|
||||
description: "Test\n".into(),
|
||||
@@ -1033,6 +1087,7 @@ mod tests {
|
||||
properties_string: None,
|
||||
cfg: None,
|
||||
shader_node: None,
|
||||
serialize: None,
|
||||
},
|
||||
fn_name: Ident::new("levels", Span::call_site()),
|
||||
struct_name: Ident::new("Levels", Span::call_site()),
|
||||
@@ -1072,6 +1127,7 @@ mod tests {
|
||||
number_display_decimal_places: None,
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
}],
|
||||
body: TokenStream2::new(),
|
||||
description: String::new(),
|
||||
@@ -1107,6 +1163,7 @@ mod tests {
|
||||
properties_string: None,
|
||||
cfg: None,
|
||||
shader_node: None,
|
||||
serialize: None,
|
||||
},
|
||||
fn_name: Ident::new("add", Span::call_site()),
|
||||
struct_name: Ident::new("Add", Span::call_site()),
|
||||
@@ -1141,6 +1198,7 @@ mod tests {
|
||||
number_display_decimal_places: None,
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
}],
|
||||
body: TokenStream2::new(),
|
||||
description: String::new(),
|
||||
@@ -1169,6 +1227,7 @@ mod tests {
|
||||
properties_string: None,
|
||||
cfg: None,
|
||||
shader_node: None,
|
||||
serialize: None,
|
||||
},
|
||||
fn_name: Ident::new("load_image", Span::call_site()),
|
||||
struct_name: Ident::new("LoadImage", Span::call_site()),
|
||||
@@ -1203,6 +1262,7 @@ mod tests {
|
||||
number_display_decimal_places: None,
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
}],
|
||||
body: TokenStream2::new(),
|
||||
description: String::new(),
|
||||
@@ -1231,6 +1291,7 @@ mod tests {
|
||||
properties_string: None,
|
||||
cfg: None,
|
||||
shader_node: None,
|
||||
serialize: None,
|
||||
},
|
||||
fn_name: Ident::new("custom_node", Span::call_site()),
|
||||
struct_name: Ident::new("CustomNode", Span::call_site()),
|
||||
|
||||
Reference in New Issue
Block a user