ซ่อม text ไทย PDF (ActualText) + revalidatePath ทุก action + คีย์ลัด DEL + ตรายาง label บรรทัดเดียว

- โหลด PDF ผ่าน /api/pdf?fileId= ที่สร้าง ToUnicode CMap ใหม่บนสำเนาในหน่วยความจำ
  จาก ActualText ใน marked content (lib/pdf-actual-text.ts + unit tests) — แก้
  ฟอนต์ไทยที่ cmap เพี้ยน (คำสั่งศาล) ให้ textLayer select/copy/ค้นหา/ชื่อ bookmark
  ถูกต้องตั้งแต่ต้น ไฟล์ใน public/ ไม่ถูกแตะ (invariant #1) — PDF ปกติได้ bytes
  เดิม ไม่มี cost และ fail แล้ว PDFViewer fallback fetch ไฟล์ดิบเอง
- ข้อความจาก selection ห้าม selection.toString() ตรง ๆ: ใช้ selectionTextWithin
  เดินทีละอักษร เรียงตามตำแหน่งบนจอ (จัดการ Thai combining marks + ctrl ขยะ)
  ปิดท้ายด้วย sanitizePdfText ก่อนเก็บ DB/ชื่อ bookmark/คลิปบอร์ด (มี tests)
- mergeRectsByLine ใช้ median ความสูง rect คำนวณเกณฑ์บรรทัดแทนค่าคงที่
- Server Actions ทุกตัวที่เขียน DB เรียก revalidateFilePages (revalidatePath
  หน้า viewer+summary) คู่กับ publish event — แก้กด back แล้วเข้าไฟล์ใหม่ได้
  RSC payload เก่าจาก Router Cache ฝั่ง client จนต้อง hard refresh
- คีย์ลัด DEL/Backspace ลบ annotation ที่เลือกอยู่ (ของตัวเองเท่านั้น ข้ามเมื่อ
  focus อยู่ใน input/textarea/select/contenteditable)
- STAMP: ย่อฟอนต์ label ให้อยู่บรรทัดเดียวพอดีกรอบทั้งบนจอ (canvas measure) และ
  export PDF (stampFitFontSize แชร์กัน) + เพิ่ม handle ปรับขนาดตรายางในแผง
  selection (uniform scale + clamp รายแกน)
- sync invariant/กติกา: CLAUDE.md #4, rules/*.md, PLAN-FEAT.md (F3)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
DESKTOP-1R2VSQH\Lenovo ThinkPad E490 2026-09-11 13:28:35 +07:00
parent 289d735bf3
commit 6fd3a1515f
19 changed files with 890 additions and 53 deletions

View file

@ -244,6 +244,29 @@ describe("scaleAnnotationData", () => {
expect(out).toEqual({ position: { x: 50, y: 10 }, message: "m" });
});
it("STAMP anchor มุมตรงข้าม ×2: position ขยายจาก anchor และ scale ×2", () => {
// bbox เดิม x 10 y 20 กว้าง 18 สูง 7.2 → anchor = มุมขวาล่าง (28, 27.2)
const out = scaleAnnotationData(
{ label: "สำคัญ", position: { x: 10, y: 20 }, scale: 18 },
{ x: 28, y: 27.2 },
2,
2,
);
expect(out).toEqual({ label: "สำคัญ", position: { x: -8, y: 12.8 }, scale: 36 });
});
it("STAMP หลังขยาย ×k รอบ anchor: bbox ใหม่ = ขอบเดิม ×k รอบ anchor (กว้าง-สูงผูกกัน)", () => {
const data = { label: "สำคัญ", position: { x: 10, y: 20 }, scale: 18 };
const anchor = { x: 28, y: 27.2 };
const out = scaleAnnotationData(data, anchor, 1.5, 1.5);
const before = annotationBBox({ type: "STAMP", data }, S);
const after = annotationBBox({ type: "STAMP", data: out }, S);
expect(after.width).toBeCloseTo(before.width * 1.5);
expect(after.height).toBeCloseTo(before.height * 1.5); // สัดส่วน 0.4 คงเดิม
expect(after.x + after.width).toBeCloseTo(anchor.x); // ขอบที่ anchor อยู่นิ่ง
expect(after.y + after.height).toBeCloseTo(anchor.y);
});
it("DRAW: strokeWidth คงเดิมตามสเปก F1", () => {
const out = scaleAnnotationData(
{ strokeColor: "#000", strokeWidth: 2, paths: [{ x: 0, y: 0 }] },