import { describe, it, expect } from 'vitest'; import { keySheet, maskStats, } from '../../src/lib/core/mask.js'; // M1-3 mask key geometry — unit tests (RED first, per strict TDD). // // Ground truth: docs/CAPTURE.md §Mask // - 4 knobs: sheet Auto/Green/Black, sensitivity 0–100, edge offset, feather // - Control value updates IMMEDIATELY; overlay recomputes 2.0 s after the // last sensitivity change (debounce). // - Approve blocked when maskStats is empty. // // All functions here are pure (no DOM) so the state is unit-testable in Node; // the live UI (M1-5) drives this machine. // // --- synthetic photo helpers (no DOM) -----------------------------------------\n// function solid(w, h, r, g, b) { const data = new Uint8ClampedArray(w * h * 4); for (let i = 0; i < w * h; i++) { data[i * 4 + 0] = r; data[i * 4 + 1] = g; data[i * 4 + 2] = b; data[i * 4 + 3] = 255; } return data; } function withRect(data, width, x0, y0, w, h, r, g, b) { for (let y = y0; y < y0 + h; y++) { for (let x = x0; x < x0 + w; x++) { const i = (y * width + x) * 4; data[i + 0] = r; data[i + 1] = g; data[i + 2] = b; data[i + 3] = 255; } } return data; } // 12 x 8 green sheet with an 8 x 4 brown slab at (2,2). const GREEN_PHOTO = () => withRect(solid(12, 8, 0, 200, 0), 12, 2, 2, 8, 4, 140, 90, 40); const DIMS = { width: 12, height: 8 }; // --- keySheet --------------------------------------------------------------------- // describe('keySheet', () => { it('keeps the slab in a green sheet photo', () => { const mask = keySheet(GREEN_PHOTO(), { sheet: 'green', sensitivity: 50, }); // 8x4 = 32 pixels expect(maskStats(mask).kept).toBe(32); }); it('keeps the slab in a black sheet photo', () => { const blackPhoto = withRect(solid(12, 8, 10, 10, 10), 12, 2, 2, 8, 4, 220, 210, 190); const mask = keySheet(blackPhoto, { sheet: 'black', sensitivity: 50, }); expect(maskStats(mask).kept).toBe(32); }); it('auto-selects the better key', () => { // A photo that works well for both should return the green one const mask = keySheet(GREEN_PHOTO(), { sheet: 'auto', sensitivity: 50, }); expect(maskStats(mask).kept).toBe(32); }); it('handles sensitivity 0 (tightest)', () => { const mask = keySheet(GREEN_PHOTO(), { sheet: 'green', sensitivity: 0, }); // At s=0, threshold is 150. // brown wood (140,90,40) dist = sqrt(140^2 + (90-255)^2 + 40^2) = sqrt(19600 + 27225 + 1600) = ~218. // 218 > 150, so kept. expect(maskStats(mask).kept).toBe(32); }); it('handles sensitivity 100 (loosest)', () => { const mask = keySheet(GREEN_PHOTO(), { sheet: 'green', sensitivity: 100, }); // At s=100, threshold is 50. // Sheet green (0,200,0) dist = 55. // 55 > 50, so sheet is kept too! // 12x8 = 96 pixels expect(maskStats(mask).kept).toBe(96); }); it('green key passes non-green pixels through (black key handles them)', () => { // Real slab photos (sample_01..06) have a dark blue-gray background // (e.g. rgb 26,27,29) with a bright warm slab (e.g. rgb 250,200,140). // The green key is designed to key out a GREEN sheet only. On a dark // (non-green) surface it passes pixels through (keeps them) so that // the black key can remove them. The green key alone does NOT remove // a dark background — that is the black key's job. Auto selects the // black key in this case (see the auto test below). const darkBg = solid(12, 8, 26, 27, 29); withRect(darkBg, 12, 2, 2, 8, 4, 250, 200, 140); const mask = keySheet(darkBg, { sheet: 'green', sensitivity: 50 }); const stats = maskStats(mask); // Green key on dark bg: all pixels are non-greenish, so all are kept. // This is BY DESIGN — the green key is a no-op on non-green surfaces, // and auto will select the black key instead. expect(stats.kept).toBe(96); }); it('auto selects black key over green key on a dark-background photo', () => { // Real photo: dark bg + bright slab. Green key keeps ~everything (bad). // Black key keeps only the slab (good). Auto must pick black. const darkBg = solid(12, 8, 26, 27, 29); withRect(darkBg, 12, 2, 2, 8, 4, 250, 200, 140); const mask = keySheet(darkBg, { sheet: 'auto', sensitivity: 50 }); const stats = maskStats(mask); // Should keep only the slab (32 px), not the whole frame (96 px). expect(stats.kept).toBe(32); }); it('auto selects green key over black key on a green-sheet photo', () => { // Green sheet + brown slab. Green key keeps slab only. Black key may // keep slab + some sheet. Auto must pick green (fewer kept = better). const photo = GREEN_PHOTO(); const mask = keySheet(photo, { sheet: 'auto', sensitivity: 50 }); const stats = maskStats(mask); expect(stats.kept).toBe(32); }); }); // --- maskStats ------------------------------------------------------------------- // describe('maskStats', () => { it('correctly reports kept count and empty status', () => { const mask = new Uint8Array([1, 0, 1, 0]); const stats = maskStats(mask); expect(stats.kept).toBe(2); expect(stats.total).toBe(4); expect(stats.empty).toBe(false); }); it('reports empty=true when no pixels kept', () => { const mask = new Uint8Array([0, 0, 0, 0]); const stats = maskStats(mask); expect(stats.kept).toBe(0); expect(stats.empty).toBe(true); }); });