@mark1russell7/packing 0.2.9 → 0.2.11

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.9",
4
+ "version": "0.2.11",
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/maxrects.ts CHANGED
@@ -90,9 +90,14 @@ function inside(a: Box, b: Box): boolean {
90
90
  function cut(free: Box[], b: Box, minW: number, minH: number): { free: Box[]; hits: number } {
91
91
  const kept: Box[] = [];
92
92
  const parts: Box[] = [];
93
+ // The side of the box at each part: 0 left, 1 right, 2 top, 3 bottom
94
+ const sides: number[] = [];
93
95
  let hits = 0;
94
- const add = (x0: number, y0: number, x1: number, y1: number): void => {
95
- if (x1 - x0 >= minW && y1 - y0 >= minH) parts.push({ x0, y0, x1, y1 });
96
+ const add = (side: number, x0: number, y0: number, x1: number, y1: number): void => {
97
+ if (x1 - x0 >= minW && y1 - y0 >= minH) {
98
+ parts.push({ x0, y0, x1, y1 });
99
+ sides.push(side);
100
+ }
96
101
  };
97
102
  for (const f of free) {
98
103
  if (b.x0 >= f.x1 || b.x1 <= f.x0 || b.y0 >= f.y1 || b.y1 <= f.y0) {
@@ -100,28 +105,67 @@ function cut(free: Box[], b: Box, minW: number, minH: number): { free: Box[]; hi
100
105
  continue;
101
106
  }
102
107
  hits++;
103
- if (b.x0 > f.x0) add(f.x0, f.y0, b.x0, f.y1);
104
- if (b.x1 < f.x1) add(b.x1, f.y0, f.x1, f.y1);
105
- if (b.y0 > f.y0) add(f.x0, f.y0, f.x1, b.y0);
106
- if (b.y1 < f.y1) add(f.x0, b.y1, f.x1, f.y1);
108
+ if (b.x0 > f.x0) add(0, f.x0, f.y0, b.x0, f.y1);
109
+ if (b.x1 < f.x1) add(1, b.x1, f.y0, f.x1, f.y1);
110
+ if (b.y0 > f.y0) add(2, f.x0, f.y0, f.x1, b.y0);
111
+ if (b.y1 < f.y1) add(3, f.x0, b.y1, f.x1, f.y1);
107
112
  }
113
+ if (parts.length === 0) return { free: kept, hits };
108
114
  // Each part is in the free rectangle that it comes from. Thus a kept rectangle is never in a part.
109
- const count = kept.length;
115
+ // A part spans the box on one axis and touches the box on the other axis. Thus a part of one side is not in a part of
116
+ // another side. A kept rectangle with the part in it has an edge on the same side of the box.
117
+ const near: Box[][] = [[], [], [], []];
118
+ for (const k of kept) {
119
+ if (k.x1 === b.x0) near[0]!.push(k);
120
+ if (k.x0 === b.x1) near[1]!.push(k);
121
+ if (k.y1 === b.y0) near[2]!.push(k);
122
+ if (k.y0 === b.y1) near[3]!.push(k);
123
+ }
124
+ // In the order of `byExtent`, a part comes after each part that holds it. Of two equal parts, the first part stays.
125
+ // Thus a part goes when a part that stays before it holds it.
126
+ const order = parts.map((_, i) => i).sort((i, j) => byExtent(parts[i]!, parts[j]!) || i - j);
127
+ const gone = new Uint8Array(parts.length);
128
+ const stay: Box[][] = [[], [], [], []];
129
+ for (const i of order) {
130
+ const p = parts[i]!;
131
+ const own = stay[sides[i]!]!;
132
+ let drop = false;
133
+ for (const q of own) {
134
+ if (inside(p, q)) {
135
+ drop = true;
136
+ break;
137
+ }
138
+ }
139
+ if (drop) gone[i] = 1;
140
+ else own.push(p);
141
+ }
110
142
  for (let i = 0; i < parts.length; i++) {
143
+ if (gone[i] === 1) continue;
111
144
  const p = parts[i]!;
112
145
  let drop = false;
113
- for (let j = 0; j < parts.length && !drop; j++) {
114
- if (j === i) continue;
115
- const q = parts[j]!;
116
- // Of two equal parts, the first part stays
117
- if (inside(p, q) && (j < i || !inside(q, p))) drop = true;
146
+ for (const k of near[sides[i]!]!) {
147
+ if (inside(p, k)) {
148
+ drop = true;
149
+ break;
150
+ }
118
151
  }
119
- for (let k = 0; k < count && !drop; k++) if (inside(p, kept[k]!)) drop = true;
120
152
  if (!drop) kept.push(p);
121
153
  }
122
154
  return { free: kept, hits };
123
155
  }
124
156
 
157
+ /**
158
+ * This function orders two boxes. The smaller `x0` comes first, then the smaller `y0`, then the larger `x1`, then the
159
+ * larger `y1`. Thus a box comes before each other box in it.
160
+ */
161
+ function byExtent(a: Box, b: Box): number {
162
+ if (a.x0 !== b.x0) return a.x0 < b.x0 ? -1 : 1;
163
+ if (a.y0 !== b.y0) return a.y0 < b.y0 ? -1 : 1;
164
+ if (a.x1 !== b.x1) return a.x1 > b.x1 ? -1 : 1;
165
+ if (a.y1 !== b.y1) return a.y1 > b.y1 ? -1 : 1;
166
+ return 0;
167
+ }
168
+
125
169
  /** The edges of the boxes on one axis: for each coordinate, the flat list of the ranges on the other axis. */
126
170
  type Edges = Map<number, number[]>;
127
171
 
@@ -315,10 +359,16 @@ export function runMaxRects<T>(rects: Rect<T>[], outer: Rect<T>, options: MaxRec
315
359
  s2 = f.y0 + bh;
316
360
  break;
317
361
  }
318
- const c: Choice = { index: i, turn: t === 1, x: f.x0, y: f.y0, w: bw, h: bh, s1, s2 };
319
- if (best === undefined || s1 < best.s1 || (s1 === best.s1 && (s2 < best.s2 || (s2 === best.s2 && (c.y < best.y || (c.y === best.y && c.x < best.x)))))) best = c;
320
- const ly = f.y0 + bh;
321
- if (low === undefined || ly < low.s1 || (ly === low.s1 && f.x0 < low.s2)) low = { ...c, s1: ly, s2: f.x0 };
362
+ const x = f.x0;
363
+ const y = f.y0;
364
+ // A choice object comes only for a better place
365
+ if (best === undefined || s1 < best.s1 || (s1 === best.s1 && (s2 < best.s2 || (s2 === best.s2 && (y < best.y || (y === best.y && x < best.x)))))) {
366
+ best = { index: i, turn: t === 1, x, y, w: bw, h: bh, s1, s2 };
367
+ }
368
+ // Only the move count of a rule other than `bl` reads the place of `bl`
369
+ if (rule === "bl") continue;
370
+ const ly = y + bh;
371
+ if (low === undefined || ly < low.s1 || (ly === low.s1 && x < low.s2)) low = { index: i, turn: t === 1, x, y, w: bw, h: bh, s1: ly, s2: x };
322
372
  }
323
373
  }
324
374
  if (best === undefined || !check || space.fits({ x: best.x + ox, y: best.y + oy, w: best.w, h: best.h })) break;
package/src/skyline.ts CHANGED
@@ -156,6 +156,9 @@ export function packSkyline<T>(rects: Rect<T>[], outer: Rect<T>, options: Skylin
156
156
  /**
157
157
  * This function packs rectangles with a skyline in a free space. Each cell has a free range: the skyline is its start,
158
158
  * and `limit` is its end. A cell with less room than the lowest rectangle left goes to its next free range.
159
+ *
160
+ * A rectangle that needs more cells than the outer rectangle has goes at the left edge. Its y is the first y where each
161
+ * cell is free over its height. Then each cell starts after the rectangle, as in the plain skyline.
159
162
  */
160
163
  function packSkylineInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder, space: FreeSpace): Rect<T> {
161
164
  const width = Math.floor(outer.size.x);
@@ -198,9 +201,29 @@ function packSkylineInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrde
198
201
  x = 0;
199
202
  y = 0;
200
203
  for (let i = 0; i < width; i++) if (Number.isFinite(skyline[i]!) && skyline[i]! > y) y = skyline[i]!;
204
+ // The rectangle covers each cell, thus it goes to the first y where each cell is free over its height
205
+ for (let moved = true; moved; ) {
206
+ moved = false;
207
+ for (let i = 0; i < width; i++) {
208
+ if (!Number.isFinite(skyline[i]!)) continue;
209
+ const [s, e] = rangeOf(i, y);
210
+ const next = s > y ? s : e < y + rh ? rangeOf(i, e)[0] : y;
211
+ if (next > y && Number.isFinite(next)) {
212
+ y = next;
213
+ moved = true;
214
+ }
215
+ }
216
+ }
201
217
  rect.position.set(x + ox, y + oy);
202
218
  rect.wasPacked = true;
203
- outer.size.y = Math.max(outer.size.y, y + rh);
219
+ const top = y + rh;
220
+ // Each cell starts after the rectangle. A cell with a range that ends before the top of the rectangle goes to its next range.
221
+ for (let i = 0; i < width; i++) {
222
+ if (!Number.isFinite(skyline[i]!)) continue;
223
+ if (top <= limit[i]!) skyline[i] = top;
224
+ else [skyline[i], limit[i]] = rangeOf(i, top);
225
+ }
226
+ outer.size.y = Math.max(outer.size.y, top);
204
227
  continue;
205
228
  }
206
229
  let found = findWindow(skyline, width, rw, limit, rh);