Zwei Radierer und warum das Löschen zum Strich gehört
Ein eigener Radiererstrich liegt nahe. Objektbewegung bricht das Modell; in Lulucat Notes gehört das Löschen zum Strich, dessen Tinte es entfernt.
Wenn Menschen in einem Notizbuch radieren, meinen sie zwei verschiedene Dinge. Manchmal ist eine Markierung falsch und die ganze Markierung soll verschwinden: eine Hilfslinie, ein durchgestrichenes Wort, ein verirrter Strich von einer ruhenden Hand. Manchmal ist nur ein Teil einer Markierung falsch: ein halbes Zeichen, ein Zweig eines Diagramms, das Ende eines Textmarkerstrichs, der über den Satz hinausgelaufen ist. Lulucat Notes liefert jetzt beides als zwei getrennte Werkzeuge aus.
Die schwierigere Designfrage lautet, wo das Löschen verankert ist.
Der Strich-Radierer kam zuerst
Der erste Radierer löschte ganze Striche. Eine Berührung prüft jeden Strich: Bounding-Boxen weisen fast alle zurück, und die übrigen durchlaufen einen Test der quadrierten Punkt-zu-Segment-Distanz gegen den Radius des Radierers plus den entlang des Segments interpolierten Strichradius. Die Breite variiert mit dem Druck, daher würde eine einzige nominale Breite dünne Striche löschen, die der Benutzer nicht berührt hat, und dicke verfehlen, die berührt wurden.
Eine Geste wird zu einem Undo-Eintrag zusammengeführt, und der Eintrag speichert jeden entfernten Strich zusammen mit seinem ursprünglichen Index. Undo fügt sie in umgekehrter Reihenfolge wieder ein, sodass die Stapelreihenfolge exakt zurückkehrt.
Dieser Radierer ist schnell, vorhersehbar und kann kein halbes Wort entfernen.
MaLiang macht den Radierer zum Pinsel
MaLiang, das MIT-lizenzierte Zeichenframework, von dem wir bereits das Stempelmodell des Textmarkers übernommen haben, beantwortet teilweises Radieren in vierundzwanzig Zeilen. Sein Eraser ist eine Unterklasse von Brush, die eine Sache überschreibt: die Blend-Optionen, um zu subtrahieren statt zu addieren. Ein Radiererstrich ist ein Element in derselben Liste wie jeder andere Strich. Undo entfernt dieses Element. Kein vorhandener Strich wird verändert, denn das Löschen geschieht nur bei der Wiedergabe in Listenreihenfolge.
Wir lieferten dieses Modell aus, und zwei Gerätesitzungen brachten es zum Scheitern.
Objektverschieben legt den fehlenden Besitzer offen
Der erste Bericht: Wählen Sie Striche mit dem Lasso aus, ziehen Sie sie, und das Loch bleibt zurück.

Dieselben radierten Striche, nach rechts verschoben. Oben: Der Radierer ist ein eigenes Element. Unten: Das Löschen gehört zu den Strichen.
Die Auswahl verschlimmerte es. Ausgewählte Striche werden aus dem Bitmap in ein Overlay gehoben, das sie direkt zeichnet – ohne das Radiererelement –, sodass ein radierter Strich im Moment seiner Auswahl wieder vollständig erschien.
Der zweite Bericht: Der Radierer schwebte über dem Textmarker. Unser Renderer gibt in zwei Durchläufen wieder, Textmarker zuerst und Stifte danach, weil Textmarkertinte unter der übrigen Tinte liegen muss. Ein Radiererelement muss an beiden Durchläufen teilnehmen, sonst kann es nicht beide Arten löschen. Die Teilnahme am Stift-Durchlauf bedeutet, dass es nach jedem Textmarker ausgeführt wird, sodass ein Radierer auch Textmarkerstriche löschte, die zeitlich später gezeichnet worden waren.
Beide Fehler haben dieselbe Quelle. MaLiang kennt weder Lasso noch Verschieben noch eine Ebenenreihenfolge zwischen Pinseln, daher reicht dort ein Element ohne Besitzer aus. Unsere Leinwand hat alle drei Dinge, und ein Löschen ohne Besitzer hat keine definierte Antwort auf „Was passiert, wenn sich die Tinte bewegt?“ oder „Auf welcher Ebene arbeitet das gerade?“
Dem Strich zugeordnetes Löschen
Jeder Stroke trägt jetzt seine eigenen Löschpfade. Der Radierer prüft, welche Striche er berührt, und hängt an jeden davon einen Pfad an, in Leinwandkoordinaten. Beim Rendern eines Strichs werden seine Tinte und die eigenen Löschungen innerhalb einer Transparenzebene gezeichnet:
context.beginTransparencyLayer(auxiliaryInfo: nil)
drawInk(stroke, in: context, clip: clip, colorOverride: colorOverride)
context.setBlendMode(.destinationOut)
for path in stroke.erasePaths { fillErasePath(path, in: context) }
context.endTransparencyLayer()
Destination-out erhält das Ziel bei und entfernt es proportional zur Quell-Alpha:
Innerhalb einer Transparenzebene ist das Ziel dieser eine Strich, sodass die Operation weder das Papier noch die darunter liegenden oder später gezeichneten Striche erreichen kann. Die Ebene kostet für Striche ohne Löschungen nichts, und das betrifft fast alle: Sie verwenden den ursprünglichen Pfad.
Die Zuordnung beantwortet die beiden Fehler direkt. Durch die Verschiebung wandern die Löschpfade zusammen mit den Sample-Punkten, sodass ein verschobener Strich seine Lücken mitnimmt. Das Auswahl-Overlay zeichnet dasselbe Strichobjekt, daher zeigt ein ausgewählter Strich dieselben Lücken. Die Ebenenreihenfolge spielt keine Rolle mehr, weil ein Löschen niemals auf etwas anderes als den eigenen Strich angewendet wird.
Undo arbeitet auf Gestenebene: Ein Eintrag führt jedes von der Geste erzeugte (stroke, path)-Paar auf; Undo entfernt diese Pfade, Redo hängt sie wieder an. Die Tinte unter einem Löschen wird nie bearbeitet, daher muss nichts rekonstruiert werden.
Live-Feedback läuft über die Kachel-Bitmap
Der gerade gezeichnete Strich wird normalerweise als Overlay über der zusammengesetzten Seite gerendert. Ein Löschen kann dort nicht funktionieren, weil dieses Overlay sowohl auf Papier als auch auf Tinte liegt und destination-out das Papier durchstoßen würde.
Der Radierer schreibt daher sofort in die Striche, und jeder Batch neuer Samples löst nur für das betroffene Rechteck eine lokale Wiedergabe aus. Die Kachel-Pipeline löscht bereits ein Rechteck und gibt die sich damit überschneidenden Striche wieder; der Radierer verwendet diesen Pfad erneut und fügt keine zweite Zeichen-Semantik hinzu.
Beide Radierer nebeneinander
Die Werkzeugleiste hat zwei Radierer-Schaltflächen statt einer Schaltfläche mit verstecktem Modus. Der Teilradierer entfernt einen Teil eines Strichs. Der Strich-Radierer entfernt bei Kontakt ganze Striche. Ihre Radien sind getrennte Konstanten, derzeit beide 4 Punkte.
Beim Löschen eines ganzen Strichs verschwinden seine Löschpfade mit ihm, da sie Teil dieses Strichs sind, und Undo bringt beide gemeinsam zurück. Löschpfade sind auch Teil der gespeicherten Datei, quantisiert wie alles andere: Radius und Koordinaten als Zehntel eines Punktes.
Eine Einschränkung bleibt. Ein Strich, der bis auf nichts gelöscht wurde, existiert weiterhin als Daten. Er rendert als nichts, aber das Lasso kann ihn weiterhin auswählen und der Strich-Radierer kann ihn löschen, und er beansprucht weiterhin Platz im Dokument. Vollständig gelöschte Striche einzusammeln erfordert einen Abdeckungstest, den wir noch nicht geschrieben haben; vorerst bleiben sie daher erhalten.
Wenn ein Modell aus einem anderen Projekt in deinem nicht mehr funktioniert, liegt der Unterschied meist nicht im kopierten Code. Er steckt in den Funktionen, die das andere Projekt nicht hat.