mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-31 02:07:21 +00:00
Update #[min/max] node macro attributes to #[soft/hard]_[min/max] and make them clamp their input data (#2464)
* Fix min and max macro not enforcing limits when data flows * Use trait based clamping * Remove min/max from testing * cargo fmt * Resolve into min, and hard_min * cargo fmt * fix traits * cargo fmt * fix tests * rename as soft_x * Add validation code * Clean up (not compiling because of DVec2 clamping) * Avoid needing to add trait bounds to node definitions * Code review --------- Co-authored-by: Dennis Kobert <dennis@kobert.dev> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
2fc4896d01
commit
9303953cf8
13 changed files with 259 additions and 46 deletions
|
@ -2,7 +2,7 @@ use crate::parsing::*;
|
|||
use convert_case::{Case, Casing};
|
||||
use proc_macro_crate::FoundCrate;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
|
@ -134,14 +134,22 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
let number_min_values: Vec<_> = fields
|
||||
.iter()
|
||||
.map(|field| match field {
|
||||
ParsedField::Regular { number_min: Some(number_min), .. } => quote!(Some(#number_min)),
|
||||
ParsedField::Regular { number_soft_min, number_hard_min, .. } => match (number_soft_min, number_hard_min) {
|
||||
(Some(soft_min), _) => quote!(Some(#soft_min)),
|
||||
(None, Some(hard_min)) => quote!(Some(#hard_min)),
|
||||
(None, None) => quote!(None),
|
||||
},
|
||||
_ => quote!(None),
|
||||
})
|
||||
.collect();
|
||||
let number_max_values: Vec<_> = fields
|
||||
.iter()
|
||||
.map(|field| match field {
|
||||
ParsedField::Regular { number_max: Some(number_max), .. } => quote!(Some(#number_max)),
|
||||
ParsedField::Regular { number_soft_max, number_hard_max, .. } => match (number_soft_max, number_hard_max) {
|
||||
(Some(soft_max), _) => quote!(Some(#soft_max)),
|
||||
(None, Some(hard_max)) => quote!(Some(#hard_max)),
|
||||
(None, None) => quote!(None),
|
||||
},
|
||||
_ => quote!(None),
|
||||
})
|
||||
.collect();
|
||||
|
@ -175,6 +183,33 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
}
|
||||
});
|
||||
|
||||
let min_max_args = fields.iter().map(|field| match field {
|
||||
ParsedField::Regular {
|
||||
pat_ident,
|
||||
number_hard_min,
|
||||
number_hard_max,
|
||||
..
|
||||
} => {
|
||||
let name = &pat_ident.ident;
|
||||
let mut tokens = quote!();
|
||||
if let Some(min) = number_hard_min {
|
||||
tokens.extend(quote_spanned! {min.span()=>
|
||||
let #name = #graphene_core::misc::Clampable::clamp_hard_min(#name, #min);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(max) = number_hard_max {
|
||||
tokens.extend(quote_spanned! {max.span()=>
|
||||
let #name = #graphene_core::misc::Clampable::clamp_hard_max(#name, #max);
|
||||
});
|
||||
}
|
||||
tokens
|
||||
}
|
||||
ParsedField::Node { .. } => {
|
||||
quote!()
|
||||
}
|
||||
});
|
||||
|
||||
let all_implementation_types = fields.iter().flat_map(|field| match field {
|
||||
ParsedField::Regular { implementations, .. } => implementations.into_iter().cloned().collect::<Vec<_>>(),
|
||||
ParsedField::Node { implementations, .. } => implementations
|
||||
|
@ -186,13 +221,27 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
|
||||
let input_type = &parsed.input.ty;
|
||||
let mut clauses = Vec::new();
|
||||
let mut clampable_clauses = Vec::new();
|
||||
|
||||
for (field, name) in fields.iter().zip(struct_generics.iter()) {
|
||||
clauses.push(match (field, *is_async) {
|
||||
(ParsedField::Regular { ty, .. }, _) => {
|
||||
(
|
||||
ParsedField::Regular {
|
||||
ty, number_hard_min, number_hard_max, ..
|
||||
},
|
||||
_,
|
||||
) => {
|
||||
let all_lifetime_ty = substitute_lifetimes(ty.clone(), "all");
|
||||
let id = future_idents.len();
|
||||
let fut_ident = format_ident!("F{}", id);
|
||||
future_idents.push(fut_ident.clone());
|
||||
|
||||
// Add Clampable bound if this field uses hard_min or hard_max
|
||||
if number_hard_min.is_some() || number_hard_max.is_some() {
|
||||
// The bound applies to the Output type of the future, which is #ty
|
||||
clampable_clauses.push(quote!(#ty: #graphene_core::misc::Clampable));
|
||||
}
|
||||
|
||||
quote!(
|
||||
#fut_ident: core::future::Future<Output = #ty> + #graphene_core::WasmNotSend + 'n,
|
||||
for<'all> #all_lifetime_ty: #graphene_core::WasmNotSend,
|
||||
|
@ -220,6 +269,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
let mut struct_where_clause = where_clause.clone();
|
||||
let extra_where: Punctuated<WherePredicate, Comma> = parse_quote!(
|
||||
#(#clauses,)*
|
||||
#(#clampable_clauses,)*
|
||||
#output_type: 'n,
|
||||
);
|
||||
struct_where_clause.predicates.extend(extra_where);
|
||||
|
@ -236,7 +286,10 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
#[inline]
|
||||
fn eval(&'n self, __input: #input_type) -> Self::Output {
|
||||
Box::pin(async move {
|
||||
use #graphene_core::misc::Clampable;
|
||||
|
||||
#(#eval_args)*
|
||||
#(#min_max_args)*
|
||||
self::#fn_name(__input #(, #field_names)*) #await_keyword
|
||||
})
|
||||
}
|
||||
|
|
|
@ -105,8 +105,10 @@ pub(crate) enum ParsedField {
|
|||
ty: Type,
|
||||
exposed: bool,
|
||||
value_source: ParsedValueSource,
|
||||
number_min: Option<LitFloat>,
|
||||
number_max: Option<LitFloat>,
|
||||
number_soft_min: Option<LitFloat>,
|
||||
number_soft_max: Option<LitFloat>,
|
||||
number_hard_min: Option<LitFloat>,
|
||||
number_hard_max: Option<LitFloat>,
|
||||
number_mode_range: Option<ExprTuple>,
|
||||
implementations: Punctuated<Type, Comma>,
|
||||
},
|
||||
|
@ -230,7 +232,7 @@ impl Parse for NodeFnAttributes {
|
|||
r#"
|
||||
Unsupported attribute in `node`.
|
||||
Supported attributes are 'category', 'path' and 'name'.
|
||||
|
||||
|
||||
Example usage:
|
||||
#[node_macro::node(category("Value"), name("Test Node"))]
|
||||
"#
|
||||
|
@ -419,16 +421,29 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
|||
_ => ParsedValueSource::None,
|
||||
};
|
||||
|
||||
let number_min = extract_attribute(attrs, "min")
|
||||
let number_soft_min = extract_attribute(attrs, "soft_min")
|
||||
.map(|attr| {
|
||||
attr.parse_args()
|
||||
.map_err(|e| Error::new_spanned(attr, format!("Invalid numerical `min` value for argument '{}': {}", ident, e)))
|
||||
.map_err(|e| Error::new_spanned(attr, format!("Invalid numerical `soft_min` value for argument '{}': {}", ident, e)))
|
||||
})
|
||||
.transpose()?;
|
||||
let number_max = extract_attribute(attrs, "max")
|
||||
let number_soft_max = extract_attribute(attrs, "soft_max")
|
||||
.map(|attr| {
|
||||
attr.parse_args()
|
||||
.map_err(|e| Error::new_spanned(attr, format!("Invalid numerical `max` value for argument '{}': {}", ident, e)))
|
||||
.map_err(|e| Error::new_spanned(attr, format!("Invalid numerical `soft_max` value for argument '{}': {}", ident, e)))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let number_hard_min = extract_attribute(attrs, "hard_min")
|
||||
.map(|attr| {
|
||||
attr.parse_args()
|
||||
.map_err(|e| Error::new_spanned(attr, format!("Invalid numerical `hard_min` value for argument '{}': {}", ident, e)))
|
||||
})
|
||||
.transpose()?;
|
||||
let number_hard_max = extract_attribute(attrs, "hard_max")
|
||||
.map(|attr| {
|
||||
attr.parse_args()
|
||||
.map_err(|e| Error::new_spanned(attr, format!("Invalid numerical `hard_max` value for argument '{}': {}", ident, e)))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
|
@ -500,8 +515,10 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
|||
description,
|
||||
widget_override,
|
||||
exposed,
|
||||
number_min,
|
||||
number_max,
|
||||
number_soft_min,
|
||||
number_soft_max,
|
||||
number_hard_min,
|
||||
number_hard_max,
|
||||
number_mode_range,
|
||||
ty,
|
||||
value_source,
|
||||
|
@ -716,8 +733,10 @@ mod tests {
|
|||
ty: parse_quote!(f64),
|
||||
exposed: false,
|
||||
value_source: ParsedValueSource::None,
|
||||
number_min: None,
|
||||
number_max: None,
|
||||
number_soft_min: None,
|
||||
number_soft_max: None,
|
||||
number_hard_min: None,
|
||||
number_hard_max: None,
|
||||
number_mode_range: None,
|
||||
implementations: Punctuated::new(),
|
||||
}],
|
||||
|
@ -781,8 +800,10 @@ mod tests {
|
|||
ty: parse_quote!(DVec2),
|
||||
exposed: false,
|
||||
value_source: ParsedValueSource::None,
|
||||
number_min: None,
|
||||
number_max: None,
|
||||
number_soft_min: None,
|
||||
number_soft_max: None,
|
||||
number_hard_min: None,
|
||||
number_hard_max: None,
|
||||
number_mode_range: None,
|
||||
implementations: Punctuated::new(),
|
||||
},
|
||||
|
@ -834,8 +855,10 @@ mod tests {
|
|||
ty: parse_quote!(f64),
|
||||
exposed: false,
|
||||
value_source: ParsedValueSource::Default(quote!(50.)),
|
||||
number_min: None,
|
||||
number_max: None,
|
||||
number_soft_min: None,
|
||||
number_soft_max: None,
|
||||
number_hard_min: None,
|
||||
number_hard_max: None,
|
||||
number_mode_range: None,
|
||||
implementations: Punctuated::new(),
|
||||
}],
|
||||
|
@ -885,8 +908,10 @@ mod tests {
|
|||
ty: parse_quote!(f64),
|
||||
exposed: false,
|
||||
value_source: ParsedValueSource::None,
|
||||
number_min: None,
|
||||
number_max: None,
|
||||
number_soft_min: None,
|
||||
number_soft_max: None,
|
||||
number_hard_min: None,
|
||||
number_hard_max: None,
|
||||
number_mode_range: None,
|
||||
implementations: {
|
||||
let mut p = Punctuated::new();
|
||||
|
@ -911,8 +936,8 @@ mod tests {
|
|||
a: f64,
|
||||
/// b
|
||||
#[range((0., 100.))]
|
||||
#[min(-500.)]
|
||||
#[max(500.)]
|
||||
#[soft_min(-500.)]
|
||||
#[soft_max(500.)]
|
||||
b: f64,
|
||||
) -> f64 {
|
||||
a + b
|
||||
|
@ -948,8 +973,10 @@ mod tests {
|
|||
ty: parse_quote!(f64),
|
||||
exposed: false,
|
||||
value_source: ParsedValueSource::None,
|
||||
number_min: Some(parse_quote!(-500.)),
|
||||
number_max: Some(parse_quote!(500.)),
|
||||
number_soft_min: Some(parse_quote!(-500.)),
|
||||
number_soft_max: Some(parse_quote!(500.)),
|
||||
number_hard_min: None,
|
||||
number_hard_max: None,
|
||||
number_mode_range: Some(parse_quote!((0., 100.))),
|
||||
implementations: Punctuated::new(),
|
||||
}],
|
||||
|
@ -999,8 +1026,10 @@ mod tests {
|
|||
widget_override: ParsedWidgetOverride::None,
|
||||
exposed: true,
|
||||
value_source: ParsedValueSource::None,
|
||||
number_min: None,
|
||||
number_max: None,
|
||||
number_soft_min: None,
|
||||
number_soft_max: None,
|
||||
number_hard_min: None,
|
||||
number_hard_max: None,
|
||||
number_mode_range: None,
|
||||
implementations: Punctuated::new(),
|
||||
}],
|
||||
|
|
|
@ -9,6 +9,7 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> {
|
|||
// Add more validators here as needed
|
||||
validate_implementations_for_generics,
|
||||
validate_primary_input_expose,
|
||||
validate_min_max,
|
||||
];
|
||||
|
||||
for validator in validators {
|
||||
|
@ -18,6 +19,64 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_min_max(parsed: &ParsedNodeFn) {
|
||||
for field in &parsed.fields {
|
||||
if let ParsedField::Regular {
|
||||
number_hard_max,
|
||||
number_hard_min,
|
||||
number_soft_max,
|
||||
number_soft_min,
|
||||
pat_ident,
|
||||
..
|
||||
} = field
|
||||
{
|
||||
if let (Some(soft_min), Some(hard_min)) = (number_soft_min, number_hard_min) {
|
||||
let soft_min_value: f64 = soft_min.base10_parse().unwrap_or_default();
|
||||
let hard_min_value: f64 = hard_min.base10_parse().unwrap_or_default();
|
||||
if soft_min_value == hard_min_value {
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"Unnecessary #[soft_min] attribute on `{}`, as #[hard_min] has the same value.",
|
||||
pat_ident.ident;
|
||||
help = "You can safely remove the #[soft_min] attribute from this field.";
|
||||
note = "#[soft_min] is redundant when it equals #[hard_min].",
|
||||
);
|
||||
} else if soft_min_value < hard_min_value {
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"The #[soft_min] attribute on `{}` is incorrectly greater than #[hard_min].",
|
||||
pat_ident.ident;
|
||||
help = "You probably meant to reverse the two attribute values.";
|
||||
note = "Allowing the possible slider range to preceed #[hard_min] doesn't make sense.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(soft_max), Some(hard_max)) = (number_soft_max, number_hard_max) {
|
||||
let soft_max_value: f64 = soft_max.base10_parse().unwrap_or_default();
|
||||
let hard_max_value: f64 = hard_max.base10_parse().unwrap_or_default();
|
||||
if soft_max_value == hard_max_value {
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"Unnecessary #[soft_max] attribute on `{}`, as #[hard_max] has the same value.",
|
||||
pat_ident.ident;
|
||||
help = "You can safely remove the #[soft_max] attribute from this field.";
|
||||
note = "#[soft_max] is redundant when it equals #[hard_max].",
|
||||
);
|
||||
} else if soft_max_value < hard_max_value {
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"The #[soft_max] attribute on `{}` is incorrectly greater than #[hard_max].",
|
||||
pat_ident.ident;
|
||||
help = "You probably meant to reverse the two attribute values.";
|
||||
note = "Allowing the possible slider range to exceed #[hard_max] doesn't make sense.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_primary_input_expose(parsed: &ParsedNodeFn) {
|
||||
if let Some(ParsedField::Regular { exposed: true, pat_ident, .. }) = parsed.fields.first() {
|
||||
emit_error!(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue