Duas borrachas e por que o apagamento pertence ao traço
Um traço de borracha como elemento próprio parece o design óbvio. Mover objetos quebra esse modelo; no Lulucat Notes, o apagamento pertence ao traço do qual remove a tinta.
Ao apagar em um caderno, as pessoas podem estar falando de duas coisas diferentes. Às vezes, uma marca está errada e deve desaparecer inteira: uma linha-guia, uma palavra riscada, um traço solto deixado pela palma apoiada. Às vezes, apenas parte de uma marca está errada: metade de um caractere, um ramo de um diagrama, a cauda de uma passada de marca-texto que passou da frase. O Lulucat Notes agora oferece as duas, como ferramentas separadas.
A questão de design mais difícil é onde o apagamento fica.
A borracha de traços veio primeiro
A primeira borracha apagava traços inteiros. Um toque testa cada traço: caixas delimitadoras rejeitam quase todos, e os sobreviventes passam por um teste de distância quadrada entre ponto e segmento, usando como limite o raio da borracha mais o raio do traço interpolado ao longo desse segmento. A largura varia com a pressão, então uma única largura nominal apagaria traços finos que o usuário não tocou e deixaria de apagar os grossos que foram tocados.
Um gesto vira uma única entrada de desfazer, e a entrada armazena cada traço removido junto com seu índice original. Desfazer os insere de volta em ordem inversa, então a ordem de empilhamento retorna exatamente como era.
Essa borracha é rápida, previsível e incapaz de remover metade de uma palavra.
MaLiang torna a borracha um pincel
MaLiang, o framework de desenho sob a licença MIT do qual já havíamos emprestado o modelo de carimbos do marca-texto, resolve o apagamento parcial em 24 linhas. Sua Eraser é uma subclasse de Brush que substitui uma única coisa, as opções de composição, para subtrair em vez de adicionar. Um traço de borracha é um elemento na mesma lista que todos os outros traços. Desfazer remove esse elemento. Nenhum traço existente é modificado, porque o apagamento só acontece durante a reprodução, na ordem da lista.
Colocamos esse modelo em produção, e ele falhou em duas sessões em dispositivos.
Mover objetos expõe o dono ausente
O primeiro relato: selecione traços com o laço, arraste-os, e o buraco fica para trás.

Os mesmos traços apagados, movidos para a direita. Acima: a borracha é um elemento próprio. Abaixo: o apagamento pertence aos traços.
A seleção piorou o problema. Os traços selecionados saem do bitmap e vão para uma sobreposição que os desenha diretamente, sem o elemento da borracha; assim que você selecionava um traço apagado, ele aparecia inteiro novamente.
O segundo relato: a borracha flutuava acima do marca-texto. Nosso renderizador reproduz em duas passagens, primeiro os marca-textos e depois as canetas, porque a tinta do marca-texto precisa ficar sob a tinta. Um elemento de borracha precisa participar das duas passagens; caso contrário, não consegue apagar os dois tipos. Participar da passagem de caneta significa executá-lo depois que cada marca-texto foi desenhado, então uma borracha também cortava traços de marca-texto desenhados mais tarde.
As duas falhas têm a mesma origem. O MaLiang não tem laço, movimento nem ordem de camadas entre pincéis, então um elemento sem dono basta ali. Nosso canvas tem os três, e um apagamento sem dono não tem resposta definida para “o que acontece quando a tinta se move” ou “em qual camada esta operação atua”.
O apagamento pertence ao traço
Todo Stroke agora carrega seus próprios caminhos de apagamento. A borracha testa quais traços toca e acrescenta um caminho a cada um deles, em coordenadas do canvas. Renderizar um traço desenha sua tinta e seus próprios apagamentos dentro de uma camada 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 mantém o destino e o remove na proporção do alfa da fonte:
Dentro de uma camada de transparência, o destino é aquele único traço, então a operação não consegue alcançar o papel, os traços abaixo nem os traços desenhados depois. A camada não tem custo para traços sem apagamentos, que são quase todos: eles seguem o caminho original.
Essa propriedade responde diretamente às duas falhas. A translação move os caminhos de apagamento junto com os pontos amostrados, então um traço movido carrega suas lacunas. A sobreposição de seleção desenha o mesmo objeto de traço, então um traço selecionado mostra as mesmas lacunas. A ordem das camadas deixa de importar, porque um apagamento nunca se aplica a nada além do próprio traço.
Desfazer funciona por gesto: uma entrada lista cada par (stroke, path) produzido por aquele gesto; desfazer remove esses caminhos, e refazer os acrescenta novamente. A tinta sob um apagamento nunca é editada, então nada precisa ser reconstruído.
O feedback ao vivo passa pelo bitmap de blocos
O traço em desenho normalmente é renderizado como uma sobreposição acima da página composta. Um apagamento não pode funcionar ali, porque essa sobreposição fica sobre o papel e a tinta, e destination-out furaria o papel.
Por isso, a borracha grava diretamente nos traços, e cada lote de novas amostras aciona uma reprodução local somente do retângulo afetado. O pipeline de blocos já limpa um retângulo e reproduz os traços que o intersectam; a borracha reutiliza esse caminho e não acrescenta uma segunda semântica de desenho.
As duas borrachas, lado a lado
A barra de ferramentas tem dois botões de borracha em vez de um botão com um modo oculto. A borracha parcial remove parte de um traço. A borracha de traços remove traços inteiros ao tocar neles. Seus raios são constantes separadas, ambos atualmente de 4 pontos.
Apagar um traço inteiro leva seus caminhos de apagamento junto, já que eles fazem parte desse traço, e desfazer traz os dois de volta juntos. Os caminhos de apagamento também fazem parte do arquivo salvo e são quantizados como todo o resto: raio e coordenadas em décimos de ponto.
Uma limitação permanece. Um traço apagado até desaparecer completamente ainda existe como dado. Ele não renderiza nada, mas o laço ainda pode selecioná-lo, a borracha de traços ainda pode apagá-lo, e ele ainda ocupa espaço no documento. Coletar traços completamente apagados exige um teste de cobertura que ainda não escrevemos, então por enquanto eles permanecem.
Quando um modelo de outro projeto para de funcionar no seu, a diferença geralmente não está no código que você copiou. Está nos recursos que aquele projeto não tem.