Dues gomes d'esborrar, i per què l'esborrat pertany al traç
Un traç d'esborrat com a element propi és el disseny evident. El moviment d'objectes el trenca; per això, a Lulucat Notes, l'esborrat pertany al traç del qual elimina la tinta.
Quan la gent esborra en una llibreta, pot voler dir dues coses diferents. De vegades una marca és incorrecta i cal fer desaparèixer tota la marca: una línia guia, una paraula ratllada, un traç perdut d’un palmell recolzat. De vegades només és incorrecta una part de la marca: la meitat d’un caràcter, una branca d’un diagrama, la cua d’una passada de ressaltador que ha anat més enllà de la frase. Lulucat Notes ofereix ara totes dues opcions, com a dues eines separades.
La pregunta de disseny més difícil és on viu l’esborrat.
La goma d’esborrar de traços va arribar primer
La primera goma d’esborrar eliminava traços sencers. Un toc prova tots els traços: les caixes delimitadores en rebutgen gairebé tots, i als supervivents se’ls aplica una prova de distància quadràtica punt-segment respecte del radi de la goma més el radi del traç interpolat al llarg del segment. L’amplada varia amb la pressió, de manera que una única amplada nominal eliminaria traços prims que l’usuari no havia tocat i no detectaria els gruixuts que sí que havia tocat.
Un gest es converteix en una única entrada de desfer, i l’entrada emmagatzema cada traç eliminat juntament amb el seu índex original. Desfer els torna a inserir en ordre invers, de manera que l’ordre d’apilament torna exactament a ser el mateix.
Aquesta goma és ràpida, previsible i incapaç d’eliminar la meitat d’una paraula.
MaLiang converteix la goma en un pinzell
MaLiang, el framework de dibuix amb llicència MIT del qual ja havíem manllevat el model de segells del ressaltador, resol l’esborrat parcial en vint-i-quatre línies. La seva Eraser és una subclasse de Brush que només sobreescriu una cosa: les opcions de fusió, per restar en lloc de sumar. Un traç d’esborrat és un element de la mateixa llista que qualsevol altre traç. Desfer elimina aquest element. No es modifica cap traç existent, perquè l’esborrat només té lloc durant la reproducció, en l’ordre de la llista.
Vam distribuir aquest model, i dues sessions de dispositiu el van trencar.
El moviment d’objectes revela el propietari que faltava
El primer informe: selecciona traços amb el llaç, arrossega’ls i el forat es queda enrere.

Els mateixos traços esborrats, moguts cap a la dreta. A dalt: la goma és un element independent. A baix: l’esborrat pertany als traços.
La selecció ho empitjorava. Els traços seleccionats surten del mapa de bits cap a una superposició que els dibuixa directament, sense l’element d’esborrat, de manera que en seleccionar un traç esborrat tornava a aparèixer sencer.
La segona denúncia: la goma flotava per damunt del ressaltador. El nostre renderitzador reprodueix en dues passades, primer els ressaltadors i després els bolígrafs, perquè la tinta del ressaltador ha de quedar sota la tinta. Un element d’esborrat ha de participar en totes dues passades; si no, no pot esborrar cap dels dos tipus. Participar en la passada dels bolígrafs vol dir executar-lo després d’haver dibuixat cada ressaltador, de manera que una goma també tallava els traços de ressaltador dibuixats més tard en el temps.
Tots dos errors tenen el mateix origen. MaLiang no té llaç, ni moviment, ni ordre de capes entre pinzells, així que allà n’hi ha prou amb un element sense propietari. El nostre llenç té les tres coses, i un esborrat sense propietari no té una resposta definida a «què passa quan es mou la tinta» o «en quina capa actua».
L’esborrat pertany al traç
Cada Stroke ara porta els seus propis camins d’esborrat. La goma comprova quins traços toca i afegeix un camí a cadascun, en coordenades del llenç. En renderitzar un traç, se’n dibuixa la tinta i els esborrats propis dins d’una capa de transparència:
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 conserva el destí i l’elimina en proporció a l’alfa de l’origen:
Dins d’una capa de transparència, el destí és aquell únic traç, de manera que l’operació no pot arribar al paper, als traços de sota ni als traços dibuixats més tard. La capa no té cap cost per als traços sense esborrats, que són gairebé tots: aquests segueixen el camí original.
La propietat respon directament als dos errors. La translació mou els camins d’esborrat juntament amb els punts de mostra, de manera que un traç mogut conserva els seus buits. La superposició de selecció dibuixa el mateix objecte Stroke, de manera que un traç seleccionat mostra els mateixos buits. L’ordre de les capes deixa d’importar, perquè un esborrat no s’aplica mai a res que no sigui el seu propi traç.
Desfer funciona per gestos: una entrada enumera cada parella (stroke, path) produïda pel gest; desfer elimina aquests camins i refer els torna a afegir. La tinta sota un esborrat no s’edita mai, de manera que no cal reconstruir res.
La previsualització en directe passa pel mapa de bits de les rajoles
El traç que s’està dibuixant normalment es renderitza com una superposició damunt de la pàgina composta. Un esborrat no hi pot funcionar, perquè aquesta superposició se situa tant damunt del paper com de la tinta, i destination-out perforaria el paper.
Per això la goma escriu immediatament dins dels traços, i cada lot de mostres noves activa una reproducció local només del rectangle afectat. El pipeline de rajoles ja esborra un rectangle i reprodueix els traços que hi intersequen; la goma reutilitza aquest camí i no hi afegeix cap segona semàntica de dibuix.
Les dues gomes, una al costat de l’altra
La barra d’eines té dos botons de goma en lloc d’un sol botó amb un mode ocult. La goma parcial n’elimina una part del traç. La goma de traços elimina traços sencers en tocar-los. Els seus radis són constants independents, tots dos actualment de quatre punts.
Eliminar un traç sencer també s’enduu els seus camins d’esborrat, perquè formen part del traç, i desfer ho restaura tot junt. Els camins d’esborrat també formen part del fitxer desat i es quantifiquen com tota la resta: radi i coordenades en dècimes de punt.
Queda una limitació. Un traç esborrat fins a desaparèixer del tot encara existeix com a dada. No renderitza res, però el llaç encara el pot seleccionar i la goma de traços encara el pot eliminar, i continua ocupant espai en el document. Recollir els traços completament esborrats requereix una prova de cobertura que encara no hem escrit, així que de moment es queden.
Quan un model d’un altre projecte deixa de funcionar en el teu, la diferència normalment no és en el codi que has copiat. És en les funcionalitats que l’altre projecte no té.