@mark1russell7/layout-contract 0.2.16 → 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 +6 -2
- package/build/index.d.ts +36 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +78 -14
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/measures.ts +6 -1
- package/src/shapes.ts +58 -14
- package/src/strategy-space.ts +52 -2
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/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/shapes.ts
CHANGED
|
@@ -58,28 +58,60 @@ export interface ShapeSupport<Id extends string = string> {
|
|
|
58
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
59
|
*/
|
|
60
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";
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
/** The level of a rule on a shape kind: an exact count, a safe count (an upper bound), or no support. */
|
|
64
78
|
export type RuleLevel = "exact" | "safe" | "none";
|
|
65
79
|
|
|
66
|
-
/** 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`. */
|
|
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`. */
|
|
67
81
|
export function ruleLevel(rule: Pick<ShapeSupport, "shapes" | "safe">, kind: ShapeKind): RuleLevel {
|
|
68
82
|
if (shapesOf(rule).includes(kind)) return "exact";
|
|
69
83
|
return rule.safe?.includes(kind) === true ? "safe" : "none";
|
|
70
84
|
}
|
|
71
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
|
+
|
|
72
99
|
/**
|
|
73
100
|
* This function gives the problems of a list of measures and rules. An empty list tells that the list is good.
|
|
74
|
-
* It refuses an unknown kind, a kind two times in one list, and a kind in
|
|
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`.
|
|
75
103
|
*/
|
|
76
104
|
export function shapeSupportProblems(uses: readonly ShapeSupport[]): string[] {
|
|
77
105
|
const out: string[] = [];
|
|
78
106
|
for (const use of uses) {
|
|
79
|
-
|
|
80
|
-
|
|
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;
|
|
81
113
|
if (!Array.isArray(list)) {
|
|
82
|
-
out.push(`${use.id}: the ${name} value is not a list of shape kinds`);
|
|
114
|
+
out.push(`${use.id}: the ${name === "bound" ? "bound shapes" : name} value is not a list of shape kinds`);
|
|
83
115
|
continue;
|
|
84
116
|
}
|
|
85
117
|
list.forEach((k: unknown, i) => {
|
|
@@ -87,9 +119,17 @@ export function shapeSupportProblems(uses: readonly ShapeSupport[]): string[] {
|
|
|
87
119
|
else if (list.indexOf(k) !== i) out.push(`${use.id}: the ${name} list has the kind ${k} two times`);
|
|
88
120
|
});
|
|
89
121
|
}
|
|
90
|
-
if (!Array.isArray(use.safe)) continue;
|
|
91
122
|
const exact: readonly unknown[] = use.shapes === undefined ? DEFAULT_SHAPES : Array.isArray(use.shapes) ? use.shapes : [];
|
|
92
|
-
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`);
|
|
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
|
+
}
|
|
93
133
|
}
|
|
94
134
|
return out;
|
|
95
135
|
}
|
|
@@ -154,16 +194,18 @@ export function shapesOf(use: Pick<ShapeSupport, "shapes">): readonly ShapeKind[
|
|
|
154
194
|
/** The options of `checkShapes`. */
|
|
155
195
|
export interface ShapeCheck {
|
|
156
196
|
/**
|
|
157
|
-
* The lowest level
|
|
158
|
-
* The value `safe` also accepts a kind of `safe
|
|
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`).
|
|
159
200
|
*/
|
|
160
|
-
readonly level?: "exact" | "safe";
|
|
201
|
+
readonly level?: "exact" | "safe" | "bound";
|
|
161
202
|
}
|
|
162
203
|
|
|
163
204
|
/**
|
|
164
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.
|
|
165
206
|
* Each break has the claim `shape`, the id of the input, and the first 5 ids of the boxes of the kind.
|
|
166
|
-
* By default, a kind of `safe` is a break, because the
|
|
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.
|
|
167
209
|
*/
|
|
168
210
|
export function checkShapes(input: Pick<LayoutInput, "id" | "boxes">, uses: readonly ShapeSupport[], options: ShapeCheck = {}): ClaimBreak[] {
|
|
169
211
|
const kinds = shapeKindsOf(input);
|
|
@@ -171,11 +213,13 @@ export function checkShapes(input: Pick<LayoutInput, "id" | "boxes">, uses: read
|
|
|
171
213
|
for (const use of uses) {
|
|
172
214
|
const shapes = shapesOf(use);
|
|
173
215
|
for (const kind of kinds) {
|
|
174
|
-
const level =
|
|
175
|
-
|
|
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;
|
|
176
219
|
const ids = input.boxes.filter((b) => shapeKindOf(b) === kind).map((b) => b.id);
|
|
177
220
|
const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, and ${ids.length - 5} more` : "");
|
|
178
|
-
const safe =
|
|
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}` : "";
|
|
179
223
|
out.push({
|
|
180
224
|
claim: "shape",
|
|
181
225
|
detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}${safe}`,
|
package/src/strategy-space.ts
CHANGED
|
@@ -813,6 +813,40 @@ function restrict(space: StrategySpace, shapes: readonly ShapeKind[] | undefined
|
|
|
813
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
814
|
const memberSpaces = new WeakMap<StageOption, StrategySpace>();
|
|
815
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
|
+
|
|
816
850
|
/** This function gives the count of the base configs of a space that is known to be good. */
|
|
817
851
|
const baseCount = (space: StrategySpace, levels: number, limit: bigint): bigint => combos(space, levels, false, limit).reduce((sum, c) => sum + c.weight!, 0n);
|
|
818
852
|
|
|
@@ -908,6 +942,13 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
908
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.`);
|
|
909
943
|
return list;
|
|
910
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
|
+
};
|
|
911
952
|
const points: Map<string, StageOption | null>[] = [];
|
|
912
953
|
let tries = 0;
|
|
913
954
|
for (let round = 0; points.length < n; round++) {
|
|
@@ -927,7 +968,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
927
968
|
if (m !== undefined && m !== s.name) choices.set(s.name, null);
|
|
928
969
|
}
|
|
929
970
|
const filled = stages.every((s) => s.optional === true || choices.get(s.name) !== null || metaPart.has(prefix.get(s.name)!));
|
|
930
|
-
if (filled && keeps(space, choices)) points.push(choices);
|
|
971
|
+
if (filled && keeps(space, choices) && stages.every((s) => withMembers(s, choices.get(s.name)))) points.push(choices);
|
|
931
972
|
}
|
|
932
973
|
}
|
|
933
974
|
const values: Values[][] = points.map(() => stages.map(() => ({})));
|
|
@@ -987,6 +1028,8 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
|
|
|
987
1028
|
* - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
|
|
988
1029
|
* - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
|
|
989
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.
|
|
990
1033
|
*/
|
|
991
1034
|
export function enumerateSpace(space: StrategySpace, options: EnumerateOptions): StrategyConfig[] {
|
|
992
1035
|
assertSpace(space);
|
|
@@ -1001,7 +1044,14 @@ export function enumerateSpace(space: StrategySpace, options: EnumerateOptions):
|
|
|
1001
1044
|
const meta = options.meta ?? "skip";
|
|
1002
1045
|
if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
|
|
1003
1046
|
if (options.n === 0) return [];
|
|
1004
|
-
|
|
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
|
+
}
|
|
1005
1055
|
}
|
|
1006
1056
|
|
|
1007
1057
|
/**
|