feat(math): offer the result next to a handwritten = (Issue #46, stage 3)
New module math_assist.rs: when the last two strokes form an = and the pen rests for 500 ms, the strokes left of it (in a band around the =, up to a wide gap) are recognized and computed. The suggestion appears next to the = and shows what was read, e.g. "2⁴ = 16"; tapping it with the pen or clicking inserts the result as a text box in the ink color and as high as the handwriting, as one undo step. The chip is not interactive for egui, so fingers and palms pass through. Any new stroke drops it. In a chain like x = 2 · 3 = the part after the previous = is computed. Off by default: ☰ → Input → "Compute formulas after =", saved in ui.json. math::display writes the recognized expression neatly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d16cd2db72
commit
307c803246
10 files changed
+775
-14
No files matched your search
+115
-5
@@ -335,7 +335,7 @@ fn commit_stroke_to_notebook(
|
||||
notebook: &mut Notebook,
|
||||
manifest: &mut NotebookManifest,
|
||||
autosave: &mut AutosaveController,
|
||||
ui_state: &UiState,
|
||||
ui_state: &mut UiState,
|
||||
window: &Window,
|
||||
snapped_shape: Option<RecognizedShape>,
|
||||
) {
|
||||
@@ -357,6 +357,7 @@ fn commit_stroke_to_notebook(
|
||||
let page = notebook.current_page_mut();
|
||||
page.replace_stroke(previous_id, stroke);
|
||||
canvas.sync_page(page);
|
||||
ui_state.math.stroke_committed(page, Instant::now());
|
||||
autosave.mark_dirty(notebook, manifest);
|
||||
window.request_redraw();
|
||||
return;
|
||||
@@ -383,6 +384,8 @@ fn commit_stroke_to_notebook(
|
||||
let page = notebook.current_page_mut();
|
||||
page.add_stroke(stroke);
|
||||
canvas.sync_page(page);
|
||||
// An `=` may have been completed (Issue #46)
|
||||
ui_state.math.stroke_committed(page, Instant::now());
|
||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
||||
autosave.mark_dirty(notebook, manifest);
|
||||
// Durchgehende Ansicht: unten auf der letzten Seite geht es auf einer neuen weiter
|
||||
@@ -915,6 +918,54 @@ fn insert_text_on_page(
|
||||
ui_state.set_export_message(t!("Text eingefügt – mit dem Text-Werkzeug (T) änderbar"));
|
||||
}
|
||||
|
||||
/// Inserts the result of a math suggestion as a text box next to its `=`, in the ink color
|
||||
/// and as high as the handwriting, as one undo step (Issue #46)
|
||||
fn insert_math_result(
|
||||
suggestion: &crate::math_assist::Suggestion,
|
||||
canvas: &mut CanvasState,
|
||||
notebook: &mut Notebook,
|
||||
manifest: &mut NotebookManifest,
|
||||
autosave: &mut AutosaveController,
|
||||
) {
|
||||
let page = notebook.current_page();
|
||||
if page.id != suggestion.page_id || page.is_readonly() {
|
||||
return;
|
||||
}
|
||||
let font = crate::text::TextFont::Sans;
|
||||
let ascent_per_em = crate::text::TextBox::new(String::new(), 1.0, font, 1.0)
|
||||
.layout()
|
||||
.ascent;
|
||||
let (origin, size) = suggestion.text_placement(ascent_per_em);
|
||||
let mut edit = crate::text::TextEditing::new_at(
|
||||
page.id,
|
||||
origin,
|
||||
page.text_right_edge(origin.x),
|
||||
size,
|
||||
font,
|
||||
suggestion.color,
|
||||
);
|
||||
edit.origin = origin;
|
||||
edit.text.content = suggestion.result.clone();
|
||||
// As wide as the result, so that it does not wrap near the edge of the page
|
||||
edit.text.wrap_width = edit
|
||||
.text
|
||||
.layout()
|
||||
.lines
|
||||
.iter()
|
||||
.map(|line| line.width)
|
||||
.fold(0.0, f32::max)
|
||||
+ size;
|
||||
let id = notebook.next_stroke_id();
|
||||
let Some(stroke) = edit.to_stroke(id) else {
|
||||
return;
|
||||
};
|
||||
let page = notebook.current_page_mut();
|
||||
page.add_stroke(stroke);
|
||||
canvas.sync_page(page);
|
||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
||||
autosave.mark_dirty(notebook, manifest);
|
||||
}
|
||||
|
||||
/// Beendet eine laufende Textbearbeitung: übernimmt das Feld (neu, geändert oder – wenn
|
||||
/// geleert – entfernt) als einen Undo-Schritt auf seiner Seite.
|
||||
fn finish_text_edit(
|
||||
@@ -1419,6 +1470,7 @@ pub fn run_with(
|
||||
ui_state.pen_presets = ui_settings.pen_presets;
|
||||
ui_state.stylus_buttons = ui_settings.stylus_buttons.clone();
|
||||
ui_state.set_eraser_radius_mm(ui_settings.eraser_radius_mm);
|
||||
ui_state.math.set_enabled(ui_settings.math_assist);
|
||||
ui_state.select_pen(ui_settings.active_pen);
|
||||
ui_state.highlighter_colors = ui_settings.highlighter_colors;
|
||||
ui_state.select_highlighter_color(ui_settings.active_highlighter);
|
||||
@@ -1864,7 +1916,7 @@ pub fn run_with(
|
||||
&mut notebook,
|
||||
&mut manifest,
|
||||
&mut autosave,
|
||||
&ui_state,
|
||||
&mut ui_state,
|
||||
&window,
|
||||
stroke_snapped_shape.take(),
|
||||
);
|
||||
@@ -2048,6 +2100,26 @@ pub fn run_with(
|
||||
stylus_tool_id = Some(tool);
|
||||
let (sx, sy) = stylus_screen_pos;
|
||||
|
||||
// Tapping a math suggestion inserts its result (Issue #46)
|
||||
if let Some(suggestion) = (!ui_state.view_mode)
|
||||
.then(|| {
|
||||
ui_state.math.take_hit(egui::pos2(sx / scale, sy / scale))
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
insert_math_result(
|
||||
&suggestion,
|
||||
&mut canvas,
|
||||
&mut notebook,
|
||||
&mut manifest,
|
||||
&mut autosave,
|
||||
);
|
||||
// The rest of this tap goes nowhere
|
||||
stylus_ui_capture = true;
|
||||
window.request_redraw();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ui::is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale)
|
||||
{
|
||||
// In die Eingabe-Warteschlange des NÄCHSTEN egui-Frames
|
||||
@@ -2275,7 +2347,7 @@ pub fn run_with(
|
||||
&mut notebook,
|
||||
&mut manifest,
|
||||
&mut autosave,
|
||||
&ui_state,
|
||||
&mut ui_state,
|
||||
&window,
|
||||
snapped_shape,
|
||||
);
|
||||
@@ -2351,6 +2423,16 @@ pub fn run_with(
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
// Pause after an `=` is over: compute the expression (Issue #46)
|
||||
if ui_state.math.update(
|
||||
notebook.current_page(),
|
||||
Instant::now(),
|
||||
is_drawing || is_stylus_drawing,
|
||||
crate::i18n::language() == crate::i18n::Language::German,
|
||||
) {
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
// Nachgleiten nach dem Wischen
|
||||
if let Some(delta) = fling.step(Instant::now()) {
|
||||
canvas.pan(delta.x, delta.y);
|
||||
@@ -2377,6 +2459,7 @@ pub fn run_with(
|
||||
} else if let Some(at) = [
|
||||
egui_repaint_at,
|
||||
long_press.deadline(),
|
||||
ui_state.math.deadline(),
|
||||
Some(disk_watch.deadline()),
|
||||
// The updater thread does not wake the loop: look for its results now and then
|
||||
matches!(
|
||||
@@ -3176,6 +3259,25 @@ pub fn run_with(
|
||||
return;
|
||||
}
|
||||
|
||||
// Clicking a math suggestion inserts its result (Issue #46)
|
||||
if button == MouseButton::Left && !ui_state.view_mode {
|
||||
let (sx, sy) = cursor_screen_pos;
|
||||
let scale = window.scale_factor() as f32;
|
||||
if let Some(suggestion) =
|
||||
ui_state.math.take_hit(egui::pos2(sx / scale, sy / scale))
|
||||
{
|
||||
insert_math_result(
|
||||
&suggestion,
|
||||
&mut canvas,
|
||||
&mut notebook,
|
||||
&mut manifest,
|
||||
&mut autosave,
|
||||
);
|
||||
window.request_redraw();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fling.stop();
|
||||
// 1. Pan via mittlere Maustaste oder Leertaste + Linksklick
|
||||
// In view-only mode the left and right buttons move the page too (Issue #35)
|
||||
@@ -3380,7 +3482,7 @@ pub fn run_with(
|
||||
&mut notebook,
|
||||
&mut manifest,
|
||||
&mut autosave,
|
||||
&ui_state,
|
||||
&mut ui_state,
|
||||
&window,
|
||||
snapped_shape,
|
||||
);
|
||||
@@ -3958,7 +4060,7 @@ pub fn run_with(
|
||||
&mut notebook,
|
||||
&mut manifest,
|
||||
&mut autosave,
|
||||
&ui_state,
|
||||
&mut ui_state,
|
||||
&window,
|
||||
snapped_shape,
|
||||
);
|
||||
@@ -5187,6 +5289,14 @@ pub fn run_with(
|
||||
}
|
||||
}
|
||||
}
|
||||
UiAction::SetMathAssist(on) => {
|
||||
ui_settings.math_assist = on;
|
||||
save_ui_settings(
|
||||
&ui_settings,
|
||||
ui_settings_path.as_deref(),
|
||||
&mut ui_state,
|
||||
);
|
||||
}
|
||||
UiAction::SetSidebarSide(side) => {
|
||||
ui_settings.sidebar_side = side;
|
||||
save_ui_settings(
|
||||
|
||||
Reference in new issue
Block a user