@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/README.md +12 -2
- package/build/index.d.ts +87 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +223 -27
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/families.ts +36 -6
- package/src/geometry.ts +28 -0
- package/src/measures.ts +6 -1
- package/src/rules.ts +3 -2
- package/src/shapes.ts +120 -8
- package/src/strategy-space.ts +106 -11
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/layout-contract",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.17",
|
|
5
5
|
"description": "The shared contract of the packing and highways layout engines: the input and record formats, the turn rule, margins, tolerances and hard rules.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"layout",
|
package/src/families.ts
CHANGED
|
@@ -30,6 +30,12 @@ export interface CorpusDimension extends SpaceParam {
|
|
|
30
30
|
* Then `generate` of a registry removes these fields from the output, and lists them in `invalidated`.
|
|
31
31
|
*/
|
|
32
32
|
readonly invalidates?: readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* The count of decimal places that the family uses for a value of a numeric dimension: a whole number of 0 or more.
|
|
35
|
+
* Only the `invalidates` compare reads it. The registry rounds the value and the default to it before the compare.
|
|
36
|
+
* The cells, the cell keys, `rngOf` and the value that `generate` gets do not change.
|
|
37
|
+
*/
|
|
38
|
+
readonly precision?: number;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
/**
|
|
@@ -226,13 +232,14 @@ export const CORPUS_DIMENSIONS: Readonly<Record<string, CorpusDimension>> = Obje
|
|
|
226
232
|
|
|
227
233
|
/**
|
|
228
234
|
* This function gives a dimension of `CORPUS_DIMENSIONS`, with the fields of `patch` over it. It throws a `RangeError` for an unknown name.
|
|
229
|
-
* With `supported` or `
|
|
235
|
+
* With `supported`, `invalidates` or `precision` in the patch, it also throws a `RangeError` for a bad dimension, as `ownerDimension` does.
|
|
230
236
|
*/
|
|
231
237
|
export function sharedDimension(name: string, patch: Partial<CorpusDimension> = {}): CorpusDimension {
|
|
232
238
|
const base = Object.hasOwn(CORPUS_DIMENSIONS, name) ? CORPUS_DIMENSIONS[name] : undefined;
|
|
233
239
|
if (base === undefined) throw new RangeError(`The shared dimension table has no dimension "${name}".`);
|
|
234
240
|
const dim: CorpusDimension = Object.freeze({ ...base, ...patch, name });
|
|
235
|
-
if (patch.supported !== undefined || patch.invalidates !== undefined
|
|
241
|
+
if (patch.supported !== undefined || patch.invalidates !== undefined || patch.precision !== undefined)
|
|
242
|
+
throwProblems(`sharedDimension: the dimension "${name}"`, ownProblems(dim));
|
|
236
243
|
return dim;
|
|
237
244
|
}
|
|
238
245
|
|
|
@@ -241,7 +248,7 @@ export type OwnerDimensionSpec = Omit<CorpusDimension, "name">;
|
|
|
241
248
|
|
|
242
249
|
/**
|
|
243
250
|
* This function gives a dimension that only its owner declares, for example `cut` of a family of `packing`.
|
|
244
|
-
* It throws a `RangeError` for a name of `CORPUS_DIMENSIONS`, the reserved name `kind`, or a bad default, level, `supported` or `
|
|
251
|
+
* It throws a `RangeError` for a name of `CORPUS_DIMENSIONS`, the reserved name `kind`, or a bad default, level, `supported`, `invalidates` or `precision`.
|
|
245
252
|
* The registry examines the `when` of the dimension, because it needs the earlier dimensions.
|
|
246
253
|
*/
|
|
247
254
|
export function ownerDimension(name: string, spec: OwnerDimensionSpec): CorpusDimension {
|
|
@@ -288,7 +295,7 @@ function supportedText(dim: CorpusDimension): string {
|
|
|
288
295
|
return `the values from ${s.min ?? dim.min} to ${s.max ?? dim.max}`;
|
|
289
296
|
}
|
|
290
297
|
|
|
291
|
-
/** This function gives the problems of one dimension that need no other dimension: the default, the levels, `supported` and `
|
|
298
|
+
/** This function gives the problems of one dimension that need no other dimension: the default, the levels, `supported`, `invalidates` and `precision`. */
|
|
292
299
|
function ownProblems(d: CorpusDimension): string[] {
|
|
293
300
|
const out: string[] = [];
|
|
294
301
|
const bad = paramProblem(d, d.default);
|
|
@@ -299,6 +306,7 @@ function ownProblems(d: CorpusDimension): string[] {
|
|
|
299
306
|
}
|
|
300
307
|
out.push(...supportProblems(d, bad === undefined));
|
|
301
308
|
out.push(...invalidatesProblems(d));
|
|
309
|
+
out.push(...precisionProblems(d));
|
|
302
310
|
return out;
|
|
303
311
|
}
|
|
304
312
|
|
|
@@ -343,6 +351,25 @@ function invalidatesProblems(d: CorpusDimension): string[] {
|
|
|
343
351
|
return out;
|
|
344
352
|
}
|
|
345
353
|
|
|
354
|
+
/** This function gives the problems of the `precision` of a dimension. A bad value and a dimension that is not numeric are problems. */
|
|
355
|
+
function precisionProblems(d: CorpusDimension): string[] {
|
|
356
|
+
const p: unknown = d.precision;
|
|
357
|
+
if (p === undefined) return [];
|
|
358
|
+
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`];
|
|
359
|
+
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}`];
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** This function rounds a number to `places` decimal places: `Math.round(v * 10 ** places) / 10 ** places`. */
|
|
364
|
+
const roundTo = (v: number, places: number): number => Math.round(v * 10 ** places) / 10 ** places;
|
|
365
|
+
|
|
366
|
+
/** This function tells if a value is the default of a dimension. With `precision`, it compares the two numbers after it rounds them. */
|
|
367
|
+
function isDefault(d: CorpusDimension, value: ParamValue | undefined): boolean {
|
|
368
|
+
const def = paramValue(d, d.default);
|
|
369
|
+
if (d.precision === undefined || typeof value !== "number" || typeof def !== "number") return value === def;
|
|
370
|
+
return roundTo(value, d.precision) === roundTo(def, d.precision);
|
|
371
|
+
}
|
|
372
|
+
|
|
346
373
|
/** This function tells if a log scale of a range starts at 0. Then the scale is the log of `1 + x - min`. */
|
|
347
374
|
const fromZero = (d: SpaceParam): boolean => d.scale === "log" && (d.min ?? 0) <= 0;
|
|
348
375
|
|
|
@@ -501,12 +528,15 @@ function sweepLevels(dimensions: readonly CorpusDimension[], options: CellsOptio
|
|
|
501
528
|
});
|
|
502
529
|
}
|
|
503
530
|
|
|
504
|
-
/**
|
|
531
|
+
/**
|
|
532
|
+
* This function gives the output fields that the values make wrong: the `invalidates` of each dimension with a value other than its default, sorted.
|
|
533
|
+
* A dimension with `precision` compares the rounded value with the rounded default.
|
|
534
|
+
*/
|
|
505
535
|
function invalidFields(dimensions: readonly CorpusDimension[], full: DimensionValues): string[] {
|
|
506
536
|
const out = new Set<string>();
|
|
507
537
|
for (const d of dimensions) {
|
|
508
538
|
if (d.invalidates === undefined || d.invalidates.length === 0) continue;
|
|
509
|
-
if (full[d.name]
|
|
539
|
+
if (!isDefault(d, full[d.name])) for (const f of d.invalidates) out.add(f);
|
|
510
540
|
}
|
|
511
541
|
return [...out].sort(byCode);
|
|
512
542
|
}
|
package/src/geometry.ts
CHANGED
|
@@ -80,6 +80,34 @@ export function boundsOf(region: Region): Rect {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* 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`.
|
|
85
|
+
* 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.
|
|
86
|
+
*/
|
|
87
|
+
export function regionArea(region: Region): number {
|
|
88
|
+
switch (region.kind) {
|
|
89
|
+
case "rect":
|
|
90
|
+
return region.w * region.h;
|
|
91
|
+
case "circle":
|
|
92
|
+
return Math.PI * region.r * region.r;
|
|
93
|
+
case "ellipse":
|
|
94
|
+
return Math.PI * region.rx * region.ry;
|
|
95
|
+
case "polygon":
|
|
96
|
+
return (region.holes ?? []).reduce((area, hole) => area - ringArea(hole), ringArea(region.points));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** This function gives the area of one ring of a polygon: the absolute value of the shoelace formula. */
|
|
101
|
+
function ringArea(ring: readonly (readonly [number, number])[]): number {
|
|
102
|
+
let twice = 0;
|
|
103
|
+
for (let i = 0; i < ring.length; i++) {
|
|
104
|
+
const [ax, ay] = ring[i]!;
|
|
105
|
+
const [bx, by] = ring[(i + 1) % ring.length]!;
|
|
106
|
+
twice += ax * by - bx * ay;
|
|
107
|
+
}
|
|
108
|
+
return Math.abs(twice) / 2;
|
|
109
|
+
}
|
|
110
|
+
|
|
83
111
|
/** This function gives the region moved by `dx` and `dy`. */
|
|
84
112
|
export function translate(region: Region, dx: number, dy: number): Region {
|
|
85
113
|
switch (region.kind) {
|
package/src/measures.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SHAPE_KIND_IDS, type ShapeKind } from "./shapes.ts";
|
|
1
|
+
import { SHAPE_KIND_IDS, type ShapeBound, type ShapeKind } from "./shapes.ts";
|
|
2
2
|
import { RECORD_EPS, RECORD_SCALE } from "./tolerance.ts";
|
|
3
3
|
|
|
4
4
|
/** The direction of a measure: the side of the better values. */
|
|
@@ -32,6 +32,11 @@ export interface MeasureSpec<Id extends string = string> {
|
|
|
32
32
|
readonly role?: "guard" | "change-guard" | "diagnostic" | "time";
|
|
33
33
|
/** The shape kinds of the boxes that the measure supports. Without it, the measure supports only `rect`. `checkShapes` refuses the other kinds. */
|
|
34
34
|
readonly shapes?: readonly ShapeKind[];
|
|
35
|
+
/**
|
|
36
|
+
* The kinds where the value is a bound of the true value, and its side. A kind is in `shapes` or in `bound.shapes`, not in the two lists.
|
|
37
|
+
* `checkShapes` refuses a bound kind by default and with `{ level: "safe" }`. With `{ level: "bound" }`, it accepts it. `docs/SHAPES.md` tells what a bound value proves for each `direction`.
|
|
38
|
+
*/
|
|
39
|
+
readonly bound?: ShapeBound;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
/** The id of a joint measure. `docs/JOINT-MEASURES.md` gives the definitions. */
|
package/src/rules.ts
CHANGED
|
@@ -132,11 +132,12 @@ export interface PackHard {
|
|
|
132
132
|
/**
|
|
133
133
|
* The hard rules of a pack, with the shape kinds that each rule supports.
|
|
134
134
|
* `no-overlap` and `in-container` test the bounding box of a shape. That count is exact only for a `rect`.
|
|
135
|
+
* 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.
|
|
135
136
|
* `locked-kept` and `complete` read only the positions and the ids, so they support each kind.
|
|
136
137
|
*/
|
|
137
138
|
export const PACK_HARD_RULES: readonly ShapeSupport<keyof PackHard>[] = Object.freeze([
|
|
138
|
-
{ id: "no-overlap", shapes: ["rect"] },
|
|
139
|
-
{ id: "in-container", shapes: ["rect"] },
|
|
139
|
+
{ id: "no-overlap", shapes: ["rect"], safe: ["polygon", "circle", "free"] },
|
|
140
|
+
{ id: "in-container", shapes: ["rect"], safe: ["polygon", "circle", "free"] },
|
|
140
141
|
{ id: "locked-kept", shapes: SHAPE_KIND_IDS },
|
|
141
142
|
{ id: "complete", shapes: SHAPE_KIND_IDS },
|
|
142
143
|
] as const);
|
package/src/shapes.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The shape kinds of the items (`docs/SHAPES.md`): the declared set, the kind of a box and the check of the measures and rules on a run.
|
|
2
2
|
import type { ClaimBreak } from "./corpus.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { regionArea, type Region } from "./geometry.ts";
|
|
4
4
|
import type { LayoutBox, LayoutInput } from "./input.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -13,7 +13,7 @@ export type ShapeKind = "rect" | "polygon" | "circle" | "free";
|
|
|
13
13
|
export interface ShapeGeometry {
|
|
14
14
|
/** The bounding box: `boundsOf` and `placedRect`. */
|
|
15
15
|
readonly bounds: boolean;
|
|
16
|
-
/** The area of the shape
|
|
16
|
+
/** The area of the shape: `regionArea` and `boxArea`. */
|
|
17
17
|
readonly area: boolean;
|
|
18
18
|
/** The test that the shape is inside a region: `rectInRegion` for a `rect`. */
|
|
19
19
|
readonly contains: boolean;
|
|
@@ -31,11 +31,14 @@ export interface ShapeKindSpec {
|
|
|
31
31
|
readonly geometry: ShapeGeometry;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The declared shape kinds, in their order. Only `rect` has all of its geometry.
|
|
36
|
+
* `polygon` and `circle` have their bounding box and their exact area. `free` has its bounding box only.
|
|
37
|
+
*/
|
|
35
38
|
export const SHAPE_KINDS: readonly ShapeKindSpec[] = Object.freeze([
|
|
36
39
|
{ id: "rect", fields: ["w", "h"], regions: ["rect"], geometry: { bounds: true, area: true, contains: true, overlap: true } },
|
|
37
|
-
{ id: "polygon", fields: ["points", "holes?"], regions: ["polygon"], geometry: { bounds: true, area:
|
|
38
|
-
{ id: "circle", fields: ["cx", "cy", "r"], regions: ["circle"], geometry: { bounds: true, area:
|
|
40
|
+
{ id: "polygon", fields: ["points", "holes?"], regions: ["polygon"], geometry: { bounds: true, area: true, contains: false, overlap: false } },
|
|
41
|
+
{ id: "circle", fields: ["cx", "cy", "r"], regions: ["circle"], geometry: { bounds: true, area: true, contains: false, overlap: false } },
|
|
39
42
|
{ id: "free", fields: ["w", "h", "shape?"], regions: ["rect", "polygon", "circle", "ellipse"], geometry: { bounds: true, area: false, contains: false, overlap: false } },
|
|
40
43
|
] as const);
|
|
41
44
|
|
|
@@ -48,7 +51,87 @@ export const DEFAULT_SHAPES: readonly ShapeKind[] = Object.freeze(["rect"] as co
|
|
|
48
51
|
/** A measure or a rule, with the shape kinds that it supports. Without `shapes`, it supports `DEFAULT_SHAPES`. */
|
|
49
52
|
export interface ShapeSupport<Id extends string = string> {
|
|
50
53
|
readonly id: Id;
|
|
54
|
+
/** The kinds with an exact count or value. */
|
|
51
55
|
readonly shapes?: readonly ShapeKind[];
|
|
56
|
+
/**
|
|
57
|
+
* The kinds with a safe count: the rule can count a break that is not real, but it does not miss a real break.
|
|
58
|
+
* Thus a safe count of 0 tells that the rule has no break. A kind is in `shapes` or in `safe`, not in the two lists.
|
|
59
|
+
*/
|
|
60
|
+
readonly safe?: readonly ShapeKind[];
|
|
61
|
+
/**
|
|
62
|
+
* The kinds where the value of a measure is a bound of the true value, not the exact value. `ShapeBound` tells the side.
|
|
63
|
+
* A kind is in one list only: `shapes`, `safe` or `bound.shapes`. `docs/SHAPES.md` tells what a bound value proves.
|
|
64
|
+
*/
|
|
65
|
+
readonly bound?: ShapeBound;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The kinds where a measure gives a bound of the true value, and the side of the bound.
|
|
70
|
+
* With `upper`, the value is the true value or more. With `lower`, the value is the true value or less.
|
|
71
|
+
*/
|
|
72
|
+
export interface ShapeBound {
|
|
73
|
+
readonly shapes: readonly ShapeKind[];
|
|
74
|
+
readonly side: "upper" | "lower";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The level of a rule on a shape kind: an exact count, a safe count (an upper bound), or no support. */
|
|
78
|
+
export type RuleLevel = "exact" | "safe" | "none";
|
|
79
|
+
|
|
80
|
+
/** 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`. */
|
|
81
|
+
export function ruleLevel(rule: Pick<ShapeSupport, "shapes" | "safe">, kind: ShapeKind): RuleLevel {
|
|
82
|
+
if (shapesOf(rule).includes(kind)) return "exact";
|
|
83
|
+
return rule.safe?.includes(kind) === true ? "safe" : "none";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The level of a measure or a rule on a shape kind. The level `bound` is a value on one side of the true value (`ShapeBound`). */
|
|
87
|
+
export type ShapeLevel = RuleLevel | "bound";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* This function gives the level of a measure or a rule on a shape kind. It joins the lists `shapes`, `safe` and `bound.shapes`.
|
|
91
|
+
* A kind of `shapesOf` is `exact`, a kind of `safe` is `safe`, a kind of `bound.shapes` is `bound`, and another kind is `none`.
|
|
92
|
+
*/
|
|
93
|
+
export function shapeLevel(use: Pick<ShapeSupport, "shapes" | "safe" | "bound">, kind: ShapeKind): ShapeLevel {
|
|
94
|
+
const level = ruleLevel(use, kind);
|
|
95
|
+
if (level !== "none") return level;
|
|
96
|
+
return use.bound?.shapes.includes(kind) === true ? "bound" : "none";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* This function gives the problems of a list of measures and rules. An empty list tells that the list is good.
|
|
101
|
+
* 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`.
|
|
102
|
+
* It also refuses a `bound` without a list of kinds, and a `side` that is not `upper` or `lower`.
|
|
103
|
+
*/
|
|
104
|
+
export function shapeSupportProblems(uses: readonly ShapeSupport[]): string[] {
|
|
105
|
+
const out: string[] = [];
|
|
106
|
+
for (const use of uses) {
|
|
107
|
+
const bound: unknown = use.bound;
|
|
108
|
+
const isBound = typeof bound === "object" && bound !== null && !Array.isArray(bound);
|
|
109
|
+
if (bound !== undefined && !isBound) out.push(`${use.id}: the bound value is not an object with shapes and a side`);
|
|
110
|
+
const boundShapes = isBound ? (bound as { readonly shapes?: unknown }).shapes : undefined;
|
|
111
|
+
for (const [name, list] of [["shapes", use.shapes], ["safe", use.safe], ["bound", boundShapes]] as const) {
|
|
112
|
+
if (list === undefined && (name !== "bound" || !isBound)) continue;
|
|
113
|
+
if (!Array.isArray(list)) {
|
|
114
|
+
out.push(`${use.id}: the ${name === "bound" ? "bound shapes" : name} value is not a list of shape kinds`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
list.forEach((k: unknown, i) => {
|
|
118
|
+
if (!isShapeKind(k)) out.push(`${use.id}: the ${name} kind ${String(k)} is not a declared kind`);
|
|
119
|
+
else if (list.indexOf(k) !== i) out.push(`${use.id}: the ${name} list has the kind ${k} two times`);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const exact: readonly unknown[] = use.shapes === undefined ? DEFAULT_SHAPES : Array.isArray(use.shapes) ? use.shapes : [];
|
|
123
|
+
if (Array.isArray(use.safe)) for (const k of new Set<unknown>(use.safe)) if (exact.includes(k)) out.push(`${use.id}: the kind ${String(k)} is in shapes and in safe`);
|
|
124
|
+
if (!isBound) continue;
|
|
125
|
+
const side: unknown = (bound as { readonly side?: unknown }).side;
|
|
126
|
+
if (side !== "upper" && side !== "lower") out.push(`${use.id}: the bound side ${String(side)} is not upper or lower`);
|
|
127
|
+
if (!Array.isArray(boundShapes)) continue;
|
|
128
|
+
const safe: readonly unknown[] = Array.isArray(use.safe) ? use.safe : [];
|
|
129
|
+
for (const k of new Set<unknown>(boundShapes)) {
|
|
130
|
+
if (exact.includes(k)) out.push(`${use.id}: the kind ${String(k)} is in shapes and in bound`);
|
|
131
|
+
if (safe.includes(k)) out.push(`${use.id}: the kind ${String(k)} is in safe and in bound`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
52
135
|
}
|
|
53
136
|
|
|
54
137
|
const REGION_KIND: Readonly<Record<Region["kind"], ShapeKind>> = { rect: "rect", polygon: "polygon", circle: "circle", ellipse: "free" };
|
|
@@ -67,6 +150,19 @@ export function shapeKindOf(box: LayoutBox): ShapeKind {
|
|
|
67
150
|
return box.shape === undefined ? "rect" : REGION_KIND[box.shape.kind];
|
|
68
151
|
}
|
|
69
152
|
|
|
153
|
+
/**
|
|
154
|
+
* This function gives the exact area of a box, or `undefined` when the contract knows only its bounding box.
|
|
155
|
+
* A box with `shape` gives `regionArea` of the shape. A `rect` box without `shape` gives `w` by `h`.
|
|
156
|
+
* 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`.
|
|
157
|
+
* A box of another kind without `shape` is a bad input, and gives `undefined`.
|
|
158
|
+
*/
|
|
159
|
+
export function boxArea(box: LayoutBox): number | undefined {
|
|
160
|
+
const kind = shapeKindOf(box);
|
|
161
|
+
if (kind === "free") return undefined;
|
|
162
|
+
if (box.shape !== undefined) return regionArea(box.shape);
|
|
163
|
+
return kind === "rect" ? box.w * box.h : undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
70
166
|
/** 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. */
|
|
71
167
|
export function shapeKindsOf(input: Pick<LayoutInput, "boxes">): ShapeKind[] {
|
|
72
168
|
const seen = new Set(input.boxes.map(shapeKindOf));
|
|
@@ -95,22 +191,38 @@ export function shapesOf(use: Pick<ShapeSupport, "shapes">): readonly ShapeKind[
|
|
|
95
191
|
return use.shapes ?? DEFAULT_SHAPES;
|
|
96
192
|
}
|
|
97
193
|
|
|
194
|
+
/** The options of `checkShapes`. */
|
|
195
|
+
export interface ShapeCheck {
|
|
196
|
+
/**
|
|
197
|
+
* The lowest level that `checkShapes` accepts. The default `exact` refuses a kind with only a safe count or a bound value.
|
|
198
|
+
* The value `safe` also accepts a kind of `safe`, as in 0.2.16. Use it only when the run claims that a rule has no break.
|
|
199
|
+
* The value `bound` also accepts a kind of `safe` and a kind of `bound.shapes`. Use it only when the run reads each bound value for the proofs of its side (`docs/SHAPES.md`).
|
|
200
|
+
*/
|
|
201
|
+
readonly level?: "exact" | "safe" | "bound";
|
|
202
|
+
}
|
|
203
|
+
|
|
98
204
|
/**
|
|
99
205
|
* 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.
|
|
100
206
|
* Each break has the claim `shape`, the id of the input, and the first 5 ids of the boxes of the kind.
|
|
207
|
+
* By default, a kind of `safe` or of `bound.shapes` is a break, because the result is not exact.
|
|
208
|
+
* 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.
|
|
101
209
|
*/
|
|
102
|
-
export function checkShapes(input: Pick<LayoutInput, "id" | "boxes">, uses: readonly ShapeSupport[]): ClaimBreak[] {
|
|
210
|
+
export function checkShapes(input: Pick<LayoutInput, "id" | "boxes">, uses: readonly ShapeSupport[], options: ShapeCheck = {}): ClaimBreak[] {
|
|
103
211
|
const kinds = shapeKindsOf(input);
|
|
104
212
|
const out: ClaimBreak[] = [];
|
|
105
213
|
for (const use of uses) {
|
|
106
214
|
const shapes = shapesOf(use);
|
|
107
215
|
for (const kind of kinds) {
|
|
108
|
-
|
|
216
|
+
const level = shapeLevel(use, kind);
|
|
217
|
+
const accepts = level === "exact" || (level === "safe" && (options.level === "safe" || options.level === "bound")) || (level === "bound" && options.level === "bound");
|
|
218
|
+
if (accepts) continue;
|
|
109
219
|
const ids = input.boxes.filter((b) => shapeKindOf(b) === kind).map((b) => b.id);
|
|
110
220
|
const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, and ${ids.length - 5} more` : "");
|
|
221
|
+
const safe =
|
|
222
|
+
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}` : "";
|
|
111
223
|
out.push({
|
|
112
224
|
claim: "shape",
|
|
113
|
-
detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}`,
|
|
225
|
+
detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}${safe}`,
|
|
114
226
|
inputs: [input.id],
|
|
115
227
|
});
|
|
116
228
|
}
|
package/src/strategy-space.ts
CHANGED
|
@@ -52,6 +52,11 @@ export interface MemberRule {
|
|
|
52
52
|
* With `pool`, a config of the option has the members of one entry, and `spaceSize` counts each entry one time.
|
|
53
53
|
*/
|
|
54
54
|
readonly pool?: readonly (readonly StrategyConfig[])[];
|
|
55
|
+
/**
|
|
56
|
+
* The view of the items that the members get. With `bounds`, the option gives its members the bounding box of each item.
|
|
57
|
+
* Then a shape filter checks the members with the kind `rect`, and the option itself with the kinds of the input. Without `view`, the members get the kinds of the input.
|
|
58
|
+
*/
|
|
59
|
+
readonly view?: "bounds";
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
/** The inputs that an option supports. A missing field supports each value. */
|
|
@@ -315,6 +320,8 @@ function optionProblems(stage: SpaceStage, o: StageOption): Problem[] {
|
|
|
315
320
|
const pool: unknown = o.members.pool;
|
|
316
321
|
if (pool !== undefined && (!Array.isArray(pool) || pool.length === 0 || !pool.every((entry: unknown) => Array.isArray(entry))))
|
|
317
322
|
out.push(["pool", `${at} has a pool that is not a list of one or more member lists`]);
|
|
323
|
+
const view: unknown = o.members.view;
|
|
324
|
+
if (view !== undefined && view !== "bounds") out.push(["view", `${at} has the member view ${show(view)}, not "bounds"`]);
|
|
318
325
|
}
|
|
319
326
|
return out;
|
|
320
327
|
}
|
|
@@ -337,6 +344,19 @@ function supportsProblems(supports: unknown, at: string): Problem[] {
|
|
|
337
344
|
const supportsShapes = (o: StageOption, shapes: readonly ShapeKind[] | undefined): boolean =>
|
|
338
345
|
shapes === undefined || o.supports?.shape === undefined || shapes.every((k) => o.supports!.shape!.includes(k));
|
|
339
346
|
|
|
347
|
+
/** The shape kinds that the members of an option with `view: "bounds"` see: a bounding box is a `rect`. */
|
|
348
|
+
const BOUNDS_VIEW: readonly ShapeKind[] = Object.freeze(["rect"] as const);
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* This function gives the shape kinds of the members of a meta option. With `view: "bounds"`, a list with a kind becomes `BOUNDS_VIEW`.
|
|
352
|
+
* Without a list, or with an empty list, the value does not change.
|
|
353
|
+
*/
|
|
354
|
+
const memberShapes = (rule: MemberRule, shapes: readonly ShapeKind[] | undefined): readonly ShapeKind[] | undefined =>
|
|
355
|
+
rule.view === "bounds" && shapes !== undefined && shapes.length > 0 ? BOUNDS_VIEW : shapes;
|
|
356
|
+
|
|
357
|
+
/** This function tells if two lists have the same shape kinds. */
|
|
358
|
+
const sameKinds = (a: readonly ShapeKind[], b: readonly ShapeKind[]): boolean => a.every((k) => b.includes(k)) && b.every((k) => a.includes(k));
|
|
359
|
+
|
|
340
360
|
/** This function gives the problems of the params of one stage choice. */
|
|
341
361
|
function choiceParamProblems(o: StageOption, params: unknown, at: string): Problem[] {
|
|
342
362
|
if (params !== undefined && !isObject(params)) return [["config", `the params of ${at} are not an object`]];
|
|
@@ -411,7 +431,7 @@ function configProblems(space: StrategySpace, config: unknown, member: boolean,
|
|
|
411
431
|
out.push(["depth", `a member has the meta option ${s.name}.${o.id}. The depth of a meta config is 1`]);
|
|
412
432
|
continue;
|
|
413
433
|
}
|
|
414
|
-
out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, shapes));
|
|
434
|
+
out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, memberShapes(o.members, shapes)));
|
|
415
435
|
}
|
|
416
436
|
for (const c of space.constraints ?? []) if (!holds(c, chosen)) out.push(["constraint", `the config breaks the constraint ${describe(c)}`]);
|
|
417
437
|
return out;
|
|
@@ -473,6 +493,7 @@ const toBreaks = (problems: readonly Problem[], inputs: readonly string[] = []):
|
|
|
473
493
|
*
|
|
474
494
|
* A member list that is not an entry of the `pool` of its option gives the claim `pool`.
|
|
475
495
|
* 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.
|
|
496
|
+
* A member of an option with `view: "bounds"` gets the check with the kind `rect`.
|
|
476
497
|
*/
|
|
477
498
|
export function checkConfig(space: StrategySpace, config: StrategyConfig, options: ShapeFilter = {}): ClaimBreak[] {
|
|
478
499
|
return toBreaks(configProblems(space, config, false, options.shapes));
|
|
@@ -738,7 +759,9 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
|
|
|
738
759
|
for (const o of s.options) {
|
|
739
760
|
if (o.members === undefined) continue;
|
|
740
761
|
const choices = new Map<string, StageOption | null>(stages.map((x) => [x.name, x === s ? o : null]));
|
|
741
|
-
const
|
|
762
|
+
const own = memberSpaces.get(o);
|
|
763
|
+
const from = own === undefined ? base : baseCount(own, levels, limit);
|
|
764
|
+
const lists = o.members.pool === undefined ? memberLists(from, o.members, limit) : BigInt(o.members.pool.length);
|
|
742
765
|
if (keeps(space, choices)) out.push({ choices, base: false, weight: times(countOf(o), lists, limit) });
|
|
743
766
|
}
|
|
744
767
|
return out;
|
|
@@ -761,6 +784,8 @@ export function spaceSize(space: StrategySpace, options: SpaceSizeOptions): Spac
|
|
|
761
784
|
* This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
|
|
762
785
|
* A stage can lose all of its options. Then the space has no config with that stage.
|
|
763
786
|
* A pool keeps only the entries whose members fit the smaller space. A meta option without such an entry goes out.
|
|
787
|
+
*
|
|
788
|
+
* The members of an option with `view: "bounds"` come from the space for the kinds of `memberShapes`. `memberSpaces` keeps that space.
|
|
764
789
|
*/
|
|
765
790
|
function restrict(space: StrategySpace, shapes: readonly ShapeKind[] | undefined): StrategySpace {
|
|
766
791
|
if (shapes === undefined) return space;
|
|
@@ -770,14 +795,61 @@ function restrict(space: StrategySpace, shapes: readonly ShapeKind[] | undefined
|
|
|
770
795
|
...(space.parts === undefined ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }),
|
|
771
796
|
};
|
|
772
797
|
const withPool = (s: SpaceStage, o: StageOption): StageOption[] => {
|
|
773
|
-
if (o.members
|
|
774
|
-
const
|
|
798
|
+
if (o.members === undefined) return [o];
|
|
799
|
+
const inner = memberShapes(o.members, shapes)!;
|
|
800
|
+
const own = sameKinds(inner, shapes) ? undefined : restrict(partOf(space, s.name).space, inner);
|
|
801
|
+
const mark = (option: StageOption): StageOption[] => {
|
|
802
|
+
if (own !== undefined) memberSpaces.set(option, own);
|
|
803
|
+
return [option];
|
|
804
|
+
};
|
|
805
|
+
if (o.members.pool === undefined) return own === undefined ? [o] : mark({ ...o });
|
|
806
|
+
const part = own ?? partOf(kept, s.name).space;
|
|
775
807
|
const pool = o.members.pool.filter((entry) => fits(part, entry));
|
|
776
|
-
return pool.length === 0 ? [] :
|
|
808
|
+
return pool.length === 0 ? [] : mark({ ...o, members: { ...o.members, pool } });
|
|
777
809
|
};
|
|
778
810
|
return { ...kept, stages: kept.stages.map((s) => ({ ...s, options: s.options.flatMap((o) => withPool(s, o)) })) };
|
|
779
811
|
}
|
|
780
812
|
|
|
813
|
+
/** 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. */
|
|
814
|
+
const memberSpaces = new WeakMap<StageOption, StrategySpace>();
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* 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.
|
|
818
|
+
* A different meta option has one when its members have a base config. `restrict` keeps only the pool entries that fit.
|
|
819
|
+
*/
|
|
820
|
+
const hasMembers = (space: StrategySpace, stage: string, o: StageOption): boolean =>
|
|
821
|
+
o.members === undefined || o.members.pool !== undefined || combos(memberSpaces.get(o) ?? partOf(space, stage).space, 1, false, 0n).length > 0;
|
|
822
|
+
|
|
823
|
+
/** 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. */
|
|
824
|
+
const hasConfig = (space: StrategySpace, meta: boolean): boolean =>
|
|
825
|
+
combos(space, 1, meta, 0n).some((c) => c.base || [...c.choices].every(([stage, o]) => o === null || hasMembers(space, stage, o)));
|
|
826
|
+
|
|
827
|
+
/** 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. */
|
|
828
|
+
function noSample(space: StrategySpace, kept: StrategySpace, shapes: readonly ShapeKind[], meta: boolean): RangeError {
|
|
829
|
+
const list = (names: readonly string[], one: string, more: string): string => (names.length === 1 ? `${one} ${names[0]}` : `${more} ${names.join(", ")}`);
|
|
830
|
+
const empty = kept.stages.filter((s) => !s.optional && !s.options.some((o) => meta || o.members === undefined)).map((s) => s.name);
|
|
831
|
+
const removed = !meta
|
|
832
|
+
? []
|
|
833
|
+
: space.stages.flatMap((s) =>
|
|
834
|
+
s.options
|
|
835
|
+
.filter((o) => o.members !== undefined && o.members.view !== "bounds" && supportsShapes(o, shapes))
|
|
836
|
+
.filter((o) => {
|
|
837
|
+
const own = kept.stages.find((x) => x.name === s.name)?.options.find((x) => x.id === o.id);
|
|
838
|
+
return own === undefined || !hasMembers(kept, s.name, own);
|
|
839
|
+
})
|
|
840
|
+
.map((o) => `${s.name}.${o.id}`),
|
|
841
|
+
);
|
|
842
|
+
let text = `The space ${space.engine} has no config for a sample with the shape kinds ${shapes.join(", ")}.`;
|
|
843
|
+
if (empty.length > 0) text += ` ${list(empty, "The stage", "The stages")} ${empty.length === 1 ? "has" : "have"} no option for these kinds.`;
|
|
844
|
+
if (removed.length > 0)
|
|
845
|
+
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.`;
|
|
846
|
+
if (empty.length === 0 && removed.length === 0) text += " The constraints remove each config.";
|
|
847
|
+
return new RangeError(text);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** This function gives the count of the base configs of a space that is known to be good. */
|
|
851
|
+
const baseCount = (space: StrategySpace, levels: number, limit: bigint): bigint => combos(space, levels, false, limit).reduce((sum, c) => sum + c.weight!, 0n);
|
|
852
|
+
|
|
781
853
|
/** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
|
|
782
854
|
function assertShapes(shapes: readonly ShapeKind[] | undefined): void {
|
|
783
855
|
if (shapes === undefined) return;
|
|
@@ -854,16 +926,29 @@ function sampleValue(p: SpaceParam, u: number): ParamValue {
|
|
|
854
926
|
/** This function gives a seed of 32 bits from a seed and a step name. */
|
|
855
927
|
const subSeed = (seed: number, step: string): number => Math.floor(stepRandom(seed, step)() * 4294967296);
|
|
856
928
|
|
|
857
|
-
/**
|
|
858
|
-
function
|
|
929
|
+
/**
|
|
930
|
+
* This function gives a sample of a space: Latin hypercube strata over the options, the params and the member counts.
|
|
931
|
+
* `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.
|
|
932
|
+
*/
|
|
933
|
+
function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: boolean, maxTries: number, canon: StrategySpace = space): StrategyConfig[] {
|
|
859
934
|
const stages = space.stages;
|
|
860
935
|
const prefix = prefixes(space);
|
|
936
|
+
// A stage without options can stay empty in a part with a meta option of `view: "bounds"`, because that option replaces the stage.
|
|
937
|
+
const bounded = new Set(stages.filter((s) => meta && s.options.some((o) => memberSpaces.has(o))).map((s) => prefix.get(s.name)!));
|
|
861
938
|
const lists = stages.map((s) => {
|
|
862
939
|
const list: (StageOption | null)[] = s.options.filter((o) => meta || o.members === undefined);
|
|
863
940
|
if (s.optional) list.push(null);
|
|
941
|
+
if (list.length === 0 && bounded.has(prefix.get(s.name)!)) list.push(null);
|
|
864
942
|
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.`);
|
|
865
943
|
return list;
|
|
866
944
|
});
|
|
945
|
+
// A meta option without a member list stays in its list, so the strata of 0.2.16 do not change. A point that chooses it draws again.
|
|
946
|
+
const usable = new Map<StageOption, boolean>();
|
|
947
|
+
const withMembers = (s: SpaceStage, o: StageOption | null | undefined): boolean => {
|
|
948
|
+
if (o === null || o === undefined || o.members === undefined) return true;
|
|
949
|
+
if (!usable.has(o)) usable.set(o, hasMembers(space, s.name, o));
|
|
950
|
+
return usable.get(o)!;
|
|
951
|
+
};
|
|
867
952
|
const points: Map<string, StageOption | null>[] = [];
|
|
868
953
|
let tries = 0;
|
|
869
954
|
for (let round = 0; points.length < n; round++) {
|
|
@@ -882,7 +967,8 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
882
967
|
const m = metaPart.get(prefix.get(s.name)!);
|
|
883
968
|
if (m !== undefined && m !== s.name) choices.set(s.name, null);
|
|
884
969
|
}
|
|
885
|
-
|
|
970
|
+
const filled = stages.every((s) => s.optional === true || choices.get(s.name) !== null || metaPart.has(prefix.get(s.name)!));
|
|
971
|
+
if (filled && keeps(space, choices) && stages.every((s) => withMembers(s, choices.get(s.name)))) points.push(choices);
|
|
886
972
|
}
|
|
887
973
|
}
|
|
888
974
|
const values: Values[][] = points.map(() => stages.map(() => ({})));
|
|
@@ -907,7 +993,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
907
993
|
});
|
|
908
994
|
continue;
|
|
909
995
|
}
|
|
910
|
-
const part = partOf(space, s.name).space;
|
|
996
|
+
const part = memberSpaces.get(o) ?? partOf(space, s.name).space;
|
|
911
997
|
const poolSize = Math.max(n, 4 * o.members.max, 8);
|
|
912
998
|
const seen = new Set<string>();
|
|
913
999
|
const pool = sampleConfigs(part, poolSize, subSeed(seed, `pool:${s.name}:${o.id}`), false, Math.max(1000, 100 * poolSize)).filter((c) => {
|
|
@@ -930,7 +1016,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
930
1016
|
const o = choices.get(s.name)!;
|
|
931
1017
|
out[s.name] = o === null ? null : o.members === undefined ? { option: o.id, params: values[i]![k]! } : { option: o.id, params: values[i]![k]!, members: members[i]![k]! };
|
|
932
1018
|
});
|
|
933
|
-
return canonicalConfig(
|
|
1019
|
+
return canonicalConfig(canon, { space: space.engine, stages: out });
|
|
934
1020
|
});
|
|
935
1021
|
}
|
|
936
1022
|
|
|
@@ -942,6 +1028,8 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
942
1028
|
* - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
|
|
943
1029
|
* - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
|
|
944
1030
|
* - A meta option with a `pool` gives only the entries of its pool. With `shapes`, only the entries whose members support each kind count.
|
|
1031
|
+
* - A sample draws again when a point chooses a meta option without a member list. `spaceSize` gives such an option 0.
|
|
1032
|
+
* - 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.
|
|
945
1033
|
*/
|
|
946
1034
|
export function enumerateSpace(space: StrategySpace, options: EnumerateOptions): StrategyConfig[] {
|
|
947
1035
|
assertSpace(space);
|
|
@@ -956,7 +1044,14 @@ export function enumerateSpace(space: StrategySpace, options: EnumerateOptions):
|
|
|
956
1044
|
const meta = options.meta ?? "skip";
|
|
957
1045
|
if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
|
|
958
1046
|
if (options.n === 0) return [];
|
|
959
|
-
|
|
1047
|
+
const kept = restrict(space, options.shapes);
|
|
1048
|
+
try {
|
|
1049
|
+
return sampleConfigs(kept, options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n), space);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
// Only a space without a config for the shape kinds gives the RangeError. A different failure keeps its own error.
|
|
1052
|
+
if (options.shapes !== undefined && !hasConfig(kept, meta === "sample")) throw noSample(space, kept, options.shapes, meta === "sample");
|
|
1053
|
+
throw error;
|
|
1054
|
+
}
|
|
960
1055
|
}
|
|
961
1056
|
|
|
962
1057
|
/**
|