@mark1russell7/packing 0.2.11 → 0.2.13

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.11",
4
+ "version": "0.2.13",
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
@@ -89,19 +89,41 @@ function inside(a: Box, b: Box): boolean {
89
89
  */
90
90
  function cut(free: Box[], b: Box, minW: number, minH: number): { free: Box[]; hits: number } {
91
91
  const kept: Box[] = [];
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[] = [];
92
+ // 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
93
+ // another side. A kept rectangle with the part in it has an edge on the same side of the box.
94
+ const near: Box[][] = [[], [], [], []];
95
+ // For each side of the box (0 left, 1 right, 2 top, 3 bottom), the parts that stay so far. Each part has five numbers
96
+ // in the list: its four edges and its index in the order of creation.
97
+ const stay: number[][] = [[], [], [], []];
98
+ let made = 0;
95
99
  let hits = 0;
100
+ // A part stays when no other part holds more than it, and no equal part comes before it. This rule does not use an order.
101
+ // Thus a new part goes when a part that stays holds it. Else it removes each part that it holds.
96
102
  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);
103
+ if (x1 - x0 < minW || y1 - y0 < minH) return;
104
+ const own = stay[side]!;
105
+ for (let k = 0; k < own.length; ) {
106
+ const qx0 = own[k]!;
107
+ const qy0 = own[k + 1]!;
108
+ const qx1 = own[k + 2]!;
109
+ const qy1 = own[k + 3]!;
110
+ if (x0 >= qx0 && y0 >= qy0 && x1 <= qx1 && y1 <= qy1) return;
111
+ if (qx0 >= x0 && qy0 >= y0 && qx1 <= x1 && qy1 <= y1) {
112
+ // The last part of the list takes the place of the removed part
113
+ const last = own.length - 5;
114
+ if (k !== last) for (let n = 0; n < 5; n++) own[k + n] = own[last + n]!;
115
+ own.length = last;
116
+ } else k += 5;
100
117
  }
118
+ own.push(x0, y0, x1, y1, made++);
101
119
  };
102
120
  for (const f of free) {
103
121
  if (b.x0 >= f.x1 || b.x1 <= f.x0 || b.y0 >= f.y1 || b.y1 <= f.y0) {
104
122
  kept.push(f);
123
+ if (f.x1 === b.x0) near[0]!.push(f);
124
+ if (f.x0 === b.x1) near[1]!.push(f);
125
+ if (f.y1 === b.y0) near[2]!.push(f);
126
+ if (f.y0 === b.y1) near[3]!.push(f);
105
127
  continue;
106
128
  }
107
129
  hits++;
@@ -110,62 +132,28 @@ function cut(free: Box[], b: Box, minW: number, minH: number): { free: Box[]; hi
110
132
  if (b.y0 > f.y0) add(2, f.x0, f.y0, f.x1, b.y0);
111
133
  if (b.y1 < f.y1) add(3, f.x0, b.y1, f.x1, f.y1);
112
134
  }
113
- if (parts.length === 0) return { free: kept, hits };
114
- // Each part is in the free rectangle that it comes from. Thus a kept rectangle is never in a part.
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
- }
142
- for (let i = 0; i < parts.length; i++) {
143
- if (gone[i] === 1) continue;
144
- const p = parts[i]!;
145
- let drop = false;
146
- for (const k of near[sides[i]!]!) {
147
- if (inside(p, k)) {
148
- drop = true;
149
- break;
135
+ if (made === 0) return { free: kept, hits };
136
+ // Each part is in the free rectangle that it comes from. Thus no kept rectangle is in a part.
137
+ // The parts that stay go to the list in the order of creation.
138
+ const out: (Box | undefined)[] = new Array<Box | undefined>(made).fill(undefined);
139
+ for (let side = 0; side < 4; side++) {
140
+ const own = stay[side]!;
141
+ for (let k = 0; k < own.length; k += 5) {
142
+ const p: Box = { x0: own[k]!, y0: own[k + 1]!, x1: own[k + 2]!, y1: own[k + 3]! };
143
+ let drop = false;
144
+ for (const q of near[side]!) {
145
+ if (inside(p, q)) {
146
+ drop = true;
147
+ break;
148
+ }
150
149
  }
150
+ if (!drop) out[own[k + 4]!] = p;
151
151
  }
152
- if (!drop) kept.push(p);
153
152
  }
153
+ for (const p of out) if (p !== undefined) kept.push(p);
154
154
  return { free: kept, hits };
155
155
  }
156
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
-
169
157
  /** The edges of the boxes on one axis: for each coordinate, the flat list of the ranges on the other axis. */
170
158
  type Edges = Map<number, number[]>;
171
159
 
@@ -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 } : {}),