Skip to content

Commit 9303953

Browse files
mTvare6TrueDoctorKeavon
authored
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>
1 parent 2fc4896 commit 9303953

File tree

13 files changed

+259
-46
lines changed

13 files changed

+259
-46
lines changed

node-graph/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Instead of manually implementing the `Node` trait with complex generics, one can
102102

103103
```rs
104104
#[node_macro::node(category("Raster: Adjustments"))]
105-
fn opacity(_input: (), #[default(424242)] color: Color,#[min(0.1)] opacity_multiplier: f64) -> Color {
105+
fn opacity(_input: (), #[default(424242)] color: Color, #[soft_min(0.1)] opacity_multiplier: f64) -> Color {
106106
let opacity_multiplier = opacity_multiplier as f32 / 100.;
107107
Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier)
108108
}

node-graph/gcore/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use core::future::Future;
99
#[cfg(feature = "log")]
1010
extern crate log;
1111
pub use crate as graphene_core;
12+
pub use num_traits;
1213

1314
#[cfg(feature = "reflections")]
1415
pub use ctor;
@@ -19,6 +20,7 @@ pub mod context;
1920
pub mod generic;
2021
pub mod instances;
2122
pub mod logic;
23+
pub mod misc;
2224
pub mod ops;
2325
pub mod structural;
2426
#[cfg(feature = "std")]

node-graph/gcore/src/misc.rs

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// TODO(TrueDoctor): Replace this with the more idiomatic approach instead of using `trait Clampable`.
2+
3+
/// A trait for types that can be clamped within a min/max range defined by f64.
4+
pub trait Clampable: Sized {
5+
/// Clamps the value to be no less than `min`.
6+
fn clamp_hard_min(self, min: f64) -> Self;
7+
/// Clamps the value to be no more than `max`.
8+
fn clamp_hard_max(self, max: f64) -> Self;
9+
}
10+
11+
// Implement for common numeric types
12+
macro_rules! impl_clampable_float {
13+
($($ty:ty),*) => {
14+
$(
15+
impl Clampable for $ty {
16+
#[inline(always)]
17+
fn clamp_hard_min(self, min: f64) -> Self {
18+
self.max(min as $ty)
19+
}
20+
#[inline(always)]
21+
fn clamp_hard_max(self, max: f64) -> Self {
22+
self.min(max as $ty)
23+
}
24+
}
25+
)*
26+
};
27+
}
28+
impl_clampable_float!(f32, f64);
29+
30+
macro_rules! impl_clampable_int {
31+
($($ty:ty),*) => {
32+
$(
33+
impl Clampable for $ty {
34+
#[inline(always)]
35+
fn clamp_hard_min(self, min: f64) -> Self {
36+
// Using try_from to handle potential range issues safely, though min should ideally be valid.
37+
// Consider using a different approach if f64 precision vs integer range is a concern.
38+
<$ty>::try_from(min.ceil() as i64).ok().map_or(self, |min_val| self.max(min_val))
39+
}
40+
#[inline(always)]
41+
fn clamp_hard_max(self, max: f64) -> Self {
42+
<$ty>::try_from(max.floor() as i64).ok().map_or(self, |max_val| self.min(max_val))
43+
}
44+
}
45+
)*
46+
};
47+
}
48+
// Add relevant integer types (adjust as needed)
49+
impl_clampable_int!(u32, u64, i32, i64);
50+
51+
// Implement for DVec2 (component-wise clamping)
52+
use glam::DVec2;
53+
impl Clampable for DVec2 {
54+
#[inline(always)]
55+
fn clamp_hard_min(self, min: f64) -> Self {
56+
self.max(DVec2::splat(min))
57+
}
58+
#[inline(always)]
59+
fn clamp_hard_max(self, max: f64) -> Self {
60+
self.min(DVec2::splat(max))
61+
}
62+
}

node-graph/gcore/src/raster/adjustments.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1399,7 +1399,7 @@ async fn posterize<T: Adjust<Color>>(
13991399
)]
14001400
mut input: T,
14011401
#[default(4)]
1402-
#[min(2.)]
1402+
#[hard_min(2.)]
14031403
levels: u32,
14041404
) -> T {
14051405
input.adjust(|color| {
@@ -1435,6 +1435,7 @@ async fn exposure<T: Adjust<Color>>(
14351435
offset: f64,
14361436
#[default(1.)]
14371437
#[range((0.01, 10.))]
1438+
#[hard_min(0.0001)]
14381439
gamma_correction: f64,
14391440
) -> T {
14401441
input.adjust(|color| {

node-graph/gcore/src/raster/color.rs

+2
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,8 @@ impl Color {
930930

931931
#[inline(always)]
932932
pub fn gamma(&self, gamma: f32) -> Color {
933+
let gamma = gamma.max(0.0001);
934+
933935
// From https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-6-gamma-correction/
934936
let inverse_gamma = 1. / gamma;
935937
self.map_rgb(|c: f32| c.powf(inverse_gamma))

node-graph/gcore/src/vector/generator_nodes.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ fn regular_polygon<T: AsU64>(
101101
_: impl Ctx,
102102
_primary: (),
103103
#[default(6)]
104-
#[min(3.)]
104+
#[hard_min(3.)]
105105
#[implementations(u32, u64, f64)]
106106
sides: T,
107107
#[default(50)] radius: f64,
@@ -116,7 +116,7 @@ fn star<T: AsU64>(
116116
_: impl Ctx,
117117
_primary: (),
118118
#[default(5)]
119-
#[min(2.)]
119+
#[hard_min(2.)]
120120
#[implementations(u32, u64, f64)]
121121
sides: T,
122122
#[default(50)] radius: f64,
@@ -153,7 +153,7 @@ fn grid<T: GridSpacing>(
153153
_: impl Ctx,
154154
_primary: (),
155155
grid_type: GridType,
156-
#[min(0.)]
156+
#[hard_min(0.)]
157157
#[default(10)]
158158
#[implementations(f64, DVec2)]
159159
spacing: T,

node-graph/gcore/src/vector/vector_nodes.rs

+10-6
Original file line numberDiff line numberDiff line change
@@ -429,14 +429,18 @@ where
429429
async fn round_corners(
430430
_: impl Ctx,
431431
source: VectorDataTable,
432-
#[min(0.)]
432+
#[hard_min(0.)]
433433
#[default(10.)]
434434
radius: PixelLength,
435435
#[range((0., 1.))]
436+
#[hard_min(0.)]
437+
#[hard_max(1.)]
436438
#[default(0.5)]
437439
roundness: f64,
438440
#[default(100.)] edge_length_limit: Percentage,
439441
#[range((0., 180.))]
442+
#[hard_min(0.)]
443+
#[hard_max(180.)]
440444
#[default(5.)]
441445
min_angle_threshold: Angle,
442446
) -> VectorDataTable {
@@ -538,7 +542,7 @@ async fn spatial_merge_by_distance(
538542
_: impl Ctx,
539543
vector_data: VectorDataTable,
540544
#[default(0.1)]
541-
#[min(0.0001)]
545+
#[hard_min(0.0001)]
542546
distance: f64,
543547
) -> VectorDataTable {
544548
let vector_data_transform = vector_data.transform();
@@ -748,7 +752,7 @@ async fn remove_handles(
748752
_: impl Ctx,
749753
vector_data: VectorDataTable,
750754
#[default(10.)]
751-
#[min(0.)]
755+
#[soft_min(0.)]
752756
max_handle_distance: f64,
753757
) -> VectorDataTable {
754758
let vector_data_transform = vector_data.transform();
@@ -879,8 +883,8 @@ async fn generate_handles(
879883
// _: impl Ctx,
880884
// source: VectorDataTable,
881885
// #[default(1.)]
882-
// #[min(1.)]
883-
// #[max(8.)]
886+
// #[hard_min(1.)]
887+
// #[soft_max(8.)]
884888
// subdivisions: f64,
885889
// ) -> VectorDataTable {
886890
// let source_transform = source.transform();
@@ -1367,7 +1371,7 @@ async fn poisson_disk_points(
13671371
_: impl Ctx,
13681372
vector_data: VectorDataTable,
13691373
#[default(10.)]
1370-
#[min(0.01)]
1374+
#[hard_min(0.01)]
13711375
separation_disk_diameter: f64,
13721376
seed: SeedValue,
13731377
) -> VectorDataTable {

node-graph/gstd/src/filter.rs

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ async fn blur(
1212
image_frame: ImageFrameTable<Color>,
1313
/// The radius of the blur kernel.
1414
#[range((0., 100.))]
15+
#[hard_min(0.)]
1516
radius: PixelLength,
1617
/// Use a lower-quality box kernel instead of a circular Gaussian kernel. This is faster but produces boxy artifacts.
1718
box_blur: bool,

node-graph/gstd/src/image_color_palette.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use graphene_core::{Color, Ctx};
55
async fn image_color_palette(
66
_: impl Ctx,
77
image: ImageFrameTable<Color>,
8-
#[min(1.)]
9-
#[max(28.)]
8+
#[hard_min(1.)]
9+
#[soft_max(28.)]
1010
max_size: u32,
1111
) -> Vec<Color> {
1212
const GRID: f32 = 3.;

node-graph/node-macro/src/codegen.rs

+57-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::parsing::*;
22
use convert_case::{Case, Casing};
33
use proc_macro_crate::FoundCrate;
44
use proc_macro2::TokenStream as TokenStream2;
5-
use quote::{format_ident, quote};
5+
use quote::{format_ident, quote, quote_spanned};
66
use std::sync::atomic::AtomicU64;
77
use syn::punctuated::Punctuated;
88
use syn::spanned::Spanned;
@@ -134,14 +134,22 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
134134
let number_min_values: Vec<_> = fields
135135
.iter()
136136
.map(|field| match field {
137-
ParsedField::Regular { number_min: Some(number_min), .. } => quote!(Some(#number_min)),
137+
ParsedField::Regular { number_soft_min, number_hard_min, .. } => match (number_soft_min, number_hard_min) {
138+
(Some(soft_min), _) => quote!(Some(#soft_min)),
139+
(None, Some(hard_min)) => quote!(Some(#hard_min)),
140+
(None, None) => quote!(None),
141+
},
138142
_ => quote!(None),
139143
})
140144
.collect();
141145
let number_max_values: Vec<_> = fields
142146
.iter()
143147
.map(|field| match field {
144-
ParsedField::Regular { number_max: Some(number_max), .. } => quote!(Some(#number_max)),
148+
ParsedField::Regular { number_soft_max, number_hard_max, .. } => match (number_soft_max, number_hard_max) {
149+
(Some(soft_max), _) => quote!(Some(#soft_max)),
150+
(None, Some(hard_max)) => quote!(Some(#hard_max)),
151+
(None, None) => quote!(None),
152+
},
145153
_ => quote!(None),
146154
})
147155
.collect();
@@ -175,6 +183,33 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
175183
}
176184
});
177185

186+
let min_max_args = fields.iter().map(|field| match field {
187+
ParsedField::Regular {
188+
pat_ident,
189+
number_hard_min,
190+
number_hard_max,
191+
..
192+
} => {
193+
let name = &pat_ident.ident;
194+
let mut tokens = quote!();
195+
if let Some(min) = number_hard_min {
196+
tokens.extend(quote_spanned! {min.span()=>
197+
let #name = #graphene_core::misc::Clampable::clamp_hard_min(#name, #min);
198+
});
199+
}
200+
201+
if let Some(max) = number_hard_max {
202+
tokens.extend(quote_spanned! {max.span()=>
203+
let #name = #graphene_core::misc::Clampable::clamp_hard_max(#name, #max);
204+
});
205+
}
206+
tokens
207+
}
208+
ParsedField::Node { .. } => {
209+
quote!()
210+
}
211+
});
212+
178213
let all_implementation_types = fields.iter().flat_map(|field| match field {
179214
ParsedField::Regular { implementations, .. } => implementations.into_iter().cloned().collect::<Vec<_>>(),
180215
ParsedField::Node { implementations, .. } => implementations
@@ -186,13 +221,27 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
186221

187222
let input_type = &parsed.input.ty;
188223
let mut clauses = Vec::new();
224+
let mut clampable_clauses = Vec::new();
225+
189226
for (field, name) in fields.iter().zip(struct_generics.iter()) {
190227
clauses.push(match (field, *is_async) {
191-
(ParsedField::Regular { ty, .. }, _) => {
228+
(
229+
ParsedField::Regular {
230+
ty, number_hard_min, number_hard_max, ..
231+
},
232+
_,
233+
) => {
192234
let all_lifetime_ty = substitute_lifetimes(ty.clone(), "all");
193235
let id = future_idents.len();
194236
let fut_ident = format_ident!("F{}", id);
195237
future_idents.push(fut_ident.clone());
238+
239+
// Add Clampable bound if this field uses hard_min or hard_max
240+
if number_hard_min.is_some() || number_hard_max.is_some() {
241+
// The bound applies to the Output type of the future, which is #ty
242+
clampable_clauses.push(quote!(#ty: #graphene_core::misc::Clampable));
243+
}
244+
196245
quote!(
197246
#fut_ident: core::future::Future<Output = #ty> + #graphene_core::WasmNotSend + 'n,
198247
for<'all> #all_lifetime_ty: #graphene_core::WasmNotSend,
@@ -220,6 +269,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
220269
let mut struct_where_clause = where_clause.clone();
221270
let extra_where: Punctuated<WherePredicate, Comma> = parse_quote!(
222271
#(#clauses,)*
272+
#(#clampable_clauses,)*
223273
#output_type: 'n,
224274
);
225275
struct_where_clause.predicates.extend(extra_where);
@@ -236,7 +286,10 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
236286
#[inline]
237287
fn eval(&'n self, __input: #input_type) -> Self::Output {
238288
Box::pin(async move {
289+
use #graphene_core::misc::Clampable;
290+
239291
#(#eval_args)*
292+
#(#min_max_args)*
240293
self::#fn_name(__input #(, #field_names)*) #await_keyword
241294
})
242295
}

0 commit comments

Comments
 (0)