/** * Sheet-vs-slab keep predicates used by the mask worker. * Extracted so unit tests can cover sensitivity curves without the worker. */ /** Keep slab pixels on a green sheet. Higher s widens the green-sheet match. */ export function greenKeep(r: number, g: number, b: number, s: number): boolean { const t = Math.max(0, Math.min(100, s)) / 100; // s=0 removes only strongly green sheet; increasing sensitivity widens the match. const isSheet = g - Math.max(r, b) > 45 - 35 * t; return !isSheet; } /** * Keep slab pixels on a black sheet. * At s=0 only near-black sheet (very low lum) is removed so dark live-edge / * bark (lum ~25–50) stays. The match widens monotonically toward s=100 (#74). */ export function blackKeep(r: number, g: number, b: number, s: number): boolean { const t = Math.max(0, Math.min(100, s)) / 100; const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b; // Old floor 40 ate dark wood at minimum sensitivity. Floor 10 isolates near-black. const isSheet = lum < 10 + 140 * t; return !isSheet; } export function autoPickSheet( samples: { r: number; g: number; b: number }[], ): 'green' | 'black' { let gr = 0, gl = 0; for (const p of samples) { if (p.g - Math.max(p.r, p.b) > 20) gr++; if (0.2126 * p.r + 0.7152 * p.g + 0.0722 * p.b < 70) gl++; } return gl >= gr ? 'black' : 'green'; }