|
use glam::DVec2; |
|
use graphene_core::gradient::GradientStops; |
|
use graphene_core::registry::types::{Fraction, Percentage, TextArea}; |
|
use graphene_core::{Color, Ctx, num_traits}; |
|
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, Div, Mul, Rem, Sub}; |
|
|
|
|
|
|
|
struct MathNodeContext { |
|
a: f64, |
|
b: f64, |
|
} |
|
|
|
impl ValueProvider for MathNodeContext { |
|
fn get_value(&self, name: &str) -> Option<Value> { |
|
if name.eq_ignore_ascii_case("a") { |
|
Some(Value::from_f64(self.a)) |
|
} else if name.eq_ignore_ascii_case("b") { |
|
Some(Value::from_f64(self.b)) |
|
} else { |
|
None |
|
} |
|
} |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Arithmetic"), properties("math_properties"))] |
|
fn math<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
operand_a: U, |
|
|
|
#[default(A + B)] |
|
expression: String, |
|
|
|
#[implementations(f64, f32)] |
|
#[default(1.)] |
|
operand_b: U, |
|
) -> U { |
|
let (node, _unit) = match ast::Node::try_parse_from_str(&expression) { |
|
Ok(expr) => expr, |
|
Err(e) => { |
|
warn!("Invalid expression: `{expression}`\n{e:?}"); |
|
return U::from(0.).unwrap(); |
|
} |
|
}; |
|
let context = EvalContext::new( |
|
MathNodeContext { |
|
a: operand_a.to_f64().unwrap(), |
|
b: operand_b.to_f64().unwrap(), |
|
}, |
|
NothingMap, |
|
); |
|
|
|
let value = match node.eval(&context) { |
|
Ok(value) => value, |
|
Err(e) => { |
|
warn!("Expression evaluation error: {e:?}"); |
|
return U::from(0.).unwrap(); |
|
} |
|
}; |
|
|
|
let Value::Number(num) = value; |
|
match num { |
|
Number::Real(val) => U::from(val).unwrap(), |
|
Number::Complex(c) => U::from(c.re).unwrap(), |
|
} |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Arithmetic"))] |
|
fn add<U: Add<T>, T>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32, DVec2, f64, DVec2)] |
|
augend: U, |
|
|
|
#[implementations(f64, f32, u32, DVec2, DVec2, f64)] |
|
addend: T, |
|
) -> <U as Add<T>>::Output { |
|
augend + addend |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Arithmetic"))] |
|
fn subtract<U: Sub<T>, T>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32, DVec2, f64, DVec2)] |
|
minuend: U, |
|
|
|
#[implementations(f64, f32, u32, DVec2, DVec2, f64)] |
|
subtrahend: T, |
|
) -> <U as Sub<T>>::Output { |
|
minuend - subtrahend |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Arithmetic"))] |
|
fn multiply<U: Mul<T>, T>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32, DVec2, f64, DVec2)] |
|
multiplier: U, |
|
|
|
#[default(1.)] |
|
#[implementations(f64, f32, u32, DVec2, DVec2, f64)] |
|
multiplicand: T, |
|
) -> <U as Mul<T>>::Output { |
|
multiplier * multiplicand |
|
} |
|
|
|
|
|
|
|
|
|
#[node_macro::node(category("Math: Arithmetic"))] |
|
fn divide<U: Div<T> + Default + PartialEq, T: Default + PartialEq>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, DVec2, f64)] |
|
numerator: U, |
|
|
|
#[default(1.)] |
|
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, f64, DVec2)] |
|
denominator: T, |
|
) -> <U as Div<T>>::Output |
|
where |
|
<U as Div<T>>::Output: Default, |
|
{ |
|
if denominator == T::default() { |
|
return <U as Div<T>>::Output::default(); |
|
} |
|
numerator / denominator |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Arithmetic"))] |
|
fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32, DVec2, DVec2, f64)] |
|
numerator: U, |
|
|
|
#[default(2.)] |
|
#[implementations(f64, f32, u32, DVec2, f64, DVec2)] |
|
modulus: T, |
|
|
|
#[default(true)] |
|
always_positive: bool, |
|
) -> <U as Rem<T>>::Output { |
|
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus } |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Arithmetic"))] |
|
fn exponent<U: Pow<T>, T>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32)] |
|
base: U, |
|
|
|
#[default(2.)] |
|
#[implementations(f64, f32, u32)] |
|
power: T, |
|
) -> <U as num_traits::Pow<T>>::Output { |
|
base.pow(power) |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Arithmetic"))] |
|
fn root<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[default(2.)] |
|
#[implementations(f64, f32)] |
|
radicand: U, |
|
|
|
#[default(2.)] |
|
#[implementations(f64, f32)] |
|
degree: U, |
|
) -> U { |
|
if degree == U::from(2.).unwrap() { |
|
radicand.sqrt() |
|
} else if degree == U::from(3.).unwrap() { |
|
radicand.cbrt() |
|
} else { |
|
radicand.powf(U::from(1.).unwrap() / degree) |
|
} |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Arithmetic"))] |
|
fn logarithm<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
value: U, |
|
|
|
#[default(2.)] |
|
#[implementations(f64, f32)] |
|
base: U, |
|
) -> U { |
|
if base == U::from(2.).unwrap() { |
|
value.log2() |
|
} else if base == U::from(10.).unwrap() { |
|
value.log10() |
|
} else if base - U::from(std::f64::consts::E).unwrap() < U::epsilon() * U::from(1e6).unwrap() { |
|
value.ln() |
|
} else { |
|
value.log(base) |
|
} |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Trig"))] |
|
fn sine<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
theta: U, |
|
|
|
radians: bool, |
|
) -> U { |
|
if radians { theta.sin() } else { theta.to_radians().sin() } |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Trig"))] |
|
fn cosine<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
theta: U, |
|
|
|
radians: bool, |
|
) -> U { |
|
if radians { theta.cos() } else { theta.to_radians().cos() } |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Trig"))] |
|
fn tangent<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
theta: U, |
|
|
|
radians: bool, |
|
) -> U { |
|
if radians { theta.tan() } else { theta.to_radians().tan() } |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Trig"))] |
|
fn sine_inverse<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
value: U, |
|
|
|
radians: bool, |
|
) -> U { |
|
if radians { value.asin() } else { value.asin().to_degrees() } |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Trig"))] |
|
fn cosine_inverse<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
value: U, |
|
|
|
radians: bool, |
|
) -> U { |
|
if radians { value.acos() } else { value.acos().to_degrees() } |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
#[node_macro::node(category("Math: Trig"))] |
|
fn tangent_inverse<U: TangentInverse>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, DVec2)] |
|
value: U, |
|
|
|
radians: bool, |
|
) -> U::Output { |
|
value.atan(radians) |
|
} |
|
|
|
pub trait TangentInverse { |
|
type Output: num_traits::float::Float; |
|
fn atan(self, radians: bool) -> Self::Output; |
|
} |
|
impl TangentInverse for f32 { |
|
type Output = f32; |
|
fn atan(self, radians: bool) -> Self::Output { |
|
if radians { self.atan() } else { self.atan().to_degrees() } |
|
} |
|
} |
|
impl TangentInverse for f64 { |
|
type Output = f64; |
|
fn atan(self, radians: bool) -> Self::Output { |
|
if radians { self.atan() } else { self.atan().to_degrees() } |
|
} |
|
} |
|
impl TangentInverse for DVec2 { |
|
type Output = f64; |
|
fn atan(self, radians: bool) -> Self::Output { |
|
if radians { self.y.atan2(self.x) } else { self.y.atan2(self.x).to_degrees() } |
|
} |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Numeric"))] |
|
fn random<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
_primary: (), |
|
|
|
seed: u64, |
|
|
|
#[implementations(f64, f32)] |
|
#[default(0.)] |
|
min: U, |
|
|
|
#[implementations(f64, f32)] |
|
#[default(1.)] |
|
max: U, |
|
) -> f64 { |
|
let mut rng = rand::rngs::StdRng::seed_from_u64(seed); |
|
let result = rng.random::<f64>(); |
|
let (min, max) = if min < max { (min, max) } else { (max, min) }; |
|
let (min, max) = (min.to_f64().unwrap(), max.to_f64().unwrap()); |
|
result * (max - min) + min |
|
} |
|
|
|
|
|
#[node_macro::node(name("To u32"), category("Math: Numeric"))] |
|
fn to_u32<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u32 { |
|
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u32::MAX as f64).unwrap()); |
|
value.to_u32().unwrap() |
|
} |
|
|
|
|
|
#[node_macro::node(name("To u64"), category("Math: Numeric"))] |
|
fn to_u64<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u64 { |
|
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u64::MAX as f64).unwrap()); |
|
value.to_u64().unwrap() |
|
} |
|
|
|
|
|
#[node_macro::node(name("To f64"), category("Math: Numeric"))] |
|
fn to_f64<U: num_traits::int::PrimInt>(_: impl Ctx, #[implementations(u32, u64)] value: U) -> f64 { |
|
value.to_f64().unwrap() |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Numeric"))] |
|
fn round<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
value: U, |
|
) -> U { |
|
value.round() |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Numeric"))] |
|
fn floor<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
value: U, |
|
) -> U { |
|
value.floor() |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Numeric"))] |
|
fn ceiling<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
value: U, |
|
) -> U { |
|
value.ceil() |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Numeric"))] |
|
fn absolute_value<U: num_traits::float::Float>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32)] |
|
value: U, |
|
) -> U { |
|
value.abs() |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Numeric"))] |
|
fn min<T: std::cmp::PartialOrd>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32, &str)] |
|
value: T, |
|
|
|
#[implementations(f64, f32, u32, &str)] |
|
other_value: T, |
|
) -> T { |
|
if value < other_value { value } else { other_value } |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Numeric"))] |
|
fn max<T: std::cmp::PartialOrd>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32, &str)] |
|
value: T, |
|
|
|
#[implementations(f64, f32, u32, &str)] |
|
other_value: T, |
|
) -> T { |
|
if value > other_value { value } else { other_value } |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Numeric"))] |
|
fn clamp<T: std::cmp::PartialOrd>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32, &str)] |
|
value: T, |
|
|
|
#[implementations(f64, f32, u32, &str)] |
|
min: T, |
|
|
|
#[implementations(f64, f32, u32, &str)] |
|
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 |
|
} |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Logic"))] |
|
fn equals<U: std::cmp::PartialEq<T>, T>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32, DVec2, &str)] |
|
value: T, |
|
|
|
#[implementations(f64, f32, u32, DVec2, &str)] |
|
other_value: U, |
|
) -> bool { |
|
other_value == value |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Logic"))] |
|
fn not_equals<U: std::cmp::PartialEq<T>, T>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32, DVec2, &str)] |
|
value: T, |
|
|
|
#[implementations(f64, f32, u32, DVec2, &str)] |
|
other_value: U, |
|
) -> bool { |
|
other_value != value |
|
} |
|
|
|
|
|
|
|
#[node_macro::node(category("Math: Logic"))] |
|
fn less_than<T: std::cmp::PartialOrd<T>>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32)] |
|
value: T, |
|
|
|
#[implementations(f64, f32, u32)] |
|
other_value: T, |
|
|
|
or_equal: bool, |
|
) -> bool { |
|
if or_equal { value <= other_value } else { value < other_value } |
|
} |
|
|
|
|
|
|
|
#[node_macro::node(category("Math: Logic"))] |
|
fn greater_than<T: std::cmp::PartialOrd<T>>( |
|
_: impl Ctx, |
|
|
|
#[implementations(f64, f32, u32)] |
|
value: T, |
|
|
|
#[implementations(f64, f32, u32)] |
|
other_value: T, |
|
|
|
or_equal: bool, |
|
) -> bool { |
|
if or_equal { value >= other_value } else { value > other_value } |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Logic"))] |
|
fn logical_or( |
|
_: impl Ctx, |
|
|
|
value: bool, |
|
|
|
other_value: bool, |
|
) -> bool { |
|
value || other_value |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Logic"))] |
|
fn logical_and( |
|
_: impl Ctx, |
|
|
|
value: bool, |
|
|
|
other_value: bool, |
|
) -> bool { |
|
value && other_value |
|
} |
|
|
|
|
|
#[node_macro::node(category("Math: Logic"))] |
|
fn logical_not( |
|
_: impl Ctx, |
|
|
|
input: bool, |
|
) -> bool { |
|
!input |
|
} |
|
|
|
|
|
#[node_macro::node(category("Value"))] |
|
fn bool_value(_: impl Ctx, _primary: (), #[name("Bool")] bool_value: bool) -> bool { |
|
bool_value |
|
} |
|
|
|
|
|
#[node_macro::node(category("Value"))] |
|
fn number_value(_: impl Ctx, _primary: (), number: f64) -> f64 { |
|
number |
|
} |
|
|
|
|
|
#[node_macro::node(category("Value"))] |
|
fn percentage_value(_: impl Ctx, _primary: (), percentage: Percentage) -> f64 { |
|
percentage |
|
} |
|
|
|
|
|
#[node_macro::node(category("Value"))] |
|
fn coordinate_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 { |
|
DVec2::new(x, y) |
|
} |
|
|
|
|
|
#[node_macro::node(category("Value"))] |
|
fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Option<Color>) -> Option<Color> { |
|
color |
|
} |
|
|
|
|
|
#[node_macro::node(category("Color"))] |
|
fn sample_gradient(_: impl Ctx, _primary: (), gradient: GradientStops, position: Fraction) -> Color { |
|
let position = position.clamp(0., 1.); |
|
gradient.evaluate(position) |
|
} |
|
|
|
|
|
#[node_macro::node(category("Value"))] |
|
fn gradient_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> GradientStops { |
|
gradient |
|
} |
|
|
|
|
|
#[node_macro::node(category("Value"))] |
|
fn string_value(_: impl Ctx, _primary: (), string: TextArea) -> String { |
|
string |
|
} |
|
|
|
#[node_macro::node(category("Math: Vector"))] |
|
fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 { |
|
vector_a.dot(vector_b) |
|
} |
|
|
|
#[cfg(test)] |
|
mod test { |
|
use super::*; |
|
use graphene_core::Node; |
|
use graphene_core::generic::FnNode; |
|
|
|
#[test] |
|
pub fn dot_product_function() { |
|
let vector_a = DVec2::new(1., 2.); |
|
let vector_b = DVec2::new(3., 4.); |
|
assert_eq!(dot_product((), vector_a, vector_b), 11.); |
|
} |
|
|
|
#[test] |
|
fn test_basic_expression() { |
|
let result = math((), 0., "2 + 2".to_string(), 0.); |
|
assert_eq!(result, 4.); |
|
} |
|
|
|
#[test] |
|
fn test_complex_expression() { |
|
let result = math((), 0., "(5 * 3) + (10 / 2)".to_string(), 0.); |
|
assert_eq!(result, 20.); |
|
} |
|
|
|
#[test] |
|
fn test_default_expression() { |
|
let result = math((), 0., "0".to_string(), 0.); |
|
assert_eq!(result, 0.); |
|
} |
|
|
|
#[test] |
|
fn test_invalid_expression() { |
|
let result = math((), 0., "invalid".to_string(), 0.); |
|
assert_eq!(result, 0.); |
|
} |
|
|
|
#[test] |
|
pub fn foo() { |
|
let fnn = FnNode::new(|(a, b)| (b, a)); |
|
assert_eq!(fnn.eval((1u32, 2u32)), (2, 1)); |
|
} |
|
|
|
#[test] |
|
pub fn add_vectors() { |
|
assert_eq!(super::add((), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.); |
|
} |
|
|
|
#[test] |
|
pub fn subtract_f64() { |
|
assert_eq!(super::subtract((), 5_f64, 3_f64), 2.); |
|
} |
|
|
|
#[test] |
|
pub fn divide_vectors() { |
|
assert_eq!(super::divide((), DVec2::ONE, 2_f64), DVec2::ONE / 2.); |
|
} |
|
|
|
#[test] |
|
pub fn modulo_positive() { |
|
assert_eq!(super::modulo((), -5_f64, 2_f64, true), 1_f64); |
|
} |
|
|
|
#[test] |
|
pub fn modulo_negative() { |
|
assert_eq!(super::modulo((), -5_f64, 2_f64, false), -1_f64); |
|
} |
|
} |
|
|