7e2a1845cf
- Add maskState.js: sheet Auto/Green/Black, sensitivity, edge offset, feather - Overlay = slab photo + checkerboard on removed px - Debounced recompute 2.0s after last sensitivity change - Approve blocked when maskStats empty - HEIC shows retake notice - Refactored mask.test.js + extended e2e mask.spec.js - 43/43 unit tests green
90 lines
3.2 KiB
JavaScript
90 lines
3.2 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import * as m from '../../src/lib/core/math.js';
|
|
|
|
// M1 deterministic core — unit tests (RED first, per TDD).
|
|
// Ground truth for the math identities is the spec (docs/CAPTURE.md):
|
|
// scale = longest_axis_px / L
|
|
// sqft = (mask_px / scale^2) / 144
|
|
// bdft = sqft * thickness_in
|
|
// Widths are sampled every `everyInches` along the approved axis.
|
|
|
|
describe('scale', () => {
|
|
it('scale = longest_axis_px / L', () => {
|
|
expect(m.scale(3360, 84)).toBe(40);
|
|
});
|
|
it('scale for fixture-b geometry (21 in)', () => {
|
|
// 21 in represented as 3360 px -> 160 px/in
|
|
expect(m.scale(3360, 21)).toBeCloseTo(160, 6);
|
|
});
|
|
});
|
|
|
|
describe('sqft', () => {
|
|
it('sqft = (mask_px / scale^2) / 144, 2 decimals', () => {
|
|
// scale=40 -> scale^2=1600; 1,000,000/1600=625; /144=4.3403 -> 4.34
|
|
expect(m.sqft(1_000_000, 40)).toBe(4.34);
|
|
});
|
|
it('zero-area mask -> 0', () => {
|
|
expect(m.sqft(0, 40)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('bdft', () => {
|
|
it('bdft = sqft * thickness_in, 2 decimals (identity 1 sqft @ 1in = 1 bdft)', () => {
|
|
expect(m.bdft(4.34, 1)).toBeCloseTo(4.34, 6);
|
|
expect(m.bdft(4.34, 2.5)).toBe(10.85);
|
|
});
|
|
});
|
|
|
|
describe('e2e fixture math (cand_b: 21in x 1.75in)', () => {
|
|
// If the slab were a clean rectangle exactly 21in long, at 160px/in that is
|
|
// 3360 px long. Pick a width of 12in -> 1920px -> area 6,451,200 px.
|
|
const Lpx = 3360, L = 21, scale = 160, thickness = 1.75;
|
|
const widthPx = 1920; // 12in
|
|
const maskPx = Lpx * widthPx; // 6,451,200
|
|
it('sqft of a 21in x 12in slab = 1.75', () => {
|
|
expect(m.sqft(maskPx, scale)).toBe(1.75);
|
|
});
|
|
it('bdft of that slab at 1.75in = 3.06', () => {
|
|
const sqft = m.sqft(maskPx, scale);
|
|
expect(m.bdft(sqft, thickness)).toBe(3.06); // 1.75*1.75=3.0625 -> 3.06 (half-up)
|
|
});
|
|
});
|
|
|
|
describe('width sampling', () => {
|
|
it('samples perpendicular widths at 0,6,12,... in along the axis', () => {
|
|
// pxPerIn=160. axisLengthPx = 3360 (21in). widthsAt samples every 6in.
|
|
// Provide a constant width of 1920px (12in) at every sample point.
|
|
const pxPerIn = 160;
|
|
const axisLengthPx = 3360;
|
|
const constantWidthPx = 1920;
|
|
const out = m.sampledWidths(constantWidthPx, pxPerIn, axisLengthPx, 6);
|
|
// 21in / 6 = 3.5 -> sample points at 0,6,12,18 (and not 24>21). 4 samples.
|
|
expect(out.length).toBe(4);
|
|
for (const w of out) expect(w).toBe(12); // 1920px/160 = 12in
|
|
});
|
|
it('min/max/avg of sampled widths', () => {
|
|
const pxPerIn = 160;
|
|
const widths = m.sampledWidthsFrom(
|
|
// widths at px positions 0,960,1920,2880 (=0,6,12,18 in)
|
|
[1600, 1920, 2080, 1600],
|
|
pxPerIn
|
|
);
|
|
expect(widths.min).toBeCloseTo(10, 6);
|
|
expect(widths.max).toBeCloseTo(13, 6);
|
|
expect(widths.avg).toBeCloseTo(11.25, 6); // (10+12+13+10)/4 = 11.25
|
|
});
|
|
});
|
|
|
|
describe('1/8 inch stepping + rounding', () => {
|
|
it('snaps a length to the nearest 1/8 inch', () => {
|
|
expect(m.snapToEighth(10.1)).toBeCloseTo(10.125, 6); // 10 1/8
|
|
expect(m.snapToEighth(10.2)).toBeCloseTo(10.25, 6); // 10 2/8=1/4
|
|
expect(m.snapToEighth(10.0)).toBe(10);
|
|
});
|
|
it('rounds half-up to 2 decimals', () => {
|
|
expect(m.roundHalfUp(30.625, 2)).toBe(30.63);
|
|
expect(m.roundHalfUp(4.345, 2)).toBe(4.35);
|
|
expect(m.roundHalfUp(1.005, 2)).toBe(1.01);
|
|
});
|
|
});
|