@mark1russell7/packing 0.2.0 → 0.2.2

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/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
+ }
package/src/space.ts ADDED
@@ -0,0 +1,379 @@
1
+ // The container layer of roadmap item k37 (docs/PACKER.md): the free space of one input, in the coordinates of the frame.
2
+ // A strategy asks the layer where a rectangle of the frame fits. The layer knows the outline of the container, its holes,
3
+ // the obstacles and the locked items. Thus each strategy packs into each container shape.
4
+
5
+ import { boundsOf, regionHitsRect, translate } from "@mark1russell7/layout-contract";
6
+ import { EPS, type PackInput, type Region } from "./contract.ts";
7
+ import { buildFrame, transposeRegion, type Frame } from "./frame.ts";
8
+
9
+ /** A free interval on one axis, from `start` to `end`. A box fits when it starts at `start` or later and ends at `end` or earlier. */
10
+ export interface FreeSpan {
11
+ readonly start: number;
12
+ readonly end: number;
13
+ }
14
+
15
+ /** A rectangle of the frame: its top-left corner and its size. */
16
+ export interface SpaceRect {
17
+ readonly x: number;
18
+ readonly y: number;
19
+ readonly w: number;
20
+ readonly h: number;
21
+ }
22
+
23
+ /**
24
+ * The free space of one input, in the coordinates of the frame of the adapters. A rectangle of the frame is an item with its
25
+ * margin, grown by half the gap on each side. Each method takes such a rectangle.
26
+ *
27
+ * - The outline of a container with a fixed size is exact. A free side uses the bounding box of the shape, and the holes stay.
28
+ * - The inset of the container counts as in `checkHard`: the item with its margin, grown by the inset, stays in the outline.
29
+ * - A rectangle obstacle and a locked item are holes with the exact rectangles of the frame.
30
+ * - Another obstacle blocks its range over the query, grown by half the gap on each axis.
31
+ */
32
+ export interface FreeSpace {
33
+ /** The width of the area of the frame. A free side has the width `Infinity`. */
34
+ readonly w: number;
35
+ /** The height of the area of the frame. A free side has the height `Infinity`. */
36
+ readonly h: number;
37
+ /**
38
+ * The value `true` tells that the container is a plain rectangle and that each obstacle is a rectangle. Then `band` does
39
+ * not test y, and `column` does not test x, because each strategy tests the fixed side itself. This is the fast path.
40
+ */
41
+ readonly plain: boolean;
42
+ /** This method gives the free x-spans, from left to right, for a rectangle from `y0` to `y1`. */
43
+ band(y0: number, y1: number): FreeSpan[];
44
+ /**
45
+ * This method gives the first free y-span for a rectangle from `x0` to `x1` that ends after `from`. The span starts
46
+ * at `from` or later. The method gives `undefined` when no such span is free.
47
+ */
48
+ column(x0: number, x1: number, from?: number): FreeSpan | undefined;
49
+ /** This method tells if a rectangle is in the free space. */
50
+ fits(rect: SpaceRect): boolean;
51
+ /** This method gives the first x from `x` where a rectangle of width `w` fits from `y0` to `y1`, or `Infinity`. */
52
+ fitX(x: number, w: number, y0: number, y1: number): number;
53
+ /**
54
+ * This method gives the next top edge after `y0` where the free spans of a rectangle of the height `y1 - y0` change, or
55
+ * `Infinity`. A row that has no place for a rectangle of width `w` steps down to this edge.
56
+ */
57
+ nextY(y0: number, y1: number, w: number): number;
58
+ }
59
+
60
+ /** A rectangle hole of a layer: its edges on the axis of the spans (`a`) and on the cross axis (`c`). */
61
+ interface Hole {
62
+ a0: number;
63
+ a1: number;
64
+ c0: number;
65
+ c1: number;
66
+ }
67
+
68
+ /** A region that blocks its range over a query, grown by `g` on each axis. */
69
+ interface Blocker {
70
+ region: Region;
71
+ g: number;
72
+ /** The cross-axis edges of the bounding box of the region. */
73
+ c0: number;
74
+ c1: number;
75
+ }
76
+
77
+ /** A ring of a polygon. */
78
+ type Ring = readonly (readonly [number, number])[];
79
+
80
+ /**
81
+ * One axis of the free space. The spans go along x, and the query goes along y. The layer of `column` is the layer of
82
+ * `band` with the axes changed.
83
+ */
84
+ interface Layer {
85
+ /** `plain` and `box`: the area of the frame. `round`: a circle or an ellipse. `rings`: a polygon. */
86
+ kind: "plain" | "box" | "round" | "rings";
87
+ /** The size of the area along the spans and across. */
88
+ w: number;
89
+ h: number;
90
+ /** The growth of a query for the outline: the inset less half the gap. */
91
+ d: number;
92
+ region?: Region;
93
+ rings: Ring[];
94
+ holes: Hole[];
95
+ blockers: Blocker[];
96
+ }
97
+
98
+ /** This function removes the closed interval from `a` to `b` from a list of spans. A span of no length goes. */
99
+ function subtract(spans: [number, number][], a: number, b: number): [number, number][] {
100
+ const out: [number, number][] = [];
101
+ for (const [s, e] of spans) {
102
+ if (b <= s || a >= e) {
103
+ out.push([s, e]);
104
+ continue;
105
+ }
106
+ if (a > s) out.push([s, a]);
107
+ if (e > b) out.push([b, e]);
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /** This function gives the x of a segment at `y`. The ends give their exact x. */
113
+ function xAt(ax: number, ay: number, bx: number, by: number, y: number): number {
114
+ if (y === ay) return ax;
115
+ if (y === by) return bx;
116
+ return ax + ((y - ay) * (bx - ax)) / (by - ay);
117
+ }
118
+
119
+ /** This function gives the x-range of the part of a segment in the open strip from `y0` to `y1`, or `undefined`. */
120
+ function edgeRange(ax: number, ay: number, bx: number, by: number, y0: number, y1: number): [number, number] | undefined {
121
+ const lo = Math.min(ay, by);
122
+ const hi = Math.max(ay, by);
123
+ if (hi <= y0 || lo >= y1) return undefined;
124
+ if (ay === by) return [Math.min(ax, bx), Math.max(ax, bx)];
125
+ const p = xAt(ax, ay, bx, by, Math.max(y0, lo));
126
+ const q = xAt(ax, ay, bx, by, Math.min(y1, hi));
127
+ return [Math.min(p, q), Math.max(p, q)];
128
+ }
129
+
130
+ /** This function gives the x-range of a region in the open strip from `y0` to `y1`, or `undefined` when they have no common point. */
131
+ function regionRange(region: Region, y0: number, y1: number): [number, number] | undefined {
132
+ switch (region.kind) {
133
+ case "rect":
134
+ return region.y < y1 && y0 < region.y + region.h ? [region.x, region.x + region.w] : undefined;
135
+ case "circle":
136
+ case "ellipse": {
137
+ const rx = region.kind === "circle" ? region.r : region.rx;
138
+ const ry = region.kind === "circle" ? region.r : region.ry;
139
+ if (y1 <= region.cy - ry || y0 >= region.cy + ry) return undefined;
140
+ const dy = y0 <= region.cy && region.cy <= y1 ? 0 : Math.min(Math.abs(y0 - region.cy), Math.abs(y1 - region.cy));
141
+ const c = rx * Math.sqrt(Math.max(0, 1 - (dy / ry) ** 2));
142
+ return [region.cx - c, region.cx + c];
143
+ }
144
+ case "polygon": {
145
+ let lo = Infinity;
146
+ let hi = -Infinity;
147
+ const ring = region.points;
148
+ for (let i = 0; i < ring.length; i++) {
149
+ const a = ring[i]!;
150
+ const b = ring[(i + 1) % ring.length]!;
151
+ const r = edgeRange(a[0], a[1], b[0], b[1], y0, y1);
152
+ if (r === undefined) continue;
153
+ lo = Math.min(lo, r[0]);
154
+ hi = Math.max(hi, r[1]);
155
+ }
156
+ return lo <= hi ? [lo, hi] : undefined;
157
+ }
158
+ }
159
+ }
160
+
161
+ /** This function gives the spans of the outline of a layer for the strip from `y0` to `y1`, before the holes and the blockers. */
162
+ function outline(layer: Layer, y0: number, y1: number): [number, number][] {
163
+ const { d } = layer;
164
+ switch (layer.kind) {
165
+ case "plain":
166
+ return [[0, layer.w]];
167
+ case "box":
168
+ return y0 < 0 || y1 > layer.h ? [] : [[0, layer.w]];
169
+ case "round": {
170
+ const r = layer.region!;
171
+ if (r.kind !== "circle" && r.kind !== "ellipse") return [];
172
+ const rx = r.kind === "circle" ? r.r : r.rx;
173
+ const ry = r.kind === "circle" ? r.r : r.ry;
174
+ const top = y0 - d;
175
+ const bottom = y1 + d;
176
+ if (top < r.cy - ry || bottom > r.cy + ry) return [];
177
+ const dy = Math.max(Math.abs(top - r.cy), Math.abs(bottom - r.cy));
178
+ // The shortest chord over the height of the query, made smaller by EPS
179
+ const c = rx * Math.sqrt(Math.max(0, 1 - (dy / ry) ** 2)) - EPS;
180
+ const a = r.cx - c + d;
181
+ const b = r.cx + c - d;
182
+ return b > a ? [[a, b]] : [];
183
+ }
184
+ case "rings": {
185
+ const top = y0 - d;
186
+ const bottom = y1 + d;
187
+ if (!(bottom > top)) return [];
188
+ const blocked: [number, number][] = [];
189
+ for (const ring of layer.rings) {
190
+ for (let i = 0; i < ring.length; i++) {
191
+ const a = ring[i]!;
192
+ const b = ring[(i + 1) % ring.length]!;
193
+ const r = edgeRange(a[0], a[1], b[0], b[1], top, bottom);
194
+ if (r !== undefined) blocked.push(r);
195
+ }
196
+ }
197
+ if (blocked.length === 0) return [];
198
+ blocked.sort((p, q) => p[0] - q[0] || p[1] - q[1]);
199
+ const merged: [number, number][] = [];
200
+ for (const r of blocked) {
201
+ const last = merged.at(-1);
202
+ if (last !== undefined && r[0] <= last[1]) last[1] = Math.max(last[1], r[1]);
203
+ else merged.push([r[0], r[1]]);
204
+ }
205
+ const spans: [number, number][] = [];
206
+ for (let i = 0; i + 1 < merged.length; i++) {
207
+ const a = merged[i]![1];
208
+ const b = merged[i + 1]![0];
209
+ if (!(b > a)) continue;
210
+ // No edge crosses the box of the gap, thus the box is fully in the polygon or fully out of it
211
+ if (!regionHitsRect(layer.region!, { x: a, y: top, w: b - a, h: bottom - top })) continue;
212
+ if (b - d > a + d) spans.push([a + d, b - d]);
213
+ }
214
+ return spans;
215
+ }
216
+ }
217
+ }
218
+
219
+ /** This function gives the free spans of a layer for the strip from `y0` to `y1`. */
220
+ function spansOf(layer: Layer, y0: number, y1: number): [number, number][] {
221
+ let spans = outline(layer, y0, y1);
222
+ for (const hole of layer.holes) {
223
+ if (spans.length === 0) return spans;
224
+ if (Math.min(y1, hole.c1) - Math.max(y0, hole.c0) > 0) spans = subtract(spans, hole.a0, hole.a1);
225
+ }
226
+ for (const b of layer.blockers) {
227
+ if (spans.length === 0) return spans;
228
+ if (b.c1 + b.g <= y0 || b.c0 - b.g >= y1) continue;
229
+ const r = regionRange(b.region, y0 - b.g, y1 + b.g);
230
+ // The range grows by EPS, as the chord of a round outline shrinks by EPS
231
+ if (r !== undefined) spans = subtract(spans, r[0] - b.g - EPS, r[1] + b.g + EPS);
232
+ }
233
+ return spans;
234
+ }
235
+
236
+ /** This function gives the first x from `x` where a box of width `w` fits in one of the spans, or `Infinity`. */
237
+ export function firstFit(spans: readonly FreeSpan[], x: number, w: number): number {
238
+ for (const s of spans) {
239
+ const at = Math.max(x, s.start);
240
+ if (at + w <= s.end) return at;
241
+ }
242
+ return Infinity;
243
+ }
244
+
245
+ /** This function gives the next top edge of a layer after `y0` for a strip of the height `y1 - y0`, as `FreeSpace.nextY` tells. */
246
+ function nextOf(layer: Layer, y0: number, y1: number, w: number): number {
247
+ const height = y1 - y0;
248
+ const { d } = layer;
249
+ let next = Infinity;
250
+ const take = (y: number): void => {
251
+ if (y > y0 + EPS && y < next) next = y;
252
+ };
253
+ for (const hole of layer.holes) if (hole.c0 < y1 && y0 < hole.c1) take(hole.c1);
254
+ for (const b of layer.blockers) if (b.c0 - b.g < y1 && y0 < b.c1 + b.g) take(b.c1 + b.g);
255
+ switch (layer.kind) {
256
+ case "plain":
257
+ break;
258
+ case "box":
259
+ if (y1 > layer.h) return Infinity;
260
+ break;
261
+ case "rings":
262
+ for (const ring of layer.rings) {
263
+ for (const [, vy] of ring) {
264
+ take(vy + d);
265
+ take(vy - height - d);
266
+ }
267
+ }
268
+ break;
269
+ case "round": {
270
+ const r = layer.region!;
271
+ if (r.kind !== "circle" && r.kind !== "ellipse") break;
272
+ const rx = r.kind === "circle" ? r.r : r.rx;
273
+ const ry = r.kind === "circle" ? r.r : r.ry;
274
+ // The half chord that a box of width w needs, and the largest distance from the center where the chord has it
275
+ const need = w / 2 + d + EPS;
276
+ if (need >= rx) return Infinity;
277
+ const reach = ry * Math.sqrt(1 - (need / rx) ** 2);
278
+ if (y1 + d > r.cy + reach) return Infinity;
279
+ take(r.cy - reach + d);
280
+ break;
281
+ }
282
+ }
283
+ return next;
284
+ }
285
+
286
+ /** This function gives the layer with the axes changed: the spans go along y, and the query goes along x. */
287
+ function transposeLayer(layer: Layer): Layer {
288
+ return {
289
+ kind: layer.kind,
290
+ w: layer.h,
291
+ h: layer.w,
292
+ d: layer.d,
293
+ ...(layer.region === undefined ? {} : { region: transposeRegion(layer.region) }),
294
+ rings: layer.rings.map((ring) => ring.map(([x, y]) => [y, x] as const)),
295
+ holes: layer.holes.map((h) => ({ a0: h.c0, a1: h.c1, c0: h.a0, c1: h.a1 })),
296
+ blockers: layer.blockers.map((b) => {
297
+ const region = transposeRegion(b.region);
298
+ const box = boundsOf(region);
299
+ return { region, g: b.g, c0: box.y, c1: box.y + box.h };
300
+ }),
301
+ };
302
+ }
303
+
304
+ /** This function makes a blocker of a region in the frame. */
305
+ function blockerOf(region: Region, g: number): Blocker {
306
+ const box = boundsOf(region);
307
+ return { region, g, c0: box.y, c1: box.y + box.h };
308
+ }
309
+
310
+ /**
311
+ * This function gives the free space of an input and its frame. The adapters use it with the frame that they pack.
312
+ * `freeSpace` makes the frame itself.
313
+ */
314
+ export function spaceOf(input: PackInput, frame: Frame): FreeSpace {
315
+ const { container } = input;
316
+ const half = (input.gap ?? 0) / 2;
317
+ const d = frame.inset - half;
318
+ const shape = container.shape === undefined ? undefined : translate(container.shape, -frame.ox, -frame.oy);
319
+ const fixedSize = Number.isFinite(frame.w) && Number.isFinite(frame.h);
320
+
321
+ const holes: Hole[] = [];
322
+ const blockers: Blocker[] = [];
323
+ let rectObstacles = true;
324
+ for (const e of frame.entries) {
325
+ if (!e.rect.locked) continue;
326
+ if (e.obstacle !== undefined && e.obstacle.region.kind !== "rect") {
327
+ rectObstacles = false;
328
+ blockers.push(blockerOf(translate(e.obstacle.region, -frame.ox, -frame.oy), half));
329
+ continue;
330
+ }
331
+ const r = e.rect;
332
+ holes.push({ a0: r.position.x, a1: r.right(), c0: r.position.y, c1: r.bottom() });
333
+ }
334
+
335
+ const plainShape = shape === undefined || shape.kind === "rect";
336
+ const plain = plainShape && rectObstacles;
337
+ let layer: Layer = { kind: plain ? "plain" : "box", w: frame.w, h: frame.h, d, rings: [], holes, blockers };
338
+ if (shape !== undefined && shape.kind !== "rect") {
339
+ if (fixedSize && shape.kind === "polygon") layer = { ...layer, kind: "rings", region: shape, rings: [shape.points, ...(shape.holes ?? [])] };
340
+ else if (fixedSize) layer = { ...layer, kind: "round", region: shape };
341
+ else if (shape.kind === "polygon") {
342
+ // A free side: the bounding box, and the holes of the shape block the rectangles grown by the inset
343
+ for (const ring of shape.holes ?? []) blockers.push(blockerOf({ kind: "polygon", points: ring }, d));
344
+ }
345
+ }
346
+ const across = transposeLayer(layer);
347
+
348
+ const band = (y0: number, y1: number): FreeSpan[] => spansOf(layer, y0, y1).map(([start, end]) => ({ start, end }));
349
+ return {
350
+ w: frame.w,
351
+ h: frame.h,
352
+ plain,
353
+ band,
354
+ column(x0, x1, from = 0) {
355
+ for (const [start, end] of spansOf(across, x0, x1)) {
356
+ const top = Math.max(start, from);
357
+ if (end > top) return { start: top, end };
358
+ }
359
+ return undefined;
360
+ },
361
+ fits(rect) {
362
+ return firstFit(band(rect.y, rect.y + rect.h), rect.x, rect.w) === rect.x;
363
+ },
364
+ fitX(x, w, y0, y1) {
365
+ return firstFit(band(y0, y1), x, w);
366
+ },
367
+ nextY(y0, y1, w) {
368
+ return nextOf(layer, y0, y1, w);
369
+ },
370
+ };
371
+ }
372
+
373
+ /**
374
+ * This function gives the free space of an input, in the coordinates of the frame of the adapters. The origin of the
375
+ * frame is the corner of the container, moved in by the inset and out by half the gap. `FreeSpace` tells the methods.
376
+ */
377
+ export function freeSpace(input: PackInput): FreeSpace {
378
+ return spaceOf(input, buildFrame(input));
379
+ }