Add Vec2 support to the scalar math operator nodes (#4360)

* Add Vec2 support to the scalar math operator nodes

* Support the scalar-with-Vec2 operand order in the Exponent, Root, Logarithm, Min, Max, and Clamp nodes

* Detect an f32 base of e in the Logarithm node despite inexact widening to f64
This commit is contained in:
Keavon Chambers
2026-07-22 19:42:02 -07:00
committed by Dennis Kobert
parent 5dbcdaf0c2
commit 28f8325a64

View File

@@ -8,7 +8,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;
@@ -179,17 +178,36 @@ fn divide<A: SafeDivide<B>, B>(
numerator.safe_divide(denominator)
}
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<T: num_traits::float::Float>(
fn reciprocal<T: Componentwise>(
_: impl Ctx,
/// The number for which the reciprocal is calculated.
#[implementations(f64, f32)]
#[implementations(f64, f32, DVec2)]
value: T,
) -> T {
if value == T::from(0.).unwrap() { T::from(0.).unwrap() } else { T::from(1.).unwrap() / value }
value.componentwise(|value| if value == 0. { 0. } else { 1. / value })
}
/// The modulo operation (`%`) calculates the remainder from the division of two scalar numbers or vectors.
@@ -212,108 +230,243 @@ fn modulo<A: Rem<B, Output: Add<B, Output: Rem<B, Output = A::Output>>>, B: Copy
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus }
}
pub trait Exponent<Rhs = Self> {
type Output;
fn power(self, power: Rhs) -> Self::Output;
}
impl Exponent for f64 {
type Output = f64;
fn power(self, power: f64) -> f64 {
self.powf(power)
}
}
impl Exponent for f32 {
type Output = f32;
fn power(self, power: f32) -> f32 {
self.powf(power)
}
}
impl Exponent for u32 {
type Output = u32;
fn power(self, power: u32) -> u32 {
self.pow(power)
}
}
impl Exponent for DVec2 {
type Output = DVec2;
fn power(self, power: DVec2) -> DVec2 {
DVec2::new(self.x.powf(power.x), self.y.powf(power.y))
}
}
impl Exponent<f64> for DVec2 {
type Output = DVec2;
fn power(self, power: f64) -> DVec2 {
DVec2::new(self.x.powf(power), self.y.powf(power))
}
}
impl Exponent<DVec2> 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 vec2 inputs, this applies separately to the X and Y components.
#[node_macro::node(category("Math: Arithmetic"))]
fn exponent<T: Pow<T>>(
fn exponent<A: Exponent<B>, B>(
_: impl Ctx,
/// The base number that is raised to the power.
#[implementations(f64, f32, u32)]
base: T,
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
base: A,
/// The power to which the base number is raised.
#[implementations(f64, f32, u32)]
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
#[default(2.)]
power: T,
) -> <T as num_traits::Pow<T>>::Output {
base.pow(power)
power: B,
) -> <A as Exponent<B>>::Output {
base.power(power)
}
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)
}
}
pub trait NthRoot<Degree = Self> {
type Output;
fn nth_root(self, degree: Degree) -> Self::Output;
}
impl NthRoot for f64 {
type Output = f64;
fn nth_root(self, degree: f64) -> f64 {
scalar_nth_root(self, degree)
}
}
impl NthRoot for f32 {
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<f64> for DVec2 {
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<DVec2> 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`.
/// 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<T: num_traits::float::Float>(
fn root<A: NthRoot<B>, B>(
_: impl Ctx,
/// The number inside the radical for which the `n`th root is calculated.
#[default(2.)]
#[implementations(f64, f32)]
radicand: T,
#[implementations(f64, f32, DVec2, DVec2, f64)]
radicand: A,
/// 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: T,
) -> T {
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()
} else {
radicand.powf(T::from(1.).unwrap() / degree)
}
#[implementations(f64, f32, f64, DVec2, DVec2)]
degree: B,
) -> <A as NthRoot<B>>::Output {
radicand.nth_root(degree)
}
/// 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".
#[node_macro::node(category("Math: Arithmetic"))]
fn logarithm<T: num_traits::float::Float>(
_: impl Ctx,
/// The number for which the logarithm is calculated.
#[implementations(f64, f32)]
value: T,
/// The base of the logarithm, such as 2 (binary), 10 (decimal), and e (natural logarithm).
#[default(2.)]
#[implementations(f64, f32)]
base: T,
) -> T {
if base == T::from(2.).unwrap() {
fn scalar_logarithm(value: f64, base: f64) -> f64 {
if base == 2. {
value.log2()
} else if base == T::from(10.).unwrap() {
} else if base == 10. {
value.log10()
} else if (base - T::from(std::f64::consts::E).unwrap()).abs() < T::epsilon() * T::from(1e6).unwrap() {
} else if (base - std::f64::consts::E).abs() < f64::EPSILON * 1e6 {
value.ln()
} else {
value.log(base)
}
}
pub trait Logarithm<Base = Self> {
type Output;
fn logarithm(self, base: Base) -> Self::Output;
}
impl Logarithm for f64 {
type Output = f64;
fn logarithm(self, base: f64) -> f64 {
scalar_logarithm(self, base)
}
}
impl Logarithm for f32 {
type Output = f32;
fn logarithm(self, base: f32) -> 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 {
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<f64> for DVec2 {
type Output = DVec2;
fn logarithm(self, base: f64) -> DVec2 {
DVec2::new(scalar_logarithm(self.x, base), scalar_logarithm(self.y, base))
}
}
impl Logarithm<DVec2> 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 vec2 inputs, this applies separately to the X and Y components.
#[node_macro::node(category("Math: Arithmetic"))]
fn logarithm<A: Logarithm<B>, B>(
_: impl Ctx,
/// The number for which the logarithm is calculated.
#[implementations(f64, f32, DVec2, DVec2, f64)]
value: A,
/// The base of the logarithm, such as 2 (binary), 10 (decimal), and e (natural logarithm).
#[default(2.)]
#[implementations(f64, f32, f64, DVec2, DVec2)]
base: B,
) -> <A as Logarithm<B>>::Output {
value.logarithm(base)
}
/// 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<T: num_traits::float::Float>(
fn sine<T: Componentwise>(
_: impl Ctx,
/// The given angle.
#[implementations(f64, f32)]
#[implementations(f64, f32, DVec2)]
theta: T,
/// Whether the given angle should be interpreted as radians instead of degrees.
radians: bool,
) -> T {
if radians { theta.sin() } else { theta.to_radians().sin() }
theta.componentwise(|theta| if radians { theta.sin() } else { theta.to_radians().sin() })
}
/// 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<T: num_traits::float::Float>(
fn cosine<T: Componentwise>(
_: impl Ctx,
/// The given angle.
#[implementations(f64, f32)]
#[implementations(f64, f32, DVec2)]
theta: T,
/// Whether the given angle should be interpreted as radians instead of degrees.
radians: bool,
) -> T {
if radians { theta.cos() } else { theta.to_radians().cos() }
theta.componentwise(|theta| if radians { theta.cos() } else { theta.to_radians().cos() })
}
/// 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<T: num_traits::float::Float>(
fn tangent<T: Componentwise>(
_: impl Ctx,
/// The given angle.
#[implementations(f64, f32)]
#[implementations(f64, f32, DVec2)]
theta: T,
/// Whether the given angle should be interpreted as radians instead of degrees.
radians: bool,
) -> T {
if radians { theta.tan() } else { theta.to_radians().tan() }
theta.componentwise(|theta| if radians { theta.tan() } else { theta.to_radians().tan() })
}
/// The inverse sine trigonometric function (`asin`) calculates the angle whose sine is the input value.
@@ -475,36 +628,42 @@ fn as_f64(_: impl Ctx, value: f64) -> f64 {
}
/// 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<T: num_traits::float::Float>(
fn round<T: Componentwise>(
_: impl Ctx,
/// The number to be rounded to the nearest whole number.
#[implementations(f64, f32)]
#[implementations(f64, f32, DVec2)]
value: T,
) -> T {
value.round()
value.componentwise(f64::round)
}
/// 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<T: num_traits::float::Float>(
fn floor<T: Componentwise>(
_: impl Ctx,
/// The number to be rounded down.
#[implementations(f64, f32)]
#[implementations(f64, f32, DVec2)]
value: T,
) -> T {
value.floor()
value.componentwise(f64::floor)
}
/// 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<T: num_traits::float::Float>(
fn ceiling<T: Componentwise>(
_: impl Ctx,
/// The number to be rounded up.
#[implementations(f64, f32)]
#[implementations(f64, f32, DVec2)]
value: T,
) -> T {
value.ceil()
value.componentwise(f64::ceil)
}
trait AbsoluteValue {
@@ -537,6 +696,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<T: AbsoluteValue>(
_: impl Ctx,
@@ -547,57 +708,129 @@ fn absolute_value<T: AbsoluteValue>(
value.abs()
}
pub trait MinMax<Rhs = Self> {
type Output;
fn minimum(self, other: Rhs) -> Self::Output;
fn maximum(self, other: Rhs) -> Self::Output;
}
impl MinMax for f64 {
type Output = f64;
fn minimum(self, other: f64) -> f64 {
if self < other { self } else { other }
}
fn maximum(self, other: f64) -> f64 {
if self > other { self } else { other }
}
}
impl MinMax for f32 {
type Output = f32;
fn minimum(self, other: f32) -> f32 {
if self < other { self } else { other }
}
fn maximum(self, other: f32) -> f32 {
if self > other { self } else { other }
}
}
impl MinMax for u32 {
type Output = u32;
fn minimum(self, other: u32) -> u32 {
if self < other { self } else { other }
}
fn maximum(self, other: u32) -> u32 {
if self > other { self } else { other }
}
}
impl MinMax for String {
type Output = String;
fn minimum(self, other: Self) -> String {
if self < other { self } else { other }
}
fn maximum(self, other: Self) -> String {
if self > other { self } else { other }
}
}
impl MinMax for DVec2 {
type Output = DVec2;
fn minimum(self, other: DVec2) -> DVec2 {
self.min(other)
}
fn maximum(self, other: DVec2) -> DVec2 {
self.max(other)
}
}
impl MinMax<f64> for DVec2 {
type Output = DVec2;
fn minimum(self, other: f64) -> DVec2 {
self.min(DVec2::splat(other))
}
fn maximum(self, other: f64) -> DVec2 {
self.max(DVec2::splat(other))
}
}
impl MinMax<DVec2> 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.
///
/// With vec2 inputs, this applies separately to the X and Y components.
#[node_macro::node(category("Math: Numeric"))]
fn min<T: std::cmp::PartialOrd>(
fn min<A: MinMax<B>, B>(
_: impl Ctx,
/// One of the two numbers, of which the lesser is returned.
#[implementations(f64, f32, u32, String)]
value: T,
#[implementations(f64, f32, u32, String, DVec2, DVec2, f64)]
value: A,
/// The other of the two numbers, of which the lesser is returned.
#[implementations(f64, f32, u32, String)]
other_value: T,
) -> T {
if value < other_value { value } else { other_value }
#[implementations(f64, f32, u32, String, DVec2, f64, DVec2)]
other_value: B,
) -> <A as MinMax<B>>::Output {
value.minimum(other_value)
}
/// 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<T: std::cmp::PartialOrd>(
fn max<A: MinMax<B>, B>(
_: impl Ctx,
/// One of the two numbers, of which the greater is returned.
#[implementations(f64, f32, u32, String)]
value: T,
#[implementations(f64, f32, u32, String, DVec2, DVec2, f64)]
value: A,
/// The other of the two numbers, of which the greater is returned.
#[implementations(f64, f32, u32, String)]
other_value: T,
) -> T {
if value > other_value { value } else { other_value }
#[implementations(f64, f32, u32, String, DVec2, f64, DVec2)]
other_value: B,
) -> <A as MinMax<B>>::Output {
value.maximum(other_value)
}
/// 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<T: std::cmp::PartialOrd>(
fn clamp<A: MinMax<B>, B: MinMax<Output = B> + 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: T,
#[implementations(f64, f32, u32, String, DVec2, DVec2, f64)]
value: A,
/// The left (smaller) side of the range. The output is never less than this number.
#[implementations(f64, f32, u32, String)]
min: T,
#[implementations(f64, f32, u32, String, DVec2, f64, DVec2)]
min: B,
/// 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, DVec2)]
#[default(1)]
max: T,
) -> T {
let (min, max) = if min < max { (min, max) } else { (max, min) };
if value < min {
min
} else if value > max {
max
} else {
value
}
max: B,
) -> <A as MinMax<B>>::Output
where
<A as MinMax<B>>::Output: MinMax<B, Output = <A as MinMax<B>>::Output>,
{
let (min, max) = (min.clone().minimum(max.clone()), min.maximum(max));
value.maximum(min).minimum(max)
}
/// The greatest common divisor (GCD) calculates the largest positive integer that divides both of the two input numbers without leaving a remainder.
@@ -1047,6 +1280,51 @@ mod test {
assert_eq!(magnitude(&(), vector), 5.);
}
#[test]
pub fn clamp_vec2_within_swapped_bounds() {
let vec2 = |x, y| DVec2::new(x, y);
assert_eq!(clamp(&(), vec2(-5., 5.), vec2(1., 1.), vec2(0., 2.)), DVec2::new(0., 2.));
}
#[test]
pub fn min_max_vec2_with_scalar() {
let vec2 = |x, y| DVec2::new(x, y);
assert_eq!(super::min(&(), vec2(-5., 5.), 0_f64), DVec2::new(-5., 0.));
assert_eq!(super::max(&(), vec2(-5., 5.), 0_f64), DVec2::new(0., 5.));
}
#[test]
pub fn scalar_with_vec2_operand_orders() {
let vec2 = |x, y| DVec2::new(x, y);
assert_eq!(super::min(&(), 0_f64, vec2(-5., 5.)), DVec2::new(-5., 0.));
assert_eq!(super::max(&(), 0_f64, vec2(-5., 5.)), DVec2::new(0., 5.));
assert_eq!(exponent(&(), 2_f64, vec2(2., 3.)), DVec2::new(4., 8.));
assert_eq!(root(&(), 64_f64, vec2(2., 3.)), DVec2::new(8., 4.));
assert_eq!(logarithm(&(), 8_f64, vec2(2., 10.)), DVec2::new(3., 8_f64.log10()));
assert_eq!(clamp(&(), 5_f64, vec2(0., 6.), vec2(1., 10.)), DVec2::new(1., 6.));
}
#[test]
pub fn vec2_degrees_and_bases() {
let vec2 = |x, y| DVec2::new(x, y);
assert_eq!(root(&(), vec2(64., 27.), vec2(2., 3.)), DVec2::new(8., 3.));
assert_eq!(logarithm(&(), vec2(8., 100.), vec2(2., 10.)), DVec2::new(3., 2.));
}
#[test]
pub fn logarithm_f32_base_e_and_near_e() {
assert_eq!(logarithm(&(), 8_f32, std::f32::consts::E), 8_f64.ln() as f32);
assert_eq!(logarithm(&(), 8_f32, 2.7_f32), 8_f64.log(2.7_f32 as f64) as f32);
}
#[test]
pub fn round_floor_ceiling_vec2() {
let vec2 = |x, y| DVec2::new(x, y);
assert_eq!(round(&(), vec2(1.5, -1.4)), DVec2::new(2., -1.));
assert_eq!(floor(&(), vec2(1.9, -1.1)), DVec2::new(1., -2.));
assert_eq!(ceiling(&(), vec2(1.1, -1.9)), DVec2::new(2., -1.));
}
#[test]
fn test_basic_expression() {
let result = math(&(), 0., "2 + 2".to_string(), 0.);