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
|
|
@ -2286,6 +2286,7 @@ dependencies = [
|
||||||
"graphite-editor",
|
"graphite-editor",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
|
"meval",
|
||||||
"ron",
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-wasm-bindgen",
|
"serde-wasm-bindgen",
|
||||||
|
|
@ -3101,9 +3102,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.5.0"
|
version = "2.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memmap2"
|
name = "memmap2"
|
||||||
|
|
@ -3165,6 +3166,16 @@ dependencies = [
|
||||||
"paste",
|
"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]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
|
@ -3401,6 +3412,12 @@ version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "1.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
|
|
@ -4340,14 +4357,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.9.3"
|
version = "1.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a"
|
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata 0.3.6",
|
"regex-automata 0.4.3",
|
||||||
"regex-syntax 0.7.4",
|
"regex-syntax 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4361,13 +4378,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.3.6"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69"
|
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax 0.7.4",
|
"regex-syntax 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4378,9 +4395,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.7.4"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
|
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remain"
|
name = "remain"
|
||||||
|
|
@ -7466,7 +7483,7 @@ version = "0.3.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7"
|
checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nom",
|
"nom 7.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
||||||
|
|
||||||
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@graphite/wasm-communication/messages";
|
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";
|
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.
|
// 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;
|
if (!editing) return;
|
||||||
|
|
||||||
const parsed = parseFloat(text);
|
let newValue = evaluateMathExpression(text);
|
||||||
const newValue = Number.isNaN(parsed) ? undefined : parsed;
|
if (newValue !== undefined && isNaN(newValue)) newValue = undefined; // Rejects `sqrt(-1)`
|
||||||
|
|
||||||
updateValue(newValue);
|
updateValue(newValue);
|
||||||
|
|
||||||
editing = false;
|
editing = false;
|
||||||
|
|
||||||
self?.unFocus();
|
self?.unFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ ron = { version = "0.8", optional = true }
|
||||||
bezier-rs = { path = "../../libraries/bezier-rs" }
|
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
|
# 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"] }
|
wgpu = { version = "0.17", features = ["fragile-send-sync-non-atomic-wasm"] }
|
||||||
|
meval = "0.2.0"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
|
|
||||||
|
|
@ -755,3 +755,86 @@ impl Drop for JsEditorHandle {
|
||||||
EDITOR_INSTANCES.with(|instances| instances.borrow_mut().remove(&self.editor_id));
|
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…
Reference in New Issue