Polish user-created subgraph nodes: imports in the Properties panel; reorder/delete/rename imports/exports (#2105)

* Remove imports/exports

* WIP: Autogenerated properties

* WIP: Input based properties

* WIP: Hashmap based input overrides

* Migrate noise pattern node to input properties

* Reorder exports

* Continue migrating properties

* WIP: Improve reorder exports

* Automatically populate all input properties for sub networks

* Complete reorder import and export

* Add widget override to node macro

* Migrate assign colors to input based properties

* WIP: Full node property override

* Node based properties override for proto nodes

* Migrate all node properties to be input based

* Rename imports/exports

* improve UI

* Protonode input valid implementations

* Valid type list

* Small formatting fixes

* Polishing small issues

* Document upgrade

* fix tests

* Upgrade noise pattern node

* remove console log

* Fix upgrade script for Noise Pattern

* Improve the Properties panel representation for graphical data

* Re-export demo art

* Code review

* code review improvements

* Cleanup for node properties overrides

* Reexport demo art

* Fix clippy lints

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adam Gerhant 2025-01-20 21:13:14 -08:00 committed by GitHub
parent ad68b1e5c8
commit eec0ef761c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 3660 additions and 2006 deletions

View file

@ -84,21 +84,36 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
ParsedField::Regular { ty, .. } => ty.clone(),
ParsedField::Node { output_type, input_type, .. } => match parsed.is_async {
true => parse_quote!(&'n impl #graphene_core::Node<'n, #input_type, Output: core::future::Future<Output=#output_type> + #graphene_core::WasmNotSend>),
false => parse_quote!(&'n impl #graphene_core::Node<'n, #input_type, Output = #output_type>),
},
})
.collect();
let widget_override: Vec<_> = fields
.iter()
.map(|field| {
let parsed_widget_override = match field {
ParsedField::Regular { widget_override, .. } => widget_override,
ParsedField::Node { widget_override, .. } => widget_override,
};
match parsed_widget_override {
ParsedWidgetOverride::None => quote!(RegistryWidgetOverride::None),
ParsedWidgetOverride::Hidden => quote!(RegistryWidgetOverride::Hidden),
ParsedWidgetOverride::String(lit_str) => quote!(RegistryWidgetOverride::String(#lit_str)),
ParsedWidgetOverride::Custom(lit_str) => quote!(RegistryWidgetOverride::Custom(#lit_str)),
}
})
.collect();
let value_sources: Vec<_> = fields
.iter()
.map(|field| match field {
ParsedField::Regular { value_source, .. } => match value_source {
ValueSource::Default(data) => quote!(ValueSource::Default(stringify!(#data))),
ValueSource::Scope(data) => quote!(ValueSource::Scope(#data)),
_ => quote!(ValueSource::None),
ParsedValueSource::Default(data) => quote!(RegistryValueSource::Default(stringify!(#data))),
ParsedValueSource::Scope(data) => quote!(RegistryValueSource::Scope(#data)),
_ => quote!(RegistryValueSource::None),
},
_ => quote!(ValueSource::None),
_ => quote!(RegistryValueSource::None),
})
.collect();
@ -213,6 +228,8 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
let register_node_impl = generate_register_node_impl(parsed, &field_names, &struct_name, &identifier)?;
let import_name = format_ident!("_IMPORT_STUB_{}", mod_name.to_string().to_case(Case::UpperSnake));
let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
Ok(quote! {
/// Underlying implementation for [#struct_name]
#[inline]
@ -235,7 +252,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
use gcore::{Node, NodeIOTypes, concrete, fn_type, future, ProtoNodeIdentifier, WasmNotSync, NodeIO};
use gcore::value::ClonedNode;
use gcore::ops::TypeNode;
use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, ValueSource};
use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, RegistryValueSource, RegistryWidgetOverride};
use gcore::ctor::ctor;
// Use the types specified in the implementation
@ -266,10 +283,12 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
display_name: #display_name,
category: #category,
description: #description,
properties: #properties,
fields: vec![
#(
FieldMetadata {
name: #input_names,
widget_override: #widget_override,
description: #input_descriptions,
exposed: #exposed,
value_source: #value_sources,

View file

@ -39,26 +39,69 @@ pub(crate) struct NodeFnAttributes {
pub(crate) display_name: Option<LitStr>,
pub(crate) path: Option<Path>,
pub(crate) skip_impl: bool,
pub(crate) properties_string: Option<LitStr>,
// Add more attributes as needed
}
#[derive(Debug, Default)]
pub enum ValueSource {
pub enum ParsedValueSource {
#[default]
None,
Default(TokenStream2),
Scope(LitStr),
}
// #[widget(ParsedWidgetOverride::Hidden)]
// #[widget(ParsedWidgetOverride::String = "Some string")]
// #[widget(ParsedWidgetOverride::Custom = "Custom string")]
#[derive(Debug, Default)]
pub enum ParsedWidgetOverride {
#[default]
None,
Hidden,
String(LitStr),
Custom(LitStr),
}
impl Parse for ParsedWidgetOverride {
fn parse(input: ParseStream) -> syn::Result<Self> {
// Parse the full path (e.g., ParsedWidgetOverride::Hidden)
let path: Path = input.parse()?;
// Ensure the path starts with `ParsedWidgetOverride`
if path.segments.len() == 2 && path.segments[0].ident == "ParsedWidgetOverride" {
let variant = &path.segments[1].ident;
match variant.to_string().as_str() {
"Hidden" => Ok(ParsedWidgetOverride::Hidden),
"String" => {
input.parse::<syn::Token![=]>()?;
let lit: LitStr = input.parse()?;
Ok(ParsedWidgetOverride::String(lit))
}
"Custom" => {
input.parse::<syn::Token![=]>()?;
let lit: LitStr = input.parse()?;
Ok(ParsedWidgetOverride::Custom(lit))
}
_ => Err(syn::Error::new(variant.span(), "Unknown ParsedWidgetOverride variant")),
}
} else {
Err(syn::Error::new(input.span(), "Expected ParsedWidgetOverride::<variant>"))
}
}
}
#[derive(Debug)]
pub(crate) enum ParsedField {
Regular {
pat_ident: PatIdent,
name: Option<LitStr>,
description: String,
widget_override: ParsedWidgetOverride,
ty: Type,
exposed: bool,
value_source: ValueSource,
value_source: ParsedValueSource,
number_min: Option<LitFloat>,
number_max: Option<LitFloat>,
number_mode_range: Option<ExprTuple>,
@ -68,6 +111,7 @@ pub(crate) enum ParsedField {
pat_ident: PatIdent,
name: Option<LitStr>,
description: String,
widget_override: ParsedWidgetOverride,
input_type: Type,
output_type: Type,
implementations: Punctuated<Implementation, Comma>,
@ -126,6 +170,7 @@ impl Parse for NodeFnAttributes {
let mut display_name = None;
let mut path = None;
let mut skip_impl = false;
let mut properties_string = None;
let content = input;
// let content;
@ -165,6 +210,16 @@ impl Parse for NodeFnAttributes {
}
skip_impl = true;
}
Meta::List(meta) if meta.path.is_ident("properties") => {
if properties_string.is_some() {
return Err(Error::new_spanned(path, "Multiple 'properties_string' attributes are not allowed"));
}
let parsed_properties_string: LitStr = meta
.parse_args()
.map_err(|_| Error::new_spanned(meta, "Expected a string for 'properties', e.g., name(\"channel_mixer_properties\")"))?;
properties_string = Some(parsed_properties_string);
}
_ => {
return Err(Error::new_spanned(
meta,
@ -187,6 +242,7 @@ impl Parse for NodeFnAttributes {
display_name,
path,
skip_impl,
properties_string,
})
}
}
@ -343,13 +399,21 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
.map(|attr| attr.parse_args().map_err(|e| Error::new_spanned(attr, format!("Invalid `name` value for argument '{}': {}", ident, e))))
.transpose()?;
let widget_override = extract_attribute(attrs, "widget")
.map(|attr| {
attr.parse_args()
.map_err(|e| Error::new_spanned(attr, format!("Invalid `widget override` value for argument '{}': {}", ident, e)))
})
.transpose()?
.unwrap_or_default();
let exposed = extract_attribute(attrs, "expose").is_some();
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), _) => ValueSource::Default(default_value),
(_, Some(scope)) => ValueSource::Scope(scope),
_ => ValueSource::None,
(Some(default_value), _) => ParsedValueSource::Default(default_value),
(_, Some(scope)) => ParsedValueSource::Scope(scope),
_ => ParsedValueSource::None,
};
let number_min = extract_attribute(attrs, "min")
@ -405,7 +469,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
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>`"))?;
if !matches!(&value_source, ValueSource::None) {
if !matches!(&value_source, ParsedValueSource::None) {
return Err(Error::new_spanned(&ty, "No default values for `impl Node` allowed"));
}
let implementations = extract_attribute(attrs, "implementations")
@ -417,6 +481,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
pat_ident,
name,
description,
widget_override,
input_type,
output_type,
implementations,
@ -430,6 +495,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
pat_ident,
name,
description,
widget_override,
exposed,
number_min,
number_max,
@ -550,11 +616,11 @@ mod tests {
assert_eq!(p_name, e_name);
assert_eq!(p_exp, e_exp);
match (p_default, e_default) {
(ValueSource::None, ValueSource::None) => {}
(ValueSource::Default(p), ValueSource::Default(e)) => {
(ParsedValueSource::None, ParsedValueSource::None) => {}
(ParsedValueSource::Default(p), ParsedValueSource::Default(e)) => {
assert_eq!(p.to_token_stream().to_string(), e.to_token_stream().to_string());
}
(ValueSource::Scope(p), ValueSource::Scope(e)) => {
(ParsedValueSource::Scope(p), ParsedValueSource::Scope(e)) => {
assert_eq!(p.value(), e.value());
}
_ => panic!("Mismatched default values"),
@ -602,6 +668,7 @@ mod tests {
display_name: None,
path: Some(parse_quote!(graphene_core::TestNode)),
skip_impl: true,
properties_string: None,
},
fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()),
@ -619,9 +686,10 @@ mod tests {
pat_ident: pat_ident("b"),
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::None,
value_source: ParsedValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
@ -655,6 +723,7 @@ mod tests {
display_name: None,
path: None,
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("transform", Span::call_site()),
struct_name: Ident::new("Transform", Span::call_site()),
@ -673,6 +742,7 @@ mod tests {
pat_ident: pat_ident("transform_target"),
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::None,
input_type: parse_quote!(Footprint),
output_type: parse_quote!(T),
implementations: Punctuated::new(),
@ -681,9 +751,10 @@ mod tests {
pat_ident: pat_ident("translate"),
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::None,
ty: parse_quote!(DVec2),
exposed: false,
value_source: ValueSource::None,
value_source: ParsedValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
@ -715,6 +786,7 @@ mod tests {
display_name: None,
path: None,
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("circle", Span::call_site()),
struct_name: Ident::new("Circle", Span::call_site()),
@ -732,9 +804,10 @@ mod tests {
pat_ident: pat_ident("radius"),
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::Default(quote!(50.)),
value_source: ParsedValueSource::Default(quote!(50.)),
number_min: None,
number_max: None,
number_mode_range: None,
@ -764,6 +837,7 @@ mod tests {
display_name: None,
path: None,
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("levels", Span::call_site()),
struct_name: Ident::new("Levels", Span::call_site()),
@ -781,9 +855,10 @@ mod tests {
pat_ident: pat_ident("shadows"),
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::None,
value_source: ParsedValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
@ -825,6 +900,7 @@ mod tests {
display_name: None,
path: Some(parse_quote!(graphene_core::TestNode)),
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()),
@ -842,9 +918,10 @@ mod tests {
pat_ident: pat_ident("b"),
name: None,
description: String::from("b"),
widget_override: ParsedWidgetOverride::None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::None,
value_source: ParsedValueSource::None,
number_min: Some(parse_quote!(-500.)),
number_max: Some(parse_quote!(500.)),
number_mode_range: Some(parse_quote!((0., 100.))),
@ -874,6 +951,7 @@ mod tests {
display_name: None,
path: None,
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("load_image", Span::call_site()),
struct_name: Ident::new("LoadImage", Span::call_site()),
@ -892,8 +970,9 @@ mod tests {
name: None,
ty: parse_quote!(String),
description: String::new(),
widget_override: ParsedWidgetOverride::None,
exposed: true,
value_source: ValueSource::None,
value_source: ParsedValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
@ -923,6 +1002,7 @@ mod tests {
display_name: Some(parse_quote!("CustomNode2")),
path: None,
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("custom_node", Span::call_site()),
struct_name: Ident::new("CustomNode", Span::call_site()),