@mark1russell7/packing 0.0.0-bootstrap.0 → 0.1.0

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/src/metrics.ts ADDED
@@ -0,0 +1,72 @@
1
+ import type { Rect } from "./rect.ts";
2
+
3
+ /** The measurements of a packing. */
4
+ export interface PackingMetrics {
5
+ /** The number of rectangles. */
6
+ count: number;
7
+ /** The size of the hull: the smallest box around the rectangles. */
8
+ hull: { width: number; height: number };
9
+ /** The width of the hull divided by its height. A hull without height gives 0. */
10
+ hullAspect: number;
11
+ /** The number of rectangles in each row, from the top. */
12
+ rows: number[];
13
+ /** The largest right edge of a row less the smallest right edge of a row. A straight right side gives 0. */
14
+ raggedness: number;
15
+ /** The part of the hull that the rectangles cover: their area divided by the area of the hull. */
16
+ utilization: number;
17
+ /** The width of the widest rectangle divided by the median width. */
18
+ widestOverMedian: number;
19
+ }
20
+
21
+ /** The options of `packingMetrics`. */
22
+ export interface MetricsOptions {
23
+ /** 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. */
24
+ rowTolerance?: number;
25
+ }
26
+
27
+ /**
28
+ * This function measures a packing. It gives the hull and its aspect, the rows and the raggedness of the right side.
29
+ * It also gives the part of the hull that the rectangles cover, and the widest rectangle against the median width.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const { utilization, raggedness } = packingMetrics(Rect.packed(cards));
34
+ * ```
35
+ */
36
+ export function packingMetrics(rects: readonly Rect[], options: MetricsOptions = {}): PackingMetrics {
37
+ const tolerance = options.rowTolerance ?? 40;
38
+ const boxes = rects.filter((r) => r.size.x > 0 && r.size.y > 0);
39
+ if (boxes.length === 0) return { count: 0, hull: { width: 0, height: 0 }, hullAspect: 0, rows: [], raggedness: 0, utilization: 0, widestOverMedian: 0 };
40
+
41
+ const left = Math.min(...boxes.map((r) => r.position.x));
42
+ const top = Math.min(...boxes.map((r) => r.position.y));
43
+ const right = Math.max(...boxes.map((r) => r.right()));
44
+ const bottom = Math.max(...boxes.map((r) => r.bottom()));
45
+ const hull = { width: right - left, height: bottom - top };
46
+
47
+ // The rows: the rectangles by their top, and a row takes each rectangle near the top of its first rectangle
48
+ const rows: { top: number; right: number; count: number }[] = [];
49
+ for (const r of boxes.toSorted((a, b) => a.position.y - b.position.y)) {
50
+ const row = rows.at(-1);
51
+ if (row && Math.abs(r.position.y - row.top) <= tolerance) {
52
+ row.right = Math.max(row.right, r.right());
53
+ row.count += 1;
54
+ } else {
55
+ rows.push({ top: r.position.y, right: r.right(), count: 1 });
56
+ }
57
+ }
58
+ const rights = rows.map((row) => row.right);
59
+ const widths = boxes.map((r) => r.size.x).toSorted((a, b) => a - b);
60
+ const median = widths[Math.floor(widths.length / 2)] ?? 0;
61
+ const widest = widths.at(-1) ?? 0;
62
+ const area = boxes.reduce((sum, r) => sum + r.area(), 0);
63
+ return {
64
+ count: boxes.length,
65
+ hull,
66
+ hullAspect: hull.height > 0 ? hull.width / hull.height : 0,
67
+ rows: rows.map((row) => row.count),
68
+ raggedness: Math.max(...rights) - Math.min(...rights),
69
+ utilization: hull.width * hull.height > 0 ? area / (hull.width * hull.height) : 0,
70
+ widestOverMedian: median > 0 ? widest / median : 0,
71
+ };
72
+ }
package/src/order.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { Rect } from "./rect.ts";
2
+
3
+ /**
4
+ * An order of the rectangles before the packing. The order changes the layout much. `input` keeps the order of
5
+ * the array of the caller: the row packing then fills no gaps, so its rows read in that order.
6
+ */
7
+ export type SortOrder = "height-desc" | "height-asc" | "width-desc" | "area-desc" | "input";
8
+
9
+ /** This constant lists each sort order. The first one is the default. */
10
+ export const SORT_ORDERS: readonly SortOrder[] = ["height-desc", "height-asc", "width-desc", "area-desc", "input"];
11
+
12
+ /** This function gives the comparator of a sort order. The sort is stable, so `input` keeps the order. */
13
+ export function compareRects(order: SortOrder): <T>(a: Rect<T>, b: Rect<T>) => number {
14
+ switch (order) {
15
+ case "height-desc":
16
+ return (a, b) => b.size.y - a.size.y;
17
+ case "height-asc":
18
+ return (a, b) => a.size.y - b.size.y;
19
+ case "width-desc":
20
+ return (a, b) => b.size.x - a.size.x;
21
+ case "area-desc":
22
+ return (a, b) => b.area() - a.area();
23
+ case "input":
24
+ return () => 0;
25
+ }
26
+ }
package/src/pack.ts ADDED
@@ -0,0 +1,132 @@
1
+ import { compareRects, type SortOrder } from "./order.ts";
2
+ import { Rect } from "./rect.ts";
3
+ import { packSkyline } from "./skyline.ts";
4
+ import { Vector } from "./vector.ts";
5
+
6
+ /** The options of `pack` and `packRows` */
7
+ export interface PackOptions {
8
+ /** The order of the rectangles before the packing. The default is `height-desc`. */
9
+ sortOrder?: SortOrder;
10
+ }
11
+
12
+ /** The options of `pack` */
13
+ export interface PackLayoutOptions extends PackOptions {
14
+ /** The packing: `rows` (the default) or `skyline`. Use `skyline` to pack around locked rectangles. */
15
+ strategy?: "rows" | "skyline";
16
+ }
17
+
18
+ function packInto<T>(rects: Rect<T>[], region: Rect<T>, offset: Vector, order: SortOrder): void {
19
+ const unpacked = Rect.unpacked(rects);
20
+ pack(unpacked, region, { sortOrder: order });
21
+ for (const r of Rect.packed(unpacked)) r.translateBy(offset);
22
+ }
23
+
24
+ function packRow<T>(rects: Rect<T>[], outer: Rect<T>, order: SortOrder): boolean {
25
+ const row: Rect<T>[] = [];
26
+ // toSorted gives a sorted copy, thus the order of the array of the caller stays
27
+ const byHeight = rects.toSorted(compareRects(order));
28
+ // A gap fill puts a later rect before an earlier one, thus the order `input` fills no gaps
29
+ const fillGaps = order !== "input";
30
+
31
+ const rowRect = new Rect<T>();
32
+ rowRect.size.set(outer.size.x, 0);
33
+
34
+ for (const rect of byHeight) {
35
+ if (rect.wasPacked) continue;
36
+
37
+ if (rect.size.x > rowRect.size.x) {
38
+ if (outer.fixedSize && rect.size.x > outer.size.x) continue;
39
+
40
+ const outerLeft = new Rect<T>();
41
+ outerLeft.position.setTo(rowRect.position);
42
+ outerLeft.size.set(outer.size.x - rowRect.position.x, rowRect.size.y);
43
+ outerLeft.fixedSize = true;
44
+ if (fillGaps) packInto(rects, outerLeft, outer.position, order);
45
+
46
+ rowRect.position.set(0, rowRect.bottom());
47
+ rowRect.size.set(outer.size.x, 0);
48
+ row.length = 0;
49
+ }
50
+
51
+ if (rowRect.position.y + rect.size.y > outer.size.y) {
52
+ if (outer.fixedSize) continue;
53
+ outer.size.y += rect.size.y;
54
+ if (!outer.fixedWidth && !outer.size.x) outer.size.x = rect.size.x;
55
+ Rect.resetPacked(rects);
56
+ return false;
57
+ }
58
+
59
+ row.push(rect);
60
+ rect.position.setTo(rowRect.position.add(outer.position));
61
+ rowRect.shrinkLeft(rect.size.x);
62
+ rect.wasPacked = true;
63
+
64
+ if (rect.size.y > rowRect.size.y) rowRect.size.y = rect.size.y;
65
+
66
+ if (fillGaps && row.length > 1) {
67
+ const rectInRow = row[row.length - 1]!;
68
+ const outerLeft = new Rect<T>();
69
+ outerLeft.position.set(rectInRow.position.x, rectInRow.bottom());
70
+ outerLeft.size.set(rectInRow.size.x, rowRect.size.y - rectInRow.size.y);
71
+ outerLeft.fixedWidth = true;
72
+ outerLeft.fixedSize = true;
73
+ packInto(rects, outerLeft, outer.position, order);
74
+ }
75
+ }
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * This function packs rectangles into an outer rectangle of a given width with a guillotine row packing. It does
81
+ * not change the width. Without `fixedSize`, the height grows until all rectangles fit. A `locked` rectangle keeps
82
+ * its position, but the rows do not go around it. `pack` sets the width first and then uses this function.
83
+ */
84
+ export function packRows<T>(rects: Rect<T>[], outer: Rect<T>, options: PackOptions = {}): Rect<T> {
85
+ const order = options.sortOrder ?? "height-desc";
86
+ for (const r of rects) if (r.locked) r.wasPacked = true;
87
+ while (!packRow(rects, outer, order));
88
+ return outer;
89
+ }
90
+
91
+ /**
92
+ * This function packs rectangles into an outer rectangle with a guillotine row packing. It sets the position
93
+ * of each rectangle and the size of the outer rectangle, and it gives the outer rectangle.
94
+ *
95
+ * - Without fixed sizes, the outer rectangle starts with a width of 1.5 times the square root of the total area.
96
+ * This width is not less than the width of the widest rectangle. The height grows until all rectangles fit.
97
+ * - With `fixedWidth`, the width stays. A rectangle wider than that width goes on its own row and overflows.
98
+ * - With `fixedSize`, a rectangle that does not fit is not packed: its `wasPacked` stays `false`.
99
+ * - `options.strategy` selects `packRows` (the default) or `packSkyline` after the width is set.
100
+ */
101
+ export function pack<T>(rects: Rect<T>[], outer: Rect<T> = new Rect(), options: PackLayoutOptions = {}): Rect<T> {
102
+ let totalArea = 0;
103
+ let widest = 0;
104
+ let minX = rects.length > 0 ? Infinity : 0;
105
+ let minY = rects.length > 0 ? Infinity : 0;
106
+ for (const r of rects) {
107
+ totalArea += r.area();
108
+ widest = Math.max(widest, r.size.x);
109
+ minX = Math.min(minX, r.size.x);
110
+ minY = Math.min(minY, r.size.y);
111
+ }
112
+
113
+ const idealWidth = outer.fixedWidth
114
+ ? outer.size.x
115
+ : Math.max(widest, Math.min(1.5 * Math.sqrt(totalArea), outer.size.x || Infinity));
116
+
117
+ const ideal = Vector.of(idealWidth).clamp(new Vector(minX, minY), Vector.Infinity);
118
+
119
+ // A fixed width stays, also when a rectangle is wider: that rectangle overflows
120
+ if (!outer.fixedSize) outer.size.setTo(outer.fixedWidth ? new Vector(outer.size.x, ideal.y) : ideal);
121
+ if (options.strategy === "skyline") packSkyline(rects, outer, options);
122
+ else packRows(rects, outer, options);
123
+
124
+ if (!outer.fixedSize) {
125
+ const contentRight = Rect.maxRight(rects);
126
+ if (!outer.size.x) outer.size.x = contentRight;
127
+ else if (!outer.fixedWidth) outer.size.x = Math.min(contentRight, outer.size.x);
128
+ outer.size.y = Rect.maxBottom(rects);
129
+ }
130
+
131
+ return outer;
132
+ }
package/src/push.ts ADDED
@@ -0,0 +1,83 @@
1
+ import type { Rect } from "./rect.ts";
2
+
3
+ /** The options of `pushCascade`. */
4
+ export interface PushOptions {
5
+ /** The space between a shoved rectangle and the rectangle that shoves it. The default is 8. */
6
+ gap?: number;
7
+ /** The most shoves. The default is eight for each rectangle. The cascade stops at this limit. */
8
+ maxShoves?: number;
9
+ }
10
+
11
+ /** A direction of a shove: an axis and a sign. */
12
+ interface Direction {
13
+ readonly axis: "x" | "y";
14
+ readonly sign: 1 | -1;
15
+ }
16
+
17
+ /** This function gives the overlap of two rectangles on each axis. A value of 0 or less is no overlap. */
18
+ function overlapOf(a: Rect, b: Rect): { x: number; y: number } {
19
+ return {
20
+ x: Math.min(a.right(), b.right()) - Math.max(a.position.x, b.position.x),
21
+ y: Math.min(a.bottom(), b.bottom()) - Math.max(a.position.y, b.position.y),
22
+ };
23
+ }
24
+
25
+ /**
26
+ * This function gives the direction of a first shove. The axis has the smaller overlap. The sign points away from the
27
+ * center of the pusher. When that shove crosses 0, the sign is positive.
28
+ */
29
+ function firstDirection(pusher: Rect, r: Rect, gap: number): Direction {
30
+ const overlap = overlapOf(pusher, r);
31
+ if (overlap.x < overlap.y) {
32
+ const back = r.position.x + r.size.x / 2 < pusher.position.x + pusher.size.x / 2 && pusher.position.x - r.size.x - gap >= 0;
33
+ return { axis: "x", sign: back ? -1 : 1 };
34
+ }
35
+ const back = r.position.y + r.size.y / 2 < pusher.position.y + pusher.size.y / 2 && pusher.position.y - r.size.y - gap >= 0;
36
+ return { axis: "y", sign: back ? -1 : 1 };
37
+ }
38
+
39
+ /** This function puts a rectangle past the pusher, along a direction. The rectangle does not go past 0. */
40
+ function shove(pusher: Rect, r: Rect, direction: Direction, gap: number): void {
41
+ if (direction.axis === "x") {
42
+ const x = direction.sign > 0 ? pusher.right() + gap : pusher.position.x - r.size.x - gap;
43
+ r.position.set(Math.max(0, x), r.position.y);
44
+ return;
45
+ }
46
+ const y = direction.sign > 0 ? pusher.bottom() + gap : pusher.position.y - r.size.y - gap;
47
+ r.position.set(r.position.x, Math.max(0, y));
48
+ }
49
+
50
+ /**
51
+ * This function moves each rectangle that a placed rectangle overlaps, and the rectangles that those overlap in turn.
52
+ * A shove takes the axis of the smaller overlap, away from the pusher. A shoved rectangle keeps the direction of its first
53
+ * shove when it shoves the next one, so the cascade goes outward. The placed rectangle and each locked rectangle stay.
54
+ * The function moves the rectangles in place and gives the rectangles that moved.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const moved = pushCascade(dropped, cards, { gap: 8 });
59
+ * ```
60
+ */
61
+ export function pushCascade<T>(placed: Rect<T>, rects: readonly Rect<T>[], options: PushOptions = {}): Rect<T>[] {
62
+ const gap = options.gap ?? 8;
63
+ const maxShoves = options.maxShoves ?? rects.length * 8;
64
+ const moved = new Set<Rect<T>>();
65
+ const queue: { pusher: Rect<T>; direction?: Direction }[] = [{ pusher: placed }];
66
+ let shoves = 0;
67
+ while (shoves < maxShoves) {
68
+ const step = queue.shift();
69
+ if (!step) break;
70
+ const { pusher, direction } = step;
71
+ for (const r of rects) {
72
+ if (r === pusher || r === placed || r.locked) continue;
73
+ const overlap = overlapOf(pusher, r);
74
+ if (overlap.x <= 0 || overlap.y <= 0) continue;
75
+ const next = direction ?? firstDirection(pusher, r, gap);
76
+ shove(pusher, r, next, gap);
77
+ moved.add(r);
78
+ queue.push({ pusher: r, direction: next });
79
+ if (++shoves >= maxShoves) break;
80
+ }
81
+ }
82
+ return [...moved];
83
+ }
package/src/rect.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { Vector } from "./vector.ts";
2
+
3
+ /** A rectangle of the packer. `id` identifies the item that it measures. The packer sets `position` and `wasPacked`. */
4
+ // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- the parameter types the id of the caller
5
+ export class Rect<T = unknown> {
6
+ readonly #position = new Vector();
7
+ readonly #size = new Vector();
8
+
9
+ /** The identity of the item that the rectangle measures. */
10
+ id?: T;
11
+ /** The packer sets this field to `true` when it places the rectangle. */
12
+ wasPacked = false;
13
+ /** For an outer rectangle: the packer keeps its width. */
14
+ fixedWidth = false;
15
+ /** For an outer rectangle: the packer keeps its width and its height, and it skips a rectangle that does not fit. */
16
+ fixedSize = false;
17
+ /**
18
+ * The packer keeps the position of a locked rectangle. `packSkyline` packs the other rectangles around it.
19
+ * `packRows` only keeps its position, and other rectangles can overlap it.
20
+ */
21
+ locked = false;
22
+
23
+ /** The top left corner. */
24
+ get position(): Vector { return this.#position; }
25
+ /** The width and the height. */
26
+ get size(): Vector { return this.#size; }
27
+
28
+ /** This method gives the right edge. */
29
+ right(): number { return this.#position.x + this.#size.x; }
30
+ /** This method gives the bottom edge. */
31
+ bottom(): number { return this.#position.y + this.#size.y; }
32
+
33
+ /** This method moves the left edge to the right by an amount, and the right edge stays. */
34
+ shrinkLeft(amount: number): this {
35
+ this.#position.x += amount;
36
+ this.#size.x -= amount;
37
+ return this;
38
+ }
39
+
40
+ /** This method moves the rectangle by an offset. */
41
+ translateBy(offset: Vector): this {
42
+ this.#position.set(this.#position.x + offset.x, this.#position.y + offset.y);
43
+ return this;
44
+ }
45
+
46
+ /** This method gives the area of the size. */
47
+ area(): number { return this.#size.area(); }
48
+
49
+ /** This function gives the rectangles that the packer placed. */
50
+ static packed<U>(rects: Rect<U>[]): Rect<U>[] {
51
+ return rects.filter((r) => r.wasPacked);
52
+ }
53
+
54
+ /** This function gives the rectangles that the packer did not place. */
55
+ static unpacked<U>(rects: Rect<U>[]): Rect<U>[] {
56
+ return rects.filter((r) => !r.wasPacked);
57
+ }
58
+
59
+ /** This function marks each rectangle as not placed. A locked rectangle stays placed. */
60
+ static resetPacked<U>(rects: Rect<U>[]): void {
61
+ for (const r of rects) r.wasPacked = r.locked;
62
+ }
63
+
64
+ /** This function gives the largest right edge, or 0 for an empty list. */
65
+ static maxRight<U>(rects: Rect<U>[]): number {
66
+ return rects.reduce((acc, r) => Math.max(acc, r.right()), 0);
67
+ }
68
+
69
+ /** This function gives the largest bottom edge, or 0 for an empty list. */
70
+ static maxBottom<U>(rects: Rect<U>[]): number {
71
+ return rects.reduce((acc, r) => Math.max(acc, r.bottom()), 0);
72
+ }
73
+ }
package/src/skyline.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { compareRects, type SortOrder } from "./order.ts";
2
+ import type { Rect } from "./rect.ts";
3
+
4
+ /**
5
+ * This function finds the lowest window of width `rw` on a skyline. For each `x` in `[0, width - rw]`, the height
6
+ * of the window is the largest value of `skyline[x, x + rw)`. The first `x` with the smallest height wins. A
7
+ * monotonic deque makes the search O(width) for one rectangle.
8
+ */
9
+ export function lowestWindow(skyline: ArrayLike<number>, width: number, rw: number): { x: number; y: number } {
10
+ const deque = new Int32Array(width);
11
+ let head = 0;
12
+ let tail = 0;
13
+ let bestX = 0;
14
+ let bestY = Infinity;
15
+ for (let i = 0; i < width; i++) {
16
+ while (tail > head && skyline[deque[tail - 1]!]! <= skyline[i]!) tail--;
17
+ deque[tail++] = i;
18
+ if (deque[head]! <= i - rw) head++;
19
+ if (i >= rw - 1) {
20
+ const max = skyline[deque[head]!]!;
21
+ if (max < bestY) {
22
+ bestY = max;
23
+ bestX = i - rw + 1;
24
+ }
25
+ }
26
+ }
27
+ return { x: bestX, y: bestY };
28
+ }
29
+
30
+ /**
31
+ * This function packs rectangles into an outer rectangle with a skyline. It puts each rectangle in the lowest place
32
+ * where it fits, and it does not change the width of the outer rectangle.
33
+ *
34
+ * - A `locked` rectangle keeps its position. The skyline starts at its bottom edge across its width, thus the other
35
+ * rectangles go under it. The space above it stays empty, because a skyline has no holes.
36
+ * - The skyline has a cell for each whole unit of the width. A rectangle that needs more cells than the outer
37
+ * rectangle has goes under all other rectangles, at the left edge.
38
+ * - Without `fixedSize`, the height of the outer rectangle grows to the content. With `fixedSize`, a rectangle that
39
+ * does not fit is not packed: its `wasPacked` stays `false`.
40
+ * - The default sort order is `height-desc`. The order of the array of the caller stays.
41
+ */
42
+ export function packSkyline<T>(rects: Rect<T>[], outer: Rect<T>, options: { sortOrder?: SortOrder } = {}): Rect<T> {
43
+ // a cell for each whole unit of the width: a rectangle never goes past the right edge
44
+ const width = Math.floor(outer.size.x);
45
+ if (width <= 0) return outer;
46
+ const skyline = new Float64Array(width);
47
+
48
+ for (const rect of rects) {
49
+ if (!rect.locked) continue;
50
+ rect.wasPacked = true;
51
+ const x0 = Math.max(0, Math.floor(rect.position.x - outer.position.x));
52
+ const x1 = Math.min(width, Math.ceil(rect.position.x - outer.position.x + rect.size.x));
53
+ const bottom = rect.position.y - outer.position.y + rect.size.y;
54
+ for (let i = x0; i < x1; i++) skyline[i] = Math.max(skyline[i]!, bottom);
55
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, bottom);
56
+ }
57
+
58
+ for (const rect of rects.toSorted(compareRects(options.sortOrder ?? "height-desc"))) {
59
+ if (rect.wasPacked) continue;
60
+ const rw = Math.ceil(rect.size.x);
61
+ const rh = rect.size.y;
62
+ if (rw <= 0 || rh <= 0) continue;
63
+
64
+ let x: number;
65
+ let y: number;
66
+ if (rw > width) {
67
+ if (outer.fixedSize) continue;
68
+ x = 0;
69
+ y = 0;
70
+ for (let i = 0; i < width; i++) if (skyline[i]! > y) y = skyline[i]!;
71
+ } else {
72
+ ({ x, y } = lowestWindow(skyline, width, rw));
73
+ if (outer.fixedSize && y + rh > outer.size.y) continue;
74
+ }
75
+
76
+ rect.position.set(x + outer.position.x, y + outer.position.y);
77
+ rect.wasPacked = true;
78
+ const top = y + rh;
79
+ for (let i = x; i < Math.min(width, x + rw); i++) skyline[i] = top;
80
+ if (!outer.fixedSize) outer.size.y = Math.max(outer.size.y, top);
81
+ }
82
+ return outer;
83
+ }
package/src/vector.ts ADDED
@@ -0,0 +1,80 @@
1
+ /** A mutable two-dimensional vector. */
2
+ export class Vector {
3
+ #x: number;
4
+ #y: number;
5
+
6
+ constructor(x = 0, y = 0) {
7
+ this.#x = x;
8
+ this.#y = y;
9
+ }
10
+
11
+ /** The first component: a width or a left position. */
12
+ get x(): number { return this.#x; }
13
+ set x(v: number) { this.#x = v; }
14
+ /** The second component: a height or a top position. */
15
+ get y(): number { return this.#y; }
16
+ set y(v: number) { this.#y = v; }
17
+
18
+ /** This method sets both components. */
19
+ set(x: number, y: number): void {
20
+ this.#x = x;
21
+ this.#y = y;
22
+ }
23
+
24
+ /** This method copies the components of another vector. */
25
+ setTo(v: Vector): void {
26
+ this.#x = v.x;
27
+ this.#y = v.y;
28
+ }
29
+
30
+ /** This method gives a new vector: the sum of this vector and another. */
31
+ add(other: Vector): Vector {
32
+ return new Vector(this.#x + other.x, this.#y + other.y);
33
+ }
34
+
35
+ /** This method gives the product of the components: the area of a size. */
36
+ area(): number {
37
+ return this.#x * this.#y;
38
+ }
39
+
40
+ /** This method gives a new vector with the same components. */
41
+ clone(): Vector {
42
+ return new Vector(this.#x, this.#y);
43
+ }
44
+
45
+ /** This method gives a new vector with each component between the components of `min` and `max`. */
46
+ clamp(min: Vector, max: Vector): Vector {
47
+ return new Vector(
48
+ Math.max(min.x, Math.min(max.x, this.#x)),
49
+ Math.max(min.y, Math.min(max.y, this.#y)),
50
+ );
51
+ }
52
+
53
+ /** This function gives a vector with the same value in both components. */
54
+ static of(scalar: number): Vector {
55
+ return new Vector(scalar, scalar);
56
+ }
57
+
58
+ /** This function gives the smallest value of each component, or a zero vector for an empty list. */
59
+ static min(vectors: Vector[]): Vector {
60
+ if (!vectors.length) return new Vector();
61
+ return new Vector(
62
+ Math.min(...vectors.map((v) => v.x)),
63
+ Math.min(...vectors.map((v) => v.y)),
64
+ );
65
+ }
66
+
67
+ /** This function gives the largest value of each component, or a zero vector for an empty list. */
68
+ static max(vectors: Vector[]): Vector {
69
+ if (!vectors.length) return new Vector();
70
+ return new Vector(
71
+ Math.max(...vectors.map((v) => v.x)),
72
+ Math.max(...vectors.map((v) => v.y)),
73
+ );
74
+ }
75
+
76
+ /** This getter gives a new infinite vector each time. A shared value is not safe, because a vector is mutable. */
77
+ static get Infinity(): Vector {
78
+ return new Vector(Infinity, Infinity);
79
+ }
80
+ }