fix(capture): rotate length-axis rectangle and scale from mask span

Length axis keeps the approved mask, replaces Flip 180 with a ± angle
slider, and sets scale from wood span along the approved direction rather
than bbox.longSidePx. Widths still sample every 6 in perpendicular to that
axis.
This commit is contained in:
2026-09-14 16:37:00 +00:00
parent b781ffeb66
commit b328871549
9 changed files with 201 additions and 45 deletions
+49 -20
View File
@@ -1,31 +1,60 @@
import type { BBox, Eighths, Measures, Widths } from '../capture/capture.types';
import { maskSpanAlong } from '../geometry/rect';
// Long-side start point per heading: heading 1 starts at corners[1] so that
// the axis reads corners[1] -> corners[0] (see rect.ts corner order).
export function axisEndpoints(bbox: BBox, heading: 1 | -1): { start: [number, number]; end: [number, number] } {
return heading === 1 ? { start: bbox.corners[1], end: bbox.corners[0] } : { start: bbox.corners[0], end: bbox.corners[1] };
// Display / direction: corners[3] → corners[0] is the long side.
export function axisEndpoints(bbox: BBox): { start: [number, number]; end: [number, number] } {
return { start: bbox.corners[3], end: bbox.corners[0] };
}
// Sampled every 6 in along the axis; each sample is a line probe perpendicular
// to the axis, width = max run of slab px across the probe.
// Wood end→end along the approved direction: project the mask onto the axis
// unit vector and return the segment on the bbox center line covering that span.
function maskAxisSegment(
bitset: Uint8Array, w: number, h: number, bbox: BBox,
): { start: [number, number]; end: [number, number]; span: number; ux: number; uy: number } {
const { start: a, end: b } = axisEndpoints(bbox);
const ex = b[0] - a[0];
const ey = b[1] - a[1];
const elen = Math.hypot(ex, ey) || 1;
const ux = ex / elen, uy = ey / elen;
const cx = (bbox.corners[0][0] + bbox.corners[1][0] + bbox.corners[2][0] + bbox.corners[3][0]) / 4;
const cy = (bbox.corners[0][1] + bbox.corners[1][1] + bbox.corners[2][1] + bbox.corners[3][1]) / 4;
const cProj = cx * ux + cy * uy;
let minP = Infinity, maxP = -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 < minP) minP = p;
if (p > maxP) maxP = p;
}
}
if (maxP < minP) return { start: a, end: b, span: 0, ux, uy };
return {
start: [cx + ux * (minP - cProj), cy + uy * (minP - cProj)],
end: [cx + ux * (maxP - cProj), cy + uy * (maxP - cProj)],
span: maxP - minP,
ux,
uy,
};
}
// Sampled every 6 in along the wood axis; each sample is a line probe
// perpendicular to the axis, width = max run of slab px across the probe.
function sampleWidths(
bitset: Uint8Array, w: number, h: number,
start: [number, number], end: [number, number],
start: [number, number], ux: number, uy: number,
scale: number, lengthIn: number,
): Eighths[] {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
const len = Math.hypot(dx, dy) || 1;
const ax = dx / len, ay = dy / len;
const px = -ay, py = ax; // perpendicular
const px = -uy, py = ux;
const stepPx = 6 * scale;
const count = Math.max(1, Math.floor(lengthIn / 6) + 1);
const out: Eighths[] = [];
const half = Math.max(2, Math.round((w + h) / 4));
for (let i = 0; i < count; i++) {
const t = (i / (count - 1 || 1)) * len;
const ox = start[0] + ax * t;
const oy = start[1] + ay * t;
const t = i * stepPx;
const ox = start[0] + ux * t;
const oy = start[1] + uy * t;
let minP = Infinity, maxP = -Infinity;
for (let p = -half; p <= half; p++) {
const x = Math.round(ox + px * p);
@@ -36,7 +65,7 @@ function sampleWidths(
if (p > maxP) maxP = p;
}
}
if (maxP < minP) continue; // no slab px on this probe
if (maxP < minP) continue;
out.push(Math.max(0, Math.round(((maxP - minP + 1) / scale) * 8)));
}
return out;
@@ -44,14 +73,14 @@ function sampleWidths(
export function measure(
bitset: Uint8Array, w: number, h: number,
bbox: BBox, heading: 1 | -1,
bbox: BBox,
lengthEighths: Eighths, thicknessEighths: Eighths,
slabPx: number,
): Omit<Measures, 'inventoryPng'> & { sourceBelowFloor: false } {
const lengthIn = lengthEighths / 8;
const scale = bbox.longSidePx / lengthIn;
const { start, end } = axisEndpoints(bbox, heading);
const every6In = sampleWidths(bitset, w, h, start, end, scale, lengthIn);
const seg = maskAxisSegment(bitset, w, h, bbox);
const scale = (seg.span || maskSpanAlong(bitset, w, h, seg.ux, seg.uy)) / lengthIn;
const every6In = sampleWidths(bitset, w, h, seg.start, seg.ux, seg.uy, scale, lengthIn);
let widths: Widths;
if (every6In.length > 0) {
const min = Math.min(...every6In);