@mark1russell7/packing 0.2.2 → 0.2.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/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.2",
4
+ "version": "0.2.3",
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,6 +3,7 @@
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";
@@ -59,9 +60,10 @@ function packFixedWidth(id: "rows" | "skyline" | "shelf", input: PackInput, opti
59
60
  const start = now();
60
61
  const frame = buildFrame(input);
61
62
  const rects = rectsOf(frame);
63
+ clearMoves();
62
64
  if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder, space: spaceOf(input, frame) });
63
65
  const output = readFrame(input, frame, []);
64
- return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length } }] };
66
+ return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length, [MOVES[id]]: moveCount(MOVES[id]) } }] };
65
67
  }
66
68
 
67
69
  /** This function makes the strategy of one function of `pack`. */
@@ -150,7 +152,7 @@ export const logSweep: SizeSearch = {
150
152
  ...output,
151
153
  trace: [
152
154
  { 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 } },
155
+ { id, ms: ms / runs, measures: { "pack.overflow": output.overflow.length, [MOVES[id]]: moveCount(MOVES[id]) } },
154
156
  ],
155
157
  };
156
158
  },
@@ -191,8 +193,9 @@ function pushFixedWidth(input: PackInput, options: PushStrategyOptions, previous
191
193
  // The cascade: each item in the order of its rank pushes the others, and then it stays
192
194
  const inPlay = frame.entries.filter((e) => !pending.includes(e)).map((e) => e.rect);
193
195
  const maxShoves = options.maxShovesPerItem * Math.max(1, inPlay.length);
196
+ let cascaded = 0;
194
197
  for (const e of positioned) {
195
- pushCascade(e.rect, inPlay, { gap: 0, maxShoves });
198
+ cascaded += pushCascade(e.rect, inPlay, { gap: 0, maxShoves }).length;
196
199
  e.rect.locked = true;
197
200
  }
198
201
 
@@ -234,7 +237,7 @@ function pushFixedWidth(input: PackInput, options: PushStrategyOptions, previous
234
237
  return {
235
238
  ...output,
236
239
  trace: [
237
- { id: "push", ms: pushMs, measures: { "pack.push.moved": moved.length } },
240
+ { id: "push", ms: pushMs, measures: { "pack.push.moved": moved.length, [MOVES.push]: cascaded } },
238
241
  { id: "skyline", ms: now() - skylineStart, measures: { "pack.push.replaced": pending.length, "pack.overflow": output.overflow.length } },
239
242
  ],
240
243
  };
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/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.
@@ -53,6 +53,8 @@ export interface LayoutMeasures {
53
53
  "pack.overflow.forced": number;
54
54
  "pack.overflow.forcedArea": number;
55
55
  "pack.overflow.excess": number;
56
+ "pack.overflow.forcedAreaMin": number;
57
+ "pack.overflow.excessArea": number;
56
58
  "pack.search.widths": number;
57
59
  "pack.search.ms": number;
58
60
  }
@@ -111,6 +113,8 @@ export const MEASURE_SPECS: Readonly<Record<MeasureId, MeasureSpec>> = Object.fr
111
113
  "pack.overflow.forced": spec("pack.overflow.forced", "overflow", "count", "lower", 0, Infinity, 0),
112
114
  "pack.overflow.forcedArea": spec("pack.overflow.forcedArea", "overflow", "share", "lower", 0, 1, 0.005),
113
115
  "pack.overflow.excess": spec("pack.overflow.excess", "overflow", "count", "lower", 0, Infinity, 0),
116
+ "pack.overflow.forcedAreaMin": spec("pack.overflow.forcedAreaMin", "overflow", "share", "lower", 0, 1, 0.005),
117
+ "pack.overflow.excessArea": spec("pack.overflow.excessArea", "overflow", "share", "lower", 0, 1, 0.005),
114
118
  "pack.stability.moved": spec("pack.stability.moved", "stability", "share", "lower", 0, 1, 0.02, { floor: 0, tol: 0.02 }),
115
119
  "pack.stability.mean": spec("pack.stability.mean", "stability", "container widths", "lower", 0, Infinity, 0.005, { floor: 0, tol: 0.005 }),
116
120
  "pack.stability.q95": spec("pack.stability.q95", "stability", "container widths", "lower", 0, Infinity, 0.01, { floor: 0, tol: 0.01 }),
@@ -205,6 +209,7 @@ function traceSum(output: PackOutput, id: string): number {
205
209
  * - The time `pack.search.ms` comes from the trace. It is not part of the determinism rule.
206
210
  * - The overflow measures count each id once. `pack.overflow.forced` is the lower bound of `overflowBound`.
207
211
  * - `pack.overflow.excess` is the overflow less the forced overflow. A container with no fixed side gives 0 for both.
212
+ * - `pack.overflow.forcedAreaMin` is a lower bound of the overflow area. `pack.overflow.excessArea` is the overflow area above it.
208
213
  *
209
214
  * @example
210
215
  * ```ts
@@ -257,6 +262,8 @@ export function measure(input: PackInput, output: PackOutput, options: MeasureOp
257
262
  if (unplaced.has(item.id)) unplacedArea += a;
258
263
  }
259
264
  const bound = overflowBound(input);
265
+ const overflowArea = itemArea > 0 ? unplacedArea / itemArea : 0;
266
+ const forcedAreaMin = itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0;
260
267
 
261
268
  return {
262
269
  "pack.area": hullArea,
@@ -269,15 +276,29 @@ export function measure(input: PackInput, output: PackOutput, options: MeasureOp
269
276
  "pack.raggedness": hull.w > 0 ? spread / hull.w : 0,
270
277
  "pack.overflow": unplaced.size,
271
278
  "pack.overflow.rate": input.items.length > 0 ? unplaced.size / input.items.length : 0,
272
- "pack.overflow.area": itemArea > 0 ? unplacedArea / itemArea : 0,
279
+ "pack.overflow.area": overflowArea,
273
280
  "pack.overflow.forced": bound.forced,
274
- "pack.overflow.forcedArea": itemArea > 0 ? Math.min(1, bound.area / itemArea) : 0,
281
+ "pack.overflow.forcedArea": forcedAreaMin,
275
282
  "pack.overflow.excess": Math.max(0, unplaced.size - bound.forced),
283
+ "pack.overflow.forcedAreaMin": forcedAreaMin,
284
+ "pack.overflow.excessArea": excessAreaOf(overflowArea, forcedAreaMin),
276
285
  "pack.search.widths": traceSum(output, "pack.search.widths"),
277
286
  "pack.search.ms": traceSum(output, "pack.search.ms"),
278
287
  };
279
288
  }
280
289
 
290
+ /** The float noise of `pack.overflow.excessArea`. A difference below 0 by this value or less becomes 0. */
291
+ const AREA_NOISE = 1e-9;
292
+
293
+ /**
294
+ * This function gives the overflow area above its lower bound. A value from `-AREA_NOISE` to 0 becomes 0. A value below
295
+ * `-AREA_NOISE` stays, because it shows a bug of the bound or of the packer.
296
+ */
297
+ function excessAreaOf(overflowArea: number, forcedAreaMin: number): number {
298
+ const excess = overflowArea - forcedAreaMin;
299
+ return excess < 0 && excess >= -AREA_NOISE ? 0 : excess;
300
+ }
301
+
281
302
  /**
282
303
  * This function gives the stability measures of a change pair: the run on the base input and the run on the changed
283
304
  * input. The items match by id. The values are not rounded.
package/src/moves.ts ADDED
@@ -0,0 +1,33 @@
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. The trace of each strategy has its id, also with the count 0. */
6
+ export const MOVES: Readonly<{ rows: "move.gapFill"; shelf: "move.stack"; skyline: "move.lowerWindow"; push: "move.cascade" }> = Object.freeze({
7
+ /** A rectangle that the gap fill of `packRow` places in a gap of a row: under a shorter neighbor, or at the row break. */
8
+ rows: "move.gapFill",
9
+ /** A rectangle that `packShelf` puts on an open shelf with a taller rectangle. The strip above it stays empty. */
10
+ shelf: "move.stack",
11
+ /** A rectangle that the skyline puts in a lower window, not in the leftmost window where it fits. */
12
+ skyline: "move.lowerWindow",
13
+ /** A rectangle that a push cascade moves. */
14
+ push: "move.cascade",
15
+ });
16
+
17
+ const counts = new Map<string, number>();
18
+
19
+ /** This function adds `n` to the count of a move. */
20
+ export function countMove(id: string, n = 1): void {
21
+ if (n > 0) counts.set(id, (counts.get(id) ?? 0) + n);
22
+ }
23
+
24
+ /** This function sets the count of a move to 0. Without an id, it sets each count to 0. */
25
+ export function clearMoves(id?: string): void {
26
+ if (id === undefined) counts.clear();
27
+ else counts.delete(id);
28
+ }
29
+
30
+ /** This function gives the count of a move since the last `clearMoves`. */
31
+ export function moveCount(id: string): number {
32
+ return counts.get(id) ?? 0;
33
+ }
package/src/pack.ts CHANGED
@@ -1,3 +1,4 @@
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
4
  import { packRowsInSpace, packShelf } from "./shelf.ts";
@@ -25,10 +26,20 @@ export interface PackLayoutOptions extends PackOptions {
25
26
  strategy?: "rows" | "skyline" | "shelf";
26
27
  }
27
28
 
29
+ /** The depth of the calls of `packInto`. Only the outer gap fill counts its rectangles as moves. */
30
+ let fillDepth = 0;
31
+
28
32
  function packInto<T>(rects: Rect<T>[], region: Rect<T>, offset: Vector, order: SortOrder): void {
29
33
  const unpacked = Rect.unpacked(rects);
30
- pack(unpacked, region, { sortOrder: order });
31
- for (const r of Rect.packed(unpacked)) r.translateBy(offset);
34
+ fillDepth++;
35
+ try {
36
+ pack(unpacked, region, { sortOrder: order });
37
+ } finally {
38
+ fillDepth--;
39
+ }
40
+ const placed = Rect.packed(unpacked);
41
+ if (fillDepth === 0) countMove(MOVES.rows, placed.length);
42
+ for (const r of placed) r.translateBy(offset);
32
43
  }
33
44
 
34
45
  function packRow<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder): boolean {
@@ -100,7 +111,8 @@ export function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options: PackOptio
100
111
  const space = options.space;
101
112
  if (space !== undefined && (!space.plain || rects.some((r) => r.locked))) return packRowsInSpace(rects, outer, order, space);
102
113
  for (const r of rects) if (r.locked) r.wasPacked = true;
103
- while (!packRow(rects, outer, order));
114
+ // A row that does not fit grows the height and starts again, thus the gap fills of the try before do not count
115
+ while (!packRow(rects, outer, order)) if (fillDepth === 0) clearMoves(MOVES.rows);
104
116
  return outer;
105
117
  }
106
118
 
package/src/shelf.ts CHANGED
@@ -1,3 +1,4 @@
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";
@@ -111,6 +112,8 @@ export function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options: ShelfOpt
111
112
 
112
113
  if (outer.fixedSize && rowTop + h > outer.size.y) continue;
113
114
 
115
+ // The move of a shelf: the rectangle joins an open shelf that a taller rectangle made
116
+ if (!rowEmpty && rowTop + h < rowBottom) countMove(MOVES.shelf);
114
117
  rect.position.set(x + outer.position.x, rowTop + outer.position.y);
115
118
  rect.wasPacked = true;
116
119
  rowEmpty = false;
package/src/skyline.ts CHANGED
@@ -1,3 +1,4 @@
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 type { FreeSpace } from "./space.ts";
@@ -11,6 +12,19 @@ import type { FreeSpace } from "./space.ts";
11
12
  * `limit[x, x + rw)`. A second deque keeps that smallest value. No valid window gives the height `Infinity`.
12
13
  */
13
14
  export function lowestWindow(skyline: ArrayLike<number>, width: number, rw: number, limit?: ArrayLike<number>, rh = 0): { x: number; y: number } {
15
+ const { x, y } = findWindow(skyline, width, rw, limit, rh);
16
+ return { x, y };
17
+ }
18
+
19
+ /** The lowest window of `findWindow`, and `first`: the x of the leftmost valid window, or `-1` */
20
+ interface Window {
21
+ x: number;
22
+ y: number;
23
+ first: number;
24
+ }
25
+
26
+ /** This function is `lowestWindow`, and it also gives the x of the leftmost valid window. */
27
+ function findWindow(skyline: ArrayLike<number>, width: number, rw: number, limit?: ArrayLike<number>, rh = 0): Window {
14
28
  if (limit !== undefined) return lowestLimitedWindow(skyline, limit, width, rw, rh);
15
29
  const deque = new Int32Array(width);
16
30
  let head = 0;
@@ -29,11 +43,16 @@ export function lowestWindow(skyline: ArrayLike<number>, width: number, rw: numb
29
43
  }
30
44
  }
31
45
  }
32
- return { x: bestX, y: bestY };
46
+ return { x: bestX, y: bestY, first: width >= rw ? 0 : -1 };
47
+ }
48
+
49
+ /** This function counts the move of the skyline: a window that is lower than the leftmost valid window. */
50
+ function countWindow(w: Window): void {
51
+ if (Number.isFinite(w.y) && w.first >= 0 && w.x !== w.first) countMove(MOVES.skyline);
33
52
  }
34
53
 
35
54
  /** This function is `lowestWindow` with a limit for each cell. */
36
- function lowestLimitedWindow(skyline: ArrayLike<number>, limit: ArrayLike<number>, width: number, rw: number, rh: number): { x: number; y: number } {
55
+ function lowestLimitedWindow(skyline: ArrayLike<number>, limit: ArrayLike<number>, width: number, rw: number, rh: number): Window {
37
56
  const high = new Int32Array(width);
38
57
  const low = new Int32Array(width);
39
58
  let hh = 0;
@@ -42,6 +61,7 @@ function lowestLimitedWindow(skyline: ArrayLike<number>, limit: ArrayLike<number
42
61
  let lt = 0;
43
62
  let bestX = 0;
44
63
  let bestY = Infinity;
64
+ let first = -1;
45
65
  for (let i = 0; i < width; i++) {
46
66
  while (ht > hh && skyline[high[ht - 1]!]! <= skyline[i]!) ht--;
47
67
  high[ht++] = i;
@@ -51,13 +71,15 @@ function lowestLimitedWindow(skyline: ArrayLike<number>, limit: ArrayLike<number
51
71
  if (low[lh]! <= i - rw) lh++;
52
72
  if (i >= rw - 1) {
53
73
  const max = skyline[high[hh]!]!;
54
- if (max < bestY && max + rh <= limit[low[lh]!]!) {
74
+ const valid = max + rh <= limit[low[lh]!]!;
75
+ if (valid && first < 0) first = i - rw + 1;
76
+ if (max < bestY && valid) {
55
77
  bestY = max;
56
78
  bestX = i - rw + 1;
57
79
  }
58
80
  }
59
81
  }
60
- return { x: bestX, y: bestY };
82
+ return { x: bestX, y: bestY, first };
61
83
  }
62
84
 
63
85
  /** The options of `packSkyline` */
@@ -116,8 +138,10 @@ export function packSkyline<T>(rects: Rect<T>[], outer: Rect<T>, options: Skylin
116
138
  y = 0;
117
139
  for (let i = 0; i < width; i++) if (skyline[i]! > y) y = skyline[i]!;
118
140
  } else {
119
- ({ x, y } = lowestWindow(skyline, width, rw));
141
+ const found = findWindow(skyline, width, rw);
142
+ ({ x, y } = found);
120
143
  if (outer.fixedSize && y + rh > outer.size.y) continue;
144
+ countWindow(found);
121
145
  }
122
146
 
123
147
  rect.position.set(x + outer.position.x, y + outer.position.y);
@@ -179,7 +203,8 @@ function packSkylineInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrde
179
203
  outer.size.y = Math.max(outer.size.y, y + rh);
180
204
  continue;
181
205
  }
182
- ({ x, y } = lowestWindow(skyline, width, rw, limit, rh));
206
+ let found = findWindow(skyline, width, rw, limit, rh);
207
+ ({ x, y } = found);
183
208
  let sky: Float64Array = skyline;
184
209
  let lim: Float64Array = limit;
185
210
  if (!Number.isFinite(y)) {
@@ -189,10 +214,12 @@ function packSkylineInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrde
189
214
  for (let i = 0; i < width; i++) {
190
215
  while (Number.isFinite(lim[i]!) && lim[i]! - sky[i]! < rh && lim[i]! < end) [sky[i], lim[i]] = rangeOf(i, lim[i]!);
191
216
  }
192
- ({ x, y } = lowestWindow(sky, width, rw, lim, rh));
217
+ found = findWindow(sky, width, rw, lim, rh);
218
+ ({ x, y } = found);
193
219
  if (!Number.isFinite(y)) continue;
194
220
  }
195
221
 
222
+ countWindow(found);
196
223
  rect.position.set(x + ox, y + oy);
197
224
  rect.wasPacked = true;
198
225
  const top = y + rh;