@mark1russell7/packing 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/index.d.ts +46 -219
- package/build/index.d.ts.map +1 -1
- package/build/index.js +166 -279
- package/build/index.js.map +1 -1
- package/package.json +5 -2
- package/src/adapters.ts +16 -13
- 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 +7 -3
- package/src/layout-input.ts +34 -43
- package/src/measures.ts +12 -15
- package/src/strategy.ts +3 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/packing",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Rectangle packing: shelf, guillotine and skyline strategies, push cascades and measures of a layout.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"rectangle-packing",
|
|
@@ -48,15 +48,18 @@
|
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=22.12"
|
|
50
50
|
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@mark1russell7/layout-contract": "^0.2.1"
|
|
53
|
+
},
|
|
51
54
|
"devDependencies": {
|
|
52
55
|
"@mark1russell7/cue": "^1.0.2",
|
|
53
|
-
"@mark1russell7/layout-contract": "github:mark1russell7/layout-contract#710ec519027dbc122d111cc5e76a1c18877f4237&path:packages/layout-contract",
|
|
54
56
|
"@types/node": "^26.6.4",
|
|
55
57
|
"tsdown": "^0.23.1",
|
|
56
58
|
"typescript": "^7.0.2",
|
|
57
59
|
"vitest": "^5.0.3"
|
|
58
60
|
},
|
|
59
61
|
"scripts": {
|
|
62
|
+
"bench": "node bench/main.ts",
|
|
60
63
|
"build": "tsdown",
|
|
61
64
|
"test": "vitest run",
|
|
62
65
|
"test:watch": "vitest",
|
package/src/adapters.ts
CHANGED
|
@@ -57,8 +57,7 @@ function packFixedWidth(id: "rows" | "skyline" | "shelf", input: PackInput, opti
|
|
|
57
57
|
const rects = rectsOf(frame);
|
|
58
58
|
if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder });
|
|
59
59
|
const output = readFrame(input, frame, []);
|
|
60
|
-
output
|
|
61
|
-
return output;
|
|
60
|
+
return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length } }] };
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
/** This function makes the strategy of one function of `pack`. */
|
|
@@ -141,11 +140,13 @@ export const logSweep: SizeSearch = {
|
|
|
141
140
|
const ms = now() - start;
|
|
142
141
|
const runs = result.tried.length + 1;
|
|
143
142
|
const output = readFrame(input, frame, []);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
return {
|
|
144
|
+
...output,
|
|
145
|
+
trace: [
|
|
146
|
+
{ id: "log-sweep", ms, measures: { "pack.search.widths": result.tried.length, "pack.search.ms": ms } },
|
|
147
|
+
{ id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length } },
|
|
148
|
+
],
|
|
149
|
+
};
|
|
149
150
|
},
|
|
150
151
|
};
|
|
151
152
|
|
|
@@ -191,7 +192,7 @@ function pushFixedWidth(input: PackInput, options: PushStrategyOptions, previous
|
|
|
191
192
|
|
|
192
193
|
// An obstacle that the cascade pushed out of the container or onto a locked item goes back to its start
|
|
193
194
|
const { bounds } = frame;
|
|
194
|
-
const moved:
|
|
195
|
+
const moved: { id: string; x: number; y: number }[] = [];
|
|
195
196
|
for (const e of obstacles) {
|
|
196
197
|
const startAt = starts.get(e)!;
|
|
197
198
|
const x = e.rect.position.x + e.dx + frame.ox;
|
|
@@ -224,11 +225,13 @@ function pushFixedWidth(input: PackInput, options: PushStrategyOptions, previous
|
|
|
224
225
|
packSkyline([...pending.map((e) => e.rect), ...kept], outerOf(frame), { sortOrder: options.sortOrder });
|
|
225
226
|
}
|
|
226
227
|
const output = readFrame(input, frame, [], moved);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
228
|
+
return {
|
|
229
|
+
...output,
|
|
230
|
+
trace: [
|
|
231
|
+
{ id: "push", ms: pushMs, measures: { "pack.push.moved": moved.length } },
|
|
232
|
+
{ id: "skyline", ms: now() - skylineStart, measures: { "pack.push.replaced": pending.length, "pack.overflow": output.overflow.length } },
|
|
233
|
+
],
|
|
234
|
+
};
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
/**
|
package/src/check.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { boundsOf, regionHitsRect, translate, type PackHard, type Rect, type Region } from "@mark1russell7/layout-contract";
|
|
2
|
+
import { DEFAULT_TOLERANCE, marginOf, RECORD_EPS, turnItem, type Item, type PackInput, type PackOutput, type Placement, type Tolerance } from "./contract.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
export interface HardBreaks {
|
|
6
|
-
"no-overlap": number;
|
|
7
|
-
"in-container": number;
|
|
8
|
-
"locked-kept": number;
|
|
9
|
-
complete: number;
|
|
10
|
-
}
|
|
4
|
+
export type { PackHard } from "@mark1russell7/layout-contract";
|
|
11
5
|
|
|
12
6
|
/** A box with its margin: the left, top, right and bottom edges. */
|
|
13
7
|
interface Box {
|
|
@@ -22,6 +16,11 @@ function overlap(a: Box, b: Box, tolerance: number): boolean {
|
|
|
22
16
|
return Math.min(a.r, b.r) - Math.max(a.l, b.l) > tolerance && Math.min(a.b, b.b) - Math.max(a.t, b.t) > tolerance;
|
|
23
17
|
}
|
|
24
18
|
|
|
19
|
+
/** This function gives the box made smaller by a tolerance on each side, as a rectangle. */
|
|
20
|
+
function inner(box: Box, tolerance: number): Rect {
|
|
21
|
+
return { x: box.l + tolerance, y: box.t + tolerance, w: box.r - box.l - 2 * tolerance, h: box.b - box.t - 2 * tolerance };
|
|
22
|
+
}
|
|
23
|
+
|
|
25
24
|
/**
|
|
26
25
|
* This function counts the breaks of the 4 hard rules of the packer for an output.
|
|
27
26
|
*
|
|
@@ -32,32 +31,35 @@ function overlap(a: Box, b: Box, tolerance: number): boolean {
|
|
|
32
31
|
* - `complete`: each item with no placement that is not in the overflow. A locked item breaks `locked-kept` in its place.
|
|
33
32
|
*
|
|
34
33
|
* A locked item with no placement stays at its input position for `no-overlap` and `in-container`. A locked item with no
|
|
35
|
-
* `x` or no `y` in the input is
|
|
34
|
+
* `x` or no `y` in the input is a bad input. The rules skip it, as `packHard` of the contract does. `checkInput` finds it.
|
|
36
35
|
*
|
|
37
|
-
* The
|
|
38
|
-
*
|
|
36
|
+
* The test of an obstacle is exact: a circle, an ellipse and a polygon with holes count only their insides, as in `packHard`.
|
|
37
|
+
* The rules read a shape of the container or of an item as its bounding box (decision 5). The bounding box of an item
|
|
38
|
+
* shape is relative to the position of the item. The result has no tolerance in it.
|
|
39
39
|
*/
|
|
40
|
-
export function checkHard(input: PackInput, output: PackOutput, tolerance: Pick<Tolerance, "overlap"> = DEFAULT_TOLERANCE):
|
|
40
|
+
export function checkHard(input: PackInput, output: PackOutput, tolerance: Pick<Tolerance, "overlap"> = DEFAULT_TOLERANCE): PackHard {
|
|
41
41
|
const tol = tolerance.overlap;
|
|
42
|
-
const placements = new Map<string,
|
|
42
|
+
const placements = new Map<string, Placement>();
|
|
43
43
|
for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
|
|
44
44
|
const overflow = new Set(output.overflow);
|
|
45
|
-
|
|
45
|
+
let lockedKept = 0;
|
|
46
|
+
let complete = 0;
|
|
46
47
|
|
|
47
48
|
const boxes: Box[] = [];
|
|
48
49
|
for (const item of input.items) {
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
if (item.locked === true && (item.x === undefined || item.y === undefined)) continue;
|
|
51
|
+
const lockedAt = item.locked === true ? { x: item.x!, y: item.y! } : undefined;
|
|
52
|
+
let p: Pick<Placement, "x" | "y" | "turn"> | undefined = placements.get(item.id);
|
|
51
53
|
if (p === undefined) {
|
|
52
54
|
if (lockedAt === undefined) {
|
|
53
|
-
if (!overflow.has(item.id))
|
|
55
|
+
if (!overflow.has(item.id)) complete += 1;
|
|
54
56
|
continue;
|
|
55
57
|
}
|
|
56
58
|
// A locked item with no placement breaks locked-kept, and it stays at its input position for the other rules
|
|
57
|
-
|
|
59
|
+
lockedKept += 1;
|
|
58
60
|
p = lockedAt;
|
|
59
61
|
} else if (lockedAt !== undefined) {
|
|
60
|
-
if (Math.abs(p.x - lockedAt.x) > RECORD_EPS || Math.abs(p.y - lockedAt.y) > RECORD_EPS)
|
|
62
|
+
if (Math.abs(p.x - lockedAt.x) > RECORD_EPS || Math.abs(p.y - lockedAt.y) > RECORD_EPS) lockedKept += 1;
|
|
61
63
|
}
|
|
62
64
|
const placed: Item = p.turn === 90 ? turnItem(item) : item;
|
|
63
65
|
const m = marginOf(placed);
|
|
@@ -68,15 +70,19 @@ export function checkHard(input: PackInput, output: PackOutput, tolerance: Pick<
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
const moved = new Map(output.moved.map((m) => [m.id, m]));
|
|
71
|
-
const obstacles:
|
|
73
|
+
const obstacles: Region[] = (input.obstacles ?? []).map((o) => {
|
|
74
|
+
const at = moved.get(o.id);
|
|
75
|
+
if (at === undefined) return o.region;
|
|
72
76
|
const b = boundsOf(o.region);
|
|
73
|
-
|
|
74
|
-
return { l: at.x, t: at.y, r: at.x + b.w, b: at.y + b.h };
|
|
77
|
+
return translate(o.region, at.x - b.x, at.y - b.y);
|
|
75
78
|
});
|
|
76
79
|
|
|
80
|
+
let overlaps = 0;
|
|
77
81
|
for (const [i, a] of boxes.entries()) {
|
|
78
|
-
for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j]!, tol))
|
|
79
|
-
|
|
82
|
+
for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j]!, tol)) overlaps += 1;
|
|
83
|
+
if (obstacles.length === 0) continue;
|
|
84
|
+
const rect = inner(a, tol);
|
|
85
|
+
for (const region of obstacles) if (regionHitsRect(region, rect)) overlaps += 1;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
const { container } = input;
|
|
@@ -88,10 +94,11 @@ export function checkHard(input: PackInput, output: PackOutput, tolerance: Pick<
|
|
|
88
94
|
const inset = container.inset ?? 0;
|
|
89
95
|
const fixedW = container.fixed === "size" || container.fixed === "width";
|
|
90
96
|
const fixedH = container.fixed === "size" || container.fixed === "height";
|
|
97
|
+
let outside = 0;
|
|
91
98
|
for (const box of boxes) {
|
|
92
99
|
const outX = fixedW && (box.l < x + inset - tol || box.r > x + w - inset + tol);
|
|
93
100
|
const outY = fixedH && (box.t < y + inset - tol || box.b > y + h - inset + tol);
|
|
94
|
-
if (outX || outY)
|
|
101
|
+
if (outX || outY) outside += 1;
|
|
95
102
|
}
|
|
96
|
-
return
|
|
103
|
+
return { "no-overlap": overlaps, "in-container": outside, "locked-kept": lockedKept, complete };
|
|
97
104
|
}
|
package/src/contract.ts
CHANGED
|
@@ -1,205 +1,43 @@
|
|
|
1
1
|
// The data types of the Packer contract (docs/PACKER.md). They are plain data, with no class and no method.
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** An obstacle: an area that no item enters. */
|
|
30
|
-
export interface Obstacle {
|
|
31
|
-
id: string;
|
|
32
|
-
region: Region;
|
|
33
|
-
/** Without it, the obstacle stays. With it, the packer can move the obstacle for `cost` times the distance. */
|
|
34
|
-
move?: { cost: number };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** The margin of an item on each side. */
|
|
38
|
-
export interface Margin {
|
|
39
|
-
top: number;
|
|
40
|
-
right: number;
|
|
41
|
-
bottom: number;
|
|
42
|
-
left: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** A port on the border of an item. */
|
|
46
|
-
export interface Port {
|
|
47
|
-
id: string;
|
|
48
|
-
side: "top" | "right" | "bottom" | "left";
|
|
49
|
-
/** The point on the border of the item, relative to its top-left corner. Without it, the router chooses the point. */
|
|
50
|
-
x?: number;
|
|
51
|
-
y?: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** An item to pack. The file format names it a box. */
|
|
55
|
-
export interface Item {
|
|
56
|
-
id: string;
|
|
57
|
-
w: number;
|
|
58
|
-
h: number;
|
|
59
|
-
/** The reading order. Without it, the index in the array is the rank. */
|
|
60
|
-
rank?: number;
|
|
61
|
-
group?: string;
|
|
62
|
-
parent?: string;
|
|
63
|
-
/** A shape relative to the origin of the item. Without it, the item is the rectangle `w` by `h`. */
|
|
64
|
-
shape?: Region;
|
|
65
|
-
/** A locked item keeps `x` and `y`. The packer reads it as an obstacle. */
|
|
66
|
-
locked?: boolean;
|
|
67
|
-
x?: number;
|
|
68
|
-
y?: number;
|
|
69
|
-
/** The value `true` lets the packer turn the item by 90 degrees. The default is `false`. */
|
|
70
|
-
turnable?: boolean;
|
|
71
|
-
/** A ring around the item that no other item and no route enters. The default is 0. */
|
|
72
|
-
margin?: number | Margin;
|
|
73
|
-
/** The ports. The packer keeps them, and turns them with the item. */
|
|
74
|
-
ports?: Port[];
|
|
75
|
-
/** The free data of the item. The packer does not read it, and the turn and the converters keep it. */
|
|
76
|
-
data?: Readonly<Record<string, unknown>>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** The input of a packing: a `layout-input/1` file without `edges`, with `items` for `boxes`. */
|
|
80
|
-
export interface PackInput {
|
|
81
|
-
container: Container;
|
|
82
|
-
items: Item[];
|
|
83
|
-
obstacles?: Obstacle[];
|
|
84
|
-
/** The free space between two items with their margins. Routes use this space. The default is 0. */
|
|
85
|
-
gap?: number;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The position of one item after the packing. */
|
|
89
|
-
export interface Placement {
|
|
90
|
-
id: string;
|
|
91
|
-
x: number;
|
|
92
|
-
y: number;
|
|
93
|
-
/** The turn: 0 or 90, only for an item with `turnable` set to `true`. */
|
|
94
|
-
turn?: 0 | 90;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** The facts of one stage of a packing. */
|
|
98
|
-
export interface StageTrace {
|
|
99
|
-
/** The id of the stage, for example `skyline` or `log-sweep`. */
|
|
100
|
-
id: string;
|
|
101
|
-
/** The time of the stage in milliseconds. */
|
|
102
|
-
ms: number;
|
|
103
|
-
/** The measures of the stage, by the names of `docs/FORMAT.md`. */
|
|
104
|
-
measures: Record<string, number>;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The output of a packing. The output does not change the input. */
|
|
108
|
-
export interface PackOutput {
|
|
109
|
-
placements: Placement[];
|
|
110
|
-
/** The ids of the items that the packer did not place. */
|
|
111
|
-
overflow: string[];
|
|
112
|
-
/** The size of the container after the packing. For a free size, the size that the search found. */
|
|
113
|
-
size: { w: number; h: number };
|
|
114
|
-
/** The new positions of the obstacles that moved. */
|
|
115
|
-
moved: { id: string; x: number; y: number }[];
|
|
116
|
-
/** The facts of each stage, for the site and the records. */
|
|
117
|
-
trace: StageTrace[];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** The tolerances of the hard rules. */
|
|
121
|
-
export interface Tolerance {
|
|
122
|
-
/** The rule `no-overlap`: two boxes overlap only by more than this value. */
|
|
123
|
-
overlap: number;
|
|
124
|
-
/** The router rule that a route does not go through a box. */
|
|
125
|
-
through: number;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** The tolerance for the comparison of two numbers in an engine. */
|
|
129
|
-
export const EPS = 1e-6;
|
|
130
|
-
|
|
131
|
-
/** The tolerance for the comparison of two numbers of a file. The files round to 3 decimals. */
|
|
132
|
-
export const RECORD_EPS = 1e-3;
|
|
133
|
-
|
|
134
|
-
/** The default tolerances of the hard rules. Each record writes its tolerances in its top-level field `tolerance`. */
|
|
135
|
-
export const DEFAULT_TOLERANCE: Readonly<Tolerance> = Object.freeze({ overlap: 0.001, through: 0.5 });
|
|
2
|
+
// Since 0.2.0, the types and the shared values come from `@mark1russell7/layout-contract`. This file adds the packer names.
|
|
3
|
+
|
|
4
|
+
import { marginSides, turnBox, type Item, type Sides } from "@mark1russell7/layout-contract";
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
DEFAULT_TOLERANCE,
|
|
8
|
+
EPS,
|
|
9
|
+
RECORD_EPS,
|
|
10
|
+
checkInput,
|
|
11
|
+
turnPoint,
|
|
12
|
+
turnPort,
|
|
13
|
+
turnRegion,
|
|
14
|
+
type Item,
|
|
15
|
+
type LayoutContainer,
|
|
16
|
+
type Margin,
|
|
17
|
+
type Obstacle,
|
|
18
|
+
type PackInput,
|
|
19
|
+
type PackOutput,
|
|
20
|
+
type Placement,
|
|
21
|
+
type Port,
|
|
22
|
+
type Region,
|
|
23
|
+
type Side,
|
|
24
|
+
type Sides,
|
|
25
|
+
type StageTrace,
|
|
26
|
+
type Tolerance,
|
|
27
|
+
} from "@mark1russell7/layout-contract";
|
|
136
28
|
|
|
137
29
|
/** This function gives the margin of an item on each side. A number is the same margin on each side. */
|
|
138
|
-
export function marginOf(item: Pick<Item, "margin">):
|
|
139
|
-
const m = item.margin
|
|
140
|
-
if (typeof m === "number") return { top: m, right: m, bottom: m, left: m };
|
|
30
|
+
export function marginOf(item: Pick<Item, "margin">): Sides {
|
|
31
|
+
const m = marginSides(item.margin);
|
|
141
32
|
return { top: m.top, right: m.right, bottom: m.bottom, left: m.left };
|
|
142
33
|
}
|
|
143
34
|
|
|
144
|
-
/** The side of a port after a turn of 90 degrees clockwise. */
|
|
145
|
-
const TURNED_SIDE = { top: "right", right: "bottom", bottom: "left", left: "top" } as const;
|
|
146
|
-
|
|
147
|
-
/** This function gives the point `px`, `py` of an item of height `h` after a turn of 90 degrees clockwise. */
|
|
148
|
-
export function turnPoint(px: number, py: number, h: number): [number, number] {
|
|
149
|
-
return [h - py, px];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** This function turns a port of an item of height `h` by 90 degrees clockwise. The side turns from top to right. */
|
|
153
|
-
export function turnPort(port: Port, h: number): Port {
|
|
154
|
-
const turned: Port = { id: port.id, side: TURNED_SIDE[port.side] };
|
|
155
|
-
if (port.x !== undefined && port.y !== undefined) {
|
|
156
|
-
const [x, y] = turnPoint(port.x, port.y, h);
|
|
157
|
-
turned.x = x;
|
|
158
|
-
turned.y = y;
|
|
159
|
-
} else if (port.x !== undefined) {
|
|
160
|
-
// Only x is set: x becomes the new y, and the router chooses the new x
|
|
161
|
-
turned.y = port.x;
|
|
162
|
-
} else if (port.y !== undefined) {
|
|
163
|
-
// Only y is set: h - y becomes the new x, and the router chooses the new y
|
|
164
|
-
turned.x = h - port.y;
|
|
165
|
-
}
|
|
166
|
-
return turned;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** This function turns a region relative to an item of height `h` by 90 degrees clockwise. */
|
|
170
|
-
export function turnRegion(region: Region, h: number): Region {
|
|
171
|
-
switch (region.kind) {
|
|
172
|
-
case "rect":
|
|
173
|
-
return { kind: "rect", x: h - region.y - region.h, y: region.x, w: region.h, h: region.w };
|
|
174
|
-
case "circle": {
|
|
175
|
-
const [cx, cy] = turnPoint(region.cx, region.cy, h);
|
|
176
|
-
return { kind: "circle", cx, cy, r: region.r };
|
|
177
|
-
}
|
|
178
|
-
case "ellipse": {
|
|
179
|
-
const [cx, cy] = turnPoint(region.cx, region.cy, h);
|
|
180
|
-
return { kind: "ellipse", cx, cy, rx: region.ry, ry: region.rx };
|
|
181
|
-
}
|
|
182
|
-
case "polygon": {
|
|
183
|
-
const turnRing = (ring: [number, number][]): [number, number][] => ring.map(([x, y]) => turnPoint(x, y, h));
|
|
184
|
-
const turned: Region = { kind: "polygon", points: turnRing(region.points) };
|
|
185
|
-
if (region.holes) turned.holes = region.holes.map(turnRing);
|
|
186
|
-
return turned;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
35
|
/**
|
|
192
36
|
* This function gives an item after a turn of 90 degrees clockwise, with the y axis down (decision 1 of
|
|
193
37
|
* `docs/PACKER.md`). The width and the height change places. The top-left corner, the id and the other fields stay.
|
|
194
38
|
* The ports, the shape and the margin turn with the item. The function does not change its argument.
|
|
39
|
+
* It is `turnBox` of the contract.
|
|
195
40
|
*/
|
|
196
41
|
export function turnItem(item: Item): Item {
|
|
197
|
-
|
|
198
|
-
if (item.ports) turned.ports = item.ports.map((p) => turnPort(p, item.h));
|
|
199
|
-
if (item.shape) turned.shape = turnRegion(item.shape, item.h);
|
|
200
|
-
if (item.margin !== undefined && typeof item.margin !== "number") {
|
|
201
|
-
const m = item.margin;
|
|
202
|
-
turned.margin = { top: m.left, right: m.top, bottom: m.right, left: m.bottom };
|
|
203
|
-
}
|
|
204
|
-
return turned;
|
|
42
|
+
return turnBox(item);
|
|
205
43
|
}
|
package/src/convert.ts
CHANGED
|
@@ -10,21 +10,17 @@ import type { Rect } from "./rect.ts";
|
|
|
10
10
|
*/
|
|
11
11
|
export function fromRects<T>(rects: readonly Rect<T>[], outer: Rect<T>): PackInput {
|
|
12
12
|
const fixed = outer.fixedSize ? "size" : outer.fixedWidth ? "width" : "none";
|
|
13
|
-
const container: PackInput["container"] = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
item.y = r.position.y;
|
|
25
|
-
}
|
|
26
|
-
return item;
|
|
27
|
-
});
|
|
13
|
+
const container: PackInput["container"] = {
|
|
14
|
+
fixed,
|
|
15
|
+
...(fixed === "none" ? {} : { w: outer.size.x }),
|
|
16
|
+
...(fixed === "size" ? { h: outer.size.y } : {}),
|
|
17
|
+
...(outer.position.x !== 0 || outer.position.y !== 0
|
|
18
|
+
? { shape: { kind: "rect", x: outer.position.x, y: outer.position.y, w: outer.size.x, h: outer.size.y } as const }
|
|
19
|
+
: {}),
|
|
20
|
+
};
|
|
21
|
+
const items = rects.map(
|
|
22
|
+
(r, i): Item => ({ id: String(i), w: r.size.x, h: r.size.y, ...(r.locked ? { locked: true, x: r.position.x, y: r.position.y } : {}) }),
|
|
23
|
+
);
|
|
28
24
|
return { container, items };
|
|
29
25
|
}
|
|
30
26
|
|
package/src/corpus.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The manifest of a corpus: `layout-corpus/1` of `docs/FORMAT.md`.
|
|
2
2
|
|
|
3
3
|
import { generator } from "./generators.ts";
|
|
4
|
-
import type { CorpusCell,
|
|
4
|
+
import type { CorpusCell, GeneratedInput, GeneratorParams } from "./layout-input.ts";
|
|
5
5
|
|
|
6
6
|
/** One spec of a corpus: a generator, its parameters and its seeds. */
|
|
7
7
|
export interface CorpusSpec {
|
|
@@ -34,7 +34,7 @@ export interface CorpusManifest {
|
|
|
34
34
|
/** A corpus: the manifest and the input files, in the order of the specs and the seeds. */
|
|
35
35
|
export interface Corpus {
|
|
36
36
|
manifest: CorpusManifest;
|
|
37
|
-
inputs:
|
|
37
|
+
inputs: GeneratedInput[];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -43,7 +43,7 @@ export interface Corpus {
|
|
|
43
43
|
*/
|
|
44
44
|
export function corpus(name: string, specs: readonly CorpusSpec[]): Corpus {
|
|
45
45
|
const manifest: CorpusManifest = { schema: "layout-corpus/1", corpus: name, specs: [], inputs: [] };
|
|
46
|
-
const inputs:
|
|
46
|
+
const inputs: GeneratedInput[] = [];
|
|
47
47
|
const ids = new Set<string>();
|
|
48
48
|
for (const spec of specs) {
|
|
49
49
|
const gen = generator(spec.generator);
|
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
|
}
|