fix(fe): downscale mask flood-fill to a working resolution

The border flood-fill on a full-res 2256x4000 (9M px) photo was too slow in
JS and the async mask pass timed out into the catch branch (overlay stayed
null, 'Mask settled' never rendered). Downscale to <=1200px for the fill,
then nearest-neighbor upsample the slab bitset back to full size so
minAreaRect/slabBounds stay in source-pixel coordinates.
This commit is contained in:
2026-09-25 09:09:30 -07:00
parent 1c58f12c5d
commit 25c1f79fc1
+33 -1
View File
@@ -117,7 +117,39 @@ function walkMask(decoded: DecodedPhoto, params: MaskParams): Uint8Array {
const { width, height, rgba } = decoded;
const tone = sheetToneFor(params.sheet, decoded);
const tolerance = 20 + (params.sensitivity / 100) * 80; // 20..100 distance
return removeBackground(width, height, rgba, { tolerance, sheetTone: tone });
// Downscale to a working resolution so the flood-fill stays fast on large
// photos; the bitset is remapped back to full size for downstream geometry.
const MAX = 1200;
const scale = Math.min(1, MAX / Math.max(width, height));
if (scale >= 0.999) {
return removeBackground(width, height, rgba, { tolerance, sheetTone: tone });
}
const sw = Math.round(width * scale);
const sh = Math.round(height * scale);
const small = new Uint8ClampedArray(sw * sh * 4);
for (let y = 0; y < sh; y++) {
for (let x = 0; x < sw; x++) {
const sx = Math.min(width - 1, Math.round((x / sw) * width));
const sy = Math.min(height - 1, Math.round((y / sh) * height));
const si = (sy * width + sx) * 4;
const di = (y * sw + x) * 4;
small[di] = rgba[si];
small[di + 1] = rgba[si + 1];
small[di + 2] = rgba[si + 2];
small[di + 3] = 255;
}
}
const smallBitset = removeBackground(sw, sh, small, { tolerance, sheetTone: tone });
// Remap the small bitset back to full resolution (nearest-neighbor upsample).
const full = new Uint8Array(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const tx = Math.min(sw - 1, Math.round((x / width) * sw));
const ty = Math.min(sh - 1, Math.round((y / height) * sh));
full[y * width + x] = smallBitset[ty * sw + tx];
}
}
return full;
}
/** Sheet tone to anchor the flood fill: sampled border, biased by the choice. */