Twee wissers, en waarom het wissen bij de streek hoort
Een wisstreek als zelfstandig element ligt voor de hand, maar objecten verplaatsen maakt dat model stuk. Daarom hoort wissen in Lulucat Notes bij de streek waaruit de inkt wordt verwijderd.
Wanneer mensen in een notitieboek wissen, kunnen ze twee verschillende dingen bedoelen. Soms klopt een markering niet en moet de hele markering weg: een hulplijn, een doorgestreept woord of een verdwaalde streek van een rustende handpalm. Soms klopt slechts een deel van een markering niet: een halve letter, één vertakking van een diagram of de staart van een markeerstiftveeg die voorbij de zin liep. Lulucat Notes levert nu beide als twee afzonderlijke gereedschappen.
De moeilijkere ontwerpvraag is waar het wissen thuishoort.
De streekwisser kwam eerst
De eerste wisser verwijderde hele streken. Bij elke aanraking worden alle streken getest: bounding boxes wijzen bijna alle streken af, en op de overgebleven streken volgt een test van de gekwadrateerde punt-tot-segmentafstand tegen de wisradius plus de streekradius die langs dat segment wordt geïnterpoleerd. De breedte varieert met de druk, dus zou één nominale breedte dunne streken verwijderen die de gebruiker niet heeft aangeraakt en dikke streken missen die wel zijn aangeraakt.
Eén gebaar wordt samengevoegd tot één undo-item, en dat item bewaart elke verwijderde streek samen met zijn oorspronkelijke index. Ongedaan maken voegt ze in omgekeerde volgorde weer in, zodat de stapelvolgorde precies terugkomt.
Deze wisser is snel en voorspelbaar, maar kan geen half woord verwijderen.
MaLiang maakt van de wisser een penseel
MaLiang, het tekenframework onder MIT-licentie waarvan we het markeerstiftstempelmodel al hadden geleend, lost gedeeltelijk wissen op in 24 regels. Zijn Eraser is een Brush-subklasse die één ding overschrijft, de blendopties, om af te trekken in plaats van op te tellen. Een wisstreek is een element in dezelfde lijst als elke andere streek. Ongedaan maken verwijdert dat element. Geen bestaande streek wordt gewijzigd, omdat het wissen alleen tijdens het opnieuw afspelen, in lijstvolgorde, gebeurt.
We brachten dat model uit, en twee sessies op apparaten maakten het stuk.
Objecten verplaatsen legt de ontbrekende eigenaar bloot
De eerste melding: selecteer streken met de lasso, sleep ze, en het gat blijft achter.

Dezelfde gewiste streken, naar rechts verplaatst. Boven: de wisser is een eigen element. Onder: het wissen hoort bij de streek.
Selectie maakte het erger. Geselecteerde streken worden uit de bitmap getild naar een overlay die ze rechtstreeks tekent, zonder het wisser-element. Zodra je een gewiste streek selecteerde, leek die daardoor weer volledig.
De tweede melding: de wisser zweefde boven de markeerstift. Onze renderer speelt in twee passes opnieuw af, eerst markeerstiften en daarna pennen, omdat markeerstiftinkt onder inkt moet liggen. Een wisser-element moet aan beide passes deelnemen, anders kan het niet beide soorten wissen. Deelname aan de pen-pass betekent dat het na elke markeerstift wordt uitgevoerd, waardoor een wisser ook markeerstiftstreken weghaalde die later in de tijd waren getekend.
Beide problemen hebben dezelfde oorzaak. MaLiang heeft geen lasso, geen verplaatsen en geen laagvolgorde tussen penselen, dus daar volstaat een element zonder eigenaar. Ons canvas heeft alle drie, en een wisactie zonder eigenaar biedt geen gedefinieerd antwoord op “wat gebeurt er als de inkt beweegt” of “op welke laag werkt dit?”
Wissen dat bij de streek hoort
Elke Stroke draagt nu zijn eigen wispaden. De wisser test welke streken hij raakt en voegt aan elk daarvan een pad toe, in canvascoördinaten. Eén streek renderen betekent de inkt en het eigen wissen ervan binnen een transparantielaag tekenen:
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 houdt de bestemming in stand en verwijdert die naar verhouding van de alfa van de bron:
Binnen een transparantielaag is de bestemming die ene streek, dus kan de bewerking het papier, de onderliggende streken of de later getekende streken niet bereiken. De laag kost niets voor streken zonder wissen, en dat zijn vrijwel alle streken: die volgen het oorspronkelijke pad.
Eigenaarschap beantwoordt de twee problemen rechtstreeks. Translatie verplaatst de wispaden samen met de samplepunten, dus een verplaatste streek draagt zijn gaten mee. De selectie-overlay tekent hetzelfde streekobject, dus een geselecteerde streek toont dezelfde gaten. Laagvolgorde doet er niet meer toe, want wissen wordt nooit toegepast op iets anders dan de eigen streek.
Ongedaan maken werkt per gebaar: één item vermeldt elk (stroke, path)-paar dat het gebaar heeft voortgebracht, ongedaan maken verwijdert die paden en opnieuw uitvoeren voegt ze weer toe. De inkt onder een wisactie wordt nooit bewerkt, dus niets hoeft te worden gereconstrueerd.
Live feedback loopt via de tegelbitmap
De streek die wordt getekend, wordt normaal als overlay boven de samengestelde pagina gerenderd. Wissen kan daar niet werken, omdat die overlay boven papier én inkt ligt en destination-out door het papier heen zou slaan.
De wisser schrijft daarom onmiddellijk in de streken, en elke batch nieuwe samples activeert alleen een lokale replay van de getroffen rechthoek. De tegelpijplijn wist al een rechthoek en speelt de streken die hem raken opnieuw af; de wisser hergebruikt dat pad en voegt geen tweede tekensemantiek toe.
Beide wissers naast elkaar
De werkbalk heeft twee wisserknoppen in plaats van één knop met een verborgen modus. De gedeeltelijke wisser haalt een deel van een streek weg. De streekwisser verwijdert bij contact hele streken. Hun radii zijn aparte constanten, beide momenteel 4 punten.
Een hele streek verwijderen neemt de wispaden mee, omdat die deel uitmaken van die streek, en ongedaan maken brengt beide samen terug. Wispaden maken ook deel uit van het opgeslagen bestand, gekwantiseerd zoals al het andere: straal en coördinaten als tienden van een punt.
Eén beperking blijft bestaan. Een streek die volledig is weggegumd, bestaat nog steeds als data. Hij rendert als niets, maar de lasso kan hem nog altijd selecteren en de streekwisser kan hem nog altijd verwijderen, en hij neemt nog steeds ruimte in het document in. Het verzamelen van volledig gewiste streken vereist een dekkingstest die we nog niet hebben geschreven, dus voorlopig blijven ze staan.
Wanneer een model uit een ander project in het jouwe niet meer werkt, ligt het verschil meestal niet in de code die je hebt gekopieerd. Het zit in de functies die dat project niet heeft.