@mark1russell7/packing 0.1.2 → 0.1.4

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.
@@ -0,0 +1,684 @@
1
+ // The seeded generators of the corpus (`docs/FORMAT.md`, "The generators"). Each generator is a pipeline of 3 steps: the
2
+ // container, then the obstacles, then the boxes. Each step has its own random numbers: `mulberry32` of
3
+ // `fnv1a32("<seed>:<step>")`. Thus a packing input and a routing input with the same seed get the same obstacles.
4
+
5
+ import type { Container, Item, Obstacle, Region } from "./contract.ts";
6
+ import {
7
+ countCell,
8
+ fnv1a32,
9
+ hashInput,
10
+ round3,
11
+ type CorpusCell,
12
+ type GeneratorParams,
13
+ type LayoutInput,
14
+ type ObstacleLayout,
15
+ type SizeDistribution,
16
+ } from "./layout-input.ts";
17
+ import { mulberry32, type DimensionValue } from "./strategy.ts";
18
+
19
+ /** The largest count of boxes that a generator makes. */
20
+ const MAX_COUNT = 100_000;
21
+
22
+ /** A seeded generator of `layout-input/1` files. The same version, parameters and seed give the same file, byte for byte. */
23
+ export interface CorpusGenerator<P extends GeneratorParams = GeneratorParams> {
24
+ /** The name. The file writes it in `generator.name`. */
25
+ readonly name: string;
26
+ /** The version. A change of the output of a generator needs a new version. */
27
+ readonly version: number;
28
+ /** The default set of an input, when the call gives no set. */
29
+ readonly set: string;
30
+ /** The default parameters. */
31
+ readonly defaults: Readonly<P>;
32
+ /** For each parameter with a list of values, the values that the generator accepts. */
33
+ readonly values: { readonly [K in keyof P]?: readonly P[K][] };
34
+ /** This method gives the full parameters: the defaults with the given parameters over them. It throws a `RangeError` for a bad parameter. */
35
+ withDefaults(params?: Partial<P>): P;
36
+ /** This method gives the cell of each input of the parameters. The cell does not depend on the seed. */
37
+ cell(params?: Partial<P>): CorpusCell;
38
+ /** This method gives the input file of a seed. The `id` is `<set>/<family>/s<seed>`. */
39
+ generate(seed: number, params?: Partial<P>, set?: string): LayoutInput;
40
+ }
41
+
42
+ /** The parts of a file that a generator makes. */
43
+ type Parts = Pick<LayoutInput, "container" | "boxes"> & { obstacles: Obstacle[] };
44
+
45
+ /** The data of a generator, before `defineGenerator` adds the common steps. */
46
+ interface GeneratorSpec<P extends GeneratorParams> {
47
+ name: string;
48
+ version: number;
49
+ set: string;
50
+ defaults: P;
51
+ values: { readonly [K in keyof P]?: readonly P[K][] };
52
+ family(params: P): string;
53
+ cell(params: P): CorpusCell;
54
+ build(seed: number, params: P): Parts;
55
+ }
56
+
57
+ /** A rectangle area in which the obstacles and the sizes of the boxes are made. */
58
+ interface Frame {
59
+ x: number;
60
+ y: number;
61
+ w: number;
62
+ h: number;
63
+ }
64
+
65
+ /** This function gives the random numbers of one step of a seed. */
66
+ const stepRandom = (seed: number, step: string): (() => number) => mulberry32(fnv1a32(`${seed}:${step}`));
67
+
68
+ /** A whole number from lo to hi, both included. */
69
+ const int = (r: () => number, lo: number, hi: number): number => lo + Math.floor(r() * (hi - lo + 1));
70
+
71
+ /** A whole number from the floor of lo to the floor of hi, both included. The upper end is not less than the lower end at any time. */
72
+ const span = (r: () => number, lo: number, hi: number): number => {
73
+ const a = Math.floor(lo);
74
+ return int(r, a, Math.max(a, Math.floor(hi)));
75
+ };
76
+
77
+ /** A number from lo to hi. */
78
+ const between = (r: () => number, lo: number, hi: number): number => lo + r() * (hi - lo);
79
+
80
+ /** This function gives an index with a probability in proportion to its weight. */
81
+ const pick = (r: () => number, weights: readonly number[]): number => {
82
+ let x = r() * weights.reduce((a, b) => a + b, 0);
83
+ for (const [i, w] of weights.entries()) {
84
+ x -= w;
85
+ if (x < 0) return i;
86
+ }
87
+ return weights.length - 1;
88
+ };
89
+
90
+ /** This function gives a copy of a value with each number rounded by `round3`. A key with the value `undefined` goes. */
91
+ function roundNumbers<T>(value: T): T {
92
+ if (typeof value === "number") return round3(value) as T;
93
+ if (Array.isArray(value)) return value.map(roundNumbers) as T;
94
+ if (value !== null && typeof value === "object") {
95
+ const out: Record<string, unknown> = {};
96
+ for (const [k, v] of Object.entries(value)) if (v !== undefined) out[k] = roundNumbers(v);
97
+ return out as T;
98
+ }
99
+ return value;
100
+ }
101
+
102
+ /** This function gives the cell of an input with rectangle items, no rotation, no order and no groups. */
103
+ function cellWith(
104
+ containerSize: DimensionValue<"containerSize">,
105
+ containerShape: DimensionValue<"containerShape">,
106
+ obstacles: ObstacleLayout,
107
+ sizes: SizeDistribution,
108
+ count: number,
109
+ locked = false,
110
+ ): CorpusCell {
111
+ const cell: CorpusCell = {
112
+ containerSize,
113
+ containerShape,
114
+ itemShape: "rect",
115
+ rotation: "none",
116
+ order: "free",
117
+ groups: "none",
118
+ obstacles,
119
+ sizes,
120
+ count: countCell(count),
121
+ mode: "offline",
122
+ };
123
+ if (obstacles !== "none" || locked) cell.obstacleMove = "fixed";
124
+ return cell;
125
+ }
126
+
127
+ /** This function makes a generator from its data. It adds the defaults, the checks, the stamp, the rounding and the hash. */
128
+ function defineGenerator<P extends GeneratorParams>(spec: GeneratorSpec<P>): CorpusGenerator<P> {
129
+ const withDefaults = (params: Partial<P> = {}): P => {
130
+ const full: GeneratorParams = { ...spec.defaults };
131
+ for (const [key, value] of Object.entries(params)) {
132
+ if (value === undefined) continue;
133
+ if (!(key in spec.defaults)) throw new RangeError(`The generator "${spec.name}" has no parameter "${key}".`);
134
+ full[key] = value;
135
+ }
136
+ for (const [key, allowed] of Object.entries(spec.values) as [string, readonly (string | number)[] | undefined][]) {
137
+ if (allowed !== undefined && !allowed.includes(full[key]!)) {
138
+ throw new RangeError(`The parameter "${key}" of the generator "${spec.name}" has no value "${String(full[key])}".`);
139
+ }
140
+ }
141
+ const count = full["count"];
142
+ if (count !== undefined && (typeof count !== "number" || !Number.isInteger(count) || count < 0 || count > MAX_COUNT)) {
143
+ throw new RangeError(`The parameter "count" of the generator "${spec.name}" is a whole number from 0 to ${MAX_COUNT}.`);
144
+ }
145
+ return full as P;
146
+ };
147
+
148
+ return Object.freeze({
149
+ name: spec.name,
150
+ version: spec.version,
151
+ set: spec.set,
152
+ defaults: Object.freeze({ ...spec.defaults }),
153
+ values: spec.values,
154
+ withDefaults,
155
+ cell: (params?: Partial<P>): CorpusCell => spec.cell(withDefaults(params)),
156
+ generate(seed: number, params?: Partial<P>, set: string = spec.set): LayoutInput {
157
+ if (!Number.isInteger(seed) || seed < 0) throw new RangeError(`The seed is a whole number from 0. It is ${seed}.`);
158
+ const full = withDefaults(params);
159
+ const family = spec.family(full);
160
+ const { container, obstacles, boxes } = spec.build(seed, full);
161
+ const file: Omit<LayoutInput, "hash"> = roundNumbers({
162
+ schema: "layout-input/1" as const,
163
+ id: `${set}/${family}/s${seed}`,
164
+ set,
165
+ family,
166
+ seed,
167
+ source: "made-up",
168
+ generator: { name: spec.name, version: spec.version, params: full, seed },
169
+ container,
170
+ obstacles,
171
+ boxes,
172
+ edges: [],
173
+ });
174
+ return { ...file, hash: hashInput(file) };
175
+ },
176
+ });
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------------------------------------------------
180
+ // The shared generator `rect-scattered` (version 1). `docs/FORMAT.md` gives it step by step. Do not change its output.
181
+
182
+ /** The parameters of `rect-scattered`. */
183
+ export type RectScatteredParams = { count: number };
184
+
185
+ /** The shared generator `rect-scattered` of `docs/FORMAT.md`, version 1: rectangle obstacles at random places in a fixed rectangle. */
186
+ export const rectScattered: CorpusGenerator<RectScatteredParams> = defineGenerator<RectScatteredParams>({
187
+ name: "rect-scattered",
188
+ version: 1,
189
+ set: "xcheck-v1",
190
+ defaults: { count: 40 },
191
+ values: {},
192
+ family: () => "rect-scattered",
193
+ cell: (p) => cellWith("fixed", "rect", "scattered", "uniform", p.count),
194
+ build(seed, { count }) {
195
+ let r = stepRandom(seed, "container");
196
+ const w = int(r, 600, 1000);
197
+ const h = int(r, 400, 800);
198
+ const container: Container = { w, h, fixed: "size" };
199
+
200
+ r = stepRandom(seed, "obstacles");
201
+ const n = int(r, 3, 8);
202
+ const obstacles: Obstacle[] = [];
203
+ for (let i = 0; i < n; i++) {
204
+ const ow = int(r, 40, 160);
205
+ const oh = int(r, 40, 160);
206
+ const x = int(r, 0, w - ow);
207
+ const y = int(r, 0, h - oh);
208
+ obstacles.push({ id: `o${i}`, region: { kind: "rect", x, y, w: ow, h: oh } });
209
+ }
210
+
211
+ r = stepRandom(seed, "boxes");
212
+ const boxes: Item[] = [];
213
+ for (let i = 0; i < count; i++) {
214
+ const bw = int(r, 20, 120);
215
+ const bh = int(r, 20, 80);
216
+ boxes.push({ id: `b${i}`, w: bw, h: bh });
217
+ }
218
+ return { container, obstacles, boxes };
219
+ },
220
+ });
221
+
222
+ // ---------------------------------------------------------------------------------------------------------------------
223
+ // The steps of `rect-grid` and of the families of a user interface.
224
+
225
+ /** The values of `containerSize` that the generators make. */
226
+ const CONTAINER_SIZES = ["fixed", "freeHeight", "freeWidth", "freeAspect", "freeArea"] as const;
227
+ /** The values of `containerShape`. */
228
+ const CONTAINER_SHAPES = ["rect", "convex", "polygon", "circle", "ellipse", "holes"] as const;
229
+ /** The values of `obstacles`. */
230
+ const OBSTACLE_LAYOUTS = ["none", "center", "scattered", "wall", "corridors", "touching", "edge"] as const;
231
+ /** The values of `sizes`. */
232
+ const SIZE_DISTRIBUTIONS = ["identical", "nearIdentical", "uniform", "heavyTail", "bimodal", "extremeAspect"] as const;
233
+
234
+ /** This function gives the container of a size value for the nominal rectangle `w` by `h`, with an optional shape. */
235
+ function containerOf(size: DimensionValue<"containerSize">, w: number, h: number, shape?: Region): Container {
236
+ const containers: Record<DimensionValue<"containerSize">, Container> = {
237
+ fixed: { w, h, fixed: "size" },
238
+ freeHeight: { w, fixed: "width" },
239
+ freeWidth: { h, fixed: "height" },
240
+ freeAspect: { fixed: "none", aspect: round3(w / h) },
241
+ freeArea: { fixed: "none", aspectWeight: 0 },
242
+ };
243
+ const container = containers[size];
244
+ if (shape !== undefined) container.shape = shape;
245
+ return container;
246
+ }
247
+
248
+ /** This function gives the 4 corners of a rectangle, clockwise from the top left. */
249
+ const corners = (x: number, y: number, w: number, h: number): [number, number][] => [
250
+ [x, y],
251
+ [x + w, y],
252
+ [x + w, y + h],
253
+ [x, y + h],
254
+ ];
255
+
256
+ /**
257
+ * This function gives the shape of a container in the rectangle `w` by `h`, and its frame. The frame is a rectangle in
258
+ * the shape. The obstacles and the sizes of the boxes come from the frame.
259
+ */
260
+ function shapeOf(r: () => number, shape: DimensionValue<"containerShape">, w: number, h: number): { region?: Region; frame: Frame } {
261
+ const m = Math.min(w, h);
262
+ switch (shape) {
263
+ case "rect":
264
+ return { frame: { x: 0, y: 0, w, h } };
265
+ case "convex": {
266
+ // A rectangle with its 4 corners cut at 45 degrees
267
+ const [a, b, c, d] = [0, 1, 2, 3].map(() => span(r, m * 0.1, m * 0.25)) as [number, number, number, number];
268
+ const points: [number, number][] = [[a, 0], [w - b, 0], [w, b], [w, h - c], [w - c, h], [d, h], [0, h - d], [0, a]];
269
+ const top = Math.max(a, b);
270
+ const bottom = Math.max(c, d);
271
+ return { region: { kind: "polygon", points }, frame: { x: 0, y: top, w, h: h - top - bottom } };
272
+ }
273
+ case "polygon": {
274
+ // A rectangle with a notch at the bottom right: an L shape
275
+ const nw = span(r, w * 0.25, w * 0.45);
276
+ const nh = span(r, h * 0.25, h * 0.45);
277
+ const points: [number, number][] = [[0, 0], [w, 0], [w, h - nh], [w - nw, h - nh], [w - nw, h], [0, h]];
278
+ return { region: { kind: "polygon", points }, frame: { x: 0, y: 0, w: w - nw, h } };
279
+ }
280
+ case "circle": {
281
+ const cx = Math.floor(w / 2);
282
+ const cy = Math.floor(h / 2);
283
+ const radius = Math.min(cx, cy);
284
+ const side = 2 * Math.floor((radius * Math.SQRT2 - 1) / 2);
285
+ return { region: { kind: "circle", cx, cy, r: radius }, frame: { x: cx - side / 2, y: cy - side / 2, w: side, h: side } };
286
+ }
287
+ case "ellipse": {
288
+ const rx = Math.floor(w / 2);
289
+ const ry = Math.floor(h / 2);
290
+ const a = Math.floor(rx / Math.SQRT2);
291
+ const b = Math.floor(ry / Math.SQRT2);
292
+ return { region: { kind: "ellipse", cx: rx, cy: ry, rx, ry }, frame: { x: rx - a, y: ry - b, w: 2 * a, h: 2 * b } };
293
+ }
294
+ case "holes": {
295
+ // 1 or 2 rectangle holes, the first in the left half and the second in the right half
296
+ const count = int(r, 1, 2);
297
+ const holes: [number, number][][] = [];
298
+ for (let i = 0; i < count; i++) {
299
+ const hw = span(r, w * 0.08, w * 0.15);
300
+ const hh = span(r, h * 0.1, h * 0.25);
301
+ const x = i === 0 ? span(r, 20, w / 2 - hw - 10) : span(r, w / 2 + 10, w - hw - 20);
302
+ const y = span(r, 20, h - hh - 20);
303
+ holes.push(corners(x, y, hw, hh));
304
+ }
305
+ return { region: { kind: "polygon", points: corners(0, 0, w, h), holes }, frame: { x: 0, y: 0, w, h } };
306
+ }
307
+ }
308
+ }
309
+
310
+ /** This function gives the obstacles of a layout in a frame. Each obstacle is a rectangle with whole-number sides. */
311
+ function obstaclesOf(r: () => number, layout: ObstacleLayout, f: Frame): Obstacle[] {
312
+ const m = Math.min(f.w, f.h);
313
+ const rects: { x: number; y: number; w: number; h: number }[] = [];
314
+ switch (layout) {
315
+ case "none":
316
+ break;
317
+ case "center": {
318
+ const w = span(r, f.w * 0.15, f.w * 0.3);
319
+ const h = span(r, f.h * 0.15, f.h * 0.3);
320
+ const jx = Math.floor(f.w * 0.05);
321
+ const jy = Math.floor(f.h * 0.05);
322
+ rects.push({ x: f.x + Math.floor((f.w - w) / 2) + int(r, -jx, jx), y: f.y + Math.floor((f.h - h) / 2) + int(r, -jy, jy), w, h });
323
+ break;
324
+ }
325
+ case "scattered": {
326
+ const n = int(r, 3, 8);
327
+ for (let i = 0; i < n; i++) {
328
+ const w = span(r, m * 0.05, m * 0.2);
329
+ const h = span(r, m * 0.05, m * 0.2);
330
+ rects.push({ x: int(r, f.x, f.x + f.w - w), y: int(r, f.y, f.y + f.h - h), w, h });
331
+ }
332
+ break;
333
+ }
334
+ case "wall": {
335
+ // One long, thin obstacle from one side. It leaves a gap at its other end.
336
+ const vertical = r() < 0.5;
337
+ const across = vertical ? f.w : f.h;
338
+ const along = vertical ? f.h : f.w;
339
+ const thick = span(r, Math.max(1, across * 0.03), Math.max(1, across * 0.06));
340
+ const length = span(r, along * 0.7, along * 0.85);
341
+ const at = span(r, across * 0.3, across * 0.7 - thick);
342
+ const from = r() < 0.5 ? 0 : along - length;
343
+ rects.push(vertical ? { x: f.x + at, y: f.y + from, w: thick, h: length } : { x: f.x + from, y: f.y + at, w: length, h: thick });
344
+ break;
345
+ }
346
+ case "corridors": {
347
+ // A grid of blocks, with a narrow path between two blocks and between a block and the border
348
+ const cols = int(r, 2, 4);
349
+ const rows = int(r, 2, 3);
350
+ const path = span(r, m * 0.08, m * 0.15);
351
+ const bw = Math.floor((f.w - (cols + 1) * path) / cols);
352
+ const bh = Math.floor((f.h - (rows + 1) * path) / rows);
353
+ for (let j = 0; j < rows; j++) {
354
+ for (let i = 0; i < cols; i++) rects.push({ x: f.x + path + i * (bw + path), y: f.y + path + j * (bh + path), w: bw, h: bh });
355
+ }
356
+ break;
357
+ }
358
+ case "touching": {
359
+ // A chain: each obstacle shares a part of a side with the obstacle before it
360
+ const n = int(r, 2, 4);
361
+ let w = span(r, m * 0.08, m * 0.15);
362
+ let h = span(r, m * 0.08, m * 0.15);
363
+ let x = span(r, f.x + f.w * 0.15, f.x + f.w * 0.35);
364
+ let y = span(r, f.y + f.h * 0.15, f.y + f.h * 0.35);
365
+ rects.push({ x, y, w, h });
366
+ for (let i = 1; i < n; i++) {
367
+ const right = r() < 0.5;
368
+ const nw = span(r, m * 0.08, m * 0.15);
369
+ const nh = span(r, m * 0.08, m * 0.15);
370
+ if (right) {
371
+ y = int(r, y, y + Math.floor(h / 2));
372
+ x += w;
373
+ } else {
374
+ x = int(r, x, x + Math.floor(w / 2));
375
+ y += h;
376
+ }
377
+ w = nw;
378
+ h = nh;
379
+ rects.push({ x, y, w, h });
380
+ }
381
+ break;
382
+ }
383
+ case "edge": {
384
+ // Each obstacle touches one side of the frame
385
+ const n = int(r, 2, 5);
386
+ for (let i = 0; i < n; i++) {
387
+ const side = int(r, 0, 3);
388
+ const w = span(r, m * 0.06, m * 0.15);
389
+ const h = span(r, m * 0.06, m * 0.15);
390
+ const x = side === 1 ? f.x + f.w - w : side === 3 ? f.x : int(r, f.x, f.x + f.w - w);
391
+ const y = side === 0 ? f.y : side === 2 ? f.y + f.h - h : int(r, f.y, f.y + f.h - h);
392
+ rects.push({ x, y, w, h });
393
+ }
394
+ break;
395
+ }
396
+ }
397
+ return rects.map((rect, i) => ({ id: `o${i}`, region: { kind: "rect", ...rect } }));
398
+ }
399
+
400
+ /**
401
+ * This function gives the boxes of a size distribution for a frame. The mean box has an area of 40 percent of the
402
+ * frame divided by the count. No box is wider or higher than the frame.
403
+ */
404
+ function boxesOf(r: () => number, sizes: SizeDistribution, count: number, f: Frame): Item[] {
405
+ const unit = Math.sqrt((f.w * f.h * 0.4) / Math.max(count, 1));
406
+ const fit = (x: number, max: number): number => Math.min(Math.max(round3(x), 0.01), max);
407
+ const boxes: Item[] = new Array<Item>(count);
408
+ const put = (i: number, w: number, h: number): void => {
409
+ boxes[i] = { id: `b${i}`, w: fit(w, f.w), h: fit(h, f.h) };
410
+ };
411
+ switch (sizes) {
412
+ case "identical":
413
+ case "nearIdentical": {
414
+ const aspect = Math.sqrt(between(r, 0.6, 1.6));
415
+ const bw = unit * aspect;
416
+ const bh = unit / aspect;
417
+ for (let i = 0; i < count; i++) {
418
+ if (sizes === "identical") put(i, bw, bh);
419
+ else put(i, bw * (1 + between(r, -0.03, 0.03)), bh * (1 + between(r, -0.03, 0.03)));
420
+ }
421
+ break;
422
+ }
423
+ case "uniform":
424
+ for (let i = 0; i < count; i++) put(i, unit * between(r, 0.5, 1.5), unit * between(r, 0.5, 1.5));
425
+ break;
426
+ case "heavyTail": {
427
+ // A Pareto distribution with the index 1.6, cut at 60 percent of the frame
428
+ const cap = 0.6 * Math.min(f.w, f.h);
429
+ for (let i = 0; i < count; i++) {
430
+ const s = Math.min(0.6 * unit * (1 - r()) ** (-1 / 1.6), cap);
431
+ put(i, s * between(r, 0.75, 1.33), s * between(r, 0.75, 1.33));
432
+ }
433
+ break;
434
+ }
435
+ case "bimodal":
436
+ for (let i = 0; i < count; i++) {
437
+ const s = r() < 0.25 ? 1.8 * unit : 0.6 * unit;
438
+ put(i, s * between(r, 0.85, 1.15), s * between(r, 0.85, 1.15));
439
+ }
440
+ break;
441
+ case "extremeAspect":
442
+ for (let i = 0; i < count; i++) {
443
+ const long = unit * between(r, 2.5, 4);
444
+ const short = unit * between(r, 0.15, 0.35);
445
+ if (r() < 0.5) put(i, long, short);
446
+ else put(i, short, long);
447
+ }
448
+ break;
449
+ }
450
+ return boxes;
451
+ }
452
+
453
+ // ---------------------------------------------------------------------------------------------------------------------
454
+ // `rect-grid`: one input for each point of the grid of the packer dimensions.
455
+
456
+ /** The parameters of `rect-grid`: one value for each dimension of the taxonomy that it covers. */
457
+ export type RectGridParams = {
458
+ containerSize: DimensionValue<"containerSize">;
459
+ containerShape: DimensionValue<"containerShape">;
460
+ obstacles: ObstacleLayout;
461
+ sizes: SizeDistribution;
462
+ count: number;
463
+ };
464
+
465
+ /**
466
+ * The generator `rect-grid`, version 1: rectangle boxes for each value of `containerSize`, `containerShape`, `obstacles`
467
+ * and `sizes`, with each count. The container is a nominal rectangle of 600 to 1000 by 400 to 800.
468
+ */
469
+ export const rectGrid: CorpusGenerator<RectGridParams> = defineGenerator<RectGridParams>({
470
+ name: "rect-grid",
471
+ version: 1,
472
+ set: "corpus-v1",
473
+ defaults: { containerSize: "fixed", containerShape: "rect", obstacles: "none", sizes: "uniform", count: 100 },
474
+ values: { containerSize: CONTAINER_SIZES, containerShape: CONTAINER_SHAPES, obstacles: OBSTACLE_LAYOUTS, sizes: SIZE_DISTRIBUTIONS },
475
+ family: (p) => `rect-grid.${p.containerSize}.${p.containerShape}.${p.obstacles}.${p.sizes}.n${p.count}`,
476
+ cell: (p) => cellWith(p.containerSize, p.containerShape, p.obstacles, p.sizes, p.count),
477
+ build(seed, p) {
478
+ let r = stepRandom(seed, "container");
479
+ const w = int(r, 600, 1000);
480
+ const h = int(r, 400, 800);
481
+ const { region, frame } = shapeOf(r, p.containerShape, w, h);
482
+ const container = containerOf(p.containerSize, w, h, region);
483
+
484
+ r = stepRandom(seed, "obstacles");
485
+ const obstacles = obstaclesOf(r, p.obstacles, frame);
486
+
487
+ r = stepRandom(seed, "boxes");
488
+ return { container, obstacles, boxes: boxesOf(r, p.sizes, p.count, frame) };
489
+ },
490
+ });
491
+
492
+ // ---------------------------------------------------------------------------------------------------------------------
493
+ // The families of a user interface: gallery, dashboard, cards and word cloud.
494
+
495
+ /** The parameters of a family of a user interface. */
496
+ export type UiParams = {
497
+ containerSize: DimensionValue<"containerSize">;
498
+ count: number;
499
+ };
500
+
501
+ /** This function makes the generator of a family of a user interface. The container is 800 to 1400 by 600 to 1000, with no obstacle. */
502
+ function uiGenerator(name: string, sizes: SizeDistribution, count: number, boxes: (r: () => number, count: number) => Item[]): CorpusGenerator<UiParams> {
503
+ return defineGenerator<UiParams>({
504
+ name,
505
+ version: 1,
506
+ set: "corpus-v1",
507
+ defaults: { containerSize: "freeHeight", count },
508
+ values: { containerSize: CONTAINER_SIZES },
509
+ family: (p) => `${name}.${p.containerSize}.n${p.count}`,
510
+ cell: (p) => cellWith(p.containerSize, "rect", "none", sizes, p.count),
511
+ build(seed, p) {
512
+ const r = stepRandom(seed, "container");
513
+ const w = int(r, 800, 1400);
514
+ const h = int(r, 600, 1000);
515
+ return { container: containerOf(p.containerSize, w, h), obstacles: [], boxes: boxes(stepRandom(seed, "boxes"), p.count) };
516
+ },
517
+ });
518
+ }
519
+
520
+ /** The aspects of the thumbnails of a gallery. */
521
+ const THUMBNAIL_ASPECTS = [1, 4 / 3, 3 / 2, 16 / 9];
522
+
523
+ /** The generator `gallery`, version 1: thumbnails with one size and one aspect (1, 4:3, 3:2 or 16:9). The default container has a fixed width. */
524
+ export const gallery: CorpusGenerator<UiParams> = uiGenerator("gallery", "identical", 100, (r, count) => {
525
+ const aspect = THUMBNAIL_ASPECTS[int(r, 0, THUMBNAIL_ASPECTS.length - 1)]!;
526
+ const w = int(r, 120, 240);
527
+ return Array.from({ length: count }, (_, i) => ({ id: `b${i}`, w, h: round3(w / aspect) }));
528
+ });
529
+
530
+ /** The spans of the dashboard tiles in grid units, and their weights. */
531
+ const TILE_SPANS: readonly [number, number][] = [[1, 1], [2, 1], [1, 2], [2, 2], [3, 2], [4, 2]];
532
+ const TILE_WEIGHTS = [4, 3, 2, 3, 1, 1];
533
+
534
+ /** The generator `dashboard`, version 1: tiles in 6 standard sizes, from 1 by 1 to 4 by 2 grid units of 60 to 100. */
535
+ export const dashboard: CorpusGenerator<UiParams> = uiGenerator("dashboard", "bimodal", 10, (r, count) => {
536
+ const unit = int(r, 60, 100);
537
+ return Array.from({ length: count }, (_, i) => {
538
+ const [cw, ch] = TILE_SPANS[pick(r, TILE_WEIGHTS)]!;
539
+ return { id: `b${i}`, w: cw * unit, h: ch * unit };
540
+ });
541
+ });
542
+
543
+ /** The generator `cards`, version 1: cards with one width from 200 to 320, and a height from 120 to 420 each. */
544
+ export const cards: CorpusGenerator<UiParams> = uiGenerator("cards", "uniform", 100, (r, count) => {
545
+ const w = int(r, 200, 320);
546
+ return Array.from({ length: count }, (_, i) => ({ id: `b${i}`, w, h: int(r, 120, 420) }));
547
+ });
548
+
549
+ /** The font sizes of a word cloud, and their weights: many small words and few large words. */
550
+ const FONT_SIZES = [12, 14, 16, 20, 28, 40];
551
+ const FONT_WEIGHTS = [30, 25, 20, 12, 8, 5];
552
+
553
+ /** The generator `word-cloud`, version 1: boxes of text. The height comes from 6 font sizes, the width from 3 to 12 letters. */
554
+ export const wordCloud: CorpusGenerator<UiParams> = uiGenerator("word-cloud", "heavyTail", 100, (r, count) =>
555
+ Array.from({ length: count }, (_, i) => {
556
+ const font = FONT_SIZES[pick(r, FONT_WEIGHTS)]!;
557
+ const letters = int(r, 3, 12);
558
+ return { id: `b${i}`, w: round3(letters * font * 0.6), h: round3(font * 1.25) };
559
+ }),
560
+ );
561
+
562
+ // ---------------------------------------------------------------------------------------------------------------------
563
+ // `edge-cases`: the inputs at the limits.
564
+
565
+ /** The cases of `edge-cases`. */
566
+ export type EdgeCase = "empty" | "one" | "oversize" | "exactTiling" | "touchObstacles";
567
+
568
+ /** The parameters of `edge-cases`. The cases `empty` and `one` ignore `count`. */
569
+ export type EdgeCaseParams = { case: EdgeCase; count: number };
570
+
571
+ /** The values of `case`. */
572
+ const EDGE_CASES = ["empty", "one", "oversize", "exactTiling", "touchObstacles"] as const;
573
+
574
+ /** This function gives the largest divisor of `n` that is not more than the square root of `n`. */
575
+ const rowsOf = (n: number): number => {
576
+ for (let d = Math.floor(Math.sqrt(n)); d > 1; d--) if (n % d === 0) return d;
577
+ return 1;
578
+ };
579
+
580
+ /**
581
+ * The generator `edge-cases`, version 1. Each case has a fixed rectangle container.
582
+ *
583
+ * - `empty`: no box.
584
+ * - `one`: one box.
585
+ * - `oversize`: `count` boxes. One box is wider than the container.
586
+ * - `exactTiling`: `count` identical boxes that fill the container with no free space.
587
+ * - `touchObstacles`: a locked box on the top of an obstacle, and boxes as wide as the path between 2 obstacles.
588
+ */
589
+ export const edgeCases: CorpusGenerator<EdgeCaseParams> = defineGenerator<EdgeCaseParams>({
590
+ name: "edge-cases",
591
+ version: 1,
592
+ set: "corpus-v1",
593
+ defaults: { case: "oversize", count: 12 },
594
+ values: { case: EDGE_CASES },
595
+ family: (p) => (p.case === "empty" || p.case === "one" ? `edge-cases.${p.case}` : `edge-cases.${p.case}.n${p.count}`),
596
+ cell: (p) => {
597
+ switch (p.case) {
598
+ case "empty":
599
+ return cellWith("fixed", "rect", "none", "identical", 0);
600
+ case "one":
601
+ return cellWith("fixed", "rect", "none", "identical", 1);
602
+ case "oversize":
603
+ return cellWith("fixed", "rect", "none", "heavyTail", p.count);
604
+ case "exactTiling":
605
+ return cellWith("fixed", "rect", "none", "identical", p.count);
606
+ case "touchObstacles":
607
+ return cellWith("fixed", "rect", "corridors", "uniform", p.count, true);
608
+ }
609
+ },
610
+ build(seed, p) {
611
+ const needsBoxes = p.case === "oversize" || p.case === "exactTiling" || p.case === "touchObstacles";
612
+ if (needsBoxes && p.count < 1) throw new RangeError(`The case "${p.case}" of "edge-cases" needs a count of 1 or more.`);
613
+
614
+ let r = stepRandom(seed, "container");
615
+ let w: number;
616
+ let h: number;
617
+ let tile = { w: 0, h: 0, cols: 0, rows: 0 };
618
+ if (p.case === "exactTiling") {
619
+ // The container step also picks the size of the tile, because the tiles give the size of the container
620
+ const rows = rowsOf(p.count);
621
+ tile = { w: int(r, 40, 120), h: int(r, 30, 90), cols: p.count / rows, rows };
622
+ w = tile.cols * tile.w;
623
+ h = tile.rows * tile.h;
624
+ } else {
625
+ w = int(r, 600, 1000);
626
+ h = int(r, 400, 800);
627
+ }
628
+ const container: Container = { w, h, fixed: "size" };
629
+
630
+ r = stepRandom(seed, "obstacles");
631
+ const obstacles: Obstacle[] = [];
632
+ let path = 0;
633
+ let first = { x: 0, y: 0 };
634
+ if (p.case === "touchObstacles") {
635
+ path = int(r, 40, 100);
636
+ const ow = int(r, 60, 150);
637
+ const oh = int(r, 100, 200);
638
+ first = { x: span(r, w * 0.1, w * 0.25), y: span(r, h * 0.2, h * 0.4) };
639
+ obstacles.push({ id: "o0", region: { kind: "rect", x: first.x, y: first.y, w: ow, h: oh } });
640
+ obstacles.push({ id: "o1", region: { kind: "rect", x: first.x + ow + path, y: first.y, w: int(r, 60, 150), h: oh } });
641
+ }
642
+
643
+ r = stepRandom(seed, "boxes");
644
+ const boxes: Item[] = [];
645
+ switch (p.case) {
646
+ case "empty":
647
+ break;
648
+ case "one":
649
+ boxes.push({ id: "b0", w: int(r, 40, 200), h: int(r, 30, 150) });
650
+ break;
651
+ case "oversize": {
652
+ const big = int(r, 0, p.count - 1);
653
+ for (let i = 0; i < p.count; i++) {
654
+ if (i === big) boxes.push({ id: `b${i}`, w: w + int(r, 10, 200), h: int(r, 40, 100) });
655
+ else boxes.push({ id: `b${i}`, w: int(r, 20, 120), h: int(r, 20, 80) });
656
+ }
657
+ break;
658
+ }
659
+ case "exactTiling":
660
+ for (let i = 0; i < p.count; i++) boxes.push({ id: `b${i}`, w: tile.w, h: tile.h });
661
+ break;
662
+ case "touchObstacles": {
663
+ const lw = int(r, 30, 80);
664
+ const lh = int(r, 20, 60);
665
+ boxes.push({ id: "b0", w: lw, h: lh, x: first.x, y: first.y - lh, locked: true });
666
+ for (let i = 1; i < p.count; i++) boxes.push({ id: `b${i}`, w: path, h: int(r, 20, 80) });
667
+ break;
668
+ }
669
+ }
670
+ return { container, obstacles, boxes };
671
+ },
672
+ });
673
+
674
+ // ---------------------------------------------------------------------------------------------------------------------
675
+
676
+ /** Each generator of the package. */
677
+ export const generators: readonly CorpusGenerator[] = Object.freeze([rectScattered, rectGrid, gallery, dashboard, cards, wordCloud, edgeCases] as CorpusGenerator[]);
678
+
679
+ /** This function gives the generator with a name. It throws a `RangeError` for a name that the package does not have. */
680
+ export function generator(name: string): CorpusGenerator {
681
+ const found = generators.find((g) => g.name === name);
682
+ if (found === undefined) throw new RangeError(`The package has no generator "${name}".`);
683
+ return found;
684
+ }