Dos borradores y por qué el borrado le pertenece al trazo
Un trazo de borrador como elemento propio parece el diseño obvio. Mover objetos lo rompe, así que en Lulucat Notes el borrado le pertenece al trazo del que elimina la tinta.
Cuando alguien borra en un cuaderno, puede referirse a dos cosas distintas. A veces una marca está equivocada y debe desaparecer completa: una línea guía, una palabra tachada, un trazo accidental de la palma apoyada. A veces solo está mal una parte de la marca: la mitad de un carácter, una rama de un diagrama, la cola de un barrido de resaltador que se pasó de la oración. Lulucat Notes ofrece ahora ambas opciones, como dos herramientas independientes.
La pregunta de diseño más difícil es dónde vive el borrado.
El borrador de trazos apareció primero
El primer borrador eliminaba trazos completos. Cada toque prueba todos los trazos: las cajas delimitadoras rechazan casi todos, y a los que sobreviven se les aplica una prueba de distancia al cuadrado entre un punto y un segmento, contra el radio del borrador más el radio del trazo interpolado a lo largo de ese segmento. El ancho cambia con la presión, así que un único ancho nominal eliminaría trazos delgados que el usuario no tocó y no detectaría los gruesos que sí tocó.
Un gesto se fusiona en una sola entrada de deshacer, y la entrada guarda cada trazo eliminado junto con su índice original. Deshacer los vuelve a insertar en orden inverso, de modo que el orden de apilamiento regresa exactamente a como estaba.
Este borrador es rápido, predecible e incapaz de eliminar media palabra.
MaLiang convierte el borrador en pincel
MaLiang, el framework de dibujo con licencia MIT del que ya habíamos tomado el modelo de sellos del resaltador, resuelve el borrado parcial en veinticuatro líneas. Su Eraser es una subclase de Brush que sobrescribe una sola cosa: las opciones de mezcla, para restar en vez de sumar. Un trazo de borrador es un elemento de la misma lista que todos los demás trazos. Deshacer elimina ese elemento. Ningún trazo existente se modifica, porque el borrado solo sucede durante la reproducción, en el orden de la lista.
Integramos ese modelo, y dos sesiones en dispositivos lo rompieron.
Mover objetos revela al propietario que faltaba
El primer reporte: selecciona trazos con el lazo, arrástralos y el hueco se queda atrás.

Los mismos trazos borrados, desplazados a la derecha. Arriba: el borrador es un elemento independiente. Abajo: el borrado le pertenece a los trazos.
La selección lo empeoró. Los trazos seleccionados salen del bitmap y pasan a una capa superpuesta que los dibuja directamente, sin el elemento de borrador; por eso, en cuanto seleccionabas un trazo borrado, volvía a verse completo.
El segundo reporte: el borrador flotaba sobre el resaltador. Nuestro renderizador reproduce en dos pasadas, primero los resaltadores y después las plumas, porque la tinta del resaltador debe quedar debajo de la tinta. Un elemento de borrador tiene que participar en ambas pasadas; de lo contrario no puede borrar los dos tipos. Participar en la pasada de plumas significa ejecutarse después de que se haya dibujado cada resaltador, así que un borrador también cortaba trazos de resaltador que se habían dibujado más tarde en el tiempo.
Ambas fallas tienen el mismo origen. MaLiang no tiene lazo, movimiento ni orden de capas entre pinceles, así que allí basta un elemento sin propietario. Nuestro lienzo tiene las tres cosas, y un borrado sin propietario no tiene una respuesta definida a «qué ocurre cuando se mueve la tinta» ni a «en qué capa está operando».
El borrado le pertenece al trazo
Ahora cada Stroke lleva sus propias rutas de borrado. El borrador comprueba qué trazos toca y añade una ruta a cada uno, en coordenadas del lienzo. Al renderizar un trazo se dibujan su tinta y sus propios borrados dentro de una capa de transparencia:
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 destino y lo elimina en proporción al alfa de origen:
Dentro de una capa de transparencia, el destino es ese único trazo, así que la operación no puede llegar al papel, a los trazos de abajo ni a los que se dibujen después. La capa no agrega ningún costo a los trazos sin borrados, que son casi todos: esos siguen la ruta original.
La propiedad responde directamente a las dos fallas. El desplazamiento mueve las rutas de borrado junto con los puntos de muestra, así que un trazo desplazado se lleva sus huecos consigo. La capa superpuesta de selección dibuja el mismo objeto Stroke, así que un trazo seleccionado muestra los mismos huecos. El orden de las capas deja de importar, porque un borrado nunca se aplica a nada salvo a su propio trazo.
Deshacer funciona por gesto: una entrada enumera cada par (stroke, path) que produjo ese gesto; deshacer elimina esas rutas y rehacer las vuelve a agregar. La tinta debajo de un borrado nunca se modifica, así que no hay que reconstruir nada.
La respuesta en vivo pasa por el bitmap de teselas
El trazo que se está dibujando normalmente se renderiza como una capa superpuesta sobre la página compuesta. El borrado no puede funcionar ahí, porque esa capa queda encima tanto del papel como de la tinta, y destination-out atravesaría el papel.
Por eso el borrador escribe de inmediato en los trazos, y cada lote de muestras nuevas activa una reproducción local únicamente del rectángulo afectado. El pipeline de teselas ya limpia un rectángulo y reproduce los trazos que lo intersectan; el borrador reutiliza ese flujo y no agrega una segunda semántica de dibujo.
Los dos borradores, lado a lado
La barra de herramientas tiene dos botones de borrador en lugar de un botón con un modo oculto. El borrador parcial elimina parte de un trazo. El borrador de trazos elimina trazos completos al tocarlos. Sus radios son constantes separadas, ambos actualmente de cuatro puntos.
Eliminar un trazo completo se lleva sus rutas de borrado, porque forman parte de ese trazo, y deshacer devuelve ambos juntos. Las rutas de borrado también forman parte del archivo guardado y se cuantizan como todo lo demás: el radio y las coordenadas en décimas de punto.
Queda una limitación. Un trazo borrado hasta desaparecer por completo todavía existe como datos. No se renderiza, pero el lazo aún puede seleccionarlo y el borrador de trazos puede eliminarlo, y sigue ocupando espacio en el documento. Recolectar los trazos completamente borrados requiere una prueba de cobertura que todavía no hemos escrito, así que por ahora se quedan.
Cuando un modelo de otro proyecto deja de funcionar en el tuyo, la diferencia normalmente no está en el código que copiaste. Está en las funcionalidades que ese proyecto no tiene.