mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
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
6 changed files with 152 additions and 8 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2386,6 +2386,7 @@ dependencies = [
|
|||
"js-sys",
|
||||
"kurbo",
|
||||
"log",
|
||||
"math-parser",
|
||||
"node-macro",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"#),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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)),* $(,)?) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue