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); }); });