@mark1russell7/layout-contract 0.2.13 → 0.2.15

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/build/index.js CHANGED
@@ -260,13 +260,63 @@ const DEFAULT_TOLERANCE = Object.freeze({
260
260
  through: .5
261
261
  });
262
262
  // src/input.ts
263
+ /**
264
+ * This function gives the point of a port on the border of its box, relative to the top-left corner of the box.
265
+ * A port with `x` and `y` gives that point. A port with `side` and `along` gives the point at that fraction of the side.
266
+ * Another port gives `undefined`, because the router picks its point.
267
+ */
268
+ function portPoint(box, port) {
269
+ if (port.x !== void 0 && port.y !== void 0) return {
270
+ x: port.x,
271
+ y: port.y
272
+ };
273
+ if (port.side === void 0 || port.along === void 0) return void 0;
274
+ switch (port.side) {
275
+ case "top": return {
276
+ x: port.along * box.w,
277
+ y: 0
278
+ };
279
+ case "bottom": return {
280
+ x: port.along * box.w,
281
+ y: box.h
282
+ };
283
+ case "left": return {
284
+ x: 0,
285
+ y: port.along * box.h
286
+ };
287
+ case "right": return {
288
+ x: box.w,
289
+ y: port.along * box.h
290
+ };
291
+ }
292
+ }
263
293
  /** This function gives the packer input of a file. An input without a container gets `{ fixed: "none" }`, so the packer picks the size. */
264
294
  function toPackInput(input) {
265
295
  return {
266
296
  container: input.container ?? { fixed: "none" },
267
297
  items: input.boxes,
268
298
  ...input.obstacles === void 0 ? {} : { obstacles: input.obstacles },
269
- ...input.gap === void 0 ? {} : { gap: input.gap }
299
+ ...input.gap === void 0 ? {} : { gap: input.gap },
300
+ ...input.pairGaps === void 0 ? {} : { pairGaps: input.pairGaps }
301
+ };
302
+ }
303
+ /** This function compares two strings by their UTF-16 code units, as `Array.prototype.sort` does without a compare function. */
304
+ const byCode$1 = (x, y) => x < y ? -1 : x > y ? 1 : 0;
305
+ /**
306
+ * This function gives the canonical form of an input. `documentHash` uses it for each `layout-input/1` document.
307
+ * Each pair of `pairGaps` gets `a` before `b`, and the list is sorted by `a` and then by `b`.
308
+ * An input without `pairGaps` is the same object, so its hash does not change.
309
+ */
310
+ function canonicalInput(input) {
311
+ if (!Array.isArray(input.pairGaps)) return input;
312
+ const pairGaps = input.pairGaps.map((p) => byCode$1(p.a, p.b) > 0 ? {
313
+ ...p,
314
+ a: p.b,
315
+ b: p.a
316
+ } : p).sort((x, y) => byCode$1(x.a, y.a) || byCode$1(x.b, y.b));
317
+ return {
318
+ ...input,
319
+ pairGaps
270
320
  };
271
321
  }
272
322
  // src/turn.ts
@@ -278,14 +328,20 @@ function turnSide(side) {
278
328
  function turnPoint(x, y, h) {
279
329
  return [h - y, x];
280
330
  }
281
- /** This function gives a port of a box of height `h` after the turn. Each value of the port turns, also a port with only `x` or only `y`. */
331
+ /**
332
+ * This function gives a port of a box of height `h` after the turn. Each value of the port turns, also a port with only `x` or only `y`.
333
+ * The start of a side is its top end or its left end. Thus `left` goes to `top` and `right` goes to `bottom` with `along` changed to `1 - along`.
334
+ * `top` goes to `right` and `bottom` goes to `left` with the same `along`.
335
+ */
282
336
  function turnPort(port, h) {
283
- const { side, x, y, ...rest } = port;
337
+ const { side, x, y, along, ...rest } = port;
338
+ const flips = side === "left" || side === "right";
284
339
  return {
285
340
  ...rest,
286
341
  ...side === void 0 ? {} : { side: turnSide(side) },
287
342
  ...y === void 0 ? {} : { x: h - y },
288
- ...x === void 0 ? {} : { y: x }
343
+ ...x === void 0 ? {} : { y: x },
344
+ ...along === void 0 ? {} : { along: flips ? 1 - along : along }
289
345
  };
290
346
  }
291
347
  /** This function gives a margin after the turn: the value of each side moves one side clockwise. */
@@ -423,20 +479,53 @@ function placedRect(box) {
423
479
  /**
424
480
  * This function counts the pairs of placed boxes whose insides, with their margins, overlap by more than `tolerance` on each axis.
425
481
  * A box with a shape counts as its bounding box. That area is larger, thus the count is safe.
482
+ *
483
+ * A pair of `pairGaps`, in either order, uses its `gap` in place of the two margins. One box grows by the gap, and the other box does not grow.
484
+ * Thus the pair breaks the rule only when the boxes are nearer than the gap on each axis. The other pairs do not change.
426
485
  */
427
- function countOverlaps(boxes, tolerance = DEFAULT_TOLERANCE.overlap) {
486
+ function countOverlaps(boxes, tolerance = DEFAULT_TOLERANCE.overlap, pairGaps) {
487
+ if (pairGaps === void 0 || pairGaps.length === 0) return marginOverlaps(boxes, tolerance, void 0);
488
+ const gaps = /* @__PURE__ */ new Map();
489
+ for (const p of pairGaps) {
490
+ const key = pairKey(p.a, p.b);
491
+ if (p.a !== p.b && !gaps.has(key)) gaps.set(key, p.gap);
492
+ }
493
+ const raw = /* @__PURE__ */ new Map();
494
+ for (const box of boxes) {
495
+ const r = placedRect(box);
496
+ if (r !== void 0 && !raw.has(box.id)) raw.set(box.id, r);
497
+ }
498
+ let count = marginOverlaps(boxes, tolerance, gaps);
499
+ for (const [key, gap] of gaps) {
500
+ const [a, b] = JSON.parse(key);
501
+ const ra = raw.get(a);
502
+ const rb = raw.get(b);
503
+ if (ra === void 0 || rb === void 0) continue;
504
+ const o = overlapOf(grow(ra, gap), rb);
505
+ if (o.x > tolerance && o.y > tolerance) count++;
506
+ }
507
+ return count;
508
+ }
509
+ /** This function gives the key of a pair of box ids, with the same key for each order. */
510
+ const pairKey = (a, b) => JSON.stringify(a < b ? [a, b] : [b, a]);
511
+ /** This function counts the overlaps of the boxes with their margins. It skips the pairs of `skip`. Without `skip`, it is the rule of 0.2.13. */
512
+ function marginOverlaps(boxes, tolerance, skip) {
428
513
  const rects = [];
429
514
  for (const box of boxes) {
430
515
  const r = placedRect(box);
431
- if (r !== void 0) rects.push(grow(r, box.margin));
516
+ if (r !== void 0) rects.push({
517
+ id: box.id,
518
+ r: grow(r, box.margin)
519
+ });
432
520
  }
433
- rects.sort((a, b) => a.x - b.x);
521
+ rects.sort((a, b) => a.r.x - b.r.x);
434
522
  let count = 0;
435
523
  for (let i = 0; i < rects.length; i++) {
436
- const a = rects[i];
524
+ const a = rects[i].r;
437
525
  for (let j = i + 1; j < rects.length; j++) {
438
- const b = rects[j];
526
+ const b = rects[j].r;
439
527
  if (b.x >= a.x + a.w - tolerance) break;
528
+ if (skip !== void 0 && skip.has(pairKey(rects[i].id, rects[j].id))) continue;
440
529
  const o = overlapOf(a, b);
441
530
  if (o.x > tolerance && o.y > tolerance) count++;
442
531
  }
@@ -671,7 +760,8 @@ function countObstacleHits(boxes, obstacles, tolerance = DEFAULT_TOLERANCE.overl
671
760
  /**
672
761
  * This function gives the count of each hard rule of the output of a packer.
673
762
  * It applies the output with `applyPack`. Only the placed boxes and the locked boxes count for the overlaps and the container.
674
- * `no-overlap` counts the pairs of boxes and the pairs of a box and an obstacle.
763
+ * `no-overlap` counts the pairs of boxes and the pairs of a box and an obstacle. A pair of `pairGaps` uses its gap in place of the margins.
764
+ *
675
765
  * A locked box without a placement, or with a move of more than `RECORD_EPS`, breaks `locked-kept`.
676
766
  * A locked box without a position in the input is a bad input, not a break. `checkInput` finds it.
677
767
  * Another box without a placement that is not in `overflow` breaks `complete`.
@@ -697,7 +787,7 @@ function packHard(input, output, tolerance = DEFAULT_TOLERANCE.overlap, options
697
787
  else if (!out.has(box.id)) incomplete++;
698
788
  }
699
789
  return {
700
- "no-overlap": countOverlaps(kept, tolerance) + countObstacleHits(kept, after.obstacles, tolerance),
790
+ "no-overlap": countOverlaps(kept, tolerance, input.pairGaps) + countObstacleHits(kept, after.obstacles, tolerance),
701
791
  "in-container": countOutside({
702
792
  ...after,
703
793
  boxes: kept
@@ -709,6 +799,8 @@ function packHard(input, output, tolerance = DEFAULT_TOLERANCE.overlap, options
709
799
  /**
710
800
  * This function gives the problems of an input file. An empty list tells that the file is good.
711
801
  * A problem is a bad input, not a break of a hard rule. Thus an engine refuses the input.
802
+ * In `pairGaps`, an unknown box, a pair of one box, a repeated pair in either order, a bad `gap` and bad `lanes` are problems.
803
+ * A port with `along` and no `side`, with `x` or `y`, or with a value outside [0, 1] is a problem.
712
804
  */
713
805
  function checkInput(input) {
714
806
  const problems = [];
@@ -720,9 +812,44 @@ function checkInput(input) {
720
812
  if (box.locked === true && (box.x === void 0 || box.y === void 0)) problems.push(`box ${box.id}: a locked box has no position`);
721
813
  const shape = shapeKindProblem(box);
722
814
  if (shape !== void 0) problems.push(shape);
815
+ for (const port of box.ports ?? []) problems.push(...alongProblems(box.id, port));
723
816
  }
724
817
  if (countPortsOffBorder(input.boxes) > 0) problems.push("a fixed port is not on the border of its box");
725
818
  for (const edge of input.edges ?? []) if (!ids.has(edge.source) || !ids.has(edge.target)) problems.push(`edge ${edge.id}: a box of the edge is not in the input`);
819
+ problems.push(...pairGapProblems(input.pairGaps, ids));
820
+ return problems;
821
+ }
822
+ /** This function gives the problems of the `along` of a port: no side, a value outside [0, 1], or `x` or `y` with it. A port without `along` has no problem. */
823
+ function alongProblems(boxId, port) {
824
+ if (port.along === void 0) return [];
825
+ const at = `box ${boxId} port ${port.id}`;
826
+ const problems = [];
827
+ if (port.side === void 0) problems.push(`${at}: a port with along has no side`);
828
+ if (typeof port.along !== "number" || !Number.isFinite(port.along) || port.along < 0 || port.along > 1) problems.push(`${at}: the along value ${String(port.along)} is not a finite number from 0 to 1`);
829
+ if (port.x !== void 0 || port.y !== void 0) problems.push(`${at}: a port with along also has x or y`);
830
+ return problems;
831
+ }
832
+ /** This function gives the problems of the `pairGaps` of an input. `ids` has the ids of the boxes. */
833
+ function pairGapProblems(pairGaps, ids) {
834
+ if (pairGaps === void 0) return [];
835
+ if (!Array.isArray(pairGaps)) return ["pairGaps: the value is not a list"];
836
+ const problems = [];
837
+ const seen = /* @__PURE__ */ new Map();
838
+ pairGaps.forEach((p, i) => {
839
+ const at = `pair gap ${i} (${String(p?.a)}, ${String(p?.b)})`;
840
+ if (typeof p !== "object" || p === null || typeof p.a !== "string" || typeof p.b !== "string") {
841
+ problems.push(`pair gap ${i}: the entry has no string ids a and b`);
842
+ return;
843
+ }
844
+ for (const id of [p.a, p.b]) if (!ids.has(id)) problems.push(`${at}: the box ${id} is not in the input`);
845
+ if (p.a === p.b) problems.push(`${at}: the two ids are the same box`);
846
+ if (typeof p.gap !== "number" || !Number.isFinite(p.gap) || p.gap < 0) problems.push(`${at}: the gap ${String(p.gap)} is not a finite number of 0 or more`);
847
+ if (p.lanes !== void 0 && (typeof p.lanes !== "number" || !Number.isInteger(p.lanes) || p.lanes < 0)) problems.push(`${at}: the lanes value ${String(p.lanes)} is not a whole number of 0 or more`);
848
+ const key = JSON.stringify(p.a < p.b ? [p.a, p.b] : [p.b, p.a]);
849
+ const first = seen.get(key);
850
+ if (first !== void 0) problems.push(`${at}: the pair is also pair gap ${first}`);
851
+ else seen.set(key, i);
852
+ });
726
853
  return problems;
727
854
  }
728
855
  // src/measures.ts
@@ -1287,10 +1414,11 @@ function outputHash(value, options = {}) {
1287
1414
  /**
1288
1415
  * This function gives the hash of a document. It uses the rule of `outputHash`, but the form does not include the field `hash` of the document.
1289
1416
  * With the default grid, it is the input hash of `docs/FORMAT.md` of `packing`.
1417
+ * A document with the schema `layout-input/1` goes through `canonicalInput` first. Thus the order of its `pairGaps` does not change the hash.
1290
1418
  */
1291
1419
  function documentHash(doc, options = {}) {
1292
1420
  const { hash: _hash, ...rest } = doc;
1293
- return outputHash(rest, options);
1421
+ return outputHash(rest.schema === "layout-input/1" ? canonicalInput(rest) : rest, options);
1294
1422
  }
1295
1423
  /**
1296
1424
  * This function gives the output groups of the candidates. The key of `hashes` is the id of a candidate.
@@ -1858,6 +1986,18 @@ function isActive(p, values) {
1858
1986
  const v = values[p.when.param];
1859
1987
  return p.when.in.some((x) => x === v);
1860
1988
  }
1989
+ /** This function gives the problem of a value of a param, or `undefined` for a value in its range. The corpus dimensions of `families.ts` use it. */
1990
+ function paramProblem(param, value) {
1991
+ return valueProblem(param, value);
1992
+ }
1993
+ /** This function tells if a param is active for the values of the earlier params. A param without `when` is always active. */
1994
+ function paramActive(param, values) {
1995
+ return isActive(param, values);
1996
+ }
1997
+ /** This function gives the stable form of a value of a param: a real number gets 12 significant digits, and `-0` becomes `0`. */
1998
+ function paramValue(param, value) {
1999
+ return normalize(param, value);
2000
+ }
1861
2001
  /** This function gives the part of a space that has a stage, and the prefix of the names of its stages. */
1862
2002
  function partOf(space, stage) {
1863
2003
  for (const part of space.parts ?? []) {
@@ -1943,6 +2083,8 @@ function optionProblems(stage, o) {
1943
2083
  if (o.members !== void 0) {
1944
2084
  const { min, max, ordered } = o.members;
1945
2085
  if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || min > max || typeof ordered !== "boolean") out.push(["members", `${at} has a member rule without whole counts 1 <= min <= max and a boolean ordered`]);
2086
+ const pool = o.members.pool;
2087
+ if (pool !== void 0 && (!Array.isArray(pool) || pool.length === 0 || !pool.every((entry) => Array.isArray(entry)))) out.push(["pool", `${at} has a pool that is not a list of one or more member lists`]);
1946
2088
  }
1947
2089
  return out;
1948
2090
  }
@@ -2053,8 +2195,39 @@ function memberProblems(part, rule, members, at, shapes) {
2053
2195
  if (ids.has(id)) out.push(["duplicate", `the members ${ids.get(id)} and ${k} of ${at} are the same config ${id}`]);
2054
2196
  else ids.set(id, k);
2055
2197
  });
2198
+ if (rule.pool !== void 0 && out.length === 0) {
2199
+ const key = memberKey(part, rule, members);
2200
+ if (!rule.pool.some((entry) => memberKey(part, rule, entry) === key)) out.push(["pool", `${at} has a member list that is not in its pool of ${rule.pool.length} ${rule.pool.length === 1 ? "list" : "lists"}`]);
2201
+ }
2202
+ return out;
2203
+ }
2204
+ /** This function gives the key of a good member list: the `configId` of each member, sorted when the rule is not ordered. */
2205
+ function memberKey(part, rule, members) {
2206
+ const ids = members.map((m) => idOf(canonicalOf(part, m, true)));
2207
+ if (!rule.ordered) ids.sort();
2208
+ return ids.join(",");
2209
+ }
2210
+ /** This function gives the problems of the pool of a meta option of a good space. `part` is the space of the members. */
2211
+ function poolProblems(part, rule, at) {
2212
+ const out = [];
2213
+ const keys = /* @__PURE__ */ new Map();
2214
+ const plain = {
2215
+ min: rule.min,
2216
+ max: rule.max,
2217
+ ordered: rule.ordered
2218
+ };
2219
+ (rule.pool ?? []).forEach((entry, j) => {
2220
+ const inner = memberProblems(part, plain, entry, `the pool entry ${j} of ${at}`);
2221
+ out.push(...inner);
2222
+ if (inner.length > 0) return;
2223
+ const key = memberKey(part, rule, entry);
2224
+ if (keys.has(key)) out.push(["duplicate", `the pool entries ${keys.get(key)} and ${j} of ${at} have the same members`]);
2225
+ else keys.set(key, j);
2226
+ });
2056
2227
  return out;
2057
2228
  }
2229
+ /** This function tells if a member list fits a space: each member is a good config of the space. A shape filter removes options from the space. */
2230
+ const fits = (part, entry) => entry.every((m) => configProblems(part, m, true).length === 0);
2058
2231
  const toBreaks = (problems, inputs = []) => problems.map(([claim, detail]) => ({
2059
2232
  claim,
2060
2233
  detail,
@@ -2063,6 +2236,8 @@ const toBreaks = (problems, inputs = []) => problems.map(([claim, detail]) => ({
2063
2236
  /**
2064
2237
  * This function checks one config of a space. It gives one break for each problem. An empty list tells that the config is good.
2065
2238
  * The `claim` of a break is the kind of the problem, for example `missing`, `unknown`, `inactive`, `param`, `constraint`, `members` or `depth`.
2239
+ *
2240
+ * A member list that is not an entry of the `pool` of its option gives the claim `pool`.
2066
2241
  * With `shapes`, an option that does not support a kind of the list gives the claim `shape`. A member of a meta option gets the same check.
2067
2242
  */
2068
2243
  function checkConfig(space, config, options = {}) {
@@ -2071,6 +2246,7 @@ function checkConfig(space, config, options = {}) {
2071
2246
  /**
2072
2247
  * This function checks a space. It gives one break for each problem. An empty list tells that the space is good.
2073
2248
  * It finds duplicate names, empty ranges, defaults out of range, enums without values, bad log scales, unknown names and bad named configs.
2249
+ * It also checks each entry of a `pool`: the count of its members, each member and a repeated entry.
2074
2250
  * The `inputs` of a break of a named config have the name of the config.
2075
2251
  */
2076
2252
  function checkSpace(space) {
@@ -2109,6 +2285,8 @@ function checkSpace(space) {
2109
2285
  }
2110
2286
  const breaks = toBreaks(out);
2111
2287
  if (breaks.length > 0) return breaks;
2288
+ for (const s of space.stages) for (const o of s.options) if (o.members?.pool !== void 0) breaks.push(...toBreaks(poolProblems(partOf(space, s.name).space, o.members, `the option ${s.name}.${o.id}`)));
2289
+ if (breaks.length > 0) return breaks;
2112
2290
  for (const [name, config] of Object.entries(space.named ?? {})) breaks.push(...toBreaks(configProblems(space, config, false).map(([c, d]) => [c, `the named config ${name}: ${d}`]), [name]));
2113
2291
  return breaks;
2114
2292
  }
@@ -2244,7 +2422,7 @@ function* gridParams(params, levels, i = 0, values = {}) {
2244
2422
  }
2245
2423
  const optionIds = (choices) => new Map([...choices].map(([k, o]) => [k, o?.id ?? null]));
2246
2424
  const keeps = (space, choices) => (space.constraints ?? []).every((c) => holds(c, optionIds(choices)));
2247
- /** This function gives the count of the member lists of a meta option from `base` base configs, or `null` above `limit`. */
2425
+ /** This function gives the count of the member lists of a meta option without a pool from `base` base configs, or `null` above `limit`. */
2248
2426
  function memberLists(base, rule, limit) {
2249
2427
  let sum = 0n;
2250
2428
  for (let m = rule.min; m <= rule.max; m++) {
@@ -2321,10 +2499,11 @@ function combos(space, levels, meta, limit) {
2321
2499
  for (const s of stages) for (const o of s.options) {
2322
2500
  if (o.members === void 0) continue;
2323
2501
  const choices = new Map(stages.map((x) => [x.name, x === s ? o : null]));
2502
+ const lists = o.members.pool === void 0 ? memberLists(base, o.members, limit) : BigInt(o.members.pool.length);
2324
2503
  if (keeps(space, choices)) out.push({
2325
2504
  choices,
2326
2505
  base: false,
2327
- weight: times(countOf(o), memberLists(base, o.members, limit), limit)
2506
+ weight: times(countOf(o), lists, limit)
2328
2507
  });
2329
2508
  }
2330
2509
  return out;
@@ -2332,6 +2511,7 @@ function combos(space, levels, meta, limit) {
2332
2511
  /**
2333
2512
  * This function gives the count of the grid points of a space, with `levels` levels for each numeric param.
2334
2513
  * It counts only the active params and the combinations that keep the constraints. It gives the meta configs apart.
2514
+ * A meta option with a `pool` counts each entry of the pool one time.
2335
2515
  * With `shapes`, it counts only the options that support each kind of the list.
2336
2516
  */
2337
2517
  function spaceSize(space, options) {
@@ -2343,10 +2523,11 @@ function spaceSize(space, options) {
2343
2523
  /**
2344
2524
  * This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
2345
2525
  * A stage can lose all of its options. Then the space has no config with that stage.
2526
+ * A pool keeps only the entries whose members fit the smaller space. A meta option without such an entry goes out.
2346
2527
  */
2347
2528
  function restrict(space, shapes) {
2348
2529
  if (shapes === void 0) return space;
2349
- return {
2530
+ const kept = {
2350
2531
  ...space,
2351
2532
  stages: space.stages.map((s) => ({
2352
2533
  ...s,
@@ -2354,6 +2535,25 @@ function restrict(space, shapes) {
2354
2535
  })),
2355
2536
  ...space.parts === void 0 ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }
2356
2537
  };
2538
+ const withPool = (s, o) => {
2539
+ if (o.members?.pool === void 0) return [o];
2540
+ const part = partOf(kept, s.name).space;
2541
+ const pool = o.members.pool.filter((entry) => fits(part, entry));
2542
+ return pool.length === 0 ? [] : [{
2543
+ ...o,
2544
+ members: {
2545
+ ...o.members,
2546
+ pool
2547
+ }
2548
+ }];
2549
+ };
2550
+ return {
2551
+ ...kept,
2552
+ stages: kept.stages.map((s) => ({
2553
+ ...s,
2554
+ options: s.options.flatMap((o) => withPool(s, o))
2555
+ }))
2556
+ };
2357
2557
  }
2358
2558
  /** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
2359
2559
  function assertShapes(shapes) {
@@ -2473,6 +2673,14 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
2473
2673
  });
2474
2674
  }
2475
2675
  if (o.members === void 0) continue;
2676
+ const fixed = o.members.pool;
2677
+ if (fixed !== void 0) {
2678
+ const r = stepRandom(seed, `pool-entry:${s.name}:${o.id}`);
2679
+ permutation(users.length, r).forEach((stratum, j) => {
2680
+ members[users[j]][k] = [...fixed[Math.min(fixed.length - 1, Math.floor((stratum + r()) / users.length * fixed.length))]];
2681
+ });
2682
+ continue;
2683
+ }
2476
2684
  const part = partOf(space, s.name).space;
2477
2685
  const poolSize = Math.max(n, 4 * o.members.max, 8);
2478
2686
  const seen = /* @__PURE__ */ new Set();
@@ -2516,6 +2724,7 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
2516
2724
  * - The mode `sample` gives `n` configs of a seeded Latin hypercube. The options of a stage get equal strata, and each active param gets its own strata.
2517
2725
  * - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
2518
2726
  * - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
2727
+ * - A meta option with a `pool` gives only the entries of its pool. With `shapes`, only the entries whose members support each kind count.
2519
2728
  */
2520
2729
  function enumerateSpace(space, options) {
2521
2730
  assertSpace(space);
@@ -2831,6 +3040,761 @@ function verdict(records, options) {
2831
3040
  dropped
2832
3041
  };
2833
3042
  }
2834
- export { DEFAULT_SHAPES, DEFAULT_TOLERANCE, EDGE_DENSITY, EDGE_STEP_VERSION, EPS, FAMILY_ANY_KIND, JOINT_MEASURES, JOINT_MOVED_MIN, PACK_HARD_RULES, RECORD_EPS, RECORD_SCALE, SHAPE_KINDS, SHAPE_KIND_IDS, SIDES, SPLIT_SCHEMA, STRATEGY_SPACE_SCHEMA, VERDICT_DRAWS, VERDICT_EXACT_CAP, VERDICT_EXACT_MAX, VERDICT_RUNS, VERDICT_SEED, applyPack, boundsOf, canonicalConfig, canonicalJson, checkConfig, checkDeclared, checkInput, checkShapes, checkSpace, checkSplit, compareMeasure, configId, corpusHashOf, countObstacleHits, countOutside, countOverlaps, countPortsOffBorder, declaredClaimOf, declaredCounts, documentHash, edgeStep, enumerateSpace, exercised, familyOf, fnv1a32, grow, isShapeKind, itemShapeOf, joinConfigs, leaveOneSeedOut, marginSides, minEffectOf, mulberry32, outputGroups, outputHash, overlapOf, packHard, paramLevels, pickMoves, placedRect, portfolio, productSpace, randomInt, rectInRegion, regionHitsRect, selectBest, sha256Hex, shapeKindOf, shapeKindProblem, shapeKindsOf, shapesOf, spaceShapeKinds, spaceSize, splitConfig, splitFileName, splitFolds, stepRandom, toLayoutRecord, toPackInput, translate, turnBox, turnMargin, turnPoint, turnPort, turnRegion, turnSide, verdict };
3043
+ // src/families.ts
3044
+ /** The name of the output field that lists the fields that `generate` of a registry removed. */
3045
+ const INVALIDATED_FIELD = "invalidated";
3046
+ /** The random numbers of the default factory: `mulberry32` of `random.ts`. Its stream is pinned in `families.test.ts`. */
3047
+ const DEFAULT_RNG = mulberry32;
3048
+ /** This function gives the `kind` of the cells of a version of a family: `<name>@<version>`, for example `rect-grid@2`. Thus two versions do not share a family key. */
3049
+ function familyKind(name, version) {
3050
+ return `${name}@${version}`;
3051
+ }
3052
+ /** The default key of a family in a manifest: the kind of the cell alone. It is the map form of `FamilyBy` with the entry `"*"`. */
3053
+ const DEFAULT_FAMILY_BY = Object.freeze({ "*": Object.freeze([]) });
3054
+ /** This function gives an `enum` dimension. */
3055
+ const choice = (name, values, value, meaning, engines) => Object.freeze({
3056
+ name,
3057
+ kind: "enum",
3058
+ values: Object.freeze([...values]),
3059
+ default: value,
3060
+ meaning,
3061
+ engines: Object.freeze([...engines])
3062
+ });
3063
+ /** This function gives an `int` or a `real` dimension. */
3064
+ const range = (name, kind, limits, meaning, engines) => Object.freeze({
3065
+ name,
3066
+ kind,
3067
+ min: limits.min,
3068
+ max: limits.max,
3069
+ default: limits.value,
3070
+ scale: limits.scale ?? "linear",
3071
+ ...limits.levels === void 0 ? {} : { levels: Object.freeze([...limits.levels]) },
3072
+ meaning,
3073
+ engines: Object.freeze([...engines])
3074
+ });
3075
+ const P = "packing";
3076
+ const H = "highways";
3077
+ const J = "joint";
3078
+ /**
3079
+ * The shared dimension table. A family takes a dimension of it with `sharedDimension`, or declares its own dimension.
3080
+ * The coverage table of each dimension and each engine is in `docs/CORPUS.md`.
3081
+ */
3082
+ const CORPUS_DIMENSIONS = Object.freeze({
3083
+ count: range("count", "int", {
3084
+ min: 0,
3085
+ max: 1e5,
3086
+ value: 100,
3087
+ scale: "log",
3088
+ levels: [
3089
+ 1,
3090
+ 10,
3091
+ 100,
3092
+ 1e3,
3093
+ 1e4
3094
+ ]
3095
+ }, "The count of the items (n) of an input.", [
3096
+ P,
3097
+ H,
3098
+ J
3099
+ ]),
3100
+ sizes: choice("sizes", [
3101
+ "identical",
3102
+ "nearIdentical",
3103
+ "uniform",
3104
+ "heavyTail",
3105
+ "bimodal",
3106
+ "extremeAspect"
3107
+ ], "uniform", "The distribution of the sizes of the items.", [P, J]),
3108
+ aspect: range("aspect", "real", {
3109
+ min: .25,
3110
+ max: 4,
3111
+ value: 1,
3112
+ scale: "log"
3113
+ }, "The width over the height of a container with no fixed size.", [P]),
3114
+ density: range("density", "real", {
3115
+ min: .05,
3116
+ max: 1,
3117
+ value: .4,
3118
+ scale: "linear"
3119
+ }, "The area of the items over the area of the frame of the container.", [P]),
3120
+ obstacles: choice("obstacles", [
3121
+ "none",
3122
+ "center",
3123
+ "scattered",
3124
+ "wall",
3125
+ "corridors",
3126
+ "touching",
3127
+ "edge"
3128
+ ], "none", "The layout of the fixed obstacles.", [P, J]),
3129
+ containerSize: choice("containerSize", [
3130
+ "fixed",
3131
+ "freeHeight",
3132
+ "freeWidth",
3133
+ "freeAspect",
3134
+ "freeArea"
3135
+ ], "fixed", "The sides of the container that the engine can change.", [P, J]),
3136
+ containerShape: choice("containerShape", [
3137
+ "rect",
3138
+ "convex",
3139
+ "polygon",
3140
+ "circle",
3141
+ "ellipse",
3142
+ "holes"
3143
+ ], "rect", "The kind of the shape of the container.", [P, J]),
3144
+ shape: choice("shape", SHAPE_KIND_IDS, "rect", "The shape kind of each item: a `ShapeKind` of `shapes.ts`.", [P]),
3145
+ order: choice("order", ["free", "soft"], "free", "The reading order of the items: no order, or a rank on each item.", [P]),
3146
+ groups: choice("groups", [
3147
+ "none",
3148
+ "flat",
3149
+ "nested"
3150
+ ], "none", "The groups of the items: no groups, one level, or groups in groups.", [H, J]),
3151
+ edgeDensity: choice("edgeDensity", [
3152
+ "sparse",
3153
+ "medium",
3154
+ "dense"
3155
+ ], "medium", "The mean count of the edges of each item: 0.5, 1.5 or 4.", [H, J]),
3156
+ edgeShape: choice("edgeShape", [
3157
+ "random",
3158
+ "clustered",
3159
+ "hubChain",
3160
+ "hubs",
3161
+ "longRange",
3162
+ "tree",
3163
+ "bundled"
3164
+ ], "random", "The topology of the edges.", [H, J]),
3165
+ ports: choice("ports", [
3166
+ "free",
3167
+ "sides",
3168
+ "fixed"
3169
+ ], "free", "The ports of the edges: free, a side for each end, or fixed ports.", [H, J]),
3170
+ hubBy: Object.freeze({
3171
+ ...choice("hubBy", ["area", "random"], "area", "The order of the hubs of the shapes hubs and tree: the largest item first, or random.", [J]),
3172
+ when: Object.freeze({
3173
+ param: "edgeShape",
3174
+ in: Object.freeze(["hubs", "tree"])
3175
+ })
3176
+ }),
3177
+ spacing: choice("spacing", [
3178
+ "stress",
3179
+ "gap",
3180
+ "margin",
3181
+ "both"
3182
+ ], "both", "The split of the route clearance between the margins and the gap. `stress` has neither.", [J]),
3183
+ margin: range("margin", "real", {
3184
+ min: 0,
3185
+ max: 32,
3186
+ value: 0
3187
+ }, "The margin of each item, in units.", [P, J]),
3188
+ gap: range("gap", "real", {
3189
+ min: 0,
3190
+ max: 32,
3191
+ value: 0
3192
+ }, "The free space between two items, in units.", [P, J])
3193
+ });
3194
+ /**
3195
+ * This function gives a dimension of `CORPUS_DIMENSIONS`, with the fields of `patch` over it. It throws a `RangeError` for an unknown name.
3196
+ * With `supported` or `invalidates` in the patch, it also throws a `RangeError` for a bad dimension, as `ownerDimension` does.
3197
+ */
3198
+ function sharedDimension(name, patch = {}) {
3199
+ const base = Object.hasOwn(CORPUS_DIMENSIONS, name) ? CORPUS_DIMENSIONS[name] : void 0;
3200
+ if (base === void 0) throw new RangeError(`The shared dimension table has no dimension "${name}".`);
3201
+ const dim = Object.freeze({
3202
+ ...base,
3203
+ ...patch,
3204
+ name
3205
+ });
3206
+ if (patch.supported !== void 0 || patch.invalidates !== void 0) throwProblems(`sharedDimension: the dimension "${name}"`, ownProblems(dim));
3207
+ return dim;
3208
+ }
3209
+ /**
3210
+ * This function gives a dimension that only its owner declares, for example `cut` of a family of `packing`.
3211
+ * It throws a `RangeError` for a name of `CORPUS_DIMENSIONS`, the reserved name `kind`, or a bad default, level, `supported` or `invalidates`.
3212
+ * The registry examines the `when` of the dimension, because it needs the earlier dimensions.
3213
+ */
3214
+ function ownerDimension(name, spec) {
3215
+ const where = `ownerDimension: the dimension "${name}"`;
3216
+ if (typeof name !== "string" || name === "") throw new RangeError("ownerDimension: the name of a dimension is a text that is not empty.");
3217
+ if (name === "kind") throw new RangeError(`${where} has a reserved name: the cell of \`cellOf\` has the \`familyKind\` of the family in \`kind\`.`);
3218
+ if (Object.hasOwn(CORPUS_DIMENSIONS, name)) throw new RangeError(`${where} is in the shared dimension table. Use sharedDimension("${name}") for it.`);
3219
+ const dim = Object.freeze({
3220
+ ...spec,
3221
+ name
3222
+ });
3223
+ throwProblems(where, ownProblems(dim));
3224
+ return dim;
3225
+ }
3226
+ /** This function throws a `RangeError` with each problem of a list. An empty list throws nothing. */
3227
+ function throwProblems(where, problems) {
3228
+ if (problems.length > 0) throw new RangeError(`${where} is not good: ${problems.join("; ")}.`);
3229
+ }
3230
+ /** This function tells if a value of the stable form of a dimension is in its `supported` values. Without `supported`, each value is in them. */
3231
+ function inSupported(dim, value) {
3232
+ const s = dim.supported;
3233
+ if (s === void 0) return true;
3234
+ if (isSupportedList(s)) return s.some((v) => paramValue(dim, v) === value);
3235
+ if (typeof value !== "number") return false;
3236
+ return (s.min === void 0 || value >= s.min) && (s.max === void 0 || value <= s.max);
3237
+ }
3238
+ /** This function tells if `SupportedValues` has the list form. */
3239
+ const isSupportedList = (s) => Array.isArray(s);
3240
+ /**
3241
+ * This function tells if a family accepts a value of a dimension. A site uses it to show the values that a family does not support.
3242
+ * It gives `false` for a bad value, and for a value outside `supported`. Without `supported`, it gives `true` for each good value.
3243
+ */
3244
+ function dimensionSupports(dim, value) {
3245
+ if (paramProblem(dim, value) !== void 0) return false;
3246
+ return inSupported(dim, paramValue(dim, value));
3247
+ }
3248
+ /** This function gives the text of the supported values of a dimension, for a message. */
3249
+ function supportedText(dim) {
3250
+ const s = dim.supported;
3251
+ if (s === void 0) return "each value";
3252
+ if (isSupportedList(s)) return s.map((v) => JSON.stringify(v)).join(", ");
3253
+ return `the values from ${s.min ?? dim.min} to ${s.max ?? dim.max}`;
3254
+ }
3255
+ /** This function gives the problems of one dimension that need no other dimension: the default, the levels, `supported` and `invalidates`. */
3256
+ function ownProblems(d) {
3257
+ const out = [];
3258
+ const bad = paramProblem(d, d.default);
3259
+ if (bad !== void 0) out.push(`the default of "${d.name}" is bad: ${bad}`);
3260
+ for (const v of d.levels ?? []) {
3261
+ const p = paramProblem(d, v);
3262
+ if (p !== void 0) out.push(`a level of "${d.name}" is bad: ${p}`);
3263
+ }
3264
+ out.push(...supportProblems(d, bad === void 0));
3265
+ out.push(...invalidatesProblems(d));
3266
+ return out;
3267
+ }
3268
+ /** This function gives the problems of the `supported` values of a dimension. `goodDefault` tells that the default is a good value. */
3269
+ function supportProblems(d, goodDefault) {
3270
+ const s = d.supported;
3271
+ if (s === void 0) return [];
3272
+ const out = [];
3273
+ const numeric = d.kind === "int" || d.kind === "real";
3274
+ if (isSupportedList(s)) {
3275
+ if (numeric) return [`the supported values of "${d.name}" are a range with min and max, because the dimension is numeric`];
3276
+ if (s.length === 0) return [`the supported values of "${d.name}" are an empty list`];
3277
+ for (const v of s) {
3278
+ const p = paramProblem(d, v);
3279
+ if (p !== void 0) out.push(`a supported value of "${d.name}" is bad: ${p}`);
3280
+ }
3281
+ if (new Set(s).size !== s.length) out.push(`the supported values of "${d.name}" have a value two times`);
3282
+ } else {
3283
+ if (s === null || typeof s !== "object") return [`the supported values of "${d.name}" are a list or a range`];
3284
+ if (!numeric) return [`the supported values of "${d.name}" are a list, because the dimension is not numeric`];
3285
+ for (const end of [s.min, s.max]) {
3286
+ if (end === void 0) continue;
3287
+ const p = paramProblem(d, end);
3288
+ if (p !== void 0) out.push(`an end of the supported range of "${d.name}" is bad: ${p}`);
3289
+ }
3290
+ if (s.min !== void 0 && s.max !== void 0 && s.min > s.max) out.push(`the supported range of "${d.name}" has a min above its max`);
3291
+ }
3292
+ if (out.length > 0) return out;
3293
+ if (goodDefault && !inSupported(d, paramValue(d, d.default))) out.push(`the default of "${d.name}" is not a supported value`);
3294
+ for (const v of d.levels ?? []) if (paramProblem(d, v) === void 0 && !inSupported(d, paramValue(d, v))) out.push(`the level ${JSON.stringify(v)} of "${d.name}" is not a supported value`);
3295
+ return out;
3296
+ }
3297
+ /** This function gives the problems of the `invalidates` fields of a dimension. */
3298
+ function invalidatesProblems(d) {
3299
+ const list = d.invalidates;
3300
+ if (list === void 0) return [];
3301
+ if (!Array.isArray(list) || !list.every((f) => typeof f === "string" && f !== "")) return [`the invalidates of "${d.name}" is not a list of field names`];
3302
+ const out = [];
3303
+ if (new Set(list).size !== list.length) out.push(`the invalidates of "${d.name}" has a field two times`);
3304
+ for (const f of ["hash", INVALIDATED_FIELD]) if (list.includes(f)) out.push(`the invalidates of "${d.name}" has the field "${f}", which the registry writes`);
3305
+ return out;
3306
+ }
3307
+ /** This function tells if a log scale of a range starts at 0. Then the scale is the log of `1 + x - min`. */
3308
+ const fromZero = (d) => d.scale === "log" && (d.min ?? 0) <= 0;
3309
+ /**
3310
+ * This function gives the value at the fraction `t` of the range of a numeric dimension, on its scale. A slider of a site uses it.
3311
+ * A log scale with a `min` of 0 or less is the log of `1 + x - min`. An `int` dimension gives a whole number.
3312
+ */
3313
+ function dimensionAt(dim, t) {
3314
+ const min = dim.min ?? 0;
3315
+ const max = dim.max ?? min;
3316
+ const u = Math.min(1, Math.max(0, t));
3317
+ let v;
3318
+ if (dim.scale !== "log") v = min + u * (max - min);
3319
+ else if (fromZero(dim)) v = min + Math.expm1(u * Math.log1p(max - min));
3320
+ else v = Math.exp(Math.log(min) + u * (Math.log(max) - Math.log(min)));
3321
+ v = Math.min(max, Math.max(min, v));
3322
+ return dim.kind === "int" ? Math.round(v) : paramValue(dim, v);
3323
+ }
3324
+ /** This function gives the fraction of the range of a numeric dimension at a value, on its scale. It is the inverse of `dimensionAt`. */
3325
+ function dimensionUnit(dim, value) {
3326
+ const min = dim.min ?? 0;
3327
+ const max = dim.max ?? min;
3328
+ if (max === min) return 0;
3329
+ const v = Math.min(max, Math.max(min, value));
3330
+ if (dim.scale !== "log") return (v - min) / (max - min);
3331
+ if (fromZero(dim)) return Math.log1p(v - min) / Math.log1p(max - min);
3332
+ return (Math.log(v) - Math.log(min)) / (Math.log(max) - Math.log(min));
3333
+ }
3334
+ /** This function gives the range of the supported values of a numeric dimension: its own `min` and `max` in place of the ends of `supported`. */
3335
+ function supportedSpan(dim) {
3336
+ const s = dim.supported;
3337
+ const range = s === void 0 || isSupportedList(s) ? {} : s;
3338
+ return {
3339
+ min: range.min ?? dim.min,
3340
+ max: range.max ?? dim.max
3341
+ };
3342
+ }
3343
+ /**
3344
+ * This function gives the values of a sweep of a dimension. `levels` of the dimension come first. Then an `enum` gives its values, and a `bool` gives `false` and `true`.
3345
+ * With `steps`, a numeric dimension gives `steps` points from `min` to `max` on its scale. Else it gives its default.
3346
+ * With `supported`, an `enum` and a `bool` give only the supported values, and the points of a numeric dimension stay in the supported range.
3347
+ */
3348
+ function levelsOf(dim, steps) {
3349
+ if (dim.levels !== void 0) return [...dim.levels];
3350
+ if (dim.kind === "enum" || dim.kind === "bool") return paramLevels(dim, 1).filter((v) => inSupported(dim, paramValue(dim, v)));
3351
+ if (steps === void 0) return paramLevels(dim, 1);
3352
+ if (dim.supported !== void 0) {
3353
+ const { supported: _supported, ...rest } = dim;
3354
+ const span = supportedSpan(dim);
3355
+ return levelsOf({
3356
+ ...rest,
3357
+ ...span.min === void 0 ? {} : { min: span.min },
3358
+ ...span.max === void 0 ? {} : { max: span.max }
3359
+ }, steps);
3360
+ }
3361
+ if (!fromZero(dim)) return paramLevels(dim, steps);
3362
+ if (!Number.isInteger(steps) || steps < 1) throw new RangeError(`The steps of a sweep are a whole number of 1 or more, not ${steps}.`);
3363
+ if (steps === 1) return [paramValue(dim, dim.default)];
3364
+ return [...new Set(Array.from({ length: steps }, (_, j) => dimensionAt(dim, j / (steps - 1))))];
3365
+ }
3366
+ /** This function gives the problems of the dimensions of a family: a repeated name, a bad default, level, `supported`, `invalidates` or `when`. */
3367
+ function dimensionProblems(dims) {
3368
+ const out = [];
3369
+ const names = dims.map((d) => d.name);
3370
+ dims.forEach((d, i) => {
3371
+ if (names.indexOf(d.name) !== i) out.push(`the dimension "${d.name}" is in the list two times`);
3372
+ out.push(...ownProblems(d));
3373
+ if (d.when === void 0) return;
3374
+ const j = names.indexOf(d.when.param);
3375
+ if (j < 0 || j >= i) out.push(`the when of "${d.name}" names "${d.when.param}", which is not an earlier dimension`);
3376
+ else if (d.when.in.length === 0) out.push(`the when of "${d.name}" has no values`);
3377
+ else for (const v of d.when.in) if (paramProblem(dims[j], v) !== void 0) out.push(`the when of "${d.name}" has a value that "${d.when.param}" does not have`);
3378
+ });
3379
+ return out;
3380
+ }
3381
+ /**
3382
+ * This function gives the full dimension values of an input. A missing dimension gets its default. An inactive dimension of `when` also gets its default.
3383
+ * Thus a default that the caller gives, a missing value and an inactive value give the same values. A real number gets its stable form.
3384
+ *
3385
+ * The function throws a `RangeError` for an unknown dimension or a bad value, also for a bad value of an inactive dimension.
3386
+ * It also throws a `RangeError` for a value outside `supported`, with the text "not supported by" and `by`.
3387
+ */
3388
+ function resolveDimensions(dimensions, dims = {}, where = "The family", by = "the family") {
3389
+ const known = new Set(dimensions.map((d) => d.name));
3390
+ for (const key of Object.keys(dims)) if (!known.has(key)) throw new RangeError(`${where} has no dimension "${key}".`);
3391
+ const active = {};
3392
+ const out = {};
3393
+ for (const d of dimensions) {
3394
+ const given = Object.hasOwn(dims, d.name) ? dims[d.name] : void 0;
3395
+ if (given !== void 0) {
3396
+ const bad = paramProblem(d, given);
3397
+ if (bad !== void 0) throw new RangeError(`${where}: the dimension "${d.name}" is bad: ${bad}.`);
3398
+ if (!inSupported(d, paramValue(d, given))) throw new RangeError(`${where}: the value ${JSON.stringify(given)} of the dimension "${d.name}" is not supported by ${by}. The supported values are ${supportedText(d)}.`);
3399
+ }
3400
+ const on = paramActive(d, active);
3401
+ const value = paramValue(d, on && given !== void 0 ? given : d.default);
3402
+ if (on) active[d.name] = value;
3403
+ out[d.name] = value;
3404
+ }
3405
+ return out;
3406
+ }
3407
+ /** This function gives the values of the active dimensions only. */
3408
+ function activeValues(dimensions, full) {
3409
+ const active = {};
3410
+ for (const d of dimensions) if (paramActive(d, active)) active[d.name] = full[d.name];
3411
+ return active;
3412
+ }
3413
+ /** This function gives the active values of each cell of a sweep, in the order of the dimensions. The last dimension changes first. */
3414
+ function* sweepOf(dimensions, levels, i = 0, values = {}) {
3415
+ if (i === dimensions.length) {
3416
+ yield { ...values };
3417
+ return;
3418
+ }
3419
+ const d = dimensions[i];
3420
+ const lv = levels[i];
3421
+ if (lv === void 0 || !paramActive(d, values)) {
3422
+ yield* sweepOf(dimensions, levels, i + 1, paramActive(d, values) ? {
3423
+ ...values,
3424
+ [d.name]: paramValue(d, d.default)
3425
+ } : values);
3426
+ return;
3427
+ }
3428
+ for (const v of lv) yield* sweepOf(dimensions, levels, i + 1, {
3429
+ ...values,
3430
+ [d.name]: paramValue(d, v)
3431
+ });
3432
+ }
3433
+ /** This function counts the cells of a sweep with no list of the cells. A dimension that a later `when` reads splits the count. */
3434
+ function countOf(dimensions, levels, i = 0, values = {}) {
3435
+ if (i === dimensions.length) return 1;
3436
+ const d = dimensions[i];
3437
+ if (!paramActive(d, values)) return countOf(dimensions, levels, i + 1, values);
3438
+ const lv = levels[i] ?? [d.default];
3439
+ if (!dimensions.slice(i + 1).some((q) => q.when?.param === d.name)) return lv.length * countOf(dimensions, levels, i + 1, values);
3440
+ let sum = 0;
3441
+ for (const v of lv) sum += countOf(dimensions, levels, i + 1, {
3442
+ ...values,
3443
+ [d.name]: paramValue(d, v)
3444
+ });
3445
+ return sum;
3446
+ }
3447
+ /** This function gives the levels of each dimension of a sweep. A dimension outside the sweep has `undefined`, so it keeps its default. */
3448
+ function sweepLevels(dimensions, options, where, by) {
3449
+ const names = new Set(dimensions.map((d) => d.name));
3450
+ for (const name of [...options.sweep ?? [], ...Object.keys(options.levels ?? {})]) if (!names.has(name)) throw new RangeError(`${where} has no dimension "${name}".`);
3451
+ return dimensions.map((d) => {
3452
+ if (options.sweep !== void 0 && !options.sweep.includes(d.name)) return void 0;
3453
+ const lv = options.levels !== void 0 && Object.hasOwn(options.levels, d.name) ? options.levels[d.name] : levelsOf(d);
3454
+ for (const v of lv) {
3455
+ const bad = paramProblem(d, v);
3456
+ if (bad !== void 0) throw new RangeError(`${where}: a level of "${d.name}" is bad: ${bad}.`);
3457
+ if (!inSupported(d, paramValue(d, v))) throw new RangeError(`${where}: the level ${JSON.stringify(v)} of "${d.name}" is not supported by ${by}.`);
3458
+ }
3459
+ return lv;
3460
+ });
3461
+ }
3462
+ /** This function gives the output fields that the values make wrong: the `invalidates` of each dimension with a value other than its default, sorted. */
3463
+ function invalidFields(dimensions, full) {
3464
+ const out = /* @__PURE__ */ new Set();
3465
+ for (const d of dimensions) {
3466
+ if (d.invalidates === void 0 || d.invalidates.length === 0) continue;
3467
+ if (full[d.name] !== paramValue(d, d.default)) for (const f of d.invalidates) out.add(f);
3468
+ }
3469
+ return [...out].sort(byCode);
3470
+ }
3471
+ /** This function compares two strings by their UTF-16 code units. */
3472
+ const byCode = (x, y) => x < y ? -1 : x > y ? 1 : 0;
3473
+ /**
3474
+ * This function removes the fields of `fields` from an output, and adds their names to its field `invalidated`, sorted.
3475
+ * Only a field of the output counts. The `hash` of text gets the value of `hash` again. An output with none of the fields is the same object.
3476
+ */
3477
+ function dropFields(output, fields, hash, where) {
3478
+ if (fields.length === 0) return output;
3479
+ if (output === null || typeof output !== "object" || Array.isArray(output)) throw new RangeError(`${where} invalidates the fields ${fields.join(", ")}, but its output is not an object.`);
3480
+ const o = output;
3481
+ const dropped = fields.filter((f) => Object.hasOwn(o, f));
3482
+ if (dropped.length === 0) return output;
3483
+ const before = o[INVALIDATED_FIELD];
3484
+ const prior = Array.isArray(before) ? before.filter((f) => typeof f === "string") : [];
3485
+ const body = {};
3486
+ for (const key of Object.keys(o)) if (key !== "hash" && key !== "invalidated" && !dropped.includes(key)) body[key] = o[key];
3487
+ body[INVALIDATED_FIELD] = [.../* @__PURE__ */ new Set([...prior, ...dropped])].sort(byCode);
3488
+ if (typeof o["hash"] !== "string") return Object.hasOwn(o, "hash") ? {
3489
+ ...body,
3490
+ hash: o["hash"]
3491
+ } : body;
3492
+ return {
3493
+ ...body,
3494
+ hash: hash(body)
3495
+ };
3496
+ }
3497
+ /** This function gives the values that make a dimension active: the first value of its `when`, and the values that make that dimension active. */
3498
+ function activation(dimensions, dim) {
3499
+ const out = {};
3500
+ let d = dim;
3501
+ while (d?.when !== void 0) {
3502
+ const param = d.when.param;
3503
+ out[param] = d.when.in[0];
3504
+ d = dimensions.find((q) => q.name === param);
3505
+ }
3506
+ return out;
3507
+ }
3508
+ /** This function throws a `RangeError` when a seed is not a whole number of 0 or more. */
3509
+ function checkSeed(seed) {
3510
+ if (!Number.isSafeInteger(seed) || seed < 0) throw new RangeError(`The seed is a whole number of 0 or more, not ${seed}.`);
3511
+ }
3512
+ /** This function gives a new registry with the families of the list. Each engine gives the list of its families. An import registers nothing. */
3513
+ function createFamilyRegistry(families = []) {
3514
+ const list = [];
3515
+ const label = (f) => `The family "${f.name}" version ${f.version}`;
3516
+ const find = (name, version) => {
3517
+ const named = list.filter((f) => f.name === name);
3518
+ if (named.length === 0) throw new RangeError(`The registry has no family "${name}".`);
3519
+ if (version === void 0) return named[0];
3520
+ const found = named.find((f) => f.version === version);
3521
+ if (found === void 0) throw new RangeError(`The family "${name}" has the versions ${named.map((f) => f.version).join(", ")}, not ${version}.`);
3522
+ return found;
3523
+ };
3524
+ /** This function removes the `kind` of a cell of `cellOf` when it is the `familyKind` of `f`. It throws a `RangeError` for another kind. */
3525
+ const ownDims = (f, dims) => {
3526
+ if (dims === void 0 || !Object.hasOwn(dims, "kind")) return dims;
3527
+ const kind = familyKind(f.name, f.version);
3528
+ if (dims["kind"] !== kind) throw new RangeError(`${label(f)} has the kind "${kind}", not the kind ${JSON.stringify(dims["kind"])} of the cell.`);
3529
+ const { kind: _kind, ...rest } = dims;
3530
+ return rest;
3531
+ };
3532
+ /** This function gives the full dimension values of an input of `f`. A message names the family as `familyKind` gives it. */
3533
+ const resolveIn = (f, dims) => resolveDimensions(f.dimensions, dims, label(f), familyKind(f.name, f.version));
3534
+ const streamOf = (f, seed, full) => {
3535
+ const rng = f.rng ?? DEFAULT_RNG;
3536
+ return {
3537
+ rng,
3538
+ random: rng(f.rngSeed === void 0 ? seed : f.rngSeed(seed, full))
3539
+ };
3540
+ };
3541
+ const registry = {
3542
+ register(family) {
3543
+ if (!Number.isInteger(family.version) || family.version < 1) throw new RangeError(`${label(family)} has a version that is not a whole number of 1 or more.`);
3544
+ if (list.some((f) => f.name === family.name && f.version === family.version)) throw new RangeError(`${label(family)} is in the registry already.`);
3545
+ const problems = dimensionProblems(family.dimensions);
3546
+ if (family.dimensions.map((d) => d.name).includes("kind")) problems.push("the dimension name \"kind\" is reserved: the cell of `cellOf` has the `familyKind` of the family in `kind`");
3547
+ for (const key of family.familyBy ?? []) {
3548
+ const d = family.dimensions.find((q) => q.name === key);
3549
+ if (d === void 0) problems.push(`the familyBy dimension "${key}" is not a dimension of the family`);
3550
+ else if (d.when !== void 0) problems.push(`the familyBy dimension "${key}" has a when, so a cell without it has no family key`);
3551
+ }
3552
+ if (problems.length > 0) throw new RangeError(`${label(family)} is not good: ${problems.join("; ")}.`);
3553
+ list.push(family);
3554
+ return registry;
3555
+ },
3556
+ family: find,
3557
+ families: () => [...list],
3558
+ resolve(name, dims, version) {
3559
+ const f = find(name, version);
3560
+ return resolveIn(f, ownDims(f, dims));
3561
+ },
3562
+ generate(name, seed, dims, options = {}) {
3563
+ checkSeed(seed);
3564
+ const f = find(name, options.version);
3565
+ const full = resolveIn(f, ownDims(f, dims));
3566
+ const context = {
3567
+ ...streamOf(f, seed, full),
3568
+ ...options.set === void 0 ? {} : { set: options.set }
3569
+ };
3570
+ const output = f.generate(seed, full, context);
3571
+ const hash = f.hash ?? documentHash;
3572
+ return dropFields(output, invalidFields(f.dimensions, full), hash, label(f));
3573
+ },
3574
+ cellOf(name, dims, version) {
3575
+ const f = find(name, version);
3576
+ return {
3577
+ kind: familyKind(f.name, f.version),
3578
+ ...activeValues(f.dimensions, resolveIn(f, ownDims(f, dims)))
3579
+ };
3580
+ },
3581
+ *cells(name, options = {}) {
3582
+ const f = find(name, options.version);
3583
+ for (const values of sweepOf(f.dimensions, sweepLevels(f.dimensions, options, label(f), familyKind(f.name, f.version)))) yield resolveIn(f, values);
3584
+ },
3585
+ cellCount(name, options = {}) {
3586
+ const f = find(name, options.version);
3587
+ return countOf(f.dimensions, sweepLevels(f.dimensions, options, label(f), familyKind(f.name, f.version)));
3588
+ },
3589
+ boundaryCells(name, version) {
3590
+ const f = find(name, version);
3591
+ const base = resolveIn(f, {});
3592
+ const out = [base];
3593
+ const seen = /* @__PURE__ */ new Set([JSON.stringify(base)]);
3594
+ for (const d of f.dimensions) {
3595
+ if (d.kind !== "int" && d.kind !== "real") continue;
3596
+ const span = supportedSpan(d);
3597
+ for (const end of [span.min, span.max]) {
3598
+ if (end === void 0) continue;
3599
+ const cell = resolveIn(f, {
3600
+ ...activation(f.dimensions, d),
3601
+ [d.name]: end
3602
+ });
3603
+ const key = JSON.stringify(cell);
3604
+ if (!seen.has(key)) {
3605
+ seen.add(key);
3606
+ out.push(cell);
3607
+ }
3608
+ }
3609
+ }
3610
+ return out;
3611
+ },
3612
+ rngOf(name, version) {
3613
+ const f = find(name, version);
3614
+ return (seed, dims) => {
3615
+ checkSeed(seed);
3616
+ return streamOf(f, seed, resolveIn(f, ownDims(f, dims))).random;
3617
+ };
3618
+ },
3619
+ familyBy() {
3620
+ const out = {};
3621
+ for (const f of list) out[familyKind(f.name, f.version)] = [...f.familyBy ?? []];
3622
+ return out;
3623
+ }
3624
+ };
3625
+ for (const f of families) registry.register(f);
3626
+ return registry;
3627
+ }
3628
+ /** This function gives the values of a list of dimensions from the full values. */
3629
+ const pick = (dims, full) => Object.fromEntries(dims.map((d) => [d.name, full[d.name]]));
3630
+ /**
3631
+ * This function gives a family that runs the base family and then each step, with the same seed.
3632
+ * The family of the input gets the part of each step after a dot. The id becomes `<set>/<family>/s<seed>`, and the input gets the hash of `options.hash`.
3633
+ * The function throws a `RangeError` when a step has a dimension with the name of an earlier dimension.
3634
+ */
3635
+ function composeFamily(base, steps, options = {}) {
3636
+ const dimensions = [...base.dimensions, ...steps.flatMap((s) => s.dimensions)];
3637
+ const seen = /* @__PURE__ */ new Set();
3638
+ for (const d of dimensions) {
3639
+ if (seen.has(d.name)) throw new RangeError(`composeFamily: the dimension "${d.name}" is in the base family or in an earlier step already.`);
3640
+ seen.add(d.name);
3641
+ }
3642
+ const hash = options.hash ?? documentHash;
3643
+ const libm = base.crossEngine === "libm-dependent" || steps.some((s) => s.crossEngine === "libm-dependent");
3644
+ return Object.freeze({
3645
+ name: options.name ?? [base.name, ...steps.map((s) => s.name)].join("+"),
3646
+ version: base.version,
3647
+ owner: options.owner ?? base.owner,
3648
+ meaning: options.meaning ?? `${base.meaning} Then the steps ${steps.map((s) => s.name).join(", ")}.`,
3649
+ dimensions,
3650
+ seeding: [base.seeding, ...steps.map((s) => `${s.name}: ${s.seeding}`)].join(" "),
3651
+ crossEngine: libm ? "libm-dependent" : "exact",
3652
+ ...base.rng === void 0 ? {} : { rng: base.rng },
3653
+ ...base.rngSeed === void 0 ? {} : { rngSeed: (seed, full) => base.rngSeed(seed, pick(base.dimensions, full)) },
3654
+ ...base.familyBy === void 0 ? {} : { familyBy: base.familyBy },
3655
+ ...options.hash === void 0 ? {} : { hash: options.hash },
3656
+ generate(seed, full, context) {
3657
+ let input = base.generate(seed, pick(base.dimensions, full), context);
3658
+ const parts = [];
3659
+ for (const step of steps) {
3660
+ const values = pick(step.dimensions, full);
3661
+ input = step.apply(input, seed, values);
3662
+ if (step.familyPart !== void 0) parts.push(step.familyPart(values));
3663
+ }
3664
+ const family = [input.family ?? base.name, ...parts].join(".");
3665
+ const set = input.set ?? context.set;
3666
+ const id = `${set === void 0 ? "" : `${set}/`}${family}/s${seed}`;
3667
+ const stepped = {
3668
+ ...input,
3669
+ id,
3670
+ family
3671
+ };
3672
+ return {
3673
+ ...stepped,
3674
+ hash: hash(stepped)
3675
+ };
3676
+ }
3677
+ });
3678
+ }
3679
+ /** The dimensions of `EDGES_STEP`: the parameters of `edgeStep`, from the shared table. */
3680
+ const EDGE_DIMENSIONS = Object.freeze([
3681
+ "edgeDensity",
3682
+ "edgeShape",
3683
+ "groups",
3684
+ "ports",
3685
+ "hubBy"
3686
+ ].map((n) => CORPUS_DIMENSIONS[n]));
3687
+ /** This function gives the parameters of `edgeStep` from the values of the dimensions. It throws a `RangeError` for a missing or a bad value. */
3688
+ function edgeParams(d) {
3689
+ const text = (name) => {
3690
+ const dim = EDGE_DIMENSIONS.find((q) => q.name === name);
3691
+ const value = Object.hasOwn(d, name) ? d[name] : void 0;
3692
+ const bad = value === void 0 ? "the value is missing" : paramProblem(dim, value);
3693
+ if (bad !== void 0 || typeof value !== "string") throw new RangeError(`EDGES_STEP: the dimension "${name}" is bad: ${bad ?? "it is not text"}.`);
3694
+ return value;
3695
+ };
3696
+ return {
3697
+ edgeDensity: text("edgeDensity"),
3698
+ edgeShape: text("edgeShape"),
3699
+ groups: text("groups"),
3700
+ ports: text("ports"),
3701
+ hubBy: text("hubBy")
3702
+ };
3703
+ }
3704
+ /**
3705
+ * The step `edges` of `edgeStep` (version 1), as a corpus step. Its family part is `e-<edgeDensity>.<edgeShape>.<groups>.<ports>`.
3706
+ * The value `random` of `hubBy` adds the part `.hub-random`.
3707
+ */
3708
+ const EDGES_STEP = Object.freeze({
3709
+ name: "edges",
3710
+ version: 1,
3711
+ owner: "layout-contract",
3712
+ meaning: "The edges, the groups and the ports of an input, from its topology alone.",
3713
+ dimensions: EDGE_DIMENSIONS,
3714
+ seeding: "stepRandom(seed, step) for the steps edges and ports.",
3715
+ crossEngine: "libm-dependent",
3716
+ familyPart: (d) => `e-${d["edgeDensity"]}.${d["edgeShape"]}.${d["groups"]}.${d["ports"]}${d["hubBy"] === "random" ? ".hub-random" : ""}`,
3717
+ apply: (input, seed, d) => edgeStep(input, edgeParams(d), seed)
3718
+ });
3719
+ /** The set of `RECT_SCATTERED` when the caller gives no set. */
3720
+ const XCHECK_SET = "xcheck-v1";
3721
+ /**
3722
+ * The shared family `rect-scattered`, version 1: a fixed rectangle container, rectangle obstacles at random places and boxes without positions.
3723
+ * It is the one copy of the generator of `docs/FORMAT.md` of `packing`. Its input is the input of `packing`, byte for byte.
3724
+ * The steps `container`, `obstacles` and `boxes` each have a stream of `stepRandom`. Each value is a whole number, so the family is `exact`.
3725
+ */
3726
+ const RECT_SCATTERED = Object.freeze({
3727
+ name: "rect-scattered",
3728
+ version: 1,
3729
+ owner: "layout-contract",
3730
+ meaning: "Rectangle obstacles at random places in a fixed rectangle container, and boxes of random sizes.",
3731
+ dimensions: Object.freeze([sharedDimension("count", { default: 40 })]),
3732
+ seeding: "stepRandom(seed, container) gives w and h. stepRandom(seed, obstacles) gives n, then ow, oh, x and y of each obstacle. stepRandom(seed, boxes) gives bw and bh of each box.",
3733
+ crossEngine: "exact",
3734
+ generate(seed, dims, context) {
3735
+ const count = dims["count"];
3736
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 0) throw new RangeError(`rect-scattered: the count is a whole number of 0 or more, not ${String(count)}.`);
3737
+ const set = context.set ?? XCHECK_SET;
3738
+ let r = stepRandom(seed, "container");
3739
+ const w = randomInt(r, 600, 1e3);
3740
+ const h = randomInt(r, 400, 800);
3741
+ r = stepRandom(seed, "obstacles");
3742
+ const n = randomInt(r, 3, 8);
3743
+ const obstacles = [];
3744
+ for (let i = 0; i < n; i++) {
3745
+ const ow = randomInt(r, 40, 160);
3746
+ const oh = randomInt(r, 40, 160);
3747
+ const x = randomInt(r, 0, w - ow);
3748
+ const y = randomInt(r, 0, h - oh);
3749
+ obstacles.push({
3750
+ id: `o${i}`,
3751
+ region: {
3752
+ kind: "rect",
3753
+ x,
3754
+ y,
3755
+ w: ow,
3756
+ h: oh
3757
+ }
3758
+ });
3759
+ }
3760
+ r = stepRandom(seed, "boxes");
3761
+ const boxes = Array.from({ length: count }, (_, i) => {
3762
+ const bw = randomInt(r, 20, 120);
3763
+ const bh = randomInt(r, 20, 80);
3764
+ return {
3765
+ id: `b${i}`,
3766
+ w: bw,
3767
+ h: bh
3768
+ };
3769
+ });
3770
+ const body = {
3771
+ schema: "layout-input/1",
3772
+ id: `${set}/rect-scattered/s${seed}`,
3773
+ set,
3774
+ family: "rect-scattered",
3775
+ seed,
3776
+ source: "made-up",
3777
+ generator: {
3778
+ name: "rect-scattered",
3779
+ version: 1,
3780
+ params: { count },
3781
+ seed
3782
+ },
3783
+ container: {
3784
+ w,
3785
+ h,
3786
+ fixed: "size"
3787
+ },
3788
+ obstacles,
3789
+ boxes,
3790
+ edges: []
3791
+ };
3792
+ return {
3793
+ ...body,
3794
+ hash: documentHash(body)
3795
+ };
3796
+ }
3797
+ });
3798
+ export { CORPUS_DIMENSIONS, DEFAULT_FAMILY_BY, DEFAULT_RNG, DEFAULT_SHAPES, DEFAULT_TOLERANCE, EDGES_STEP, EDGE_DENSITY, EDGE_STEP_VERSION, EPS, FAMILY_ANY_KIND, INVALIDATED_FIELD, JOINT_MEASURES, JOINT_MOVED_MIN, PACK_HARD_RULES, RECORD_EPS, RECORD_SCALE, RECT_SCATTERED, SHAPE_KINDS, SHAPE_KIND_IDS, SIDES, SPLIT_SCHEMA, STRATEGY_SPACE_SCHEMA, VERDICT_DRAWS, VERDICT_EXACT_CAP, VERDICT_EXACT_MAX, VERDICT_RUNS, VERDICT_SEED, applyPack, boundsOf, canonicalConfig, canonicalInput, canonicalJson, checkConfig, checkDeclared, checkInput, checkShapes, checkSpace, checkSplit, compareMeasure, composeFamily, configId, corpusHashOf, countObstacleHits, countOutside, countOverlaps, countPortsOffBorder, createFamilyRegistry, declaredClaimOf, declaredCounts, dimensionAt, dimensionSupports, dimensionUnit, documentHash, edgeStep, enumerateSpace, exercised, familyKind, familyOf, fnv1a32, grow, isShapeKind, itemShapeOf, joinConfigs, leaveOneSeedOut, levelsOf, marginSides, minEffectOf, mulberry32, outputGroups, outputHash, overlapOf, ownerDimension, packHard, paramActive, paramLevels, paramProblem, paramValue, pickMoves, placedRect, portPoint, portfolio, productSpace, randomInt, rectInRegion, regionHitsRect, resolveDimensions, selectBest, sha256Hex, shapeKindOf, shapeKindProblem, shapeKindsOf, shapesOf, sharedDimension, spaceShapeKinds, spaceSize, splitConfig, splitFileName, splitFolds, stepRandom, toLayoutRecord, toPackInput, translate, turnBox, turnMargin, turnPoint, turnPort, turnRegion, turnSide, verdict };
2835
3799
 
2836
3800
  //# sourceMappingURL=index.js.map