@mark1russell7/highways 0.9.3 → 0.9.5

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.
Files changed (39) hide show
  1. package/README.md +85 -5
  2. package/build/index.d.ts +954 -5
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +10689 -5294
  5. package/build/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/bench/screen/exercise.ts +4 -1
  8. package/src/bench/screen/set.ts +58 -1
  9. package/src/families/cases.ts +17 -9
  10. package/src/families/coverage.ts +502 -0
  11. package/src/families/index.ts +4 -1
  12. package/src/highways/balance/index.ts +93 -0
  13. package/src/highways/bronchial/index.ts +144 -0
  14. package/src/highways/index.ts +5 -0
  15. package/src/highways/knee/index.ts +154 -0
  16. package/src/highways/knee/root.ts +19 -0
  17. package/src/highways/manifolds/index.ts +157 -0
  18. package/src/highways/slots/index.ts +178 -0
  19. package/src/index.ts +8 -0
  20. package/src/levels/detail/index.ts +140 -0
  21. package/src/levels/venation/index.ts +191 -0
  22. package/src/plugs/leaders/index.ts +42 -0
  23. package/src/plugs/spread/index.ts +39 -5
  24. package/src/routers/adam/index.ts +294 -0
  25. package/src/routers/annealing/index.ts +260 -0
  26. package/src/routers/canals/index.ts +191 -0
  27. package/src/routers/frames.ts +80 -16
  28. package/src/routers/gutter.ts +545 -0
  29. package/src/routers/index.ts +8 -2
  30. package/src/routers/reroute/index.ts +164 -0
  31. package/src/routers/shared.ts +43 -2
  32. package/src/routers/tight/index.ts +122 -0
  33. package/src/strategies/builtins.ts +158 -2
  34. package/src/strategies/schemas.ts +143 -0
  35. package/src/whole/curriculum/index.ts +243 -0
  36. package/src/whole/fireflies/index.ts +177 -0
  37. package/src/whole/portfolio/index.ts +108 -0
  38. package/src/whole/preferences/index.ts +247 -0
  39. package/src/whole/tracing/index.ts +144 -0
@@ -0,0 +1,545 @@
1
+ import type { Box, Point } from "../geom.ts";
2
+ import type { Node, Side } from "../model.ts";
3
+ import { SIDE_NORMAL, type Plug } from "../plugs.ts";
4
+
5
+ /**
6
+ * One inner edge of a frame for the gutter mode. `plug` is the plug of the wall, with the outward normal of the wall.
7
+ * `end` is the node at the other end. `port` is the fixed plug of that node, if it has one. `side` limits the entry to one side of the node.
8
+ */
9
+ export interface GutterEdge {
10
+ readonly plug: Plug;
11
+ readonly end: Node;
12
+ readonly port?: Plug;
13
+ readonly side?: Side;
14
+ }
15
+
16
+ /** The options of the gutter mode. */
17
+ export interface GutterOptions {
18
+ /** The cost of one bend, in units of length. The default is 16. */
19
+ readonly bendCost?: number;
20
+ /** The largest space between two lanes of one gap. A narrow gap gets a smaller, even space. The default is 5. */
21
+ readonly laneGap?: number;
22
+ }
23
+
24
+ /** The default cost of one bend of the gutter mode. */
25
+ export const GUTTER_BEND_COST = 16;
26
+
27
+ const EPS = 1e-6;
28
+ /** The directions of a step: east, south, west and north, with the y axis down. */
29
+ const DX = [1, 0, -1, 0] as const;
30
+ const DY = [0, 1, 0, -1] as const;
31
+
32
+ /** An entry into the end node: the point on its side and the outward normal of that side. */
33
+ interface Entry {
34
+ readonly point: Point;
35
+ readonly normal: Point;
36
+ /** This flag is true when the point is a fixed plug, so that the lanes do not move it. */
37
+ readonly fixed: boolean;
38
+ }
39
+
40
+ /**
41
+ * This function gives the orthogonal route of each edge in the free room of a frame: the box `frame` minus the boxes of `nodes`.
42
+ * Each route starts at the plug of the wall, and goes along the normal of the wall into the frame. It ends on a side of its end node.
43
+ *
44
+ * The route goes on a grid of lines. A line goes through the middle of each free band between the box edges, each plug and each entry.
45
+ * A shortest path with a cost for each bend (`bendCost`) gives the route. Then the routes that share one gap get a lane each.
46
+ * An edge without a path through the free room gets `undefined`.
47
+ */
48
+ export function gutterRoutes(frame: Box, nodes: readonly Node[], edges: readonly GutterEdge[], options: GutterOptions = {}): (Point[] | undefined)[] {
49
+ const bend = options.bendCost ?? GUTTER_BEND_COST;
50
+ const boxes = nodes.map((n) => n.box).filter((b) => b.w > EPS && b.h > EPS);
51
+ const entries = edges.map(entriesOf);
52
+ const xs = new Set<number>();
53
+ const ys = new Set<number>();
54
+ for (const [lines, lo, size, key] of [
55
+ [xs, frame.x, frame.w, "x"],
56
+ [ys, frame.y, frame.h, "y"],
57
+ ] as const) {
58
+ const cuts = [lo, lo + size];
59
+ for (const b of boxes) for (const v of key === "x" ? [b.x, b.x + b.w] : [b.y, b.y + b.h]) if (v > lo && v < lo + size) cuts.push(v);
60
+ cuts.sort((a, b) => a - b);
61
+ for (let i = 1; i < cuts.length; i++) if (cuts[i]! - cuts[i - 1]! > EPS) lines.add((cuts[i]! + cuts[i - 1]!) / 2);
62
+ }
63
+ edges.forEach((e, i) => {
64
+ for (const { point, normal } of [{ point: e.plug.point, normal: e.plug.normal }, ...entries[i]!]) (normal.x !== 0 ? ys : xs).add(normal.x !== 0 ? point.y : point.x);
65
+ });
66
+ const grid = new Grid(frame, boxes, [...xs].sort((a, b) => a - b), [...ys].sort((a, b) => a - b));
67
+ const routes = edges.map((e, i) => stubOnly(e, boxes) ?? grid.path(e.plug, e.end.box, entries[i]!, bend));
68
+ return lanes(frame, boxes, edges, entries, routes, options.laneGap ?? 5);
69
+ }
70
+
71
+ /** This function gives the entries of the end node. They are its fixed plug, or the middles of its west and east sides and two points on each long side. */
72
+ function entriesOf(e: GutterEdge): Entry[] {
73
+ if (e.port) return [{ point: e.port.point, normal: e.port.normal, fixed: true }];
74
+ const b = e.end.box;
75
+ const cy = b.y + b.h / 2;
76
+ const d = Math.min(6, b.w / 4);
77
+ const out: [Side, Point][] = [
78
+ ["west", { x: b.x, y: cy }],
79
+ ["east", { x: b.x + b.w, y: cy }],
80
+ ["north", { x: b.x + d, y: b.y }],
81
+ ["north", { x: b.x + b.w - d, y: b.y }],
82
+ ["south", { x: b.x + d, y: b.y + b.h }],
83
+ ["south", { x: b.x + b.w - d, y: b.y + b.h }],
84
+ ];
85
+ return out.filter(([s]) => e.side === undefined || s === e.side).map(([s, point]) => ({ point, normal: SIDE_NORMAL[s], fixed: false }));
86
+ }
87
+
88
+ /**
89
+ * This function gives the route of an edge whose plug is level with its end node. The way straight in from the plug must meet that node first.
90
+ * The route is the stub only: from the plug to the side of the node. A node that touches the wall gives a stub of length 0.
91
+ */
92
+ function stubOnly(e: GutterEdge, boxes: readonly Box[]): Point[] | undefined {
93
+ if (e.port) return undefined;
94
+ const p = e.plug.point;
95
+ const b = e.end.box;
96
+ const n = e.plug.normal;
97
+ let q: Point;
98
+ if (n.x !== 0) {
99
+ if (!(p.y > b.y + EPS && p.y < b.y + b.h - EPS)) return undefined;
100
+ q = { x: n.x > 0 ? b.x + b.w : b.x, y: p.y };
101
+ if ((q.x - p.x) * n.x > EPS) return undefined;
102
+ } else {
103
+ if (!(p.x > b.x + EPS && p.x < b.x + b.w - EPS)) return undefined;
104
+ q = { x: p.x, y: n.y > 0 ? b.y + b.h : b.y };
105
+ if ((q.y - p.y) * n.y > EPS) return undefined;
106
+ }
107
+ for (const o of boxes) if (o !== b && blocks(p, q, o)) return undefined;
108
+ return [p, q];
109
+ }
110
+
111
+ /** This function tells if the axis-parallel segment `ab` meets the closed box `o` along a part longer than a tiny length. A segment along a side meets the box. */
112
+ function blocks(a: Point, b: Point, o: Box): boolean {
113
+ if (a.y === b.y) {
114
+ if (a.y < o.y - EPS || a.y > o.y + o.h + EPS) return false;
115
+ return Math.min(Math.max(a.x, b.x), o.x + o.w) - Math.max(Math.min(a.x, b.x), o.x) > EPS;
116
+ }
117
+ if (a.x < o.x - EPS || a.x > o.x + o.w + EPS) return false;
118
+ return Math.min(Math.max(a.y, b.y), o.y + o.h) - Math.max(Math.min(a.y, b.y), o.y) > EPS;
119
+ }
120
+
121
+ /** This function gives the direction index of a unit normal: 0 east, 1 south, 2 west, 3 north. */
122
+ function dirOf(n: Point): number {
123
+ return n.x > 0 ? 0 : n.y > 0 ? 1 : n.x < 0 ? 2 : 3;
124
+ }
125
+
126
+ /** The grid of the free room: the crossings of the lines that are strictly in the frame and not in a closed box. */
127
+ class Grid {
128
+ readonly #xs: readonly number[];
129
+ readonly #ys: readonly number[];
130
+ readonly #free: Uint8Array;
131
+ readonly #boxes: readonly Box[];
132
+
133
+ constructor(frame: Box, boxes: readonly Box[], xs: readonly number[], ys: readonly number[]) {
134
+ this.#xs = xs;
135
+ this.#ys = ys;
136
+ this.#boxes = boxes;
137
+ this.#free = new Uint8Array(xs.length * ys.length);
138
+ for (let i = 0; i < ys.length; i++) {
139
+ const y = ys[i]!;
140
+ if (y <= frame.y + EPS || y >= frame.y + frame.h - EPS) continue;
141
+ for (let j = 0; j < xs.length; j++) {
142
+ const x = xs[j]!;
143
+ if (x <= frame.x + EPS || x >= frame.x + frame.w - EPS) continue;
144
+ let free = true;
145
+ for (const b of boxes)
146
+ if (x >= b.x - EPS && x <= b.x + b.w + EPS && y >= b.y - EPS && y <= b.y + b.h + EPS) {
147
+ free = false;
148
+ break;
149
+ }
150
+ if (free) this.#free[i * xs.length + j] = 1;
151
+ }
152
+ }
153
+ }
154
+
155
+ #point(id: number): Point {
156
+ const nx = this.#xs.length;
157
+ return { x: this.#xs[id % nx]!, y: this.#ys[Math.floor(id / nx)]! };
158
+ }
159
+
160
+ /** This method gives the neighbour of the crossing `id` in the direction `d`, or -1. Two free crossings next to each other on a line have no box between them. */
161
+ #step(id: number, d: number): number {
162
+ const nx = this.#xs.length;
163
+ const i = Math.floor(id / nx);
164
+ const j = id % nx;
165
+ const ni = i + DY[d]!;
166
+ const nj = j + DX[d]!;
167
+ if (ni < 0 || nj < 0 || ni >= this.#ys.length || nj >= nx) return -1;
168
+ const k = ni * nx + nj;
169
+ return this.#free[k] ? k : -1;
170
+ }
171
+
172
+ /** This method gives the first free crossing from the point `p` in the direction `d`. The way to that crossing must be clear. */
173
+ #enter(p: Point, d: number, skip?: Box): number {
174
+ const nx = this.#xs.length;
175
+ const along = d % 2 === 0;
176
+ const line = along ? this.#ys.indexOf(p.y) : this.#xs.indexOf(p.x);
177
+ if (line < 0) return -1;
178
+ const list = along ? this.#xs : this.#ys;
179
+ const v = along ? p.x : p.y;
180
+ const sign = d < 2 ? 1 : -1;
181
+ let k = -1;
182
+ if (sign > 0) {
183
+ for (let m = 0; m < list.length; m++)
184
+ if (list[m]! > v + EPS) {
185
+ k = m;
186
+ break;
187
+ }
188
+ } else {
189
+ for (let m = list.length - 1; m >= 0; m--)
190
+ if (list[m]! < v - EPS) {
191
+ k = m;
192
+ break;
193
+ }
194
+ }
195
+ if (k < 0) return -1;
196
+ const id = along ? line * nx + k : k * nx + line;
197
+ if (!this.#free[id]) return -1;
198
+ const q = this.#point(id);
199
+ for (const b of this.#boxes) if (b !== skip && blocks(p, q, b)) return -1;
200
+ return id;
201
+ }
202
+
203
+ /**
204
+ * This method gives the shortest path from the plug into the end box, with the cost `bend` for each bend, or `undefined`.
205
+ * The path leaves the plug along the inward normal, and it enters an entry along the inward normal of the side.
206
+ */
207
+ path(plug: Plug, end: Box, entries: readonly Entry[], bend: number): Point[] | undefined {
208
+ const n = this.#free.length;
209
+ const d0 = dirOf({ x: -plug.normal.x, y: -plug.normal.y });
210
+ const first = this.#enter(plug.point, d0);
211
+ if (first < 0) return undefined;
212
+ // The goals: for each entry, the free crossing next to it outside the end box, the last direction and the point.
213
+ const goals: { readonly id: number; readonly dir: number; readonly point: Point }[] = [];
214
+ for (const e of entries) {
215
+ const id = this.#enter(e.point, dirOf(e.normal), end);
216
+ if (id >= 0) goals.push({ id, dir: (dirOf(e.normal) + 2) % 4, point: e.point });
217
+ }
218
+ if (goals.length === 0) return undefined;
219
+ const byNode = new Map<number, number[]>();
220
+ goals.forEach((g, k) => {
221
+ const list = byNode.get(g.id);
222
+ if (list) list.push(k);
223
+ else byNode.set(g.id, [k]);
224
+ });
225
+ // The states are a crossing and a direction. The goal states come after them.
226
+ const states = 4 * n + goals.length;
227
+ const cost = new Float64Array(states).fill(Infinity);
228
+ const prev = new Int32Array(states).fill(-1);
229
+ const heap = new Heap();
230
+ const s0 = first * 4 + d0;
231
+ cost[s0] = dist(plug.point, this.#point(first));
232
+ heap.push(cost[s0]!, s0);
233
+ let found = -1;
234
+ while (heap.size > 0) {
235
+ const [c, s] = heap.pop();
236
+ if (c > cost[s]!) continue;
237
+ if (s >= 4 * n) {
238
+ found = s;
239
+ break;
240
+ }
241
+ const id = s >> 2;
242
+ const d = s & 3;
243
+ const here = this.#point(id);
244
+ for (const k of byNode.get(id) ?? []) {
245
+ const g = goals[k]!;
246
+ // The last leg does not turn back on the leg before it.
247
+ if (g.dir === (d + 2) % 4) continue;
248
+ const gs = 4 * n + k;
249
+ const gc = c + dist(here, g.point) + (g.dir === d ? 0 : bend);
250
+ if (gc < cost[gs]!) {
251
+ cost[gs] = gc;
252
+ prev[gs] = s;
253
+ heap.push(gc, gs);
254
+ }
255
+ }
256
+ for (let nd = 0; nd < 4; nd++) {
257
+ if (nd === (d + 2) % 4) continue;
258
+ const next = this.#step(id, nd);
259
+ if (next < 0) continue;
260
+ const ns = next * 4 + nd;
261
+ const nc = c + dist(here, this.#point(next)) + (nd === d ? 0 : bend);
262
+ if (nc < cost[ns]!) {
263
+ cost[ns] = nc;
264
+ prev[ns] = s;
265
+ heap.push(nc, ns);
266
+ }
267
+ }
268
+ }
269
+ if (found < 0) return undefined;
270
+ const pts: Point[] = [goals[found - 4 * n]!.point];
271
+ for (let s = prev[found]!; s >= 0; s = prev[s]!) pts.push(this.#point(s >> 2));
272
+ pts.push(plug.point);
273
+ return simplify(pts.reverse());
274
+ }
275
+ }
276
+
277
+ function dist(a: Point, b: Point): number {
278
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
279
+ }
280
+
281
+ /** This function takes out each point between two collinear legs, and each repeated point. */
282
+ function simplify(pts: readonly Point[]): Point[] {
283
+ const out: Point[] = [];
284
+ for (const p of pts) {
285
+ const a = out[out.length - 1];
286
+ if (a && a.x === p.x && a.y === p.y) continue;
287
+ const b = out[out.length - 2];
288
+ if (a && b && ((b.x === a.x && a.x === p.x) || (b.y === a.y && a.y === p.y))) out[out.length - 1] = p;
289
+ else out.push(p);
290
+ }
291
+ return out;
292
+ }
293
+
294
+ /** A binary min-heap of (cost, state) pairs. Equal costs come out by the state, so the order is deterministic. */
295
+ class Heap {
296
+ readonly #c: number[] = [];
297
+ readonly #s: number[] = [];
298
+
299
+ get size(): number {
300
+ return this.#c.length;
301
+ }
302
+
303
+ #less(i: number, j: number): boolean {
304
+ return this.#c[i]! < this.#c[j]! || (this.#c[i] === this.#c[j] && this.#s[i]! < this.#s[j]!);
305
+ }
306
+
307
+ #swap(i: number, j: number): void {
308
+ [this.#c[i], this.#c[j]] = [this.#c[j]!, this.#c[i]!];
309
+ [this.#s[i], this.#s[j]] = [this.#s[j]!, this.#s[i]!];
310
+ }
311
+
312
+ push(c: number, s: number): void {
313
+ this.#c.push(c);
314
+ this.#s.push(s);
315
+ for (let i = this.#c.length - 1; i > 0; ) {
316
+ const p = (i - 1) >> 1;
317
+ if (!this.#less(i, p)) break;
318
+ this.#swap(i, p);
319
+ i = p;
320
+ }
321
+ }
322
+
323
+ pop(): [number, number] {
324
+ const top: [number, number] = [this.#c[0]!, this.#s[0]!];
325
+ const c = this.#c.pop()!;
326
+ const s = this.#s.pop()!;
327
+ if (this.#c.length > 0) {
328
+ this.#c[0] = c;
329
+ this.#s[0] = s;
330
+ for (let i = 0; ; ) {
331
+ const l = 2 * i + 1;
332
+ const r = l + 1;
333
+ let m = i;
334
+ if (l < this.#c.length && this.#less(l, m)) m = l;
335
+ if (r < this.#c.length && this.#less(r, m)) m = r;
336
+ if (m === i) break;
337
+ this.#swap(i, m);
338
+ i = m;
339
+ }
340
+ }
341
+ return top;
342
+ }
343
+ }
344
+
345
+ /** One leg of a route for the lanes. `across` is the coordinate that a lane changes, and `s0` to `s1` is the span along the leg. */
346
+ interface Leg {
347
+ readonly route: number;
348
+ readonly index: number;
349
+ readonly horizontal: boolean;
350
+ readonly across: number;
351
+ readonly s0: number;
352
+ readonly s1: number;
353
+ /** The side to which the route turns at `s0` and at `s1`: -1, +1, or 0 at an end of the route. */
354
+ readonly t0: number;
355
+ readonly t1: number;
356
+ /** The free range of `across`, so that the leg and its two next legs stay in the free room. */
357
+ readonly lo: number;
358
+ readonly hi: number;
359
+ readonly pinned: boolean;
360
+ }
361
+
362
+ /**
363
+ * This function gives each leg that shares a gap with legs of other routes its own lane. The lanes are evenly spaced in the gap,
364
+ * at most `laneGap` apart, in an order that gives no crossing where it can. A leg from a plug of the wall and a leg into a fixed plug do not move.
365
+ */
366
+ function lanes(frame: Box, boxes: readonly Box[], edges: readonly GutterEdge[], entries: readonly Entry[][], routes: (Point[] | undefined)[], laneGap: number): (Point[] | undefined)[] {
367
+ const legs: Leg[] = [];
368
+ routes.forEach((pts, r) => {
369
+ if (!pts || pts.length < 2) return;
370
+ const last = pts[pts.length - 1]!;
371
+ const entry = entries[r]!.find((e) => e.point.x === last.x && e.point.y === last.y);
372
+ for (let k = 0; k + 1 < pts.length; k++) {
373
+ const a = pts[k]!;
374
+ const b = pts[k + 1]!;
375
+ if (a.x === b.x && a.y === b.y) continue;
376
+ const horizontal = a.y === b.y;
377
+ const [al, ac] = horizontal ? (["x", "y"] as const) : (["y", "x"] as const);
378
+ const turn = (p: Point | undefined): number => (p === undefined ? 0 : Math.sign(p[ac] - a[ac]));
379
+ const ta = turn(pts[k - 1]);
380
+ const tb = turn(pts[k + 2]);
381
+ const forward = b[al] > a[al];
382
+ let [lo, hi] = free(frame, boxes, horizontal, a[ac], Math.min(a[al], b[al]), Math.max(a[al], b[al]));
383
+ const end = k + 2 === pts.length;
384
+ if (end && entry && !entry.fixed) {
385
+ // A leg into a free entry stays on the side of the end box.
386
+ const e = edges[r]!.end.box;
387
+ const [elo, size] = horizontal ? [e.y, e.h] : [e.x, e.w];
388
+ const q = Math.min(2, size / 4);
389
+ lo = Math.max(lo, elo + q);
390
+ hi = Math.min(hi, elo + size - q);
391
+ }
392
+ legs.push({
393
+ route: r,
394
+ index: k,
395
+ horizontal,
396
+ across: a[ac],
397
+ s0: Math.min(a[al], b[al]),
398
+ s1: Math.max(a[al], b[al]),
399
+ t0: forward ? ta : tb,
400
+ t1: forward ? tb : ta,
401
+ lo,
402
+ hi,
403
+ pinned: k === 0 || (end && (entry === undefined || entry.fixed)),
404
+ });
405
+ }
406
+ });
407
+ // The groups: legs of other routes that overlap along one gap, or nearly. Each leg is in the free range of the other.
408
+ const parent = legs.map((_, i) => i);
409
+ const root = (i: number): number => (parent[i] === i ? i : (parent[i] = root(parent[i]!)));
410
+ for (let i = 0; i < legs.length; i++)
411
+ for (let j = i + 1; j < legs.length; j++) {
412
+ const a = legs[i]!;
413
+ const b = legs[j]!;
414
+ if (a.route === b.route || a.horizontal !== b.horizontal) continue;
415
+ // Two legs that end near each other on one line share a lane group too, because the lanes of the next legs can make them overlap.
416
+ if (Math.min(a.s1, b.s1) - Math.max(a.s0, b.s0) <= -laneGap) continue;
417
+ if (a.across < b.lo - EPS || a.across > b.hi + EPS || b.across < a.lo - EPS || b.across > a.hi + EPS) continue;
418
+ parent[root(i)] = root(j);
419
+ }
420
+ const groups = new Map<number, number[]>();
421
+ legs.forEach((_, i) => {
422
+ const g = root(i);
423
+ const list = groups.get(g);
424
+ if (list) list.push(i);
425
+ else groups.set(g, [i]);
426
+ });
427
+ const out = routes.map((pts) => (pts ? pts.map((p) => ({ ...p })) : undefined));
428
+ for (const members of groups.values()) {
429
+ if (members.length < 2) continue;
430
+ const group = members.map((i) => legs[i]!);
431
+ const at = place(group, laneGap);
432
+ group.forEach((leg, m) => {
433
+ const pts = out[leg.route]!;
434
+ const key = leg.horizontal ? "y" : "x";
435
+ pts[leg.index]![key] = at[m]!;
436
+ pts[leg.index + 1]![key] = at[m]!;
437
+ });
438
+ }
439
+ return out;
440
+ }
441
+
442
+ /** This function gives the free range across a leg: the nearest box sides or walls on each side, over the span of the leg. */
443
+ function free(frame: Box, boxes: readonly Box[], horizontal: boolean, across: number, s0: number, s1: number): [number, number] {
444
+ let lo = horizontal ? frame.y : frame.x;
445
+ let hi = horizontal ? frame.y + frame.h : frame.x + frame.w;
446
+ for (const b of boxes) {
447
+ const [bs, bl, cs, cl] = horizontal ? [b.x, b.w, b.y, b.h] : [b.y, b.h, b.x, b.w];
448
+ if (Math.min(s1, bs + bl) - Math.max(s0, bs) <= EPS) continue;
449
+ if (cs + cl <= across + EPS) lo = Math.max(lo, cs + cl);
450
+ if (cs >= across - EPS) hi = Math.min(hi, cs);
451
+ }
452
+ return [lo, hi];
453
+ }
454
+
455
+ /** This function tells the side of the leg `b` for the leg `a`. It gives -1 for the smaller coordinate, +1 for the larger and 0 for no rule. */
456
+ function want(a: Leg, b: Leg): number {
457
+ const side = (sa: number, sb: number, ta: number, tb: number, inner: (x: number, y: number) => boolean): number => {
458
+ if (inner(sa, sb)) return ta;
459
+ if (inner(sb, sa)) return -tb;
460
+ return ta === tb ? 0 : Math.sign(ta - tb);
461
+ };
462
+ const left = side(a.s0, b.s0, a.t0, b.t0, (x, y) => x > y + EPS);
463
+ const right = side(a.s1, b.s1, a.t1, b.t1, (x, y) => x < y - EPS);
464
+ if (left !== 0 && right !== 0 && left !== right) return left;
465
+ return left || right;
466
+ }
467
+
468
+ /**
469
+ * This function gives the lane of each leg of a group. A pinned leg keeps its place, and the pinned legs keep their order.
470
+ * The other legs go in the order of `want`, evenly spaced between the pinned legs and the ends of the free range.
471
+ */
472
+ function place(group: readonly Leg[], laneGap: number): number[] {
473
+ const n = group.length;
474
+ const soft: number[][] = group.map(() => []);
475
+ const hard: number[][] = group.map(() => []);
476
+ const softIn = new Array<number>(n).fill(0);
477
+ const hardIn = new Array<number>(n).fill(0);
478
+ for (let i = 0; i < n; i++)
479
+ for (let j = i + 1; j < n; j++) {
480
+ const a = group[i]!;
481
+ const b = group[j]!;
482
+ if (a.pinned && b.pinned) {
483
+ if (Math.abs(a.across - b.across) <= EPS) continue;
484
+ const [x, y] = a.across < b.across ? [i, j] : [j, i];
485
+ hard[x]!.push(y);
486
+ hardIn[y]!++;
487
+ continue;
488
+ }
489
+ const w = want(a, b);
490
+ if (w === 0) continue;
491
+ const [x, y] = w < 0 ? [i, j] : [j, i];
492
+ soft[x]!.push(y);
493
+ softIn[y]!++;
494
+ }
495
+ // A topological order. On a cycle of soft rules, the leg with the fewest rules against it goes first.
496
+ const order: number[] = [];
497
+ const done = new Array<boolean>(n).fill(false);
498
+ while (order.length < n) {
499
+ let best = -1;
500
+ for (let i = 0; i < n; i++) {
501
+ if (done[i] || hardIn[i]! > 0) continue;
502
+ if (best < 0 || softIn[i]! < softIn[best]! || (softIn[i] === softIn[best] && group[i]!.across < group[best]!.across - EPS)) best = i;
503
+ }
504
+ done[best] = true;
505
+ order.push(best);
506
+ for (const y of soft[best]!) softIn[y]!--;
507
+ for (const y of hard[best]!) hardIn[y]!--;
508
+ }
509
+ // The pinned legs on one line stay next to each other: each one moves to the place of the first of them.
510
+ for (let k = 0; k < order.length; k++) {
511
+ const a = group[order[k]!]!;
512
+ if (!a.pinned) continue;
513
+ let to = k + 1;
514
+ for (let m = k + 1; m < order.length; m++) {
515
+ const b = group[order[m]!]!;
516
+ if (b.pinned && Math.abs(b.across - a.across) <= EPS) order.splice(to++, 0, ...order.splice(m, 1));
517
+ }
518
+ k = to - 1;
519
+ }
520
+ const at = new Array<number>(n).fill(0);
521
+ // The anchors: the pinned legs. The free legs between two anchors are evenly spaced in the free range of all of them.
522
+ let start = 0;
523
+ let from = -Infinity;
524
+ for (let k = 0; k <= n; k++) {
525
+ const i = order[k];
526
+ const pin = i !== undefined && group[i]!.pinned;
527
+ if (k < n && !pin) continue;
528
+ const run = order.slice(start, k);
529
+ if (run.length > 0) {
530
+ const lo = Math.max(from, ...run.map((r) => group[r]!.lo));
531
+ const hi = Math.min(k < n ? group[i!]!.across : Infinity, ...run.map((r) => group[r]!.hi));
532
+ const room = hi - lo;
533
+ const s = Math.min(laneGap, room / (run.length + 1));
534
+ const width = (run.length - 1) * s;
535
+ const mean = run.reduce((t, r) => t + group[r]!.across, 0) / run.length;
536
+ // A free leg keeps at least the space s from a pinned leg and from an end of the free range. Without room, each leg stays.
537
+ const first = Math.min(Math.max(mean - width / 2, lo + s), hi - s - width);
538
+ run.forEach((r, m) => (at[r] = room > EPS && s > 0 ? first + m * s : group[r]!.across));
539
+ }
540
+ if (k < n) at[i!] = group[i!]!.across;
541
+ start = k + 1;
542
+ if (k < n) from = group[i!]!.across;
543
+ }
544
+ return at;
545
+ }
@@ -9,8 +9,14 @@ import { byCollection, trunksRouter } from "./trunks.ts";
9
9
  export { boxExit, straightRouter } from "./straight.ts";
10
10
  export { plugsRouter } from "./plugs.ts";
11
11
  export { cornersRouter, type CornersOptions } from "./corners.ts";
12
- export { frameRoutes, type FrameOptions, type Outside } from "./frames.ts";
13
- export { sharedRouter, type ShareCost, type SharedNegotiation, type SharedOptions, type ShareMerge, type ShareOrder } from "./shared.ts";
12
+ export { frameRoutes, type FrameOptions, type InnerMode, type Outside } from "./frames.ts";
13
+ export { GUTTER_BEND_COST, gutterRoutes, type GutterEdge, type GutterOptions } from "./gutter.ts";
14
+ export { sharedRouter, type ShareCost, type SharedHooks, type SharedNegotiation, type SharedOptions, type SharedSearch, type ShareMerge, type ShareOrder } from "./shared.ts";
15
+ export { ANNEALING_DEFAULTS, ANNEALING_SCHEMA, annealingRouter, annealingStrategy, annealWays, type AnnealingOptions } from "./annealing/index.ts";
16
+ export { CANAL_DEFAULTS, CANAL_SCHEMA, canalHubs, canalRouter, canalStrategy, canalWays, halfPower, type CanalHub, type CanalOptions } from "./canals/index.ts";
17
+ export { ADAM_DEFAULTS, ADAM_SCHEMA, adamRouter, adamRoutes, adamStrategy, type AdamOptions } from "./adam/index.ts";
18
+ export { REROUTE_DEFAULTS, REROUTE_SCHEMA, rerouteRouter, rerouteStrategy, rerouteWays, turnsOf, type RerouteOptions } from "./reroute/index.ts";
19
+ export { TIGHT_DEFAULTS, TIGHT_FIRSTS, TIGHT_SCHEMA, TIGHT_TIES, tightRouter, tightStrategy, waySlacks, type TightFirst, type TightOptions, type TightTies } from "./tight/index.ts";
14
20
  export { ATTRACTION_DEFAULTS, ATTRACTION_SCHEMA, attractionRouter, attractionStrategy, type AttractionOptions } from "./attraction/index.ts";
15
21
  export { sectionsRouter, type SectionsOptions } from "./sections.ts";
16
22
  export { byCollection, groupEdges, laneOrder, splitPoint, trunksRouter, type TrunkFan, type TrunkSplit, type TrunksOptions } from "./trunks.ts";