Files
PacketStation/frontend/src/lib/geometry.ts
T

93 lines
3.4 KiB
TypeScript
Raw Normal View History

// Mark positions on a drawing page. Every position is a fraction of the page (0..1), so a mark stays on its
// joint whatever the zoom or the bitmap size. A port of the old build's clamp01, bpos, arrowRot and toNorm.
/** The viewer transform: the stage is translated by (x, y) and scaled by z. w and h are the page size at z = 1. */
export interface View {
x: number;
y: number;
z: number;
w: number;
h: number;
}
/** A point on the page, as fractions. */
export interface Frac {
fx: number;
fy: number;
}
/** Marks keep a small margin from the page edge. */
export function clamp01(v: number): number {
return Math.max(0.01, Math.min(0.99, v));
}
/** Plain 0..1 clamp, for drops and table corners. */
export function clamp(v: number, lo = 0, hi = 1): number {
return Math.max(lo, Math.min(hi, v));
}
export interface Placed {
fx: number | null;
fy: number | null;
bfx?: number | null | undefined;
bfy?: number | null | undefined;
}
/**
* Where the label (bubble) sits. A stored bfx, bfy wins. Otherwise the old default: a little right of the arrow
* point (left near the right edge) and a little above it (below near the top edge).
*/
export function bpos(m: Placed): { bfx: number; bfy: number } {
const fx = m.fx ?? 0.5;
const fy = m.fy ?? 0.5;
return {
bfx: m.bfx ?? clamp01(fx + (fx > 0.85 ? -0.05 : 0.05)),
bfy: m.bfy ?? clamp01(fy + (fy < 0.12 ? 0.07 : -0.07)),
};
}
/** A client point to page fractions. `frame` is the viewer's bounding box. Not clamped. */
export function toFraction(clientX: number, clientY: number, frame: { left: number; top: number }, view: View): Frac {
if (!view.w || !view.h || !view.z) return { fx: NaN, fy: NaN };
return {
fx: (clientX - frame.left - view.x) / view.z / view.w,
fy: (clientY - frame.top - view.y) / view.z / view.h,
};
}
/** Page fractions to pixels inside the viewer frame. The inverse of toFraction. */
export function toPixels(fx: number, fy: number, view: View): { px: number; py: number } {
return { px: view.x + fx * view.w * view.z, py: view.y + fy * view.h * view.z };
}
/** True when the fraction lies on the page. */
export function onPage(f: Frac): boolean {
return f.fx >= 0 && f.fx <= 1 && f.fy >= 0 && f.fy <= 1;
}
/**
* The arrowhead's rotation in degrees: it points from the bubble (bx, by) to the anchor (ax, ay).
* The fractions are scaled by the page aspect (width / height) so the angle is right on a non-square page.
*/
export function arrowRotation(ax: number, ay: number, bx: number, by: number, aspect: number): number {
return (Math.atan2(ay - by, (ax - bx) * (aspect || 1.5)) * 180) / Math.PI;
}
/** The weld circle radius in page pixels for the printed raster. */
export function markRadius(width: number): number {
return Math.max(18, width / 150);
}
/** The fit transform: the whole page in the frame, centred. */
export function fitView(frameW: number, frameH: number, w: number, h: number): View {
const z = Math.min(frameW / w, frameH / h) || 1;
return { x: (frameW - w * z) / 2, y: (frameH - h * z) / 2, z, w, h };
}
/** Zoom by factor f around the frame point (cx, cy), clamped between minZ and maxZ. */
export function zoomAt(view: View, f: number, cx: number, cy: number, minZ: number, maxZ = 6): View {
const nz = Math.max(minZ, Math.min(maxZ, view.z * f));
const k = nz / view.z;
return { ...view, x: cx - (cx - view.x) * k, y: cy - (cy - view.y) * k, z: nz };
}