@mark1russell7/highways 0.2.0 → 0.4.0
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 +265 -9
- package/build/index.d.ts +2168 -101
- package/build/index.d.ts.map +1 -1
- package/build/index.js +11624 -3422
- package/build/index.js.map +1 -1
- package/package.json +4 -1
- package/src/bench/compare.ts +180 -0
- package/src/bench/curves.ts +514 -2
- package/src/bench/links.ts +28 -6
- package/src/bench/measures/bundles.ts +119 -0
- package/src/bench/measures/curves.ts +211 -0
- package/src/bench/measures/groups.ts +122 -0
- package/src/bench/measures/index.ts +9 -0
- package/src/bench/measures/levels.ts +103 -0
- package/src/bench/measures/pareto.ts +37 -0
- package/src/bench/measures/room.ts +43 -0
- package/src/bench/measures/routes.ts +226 -0
- package/src/bench/measures/stability.ts +104 -0
- package/src/bench/measures/stats.ts +114 -0
- package/src/bench/properties.ts +18 -2
- package/src/bench/room.ts +209 -2
- package/src/bench/run.ts +86 -11
- package/src/bench/screen/forms.ts +372 -0
- package/src/bench/screen/generators.ts +54 -0
- package/src/bench/screen/hash.ts +129 -0
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +114 -0
- package/src/bench/screen/screen.ts +390 -0
- package/src/bench/screen/set.ts +136 -0
- package/src/bench/suite.ts +839 -0
- package/src/cases.ts +41 -5
- package/src/curves/clothoid/index.ts +150 -0
- package/src/curves/corner.ts +116 -0
- package/src/curves/fit.ts +79 -0
- package/src/curves/highway/index.ts +636 -0
- package/src/curves/index.ts +16 -1
- package/src/curves/model.ts +4 -2
- package/src/curves/smooth/index.ts +50 -0
- package/src/curves/space.ts +76 -0
- package/src/groups/cut/index.ts +292 -0
- package/src/groups/index.ts +2 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/highways/corridors/index.ts +305 -0
- package/src/highways/follow.ts +155 -0
- package/src/highways/index.ts +7 -0
- package/src/highways/keys/index.ts +15 -0
- package/src/highways/model.ts +111 -0
- package/src/highways/result.ts +34 -0
- package/src/highways/router.ts +40 -0
- package/src/highways/shared-ways/index.ts +15 -0
- package/src/index.ts +6 -0
- package/src/levels.ts +163 -5
- package/src/room/bends.ts +203 -0
- package/src/room/index.ts +28 -0
- package/src/room/levels/index.ts +155 -0
- package/src/routers/sections.ts +15 -40
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +47 -1
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +206 -9
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +55 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +234 -0
- package/src/visibility.ts +190 -1
package/README.md
CHANGED
|
@@ -36,6 +36,10 @@ A router gives polylines. The curve layer turns a polyline into a curve of a typ
|
|
|
36
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
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
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
|
+
| `smooth` | A cubic spiral, an arc and a second cubic spiral at each corner, as the corner smoothing of Figma. The `smoothing` goes from 0 (the curve `arc`) to 1 (no arc). | The first and the last legs stay. |
|
|
40
|
+
| `clothoid` | A clothoid, an arc and a second clothoid at each corner, as a transition curve of a road. The `spiral` goes from 0 (the curve `arc`) to 1 (no arc). Cubic pieces draw each clothoid within the `tolerance`. | The first and the last legs stay. |
|
|
41
|
+
|
|
42
|
+
The curves `smooth` and `clothoid` have no jump of the curvature. The curvature grows from 0 on a leg to `1 / radius` on the arc. A cubic spiral turns by 1 radian or less, so a sharp corner keeps a part of its arc. The free space tests the triangle of each corner of these curves: the start of the corner, the vertex and the end of the corner. The curve stays in this triangle, so the test needs no samples.
|
|
39
43
|
|
|
40
44
|
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
45
|
|
|
@@ -66,7 +70,75 @@ for (const route of result.routes) {
|
|
|
66
70
|
|
|
67
71
|
A canvas draws an `arc` segment with `ctx.arc(center.x, center.y, radius, start, start + sweep, sweep < 0)`.
|
|
68
72
|
|
|
69
|
-
Each type has its own function too: `straightCurve`, `roundedCurve`, `splineCurve` and `
|
|
73
|
+
Each type has its own function too: `straightCurve`, `roundedCurve`, `splineCurve`, `arcCurve`, `smoothCurve` and `clothoidCurve`.
|
|
74
|
+
|
|
75
|
+
### Curved highways
|
|
76
|
+
|
|
77
|
+
A curve of one lane does not know the other lanes of its bundle. So the lanes of `laneRoutes` lose their spacing at a bend. `highwayCurves` curves all the lanes together, and a bundle bends as one.
|
|
78
|
+
|
|
79
|
+
- At each bend of a bundle, the lanes are arcs with one center. The radius of a lane is the radius of the center line plus or minus its offset.
|
|
80
|
+
- The bend grows to keep the inner lane at `minRadius`. It shrinks to fit on its legs and in the free space.
|
|
81
|
+
- A lane joins or leaves a bundle on an S curve on the leg with fewer lanes. The tangent has no break there.
|
|
82
|
+
- The option `jog` sets the shape of the S curve. A `biarc` (the default) is two arcs. A `cubic` S curve is one cubic Bezier curve. A `quintic` S curve has no jump of the curvature, in `jogPieces` cubic pieces.
|
|
83
|
+
- The curvature of each S curve is `1 / radius` or less. So a `cubic` or a `quintic` S curve is longer than a biarc. Where it does not fit on its leg, the lane gets a biarc.
|
|
84
|
+
- A lane in no open bundle gets the curve of `arcCurve`.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { highwayCurves, runKey, segmentsOf, toSvgPath, curveSpace } from "@mark1russell7/highways";
|
|
88
|
+
|
|
89
|
+
const graph = segmentsOf(result);
|
|
90
|
+
const open = new Set(graph.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges)));
|
|
91
|
+
const space = curveSpace(board.nodes.map((n) => n.box));
|
|
92
|
+
// The same input as laneRoutes(graph, open, 4, 16). The curves come in the order of laneRoutes.
|
|
93
|
+
for (const lane of highwayCurves(graph, open, 4, { maxWidth: 16, radius: 12, minRadius: 4, space })) {
|
|
94
|
+
const d = toSvgPath(lane.segments);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The curve strategy `highway` is in the registry. For one polyline, it gives the curve of `arc`. `benchHighways` compares each lane curved alone with `rounded` to the lanes of `highwayCurves`. It gives the error of the lane spacing and the crossings in a bundle. It also gives the breaks of the tangent, the smallest radius and the curves through a box.
|
|
99
|
+
|
|
100
|
+
### Lane order through a bundle
|
|
101
|
+
|
|
102
|
+
Two edges can go along several runs one after the other. The option `laneOrder` of the routers `trunks`, `shared` and `sections` sets their lane order on these runs.
|
|
103
|
+
|
|
104
|
+
- With `bundle`, two edges keep one side on each run of their common stretch. Where they part, the edge that turns more to the left gets the left lane.
|
|
105
|
+
- Two edges can part on the left at one end and on the right at the other end. Then they must cross. The end with the smaller x, then the smaller y, sets the side. The crossing is where they part, not between two runs.
|
|
106
|
+
- With `run`, each run gets its order alone. Two edges can change sides between two runs.
|
|
107
|
+
- The default of `trunks` and `shared` is `bundle`. The default of `sections` is `run`, so its runs keep the lane order of version 0.2.0.
|
|
108
|
+
- `orderLanes(run, routes, order)` gives the order of one run. Its default order is `run`.
|
|
109
|
+
- The option changes only the lane order. The routes, the runs and their keys stay the same.
|
|
110
|
+
|
|
111
|
+
The benchmark measures the lanes of `highwayCurves` on the case boards, with each lane order. A crossing in the body is between the middles of the first and the last leg of a bundle. An end crossing is near a merge or a split.
|
|
112
|
+
|
|
113
|
+
| Router | Board | Body crossings, `run` → `bundle` | End crossings, `run` → `bundle` |
|
|
114
|
+
|---|---|---:|---:|
|
|
115
|
+
| `shared` | `case-dense-groups` | 10 → 0 | 366 → 162 |
|
|
116
|
+
| `shared-all` | `case-dense-groups` | 0 → 0 | 1586 → 838 |
|
|
117
|
+
| `shared-all` | `case-hub-chain` | 2 → 3 | 603 → 73 |
|
|
118
|
+
| `shared-all` | `case-many-groups` | 12 → 0 | 2301 → 687 |
|
|
119
|
+
|
|
120
|
+
With `bundle`, 90% or more of the end crossings that stay are necessary. Each comes from two edges that part on opposite sides at the two ends of their stretch. The order changes no crossing of the routes. The rows of `trunks` change by one end crossing at most. On the 9 rows of `sections`, 12 of the 18 crossing counts fall and 5 rise. Thus `run` stays the default of `sections`.
|
|
121
|
+
|
|
122
|
+
### Where the lanes leave a trunk
|
|
123
|
+
|
|
124
|
+
The router `trunks` gives each group one trunk from the plug to a split point. The option `leave` sets where each lane leaves the trunk.
|
|
125
|
+
|
|
126
|
+
- With `part`, the default, a lane leaves the trunk where its way parts from the ways of the other lanes.
|
|
127
|
+
- A lane that turns back over the trunk by more than 90 degrees leaves at an earlier point of the trunk. It does so only on a line that is clear of the boxes.
|
|
128
|
+
- On the second leg of the trunk, the lane leaves at the foot of its next point. A foot less than two margins from the start of that leg puts the edge out of the bundle.
|
|
129
|
+
- The bundle keeps the points that all its routes share. Past them, the lanes that go on together share a run, in the `runs` of the result.
|
|
130
|
+
- With `split`, each lane leaves at the split point, as in version 0.2.0. Lanes that go on together past the split point have no run.
|
|
131
|
+
- `leaveTrunk(trunk, branch, clear, min)` gives the place where one branch leaves its trunk.
|
|
132
|
+
|
|
133
|
+
The benchmark measures the lanes of `highwayCurves` on the case boards, with each value of `leave`.
|
|
134
|
+
|
|
135
|
+
| Router | Board | Worst lane spacing error, `split` → `part` | Body crossings, `split` → `part` |
|
|
136
|
+
|---|---|---:|---:|
|
|
137
|
+
| `trunks` | `case-dense-groups` | 45.6% → 0.6% | 0 → 0 |
|
|
138
|
+
| `trunks-all` | `case-dense-groups` | 133.2% → 0.8% | 3 → 0 |
|
|
139
|
+
| `trunks-all` | `case-many-groups` | 68.2% → 47.6% | 1 → 0 |
|
|
140
|
+
|
|
141
|
+
The error of 47.6% is on a new run of two lanes. With `split`, these lanes go on one line with no lane spacing, and the benchmark does not measure them. The run starts at the plug of a bundle with a very short trunk. Its first leg is 13 units long, to the corner of a box.
|
|
70
142
|
|
|
71
143
|
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
144
|
|
|
@@ -81,6 +153,8 @@ A host layout can pack the boxes of a far view in a grid, with no gap between th
|
|
|
81
153
|
- `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
154
|
- `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
155
|
- 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.
|
|
156
|
+
- The option `bends` gives room to the bends of the bundles (HWC-D). It is off by default. Its router routes the open board, and each segment with two or more edges is a bundle.
|
|
157
|
+
- With `bends`, a leg of a bundle that is too short for its arcs grows by one gap between its ends. A bend of the width `W` needs the radius `W/2 + minRadius` on its center line.
|
|
84
158
|
- `roomLinks(graph)` gives the links of a graph, with the count of the edges of each pair.
|
|
85
159
|
- `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
160
|
- 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).
|
|
@@ -100,12 +174,12 @@ The benchmark measures the board `packedGridBoard`: 96 touching boxes and 132 li
|
|
|
100
174
|
|
|
101
175
|
| Board | Area | Through | Crossings | Mean detour | Worst detour | Lane routes through a box |
|
|
102
176
|
|---|---:|---:|---:|---:|---:|---:|
|
|
103
|
-
| Packed | 1.00 |
|
|
177
|
+
| Packed | 1.00 | 56 | 113 | 1.14 | 1.48 | 94 |
|
|
104
178
|
| Gutter only | 1.13 | 0 | 345 | 1.34 | 2.38 | 10 |
|
|
105
179
|
| Method `routes` | 1.50 | 0 | 276 | 1.32 | 2.09 | 2 |
|
|
106
180
|
| Method `lines` | 3.80 | 0 | 382 | 1.20 | 1.47 | 5 |
|
|
107
181
|
|
|
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.
|
|
182
|
+
On the packed board, the links are short only because they go through the boxes. Before ER3d, 62 links went through a box, with 162 crossings. The method `routes` is the default, because it gives the fewest crossings for its area.
|
|
109
183
|
|
|
110
184
|
`sweepRoom` changes one knob at a time on the same board. `growthSettings`, `laneSettings` and `passSettings` give the settings of the benchmark.
|
|
111
185
|
|
|
@@ -122,9 +196,52 @@ On the packed board, the links are short only because they go through the boxes.
|
|
|
122
196
|
|
|
123
197
|
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
198
|
|
|
199
|
+
### Room at each zoom level
|
|
200
|
+
|
|
201
|
+
At a far view, a group of nodes is one box. The boxes of the groups touch, so the level links have no channel. `levelRoom` gives each zoom level the gaps that its own links need.
|
|
202
|
+
|
|
203
|
+
- `levelRoom(nodes, links, root, options)` takes the nodes, the links and the tree of sections. The indices of the tree are the indices of `nodes`.
|
|
204
|
+
- The far levels open first. Each nearer level measures its links on the board of the level before it, and only adds growth. Thus a box moves with its parent.
|
|
205
|
+
- The nodes move by one monotone shift on each axis. Thus each level keeps the order and the alignment of the boxes, and no gap of a far level gets narrower.
|
|
206
|
+
- Each level of the result gives its `items` at their open places, its links (`pairs`), its demand (`gaps`) and its `nodes`.
|
|
207
|
+
- With `board: "shared"`, the default, each level uses the board of the nearest level. No box moves between two levels.
|
|
208
|
+
- With `board: "each"`, each level has its own board. The items of a far level stay nearer together, but a box moves in its parent when the level changes.
|
|
209
|
+
- `depths` sets the depths that get room. With `nodes: true`, the level with each node alone also gets room.
|
|
210
|
+
- The result also gives the `gaps` of the nodes: `openGaps(nodes, gaps)` gives the board of the nearest level. The room strategy `levels` gives these gaps.
|
|
211
|
+
- `levelCut(nodes, root, depth)` gives the items of one level, as the router `sections` makes them.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { buildSections, levelRoom, roomLinks, sectionsRouter } from "@mark1russell7/highways";
|
|
215
|
+
|
|
216
|
+
const root = buildSections(graph.nodes);
|
|
217
|
+
const room = levelRoom(graph.nodes, roomLinks(graph), root, { gutter: 8, spacing: 4 });
|
|
218
|
+
// Each level gives its items at their open places.
|
|
219
|
+
const far = room.levels[1]!.items;
|
|
220
|
+
const result = sectionsRouter({ name: "sections" }).route({ nodes: room.items, edges: graph.edges });
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The router makes the tree of sections again from the open board. To keep the tree of the packed board, give the router a `group` strategy that gives `root`.
|
|
224
|
+
|
|
225
|
+
`benchLevelRoom` measures each level with groups. The board `nestedGridBoard` has 256 touching boxes, and its quadtree has 4, 16 and 64 groups. The fair baseline opens each level to the gutter only. Each link gets a lane of 4 units in each gap, and the crossings count these lanes. The shift is the move of a box in its parent from the level before, in gutters.
|
|
226
|
+
|
|
227
|
+
| Board | Depth | Area | Through | Crossings | Worst detour | Order changes | Mean shift |
|
|
228
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
229
|
+
| Packed | 2 | 1.00 | 6 | 14 | 2.36 | 0 | 0 |
|
|
230
|
+
| Packed | 3 | 1.00 | 36 | 122 | 1.87 | 0 | 0 |
|
|
231
|
+
| Gutter only | 1 | 1.01 | 0 | 0 | 1.32 | 0 | 0.71 |
|
|
232
|
+
| Gutter only | 2 | 1.03 | 0 | 290 | 2.45 | 0 | 0.71 |
|
|
233
|
+
| Gutter only | 3 | 1.07 | 0 | 836 | 2.37 | 0 | 0.71 |
|
|
234
|
+
| Room of the nodes alone (`makeRoom`) | 1 to 3 | 1.61 | 0 | 0, 273, 836 | 1.38, 2.27, 2.15 | 0 | 0 |
|
|
235
|
+
| `levelRoom`, board `shared` | 1 to 3 | 1.35 | 0 | 0, 273, 836 | 1.40, 2.18, 2.14 | 0 | 0 |
|
|
236
|
+
| `levelRoom`, board `each` | 1 | 1.02 | 0 | 0 | 1.41 | 0 | 1.12 |
|
|
237
|
+
| `levelRoom`, board `each` | 2 | 1.17 | 0 | 273 | 2.21 | 0 | 3.66 |
|
|
238
|
+
| `levelRoom`, board `each` | 3 | 1.35 | 0 | 836 | 2.14 | 0 | 2.63 |
|
|
239
|
+
|
|
240
|
+
The room removes each level link through a box, and no level changes the order of the boxes. Against the gutter only, the crossings with lanes fall 6% at depth 2 and stay the same at depth 3. Most crossings come from the links themselves, not from the width of the gaps. The board `shared` uses 16% less area than the room of the nodes alone, and no box moves between two levels. The board `each` keeps the far levels small, but the boxes move 1 to 4 gutters in their parents. Thus `shared` is the default.
|
|
241
|
+
|
|
125
242
|
## Options of the zoom levels
|
|
126
243
|
|
|
127
|
-
The router `sections` makes its hubs and its zoom levels from a tree of groups. Each rule and each limit of that tree is an option. The defaults give the output of version 0.1.5.
|
|
244
|
+
The router `sections` makes its hubs and its zoom levels from a tree of groups. Each rule and each limit of that tree is an option. The defaults give the output of version 0.1.5, but `throughCost` changes the links of a walled-in level.
|
|
128
245
|
|
|
129
246
|
| Option | Default | Where | What it sets |
|
|
130
247
|
|---|---|---|---|
|
|
@@ -139,12 +256,17 @@ The router `sections` makes its hubs and its zoom levels from a tree of groups.
|
|
|
139
256
|
| `collapse` | 32 | `buildSections` | A cell shrinks to one quarter at most this many times without a split. |
|
|
140
257
|
| `tightMargin` | 1 | `levelLinks` | The margin of the second corner graph of the level links. |
|
|
141
258
|
| `plugInset` | 6 | `levelLinks` | A plug of a level link stays this far from the corners of its side. |
|
|
259
|
+
| `throughCost` | 8 | `levelLinks` | The cost of a level link through a far box, in mean box sizes. The value 0 gives the links of version 0.1.5. |
|
|
142
260
|
| `routesAt`, `itemAt` | 22, 120 | `pickLevel` | The far-view thresholds, in pixels on the screen. `PICK` holds them. |
|
|
143
261
|
|
|
144
262
|
`SECTIONS_DEFAULTS`, `SECTION_DEFAULTS`, `LEVEL_LINK_DEFAULTS` and `PICK` give the default numbers. `itemSizes(levels)` gives the sizes that `pickLevel(sizes, scale, options)` reads.
|
|
145
263
|
|
|
146
264
|
The built-in grouping rules are in `groupStrategies`. The rule `quadtree` splits the plane, as `buildSections` does. The rule `keys` makes one group for each key of the nodes. Its default key is the string `data.group` of a node, and a node without a key is a group alone. With the option `split`, a large group splits into a quadtree.
|
|
147
265
|
|
|
266
|
+
The rule `kd` splits the plane with lines across one axis, as a k-d tree does. The option `line` sets the place of each line. The value `median` puts half of the nodes on each side. The value `middle` cuts the cell in half. The value `slide` cuts the cell in half and moves a line with an empty side to the nearest node. With the fan-out 4, each half splits again on the same level.
|
|
267
|
+
|
|
268
|
+
The rule `cut` also splits the plane with lines, but each line cuts the fewest edges. The option `balance` gives the smallest share of the nodes on each side. The option `objective` sets the cost of a line, and the option `pull` moves the line to the middle of the cell. With the option `guard`, each section also tries the split of the quadtree and keeps the split that cuts fewer edges.
|
|
269
|
+
|
|
148
270
|
A consumer rule groups the nodes by its own field. Then the far view shows one item for each group:
|
|
149
271
|
|
|
150
272
|
```ts
|
|
@@ -174,25 +296,65 @@ The benchmark sweeps the hub limit, `hubSlack`, `cap` and the grouping rules on
|
|
|
174
296
|
|
|
175
297
|
A smaller hub limit gives shorter routes and fewer shared runs. No route goes through a box or out and back at any value. On `case-many-groups`, the rule `keys` has one key for each group of cards. It gives 32 items at the far view, and no level link goes out and back.
|
|
176
298
|
|
|
299
|
+
### Links through a walled-in level
|
|
300
|
+
|
|
301
|
+
On a walled-in level, each group overlaps its neighbours, so some level links find no way around the groups. The measures split the level links through a box in two:
|
|
302
|
+
|
|
303
|
+
- An unavoidable link goes only through boxes that overlap its source or its target.
|
|
304
|
+
- An avoidable link also goes through a far box. A far box overlaps neither end of the link.
|
|
305
|
+
|
|
306
|
+
A level link through a far box gets a second way. That way can go through boxes, and each far box costs `throughCost` mean box sizes. The link keeps the way with fewer far boxes. The benchmark sweeps `throughCost`. On `mid-clustered`, at depth 3, in CI:
|
|
307
|
+
|
|
308
|
+
| `throughCost` | Through | Unavoidable | Avoidable | Out and back | Worst detour |
|
|
309
|
+
|---|---:|---:|---:|---:|---:|
|
|
310
|
+
| 0 (version 0.1.5) | 208 | 94 | 114 | 124 | 5.88 |
|
|
311
|
+
| 2 | 208 | 123 | 85 | 124 | 5.88 |
|
|
312
|
+
| 4 | 208 | 141 | 67 | 124 | 5.88 |
|
|
313
|
+
| 8 (default) | 208 | 154 | 54 | 124 | 5.88 |
|
|
314
|
+
| 16 | 208 | 157 | 51 | 124 | 5.88 |
|
|
315
|
+
|
|
316
|
+
The second way does not change the number of links through a box. It moves them from far boxes to the boxes next to their ends.
|
|
317
|
+
|
|
177
318
|
## Strategies
|
|
178
319
|
|
|
179
320
|
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.
|
|
180
321
|
|
|
181
322
|
| Step | Input | Output | Built-in strategies |
|
|
182
323
|
|---|---|---|---|
|
|
183
|
-
| `group` | `Graph` | `Section`: the tree of groups of the router `sections` | `quadtree`, `keys` |
|
|
324
|
+
| `group` | `Graph` | `Section`: the tree of groups of the router `sections` | `quadtree`, `keys`, `kd`, `cut` |
|
|
184
325
|
| `router` | `Graph` | `Result` | `straight`, `plugs`, `corners`, `corners-smooth`, `trunks`, `shared`, `shared-all`, `sections`, `sections-tree` |
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
326
|
+
| `highway` | `HighwayInput`: the graph, and optionally the first routes of a router | `HighwayPlan`: the highways as data | `keys`, `shared-ways`, `corridors` |
|
|
327
|
+
| `curve` | `CurveInput`: the points, the normals of the plugs, the free space and the lane on its board | `CurveSegment[]` | `straight`, `rounded`, `spline`, `arc`, `smooth`, `clothoid`, `highway` |
|
|
328
|
+
| `room` | `RoomInput`: the boxes and the links of a board, and the tree of sections for `levels` | `Gap[]` | `routes`, `lines`, `levels` |
|
|
187
329
|
|
|
188
330
|
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.
|
|
189
331
|
|
|
190
|
-
- `defineStrategy({ step, name, describe, defaults, run, maxNodes? })` makes a strategy. The function `run` gets each option.
|
|
332
|
+
- `defineStrategy({ step, name, describe, defaults, schema?, run, maxNodes? })` makes a strategy. The function `run` gets each option.
|
|
191
333
|
- `createRegistry(strategies)` makes a registry. Two strategies with one step and one name are an error.
|
|
192
334
|
- `register(registry, ...strategies)` gives a new registry. It does not change the registry before it.
|
|
193
335
|
- `listStrategies(registry, step)` gives the strategies of a step, in order. `findStrategy` and `getStrategy` give one strategy by its name.
|
|
194
336
|
- `asRouter(strategy, options?)` gives a `Router`, and `routersOf(registry)` gives the routers of a registry.
|
|
195
|
-
- `STRATEGIES` is the registry of `BUILT_IN_STRATEGIES`. The records `groupStrategies`, `routerStrategies`, `curveStrategies` and `roomStrategies` give each built-in strategy with its typed options.
|
|
337
|
+
- `STRATEGIES` is the registry of `BUILT_IN_STRATEGIES`. The records `groupStrategies`, `routerStrategies`, `highwayStrategies`, `curveStrategies` and `roomStrategies` give each built-in strategy with its typed options.
|
|
338
|
+
|
|
339
|
+
### The step `highway`
|
|
340
|
+
|
|
341
|
+
The step `highway` decides which edges share a way. It gives a plan, not routes. `follow` routes the plan.
|
|
342
|
+
|
|
343
|
+
- A `Highway` has an `id`, a `key`, a `spine`, its `lanes` and its `members`. Lane 0 is on the left of the spine.
|
|
344
|
+
- A `HighwayMember` is an edge with its merge point and its split point. `on` and `off` give their places on the spine.
|
|
345
|
+
- `checkPlan(graph, plan)` lists the problems of a plan. A good plan gives an empty list.
|
|
346
|
+
- `follow(graph, plan, options?)` routes each member from its plug to its merge point, along the spine, and from its split point to its target.
|
|
347
|
+
- `follow` routes an edge that is not a member with its first route, or with the way finder. It gives one run for each stretch that two members or more share.
|
|
348
|
+
- `highwayRouter(strategy)` and `highwayRouterStrategy(strategy)` give a router that makes the plan and then calls `follow`.
|
|
349
|
+
|
|
350
|
+
| Strategy | Decision |
|
|
351
|
+
|---|---|
|
|
352
|
+
| `keys` | The trunks and the runs of the router `trunks`. `planOfResult` reads them. The router does not change. |
|
|
353
|
+
| `shared-ways` | The runs of the router `shared`. `planOfResult` reads them. The router does not change. |
|
|
354
|
+
| `corridors` | The corridor signatures of HWY-031. The links of the first routes get a MinHash signature, and each cluster of ways gives a highway. |
|
|
355
|
+
|
|
356
|
+
The strategy `corridors` joins two ways when they share a long length. The length is `BREAK_EVEN` times the lane width or more, and the share `rho` of the shorter way or more.
|
|
357
|
+
The routers `trunks` and `shared` keep their own code. A test pins their output, so the strategies `keys` and `shared-ways` read the same decision.
|
|
196
358
|
|
|
197
359
|
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:
|
|
198
360
|
|
|
@@ -204,6 +366,7 @@ const elbow = defineStrategy({
|
|
|
204
366
|
name: "elbow",
|
|
205
367
|
describe: "Each edge goes along one axis, then along the other, from center to center.",
|
|
206
368
|
defaults: { turn: "x" as "x" | "y" },
|
|
369
|
+
schema: { turn: { type: "enum", values: ["x", "y"], describe: "The axis of the first leg." } },
|
|
207
370
|
run(graph, o) {
|
|
208
371
|
const box = new Map(graph.nodes.map((n) => [n.id, n.box]));
|
|
209
372
|
const routes = graph.edges.map((e) => {
|
|
@@ -220,8 +383,101 @@ const rows = runBenchmark(routersOf(registry), FIXTURES.slice(0, 1)); // The row
|
|
|
220
383
|
|
|
221
384
|
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.
|
|
222
385
|
|
|
386
|
+
### Declarations of the options
|
|
387
|
+
|
|
388
|
+
Each strategy declares its options in `schema`. A declaration tells the type, the range, the step and the meaning of one option. Tools make controls and sweeps from these declarations, with no other data.
|
|
389
|
+
|
|
390
|
+
| Type | Fields | Values |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| `number`, `integer` | `min`, `max`, `step`, `scale`, `special`, `marks` | A number. An `integer` takes only whole numbers. |
|
|
393
|
+
| `boolean` | | `false` or `true`. |
|
|
394
|
+
| `enum` | `values` | One of `values`. |
|
|
395
|
+
| `function`, `strategy`, `list`, `object` | `step` (for `strategy`) | A rule, a strategy, an array or an object. A sweep cannot set them. |
|
|
396
|
+
|
|
397
|
+
Each declaration has a `describe` sentence and an optional `default`.
|
|
398
|
+
|
|
399
|
+
- The range from `min` to `max` is the range of a control and of a sweep. The strategy can take a value outside it.
|
|
400
|
+
- The `scale` is `linear` or `log`. A sweep on the scale `log` gives values with equal ratios.
|
|
401
|
+
- The `special` values are valid outside the range. `Infinity` sets no limit.
|
|
402
|
+
- The `marks` are the notable values: the default and the values of the sweeps of the benchmark.
|
|
403
|
+
- The `default` is the value that the strategy uses when the caller and `defaults` give no value. A value of `defaults` comes first.
|
|
404
|
+
|
|
405
|
+
The type `OptionSchema<O>` needs one declaration for each key of the options, and no other key. A number option needs a number declaration, and an enum declaration takes only the values of its type.
|
|
406
|
+
|
|
407
|
+
The helpers read the declarations:
|
|
408
|
+
|
|
409
|
+
- `optionDefaults(strategy)` gives the effective default of each option. The strategy gives the same output with this object as with no options.
|
|
410
|
+
- `listOptions(strategy)` gives each option with its key, its declaration and its default, in the order of the schema.
|
|
411
|
+
- `checkOptions(strategy, options)` reports each value without a declaration, of the wrong type, out of the range or not a whole number. It changes no value.
|
|
412
|
+
- `clampOptions(strategy, options)` moves each number into its range, for a control. A special value stays.
|
|
413
|
+
- `checkSchema(strategy)` reports each option of `defaults` without a declaration, and each bad declaration.
|
|
414
|
+
- `sweepValues(declaration, { count, special })` gives the values of a sweep. Without `count`, a number option gives its `marks`.
|
|
415
|
+
- `sweepOf(strategy, key, options)` gives one setting for each value of a sweep. The name of a setting is the key and the value.
|
|
416
|
+
- `sweepGrid(strategy, keys, options)` gives one setting for each combination of the values of the keys.
|
|
417
|
+
|
|
418
|
+
The benchmark functions take these settings. A sweep from the declarations gives the same table as the hub-limit sweep of CUSTOM:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { CASES, routerStrategies, sweepOf, sweepSections } from "@mark1russell7/highways";
|
|
422
|
+
|
|
423
|
+
const graph = CASES.find((c) => c.name === "case-hub-chain")!.make();
|
|
424
|
+
const rows = sweepSections("case-hub-chain", graph, sweepOf(routerStrategies.sections, "stretch"));
|
|
425
|
+
// The settings are "stretch 1.25", "stretch 1.5", "stretch 2", "stretch 3" and "stretch Infinity".
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
A test examines each built-in strategy with `checkSchema`. Thus an option of `defaults` without a declaration makes the test fail.
|
|
429
|
+
|
|
223
430
|
The plugs have one strategy, so they have no step yet. The step `group` gives the grouping rule of the zoom levels.
|
|
224
431
|
|
|
432
|
+
## Measures and the report
|
|
433
|
+
|
|
434
|
+
The [note on the measures](https://github.com/mark1russell7/highways/blob/main/docs/research/MEASURES.md) gives an id, a formula and a scale to each measure. Each measure is a pure function, and its TSDoc names its id.
|
|
435
|
+
|
|
436
|
+
| Ids | Functions |
|
|
437
|
+
|---|---|
|
|
438
|
+
| R1, R2 | `crossingAngles`: the crossings and their angles |
|
|
439
|
+
| R4, R5 | `totalTurn`, `detourSpread` |
|
|
440
|
+
| R11 | `plugSpacing`: the gaps between the plugs of each side |
|
|
441
|
+
| B1, B4 | `inkSaved`, `ambiguity` |
|
|
442
|
+
| C1, C2, C3 | `g1Defect`, `g2Defect`, `bendingEnergy` |
|
|
443
|
+
| Z6, Z7, Z8 | `nestingFaults`, `positionStability`, `orderStability` |
|
|
444
|
+
| Z10 | `changeGraph` makes a seeded small change, and `routeStability` compares the routes |
|
|
445
|
+
| G1, G2, G8, G9 | `modularity`, `cutShare`, `treeCost`, `groupStability` |
|
|
446
|
+
| M2 | `roomOrderFaults` |
|
|
447
|
+
|
|
448
|
+
A quadratic measure examines a seeded sample above its limit, so the values of a large board are estimates.
|
|
449
|
+
|
|
450
|
+
`runSuite` measures several seeds of each fixture family, for the steps `router`, `curve`, `room` and `group`. The first seed of a family is the fixture itself. The report has these parts:
|
|
451
|
+
|
|
452
|
+
- `rows`: one row for each step, fixture, seed and strategy.
|
|
453
|
+
- `summaries`: the median, the interquartile range, the minimum and the maximum of each measure over the seeds.
|
|
454
|
+
- `fronts`: the Pareto front of each step on each fixture family, on the axes of `FRONT_OBJECTIVES`.
|
|
455
|
+
- `rules`: the counts of the six proposed hard rules of MEAS0 for each strategy.
|
|
456
|
+
|
|
457
|
+
A strategy with a violation of a hard rule of today does not enter the front.
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
import { formatFronts, runSuite } from "@mark1russell7/highways";
|
|
461
|
+
|
|
462
|
+
const { report } = runSuite({ max: 1000, seeds: 5 });
|
|
463
|
+
console.log(formatFronts(report.fronts));
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The script `bench` takes `--seeds <n>` and `--report <file>`. A family with more nodes than `--seeds-max-nodes` (default 1 000) gets one seed only. `--suite-steps <a,b>` runs only these steps of the suite. The workflow `Bench` keeps `report.json` and `bench.json` as an artifact. The tables of today do not change.
|
|
467
|
+
|
|
468
|
+
The workflow `Bench` runs the suite in parts. Each part writes its report, and `joinReports` joins the reports into the report of one run.
|
|
469
|
+
|
|
470
|
+
`compareStep` runs each strategy of one step on one board, with the measures of the suite. It gives one row for each strategy and the Pareto front of the step. The options give each strategy its own options. The compare page of the site uses it.
|
|
471
|
+
|
|
472
|
+
With the option `lanes`, the step `curve` opens each shared way of the routes of `curveRouter` into lanes, and each curve draws the lanes. Each input then has its `lane`, so the curve `highway` bends the lanes of a bundle as one with each of its options.
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import { compareStep } from "@mark1russell7/highways";
|
|
476
|
+
|
|
477
|
+
const { rows, front } = compareStep("curve", graph, { options: { arc: { radius: 20 } } });
|
|
478
|
+
const lanes = compareStep("curve", graph, { curveRouter: "shared", lanes: { width: 5, maxWidth: 30 }, options: { highway: { jog: "quintic" } } });
|
|
479
|
+
```
|
|
480
|
+
|
|
225
481
|
## License
|
|
226
482
|
|
|
227
483
|
MIT
|