import { minAreaRect, slabBounds } from '../geometry/rect'; import type { MaskParams, WorkerIn, WorkerOut } from '../capture/capture.types'; let bitmap: ImageBitmap | null = null; let data: Uint8ClampedArray | null = null; let w = 0, h = 0; let current = 0; // highest job id issued by main; older replies are dropped // Keep slab pixels; remove the sheet. Higher sensitivity widens the sheet match. const greenKeep = (r: number, g: number, b: number, s: number): boolean => { const t = s / 100; const isSheet = g - Math.max(r, b) > 10 + 35 * t; return !isSheet; }; const blackKeep = (r: number, g: number, b: number, s: number): boolean => { const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b; const isSheet = lum < 40 + 140 * (s / 100); return !isSheet; }; const autoPick = (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'; }; function runMask(params: MaskParams, id: number): WorkerOut { if (!data) return { id, kind: 'failed', reason: 'no source decoded' }; let sheet = params.sheet; if (sheet === 'auto') { const samples: { r: number; g: number; b: number }[] = []; for (let y = 0; y < h; y += 16) for (let x = 0; x < w; x += 16) { const i = (y * w + x) * 4; samples.push({ r: data[i], g: data[i + 1], b: data[i + 2] }); } sheet = autoPick(samples); } const keep = sheet === 'green' ? greenKeep : blackKeep; const s = Math.max(0, Math.min(100, params.sensitivity)); const bitset = new Uint8Array(w * h); let slabPx = 0; for (let i = 0, p = 0; i < data.length; i += 4, p++) { if (keep(data[i], data[i + 1], data[i + 2], s)) { bitset[p] = 1; slabPx++; } } if (slabPx === 0) { return { id, kind: 'ready', slabPx: 0, bitset, bbox: { corners: [], longSidePx: 0, angleDeg: 0 }, slabBounds: { x: 0, y: 0, w: 0, h: 0 } }; } const bbox = minAreaRect(bitset, w, h); if (!bbox) { return { id, kind: 'ready', slabPx: 0, bitset, bbox: { corners: [], longSidePx: 0, angleDeg: 0 }, slabBounds: { x: 0, y: 0, w: 0, h: 0 } }; } return { id, kind: 'ready', slabPx, bitset, bbox, slabBounds: slabBounds(bitset, w, h) }; } self.onmessage = (e: MessageEvent) => { const msg = e.data; if (msg.id < current) return; // superseded by a newer job current = msg.id; if (msg.kind === 'init') { if (bitmap) { bitmap.close(); bitmap = null; } data = null; w = h = 0; createImageBitmap(msg.blob) .then((bmp) => { w = bmp.width; h = bmp.height; const canvas = new OffscreenCanvas(w, h); const ctx = canvas.getContext('2d')!; ctx.drawImage(bmp, 0, 0); data = ctx.getImageData(0, 0, w, h).data; bitmap = bmp; self.postMessage({ id: msg.id, kind: 'decoded', w, h } as WorkerOut); }) .catch((err) => self.postMessage({ id: msg.id, kind: 'failed', reason: String(err) } satisfies WorkerOut)); return; } if (msg.kind === 'invalidate') { if (bitmap) { bitmap.close(); bitmap = null; } data = null; w = h = 0; return; } try { const out = runMask({ sheet: msg.sheet, sensitivity: msg.sensitivity }, msg.id); if (out.kind === 'ready') { self.postMessage(out, [out.bitset.buffer]); } else { self.postMessage(out); } } catch (err) { self.postMessage({ id: msg.id, kind: 'failed', reason: String(err) } satisfies WorkerOut); } }; export {};