import { pack, unpackX, unpackY } from "../../helpers/helpers.js"; import type { Rect } from "../../types/types.js"; /** * Greedy set-cover with a deterministic three-key score: * * 1. number of uncovered required pixels covered (primary) * 2. covered / area ratio (efficiency — prefer compact rects) * 3. proximity of rect centroid to the centroid of the remaining required * pixels (tiebreaker — keep selections clustered) * * When no candidate rect intersects the remaining uncovered set, fall back * to emitting 1×1 rects for every residual pixel. This guarantees the cover * is always complete even on pathological inputs. */ export function enhancedGreedyCover( candidates: ReadonlyArray, required: boolean[][], width: number, height: number ): Rect[] { const uncovered = new Set(); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { if (required[y]?.[x]) uncovered.add(pack(x, y, width)); } } if (uncovered.size === 0) return []; const selected: Rect[] = []; const maxDist = Math.sqrt(width ** 2 + height ** 2); while (uncovered.size > 0) { let cx = 0; let cy = 0; for (const idx of uncovered) { cx += unpackX(idx, width); cy += unpackY(idx, width); } cx /= uncovered.size; cy /= uncovered.size; let bestRect: Rect | null = null; let bestScore = Number.NEGATIVE_INFINITY; for (const rect of candidates) { let covered = 0; for (let ry = rect.y; ry < rect.y + rect.h; ry++) { for (let rx = rect.x; rx < rect.x + rect.w; rx++) { if (uncovered.has(pack(rx, ry, width))) covered++; } } if (covered === 0) continue; const area = rect.w * rect.h; const efficiency = covered / area; const rcx = rect.x + rect.w / 2; const rcy = rect.y + rect.h / 2; const dist = Math.sqrt((rcx - cx) ** 2 + (rcy - cy) ** 2); const proximity = 1 - dist / maxDist; const score = covered * 1000 + efficiency * 100 + proximity * 10; if (score > bestScore) { bestScore = score; bestRect = rect; } } if (!bestRect) { for (const idx of uncovered) { selected.push({ x: unpackX(idx, width), y: unpackY(idx, width), w: 1, h: 1, }); } uncovered.clear(); break; } selected.push(bestRect); for (let ry = bestRect.y; ry < bestRect.y + bestRect.h; ry++) { for (let rx = bestRect.x; rx < bestRect.x + bestRect.w; rx++) { uncovered.delete(pack(rx, ry, width)); } } } return selected; } /** * Per-island independent cover: build a local `required` mask for each * island and run `enhancedGreedyCover` against the subset of candidates * that overlap it. This avoids wasting selection budget on spanning rects * between disconnected islands. */ export function perIslandCover( islands: ReadonlyArray<{ readonly pixels: Set }>, candidates: ReadonlyArray, width: number, height: number ): Rect[] { const out: Rect[] = []; for (const island of islands) { const local: boolean[][] = Array.from({ length: height }, () => Array(width).fill(false) ); for (const idx of island.pixels) { (local[unpackY(idx, width)] as boolean[])[unpackX(idx, width)] = true; } const localCandidates = candidates.filter((r) => { for (let ry = r.y; ry < r.y + r.h; ry++) { for (let rx = r.x; rx < r.x + r.w; rx++) { if (local[ry]?.[rx]) return true; } } return false; }); out.push(...enhancedGreedyCover(localCandidates, local, width, height)); } return out; }