diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index 0b72449d..92fa7b22 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -1747,11 +1747,13 @@ document.addEventListener("alpine:init", () => { const first = ctx.rects[0]; const last = ctx.rects[ctx.rects.length - 1]; if (!first || !last) return; - // Start dot at the first rect's LEFT-MIDDLE, end dot at the last - // rect's RIGHT-MIDDLE — the range boundary points. + // Start dot floating ABOVE the first rect's top-left corner, end + // dot above the last rect's top-right corner — the teardrop tip + // points down at the boundary so the highlighted text underneath + // stays readable (user request: the handle covered the text). const pts: Array<{ which: "start" | "end"; fx: number; fy: number }> = [ - { which: "start", fx: first[0], fy: first[1] + first[3] / 2 }, - { which: "end", fx: last[0] + last[2], fy: last[1] + last[3] / 2 }, + { which: "start", fx: first[0], fy: first[1] }, + { which: "end", fx: last[0] + last[2], fy: last[1] }, ]; for (const pt of pts) { let dot = layer.querySelector( @@ -1790,8 +1792,10 @@ document.addEventListener("alpine:init", () => { layer.appendChild(dot); } const host = map.toHost(pt.fx, pt.fy); + // The teardrop's rotated tip is its bottom point: hang the box + // ABOVE the boundary corner. dot.style.left = `${host.x - 13}px`; - dot.style.top = `${host.y - 13}px`; + dot.style.top = `${host.y - 22}px`; } }, /** Line boxes (page fractions) of a contiguous char-table span: @@ -1846,15 +1850,27 @@ document.addEventListener("alpine:init", () => { if (!ctx) return; const map = this.pdfPageMapping(ctx.index); if (!map) return; - const { fx, fy } = map.toFraction(clientX, clientY); + const { fx } = 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; let best = 0; let bestD = Infinity; for (let i = 0; i < chars.length; i++) { - const cx = (chars[i].x0 + chars[i].x1) / 2; - const cy = (chars[i].y0 + chars[i].y1) / 2; - const d = (cx - fx) * (cx - fx) + (cy - fy) * (cy - fy); + 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); if (d < bestD) { bestD = d; best = i;