first commit
This commit is contained in:
134
docs/media/image/emblem-solver/README.md
Normal file
134
docs/media/image/emblem-solver/README.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# @suaveplan/emblem-solver - Minimal-rectangle SVG converter for pixelated sprite emblems: layered occlusion, overlapping rect optimisation, dual-axis maximal rectangles, strip decomposition, and exhaustive layer ordering
|
||||
|
||||
Minimal-rectangle SVG converter for pixelated sprite emblems.
|
||||
|
||||
Converts a pixel grid (`RGBA[]`) into the smallest possible set of
|
||||
axis-aligned coloured rectangles by combining four covering strategies
|
||||
(dual-axis maximal rectangles + greedy cover, per-island independent
|
||||
cover, strip decomposition, adjacent-rect merge), pairwise + triple-wise
|
||||
overlap reductions, and exhaustive z-stacking search across up to 8! layer
|
||||
orderings. Outputs a deterministic SVG string ready for direct embedding
|
||||
or further compression.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [converter](./src/converter/converter.md) — This module is the public-facing entry point of `@suaveplan/emblem-solver`. It coordinates the entire pipeline — schema…
|
||||
- [converter](./src/converter/converter.md) — This module is the public-facing entry point of `@suaveplan/emblem-solver`. It coordinates the entire pipeline — schema…
|
||||
- [errors](./src/errors/errors.md) — This module exposes the typed error surface for `@suaveplan/emblem-solver`. It defines the stable, namespaced code regi…
|
||||
- [errors](./src/errors/errors.md) — This module exposes the typed error surface for `@suaveplan/emblem-solver`. It defines the stable, namespaced code regi…
|
||||
- [fixtures](./src/fixtures/fixtures.md) — This module is the canonical test-fixture surface for the package. Each exported function returns a deterministic `RGBA…
|
||||
- [fixtures](./src/fixtures/fixtures.md) — This module is the canonical test-fixture surface for the package. Each exported function returns a deterministic `RGBA…
|
||||
- [analyse](./src/grid/analyse/analyse.md) — This module owns `analyseGrid`, the diagnostic phase of the conversion pipeline. It walks an RGBA grid once, buckets op…
|
||||
- [analyse](./src/grid/analyse/analyse.md) — This module owns `analyseGrid`, the diagnostic phase of the conversion pipeline. It walks an RGBA grid once, buckets op…
|
||||
- [helpers](./src/helpers/helpers.md) — This module is the shared kernel of utility functions used by the analyser, the solver strategies, and the optimisers.…
|
||||
- [helpers](./src/helpers/helpers.md) — This module is the shared kernel of utility functions used by the analyser, the solver strategies, and the optimisers.…
|
||||
- [parse](./src/io/parse/parse.md) — This module owns the two import paths into the solver: `parseRGBABuffer` turns a raw RGBA `Uint8Array` (e.g. the output…
|
||||
- [parse](./src/io/parse/parse.md) — This module owns the two import paths into the solver: `parseRGBABuffer` turns a raw RGBA `Uint8Array` (e.g. the output…
|
||||
- [merge](./src/optimizer/merge/merge.md) — This module owns `mergeAdjacentRects`, the post-processing pass that combines pairs of edge-sharing same-extent rectang…
|
||||
- [merge](./src/optimizer/merge/merge.md) — This module owns `mergeAdjacentRects`, the post-processing pass that combines pairs of edge-sharing same-extent rectang…
|
||||
- [overlap](./src/optimizer/overlap/overlap.md) — This module owns `optimiseOverlap`, the deepest cover-reduction pass in the package. It looks for three patterns that t…
|
||||
- [overlap](./src/optimizer/overlap/overlap.md) — This module owns `optimiseOverlap`, the deepest cover-reduction pass in the package. It looks for three patterns that t…
|
||||
- [redundancy](./src/optimizer/redundancy/redundancy.md) — This module owns `eliminateRedundant`, the final cross-layer optimisation the converter runs after every layer's cover…
|
||||
- [redundancy](./src/optimizer/redundancy/redundancy.md) — This module owns `eliminateRedundant`, the final cross-layer optimisation the converter runs after every layer's cover…
|
||||
- [schemas](./src/schemas/schemas.md) — This module owns the runtime validation surface for `@suaveplan/emblem-solver`. It exposes `ConvertOptionsSchema`, a Zo…
|
||||
- [schemas](./src/schemas/schemas.md) — This module owns the runtime validation surface for `@suaveplan/emblem-solver`. It exposes `ConvertOptionsSchema`, a Zo…
|
||||
- [greedy](./src/solver/greedy/greedy.md) — This module provides the greedy selector that drives the primary covering strategy. `enhancedGreedyCover` picks rectang…
|
||||
- [greedy](./src/solver/greedy/greedy.md) — This module provides the greedy selector that drives the primary covering strategy. `enhancedGreedyCover` picks rectang…
|
||||
- [histogram](./src/solver/histogram/histogram.md) — This module enumerates every distinct maximal-rectangle candidate inside an opaque-pixel grid. It is the candidate pool…
|
||||
- [histogram](./src/solver/histogram/histogram.md) — This module enumerates every distinct maximal-rectangle candidate inside an opaque-pixel grid. It is the candidate pool…
|
||||
- [layer](./src/solver/layer/layer.md) — This module owns `processLayerMultiStrategy`, the function the converter calls once per layer in the current ordering.…
|
||||
- [layer](./src/solver/layer/layer.md) — This module owns `processLayerMultiStrategy`, the function the converter calls once per layer in the current ordering.…
|
||||
- [ordering](./src/solver/ordering/ordering.md) — This module provides the layer-ordering machinery the converter consults when it has to decide which colour sits on the…
|
||||
- [ordering](./src/solver/ordering/ordering.md) — This module provides the layer-ordering machinery the converter consults when it has to decide which colour sits on the…
|
||||
- [strip](./src/solver/strip/strip.md) — This module implements `stripDecomposition`, a complementary cover strategy that ignores the maximal-rect candidate poo…
|
||||
- [strip](./src/solver/strip/strip.md) — This module implements `stripDecomposition`, a complementary cover strategy that ignores the maximal-rect candidate poo…
|
||||
- [build](./src/svg/build/build.md) — This module owns `buildSVG`, the deterministic SVG serialiser the converter uses to produce its final `<svg>` string. I…
|
||||
- [build](./src/svg/build/build.md) — This module owns `buildSVG`, the deterministic SVG serialiser the converter uses to produce its final `<svg>` string. I…
|
||||
- [telemetry](./src/telemetry/telemetry.md) — This module owns the OpenTelemetry instruments emitted by `convert()`. It exposes a counter (`emblem_solver.convert.tot…
|
||||
- [telemetry](./src/telemetry/telemetry.md) — This module owns the OpenTelemetry instruments emitted by `convert()`. It exposes a counter (`emblem_solver.convert.tot…
|
||||
- [types](./src/types/types.md) — This module is the single source of truth for the public type surface of `@suaveplan/emblem-solver`. It defines the imm…
|
||||
- [types](./src/types/types.md) — This module is the single source of truth for the public type surface of `@suaveplan/emblem-solver`. It defines the imm…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
bun add @suaveplan/emblem-solver
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { convert, fromIndexedGrid } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { pixels, width, height } = fromIndexedGrid(
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
new Map([[1, { r: 255, g: 0, b: 0 }]])
|
||||
);
|
||||
|
||||
const { svg, totalRects, ratio } = convert(pixels, width, height);
|
||||
console.log(svg); // <svg …>…</svg>
|
||||
console.log(totalRects); // 2 — the cross collapses to two overlapping rects
|
||||
console.log(ratio.toFixed(1)); // 2.5 — naïve per-pixel would have been 5 rects
|
||||
```
|
||||
|
||||
## API surface
|
||||
|
||||
| Symbol | Subpath | Purpose |
|
||||
|---|---|---|
|
||||
| `convert` | root | Public orchestrator |
|
||||
| `analyseGrid` | `/grid` | Per-colour islands, density, fragmentation |
|
||||
| `parseRGBABuffer`, `fromIndexedGrid` | `/io` | Input adapters |
|
||||
| `buildSVG` | `/svg` | Standalone serialiser |
|
||||
| `dualAxisMaximalRects`, `enhancedGreedyCover`, `perIslandCover`, `stripDecomposition`, `processLayerMultiStrategy`, `permutations`, `factorial`, `getStaticOrdering` | `/solver` | Composition primitives |
|
||||
| `mergeAdjacentRects`, `optimiseOverlap`, `eliminateRedundant` | `/optimizer` | Composition primitives |
|
||||
| `ConvertOptionsSchema` | `/schemas` | Zod input validation |
|
||||
| `EmblemSolverError`, `EMBLEM_SOLVER_CODES` | `/errors` | Typed errors |
|
||||
|
||||
## Options
|
||||
|
||||
```ts
|
||||
interface ConvertOptions {
|
||||
/** Default: "auto" */
|
||||
layerOrder?: "auto" | "area-desc" | "area-asc" | "island-aware" | "perimeter-desc";
|
||||
/** Default: 40320 (= 8!) */
|
||||
maxPermutations?: number;
|
||||
/** Default: Math.random — supply a seeded RNG for deterministic auto-ordering on > 8-colour inputs */
|
||||
rng?: () => number;
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
All failures throw `EmblemSolverError` whose `.code` is one of:
|
||||
|
||||
- `EMBLEM_SOLVER.INVALID_GRID` — dimensions non-positive or
|
||||
`pixels.length !== width * height`.
|
||||
- `EMBLEM_SOLVER.INVALID_OPTIONS` — Zod rejected the options bag.
|
||||
- `EMBLEM_SOLVER.INVALID_PIXEL` — reserved for future use.
|
||||
|
||||
## Observability
|
||||
|
||||
`convert()` emits two OpenTelemetry instruments via the meter
|
||||
`@suaveplan/emblem-solver`:
|
||||
|
||||
- Counter `emblem_solver.convert.total`
|
||||
- Histogram `emblem_solver.convert.duration_ms` (unit: `ms`)
|
||||
|
||||
Instruments are registered lazily on first call, so importing the package
|
||||
has no observability side effects.
|
||||
|
||||
## Tier
|
||||
|
||||
Universal: zero browser / Node / React dependencies. Runs unchanged on
|
||||
Bun, Node, Deno, and modern browsers.
|
||||
|
||||
## License
|
||||
|
||||
MIT — Suaveplan Team.
|
||||
|
||||
98
docs/media/image/emblem-solver/src/converter/converter.md
Normal file
98
docs/media/image/emblem-solver/src/converter/converter.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# `converter/converter.ts` — public `convert()` orchestrator
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the public-facing entry point of `@suaveplan/emblem-solver`.
|
||||
It coordinates the entire pipeline — schema validation, grid validation,
|
||||
grid analysis, ordering search, per-layer covering, cross-layer redundancy
|
||||
elimination, SVG serialisation, and OpenTelemetry instrument recording —
|
||||
into a single deterministic call: `convert(pixels, width, height,
|
||||
options?)`.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated options with strict-mode unknown-key rejection.
|
||||
- Grid validation (`width > 0`, `height > 0`, `pixels.length === w*h`).
|
||||
- Two orderings paths: exhaustive permutation search when
|
||||
`factorial(colourCount) ≤ maxPermutations`, otherwise four static
|
||||
heuristics plus up to 1000 RNG-driven random samples.
|
||||
- Injectable `rng` for deterministic auto-ordering on > 8-colour inputs.
|
||||
- OTel counter + histogram recorded around the algorithmic core.
|
||||
- Early-return for fully-transparent input — emits a viewBox-only SVG and
|
||||
zero layers.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { convert, fromIndexedGrid } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { pixels, width, height } = fromIndexedGrid(
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
new Map([[1, { r: 255, g: 0, b: 0 }]])
|
||||
);
|
||||
const result = convert(pixels, width, height);
|
||||
console.log(result.totalRects, "rects;", result.svg.length, "bytes");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { convert } from "@suaveplan/emblem-solver";
|
||||
|
||||
// Seeded RNG → deterministic auto-mode output on >8-colour inputs.
|
||||
function mulberry32(seed: number): () => number {
|
||||
let s = seed >>> 0;
|
||||
return () => {
|
||||
s = (s + 0x6d2b79f5) >>> 0;
|
||||
let t = s;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
const a = convert(pixels, w, h, {
|
||||
layerOrder: "auto",
|
||||
maxPermutations: 16,
|
||||
rng: mulberry32(123),
|
||||
});
|
||||
const b = convert(pixels, w, h, {
|
||||
layerOrder: "auto",
|
||||
maxPermutations: 16,
|
||||
rng: mulberry32(123),
|
||||
});
|
||||
console.assert(a.svg === b.svg, "deterministic round-trip");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `convert(pixels, w, h, options?)` | fn | Returns `ConvertResult` |
|
||||
|
||||
| Throws | When |
|
||||
|---|---|
|
||||
| `EmblemSolverError(INVALID_OPTIONS)` | Zod schema rejects `options` |
|
||||
| `EmblemSolverError(INVALID_GRID)` | non-positive dims or `pixels.length` mismatch |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The orchestrator uses `Date.now()` for the duration histogram —
|
||||
millisecond resolution is sufficient for OTel and the clock is universal
|
||||
across Bun, Node, Deno, and browsers.
|
||||
The fisher-Yates shuffle uses the injected `rng` directly so a seeded
|
||||
source is byte-stable across runs. The heuristic branch is deliberate
|
||||
about its sample budget: `min(maxPermutations - 4, 1000)` because the
|
||||
four static strategies are evaluated first, and beyond 1000 samples the
|
||||
upstream measurements showed diminishing returns. `eliminateRedundant`
|
||||
mutates `bestLayers` in place inside `processOrdering`, which is safe
|
||||
because `processOrdering` returns a freshly-allocated array per
|
||||
invocation.
|
||||
83
docs/media/image/emblem-solver/src/errors/errors.md
Normal file
83
docs/media/image/emblem-solver/src/errors/errors.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# `errors/errors.ts` — typed error hierarchy
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module exposes the typed error surface for `@suaveplan/emblem-solver`.
|
||||
It defines the stable, namespaced code registry `EMBLEM_SOLVER_CODES` and
|
||||
the `EmblemSolverError` class that extends `SuaveplanError`. Every failure
|
||||
path inside the package — schema validation, grid validation, IO checks —
|
||||
funnels through this hierarchy so callers can pattern-match on `error.code`
|
||||
to distinguish user input mistakes from defensive guards.
|
||||
|
||||
## Features
|
||||
|
||||
- Codes are produced by `defineCodes("EMBLEM_SOLVER", [...] as const)`,
|
||||
yielding the namespaced strings `EMBLEM_SOLVER.INVALID_GRID`,
|
||||
`EMBLEM_SOLVER.INVALID_OPTIONS`, and `EMBLEM_SOLVER.INVALID_PIXEL`.
|
||||
- The registry is frozen — consumers can rely on identity equality across
|
||||
the module.
|
||||
- `EmblemSolverError` extends `SuaveplanError<EmblemSolverCode>`, so
|
||||
`error.code` narrows the union and `instanceof EmblemSolverError`
|
||||
discriminates the throw site.
|
||||
- Supports the full `SuaveplanErrorOptions` bag: `cause`, `context`,
|
||||
`traceId`, and `clock` for testing.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
EMBLEM_SOLVER_CODES,
|
||||
EmblemSolverError,
|
||||
} from "@suaveplan/emblem-solver/errors";
|
||||
|
||||
if (badInput) {
|
||||
throw new EmblemSolverError(
|
||||
EMBLEM_SOLVER_CODES.INVALID_GRID,
|
||||
"emblem-solver.invalid_grid"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
try {
|
||||
convert(pixels, w, h, options);
|
||||
} catch (err) {
|
||||
if (err instanceof EmblemSolverError) {
|
||||
switch (err.code) {
|
||||
case EMBLEM_SOLVER_CODES.INVALID_OPTIONS:
|
||||
return reportInputProblem(err);
|
||||
case EMBLEM_SOLVER_CODES.INVALID_GRID:
|
||||
return reportShapeProblem(err);
|
||||
case EMBLEM_SOLVER_CODES.INVALID_PIXEL:
|
||||
return reportPixelProblem(err);
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `EMBLEM_SOLVER_CODES` | const | Frozen `{ INVALID_GRID, INVALID_OPTIONS, INVALID_PIXEL }` map |
|
||||
| `EmblemSolverCode` | type | Union of the namespaced strings |
|
||||
| `EmblemSolverError` | class | `extends SuaveplanError<EmblemSolverCode>` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` runs at module load and `Object.freeze`s the registry,
|
||||
guaranteeing identity stability — consumers that import via the root barrel
|
||||
or the `./errors` subpath see the same object. The class constructor is
|
||||
deliberately thin: it forwards to `SuaveplanError`'s `protected` constructor
|
||||
and sets `name = "EmblemSolverError"` so stack frames disambiguate. The
|
||||
`INVALID_PIXEL` code is reserved for future use (it appears in the code
|
||||
registry so consumers can switch on it ahead of any algorithmic change that
|
||||
introduces an unrepresentable RGBA value); the test suite exercises it via
|
||||
the round-trip code coverage check.
|
||||
73
docs/media/image/emblem-solver/src/fixtures/fixtures.md
Normal file
73
docs/media/image/emblem-solver/src/fixtures/fixtures.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# `fixtures/fixtures.ts` — canonical regression grids
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the canonical test-fixture surface for the package. Each
|
||||
exported function returns a deterministic `RGBA[]` grid with known
|
||||
algorithmic properties — chessboards, donuts, crosses, T-shapes, stars,
|
||||
flags, shields, and crests. Together they form the regression suite that
|
||||
locks the solver's quality numbers in place: any change that regresses
|
||||
the rect count on one of these fixtures fails the test gate.
|
||||
|
||||
## Features
|
||||
|
||||
- Twelve fixtures total: `chessboard8x8`, `cross3x3`, `cross6x6`,
|
||||
`tShape`, `donut5x5`, `circle16`, `flagCrosses`, `multiIslandStripes`,
|
||||
`shieldEmblem32`, `crest12x12`, `star`, plus the added
|
||||
`nineColours` (>8 distinct colours, used to exercise the auto-ordering
|
||||
heuristic branch).
|
||||
- Every fixture is purely declarative — built from a `number[][]` palette
|
||||
index map and `fromIndexedGrid`. No randomness, no `Date.now`, no
|
||||
module-load side effects.
|
||||
- Index `0` is reserved for transparency; every fixture follows that
|
||||
convention.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { chessboard8x8 } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { pixels, width, height } = chessboard8x8();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { convert, crest12x12 } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { pixels, width, height } = crest12x12();
|
||||
const result = convert(pixels, width, height, { layerOrder: "auto" });
|
||||
console.log(`crest compressed to ${result.totalRects} rects`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Returns |
|
||||
|---|---|
|
||||
| `chessboard8x8()` | 8x8 alternating black/white grid |
|
||||
| `cross3x3()` / `cross6x6()` | Plus-sign crosses |
|
||||
| `tShape()` | 5x4 T-shape |
|
||||
| `donut5x5()` | Frame with transparent centre |
|
||||
| `circle16()` | 16-px pixel circle |
|
||||
| `flagCrosses()` | 16x16 layered three-colour flag |
|
||||
| `multiIslandStripes()` | 10x2 alternating two-colour stripes |
|
||||
| `shieldEmblem32()` | 32x32 three-colour shield |
|
||||
| `crest12x12()` | 12x12 four-colour crest |
|
||||
| `star()` | Irregular star outline |
|
||||
| `nineColours()` | 4x4 grid with nine distinct colours |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The fixtures are exported as zero-argument functions rather than constants
|
||||
so each test call builds a fresh pixel array — the solver does not
|
||||
mutate its input, but defensive copies keep test isolation strong. The
|
||||
`nineColours` grid exists specifically to push `factorial(colourCount) >
|
||||
maxPermutations` so the heuristic + sampling branch in `convert()` is
|
||||
exercised; without it that branch would be unreachable and coverage would
|
||||
drop. Fixtures are excluded from the package's coverage threshold
|
||||
(`coverage.exclude` in `vitest.config.ts`) because they are test
|
||||
material, not production code.
|
||||
75
docs/media/image/emblem-solver/src/grid/analyse/analyse.md
Normal file
75
docs/media/image/emblem-solver/src/grid/analyse/analyse.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# `grid/analyse/analyse.ts` — colour groups, islands, and shape stats
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `analyseGrid`, the diagnostic phase of the conversion
|
||||
pipeline. It walks an RGBA grid once, buckets opaque pixels by colour, runs
|
||||
a 4-connected flood-fill on each bucket to recover the islands, and
|
||||
computes per-colour `density` (`pixels / bbox area`) and `fragmentation`
|
||||
(`islands / pixels`). The result feeds the layer-ordering heuristics and
|
||||
the per-island cover strategy in the solver pipeline.
|
||||
|
||||
## Features
|
||||
|
||||
- Single linear pass over the grid to populate `opaquePixels` and the
|
||||
colour buckets.
|
||||
- 4-connected BFS flood-fill — diagonals are treated as separate islands,
|
||||
matching the upstream reference and the convention used by every
|
||||
pixel-art tool.
|
||||
- Density and fragmentation are pure functions of the bucket and its
|
||||
bounding box; the solver uses both to score the `island-aware` ordering.
|
||||
- Output is structurally shared with the rest of the package via
|
||||
`GridAnalysis` — solver kernels consume `colourGroups`/`colourAnalysis`
|
||||
directly.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { analyseGrid } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { colourGroups, colourAnalysis } = analyseGrid(pixels, w, h);
|
||||
console.log("colours:", colourGroups.size);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { analyseGrid } from "@suaveplan/emblem-solver";
|
||||
|
||||
function pickBackgroundColour(pixels, w, h) {
|
||||
const a = analyseGrid(pixels, w, h);
|
||||
let best = "";
|
||||
let bestScore = -Infinity;
|
||||
for (const [key, info] of a.colourAnalysis) {
|
||||
const score = info.totalPixels * info.density;
|
||||
if (score > bestScore) {
|
||||
best = key;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `analyseGrid(pixels, w, h)` | fn | Returns `GridAnalysis` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The flood-fill uses an explicit stack (`pop()`) rather than recursion to
|
||||
avoid blowing the call stack on large emblems. We `add` to `visited`
|
||||
*before* enqueueing the neighbour, which guarantees each pixel is touched
|
||||
at most twice (once when discovered, once when its packed index is popped).
|
||||
The neighbour set is computed as a small inline array of `-1`-sentinel
|
||||
checks for the four cardinal directions; benchmarks against a `for (const
|
||||
[dx,dy] …)` form showed the inline form measurably faster in V8 and Bun.
|
||||
The `Map<string, Set<number>>` colour-group layout is intentional: the
|
||||
string key is the canonical colour identity, while the packed-index `Set`
|
||||
gives O(1) `has`/`add`/`delete` for the cover strategies.
|
||||
85
docs/media/image/emblem-solver/src/helpers/helpers.md
Normal file
85
docs/media/image/emblem-solver/src/helpers/helpers.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# `helpers/helpers.ts` — packing, keys, and grid primitives
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the shared kernel of utility functions used by the analyser,
|
||||
the solver strategies, and the optimisers. It owns the canonical
|
||||
representation of a pixel index (a packed `y * width + x` integer), the
|
||||
canonical colour key (`"r,g,b"`), the `computeBounds` reduction over a
|
||||
packed-pixel set, and the boolean-grid builders that drive the
|
||||
maximal-rectangle sweep and per-layer covering.
|
||||
|
||||
## Features
|
||||
|
||||
- `pack` / `unpackX` / `unpackY` give the package a single, branch-free way
|
||||
to convert between `(x, y)` and a single integer. Using a `Set<number>`
|
||||
of packed indices is dramatically cheaper than a `Set<string>` of
|
||||
`"x,y"`.
|
||||
- `colourKey` / `parseKey` round-trip an RGBA without committing to alpha;
|
||||
the solver does not need alpha for keying.
|
||||
- `computeBounds` is the only place we touch unpack-loop machinery for
|
||||
bounds computation; everywhere else delegates here.
|
||||
- `isFullyAvailable`, `rectCoversAnyRequired`, and `countRequired` are the
|
||||
three predicates the optimiser relies on; they share an early-exit
|
||||
pattern that keeps the solver O(area × candidates) rather than
|
||||
O(area² × candidates).
|
||||
- `buildAvailableGrid` / `buildRequiredGrid` allocate the 2-D `boolean[][]`
|
||||
masks used by the cover strategies.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { pack, unpackX, unpackY } from "@suaveplan/emblem-solver";
|
||||
|
||||
const w = 8;
|
||||
const idx = pack(3, 2, w); // 19
|
||||
unpackX(idx, w); // 3
|
||||
unpackY(idx, w); // 2
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildAvailableGrid,
|
||||
buildRequiredGrid,
|
||||
isFullyAvailable,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
const opaque = new Set<number>([0, 1, 2, 3]);
|
||||
const earlier = new Set<number>();
|
||||
const avail = buildAvailableGrid(2, 2, opaque, earlier);
|
||||
const req = buildRequiredGrid(opaque, 2, 2);
|
||||
isFullyAvailable({ x: 0, y: 0, w: 2, h: 2 }, avail); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `pack(x, y, w)` | fn | Row-major packed index |
|
||||
| `unpackX(i, w)` | fn | Recover `x` |
|
||||
| `unpackY(i, w)` | fn | Recover `y` |
|
||||
| `colourKey(rgba)` | fn | `"r,g,b"` |
|
||||
| `parseKey(str)` | fn | `RGBA` with `a:1` |
|
||||
| `computeBounds(set, w)` | fn | Tight bounding `Rect` |
|
||||
| `isFullyAvailable(rect, grid)` | fn | All cells `true`? |
|
||||
| `rectCoversAnyRequired(rect, req)` | fn | Any overlap? |
|
||||
| `countRequired(rect, req)` | fn | Overlap count |
|
||||
| `buildAvailableGrid(w, h, opaque, earlier)` | fn | 2-D opaque mask minus earlier layers |
|
||||
| `buildRequiredGrid(set, w, h)` | fn | 2-D mask from packed pixel set |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The functions are deliberately monomorphic over `boolean[][]`/`number` for
|
||||
predictable JIT performance. The bounding-box reduction uses `±Infinity`
|
||||
sentinels so it works for any non-empty set. The available-grid builder
|
||||
skips earlier-layer pixels in O(1) per packed index — important when the
|
||||
solver iterates orderings exhaustively over a colour set. None of these
|
||||
helpers allocate beyond the grids they explicitly build; the predicates
|
||||
return as soon as their early-exit condition is met, so worst-case work is
|
||||
bounded by `rect.w * rect.h`.
|
||||
71
docs/media/image/emblem-solver/src/io/parse/parse.md
Normal file
71
docs/media/image/emblem-solver/src/io/parse/parse.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# `io/parse/parse.ts` — RGBA buffer + indexed-grid IO
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the two import paths into the solver: `parseRGBABuffer`
|
||||
turns a raw RGBA `Uint8Array` (e.g. the output of `<canvas>.getImageData`,
|
||||
a `sharp` raw extraction, or a network frame) into the `RGBA[]` the
|
||||
solver consumes, and `fromIndexedGrid` builds the same `RGBA[]` from a
|
||||
`number[][]` colour-index map plus a palette. The latter is the path used
|
||||
by tests and by tooling that authors emblems programmatically.
|
||||
|
||||
## Features
|
||||
|
||||
- Alpha quantisation at the standard half-byte threshold (`byte ≥ 128`
|
||||
→ opaque). The solver does not reason about partial alpha; this matches
|
||||
the upstream contract.
|
||||
- Hard `EmblemSolverError(INVALID_GRID)` on dimension mismatch instead of
|
||||
silently emitting fewer/more pixels.
|
||||
- Indexed-grid form: index `0` is reserved for transparent, and any index
|
||||
missing from the palette also produces a transparent pixel — a forgiving
|
||||
convention that lets callers omit the background entry from their
|
||||
palettes.
|
||||
- Output is a fresh `RGBA[]` with no shared references to the input.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseRGBABuffer } from "@suaveplan/emblem-solver";
|
||||
|
||||
const pixels = parseRGBABuffer(new Uint8Array(w * h * 4), w, h);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { fromIndexedGrid, convert } from "@suaveplan/emblem-solver";
|
||||
|
||||
const { pixels, width, height } = fromIndexedGrid(
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
new Map([[1, { r: 255, g: 0, b: 0 }]])
|
||||
);
|
||||
const result = convert(pixels, width, height);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `parseRGBABuffer(buf, w, h)` | fn | `RGBA[]` of length `w*h`, alpha quantised |
|
||||
| `fromIndexedGrid(grid, palette)` | fn | `{ pixels, width, height }` |
|
||||
|
||||
Both throw `EmblemSolverError(EMBLEM_SOLVER_CODES.INVALID_GRID)` on
|
||||
non-positive dimensions or buffer-length mismatch.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`parseRGBABuffer` does a tight 4-stride loop over the input, reading via
|
||||
the optional-chaining default of `0` so a truncated buffer is rejected
|
||||
before it can produce a malformed pixel. The `idx === 0` short-circuit in
|
||||
`fromIndexedGrid` is deliberate: many palette authors reserve `0` for
|
||||
transparency, so we evaluate this branch before the `Map.get` lookup. The
|
||||
return shape `{ pixels, width, height }` matches the upstream contract
|
||||
exactly so existing fixture code ports verbatim.
|
||||
67
docs/media/image/emblem-solver/src/optimizer/merge/merge.md
Normal file
67
docs/media/image/emblem-solver/src/optimizer/merge/merge.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# `optimizer/merge/merge.ts` — adjacent-rect merge pass
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `mergeAdjacentRects`, the post-processing pass that
|
||||
combines pairs of edge-sharing same-extent rectangles into a single larger
|
||||
rectangle, provided the merged span lies entirely inside the available
|
||||
mask. It is the cheapest of the optimiser passes and runs after every
|
||||
covering strategy in the layer pipeline.
|
||||
|
||||
## Features
|
||||
|
||||
- Merges along both axes: two rects with the same `y` and `h` merge
|
||||
horizontally when adjacent; two rects with the same `x` and `w` merge
|
||||
vertically when adjacent.
|
||||
- Iterates until a full pass produces no merges, so chains of three or
|
||||
more rects collapse to one in `O(n²)` per pass and `O(k · n²)` overall
|
||||
for `k` passes (bounded by the rect count).
|
||||
- Available-mask check (`isFullyAvailable`) guarantees the merged span
|
||||
never paints transparent cells.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { mergeAdjacentRects } from "@suaveplan/emblem-solver";
|
||||
|
||||
const merged = mergeAdjacentRects(
|
||||
[
|
||||
{ x: 0, y: 0, w: 1, h: 1 },
|
||||
{ x: 1, y: 0, w: 1, h: 1 },
|
||||
],
|
||||
fullyAvailableGrid
|
||||
);
|
||||
// merged === [{ x: 0, y: 0, w: 2, h: 1 }]
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { mergeAdjacentRects } from "@suaveplan/emblem-solver";
|
||||
|
||||
let rects = greedyCover;
|
||||
rects = mergeAdjacentRects(rects, available);
|
||||
rects = mergeAdjacentRects(rects, available); // idempotent — no further change
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `mergeAdjacentRects(rects, available)` | fn | Greedily merge edge-sharing same-extent rects |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The inner `tryMerge` first tests the horizontal merge (same `y`, same
|
||||
`h`, sharing an edge in `x`), then the vertical merge (same `x`, same
|
||||
`w`, sharing an edge in `y`). Either form checks the combined rectangle
|
||||
against `available` before accepting; this avoids painting through
|
||||
transparent pixels. The outer loop terminates when a full pass produces
|
||||
zero merges, so each call is idempotent. We deliberately do not try
|
||||
*non*-same-extent merges here — that is the job of `optimiseOverlap`,
|
||||
which can produce overlapping output and is therefore a strictly stronger
|
||||
(but more expensive) operation.
|
||||
@@ -0,0 +1,73 @@
|
||||
# `optimizer/overlap/overlap.ts` — overlap-based reductions
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `optimiseOverlap`, the deepest cover-reduction pass in
|
||||
the package. It looks for three patterns that the greedy cover cannot
|
||||
exploit on its own: (1) pairs whose bounding-box union is fully available
|
||||
and covers exactly the required pixels of the originals, (2) one rect
|
||||
extended to subsume a neighbour while staying inside available, and (3)
|
||||
triples of rects replaceable by two overlapping rects forming a cross,
|
||||
T-junction, or L-shape.
|
||||
|
||||
## Features
|
||||
|
||||
- Pass 1 — pairwise bbox merge with required-set inclusion check.
|
||||
- Pass 2 — `tryExtendToSubsume` grows one rect in the four cardinal
|
||||
directions toward another, accepting only steps that improve coverage
|
||||
of `bRequired` and keep the candidate inside `available`.
|
||||
- Pass 3 — `tryCrossReplace` enumerates horizontal slice positions inside
|
||||
the bbox of a 3-rect group, tests whether a single horizontal slice
|
||||
plus a remainder vertical slice covers every required pixel, and emits
|
||||
the 2-rect replacement when it does.
|
||||
- All passes are iterative: a successful merge restarts the search so
|
||||
the rect list converges to a local optimum.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { optimiseOverlap } from "@suaveplan/emblem-solver";
|
||||
|
||||
const reduced = optimiseOverlap(
|
||||
greedyCover,
|
||||
requiredGrid,
|
||||
availableGrid,
|
||||
rows,
|
||||
cols
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { optimiseOverlap } from "@suaveplan/emblem-solver";
|
||||
|
||||
let rects = greedyCover;
|
||||
do {
|
||||
const next = optimiseOverlap(rects, required, available, rows, cols);
|
||||
if (next.length === rects.length) break;
|
||||
rects = next;
|
||||
} while (true);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `optimiseOverlap(rects, required, available, rows, cols)` | fn | Apply pairwise, extend, and cross-replace reductions |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The pass 1 inclusion check uses `Set<number>` of packed pixel indices so
|
||||
the comparison is O(k) where `k` is the cell count of the union. Pass 2
|
||||
limits itself to `max(rows, cols)` extension attempts so we cannot loop
|
||||
forever on a pathological input. Pass 3 enumerates all `(hy, hh)` slice
|
||||
positions inside the bbox; this is O(bboxH²) per triple, acceptable
|
||||
because the rect list is small after pass 1. We deliberately do not
|
||||
attempt 4-rect reductions — the upstream measurements show diminishing
|
||||
returns beyond the cross/T/L family and the worst-case complexity scales
|
||||
unfavourably.
|
||||
@@ -0,0 +1,61 @@
|
||||
# `optimizer/redundancy/redundancy.ts` — cross-layer redundancy elimination
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `eliminateRedundant`, the final cross-layer optimisation
|
||||
the converter runs after every layer's cover has been built. It removes
|
||||
rectangles that produce no visible output: either because higher-z
|
||||
rectangles of any colour overdraw them, or because sibling rectangles in
|
||||
the same layer already cover the same cells. The pass operates in place
|
||||
on the layer list and is the only pass that mutates `layer.rects`.
|
||||
|
||||
## Features
|
||||
|
||||
- Composites every layer into a per-cell `target` grid first (lower-z to
|
||||
higher-z), then walks rect by rect deciding whether removal would
|
||||
change the composited output.
|
||||
- A rect is removed iff *every* one of its required cells is either
|
||||
covered by a sibling rect at the same z-index or overdrawn by a higher-z
|
||||
rect of any colour.
|
||||
- In-place mutation via `Array.prototype.splice` — the converter calls
|
||||
this once per ordering, so the in-place form keeps allocations bounded.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { eliminateRedundant } from "@suaveplan/emblem-solver";
|
||||
|
||||
eliminateRedundant(layers, width, height);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { eliminateRedundant } from "@suaveplan/emblem-solver";
|
||||
|
||||
const before = layers.reduce((s, l) => s + l.rects.length, 0);
|
||||
eliminateRedundant(layers, width, height);
|
||||
const after = layers.reduce((s, l) => s + l.rects.length, 0);
|
||||
console.log(`pruned ${before - after} redundant rects`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `eliminateRedundant(layers, width, height)` | fn | In-place prune of fully-occluded rects |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The compositing step builds a `(string | null)[][]` grid keyed by the
|
||||
canonical colour key — using a string rather than a packed integer makes
|
||||
the per-cell comparison a single `===` against the layer's fill string,
|
||||
which V8 deduplicates aggressively. The two-pass design (composite first,
|
||||
then walk rect by rect) avoids the quadratic blow-up of repeatedly
|
||||
re-compositing after each removal. The removable indices are pushed in
|
||||
ascending order and spliced in reverse, so we never break the index
|
||||
correspondence mid-removal.
|
||||
79
docs/media/image/emblem-solver/src/schemas/schemas.md
Normal file
79
docs/media/image/emblem-solver/src/schemas/schemas.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# `schemas/schemas.ts` — Zod options schema
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the runtime validation surface for `@suaveplan/emblem-solver`.
|
||||
It exposes `ConvertOptionsSchema`, a Zod schema that mirrors the static
|
||||
`ConvertOptions` interface, applies sensible defaults, and rejects unknown
|
||||
keys. `convert()` runs every caller-supplied options object through
|
||||
`safeParse` so typos or out-of-range values fail loudly with
|
||||
`EmblemSolverError(INVALID_OPTIONS)` rather than silently degrading the
|
||||
solver.
|
||||
|
||||
## Features
|
||||
|
||||
- `layerOrder` defaults to `"auto"` and is validated as a closed enum over
|
||||
the five supported strategies.
|
||||
- `maxPermutations` defaults to `40320 = 8!` — the threshold beyond which
|
||||
exhaustive permutation search is abandoned in favour of heuristics +
|
||||
random sampling. The schema enforces `int().min(1)` so a zero or negative
|
||||
value is rejected at the boundary.
|
||||
- `rng` is an optional `() => number` validated with `z.custom<…>()`. It is
|
||||
the only injection point for the package — supplying it makes the
|
||||
heuristic + sampling path fully deterministic.
|
||||
- `.strict()` rejects unknown keys, so misspelled options never silently
|
||||
succeed.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ConvertOptionsSchema } from "@suaveplan/emblem-solver/schemas";
|
||||
|
||||
const parsed = ConvertOptionsSchema.parse({ layerOrder: "area-desc" });
|
||||
parsed.maxPermutations; // 40320 (default applied)
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { ConvertOptionsSchema } from "@suaveplan/emblem-solver/schemas";
|
||||
import { EMBLEM_SOLVER_CODES, EmblemSolverError } from
|
||||
"@suaveplan/emblem-solver/errors";
|
||||
|
||||
function safeOptions(raw: unknown) {
|
||||
const r = ConvertOptionsSchema.safeParse(raw);
|
||||
if (!r.success) {
|
||||
throw new EmblemSolverError(
|
||||
EMBLEM_SOLVER_CODES.INVALID_OPTIONS,
|
||||
`bad options: ${r.error.issues
|
||||
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
||||
.join("; ")}`
|
||||
);
|
||||
}
|
||||
return r.data;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `ConvertOptionsSchema` | `z.ZodObject` | Strict schema for `ConvertOptions` |
|
||||
| `ConvertOptionsParsed` | type | Post-defaults shape of the parsed result |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema lives in its own module so the rest of the package does not pay
|
||||
the Zod cost at import time when consumers do not need validation (e.g.
|
||||
hot-loop callers that constructed options at startup). `convert()` always
|
||||
runs through this schema, but downstream tooling can call
|
||||
`ConvertOptionsSchema.parse` ahead of time and pass the parsed result into
|
||||
its own caching layer. We use `z.custom<() => number>` rather than
|
||||
`z.function()` because Zod 4 deprecated runtime function validation in
|
||||
favour of a custom predicate; this matches the catalog `zod ^4.x`. The
|
||||
`rng` field stays optional (rather than defaulting to `Math.random`) so the
|
||||
test harness can detect whether a caller injected a deterministic source.
|
||||
72
docs/media/image/emblem-solver/src/solver/greedy/greedy.md
Normal file
72
docs/media/image/emblem-solver/src/solver/greedy/greedy.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# `solver/greedy/greedy.ts` — deterministic greedy set-cover
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module provides the greedy selector that drives the primary covering
|
||||
strategy. `enhancedGreedyCover` picks rectangles from a candidate pool
|
||||
using a deterministic three-key score (covered count, efficiency, centroid
|
||||
proximity). `perIslandCover` runs the same selector independently on each
|
||||
4-connected island so the algorithm never wastes budget on rectangles that
|
||||
span the transparent gap between two disjoint islands.
|
||||
|
||||
## Features
|
||||
|
||||
- Three-key score: `covered * 1000 + efficiency * 100 + proximity * 10`.
|
||||
The factors are deliberately spaced so a more-covering rect always beats
|
||||
a more-efficient one, and ties are broken by centroid proximity.
|
||||
- Falls back to emitting `1×1` rectangles for any residual uncovered
|
||||
pixels — guarantees the cover is complete even when no candidate
|
||||
intersects the residual.
|
||||
- `perIslandCover` filters the candidate pool per island so the selector
|
||||
never considers a spanning rect; this halves selection work on
|
||||
multi-island colours.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
dualAxisMaximalRects,
|
||||
enhancedGreedyCover,
|
||||
} from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
const candidates = dualAxisMaximalRects(available, rows, cols);
|
||||
const cover = enhancedGreedyCover(candidates, required, cols, rows);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
analyseGrid,
|
||||
perIslandCover,
|
||||
dualAxisMaximalRects,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
const a = analyseGrid(pixels, w, h);
|
||||
const info = a.colourAnalysis.get("255,0,0")!;
|
||||
const candidates = dualAxisMaximalRects(available, h, w);
|
||||
const cover = perIslandCover(info.islands, candidates, w, h);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `enhancedGreedyCover(candidates, required, w, h)` | fn | Greedy cover with three-key score |
|
||||
| `perIslandCover(islands, candidates, w, h)` | fn | Independent cover per island |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The centroid term is computed against the running set of *uncovered*
|
||||
required pixels, not the original set — this keeps selections clustered
|
||||
even as the residual shrinks. We use a precomputed `maxDist =
|
||||
sqrt(w² + h²)` so the proximity term is normalised to `[0, 1]`; without
|
||||
this, the third tiebreaker could dominate the second on large grids.
|
||||
`perIslandCover` materialises a fresh `boolean[][]` per island; this is
|
||||
acceptable because the typical island count is small (≤ 10 on the
|
||||
upstream fixtures) and the allocation cost is dominated by the cover work
|
||||
inside.
|
||||
@@ -0,0 +1,67 @@
|
||||
# `solver/histogram/histogram.ts` — dual-axis maximal rectangles
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module enumerates every distinct maximal-rectangle candidate inside an
|
||||
opaque-pixel grid. It is the candidate pool consumed by
|
||||
`enhancedGreedyCover` and the layer-pipeline. The implementation runs the
|
||||
classic "largest rectangle in histogram" stack sweep on both the original
|
||||
grid and its transpose, deduplicates by `(x, y, w, h)` key, and returns the
|
||||
union. Sweeping both axes is what makes the cover competitive on
|
||||
tall/narrow patterns the single-axis sweep misses.
|
||||
|
||||
## Features
|
||||
|
||||
- O(rows × cols) histogram sweep per axis, O(rows × cols) transposition,
|
||||
O(unique-rects) merge into the final set.
|
||||
- Emits every nested maximal rectangle, not just the largest — the greedy
|
||||
cover needs the full pool to make sensible trade-offs.
|
||||
- No allocation per cell beyond the heights table; the rect set uses string
|
||||
keys so identical rects from both axes collapse to one.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { dualAxisMaximalRects } from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
const available = [
|
||||
[true, true, false],
|
||||
[true, true, false],
|
||||
];
|
||||
const rects = dualAxisMaximalRects(available, 2, 3);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
dualAxisMaximalRects,
|
||||
enhancedGreedyCover,
|
||||
} from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
const candidates = dualAxisMaximalRects(available, rows, cols);
|
||||
candidates.sort((a, b) => b.w * b.h - a.w * a.h);
|
||||
const cover = enhancedGreedyCover(candidates, required, cols, rows);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `dualAxisMaximalRects(available, rows, cols)` | fn | Deduplicated maximal rects across both sweep axes |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The internal `histogramSweep` builds a column-by-column heights table and
|
||||
then walks each row with a monotonic stack. When the stack pops, every
|
||||
height ≥ the popped column's height yields a distinct maximal rect of
|
||||
width `x - leftBound` and height `rectH`. We emit *every* nested height so
|
||||
the greedy cover can pick a smaller-but-better-positioned rect when the
|
||||
maximal-tall one would force an awkward residual. The transposed sweep is
|
||||
implemented as a fresh `boolean[][]` rather than an index permutation
|
||||
because the inner loop's access pattern is row-major and we want to keep
|
||||
cache locality on the hot path.
|
||||
84
docs/media/image/emblem-solver/src/solver/layer/layer.md
Normal file
84
docs/media/image/emblem-solver/src/solver/layer/layer.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# `solver/layer/layer.ts` — multi-strategy per-layer cover
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `processLayerMultiStrategy`, the function the converter
|
||||
calls once per layer in the current ordering. It runs every covering
|
||||
strategy the package implements (dual-axis histogram + greedy, per-island
|
||||
cover, strip decomposition, adjacent-rect merge) and returns the result
|
||||
with the fewest rectangles. Running every strategy is what makes the
|
||||
upstream algorithm robust — no single strategy wins on every emblem.
|
||||
|
||||
## Features
|
||||
|
||||
- Strategy A (dual-axis histogram → greedy → overlap optimise) is always
|
||||
evaluated; it is the workhorse.
|
||||
- Strategy B (per-island cover) only runs when the colour has more than
|
||||
one island — pure optimisation, since a single-island colour would
|
||||
produce the same answer.
|
||||
- Strategy C (strip decomposition) is only adopted when the strips fully
|
||||
cover the required pixels; the layer pipeline does not stitch in a
|
||||
partial strip cover.
|
||||
- Strategy D (adjacent-rect merge) runs against the best-so-far so we
|
||||
never reject a free merge.
|
||||
- The final winner is the smallest of all four (or three when B is
|
||||
skipped).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
analyseGrid,
|
||||
processLayerMultiStrategy,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
const a = analyseGrid(pixels, w, h);
|
||||
const ownPixels = a.colourGroups.get("255,0,0")!;
|
||||
const rects = processLayerMultiStrategy(
|
||||
"255,0,0",
|
||||
ownPixels,
|
||||
a,
|
||||
new Set<number>()
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { processLayerMultiStrategy } from "@suaveplan/emblem-solver";
|
||||
|
||||
const earlier = new Set<number>();
|
||||
for (const key of orderedKeys) {
|
||||
const ownPixels = analysis.colourGroups.get(key)!;
|
||||
const rects = processLayerMultiStrategy(
|
||||
key,
|
||||
ownPixels,
|
||||
analysis,
|
||||
earlier
|
||||
);
|
||||
layers.push({ key, rects /* ... */ });
|
||||
for (const p of ownPixels) earlier.add(p);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `processLayerMultiStrategy(key, ownPixels, analysis, earlier)` | fn | Smallest rect cover across all strategies |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The strip strategy only contributes when it fully covers the required
|
||||
mask. We compute coverage by intersecting each valid strip with the
|
||||
`required` grid (via `pack`) and comparing the resulting count to a
|
||||
single pre-tallied `totalRequired`. The adjacent-rect merge pass runs
|
||||
*after* the strategies have produced their winners; running it earlier
|
||||
would force a merge on results that would have been pruned anyway. The
|
||||
final reduce uses a strictly-less-than comparison (`a.length <= b.length`)
|
||||
so the first result wins on ties — this preserves deterministic ordering
|
||||
across runs.
|
||||
@@ -0,0 +1,74 @@
|
||||
# `solver/ordering/ordering.ts` — layer-ordering search primitives
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module provides the layer-ordering machinery the converter consults
|
||||
when it has to decide which colour sits on the bottom of the z-stack and
|
||||
which paints on top. `getStaticOrdering` returns one deterministic
|
||||
ordering per named strategy (`area-desc`, `area-asc`, `island-aware`,
|
||||
`perimeter-desc`). `permutations` lazily yields every n! ordering via
|
||||
Heap's algorithm — used by `convert()` in `auto` mode when
|
||||
`factorial(colourCount) ≤ maxPermutations`. `factorial` is the standard
|
||||
mathematical factorial used to pick between the exhaustive and heuristic
|
||||
branches.
|
||||
|
||||
## Features
|
||||
|
||||
- Four named static strategies, each O(n log n) over the colour count.
|
||||
- `permutations` is a generator yielding fresh arrays — safe to break out
|
||||
of early without materialising the full n! list.
|
||||
- `factorial` short-circuits at `n ≤ 1` so it handles the trivial
|
||||
edge cases without recursion.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
getStaticOrdering,
|
||||
permutations,
|
||||
factorial,
|
||||
} from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
const order = getStaticOrdering(analysis, "area-desc");
|
||||
factorial(5); // 120
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { permutations } from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
let best = Infinity;
|
||||
let bestPerm: string[] | undefined;
|
||||
for (const perm of permutations(["r", "g", "b"])) {
|
||||
const score = scoreOrdering(perm);
|
||||
if (score < best) {
|
||||
best = score;
|
||||
bestPerm = [...perm];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `getStaticOrdering(analysis, strategy)` | fn | Sorted colour keys for a named strategy |
|
||||
| `permutations<T>(arr)` | gen | Yields every permutation (Heap's algorithm) |
|
||||
| `factorial(n)` | fn | `factorial(0) === factorial(1) === 1` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`islandAwareScore` mixes four signals: coverage (how much of the grid the
|
||||
colour occupies), occlusion benefit (the visual mass it removes from
|
||||
higher layers), island penalty (more islands ⇒ more rectangles needed),
|
||||
and hole ratio (bbox area / pixel count — large hole ratios suggest the
|
||||
colour sits below another). The coefficients (`2.0`, `3.0`, `1.5`, `0.5`)
|
||||
are inherited verbatim from the upstream implementation; they were tuned
|
||||
empirically against the canonical sprite fixtures. `permutations` uses
|
||||
Heap's algorithm with an explicit counter array rather than recursion;
|
||||
this keeps stack pressure bounded for the worst-case 8! enumeration.
|
||||
64
docs/media/image/emblem-solver/src/solver/strip/strip.md
Normal file
64
docs/media/image/emblem-solver/src/solver/strip/strip.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# `solver/strip/strip.ts` — horizontal/vertical strip decomposition
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module implements `stripDecomposition`, a complementary cover strategy
|
||||
that ignores the maximal-rect candidate pool entirely. It finds maximal
|
||||
horizontal runs of required pixels, merges vertically-adjacent runs that
|
||||
share both `x` and `length`, and runs the transposed variant (vertical runs
|
||||
merged horizontally). The smaller of the two orientations wins. This
|
||||
strategy is what gives `convert()` the right answer on tall thin shapes
|
||||
that the histogram + greedy combo fragments.
|
||||
|
||||
## Features
|
||||
|
||||
- O(rows × cols) per axis; tail-call-free `while` loops.
|
||||
- Two orientations evaluated, smaller wins — deterministic.
|
||||
- Produces non-overlapping rects (unlike the overlap optimiser); the layer
|
||||
pipeline only adopts the strip result if it covers the entire required
|
||||
mask, otherwise it falls back to the greedy cover.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { stripDecomposition } from "@suaveplan/emblem-solver/solver";
|
||||
|
||||
const required = [
|
||||
[true, true, true],
|
||||
[true, true, true],
|
||||
];
|
||||
const rects = stripDecomposition(required, 2, 3);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
stripDecomposition,
|
||||
isFullyAvailable,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
const strips = stripDecomposition(required, rows, cols);
|
||||
const valid = strips.filter((r) => isFullyAvailable(r, available));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `stripDecomposition(required, rows, cols)` | fn | Smaller of horizontal-strips / vertical-strips |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`mergeRuns` sorts runs by their primary coordinate, then their length, then
|
||||
their secondary coordinate. The merge loop walks the sorted list once and
|
||||
collapses consecutive entries that share `(x, length)` (horizontal) or
|
||||
`(y, length)` (vertical) into a single taller/wider rect. Because the sort
|
||||
is stable on JavaScript's `Array.prototype.sort` and we break on a length
|
||||
mismatch, the merge terminates in O(n). The dual-orientation evaluation
|
||||
ensures we never make the cover worse by picking the wrong axis — the
|
||||
chess-board test exercises this branch heavily.
|
||||
61
docs/media/image/emblem-solver/src/svg/build/build.md
Normal file
61
docs/media/image/emblem-solver/src/svg/build/build.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# `svg/build/build.ts` — SVG serialiser
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns `buildSVG`, the deterministic SVG serialiser the
|
||||
converter uses to produce its final `<svg>` string. It accepts a list of
|
||||
`ColourLayer` objects (already z-stacked) and emits one `<rect>` per
|
||||
rectangle in z-ascending order. The output is byte-stable across calls
|
||||
given the same layers — the converter relies on this for the deterministic
|
||||
`auto`-ordering round-trip test.
|
||||
|
||||
## Features
|
||||
|
||||
- `viewBox="0 0 W H"` matches the input grid dimensions exactly so the
|
||||
SVG scales 1:1 with the source pixel grid.
|
||||
- `shape-rendering="crispEdges"` instructs the renderer not to
|
||||
anti-alias rect edges; this is essential for pixel-art emblems where
|
||||
smoothing would blur the silhouette.
|
||||
- Fills use the `rgb(r,g,b)` form (rather than hex) so they round-trip
|
||||
identically across renderers that disagree about uppercase hex.
|
||||
- Two-space indent on each `<rect>` line; trailing `</svg>` newline-free
|
||||
so the output is `Buffer.from(out).length` bytes regardless of platform
|
||||
line endings.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { buildSVG } from "@suaveplan/emblem-solver";
|
||||
|
||||
const svg = buildSVG(layers, width, height);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { buildSVG } from "@suaveplan/emblem-solver";
|
||||
|
||||
const reordered = layers.map((l, i) => ({ ...l, zIndex: i }));
|
||||
const svg = buildSVG(reordered, width, height);
|
||||
console.log(svg.length, "bytes");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `buildSVG(layers, w, h)` | fn | Serialise z-stacked layers to an SVG string |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function sorts a copy of the layer list (never the caller's reference)
|
||||
so callers can pass `result.layers` without losing the original ordering
|
||||
metadata. The string is assembled via an array `join("\n")` rather than
|
||||
incremental concatenation — V8 benchmarks show this is faster for the
|
||||
sub-100-rect outputs typical of emblems. We deliberately do not gzip or
|
||||
base64 here; the package's job is to produce the canonical SVG bytes and
|
||||
let downstream tooling apply transport-level compression.
|
||||
69
docs/media/image/emblem-solver/src/telemetry/telemetry.md
Normal file
69
docs/media/image/emblem-solver/src/telemetry/telemetry.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# `telemetry/telemetry.ts` — OpenTelemetry instruments
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the OpenTelemetry instruments emitted by `convert()`. It
|
||||
exposes a counter (`emblem_solver.convert.total`) and a histogram
|
||||
(`emblem_solver.convert.duration_ms`), both lazily initialised on first
|
||||
use so importing the package does not register instruments unless they
|
||||
are actually needed.
|
||||
|
||||
## Features
|
||||
|
||||
- Lazy registration — no instruments are created at module load; the
|
||||
first call to `getConvertCounter()` or `getConvertDuration()` reaches
|
||||
into `metrics.getMeter("@suaveplan/emblem-solver")` and caches the
|
||||
result.
|
||||
- Instruments are independent objects so a downstream OTel SDK can
|
||||
configure views per instrument.
|
||||
- Test helpers (`__resetTelemetryForTests`, `__peekTelemetryForTests`)
|
||||
let the test suite assert lazy-registration behaviour without forcing
|
||||
the production code to expose internals.
|
||||
- Histogram unit is `ms`; emitting an OTel view that converts to seconds
|
||||
is the consumer's responsibility.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
getConvertCounter,
|
||||
getConvertDuration,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
getConvertCounter().add(1);
|
||||
getConvertDuration().record(12.5);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import { getConvertCounter } from "@suaveplan/emblem-solver";
|
||||
|
||||
metrics.setGlobalMeterProvider(myProvider);
|
||||
// the next call lazily creates the counter against `myProvider`'s meter:
|
||||
getConvertCounter().add(1, { layerOrder: "auto" });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---|---|---|
|
||||
| `getConvertCounter()` | fn | Lazy `Counter` for `emblem_solver.convert.total` |
|
||||
| `getConvertDuration()` | fn | Lazy `Histogram` for `emblem_solver.convert.duration_ms` |
|
||||
| `__resetTelemetryForTests()` | fn | Test-only: drop cached instruments |
|
||||
| `__peekTelemetryForTests()` | fn | Test-only: inspect cached instruments |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `metrics.getMeter` call is deferred until first use so that a host
|
||||
that swaps the global meter provider after import still sees its
|
||||
provider's instruments. Without this, instruments would be bound to the
|
||||
no-op default and silently discard recordings. The test helpers are
|
||||
exported from the module (rather than placed in a separate `__internal`
|
||||
file) because they need to reach the module-scope `let` bindings; keeping
|
||||
them here avoids exposing those bindings through a different surface.
|
||||
92
docs/media/image/emblem-solver/src/types/types.md
Normal file
92
docs/media/image/emblem-solver/src/types/types.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# `types/types.ts` — public type contract
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the single source of truth for the public type surface of
|
||||
`@suaveplan/emblem-solver`. It defines the immutable data shapes that flow
|
||||
through the entire conversion pipeline (`RGBA`, `Rect`, `Island`,
|
||||
`ColourAnalysis`, `ColourLayer`, `ConvertOptions`, `ConvertResult`,
|
||||
`GridAnalysis`, `LayerOrderStrategy`, `Rng`), so every consumer can pin
|
||||
strongly-typed bindings without depending on internal implementation
|
||||
modules.
|
||||
|
||||
Types live in `types/` (per CLAUDE.md feature-layout exemption) because
|
||||
they are shared across grid analysis, the solver kernels, the optimisers,
|
||||
and the SVG emitter. Keeping them in one file avoids cycles and lets
|
||||
TypeScript inline them aggressively.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure data; zero runtime, zero side effects.
|
||||
- `RGBA.a` is narrowed to `0 | 1` — pixels are opaque or transparent; the
|
||||
upstream solver never reasoned about partial alpha and we preserve that
|
||||
contract.
|
||||
- `Rect` uses `{x, y, w, h}` (not `width/height`) to match the upstream
|
||||
on-the-wire layout.
|
||||
- `Island.pixels` is a `Set<number>` of packed indices to keep flood-fill
|
||||
inexpensive.
|
||||
- `ConvertOptions.rng` is the injectable random source used by the
|
||||
`auto`-ordering heuristic branch.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type { RGBA, ConvertOptions } from "@suaveplan/emblem-solver";
|
||||
|
||||
const px: RGBA = { r: 255, g: 0, b: 0, a: 1 };
|
||||
const opts: ConvertOptions = { layerOrder: "area-desc" };
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import type {
|
||||
ColourAnalysis,
|
||||
ColourLayer,
|
||||
GridAnalysis,
|
||||
Rng,
|
||||
} from "@suaveplan/emblem-solver";
|
||||
|
||||
function pickBottomLayer(analysis: GridAnalysis): ColourAnalysis | undefined {
|
||||
return [...analysis.colourAnalysis.values()].sort(
|
||||
(a, b) => b.totalPixels - a.totalPixels
|
||||
)[0];
|
||||
}
|
||||
|
||||
const seeded: Rng = (() => {
|
||||
let s = 1;
|
||||
return () => {
|
||||
s = (s * 1664525 + 1013904223) >>> 0;
|
||||
return s / 0x1_0000_0000;
|
||||
};
|
||||
})();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Notes |
|
||||
|---|---|---|
|
||||
| `RGBA` | interface | `{ r, g, b, a: 0 \| 1 }` |
|
||||
| `Rect` | interface | `{ x, y, w, h }` |
|
||||
| `Island` | interface | `{ pixels: Set<number>, bounds, size }` |
|
||||
| `ColourAnalysis` | interface | Per-colour analysis (density, fragmentation, islands) |
|
||||
| `ColourLayer` | interface | A z-stacked layer with `rects` + `analysis` |
|
||||
| `LayerOrderStrategy` | union | `"auto" \| "area-desc" \| "area-asc" \| "island-aware" \| "perimeter-desc"` |
|
||||
| `Rng` | type | `() => number` in `[0, 1)` |
|
||||
| `ConvertOptions` | interface | Public input to `convert()` |
|
||||
| `ConvertResult` | interface | Public output of `convert()` |
|
||||
| `GridAnalysis` | interface | Diagnostic data returned by `analyseGrid` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Every interface marked `readonly` enforces the immutability contract at the
|
||||
type level — solver internals deliberately mutate the working sets they
|
||||
allocate; the public types do not advertise mutability. `ColourLayer` keeps
|
||||
`rects` mutable because the cross-layer redundancy eliminator splices it
|
||||
in place. The types deliberately avoid Zod schemas at this layer; the
|
||||
runtime schema lives in `schemas/` so consumers who do not need validation
|
||||
do not pay the cost of importing Zod.
|
||||
Reference in New Issue
Block a user