@mark1russell7/layout-contract 0.2.15 → 0.2.17

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
@@ -73,6 +73,28 @@ function boundsOf(region) {
73
73
  }
74
74
  }
75
75
  }
76
+ /**
77
+ * This function gives the exact area of a region. A `rect` gives `w` by `h`, a circle gives `PI * r * r`, and an ellipse gives `PI * rx * ry`.
78
+ * A polygon gives the absolute value of the shoelace formula, less the area of each hole. A ring with fewer than 3 points has the area 0.
79
+ */
80
+ function regionArea(region) {
81
+ switch (region.kind) {
82
+ case "rect": return region.w * region.h;
83
+ case "circle": return Math.PI * region.r * region.r;
84
+ case "ellipse": return Math.PI * region.rx * region.ry;
85
+ case "polygon": return (region.holes ?? []).reduce((area, hole) => area - ringArea(hole), ringArea(region.points));
86
+ }
87
+ }
88
+ /** This function gives the area of one ring of a polygon: the absolute value of the shoelace formula. */
89
+ function ringArea(ring) {
90
+ let twice = 0;
91
+ for (let i = 0; i < ring.length; i++) {
92
+ const [ax, ay] = ring[i];
93
+ const [bx, by] = ring[(i + 1) % ring.length];
94
+ twice += ax * by - bx * ay;
95
+ }
96
+ return Math.abs(twice) / 2;
97
+ }
76
98
  /** This function gives the region moved by `dx` and `dy`. */
77
99
  function translate(region, dx, dy) {
78
100
  switch (region.kind) {
@@ -111,7 +133,10 @@ function overlapOf(a, b) {
111
133
  };
112
134
  }
113
135
  // src/shapes.ts
114
- /** The declared shape kinds, in their order. Only `rect` has all of its geometry. The other kinds have their bounding box only. */
136
+ /**
137
+ * The declared shape kinds, in their order. Only `rect` has all of its geometry.
138
+ * `polygon` and `circle` have their bounding box and their exact area. `free` has its bounding box only.
139
+ */
115
140
  const SHAPE_KINDS = Object.freeze([
116
141
  {
117
142
  id: "rect",
@@ -130,7 +155,7 @@ const SHAPE_KINDS = Object.freeze([
130
155
  regions: ["polygon"],
131
156
  geometry: {
132
157
  bounds: true,
133
- area: false,
158
+ area: true,
134
159
  contains: false,
135
160
  overlap: false
136
161
  }
@@ -145,7 +170,7 @@ const SHAPE_KINDS = Object.freeze([
145
170
  regions: ["circle"],
146
171
  geometry: {
147
172
  bounds: true,
148
- area: false,
173
+ area: true,
149
174
  contains: false,
150
175
  overlap: false
151
176
  }
@@ -175,6 +200,63 @@ const SHAPE_KINDS = Object.freeze([
175
200
  const SHAPE_KIND_IDS = Object.freeze(SHAPE_KINDS.map((k) => k.id));
176
201
  /** The shape kinds of a measure or a rule without `shapes`. The contract measured only rectangles before L46. */
177
202
  const DEFAULT_SHAPES = Object.freeze(["rect"]);
203
+ /** This function gives the level of a rule on a shape kind. A kind of `shapesOf` is `exact`, a kind of `safe` is `safe`, and another kind is `none`. It does not read `bound`. */
204
+ function ruleLevel(rule, kind) {
205
+ if (shapesOf(rule).includes(kind)) return "exact";
206
+ return rule.safe?.includes(kind) === true ? "safe" : "none";
207
+ }
208
+ /**
209
+ * This function gives the level of a measure or a rule on a shape kind. It joins the lists `shapes`, `safe` and `bound.shapes`.
210
+ * A kind of `shapesOf` is `exact`, a kind of `safe` is `safe`, a kind of `bound.shapes` is `bound`, and another kind is `none`.
211
+ */
212
+ function shapeLevel(use, kind) {
213
+ const level = ruleLevel(use, kind);
214
+ if (level !== "none") return level;
215
+ return use.bound?.shapes.includes(kind) === true ? "bound" : "none";
216
+ }
217
+ /**
218
+ * This function gives the problems of a list of measures and rules. An empty list tells that the list is good.
219
+ * It refuses an unknown kind, a kind two times in one list, and a kind in more than one of `shapesOf`, `safe` and `bound.shapes`.
220
+ * It also refuses a `bound` without a list of kinds, and a `side` that is not `upper` or `lower`.
221
+ */
222
+ function shapeSupportProblems(uses) {
223
+ const out = [];
224
+ for (const use of uses) {
225
+ const bound = use.bound;
226
+ const isBound = typeof bound === "object" && bound !== null && !Array.isArray(bound);
227
+ if (bound !== void 0 && !isBound) out.push(`${use.id}: the bound value is not an object with shapes and a side`);
228
+ const boundShapes = isBound ? bound.shapes : void 0;
229
+ for (const [name, list] of [
230
+ ["shapes", use.shapes],
231
+ ["safe", use.safe],
232
+ ["bound", boundShapes]
233
+ ]) {
234
+ if (list === void 0 && (name !== "bound" || !isBound)) continue;
235
+ if (!Array.isArray(list)) {
236
+ out.push(`${use.id}: the ${name === "bound" ? "bound shapes" : name} value is not a list of shape kinds`);
237
+ continue;
238
+ }
239
+ list.forEach((k, i) => {
240
+ if (!isShapeKind(k)) out.push(`${use.id}: the ${name} kind ${String(k)} is not a declared kind`);
241
+ else if (list.indexOf(k) !== i) out.push(`${use.id}: the ${name} list has the kind ${k} two times`);
242
+ });
243
+ }
244
+ const exact = use.shapes === void 0 ? DEFAULT_SHAPES : Array.isArray(use.shapes) ? use.shapes : [];
245
+ if (Array.isArray(use.safe)) {
246
+ for (const k of new Set(use.safe)) if (exact.includes(k)) out.push(`${use.id}: the kind ${String(k)} is in shapes and in safe`);
247
+ }
248
+ if (!isBound) continue;
249
+ const side = bound.side;
250
+ if (side !== "upper" && side !== "lower") out.push(`${use.id}: the bound side ${String(side)} is not upper or lower`);
251
+ if (!Array.isArray(boundShapes)) continue;
252
+ const safe = Array.isArray(use.safe) ? use.safe : [];
253
+ for (const k of new Set(boundShapes)) {
254
+ if (exact.includes(k)) out.push(`${use.id}: the kind ${String(k)} is in shapes and in bound`);
255
+ if (safe.includes(k)) out.push(`${use.id}: the kind ${String(k)} is in safe and in bound`);
256
+ }
257
+ }
258
+ return out;
259
+ }
178
260
  const REGION_KIND = {
179
261
  rect: "rect",
180
262
  polygon: "polygon",
@@ -193,6 +275,18 @@ function shapeKindOf(box) {
193
275
  if (box.shapeKind !== void 0) return box.shapeKind;
194
276
  return box.shape === void 0 ? "rect" : REGION_KIND[box.shape.kind];
195
277
  }
278
+ /**
279
+ * This function gives the exact area of a box, or `undefined` when the contract knows only its bounding box.
280
+ * A box with `shape` gives `regionArea` of the shape. A `rect` box without `shape` gives `w` by `h`.
281
+ * A box of the kind `free` gives `undefined`, also with a shape. Thus a box with an ellipse shape gives `undefined`, because `shapeKindOf` makes an ellipse `free`.
282
+ * A box of another kind without `shape` is a bad input, and gives `undefined`.
283
+ */
284
+ function boxArea(box) {
285
+ const kind = shapeKindOf(box);
286
+ if (kind === "free") return void 0;
287
+ if (box.shape !== void 0) return regionArea(box.shape);
288
+ return kind === "rect" ? box.w * box.h : void 0;
289
+ }
196
290
  /** This function gives the shape kinds of the boxes of an input, in the order of `SHAPE_KINDS`. An input without boxes gives an empty list. */
197
291
  function shapeKindsOf(input) {
198
292
  const seen = new Set(input.boxes.map(shapeKindOf));
@@ -219,19 +313,23 @@ function shapesOf(use) {
219
313
  /**
220
314
  * This function checks that each measure and each rule of a run supports each shape kind of the input. An empty list tells that the run is good.
221
315
  * Each break has the claim `shape`, the id of the input, and the first 5 ids of the boxes of the kind.
316
+ * By default, a kind of `safe` or of `bound.shapes` is a break, because the result is not exact.
317
+ * With the level `safe`, a kind of `safe` is not a break. With the level `bound`, a kind of `safe` and a kind of `bound.shapes` are not a break.
222
318
  */
223
- function checkShapes(input, uses) {
319
+ function checkShapes(input, uses, options = {}) {
224
320
  const kinds = shapeKindsOf(input);
225
321
  const out = [];
226
322
  for (const use of uses) {
227
323
  const shapes = shapesOf(use);
228
324
  for (const kind of kinds) {
229
- if (shapes.includes(kind)) continue;
325
+ const level = shapeLevel(use, kind);
326
+ if (level === "exact" || level === "safe" && (options.level === "safe" || options.level === "bound") || level === "bound" && options.level === "bound") continue;
230
327
  const ids = input.boxes.filter((b) => shapeKindOf(b) === kind).map((b) => b.id);
231
328
  const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, and ${ids.length - 5} more` : "");
329
+ const safe = level === "safe" ? `. The count of ${use.id} on ${kind} is only safe` : level === "bound" ? `. The value of ${use.id} is only ${use.bound.side === "upper" ? "an upper" : "a lower"} bound for ${kind}` : "";
232
330
  out.push({
233
331
  claim: "shape",
234
- detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}`,
332
+ detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}${safe}`,
235
333
  inputs: [input.id]
236
334
  });
237
335
  }
@@ -585,16 +683,27 @@ function countPortsOffBorder(boxes, eps = RECORD_EPS) {
585
683
  /**
586
684
  * The hard rules of a pack, with the shape kinds that each rule supports.
587
685
  * `no-overlap` and `in-container` test the bounding box of a shape. That count is exact only for a `rect`.
686
+ * For the other kinds, the count is safe: a shape is inside its bounding box, so the count is an upper bound. A count of 0 tells that the rule has no break.
588
687
  * `locked-kept` and `complete` read only the positions and the ids, so they support each kind.
589
688
  */
590
689
  const PACK_HARD_RULES = Object.freeze([
591
690
  {
592
691
  id: "no-overlap",
593
- shapes: ["rect"]
692
+ shapes: ["rect"],
693
+ safe: [
694
+ "polygon",
695
+ "circle",
696
+ "free"
697
+ ]
594
698
  },
595
699
  {
596
700
  id: "in-container",
597
- shapes: ["rect"]
701
+ shapes: ["rect"],
702
+ safe: [
703
+ "polygon",
704
+ "circle",
705
+ "free"
706
+ ]
598
707
  },
599
708
  {
600
709
  id: "locked-kept",
@@ -2085,6 +2194,8 @@ function optionProblems(stage, o) {
2085
2194
  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
2195
  const pool = o.members.pool;
2087
2196
  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`]);
2197
+ const view = o.members.view;
2198
+ if (view !== void 0 && view !== "bounds") out.push(["view", `${at} has the member view ${show(view)}, not "bounds"`]);
2088
2199
  }
2089
2200
  return out;
2090
2201
  }
@@ -2103,6 +2214,15 @@ function supportsProblems(supports, at) {
2103
2214
  }
2104
2215
  /** This function tells if an option supports each shape kind of the list. */
2105
2216
  const supportsShapes = (o, shapes) => shapes === void 0 || o.supports?.shape === void 0 || shapes.every((k) => o.supports.shape.includes(k));
2217
+ /** The shape kinds that the members of an option with `view: "bounds"` see: a bounding box is a `rect`. */
2218
+ const BOUNDS_VIEW = Object.freeze(["rect"]);
2219
+ /**
2220
+ * This function gives the shape kinds of the members of a meta option. With `view: "bounds"`, a list with a kind becomes `BOUNDS_VIEW`.
2221
+ * Without a list, or with an empty list, the value does not change.
2222
+ */
2223
+ const memberShapes = (rule, shapes) => rule.view === "bounds" && shapes !== void 0 && shapes.length > 0 ? BOUNDS_VIEW : shapes;
2224
+ /** This function tells if two lists have the same shape kinds. */
2225
+ const sameKinds = (a, b) => a.every((k) => b.includes(k)) && b.every((k) => a.includes(k));
2106
2226
  /** This function gives the problems of the params of one stage choice. */
2107
2227
  function choiceParamProblems(o, params, at) {
2108
2228
  if (params !== void 0 && !isObject(params)) return [["config", `the params of ${at} are not an object`]];
@@ -2176,7 +2296,7 @@ function configProblems(space, config, member, shapes) {
2176
2296
  out.push(["depth", `a member has the meta option ${s.name}.${o.id}. The depth of a meta config is 1`]);
2177
2297
  continue;
2178
2298
  }
2179
- out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, shapes));
2299
+ out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, memberShapes(o.members, shapes)));
2180
2300
  }
2181
2301
  for (const c of space.constraints ?? []) if (!holds(c, chosen)) out.push(["constraint", `the config breaks the constraint ${describe(c)}`]);
2182
2302
  return out;
@@ -2239,6 +2359,7 @@ const toBreaks = (problems, inputs = []) => problems.map(([claim, detail]) => ({
2239
2359
  *
2240
2360
  * A member list that is not an entry of the `pool` of its option gives the claim `pool`.
2241
2361
  * 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.
2362
+ * A member of an option with `view: "bounds"` gets the check with the kind `rect`.
2242
2363
  */
2243
2364
  function checkConfig(space, config, options = {}) {
2244
2365
  return toBreaks(configProblems(space, config, false, options.shapes));
@@ -2499,7 +2620,9 @@ function combos(space, levels, meta, limit) {
2499
2620
  for (const s of stages) for (const o of s.options) {
2500
2621
  if (o.members === void 0) continue;
2501
2622
  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);
2623
+ const own = memberSpaces.get(o);
2624
+ const from = own === void 0 ? base : baseCount(own, levels, limit);
2625
+ const lists = o.members.pool === void 0 ? memberLists(from, o.members, limit) : BigInt(o.members.pool.length);
2503
2626
  if (keeps(space, choices)) out.push({
2504
2627
  choices,
2505
2628
  base: false,
@@ -2524,6 +2647,8 @@ function spaceSize(space, options) {
2524
2647
  * This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
2525
2648
  * A stage can lose all of its options. Then the space has no config with that stage.
2526
2649
  * A pool keeps only the entries whose members fit the smaller space. A meta option without such an entry goes out.
2650
+ *
2651
+ * The members of an option with `view: "bounds"` come from the space for the kinds of `memberShapes`. `memberSpaces` keeps that space.
2527
2652
  */
2528
2653
  function restrict(space, shapes) {
2529
2654
  if (shapes === void 0) return space;
@@ -2536,16 +2661,23 @@ function restrict(space, shapes) {
2536
2661
  ...space.parts === void 0 ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }
2537
2662
  };
2538
2663
  const withPool = (s, o) => {
2539
- if (o.members?.pool === void 0) return [o];
2540
- const part = partOf(kept, s.name).space;
2664
+ if (o.members === void 0) return [o];
2665
+ const inner = memberShapes(o.members, shapes);
2666
+ const own = sameKinds(inner, shapes) ? void 0 : restrict(partOf(space, s.name).space, inner);
2667
+ const mark = (option) => {
2668
+ if (own !== void 0) memberSpaces.set(option, own);
2669
+ return [option];
2670
+ };
2671
+ if (o.members.pool === void 0) return own === void 0 ? [o] : mark({ ...o });
2672
+ const part = own ?? partOf(kept, s.name).space;
2541
2673
  const pool = o.members.pool.filter((entry) => fits(part, entry));
2542
- return pool.length === 0 ? [] : [{
2674
+ return pool.length === 0 ? [] : mark({
2543
2675
  ...o,
2544
2676
  members: {
2545
2677
  ...o.members,
2546
2678
  pool
2547
2679
  }
2548
- }];
2680
+ });
2549
2681
  };
2550
2682
  return {
2551
2683
  ...kept,
@@ -2555,6 +2687,31 @@ function restrict(space, shapes) {
2555
2687
  }))
2556
2688
  };
2557
2689
  }
2690
+ /** The space of the members of a meta option of a restricted space, when it is not the part of the option. Only `view: "bounds"` makes such a space. */
2691
+ const memberSpaces = /* @__PURE__ */ new WeakMap();
2692
+ /**
2693
+ * This function tells if an option of a restricted space has a member list. A base option and a meta option with a `pool` have one.
2694
+ * A different meta option has one when its members have a base config. `restrict` keeps only the pool entries that fit.
2695
+ */
2696
+ const hasMembers = (space, stage, o) => o.members === void 0 || o.members.pool !== void 0 || combos(memberSpaces.get(o) ?? partOf(space, stage).space, 1, false, 0n).length > 0;
2697
+ /** This function tells if a restricted space has a config. A base config counts. With `meta`, a meta config counts when each of its meta options has a member list. */
2698
+ const hasConfig = (space, meta) => combos(space, 1, meta, 0n).some((c) => c.base || [...c.choices].every(([stage, o]) => o === null || hasMembers(space, stage, o)));
2699
+ /** This function gives the error of a sample of a space that has no config for the shape kinds `shapes`. `kept` is the restricted space. */
2700
+ function noSample(space, kept, shapes, meta) {
2701
+ const list = (names, one, more) => names.length === 1 ? `${one} ${names[0]}` : `${more} ${names.join(", ")}`;
2702
+ const empty = kept.stages.filter((s) => !s.optional && !s.options.some((o) => meta || o.members === void 0)).map((s) => s.name);
2703
+ const removed = !meta ? [] : space.stages.flatMap((s) => s.options.filter((o) => o.members !== void 0 && o.members.view !== "bounds" && supportsShapes(o, shapes)).filter((o) => {
2704
+ const own = kept.stages.find((x) => x.name === s.name)?.options.find((x) => x.id === o.id);
2705
+ return own === void 0 || !hasMembers(kept, s.name, own);
2706
+ }).map((o) => `${s.name}.${o.id}`));
2707
+ let text = `The space ${space.engine} has no config for a sample with the shape kinds ${shapes.join(", ")}.`;
2708
+ if (empty.length > 0) text += ` ${list(empty, "The stage", "The stages")} ${empty.length === 1 ? "has" : "have"} no option for these kinds.`;
2709
+ if (removed.length > 0) text += ` ${list(removed, "The meta option", "The meta options")} ${removed.length === 1 ? "has" : "have"} no member config for these kinds. Set members.view to "bounds" when the members see only the bounding boxes.`;
2710
+ if (empty.length === 0 && removed.length === 0) text += " The constraints remove each config.";
2711
+ return new RangeError(text);
2712
+ }
2713
+ /** This function gives the count of the base configs of a space that is known to be good. */
2714
+ const baseCount = (space, levels, limit) => combos(space, levels, false, limit).reduce((sum, c) => sum + c.weight, 0n);
2558
2715
  /** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
2559
2716
  function assertShapes(shapes) {
2560
2717
  if (shapes === void 0) return;
@@ -2631,16 +2788,27 @@ function sampleValue(p, u) {
2631
2788
  }
2632
2789
  /** This function gives a seed of 32 bits from a seed and a step name. */
2633
2790
  const subSeed = (seed, step) => Math.floor(stepRandom(seed, step)() * 4294967296);
2634
- /** This function gives a sample of a space: Latin hypercube strata over the options, the params and the member counts. */
2635
- function sampleConfigs(space, n, seed, meta, maxTries) {
2791
+ /**
2792
+ * This function gives a sample of a space: Latin hypercube strata over the options, the params and the member counts.
2793
+ * `canon` is the space of `canonicalConfig`: the space before a shape filter. A member of an option with `view: "bounds"` can have an option that the filter removed.
2794
+ */
2795
+ function sampleConfigs(space, n, seed, meta, maxTries, canon = space) {
2636
2796
  const stages = space.stages;
2637
2797
  const prefix = prefixes(space);
2798
+ const bounded = new Set(stages.filter((s) => meta && s.options.some((o) => memberSpaces.has(o))).map((s) => prefix.get(s.name)));
2638
2799
  const lists = stages.map((s) => {
2639
2800
  const list = s.options.filter((o) => meta || o.members === void 0);
2640
2801
  if (s.optional) list.push(null);
2802
+ if (list.length === 0 && bounded.has(prefix.get(s.name))) list.push(null);
2641
2803
  if (list.length === 0) throw new Error(`The stage ${s.name} of the space ${space.engine} has no option for a sample. A shape filter can remove all of its options.`);
2642
2804
  return list;
2643
2805
  });
2806
+ const usable = /* @__PURE__ */ new Map();
2807
+ const withMembers = (s, o) => {
2808
+ if (o === null || o === void 0 || o.members === void 0) return true;
2809
+ if (!usable.has(o)) usable.set(o, hasMembers(space, s.name, o));
2810
+ return usable.get(o);
2811
+ };
2644
2812
  const points = [];
2645
2813
  let tries = 0;
2646
2814
  for (let round = 0; points.length < n; round++) {
@@ -2656,7 +2824,7 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
2656
2824
  const m = metaPart.get(prefix.get(s.name));
2657
2825
  if (m !== void 0 && m !== s.name) choices.set(s.name, null);
2658
2826
  }
2659
- if (keeps(space, choices)) points.push(choices);
2827
+ if (stages.every((s) => s.optional === true || choices.get(s.name) !== null || metaPart.has(prefix.get(s.name))) && keeps(space, choices) && stages.every((s) => withMembers(s, choices.get(s.name)))) points.push(choices);
2660
2828
  }
2661
2829
  }
2662
2830
  const values = points.map(() => stages.map(() => ({})));
@@ -2681,7 +2849,7 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
2681
2849
  });
2682
2850
  continue;
2683
2851
  }
2684
- const part = partOf(space, s.name).space;
2852
+ const part = memberSpaces.get(o) ?? partOf(space, s.name).space;
2685
2853
  const poolSize = Math.max(n, 4 * o.members.max, 8);
2686
2854
  const seen = /* @__PURE__ */ new Set();
2687
2855
  const pool = sampleConfigs(part, poolSize, subSeed(seed, `pool:${s.name}:${o.id}`), false, Math.max(1e3, 100 * poolSize)).filter((c) => {
@@ -2711,7 +2879,7 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
2711
2879
  members: members[i][k]
2712
2880
  };
2713
2881
  });
2714
- return canonicalConfig(space, {
2882
+ return canonicalConfig(canon, {
2715
2883
  space: space.engine,
2716
2884
  stages: out
2717
2885
  });
@@ -2725,6 +2893,8 @@ function sampleConfigs(space, n, seed, meta, maxTries) {
2725
2893
  * - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
2726
2894
  * - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
2727
2895
  * - A meta option with a `pool` gives only the entries of its pool. With `shapes`, only the entries whose members support each kind count.
2896
+ * - A sample draws again when a point chooses a meta option without a member list. `spaceSize` gives such an option 0.
2897
+ * - With `shapes`, a sample throws a `RangeError` when the space has no config for the kinds. The error names the stages without an option and the meta options without members.
2728
2898
  */
2729
2899
  function enumerateSpace(space, options) {
2730
2900
  assertSpace(space);
@@ -2739,7 +2909,13 @@ function enumerateSpace(space, options) {
2739
2909
  const meta = options.meta ?? "skip";
2740
2910
  if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
2741
2911
  if (options.n === 0) return [];
2742
- return sampleConfigs(restrict(space, options.shapes), options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1e3, 100 * options.n));
2912
+ const kept = restrict(space, options.shapes);
2913
+ try {
2914
+ return sampleConfigs(kept, options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1e3, 100 * options.n), space);
2915
+ } catch (error) {
2916
+ if (options.shapes !== void 0 && !hasConfig(kept, meta === "sample")) throw noSample(space, kept, options.shapes, meta === "sample");
2917
+ throw error;
2918
+ }
2743
2919
  }
2744
2920
  /**
2745
2921
  * This function gives the shape kinds that a space accepts, in the order of `SHAPE_KINDS`. The value comes from the `supports` of the options.
@@ -3193,7 +3369,7 @@ const CORPUS_DIMENSIONS = Object.freeze({
3193
3369
  });
3194
3370
  /**
3195
3371
  * 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.
3372
+ * With `supported`, `invalidates` or `precision` in the patch, it also throws a `RangeError` for a bad dimension, as `ownerDimension` does.
3197
3373
  */
3198
3374
  function sharedDimension(name, patch = {}) {
3199
3375
  const base = Object.hasOwn(CORPUS_DIMENSIONS, name) ? CORPUS_DIMENSIONS[name] : void 0;
@@ -3203,12 +3379,12 @@ function sharedDimension(name, patch = {}) {
3203
3379
  ...patch,
3204
3380
  name
3205
3381
  });
3206
- if (patch.supported !== void 0 || patch.invalidates !== void 0) throwProblems(`sharedDimension: the dimension "${name}"`, ownProblems(dim));
3382
+ if (patch.supported !== void 0 || patch.invalidates !== void 0 || patch.precision !== void 0) throwProblems(`sharedDimension: the dimension "${name}"`, ownProblems(dim));
3207
3383
  return dim;
3208
3384
  }
3209
3385
  /**
3210
3386
  * 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`.
3387
+ * It throws a `RangeError` for a name of `CORPUS_DIMENSIONS`, the reserved name `kind`, or a bad default, level, `supported`, `invalidates` or `precision`.
3212
3388
  * The registry examines the `when` of the dimension, because it needs the earlier dimensions.
3213
3389
  */
3214
3390
  function ownerDimension(name, spec) {
@@ -3252,7 +3428,7 @@ function supportedText(dim) {
3252
3428
  if (isSupportedList(s)) return s.map((v) => JSON.stringify(v)).join(", ");
3253
3429
  return `the values from ${s.min ?? dim.min} to ${s.max ?? dim.max}`;
3254
3430
  }
3255
- /** This function gives the problems of one dimension that need no other dimension: the default, the levels, `supported` and `invalidates`. */
3431
+ /** This function gives the problems of one dimension that need no other dimension: the default, the levels, `supported`, `invalidates` and `precision`. */
3256
3432
  function ownProblems(d) {
3257
3433
  const out = [];
3258
3434
  const bad = paramProblem(d, d.default);
@@ -3263,6 +3439,7 @@ function ownProblems(d) {
3263
3439
  }
3264
3440
  out.push(...supportProblems(d, bad === void 0));
3265
3441
  out.push(...invalidatesProblems(d));
3442
+ out.push(...precisionProblems(d));
3266
3443
  return out;
3267
3444
  }
3268
3445
  /** This function gives the problems of the `supported` values of a dimension. `goodDefault` tells that the default is a good value. */
@@ -3304,6 +3481,22 @@ function invalidatesProblems(d) {
3304
3481
  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
3482
  return out;
3306
3483
  }
3484
+ /** This function gives the problems of the `precision` of a dimension. A bad value and a dimension that is not numeric are problems. */
3485
+ function precisionProblems(d) {
3486
+ const p = d.precision;
3487
+ if (p === void 0) return [];
3488
+ if (typeof p !== "number" || !Number.isInteger(p) || p < 0) return [`the precision of "${d.name}" is ${JSON.stringify(p)}, not a whole number of 0 or more`];
3489
+ if (d.kind !== "int" && d.kind !== "real") return [`the precision of "${d.name}" is for a numeric dimension, but the dimension is a ${d.kind}`];
3490
+ return [];
3491
+ }
3492
+ /** This function rounds a number to `places` decimal places: `Math.round(v * 10 ** places) / 10 ** places`. */
3493
+ const roundTo = (v, places) => Math.round(v * 10 ** places) / 10 ** places;
3494
+ /** This function tells if a value is the default of a dimension. With `precision`, it compares the two numbers after it rounds them. */
3495
+ function isDefault(d, value) {
3496
+ const def = paramValue(d, d.default);
3497
+ if (d.precision === void 0 || typeof value !== "number" || typeof def !== "number") return value === def;
3498
+ return roundTo(value, d.precision) === roundTo(def, d.precision);
3499
+ }
3307
3500
  /** This function tells if a log scale of a range starts at 0. Then the scale is the log of `1 + x - min`. */
3308
3501
  const fromZero = (d) => d.scale === "log" && (d.min ?? 0) <= 0;
3309
3502
  /**
@@ -3459,12 +3652,15 @@ function sweepLevels(dimensions, options, where, by) {
3459
3652
  return lv;
3460
3653
  });
3461
3654
  }
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. */
3655
+ /**
3656
+ * This function gives the output fields that the values make wrong: the `invalidates` of each dimension with a value other than its default, sorted.
3657
+ * A dimension with `precision` compares the rounded value with the rounded default.
3658
+ */
3463
3659
  function invalidFields(dimensions, full) {
3464
3660
  const out = /* @__PURE__ */ new Set();
3465
3661
  for (const d of dimensions) {
3466
3662
  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);
3663
+ if (!isDefault(d, full[d.name])) for (const f of d.invalidates) out.add(f);
3468
3664
  }
3469
3665
  return [...out].sort(byCode);
3470
3666
  }
@@ -3795,6 +3991,6 @@ const RECT_SCATTERED = Object.freeze({
3795
3991
  };
3796
3992
  }
3797
3993
  });
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 };
3994
+ 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, boxArea, 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, regionArea, regionHitsRect, resolveDimensions, ruleLevel, selectBest, sha256Hex, shapeKindOf, shapeKindProblem, shapeKindsOf, shapeLevel, shapeSupportProblems, shapesOf, sharedDimension, spaceShapeKinds, spaceSize, splitConfig, splitFileName, splitFolds, stepRandom, toLayoutRecord, toPackInput, translate, turnBox, turnMargin, turnPoint, turnPort, turnRegion, turnSide, verdict };
3799
3995
 
3800
3996
  //# sourceMappingURL=index.js.map