@mark1russell7/packing 0.1.7 → 0.2.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/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
+ }
package/src/strategy.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Container, PackInput, PackOutput, Region } from "./contract.ts";
1
+ import type { LayoutContainer, PackInput, PackOutput, Region } from "./contract.ts";
2
2
 
3
3
  /**
4
4
  * For each dimension of the taxonomy, the values that a strategy supports. `docs/research/TAXONOMY.md` gives the values.
@@ -108,7 +108,7 @@ export function createContext(seed = 0, more: Omit<PackContext, "seed" | "rng">
108
108
  }
109
109
 
110
110
  /** This function tells if a polygon ring is convex. A ring with fewer than 3 points is convex. */
111
- function isConvex(points: readonly [number, number][]): boolean {
111
+ function isConvex(points: readonly (readonly [number, number])[]): boolean {
112
112
  let sign = 0;
113
113
  const n = points.length;
114
114
  for (let i = 0; i < n; i++) {
@@ -163,7 +163,7 @@ export function cellOf(input: PackInput): Cell {
163
163
  const { container, items } = input;
164
164
  const obstacles = input.obstacles ?? [];
165
165
 
166
- const sizes: Record<Container["fixed"], DimensionValue<"containerSize">> = {
166
+ const sizes: Record<LayoutContainer["fixed"], DimensionValue<"containerSize">> = {
167
167
  size: "fixed",
168
168
  width: "freeHeight",
169
169
  height: "freeWidth",