@mark1russell7/layout-contract 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/build/index.d.ts +475 -150
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1022 -40
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/families.ts +618 -0
- package/src/hash.ts +4 -2
- package/src/index.ts +2 -0
- package/src/input.ts +41 -0
- package/src/measures.ts +21 -10
- package/src/record.ts +11 -2
- package/src/rules.ts +84 -9
- package/src/shapes.ts +119 -0
- package/src/strategy-space.ts +177 -19
package/src/input.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Margin, Region, Side } from "./geometry.ts";
|
|
2
|
+
import type { ShapeKind } from "./shapes.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A port of a box: a place where edges go into the box.
|
|
@@ -28,6 +29,11 @@ export interface LayoutBox {
|
|
|
28
29
|
readonly group?: string;
|
|
29
30
|
/** A region relative to the origin of the box. Without it, the box is the rectangle `w` by `h`. */
|
|
30
31
|
readonly shape?: Region;
|
|
32
|
+
/**
|
|
33
|
+
* The shape kind of the box (`docs/SHAPES.md`). Without it, `shapeKindOf` gives the kind of `shape`, or `rect` without a shape.
|
|
34
|
+
* The kind `free` tells that the contract knows only the bounding box `w` by `h`.
|
|
35
|
+
*/
|
|
36
|
+
readonly shapeKind?: ShapeKind;
|
|
31
37
|
readonly ports?: readonly Port[];
|
|
32
38
|
/** A ring around the box. No other box enters it. Only a route from a port of the box enters it, at a right angle. */
|
|
33
39
|
readonly margin?: Margin;
|
|
@@ -80,6 +86,19 @@ export interface LayoutGenerator {
|
|
|
80
86
|
readonly seed: number;
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
/**
|
|
90
|
+
* One minimum distance between two boxes (`a` and `b`). It replaces the two margins of the boxes for this pair, and the global `gap`.
|
|
91
|
+
* The order of `a` and `b` has no meaning. A pair without an entry keeps the margins and the `gap`.
|
|
92
|
+
*/
|
|
93
|
+
export interface PairGap {
|
|
94
|
+
readonly a: string;
|
|
95
|
+
readonly b: string;
|
|
96
|
+
/** The minimum distance between the two boxes, in units: a finite number of 0 or more. */
|
|
97
|
+
readonly gap: number;
|
|
98
|
+
/** The count of the channels that the gap has space for. It is information only: a whole number of 0 or more. */
|
|
99
|
+
readonly lanes?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
83
102
|
/** The cell of an input in the taxonomy: one value for each dimension. The dimensions are in `docs/FORMAT.md` of `packing`. */
|
|
84
103
|
export interface LayoutCell {
|
|
85
104
|
readonly containerSize?: "fixed" | "freeHeight" | "freeWidth" | "freeAspect" | "freeArea";
|
|
@@ -92,6 +111,8 @@ export interface LayoutCell {
|
|
|
92
111
|
readonly edgeShape?: "random" | "clustered" | "hubChain" | "longRange" | "tree" | "bundled";
|
|
93
112
|
readonly groups?: "none" | "flat" | "nested";
|
|
94
113
|
readonly ports?: "free" | "sides" | "fixed";
|
|
114
|
+
/** The shape kinds of the boxes: one kind, or `mixed` for more kinds. Without it, each box is a `rect`. `itemShapeOf` gives the value. */
|
|
115
|
+
readonly itemShape?: ShapeKind | "mixed";
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
/** An input of the corpus: the format `layout-input/1`. */
|
|
@@ -116,6 +137,8 @@ export interface LayoutInput {
|
|
|
116
137
|
readonly container?: LayoutContainer;
|
|
117
138
|
/** The free space between two boxes with their margins. Routes use it. */
|
|
118
139
|
readonly gap?: number;
|
|
140
|
+
/** The distances of some pairs of boxes. An entry replaces the margins and the `gap` for its pair. `canonicalInput` sorts the list. */
|
|
141
|
+
readonly pairGaps?: readonly PairGap[];
|
|
119
142
|
readonly boxes: readonly LayoutBox[];
|
|
120
143
|
readonly edges?: readonly LayoutEdge[];
|
|
121
144
|
readonly obstacles?: readonly Obstacle[];
|
|
@@ -133,6 +156,7 @@ export interface PackInput {
|
|
|
133
156
|
readonly items: readonly Item[];
|
|
134
157
|
readonly obstacles?: readonly Obstacle[];
|
|
135
158
|
readonly gap?: number;
|
|
159
|
+
readonly pairGaps?: readonly PairGap[];
|
|
136
160
|
}
|
|
137
161
|
|
|
138
162
|
/** This function gives the packer input of a file. An input without a container gets `{ fixed: "none" }`, so the packer picks the size. */
|
|
@@ -142,5 +166,22 @@ export function toPackInput(input: LayoutInput): PackInput {
|
|
|
142
166
|
items: input.boxes,
|
|
143
167
|
...(input.obstacles === undefined ? {} : { obstacles: input.obstacles }),
|
|
144
168
|
...(input.gap === undefined ? {} : { gap: input.gap }),
|
|
169
|
+
...(input.pairGaps === undefined ? {} : { pairGaps: input.pairGaps }),
|
|
145
170
|
};
|
|
146
171
|
}
|
|
172
|
+
|
|
173
|
+
/** This function compares two strings by their UTF-16 code units, as `Array.prototype.sort` does without a compare function. */
|
|
174
|
+
const byCode = (x: string, y: string): number => (x < y ? -1 : x > y ? 1 : 0);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* This function gives the canonical form of an input. `documentHash` uses it for each `layout-input/1` document.
|
|
178
|
+
* Each pair of `pairGaps` gets `a` before `b`, and the list is sorted by `a` and then by `b`.
|
|
179
|
+
* An input without `pairGaps` is the same object, so its hash does not change.
|
|
180
|
+
*/
|
|
181
|
+
export function canonicalInput<T extends LayoutInput>(input: T): T {
|
|
182
|
+
if (!Array.isArray(input.pairGaps)) return input;
|
|
183
|
+
const pairGaps = input.pairGaps
|
|
184
|
+
.map((p): PairGap => (byCode(p.a, p.b) > 0 ? { ...p, a: p.b, b: p.a } : p))
|
|
185
|
+
.sort((x, y) => byCode(x.a, y.a) || byCode(x.b, y.b));
|
|
186
|
+
return { ...input, pairGaps };
|
|
187
|
+
}
|
package/src/measures.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SHAPE_KIND_IDS, type ShapeKind } from "./shapes.ts";
|
|
1
2
|
import { RECORD_EPS, RECORD_SCALE } from "./tolerance.ts";
|
|
2
3
|
|
|
3
4
|
/** The direction of a measure: the side of the better values. */
|
|
@@ -29,6 +30,8 @@ export interface MeasureSpec<Id extends string = string> {
|
|
|
29
30
|
readonly minEffect?: number | { readonly rel: number };
|
|
30
31
|
/** The role of the measure in the joint selector. */
|
|
31
32
|
readonly role?: "guard" | "change-guard" | "diagnostic" | "time";
|
|
33
|
+
/** The shape kinds of the boxes that the measure supports. Without it, the measure supports only `rect`. `checkShapes` refuses the other kinds. */
|
|
34
|
+
readonly shapes?: readonly ShapeKind[];
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/** The id of a joint measure. `docs/JOINT-MEASURES.md` gives the definitions. */
|
|
@@ -43,17 +46,25 @@ export type JointMeasureId =
|
|
|
43
46
|
| "joint.proxy.rank"
|
|
44
47
|
| "joint.ms";
|
|
45
48
|
|
|
46
|
-
/** The
|
|
49
|
+
/** The shape kinds of a joint measure that reads only the bounding boxes, the centers and the positions of the boxes. */
|
|
50
|
+
const BOUNDS_ONLY = SHAPE_KIND_IDS;
|
|
51
|
+
/** The shape kinds of a joint measure that reads the area `w` by `h` of each box. */
|
|
52
|
+
const AREA: readonly ShapeKind[] = ["rect"];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The specs of the joint measures, in the order of `docs/JOINT-MEASURES.md`.
|
|
56
|
+
* `joint.fill` and `joint.straight.length` read the area of each box, so they support only `rect`. `docs/SHAPES.md` tells the reasons.
|
|
57
|
+
*/
|
|
47
58
|
export const JOINT_MEASURES: readonly MeasureSpec<JointMeasureId>[] = Object.freeze([
|
|
48
|
-
{ id: "joint.fill", minEffect: 0.001, unit: "0 to 1", direction: "higher", min: 0, max: 1, floor: 1, role: "guard" },
|
|
49
|
-
{ id: "joint.aspectError", minEffect: 0.01, unit: "natural log", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard" },
|
|
50
|
-
{ id: "joint.spill", minEffect: 0.001, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.001, role: "guard" },
|
|
51
|
-
{ id: "joint.straight.crossings", minEffect: 0, unit: "count", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0, role: "guard" },
|
|
52
|
-
{ id: "joint.straight.length", minEffect: 0.01, unit: "S for each edge", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard" },
|
|
53
|
-
{ id: "joint.stability.mean", minEffect: 0.005, unit: "W", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.005, role: "change-guard" },
|
|
54
|
-
{ id: "joint.stability.moved", minEffect: 0.02, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.02, role: "change-guard" },
|
|
55
|
-
{ id: "joint.proxy.rank", unit: "-1 to 1", direction: "higher", min: -1, max: 1, role: "diagnostic" },
|
|
56
|
-
{ id: "joint.ms", unit: "milliseconds", direction: "lower", min: 0, max: Infinity, role: "time" },
|
|
59
|
+
{ id: "joint.fill", minEffect: 0.001, unit: "0 to 1", direction: "higher", min: 0, max: 1, floor: 1, role: "guard", shapes: AREA },
|
|
60
|
+
{ id: "joint.aspectError", minEffect: 0.01, unit: "natural log", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard", shapes: BOUNDS_ONLY },
|
|
61
|
+
{ id: "joint.spill", minEffect: 0.001, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.001, role: "guard", shapes: BOUNDS_ONLY },
|
|
62
|
+
{ id: "joint.straight.crossings", minEffect: 0, unit: "count", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0, role: "guard", shapes: BOUNDS_ONLY },
|
|
63
|
+
{ id: "joint.straight.length", minEffect: 0.01, unit: "S for each edge", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.01, role: "guard", shapes: AREA },
|
|
64
|
+
{ id: "joint.stability.mean", minEffect: 0.005, unit: "W", direction: "lower", min: 0, max: Infinity, floor: 0, tol: 0.005, role: "change-guard", shapes: BOUNDS_ONLY },
|
|
65
|
+
{ id: "joint.stability.moved", minEffect: 0.02, unit: "0 to 1", direction: "lower", min: 0, max: 1, floor: 0, tol: 0.02, role: "change-guard", shapes: BOUNDS_ONLY },
|
|
66
|
+
{ id: "joint.proxy.rank", unit: "-1 to 1", direction: "higher", min: -1, max: 1, role: "diagnostic", shapes: BOUNDS_ONLY },
|
|
67
|
+
{ id: "joint.ms", unit: "milliseconds", direction: "lower", min: 0, max: Infinity, role: "time", shapes: BOUNDS_ONLY },
|
|
57
68
|
] as const);
|
|
58
69
|
|
|
59
70
|
/** The threshold of `joint.stability.moved`, in widths `W` of the base. */
|
package/src/record.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Tolerance } from "./tolerance.ts";
|
|
2
2
|
import type { LayoutInput } from "./input.ts";
|
|
3
|
+
import { shapeKindsOf, type ShapeKind } from "./shapes.ts";
|
|
3
4
|
|
|
4
5
|
/** The stage of a record: the packer, the router, or both together. */
|
|
5
6
|
export type Stage = "pack" | "route" | "joint";
|
|
@@ -21,6 +22,8 @@ export interface LayoutRecord {
|
|
|
21
22
|
readonly seed: number;
|
|
22
23
|
readonly nodes: number;
|
|
23
24
|
readonly edges: number;
|
|
25
|
+
/** The shape kinds of the boxes of the input, in the order of `SHAPE_KINDS`. Without it, each box is a `rect`. */
|
|
26
|
+
readonly shapes?: readonly ShapeKind[];
|
|
24
27
|
/** The value of each measure, by its name: `pack.*`, `route.*` or `joint.*`. */
|
|
25
28
|
readonly measures: Readonly<Record<string, number>>;
|
|
26
29
|
/** The count of the breaks of each hard rule. A good run has 0 for each rule. */
|
|
@@ -67,10 +70,15 @@ export interface RunEnv {
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
/** The facts of a run that the input does not give. */
|
|
70
|
-
export type RunFacts = Omit<LayoutRecord, "schema" | "input" | "inputHash" | "set" | "nodes" | "edges">;
|
|
73
|
+
export type RunFacts = Omit<LayoutRecord, "schema" | "input" | "inputHash" | "set" | "nodes" | "edges" | "shapes">;
|
|
71
74
|
|
|
72
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* This function gives the record of a run. The input gives its id, its hash, its set and the counts of the nodes and the edges.
|
|
77
|
+
* The input also gives `shapes`. An input with only `rect` boxes gets no `shapes`, so its record does not change.
|
|
78
|
+
*/
|
|
73
79
|
export function toLayoutRecord(input: LayoutInput, run: RunFacts): LayoutRecord {
|
|
80
|
+
const shapes = shapeKindsOf(input);
|
|
81
|
+
const onlyRect = shapes.length === 0 || (shapes.length === 1 && shapes[0] === "rect");
|
|
74
82
|
return {
|
|
75
83
|
schema: "layout-record/1",
|
|
76
84
|
...run,
|
|
@@ -79,5 +87,6 @@ export function toLayoutRecord(input: LayoutInput, run: RunFacts): LayoutRecord
|
|
|
79
87
|
set: input.set ?? "",
|
|
80
88
|
nodes: input.boxes.length,
|
|
81
89
|
edges: input.edges?.length ?? 0,
|
|
90
|
+
...(onlyRect ? {} : { shapes }),
|
|
82
91
|
};
|
|
83
92
|
}
|
package/src/rules.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { boundsOf, grow, overlapOf, type Rect, type Region } from "./geometry.ts";
|
|
2
|
-
import type { LayoutBox, LayoutInput, Obstacle } from "./input.ts";
|
|
2
|
+
import type { LayoutBox, LayoutInput, Obstacle, PairGap } from "./input.ts";
|
|
3
3
|
import { applyPack, type PackOutput } from "./output.ts";
|
|
4
|
+
import { SHAPE_KIND_IDS, shapeKindProblem, type ShapeSupport } from "./shapes.ts";
|
|
4
5
|
import { DEFAULT_TOLERANCE, EPS, RECORD_EPS } from "./tolerance.ts";
|
|
5
6
|
|
|
6
7
|
/** This function gives the area of a placed box: its shape or its rectangle, at its position. A box without a position gives `undefined`. */
|
|
@@ -14,20 +15,52 @@ export function placedRect(box: LayoutBox): Rect | undefined {
|
|
|
14
15
|
/**
|
|
15
16
|
* This function counts the pairs of placed boxes whose insides, with their margins, overlap by more than `tolerance` on each axis.
|
|
16
17
|
* A box with a shape counts as its bounding box. That area is larger, thus the count is safe.
|
|
18
|
+
*
|
|
19
|
+
* A pair of `pairGaps`, in either order, uses its `gap` in place of the two margins. One box grows by the gap, and the other box does not grow.
|
|
20
|
+
* Thus the pair breaks the rule only when the boxes are nearer than the gap on each axis. The other pairs do not change.
|
|
17
21
|
*/
|
|
18
|
-
export function countOverlaps(boxes: readonly LayoutBox[], tolerance: number = DEFAULT_TOLERANCE.overlap): number {
|
|
19
|
-
|
|
22
|
+
export function countOverlaps(boxes: readonly LayoutBox[], tolerance: number = DEFAULT_TOLERANCE.overlap, pairGaps?: readonly PairGap[]): number {
|
|
23
|
+
if (pairGaps === undefined || pairGaps.length === 0) return marginOverlaps(boxes, tolerance, undefined);
|
|
24
|
+
const gaps = new Map<string, number>();
|
|
25
|
+
for (const p of pairGaps) {
|
|
26
|
+
const key = pairKey(p.a, p.b);
|
|
27
|
+
if (p.a !== p.b && !gaps.has(key)) gaps.set(key, p.gap);
|
|
28
|
+
}
|
|
29
|
+
const raw = new Map<string, Rect>();
|
|
30
|
+
for (const box of boxes) {
|
|
31
|
+
const r = placedRect(box);
|
|
32
|
+
if (r !== undefined && !raw.has(box.id)) raw.set(box.id, r);
|
|
33
|
+
}
|
|
34
|
+
let count = marginOverlaps(boxes, tolerance, gaps);
|
|
35
|
+
for (const [key, gap] of gaps) {
|
|
36
|
+
const [a, b] = JSON.parse(key) as [string, string];
|
|
37
|
+
const ra = raw.get(a);
|
|
38
|
+
const rb = raw.get(b);
|
|
39
|
+
if (ra === undefined || rb === undefined) continue;
|
|
40
|
+
const o = overlapOf(grow(ra, gap), rb);
|
|
41
|
+
if (o.x > tolerance && o.y > tolerance) count++;
|
|
42
|
+
}
|
|
43
|
+
return count;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** This function gives the key of a pair of box ids, with the same key for each order. */
|
|
47
|
+
const pairKey = (a: string, b: string): string => JSON.stringify(a < b ? [a, b] : [b, a]);
|
|
48
|
+
|
|
49
|
+
/** This function counts the overlaps of the boxes with their margins. It skips the pairs of `skip`. Without `skip`, it is the rule of 0.2.13. */
|
|
50
|
+
function marginOverlaps(boxes: readonly LayoutBox[], tolerance: number, skip: ReadonlyMap<string, number> | undefined): number {
|
|
51
|
+
const rects: { readonly id: string; readonly r: Rect }[] = [];
|
|
20
52
|
for (const box of boxes) {
|
|
21
53
|
const r = placedRect(box);
|
|
22
|
-
if (r !== undefined) rects.push(grow(r, box.margin));
|
|
54
|
+
if (r !== undefined) rects.push({ id: box.id, r: grow(r, box.margin) });
|
|
23
55
|
}
|
|
24
|
-
rects.sort((a, b) => a.x - b.x);
|
|
56
|
+
rects.sort((a, b) => a.r.x - b.r.x);
|
|
25
57
|
let count = 0;
|
|
26
58
|
for (let i = 0; i < rects.length; i++) {
|
|
27
|
-
const a = rects[i]
|
|
59
|
+
const a = rects[i]!.r;
|
|
28
60
|
for (let j = i + 1; j < rects.length; j++) {
|
|
29
|
-
const b = rects[j]
|
|
61
|
+
const b = rects[j]!.r;
|
|
30
62
|
if (b.x >= a.x + a.w - tolerance) break;
|
|
63
|
+
if (skip !== undefined && skip.has(pairKey(rects[i]!.id, rects[j]!.id))) continue;
|
|
31
64
|
const o = overlapOf(a, b);
|
|
32
65
|
if (o.x > tolerance && o.y > tolerance) count++;
|
|
33
66
|
}
|
|
@@ -96,6 +129,18 @@ export interface PackHard {
|
|
|
96
129
|
readonly complete: number;
|
|
97
130
|
}
|
|
98
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The hard rules of a pack, with the shape kinds that each rule supports.
|
|
134
|
+
* `no-overlap` and `in-container` test the bounding box of a shape. That count is exact only for a `rect`.
|
|
135
|
+
* `locked-kept` and `complete` read only the positions and the ids, so they support each kind.
|
|
136
|
+
*/
|
|
137
|
+
export const PACK_HARD_RULES: readonly ShapeSupport<keyof PackHard>[] = Object.freeze([
|
|
138
|
+
{ id: "no-overlap", shapes: ["rect"] },
|
|
139
|
+
{ id: "in-container", shapes: ["rect"] },
|
|
140
|
+
{ id: "locked-kept", shapes: SHAPE_KIND_IDS },
|
|
141
|
+
{ id: "complete", shapes: SHAPE_KIND_IDS },
|
|
142
|
+
] as const);
|
|
143
|
+
|
|
99
144
|
/** This function tells if the inside of `region` and the inside of `rect` have a common point. */
|
|
100
145
|
export function regionHitsRect(region: Region, rect: Rect): boolean {
|
|
101
146
|
if (rect.w <= 0 || rect.h <= 0) return false;
|
|
@@ -266,7 +311,8 @@ export function countObstacleHits(
|
|
|
266
311
|
/**
|
|
267
312
|
* This function gives the count of each hard rule of the output of a packer.
|
|
268
313
|
* It applies the output with `applyPack`. Only the placed boxes and the locked boxes count for the overlaps and the container.
|
|
269
|
-
* `no-overlap` counts the pairs of boxes and the pairs of a box and an obstacle.
|
|
314
|
+
* `no-overlap` counts the pairs of boxes and the pairs of a box and an obstacle. A pair of `pairGaps` uses its gap in place of the margins.
|
|
315
|
+
*
|
|
270
316
|
* A locked box without a placement, or with a move of more than `RECORD_EPS`, breaks `locked-kept`.
|
|
271
317
|
* A locked box without a position in the input is a bad input, not a break. `checkInput` finds it.
|
|
272
318
|
* Another box without a placement that is not in `overflow` breaks `complete`.
|
|
@@ -295,7 +341,7 @@ export function packHard(input: LayoutInput, output: PackOutput, tolerance: numb
|
|
|
295
341
|
}
|
|
296
342
|
}
|
|
297
343
|
return {
|
|
298
|
-
"no-overlap": countOverlaps(kept, tolerance) + countObstacleHits(kept, after.obstacles, tolerance),
|
|
344
|
+
"no-overlap": countOverlaps(kept, tolerance, input.pairGaps) + countObstacleHits(kept, after.obstacles, tolerance),
|
|
299
345
|
"in-container": countOutside({ ...after, boxes: kept }, tolerance, options),
|
|
300
346
|
"locked-kept": lockedMoved,
|
|
301
347
|
complete: incomplete,
|
|
@@ -305,6 +351,7 @@ export function packHard(input: LayoutInput, output: PackOutput, tolerance: numb
|
|
|
305
351
|
/**
|
|
306
352
|
* This function gives the problems of an input file. An empty list tells that the file is good.
|
|
307
353
|
* A problem is a bad input, not a break of a hard rule. Thus an engine refuses the input.
|
|
354
|
+
* In `pairGaps`, an unknown box, a pair of one box, a repeated pair in either order, a bad `gap` and bad `lanes` are problems.
|
|
308
355
|
*/
|
|
309
356
|
export function checkInput(input: LayoutInput): readonly string[] {
|
|
310
357
|
const problems: string[] = [];
|
|
@@ -314,10 +361,38 @@ export function checkInput(input: LayoutInput): readonly string[] {
|
|
|
314
361
|
ids.add(box.id);
|
|
315
362
|
if (!(box.w > 0 && box.h > 0 && Number.isFinite(box.w) && Number.isFinite(box.h))) problems.push(`box ${box.id}: the size is not positive and finite`);
|
|
316
363
|
if (box.locked === true && (box.x === undefined || box.y === undefined)) problems.push(`box ${box.id}: a locked box has no position`);
|
|
364
|
+
const shape = shapeKindProblem(box);
|
|
365
|
+
if (shape !== undefined) problems.push(shape);
|
|
317
366
|
}
|
|
318
367
|
if (countPortsOffBorder(input.boxes) > 0) problems.push("a fixed port is not on the border of its box");
|
|
319
368
|
for (const edge of input.edges ?? []) {
|
|
320
369
|
if (!ids.has(edge.source) || !ids.has(edge.target)) problems.push(`edge ${edge.id}: a box of the edge is not in the input`);
|
|
321
370
|
}
|
|
371
|
+
problems.push(...pairGapProblems(input.pairGaps, ids));
|
|
372
|
+
return problems;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** This function gives the problems of the `pairGaps` of an input. `ids` has the ids of the boxes. */
|
|
376
|
+
function pairGapProblems(pairGaps: LayoutInput["pairGaps"], ids: ReadonlySet<string>): string[] {
|
|
377
|
+
if (pairGaps === undefined) return [];
|
|
378
|
+
if (!Array.isArray(pairGaps)) return ["pairGaps: the value is not a list"];
|
|
379
|
+
const problems: string[] = [];
|
|
380
|
+
const seen = new Map<string, number>();
|
|
381
|
+
pairGaps.forEach((p: PairGap, i) => {
|
|
382
|
+
const at = `pair gap ${i} (${String(p?.a)}, ${String(p?.b)})`;
|
|
383
|
+
if (typeof p !== "object" || p === null || typeof p.a !== "string" || typeof p.b !== "string") {
|
|
384
|
+
problems.push(`pair gap ${i}: the entry has no string ids a and b`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
for (const id of [p.a, p.b]) if (!ids.has(id)) problems.push(`${at}: the box ${id} is not in the input`);
|
|
388
|
+
if (p.a === p.b) problems.push(`${at}: the two ids are the same box`);
|
|
389
|
+
if (typeof p.gap !== "number" || !Number.isFinite(p.gap) || p.gap < 0) problems.push(`${at}: the gap ${String(p.gap)} is not a finite number of 0 or more`);
|
|
390
|
+
if (p.lanes !== undefined && (typeof p.lanes !== "number" || !Number.isInteger(p.lanes) || p.lanes < 0))
|
|
391
|
+
problems.push(`${at}: the lanes value ${String(p.lanes)} is not a whole number of 0 or more`);
|
|
392
|
+
const key = JSON.stringify(p.a < p.b ? [p.a, p.b] : [p.b, p.a]);
|
|
393
|
+
const first = seen.get(key);
|
|
394
|
+
if (first !== undefined) problems.push(`${at}: the pair is also pair gap ${first}`);
|
|
395
|
+
else seen.set(key, i);
|
|
396
|
+
});
|
|
322
397
|
return problems;
|
|
323
398
|
}
|
package/src/shapes.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
import type { ClaimBreak } from "./corpus.ts";
|
|
3
|
+
import type { Region } from "./geometry.ts";
|
|
4
|
+
import type { LayoutBox, LayoutInput } from "./input.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The kind of the shape of an item. A box without `shape` and without `shapeKind` is a `rect`.
|
|
8
|
+
* The set is open: a later version can add a kind. A reader treats an unknown kind as a kind that it does not support.
|
|
9
|
+
*/
|
|
10
|
+
export type ShapeKind = "rect" | "polygon" | "circle" | "free";
|
|
11
|
+
|
|
12
|
+
/** The geometry of a shape kind that this package gives. A `false` value tells that the kind declares the operation, but this package does not supply it. */
|
|
13
|
+
export interface ShapeGeometry {
|
|
14
|
+
/** The bounding box: `boundsOf` and `placedRect`. */
|
|
15
|
+
readonly bounds: boolean;
|
|
16
|
+
/** The area of the shape. */
|
|
17
|
+
readonly area: boolean;
|
|
18
|
+
/** The test that the shape is inside a region: `rectInRegion` for a `rect`. */
|
|
19
|
+
readonly contains: boolean;
|
|
20
|
+
/** The test that two shapes overlap: `overlapOf` for two `rect` shapes. */
|
|
21
|
+
readonly overlap: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The facts of one shape kind: its fields, the kinds of `Region` that give it, and its geometry. */
|
|
25
|
+
export interface ShapeKindSpec {
|
|
26
|
+
readonly id: ShapeKind;
|
|
27
|
+
/** The fields of the outline, relative to the top-left corner of the box. A name with `?` is optional. */
|
|
28
|
+
readonly fields: readonly string[];
|
|
29
|
+
/** The kinds of `LayoutBox.shape` that agree with this kind. */
|
|
30
|
+
readonly regions: readonly Region["kind"][];
|
|
31
|
+
readonly geometry: ShapeGeometry;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The declared shape kinds, in their order. Only `rect` has all of its geometry. The other kinds have their bounding box only. */
|
|
35
|
+
export const SHAPE_KINDS: readonly ShapeKindSpec[] = Object.freeze([
|
|
36
|
+
{ 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 } },
|
|
39
|
+
{ id: "free", fields: ["w", "h", "shape?"], regions: ["rect", "polygon", "circle", "ellipse"], geometry: { bounds: true, area: false, contains: false, overlap: false } },
|
|
40
|
+
] as const);
|
|
41
|
+
|
|
42
|
+
/** The ids of `SHAPE_KINDS`, in their order. */
|
|
43
|
+
export const SHAPE_KIND_IDS: readonly ShapeKind[] = Object.freeze(SHAPE_KINDS.map((k) => k.id));
|
|
44
|
+
|
|
45
|
+
/** The shape kinds of a measure or a rule without `shapes`. The contract measured only rectangles before L46. */
|
|
46
|
+
export const DEFAULT_SHAPES: readonly ShapeKind[] = Object.freeze(["rect"] as const);
|
|
47
|
+
|
|
48
|
+
/** A measure or a rule, with the shape kinds that it supports. Without `shapes`, it supports `DEFAULT_SHAPES`. */
|
|
49
|
+
export interface ShapeSupport<Id extends string = string> {
|
|
50
|
+
readonly id: Id;
|
|
51
|
+
readonly shapes?: readonly ShapeKind[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const REGION_KIND: Readonly<Record<Region["kind"], ShapeKind>> = { rect: "rect", polygon: "polygon", circle: "circle", ellipse: "free" };
|
|
55
|
+
|
|
56
|
+
/** This function tells if a value is a declared shape kind. */
|
|
57
|
+
export function isShapeKind(value: unknown): value is ShapeKind {
|
|
58
|
+
return typeof value === "string" && (SHAPE_KIND_IDS as readonly string[]).includes(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* This function gives the shape kind of a box. A box with `shapeKind` has that kind.
|
|
63
|
+
* Without it, the kind comes from `shape`: no shape is a `rect`, and an ellipse is `free`.
|
|
64
|
+
*/
|
|
65
|
+
export function shapeKindOf(box: LayoutBox): ShapeKind {
|
|
66
|
+
if (box.shapeKind !== undefined) return box.shapeKind;
|
|
67
|
+
return box.shape === undefined ? "rect" : REGION_KIND[box.shape.kind];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 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
|
+
export function shapeKindsOf(input: Pick<LayoutInput, "boxes">): ShapeKind[] {
|
|
72
|
+
const seen = new Set(input.boxes.map(shapeKindOf));
|
|
73
|
+
return SHAPE_KIND_IDS.filter((k) => seen.has(k));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** This function gives the value of the cell dimension `itemShape` of an input: its one kind, `mixed` for more kinds, or `rect` without boxes. */
|
|
77
|
+
export function itemShapeOf(input: Pick<LayoutInput, "boxes">): ShapeKind | "mixed" {
|
|
78
|
+
const kinds = shapeKindsOf(input);
|
|
79
|
+
return kinds.length === 0 ? "rect" : kinds.length === 1 ? kinds[0]! : "mixed";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** This function gives the problem of the shape kind of a box, or `undefined` when `shapeKind` and `shape` agree. */
|
|
83
|
+
export function shapeKindProblem(box: LayoutBox): string | undefined {
|
|
84
|
+
const k: unknown = box.shapeKind;
|
|
85
|
+
if (k === undefined) return undefined;
|
|
86
|
+
const spec = SHAPE_KINDS.find((s) => s.id === k);
|
|
87
|
+
if (spec === undefined) return `box ${box.id}: the shape kind ${String(k)} is not a declared kind`;
|
|
88
|
+
if (box.shape === undefined) return k === "rect" || k === "free" ? undefined : `box ${box.id}: the shape kind ${k} has no shape`;
|
|
89
|
+
if (!spec.regions.includes(box.shape.kind)) return `box ${box.id}: the shape kind ${k} does not agree with a shape of the kind ${box.shape.kind}`;
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** This function gives the shape kinds that a measure or a rule supports. */
|
|
94
|
+
export function shapesOf(use: Pick<ShapeSupport, "shapes">): readonly ShapeKind[] {
|
|
95
|
+
return use.shapes ?? DEFAULT_SHAPES;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 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
|
+
* Each break has the claim `shape`, the id of the input, and the first 5 ids of the boxes of the kind.
|
|
101
|
+
*/
|
|
102
|
+
export function checkShapes(input: Pick<LayoutInput, "id" | "boxes">, uses: readonly ShapeSupport[]): ClaimBreak[] {
|
|
103
|
+
const kinds = shapeKindsOf(input);
|
|
104
|
+
const out: ClaimBreak[] = [];
|
|
105
|
+
for (const use of uses) {
|
|
106
|
+
const shapes = shapesOf(use);
|
|
107
|
+
for (const kind of kinds) {
|
|
108
|
+
if (shapes.includes(kind)) continue;
|
|
109
|
+
const ids = input.boxes.filter((b) => shapeKindOf(b) === kind).map((b) => b.id);
|
|
110
|
+
const shown = ids.slice(0, 5).join(", ") + (ids.length > 5 ? `, and ${ids.length - 5} more` : "");
|
|
111
|
+
out.push({
|
|
112
|
+
claim: "shape",
|
|
113
|
+
detail: `${use.id} supports only the shape kinds ${shapes.join(", ") || "(none)"}, but the input ${input.id} has ${kind} boxes: ${shown}`,
|
|
114
|
+
inputs: [input.id],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|