@mark1russell7/highways 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +67 -1
  2. package/build/index.d.ts +954 -2
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +5914 -1419
  5. package/build/index.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/bench/measures/bundles.ts +101 -2
  8. package/src/bench/measures/highways.ts +93 -0
  9. package/src/bench/measures/index.ts +2 -0
  10. package/src/bench/measures/order.ts +97 -0
  11. package/src/bench/room.ts +126 -9
  12. package/src/bench/screen/measures.ts +17 -0
  13. package/src/bench/screen/screen.ts +87 -9
  14. package/src/highways/base.ts +126 -0
  15. package/src/highways/flow-field/index.ts +49 -0
  16. package/src/highways/gates/ambiguity/index.ts +110 -0
  17. package/src/highways/gates/breakeven/index.ts +62 -0
  18. package/src/highways/gates/hysteresis/index.ts +48 -0
  19. package/src/highways/gates/ids/index.ts +59 -0
  20. package/src/highways/gates/index.ts +6 -0
  21. package/src/highways/gates/model.ts +129 -0
  22. package/src/highways/gates/stretch/index.ts +39 -0
  23. package/src/highways/grouped.ts +123 -0
  24. package/src/highways/index.ts +15 -0
  25. package/src/highways/level-links/index.ts +114 -0
  26. package/src/highways/meet/index.ts +84 -0
  27. package/src/highways/model.ts +5 -0
  28. package/src/highways/od-cells/index.ts +50 -0
  29. package/src/highways/quickbundles/index.ts +117 -0
  30. package/src/highways/road-classes/index.ts +74 -0
  31. package/src/highways/router.ts +1 -1
  32. package/src/highways/select/index.ts +162 -0
  33. package/src/highways/stretches.ts +141 -0
  34. package/src/highways/stubs/index.ts +117 -0
  35. package/src/highways/weiszfeld/index.ts +105 -0
  36. package/src/highways/wspd/index.ts +130 -0
  37. package/src/highways/zipper/index.ts +168 -0
  38. package/src/index.ts +2 -0
  39. package/src/order/index.ts +65 -0
  40. package/src/plugs/justify.ts +48 -0
  41. package/src/plugs/side.ts +138 -0
  42. package/src/repair/index.ts +377 -0
  43. package/src/repair/walk.ts +220 -0
  44. package/src/room/lanes.ts +109 -10
  45. package/src/routers/corners.ts +73 -6
  46. package/src/routers/fan.ts +145 -0
  47. package/src/routers/index.ts +2 -2
  48. package/src/routers/shared.ts +107 -8
  49. package/src/routers/snell.ts +140 -0
  50. package/src/routers/trunks.ts +43 -2
  51. package/src/strategies/builtins.ts +149 -7
  52. package/src/strategies/schemas.ts +173 -2
package/src/room/lanes.ts CHANGED
@@ -15,6 +15,19 @@ export type LaneOrder = "links" | "ends" | "crossings";
15
15
  /** The lane orders of `gapLanes`, in the order of the benchmark. */
16
16
  export const LANE_ORDERS: readonly LaneOrder[] = ["links", "ends", "crossings"];
17
17
 
18
+ /**
19
+ * The rule that gives each piece of a gap its lane (its track), after the lane order.
20
+ *
21
+ * - `stack`: a piece goes one lane past each piece before it in the order that it overlaps. Thus two pieces that overlap keep their order.
22
+ * - `left-edge`: the constrained left-edge rule of chip routing (EDA-004). Two pieces that overlap keep their order only where a swap adds crossings.
23
+ * The rule fills the lowest lane first, with the pieces sorted by their start. Two pieces that do not overlap can share a lane.
24
+ * - `free`: the left-edge rule without the order. It gives the fewest lanes, but it can add crossings.
25
+ */
26
+ export type LaneTracks = "stack" | "left-edge" | "free";
27
+
28
+ /** The lane rules of `gapLanes`, in the order of the benchmark. */
29
+ export const LANE_TRACKS: readonly LaneTracks[] = ["stack", "left-edge", "free"];
30
+
18
31
  /** The options of `gapLanes`. */
19
32
  export interface GapLaneOptions {
20
33
  /** The largest distance between two lanes. A narrow gap puts its lanes nearer. The default is 4. */
@@ -23,6 +36,22 @@ export interface GapLaneOptions {
23
36
  readonly order?: LaneOrder;
24
37
  /** Two box edges nearer than this distance make one boundary of the sweep, as in `boardGaps`. The default is 0.5. */
25
38
  readonly snap?: number;
39
+ /** The rule that gives each piece its lane. The default is `stack`. */
40
+ readonly tracks?: LaneTracks;
41
+ }
42
+
43
+ /** The lanes of one gap: the axis and the place of the gap, and the number of lanes that its pieces use. */
44
+ export interface GapLaneCount {
45
+ readonly axis: "x" | "y";
46
+ readonly at: number;
47
+ /** The width of the gap. */
48
+ readonly width: number;
49
+ /** The number of lanes of the gap. */
50
+ readonly lanes: number;
51
+ /** The number of pieces in the gap: the legs of the links that run along it. */
52
+ readonly pieces: number;
53
+ /** The largest number of pieces that overlap at one place of the gap. No rule gives fewer lanes. */
54
+ readonly density: number;
26
55
  }
27
56
 
28
57
  /** A piece of a link: a leg of the link that runs along a gap. */
@@ -47,9 +76,21 @@ const ROUNDS = 8;
47
76
  * When a move puts a link through more boxes than before, the link keeps its points.
48
77
  */
49
78
  export function gapLanes(items: readonly Boxed[], links: readonly LevelLink[], options: GapLaneOptions = {}): LevelLink[] {
79
+ return laneGaps(items, links, options).links;
80
+ }
81
+
82
+ /** This function gives the lanes of each gap that holds a piece of a link, with the options of `gapLanes`. A gap without a piece is not in the list. */
83
+ export function gapLaneCounts(items: readonly Boxed[], links: readonly LevelLink[], options: GapLaneOptions = {}): GapLaneCount[] {
84
+ return laneGaps(items, links, options).counts;
85
+ }
86
+
87
+ /** This function does the work of `gapLanes` and `gapLaneCounts`. It gives the moved links and the lanes of each gap. */
88
+ function laneGaps(items: readonly Boxed[], links: readonly LevelLink[], options: GapLaneOptions): { links: LevelLink[]; counts: GapLaneCount[] } {
50
89
  const spacing = Math.max(0, options.spacing ?? 4);
51
90
  const order = options.order ?? "crossings";
91
+ const tracks = options.tracks ?? "stack";
52
92
  const moved = links.map((l) => l.points.map((p) => ({ x: p.x, y: p.y })));
93
+ const counts: GapLaneCount[] = [];
53
94
  for (const g of boardGaps(items, options.snap)) {
54
95
  if (g.width <= EPS) continue;
55
96
  const across = g.axis;
@@ -80,15 +121,9 @@ export function gapLanes(items: readonly Boxed[], links: readonly LevelLink[], o
80
121
  });
81
122
  if (pieces.length === 0) continue;
82
123
  const list = orderPieces(pieces, order);
83
- // A piece goes one lane past each piece before it in the order that it overlaps. Thus two pieces that overlap keep their order.
84
- const lane: number[] = [];
85
- let n = 0;
86
- list.forEach((P, i) => {
87
- let k = 0;
88
- for (let j = 0; j < i; j++) if (overlaps(list[j]!, P)) k = Math.max(k, lane[j]! + 1);
89
- lane.push(k);
90
- n = Math.max(n, k + 1);
91
- });
124
+ const lane = tracks === "stack" ? stackLanes(list) : leftEdgeLanes(list, tracks === "left-edge");
125
+ const n = lane.reduce((m, k) => Math.max(m, k + 1), 0);
126
+ counts.push({ axis: g.axis, at: g.at, width: g.width, lanes: n, pieces: list.length, density: density(list) });
92
127
  const step = Math.min(spacing, g.width / (n + 1));
93
128
  const mid = (lo + hi) / 2;
94
129
  list.forEach((P, i) => {
@@ -99,7 +134,71 @@ export function gapLanes(items: readonly Boxed[], links: readonly LevelLink[], o
99
134
  }
100
135
  const boxes = new Grid<Box>(Math.max(20, meanSide(items)));
101
136
  for (const i of items) boxes.add(i.box, i.box);
102
- return links.map((l, i) => (hits(boxes, moved[i]!) > hits(boxes, l.points) ? l : { ...l, points: moved[i]! }));
137
+ return { links: links.map((l, i) => (hits(boxes, moved[i]!) > hits(boxes, l.points) ? l : { ...l, points: moved[i]! })), counts };
138
+ }
139
+
140
+ /** This function gives the lanes of the rule `stack`. A piece goes one lane past each piece before it in the order that it overlaps. */
141
+ function stackLanes(list: readonly Piece[]): number[] {
142
+ const lane: number[] = [];
143
+ list.forEach((P, i) => {
144
+ let k = 0;
145
+ for (let j = 0; j < i; j++) if (overlaps(list[j]!, P)) k = Math.max(k, lane[j]! + 1);
146
+ lane.push(k);
147
+ });
148
+ return lane;
149
+ }
150
+
151
+ /**
152
+ * This function gives the lanes of the left-edge rule (Hashimoto and Stevens, 1971) for the pieces in lane order.
153
+ * With `constrained`, a piece stays below a later piece that it overlaps when the swap of the two adds crossings (`cost`).
154
+ * These constraints follow the lane order, so they have no cycle. Each pass fills one lane, from the near side.
155
+ * A pass takes the free pieces whose constraints are met, in the order of their start. It puts each piece that does not overlap the last piece of the lane.
156
+ */
157
+ function leftEdgeLanes(list: readonly Piece[], constrained: boolean): number[] {
158
+ const n = list.length;
159
+ const below: number[][] = list.map(() => []);
160
+ const wait = new Array<number>(n).fill(0);
161
+ if (constrained)
162
+ for (let i = 0; i < n; i++)
163
+ for (let j = i + 1; j < n; j++) {
164
+ const P = list[i]!;
165
+ const Q = list[j]!;
166
+ if (overlaps(P, Q) && cost(Q, P) > cost(P, Q)) {
167
+ below[i]!.push(j);
168
+ wait[j]!++;
169
+ }
170
+ }
171
+ const byStart = list.map((_, i) => i).sort((a, b) => list[a]!.lo - list[b]!.lo || a - b);
172
+ const lane = new Array<number>(n).fill(-1);
173
+ let placed = 0;
174
+ for (let track = 0; placed < n; track++) {
175
+ const now: number[] = [];
176
+ let end = -Infinity;
177
+ for (const i of byStart) {
178
+ if (lane[i]! >= 0 || wait[i]! > 0 || list[i]!.lo < end - EPS) continue;
179
+ now.push(i);
180
+ end = list[i]!.hi;
181
+ }
182
+ for (const i of now) lane[i] = track;
183
+ for (const i of now) for (const j of below[i]!) wait[j]!--;
184
+ placed += now.length;
185
+ }
186
+ return lane;
187
+ }
188
+
189
+ /** This function gives the largest number of pieces that overlap at one place. Pieces that only touch do not overlap. */
190
+ function density(list: readonly Piece[]): number {
191
+ const events: [number, number][] = [];
192
+ for (const P of list) events.push([P.lo, 1], [P.hi, -1]);
193
+ // At one place, an end comes before a start, so pieces that only touch do not overlap.
194
+ events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
195
+ let open = 0;
196
+ let most = 0;
197
+ for (const [, d] of events) {
198
+ open += d;
199
+ most = Math.max(most, open);
200
+ }
201
+ return most;
103
202
  }
104
203
 
105
204
  const overlaps = (P: Piece, Q: Piece): boolean => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS;
@@ -1,14 +1,29 @@
1
- import { center } from "../geom.ts";
2
- import { nodeIndex, type Graph, type Node, type Result, type Route, type Router } from "../model.ts";
3
- import { placePlug } from "../plugs.ts";
1
+ import { center, type Point } from "../geom.ts";
2
+ import { nodeIndex, type Edge, type Graph, type Node, type Result, type Route, type Router } from "../model.ts";
3
+ import { placePlug, type Plug } from "../plugs.ts";
4
+ import { justifyPlugs } from "../plugs/justify.ts";
5
+ import { sidePlugs } from "../plugs/side.ts";
4
6
  import { WayFinder, type WayOptions } from "../ways.ts";
5
7
 
8
+ /** The ways to place a free plug of the corner router. */
9
+ export type PlugPlace = "facing" | "side";
10
+
6
11
  /** The options of the corner router. */
7
12
  export interface CornersOptions extends WayOptions {
8
13
  /** The name in the benchmark. */
9
14
  readonly name: string;
10
15
  /** The benchmark skips a fixture with more nodes. */
11
16
  readonly maxNodes?: number;
17
+ /**
18
+ * The place of a free plug. The default is `facing`: the plug faces the other end (`placePlug`).
19
+ * With `side`, the search starts from the whole free side, and the plug goes where the way leaves the side (`sidePlugs`, WILD-001).
20
+ */
21
+ readonly plugs?: PlugPlace;
22
+ /**
23
+ * The least gap between two free plugs of one side. The default is 0: each plug stays where it is.
24
+ * Above 0, the free plugs of each side keep their order and move the least distance (`justifyPlugs`, WILD-002).
25
+ */
26
+ readonly plugGap?: number;
12
27
  }
13
28
 
14
29
  /**
@@ -23,16 +38,68 @@ export function cornersRouter(options: CornersOptions): Router {
23
38
  const byId = nodeIndex(graph);
24
39
  const ways = new WayFinder(graph.nodes, options);
25
40
  const routes: Route[] = [];
41
+ if ((options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
42
+ for (const e of graph.edges) {
43
+ const s = byId.get(e.source.node);
44
+ const t = byId.get(e.target.node);
45
+ if (!s || !t) continue;
46
+ const ps = placePlug(s, e.source, center(t.box));
47
+ const pt = placePlug(t, e.target, center(s.box));
48
+ routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
49
+ }
50
+ return { routes };
51
+ }
52
+ // First the plugs of each edge, then the gaps of the plugs of each side, then the ways between the plugs.
53
+ const ends: [Edge, Node, Node, Plug, Plug][] = [];
26
54
  for (const e of graph.edges) {
27
55
  const s = byId.get(e.source.node);
28
56
  const t = byId.get(e.target.node);
29
57
  if (!s || !t) continue;
30
- const ps = placePlug(s, e.source, center(t.box));
31
- const pt = placePlug(t, e.target, center(s.box));
32
- routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
58
+ const [ps, pt] =
59
+ options.plugs === "side" ? sidePlugs(ways, s, e.source, t, e.target) : [placePlug(s, e.source, center(t.box)), placePlug(t, e.target, center(s.box))];
60
+ ends.push([e, s, t, ps, pt]);
33
61
  }
62
+ if ((options.plugGap ?? 0) > 0) spacePlugs(ends, options.plugGap!);
63
+ for (const [e, s, t, ps, pt] of ends) routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
34
64
  return { routes };
35
65
  },
36
66
  };
37
67
  return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
38
68
  }
69
+
70
+ /**
71
+ * This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
72
+ * The plugs keep their order on the side. Two plugs at one place take the order of their other ends. A fixed plug does not move.
73
+ */
74
+ function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number): void {
75
+ const inset = 6;
76
+ // The free plugs of each side: the index of the edge, the end (3 for the source, 4 for the target) and the point of the other end.
77
+ const sides = new Map<string, { node: Node; normal: Point; items: [number, 3 | 4, Point][] }>();
78
+ ends.forEach(([e, s, t, ps, pt], i) => {
79
+ for (const [end, node, plug, other, k] of [
80
+ [e.source, s, ps, pt, 3],
81
+ [e.target, t, pt, ps, 4],
82
+ ] as const) {
83
+ const port = end.port === undefined ? undefined : node.ports?.find((p) => p.id === end.port);
84
+ if (port?.at !== undefined) continue;
85
+ const key = JSON.stringify([node.id, plug.normal.x, plug.normal.y]);
86
+ let side = sides.get(key);
87
+ if (!side) sides.set(key, (side = { node, normal: plug.normal, items: [] }));
88
+ side.items.push([i, k, other.point]);
89
+ }
90
+ });
91
+ for (const { node, normal, items } of sides.values()) {
92
+ if (items.length < 2) continue;
93
+ const along = normal.x !== 0 ? "y" : "x";
94
+ const b = node.box;
95
+ const start = along === "y" ? b.y : b.x;
96
+ const len = along === "y" ? b.h : b.w;
97
+ const m = Math.min(inset, len / 4);
98
+ const ideal = items.map(([i, k]) => ends[i]![k].point[along]);
99
+ const placed = justifyPlugs(ideal, start + m, start + len - m, gap, (j) => items[j]![2][along]);
100
+ items.forEach(([i, k], j) => {
101
+ const old = ends[i]![k];
102
+ ends[i]![k] = { point: along === "y" ? { x: old.point.x, y: placed[j]! } : { x: placed[j]!, y: old.point.y }, normal: old.normal };
103
+ });
104
+ }
105
+ }
@@ -0,0 +1,145 @@
1
+ import { dist, type Point } from "../geom.ts";
2
+
3
+ /**
4
+ * This function gives the Gilbert split point of a trunk with `targets.length` edges (EDA-030). The point makes the flow cost least:
5
+ * `len(start, s) * k^alpha + Σ_j len(s, t_j)`, with `k` the number of targets. Weighted Weiszfeld steps find it.
6
+ * The distance from `start` stays from `lo` to `hi` times the distance to the center of the targets.
7
+ * It is also at most half of the distance to the nearest target.
8
+ * The point moves back toward `start` out of the boxes, in steps of `step`.
9
+ */
10
+ export function gilbertPoint(
11
+ start: Point,
12
+ targets: readonly Point[],
13
+ alpha: number,
14
+ free: (p: Point) => boolean,
15
+ step: number,
16
+ lo = 0.2,
17
+ hi = 0.8,
18
+ ): Point {
19
+ if (targets.length === 0) return start;
20
+ const k = targets.length;
21
+ const c = { x: targets.reduce((s, p) => s + p.x, 0) / k, y: targets.reduce((s, p) => s + p.y, 0) / k };
22
+ const L = dist(start, c);
23
+ if (L === 0) return start;
24
+ const w0 = k ** Math.min(1, Math.max(0, alpha));
25
+ let s = { x: start.x + (c.x - start.x) * 0.5, y: start.y + (c.y - start.y) * 0.5 };
26
+ for (let it = 0; it < 32; it++) {
27
+ let sx = 0;
28
+ let sy = 0;
29
+ let sw = 0;
30
+ const pull = (p: Point, w: number): void => {
31
+ const d = Math.max(1e-6, dist(s, p));
32
+ sx += (w * p.x) / d;
33
+ sy += (w * p.y) / d;
34
+ sw += w / d;
35
+ };
36
+ pull(start, w0);
37
+ for (const t of targets) pull(t, 1);
38
+ const n = { x: sx / sw, y: sy / sw };
39
+ const moved = dist(n, s);
40
+ s = n;
41
+ if (moved < 1e-3) break;
42
+ }
43
+ // The clamp keeps the split from `lo` to `hi` of the way, and at most half of the way to the nearest target.
44
+ const r = dist(start, s);
45
+ const most = Math.min(hi * L, 0.5 * Math.min(...targets.map((p) => dist(start, p))));
46
+ const want = Math.min(most, Math.max(lo * L, r));
47
+ const dir = r > 1e-9 ? { x: (s.x - start.x) / r, y: (s.y - start.y) / r } : { x: (c.x - start.x) / L, y: (c.y - start.y) / L };
48
+ const at = (t: number): Point => ({ x: start.x + dir.x * t, y: start.y + dir.y * t });
49
+ let d = want;
50
+ while (d > 0 && !free(at(d))) d -= step;
51
+ return d > 0 ? at(d) : start;
52
+ }
53
+
54
+ /** One point of a balancing-factor tree. */
55
+ export interface FanPoint {
56
+ readonly point: Point;
57
+ /** The index of the point before, toward the root. The root has -1. */
58
+ parent: number;
59
+ /** The length of the tree from the root to this point. */
60
+ pl: number;
61
+ /** A new branch can join the tree at this point, and on the link from its parent. A plug of a target is not open. */
62
+ readonly open: boolean;
63
+ }
64
+
65
+ /** The result of `balancedFan`: the points of the tree, and the index of the last point of the route of each target. */
66
+ export interface Fan {
67
+ readonly points: readonly FanPoint[];
68
+ readonly leaf: readonly number[];
69
+ }
70
+
71
+ /**
72
+ * This function grows a balancing-factor tree from `root` to the targets (NAT-017, after Cuntz and others, 2010).
73
+ * At each step, the target `t` joins the place `v` of the tree with the least `|v - goal(t)| + bf * PL(v)`.
74
+ * `PL(v)` is the length of the tree from the root to `v`.
75
+ * A place is a point of the tree, or the foot of `goal(t)` on a link of the tree. `bf` 0 gives the least ink, and a large `bf` gives direct routes.
76
+ * `branch(v, t)` gives the way from `v` to the plug of the target `t`: its first point is `v`, and its last point is the plug.
77
+ */
78
+ export function balancedFan(root: Point, goals: readonly Point[], bf: number, branch: (from: Point, t: number) => Point[]): Fan {
79
+ const points: FanPoint[] = [{ point: root, parent: -1, pl: 0, open: true }];
80
+ const leaf: number[] = goals.map(() => -1);
81
+ const left = new Set(goals.map((_, i) => i));
82
+ while (left.size > 0) {
83
+ let best = Infinity;
84
+ let pick = -1;
85
+ let at = -1;
86
+ let foot: { point: Point; pl: number } | undefined;
87
+ for (const t of left) {
88
+ const g = goals[t]!;
89
+ for (let v = 0; v < points.length; v++) {
90
+ const p = points[v]!;
91
+ if (!p.open) continue;
92
+ const c = dist(p.point, g) + bf * p.pl;
93
+ if (c < best - 1e-9) {
94
+ best = c;
95
+ pick = t;
96
+ at = v;
97
+ foot = undefined;
98
+ }
99
+ // The foot of the goal on the link from the parent of v to v.
100
+ if (p.parent < 0) continue;
101
+ const a = points[p.parent]!.point;
102
+ const b = p.point;
103
+ const L = dist(a, b);
104
+ if (L <= 1e-9) continue;
105
+ const u = ((g.x - a.x) * (b.x - a.x) + (g.y - a.y) * (b.y - a.y)) / (L * L);
106
+ if (u <= 1e-6 || u >= 1 - 1e-6) continue;
107
+ const f = { x: a.x + u * (b.x - a.x), y: a.y + u * (b.y - a.y) };
108
+ const pl = points[p.parent]!.pl + u * L;
109
+ const cf = dist(f, g) + bf * pl;
110
+ if (cf < best - 1e-9) {
111
+ best = cf;
112
+ pick = t;
113
+ at = v;
114
+ foot = { point: f, pl };
115
+ }
116
+ }
117
+ }
118
+ left.delete(pick);
119
+ let from = at;
120
+ if (foot) {
121
+ // The foot splits the link from the parent of `at` to `at`.
122
+ points.push({ point: foot.point, parent: points[at]!.parent, pl: foot.pl, open: true });
123
+ from = points.length - 1;
124
+ points[at]!.parent = from;
125
+ }
126
+ const way = branch(points[from]!.point, pick);
127
+ let prev = from;
128
+ for (let i = 1; i < way.length; i++) {
129
+ const q = way[i]!;
130
+ const base = points[prev]!;
131
+ if (dist(q, base.point) <= 1e-9) continue;
132
+ points.push({ point: q, parent: prev, pl: base.pl + dist(base.point, q), open: i < way.length - 1 });
133
+ prev = points.length - 1;
134
+ }
135
+ leaf[pick] = prev;
136
+ }
137
+ return { points, leaf };
138
+ }
139
+
140
+ /** This function gives the points of the tree from the root to the point `i`. */
141
+ export function fanPath(fan: Fan, i: number): Point[] {
142
+ const out: Point[] = [];
143
+ for (let j = i; j >= 0; j = fan.points[j]!.parent) out.push(fan.points[j]!.point);
144
+ return out.reverse();
145
+ }
@@ -9,9 +9,9 @@ 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 { sharedRouter, type SharedOptions } from "./shared.ts";
12
+ export { sharedRouter, type ShareCost, type SharedOptions, type ShareMerge, type ShareOrder } from "./shared.ts";
13
13
  export { sectionsRouter, type SectionsOptions } from "./sections.ts";
14
- export { byCollection, groupEdges, laneOrder, splitPoint, trunksRouter, type TrunksOptions } from "./trunks.ts";
14
+ export { byCollection, groupEdges, laneOrder, splitPoint, trunksRouter, type TrunkFan, type TrunkSplit, type TrunksOptions } from "./trunks.ts";
15
15
 
16
16
  /** The built-in routers. The registry `STRATEGIES` holds the same routers as strategies, and the benchmark races the routers of the registry. */
17
17
  export const ROUTERS: readonly Router[] = [
@@ -1,8 +1,19 @@
1
1
  import { center, dist, type Point } from "../geom.ts";
2
2
  import { nodeIndex, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
3
3
  import { placePlug, type Plug } from "../plugs.ts";
4
+ import { buildSections, type Section } from "../sections.ts";
4
5
  import { orderLanes, type RunOrder } from "../segments.ts";
5
6
  import { WayFinder, type WayOptions } from "../ways.ts";
7
+ import { snellMerges } from "./snell.ts";
8
+
9
+ /** The cost of a link that edges of one key share: a fixed discount, or the flow cost `len * f^alpha` (EDA-029). */
10
+ export type ShareCost = "fixed" | "flow";
11
+
12
+ /** The order of the edges of one key: the longest first, the shortest first, the most load first, or the order of the input (NAT-024). */
13
+ export type ShareOrder = "span" | "shortest" | "load" | "input";
14
+
15
+ /** The place where a route joins or leaves a run: at a corner of the graph, or at the Snell angle (NAT-035). */
16
+ export type ShareMerge = "corner" | "snell";
6
17
 
7
18
  /** The options of the router with shared ways. */
8
19
  export interface SharedOptions extends WayOptions {
@@ -14,11 +25,31 @@ export interface SharedOptions extends WayOptions {
14
25
  readonly discount?: number;
15
26
  /** The lane order of the runs. With `bundle`, the default, two edges keep one order on each run of their common stretch. With `run`, each run gets its order alone. */
16
27
  readonly laneOrder?: RunOrder;
28
+ /**
29
+ * The cost of a shared link. With `fixed`, the default, a link that the key uses already costs `discount` times its length.
30
+ * With `flow`, one more edge on a link with `f` edges costs `len * ((f + 1)^alpha - f^alpha)` (EDA-029).
31
+ */
32
+ readonly cost?: ShareCost;
33
+ /** The exponent of the flow cost, from 0.1 to 1. A smaller value gives more sharing. The default is 0.6. Only the cost `flow` uses it. */
34
+ readonly alpha?: number;
35
+ /** The number of passes. In each pass after the first, each edge leaves its links and finds its way again with the others fixed (HWY-039). The default is 1. */
36
+ readonly passes?: number;
37
+ /**
38
+ * The order of the edges of one key. With `span`, the default, the longest edge goes first. With `shortest`, the shortest edge goes first.
39
+ * With `load`, the edge with the most edges between its two sections goes first. With `input`, the edges keep the order of the graph (NAT-024).
40
+ */
41
+ readonly order?: ShareOrder;
42
+ /**
43
+ * The place where a route joins or leaves a run. With `corner`, the default, it is a corner of the graph.
44
+ * With `snell`, the route meets the run at the angle `acos(d)`, with `d` the cost share of the run for the route (NAT-035).
45
+ */
46
+ readonly merge?: ShareMerge;
17
47
  /** The benchmark skips a fixture with more nodes. */
18
48
  readonly maxNodes?: number;
19
49
  }
20
50
 
21
- interface Way {
51
+ /** A way of one edge through the corner graph. */
52
+ export interface Way {
22
53
  readonly edge: Edge;
23
54
  readonly key: string;
24
55
  readonly ps: Plug;
@@ -58,6 +89,11 @@ function turn(from: Point, base: Point, to: Point): number {
58
89
  */
59
90
  export function sharedRouter(options: SharedOptions): Router {
60
91
  const discount = Math.min(1, Math.max(0.05, options.discount ?? 0.5));
92
+ const flow = options.cost === "flow";
93
+ const alpha = Math.min(1, Math.max(0.1, options.alpha ?? 0.6));
94
+ // The cost share of a link for one more edge, when `f` edges of the key use it already. The share falls when `f` grows.
95
+ const share = (f: number): number => (f <= 0 ? 1 : flow ? (f + 1) ** alpha - f ** alpha : discount);
96
+ const passes = Math.max(1, Math.floor(options.passes ?? 1));
61
97
  const keyOf = (e: Edge): string => {
62
98
  if (!options.rule) return "*";
63
99
  return options.rule(e) ?? `\u0000${e.id}`;
@@ -90,17 +126,21 @@ export function sharedRouter(options: SharedOptions): Router {
90
126
  all.push({ edge: e, key: keyOf(e), ps, pt, seq: [], ids: [], found: false });
91
127
  }
92
128
 
93
- // The edges of one key go one after the other, the longest first, so that the short edges join the long ways.
129
+ // The edges of one key go one after the other, by default the longest first, so that the short edges join the long ways.
94
130
  const discounted = new Map<string, Map<number, string[]>>();
95
- const order = [...all].sort(
96
- (p, q) => byText(p.key, q.key) || dist(q.ps.point, q.pt.point) - dist(p.ps.point, p.pt.point) || byText(p.edge.id, q.edge.id),
97
- );
98
- for (const w of order) {
131
+ const order = orderWays(all, options.order ?? "span", graph);
132
+ // The largest number of edges on one link of each key. The cost share of that number is the floor of the search.
133
+ const top = new Map<string, number>();
134
+ const route = (w: Way): void => {
99
135
  let used = discounted.get(w.key);
100
136
  if (!used) discounted.set(w.key, (used = new Map()));
101
137
  const mine = used;
102
- const way = corners.path(ways.out(w.ps), ways.out(w.pt), (i, j, d) => (mine.has(link(i, j)) ? d * discount : d), discount);
103
- if (!way) continue;
138
+ const floor = flow ? share(top.get(w.key) ?? 0) : discount;
139
+ const cost = flow
140
+ ? (i: number, j: number, d: number): number => d * share(mine.get(link(i, j))?.length ?? 0)
141
+ : (i: number, j: number, d: number): number => (mine.has(link(i, j)) ? d * discount : d);
142
+ const way = corners.path(ways.out(w.ps), ways.out(w.pt), cost, floor);
143
+ if (!way) return;
104
144
  w.found = true;
105
145
  w.seq = way;
106
146
  w.ids = way.map((p, i) => (i === 0 || i === way.length - 1 ? endId(p) : index.get(p)!));
@@ -111,7 +151,37 @@ export function sharedRouter(options: SharedOptions): Router {
111
151
  const list = mine.get(link(a, b));
112
152
  if (list) list.push(w.edge.id);
113
153
  else mine.set(link(a, b), [w.edge.id]);
154
+ if (flow) top.set(w.key, Math.max(top.get(w.key) ?? 0, mine.get(link(a, b))!.length));
114
155
  }
156
+ };
157
+ for (const w of order) route(w);
158
+ // Each later pass takes each edge off its links and finds its way again, with the ways of the other edges fixed.
159
+ for (let pass = 1; pass < passes; pass++) {
160
+ for (const w of order) {
161
+ if (!w.found) continue;
162
+ const mine = discounted.get(w.key)!;
163
+ for (let i = 1; i < w.ids.length; i++) {
164
+ const a = w.ids[i - 1]!;
165
+ const b = w.ids[i]!;
166
+ if (a >= corners.points.length || b >= corners.points.length) continue;
167
+ const k = link(a, b);
168
+ const list = mine.get(k);
169
+ if (!list) continue;
170
+ const at = list.indexOf(w.edge.id);
171
+ if (at >= 0) list.splice(at, 1);
172
+ if (list.length === 0) mine.delete(k);
173
+ }
174
+ w.found = false;
175
+ w.seq = [];
176
+ w.ids = [];
177
+ route(w);
178
+ }
179
+ }
180
+
181
+ // With `merge: snell`, each route that joins or leaves a run moves its merge point along the run to the Snell angle.
182
+ if (options.merge === "snell") {
183
+ const pad = ways.margin / 2;
184
+ snellMerges(order, corners.points.length + ends.size, (a, b) => ways.obstacles.clear(a, b, pad), (f) => Math.min(0.95, share(f)));
115
185
  }
116
186
 
117
187
  // The users of each link, with the links to the ends. Two edges from one plug share the link from it.
@@ -185,6 +255,35 @@ export function sharedRouter(options: SharedOptions): Router {
185
255
  return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
186
256
  }
187
257
 
258
+ /**
259
+ * This function gives the order of the ways for the router `shared`. The ways of one key stay together, in the order of the keys.
260
+ * In a key, the order is `order`, and the edge id breaks each tie. The order `load` counts the edges of the key between the same two sections.
261
+ */
262
+ function orderWays(all: readonly Way[], order: ShareOrder, graph: Graph): Way[] {
263
+ const span = (w: Way): number => dist(w.ps.point, w.pt.point);
264
+ if (order === "input") {
265
+ const at = new Map(all.map((w, i) => [w, i]));
266
+ return [...all].sort((p, q) => byText(p.key, q.key) || at.get(p)! - at.get(q)!);
267
+ }
268
+ if (order === "shortest") return [...all].sort((p, q) => byText(p.key, q.key) || span(p) - span(q) || byText(p.edge.id, q.edge.id));
269
+ if (order === "span") return [...all].sort((p, q) => byText(p.key, q.key) || span(q) - span(p) || byText(p.edge.id, q.edge.id));
270
+ // The leaf section of each node, then the number of edges of each key between each pair of leaves.
271
+ const leafOf = new Map<string, string>();
272
+ const walk = (s: Section): void => {
273
+ if (s.children.length === 0) for (const i of s.nodes) leafOf.set(graph.nodes[i]!.id, s.id);
274
+ for (const c of s.children) walk(c);
275
+ };
276
+ walk(buildSections(graph.nodes));
277
+ const pair = (w: Way): string => {
278
+ const a = leafOf.get(w.edge.source.node) ?? "";
279
+ const b = leafOf.get(w.edge.target.node) ?? "";
280
+ return `${w.key}\u0000${a < b ? a : b}\u0000${a < b ? b : a}`;
281
+ };
282
+ const load = new Map<string, number>();
283
+ for (const w of all) load.set(pair(w), (load.get(pair(w)) ?? 0) + 1);
284
+ return [...all].sort((p, q) => byText(p.key, q.key) || load.get(pair(q))! - load.get(pair(p))! || span(q) - span(p) || byText(p.edge.id, q.edge.id));
285
+ }
286
+
188
287
  /**
189
288
  * This function gives the lane order of a run with the option `laneOrder: "run"` (metro-line order). Two edges of the run go together past its last corner
190
289
  * until they part. The edge that turns more to the left there gets the lane more to the left.