Deux gommes : pourquoi l’effacement appartient au tracé
Un tracé de gomme comme élément distinct semble évident. Le déplacement d’objets le brise : dans Lulucat Notes, l’effacement appartient au tracé auquel il enlève de l’encre.
Dans un carnet, effacer peut vouloir dire deux choses différentes. Parfois, une marque est erronée et doit disparaître au complet : une ligne guide, un mot rayé, un trait parasite laissé par la 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 diagramme, la queue d’un coup de surligneur qui a dépassé la phrase. Lulucat Notes offre maintenant les deux, sous la forme de deux outils distincts.
La question de conception la plus difficile est de savoir où se trouve l’effacement.
La gomme de tracés est arrivée en premier
La première gomme supprimait des tracés entiers. Au contact, elle met chaque tracé à l’épreuve : les boîtes englobantes en rejettent presque tous, et les survivants passent un test de distance au carré entre un point et un segment, en fonction du rayon de la gomme plus le rayon du tracé interpolé le long de ce segment. La largeur varie selon la pression ; une seule largeur nominale supprimerait donc des tracés fins que l’utilisateur n’a pas touchés et manquerait les tracés épais qu’il a touchés.
Un geste est regroupé dans une seule entrée d’annulation, qui conserve chaque tracé supprimé avec son index d’origine. L’annulation les réinsère en ordre inverse, ce qui rétablit exactement l’ordre d’empilement initial.
Cette gomme est rapide, prévisible et incapable de supprimer la moitié d’un mot.
MaLiang transforme la gomme en pinceau
MaLiang, le cadriciel de dessin sous licence MIT dont nous avions déjà repris le modèle de tampons du surligneur, règle l’effacement partiel 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 plutôt qu’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é, puisque l’effacement n’a lieu que pendant la relecture, dans l’ordre de la liste.
Nous avons livré ce modèle, et deux séances sur appareil l’ont mis en échec.
Le déplacement d’objets révèle le propriétaire manquant
Le premier signalement : sélectionnez des tracés au lasso, faites-les glisser 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 distinct. En bas : l’effacement appartient aux tracés.
La sélection empirait les choses. Les tracés sélectionnés sont retirés du bitmap et placés dans une surcouche qui les dessine directement, sans l’élément de gomme ; dès qu’un tracé effacé était sélectionné, il réapparaissait donc au complet.
Le deuxième signalement : la gomme flottait au-dessus du surligneur. Notre moteur de rendu rejoue en deux passes, les surligneurs d’abord, puis les stylos, parce que l’encre de surligneur doit se trouver sous l’encre. 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 le dessin de chaque surligneur, de sorte qu’une gomme coupait aussi les tracés de surligneur dessinés plus tard dans le temps.
Les deux échecs ont la même origine. MaLiang n’a ni lasso, ni déplacement, ni ordre de calque entre les pinceaux, alors un élément sans propriétaire y suffit. Notre canevas possède les trois, et un effacement sans propriétaire n’a pas de réponse définie à « que se passe-t-il quand l’encre se déplace ? » ni à « sur quel calque cette opération agit-elle ? »
L’effacement appartient au tracé
Chaque Stroke porte maintenant ses propres chemins d’effacement. La gomme détermine quels tracés elle touche et ajoute à chacun un chemin, en 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 proportionnellement à l’alpha de la source :
Dans un calque de transparence, la destination est ce tracé unique, de sorte que l’opération ne peut atteindre ni le papier, ni les tracés sous-jacents, ni ceux dessinés plus tard. Le calque n’ajoute aucun coût aux tracés sans effacement, qui sont presque tous ; ceux-ci suivent 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. La surcouche de sélection dessine le même objet Stroke, alors un tracé sélectionné affiche les mêmes vides. L’ordre des calques cesse d’avoir de l’importance, puisqu’un effacement ne s’applique jamais à autre chose qu’à son propre tracé.
L’annulation s’applique au geste : une entrée liste chaque paire (stroke, path) produite par ce geste ; annuler supprime ces chemins, refaire les ajoute de nouveau. L’encre sous un effacement n’est jamais modifiée, donc rien n’a besoin d’être reconstruit.
Le retour en direct passe par le bitmap des tuiles
Le tracé en cours de dessin est normalement rendu en surcouche au-dessus de la page composée. Une gomme ne peut pas fonctionner là, car cette surcouche recouvre aussi bien le papier que l’encre, et destination-out transpercerait le papier.
La gomme écrit donc immédiatement dans les tracés, et chaque lot de nouveaux échantillons déclenche une relecture locale du seul rectangle touché. 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 deuxième sémantique de dessin.
Les deux gommes, côte à côte
La barre d’outils comporte deux boutons de gomme plutôt qu’un seul bouton avec un mode caché. La gomme partielle enlève une partie d’un tracé. La gomme de tracés 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 font partie de ce tracé, et l’annulation restaure les deux ensemble. Les chemins d’effacement font aussi partie du fichier enregistré, quantifiés comme tout le reste : rayon et coordonnées au dixième de point.
Une limite demeure. Un tracé effacé jusqu’à disparition complète existe toujours comme donnée. Il ne produit rien au rendu, mais le lasso peut encore le sélectionner et la gomme de tracés peut encore le supprimer ; il occupe toujours de l’espace dans le document. Le nettoyage des tracés entièrement effacés nécessite un test de couverture que nous n’avons pas encore écrit ; ils restent donc en place pour l’instant.
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 fonctions que cet autre projet n’a pas.