Repository: GraphiteEditor/Graphite (language: rust)

Issue:
Math node for running custom expressions with A and B
The new maths parser introduced in #2033 is currently only utilised to immediately evaluate expressions in number inputs.

To expand its utility, a new node should be introduced to run an expression during the runtime of the graph. In future, this node could take inputs from the graph. However this is blocked on implementing nodes with variadic inputs.


Discussion / hints:
hi @Keavon  , I'm interested to picking it up , could you please assign it to me .

@rajiknows thanks for your interest! We do issue assignments near the end of the PR lifecycle now, so we'll assign it once you have a working PR that's looking like it has progress towards being merged. Good luck and thanks!
Hi @Keavon, going through the discussion I can see there should be an working PR soon. However, I was wondering if I can try to also contribute regardless of whether mine will be accepted at the end of the day. Thank you 😄
@jacinyan Go ahead, and sorry if I’m blocking you. I got really busy with a college event. I’ll start working on this soon. Meanwhile, if you successfully solve it, please raise a PR.
> @jacinyan Go ahead, and sorry if I’m blocking you. I got really busy with a college event. I’ll start working on this soon. Meanwhile, if you successfully solve it, please raise a PR.

No, no no definitely not, and we might probably go with co-authoring if you interested (provide me with guidance and whatsoever :) ). Do you mind if I join you?

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 7e1bfb0d62..706ba1cfb6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2386,6 +2386,7 @@ dependencies = [
  "js-sys",
  "kurbo",
  "log",
+ "math-parser",
  "node-macro",
  "num-derive",
  "num-traits",
diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
index 9c1e61dcaa..a5d8ef5c86 100644
--- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
+++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
@@ -2530,6 +2530,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 			"graphene_core::raster::adjustments::SelectiveColorNode",
 			&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::vector::generator_nodes::RectangleNode", &node_properties::rectangle_properties as PropertiesLayout),
 		("graphene_core::vector::AssignColorsNode", &node_properties::assign_colors_properties as PropertiesLayout),
diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs
index 822d8997fb..1ca81ace6a 100644
--- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs
+++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs
@@ -2616,3 +2616,52 @@ pub(crate) fn artboard_properties(document_node: &DocumentNode, node_id: NodeId,
 
 	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"#),
+	]
+}
diff --git a/libraries/math-parser/src/lib.rs b/libraries/math-parser/src/lib.rs
index 584ea3f80c..e164de2146 100644
--- a/libraries/math-parser/src/lib.rs
+++ b/libraries/math-parser/src/lib.rs
@@ -27,7 +27,7 @@ mod tests {
 
 	use super::*;
 
-	const EPSILON: f64 = 1e10_f64;
+	const EPSILON: f64 = 1e-10_f64;
 
 	macro_rules! test_end_to_end{
 		($($name:ident: $input:expr => ($expected_value:expr, $expected_unit:expr)),* $(,)?) => {
diff --git a/node-graph/gcore/Cargo.toml b/node-graph/gcore/Cargo.toml
index 5271a7db00..ac4465bdf1 100644
--- a/node-graph/gcore/Cargo.toml
+++ b/node-graph/gcore/Cargo.toml
@@ -78,6 +78,7 @@ web-sys = { workspace = true, optional = true, features = [
 image = { workspace = true, optional = true, default-features = false, features = [
 	"png",
 ] }
+math-parser = { path = "../../libraries/math-parser" }
 
 [dev-dependencies]
 # Workspace dependencies
diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs
index 807156664b..241bf4bc0d 100644
--- a/node-graph/gcore/src/ops.rs
+++ b/node-graph/gcore/src/ops.rs
@@ -4,6 +4,10 @@ use crate::registry::types::Percentage;
 use crate::vector::style::GradientStops;
 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::ops::{Add, Div, Mul, Rem, Sub};
 use glam::DVec2;
@@ -13,6 +17,70 @@ use rand::{Rng, SeedableRng};
 #[cfg(target_arch = "spirv")]
 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.
 #[node_macro::node(category("Math: Arithmetic"))]
 fn add<U: Add<T>, T>(
@@ -471,6 +539,37 @@ mod test {
 	use super::*;
 	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]
 	pub fn identity_node() {
 		let value = ValueNode(4u32).then(IdentityNode::new());
@@ -482,11 +581,4 @@ mod test {
 		let fnn = FnNode::new(|(a, b)| (b, a));
 		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.);
-	}
 }


Test patch: