From 651e354cadf666070add5ce56562cd49878cbf80 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Sep 2026 09:56:00 +0700 Subject: [PATCH] =?UTF-8?q?stamp-editor:=20=E0=B8=95=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=A2=E0=B8=B2=E0=B8=87=E0=B8=9E=E0=B8=B4=E0=B8=A1=E0=B8=9E?= =?UTF-8?q?=E0=B9=8C=E0=B9=80=E0=B8=AD=E0=B8=87+=E0=B8=88=E0=B8=B1?= =?UTF-8?q?=E0=B8=94=E0=B8=9A=E0=B8=A3=E0=B8=A3=E0=B8=97=E0=B8=B1=E0=B8=94?= =?UTF-8?q?=E0=B9=81=E0=B8=9A=E0=B8=9A=E0=B8=96=E0=B8=AD=E0=B8=A2=E0=B8=AB?= =?UTF-8?q?=E0=B8=A5=E0=B8=B1=E0=B8=87+=E0=B8=A1=E0=B8=B8=E0=B8=A1?= =?UTF-8?q?=E0=B9=80=E0=B8=AD=E0=B8=B5=E0=B8=A2=E0=B8=87,=20editor=20?= =?UTF-8?q?=E0=B8=81=E0=B8=A5=E0=B9=88=E0=B8=AD=E0=B8=87=E0=B8=82=E0=B9=89?= =?UTF-8?q?=E0=B8=AD=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1=20contentEditable?= =?UTF-8?q?=20+=20=E0=B9=81=E0=B8=81=E0=B9=89=E0=B8=95=E0=B8=B2=E0=B8=A1?= =?UTF-8?q?=20code=20review=202026-09-29=20=E0=B8=84=E0=B8=A3=E0=B8=9A=205?= =?UTF-8?q?=20=E0=B8=82=E0=B9=89=E0=B8=AD=20=E2=80=94=20(1)=20STAMP=20?= =?UTF-8?q?=E0=B8=84=E0=B8=B3=E0=B8=9E=E0=B8=B4=E0=B8=A1=E0=B8=9E=E0=B9=8C?= =?UTF-8?q?=E0=B9=80=E0=B8=AD=E0=B8=87=20(D7):=20dropdown=20=E0=B8=82?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=20tool=20stamp=20=E0=B9=80=E0=B8=9B=E0=B9=87?= =?UTF-8?q?=E0=B8=99=202=20=E0=B8=AB=E0=B8=99=E0=B9=89=E0=B8=B2=20?= =?UTF-8?q?=E2=80=94=20preset=20+=20=E0=B9=81=E0=B8=96=E0=B8=A7=20"?= =?UTF-8?q?=E0=B8=81=E0=B8=B3=E0=B8=AB=E0=B8=99=E0=B8=94=E0=B9=80=E0=B8=AD?= =?UTF-8?q?=E0=B8=87=E2=80=A6"=20(stampCustomMode=20reset=20=E0=B8=97?= =?UTF-8?q?=E0=B8=B8=E0=B8=81=E0=B8=84=E0=B8=A3=E0=B8=B1=E0=B9=89=E0=B8=87?= =?UTF-8?q?=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B9=80=E0=B8=9B=E0=B8=B4=E0=B8=94?= =?UTF-8?q?=E0=B9=80=E0=B8=A1=E0=B8=99=E0=B8=B9)=20=E0=B9=80=E0=B8=9B?= =?UTF-8?q?=E0=B8=B4=E0=B8=94=20popup=20=E0=B9=81=E0=B8=81=E0=B9=89?= =?UTF-8?q?=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1?= =?UTF-8?q?/=E0=B8=AA=E0=B8=B5/=E0=B8=82=E0=B8=99=E0=B8=B2=E0=B8=94=20?= =?UTF-8?q?=E0=B8=9E=E0=B8=A3=E0=B9=89=E0=B8=AD=E0=B8=A1=20preview=20?= =?UTF-8?q?=E0=B8=95=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=B2=E0=B8=87=20live?= =?UTF-8?q?=20=E0=B8=95=E0=B8=B2=E0=B8=A1=E0=B8=AA=E0=B8=B1=E0=B8=94?= =?UTF-8?q?=E0=B8=AA=E0=B9=88=E0=B8=A7=E0=B8=99=E0=B8=88=E0=B8=A3=E0=B8=B4?= =?UTF-8?q?=E0=B8=87=20(=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1=E0=B8=81?= =?UTF-8?q?=E0=B8=A7=E0=B9=89=E0=B8=B2=E0=B8=87=E0=B8=AB=E0=B8=99=E0=B9=89?= =?UTF-8?q?=E0=B8=B2=E0=B9=81=E0=B8=A3=E0=B8=81=20=C3=97=20scale%)=20?= =?UTF-8?q?=E0=B9=83=E0=B8=8A=E0=B9=89=E0=B8=AA=E0=B8=B9=E0=B8=95=E0=B8=A3?= =?UTF-8?q?/=E0=B8=9F=E0=B8=AD=E0=B8=99=E0=B8=95=E0=B9=8C/measure=20?= =?UTF-8?q?=E0=B8=95=E0=B8=B1=E0=B8=A7=E0=B9=80=E0=B8=94=E0=B8=B5=E0=B8=A2?= =?UTF-8?q?=E0=B8=A7=E0=B8=81=E0=B8=B1=E0=B8=9A=20render=20=E0=B8=88?= =?UTF-8?q?=E0=B8=A3=E0=B8=B4=E0=B8=87,=20=E0=B8=84=E0=B8=B3=E0=B8=9E?= =?UTF-8?q?=E0=B8=B4=E0=B8=A1=E0=B8=9E=E0=B9=8C=20live=20=E0=B9=84?= =?UTF-8?q?=E0=B8=9B=20stampPreset=20=E0=B9=80=E0=B8=94=E0=B8=B5=E0=B8=A2?= =?UTF-8?q?=E0=B8=A7=E0=B8=81=E0=B8=B1=E0=B8=9A=20preset,=20Enter/?= =?UTF-8?q?=E0=B8=9B=E0=B8=B8=E0=B9=88=E0=B8=A1=20"=E0=B9=83=E0=B8=8A?= =?UTF-8?q?=E0=B9=89=E0=B8=95=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=E0=B8=99=E0=B8=B5=E0=B9=89"=20=3D=20=E0=B8=9B=E0=B8=B4?= =?UTF-8?q?=E0=B8=94=E0=B9=80=E0=B8=A1=E0=B8=99=E0=B8=B9=E0=B9=81=E0=B8=9A?= =?UTF-8?q?=E0=B8=9A=E0=B9=80=E0=B8=A5=E0=B8=B7=E0=B8=AD=E0=B8=81=E0=B8=84?= =?UTF-8?q?=E0=B9=88=E0=B8=B2=E0=B8=88=E0=B8=9A,=20=E0=B8=9B=E0=B8=B8?= =?UTF-8?q?=E0=B9=88=E0=B8=A1=20tool=20=E0=B9=81=E0=B8=AA=E0=B8=94?= =?UTF-8?q?=E0=B8=87=20"=E0=B8=81=E0=B8=B3=E0=B8=AB=E0=B8=99=E0=B8=94?= =?UTF-8?q?=E0=B9=80=E0=B8=AD=E0=B8=87"=20=E0=B9=81=E0=B8=97=E0=B8=99?= =?UTF-8?q?=E0=B8=84=E0=B8=B3=E0=B9=80=E0=B8=95=E0=B9=87=E0=B8=A1=20+=20?= =?UTF-8?q?=E0=B9=81=E0=B8=81=E0=B9=89=E0=B8=84=E0=B8=B3=E0=B8=95=E0=B8=A3?= =?UTF-8?q?=E0=B8=B2=E0=B8=A2=E0=B8=B2=E0=B8=87=E0=B8=97=E0=B8=B5=E0=B9=88?= =?UTF-8?q?=E0=B8=A7=E0=B8=B2=E0=B8=87=E0=B9=81=E0=B8=A5=E0=B9=89=E0=B8=A7?= =?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=81=E0=B8=81=E0=B9=89=E0=B9=84=E0=B8=82?= =?UTF-8?q?=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1?= =?UTF-8?q?"=20=E0=B9=83=E0=B8=99=E0=B9=81=E0=B8=9C=E0=B8=87=20selection?= =?UTF-8?q?=20(StampEditor=20textarea=20=E0=B9=83=E0=B8=99=20TextboxStampL?= =?UTF-8?q?ayer=20=E0=B9=83=E0=B8=8A=E0=B9=89=20editingId/commitEdit=20?= =?UTF-8?q?=E0=B8=A3=E0=B9=88=E0=B8=A7=E0=B8=A1=E0=B8=81=E0=B8=B1=E0=B8=9A?= =?UTF-8?q?=20TEXTBOX,=20=E0=B8=88=E0=B8=B3=E0=B8=81=E0=B8=B1=E0=B8=94=20S?= =?UTF-8?q?TAMP=5FMAX=5FLABEL=5FCHARS=3D50=20=E0=B8=97=E0=B8=B5=E0=B9=88?= =?UTF-8?q?=20UI=20=E2=80=94=20validator=20=E0=B8=A2=E0=B8=B1=E0=B8=87?= =?UTF-8?q?=E0=B8=A2=E0=B8=AD=E0=B8=A1=20MAX=5FTEXT=5FCHARS=20=E0=B8=95?= =?UTF-8?q?=E0=B8=B2=E0=B8=A1=E0=B9=80=E0=B8=88=E0=B8=95=E0=B8=99=E0=B8=B2?= =?UTF-8?q?)=20(2)=20=E0=B8=88=E0=B8=B1=E0=B8=94=E0=B8=9A=E0=B8=A3?= =?UTF-8?q?=E0=B8=A3=E0=B8=97=E0=B8=B1=E0=B8=94=E0=B9=81=E0=B8=9A=E0=B8=9A?= =?UTF-8?q?=E0=B8=96=E0=B8=AD=E0=B8=A2=E0=B8=AB=E0=B8=A5=E0=B8=B1=E0=B8=87?= =?UTF-8?q?=20(=E0=B8=84=E0=B8=B3=E0=B8=82=E0=B8=AD=E0=B8=9C=E0=B8=B9?= =?UTF-8?q?=E0=B9=89=E0=B9=83=E0=B8=8A=E0=B9=89=E0=B8=A3=E0=B8=AD=E0=B8=9A?= =?UTF-8?q?=E0=B8=AA=E0=B8=AD=E0=B8=87):=20=E0=B8=AA=E0=B8=B9=E0=B8=95?= =?UTF-8?q?=E0=B8=A3=E0=B8=A2=E0=B9=89=E0=B8=B2=E0=B8=A2=E0=B8=A3=E0=B8=A7?= =?UTF-8?q?=E0=B8=A1=E0=B8=97=E0=B8=B5=E0=B9=88=20utils/geometry.ts=20?= =?UTF-8?q?=E2=80=94=20stampLayout:=20=E0=B8=84=E0=B8=B3=E0=B8=AA=E0=B8=B1?= =?UTF-8?q?=E0=B9=89=E0=B8=99=201=20=E0=B8=9A=E0=B8=A3=E0=B8=A3=E0=B8=97?= =?UTF-8?q?=E0=B8=B1=E0=B8=94=E0=B8=9F=E0=B8=AD=E0=B8=99=E0=B8=95=E0=B9=8C?= =?UTF-8?q?=E0=B9=80=E0=B8=95=E0=B9=87=E0=B8=A1,=20=E0=B8=84=E0=B8=B3?= =?UTF-8?q?=E0=B8=A2=E0=B8=B2=E0=B8=A7=E0=B8=9E=E0=B8=AD=E0=B8=94=E0=B8=B5?= =?UTF-8?q?=20=E0=B9=86=20=E0=B8=A2=E0=B9=88=E0=B8=AD=E0=B8=9F=E0=B8=AD?= =?UTF-8?q?=E0=B8=99=E0=B8=95=E0=B9=8C=E0=B9=83=E0=B8=AB=E0=B9=89=E0=B8=AD?= =?UTF-8?q?=E0=B8=A2=E0=B8=B9=E0=B9=88=E0=B8=9A=E0=B8=A3=E0=B8=A3=E0=B8=97?= =?UTF-8?q?=E0=B8=B1=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=B9=88=E0=B8=AD=E0=B8=99=20(stampFitFontSize=20?= =?UTF-8?q?=E0=B9=80=E0=B8=8A=E0=B8=B4=E0=B8=87=E0=B9=80=E0=B8=AA=E0=B9=89?= =?UTF-8?q?=E0=B8=99)=20=E0=B8=88=E0=B8=99=E0=B8=96=E0=B8=B6=E0=B8=87=20fl?= =?UTF-8?q?oor=2010%=20=E0=B8=82=E0=B8=AD=E0=B8=87=E0=B8=84=E0=B8=A7?= =?UTF-8?q?=E0=B8=B2=E0=B8=A1=E0=B8=81=E0=B8=A7=E0=B9=89=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=9A=20("=E0=B8=95=E0=B8=A3?= =?UTF-8?q?=E0=B8=A7=E0=B8=88=E0=B8=AA=E0=B8=AD=E0=B8=9A=E0=B9=81=E0=B8=A5?= =?UTF-8?q?=E0=B9=89=E0=B8=A7"=20=E0=B8=A2=E0=B9=88=E0=B8=AD=20~25%=20=3D?= =?UTF-8?q?=20=E0=B8=9A=E0=B8=A3=E0=B8=A3=E0=B8=97=E0=B8=B1=E0=B8=94?= =?UTF-8?q?=E0=B9=80=E0=B8=94=E0=B8=B5=E0=B8=A2=E0=B8=A7),=20=E0=B8=A2?= =?UTF-8?q?=E0=B8=B2=E0=B8=A7=E0=B9=80=E0=B8=81=E0=B8=B4=E0=B8=99=20floor?= =?UTF-8?q?=20=E0=B8=81=E0=B8=A5=E0=B8=B1=E0=B8=9A=E0=B9=84=E0=B8=9B?= =?UTF-8?q?=E0=B8=9F=E0=B8=AD=E0=B8=99=E0=B8=95=E0=B9=8C=E0=B9=83=E0=B8=AB?= =?UTF-8?q?=E0=B8=8D=E0=B9=88=E0=B9=81=E0=B8=A5=E0=B9=89=E0=B8=A7=20wrapSt?= =?UTF-8?q?ampLabel=20=E2=89=A43=20=E0=B8=9A=E0=B8=A3=E0=B8=A3=E0=B8=97?= =?UTF-8?q?=E0=B8=B1=E0=B8=94=20(=C3=970.92/=E0=B8=A3=E0=B8=AD=E0=B8=9A=20?= =?UTF-8?q?=E0=B9=80=E0=B8=81=E0=B8=B4=E0=B8=99=E0=B8=9A=E0=B8=A3=E0=B8=A3?= =?UTF-8?q?=E0=B8=97=E0=B8=B1=E0=B8=94=E0=B8=A2=E0=B8=AD=E0=B8=A1=E0=B9=81?= =?UTF-8?q?=E0=B8=A5=E0=B8=81=E0=B8=81=E0=B8=B1=E0=B8=9A=E0=B8=9F=E0=B8=AD?= =?UTF-8?q?=E0=B8=99=E0=B8=95=E0=B9=8C=E0=B8=AD=E0=B9=88=E0=B8=B2=E0=B8=99?= =?UTF-8?q?=E0=B8=AD=E0=B8=AD=E0=B8=81),=20stampBoxHeight=20=E0=B9=83?= =?UTF-8?q?=E0=B8=AB=E0=B9=89=201=20=E0=B8=9A=E0=B8=A3=E0=B8=A3=E0=B8=97?= =?UTF-8?q?=E0=B8=B1=E0=B8=94=20=3D=200.4w=20=E0=B9=80=E0=B8=9B=E0=B9=8A?= =?UTF-8?q?=E0=B8=B0=E0=B9=80=E0=B8=AB=E0=B8=A1=E0=B8=B7=E0=B8=AD=E0=B8=99?= =?UTF-8?q?=E0=B9=80=E0=B8=94=E0=B8=B4=E0=B8=A1=20=E2=80=94=20=E0=B8=97?= =?UTF-8?q?=E0=B8=B8=E0=B8=81=E0=B8=A1=E0=B8=B4=E0=B8=95=E0=B8=B4=E0=B9=80?= =?UTF-8?q?=E0=B8=9B=E0=B9=87=E0=B8=99=E0=B8=AA=E0=B8=B1=E0=B8=94=E0=B8=AA?= =?UTF-8?q?=E0=B9=88=E0=B8=A7=E0=B8=99=E0=B8=82=E0=B8=AD=E0=B8=87=20w=20?= =?UTF-8?q?=E0=B8=88=E0=B8=B3=E0=B8=99=E0=B8=A7=E0=B8=99=E0=B8=9A=E0=B8=A3?= =?UTF-8?q?=E0=B8=A3=E0=B8=97=E0=B8=B1=E0=B8=94=E0=B8=88=E0=B8=B6=E0=B8=87?= =?UTF-8?q?=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B8=82=E0=B8=B6=E0=B9=89=E0=B8=99?= =?UTF-8?q?=E0=B8=81=E0=B8=B1=E0=B8=9A=20scale=20=E2=80=94=20constants=20+?= =?UTF-8?q?=20=E0=B8=9F=E0=B8=B1=E0=B8=87=E0=B8=81=E0=B9=8C=E0=B8=8A?= =?UTF-8?q?=E0=B8=B1=E0=B8=99=20re-export=20=E0=B8=88=E0=B8=B2=E0=B8=81=20?= =?UTF-8?q?utils/annotation-display.ts=20=E0=B9=83=E0=B8=AB=E0=B9=89=20imp?= =?UTF-8?q?ort=20=E0=B9=80=E0=B8=94=E0=B8=B4=E0=B8=A1=E0=B8=97=E0=B8=B8?= =?UTF-8?q?=E0=B8=81=E0=B8=88=E0=B8=B8=E0=B8=94=20+=20measureStampText=20(?= =?UTF-8?q?canvas=20bold=20system-ui=20=E0=B8=84=E0=B8=B7=E0=B8=99=200=20?= =?UTF-8?q?=E0=B8=95=E0=B8=AD=E0=B8=99=20SSR)=20=E0=B9=80=E0=B8=9B?= =?UTF-8?q?=E0=B9=87=E0=B8=99=20measure=20=E0=B8=82=E0=B8=AD=E0=B8=87=20re?= =?UTF-8?q?nder/preview/hit-test/eraser=20=E0=B8=97=E0=B8=B1=E0=B9=89?= =?UTF-8?q?=E0=B8=87=E0=B8=9D=E0=B8=B1=E0=B9=88=E0=B8=87=20client=20(annot?= =?UTF-8?q?ationBBox=20=E0=B8=A3=E0=B8=B1=E0=B8=9A=20optional=20measure=20?= =?UTF-8?q?=E2=80=94=20=E0=B9=83=E0=B8=AB=E0=B9=89=E0=B8=84=E0=B8=A7?= =?UTF-8?q?=E0=B8=B2=E0=B8=A1=E0=B8=AA=E0=B8=B9=E0=B8=87=E0=B8=81=E0=B8=A3?= =?UTF-8?q?=E0=B8=AD=E0=B8=9A=E0=B8=95=E0=B8=A3=E0=B8=87=20render=20?= =?UTF-8?q?=E0=B8=88=E0=B8=A3=E0=B8=B4=E0=B8=87=20=E0=B9=81=E0=B8=81?= =?UTF-8?q?=E0=B9=89=E0=B8=82=E0=B8=AD=E0=B8=9A=E0=B8=A5=E0=B9=88=E0=B8=B2?= =?UTF-8?q?=E0=B8=87=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=9A=E0=B9=80=E0=B8=81?= =?UTF-8?q?=E0=B8=B4=E0=B8=99=E0=B8=95=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=B2?= =?UTF-8?q?=E0=B8=87)=20=E0=B8=AA=E0=B9=88=E0=B8=A7=E0=B8=99=E0=B8=9D?= =?UTF-8?q?=E0=B8=B1=E0=B9=88=E0=B8=87=20server=20(validator/export=20stic?= =?UTF-8?q?ky=20note/describeAnnotationChange)=20=E0=B9=83=E0=B8=8A?= =?UTF-8?q?=E0=B9=89=20stampLayoutEstimate=20=E0=B8=84=E0=B8=A7=E0=B8=B2?= =?UTF-8?q?=E0=B8=A1=E0=B8=81=E0=B8=A7=E0=B9=89=E0=B8=B2=E0=B8=87=E0=B8=95?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=AD=E0=B8=B1=E0=B8=81=E0=B8=A9=E0=B8=A3?= =?UTF-8?q?=200.58=20(3)=20=E0=B8=A1=E0=B8=B8=E0=B8=A1=E0=B9=80=E0=B8=AD?= =?UTF-8?q?=E0=B8=B5=E0=B8=A2=E0=B8=87=20StampData.rotation=3F=20(?= =?UTF-8?q?=E2=88=92180..180=20=E0=B8=AB=E0=B8=A1=E0=B8=B8=E0=B8=99?= =?UTF-8?q?=E0=B8=A3=E0=B8=AD=E0=B8=9A=E0=B8=81=E0=B8=A5=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=9A):=20=E0=B8=8A=E0=B9=88?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=20"=E0=B8=A1=E0=B8=B8=E0=B8=A1=E0=B9=80?= =?UTF-8?q?=E0=B8=AD=E0=B8=B5=E0=B8=A2=E0=B8=87"=20=E0=B9=83=E0=B8=99?= =?UTF-8?q?=E0=B9=81=E0=B8=9C=E0=B8=87=20selection=20(patch=20stampRotatio?= =?UTF-8?q?n,=200=C2=B0=20=3D=20=E0=B8=96=E0=B8=AD=E0=B8=94=20field)=20+?= =?UTF-8?q?=20=E0=B8=88=E0=B8=B8=E0=B8=94=E0=B8=88=E0=B8=B1=E0=B8=9A?= =?UTF-8?q?=E0=B8=AB=E0=B8=A1=E0=B8=B8=E0=B8=99=E0=B8=A7=E0=B8=87=E0=B8=81?= =?UTF-8?q?=E0=B8=A5=E0=B8=A1=E0=B9=80=E0=B8=AB=E0=B8=99=E0=B8=B7=E0=B8=AD?= =?UTF-8?q?=E0=B8=82=E0=B8=AD=E0=B8=9A=E0=B8=9A=E0=B8=99=20(mode=20rotate?= =?UTF-8?q?=20=E2=80=94=20atan2=20delta=20=E0=B8=A3=E0=B8=AD=E0=B8=9A?= =?UTF-8?q?=E0=B8=81=E0=B8=A5=E0=B8=B2=E0=B8=87=E0=B8=81=E0=B8=A3=E0=B8=AD?= =?UTF-8?q?=E0=B8=9A=20+=20snap=20=E0=B8=97=E0=B8=A7=E0=B8=B5=E0=B8=84?= =?UTF-8?q?=E0=B8=B9=E0=B8=93=2015=C2=B0=20=E2=89=A44=C2=B0),=20=E0=B8=81?= =?UTF-8?q?=E0=B8=A3=E0=B8=AD=E0=B8=9A=20selection+handles=20=E0=B8=AB?= =?UTF-8?q?=E0=B8=A1=E0=B8=B8=E0=B8=99=E0=B8=94=E0=B9=89=E0=B8=A7=E0=B8=A2?= =?UTF-8?q?=20CSS=20transform=20=E0=B8=97=E0=B8=B5=E0=B9=88=20origin=20?= =?UTF-8?q?=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=E0=B8=9E=E0=B8=B4=E0=B8=81?= =?UTF-8?q?=E0=B8=B1=E0=B8=94=E0=B9=83=E0=B8=99=20element=20=E0=B8=9C?= =?UTF-8?q?=E0=B9=88=E0=B8=B2=E0=B8=99=20rotOn()=20(=E0=B9=83=E0=B8=AA?= =?UTF-8?q?=E0=B9=88=E0=B8=9E=E0=B8=B4=E0=B8=81=E0=B8=B1=E0=B8=94=20page?= =?UTF-8?q?=20=E0=B9=81=E0=B8=A5=E0=B9=89=E0=B8=A7=20div=20=E0=B9=81?= =?UTF-8?q?=E0=B8=95=E0=B9=88=E0=B8=A5=E0=B8=B0=E0=B8=95=E0=B8=B1=E0=B8=A7?= =?UTF-8?q?=E0=B8=AB=E0=B8=A1=E0=B8=B8=E0=B8=99=E0=B8=84=E0=B8=99=E0=B8=A5?= =?UTF-8?q?=E0=B8=B0=E0=B8=88=E0=B8=B8=E0=B8=94=E0=B8=88=E0=B8=99=E0=B8=81?= =?UTF-8?q?=E0=B8=A3=E0=B8=AD=E0=B8=9A=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B8=95?= =?UTF-8?q?=E0=B8=A3=E0=B8=87=E0=B8=95=E0=B8=B1=E0=B8=A7=E0=B8=95=E0=B8=A3?= =?UTF-8?q?=E0=B8=B2=E0=B8=A2=E0=B8=B2=E0=B8=87=20=E2=80=94=20bug=202026-0?= =?UTF-8?q?9-28),=20annotationBBox=20=E0=B8=84=E0=B8=B7=E0=B8=99=20AABB=20?= =?UTF-8?q?=E0=B8=82=E0=B8=AD=E0=B8=87=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=9A?= =?UTF-8?q?=E0=B8=AB=E0=B8=A1=E0=B8=B8=E0=B8=99=20(stampRect/rotatedRectAA?= =?UTF-8?q?BB/rotatePoint=20=E2=80=94=20anchor=20handle=20scale=20?= =?UTF-8?q?=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=E0=B8=A1=E0=B8=B8=E0=B8=A1?= =?UTF-8?q?=E0=B8=AB=E0=B8=A5=E0=B8=B1=E0=B8=87=E0=B8=AB=E0=B8=A1=E0=B8=B8?= =?UTF-8?q?=E0=B8=99=E0=B8=9A=E0=B8=99=E0=B8=88=E0=B8=AD=20=E0=B9=81?= =?UTF-8?q?=E0=B8=A5=E0=B8=B0=20k=20=E0=B8=84=E0=B8=B3=E0=B8=99=E0=B8=A7?= =?UTF-8?q?=E0=B8=93=E0=B9=83=E0=B8=99=20frame=20=E0=B8=82=E0=B8=AD?= =?UTF-8?q?=E0=B8=87=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=9A),=20clampStampInt?= =?UTF-8?q?oPage=20=E0=B9=80=E0=B8=A5=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B8=99?= =?UTF-8?q?=20position=20=E0=B8=81=E0=B8=A5=E0=B8=B1=E0=B8=9A=E0=B9=80?= =?UTF-8?q?=E0=B8=82=E0=B9=89=E0=B8=B2=E0=B8=AB=E0=B8=99=E0=B9=89=E0=B8=B2?= =?UTF-8?q?=E0=B8=95=E0=B8=AD=E0=B8=99=E0=B8=95=E0=B8=B1=E0=B9=89=E0=B8=87?= =?UTF-8?q?=E0=B8=A1=E0=B8=B8=E0=B8=A1=20(validator=20=E0=B8=95=E0=B8=A3?= =?UTF-8?q?=E0=B8=A7=E0=B8=88=20AABB=20=E0=B9=84=E0=B8=A1=E0=B9=88?= =?UTF-8?q?=E0=B8=95=E0=B8=B4=E0=B8=94=E0=B8=A5=E0=B8=9A=20=E0=B9=84?= =?UTF-8?q?=E0=B8=A1=E0=B9=88=E0=B9=83=E0=B8=8A=E0=B9=88=20position=20?= =?UTF-8?q?=E0=B8=95=E0=B8=A3=E0=B8=87=20=E0=B9=86=20=E2=80=94=20position?= =?UTF-8?q?=20=E0=B8=A1=E0=B8=B8=E0=B8=A1=E0=B8=81=E0=B9=88=E0=B8=AD?= =?UTF-8?q?=E0=B8=99=E0=B8=AB=E0=B8=A1=E0=B8=B8=E0=B8=99=E0=B8=95=E0=B8=B4?= =?UTF-8?q?=E0=B8=94=E0=B8=A5=E0=B8=9A=E0=B9=84=E0=B8=94=E0=B9=89=E0=B9=80?= =?UTF-8?q?=E0=B8=A1=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B8=81=E0=B8=A3=E0=B8=AD?= =?UTF-8?q?=E0=B8=9A=E0=B8=88=E0=B8=A3=E0=B8=B4=E0=B8=87=E0=B8=A2=E0=B8=B1?= =?UTF-8?q?=E0=B8=87=E0=B9=83=E0=B8=99=E0=B8=AB=E0=B8=99=E0=B9=89=E0=B8=B2?= =?UTF-8?q?),=20StampEditor=20=E0=B9=81=E0=B8=AA=E0=B8=94=E0=B8=87?= =?UTF-8?q?=E0=B8=95=E0=B8=A3=E0=B8=87=200=C2=B0=20=E0=B8=9E=E0=B8=B4?= =?UTF-8?q?=E0=B8=A1=E0=B8=9E=E0=B9=8C=E0=B8=87=E0=B9=88=E0=B8=B2=E0=B8=A2?= =?UTF-8?q?=20=E0=B8=9B=E0=B8=B4=E0=B8=94=E0=B9=81=E0=B8=A5=E0=B9=89?= =?UTF-8?q?=E0=B8=A7=E0=B8=81=E0=B8=A5=E0=B8=B1=E0=B8=9A=E0=B9=84=E0=B8=9B?= =?UTF-8?q?=E0=B9=80=E0=B8=AD=E0=B8=B5=E0=B8=A2=E0=B8=87=E0=B8=84=E0=B9=88?= =?UTF-8?q?=E0=B8=B2=E0=B9=80=E0=B8=94=E0=B8=B4=E0=B8=A1,=20export=20?= =?UTF-8?q?=E0=B9=83=E0=B8=8A=E0=B9=89=20pdf-lib=20rotate:=20degrees(?= =?UTF-8?q?=E2=88=92=CE=B8)=20+=20=E0=B9=80=E0=B8=A5=E0=B8=B7=E0=B9=88?= =?UTF-8?q?=E0=B8=AD=E0=B8=99=20anchor=20=E0=B9=84=E0=B8=9B=E0=B8=A1?= =?UTF-8?q?=E0=B8=B8=E0=B8=A1=E0=B8=A5=E0=B9=88=E0=B8=B2=E0=B8=87=E0=B8=8B?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=A2=E0=B8=AB=E0=B8=A5=E0=B8=B1=E0=B8=87?= =?UTF-8?q?=E0=B8=AB=E0=B8=A1=E0=B8=B8=E0=B8=99=20(pdf-lib=20=E0=B8=AB?= =?UTF-8?q?=E0=B8=A1=E0=B8=B8=E0=B8=99=E0=B8=A3=E0=B8=AD=E0=B8=9A=20anchor?= =?UTF-8?q?=20=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B9=83=E0=B8=8A=E0=B9=88=20cen?= =?UTF-8?q?ter)=20+=20per-line=20baseline=20=E0=B8=AB=E0=B8=A1=E0=B8=B8?= =?UTF-8?q?=E0=B8=99=E0=B8=A3=E0=B8=AD=E0=B8=9A=E0=B8=81=E0=B8=A5=E0=B8=B2?= =?UTF-8?q?=E0=B8=87=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=9A=E0=B8=95=E0=B8=A3?= =?UTF-8?q?=E0=B8=87=20CSS=20rotate=20=E0=B8=97=E0=B8=B8=E0=B8=81=E0=B8=AD?= =?UTF-8?q?=E0=B8=87=E0=B8=A8=E0=B8=B2=20(4)=20TextboxEditor=20=E0=B9=80?= =?UTF-8?q?=E0=B8=9B=E0=B9=87=E0=B8=99=20contentEditable=20WYSIWYG=20?= =?UTF-8?q?=E0=B9=81=E0=B8=97=E0=B8=99=20textarea=20(=E0=B8=95=E0=B8=B1?= =?UTF-8?q?=E0=B8=A7=E0=B8=AB=E0=B8=99=E0=B8=B2=E0=B9=80=E0=B8=89=E0=B8=9E?= =?UTF-8?q?=E0=B8=B2=E0=B8=B0=E0=B8=AA=E0=B9=88=E0=B8=A7=E0=B8=99=20Ctrl/C?= =?UTF-8?q?md+B=20+=20Enter=20=3D=20insertLineBreak=20=E0=B9=84=E0=B8=A1?= =?UTF-8?q?=E0=B9=88=E0=B9=81=E0=B8=95=E0=B8=81=20div,=20paste=20=E0=B9=80?= =?UTF-8?q?=E0=B8=9B=E0=B9=87=E0=B8=99=20plain=20text):=20utils/textbox-ed?= =?UTF-8?q?itor.ts=20=E0=B9=83=E0=B8=AB=E0=B8=A1=E0=B9=88=20=E2=80=94=20se?= =?UTF-8?q?gmentsToHtml=20(escape=20&=20<=20>=20=E0=B8=81=E0=B9=88?= =?UTF-8?q?=E0=B8=AD=E0=B8=99=E0=B9=83=E0=B8=AA=E0=B9=88=20dangerouslySetI?= =?UTF-8?q?nnerHTML=20+=20=20+=20
)=20=E0=B9=81=E0=B8=A5=E0=B8=B0=20?= =?UTF-8?q?serializeEditor=20(DOM=20=E2=86=92=20text=20=E0=B8=A3=E0=B8=A7?= =?UTF-8?q?=E0=B8=A1=20+=20segments=20=E0=B9=81=E0=B8=A2=E0=B8=81=E0=B8=95?= =?UTF-8?q?=E0=B8=B1=E0=B8=A7=E0=B8=AB=E0=B8=99=E0=B8=B2:=20=E0=B8=82?= =?UTF-8?q?=E0=B8=B6=E0=B9=89=E0=B8=99=E0=B8=9A=E0=B8=A3=E0=B8=A3=E0=B8=97?= =?UTF-8?q?=E0=B8=B1=E0=B8=94=E0=B8=88=E0=B8=B2=E0=B8=81=E0=B8=97=E0=B8=B1?= =?UTF-8?q?=E0=B9=89=E0=B8=87=20
=20=E0=B9=81=E0=B8=A5=E0=B8=B0=20
?= =?UTF-8?q?/

=20=E0=B8=97=E0=B8=B5=E0=B9=88=20browser=20=E0=B8=AA?= =?UTF-8?q?=E0=B8=A3=E0=B9=89=E0=B8=B2=E0=B8=87=20=E2=80=94=20push=20\n=20?= =?UTF-8?q?=E0=B8=81=E0=B9=88=E0=B8=AD=E0=B8=99=E0=B9=80=E0=B8=82=E0=B9=89?= =?UTF-8?q?=E0=B8=B2=20block=20=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B9=83?= =?UTF-8?q?=E0=B8=8A=E0=B9=88=E0=B8=AB=E0=B8=A5=E0=B8=B1=E0=B8=87=E0=B8=88?= =?UTF-8?q?=E0=B8=9A=20block=20=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B8=87?= =?UTF-8?q?=E0=B8=B1=E0=B9=89=E0=B8=99=20"abc

def
"=20=E0=B8=82?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=20Chrome=20=E0=B8=81=E0=B8=A5=E0=B8=B2?= =?UTF-8?q?=E0=B8=A2=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=20"abcdef",=20ded?= =?UTF-8?q?upe=20
+block=20=E0=B8=95=E0=B8=B4=E0=B8=94=E0=B8=81?= =?UTF-8?q?=E0=B8=B1=E0=B8=99,=20trimEnd=20=E0=B8=95=E0=B8=B1=E0=B8=94?= =?UTF-8?q?=E0=B8=88=E0=B8=B2=E0=B8=81=20segments=20=E0=B9=83=E0=B8=AB?= =?UTF-8?q?=E0=B9=89=E0=B8=95=E0=B8=A3=E0=B8=87=20text=20=E0=B8=81?= =?UTF-8?q?=E0=B8=B1=E0=B8=99=20
=20=E0=B8=9B=E0=B8=B4=E0=B8=94?= =?UTF-8?q?=E0=B8=97=E0=B9=89=E0=B8=B2=E0=B8=A2=E0=B8=82=E0=B8=AD=E0=B8=87?= =?UTF-8?q?=20browser=20=E0=B8=97=E0=B8=B4=E0=B9=89=E0=B8=87=20\n=20?= =?UTF-8?q?=E0=B8=84=E0=B9=89=E0=B8=B2=E0=B8=87=E0=B9=83=E0=B8=99=20DB)=20?= =?UTF-8?q?=E2=80=94=20=E0=B9=81=E0=B8=A2=E0=B8=81=E0=B9=80=E0=B8=9B?= =?UTF-8?q?=E0=B9=87=E0=B8=99=20pure=20DOM=20util=20=E0=B9=83=E0=B8=AB?= =?UTF-8?q?=E0=B9=89=20unit=20test=20=E0=B8=81=E0=B8=B1=E0=B8=9A=20jsdom?= =?UTF-8?q?=20=E0=B9=84=E0=B8=94=E0=B9=89=20(tests/utils/textbox-editor.te?= =?UTF-8?q?st.ts=20=E0=B9=83=E0=B8=AB=E0=B8=A1=E0=B9=88=2016=20=E0=B9=80?= =?UTF-8?q?=E0=B8=84=E0=B8=AA=20=E0=B8=95=E0=B8=B2=E0=B8=A1=20pattern=20us?= =?UTF-8?q?eCustomSelection-dom)=20(5)=20=E0=B8=84=E0=B8=B4=E0=B8=A7=20upd?= =?UTF-8?q?ate=20=E0=B8=95=E0=B9=88=E0=B8=AD=20id=20=E0=B9=83=E0=B8=99=20P?= =?UTF-8?q?DFViewer=20(updateChainRef=20+=20pendingDataBaseRef):=20update?= =?UTF-8?q?=20=E0=B8=97=E0=B8=B8=E0=B8=81=E0=B8=95=E0=B8=B1=E0=B8=A7?= =?UTF-8?q?=E0=B8=82=E0=B8=AD=E0=B8=87=20id=20=E0=B9=80=E0=B8=94=E0=B8=B5?= =?UTF-8?q?=E0=B8=A2=E0=B8=A7=E0=B8=81=E0=B8=B1=E0=B8=99=E0=B9=80=E0=B8=82?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=84=E0=B8=B4=E0=B8=A7=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=99=20base?= =?UTF-8?q?=20=E0=B8=84=E0=B8=B3=E0=B8=99=E0=B8=A7=E0=B8=93=E0=B8=95?= =?UTF-8?q?=E0=B8=AD=E0=B8=99=E0=B8=96=E0=B8=B6=E0=B8=87=E0=B8=95=E0=B8=B2?= =?UTF-8?q?=E0=B8=84=E0=B8=B4=E0=B8=A7=E0=B8=88=E0=B8=B2=E0=B8=81=20data?= =?UTF-8?q?=20=E0=B8=A5=E0=B9=88=E0=B8=B2=E0=B8=AA=E0=B8=B8=E0=B8=94?= =?UTF-8?q?=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B8=84=E0=B8=B4=E0=B8=A7=E0=B8=81?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=99=E0=B8=AB=E0=B8=99=E0=B9=89=E0=B8=B2?= =?UTF-8?q?=E0=B8=9A=E0=B8=B1=E0=B8=99=E0=B8=97=E0=B8=B6=E0=B8=81=E0=B9=84?= =?UTF-8?q?=E0=B8=A7=E0=B9=89=20=E2=80=94=20=E0=B8=88=E0=B8=AD=E0=B8=AA?= =?UTF-8?q?=E0=B8=B1=E0=B8=A1=E0=B8=9C=E0=B8=B1=E0=B8=AA=20blur-commit=20?= =?UTF-8?q?=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1?= =?UTF-8?q?=E0=B9=81=E0=B8=A5=E0=B9=89=E0=B8=A7=E0=B9=81=E0=B8=95=E0=B8=B0?= =?UTF-8?q?=E0=B8=AA=E0=B8=B5/=E0=B8=82=E0=B8=99=E0=B8=B2=E0=B8=94?= =?UTF-8?q?=E0=B8=95=E0=B9=88=E0=B8=AD=E0=B8=97=E0=B8=B1=E0=B8=99=E0=B8=97?= =?UTF-8?q?=E0=B8=B5=20=E0=B8=84=E0=B8=AD=E0=B8=A1=E0=B8=A1=E0=B8=B4?= =?UTF-8?q?=E0=B8=95=20full=20object=20=E0=B8=88=E0=B8=B2=E0=B8=81=20base?= =?UTF-8?q?=20=E0=B9=80=E0=B8=81=E0=B9=88=E0=B8=B2=E0=B8=88=E0=B8=B0?= =?UTF-8?q?=E0=B8=97=E0=B8=B1=E0=B8=9A=E0=B8=84=E0=B9=88=E0=B8=B2=E0=B8=97?= =?UTF-8?q?=E0=B8=B5=E0=B9=88=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88=E0=B8=87?= =?UTF-8?q?=E0=B9=80=E0=B8=9B=E0=B8=A5=E0=B8=B5=E0=B9=88=E0=B8=A2=E0=B8=99?= =?UTF-8?q?=20(code=20review=202026-09-28=20=E0=B8=A3=E0=B8=AD=E0=B8=9A?= =?UTF-8?q?=E0=B8=AA=E0=B8=AD=E0=B8=87)=20=E2=80=94=20=E0=B8=95=E0=B8=B1?= =?UTF-8?q?=E0=B8=A7=E0=B8=84=E0=B8=A7=E0=B8=9A=E0=B8=84=E0=B8=B8=E0=B8=A1?= =?UTF-8?q?=E0=B9=80=E0=B8=A5=E0=B9=87=E0=B8=81=E0=B8=82=E0=B8=AD=E0=B8=87?= =?UTF-8?q?=E0=B9=81=E0=B8=9C=E0=B8=87/editor=20commit=20=E0=B9=80?= =?UTF-8?q?=E0=B8=9B=E0=B9=87=E0=B8=99=20AnnotationPatch=20=E0=B8=9C?= =?UTF-8?q?=E0=B8=AA=E0=B8=B2=E0=B8=99=E0=B8=9C=E0=B9=88=E0=B8=B2=E0=B8=99?= =?UTF-8?q?=20applyAnnotationPatch=20(color/strokeWidth/highlightStyle/hig?= =?UTF-8?q?hlightOpacity/stampScale/stampRotation/textbox/textboxText/labe?= =?UTF-8?q?l)=20(6)=20describeAnnotationChange:=20STAMP=20=E0=B9=81?= =?UTF-8?q?=E0=B8=AA=E0=B8=94=E0=B8=87=20"=E0=B8=A1=E0=B8=B8=E0=B8=A1?= =?UTF-8?q?=E0=B9=80=E0=B8=AD=E0=B8=B5=E0=B8=A2=E0=B8=87"=20=E0=B9=81?= =?UTF-8?q?=E0=B8=A5=E0=B8=B0=E0=B8=82=E0=B9=89=E0=B8=B2=E0=B8=A1=20geomet?= =?UTF-8?q?ry=20=E0=B9=80=E0=B8=A1=E0=B8=B7=E0=B9=88=E0=B8=AD=20label/?= =?UTF-8?q?=E0=B8=A1=E0=B8=B8=E0=B8=A1=E0=B9=80=E0=B8=9B=E0=B8=A5=E0=B8=B5?= =?UTF-8?q?=E0=B9=88=E0=B8=A2=E0=B8=99=20(geometry=20=E0=B8=95=E0=B9=88?= =?UTF-8?q?=E0=B8=B2=E0=B8=87=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=E0=B8=9C?= =?UTF-8?q?=E0=B8=A5=E0=B8=9E=E0=B8=A5=E0=B8=AD=E0=B8=87=E0=B8=82=E0=B8=AD?= =?UTF-8?q?=E0=B8=87=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=88=E0=B8=B1=E0=B8=94?= =?UTF-8?q?=E0=B8=A7=E0=B8=B2=E0=B8=87=20=E0=B9=81=E0=B8=9A=E0=B8=9A?= =?UTF-8?q?=E0=B9=80=E0=B8=94=E0=B8=B5=E0=B8=A2=E0=B8=A7=E0=B8=81=E0=B8=B1?= =?UTF-8?q?=E0=B8=9A=20TEXTBOX=20=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B8=82?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=A1=E0=B8=9B=E0=B8=A3=E0=B8=B1=E0=B8=9A?= =?UTF-8?q?=E0=B8=82=E0=B8=99=E0=B8=B2=E0=B8=94=E0=B9=80=E0=B8=A1=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=84=E0=B8=A7?= =?UTF-8?q?=E0=B8=B2=E0=B8=A1=E0=B9=80=E0=B8=9B=E0=B8=A5=E0=B8=B5=E0=B9=88?= =?UTF-8?q?=E0=B8=A2=E0=B8=99)=20(7)=20=E0=B9=81=E0=B8=81=E0=B9=89?= =?UTF-8?q?=E0=B8=95=E0=B8=B2=E0=B8=A1=20code=20review=202026-09-29=20?= =?UTF-8?q?=E0=B8=84=E0=B8=A3=E0=B8=9A:=20(a)=20patch=20stampScale/label?= =?UTF-8?q?=20=E0=B8=82=E0=B8=AD=E0=B8=87=E0=B8=95=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=A2=E0=B8=B2=E0=B8=87=E0=B9=80=E0=B8=AD=E0=B8=B5=E0=B8=A2?= =?UTF-8?q?=E0=B8=87=E0=B9=80=E0=B8=A3=E0=B8=B5=E0=B8=A2=E0=B8=81=20clampS?= =?UTF-8?q?tampIntoPage=20=E0=B9=80=E0=B8=AB=E0=B8=A1=E0=B8=B7=E0=B8=AD?= =?UTF-8?q?=E0=B8=99=20stampRotation=20=E2=80=94=20=E0=B9=80=E0=B8=94?= =?UTF-8?q?=E0=B8=B4=E0=B8=A1=E0=B8=82=E0=B8=A2=E0=B8=B2=E0=B8=A2/?= =?UTF-8?q?=E0=B9=81=E0=B8=81=E0=B9=89=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=84?= =?UTF-8?q?=E0=B8=A7=E0=B8=B2=E0=B8=A1=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B8=8A?= =?UTF-8?q?=E0=B8=B4=E0=B8=94=E0=B8=82=E0=B8=AD=E0=B8=9A=E0=B9=81=E0=B8=A5?= =?UTF-8?q?=E0=B9=89=E0=B8=A7=20AABB=20=E0=B8=A2=E0=B8=B7=E0=B9=88?= =?UTF-8?q?=E0=B8=99=E0=B8=95=E0=B8=B4=E0=B8=94=E0=B8=A5=E0=B8=9A=20update?= =?UTF-8?q?=20=E0=B9=82=E0=B8=94=E0=B8=99=20validator=20=E0=B8=9B=E0=B8=8F?= =?UTF-8?q?=E0=B8=B4=E0=B9=80=E0=B8=AA=E0=B8=98=E0=B9=80=E0=B8=94=E0=B9=89?= =?UTF-8?q?=E0=B8=87=E0=B8=81=E0=B8=A5=E0=B8=B1=E0=B8=9A=E0=B8=97=E0=B8=B1?= =?UTF-8?q?=E0=B9=89=E0=B8=87=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B8=9A=E0=B8=99?= =?UTF-8?q?=E0=B8=88=E0=B8=AD=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=9A=E0=B8=A2?= =?UTF-8?q?=E0=B8=B1=E0=B8=87=E0=B8=AD=E0=B8=A2=E0=B8=B9=E0=B9=88=E0=B9=83?= =?UTF-8?q?=E0=B8=99=E0=B8=AB=E0=B8=99=E0=B9=89=E0=B8=B2=20(b)=20validator?= =?UTF-8?q?=20STAMP=20=E0=B8=9C=E0=B9=88=E0=B8=AD=E0=B8=99=20tolerance=20?= =?UTF-8?q?=E0=B9=83=E0=B8=AB=E0=B9=89=20AABB=20=E0=B8=95=E0=B8=B4?= =?UTF-8?q?=E0=B8=94=E0=B8=A5=E0=B8=9A=E0=B9=84=E0=B8=94=E0=B9=89=20?= =?UTF-8?q?=E2=89=A42%=20=E0=B8=82=E0=B8=AD=E0=B8=87=E0=B8=AB=E0=B8=99?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=20=E0=B8=81=E0=B8=A5=E0=B8=B7=E0=B8=99?= =?UTF-8?q?=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1=E0=B8=84=E0=B8=A5=E0=B8=B2?= =?UTF-8?q?=E0=B8=94=20measure=20=E0=B8=88=E0=B8=A3=E0=B8=B4=E0=B8=87?= =?UTF-8?q?=E0=B8=82=E0=B8=AD=E0=B8=87=20browser=20=E0=B8=81=E0=B8=B1?= =?UTF-8?q?=E0=B8=9A=20stampLayoutEstimate=20=E0=B8=9D=E0=B8=B1=E0=B9=88?= =?UTF-8?q?=E0=B8=87=20server=20(=E0=B8=9F=E0=B8=AD=E0=B8=99=E0=B8=95?= =?UTF-8?q?=E0=B9=8C=E0=B8=95=E0=B9=88=E0=B8=AD=E0=B9=80=E0=B8=84=E0=B8=A3?= =?UTF-8?q?=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B8=87=E0=B8=97=E0=B8=B3=E0=B9=83?= =?UTF-8?q?=E0=B8=AB=E0=B9=89=E0=B8=88=E0=B8=B3=E0=B8=99=E0=B8=A7=E0=B8=99?= =?UTF-8?q?=E0=B8=9A=E0=B8=A3=E0=B8=A3=E0=B8=97=E0=B8=B1=E0=B8=94/AABB=20?= =?UTF-8?q?=E0=B8=84=E0=B8=A5=E0=B8=B2=E0=B8=94=20=E2=80=94=20=E0=B8=84?= =?UTF-8?q?=E0=B9=88=E0=B8=B2=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B8=9A=E0=B8=99?= =?UTF-8?q?=E0=B8=88=E0=B8=AD=E0=B8=9E=E0=B8=AD=E0=B8=94=E0=B8=B5=E0=B9=80?= =?UTF-8?q?=E0=B8=9B=E0=B9=8A=E0=B8=B0=E0=B8=95=E0=B9=89=E0=B8=AD=E0=B8=87?= =?UTF-8?q?=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B8=96=E0=B8=B9=E0=B8=81=E0=B8=9B?= =?UTF-8?q?=E0=B8=8F=E0=B8=B4=E0=B9=80=E0=B8=AA=E0=B8=98)=20(c)=20applyUpd?= =?UTF-8?q?ate:=20applyAnnotation=20=E0=B9=80=E0=B8=82=E0=B9=89=E0=B8=B2?= =?UTF-8?q?=20try=20=E0=B9=80=E0=B8=94=E0=B8=B5=E0=B8=A2=E0=B8=A7=E0=B8=81?= =?UTF-8?q?=E0=B8=B1=E0=B8=9A=20action=20=E0=B9=83=E0=B8=AB=E0=B9=89=20res?= =?UTF-8?q?olve()=20=E0=B9=83=E0=B8=99=20finally=20=E0=B8=84=E0=B8=A3?= =?UTF-8?q?=E0=B8=AD=E0=B8=9A=E0=B8=97=E0=B8=B1=E0=B9=89=E0=B8=87=20callba?= =?UTF-8?q?ck=20=E2=80=94=20=E0=B9=80=E0=B8=94=E0=B8=B4=E0=B8=A1=20dispatc?= =?UTF-8?q?her=20=E0=B9=82=E0=B8=A2=E0=B8=99=20sync=20=E0=B9=81=E0=B8=A5?= =?UTF-8?q?=E0=B9=89=E0=B8=A7=20promise=20=E0=B9=84=E0=B8=A1=E0=B9=88=20se?= =?UTF-8?q?ttle=20=E0=B8=95=E0=B8=A5=E0=B8=AD=E0=B8=94=E0=B8=81=E0=B8=B2?= =?UTF-8?q?=E0=B8=A5=E0=B8=88=E0=B8=99=E0=B8=84=E0=B8=B4=E0=B8=A7=E0=B8=82?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=20id=20=E0=B8=99=E0=B8=B1=E0=B9=89=E0=B8=99?= =?UTF-8?q?=E0=B8=95=E0=B8=B2=E0=B8=A2=E0=B8=96=E0=B8=B2=E0=B8=A7=E0=B8=A3?= =?UTF-8?q?=20+=20enqueueUpdate=20=E0=B9=80=E0=B8=9B=E0=B8=A5=E0=B8=B5?= =?UTF-8?q?=E0=B9=88=E0=B8=A2=E0=B8=99=20then(cleanup,=20cleanup)=20?= =?UTF-8?q?=E0=B8=81=E0=B8=B1=E0=B8=99=20unhandled=20rejection=20(d)=20cat?= =?UTF-8?q?ch=20=E0=B8=82=E0=B8=AD=E0=B8=87=20applyUpdate=20=E0=B8=A5?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=87=20pendingDataBaseRef=20=E2=80=94=20?= =?UTF-8?q?=E0=B9=80=E0=B8=94=E0=B8=B4=E0=B8=A1=20action=20=E0=B8=81?= =?UTF-8?q?=E0=B8=A5=E0=B8=B2=E0=B8=87=E0=B8=84=E0=B8=B4=E0=B8=A7=E0=B8=A5?= =?UTF-8?q?=E0=B9=89=E0=B8=A1=E0=B9=81=E0=B8=A5=E0=B9=89=E0=B8=A7=20update?= =?UTF-8?q?=20=E0=B8=96=E0=B8=B1=E0=B8=94=E0=B9=84=E0=B8=9B=E0=B8=9C?= =?UTF-8?q?=E0=B8=AA=E0=B8=B2=E0=B8=99=20base=20=E0=B8=97=E0=B8=B5?= =?UTF-8?q?=E0=B9=88=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B9=80=E0=B8=84=E0=B8=A2?= =?UTF-8?q?=E0=B8=95=E0=B8=B4=E0=B8=94=20DB=20=E0=B8=88=E0=B8=99=20commit?= =?UTF-8?q?=20=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B9=80=E0=B8=9B=E0=B8=A5?= =?UTF-8?q?=E0=B8=B5=E0=B9=88=E0=B8=A2=E0=B8=99=E0=B8=97=E0=B8=B5=E0=B9=88?= =?UTF-8?q?=E0=B8=A5=E0=B9=89=E0=B8=A1=E0=B9=84=E0=B8=9B=E0=B8=94=E0=B9=89?= =?UTF-8?q?=E0=B8=A7=E0=B8=A2=20=E0=B9=81=E0=B8=A5=E0=B8=B0=20undo=20?= =?UTF-8?q?=E0=B8=81=E0=B8=B9=E0=B9=89=E0=B8=AA=E0=B8=96=E0=B8=B2=E0=B8=99?= =?UTF-8?q?=E0=B8=B0=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B9=84=E0=B8=A1=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=E0=B8=B5=E0=B8=AD=E0=B8=A2=E0=B8=B9=E0=B9=88=E0=B8=88?= =?UTF-8?q?=E0=B8=A3=E0=B8=B4=E0=B8=87=20(e)=20=E0=B8=96=E0=B8=AD=E0=B8=94?= =?UTF-8?q?=20as=20StampData=20cast=202=20=E0=B8=88=E0=B8=B8=E0=B8=94?= =?UTF-8?q?=E0=B9=83=E0=B8=99=20AnnotationSelectionLayer=20=E2=80=94=20?= =?UTF-8?q?=E0=B8=A2=E0=B9=89=E0=B8=B2=E0=B8=A2=20stampData=20=E0=B8=97?= =?UTF-8?q?=E0=B8=B5=E0=B9=88=20narrow=20=E0=B8=94=E0=B9=89=E0=B8=A7?= =?UTF-8?q?=E0=B8=A2=20"label"=20in=20=E0=B8=82=E0=B8=B6=E0=B9=89=E0=B8=99?= =?UTF-8?q?=E0=B9=83=E0=B8=8A=E0=B9=89=E0=B9=81=E0=B8=97=E0=B8=99=E0=B9=83?= =?UTF-8?q?=E0=B8=AB=E0=B9=89=20type=20guard=20=E0=B8=97=E0=B8=B3=E0=B8=87?= =?UTF-8?q?=E0=B8=B2=E0=B8=99=20(f)=20export=20sticky=20note=20=E0=B8=82?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=20STAMP=20=E0=B8=AA=E0=B9=88=E0=B8=87=20meas?= =?UTF-8?q?ure=20=E0=B9=80=E0=B8=94=E0=B8=B5=E0=B8=A2=E0=B8=A7=E0=B8=81?= =?UTF-8?q?=E0=B8=B1=E0=B8=9A=E0=B8=95=E0=B8=AD=E0=B8=99=E0=B8=A7=E0=B8=B2?= =?UTF-8?q?=E0=B8=94=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=9A=20(thaiFont.width?= =?UTF-8?q?OfTextAtSize)=20=E0=B9=83=E0=B8=AB=E0=B9=89=E0=B8=A1=E0=B8=B8?= =?UTF-8?q?=E0=B8=A1=E0=B9=80=E0=B8=81=E0=B8=B2=E0=B8=B0=E0=B8=95=E0=B8=A3?= =?UTF-8?q?=E0=B8=87=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=9A=E0=B8=88=E0=B8=A3?= =?UTF-8?q?=E0=B8=B4=E0=B8=87=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B9=83=E0=B8=8A?= =?UTF-8?q?=E0=B9=88=20estimate=20(8)=20InkLayer=20eraser=20=E0=B8=82?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=20stamp=20=E0=B8=AA=E0=B9=88=E0=B8=87=20meas?= =?UTF-8?q?ureStampText=20=E0=B9=83=E0=B8=AB=E0=B9=89=20bbox=20=E0=B8=95?= =?UTF-8?q?=E0=B8=A3=E0=B8=87=20render=20=E0=B8=88=E0=B8=A3=E0=B8=B4?= =?UTF-8?q?=E0=B8=87=20(9)=20docs=20sync=20=E0=B8=84=E0=B8=A3=E0=B8=9A:=20?= =?UTF-8?q?PLAN-FEAT.md=20D7/F18=20+=20CLAUDE.md=20+=20rules/coordinates.m?= =?UTF-8?q?d=20(patch=20=E0=B9=80=E0=B8=9B=E0=B8=A5=E0=B8=B5=E0=B9=88?= =?UTF-8?q?=E0=B8=A2=E0=B8=99=E0=B8=82=E0=B8=99=E0=B8=B2=E0=B8=94=E0=B8=81?= =?UTF-8?q?=E0=B8=A3=E0=B8=AD=E0=B8=9A=E0=B9=80=E0=B8=AD=E0=B8=B5=E0=B8=A2?= =?UTF-8?q?=E0=B8=87=E0=B8=95=E0=B9=89=E0=B8=AD=E0=B8=87=20clamp=20+=20tol?= =?UTF-8?q?erance=20validator=202%)=20+=20rules/pdf-viewer.md=20=E2=80=94?= =?UTF-8?q?=20=E0=B8=9C=E0=B9=88=E0=B8=B2=E0=B8=99=20code=20review=20(?= =?UTF-8?q?=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B8=A1=E0=B8=B5=20bug=20=E0=B8=A3?= =?UTF-8?q?=E0=B8=B0=E0=B8=94=E0=B8=B1=E0=B8=9A=E0=B8=95=E0=B9=89=E0=B8=AD?= =?UTF-8?q?=E0=B8=87=E0=B9=81=E0=B8=81=E0=B9=89)=20+=20tsc=20+=20vitest=20?= =?UTF-8?q?193/193=20(=E0=B8=84=E0=B8=B3=E0=B8=82=E0=B8=AD=E0=B8=9C?= =?UTF-8?q?=E0=B8=B9=E0=B9=89=E0=B9=83=E0=B8=8A=E0=B9=89=202026-09-28/29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- .claude/rules/coordinates.md | 2 +- .claude/rules/pdf-viewer.md | 4 +- CLAUDE.md | 2 +- app/actions/annotations.ts | 48 +- app/api/annotations/export/route.ts | 80 +++- components/pdf/AnnotationSelectionLayer.tsx | 351 ++++++++++---- components/pdf/InkLayer.tsx | 3 +- components/pdf/PDFViewer.tsx | 485 ++++++++++++++++---- components/pdf/TextboxStampLayer.tsx | 361 +++++++++++---- db/schema.ts | 11 +- docs/PLAN-FEAT.md | 4 +- tests/utils/annotation-display.test.ts | 26 ++ tests/utils/geometry.test.ts | 208 +++++++++ tests/utils/textbox-editor.test.ts | 115 +++++ utils/annotation-display.ts | 63 ++- utils/geometry.ts | 200 +++++++- utils/textbox-editor.ts | 68 +++ 17 files changed, 1699 insertions(+), 332 deletions(-) create mode 100644 tests/utils/textbox-editor.test.ts create mode 100644 utils/textbox-editor.ts diff --git a/.claude/rules/coordinates.md b/.claude/rules/coordinates.md index 9b81497..cddb3d4 100644 --- a/.claude/rules/coordinates.md +++ b/.claude/rules/coordinates.md @@ -13,5 +13,5 @@ paths: - `mergeRectsByLine(rects)`: จัดกลุ่ม rects ที่ y/height ใกล้กัน (บรรทัดเดียวกัน) → union แกน x → คืน 1 rect ต่อบรรทัด — เรียกทุกครั้ง**ก่อนบันทึก HIGHLIGHT** ลง DB - InkLayer: SVG **ห้ามมี `viewBox`** (ความหนาเส้นจะบิดเบี้ยวเมื่อ scale แกน x/y ไม่เท่ากัน) — render ระบบ px จริง แปลง % → px ด้วยขนาดจาก `ResizeObserver` แล้ว re-render เมื่อ resize/zoom - พิกัดจาก event ใช้ `getBoundingClientRect()` ของ page element เป็นฐานเสมอ และ clamp ค่า % ให้อยู่ 0–100 -- `utils/geometry.ts` (PLAN-FEAT.md F1) — pure functions บนพิกัด %: `annotationBBox` / `translateAnnotationData` / `scaleAnnotationData` / `hitTestPath` — ก่อนส่ง data ที่แปลงแล้วเข้า `updateAnnotation` ต้อง **clamp offset/scale ให้ bbox ทั้งกลุ่มอยู่ใน 0–100** เพราะ validator ปฏิเสธค่าติดลบ +- `utils/geometry.ts` (PLAN-FEAT.md F1) — pure functions บนพิกัด %: `annotationBBox` / `translateAnnotationData` / `scaleAnnotationData` / `hitTestPath` — ก่อนส่ง data ที่แปลงแล้วเข้า `updateAnnotation` ต้อง **clamp offset/scale ให้ bbox ทั้งกลุ่มอยู่ใน 0–100** เพราะ validator ปฏิเสธค่าติดลบ — **ยกเว้น STAMP เอียง (มี `rotation`): `position` เป็นมุมกรอบ*ก่อนหมุน* ซึ่งไม่ถูกครอบโดยกรอบจริงบนจอ — เกณฑ์ความถูกต้อง (ทั้ง clamp ฝั่ง client และ validator) ใช้ **AABB ของกรอบหมุน** (`stampRect`/`rotatedRectAABB`) ตัวเดียวกันทั้งสองฝั่ง ไม่ใช่ position ตรง ๆ — ใช้คนละเกณฑ์แล้วลากถึงขอบหน้าแล้ว commit โดนปฏิเสธ เด้งกลับ (code review 2026-09-28) · ตอนตั้งมุมใหม่ (patch/ลากหมุน) และตอน patch ที่เปลี่ยนขนาดกรอบของตรายางเอียง (`stampScale`/`label` เมื่อ rotation ≠ 0) ต้องเรียก `clampStampIntoPage` เลื่อน position กลับเข้าหน้าทันที (code review 2026-09-29) · validator ฝั่ง server ยึด `stampLayoutEstimate` ขณะ client clamp ยึด measure จริงของ browser — สองตัววัดนี้คลาดกันตามฟอนต์ของเครื่อง validator จึงผ่อน tolerance ให้ AABB ติดลบได้ไม่เกิน 2% ของหน้า ไม่ใช่ปฏิเสธทันที (code review 2026-09-29)** - AnnotationSelectionLayer: container `pointer-events: none` เสมอ เฉพาะ handle มุม/ขอบกรอบ/แถบเครื่องมือที่ `auto` — ห้ามให้กรอบ selection บัง textLayer diff --git a/.claude/rules/pdf-viewer.md b/.claude/rules/pdf-viewer.md index a0bcf82..83e7523 100644 --- a/.claude/rules/pdf-viewer.md +++ b/.claude/rules/pdf-viewer.md @@ -21,7 +21,7 @@ paths: - Bookmark scroll: คำนวณ absolute position ครั้งเดียวแล้ว smooth-scroll ครั้งเดียว (ห้าม scroll ซ้อนสองจังหวะ) - Deep link (F12/C4): PDFViewer อ่าน `window.location.search` **ครั้งเดียวหลัง `pageSizes` พร้อม** (guard ด้วย ref — ไม่ใช้ `useSearchParams` เลี่ยงข้อจำกัด prerender ของ Next 14) — `?page=N` (1-based) scroll ไปหน้านั้น, `?ann=` scroll ไปหน้าของ annotation นั้น + `setSelectedId` — placeholder ขนาดจริงทุกหน้าทำให้คำนวณ absolute scroll ได้แม้หน้ายังไม่ render (กลไกเดียวกับ bookmark); ปุ่ม "คัดลอกลิงก์" ใน sidebar สร้าง URL รูป `/annotate/?page=&ann=` - Dialog `fixed z-50` ใน sidebar (ประวัติ) ต้อง render ผ่าน **`createPortal` ไป `document.body`** + guard `typeof document === "undefined"` ตอน SSR — `aside.sticky` สร้าง stacking context ครอบ z-index ของ dialog ไว้ ทำให้ `fixed z-50` แพ้ `textLayer` (z-[2] ระดับ root) จนคลิกปุ่มไม่ได้ (PLAN-FEAT.md §5) -- เมนู dropdown ของ tool ทั้ง 5 (highlight/draw/shape/stamp/textbox) + popup bookmark ก็ render ผ่าน **`createPortal` → `document.body` เป็น `position: fixed`** เหมือนกัน แต่ยึดพิกัดจริงของปุ่ม (`fixedMenuPos` — `getBoundingClientRect()` ตอนคลิก + clamp ให้อยู่ในจอ; popup bookmark คำนวณจาก ref ของปุ่ม 🔖 เพราะเปิดได้จากคีย์ลัด B/ปุ่มในแผง selection ด้วย) — **ห้าม**กลับไปเป็น `absolute` ใน scroll container ของแถบ tool และ**ห้ามสลับ `overflow` ของแถบตอนเปิดเมนู** (สลับแล้ว scroll position ของแถบรีเซ็ต เมนูเปิดนอกจอบนมือถือ และทั้ง app เอียงเลื่อนแนวนอนได้); เลื่อนแถบต่อหรือ **ความกว้างจอเปลี่ยน** (หมุนจอ/split view — เทียบ `menuOpenWRef` ตอนเปิด) = ปิดเมนูทั้งหมด แต่ resize ที่เปลี่ยนแค่ความสูง (URL bar ยุบ/คีย์บอร์ดของมือถือ) ปิดไม่ได้ เพราะเมนู fixed ยึด sticky header ยังอยู่กับที่ (ตำแหน่งคำนวณตอนเปิดเท่านั้น ไม่ตามปุ่มที่เลื่อนไป); outside-click ต้อง**ข้ามโหนดใน `[data-tool-menu]` ก่อน** — ref ของ wrapper มองไม่เห็นโหนดใน portal ไม่ข้ามแล้วคลิกในเมนู = ปิดเมนูเอง +- เมนู dropdown ของ tool ทั้ง 5 (highlight/draw/shape/stamp/textbox) + popup bookmark ก็ render ผ่าน **`createPortal` → `document.body` เป็น `position: fixed`** เหมือนกัน แต่ยึดพิกัดจริงของปุ่ม (`fixedMenuPos` — `getBoundingClientRect()` ตอนคลิก + clamp ให้อยู่ในจอ; popup bookmark คำนวณจาก ref ของปุ่ม 🔖 เพราะเปิดได้จากคีย์ลัด B/ปุ่มในแผง selection ด้วย) — **ห้าม**กลับไปเป็น `absolute` ใน scroll container ของแถบ tool และ**ห้ามสลับ `overflow` ของแถบตอนเปิดเมนู** (สลับแล้ว scroll position ของแถบรีเซ็ต เมนูเปิดนอกจอบนมือถือ และทั้ง app เอียงเลื่อนแนวนอนได้); เลื่อนแถบต่อหรือ **ความกว้างจอเปลี่ยน** (หมุนจอ/split view — เทียบ `menuOpenWRef` ตอนเปิด) = ปิดเมนูทั้งหมด แต่ resize ที่เปลี่ยนแค่ความสูง (URL bar ยุบ/คีย์บอร์ดของมือถือ) ปิดไม่ได้ เพราะเมนู fixed ยึด sticky header ยังอยู่กับที่ (ตำแหน่งคำนวณตอนเปิดเท่านั้น ไม่ตามปุ่มที่เลื่อนไป); outside-click ต้อง**ข้ามโหนดใน `[data-tool-menu]` ก่อน** — ref ของ wrapper มองไม่เห็นโหนดใน portal ไม่ข้ามแล้วคลิกในเมนู = ปิดเมนูเอง; **เมนูตรายางเป็น 2 หน้าใน portal เดียว** (2026-09-28 — preset + แถว "กำหนดเอง…" → หน้า popup `stampCustomMode` reset กลับหน้า preset ทุกครั้งที่เปิดเมนูใหม่): หน้ากำหนดเอง = ช่องข้อความ (พิมพ์ live ไป `stampPreset` เดียวกับ preset, maxLength `STAMP_MAX_LABEL_CHARS`) + สี + ขนาด + **preview ตรายางตามสัดส่วนจริงบนจอ** (ความกว้างหน้าแรก × scale% — ใหญ่เกินเมนูย่อทั้งชุด; กรอบ/ฟอนต์/การจัดวาง/measure ใช้ตัวเดียวกับ render จริง: `stampLayout`/`stampBoxHeight` จาก `utils/geometry.ts` + `STAMP_FONT_STACK`/`measureStampText` จาก `utils/annotation-display.ts` — **คำยาวจัดบรรทัดแบบถอยหลัง: ย่อฟอนต์ให้อยู่บรรทัดเดียวก่อนลงได้จนถึง floor 10% ของความกว้างกรอบ (คำอย่าง "ตรวจสอบแล้ว" ย่อ ~25% ก็พอ = บรรทัดเดียว ไม่ตก 2 บรรทัด — คำขอผู้ใช้ 2026-09-28 รอบสอง) คำยาวเกิน floor ค่อย wrap ≤3 บรรทัด กรอบสูงขึ้นตามจำนวนบรรทัด คำสั้น 1 บรรทัดฟอนต์เต็มสูง 0.4w เหมือนเดิมเป๊ะ — ทุกมิติเป็นสัดส่วนของความกว้างกรอบ จำนวนบรรทัดจึงไม่ขึ้นกับ scale** ปิดท้ายด้วยปุ่ม "ใช้ตรายางนี้" — ปุ่มนี้และ Enter ในช่อง = ปิดเมนูแบบเลือกค่าจบ (setState ตรง ไม่ผ่าน markMenuClosed — แตะหน้าวางได้ทันที ไม่โดน guard กลืน tap), Escape ในช่อง = ย้อนกลับหน้า preset; ปุ่ม tool แสดง label "กำหนดเอง" แทนคำเต็มเมื่อค่าปัจจุบันเป็นคำกำหนดเอง - DRAW วาดเส้นตรง: **ปล่อยเมาส์แล้ว snap อัตโนมัติ** เมื่อจุดทุกจุดห่างจากเส้นตรง จุดเริ่ม→จุดสุดท้าย ไม่เกิน max(5% ของความยาว, 3px) และเส้นยาว ≥ 24px (`isNearlyStraight`) — หรือกดค้าง **Shift** ระหว่างลาก (กด/ปล่อยกลางทางได้ — listener ระดับ window + reset ตอน blur) เพื่อบังคับตรงเสมอ → บันทึกเหลือจุดเริ่ม→จุดสุดท้าย 2 จุด (ความหนา uniform — ไม่เก็บ `widths` จาก pressure) — ใช้ได้เฉพาะโหมดวาดอิสระ ไม่ใช่ shape/eraser/redact; เส้นโค้ง/ขดจริงหลุดเกณฑ์จึงเหลือ freehand - Tool `pan` (มือเลื่อนหน้า — ปุ่มแยกถัดจาก select ในแถบ tool): ลากบน `
` = `mainRef.current.scrollBy` สวนทางการลาก (ทั้งแกน x/y) + 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` บน desktop / custom selection ของ `useCustomSelection` บนจอสัมผัส + ซ่อน native selection) — tool "เลือก" คลุมแล้วเกิด popup ไม่ได้แล้ว (ถอดออก) และคีย์ลัด `H` สลับไป tool highlight @@ -29,7 +29,7 @@ paths: - **tap/คลิกที่ใช้ "ปิดเมนู" ต้องไม่สร้างรายการ**: outside-click ปิดเมนูที่ `mousedown` ก่อน `click` ยิงถึง page wrapper ทำให้ tool comment/textbox/stamp วางรายการจาก tap ปิดเมนูโดยไม่ตั้งใจ — `handlePageClick` เช็ค `menuClosedAtRef` (เขียนทุก path ปิดเมนูผ่าน `markMenuClosed`: outside-click/Escape/switchTool/เลื่อนแถบ/resize; ยกเว้นคีย์ B ปิดเมนูเพื่อเปิด popup bookmark — คีย์บอร์ดไม่มี click หลงเหลือให้กลืน และยกเว้นการเลือกแถวเดี่ยวในเมนู (รูปแบบมาร์ก/ชนิดปากกา/ชนิดรูปทรง/preset ตรายาง — UX audit 2026-09-22) ที่ปิดเมนูด้วย setState ตรง ๆ ไม่ผ่าน markMenuClosed: tap นั้นอยู่บนเมนูเองไม่มี click หลงเหลือถึงหน้า และผู้ใช้เลือกค่าแล้วตั้งใจแตะหน้าวางต่อทันที — ส่วนแถวปรับค่า สี/ความหนา/ความทึบ/ขนาด ยังคงเมนูเปิดให้ปรับต่อ) และเฉย 300ms แรก คลิกที่สองค่อยวาง (มาตรฐาน dropdown ทั่วไป ใช้ทั้ง desktop และ touch) - **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 เดิม; popover ของ COMMENT เปิดอยู่ (`openPinId === selectedId`) = **ไม่ render กรอบ selection/แผงเครื่องมือของตัวนั้น** — bbox ของ COMMENT ครอบพื้นที่ pin พอดี แผงจะทับ popover และแถบ handle บังการคลิก (sidebar ยังตาม `selectedId` ไฮไลต์เหมือนเดิม) - **คลุมข้อความได้ 5 tool** (`TEXT_SELECTION_TOOLS` set ใน PDFViewer: select/highlight/comment/textbox/stamp — ที่เหลือ textLayer ปิด pointer-events คลุมไม่ได้): ทั้งหมด inject `