feat: Stifte, Seiten löschen, lückenlose Seiten, sauberer Textmarker (Issues #11, #12, #14, #15, #17, #19)

- #15: Koordinatensystem ohne Kerbe am Ursprung (Achsen beginnen eine halbe
  Linienbreite hinter dem Ursprung; im PDF ein Linienzug mit spitzer Ecke)
- #17: Jeder Strich deckt ein Pixel nur einmal (Stencil-Puffer), der
  Textmarker wird innerhalb eines Strichs nicht dunkler. Im PDF-Export ist der
  Textmarker jetzt halbtransparent statt deckend.
- #11: ➕ in der Werkzeugleiste fügt nach der aktuellen Seite eine Seite ein
- #12: Seite löschen im ☰-Menü mit Rückfrage, in der Sitzung wiederherstellbar
- #14: Seiten liegen lückenlos untereinander, eine dunkle Linie markiert die Grenze
- #19: Vier Stifte mit eigener Farbe und Stärke, die sich Änderungen merken
  (ui.json); Blätterpfeile nur noch in der Einzelseitenansicht

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 15:55:32 +02:00
co-authored by Claude Opus 5.5
parent 904b36e5f7
commit 3890b6f182
14 changed files with 844 additions and 80 deletions
+36 -7
View File
@@ -1924,16 +1924,21 @@ pub fn shape_to_mesh(
(*y_axis_end, *x_axis_end)
};
// Beide Achsen beginnen um die halbe Linienbreite hinter dem Ursprung, damit die
// äußere Ecke geschlossen ist (sonst bleibt dort eine quadratische Kerbe)
let y_start = *origin - y_dir * half_w;
let x_start = *origin - x_dir * half_w;
// 1. Y-Achse (vom Ursprung zur Spitze)
let y_norm = Vec2::new(-y_dir.y, y_dir.x) * half_w;
vertices.push(InkVertex::new(
origin.x + y_norm.x,
origin.y + y_norm.y,
y_start.x + y_norm.x,
y_start.y + y_norm.y,
color,
));
vertices.push(InkVertex::new(
origin.x - y_norm.x,
origin.y - y_norm.y,
y_start.x - y_norm.x,
y_start.y - y_norm.y,
color,
));
vertices.push(InkVertex::new(
@@ -1950,15 +1955,15 @@ pub fn shape_to_mesh(
// Degenerierte Dreiecke für den Übergang zur X-Achse
let x_norm = Vec2::new(-x_dir.y, x_dir.x) * half_w;
let last_v = *vertices.last().unwrap();
let next_v = InkVertex::new(origin.x + x_norm.x, origin.y + x_norm.y, color);
let next_v = InkVertex::new(x_start.x + x_norm.x, x_start.y + x_norm.y, color);
vertices.push(last_v);
vertices.push(next_v);
// 2. X-Achse
vertices.push(next_v);
vertices.push(InkVertex::new(
origin.x - x_norm.x,
origin.y - x_norm.y,
x_start.x - x_norm.x,
x_start.y - x_norm.y,
color,
));
vertices.push(InkVertex::new(
@@ -2195,6 +2200,30 @@ mod tests {
}
}
#[test]
fn test_coordinate_system_corner_is_closed() {
// Die Achsen treffen sich ohne Kerbe: Die äußere Ecke des Ursprungs ist ein Eckpunkt
// der Achsenbänder (Issue #15)
let origin = Vec2::new(300.0, 900.0);
let shape = RecognizedShape::CoordinateSystem {
origin,
x_axis_end: Vec2::new(900.0, 900.0),
y_axis_end: Vec2::new(300.0, 300.0),
with_arrows: true,
};
let width = 30.0;
let (_, vertices) = shape_to_mesh(&shape, width, [0.0, 0.0, 0.0, 1.0]);
let corner = origin + Vec2::new(-width / 2.0, width / 2.0);
let nearest = vertices
.iter()
.map(|v| (Vec2::from(v.position) - corner).length())
.fold(f32::INFINITY, f32::min);
assert!(
nearest < 1e-3,
"äußere Ecke fehlt, nächster Punkt {nearest}"
);
}
#[test]
fn test_detect_triangle() {
let mut points = Vec::new();