* feat(browser): add an eraser to screenshot markup
Click or drag across marks to remove them whole, one undo step per drag.
Fixes#23258
* fix(browser): drop the in-flight markup gesture on undo and redo
Undo mid-erase left marks hidden against the restored drawing, so release removed them and cleared redo.
* fix(browser): settle a markup gesture whose release was lost
A gesture now blocks any new press so a second finger cannot steer it.
If its own release never arrived, that block also swallowed the next
press from the same pointer, so the next drag continued the stale
gesture: a stroke jumped from its old end, and an erase swept from the
old point, removing marks along a line the user never touched.
A pointer cannot press twice without releasing, so a press from the
gesture's own pointer now settles the old gesture as its release would
have, then starts the new one.
* fix(browser): an eraser click takes only the top mark, a drag takes all
A click on overlapping marks now erases only the one drawn last, matching
what a user expects from pointing at a mark. Once the pointer really moves
the press becomes a drag, whose first sweep starts at the press point, so
every mark under it and along the path is erased. A move that reports the
press point again does not turn a click into a drag.
* fix(browser): undo mid-gesture takes back only the gesture
Undo pressed while a stroke or an erase is still held now drops just that
gesture, as the newest step; the committed marks are untouched, so the next
Undo takes back the last of them. Redo and Clear drop the gesture, then apply
as before. With the gesture gone, the rest of that drag neither draws nor
erases until the next press. Undo is enabled while a gesture is in flight,
since it now has something to take back.
* fix(browser): keep an eraser tap a click within a 4px slop
A touch or pen tap often reports a move a fraction of a pixel from the
press point. The eraser treated any move to a different point as a
drag, so such a tap erased every mark under the finger instead of only
the top one. The erase gesture now records its press point and a
pressed/dragging phase: it stays a click until the pointer travels 4 CSS
px, then sweeps from the press point as a drag.
* fix(browser): undo over an empty erase undoes the last mark
Pressing Undo while an eraser was held over empty space only dropped
the erase, which had hidden nothing, so Undo looked dead and the Undo
button was enabled for a step with no visible effect. Undo now treats
a held gesture as a step only when releasing it would change the
markup (a stroke being drawn, or an erase that hides at least one
mark). Otherwise Undo takes back the last committed mark and ends the
erase; with nothing to undo it leaves the erase alone. The Undo button
is enabled exactly when pressing it would change something.
* fix(browser): redo with nothing to redo keeps a held markup gesture
The Redo button is disabled when there is nothing to redo, but the
Cmd/Ctrl+Shift+Z shortcut is not. Pressed while a stroke or erase was
held down, it replaced the document with itself and dropped the
gesture, so the stroke or erase silently stopped. Redo now leaves the
state untouched when there is nothing to redo. Clear still always
drops the gesture.
* fix(browser): discard a markup gesture whose pointer was cancelled
pointercancel was wired to the release handler, so when the system took
a touch or pen pointer away mid-gesture (an OS gesture or palm
rejection), the stroke was saved and an erase was applied as if the
user had lifted deliberately. pointercancel now discards the in-flight
gesture owned by that pointer: the stroke is not saved and the marks an
erase was hiding come back, with no undo step. Lost pointer capture and
a press that reveals a missed release still commit, since both stand in
for a release that did happen.
* test(browser): pin that losing the pointer commits a markup gesture
The canvas commits a stroke or erase when it loses pointer capture
without a release, so a gesture can never be left open. No test covered
that wiring: removing it, or routing it to the cancel handler, left every
test green. The new test ends a stroke with only the lost capture and
checks that Undo then leaves it to redo, which only a committed stroke
does.
* fix(browser): do not save a markup shape pressed without dragging
A Rectangle, Ellipse or Arrow press released without dragging paints
nothing, but it was saved as a zero-size mark. With the eraser, a click
takes only the topmost mark, so that invisible mark made the next eraser
click on a visible mark at the same spot appear to do nothing. It also
added an undo step with no visible effect, and while such a press was
held Undo was enabled but only dropped the invisible shape.
A shape with no size is now not saved, and a held unmoved shape press
is not something Undo takes back. Pen and Highlighter taps still save
the dot they draw; text is unchanged.
* test(browser): drop a cancelled-stroke test the overlay test already covers
MarkupOverlay.test.tsx drives the same press, move, pointercancel and
following lost capture through the real canvas wiring and asserts Undo
stays disabled, which already proves nothing was committed or held.
* docs(browser): note the erase drag re-rasterizes the markup layer
The committed layer now renders the visible marks, so an erase drag
re-rasterizes it once per newly hit mark; the comment said never.
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>