From 4934b9d0b388775afd41e4b63a50025d37645e22 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Mon, 14 Sep 2026 09:59:01 +0700 Subject: [PATCH] =?UTF-8?q?eraser=20=E0=B8=A5=E0=B8=9A=E0=B8=95=E0=B8=A3?= =?UTF-8?q?=E0=B8=B2=E0=B8=A2=E0=B8=B2=E0=B8=87=20(STAMP)=20=E0=B8=82?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=E0=B8=95=E0=B8=B1=E0=B8=A7=E0=B9=80=E0=B8=AD?= =?UTF-8?q?=E0=B8=87=E0=B9=84=E0=B8=94=E0=B9=89:=20hit-test=20=E0=B9=83?= =?UTF-8?q?=E0=B8=99=20InkLayer.eraseAt=20=E0=B8=94=E0=B9=89=E0=B8=A7?= =?UTF-8?q?=E0=B8=A2=20bbox=20=E0=B8=88=E0=B8=B2=E0=B8=81=20annotationBBox?= =?UTF-8?q?=20(=E0=B8=AA=E0=B8=B9=E0=B8=95=E0=B8=A3=E0=B9=80=E0=B8=94?= =?UTF-8?q?=E0=B8=B5=E0=B8=A2=E0=B8=A7=E0=B8=81=E0=B8=B1=E0=B8=9A=20render?= =?UTF-8?q?)=20=E0=B9=80=E0=B8=8A=E0=B9=87=E0=B8=84=E0=B8=95=E0=B8=A3?= =?UTF-8?q?=E0=B8=B2=E0=B8=81=E0=B9=88=E0=B8=AD=E0=B8=99=E0=B9=80=E0=B8=AA?= =?UTF-8?q?=E0=B9=89=E0=B8=99/=E0=B8=A3=E0=B8=B9=E0=B8=9B=E0=B8=97?= =?UTF-8?q?=E0=B8=A3=E0=B8=87=E0=B9=80=E0=B8=9E=E0=B8=A3=E0=B8=B2=E0=B8=B0?= =?UTF-8?q?=E0=B8=95=E0=B8=A3=E0=B8=B2=E0=B8=AD=E0=B8=A2=E0=B8=B9=E0=B9=88?= =?UTF-8?q?=20layer=20=E0=B8=9A=E0=B8=99=20=E2=80=94=20=E0=B8=95=E0=B8=AD?= =?UTF-8?q?=E0=B8=99=20tool=20erase=20=E0=B8=84=E0=B9=88=E0=B8=B2=20Textbo?= =?UTF-8?q?xStampLayer=20=E0=B8=9B=E0=B8=B4=E0=B8=94=20pointer-events=20?= =?UTF-8?q?=E0=B8=AD=E0=B8=A2=E0=B8=B9=E0=B9=88=E0=B9=81=E0=B8=A5=E0=B9=89?= =?UTF-8?q?=E0=B8=A7=E0=B8=88=E0=B8=B6=E0=B8=87=E0=B9=84=E0=B8=A1=E0=B9=88?= =?UTF-8?q?=E0=B8=95=E0=B9=89=E0=B8=AD=E0=B8=87=E0=B9=81=E0=B8=95=E0=B8=B0?= =?UTF-8?q?=20layer=20stack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- components/pdf/InkLayer.tsx | 26 +++++++++++++++++++++++--- components/pdf/PDFViewer.tsx | 1 + docs/PLAN-FEAT.md | 3 ++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/components/pdf/InkLayer.tsx b/components/pdf/InkLayer.tsx index 37cbfbe..d26a1ce 100644 --- a/components/pdf/InkLayer.tsx +++ b/components/pdf/InkLayer.tsx @@ -20,7 +20,8 @@ import { type Rect, type Size, } from "@/utils/coordinates"; -import { hitTestPath, hitTestShape } from "@/utils/geometry"; +import { annotationBBox, hitTestPath, hitTestShape } from "@/utils/geometry"; +import type { InkStamp } from "./TextboxStampLayer"; /** เส้นวาดที่เก็บใน DB — จุดทุกจุดเป็น % ของหน้า, strokeWidth เป็น % ของความกว้าง */ export type InkStroke = { @@ -79,6 +80,7 @@ export default function InkLayer({ size, strokes, shapes, + stamps, redactions, active, nextStroke, @@ -97,6 +99,8 @@ export default function InkLayer({ strokes: InkStroke[]; /** รูปทรงเดิมจาก DB (พิกัด %) */ shapes: InkShape[]; + /** ตรายางเดิมจาก DB (พิกัด %) — render ที่ TextboxStampLayer แต่ eraser hit-test ที่นี่ */ + stamps: InkStamp[]; /** ปิดพรางเดิมจาก DB (พิกัด %) — วาดทับบังข้อความจริงบนจอ (F8) */ redactions: InkRedaction[]; /** tool วาด/รูปทรง/eraser/redact → รับ pointer events */ @@ -289,11 +293,27 @@ export default function InkLayer({ } } - /** hit-test ตำแหน่ง pointer กับเส้น/รูปทรง **ของตัวเอง** — โดนเมื่อไรลบทันทีทีละรายการ (F6) */ + /** hit-test ตำแหน่ง pointer กับเส้น/รูปทรง/ตรายาง **ของตัวเอง** — โดนเมื่อไรลบทันทีทีละรายการ (F6) */ function eraseAt(e: ReactPointerEvent) { if (!onErase || ownUserId === "") return; - const p = pointToPercent(toLocalPx(e), measured); + const lp = toLocalPx(e); + const p = pointToPercent(lp, measured); const pad = 6; // เผื่อความคาดหวังของผู้ใช้นอกเส้นจริง (px) + // ตรายาง render อยู่ layer บน (TextboxStampLayer) → เช็คก่อน ให้ตัวบนสุดชนะ + // (bbox = สูตรเดียวกับ render และกรอบ selection — annotationBBox) + for (const s of stamps) { + if (s.userId !== ownUserId) continue; + const b = annotationBBox({ type: "STAMP", data: s.data }, measured); + if ( + lp.x >= b.x - pad && + lp.x <= b.x + b.width + pad && + lp.y >= b.y - pad && + lp.y <= b.y + b.height + pad + ) { + onErase(s.id); + return; + } + } for (const s of strokes) { if (s.userId !== ownUserId) continue; const threshold = toPx(s.strokeWidthPct, measured.width) / 2 + pad; diff --git a/components/pdf/PDFViewer.tsx b/components/pdf/PDFViewer.tsx index 9ac4d47..d57f2af 100644 --- a/components/pdf/PDFViewer.tsx +++ b/components/pdf/PDFViewer.tsx @@ -853,6 +853,7 @@ function PdfPage({ strokes={inkStrokes} shapes={inkShapes} redactions={inkRedactions} + stamps={inkStamps} active={inkActive} nextStroke={{ color: drawStyle.color, diff --git a/docs/PLAN-FEAT.md b/docs/PLAN-FEAT.md index 2fd2909..550285a 100644 --- a/docs/PLAN-FEAT.md +++ b/docs/PLAN-FEAT.md @@ -139,9 +139,10 @@ Index: `(file_id)` ### F6 · B3 Eraser -- Tool `erase`: InkLayer active (รับ pointer, `touch-action: none`), `pointerdown/move` ทำ hit-test ตำแหน่ง pointer กับเส้น/รูปทรง **ของตัวเองเท่านั้น** (ส่ง `userId` มากับ strokes): +- Tool `erase`: InkLayer active (รับ pointer, `touch-action: none`), `pointerdown/move` ทำ hit-test ตำแหน่ง pointer กับเส้น/รูปทรง/ตรายาง **ของตัวเองเท่านั้น** (ส่ง `userId` มากับ strokes): - DRAW: ระยะจากจุดถึง segment ทุกท่อน ≤ `strokeWidth/2 + 6px` - SHAPE: ระยะถึงเส้นขอบ (stroke) — ลากผ่าน "พื้นที่" รูปทรงไม่โดน (เหมือนสัมผัสเส้นจริง) + - STAMP: จุดอยู่ใน bbox ของตรา (`annotationBBox` สูตรเดียวกับ render) + 6px — เช็ค**ก่อน**เส้น/รูปทรงเพราะตรา render อยู่ layer บน (TextboxStampLayer) ให้ตัวบนสุดชนะ - โดน = `deleteAnnotation` ทันที (ลากผ่านหลายเส้น = ลบทีละเส้นระหว่างเคลื่อน) — เส้นของคนอื่นไม่ผ่าน hit-test จึงลบไม่ได้ ### F8 · B4+C1 Redaction + Export PDF (ทำคู่กันเสมอ — D4)