83 lines
3.3 KiB
TypeScript
83 lines
3.3 KiB
TypeScript
|
|
import type { Point, Rect } from "@/db/schema";
|
|||
|
|
|
|||
|
|
// พิกัดทุกอย่างที่เก็บลง DB เป็น % ของขนาดหน้า PDF (PLAN.md §2, CLAUDE.md invariant #2)
|
|||
|
|
// ทิศทางแปลง: px → % = ÷size×100, % → px = ×size÷100 — ผ่านไฟล์นี้เท่านั้น
|
|||
|
|
// Point/Rect define ที่ db/schema.ts (PLAN-FEAT.md §1) แล้ว re-export ที่นี่
|
|||
|
|
|
|||
|
|
export type { Rect, Point };
|
|||
|
|
|
|||
|
|
export type Size = { width: number; height: number };
|
|||
|
|
|
|||
|
|
export const toPercent = (px: number, size: number): number => (px / size) * 100;
|
|||
|
|
|
|||
|
|
export const toPx = (percent: number, size: number): number => (percent / 100) * size;
|
|||
|
|
|
|||
|
|
export const clamp = (value: number, min: number, max: number): number =>
|
|||
|
|
Math.min(Math.max(value, min), max);
|
|||
|
|
|
|||
|
|
export const pointToPercent = (p: Point, size: Size): Point => ({
|
|||
|
|
x: toPercent(p.x, size.width),
|
|||
|
|
y: toPercent(p.y, size.height),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export const pointToPx = (p: Point, size: Size): Point => ({
|
|||
|
|
x: toPx(p.x, size.width),
|
|||
|
|
y: toPx(p.y, size.height),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export const rectToPercent = (r: Rect, size: Size): Rect => ({
|
|||
|
|
x: toPercent(r.x, size.width),
|
|||
|
|
y: toPercent(r.y, size.height),
|
|||
|
|
width: toPercent(r.width, size.width),
|
|||
|
|
height: toPercent(r.height, size.height),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export const rectToPx = (r: Rect, size: Size): Rect => ({
|
|||
|
|
x: toPx(r.x, size.width),
|
|||
|
|
y: toPx(r.y, size.height),
|
|||
|
|
width: toPx(r.width, size.width),
|
|||
|
|
height: toPx(r.height, size.height),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export const pathToPercent = (path: Point[], size: Size): Point[] =>
|
|||
|
|
path.map((p) => pointToPercent(p, size));
|
|||
|
|
|
|||
|
|
export const pathToPx = (path: Point[], size: Size): Point[] =>
|
|||
|
|
path.map((p) => pointToPx(p, size));
|
|||
|
|
|
|||
|
|
/** พิกัด pointer จาก event เทียบ page element → % (clamp 0–100 เสมอ) */
|
|||
|
|
export function getPointerPercent(
|
|||
|
|
e: { clientX: number; clientY: number },
|
|||
|
|
pageEl: HTMLElement,
|
|||
|
|
): Point {
|
|||
|
|
const rect = pageEl.getBoundingClientRect();
|
|||
|
|
return {
|
|||
|
|
x: clamp(toPercent(e.clientX - rect.left, rect.width), 0, 100),
|
|||
|
|
y: clamp(toPercent(e.clientY - rect.top, rect.height), 0, 100),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* รวบ rects ที่อยู่บรรทัดเดียวกัน (แนวตั้ง overlap กัน = y/height ใกล้กัน)
|
|||
|
|
* เหลือ 1 rect ต่อบรรทัด โดย union แกน x ให้ครอบข้อความเดิมทั้งบรรทัด
|
|||
|
|
* เรียกทุกครั้งก่อนบันทึก HIGHLIGHT ลง DB กัน JSONB บวม
|
|||
|
|
*/
|
|||
|
|
export function mergeRectsByLine(rects: Rect[]): Rect[] {
|
|||
|
|
const sorted = [...rects].sort((a, b) => a.y - b.y || a.x - b.x);
|
|||
|
|
const merged: Rect[] = [];
|
|||
|
|
for (const r of sorted) {
|
|||
|
|
const last = merged[merged.length - 1];
|
|||
|
|
if (last && r.y < last.y + last.height) {
|
|||
|
|
// บรรทัดเดียวกัน → ขยายกรอบเดิมให้ครอบ rect นี้ด้วย
|
|||
|
|
const right = Math.max(last.x + last.width, r.x + r.width);
|
|||
|
|
last.x = Math.min(last.x, r.x);
|
|||
|
|
last.width = right - last.x;
|
|||
|
|
const bottom = Math.max(last.y + last.height, r.y + r.height);
|
|||
|
|
last.height = bottom - last.y;
|
|||
|
|
} else {
|
|||
|
|
merged.push({ ...r });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return merged;
|
|||
|
|
}
|