@mark1russell7/packing 0.2.3 → 0.2.5
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 +28 -2
- package/build/index.d.ts +146 -31
- package/build/index.d.ts.map +1 -1
- package/build/index.js +725 -42
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters.ts +110 -23
- package/src/frame.ts +37 -10
- package/src/index.ts +13 -2
- package/src/maxrects.ts +367 -0
- package/src/measures.ts +18 -7
- package/src/moves.ts +32 -3
- package/src/pack.ts +17 -5
- package/src/registry.ts +2 -2
- package/src/shelf.ts +200 -0
- package/src/space.ts +36 -1
package/src/shelf.ts
CHANGED
|
@@ -3,6 +3,18 @@ import { compareRects, type SortOrder } from "./order.ts";
|
|
|
3
3
|
import type { Rect } from "./rect.ts";
|
|
4
4
|
import { firstFit, type FreeSpace, type FreeSpan } from "./space.ts";
|
|
5
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
|
+
|
|
6
18
|
/** The options of `packShelf` */
|
|
7
19
|
export interface ShelfOptions {
|
|
8
20
|
/** The order of the rectangles before the packing. The default is `height-desc`. */
|
|
@@ -12,6 +24,11 @@ export interface ShelfOptions {
|
|
|
12
24
|
* outer rectangle and the locked rectangles.
|
|
13
25
|
*/
|
|
14
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;
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
/** An obstacle in the coordinates of the outer rectangle */
|
|
@@ -60,8 +77,10 @@ function nextTop(obstacles: readonly Box[], top: number, bottom: number): number
|
|
|
60
77
|
* - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
|
|
61
78
|
* rectangle that does not fit is not packed: its `wasPacked` stays `false`.
|
|
62
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`.
|
|
63
81
|
*/
|
|
64
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);
|
|
65
84
|
if (options.space !== undefined && !options.space.plain) return packRowsInSpace(rects, outer, options.sortOrder ?? "height-desc", options.space);
|
|
66
85
|
const width = outer.size.x;
|
|
67
86
|
const obstacles: Box[] = [];
|
|
@@ -229,3 +248,184 @@ export function packRowsInSpace<T>(rects: Rect<T>[], outer: Rect<T>, order: Sort
|
|
|
229
248
|
if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, contentBottom);
|
|
230
249
|
return outer;
|
|
231
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
|
+
}
|
package/src/space.ts
CHANGED
|
@@ -346,7 +346,7 @@ export function spaceOf(input: PackInput, frame: Frame): FreeSpace {
|
|
|
346
346
|
const across = transposeLayer(layer);
|
|
347
347
|
|
|
348
348
|
const band = (y0: number, y1: number): FreeSpan[] => spansOf(layer, y0, y1).map(([start, end]) => ({ start, end }));
|
|
349
|
-
|
|
349
|
+
const space: FreeSpace = {
|
|
350
350
|
w: frame.w,
|
|
351
351
|
h: frame.h,
|
|
352
352
|
plain,
|
|
@@ -368,6 +368,41 @@ export function spaceOf(input: PackInput, frame: Frame): FreeSpace {
|
|
|
368
368
|
return nextOf(layer, y0, y1, w);
|
|
369
369
|
},
|
|
370
370
|
};
|
|
371
|
+
const bare: Layer = { ...layer, holes: [] };
|
|
372
|
+
let bottom = 0;
|
|
373
|
+
for (const b of blockers) bottom = Math.max(bottom, b.c1 + Math.max(0, b.g));
|
|
374
|
+
PARTS.set(space, {
|
|
375
|
+
kind: layer.kind,
|
|
376
|
+
d,
|
|
377
|
+
holes: holes.map((r) => ({ x: r.a0, y: r.c0, w: r.a1 - r.a0, h: r.c1 - r.c0 })),
|
|
378
|
+
bottom: Number.isFinite(frame.h) ? frame.h : bottom,
|
|
379
|
+
bare: (y0, y1) => spansOf(bare, y0, y1).map(([start, end]) => ({ start, end })),
|
|
380
|
+
});
|
|
381
|
+
return space;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The parts of a free space of `spaceOf`. The packer `maxrects` reads them to make its first free rectangles. The
|
|
386
|
+
* package root does not export them.
|
|
387
|
+
*/
|
|
388
|
+
export interface SpaceParts {
|
|
389
|
+
/** The kind of the outline: `plain`, `box`, `round` or `rings`. */
|
|
390
|
+
readonly kind: "plain" | "box" | "round" | "rings";
|
|
391
|
+
/** The growth of a query for the outline: the inset less half the gap. */
|
|
392
|
+
readonly d: number;
|
|
393
|
+
/** The rectangle holes: the locked items and the rectangle obstacles, in the coordinates of the frame. */
|
|
394
|
+
readonly holes: readonly SpaceRect[];
|
|
395
|
+
/** The height to cut into bands. A fixed height gives the height. A free height gives the lowest edge of the blockers. */
|
|
396
|
+
readonly bottom: number;
|
|
397
|
+
/** This method gives the free x-spans of the outline and the blockers from `y0` to `y1`, without the rectangle holes. */
|
|
398
|
+
bare(y0: number, y1: number): FreeSpan[];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const PARTS = new WeakMap<FreeSpace, SpaceParts>();
|
|
402
|
+
|
|
403
|
+
/** This function gives the parts of a free space of `spaceOf`, or `undefined` for another free space. */
|
|
404
|
+
export function spaceParts(space: FreeSpace): SpaceParts | undefined {
|
|
405
|
+
return PARTS.get(space);
|
|
371
406
|
}
|
|
372
407
|
|
|
373
408
|
/**
|