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, }; } // 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], ux: number, uy: number, scale: number, lengthIn: number, ): Eighths[] { 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 * 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 & { 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, }; }