@mark1russell7/layout-contract 0.2.13 → 0.2.14

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/src/input.ts CHANGED
@@ -86,6 +86,19 @@ export interface LayoutGenerator {
86
86
  readonly seed: number;
87
87
  }
88
88
 
89
+ /**
90
+ * One minimum distance between two boxes (`a` and `b`). It replaces the two margins of the boxes for this pair, and the global `gap`.
91
+ * The order of `a` and `b` has no meaning. A pair without an entry keeps the margins and the `gap`.
92
+ */
93
+ export interface PairGap {
94
+ readonly a: string;
95
+ readonly b: string;
96
+ /** The minimum distance between the two boxes, in units: a finite number of 0 or more. */
97
+ readonly gap: number;
98
+ /** The count of the channels that the gap has space for. It is information only: a whole number of 0 or more. */
99
+ readonly lanes?: number;
100
+ }
101
+
89
102
  /** The cell of an input in the taxonomy: one value for each dimension. The dimensions are in `docs/FORMAT.md` of `packing`. */
90
103
  export interface LayoutCell {
91
104
  readonly containerSize?: "fixed" | "freeHeight" | "freeWidth" | "freeAspect" | "freeArea";
@@ -124,6 +137,8 @@ export interface LayoutInput {
124
137
  readonly container?: LayoutContainer;
125
138
  /** The free space between two boxes with their margins. Routes use it. */
126
139
  readonly gap?: number;
140
+ /** The distances of some pairs of boxes. An entry replaces the margins and the `gap` for its pair. `canonicalInput` sorts the list. */
141
+ readonly pairGaps?: readonly PairGap[];
127
142
  readonly boxes: readonly LayoutBox[];
128
143
  readonly edges?: readonly LayoutEdge[];
129
144
  readonly obstacles?: readonly Obstacle[];
@@ -141,6 +156,7 @@ export interface PackInput {
141
156
  readonly items: readonly Item[];
142
157
  readonly obstacles?: readonly Obstacle[];
143
158
  readonly gap?: number;
159
+ readonly pairGaps?: readonly PairGap[];
144
160
  }
145
161
 
146
162
  /** This function gives the packer input of a file. An input without a container gets `{ fixed: "none" }`, so the packer picks the size. */
@@ -150,5 +166,22 @@ export function toPackInput(input: LayoutInput): PackInput {
150
166
  items: input.boxes,
151
167
  ...(input.obstacles === undefined ? {} : { obstacles: input.obstacles }),
152
168
  ...(input.gap === undefined ? {} : { gap: input.gap }),
169
+ ...(input.pairGaps === undefined ? {} : { pairGaps: input.pairGaps }),
153
170
  };
154
171
  }
172
+
173
+ /** This function compares two strings by their UTF-16 code units, as `Array.prototype.sort` does without a compare function. */
174
+ const byCode = (x: string, y: string): number => (x < y ? -1 : x > y ? 1 : 0);
175
+
176
+ /**
177
+ * This function gives the canonical form of an input. `documentHash` uses it for each `layout-input/1` document.
178
+ * Each pair of `pairGaps` gets `a` before `b`, and the list is sorted by `a` and then by `b`.
179
+ * An input without `pairGaps` is the same object, so its hash does not change.
180
+ */
181
+ export function canonicalInput<T extends LayoutInput>(input: T): T {
182
+ if (!Array.isArray(input.pairGaps)) return input;
183
+ const pairGaps = input.pairGaps
184
+ .map((p): PairGap => (byCode(p.a, p.b) > 0 ? { ...p, a: p.b, b: p.a } : p))
185
+ .sort((x, y) => byCode(x.a, y.a) || byCode(x.b, y.b));
186
+ return { ...input, pairGaps };
187
+ }
package/src/rules.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { boundsOf, grow, overlapOf, type Rect, type Region } from "./geometry.ts";
2
- import type { LayoutBox, LayoutInput, Obstacle } from "./input.ts";
2
+ import type { LayoutBox, LayoutInput, Obstacle, PairGap } from "./input.ts";
3
3
  import { applyPack, type PackOutput } from "./output.ts";
4
4
  import { SHAPE_KIND_IDS, shapeKindProblem, type ShapeSupport } from "./shapes.ts";
5
5
  import { DEFAULT_TOLERANCE, EPS, RECORD_EPS } from "./tolerance.ts";
@@ -15,20 +15,52 @@ export function placedRect(box: LayoutBox): Rect | undefined {
15
15
  /**
16
16
  * This function counts the pairs of placed boxes whose insides, with their margins, overlap by more than `tolerance` on each axis.
17
17
  * A box with a shape counts as its bounding box. That area is larger, thus the count is safe.
18
+ *
19
+ * 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.
20
+ * Thus the pair breaks the rule only when the boxes are nearer than the gap on each axis. The other pairs do not change.
18
21
  */
19
- export function countOverlaps(boxes: readonly LayoutBox[], tolerance: number = DEFAULT_TOLERANCE.overlap): number {
20
- const rects: Rect[] = [];
22
+ export function countOverlaps(boxes: readonly LayoutBox[], tolerance: number = DEFAULT_TOLERANCE.overlap, pairGaps?: readonly PairGap[]): number {
23
+ if (pairGaps === undefined || pairGaps.length === 0) return marginOverlaps(boxes, tolerance, undefined);
24
+ const gaps = new Map<string, number>();
25
+ for (const p of pairGaps) {
26
+ const key = pairKey(p.a, p.b);
27
+ if (p.a !== p.b && !gaps.has(key)) gaps.set(key, p.gap);
28
+ }
29
+ const raw = new Map<string, Rect>();
30
+ for (const box of boxes) {
31
+ const r = placedRect(box);
32
+ if (r !== undefined && !raw.has(box.id)) raw.set(box.id, r);
33
+ }
34
+ let count = marginOverlaps(boxes, tolerance, gaps);
35
+ for (const [key, gap] of gaps) {
36
+ const [a, b] = JSON.parse(key) as [string, string];
37
+ const ra = raw.get(a);
38
+ const rb = raw.get(b);
39
+ if (ra === undefined || rb === undefined) continue;
40
+ const o = overlapOf(grow(ra, gap), rb);
41
+ if (o.x > tolerance && o.y > tolerance) count++;
42
+ }
43
+ return count;
44
+ }
45
+
46
+ /** This function gives the key of a pair of box ids, with the same key for each order. */
47
+ const pairKey = (a: string, b: string): string => JSON.stringify(a < b ? [a, b] : [b, a]);
48
+
49
+ /** 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. */
50
+ function marginOverlaps(boxes: readonly LayoutBox[], tolerance: number, skip: ReadonlyMap<string, number> | undefined): number {
51
+ const rects: { readonly id: string; readonly r: Rect }[] = [];
21
52
  for (const box of boxes) {
22
53
  const r = placedRect(box);
23
- if (r !== undefined) rects.push(grow(r, box.margin));
54
+ if (r !== undefined) rects.push({ id: box.id, r: grow(r, box.margin) });
24
55
  }
25
- rects.sort((a, b) => a.x - b.x);
56
+ rects.sort((a, b) => a.r.x - b.r.x);
26
57
  let count = 0;
27
58
  for (let i = 0; i < rects.length; i++) {
28
- const a = rects[i]!;
59
+ const a = rects[i]!.r;
29
60
  for (let j = i + 1; j < rects.length; j++) {
30
- const b = rects[j]!;
61
+ const b = rects[j]!.r;
31
62
  if (b.x >= a.x + a.w - tolerance) break;
63
+ if (skip !== undefined && skip.has(pairKey(rects[i]!.id, rects[j]!.id))) continue;
32
64
  const o = overlapOf(a, b);
33
65
  if (o.x > tolerance && o.y > tolerance) count++;
34
66
  }
@@ -279,7 +311,8 @@ export function countObstacleHits(
279
311
  /**
280
312
  * This function gives the count of each hard rule of the output of a packer.
281
313
  * It applies the output with `applyPack`. Only the placed boxes and the locked boxes count for the overlaps and the container.
282
- * `no-overlap` counts the pairs of boxes and the pairs of a box and an obstacle.
314
+ * `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.
315
+ *
283
316
  * A locked box without a placement, or with a move of more than `RECORD_EPS`, breaks `locked-kept`.
284
317
  * A locked box without a position in the input is a bad input, not a break. `checkInput` finds it.
285
318
  * Another box without a placement that is not in `overflow` breaks `complete`.
@@ -308,7 +341,7 @@ export function packHard(input: LayoutInput, output: PackOutput, tolerance: numb
308
341
  }
309
342
  }
310
343
  return {
311
- "no-overlap": countOverlaps(kept, tolerance) + countObstacleHits(kept, after.obstacles, tolerance),
344
+ "no-overlap": countOverlaps(kept, tolerance, input.pairGaps) + countObstacleHits(kept, after.obstacles, tolerance),
312
345
  "in-container": countOutside({ ...after, boxes: kept }, tolerance, options),
313
346
  "locked-kept": lockedMoved,
314
347
  complete: incomplete,
@@ -318,6 +351,7 @@ export function packHard(input: LayoutInput, output: PackOutput, tolerance: numb
318
351
  /**
319
352
  * This function gives the problems of an input file. An empty list tells that the file is good.
320
353
  * A problem is a bad input, not a break of a hard rule. Thus an engine refuses the input.
354
+ * In `pairGaps`, an unknown box, a pair of one box, a repeated pair in either order, a bad `gap` and bad `lanes` are problems.
321
355
  */
322
356
  export function checkInput(input: LayoutInput): readonly string[] {
323
357
  const problems: string[] = [];
@@ -334,5 +368,31 @@ export function checkInput(input: LayoutInput): readonly string[] {
334
368
  for (const edge of input.edges ?? []) {
335
369
  if (!ids.has(edge.source) || !ids.has(edge.target)) problems.push(`edge ${edge.id}: a box of the edge is not in the input`);
336
370
  }
371
+ problems.push(...pairGapProblems(input.pairGaps, ids));
372
+ return problems;
373
+ }
374
+
375
+ /** This function gives the problems of the `pairGaps` of an input. `ids` has the ids of the boxes. */
376
+ function pairGapProblems(pairGaps: LayoutInput["pairGaps"], ids: ReadonlySet<string>): string[] {
377
+ if (pairGaps === undefined) return [];
378
+ if (!Array.isArray(pairGaps)) return ["pairGaps: the value is not a list"];
379
+ const problems: string[] = [];
380
+ const seen = new Map<string, number>();
381
+ pairGaps.forEach((p: PairGap, i) => {
382
+ const at = `pair gap ${i} (${String(p?.a)}, ${String(p?.b)})`;
383
+ if (typeof p !== "object" || p === null || typeof p.a !== "string" || typeof p.b !== "string") {
384
+ problems.push(`pair gap ${i}: the entry has no string ids a and b`);
385
+ return;
386
+ }
387
+ for (const id of [p.a, p.b]) if (!ids.has(id)) problems.push(`${at}: the box ${id} is not in the input`);
388
+ if (p.a === p.b) problems.push(`${at}: the two ids are the same box`);
389
+ 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`);
390
+ if (p.lanes !== undefined && (typeof p.lanes !== "number" || !Number.isInteger(p.lanes) || p.lanes < 0))
391
+ problems.push(`${at}: the lanes value ${String(p.lanes)} is not a whole number of 0 or more`);
392
+ const key = JSON.stringify(p.a < p.b ? [p.a, p.b] : [p.b, p.a]);
393
+ const first = seen.get(key);
394
+ if (first !== undefined) problems.push(`${at}: the pair is also pair gap ${first}`);
395
+ else seen.set(key, i);
396
+ });
337
397
  return problems;
338
398
  }
@@ -46,6 +46,12 @@ export interface MemberRule {
46
46
  readonly min: number;
47
47
  readonly max: number;
48
48
  readonly ordered: boolean;
49
+ /**
50
+ * The member lists that the option permits. Each entry is a list of whole configs of the space of the members.
51
+ * Without `pool`, each list of base configs with a good count is a member list.
52
+ * With `pool`, a config of the option has the members of one entry, and `spaceSize` counts each entry one time.
53
+ */
54
+ readonly pool?: readonly (readonly StrategyConfig[])[];
49
55
  }
50
56
 
51
57
  /** The inputs that an option supports. A missing field supports each value. */
@@ -149,7 +155,7 @@ export interface SampleOptions extends ShapeFilter {
149
155
  /** The count of the configs, a whole number of 0 or more. */
150
156
  readonly n: number;
151
157
  readonly seed: number;
152
- /** With `skip` (the default), the sample has no meta option. With `sample`, a meta option draws its members from base samples. */
158
+ /** With `skip` (the default), the sample has no meta option. With `sample`, a meta option draws its members from base samples, or one entry of its `pool`. */
153
159
  readonly meta?: "skip" | "sample";
154
160
  /** The largest count of option draws. The default is the larger value of 1000 and `100 * n`. */
155
161
  readonly maxTries?: number;
@@ -204,6 +210,21 @@ function isActive(p: SpaceParam, values: Values): boolean {
204
210
  return p.when.in.some((x) => x === v);
205
211
  }
206
212
 
213
+ /** 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. */
214
+ export function paramProblem(param: SpaceParam, value: unknown): string | undefined {
215
+ return valueProblem(param, value);
216
+ }
217
+
218
+ /** This function tells if a param is active for the values of the earlier params. A param without `when` is always active. */
219
+ export function paramActive(param: SpaceParam, values: Readonly<Record<string, ParamValue>>): boolean {
220
+ return isActive(param, values);
221
+ }
222
+
223
+ /** This function gives the stable form of a value of a param: a real number gets 12 significant digits, and `-0` becomes `0`. */
224
+ export function paramValue(param: SpaceParam, value: ParamValue): ParamValue {
225
+ return normalize(param, value);
226
+ }
227
+
207
228
  /** This function gives the part of a space that has a stage, and the prefix of the names of its stages. */
208
229
  function partOf(space: StrategySpace, stage: string): { readonly space: StrategySpace; readonly prefix: string } {
209
230
  for (const part of space.parts ?? []) {
@@ -291,6 +312,9 @@ function optionProblems(stage: SpaceStage, o: StageOption): Problem[] {
291
312
  const { min, max, ordered } = o.members;
292
313
  if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || min > max || typeof ordered !== "boolean")
293
314
  out.push(["members", `${at} has a member rule without whole counts 1 <= min <= max and a boolean ordered`]);
315
+ const pool: unknown = o.members.pool;
316
+ if (pool !== undefined && (!Array.isArray(pool) || pool.length === 0 || !pool.every((entry: unknown) => Array.isArray(entry))))
317
+ out.push(["pool", `${at} has a pool that is not a list of one or more member lists`]);
294
318
  }
295
319
  return out;
296
320
  }
@@ -407,14 +431,47 @@ function memberProblems(part: StrategySpace, rule: MemberRule, members: unknown,
407
431
  if (ids.has(id)) out.push(["duplicate", `the members ${ids.get(id)} and ${k} of ${at} are the same config ${id}`]);
408
432
  else ids.set(id, k);
409
433
  });
434
+ if (rule.pool !== undefined && out.length === 0) {
435
+ const key = memberKey(part, rule, members as readonly StrategyConfig[]);
436
+ if (!rule.pool.some((entry) => memberKey(part, rule, entry) === key))
437
+ out.push(["pool", `${at} has a member list that is not in its pool of ${rule.pool.length} ${rule.pool.length === 1 ? "list" : "lists"}`]);
438
+ }
439
+ return out;
440
+ }
441
+
442
+ /** This function gives the key of a good member list: the `configId` of each member, sorted when the rule is not ordered. */
443
+ function memberKey(part: StrategySpace, rule: MemberRule, members: readonly StrategyConfig[]): string {
444
+ const ids = members.map((m) => idOf(canonicalOf(part, m, true)));
445
+ if (!rule.ordered) ids.sort();
446
+ return ids.join(",");
447
+ }
448
+
449
+ /** This function gives the problems of the pool of a meta option of a good space. `part` is the space of the members. */
450
+ function poolProblems(part: StrategySpace, rule: MemberRule, at: string): Problem[] {
451
+ const out: Problem[] = [];
452
+ const keys = new Map<string, number>();
453
+ const plain: MemberRule = { min: rule.min, max: rule.max, ordered: rule.ordered };
454
+ (rule.pool ?? []).forEach((entry, j) => {
455
+ const inner = memberProblems(part, plain, entry, `the pool entry ${j} of ${at}`);
456
+ out.push(...inner);
457
+ if (inner.length > 0) return;
458
+ const key = memberKey(part, rule, entry);
459
+ if (keys.has(key)) out.push(["duplicate", `the pool entries ${keys.get(key)} and ${j} of ${at} have the same members`]);
460
+ else keys.set(key, j);
461
+ });
410
462
  return out;
411
463
  }
412
464
 
465
+ /** 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. */
466
+ const fits = (part: StrategySpace, entry: readonly StrategyConfig[]): boolean => entry.every((m) => configProblems(part, m, true).length === 0);
467
+
413
468
  const toBreaks = (problems: readonly Problem[], inputs: readonly string[] = []): ClaimBreak[] => problems.map(([claim, detail]) => ({ claim, detail, inputs }));
414
469
 
415
470
  /**
416
471
  * This function checks one config of a space. It gives one break for each problem. An empty list tells that the config is good.
417
472
  * The `claim` of a break is the kind of the problem, for example `missing`, `unknown`, `inactive`, `param`, `constraint`, `members` or `depth`.
473
+ *
474
+ * A member list that is not an entry of the `pool` of its option gives the claim `pool`.
418
475
  * 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.
419
476
  */
420
477
  export function checkConfig(space: StrategySpace, config: StrategyConfig, options: ShapeFilter = {}): ClaimBreak[] {
@@ -424,6 +481,7 @@ export function checkConfig(space: StrategySpace, config: StrategyConfig, option
424
481
  /**
425
482
  * This function checks a space. It gives one break for each problem. An empty list tells that the space is good.
426
483
  * It finds duplicate names, empty ranges, defaults out of range, enums without values, bad log scales, unknown names and bad named configs.
484
+ * It also checks each entry of a `pool`: the count of its members, each member and a repeated entry.
427
485
  * The `inputs` of a break of a named config have the name of the config.
428
486
  */
429
487
  export function checkSpace(space: StrategySpace): ClaimBreak[] {
@@ -462,6 +520,9 @@ export function checkSpace(space: StrategySpace): ClaimBreak[] {
462
520
  for (const s of space.stages) if (partOf(space, s.name).prefix === "") out.push(["parts", `the stage ${s.name} is not a stage of a part`]);
463
521
  const breaks = toBreaks(out);
464
522
  if (breaks.length > 0) return breaks;
523
+ for (const s of space.stages)
524
+ for (const o of s.options) if (o.members?.pool !== undefined) breaks.push(...toBreaks(poolProblems(partOf(space, s.name).space, o.members, `the option ${s.name}.${o.id}`)));
525
+ if (breaks.length > 0) return breaks;
465
526
  for (const [name, config] of Object.entries(space.named ?? {}))
466
527
  breaks.push(...toBreaks(configProblems(space, config, false).map(([c, d]) => [c, `the named config ${name}: ${d}`] as const), [name]));
467
528
  return breaks;
@@ -608,7 +669,7 @@ interface Combo {
608
669
  const optionIds = (choices: Choices): Map<string, string | null> => new Map([...choices].map(([k, o]) => [k, o?.id ?? null]));
609
670
  const keeps = (space: StrategySpace, choices: Choices): boolean => (space.constraints ?? []).every((c) => holds(c, optionIds(choices)));
610
671
 
611
- /** This function gives the count of the member lists of a meta option from `base` base configs, or `null` above `limit`. */
672
+ /** This function gives the count of the member lists of a meta option without a pool from `base` base configs, or `null` above `limit`. */
612
673
  function memberLists(base: bigint, rule: MemberRule, limit: bigint): bigint | null {
613
674
  let sum = 0n;
614
675
  for (let m = rule.min; m <= rule.max; m++) {
@@ -677,7 +738,8 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
677
738
  for (const o of s.options) {
678
739
  if (o.members === undefined) continue;
679
740
  const choices = new Map<string, StageOption | null>(stages.map((x) => [x.name, x === s ? o : null]));
680
- if (keeps(space, choices)) out.push({ choices, base: false, weight: times(countOf(o), memberLists(base, o.members, limit), limit) });
741
+ const lists = o.members.pool === undefined ? memberLists(base, o.members, limit) : BigInt(o.members.pool.length);
742
+ if (keeps(space, choices)) out.push({ choices, base: false, weight: times(countOf(o), lists, limit) });
681
743
  }
682
744
  return out;
683
745
  }
@@ -685,6 +747,7 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
685
747
  /**
686
748
  * This function gives the count of the grid points of a space, with `levels` levels for each numeric param.
687
749
  * It counts only the active params and the combinations that keep the constraints. It gives the meta configs apart.
750
+ * A meta option with a `pool` counts each entry of the pool one time.
688
751
  * With `shapes`, it counts only the options that support each kind of the list.
689
752
  */
690
753
  export function spaceSize(space: StrategySpace, options: SpaceSizeOptions): SpaceSize {
@@ -697,14 +760,22 @@ export function spaceSize(space: StrategySpace, options: SpaceSizeOptions): Spac
697
760
  /**
698
761
  * This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
699
762
  * A stage can lose all of its options. Then the space has no config with that stage.
763
+ * A pool keeps only the entries whose members fit the smaller space. A meta option without such an entry goes out.
700
764
  */
701
765
  function restrict(space: StrategySpace, shapes: readonly ShapeKind[] | undefined): StrategySpace {
702
766
  if (shapes === undefined) return space;
703
- return {
767
+ const kept: StrategySpace = {
704
768
  ...space,
705
769
  stages: space.stages.map((s) => ({ ...s, options: s.options.filter((o) => supportsShapes(o, shapes)) })),
706
770
  ...(space.parts === undefined ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }),
707
771
  };
772
+ const withPool = (s: SpaceStage, o: StageOption): StageOption[] => {
773
+ if (o.members?.pool === undefined) return [o];
774
+ const part = partOf(kept, s.name).space;
775
+ const pool = o.members.pool.filter((entry) => fits(part, entry));
776
+ return pool.length === 0 ? [] : [{ ...o, members: { ...o.members, pool } }];
777
+ };
778
+ return { ...kept, stages: kept.stages.map((s) => ({ ...s, options: s.options.flatMap((o) => withPool(s, o)) })) };
708
779
  }
709
780
 
710
781
  /** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
@@ -828,6 +899,14 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
828
899
  });
829
900
  }
830
901
  if (o.members === undefined) continue;
902
+ const fixed = o.members.pool;
903
+ if (fixed !== undefined) {
904
+ const r = stepRandom(seed, `pool-entry:${s.name}:${o.id}`);
905
+ permutation(users.length, r).forEach((stratum, j) => {
906
+ members[users[j]!]![k] = [...fixed[Math.min(fixed.length - 1, Math.floor(((stratum + r()) / users.length) * fixed.length))]!];
907
+ });
908
+ continue;
909
+ }
831
910
  const part = partOf(space, s.name).space;
832
911
  const poolSize = Math.max(n, 4 * o.members.max, 8);
833
912
  const seen = new Set<string>();
@@ -862,6 +941,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
862
941
  * - 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.
863
942
  * - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
864
943
  * - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
944
+ * - A meta option with a `pool` gives only the entries of its pool. With `shapes`, only the entries whose members support each kind count.
865
945
  */
866
946
  export function enumerateSpace(space: StrategySpace, options: EnumerateOptions): StrategyConfig[] {
867
947
  assertSpace(space);