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
119 lines
3.5 KiB
JavaScript
119 lines
3.5 KiB
JavaScript
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);
|
||
});
|
||
});
|
||
|
||
// --- 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);
|
||
});
|
||
});
|