fix(pdf-selection): match text to dragged lines
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 52s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 13s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s

ชื่อ bookmark/ข้อความ copy จากการคลุมข้อความเพี้ยน 3 รูปแบบบน
เอกสารไทย (คำขอผู้ใช้ 2026-10-05 รายงานบนฟอร์มราชการ
"แบบบันทึกผลการทดลองปฏิบัติหน้าที่ราชการ" ที่บรรทัดคุมชิด ~16px)

(1) สองบรรทัดสอดแทรกตัวต่อตัว ("แบบบันทึกผ(สำลกหารัรบท…") —
    บรรทัดไทยคุมชิด rect สูง ~16px เท่าระยะห่างบรรทัด ตัวอักษร
    ของบรรทัดถัดไป (สระบนดันกรอบขึ้น) เหลื่อมกับ rect บรรทัดก่อน
    หน้าจริง 1-3px: hit-test เดิม (findIndex ตัวแรกที่เจอ + eps)
    จับข้ามบรรทัดจนโดนเรียงรวมกันด้วย x — แก้ selectionTextWithin
    เป็นประตู 2 ชั้น: จุดกึ่งกลางแนวตั้งของตัวอักษรต้องอยู่ในแถบ
    ของ rect (เกณฑ์หลักแยกบรรทัด) แล้วเลือก rect ที่ซ้อนทับมากสุด

(2) ลากบรรทัดเดียวแต่ได้หลายบรรทัด — DOM range ของ pdf.js มักครอบ
    span บรรทัดถัดไปด้วย (ลากจบท้ายบรรทัด/เลยขอบขวา แล้ว caret
    ปลายทางหลุดเข้า span ถัดไป: range.toString() ได้ 3 บรรทัดทั้งที่
    ลากบรรทัดเดียว) — ประตูจุดกึ่งกลางใน (1) ตัดตัวอักษรนอกแถบ
    ทิ้งเอง ได้เฉพาะข้อความที่เห็นบนจอจริง (ดีกว่า native)

(3) ✓ checkbox คอลัมน์ขวาติดมากับบล็อกที่เลือก — pdf.js วาง span ✓
    ไว้ระหว่างบรรทัดข้อความพอดีจน native selection ครอบมันตาม DOM
    (raw toString มี ✓ เหมือนกัน) — rectsInSweptPath: บรรทัดกลาง
    เดิมเก็บทั้งบรรทัดแบบไม่ดูแกน x เปลี่ยนเป็นเก็บเฉพาะช่วงคอลัมน์
    เดียวกับข้อความที่เก็บได้จริง: ยึดขอบซ้าย/ขวาจริงของ rect ที่เก็บ
    ได้จากบรรทัดแรก/สุดท้าย + ขยายริมละ 2×lineTol (ทน justify gap
    ระหว่าง rect, ตาม zoom อัตโนมัติ) — span ที่ห่างเกิน (✓ gap
    30px+) ถูกตัดทั้งจากข้อความและกรอบมาร์กเหลือง (rect ชุดเดียวกัน)

(4) แยกบรรทัด/เรียงลำดับกลุ่ม rect ใช้เกณฑ์เดียวกัน — เดิม sameLine
    (แทรก \n) ใช้ |midY| ≤ max(height) ซึ่งตัดสินผิดกับบรรทัดคุมชิด
    (16 ≤ 16 = บรรทัดเดียวกัน) และ comparator เรียง rectOrder ก็ใช้
    heuristic เดิมจนย่อหน้าที่บรรทัดแรกเยื้องเข้า (left มากกว่า
    บรรทัดถัดไป) โดนเรียงตาม left กลายเป็นข้อความสลับลำดับบรรทัด —
    ทั้งคู่เปลี่ยนมาใช้ "rect ไม่ซ้อนทับแนวตั้งกัน = คนละบรรทัด"

(5) แก้ตาม code review 2026-10-05 ข้อ 1: หน้าต่างคอลัมน์ของบรรทัด
    กลางห้ามยึดพิกัดจุดลากตรง ๆ (ลากชัน/ลาก margin ซ้ายแล้ว rect
    ครึ่งขวาของบรรทัดกลางโดนตัด) — ยึดขอบจริงตาม (3) แทน; doc comment
    ของ rectsInSweptPath ที่ยังบอก "ไม่ตัดแกน x" sync ให้ตรงพฤติกรรม

(6) tests: coordinates +3 เคส (✓ คนละคอลัมน์ถูกตัด + rect คอลัมน์
    เดียวกันยังเก็บ / ลากชันผ่าน justify gap ยังเก็บเต็มบรรทัด /
    ลาก margin ซ้ายบรรทัดกลางไม่หาย) — รวม 215/215 ผ่าน + tsc

(7) docs sync: rules/pdf-viewer.md + rules/coordinates.md

ยืนยันใน browser จริง (Playwright) บนไฟล์ฟอร์มราชการของผู้ใช้:
heading บรรทัดเดียว / 2 บรรทัด / ลากเลยขอบขวา / บล็อก ๑+๒ ปกติ
และลากชัน — ชื่อ bookmark ตรงเป๊ะ ไม่มีข้อความปน/สลับ/✓ ติดมา

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-10-05 11:31:33 +07:00
parent c08aaab1ed
commit b123ecc9b2
5 changed files with 120 additions and 22 deletions

View file

@ -311,13 +311,29 @@ function selectionTextWithin(kept: DOMRect[], range: Range): string {
const r = probe.getClientRects()[0];
let hit = -1;
if (r !== undefined) {
hit = kept.findIndex(
(k) =>
r.left < k.right + eps &&
r.right > k.left - eps &&
r.top < k.bottom + eps &&
r.bottom > k.top - eps,
);
// สองเงื่อนไขในการนับตัวอักษรเข้ากลุ่ม rect ของบรรทัด: (1) จุดกึ่งกลาง
// แนวตั้งของตัวอักษรต้องอยู่ในแถบของ rect (2) เลือก rect ที่ซ้อนทับแนวตั้ง
// มากที่สุด ไม่ใช่ตัวแรกที่เจอ — บรรทัดไทยคุมชิด (ห่าง ~16px แต่ rect สูง
// ~16px เพราะครอบสระบน-ล่าง) ตัวอักษรของบรรทัดถัดไป (โดยเฉพาะตัวที่มีสระ
// บนดันกรอบขึ้น) เหลื่อมกับ rect ของบรรทัดก่อนหน้าจริง ๆ 1-3px การเช็คแค่
// การเหลื่อมจึงไม่พอ: DOM range ของ pdf.js มักครอบ span บรรทัดถัดไปด้วย
// (ลากจบท้ายบรรทัดแล้ว caret ปลายทางหลุดเข้า span ถัดไป) เดินถึงตัวอักษร
// ของบรรทัดนั้นแล้วดูดเข้ากลุ่มเดียวกัน โดนเรียงรวมด้วย x จนสองบรรทัดสอด
// แทรกตัวต่อตัวเป็น mojibake — จุดกึ่งกลางของตัวอักษรบรรทัดถัดไปยังอยู่นอก
// แถบเสมอ (เหลื่อมแค่ขอบ) เป็นเกณฑ์แยกบรรทัดที่เด็ดกว่า (bug report 2026-10-05)
const cy = r.top + r.height / 2;
let best = 0;
for (let ki = 0; ki < kept.length; ki++) {
const k = kept[ki];
if (r.left >= k.right + eps || r.right <= k.left - eps) continue;
if (cy < k.top - eps || cy > k.bottom + eps) continue;
const score =
Math.min(r.bottom, k.bottom + eps) - Math.max(r.top, k.top - eps);
if (score > best) {
best = score;
hit = ki;
}
}
}
// combining mark / ตัวไม่มีความกว้าง = ผูกกับตัวก่อนหน้า ตาม rect+x ของตัวนั้น
const sticky = prevRect >= 0 && (isThaiCombiningMark(ch) || !r || r.width === 0);
@ -343,9 +359,13 @@ function selectionTextWithin(kept: DOMRect[], range: Range): string {
const rectOrder = Array.from(new Set(entries.map((en) => en.rect))).sort((a, b) => {
const ra = kept[a];
const rb = kept[b];
if (Math.abs(midY(ra) - midY(rb)) > Math.max(ra.height, rb.height)) {
return midY(ra) - midY(rb);
}
// ใช้เกณฑ์เดียวกับ sameLine ด้านล่าง: rect ไม่ซ้อนทับแนวตั้งกัน = คนละบรรทัด
// เรียงบน→ล่าง, ซ้อนกัน = บรรทัดเดียวกันเรียงซ้าย→ขวา — เดิมเทียบ |midY| กับ
// ความสูงซึ่งตัดสินผิดกับบรรทัดไทยคุมชิด (ห่าง 16px สูง 16px = "บรรทัดเดียวกัน")
// จนย่อหน้าที่บรรทัดแรกเยื้องเข้า (left มากกว่าบรรทัดถัดไป) โดนเรียงตาม left
// กลางเป็นข้อความสลับลำดับบรรทัด (code review 2026-10-05)
const vOv = Math.min(ra.bottom, rb.bottom) - Math.max(ra.top, rb.top);
if (vOv <= eps) return midY(ra) - midY(rb);
return ra.left - rb.left;
});
let out = "";
@ -354,7 +374,11 @@ function selectionTextWithin(kept: DOMRect[], range: Range): string {
if (prev >= 0) {
const a = kept[prev];
const b = kept[ri];
const sameLine = Math.abs(midY(a) - midY(b)) <= Math.max(a.height, b.height);
// คนละบรรทัด = rect ไม่ซ้อนทับแนวตั้งกัน (แค่ชิด/แตะกัน) — เดิมใช้เทียบ midY
// กับความสูง ซึ่งตัดสินผิดกับบรรทัดไทยคุมชิด (ห่าง 16px, สูง 16px: 16 ≤ 16
// = "บรรทัดเดียวกัน") จนไม่มี \n คั่น (และช่วยให้โดนจัดกลุ่มรวมกันในตอน hit)
const vOverlap = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
const sameLine = vOverlap > eps;
if (!sameLine) out += "\n"; // span คนละบรรทัด = ขึ้นบรรทัดใหม่เหมือน toString()
}
out += entries