diff --git a/node-graph/README.md b/node-graph/README.md index 0e7e381e1b..6011122f28 100644 --- a/node-graph/README.md +++ b/node-graph/README.md @@ -102,7 +102,7 @@ Instead of manually implementing the `Node` trait with complex generics, one can ```rs #[node_macro::node(category("Raster: Adjustments"))] -fn opacity(_input: (), #[default(424242)] color: Color,#[min(0.1)] opacity_multiplier: f64) -> Color { +fn opacity(_input: (), #[default(424242)] color: Color,#[soft_min(0.1)] opacity_multiplier: f64) -> Color { let opacity_multiplier = opacity_multiplier as f32 / 100.; Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier) } diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs index f4a8649e44..8fd4ebf5fa 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -9,6 +9,7 @@ use core::future::Future; #[cfg(feature = "log")] extern crate log; pub use crate as graphene_core; +pub use num_traits; #[cfg(feature = "reflections")] pub use ctor; diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index bd35b3707f..c5763c63ac 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -1399,7 +1399,7 @@ async fn posterize>( )] mut input: T, #[default(4)] - #[min(2.)] + #[hard_min(2.)] levels: u32, ) -> T { input.adjust(|color| { diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index a5dc3dd2cf..3b10808dd0 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -1,6 +1,7 @@ use super::misc::{ArcType, AsU64, GridType}; use super::{PointId, SegmentId, StrokeId}; use crate::Ctx; +use crate::graphene_core::num_traits::FromPrimitive; use crate::registry::types::Angle; use crate::vector::{HandleId, VectorData, VectorDataTable}; use bezier_rs::Subpath; @@ -97,11 +98,11 @@ fn rectangle( } #[node_macro::node(category("Vector: Shape"))] -fn regular_polygon( +fn regular_polygon( _: impl Ctx, _primary: (), #[default(6)] - #[min(3.)] + #[hard_min(3.)] #[implementations(u32, u64, f64)] sides: T, #[default(50)] radius: f64, @@ -112,11 +113,11 @@ fn regular_polygon( } #[node_macro::node(category("Vector: Shape"))] -fn star( +fn star( _: impl Ctx, _primary: (), #[default(5)] - #[min(2.)] + #[hard_min(2.)] #[implementations(u32, u64, f64)] sides: T, #[default(50)] radius: f64, diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index d0fda86e8d..b552c092c1 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -404,7 +404,7 @@ where async fn round_corners( _: impl Ctx, source: VectorDataTable, - #[min(0.)] + #[hard_min(0.)] #[default(10.)] radius: PixelLength, #[range((0., 1.))] @@ -513,7 +513,7 @@ async fn spatial_merge_by_distance( _: impl Ctx, vector_data: VectorDataTable, #[default(0.1)] - #[min(0.0001)] + #[hard_min(0.0001)] distance: f64, ) -> VectorDataTable { let vector_data_transform = vector_data.transform(); @@ -723,7 +723,7 @@ async fn remove_handles( _: impl Ctx, vector_data: VectorDataTable, #[default(10.)] - #[min(0.)] + #[soft_min(0.)] max_handle_distance: f64, ) -> VectorDataTable { let vector_data_transform = vector_data.transform(); @@ -1331,7 +1331,7 @@ async fn poisson_disk_points( _: impl Ctx, vector_data: VectorDataTable, #[default(10.)] - #[min(0.01)] + #[hard_min(0.01)] separation_disk_diameter: f64, seed: SeedValue, ) -> VectorDataTable { diff --git a/node-graph/gstd/src/image_color_palette.rs b/node-graph/gstd/src/image_color_palette.rs index bd6ce2832d..ce47961d74 100644 --- a/node-graph/gstd/src/image_color_palette.rs +++ b/node-graph/gstd/src/image_color_palette.rs @@ -5,8 +5,8 @@ use graphene_core::{Color, Ctx}; async fn image_color_palette( _: impl Ctx, image: ImageFrameTable, - #[min(1.)] - #[max(28.)] + #[hard_min(1.)] + #[hard_max(28.)] max_size: u32, ) -> Vec { const GRID: f32 = 3.; diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index af04dd8a0f..7d98e02637 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -134,14 +134,22 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result = 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 { + let name = &pat_ident.ident; + let mut tokens = quote!(); + if let Some(min) = number_hard_min { + tokens.extend(quote! { + let #name = #graphene_core::num_traits::clamp_min(#name, #graphene_core::num_traits::FromPrimitive::from_f64(#min).unwrap()); + }); + } + + if let Some(max) = number_hard_max { + tokens.extend(quote! { + let #name = #graphene_core::num_traits::clamp_max(#name, #graphene_core::num_traits::FromPrimitive::from_f64(#max).unwrap()); + }); + } + tokens + } + ParsedField::Node { .. } => { + quote!() + } + }); + let all_implementation_types = fields.iter().flat_map(|field| match field { ParsedField::Regular { implementations, .. } => implementations.into_iter().cloned().collect::>(), ParsedField::Node { implementations, .. } => implementations @@ -237,6 +272,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result Self::Output { Box::pin(async move { #(#eval_args)* + #(#min_max_args)* self::#fn_name(__input #(, #field_names)*) #await_keyword }) } diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index c66ae276d5..ee8030d55c 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -105,8 +105,10 @@ pub(crate) enum ParsedField { ty: Type, exposed: bool, value_source: ParsedValueSource, - number_min: Option, - number_max: Option, + number_soft_min: Option, + number_soft_max: Option, + number_hard_min: Option, + number_hard_max: Option, number_mode_range: Option, implementations: Punctuated, }, @@ -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 `soft_min` value for argument '{}': {}", ident, e))) + }) + .transpose()?; + let number_soft_max = extract_attribute(attrs, "soft_max") + .map(|attr| { + attr.parse_args() + .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 `min` value for argument '{}': {}", ident, e))) + .map_err(|e| Error::new_spanned(attr, format!("Invalid numerical `hard_min` value for argument '{}': {}", ident, e))) }) .transpose()?; - let number_max = extract_attribute(attrs, "max") + let number_hard_max = extract_attribute(attrs, "hard_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 `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(), }], diff --git a/node-graph/node-macro/src/validation.rs b/node-graph/node-macro/src/validation.rs index e6eece2128..c475fff6db 100644 --- a/node-graph/node-macro/src/validation.rs +++ b/node-graph/node-macro/src/validation.rs @@ -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,73 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> { Ok(()) } +fn validate_min_max(parsed: &ParsedNodeFn) { + for field in &parsed.fields { + match field { + ParsedField::Regular { + number_hard_max, + number_hard_min, + number_soft_max, + number_soft_min, + pat_ident, + .. + } => { + match (number_soft_min, number_hard_min) { + (Some(soft_min), Some(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] equals to it.", + pat_ident.ident; + help = "You can safely remove the #[soft_min] attribute from this field."; + note = "The #[hard_min] also limits the range to same without #[soft_min]", + ); + } else if soft_min_value < hard_min_value { + emit_error!( + pat_ident.span(), + "The #[soft_min] attribute exists on `{}` and is lower than #[hard_min].", + pat_ident.ident; + help = "You probably meant to reverse the two macros"; + note = "Allowing the possible range in slider to be more than #[hard_min] doesn't make sense", + ); + } + } + _ => (), + } + + match (number_soft_max, number_hard_max) { + (Some(soft_max), Some(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] equals to it.", + pat_ident.ident; + help = "You can safely remove the #[soft_max] attribute from this field."; + note = "The #[hard_max] also limits the range to same without #[soft_max]", + ); + } else if soft_max_value < hard_max_value { + emit_error!( + pat_ident.span(), + "The #[soft_max] attribute exists on `{}` and is greater than #[hard_max].", + pat_ident.ident; + help = "You probably meant to reverse the two macros"; + note = "Allowing the possible range in slider to be more than #[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!(