Deux gommes, et pourquoi l’effacement appartient au tracé
Un tracé de gomme autonome est le choix évident. Le déplacement d’objets le met en échec : dans Lulucat Notes, l’effacement appartient au tracé dont il retire l’encre.
Quand on efface dans un carnet, on peut parler de deux opérations différentes. Parfois, une marque est erronée et doit disparaître en entier : une ligne guide, un mot barré, un trait parasite laissé par une paume posée sur la page. Parfois, seule une partie d’une marque est erronée : la moitié d’un caractère, une branche d’un schéma, la fin d’un coup de surligneur qui a dépassé la phrase. Lulucat Notes propose désormais les deux, sous la forme de deux outils distincts.
La question de conception la plus difficile est de savoir où vit l’effacement.
La gomme de tracé est arrivée la première
La première gomme supprimait des tracés entiers. À chaque contact, elle teste chaque tracé : les boîtes englobantes en écartent presque tous, puis les survivants passent un test de distance au carré entre le point et le segment, avec le rayon de la gomme augmenté du rayon du tracé interpolé le long de ce segment. La largeur varie selon la pression, donc une simple largeur nominale supprimerait des tracés fins que l’utilisateur n’a pas touchés et manquerait les tracés épais qui l’ont été.
Un seul geste forme une seule entrée d’annulation, qui stocke chaque tracé supprimé avec son index d’origine. L’annulation les réinsère dans l’ordre inverse, de sorte que l’ordre de superposition revient exactement à ce qu’il était.
Cette gomme est rapide, prévisible et incapable de supprimer la moitié d’un mot.
MaLiang transforme la gomme en pinceau
MaLiang, le framework de dessin sous licence MIT dont nous avions déjà repris le modèle de tampons du surligneur, rend l’effacement partiel possible en vingt-quatre lignes. Son Eraser est une sous-classe de Brush qui ne redéfinit qu’une chose : les options de fusion, pour soustraire au lieu d’ajouter. Un tracé de gomme est un élément de la même liste que tous les autres tracés. L’annulation supprime cet élément. Aucun tracé existant n’est modifié, car l’effacement ne se produit qu’au rejeu, dans l’ordre de la liste.
Nous avons livré ce modèle, et deux sessions sur appareil l’ont mis en défaut.
Le déplacement d’objets révèle le propriétaire manquant
Le premier signalement : sélectionne des tracés avec le lasso, déplace-les, et le trou reste derrière.

Les mêmes tracés effacés, déplacés vers la droite. En haut : la gomme est un élément autonome. En bas : l’effacement appartient aux tracés.
La sélection aggravait le problème. Les tracés sélectionnés sortent du bitmap pour rejoindre une surcouche qui les dessine directement, sans l’élément de gomme ; dès que vous sélectionniez un tracé effacé, il réapparaissait donc entier.
Le deuxième signalement : la gomme flottait au-dessus du surligneur. Notre moteur de rendu rejoue en deux passes, les surligneurs d’abord et les stylos ensuite, car l’encre du surligneur doit rester sous l’encre des stylos. Un élément de gomme doit participer aux deux passes, sinon il ne peut pas effacer les deux types. Participer à la passe des stylos signifie l’exécuter après chaque surligneur déjà dessiné, de sorte qu’une gomme effaçait aussi des tracés de surligneur dessinés plus tard dans le temps.
Les deux échecs ont la même origine. MaLiang ne connaît ni lasso, ni déplacement, ni ordre des calques entre pinceaux : un élément sans propriétaire y suffit donc. Notre canevas possède ces trois fonctionnalités, et un effacement sans propriétaire n’offre aucune réponse définie à « que se passe-t-il quand l’encre se déplace ? » ou « sur quel calque cette opération agit-elle ? »
L’effacement appartient au tracé
Chaque Stroke porte désormais ses propres chemins d’effacement. La gomme détermine les tracés qu’elle touche et ajoute un chemin à chacun, dans les coordonnées du canevas. Le rendu d’un tracé dessine son encre et ses propres effacements dans un calque de transparence :
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()
Le mode destination-out conserve la destination et la supprime dans une proportion déterminée par l’alpha de la source :
À l’intérieur d’un calque de transparence, la destination est ce tracé unique ; l’opération ne peut donc atteindre ni le papier, ni les tracés situés dessous, ni ceux dessinés plus tard. Le calque ne coûte rien pour les tracés sans effacements, c’est-à-dire presque tous : ceux-ci empruntent le chemin d’origine.
Le fait de rattacher l’effacement au tracé répond directement aux deux échecs. La translation déplace les chemins d’effacement avec les points échantillonnés, de sorte qu’un tracé déplacé emporte ses vides avec lui. La surcouche de sélection dessine le même objet Stroke, donc un tracé sélectionné affiche les mêmes vides. L’ordre des calques cesse d’avoir de l’importance, car un effacement ne s’applique jamais à autre chose qu’à son propre tracé.
L’annulation fonctionne au niveau du geste : une entrée liste chaque paire (stroke, path) produite par ce geste, l’annulation retire ces chemins et le rétablissement les ajoute de nouveau. L’encre sous un effacement n’est jamais modifiée, donc rien n’a besoin d’être reconstruit.
Le retour visuel en direct passe par le bitmap des tuiles
Le tracé en cours de dessin est normalement rendu comme une surcouche au-dessus de la page composée. Une gomme ne peut pas fonctionner là, car cette surcouche se trouve au-dessus du papier comme de l’encre, et destination-out percerait aussi le papier.
La gomme écrit donc immédiatement dans les tracés, et chaque lot de nouveaux échantillons déclenche un rejeu local de la seule zone rectangulaire touchée. Le pipeline de tuiles efface déjà un rectangle et rejoue les tracés qui l’intersectent ; la gomme réutilise ce chemin et n’ajoute aucune seconde sémantique de dessin.
Les deux gommes, côte à côte
La barre d’outils comporte deux boutons de gomme plutôt qu’un bouton unique doté d’un mode caché. La gomme partielle efface une partie d’un tracé. La gomme de tracé supprime les tracés entiers au contact. Leurs rayons sont des constantes distinctes, tous deux actuellement de quatre points.
Supprimer un tracé entier supprime aussi ses chemins d’effacement, puisqu’ils en font partie, et l’annulation restaure les deux ensemble. Les chemins d’effacement font aussi partie du fichier enregistré et sont quantifiés comme le reste : rayon et coordonnées au dixième de point.
Une limitation demeure. Un tracé effacé jusqu’à ne plus rien laisser existe toujours comme donnée. Il ne rend rien, mais le lasso peut encore le sélectionner, la gomme de tracé peut encore le supprimer et il occupe toujours de l’espace dans le document. La collecte des tracés totalement effacés exige un test de couverture que nous n’avons pas écrit ; pour l’instant, ils restent.
Lorsqu’un modèle provenant d’un autre projet cesse de fonctionner dans le vôtre, la différence ne se trouve généralement pas dans le code que vous avez copié. Elle se trouve dans les fonctionnalités que cet autre projet n’a pas.