Files
SlabUploader/frontend/src/measure/measure.ts
T
grokbot_ty eaedcb53dd fix(capture): drop first and last 6in from width samples
sampleWidths used floor(L/6)+1 stations from t=0, so tapered tips
dominated min/max. Sample only interior stations at 6, 12, … ≤ L−6.
When L≤12 there is no interior span: every6In stays empty and
min/max/avg stay 0. CAPTURE.md Widths bullet matches.

sqft, bdft, scale, and inventory PNG are unchanged.
2026-09-15 03:20:28 +00:00

105 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { BBox, Eighths, Measures, Widths } from '../capture/capture.types';
import { maskSpanAlong } from '../geometry/rect';
// 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] };
}
// 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,
};
}
// Interior stations only (6, 12, … ≤ L−6). Tips dominate min/max on tapered
// boards, so the first and last 6 in are excluded. Each sample is a line
// probe perpendicular to the axis; width = max run of slab px across it.
function sampleWidths(
bitset: Uint8Array, w: number, h: number,
start: [number, number], ux: number, uy: number,
scale: number, lengthIn: number,
): Eighths[] {
const px = -uy, py = ux;
const stepPx = 6 * scale;
const lastPx = lengthIn > 12 ? Math.floor((lengthIn - 6) / 6) * 6 * scale : 0;
const out: Eighths[] = [];
const half = Math.max(2, Math.round((w + h) / 4));
for (let t = stepPx; t <= lastPx; t += 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);
const y = Math.round(oy + py * p);
if (x < 0 || y < 0 || x >= w || y >= h) continue;
if (bitset[y * w + x]) {
if (p < minP) minP = p;
if (p > maxP) maxP = p;
}
}
if (maxP < minP) continue;
out.push(Math.max(0, Math.round(((maxP - minP + 1) / scale) * 8)));
}
return out;
}
export function measure(
bitset: Uint8Array, w: number, h: number,
bbox: BBox,
lengthEighths: Eighths, thicknessEighths: Eighths,
slabPx: number,
): Omit<Measures, 'inventoryPng'> & { sourceBelowFloor: false } {
const lengthIn = lengthEighths / 8;
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);
const max = Math.max(...every6In);
const avg = Math.round(every6In.reduce((a, b) => a + b, 0) / every6In.length);
widths = { min, max, avg, every6In };
} else {
widths = { min: 0, max: 0, avg: 0, every6In: [] };
}
const sqft = Math.round((slabPx / (scale * scale) / 144) * 100) / 100;
const bdft = Math.round(sqft * (thicknessEighths / 8) * 100) / 100;
return {
lengthEighths,
thicknessEighths,
scale,
widths,
sqft,
bdft,
sourceBelowFloor: false,
};
}