@mark1russell7/highways 0.8.1 → 0.9.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.
- package/README.md +112 -8
- package/build/index.d.ts +989 -187
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9761 -6738
- package/build/index.js.map +1 -1
- package/package.json +3 -2
- package/src/bench/compare.ts +174 -5
- package/src/bench/room.ts +1 -1
- package/src/bench/run.ts +38 -3
- package/src/bench/screen/exercise.ts +5 -0
- package/src/bench/screen/forms.ts +5 -1
- package/src/bench/screen/generators.ts +11 -41
- package/src/bench/screen/screen.ts +16 -10
- package/src/bench/screen/set.ts +156 -49
- package/src/bench/suite.ts +110 -12
- package/src/cases.ts +64 -18
- package/src/families/cases.ts +108 -0
- package/src/families/fixtures.ts +101 -0
- package/src/families/index.ts +36 -0
- package/src/families/model.ts +55 -0
- package/src/fixtures.ts +8 -2
- package/src/highways/fixtures.test-util.ts +13 -0
- package/src/highways/follow.ts +3 -2
- package/src/highways/free-lines.ts +141 -0
- package/src/highways/gaps/index.ts +85 -0
- package/src/highways/index.ts +6 -0
- package/src/highways/interchanges/index.ts +231 -0
- package/src/highways/search/index.ts +170 -0
- package/src/highways/snap.ts +4 -2
- package/src/highways/soft-keys/index.ts +8 -4
- package/src/highways/steiner/index.ts +215 -0
- package/src/highways/streets/index.ts +78 -0
- package/src/index.ts +8 -0
- package/src/lane-order.ts +37 -0
- package/src/lane-orders/stretch/index.ts +486 -0
- package/src/lane-orders/stretch/router.ts +22 -0
- package/src/levels/depths/index.ts +57 -0
- package/src/model.ts +60 -4
- package/src/plugs/facing/index.ts +22 -0
- package/src/portfolio/router.ts +44 -0
- package/src/routers/attraction/index.ts +61 -0
- package/src/routers/corners.ts +31 -7
- package/src/routers/index.ts +2 -1
- package/src/routers/plugs.ts +3 -2
- package/src/routers/sections.ts +25 -42
- package/src/routers/shared.ts +96 -14
- package/src/routers/straight.ts +3 -2
- package/src/routers/trunks.ts +24 -5
- package/src/segments.ts +4 -25
- package/src/strategies/builtins.ts +109 -2
- package/src/strategies/model.ts +55 -10
- package/src/strategies/options.ts +36 -4
- package/src/strategies/schemas.ts +124 -5
- package/src/whole/pipeline/index.ts +66 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { levelLinks, type ItemPair, type LevelLinkOptions } from "../../levels.ts";
|
|
2
|
+
import type { Level, Route } from "../../model.ts";
|
|
3
|
+
import { levelCut, treeDepth } from "../../sections.ts";
|
|
4
|
+
import type { LevelInput } from "../../strategies/model.ts";
|
|
5
|
+
|
|
6
|
+
/** The options of the level strategy `depths`: the limits of the level links, and the depths that get a level with groups. */
|
|
7
|
+
export interface DepthLevelOptions extends Omit<LevelLinkOptions, "smooth"> {
|
|
8
|
+
/**
|
|
9
|
+
* The depths of the tree that get a zoom level with groups. The last level, with each node alone, always comes after them.
|
|
10
|
+
* The default is each depth, from 0 to the depth of the deepest section.
|
|
11
|
+
*/
|
|
12
|
+
readonly levels?: readonly number[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This function gives the zoom levels of the level strategy `depths`.
|
|
17
|
+
* It gives one level for each depth of the tree, and a last level with each node alone.
|
|
18
|
+
* It is the default of the router `sections`. With the option `levels`, only those depths get a level with groups.
|
|
19
|
+
* At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
|
|
20
|
+
* In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
|
|
21
|
+
* In the last level, the points of a link are the route of its first edge.
|
|
22
|
+
*/
|
|
23
|
+
export function depthLevels(input: LevelInput, options: DepthLevelOptions = {}): Level[] {
|
|
24
|
+
const { graph, root } = input;
|
|
25
|
+
const routeOf = new Map<string, Route>(input.routes.map((r) => [r.edge, r]));
|
|
26
|
+
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
27
|
+
const levels: Level[] = [];
|
|
28
|
+
const deepest = treeDepth(root);
|
|
29
|
+
const shown = options.levels && new Set(options.levels);
|
|
30
|
+
for (let d = 0; d <= deepest + 1; d++) {
|
|
31
|
+
if (shown && d <= deepest && !shown.has(d)) continue;
|
|
32
|
+
const { items, owner } = levelCut(graph.nodes, root, d);
|
|
33
|
+
const links = new Map<string, { source: string; target: string; count: number; edge: string }>();
|
|
34
|
+
for (const e of graph.edges) {
|
|
35
|
+
const iu = indexOfNode.get(e.source.node);
|
|
36
|
+
const iv = indexOfNode.get(e.target.node);
|
|
37
|
+
if (iu === undefined || iv === undefined) continue;
|
|
38
|
+
const a = owner[iu]!;
|
|
39
|
+
const b = owner[iv]!;
|
|
40
|
+
if (a === b) continue;
|
|
41
|
+
const ida = items[a]!.id;
|
|
42
|
+
const idb = items[b]!.id;
|
|
43
|
+
const key = `${ida}\u0000${idb}`;
|
|
44
|
+
const link = links.get(key);
|
|
45
|
+
if (link) {
|
|
46
|
+
link.count++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
links.set(key, { source: ida, target: idb, count: 1, edge: e.id });
|
|
50
|
+
}
|
|
51
|
+
const pairs: ItemPair[] = [...links.values()].map(({ source, target, count }) => ({ source, target, count }));
|
|
52
|
+
if (d > deepest)
|
|
53
|
+
levels.push({ depth: d, items, links: [...links.values()].map(({ edge, ...l }) => ({ ...l, points: routeOf.get(edge)?.points ?? [] })) });
|
|
54
|
+
else levels.push({ depth: d, items, links: levelLinks(items, pairs, options) });
|
|
55
|
+
}
|
|
56
|
+
return levels;
|
|
57
|
+
}
|
package/src/model.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { portPoint } from "@mark1russell7/layout-contract";
|
|
1
2
|
import type { Box, Point } from "./geom.ts";
|
|
2
3
|
|
|
3
4
|
/** A side of a node box. */
|
|
@@ -5,7 +6,8 @@ export type Side = "north" | "east" | "south" | "west";
|
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* A plug of a node: a place where edges go into the node.
|
|
8
|
-
* A plug with `at` is fixed. The router does not move it. A plug
|
|
9
|
+
* A plug with `at` is fixed. The router does not move it. A plug with `side` and `along` is fixed too.
|
|
10
|
+
* Another plug is free: the router places it.
|
|
9
11
|
*/
|
|
10
12
|
export interface Port {
|
|
11
13
|
readonly id: string;
|
|
@@ -13,6 +15,12 @@ export interface Port {
|
|
|
13
15
|
readonly at?: Point;
|
|
14
16
|
/** This side is where a free plug goes. Without it, the router picks the side. */
|
|
15
17
|
readonly side?: Side;
|
|
18
|
+
/**
|
|
19
|
+
* This fraction from 0 to 1 fixes the plug on its `side`. The plug is at the side start plus `along` times the side length.
|
|
20
|
+
* The start of `north` and `south` is the left end. The start of `east` and `west` is the top end.
|
|
21
|
+
* A plug with `along` needs a `side` and has no `at`. `resolvePorts` throws a `RangeError` for a bad value.
|
|
22
|
+
*/
|
|
23
|
+
readonly along?: number;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
/** A node: a box that routes must not go through. */
|
|
@@ -131,10 +139,58 @@ export function nodeIndex(g: Graph): Map<string, Node> {
|
|
|
131
139
|
return m;
|
|
132
140
|
}
|
|
133
141
|
|
|
134
|
-
/**
|
|
142
|
+
/** The names of the sides in `@mark1russell7/layout-contract`. */
|
|
143
|
+
const CONTRACT_SIDE = { north: "top", east: "right", south: "bottom", west: "left" } as const;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* This function gives the place of a fixed plug, relative to the top-left corner of the node box, or `undefined` for a free plug.
|
|
147
|
+
* A plug with `at` gives `at`. A plug with `side` and `along` gives the point of `portPoint` of `@mark1russell7/layout-contract`.
|
|
148
|
+
*/
|
|
149
|
+
export function portOffset(box: Box, port: Port): Point | undefined {
|
|
150
|
+
if (port.at !== undefined) return port.at;
|
|
151
|
+
if (port.side === undefined || port.along === undefined) return undefined;
|
|
152
|
+
return portPoint(box, { id: port.id, side: CONTRACT_SIDE[port.side], along: port.along });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** This function throws a `RangeError` when the `along` of a plug is bad: not finite, outside 0 to 1, without `side` or with `at`. */
|
|
156
|
+
export function checkPort(node: Node, port: Port): void {
|
|
157
|
+
if (port.along === undefined) return;
|
|
158
|
+
const at = `The plug "${port.id}" of the node "${node.id}"`;
|
|
159
|
+
if (typeof port.along !== "number" || !Number.isFinite(port.along) || port.along < 0 || port.along > 1)
|
|
160
|
+
throw new RangeError(`${at} has the along value ${String(port.along)}. It must be a finite number from 0 to 1.`);
|
|
161
|
+
if (port.side === undefined) throw new RangeError(`${at} has an along value and no side. A plug with along needs a side.`);
|
|
162
|
+
if (port.at !== undefined) throw new RangeError(`${at} has an along value and an at point. Give one of the two.`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* This function checks the plugs of a graph, and gives each plug with `side` and `along` a fixed `at`. The plug keeps its `side`.
|
|
167
|
+
* Each router calls it at the start of `route`, so all routers agree. A graph without `along` comes back as the same object.
|
|
168
|
+
* It throws a `RangeError` for a bad `along` (`checkPort`).
|
|
169
|
+
*/
|
|
170
|
+
export function resolvePorts(graph: Graph): Graph {
|
|
171
|
+
let found = false;
|
|
172
|
+
for (const n of graph.nodes)
|
|
173
|
+
for (const p of n.ports ?? []) {
|
|
174
|
+
checkPort(n, p);
|
|
175
|
+
if (p.along !== undefined) found = true;
|
|
176
|
+
}
|
|
177
|
+
if (!found) return graph;
|
|
178
|
+
const nodes = graph.nodes.map((n): Node => {
|
|
179
|
+
if (!n.ports?.some((p) => p.along !== undefined)) return n;
|
|
180
|
+
const ports = n.ports.map((p): Port => {
|
|
181
|
+
if (p.along === undefined) return p;
|
|
182
|
+
return { id: p.id, side: p.side!, at: portOffset(n.box, p)! };
|
|
183
|
+
});
|
|
184
|
+
return { ...n, ports };
|
|
185
|
+
});
|
|
186
|
+
return { ...graph, nodes };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** This function gives the absolute place of a fixed plug, or `undefined` for a free plug or no plug. A plug with `side` and `along` is fixed. */
|
|
135
190
|
export function fixedPortPoint(node: Node, end: End): Point | undefined {
|
|
136
191
|
if (end.port === undefined) return undefined;
|
|
137
192
|
const p = node.ports?.find((q) => q.id === end.port);
|
|
138
|
-
|
|
139
|
-
|
|
193
|
+
const at = p === undefined ? undefined : portOffset(node.box, p);
|
|
194
|
+
if (at === undefined) return undefined;
|
|
195
|
+
return { x: node.box.x + at.x, y: node.box.y + at.y };
|
|
140
196
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { center } from "../../geom.ts";
|
|
2
|
+
import { nodeIndex } from "../../model.ts";
|
|
3
|
+
import { placePlug } from "../../plugs.ts";
|
|
4
|
+
import type { PlugPair, PlugInput } from "../../strategies/model.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This function gives the plugs of the plug strategy `facing`: each free plug faces the other end of its edge (`placePlug`).
|
|
8
|
+
* A fixed plug stays at its point, and a plug with a side stays on that side. It is the default of the router `corners`.
|
|
9
|
+
* The output has one entry for each edge with two known nodes, in the order of the edges.
|
|
10
|
+
*/
|
|
11
|
+
export function facingEnds(input: PlugInput): PlugPair[] {
|
|
12
|
+
const { graph } = input;
|
|
13
|
+
const byId = nodeIndex(graph);
|
|
14
|
+
const out: PlugPair[] = [];
|
|
15
|
+
for (const e of graph.edges) {
|
|
16
|
+
const s = byId.get(e.source.node);
|
|
17
|
+
const t = byId.get(e.target.node);
|
|
18
|
+
if (!s || !t) continue;
|
|
19
|
+
out.push({ edge: e.id, source: placePlug(s, e.source, center(t.box)), target: placePlug(t, e.target, center(s.box)) });
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
package/src/portfolio/router.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { pickMoves, portfolio, type Member, type PortfolioTrace } from "../bench/screen/portfolio-pick.ts";
|
|
5
5
|
import type { Graph, Result, Router } from "../model.ts";
|
|
6
6
|
import { asRouter, defineStrategy, type Strategy } from "../strategies/model.ts";
|
|
7
|
+
import type { OptionSchema } from "../strategies/options.ts";
|
|
7
8
|
import { PORTFOLIO_CAPS, PORTFOLIO_KS_API, PORTFOLIO_RULES, portfolioBand, type PortfolioCap, type PortfolioRule } from "./bands.ts";
|
|
8
9
|
import { PORTFOLIO_FIT } from "./fit.ts";
|
|
9
10
|
import { portfolioKey } from "./key.ts";
|
|
@@ -135,6 +136,47 @@ export function portfolioRouter(options: PortfolioRouterOptions = {}): Router {
|
|
|
135
136
|
return { name: portfolioName(options), maxNodes: 10_000, route: (graph) => routePortfolio(graph, options).result };
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
/** The largest count of members after the base: the largest list of the fit and of the sets. */
|
|
140
|
+
const MAX_AFTER_BASE = Math.max(...PORTFOLIO_KS_API, ...Object.values(PORTFOLIO_FIT).map((l) => l.length), ...Object.values(PORTFOLIO_SETS).map((l) => l.length));
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The declarations of the options of the portfolio router (SCHEMA-GAPS). The lists of values come from the constants, so a new rule or cap needs no change here.
|
|
144
|
+
* The option `now` has no declaration. It is a clock for the tests, not a choice of the layout, and no control or sweep can set a function.
|
|
145
|
+
*/
|
|
146
|
+
const PORTFOLIO_SCHEMA: OptionSchema<Omit<PortfolioRouterOptions, "now">> = {
|
|
147
|
+
rule: { type: "enum", values: PORTFOLIO_RULES, default: "collection", describe: "With `collection`, a shared segment holds edges of one collection only. With `all`, collections can mix." },
|
|
148
|
+
cap: {
|
|
149
|
+
type: "number",
|
|
150
|
+
min: Math.min(...PORTFOLIO_CAPS),
|
|
151
|
+
max: Math.max(...PORTFOLIO_CAPS),
|
|
152
|
+
values: PORTFOLIO_CAPS,
|
|
153
|
+
default: 1.5,
|
|
154
|
+
describe: "The stretch cap. An output with an edge above the cap does not win.",
|
|
155
|
+
},
|
|
156
|
+
k: {
|
|
157
|
+
type: "integer",
|
|
158
|
+
min: Math.min(...PORTFOLIO_KS_API),
|
|
159
|
+
max: Math.max(...PORTFOLIO_KS_API),
|
|
160
|
+
values: PORTFOLIO_KS_API,
|
|
161
|
+
describe: "The count of members after the base. Without it, the router uses the fit of the size band.",
|
|
162
|
+
},
|
|
163
|
+
budgetMs: {
|
|
164
|
+
type: "integer",
|
|
165
|
+
min: 0,
|
|
166
|
+
max: 10_000,
|
|
167
|
+
step: 10,
|
|
168
|
+
describe: "The time limit of the pick in milliseconds. Without it, there is no limit. A cut by the limit makes the output depend on the machine.",
|
|
169
|
+
},
|
|
170
|
+
maxMembers: {
|
|
171
|
+
type: "integer",
|
|
172
|
+
min: 1,
|
|
173
|
+
max: 1 + MAX_AFTER_BASE,
|
|
174
|
+
step: 1,
|
|
175
|
+
describe: "The largest count of members that run, with the base. Without it, each member of the pick runs.",
|
|
176
|
+
},
|
|
177
|
+
hashes: { type: "enum", values: PORTFOLIO_HASHES, default: "pick", describe: "With `pick`, only the base and the winner get an output hash in the trace. With `all`, each member gets one." },
|
|
178
|
+
};
|
|
179
|
+
|
|
138
180
|
/**
|
|
139
181
|
* The portfolio router as a router strategy. It is not in the registry `STRATEGIES`, so the benchmark and the site do not race it by default.
|
|
140
182
|
* A caller adds it to a registry of its own, or calls `routePortfolio` for the trace.
|
|
@@ -144,6 +186,8 @@ export const portfolioStrategy: Strategy<"router", PortfolioRouterOptions> = def
|
|
|
144
186
|
name: "portfolio",
|
|
145
187
|
describe: "The portfolio router (PORTFOLIO-API). It runs `shared` and the fit of the size band, and gives the output with the most ink saved inside the rule and the cap.",
|
|
146
188
|
defaults: { rule: "collection", cap: 1.5 },
|
|
189
|
+
// The schema leaves out the clock `now`, so its type is the options without `now`.
|
|
190
|
+
schema: PORTFOLIO_SCHEMA as OptionSchema<PortfolioRouterOptions>,
|
|
147
191
|
maxNodes: 10_000,
|
|
148
192
|
run: (graph, options) => routePortfolio(graph, options).result,
|
|
149
193
|
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Graph, GroupRule, Result, Router } from "../../model.ts";
|
|
2
|
+
import { defineStrategy, type Strategy } from "../../strategies/model.ts";
|
|
3
|
+
import type { OptionSchema } from "../../strategies/options.ts";
|
|
4
|
+
import { sharedRouter } from "../shared.ts";
|
|
5
|
+
import { byCollection } from "../trunks.ts";
|
|
6
|
+
|
|
7
|
+
/** The options of the router `attraction` (HWY-044 of the note on highways: negotiated attraction, PathFinder turned around). */
|
|
8
|
+
export interface AttractionOptions {
|
|
9
|
+
/** The number of passes, from 1 to 10. Each pass after the first routes each edge again from no ways. The default is 3. */
|
|
10
|
+
readonly passes: number;
|
|
11
|
+
/** The pull `a` of the history of a link, from 0 to 1. A link then costs `1 - a h` times more. The default is 0.5. */
|
|
12
|
+
readonly attraction: number;
|
|
13
|
+
/** The cost weight `b` of each edge over the capacity of a link, from 0 to 10. The default is 1. */
|
|
14
|
+
readonly capacityWeight: number;
|
|
15
|
+
/** The width of one lane. The capacity of a link is its free width divided by this width. The default is 4. */
|
|
16
|
+
readonly laneWidth: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The default options of the router `attraction`. */
|
|
20
|
+
export const ATTRACTION_DEFAULTS: Readonly<AttractionOptions> = Object.freeze({ passes: 3, attraction: 0.5, capacityWeight: 1, laneWidth: 4 });
|
|
21
|
+
|
|
22
|
+
/** The declarations of the options of the router `attraction` (HWY-044). */
|
|
23
|
+
export const ATTRACTION_SCHEMA: OptionSchema<AttractionOptions> = {
|
|
24
|
+
passes: { type: "integer", min: 1, max: 10, step: 1, marks: [2, 3, 5], default: 3, describe: "The number of passes. Each later pass routes each edge again from no ways." },
|
|
25
|
+
attraction: { type: "number", min: 0, max: 1, step: 0.05, marks: [0.25, 0.5, 0.75], default: 0.5, describe: "The pull of the history of a link. A busy link of the passes before costs less." },
|
|
26
|
+
capacityWeight: { type: "number", min: 0, max: 10, step: 0.5, marks: [0, 1, 4], default: 1, describe: "The cost weight of each edge over the capacity of a link." },
|
|
27
|
+
laneWidth: { type: "number", min: 1, max: 12, step: 1, marks: [4, 6], default: 4, describe: "The width of one lane. The capacity of a link is its free width divided by this width." },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* This function makes the router `attraction` (HWY-044). It is the router `shared` with a negotiation over the passes.
|
|
32
|
+
* After each pass, the history `h` of each link grows by `use / m`, with `m` the edges of the key. The history stops at `0.9 / a`.
|
|
33
|
+
* In each pass, a link costs `len (1 - a h) (1 + b max(0, use - cap))` times the cost in `shared`.
|
|
34
|
+
* The busy links of the passes before thus pull the edges, and a link over its capacity pushes them away.
|
|
35
|
+
*/
|
|
36
|
+
export function attractionRouter(options: Partial<AttractionOptions> & { readonly name: string; readonly rule?: GroupRule; readonly maxNodes?: number }): Router {
|
|
37
|
+
const o = { ...ATTRACTION_DEFAULTS, ...options };
|
|
38
|
+
return sharedRouter(
|
|
39
|
+
{ name: o.name, ...(o.rule === undefined ? {} : { rule: o.rule }), ...(o.maxNodes === undefined ? {} : { maxNodes: o.maxNodes }) },
|
|
40
|
+
{
|
|
41
|
+
passes: Math.max(1, Math.floor(o.passes)),
|
|
42
|
+
attraction: Math.min(1, Math.max(0, o.attraction)),
|
|
43
|
+
capacityWeight: Math.max(0, o.capacityWeight),
|
|
44
|
+
laneWidth: Math.max(1e-6, o.laneWidth),
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The router strategy `attraction` (HWY-044), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
|
|
51
|
+
* A screen module or a consumer adds it to a registry.
|
|
52
|
+
*/
|
|
53
|
+
export const attractionStrategy: Strategy<"router", AttractionOptions> = defineStrategy({
|
|
54
|
+
step: "router",
|
|
55
|
+
name: "attraction",
|
|
56
|
+
describe: "Negotiated attraction (HWY-044). The router `shared` routes each edge again in each pass, and the busy links of the passes before pull the edges.",
|
|
57
|
+
defaults: { ...ATTRACTION_DEFAULTS },
|
|
58
|
+
schema: ATTRACTION_SCHEMA,
|
|
59
|
+
maxNodes: 10_000,
|
|
60
|
+
run: (graph: Graph, o: Readonly<AttractionOptions>): Result => attractionRouter({ ...o, name: "attraction", rule: byCollection }).route(graph),
|
|
61
|
+
});
|
package/src/routers/corners.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
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";
|
|
2
|
+
import { nodeIndex, fixedPortPoint, resolvePorts, 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
4
|
import { justifyPlugs } from "../plugs/justify.ts";
|
|
5
5
|
import { sidePlugs } from "../plugs/side.ts";
|
|
6
|
+
import type { PlugPair, Strategy } from "../strategies/model.ts";
|
|
6
7
|
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
7
8
|
|
|
8
9
|
/** The ways to place a free plug of the corner router. */
|
|
@@ -24,6 +25,13 @@ export interface CornersOptions extends WayOptions {
|
|
|
24
25
|
* Above 0, the free plugs of each side keep their order and move the least distance (`justifyPlugs`, WILD-002).
|
|
25
26
|
*/
|
|
26
27
|
readonly plugGap?: number;
|
|
28
|
+
/**
|
|
29
|
+
* The plug rule: a strategy of the step `plugs` that places the plugs of each edge. It takes the place of the option `plugs`.
|
|
30
|
+
* Without it, the router places the plugs with the option `plugs`. An edge without plugs from the strategy gets the plugs of `facing`.
|
|
31
|
+
*/
|
|
32
|
+
readonly plug?: Strategy<"plugs">;
|
|
33
|
+
/** The options of the strategy `plug`. The strategy gets its `defaults` for each option that this object does not give. */
|
|
34
|
+
readonly plugOptions?: object;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/**
|
|
@@ -34,11 +42,12 @@ export interface CornersOptions extends WayOptions {
|
|
|
34
42
|
export function cornersRouter(options: CornersOptions): Router {
|
|
35
43
|
const router: Router = {
|
|
36
44
|
name: options.name,
|
|
37
|
-
route(
|
|
45
|
+
route(input: Graph): Result {
|
|
46
|
+
const graph = resolvePorts(input);
|
|
38
47
|
const byId = nodeIndex(graph);
|
|
39
48
|
const ways = new WayFinder(graph.nodes, options);
|
|
40
49
|
const routes: Route[] = [];
|
|
41
|
-
if ((options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
|
|
50
|
+
if (!options.plug && (options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
|
|
42
51
|
for (const e of graph.edges) {
|
|
43
52
|
const s = byId.get(e.source.node);
|
|
44
53
|
const t = byId.get(e.target.node);
|
|
@@ -51,12 +60,17 @@ export function cornersRouter(options: CornersOptions): Router {
|
|
|
51
60
|
}
|
|
52
61
|
// First the plugs of each edge, then the gaps of the plugs of each side, then the ways between the plugs.
|
|
53
62
|
const ends: [Edge, Node, Node, Plug, Plug][] = [];
|
|
63
|
+
const placed = options.plug ? plugsByEdge(options.plug.run({ graph, ways }, options.plugOptions)) : undefined;
|
|
54
64
|
for (const e of graph.edges) {
|
|
55
65
|
const s = byId.get(e.source.node);
|
|
56
66
|
const t = byId.get(e.target.node);
|
|
57
67
|
if (!s || !t) continue;
|
|
58
|
-
const
|
|
59
|
-
|
|
68
|
+
const own = placed?.get(e.id)?.shift();
|
|
69
|
+
const [ps, pt]: [Plug, Plug] = own
|
|
70
|
+
? [own.source, own.target]
|
|
71
|
+
: options.plugs === "side" && !placed
|
|
72
|
+
? sidePlugs(ways, s, e.source, t, e.target)
|
|
73
|
+
: [placePlug(s, e.source, center(t.box)), placePlug(t, e.target, center(s.box))];
|
|
60
74
|
ends.push([e, s, t, ps, pt]);
|
|
61
75
|
}
|
|
62
76
|
if ((options.plugGap ?? 0) > 0) spacePlugs(ends, options.plugGap!);
|
|
@@ -67,6 +81,17 @@ export function cornersRouter(options: CornersOptions): Router {
|
|
|
67
81
|
return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
|
|
68
82
|
}
|
|
69
83
|
|
|
84
|
+
/** This function gives the plugs of a plug strategy by the edge id. Two edges with one id take the entries of that id in order. */
|
|
85
|
+
function plugsByEdge(list: readonly PlugPair[]): Map<string, PlugPair[]> {
|
|
86
|
+
const out = new Map<string, PlugPair[]>();
|
|
87
|
+
for (const p of list) {
|
|
88
|
+
const own = out.get(p.edge);
|
|
89
|
+
if (own) own.push(p);
|
|
90
|
+
else out.set(p.edge, [p]);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
70
95
|
/**
|
|
71
96
|
* This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
|
|
72
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.
|
|
@@ -80,8 +105,7 @@ function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number): void {
|
|
|
80
105
|
[e.source, s, ps, pt, 3],
|
|
81
106
|
[e.target, t, pt, ps, 4],
|
|
82
107
|
] as const) {
|
|
83
|
-
|
|
84
|
-
if (port?.at !== undefined) continue;
|
|
108
|
+
if (fixedPortPoint(node, end) !== undefined) continue;
|
|
85
109
|
const key = JSON.stringify([node.id, plug.normal.x, plug.normal.y]);
|
|
86
110
|
let side = sides.get(key);
|
|
87
111
|
if (!side) sides.set(key, (side = { node, normal: plug.normal, items: [] }));
|
package/src/routers/index.ts
CHANGED
|
@@ -9,7 +9,8 @@ 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 ShareCost, type SharedOptions, type ShareMerge, type ShareOrder } from "./shared.ts";
|
|
12
|
+
export { sharedRouter, type ShareCost, type SharedNegotiation, type SharedOptions, type ShareMerge, type ShareOrder } from "./shared.ts";
|
|
13
|
+
export { ATTRACTION_DEFAULTS, ATTRACTION_SCHEMA, attractionRouter, attractionStrategy, type AttractionOptions } from "./attraction/index.ts";
|
|
13
14
|
export { sectionsRouter, type SectionsOptions } from "./sections.ts";
|
|
14
15
|
export { byCollection, groupEdges, laneOrder, splitPoint, trunksRouter, type TrunkFan, type TrunkSplit, type TrunksOptions } from "./trunks.ts";
|
|
15
16
|
|
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, type Graph, type Result, type Route, type Router } from "../model.ts";
|
|
3
|
+
import { nodeIndex, resolvePorts, type Graph, type Result, type Route, type Router } from "../model.ts";
|
|
4
4
|
import { placePlug } from "../plugs.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -9,7 +9,8 @@ import { placePlug } from "../plugs.ts";
|
|
|
9
9
|
*/
|
|
10
10
|
export const plugsRouter: Router = {
|
|
11
11
|
name: "plugs",
|
|
12
|
-
route(
|
|
12
|
+
route(input: Graph): Result {
|
|
13
|
+
const graph = resolvePorts(input);
|
|
13
14
|
const byId = nodeIndex(graph);
|
|
14
15
|
const routes: Route[] = [];
|
|
15
16
|
for (const e of graph.edges) {
|
package/src/routers/sections.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { BACKTRACK_MIN, goesOutAndBack } from "../bench/links.ts";
|
|
2
2
|
import { center, dist, pointSegmentDistance, polylineLength, type Point } from "../geom.ts";
|
|
3
|
-
import { LEVEL_LINK_DEFAULTS,
|
|
4
|
-
import {
|
|
3
|
+
import { LEVEL_LINK_DEFAULTS, type LevelLinkOptions } from "../levels.ts";
|
|
4
|
+
import { depthLevels, type DepthLevelOptions } from "../levels/depths/index.ts";
|
|
5
|
+
import { nodeIndex, resolvePorts, type Graph, type Level, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
5
6
|
import { placePlug } from "../plugs.ts";
|
|
6
|
-
import { allSections, buildSections, checkSections,
|
|
7
|
+
import { allSections, buildSections, checkSections, SECTION_DEFAULTS, type Section, type SectionOptions } from "../sections.ts";
|
|
7
8
|
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
8
9
|
import type { Strategy } from "../strategies/model.ts";
|
|
9
10
|
import { WayFinder } from "../ways.ts";
|
|
@@ -45,6 +46,14 @@ export interface SectionsOptions extends LevelLinkOptions, SectionOptions {
|
|
|
45
46
|
* The default is each depth, from 0 to the depth of the deepest section.
|
|
46
47
|
*/
|
|
47
48
|
readonly levels?: readonly number[];
|
|
49
|
+
/**
|
|
50
|
+
* The level rule: a strategy of the step `levels` that makes the zoom levels from the tree and the routes.
|
|
51
|
+
* Without it, the router uses the strategy `depths` (`depthLevels`). The strategy gets the options `levels`, `margin`, `k`, `tightMargin`, `plugInset` and `throughCost` of the router,
|
|
52
|
+
* and then `levelOptions`.
|
|
53
|
+
*/
|
|
54
|
+
readonly level?: Strategy<"levels">;
|
|
55
|
+
/** The options of the strategy `level`. They go over the level options of the router. */
|
|
56
|
+
readonly levelOptions?: object;
|
|
48
57
|
/** The lane order of the runs. With `run`, the default, each run gets its order alone. With `bundle`, two edges keep one order on each run of their common stretch. */
|
|
49
58
|
readonly laneOrder?: RunOrder;
|
|
50
59
|
/** The benchmark skips a fixture with more nodes. */
|
|
@@ -82,7 +91,8 @@ const dedupe = (p: readonly Point[]): Point[] => p.filter((q, i) => i === 0 || d
|
|
|
82
91
|
export function sectionsRouter(options: SectionsOptions): Router {
|
|
83
92
|
const router: Router = {
|
|
84
93
|
name: options.name,
|
|
85
|
-
route(
|
|
94
|
+
route(input: Graph): Result {
|
|
95
|
+
const graph = resolvePorts(input);
|
|
86
96
|
const stretch = options.stretch ?? SECTIONS_DEFAULTS.stretch;
|
|
87
97
|
const stretchPad = options.stretchPad ?? SECTIONS_DEFAULTS.stretchPad;
|
|
88
98
|
const hubSlack = options.hubSlack ?? SECTIONS_DEFAULTS.hubSlack;
|
|
@@ -210,7 +220,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
210
220
|
if (list.length < 2 || p.length < 2) continue;
|
|
211
221
|
runs.push({ id: `r${runs.length}`, key, points: p, edges: orderLanes(p, list.map((id) => routeOf.get(id)!), options.laneOrder) });
|
|
212
222
|
}
|
|
213
|
-
return { routes, runs, levels: makeLevels(graph, root,
|
|
223
|
+
return { routes, runs, levels: makeLevels(graph, root, routes, options) };
|
|
214
224
|
},
|
|
215
225
|
};
|
|
216
226
|
return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
|
|
@@ -224,43 +234,16 @@ function groupNodes(graph: Graph, options: SectionsOptions): Section {
|
|
|
224
234
|
return root;
|
|
225
235
|
}
|
|
226
236
|
|
|
237
|
+
/** The keys of the router options that the level rule gets. */
|
|
238
|
+
const LEVEL_KEYS = ["levels", "margin", "k", "tightMargin", "plugInset", "throughCost"] as const;
|
|
239
|
+
|
|
227
240
|
/**
|
|
228
|
-
* This function gives
|
|
229
|
-
*
|
|
230
|
-
* At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
|
|
231
|
-
* In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
|
|
232
|
-
* In the last level, the points of a link are the route of its first edge.
|
|
241
|
+
* This function gives the zoom levels of the router: the levels of the strategy `level`, or else the levels of `depthLevels`.
|
|
242
|
+
* The strategy gets the level options of the router that have a value, and then `levelOptions`.
|
|
233
243
|
*/
|
|
234
|
-
function makeLevels(graph: Graph, root: Section,
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
for (let d = 0; d <= deepest + 1; d++) {
|
|
240
|
-
if (shown && d <= deepest && !shown.has(d)) continue;
|
|
241
|
-
const { items, owner } = levelCut(graph.nodes, root, d);
|
|
242
|
-
const links = new Map<string, { source: string; target: string; count: number; edge: string }>();
|
|
243
|
-
for (const e of graph.edges) {
|
|
244
|
-
const iu = indexOfNode.get(e.source.node);
|
|
245
|
-
const iv = indexOfNode.get(e.target.node);
|
|
246
|
-
if (iu === undefined || iv === undefined) continue;
|
|
247
|
-
const a = owner[iu]!;
|
|
248
|
-
const b = owner[iv]!;
|
|
249
|
-
if (a === b) continue;
|
|
250
|
-
const ida = items[a]!.id;
|
|
251
|
-
const idb = items[b]!.id;
|
|
252
|
-
const key = `${ida}\u0000${idb}`;
|
|
253
|
-
const link = links.get(key);
|
|
254
|
-
if (link) {
|
|
255
|
-
link.count++;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
links.set(key, { source: ida, target: idb, count: 1, edge: e.id });
|
|
259
|
-
}
|
|
260
|
-
const pairs: ItemPair[] = [...links.values()].map(({ source, target, count }) => ({ source, target, count }));
|
|
261
|
-
if (d > deepest)
|
|
262
|
-
levels.push({ depth: d, items, links: [...links.values()].map(({ edge, ...l }) => ({ ...l, points: routeOf.get(edge)?.points ?? [] })) });
|
|
263
|
-
else levels.push({ depth: d, items, links: levelLinks(items, pairs, options) });
|
|
264
|
-
}
|
|
265
|
-
return levels;
|
|
244
|
+
function makeLevels(graph: Graph, root: Section, routes: readonly Route[], options: SectionsOptions): Level[] {
|
|
245
|
+
if (!options.level) return depthLevels({ graph, root, routes }, options);
|
|
246
|
+
const own: Record<string, unknown> = {};
|
|
247
|
+
for (const key of LEVEL_KEYS) if (options[key] !== undefined) own[key] = options[key];
|
|
248
|
+
return options.level.run({ graph, root, routes }, { ...(own as DepthLevelOptions), ...options.levelOptions });
|
|
266
249
|
}
|