@mark1russell7/highways 0.9.1 → 0.9.3
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 +66 -2
- package/build/index.d.ts +195 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3530 -2298
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/compare.ts +13 -1
- package/src/bench/measures/curves.ts +55 -1
- package/src/bench/measures/groups.ts +71 -0
- package/src/bench/measures/index.ts +1 -0
- package/src/bench/measures/levels.ts +24 -0
- package/src/bench/measures/room.ts +106 -1
- package/src/bench/measures/symmetry.ts +114 -0
- package/src/bench/screen/forms.ts +9 -2
- package/src/bench/screen/measures.ts +40 -3
- package/src/bench/screen/screen.ts +40 -5
- package/src/bench/suite.ts +10 -2
- package/src/highways/follow.ts +2 -2
- package/src/index.ts +2 -0
- package/src/model.ts +67 -0
- package/src/plugs/fix.ts +112 -0
- package/src/plugs/justify.ts +66 -0
- package/src/plugs/row-cards.ts +46 -0
- package/src/plugs/side.ts +23 -2
- package/src/plugs/spread/index.ts +217 -0
- package/src/routers/corners.ts +15 -44
- package/src/routers/frames.ts +136 -0
- package/src/routers/index.ts +1 -0
- package/src/routers/plugs.ts +2 -2
- package/src/routers/sections.ts +2 -2
- package/src/routers/shared.ts +2 -2
- package/src/routers/straight.ts +2 -2
- package/src/routers/trunks.ts +2 -2
- package/src/strategies/builtins.ts +31 -1
- package/src/strategies/schemas.ts +35 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plug strategy `spread` (PLUG-SPREAD): the free plugs of each wall share the room of that wall with a minimum gap.
|
|
3
|
+
* The order on the wall keeps the order of the far ends, so two routes do not cross at the wall. No plug drops.
|
|
4
|
+
*/
|
|
5
|
+
import { center, type Box, type Point } from "../../geom.ts";
|
|
6
|
+
import { nodeIndex, portOffset, type End, type Node, type Side } from "../../model.ts";
|
|
7
|
+
import { nearestSide, placePlug, SIDE_NORMAL, sideToward, type Plug } from "../../plugs.ts";
|
|
8
|
+
import type { PlugInput, PlugPair } from "../../strategies/model.ts";
|
|
9
|
+
import { isotonic } from "../justify.ts";
|
|
10
|
+
|
|
11
|
+
/** The options of the plug strategy `spread`. */
|
|
12
|
+
export interface SpreadOptions {
|
|
13
|
+
/** The least gap between two plugs of one wall. When the room of the wall is too small, the gap shrinks evenly. The default is 8. */
|
|
14
|
+
readonly plugGap?: number;
|
|
15
|
+
/** The distance from a plug to a corner of its box, or a quarter of the wall when the wall is short. The default is 6. */
|
|
16
|
+
readonly inset?: number;
|
|
17
|
+
/** The number of passes that sort each wall again by the new places of the far ends. The default is 2. */
|
|
18
|
+
readonly rounds?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The defaults of the plug strategy `spread`. */
|
|
22
|
+
export const SPREAD_DEFAULTS: Required<SpreadOptions> = Object.freeze({ plugGap: 8, inset: 6, rounds: 2 });
|
|
23
|
+
|
|
24
|
+
/** A free plug: one port of a node, or one end without a port. All the ends of one port share its plug. */
|
|
25
|
+
interface Slot {
|
|
26
|
+
readonly node: Node;
|
|
27
|
+
readonly side: Side;
|
|
28
|
+
/** The place of the plug that faces the far end (`placePlug`), on the axis along the wall. */
|
|
29
|
+
readonly ideal: number;
|
|
30
|
+
/** The index of the port in the ports of its node, then the index of the slot. Thus the rows of a card keep their order in a tie. */
|
|
31
|
+
readonly rank: readonly [number, number];
|
|
32
|
+
/** The far end of the first end of the slot: a fixed plug, or the key of its slot. */
|
|
33
|
+
far?: Plug | string;
|
|
34
|
+
/** The place of the plug on the axis along the wall. */
|
|
35
|
+
at: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A wall: one side of one node, with its free slots and the places of its fixed plugs on the axis along the wall. */
|
|
39
|
+
interface Wall {
|
|
40
|
+
readonly node: Node;
|
|
41
|
+
readonly side: Side;
|
|
42
|
+
readonly slots: Slot[];
|
|
43
|
+
readonly fixed: number[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** This function gives the axis along a side. */
|
|
47
|
+
function alongOf(side: Side): "x" | "y" {
|
|
48
|
+
return side === "east" || side === "west" ? "y" : "x";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** This function gives the room of a wall: the segment of the axis along the side, without the corner insets. */
|
|
52
|
+
function roomOf(box: Box, side: Side, inset: number): [number, number] {
|
|
53
|
+
const along = alongOf(side);
|
|
54
|
+
const start = along === "y" ? box.y : box.x;
|
|
55
|
+
const len = along === "y" ? box.h : box.w;
|
|
56
|
+
const m = Math.min(inset, len / 4);
|
|
57
|
+
return [start + m, start + len - m];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** This function gives the point of a side at `v` on the axis along the side. */
|
|
61
|
+
function pointAt(box: Box, side: Side, v: number): Point {
|
|
62
|
+
if (side === "east") return { x: box.x + box.w, y: v };
|
|
63
|
+
if (side === "west") return { x: box.x, y: v };
|
|
64
|
+
if (side === "south") return { x: v, y: box.y + box.h };
|
|
65
|
+
return { x: v, y: box.y };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* This function gives the sort key of a far point on a wall. The key has the order of the angle from the normal of the wall, from the center of the box.
|
|
70
|
+
* The key grows in the direction of the axis along the wall. Thus the plugs in the order of the keys do not cross at the wall.
|
|
71
|
+
*/
|
|
72
|
+
function keyOf(box: Box, side: Side, far: Point): number {
|
|
73
|
+
const c = center(box);
|
|
74
|
+
const n = SIDE_NORMAL[side];
|
|
75
|
+
const dx = far.x - c.x;
|
|
76
|
+
const dy = far.y - c.y;
|
|
77
|
+
const out = dx * n.x + dy * n.y;
|
|
78
|
+
const along = alongOf(side) === "y" ? dy : dx;
|
|
79
|
+
// A pseudo-angle with the order of the angle: a sum and one division, with no transcendental call. It goes from -2 to 2.
|
|
80
|
+
const size = Math.abs(out) + Math.abs(along);
|
|
81
|
+
if (size === 0) return 0;
|
|
82
|
+
const p = along / size;
|
|
83
|
+
if (out >= 0) return p;
|
|
84
|
+
return along >= 0 ? 2 - p : -2 - p;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* This function places `ideal.length` plugs in their given order on the segment from `lo` to `hi`. Two neighbours are at least `gap` apart.
|
|
89
|
+
* The gap shrinks evenly when the segment is too short. The sum of the squares of the moves from the ideal places is the least.
|
|
90
|
+
*/
|
|
91
|
+
function placeInOrder(ideal: readonly number[], lo: number, hi: number, gap: number): number[] {
|
|
92
|
+
const k = ideal.length;
|
|
93
|
+
if (k === 0) return [];
|
|
94
|
+
const s = k > 1 ? Math.max(0, Math.min(gap, (hi - lo) / (k - 1))) : 0;
|
|
95
|
+
const y = isotonic(ideal.map((v, i) => v - i * s));
|
|
96
|
+
const top = Math.max(lo, hi - (k - 1) * s);
|
|
97
|
+
return y.map((v, i) => Math.max(lo, Math.min(top, v)) + i * s);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* This function places the slots of one wall in their order. The fixed plugs cut the room into parts.
|
|
102
|
+
* Each slot goes into the part of its ideal place, and the parts keep the order of the slots. In each part, two plugs and a plug and a fixed plug are at least `gap` apart.
|
|
103
|
+
* The gap of a part shrinks evenly when the part is too short.
|
|
104
|
+
*/
|
|
105
|
+
function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number, hi: number, gap: number): void {
|
|
106
|
+
const cuts = [...new Set(fixed.filter((f) => f > lo - 1e-9 && f < hi + 1e-9))].sort((a, b) => a - b);
|
|
107
|
+
// The part i is from cuts[i - 1] to cuts[i]. The first part starts at lo, and the last part ends at hi.
|
|
108
|
+
const ends = [lo, ...cuts, hi];
|
|
109
|
+
const parts: Slot[][] = cuts.map(() => []).concat([[]]);
|
|
110
|
+
let part = 0;
|
|
111
|
+
for (const s of slots) {
|
|
112
|
+
let p = 0;
|
|
113
|
+
while (p < cuts.length && s.ideal > cuts[p]!) p++;
|
|
114
|
+
part = Math.max(part, p);
|
|
115
|
+
parts[part]!.push(s);
|
|
116
|
+
}
|
|
117
|
+
parts.forEach((list, p) => {
|
|
118
|
+
if (list.length === 0) return;
|
|
119
|
+
const a = ends[p]!;
|
|
120
|
+
const b = ends[p + 1]!;
|
|
121
|
+
const fa = p > 0 ? 1 : 0;
|
|
122
|
+
const fb = p < cuts.length ? 1 : 0;
|
|
123
|
+
const steps = list.length - 1 + fa + fb;
|
|
124
|
+
const s = steps > 0 ? Math.max(0, Math.min(gap, (b - a) / steps)) : gap;
|
|
125
|
+
const lower = a + fa * s;
|
|
126
|
+
const upper = Math.max(lower, b - fb * s);
|
|
127
|
+
const at = placeInOrder(
|
|
128
|
+
list.map((x) => x.ideal),
|
|
129
|
+
lower,
|
|
130
|
+
upper,
|
|
131
|
+
s,
|
|
132
|
+
);
|
|
133
|
+
list.forEach((x, i) => {
|
|
134
|
+
x.at = at[i]!;
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* This function gives the plugs of the plug strategy `spread`. A fixed plug stays at its point, and a plug with a side stays on that side.
|
|
141
|
+
* Each free plug goes on the side that faces the other end (`sideToward`). Then the free plugs of each wall share its room with at least `plugGap` between two of them.
|
|
142
|
+
* They keep the order of their far ends. The ends of one port share one plug.
|
|
143
|
+
*
|
|
144
|
+
* The output has one entry for each edge with two known nodes, in the order of the edges.
|
|
145
|
+
*/
|
|
146
|
+
export function spreadEnds(input: PlugInput, options: SpreadOptions = {}): PlugPair[] {
|
|
147
|
+
const { graph } = input;
|
|
148
|
+
const gap = options.plugGap ?? SPREAD_DEFAULTS.plugGap;
|
|
149
|
+
const inset = options.inset ?? SPREAD_DEFAULTS.inset;
|
|
150
|
+
const rounds = options.rounds ?? SPREAD_DEFAULTS.rounds;
|
|
151
|
+
const byId = nodeIndex(graph);
|
|
152
|
+
const slots = new Map<string, Slot>();
|
|
153
|
+
const walls = new Map<string, Wall>();
|
|
154
|
+
// The plug of each end: a fixed plug, or the key of its slot.
|
|
155
|
+
const ends: { edge: string; source: Plug | string; target: Plug | string }[] = [];
|
|
156
|
+
const wallOf = (node: Node, side: Side): Wall => {
|
|
157
|
+
const key = JSON.stringify([node.id, side]);
|
|
158
|
+
let wall = walls.get(key);
|
|
159
|
+
if (!wall) {
|
|
160
|
+
const fixed: number[] = [];
|
|
161
|
+
for (const p of node.ports ?? []) {
|
|
162
|
+
const at = portOffset(node.box, p);
|
|
163
|
+
if (at === undefined) continue;
|
|
164
|
+
const point = { x: node.box.x + at.x, y: node.box.y + at.y };
|
|
165
|
+
if ((p.side ?? nearestSide(node.box, point)) === side) fixed.push(point[alongOf(side)]);
|
|
166
|
+
}
|
|
167
|
+
walls.set(key, (wall = { node, side, slots: [], fixed }));
|
|
168
|
+
}
|
|
169
|
+
return wall;
|
|
170
|
+
};
|
|
171
|
+
const plugOf = (node: Node, end: End, far: Node): Plug | string => {
|
|
172
|
+
const toward = center(far.box);
|
|
173
|
+
const port = end.port === undefined ? undefined : node.ports?.find((p) => p.id === end.port);
|
|
174
|
+
if (port && portOffset(node.box, port) !== undefined) return placePlug(node, end, toward);
|
|
175
|
+
const key = port ? JSON.stringify([node.id, port.id]) : JSON.stringify([node.id, slots.size]);
|
|
176
|
+
if (slots.has(key)) return key;
|
|
177
|
+
const side = port?.side ?? sideToward(node.box, toward);
|
|
178
|
+
const ideal = placePlug(node, end, toward).point[alongOf(side)];
|
|
179
|
+
const slot: Slot = { node, side, ideal, rank: [port ? node.ports!.indexOf(port) : -1, slots.size], at: ideal };
|
|
180
|
+
slots.set(key, slot);
|
|
181
|
+
wallOf(node, side).slots.push(slot);
|
|
182
|
+
return key;
|
|
183
|
+
};
|
|
184
|
+
for (const e of graph.edges) {
|
|
185
|
+
const s = byId.get(e.source.node);
|
|
186
|
+
const t = byId.get(e.target.node);
|
|
187
|
+
if (!s || !t) continue;
|
|
188
|
+
const source = plugOf(s, e.source, t);
|
|
189
|
+
const target = plugOf(t, e.target, s);
|
|
190
|
+
for (const [own, far] of [
|
|
191
|
+
[source, target],
|
|
192
|
+
[target, source],
|
|
193
|
+
] as const) {
|
|
194
|
+
const slot = typeof own === "string" ? slots.get(own)! : undefined;
|
|
195
|
+
if (slot && slot.far === undefined) slot.far = far;
|
|
196
|
+
}
|
|
197
|
+
ends.push({ edge: e.id, source, target });
|
|
198
|
+
}
|
|
199
|
+
const resolve = (p: Plug | string): Plug => {
|
|
200
|
+
if (typeof p !== "string") return p;
|
|
201
|
+
const s = slots.get(p)!;
|
|
202
|
+
return { point: pointAt(s.node.box, s.side, s.at), normal: SIDE_NORMAL[s.side] };
|
|
203
|
+
};
|
|
204
|
+
// A pass sorts each wall by the places of the far ends as they are now, and places its plugs. A tie keeps the order of the last pass.
|
|
205
|
+
const order = new Map<Slot, number>();
|
|
206
|
+
for (let round = 0; round <= rounds; round++) {
|
|
207
|
+
for (const wall of walls.values()) {
|
|
208
|
+
const [lo, hi] = roomOf(wall.node.box, wall.side, inset);
|
|
209
|
+
const keys = new Map<Slot, number>(wall.slots.map((s) => [s, keyOf(wall.node.box, wall.side, resolve(s.far!).point)]));
|
|
210
|
+
const tie = (a: Slot, b: Slot): number => (round === 0 ? a.rank[0] - b.rank[0] || a.rank[1] - b.rank[1] : order.get(a)! - order.get(b)!);
|
|
211
|
+
const sorted = [...wall.slots].sort((a, b) => keys.get(a)! - keys.get(b)! || tie(a, b));
|
|
212
|
+
sorted.forEach((s, i) => order.set(s, i));
|
|
213
|
+
placeWall(sorted, wall.fixed, lo, hi, gap);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return ends.map((x) => ({ edge: x.edge, source: resolve(x.source), target: resolve(x.target) }));
|
|
217
|
+
}
|
package/src/routers/corners.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { center
|
|
2
|
-
import { nodeIndex,
|
|
1
|
+
import { center } from "../geom.ts";
|
|
2
|
+
import { nodeIndex, resolvePorts, splitFrames, type Edge, type Graph, type Node, type Result, type Route, type Router } from "../model.ts";
|
|
3
3
|
import { placePlug, type Plug } from "../plugs.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { spacePlugs } from "../plugs/justify.ts";
|
|
5
5
|
import { sidePlugs } from "../plugs/side.ts";
|
|
6
6
|
import type { PlugPair, Strategy } from "../strategies/model.ts";
|
|
7
|
-
import { WayFinder
|
|
7
|
+
import { WayFinder } from "../ways.ts";
|
|
8
|
+
import { frameRoutes, type FrameOptions } from "./frames.ts";
|
|
8
9
|
|
|
9
10
|
/** The ways to place a free plug of the corner router. */
|
|
10
11
|
export type PlugPlace = "facing" | "side";
|
|
11
12
|
|
|
12
13
|
/** The options of the corner router. */
|
|
13
|
-
export interface CornersOptions extends
|
|
14
|
+
export interface CornersOptions extends FrameOptions {
|
|
14
15
|
/** The name in the benchmark. */
|
|
15
16
|
readonly name: string;
|
|
16
17
|
/** The benchmark skips a fixture with more nodes. */
|
|
@@ -38,15 +39,21 @@ export interface CornersOptions extends WayOptions {
|
|
|
38
39
|
* This function makes a router that goes around the nodes.
|
|
39
40
|
* Each edge leaves its plug along the normal of the side. It goes a shortest way (A*) through a sparse visibility graph
|
|
40
41
|
* of the box corners, without the needless corners. It enters the target plug along its normal.
|
|
42
|
+
* An inner edge (`splitFrames`) gets an inner route in the bounds of its frame (`frameRoutes`), after the outer routes.
|
|
41
43
|
*/
|
|
42
44
|
export function cornersRouter(options: CornersOptions): Router {
|
|
43
45
|
const router: Router = {
|
|
44
46
|
name: options.name,
|
|
45
47
|
route(input: Graph): Result {
|
|
46
|
-
const graph = resolvePorts(input);
|
|
48
|
+
const { graph, inner } = splitFrames(resolvePorts(input));
|
|
47
49
|
const byId = nodeIndex(graph);
|
|
48
50
|
const ways = new WayFinder(graph.nodes, options);
|
|
49
51
|
const routes: Route[] = [];
|
|
52
|
+
// The inner routes come after the outer routes.
|
|
53
|
+
const done = (): Result => {
|
|
54
|
+
if (inner.length > 0) routes.push(...frameRoutes(graph, inner, options));
|
|
55
|
+
return { routes };
|
|
56
|
+
};
|
|
50
57
|
if (!options.plug && (options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
|
|
51
58
|
for (const e of graph.edges) {
|
|
52
59
|
const s = byId.get(e.source.node);
|
|
@@ -56,7 +63,7 @@ export function cornersRouter(options: CornersOptions): Router {
|
|
|
56
63
|
const pt = placePlug(t, e.target, center(s.box));
|
|
57
64
|
routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
|
|
58
65
|
}
|
|
59
|
-
return
|
|
66
|
+
return done();
|
|
60
67
|
}
|
|
61
68
|
// First the plugs of each edge, then the gaps of the plugs of each side, then the ways between the plugs.
|
|
62
69
|
const ends: [Edge, Node, Node, Plug, Plug][] = [];
|
|
@@ -75,7 +82,7 @@ export function cornersRouter(options: CornersOptions): Router {
|
|
|
75
82
|
}
|
|
76
83
|
if ((options.plugGap ?? 0) > 0) spacePlugs(ends, options.plugGap!);
|
|
77
84
|
for (const [e, s, t, ps, pt] of ends) routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
|
|
78
|
-
return
|
|
85
|
+
return done();
|
|
79
86
|
},
|
|
80
87
|
};
|
|
81
88
|
return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
|
|
@@ -91,39 +98,3 @@ function plugsByEdge(list: readonly PlugPair[]): Map<string, PlugPair[]> {
|
|
|
91
98
|
}
|
|
92
99
|
return out;
|
|
93
100
|
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
|
|
97
|
-
* 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.
|
|
98
|
-
*/
|
|
99
|
-
function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number): void {
|
|
100
|
-
const inset = 6;
|
|
101
|
-
// 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.
|
|
102
|
-
const sides = new Map<string, { node: Node; normal: Point; items: [number, 3 | 4, Point][] }>();
|
|
103
|
-
ends.forEach(([e, s, t, ps, pt], i) => {
|
|
104
|
-
for (const [end, node, plug, other, k] of [
|
|
105
|
-
[e.source, s, ps, pt, 3],
|
|
106
|
-
[e.target, t, pt, ps, 4],
|
|
107
|
-
] as const) {
|
|
108
|
-
if (fixedPortPoint(node, end) !== undefined) continue;
|
|
109
|
-
const key = JSON.stringify([node.id, plug.normal.x, plug.normal.y]);
|
|
110
|
-
let side = sides.get(key);
|
|
111
|
-
if (!side) sides.set(key, (side = { node, normal: plug.normal, items: [] }));
|
|
112
|
-
side.items.push([i, k, other.point]);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
for (const { node, normal, items } of sides.values()) {
|
|
116
|
-
if (items.length < 2) continue;
|
|
117
|
-
const along = normal.x !== 0 ? "y" : "x";
|
|
118
|
-
const b = node.box;
|
|
119
|
-
const start = along === "y" ? b.y : b.x;
|
|
120
|
-
const len = along === "y" ? b.h : b.w;
|
|
121
|
-
const m = Math.min(inset, len / 4);
|
|
122
|
-
const ideal = items.map(([i, k]) => ends[i]![k].point[along]);
|
|
123
|
-
const placed = justifyPlugs(ideal, start + m, start + len - m, gap, (j) => items[j]![2][along]);
|
|
124
|
-
items.forEach(([i, k], j) => {
|
|
125
|
-
const old = ends[i]![k];
|
|
126
|
-
ends[i]![k] = { point: along === "y" ? { x: old.point.x, y: placed[j]! } : { x: placed[j]!, y: old.point.y }, normal: old.normal };
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
@@ -0,0 +1,136 @@
|
|
|
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";
|
|
3
|
+
import { placePlug, type Plug } from "../plugs.ts";
|
|
4
|
+
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The rule for a node box that is not fully inside the box of its frame.
|
|
8
|
+
* With `throw`, the router throws a `RangeError`. With `skip`, the node is not an obstacle, and its inner edges get no route.
|
|
9
|
+
* With `clamp`, the router cuts the box to the bounds of the frame.
|
|
10
|
+
*/
|
|
11
|
+
export type Outside = "throw" | "skip" | "clamp";
|
|
12
|
+
|
|
13
|
+
/** The options of the inner routes: the options of the way finder, the rule `outside` and the inset `frameInset`. */
|
|
14
|
+
export interface FrameOptions extends WayOptions {
|
|
15
|
+
/** The rule for a node box outside the box of its frame. The default is `throw`. */
|
|
16
|
+
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. */
|
|
18
|
+
readonly frameInset?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The inside of one frame for the inner edges to one depth: the bounds, the way finder and the obstacles by id. */
|
|
22
|
+
interface Room {
|
|
23
|
+
readonly ways: WayFinder;
|
|
24
|
+
readonly nodes: ReadonlyMap<string, Node>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The tiny gap between the bounds and the band of a wall, so that a way along the bounds stays clear of the wall. */
|
|
28
|
+
const EPS = 1e-6;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* This function gives the inner routes of the inner edges (`splitFrames`). Each route stays in the bounds of its frame:
|
|
32
|
+
* the box of the frame, smaller by `frameInset`. It goes around the other nodes of the frame.
|
|
33
|
+
* It leaves the plug of the frame into the frame, along the normal of the wall. Each route has the id of its frame in `frame`.
|
|
34
|
+
*/
|
|
35
|
+
export function frameRoutes(graph: Graph, inner: readonly InnerEdge[], options: FrameOptions): Route[] {
|
|
36
|
+
const byId = nodeIndex(graph);
|
|
37
|
+
const children = new Map<string, Node[]>();
|
|
38
|
+
for (const n of graph.nodes) {
|
|
39
|
+
if (n.parent === undefined) continue;
|
|
40
|
+
const list = children.get(n.parent);
|
|
41
|
+
if (list) list.push(n);
|
|
42
|
+
else children.set(n.parent, [n]);
|
|
43
|
+
}
|
|
44
|
+
const margin = options.margin ?? 5;
|
|
45
|
+
const inset = options.frameInset ?? margin;
|
|
46
|
+
const rooms = new Map<string, Room>();
|
|
47
|
+
const routes: Route[] = [];
|
|
48
|
+
for (const { edge, frame, plug } of inner) {
|
|
49
|
+
const f = byId.get(frame)!;
|
|
50
|
+
const [own, far] = plug === "source" ? [edge.source, edge.target] : [edge.target, edge.source];
|
|
51
|
+
// The nodes between the frame and the far end: each of them holds the far end, so it is not an obstacle.
|
|
52
|
+
const chain: string[] = [];
|
|
53
|
+
for (let p = byId.get(far.node)!.parent!; p !== frame; p = byId.get(p)!.parent!) chain.push(p);
|
|
54
|
+
const key = JSON.stringify([frame, ...chain]);
|
|
55
|
+
let room = rooms.get(key);
|
|
56
|
+
if (!room) rooms.set(key, (room = makeRoom(f, chain, children, byId, inset, options)));
|
|
57
|
+
const end = room.nodes.get(far.node);
|
|
58
|
+
if (!end) continue;
|
|
59
|
+
const fp = placePlug(f, own, center(end.box));
|
|
60
|
+
const inward = { x: -fp.normal.x, y: -fp.normal.y };
|
|
61
|
+
// The way starts on the bounds, straight in from the plug of the wall.
|
|
62
|
+
const start: Plug = { point: { x: fp.point.x + inward.x * inset, y: fp.point.y + inward.y * inset }, normal: inward };
|
|
63
|
+
const ep = placePlug(end, far, fp.point);
|
|
64
|
+
const ends = new Set<Node>([end]);
|
|
65
|
+
const points =
|
|
66
|
+
plug === "source" ? stub(fp.point, room.ways.between(start, ep, ends)) : stub(fp.point, room.ways.between(ep, start, ends).reverse()).reverse();
|
|
67
|
+
routes.push({ edge: edge.id, points, frame });
|
|
68
|
+
}
|
|
69
|
+
return routes;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 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
|
+
function stub(plug: Point, way: Point[]): Point[] {
|
|
74
|
+
const [a, b] = way;
|
|
75
|
+
if (!a || dist(a, plug) === 0) return way;
|
|
76
|
+
if (b && Math.abs((a.x - plug.x) * (b.y - plug.y) - (a.y - plug.y) * (b.x - plug.x)) < 1e-9 && (a.x - plug.x) * (b.x - a.x) + (a.y - plug.y) * (b.y - a.y) > 0)
|
|
77
|
+
return [plug, ...way.slice(1)];
|
|
78
|
+
return [plug, ...way];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* This function makes the room of a frame for the far ends below the nodes of `chain` (the nearest first).
|
|
83
|
+
* The obstacles are the children of the frame and of each node of the chain, except the nodes of the chain.
|
|
84
|
+
* Four walls close the bounds, so that no way leaves them.
|
|
85
|
+
*/
|
|
86
|
+
function makeRoom(f: Node, chain: readonly string[], children: ReadonlyMap<string, readonly Node[]>, byId: ReadonlyMap<string, Node>, inset: number, options: FrameOptions): Room {
|
|
87
|
+
const margin = options.margin ?? 5;
|
|
88
|
+
const bounds = shrink(f.box, inset);
|
|
89
|
+
const skip = new Set(chain);
|
|
90
|
+
const nodes = new Map<string, Node>();
|
|
91
|
+
for (const holder of [f.id, ...chain]) {
|
|
92
|
+
for (const n of children.get(holder) ?? []) {
|
|
93
|
+
if (skip.has(n.id)) continue;
|
|
94
|
+
const box = byId.get(holder)!.box;
|
|
95
|
+
if (inside(n.box, box)) {
|
|
96
|
+
nodes.set(n.id, n);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const rule = options.outside ?? "throw";
|
|
100
|
+
if (rule === "throw")
|
|
101
|
+
throw new RangeError(`The box of the node "${n.id}" is not inside the box of its frame "${holder}". The option outside can skip or clamp it.`);
|
|
102
|
+
if (rule === "skip") continue;
|
|
103
|
+
const cut = cutBox(n.box, bounds);
|
|
104
|
+
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
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// The band of each wall starts half a margin outside the bounds, so that a way keeps in the bounds.
|
|
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 };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** This function gives the box, smaller by `d` on each side. A negative `d` makes it larger. */
|
|
120
|
+
function shrink(b: Box, d: number): Box {
|
|
121
|
+
return { x: b.x + d, y: b.y + d, w: Math.max(0, b.w - 2 * d), h: Math.max(0, b.h - 2 * d) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** This function tells if the box `a` is inside the box `b`. */
|
|
125
|
+
function inside(a: Box, b: Box): boolean {
|
|
126
|
+
return a.x >= b.x && a.y >= b.y && a.x + a.w <= b.x + b.w && a.y + a.h <= b.y + b.h;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** This function gives the part of the box `a` inside the box `b`, or `undefined` when they have no common area. */
|
|
130
|
+
function cutBox(a: Box, b: Box): Box | undefined {
|
|
131
|
+
const x = Math.max(a.x, b.x);
|
|
132
|
+
const y = Math.max(a.y, b.y);
|
|
133
|
+
const w = Math.min(a.x + a.w, b.x + b.w) - x;
|
|
134
|
+
const h = Math.min(a.y + a.h, b.y + b.h) - y;
|
|
135
|
+
return w > 0 && h > 0 ? { x, y, w, h } : undefined;
|
|
136
|
+
}
|
package/src/routers/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { byCollection, trunksRouter } from "./trunks.ts";
|
|
|
9
9
|
export { boxExit, straightRouter } from "./straight.ts";
|
|
10
10
|
export { plugsRouter } from "./plugs.ts";
|
|
11
11
|
export { cornersRouter, type CornersOptions } from "./corners.ts";
|
|
12
|
+
export { frameRoutes, type FrameOptions, type Outside } from "./frames.ts";
|
|
12
13
|
export { sharedRouter, type ShareCost, type SharedNegotiation, type SharedOptions, type ShareMerge, type ShareOrder } from "./shared.ts";
|
|
13
14
|
export { ATTRACTION_DEFAULTS, ATTRACTION_SCHEMA, attractionRouter, attractionStrategy, type AttractionOptions } from "./attraction/index.ts";
|
|
14
15
|
export { sectionsRouter, type SectionsOptions } from "./sections.ts";
|
package/src/routers/plugs.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { inflectionCurve } from "../curves.ts";
|
|
2
2
|
import { center } from "../geom.ts";
|
|
3
|
-
import { nodeIndex,
|
|
3
|
+
import { nodeIndex, outerGraph, type Graph, type Result, type Route, type Router } from "../model.ts";
|
|
4
4
|
import { placePlug } from "../plugs.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -10,7 +10,7 @@ import { placePlug } from "../plugs.ts";
|
|
|
10
10
|
export const plugsRouter: Router = {
|
|
11
11
|
name: "plugs",
|
|
12
12
|
route(input: Graph): Result {
|
|
13
|
-
const graph =
|
|
13
|
+
const graph = outerGraph(input);
|
|
14
14
|
const byId = nodeIndex(graph);
|
|
15
15
|
const routes: Route[] = [];
|
|
16
16
|
for (const e of graph.edges) {
|
package/src/routers/sections.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { BACKTRACK_MIN, goesOutAndBack } from "../bench/links.ts";
|
|
|
2
2
|
import { center, dist, pointSegmentDistance, polylineLength, type Point } from "../geom.ts";
|
|
3
3
|
import { LEVEL_LINK_DEFAULTS, type LevelLinkOptions } from "../levels.ts";
|
|
4
4
|
import { depthLevels, type DepthLevelOptions } from "../levels/depths/index.ts";
|
|
5
|
-
import { nodeIndex,
|
|
5
|
+
import { nodeIndex, outerGraph, type Graph, type Level, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
6
6
|
import { placePlug } from "../plugs.ts";
|
|
7
7
|
import { allSections, buildSections, checkSections, SECTION_DEFAULTS, type Section, type SectionOptions } from "../sections.ts";
|
|
8
8
|
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
@@ -92,7 +92,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
92
92
|
const router: Router = {
|
|
93
93
|
name: options.name,
|
|
94
94
|
route(input: Graph): Result {
|
|
95
|
-
const graph =
|
|
95
|
+
const graph = outerGraph(input);
|
|
96
96
|
const stretch = options.stretch ?? SECTIONS_DEFAULTS.stretch;
|
|
97
97
|
const stretchPad = options.stretchPad ?? SECTIONS_DEFAULTS.stretchPad;
|
|
98
98
|
const hubSlack = options.hubSlack ?? SECTIONS_DEFAULTS.hubSlack;
|
package/src/routers/shared.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { center, dist, type Point } from "../geom.ts";
|
|
2
|
-
import { nodeIndex,
|
|
2
|
+
import { nodeIndex, outerGraph, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
3
3
|
import { channelWidth } from "../channels.ts";
|
|
4
4
|
import { orderRun } from "../lane-order.ts";
|
|
5
5
|
import { stretchLaneOrders, type LaneStretchOrder } from "../lane-orders/stretch/index.ts";
|
|
@@ -149,7 +149,7 @@ export function sharedRouter(options: SharedOptions, negotiation?: SharedNegotia
|
|
|
149
149
|
const router: Router = {
|
|
150
150
|
name: options.name,
|
|
151
151
|
route(input: Graph): Result {
|
|
152
|
-
const graph =
|
|
152
|
+
const graph = outerGraph(input);
|
|
153
153
|
const byId = nodeIndex(graph);
|
|
154
154
|
const ways = new WayFinder(graph.nodes, options);
|
|
155
155
|
const corners = ways.corners;
|
package/src/routers/straight.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { center, type Box, type Point } from "../geom.ts";
|
|
2
|
-
import { fixedPortPoint, nodeIndex,
|
|
2
|
+
import { fixedPortPoint, nodeIndex, outerGraph, type Graph, type Result, type Route, type Router } from "../model.ts";
|
|
3
3
|
|
|
4
4
|
/** This function gives the point where the ray from the center of the box to `toward` leaves the box. */
|
|
5
5
|
export function boxExit(box: Box, toward: Point): Point {
|
|
@@ -20,7 +20,7 @@ export function boxExit(box: Box, toward: Point): Point {
|
|
|
20
20
|
export const straightRouter: Router = {
|
|
21
21
|
name: "straight",
|
|
22
22
|
route(input: Graph): Result {
|
|
23
|
-
const graph =
|
|
23
|
+
const graph = outerGraph(input);
|
|
24
24
|
const byId = nodeIndex(graph);
|
|
25
25
|
const routes: Route[] = [];
|
|
26
26
|
for (const e of graph.edges) {
|
package/src/routers/trunks.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { center, dist, type Point } from "../geom.ts";
|
|
2
|
-
import { nodeIndex,
|
|
2
|
+
import { nodeIndex, outerGraph, type Bundle, type Edge, type Graph, type GroupRule, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
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";
|
|
@@ -210,7 +210,7 @@ export function trunksRouter(options: TrunksOptions): Router {
|
|
|
210
210
|
const router: Router = {
|
|
211
211
|
name: options.name,
|
|
212
212
|
route(input: Graph): Result {
|
|
213
|
-
const graph =
|
|
213
|
+
const graph = outerGraph(input);
|
|
214
214
|
const byId = nodeIndex(graph);
|
|
215
215
|
const ways = new WayFinder(graph.nodes, options);
|
|
216
216
|
const points = new Map<string, readonly Point[]>();
|
|
@@ -74,6 +74,9 @@ import { ZIPPER_DEFAULTS, zipperPlan, type ZipperOptions } from "../highways/zip
|
|
|
74
74
|
import { depthLevels, type DepthLevelOptions } from "../levels/depths/index.ts";
|
|
75
75
|
import type { Graph, Result, Router } from "../model.ts";
|
|
76
76
|
import { facingEnds } from "../plugs/facing/index.ts";
|
|
77
|
+
import { justifyEnds, type JustifyOptions } from "../plugs/justify.ts";
|
|
78
|
+
import { sideEnds, type SidePlugOptions } from "../plugs/side.ts";
|
|
79
|
+
import { SPREAD_DEFAULTS, spreadEnds, type SpreadOptions } from "../plugs/spread/index.ts";
|
|
77
80
|
import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
|
|
78
81
|
import { levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
|
|
79
82
|
import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
|
|
@@ -87,7 +90,7 @@ import { runPipeline, type PipelineOptions } from "../whole/pipeline/index.ts";
|
|
|
87
90
|
import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type LaneBoard, type Registry, type Strategy } from "./model.ts";
|
|
88
91
|
import type { OptionSchema } from "./options.ts";
|
|
89
92
|
import { AREA_SCHEMA, CONFLUENT_SCHEMA, CROSSBAR_SCHEMA, FRECHET_SCHEMA, MLSEB_SCHEMA, POWER_SCHEMA, SBEB_SCHEMA } from "./schemas.ts";
|
|
90
|
-
import { DEPTHS_SCHEMA, PIPELINE_SCHEMA } from "./schemas.ts";
|
|
93
|
+
import { DEPTHS_SCHEMA, JUSTIFY_SCHEMA, PIPELINE_SCHEMA, SIDE_PLUG_SCHEMA, SPREAD_SCHEMA } from "./schemas.ts";
|
|
91
94
|
import { CLOTHOID_SCHEMA, CORNERS_SCHEMA, CORRIDOR_SCHEMA, CONSENSUS_SCHEMA, CUBU_SCHEMA, CUT_SCHEMA, DIFFUSION_SCHEMA, DIVIDED_SCHEMA, EDGE_PATH_SCHEMA, FDEB_SCHEMA, FLOW_CLUSTER_SCHEMA, FLOW_FIELD_SCHEMA, GROWTH_SCHEMA, HDBSCAN_SCHEMA, HIERARCHY_SCHEMA, HIGHWAY_SCHEMA, HOUGH_SCHEMA, HUB_ARC_SCHEMA, KD_SCHEMA, KDEEB_SCHEMA, KEY_SCHEMA, LANES_SCHEMA, LEVEL_HIGHWAY_SCHEMA, LEVEL_ROOM_SCHEMA, LINK_COMMUNITY_SCHEMA, MEET_SCHEMA, MESH_SCHEMA, MINGLE_SCHEMA, NESTED_SCHEMA, OD_CELL_SCHEMA, P_HUB_SCHEMA, QUICKBUNDLES_SCHEMA, RADIUS_SCHEMA, RESISTANCE_SCHEMA, RIDGE_SCHEMA, ROAD_CLASS_SCHEMA, ROOM_SCHEMA, SCALE_SPACE_SCHEMA, SECTION_SCHEMA, SELECT_SCHEMA, SECTIONS_SCHEMA, SHARED_ALL_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, SOFT_KEY_SCHEMA, SPECTRAL_SCHEMA, SPIRAL_SCHEMA, STUB_SCHEMA, TENSION_SCHEMA, TERRITORY_SCHEMA, TRACLUS_SCHEMA, TRUNKS_SCHEMA, WEISZFELD_SCHEMA, WSPD_SCHEMA, ZIPPER_SCHEMA, STEINER_SCHEMA, GAP_SCHEMA, STREET_SCHEMA, SEARCH_SCHEMA, INTERCHANGE_SCHEMA } from "./schemas.ts";
|
|
92
95
|
|
|
93
96
|
/** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
|
|
@@ -957,6 +960,9 @@ export const highwayStrategies: {
|
|
|
957
960
|
/** The built-in plug rules: the strategies of the step `plugs`. The router `corners` routes between their plugs with its option `plug`. */
|
|
958
961
|
export const plugStrategies: {
|
|
959
962
|
readonly facing: Strategy<"plugs", object>;
|
|
963
|
+
readonly side: Strategy<"plugs", SidePlugOptions>;
|
|
964
|
+
readonly justify: Strategy<"plugs", JustifyOptions>;
|
|
965
|
+
readonly spread: Strategy<"plugs", SpreadOptions>;
|
|
960
966
|
} = Object.freeze({
|
|
961
967
|
facing: defineStrategy({
|
|
962
968
|
step: "plugs",
|
|
@@ -966,6 +972,30 @@ export const plugStrategies: {
|
|
|
966
972
|
schema: {},
|
|
967
973
|
run: (input) => facingEnds(input),
|
|
968
974
|
}),
|
|
975
|
+
side: defineStrategy<"plugs", SidePlugOptions>({
|
|
976
|
+
step: "plugs",
|
|
977
|
+
name: "side",
|
|
978
|
+
describe: "WILD-001. The search of a route starts from the whole free side, and the plug goes where the way leaves the side. It is `corners` with `plugs: side`.",
|
|
979
|
+
defaults: { bend: 1, inset: 6, most: 16 },
|
|
980
|
+
schema: SIDE_PLUG_SCHEMA,
|
|
981
|
+
run: (input, o) => sideEnds(input, o),
|
|
982
|
+
}),
|
|
983
|
+
justify: defineStrategy<"plugs", JustifyOptions>({
|
|
984
|
+
step: "plugs",
|
|
985
|
+
name: "justify",
|
|
986
|
+
describe: "WILD-002. The plugs of `facing`, then the free plugs of each side keep their order and a minimum gap. They move the least distance.",
|
|
987
|
+
defaults: { plugGap: 8 },
|
|
988
|
+
schema: JUSTIFY_SCHEMA,
|
|
989
|
+
run: (input, o) => justifyEnds(input, o),
|
|
990
|
+
}),
|
|
991
|
+
spread: defineStrategy<"plugs", SpreadOptions>({
|
|
992
|
+
step: "plugs",
|
|
993
|
+
name: "spread",
|
|
994
|
+
describe: "PLUG-SPREAD. The free plugs of each wall share its room with a minimum gap, in the order of their far ends. On a short wall the gap shrinks, and no plug drops.",
|
|
995
|
+
defaults: { ...SPREAD_DEFAULTS },
|
|
996
|
+
schema: SPREAD_SCHEMA,
|
|
997
|
+
run: (input, o) => spreadEnds(input, o),
|
|
998
|
+
}),
|
|
969
999
|
});
|
|
970
1000
|
|
|
971
1001
|
/** The built-in level rules: the strategies of the step `levels`. The router `sections` makes its zoom levels with its option `level`. */
|