@mark1russell7/highways 0.2.0 → 0.4.0
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 +265 -9
- package/build/index.d.ts +2168 -101
- package/build/index.d.ts.map +1 -1
- package/build/index.js +11624 -3422
- package/build/index.js.map +1 -1
- package/package.json +4 -1
- package/src/bench/compare.ts +180 -0
- package/src/bench/curves.ts +514 -2
- package/src/bench/links.ts +28 -6
- package/src/bench/measures/bundles.ts +119 -0
- package/src/bench/measures/curves.ts +211 -0
- package/src/bench/measures/groups.ts +122 -0
- package/src/bench/measures/index.ts +9 -0
- package/src/bench/measures/levels.ts +103 -0
- package/src/bench/measures/pareto.ts +37 -0
- package/src/bench/measures/room.ts +43 -0
- package/src/bench/measures/routes.ts +226 -0
- package/src/bench/measures/stability.ts +104 -0
- package/src/bench/measures/stats.ts +114 -0
- package/src/bench/properties.ts +18 -2
- package/src/bench/room.ts +209 -2
- package/src/bench/run.ts +86 -11
- package/src/bench/screen/forms.ts +372 -0
- package/src/bench/screen/generators.ts +54 -0
- package/src/bench/screen/hash.ts +129 -0
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +114 -0
- package/src/bench/screen/screen.ts +390 -0
- package/src/bench/screen/set.ts +136 -0
- package/src/bench/suite.ts +839 -0
- package/src/cases.ts +41 -5
- package/src/curves/clothoid/index.ts +150 -0
- package/src/curves/corner.ts +116 -0
- package/src/curves/fit.ts +79 -0
- package/src/curves/highway/index.ts +636 -0
- package/src/curves/index.ts +16 -1
- package/src/curves/model.ts +4 -2
- package/src/curves/smooth/index.ts +50 -0
- package/src/curves/space.ts +76 -0
- package/src/groups/cut/index.ts +292 -0
- package/src/groups/index.ts +2 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/highways/corridors/index.ts +305 -0
- package/src/highways/follow.ts +155 -0
- package/src/highways/index.ts +7 -0
- package/src/highways/keys/index.ts +15 -0
- package/src/highways/model.ts +111 -0
- package/src/highways/result.ts +34 -0
- package/src/highways/router.ts +40 -0
- package/src/highways/shared-ways/index.ts +15 -0
- package/src/index.ts +6 -0
- package/src/levels.ts +163 -5
- package/src/room/bends.ts +203 -0
- package/src/room/index.ts +28 -0
- package/src/room/levels/index.ts +155 -0
- package/src/routers/sections.ts +15 -40
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +47 -1
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +206 -9
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +55 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +234 -0
- package/src/visibility.ts +190 -1
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import type { Edge, Graph, Node, Port, Side } from "../../model.ts";
|
|
2
|
+
import { documentHash, round3 } from "./hash.ts";
|
|
3
|
+
|
|
4
|
+
/** The side of a port in an input file. The router calls the same sides `north`, `east`, `south` and `west`. */
|
|
5
|
+
export type LayoutSide = "top" | "right" | "bottom" | "left";
|
|
6
|
+
|
|
7
|
+
/** A port of a box in an input file. */
|
|
8
|
+
export interface LayoutPort {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
/** The side of the box. Without it and without `x` and `y`, the router picks the side. */
|
|
11
|
+
readonly side?: LayoutSide;
|
|
12
|
+
/** The fixed place of the port, from the top-left corner of the box. The router does not move a port with `x` and `y`. */
|
|
13
|
+
readonly x?: number;
|
|
14
|
+
readonly y?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A point of a region: `[x, y]`. The y axis points down. */
|
|
18
|
+
export type RegionPoint = readonly [number, number];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* An area of the plane. The y axis points down. A ring of a polygon closes itself: its last point joins its first point.
|
|
22
|
+
* A hole of a polygon does not touch the outer ring. The router reads only the bounding box of a region.
|
|
23
|
+
*/
|
|
24
|
+
export type Region =
|
|
25
|
+
| { readonly kind: "rect"; readonly x: number; readonly y: number; readonly w: number; readonly h: number }
|
|
26
|
+
| { readonly kind: "polygon"; readonly points: readonly RegionPoint[]; readonly holes?: readonly (readonly RegionPoint[])[] }
|
|
27
|
+
| { readonly kind: "circle"; readonly cx: number; readonly cy: number; readonly r: number }
|
|
28
|
+
| { readonly kind: "ellipse"; readonly cx: number; readonly cy: number; readonly rx: number; readonly ry: number };
|
|
29
|
+
|
|
30
|
+
/** An area that the boxes and the routes must not use. The router uses the bounding box of its region as a box without edges. */
|
|
31
|
+
export interface LayoutObstacle {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly region: Region;
|
|
34
|
+
/** The cost to move the obstacle. Only the packer reads it. */
|
|
35
|
+
readonly move?: { readonly cost: number };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A box of an input file. */
|
|
39
|
+
export interface LayoutBox {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly w: number;
|
|
42
|
+
readonly h: number;
|
|
43
|
+
/** The position of the top-left corner. The packer writes it. The router reads it. */
|
|
44
|
+
readonly x?: number;
|
|
45
|
+
readonly y?: number;
|
|
46
|
+
/** The id of the box that holds this box. */
|
|
47
|
+
readonly parent?: string;
|
|
48
|
+
/** The group of the box. */
|
|
49
|
+
readonly group?: string;
|
|
50
|
+
/** This value is `true` when the box is an obstacle: the packer keeps its `x` and `y`. The router ignores it. */
|
|
51
|
+
readonly locked?: true;
|
|
52
|
+
/** The reading order that the caller wants. The router ignores it. */
|
|
53
|
+
readonly rank?: number;
|
|
54
|
+
readonly ports?: readonly LayoutPort[];
|
|
55
|
+
/** The shape of the box, relative to the top-left corner of the box. The router ignores it and uses `w` and `h` as the bounding box. */
|
|
56
|
+
readonly shape?: Region;
|
|
57
|
+
/** The free data of the node. The grouping rule `keys` reads `data.group`. */
|
|
58
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** An edge of an input file. `sourcePort` and `targetPort` name a port of the box at that end. */
|
|
62
|
+
export interface LayoutEdge {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly source: string;
|
|
65
|
+
readonly target: string;
|
|
66
|
+
readonly sourcePort?: string;
|
|
67
|
+
readonly targetPort?: string;
|
|
68
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The change of a variant against its base input. */
|
|
72
|
+
export type LayoutChange = "add" | "remove" | "resize" | "reorder" | "packed";
|
|
73
|
+
|
|
74
|
+
/** The origin of an input: `made-up`, `published:<set>` or `known-best:<construction>`. */
|
|
75
|
+
export type LayoutSource = "made-up" | `published:${string}` | `known-best:${string}`;
|
|
76
|
+
|
|
77
|
+
/** The generator of an input. The same name, version, parameters and seed give the same file, byte for byte. */
|
|
78
|
+
export interface LayoutGenerator {
|
|
79
|
+
readonly name: string;
|
|
80
|
+
readonly version: number;
|
|
81
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
82
|
+
readonly seed: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The cell of an input in the taxonomy of the corpus, with one value for each dimension.
|
|
87
|
+
* The packer reads the first six dimensions. The router reads the last four.
|
|
88
|
+
*/
|
|
89
|
+
export interface LayoutCell {
|
|
90
|
+
readonly containerSize?: "fixed" | "freeWidth" | "freeAspect";
|
|
91
|
+
readonly containerShape?: "rect" | "convex" | "polygon" | "circle" | "ellipse" | "holes";
|
|
92
|
+
readonly obstacles?: "none" | "center" | "scattered" | "wall" | "corridors" | "touching" | "edge";
|
|
93
|
+
readonly sizes?: "identical" | "uniform" | "heavyTail" | "bimodal" | "extremeAspect" | "nearIdentical";
|
|
94
|
+
readonly count?: "1" | "10" | "100" | "1k" | "10k";
|
|
95
|
+
readonly mode?: "offline" | "online" | "incremental";
|
|
96
|
+
readonly edgeDensity?: "sparse" | "medium" | "dense";
|
|
97
|
+
readonly edgeShape?: "random" | "clustered" | "hubChain" | "longRange" | "tree" | "bundled";
|
|
98
|
+
readonly groups?: "none" | "flat" | "nested";
|
|
99
|
+
readonly ports?: "free" | "sides" | "fixed";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The outer rectangle of a packing. The router ignores it. */
|
|
103
|
+
export interface LayoutContainer {
|
|
104
|
+
readonly w?: number;
|
|
105
|
+
readonly h?: number;
|
|
106
|
+
/** `none`: the packer picks the size. `width`: the width stays. `height`: the height stays. `size`: the width and the height stay. */
|
|
107
|
+
readonly fixed: "none" | "width" | "height" | "size";
|
|
108
|
+
/** The ratio `w / h` that the packer aims for. */
|
|
109
|
+
readonly aspect?: number;
|
|
110
|
+
/** The shape of the container. */
|
|
111
|
+
readonly shape?: Region;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The identity of an input in a set: the set, the family, the seed, and the base of a variant. */
|
|
115
|
+
export interface LayoutInputHead {
|
|
116
|
+
/** The id: `<set>/<family>/s<seed>`. */
|
|
117
|
+
readonly id: string;
|
|
118
|
+
readonly set: string;
|
|
119
|
+
readonly family: string;
|
|
120
|
+
readonly seed: number;
|
|
121
|
+
/** The id of the base input of a variant. The stability measures compare the run on the variant with the run on its base. */
|
|
122
|
+
readonly base?: string;
|
|
123
|
+
readonly change?: LayoutChange;
|
|
124
|
+
/** The place of the input in a change sequence. */
|
|
125
|
+
readonly step?: number;
|
|
126
|
+
/** The generator of the input. The default is `null`: a hand-made input. */
|
|
127
|
+
readonly generator?: LayoutGenerator | null;
|
|
128
|
+
/** The origin of the input. The default is `made-up`. */
|
|
129
|
+
readonly source?: LayoutSource;
|
|
130
|
+
readonly cell?: LayoutCell;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* An input file of the form `layout-input/1`. The packer and the router read the same files.
|
|
135
|
+
* The file `docs/LAYOUT-RECORDS.md` gives the form. The repository `packing` keeps a twin of that document.
|
|
136
|
+
*/
|
|
137
|
+
export interface LayoutInput extends LayoutInputHead {
|
|
138
|
+
readonly schema: "layout-input/1";
|
|
139
|
+
/** The origin of the input. The data of this repository is made-up, because the repository is public. */
|
|
140
|
+
readonly source: LayoutSource;
|
|
141
|
+
/** The generator of the input, or `null` for a hand-made input. */
|
|
142
|
+
readonly generator: LayoutGenerator | null;
|
|
143
|
+
/** The first 16 hex characters of the SHA-256 of the canonical file without this field. `documentHash` calculates it. */
|
|
144
|
+
readonly hash: string;
|
|
145
|
+
readonly container?: LayoutContainer;
|
|
146
|
+
/** The space between two boxes. The router ignores it. */
|
|
147
|
+
readonly gap?: number;
|
|
148
|
+
/** The areas that the boxes and the routes must not use. */
|
|
149
|
+
readonly obstacles?: readonly LayoutObstacle[];
|
|
150
|
+
readonly boxes: readonly LayoutBox[];
|
|
151
|
+
readonly edges: readonly LayoutEdge[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** One run of one strategy on one input and one seed: a line of the form `layout-record/1`. */
|
|
155
|
+
export interface LayoutRecord {
|
|
156
|
+
readonly schema: "layout-record/1";
|
|
157
|
+
/** `pack`, `route` or `joint`. The screen of this package writes `route`. */
|
|
158
|
+
readonly stage: "pack" | "route" | "joint";
|
|
159
|
+
readonly step: string;
|
|
160
|
+
readonly strategy: string;
|
|
161
|
+
/** The options of the run: the defaults of the strategy with the options of the caller. A function is the text `function:<name>`. */
|
|
162
|
+
readonly options: Readonly<Record<string, unknown>>;
|
|
163
|
+
/** The corpus version, for example `screen-v1`. */
|
|
164
|
+
readonly corpus: string;
|
|
165
|
+
/** The id of the input. */
|
|
166
|
+
readonly input: string;
|
|
167
|
+
/** The `hash` of the input file. */
|
|
168
|
+
readonly inputHash: string;
|
|
169
|
+
readonly set: string;
|
|
170
|
+
readonly seed: number;
|
|
171
|
+
readonly nodes: number;
|
|
172
|
+
readonly edges: number;
|
|
173
|
+
/** The measures, by the names of `MEASURES`. A measure without a finite value is not in the record. */
|
|
174
|
+
readonly measures: Readonly<Record<string, number>>;
|
|
175
|
+
/** The count of the breaks of each hard rule. A good run has 0 for each rule. */
|
|
176
|
+
readonly hard: Readonly<Record<string, number>>;
|
|
177
|
+
/** The tolerance of each hard rule of the run, for example `{ through: 0.5 }`. */
|
|
178
|
+
readonly tolerance?: Readonly<Record<string, number>>;
|
|
179
|
+
/** This value is `true` when two runs on the same input give the same output. */
|
|
180
|
+
readonly deterministic: boolean;
|
|
181
|
+
/** The id of the prediction of this run, from the predictions file. */
|
|
182
|
+
readonly prediction?: string;
|
|
183
|
+
/** The time of the run, in milliseconds. Only the times of CI compare. */
|
|
184
|
+
readonly ms: number;
|
|
185
|
+
/** The commit of the code that made the record. */
|
|
186
|
+
readonly commit?: string;
|
|
187
|
+
/** The reason when the strategy did not run, for example an input over its `maxNodes`. Then the record has no measures. */
|
|
188
|
+
readonly skipped?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The comparison of a prediction. Without it, `<=` applies to a measure where lower is better, and `>=` to the other measures. */
|
|
192
|
+
export type PredictionOp = "<" | "<=" | "=" | ">=" | ">";
|
|
193
|
+
|
|
194
|
+
/** A prediction about a run: one line of a predictions file. A commit adds the file before the run starts. */
|
|
195
|
+
export interface LayoutPrediction {
|
|
196
|
+
readonly schema?: "layout-prediction/1";
|
|
197
|
+
readonly id: string;
|
|
198
|
+
readonly strategy: string;
|
|
199
|
+
readonly set: string;
|
|
200
|
+
/** The id of one input. Without it, the prediction is about the median of the measure over the set. */
|
|
201
|
+
readonly input?: string;
|
|
202
|
+
/** The name of the measure, from `MEASURES`. */
|
|
203
|
+
readonly measure: string;
|
|
204
|
+
readonly expected: number;
|
|
205
|
+
readonly op?: PredictionOp;
|
|
206
|
+
readonly reason: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const TO_FILE: Readonly<Record<Side, LayoutSide>> = { north: "top", east: "right", south: "bottom", west: "left" };
|
|
210
|
+
const TO_MODEL: Readonly<Record<LayoutSide, Side>> = { top: "north", right: "east", bottom: "south", left: "west" };
|
|
211
|
+
|
|
212
|
+
/** This function gives the side of a box that holds a point, relative to the top-left corner of the box. */
|
|
213
|
+
function sideAt(p: { x: number; y: number }, w: number, h: number): LayoutSide {
|
|
214
|
+
const d: [LayoutSide, number][] = [
|
|
215
|
+
["top", Math.abs(p.y)],
|
|
216
|
+
["right", Math.abs(w - p.x)],
|
|
217
|
+
["bottom", Math.abs(h - p.y)],
|
|
218
|
+
["left", Math.abs(p.x)],
|
|
219
|
+
];
|
|
220
|
+
return d.reduce((a, b) => (b[1] < a[1] ? b : a))[0];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** This function gives 0 for -0, because JSON writes -0 as 0. Then a file and the graph that it comes from hold the same numbers. */
|
|
224
|
+
const num = round3;
|
|
225
|
+
|
|
226
|
+
function portToFile(p: Port, n: Node): LayoutPort {
|
|
227
|
+
const side = p.side ? TO_FILE[p.side] : p.at ? sideAt(p.at, n.box.w, n.box.h) : undefined;
|
|
228
|
+
return { id: p.id, ...(side ? { side } : {}), ...(p.at ? { x: num(p.at.x), y: num(p.at.y) } : {}) };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function portToModel(p: LayoutPort): Port {
|
|
232
|
+
if (p.x !== undefined && p.y !== undefined) return { id: p.id, at: { x: p.x, y: p.y } };
|
|
233
|
+
return { id: p.id, ...(p.side ? { side: TO_MODEL[p.side] } : {}) };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* This function writes a graph as an input file of the form `layout-input/1`, with its hash.
|
|
238
|
+
* A fixed plug gets `x`, `y` and the side that holds it. `inputGraph` reads the file back.
|
|
239
|
+
*/
|
|
240
|
+
export function graphInput(graph: Graph, head: LayoutInputHead): LayoutInput {
|
|
241
|
+
const boxes = graph.nodes.map(
|
|
242
|
+
(n): LayoutBox => ({
|
|
243
|
+
id: n.id,
|
|
244
|
+
w: num(n.box.w),
|
|
245
|
+
h: num(n.box.h),
|
|
246
|
+
x: num(n.box.x),
|
|
247
|
+
y: num(n.box.y),
|
|
248
|
+
...(n.ports ? { ports: n.ports.map((p) => portToFile(p, n)) } : {}),
|
|
249
|
+
...(n.data ? { data: n.data } : {}),
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
const edges = graph.edges.map(
|
|
253
|
+
(e): LayoutEdge => ({
|
|
254
|
+
id: e.id,
|
|
255
|
+
source: e.source.node,
|
|
256
|
+
target: e.target.node,
|
|
257
|
+
...(e.source.port !== undefined ? { sourcePort: e.source.port } : {}),
|
|
258
|
+
...(e.target.port !== undefined ? { targetPort: e.target.port } : {}),
|
|
259
|
+
...(e.data ? { data: e.data } : {}),
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
const body = {
|
|
263
|
+
schema: "layout-input/1" as const,
|
|
264
|
+
id: head.id,
|
|
265
|
+
set: head.set,
|
|
266
|
+
family: head.family,
|
|
267
|
+
seed: head.seed,
|
|
268
|
+
...(head.base !== undefined ? { base: head.base } : {}),
|
|
269
|
+
...(head.change !== undefined ? { change: head.change } : {}),
|
|
270
|
+
...(head.step !== undefined ? { step: head.step } : {}),
|
|
271
|
+
source: head.source ?? "made-up",
|
|
272
|
+
generator: head.generator ?? null,
|
|
273
|
+
...(head.cell !== undefined ? { cell: head.cell } : {}),
|
|
274
|
+
boxes,
|
|
275
|
+
edges,
|
|
276
|
+
};
|
|
277
|
+
return { ...body, hash: documentHash(body) };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* This function gives the bounding box of a region: the smallest box that holds it.
|
|
282
|
+
*/
|
|
283
|
+
export function regionBounds(region: Region): { x: number; y: number; w: number; h: number } {
|
|
284
|
+
if (region.kind === "rect") return { x: region.x, y: region.y, w: region.w, h: region.h };
|
|
285
|
+
if (region.kind === "circle") return { x: region.cx - region.r, y: region.cy - region.r, w: 2 * region.r, h: 2 * region.r };
|
|
286
|
+
if (region.kind === "ellipse") return { x: region.cx - region.rx, y: region.cy - region.ry, w: 2 * region.rx, h: 2 * region.ry };
|
|
287
|
+
const xs = region.points.map((p) => p[0]);
|
|
288
|
+
const ys = region.points.map((p) => p[1]);
|
|
289
|
+
const x = Math.min(...xs);
|
|
290
|
+
const y = Math.min(...ys);
|
|
291
|
+
return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* This function reads the graph of the router from an input file. The router reads the positions, the ports, the edges and the data.
|
|
296
|
+
* Each obstacle becomes a node without edges, with the bounding box of its region, after the boxes.
|
|
297
|
+
* The router ignores the other fields of the packer: `container`, `gap`, `locked`, `rank`, the `shape` of a box and the `move` of an obstacle.
|
|
298
|
+
* A box without `x` and `y` is an error.
|
|
299
|
+
*/
|
|
300
|
+
export function inputGraph(input: LayoutInput): Graph {
|
|
301
|
+
const nodes = input.boxes.map((b): Node => {
|
|
302
|
+
if (b.x === undefined || b.y === undefined) throw new Error(`The box "${b.id}" of the input "${input.id}" has no position. The router needs a placed input.`);
|
|
303
|
+
return { id: b.id, box: { x: b.x, y: b.y, w: b.w, h: b.h }, ...(b.ports ? { ports: b.ports.map(portToModel) } : {}), ...(b.data ? { data: b.data } : {}) };
|
|
304
|
+
});
|
|
305
|
+
for (const o of input.obstacles ?? []) nodes.push({ id: o.id, box: regionBounds(o.region) });
|
|
306
|
+
const edges = input.edges.map(
|
|
307
|
+
(e): Edge => ({
|
|
308
|
+
id: e.id,
|
|
309
|
+
source: { node: e.source, ...(e.sourcePort !== undefined ? { port: e.sourcePort } : {}) },
|
|
310
|
+
target: { node: e.target, ...(e.targetPort !== undefined ? { port: e.targetPort } : {}) },
|
|
311
|
+
...(e.data ? { data: e.data } : {}),
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
return { nodes, edges };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* This function writes an input file as text: the keys in sorted order, and one box or one edge on each line.
|
|
319
|
+
* `JSON.parse` of the text gives the input again.
|
|
320
|
+
*/
|
|
321
|
+
export function formatInput(input: LayoutInput): string {
|
|
322
|
+
const o = input as unknown as Record<string, unknown>;
|
|
323
|
+
const keys = Object.keys(o)
|
|
324
|
+
.filter((k) => o[k] !== undefined)
|
|
325
|
+
.sort();
|
|
326
|
+
const lines = keys.map((k) => {
|
|
327
|
+
const v = o[k];
|
|
328
|
+
if (Array.isArray(v)) return ` ${JSON.stringify(k)}: [${v.length === 0 ? "]" : `\n${v.map((x) => ` ${canonical(x)}`).join(",\n")}\n ]`}`;
|
|
329
|
+
return ` ${JSON.stringify(k)}: ${canonical(v)}`;
|
|
330
|
+
});
|
|
331
|
+
return `{\n${lines.join(",\n")}\n}\n`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** This function writes a value as canonical JSON with a space after each colon and comma, for a line of a file. */
|
|
335
|
+
function canonical(value: unknown): string {
|
|
336
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
337
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(", ")}]`;
|
|
338
|
+
const o = value as Record<string, unknown>;
|
|
339
|
+
const keys = Object.keys(o)
|
|
340
|
+
.filter((k) => o[k] !== undefined)
|
|
341
|
+
.sort();
|
|
342
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}: ${canonical(o[k])}`).join(", ")}}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** This function writes records as JSON Lines: one record on each line. */
|
|
346
|
+
export function recordLines(records: readonly LayoutRecord[]): string {
|
|
347
|
+
return records.map((r) => JSON.stringify(r) + "\n").join("");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** This function reads JSON Lines. It ignores an empty line. */
|
|
351
|
+
export function parseLines<T>(text: string): T[] {
|
|
352
|
+
return text
|
|
353
|
+
.split(/\r?\n/)
|
|
354
|
+
.filter((l) => l.trim().length > 0)
|
|
355
|
+
.map((l) => JSON.parse(l) as T);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* This function gives a copy of a value that JSON keeps.
|
|
360
|
+
* A function becomes the text `function:<name>`, and a number that is not finite becomes its text.
|
|
361
|
+
*/
|
|
362
|
+
export function jsonSafe(value: unknown): unknown {
|
|
363
|
+
if (typeof value === "function") return `function:${(value as { name?: string }).name || "anonymous"}`;
|
|
364
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
365
|
+
if (value === null || typeof value !== "object") return value;
|
|
366
|
+
if (Array.isArray(value)) return value.map(jsonSafe);
|
|
367
|
+
if (value instanceof Set) return [...value].map(jsonSafe);
|
|
368
|
+
if (value instanceof Map) return Object.fromEntries([...value].map(([k, v]) => [String(k), jsonSafe(v)]));
|
|
369
|
+
const out: Record<string, unknown> = {};
|
|
370
|
+
for (const [k, v] of Object.entries(value)) if (v !== undefined) out[k] = jsonSafe(v);
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { LayoutInput, LayoutObstacle } from "./forms.ts";
|
|
2
|
+
import { documentHash, mulberry32, stepSeed } from "./hash.ts";
|
|
3
|
+
|
|
4
|
+
/** This function gives a whole number from `lo` to `hi`, both included, from a generator of random numbers. */
|
|
5
|
+
export function randomInt(r: () => number, lo: number, hi: number): number {
|
|
6
|
+
return lo + Math.floor(r() * (hi - lo + 1));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The parameters of the generator `rect-scattered`. */
|
|
10
|
+
export interface RectScatteredParams {
|
|
11
|
+
/** The number of boxes. */
|
|
12
|
+
readonly count: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This function makes the input of the shared generator `rect-scattered`, version 1: a fixed rectangle container, scattered obstacles and boxes without positions.
|
|
17
|
+
* The repository `packing` makes the same file, byte for byte. A test in each repository pins the hashes of the seeds 1, 2 and 3.
|
|
18
|
+
* The steps are `container`, `obstacles` and `boxes`. Each step has its own generator from `stepSeed`.
|
|
19
|
+
*/
|
|
20
|
+
export function rectScattered(seed: number, params: RectScatteredParams, set = "xcheck-v1"): LayoutInput {
|
|
21
|
+
let r = mulberry32(stepSeed(seed, "container"));
|
|
22
|
+
const w = randomInt(r, 600, 1000);
|
|
23
|
+
const h = randomInt(r, 400, 800);
|
|
24
|
+
r = mulberry32(stepSeed(seed, "obstacles"));
|
|
25
|
+
const n = randomInt(r, 3, 8);
|
|
26
|
+
const obstacles: LayoutObstacle[] = [];
|
|
27
|
+
for (let i = 0; i < n; i++) {
|
|
28
|
+
const ow = randomInt(r, 40, 160);
|
|
29
|
+
const oh = randomInt(r, 40, 160);
|
|
30
|
+
const x = randomInt(r, 0, w - ow);
|
|
31
|
+
const y = randomInt(r, 0, h - oh);
|
|
32
|
+
obstacles.push({ id: `o${i}`, region: { kind: "rect", x, y, w: ow, h: oh } });
|
|
33
|
+
}
|
|
34
|
+
r = mulberry32(stepSeed(seed, "boxes"));
|
|
35
|
+
const boxes = Array.from({ length: params.count }, (_, i) => {
|
|
36
|
+
const bw = randomInt(r, 20, 120);
|
|
37
|
+
const bh = randomInt(r, 20, 80);
|
|
38
|
+
return { id: `b${i}`, w: bw, h: bh };
|
|
39
|
+
});
|
|
40
|
+
const body = {
|
|
41
|
+
schema: "layout-input/1" as const,
|
|
42
|
+
id: `${set}/rect-scattered/s${seed}`,
|
|
43
|
+
set,
|
|
44
|
+
family: "rect-scattered",
|
|
45
|
+
seed,
|
|
46
|
+
source: "made-up" as const,
|
|
47
|
+
generator: { name: "rect-scattered", version: 1, params: { count: params.count }, seed },
|
|
48
|
+
container: { w, h, fixed: "size" as const },
|
|
49
|
+
obstacles,
|
|
50
|
+
boxes,
|
|
51
|
+
edges: [],
|
|
52
|
+
};
|
|
53
|
+
return { ...body, hash: documentHash(body) };
|
|
54
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// The core runs in any host, so this file has its own SHA-256. It needs no API of Node or of the browser.
|
|
2
|
+
|
|
3
|
+
const K: readonly number[] = [
|
|
4
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
|
|
5
|
+
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
|
6
|
+
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
7
|
+
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
8
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
/** This function gives the bytes of a string in UTF-8. */
|
|
12
|
+
export function utf8(text: string): Uint8Array {
|
|
13
|
+
const out: number[] = [];
|
|
14
|
+
for (const ch of text) {
|
|
15
|
+
const c = ch.codePointAt(0)!;
|
|
16
|
+
if (c < 0x80) out.push(c);
|
|
17
|
+
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
|
|
18
|
+
else if (c < 0x10000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
19
|
+
else out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
20
|
+
}
|
|
21
|
+
return Uint8Array.from(out);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** This function gives the SHA-256 digest of a string, as 64 lowercase hex characters. It encodes the string in UTF-8. */
|
|
25
|
+
export function sha256Hex(text: string): string {
|
|
26
|
+
const bytes = utf8(text);
|
|
27
|
+
const bits = bytes.length * 8;
|
|
28
|
+
const total = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
29
|
+
const data = new Uint8Array(total);
|
|
30
|
+
data.set(bytes);
|
|
31
|
+
data[bytes.length] = 0x80;
|
|
32
|
+
const view = new DataView(data.buffer);
|
|
33
|
+
view.setUint32(total - 8, Math.floor(bits / 0x100000000));
|
|
34
|
+
view.setUint32(total - 4, bits >>> 0);
|
|
35
|
+
const h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
|
36
|
+
const w = new Uint32Array(64);
|
|
37
|
+
const rotr = (x: number, n: number): number => (x >>> n) | (x << (32 - n));
|
|
38
|
+
for (let off = 0; off < total; off += 64) {
|
|
39
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + 4 * i);
|
|
40
|
+
for (let i = 16; i < 64; i++) {
|
|
41
|
+
const a = w[i - 15]!;
|
|
42
|
+
const b = w[i - 2]!;
|
|
43
|
+
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3);
|
|
44
|
+
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10);
|
|
45
|
+
w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) >>> 0;
|
|
46
|
+
}
|
|
47
|
+
let [a, b, c, d, e, f, g, k] = h as [number, number, number, number, number, number, number, number];
|
|
48
|
+
for (let i = 0; i < 64; i++) {
|
|
49
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
50
|
+
const ch = (e & f) ^ (~e & g);
|
|
51
|
+
const t1 = (k + s1 + ch + K[i]! + w[i]!) >>> 0;
|
|
52
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
53
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
54
|
+
const t2 = (s0 + maj) >>> 0;
|
|
55
|
+
k = g;
|
|
56
|
+
g = f;
|
|
57
|
+
f = e;
|
|
58
|
+
e = (d + t1) >>> 0;
|
|
59
|
+
d = c;
|
|
60
|
+
c = b;
|
|
61
|
+
b = a;
|
|
62
|
+
a = (t1 + t2) >>> 0;
|
|
63
|
+
}
|
|
64
|
+
h[0] = (h[0]! + a) >>> 0;
|
|
65
|
+
h[1] = (h[1]! + b) >>> 0;
|
|
66
|
+
h[2] = (h[2]! + c) >>> 0;
|
|
67
|
+
h[3] = (h[3]! + d) >>> 0;
|
|
68
|
+
h[4] = (h[4]! + e) >>> 0;
|
|
69
|
+
h[5] = (h[5]! + f) >>> 0;
|
|
70
|
+
h[6] = (h[6]! + g) >>> 0;
|
|
71
|
+
h[7] = (h[7]! + k) >>> 0;
|
|
72
|
+
}
|
|
73
|
+
return h.map((x) => x.toString(16).padStart(8, "0")).join("");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* This function writes a value as canonical JSON: the keys of each object in sorted order, and no whitespace.
|
|
78
|
+
* It drops a key whose value is `undefined`, as `JSON.stringify` does.
|
|
79
|
+
*/
|
|
80
|
+
export function canonicalJson(value: unknown): string {
|
|
81
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
82
|
+
if (Array.isArray(value)) return `[${value.map((v) => (v === undefined ? "null" : canonicalJson(v))).join(",")}]`;
|
|
83
|
+
const o = value as Record<string, unknown>;
|
|
84
|
+
const keys = Object.keys(o)
|
|
85
|
+
.filter((k) => o[k] !== undefined)
|
|
86
|
+
.sort();
|
|
87
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* This function gives the hash of a JSON document: the first 16 hex characters of the SHA-256 of its canonical JSON.
|
|
92
|
+
* The document does not include its own field `hash`. The function removes that field before it calculates the hash.
|
|
93
|
+
*/
|
|
94
|
+
export function documentHash(value: object): string {
|
|
95
|
+
const { hash: _hash, ...rest } = value as { hash?: unknown };
|
|
96
|
+
return sha256Hex(canonicalJson(rest)).slice(0, 16);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** This function gives the 32-bit FNV-1a hash of the UTF-8 bytes of a text, as an unsigned integer. */
|
|
100
|
+
export function fnv1a32(text: string): number {
|
|
101
|
+
let h = 0x811c9dc5;
|
|
102
|
+
for (const byte of utf8(text)) {
|
|
103
|
+
h ^= byte;
|
|
104
|
+
h = Math.imul(h, 0x01000193);
|
|
105
|
+
}
|
|
106
|
+
return h >>> 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** This function gives the seed of one step of a generator: `fnv1a32("<seed>:<step>")`. */
|
|
110
|
+
export function stepSeed(seed: number, step: string): number {
|
|
111
|
+
return fnv1a32(`${seed}:${step}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** This function gives the generator `mulberry32` for a seed. Each call gives the next number from 0 to below 1. */
|
|
115
|
+
export function mulberry32(seed: number): () => number {
|
|
116
|
+
let a = seed >>> 0;
|
|
117
|
+
return (): number => {
|
|
118
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
119
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
120
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
121
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** This function rounds a number to 3 decimals, as the files of a corpus keep it. The value `-0` becomes `0`. */
|
|
126
|
+
export function round3(value: number): number {
|
|
127
|
+
const r = Math.round(value * 1000) / 1000;
|
|
128
|
+
return r === 0 ? 0 : r;
|
|
129
|
+
}
|