@mark1russell7/packing 0.1.7 → 0.2.1
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 +1 -1
- package/build/index.d.ts +141 -233
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1906 -1496
- package/build/index.js.map +1 -1
- package/package.json +5 -2
- package/src/adapters.ts +30 -21
- package/src/check.ts +34 -27
- package/src/contract.ts +30 -192
- package/src/convert.ts +11 -15
- package/src/corpus.ts +3 -3
- package/src/frame.ts +37 -50
- package/src/generators.ts +11 -12
- package/src/index.ts +9 -4
- package/src/layout-input.ts +34 -43
- package/src/measures.ts +12 -15
- package/src/pack.ts +12 -1
- package/src/shelf.ts +107 -0
- package/src/skyline.ts +125 -2
- package/src/space.ts +379 -0
- package/src/strategy.ts +3 -3
package/src/frame.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Each item grows by its margin and by half the gap on each side. Each obstacle grows by half the gap. Thus two items
|
|
3
3
|
// are `mA + gap + mB` apart. The area of the frame is the container less the inset, grown by half the gap on each side.
|
|
4
4
|
|
|
5
|
+
import { boundsOf } from "@mark1russell7/layout-contract";
|
|
5
6
|
import { EPS, marginOf, type Item, type Obstacle, type PackInput, type PackOutput, type Placement, type Region, type StageTrace } from "./contract.ts";
|
|
6
7
|
import { Rect } from "./rect.ts";
|
|
7
8
|
|
|
@@ -12,24 +13,8 @@ export function now(): number {
|
|
|
12
13
|
return clock ? clock.now() : Date.now();
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
export
|
|
17
|
-
switch (region.kind) {
|
|
18
|
-
case "rect":
|
|
19
|
-
return { x: region.x, y: region.y, w: region.w, h: region.h };
|
|
20
|
-
case "circle":
|
|
21
|
-
return { x: region.cx - region.r, y: region.cy - region.r, w: 2 * region.r, h: 2 * region.r };
|
|
22
|
-
case "ellipse":
|
|
23
|
-
return { x: region.cx - region.rx, y: region.cy - region.ry, w: 2 * region.rx, h: 2 * region.ry };
|
|
24
|
-
case "polygon": {
|
|
25
|
-
const xs = region.points.map((p) => p[0]);
|
|
26
|
-
const ys = region.points.map((p) => p[1]);
|
|
27
|
-
const x = Math.min(...xs);
|
|
28
|
-
const y = Math.min(...ys);
|
|
29
|
-
return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
16
|
+
/** The bounding box of a region comes from the contract. */
|
|
17
|
+
export { boundsOf } from "@mark1russell7/layout-contract";
|
|
33
18
|
|
|
34
19
|
/** The rectangle of a container: its origin and its size. A free side has the size `Infinity`. */
|
|
35
20
|
export interface Bounds {
|
|
@@ -147,7 +132,7 @@ export function rectsOf(frame: Frame): Rect<number>[] {
|
|
|
147
132
|
* This function reads the output from the rectangles of a frame after a packing of 0.1. A locked item keeps its
|
|
148
133
|
* position exactly. Each other item that the packer did not place goes to the overflow.
|
|
149
134
|
*/
|
|
150
|
-
export function readFrame(input: PackInput, frame: Frame, trace: StageTrace[], moved: PackOutput["moved"] = []): PackOutput {
|
|
135
|
+
export function readFrame(input: PackInput, frame: Frame, trace: readonly StageTrace[], moved: PackOutput["moved"] = []): PackOutput {
|
|
151
136
|
const placements: Placement[] = [];
|
|
152
137
|
const overflow: string[] = [...frame.tooLarge];
|
|
153
138
|
for (const e of frame.entries) {
|
|
@@ -197,44 +182,48 @@ export function transposeRegion(region: Region): Region {
|
|
|
197
182
|
case "ellipse":
|
|
198
183
|
return { kind: "ellipse", cx: region.cy, cy: region.cx, rx: region.ry, ry: region.rx };
|
|
199
184
|
case "polygon": {
|
|
200
|
-
const swap = (ring: [number, number][]): [number, number][] => ring.map(([x, y]) => [y, x]);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
185
|
+
const swap = (ring: readonly (readonly [number, number])[]): [number, number][] => ring.map(([x, y]) => [y, x]);
|
|
186
|
+
return region.holes === undefined
|
|
187
|
+
? { kind: "polygon", points: swap(region.points) }
|
|
188
|
+
: { kind: "polygon", points: swap(region.points), holes: region.holes.map(swap) };
|
|
204
189
|
}
|
|
205
190
|
}
|
|
206
191
|
}
|
|
207
192
|
|
|
208
193
|
/** This function gives an item with the axes changed. The ports go, because the adapters of 0.1 do not read them. */
|
|
209
194
|
function transposeItem(item: Item): Item {
|
|
210
|
-
const { ports: _ports, ...rest } = item;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
195
|
+
const { ports: _ports, x, y, shape, margin, ...rest } = item;
|
|
196
|
+
return {
|
|
197
|
+
...rest,
|
|
198
|
+
w: item.h,
|
|
199
|
+
h: item.w,
|
|
200
|
+
...(y === undefined ? {} : { x: y }),
|
|
201
|
+
...(x === undefined ? {} : { y: x }),
|
|
202
|
+
...(shape === undefined ? {} : { shape: transposeRegion(shape) }),
|
|
203
|
+
...(margin === undefined
|
|
204
|
+
? {}
|
|
205
|
+
: { margin: typeof margin === "number" ? margin : { top: margin.left, right: margin.bottom, bottom: margin.right, left: margin.top } }),
|
|
206
|
+
};
|
|
222
207
|
}
|
|
223
208
|
|
|
224
209
|
/** This function gives an input with the axes changed. A fixed height becomes a fixed width. */
|
|
225
210
|
export function transposeInput(input: PackInput): PackInput {
|
|
226
211
|
const c = input.container;
|
|
227
|
-
const container: PackInput["container"] = {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
212
|
+
const container: PackInput["container"] = {
|
|
213
|
+
fixed: c.fixed === "height" ? "width" : c.fixed === "width" ? "height" : c.fixed,
|
|
214
|
+
...(c.h === undefined ? {} : { w: c.h }),
|
|
215
|
+
...(c.w === undefined ? {} : { h: c.w }),
|
|
216
|
+
...(c.aspect === undefined ? {} : { aspect: 1 / c.aspect }),
|
|
217
|
+
...(c.aspectWeight === undefined ? {} : { aspectWeight: c.aspectWeight }),
|
|
218
|
+
...(c.shape === undefined ? {} : { shape: transposeRegion(c.shape) }),
|
|
219
|
+
...(c.inset === undefined ? {} : { inset: c.inset }),
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
container,
|
|
223
|
+
items: input.items.map(transposeItem),
|
|
224
|
+
...(input.obstacles === undefined ? {} : { obstacles: input.obstacles.map((o) => ({ ...o, region: transposeRegion(o.region) })) }),
|
|
225
|
+
...(input.gap === undefined ? {} : { gap: input.gap }),
|
|
226
|
+
};
|
|
238
227
|
}
|
|
239
228
|
|
|
240
229
|
/** This function gives an output with the axes changed. */
|
|
@@ -244,7 +233,7 @@ export function transposeOutput(output: PackOutput): PackOutput {
|
|
|
244
233
|
overflow: [...output.overflow],
|
|
245
234
|
size: { w: output.size.h, h: output.size.w },
|
|
246
235
|
moved: output.moved.map((m) => ({ id: m.id, x: m.y, y: m.x })),
|
|
247
|
-
trace: output.trace,
|
|
236
|
+
...(output.trace === undefined ? {} : { trace: output.trace }),
|
|
248
237
|
};
|
|
249
238
|
}
|
|
250
239
|
|
|
@@ -256,7 +245,5 @@ export function viaAxes(input: PackInput, previous: PackOutput | undefined, run:
|
|
|
256
245
|
const start = now();
|
|
257
246
|
const turned = transposeInput(input);
|
|
258
247
|
const inner = run(turned, previous === undefined ? undefined : transposeOutput(previous));
|
|
259
|
-
|
|
260
|
-
output.trace = [{ id: "axes", ms: now() - start, measures: {} }, ...inner.trace];
|
|
261
|
-
return output;
|
|
248
|
+
return { ...transposeOutput(inner), trace: [{ id: "axes", ms: now() - start, measures: {} }, ...(inner.trace ?? [])] };
|
|
262
249
|
}
|
package/src/generators.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// container, then the obstacles, then the boxes. Each step has its own random numbers: `mulberry32` of
|
|
3
3
|
// `fnv1a32("<seed>:<step>")`. Thus a packing input and a routing input with the same seed get the same obstacles.
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { Item, LayoutContainer, Obstacle, Region } from "./contract.ts";
|
|
6
6
|
import {
|
|
7
7
|
countCell,
|
|
8
8
|
fnv1a32,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
round3,
|
|
11
11
|
type CorpusCell,
|
|
12
12
|
type GeneratorParams,
|
|
13
|
-
type
|
|
13
|
+
type GeneratedInput,
|
|
14
14
|
type ObstacleLayout,
|
|
15
15
|
type SizeDistribution,
|
|
16
16
|
} from "./layout-input.ts";
|
|
@@ -36,11 +36,11 @@ export interface CorpusGenerator<P extends GeneratorParams = GeneratorParams> {
|
|
|
36
36
|
/** This method gives the cell of each input of the parameters. The cell does not depend on the seed. */
|
|
37
37
|
cell(params?: Partial<P>): CorpusCell;
|
|
38
38
|
/** This method gives the input file of a seed. The `id` is `<set>/<family>/s<seed>`. */
|
|
39
|
-
generate(seed: number, params?: Partial<P>, set?: string):
|
|
39
|
+
generate(seed: number, params?: Partial<P>, set?: string): GeneratedInput;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/** The parts of a file that a generator makes. */
|
|
43
|
-
type Parts = Pick<
|
|
43
|
+
type Parts = Pick<GeneratedInput, "container" | "boxes"> & { obstacles: Obstacle[] };
|
|
44
44
|
|
|
45
45
|
/** The data of a generator, before `defineGenerator` adds the common steps. */
|
|
46
46
|
interface GeneratorSpec<P extends GeneratorParams> {
|
|
@@ -153,12 +153,12 @@ function defineGenerator<P extends GeneratorParams>(spec: GeneratorSpec<P>): Cor
|
|
|
153
153
|
values: spec.values,
|
|
154
154
|
withDefaults,
|
|
155
155
|
cell: (params?: Partial<P>): CorpusCell => spec.cell(withDefaults(params)),
|
|
156
|
-
generate(seed: number, params?: Partial<P>, set: string = spec.set):
|
|
156
|
+
generate(seed: number, params?: Partial<P>, set: string = spec.set): GeneratedInput {
|
|
157
157
|
if (!Number.isInteger(seed) || seed < 0) throw new RangeError(`The seed is a whole number from 0. It is ${seed}.`);
|
|
158
158
|
const full = withDefaults(params);
|
|
159
159
|
const family = spec.family(full);
|
|
160
160
|
const { container, obstacles, boxes } = spec.build(seed, full);
|
|
161
|
-
const file: Omit<
|
|
161
|
+
const file: Omit<GeneratedInput, "hash"> = roundNumbers({
|
|
162
162
|
schema: "layout-input/1" as const,
|
|
163
163
|
id: `${set}/${family}/s${seed}`,
|
|
164
164
|
set,
|
|
@@ -195,7 +195,7 @@ export const rectScattered: CorpusGenerator<RectScatteredParams> = defineGenerat
|
|
|
195
195
|
let r = stepRandom(seed, "container");
|
|
196
196
|
const w = int(r, 600, 1000);
|
|
197
197
|
const h = int(r, 400, 800);
|
|
198
|
-
const container:
|
|
198
|
+
const container: LayoutContainer = { w, h, fixed: "size" };
|
|
199
199
|
|
|
200
200
|
r = stepRandom(seed, "obstacles");
|
|
201
201
|
const n = int(r, 3, 8);
|
|
@@ -232,8 +232,8 @@ const OBSTACLE_LAYOUTS = ["none", "center", "scattered", "wall", "corridors", "t
|
|
|
232
232
|
const SIZE_DISTRIBUTIONS = ["identical", "nearIdentical", "uniform", "heavyTail", "bimodal", "extremeAspect"] as const;
|
|
233
233
|
|
|
234
234
|
/** This function gives the container of a size value for the nominal rectangle `w` by `h`, with an optional shape. */
|
|
235
|
-
function containerOf(size: DimensionValue<"containerSize">, w: number, h: number, shape?: Region):
|
|
236
|
-
const containers: Record<DimensionValue<"containerSize">,
|
|
235
|
+
function containerOf(size: DimensionValue<"containerSize">, w: number, h: number, shape?: Region): LayoutContainer {
|
|
236
|
+
const containers: Record<DimensionValue<"containerSize">, LayoutContainer> = {
|
|
237
237
|
fixed: { w, h, fixed: "size" },
|
|
238
238
|
freeHeight: { w, fixed: "width" },
|
|
239
239
|
freeWidth: { h, fixed: "height" },
|
|
@@ -241,8 +241,7 @@ function containerOf(size: DimensionValue<"containerSize">, w: number, h: number
|
|
|
241
241
|
freeArea: { fixed: "none", aspectWeight: 0 },
|
|
242
242
|
};
|
|
243
243
|
const container = containers[size];
|
|
244
|
-
|
|
245
|
-
return container;
|
|
244
|
+
return shape === undefined ? container : { ...container, shape };
|
|
246
245
|
}
|
|
247
246
|
|
|
248
247
|
/** This function gives the 4 corners of a rectangle, clockwise from the top left. */
|
|
@@ -625,7 +624,7 @@ export const edgeCases: CorpusGenerator<EdgeCaseParams> = defineGenerator<EdgeCa
|
|
|
625
624
|
w = int(r, 600, 1000);
|
|
626
625
|
h = int(r, 400, 800);
|
|
627
626
|
}
|
|
628
|
-
const container:
|
|
627
|
+
const container: LayoutContainer = { w, h, fixed: "size" };
|
|
629
628
|
|
|
630
629
|
r = stepRandom(seed, "obstacles");
|
|
631
630
|
const obstacles: Obstacle[] = [];
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { Vector } from "./vector.ts";
|
|
2
2
|
export { Rect } from "./rect.ts";
|
|
3
3
|
export { pack, packRows, type PackOptions, type PackLayoutOptions } from "./pack.ts";
|
|
4
|
-
export { packSkyline, lowestWindow } from "./skyline.ts";
|
|
4
|
+
export { packSkyline, lowestWindow, type SkylineOptions } from "./skyline.ts";
|
|
5
|
+
export { freeSpace, type FreeSpace, type FreeSpan, type SpaceRect } from "./space.ts";
|
|
5
6
|
export { packShelf, type ShelfOptions } from "./shelf.ts";
|
|
6
7
|
export { packFree, aspectError, type FreeWidthOptions, type FreeWidthResult, type FreeWidthTry } from "./free.ts";
|
|
7
8
|
export { compareRects, SORT_ORDERS, type SortOrder } from "./order.ts";
|
|
@@ -29,13 +30,14 @@ export {
|
|
|
29
30
|
DEFAULT_TOLERANCE,
|
|
30
31
|
EPS,
|
|
31
32
|
RECORD_EPS,
|
|
33
|
+
checkInput,
|
|
32
34
|
marginOf,
|
|
33
35
|
turnItem,
|
|
34
36
|
turnPoint,
|
|
35
37
|
turnPort,
|
|
36
38
|
turnRegion,
|
|
37
|
-
type Container,
|
|
38
39
|
type Item,
|
|
40
|
+
type LayoutContainer,
|
|
39
41
|
type Margin,
|
|
40
42
|
type Obstacle,
|
|
41
43
|
type PackInput,
|
|
@@ -43,6 +45,8 @@ export {
|
|
|
43
45
|
type Placement,
|
|
44
46
|
type Port,
|
|
45
47
|
type Region,
|
|
48
|
+
type Side,
|
|
49
|
+
type Sides,
|
|
46
50
|
type StageTrace,
|
|
47
51
|
type Tolerance,
|
|
48
52
|
} from "./contract.ts";
|
|
@@ -65,7 +69,7 @@ export {
|
|
|
65
69
|
export { logSweep, push, rows, shelf, skyline, type LayoutStrategyOptions, type PushStrategyOptions } from "./adapters.ts";
|
|
66
70
|
export { fromRects, toRects } from "./convert.ts";
|
|
67
71
|
export { packWith, strategies, strategy } from "./registry.ts";
|
|
68
|
-
export { checkHard, type
|
|
72
|
+
export { checkHard, type PackHard } from "./check.ts";
|
|
69
73
|
export {
|
|
70
74
|
canonicalJson,
|
|
71
75
|
countCell,
|
|
@@ -75,9 +79,10 @@ export {
|
|
|
75
79
|
toPackInput,
|
|
76
80
|
type CorpusCell,
|
|
77
81
|
type CountScale,
|
|
78
|
-
type
|
|
82
|
+
type GeneratedInput,
|
|
79
83
|
type GeneratorParams,
|
|
80
84
|
type GeneratorStamp,
|
|
85
|
+
type LayoutEdge,
|
|
81
86
|
type LayoutInput,
|
|
82
87
|
type ObstacleLayout,
|
|
83
88
|
type SizeDistribution,
|
package/src/layout-input.ts
CHANGED
|
@@ -1,52 +1,39 @@
|
|
|
1
1
|
// The input file `layout-input/1` of `docs/FORMAT.md`: its types, its numbers, its canonical form and its hash.
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { LayoutContainer, LayoutEdge, LayoutGenerator, LayoutInput } from "@mark1russell7/layout-contract";
|
|
4
|
+
import type { PackInput } from "./contract.ts";
|
|
4
5
|
import { sha256Hex, utf8 } from "./sha256.ts";
|
|
5
6
|
import type { Cell } from "./strategy.ts";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
export interface Edge {
|
|
9
|
-
id: string;
|
|
10
|
-
source: string;
|
|
11
|
-
target: string;
|
|
12
|
-
sourcePort?: string;
|
|
13
|
-
targetPort?: string;
|
|
14
|
-
/** The free data of the edge. The packer does not read it. */
|
|
15
|
-
data?: Readonly<Record<string, unknown>>;
|
|
16
|
-
}
|
|
8
|
+
export type { LayoutEdge, LayoutInput } from "@mark1russell7/layout-contract";
|
|
17
9
|
|
|
18
10
|
/** The parameters of a generator: a name and a string or a number for each parameter. */
|
|
19
11
|
export type GeneratorParams = Record<string, string | number>;
|
|
20
12
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The stamp of a generated input: the generator, its version, the full parameters and the seed.
|
|
15
|
+
* It is `LayoutGenerator` of the contract with a string or a number for each parameter.
|
|
16
|
+
*/
|
|
17
|
+
export interface GeneratorStamp extends LayoutGenerator {
|
|
18
|
+
readonly params: Readonly<GeneratorParams>;
|
|
27
19
|
}
|
|
28
20
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
/**
|
|
22
|
+
* A generated input file `layout-input/1`. It is a `LayoutInput` with each field that a generator writes.
|
|
23
|
+
* `docs/FORMAT.md` gives each field.
|
|
24
|
+
*/
|
|
25
|
+
export interface GeneratedInput extends LayoutInput {
|
|
26
|
+
readonly set: string;
|
|
27
|
+
readonly family: string;
|
|
28
|
+
readonly seed: number;
|
|
36
29
|
/** The origin: `made-up`, `published:<set>` or `known-best:<construction>`. */
|
|
37
|
-
source: string;
|
|
30
|
+
readonly source: string;
|
|
38
31
|
/** The generator of a generated input. A hand-made or published input has `null`. */
|
|
39
|
-
generator: GeneratorStamp | null;
|
|
40
|
-
container:
|
|
41
|
-
|
|
42
|
-
gap?: number;
|
|
43
|
-
boxes: Item[];
|
|
44
|
-
edges: Edge[];
|
|
45
|
-
base?: string;
|
|
46
|
-
change?: string;
|
|
47
|
-
step?: number;
|
|
32
|
+
readonly generator: GeneratorStamp | null;
|
|
33
|
+
readonly container: LayoutContainer;
|
|
34
|
+
readonly edges: readonly LayoutEdge[];
|
|
48
35
|
/** The first 16 hex characters of the SHA-256 of the canonical form of the other fields. */
|
|
49
|
-
hash: string;
|
|
36
|
+
readonly hash: string;
|
|
50
37
|
}
|
|
51
38
|
|
|
52
39
|
/** The layout of the obstacles (`docs/research/TAXONOMY.md`, dimension 3). */
|
|
@@ -98,9 +85,8 @@ export function canonicalJson(value: unknown): string {
|
|
|
98
85
|
}
|
|
99
86
|
|
|
100
87
|
/** This function gives the hash of an input file: the first 16 hex characters of the SHA-256 of its canonical form without `hash`. */
|
|
101
|
-
export function hashInput(file:
|
|
102
|
-
const
|
|
103
|
-
delete rest.hash;
|
|
88
|
+
export function hashInput(file: LayoutInput): string {
|
|
89
|
+
const { hash: _hash, ...rest } = file;
|
|
104
90
|
return sha256Hex(canonicalJson(rest)).slice(0, 16);
|
|
105
91
|
}
|
|
106
92
|
|
|
@@ -113,10 +99,15 @@ export function countCell(count: number): CountScale {
|
|
|
113
99
|
return "10k";
|
|
114
100
|
}
|
|
115
101
|
|
|
116
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* This function gives the packing input of a file. The boxes become the items, and the edges go.
|
|
104
|
+
* A file without a container gets `{ fixed: "none" }`, so the packer picks the size. It is `toPackInput` of the contract.
|
|
105
|
+
*/
|
|
117
106
|
export function toPackInput(file: Pick<LayoutInput, "container" | "boxes" | "obstacles" | "gap">): PackInput {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
return {
|
|
108
|
+
container: file.container ?? { fixed: "none" },
|
|
109
|
+
items: file.boxes,
|
|
110
|
+
...(file.obstacles === undefined ? {} : { obstacles: file.obstacles }),
|
|
111
|
+
...(file.gap === undefined ? {} : { gap: file.gap }),
|
|
112
|
+
};
|
|
122
113
|
}
|
package/src/measures.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// The algebra of measures (roadmap item k8). `docs/MEASURES.md` defines each measure. This file computes them.
|
|
2
2
|
|
|
3
|
+
import type { MeasureSpec as ContractMeasureSpec, Direction } from "@mark1russell7/layout-contract";
|
|
3
4
|
import { RECORD_EPS, type PackInput, type PackOutput } from "./contract.ts";
|
|
4
5
|
import { aspectError } from "./free.ts";
|
|
5
6
|
import { round3 } from "./layout-input.ts";
|
|
6
7
|
|
|
7
|
-
/** The direction of a measure: `lower` when a lower value is better, `higher` when a higher value is better. */
|
|
8
|
-
export type Direction
|
|
8
|
+
/** The direction of a measure comes from the contract: `lower` when a lower value is better, `higher` when a higher value is better. */
|
|
9
|
+
export type { Direction } from "@mark1russell7/layout-contract";
|
|
9
10
|
|
|
10
11
|
/** The family of a measure. Each family has a section in `docs/MEASURES.md`. */
|
|
11
12
|
export type MeasureFamily = "density" | "aspect" | "order" | "raggedness" | "overflow" | "stability" | "determinism" | "speed";
|
|
@@ -13,21 +14,17 @@ export type MeasureFamily = "density" | "aspect" | "order" | "raggedness" | "ove
|
|
|
13
14
|
/** The id of a measure of the packer. The ids are those of `docs/FORMAT.md`, and `highways` uses the same ids. */
|
|
14
15
|
export type MeasureId = "pack.area" | "pack.fill" | "pack.whitespace" | "pack.aspect" | "pack.aspectError" | "pack.order.inversions" | "pack.order.breaks" | "pack.raggedness" | "pack.overflow" | "pack.stability.moved" | "pack.stability.mean" | "pack.stability.q95" | "pack.deterministic" | "pack.search.widths" | "pack.search.ms";
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/** The lowest value. */
|
|
23
|
-
min: number;
|
|
24
|
-
/** The highest value. The value `Infinity` tells that the measure has no upper limit. */
|
|
25
|
-
max: number;
|
|
17
|
+
/**
|
|
18
|
+
* The facts of one measure: its family, its unit, its direction and its range.
|
|
19
|
+
* It is `MeasureSpec` of the contract for a packer measure id, with a family.
|
|
20
|
+
*/
|
|
21
|
+
export interface MeasureSpec extends ContractMeasureSpec<MeasureId> {
|
|
22
|
+
readonly family: MeasureFamily;
|
|
26
23
|
/**
|
|
27
24
|
* The floor is the best value of a guard measure. The floor rule of `docs/PROMOTION.md` of `highways` reads it. Each
|
|
28
25
|
* guard of the packer has the floor 0. A measure without a floor has no floor rule.
|
|
29
26
|
*/
|
|
30
|
-
floor?: number;
|
|
27
|
+
readonly floor?: number;
|
|
31
28
|
/**
|
|
32
29
|
* The tolerance is the absolute tolerance at the floor. A candidate within the tolerance ties with a default at the floor.
|
|
33
30
|
*
|
|
@@ -36,7 +33,7 @@ export interface MeasureSpec {
|
|
|
36
33
|
* - `pack.stability.mean` has 0.005 container widths, and `pack.stability.q95` has 0.01. The default often moves no item.
|
|
37
34
|
* - `pack.stability.moved` has 0.02: 2% of the items that stay. On a small change, the default often moves no item.
|
|
38
35
|
*/
|
|
39
|
-
tol?: number;
|
|
36
|
+
readonly tol?: number;
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
/** The measures of one layout, by id. `measure` gives them. The values are not rounded. */
|
|
@@ -175,7 +172,7 @@ export function readsBefore(a: Pick<Box, "x" | "y" | "h">, b: Pick<Box, "x" | "y
|
|
|
175
172
|
/** This function sums one measure over the stages of a trace. A trace without the measure gives 0. */
|
|
176
173
|
function traceSum(output: PackOutput, id: string): number {
|
|
177
174
|
let sum = 0;
|
|
178
|
-
for (const stage of output.trace) sum += stage.measures[id] ?? 0;
|
|
175
|
+
for (const stage of output.trace ?? []) sum += stage.measures?.[id] ?? 0;
|
|
179
176
|
return sum;
|
|
180
177
|
}
|
|
181
178
|
|
package/src/pack.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { compareRects, type SortOrder } from "./order.ts";
|
|
2
2
|
import { Rect } from "./rect.ts";
|
|
3
|
-
import { packShelf } from "./shelf.ts";
|
|
3
|
+
import { packRowsInSpace, packShelf } from "./shelf.ts";
|
|
4
4
|
import { packSkyline } from "./skyline.ts";
|
|
5
|
+
import type { FreeSpace } from "./space.ts";
|
|
5
6
|
import { Vector } from "./vector.ts";
|
|
6
7
|
|
|
7
8
|
/** The options of `pack` and `packRows` */
|
|
8
9
|
export interface PackOptions {
|
|
9
10
|
/** The order of the rectangles before the packing. The default is `height-desc`. */
|
|
10
11
|
sortOrder?: SortOrder;
|
|
12
|
+
/**
|
|
13
|
+
* The free space of the container: `freeSpace` of the input. With a space that is not plain, each strategy packs into
|
|
14
|
+
* the outline of the container and around its holes. The rows of `packRows` also go around the locked rectangles.
|
|
15
|
+
*/
|
|
16
|
+
space?: FreeSpace;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
/** The options of `pack` */
|
|
@@ -85,9 +91,14 @@ function packRow<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder): boolean
|
|
|
85
91
|
* This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does
|
|
86
92
|
* not change the width. Without `fixedSize`, the height grows until all rectangles fit. A `locked` rectangle keeps
|
|
87
93
|
* its position, but the rows do not go around it. `pack` sets the width first and then uses this function.
|
|
94
|
+
*
|
|
95
|
+
* With `options.space`, the rows go around the locked rectangles and the holes of the space, inside its outline. A space
|
|
96
|
+
* that is plain, with no locked rectangle, gives the rows of 0.1.
|
|
88
97
|
*/
|
|
89
98
|
export function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options: PackOptions = {}): Rect<T> {
|
|
90
99
|
const order = options.sortOrder ?? "height-desc";
|
|
100
|
+
const space = options.space;
|
|
101
|
+
if (space !== undefined && (!space.plain || rects.some((r) => r.locked))) return packRowsInSpace(rects, outer, order, space);
|
|
91
102
|
for (const r of rects) if (r.locked) r.wasPacked = true;
|
|
92
103
|
while (!packRow(rects, outer, order));
|
|
93
104
|
return outer;
|
package/src/shelf.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { compareRects, type SortOrder } from "./order.ts";
|
|
2
2
|
import type { Rect } from "./rect.ts";
|
|
3
|
+
import { firstFit, type FreeSpace, type FreeSpan } from "./space.ts";
|
|
3
4
|
|
|
4
5
|
/** The options of `packShelf` */
|
|
5
6
|
export interface ShelfOptions {
|
|
6
7
|
/** The order of the rectangles before the packing. The default is `height-desc`. */
|
|
7
8
|
sortOrder?: SortOrder;
|
|
9
|
+
/**
|
|
10
|
+
* The free space of the container: `freeSpace` of the input. Without it, or when it is plain, the packer uses only the
|
|
11
|
+
* outer rectangle and the locked rectangles.
|
|
12
|
+
*/
|
|
13
|
+
space?: FreeSpace;
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
/** An obstacle in the coordinates of the outer rectangle */
|
|
@@ -52,8 +58,10 @@ function nextTop(obstacles: readonly Box[], top: number, bottom: number): number
|
|
|
52
58
|
* - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
|
|
53
59
|
* - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
|
|
54
60
|
* rectangle that does not fit is not packed: its `wasPacked` stays `false`.
|
|
61
|
+
* - With a space that is not plain, `packRowsInSpace` packs the rows into the outline of the space and around its holes.
|
|
55
62
|
*/
|
|
56
63
|
export function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options: ShelfOptions = {}): Rect<T> {
|
|
64
|
+
if (options.space !== undefined && !options.space.plain) return packRowsInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
|
|
57
65
|
const width = outer.size.x;
|
|
58
66
|
const obstacles: Box[] = [];
|
|
59
67
|
let contentBottom = 0;
|
|
@@ -119,3 +127,102 @@ export function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options: ShelfOpt
|
|
|
119
127
|
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
|
|
120
128
|
return outer;
|
|
121
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* This function gives the first x from `start` where a rectangle of width `w` fits. There, it crosses no locked rectangle,
|
|
133
|
+
* and it is in a free span of its band. It takes `firstFreeX` and `firstFit` in turn until they agree. It gives `Infinity` when no x fits.
|
|
134
|
+
*/
|
|
135
|
+
function placeInBand(obstacles: readonly Box[], spans: readonly FreeSpan[], start: number, w: number, top: number, bottom: number): number {
|
|
136
|
+
let x = start;
|
|
137
|
+
for (;;) {
|
|
138
|
+
const free = firstFreeX(obstacles, x, w, top, bottom);
|
|
139
|
+
const fit = firstFit(spans, free, w);
|
|
140
|
+
if (fit === free || !Number.isFinite(fit)) return fit;
|
|
141
|
+
x = fit;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* This function packs rectangles into rows inside a free space. `packShelf` and `packRows` use it when the space is not
|
|
147
|
+
* plain. It sets the position of each rectangle and gives the outer rectangle.
|
|
148
|
+
*
|
|
149
|
+
* - A rectangle goes to the first x from the row cursor where it fits in the space and crosses no locked rectangle.
|
|
150
|
+
* - When it does not fit, a new row starts under the current row. An empty row steps down to `nextY` of the space.
|
|
151
|
+
* - When the steps of an empty row find no place, the row goes back to its last top edge. Later rectangles can use it.
|
|
152
|
+
* - A rectangle wider than the outer rectangle overflows. Without `fixedSize`, it goes on its own row at the left edge.
|
|
153
|
+
*/
|
|
154
|
+
export function packRowsInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder, space: FreeSpace): Rect<T> {
|
|
155
|
+
const width = outer.size.x;
|
|
156
|
+
const ox = outer.position.x;
|
|
157
|
+
const oy = outer.position.y;
|
|
158
|
+
const limit = outer.fixedSize ? outer.size.y : Infinity;
|
|
159
|
+
const obstacles: Box[] = [];
|
|
160
|
+
let contentBottom = 0;
|
|
161
|
+
for (const rect of rects) {
|
|
162
|
+
if (!rect.locked) continue;
|
|
163
|
+
rect.wasPacked = true;
|
|
164
|
+
const left = rect.position.x - ox;
|
|
165
|
+
const top = rect.position.y - oy;
|
|
166
|
+
obstacles.push({ left, top, right: left + rect.size.x, bottom: top + rect.size.y });
|
|
167
|
+
contentBottom = Math.max(contentBottom, top + rect.size.y);
|
|
168
|
+
}
|
|
169
|
+
// The x in the row, in the coordinates of the outer rectangle, or Infinity
|
|
170
|
+
const place = (start: number, w: number, top: number, h: number): number => {
|
|
171
|
+
const spans = space.band(top + oy, top + h + oy).map((s) => ({ start: s.start - ox, end: s.end - ox }));
|
|
172
|
+
return placeInBand(obstacles, spans, start, w, top, top + h);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
let rowTop = 0;
|
|
176
|
+
let rowBottom = 0;
|
|
177
|
+
let cursor = 0;
|
|
178
|
+
let rowEmpty = true;
|
|
179
|
+
for (const rect of rects.toSorted(compareRects(order))) {
|
|
180
|
+
if (rect.wasPacked) continue;
|
|
181
|
+
const w = rect.size.x;
|
|
182
|
+
const h = rect.size.y;
|
|
183
|
+
if (!(w > 0 && h > 0)) continue;
|
|
184
|
+
if (w > width) {
|
|
185
|
+
if (outer.fixedSize) continue;
|
|
186
|
+
const top = rowEmpty ? rowTop : rowBottom;
|
|
187
|
+
rect.position.set(ox, top + oy);
|
|
188
|
+
rect.wasPacked = true;
|
|
189
|
+
rowTop = top + h;
|
|
190
|
+
rowBottom = rowTop;
|
|
191
|
+
cursor = 0;
|
|
192
|
+
rowEmpty = true;
|
|
193
|
+
contentBottom = Math.max(contentBottom, top + h);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let x = place(cursor, w, rowTop, h);
|
|
198
|
+
if (!(x + w <= width) && !rowEmpty) {
|
|
199
|
+
rowTop = rowBottom;
|
|
200
|
+
cursor = 0;
|
|
201
|
+
rowEmpty = true;
|
|
202
|
+
x = place(0, w, rowTop, h);
|
|
203
|
+
}
|
|
204
|
+
// The steps of an empty row. They stay only when the rectangle finds a place.
|
|
205
|
+
let top = rowTop;
|
|
206
|
+
while (!(x + w <= width) && top + h <= limit) {
|
|
207
|
+
const next = Math.min(space.nextY(top + oy, top + h + oy, w) - oy, nextTop(obstacles, top, top + h));
|
|
208
|
+
if (!Number.isFinite(next)) break;
|
|
209
|
+
top = next;
|
|
210
|
+
x = place(0, w, top, h);
|
|
211
|
+
}
|
|
212
|
+
if (!(x + w <= width) || top + h > limit) continue;
|
|
213
|
+
if (top !== rowTop) {
|
|
214
|
+
rowTop = top;
|
|
215
|
+
rowBottom = top;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
rect.position.set(x + ox, rowTop + oy);
|
|
219
|
+
rect.wasPacked = true;
|
|
220
|
+
rowEmpty = false;
|
|
221
|
+
cursor = x + w;
|
|
222
|
+
rowBottom = Math.max(rowBottom, rowTop + h);
|
|
223
|
+
contentBottom = Math.max(contentBottom, rowTop + h);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
|
|
227
|
+
return outer;
|
|
228
|
+
}
|