New node: Math (#2121)
* 2115 IP * Initial implementation of Expression node * Register Expression Node * Add Expression DocumentNode Definition * DocumentNodeImplementation::Expresssion in guess_type_from_node * Move expression.rs to graphene-core * WIP: Investigating 'exposed' & 'value_source' params for Expression property * Node graph render debug IP * Single input can change node properties; complex debug IP * Fix epsilon in test * Handle invalid expressions in expression_node by returning 0.0 * Run cargo fmt * Set the default expression to "1 + 1" * Hardcode the A and B inputs at Keavon's request * Rename and clean up UX * Move into ops.rs --------- Co-authored-by: hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
79b4f4df7b
commit
3423c8ec13
|
|
@ -2386,6 +2386,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"kurbo",
|
"kurbo",
|
||||||
"log",
|
"log",
|
||||||
|
"math-parser",
|
||||||
"node-macro",
|
"node-macro",
|
||||||
"num-derive",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
|
|
||||||
|
|
@ -2530,6 +2530,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||||
"graphene_core::raster::adjustments::SelectiveColorNode",
|
"graphene_core::raster::adjustments::SelectiveColorNode",
|
||||||
&node_properties::selective_color_properties as PropertiesLayout,
|
&node_properties::selective_color_properties as PropertiesLayout,
|
||||||
),
|
),
|
||||||
|
("graphene_core::ops::MathNode", &node_properties::math_properties as PropertiesLayout),
|
||||||
("graphene_core::raster::ExposureNode", &node_properties::exposure_properties as PropertiesLayout),
|
("graphene_core::raster::ExposureNode", &node_properties::exposure_properties as PropertiesLayout),
|
||||||
("graphene_core::vector::generator_nodes::RectangleNode", &node_properties::rectangle_properties as PropertiesLayout),
|
("graphene_core::vector::generator_nodes::RectangleNode", &node_properties::rectangle_properties as PropertiesLayout),
|
||||||
("graphene_core::vector::AssignColorsNode", &node_properties::assign_colors_properties as PropertiesLayout),
|
("graphene_core::vector::AssignColorsNode", &node_properties::assign_colors_properties as PropertiesLayout),
|
||||||
|
|
|
||||||
|
|
@ -2616,3 +2616,52 @@ pub(crate) fn artboard_properties(document_node: &DocumentNode, node_id: NodeId,
|
||||||
|
|
||||||
vec![location, dimensions, background, clip_row]
|
vec![location, dimensions, background, clip_row]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn math_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
|
let expression_index = 1;
|
||||||
|
let operation_b_index = 2;
|
||||||
|
|
||||||
|
let expression = (|| {
|
||||||
|
let mut widgets = start_widgets(document_node, node_id, expression_index, "Expression", FrontendGraphDataType::General, true);
|
||||||
|
|
||||||
|
let Some(input) = document_node.inputs.get(expression_index) else {
|
||||||
|
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
if let Some(TaggedValue::String(x)) = &input.as_non_exposed_value() {
|
||||||
|
widgets.extend_from_slice(&[
|
||||||
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
|
TextInput::new(x.clone())
|
||||||
|
.centered(true)
|
||||||
|
.on_update(update_value(
|
||||||
|
|x: &TextInput| {
|
||||||
|
TaggedValue::String({
|
||||||
|
let mut expression = x.value.trim().to_string();
|
||||||
|
|
||||||
|
if ["+", "-", "*", "/", "^", "%"].iter().any(|&infix| infix == expression) {
|
||||||
|
expression = format!("A {} B", expression);
|
||||||
|
} else if expression == "^" {
|
||||||
|
expression = String::from("A^B");
|
||||||
|
}
|
||||||
|
|
||||||
|
expression
|
||||||
|
})
|
||||||
|
},
|
||||||
|
node_id,
|
||||||
|
expression_index,
|
||||||
|
))
|
||||||
|
.on_commit(commit_value)
|
||||||
|
.widget_holder(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
widgets
|
||||||
|
})();
|
||||||
|
let operand_b = number_widget(document_node, node_id, operation_b_index, "Operand B", NumberInput::default(), true);
|
||||||
|
let operand_a_hint = vec![TextLabel::new("(Operand A is the primary input)").widget_holder()];
|
||||||
|
|
||||||
|
vec![
|
||||||
|
LayoutGroup::Row { widgets: expression }.with_tooltip(r#"A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2""#),
|
||||||
|
LayoutGroup::Row { widgets: operand_b }.with_tooltip(r#"The value of "B" when calculating the expression"#),
|
||||||
|
LayoutGroup::Row { widgets: operand_a_hint }.with_tooltip(r#""A" is fed by the value from the previous node in the primary data flow, or it is 0 if disconnected"#),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const EPSILON: f64 = 1e10_f64;
|
const EPSILON: f64 = 1e-10_f64;
|
||||||
|
|
||||||
macro_rules! test_end_to_end{
|
macro_rules! test_end_to_end{
|
||||||
($($name:ident: $input:expr => ($expected_value:expr, $expected_unit:expr)),* $(,)?) => {
|
($($name:ident: $input:expr => ($expected_value:expr, $expected_unit:expr)),* $(,)?) => {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ web-sys = { workspace = true, optional = true, features = [
|
||||||
image = { workspace = true, optional = true, default-features = false, features = [
|
image = { workspace = true, optional = true, default-features = false, features = [
|
||||||
"png",
|
"png",
|
||||||
] }
|
] }
|
||||||
|
math-parser = { path = "../../libraries/math-parser" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# Workspace dependencies
|
# Workspace dependencies
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ use crate::registry::types::Percentage;
|
||||||
use crate::vector::style::GradientStops;
|
use crate::vector::style::GradientStops;
|
||||||
use crate::{Color, Node};
|
use crate::{Color, Node};
|
||||||
|
|
||||||
|
use math_parser::ast;
|
||||||
|
use math_parser::context::{EvalContext, NothingMap, ValueProvider};
|
||||||
|
use math_parser::value::{Number, Value};
|
||||||
|
|
||||||
use core::marker::PhantomData;
|
use core::marker::PhantomData;
|
||||||
use core::ops::{Add, Div, Mul, Rem, Sub};
|
use core::ops::{Add, Div, Mul, Rem, Sub};
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|
@ -13,6 +17,70 @@ use rand::{Rng, SeedableRng};
|
||||||
#[cfg(target_arch = "spirv")]
|
#[cfg(target_arch = "spirv")]
|
||||||
use spirv_std::num_traits::float::Float;
|
use spirv_std::num_traits::float::Float;
|
||||||
|
|
||||||
|
/// The struct that stores the context for the maths parser.
|
||||||
|
/// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates a mathematical expression with input values "A" and "B"
|
||||||
|
#[node_macro::node(category("Math"))]
|
||||||
|
fn math<U: num_traits::float::Float>(
|
||||||
|
_: (),
|
||||||
|
/// The value of "A" when calculating the expression
|
||||||
|
#[implementations(f64, f32)]
|
||||||
|
operand_a: U,
|
||||||
|
/// A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2"
|
||||||
|
#[default(A + B)]
|
||||||
|
expression: String,
|
||||||
|
/// The value of "B" when calculating the expression
|
||||||
|
#[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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The addition operation (+) calculates the sum of two numbers.
|
/// The addition operation (+) calculates the sum of two numbers.
|
||||||
#[node_macro::node(category("Math: Arithmetic"))]
|
#[node_macro::node(category("Math: Arithmetic"))]
|
||||||
fn add<U: Add<T>, T>(
|
fn add<U: Add<T>, T>(
|
||||||
|
|
@ -471,6 +539,37 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{generic::*, structural::*, value::*};
|
use crate::{generic::*, structural::*, value::*};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn dot_product_function() {
|
||||||
|
let vector_a = glam::DVec2::new(1., 2.);
|
||||||
|
let vector_b = glam::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]
|
#[test]
|
||||||
pub fn identity_node() {
|
pub fn identity_node() {
|
||||||
let value = ValueNode(4u32).then(IdentityNode::new());
|
let value = ValueNode(4u32).then(IdentityNode::new());
|
||||||
|
|
@ -482,11 +581,4 @@ mod test {
|
||||||
let fnn = FnNode::new(|(a, b)| (b, a));
|
let fnn = FnNode::new(|(a, b)| (b, a));
|
||||||
assert_eq!(fnn.eval((1u32, 2u32)), (2, 1));
|
assert_eq!(fnn.eval((1u32, 2u32)), (2, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn dot_product_function() {
|
|
||||||
let vector_a = glam::DVec2::new(1., 2.);
|
|
||||||
let vector_b = glam::DVec2::new(3., 4.);
|
|
||||||
assert_eq!(dot_product(vector_a, vector_b), 11.);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue