@mark1russell7/highways 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/build/index.d.ts +406 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3048 -443
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/screen/exercise.ts +2 -0
- package/src/bench/screen/set.ts +58 -1
- package/src/families/cases.ts +17 -9
- package/src/families/coverage.ts +502 -0
- package/src/families/index.ts +4 -1
- package/src/highways/bronchial/index.ts +144 -0
- package/src/highways/index.ts +2 -0
- package/src/highways/knee/index.ts +154 -0
- package/src/highways/knee/root.ts +19 -0
- package/src/index.ts +2 -0
- package/src/levels/venation/index.ts +191 -0
- package/src/routers/annealing/index.ts +260 -0
- package/src/routers/frames.ts +80 -16
- package/src/routers/gutter.ts +545 -0
- package/src/routers/index.ts +5 -2
- package/src/routers/shared.ts +40 -2
- package/src/routers/tight/index.ts +122 -0
- package/src/strategies/builtins.ts +57 -2
- package/src/strategies/schemas.ts +54 -0
- package/src/whole/portfolio/index.ts +108 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { rng } from "../../fixtures.ts";
|
|
2
|
+
import { dist, type Point } from "../../geom.ts";
|
|
3
|
+
import type { Graph, GroupRule, Result, Router } from "../../model.ts";
|
|
4
|
+
import { defineStrategy, type Strategy } from "../../strategies/model.ts";
|
|
5
|
+
import type { OptionSchema } from "../../strategies/options.ts";
|
|
6
|
+
import { sharedRouter, type SharedSearch, type Way } from "../shared.ts";
|
|
7
|
+
import { byCollection } from "../trunks.ts";
|
|
8
|
+
|
|
9
|
+
/** The options of the router `annealing` (RULE-021: simulated annealing over candidate routes, with its rider WILD-028). */
|
|
10
|
+
export interface AnnealingOptions {
|
|
11
|
+
/** The number of candidate ways of each edge, from 3 to 8. The default is 4. */
|
|
12
|
+
readonly candidates: number;
|
|
13
|
+
/** The cost of one crossing, in units of the median box side, from 0 to 20. The default is 4. */
|
|
14
|
+
readonly crossing: number;
|
|
15
|
+
/** The start temperature, in units of the median box side, from 0.1 to 10. The default is 1. */
|
|
16
|
+
readonly temperature: number;
|
|
17
|
+
/** The factor of the temperature after each sweep of the edges, from 0.9 to 0.99. The default is 0.95. */
|
|
18
|
+
readonly cooling: number;
|
|
19
|
+
/** The budget: the number of changes for each edge, from 20 to 200. The default is 50. */
|
|
20
|
+
readonly budget: number;
|
|
21
|
+
/** The seed of the random choices, a whole number from 0. The same seed gives the same routes. The default is 1. */
|
|
22
|
+
readonly seed: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The default options of the router `annealing`. */
|
|
26
|
+
export const ANNEALING_DEFAULTS: Readonly<AnnealingOptions> = Object.freeze({ candidates: 4, crossing: 4, temperature: 1, cooling: 0.95, budget: 50, seed: 1 });
|
|
27
|
+
|
|
28
|
+
/** The declarations of the options of the router `annealing` (RULE-021). */
|
|
29
|
+
export const ANNEALING_SCHEMA: OptionSchema<AnnealingOptions> = {
|
|
30
|
+
candidates: { type: "integer", min: 3, max: 8, step: 1, marks: [3, 4, 6], default: 4, describe: "The number of candidate ways of each edge." },
|
|
31
|
+
crossing: { type: "number", min: 0, max: 20, step: 0.5, marks: [1, 4, 10], default: 4, describe: "The cost of one crossing, in units of the median box side." },
|
|
32
|
+
temperature: { type: "number", min: 0.1, max: 10, step: 0.1, marks: [0.5, 1, 4], default: 1, describe: "The start temperature, in units of the median box side." },
|
|
33
|
+
cooling: { type: "number", min: 0.9, max: 0.99, step: 0.01, marks: [0.9, 0.95, 0.98], default: 0.95, describe: "The factor of the temperature after each sweep of the edges." },
|
|
34
|
+
budget: { type: "integer", min: 20, max: 200, step: 10, marks: [20, 50, 100], default: 50, describe: "The budget: the number of changes for each edge." },
|
|
35
|
+
seed: { type: "integer", min: 0, max: 1_000_000, step: 1, marks: [1, 2, 3], default: 1, describe: "The seed of the random choices. The same seed gives the same routes." },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** The share of the cost of a link that the edges of one key use already, as in `shared` with its default discount. */
|
|
39
|
+
const DISCOUNT = 0.5;
|
|
40
|
+
/** The cost factor of the links of the earlier candidates in the search of the next candidate. */
|
|
41
|
+
const PENALTY = 3;
|
|
42
|
+
|
|
43
|
+
/** One candidate way of an edge: its points, the indices of its points and the keys of its links. */
|
|
44
|
+
interface Candidate {
|
|
45
|
+
readonly seq: Point[];
|
|
46
|
+
readonly ids: number[];
|
|
47
|
+
readonly links: number[];
|
|
48
|
+
readonly lengths: number[];
|
|
49
|
+
readonly length: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** This function tells if the segments `a b` and `c d` cross at one inner point of each. A touch at an end or a common line is not a crossing. */
|
|
53
|
+
function properCross(a: Point, b: Point, c: Point, d: Point): boolean {
|
|
54
|
+
const o = (p: Point, q: Point, r: Point): number => (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x);
|
|
55
|
+
const d1 = o(c, d, a);
|
|
56
|
+
const d2 = o(c, d, b);
|
|
57
|
+
const d3 = o(a, b, c);
|
|
58
|
+
const d4 = o(a, b, d);
|
|
59
|
+
return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** This function gives the median of the smaller side of the boxes, the unit of the costs. */
|
|
63
|
+
function unitOf(graph: Graph): number {
|
|
64
|
+
const sides = graph.nodes.map((n) => Math.min(n.box.w, n.box.h)).sort((a, b) => a - b);
|
|
65
|
+
const u = sides.length === 0 ? 1 : sides[Math.floor((sides.length - 1) / 2)]!;
|
|
66
|
+
return u > 0 ? u : 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* This function anneals the choice of one candidate way for each edge (RULE-021, after Davidson and Harel, 1996).
|
|
71
|
+
* The candidates of an edge are its way in `shared` and its shortest way. Other candidates are the shortest ways when the links of earlier candidates cost 3 times more.
|
|
72
|
+
* The energy is `(1 - 0.5) ink + 0.5 Σ L + crossing u X`: the cost of `shared` plus a cost for each crossing `X` between the ways of two edges.
|
|
73
|
+
* A change takes one edge to another candidate. A worse change stays with the rational chance `1 / (1 + y + y^2 / 2)`, with `y = ΔE / T`.
|
|
74
|
+
* The temperature falls by `cooling` after each sweep. The ways at the end are the ways of the lowest energy of the run.
|
|
75
|
+
*/
|
|
76
|
+
export function annealWays(ways: readonly Way[], search: SharedSearch, o: Readonly<AnnealingOptions>): void {
|
|
77
|
+
const live = ways.filter((w) => w.found);
|
|
78
|
+
const m = live.length;
|
|
79
|
+
if (m < 2) return;
|
|
80
|
+
const unit = unitOf(search.graph);
|
|
81
|
+
const n = search.corners;
|
|
82
|
+
let maxId = 0;
|
|
83
|
+
for (const w of live) for (const i of w.ids) maxId = Math.max(maxId, i);
|
|
84
|
+
const N = maxId + 1;
|
|
85
|
+
const pairOf = (a: number, b: number): number => (a < b ? a * N + b : b * N + a);
|
|
86
|
+
const make = (seq: Point[], ids: number[]): Candidate => {
|
|
87
|
+
const links: number[] = [];
|
|
88
|
+
const lengths: number[] = [];
|
|
89
|
+
let length = 0;
|
|
90
|
+
for (let i = 1; i < ids.length; i++) {
|
|
91
|
+
const d = dist(seq[i - 1]!, seq[i]!);
|
|
92
|
+
links.push(pairOf(ids[i - 1]!, ids[i]!));
|
|
93
|
+
lengths.push(d);
|
|
94
|
+
length += d;
|
|
95
|
+
}
|
|
96
|
+
return { seq, ids, links, lengths, length };
|
|
97
|
+
};
|
|
98
|
+
const k = Math.min(8, Math.max(1, Math.floor(o.candidates)));
|
|
99
|
+
// The candidates of each edge. The first is the way of `shared`, the start state.
|
|
100
|
+
const cands: Candidate[][] = live.map((w) => {
|
|
101
|
+
const list = [make(w.seq, w.ids)];
|
|
102
|
+
const seen = new Set([w.ids.join(",")]);
|
|
103
|
+
const hit = new Set<number>();
|
|
104
|
+
const mark = (c: Candidate): void => {
|
|
105
|
+
for (let i = 1; i < c.ids.length; i++) if (c.ids[i - 1]! < n && c.ids[i]! < n) hit.add(search.link(c.ids[i - 1]!, c.ids[i]!));
|
|
106
|
+
};
|
|
107
|
+
const add = (found: { seq: Point[]; ids: number[] } | undefined): boolean => {
|
|
108
|
+
if (!found) return false;
|
|
109
|
+
const key = found.ids.join(",");
|
|
110
|
+
if (seen.has(key)) return false;
|
|
111
|
+
seen.add(key);
|
|
112
|
+
const c = make(found.seq, found.ids);
|
|
113
|
+
list.push(c);
|
|
114
|
+
mark(c);
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
mark(list[0]!);
|
|
118
|
+
if (list.length < k) add(search.find(w));
|
|
119
|
+
for (let tries = 0; list.length < k && tries < k; tries++) {
|
|
120
|
+
if (!add(search.find(w, (i, j, d) => (hit.has(search.link(i, j)) ? d * PENALTY : d), 1))) break;
|
|
121
|
+
}
|
|
122
|
+
return list;
|
|
123
|
+
});
|
|
124
|
+
// The crossings of each pair of candidates of two edges, in a table for each pair of edges whose boxes meet.
|
|
125
|
+
type Bounds = { x0: number; y0: number; x1: number; y1: number };
|
|
126
|
+
const boundsOf = (seqs: readonly Point[][]): Bounds => {
|
|
127
|
+
let x0 = Infinity;
|
|
128
|
+
let y0 = Infinity;
|
|
129
|
+
let x1 = -Infinity;
|
|
130
|
+
let y1 = -Infinity;
|
|
131
|
+
for (const seq of seqs)
|
|
132
|
+
for (const p of seq) {
|
|
133
|
+
x0 = Math.min(x0, p.x);
|
|
134
|
+
y0 = Math.min(y0, p.y);
|
|
135
|
+
x1 = Math.max(x1, p.x);
|
|
136
|
+
y1 = Math.max(y1, p.y);
|
|
137
|
+
}
|
|
138
|
+
return { x0, y0, x1, y1 };
|
|
139
|
+
};
|
|
140
|
+
const apart = (A: Bounds, B: Bounds): boolean => A.x1 < B.x0 || B.x1 < A.x0 || A.y1 < B.y0 || B.y1 < A.y0;
|
|
141
|
+
const own = cands.map((list) => list.map((c) => boundsOf([c.seq])));
|
|
142
|
+
const boxes = cands.map((list) => boundsOf(list.map((c) => c.seq)));
|
|
143
|
+
const near: { f: number; table: Int32Array; mine: boolean }[][] = live.map(() => []);
|
|
144
|
+
for (let e = 0; e < m; e++) {
|
|
145
|
+
for (let f = e + 1; f < m; f++) {
|
|
146
|
+
if (apart(boxes[e]!, boxes[f]!)) continue;
|
|
147
|
+
const ke = cands[e]!.length;
|
|
148
|
+
const kf = cands[f]!.length;
|
|
149
|
+
const table = new Int32Array(ke * kf);
|
|
150
|
+
let any = false;
|
|
151
|
+
for (let i = 0; i < ke; i++) {
|
|
152
|
+
const p = cands[e]![i]!.seq;
|
|
153
|
+
for (let j = 0; j < kf; j++) {
|
|
154
|
+
if (apart(own[e]![i]!, own[f]![j]!)) continue;
|
|
155
|
+
const q = cands[f]![j]!.seq;
|
|
156
|
+
let x = 0;
|
|
157
|
+
for (let s = 1; s < p.length; s++) for (let t = 1; t < q.length; t++) if (properCross(p[s - 1]!, p[s]!, q[t - 1]!, q[t]!)) x++;
|
|
158
|
+
table[i * kf + j] = x;
|
|
159
|
+
if (x > 0) any = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!any) continue;
|
|
163
|
+
near[e]!.push({ f, table, mine: true });
|
|
164
|
+
near[f]!.push({ f: e, table, mine: false });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// The users of each link in each key, for the ink.
|
|
168
|
+
const keys = live.map((w) => w.key);
|
|
169
|
+
const users = new Map<string, Map<number, number>>();
|
|
170
|
+
const use = (e: number, c: Candidate, step: number): number => {
|
|
171
|
+
let mine = users.get(keys[e]!);
|
|
172
|
+
if (!mine) users.set(keys[e]!, (mine = new Map()));
|
|
173
|
+
let ink = 0;
|
|
174
|
+
for (let i = 0; i < c.links.length; i++) {
|
|
175
|
+
const before = mine.get(c.links[i]!) ?? 0;
|
|
176
|
+
const after = before + step;
|
|
177
|
+
if (after === 0) mine.delete(c.links[i]!);
|
|
178
|
+
else mine.set(c.links[i]!, after);
|
|
179
|
+
if (before === 0 && after > 0) ink += c.lengths[i]!;
|
|
180
|
+
if (before > 0 && after === 0) ink -= c.lengths[i]!;
|
|
181
|
+
}
|
|
182
|
+
return ink;
|
|
183
|
+
};
|
|
184
|
+
const choice = new Int32Array(m);
|
|
185
|
+
for (let e = 0; e < m; e++) use(e, cands[e]![0]!, 1);
|
|
186
|
+
const crossCost = Math.max(0, o.crossing) * unit;
|
|
187
|
+
const crossings = (e: number, i: number): number => {
|
|
188
|
+
let x = 0;
|
|
189
|
+
for (const { f, table, mine } of near[e]!) {
|
|
190
|
+
const kf = cands[f]!.length;
|
|
191
|
+
const ke = cands[e]!.length;
|
|
192
|
+
x += mine ? table[i * kf + choice[f]!]! : table[choice[f]! * ke + i]!;
|
|
193
|
+
}
|
|
194
|
+
return x;
|
|
195
|
+
};
|
|
196
|
+
const random = rng(Math.max(0, Math.floor(o.seed)));
|
|
197
|
+
const cooling = Math.min(0.99, Math.max(0.9, o.cooling));
|
|
198
|
+
let temperature = Math.max(1e-6, o.temperature) * unit;
|
|
199
|
+
const moves = Math.max(1, Math.floor(o.budget)) * m;
|
|
200
|
+
let energy = 0;
|
|
201
|
+
let best = 0;
|
|
202
|
+
let bestChoice = choice.slice();
|
|
203
|
+
for (let s = 0; s < moves; s++) {
|
|
204
|
+
if (s > 0 && s % m === 0) temperature *= cooling;
|
|
205
|
+
const e = Math.floor(random() * m);
|
|
206
|
+
const list = cands[e]!;
|
|
207
|
+
if (list.length < 2) continue;
|
|
208
|
+
const from = choice[e]!;
|
|
209
|
+
const to = (from + 1 + Math.floor(random() * (list.length - 1))) % list.length;
|
|
210
|
+
// The change of the energy: the ink and the length of the key, and the crossings with the other edges.
|
|
211
|
+
const dInk = use(e, list[from]!, -1) + use(e, list[to]!, 1);
|
|
212
|
+
const dLength = list[to]!.length - list[from]!.length;
|
|
213
|
+
const dCross = crossings(e, to) - crossings(e, from);
|
|
214
|
+
const delta = (1 - DISCOUNT) * dInk + DISCOUNT * dLength + crossCost * dCross;
|
|
215
|
+
const y = delta / temperature;
|
|
216
|
+
if (delta <= 0 || random() < 1 / (1 + y + (y * y) / 2)) {
|
|
217
|
+
choice[e] = to;
|
|
218
|
+
energy += delta;
|
|
219
|
+
if (energy < best - 1e-9) {
|
|
220
|
+
best = energy;
|
|
221
|
+
bestChoice = choice.slice();
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
use(e, list[to]!, -1);
|
|
225
|
+
use(e, list[from]!, 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
live.forEach((w, e) => {
|
|
229
|
+
const c = cands[e]![bestChoice[e]!]!;
|
|
230
|
+
w.seq = c.seq;
|
|
231
|
+
w.ids = c.ids;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* This function makes the router `annealing` (RULE-021 with its rider WILD-028). It is the router `shared`, then the annealing of `annealWays`.
|
|
237
|
+
* Each candidate is a way through the corner graph, as in `shared`, so each one keeps the hard rules.
|
|
238
|
+
*/
|
|
239
|
+
export function annealingRouter(options: Partial<AnnealingOptions> & { readonly name: string; readonly rule?: GroupRule; readonly maxNodes?: number }): Router {
|
|
240
|
+
const o = { ...ANNEALING_DEFAULTS, ...options };
|
|
241
|
+
return sharedRouter(
|
|
242
|
+
{ name: o.name, ...(o.rule === undefined ? {} : { rule: o.rule }), ...(o.maxNodes === undefined ? {} : { maxNodes: o.maxNodes }) },
|
|
243
|
+
undefined,
|
|
244
|
+
{ refine: (ways, search) => annealWays(ways, search, o) },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The router strategy `annealing` (RULE-021), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
|
|
250
|
+
* A screen module or a consumer adds it to a registry.
|
|
251
|
+
*/
|
|
252
|
+
export const annealingStrategy: Strategy<"router", AnnealingOptions> = defineStrategy({
|
|
253
|
+
step: "router",
|
|
254
|
+
name: "annealing",
|
|
255
|
+
describe: "Simulated annealing over candidate routes (RULE-021, with WILD-028). Each edge picks one of a few ways, for less ink and fewer crossings.",
|
|
256
|
+
defaults: { ...ANNEALING_DEFAULTS },
|
|
257
|
+
schema: ANNEALING_SCHEMA,
|
|
258
|
+
maxNodes: 10_000,
|
|
259
|
+
run: (graph: Graph, o: Readonly<AnnealingOptions>): Result => annealingRouter({ ...o, name: "annealing", rule: byCollection }).route(graph),
|
|
260
|
+
});
|
package/src/routers/frames.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { center, dist, type Box, type Point } from "../geom.ts";
|
|
2
|
-
import { nodeIndex, type Graph, type InnerEdge, type Node, type Route } from "../model.ts";
|
|
2
|
+
import { fixedPortPoint, nodeIndex, type Graph, type InnerEdge, type Node, type Route } from "../model.ts";
|
|
3
3
|
import { placePlug, type Plug } from "../plugs.ts";
|
|
4
4
|
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
5
|
+
import { gutterRoutes, type GutterEdge } from "./gutter.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The rule for a node box that is not fully inside the box of its frame.
|
|
@@ -10,12 +11,22 @@ import { WayFinder, type WayOptions } from "../ways.ts";
|
|
|
10
11
|
*/
|
|
11
12
|
export type Outside = "throw" | "skip" | "clamp";
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The mode of the inner routes. With `corners`, the corner graph gives each route, in the box of the frame smaller by `frameInset`.
|
|
16
|
+
* With `gutter`, each route is orthogonal in the free room of the frame box: the gaps between the nodes and the strips at the walls.
|
|
17
|
+
*/
|
|
18
|
+
export type InnerMode = "corners" | "gutter";
|
|
19
|
+
|
|
20
|
+
/** The options of the inner routes: the options of the way finder, the rule `outside`, the inset `frameInset`, the mode `inner` and `bendCost`. */
|
|
14
21
|
export interface FrameOptions extends WayOptions {
|
|
15
22
|
/** The rule for a node box outside the box of its frame. The default is `throw`. */
|
|
16
23
|
readonly outside?: Outside;
|
|
17
|
-
/** The distance from the walls of a frame to the bounds of its inner routes. The default is the margin of the router. */
|
|
24
|
+
/** The distance from the walls of a frame to the bounds of its inner routes. The default is the margin of the router. The mode `gutter` does not use it. */
|
|
18
25
|
readonly frameInset?: number;
|
|
26
|
+
/** The mode of the inner routes. The default is `corners`. */
|
|
27
|
+
readonly inner?: InnerMode;
|
|
28
|
+
/** The cost of one bend of an inner route of the mode `gutter`, in units of length. The default is 16. */
|
|
29
|
+
readonly bendCost?: number;
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
/** The inside of one frame for the inner edges to one depth: the bounds, the way finder and the obstacles by id. */
|
|
@@ -41,6 +52,7 @@ export function frameRoutes(graph: Graph, inner: readonly InnerEdge[], options:
|
|
|
41
52
|
if (list) list.push(n);
|
|
42
53
|
else children.set(n.parent, [n]);
|
|
43
54
|
}
|
|
55
|
+
if (options.inner === "gutter") return gutterFrameRoutes(inner, options, byId, children);
|
|
44
56
|
const margin = options.margin ?? 5;
|
|
45
57
|
const inset = options.frameInset ?? margin;
|
|
46
58
|
const rooms = new Map<string, Room>();
|
|
@@ -48,9 +60,7 @@ export function frameRoutes(graph: Graph, inner: readonly InnerEdge[], options:
|
|
|
48
60
|
for (const { edge, frame, plug } of inner) {
|
|
49
61
|
const f = byId.get(frame)!;
|
|
50
62
|
const [own, far] = plug === "source" ? [edge.source, edge.target] : [edge.target, edge.source];
|
|
51
|
-
|
|
52
|
-
const chain: string[] = [];
|
|
53
|
-
for (let p = byId.get(far.node)!.parent!; p !== frame; p = byId.get(p)!.parent!) chain.push(p);
|
|
63
|
+
const chain = chainOf(byId, frame, far.node);
|
|
54
64
|
const key = JSON.stringify([frame, ...chain]);
|
|
55
65
|
let room = rooms.get(key);
|
|
56
66
|
if (!room) rooms.set(key, (room = makeRoom(f, chain, children, byId, inset, options)));
|
|
@@ -69,6 +79,51 @@ export function frameRoutes(graph: Graph, inner: readonly InnerEdge[], options:
|
|
|
69
79
|
return routes;
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
/** This function gives the nodes between the frame and the far end, the nearest first. Each of them holds the far end, so it is not an obstacle. */
|
|
83
|
+
function chainOf(byId: ReadonlyMap<string, Node>, frame: string, far: string): string[] {
|
|
84
|
+
const chain: string[] = [];
|
|
85
|
+
for (let p = byId.get(far)!.parent!; p !== frame; p = byId.get(p)!.parent!) chain.push(p);
|
|
86
|
+
return chain;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* This function gives the inner routes of the mode `gutter`. The inner edges of one room route together, so that they share the gaps in lanes.
|
|
91
|
+
* A route is orthogonal, in the free room of the frame box. An edge without a way through the free room gets no route.
|
|
92
|
+
*/
|
|
93
|
+
function gutterFrameRoutes(inner: readonly InnerEdge[], options: FrameOptions, byId: ReadonlyMap<string, Node>, children: ReadonlyMap<string, readonly Node[]>): Route[] {
|
|
94
|
+
const rooms = new Map<string, { readonly nodes: Map<string, Node>; readonly items: { at: number; edge: GutterEdge }[]; readonly frame: Box }>();
|
|
95
|
+
const routes: (Route | undefined)[] = inner.map(() => undefined);
|
|
96
|
+
inner.forEach(({ edge, frame, plug }, at) => {
|
|
97
|
+
const f = byId.get(frame)!;
|
|
98
|
+
const [own, far] = plug === "source" ? [edge.source, edge.target] : [edge.target, edge.source];
|
|
99
|
+
const chain = chainOf(byId, frame, far.node);
|
|
100
|
+
const key = JSON.stringify([frame, ...chain]);
|
|
101
|
+
let room = rooms.get(key);
|
|
102
|
+
if (!room) rooms.set(key, (room = { nodes: roomNodes(f, chain, children, byId, f.box, options), items: [], frame: f.box }));
|
|
103
|
+
const end = room.nodes.get(far.node);
|
|
104
|
+
if (!end) return;
|
|
105
|
+
const fp = placePlug(f, own, center(end.box));
|
|
106
|
+
const port = fixedPortPoint(end, far) === undefined ? undefined : placePlug(end, far, fp.point);
|
|
107
|
+
const side = far.port === undefined ? undefined : end.ports?.find((p) => p.id === far.port)?.side;
|
|
108
|
+
room.items.push({ at, edge: { plug: fp, end, ...(port ? { port } : {}), ...(side ? { side } : {}) } });
|
|
109
|
+
});
|
|
110
|
+
for (const room of rooms.values()) {
|
|
111
|
+
const found = gutterRoutes(
|
|
112
|
+
room.frame,
|
|
113
|
+
[...room.nodes.values()],
|
|
114
|
+
room.items.map((i) => i.edge),
|
|
115
|
+
{ laneGap: options.margin ?? 5, ...(options.bendCost === undefined ? {} : { bendCost: options.bendCost }) },
|
|
116
|
+
);
|
|
117
|
+
room.items.forEach(({ at }, k) => {
|
|
118
|
+
const points = found[k];
|
|
119
|
+
if (!points) return;
|
|
120
|
+
const { edge, frame, plug } = inner[at]!;
|
|
121
|
+
routes[at] = { edge: edge.id, points: plug === "source" ? points : points.reverse(), frame };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return routes.filter((r): r is Route => r !== undefined);
|
|
125
|
+
}
|
|
126
|
+
|
|
72
127
|
/** This function puts the plug of the wall before a way that starts on the bounds. A first point on the line of the stub goes. */
|
|
73
128
|
function stub(plug: Point, way: Point[]): Point[] {
|
|
74
129
|
const [a, b] = way;
|
|
@@ -86,6 +141,24 @@ function stub(plug: Point, way: Point[]): Point[] {
|
|
|
86
141
|
function makeRoom(f: Node, chain: readonly string[], children: ReadonlyMap<string, readonly Node[]>, byId: ReadonlyMap<string, Node>, inset: number, options: FrameOptions): Room {
|
|
87
142
|
const margin = options.margin ?? 5;
|
|
88
143
|
const bounds = shrink(f.box, inset);
|
|
144
|
+
const nodes = roomNodes(f, chain, children, byId, bounds, options);
|
|
145
|
+
// The band of each wall starts half a margin outside the bounds, so that a way keeps in the bounds.
|
|
146
|
+
const w = shrink(bounds, -(margin / 2 + EPS));
|
|
147
|
+
const t = 2 * margin + 1;
|
|
148
|
+
const walls: Node[] = [
|
|
149
|
+
{ x: w.x - t, y: w.y - t, w: w.w + 2 * t, h: t },
|
|
150
|
+
{ x: w.x - t, y: w.y + w.h, w: w.w + 2 * t, h: t },
|
|
151
|
+
{ x: w.x - t, y: w.y, w: t, h: w.h },
|
|
152
|
+
{ x: w.x + w.w, y: w.y, w: t, h: w.h },
|
|
153
|
+
].map((box, i) => ({ id: `\u0000wall${i}`, box }));
|
|
154
|
+
return { ways: new WayFinder([...nodes.values(), ...walls], options), nodes };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* This function gives the obstacles of a room by id. They are the children of the frame and of each node of the chain, but not the chain.
|
|
159
|
+
* It applies the rule `outside` to each box outside the box of its holder. The rule `clamp` cuts such a box to `bounds`.
|
|
160
|
+
*/
|
|
161
|
+
function roomNodes(f: Node, chain: readonly string[], children: ReadonlyMap<string, readonly Node[]>, byId: ReadonlyMap<string, Node>, bounds: Box, options: FrameOptions): Map<string, Node> {
|
|
89
162
|
const skip = new Set(chain);
|
|
90
163
|
const nodes = new Map<string, Node>();
|
|
91
164
|
for (const holder of [f.id, ...chain]) {
|
|
@@ -104,16 +177,7 @@ function makeRoom(f: Node, chain: readonly string[], children: ReadonlyMap<strin
|
|
|
104
177
|
if (cut) nodes.set(n.id, { ...n, box: cut, ...(n.ports ? { ports: n.ports.map((p) => (p.at ? { ...p, at: { x: p.at.x + n.box.x - cut.x, y: p.at.y + n.box.y - cut.y } } : p)) } : {}) });
|
|
105
178
|
}
|
|
106
179
|
}
|
|
107
|
-
|
|
108
|
-
const w = shrink(bounds, -(margin / 2 + EPS));
|
|
109
|
-
const t = 2 * margin + 1;
|
|
110
|
-
const walls: Node[] = [
|
|
111
|
-
{ x: w.x - t, y: w.y - t, w: w.w + 2 * t, h: t },
|
|
112
|
-
{ x: w.x - t, y: w.y + w.h, w: w.w + 2 * t, h: t },
|
|
113
|
-
{ x: w.x - t, y: w.y, w: t, h: w.h },
|
|
114
|
-
{ x: w.x + w.w, y: w.y, w: t, h: w.h },
|
|
115
|
-
].map((box, i) => ({ id: `\u0000wall${i}`, box }));
|
|
116
|
-
return { ways: new WayFinder([...nodes.values(), ...walls], options), nodes };
|
|
180
|
+
return nodes;
|
|
117
181
|
}
|
|
118
182
|
|
|
119
183
|
/** This function gives the box, smaller by `d` on each side. A negative `d` makes it larger. */
|