@mark1russell7/packing 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,68 @@
1
- # Temporary Holding Version
1
+ # @mark1russell7/packing
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@mark1russell7/packing?color=2346c4)](https://www.npmjs.com/package/@mark1russell7/packing) [![CI](https://github.com/mark1russell7/packing/actions/workflows/ci.yml/badge.svg)](https://github.com/mark1russell7/packing/actions/workflows/ci.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-2346c4)](https://github.com/mark1russell7/packing/blob/main/packages/packing/LICENSE)
4
+
5
+ Rectangle packing for layouts: a guillotine row packer and a skyline packer that place rectangles in an outer rectangle.
6
+
7
+ ```sh
8
+ npm i @mark1russell7/packing
9
+ ```
10
+
11
+ ```ts
12
+ import { pack, Rect } from "@mark1russell7/packing";
13
+
14
+ const rects = [[30, 10], [20, 20], [10, 5]].map(([w, h]) => {
15
+ const r = new Rect<string>();
16
+ r.size.set(w, h);
17
+ return r;
18
+ });
19
+ const outer = pack(rects);
20
+ outer.size; // the size of the layout
21
+ rects[0].position; // the place of the first rectangle
22
+ ```
23
+
24
+ Without fixed sizes, the outer rectangle grows until all rectangles fit. With `fixedWidth`, the width stays. With `fixedSize`, a rectangle that does not fit stays unpacked.
25
+
26
+ ## Strategies
27
+
28
+ `pack` sets the width of the outer rectangle, and then packs with a strategy:
29
+
30
+ ```ts
31
+ pack(rects, outer, { strategy: "skyline", sortOrder: "area-desc" });
32
+ ```
33
+
34
+ - `packRows` (the default): a guillotine row packing. It fills the space under the short rectangles of each row.
35
+ - `packSkyline`: each rectangle goes to the lowest place where it fits. It packs around a `locked` rectangle.
36
+ - `packShelf`: it fills rows left to right in the sort order, and goes around each `locked` rectangle. With the sort order `input`, the rows read in your order.
37
+
38
+ Both functions also work alone, in an outer rectangle with a width that you set. They do not change the width.
39
+
40
+ A `locked` rectangle keeps its position. `packSkyline` and `packShelf` put the other rectangles around it. `packRows` only keeps its position.
41
+
42
+ The sort orders are `height-desc` (the default), `height-asc`, `width-desc`, `area-desc` and `input`. `SORT_ORDERS` lists them, and `compareRects` gives the comparator of one.
43
+
44
+ The order `input` keeps the order of your array. Use it for a ranked list. `packRows` then fills no gaps, so the rows read in your order: left to right, and top to bottom. `packSkyline` places the rectangles in your order, but a later rectangle can go higher than an earlier one. For a ranked list with `locked` rectangles, use `packShelf`.
45
+
46
+ ## Pushes and measurements
47
+
48
+ `pushCascade` moves the rectangles that a placed rectangle overlaps, and the rectangles that those overlap in turn:
49
+
50
+ ```ts
51
+ const moved = pushCascade(dropped, cards, { gap: 8 });
52
+ ```
53
+
54
+ - Each shove takes the axis of the smaller overlap, away from the pusher. The cascade goes outward, and a rectangle does not go past 0.
55
+ - The placed rectangle and each `locked` rectangle stay. `maxShoves` limits the work.
56
+
57
+ `packingMetrics` measures a packing: the hull and its aspect, the rows and the raggedness of the right side. It also gives the part of the hull that the rectangles cover (`utilization`), and the widest rectangle against the median.
58
+
59
+ ```ts
60
+ const { utilization, raggedness, rows } = packingMetrics(Rect.packed(rects));
61
+ ```
62
+
63
+ ## More
64
+
65
+ - [The repository](https://github.com/mark1russell7/packing): the source, the tests and the roadmap.
66
+ - This package was `@mark1russell7/render-pack` before 0.1.0. `render-pack` re-exports this package for one more minor, and then it is deprecated.
67
+
68
+ The package is ESM, has TypeScript declarations, and works in Node.js 22.12 or later and in browsers. The license is MIT.
@@ -0,0 +1,209 @@
1
+ /** A mutable two-dimensional vector. */
2
+ export declare class Vector {
3
+ #private;
4
+ constructor(x?: number, y?: number);
5
+ /** The first component: a width or a left position. */
6
+ get x(): number;
7
+ set x(v: number);
8
+ /** The second component: a height or a top position. */
9
+ get y(): number;
10
+ set y(v: number);
11
+ /** This method sets both components. */
12
+ set(x: number, y: number): void;
13
+ /** This method copies the components of another vector. */
14
+ setTo(v: Vector): void;
15
+ /** This method gives a new vector: the sum of this vector and another. */
16
+ add(other: Vector): Vector;
17
+ /** This method gives the product of the components: the area of a size. */
18
+ area(): number;
19
+ /** This method gives a new vector with the same components. */
20
+ clone(): Vector;
21
+ /** This method gives a new vector with each component between the components of `min` and `max`. */
22
+ clamp(min: Vector, max: Vector): Vector;
23
+ /** This function gives a vector with the same value in both components. */
24
+ static of(scalar: number): Vector;
25
+ /** This function gives the smallest value of each component, or a zero vector for an empty list. */
26
+ static min(vectors: Vector[]): Vector;
27
+ /** This function gives the largest value of each component, or a zero vector for an empty list. */
28
+ static max(vectors: Vector[]): Vector;
29
+ /** This getter gives a new infinite vector each time. A shared value is not safe, because a vector is mutable. */
30
+ static get Infinity(): Vector;
31
+ }
32
+ /** A rectangle of the packer. `id` identifies the item that it measures. The packer sets `position` and `wasPacked`. */
33
+ export declare class Rect<T = unknown> {
34
+ #private;
35
+ /** The identity of the item that the rectangle measures. */
36
+ id?: T;
37
+ /** The packer sets this field to `true` when it places the rectangle. */
38
+ wasPacked: boolean;
39
+ /** For an outer rectangle: the packer keeps its width. */
40
+ fixedWidth: boolean;
41
+ /** For an outer rectangle: the packer keeps its width and its height, and it skips a rectangle that does not fit. */
42
+ fixedSize: boolean;
43
+ /**
44
+ * The packer keeps the position of a locked rectangle. `packSkyline` packs the other rectangles around it.
45
+ * `packRows` only keeps its position, and other rectangles can overlap it.
46
+ */
47
+ locked: boolean;
48
+ /** The top left corner. */
49
+ get position(): Vector;
50
+ /** The width and the height. */
51
+ get size(): Vector;
52
+ /** This method gives the right edge. */
53
+ right(): number;
54
+ /** This method gives the bottom edge. */
55
+ bottom(): number;
56
+ /** This method moves the left edge to the right by an amount, and the right edge stays. */
57
+ shrinkLeft(amount: number): this;
58
+ /** This method moves the rectangle by an offset. */
59
+ translateBy(offset: Vector): this;
60
+ /** This method gives the area of the size. */
61
+ area(): number;
62
+ /** This function gives the rectangles that the packer placed. */
63
+ static packed<U>(rects: Rect<U>[]): Rect<U>[];
64
+ /** This function gives the rectangles that the packer did not place. */
65
+ static unpacked<U>(rects: Rect<U>[]): Rect<U>[];
66
+ /** This function marks each rectangle as not placed. A locked rectangle stays placed. */
67
+ static resetPacked<U>(rects: Rect<U>[]): void;
68
+ /** This function gives the largest right edge, or 0 for an empty list. */
69
+ static maxRight<U>(rects: Rect<U>[]): number;
70
+ /** This function gives the largest bottom edge, or 0 for an empty list. */
71
+ static maxBottom<U>(rects: Rect<U>[]): number;
72
+ }
73
+ /**
74
+ * An order of the rectangles before the packing. The order changes the layout much. `input` keeps the order of
75
+ * the array of the caller: the row packing then fills no gaps, so its rows read in that order.
76
+ */
77
+ type SortOrder = "height-desc" | "height-asc" | "width-desc" | "area-desc" | "input";
78
+ /** This constant lists each sort order. The first one is the default. */
79
+ export declare const SORT_ORDERS: readonly SortOrder[];
80
+ /** This function gives the comparator of a sort order. The sort is stable, so `input` keeps the order. */
81
+ export declare function compareRects(order: SortOrder): <T>(a: Rect<T>, b: Rect<T>) => number;
82
+ /** The options of `pack` and `packRows` */
83
+ interface PackOptions {
84
+ /** The order of the rectangles before the packing. The default is `height-desc`. */
85
+ sortOrder?: SortOrder;
86
+ }
87
+ /** The options of `pack` */
88
+ interface PackLayoutOptions extends PackOptions {
89
+ /**
90
+ * The packing: `rows` (the default), `skyline` or `shelf`. `skyline` and `shelf` pack around locked rectangles.
91
+ * Use `shelf` with the sort order `input` to keep the order of the array around locked rectangles.
92
+ */
93
+ strategy?: "rows" | "skyline" | "shelf";
94
+ }
95
+ /**
96
+ * This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does
97
+ * not change the width. Without `fixedSize`, the height grows until all rectangles fit. A `locked` rectangle keeps
98
+ * its position, but the rows do not go around it. `pack` sets the width first and then uses this function.
99
+ */
100
+ export declare function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options?: PackOptions): Rect<T>;
101
+ /**
102
+ * This function packs rectangles into an outer rectangle with a guillotine row packing. It sets the position
103
+ * of each rectangle and the size of the outer rectangle, and it gives the outer rectangle.
104
+ *
105
+ * - Without fixed sizes, the outer rectangle starts with a width of 1.5 times the square root of the total area.
106
+ * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
107
+ * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
108
+ * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
109
+ * - `options.strategy` selects `packRows` (the default), `packSkyline` or `packShelf` after the width is set.
110
+ */
111
+ export declare function pack<T>(rects: Rect<T>[], outer?: Rect<T>, options?: PackLayoutOptions): Rect<T>;
112
+ /**
113
+ * This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height
114
+ * of the window is the largest value of `skyline[x, x + rw)`. The first `x` with the smallest height wins. A
115
+ * monotonic deque makes the search O(width) for one rectangle.
116
+ */
117
+ export declare function lowestWindow(skyline: ArrayLike<number>, width: number, rw: number): {
118
+ x: number;
119
+ y: number;
120
+ };
121
+ /**
122
+ * This function packs rectangles into an outer rectangle with a skyline. It puts each rectangle in the lowest place
123
+ * where it fits, and it does not change the width of the outer rectangle.
124
+ *
125
+ * - A `locked` rectangle keeps its position. The skyline starts at its bottom edge across its width, thus the other
126
+ * rectangles go under it. The space above it stays empty, because a skyline has no holes.
127
+ * - The skyline has a cell for each whole unit of the width. A rectangle that needs more cells than the outer
128
+ * rectangle has goes under all other rectangles, at the left edge.
129
+ * - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a rectangle that
130
+ * does not fit is not packed: its `wasPacked` stays `false`.
131
+ * - The default sort order is `height-desc`. The order of the array of the caller stays.
132
+ */
133
+ export declare function packSkyline<T>(rects: Rect<T>[], outer: Rect<T>, options?: {
134
+ sortOrder?: SortOrder;
135
+ }): Rect<T>;
136
+ /** The options of `packShelf` */
137
+ interface ShelfOptions {
138
+ /** The order of the rectangles before the packing. The default is `height-desc`. */
139
+ sortOrder?: SortOrder;
140
+ }
141
+ /**
142
+ * This function packs rectangles into shelves: rows that it fills left to right in the sort order, and does not fill
143
+ * again. It goes around each `locked` rectangle. It sets the position of each rectangle and gives the outer rectangle.
144
+ *
145
+ * - With the sort order `input`, the rows read in the order of the array: left to right, and top to bottom.
146
+ * - With `height-desc` (the default), it is the classic Next-Fit Decreasing Height packing.
147
+ * - A rectangle goes to the first x in the row where it crosses no locked rectangle. If no such x is in the width,
148
+ * a new row starts under the current row.
149
+ * - A rectangle wider than the outer rectangle goes on its own row, at the left edge, and overflows.
150
+ * - It does not change the width. Without `fixedSize`, the height grows to the content. With `fixedSize`, a
151
+ * rectangle that does not fit is not packed: its `wasPacked` stays `false`.
152
+ */
153
+ export declare function packShelf<T>(rects: Rect<T>[], outer: Rect<T>, options?: ShelfOptions): Rect<T>;
154
+ /** The options of `pushCascade`. */
155
+ interface PushOptions {
156
+ /** The space between a shoved rectangle and the rectangle that shoves it. The default is 8. */
157
+ gap?: number;
158
+ /** The most shoves. The default is eight for each rectangle. The cascade stops at this limit. */
159
+ maxShoves?: number;
160
+ }
161
+ /**
162
+ * This function moves each rectangle that a placed rectangle overlaps, and the rectangles that those overlap in turn.
163
+ * A shove takes the axis of the smaller overlap, away from the pusher. A shoved rectangle keeps the direction of its first
164
+ * shove when it shoves the next one, so the cascade goes outward. The placed rectangle and each locked rectangle stay.
165
+ * The function moves the rectangles in place and gives the rectangles that moved.
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * const moved = pushCascade(dropped, cards, { gap: 8 });
170
+ * ```
171
+ */
172
+ export declare function pushCascade<T>(placed: Rect<T>, rects: readonly Rect<T>[], options?: PushOptions): Rect<T>[];
173
+ /** The measurements of a packing. */
174
+ interface PackingMetrics {
175
+ /** The number of rectangles. */
176
+ count: number;
177
+ /** The size of the hull: the smallest box around the rectangles. */
178
+ hull: {
179
+ width: number;
180
+ height: number;
181
+ };
182
+ /** The width of the hull divided by its height. A hull without height gives 0. */
183
+ hullAspect: number;
184
+ /** The number of rectangles in each row, from the top. */
185
+ rows: number[];
186
+ /** The largest right edge of a row less the smallest right edge of a row. A straight right side gives 0. */
187
+ raggedness: number;
188
+ /** The part of the hull that the rectangles cover: their area divided by the area of the hull. */
189
+ utilization: number;
190
+ /** The width of the widest rectangle divided by the median width. */
191
+ widestOverMedian: number;
192
+ }
193
+ /** The options of `packingMetrics`. */
194
+ interface MetricsOptions {
195
+ /** A rectangle joins a row when its top is this close to the top of the first rectangle of the row. The default is 40. */
196
+ rowTolerance?: number;
197
+ }
198
+ /**
199
+ * This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.
200
+ * It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
205
+ * ```
206
+ */
207
+ export declare function packingMetrics(rects: readonly Rect[], options?: MetricsOptions): PackingMetrics;
208
+ export type { MetricsOptions, PackLayoutOptions, PackOptions, PackingMetrics, PushOptions, ShelfOptions, SortOrder };
209
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/vector.ts","../src/rect.ts","../src/order.ts","../src/pack.ts","../src/skyline.ts","../src/shelf.ts","../src/push.ts","../src/metrics.ts"],"mappings":";qBACa;;EAIX,YAAY,YAAO;;MAMf;MACA,EAAE;;MAEF;MACA,EAAE;;EAGN,IAAI,WAAW;;EAMf,MAAM,GAAG;;EAMT,IAAI,OAAO,SAAS;;EAKpB;;EAKA,SAAS;;EAKT,MAAM,KAAK,QAAQ,KAAK,SAAS;;SAQ1B,GAAG,iBAAiB;;SAKpB,IAAI,SAAS,WAAW;;SASxB,IAAI,SAAS,WAAW;;aASpB,YAAY;;;qBCxEZ,KAAK;;;EAKhB,KAAK;;EAEL;;EAEA;;EAEA;;;;;EAKA;;MAGI,YAAY;;MAEZ,QAAQ;;EAGZ;;EAEA;;EAGA,WAAW;;EAOX,YAAY,QAAQ;;EAMpB;;SAGO,OAAO,GAAG,OAAO,KAAK,OAAO,KAAK;;SAKlC,SAAS,GAAG,OAAO,KAAK,OAAO,KAAK;;SAKpC,YAAY,GAAG,OAAO,KAAK;;SAK3B,SAAS,GAAG,OAAO,KAAK;;SAKxB,UAAU,GAAG,OAAO,KAAK;;;;;;KC/DtB;;qBAGC,sBAAsB;;wBAGnB,aAAa,OAAO,aAAa,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK;;UCLvD;;EAEf,YAAY;;;UAIG,0BAA0B;;;;;EAKzC;;;;;;;wBAqEc,SAAS,GAAG,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,UAAS,cAAmB,KAAK;;;;;;;;;;;wBAiB/E,KAAK,GAAG,OAAO,KAAK,MAAM,QAAO,KAAK,IAAiB,UAAS,oBAAyB,KAAK;;;;;;wBChG9F,aAAa,SAAS,mBAAmB,eAAe;EAAe;EAAW;;;;;;;;;;;;;;wBAiClF,YAAY,GAAG,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI;EAAW,YAAY;IAAmB,KAAK;;UCrC/F;;EAEf,YAAY;;;;;;;;;;;;;;wBAiDE,UAAU,GAAG,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,UAAS,eAAoB,KAAK;;UCpDhF;;EAEf;;EAEA;;;;;;;;;;;;;wBAqDc,YAAY,GAAG,QAAQ,KAAK,IAAI,gBAAgB,KAAK,MAAM,UAAS,cAAmB,KAAK;;UCzD3F;;EAEf;;EAEA;IAAQ;IAAe;;;EAEvB;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIe;;EAEf;;;;;;;;;;;wBAYc,eAAe,gBAAgB,QAAQ,UAAS,iBAAsB"}