| 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); |
| } |
| } |
|
|