Skip to content

Commit 3423c8e

Browse files
jacinyan0HyperCubeKeavon
authored
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>
1 parent 79b4f4d commit 3423c8e

6 files changed

Lines changed: 152 additions & 8 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2530,6 +2530,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
25302530
"graphene_core::raster::adjustments::SelectiveColorNode",
25312531
&node_properties::selective_color_properties as PropertiesLayout,
25322532
),
2533+
("graphene_core::ops::MathNode", &node_properties::math_properties as PropertiesLayout),
25332534
("graphene_core::raster::ExposureNode", &node_properties::exposure_properties as PropertiesLayout),
25342535
("graphene_core::vector::generator_nodes::RectangleNode", &node_properties::rectangle_properties as PropertiesLayout),
25352536
("graphene_core::vector::AssignColorsNode", &node_properties::assign_colors_properties as PropertiesLayout),

editor/src/messages/portfolio/document/node_graph/node_properties.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2616,3 +2616,52 @@ pub(crate) fn artboard_properties(document_node: &DocumentNode, node_id: NodeId,
26162616

26172617
vec![location, dimensions, background, clip_row]
26182618
}
2619+
2620+
pub fn math_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
2621+
let expression_index = 1;
2622+
let operation_b_index = 2;
2623+
2624+
let expression = (|| {
2625+
let mut widgets = start_widgets(document_node, node_id, expression_index, "Expression", FrontendGraphDataType::General, true);
2626+
2627+
let Some(input) = document_node.inputs.get(expression_index) else {
2628+
log::warn!("A widget failed to be built because its node's input index is invalid.");
2629+
return vec![];
2630+
};
2631+
if let Some(TaggedValue::String(x)) = &input.as_non_exposed_value() {
2632+
widgets.extend_from_slice(&[
2633+
Separator::new(SeparatorType::Unrelated).widget_holder(),
2634+
TextInput::new(x.clone())
2635+
.centered(true)
2636+
.on_update(update_value(
2637+
|x: &TextInput| {
2638+
TaggedValue::String({
2639+
let mut expression = x.value.trim().to_string();
2640+
2641+
if ["+", "-", "*", "/", "^", "%"].iter().any(|&infix| infix == expression) {
2642+
expression = format!("A {} B", expression);
2643+
} else if expression == "^" {
2644+
expression = String::from("A^B");
2645+
}
2646+
2647+
expression
2648+
})
2649+
},
2650+
node_id,
2651+
expression_index,
2652+
))
2653+
.on_commit(commit_value)
2654+
.widget_holder(),
2655+
])
2656+
}
2657+
widgets
2658+
})();
2659+
let operand_b = number_widget(document_node, node_id, operation_b_index, "Operand B", NumberInput::default(), true);
2660+
let operand_a_hint = vec![TextLabel::new("(Operand A is the primary input)").widget_holder()];
2661+
2662+
vec![
2663+
LayoutGroup::Row { widgets: expression }.with_tooltip(r#"A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2""#),
2664+
LayoutGroup::Row { widgets: operand_b }.with_tooltip(r#"The value of "B" when calculating the expression"#),
2665+
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"#),
2666+
]
2667+
}

libraries/math-parser/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ mod tests {
2727

2828
use super::*;
2929

30-
const EPSILON: f64 = 1e10_f64;
30+
const EPSILON: f64 = 1e-10_f64;
3131

3232
macro_rules! test_end_to_end{
3333
($($name:ident: $input:expr => ($expected_value:expr, $expected_unit:expr)),* $(,)?) => {

node-graph/gcore/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ web-sys = { workspace = true, optional = true, features = [
7878
image = { workspace = true, optional = true, default-features = false, features = [
7979
"png",
8080
] }
81+
math-parser = { path = "../../libraries/math-parser" }
8182

8283
[dev-dependencies]
8384
# Workspace dependencies

node-graph/gcore/src/ops.rs

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ use crate::registry::types::Percentage;
44
use crate::vector::style::GradientStops;
55
use crate::{Color, Node};
66

7+
use math_parser::ast;
8+
use math_parser::context::{EvalContext, NothingMap, ValueProvider};
9+
use math_parser::value::{Number, Value};
10+
711
use core::marker::PhantomData;
812
use core::ops::{Add, Div, Mul, Rem, Sub};
913
use glam::DVec2;
@@ -13,6 +17,70 @@ use rand::{Rng, SeedableRng};
1317
#[cfg(target_arch = "spirv")]
1418
use spirv_std::num_traits::float::Float;
1519

20+
/// The struct that stores the context for the maths parser.
21+
/// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs.
22+
struct MathNodeContext {
23+
a: f64,
24+
b: f64,
25+
}
26+
27+
impl ValueProvider for MathNodeContext {
28+
fn get_value(&self, name: &str) -> Option<Value> {
29+
if name.eq_ignore_ascii_case("a") {
30+
Some(Value::from_f64(self.a))
31+
} else if name.eq_ignore_ascii_case("b") {
32+
Some(Value::from_f64(self.b))
33+
} else {
34+
None
35+
}
36+
}
37+
}
38+
39+
/// Calculates a mathematical expression with input values "A" and "B"
40+
#[node_macro::node(category("Math"))]
41+
fn math<U: num_traits::float::Float>(
42+
_: (),
43+
/// The value of "A" when calculating the expression
44+
#[implementations(f64, f32)]
45+
operand_a: U,
46+
/// A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2"
47+
#[default(A + B)]
48+
expression: String,
49+
/// The value of "B" when calculating the expression
50+
#[implementations(f64, f32)]
51+
#[default(1.)]
52+
operand_b: U,
53+
) -> U {
54+
let (node, _unit) = match ast::Node::try_parse_from_str(&expression) {
55+
Ok(expr) => expr,
56+
Err(e) => {
57+
warn!("Invalid expression: `{expression}`\n{e:?}");
58+
return U::from(0.).unwrap();
59+
}
60+
};
61+
let context = EvalContext::new(
62+
MathNodeContext {
63+
a: operand_a.to_f64().unwrap(),
64+
b: operand_b.to_f64().unwrap(),
65+
},
66+
NothingMap,
67+
);
68+
69+
let value = match node.eval(&context) {
70+
Ok(value) => value,
71+
Err(e) => {
72+
warn!("Expression evaluation error: {e:?}");
73+
return U::from(0.).unwrap();
74+
}
75+
};
76+
77+
let Value::Number(num) = value;
78+
match num {
79+
Number::Real(val) => U::from(val).unwrap(),
80+
Number::Complex(c) => U::from(c.re).unwrap(),
81+
}
82+
}
83+
1684
/// The addition operation (+) calculates the sum of two numbers.
1785
#[node_macro::node(category("Math: Arithmetic"))]
1886
fn add<U: Add<T>, T>(
@@ -471,6 +539,37 @@ mod test {
471539
use super::*;
472540
use crate::{generic::*, structural::*, value::*};
473541

542+
#[test]
543+
pub fn dot_product_function() {
544+
let vector_a = glam::DVec2::new(1., 2.);
545+
let vector_b = glam::DVec2::new(3., 4.);
546+
assert_eq!(dot_product(vector_a, vector_b), 11.);
547+
}
548+
549+
#[test]
550+
fn test_basic_expression() {
551+
let result = math((), 0., "2 + 2".to_string(), 0.);
552+
assert_eq!(result, 4.);
553+
}
554+
555+
#[test]
556+
fn test_complex_expression() {
557+
let result = math((), 0., "(5 * 3) + (10 / 2)".to_string(), 0.);
558+
assert_eq!(result, 20.);
559+
}
560+
561+
#[test]
562+
fn test_default_expression() {
563+
let result = math((), 0., "0".to_string(), 0.);
564+
assert_eq!(result, 0.);
565+
}
566+
567+
#[test]
568+
fn test_invalid_expression() {
569+
let result = math((), 0., "invalid".to_string(), 0.);
570+
assert_eq!(result, 0.);
571+
}
572+
474573
#[test]
475574
pub fn identity_node() {
476575
let value = ValueNode(4u32).then(IdentityNode::new());
@@ -482,11 +581,4 @@ mod test {
482581
let fnn = FnNode::new(|(a, b)| (b, a));
483582
assert_eq!(fnn.eval((1u32, 2u32)), (2, 1));
484583
}
485-
486-
#[test]
487-
pub fn dot_product_function() {
488-
let vector_a = glam::DVec2::new(1., 2.);
489-
let vector_b = glam::DVec2::new(3., 4.);
490-
assert_eq!(dot_product(vector_a, vector_b), 11.);
491-
}
492584
}

0 commit comments

Comments
 (0)