From f01d848cfa973b8750ec1269265a2d891d4291c6 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 5 Oct 2026 10:02:18 +0200 Subject: [PATCH 1/2] fix(html): preserve ink stroke pointer ownership and endpoint --- CHANGELOG.md | 3 ++ .../internal/html/frontend/pdf-annotation.js | 11 +++++++- test/browser/annotation/tests.html | 28 ++++++++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70003f389..8713ba00a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- PDF ink annotations retain their final pointer-up position and ignore + competing pointers until the active stroke ends. + - The declared CMake minimum is now 3.20, the first version that recognizes the build's existing C++23 requirement. diff --git a/src/odr/internal/html/frontend/pdf-annotation.js b/src/odr/internal/html/frontend/pdf-annotation.js index cb7cbf7c2..2f6a89de2 100644 --- a/src/odr/internal/html/frontend/pdf-annotation.js +++ b/src/odr/internal/html/frontend/pdf-annotation.js @@ -500,6 +500,9 @@ } function onPointerDown(event) { + if (stroke) { + return; + } pointerDown = true; // a new gesture supersedes a mark the previous one had queued window.clearTimeout(settle); @@ -574,13 +577,19 @@ } function onPointerUp(event) { + if (stroke && event.pointerId !== strokePointer) { + return; + } pointerDown = false; dragging = false; updateSelecting(); scheduleMark(); - if (!stroke || (event && event.pointerId !== strokePointer)) { + if (!stroke) { return; } + if (event.type === "pointerup") { + onPointerMove(event); + } var points = stroke.strokes[0]; if (points.length < 4) { // a tap with no drag leaves a dot, which is a legitimate mark diff --git a/test/browser/annotation/tests.html b/test/browser/annotation/tests.html index 6edd0c4b2..ece307802 100644 --- a/test/browser/annotation/tests.html +++ b/test/browser/annotation/tests.html @@ -247,18 +247,44 @@ page1.setPointerCapture = () => {}; document.dispatchEvent(new PointerEvent("pointerdown", at(100, 100))); document.dispatchEvent(new PointerEvent("pointermove", at(140, 140))); + document.dispatchEvent( + new PointerEvent("pointerdown", { ...at(150, 150), pointerId: 2 }) + ); + document.dispatchEvent( + new PointerEvent("pointerup", { ...at(160, 160), pointerId: 2 }) + ); document.dispatchEvent(new PointerEvent("pointermove", at(180, 100))); - document.dispatchEvent(new PointerEvent("pointerup", at(180, 100))); + document.dispatchEvent(new PointerEvent("pointerup", at(220, 120))); + check("another pointer does not split the stroke", api.list().length === 1); const ink = payload().annotations[0]; check("a drag makes an ink annotation", ink && ink.type === "ink", ink); check("its width carries", ink && ink.width === 3, ink && ink.width); + check( + "the pointer-up position ends the stroke", + ink && near(ink.strokes[0].at(-2), 220) && near(ink.strokes[0].at(-1), 672), + ink + ); + check( "its first point is where the pointer went down", ink && near(ink.strokes[0][0], 100) && near(ink.strokes[0][1], 692), ink && ink.strokes[0].slice(0, 2) ); + api.clear(); + document.dispatchEvent(new PointerEvent("pointerdown", at(100, 100))); + document.dispatchEvent(new PointerEvent("pointermove", at(140, 140))); + document.dispatchEvent(new PointerEvent("pointercancel", at(0, 0))); + const cancelled = payload().annotations[0]; + check( + "cancellation keeps the last real sample", + cancelled && + near(cancelled.strokes[0].at(-2), 140) && + near(cancelled.strokes[0].at(-1), 652), + cancelled + ); + // --- ink where the rects leave the fit out ---------------------------- // Chromium reports the two alike and cannot show it, so the case is made // by standing in a getViewportRect that halves the box. From febe1d82281485009109af25ac97d1728d1ee885 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 6 Oct 2026 21:32:15 +0200 Subject: [PATCH 2/2] fix(html): let the stroke's own pointer start a new ink stroke The guard ignored every pointerdown while a stroke was active. When the stroke's pointerup was lost, the next press of the same pointer then continued the old stroke until some later pointerup. Only another pointer is ignored now; the same one going down again keeps the stroke so far and starts a new one, as before. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MxyTMutqSUJRGfxA8CyzMc --- .../internal/html/frontend/pdf-annotation.js | 4 +++- test/browser/annotation/tests.html | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/odr/internal/html/frontend/pdf-annotation.js b/src/odr/internal/html/frontend/pdf-annotation.js index 2f6a89de2..177a37cad 100644 --- a/src/odr/internal/html/frontend/pdf-annotation.js +++ b/src/odr/internal/html/frontend/pdf-annotation.js @@ -500,7 +500,9 @@ } function onPointerDown(event) { - if (stroke) { + // another pointer cannot take over a stroke; the same one going down again + // lost its pointerup and starts afresh + if (stroke && event.pointerId !== strokePointer) { return; } pointerDown = true; diff --git a/test/browser/annotation/tests.html b/test/browser/annotation/tests.html index ece307802..83e26232b 100644 --- a/test/browser/annotation/tests.html +++ b/test/browser/annotation/tests.html @@ -285,6 +285,24 @@ cancelled ); + // a pointer that goes down again lost its pointerup: the stroke so far + // stays, and a new one starts + api.clear(); + document.dispatchEvent(new PointerEvent("pointerdown", at(100, 100))); + document.dispatchEvent(new PointerEvent("pointermove", at(140, 140))); + document.dispatchEvent(new PointerEvent("pointerdown", at(200, 200))); + document.dispatchEvent(new PointerEvent("pointermove", at(240, 240))); + document.dispatchEvent(new PointerEvent("pointerup", at(240, 240))); + const restarted = payload().annotations; + check( + "the same pointer going down again starts a new stroke", + restarted.length === 2 && + near(restarted[0].strokes[0].at(-2), 140) && + near(restarted[1].strokes[0][0], 200) && + near(restarted[1].strokes[0][1], 592), + restarted + ); + // --- ink where the rects leave the fit out ---------------------------- // Chromium reports the two alike and cannot show it, so the case is made // by standing in a getViewportRect that halves the box.