@mark1russell7/packing 0.2.10 → 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/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.10",
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 package. A different version is an error. */
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 size of the container after a packing. A fixed side keeps its size. A free side goes to the
176
- * far edge of the items with their margins and the inset, or of the obstacles.
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 sizeOf(input: PackInput, frame: Frame, placements: Placement[], moved: PackOutput["moved"], turns?: FrameTurns): { w: number; h: number } {
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 && turns !== undefined ? turns.turn(given) : given;
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 + frame.inset - frame.bounds.x);
187
- bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
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 - frame.bounds.x);
194
- bottom = Math.max(bottom, at.y + b.h - frame.bounds.y);
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(seed, p) {
477
- let r = stepRandom(seed, "container");
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
- r = stepRandom(seed, "obstacles");
484
- const obstacles = obstaclesOf(r, p.obstacles, frame);
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
- r = stepRandom(seed, "boxes");
487
- return { container, obstacles, boxes: boxesOf(r, p.sizes, p.count, frame) };
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
- /** This function gives the generator with a name. It throws a `RangeError` for a name that the package does not have. */
679
- export function generator(name: string): CorpusGenerator {
680
- const found = generators.find((g) => g.name === name);
681
- if (found === undefined) throw new RangeError(`The package has no generator "${name}".`);
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";
package/src/maxrects.ts CHANGED
@@ -90,9 +90,14 @@ function inside(a: Box, b: Box): boolean {
90
90
  function cut(free: Box[], b: Box, minW: number, minH: number): { free: Box[]; hits: number } {
91
91
  const kept: Box[] = [];
92
92
  const parts: Box[] = [];
93
+ // The side of the box at each part: 0 left, 1 right, 2 top, 3 bottom
94
+ const sides: number[] = [];
93
95
  let hits = 0;
94
- const add = (x0: number, y0: number, x1: number, y1: number): void => {
95
- if (x1 - x0 >= minW && y1 - y0 >= minH) parts.push({ x0, y0, x1, y1 });
96
+ const add = (side: number, x0: number, y0: number, x1: number, y1: number): void => {
97
+ if (x1 - x0 >= minW && y1 - y0 >= minH) {
98
+ parts.push({ x0, y0, x1, y1 });
99
+ sides.push(side);
100
+ }
96
101
  };
97
102
  for (const f of free) {
98
103
  if (b.x0 >= f.x1 || b.x1 <= f.x0 || b.y0 >= f.y1 || b.y1 <= f.y0) {
@@ -100,28 +105,67 @@ function cut(free: Box[], b: Box, minW: number, minH: number): { free: Box[]; hi
100
105
  continue;
101
106
  }
102
107
  hits++;
103
- if (b.x0 > f.x0) add(f.x0, f.y0, b.x0, f.y1);
104
- if (b.x1 < f.x1) add(b.x1, f.y0, f.x1, f.y1);
105
- if (b.y0 > f.y0) add(f.x0, f.y0, f.x1, b.y0);
106
- if (b.y1 < f.y1) add(f.x0, b.y1, f.x1, f.y1);
108
+ if (b.x0 > f.x0) add(0, f.x0, f.y0, b.x0, f.y1);
109
+ if (b.x1 < f.x1) add(1, b.x1, f.y0, f.x1, f.y1);
110
+ if (b.y0 > f.y0) add(2, f.x0, f.y0, f.x1, b.y0);
111
+ if (b.y1 < f.y1) add(3, f.x0, b.y1, f.x1, f.y1);
107
112
  }
113
+ if (parts.length === 0) return { free: kept, hits };
108
114
  // Each part is in the free rectangle that it comes from. Thus a kept rectangle is never in a part.
109
- const count = kept.length;
115
+ // A part spans the box on one axis and touches the box on the other axis. Thus a part of one side is not in a part of
116
+ // another side. A kept rectangle with the part in it has an edge on the same side of the box.
117
+ const near: Box[][] = [[], [], [], []];
118
+ for (const k of kept) {
119
+ if (k.x1 === b.x0) near[0]!.push(k);
120
+ if (k.x0 === b.x1) near[1]!.push(k);
121
+ if (k.y1 === b.y0) near[2]!.push(k);
122
+ if (k.y0 === b.y1) near[3]!.push(k);
123
+ }
124
+ // In the order of `byExtent`, a part comes after each part that holds it. Of two equal parts, the first part stays.
125
+ // Thus a part goes when a part that stays before it holds it.
126
+ const order = parts.map((_, i) => i).sort((i, j) => byExtent(parts[i]!, parts[j]!) || i - j);
127
+ const gone = new Uint8Array(parts.length);
128
+ const stay: Box[][] = [[], [], [], []];
129
+ for (const i of order) {
130
+ const p = parts[i]!;
131
+ const own = stay[sides[i]!]!;
132
+ let drop = false;
133
+ for (const q of own) {
134
+ if (inside(p, q)) {
135
+ drop = true;
136
+ break;
137
+ }
138
+ }
139
+ if (drop) gone[i] = 1;
140
+ else own.push(p);
141
+ }
110
142
  for (let i = 0; i < parts.length; i++) {
143
+ if (gone[i] === 1) continue;
111
144
  const p = parts[i]!;
112
145
  let drop = false;
113
- for (let j = 0; j < parts.length && !drop; j++) {
114
- if (j === i) continue;
115
- const q = parts[j]!;
116
- // Of two equal parts, the first part stays
117
- if (inside(p, q) && (j < i || !inside(q, p))) drop = true;
146
+ for (const k of near[sides[i]!]!) {
147
+ if (inside(p, k)) {
148
+ drop = true;
149
+ break;
150
+ }
118
151
  }
119
- for (let k = 0; k < count && !drop; k++) if (inside(p, kept[k]!)) drop = true;
120
152
  if (!drop) kept.push(p);
121
153
  }
122
154
  return { free: kept, hits };
123
155
  }
124
156
 
157
+ /**
158
+ * This function orders two boxes. The smaller `x0` comes first, then the smaller `y0`, then the larger `x1`, then the
159
+ * larger `y1`. Thus a box comes before each other box in it.
160
+ */
161
+ function byExtent(a: Box, b: Box): number {
162
+ if (a.x0 !== b.x0) return a.x0 < b.x0 ? -1 : 1;
163
+ if (a.y0 !== b.y0) return a.y0 < b.y0 ? -1 : 1;
164
+ if (a.x1 !== b.x1) return a.x1 > b.x1 ? -1 : 1;
165
+ if (a.y1 !== b.y1) return a.y1 > b.y1 ? -1 : 1;
166
+ return 0;
167
+ }
168
+
125
169
  /** The edges of the boxes on one axis: for each coordinate, the flat list of the ranges on the other axis. */
126
170
  type Edges = Map<number, number[]>;
127
171
 
@@ -315,10 +359,16 @@ export function runMaxRects<T>(rects: Rect<T>[], outer: Rect<T>, options: MaxRec
315
359
  s2 = f.y0 + bh;
316
360
  break;
317
361
  }
318
- const c: Choice = { index: i, turn: t === 1, x: f.x0, y: f.y0, w: bw, h: bh, s1, s2 };
319
- if (best === undefined || s1 < best.s1 || (s1 === best.s1 && (s2 < best.s2 || (s2 === best.s2 && (c.y < best.y || (c.y === best.y && c.x < best.x)))))) best = c;
320
- const ly = f.y0 + bh;
321
- if (low === undefined || ly < low.s1 || (ly === low.s1 && f.x0 < low.s2)) low = { ...c, s1: ly, s2: f.x0 };
362
+ const x = f.x0;
363
+ const y = f.y0;
364
+ // A choice object comes only for a better place
365
+ if (best === undefined || s1 < best.s1 || (s1 === best.s1 && (s2 < best.s2 || (s2 === best.s2 && (y < best.y || (y === best.y && x < best.x)))))) {
366
+ best = { index: i, turn: t === 1, x, y, w: bw, h: bh, s1, s2 };
367
+ }
368
+ // Only the move count of a rule other than `bl` reads the place of `bl`
369
+ if (rule === "bl") continue;
370
+ const ly = y + bh;
371
+ if (low === undefined || ly < low.s1 || (ly === low.s1 && x < low.s2)) low = { index: i, turn: t === 1, x, y, w: bw, h: bh, s1: ly, s2: x };
322
372
  }
323
373
  }
324
374
  if (best === undefined || !check || space.fits({ x: best.x + ox, y: best.y + oy, w: best.w, h: best.h })) break;
@@ -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
- * - The strategy throws a `RangeError` for a `k` other than 2 or 3.
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
- return packPortfolio((x: PackInput) => portfolioMembers(x, size), { id: k === undefined ? "portfolio" : `portfolio-k${k}`, seed: context.seed, ...limits }).run(input);
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 PortfolioKey, type PortfolioPick, type SelectLevel } from "@mark1russell7/layout-contract";
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 { outputHash } from "./layout-input.ts";
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 and two members with the same id.
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
- // Each level reads the measures of one output, so the measures of an output are computed one time
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 {