M1-5: Measure + PNG + full capture UI (TDD)
- 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
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user