Två suddgummin – och varför raderingen hör till strecket
Ett suddstreck som eget element är den självklara designen. Objektflytt bryter sönder den, så i Lulucat Notes hör raderingen till strecket vars bläck den tar bort.
När man suddar i en anteckningsbok kan man mena två olika saker. Ibland är ett streck fel och hela strecket ska bort: en hjälplinje, ett överstruket ord, ett oavsiktligt streck från en handflata som vilat mot sidan. Ibland är bara en del av ett streck fel: halva ett tecken, en gren i ett diagram, slutet på ett överstrykningsdrag som fortsatte förbi meningen. Lulucat Notes har nu båda, som två separata verktyg.
Den svårare designfrågan är var raderingen hör hemma.
Suddgummit för hela streck kom först
Det första suddgummit tog bort hela streck. En beröring testar varje streck: begränsningslådorna avvisar nästan alla, och de som återstår får genomgå ett test av det kvadrerade avståndet från punkt till segment mot suddgummits radie plus streckets radie, interpolerad längs segmentet. Bredden varierar med trycket, så en enda nominell bredd skulle ta bort tunna streck som användaren inte rört och missa tjocka som faktiskt berörts.
En gest blir en enda ångra-post, och posten sparar varje borttaget streck tillsammans med dess ursprungliga index. Ångra lägger tillbaka dem i omvänd ordning, så lagerordningen återställs exakt som den var.
Det här suddgummit är snabbt, förutsägbart och kan inte ta bort halva ett ord.
MaLiang gör suddgummit till en pensel
MaLiang, det MIT-licensierade ritramverk som vi redan lånat stämpelmodellen för överstrykningspennan från, löser delvis radering på 24 rader. Dess Eraser är en underklass av Brush som skriver över en sak, blandningsalternativen, för att subtrahera i stället för att addera. Ett suddstreck är ett element i samma lista som alla andra streck. Ångra tar bort det elementet. Inget befintligt streck ändras, eftersom raderingen bara sker under uppspelningen, i listordning.
Vi levererade modellen, och två enhetssessioner fick den att gå sönder.
Objektflytt blottlägger den saknade ägaren
Det första felet: välj streck med lassot, dra dem, och hålet blir kvar.

Samma raderade streck, flyttade åt höger. Ovan: suddgummit är ett eget element. Nedan: raderingen hör till strecken.
Markering gjorde saken värre. Markerade streck lyfts ut ur bitmappen till ett överlägg som ritar dem direkt, utan suddgummielementet, så i samma ögonblick som ett raderat streck markerades syntes det helt igen.
Det andra felet: suddgummit svävade ovanpå överstrykningspennan. Vår renderare spelar upp i två pass, först överstrykningspennor och sedan pennor, eftersom överstrykningsbläcket måste ligga under det vanliga bläcket. Ett suddgummielement måste delta i båda passen, annars kan det sudda båda sorterna. Att delta i pennpasset innebär att det körs efter varje överstrykningspenna har ritats, så ett suddgummielement skar också bort överstrykningsstreck som ritats senare.
Båda felen har samma källa. MaLiang har inget lasso, ingen flyttning och ingen lagerordning mellan penslar, så där räcker ett element utan ägare. Vår canvas har alla tre, och en radering utan ägare ger inget definierat svar på ”vad händer när bläcket flyttas?” eller ”vilket lager verkar detta på?”.
Radering som ägs av strecket
Varje Stroke bär nu sina egna raderingsbanor. Suddgummit testar vilka streck det träffar och lägger till en bana i vart och ett av dem, i canvas-koordinater. Att rendera ett streck ritar dess bläck och dess egna raderingar i ett transparenslager:
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 behåller destinationen och tar bort den i proportion till källans alfa:
Inuti ett transparenslager är destinationen just det strecket, så operationen kan inte nå papperet, strecken under eller strecken som ritas senare. Lagret kostar inget för streck utan raderingar, vilket gäller nästan alla: de tar den ursprungliga renderingsvägen.
Detta ägarskap löser båda felen direkt. En förflyttning flyttar raderingsbanorna tillsammans med sampelpunkterna, så ett flyttat streck bär med sig sina luckor. Markeringsöverlägget ritar samma streckobjekt, så ett markerat streck visar samma luckor. Lagerordningen slutar spela roll, eftersom en radering aldrig påverkar något annat än det egna strecket.
Ångra arbetar på gestnivå: en post listar varje (stroke, path)-par som gesten skapade, ångra tar bort dessa banor och kommandot Gör om lägger till dem igen. Bläcket under en radering redigeras aldrig, så inget behöver rekonstrueras.
Direkt återkoppling går via tile-bitmappen
Det streck som ritas renderas normalt som ett överlägg ovanpå den kompositade sidan. En radering fungerar inte där, eftersom överlägget ligger ovanpå både papper och bläck, och destination-out skulle slå hål genom papperet.
Därför skriver suddgummit omedelbart in i strecken, och varje batch med nya sampel utlöser en lokal uppspelning av endast den påverkade rektangeln. Tile-pipelinen rensar redan en rektangel och spelar upp de streck som skär den; suddgummit återanvänder den vägen och lägger inte till någon ny ritsemantik.
De båda suddgummina, sida vid sida
Verktygsfältet har två suddknappar i stället för en knapp med ett dolt läge. Suddgummit för delvis radering tar bort en del av ett streck. Suddgummit för hela streck tar bort hela streck vid kontakt. Deras radier är separata konstanter, båda för närvarande 4 punkter.
När ett helt streck tas bort följer dess raderingsbanor med eftersom de ingår i strecket, och ångra tar tillbaka båda tillsammans. Raderingsbanor ingår också i den sparade filen och kvantiseras som allt annat: radie och koordinater i tiondelar av en punkt.
En begränsning kvarstår. Ett streck som suddats ned till ingenting finns fortfarande som data. Det renderas inte alls, men lassot kan fortfarande markera det och suddgummit för hela streck kan fortfarande ta bort det, och det tar fortfarande plats i dokumentet. Att samla in helt raderade streck kräver ett täckningstest som vi inte har skrivit, så tills vidare blir de kvar.
När en modell från ett annat projekt slutar fungera i ditt, ligger skillnaden vanligtvis inte i koden du kopierade. Den finns i funktionerna som det projektet saknar.