From e4ecc026b025687df33b158af8b45605db841b4c Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Sep 2026 14:49:05 +0700 Subject: [PATCH] perf(pdf-viewer): limit drag preview re-render to its page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit แก้ "ลากย้ายเส้น/กล่องข้อความ/ตรายาง/pin ไม่ smooth เป็น step" (คำขอผู้ใช้ 2026-09-29): - สาเหตุ: preview ของการลาก (onPreview → setPreviewData) เป็น state ระดับ root ของ PDFViewer — ทุก pointermove ระหว่างลาก re-render ทั้ง ต้นไม้ (toolbar + sidebar + ทุกหน้าที่ mounted ตาม virtualization) ทั้งที่ผู้ใช้ลากอยู่หน้าเดียว และค่าถูกอ่านแค่จุดเดียวคือ substitution ของหน้าที่ลาก — เทียบกับการวาดเส้นที่ smooth เพราะ live state อยู่ใน InkLayer เอง + rAF throttle (FIX-8) อยู่แล้ว - แก้: ย้าย preview state ลงเป็น state ท้องถิ่นของ PdfPage (substitution ย้ายตามมาอยู่ก่อน derive layers ทั้งหมด) — ระหว่างลาก re-render เฉพาะ หน้าที่ลาก; root สั่งเคลียร์ผ่าน counter previewClear (useReducer) แทน setter ตรง ครบทุกจุดเดิม 9 จุด (ปิด popup/Escape/auto-select/ delete/ก่อนคิว update/deselect/goToAnnotation/เลือกรายการ) - onPreview ของ AnnotationSelectionLayer throttle ด้วย rAF ≤1 ครั้ง/ เฟรม (pattern FIX-8 ของ InkLayer): ทุก pointermove ยังคำนวณ + เก็บ latestRef เสมอ (ค่า commit ตอน pointerup เป็นค่าล่าสุดสุดเสมอ) เฉพาะ การ render ที่จัดคิวตามเฟรม + cancel ครบทุกทางออกของ drag (pointerup/pointercancel/unmount) - แก้ตาม code review 2026-09-29: preview เก็บเป็น {id, data} เทียบ id เจ้าของตอน substitute แทน selectedId — กันจอสัมผัสเปลี่ยน selection กลาง drag (นิ้วหนึ่งลากค้าง อีกนิ้วเลือกอื่น) แล้ว preview ของรายการเก่า ไปทดแทนรายการใหม่หนึ่งเฟรม (การเคลียร์จาก root เป็น passive effect หลัง paint) - sync สถาปัตยกรรมใหม่ใน .claude/rules/pdf-viewer.md (หัวข้อกรอบ selection F1) ผ่าน code-reviewer + plan-reviewer (ไม่มี bug ค้าง/ไม่ละเมิด invariant) + tsc + vitest 207/207 + e2e Playwright: ลาก DRAW FPS≈60.6 (commit เลื่อนตรงระยะ/undo/redo/reload คงอยู่) + ลาก TEXTBOX FPS≈59-60.9 ไม่มี เฟรม drop (commit/reload ตรงเป๊ะ) Co-Authored-By: Claude Code --- .claude/rules/pdf-viewer.md | 2 +- components/pdf/AnnotationSelectionLayer.tsx | 31 ++++++++- components/pdf/PDFViewer.tsx | 71 ++++++++++++++------- 3 files changed, 80 insertions(+), 24 deletions(-) diff --git a/.claude/rules/pdf-viewer.md b/.claude/rules/pdf-viewer.md index c747c1a..f7b33c4 100644 --- a/.claude/rules/pdf-viewer.md +++ b/.claude/rules/pdf-viewer.md @@ -30,7 +30,7 @@ paths: - **popup บนหน้าเปิดได้ทีละชุด ห้ามค้างเปิดซ้อนกัน** (bug "กดแก้ไขแล้วกดปุ่ม tool popup ไม่ปิด/ซ้อนกัน"): ปุ่ม dropdown ทั้ง 5 **เรียก `switchTool` เสมอแม้เป็น tool เดิม** (จำ intent เปิด/พับเมนูไว้ก่อนเรียก เพราะ `switchTool` ปิดเมนูทุกตัว) เนื่องจาก `switchTool` เป็นที่เดียวที่เคลียร์ popup บนหน้าทั้งหมด — composer/pin popover/แผง selection/editing comment·textbox + popup bookmark (`setBookmarkComposer(null)`); **สร้างรายการแล้ว auto-select (`AUTO_SELECT_TYPES`) ก็ปิดเมนู+popup bookmark เช่นกัน** — บน iOS การลาก (คลุมข้อความ/วาดเส้น) ไม่ยิง compat mousedown ให้ outside-click ปิดเมนูเอง (Android/desktop ปิดตั้งแต่เริ่มกดแล้ว) ไม่ปิดตรงนี้ = เมนูค้างเปิดซ้อนแผงที่โผล่หลังสร้าง; คลิกหน้าว่าง (`handlePageClick`) ปิด popover ของ pin + popup bookmark; เลือกรายการอื่น (`handleSelectAnnotation`/`goToAnnotation`) ปิด popover เดิม; **ทางเปิด popup bookmark ทุกทาง (คีย์ B/`openBookmarkComposer` + ปุ่ม bookmark ในแผง selection `handleAnnotationBookmark`) เคลียร์ popup บนหน้าทั้งชุดก่อนเปิด (2026-09-29) — ไม่งั้น popup bookmark ซ้อน popover/editor ที่ค้างเปิด และ autoFocus ของช่องชื่อแย่ง focus จาก editor**; popover ของ COMMENT เปิดอยู่ (`openPinId === selectedId`) = **ไม่ render กรอบ selection/แผงเครื่องมือของตัวนั้น** — bbox ของ COMMENT ครอบพื้นที่ pin พอดี แผงจะทับ popover และแถบ handle บังการคลิก (sidebar ยังตาม `selectedId` ไฮไลต์เหมือนเดิม) - **ชุดปิดเมนู dropdown รวมศูนย์จุดเดียว (2026-09-29, PLAN-TOOLBAR ขั้น 2):** `closeToolMenus()` วนจาก `TOOL_MENU_KEYS` (toolbar-tools.ts) + `TOOL_MENU_TABLE` (Record ใน PDFViewer — ผูก open/ref/setter ครบทุก key โดย type บังคับ: เพิ่มเมนูใหม่แล้วลืมแถว = compile fail ไม่ drift เงียบ แบบ title bug 2026-09-23) ใช้ร่วมทุกจุดที่เคย enumerate setter ด้วยมือ 6 แห่ง — switchTool / outside-click / เลื่อนแถบ·resize ความกว้าง / auto-select / openBookmarkComposer / reset tool ตรง (effect `scannedOnly` + สลับไฟล์ภาพ — เมนู portal ต้องไม่ค้างลอยไร้ปุ่มยึดเมื่อปุ่ม highlight ถูกซ่อน) ส่วน popup บนหน้าทั้งชุดอยู่ที่ `closePagePopups()` (switchTool + ทางเปิด popup bookmark ทั้งสอง + **Esc ของคีย์ลัด** ใช้ร่วม — code review ขั้น 2) - **คลุมข้อความได้ 5 tool** (`TEXT_SELECTION_TOOLS` set ใน PDFViewer: select/highlight/comment/textbox/stamp — ที่เหลือ textLayer ปิด pointer-events คลุมไม่ได้): ทั้งหมด inject `