@mark1russell7/highways 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.
@@ -0,0 +1,140 @@
1
+ import { dist, type Point } from "../geom.ts";
2
+ import type { Way } from "./shared.ts";
3
+
4
+ /**
5
+ * This function gives the Snell merge point of a route (NAT-035).
6
+ * The route comes from `from`, reaches a run at `at` and goes on along the run to `next`.
7
+ * A unit of length on the run costs `d` against 1 off it. The cheapest route meets the run at the angle `acos(d)`.
8
+ * The function gives the distance from `at` toward `next` of that merge point. A value of 0 or less tells that the turn at `at` is already at most `acos(d)`.
9
+ */
10
+ export function snellOffset(from: Point, at: Point, next: Point, d: number): number {
11
+ const len = dist(at, next);
12
+ if (len <= 0) return 0;
13
+ const ux = (next.x - at.x) / len;
14
+ const uy = (next.y - at.y) / len;
15
+ const vx = from.x - at.x;
16
+ const vy = from.y - at.y;
17
+ // The foot of `from` on the line of the run is at `along` from `at`, and `from` is `h` from the line.
18
+ const along = vx * ux + vy * uy;
19
+ const h = Math.abs(vx * uy - vy * ux);
20
+ const c = Math.min(0.999, Math.max(0, d));
21
+ return along + (h * c) / Math.sqrt(1 - c * c);
22
+ }
23
+
24
+ /** The turn, in radians from 0 to π, at `b` on the way from `a` through `b` to `c`. */
25
+ function turnAt(a: Point, b: Point, c: Point): number {
26
+ const ux = b.x - a.x;
27
+ const uy = b.y - a.y;
28
+ const vx = c.x - b.x;
29
+ const vy = c.y - b.y;
30
+ return Math.abs(Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy));
31
+ }
32
+
33
+ /**
34
+ * This function moves the merge points of the ways of the router `shared` to the Snell angle (NAT-035). It changes the ways in place.
35
+ *
36
+ * A way joins a run at a corner `J` when it comes alone from the corner before.
37
+ * The link from `J` to the next corner has other edges of its key.
38
+ * When the way turns at `J` by more than `acos(d)`, it goes directly to a new point `J*` on that link.
39
+ * At `J*`, the way meets the link at the angle `acos(d)`.
40
+ * Each way on the link gets `J*` as a new corner, so the run keeps the same points in each route. Then the same rule moves the exits, on the reversed ways.
41
+ *
42
+ * `firstId` is the first free id of a corner. `clear` tells if a line keeps out of the boxes. `delta` gives the cost share `d` of a link with `f` other edges.
43
+ */
44
+ export function snellMerges(ways: readonly Way[], firstId: number, clear: (a: Point, b: Point) => boolean, delta: (f: number) => number): void {
45
+ let next = firstId;
46
+ const keys = new Map<string, Way[]>();
47
+ for (const w of ways) {
48
+ if (!w.found) continue;
49
+ const list = keys.get(w.key);
50
+ if (list) list.push(w);
51
+ else keys.set(w.key, [w]);
52
+ }
53
+ for (const group of keys.values()) {
54
+ if (group.length < 2) continue;
55
+ const linkOf = (a: number, b: number): string => (a < b ? `${a}:${b}` : `${b}:${a}`);
56
+ const on = new Map<string, Set<Way>>();
57
+ const add = (k: string, w: Way): void => {
58
+ const set = on.get(k);
59
+ if (set) set.add(w);
60
+ else on.set(k, new Set([w]));
61
+ };
62
+ const drop = (k: string, w: Way): void => {
63
+ const set = on.get(k);
64
+ if (!set) return;
65
+ set.delete(w);
66
+ if (set.size === 0) on.delete(k);
67
+ };
68
+ for (const w of group) for (let i = 1; i < w.ids.length; i++) add(linkOf(w.ids[i - 1]!, w.ids[i]!), w);
69
+ // First the merges, then the exits: an exit is a merge of the reversed way.
70
+ for (const back of [false, true]) {
71
+ for (const w of group) {
72
+ if (back) {
73
+ w.seq.reverse();
74
+ w.ids.reverse();
75
+ }
76
+ for (let i = 1; i < w.ids.length - 1; i++) {
77
+ const before = on.get(linkOf(w.ids[i - 1]!, w.ids[i]!));
78
+ const after = on.get(linkOf(w.ids[i]!, w.ids[i + 1]!));
79
+ if (!before || !after || before.size !== 1 || after.size < 2) continue;
80
+ const P = w.seq[i - 1]!;
81
+ const J = w.seq[i]!;
82
+ const N = w.seq[i + 1]!;
83
+ const d = delta(after.size - 1);
84
+ const turn = turnAt(P, J, N);
85
+ if (turn <= Math.acos(d) + 1e-3) continue;
86
+ const len = dist(J, N);
87
+ let t = Math.min(len, snellOffset(P, J, N, d));
88
+ // A line from P to the merge point that goes through a box moves the point back toward J, at most three times.
89
+ const pointAt = (s: number): Point => ({ x: J.x + ((N.x - J.x) * s) / len, y: J.y + ((N.y - J.y) * s) / len });
90
+ let tries = 0;
91
+ while (t >= 1 && !clear(P, pointAt(t)) && tries < 3) {
92
+ t /= 2;
93
+ tries++;
94
+ }
95
+ if (t < 1 || !clear(P, pointAt(t))) continue;
96
+ const a = w.ids[i - 1]!;
97
+ const j = w.ids[i]!;
98
+ const n = w.ids[i + 1]!;
99
+ if (t >= len - 1e-6) {
100
+ // The merge point is the next corner: the way skips J.
101
+ drop(linkOf(a, j), w);
102
+ drop(linkOf(j, n), w);
103
+ w.seq.splice(i, 1);
104
+ w.ids.splice(i, 1);
105
+ add(linkOf(a, n), w);
106
+ continue;
107
+ }
108
+ const star = pointAt(t);
109
+ const id = next++;
110
+ const users = [...after];
111
+ on.delete(linkOf(j, n));
112
+ // Each way on the link from J to N gets the new corner between them.
113
+ for (const o of users) {
114
+ for (let k = 0; k < o.ids.length - 1; k++) {
115
+ const x = o.ids[k]!;
116
+ const y = o.ids[k + 1]!;
117
+ if ((x === j && y === n) || (x === n && y === j)) {
118
+ o.seq.splice(k + 1, 0, star);
119
+ o.ids.splice(k + 1, 0, id);
120
+ break;
121
+ }
122
+ }
123
+ add(linkOf(j, id), o);
124
+ add(linkOf(id, n), o);
125
+ }
126
+ // The way itself goes from P directly to the new corner.
127
+ drop(linkOf(a, j), w);
128
+ drop(linkOf(j, id), w);
129
+ w.seq.splice(i, 1);
130
+ w.ids.splice(i, 1);
131
+ add(linkOf(a, id), w);
132
+ }
133
+ if (back) {
134
+ w.seq.reverse();
135
+ w.ids.reverse();
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
@@ -3,6 +3,7 @@ import { nodeIndex, type Bundle, type Edge, type Graph, type GroupRule, type Nod
3
3
  import { placePlug, type Plug } from "../plugs.ts";
4
4
  import { orderLanes, type RunOrder } from "../segments.ts";
5
5
  import { WayFinder, type WayOptions } from "../ways.ts";
6
+ import { balancedFan, fanPath, gilbertPoint } from "./fan.ts";
6
7
 
7
8
  /** The options of the trunk router. */
8
9
  export interface TrunksOptions extends WayOptions {
@@ -21,6 +22,20 @@ export interface TrunksOptions extends WayOptions {
21
22
  * A lane that turns back over the trunk leaves at an earlier point of the trunk. With `split`, each lane leaves at the split point, as in version 0.2.0.
22
23
  */
23
24
  readonly leave?: TrunkLeave;
25
+ /**
26
+ * The place of the split point. With `share`, the default, it is at `split` of the way to the center of the targets.
27
+ * With `gilbert`, it is where the flow cost of the trunk and its branches is least, with the exponent `alpha` (EDA-030).
28
+ */
29
+ readonly splitAt?: TrunkSplit;
30
+ /** The exponent of the flow cost of the split `gilbert`, from 0 to 1. The default is 0.5. */
31
+ readonly alpha?: number;
32
+ /**
33
+ * The shape of each group. With `split`, the default, the edges share a trunk to a split point.
34
+ * With `tree`, the edges grow a balancing-factor tree from the plug, with the factor `bf` (NAT-017).
35
+ */
36
+ readonly fan?: TrunkFan;
37
+ /** The balancing factor of the fan `tree`, from 0 to 1. 0 gives the least ink, and a larger factor gives more direct routes. The default is 0.4. */
38
+ readonly bf?: number;
24
39
  /** The benchmark skips a fixture with more nodes. */
25
40
  readonly maxNodes?: number;
26
41
  }
@@ -28,6 +43,12 @@ export interface TrunksOptions extends WayOptions {
28
43
  /** The place where the lanes of a trunk leave it: where their ways part, or at the split point. */
29
44
  export type TrunkLeave = "part" | "split";
30
45
 
46
+ /** The place of the split point of a trunk: at a share of the way, or at the Gilbert point of the flow cost (EDA-030). */
47
+ export type TrunkSplit = "share" | "gilbert";
48
+
49
+ /** The shape of a group of the router `trunks`: a trunk and a split point, or a balancing-factor tree (NAT-017). */
50
+ export type TrunkFan = "split" | "tree";
51
+
31
52
  /** The rule that groups the edges by the collection of their target, in `data.collection`. */
32
53
  export const byCollection: GroupRule = (e) => {
33
54
  const c = e.data?.["collection"];
@@ -188,7 +209,28 @@ export function trunksRouter(options: TrunksOptions): Router {
188
209
  const c = { x: centers.reduce((a, p) => a + p.x, 0) / centers.length, y: centers.reduce((a, p) => a + p.y, 0) / centers.length };
189
210
  const plug: Plug = placePlug(s, first.source, c);
190
211
  const start = ways.out(plug);
191
- const split = splitPoint(start, centers, options.split ?? 0.5, (p) => !ways.obstacles.inside(p, ways.margin), ways.margin);
212
+ const key = id.split("\u0000")[2]!;
213
+ if (options.fan === "tree") {
214
+ // Each edge follows the tree from the plug to its target. The routes share the points of the tree.
215
+ const pts = group.map((e, i) => placePlug(targets[i]!, e.target, start));
216
+ const bf = Math.min(1, Math.max(0, options.bf ?? 0.4));
217
+ const fan = balancedFan(start, pts.map((pt) => ways.out(pt)), bf, (from, t) => ways.toPlug(from, pts[t]!) ?? [from, pts[t]!.point]);
218
+ const own: Route[] = group.map((e, i) => ({ edge: e.id, points: dedupe([plug.point, ...fanPath(fan, fan.leaf[i]!)]) }));
219
+ for (const r of own) points.set(r.edge, r.points);
220
+ const order = options.laneOrder ?? "bundle";
221
+ const common = own[0]!.points.slice(0, commonStart(own, 0));
222
+ bundles.push({ id: `b${bundles.length}`, key, node: s.id, edges: orderLanes(common, own, order), trunk: common });
223
+ for (const way of sharedWays(own, common.length)) {
224
+ const users = own.filter((r) => way.edges.includes(r.edge));
225
+ runs.push({ id: `r${runs.length}`, key, points: way.points, edges: orderLanes(way.points, users, order) });
226
+ }
227
+ continue;
228
+ }
229
+ const free = (p: Point): boolean => !ways.obstacles.inside(p, ways.margin);
230
+ const split =
231
+ options.splitAt === "gilbert"
232
+ ? gilbertPoint(start, centers, options.alpha ?? 0.5, free, ways.margin)
233
+ : splitPoint(start, centers, options.split ?? 0.5, free, ways.margin);
192
234
  const way = ways.fromPlug(plug, split);
193
235
  if (!way) continue;
194
236
  const trunk = dedupe(way);
@@ -211,7 +253,6 @@ export function trunksRouter(options: TrunksOptions): Router {
211
253
  const exit = leaveTrunk(trunk, branch, clear, 2 * ways.margin);
212
254
  if (exit) exits.push({ edge: e.id, exit });
213
255
  });
214
- const key = id.split("\u0000")[2]!;
215
256
  if (!part) {
216
257
  for (const r of own) points.set(r.edge, r.points);
217
258
  const edges = options.laneOrder === "run" || trunk.length < 2 ? laneOrder(dir, end, branches) : orderLanes(trunk, own, "bundle");
@@ -7,6 +7,7 @@ import type { SectionsOptions } from "../routers/sections.ts";
7
7
  import type { SharedOptions } from "../routers/shared.ts";
8
8
  import type { TrunksOptions } from "../routers/trunks.ts";
9
9
  import type { SectionOptions } from "../sections.ts";
10
+ import type { WayOptions } from "../ways.ts";
10
11
  import type { RunOrder } from "../segments.ts";
11
12
  import type { ClothoidStrategyOptions, HighwayStrategyOptions, LevelRoomPreset, RadiusOptions, RoomPreset, RouterPreset, SmoothStrategyOptions, TensionOptions } from "./builtins.ts";
12
13
  import type { EnumOption, OptionSchema } from "./options.ts";
@@ -15,7 +16,7 @@ import type { EnumOption, OptionSchema } from "./options.ts";
15
16
  // The test of the defaults runs each strategy with `optionDefaults` and with no option, and compares the output.
16
17
 
17
18
  /** The options of the way finder (`WayOptions`), for each router that goes around the nodes. */
18
- const WAY_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = {
19
+ const WAY_SCHEMA: OptionSchema<WayOptions> = {
19
20
  margin: { type: "number", min: 1, max: 20, step: 1, default: 5, describe: "The distance from a box to the corners of the graph." },
20
21
  k: { type: "integer", min: 2, max: 16, step: 1, default: 8, describe: "The number of visible neighbours of each corner." },
21
22
  smooth: { type: "boolean", default: false, describe: "With `true`, a way in clear view uses the inflection rule, and the other ways get round corners." },
@@ -33,7 +34,23 @@ const LANE_ORDER: EnumOption<RunOrder> = {
33
34
  const RUN_LANE_ORDER: EnumOption<RunOrder> = { ...LANE_ORDER, default: "run" };
34
35
 
35
36
  /** The declarations of the options of the routers `corners` and `corners-smooth`. */
36
- export const CORNERS_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = WAY_SCHEMA;
37
+ export const CORNERS_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = {
38
+ ...WAY_SCHEMA,
39
+ plugs: {
40
+ type: "enum",
41
+ values: ["facing", "side"],
42
+ default: "facing",
43
+ describe: "With `facing`, a free plug faces the other end. With `side`, the search starts from the whole free side, and the plug goes where the way leaves (WILD-001).",
44
+ },
45
+ plugGap: {
46
+ type: "number",
47
+ min: 0,
48
+ max: 24,
49
+ step: 1,
50
+ default: 0,
51
+ describe: "With a gap above 0, the free plugs of a side keep their order and at least this gap (WILD-002). 0 keeps each plug where it is.",
52
+ },
53
+ };
37
54
 
38
55
  /** The declarations of the options of the router `trunks`. */
39
56
  export const TRUNKS_SCHEMA: OptionSchema<RouterPreset<TrunksOptions>> = {
@@ -48,6 +65,20 @@ export const TRUNKS_SCHEMA: OptionSchema<RouterPreset<TrunksOptions>> = {
48
65
  default: "part",
49
66
  describe: "With `part`, a lane leaves the trunk where its way parts from the other ways. With `split`, each lane leaves at the split point.",
50
67
  },
68
+ splitAt: {
69
+ type: "enum",
70
+ values: ["share", "gilbert"],
71
+ default: "share",
72
+ describe: "With `share`, the split point is at `split` of the way. With `gilbert`, it is where the flow cost is least (EDA-030).",
73
+ },
74
+ alpha: { type: "number", min: 0, max: 1, step: 0.05, default: 0.5, describe: "The exponent of the flow cost of the split `gilbert`. A smaller value moves the split toward the targets." },
75
+ fan: {
76
+ type: "enum",
77
+ values: ["split", "tree"],
78
+ default: "split",
79
+ describe: "With `split`, the edges of a group share a trunk to a split point. With `tree`, they grow a balancing-factor tree (NAT-017).",
80
+ },
81
+ bf: { type: "number", min: 0, max: 1, step: 0.05, default: 0.4, describe: "The balancing factor of the fan `tree`. 0 gives the least ink, and a larger factor gives more direct routes." },
51
82
  };
52
83
 
53
84
  /** The declarations of the options of the routers `shared` and `shared-all`. */
@@ -63,6 +94,26 @@ export const SHARED_SCHEMA: OptionSchema<RouterPreset<SharedOptions>> = {
63
94
  describe: "The cost of a link that an edge with the same key uses already, as a share of its length.",
64
95
  },
65
96
  laneOrder: LANE_ORDER,
97
+ cost: {
98
+ type: "enum",
99
+ values: ["fixed", "flow"],
100
+ default: "fixed",
101
+ describe: "With `fixed`, a shared link costs `discount` of its length. With `flow`, a link with `f` edges costs `len * f^alpha` (EDA-029).",
102
+ },
103
+ alpha: { type: "number", min: 0.1, max: 1, step: 0.05, default: 0.6, describe: "The exponent of the cost `flow`. A smaller value gives more sharing." },
104
+ passes: { type: "integer", min: 1, max: 5, step: 1, default: 1, describe: "In each pass after the first, each edge finds its way again with the ways of the other edges fixed (HWY-039)." },
105
+ order: {
106
+ type: "enum",
107
+ values: ["span", "shortest", "load", "input"],
108
+ default: "span",
109
+ describe: "The order of the edges of a key: the longest first, the shortest first, the most edges between two sections first, or the input order (NAT-024).",
110
+ },
111
+ merge: {
112
+ type: "enum",
113
+ values: ["corner", "snell"],
114
+ default: "corner",
115
+ describe: "With `corner`, a route joins a run at a corner of the graph. With `snell`, it joins at the angle `acos(d)` (NAT-035).",
116
+ },
66
117
  };
67
118
 
68
119
  /** The declarations of the options of the highway strategy `corridors` (HWY-031). */