@mark1russell7/packing 0.2.0 → 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/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
+ }