@mark1russell7/packing 0.1.2 → 0.1.4
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 +32 -0
- package/build/index.d.ts +520 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2095 -2
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters.ts +261 -0
- package/src/check.ts +84 -0
- package/src/contract.ts +203 -0
- package/src/convert.ts +51 -0
- package/src/corpus.ts +65 -0
- package/src/frame.ts +262 -0
- package/src/generators.ts +684 -0
- package/src/index.ts +75 -0
- package/src/layout-input.ts +120 -0
- package/src/pack.ts +2 -1
- package/src/registry.ts +23 -0
- package/src/sha256.ts +93 -0
- package/src/strategy.ts +214 -0
package/src/index.ts
CHANGED
|
@@ -7,3 +7,78 @@ export { packFree, aspectError, type FreeWidthOptions, type FreeWidthResult, typ
|
|
|
7
7
|
export { compareRects, SORT_ORDERS, type SortOrder } from "./order.ts";
|
|
8
8
|
export { pushCascade, type PushOptions } from "./push.ts";
|
|
9
9
|
export { packingMetrics, type MetricsOptions, type PackingMetrics } from "./metrics.ts";
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_TOLERANCE,
|
|
12
|
+
EPS,
|
|
13
|
+
RECORD_EPS,
|
|
14
|
+
marginOf,
|
|
15
|
+
turnItem,
|
|
16
|
+
turnPoint,
|
|
17
|
+
turnPort,
|
|
18
|
+
turnRegion,
|
|
19
|
+
type Container,
|
|
20
|
+
type Item,
|
|
21
|
+
type Margin,
|
|
22
|
+
type Obstacle,
|
|
23
|
+
type PackInput,
|
|
24
|
+
type PackOutput,
|
|
25
|
+
type Placement,
|
|
26
|
+
type Port,
|
|
27
|
+
type Region,
|
|
28
|
+
type StageTrace,
|
|
29
|
+
type Tolerance,
|
|
30
|
+
} from "./contract.ts";
|
|
31
|
+
export {
|
|
32
|
+
UnsupportedCell,
|
|
33
|
+
assertSupported,
|
|
34
|
+
cellOf,
|
|
35
|
+
createContext,
|
|
36
|
+
mulberry32,
|
|
37
|
+
supportsCell,
|
|
38
|
+
unsupportedOf,
|
|
39
|
+
type Cell,
|
|
40
|
+
type Dimension,
|
|
41
|
+
type DimensionValue,
|
|
42
|
+
type PackContext,
|
|
43
|
+
type SizeSearch,
|
|
44
|
+
type Strategy,
|
|
45
|
+
type Supports,
|
|
46
|
+
} from "./strategy.ts";
|
|
47
|
+
export { logSweep, push, rows, shelf, skyline, type LayoutStrategyOptions, type PushStrategyOptions } from "./adapters.ts";
|
|
48
|
+
export { fromRects, toRects } from "./convert.ts";
|
|
49
|
+
export { packWith, strategies, strategy } from "./registry.ts";
|
|
50
|
+
export { checkHard, type HardBreaks } from "./check.ts";
|
|
51
|
+
export {
|
|
52
|
+
canonicalJson,
|
|
53
|
+
countCell,
|
|
54
|
+
fnv1a32,
|
|
55
|
+
hashInput,
|
|
56
|
+
round3,
|
|
57
|
+
toPackInput,
|
|
58
|
+
type CorpusCell,
|
|
59
|
+
type CountScale,
|
|
60
|
+
type Edge,
|
|
61
|
+
type GeneratorParams,
|
|
62
|
+
type GeneratorStamp,
|
|
63
|
+
type LayoutInput,
|
|
64
|
+
type ObstacleLayout,
|
|
65
|
+
type SizeDistribution,
|
|
66
|
+
} from "./layout-input.ts";
|
|
67
|
+
export {
|
|
68
|
+
cards,
|
|
69
|
+
dashboard,
|
|
70
|
+
edgeCases,
|
|
71
|
+
gallery,
|
|
72
|
+
generator,
|
|
73
|
+
generators,
|
|
74
|
+
rectGrid,
|
|
75
|
+
rectScattered,
|
|
76
|
+
wordCloud,
|
|
77
|
+
type CorpusGenerator,
|
|
78
|
+
type EdgeCase,
|
|
79
|
+
type EdgeCaseParams,
|
|
80
|
+
type RectGridParams,
|
|
81
|
+
type RectScatteredParams,
|
|
82
|
+
type UiParams,
|
|
83
|
+
} from "./generators.ts";
|
|
84
|
+
export { corpus, type Corpus, type CorpusEntry, type CorpusManifest, type CorpusSpec } from "./corpus.ts";
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// The input file `layout-input/1` of `docs/FORMAT.md`: its types, its numbers, its canonical form and its hash.
|
|
2
|
+
|
|
3
|
+
import type { Container, Item, Obstacle, PackInput } from "./contract.ts";
|
|
4
|
+
import { sha256Hex, utf8 } from "./sha256.ts";
|
|
5
|
+
import type { Cell } from "./strategy.ts";
|
|
6
|
+
|
|
7
|
+
/** An edge between two boxes. The router reads it, and the packer ignores it. */
|
|
8
|
+
export interface Edge {
|
|
9
|
+
id: string;
|
|
10
|
+
source: string;
|
|
11
|
+
target: string;
|
|
12
|
+
sourcePort?: string;
|
|
13
|
+
targetPort?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The parameters of a generator: a name and a string or a number for each parameter. */
|
|
17
|
+
export type GeneratorParams = Record<string, string | number>;
|
|
18
|
+
|
|
19
|
+
/** The stamp of a generated input: the generator, its version, the full parameters and the seed. */
|
|
20
|
+
export interface GeneratorStamp {
|
|
21
|
+
name: string;
|
|
22
|
+
version: number;
|
|
23
|
+
params: GeneratorParams;
|
|
24
|
+
seed: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One input file `layout-input/1`. `docs/FORMAT.md` gives each field. */
|
|
28
|
+
export interface LayoutInput {
|
|
29
|
+
schema: "layout-input/1";
|
|
30
|
+
id: string;
|
|
31
|
+
set: string;
|
|
32
|
+
family: string;
|
|
33
|
+
seed: number;
|
|
34
|
+
/** The origin: `made-up`, `published:<set>` or `known-best:<construction>`. */
|
|
35
|
+
source: string;
|
|
36
|
+
/** The generator of a generated input. A hand-made or published input has `null`. */
|
|
37
|
+
generator: GeneratorStamp | null;
|
|
38
|
+
container: Container;
|
|
39
|
+
obstacles?: Obstacle[];
|
|
40
|
+
gap?: number;
|
|
41
|
+
boxes: Item[];
|
|
42
|
+
edges: Edge[];
|
|
43
|
+
base?: string;
|
|
44
|
+
change?: string;
|
|
45
|
+
step?: number;
|
|
46
|
+
/** The first 16 hex characters of the SHA-256 of the canonical form of the other fields. */
|
|
47
|
+
hash: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The layout of the obstacles (`docs/research/TAXONOMY.md`, dimension 3). */
|
|
51
|
+
export type ObstacleLayout = "none" | "center" | "scattered" | "wall" | "corridors" | "touching" | "edge";
|
|
52
|
+
|
|
53
|
+
/** The distribution of the sizes of the items (`docs/research/TAXONOMY.md`, dimension 6). */
|
|
54
|
+
export type SizeDistribution = "identical" | "nearIdentical" | "uniform" | "heavyTail" | "bimodal" | "extremeAspect";
|
|
55
|
+
|
|
56
|
+
/** The scale of the count of the items (`docs/research/TAXONOMY.md`, dimension 7). */
|
|
57
|
+
export type CountScale = "1" | "10" | "100" | "1k" | "10k";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The cell of a generated input: one value for each dimension of the packer. The cell of `cellOf` has the dimensions
|
|
61
|
+
* that the input tells. The generator adds the layout of the obstacles, the sizes, the count and the mode.
|
|
62
|
+
*/
|
|
63
|
+
export interface CorpusCell extends Cell {
|
|
64
|
+
obstacles: ObstacleLayout;
|
|
65
|
+
sizes: SizeDistribution;
|
|
66
|
+
count: CountScale;
|
|
67
|
+
mode: "offline" | "online" | "incremental";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** This function gives the `fnv1a32` hash of the UTF-8 bytes of a string, as `docs/FORMAT.md` gives it. */
|
|
71
|
+
export function fnv1a32(text: string): number {
|
|
72
|
+
let h = 0x811c9dc5;
|
|
73
|
+
for (const b of utf8(text)) h = Math.imul(h ^ b, 0x01000193) >>> 0;
|
|
74
|
+
return h;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** This function rounds the numbers of a file to 3 decimals. A half goes up, and the value `-0` becomes 0. */
|
|
78
|
+
export function round3(x: number): number {
|
|
79
|
+
const v = Math.round(x * 1000) / 1000;
|
|
80
|
+
return Object.is(v, -0) ? 0 : v;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* This function gives the canonical form of a value: sorted keys, no whitespace and the numbers of `JSON.stringify`
|
|
85
|
+
* after `round3`. A key with the value `undefined` is not in the form.
|
|
86
|
+
*/
|
|
87
|
+
export function canonicalJson(value: unknown): string {
|
|
88
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
89
|
+
if (value !== null && typeof value === "object") {
|
|
90
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
91
|
+
.filter(([, v]) => v !== undefined)
|
|
92
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
93
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
94
|
+
}
|
|
95
|
+
return JSON.stringify(typeof value === "number" ? round3(value) : value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** This function gives the hash of an input file: the first 16 hex characters of the SHA-256 of its canonical form without `hash`. */
|
|
99
|
+
export function hashInput(file: Omit<LayoutInput, "hash"> | LayoutInput): string {
|
|
100
|
+
const rest: Partial<LayoutInput> = { ...file };
|
|
101
|
+
delete rest.hash;
|
|
102
|
+
return sha256Hex(canonicalJson(rest)).slice(0, 16);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** This function gives the scale of a count: the nearest of 1, 10, 100, 1000 and 10000 on a log scale. The count 0 gives `1`. */
|
|
106
|
+
export function countCell(count: number): CountScale {
|
|
107
|
+
if (count <= 3) return "1";
|
|
108
|
+
if (count <= 31) return "10";
|
|
109
|
+
if (count <= 316) return "100";
|
|
110
|
+
if (count <= 3162) return "1k";
|
|
111
|
+
return "10k";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** This function gives the packing input of a file. The boxes become the items, and the edges go. */
|
|
115
|
+
export function toPackInput(file: Pick<LayoutInput, "container" | "boxes" | "obstacles" | "gap">): PackInput {
|
|
116
|
+
const input: PackInput = { container: file.container, items: file.boxes };
|
|
117
|
+
if (file.obstacles !== undefined) input.obstacles = file.obstacles;
|
|
118
|
+
if (file.gap !== undefined) input.gap = file.gap;
|
|
119
|
+
return input;
|
|
120
|
+
}
|
package/src/pack.ts
CHANGED
|
@@ -70,7 +70,8 @@ function packRow<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder): boolean
|
|
|
70
70
|
if (fillGaps && row.length > 1) {
|
|
71
71
|
const rectInRow = row[row.length - 1]!;
|
|
72
72
|
const outerLeft = new Rect<T>();
|
|
73
|
-
|
|
73
|
+
// The position of the region is relative to the outer rectangle, as packInto adds the position of the outer rectangle
|
|
74
|
+
outerLeft.position.set(rectInRow.position.x - outer.position.x, rectInRow.bottom() - outer.position.y);
|
|
74
75
|
outerLeft.size.set(rectInRow.size.x, rowRect.size.y - rectInRow.size.y);
|
|
75
76
|
outerLeft.fixedWidth = true;
|
|
76
77
|
outerLeft.fixedSize = true;
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { push, rows, shelf, skyline } from "./adapters.ts";
|
|
2
|
+
import type { PackInput, PackOutput } from "./contract.ts";
|
|
3
|
+
import { createContext, type Strategy } from "./strategy.ts";
|
|
4
|
+
|
|
5
|
+
/** Each strategy of the package. The benchmark starts each one on each input of the corpus. */
|
|
6
|
+
export const strategies: readonly Strategy[] = Object.freeze([rows, skyline, shelf, push] as Strategy[]);
|
|
7
|
+
|
|
8
|
+
/** This function gives the strategy with an id. It throws a `RangeError` for an id that the registry does not have. */
|
|
9
|
+
export function strategy(id: string): Strategy {
|
|
10
|
+
const found = strategies.find((s) => s.id === id);
|
|
11
|
+
if (found === undefined) throw new RangeError(`The registry has no strategy "${id}".`);
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This function packs an input with the strategy of an id. The options go over the defaults of the strategy. The
|
|
17
|
+
* context gets `mulberry32` with the seed. The default seed is 0.
|
|
18
|
+
*/
|
|
19
|
+
export function packWith(id: string, input: PackInput, options?: unknown, seed = 0): PackOutput {
|
|
20
|
+
const s = strategy(id);
|
|
21
|
+
const merged: unknown = { ...(s.defaults as object), ...(options as object | undefined) };
|
|
22
|
+
return s.pack(input, merged, createContext(seed));
|
|
23
|
+
}
|
package/src/sha256.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// SHA-256 (FIPS 180-4) in plain TypeScript. The hash of a corpus file needs it, and a browser has no synchronous SHA-256.
|
|
2
|
+
|
|
3
|
+
/** The round constants of SHA-256. */
|
|
4
|
+
const K = new Uint32Array([
|
|
5
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be,
|
|
6
|
+
0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa,
|
|
7
|
+
0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85,
|
|
8
|
+
0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
|
9
|
+
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f,
|
|
10
|
+
0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/** The first hash value of SHA-256. */
|
|
14
|
+
const H0 = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
|
15
|
+
|
|
16
|
+
/** This function turns a 32-bit word to the right by `n` bits. */
|
|
17
|
+
const rotr = (x: number, n: number): number => (x >>> n) | (x << (32 - n));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This function gives the UTF-8 bytes of a string. A lone surrogate becomes U+FFFD, as with `TextEncoder`. The
|
|
21
|
+
* package has no `TextEncoder` type, because its code operates on each runtime with no global types.
|
|
22
|
+
*/
|
|
23
|
+
export function utf8(text: string): Uint8Array {
|
|
24
|
+
const out: number[] = [];
|
|
25
|
+
for (let i = 0; i < text.length; i++) {
|
|
26
|
+
let c = text.charCodeAt(i);
|
|
27
|
+
if (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length) {
|
|
28
|
+
const next = text.charCodeAt(i + 1);
|
|
29
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
30
|
+
c = 0x10000 + ((c - 0xd800) << 10) + (next - 0xdc00);
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (c >= 0xd800 && c <= 0xdfff) c = 0xfffd;
|
|
35
|
+
if (c < 0x80) out.push(c);
|
|
36
|
+
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
|
|
37
|
+
else if (c < 0x10000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
38
|
+
else out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
39
|
+
}
|
|
40
|
+
return Uint8Array.from(out);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** This function gives the SHA-256 of the UTF-8 bytes of a string, as 64 lowercase hex characters. */
|
|
44
|
+
export function sha256Hex(text: string): string {
|
|
45
|
+
const bytes = utf8(text);
|
|
46
|
+
const length = bytes.length;
|
|
47
|
+
const padded = new Uint8Array((((length + 9 + 63) / 64) | 0) * 64);
|
|
48
|
+
padded.set(bytes);
|
|
49
|
+
padded[length] = 0x80;
|
|
50
|
+
const view = new DataView(padded.buffer);
|
|
51
|
+
const bits = length * 8;
|
|
52
|
+
view.setUint32(padded.length - 8, Math.floor(bits / 0x100000000));
|
|
53
|
+
view.setUint32(padded.length - 4, bits >>> 0);
|
|
54
|
+
|
|
55
|
+
const hash = [...H0];
|
|
56
|
+
const w = new Uint32Array(64);
|
|
57
|
+
for (let block = 0; block < padded.length; block += 64) {
|
|
58
|
+
for (let t = 0; t < 16; t++) w[t] = view.getUint32(block + t * 4);
|
|
59
|
+
for (let t = 16; t < 64; t++) {
|
|
60
|
+
const a = w[t - 15]!;
|
|
61
|
+
const b = w[t - 2]!;
|
|
62
|
+
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3);
|
|
63
|
+
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10);
|
|
64
|
+
w[t] = (w[t - 16]! + s0 + w[t - 7]! + s1) | 0;
|
|
65
|
+
}
|
|
66
|
+
let [a, b, c, d, e, f, g, h] = hash as [number, number, number, number, number, number, number, number];
|
|
67
|
+
for (let t = 0; t < 64; t++) {
|
|
68
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
69
|
+
const ch = (e & f) ^ (~e & g);
|
|
70
|
+
const t1 = (h + s1 + ch + K[t]! + w[t]!) | 0;
|
|
71
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
72
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
73
|
+
const t2 = (s0 + maj) | 0;
|
|
74
|
+
h = g;
|
|
75
|
+
g = f;
|
|
76
|
+
f = e;
|
|
77
|
+
e = (d + t1) | 0;
|
|
78
|
+
d = c;
|
|
79
|
+
c = b;
|
|
80
|
+
b = a;
|
|
81
|
+
a = (t1 + t2) | 0;
|
|
82
|
+
}
|
|
83
|
+
hash[0] = (hash[0]! + a) | 0;
|
|
84
|
+
hash[1] = (hash[1]! + b) | 0;
|
|
85
|
+
hash[2] = (hash[2]! + c) | 0;
|
|
86
|
+
hash[3] = (hash[3]! + d) | 0;
|
|
87
|
+
hash[4] = (hash[4]! + e) | 0;
|
|
88
|
+
hash[5] = (hash[5]! + f) | 0;
|
|
89
|
+
hash[6] = (hash[6]! + g) | 0;
|
|
90
|
+
hash[7] = (hash[7]! + h) | 0;
|
|
91
|
+
}
|
|
92
|
+
return hash.map((x) => (x >>> 0).toString(16).padStart(8, "0")).join("");
|
|
93
|
+
}
|
package/src/strategy.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { Container, PackInput, PackOutput, Region } from "./contract.ts";
|
|
2
|
+
|
|
3
|
+
/** For each dimension of the taxonomy, the values that a strategy supports. `docs/research/TAXONOMY.md` gives the values. */
|
|
4
|
+
export interface Supports {
|
|
5
|
+
containerSize: ("fixed" | "freeHeight" | "freeWidth" | "freeAspect" | "freeArea")[];
|
|
6
|
+
containerShape: ("rect" | "convex" | "polygon" | "circle" | "ellipse" | "holes")[];
|
|
7
|
+
/** The movement of the obstacles. An empty list supports only an input with no obstacle and no locked item. */
|
|
8
|
+
obstacleMove: ("fixed" | "costly" | "free")[];
|
|
9
|
+
itemShape: ("rect" | "circle" | "convex" | "polygon" | "mixed")[];
|
|
10
|
+
rotation: ("none" | "quarter" | "any")[];
|
|
11
|
+
order: ("free" | "soft" | "hard")[];
|
|
12
|
+
groups: ("none" | "flat" | "nested")[];
|
|
13
|
+
mode: ("offline" | "online" | "incremental")[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A dimension of `Supports`. */
|
|
17
|
+
export type Dimension = keyof Supports;
|
|
18
|
+
|
|
19
|
+
/** A value of a dimension of `Supports`. */
|
|
20
|
+
export type DimensionValue<D extends Dimension = Dimension> = Supports[D][number];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The cell of an input: one value for each packer dimension that the input tells. The input does not tell the mode.
|
|
24
|
+
* Without obstacles and locked items, the cell has no `obstacleMove`.
|
|
25
|
+
*/
|
|
26
|
+
export interface Cell {
|
|
27
|
+
containerSize: DimensionValue<"containerSize">;
|
|
28
|
+
containerShape: DimensionValue<"containerShape">;
|
|
29
|
+
obstacleMove?: DimensionValue<"obstacleMove">;
|
|
30
|
+
itemShape: DimensionValue<"itemShape">;
|
|
31
|
+
rotation: DimensionValue<"rotation">;
|
|
32
|
+
order: DimensionValue<"order">;
|
|
33
|
+
groups: DimensionValue<"groups">;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The context of one packing. */
|
|
37
|
+
export interface PackContext {
|
|
38
|
+
/** The seed. A strategy gets each random number from `rng`, not from `Math.random`. */
|
|
39
|
+
seed: number;
|
|
40
|
+
/** The random numbers in `[0, 1)`, from `mulberry32` with `seed`. */
|
|
41
|
+
rng: () => number;
|
|
42
|
+
/** A limit for the strategies that search. The strategy gives its best result at the limit. */
|
|
43
|
+
budget?: { ms?: number; steps?: number };
|
|
44
|
+
/** The previous output, for the mode `incremental`. */
|
|
45
|
+
previous?: PackOutput;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A packing strategy behind the Packer interface. */
|
|
49
|
+
export interface Strategy<O = unknown> {
|
|
50
|
+
/** A stable name, for example `shelf`. The records use it. */
|
|
51
|
+
id: string;
|
|
52
|
+
/** The family of the catalog, for example `shelf` or `skyline`. */
|
|
53
|
+
family: string;
|
|
54
|
+
/** The ids of the catalog rows that this strategy builds. */
|
|
55
|
+
catalog: string[];
|
|
56
|
+
/** The cells that the strategy supports. */
|
|
57
|
+
supports: Supports;
|
|
58
|
+
/** The default options. A record writes the options in full. */
|
|
59
|
+
defaults: O;
|
|
60
|
+
/** The value `true` tells that the result depends only on the input, the options and the seed. */
|
|
61
|
+
deterministic: boolean;
|
|
62
|
+
/** This method packs an input. It throws `UnsupportedCell` for an input outside the cells of `supports`. */
|
|
63
|
+
pack(input: PackInput, options: O, context: PackContext): PackOutput;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A size search: it gives a strategy of a fixed width the cells `freeAspect` and `freeArea`. */
|
|
67
|
+
export interface SizeSearch {
|
|
68
|
+
id: string;
|
|
69
|
+
/** This method searches the size of the container for an input with `fixed` set to `none`. `options` go to `inner`. */
|
|
70
|
+
search(input: PackInput, inner: Strategy, context: PackContext, options?: unknown): PackOutput;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The error of a strategy for an input outside its cells. The benchmark records this result. It is not a failure of the
|
|
75
|
+
* strategy.
|
|
76
|
+
*/
|
|
77
|
+
export class UnsupportedCell extends Error {
|
|
78
|
+
/** The dimension of the taxonomy. */
|
|
79
|
+
readonly dimension: Dimension;
|
|
80
|
+
/** The value of the input that the strategy does not support. */
|
|
81
|
+
readonly value: string;
|
|
82
|
+
|
|
83
|
+
constructor(dimension: Dimension, value: string) {
|
|
84
|
+
super(`The strategy does not support the value "${value}" of the dimension "${dimension}".`);
|
|
85
|
+
this.name = "UnsupportedCell";
|
|
86
|
+
this.dimension = dimension;
|
|
87
|
+
this.value = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** This function gives a generator of random numbers in `[0, 1)`: `mulberry32` of `docs/FORMAT.md`. */
|
|
92
|
+
export function mulberry32(seed: number): () => number {
|
|
93
|
+
let s = seed >>> 0;
|
|
94
|
+
return () => {
|
|
95
|
+
s = (s + 0x6d2b79f5) | 0;
|
|
96
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
97
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
98
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** This function makes the context of one packing from a seed. */
|
|
103
|
+
export function createContext(seed = 0, more: Omit<PackContext, "seed" | "rng"> = {}): PackContext {
|
|
104
|
+
return { ...more, seed, rng: mulberry32(seed) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** This function tells if a polygon ring is convex. A ring with fewer than 3 points is convex. */
|
|
108
|
+
function isConvex(points: readonly [number, number][]): boolean {
|
|
109
|
+
let sign = 0;
|
|
110
|
+
const n = points.length;
|
|
111
|
+
for (let i = 0; i < n; i++) {
|
|
112
|
+
const [ax, ay] = points[i]!;
|
|
113
|
+
const [bx, by] = points[(i + 1) % n]!;
|
|
114
|
+
const [cx, cy] = points[(i + 2) % n]!;
|
|
115
|
+
const cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx);
|
|
116
|
+
if (cross === 0) continue;
|
|
117
|
+
if (sign === 0) sign = Math.sign(cross);
|
|
118
|
+
else if (Math.sign(cross) !== sign) return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** This function gives the value of `containerShape` of one region. */
|
|
124
|
+
function shapeOf(region: Region): DimensionValue<"containerShape"> {
|
|
125
|
+
switch (region.kind) {
|
|
126
|
+
case "rect":
|
|
127
|
+
case "circle":
|
|
128
|
+
case "ellipse":
|
|
129
|
+
return region.kind;
|
|
130
|
+
case "polygon":
|
|
131
|
+
if (region.holes && region.holes.length > 0) return "holes";
|
|
132
|
+
return isConvex(region.points) ? "convex" : "polygon";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** This function gives the value of `itemShape` of one item shape. An ellipse is convex. */
|
|
137
|
+
function itemShapeOf(region: Region | undefined): DimensionValue<"itemShape"> {
|
|
138
|
+
if (region === undefined) return "rect";
|
|
139
|
+
switch (region.kind) {
|
|
140
|
+
case "rect":
|
|
141
|
+
case "circle":
|
|
142
|
+
return region.kind;
|
|
143
|
+
case "ellipse":
|
|
144
|
+
return "convex";
|
|
145
|
+
case "polygon":
|
|
146
|
+
return !region.holes?.length && isConvex(region.points) ? "convex" : "polygon";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* This function gives the cell of an input: the values of the packer dimensions that the input tells.
|
|
152
|
+
*
|
|
153
|
+
* - `containerSize` comes from `fixed`. The value `none` gives `freeArea` when `aspectWeight` is 0, and `freeAspect` if not.
|
|
154
|
+
* - `containerShape` comes from the shape. A rectangle container with an obstacle of another region gives `holes`.
|
|
155
|
+
* - `obstacleMove` is the hardest value of the obstacles and the locked items: `costly`, then `free`, then `fixed`.
|
|
156
|
+
* - `order` is `soft` when an item has a rank, and `free` if not. The input does not tell a hard order.
|
|
157
|
+
* - `groups` is `nested` when an item has a parent, `flat` when an item has a group, and `none` if not.
|
|
158
|
+
*/
|
|
159
|
+
export function cellOf(input: PackInput): Cell {
|
|
160
|
+
const { container, items } = input;
|
|
161
|
+
const obstacles = input.obstacles ?? [];
|
|
162
|
+
|
|
163
|
+
const sizes: Record<Container["fixed"], DimensionValue<"containerSize">> = {
|
|
164
|
+
size: "fixed",
|
|
165
|
+
width: "freeHeight",
|
|
166
|
+
height: "freeWidth",
|
|
167
|
+
none: container.aspectWeight === 0 ? "freeArea" : "freeAspect",
|
|
168
|
+
};
|
|
169
|
+
const containerSize = sizes[container.fixed];
|
|
170
|
+
|
|
171
|
+
let containerShape = container.shape ? shapeOf(container.shape) : "rect";
|
|
172
|
+
if (containerShape === "rect" && obstacles.some((o) => o.region.kind !== "rect")) containerShape = "holes";
|
|
173
|
+
|
|
174
|
+
let obstacleMove: DimensionValue<"obstacleMove"> | undefined = items.some((i) => i.locked === true) ? "fixed" : undefined;
|
|
175
|
+
for (const o of obstacles) {
|
|
176
|
+
const move = o.move === undefined ? "fixed" : o.move.cost > 0 ? "costly" : "free";
|
|
177
|
+
if (move === "costly" || obstacleMove === undefined || (move === "free" && obstacleMove === "fixed")) obstacleMove = move;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const shapes = new Set(items.map((i) => itemShapeOf(i.shape)));
|
|
181
|
+
const itemShape = shapes.size > 1 ? "mixed" : ([...shapes][0] ?? "rect");
|
|
182
|
+
|
|
183
|
+
const cell: Cell = {
|
|
184
|
+
containerSize,
|
|
185
|
+
containerShape,
|
|
186
|
+
itemShape,
|
|
187
|
+
rotation: items.some((i) => i.turnable === true) ? "quarter" : "none",
|
|
188
|
+
order: items.some((i) => i.rank !== undefined) ? "soft" : "free",
|
|
189
|
+
groups: items.some((i) => i.parent !== undefined) ? "nested" : items.some((i) => i.group !== undefined) ? "flat" : "none",
|
|
190
|
+
};
|
|
191
|
+
if (obstacleMove !== undefined) cell.obstacleMove = obstacleMove;
|
|
192
|
+
return cell;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** This function gives the first dimension of a cell that `supports` does not hold, or `undefined` when it holds each one. */
|
|
196
|
+
export function unsupportedOf(supports: Supports, cell: Partial<Record<Dimension, string>>): { dimension: Dimension; value: string } | undefined {
|
|
197
|
+
for (const dimension of Object.keys(supports) as Dimension[]) {
|
|
198
|
+
const value = cell[dimension];
|
|
199
|
+
if (value === undefined) continue;
|
|
200
|
+
if (!(supports[dimension] as readonly string[]).includes(value)) return { dimension, value };
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** This function tells if `supports` holds each value of a cell. A dimension that the cell does not have is held. */
|
|
206
|
+
export function supportsCell(supports: Supports, cell: Partial<Record<Dimension, string>>): boolean {
|
|
207
|
+
return unsupportedOf(supports, cell) === undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** This function throws `UnsupportedCell` for the first value of the cell of an input that `supports` does not hold. */
|
|
211
|
+
export function assertSupported(supports: Supports, input: PackInput): void {
|
|
212
|
+
const miss = unsupportedOf(supports, cellOf(input));
|
|
213
|
+
if (miss) throw new UnsupportedCell(miss.dimension, miss.value);
|
|
214
|
+
}
|