Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
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<Rect>,
required: boolean[][],
width: number,
height: number
): Rect[] {
const uncovered = new Set<number>();
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<number> }>,
candidates: ReadonlyArray<Rect>,
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;
}