@mark1russell7/layout-contract 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/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 { Region } from "./geometry.ts";
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
- /** The declared shape kinds, in their order. Only `rect` has all of its geometry. The other kinds have their bounding box only. */
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: false, contains: false, overlap: false } },
38
- { id: "circle", fields: ["cx", "cy", "r"], regions: ["circle"], geometry: { bounds: true, area: false, contains: false, overlap: false } },
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,47 @@ 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
+
63
+ /** The level of a rule on a shape kind: an exact count, a safe count (an upper bound), or no support. */
64
+ export type RuleLevel = "exact" | "safe" | "none";
65
+
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`. */
67
+ export function ruleLevel(rule: Pick<ShapeSupport, "shapes" | "safe">, kind: ShapeKind): RuleLevel {
68
+ if (shapesOf(rule).includes(kind)) return "exact";
69
+ return rule.safe?.includes(kind) === true ? "safe" : "none";
70
+ }
71
+
72
+ /**
73
+ * 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 `safe` that is also in `shapesOf`.
75
+ */
76
+ export function shapeSupportProblems(uses: readonly ShapeSupport[]): string[] {
77
+ const out: string[] = [];
78
+ for (const use of uses) {
79
+ for (const [name, list] of [["shapes", use.shapes], ["safe", use.safe]] as const) {
80
+ if (list === undefined) continue;
81
+ if (!Array.isArray(list)) {
82
+ out.push(`${use.id}: the ${name} value is not a list of shape kinds`);
83
+ continue;
84
+ }
85
+ list.forEach((k: unknown, i) => {
86
+ if (!isShapeKind(k)) out.push(`${use.id}: the ${name} kind ${String(k)} is not a declared kind`);
87
+ else if (list.indexOf(k) !== i) out.push(`${use.id}: the ${name} list has the kind ${k} two times`);
88
+ });
89
+ }
90
+ if (!Array.isArray(use.safe)) continue;
91
+ 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`);
93
+ }
94
+ return out;
52
95
  }
53
96
 
54
97
  const REGION_KIND: Readonly<Record<Region["kind"], ShapeKind>> = { rect: "rect", polygon: "polygon", circle: "circle", ellipse: "free" };
@@ -67,6 +110,19 @@ export function shapeKindOf(box: LayoutBox): ShapeKind {
67
110
  return box.shape === undefined ? "rect" : REGION_KIND[box.shape.kind];
68
111
  }
69
112
 
113
+ /**
114
+ * This function gives the exact area of a box, or `undefined` when the contract knows only its bounding box.
115
+ * A box with `shape` gives `regionArea` of the shape. A `rect` box without `shape` gives `w` by `h`.
116
+ * 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`.
117
+ * A box of another kind without `shape` is a bad input, and gives `undefined`.
118
+ */
119
+ export function boxArea(box: LayoutBox): number | undefined {
120
+ const kind = shapeKindOf(box);
121
+ if (kind === "free") return undefined;
122
+ if (box.shape !== undefined) return regionArea(box.shape);
123
+ return kind === "rect" ? box.w * box.h : undefined;
124
+ }
125
+
70
126
  /** 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
127
  export function shapeKindsOf(input: Pick<LayoutInput, "boxes">): ShapeKind[] {
72
128
  const seen = new Set(input.boxes.map(shapeKindOf));
@@ -95,22 +151,34 @@ export function shapesOf(use: Pick<ShapeSupport, "shapes">): readonly ShapeKind[
95
151
  return use.shapes ?? DEFAULT_SHAPES;
96
152
  }
97
153
 
154
+ /** The options of `checkShapes`. */
155
+ export interface ShapeCheck {
156
+ /**
157
+ * The lowest level of a rule that `checkShapes` accepts. The default `exact` refuses a kind with only a safe count.
158
+ * The value `safe` also accepts a kind of `safe`. Use it only when the run claims that a rule has no break.
159
+ */
160
+ readonly level?: "exact" | "safe";
161
+ }
162
+
98
163
  /**
99
164
  * 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
165
  * 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 count of the rule is not exact. With the level `safe`, it is not a break.
101
167
  */
102
- export function checkShapes(input: Pick<LayoutInput, "id" | "boxes">, uses: readonly ShapeSupport[]): ClaimBreak[] {
168
+ export function checkShapes(input: Pick<LayoutInput, "id" | "boxes">, uses: readonly ShapeSupport[], options: ShapeCheck = {}): ClaimBreak[] {
103
169
  const kinds = shapeKindsOf(input);
104
170
  const out: ClaimBreak[] = [];
105
171
  for (const use of uses) {
106
172
  const shapes = shapesOf(use);
107
173
  for (const kind of kinds) {
108
- if (shapes.includes(kind)) continue;
174
+ const level = ruleLevel(use, kind);
175
+ if (level === "exact" || (level === "safe" && options.level === "safe")) continue;
109
176
  const ids = input.boxes.filter((b) => shapeKindOf(b) === kind).map((b) => b.id);
110
177
  const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, and ${ids.length - 5} more` : "");
178
+ const safe = level === "safe" ? `. The count of ${use.id} on ${kind} is only safe` : "";
111
179
  out.push({
112
180
  claim: "shape",
113
- detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}`,
181
+ detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}${safe}`,
114
182
  inputs: [input.id],
115
183
  });
116
184
  }
@@ -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 lists = o.members.pool === undefined ? memberLists(base, o.members, limit) : BigInt(o.members.pool.length);
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,27 @@ 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?.pool === undefined) return [o];
774
- const part = partOf(kept, s.name).space;
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 ? [] : [{ ...o, members: { ...o.members, pool } }];
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
+ /** This function gives the count of the base configs of a space that is known to be good. */
817
+ const baseCount = (space: StrategySpace, levels: number, limit: bigint): bigint => combos(space, levels, false, limit).reduce((sum, c) => sum + c.weight!, 0n);
818
+
781
819
  /** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
782
820
  function assertShapes(shapes: readonly ShapeKind[] | undefined): void {
783
821
  if (shapes === undefined) return;
@@ -854,13 +892,19 @@ function sampleValue(p: SpaceParam, u: number): ParamValue {
854
892
  /** This function gives a seed of 32 bits from a seed and a step name. */
855
893
  const subSeed = (seed: number, step: string): number => Math.floor(stepRandom(seed, step)() * 4294967296);
856
894
 
857
- /** This function gives a sample of a space: Latin hypercube strata over the options, the params and the member counts. */
858
- function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: boolean, maxTries: number): StrategyConfig[] {
895
+ /**
896
+ * This function gives a sample of a space: Latin hypercube strata over the options, the params and the member counts.
897
+ * `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.
898
+ */
899
+ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: boolean, maxTries: number, canon: StrategySpace = space): StrategyConfig[] {
859
900
  const stages = space.stages;
860
901
  const prefix = prefixes(space);
902
+ // A stage without options can stay empty in a part with a meta option of `view: "bounds"`, because that option replaces the stage.
903
+ const bounded = new Set(stages.filter((s) => meta && s.options.some((o) => memberSpaces.has(o))).map((s) => prefix.get(s.name)!));
861
904
  const lists = stages.map((s) => {
862
905
  const list: (StageOption | null)[] = s.options.filter((o) => meta || o.members === undefined);
863
906
  if (s.optional) list.push(null);
907
+ if (list.length === 0 && bounded.has(prefix.get(s.name)!)) list.push(null);
864
908
  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
909
  return list;
866
910
  });
@@ -882,7 +926,8 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
882
926
  const m = metaPart.get(prefix.get(s.name)!);
883
927
  if (m !== undefined && m !== s.name) choices.set(s.name, null);
884
928
  }
885
- if (keeps(space, choices)) points.push(choices);
929
+ 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);
886
931
  }
887
932
  }
888
933
  const values: Values[][] = points.map(() => stages.map(() => ({})));
@@ -907,7 +952,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
907
952
  });
908
953
  continue;
909
954
  }
910
- const part = partOf(space, s.name).space;
955
+ const part = memberSpaces.get(o) ?? partOf(space, s.name).space;
911
956
  const poolSize = Math.max(n, 4 * o.members.max, 8);
912
957
  const seen = new Set<string>();
913
958
  const pool = sampleConfigs(part, poolSize, subSeed(seed, `pool:${s.name}:${o.id}`), false, Math.max(1000, 100 * poolSize)).filter((c) => {
@@ -930,7 +975,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
930
975
  const o = choices.get(s.name)!;
931
976
  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
977
  });
933
- return canonicalConfig(space, { space: space.engine, stages: out });
978
+ return canonicalConfig(canon, { space: space.engine, stages: out });
934
979
  });
935
980
  }
936
981
 
@@ -956,7 +1001,7 @@ export function enumerateSpace(space: StrategySpace, options: EnumerateOptions):
956
1001
  const meta = options.meta ?? "skip";
957
1002
  if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
958
1003
  if (options.n === 0) return [];
959
- return sampleConfigs(restrict(space, options.shapes), options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n));
1004
+ return sampleConfigs(restrict(space, options.shapes), options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n), space);
960
1005
  }
961
1006
 
962
1007
  /**
package/src/turn.ts CHANGED
@@ -18,14 +18,20 @@ export function turnPoint(x: number, y: number, h: number): [number, number] {
18
18
  return [h - y, x];
19
19
  }
20
20
 
21
- /** This function gives a port of a box of height `h` after the turn. Each value of the port turns, also a port with only `x` or only `y`. */
21
+ /**
22
+ * This function gives a port of a box of height `h` after the turn. Each value of the port turns, also a port with only `x` or only `y`.
23
+ * The start of a side is its top end or its left end. Thus `left` goes to `top` and `right` goes to `bottom` with `along` changed to `1 - along`.
24
+ * `top` goes to `right` and `bottom` goes to `left` with the same `along`.
25
+ */
22
26
  export function turnPort(port: Port, h: number): Port {
23
- const { side, x, y, ...rest } = port;
27
+ const { side, x, y, along, ...rest } = port;
28
+ const flips = side === "left" || side === "right";
24
29
  return {
25
30
  ...rest,
26
31
  ...(side === undefined ? {} : { side: turnSide(side) }),
27
32
  ...(y === undefined ? {} : { x: h - y }),
28
33
  ...(x === undefined ? {} : { y: x }),
34
+ ...(along === undefined ? {} : { along: flips ? 1 - along : along }),
29
35
  };
30
36
  }
31
37