mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-23 23:55:06 +00:00
Add math expression evaluation to NumberInput boxes (#1472)
* Add math expression parsing to NumberInput boxes * Prevent NaN results * Add support for implicit multiplication in expressions
This commit is contained in:
parent
ab3410cffe
commit
2515620a77
4 changed files with 116 additions and 16 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
@ -2286,6 +2286,7 @@ dependencies = [
|
|||
"graphite-editor",
|
||||
"js-sys",
|
||||
"log",
|
||||
"meval",
|
||||
"ron",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
|
@ -3101,9 +3102,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
version = "2.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
|
@ -3165,6 +3166,16 @@ dependencies = [
|
|||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "meval"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"nom 1.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
|
@ -3401,6 +3412,12 @@ version = "0.1.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
@ -4340,14 +4357,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.9.3"
|
||||
version = "1.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a"
|
||||
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.3.6",
|
||||
"regex-syntax 0.7.4",
|
||||
"regex-automata 0.4.3",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4361,13 +4378,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.6"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69"
|
||||
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.7.4",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4378,9 +4395,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.4"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "remain"
|
||||
|
@ -7466,7 +7483,7 @@ version = "0.3.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"nom 7.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
||||
|
||||
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@graphite/wasm-communication/messages";
|
||||
import { evaluateMathExpression } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
|
||||
|
||||
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
|
||||
|
||||
|
@ -185,13 +186,11 @@
|
|||
// The `unFocus()` call at the bottom of this function and in `onTextChangeCanceled()` causes this function to be run again, so this check skips a second run.
|
||||
if (!editing) return;
|
||||
|
||||
const parsed = parseFloat(text);
|
||||
const newValue = Number.isNaN(parsed) ? undefined : parsed;
|
||||
|
||||
let newValue = evaluateMathExpression(text);
|
||||
if (newValue !== undefined && isNaN(newValue)) newValue = undefined; // Rejects `sqrt(-1)`
|
||||
updateValue(newValue);
|
||||
|
||||
editing = false;
|
||||
|
||||
self?.unFocus();
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ ron = { version = "0.8", optional = true }
|
|||
bezier-rs = { path = "../../libraries/bezier-rs" }
|
||||
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
|
||||
wgpu = { version = "0.17", features = ["fragile-send-sync-non-atomic-wasm"] }
|
||||
meval = "0.2.0"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.4"
|
||||
|
|
|
@ -755,3 +755,86 @@ impl Drop for JsEditorHandle {
|
|||
EDITOR_INSTANCES.with(|instances| instances.borrow_mut().remove(&self.editor_id));
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = evaluateMathExpression)]
|
||||
pub fn evaluate_math_expression(expression: &str) -> Option<f64> {
|
||||
// TODO: Rewrite our own purpose-built math expression parser that supports unit conversions.
|
||||
|
||||
let mut context = meval::Context::new();
|
||||
context.var("tau", std::f64::consts::TAU);
|
||||
context.func("log", f64::log10);
|
||||
context.func("log10", f64::log10);
|
||||
context.func("log2", f64::log2);
|
||||
|
||||
// Insert asterisks where implicit multiplication is used in the expression string
|
||||
let expression = implicit_multiplication_preprocess(expression);
|
||||
|
||||
meval::eval_str_with_context(&expression, &context).ok()
|
||||
}
|
||||
|
||||
// Modified from this public domain snippet: <https://gist.github.com/Titaniumtown/c181be5d06505e003d8c4d1e372684ff>
|
||||
// Discussion: <https://github.com/rekka/meval-rs/issues/28#issuecomment-1826381922>
|
||||
pub fn implicit_multiplication_preprocess(expression: &str) -> String {
|
||||
let function = expression.to_lowercase().replace("log10(", "log(").replace("log2(", "logtwo(").replace("pi", "π").replace("tau", "τ");
|
||||
let valid_variables: Vec<char> = "eπτ".chars().collect();
|
||||
let letters: Vec<char> = ('a'..='z').chain('A'..='Z').collect();
|
||||
let numbers: Vec<char> = ('0'..='9').collect();
|
||||
let function_chars: Vec<char> = function.chars().collect();
|
||||
let mut output_string: String = String::new();
|
||||
let mut prev_chars: Vec<char> = Vec::new();
|
||||
|
||||
for c in function_chars {
|
||||
let mut add_asterisk: bool = false;
|
||||
let prev_chars_len = prev_chars.len();
|
||||
|
||||
let prev_prev_char = if prev_chars_len >= 2 { *prev_chars.get(prev_chars_len - 2).unwrap() } else { ' ' };
|
||||
|
||||
let prev_char = if prev_chars_len >= 1 { *prev_chars.get(prev_chars_len - 1).unwrap() } else { ' ' };
|
||||
|
||||
let c_letters_var = letters.contains(&c) | valid_variables.contains(&c);
|
||||
let prev_letters_var = valid_variables.contains(&prev_char) | letters.contains(&prev_char);
|
||||
|
||||
if prev_char == ')' {
|
||||
if (c == '(') | numbers.contains(&c) | c_letters_var {
|
||||
add_asterisk = true;
|
||||
}
|
||||
} else if c == '(' {
|
||||
if (valid_variables.contains(&prev_char) | (')' == prev_char) | numbers.contains(&prev_char)) && !letters.contains(&prev_prev_char) {
|
||||
add_asterisk = true;
|
||||
}
|
||||
} else if numbers.contains(&prev_char) {
|
||||
if (c == '(') | c_letters_var {
|
||||
add_asterisk = true;
|
||||
}
|
||||
} else if letters.contains(&c) {
|
||||
if numbers.contains(&prev_char) | (valid_variables.contains(&prev_char) && valid_variables.contains(&c)) {
|
||||
add_asterisk = true;
|
||||
}
|
||||
} else if (numbers.contains(&c) | c_letters_var) && prev_letters_var {
|
||||
add_asterisk = true;
|
||||
}
|
||||
|
||||
if add_asterisk {
|
||||
output_string += "*";
|
||||
}
|
||||
|
||||
prev_chars.push(c);
|
||||
output_string += &c.to_string();
|
||||
}
|
||||
|
||||
// We have to convert the Greek symbols back to ASCII because meval doesn't support unicode symbols as context constants
|
||||
output_string.replace("logtwo(", "log2(").replace("π", "pi").replace("τ", "tau")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_multiplication_preprocess_tests() {
|
||||
assert_eq!(implicit_multiplication_preprocess("2pi"), "2*pi");
|
||||
assert_eq!(implicit_multiplication_preprocess("sin(2pi)"), "sin(2*pi)");
|
||||
assert_eq!(implicit_multiplication_preprocess("2sin(pi)"), "2*sin(pi)");
|
||||
assert_eq!(implicit_multiplication_preprocess("2sin(3(4 + 5))"), "2*sin(3*(4 + 5))");
|
||||
assert_eq!(implicit_multiplication_preprocess("3abs(-4)"), "3*abs(-4)");
|
||||
assert_eq!(implicit_multiplication_preprocess("-1(4)"), "-1*(4)");
|
||||
assert_eq!(implicit_multiplication_preprocess("(-1)4"), "(-1)*4");
|
||||
assert_eq!(implicit_multiplication_preprocess("(((-1)))(4)"), "(((-1)))*(4)");
|
||||
assert_eq!(implicit_multiplication_preprocess("2sin(pi) + 2cos(tau)"), "2*sin(pi) + 2*cos(tau)");
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue