52240083b3
- One crop_fill (default 0.9) replaces duplicated FILL in inventory/overlay - Thin localStorage settings store; crop % only moves long-axis end insets - Pure tapeGeometry: fixed stage line, stage↔image map, tick span → px - measure() prefers tape tick span for scale; maskSpanAlong demoted - rotateCalibration bakes cal tilt into inventory + overlay only - Fix stale 80%-fill comment on slabBounds
133 lines
5.3 KiB
TypeScript
133 lines
5.3 KiB
TypeScript
import type { ABox, BBox } from '../capture/capture.types';
|
||
|
||
export function slabBounds(bitset: Uint8Array, w: number, h: number): ABox {
|
||
let minX = w, minY = h, maxX = -1, maxY = -1;
|
||
for (let y = 0; y < h; y++) {
|
||
const row = y * w;
|
||
for (let x = 0; x < w; x++) {
|
||
if (bitset[row + x]) {
|
||
if (x < minX) minX = x;
|
||
if (x > maxX) maxX = x;
|
||
if (y < minY) minY = y;
|
||
if (y > maxY) maxY = y;
|
||
}
|
||
}
|
||
}
|
||
if (maxX < 0) return { x: 0, y: 0, w: 0, h: 0 };
|
||
return { x: minX, y: minY, w: maxX - minX + 1, h: maxY - minY + 1 };
|
||
}
|
||
|
||
// Convex hull (Andrew's monotone chain). Points are [x, y].
|
||
export function convexHull(pts: [number, number][]): [number, number][] {
|
||
const p = [...pts].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
||
if (p.length < 3) return p;
|
||
const cross = (o: [number, number], a: [number, number], b: [number, number]) =>
|
||
(a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
||
const lower: [number, number][] = [];
|
||
for (const pt of p) {
|
||
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], pt) <= 0) lower.pop();
|
||
lower.push(pt);
|
||
}
|
||
const upper: [number, number][] = [];
|
||
for (let i = p.length - 1; i >= 0; i--) {
|
||
const pt = p[i];
|
||
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], pt) <= 0) upper.pop();
|
||
upper.push(pt);
|
||
}
|
||
lower.pop();
|
||
upper.pop();
|
||
return lower.concat(upper);
|
||
}
|
||
|
||
// Rotating calipers: true minimum-area bounding rectangle of the mask.
|
||
export function minAreaRect(bitset: Uint8Array, w: number, h: number): BBox | null {
|
||
const pts: [number, number][] = [];
|
||
for (let y = 0; y < h; y++) {
|
||
const row = y * w;
|
||
for (let x = 0; x < w; x++) if (bitset[row + x]) pts.push([x, y]);
|
||
}
|
||
if (pts.length < 2) return null;
|
||
const hull = convexHull(pts);
|
||
|
||
let best = { area: Infinity, cx: 0, cy: 0, ux: 1, uy: 0, hw: 0, hh: 0 };
|
||
const n = hull.length;
|
||
for (let i = 0; i < n; i++) {
|
||
const j = (i + 1) % n;
|
||
const ex = hull[j][0] - hull[i][0];
|
||
const ey = hull[j][1] - hull[i][1];
|
||
const len = Math.hypot(ex, ey);
|
||
if (len === 0) continue;
|
||
const ux = ex / len, uy = ey / len;
|
||
const vx = -uy, vy = ux;
|
||
let minU = Infinity, maxU = -Infinity, minV = Infinity, maxV = -Infinity;
|
||
for (let k = 0; k < n; k++) {
|
||
const dx = hull[k][0] - hull[i][0];
|
||
const dy = hull[k][1] - hull[i][1];
|
||
const u = dx * ux + dy * uy;
|
||
const v = dx * vx + dy * vy;
|
||
if (u < minU) minU = u;
|
||
if (u > maxU) maxU = u;
|
||
if (v < minV) minV = v;
|
||
if (v > maxV) maxV = v;
|
||
}
|
||
const rw = maxU - minU;
|
||
const rh = maxV - minV;
|
||
const area = rw * rh;
|
||
if (area < best.area) {
|
||
best = { area, cx: hull[i][0] + (minU + maxU) / 2 * ux + (minV + maxV) / 2 * vx, cy: hull[i][1] + (minU + maxU) / 2 * uy + (minV + maxV) / 2 * vy, ux, uy, hw: rw / 2, hh: rh / 2 };
|
||
}
|
||
}
|
||
|
||
// long-side direction: the edge with the larger extent
|
||
const lu = best.hw >= best.hh ? best.ux : -best.uy;
|
||
const lv = best.hw >= best.hh ? best.uy : best.ux;
|
||
const su = best.hw >= best.hh ? -best.uy : best.ux;
|
||
const sv = best.hw >= best.hh ? -best.ux : -best.uy;
|
||
const halfL = best.hw >= best.hh ? best.hw : best.hh;
|
||
const halfS = best.hw >= best.hh ? best.hh : best.hw;
|
||
const corners: [number, number][] = [];
|
||
for (const [a, b] of [[1, 1], [1, -1], [-1, -1], [-1, 1]] as const) {
|
||
corners.push([best.cx + a * halfL * lu + b * halfS * su, best.cy + a * halfL * lv + b * halfS * sv]);
|
||
}
|
||
const angleDeg = ((Math.atan2(lv, lu) * 180) / Math.PI + 180) % 180;
|
||
return { corners, longSidePx: halfL * 2, angleDeg };
|
||
}
|
||
|
||
// Rotate the long side to a new angle, keeping center and half-extents.
|
||
// Corners are rebuilt in minAreaRect order so corners[3] → corners[0] stays the
|
||
// long side (the axis arrow direction).
|
||
export function orientedRect(prev: BBox, angleDeg: number): BBox {
|
||
const a = ((angleDeg % 180) + 180) % 180;
|
||
const halfL = prev.longSidePx / 2;
|
||
const halfS = Math.hypot(prev.corners[0][0] - prev.corners[1][0], prev.corners[0][1] - prev.corners[1][1]) / 2;
|
||
// corners[i] is a corner point: average of all four = center
|
||
const cx = (prev.corners[0][0] + prev.corners[1][0] + prev.corners[2][0] + prev.corners[3][0]) / 4;
|
||
const cy = (prev.corners[0][1] + prev.corners[1][1] + prev.corners[2][1] + prev.corners[3][1]) / 4;
|
||
const r = (a * Math.PI) / 180;
|
||
const lu = Math.cos(r), lv = Math.sin(r);
|
||
const su = -lv, sv = lu;
|
||
const corners: [number, number][] = [];
|
||
for (const [aSign, bSign] of [[1, 1], [1, -1], [-1, -1], [-1, 1]] as const) {
|
||
corners.push([cx + aSign * halfL * lu + bSign * halfS * su, cy + aSign * halfL * lv + bSign * halfS * sv]);
|
||
}
|
||
return { corners, longSidePx: prev.longSidePx, angleDeg: a };
|
||
}
|
||
|
||
// Project every slab pixel onto the unit vector (ux, uy); return max − min
|
||
// extent in px. 0 when the mask is empty.
|
||
// #99: demoted to an optional sanity check — declared L comes from tape tick
|
||
// distance in image space, not this projected guess.
|
||
export function maskSpanAlong(bitset: Uint8Array, w: number, h: number, ux: number, uy: number): number {
|
||
let min = Infinity, max = -Infinity;
|
||
for (let y = 0; y < h; y++) {
|
||
const row = y * w;
|
||
for (let x = 0; x < w; x++) {
|
||
if (!bitset[row + x]) continue;
|
||
const p = x * ux + y * uy;
|
||
if (p < min) min = p;
|
||
if (p > max) max = p;
|
||
}
|
||
}
|
||
return max < min ? 0 : max - min;
|
||
}
|