93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
// 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 };
|
||
|
|
}
|