@mark1russell7/highways 0.1.5 → 0.3.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 +293 -8
- package/build/index.d.ts +1774 -236
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9307 -3090
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/compare.ts +145 -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/sections.ts +121 -0
- package/src/bench/suite.ts +781 -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 +4 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/groups/keys/index.ts +81 -0
- package/src/groups/quadtree/index.ts +10 -0
- package/src/index.ts +7 -0
- package/src/levels.ts +178 -9
- package/src/model.ts +2 -0
- package/src/pick.ts +36 -0
- 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 +78 -55
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +93 -4
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +153 -7
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +29 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +221 -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,24 +196,144 @@ 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
|
+
|
|
242
|
+
## Options of the zoom levels
|
|
243
|
+
|
|
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.
|
|
245
|
+
|
|
246
|
+
| Option | Default | Where | What it sets |
|
|
247
|
+
|---|---|---|---|
|
|
248
|
+
| `group` | the quadtree | `sectionsRouter` | The grouping rule: a strategy of the step `group`. `groupOptions` gives its options. |
|
|
249
|
+
| `levels` | each depth | `sectionsRouter` | The depths of the tree that get a level with groups. The level of single nodes always comes last. |
|
|
250
|
+
| `stretch` | 2 | `sectionsRouter` | The hub limit. A way through the hubs is at most this many times the straight line. `Infinity` keeps each such way. |
|
|
251
|
+
| `stretchPad` | 4 | `sectionsRouter` | A way through the hubs can be this many margins longer than the hub limit. |
|
|
252
|
+
| `hubSlack` | 80 | `sectionsRouter` | A way skips a hub that is this many units farther from the target than the way was before. |
|
|
253
|
+
| `hubs` | `Infinity` | `sectionsRouter` | The number of hubs on each side of the lowest common section that a way passes. |
|
|
254
|
+
| `cap` | 8 | `buildSections` | The section limit. A section with more nodes splits in four. |
|
|
255
|
+
| `maxDepth` | 16 | `buildSections` | The depth limit of the tree. |
|
|
256
|
+
| `collapse` | 32 | `buildSections` | A cell shrinks to one quarter at most this many times without a split. |
|
|
257
|
+
| `tightMargin` | 1 | `levelLinks` | The margin of the second corner graph of the level links. |
|
|
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. |
|
|
260
|
+
| `routesAt`, `itemAt` | 22, 120 | `pickLevel` | The far-view thresholds, in pixels on the screen. `PICK` holds them. |
|
|
261
|
+
|
|
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.
|
|
263
|
+
|
|
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.
|
|
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
|
+
|
|
270
|
+
A consumer rule groups the nodes by its own field. Then the far view shows one item for each group:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { defineStrategy, keyedSections, sectionsRouter } from "@mark1russell7/highways";
|
|
274
|
+
|
|
275
|
+
const byTeam = defineStrategy({
|
|
276
|
+
step: "group",
|
|
277
|
+
name: "team",
|
|
278
|
+
describe: "One group for each team.",
|
|
279
|
+
defaults: {},
|
|
280
|
+
run: (graph) => keyedSections(graph.nodes, { key: (node) => String(node.data?.["team"]) }),
|
|
281
|
+
});
|
|
282
|
+
const result = sectionsRouter({ name: "teams", group: byTeam }).route(graph);
|
|
283
|
+
// result.levels[1] has one item for each team. The id of an item is "s:" and the team.
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The router examines the tree of a consumer rule with `checkSections`. Each node must be in exactly one leaf. The children of a section split its nodes, and a child is one level deeper than its parent.
|
|
287
|
+
|
|
288
|
+
The benchmark sweeps the hub limit, `hubSlack`, `cap` and the grouping rules on the case boards. Each table starts with the defaults. On `case-hub-chain`:
|
|
289
|
+
|
|
290
|
+
| `stretch` | Worst detour | Routes through hubs | Shared runs | Out and back |
|
|
291
|
+
|---|---:|---:|---:|---:|
|
|
292
|
+
| 1.25 | 1.19 | 14 | 4 | 0 |
|
|
293
|
+
| 1.5 | 1.39 | 20 | 8 | 0 |
|
|
294
|
+
| 2 (default) | 1.78 | 24 | 10 | 0 |
|
|
295
|
+
| 3 | 2.05 | 25 | 11 | 0 |
|
|
296
|
+
|
|
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.
|
|
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
|
+
|
|
125
318
|
## Strategies
|
|
126
319
|
|
|
127
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.
|
|
128
321
|
|
|
129
322
|
| Step | Input | Output | Built-in strategies |
|
|
130
323
|
|---|---|---|---|
|
|
324
|
+
| `group` | `Graph` | `Section`: the tree of groups of the router `sections` | `quadtree`, `keys`, `kd`, `cut` |
|
|
131
325
|
| `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` |
|
|
326
|
+
| `curve` | `CurveInput`: the points, the normals of the plugs and the free space | `CurveSegment[]` | `straight`, `rounded`, `spline`, `arc`, `smooth`, `clothoid` |
|
|
327
|
+
| `room` | `RoomInput`: the boxes and the links of a board, and the tree of sections for `levels` | `Gap[]` | `routes`, `lines`, `levels` |
|
|
134
328
|
|
|
135
329
|
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
330
|
|
|
137
|
-
- `defineStrategy({ step, name, describe, defaults, run, maxNodes? })` makes a strategy. The function `run` gets each option.
|
|
331
|
+
- `defineStrategy({ step, name, describe, defaults, schema?, run, maxNodes? })` makes a strategy. The function `run` gets each option.
|
|
138
332
|
- `createRegistry(strategies)` makes a registry. Two strategies with one step and one name are an error.
|
|
139
333
|
- `register(registry, ...strategies)` gives a new registry. It does not change the registry before it.
|
|
140
334
|
- `listStrategies(registry, step)` gives the strategies of a step, in order. `findStrategy` and `getStrategy` give one strategy by its name.
|
|
141
335
|
- `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.
|
|
336
|
+
- `STRATEGIES` is the registry of `BUILT_IN_STRATEGIES`. The records `groupStrategies`, `routerStrategies`, `curveStrategies` and `roomStrategies` give each built-in strategy with its typed options.
|
|
143
337
|
|
|
144
338
|
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
339
|
|
|
@@ -151,6 +345,7 @@ const elbow = defineStrategy({
|
|
|
151
345
|
name: "elbow",
|
|
152
346
|
describe: "Each edge goes along one axis, then along the other, from center to center.",
|
|
153
347
|
defaults: { turn: "x" as "x" | "y" },
|
|
348
|
+
schema: { turn: { type: "enum", values: ["x", "y"], describe: "The axis of the first leg." } },
|
|
154
349
|
run(graph, o) {
|
|
155
350
|
const box = new Map(graph.nodes.map((n) => [n.id, n.box]));
|
|
156
351
|
const routes = graph.edges.map((e) => {
|
|
@@ -167,7 +362,97 @@ const rows = runBenchmark(routersOf(registry), FIXTURES.slice(0, 1)); // The row
|
|
|
167
362
|
|
|
168
363
|
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
364
|
|
|
170
|
-
|
|
365
|
+
### Declarations of the options
|
|
366
|
+
|
|
367
|
+
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.
|
|
368
|
+
|
|
369
|
+
| Type | Fields | Values |
|
|
370
|
+
|---|---|---|
|
|
371
|
+
| `number`, `integer` | `min`, `max`, `step`, `scale`, `special`, `marks` | A number. An `integer` takes only whole numbers. |
|
|
372
|
+
| `boolean` | | `false` or `true`. |
|
|
373
|
+
| `enum` | `values` | One of `values`. |
|
|
374
|
+
| `function`, `strategy`, `list`, `object` | `step` (for `strategy`) | A rule, a strategy, an array or an object. A sweep cannot set them. |
|
|
375
|
+
|
|
376
|
+
Each declaration has a `describe` sentence and an optional `default`.
|
|
377
|
+
|
|
378
|
+
- The range from `min` to `max` is the range of a control and of a sweep. The strategy can take a value outside it.
|
|
379
|
+
- The `scale` is `linear` or `log`. A sweep on the scale `log` gives values with equal ratios.
|
|
380
|
+
- The `special` values are valid outside the range. `Infinity` sets no limit.
|
|
381
|
+
- The `marks` are the notable values: the default and the values of the sweeps of the benchmark.
|
|
382
|
+
- The `default` is the value that the strategy uses when the caller and `defaults` give no value. A value of `defaults` comes first.
|
|
383
|
+
|
|
384
|
+
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.
|
|
385
|
+
|
|
386
|
+
The helpers read the declarations:
|
|
387
|
+
|
|
388
|
+
- `optionDefaults(strategy)` gives the effective default of each option. The strategy gives the same output with this object as with no options.
|
|
389
|
+
- `listOptions(strategy)` gives each option with its key, its declaration and its default, in the order of the schema.
|
|
390
|
+
- `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.
|
|
391
|
+
- `clampOptions(strategy, options)` moves each number into its range, for a control. A special value stays.
|
|
392
|
+
- `checkSchema(strategy)` reports each option of `defaults` without a declaration, and each bad declaration.
|
|
393
|
+
- `sweepValues(declaration, { count, special })` gives the values of a sweep. Without `count`, a number option gives its `marks`.
|
|
394
|
+
- `sweepOf(strategy, key, options)` gives one setting for each value of a sweep. The name of a setting is the key and the value.
|
|
395
|
+
- `sweepGrid(strategy, keys, options)` gives one setting for each combination of the values of the keys.
|
|
396
|
+
|
|
397
|
+
The benchmark functions take these settings. A sweep from the declarations gives the same table as the hub-limit sweep of CUSTOM:
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
import { CASES, routerStrategies, sweepOf, sweepSections } from "@mark1russell7/highways";
|
|
401
|
+
|
|
402
|
+
const graph = CASES.find((c) => c.name === "case-hub-chain")!.make();
|
|
403
|
+
const rows = sweepSections("case-hub-chain", graph, sweepOf(routerStrategies.sections, "stretch"));
|
|
404
|
+
// The settings are "stretch 1.25", "stretch 1.5", "stretch 2", "stretch 3" and "stretch Infinity".
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
A test examines each built-in strategy with `checkSchema`. Thus an option of `defaults` without a declaration makes the test fail.
|
|
408
|
+
|
|
409
|
+
The plugs have one strategy, so they have no step yet. The step `group` gives the grouping rule of the zoom levels.
|
|
410
|
+
|
|
411
|
+
## Measures and the report
|
|
412
|
+
|
|
413
|
+
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.
|
|
414
|
+
|
|
415
|
+
| Ids | Functions |
|
|
416
|
+
|---|---|
|
|
417
|
+
| R1, R2 | `crossingAngles`: the crossings and their angles |
|
|
418
|
+
| R4, R5 | `totalTurn`, `detourSpread` |
|
|
419
|
+
| R11 | `plugSpacing`: the gaps between the plugs of each side |
|
|
420
|
+
| B1, B4 | `inkSaved`, `ambiguity` |
|
|
421
|
+
| C1, C2, C3 | `g1Defect`, `g2Defect`, `bendingEnergy` |
|
|
422
|
+
| Z6, Z7, Z8 | `nestingFaults`, `positionStability`, `orderStability` |
|
|
423
|
+
| Z10 | `changeGraph` makes a seeded small change, and `routeStability` compares the routes |
|
|
424
|
+
| G1, G2, G8, G9 | `modularity`, `cutShare`, `treeCost`, `groupStability` |
|
|
425
|
+
| M2 | `roomOrderFaults` |
|
|
426
|
+
|
|
427
|
+
A quadratic measure examines a seeded sample above its limit, so the values of a large board are estimates.
|
|
428
|
+
|
|
429
|
+
`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:
|
|
430
|
+
|
|
431
|
+
- `rows`: one row for each step, fixture, seed and strategy.
|
|
432
|
+
- `summaries`: the median, the interquartile range, the minimum and the maximum of each measure over the seeds.
|
|
433
|
+
- `fronts`: the Pareto front of each step on each fixture family, on the axes of `FRONT_OBJECTIVES`.
|
|
434
|
+
- `rules`: the counts of the six proposed hard rules of MEAS0 for each strategy.
|
|
435
|
+
|
|
436
|
+
A strategy with a violation of a hard rule of today does not enter the front.
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
import { formatFronts, runSuite } from "@mark1russell7/highways";
|
|
440
|
+
|
|
441
|
+
const { report } = runSuite({ max: 1000, seeds: 5 });
|
|
442
|
+
console.log(formatFronts(report.fronts));
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
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.
|
|
446
|
+
|
|
447
|
+
The workflow `Bench` runs the suite in parts. Each part writes its report, and `joinReports` joins the reports into the report of one run.
|
|
448
|
+
|
|
449
|
+
`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.
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { compareStep } from "@mark1russell7/highways";
|
|
453
|
+
|
|
454
|
+
const { rows, front } = compareStep("curve", graph, { options: { arc: { radius: 20 } } });
|
|
455
|
+
```
|
|
171
456
|
|
|
172
457
|
## License
|
|
173
458
|
|