@mark1russell7/packing 0.1.2 → 0.1.3

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.
@@ -0,0 +1,261 @@
1
+ // The adapters of 0.1 (docs/PACKER.md): each function of 0.1 runs behind the Packer interface as a strategy.
2
+
3
+ import { EPS, type PackInput, type PackOutput } from "./contract.ts";
4
+ import { boundsOf, buildFrame, now, outerOf, readFrame, rectsOf, viaAxes, type Entry } from "./frame.ts";
5
+ import { packFree } from "./free.ts";
6
+ import type { SortOrder } from "./order.ts";
7
+ import { pack } from "./pack.ts";
8
+ import { pushCascade } from "./push.ts";
9
+ import { Rect } from "./rect.ts";
10
+ import { packSkyline } from "./skyline.ts";
11
+ import { assertSupported, cellOf, UnsupportedCell, type PackContext, type SizeSearch, type Strategy, type Supports } from "./strategy.ts";
12
+
13
+ /** The options of the strategies `rows`, `skyline` and `shelf`. */
14
+ export interface LayoutStrategyOptions {
15
+ /** The order of the items before the packing. The items are in the order of their rank first. */
16
+ sortOrder: SortOrder;
17
+ /** The number of widths that the first pass of the size search `log-sweep` tries. */
18
+ samples: number;
19
+ }
20
+
21
+ /** The options of the strategy `push`. */
22
+ export interface PushStrategyOptions {
23
+ /** The order of the items that the cascade does not place. */
24
+ sortOrder: SortOrder;
25
+ /** The most shoves for each item. The cascade stops at this limit. */
26
+ maxShovesPerItem: number;
27
+ }
28
+
29
+ /** The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. */
30
+ const LAYOUT_CELLS = {
31
+ containerSize: ["fixed", "freeHeight", "freeWidth", "freeAspect", "freeArea"],
32
+ containerShape: ["rect"],
33
+ itemShape: ["rect"],
34
+ rotation: ["none"],
35
+ groups: ["none"],
36
+ mode: ["offline"],
37
+ } as const satisfies Partial<Supports>;
38
+
39
+ /** This function makes the supports of a strategy, with copies of each list. */
40
+ function supportsOf(cells: { [D in keyof Supports]: readonly Supports[D][number][] }): Supports {
41
+ return {
42
+ containerSize: [...cells.containerSize],
43
+ containerShape: [...cells.containerShape],
44
+ obstacleMove: [...cells.obstacleMove],
45
+ itemShape: [...cells.itemShape],
46
+ rotation: [...cells.rotation],
47
+ order: [...cells.order],
48
+ groups: [...cells.groups],
49
+ mode: [...cells.mode],
50
+ };
51
+ }
52
+
53
+ /** This function packs an input with a fixed size or a fixed width with one strategy of `pack`. */
54
+ function packFixedWidth(id: "rows" | "skyline" | "shelf", input: PackInput, options: LayoutStrategyOptions): PackOutput {
55
+ const start = now();
56
+ const frame = buildFrame(input);
57
+ const rects = rectsOf(frame);
58
+ if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder });
59
+ const output = readFrame(input, frame, []);
60
+ output.trace.push({ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length } });
61
+ return output;
62
+ }
63
+
64
+ /** This function makes the strategy of one function of `pack`. */
65
+ function layoutStrategy(
66
+ id: "rows" | "skyline" | "shelf",
67
+ family: string,
68
+ catalog: string[],
69
+ cells: Pick<Supports, "obstacleMove" | "order">,
70
+ ): Strategy<LayoutStrategyOptions> {
71
+ const self: Strategy<LayoutStrategyOptions> = {
72
+ id,
73
+ family,
74
+ catalog,
75
+ supports: supportsOf({ ...LAYOUT_CELLS, ...cells }),
76
+ defaults: { sortOrder: "height-desc", samples: 48 },
77
+ deterministic: true,
78
+ pack(input, options, context) {
79
+ assertSupported(self.supports, input);
80
+ const o = { ...self.defaults, ...options };
81
+ switch (input.container.fixed) {
82
+ case "height":
83
+ return viaAxes(input, context.previous, (turned) => packFixedWidth(id, turned, o));
84
+ case "none":
85
+ return logSweep.search(input, self, context, o);
86
+ default:
87
+ return packFixedWidth(id, input, o);
88
+ }
89
+ },
90
+ };
91
+ return self;
92
+ }
93
+
94
+ /**
95
+ * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. It supports no obstacle and
96
+ * no locked item, because `packRows` does not go around them. Turnable items, groups and shapes throw `UnsupportedCell`.
97
+ */
98
+ export const rows: Strategy<LayoutStrategyOptions> = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
99
+ obstacleMove: [],
100
+ order: ["free", "soft", "hard"],
101
+ });
102
+
103
+ /**
104
+ * The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
105
+ * locked items and the fixed obstacles. Turnable items, groups and shapes throw `UnsupportedCell`.
106
+ */
107
+ export const skyline: Strategy<LayoutStrategyOptions> = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
108
+ obstacleMove: ["fixed"],
109
+ order: ["free", "soft"],
110
+ });
111
+
112
+ /**
113
+ * The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
114
+ * With the sort order `input`, the rows keep the rank order. Turnable items, groups and shapes throw `UnsupportedCell`.
115
+ */
116
+ export const shelf: Strategy<LayoutStrategyOptions> = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
117
+ obstacleMove: ["fixed"],
118
+ order: ["free", "soft", "hard"],
119
+ });
120
+
121
+ /**
122
+ * The size search `log-sweep` uses `packFree` of 0.1.2: widths on a log scale, then the widths near the best width. It
123
+ * wraps `rows`, `skyline` and `shelf`. The objective is the area of the frame times `1 + aspectWeight * aspectError`.
124
+ */
125
+ export const logSweep: SizeSearch = {
126
+ id: "log-sweep",
127
+ search(input: PackInput, inner: Strategy, context: PackContext, options?: unknown): PackOutput {
128
+ if (input.container.fixed !== "none") return inner.pack(input, options ?? inner.defaults, context);
129
+ const id = inner.id;
130
+ if (id !== "rows" && id !== "skyline" && id !== "shelf") throw new UnsupportedCell("containerSize", cellOf(input).containerSize);
131
+ const o = { ...(inner.defaults as LayoutStrategyOptions), ...(options as Partial<LayoutStrategyOptions> | undefined) };
132
+ const start = now();
133
+ const frame = buildFrame(input);
134
+ const result = packFree(rectsOf(frame), new Rect<number>(), {
135
+ strategy: id,
136
+ sortOrder: o.sortOrder,
137
+ samples: o.samples,
138
+ aspect: input.container.aspect ?? 1,
139
+ aspectWeight: input.container.aspectWeight ?? 1,
140
+ });
141
+ const ms = now() - start;
142
+ const runs = result.tried.length + 1;
143
+ const output = readFrame(input, frame, []);
144
+ output.trace.push(
145
+ { id: "log-sweep", ms, measures: { "pack.search.widths": result.tried.length, "pack.search.ms": ms } },
146
+ { id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length } },
147
+ );
148
+ return output;
149
+ },
150
+ };
151
+
152
+ /** This function tells if the insides of two rectangles overlap by more than `EPS`. */
153
+ function overlaps(a: Rect, b: Rect): boolean {
154
+ return (
155
+ Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x) > EPS &&
156
+ Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y) > EPS
157
+ );
158
+ }
159
+
160
+ /** This function packs an input with a fixed size or a fixed width with a push cascade. */
161
+ function pushFixedWidth(input: PackInput, options: PushStrategyOptions, previous: PackOutput | undefined): PackOutput {
162
+ const start = now();
163
+ const frame = buildFrame(input, true);
164
+ const before = new Map(previous?.placements.map((p) => [p.id, p]));
165
+ const positioned: Entry[] = [];
166
+ const pending: Entry[] = [];
167
+ const obstacles = frame.entries.filter((e) => e.obstacle !== undefined);
168
+ const starts = new Map(obstacles.map((e) => [e, { x: e.rect.position.x, y: e.rect.position.y }]));
169
+ const lockedItems = frame.entries.filter((e) => e.item !== undefined && e.rect.locked);
170
+
171
+ for (const e of frame.entries) {
172
+ const item = e.item;
173
+ if (item === undefined || e.rect.locked) continue;
174
+ const x = item.x ?? before.get(item.id)?.x;
175
+ const y = item.y ?? before.get(item.id)?.y;
176
+ if (x === undefined || y === undefined) {
177
+ pending.push(e);
178
+ continue;
179
+ }
180
+ e.rect.position.set(x - e.dx - frame.ox, y - e.dy - frame.oy);
181
+ positioned.push(e);
182
+ }
183
+
184
+ // The cascade: each item in the order of its rank pushes the others, and then it stays
185
+ const inPlay = frame.entries.filter((e) => !pending.includes(e)).map((e) => e.rect);
186
+ const maxShoves = options.maxShovesPerItem * Math.max(1, inPlay.length);
187
+ for (const e of positioned) {
188
+ pushCascade(e.rect, inPlay, { gap: 0, maxShoves });
189
+ e.rect.locked = true;
190
+ }
191
+
192
+ // An obstacle that the cascade pushed out of the container or onto a locked item goes back to its start
193
+ const { bounds } = frame;
194
+ const moved: PackOutput["moved"] = [];
195
+ for (const e of obstacles) {
196
+ const startAt = starts.get(e)!;
197
+ const x = e.rect.position.x + e.dx + frame.ox;
198
+ const y = e.rect.position.y + e.dy + frame.oy;
199
+ const b = boundsOf(e.obstacle!.region);
200
+ const out = x < bounds.x - EPS || y < bounds.y - EPS || x + b.w > bounds.x + bounds.w + EPS || y + b.h > bounds.y + bounds.h + EPS;
201
+ if (out || lockedItems.some((l) => overlaps(l.rect, e.rect))) e.rect.position.set(startAt.x, startAt.y);
202
+ else if (Math.abs(e.rect.position.x - startAt.x) > EPS || Math.abs(e.rect.position.y - startAt.y) > EPS) moved.push({ id: e.obstacle!.id, x, y });
203
+ }
204
+ const pushMs = now() - start;
205
+
206
+ // Each item that is out of the area or overlaps a kept rectangle goes to the skyline, around the kept rectangles
207
+ const kept = [...obstacles, ...lockedItems].map((e) => e.rect);
208
+ for (const e of positioned) {
209
+ const r = e.rect;
210
+ const inside = r.position.x >= -EPS && r.position.y >= -EPS && r.right() <= frame.w + EPS && r.bottom() <= frame.h + EPS;
211
+ if (inside && !kept.some((k) => overlaps(k, r))) kept.push(r);
212
+ else pending.push(e);
213
+ }
214
+ const skylineStart = now();
215
+ for (const r of kept) {
216
+ r.locked = true;
217
+ r.wasPacked = true;
218
+ }
219
+ if (pending.length > 0 && frame.w > 0) {
220
+ for (const e of pending) {
221
+ e.rect.locked = false;
222
+ e.rect.wasPacked = false;
223
+ }
224
+ packSkyline([...pending.map((e) => e.rect), ...kept], outerOf(frame), { sortOrder: options.sortOrder });
225
+ }
226
+ const output = readFrame(input, frame, [], moved);
227
+ output.trace.push(
228
+ { id: "push", ms: pushMs, measures: { "pack.push.moved": moved.length } },
229
+ { id: "skyline", ms: now() - skylineStart, measures: { "pack.push.replaced": pending.length, "pack.overflow": output.overflow.length } },
230
+ );
231
+ return output;
232
+ }
233
+
234
+ /**
235
+ * The strategy `push` uses `pushCascade`. Each item starts at its `x` and `y`, or at its place in `context.previous`.
236
+ * Each item in rank order pushes the items and the movable obstacles that it overlaps. Then `packSkyline` places the
237
+ * items with no start, and the items that still overlap. The output tells each obstacle that moved.
238
+ */
239
+ export const push: Strategy<PushStrategyOptions> = {
240
+ id: "push",
241
+ family: "incremental",
242
+ catalog: ["incremental-push"],
243
+ supports: supportsOf({
244
+ containerSize: ["fixed", "freeHeight", "freeWidth"],
245
+ containerShape: ["rect"],
246
+ obstacleMove: ["fixed", "free"],
247
+ itemShape: ["rect"],
248
+ rotation: ["none"],
249
+ order: ["free", "soft"],
250
+ groups: ["none"],
251
+ mode: ["incremental"],
252
+ }),
253
+ defaults: { sortOrder: "height-desc", maxShovesPerItem: 8 },
254
+ deterministic: true,
255
+ pack(input, options, context) {
256
+ assertSupported(push.supports, input);
257
+ const o = { ...push.defaults, ...options };
258
+ if (input.container.fixed === "height") return viaAxes(input, context.previous, (turned, previous) => pushFixedWidth(turned, o, previous));
259
+ return pushFixedWidth(input, o, context.previous);
260
+ },
261
+ };
package/src/check.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { DEFAULT_TOLERANCE, marginOf, RECORD_EPS, turnItem, type Item, type PackInput, type PackOutput, type Tolerance } from "./contract.ts";
2
+ import { boundsOf } from "./frame.ts";
3
+
4
+ /** The count of the breaks of each hard rule of the packer (`docs/FORMAT.md`). A good run has 0 for each rule. */
5
+ export interface HardBreaks {
6
+ "no-overlap": number;
7
+ "in-container": number;
8
+ "locked-kept": number;
9
+ complete: number;
10
+ }
11
+
12
+ /** A box with its margin: the left, top, right and bottom edges. */
13
+ interface Box {
14
+ l: number;
15
+ t: number;
16
+ r: number;
17
+ b: number;
18
+ }
19
+
20
+ /** This function tells if the insides of two boxes overlap by more than a tolerance on each axis. */
21
+ function overlap(a: Box, b: Box, tolerance: number): boolean {
22
+ 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
+ }
24
+
25
+ /**
26
+ * This function counts the breaks of the 4 hard rules of the packer for an output.
27
+ *
28
+ * - `no-overlap`: each pair of items with their margins, and each pair of an item and an obstacle, that overlap by more than `tolerance.overlap`.
29
+ * - `in-container`: each item with its margin that goes out of a fixed side by more than `tolerance.overlap`.
30
+ * - `locked-kept`: each locked item that moved by more than `RECORD_EPS`, or that has no position.
31
+ * - `complete`: each item with no position that is not in the overflow.
32
+ *
33
+ * The rules read a shape of the container or of an obstacle as its bounding box. The result has no tolerance in it.
34
+ */
35
+ export function checkHard(input: PackInput, output: PackOutput, tolerance: Pick<Tolerance, "overlap"> = DEFAULT_TOLERANCE): HardBreaks {
36
+ const tol = tolerance.overlap;
37
+ const placements = new Map<string, PackOutput["placements"][number]>();
38
+ for (const p of output.placements) if (!placements.has(p.id)) placements.set(p.id, p);
39
+ const overflow = new Set(output.overflow);
40
+ const breaks: HardBreaks = { "no-overlap": 0, "in-container": 0, "locked-kept": 0, complete: 0 };
41
+
42
+ const boxes: Box[] = [];
43
+ for (const item of input.items) {
44
+ const p = placements.get(item.id);
45
+ if (p === undefined) {
46
+ if (item.locked === true && item.x !== undefined && item.y !== undefined) breaks["locked-kept"] += 1;
47
+ if (!overflow.has(item.id)) breaks.complete += 1;
48
+ continue;
49
+ }
50
+ if (item.locked === true && item.x !== undefined && item.y !== undefined) {
51
+ if (Math.abs(p.x - item.x) > RECORD_EPS || Math.abs(p.y - item.y) > RECORD_EPS) breaks["locked-kept"] += 1;
52
+ }
53
+ const placed: Item = p.turn === 90 ? turnItem(item) : item;
54
+ const m = marginOf(placed);
55
+ boxes.push({ l: p.x - m.left, t: p.y - m.top, r: p.x + placed.w + m.right, b: p.y + placed.h + m.bottom });
56
+ }
57
+
58
+ const moved = new Map(output.moved.map((m) => [m.id, m]));
59
+ const obstacles: Box[] = (input.obstacles ?? []).map((o) => {
60
+ const b = boundsOf(o.region);
61
+ const at = moved.get(o.id) ?? b;
62
+ return { l: at.x, t: at.y, r: at.x + b.w, b: at.y + b.h };
63
+ });
64
+
65
+ for (const [i, a] of boxes.entries()) {
66
+ for (let j = i + 1; j < boxes.length; j++) if (overlap(a, boxes[j]!, tol)) breaks["no-overlap"] += 1;
67
+ for (const o of obstacles) if (overlap(a, o, tol)) breaks["no-overlap"] += 1;
68
+ }
69
+
70
+ const { container } = input;
71
+ const shape = container.shape === undefined ? undefined : boundsOf(container.shape);
72
+ const x = shape?.x ?? 0;
73
+ const y = shape?.y ?? 0;
74
+ const w = shape?.w ?? container.w ?? Infinity;
75
+ const h = shape?.h ?? container.h ?? Infinity;
76
+ const fixedW = container.fixed === "size" || container.fixed === "width";
77
+ const fixedH = container.fixed === "size" || container.fixed === "height";
78
+ for (const box of boxes) {
79
+ const outX = fixedW && (box.l < x - tol || box.r > x + w + tol);
80
+ const outY = fixedH && (box.t < y - tol || box.b > y + h + tol);
81
+ if (outX || outY) breaks["in-container"] += 1;
82
+ }
83
+ return breaks;
84
+ }
@@ -0,0 +1,203 @@
1
+ // The data types of the Packer contract (docs/PACKER.md). They are plain data, with no class and no method.
2
+ // The shared package `@mark1russell7/layout-contract` gets these types later. Until then, this file is a local copy.
3
+
4
+ /** A region. The forms and the rules are those of `docs/FORMAT.md`. The y axis points down. */
5
+ export type Region =
6
+ | { kind: "rect"; x: number; y: number; w: number; h: number }
7
+ | { kind: "polygon"; points: [number, number][]; holes?: [number, number][][] }
8
+ | { kind: "circle"; cx: number; cy: number; r: number }
9
+ | { kind: "ellipse"; cx: number; cy: number; rx: number; ry: number };
10
+
11
+ /** The container of a packing. */
12
+ export interface Container {
13
+ /** The width. Without it, the packer searches the width. */
14
+ w?: number;
15
+ /** The height. Without it, the packer searches the height. */
16
+ h?: number;
17
+ /** The sides that stay: `size` keeps `w` and `h`, `width` keeps `w`, `height` keeps `h`, and `none` searches both. */
18
+ fixed: "size" | "width" | "height" | "none";
19
+ /** The target aspect (the width divided by the height) for the value `none` of `fixed`. The default is 1. */
20
+ aspect?: number;
21
+ /** The weight of the aspect error in the objective. The value 0 gives the least area. The default is 1. */
22
+ aspectWeight?: number;
23
+ /** The shape. Without it, the container is the rectangle from 0, 0 to `w`, `h`. */
24
+ shape?: Region;
25
+ /** The space between the border and the items with their margins. The default is 0. */
26
+ inset?: number;
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
+ }
76
+
77
+ /** The input of a packing: a `layout-input/1` file without `edges`, with `items` for `boxes`. */
78
+ export interface PackInput {
79
+ container: Container;
80
+ items: Item[];
81
+ obstacles?: Obstacle[];
82
+ /** The free space between two items with their margins. Routes use this space. The default is 0. */
83
+ gap?: number;
84
+ }
85
+
86
+ /** The position of one item after the packing. */
87
+ export interface Placement {
88
+ id: string;
89
+ x: number;
90
+ y: number;
91
+ /** The turn: 0 or 90, only for an item with `turnable` set to `true`. */
92
+ turn?: 0 | 90;
93
+ }
94
+
95
+ /** The facts of one stage of a packing. */
96
+ export interface StageTrace {
97
+ /** The id of the stage, for example `skyline` or `log-sweep`. */
98
+ id: string;
99
+ /** The time of the stage in milliseconds. */
100
+ ms: number;
101
+ /** The measures of the stage, by the names of `docs/FORMAT.md`. */
102
+ measures: Record<string, number>;
103
+ }
104
+
105
+ /** The output of a packing. The output does not change the input. */
106
+ export interface PackOutput {
107
+ placements: Placement[];
108
+ /** The ids of the items that the packer did not place. */
109
+ overflow: string[];
110
+ /** The size of the container after the packing. For a free size, the size that the search found. */
111
+ size: { w: number; h: number };
112
+ /** The new positions of the obstacles that moved. */
113
+ moved: { id: string; x: number; y: number }[];
114
+ /** The facts of each stage, for the site and the records. */
115
+ trace: StageTrace[];
116
+ }
117
+
118
+ /** The tolerances of the hard rules. */
119
+ export interface Tolerance {
120
+ /** The rule `no-overlap`: two boxes overlap only by more than this value. */
121
+ overlap: number;
122
+ /** The router rule that a route does not go through a box. */
123
+ through: number;
124
+ }
125
+
126
+ /** The tolerance for the comparison of two numbers in an engine. */
127
+ export const EPS = 1e-6;
128
+
129
+ /** The tolerance for the comparison of two numbers of a file. The files round to 3 decimals. */
130
+ export const RECORD_EPS = 1e-3;
131
+
132
+ /** The default tolerances of the hard rules. Each record writes its tolerances in its top-level field `tolerance`. */
133
+ export const DEFAULT_TOLERANCE: Readonly<Tolerance> = Object.freeze({ overlap: 0.001, through: 0.5 });
134
+
135
+ /** This function gives the margin of an item on each side. A number is the same margin on each side. */
136
+ export function marginOf(item: Pick<Item, "margin">): Margin {
137
+ const m = item.margin ?? 0;
138
+ if (typeof m === "number") return { top: m, right: m, bottom: m, left: m };
139
+ return { top: m.top, right: m.right, bottom: m.bottom, left: m.left };
140
+ }
141
+
142
+ /** The side of a port after a turn of 90 degrees clockwise. */
143
+ const TURNED_SIDE = { top: "right", right: "bottom", bottom: "left", left: "top" } as const;
144
+
145
+ /** This function gives the point `px`, `py` of an item of height `h` after a turn of 90 degrees clockwise. */
146
+ export function turnPoint(px: number, py: number, h: number): [number, number] {
147
+ return [h - py, px];
148
+ }
149
+
150
+ /** This function turns a port of an item of height `h` by 90 degrees clockwise. The side turns from top to right. */
151
+ export function turnPort(port: Port, h: number): Port {
152
+ const turned: Port = { id: port.id, side: TURNED_SIDE[port.side] };
153
+ if (port.x !== undefined && port.y !== undefined) {
154
+ const [x, y] = turnPoint(port.x, port.y, h);
155
+ turned.x = x;
156
+ turned.y = y;
157
+ } else if (port.x !== undefined) {
158
+ // Only x is set: x becomes the new y, and the router chooses the new x
159
+ turned.y = port.x;
160
+ } else if (port.y !== undefined) {
161
+ // Only y is set: h - y becomes the new x, and the router chooses the new y
162
+ turned.x = h - port.y;
163
+ }
164
+ return turned;
165
+ }
166
+
167
+ /** This function turns a region relative to an item of height `h` by 90 degrees clockwise. */
168
+ export function turnRegion(region: Region, h: number): Region {
169
+ switch (region.kind) {
170
+ case "rect":
171
+ return { kind: "rect", x: h - region.y - region.h, y: region.x, w: region.h, h: region.w };
172
+ case "circle": {
173
+ const [cx, cy] = turnPoint(region.cx, region.cy, h);
174
+ return { kind: "circle", cx, cy, r: region.r };
175
+ }
176
+ case "ellipse": {
177
+ const [cx, cy] = turnPoint(region.cx, region.cy, h);
178
+ return { kind: "ellipse", cx, cy, rx: region.ry, ry: region.rx };
179
+ }
180
+ case "polygon": {
181
+ const turnRing = (ring: [number, number][]): [number, number][] => ring.map(([x, y]) => turnPoint(x, y, h));
182
+ const turned: Region = { kind: "polygon", points: turnRing(region.points) };
183
+ if (region.holes) turned.holes = region.holes.map(turnRing);
184
+ return turned;
185
+ }
186
+ }
187
+ }
188
+
189
+ /**
190
+ * This function gives an item after a turn of 90 degrees clockwise, with the y axis down (decision 1 of
191
+ * `docs/PACKER.md`). The width and the height change places. The top-left corner, the id and the other fields stay.
192
+ * The ports, the shape and the margin turn with the item. The function does not change its argument.
193
+ */
194
+ export function turnItem(item: Item): Item {
195
+ const turned: Item = { ...item, w: item.h, h: item.w };
196
+ if (item.ports) turned.ports = item.ports.map((p) => turnPort(p, item.h));
197
+ if (item.shape) turned.shape = turnRegion(item.shape, item.h);
198
+ if (item.margin !== undefined && typeof item.margin !== "number") {
199
+ const m = item.margin;
200
+ turned.margin = { top: m.left, right: m.top, bottom: m.right, left: m.bottom };
201
+ }
202
+ return turned;
203
+ }
package/src/convert.ts ADDED
@@ -0,0 +1,51 @@
1
+ import type { Item, PackInput, PackOutput } from "./contract.ts";
2
+ import type { Rect } from "./rect.ts";
3
+
4
+ /**
5
+ * This function makes a `PackInput` from the `Rect` objects of 0.1. The id of each item is the index of its rectangle.
6
+ *
7
+ * - `fixedSize` gives `fixed` set to `size`, `fixedWidth` gives `width`, and an outer rectangle without them gives `none`.
8
+ * - A locked rectangle gives a locked item at its position.
9
+ * - An outer rectangle at a position other than 0, 0 gives a rectangle shape at that position.
10
+ */
11
+ export function fromRects<T>(rects: readonly Rect<T>[], outer: Rect<T>): PackInput {
12
+ const fixed = outer.fixedSize ? "size" : outer.fixedWidth ? "width" : "none";
13
+ const container: PackInput["container"] = { fixed };
14
+ if (fixed !== "none") container.w = outer.size.x;
15
+ if (fixed === "size") container.h = outer.size.y;
16
+ if (outer.position.x !== 0 || outer.position.y !== 0) {
17
+ container.shape = { kind: "rect", x: outer.position.x, y: outer.position.y, w: outer.size.x, h: outer.size.y };
18
+ }
19
+ const items = rects.map((r, i): Item => {
20
+ const item: Item = { id: String(i), w: r.size.x, h: r.size.y };
21
+ if (r.locked) {
22
+ item.locked = true;
23
+ item.x = r.position.x;
24
+ item.y = r.position.y;
25
+ }
26
+ return item;
27
+ });
28
+ return { container, items };
29
+ }
30
+
31
+ /**
32
+ * This function writes an output back into the `Rect` objects of `fromRects`. It sets the position and `wasPacked` of
33
+ * each rectangle, and the free sides of the outer rectangle. It gives the outer rectangle.
34
+ */
35
+ export function toRects<T>(output: PackOutput, rects: Rect<T>[], outer: Rect<T>): Rect<T> {
36
+ const placements = new Map(output.placements.map((p) => [p.id, p]));
37
+ for (const [i, r] of rects.entries()) {
38
+ const p = placements.get(String(i));
39
+ if (p === undefined) {
40
+ r.wasPacked = r.locked;
41
+ continue;
42
+ }
43
+ r.position.set(p.x, p.y);
44
+ r.wasPacked = true;
45
+ }
46
+ if (!outer.fixedSize) {
47
+ if (!outer.fixedWidth) outer.size.x = output.size.w;
48
+ outer.size.y = output.size.h;
49
+ }
50
+ return outer;
51
+ }