diff --git a/Cargo.lock b/Cargo.lock index 85a5fe263..95f0c0f32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/frontend/src/components/widgets/inputs/NumberInput.svelte b/frontend/src/components/widgets/inputs/NumberInput.svelte index 51e87cc83..4f05b7f6f 100644 --- a/frontend/src/components/widgets/inputs/NumberInput.svelte +++ b/frontend/src/components/widgets/inputs/NumberInput.svelte @@ -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(); } diff --git a/frontend/wasm/Cargo.toml b/frontend/wasm/Cargo.toml index 6fbe960e0..3dfef0c62 100644 --- a/frontend/wasm/Cargo.toml +++ b/frontend/wasm/Cargo.toml @@ -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" diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index e6defe20b..c0a92b744 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -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 { + // 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: +// Discussion: +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 = "eπτ".chars().collect(); + let letters: Vec = ('a'..='z').chain('A'..='Z').collect(); + let numbers: Vec = ('0'..='9').collect(); + let function_chars: Vec = function.chars().collect(); + let mut output_string: String = String::new(); + let mut prev_chars: Vec = 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)"); +}