@mark1russell7/packing 0.2.11 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/build/index.d.ts +81 -9
- package/build/index.d.ts.map +1 -1
- package/build/index.js +475 -175
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/aspect-fit.ts +133 -0
- package/src/corpus.ts +2 -5
- package/src/frame.ts +24 -8
- package/src/generators.ts +58 -15
- package/src/index.ts +4 -1
- package/src/portfolio-strategy.ts +13 -6
- package/src/portfolio.ts +13 -30
- package/src/registry.ts +46 -3
- package/src/select-key.ts +48 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/packing",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.12",
|
|
5
5
|
"description": "Rectangle packing: shelf, guillotine and skyline strategies, push cascades and measures of a layout.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"rectangle-packing",
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// The wrapper `aspectFit` of roadmap item k62 (`docs/PACKER.md`, "The aspect fit"). For a free-size container, it packs
|
|
2
|
+
// with the wrapped strategy at some widths near the target aspect, and it keeps the best output by a key with the aspect
|
|
3
|
+
// error in it. Nothing default uses it.
|
|
4
|
+
|
|
5
|
+
import { pickMoves, portfolio, type Member, type SelectLevel } from "@mark1russell7/layout-contract";
|
|
6
|
+
import { marginOf, turnItem, type PackInput, type PackOutput, type Region } from "./contract.ts";
|
|
7
|
+
import { boundsOf, containerBounds, extentOf, viaAxes } from "./frame.ts";
|
|
8
|
+
import { MEASURE_SPECS, measure } from "./measures.ts";
|
|
9
|
+
import { measureKey } from "./select-key.ts";
|
|
10
|
+
import { createContext, type PackContext, type Strategy } from "./strategy.ts";
|
|
11
|
+
|
|
12
|
+
/** The options of `aspectFit` */
|
|
13
|
+
export interface AspectFitOptions {
|
|
14
|
+
/** The target aspect: the width divided by the height. The default is `aspect` of the container, else 1. */
|
|
15
|
+
targetAspect?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The id of the trace stage of `aspectFit`. Its measure `move.aspectPick` is 1 when the pick is not the output of the wrapped strategy. */
|
|
19
|
+
export const ASPECT_FIT_STAGE = "aspect-fit";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The factors of the widths of `aspectFit`. Each width is a factor times the square root of the area of the first output
|
|
23
|
+
* times the target aspect. The steps are approximately 5% of the width, thus approximately 0.1 of the aspect error.
|
|
24
|
+
*/
|
|
25
|
+
export const ASPECT_FIT_FACTORS: readonly number[] = Object.freeze([0.86, 0.9, 0.95, 1, 1.05, 1.1, 1.16]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The levels of the pick of `aspectFit`, in order. Level 0 is `hard`.
|
|
29
|
+
*
|
|
30
|
+
* - `hard`: the count of hard breaks of `checkHard` with `exactContainer`. Lower is better, with the minimum effect 0.
|
|
31
|
+
* - `pack.overflow.excessArea`: the overflow area above its bound. Lower is better.
|
|
32
|
+
* - `pack.aspectError`: the distance from the target aspect. Lower is better, with the minimum effect 0.01.
|
|
33
|
+
* - `pack.area`: the area of the hull. Lower is better, with the minimum effect of 1% of the area.
|
|
34
|
+
*/
|
|
35
|
+
export const ASPECT_FIT_LEVELS: readonly SelectLevel[] = Object.freeze([
|
|
36
|
+
{ id: "hard", spec: { direction: "lower", minEffect: 0 } },
|
|
37
|
+
{ id: "pack.overflow.excessArea", spec: MEASURE_SPECS["pack.overflow.excessArea"] },
|
|
38
|
+
{ id: "pack.aspectError", spec: MEASURE_SPECS["pack.aspectError"] },
|
|
39
|
+
{ id: "pack.area", spec: MEASURE_SPECS["pack.area"] },
|
|
40
|
+
] as SelectLevel[]);
|
|
41
|
+
|
|
42
|
+
/** This function gives a shape with the origin of `shape` and the width `w`. A shape that is not a rectangle keeps the holes of its polygon. */
|
|
43
|
+
function stripOf(shape: Region, w: number): Region {
|
|
44
|
+
const b = boundsOf(shape);
|
|
45
|
+
if (shape.kind === "rect") return { kind: "rect", x: b.x, y: b.y, w, h: b.h };
|
|
46
|
+
const points: [number, number][] = [
|
|
47
|
+
[b.x, b.y],
|
|
48
|
+
[b.x + w, b.y],
|
|
49
|
+
[b.x + w, b.y + b.h],
|
|
50
|
+
[b.x, b.y + b.h],
|
|
51
|
+
];
|
|
52
|
+
return shape.kind === "polygon" && shape.holes !== undefined ? { kind: "polygon", points, holes: shape.holes } : { kind: "polygon", points };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** This function gives an input with the fixed width `w` and a free height. A shape becomes a strip of the width `w`. */
|
|
56
|
+
function atWidth(input: PackInput, w: number): PackInput {
|
|
57
|
+
const c = input.container;
|
|
58
|
+
return { ...input, container: { ...c, fixed: "width", w, ...(c.shape === undefined ? {} : { shape: stripOf(c.shape, w) }) } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** This function gives the widths of `aspectFit` for an input and the first output. Each width is narrower than a fixed width. */
|
|
62
|
+
function widthsOf(input: PackInput, first: PackOutput, target: number): number[] {
|
|
63
|
+
const area = measure(input, first)["pack.area"];
|
|
64
|
+
if (!(area > 0)) return [];
|
|
65
|
+
const inset = input.container.inset ?? 0;
|
|
66
|
+
const fixed = input.container.fixed === "width" ? containerBounds(input).w : Infinity;
|
|
67
|
+
let widest = 0;
|
|
68
|
+
for (const item of input.items) {
|
|
69
|
+
const m = marginOf(item);
|
|
70
|
+
widest = Math.max(widest, item.w + m.left + m.right);
|
|
71
|
+
}
|
|
72
|
+
const ideal = Math.sqrt(area * target);
|
|
73
|
+
const out: number[] = [];
|
|
74
|
+
for (const f of ASPECT_FIT_FACTORS) {
|
|
75
|
+
const w = Math.max(Math.round(f * ideal + 2 * inset), Math.ceil(widest + 2 * inset));
|
|
76
|
+
if (w < fixed && !out.includes(w)) out.push(w);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** This function packs an input with a free height or a free size near the target aspect. */
|
|
82
|
+
function fit<O>(input: PackInput, target: number, inner: Strategy<O>, options: O, context: PackContext): PackOutput {
|
|
83
|
+
const fresh = (): PackContext => createContext(context.seed, { ...(context.budget === undefined ? {} : { budget: context.budget }), ...(context.previous === undefined ? {} : { previous: context.previous }) });
|
|
84
|
+
const first = inner.pack(input, options, fresh());
|
|
85
|
+
const widths = widthsOf(input, first, target);
|
|
86
|
+
if (widths.length === 0) return first;
|
|
87
|
+
const origin = containerBounds(input);
|
|
88
|
+
const inset = input.container.inset ?? 0;
|
|
89
|
+
const free = input.container.fixed === "none";
|
|
90
|
+
// A free width goes to the far edge of the content. A fixed width keeps its size.
|
|
91
|
+
const sized = (out: PackOutput): PackOutput => {
|
|
92
|
+
const w = free ? extentOf(input, out.placements, out.moved, origin, inset, turnItem).right : origin.w;
|
|
93
|
+
return { ...out, size: { w, h: out.size.h } };
|
|
94
|
+
};
|
|
95
|
+
const members: Member<PackInput, PackOutput>[] = [
|
|
96
|
+
{ id: inner.id, run: () => first },
|
|
97
|
+
...widths.map((w) => ({ id: `${inner.id}@${w}`, run: (x: PackInput): PackOutput => sized(inner.pack(atWidth(x, w), options, fresh())) })),
|
|
98
|
+
];
|
|
99
|
+
const view = (x: PackInput): PackInput => (x.container.aspect === target ? x : { ...x, container: { ...x.container, aspect: target } });
|
|
100
|
+
const got = portfolio(ASPECT_FIT_STAGE, members, measureKey(ASPECT_FIT_LEVELS.slice(1), view)).pick(input);
|
|
101
|
+
const stage = { id: ASPECT_FIT_STAGE, measures: { "move.aspectPick": pickMoves(got.trace).pick } };
|
|
102
|
+
return { ...got.output, trace: [...(got.output.trace ?? []), stage] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* This function wraps a strategy. For a free-size container, it packs at some widths near the target aspect and keeps the best output.
|
|
107
|
+
*
|
|
108
|
+
* - The target is `targetAspect`, else `aspect` of the container, else 1.
|
|
109
|
+
* - A container with the size `fixed` goes to the wrapped strategy without a change.
|
|
110
|
+
* - The first member is the output of the wrapped strategy. Each other member packs at one fixed width of `ASPECT_FIT_FACTORS`.
|
|
111
|
+
* - The key is `ASPECT_FIT_LEVELS`, then `outputHash`. The aspect error reads the target. `selectBest` of the contract picks the winner.
|
|
112
|
+
* - A fixed height goes through the turn of the axes, with the target `1 / target`.
|
|
113
|
+
* - The function throws a `RangeError` for a target that is not a number above 0.
|
|
114
|
+
*/
|
|
115
|
+
export function aspectFit<O>(inner: Strategy<O>, options: AspectFitOptions = {}): Strategy<O> {
|
|
116
|
+
const given = options.targetAspect;
|
|
117
|
+
if (given !== undefined && !(Number.isFinite(given) && given > 0)) throw new RangeError(`The target aspect of aspectFit is a number above 0, not ${String(given)}.`);
|
|
118
|
+
return {
|
|
119
|
+
id: `aspect-fit(${inner.id})`,
|
|
120
|
+
family: inner.family,
|
|
121
|
+
catalog: [],
|
|
122
|
+
supports: inner.supports,
|
|
123
|
+
defaults: inner.defaults,
|
|
124
|
+
deterministic: inner.deterministic,
|
|
125
|
+
pack(input: PackInput, o: O, context: PackContext): PackOutput {
|
|
126
|
+
const c = input.container;
|
|
127
|
+
if (c.fixed === "size") return inner.pack(input, o, context);
|
|
128
|
+
const target = given ?? c.aspect ?? 1;
|
|
129
|
+
if (c.fixed === "height") return viaAxes(input, context.previous, (turned, previous) => fit(turned, 1 / target, inner, o, { ...context, ...(previous === undefined ? {} : { previous }) }));
|
|
130
|
+
return fit(input, target, inner, o, context);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
package/src/corpus.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { CorpusCell, GeneratedInput, GeneratorParams } from "./layout-input
|
|
|
7
7
|
export interface CorpusSpec {
|
|
8
8
|
/** The name of the generator. */
|
|
9
9
|
generator: string;
|
|
10
|
-
/** The version of the generator. Without it, the spec uses the version of the
|
|
10
|
+
/** The version of the generator. Without it, the spec uses the first version of the name. A version that the package does not have is an error. */
|
|
11
11
|
version?: number;
|
|
12
12
|
/** The parameters over the defaults of the generator. */
|
|
13
13
|
params?: GeneratorParams;
|
|
@@ -46,10 +46,7 @@ export function corpus(name: string, specs: readonly CorpusSpec[]): Corpus {
|
|
|
46
46
|
const inputs: GeneratedInput[] = [];
|
|
47
47
|
const ids = new Set<string>();
|
|
48
48
|
for (const spec of specs) {
|
|
49
|
-
const gen = generator(spec.generator);
|
|
50
|
-
if (spec.version !== undefined && spec.version !== gen.version) {
|
|
51
|
-
throw new RangeError(`The generator "${gen.name}" has the version ${gen.version}, not ${spec.version}.`);
|
|
52
|
-
}
|
|
49
|
+
const gen = generator(spec.generator, spec.version);
|
|
53
50
|
const params = gen.withDefaults(spec.params);
|
|
54
51
|
const cell = gen.cell(params);
|
|
55
52
|
manifest.specs.push({ generator: gen.name, version: gen.version, params, seeds: [...spec.seeds] });
|
package/src/frame.ts
CHANGED
|
@@ -172,27 +172,43 @@ export function readFrame(input: PackInput, frame: Frame, trace: readonly StageT
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
* This function gives the
|
|
176
|
-
*
|
|
175
|
+
* This function gives the far edges of a packing from the corner of the container. It reads the items with their margins,
|
|
176
|
+
* the inset and the obstacles. With `turn`, a placement with `turn: 90` reads the turned item.
|
|
177
177
|
*/
|
|
178
|
-
function
|
|
178
|
+
export function extentOf(
|
|
179
|
+
input: PackInput,
|
|
180
|
+
placements: readonly Placement[],
|
|
181
|
+
moved: PackOutput["moved"],
|
|
182
|
+
origin: { x: number; y: number } = containerBounds(input),
|
|
183
|
+
inset: number = input.container.inset ?? 0,
|
|
184
|
+
turn?: (item: Item) => Item,
|
|
185
|
+
): { right: number; bottom: number } {
|
|
179
186
|
const items = new Map(input.items.map((i) => [i.id, i]));
|
|
180
187
|
let right = 0;
|
|
181
188
|
let bottom = 0;
|
|
182
189
|
for (const p of placements) {
|
|
183
190
|
const given = items.get(p.id)!;
|
|
184
|
-
const item = p.turn === 90 &&
|
|
191
|
+
const item = p.turn === 90 && turn !== undefined ? turn(given) : given;
|
|
185
192
|
const m = marginOf(item);
|
|
186
|
-
right = Math.max(right, p.x + item.w + m.right +
|
|
187
|
-
bottom = Math.max(bottom, p.y + item.h + m.bottom +
|
|
193
|
+
right = Math.max(right, p.x + item.w + m.right + inset - origin.x);
|
|
194
|
+
bottom = Math.max(bottom, p.y + item.h + m.bottom + inset - origin.y);
|
|
188
195
|
}
|
|
189
196
|
const movedById = new Map(moved.map((m) => [m.id, m]));
|
|
190
197
|
for (const o of input.obstacles ?? []) {
|
|
191
198
|
const b = boundsOf(o.region);
|
|
192
199
|
const at = movedById.get(o.id) ?? b;
|
|
193
|
-
right = Math.max(right, at.x + b.w -
|
|
194
|
-
bottom = Math.max(bottom, at.y + b.h -
|
|
200
|
+
right = Math.max(right, at.x + b.w - origin.x);
|
|
201
|
+
bottom = Math.max(bottom, at.y + b.h - origin.y);
|
|
195
202
|
}
|
|
203
|
+
return { right, bottom };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
|
|
208
|
+
* far edge of the items with their margins and the inset, or of the obstacles.
|
|
209
|
+
*/
|
|
210
|
+
function sizeOf(input: PackInput, frame: Frame, placements: Placement[], moved: PackOutput["moved"], turns?: FrameTurns): { w: number; h: number } {
|
|
211
|
+
const { right, bottom } = extentOf(input, placements, moved, frame.bounds, frame.inset, turns?.turn);
|
|
196
212
|
return {
|
|
197
213
|
w: Number.isFinite(frame.bounds.w) ? frame.bounds.w : right,
|
|
198
214
|
h: Number.isFinite(frame.bounds.h) ? frame.bounds.h : bottom,
|
package/src/generators.ts
CHANGED
|
@@ -473,18 +473,55 @@ export const rectGrid: CorpusGenerator<RectGridParams> = defineGenerator<RectGri
|
|
|
473
473
|
values: { containerSize: CONTAINER_SIZES, containerShape: CONTAINER_SHAPES, obstacles: OBSTACLE_LAYOUTS, sizes: SIZE_DISTRIBUTIONS },
|
|
474
474
|
family: (p) => `rect-grid.${p.containerSize}.${p.containerShape}.${p.obstacles}.${p.sizes}.n${p.count}`,
|
|
475
475
|
cell: (p) => cellWith(p.containerSize, p.containerShape, p.obstacles, p.sizes, p.count),
|
|
476
|
-
build
|
|
477
|
-
|
|
478
|
-
const w = int(r, 600, 1000);
|
|
479
|
-
const h = int(r, 400, 800);
|
|
480
|
-
const { region, frame } = shapeOf(r, p.containerShape, w, h);
|
|
481
|
-
const container = containerOf(p.containerSize, w, h, region);
|
|
476
|
+
build: gridOf,
|
|
477
|
+
});
|
|
482
478
|
|
|
483
|
-
|
|
484
|
-
|
|
479
|
+
/** This function builds an input of `rect-grid`: the container, then the obstacles, then the boxes. */
|
|
480
|
+
function gridOf(seed: number, p: RectGridParams): Parts {
|
|
481
|
+
let r = stepRandom(seed, "container");
|
|
482
|
+
const w = int(r, 600, 1000);
|
|
483
|
+
const h = int(r, 400, 800);
|
|
484
|
+
const { region, frame } = shapeOf(r, p.containerShape, w, h);
|
|
485
|
+
const container = containerOf(p.containerSize, w, h, region);
|
|
485
486
|
|
|
486
|
-
|
|
487
|
-
|
|
487
|
+
r = stepRandom(seed, "obstacles");
|
|
488
|
+
const obstacles = obstaclesOf(r, p.obstacles, frame);
|
|
489
|
+
|
|
490
|
+
r = stepRandom(seed, "boxes");
|
|
491
|
+
return { container, obstacles, boxes: boxesOf(r, p.sizes, p.count, frame) };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** The values of the parameter `order` of `rect-grid` version 2. The input format declares no hard order. */
|
|
495
|
+
const GRID_ORDERS = ["free", "soft"] as const;
|
|
496
|
+
|
|
497
|
+
/** The parameters of `rect-grid` version 2: the parameters of version 1 and the reading order of the boxes. */
|
|
498
|
+
export type RectGridV2Params = RectGridParams & {
|
|
499
|
+
order: (typeof GRID_ORDERS)[number];
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* The generator `rect-grid`, version 2: the inputs of version 1 with the parameter `order`. With `soft`, each box gets a
|
|
504
|
+
* rank, a random permutation of the box indexes. With `free`, the boxes have no rank.
|
|
505
|
+
*/
|
|
506
|
+
export const rectGridV2: CorpusGenerator<RectGridV2Params> = defineGenerator<RectGridV2Params>({
|
|
507
|
+
name: "rect-grid",
|
|
508
|
+
version: 2,
|
|
509
|
+
set: "corpus-v1",
|
|
510
|
+
defaults: { containerSize: "fixed", containerShape: "rect", obstacles: "none", sizes: "uniform", order: "free", count: 100 },
|
|
511
|
+
values: { containerSize: CONTAINER_SIZES, containerShape: CONTAINER_SHAPES, obstacles: OBSTACLE_LAYOUTS, sizes: SIZE_DISTRIBUTIONS, order: GRID_ORDERS },
|
|
512
|
+
family: (p) => `rect-grid.${p.containerSize}.${p.containerShape}.${p.obstacles}.${p.sizes}.${p.order}.n${p.count}`,
|
|
513
|
+
cell: (p) => ({ ...cellWith(p.containerSize, p.containerShape, p.obstacles, p.sizes, p.count), order: p.order }),
|
|
514
|
+
build(seed, p) {
|
|
515
|
+
const built = gridOf(seed, p);
|
|
516
|
+
if (p.order === "free") return built;
|
|
517
|
+
// A shuffle of Fisher and Yates gives the ranks
|
|
518
|
+
const r = stepRandom(seed, "order");
|
|
519
|
+
const ranks = built.boxes.map((_, i) => i);
|
|
520
|
+
for (let i = ranks.length - 1; i > 0; i--) {
|
|
521
|
+
const j = Math.floor(r() * (i + 1));
|
|
522
|
+
[ranks[i], ranks[j]] = [ranks[j]!, ranks[i]!];
|
|
523
|
+
}
|
|
524
|
+
return { ...built, boxes: built.boxes.map((b, i) => ({ ...b, rank: ranks[i]! })) };
|
|
488
525
|
},
|
|
489
526
|
});
|
|
490
527
|
|
|
@@ -673,11 +710,17 @@ export const edgeCases: CorpusGenerator<EdgeCaseParams> = defineGenerator<EdgeCa
|
|
|
673
710
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
674
711
|
|
|
675
712
|
/** Each generator of the package. */
|
|
676
|
-
export const generators: readonly CorpusGenerator[] = Object.freeze([rectScattered, rectGrid, gallery, dashboard, cards, wordCloud, edgeCases] as CorpusGenerator[]);
|
|
713
|
+
export const generators: readonly CorpusGenerator[] = Object.freeze([rectScattered, rectGrid, rectGridV2, gallery, dashboard, cards, wordCloud, edgeCases] as CorpusGenerator[]);
|
|
677
714
|
|
|
678
|
-
/**
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
715
|
+
/**
|
|
716
|
+
* This function gives the generator with a name and a version. Without a version, it gives the first generator of the name
|
|
717
|
+
* in `generators`: version 1 of `rect-grid`. It throws a `RangeError` for a name or a version that the package does not have.
|
|
718
|
+
*/
|
|
719
|
+
export function generator(name: string, version?: number): CorpusGenerator {
|
|
720
|
+
const named = generators.filter((g) => g.name === name);
|
|
721
|
+
if (named.length === 0) throw new RangeError(`The package has no generator "${name}".`);
|
|
722
|
+
if (version === undefined) return named[0]!;
|
|
723
|
+
const found = named.find((g) => g.version === version);
|
|
724
|
+
if (found === undefined) throw new RangeError(`The generator "${name}" has the versions ${named.map((g) => g.version).join(", ")}, not ${version}.`);
|
|
682
725
|
return found;
|
|
683
726
|
}
|
package/src/index.ts
CHANGED
|
@@ -79,7 +79,8 @@ export {
|
|
|
79
79
|
type PushStrategyOptions,
|
|
80
80
|
} from "./adapters.ts";
|
|
81
81
|
export { fromRects, toRects } from "./convert.ts";
|
|
82
|
-
export { composites, packWith, strategies, strategy } from "./registry.ts";
|
|
82
|
+
export { aspectFitStrategy, composites, packWith, strategies, strategy, type AspectFitStrategyOptions } from "./registry.ts";
|
|
83
|
+
export { ASPECT_FIT_FACTORS, ASPECT_FIT_LEVELS, ASPECT_FIT_STAGE, aspectFit, type AspectFitOptions } from "./aspect-fit.ts";
|
|
83
84
|
export { hardBreaks, packPortfolio, PORTFOLIO_LEVELS, PORTFOLIO_STAGE, type PackPortfolioOptions, type PortfolioEntry } from "./portfolio.ts";
|
|
84
85
|
export {
|
|
85
86
|
cellKey,
|
|
@@ -130,12 +131,14 @@ export {
|
|
|
130
131
|
generator,
|
|
131
132
|
generators,
|
|
132
133
|
rectGrid,
|
|
134
|
+
rectGridV2,
|
|
133
135
|
rectScattered,
|
|
134
136
|
wordCloud,
|
|
135
137
|
type CorpusGenerator,
|
|
136
138
|
type EdgeCase,
|
|
137
139
|
type EdgeCaseParams,
|
|
138
140
|
type RectGridParams,
|
|
141
|
+
type RectGridV2Params,
|
|
139
142
|
type RectScatteredParams,
|
|
140
143
|
type UiParams,
|
|
141
144
|
} from "./generators.ts";
|
|
@@ -5,7 +5,7 @@ import { maxrects, push, rows, shelf, skyline } from "./adapters.ts";
|
|
|
5
5
|
import type { PackInput, PackOutput } from "./contract.ts";
|
|
6
6
|
import { portfolioDefaultMembers, portfolioMembers } from "./portfolio-cell.ts";
|
|
7
7
|
import { PORTFOLIO_SETS } from "./portfolio-sets.ts";
|
|
8
|
-
import { packPortfolio } from "./portfolio.ts";
|
|
8
|
+
import { packPortfolio, type PortfolioEntry } from "./portfolio.ts";
|
|
9
9
|
import type { PackContext, Strategy, Supports } from "./strategy.ts";
|
|
10
10
|
|
|
11
11
|
/** The options of the strategy `portfolio`. Without options, the output is a function of the input only. */
|
|
@@ -16,6 +16,10 @@ export interface PortfolioStrategyOptions {
|
|
|
16
16
|
maxMembers?: number;
|
|
17
17
|
/** The opt-in time limit of `portfolio` of the contract, in milliseconds. It has no default. A cut member makes the result not deterministic. */
|
|
18
18
|
budgetMs?: number;
|
|
19
|
+
/** The members for each input, in place of the set of the cell. The function refuses `members` with the option `k`. */
|
|
20
|
+
members?: readonly (string | PortfolioEntry)[];
|
|
21
|
+
/** The measure ids of the key after the hard breaks, as the option `key` of `packPortfolio`. The default is the key of `PORTFOLIO_LEVELS`. */
|
|
22
|
+
key?: readonly string[];
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/** The single strategies that the members of the table use */
|
|
@@ -42,7 +46,8 @@ function unionSupports(): Supports {
|
|
|
42
46
|
* The default output thus does not depend on the clock.
|
|
43
47
|
* - A member that does not support the cell of an input throws, and it does not win. When each member throws, the strategy throws the first error.
|
|
44
48
|
* - The option `budgetMs` goes to `portfolio` of the contract. The first member always runs.
|
|
45
|
-
* -
|
|
49
|
+
* - With `members`, each input gets these members, and `maxMembers` has no default. The id of the portfolio stays `portfolio`.
|
|
50
|
+
* - The strategy throws a `RangeError` for a `k` other than 2 or 3, and for `k` with `members`.
|
|
46
51
|
*/
|
|
47
52
|
export const portfolioStrategy: Strategy<PortfolioStrategyOptions> = {
|
|
48
53
|
id: "portfolio",
|
|
@@ -52,11 +57,13 @@ export const portfolioStrategy: Strategy<PortfolioStrategyOptions> = {
|
|
|
52
57
|
defaults: {},
|
|
53
58
|
deterministic: true,
|
|
54
59
|
pack(input: PackInput, options: PortfolioStrategyOptions, context: PackContext): PackOutput {
|
|
55
|
-
const { k, maxMembers, budgetMs } = options;
|
|
60
|
+
const { k, maxMembers, budgetMs, members, key } = options;
|
|
56
61
|
if (k !== undefined && k !== 2 && k !== 3) throw new RangeError(`The option k of the strategy portfolio is 2 or 3, not ${String(k)}.`);
|
|
62
|
+
if (k !== undefined && members !== undefined) throw new RangeError("The strategy portfolio takes the option k or the option members, not the two.");
|
|
57
63
|
const size = k ?? 3;
|
|
58
|
-
const count = maxMembers ?? (k === undefined ? portfolioDefaultMembers(input) : undefined);
|
|
59
|
-
const limits = { ...(count === undefined ? {} : { maxMembers: count }), ...(budgetMs === undefined ? {} : { budgetMs }) };
|
|
60
|
-
|
|
64
|
+
const count = maxMembers ?? (k === undefined && members === undefined ? portfolioDefaultMembers(input) : undefined);
|
|
65
|
+
const limits = { ...(count === undefined ? {} : { maxMembers: count }), ...(budgetMs === undefined ? {} : { budgetMs }), ...(key === undefined ? {} : { key }) };
|
|
66
|
+
const list = members ?? ((x: PackInput) => portfolioMembers(x, size));
|
|
67
|
+
return packPortfolio(list, { id: k === undefined ? "portfolio" : `portfolio-k${k}`, seed: context.seed, ...limits }).run(input);
|
|
61
68
|
},
|
|
62
69
|
};
|
package/src/portfolio.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// The portfolio of roadmap item k20, part 1 (`docs/PACKER.md`, "The portfolio"). A portfolio packs an input with each
|
|
2
2
|
// member and keeps the best output by the one pick rule `selectBest` of the contract. Nothing default uses it.
|
|
3
3
|
|
|
4
|
-
import { pickMoves, portfolio, type Member, type Portfolio, type
|
|
5
|
-
import { checkHard } from "./check.ts";
|
|
4
|
+
import { pickMoves, portfolio, type Member, type Portfolio, type PortfolioPick, type SelectLevel } from "@mark1russell7/layout-contract";
|
|
6
5
|
import type { PackInput, PackOutput } from "./contract.ts";
|
|
7
|
-
import {
|
|
8
|
-
import { MEASURE_SPECS, measure, roundMeasures, type LayoutMeasures } from "./measures.ts";
|
|
6
|
+
import { MEASURE_SPECS } from "./measures.ts";
|
|
9
7
|
import { packWith, strategy } from "./registry.ts";
|
|
8
|
+
import { measureKey, measureLevel } from "./select-key.ts";
|
|
9
|
+
|
|
10
|
+
export { hardBreaks } from "./select-key.ts";
|
|
10
11
|
|
|
11
12
|
/** A member of `packPortfolio`: a strategy of the registry with some options. The id names the member in the trace. */
|
|
12
13
|
export interface PortfolioEntry {
|
|
@@ -29,6 +30,11 @@ export interface PackPortfolioOptions {
|
|
|
29
30
|
maxMembers?: number;
|
|
30
31
|
/** The clock in milliseconds. The default is `performance.now`. */
|
|
31
32
|
now?: () => number;
|
|
33
|
+
/**
|
|
34
|
+
* The measure ids of the key after the hard breaks, in order. Each id is a measure of `MEASURE_SPECS`. The default is
|
|
35
|
+
* `pack.overflow.excessArea`, then `pack.fill`, as in `PORTFOLIO_LEVELS`.
|
|
36
|
+
*/
|
|
37
|
+
key?: readonly string[];
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/** The id of the trace stage of `packPortfolio`. Its measure `move.pick` is 1 when the pick is not member 0. */
|
|
@@ -47,11 +53,6 @@ export const PORTFOLIO_LEVELS: readonly SelectLevel[] = Object.freeze([
|
|
|
47
53
|
{ id: "pack.fill", spec: MEASURE_SPECS["pack.fill"] },
|
|
48
54
|
] as SelectLevel[]);
|
|
49
55
|
|
|
50
|
-
/** This function gives the count of the hard breaks of an output: the sum of `checkHard` with `exactContainer`. */
|
|
51
|
-
export function hardBreaks(input: PackInput, output: PackOutput): number {
|
|
52
|
-
return Object.values(checkHard(input, output, undefined, { exactContainer: true })).reduce((sum, n) => sum + n, 0);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
56
|
/**
|
|
56
57
|
* This function makes an opt-in portfolio of strategies of the registry with `portfolio` of the contract.
|
|
57
58
|
* Its `pick(input)` packs the input with each member and gives the output of the winner, its id and the trace.
|
|
@@ -59,10 +60,11 @@ export function hardBreaks(input: PackInput, output: PackOutput): number {
|
|
|
59
60
|
* - A member is a strategy id, or a `PortfolioEntry` with options. The members run in the given order.
|
|
60
61
|
* - `members` is a list, or a function of the input that gives the list. The strategy `portfolio` uses the function form.
|
|
61
62
|
* - The key is the hard breaks, then `pack.overflow.excessArea`, then `pack.fill`. `PORTFOLIO_LEVELS` gives the specs.
|
|
63
|
+
* The option `key` gives other measures after the hard breaks.
|
|
62
64
|
* - The key reads the measures after `roundMeasures`, as a record keeps them. A replay of the records gives the same pick.
|
|
63
65
|
* - The hash of the key is `outputHash`. `selectBest` of the contract picks the winner.
|
|
64
66
|
* - The output of the winner gets the trace stage `portfolio` with `move.pick` of `pickMoves`.
|
|
65
|
-
* - The function throws a `RangeError` for an unknown strategy, an empty list
|
|
67
|
+
* - The function throws a `RangeError` for an unknown strategy, an empty list, two members with the same id and an unknown measure of `key`.
|
|
66
68
|
* With the function form, `pick` throws this error for the list of an input.
|
|
67
69
|
*/
|
|
68
70
|
export function packPortfolio(
|
|
@@ -77,26 +79,7 @@ export function packPortfolio(
|
|
|
77
79
|
return { id: e.id, run: (input: PackInput): PackOutput => packWith(e.strategy, input, e.options, seed) };
|
|
78
80
|
});
|
|
79
81
|
const list = typeof members === "function" ? (input: PackInput): Member<PackInput, PackOutput>[] => toMembers(members(input)) : toMembers(members);
|
|
80
|
-
|
|
81
|
-
const cache = new WeakMap<PackOutput, LayoutMeasures>();
|
|
82
|
-
const measured = (input: PackInput, output: PackOutput): LayoutMeasures => {
|
|
83
|
-
let m = cache.get(output);
|
|
84
|
-
if (m === undefined) {
|
|
85
|
-
// The key reads the measures as a record keeps them, so a replay of the records gives the same pick
|
|
86
|
-
m = roundMeasures(measure(input, output));
|
|
87
|
-
cache.set(output, m);
|
|
88
|
-
}
|
|
89
|
-
return m;
|
|
90
|
-
};
|
|
91
|
-
const key: PortfolioKey<PackInput, PackOutput> = {
|
|
92
|
-
hard: hardBreaks,
|
|
93
|
-
levels: PORTFOLIO_LEVELS.slice(1).map((level) => ({
|
|
94
|
-
id: level.id,
|
|
95
|
-
spec: level.spec,
|
|
96
|
-
value: (input: PackInput, output: PackOutput): number => measured(input, output)[level.id as "pack.fill" | "pack.overflow.excessArea"],
|
|
97
|
-
})),
|
|
98
|
-
hash: outputHash,
|
|
99
|
-
};
|
|
82
|
+
const key = measureKey(options.key === undefined ? PORTFOLIO_LEVELS.slice(1) : options.key.map(measureLevel));
|
|
100
83
|
const limits = {
|
|
101
84
|
...(options.budgetMs !== undefined ? { budgetMs: options.budgetMs } : {}),
|
|
102
85
|
...(options.maxMembers !== undefined ? { maxMembers: options.maxMembers } : {}),
|
package/src/registry.ts
CHANGED
|
@@ -1,16 +1,59 @@
|
|
|
1
1
|
import { maxrects, push, rows, shelf, skyline } from "./adapters.ts";
|
|
2
|
+
import { aspectFit } from "./aspect-fit.ts";
|
|
2
3
|
import type { PackInput, PackOutput } from "./contract.ts";
|
|
3
4
|
import { portfolioStrategy } from "./portfolio-strategy.ts";
|
|
4
|
-
import { createContext, type Strategy } from "./strategy.ts";
|
|
5
|
+
import { createContext, type PackContext, type Strategy, type Supports } from "./strategy.ts";
|
|
5
6
|
|
|
6
7
|
/** Each single strategy of the package. The benchmark starts each one on each input of the corpus. */
|
|
7
8
|
export const strategies: readonly Strategy[] = Object.freeze([rows, skyline, shelf, push, maxrects] as Strategy[]);
|
|
8
9
|
|
|
10
|
+
/** The options of the strategy `aspect-fit` */
|
|
11
|
+
export interface AspectFitStrategyOptions {
|
|
12
|
+
/** The id of the wrapped strategy of the registry. The option is necessary. */
|
|
13
|
+
strategy?: string;
|
|
14
|
+
/** The options of the wrapped strategy over its defaults */
|
|
15
|
+
options?: unknown;
|
|
16
|
+
/** The target aspect of `aspectFit`. The default is `aspect` of the container, else 1. */
|
|
17
|
+
targetAspect?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** This function gives the union of the supports of some strategies. */
|
|
21
|
+
function unionOf(list: readonly Strategy[]): Supports {
|
|
22
|
+
const out: Record<string, string[]> = {};
|
|
23
|
+
for (const s of list) {
|
|
24
|
+
for (const [dim, values] of Object.entries(s.supports) as [string, string[]][]) {
|
|
25
|
+
const own = (out[dim] ??= []);
|
|
26
|
+
for (const v of values) if (!own.includes(v)) own.push(v);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out as unknown as Supports;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The strategy `aspect-fit`: `aspectFit` of roadmap item k62 around the strategy of the option `strategy`. Its supports are
|
|
34
|
+
* the union of the supports of the single strategies. A cell that the wrapped strategy does not support throws its error.
|
|
35
|
+
* The strategy throws a `RangeError` without the option `strategy`.
|
|
36
|
+
*/
|
|
37
|
+
export const aspectFitStrategy: Strategy<AspectFitStrategyOptions> = {
|
|
38
|
+
id: "aspect-fit",
|
|
39
|
+
family: "aspect-fit",
|
|
40
|
+
catalog: [],
|
|
41
|
+
supports: unionOf(strategies),
|
|
42
|
+
defaults: {},
|
|
43
|
+
deterministic: true,
|
|
44
|
+
pack(input: PackInput, options: AspectFitStrategyOptions, context: PackContext): PackOutput {
|
|
45
|
+
if (options.strategy === undefined) throw new RangeError("The strategy aspect-fit needs the option strategy.");
|
|
46
|
+
const inner = strategy(options.strategy);
|
|
47
|
+
const merged: unknown = { ...(inner.defaults as object), ...(options.options as object | undefined) };
|
|
48
|
+
return aspectFit(inner, options.targetAspect === undefined ? {} : { targetAspect: options.targetAspect }).pack(input, merged, context);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
9
52
|
/**
|
|
10
53
|
* The composite strategies of the registry: each one packs an input with other strategies. They are not in `strategies`.
|
|
11
|
-
* Since 0.2.7, the list has `portfolio` of roadmap item k20.
|
|
54
|
+
* Since 0.2.7, the list has `portfolio` of roadmap item k20. Roadmap item k62 adds `aspect-fit`.
|
|
12
55
|
*/
|
|
13
|
-
export const composites: readonly Strategy[] = Object.freeze([portfolioStrategy] as Strategy[]);
|
|
56
|
+
export const composites: readonly Strategy[] = Object.freeze([portfolioStrategy, aspectFitStrategy] as Strategy[]);
|
|
14
57
|
|
|
15
58
|
/** This function gives the strategy with an id, also a composite strategy. It throws a `RangeError` for an id that the registry does not have. */
|
|
16
59
|
export function strategy(id: string): Strategy {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The key of a pick over the outputs of one input: the hard breaks, then some measures of `MEASURE_SPECS`, then the hash.
|
|
2
|
+
// `packPortfolio` and `aspectFit` use it. The module is internal: the package root exports only `hardBreaks`.
|
|
3
|
+
|
|
4
|
+
import type { PortfolioKey, SelectLevel } from "@mark1russell7/layout-contract";
|
|
5
|
+
import { checkHard } from "./check.ts";
|
|
6
|
+
import type { PackInput, PackOutput } from "./contract.ts";
|
|
7
|
+
import { outputHash } from "./layout-input.ts";
|
|
8
|
+
import { MEASURE_SPECS, measure, roundMeasures, type LayoutMeasures } from "./measures.ts";
|
|
9
|
+
|
|
10
|
+
/** This function gives the count of the hard breaks of an output: the sum of `checkHard` with `exactContainer`. */
|
|
11
|
+
export function hardBreaks(input: PackInput, output: PackOutput): number {
|
|
12
|
+
return Object.values(checkHard(input, output, undefined, { exactContainer: true })).reduce((sum, n) => sum + n, 0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** This function gives the level of a measure id with its spec of `MEASURE_SPECS`. It throws a `RangeError` for an unknown id. */
|
|
16
|
+
export function measureLevel(id: string): SelectLevel {
|
|
17
|
+
const spec = (MEASURE_SPECS as Readonly<Record<string, SelectLevel["spec"] | undefined>>)[id];
|
|
18
|
+
if (spec === undefined || id === "hard") throw new RangeError(`A level of a key is a measure of MEASURE_SPECS, not "${id}".`);
|
|
19
|
+
return { id, spec };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* This function makes the key of a pick: `hardBreaks`, then the measures of `levels` in order, then `outputHash`.
|
|
24
|
+
*
|
|
25
|
+
* - The levels read the measures after `roundMeasures`, as a record keeps them. A replay of the records gives the same pick.
|
|
26
|
+
* - The measures of one output are computed one time.
|
|
27
|
+
* - `view` gives the input of the measures. The default is the input. The hard breaks always read the input.
|
|
28
|
+
*/
|
|
29
|
+
export function measureKey(levels: readonly SelectLevel[], view: (input: PackInput) => PackInput = (input) => input): PortfolioKey<PackInput, PackOutput> {
|
|
30
|
+
const cache = new WeakMap<PackOutput, LayoutMeasures>();
|
|
31
|
+
const measured = (input: PackInput, output: PackOutput): LayoutMeasures => {
|
|
32
|
+
let m = cache.get(output);
|
|
33
|
+
if (m === undefined) {
|
|
34
|
+
m = roundMeasures(measure(view(input), output));
|
|
35
|
+
cache.set(output, m);
|
|
36
|
+
}
|
|
37
|
+
return m;
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
hard: hardBreaks,
|
|
41
|
+
levels: levels.map((level) => ({
|
|
42
|
+
id: level.id,
|
|
43
|
+
spec: level.spec,
|
|
44
|
+
value: (input: PackInput, output: PackOutput): number => (measured(input, output) as unknown as Readonly<Record<string, number>>)[level.id]!,
|
|
45
|
+
})),
|
|
46
|
+
hash: outputHash,
|
|
47
|
+
};
|
|
48
|
+
}
|