From f69e659d948f6c554f373a02cf94802af2f0573d Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 10 Oct 2026 18:25:29 -0400 Subject: [PATCH] =?UTF-8?q?feat(web):=20PDF=20adjust=20v2=20=E2=80=94=20re?= =?UTF-8?q?-selection=20replaces=20the=20extent;=20vertical=20grip=20clamp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-line partial extents ("line 1's tail + the first words of line 2") were inexpressible with one-rect-per-drag grips, and the vertical drags produced sliver rects whose text recompute swept fragments. The web's re-selection uses what creation already uses: a selection made during a rect-edge session is intercepted in checkPDFSelectionFor — rects rebuild from the selection's per-line client rects, text re-reads via the deduped glyph boxes, saved immediately (grip release-to-save parity), and the create popover is suppressed for that gesture. Vertical grip drags clamp to the rect's OWN line box (band ±quarter height) — multi-line changes go through re-selection, slivers are gone. Also verifies clean under the duplicated text-layer span sets (the fork's pdf.js render never clears .textLayer — open bug): the text recompute dedupes by glyph overlap. CDP-verified end-to-end: session → multi-line selection → 6-rect extent + clean text persisted (PUT 200); create popover correctly suppressed during the session. --- web/src/reader/reader.ts | 49 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index 1a580954..8329d5d7 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -1798,8 +1798,12 @@ document.addEventListener("alpine:init", () => { if (!map || !r) return; const vertical = edge === "left" || edge === "right"; // Band: the rect's y-range widened to catch neighbouring line - // boundaries (horizontal-edge drags snap to line tops/bottoms). - const bandPad = vertical ? 0.002 : Math.max(r[3] * 0.75, 0.008); + // boundaries — but only ±quarter height now: a vertical drag adjusts + // THIS line box's height and can no longer swallow a neighbouring + // line (that produced sliver rects whose text recompute swept + // fragments — "some 14ng that scholars…"). Multi-line changes go + // through re-selection below. + const bandPad = vertical ? 0.002 : Math.max(r[3] * 0.25, 0.004); const bandTop = r[1] - bandPad; const bandBottom = r[1] + r[3] + bandPad; const band = this.pdfCharBoxes(map.doc, bandTop, bandBottom); @@ -2150,6 +2154,47 @@ document.addEventListener("alpine:init", () => { const range = sel.getRangeAt(0); const text = sel.toString().replace(/\s+/g, " ").trim(); if (!text) return; + // Adjust re-selection (v2): a selection made during a rect-edge + // session REPLACES the working extent — any lines, partial lines + // (the exact gap the grips could not express). Rects rebuild from + // the selection's line boxes, text re-reads, saved immediately: + // the grips' release-to-save parity. + if (this.pdfAdjust) { + const canvasRect = + (doc.querySelector("#canvas canvas") as HTMLElement) || + (doc.querySelector("img") as HTMLElement); + const dr = canvasRect?.getBoundingClientRect(); + if (!dr || !dr.width || !dr.height) return; + const rects: number[][] = []; + for (const r of range.getClientRects()) { + const x = (r.left - dr.left) / dr.width; + const y = (r.top - dr.top) / dr.height; + const w = r.width / dr.width; + const h = r.height / dr.height; + if (w > 0 && h > 0) rects.push([x, y, w, h]); + } + if (!rects.length) return; + const t = this.pdfTextForRects(doc, rects); + if (!t) { + showToast("Highlights can't be empty", "error"); + return; + } + const session = this.pdfAdjust; + session.committed = true; + this.cancelPdfAdjust(); + const p = this.selectionPopover; + p.id = session.id; + p.pdfPage = session.page; + p.pdfRects = rects; + p.text = t; + this.saveHighlightChanges(); + try { + sel.removeAllRanges(); + } catch { + /* doc may be gone */ + } + return; + } // Denominator: the element whose post-transform screen rect IS // the visible page. For PDFs that's the rendered canvas — pdf.js // scales by 1/devicePixelRatio, so documentElement's rect