Ob ein Notizbuch Seite für Seite oder durchgehend angezeigt wird, steht jetzt im Manifest (`layout`). Beim Anlegen ist „Feste Seiten“ vorgewählt; im ☰-Menü lässt sich die Anordnung des offenen Notizbuchs umschalten. Notizbücher ohne Angabe behalten die frühere globale Einstellung, ältere App-Versionen ignorieren das Feld. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
14 KiB
Architektur von ink-notes
Dieses Dokument beschreibt, wie ink-notes intern aufgebaut ist: Eingabe und Strichverarbeitung, Rendering, Seiten und Ansicht, Formerkennung, Speichern und Export. Das Speicherformat steht in FILE_FORMAT.md.
1. Überblick
ink-notes ist ein einzelnes Programm mit einem Event-Loop (winit) im Haupt-Thread. Dazu kommen drei Hintergrund-Threads:
| Thread | Aufgabe |
|---|---|
| Haupt-Thread | Eingaben, Datenmodell, Rendering (wgpu), Oberfläche (egui) |
| Autosave | Seiten und Manifest auf die Festplatte schreiben (autosave.rs) |
| PDF-Rendering | Importierte PDF-Seiten zu Texturen rendern (pdf_doc.rs, hayro) |
| Export | PDF/PNG/SVG-Export, damit die Oberfläche nicht hängt (main.rs) |
Stifteingaben kommen über octotablet (Tablet-Protokoll von Wayland bzw. X11), Maus und Touch über winit. Die Oberfläche (Werkzeugleiste, Menüs, Dialoge, Textbearbeitung) ist mit egui gebaut und wird im selben Render-Durchgang über das Blatt gezeichnet.
2. Vom Stift zum Strich
[Stift / Touch / Maus]
│ Bildschirmposition, Druck, Zeit
▼
[Bildschirm → Seitenkoordinaten] (viewport.rs)
│
▼
[Druckkurve] (settings.rs, vom Nutzer einstellbar)
│
▼
[1€-Filter] (filter.rs)
│
▼
[Catmull-Rom-Spline + Dreiecksstreifen] (spline.rs) → Live-Puffer auf der GPU
│ Stift abgehoben
▼
[Nachbearbeitung] (spline.rs: finalize_stroke)
│
▼
[Strich auf der Seite + Undo-Eintrag] (document.rs, history.rs)
2.1 1€-Filter
Langsame Bewegungen zittern, schnelle sollen nicht nachhinken. Der 1€-Filter (filter.rs) passt seine Grenzfrequenz an die Geschwindigkeit an:
f_c = f_{c,\min} + \beta \cdot |\dot{x}|
Bei langsamer Bewegung glättet er stark, bei schneller kaum.
2.2 Zentripetaler Catmull-Rom-Spline
Die gefilterten Punkte werden mit einem zentripetalen Catmull-Rom-Spline (\alpha = 0{,}5) interpoliert:
t_{i+1} = t_i + \lVert P_{i+1} - P_i \rVert^{\alpha}
Diese Parametrisierung erzeugt bei engen Kurven keine Schlaufen oder Spitzen und ist an den Stützstellen $C^1$-stetig.
2.3 Dreiecksstreifen
Aus den Spline-Punkten mit Radius r_i (aus Druck und Strichstärke) entsteht ein Dreiecksstreifen:
V_{i,\text{links}} = P_i + N_i \cdot r_i, \qquad V_{i,\text{rechts}} = P_i - N_i \cdot r_i
N_i ist die Normale der Laufrichtung; an inneren Punkten wird über benachbarte Segmente gemittelt.
2.4 Zwei Stufen
-
Während des Schreibens: Filter und Spline arbeiten nur mit bisherigen Punkten; der Strich wird sofort in einen eigenen Live-Puffer geschrieben.
-
Beim Absetzen wird der ganze Strich noch einmal bearbeitet (
finalize_stroke):- Druckverlauf mit dem Fenster
[0{,}25;\ 0{,}5;\ 0{,}25]glätten, - Punkte mit Ramer-Douglas-Peucker ausdünnen (
\varepsilon = 0{,}75Einheiten= 0{,}075mm), - an Knicken über 65° den Spline auftrennen, damit Ecken nicht abgerundet werden,
- runde Endkappen anfügen.
Danach wird der Live-Puffer geleert und der Strich in die Seite übernommen.
- Druckverlauf mit dem Fenster
2.5 Lineal
Beginnt ein Strich nahe einer Linealkante (ruler.rs), werden alle seine Punkte auf diese Kante projiziert, bis der Stift abgehoben wird.
3. Koordinaten, Seiten und Ansicht
3.1 Seitenkoordinaten
Gerechnet wird in Seiteneinheiten: 10 Einheiten = 1 mm. Eine DIN-A4-Seite ist 2100 × 2970 Einheiten groß. Jede Seite hat ihre eigene Größe (Page::size): Standard ist A4 hoch; beim PDF-Import bekommen Querformat-Seiten 2970 Einheiten Breite und eine Höhe nach Seitenverhältnis (document::pdf_page_size). Neue Seiten übernehmen die Größe der aktuellen.
3.2 Maßstab und Kamera
PageViewport(document.rs) passt einen Rahmen, in den alle Seiten des Notizbuchs passen (Notebook::fit_box), ins Fenster ein. Daraus ergibt sich ein Maßstab (Bildschirmpixel je Einheit), der für alle Seiten gilt. Die aktuelle Seite liegt waagerecht zentriert und oben im Rahmen. Den Platz der Werkzeugleiste rechnet der Viewport heraus.ViewportState(viewport.rs) ergänzt Zoom (25 % bis 1000 %) und Verschiebung (Pan). Beim Zoomen bleibt der Punkt unter dem Mauszeiger stehen:
\vec{p}_{\text{neu}} = \vec{c} - (\vec{c} - \vec{p}_{\text{alt}}) \cdot \frac{z_{\text{neu}}}{z_{\text{alt}}}
- Im Shader wird jeder Punkt mit
transform_scaleundtransform_offset(ausCameraUniforms) direkt in Gerätekoordinaten umgerechnet.
3.3 Durchgehende Ansicht
Ob ein Notizbuch Seite für Seite oder durchgehend angezeigt wird, steht in seinem Manifest (layout, siehe FILE_FORMAT.md).
Alle Koordinaten beziehen sich auf die aktuelle Seite. PageStack (viewport.rs) legt die übrigen Seiten lückenlos darunter bzw. darüber (die Grenze zeichnet die Oberfläche als dunkle Linie), waagerecht zur aktuellen Seite zentriert. Jede sichtbare Nachbarseite wird mit einer eigenen Kamera gezeichnet, die um die Lage der Seite verschoben ist (höchstens 12 Nachbarseiten).
Nach dem Scrollen wird die Seite in der Fenstermitte zur aktuellen Seite; die Kamera wird dabei so verschoben, dass sich das Bild nicht bewegt. Während eines Strichs, einer Auswahl, beim Ziehen des Lineals oder beim Schreiben von Text wird nicht gewechselt. Wer auf einer sichtbaren Nachbarseite zu zeichnen beginnt, wechselt zuerst auf diese Seite.
4. Rendering
Ein Frame besteht aus einem Render-Durchgang auf die Fensterfläche (canvas.rs):
- Für jede sichtbare Nachbarseite: Papier und Muster, Bilder/PDF-Seite/Text, Tinte.
- Für die aktuelle Seite: Papier, Muster (Karo, Linien, Punkte), Bildebene, gespeicherte Striche, Live-Strich.
- Die
egui-Oberfläche: Werkzeugleiste, Menüs, Seitengrenzen, Auswahlrahmen und Griffe, Lineal, Textbearbeitung.
Jeder Strich ist ein eigener Draw-Call mit eigener Nummer im Stencil-Puffer: Ein Pixel, das der Strich schon gefärbt hat, wird kein zweites Mal gefärbt. So bleibt halbtransparente Tinte (Textmarker) gleichmäßig, auch wo sich der Strich selbst überlappt (Anfangskappe, Kehren, langsames Hin- und Herfahren); erst ein neuer Strich deckt stärker. Nach 255 Strichen setzt ein bildschirmfüllendes Dreieck den Stencil-Puffer zurück.
4.1 Bildebene
image_layer.rs zeichnet texturierte Vierecke unter der Tinte: importierte PDF-Seiten, eingefügte Bilder und Textfelder. Texturen werden beim ersten Bedarf geladen, bekommen Mipmaps und werden freigegeben, wenn mehr als 384 MB belegt sind.
- PDF-Seiten rendert ein Hintergrund-Thread mit
hayro(längste Kante 2560 Pixel, beim starken Hineinzoomen 5120 Pixel). Bis die schärfere Fassung fertig ist, bleibt die vorhandene sichtbar. - Textfelder werden mit
ab_glyphgerastert, in einer Auflösung, die dem Zoom folgt (in Vierteln einer Zweierpotenz, höchstens 8 Pixel je Einheit). Der Texturschlüssel enthält einen Hash von Inhalt, Schrift und Farbe; jede Änderung erzeugt eine neue Textur.
4.2 Papier- und Musterfarben
Papierfarbe (PaperTheme) und Musterfarbe (PatternColor) stehen in background.rs; sie sind linear angegeben und werden im Shader nach sRGB umgerechnet. Tintenfarben (Striche, Text) sind sRGB-Werte – dieselben Zahlen landen im PDF und in den Farbpunkten der Oberfläche. Gezeichnet wird ohne sRGB-Umrechnung der GPU (render_format), Texturen sind Rgba8Unorm: So wird halbtransparente Tinte wie im PDF in sRGB gemischt und sieht am Bildschirm genauso aus wie im Export. Auf dunklem Papier kann der Shader dunkle Tinte aufhellen (ink_params in CameraUniforms), gespeichert werden immer die Originalfarben.
5. Objekte auf der Seite
Alles auf einer Seite ist ein Stroke (document.rs):
- Tinte: Abtastpunkte, Werkzeug (Stift, Textmarker, Form), Breite, Farbe; die Dreiecke werden beim Laden aus den Punkten neu berechnet.
- Formen: zusätzlich eine
RecognizedShape(siehe Abschnitt 6). - Bilder und Textfelder: vier Eckpunkte (oben links, oben rechts, unten rechts, unten links) plus Verweis auf die Bilddatei bzw. ein
TextBox. Weil die Ecken normale Punkte sind, wirken Verschieben, Drehen und Skalieren der Auswahl ohne Sonderfälle.
5.1 Textfelder
text.rs setzt den Text selbst: Zeilenumbruch an Leerzeichen (zu lange Wörter zeichenweise), Unterschneidung, Tabulator = 4 Leerzeichen. Bildschirm-Textur, PDF-Export und Bearbeitung verwenden dieselben Schriften (Ubuntu Light und Hack aus den eingebauten egui-Schriften) und denselben Umbruch. Bearbeitet wird in einem egui-Textfeld an der Stelle des Felds; danach wird die Höhe des Felds an den Text angepasst, Lage, Drehung und Skalierung bleiben.
5.2 Rückgängig
Jede Seite hat einen eigenen Verlauf (history.rs): Hinzufügen, Entfernen, Ersetzen und Ändern von Strichen. Verschieben/Drehen/Skalieren einer Auswahl, ein Radierzug mit dem Teilstrich-Radierer und eine Textbearbeitung sind jeweils ein Schritt.
Platz schaffen (Notebook::make_space) betrifft mehrere Seiten: Die Seite, auf der angesetzt wurde, und die folgenden bis vor die nächste PDF-Seite gelten als durchgehende Spalte. Alles unterhalb der Ansatzhöhe rutscht um denselben Betrag; ein Objekt, das über ein Seitenende ragen würde, beginnt 10 mm unter dem Anfang der nächsten Seite, und alles danach rutscht entsprechend weiter. Fehlende Seiten werden eingefügt. Der Schritt (SpaceShift: jeder verschobene Strich vorher/nachher mit Seite) landet im Verlauf der Seite, auf der angesetzt wurde; Notebook::undo/redo buchen ihn über alle Seiten zurück (später geänderte Striche bleiben unberührt) und entfernen dafür angelegte, leer gebliebene Seiten wieder.
6. Formerkennung
geometry.rs erkennt Formen rein geometrisch, ohne Machine Learning. Zuerst wird auf ein Achsenkreuz geprüft (alle Teilstücke liegen auf einer waagerechten und einer senkrechten Linie, beide reichen auf beiden Seiten über den Schnittpunkt hinaus). Geschlossene Formen (Kreis, Ellipse, Dreieck, Rechteck, Raute) treten gegeneinander an; gewählt wird die, deren Umriss am besten zu den Punkten passt. Danach folgen Vielecke, Pfeile, Koordinatensysteme und Linien. Übernommen wird eine Form nur, wenn der Strich im Mittel höchstens 4 % seiner Diagonale von ihr abweicht – sonst bleibt er Handschrift.
- Linie: Verhältnis von Abstand Anfang–Ende zur Strichlänge über 0,92.
- Kreis, Ellipse: Kreis- bzw. Ellipsen-Fit über die Punkte.
- Rechteck, Dreieck, Raute, Vieleck, Pfeil: Analyse der mit Douglas-Peucker vereinfachten Kontur (Eckenzahl, Winkel, Seitenverhältnisse).
- Koordinatensystem: zwei etwa rechtwinklige Achsen; der Winkel wird auf 90° gesetzt.
- Achsenkreuz: ein „+“ in einem Zug; die Achsen werden waagerecht und senkrecht ausgerichtet und bekommen an allen vier Enden Pfeilspitzen.
- Oktaeder: Skizze einer Doppelpyramide als 3D-Kantenmodell; nur, wenn kein einfacher Umriss passt.
Halten am Strichende: Bleibt der Stift über 400 ms innerhalb von 3 Pixeln ruhig, wird die erkannte Form schon vor dem Absetzen angezeigt und beim Absetzen übernommen. Mit dem Formen-Werkzeug wird jeder Strich beim Absetzen geprüft.
7. Speichern
[Änderung]
│ mark_page_dirty / mark_manifest_dirty
▼
[AutosaveController] ── 1,5 s nach der letzten Änderung, spätestens nach 10 s ──► [Autosave-Thread]
│
▼
[temporäre Datei schreiben + fsync]
│
▼
[rename über die alte Datei, Ordner-fsync]
- Der Haupt-Thread schreibt nie selbst; er übergibt fertige Seitendaten an den Autosave-Thread.
- Schlägt ein Schreibvorgang fehl, wird die Seite mit wachsendem Abstand (1 s bis 30 s) erneut versucht; der Status wird rot.
flush()schreibt alles Ausstehende und wartet darauf. Es läuft beiStrg + S, beim Wechsel des Notizbuchs und beim Beenden. Schlägt es beim Beenden fehl, bleibt die App offen und bietet Wiederholen oder einen Notfall-Export der ungespeicherten Seiten an (~/.local/share/ink-notes/recovery/).- Seiten werden erst beim Anzeigen geladen (Lazy Loading); Titel, Hintergrund und Größe stehen im Manifest, damit Seitenübersicht und durchgehende Ansicht sie vorher kennen.
- Ein zweites Fenster mit demselben Notizbuch öffnet es schreibgeschützt (Sperrdatei).
8. Export
export.rs erzeugt das PDF mit printpdf. Jede Seite bekommt ihre eigene Größe und diese Ebenen:
- Background: Karo, Linien oder Punkte in der gewählten Musterfarbe (immer auf weißem Papier).
- PDF (nur im Notfall): importierte PDF-Seite als Bild, falls sie sich nicht als Vektor übernehmen lässt.
- Images: eingefügte Bilder (höchstens 300 dpi).
- Text: Textfelder als echter Text mit eingebetteter Schrift (durchsuchbar, kopierbar). Jedes Feld liegt in einem eigenen Koordinatensystem, das eine Transformationsmatrix auf seine Ecken abbildet.
- Ink: Striche als gefüllte Polygone, Formen als Pfade. Halbtransparente Striche (Textmarker) bekommen einen Grafikzustand mit Deckkraft (
ca/CA); printpdf kann solche Zustände nicht anlegen, deshalb trägtadd_alpha_statessie nach dem Speichern mitlopdfin die Seitenressourcen ein. Jeder Strich ist ein einziger Pfad und deckt sich daher nicht selbst doppelt.
Importierte PDF-Seiten werden anschließend als Form-XObject unter die Seite gelegt (pdf_merge.rs), bleiben also Vektorgrafik. Seitentitel werden zu Lesezeichen.
PNG und SVG einer Seite (page_export.rs) entstehen aus diesem PDF: hayro rastert es bzw. wandelt es in SVG um. So sehen alle Exporte gleich aus.
9. Updates
updater.rs fragt die Gitea-Releases ab, vergleicht die Version (SemVer) und lädt auf Wunsch das passende Paket. Vor der Installation prüft er die Minisign-Signatur von SHA256SUMS.txt gegen den eingebauten Schlüssel und danach die SHA-256-Prüfsumme des Pakets. Ein AppImage wird per rename ersetzt; ein .deb-Paket wird mit xdg-open an die Softwareinstallation des Systems übergeben. Details im README unter „Sicherheit der Updates“.