From 78d02736d9373fd4e7c4fcf9e74683331e5e635e Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 22 Jul 2026 18:11:24 -0700 Subject: [PATCH 1/3] Add Vec2 support to the scalar math operator nodes --- node-graph/nodes/math/src/lib.rs | 377 +++++++++++++++++++++++-------- 1 file changed, 277 insertions(+), 100 deletions(-) diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index fd5d825a07..362b52558e 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -11,7 +11,6 @@ use log::warn; use math_parser::ast; use math_parser::context::{EvalContext, NothingMap, ValueProvider}; use math_parser::value::{Number, Value}; -use num_traits::Pow; use rand::{Rng, SeedableRng}; use std::ops::{Add, Mul, Rem, Sub}; use vector_types::Gradient; @@ -194,20 +193,38 @@ fn divide, B>( Item::from_parts(numerator.safe_divide(denominator.into_element()), attributes) } +trait Componentwise { + fn componentwise(self, f: impl Fn(f64) -> f64) -> Self; +} +impl Componentwise for f64 { + fn componentwise(self, f: impl Fn(f64) -> f64) -> Self { + f(self) + } +} +impl Componentwise for f32 { + fn componentwise(self, f: impl Fn(f64) -> f64) -> Self { + f(self as f64) as f32 + } +} +impl Componentwise for DVec2 { + fn componentwise(self, f: impl Fn(f64) -> f64) -> Self { + DVec2::new(f(self.x), f(self.y)) + } +} + /// The reciprocal operation (`1/x`) calculates the multiplicative inverse of a number. /// -/// Produces 0 if the input is 0. +/// Produces 0 if the input is 0. With a vec2 input, this applies separately to the X and Y components. #[node_macro::node(category("Math: Arithmetic"))] -fn reciprocal( +fn reciprocal( _: impl Ctx, /// The number for which the reciprocal is calculated. - #[implementations(f64, f32)] + #[implementations(f64, f32, DVec2)] value: Item, ) -> Item { let (value, attributes) = value.into_parts(); - let result = if value == T::from(0.).unwrap() { T::from(0.).unwrap() } else { T::from(1.).unwrap() / value }; - Item::from_parts(result, attributes) + Item::from_parts(value.componentwise(|value| if value == 0. { 0. } else { 1. / value }), attributes) } /// The modulo operation (`%`) calculates the remainder from the division of two scalar numbers or vectors. @@ -234,128 +251,210 @@ fn modulo>>, B: Copy Item::from_parts(result, attributes) } +trait Exponent { + fn power(self, power: Rhs) -> Self; +} +impl Exponent for f64 { + fn power(self, power: f64) -> Self { + self.powf(power) + } +} +impl Exponent for f32 { + fn power(self, power: f32) -> Self { + self.powf(power) + } +} +impl Exponent for u32 { + fn power(self, power: u32) -> Self { + self.pow(power) + } +} +impl Exponent for DVec2 { + fn power(self, power: DVec2) -> Self { + DVec2::new(self.x.powf(power.x), self.y.powf(power.y)) + } +} +impl Exponent for DVec2 { + fn power(self, power: f64) -> Self { + DVec2::new(self.x.powf(power), self.y.powf(power)) + } +} + /// The exponent operation (`^`) calculates the result of raising a number to a power. +/// +/// With a vec2 base, this applies separately to the X and Y components. #[node_macro::node(category("Math: Arithmetic"))] -fn exponent>( +fn exponent, B>( _: impl Ctx, /// The base number that is raised to the power. - #[implementations(f64, f32, u32)] - base: Item, + #[implementations(f64, f32, u32, DVec2, DVec2)] + base: Item, /// The power to which the base number is raised. - #[implementations(f64, f32, u32)] + #[implementations(f64, f32, u32, DVec2, f64)] #[default(2.)] - power: Item, -) -> Item<>::Output> { + power: Item, +) -> Item { let (base, attributes) = base.into_parts(); - Item::from_parts(base.pow(power.into_element()), attributes) + Item::from_parts(base.power(power.into_element()), attributes) +} + +fn scalar_nth_root(radicand: f64, degree: f64) -> f64 { + if degree == 2. { + radicand.sqrt() + } else if degree == 3. { + radicand.cbrt() + } else if degree <= 0. { + 0. + } else { + radicand.powf(1. / degree) + } +} + +trait NthRoot { + fn nth_root(self, degree: Degree) -> Self; +} +impl NthRoot for f64 { + fn nth_root(self, degree: f64) -> Self { + scalar_nth_root(self, degree) + } +} +impl NthRoot for f32 { + fn nth_root(self, degree: f32) -> Self { + scalar_nth_root(self as f64, degree as f64) as f32 + } +} +impl NthRoot for DVec2 { + fn nth_root(self, degree: f64) -> Self { + DVec2::new(scalar_nth_root(self.x, degree), scalar_nth_root(self.y, degree)) + } } /// The `n`th root operation (`√`) calculates the inverse of exponentiation. Square root inverts squaring, cube root inverts cubing, and so on. /// -/// This is equivalent to raising the number to the power of `1/n`. +/// This is equivalent to raising the number to the power of `1/n`. With a vec2 radicand, this applies separately to the X and Y components. #[node_macro::node(category("Math: Arithmetic"))] -fn root( +fn root, B>( _: impl Ctx, /// The number inside the radical for which the `n`th root is calculated. #[default(2.)] - #[implementations(f64, f32)] - radicand: Item, + #[implementations(f64, f32, DVec2)] + radicand: Item, /// The degree of the root to be calculated. Square root is 2, cube root is 3, and so on. /// Degrees 0 or less are invalid and will produce an output of 0. #[default(2.)] - #[implementations(f64, f32)] - degree: Item, -) -> Item { + #[implementations(f64, f32, f64)] + degree: Item, +) -> Item { let (radicand, attributes) = radicand.into_parts(); - let degree = *degree.element(); - let result = if degree == T::from(2.).unwrap() { - radicand.sqrt() - } else if degree == T::from(3.).unwrap() { - radicand.cbrt() - } else if degree <= T::from(0.).unwrap() { - T::from(0.).unwrap() + Item::from_parts(radicand.nth_root(degree.into_element()), attributes) +} + +fn scalar_logarithm(value: f64, base: f64) -> f64 { + if base == 2. { + value.log2() + } else if base == 10. { + value.log10() + } else if (base - std::f64::consts::E).abs() < f64::EPSILON * 1e6 { + value.ln() } else { - radicand.powf(T::from(1.).unwrap() / degree) - }; + value.log(base) + } +} - Item::from_parts(result, attributes) +trait Logarithm { + fn logarithm(self, base: Base) -> Self; +} +impl Logarithm for f64 { + fn logarithm(self, base: f64) -> Self { + scalar_logarithm(self, base) + } +} +impl Logarithm for f32 { + fn logarithm(self, base: f32) -> Self { + scalar_logarithm(self as f64, base as f64) as f32 + } +} +impl Logarithm for DVec2 { + fn logarithm(self, base: f64) -> Self { + DVec2::new(scalar_logarithm(self.x, base), scalar_logarithm(self.y, base)) + } } /// The logarithmic function (`log`) calculates the logarithm of a number with a specified base. If the natural logarithm function (`ln`) is desired, set the base to "e". +/// +/// With a vec2 input, this applies separately to the X and Y components. #[node_macro::node(category("Math: Arithmetic"))] -fn logarithm( +fn logarithm, B>( _: impl Ctx, /// The number for which the logarithm is calculated. - #[implementations(f64, f32)] - value: Item, + #[implementations(f64, f32, DVec2)] + value: Item, /// The base of the logarithm, such as 2 (binary), 10 (decimal), and e (natural logarithm). #[default(2.)] - #[implementations(f64, f32)] - base: Item, -) -> Item { + #[implementations(f64, f32, f64)] + base: Item, +) -> Item { let (value, attributes) = value.into_parts(); - let base = *base.element(); - let result = if base == T::from(2.).unwrap() { - value.log2() - } else if base == T::from(10.).unwrap() { - value.log10() - } else if (base - T::from(std::f64::consts::E).unwrap()).abs() < T::epsilon() * T::from(1e6).unwrap() { - value.ln() - } else { - value.log(base) - }; - - Item::from_parts(result, attributes) + Item::from_parts(value.logarithm(base.into_element()), attributes) } /// The sine trigonometric function (`sin`) calculates the ratio of the angle's opposite side length to its hypotenuse length. +/// +/// With a vec2 input, this applies separately to the X and Y components. #[node_macro::node(category("Math: Trig"))] -fn sine( +fn sine( _: impl Ctx, /// The given angle. - #[implementations(f64, f32)] + #[implementations(f64, f32, DVec2)] theta: Item, /// Whether the given angle should be interpreted as radians instead of degrees. radians: Item, ) -> Item { let (theta, attributes) = theta.into_parts(); + let radians = *radians.element(); - let result = if *radians.element() { theta.sin() } else { theta.to_radians().sin() }; + let result = theta.componentwise(|theta| if radians { theta.sin() } else { theta.to_radians().sin() }); Item::from_parts(result, attributes) } /// The cosine trigonometric function (`cos`) calculates the ratio of the angle's adjacent side length to its hypotenuse length. +/// +/// With a vec2 input, this applies separately to the X and Y components. #[node_macro::node(category("Math: Trig"))] -fn cosine( +fn cosine( _: impl Ctx, /// The given angle. - #[implementations(f64, f32)] + #[implementations(f64, f32, DVec2)] theta: Item, /// Whether the given angle should be interpreted as radians instead of degrees. radians: Item, ) -> Item { let (theta, attributes) = theta.into_parts(); + let radians = *radians.element(); - let result = if *radians.element() { theta.cos() } else { theta.to_radians().cos() }; + let result = theta.componentwise(|theta| if radians { theta.cos() } else { theta.to_radians().cos() }); Item::from_parts(result, attributes) } /// The tangent trigonometric function (`tan`) calculates the ratio of the angle's opposite side length to its adjacent side length. +/// +/// With a vec2 input, this applies separately to the X and Y components. #[node_macro::node(category("Math: Trig"))] -fn tangent( +fn tangent( _: impl Ctx, /// The given angle. - #[implementations(f64, f32)] + #[implementations(f64, f32, DVec2)] theta: Item, /// Whether the given angle should be interpreted as radians instead of degrees. radians: Item, ) -> Item { let (theta, attributes) = theta.into_parts(); + let radians = *radians.element(); - let result = if *radians.element() { theta.tan() } else { theta.to_radians().tan() }; + let result = theta.componentwise(|theta| if radians { theta.tan() } else { theta.to_radians().tan() }); Item::from_parts(result, attributes) } @@ -532,42 +631,48 @@ fn as_f64(_: impl Ctx, value: Item) -> Item { } /// The rounding function (`round`) maps an input value to its nearest whole number. Halfway values are rounded away from zero. +/// +/// With a vec2 input, this applies separately to the X and Y components. #[node_macro::node(category("Math: Numeric"))] -fn round( +fn round( _: impl Ctx, /// The number to be rounded to the nearest whole number. - #[implementations(f64, f32)] + #[implementations(f64, f32, DVec2)] value: Item, ) -> Item { let (value, attributes) = value.into_parts(); - Item::from_parts(value.round(), attributes) + Item::from_parts(value.componentwise(f64::round), attributes) } /// The floor function (`floor`) rounds down an input value to the nearest whole number, unless the input number is already whole. +/// +/// With a vec2 input, this applies separately to the X and Y components. #[node_macro::node(category("Math: Numeric"))] -fn floor( +fn floor( _: impl Ctx, /// The number to be rounded down. - #[implementations(f64, f32)] + #[implementations(f64, f32, DVec2)] value: Item, ) -> Item { let (value, attributes) = value.into_parts(); - Item::from_parts(value.floor(), attributes) + Item::from_parts(value.componentwise(f64::floor), attributes) } /// The ceiling function (`ceil`) rounds up an input value to the nearest whole number, unless the input number is already whole. +/// +/// With a vec2 input, this applies separately to the X and Y components. #[node_macro::node(category("Math: Numeric"))] -fn ceiling( +fn ceiling( _: impl Ctx, /// The number to be rounded up. - #[implementations(f64, f32)] + #[implementations(f64, f32, DVec2)] value: Item, ) -> Item { let (value, attributes) = value.into_parts(); - Item::from_parts(value.ceil(), attributes) + Item::from_parts(value.componentwise(f64::ceil), attributes) } trait AbsoluteValue { @@ -600,6 +705,8 @@ impl AbsoluteValue for i64 { } /// The absolute value function (`abs`) removes the negative sign from an input value, if present. +/// +/// With a vec2 input, this applies separately to the X and Y components. For the overall length of a vec2, see the "Magnitude" node instead. #[node_macro::node(category("Math: Numeric"))] fn absolute_value( _: impl Ctx, @@ -612,68 +719,117 @@ fn absolute_value( Item::from_parts(value.abs(), attributes) } +trait MinMax { + fn minimum(self, other: Rhs) -> Self; + fn maximum(self, other: Rhs) -> Self; +} +impl MinMax for f64 { + fn minimum(self, other: f64) -> Self { + if self < other { self } else { other } + } + fn maximum(self, other: f64) -> Self { + if self > other { self } else { other } + } +} +impl MinMax for f32 { + fn minimum(self, other: f32) -> Self { + if self < other { self } else { other } + } + fn maximum(self, other: f32) -> Self { + if self > other { self } else { other } + } +} +impl MinMax for u32 { + fn minimum(self, other: u32) -> Self { + if self < other { self } else { other } + } + fn maximum(self, other: u32) -> Self { + if self > other { self } else { other } + } +} +impl MinMax for String { + fn minimum(self, other: Self) -> Self { + if self < other { self } else { other } + } + fn maximum(self, other: Self) -> Self { + if self > other { self } else { other } + } +} +impl MinMax for DVec2 { + fn minimum(self, other: DVec2) -> Self { + self.min(other) + } + fn maximum(self, other: DVec2) -> Self { + self.max(other) + } +} +impl MinMax for DVec2 { + fn minimum(self, other: f64) -> Self { + self.min(DVec2::splat(other)) + } + fn maximum(self, other: f64) -> Self { + self.max(DVec2::splat(other)) + } +} + /// The minimum function (`min`) picks the smaller of two numbers. +/// +/// With vec2 inputs, this applies separately to the X and Y components. #[node_macro::node(category("Math: Numeric"))] -fn min( +fn min, B>( _: impl Ctx, /// One of the two numbers, of which the lesser is returned. - #[implementations(f64, f32, u32, String)] - value: Item, + #[implementations(f64, f32, u32, String, DVec2, DVec2)] + value: Item, /// The other of the two numbers, of which the lesser is returned. - #[implementations(f64, f32, u32, String)] - other_value: Item, -) -> Item { + #[implementations(f64, f32, u32, String, DVec2, f64)] + other_value: Item, +) -> Item { let (value, attributes) = value.into_parts(); - let other_value = other_value.into_element(); - Item::from_parts(if value < other_value { value } else { other_value }, attributes) + Item::from_parts(value.minimum(other_value.into_element()), attributes) } /// The maximum function (`max`) picks the larger of two numbers. +/// +/// With vec2 inputs, this applies separately to the X and Y components. #[node_macro::node(category("Math: Numeric"))] -fn max( +fn max, B>( _: impl Ctx, /// One of the two numbers, of which the greater is returned. - #[implementations(f64, f32, u32, String)] - value: Item, + #[implementations(f64, f32, u32, String, DVec2, DVec2)] + value: Item, /// The other of the two numbers, of which the greater is returned. - #[implementations(f64, f32, u32, String)] - other_value: Item, -) -> Item { + #[implementations(f64, f32, u32, String, DVec2, f64)] + other_value: Item, +) -> Item { let (value, attributes) = value.into_parts(); - let other_value = other_value.into_element(); - Item::from_parts(if value > other_value { value } else { other_value }, attributes) + Item::from_parts(value.maximum(other_value.into_element()), attributes) } /// The clamp function (`clamp`) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed. +/// +/// With vec2 inputs, this applies separately to the X and Y components. #[node_macro::node(category("Math: Numeric"))] -fn clamp( +fn clamp, B: MinMax + Clone>( _: impl Ctx, /// The number to be clamped, which is restricted to the range between the minimum and maximum values. - #[implementations(f64, f32, u32, String)] - value: Item, + #[implementations(f64, f32, u32, String, DVec2, DVec2)] + value: Item, /// The left (smaller) side of the range. The output is never less than this number. - #[implementations(f64, f32, u32, String)] - min: Item, + #[implementations(f64, f32, u32, String, DVec2, f64)] + min: Item, /// The right (greater) side of the range. The output is never greater than this number. - #[implementations(f64, f32, u32, String)] + #[implementations(f64, f32, u32, String, DVec2, f64)] #[default(1)] - max: Item, -) -> Item { + max: Item, +) -> Item { let (value, attributes) = value.into_parts(); let (min, max) = (min.into_element(), max.into_element()); - let (min, max) = if min < max { (min, max) } else { (max, min) }; - let result = if value < min { - min - } else if value > max { - max - } else { - value - }; - - Item::from_parts(result, attributes) + let (min, max) = (min.clone().minimum(max.clone()), min.maximum(max)); + Item::from_parts(value.maximum(min).minimum(max), attributes) } /// The greatest common divisor (GCD) calculates the largest positive integer that divides both of the two input numbers without leaving a remainder. @@ -1242,6 +1398,27 @@ mod test { assert_eq!(magnitude((), vector).into_element(), 5.); } + #[test] + pub fn clamp_vec2_within_swapped_bounds() { + let vec2 = |x, y| Item::new_from_element(DVec2::new(x, y)); + assert_eq!(clamp((), vec2(-5., 5.), vec2(1., 1.), vec2(0., 2.)).into_element(), DVec2::new(0., 2.)); + } + + #[test] + pub fn min_max_vec2_with_scalar() { + let vec2 = |x, y| Item::new_from_element(DVec2::new(x, y)); + assert_eq!(super::min((), vec2(-5., 5.), Item::new_from_element(0_f64)).into_element(), DVec2::new(-5., 0.)); + assert_eq!(super::max((), vec2(-5., 5.), Item::new_from_element(0_f64)).into_element(), DVec2::new(0., 5.)); + } + + #[test] + pub fn round_floor_ceiling_vec2() { + let vec2 = |x, y| Item::new_from_element(DVec2::new(x, y)); + assert_eq!(round((), vec2(1.5, -1.4)).into_element(), DVec2::new(2., -1.)); + assert_eq!(floor((), vec2(1.9, -1.1)).into_element(), DVec2::new(1., -2.)); + assert_eq!(ceiling((), vec2(1.1, -1.9)).into_element(), DVec2::new(2., -1.)); + } + #[test] fn test_basic_expression() { let result = math((), Item::new_from_element(0.), Item::new_from_element("2 + 2".to_string()), Item::new_from_element(0.)); From fb96458d94c1966cc1e0b59407b5c1b9edf87899 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 22 Jul 2026 18:59:39 -0700 Subject: [PATCH 2/3] Support the scalar-with-Vec2 operand order in the Exponent, Root, Logarithm, Min, Max, and Clamp nodes --- node-graph/nodes/math/src/lib.rs | 191 ++++++++++++++++++++++--------- 1 file changed, 136 insertions(+), 55 deletions(-) diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 362b52558e..9097c4144f 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -251,49 +251,61 @@ fn modulo>>, B: Copy Item::from_parts(result, attributes) } -trait Exponent { - fn power(self, power: Rhs) -> Self; +pub trait Exponent { + type Output; + fn power(self, power: Rhs) -> Self::Output; } impl Exponent for f64 { - fn power(self, power: f64) -> Self { + type Output = f64; + fn power(self, power: f64) -> f64 { self.powf(power) } } impl Exponent for f32 { - fn power(self, power: f32) -> Self { + type Output = f32; + fn power(self, power: f32) -> f32 { self.powf(power) } } impl Exponent for u32 { - fn power(self, power: u32) -> Self { + type Output = u32; + fn power(self, power: u32) -> u32 { self.pow(power) } } impl Exponent for DVec2 { - fn power(self, power: DVec2) -> Self { + type Output = DVec2; + fn power(self, power: DVec2) -> DVec2 { DVec2::new(self.x.powf(power.x), self.y.powf(power.y)) } } impl Exponent for DVec2 { - fn power(self, power: f64) -> Self { + type Output = DVec2; + fn power(self, power: f64) -> DVec2 { DVec2::new(self.x.powf(power), self.y.powf(power)) } } +impl Exponent for f64 { + type Output = DVec2; + fn power(self, power: DVec2) -> DVec2 { + DVec2::new(self.powf(power.x), self.powf(power.y)) + } +} /// The exponent operation (`^`) calculates the result of raising a number to a power. /// -/// With a vec2 base, this applies separately to the X and Y components. +/// With vec2 inputs, this applies separately to the X and Y components. #[node_macro::node(category("Math: Arithmetic"))] fn exponent, B>( _: impl Ctx, /// The base number that is raised to the power. - #[implementations(f64, f32, u32, DVec2, DVec2)] + #[implementations(f64, f32, u32, DVec2, DVec2, f64)] base: Item, /// The power to which the base number is raised. - #[implementations(f64, f32, u32, DVec2, f64)] + #[implementations(f64, f32, u32, DVec2, f64, DVec2)] #[default(2.)] power: Item, -) -> Item { +) -> Item<>::Output> { let (base, attributes) = base.into_parts(); Item::from_parts(base.power(power.into_element()), attributes) @@ -311,41 +323,57 @@ fn scalar_nth_root(radicand: f64, degree: f64) -> f64 { } } -trait NthRoot { - fn nth_root(self, degree: Degree) -> Self; +pub trait NthRoot { + type Output; + fn nth_root(self, degree: Degree) -> Self::Output; } impl NthRoot for f64 { - fn nth_root(self, degree: f64) -> Self { + type Output = f64; + fn nth_root(self, degree: f64) -> f64 { scalar_nth_root(self, degree) } } impl NthRoot for f32 { - fn nth_root(self, degree: f32) -> Self { + type Output = f32; + fn nth_root(self, degree: f32) -> f32 { scalar_nth_root(self as f64, degree as f64) as f32 } } +impl NthRoot for DVec2 { + type Output = DVec2; + fn nth_root(self, degree: DVec2) -> DVec2 { + DVec2::new(scalar_nth_root(self.x, degree.x), scalar_nth_root(self.y, degree.y)) + } +} impl NthRoot for DVec2 { - fn nth_root(self, degree: f64) -> Self { + type Output = DVec2; + fn nth_root(self, degree: f64) -> DVec2 { DVec2::new(scalar_nth_root(self.x, degree), scalar_nth_root(self.y, degree)) } } +impl NthRoot for f64 { + type Output = DVec2; + fn nth_root(self, degree: DVec2) -> DVec2 { + DVec2::new(scalar_nth_root(self, degree.x), scalar_nth_root(self, degree.y)) + } +} /// The `n`th root operation (`√`) calculates the inverse of exponentiation. Square root inverts squaring, cube root inverts cubing, and so on. /// -/// This is equivalent to raising the number to the power of `1/n`. With a vec2 radicand, this applies separately to the X and Y components. +/// This is equivalent to raising the number to the power of `1/n`. With vec2 inputs, this applies separately to the X and Y components. #[node_macro::node(category("Math: Arithmetic"))] fn root, B>( _: impl Ctx, /// The number inside the radical for which the `n`th root is calculated. #[default(2.)] - #[implementations(f64, f32, DVec2)] + #[implementations(f64, f32, DVec2, DVec2, f64)] radicand: Item, /// The degree of the root to be calculated. Square root is 2, cube root is 3, and so on. /// Degrees 0 or less are invalid and will produce an output of 0. #[default(2.)] - #[implementations(f64, f32, f64)] + #[implementations(f64, f32, f64, DVec2, DVec2)] degree: Item, -) -> Item { +) -> Item<>::Output> { let (radicand, attributes) = radicand.into_parts(); Item::from_parts(radicand.nth_root(degree.into_element()), attributes) @@ -363,39 +391,55 @@ fn scalar_logarithm(value: f64, base: f64) -> f64 { } } -trait Logarithm { - fn logarithm(self, base: Base) -> Self; +pub trait Logarithm { + type Output; + fn logarithm(self, base: Base) -> Self::Output; } impl Logarithm for f64 { - fn logarithm(self, base: f64) -> Self { + type Output = f64; + fn logarithm(self, base: f64) -> f64 { scalar_logarithm(self, base) } } impl Logarithm for f32 { - fn logarithm(self, base: f32) -> Self { + type Output = f32; + fn logarithm(self, base: f32) -> f32 { scalar_logarithm(self as f64, base as f64) as f32 } } +impl Logarithm for DVec2 { + type Output = DVec2; + fn logarithm(self, base: DVec2) -> DVec2 { + DVec2::new(scalar_logarithm(self.x, base.x), scalar_logarithm(self.y, base.y)) + } +} impl Logarithm for DVec2 { - fn logarithm(self, base: f64) -> Self { + type Output = DVec2; + fn logarithm(self, base: f64) -> DVec2 { DVec2::new(scalar_logarithm(self.x, base), scalar_logarithm(self.y, base)) } } +impl Logarithm for f64 { + type Output = DVec2; + fn logarithm(self, base: DVec2) -> DVec2 { + DVec2::new(scalar_logarithm(self, base.x), scalar_logarithm(self, base.y)) + } +} /// The logarithmic function (`log`) calculates the logarithm of a number with a specified base. If the natural logarithm function (`ln`) is desired, set the base to "e". /// -/// With a vec2 input, this applies separately to the X and Y components. +/// With vec2 inputs, this applies separately to the X and Y components. #[node_macro::node(category("Math: Arithmetic"))] fn logarithm, B>( _: impl Ctx, /// The number for which the logarithm is calculated. - #[implementations(f64, f32, DVec2)] + #[implementations(f64, f32, DVec2, DVec2, f64)] value: Item, /// The base of the logarithm, such as 2 (binary), 10 (decimal), and e (natural logarithm). #[default(2.)] - #[implementations(f64, f32, f64)] + #[implementations(f64, f32, f64, DVec2, DVec2)] base: Item, -) -> Item { +) -> Item<>::Output> { let (value, attributes) = value.into_parts(); Item::from_parts(value.logarithm(base.into_element()), attributes) @@ -719,58 +763,74 @@ fn absolute_value( Item::from_parts(value.abs(), attributes) } -trait MinMax { - fn minimum(self, other: Rhs) -> Self; - fn maximum(self, other: Rhs) -> Self; +pub trait MinMax { + type Output; + fn minimum(self, other: Rhs) -> Self::Output; + fn maximum(self, other: Rhs) -> Self::Output; } impl MinMax for f64 { - fn minimum(self, other: f64) -> Self { + type Output = f64; + fn minimum(self, other: f64) -> f64 { if self < other { self } else { other } } - fn maximum(self, other: f64) -> Self { + fn maximum(self, other: f64) -> f64 { if self > other { self } else { other } } } impl MinMax for f32 { - fn minimum(self, other: f32) -> Self { + type Output = f32; + fn minimum(self, other: f32) -> f32 { if self < other { self } else { other } } - fn maximum(self, other: f32) -> Self { + fn maximum(self, other: f32) -> f32 { if self > other { self } else { other } } } impl MinMax for u32 { - fn minimum(self, other: u32) -> Self { + type Output = u32; + fn minimum(self, other: u32) -> u32 { if self < other { self } else { other } } - fn maximum(self, other: u32) -> Self { + fn maximum(self, other: u32) -> u32 { if self > other { self } else { other } } } impl MinMax for String { - fn minimum(self, other: Self) -> Self { + type Output = String; + fn minimum(self, other: Self) -> String { if self < other { self } else { other } } - fn maximum(self, other: Self) -> Self { + fn maximum(self, other: Self) -> String { if self > other { self } else { other } } } impl MinMax for DVec2 { - fn minimum(self, other: DVec2) -> Self { + type Output = DVec2; + fn minimum(self, other: DVec2) -> DVec2 { self.min(other) } - fn maximum(self, other: DVec2) -> Self { + fn maximum(self, other: DVec2) -> DVec2 { self.max(other) } } impl MinMax for DVec2 { - fn minimum(self, other: f64) -> Self { + type Output = DVec2; + fn minimum(self, other: f64) -> DVec2 { self.min(DVec2::splat(other)) } - fn maximum(self, other: f64) -> Self { + fn maximum(self, other: f64) -> DVec2 { self.max(DVec2::splat(other)) } } +impl MinMax for f64 { + type Output = DVec2; + fn minimum(self, other: DVec2) -> DVec2 { + DVec2::splat(self).min(other) + } + fn maximum(self, other: DVec2) -> DVec2 { + DVec2::splat(self).max(other) + } +} /// The minimum function (`min`) picks the smaller of two numbers. /// @@ -779,12 +839,12 @@ impl MinMax for DVec2 { fn min, B>( _: impl Ctx, /// One of the two numbers, of which the lesser is returned. - #[implementations(f64, f32, u32, String, DVec2, DVec2)] + #[implementations(f64, f32, u32, String, DVec2, DVec2, f64)] value: Item, /// The other of the two numbers, of which the lesser is returned. - #[implementations(f64, f32, u32, String, DVec2, f64)] + #[implementations(f64, f32, u32, String, DVec2, f64, DVec2)] other_value: Item, -) -> Item { +) -> Item<>::Output> { let (value, attributes) = value.into_parts(); Item::from_parts(value.minimum(other_value.into_element()), attributes) @@ -797,12 +857,12 @@ fn min, B>( fn max, B>( _: impl Ctx, /// One of the two numbers, of which the greater is returned. - #[implementations(f64, f32, u32, String, DVec2, DVec2)] + #[implementations(f64, f32, u32, String, DVec2, DVec2, f64)] value: Item, /// The other of the two numbers, of which the greater is returned. - #[implementations(f64, f32, u32, String, DVec2, f64)] + #[implementations(f64, f32, u32, String, DVec2, f64, DVec2)] other_value: Item, -) -> Item { +) -> Item<>::Output> { let (value, attributes) = value.into_parts(); Item::from_parts(value.maximum(other_value.into_element()), attributes) @@ -812,19 +872,22 @@ fn max, B>( /// /// With vec2 inputs, this applies separately to the X and Y components. #[node_macro::node(category("Math: Numeric"))] -fn clamp, B: MinMax + Clone>( +fn clamp, B: MinMax + Clone>( _: impl Ctx, /// The number to be clamped, which is restricted to the range between the minimum and maximum values. - #[implementations(f64, f32, u32, String, DVec2, DVec2)] + #[implementations(f64, f32, u32, String, DVec2, DVec2, f64)] value: Item, /// The left (smaller) side of the range. The output is never less than this number. - #[implementations(f64, f32, u32, String, DVec2, f64)] + #[implementations(f64, f32, u32, String, DVec2, f64, DVec2)] min: Item, /// The right (greater) side of the range. The output is never greater than this number. - #[implementations(f64, f32, u32, String, DVec2, f64)] + #[implementations(f64, f32, u32, String, DVec2, f64, DVec2)] #[default(1)] max: Item, -) -> Item { +) -> Item<>::Output> +where + >::Output: MinMax>::Output>, +{ let (value, attributes) = value.into_parts(); let (min, max) = (min.into_element(), max.into_element()); @@ -1411,6 +1474,24 @@ mod test { assert_eq!(super::max((), vec2(-5., 5.), Item::new_from_element(0_f64)).into_element(), DVec2::new(0., 5.)); } + #[test] + pub fn scalar_with_vec2_operand_orders() { + let vec2 = |x, y| Item::new_from_element(DVec2::new(x, y)); + assert_eq!(super::min((), Item::new_from_element(0_f64), vec2(-5., 5.)).into_element(), DVec2::new(-5., 0.)); + assert_eq!(super::max((), Item::new_from_element(0_f64), vec2(-5., 5.)).into_element(), DVec2::new(0., 5.)); + assert_eq!(exponent((), Item::new_from_element(2_f64), vec2(2., 3.)).into_element(), DVec2::new(4., 8.)); + assert_eq!(root((), Item::new_from_element(64_f64), vec2(2., 3.)).into_element(), DVec2::new(8., 4.)); + assert_eq!(logarithm((), Item::new_from_element(8_f64), vec2(2., 10.)).into_element(), DVec2::new(3., 8_f64.log10())); + assert_eq!(clamp((), Item::new_from_element(5_f64), vec2(0., 6.), vec2(1., 10.)).into_element(), DVec2::new(1., 6.)); + } + + #[test] + pub fn vec2_degrees_and_bases() { + let vec2 = |x, y| Item::new_from_element(DVec2::new(x, y)); + assert_eq!(root((), vec2(64., 27.), vec2(2., 3.)).into_element(), DVec2::new(8., 3.)); + assert_eq!(logarithm((), vec2(8., 100.), vec2(2., 10.)).into_element(), DVec2::new(3., 2.)); + } + #[test] pub fn round_floor_ceiling_vec2() { let vec2 = |x, y| Item::new_from_element(DVec2::new(x, y)); From a0c3511799c288b2f13efc1f51836ea7d0e84213 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 22 Jul 2026 19:25:31 -0700 Subject: [PATCH 3/3] Detect an f32 base of e in the Logarithm node despite inexact widening to f64 --- node-graph/nodes/math/src/lib.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 9097c4144f..6ecb11740b 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -404,7 +404,13 @@ impl Logarithm for f64 { impl Logarithm for f32 { type Output = f32; fn logarithm(self, base: f32) -> f32 { - scalar_logarithm(self as f64, base as f64) as f32 + // The f32 representation of e widens inexactly, so match it against e at f32 precision and substitute the exact f64 e + let base = if (base - std::f32::consts::E).abs() < f32::EPSILON * 10. { + std::f64::consts::E + } else { + base as f64 + }; + scalar_logarithm(self as f64, base) as f32 } } impl Logarithm for DVec2 { @@ -1492,6 +1498,18 @@ mod test { assert_eq!(logarithm((), vec2(8., 100.), vec2(2., 10.)).into_element(), DVec2::new(3., 2.)); } + #[test] + pub fn logarithm_f32_base_e_and_near_e() { + assert_eq!( + logarithm((), Item::new_from_element(8_f32), Item::new_from_element(std::f32::consts::E)).into_element(), + 8_f64.ln() as f32 + ); + assert_eq!( + logarithm((), Item::new_from_element(8_f32), Item::new_from_element(2.7_f32)).into_element(), + 8_f64.log(2.7_f32 as f64) as f32 + ); + } + #[test] pub fn round_floor_ceiling_vec2() { let vec2 = |x, y| Item::new_from_element(DVec2::new(x, y));