When taking math or scientific lecture notes with a digital stylus, it is cumbersome to leave the app to calculate numbers or expand formulas.
We should add an inline handwritten math helper: when a user writes an equation ending with an = sign (e.g., 2^4 = or (x + 3)^2 =), the app should recognize the handwritten expression, calculate or simplify the result, and display it next to the = sign.
User Experience (UX) Flow
The user writes a formula with the stylus: e.g. 2^4 =.
Upon completing the = sign (and a ~500ms pen pause), the app detects the trigger.
A subtle preview chip or ghost text appears to the right of =: 16.
Tapping the preview inserts the result permanently into the document.
To guarantee full OS independence and native support for Linux (Zorin OS) without external C/C++ runtime dependencies (.so / .dll), the feature should use:
Stroke Bounding Box & Rasterizer:
Detect the strokes preceding the trailing = sign.
Render the selected vector strokes into an in-memory grayscale image buffer (fixed height of 64px, dynamic width scaled to maintain aspect ratio).
Handwriting Recognition (tract-onnx):
Run a tiny, pre-trained math OCR model (CRNN / mini-ViT trained on math datasets like CROHME) embedded as an ONNX file.
Execute inference strictly on CPU using tract-onnx (100% pure Rust, zero system dependencies, fast SIMD execution).
Decode the output tensor into a clean math string (e.g., "2^4").
Deterministic Math Evaluator:
Arithmetic: Evaluate numeric expressions using a pure-Rust parser like evalexpr (handles powers ^, basic operations, trig).
Symbolic Fallback (Binomials/Formulas): If variables like x or a are present, use a symbolic engine (e.g. kalk or symbolic_polynomials) to expand expressions like (x + 3)^2\rightarrowx^2 + 6x + 9.
Technical Constraints
Zero C/C++ dynamic linking: Must compile using standard cargo build on Linux (Zorin OS / Ubuntu), Windows, and macOS.
Offline & Private: All inference and evaluation must run 100% locally on CPU without external API calls.
Low Latency: Recognition and evaluation combined must execute in < 25ms on modern laptop CPUs so note-taking remains fluid.
Acceptance Criteria / Tasks
Implement trailing = stroke gesture detection.
Create an in-memory stroke rasterizer (64px height grayscale tensor).
Integrate tract-onnx to load an embedded math OCR model and run CPU inference.
Add the math evaluation pipeline (evalexpr for arithmetic, symbolic fallback for variables).
Render the computed result inline next to the = sign with an interaction to confirm/bake into ink.
Add unit tests for expression decoding and mathematical evaluation.
### Summary
When taking math or scientific lecture notes with a digital stylus, it is cumbersome to leave the app to calculate numbers or expand formulas.
We should add an **inline handwritten math helper**: when a user writes an equation ending with an `=` sign (e.g., `2^4 =` or `(x + 3)^2 =`), the app should recognize the handwritten expression, calculate or simplify the result, and display it next to the `=` sign.
---
### User Experience (UX) Flow
1. The user writes a formula with the stylus: e.g. `2^4 =`.
2. Upon completing the `=` sign (and a ~500ms pen pause), the app detects the trigger.
3. A subtle preview chip or ghost text appears to the right of `=`: `16`.
4. Tapping the preview inserts the result permanently into the document.
---
### Proposed Architecture (Approach B: Canvas Crop + Pure-Rust Inference)
To guarantee full **OS independence** and native support for Linux (Zorin OS) without external C/C++ runtime dependencies (`.so` / `.dll`), the feature should use:
1. **Stroke Bounding Box & Rasterizer:**
- Detect the strokes preceding the trailing `=` sign.
- Render the selected vector strokes into an in-memory grayscale image buffer (fixed height of `64px`, dynamic width scaled to maintain aspect ratio).
2. **Handwriting Recognition (`tract-onnx`):**
- Run a tiny, pre-trained math OCR model (CRNN / mini-ViT trained on math datasets like CROHME) embedded as an ONNX file.
- Execute inference strictly on CPU using [`tract-onnx`](https://crates.io/crates/tract-onnx) (100% pure Rust, zero system dependencies, fast SIMD execution).
- Decode the output tensor into a clean math string (e.g., `"2^4"`).
3. **Deterministic Math Evaluator:**
- **Arithmetic:** Evaluate numeric expressions using a pure-Rust parser like [`evalexpr`](https://crates.io/crates/evalexpr) (handles powers `^`, basic operations, trig).
- **Symbolic Fallback (Binomials/Formulas):** If variables like `x` or `a` are present, use a symbolic engine (e.g. [`kalk`](https://crates.io/crates/kalk) or [`symbolic_polynomials`](https://crates.io/crates/symbolic_polynomials)) to expand expressions like `(x + 3)^2` $\rightarrow$ `x^2 + 6x + 9`.
---
### Technical Constraints
- **Zero C/C++ dynamic linking:** Must compile using standard `cargo build` on Linux (Zorin OS / Ubuntu), Windows, and macOS.
- **Offline & Private:** All inference and evaluation must run 100% locally on CPU without external API calls.
- **Low Latency:** Recognition and evaluation combined must execute in < 25ms on modern laptop CPUs so note-taking remains fluid.
---
### Acceptance Criteria / Tasks
- [ ] Implement trailing `=` stroke gesture detection.
- [ ] Create an in-memory stroke rasterizer (`64px` height grayscale tensor).
- [ ] Integrate `tract-onnx` to load an embedded math OCR model and run CPU inference.
- [ ] Add the math evaluation pipeline (`evalexpr` for arithmetic, symbolic fallback for variables).
- [ ] Render the computed result inline next to the `=` sign with an interaction to confirm/bake into ink.
- [ ] Add unit tests for expression decoding and mathematical evaluation.
Review against the code (planned for the future, not started)
Platform
ink-notes runs on Linux only (Wayland/X11, octotablet for the pen), with Android planned in #42–#45. Windows and macOS are out of scope, so "zero C/C++ dynamic linking" only has to hold for Linux and Android. That is still a good rule; tract and the evaluator are pure Rust.
Recognition: use the strokes, not a raster crop
Every stroke is stored as vector samples with position, pressure and a timestamp (StylusSample). That makes online recognition possible: the order of the strokes and the pen direction are known. This is more accurate and much cheaper than recognising a 64 px image.
Rasterising would also be extra work. The current CPU rasteriser goes through PDF and hayro (page_export.rs), which is far too slow for < 25 ms. A dedicated stroke-to-tensor rasteriser would be needed.
A suggested pipeline that fits the 25 ms budget:
Group strokes into symbols, by overlap and gap, plus time.
Classify each symbol with a small CNN or MLP on the resampled stroke points: digits, + − · × : / ( ) = ^ x y a b. The model is a few hundred KB, runs on tract-onnx or as hand-written matrix multiplications, and needs well under 1 ms per symbol.
Recognise the structure from the bounding boxes: baseline, superscript (2⁴ is written as a raised 4, there is no ^ on paper), fraction bars.
A full image-to-LaTeX model trained on CROHME (CRNN/ViT encoder-decoder such as BTTR or CoMER) would be tens of MB, and decoding it autoregressively is unlikely to reach < 25 ms on a laptop CPU. A CRNN with CTC reads only one line and cannot represent superscripts. Also to check: whether a suitable pretrained model and the CROHME data have licenses that allow shipping the model in the app.
Evaluation
evalexpr uses ^ for powers and has math::sin etc. That covers arithmetic. But the recognised string has to be normalised first:
the German decimal comma (2,5);
·, × and : for multiply and divide;
implicit multiplication (2x, 3(x+1)).
For the symbolic part ((x + 3)^2): as far as I know, kalk is a calculator with variables but does not expand polynomials, so that needs checking. A small polynomial module of our own (sums of monomials with rational coefficients; + − · and integer powers) is about 200 lines and fully testable. I would prefer it to an extra dependency.
Triggering and conflicts
Draw-and-hold already snaps a stroke to a shape when the pen is held still for over 400 ms. The two bars of = may therefore arrive as snapped lines and not as ink, so detection must accept both.
= = two short, roughly horizontal, parallel strokes of similar length, drawn one after the other. The trigger fires after the pen is lifted and 500 ms pass without a new stroke. A new stroke cancels it.
Expression strokes = strokes left of the = on the same line (vertical overlap with a band around the =), with no large gap, written in this session.
The feature should be off by default (toggle in ☰ settings), so that an = in normal notes never gets a suggestion it did not ask for.
Showing and inserting the result
The preview chip can be an egui overlay at the page position. It must not catch palm or finger touches; pen tap and click only.
Insert the result as a text object, using the existing text tool (insert_text_on_page, in pen colour and matching the height of the handwriting), as one undo step. It stays editable and is exported to PDF like other text. Turning it into ink would need a handwriting font, so it could come later as an option.
Tests
Symbol grouping and structure from synthetic strokes, normalising the string, and the evaluator and polynomial expansion.
A small set of real handwriting samples (Pia's?) as a regression test for the classifier.
**Review against the code** (planned for the future, not started)
**Platform**
- ink-notes runs on Linux only (Wayland/X11, octotablet for the pen), with Android planned in #42–#45. Windows and macOS are out of scope, so "zero C/C++ dynamic linking" only has to hold for Linux and Android. That is still a good rule; tract and the evaluator are pure Rust.
**Recognition: use the strokes, not a raster crop**
- Every stroke is stored as vector samples with position, pressure and a timestamp (`StylusSample`). That makes *online* recognition possible: the order of the strokes and the pen direction are known. This is more accurate and much cheaper than recognising a 64 px image.
- Rasterising would also be extra work. The current CPU rasteriser goes through PDF and hayro (`page_export.rs`), which is far too slow for < 25 ms. A dedicated stroke-to-tensor rasteriser would be needed.
- A suggested pipeline that fits the 25 ms budget:
1. **Group strokes into symbols**, by overlap and gap, plus time.
2. **Classify each symbol** with a small CNN or MLP on the resampled stroke points: digits, `+ − · × : / ( ) = ^ x y a b`. The model is a few hundred KB, runs on tract-onnx or as hand-written matrix multiplications, and needs well under 1 ms per symbol.
3. **Recognise the structure** from the bounding boxes: baseline, superscript (`2⁴` is written as a raised `4`, there is no `^` on paper), fraction bars.
- A full image-to-LaTeX model trained on CROHME (CRNN/ViT encoder-decoder such as BTTR or CoMER) would be tens of MB, and decoding it autoregressively is unlikely to reach < 25 ms on a laptop CPU. A CRNN with CTC reads only one line and cannot represent superscripts. Also to check: whether a suitable pretrained model and the CROHME data have licenses that allow shipping the model in the app.
**Evaluation**
- `evalexpr` uses `^` for powers and has `math::sin` etc. That covers arithmetic. But the recognised string has to be normalised first:
- the German decimal comma (`2,5`);
- `·`, `×` and `:` for multiply and divide;
- implicit multiplication (`2x`, `3(x+1)`).
- For the symbolic part (`(x + 3)^2`): as far as I know, `kalk` is a calculator with variables but does not expand polynomials, so that needs checking. A small polynomial module of our own (sums of monomials with rational coefficients; `+ − ·` and integer powers) is about 200 lines and fully testable. I would prefer it to an extra dependency.
**Triggering and conflicts**
- Draw-and-hold already snaps a stroke to a shape when the pen is held still for over 400 ms. The two bars of `=` may therefore arrive as snapped lines and not as ink, so detection must accept both.
- `=` = two short, roughly horizontal, parallel strokes of similar length, drawn one after the other. The trigger fires after the pen is lifted and 500 ms pass without a new stroke. A new stroke cancels it.
- Expression strokes = strokes left of the `=` on the same line (vertical overlap with a band around the `=`), with no large gap, written in this session.
- The feature should be **off by default** (toggle in ☰ settings), so that an `=` in normal notes never gets a suggestion it did not ask for.
**Showing and inserting the result**
- The preview chip can be an egui overlay at the page position. It must not catch palm or finger touches; pen tap and click only.
- Insert the result as a **text object**, using the existing text tool (`insert_text_on_page`, in pen colour and matching the height of the handwriting), as one undo step. It stays editable and is exported to PDF like other text. Turning it into ink would need a handwriting font, so it could come later as an option.
**Tests**
- Symbol grouping and structure from synthetic strokes, normalising the string, and the evaluator and polynomial expansion.
- A small set of real handwriting samples (Pia's?) as a regression test for the classifier.
Erste Version in v0.41.0, experimentell und standardmäßig aus.
Einschalten: ☰ → Eingabe → „Formeln nach „=“ ausrechnen“.
Ablauf: Nach einem handgeschriebenen = und kurzer Pause erscheint daneben das Gelesene samt Ergebnis, z. B. 2⁴ = 16 oder (x + 3)² = x² + 6x + 9.
Antippen mit dem Stift oder Klicken fügt das Ergebnis als Text ein, in Stiftfarbe und Handschrifthöhe.
Finger und Handballen lösen nichts aus.
Gelesen werden: Ziffern, + − · : /, Klammern, Hochzahlen, Bruchstriche, Dezimalkomma sowie x, y, a, b.
Rechnen: Zahlen werden exakt gerechnet, Ausdrücke mit Variablen ausmultipliziert.
Erkennung: Punktwolken-Vorlagen ($P), offline, ohne trainiertes Modell, etwa 4 ms pro Formel.
Bisher nur mit künstlicher Handschrift getestet. Bitte mit echter Handschrift ausprobieren und melden, welche Zeichen falsch gelesen werden. Daraus entsteht der nächste Schritt: eigene Vorlagen für die eigene Handschrift.
Erste Version in **v0.41.0**, experimentell und standardmäßig aus.
- **Einschalten:** ☰ → Eingabe → „Formeln nach „=“ ausrechnen“.
- **Ablauf:** Nach einem handgeschriebenen `=` und kurzer Pause erscheint daneben das Gelesene samt Ergebnis, z. B. `2⁴ = 16` oder `(x + 3)² = x² + 6x + 9`.
- Antippen mit dem Stift oder Klicken fügt das Ergebnis als Text ein, in Stiftfarbe und Handschrifthöhe.
- Finger und Handballen lösen nichts aus.
- **Gelesen werden:** Ziffern, `+ − · : /`, Klammern, Hochzahlen, Bruchstriche, Dezimalkomma sowie `x`, `y`, `a`, `b`.
- **Rechnen:** Zahlen werden exakt gerechnet, Ausdrücke mit Variablen ausmultipliziert.
- **Erkennung:** Punktwolken-Vorlagen ($P), offline, ohne trainiertes Modell, etwa 4 ms pro Formel.
Bisher nur mit künstlicher Handschrift getestet. Bitte mit echter Handschrift ausprobieren und melden, welche Zeichen falsch gelesen werden. Daraus entsteht der nächste Schritt: eigene Vorlagen für die eigene Handschrift.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
When taking math or scientific lecture notes with a digital stylus, it is cumbersome to leave the app to calculate numbers or expand formulas.
We should add an inline handwritten math helper: when a user writes an equation ending with an
=sign (e.g.,2^4 =or(x + 3)^2 =), the app should recognize the handwritten expression, calculate or simplify the result, and display it next to the=sign.User Experience (UX) Flow
2^4 =.=sign (and a ~500ms pen pause), the app detects the trigger.=:16.Proposed Architecture (Approach B: Canvas Crop + Pure-Rust Inference)
To guarantee full OS independence and native support for Linux (Zorin OS) without external C/C++ runtime dependencies (
.so/.dll), the feature should use:Stroke Bounding Box & Rasterizer:
=sign.64px, dynamic width scaled to maintain aspect ratio).Handwriting Recognition (
tract-onnx):tract-onnx(100% pure Rust, zero system dependencies, fast SIMD execution)."2^4").Deterministic Math Evaluator:
evalexpr(handles powers^, basic operations, trig).xoraare present, use a symbolic engine (e.g.kalkorsymbolic_polynomials) to expand expressions like(x + 3)^2\rightarrowx^2 + 6x + 9.Technical Constraints
cargo buildon Linux (Zorin OS / Ubuntu), Windows, and macOS.Acceptance Criteria / Tasks
=stroke gesture detection.64pxheight grayscale tensor).tract-onnxto load an embedded math OCR model and run CPU inference.evalexprfor arithmetic, symbolic fallback for variables).=sign with an interaction to confirm/bake into ink.Review against the code (planned for the future, not started)
Platform
Recognition: use the strokes, not a raster crop
StylusSample). That makes online recognition possible: the order of the strokes and the pen direction are known. This is more accurate and much cheaper than recognising a 64 px image.page_export.rs), which is far too slow for < 25 ms. A dedicated stroke-to-tensor rasteriser would be needed.+ − · × : / ( ) = ^ x y a b. The model is a few hundred KB, runs on tract-onnx or as hand-written matrix multiplications, and needs well under 1 ms per symbol.2⁴is written as a raised4, there is no^on paper), fraction bars.Evaluation
evalexpruses^for powers and hasmath::sinetc. That covers arithmetic. But the recognised string has to be normalised first:2,5);·,×and:for multiply and divide;2x,3(x+1)).(x + 3)^2): as far as I know,kalkis a calculator with variables but does not expand polynomials, so that needs checking. A small polynomial module of our own (sums of monomials with rational coefficients;+ − ·and integer powers) is about 200 lines and fully testable. I would prefer it to an extra dependency.Triggering and conflicts
=may therefore arrive as snapped lines and not as ink, so detection must accept both.== two short, roughly horizontal, parallel strokes of similar length, drawn one after the other. The trigger fires after the pen is lifted and 500 ms pass without a new stroke. A new stroke cancels it.=on the same line (vertical overlap with a band around the=), with no large gap, written in this session.=in normal notes never gets a suggestion it did not ask for.Showing and inserting the result
insert_text_on_page, in pen colour and matching the height of the handwriting), as one undo step. It stays editable and is exported to PDF like other text. Turning it into ink would need a handwriting font, so it could come later as an option.Tests
Erste Version in v0.41.0, experimentell und standardmäßig aus.
=und kurzer Pause erscheint daneben das Gelesene samt Ergebnis, z. B.2⁴ = 16oder(x + 3)² = x² + 6x + 9.+ − · : /, Klammern, Hochzahlen, Bruchstriche, Dezimalkomma sowiex,y,a,b.Bisher nur mit künstlicher Handschrift getestet. Bitte mit echter Handschrift ausprobieren und melden, welche Zeichen falsch gelesen werden. Daraus entsteht der nächste Schritt: eigene Vorlagen für die eigene Handschrift.