@mark1russell7/layout-contract 0.2.14 → 0.2.15
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 +7 -1
- package/build/index.d.ts +84 -8
- package/build/index.d.ts.map +1 -1
- package/build/index.js +243 -30
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/families.ts +234 -26
- package/src/input.ts +34 -2
- package/src/rules.ts +15 -1
- package/src/turn.ts +8 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/layout-contract",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.15",
|
|
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
CHANGED
|
@@ -19,8 +19,35 @@ export interface CorpusDimension extends SpaceParam {
|
|
|
19
19
|
readonly engines: readonly string[];
|
|
20
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
21
|
readonly levels?: readonly ParamValue[];
|
|
22
|
+
/**
|
|
23
|
+
* The values that the family accepts. Without it, the family accepts each value of the dimension.
|
|
24
|
+
* A list is for an `enum` or a `bool`. A sub-range is for an `int` or a `real`.
|
|
25
|
+
* The default and each level are supported values. `resolve` and `generate` refuse a value that is not supported.
|
|
26
|
+
*/
|
|
27
|
+
readonly supported?: SupportedValues;
|
|
28
|
+
/**
|
|
29
|
+
* The output fields that a value other than the default makes wrong, for example `known`.
|
|
30
|
+
* Then `generate` of a registry removes these fields from the output, and lists them in `invalidated`.
|
|
31
|
+
*/
|
|
32
|
+
readonly invalidates?: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The supported values of a dimension (`CorpusDimension.supported`).
|
|
37
|
+
* The list form gives the values of an `enum` or a `bool`. The range form gives a sub-range of an `int` or a `real`.
|
|
38
|
+
* A range without `min` or `max` keeps that end of the dimension.
|
|
39
|
+
*/
|
|
40
|
+
export type SupportedValues = readonly ParamValue[] | SupportedRange;
|
|
41
|
+
|
|
42
|
+
/** The range form of `SupportedValues`: the least and the largest supported value of a numeric dimension. */
|
|
43
|
+
export interface SupportedRange {
|
|
44
|
+
readonly min?: number;
|
|
45
|
+
readonly max?: number;
|
|
22
46
|
}
|
|
23
47
|
|
|
48
|
+
/** The name of the output field that lists the fields that `generate` of a registry removed. */
|
|
49
|
+
export const INVALIDATED_FIELD = "invalidated";
|
|
50
|
+
|
|
24
51
|
/** The values of the dimensions of one input: one value for each name. */
|
|
25
52
|
export type DimensionValues = Readonly<Record<string, ParamValue>>;
|
|
26
53
|
|
|
@@ -48,7 +75,8 @@ export interface GenerateContext {
|
|
|
48
75
|
|
|
49
76
|
/**
|
|
50
77
|
* 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.
|
|
78
|
+
* The registry gives `generate` the raw seed and the full dimension values.
|
|
79
|
+
* The registry changes the output only for a dimension with `invalidates` at a value other than its default.
|
|
52
80
|
*/
|
|
53
81
|
export interface CorpusFamily<O = LayoutInput> {
|
|
54
82
|
/** The name of the family, for example `rect-grid`. */
|
|
@@ -71,6 +99,12 @@ export interface CorpusFamily<O = LayoutInput> {
|
|
|
71
99
|
readonly rngSeed?: (seed: number, dims: DimensionValues) => number;
|
|
72
100
|
/** 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
101
|
readonly familyBy?: readonly string[];
|
|
102
|
+
/**
|
|
103
|
+
* This function gives the hash of an output after the registry removes the fields of `invalidates`. The default is `documentHash` of `hash.ts`.
|
|
104
|
+
* The registry uses it only for an output with a `hash` field of text. The registry gives it an output of the family.
|
|
105
|
+
* Its parameter has the type `never`, so a family of each output type can be a `CorpusFamily<unknown>`.
|
|
106
|
+
*/
|
|
107
|
+
readonly hash?: (output: never) => string;
|
|
74
108
|
/** This function gives the input of a seed. `dims` has a value for each dimension, also for an inactive dimension. */
|
|
75
109
|
generate(seed: number, dims: DimensionValues, context: GenerateContext): O;
|
|
76
110
|
}
|
|
@@ -109,11 +143,14 @@ export interface FamilyRegistry<O = LayoutInput> {
|
|
|
109
143
|
/** This method gives each family, in the order of registration. */
|
|
110
144
|
families(): readonly CorpusFamily<O>[];
|
|
111
145
|
/**
|
|
112
|
-
* This method gives the full dimension values of an input. It throws a `RangeError` for an unknown dimension or a
|
|
146
|
+
* This method gives the full dimension values of an input. It throws a `RangeError` for an unknown dimension, a bad value or a value that is not supported.
|
|
113
147
|
* 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
148
|
*/
|
|
115
149
|
resolve(name: string, dims?: Readonly<Record<string, ParamValue>>, version?: number): DimensionValues;
|
|
116
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* This method gives the input of a seed: the output of the family. A dimension with `invalidates` at a value other than its default changes it.
|
|
152
|
+
* Then the method removes those fields, adds their names to `invalidated` and calculates the `hash` again. Another output has no change.
|
|
153
|
+
*/
|
|
117
154
|
generate(name: string, seed: number, dims?: Readonly<Record<string, ParamValue>>, options?: GenerateOptions): O;
|
|
118
155
|
/** This method gives the cell of an input: the `familyKind` of the family version and the value of each active dimension. */
|
|
119
156
|
cellOf(name: string, dims?: Readonly<Record<string, ParamValue>>, version?: number): RegistryCell;
|
|
@@ -187,11 +224,123 @@ export const CORPUS_DIMENSIONS: Readonly<Record<string, CorpusDimension>> = Obje
|
|
|
187
224
|
gap: range("gap", "real", { min: 0, max: 32, value: 0 }, "The free space between two items, in units.", [P, J]),
|
|
188
225
|
});
|
|
189
226
|
|
|
190
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* This function gives a dimension of `CORPUS_DIMENSIONS`, with the fields of `patch` over it. It throws a `RangeError` for an unknown name.
|
|
229
|
+
* With `supported` or `invalidates` in the patch, it also throws a `RangeError` for a bad dimension, as `ownerDimension` does.
|
|
230
|
+
*/
|
|
191
231
|
export function sharedDimension(name: string, patch: Partial<CorpusDimension> = {}): CorpusDimension {
|
|
192
232
|
const base = Object.hasOwn(CORPUS_DIMENSIONS, name) ? CORPUS_DIMENSIONS[name] : undefined;
|
|
193
233
|
if (base === undefined) throw new RangeError(`The shared dimension table has no dimension "${name}".`);
|
|
194
|
-
|
|
234
|
+
const dim: CorpusDimension = Object.freeze({ ...base, ...patch, name });
|
|
235
|
+
if (patch.supported !== undefined || patch.invalidates !== undefined) throwProblems(`sharedDimension: the dimension "${name}"`, ownProblems(dim));
|
|
236
|
+
return dim;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The fields of a dimension that the owner declares: each field of `CorpusDimension` but the name. */
|
|
240
|
+
export type OwnerDimensionSpec = Omit<CorpusDimension, "name">;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* This function gives a dimension that only its owner declares, for example `cut` of a family of `packing`.
|
|
244
|
+
* It throws a `RangeError` for a name of `CORPUS_DIMENSIONS`, the reserved name `kind`, or a bad default, level, `supported` or `invalidates`.
|
|
245
|
+
* The registry examines the `when` of the dimension, because it needs the earlier dimensions.
|
|
246
|
+
*/
|
|
247
|
+
export function ownerDimension(name: string, spec: OwnerDimensionSpec): CorpusDimension {
|
|
248
|
+
const where = `ownerDimension: the dimension "${name}"`;
|
|
249
|
+
if (typeof name !== "string" || name === "") throw new RangeError("ownerDimension: the name of a dimension is a text that is not empty.");
|
|
250
|
+
if (name === "kind") throw new RangeError(`${where} has a reserved name: the cell of \`cellOf\` has the \`familyKind\` of the family in \`kind\`.`);
|
|
251
|
+
if (Object.hasOwn(CORPUS_DIMENSIONS, name)) throw new RangeError(`${where} is in the shared dimension table. Use sharedDimension("${name}") for it.`);
|
|
252
|
+
const dim: CorpusDimension = Object.freeze({ ...spec, name });
|
|
253
|
+
throwProblems(where, ownProblems(dim));
|
|
254
|
+
return dim;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** This function throws a `RangeError` with each problem of a list. An empty list throws nothing. */
|
|
258
|
+
function throwProblems(where: string, problems: readonly string[]): void {
|
|
259
|
+
if (problems.length > 0) throw new RangeError(`${where} is not good: ${problems.join("; ")}.`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** This function tells if a value of the stable form of a dimension is in its `supported` values. Without `supported`, each value is in them. */
|
|
263
|
+
function inSupported(dim: CorpusDimension, value: ParamValue): boolean {
|
|
264
|
+
const s = dim.supported;
|
|
265
|
+
if (s === undefined) return true;
|
|
266
|
+
if (isSupportedList(s)) return s.some((v) => paramValue(dim, v) === value);
|
|
267
|
+
if (typeof value !== "number") return false;
|
|
268
|
+
return (s.min === undefined || value >= s.min) && (s.max === undefined || value <= s.max);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** This function tells if `SupportedValues` has the list form. */
|
|
272
|
+
const isSupportedList = (s: SupportedValues): s is readonly ParamValue[] => Array.isArray(s);
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* This function tells if a family accepts a value of a dimension. A site uses it to show the values that a family does not support.
|
|
276
|
+
* It gives `false` for a bad value, and for a value outside `supported`. Without `supported`, it gives `true` for each good value.
|
|
277
|
+
*/
|
|
278
|
+
export function dimensionSupports(dim: CorpusDimension, value: unknown): boolean {
|
|
279
|
+
if (paramProblem(dim, value) !== undefined) return false;
|
|
280
|
+
return inSupported(dim, paramValue(dim, value as ParamValue));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** This function gives the text of the supported values of a dimension, for a message. */
|
|
284
|
+
function supportedText(dim: CorpusDimension): string {
|
|
285
|
+
const s = dim.supported;
|
|
286
|
+
if (s === undefined) return "each value";
|
|
287
|
+
if (isSupportedList(s)) return s.map((v) => JSON.stringify(v)).join(", ");
|
|
288
|
+
return `the values from ${s.min ?? dim.min} to ${s.max ?? dim.max}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** This function gives the problems of one dimension that need no other dimension: the default, the levels, `supported` and `invalidates`. */
|
|
292
|
+
function ownProblems(d: CorpusDimension): string[] {
|
|
293
|
+
const out: string[] = [];
|
|
294
|
+
const bad = paramProblem(d, d.default);
|
|
295
|
+
if (bad !== undefined) out.push(`the default of "${d.name}" is bad: ${bad}`);
|
|
296
|
+
for (const v of d.levels ?? []) {
|
|
297
|
+
const p = paramProblem(d, v);
|
|
298
|
+
if (p !== undefined) out.push(`a level of "${d.name}" is bad: ${p}`);
|
|
299
|
+
}
|
|
300
|
+
out.push(...supportProblems(d, bad === undefined));
|
|
301
|
+
out.push(...invalidatesProblems(d));
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** This function gives the problems of the `supported` values of a dimension. `goodDefault` tells that the default is a good value. */
|
|
306
|
+
function supportProblems(d: CorpusDimension, goodDefault: boolean): string[] {
|
|
307
|
+
const s = d.supported;
|
|
308
|
+
if (s === undefined) return [];
|
|
309
|
+
const out: string[] = [];
|
|
310
|
+
const numeric = d.kind === "int" || d.kind === "real";
|
|
311
|
+
if (isSupportedList(s)) {
|
|
312
|
+
if (numeric) return [`the supported values of "${d.name}" are a range with min and max, because the dimension is numeric`];
|
|
313
|
+
if (s.length === 0) return [`the supported values of "${d.name}" are an empty list`];
|
|
314
|
+
for (const v of s) {
|
|
315
|
+
const p = paramProblem(d, v);
|
|
316
|
+
if (p !== undefined) out.push(`a supported value of "${d.name}" is bad: ${p}`);
|
|
317
|
+
}
|
|
318
|
+
if (new Set(s).size !== s.length) out.push(`the supported values of "${d.name}" have a value two times`);
|
|
319
|
+
} else {
|
|
320
|
+
if (s === null || typeof s !== "object") return [`the supported values of "${d.name}" are a list or a range`];
|
|
321
|
+
if (!numeric) return [`the supported values of "${d.name}" are a list, because the dimension is not numeric`];
|
|
322
|
+
for (const end of [s.min, s.max]) {
|
|
323
|
+
if (end === undefined) continue;
|
|
324
|
+
const p = paramProblem(d, end);
|
|
325
|
+
if (p !== undefined) out.push(`an end of the supported range of "${d.name}" is bad: ${p}`);
|
|
326
|
+
}
|
|
327
|
+
if (s.min !== undefined && s.max !== undefined && s.min > s.max) out.push(`the supported range of "${d.name}" has a min above its max`);
|
|
328
|
+
}
|
|
329
|
+
if (out.length > 0) return out;
|
|
330
|
+
if (goodDefault && !inSupported(d, paramValue(d, d.default))) out.push(`the default of "${d.name}" is not a supported value`);
|
|
331
|
+
for (const v of d.levels ?? []) if (paramProblem(d, v) === undefined && !inSupported(d, paramValue(d, v))) out.push(`the level ${JSON.stringify(v)} of "${d.name}" is not a supported value`);
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** This function gives the problems of the `invalidates` fields of a dimension. */
|
|
336
|
+
function invalidatesProblems(d: CorpusDimension): string[] {
|
|
337
|
+
const list = d.invalidates;
|
|
338
|
+
if (list === undefined) return [];
|
|
339
|
+
if (!Array.isArray(list) || !list.every((f) => typeof f === "string" && f !== "")) return [`the invalidates of "${d.name}" is not a list of field names`];
|
|
340
|
+
const out: string[] = [];
|
|
341
|
+
if (new Set(list).size !== list.length) out.push(`the invalidates of "${d.name}" has a field two times`);
|
|
342
|
+
for (const f of ["hash", INVALIDATED_FIELD]) if (list.includes(f)) out.push(`the invalidates of "${d.name}" has the field "${f}", which the registry writes`);
|
|
343
|
+
return out;
|
|
195
344
|
}
|
|
196
345
|
|
|
197
346
|
/** This function tells if a log scale of a range starts at 0. Then the scale is the log of `1 + x - min`. */
|
|
@@ -224,31 +373,40 @@ export function dimensionUnit(dim: SpaceParam, value: number): number {
|
|
|
224
373
|
return (Math.log(v) - Math.log(min)) / (Math.log(max) - Math.log(min));
|
|
225
374
|
}
|
|
226
375
|
|
|
376
|
+
/** This function gives the range of the supported values of a numeric dimension: its own `min` and `max` in place of the ends of `supported`. */
|
|
377
|
+
function supportedSpan(dim: CorpusDimension): { readonly min: number | undefined; readonly max: number | undefined } {
|
|
378
|
+
const s = dim.supported;
|
|
379
|
+
const range = s === undefined || isSupportedList(s) ? {} : s;
|
|
380
|
+
return { min: range.min ?? dim.min, max: range.max ?? dim.max };
|
|
381
|
+
}
|
|
382
|
+
|
|
227
383
|
/**
|
|
228
384
|
* 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
385
|
* With `steps`, a numeric dimension gives `steps` points from `min` to `max` on its scale. Else it gives its default.
|
|
386
|
+
* With `supported`, an `enum` and a `bool` give only the supported values, and the points of a numeric dimension stay in the supported range.
|
|
230
387
|
*/
|
|
231
388
|
export function levelsOf(dim: CorpusDimension, steps?: number): ParamValue[] {
|
|
232
389
|
if (dim.levels !== undefined) return [...dim.levels];
|
|
233
|
-
if (dim.kind === "enum" || dim.kind === "bool"
|
|
390
|
+
if (dim.kind === "enum" || dim.kind === "bool") return paramLevels(dim, 1).filter((v) => inSupported(dim, paramValue(dim, v)));
|
|
391
|
+
if (steps === undefined) return paramLevels(dim, 1);
|
|
392
|
+
if (dim.supported !== undefined) {
|
|
393
|
+
const { supported: _supported, ...rest } = dim;
|
|
394
|
+
const span = supportedSpan(dim);
|
|
395
|
+
return levelsOf({ ...rest, ...(span.min === undefined ? {} : { min: span.min }), ...(span.max === undefined ? {} : { max: span.max }) }, steps);
|
|
396
|
+
}
|
|
234
397
|
if (!fromZero(dim)) return paramLevels(dim, steps);
|
|
235
398
|
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
399
|
if (steps === 1) return [paramValue(dim, dim.default)];
|
|
237
400
|
return [...new Set(Array.from({ length: steps }, (_, j) => dimensionAt(dim, j / (steps - 1))))];
|
|
238
401
|
}
|
|
239
402
|
|
|
240
|
-
/** This function gives the problems of the dimensions of a family: a repeated name, a bad default,
|
|
403
|
+
/** This function gives the problems of the dimensions of a family: a repeated name, a bad default, level, `supported`, `invalidates` or `when`. */
|
|
241
404
|
function dimensionProblems(dims: readonly CorpusDimension[]): string[] {
|
|
242
405
|
const out: string[] = [];
|
|
243
406
|
const names = dims.map((d) => d.name);
|
|
244
407
|
dims.forEach((d, i) => {
|
|
245
408
|
if (names.indexOf(d.name) !== i) out.push(`the dimension "${d.name}" is in the list two times`);
|
|
246
|
-
|
|
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
|
-
}
|
|
409
|
+
out.push(...ownProblems(d));
|
|
252
410
|
if (d.when === undefined) return;
|
|
253
411
|
const j = names.indexOf(d.when.param);
|
|
254
412
|
if (j < 0 || j >= i) out.push(`the when of "${d.name}" names "${d.when.param}", which is not an earlier dimension`);
|
|
@@ -261,9 +419,16 @@ function dimensionProblems(dims: readonly CorpusDimension[]): string[] {
|
|
|
261
419
|
/**
|
|
262
420
|
* 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
421
|
* 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.
|
|
422
|
+
*
|
|
264
423
|
* The function throws a `RangeError` for an unknown dimension or a bad value, also for a bad value of an inactive dimension.
|
|
424
|
+
* It also throws a `RangeError` for a value outside `supported`, with the text "not supported by" and `by`.
|
|
265
425
|
*/
|
|
266
|
-
export function resolveDimensions(
|
|
426
|
+
export function resolveDimensions(
|
|
427
|
+
dimensions: readonly CorpusDimension[],
|
|
428
|
+
dims: Readonly<Record<string, ParamValue>> = {},
|
|
429
|
+
where: string = "The family",
|
|
430
|
+
by: string = "the family",
|
|
431
|
+
): DimensionValues {
|
|
267
432
|
const known = new Set(dimensions.map((d) => d.name));
|
|
268
433
|
for (const key of Object.keys(dims)) if (!known.has(key)) throw new RangeError(`${where} has no dimension "${key}".`);
|
|
269
434
|
const active: Record<string, ParamValue> = {};
|
|
@@ -273,6 +438,9 @@ export function resolveDimensions(dimensions: readonly CorpusDimension[], dims:
|
|
|
273
438
|
if (given !== undefined) {
|
|
274
439
|
const bad = paramProblem(d, given);
|
|
275
440
|
if (bad !== undefined) throw new RangeError(`${where}: the dimension "${d.name}" is bad: ${bad}.`);
|
|
441
|
+
if (!inSupported(d, paramValue(d, given))) {
|
|
442
|
+
throw new RangeError(`${where}: the value ${JSON.stringify(given)} of the dimension "${d.name}" is not supported by ${by}. The supported values are ${supportedText(d)}.`);
|
|
443
|
+
}
|
|
276
444
|
}
|
|
277
445
|
const on = paramActive(d, active);
|
|
278
446
|
const value = paramValue(d, on && given !== undefined ? given : d.default);
|
|
@@ -318,7 +486,7 @@ function countOf(dimensions: readonly CorpusDimension[], levels: readonly (reado
|
|
|
318
486
|
}
|
|
319
487
|
|
|
320
488
|
/** 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)[] {
|
|
489
|
+
function sweepLevels(dimensions: readonly CorpusDimension[], options: CellsOptions, where: string, by: string): (readonly ParamValue[] | undefined)[] {
|
|
322
490
|
const names = new Set(dimensions.map((d) => d.name));
|
|
323
491
|
for (const name of [...(options.sweep ?? []), ...Object.keys(options.levels ?? {})]) if (!names.has(name)) throw new RangeError(`${where} has no dimension "${name}".`);
|
|
324
492
|
return dimensions.map((d) => {
|
|
@@ -327,11 +495,44 @@ function sweepLevels(dimensions: readonly CorpusDimension[], options: CellsOptio
|
|
|
327
495
|
for (const v of lv) {
|
|
328
496
|
const bad = paramProblem(d, v);
|
|
329
497
|
if (bad !== undefined) throw new RangeError(`${where}: a level of "${d.name}" is bad: ${bad}.`);
|
|
498
|
+
if (!inSupported(d, paramValue(d, v))) throw new RangeError(`${where}: the level ${JSON.stringify(v)} of "${d.name}" is not supported by ${by}.`);
|
|
330
499
|
}
|
|
331
500
|
return lv;
|
|
332
501
|
});
|
|
333
502
|
}
|
|
334
503
|
|
|
504
|
+
/** This function gives the output fields that the values make wrong: the `invalidates` of each dimension with a value other than its default, sorted. */
|
|
505
|
+
function invalidFields(dimensions: readonly CorpusDimension[], full: DimensionValues): string[] {
|
|
506
|
+
const out = new Set<string>();
|
|
507
|
+
for (const d of dimensions) {
|
|
508
|
+
if (d.invalidates === undefined || d.invalidates.length === 0) continue;
|
|
509
|
+
if (full[d.name] !== paramValue(d, d.default)) for (const f of d.invalidates) out.add(f);
|
|
510
|
+
}
|
|
511
|
+
return [...out].sort(byCode);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** This function compares two strings by their UTF-16 code units. */
|
|
515
|
+
const byCode = (x: string, y: string): number => (x < y ? -1 : x > y ? 1 : 0);
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* This function removes the fields of `fields` from an output, and adds their names to its field `invalidated`, sorted.
|
|
519
|
+
* Only a field of the output counts. The `hash` of text gets the value of `hash` again. An output with none of the fields is the same object.
|
|
520
|
+
*/
|
|
521
|
+
function dropFields<O>(output: O, fields: readonly string[], hash: (output: O) => string, where: string): O {
|
|
522
|
+
if (fields.length === 0) return output;
|
|
523
|
+
if (output === null || typeof output !== "object" || Array.isArray(output)) throw new RangeError(`${where} invalidates the fields ${fields.join(", ")}, but its output is not an object.`);
|
|
524
|
+
const o = output as Record<string, unknown>;
|
|
525
|
+
const dropped = fields.filter((f) => Object.hasOwn(o, f));
|
|
526
|
+
if (dropped.length === 0) return output;
|
|
527
|
+
const before = o[INVALIDATED_FIELD];
|
|
528
|
+
const prior = Array.isArray(before) ? before.filter((f): f is string => typeof f === "string") : [];
|
|
529
|
+
const body: Record<string, unknown> = {};
|
|
530
|
+
for (const key of Object.keys(o)) if (key !== "hash" && key !== INVALIDATED_FIELD && !dropped.includes(key)) body[key] = o[key];
|
|
531
|
+
body[INVALIDATED_FIELD] = [...new Set([...prior, ...dropped])].sort(byCode);
|
|
532
|
+
if (typeof o["hash"] !== "string") return (Object.hasOwn(o, "hash") ? { ...body, hash: o["hash"] } : body) as O;
|
|
533
|
+
return { ...body, hash: hash(body as O) } as O;
|
|
534
|
+
}
|
|
535
|
+
|
|
335
536
|
/** 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
537
|
function activation(dimensions: readonly CorpusDimension[], dim: CorpusDimension): Record<string, ParamValue> {
|
|
337
538
|
const out: Record<string, ParamValue> = {};
|
|
@@ -369,6 +570,9 @@ export function createFamilyRegistry<O = LayoutInput>(families: Iterable<CorpusF
|
|
|
369
570
|
const { kind: _kind, ...rest } = dims;
|
|
370
571
|
return rest;
|
|
371
572
|
};
|
|
573
|
+
/** This function gives the full dimension values of an input of `f`. A message names the family as `familyKind` gives it. */
|
|
574
|
+
const resolveIn = (f: CorpusFamily<O>, dims: Readonly<Record<string, ParamValue>> | undefined): DimensionValues =>
|
|
575
|
+
resolveDimensions(f.dimensions, dims, label(f), familyKind(f.name, f.version));
|
|
372
576
|
const streamOf = (f: CorpusFamily<O>, seed: number, full: DimensionValues): { readonly rng: RngFactory; readonly random: () => number } => {
|
|
373
577
|
const rng = f.rng ?? DEFAULT_RNG;
|
|
374
578
|
return { rng, random: rng(f.rngSeed === undefined ? seed : f.rngSeed(seed, full)) };
|
|
@@ -393,37 +597,40 @@ export function createFamilyRegistry<O = LayoutInput>(families: Iterable<CorpusF
|
|
|
393
597
|
families: () => [...list],
|
|
394
598
|
resolve(name, dims, version) {
|
|
395
599
|
const f = find(name, version);
|
|
396
|
-
return
|
|
600
|
+
return resolveIn(f, ownDims(f, dims));
|
|
397
601
|
},
|
|
398
602
|
generate(name, seed, dims, options = {}) {
|
|
399
603
|
checkSeed(seed);
|
|
400
604
|
const f = find(name, options.version);
|
|
401
|
-
const full =
|
|
605
|
+
const full = resolveIn(f, ownDims(f, dims));
|
|
402
606
|
const context: GenerateContext = { ...streamOf(f, seed, full), ...(options.set === undefined ? {} : { set: options.set }) };
|
|
403
|
-
|
|
607
|
+
const output = f.generate(seed, full, context);
|
|
608
|
+
const hash = (f.hash ?? documentHash) as (o: O) => string;
|
|
609
|
+
return dropFields(output, invalidFields(f.dimensions, full), hash, label(f));
|
|
404
610
|
},
|
|
405
611
|
cellOf(name, dims, version) {
|
|
406
612
|
const f = find(name, version);
|
|
407
|
-
return { kind: familyKind(f.name, f.version), ...activeValues(f.dimensions,
|
|
613
|
+
return { kind: familyKind(f.name, f.version), ...activeValues(f.dimensions, resolveIn(f, ownDims(f, dims))) };
|
|
408
614
|
},
|
|
409
615
|
*cells(name, options = {}) {
|
|
410
616
|
const f = find(name, options.version);
|
|
411
|
-
for (const values of sweepOf(f.dimensions, sweepLevels(f.dimensions, options, label(f)))) yield
|
|
617
|
+
for (const values of sweepOf(f.dimensions, sweepLevels(f.dimensions, options, label(f), familyKind(f.name, f.version)))) yield resolveIn(f, values);
|
|
412
618
|
},
|
|
413
619
|
cellCount(name, options = {}) {
|
|
414
620
|
const f = find(name, options.version);
|
|
415
|
-
return countOf(f.dimensions, sweepLevels(f.dimensions, options, label(f)));
|
|
621
|
+
return countOf(f.dimensions, sweepLevels(f.dimensions, options, label(f), familyKind(f.name, f.version)));
|
|
416
622
|
},
|
|
417
623
|
boundaryCells(name, version) {
|
|
418
624
|
const f = find(name, version);
|
|
419
|
-
const base =
|
|
625
|
+
const base = resolveIn(f, {});
|
|
420
626
|
const out: DimensionValues[] = [base];
|
|
421
627
|
const seen = new Set([JSON.stringify(base)]);
|
|
422
628
|
for (const d of f.dimensions) {
|
|
423
629
|
if (d.kind !== "int" && d.kind !== "real") continue;
|
|
424
|
-
|
|
630
|
+
const span = supportedSpan(d);
|
|
631
|
+
for (const end of [span.min, span.max]) {
|
|
425
632
|
if (end === undefined) continue;
|
|
426
|
-
const cell =
|
|
633
|
+
const cell = resolveIn(f, { ...activation(f.dimensions, d), [d.name]: end });
|
|
427
634
|
const key = JSON.stringify(cell);
|
|
428
635
|
if (!seen.has(key)) {
|
|
429
636
|
seen.add(key);
|
|
@@ -437,7 +644,7 @@ export function createFamilyRegistry<O = LayoutInput>(families: Iterable<CorpusF
|
|
|
437
644
|
const f = find(name, version);
|
|
438
645
|
return (seed, dims) => {
|
|
439
646
|
checkSeed(seed);
|
|
440
|
-
return streamOf(f, seed,
|
|
647
|
+
return streamOf(f, seed, resolveIn(f, ownDims(f, dims))).random;
|
|
441
648
|
};
|
|
442
649
|
},
|
|
443
650
|
familyBy() {
|
|
@@ -475,7 +682,7 @@ export interface ComposeOptions {
|
|
|
475
682
|
/** The default is the owner of the base family. */
|
|
476
683
|
readonly owner?: string;
|
|
477
684
|
readonly meaning?: string;
|
|
478
|
-
/** The hash of the composed input. The default is `documentHash` of `hash.ts`. */
|
|
685
|
+
/** The hash of the composed input. The default is `documentHash` of `hash.ts`. The composed family also gives it to the registry in `hash`. */
|
|
479
686
|
readonly hash?: (input: LayoutInput) => string;
|
|
480
687
|
}
|
|
481
688
|
|
|
@@ -507,6 +714,7 @@ export function composeFamily(base: CorpusFamily<LayoutInput>, steps: readonly C
|
|
|
507
714
|
...(base.rng === undefined ? {} : { rng: base.rng }),
|
|
508
715
|
...(base.rngSeed === undefined ? {} : { rngSeed: (seed: number, full: DimensionValues) => base.rngSeed!(seed, pick(base.dimensions, full)) }),
|
|
509
716
|
...(base.familyBy === undefined ? {} : { familyBy: base.familyBy }),
|
|
717
|
+
...(options.hash === undefined ? {} : { hash: options.hash }),
|
|
510
718
|
generate(seed: number, full: DimensionValues, context: GenerateContext): LayoutInput {
|
|
511
719
|
let input = base.generate(seed, pick(base.dimensions, full), context);
|
|
512
720
|
const parts: string[] = [];
|
package/src/input.ts
CHANGED
|
@@ -1,16 +1,43 @@
|
|
|
1
|
-
import type { Margin, Region, Side } from "./geometry.ts";
|
|
1
|
+
import type { Margin, Point, Region, Side } from "./geometry.ts";
|
|
2
2
|
import type { ShapeKind } from "./shapes.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A port of a box: a place where edges go into the box.
|
|
6
6
|
* With `x` and `y`, the port is fixed on the border of the box, relative to its top-left corner.
|
|
7
|
-
*
|
|
7
|
+
* With `side` and `along`, the port is fixed at a fraction of its side.
|
|
8
|
+
* With only a side, the router places the port on that side. Without a side, the router picks the side.
|
|
8
9
|
*/
|
|
9
10
|
export interface Port {
|
|
10
11
|
readonly id: string;
|
|
11
12
|
readonly side?: Side;
|
|
12
13
|
readonly x?: number;
|
|
13
14
|
readonly y?: number;
|
|
15
|
+
/**
|
|
16
|
+
* The place of the port on its `side`: a fraction from 0 to 1, from the start of the side.
|
|
17
|
+
* The start of `left` and `right` is the top end. The start of `top` and `bottom` is the left end.
|
|
18
|
+
* It needs a `side`. A port with `along` has no `x` and no `y`.
|
|
19
|
+
*/
|
|
20
|
+
readonly along?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This function gives the point of a port on the border of its box, relative to the top-left corner of the box.
|
|
25
|
+
* A port with `x` and `y` gives that point. A port with `side` and `along` gives the point at that fraction of the side.
|
|
26
|
+
* Another port gives `undefined`, because the router picks its point.
|
|
27
|
+
*/
|
|
28
|
+
export function portPoint(box: Pick<LayoutBox, "w" | "h">, port: Port): Point | undefined {
|
|
29
|
+
if (port.x !== undefined && port.y !== undefined) return { x: port.x, y: port.y };
|
|
30
|
+
if (port.side === undefined || port.along === undefined) return undefined;
|
|
31
|
+
switch (port.side) {
|
|
32
|
+
case "top":
|
|
33
|
+
return { x: port.along * box.w, y: 0 };
|
|
34
|
+
case "bottom":
|
|
35
|
+
return { x: port.along * box.w, y: box.h };
|
|
36
|
+
case "left":
|
|
37
|
+
return { x: 0, y: port.along * box.h };
|
|
38
|
+
case "right":
|
|
39
|
+
return { x: box.w, y: port.along * box.h };
|
|
40
|
+
}
|
|
14
41
|
}
|
|
15
42
|
|
|
16
43
|
/** A box of an input. In the packer, its name is item. */
|
|
@@ -148,6 +175,11 @@ export interface LayoutInput {
|
|
|
148
175
|
*/
|
|
149
176
|
readonly base?: string;
|
|
150
177
|
readonly change?: "add" | "remove" | "resize" | "reorder" | "packed";
|
|
178
|
+
/**
|
|
179
|
+
* The fields that a corpus registry removed, because a dimension value made them wrong (`CorpusDimension.invalidates`).
|
|
180
|
+
* The names are sorted. An input with no removed field has no `invalidated`.
|
|
181
|
+
*/
|
|
182
|
+
readonly invalidated?: readonly string[];
|
|
151
183
|
}
|
|
152
184
|
|
|
153
185
|
/** The input of a packer: a `layout-input/1` file without the edges. */
|
package/src/rules.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { boundsOf, grow, overlapOf, type Rect, type Region } from "./geometry.ts";
|
|
2
|
-
import type { LayoutBox, LayoutInput, Obstacle, PairGap } from "./input.ts";
|
|
2
|
+
import type { LayoutBox, LayoutInput, Obstacle, PairGap, Port } from "./input.ts";
|
|
3
3
|
import { applyPack, type PackOutput } from "./output.ts";
|
|
4
4
|
import { SHAPE_KIND_IDS, shapeKindProblem, type ShapeSupport } from "./shapes.ts";
|
|
5
5
|
import { DEFAULT_TOLERANCE, EPS, RECORD_EPS } from "./tolerance.ts";
|
|
@@ -352,6 +352,7 @@ export function packHard(input: LayoutInput, output: PackOutput, tolerance: numb
|
|
|
352
352
|
* This function gives the problems of an input file. An empty list tells that the file is good.
|
|
353
353
|
* A problem is a bad input, not a break of a hard rule. Thus an engine refuses the input.
|
|
354
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.
|
|
355
|
+
* A port with `along` and no `side`, with `x` or `y`, or with a value outside [0, 1] is a problem.
|
|
355
356
|
*/
|
|
356
357
|
export function checkInput(input: LayoutInput): readonly string[] {
|
|
357
358
|
const problems: string[] = [];
|
|
@@ -363,6 +364,7 @@ export function checkInput(input: LayoutInput): readonly string[] {
|
|
|
363
364
|
if (box.locked === true && (box.x === undefined || box.y === undefined)) problems.push(`box ${box.id}: a locked box has no position`);
|
|
364
365
|
const shape = shapeKindProblem(box);
|
|
365
366
|
if (shape !== undefined) problems.push(shape);
|
|
367
|
+
for (const port of box.ports ?? []) problems.push(...alongProblems(box.id, port));
|
|
366
368
|
}
|
|
367
369
|
if (countPortsOffBorder(input.boxes) > 0) problems.push("a fixed port is not on the border of its box");
|
|
368
370
|
for (const edge of input.edges ?? []) {
|
|
@@ -372,6 +374,18 @@ export function checkInput(input: LayoutInput): readonly string[] {
|
|
|
372
374
|
return problems;
|
|
373
375
|
}
|
|
374
376
|
|
|
377
|
+
/** This function gives the problems of the `along` of a port: no side, a value outside [0, 1], or `x` or `y` with it. A port without `along` has no problem. */
|
|
378
|
+
function alongProblems(boxId: string, port: Port): string[] {
|
|
379
|
+
if (port.along === undefined) return [];
|
|
380
|
+
const at = `box ${boxId} port ${port.id}`;
|
|
381
|
+
const problems: string[] = [];
|
|
382
|
+
if (port.side === undefined) problems.push(`${at}: a port with along has no side`);
|
|
383
|
+
if (typeof port.along !== "number" || !Number.isFinite(port.along) || port.along < 0 || port.along > 1)
|
|
384
|
+
problems.push(`${at}: the along value ${String(port.along)} is not a finite number from 0 to 1`);
|
|
385
|
+
if (port.x !== undefined || port.y !== undefined) problems.push(`${at}: a port with along also has x or y`);
|
|
386
|
+
return problems;
|
|
387
|
+
}
|
|
388
|
+
|
|
375
389
|
/** This function gives the problems of the `pairGaps` of an input. `ids` has the ids of the boxes. */
|
|
376
390
|
function pairGapProblems(pairGaps: LayoutInput["pairGaps"], ids: ReadonlySet<string>): string[] {
|
|
377
391
|
if (pairGaps === undefined) return [];
|
package/src/turn.ts
CHANGED
|
@@ -18,14 +18,20 @@ export function turnPoint(x: number, y: number, h: number): [number, number] {
|
|
|
18
18
|
return [h - y, x];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* This function gives a port of a box of height `h` after the turn. Each value of the port turns, also a port with only `x` or only `y`.
|
|
23
|
+
* The start of a side is its top end or its left end. Thus `left` goes to `top` and `right` goes to `bottom` with `along` changed to `1 - along`.
|
|
24
|
+
* `top` goes to `right` and `bottom` goes to `left` with the same `along`.
|
|
25
|
+
*/
|
|
22
26
|
export function turnPort(port: Port, h: number): Port {
|
|
23
|
-
const { side, x, y, ...rest } = port;
|
|
27
|
+
const { side, x, y, along, ...rest } = port;
|
|
28
|
+
const flips = side === "left" || side === "right";
|
|
24
29
|
return {
|
|
25
30
|
...rest,
|
|
26
31
|
...(side === undefined ? {} : { side: turnSide(side) }),
|
|
27
32
|
...(y === undefined ? {} : { x: h - y }),
|
|
28
33
|
...(x === undefined ? {} : { y: x }),
|
|
34
|
+
...(along === undefined ? {} : { along: flips ? 1 - along : along }),
|
|
29
35
|
};
|
|
30
36
|
}
|
|
31
37
|
|