From b3b884c736b435bd9e4ca140861e73ed302a9af9 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Mon, 14 Sep 2026 13:07:08 +0700 Subject: [PATCH] =?UTF-8?q?=E0=B8=88=E0=B8=AD=E0=B8=AA=E0=B8=B1=E0=B8=A1?= =?UTF-8?q?=E0=B8=9C=E0=B8=B1=E0=B8=AA=20(iOS/Android):=20=E0=B9=84?= =?UTF-8?q?=E0=B8=AE=E0=B9=84=E0=B8=A5=E0=B8=95=E0=B9=8C=20commit=20?= =?UTF-8?q?=E0=B8=9C=E0=B9=88=E0=B8=B2=E0=B8=99=E0=B8=9B=E0=B8=B8=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=20"=E0=B9=84=E0=B8=AE=E0=B9=84=E0=B8=A5=E0=B8=95?= =?UTF-8?q?=E0=B9=8C"=20=E0=B8=A5=E0=B8=AD=E0=B8=A2=E0=B9=83=E0=B8=95?= =?UTF-8?q?=E0=B9=89=20selection=20+=20=E0=B8=A5=E0=B8=B2=E0=B8=81=20handl?= =?UTF-8?q?e=20=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=9A=20selection=20?= =?UTF-8?q?=E0=B9=84=E0=B8=94=E0=B9=89=E0=B8=88=E0=B8=A3=E0=B8=B4=E0=B8=87?= =?UTF-8?q?:=20(1)=20=E0=B8=9A=E0=B8=99=20touch=20=E0=B8=81=E0=B8=B2?= =?UTF-8?q?=E0=B8=A3=E0=B8=A5=E0=B8=B2=E0=B8=81=E0=B8=84=E0=B8=A5=E0=B8=B8?= =?UTF-8?q?=E0=B8=A1=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=84=E0=B8=A7=E0=B8=B2?= =?UTF-8?q?=E0=B8=A1=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B8=A2=E0=B8=B4=E0=B8=87?= =?UTF-8?q?=20synthetic=20mouseup=20(=E0=B8=A2=E0=B8=B4=E0=B8=87=E0=B9=80?= =?UTF-8?q?=E0=B8=89=E0=B8=9E=E0=B8=B2=E0=B8=B0=20tap)=20=E0=B8=97?= =?UTF-8?q?=E0=B8=B3=E0=B9=83=E0=B8=AB=E0=B9=89=20HIGHLIGHT=20=E0=B9=84?= =?UTF-8?q?=E0=B8=A1=E0=B9=88=E0=B8=96=E0=B8=B9=E0=B8=81=E0=B8=AA=E0=B8=A3?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=87=E0=B9=80=E0=B8=A5=E0=B8=A2=20?= =?UTF-8?q?=E2=80=94=20=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88=E0=B8=A1?= =?UTF-8?q?=E0=B8=9B=E0=B8=B8=E0=B9=88=E0=B8=A1=20"=E0=B9=84=E0=B8=AE?= =?UTF-8?q?=E0=B9=84=E0=B8=A5=E0=B8=95=E0=B9=8C"=20=E0=B8=A5=E0=B8=AD?= =?UTF-8?q?=E0=B8=A2=E0=B9=83=E0=B8=95=E0=B9=89=E0=B8=9A=E0=B8=A3=E0=B8=A3?= =?UTF-8?q?=E0=B8=97=E0=B8=B1=E0=B8=94=E0=B8=AA=E0=B8=B8=E0=B8=94=E0=B8=97?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=A2=E0=B8=82=E0=B8=AD=E0=B8=87=20select?= =?UTF-8?q?ion=20(=E0=B9=81=E0=B8=9E=E0=B8=97=E0=B9=80=E0=B8=97=E0=B8=B4?= =?UTF-8?q?=E0=B8=A3=E0=B9=8C=E0=B8=99=20Acrobat=20mobile;=20=E0=B8=94?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=99=E0=B8=9A=E0=B8=99=E0=B8=96=E0=B8=B9?= =?UTF-8?q?=E0=B8=81=E0=B9=80=E0=B8=A1=E0=B8=99=E0=B8=B9/handles=20?= =?UTF-8?q?=E0=B8=82=E0=B8=AD=E0=B8=87=20OS=20=E0=B8=A2=E0=B8=B6=E0=B8=94?= =?UTF-8?q?=20=E0=B9=83=E0=B8=81=E0=B8=A5=E0=B9=89=E0=B8=82=E0=B8=AD?= =?UTF-8?q?=E0=B8=9A=E0=B8=A5=E0=B9=88=E0=B8=B2=E0=B8=87=E0=B8=AB=E0=B8=99?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B9=80=E0=B8=81=E0=B8=B4=E0=B8=99=E0=B8=9E?= =?UTF-8?q?=E0=B8=A5=E0=B8=B4=E0=B8=81=E0=B8=82=E0=B8=B6=E0=B9=89=E0=B8=99?= =?UTF-8?q?=E0=B9=80=E0=B8=AB=E0=B8=99=E0=B8=B7=E0=B8=AD=20rect=20?= =?UTF-8?q?=E0=B9=81=E0=B8=A3=E0=B8=81)=20=E0=B9=81=E0=B8=AA=E0=B8=94?= =?UTF-8?q?=E0=B8=87=E0=B9=80=E0=B8=89=E0=B8=9E=E0=B8=B2=E0=B8=B0=20pointe?= =?UTF-8?q?r:coarse=20+=20tool=20highlight=20=E0=B9=80=E0=B8=A1=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=20selection=20=E0=B8=AB=E0=B8=A2=E0=B8=B8?= =?UTF-8?q?=E0=B8=94=E0=B8=99=E0=B8=B4=E0=B9=88=E0=B8=87=20~400ms=20(debou?= =?UTF-8?q?nce=20=E0=B9=83=E0=B8=99=20effect=20selectionchange=20=E0=B8=82?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=A1=E0=B8=8A=E0=B9=88=E0=B8=A7=E0=B8=87?= =?UTF-8?q?=E0=B8=A5=E0=B8=B2=E0=B8=81=20handles)=20(2)=20=E0=B9=81?= =?UTF-8?q?=E0=B8=95=E0=B8=B0=E0=B8=9B=E0=B8=B8=E0=B9=88=E0=B8=A1=20commit?= =?UTF-8?q?=20=E0=B8=88=E0=B8=B2=E0=B8=81=20snapshot=20=E0=B9=83=E0=B8=99?= =?UTF-8?q?=20touchSelRef=20=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B9=83=E0=B8=8A?= =?UTF-8?q?=E0=B9=88=20selection=20=E0=B8=AA=E0=B8=94=20=E0=B9=80=E0=B8=9E?= =?UTF-8?q?=E0=B8=A3=E0=B8=B2=E0=B8=B0=20iOS=20=E0=B9=80=E0=B8=84=E0=B8=A5?= =?UTF-8?q?=E0=B8=B5=E0=B8=A2=E0=B8=A3=E0=B9=8C=20selection=20=E0=B8=97?= =?UTF-8?q?=E0=B8=B1=E0=B8=99=E0=B8=97=E0=B8=B5=E0=B8=97=E0=B8=B5=E0=B9=88?= =?UTF-8?q?=E0=B9=81=E0=B8=95=E0=B8=B0=E0=B8=9B=E0=B8=B8=E0=B9=88=E0=B8=A1?= =?UTF-8?q?=E0=B8=81=E0=B9=88=E0=B8=AD=E0=B8=99=20tap=20=E0=B8=88=E0=B8=B0?= =?UTF-8?q?=E0=B8=96=E0=B8=B6=E0=B8=87=20=E2=80=94=20=E0=B8=81=E0=B8=B2?= =?UTF-8?q?=E0=B8=A3=E0=B8=8B=E0=B9=88=E0=B8=AD=E0=B8=99=E0=B8=9B=E0=B8=B8?= =?UTF-8?q?=E0=B9=88=E0=B8=A1+=E0=B8=A5=E0=B9=89=E0=B8=B2=E0=B8=87=20snaps?= =?UTF-8?q?hot=20=E0=B8=88=E0=B8=B6=E0=B8=87=E0=B8=AB=E0=B8=99=E0=B9=88?= =?UTF-8?q?=E0=B8=A7=E0=B8=87=20250ms=20(scheduleTouchHide)=20=E0=B9=83?= =?UTF-8?q?=E0=B8=AB=E0=B9=89=20tap=20=E0=B8=99=E0=B8=B1=E0=B9=89=E0=B8=99?= =?UTF-8?q?=20commit=20=E0=B9=84=E0=B8=94=E0=B9=89=E0=B8=81=E0=B9=88?= =?UTF-8?q?=E0=B8=AD=E0=B8=99=20=E0=B9=81=E0=B8=A5=E0=B8=B0=E0=B8=9C?= =?UTF-8?q?=E0=B8=B9=E0=B8=81=20commit=20=E0=B8=97=E0=B8=B5=E0=B9=88=20poi?= =?UTF-8?q?nterdown=20(=E0=B9=80=E0=B8=A3=E0=B9=87=E0=B8=A7=E0=B8=81?= =?UTF-8?q?=E0=B8=A7=E0=B9=88=E0=B8=B2=20click);=20desktop=20=E0=B8=A2?= =?UTF-8?q?=E0=B8=B1=E0=B8=87=E0=B9=83=E0=B8=8A=E0=B9=89=20mouseup=20?= =?UTF-8?q?=E0=B8=95=E0=B8=B2=E0=B8=A1=E0=B9=80=E0=B8=94=E0=B8=B4=E0=B8=A1?= =?UTF-8?q?=20=E2=80=94=20=E0=B8=AA=E0=B8=AD=E0=B8=87=20path=20=E0=B8=9C?= =?UTF-8?q?=E0=B9=88=E0=B8=B2=E0=B8=99=20commitHighlight=20=E0=B8=8A?= =?UTF-8?q?=E0=B8=B8=E0=B8=94=E0=B9=80=E0=B8=94=E0=B8=B5=E0=B8=A2=E0=B8=A7?= =?UTF-8?q?=E0=B8=81=E0=B8=B1=E0=B8=99=20+=20=E0=B8=95=E0=B8=B1=E0=B9=89?= =?UTF-8?q?=E0=B8=87=20lastSelectionTextRef=20(=E0=B8=8A=E0=B8=B7=E0=B9=88?= =?UTF-8?q?=E0=B8=AD=20bookmark=20default)=20=E0=B9=83=E0=B8=99=20debounce?= =?UTF-8?q?=20=E0=B9=80=E0=B8=AB=E0=B8=A1=E0=B8=B7=E0=B8=AD=E0=B8=99=20mou?= =?UTF-8?q?seup=20(3)=20=E0=B9=80=E0=B8=95=E0=B8=B4=E0=B8=A1=20touch-none?= =?UTF-8?q?=20=E0=B8=97=E0=B8=B5=E0=B9=88=20handle=20=E0=B8=97=E0=B8=B1?= =?UTF-8?q?=E0=B9=89=E0=B8=87=208=20=E0=B8=82=E0=B8=AD=E0=B8=87=20Annotati?= =?UTF-8?q?onSelectionLayer=20(=E0=B8=82=E0=B8=AD=E0=B8=9A=204=20+=20?= =?UTF-8?q?=E0=B8=A1=E0=B8=B8=E0=B8=A1=204=20=E2=80=94=20InkLayer=20?= =?UTF-8?q?=E0=B8=A1=E0=B8=B5=E0=B8=AD=E0=B8=A2=E0=B8=B9=E0=B9=88=E0=B9=81?= =?UTF-8?q?=E0=B8=A5=E0=B9=89=E0=B8=A7)=20=E0=B9=84=E0=B8=A1=E0=B9=88?= =?UTF-8?q?=E0=B8=87=E0=B8=B1=E0=B9=89=E0=B8=99=20browser=20=E0=B8=A2?= =?UTF-8?q?=E0=B8=B6=E0=B8=94=20gesture=20=E0=B9=80=E0=B8=9B=E0=B9=87?= =?UTF-8?q?=E0=B8=99=20scroll=20=E0=B8=AA=E0=B9=88=E0=B8=87=20pointercance?= =?UTF-8?q?l=20=E0=B8=81=E0=B8=A5=E0=B8=B2=E0=B8=A2=E0=B9=80=E0=B8=9B?= =?UTF-8?q?=E0=B9=87=E0=B8=99=E0=B9=80=E0=B8=A5=E0=B8=B7=E0=B9=88=E0=B8=AD?= =?UTF-8?q?=E0=B8=99=E0=B8=AB=E0=B8=99=E0=B9=89=E0=B8=B2=E0=B9=81=E0=B8=97?= =?UTF-8?q?=E0=B8=99=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=A5=E0=B8=B2=E0=B8=81?= =?UTF-8?q?=E0=B8=A2=E0=B9=89=E0=B8=B2=E0=B8=A2/=E0=B8=9B=E0=B8=A3?= =?UTF-8?q?=E0=B8=B1=E0=B8=9A=E0=B8=82=E0=B8=99=E0=B8=B2=E0=B8=94=20(4)=20?= =?UTF-8?q?sync=20rules/pdf-viewer.md=20=E2=80=94=20verify=20=E0=B8=94?= =?UTF-8?q?=E0=B9=89=E0=B8=A7=E0=B8=A2=20Playwright=20touch=20emulation:?= =?UTF-8?q?=20=E0=B8=9B=E0=B8=B8=E0=B9=88=E0=B8=A1=E0=B9=82=E0=B8=9C?= =?UTF-8?q?=E0=B8=A5=E0=B9=88=E0=B8=AB=E0=B8=A5=E0=B8=B1=E0=B8=87=20deboun?= =?UTF-8?q?ce,=20=E0=B9=81=E0=B8=95=E0=B8=B0=E0=B9=81=E0=B8=A5=E0=B9=89?= =?UTF-8?q?=E0=B8=A7=E0=B8=AA=E0=B8=A3=E0=B9=89=E0=B8=B2=E0=B8=87=20HIGHLI?= =?UTF-8?q?GHT=20=E0=B8=88=E0=B8=A3=E0=B8=B4=E0=B8=87=20(server=20action?= =?UTF-8?q?=20+=20DOM=20+=20sidebar),=20=E0=B8=A7=E0=B8=B2=E0=B8=94?= =?UTF-8?q?=E0=B9=80=E0=B8=AA=E0=B9=89=E0=B8=99=E0=B8=94=E0=B9=89=E0=B8=A7?= =?UTF-8?q?=E0=B8=A2=E0=B8=99=E0=B8=B4=E0=B9=89=E0=B8=A7=E0=B9=84=E0=B8=94?= =?UTF-8?q?=E0=B9=89,=20handles=209=20=E0=B8=88=E0=B8=B8=E0=B8=94=20touch-?= =?UTF-8?q?action:none=20=E0=B8=84=E0=B8=A3=E0=B8=9A,=20tsc=20=E0=B8=9C?= =?UTF-8?q?=E0=B9=88=E0=B8=B2=E0=B8=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- .claude/rules/pdf-viewer.md | 3 +- components/pdf/AnnotationSelectionLayer.tsx | 13 +- components/pdf/PDFViewer.tsx | 158 ++++++++++++++++++-- 3 files changed, 158 insertions(+), 16 deletions(-) diff --git a/.claude/rules/pdf-viewer.md b/.claude/rules/pdf-viewer.md index 6cc7d4b..22a138c 100644 --- a/.claude/rules/pdf-viewer.md +++ b/.claude/rules/pdf-viewer.md @@ -23,8 +23,9 @@ paths: - DRAW วาดเส้นตรง: **ปล่อยเมาส์แล้ว snap อัตโนมัติ** เมื่อจุดทุกจุดห่างจากเส้นตรง จุดเริ่ม→จุดสุดท้าย ไม่เกิน max(5% ของความยาว, 3px) และเส้นยาว ≥ 24px (`isNearlyStraight`) — หรือกดค้าง **Shift** ระหว่างลาก (กด/ปล่อยกลางทางได้ — listener ระดับ window + reset ตอน blur) เพื่อบังคับตรงเสมอ → บันทึกเหลือจุดเริ่ม→จุดสุดท้าย 2 จุด (ความหนา uniform — ไม่เก็บ `widths` จาก pressure) — ใช้ได้เฉพาะโหมดวาดอิสระ ไม่ใช่ shape/eraser/redact; เส้นโค้ง/ขดจริงหลุดเกณฑ์จึงเหลือ freehand - Tool `pan` (มือเลื่อนหน้า — ปุ่มแยกถัดจาก select ในแถบ tool): ลากบน `
` = `window.scrollBy` สวนทางการลาก + cursor grab/grabbing; ระหว่างใช้ pan ทุก overlay ปิด pointer-events ผ่านตัวแปร `overlayFrozen` (`inkActive || pan`) **ยกเว้น InkLayer ต้องไม่ active** (ไม่งั้น pan กลายเป็นวาดเส้น) - HIGHLIGHT มี `style?` 4 แบบ: highlight ทึบเหมือนเดิม / underline = เส้นที่ขอบล่าง rect ต่อบรรทัด / strikethrough = เส้นกลางบรรทัด / squiggle = svg path ขยุกขยิก (`squigglePath`) ที่ขอบล่าง — export วาด line/sine แทน rect ทึบ; สร้างได้จาก **tool "highlight" ทางเดียว** (dropdown บนปุ่ม = รูปแบบ + สี → คลุมข้อความ = สร้างทันที พร้อม live preview ผ่าน `selectionchange` + ซ่อน native selection) — tool "เลือกข้อความ" คลุมแล้วเกิด popup ไม่ได้แล้ว (ถอดออก) และคีย์ลัด `H` สลับไป tool highlight +- **จอสัมผัส (iOS/Android) ไม่มี `mouseup` หลังคลุมข้อความด้วยนิ้ว** (synthetic mouse events เกิดเฉพาะ tap — การ commit ตอนปล่อยเมาส์ใช้ไม่ได้): อุปกรณ์ที่ `matchMedia("(pointer: coarse)")` จริง + tool highlight → effect `selectionchange` debounce ~400ms หลัง selection หยุดนิ่ง แล้ว snapshot (rects + ข้อความผ่าน `readTextSelection(pi, { withText: true })`) เก็บใน `touchSelRef` + แสดงปุ่ม "ไฮไลต์" ลอย**ใต้**บรรทัดสุดท้ายของ selection (ด้านบนถูกเมนู/handles ของ OS ยึด — ใกล้ขอบล่างหน้าเกินพลิกขึ้นเหนือ rect แรก) — commit ต้องใช้ **snapshot ใน ref เท่านั้น ไม่ใช่ selection สด** เพราะ iOS เคลียร์ selection ทันทีที่แตะปุ่ม ก่อน click ยิง; ทั้ง desktop (mouseup) และ touch (ปุ่มยืนยัน) ผ่าน `commitHighlight` ชุดเดียวกัน; เคลียร์ปุ่ม+timer ทุกครั้งที่ selection หาย/ออกนอกหน้า/เปลี่ยน tool - **คลุมข้อความได้ 5 tool** (`TEXT_SELECTION_TOOLS` set ใน PDFViewer: select/highlight/comment/textbox/stamp — ที่เหลือ textLayer ปิด pointer-events คลุมไม่ได้): ทั้งหมด inject `