@mark1russell7/packing 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/build/index.d.ts +63 -26
- package/build/index.d.ts.map +1 -1
- package/build/index.js +312 -19
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters.ts +30 -5
- package/src/free.ts +3 -0
- package/src/index.ts +1 -1
- package/src/measures.ts +35 -3
- package/src/moves.ts +47 -0
- package/src/pack.ts +18 -4
- package/src/shelf.ts +203 -0
- package/src/skyline.ts +34 -7
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.2.
|
|
4
|
+
"version": "0.2.4",
|
|
5
5
|
"description": "Rectangle packing: shelf, guillotine and skyline strategies, push cascades and measures of a layout.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"rectangle-packing",
|
package/src/adapters.ts
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
import { EPS, type PackInput, type PackOutput } from "./contract.ts";
|
|
4
4
|
import { boundsOf, buildFrame, now, outerOf, readFrame, rectsOf, viaAxes, type Entry } from "./frame.ts";
|
|
5
5
|
import { packFree } from "./free.ts";
|
|
6
|
+
import { clearMoves, moveCount, MOVES } from "./moves.ts";
|
|
6
7
|
import type { SortOrder } from "./order.ts";
|
|
7
8
|
import { pack } from "./pack.ts";
|
|
8
9
|
import { pushCascade } from "./push.ts";
|
|
9
10
|
import { Rect } from "./rect.ts";
|
|
11
|
+
import type { ShelfFit } from "./shelf.ts";
|
|
10
12
|
import { packSkyline } from "./skyline.ts";
|
|
11
13
|
import { spaceOf } from "./space.ts";
|
|
12
14
|
import { assertSupported, cellOf, UnsupportedCell, type PackContext, type SizeSearch, type Strategy, type Supports } from "./strategy.ts";
|
|
@@ -17,6 +19,11 @@ export interface LayoutStrategyOptions {
|
|
|
17
19
|
sortOrder: SortOrder;
|
|
18
20
|
/** The number of widths that the first pass of the size search `log-sweep` tries. */
|
|
19
21
|
samples: number;
|
|
22
|
+
/**
|
|
23
|
+
* The rule of `shelf` to choose a shelf: `next`, `first` or `best`. Only `shelf` reads it. Without it, `shelf` packs
|
|
24
|
+
* as in 0.2.3. With it, the trace also counts `move.firstFit` and `move.bestFit`.
|
|
25
|
+
*/
|
|
26
|
+
fit?: ShelfFit;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
/** The options of the strategy `push`. */
|
|
@@ -54,14 +61,28 @@ function supportsOf(cells: { [D in keyof Supports]: readonly Supports[D][number]
|
|
|
54
61
|
};
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
/** This function gives the fit of the options of a strategy: only `shelf` reads it. */
|
|
65
|
+
function fitOf(id: "rows" | "skyline" | "shelf", options: Partial<LayoutStrategyOptions>): { fit?: ShelfFit } {
|
|
66
|
+
return id === "shelf" && options.fit !== undefined ? { fit: options.fit } : {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** This function gives the move counts of the trace of a strategy: its own move, and the moves of a shelf fit. */
|
|
70
|
+
function movesOf(id: "rows" | "skyline" | "shelf", fit: { fit?: ShelfFit }): Record<string, number> {
|
|
71
|
+
const out: Record<string, number> = { [MOVES[id]]: moveCount(MOVES[id]) };
|
|
72
|
+
if (fit.fit !== undefined) for (const move of [MOVES.firstFit, MOVES.bestFit]) out[move] = moveCount(move);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
/** This function packs an input with a fixed size or a fixed width with one strategy of `pack`. */
|
|
58
77
|
function packFixedWidth(id: "rows" | "skyline" | "shelf", input: PackInput, options: LayoutStrategyOptions): PackOutput {
|
|
59
78
|
const start = now();
|
|
60
79
|
const frame = buildFrame(input);
|
|
61
80
|
const rects = rectsOf(frame);
|
|
62
|
-
|
|
81
|
+
const fit = fitOf(id, options);
|
|
82
|
+
clearMoves();
|
|
83
|
+
if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder, space: spaceOf(input, frame), ...fit });
|
|
63
84
|
const output = readFrame(input, frame, []);
|
|
64
|
-
return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length } }] };
|
|
85
|
+
return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length, ...movesOf(id, fit) } }] };
|
|
65
86
|
}
|
|
66
87
|
|
|
67
88
|
/** This function makes the strategy of one function of `pack`. */
|
|
@@ -116,6 +137,7 @@ export const skyline: Strategy<LayoutStrategyOptions> = layoutStrategy("skyline"
|
|
|
116
137
|
/**
|
|
117
138
|
* The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
|
|
118
139
|
* With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
|
|
140
|
+
* The option `fit` selects a shelf rule of roadmap item k21: `next`, `first` or `best`. The default has no `fit`.
|
|
119
141
|
*/
|
|
120
142
|
export const shelf: Strategy<LayoutStrategyOptions> = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
|
|
121
143
|
obstacleMove: ["fixed"],
|
|
@@ -135,6 +157,7 @@ export const logSweep: SizeSearch = {
|
|
|
135
157
|
const o = { ...(inner.defaults as LayoutStrategyOptions), ...(options as Partial<LayoutStrategyOptions> | undefined) };
|
|
136
158
|
const start = now();
|
|
137
159
|
const frame = buildFrame(input);
|
|
160
|
+
const fit = fitOf(id, o);
|
|
138
161
|
const result = packFree(rectsOf(frame), new Rect<number>(), {
|
|
139
162
|
strategy: id,
|
|
140
163
|
sortOrder: o.sortOrder,
|
|
@@ -142,6 +165,7 @@ export const logSweep: SizeSearch = {
|
|
|
142
165
|
samples: o.samples,
|
|
143
166
|
aspect: input.container.aspect ?? 1,
|
|
144
167
|
aspectWeight: input.container.aspectWeight ?? 1,
|
|
168
|
+
...fit,
|
|
145
169
|
});
|
|
146
170
|
const ms = now() - start;
|
|
147
171
|
const runs = result.tried.length + 1;
|
|
@@ -150,7 +174,7 @@ export const logSweep: SizeSearch = {
|
|
|
150
174
|
...output,
|
|
151
175
|
trace: [
|
|
152
176
|
{ id: "log-sweep", ms, measures: { "pack.search.widths": result.tried.length, "pack.search.ms": ms } },
|
|
153
|
-
{ id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length } },
|
|
177
|
+
{ id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length, ...movesOf(id, fit) } },
|
|
154
178
|
],
|
|
155
179
|
};
|
|
156
180
|
},
|
|
@@ -191,8 +215,9 @@ function pushFixedWidth(input: PackInput, options: PushStrategyOptions, previous
|
|
|
191
215
|
// The cascade: each item in the order of its rank pushes the others, and then it stays
|
|
192
216
|
const inPlay = frame.entries.filter((e) => !pending.includes(e)).map((e) => e.rect);
|
|
193
217
|
const maxShoves = options.maxShovesPerItem * Math.max(1, inPlay.length);
|
|
218
|
+
let cascaded = 0;
|
|
194
219
|
for (const e of positioned) {
|
|
195
|
-
pushCascade(e.rect, inPlay, { gap: 0, maxShoves });
|
|
220
|
+
cascaded += pushCascade(e.rect, inPlay, { gap: 0, maxShoves }).length;
|
|
196
221
|
e.rect.locked = true;
|
|
197
222
|
}
|
|
198
223
|
|
|
@@ -234,7 +259,7 @@ function pushFixedWidth(input: PackInput, options: PushStrategyOptions, previous
|
|
|
234
259
|
return {
|
|
235
260
|
...output,
|
|
236
261
|
trace: [
|
|
237
|
-
{ id: "push", ms: pushMs, measures: { "pack.push.moved": moved.length } },
|
|
262
|
+
{ id: "push", ms: pushMs, measures: { "pack.push.moved": moved.length, [MOVES.push]: cascaded } },
|
|
238
263
|
{ id: "skyline", ms: now() - skylineStart, measures: { "pack.push.replaced": pending.length, "pack.overflow": output.overflow.length } },
|
|
239
264
|
],
|
|
240
265
|
};
|
package/src/free.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { clearMoves } from "./moves.ts";
|
|
1
2
|
import { pack, type PackLayoutOptions } from "./pack.ts";
|
|
2
3
|
import { Rect } from "./rect.ts";
|
|
3
4
|
|
|
@@ -85,6 +86,8 @@ export function packFree<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), optio
|
|
|
85
86
|
|
|
86
87
|
const tried: FreeWidthTry[] = [];
|
|
87
88
|
const run = (width: number): FreeWidthTry => {
|
|
89
|
+
// The counts of the moves come from the last run: the run with the best width
|
|
90
|
+
clearMoves();
|
|
88
91
|
Rect.resetPacked(rects);
|
|
89
92
|
const box = new Rect<T>();
|
|
90
93
|
box.position.setTo(outer.position);
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { Rect } from "./rect.ts";
|
|
|
3
3
|
export { pack, packRows, type PackOptions, type PackLayoutOptions } from "./pack.ts";
|
|
4
4
|
export { packSkyline, lowestWindow, type SkylineOptions } from "./skyline.ts";
|
|
5
5
|
export { freeSpace, type FreeSpace, type FreeSpan, type SpaceRect } from "./space.ts";
|
|
6
|
-
export { packShelf, type ShelfOptions } from "./shelf.ts";
|
|
6
|
+
export { packShelf, SHELF_FITS, type ShelfFit, type ShelfOptions } from "./shelf.ts";
|
|
7
7
|
export { packFree, aspectError, type FreeWidthOptions, type FreeWidthResult, type FreeWidthTry } from "./free.ts";
|
|
8
8
|
export { compareRects, SORT_ORDERS, type SortOrder } from "./order.ts";
|
|
9
9
|
export { pushCascade, type PushOptions } from "./push.ts";
|
package/src/measures.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type { Direction } from "@mark1russell7/layout-contract";
|
|
|
13
13
|
export type MeasureFamily = "density" | "aspect" | "order" | "raggedness" | "overflow" | "stability" | "determinism" | "speed";
|
|
14
14
|
|
|
15
15
|
/** The id of a measure of the packer. The ids are those of `docs/FORMAT.md`, and `highways` uses the same ids. */
|
|
16
|
-
export type MeasureId = "pack.area" | "pack.fill" | "pack.whitespace" | "pack.aspect" | "pack.aspectError" | "pack.order.inversions" | "pack.order.breaks" | "pack.raggedness" | "pack.overflow" | "pack.overflow.rate" | "pack.overflow.area" | "pack.overflow.forced" | "pack.overflow.forcedArea" | "pack.overflow.excess" | "pack.stability.moved" | "pack.stability.mean" | "pack.stability.q95" | "pack.deterministic" | "pack.search.widths" | "pack.search.ms";
|
|
16
|
+
export type MeasureId = "pack.area" | "pack.fill" | "pack.whitespace" | "pack.aspect" | "pack.aspectError" | "pack.order.inversions" | "pack.order.breaks" | "pack.raggedness" | "pack.overflow" | "pack.overflow.rate" | "pack.overflow.area" | "pack.overflow.forced" | "pack.overflow.forcedArea" | "pack.overflow.excess" | "pack.overflow.forcedAreaMin" | "pack.overflow.excessArea" | "pack.stability.moved" | "pack.stability.mean" | "pack.stability.q95" | "pack.deterministic" | "pack.search.widths" | "pack.search.ms";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* The facts of one measure: its family, its unit, its direction and its range.
|
|
@@ -51,8 +51,17 @@ export interface LayoutMeasures {
|
|
|
51
51
|
"pack.overflow.rate": number;
|
|
52
52
|
"pack.overflow.area": number;
|
|
53
53
|
"pack.overflow.forced": number;
|
|
54
|
+
/** The tight lower bound of `pack.overflow.area`. `docs/MEASURES.md` defines it. */
|
|
54
55
|
"pack.overflow.forcedArea": number;
|
|
55
56
|
"pack.overflow.excess": number;
|
|
57
|
+
/**
|
|
58
|
+
* This measure is an alias of `pack.overflow.forcedArea`, with the same value.
|
|
59
|
+
*
|
|
60
|
+
* @deprecated Use `pack.overflow.forcedArea`. The alias stays until the next minor version.
|
|
61
|
+
*/
|
|
62
|
+
"pack.overflow.forcedAreaMin": number;
|
|
63
|
+
/** The overflow area above its bound: `pack.overflow.area` less `pack.overflow.forcedArea`. */
|
|
64
|
+
"pack.overflow.excessArea": number;
|
|
56
65
|
"pack.search.widths": number;
|
|
57
66
|
"pack.search.ms": number;
|
|
58
67
|
}
|
|
@@ -95,6 +104,8 @@ const REL = Object.freeze({ rel: 0.01 });
|
|
|
95
104
|
* - A share of an area from 0 to 1 has 0.005. A guard has its tolerance.
|
|
96
105
|
* - An area and a ratio of lengths have `{ rel: 0.01 }`: 1% of the larger value.
|
|
97
106
|
* - The time has no minimum effect, because the times of CI change each time.
|
|
107
|
+
* - `pack.overflow.forcedAreaMin` is a deprecated alias of `pack.overflow.forcedArea`. Its spec is the same, except the id.
|
|
108
|
+
* The alias stays until the next minor version.
|
|
98
109
|
*/
|
|
99
110
|
export const MEASURE_SPECS: Readonly<Record<MeasureId, MeasureSpec>> = Object.freeze({
|
|
100
111
|
"pack.area": spec("pack.area", "density", "square units", "lower", 0, Infinity, REL),
|
|
@@ -111,6 +122,9 @@ export const MEASURE_SPECS: Readonly<Record<MeasureId, MeasureSpec>> = Object.fr
|
|
|
111
122
|
"pack.overflow.forced": spec("pack.overflow.forced", "overflow", "count", "lower", 0, Infinity, 0),
|
|
112
123
|
"pack.overflow.forcedArea": spec("pack.overflow.forcedArea", "overflow", "share", "lower", 0, 1, 0.005),
|
|
113
124
|
"pack.overflow.excess": spec("pack.overflow.excess", "overflow", "count", "lower", 0, Infinity, 0),
|
|
125
|
+
// The deprecated alias of pack.overflow.forcedArea, until the next minor version
|
|
126
|
+
"pack.overflow.forcedAreaMin": spec("pack.overflow.forcedAreaMin", "overflow", "share", "lower", 0, 1, 0.005),
|
|
127
|
+
"pack.overflow.excessArea": spec("pack.overflow.excessArea", "overflow", "share", "lower", 0, 1, 0.005),
|
|
114
128
|
"pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, 0.02, { floor: 0, tol: 0.02 }),
|
|
115
129
|
"pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, 0.005, { floor: 0, tol: 0.005 }),
|
|
116
130
|
"pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, 0.01, { floor: 0, tol: 0.01 }),
|
|
@@ -205,6 +219,8 @@ function traceSum(output: PackOutput, id: string): number {
|
|
|
205
219
|
* - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
|
|
206
220
|
* - The overflow measures count each id once. `pack.overflow.forced` is the lower bound of `overflowBound`.
|
|
207
221
|
* - `pack.overflow.excess` is the overflow less the forced overflow. A container with no fixed side gives 0 for both.
|
|
222
|
+
* - `pack.overflow.forcedArea` is a lower bound of the overflow area. `pack.overflow.excessArea` is the overflow area above it.
|
|
223
|
+
* - `pack.overflow.forcedAreaMin` is a deprecated alias of `pack.overflow.forcedArea`, with the same value.
|
|
208
224
|
*
|
|
209
225
|
* @example
|
|
210
226
|
* ```ts
|
|
@@ -257,6 +273,8 @@ export function measure(input: PackInput, output: PackOutput, options: MeasureOp
|
|
|
257
273
|
if (unplaced.has(item.id)) unplacedArea += a;
|
|
258
274
|
}
|
|
259
275
|
const bound = overflowBound(input);
|
|
276
|
+
const overflowArea = itemArea > 0 ? unplacedArea / itemArea : 0;
|
|
277
|
+
const forcedArea = itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0;
|
|
260
278
|
|
|
261
279
|
return {
|
|
262
280
|
"pack.area": hullArea,
|
|
@@ -269,15 +287,29 @@ export function measure(input: PackInput, output: PackOutput, options: MeasureOp
|
|
|
269
287
|
"pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
|
|
270
288
|
"pack.overflow": unplaced.size,
|
|
271
289
|
"pack.overflow.rate": input.items.length > 0 ? unplaced.size / input.items.length : 0,
|
|
272
|
-
"pack.overflow.area":
|
|
290
|
+
"pack.overflow.area": overflowArea,
|
|
273
291
|
"pack.overflow.forced": bound.forced,
|
|
274
|
-
"pack.overflow.forcedArea":
|
|
292
|
+
"pack.overflow.forcedArea": forcedArea,
|
|
275
293
|
"pack.overflow.excess": Math.max(0, unplaced.size - bound.forced),
|
|
294
|
+
"pack.overflow.forcedAreaMin": forcedArea,
|
|
295
|
+
"pack.overflow.excessArea": excessAreaOf(overflowArea, forcedArea),
|
|
276
296
|
"pack.search.widths": traceSum(output, "pack.search.widths"),
|
|
277
297
|
"pack.search.ms": traceSum(output, "pack.search.ms"),
|
|
278
298
|
};
|
|
279
299
|
}
|
|
280
300
|
|
|
301
|
+
/** The float noise of `pack.overflow.excessArea`. A difference below 0 by this value or less becomes 0. */
|
|
302
|
+
const AREA_NOISE = 1e-9;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* This function gives the overflow area above its lower bound. A value from `-AREA_NOISE` to 0 becomes 0. A value below
|
|
306
|
+
* `-AREA_NOISE` stays, because it shows a bug of the bound or of the packer.
|
|
307
|
+
*/
|
|
308
|
+
function excessAreaOf(overflowArea: number, forcedArea: number): number {
|
|
309
|
+
const excess = overflowArea - forcedArea;
|
|
310
|
+
return excess < 0 && excess >= -AREA_NOISE ? 0 : excess;
|
|
311
|
+
}
|
|
312
|
+
|
|
281
313
|
/**
|
|
282
314
|
* This function gives the stability measures of a change pair: the run on the base input and the run on the changed
|
|
283
315
|
* input. The items match by id. The values are not rounded.
|
package/src/moves.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The counts of the characteristic move of each strategy (roadmap item k59, `docs/BENCH.md`, "Activation"). The
|
|
2
|
+
// packers count their move here, and the adapters write the count in `trace.measures`. The counts do not change a layout.
|
|
3
|
+
// The module is internal: the package root does not export it.
|
|
4
|
+
|
|
5
|
+
/** The id of the characteristic move of each strategy and of each shelf fit. The trace of each strategy has its id, also with the count 0. */
|
|
6
|
+
export const MOVES: Readonly<{
|
|
7
|
+
rows: "move.gapFill";
|
|
8
|
+
shelf: "move.stack";
|
|
9
|
+
skyline: "move.lowerWindow";
|
|
10
|
+
push: "move.cascade";
|
|
11
|
+
firstFit: "move.firstFit";
|
|
12
|
+
bestFit: "move.bestFit";
|
|
13
|
+
}> = Object.freeze({
|
|
14
|
+
/** A rectangle that the gap fill of `packRow` places in a gap of a row: under a shorter neighbor, or at the row break. */
|
|
15
|
+
rows: "move.gapFill",
|
|
16
|
+
/**
|
|
17
|
+
* A rectangle that `packShelf` puts on an open shelf with a taller rectangle. The strip above it stays empty. With a
|
|
18
|
+
* `fit`, the move is a rectangle in the strip above a shorter rectangle of a shelf.
|
|
19
|
+
*/
|
|
20
|
+
shelf: "move.stack",
|
|
21
|
+
/** A rectangle that the skyline puts in a lower window, not in the leftmost window where it fits. */
|
|
22
|
+
skyline: "move.lowerWindow",
|
|
23
|
+
/** A rectangle that a push cascade moves. */
|
|
24
|
+
push: "move.cascade",
|
|
25
|
+
/** A rectangle that a shelf fit puts on a shelf that is not the open shelf. The open shelf is the last shelf that the packing opened. */
|
|
26
|
+
firstFit: "move.firstFit",
|
|
27
|
+
/** A rectangle that the fit `best` puts on a shelf that is not the first shelf where it fits. */
|
|
28
|
+
bestFit: "move.bestFit",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const counts = new Map<string, number>();
|
|
32
|
+
|
|
33
|
+
/** This function adds `n` to the count of a move. */
|
|
34
|
+
export function countMove(id: string, n = 1): void {
|
|
35
|
+
if (n > 0) counts.set(id, (counts.get(id) ?? 0) + n);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** This function sets the count of a move to 0. Without an id, it sets each count to 0. */
|
|
39
|
+
export function clearMoves(id?: string): void {
|
|
40
|
+
if (id === undefined) counts.clear();
|
|
41
|
+
else counts.delete(id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** This function gives the count of a move since the last `clearMoves`. */
|
|
45
|
+
export function moveCount(id: string): number {
|
|
46
|
+
return counts.get(id) ?? 0;
|
|
47
|
+
}
|
package/src/pack.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { clearMoves, countMove, MOVES } from "./moves.ts";
|
|
1
2
|
import { compareRects, type SortOrder } from "./order.ts";
|
|
2
3
|
import { Rect } from "./rect.ts";
|
|
3
|
-
import { packRowsInSpace, packShelf } from "./shelf.ts";
|
|
4
|
+
import { packRowsInSpace, packShelf, type ShelfFit } from "./shelf.ts";
|
|
4
5
|
import { packSkyline } from "./skyline.ts";
|
|
5
6
|
import type { FreeSpace } from "./space.ts";
|
|
6
7
|
import { Vector } from "./vector.ts";
|
|
@@ -23,12 +24,24 @@ export interface PackLayoutOptions extends PackOptions {
|
|
|
23
24
|
* Use `shelf` with the sort order `input` to keep the order of the array around locked rectangles.
|
|
24
25
|
*/
|
|
25
26
|
strategy?: "rows" | "skyline" | "shelf";
|
|
27
|
+
/** The rule of `shelf` to choose a shelf. Only `shelf` reads it. `ShelfOptions` tells it. Without it, `shelf` packs as in 0.2.3. */
|
|
28
|
+
fit?: ShelfFit;
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
/** The depth of the calls of `packInto`. Only the outer gap fill counts its rectangles as moves. */
|
|
32
|
+
let fillDepth = 0;
|
|
33
|
+
|
|
28
34
|
function packInto<T>(rects: Rect<T>[], region: Rect<T>, offset: Vector, order: SortOrder): void {
|
|
29
35
|
const unpacked = Rect.unpacked(rects);
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
fillDepth++;
|
|
37
|
+
try {
|
|
38
|
+
pack(unpacked, region, { sortOrder: order });
|
|
39
|
+
} finally {
|
|
40
|
+
fillDepth--;
|
|
41
|
+
}
|
|
42
|
+
const placed = Rect.packed(unpacked);
|
|
43
|
+
if (fillDepth === 0) countMove(MOVES.rows, placed.length);
|
|
44
|
+
for (const r of placed) r.translateBy(offset);
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
function packRow<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder): boolean {
|
|
@@ -100,7 +113,8 @@ export function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options: PackOptio
|
|
|
100
113
|
const space = options.space;
|
|
101
114
|
if (space !== undefined && (!space.plain || rects.some((r) => r.locked))) return packRowsInSpace(rects, outer, order, space);
|
|
102
115
|
for (const r of rects) if (r.locked) r.wasPacked = true;
|
|
103
|
-
|
|
116
|
+
// A row that does not fit grows the height and starts again, thus the gap fills of the try before do not count
|
|
117
|
+
while (!packRow(rects, outer, order)) if (fillDepth === 0) clearMoves(MOVES.rows);
|
|
104
118
|
return outer;
|
|
105
119
|
}
|
|
106
120
|
|
package/src/shelf.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
import { countMove, MOVES } from "./moves.ts";
|
|
1
2
|
import { compareRects, type SortOrder } from "./order.ts";
|
|
2
3
|
import type { Rect } from "./rect.ts";
|
|
3
4
|
import { firstFit, type FreeSpace, type FreeSpan } from "./space.ts";
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The rule of a shelf packing to choose the shelf of a rectangle. `packShelf` tells each rule.
|
|
8
|
+
*
|
|
9
|
+
* - `next`: only the open shelf, the last shelf that the packing opened.
|
|
10
|
+
* - `first`: the first shelf from the top where the rectangle fits.
|
|
11
|
+
* - `best`: the shelf with the least height left over above the rectangle.
|
|
12
|
+
*/
|
|
13
|
+
export type ShelfFit = "next" | "first" | "best";
|
|
14
|
+
|
|
15
|
+
/** This constant lists each shelf fit. */
|
|
16
|
+
export const SHELF_FITS: readonly ShelfFit[] = ["next", "first", "best"];
|
|
17
|
+
|
|
5
18
|
/** The options of `packShelf` */
|
|
6
19
|
export interface ShelfOptions {
|
|
7
20
|
/** The order of the rectangles before the packing. The default is `height-desc`. */
|
|
@@ -11,6 +24,11 @@ export interface ShelfOptions {
|
|
|
11
24
|
* outer rectangle and the locked rectangles.
|
|
12
25
|
*/
|
|
13
26
|
space?: FreeSpace;
|
|
27
|
+
/**
|
|
28
|
+
* The rule to choose a shelf. Without it, `packShelf` packs as in 0.2.3. With it, `packShelf` uses its own shelves in
|
|
29
|
+
* each container shape, and it stacks rectangles in the strips above the shorter rectangles.
|
|
30
|
+
*/
|
|
31
|
+
fit?: ShelfFit;
|
|
14
32
|
}
|
|
15
33
|
|
|
16
34
|
/** An obstacle in the coordinates of the outer rectangle */
|
|
@@ -59,8 +77,10 @@ function nextTop(obstacles: readonly Box[], top: number, bottom: number): number
|
|
|
59
77
|
* - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
|
|
60
78
|
* rectangle that does not fit is not packed: its `wasPacked` stays `false`.
|
|
61
79
|
* - With a space that is not plain, `packRowsInSpace` packs the rows into the outline of the space and around its holes.
|
|
80
|
+
* - With `fit`, `packShelvesInSpace` packs the rectangles, in each container shape. A `fit` that is not in `SHELF_FITS` throws a `RangeError`.
|
|
62
81
|
*/
|
|
63
82
|
export function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options: ShelfOptions = {}): Rect<T> {
|
|
83
|
+
if (options.fit !== undefined) return packShelvesInSpace(rects, outer, options.sortOrder ?? "height-desc", options.fit, options.space);
|
|
64
84
|
if (options.space !== undefined && !options.space.plain) return packRowsInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
|
|
65
85
|
const width = outer.size.x;
|
|
66
86
|
const obstacles: Box[] = [];
|
|
@@ -111,6 +131,8 @@ export function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options: ShelfOpt
|
|
|
111
131
|
|
|
112
132
|
if (outer.fixedSize && rowTop + h > outer.size.y) continue;
|
|
113
133
|
|
|
134
|
+
// The move of a shelf: the rectangle joins an open shelf that a taller rectangle made
|
|
135
|
+
if (!rowEmpty && rowTop + h < rowBottom) countMove(MOVES.shelf);
|
|
114
136
|
rect.position.set(x + outer.position.x, rowTop + outer.position.y);
|
|
115
137
|
rect.wasPacked = true;
|
|
116
138
|
rowEmpty = false;
|
|
@@ -226,3 +248,184 @@ export function packRowsInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: Sort
|
|
|
226
248
|
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
|
|
227
249
|
return outer;
|
|
228
250
|
}
|
|
251
|
+
|
|
252
|
+
/** A shelf of `packShelvesInSpace`, in the coordinates of the outer rectangle. */
|
|
253
|
+
interface Shelf {
|
|
254
|
+
/** The top edge of the shelf. */
|
|
255
|
+
top: number;
|
|
256
|
+
/** The height of the shelf: the height of its first rectangle. Only the last shelf grows. */
|
|
257
|
+
height: number;
|
|
258
|
+
/** The x after the last rectangle on the floor of the shelf. The value `Infinity` closes the shelf. */
|
|
259
|
+
cursor: number;
|
|
260
|
+
/** The strips above the rectangles on the floor. A later rectangle stacks in a strip. */
|
|
261
|
+
strips: Strip[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The free strip above a rectangle of a shelf: from `x` to `x + w`, and from `bottom` to the top edge of the next shelf. */
|
|
265
|
+
interface Strip {
|
|
266
|
+
x: number;
|
|
267
|
+
w: number;
|
|
268
|
+
bottom: number;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** A place for a rectangle on a shelf. `left` is the height of the shelf that stays free above the rectangle. */
|
|
272
|
+
interface Spot {
|
|
273
|
+
shelf: Shelf;
|
|
274
|
+
x: number;
|
|
275
|
+
y: number;
|
|
276
|
+
left: number;
|
|
277
|
+
/** The strip of a stacked rectangle. A rectangle on the floor has no strip. */
|
|
278
|
+
strip?: Strip;
|
|
279
|
+
/** The value `true` when the rectangle is taller than the last shelf, and the shelf grows. */
|
|
280
|
+
grow?: boolean;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* This function packs rectangles into shelves with a fit: `next`, `first` or `best`. `packShelf` uses it when its
|
|
285
|
+
* options have a `fit`. It sets the position of each rectangle and gives the outer rectangle.
|
|
286
|
+
*
|
|
287
|
+
* - A shelf opens at the bottom edge of the last shelf, at the first x where the rectangle fits in the free space.
|
|
288
|
+
* When no x fits, the shelf steps down to `nextY` of the space, as an empty row of `packRowsInSpace` does.
|
|
289
|
+
* - The first rectangle of a shelf sets its height. A rectangle goes on the floor of a shelf at its cursor, or into
|
|
290
|
+
* the strip above a shorter rectangle of the shelf. The strip is the move `move.stack`.
|
|
291
|
+
* - The fit `next` uses only the open shelf: the last shelf that the packing opened. The fit `first` uses the first
|
|
292
|
+
* shelf from the top where the rectangle fits. The fit `best` uses the place with the least height left over.
|
|
293
|
+
* - In a shelf, the rectangle takes the place with the least height left over. A tie goes to a strip, from left to right.
|
|
294
|
+
* - When no shelf has a place, the last shelf grows for a taller rectangle, if it fits. Else a new shelf opens.
|
|
295
|
+
* - A rectangle wider than the outer rectangle overflows. Without `fixedSize`, it goes on a closed shelf of its own at the left edge.
|
|
296
|
+
* - Each place is in the free space and crosses no locked rectangle. Without a space, the outer rectangle and the
|
|
297
|
+
* locked rectangles give the free space.
|
|
298
|
+
*/
|
|
299
|
+
function packShelvesInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder, fit: ShelfFit, space: FreeSpace | undefined): Rect<T> {
|
|
300
|
+
if (!SHELF_FITS.includes(fit)) throw new RangeError(`The shelf fit "${String(fit)}" is not one of ${SHELF_FITS.join(", ")}.`);
|
|
301
|
+
const width = outer.size.x;
|
|
302
|
+
const ox = outer.position.x;
|
|
303
|
+
const oy = outer.position.y;
|
|
304
|
+
const limit = outer.fixedSize ? outer.size.y : Infinity;
|
|
305
|
+
const obstacles: Box[] = [];
|
|
306
|
+
let contentBottom = 0;
|
|
307
|
+
for (const rect of rects) {
|
|
308
|
+
if (!rect.locked) continue;
|
|
309
|
+
rect.wasPacked = true;
|
|
310
|
+
const left = rect.position.x - ox;
|
|
311
|
+
const top = rect.position.y - oy;
|
|
312
|
+
obstacles.push({ left, top, right: left + rect.size.x, bottom: top + rect.size.y });
|
|
313
|
+
contentBottom = Math.max(contentBottom, top + rect.size.y);
|
|
314
|
+
}
|
|
315
|
+
const whole: readonly FreeSpan[] = [{ start: 0, end: Infinity }];
|
|
316
|
+
// The x in the band, in the coordinates of the outer rectangle, or Infinity
|
|
317
|
+
const place = (start: number, w: number, top: number, h: number): number => {
|
|
318
|
+
const spans = space === undefined ? whole : space.band(top + oy, top + h + oy).map((s) => ({ start: s.start - ox, end: s.end - ox }));
|
|
319
|
+
return placeInBand(obstacles, spans, start, w, top, top + h);
|
|
320
|
+
};
|
|
321
|
+
// The next top edge where the free spans change, or Infinity
|
|
322
|
+
const stepDown = (top: number, w: number, h: number): number =>
|
|
323
|
+
Math.min(space === undefined ? Infinity : space.nextY(top + oy, top + h + oy, w) - oy, nextTop(obstacles, top, top + h));
|
|
324
|
+
|
|
325
|
+
// The places of a shelf that the sizes allow, by the height left over. A strip comes before the floor on a tie.
|
|
326
|
+
const spotsOn = (shelf: Shelf, w: number, h: number, grow: boolean): Spot[] => {
|
|
327
|
+
const ceiling = shelf.top + shelf.height;
|
|
328
|
+
const spots: Spot[] = [];
|
|
329
|
+
if (!grow) {
|
|
330
|
+
for (const strip of shelf.strips) {
|
|
331
|
+
if (w <= strip.w && strip.bottom + h <= ceiling) spots.push({ shelf, x: strip.x, y: strip.bottom, left: ceiling - strip.bottom - h, strip });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const tall = grow ? h > shelf.height && shelf.top + h <= limit : h <= shelf.height;
|
|
335
|
+
if (Number.isFinite(shelf.cursor) && tall) spots.push({ shelf, x: shelf.cursor, y: shelf.top, left: grow ? 0 : shelf.height - h, ...(grow ? { grow } : {}) });
|
|
336
|
+
return spots.toSorted((a, b) => a.left - b.left);
|
|
337
|
+
};
|
|
338
|
+
// The place of a spot in the free space, or undefined. A rectangle on the floor can move right of the cursor.
|
|
339
|
+
const verify = (spot: Spot, w: number, h: number): Spot | undefined => {
|
|
340
|
+
const x = place(spot.x, w, spot.y, h);
|
|
341
|
+
if (spot.strip !== undefined) return x === spot.x ? spot : undefined;
|
|
342
|
+
return x + w <= width ? { ...spot, x } : undefined;
|
|
343
|
+
};
|
|
344
|
+
const firstOn = (shelf: Shelf, w: number, h: number): Spot | undefined => {
|
|
345
|
+
for (const spot of spotsOn(shelf, w, h, false)) {
|
|
346
|
+
const found = verify(spot, w, h);
|
|
347
|
+
if (found !== undefined) return found;
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const shelves: Shelf[] = [];
|
|
353
|
+
for (const rect of rects.toSorted(compareRects(order))) {
|
|
354
|
+
if (rect.wasPacked) continue;
|
|
355
|
+
const w = rect.size.x;
|
|
356
|
+
const h = rect.size.y;
|
|
357
|
+
if (!(w > 0 && h > 0)) continue;
|
|
358
|
+
const open = shelves.at(-1);
|
|
359
|
+
const floor = open === undefined ? 0 : open.top + open.height;
|
|
360
|
+
|
|
361
|
+
if (w > width) {
|
|
362
|
+
if (outer.fixedSize) continue;
|
|
363
|
+
// An overflowing rectangle goes on a closed shelf of its own, under the locked rectangles at the left edge
|
|
364
|
+
let top = floor;
|
|
365
|
+
while (firstFreeX(obstacles, 0, w, top, top + h) !== 0) {
|
|
366
|
+
const next = nextTop(obstacles, top, top + h);
|
|
367
|
+
if (!Number.isFinite(next)) break;
|
|
368
|
+
top = next;
|
|
369
|
+
}
|
|
370
|
+
rect.position.set(ox, top + oy);
|
|
371
|
+
rect.wasPacked = true;
|
|
372
|
+
shelves.push({ top, height: h, cursor: Infinity, strips: [] });
|
|
373
|
+
contentBottom = Math.max(contentBottom, top + h);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Step 1: a shelf where the rectangle fits without growth
|
|
378
|
+
let spot: Spot | undefined;
|
|
379
|
+
let firstShelf: Shelf | undefined;
|
|
380
|
+
if (fit === "best") {
|
|
381
|
+
for (const s of shelves.flatMap((shelf) => spotsOn(shelf, w, h, false)).toSorted((a, b) => a.left - b.left)) {
|
|
382
|
+
spot = verify(s, w, h);
|
|
383
|
+
if (spot !== undefined) break;
|
|
384
|
+
}
|
|
385
|
+
if (spot !== undefined) firstShelf = shelves.find((shelf) => shelf === spot!.shelf || firstOn(shelf, w, h) !== undefined);
|
|
386
|
+
} else {
|
|
387
|
+
for (const shelf of fit === "next" ? shelves.slice(-1) : shelves) {
|
|
388
|
+
spot = firstOn(shelf, w, h);
|
|
389
|
+
if (spot !== undefined) break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Step 2: the last shelf grows. Step 3: a new shelf opens under the last shelf.
|
|
393
|
+
if (spot === undefined && open !== undefined) {
|
|
394
|
+
const grown = spotsOn(open, w, h, true)[0];
|
|
395
|
+
if (grown !== undefined) spot = verify(grown, w, h);
|
|
396
|
+
}
|
|
397
|
+
if (spot === undefined) {
|
|
398
|
+
let top = floor;
|
|
399
|
+
let x = place(0, w, top, h);
|
|
400
|
+
while (!(x + w <= width) && top + h <= limit) {
|
|
401
|
+
const next = stepDown(top, w, h);
|
|
402
|
+
if (!Number.isFinite(next)) break;
|
|
403
|
+
top = next;
|
|
404
|
+
x = place(0, w, top, h);
|
|
405
|
+
}
|
|
406
|
+
if (!(x + w <= width) || top + h > limit) continue;
|
|
407
|
+
const shelf: Shelf = { top, height: h, cursor: x, strips: [] };
|
|
408
|
+
shelves.push(shelf);
|
|
409
|
+
spot = { shelf, x, y: top, left: 0 };
|
|
410
|
+
} else {
|
|
411
|
+
if (spot.shelf !== open) countMove(MOVES.firstFit);
|
|
412
|
+
if (firstShelf !== undefined && spot.shelf !== firstShelf) countMove(MOVES.bestFit);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const { shelf } = spot;
|
|
416
|
+
rect.position.set(spot.x + ox, spot.y + oy);
|
|
417
|
+
rect.wasPacked = true;
|
|
418
|
+
if (spot.strip !== undefined) {
|
|
419
|
+
countMove(MOVES.shelf);
|
|
420
|
+
spot.strip.bottom = spot.y + h;
|
|
421
|
+
} else {
|
|
422
|
+
shelf.cursor = spot.x + w;
|
|
423
|
+
shelf.strips.push({ x: spot.x, w, bottom: spot.y + h });
|
|
424
|
+
if (spot.grow === true) shelf.height = h;
|
|
425
|
+
}
|
|
426
|
+
contentBottom = Math.max(contentBottom, spot.y + h);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
|
|
430
|
+
return outer;
|
|
431
|
+
}
|