b124450d0c
- measure.js: sampleWidthsPx scanline sampler - inventory.js: prepareInventoryMetadata - +page.svelte: full 4-state capture flow (Add photo, Mask, Axis, Measure) - Canvas overlay: checkerboard mask, bounding rect, axis line - Inventory PNG: 3:4 portrait, slab centered 80% fill, alpha - 77/77 unit tests green
75 lines
3.2 KiB
JavaScript
75 lines
3.2 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { sampleWidthsPx } from '../../src/lib/core/measure.js';
|
|
|
|
// M1-5 Measure — pure geometry tests.
|
|
//
|
|
// Ground truth: docs/CAPTURE.md §Measure
|
|
// - Widths: min, max, avg, sampled every 6 in along the approved length
|
|
// axis, perpendicular to it
|
|
// - scale = longest_axis_px / L
|
|
//
|
|
// sampleWidthsPx runs the real scanline mask sampling that the UI uses on
|
|
// the approved mask, so these tests pin the exact pixel widths the e2e
|
|
// fixture math is computed from.
|
|
|
|
/**
|
|
* Build a binary mask directly (no keySheet dependency): 1 inside the slab
|
|
* rectangle, 0 elsewhere. This keeps the test self-contained and focused on
|
|
* sampleWidthsPx geometry, which is what the test suite is for.
|
|
*/
|
|
function buildMask(width, height, slabW, slabH, ox, oy) {
|
|
const mask = new Uint8Array(width * height);
|
|
for (let y = 0; y < height; y++)
|
|
for (let x = 0; x < width; x++)
|
|
mask[y * width + x] =
|
|
x >= ox && x < ox + slabW && y >= oy && y < oy + slabH ? 1 : 0;
|
|
return { mask, width, height };
|
|
}
|
|
|
|
describe('sampleWidthsPx (M1-5 Measure geometry)', () => {
|
|
it('samples perpendicular widths every 6in along a horizontal axis', () => {
|
|
// 16 px/in. Photo 400x300. Slab 288x96 px (18in x 6in) at (0,0).
|
|
// Axis along x at slab center y=48, from x=0, length 288 px.
|
|
// stepPx = 6*16 = 96. Samples at t=0,96,192,288.
|
|
// t=288 lands on the slab boundary (x=288, slab is x<288) -> width 0.
|
|
const { mask, width, height } = buildMask(400, 300, 288, 96, 0, 0);
|
|
const axis = { origin: { x: 0, y: 48 }, dir: { x: 1, y: 0 }, lengthPx: 288 };
|
|
const widths = sampleWidthsPx(mask, width, height, axis, 16);
|
|
expect(widths.length).toBe(4);
|
|
expect(widths[0]).toBeCloseTo(96, 0);
|
|
expect(widths[1]).toBeCloseTo(96, 0);
|
|
expect(widths[2]).toBeCloseTo(96, 0);
|
|
expect(widths[3]).toBe(0);
|
|
});
|
|
|
|
it('returns widths perpendicular to a vertical axis', () => {
|
|
// Slab 96x288 px (6in x 18in) at (0,0). Axis along y at x=48, length 288.
|
|
const { mask, width, height } = buildMask(400, 300, 96, 288, 0, 0);
|
|
const axis = { origin: { x: 48, y: 0 }, dir: { x: 0, y: 1 }, lengthPx: 288 };
|
|
const widths = sampleWidthsPx(mask, width, height, axis, 16);
|
|
expect(widths.length).toBe(4);
|
|
expect(widths[0]).toBeCloseTo(96, 0);
|
|
expect(widths[1]).toBeCloseTo(96, 0);
|
|
expect(widths[2]).toBeCloseTo(96, 0);
|
|
expect(widths[3]).toBe(0);
|
|
});
|
|
|
|
it('samples only up to the axis length, not beyond', () => {
|
|
// Slab 80 px (5in) wide: 2nd sample at 96 px > lengthPx=80.
|
|
const { mask, width, height } = buildMask(200, 150, 80, 80, 0, 0);
|
|
const axis = { origin: { x: 0, y: 0 }, dir: { x: 1, y: 0 }, lengthPx: 80 };
|
|
const widths = sampleWidthsPx(mask, width, height, axis, 16);
|
|
expect(widths.length).toBe(1);
|
|
});
|
|
|
|
it('reports the real mask extent, not the photo extent', () => {
|
|
// Slab 80 px tall at y=35: the perpendicular sample must be 80,
|
|
// never the 150 px photo height.
|
|
const { mask, width, height } = buildMask(200, 150, 80, 80, 0, 35);
|
|
const axis = { origin: { x: 0, y: 35 }, dir: { x: 1, y: 0 }, lengthPx: 80 };
|
|
const widths = sampleWidthsPx(mask, width, height, axis, 16);
|
|
expect(widths.length).toBe(1);
|
|
expect(widths[0]).toBeCloseTo(80, 0);
|
|
});
|
|
});
|