diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index 556d0054..a1eb3185 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -1855,27 +1855,22 @@ document.addEventListener("alpine:init", () => { if (!ctx) return; const map = this.pdfPageMapping(ctx.index); if (!map) return; - const { fx } = map.toFraction(clientX, clientY); + const { fx, fy } = map.toFraction(clientX, clientY); const chars = ctx.chars; if (!chars.length) return; - // The boundary stays on its OWN line: candidates are the chars in - // the boundary rect's line band, and the finger's X picks the - // glyph (vertical finger movement is ignored — cross-line changes - // go through re-selection). A whole-table Euclidean scan here - // re-anchored onto the line ABOVE when the finger floated above - // the text (lifted handles). - const boundary = - which === "start" ? ctx.rects[0] : ctx.rects[ctx.rects.length - 1]; - const bandTop = boundary[1] - 0.002; - const bandBottom = boundary[1] + boundary[3] + 0.002; + // Full 2D nearest glyph: the tip crosses lines freely (up/down) + // and moves along them (side to side). No line-band pin — the + // lifted handle's finger sits above the text, and a y-band filter + // here made vertical drags impossible (user report). Sliver rects + // stay impossible: the extent is a contiguous char span and the + // rects are its per-line unions. let best = 0; let bestD = Infinity; for (let i = 0; i < chars.length; i++) { const c = chars[i]; - const cy = (c.y0 + c.y1) / 2; - if (cy < bandTop || cy > bandBottom) continue; const cx = (c.x0 + c.x1) / 2; - const d = (cx - fx) * (cx - fx); + const cy = (c.y0 + c.y1) / 2; + const d = (cx - fx) * (cx - fx) + (cy - fy) * (cy - fy); if (d < bestD) { bestD = d; best = i;