@mark1russell7/highways 0.1.3 → 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 +174 -0
- package/build/index.d.ts +529 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1575 -193
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/curves.ts +125 -0
- package/src/bench/room.ts +190 -0
- package/src/cases.ts +28 -0
- package/src/curves/arc/index.ts +71 -0
- package/src/curves/index.ts +48 -0
- package/src/curves/model.ts +64 -0
- package/src/curves/rounded/index.ts +60 -0
- package/src/curves/sample.ts +73 -0
- package/src/curves/space.ts +91 -0
- package/src/curves/spline/index.ts +77 -0
- package/src/curves/straight/index.ts +7 -0
- package/src/index.ts +6 -0
- package/src/levels.ts +3 -2
- package/src/room/index.ts +354 -0
- 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/src/visibility.ts +28 -3
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# @mark1russell7/highways
|
|
2
|
+
|
|
3
|
+
`@mark1russell7/highways` routes the edges of large graphs. It knows no renderer. It takes boxes and edges, and it gives polylines and zoom levels.
|
|
4
|
+
|
|
5
|
+
The [repository README](https://github.com/mark1russell7/highways#readme) tells the idea, the routers and the benchmark. The [site](https://mark1russell7.github.io/highways/) shows a live map.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @mark1russell7/highways
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Route a graph
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { cornersRouter, type Graph } from "@mark1russell7/highways";
|
|
17
|
+
|
|
18
|
+
const graph: Graph = {
|
|
19
|
+
nodes: [
|
|
20
|
+
{ id: "a", box: { x: 0, y: 0, w: 80, h: 40 } },
|
|
21
|
+
{ id: "b", box: { x: 240, y: 120, w: 80, h: 40 } },
|
|
22
|
+
],
|
|
23
|
+
edges: [{ id: "ab", source: { node: "a" }, target: { node: "b" } }],
|
|
24
|
+
};
|
|
25
|
+
const result = cornersRouter({ name: "corners" }).route(graph);
|
|
26
|
+
// Each route is a polyline: result.routes[0].points
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Curve types
|
|
30
|
+
|
|
31
|
+
A router gives polylines. The curve layer turns a polyline into a curve of a type. The polyline can come from any router, from a lane of `laneRoutes` or from a link of a zoom level.
|
|
32
|
+
|
|
33
|
+
| Type | Curve | Ends |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `straight` | The polyline as it is. | As the polyline. |
|
|
36
|
+
| `rounded` | A quadratic Bezier curve at each corner, as `roundCorners` draws it. The `radius` sets the size of the corners. | The first and the last legs stay. |
|
|
37
|
+
| `spline` | A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece. The `tension` goes from 0 (the spline) to 1 (the polyline). | With `startNormal` and `endNormal`, the curve leaves and enters each plug along its normal. |
|
|
38
|
+
| `arc` | A circular arc at each corner, tangent to the two legs. On horizontal and vertical legs, each arc is a quarter circle. | The first and the last legs stay. |
|
|
39
|
+
|
|
40
|
+
The output is a list of segments: `move`, `line`, `cubic` and `arc`. A renderer maps them to SVG path data, to canvas calls or to the vertices of a mesh.
|
|
41
|
+
|
|
42
|
+
- `curveOf(points, options)` gives the segments of a curve. The `type` of `options` selects the curve type.
|
|
43
|
+
- `toSvgPath(segments, digits = 2)` writes the segments as SVG path data.
|
|
44
|
+
- `sampleCurve(segments, tolerance = 0.5)` gives a polyline within `tolerance` of the curve, for hit tests and measures.
|
|
45
|
+
- `curveSpace(boxes)` makes the free space of a board one time. With `space`, a curve goes through no box that its polyline avoids.
|
|
46
|
+
|
|
47
|
+
Where the full curve goes through a box, the free space makes the curve smaller. It shrinks the radius of a corner, or the tangents of a piece of a spline. A corner with no clear radius stays sharp. A piece of a spline with no clear tangent is its straight leg.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { curveOf, curveSpace, nearestSide, SIDE_NORMAL, toSvgPath } from "@mark1russell7/highways";
|
|
51
|
+
|
|
52
|
+
const space = curveSpace(graph.nodes.map((n) => n.box));
|
|
53
|
+
const box = new Map(graph.nodes.map((n) => [n.id, n.box]));
|
|
54
|
+
for (const route of result.routes) {
|
|
55
|
+
const edge = graph.edges.find((e) => e.id === route.edge)!;
|
|
56
|
+
const p = route.points;
|
|
57
|
+
const curve = curveOf(p, {
|
|
58
|
+
type: "spline",
|
|
59
|
+
startNormal: SIDE_NORMAL[nearestSide(box.get(edge.source.node)!, p[0]!)],
|
|
60
|
+
endNormal: SIDE_NORMAL[nearestSide(box.get(edge.target.node)!, p[p.length - 1]!)],
|
|
61
|
+
space,
|
|
62
|
+
});
|
|
63
|
+
const d = toSvgPath(curve); // The data of an SVG path element: "M 80 20 C ..."
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A canvas draws an `arc` segment with `ctx.arc(center.x, center.y, radius, start, start + sweep, sweep < 0)`.
|
|
68
|
+
|
|
69
|
+
Each type has its own function too: `straightCurve`, `roundedCurve`, `splineCurve` and `arcCurve`.
|
|
70
|
+
|
|
71
|
+
The benchmark compares each curve type to the polylines of each router, on the fixtures of 100 nodes or fewer. It counts the curves that go through a box, with and without the free space. It also gives the ratio of the lengths.
|
|
72
|
+
|
|
73
|
+
## Room for the links
|
|
74
|
+
|
|
75
|
+
A host layout can pack the boxes of a far view in a grid, with no gap between them. Then the links have no channel, and they go through the boxes. The router knows the room that the links need. The room functions give that room, and they can open the gaps.
|
|
76
|
+
|
|
77
|
+
- `roomDemand(items, links, options)` gives the gaps of the board. A gap is a boundary between two columns (`axis: "x"`) or two rows (`axis: "y"`). Each gap has its current `width`, its `lanes` and its `need`.
|
|
78
|
+
- The need of a gap is `gutter + lanes × spacing`, and not less than its current width. The default gutter is 8, and the default spacing is 4.
|
|
79
|
+
- With the method `routes`, the default, the function opens each gap to the gutter and routes the links one time. The lanes of a gap are the most routes that go along it side by side.
|
|
80
|
+
- With the method `lines`, the lanes of a gap are the straight lines between the centers of the boxes that cross the gap.
|
|
81
|
+
- `openGaps(items, gaps)` moves each box by the growth of each gap before it. The boxes keep their size, their order and their alignment, so a grid stays a grid.
|
|
82
|
+
- `makeRoom(items, links, options)` gives the gaps and the open board. `maxGrowth` limits the growth of the board on each axis, as a share of its size.
|
|
83
|
+
- With `passes: 2`, `roomDemand` finds the lanes again on the open board and adds the growth that they need. `maxGrowth` limits the sum of the passes.
|
|
84
|
+
- `roomLinks(graph)` gives the links of a graph, with the count of the edges of each pair.
|
|
85
|
+
- `gapLanes(items, links, options)` gives each link of a far view its own lane in each gap. The lanes are `spacing` apart, around the middle of the gap.
|
|
86
|
+
- The option `order` of `gapLanes` sets the lane order in a gap: `links` (the order of the list), `ends` (by the sides of the turns) or `crossings` (the default).
|
|
87
|
+
|
|
88
|
+
The host layout still packs the boxes. These functions do not place a box. They only open the gaps between the boxes.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { cornersRouter, makeRoom, roomLinks } from "@mark1russell7/highways";
|
|
92
|
+
|
|
93
|
+
const room = makeRoom(graph.nodes, roomLinks(graph), { gutter: 8, spacing: 4 });
|
|
94
|
+
// Each gap tells its axis, its place `at`, its `width`, its `lanes` and its `need`.
|
|
95
|
+
const open = { nodes: room.items, edges: graph.edges };
|
|
96
|
+
const result = cornersRouter({ name: "corners" }).route(open);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The benchmark measures the board `packedGridBoard`: 96 touching boxes and 132 links, at gutter 8 and spacing 4. The links are the links of the far view. A lane route of the router `sections` gets a lane of 4 units for each edge of a shared segment.
|
|
100
|
+
|
|
101
|
+
| Board | Area | Through | Crossings | Mean detour | Worst detour | Lane routes through a box |
|
|
102
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
103
|
+
| Packed | 1.00 | 62 | 162 | 1.12 | 1.48 | 94 |
|
|
104
|
+
| Gutter only | 1.13 | 0 | 345 | 1.34 | 2.38 | 10 |
|
|
105
|
+
| Method `routes` | 1.50 | 0 | 276 | 1.32 | 2.09 | 2 |
|
|
106
|
+
| Method `lines` | 3.80 | 0 | 382 | 1.20 | 1.47 | 5 |
|
|
107
|
+
|
|
108
|
+
On the packed board, the links are short only because they go through the boxes. The method `routes` is the default, because it gives the fewest crossings for its area.
|
|
109
|
+
|
|
110
|
+
`sweepRoom` changes one knob at a time on the same board. `growthSettings`, `laneSettings` and `passSettings` give the settings of the benchmark.
|
|
111
|
+
|
|
112
|
+
| Knob | Setting | Area | Crossings | Mean detour | Worst detour | Lane routes through a box |
|
|
113
|
+
|---|---|---:|---:|---:|---:|---:|
|
|
114
|
+
| `maxGrowth` | 0.12 | 1.25 | 288 | 1.34 | 2.33 | 6 |
|
|
115
|
+
| `maxGrowth` | 0.25 | 1.46 | 284 | 1.32 | 2.14 | 2 |
|
|
116
|
+
| `maxGrowth` | 0.30 and more | 1.50 | 276 | 1.32 | 2.09 | 2 |
|
|
117
|
+
| Lane order | `links` | 1.50 | 585 | 1.34 | 2.30 | 2 |
|
|
118
|
+
| Lane order | `ends` | 1.50 | 551 | 1.33 | 2.19 | 2 |
|
|
119
|
+
| Lane order | `crossings` | 1.50 | 467 | 1.33 | 2.23 | 2 |
|
|
120
|
+
| Passes | 2 | 1.57 | 281 | 1.32 | 2.03 | 2 |
|
|
121
|
+
| Gutter | 12 | 1.58 | 282 | 1.32 | 2.03 | 2 |
|
|
122
|
+
|
|
123
|
+
The links of a gap without lanes share one line. Thus the crossings on that line do not count. With a lane for each link, the crossings show, and the order `crossings` removes 20% of them. The spacing changes the crossings less than 5%. The mean detour stays near 1.32 for each knob, because a link with legs parallel to the axes goes around the boxes.
|
|
124
|
+
|
|
125
|
+
## Strategies
|
|
126
|
+
|
|
127
|
+
The steps of the pipeline that vary have strategies. Each step has one interface, so a consumer chooses a strategy by its name or adds its own.
|
|
128
|
+
|
|
129
|
+
| Step | Input | Output | Built-in strategies |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| `router` | `Graph` | `Result` | `straight`, `plugs`, `corners`, `corners-smooth`, `trunks`, `shared`, `shared-all`, `sections`, `sections-tree` |
|
|
132
|
+
| `curve` | `CurveInput`: the points, the normals of the plugs and the free space | `CurveSegment[]` | `straight`, `rounded`, `spline`, `arc` |
|
|
133
|
+
| `room` | `RoomInput`: the boxes and the links of a board | `Gap[]` | `routes`, `lines` |
|
|
134
|
+
|
|
135
|
+
A strategy has a `step`, a `name`, a `describe` text, its `defaults` and the method `run(input, options?)`. The type of the options comes from `defaults`. Each option that a call does not give gets its default value.
|
|
136
|
+
|
|
137
|
+
- `defineStrategy({ step, name, describe, defaults, run, maxNodes? })` makes a strategy. The function `run` gets each option.
|
|
138
|
+
- `createRegistry(strategies)` makes a registry. Two strategies with one step and one name are an error.
|
|
139
|
+
- `register(registry, ...strategies)` gives a new registry. It does not change the registry before it.
|
|
140
|
+
- `listStrategies(registry, step)` gives the strategies of a step, in order. `findStrategy` and `getStrategy` give one strategy by its name.
|
|
141
|
+
- `asRouter(strategy, options?)` gives a `Router`, and `routersOf(registry)` gives the routers of a registry.
|
|
142
|
+
- `STRATEGIES` is the registry of `BUILT_IN_STRATEGIES`. The records `routerStrategies`, `curveStrategies` and `roomStrategies` give each built-in strategy with its typed options.
|
|
143
|
+
|
|
144
|
+
A registry is data. No import changes it. The benchmark and the site take their lists from `STRATEGIES`, so a new built-in strategy gets its rows and its button with no other edit. A consumer makes its own registry:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { BUILT_IN_STRATEGIES, center, createRegistry, defineStrategy, FIXTURES, routersOf, runBenchmark } from "@mark1russell7/highways";
|
|
148
|
+
|
|
149
|
+
const elbow = defineStrategy({
|
|
150
|
+
step: "router",
|
|
151
|
+
name: "elbow",
|
|
152
|
+
describe: "Each edge goes along one axis, then along the other, from center to center.",
|
|
153
|
+
defaults: { turn: "x" as "x" | "y" },
|
|
154
|
+
run(graph, o) {
|
|
155
|
+
const box = new Map(graph.nodes.map((n) => [n.id, n.box]));
|
|
156
|
+
const routes = graph.edges.map((e) => {
|
|
157
|
+
const a = center(box.get(e.source.node)!);
|
|
158
|
+
const b = center(box.get(e.target.node)!);
|
|
159
|
+
return { edge: e.id, points: [a, o.turn === "x" ? { x: b.x, y: a.y } : { x: a.x, y: b.y }, b] };
|
|
160
|
+
});
|
|
161
|
+
return { routes };
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const registry = createRegistry([...BUILT_IN_STRATEGIES, elbow]);
|
|
165
|
+
const rows = runBenchmark(routersOf(registry), FIXTURES.slice(0, 1)); // The rows include the router `elbow`.
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The built-in strategies give the same output as their functions. `curveStrategies.rounded.run({ points, space }, { radius: 8 })` gives the segments of `curveOf(points, { type: "rounded", space, radius: 8 })`. `benchCurveStrategies` and the fourth argument of `benchRoom` take a list of strategies.
|
|
169
|
+
|
|
170
|
+
The plugs and the zoom levels have one strategy each, so they have no step yet.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT
|