@mark1russell7/highways 0.1.4 → 0.1.5
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 +65 -0
- package/build/index.d.ts +217 -10
- package/build/index.d.ts.map +1 -1
- package/build/index.js +500 -44
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/curves.ts +35 -8
- package/src/bench/room.ts +83 -15
- package/src/index.ts +2 -0
- package/src/room/index.ts +22 -2
- package/src/room/lanes.ts +175 -0
- package/src/routers/index.ts +1 -1
- package/src/strategies/builtins.ts +169 -0
- package/src/strategies/index.ts +2 -0
- package/src/strategies/model.ts +157 -0
package/package.json
CHANGED
package/src/bench/curves.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CURVE_TYPES, curveSpace, sampleCurve, type CurveType } from "../curves/index.ts";
|
|
2
2
|
import { polylineLength } from "../geom.ts";
|
|
3
3
|
import { nodeIndex, type Graph, type Router } from "../model.ts";
|
|
4
4
|
import { nearestSide, SIDE_NORMAL } from "../plugs.ts";
|
|
5
|
+
import { curveStrategies } from "../strategies/builtins.ts";
|
|
6
|
+
import type { Strategy } from "../strategies/model.ts";
|
|
5
7
|
import { measureLinks, type MeasuredLink } from "./links.ts";
|
|
6
8
|
|
|
7
9
|
// The core runs in any host, so it reads the clock without the types of Node or the browser.
|
|
8
10
|
const now = (): number => (globalThis as { performance?: { now(): number } }).performance?.now() ?? Date.now();
|
|
9
11
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* One row of the benchmark of the curves: one curve type on the routes of one router on one fixture.
|
|
14
|
+
* The type `T` of the name of the curve is `CurveType` for `benchCurves`, and `string` for `benchCurveStrategies`.
|
|
15
|
+
*/
|
|
16
|
+
export interface CurveBenchRow<T extends string = CurveType> {
|
|
12
17
|
readonly router: string;
|
|
13
18
|
readonly fixture: string;
|
|
14
|
-
readonly type:
|
|
19
|
+
readonly type: T;
|
|
15
20
|
/** The number of routes. */
|
|
16
21
|
readonly links: number;
|
|
17
22
|
/** The number of polylines that go through a box that is not one of their ends (`measureLinks`). */
|
|
@@ -40,6 +45,27 @@ export interface CurveBenchOptions {
|
|
|
40
45
|
* The type `spline` leaves each plug along the normal of the nearest side of its node.
|
|
41
46
|
*/
|
|
42
47
|
export function benchCurves(router: Router, fixture: string, graph: Graph, options: CurveBenchOptions = {}): CurveBenchRow[] {
|
|
48
|
+
// Each built-in curve strategy has the name of its curve type, so each row has a `CurveType`.
|
|
49
|
+
return benchCurveStrategies(
|
|
50
|
+
router,
|
|
51
|
+
fixture,
|
|
52
|
+
graph,
|
|
53
|
+
CURVE_TYPES.map((t) => curveStrategies[t]),
|
|
54
|
+
options,
|
|
55
|
+
) as CurveBenchRow[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* This function does the work of `benchCurves` with a list of curve strategies, in place of the curve types.
|
|
60
|
+
* The option `radius` goes to each strategy. A strategy without such an option does not read it.
|
|
61
|
+
*/
|
|
62
|
+
export function benchCurveStrategies(
|
|
63
|
+
router: Router,
|
|
64
|
+
fixture: string,
|
|
65
|
+
graph: Graph,
|
|
66
|
+
curves: readonly Strategy<"curve">[],
|
|
67
|
+
options: CurveBenchOptions = {},
|
|
68
|
+
): CurveBenchRow<string>[] {
|
|
43
69
|
const tolerance = options.tolerance ?? 0.25;
|
|
44
70
|
const result = router.route(graph);
|
|
45
71
|
const byId = nodeIndex(graph);
|
|
@@ -60,13 +86,14 @@ export function benchCurves(router: Router, fixture: string, graph: Graph, optio
|
|
|
60
86
|
});
|
|
61
87
|
const base = measureLinks(boxes, routes);
|
|
62
88
|
const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
|
|
63
|
-
|
|
89
|
+
const radius = { radius: options.radius ?? 12 };
|
|
90
|
+
return curves.map((curve): CurveBenchRow<string> => {
|
|
64
91
|
const make = (withSpace: boolean): MeasuredLink[] =>
|
|
65
92
|
routes.map((r) => ({
|
|
66
93
|
source: r.source,
|
|
67
94
|
target: r.target,
|
|
68
95
|
points: sampleCurve(
|
|
69
|
-
|
|
96
|
+
curve.run({ points: r.points, startNormal: r.startNormal, endNormal: r.endNormal, ...(withSpace ? { space } : {}) }, radius),
|
|
70
97
|
tolerance,
|
|
71
98
|
),
|
|
72
99
|
}));
|
|
@@ -78,7 +105,7 @@ export function benchCurves(router: Router, fixture: string, graph: Graph, optio
|
|
|
78
105
|
return {
|
|
79
106
|
router: router.name,
|
|
80
107
|
fixture,
|
|
81
|
-
type,
|
|
108
|
+
type: curve.name,
|
|
82
109
|
links: routes.length,
|
|
83
110
|
through: base.through,
|
|
84
111
|
free: free.through,
|
|
@@ -90,7 +117,7 @@ export function benchCurves(router: Router, fixture: string, graph: Graph, optio
|
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
/** This function writes the rows of `benchCurves` as a Markdown table. */
|
|
93
|
-
export function formatCurves(rows: readonly CurveBenchRow[]): string {
|
|
120
|
+
export function formatCurves(rows: readonly CurveBenchRow<string>[]): string {
|
|
94
121
|
const head = "| router | fixture | curve | routes | through, polyline | through, free | through, clamped | length | ms |";
|
|
95
122
|
const line = "|---|---|---|---:|---:|---:|---:|---:|---:|";
|
|
96
123
|
const body = rows.map((r) => `| ${r.router} | ${r.fixture} | ${r.type} | ${r.links} | ${r.through} | ${r.free} | ${r.clamped} | ${r.length.toFixed(3)} | ${r.ms.toFixed(1)} |`);
|
package/src/bench/room.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { levelLinks } from "../levels.ts";
|
|
2
2
|
import type { Graph, Node, Result } from "../model.ts";
|
|
3
|
-
import { makeRoom, roomLinks, type RoomOptions } from "../room/index.ts";
|
|
3
|
+
import { makeRoom, openGaps, roomLinks, type RoomOptions } from "../room/index.ts";
|
|
4
|
+
import { gapLanes, LANE_ORDERS, type LaneOrder } from "../room/lanes.ts";
|
|
4
5
|
import { sectionsRouter } from "../routers/sections.ts";
|
|
5
6
|
import { laneRoutes, runKey, segmentsOf } from "../segments.ts";
|
|
7
|
+
import { roomStrategies } from "../strategies/builtins.ts";
|
|
8
|
+
import type { Strategy } from "../strategies/model.ts";
|
|
6
9
|
import { measureLinks, measureRouteFaults, type LinkFaults } from "./links.ts";
|
|
7
10
|
import { measureQuality } from "./quality.ts";
|
|
8
11
|
|
|
@@ -50,15 +53,18 @@ function areaOf(nodes: readonly Node[]): number {
|
|
|
50
53
|
|
|
51
54
|
/**
|
|
52
55
|
* This function measures the links of one board. It measures the links of the far view, with each node of the graph as one item of a level.
|
|
53
|
-
*
|
|
56
|
+
* With a lane order, each link of the far view gets its own lane in each gap (`gapLanes` at the lane width `spacing`).
|
|
57
|
+
* Without it, the links of a gap share the lines of the level links.
|
|
58
|
+
* The function also measures the lane routes of the router `sections` at the lane width `spacing`.
|
|
54
59
|
*/
|
|
55
|
-
export function measureBoard(graph: Graph, spacing: number): Omit<RoomBenchRow, "fixture" | "board" | "area" | "ms"> {
|
|
60
|
+
export function measureBoard(graph: Graph, spacing: number, order?: LaneOrder): Omit<RoomBenchRow, "fixture" | "board" | "area" | "ms"> {
|
|
56
61
|
const links = roomLinks(graph);
|
|
57
62
|
const items = graph.nodes.map((n) => ({ id: n.id, box: n.box, nodes: [n.id] }));
|
|
58
|
-
const
|
|
63
|
+
const level = levelLinks(
|
|
59
64
|
items,
|
|
60
65
|
links.map((l) => ({ source: l.source, target: l.target, count: l.count ?? 1 })),
|
|
61
66
|
);
|
|
67
|
+
const drawn = order ? gapLanes(graph.nodes, level, { spacing, order }) : level;
|
|
62
68
|
const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
|
|
63
69
|
// `measureQuality` reads routes of edges, so each link of the far view stands for the first edge of its pair.
|
|
64
70
|
const edgeOf = new Map<string, string>();
|
|
@@ -82,10 +88,15 @@ export function measureBoard(graph: Graph, spacing: number): Omit<RoomBenchRow,
|
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
/**
|
|
85
|
-
* This function measures a packed board, the board with each gap open to the gutter only, and the board that
|
|
86
|
-
* The options
|
|
91
|
+
* This function measures a packed board, the board with each gap open to the gutter only, and the board that each room strategy opens.
|
|
92
|
+
* The options are the same for each strategy, except `method`. The default strategies are the methods `routes` and `lines` of `roomDemand`.
|
|
87
93
|
*/
|
|
88
|
-
export function benchRoom(
|
|
94
|
+
export function benchRoom(
|
|
95
|
+
fixture: string,
|
|
96
|
+
graph: Graph,
|
|
97
|
+
options: RoomOptions = {},
|
|
98
|
+
methods: readonly Strategy<"room">[] = [roomStrategies.routes, roomStrategies.lines],
|
|
99
|
+
): RoomBenchRow[] {
|
|
89
100
|
const spacing = options.spacing ?? 4;
|
|
90
101
|
const area0 = areaOf(graph.nodes);
|
|
91
102
|
const links = roomLinks(graph);
|
|
@@ -97,19 +108,76 @@ export function benchRoom(fixture: string, graph: Graph, options: RoomOptions =
|
|
|
97
108
|
...measureBoard({ nodes, edges: graph.edges }, spacing),
|
|
98
109
|
});
|
|
99
110
|
const rows = [row("packed", graph.nodes, 0)];
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
const t0 = now();
|
|
112
|
+
const gutter = makeRoom(graph.nodes, links, { ...options, spacing: 0, method: "routes" });
|
|
113
|
+
rows.push(row("gutter", gutter.items, now() - t0));
|
|
114
|
+
const { method: _method, ...preset } = options;
|
|
115
|
+
for (const s of methods) {
|
|
116
|
+
const t1 = now();
|
|
117
|
+
const gaps = s.run({ items: graph.nodes, links }, preset);
|
|
118
|
+
const nodes = openGaps(graph.nodes, gaps, options.snap);
|
|
119
|
+
rows.push(row(s.name, nodes, now() - t1));
|
|
120
|
+
}
|
|
121
|
+
return rows;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** One setting of a sweep of the room: the options of `makeRoom` and the lane order of the far view. */
|
|
125
|
+
export interface RoomSetting {
|
|
126
|
+
/** The name of the setting in the column `board`. */
|
|
127
|
+
readonly name: string;
|
|
128
|
+
readonly options: RoomOptions;
|
|
129
|
+
/** The lane order of the far view. Without it, the links of a gap share the lines of the level links. */
|
|
130
|
+
readonly lanes?: LaneOrder;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* This function measures the board that `makeRoom` opens with each setting, with the method `routes`.
|
|
135
|
+
* The first row is the fair baseline: the board with each gap open to the gutter of `baseline` only, measured at the spacing of `baseline`.
|
|
136
|
+
*/
|
|
137
|
+
export function sweepRoom(fixture: string, graph: Graph, settings: readonly RoomSetting[], baseline: RoomOptions = { gutter: 8, spacing: 4 }): RoomBenchRow[] {
|
|
138
|
+
const area0 = areaOf(graph.nodes);
|
|
139
|
+
const links = roomLinks(graph);
|
|
140
|
+
const row = (board: string, nodes: readonly Node[], ms: number, spacing: number, lanes?: LaneOrder): RoomBenchRow => ({
|
|
141
|
+
fixture,
|
|
142
|
+
board,
|
|
143
|
+
area: area0 > 0 ? areaOf(nodes) / area0 : 1,
|
|
144
|
+
ms,
|
|
145
|
+
...measureBoard({ nodes, edges: graph.edges }, spacing, lanes),
|
|
146
|
+
});
|
|
147
|
+
const fair = makeRoom(graph.nodes, links, { ...baseline, spacing: 0, method: "routes", maxGrowth: Infinity, passes: 1 });
|
|
148
|
+
const rows = [row("gutter", fair.items, 0, baseline.spacing ?? 4)];
|
|
149
|
+
for (const s of settings) {
|
|
106
150
|
const t0 = now();
|
|
107
|
-
const
|
|
108
|
-
rows.push(row(
|
|
151
|
+
const open = makeRoom(graph.nodes, links, { ...s.options, method: "routes" });
|
|
152
|
+
rows.push(row(s.name, open.items, now() - t0, s.options.spacing ?? 4, s.lanes));
|
|
109
153
|
}
|
|
110
154
|
return rows;
|
|
111
155
|
}
|
|
112
156
|
|
|
157
|
+
/** The values of `maxGrowth` of the sweep of ROOM-A. */
|
|
158
|
+
export const GROWTH_SWEEP: readonly number[] = [0, 0.06, 0.09, 0.12, 0.15, 0.18, 0.21, 0.25, 0.3, 0.35, 0.5, Infinity];
|
|
159
|
+
|
|
160
|
+
/** This function gives the settings of ROOM-A: the area and the faults for each `maxGrowth`, at gutter 8 and spacing 4. */
|
|
161
|
+
export function growthSettings(values: readonly number[] = GROWTH_SWEEP): RoomSetting[] {
|
|
162
|
+
return values.map((m) => ({ name: `maxGrowth ${m}`, options: { gutter: 8, spacing: 4, maxGrowth: m } }));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* This function gives the settings of ROOM-C: each lane order of the far view at each spacing, at gutter 8.
|
|
167
|
+
* The order `shared` is the far view without lanes, as before ROOM-C.
|
|
168
|
+
*/
|
|
169
|
+
export function laneSettings(spacings: readonly number[] = [2, 4, 6, 8], orders: readonly LaneOrder[] = LANE_ORDERS): RoomSetting[] {
|
|
170
|
+
return spacings.flatMap((spacing) => [
|
|
171
|
+
{ name: `shared spacing ${spacing}`, options: { gutter: 8, spacing } },
|
|
172
|
+
...orders.map((lanes) => ({ name: `${lanes} spacing ${spacing}`, options: { gutter: 8, spacing }, lanes })),
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** This function gives the settings of ROOM-D: each gutter with each number of passes, at spacing 4. */
|
|
177
|
+
export function passSettings(gutters: readonly number[] = [4, 8, 12, 16], passes: readonly number[] = [1, 2]): RoomSetting[] {
|
|
178
|
+
return gutters.flatMap((gutter) => passes.map((p) => ({ name: `gutter ${gutter} passes ${p}`, options: { gutter, spacing: 4, passes: p } })));
|
|
179
|
+
}
|
|
180
|
+
|
|
113
181
|
/** This function writes the rows of `benchRoom` as a Markdown table. */
|
|
114
182
|
export function formatRoom(rows: readonly RoomBenchRow[]): string {
|
|
115
183
|
const head = "| fixture | board | area | links | through | outside | out and back | worst detour | crossings | ink | detour | lane through | ms |";
|
package/src/index.ts
CHANGED
|
@@ -12,9 +12,11 @@ export * from "./segments.ts";
|
|
|
12
12
|
export * from "./sections.ts";
|
|
13
13
|
export * from "./routers/index.ts";
|
|
14
14
|
export * from "./room/index.ts";
|
|
15
|
+
export * from "./room/lanes.ts";
|
|
15
16
|
export * from "./bench/properties.ts";
|
|
16
17
|
export * from "./bench/quality.ts";
|
|
17
18
|
export * from "./bench/links.ts";
|
|
18
19
|
export * from "./bench/run.ts";
|
|
19
20
|
export * from "./bench/curves.ts";
|
|
20
21
|
export * from "./bench/room.ts";
|
|
22
|
+
export * from "./strategies/index.ts";
|
package/src/room/index.ts
CHANGED
|
@@ -64,6 +64,12 @@ export interface RoomOptions {
|
|
|
64
64
|
readonly lanes?: "link" | "count";
|
|
65
65
|
/** Two box edges nearer than this distance make one boundary of the sweep. The default is 0.5. */
|
|
66
66
|
readonly snap?: number;
|
|
67
|
+
/**
|
|
68
|
+
* The number of passes of the demand. The default is 1.
|
|
69
|
+
* Each pass after the first finds the lanes again on the board that the passes before it opened, and adds the growth that it needs.
|
|
70
|
+
* The limit `maxGrowth` applies to the sum of the passes.
|
|
71
|
+
*/
|
|
72
|
+
readonly passes?: number;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
/** The demand of a board and the board with its gaps open. */
|
|
@@ -147,14 +153,28 @@ export function openGaps<T extends Boxed>(items: readonly T[], gaps: readonly Ga
|
|
|
147
153
|
* The lanes of a gap come from the `method` of the options. The need of a gap is `gutter + lanes × spacing`, and not less than its current width.
|
|
148
154
|
*/
|
|
149
155
|
export function roomDemand(items: readonly Boxed[], links: readonly RoomLink[], options: RoomOptions = {}): Gap[] {
|
|
156
|
+
const gutter = options.gutter ?? DEFAULT_GUTTER;
|
|
157
|
+
const snap = options.snap ?? DEFAULT_SNAP;
|
|
158
|
+
const passes = Math.max(1, Math.floor(options.passes ?? 1));
|
|
159
|
+
let total = demandOnce(items, links, options);
|
|
160
|
+
for (let p = 1; p < passes; p++) {
|
|
161
|
+
// The next pass measures the board that the passes before it opened. Its gaps are the same gaps, in the same order.
|
|
162
|
+
const next = demandOnce(openGaps(items, total, snap), links, options);
|
|
163
|
+
if (next.length !== total.length || next.some((g, i) => g.axis !== total[i]!.axis)) break;
|
|
164
|
+
total = total.map((g, i) => ({ ...g, lanes: Math.max(g.lanes, next[i]!.lanes), need: g.need + Math.max(0, next[i]!.need - next[i]!.width) }));
|
|
165
|
+
}
|
|
166
|
+
return capGrowth(items, total, gutter, options.maxGrowth ?? Infinity);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** This function gives the demand of one pass, without the limit `maxGrowth`. */
|
|
170
|
+
function demandOnce(items: readonly Boxed[], links: readonly RoomLink[], options: RoomOptions): Gap[] {
|
|
150
171
|
const gutter = options.gutter ?? DEFAULT_GUTTER;
|
|
151
172
|
const spacing = options.spacing ?? DEFAULT_SPACING;
|
|
152
173
|
const snap = options.snap ?? DEFAULT_SNAP;
|
|
153
174
|
const weight = (l: RoomLink): number => (options.lanes === "count" ? Math.max(1, l.count ?? 1) : 1);
|
|
154
175
|
const gaps = boardGaps(items, snap);
|
|
155
176
|
const lanes = (options.method ?? "routes") === "lines" ? lineLanes(items, links, gaps, weight) : routeLanes(items, links, gaps, gutter, snap, weight, options.router);
|
|
156
|
-
|
|
157
|
-
return capGrowth(items, raw, gutter, options.maxGrowth ?? Infinity);
|
|
177
|
+
return gaps.map((g, i) => ({ ...g, lanes: lanes[i]!, need: Math.max(g.width, gutter + lanes[i]! * spacing) }));
|
|
158
178
|
}
|
|
159
179
|
|
|
160
180
|
/** This function gives the demand of the board and the board with each gap open to its need. */
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { segmentHitsBox, type Box, type Point } from "../geom.ts";
|
|
2
|
+
import { Grid } from "../grid.ts";
|
|
3
|
+
import type { LevelLink } from "../model.ts";
|
|
4
|
+
import { boardGaps, type Boxed } from "./index.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The lane order of the pieces of the links in one gap.
|
|
8
|
+
*
|
|
9
|
+
* - `links`: the order of the list of links. This order reads nothing of the board.
|
|
10
|
+
* - `ends`: the pieces that turn to the near side at their two ends come first. The pieces that turn to the far side come last.
|
|
11
|
+
* - `crossings`: the order with the fewest crossings between the pieces of the gap and the legs at their ends. A local search finds it.
|
|
12
|
+
*/
|
|
13
|
+
export type LaneOrder = "links" | "ends" | "crossings";
|
|
14
|
+
|
|
15
|
+
/** The lane orders of `gapLanes`, in the order of the benchmark. */
|
|
16
|
+
export const LANE_ORDERS: readonly LaneOrder[] = ["links", "ends", "crossings"];
|
|
17
|
+
|
|
18
|
+
/** The options of `gapLanes`. */
|
|
19
|
+
export interface GapLaneOptions {
|
|
20
|
+
/** The largest distance between two lanes. A narrow gap puts its lanes nearer. The default is 4. */
|
|
21
|
+
readonly spacing?: number;
|
|
22
|
+
/** The lane order in each gap. The default is `crossings`. */
|
|
23
|
+
readonly order?: LaneOrder;
|
|
24
|
+
/** Two box edges nearer than this distance make one boundary of the sweep, as in `boardGaps`. The default is 0.5. */
|
|
25
|
+
readonly snap?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A piece of a link: a leg of the link that runs along a gap. */
|
|
29
|
+
interface Piece {
|
|
30
|
+
/** The index of the link. */
|
|
31
|
+
readonly link: number;
|
|
32
|
+
/** The index of the last point of the leg. The leg goes from the point `seg - 1` to the point `seg`. */
|
|
33
|
+
readonly seg: number;
|
|
34
|
+
/** The span of the leg along the gap. */
|
|
35
|
+
readonly lo: number;
|
|
36
|
+
readonly hi: number;
|
|
37
|
+
/** For each end of the leg: its place along the gap, and the side (-1 or 1) across the gap where the link turns. */
|
|
38
|
+
readonly turns: readonly (readonly [number, number])[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const EPS = 1e-6;
|
|
42
|
+
const ROUNDS = 8;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This function gives each link that runs along a gap its own lane in the gap. The lanes of a gap are `spacing` apart, around its middle line.
|
|
46
|
+
* A leg of a link moves across its gap only, so the link keeps its plugs and the axes of its legs.
|
|
47
|
+
* When a move puts a link through more boxes than before, the link keeps its points.
|
|
48
|
+
*/
|
|
49
|
+
export function gapLanes(items: readonly Boxed[], links: readonly LevelLink[], options: GapLaneOptions = {}): LevelLink[] {
|
|
50
|
+
const spacing = Math.max(0, options.spacing ?? 4);
|
|
51
|
+
const order = options.order ?? "crossings";
|
|
52
|
+
const moved = links.map((l) => l.points.map((p) => ({ x: p.x, y: p.y })));
|
|
53
|
+
for (const g of boardGaps(items, options.snap)) {
|
|
54
|
+
if (g.width <= EPS) continue;
|
|
55
|
+
const across = g.axis;
|
|
56
|
+
const along = g.axis === "x" ? "y" : "x";
|
|
57
|
+
const lo = g.at - g.width;
|
|
58
|
+
const hi = g.at;
|
|
59
|
+
const pieces: Piece[] = [];
|
|
60
|
+
links.forEach((l, li) => {
|
|
61
|
+
const p = l.points;
|
|
62
|
+
// The first and the last legs hold the plugs, so they do not move.
|
|
63
|
+
for (let i = 2; i <= p.length - 2; i++) {
|
|
64
|
+
const a = p[i - 1]!;
|
|
65
|
+
const b = p[i]!;
|
|
66
|
+
if (Math.abs(a[across] - b[across]) > EPS || a[across] <= lo + EPS || a[across] >= hi - EPS) continue;
|
|
67
|
+
if (Math.abs(a[along] - b[along]) <= EPS) continue;
|
|
68
|
+
const side = (q: Point, r: Point): number => Math.sign(r[across] - q[across]);
|
|
69
|
+
pieces.push({
|
|
70
|
+
link: li,
|
|
71
|
+
seg: i,
|
|
72
|
+
lo: Math.min(a[along], b[along]),
|
|
73
|
+
hi: Math.max(a[along], b[along]),
|
|
74
|
+
turns: [
|
|
75
|
+
[a[along], side(a, p[i - 2]!)],
|
|
76
|
+
[b[along], side(b, p[i + 1]!)],
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (pieces.length === 0) continue;
|
|
82
|
+
const list = orderPieces(pieces, order);
|
|
83
|
+
// A piece goes one lane past each piece before it in the order that it overlaps. Thus two pieces that overlap keep their order.
|
|
84
|
+
const lane: number[] = [];
|
|
85
|
+
let n = 0;
|
|
86
|
+
list.forEach((P, i) => {
|
|
87
|
+
let k = 0;
|
|
88
|
+
for (let j = 0; j < i; j++) if (overlaps(list[j]!, P)) k = Math.max(k, lane[j]! + 1);
|
|
89
|
+
lane.push(k);
|
|
90
|
+
n = Math.max(n, k + 1);
|
|
91
|
+
});
|
|
92
|
+
const step = Math.min(spacing, g.width / (n + 1));
|
|
93
|
+
const mid = (lo + hi) / 2;
|
|
94
|
+
list.forEach((P, i) => {
|
|
95
|
+
const v = mid + (lane[i]! - (n - 1) / 2) * step;
|
|
96
|
+
moved[P.link]![P.seg - 1]![across] = v;
|
|
97
|
+
moved[P.link]![P.seg]![across] = v;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const boxes = new Grid<Box>(Math.max(20, meanSide(items)));
|
|
101
|
+
for (const i of items) boxes.add(i.box, i.box);
|
|
102
|
+
return links.map((l, i) => (hits(boxes, moved[i]!) > hits(boxes, l.points) ? l : { ...l, points: moved[i]! }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const overlaps = (P: Piece, Q: Piece): boolean => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* This function counts the crossings when the piece `P` is on the near side of the piece `Q`.
|
|
109
|
+
* A turn of `P` to the far side crosses `Q` when it is inside the span of `Q`. A turn of `Q` to the near side crosses `P` in the same way.
|
|
110
|
+
*/
|
|
111
|
+
function cost(P: Piece, Q: Piece): number {
|
|
112
|
+
let c = 0;
|
|
113
|
+
for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS && y < Q.hi - EPS) c++;
|
|
114
|
+
for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS && y < P.hi - EPS) c++;
|
|
115
|
+
return c;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** This function gives the pieces of one gap in lane order, from the near side to the far side. */
|
|
119
|
+
function orderPieces(pieces: readonly Piece[], order: LaneOrder): Piece[] {
|
|
120
|
+
if (order === "links") return [...pieces];
|
|
121
|
+
const sum = (P: Piece): number => P.turns.reduce((s, [, side]) => s + side, 0);
|
|
122
|
+
const list = [...pieces].sort((P, Q) => sum(P) - sum(Q) || P.link - Q.link || P.seg - Q.seg);
|
|
123
|
+
if (order === "ends") return list;
|
|
124
|
+
// The local search moves one piece to the place that removes the most crossings, until no move removes one.
|
|
125
|
+
const n = list.length;
|
|
126
|
+
const index = new Map(list.map((P, i) => [P, i]));
|
|
127
|
+
const c: number[][] = list.map((P) => list.map((Q) => (P !== Q && overlaps(P, Q) ? cost(P, Q) : 0)));
|
|
128
|
+
const at = (P: Piece, Q: Piece): number => c[index.get(P)!]![index.get(Q)!]!;
|
|
129
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
130
|
+
let better = false;
|
|
131
|
+
for (let i = 0; i < n; i++) {
|
|
132
|
+
const x = list[i]!;
|
|
133
|
+
// The change of the crossings when `x` moves to the place `k`, for each `k`.
|
|
134
|
+
let best = 0;
|
|
135
|
+
let to = i;
|
|
136
|
+
let d = 0;
|
|
137
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
138
|
+
d += at(x, list[k]!) - at(list[k]!, x);
|
|
139
|
+
if (d < best) [best, to] = [d, k];
|
|
140
|
+
}
|
|
141
|
+
d = 0;
|
|
142
|
+
for (let k = i + 1; k < n; k++) {
|
|
143
|
+
d += at(list[k]!, x) - at(x, list[k]!);
|
|
144
|
+
if (d < best) [best, to] = [d, k];
|
|
145
|
+
}
|
|
146
|
+
if (to !== i) {
|
|
147
|
+
list.splice(i, 1);
|
|
148
|
+
list.splice(to, 0, x);
|
|
149
|
+
better = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!better) break;
|
|
153
|
+
}
|
|
154
|
+
return list;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** This function gives the mean of the longer side of the boxes. */
|
|
158
|
+
function meanSide(items: readonly Boxed[]): number {
|
|
159
|
+
let s = 0;
|
|
160
|
+
for (const i of items) s += Math.max(i.box.w, i.box.h);
|
|
161
|
+
return items.length ? s / items.length : 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** This function counts the legs of a polyline that go through a box, for each box. */
|
|
165
|
+
function hits(boxes: Grid<Box>, p: readonly Point[]): number {
|
|
166
|
+
let n = 0;
|
|
167
|
+
for (let i = 1; i < p.length; i++) {
|
|
168
|
+
const a = p[i - 1]!;
|
|
169
|
+
const b = p[i]!;
|
|
170
|
+
boxes.querySegment(a, b, 0, (box) => {
|
|
171
|
+
if (segmentHitsBox(a, b, box)) n++;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return n;
|
|
175
|
+
}
|
package/src/routers/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { sharedRouter, type SharedOptions } from "./shared.ts";
|
|
|
13
13
|
export { sectionsRouter, type SectionsOptions } from "./sections.ts";
|
|
14
14
|
export { byCollection, groupEdges, laneOrder, splitPoint, trunksRouter, type TrunksOptions } from "./trunks.ts";
|
|
15
15
|
|
|
16
|
-
/** The routers
|
|
16
|
+
/** The built-in routers. The registry `STRATEGIES` holds the same routers as strategies, and the benchmark races the routers of the registry. */
|
|
17
17
|
export const ROUTERS: readonly Router[] = [
|
|
18
18
|
straightRouter,
|
|
19
19
|
plugsRouter,
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { arcCurve, type ArcOptions } from "../curves/arc/index.ts";
|
|
2
|
+
import { roundedCurve, type RoundedOptions } from "../curves/rounded/index.ts";
|
|
3
|
+
import { splineCurve, type SplineOptions } from "../curves/spline/index.ts";
|
|
4
|
+
import { straightCurve } from "../curves/straight/index.ts";
|
|
5
|
+
import type { Graph, Result, Router } from "../model.ts";
|
|
6
|
+
import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
|
|
7
|
+
import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
|
|
8
|
+
import { plugsRouter } from "../routers/plugs.ts";
|
|
9
|
+
import { sectionsRouter, type SectionsOptions } from "../routers/sections.ts";
|
|
10
|
+
import { sharedRouter, type SharedOptions } from "../routers/shared.ts";
|
|
11
|
+
import { straightRouter } from "../routers/straight.ts";
|
|
12
|
+
import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunks.ts";
|
|
13
|
+
import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type Registry, type Strategy } from "./model.ts";
|
|
14
|
+
|
|
15
|
+
/** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
|
|
16
|
+
export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
|
|
17
|
+
|
|
18
|
+
/** This function makes a router strategy from a router factory and a preset of its options. Each call makes the router of its options. */
|
|
19
|
+
function routerStrategy<O extends object>(
|
|
20
|
+
name: string,
|
|
21
|
+
describe: string,
|
|
22
|
+
maxNodes: number,
|
|
23
|
+
defaults: O,
|
|
24
|
+
make: (options: O & { readonly name: string }) => Router,
|
|
25
|
+
): Strategy<"router", O> {
|
|
26
|
+
return defineStrategy({ step: "router", name, describe, defaults, maxNodes, run: (graph: Graph, options: Readonly<O>): Result => make({ ...options, name }).route(graph) });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** This function makes a router strategy for a router without options. */
|
|
30
|
+
function fixedRouter(router: Router, describe: string): Strategy<"router", object> {
|
|
31
|
+
return defineStrategy({ step: "router", name: router.name, describe, defaults: {}, run: (graph: Graph): Result => router.route(graph) });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The built-in router strategies, in the order of the benchmark. They route as the routers of `ROUTERS`. */
|
|
35
|
+
export const routerStrategies: {
|
|
36
|
+
readonly straight: Strategy<"router", object>;
|
|
37
|
+
readonly plugs: Strategy<"router", object>;
|
|
38
|
+
readonly corners: Strategy<"router", RouterPreset<CornersOptions>>;
|
|
39
|
+
readonly "corners-smooth": Strategy<"router", RouterPreset<CornersOptions>>;
|
|
40
|
+
readonly trunks: Strategy<"router", RouterPreset<TrunksOptions>>;
|
|
41
|
+
readonly shared: Strategy<"router", RouterPreset<SharedOptions>>;
|
|
42
|
+
readonly "shared-all": Strategy<"router", RouterPreset<SharedOptions>>;
|
|
43
|
+
readonly sections: Strategy<"router", RouterPreset<SectionsOptions>>;
|
|
44
|
+
readonly "sections-tree": Strategy<"router", RouterPreset<SectionsOptions>>;
|
|
45
|
+
} = Object.freeze({
|
|
46
|
+
straight: fixedRouter(straightRouter, "The control. Each edge is one straight line between its plugs."),
|
|
47
|
+
plugs: fixedRouter(
|
|
48
|
+
plugsRouter,
|
|
49
|
+
"Each edge leaves its plug along the normal of the side and follows the curve of the inflection rule. It does not go around the nodes.",
|
|
50
|
+
),
|
|
51
|
+
corners: routerStrategy<RouterPreset<CornersOptions>>(
|
|
52
|
+
"corners",
|
|
53
|
+
"Each edge goes around the nodes on a shortest path (A*) through a sparse visibility graph of the box corners.",
|
|
54
|
+
10_000,
|
|
55
|
+
{},
|
|
56
|
+
cornersRouter,
|
|
57
|
+
),
|
|
58
|
+
"corners-smooth": routerStrategy<RouterPreset<CornersOptions>>(
|
|
59
|
+
"corners-smooth",
|
|
60
|
+
"The same paths as `corners`. A route in clear view follows the inflection rule, and the other routes get round corners.",
|
|
61
|
+
10_000,
|
|
62
|
+
{ smooth: true },
|
|
63
|
+
cornersRouter,
|
|
64
|
+
),
|
|
65
|
+
trunks: routerStrategy<RouterPreset<TrunksOptions>>(
|
|
66
|
+
"trunks",
|
|
67
|
+
"A rule puts edges in groups. The edges of a group leave one plug of one node on one trunk. At a split point, each edge goes on to its target.",
|
|
68
|
+
10_000,
|
|
69
|
+
{ rule: byCollection },
|
|
70
|
+
trunksRouter,
|
|
71
|
+
),
|
|
72
|
+
shared: routerStrategy<RouterPreset<SharedOptions>>(
|
|
73
|
+
"shared",
|
|
74
|
+
"The edges of one collection share their ways through the corner graph. A link that the group uses already costs half.",
|
|
75
|
+
10_000,
|
|
76
|
+
{ rule: byCollection },
|
|
77
|
+
sharedRouter,
|
|
78
|
+
),
|
|
79
|
+
"shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter),
|
|
80
|
+
sections: routerStrategy<RouterPreset<SectionsOptions>>(
|
|
81
|
+
"sections",
|
|
82
|
+
"A tree of sections, with a hub in each section. A long edge goes through the hubs, and a short edge goes directly.",
|
|
83
|
+
100_000,
|
|
84
|
+
{},
|
|
85
|
+
sectionsRouter,
|
|
86
|
+
),
|
|
87
|
+
"sections-tree": routerStrategy<RouterPreset<SectionsOptions>>(
|
|
88
|
+
"sections-tree",
|
|
89
|
+
"The same as `sections`, but without the limit of the stretch.",
|
|
90
|
+
100_000,
|
|
91
|
+
{ stretch: Infinity },
|
|
92
|
+
sectionsRouter,
|
|
93
|
+
),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/** The context of a curve input, without the keys that have no value. */
|
|
97
|
+
const contextOf = (input: CurveInput): Omit<CurveInput, "points"> => ({
|
|
98
|
+
...(input.startNormal ? { startNormal: input.startNormal } : {}),
|
|
99
|
+
...(input.endNormal ? { endNormal: input.endNormal } : {}),
|
|
100
|
+
...(input.space ? { space: input.space } : {}),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/** The options of the curve strategies `rounded` and `arc`. */
|
|
104
|
+
export interface RadiusOptions {
|
|
105
|
+
/** The radius of the corners. The default is 12. */
|
|
106
|
+
readonly radius: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The options of the curve strategy `spline`. */
|
|
110
|
+
export interface TensionOptions {
|
|
111
|
+
/** The tension of the spline, from 0 (the spline) to 1 (the polyline). The default is 0. */
|
|
112
|
+
readonly tension: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The built-in curve strategies, in the order of `CURVE_TYPES`. Each one gives the same segments as `curveOf` with its type. */
|
|
116
|
+
export const curveStrategies: {
|
|
117
|
+
readonly straight: Strategy<"curve", object>;
|
|
118
|
+
readonly rounded: Strategy<"curve", RadiusOptions>;
|
|
119
|
+
readonly spline: Strategy<"curve", TensionOptions>;
|
|
120
|
+
readonly arc: Strategy<"curve", RadiusOptions>;
|
|
121
|
+
} = Object.freeze({
|
|
122
|
+
straight: defineStrategy({ step: "curve", name: "straight", describe: "The polyline as it is.", defaults: {}, run: (input: CurveInput) => straightCurve(input.points) }),
|
|
123
|
+
rounded: defineStrategy({
|
|
124
|
+
step: "curve",
|
|
125
|
+
name: "rounded",
|
|
126
|
+
describe: "A quadratic Bezier curve at each corner, as `roundCorners` draws it. The first and the last legs stay.",
|
|
127
|
+
defaults: { radius: 12 },
|
|
128
|
+
run: (input: CurveInput, o: Readonly<RadiusOptions>) => roundedCurve(input.points, { ...contextOf(input), radius: o.radius } satisfies RoundedOptions),
|
|
129
|
+
}),
|
|
130
|
+
spline: defineStrategy({
|
|
131
|
+
step: "curve",
|
|
132
|
+
name: "spline",
|
|
133
|
+
describe: "A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece.",
|
|
134
|
+
defaults: { tension: 0 },
|
|
135
|
+
run: (input: CurveInput, o: Readonly<TensionOptions>) => splineCurve(input.points, { ...contextOf(input), tension: o.tension } satisfies SplineOptions),
|
|
136
|
+
}),
|
|
137
|
+
arc: defineStrategy({
|
|
138
|
+
step: "curve",
|
|
139
|
+
name: "arc",
|
|
140
|
+
describe: "A circular arc at each corner, tangent to the two legs. The first and the last legs stay.",
|
|
141
|
+
defaults: { radius: 12 },
|
|
142
|
+
run: (input: CurveInput, o: Readonly<RadiusOptions>) => arcCurve(input.points, { ...contextOf(input), radius: o.radius } satisfies ArcOptions),
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/** The options of a room strategy: the options of `roomDemand`, without the method. */
|
|
147
|
+
export type RoomPreset = Omit<RoomOptions, "method">;
|
|
148
|
+
|
|
149
|
+
const roomStrategy = (method: RoomMethod, describe: string): Strategy<"room", RoomPreset> =>
|
|
150
|
+
defineStrategy<"room", RoomPreset>({ step: "room", name: method, describe, defaults: {}, run: (input, o) => roomDemand(input.items, input.links, { ...o, method }) });
|
|
151
|
+
|
|
152
|
+
/** The built-in room strategies: the methods of `roomDemand`. */
|
|
153
|
+
export const roomStrategies: {
|
|
154
|
+
readonly routes: Strategy<"room", RoomPreset>;
|
|
155
|
+
readonly lines: Strategy<"room", RoomPreset>;
|
|
156
|
+
} = Object.freeze({
|
|
157
|
+
routes: roomStrategy("routes", "The lanes of a gap are the most routes that run along it side by side, with each gap open to the gutter."),
|
|
158
|
+
lines: roomStrategy("lines", "The lanes of a gap are the straight lines between the centers of the boxes that cross the gap."),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
|
|
162
|
+
export const BUILT_IN_STRATEGIES: readonly AnyStrategy[] = Object.freeze([
|
|
163
|
+
...Object.values(routerStrategies),
|
|
164
|
+
...Object.values(curveStrategies),
|
|
165
|
+
...Object.values(roomStrategies),
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
/** The registry of the built-in strategies. The benchmark and the site take their lists from it. */
|
|
169
|
+
export const STRATEGIES: Registry = createRegistry(BUILT_IN_STRATEGIES);
|