feat(eraser): erase text boxes as a whole (Issue #54)
The eraser skipped text boxes along with images, so a typed text or an inserted math result could only be removed with the selection tool. The whole-stroke and partial erasers now remove a text box once they touch it; the highlighter eraser leaves it alone and images are still never erased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1b22ffa526
commit
055e39c24e
3 files changed
+78
-8
No files matched your search
+1
-1
@@ -27,7 +27,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra
|
||||
- **Glättung in zwei Stufen:** Während des Schreibens glättet ein 1€-Filter das Zittern, die Linie erscheint sofort. Beim Absetzen wird der Strich nachbearbeitet: Ecken (Knicke über 65°) bleiben erhalten und werden rund verbunden, der Druckverlauf wird geglättet, überflüssige Punkte entfallen, die Enden werden rund.
|
||||
- **Druckkurve** im ☰-Menü an den eigenen Stift anpassen (weicher/härter, Schwelle gegen Grundrauschen).
|
||||
- **Tasten am Stift** im ☰-Menü belegen: Radieren (Standard), Auswählen, Textmarker, Stift oder nichts – gilt, solange die Taste gedrückt ist. Jede Taste erscheint dort nach dem ersten Drücken.
|
||||
- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker; die Größe stellt man in einem Menü wie bei den Stiften ein (wird gemerkt). Die Stiftrückseite radiert ebenfalls.
|
||||
- **Radierer** in drei Modi: ganze Striche, nur den überstrichenen Teil (Teilstrich) oder nur Textmarker; berührte Textfelder verschwinden ganz, Bilder bleiben; die Größe stellt man in einem Menü wie bei den Stiften ein (wird gemerkt). Die Stiftrückseite radiert ebenfalls.
|
||||
- **Virtuelles Lineal** (`L`): verschieben, am Griff drehen (rastet bei 15° ein); Striche entlang einer Kante werden gerade.
|
||||
- **Nur ansehen** (👁 in der Werkzeugleiste): Stift, Finger und Maus scrollen und zoomen nur; die Werkzeuge verschwinden, nichts lässt sich versehentlich ändern. Erneut 👁 tippen, um zu schreiben.
|
||||
- **Platz schaffen** (↕ in der Werkzeugleiste): aufs Blatt tippen und nach unten ziehen – alles darunter rutscht mit, auch auf den folgenden Seiten. Was über ein Seitenende rutschen würde, beginnt auf der nächsten Seite; fehlende Seiten werden angehängt. Nach oben ziehen schließt leeren Platz. Ein Schritt für Rückgängig.
|
||||
|
||||
@@ -27,7 +27,7 @@ The interface is available in English and German; ink-notes follows the system l
|
||||
- **Two-stage smoothing:** While you write, a 1€ filter removes jitter and the line appears immediately. When you lift the pen, the stroke is refined: corners (bends over 65°) are kept and drawn round, the pressure curve is smoothed, redundant points are dropped and the ends become round.
|
||||
- **Pressure curve** adjustable to your pen in the ☰ menu (softer/harder, threshold against base noise).
|
||||
- **Pen buttons** can be assigned in the ☰ menu: erase (default), select, highlighter, pen or nothing – active while the button is held. Each button appears there after it is pressed for the first time.
|
||||
- **Eraser** with three modes: whole strokes, only the part you wipe over (partial stroke), or highlighter only; its size is set in a menu like the pens (remembered). The back end of the pen erases too.
|
||||
- **Eraser** with three modes: whole strokes, only the part you wipe over (partial stroke), or highlighter only; text boxes it touches go as a whole, images stay; its size is set in a menu like the pens (remembered). The back end of the pen erases too.
|
||||
- **Virtual ruler** (`L`): move it, rotate it by its handle (snaps to 15°); strokes along an edge become straight.
|
||||
- **View only** (👁 in the toolbar): pen, finger and mouse only scroll and zoom; the tools disappear and nothing can be changed by accident. Tap 👁 again to write.
|
||||
- **Make space** (↕ in the toolbar): tap the page and drag down – everything below moves along, also on the following pages. Whatever would slide past the end of a page continues on the next one; missing pages are appended. Dragging up closes empty space. Undo reverts it in one step.
|
||||
|
||||
+76
-6
@@ -275,7 +275,7 @@ pub enum EraserMode {
|
||||
/// Nur Textmarker – die Handschrift darunter bleibt erhalten
|
||||
HighlighterOnly,
|
||||
/// Nur den überstrichenen Teil eines Strichs entfernen; der Rest bleibt in Stücken
|
||||
/// erhalten. Formen werden als Ganzes radiert.
|
||||
/// erhalten. Formen und Textfelder werden als Ganzes radiert.
|
||||
Partial,
|
||||
}
|
||||
|
||||
@@ -578,6 +578,12 @@ impl Stroke {
|
||||
min_dist
|
||||
}
|
||||
|
||||
/// A text box goes as a whole once the eraser touches it, like a typed or inserted math
|
||||
/// result (Issue #54). Images are never erased.
|
||||
fn erased_as_text(&self, eraser_pos: glam::Vec2, eraser_radius: f32) -> bool {
|
||||
self.is_text() && self.distance_to_point(eraser_pos) <= eraser_radius
|
||||
}
|
||||
|
||||
/// Prüft, ob dieser Strich von einem Radierer an Position `eraser_pos` mit Radius `eraser_radius` getroffen wird.
|
||||
pub fn intersects_eraser(
|
||||
&self,
|
||||
@@ -942,10 +948,15 @@ impl Page {
|
||||
let mut hit_indices = Vec::new();
|
||||
|
||||
for (i, stroke) in self.strokes.iter().enumerate() {
|
||||
if !stroke.is_object()
|
||||
&& mode.erases(stroke.tool)
|
||||
&& stroke.intersects_eraser(eraser_pos, eraser_radius, max_stroke_radius)
|
||||
{
|
||||
let hit = if stroke.is_text() {
|
||||
mode != EraserMode::HighlighterOnly
|
||||
&& stroke.erased_as_text(eraser_pos, eraser_radius)
|
||||
} else {
|
||||
!stroke.is_object()
|
||||
&& mode.erases(stroke.tool)
|
||||
&& stroke.intersects_eraser(eraser_pos, eraser_radius, max_stroke_radius)
|
||||
};
|
||||
if hit {
|
||||
hit_indices.push(i);
|
||||
}
|
||||
}
|
||||
@@ -970,7 +981,7 @@ impl Page {
|
||||
|
||||
/// Teilradierer: entfernt den Teil aller Striche im Kreis um `eraser_pos`. Getroffene
|
||||
/// Striche werden durch ihre übrig gebliebenen Stücke ersetzt (neue IDs aus `next_id`),
|
||||
/// Formen als Ganzes entfernt, Bilder und Textfelder nie. Aufeinanderfolgende Schnitte
|
||||
/// Formen und Textfelder als Ganzes entfernt, Bilder nie. Aufeinanderfolgende Schnitte
|
||||
/// eines Radierzugs bilden einen gemeinsamen Undo-Schritt. Gibt `true` zurück, wenn sich etwas geändert hat.
|
||||
pub fn erase_partial_at(
|
||||
&mut self,
|
||||
@@ -982,6 +993,14 @@ impl Page {
|
||||
let mut added = Vec::new();
|
||||
let mut result = Vec::with_capacity(self.strokes.len());
|
||||
for stroke in std::mem::take(&mut self.strokes) {
|
||||
if stroke.is_text() {
|
||||
if stroke.erased_as_text(eraser_pos, eraser_radius) {
|
||||
removed.push(stroke);
|
||||
} else {
|
||||
result.push(stroke);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let reach = eraser_radius + stroke.width / 2.0;
|
||||
if stroke.is_object() || !stroke.aabb.expanded(reach).contains_point(eraser_pos) {
|
||||
result.push(stroke);
|
||||
@@ -2177,6 +2196,57 @@ mod tests {
|
||||
assert!(page.strokes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eraser_removes_text_boxes_but_not_images() {
|
||||
let corners = |x: f32| {
|
||||
[
|
||||
glam::Vec2::new(x, 100.0),
|
||||
glam::Vec2::new(x + 100.0, 100.0),
|
||||
glam::Vec2::new(x + 100.0, 150.0),
|
||||
glam::Vec2::new(x, 150.0),
|
||||
]
|
||||
};
|
||||
let text = || {
|
||||
Stroke::new_text(
|
||||
1,
|
||||
crate::text::TextBox::new("10".into(), 40.0, crate::text::TextFont::Sans, 100.0),
|
||||
corners(100.0),
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
)
|
||||
};
|
||||
for mode in [EraserMode::All, EraserMode::Partial] {
|
||||
let mut page = Page::new(1, PageBackground::Grid5mm);
|
||||
page.add_stroke(text());
|
||||
page.add_stroke(Stroke::new_image(2, "a".into(), corners(300.0)));
|
||||
let mut next_id = 10;
|
||||
let mut erase = |page: &mut Page, at: glam::Vec2| match mode {
|
||||
EraserMode::Partial => page.erase_partial_at(at, 10.0, &mut next_id),
|
||||
_ => !page.erase_at(at, 10.0, 2.0, mode).is_empty(),
|
||||
};
|
||||
// Next to the box: nothing; touching its edge or inside: the whole box goes
|
||||
assert!(!erase(&mut page, glam::Vec2::new(85.0, 120.0)));
|
||||
assert!(!erase(&mut page, glam::Vec2::new(350.0, 120.0)));
|
||||
assert!(erase(&mut page, glam::Vec2::new(95.0, 120.0)));
|
||||
assert_eq!(
|
||||
page.strokes.iter().map(|s| s.id).collect::<Vec<_>>(),
|
||||
vec![2]
|
||||
);
|
||||
assert!(page.undo());
|
||||
assert_eq!(page.strokes.len(), 2);
|
||||
}
|
||||
// The highlighter eraser leaves text alone
|
||||
let mut page = Page::new(1, PageBackground::Grid5mm);
|
||||
page.add_stroke(text());
|
||||
assert!(page
|
||||
.erase_at(
|
||||
glam::Vec2::new(150.0, 120.0),
|
||||
10.0,
|
||||
2.0,
|
||||
EraserMode::HighlighterOnly
|
||||
)
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pdf_page_size_follows_orientation() {
|
||||
// Hochformat (A4, Letter) bleibt A4 hoch
|
||||
|
||||
Reference in new issue
Block a user