@mark1russell7/highways 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +240 -8
  2. package/build/index.d.ts +1459 -78
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +9315 -3411
  5. package/build/index.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/bench/compare.ts +145 -0
  8. package/src/bench/curves.ts +514 -2
  9. package/src/bench/links.ts +28 -6
  10. package/src/bench/measures/bundles.ts +119 -0
  11. package/src/bench/measures/curves.ts +211 -0
  12. package/src/bench/measures/groups.ts +122 -0
  13. package/src/bench/measures/index.ts +9 -0
  14. package/src/bench/measures/levels.ts +103 -0
  15. package/src/bench/measures/pareto.ts +37 -0
  16. package/src/bench/measures/room.ts +43 -0
  17. package/src/bench/measures/routes.ts +226 -0
  18. package/src/bench/measures/stability.ts +104 -0
  19. package/src/bench/measures/stats.ts +114 -0
  20. package/src/bench/properties.ts +18 -2
  21. package/src/bench/room.ts +209 -2
  22. package/src/bench/run.ts +86 -11
  23. package/src/bench/suite.ts +781 -0
  24. package/src/cases.ts +41 -5
  25. package/src/curves/clothoid/index.ts +150 -0
  26. package/src/curves/corner.ts +116 -0
  27. package/src/curves/fit.ts +79 -0
  28. package/src/curves/highway/index.ts +636 -0
  29. package/src/curves/index.ts +16 -1
  30. package/src/curves/model.ts +4 -2
  31. package/src/curves/smooth/index.ts +50 -0
  32. package/src/curves/space.ts +76 -0
  33. package/src/groups/cut/index.ts +292 -0
  34. package/src/groups/index.ts +2 -0
  35. package/src/groups/kd/index.ts +207 -0
  36. package/src/index.ts +4 -0
  37. package/src/levels.ts +163 -5
  38. package/src/room/bends.ts +203 -0
  39. package/src/room/index.ts +28 -0
  40. package/src/room/levels/index.ts +155 -0
  41. package/src/routers/sections.ts +15 -40
  42. package/src/routers/shared.ts +7 -2
  43. package/src/routers/trunks.ts +131 -6
  44. package/src/sections.ts +47 -1
  45. package/src/segments.ts +81 -31
  46. package/src/strategies/builtins.ts +129 -8
  47. package/src/strategies/index.ts +1 -0
  48. package/src/strategies/model.ts +19 -1
  49. package/src/strategies/options.ts +296 -0
  50. package/src/strategies/schemas.ts +221 -0
  51. package/src/visibility.ts +190 -1
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./pick.ts";
15
15
  export * from "./routers/index.ts";
16
16
  export * from "./room/index.ts";
17
17
  export * from "./room/lanes.ts";
18
+ export * from "./room/levels/index.ts";
18
19
  export * from "./bench/properties.ts";
19
20
  export * from "./bench/quality.ts";
20
21
  export * from "./bench/links.ts";
@@ -22,4 +23,7 @@ export * from "./bench/run.ts";
22
23
  export * from "./bench/curves.ts";
23
24
  export * from "./bench/room.ts";
24
25
  export * from "./bench/sections.ts";
26
+ export * from "./bench/measures/index.ts";
27
+ export * from "./bench/suite.ts";
25
28
  export * from "./strategies/index.ts";
29
+ export * from "./bench/compare.ts";
package/src/levels.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import { boxesOverlap, goesOutAndBack } from "./bench/links.ts";
1
2
  import { center, dist, polylineLength, type Box, type Point } from "./geom.ts";
2
3
  import type { LevelItem, LevelLink, Node, Side } from "./model.ts";
3
4
  import { pointOnSide, SIDE_NORMAL, sideToward, type Plug } from "./plugs.ts";
4
- import { CornerGraph, Obstacles } from "./visibility.ts";
5
+ import { CornerGraph, Obstacles, SoftGraph } from "./visibility.ts";
5
6
  import type { WayFinder, WayOptions } from "./ways.ts";
6
7
 
7
8
  /** Two items of a level that a link joins, and the count of the edges between them. */
@@ -26,6 +27,10 @@ interface LevelWays extends Ways {
26
27
  readonly tight: () => CornerGraph;
27
28
  /** The free plugs of each node that a link needed. */
28
29
  readonly free: Map<Node, FreePlugs>;
30
+ /** This function gives the graph of `SoftGraph`. The first link that needs it makes it. */
31
+ readonly soft: () => SoftGraph;
32
+ /** The mean size of the boxes: the mean of the larger side of each box. */
33
+ readonly size: number;
29
34
  }
30
35
 
31
36
  /** The options of the links of a level: the options of the ways, and two limits of the level links. */
@@ -37,18 +42,39 @@ export interface LevelLinkOptions extends WayOptions {
37
42
  readonly tightMargin?: number;
38
43
  /** A plug stays at least this far from the corners of its side. The level links use the smaller of this value and `margin`. The default is 6. */
39
44
  readonly plugInset?: number;
45
+ /**
46
+ * The cost of a level link through a far box, in units of the mean box size. A far box overlaps neither end of the link.
47
+ * A link through a far box gets a second way that can go through boxes (`SoftGraph`). The link keeps the way with fewer far boxes.
48
+ * The value 0 turns the second way off and gives the links of version 0.1.5. The default is 8.
49
+ */
50
+ readonly throughCost?: number;
40
51
  }
41
52
 
42
- /** The default values of the two limits of `LevelLinkOptions`. */
43
- export const LEVEL_LINK_DEFAULTS: Readonly<Required<Pick<LevelLinkOptions, "tightMargin" | "plugInset">>> = Object.freeze({ tightMargin: 1, plugInset: 6 });
53
+ /** The default values of the three limits of `LevelLinkOptions`. */
54
+ export const LEVEL_LINK_DEFAULTS: Readonly<Required<Pick<LevelLinkOptions, "tightMargin" | "plugInset" | "throughCost">>> = Object.freeze({
55
+ tightMargin: 1,
56
+ plugInset: 6,
57
+ throughCost: 8,
58
+ });
59
+
60
+ /** The values of `throughCost` of the sweep of ER3d in the benchmark. The value 0 gives the links of version 0.1.5. */
61
+ export const THROUGH_COST_SWEEP: readonly number[] = [0, 2, 4, 8, 16];
44
62
 
45
63
  const EPS = 1e-6;
46
64
 
47
65
  /**
48
66
  * This function removes the repeated points, the points in the middle of a straight run,
49
67
  * and the points where the way turns back on itself.
68
+ * A way of two points or more keeps two points: its first point and its last point.
69
+ * Two items that touch can get a link of zero length, and such a link keeps an end on each item.
50
70
  */
51
71
  function tidy(p: readonly Point[]): Point[] {
72
+ const out = tidyPoints(p);
73
+ return out.length === 1 && p.length > 1 ? [p[0]!, p[p.length - 1]!] : out;
74
+ }
75
+
76
+ /** This function does the work of `tidy`. It can give one point only. */
77
+ function tidyPoints(p: readonly Point[]): Point[] {
52
78
  const out: Point[] = [];
53
79
  for (const q of p) {
54
80
  if (out.length > 0 && dist(q, out[out.length - 1]!) <= EPS) continue;
@@ -77,6 +103,7 @@ export function levelLinks(items: readonly LevelItem[], pairs: readonly ItemPair
77
103
  const byId = new Map(nodes.map((n) => [n.id, n]));
78
104
  const ways = levelWays(nodes, options);
79
105
  const inset = Math.min(options.plugInset ?? LEVEL_LINK_DEFAULTS.plugInset, ways.margin);
106
+ const throughCost = options.throughCost ?? LEVEL_LINK_DEFAULTS.throughCost;
80
107
  const out: LevelLink[] = [];
81
108
  for (const p of pairs) {
82
109
  const s = byId.get(p.source);
@@ -89,7 +116,16 @@ export function levelLinks(items: readonly LevelItem[], pairs: readonly ItemPair
89
116
  points ??= sideWay(ways, s, t, inset);
90
117
  out.push({ source: p.source, target: p.target, count: p.count, points });
91
118
  }
92
- return out;
119
+ if (throughCost <= 0) return out;
120
+ // The second way never makes the worst detour of the level longer.
121
+ const limit = out.reduce((m, l) => Math.max(m, detour(l.points)), 1);
122
+ return out.map((l) => ({ ...l, points: fewerFar(ways, byId.get(l.source)!, byId.get(l.target)!, l.points, throughCost, limit) }));
123
+ }
124
+
125
+ /** This function gives the length of the polyline divided by the distance between its ends, as the measure `worstDetour` gives it. */
126
+ function detour(p: readonly Point[]): number {
127
+ const span = p.length > 1 ? dist(p[0]!, p[p.length - 1]!) : 0;
128
+ return span > 1 ? polylineLength(p) / span : 1;
93
129
  }
94
130
 
95
131
  /**
@@ -122,7 +158,129 @@ function levelWays(nodes: readonly Node[], options: LevelLinkOptions): LevelWays
122
158
  const out = (p: Plug): Point => ({ x: p.point.x + p.normal.x * margin, y: p.point.y + p.normal.y * margin });
123
159
  let tight: CornerGraph | undefined;
124
160
  const tightGraph = (): CornerGraph => (tight ??= new CornerGraph(nodes, obstacles, { margin: Math.min(margin, options.tightMargin ?? LEVEL_LINK_DEFAULTS.tightMargin), k, cones: true }));
125
- return { obstacles, corners, margin, out, tight: tightGraph, free: new Map() };
161
+ let soft: SoftGraph | undefined;
162
+ const softGraph = (): SoftGraph => (soft ??= new SoftGraph(nodes, obstacles, { margin, k }));
163
+ return { obstacles, corners, margin, out, tight: tightGraph, free: new Map(), soft: softGraph, size: size / Math.max(1, nodes.length) };
164
+ }
165
+
166
+ /** This function gives the boxes that the polyline goes through, without the boxes of `s` and `t`, and the number of far boxes among them. */
167
+ function boxesThrough(ways: Ways, p: readonly Point[], s: Node, t: Node, near: ReadonlySet<Node>): { boxes: Set<Node>; far: number } {
168
+ const boxes = new Set<Node>();
169
+ let far = 0;
170
+ for (let i = 1; i < p.length; i++)
171
+ ways.obstacles.through(p[i - 1]!, p[i]!, (n) => {
172
+ if (n === s || n === t || boxes.has(n)) return;
173
+ boxes.add(n);
174
+ if (!near.has(n)) far++;
175
+ });
176
+ return { boxes, far };
177
+ }
178
+
179
+ /** This function gives the nodes whose boxes overlap the box of `s` or the box of `t`, as `boxesOverlap` sees it. */
180
+ function nearBoxes(ways: Ways, s: Node, t: Node): Set<Node> {
181
+ const near = new Set<Node>();
182
+ for (const end of [s, t])
183
+ ways.obstacles.query(end.box, (n) => {
184
+ if (n !== s && n !== t && boxesOverlap(n.box, end.box)) near.add(n);
185
+ });
186
+ return near;
187
+ }
188
+
189
+ /**
190
+ * This function gives a way with fewer far boxes than `way`, or `way`. A far box overlaps neither end of the link.
191
+ * On a walled-in level, each box overlaps its neighbours, so a link can find no way around the boxes.
192
+ * Then a search of `SoftGraph` goes from the center of `s` to the center of `t`.
193
+ *
194
+ * In the search, a far box costs `cost` mean box sizes, and a near box costs one mean box size.
195
+ * The way starts where the path leaves the box of `s` for the last time. It ends where the path then first enters the box of `t`.
196
+ * The link keeps `way` when the new way goes out and back and `way` does not.
197
+ * It also keeps `way` when the detour of the new way is longer than `limit`.
198
+ */
199
+ function fewerFar(ways: LevelWays, s: Node, t: Node, way: readonly Point[], cost: number, limit: number): readonly Point[] {
200
+ const near = nearBoxes(ways, s, t);
201
+ const before = boxesThrough(ways, way, s, t, near);
202
+ if (before.far === 0) return way;
203
+ const far = cost * ways.size;
204
+ const path = ways.soft().path(center(s.box), center(t.box), (n) => (n === s || n === t ? 0 : near.has(n) ? ways.size : far));
205
+ const clipped = clip(path, s.box, t.box);
206
+ if (!clipped) return way;
207
+ const soft = tidy(pullThrough(ways, clipped, s, t));
208
+ // A way that goes out and back is a new fault, so the link keeps the old way.
209
+ if (goesOutAndBack(soft) && !goesOutAndBack(way)) return way;
210
+ if (detour(soft) > limit) return way;
211
+ return boxesThrough(ways, soft, s, t, near).far < before.far ? soft : way;
212
+ }
213
+
214
+ /** This function tells if the point is inside the box, not on its boundary. */
215
+ const within = (p: Point, b: Box): boolean => p.x > b.x && p.x < b.x + b.w && p.y > b.y && p.y < b.y + b.h;
216
+
217
+ /** This function gives the point where the segment from `a`, inside the box, to `b` leaves the box. */
218
+ function exitPoint(a: Point, b: Point, box: Box): Point {
219
+ const dx = b.x - a.x;
220
+ const dy = b.y - a.y;
221
+ let u = 1;
222
+ if (dx > 0) u = Math.min(u, (box.x + box.w - a.x) / dx);
223
+ if (dx < 0) u = Math.min(u, (box.x - a.x) / dx);
224
+ if (dy > 0) u = Math.min(u, (box.y + box.h - a.y) / dy);
225
+ if (dy < 0) u = Math.min(u, (box.y - a.y) / dy);
226
+ u = Math.max(0, u);
227
+ return { x: a.x + u * dx, y: a.y + u * dy };
228
+ }
229
+
230
+ /**
231
+ * This function cuts a path from the inside of the box `a` to the inside of the box `b`. The cut path starts where the path leaves `a` for the last time.
232
+ * It ends where the path then first enters `b`. It gives `undefined` when no such part is left.
233
+ */
234
+ function clip(p: readonly Point[], a: Box, b: Box): Point[] | undefined {
235
+ let i = -1;
236
+ for (let k = 0; k < p.length; k++) if (within(p[k]!, a)) i = k;
237
+ if (i < 0 || i === p.length - 1) return undefined;
238
+ let j = -1;
239
+ for (let k = i + 1; k < p.length && j < 0; k++) if (within(p[k]!, b)) j = k;
240
+ if (j < 0) return undefined;
241
+ const start = exitPoint(p[i]!, p[i + 1]!, a);
242
+ const end = exitPoint(p[j]!, p[j - 1]!, b);
243
+ if (within(start, b) || within(end, a)) return undefined;
244
+ return [start, ...p.slice(i + 1, j), end];
245
+ }
246
+
247
+ /**
248
+ * This function pulls a soft path tight. From each vertex, it goes to the farthest vertex of the next 16 vertices with a straight line.
249
+ * The line can go only through the boxes that the path between the two vertices goes through.
250
+ */
251
+ function pullThrough(ways: Ways, p: readonly Point[], s: Node, t: Node, lookahead = 16): Point[] {
252
+ if (p.length < 3) return [...p];
253
+ const boxesOf = (a: Point, b: Point): Node[] => {
254
+ const out: Node[] = [];
255
+ ways.obstacles.through(a, b, (n) => {
256
+ if (n !== s && n !== t) out.push(n);
257
+ });
258
+ return out;
259
+ };
260
+ // The boxes of each leg of the path, found one time.
261
+ const legs = p.map((q, m) => (m === 0 ? [] : boxesOf(p[m - 1]!, q)));
262
+ const out: Point[] = [p[0]!];
263
+ let i = 0;
264
+ while (i < p.length - 1) {
265
+ let next = i + 1;
266
+ const last = Math.min(p.length - 1, i + lookahead);
267
+ // The boxes of the path from vertex i to each vertex j.
268
+ const along: Set<Node>[] = [];
269
+ let seen = new Set<Node>();
270
+ for (let j = i + 1; j <= last; j++) {
271
+ seen = new Set([...seen, ...legs[j]!]);
272
+ along[j] = seen;
273
+ }
274
+ for (let j = last; j > i + 1; j--) {
275
+ if (boxesOf(p[i]!, p[j]!).every((n) => along[j]!.has(n))) {
276
+ next = j;
277
+ break;
278
+ }
279
+ }
280
+ out.push(p[next]!);
281
+ i = next;
282
+ }
283
+ return out;
126
284
  }
127
285
 
128
286
  /**
@@ -0,0 +1,203 @@
1
+ import type { Box, Point } from "../geom.ts";
2
+ import { Grid } from "../grid.ts";
3
+ import type { Edge, Router } from "../model.ts";
4
+ import { segmentsOf, type SegmentGraph } from "../segments.ts";
5
+ import type { Boxed, Gap, RoomLink } from "./index.ts";
6
+
7
+ /**
8
+ * The options of the room for the bends of the bundles (HWC-D). With them, `roomDemand` also opens the gaps where a bundle bends.
9
+ * The values agree with the options of `highwayCurves` that draw the bundles.
10
+ */
11
+ export interface RoomBends {
12
+ /** The router that bundles the edges. Each segment of its routes with two or more edges is a bundle. */
13
+ readonly router: Router;
14
+ /** The edges that the router routes. Without them, each link gives `count` edges, with no data. */
15
+ readonly edges?: readonly Edge[];
16
+ /** The distance between two lanes of a bundle, as `laneWidth` of `highwayCurves`. The default is 4. */
17
+ readonly spacing?: number;
18
+ /** The largest width of a bundle, from its outer lane to its outer lane, as `maxWidth` of `highwayCurves`. The default is `Infinity`. */
19
+ readonly maxWidth?: number;
20
+ /** The smallest radius of each lane, as `minRadius` of `highwayCurves`. The default is 4. */
21
+ readonly minRadius?: number;
22
+ /** The number of passes of the demand of the bends. Each pass routes the board that the passes before it opened. The default is 1. */
23
+ readonly passes?: number;
24
+ }
25
+
26
+ /** One leg of the route of an edge: a straight piece of the center line of one segment. */
27
+ interface BendLeg {
28
+ readonly a: Point;
29
+ readonly b: Point;
30
+ readonly u: Point;
31
+ readonly len: number;
32
+ /** The number of lanes of the segment of the leg. A segment of one edge has 1. */
33
+ readonly lanes: number;
34
+ /** The offset of the lane of the edge from the center line, to the left of the leg. */
35
+ readonly off: number;
36
+ /** The key of the leg: its segment and its place in the segment. Two edges of one segment share the key. */
37
+ readonly key: string;
38
+ }
39
+
40
+ /** A turn smaller than this angle, in radians, is no corner. A turn nearer to a half turn than this angle is a spike, and it gets no arc. */
41
+ const FLAT = 1e-6;
42
+ const SPIKE = 0.01;
43
+ const SAME = 1e-6;
44
+ /** A leg with a smaller share of its length on an axis does not grow on that axis. */
45
+ const SLANT = 0.2;
46
+
47
+ const turnOf = (u: Point, w: Point): number => Math.atan2(u.x * w.y - u.y * w.x, u.x * w.x + u.y * w.y);
48
+
49
+ /**
50
+ * This function gives the length along a leg of an S of two arcs with the radius `r` that shifts a lane by `shift`.
51
+ * It is the length of the shift of `highwayCurves` at a merge or a split.
52
+ */
53
+ function jogLength(r: number, shift: number): number {
54
+ const d = Math.abs(shift);
55
+ if (d < SAME || r <= 0) return 0;
56
+ if (r < d / 2) return 2 * r;
57
+ return 2 * r * Math.sin(Math.acos(1 - d / (2 * r)));
58
+ }
59
+
60
+ /** This function gives the legs of the route of each edge, with the lanes and the offset of the lane on each leg. */
61
+ function legsOf(graph: SegmentGraph, spacing: number, maxWidth: number): BendLeg[][] {
62
+ const byId = new Map(graph.segments.map((s) => [s.id, s]));
63
+ return graph.paths.map((path) => {
64
+ const legs: BendLeg[] = [];
65
+ for (const ref of path.segments) {
66
+ const s = byId.get(ref.segment);
67
+ if (!s) continue;
68
+ const sp = s.count > 1 ? Math.min(spacing, maxWidth / (s.count - 1)) : 0;
69
+ const offset = ((s.count - 1) / 2 - ref.lane) * sp;
70
+ const c: Point[] = [];
71
+ for (const p of s.points) if (c.length === 0 || Math.hypot(p.x - c[c.length - 1]!.x, p.y - c[c.length - 1]!.y) > SAME) c.push(p);
72
+ const n = c.length;
73
+ for (let j = 0; j + 1 < n; j++) {
74
+ const from = ref.reverse ? n - 1 - j : j;
75
+ const to = ref.reverse ? n - 2 - j : j + 1;
76
+ const a = c[from]!;
77
+ const b = c[to]!;
78
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
79
+ legs.push({
80
+ a,
81
+ b,
82
+ u: { x: (b.x - a.x) / len, y: (b.y - a.y) / len },
83
+ len,
84
+ lanes: sp > 0 ? s.count : 1,
85
+ off: ref.reverse ? -offset : offset,
86
+ key: `${s.id}:${Math.min(from, to)}`,
87
+ });
88
+ }
89
+ }
90
+ return legs;
91
+ });
92
+ }
93
+
94
+ /**
95
+ * This function gives the length that the arcs at one end of a leg use on the leg, as the lanes of `highwayCurves` use it.
96
+ * `A` is the leg before the vertex, `B` the leg after it, and `X` the leg of the two that the length is for.
97
+ *
98
+ * - At a bend of two bundles, the center line has the radius `R = W/2 + minRadius`, with `W` the width of the wider bundle. The tangent length is `T = R tan(φ/2)`.
99
+ * Each lane uses the same part of the leg, because the arcs are concentric.
100
+ * - At another corner, the lane has the radius `minRadius` at its own corner, at the offset `c` of the lane. The tangent length is `(minRadius + |c|) tan(φ/2)`.
101
+ * - A lane that joins or leaves a bundle shifts on the leg with fewer lanes. The S of the shift adds its length on that leg.
102
+ */
103
+ function endLength(A: BendLeg, B: BendLeg, X: BendLeg, minRadius: number): number {
104
+ const phi = Math.abs(turnOf(A.u, B.u));
105
+ if (Math.PI - phi < SPIKE) return 0;
106
+ const k = phi < FLAT ? 0 : Math.tan(phi / 2);
107
+ const c = A.lanes < B.lanes ? B.off : A.off;
108
+ if (A.lanes > 1 && B.lanes > 1) return k * (Math.max(Math.abs(A.off), Math.abs(B.off), Math.abs(c)) + minRadius);
109
+ return k * (minRadius + Math.abs(c)) + jogLength(minRadius, c - X.off);
110
+ }
111
+
112
+ /**
113
+ * This function gives, for each leg of the routes, the length that the leg lacks for the arcs at its two ends.
114
+ * `highwayCurves` gives each end of a leg at most the half of the leg. So a leg needs `2 max(T_A, T_B)`, and this length is at least `T_A + T_B`.
115
+ * The first and the last leg of a route end at a plug, and the plug end needs no length.
116
+ */
117
+ function shortLegs(legs: readonly (readonly BendLeg[])[], minRadius: number): Map<string, { leg: BendLeg; short: number }> {
118
+ const out = new Map<string, { leg: BendLeg; short: number }>();
119
+ for (const path of legs) {
120
+ if (path.every((l) => l.lanes < 2)) continue;
121
+ for (let j = 0; j < path.length; j++) {
122
+ const X = path[j]!;
123
+ const start = j > 0 ? endLength(path[j - 1]!, X, X, minRadius) : 0;
124
+ const end = j + 1 < path.length ? endLength(X, path[j + 1]!, X, minRadius) : 0;
125
+ const short = 2 * Math.max(start, end) - X.len;
126
+ if (short <= SAME) continue;
127
+ const known = out.get(X.key);
128
+ if (!known || known.short < short) out.set(X.key, { leg: X, short });
129
+ }
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /** This function gives the mean of the longer side of the boxes. */
135
+ function meanSide(items: readonly Boxed[]): number {
136
+ let s = 0;
137
+ for (const i of items) s += Math.max(i.box.w, i.box.h);
138
+ return items.length ? s / items.length : 1;
139
+ }
140
+
141
+ /** This function gives the box nearest to a point, among the boxes within `reach` of it. */
142
+ function nearestBox(grid: Grid<Box>, p: Point, reach: number): Box | undefined {
143
+ let best: Box | undefined;
144
+ let bestD = Infinity;
145
+ grid.query({ x: p.x - reach, y: p.y - reach, w: 2 * reach, h: 2 * reach }, (b) => {
146
+ const dx = Math.max(b.x - p.x, 0, p.x - b.x - b.w);
147
+ const dy = Math.max(b.y - p.y, 0, p.y - b.y - b.h);
148
+ const d = Math.hypot(dx, dy);
149
+ if (d < bestD || (d === bestD && best && (b.x < best.x || (b.x === best.x && b.y < best.y)))) [best, bestD] = [b, d];
150
+ });
151
+ return bestD <= reach ? best : undefined;
152
+ }
153
+
154
+ /**
155
+ * This function gives the growth that each gap of the board needs for the bends of the bundles, in the order of `gaps`.
156
+ * The router routes the board. Each segment with two or more edges is a bundle, as `highwayCurves` opens it.
157
+ *
158
+ * A leg that is too short for the arcs at its ends grows by the gaps between its two ends.
159
+ * Each end of a leg moves with its nearest box, as `openGaps` moves the boxes. So a gap between the left (or top) edges of the two boxes lengthens the leg.
160
+ * The leg grows on the axis of its longer side. The gap nearest to the middle of the leg takes the growth.
161
+ * A gap that more legs need takes the largest growth, not the sum.
162
+ */
163
+ export function bendGrowth(items: readonly Boxed[], links: readonly RoomLink[], gaps: readonly Gap[], bends: RoomBends, snap: number): number[] {
164
+ const spacing = Math.max(0, bends.spacing ?? 4);
165
+ const maxWidth = bends.maxWidth ?? Infinity;
166
+ const minRadius = Math.max(0, bends.minRadius ?? 4);
167
+ const edges: Edge[] = bends.edges
168
+ ? [...bends.edges]
169
+ : links.flatMap((l, i) => Array.from({ length: Math.max(1, l.count ?? 1) }, (_, k) => ({ id: `l${i}-${k}`, source: { node: l.source }, target: { node: l.target } })));
170
+ const graph = segmentsOf(bends.router.route({ nodes: items, edges }));
171
+ const short = shortLegs(legsOf(graph, spacing, maxWidth), minRadius);
172
+ const grow: number[] = gaps.map(() => 0);
173
+ if (short.size === 0) return grow;
174
+ const side = meanSide(items);
175
+ const grid = new Grid<Box>(Math.max(20, side));
176
+ for (const i of items) grid.add(i.box, i.box);
177
+ const anchor = (p: Point): Box | undefined => nearestBox(grid, p, side) ?? nearestBox(grid, p, 4 * side);
178
+ const byAxis = { x: [] as number[], y: [] as number[] };
179
+ gaps.forEach((g, i) => byAxis[g.axis].push(i));
180
+ for (const { leg, short: s } of short.values()) {
181
+ const A = anchor(leg.a);
182
+ const B = anchor(leg.b);
183
+ if (!A || !B) continue;
184
+ const axes = (["x", "y"] as const).filter((x) => Math.abs(leg.u[x]) >= SLANT).sort((p, q) => Math.abs(leg.u[q]) - Math.abs(leg.u[p]));
185
+ for (const axis of axes) {
186
+ const la = A[axis];
187
+ const lb = B[axis];
188
+ const lo = Math.min(la, lb);
189
+ const hi = Math.max(la, lb);
190
+ const mid = (leg.a[axis] + leg.b[axis]) / 2;
191
+ let pick = -1;
192
+ for (const i of byAxis[axis]) {
193
+ const g = gaps[i]!;
194
+ if (g.at <= lo + snap || g.at > hi + snap) continue;
195
+ if (pick < 0 || Math.abs(g.at - mid) < Math.abs(gaps[pick]!.at - mid)) pick = i;
196
+ }
197
+ if (pick < 0) continue;
198
+ grow[pick] = Math.max(grow[pick]!, s / Math.abs(leg.u[axis]));
199
+ break;
200
+ }
201
+ }
202
+ return grow;
203
+ }
package/src/room/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { center, type Box, type Point } from "../geom.ts";
2
2
  import { levelLinks } from "../levels.ts";
3
3
  import type { Edge, Graph, Router } from "../model.ts";
4
+ import { bendGrowth, type RoomBends } from "./bends.ts";
5
+
6
+ export type { RoomBends } from "./bends.ts";
4
7
 
5
8
  /** An item with a box: a node of a graph, an item of a level or a box of a host layout. */
6
9
  export interface Boxed {
@@ -70,6 +73,12 @@ export interface RoomOptions {
70
73
  * The limit `maxGrowth` applies to the sum of the passes.
71
74
  */
72
75
  readonly passes?: number;
76
+ /**
77
+ * The room for the bends of the bundles (HWC-D). Without it, the default, the demand counts only the lanes of the links.
78
+ * With it, the router of `bends` routes the open board. Each leg of a bundle that is too short for its arcs grows by a gap.
79
+ * The limit `maxGrowth` applies to the sum of the growth of the links and of the bends.
80
+ */
81
+ readonly bends?: RoomBends;
73
82
  }
74
83
 
75
84
  /** The demand of a board and the board with its gaps open. */
@@ -163,9 +172,28 @@ export function roomDemand(items: readonly Boxed[], links: readonly RoomLink[],
163
172
  if (next.length !== total.length || next.some((g, i) => g.axis !== total[i]!.axis)) break;
164
173
  total = total.map((g, i) => ({ ...g, lanes: Math.max(g.lanes, next[i]!.lanes), need: g.need + Math.max(0, next[i]!.need - next[i]!.width) }));
165
174
  }
175
+ if (options.bends) total = withBends(items, links, total, options.bends, snap);
166
176
  return capGrowth(items, total, gutter, options.maxGrowth ?? Infinity);
167
177
  }
168
178
 
179
+ /**
180
+ * This function adds the growth for the bends of the bundles to the need of each gap. Each pass routes the board with each gap open to its need.
181
+ * The open board has the same gaps in the same order. So the growth of each gap adds to the need of that gap.
182
+ */
183
+ export function withBends(items: readonly Boxed[], links: readonly RoomLink[], gaps: readonly Gap[], bends: RoomBends, snap: number = DEFAULT_SNAP): Gap[] {
184
+ let total = [...gaps];
185
+ const passes = Math.max(1, Math.floor(bends.passes ?? 1));
186
+ for (let p = 0; p < passes; p++) {
187
+ const board = openGaps(items, total, snap);
188
+ const open = boardGaps(board, snap);
189
+ if (open.length !== total.length || open.some((g, i) => g.axis !== total[i]!.axis)) break;
190
+ const grow = bendGrowth(board, links, open, bends, snap);
191
+ if (grow.every((g) => g <= 0)) break;
192
+ total = total.map((g, i) => (grow[i]! > 0 ? { ...g, need: g.need + grow[i]! } : g));
193
+ }
194
+ return total;
195
+ }
196
+
169
197
  /** This function gives the demand of one pass, without the limit `maxGrowth`. */
170
198
  function demandOnce(items: readonly Boxed[], links: readonly RoomLink[], options: RoomOptions): Gap[] {
171
199
  const gutter = options.gutter ?? DEFAULT_GUTTER;
@@ -0,0 +1,155 @@
1
+ import type { ItemPair } from "../../levels.ts";
2
+ import type { LevelItem } from "../../model.ts";
3
+ import { levelCut, treeDepth, type Section } from "../../sections.ts";
4
+ import { boardGaps, openGaps, roomDemand, withBends, type Boxed, type Gap, type RoomLink, type RoomOptions } from "../index.ts";
5
+
6
+ /**
7
+ * The boards of the zoom levels of `levelRoom`.
8
+ *
9
+ * - `shared`: each level uses the board of the nearest level. No box moves between two levels. A far level gets the area of the nearest level.
10
+ * - `each`: each level has its own board. A far level opens only the gaps of its own links, and a nearer level adds to them.
11
+ * Thus the items of a far level are nearer together, but a box moves in its parent when the level changes.
12
+ */
13
+ export type LevelBoard = "each" | "shared";
14
+
15
+ /** The options of `levelRoom`: the options of `roomDemand` for each level, and the levels that get room. */
16
+ export interface LevelRoomOptions extends RoomOptions {
17
+ /** The depths of the tree that get room for their links. The default is each depth from 1 to the depth of the deepest section. */
18
+ readonly depths?: readonly number[];
19
+ /** With `true`, the last level, with each node alone, also gets room for the links between the nodes. The default is `false`. */
20
+ readonly nodes?: boolean;
21
+ /** The board of each level. The default is `shared`. */
22
+ readonly board?: LevelBoard;
23
+ }
24
+
25
+ /** One zoom level of `levelRoom`: its items at their open places, its links and its demand. */
26
+ export interface RoomLevel<T extends Boxed> {
27
+ readonly depth: number;
28
+ /** The items of the level, with the bounding box of their nodes at the places of this level. */
29
+ readonly items: LevelItem[];
30
+ /** The links between the items of the level. A link counts the links of the nodes between its two items. */
31
+ readonly pairs: ItemPair[];
32
+ /** The demand of the level on the board that it gets from the level before it. A level without room has no gaps. */
33
+ readonly gaps: Gap[];
34
+ /** The nodes at the places of this level. */
35
+ readonly nodes: T[];
36
+ }
37
+
38
+ /** The room of each zoom level, and the board of the nearest level. */
39
+ export interface LevelRoom<T extends Boxed> {
40
+ /** The levels from depth 0 to the level with each node alone, in order. */
41
+ readonly levels: RoomLevel<T>[];
42
+ /** The gaps of the board of the nodes. `openGaps(nodes, gaps)` gives the nodes of the nearest level. */
43
+ readonly gaps: Gap[];
44
+ /** The nodes of the nearest level. */
45
+ readonly items: T[];
46
+ }
47
+
48
+ const DEFAULT_SNAP = 0.5;
49
+
50
+ /**
51
+ * This function gives the links between the items of a level. Each pair of items gets one link, in the order of the first link of the pair.
52
+ * The count of a link is the sum of the counts of its node links. A link inside one item does not count.
53
+ */
54
+ export function levelPairs(items: readonly LevelItem[], owner: readonly number[], indexOf: ReadonlyMap<string, number>, links: readonly RoomLink[]): ItemPair[] {
55
+ const byPair = new Map<string, { source: string; target: string; count: number }>();
56
+ for (const l of links) {
57
+ const iu = indexOf.get(l.source);
58
+ const iv = indexOf.get(l.target);
59
+ if (iu === undefined || iv === undefined) continue;
60
+ const a = owner[iu]!;
61
+ const b = owner[iv]!;
62
+ if (a === b) continue;
63
+ const key = a < b ? `${a} ${b}` : `${b} ${a}`;
64
+ const known = byPair.get(key);
65
+ if (known) known.count += l.count ?? 1;
66
+ else byPair.set(key, { source: items[a]!.id, target: items[b]!.id, count: l.count ?? 1 });
67
+ }
68
+ return [...byPair.values()];
69
+ }
70
+
71
+ /**
72
+ * This function gives the room of each zoom level of a tree of sections. The indices of the nodes of the tree are the indices of `nodes`.
73
+ * The far levels open first, and each nearer level keeps their growth.
74
+ *
75
+ * At each level, `roomDemand` measures the links of that level on the board of the level before it. Then the nodes move with `openGaps`.
76
+ * The nodes move by one monotone shift on each axis, so each level keeps the order and the alignment of the boxes.
77
+ * A nearer level only adds growth, so no gap of a far level gets narrower, and a box moves with its parent.
78
+ */
79
+ export function levelRoom<T extends Boxed>(nodes: readonly T[], links: readonly RoomLink[], root: Section, options: LevelRoomOptions = {}): LevelRoom<T> {
80
+ const snap = options.snap ?? DEFAULT_SNAP;
81
+ const deepest = treeDepth(root);
82
+ const depths = new Set(options.depths ?? Array.from({ length: deepest }, (_, i) => i + 1));
83
+ if (options.nodes) depths.add(deepest + 1);
84
+ const indexOf = new Map(nodes.map((n, i) => [n.id, i]));
85
+ const levels: RoomLevel<T>[] = [];
86
+ let current: T[] = [...nodes];
87
+ // The edges of the bends join nodes, not the items of a level. So the bends get room on the board of the nodes, at the last level.
88
+ const { bends, ...room } = options;
89
+ for (let d = 0; d <= deepest + 1; d++) {
90
+ let gaps: Gap[] = [];
91
+ let cut = levelCut(current, root, d);
92
+ const pairs = levelPairs(cut.items, cut.owner, indexOf, links);
93
+ if (depths.has(d) && pairs.length > 0) {
94
+ gaps = roomDemand(cut.items, pairs, room);
95
+ // The shift of each gap applies to the nodes, so a node moves with its item, and the items of a far level stay apart.
96
+ if (gaps.some((g) => g.need > g.width)) {
97
+ current = openGaps(current, gaps, snap);
98
+ cut = levelCut(current, root, d);
99
+ }
100
+ }
101
+ if (bends && d === deepest + 1) {
102
+ const bent = withBends(current, links, boardGaps(current, snap), bends, snap);
103
+ if (bent.some((g) => g.need > g.width)) {
104
+ current = openGaps(current, bent, snap);
105
+ cut = levelCut(current, root, d);
106
+ }
107
+ }
108
+ levels.push({ depth: d, items: cut.items, pairs, gaps, nodes: current });
109
+ }
110
+ const shared = (options.board ?? "shared") === "shared";
111
+ const out = shared ? levels.map((l) => ({ ...l, items: levelCut(current, root, l.depth).items, nodes: current })) : levels;
112
+ return { levels: out, gaps: nodeGaps(nodes, current, levels, snap), items: current };
113
+ }
114
+
115
+ /**
116
+ * This function gives the gaps of the board of the nodes that move each node from `from` to `to`.
117
+ * The growth of a gap is the shift of the nodes after it, less the shift of the nodes before it. Its lanes are the most lanes of a level gap there.
118
+ */
119
+ function nodeGaps<T extends Boxed>(from: readonly T[], to: readonly T[], levels: readonly RoomLevel<T>[], snap: number): Gap[] {
120
+ const indexOf = new Map(from.map((n, i) => [n.id, i]));
121
+ const gaps = boardGaps(from, snap);
122
+ const shift = (id: string, axis: "x" | "y"): number => {
123
+ const i = indexOf.get(id)!;
124
+ return to[i]!.box[axis] - from[i]!.box[axis];
125
+ };
126
+ // The lanes of each level gap go to the gap of the nodes at the left (or top) edge of its first item after it.
127
+ const lanes = new Map<number, number>();
128
+ const boundaryOf = new Map<string, number>();
129
+ gaps.forEach((g, k) => {
130
+ for (const id of g.after) boundaryOf.set(`${g.axis} ${id}`, k);
131
+ });
132
+ for (const level of levels) {
133
+ const byId = new Map(level.items.map((i) => [i.id, i]));
134
+ for (const g of level.gaps) {
135
+ if (g.lanes === 0) continue;
136
+ for (const id of g.after) {
137
+ const item = byId.get(id);
138
+ if (!item) continue;
139
+ // The first node of the item on the axis of the gap starts the item.
140
+ let first = item.nodes[0]!;
141
+ for (const n of item.nodes) if (from[indexOf.get(n)!]!.box[g.axis] < from[indexOf.get(first)!]!.box[g.axis]) first = n;
142
+ const k = boundaryOf.get(`${g.axis} ${first}`);
143
+ if (k !== undefined) lanes.set(k, Math.max(lanes.get(k) ?? 0, g.lanes));
144
+ }
145
+ }
146
+ }
147
+ const last: Record<"x" | "y", number> = { x: 0, y: 0 };
148
+ return gaps.map((g, k) => {
149
+ let s = Infinity;
150
+ for (const id of g.after) s = Math.min(s, shift(id, g.axis));
151
+ const growth = Math.max(0, s - last[g.axis]);
152
+ last[g.axis] = Math.max(last[g.axis], s);
153
+ return { ...g, lanes: lanes.get(k) ?? 0, need: g.width + growth };
154
+ });
155
+ }