@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/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.14",
|
|
5
5
|
"description": "The shared contract of the packing and highways layout engines: the input and record formats, the turn rule, margins, tolerances and hard rules.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"layout",
|
package/src/families.ts
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
// The shared corpus generator (`docs/CORPUS.md`): the dimension schema, the family registry and the seeded random numbers.
|
|
2
|
+
// An engine owns its families and gives them to a registry. This file imports no engine and no API of Node, so a browser can run it.
|
|
3
|
+
import type { FamilyBy } from "./corpus.ts";
|
|
4
|
+
import { edgeStep, type EdgeStepParams } from "./edges.ts";
|
|
5
|
+
import { documentHash } from "./hash.ts";
|
|
6
|
+
import type { LayoutInput, Obstacle } from "./input.ts";
|
|
7
|
+
import { mulberry32, randomInt, stepRandom } from "./random.ts";
|
|
8
|
+
import { SHAPE_KIND_IDS } from "./shapes.ts";
|
|
9
|
+
import { paramActive, paramLevels, paramProblem, paramValue, type ParamValue, type SpaceParam } from "./strategy-space.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One dimension of a corpus: a param of `strategy-space.ts` with its meaning and the engines that use it.
|
|
13
|
+
* The field `when` has the form of `SpaceParam.when`: the dimension is active only for some values of an earlier dimension.
|
|
14
|
+
*/
|
|
15
|
+
export interface CorpusDimension extends SpaceParam {
|
|
16
|
+
/** The meaning of the dimension, in one sentence. */
|
|
17
|
+
readonly meaning: string;
|
|
18
|
+
/** The engines that read the dimension, for example `packing`, `highways` and `joint`. */
|
|
19
|
+
readonly engines: readonly string[];
|
|
20
|
+
/** The values of a sweep. Without them, a sweep uses the values of an `enum`, `false` and `true` of a `bool`, or the default. */
|
|
21
|
+
readonly levels?: readonly ParamValue[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The values of the dimensions of one input: one value for each name. */
|
|
25
|
+
export type DimensionValues = Readonly<Record<string, ParamValue>>;
|
|
26
|
+
|
|
27
|
+
/** A factory of seeded random numbers: it gives a source of numbers in [0, 1) for a seed. */
|
|
28
|
+
export type RngFactory = (seed: number) => () => number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The cross-engine note of a family. `exact` tells that each JavaScript engine gives the same output.
|
|
32
|
+
* `libm-dependent` tells that the family uses a function that is not correctly rounded in each engine, for example `Math.log`, `Math.cos` or `**`.
|
|
33
|
+
*/
|
|
34
|
+
export type CrossEngine = "exact" | "libm-dependent";
|
|
35
|
+
|
|
36
|
+
/** The random numbers of the default factory: `mulberry32` of `random.ts`. Its stream is pinned in `families.test.ts`. */
|
|
37
|
+
export const DEFAULT_RNG: RngFactory = mulberry32;
|
|
38
|
+
|
|
39
|
+
/** The context that a registry gives to `generate`. */
|
|
40
|
+
export interface GenerateContext {
|
|
41
|
+
/** The random factory of the family: its own `rng`, or `DEFAULT_RNG`. */
|
|
42
|
+
readonly rng: RngFactory;
|
|
43
|
+
/** The source of random numbers of the input: `rng(rngSeed(seed, dims))`. A family that does not use it takes no numbers from it. */
|
|
44
|
+
readonly random: () => number;
|
|
45
|
+
/** The name of the corpus of the input, when the caller gives one. */
|
|
46
|
+
readonly set?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A family of a corpus: a seeded generator with declared dimensions. The engine that owns it gives it to a registry.
|
|
51
|
+
* The registry gives `generate` the raw seed and the full dimension values. It does not change the output, and it does not calculate a hash.
|
|
52
|
+
*/
|
|
53
|
+
export interface CorpusFamily<O = LayoutInput> {
|
|
54
|
+
/** The name of the family, for example `rect-grid`. */
|
|
55
|
+
readonly name: string;
|
|
56
|
+
/** The version. A change of the output of the family needs a new version. A dimension of only some versions is in the list of those versions only. */
|
|
57
|
+
readonly version: number;
|
|
58
|
+
/** The engine that owns the definition of the family, for example `packing`. */
|
|
59
|
+
readonly owner: string;
|
|
60
|
+
/** The meaning of the family, in one sentence. */
|
|
61
|
+
readonly meaning: string;
|
|
62
|
+
/** The dimensions of the family, in their order. A dimension with `when` comes after the dimension that its `when` names. The name `kind` is reserved. */
|
|
63
|
+
readonly dimensions: readonly CorpusDimension[];
|
|
64
|
+
/** The text of the rule that gives the random numbers of an input from its seed and its dimensions. */
|
|
65
|
+
readonly seeding: string;
|
|
66
|
+
/** The cross-engine note of the family. */
|
|
67
|
+
readonly crossEngine: CrossEngine;
|
|
68
|
+
/** The random factory of the family. The default is `DEFAULT_RNG`. */
|
|
69
|
+
readonly rng?: RngFactory;
|
|
70
|
+
/** This function gives the seed of `context.random` from the input seed and the full dimension values. The default gives the input seed. */
|
|
71
|
+
readonly rngSeed?: (seed: number, dims: DimensionValues) => number;
|
|
72
|
+
/** The dimensions of the family key of `familyBy`, after the kind of the cell. A dimension with `when` is not permitted. The default is no dimension. */
|
|
73
|
+
readonly familyBy?: readonly string[];
|
|
74
|
+
/** This function gives the input of a seed. `dims` has a value for each dimension, also for an inactive dimension. */
|
|
75
|
+
generate(seed: number, dims: DimensionValues, context: GenerateContext): O;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The options of `generate` of a registry. */
|
|
79
|
+
export interface GenerateOptions {
|
|
80
|
+
/** The version of the family. The default is the first version that the registry got. */
|
|
81
|
+
readonly version?: number;
|
|
82
|
+
/** The name of the corpus. The registry gives it to the family in `context.set`. */
|
|
83
|
+
readonly set?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The options of `cells` and `cellCount` of a registry. */
|
|
87
|
+
export interface CellsOptions {
|
|
88
|
+
readonly version?: number;
|
|
89
|
+
/** The dimensions of the sweep. The other dimensions keep their defaults. The default is each dimension of the family. */
|
|
90
|
+
readonly sweep?: readonly string[];
|
|
91
|
+
/** The values of a sweep for some dimensions, in place of the levels of the dimensions. */
|
|
92
|
+
readonly levels?: Readonly<Record<string, readonly ParamValue[]>>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The cell of an input of a registry: `<name>@<version>` of its family in `kind` (`familyKind`), and the value of each active dimension. */
|
|
96
|
+
export type RegistryCell = Readonly<Record<string, ParamValue>> & { readonly kind: string };
|
|
97
|
+
|
|
98
|
+
/** This function gives the `kind` of the cells of a version of a family: `<name>@<version>`, for example `rect-grid@2`. Thus two versions do not share a family key. */
|
|
99
|
+
export function familyKind(name: string, version: number): string {
|
|
100
|
+
return `${name}@${version}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A registry of families. An import registers nothing: a host makes a registry from the lists of families of the engines. */
|
|
104
|
+
export interface FamilyRegistry<O = LayoutInput> {
|
|
105
|
+
/** This method adds a family. It throws a `RangeError` for a second family with the same name and version, or for a bad dimension. */
|
|
106
|
+
register(family: CorpusFamily<O>): FamilyRegistry<O>;
|
|
107
|
+
/** This method gives a family. Without a version, it gives the first version that the registry got. It throws a `RangeError` for an unknown family. */
|
|
108
|
+
family(name: string, version?: number): CorpusFamily<O>;
|
|
109
|
+
/** This method gives each family, in the order of registration. */
|
|
110
|
+
families(): readonly CorpusFamily<O>[];
|
|
111
|
+
/**
|
|
112
|
+
* This method gives the full dimension values of an input. It throws a `RangeError` for an unknown dimension or a bad value.
|
|
113
|
+
* The methods with `dims` accept a cell of `cellOf`: they ignore a `kind` that is the `familyKind` of the family, and refuse another `kind`.
|
|
114
|
+
*/
|
|
115
|
+
resolve(name: string, dims?: Readonly<Record<string, ParamValue>>, version?: number): DimensionValues;
|
|
116
|
+
/** This method gives the input of a seed. The output is the output of the family, with no change. */
|
|
117
|
+
generate(name: string, seed: number, dims?: Readonly<Record<string, ParamValue>>, options?: GenerateOptions): O;
|
|
118
|
+
/** This method gives the cell of an input: the `familyKind` of the family version and the value of each active dimension. */
|
|
119
|
+
cellOf(name: string, dims?: Readonly<Record<string, ParamValue>>, version?: number): RegistryCell;
|
|
120
|
+
/** This method gives the full dimension values of each cell of a sweep, without the dead combinations of `when`. */
|
|
121
|
+
cells(name: string, options?: CellsOptions): Generator<DimensionValues>;
|
|
122
|
+
/** This method gives the count of the cells of `cells`. It makes no input. */
|
|
123
|
+
cellCount(name: string, options?: CellsOptions): number;
|
|
124
|
+
/** This method gives the defaults, and each end of each numeric range. The dimensions that a `when` needs get its first value. The other dimensions keep their defaults. */
|
|
125
|
+
boundaryCells(name: string, version?: number): DimensionValues[];
|
|
126
|
+
/** This method gives the random source of an input: the stream that `generate` gets in `context.random`. */
|
|
127
|
+
rngOf(name: string, version?: number): (seed: number, dims?: Readonly<Record<string, ParamValue>>) => () => number;
|
|
128
|
+
/** This method gives the map form of `FamilyBy` for the cells of `cellOf`: the `familyKind` of each family version, with its own `familyBy`. */
|
|
129
|
+
familyBy(): FamilyBy;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The default key of a family in a manifest: the kind of the cell alone. It is the map form of `FamilyBy` with the entry `"*"`. */
|
|
133
|
+
export const DEFAULT_FAMILY_BY: FamilyBy = Object.freeze({ "*": Object.freeze([]) as readonly string[] });
|
|
134
|
+
|
|
135
|
+
/** This function gives an `enum` dimension. */
|
|
136
|
+
const choice = (name: string, values: readonly string[], value: string, meaning: string, engines: readonly string[]): CorpusDimension =>
|
|
137
|
+
Object.freeze({ name, kind: "enum", values: Object.freeze([...values]), default: value, meaning, engines: Object.freeze([...engines]) });
|
|
138
|
+
|
|
139
|
+
/** This function gives an `int` or a `real` dimension. */
|
|
140
|
+
const range = (
|
|
141
|
+
name: string,
|
|
142
|
+
kind: "int" | "real",
|
|
143
|
+
limits: { readonly min: number; readonly max: number; readonly value: number; readonly scale?: "linear" | "log"; readonly levels?: readonly number[] },
|
|
144
|
+
meaning: string,
|
|
145
|
+
engines: readonly string[],
|
|
146
|
+
): CorpusDimension =>
|
|
147
|
+
Object.freeze({
|
|
148
|
+
name,
|
|
149
|
+
kind,
|
|
150
|
+
min: limits.min,
|
|
151
|
+
max: limits.max,
|
|
152
|
+
default: limits.value,
|
|
153
|
+
scale: limits.scale ?? "linear",
|
|
154
|
+
...(limits.levels === undefined ? {} : { levels: Object.freeze([...limits.levels]) }),
|
|
155
|
+
meaning,
|
|
156
|
+
engines: Object.freeze([...engines]),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const P = "packing";
|
|
160
|
+
const H = "highways";
|
|
161
|
+
const J = "joint";
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The shared dimension table. A family takes a dimension of it with `sharedDimension`, or declares its own dimension.
|
|
165
|
+
* The coverage table of each dimension and each engine is in `docs/CORPUS.md`.
|
|
166
|
+
*/
|
|
167
|
+
export const CORPUS_DIMENSIONS: Readonly<Record<string, CorpusDimension>> = Object.freeze({
|
|
168
|
+
count: range("count", "int", { min: 0, max: 100_000, value: 100, scale: "log", levels: [1, 10, 100, 1000, 10_000] }, "The count of the items (n) of an input.", [P, H, J]),
|
|
169
|
+
sizes: choice("sizes", ["identical", "nearIdentical", "uniform", "heavyTail", "bimodal", "extremeAspect"], "uniform", "The distribution of the sizes of the items.", [P, J]),
|
|
170
|
+
aspect: range("aspect", "real", { min: 0.25, max: 4, value: 1, scale: "log" }, "The width over the height of a container with no fixed size.", [P]),
|
|
171
|
+
density: range("density", "real", { min: 0.05, max: 1, value: 0.4, scale: "linear" }, "The area of the items over the area of the frame of the container.", [P]),
|
|
172
|
+
obstacles: choice("obstacles", ["none", "center", "scattered", "wall", "corridors", "touching", "edge"], "none", "The layout of the fixed obstacles.", [P, J]),
|
|
173
|
+
containerSize: choice("containerSize", ["fixed", "freeHeight", "freeWidth", "freeAspect", "freeArea"], "fixed", "The sides of the container that the engine can change.", [P, J]),
|
|
174
|
+
containerShape: choice("containerShape", ["rect", "convex", "polygon", "circle", "ellipse", "holes"], "rect", "The kind of the shape of the container.", [P, J]),
|
|
175
|
+
shape: choice("shape", SHAPE_KIND_IDS, "rect", "The shape kind of each item: a `ShapeKind` of `shapes.ts`.", [P]),
|
|
176
|
+
order: choice("order", ["free", "soft"], "free", "The reading order of the items: no order, or a rank on each item.", [P]),
|
|
177
|
+
groups: choice("groups", ["none", "flat", "nested"], "none", "The groups of the items: no groups, one level, or groups in groups.", [H, J]),
|
|
178
|
+
edgeDensity: choice("edgeDensity", ["sparse", "medium", "dense"], "medium", "The mean count of the edges of each item: 0.5, 1.5 or 4.", [H, J]),
|
|
179
|
+
edgeShape: choice("edgeShape", ["random", "clustered", "hubChain", "hubs", "longRange", "tree", "bundled"], "random", "The topology of the edges.", [H, J]),
|
|
180
|
+
ports: choice("ports", ["free", "sides", "fixed"], "free", "The ports of the edges: free, a side for each end, or fixed ports.", [H, J]),
|
|
181
|
+
hubBy: Object.freeze({
|
|
182
|
+
...choice("hubBy", ["area", "random"], "area", "The order of the hubs of the shapes hubs and tree: the largest item first, or random.", [J]),
|
|
183
|
+
when: Object.freeze({ param: "edgeShape", in: Object.freeze(["hubs", "tree"]) }),
|
|
184
|
+
}),
|
|
185
|
+
spacing: choice("spacing", ["stress", "gap", "margin", "both"], "both", "The split of the route clearance between the margins and the gap. `stress` has neither.", [J]),
|
|
186
|
+
margin: range("margin", "real", { min: 0, max: 32, value: 0 }, "The margin of each item, in units.", [P, J]),
|
|
187
|
+
gap: range("gap", "real", { min: 0, max: 32, value: 0 }, "The free space between two items, in units.", [P, J]),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/** This function gives a dimension of `CORPUS_DIMENSIONS`, with the fields of `patch` over it. It throws a `RangeError` for an unknown name. */
|
|
191
|
+
export function sharedDimension(name: string, patch: Partial<CorpusDimension> = {}): CorpusDimension {
|
|
192
|
+
const base = Object.hasOwn(CORPUS_DIMENSIONS, name) ? CORPUS_DIMENSIONS[name] : undefined;
|
|
193
|
+
if (base === undefined) throw new RangeError(`The shared dimension table has no dimension "${name}".`);
|
|
194
|
+
return Object.freeze({ ...base, ...patch, name });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** This function tells if a log scale of a range starts at 0. Then the scale is the log of `1 + x - min`. */
|
|
198
|
+
const fromZero = (d: SpaceParam): boolean => d.scale === "log" && (d.min ?? 0) <= 0;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* This function gives the value at the fraction `t` of the range of a numeric dimension, on its scale. A slider of a site uses it.
|
|
202
|
+
* A log scale with a `min` of 0 or less is the log of `1 + x - min`. An `int` dimension gives a whole number.
|
|
203
|
+
*/
|
|
204
|
+
export function dimensionAt(dim: SpaceParam, t: number): number {
|
|
205
|
+
const min = dim.min ?? 0;
|
|
206
|
+
const max = dim.max ?? min;
|
|
207
|
+
const u = Math.min(1, Math.max(0, t));
|
|
208
|
+
let v: number;
|
|
209
|
+
if (dim.scale !== "log") v = min + u * (max - min);
|
|
210
|
+
else if (fromZero(dim)) v = min + Math.expm1(u * Math.log1p(max - min));
|
|
211
|
+
else v = Math.exp(Math.log(min) + u * (Math.log(max) - Math.log(min)));
|
|
212
|
+
v = Math.min(max, Math.max(min, v));
|
|
213
|
+
return dim.kind === "int" ? Math.round(v) : (paramValue(dim, v) as number);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** This function gives the fraction of the range of a numeric dimension at a value, on its scale. It is the inverse of `dimensionAt`. */
|
|
217
|
+
export function dimensionUnit(dim: SpaceParam, value: number): number {
|
|
218
|
+
const min = dim.min ?? 0;
|
|
219
|
+
const max = dim.max ?? min;
|
|
220
|
+
if (max === min) return 0;
|
|
221
|
+
const v = Math.min(max, Math.max(min, value));
|
|
222
|
+
if (dim.scale !== "log") return (v - min) / (max - min);
|
|
223
|
+
if (fromZero(dim)) return Math.log1p(v - min) / Math.log1p(max - min);
|
|
224
|
+
return (Math.log(v) - Math.log(min)) / (Math.log(max) - Math.log(min));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* This function gives the values of a sweep of a dimension. `levels` of the dimension come first. Then an `enum` gives its values, and a `bool` gives `false` and `true`.
|
|
229
|
+
* With `steps`, a numeric dimension gives `steps` points from `min` to `max` on its scale. Else it gives its default.
|
|
230
|
+
*/
|
|
231
|
+
export function levelsOf(dim: CorpusDimension, steps?: number): ParamValue[] {
|
|
232
|
+
if (dim.levels !== undefined) return [...dim.levels];
|
|
233
|
+
if (dim.kind === "enum" || dim.kind === "bool" || steps === undefined) return paramLevels(dim, 1);
|
|
234
|
+
if (!fromZero(dim)) return paramLevels(dim, steps);
|
|
235
|
+
if (!Number.isInteger(steps) || steps < 1) throw new RangeError(`The steps of a sweep are a whole number of 1 or more, not ${steps}.`);
|
|
236
|
+
if (steps === 1) return [paramValue(dim, dim.default)];
|
|
237
|
+
return [...new Set(Array.from({ length: steps }, (_, j) => dimensionAt(dim, j / (steps - 1))))];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** This function gives the problems of the dimensions of a family: a repeated name, a bad default, a bad level or a bad `when`. */
|
|
241
|
+
function dimensionProblems(dims: readonly CorpusDimension[]): string[] {
|
|
242
|
+
const out: string[] = [];
|
|
243
|
+
const names = dims.map((d) => d.name);
|
|
244
|
+
dims.forEach((d, i) => {
|
|
245
|
+
if (names.indexOf(d.name) !== i) out.push(`the dimension "${d.name}" is in the list two times`);
|
|
246
|
+
const bad = paramProblem(d, d.default);
|
|
247
|
+
if (bad !== undefined) out.push(`the default of "${d.name}" is bad: ${bad}`);
|
|
248
|
+
for (const v of d.levels ?? []) {
|
|
249
|
+
const p = paramProblem(d, v);
|
|
250
|
+
if (p !== undefined) out.push(`a level of "${d.name}" is bad: ${p}`);
|
|
251
|
+
}
|
|
252
|
+
if (d.when === undefined) return;
|
|
253
|
+
const j = names.indexOf(d.when.param);
|
|
254
|
+
if (j < 0 || j >= i) out.push(`the when of "${d.name}" names "${d.when.param}", which is not an earlier dimension`);
|
|
255
|
+
else if (d.when.in.length === 0) out.push(`the when of "${d.name}" has no values`);
|
|
256
|
+
else for (const v of d.when.in) if (paramProblem(dims[j]!, v) !== undefined) out.push(`the when of "${d.name}" has a value that "${d.when.param}" does not have`);
|
|
257
|
+
});
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* This function gives the full dimension values of an input. A missing dimension gets its default. An inactive dimension of `when` also gets its default.
|
|
263
|
+
* Thus a default that the caller gives, a missing value and an inactive value give the same values. A real number gets its stable form.
|
|
264
|
+
* The function throws a `RangeError` for an unknown dimension or a bad value, also for a bad value of an inactive dimension.
|
|
265
|
+
*/
|
|
266
|
+
export function resolveDimensions(dimensions: readonly CorpusDimension[], dims: Readonly<Record<string, ParamValue>> = {}, where: string = "The family"): DimensionValues {
|
|
267
|
+
const known = new Set(dimensions.map((d) => d.name));
|
|
268
|
+
for (const key of Object.keys(dims)) if (!known.has(key)) throw new RangeError(`${where} has no dimension "${key}".`);
|
|
269
|
+
const active: Record<string, ParamValue> = {};
|
|
270
|
+
const out: Record<string, ParamValue> = {};
|
|
271
|
+
for (const d of dimensions) {
|
|
272
|
+
const given = Object.hasOwn(dims, d.name) ? dims[d.name] : undefined;
|
|
273
|
+
if (given !== undefined) {
|
|
274
|
+
const bad = paramProblem(d, given);
|
|
275
|
+
if (bad !== undefined) throw new RangeError(`${where}: the dimension "${d.name}" is bad: ${bad}.`);
|
|
276
|
+
}
|
|
277
|
+
const on = paramActive(d, active);
|
|
278
|
+
const value = paramValue(d, on && given !== undefined ? given : d.default);
|
|
279
|
+
if (on) active[d.name] = value;
|
|
280
|
+
out[d.name] = value;
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** This function gives the values of the active dimensions only. */
|
|
286
|
+
function activeValues(dimensions: readonly CorpusDimension[], full: DimensionValues): Record<string, ParamValue> {
|
|
287
|
+
const active: Record<string, ParamValue> = {};
|
|
288
|
+
for (const d of dimensions) if (paramActive(d, active)) active[d.name] = full[d.name]!;
|
|
289
|
+
return active;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** This function gives the active values of each cell of a sweep, in the order of the dimensions. The last dimension changes first. */
|
|
293
|
+
function* sweepOf(dimensions: readonly CorpusDimension[], levels: readonly (readonly ParamValue[] | undefined)[], i = 0, values: Record<string, ParamValue> = {}): Generator<Record<string, ParamValue>> {
|
|
294
|
+
if (i === dimensions.length) {
|
|
295
|
+
yield { ...values };
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const d = dimensions[i]!;
|
|
299
|
+
const lv = levels[i];
|
|
300
|
+
if (lv === undefined || !paramActive(d, values)) {
|
|
301
|
+
yield* sweepOf(dimensions, levels, i + 1, paramActive(d, values) ? { ...values, [d.name]: paramValue(d, d.default) } : values);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
for (const v of lv) yield* sweepOf(dimensions, levels, i + 1, { ...values, [d.name]: paramValue(d, v) });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** This function counts the cells of a sweep with no list of the cells. A dimension that a later `when` reads splits the count. */
|
|
308
|
+
function countOf(dimensions: readonly CorpusDimension[], levels: readonly (readonly ParamValue[] | undefined)[], i = 0, values: Record<string, ParamValue> = {}): number {
|
|
309
|
+
if (i === dimensions.length) return 1;
|
|
310
|
+
const d = dimensions[i]!;
|
|
311
|
+
if (!paramActive(d, values)) return countOf(dimensions, levels, i + 1, values);
|
|
312
|
+
const lv = levels[i] ?? [d.default];
|
|
313
|
+
const readLater = dimensions.slice(i + 1).some((q) => q.when?.param === d.name);
|
|
314
|
+
if (!readLater) return lv.length * countOf(dimensions, levels, i + 1, values);
|
|
315
|
+
let sum = 0;
|
|
316
|
+
for (const v of lv) sum += countOf(dimensions, levels, i + 1, { ...values, [d.name]: paramValue(d, v) });
|
|
317
|
+
return sum;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** This function gives the levels of each dimension of a sweep. A dimension outside the sweep has `undefined`, so it keeps its default. */
|
|
321
|
+
function sweepLevels(dimensions: readonly CorpusDimension[], options: CellsOptions, where: string): (readonly ParamValue[] | undefined)[] {
|
|
322
|
+
const names = new Set(dimensions.map((d) => d.name));
|
|
323
|
+
for (const name of [...(options.sweep ?? []), ...Object.keys(options.levels ?? {})]) if (!names.has(name)) throw new RangeError(`${where} has no dimension "${name}".`);
|
|
324
|
+
return dimensions.map((d) => {
|
|
325
|
+
if (options.sweep !== undefined && !options.sweep.includes(d.name)) return undefined;
|
|
326
|
+
const lv = options.levels !== undefined && Object.hasOwn(options.levels, d.name) ? options.levels[d.name]! : levelsOf(d);
|
|
327
|
+
for (const v of lv) {
|
|
328
|
+
const bad = paramProblem(d, v);
|
|
329
|
+
if (bad !== undefined) throw new RangeError(`${where}: a level of "${d.name}" is bad: ${bad}.`);
|
|
330
|
+
}
|
|
331
|
+
return lv;
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** This function gives the values that make a dimension active: the first value of its `when`, and the values that make that dimension active. */
|
|
336
|
+
function activation(dimensions: readonly CorpusDimension[], dim: CorpusDimension): Record<string, ParamValue> {
|
|
337
|
+
const out: Record<string, ParamValue> = {};
|
|
338
|
+
let d: CorpusDimension | undefined = dim;
|
|
339
|
+
while (d?.when !== undefined) {
|
|
340
|
+
const param: string = d.when.param;
|
|
341
|
+
out[param] = d.when.in[0]!;
|
|
342
|
+
d = dimensions.find((q) => q.name === param);
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** This function throws a `RangeError` when a seed is not a whole number of 0 or more. */
|
|
348
|
+
function checkSeed(seed: number): void {
|
|
349
|
+
if (!Number.isSafeInteger(seed) || seed < 0) throw new RangeError(`The seed is a whole number of 0 or more, not ${seed}.`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** This function gives a new registry with the families of the list. Each engine gives the list of its families. An import registers nothing. */
|
|
353
|
+
export function createFamilyRegistry<O = LayoutInput>(families: Iterable<CorpusFamily<O>> = []): FamilyRegistry<O> {
|
|
354
|
+
const list: CorpusFamily<O>[] = [];
|
|
355
|
+
const label = (f: CorpusFamily<O>): string => `The family "${f.name}" version ${f.version}`;
|
|
356
|
+
const find = (name: string, version?: number): CorpusFamily<O> => {
|
|
357
|
+
const named = list.filter((f) => f.name === name);
|
|
358
|
+
if (named.length === 0) throw new RangeError(`The registry has no family "${name}".`);
|
|
359
|
+
if (version === undefined) return named[0]!;
|
|
360
|
+
const found = named.find((f) => f.version === version);
|
|
361
|
+
if (found === undefined) throw new RangeError(`The family "${name}" has the versions ${named.map((f) => f.version).join(", ")}, not ${version}.`);
|
|
362
|
+
return found;
|
|
363
|
+
};
|
|
364
|
+
/** This function removes the `kind` of a cell of `cellOf` when it is the `familyKind` of `f`. It throws a `RangeError` for another kind. */
|
|
365
|
+
const ownDims = (f: CorpusFamily<O>, dims: Readonly<Record<string, ParamValue>> | undefined): Readonly<Record<string, ParamValue>> | undefined => {
|
|
366
|
+
if (dims === undefined || !Object.hasOwn(dims, "kind")) return dims;
|
|
367
|
+
const kind = familyKind(f.name, f.version);
|
|
368
|
+
if (dims["kind"] !== kind) throw new RangeError(`${label(f)} has the kind "${kind}", not the kind ${JSON.stringify(dims["kind"])} of the cell.`);
|
|
369
|
+
const { kind: _kind, ...rest } = dims;
|
|
370
|
+
return rest;
|
|
371
|
+
};
|
|
372
|
+
const streamOf = (f: CorpusFamily<O>, seed: number, full: DimensionValues): { readonly rng: RngFactory; readonly random: () => number } => {
|
|
373
|
+
const rng = f.rng ?? DEFAULT_RNG;
|
|
374
|
+
return { rng, random: rng(f.rngSeed === undefined ? seed : f.rngSeed(seed, full)) };
|
|
375
|
+
};
|
|
376
|
+
const registry: FamilyRegistry<O> = {
|
|
377
|
+
register(family) {
|
|
378
|
+
if (!Number.isInteger(family.version) || family.version < 1) throw new RangeError(`${label(family)} has a version that is not a whole number of 1 or more.`);
|
|
379
|
+
if (list.some((f) => f.name === family.name && f.version === family.version)) throw new RangeError(`${label(family)} is in the registry already.`);
|
|
380
|
+
const problems = dimensionProblems(family.dimensions);
|
|
381
|
+
const names = family.dimensions.map((d) => d.name);
|
|
382
|
+
if (names.includes("kind")) problems.push('the dimension name "kind" is reserved: the cell of `cellOf` has the `familyKind` of the family in `kind`');
|
|
383
|
+
for (const key of family.familyBy ?? []) {
|
|
384
|
+
const d = family.dimensions.find((q) => q.name === key);
|
|
385
|
+
if (d === undefined) problems.push(`the familyBy dimension "${key}" is not a dimension of the family`);
|
|
386
|
+
else if (d.when !== undefined) problems.push(`the familyBy dimension "${key}" has a when, so a cell without it has no family key`);
|
|
387
|
+
}
|
|
388
|
+
if (problems.length > 0) throw new RangeError(`${label(family)} is not good: ${problems.join("; ")}.`);
|
|
389
|
+
list.push(family);
|
|
390
|
+
return registry;
|
|
391
|
+
},
|
|
392
|
+
family: find,
|
|
393
|
+
families: () => [...list],
|
|
394
|
+
resolve(name, dims, version) {
|
|
395
|
+
const f = find(name, version);
|
|
396
|
+
return resolveDimensions(f.dimensions, ownDims(f, dims), label(f));
|
|
397
|
+
},
|
|
398
|
+
generate(name, seed, dims, options = {}) {
|
|
399
|
+
checkSeed(seed);
|
|
400
|
+
const f = find(name, options.version);
|
|
401
|
+
const full = resolveDimensions(f.dimensions, ownDims(f, dims), label(f));
|
|
402
|
+
const context: GenerateContext = { ...streamOf(f, seed, full), ...(options.set === undefined ? {} : { set: options.set }) };
|
|
403
|
+
return f.generate(seed, full, context);
|
|
404
|
+
},
|
|
405
|
+
cellOf(name, dims, version) {
|
|
406
|
+
const f = find(name, version);
|
|
407
|
+
return { kind: familyKind(f.name, f.version), ...activeValues(f.dimensions, resolveDimensions(f.dimensions, ownDims(f, dims), label(f))) };
|
|
408
|
+
},
|
|
409
|
+
*cells(name, options = {}) {
|
|
410
|
+
const f = find(name, options.version);
|
|
411
|
+
for (const values of sweepOf(f.dimensions, sweepLevels(f.dimensions, options, label(f)))) yield resolveDimensions(f.dimensions, values, label(f));
|
|
412
|
+
},
|
|
413
|
+
cellCount(name, options = {}) {
|
|
414
|
+
const f = find(name, options.version);
|
|
415
|
+
return countOf(f.dimensions, sweepLevels(f.dimensions, options, label(f)));
|
|
416
|
+
},
|
|
417
|
+
boundaryCells(name, version) {
|
|
418
|
+
const f = find(name, version);
|
|
419
|
+
const base = resolveDimensions(f.dimensions, {}, label(f));
|
|
420
|
+
const out: DimensionValues[] = [base];
|
|
421
|
+
const seen = new Set([JSON.stringify(base)]);
|
|
422
|
+
for (const d of f.dimensions) {
|
|
423
|
+
if (d.kind !== "int" && d.kind !== "real") continue;
|
|
424
|
+
for (const end of [d.min, d.max]) {
|
|
425
|
+
if (end === undefined) continue;
|
|
426
|
+
const cell = resolveDimensions(f.dimensions, { ...activation(f.dimensions, d), [d.name]: end }, label(f));
|
|
427
|
+
const key = JSON.stringify(cell);
|
|
428
|
+
if (!seen.has(key)) {
|
|
429
|
+
seen.add(key);
|
|
430
|
+
out.push(cell);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
},
|
|
436
|
+
rngOf(name, version) {
|
|
437
|
+
const f = find(name, version);
|
|
438
|
+
return (seed, dims) => {
|
|
439
|
+
checkSeed(seed);
|
|
440
|
+
return streamOf(f, seed, resolveDimensions(f.dimensions, ownDims(f, dims), label(f))).random;
|
|
441
|
+
};
|
|
442
|
+
},
|
|
443
|
+
familyBy() {
|
|
444
|
+
const out: Record<string, readonly string[]> = {};
|
|
445
|
+
for (const f of list) out[familyKind(f.name, f.version)] = [...(f.familyBy ?? [])];
|
|
446
|
+
return out;
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
for (const f of families) registry.register(f);
|
|
450
|
+
return registry;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** A step of a composed family: a pure function that changes an input after its base family, as `edgeStep` does. */
|
|
454
|
+
export interface CorpusStep {
|
|
455
|
+
readonly name: string;
|
|
456
|
+
readonly version: number;
|
|
457
|
+
/** The engine that owns the definition of the step. */
|
|
458
|
+
readonly owner: string;
|
|
459
|
+
readonly meaning: string;
|
|
460
|
+
/** The dimensions of the step. Their names are not names of the dimensions of the base family. */
|
|
461
|
+
readonly dimensions: readonly CorpusDimension[];
|
|
462
|
+
/** The text of the rule that gives the random numbers of the step from the seed. */
|
|
463
|
+
readonly seeding: string;
|
|
464
|
+
readonly crossEngine: CrossEngine;
|
|
465
|
+
/** This function gives the part of the family of the input that tells the values of the step. Without it, the family does not change. */
|
|
466
|
+
readonly familyPart?: (dims: DimensionValues) => string;
|
|
467
|
+
/** This function gives the changed input. It removes the old `hash` of the input. */
|
|
468
|
+
apply(input: LayoutInput, seed: number, dims: DimensionValues): LayoutInput;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** The options of `composeFamily`. */
|
|
472
|
+
export interface ComposeOptions {
|
|
473
|
+
/** The default is the name of the base family and the names of the steps, with `+` between them. */
|
|
474
|
+
readonly name?: string;
|
|
475
|
+
/** The default is the owner of the base family. */
|
|
476
|
+
readonly owner?: string;
|
|
477
|
+
readonly meaning?: string;
|
|
478
|
+
/** The hash of the composed input. The default is `documentHash` of `hash.ts`. */
|
|
479
|
+
readonly hash?: (input: LayoutInput) => string;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** This function gives the values of a list of dimensions from the full values. */
|
|
483
|
+
const pick = (dims: readonly CorpusDimension[], full: DimensionValues): DimensionValues => Object.fromEntries(dims.map((d) => [d.name, full[d.name]!]));
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* This function gives a family that runs the base family and then each step, with the same seed.
|
|
487
|
+
* The family of the input gets the part of each step after a dot. The id becomes `<set>/<family>/s<seed>`, and the input gets the hash of `options.hash`.
|
|
488
|
+
* The function throws a `RangeError` when a step has a dimension with the name of an earlier dimension.
|
|
489
|
+
*/
|
|
490
|
+
export function composeFamily(base: CorpusFamily<LayoutInput>, steps: readonly CorpusStep[], options: ComposeOptions = {}): CorpusFamily<LayoutInput> {
|
|
491
|
+
const dimensions = [...base.dimensions, ...steps.flatMap((s) => s.dimensions)];
|
|
492
|
+
const seen = new Set<string>();
|
|
493
|
+
for (const d of dimensions) {
|
|
494
|
+
if (seen.has(d.name)) throw new RangeError(`composeFamily: the dimension "${d.name}" is in the base family or in an earlier step already.`);
|
|
495
|
+
seen.add(d.name);
|
|
496
|
+
}
|
|
497
|
+
const hash = options.hash ?? documentHash;
|
|
498
|
+
const libm = base.crossEngine === "libm-dependent" || steps.some((s) => s.crossEngine === "libm-dependent");
|
|
499
|
+
return Object.freeze({
|
|
500
|
+
name: options.name ?? [base.name, ...steps.map((s) => s.name)].join("+"),
|
|
501
|
+
version: base.version,
|
|
502
|
+
owner: options.owner ?? base.owner,
|
|
503
|
+
meaning: options.meaning ?? `${base.meaning} Then the steps ${steps.map((s) => s.name).join(", ")}.`,
|
|
504
|
+
dimensions,
|
|
505
|
+
seeding: [base.seeding, ...steps.map((s) => `${s.name}: ${s.seeding}`)].join(" "),
|
|
506
|
+
crossEngine: libm ? "libm-dependent" : "exact",
|
|
507
|
+
...(base.rng === undefined ? {} : { rng: base.rng }),
|
|
508
|
+
...(base.rngSeed === undefined ? {} : { rngSeed: (seed: number, full: DimensionValues) => base.rngSeed!(seed, pick(base.dimensions, full)) }),
|
|
509
|
+
...(base.familyBy === undefined ? {} : { familyBy: base.familyBy }),
|
|
510
|
+
generate(seed: number, full: DimensionValues, context: GenerateContext): LayoutInput {
|
|
511
|
+
let input = base.generate(seed, pick(base.dimensions, full), context);
|
|
512
|
+
const parts: string[] = [];
|
|
513
|
+
for (const step of steps) {
|
|
514
|
+
const values = pick(step.dimensions, full);
|
|
515
|
+
input = step.apply(input, seed, values);
|
|
516
|
+
if (step.familyPart !== undefined) parts.push(step.familyPart(values));
|
|
517
|
+
}
|
|
518
|
+
const family = [input.family ?? base.name, ...parts].join(".");
|
|
519
|
+
const set = input.set ?? context.set;
|
|
520
|
+
const id = `${set === undefined ? "" : `${set}/`}${family}/s${seed}`;
|
|
521
|
+
const stepped: LayoutInput = { ...input, id, family };
|
|
522
|
+
return { ...stepped, hash: hash(stepped) };
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The dimensions of `EDGES_STEP`: the parameters of `edgeStep`, from the shared table. */
|
|
528
|
+
const EDGE_DIMENSIONS: readonly CorpusDimension[] = Object.freeze(["edgeDensity", "edgeShape", "groups", "ports", "hubBy"].map((n) => CORPUS_DIMENSIONS[n]!));
|
|
529
|
+
|
|
530
|
+
/** This function gives the parameters of `edgeStep` from the values of the dimensions. It throws a `RangeError` for a missing or a bad value. */
|
|
531
|
+
function edgeParams(d: DimensionValues): EdgeStepParams {
|
|
532
|
+
const text = (name: string): string => {
|
|
533
|
+
const dim = EDGE_DIMENSIONS.find((q) => q.name === name)!;
|
|
534
|
+
const value = Object.hasOwn(d, name) ? d[name] : undefined;
|
|
535
|
+
const bad = value === undefined ? "the value is missing" : paramProblem(dim, value);
|
|
536
|
+
if (bad !== undefined || typeof value !== "string") throw new RangeError(`EDGES_STEP: the dimension "${name}" is bad: ${bad ?? "it is not text"}.`);
|
|
537
|
+
return value;
|
|
538
|
+
};
|
|
539
|
+
return {
|
|
540
|
+
edgeDensity: text("edgeDensity") as EdgeStepParams["edgeDensity"],
|
|
541
|
+
edgeShape: text("edgeShape") as EdgeStepParams["edgeShape"],
|
|
542
|
+
groups: text("groups") as NonNullable<EdgeStepParams["groups"]>,
|
|
543
|
+
ports: text("ports") as NonNullable<EdgeStepParams["ports"]>,
|
|
544
|
+
hubBy: text("hubBy") as NonNullable<EdgeStepParams["hubBy"]>,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* The step `edges` of `edgeStep` (version 1), as a corpus step. Its family part is `e-<edgeDensity>.<edgeShape>.<groups>.<ports>`.
|
|
550
|
+
* The value `random` of `hubBy` adds the part `.hub-random`.
|
|
551
|
+
*/
|
|
552
|
+
export const EDGES_STEP: CorpusStep = Object.freeze({
|
|
553
|
+
name: "edges",
|
|
554
|
+
version: 1,
|
|
555
|
+
owner: "layout-contract",
|
|
556
|
+
meaning: "The edges, the groups and the ports of an input, from its topology alone.",
|
|
557
|
+
dimensions: EDGE_DIMENSIONS,
|
|
558
|
+
seeding: "stepRandom(seed, step) for the steps edges and ports.",
|
|
559
|
+
crossEngine: "libm-dependent",
|
|
560
|
+
familyPart: (d: DimensionValues) => `e-${d["edgeDensity"]}.${d["edgeShape"]}.${d["groups"]}.${d["ports"]}${d["hubBy"] === "random" ? ".hub-random" : ""}`,
|
|
561
|
+
apply: (input: LayoutInput, seed: number, d: DimensionValues) => edgeStep(input, edgeParams(d), seed),
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
/** The set of `RECT_SCATTERED` when the caller gives no set. */
|
|
565
|
+
const XCHECK_SET = "xcheck-v1";
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The shared family `rect-scattered`, version 1: a fixed rectangle container, rectangle obstacles at random places and boxes without positions.
|
|
569
|
+
* It is the one copy of the generator of `docs/FORMAT.md` of `packing`. Its input is the input of `packing`, byte for byte.
|
|
570
|
+
* The steps `container`, `obstacles` and `boxes` each have a stream of `stepRandom`. Each value is a whole number, so the family is `exact`.
|
|
571
|
+
*/
|
|
572
|
+
export const RECT_SCATTERED: CorpusFamily<LayoutInput> = Object.freeze({
|
|
573
|
+
name: "rect-scattered",
|
|
574
|
+
version: 1,
|
|
575
|
+
owner: "layout-contract",
|
|
576
|
+
meaning: "Rectangle obstacles at random places in a fixed rectangle container, and boxes of random sizes.",
|
|
577
|
+
dimensions: Object.freeze([sharedDimension("count", { default: 40 })]),
|
|
578
|
+
seeding: "stepRandom(seed, container) gives w and h. stepRandom(seed, obstacles) gives n, then ow, oh, x and y of each obstacle. stepRandom(seed, boxes) gives bw and bh of each box.",
|
|
579
|
+
crossEngine: "exact",
|
|
580
|
+
generate(seed: number, dims: DimensionValues, context: GenerateContext): LayoutInput {
|
|
581
|
+
const count = dims["count"];
|
|
582
|
+
if (typeof count !== "number" || !Number.isInteger(count) || count < 0) throw new RangeError(`rect-scattered: the count is a whole number of 0 or more, not ${String(count)}.`);
|
|
583
|
+
const set = context.set ?? XCHECK_SET;
|
|
584
|
+
let r = stepRandom(seed, "container");
|
|
585
|
+
const w = randomInt(r, 600, 1000);
|
|
586
|
+
const h = randomInt(r, 400, 800);
|
|
587
|
+
r = stepRandom(seed, "obstacles");
|
|
588
|
+
const n = randomInt(r, 3, 8);
|
|
589
|
+
const obstacles: Obstacle[] = [];
|
|
590
|
+
for (let i = 0; i < n; i++) {
|
|
591
|
+
const ow = randomInt(r, 40, 160);
|
|
592
|
+
const oh = randomInt(r, 40, 160);
|
|
593
|
+
const x = randomInt(r, 0, w - ow);
|
|
594
|
+
const y = randomInt(r, 0, h - oh);
|
|
595
|
+
obstacles.push({ id: `o${i}`, region: { kind: "rect", x, y, w: ow, h: oh } });
|
|
596
|
+
}
|
|
597
|
+
r = stepRandom(seed, "boxes");
|
|
598
|
+
const boxes = Array.from({ length: count }, (_, i) => {
|
|
599
|
+
const bw = randomInt(r, 20, 120);
|
|
600
|
+
const bh = randomInt(r, 20, 80);
|
|
601
|
+
return { id: `b${i}`, w: bw, h: bh };
|
|
602
|
+
});
|
|
603
|
+
const body: LayoutInput = {
|
|
604
|
+
schema: "layout-input/1",
|
|
605
|
+
id: `${set}/rect-scattered/s${seed}`,
|
|
606
|
+
set,
|
|
607
|
+
family: "rect-scattered",
|
|
608
|
+
seed,
|
|
609
|
+
source: "made-up",
|
|
610
|
+
generator: { name: "rect-scattered", version: 1, params: { count }, seed },
|
|
611
|
+
container: { w, h, fixed: "size" },
|
|
612
|
+
obstacles,
|
|
613
|
+
boxes,
|
|
614
|
+
edges: [],
|
|
615
|
+
};
|
|
616
|
+
return { ...body, hash: documentHash(body) };
|
|
617
|
+
},
|
|
618
|
+
});
|
package/src/hash.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { canonicalInput, type LayoutInput } from "./input.ts";
|
|
1
2
|
import { RECORD_EPS } from "./tolerance.ts";
|
|
2
3
|
import { utf8 } from "./utf8.ts";
|
|
3
4
|
|
|
@@ -131,10 +132,11 @@ export function outputHash(value: unknown, options: HashOptions = {}): string {
|
|
|
131
132
|
/**
|
|
132
133
|
* This function gives the hash of a document. It uses the rule of `outputHash`, but the form does not include the field `hash` of the document.
|
|
133
134
|
* With the default grid, it is the input hash of `docs/FORMAT.md` of `packing`.
|
|
135
|
+
* A document with the schema `layout-input/1` goes through `canonicalInput` first. Thus the order of its `pairGaps` does not change the hash.
|
|
134
136
|
*/
|
|
135
137
|
export function documentHash(doc: object, options: HashOptions = {}): string {
|
|
136
|
-
const { hash: _hash, ...rest } = doc as { readonly hash?: unknown };
|
|
137
|
-
return outputHash(rest, options);
|
|
138
|
+
const { hash: _hash, ...rest } = doc as { readonly hash?: unknown; readonly schema?: unknown };
|
|
139
|
+
return outputHash(rest.schema === "layout-input/1" ? canonicalInput(rest as unknown as LayoutInput) : rest, options);
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
/** One output group: the candidates with the same output hash on each decision input. */
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./geometry.ts";
|
|
2
|
+
export * from "./shapes.ts";
|
|
2
3
|
export * from "./tolerance.ts";
|
|
3
4
|
export * from "./input.ts";
|
|
4
5
|
export * from "./output.ts";
|
|
@@ -14,3 +15,4 @@ export * from "./portfolio.ts";
|
|
|
14
15
|
export * from "./corpus.ts";
|
|
15
16
|
export * from "./strategy-space.ts";
|
|
16
17
|
export * from "./verdict.ts";
|
|
18
|
+
export * from "./families.ts";
|