@mark1russell7/packing 0.2.0 → 0.2.1

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.0",
4
+ "version": "0.2.1",
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
@@ -8,6 +8,7 @@ import { pack } from "./pack.ts";
8
8
  import { pushCascade } from "./push.ts";
9
9
  import { Rect } from "./rect.ts";
10
10
  import { packSkyline } from "./skyline.ts";
11
+ import { spaceOf } from "./space.ts";
11
12
  import { assertSupported, cellOf, UnsupportedCell, type PackContext, type SizeSearch, type Strategy, type Supports } from "./strategy.ts";
12
13
 
13
14
  /** The options of the strategies `rows`, `skyline` and `shelf`. */
@@ -26,10 +27,13 @@ export interface PushStrategyOptions {
26
27
  maxShovesPerItem: number;
27
28
  }
28
29
 
29
- /** The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. */
30
+ /**
31
+ * The cells of `rows`, `skyline` and `shelf`, without the movement of the obstacles. The container layer of `src/space.ts`
32
+ * gives them each container shape.
33
+ */
30
34
  const LAYOUT_CELLS = {
31
35
  containerSize: ["fixed", "freeHeight", "freeWidth", "freeAspect", "freeArea"],
32
- containerShape: ["rect"],
36
+ containerShape: ["rect", "convex", "polygon", "circle", "ellipse", "holes"],
33
37
  itemShape: ["rect"],
34
38
  rotation: ["none"],
35
39
  groups: ["none"],
@@ -55,7 +59,7 @@ function packFixedWidth(id: "rows" | "skyline" | "shelf", input: PackInput, opti
55
59
  const start = now();
56
60
  const frame = buildFrame(input);
57
61
  const rects = rectsOf(frame);
58
- if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder });
62
+ if (frame.w > 0) pack(rects, outerOf(frame), { strategy: id, sortOrder: options.sortOrder, space: spaceOf(input, frame) });
59
63
  const output = readFrame(input, frame, []);
60
64
  return { ...output, trace: [{ id, ms: now() - start, measures: { "pack.overflow": output.overflow.length } }] };
61
65
  }
@@ -91,17 +95,18 @@ function layoutStrategy(
91
95
  }
92
96
 
93
97
  /**
94
- * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. It supports no obstacle and
95
- * no locked item, because `packRows` does not go around them. Turnable items, groups and shapes throw `UnsupportedCell`.
98
+ * The strategy `rows` uses `packRows`: a guillotine row packing with a recursive gap fill. With obstacles, locked items or a
99
+ * container shape, the rows go around the holes of the free space and lose the gap fill. Turnable items, groups and item
100
+ * shapes throw `UnsupportedCell`.
96
101
  */
97
102
  export const rows: Strategy<LayoutStrategyOptions> = layoutStrategy("rows", "shelf", ["rows-gap-fill", "rows-input"], {
98
- obstacleMove: [],
103
+ obstacleMove: ["fixed"],
99
104
  order: ["free", "soft", "hard"],
100
105
  });
101
106
 
102
107
  /**
103
108
  * The strategy `skyline` uses `packSkyline`: each item goes to the lowest place where it fits. It packs around the
104
- * locked items and the fixed obstacles. Turnable items, groups and shapes throw `UnsupportedCell`.
109
+ * locked items and the fixed obstacles, in each container shape. Turnable items, groups and item shapes throw `UnsupportedCell`.
105
110
  */
106
111
  export const skyline: Strategy<LayoutStrategyOptions> = layoutStrategy("skyline", "skyline", ["skyline-bl"], {
107
112
  obstacleMove: ["fixed"],
@@ -110,7 +115,7 @@ export const skyline: Strategy<LayoutStrategyOptions> = layoutStrategy("skyline"
110
115
 
111
116
  /**
112
117
  * The strategy `shelf` uses `packShelf`: rows filled left to right, around the locked items and the fixed obstacles.
113
- * With the sort order `input`, the rows keep the rank order. Turnable items, groups and shapes throw `UnsupportedCell`.
118
+ * With the sort order `input`, the rows keep the rank order. Turnable items, groups and item shapes throw `UnsupportedCell`.
114
119
  */
115
120
  export const shelf: Strategy<LayoutStrategyOptions> = layoutStrategy("shelf", "shelf", ["shelf-nf", "shelf-nfdh"], {
116
121
  obstacleMove: ["fixed"],
@@ -133,6 +138,7 @@ export const logSweep: SizeSearch = {
133
138
  const result = packFree(rectsOf(frame), new Rect<number>(), {
134
139
  strategy: id,
135
140
  sortOrder: o.sortOrder,
141
+ space: spaceOf(input, frame),
136
142
  samples: o.samples,
137
143
  aspect: input.container.aspect ?? 1,
138
144
  aspectWeight: input.container.aspectWeight ?? 1,
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export { Vector } from "./vector.ts";
2
2
  export { Rect } from "./rect.ts";
3
3
  export { pack, packRows, type PackOptions, type PackLayoutOptions } from "./pack.ts";
4
- export { packSkyline, lowestWindow } from "./skyline.ts";
4
+ export { packSkyline, lowestWindow, type SkylineOptions } from "./skyline.ts";
5
+ export { freeSpace, type FreeSpace, type FreeSpan, type SpaceRect } from "./space.ts";
5
6
  export { packShelf, type ShelfOptions } from "./shelf.ts";
6
7
  export { packFree, aspectError, type FreeWidthOptions, type FreeWidthResult, type FreeWidthTry } from "./free.ts";
7
8
  export { compareRects, SORT_ORDERS, type SortOrder } from "./order.ts";
package/src/pack.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  import { compareRects, type SortOrder } from "./order.ts";
2
2
  import { Rect } from "./rect.ts";
3
- import { packShelf } from "./shelf.ts";
3
+ import { packRowsInSpace, packShelf } from "./shelf.ts";
4
4
  import { packSkyline } from "./skyline.ts";
5
+ import type { FreeSpace } from "./space.ts";
5
6
  import { Vector } from "./vector.ts";
6
7
 
7
8
  /** The options of `pack` and `packRows` */
8
9
  export interface PackOptions {
9
10
  /** The order of the rectangles before the packing. The default is `height-desc`. */
10
11
  sortOrder?: SortOrder;
12
+ /**
13
+ * The free space of the container: `freeSpace` of the input. With a space that is not plain, each strategy packs into
14
+ * the outline of the container and around its holes. The rows of `packRows` also go around the locked rectangles.
15
+ */
16
+ space?: FreeSpace;
11
17
  }
12
18
 
13
19
  /** The options of `pack` */
@@ -85,9 +91,14 @@ function packRow<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder): boolean
85
91
  * This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does
86
92
  * not change the width. Without `fixedSize`, the height grows until all rectangles fit. A `locked` rectangle keeps
87
93
  * its position, but the rows do not go around it. `pack` sets the width first and then uses this function.
94
+ *
95
+ * With `options.space`, the rows go around the locked rectangles and the holes of the space, inside its outline. A space
96
+ * that is plain, with no locked rectangle, gives the rows of 0.1.
88
97
  */
89
98
  export function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options: PackOptions = {}): Rect<T> {
90
99
  const order = options.sortOrder ?? "height-desc";
100
+ const space = options.space;
101
+ if (space !== undefined && (!space.plain || rects.some((r) => r.locked))) return packRowsInSpace(rects, outer, order, space);
91
102
  for (const r of rects) if (r.locked) r.wasPacked = true;
92
103
  while (!packRow(rects, outer, order));
93
104
  return outer;
package/src/shelf.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import { compareRects, type SortOrder } from "./order.ts";
2
2
  import type { Rect } from "./rect.ts";
3
+ import { firstFit, type FreeSpace, type FreeSpan } from "./space.ts";
3
4
 
4
5
  /** The options of `packShelf` */
5
6
  export interface ShelfOptions {
6
7
  /** The order of the rectangles before the packing. The default is `height-desc`. */
7
8
  sortOrder?: SortOrder;
9
+ /**
10
+ * The free space of the container: `freeSpace` of the input. Without it, or when it is plain, the packer uses only the
11
+ * outer rectangle and the locked rectangles.
12
+ */
13
+ space?: FreeSpace;
8
14
  }
9
15
 
10
16
  /** An obstacle in the coordinates of the outer rectangle */
@@ -52,8 +58,10 @@ function nextTop(obstacles: readonly Box[], top: number, bottom: number): number
52
58
  * - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
53
59
  * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
54
60
  * rectangle that does not fit is not packed: its `wasPacked` stays `false`.
61
+ * - With a space that is not plain, `packRowsInSpace` packs the rows into the outline of the space and around its holes.
55
62
  */
56
63
  export function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options: ShelfOptions = {}): Rect<T> {
64
+ if (options.space !== undefined && !options.space.plain) return packRowsInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
57
65
  const width = outer.size.x;
58
66
  const obstacles: Box[] = [];
59
67
  let contentBottom = 0;
@@ -119,3 +127,102 @@ export function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options: ShelfOpt
119
127
  if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
120
128
  return outer;
121
129
  }
130
+
131
+ /**
132
+ * This function gives the first x from `start` where a rectangle of width `w` fits. There, it crosses no locked rectangle,
133
+ * and it is in a free span of its band. It takes `firstFreeX` and `firstFit` in turn until they agree. It gives `Infinity` when no x fits.
134
+ */
135
+ function placeInBand(obstacles: readonly Box[], spans: readonly FreeSpan[], start: number, w: number, top: number, bottom: number): number {
136
+ let x = start;
137
+ for (;;) {
138
+ const free = firstFreeX(obstacles, x, w, top, bottom);
139
+ const fit = firstFit(spans, free, w);
140
+ if (fit === free || !Number.isFinite(fit)) return fit;
141
+ x = fit;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * This function packs rectangles into rows inside a free space. `packShelf` and `packRows` use it when the space is not
147
+ * plain. It sets the position of each rectangle and gives the outer rectangle.
148
+ *
149
+ * - A rectangle goes to the first x from the row cursor where it fits in the space and crosses no locked rectangle.
150
+ * - When it does not fit, a new row starts under the current row. An empty row steps down to `nextY` of the space.
151
+ * - When the steps of an empty row find no place, the row goes back to its last top edge. Later rectangles can use it.
152
+ * - A rectangle wider than the outer rectangle overflows. Without `fixedSize`, it goes on its own row at the left edge.
153
+ */
154
+ export function packRowsInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder, space: FreeSpace): Rect<T> {
155
+ const width = outer.size.x;
156
+ const ox = outer.position.x;
157
+ const oy = outer.position.y;
158
+ const limit = outer.fixedSize ? outer.size.y : Infinity;
159
+ const obstacles: Box[] = [];
160
+ let contentBottom = 0;
161
+ for (const rect of rects) {
162
+ if (!rect.locked) continue;
163
+ rect.wasPacked = true;
164
+ const left = rect.position.x - ox;
165
+ const top = rect.position.y - oy;
166
+ obstacles.push({ left, top, right: left + rect.size.x, bottom: top + rect.size.y });
167
+ contentBottom = Math.max(contentBottom, top + rect.size.y);
168
+ }
169
+ // The x in the row, in the coordinates of the outer rectangle, or Infinity
170
+ const place = (start: number, w: number, top: number, h: number): number => {
171
+ const spans = space.band(top + oy, top + h + oy).map((s) => ({ start: s.start - ox, end: s.end - ox }));
172
+ return placeInBand(obstacles, spans, start, w, top, top + h);
173
+ };
174
+
175
+ let rowTop = 0;
176
+ let rowBottom = 0;
177
+ let cursor = 0;
178
+ let rowEmpty = true;
179
+ for (const rect of rects.toSorted(compareRects(order))) {
180
+ if (rect.wasPacked) continue;
181
+ const w = rect.size.x;
182
+ const h = rect.size.y;
183
+ if (!(w > 0 && h > 0)) continue;
184
+ if (w > width) {
185
+ if (outer.fixedSize) continue;
186
+ const top = rowEmpty ? rowTop : rowBottom;
187
+ rect.position.set(ox, top + oy);
188
+ rect.wasPacked = true;
189
+ rowTop = top + h;
190
+ rowBottom = rowTop;
191
+ cursor = 0;
192
+ rowEmpty = true;
193
+ contentBottom = Math.max(contentBottom, top + h);
194
+ continue;
195
+ }
196
+
197
+ let x = place(cursor, w, rowTop, h);
198
+ if (!(x + w <= width) && !rowEmpty) {
199
+ rowTop = rowBottom;
200
+ cursor = 0;
201
+ rowEmpty = true;
202
+ x = place(0, w, rowTop, h);
203
+ }
204
+ // The steps of an empty row. They stay only when the rectangle finds a place.
205
+ let top = rowTop;
206
+ while (!(x + w <= width) && top + h <= limit) {
207
+ const next = Math.min(space.nextY(top + oy, top + h + oy, w) - oy, nextTop(obstacles, top, top + h));
208
+ if (!Number.isFinite(next)) break;
209
+ top = next;
210
+ x = place(0, w, top, h);
211
+ }
212
+ if (!(x + w <= width) || top + h > limit) continue;
213
+ if (top !== rowTop) {
214
+ rowTop = top;
215
+ rowBottom = top;
216
+ }
217
+
218
+ rect.position.set(x + ox, rowTop + oy);
219
+ rect.wasPacked = true;
220
+ rowEmpty = false;
221
+ cursor = x + w;
222
+ rowBottom = Math.max(rowBottom, rowTop + h);
223
+ contentBottom = Math.max(contentBottom, rowTop + h);
224
+ }
225
+
226
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
227
+ return outer;
228
+ }
package/src/skyline.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  import { compareRects, type SortOrder } from "./order.ts";
2
2
  import type { Rect } from "./rect.ts";
3
+ import type { FreeSpace } from "./space.ts";
3
4
 
4
5
  /**
5
6
  * This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height
6
7
  * of the window is the largest value of `skyline[x, x + rw)`. The first `x` with the smallest height wins. A
7
8
  * monotonic deque makes the search O(width) for one rectangle.
9
+ *
10
+ * With `limit`, a window is valid only when its height plus `rh` is not more than the smallest value of
11
+ * `limit[x, x + rw)`. A second deque keeps that smallest value. No valid window gives the height `Infinity`.
8
12
  */
9
- export function lowestWindow(skyline: ArrayLike<number>, width: number, rw: number): { x: number; y: number } {
13
+ export function lowestWindow(skyline: ArrayLike<number>, width: number, rw: number, limit?: ArrayLike<number>, rh = 0): { x: number; y: number } {
14
+ if (limit !== undefined) return lowestLimitedWindow(skyline, limit, width, rw, rh);
10
15
  const deque = new Int32Array(width);
11
16
  let head = 0;
12
17
  let tail = 0;
@@ -27,6 +32,45 @@ export function lowestWindow(skyline: ArrayLike<number>, width: number, rw: numb
27
32
  return { x: bestX, y: bestY };
28
33
  }
29
34
 
35
+ /** 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 } {
37
+ const high = new Int32Array(width);
38
+ const low = new Int32Array(width);
39
+ let hh = 0;
40
+ let ht = 0;
41
+ let lh = 0;
42
+ let lt = 0;
43
+ let bestX = 0;
44
+ let bestY = Infinity;
45
+ for (let i = 0; i < width; i++) {
46
+ while (ht > hh && skyline[high[ht - 1]!]! <= skyline[i]!) ht--;
47
+ high[ht++] = i;
48
+ if (high[hh]! <= i - rw) hh++;
49
+ while (lt > lh && limit[low[lt - 1]!]! >= limit[i]!) lt--;
50
+ low[lt++] = i;
51
+ if (low[lh]! <= i - rw) lh++;
52
+ if (i >= rw - 1) {
53
+ const max = skyline[high[hh]!]!;
54
+ if (max < bestY && max + rh <= limit[low[lh]!]!) {
55
+ bestY = max;
56
+ bestX = i - rw + 1;
57
+ }
58
+ }
59
+ }
60
+ return { x: bestX, y: bestY };
61
+ }
62
+
63
+ /** The options of `packSkyline` */
64
+ export interface SkylineOptions {
65
+ /** The order of the rectangles before the packing. The default is `height-desc`. */
66
+ sortOrder?: SortOrder;
67
+ /**
68
+ * The free space of the container: `freeSpace` of the input. Without it, or when it is plain, the skyline uses only the
69
+ * outer rectangle and the locked rectangles.
70
+ */
71
+ space?: FreeSpace;
72
+ }
73
+
30
74
  /**
31
75
  * This function packs rectangles into an outer rectangle with a skyline. It puts each rectangle in the lowest place
32
76
  * where it fits, and it does not change the width of the outer rectangle.
@@ -38,8 +82,11 @@ export function lowestWindow(skyline: ArrayLike<number>, width: number, rw: numb
38
82
  * - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a rectangle that
39
83
  * does not fit is not packed: its `wasPacked` stays `false`.
40
84
  * - The default sort order is `height-desc`. The order of the array of the caller stays.
85
+ * - With a space that is not plain, each cell gets its start and its limit from `column` of the space. The space holds
86
+ * the obstacles and the locked items, thus the locked rectangles only keep their positions.
41
87
  */
42
- export function packSkyline<T>(rects: Rect<T>[], outer: Rect<T>, options: { sortOrder?: SortOrder } = {}): Rect<T> {
88
+ export function packSkyline<T>(rects: Rect<T>[], outer: Rect<T>, options: SkylineOptions = {}): Rect<T> {
89
+ if (options.space !== undefined && !options.space.plain) return packSkylineInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
43
90
  // a cell for each whole unit of the width: a rectangle never goes past the right edge
44
91
  const width = Math.floor(outer.size.x);
45
92
  if (width <= 0) return outer;
@@ -81,3 +128,79 @@ export function packSkyline<T>(rects: Rect<T>[], outer: Rect<T>, options: { sort
81
128
  }
82
129
  return outer;
83
130
  }
131
+
132
+ /**
133
+ * This function packs rectangles with a skyline in a free space. Each cell has a free range: the skyline is its start,
134
+ * and `limit` is its end. A cell with less room than the lowest rectangle left goes to its next free range.
135
+ */
136
+ function packSkylineInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder, space: FreeSpace): Rect<T> {
137
+ const width = Math.floor(outer.size.x);
138
+ if (width <= 0) return outer;
139
+ const ox = outer.position.x;
140
+ const oy = outer.position.y;
141
+ const end = outer.fixedSize ? outer.size.y : Infinity;
142
+ for (const rect of rects) if (rect.locked) rect.wasPacked = true;
143
+
144
+ // The free range of a cell from a y, in the coordinates of the outer rectangle
145
+ const rangeOf = (i: number, from: number): [number, number] => {
146
+ const r = space.column(ox + i, ox + i + 1, from + oy);
147
+ if (r === undefined) return [Infinity, -Infinity];
148
+ return [r.start - oy, Math.min(r.end - oy, end)];
149
+ };
150
+ const skyline = new Float64Array(width);
151
+ const limit = new Float64Array(width);
152
+ for (let i = 0; i < width; i++) [skyline[i], limit[i]] = rangeOf(i, 0);
153
+
154
+ const sorted = rects.toSorted(compareRects(order)).filter((r) => !r.wasPacked && Math.ceil(r.size.x) > 0 && r.size.y > 0);
155
+ // The lowest height of the rectangles from each index of the sorted list to its end
156
+ const lowest = new Float64Array(sorted.length + 1).fill(Infinity);
157
+ for (let k = sorted.length - 1; k >= 0; k--) lowest[k] = Math.min(lowest[k + 1]!, sorted[k]!.size.y);
158
+ const advance = (k: number): void => {
159
+ const need = lowest[k]!;
160
+ if (!Number.isFinite(need)) return;
161
+ for (let i = 0; i < width; i++) {
162
+ while (Number.isFinite(limit[i]!) && limit[i]! - skyline[i]! < need && limit[i]! < end) [skyline[i], limit[i]] = rangeOf(i, limit[i]!);
163
+ }
164
+ };
165
+
166
+ for (const [k, rect] of sorted.entries()) {
167
+ advance(k);
168
+ const rw = Math.ceil(rect.size.x);
169
+ const rh = rect.size.y;
170
+ let x: number;
171
+ let y: number;
172
+ if (rw > width) {
173
+ if (outer.fixedSize) continue;
174
+ x = 0;
175
+ y = 0;
176
+ for (let i = 0; i < width; i++) if (Number.isFinite(skyline[i]!) && skyline[i]! > y) y = skyline[i]!;
177
+ rect.position.set(x + ox, y + oy);
178
+ rect.wasPacked = true;
179
+ outer.size.y = Math.max(outer.size.y, y + rh);
180
+ continue;
181
+ }
182
+ ({ x, y } = lowestWindow(skyline, width, rw, limit, rh));
183
+ let sky: Float64Array = skyline;
184
+ let lim: Float64Array = limit;
185
+ if (!Number.isFinite(y)) {
186
+ // The second try: each cell goes to its first free range with room for this rectangle
187
+ sky = skyline.slice();
188
+ lim = limit.slice();
189
+ for (let i = 0; i < width; i++) {
190
+ while (Number.isFinite(lim[i]!) && lim[i]! - sky[i]! < rh && lim[i]! < end) [sky[i], lim[i]] = rangeOf(i, lim[i]!);
191
+ }
192
+ ({ x, y } = lowestWindow(sky, width, rw, lim, rh));
193
+ if (!Number.isFinite(y)) continue;
194
+ }
195
+
196
+ rect.position.set(x + ox, y + oy);
197
+ rect.wasPacked = true;
198
+ const top = y + rh;
199
+ for (let i = x; i < x + rw; i++) {
200
+ skyline[i] = top;
201
+ limit[i] = lim[i]!;
202
+ }
203
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);
204
+ }
205
+ return outer;
206
+ }