@mark1russell7/packing 0.1.1 → 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.
- package/README.md +50 -0
- package/build/index.d.ts +391 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1089 -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/frame.ts +262 -0
- package/src/free.ts +153 -0
- package/src/index.ts +42 -0
- package/src/pack.ts +2 -1
- package/src/registry.ts +23 -0
- package/src/strategy.ts +214 -0
package/src/frame.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// The internal frame of the adapters of 0.1: the conversion of a PackInput to the Rect objects of 0.1, and back.
|
|
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
|
+
// 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
|
+
|
|
5
|
+
import { EPS, marginOf, type Item, type Obstacle, type PackInput, type PackOutput, type Placement, type Region, type StageTrace } from "./contract.ts";
|
|
6
|
+
import { Rect } from "./rect.ts";
|
|
7
|
+
|
|
8
|
+
const clock = (globalThis as { performance?: { now(): number } }).performance;
|
|
9
|
+
|
|
10
|
+
/** This function gives the time in milliseconds. */
|
|
11
|
+
export function now(): number {
|
|
12
|
+
return clock ? clock.now() : Date.now();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** This function gives the bounding box of a region. */
|
|
16
|
+
export function boundsOf(region: Region): { x: number; y: number; w: number; h: number } {
|
|
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
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The rectangle of a container: its origin and its size. A free side has the size `Infinity`. */
|
|
35
|
+
export interface Bounds {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
w: number;
|
|
39
|
+
h: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** This function gives the rectangle of a container. A rectangle shape gives the origin and the sizes of the fixed sides. */
|
|
43
|
+
export function containerBounds(input: PackInput): Bounds {
|
|
44
|
+
const { container } = input;
|
|
45
|
+
const shape = container.shape === undefined ? undefined : boundsOf(container.shape);
|
|
46
|
+
const fixedW = container.fixed === "size" || container.fixed === "width";
|
|
47
|
+
const fixedH = container.fixed === "size" || container.fixed === "height";
|
|
48
|
+
const w = shape?.w ?? container.w;
|
|
49
|
+
const h = shape?.h ?? container.h;
|
|
50
|
+
if (fixedW && w === undefined) throw new TypeError(`A container with fixed "${container.fixed}" needs a width.`);
|
|
51
|
+
if (fixedH && h === undefined) throw new TypeError(`A container with fixed "${container.fixed}" needs a height.`);
|
|
52
|
+
return { x: shape?.x ?? 0, y: shape?.y ?? 0, w: fixedW ? w! : Infinity, h: fixedH ? h! : Infinity };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One rectangle of the frame, and the item or the obstacle that it measures. */
|
|
56
|
+
export interface Entry {
|
|
57
|
+
rect: Rect<number>;
|
|
58
|
+
item?: Item;
|
|
59
|
+
obstacle?: Obstacle;
|
|
60
|
+
/** The offset from the corner of the rectangle to the corner of the item or the obstacle. */
|
|
61
|
+
dx: number;
|
|
62
|
+
dy: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The rectangles of 0.1 for one input. */
|
|
66
|
+
export interface Frame {
|
|
67
|
+
/** The container coordinates of the origin of the frame. */
|
|
68
|
+
ox: number;
|
|
69
|
+
oy: number;
|
|
70
|
+
/** The size of the area of the frame. A free side has the size `Infinity`. */
|
|
71
|
+
w: number;
|
|
72
|
+
h: number;
|
|
73
|
+
bounds: Bounds;
|
|
74
|
+
inset: number;
|
|
75
|
+
/** The items in the order of their rank, then the obstacles. */
|
|
76
|
+
entries: Entry[];
|
|
77
|
+
/** The ids of the items that are too large for the area. The frame has no rectangle for them. */
|
|
78
|
+
tooLarge: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** This function gives the items in the order of their rank. The index in the array is the rank of an item without one. */
|
|
82
|
+
export function byRank(items: readonly Item[]): Item[] {
|
|
83
|
+
return items
|
|
84
|
+
.map((item, index) => ({ item, rank: item.rank ?? index }))
|
|
85
|
+
.toSorted((a, b) => a.rank - b.rank)
|
|
86
|
+
.map((e) => e.item);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* This function makes the frame of an input. A locked item with a position and an obstacle without `move` are locked
|
|
91
|
+
* rectangles. An obstacle with `move` is a free rectangle only when `movable` is `true`.
|
|
92
|
+
*/
|
|
93
|
+
export function buildFrame(input: PackInput, movable = false): Frame {
|
|
94
|
+
const bounds = containerBounds(input);
|
|
95
|
+
const inset = input.container.inset ?? 0;
|
|
96
|
+
const half = (input.gap ?? 0) / 2;
|
|
97
|
+
const ox = bounds.x + inset - half;
|
|
98
|
+
const oy = bounds.y + inset - half;
|
|
99
|
+
const w = bounds.w - 2 * inset + 2 * half;
|
|
100
|
+
const h = bounds.h - 2 * inset + 2 * half;
|
|
101
|
+
const entries: Entry[] = [];
|
|
102
|
+
const tooLarge: string[] = [];
|
|
103
|
+
|
|
104
|
+
for (const item of byRank(input.items)) {
|
|
105
|
+
const m = marginOf(item);
|
|
106
|
+
const rect = new Rect<number>();
|
|
107
|
+
rect.size.set(item.w + m.left + m.right + 2 * half, item.h + m.top + m.bottom + 2 * half);
|
|
108
|
+
const dx = m.left + half;
|
|
109
|
+
const dy = m.top + half;
|
|
110
|
+
if (item.locked === true && item.x !== undefined && item.y !== undefined) {
|
|
111
|
+
rect.locked = true;
|
|
112
|
+
rect.position.set(item.x - dx - ox, item.y - dy - oy);
|
|
113
|
+
} else if (rect.size.x > w + EPS || rect.size.y > h + EPS) {
|
|
114
|
+
tooLarge.push(item.id);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
entries.push({ rect, item, dx, dy });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const obstacle of input.obstacles ?? []) {
|
|
121
|
+
const b = boundsOf(obstacle.region);
|
|
122
|
+
const rect = new Rect<number>();
|
|
123
|
+
rect.size.set(b.w + 2 * half, b.h + 2 * half);
|
|
124
|
+
rect.position.set(b.x - half - ox, b.y - half - oy);
|
|
125
|
+
rect.locked = !(movable && obstacle.move !== undefined);
|
|
126
|
+
entries.push({ rect, obstacle, dx: half, dy: half });
|
|
127
|
+
}
|
|
128
|
+
return { ox, oy, w, h, bounds, inset, entries, tooLarge };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** This function gives the outer rectangle of 0.1 for a frame. A free height starts at 0 and grows. */
|
|
132
|
+
export function outerOf(frame: Frame): Rect<number> {
|
|
133
|
+
const outer = new Rect<number>();
|
|
134
|
+
const fixedSize = Number.isFinite(frame.h);
|
|
135
|
+
outer.size.set(frame.w, fixedSize ? frame.h : 0);
|
|
136
|
+
outer.fixedWidth = true;
|
|
137
|
+
outer.fixedSize = fixedSize;
|
|
138
|
+
return outer;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** This function gives the rectangles of a frame, in the order of the entries. */
|
|
142
|
+
export function rectsOf(frame: Frame): Rect<number>[] {
|
|
143
|
+
return frame.entries.map((e) => e.rect);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* This function reads the output from the rectangles of a frame after a packing of 0.1. A locked item keeps its
|
|
148
|
+
* position exactly. Each other item that the packer did not place goes to the overflow.
|
|
149
|
+
*/
|
|
150
|
+
export function readFrame(input: PackInput, frame: Frame, trace: StageTrace[], moved: PackOutput["moved"] = []): PackOutput {
|
|
151
|
+
const placements: Placement[] = [];
|
|
152
|
+
const overflow: string[] = [...frame.tooLarge];
|
|
153
|
+
for (const e of frame.entries) {
|
|
154
|
+
const item = e.item;
|
|
155
|
+
if (item === undefined) continue;
|
|
156
|
+
if (item.locked === true && item.x !== undefined && item.y !== undefined) placements.push({ id: item.id, x: item.x, y: item.y });
|
|
157
|
+
else if (e.rect.wasPacked) placements.push({ id: item.id, x: e.rect.position.x + e.dx + frame.ox, y: e.rect.position.y + e.dy + frame.oy });
|
|
158
|
+
else overflow.push(item.id);
|
|
159
|
+
}
|
|
160
|
+
return { placements, overflow, size: sizeOf(input, frame, placements, moved), moved, trace };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* This function gives the size of the container after a packing. A fixed side keeps its size. A free side goes to the
|
|
165
|
+
* far edge of the items with their margins and the inset, or of the obstacles.
|
|
166
|
+
*/
|
|
167
|
+
function sizeOf(input: PackInput, frame: Frame, placements: Placement[], moved: PackOutput["moved"]): { w: number; h: number } {
|
|
168
|
+
const items = new Map(input.items.map((i) => [i.id, i]));
|
|
169
|
+
let right = 0;
|
|
170
|
+
let bottom = 0;
|
|
171
|
+
for (const p of placements) {
|
|
172
|
+
const item = items.get(p.id)!;
|
|
173
|
+
const m = marginOf(item);
|
|
174
|
+
right = Math.max(right, p.x + item.w + m.right + frame.inset - frame.bounds.x);
|
|
175
|
+
bottom = Math.max(bottom, p.y + item.h + m.bottom + frame.inset - frame.bounds.y);
|
|
176
|
+
}
|
|
177
|
+
const movedById = new Map(moved.map((m) => [m.id, m]));
|
|
178
|
+
for (const o of input.obstacles ?? []) {
|
|
179
|
+
const b = boundsOf(o.region);
|
|
180
|
+
const at = movedById.get(o.id) ?? b;
|
|
181
|
+
right = Math.max(right, at.x + b.w - frame.bounds.x);
|
|
182
|
+
bottom = Math.max(bottom, at.y + b.h - frame.bounds.y);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
w: Number.isFinite(frame.bounds.w) ? frame.bounds.w : right,
|
|
186
|
+
h: Number.isFinite(frame.bounds.h) ? frame.bounds.h : bottom,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** This function gives a region with the axes changed: x for y, and w for h. */
|
|
191
|
+
export function transposeRegion(region: Region): Region {
|
|
192
|
+
switch (region.kind) {
|
|
193
|
+
case "rect":
|
|
194
|
+
return { kind: "rect", x: region.y, y: region.x, w: region.h, h: region.w };
|
|
195
|
+
case "circle":
|
|
196
|
+
return { kind: "circle", cx: region.cy, cy: region.cx, r: region.r };
|
|
197
|
+
case "ellipse":
|
|
198
|
+
return { kind: "ellipse", cx: region.cy, cy: region.cx, rx: region.ry, ry: region.rx };
|
|
199
|
+
case "polygon": {
|
|
200
|
+
const swap = (ring: [number, number][]): [number, number][] => ring.map(([x, y]) => [y, x]);
|
|
201
|
+
const turned: Region = { kind: "polygon", points: swap(region.points) };
|
|
202
|
+
if (region.holes) turned.holes = region.holes.map(swap);
|
|
203
|
+
return turned;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** This function gives an item with the axes changed. The ports go, because the adapters of 0.1 do not read them. */
|
|
209
|
+
function transposeItem(item: Item): Item {
|
|
210
|
+
const { ports: _ports, ...rest } = item;
|
|
211
|
+
const turned: Item = { ...rest, w: item.h, h: item.w };
|
|
212
|
+
delete turned.x;
|
|
213
|
+
delete turned.y;
|
|
214
|
+
if (item.y !== undefined) turned.x = item.y;
|
|
215
|
+
if (item.x !== undefined) turned.y = item.x;
|
|
216
|
+
if (item.shape) turned.shape = transposeRegion(item.shape);
|
|
217
|
+
if (item.margin !== undefined && typeof item.margin !== "number") {
|
|
218
|
+
const m = item.margin;
|
|
219
|
+
turned.margin = { top: m.left, right: m.bottom, bottom: m.right, left: m.top };
|
|
220
|
+
}
|
|
221
|
+
return turned;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** This function gives an input with the axes changed. A fixed height becomes a fixed width. */
|
|
225
|
+
export function transposeInput(input: PackInput): PackInput {
|
|
226
|
+
const c = input.container;
|
|
227
|
+
const container: PackInput["container"] = { fixed: c.fixed === "height" ? "width" : c.fixed === "width" ? "height" : c.fixed };
|
|
228
|
+
if (c.h !== undefined) container.w = c.h;
|
|
229
|
+
if (c.w !== undefined) container.h = c.w;
|
|
230
|
+
if (c.aspect !== undefined) container.aspect = 1 / c.aspect;
|
|
231
|
+
if (c.aspectWeight !== undefined) container.aspectWeight = c.aspectWeight;
|
|
232
|
+
if (c.shape !== undefined) container.shape = transposeRegion(c.shape);
|
|
233
|
+
if (c.inset !== undefined) container.inset = c.inset;
|
|
234
|
+
const turned: PackInput = { container, items: input.items.map(transposeItem) };
|
|
235
|
+
if (input.obstacles) turned.obstacles = input.obstacles.map((o) => ({ ...o, region: transposeRegion(o.region) }));
|
|
236
|
+
if (input.gap !== undefined) turned.gap = input.gap;
|
|
237
|
+
return turned;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** This function gives an output with the axes changed. */
|
|
241
|
+
export function transposeOutput(output: PackOutput): PackOutput {
|
|
242
|
+
return {
|
|
243
|
+
placements: output.placements.map((p) => ({ ...p, x: p.y, y: p.x })),
|
|
244
|
+
overflow: [...output.overflow],
|
|
245
|
+
size: { w: output.size.h, h: output.size.w },
|
|
246
|
+
moved: output.moved.map((m) => ({ id: m.id, x: m.y, y: m.x })),
|
|
247
|
+
trace: output.trace,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* This function packs an input with a fixed height through the turn of the axes. It changes x for y in the input.
|
|
253
|
+
* Then it packs with a fixed width, and changes the axes of the output back.
|
|
254
|
+
*/
|
|
255
|
+
export function viaAxes(input: PackInput, previous: PackOutput | undefined, run: (turned: PackInput, previous: PackOutput | undefined) => PackOutput): PackOutput {
|
|
256
|
+
const start = now();
|
|
257
|
+
const turned = transposeInput(input);
|
|
258
|
+
const inner = run(turned, previous === undefined ? undefined : transposeOutput(previous));
|
|
259
|
+
const output = transposeOutput(inner);
|
|
260
|
+
output.trace = [{ id: "axes", ms: now() - start, measures: {} }, ...inner.trace];
|
|
261
|
+
return output;
|
|
262
|
+
}
|
package/src/free.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { pack, type PackLayoutOptions } from "./pack.ts";
|
|
2
|
+
import { Rect } from "./rect.ts";
|
|
3
|
+
|
|
4
|
+
/** The options of `packFree` */
|
|
5
|
+
export interface FreeWidthOptions extends PackLayoutOptions {
|
|
6
|
+
/** The target aspect: the width divided by the height. The default is 1, a square. */
|
|
7
|
+
aspect?: number;
|
|
8
|
+
/**
|
|
9
|
+
* The weight of the aspect error in the objective. The objective is the area times `1 + aspectWeight * aspectError`.
|
|
10
|
+
* The default is 1. With 0, only the area counts.
|
|
11
|
+
*/
|
|
12
|
+
aspectWeight?: number;
|
|
13
|
+
/** The number of widths that the first pass tries. The default is 48. */
|
|
14
|
+
samples?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** One width that `packFree` tried, and its result */
|
|
18
|
+
export interface FreeWidthTry {
|
|
19
|
+
/** The width of the container for this try */
|
|
20
|
+
width: number;
|
|
21
|
+
/** The width of the content: the right edge of the rectangles */
|
|
22
|
+
contentWidth: number;
|
|
23
|
+
/** The height of the content */
|
|
24
|
+
contentHeight: number;
|
|
25
|
+
/** The objective of this try. Lower is better. */
|
|
26
|
+
objective: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The result of `packFree` */
|
|
30
|
+
export interface FreeWidthResult<T> {
|
|
31
|
+
/** The outer rectangle, with the size of the content */
|
|
32
|
+
outer: Rect<T>;
|
|
33
|
+
/** The area of the bounding box of the content */
|
|
34
|
+
area: number;
|
|
35
|
+
/** The area of the rectangles divided by the area of the bounding box */
|
|
36
|
+
fill: number;
|
|
37
|
+
/** The aspect of the bounding box: the width divided by the height */
|
|
38
|
+
aspect: number;
|
|
39
|
+
/** The distance from the target aspect: the absolute value of `log(aspect) - log(target)` */
|
|
40
|
+
aspectError: number;
|
|
41
|
+
/** The objective of the chosen width. Lower is better. */
|
|
42
|
+
objective: number;
|
|
43
|
+
/** Each width that the search tried, in the order of the search */
|
|
44
|
+
tried: FreeWidthTry[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** This function gives the distance between an aspect and a target aspect on a log scale. */
|
|
48
|
+
export function aspectError(width: number, height: number, target = 1): number {
|
|
49
|
+
if (!(width > 0 && height > 0 && target > 0)) return Infinity;
|
|
50
|
+
return Math.abs(Math.log(width / height) - Math.log(target));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This function packs rectangles with a free width. It tries many widths with the strategy of `options`, and keeps
|
|
55
|
+
* the width with the lowest objective: the area of the bounding box times `1 + aspectWeight * aspectError`.
|
|
56
|
+
*
|
|
57
|
+
* - The first pass tries `samples` widths on a log scale, from the widest rectangle to the width of one row.
|
|
58
|
+
* - The second pass tries the widths between the two neighbours of the best width of the first pass.
|
|
59
|
+
* - The search is deterministic: the same input gives the same widths and the same result.
|
|
60
|
+
* - A `locked` rectangle keeps its position. The width is not less than its right edge.
|
|
61
|
+
* - Each strategy of `pack` works with this function, because it only changes the width.
|
|
62
|
+
*/
|
|
63
|
+
export function packFree<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), options: FreeWidthOptions = {}): FreeWidthResult<T> {
|
|
64
|
+
const target = options.aspect ?? 1;
|
|
65
|
+
const weight = options.aspectWeight ?? 1;
|
|
66
|
+
const samples = Math.max(2, Math.floor(options.samples ?? 48));
|
|
67
|
+
const ox = outer.position.x;
|
|
68
|
+
const oy = outer.position.y;
|
|
69
|
+
|
|
70
|
+
let itemArea = 0;
|
|
71
|
+
let minWidth = 0;
|
|
72
|
+
let rowWidth = 0;
|
|
73
|
+
for (const r of rects) {
|
|
74
|
+
if (r.locked) {
|
|
75
|
+
minWidth = Math.max(minWidth, r.right() - ox);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!(r.size.x > 0 && r.size.y > 0)) continue;
|
|
79
|
+
itemArea += r.area();
|
|
80
|
+
minWidth = Math.max(minWidth, r.size.x);
|
|
81
|
+
rowWidth += r.size.x;
|
|
82
|
+
}
|
|
83
|
+
for (const r of rects) if (r.locked) itemArea += r.area();
|
|
84
|
+
const maxWidth = Math.max(minWidth, rowWidth);
|
|
85
|
+
|
|
86
|
+
const tried: FreeWidthTry[] = [];
|
|
87
|
+
const run = (width: number): FreeWidthTry => {
|
|
88
|
+
Rect.resetPacked(rects);
|
|
89
|
+
const box = new Rect<T>();
|
|
90
|
+
box.position.setTo(outer.position);
|
|
91
|
+
box.size.set(width, 0);
|
|
92
|
+
box.fixedWidth = true;
|
|
93
|
+
pack(rects, box, options);
|
|
94
|
+
const placed = Rect.packed(rects);
|
|
95
|
+
const contentWidth = Math.max(0, Rect.maxRight(placed) - ox);
|
|
96
|
+
const contentHeight = Math.max(0, Rect.maxBottom(placed) - oy);
|
|
97
|
+
const area = contentWidth * contentHeight;
|
|
98
|
+
const objective = area > 0 ? area * (1 + weight * aspectError(contentWidth, contentHeight, target)) : 0;
|
|
99
|
+
const t = { width, contentWidth, contentHeight, objective };
|
|
100
|
+
tried.push(t);
|
|
101
|
+
return t;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// The first pass: widths on a log scale. Each width is a whole number when the sizes are whole numbers.
|
|
105
|
+
const widths: number[] = [];
|
|
106
|
+
if (maxWidth > 0) {
|
|
107
|
+
const ratio = maxWidth / minWidth;
|
|
108
|
+
for (let i = 0; i < samples; i++) {
|
|
109
|
+
const w = minWidth * ratio ** (i / (samples - 1));
|
|
110
|
+
const rounded = i === samples - 1 ? maxWidth : Math.max(minWidth, Math.round(w));
|
|
111
|
+
if (widths.at(-1) !== rounded) widths.push(rounded);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
let best: FreeWidthTry | undefined = undefined;
|
|
115
|
+
let bestIndex = 0;
|
|
116
|
+
for (const [i, w] of widths.entries()) {
|
|
117
|
+
const t = run(w);
|
|
118
|
+
if (best === undefined || t.objective < best.objective) {
|
|
119
|
+
best = t;
|
|
120
|
+
bestIndex = i;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The second pass: the widths between the two neighbours of the best width
|
|
125
|
+
if (widths.length > 2) {
|
|
126
|
+
const low = widths[Math.max(0, bestIndex - 1)]!;
|
|
127
|
+
const high = widths[Math.min(widths.length - 1, bestIndex + 1)]!;
|
|
128
|
+
const steps = Math.min(Math.floor(samples / 2), Math.max(0, Math.round(high - low) - 1));
|
|
129
|
+
for (let i = 1; i <= steps; i++) {
|
|
130
|
+
const w = Math.round(low + ((high - low) * i) / (steps + 1));
|
|
131
|
+
if (widths.includes(w)) continue;
|
|
132
|
+
const t = run(w);
|
|
133
|
+
if (best === undefined || t.objective < best.objective) best = t;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The last run packs with the best width, so the positions of the rectangles are those of the best width
|
|
138
|
+
const chosen = best === undefined ? undefined : run(best.width);
|
|
139
|
+
tried.pop();
|
|
140
|
+
const w = chosen?.contentWidth ?? 0;
|
|
141
|
+
const h = chosen?.contentHeight ?? 0;
|
|
142
|
+
if (!outer.fixedSize) outer.size.set(w, h);
|
|
143
|
+
const area = w * h;
|
|
144
|
+
return {
|
|
145
|
+
outer,
|
|
146
|
+
area,
|
|
147
|
+
fill: area > 0 ? itemArea / area : 0,
|
|
148
|
+
aspect: h > 0 ? w / h : 0,
|
|
149
|
+
aspectError: aspectError(w, h, target),
|
|
150
|
+
objective: chosen?.objective ?? 0,
|
|
151
|
+
tried,
|
|
152
|
+
};
|
|
153
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,48 @@ export { Rect } from "./rect.ts";
|
|
|
3
3
|
export { pack, packRows, type PackOptions, type PackLayoutOptions } from "./pack.ts";
|
|
4
4
|
export { packSkyline, lowestWindow } from "./skyline.ts";
|
|
5
5
|
export { packShelf, type ShelfOptions } from "./shelf.ts";
|
|
6
|
+
export { packFree, aspectError, type FreeWidthOptions, type FreeWidthResult, type FreeWidthTry } from "./free.ts";
|
|
6
7
|
export { compareRects, SORT_ORDERS, type SortOrder } from "./order.ts";
|
|
7
8
|
export { pushCascade, type PushOptions } from "./push.ts";
|
|
8
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";
|
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
|
+
}
|