@mark1russell7/highways 0.1.4 → 0.2.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 +118 -0
- package/build/index.d.ts +438 -74
- package/build/index.d.ts.map +1 -1
- package/build/index.js +837 -68
- 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/bench/sections.ts +121 -0
- package/src/groups/index.ts +2 -0
- package/src/groups/keys/index.ts +81 -0
- package/src/groups/quadtree/index.ts +10 -0
- package/src/index.ts +5 -0
- package/src/levels.ts +17 -6
- package/src/model.ts +2 -0
- package/src/pick.ts +36 -0
- package/src/room/index.ts +22 -2
- package/src/room/lanes.ts +175 -0
- package/src/routers/index.ts +1 -1
- package/src/routers/sections.ts +66 -18
- package/src/sections.ts +46 -3
- package/src/strategies/builtins.ts +194 -0
- package/src/strategies/index.ts +2 -0
- package/src/strategies/model.ts +161 -0
package/build/index.js
CHANGED
|
@@ -1510,7 +1510,7 @@ var Obstacles = class {
|
|
|
1510
1510
|
hit(a, b, pad, skip) {
|
|
1511
1511
|
let found;
|
|
1512
1512
|
this.#grid.someSegment(a, b, pad, (n) => {
|
|
1513
|
-
if (skip?.has(n) || !hits(a, b, n, pad)) return false;
|
|
1513
|
+
if (skip?.has(n) || !hits$1(a, b, n, pad)) return false;
|
|
1514
1514
|
found = n;
|
|
1515
1515
|
return true;
|
|
1516
1516
|
});
|
|
@@ -1543,7 +1543,7 @@ var Obstacles = class {
|
|
|
1543
1543
|
}
|
|
1544
1544
|
};
|
|
1545
1545
|
/** This function tells if the segment `ab` goes into the box of the node, made larger by `pad`. */
|
|
1546
|
-
function hits(a, b, n, pad) {
|
|
1546
|
+
function hits$1(a, b, n, pad) {
|
|
1547
1547
|
const x = n.box;
|
|
1548
1548
|
return segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad);
|
|
1549
1549
|
}
|
|
@@ -1738,7 +1738,7 @@ var CornerGraph = class {
|
|
|
1738
1738
|
if ((full & cone) !== 0) continue;
|
|
1739
1739
|
}
|
|
1740
1740
|
let blocked = false;
|
|
1741
|
-
for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits(p, q, blockers[b], pad);
|
|
1741
|
+
for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits$1(p, q, blockers[b], pad);
|
|
1742
1742
|
if (blocked) continue;
|
|
1743
1743
|
const hit = this.#obstacles.hit(p, q, pad);
|
|
1744
1744
|
if (hit !== void 0) blockers.push(hit);
|
|
@@ -2340,13 +2340,20 @@ function orderLanes(run, routes) {
|
|
|
2340
2340
|
return [...placed.map((x) => x.edge), ...lost.sort()];
|
|
2341
2341
|
}
|
|
2342
2342
|
// src/sections.ts
|
|
2343
|
+
/** The default values of `SectionOptions`. */
|
|
2344
|
+
const SECTION_DEFAULTS = Object.freeze({
|
|
2345
|
+
cap: 8,
|
|
2346
|
+
maxDepth: 16,
|
|
2347
|
+
collapse: 32
|
|
2348
|
+
});
|
|
2343
2349
|
/**
|
|
2344
2350
|
* This function makes the tree of sections of the nodes. A cell with more than `cap` nodes splits in four.
|
|
2345
2351
|
* When the nodes of a cell are all in one quarter, the cell becomes that quarter and does not make a level.
|
|
2346
2352
|
*/
|
|
2347
2353
|
function buildSections(nodes, options = {}) {
|
|
2348
|
-
const cap = Math.max(1, options.cap ??
|
|
2349
|
-
const maxDepth = options.maxDepth ??
|
|
2354
|
+
const cap = Math.max(1, options.cap ?? SECTION_DEFAULTS.cap);
|
|
2355
|
+
const maxDepth = options.maxDepth ?? SECTION_DEFAULTS.maxDepth;
|
|
2356
|
+
const collapse = options.collapse ?? SECTION_DEFAULTS.collapse;
|
|
2350
2357
|
const centers = nodes.map((n) => center(n.box));
|
|
2351
2358
|
let x0 = Infinity;
|
|
2352
2359
|
let y0 = Infinity;
|
|
@@ -2371,7 +2378,7 @@ function buildSections(nodes, options = {}) {
|
|
|
2371
2378
|
};
|
|
2372
2379
|
const make = (id, depth, cell, list) => {
|
|
2373
2380
|
let box = cell;
|
|
2374
|
-
for (let guard = 0; list.length > cap && depth < maxDepth && guard <
|
|
2381
|
+
for (let guard = 0; list.length > cap && depth < maxDepth && guard < collapse; guard++) {
|
|
2375
2382
|
const mx = box.x + box.w / 2;
|
|
2376
2383
|
const my = box.y + box.h / 2;
|
|
2377
2384
|
const parts = [
|
|
@@ -2425,6 +2432,156 @@ function allSections(root) {
|
|
|
2425
2432
|
function treeDepth(root) {
|
|
2426
2433
|
return root.children.reduce((d, c) => Math.max(d, treeDepth(c)), root.depth);
|
|
2427
2434
|
}
|
|
2435
|
+
/**
|
|
2436
|
+
* This function examines a tree of sections from a grouping rule. It throws an error when the tree is not valid for `count` nodes.
|
|
2437
|
+
* In a valid tree, each node is in exactly one leaf. The children of a section split its nodes, and a child is one level deeper than its parent.
|
|
2438
|
+
*/
|
|
2439
|
+
function checkSections(root, count) {
|
|
2440
|
+
const seen = new Array(count).fill(0);
|
|
2441
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2442
|
+
const fail = (s, what) => {
|
|
2443
|
+
throw new Error(`The tree of sections is not valid at the section "${s.id}": ${what}.`);
|
|
2444
|
+
};
|
|
2445
|
+
if (root.depth !== 0) fail(root, "the root has a depth that is not 0");
|
|
2446
|
+
if (root.nodes.length !== count) fail(root, `the root has ${root.nodes.length} nodes, and the graph has ${count}`);
|
|
2447
|
+
const walk = (s) => {
|
|
2448
|
+
if (ids.has(s.id)) fail(s, "two sections have this id");
|
|
2449
|
+
ids.add(s.id);
|
|
2450
|
+
if (s.children.length === 0) {
|
|
2451
|
+
for (const i of s.nodes) {
|
|
2452
|
+
if (!Number.isInteger(i) || i < 0 || i >= count) fail(s, `the node index ${i} is not in the graph`);
|
|
2453
|
+
seen[i]++;
|
|
2454
|
+
}
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
let total = 0;
|
|
2458
|
+
const own = new Set(s.nodes);
|
|
2459
|
+
for (const c of s.children) {
|
|
2460
|
+
if (c.depth !== s.depth + 1) fail(c, "a child is not one level deeper than its parent");
|
|
2461
|
+
for (const i of c.nodes) if (!own.has(i)) fail(c, `the node index ${i} is not in the parent`);
|
|
2462
|
+
total += c.nodes.length;
|
|
2463
|
+
walk(c);
|
|
2464
|
+
}
|
|
2465
|
+
if (total !== s.nodes.length) fail(s, "the children do not split the nodes of the section");
|
|
2466
|
+
};
|
|
2467
|
+
walk(root);
|
|
2468
|
+
const lost = seen.findIndex((n) => n !== 1);
|
|
2469
|
+
if (lost >= 0) fail(root, `the node index ${lost} is in ${seen[lost]} leaves, not in one`);
|
|
2470
|
+
}
|
|
2471
|
+
// src/groups/quadtree/index.ts
|
|
2472
|
+
/**
|
|
2473
|
+
* This function gives the tree of sections of the grouping rule `quadtree`: the cells of a quadtree of the plane (`buildSections`).
|
|
2474
|
+
* It is the default grouping rule of the router `sections`.
|
|
2475
|
+
*/
|
|
2476
|
+
function quadtreeGroups(graph, options = {}) {
|
|
2477
|
+
return buildSections(graph.nodes, options);
|
|
2478
|
+
}
|
|
2479
|
+
// src/groups/keys/index.ts
|
|
2480
|
+
/** This key rule gives the string `data.group` of the node, or `undefined` when the node has no such string. */
|
|
2481
|
+
const byDataGroup = (node) => {
|
|
2482
|
+
const group = node.data?.["group"];
|
|
2483
|
+
return typeof group === "string" ? group : void 0;
|
|
2484
|
+
};
|
|
2485
|
+
/** This function gives the bounding box of the boxes of the nodes of the list. */
|
|
2486
|
+
function boundsOf(nodes, list) {
|
|
2487
|
+
let x0 = Infinity;
|
|
2488
|
+
let y0 = Infinity;
|
|
2489
|
+
let x1 = -Infinity;
|
|
2490
|
+
let y1 = -Infinity;
|
|
2491
|
+
for (const i of list) {
|
|
2492
|
+
const b = nodes[i].box;
|
|
2493
|
+
x0 = Math.min(x0, b.x);
|
|
2494
|
+
y0 = Math.min(y0, b.y);
|
|
2495
|
+
x1 = Math.max(x1, b.x + b.w);
|
|
2496
|
+
y1 = Math.max(y1, b.y + b.h);
|
|
2497
|
+
}
|
|
2498
|
+
return list.length ? {
|
|
2499
|
+
x: x0,
|
|
2500
|
+
y: y0,
|
|
2501
|
+
w: x1 - x0,
|
|
2502
|
+
h: y1 - y0
|
|
2503
|
+
} : {
|
|
2504
|
+
x: 0,
|
|
2505
|
+
y: 0,
|
|
2506
|
+
w: 0,
|
|
2507
|
+
h: 0
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* This function gives the tree of sections of the grouping rule `keys`. The root holds each node. Each child of the root holds the nodes of one key,
|
|
2512
|
+
* in the order of their first node. The id of a group is `s:` and its key. A node without a key is a group alone, with the id `s#` and its index.
|
|
2513
|
+
* Thus the far view of the router `sections` shows one item for each group.
|
|
2514
|
+
*/
|
|
2515
|
+
function keyedSections(nodes, options = {}) {
|
|
2516
|
+
const key = options.key ?? byDataGroup;
|
|
2517
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2518
|
+
nodes.forEach((n, i) => {
|
|
2519
|
+
const k = key(n, i);
|
|
2520
|
+
const id = k === void 0 ? `s#${i}` : `s:${k}`;
|
|
2521
|
+
const list = groups.get(id);
|
|
2522
|
+
if (list) list.push(i);
|
|
2523
|
+
else groups.set(id, [i]);
|
|
2524
|
+
});
|
|
2525
|
+
const children = [...groups].map(([id, list]) => group(nodes, id, list, options.split));
|
|
2526
|
+
return {
|
|
2527
|
+
id: "s",
|
|
2528
|
+
depth: 0,
|
|
2529
|
+
cell: boundsOf(nodes, nodes.map((_, i) => i)),
|
|
2530
|
+
nodes: nodes.map((_, i) => i),
|
|
2531
|
+
children
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
/** This function gives the section of one group at depth 1. With `split`, the section has the quadtree of its nodes below it. */
|
|
2535
|
+
function group(nodes, id, list, split) {
|
|
2536
|
+
if (!split) return {
|
|
2537
|
+
id,
|
|
2538
|
+
depth: 1,
|
|
2539
|
+
cell: boundsOf(nodes, list),
|
|
2540
|
+
nodes: list,
|
|
2541
|
+
children: []
|
|
2542
|
+
};
|
|
2543
|
+
const tree = buildSections(list.map((i) => nodes[i]), split);
|
|
2544
|
+
const rebase = (s) => ({
|
|
2545
|
+
id: s.id === "s" ? id : `${id}/${s.id.slice(1)}`,
|
|
2546
|
+
depth: s.depth + 1,
|
|
2547
|
+
cell: s.cell,
|
|
2548
|
+
nodes: s.nodes.map((j) => list[j]),
|
|
2549
|
+
children: s.children.map(rebase)
|
|
2550
|
+
});
|
|
2551
|
+
return rebase(tree);
|
|
2552
|
+
}
|
|
2553
|
+
/** This function gives the tree of sections of the grouping rule `keys` for a graph. */
|
|
2554
|
+
function keyGroups(graph, options = {}) {
|
|
2555
|
+
return keyedSections(graph.nodes, options);
|
|
2556
|
+
}
|
|
2557
|
+
// src/pick.ts
|
|
2558
|
+
/** This function gives the median width of the shown items of each level, in units of the plane. */
|
|
2559
|
+
function itemSizes(levels) {
|
|
2560
|
+
return levels.map((level) => {
|
|
2561
|
+
const sizes = level.items.map((i) => Math.max(i.box.w, i.box.h)).sort((a, b) => a - b);
|
|
2562
|
+
return sizes.length ? sizes[Math.floor(sizes.length / 2)] : 0;
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
/** The default thresholds: a node of 22 pixels shows its routes, and a group of 120 pixels shows its parts. */
|
|
2566
|
+
const PICK = Object.freeze({
|
|
2567
|
+
routesAt: 22,
|
|
2568
|
+
itemAt: 120
|
|
2569
|
+
});
|
|
2570
|
+
/**
|
|
2571
|
+
* This function picks the level that a camera shows at a scale, in pixels for each unit of the plane. The sizes come from `itemSizes`.
|
|
2572
|
+
* The value `sizes.length` stands for the routes of each edge. The last level holds the nodes alone.
|
|
2573
|
+
* Each threshold that `options` does not give gets its value from `PICK`.
|
|
2574
|
+
*/
|
|
2575
|
+
function pickLevel(sizes, scale, options = PICK) {
|
|
2576
|
+
const routesAt = options.routesAt ?? PICK.routesAt;
|
|
2577
|
+
const itemAt = options.itemAt ?? PICK.itemAt;
|
|
2578
|
+
const n = sizes.length;
|
|
2579
|
+
if (n === 0) return 0;
|
|
2580
|
+
if (sizes[n - 1] * scale >= routesAt) return n;
|
|
2581
|
+
let pick = 0;
|
|
2582
|
+
for (let d = 0; d < n - 1; d++) if (sizes[d] * scale >= itemAt) pick = d;
|
|
2583
|
+
return pick;
|
|
2584
|
+
}
|
|
2428
2585
|
// src/routers/corners.ts
|
|
2429
2586
|
/**
|
|
2430
2587
|
* This function makes a router that goes around the nodes.
|
|
@@ -2623,9 +2780,12 @@ function sumFaults(list) {
|
|
|
2623
2780
|
});
|
|
2624
2781
|
}
|
|
2625
2782
|
// src/levels.ts
|
|
2626
|
-
/** The
|
|
2627
|
-
const
|
|
2628
|
-
|
|
2783
|
+
/** The default values of the two limits of `LevelLinkOptions`. */
|
|
2784
|
+
const LEVEL_LINK_DEFAULTS = Object.freeze({
|
|
2785
|
+
tightMargin: 1,
|
|
2786
|
+
plugInset: 6
|
|
2787
|
+
});
|
|
2788
|
+
const EPS$1 = 1e-6;
|
|
2629
2789
|
/**
|
|
2630
2790
|
* This function removes the repeated points, the points in the middle of a straight run,
|
|
2631
2791
|
* and the points where the way turns back on itself.
|
|
@@ -2633,15 +2793,15 @@ const EPS = 1e-6;
|
|
|
2633
2793
|
function tidy(p) {
|
|
2634
2794
|
const out = [];
|
|
2635
2795
|
for (const q of p) {
|
|
2636
|
-
if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
|
|
2796
|
+
if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$1) continue;
|
|
2637
2797
|
while (out.length >= 2) {
|
|
2638
2798
|
const a = out[out.length - 2];
|
|
2639
2799
|
const b = out[out.length - 1];
|
|
2640
2800
|
const cross = (b.x - a.x) * (q.y - b.y) - (b.y - a.y) * (q.x - b.x);
|
|
2641
|
-
if (Math.abs(cross) > EPS * (1 + dist(a, b) * dist(b, q))) break;
|
|
2801
|
+
if (Math.abs(cross) > EPS$1 * (1 + dist(a, b) * dist(b, q))) break;
|
|
2642
2802
|
out.pop();
|
|
2643
2803
|
}
|
|
2644
|
-
if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
|
|
2804
|
+
if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$1) continue;
|
|
2645
2805
|
out.push(q);
|
|
2646
2806
|
}
|
|
2647
2807
|
return out;
|
|
@@ -2659,7 +2819,7 @@ function levelLinks(items, pairs, options = {}) {
|
|
|
2659
2819
|
}));
|
|
2660
2820
|
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
2661
2821
|
const ways = levelWays(nodes, options);
|
|
2662
|
-
const inset = Math.min(
|
|
2822
|
+
const inset = Math.min(options.plugInset ?? LEVEL_LINK_DEFAULTS.plugInset, ways.margin);
|
|
2663
2823
|
const out = [];
|
|
2664
2824
|
for (const p of pairs) {
|
|
2665
2825
|
const s = byId.get(p.source);
|
|
@@ -2713,7 +2873,7 @@ function levelWays(nodes, options) {
|
|
|
2713
2873
|
});
|
|
2714
2874
|
let tight;
|
|
2715
2875
|
const tightGraph = () => tight ??= new CornerGraph(nodes, obstacles, {
|
|
2716
|
-
margin: Math.min(margin,
|
|
2876
|
+
margin: Math.min(margin, options.tightMargin ?? LEVEL_LINK_DEFAULTS.tightMargin),
|
|
2717
2877
|
k,
|
|
2718
2878
|
cones: true
|
|
2719
2879
|
});
|
|
@@ -2881,7 +3041,7 @@ function freeSpans(lo, hi, covered) {
|
|
|
2881
3041
|
if (x >= hi) break;
|
|
2882
3042
|
}
|
|
2883
3043
|
if (x < hi) out.push([x, hi]);
|
|
2884
|
-
return out.filter(([a, b]) => b - a > EPS);
|
|
3044
|
+
return out.filter(([a, b]) => b - a > EPS$1);
|
|
2885
3045
|
}
|
|
2886
3046
|
/**
|
|
2887
3047
|
* This function gives the shortest way between the free plugs of the two nodes, or `undefined` when the second corner graph has none.
|
|
@@ -3013,7 +3173,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
|
|
|
3013
3173
|
out.push(...step.legs);
|
|
3014
3174
|
const end = out[out.length - 1];
|
|
3015
3175
|
const before = out[out.length - 2];
|
|
3016
|
-
axis = Math.abs(end.y - before.y) < EPS ? "x" : Math.abs(end.x - before.x) < EPS ? "y" : void 0;
|
|
3176
|
+
axis = Math.abs(end.y - before.y) < EPS$1 ? "x" : Math.abs(end.x - before.x) < EPS$1 ? "y" : void 0;
|
|
3017
3177
|
i = step.j;
|
|
3018
3178
|
}
|
|
3019
3179
|
return out;
|
|
@@ -3023,7 +3183,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
|
|
|
3023
3183
|
* Of two clear corners, it picks the corner that continues the axis `from` and arrives along the axis `to`.
|
|
3024
3184
|
*/
|
|
3025
3185
|
function square(a, b, obstacles, pad, from, to) {
|
|
3026
|
-
if (Math.abs(a.x - b.x) < EPS || Math.abs(a.y - b.y) < EPS) return obstacles.clear(a, b, pad) ? [b] : void 0;
|
|
3186
|
+
if (Math.abs(a.x - b.x) < EPS$1 || Math.abs(a.y - b.y) < EPS$1) return obstacles.clear(a, b, pad) ? [b] : void 0;
|
|
3027
3187
|
const options = [{
|
|
3028
3188
|
c: {
|
|
3029
3189
|
x: b.x,
|
|
@@ -3041,13 +3201,20 @@ function square(a, b, obstacles, pad, from, to) {
|
|
|
3041
3201
|
for (const { c } of options) if (obstacles.clear(a, c, pad) && obstacles.clear(c, b, pad)) return [c, b];
|
|
3042
3202
|
}
|
|
3043
3203
|
// src/routers/sections.ts
|
|
3204
|
+
/** The default values of the numbers of `SectionsOptions`: the limits of the hubs, of the tree and of the level links. */
|
|
3205
|
+
const SECTIONS_DEFAULTS = Object.freeze({
|
|
3206
|
+
cap: SECTION_DEFAULTS.cap,
|
|
3207
|
+
maxDepth: SECTION_DEFAULTS.maxDepth,
|
|
3208
|
+
collapse: SECTION_DEFAULTS.collapse,
|
|
3209
|
+
tightMargin: LEVEL_LINK_DEFAULTS.tightMargin,
|
|
3210
|
+
plugInset: LEVEL_LINK_DEFAULTS.plugInset,
|
|
3211
|
+
hubs: Infinity,
|
|
3212
|
+
stretch: 2,
|
|
3213
|
+
stretchPad: 4,
|
|
3214
|
+
hubSlack: 80
|
|
3215
|
+
});
|
|
3044
3216
|
const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
|
|
3045
3217
|
/**
|
|
3046
|
-
* A hub can be this many units farther from the target than the nearest approach of the way before it.
|
|
3047
|
-
* A hub farther away goes out and back, so the way skips it.
|
|
3048
|
-
*/
|
|
3049
|
-
const HUB_SLACK = 80;
|
|
3050
|
-
/**
|
|
3051
3218
|
* This function makes a router with a tree of sections. Each section has a hub: the free corner nearest to the center of its nodes.
|
|
3052
3219
|
* An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
|
|
3053
3220
|
* Each way from hub to hub is found one time, so all the edges on it share its points.
|
|
@@ -3062,12 +3229,14 @@ function sectionsRouter(options) {
|
|
|
3062
3229
|
const router = {
|
|
3063
3230
|
name: options.name,
|
|
3064
3231
|
route(graph) {
|
|
3065
|
-
const stretch = options.stretch ??
|
|
3232
|
+
const stretch = options.stretch ?? SECTIONS_DEFAULTS.stretch;
|
|
3233
|
+
const stretchPad = options.stretchPad ?? SECTIONS_DEFAULTS.stretchPad;
|
|
3234
|
+
const hubSlack = options.hubSlack ?? SECTIONS_DEFAULTS.hubSlack;
|
|
3066
3235
|
const byId = nodeIndex(graph);
|
|
3067
3236
|
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
3068
3237
|
const ways = new WayFinder(graph.nodes, options);
|
|
3069
3238
|
const corners = ways.corners;
|
|
3070
|
-
const root =
|
|
3239
|
+
const root = groupNodes(graph, options);
|
|
3071
3240
|
const parent = /* @__PURE__ */ new Map();
|
|
3072
3241
|
const leafOf = [];
|
|
3073
3242
|
for (const s of allSections(root)) {
|
|
@@ -3101,7 +3270,7 @@ function sectionsRouter(options) {
|
|
|
3101
3270
|
let last = from;
|
|
3102
3271
|
for (const c of list) {
|
|
3103
3272
|
const h = hub(c);
|
|
3104
|
-
if (dist(h, to) > near +
|
|
3273
|
+
if (dist(h, to) > near + hubSlack) continue;
|
|
3105
3274
|
near = Math.min(near, pointSegmentDistance(to, last, h));
|
|
3106
3275
|
last = h;
|
|
3107
3276
|
kept.push(c);
|
|
@@ -3151,7 +3320,7 @@ function sectionsRouter(options) {
|
|
|
3151
3320
|
const b = up(lv);
|
|
3152
3321
|
const common = new Set(b);
|
|
3153
3322
|
const lca = a.find((x) => common.has(x));
|
|
3154
|
-
const keep = options.hubs ??
|
|
3323
|
+
const keep = options.hubs ?? SECTIONS_DEFAULTS.hubs;
|
|
3155
3324
|
const side = (list) => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
|
|
3156
3325
|
const end = ways.out(pt);
|
|
3157
3326
|
const start = ways.out(ps);
|
|
@@ -3172,7 +3341,7 @@ function sectionsRouter(options) {
|
|
|
3172
3341
|
for (let i = 1; i < chain.length; i++) append(piece(chain[i - 1].id, hub(chain[i - 1]), chain[i].id, hub(chain[i]), used));
|
|
3173
3342
|
append(piece(chain[chain.length - 1].id, hub(chain[chain.length - 1]), at(end), end, used));
|
|
3174
3343
|
append([pt.point]);
|
|
3175
|
-
if (polylineLength(out) > stretch * dist(ps.point, pt.point) +
|
|
3344
|
+
if (polylineLength(out) > stretch * dist(ps.point, pt.point) + stretchPad * ways.margin || goesOutAndBack(out)) {
|
|
3176
3345
|
routes.push({
|
|
3177
3346
|
edge: e.id,
|
|
3178
3347
|
points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
|
|
@@ -3213,8 +3382,16 @@ function sectionsRouter(options) {
|
|
|
3213
3382
|
maxNodes: options.maxNodes
|
|
3214
3383
|
};
|
|
3215
3384
|
}
|
|
3385
|
+
/** This function gives the tree of sections of the grouping rule. It examines the tree of a strategy (`checkSections`). */
|
|
3386
|
+
function groupNodes(graph, options) {
|
|
3387
|
+
if (!options.group) return buildSections(graph.nodes, options);
|
|
3388
|
+
const root = options.group.run(graph, options.groupOptions);
|
|
3389
|
+
checkSections(root, graph.nodes.length);
|
|
3390
|
+
return root;
|
|
3391
|
+
}
|
|
3216
3392
|
/**
|
|
3217
3393
|
* This function gives one zoom level for each depth of the tree, and a last level with each node alone.
|
|
3394
|
+
* With the option `levels`, only those depths get a level with groups.
|
|
3218
3395
|
* At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
|
|
3219
3396
|
* In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
|
|
3220
3397
|
* In the last level, the points of a link are the route of its first edge.
|
|
@@ -3223,7 +3400,9 @@ function makeLevels(graph, root, routeOf, options) {
|
|
|
3223
3400
|
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
3224
3401
|
const levels = [];
|
|
3225
3402
|
const deepest = treeDepth(root);
|
|
3403
|
+
const shown = options.levels && new Set(options.levels);
|
|
3226
3404
|
for (let d = 0; d <= deepest + 1; d++) {
|
|
3405
|
+
if (shown && d <= deepest && !shown.has(d)) continue;
|
|
3227
3406
|
const items = [];
|
|
3228
3407
|
const itemOf = [];
|
|
3229
3408
|
const boxOf = (list) => {
|
|
@@ -3700,7 +3879,7 @@ function trunksRouter(options) {
|
|
|
3700
3879
|
};
|
|
3701
3880
|
}
|
|
3702
3881
|
// src/routers/index.ts
|
|
3703
|
-
/** The routers
|
|
3882
|
+
/** The built-in routers. The registry `STRATEGIES` holds the same routers as strategies, and the benchmark races the routers of the registry. */
|
|
3704
3883
|
const ROUTERS = [
|
|
3705
3884
|
straightRouter,
|
|
3706
3885
|
plugsRouter,
|
|
@@ -3823,17 +4002,34 @@ function openGaps(items, gaps, snap = DEFAULT_SNAP) {
|
|
|
3823
4002
|
* 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.
|
|
3824
4003
|
*/
|
|
3825
4004
|
function roomDemand(items, links, options = {}) {
|
|
4005
|
+
const gutter = options.gutter ?? DEFAULT_GUTTER;
|
|
4006
|
+
const snap = options.snap ?? DEFAULT_SNAP;
|
|
4007
|
+
const passes = Math.max(1, Math.floor(options.passes ?? 1));
|
|
4008
|
+
let total = demandOnce(items, links, options);
|
|
4009
|
+
for (let p = 1; p < passes; p++) {
|
|
4010
|
+
const next = demandOnce(openGaps(items, total, snap), links, options);
|
|
4011
|
+
if (next.length !== total.length || next.some((g, i) => g.axis !== total[i].axis)) break;
|
|
4012
|
+
total = total.map((g, i) => ({
|
|
4013
|
+
...g,
|
|
4014
|
+
lanes: Math.max(g.lanes, next[i].lanes),
|
|
4015
|
+
need: g.need + Math.max(0, next[i].need - next[i].width)
|
|
4016
|
+
}));
|
|
4017
|
+
}
|
|
4018
|
+
return capGrowth(items, total, gutter, options.maxGrowth ?? Infinity);
|
|
4019
|
+
}
|
|
4020
|
+
/** This function gives the demand of one pass, without the limit `maxGrowth`. */
|
|
4021
|
+
function demandOnce(items, links, options) {
|
|
3826
4022
|
const gutter = options.gutter ?? DEFAULT_GUTTER;
|
|
3827
4023
|
const spacing = options.spacing ?? DEFAULT_SPACING;
|
|
3828
4024
|
const snap = options.snap ?? DEFAULT_SNAP;
|
|
3829
4025
|
const weight = (l) => options.lanes === "count" ? Math.max(1, l.count ?? 1) : 1;
|
|
3830
4026
|
const gaps = boardGaps(items, snap);
|
|
3831
4027
|
const lanes = (options.method ?? "routes") === "lines" ? lineLanes(items, links, gaps, weight) : routeLanes(items, links, gaps, gutter, snap, weight, options.router);
|
|
3832
|
-
return
|
|
4028
|
+
return gaps.map((g, i) => ({
|
|
3833
4029
|
...g,
|
|
3834
4030
|
lanes: lanes[i],
|
|
3835
4031
|
need: Math.max(g.width, gutter + lanes[i] * spacing)
|
|
3836
|
-
}))
|
|
4032
|
+
}));
|
|
3837
4033
|
}
|
|
3838
4034
|
/** This function gives the demand of the board and the board with each gap open to its need. */
|
|
3839
4035
|
function makeRoom(items, links, options = {}) {
|
|
@@ -4018,6 +4214,141 @@ function capGrowth(items, gaps, gutter, maxGrowth) {
|
|
|
4018
4214
|
}
|
|
4019
4215
|
return out;
|
|
4020
4216
|
}
|
|
4217
|
+
// src/room/lanes.ts
|
|
4218
|
+
/** The lane orders of `gapLanes`, in the order of the benchmark. */
|
|
4219
|
+
const LANE_ORDERS = [
|
|
4220
|
+
"links",
|
|
4221
|
+
"ends",
|
|
4222
|
+
"crossings"
|
|
4223
|
+
];
|
|
4224
|
+
const EPS = 1e-6;
|
|
4225
|
+
const ROUNDS = 8;
|
|
4226
|
+
/**
|
|
4227
|
+
* 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.
|
|
4228
|
+
* A leg of a link moves across its gap only, so the link keeps its plugs and the axes of its legs.
|
|
4229
|
+
* When a move puts a link through more boxes than before, the link keeps its points.
|
|
4230
|
+
*/
|
|
4231
|
+
function gapLanes(items, links, options = {}) {
|
|
4232
|
+
const spacing = Math.max(0, options.spacing ?? 4);
|
|
4233
|
+
const order = options.order ?? "crossings";
|
|
4234
|
+
const moved = links.map((l) => l.points.map((p) => ({
|
|
4235
|
+
x: p.x,
|
|
4236
|
+
y: p.y
|
|
4237
|
+
})));
|
|
4238
|
+
for (const g of boardGaps(items, options.snap)) {
|
|
4239
|
+
if (g.width <= EPS) continue;
|
|
4240
|
+
const across = g.axis;
|
|
4241
|
+
const along = g.axis === "x" ? "y" : "x";
|
|
4242
|
+
const lo = g.at - g.width;
|
|
4243
|
+
const hi = g.at;
|
|
4244
|
+
const pieces = [];
|
|
4245
|
+
links.forEach((l, li) => {
|
|
4246
|
+
const p = l.points;
|
|
4247
|
+
for (let i = 2; i <= p.length - 2; i++) {
|
|
4248
|
+
const a = p[i - 1];
|
|
4249
|
+
const b = p[i];
|
|
4250
|
+
if (Math.abs(a[across] - b[across]) > EPS || a[across] <= lo + EPS || a[across] >= hi - EPS) continue;
|
|
4251
|
+
if (Math.abs(a[along] - b[along]) <= EPS) continue;
|
|
4252
|
+
const side = (q, r) => Math.sign(r[across] - q[across]);
|
|
4253
|
+
pieces.push({
|
|
4254
|
+
link: li,
|
|
4255
|
+
seg: i,
|
|
4256
|
+
lo: Math.min(a[along], b[along]),
|
|
4257
|
+
hi: Math.max(a[along], b[along]),
|
|
4258
|
+
turns: [[a[along], side(a, p[i - 2])], [b[along], side(b, p[i + 1])]]
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
4262
|
+
if (pieces.length === 0) continue;
|
|
4263
|
+
const list = orderPieces(pieces, order);
|
|
4264
|
+
const lane = [];
|
|
4265
|
+
let n = 0;
|
|
4266
|
+
list.forEach((P, i) => {
|
|
4267
|
+
let k = 0;
|
|
4268
|
+
for (let j = 0; j < i; j++) if (overlaps(list[j], P)) k = Math.max(k, lane[j] + 1);
|
|
4269
|
+
lane.push(k);
|
|
4270
|
+
n = Math.max(n, k + 1);
|
|
4271
|
+
});
|
|
4272
|
+
const step = Math.min(spacing, g.width / (n + 1));
|
|
4273
|
+
const mid = (lo + hi) / 2;
|
|
4274
|
+
list.forEach((P, i) => {
|
|
4275
|
+
const v = mid + (lane[i] - (n - 1) / 2) * step;
|
|
4276
|
+
moved[P.link][P.seg - 1][across] = v;
|
|
4277
|
+
moved[P.link][P.seg][across] = v;
|
|
4278
|
+
});
|
|
4279
|
+
}
|
|
4280
|
+
const boxes = new Grid(Math.max(20, meanSide(items)));
|
|
4281
|
+
for (const i of items) boxes.add(i.box, i.box);
|
|
4282
|
+
return links.map((l, i) => hits(boxes, moved[i]) > hits(boxes, l.points) ? l : {
|
|
4283
|
+
...l,
|
|
4284
|
+
points: moved[i]
|
|
4285
|
+
});
|
|
4286
|
+
}
|
|
4287
|
+
const overlaps = (P, Q) => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS;
|
|
4288
|
+
/**
|
|
4289
|
+
* This function counts the crossings when the piece `P` is on the near side of the piece `Q`.
|
|
4290
|
+
* 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.
|
|
4291
|
+
*/
|
|
4292
|
+
function cost(P, Q) {
|
|
4293
|
+
let c = 0;
|
|
4294
|
+
for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS && y < Q.hi - EPS) c++;
|
|
4295
|
+
for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS && y < P.hi - EPS) c++;
|
|
4296
|
+
return c;
|
|
4297
|
+
}
|
|
4298
|
+
/** This function gives the pieces of one gap in lane order, from the near side to the far side. */
|
|
4299
|
+
function orderPieces(pieces, order) {
|
|
4300
|
+
if (order === "links") return [...pieces];
|
|
4301
|
+
const sum = (P) => P.turns.reduce((s, [, side]) => s + side, 0);
|
|
4302
|
+
const list = [...pieces].sort((P, Q) => sum(P) - sum(Q) || P.link - Q.link || P.seg - Q.seg);
|
|
4303
|
+
if (order === "ends") return list;
|
|
4304
|
+
const n = list.length;
|
|
4305
|
+
const index = new Map(list.map((P, i) => [P, i]));
|
|
4306
|
+
const c = list.map((P) => list.map((Q) => P !== Q && overlaps(P, Q) ? cost(P, Q) : 0));
|
|
4307
|
+
const at = (P, Q) => c[index.get(P)][index.get(Q)];
|
|
4308
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
4309
|
+
let better = false;
|
|
4310
|
+
for (let i = 0; i < n; i++) {
|
|
4311
|
+
const x = list[i];
|
|
4312
|
+
let best = 0;
|
|
4313
|
+
let to = i;
|
|
4314
|
+
let d = 0;
|
|
4315
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
4316
|
+
d += at(x, list[k]) - at(list[k], x);
|
|
4317
|
+
if (d < best) [best, to] = [d, k];
|
|
4318
|
+
}
|
|
4319
|
+
d = 0;
|
|
4320
|
+
for (let k = i + 1; k < n; k++) {
|
|
4321
|
+
d += at(list[k], x) - at(x, list[k]);
|
|
4322
|
+
if (d < best) [best, to] = [d, k];
|
|
4323
|
+
}
|
|
4324
|
+
if (to !== i) {
|
|
4325
|
+
list.splice(i, 1);
|
|
4326
|
+
list.splice(to, 0, x);
|
|
4327
|
+
better = true;
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
if (!better) break;
|
|
4331
|
+
}
|
|
4332
|
+
return list;
|
|
4333
|
+
}
|
|
4334
|
+
/** This function gives the mean of the longer side of the boxes. */
|
|
4335
|
+
function meanSide(items) {
|
|
4336
|
+
let s = 0;
|
|
4337
|
+
for (const i of items) s += Math.max(i.box.w, i.box.h);
|
|
4338
|
+
return items.length ? s / items.length : 1;
|
|
4339
|
+
}
|
|
4340
|
+
/** This function counts the legs of a polyline that go through a box, for each box. */
|
|
4341
|
+
function hits(boxes, p) {
|
|
4342
|
+
let n = 0;
|
|
4343
|
+
for (let i = 1; i < p.length; i++) {
|
|
4344
|
+
const a = p[i - 1];
|
|
4345
|
+
const b = p[i];
|
|
4346
|
+
boxes.querySegment(a, b, 0, (box) => {
|
|
4347
|
+
if (segmentHitsBox(a, b, box)) n++;
|
|
4348
|
+
});
|
|
4349
|
+
}
|
|
4350
|
+
return n;
|
|
4351
|
+
}
|
|
4021
4352
|
// src/bench/quality.ts
|
|
4022
4353
|
/** The clearance stops at this distance. */
|
|
4023
4354
|
const CLEARANCE_CAP = 40;
|
|
@@ -4203,7 +4534,7 @@ function measureStability(before, after) {
|
|
|
4203
4534
|
};
|
|
4204
4535
|
}
|
|
4205
4536
|
// src/bench/run.ts
|
|
4206
|
-
const now$
|
|
4537
|
+
const now$3 = () => globalThis.performance?.now() ?? Date.now();
|
|
4207
4538
|
/** This function routes each fixture with each router, checks the hard rules and measures the quality. */
|
|
4208
4539
|
function runBenchmark(routers, fixtures, options = {}) {
|
|
4209
4540
|
const rows = [];
|
|
@@ -4242,9 +4573,9 @@ function benchOne(router, fixture, graph, repeat = 3) {
|
|
|
4242
4573
|
let first;
|
|
4243
4574
|
let deterministic = true;
|
|
4244
4575
|
for (let i = 0; i < Math.max(2, repeat); i++) {
|
|
4245
|
-
const t0 = now$
|
|
4576
|
+
const t0 = now$3();
|
|
4246
4577
|
const r = router.route(graph);
|
|
4247
|
-
times.push(now$
|
|
4578
|
+
times.push(now$3() - t0);
|
|
4248
4579
|
if (!first) first = r;
|
|
4249
4580
|
else if (i === 1 && !sameData(r, first)) deterministic = false;
|
|
4250
4581
|
}
|
|
@@ -4306,14 +4637,233 @@ function formatFaults(rows) {
|
|
|
4306
4637
|
})
|
|
4307
4638
|
].join("\n");
|
|
4308
4639
|
}
|
|
4640
|
+
// src/strategies/model.ts
|
|
4641
|
+
/** The steps that have strategies, in the order of the pipeline. */
|
|
4642
|
+
const STEPS = [
|
|
4643
|
+
"group",
|
|
4644
|
+
"router",
|
|
4645
|
+
"curve",
|
|
4646
|
+
"room"
|
|
4647
|
+
];
|
|
4648
|
+
/**
|
|
4649
|
+
* This function makes a strategy from its definition. The strategy fills the missing options from `defaults`.
|
|
4650
|
+
* When the caller gives no option, the definition gets the `defaults` object itself.
|
|
4651
|
+
*/
|
|
4652
|
+
function defineStrategy(definition) {
|
|
4653
|
+
const { step, name, describe, defaults, maxNodes } = definition;
|
|
4654
|
+
if (name.length === 0) throw new Error(`A strategy of the step "${step}" has no name.`);
|
|
4655
|
+
const strategy = {
|
|
4656
|
+
step,
|
|
4657
|
+
name,
|
|
4658
|
+
describe,
|
|
4659
|
+
defaults: Object.freeze({ ...defaults }),
|
|
4660
|
+
...maxNodes === void 0 ? {} : { maxNodes },
|
|
4661
|
+
run(input, options) {
|
|
4662
|
+
const all = options === void 0 || Object.keys(options).length === 0 ? strategy.defaults : {
|
|
4663
|
+
...strategy.defaults,
|
|
4664
|
+
...options
|
|
4665
|
+
};
|
|
4666
|
+
return definition.run(input, all);
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
return Object.freeze(strategy);
|
|
4670
|
+
}
|
|
4671
|
+
/** This function makes a registry from a list of strategies. It keeps their order. Two strategies with one step and one name are an error. */
|
|
4672
|
+
function createRegistry(strategies) {
|
|
4673
|
+
const out = {
|
|
4674
|
+
group: [],
|
|
4675
|
+
router: [],
|
|
4676
|
+
curve: [],
|
|
4677
|
+
room: []
|
|
4678
|
+
};
|
|
4679
|
+
for (const s of strategies) {
|
|
4680
|
+
const list = out[s.step];
|
|
4681
|
+
if (!list) throw new Error(`There is no step "${String(s.step)}". The steps are ${STEPS.join(", ")}.`);
|
|
4682
|
+
if (list.some((t) => t.name === s.name)) throw new Error(`The step "${s.step}" has two strategies with the name "${s.name}".`);
|
|
4683
|
+
list.push(s);
|
|
4684
|
+
}
|
|
4685
|
+
return Object.freeze({
|
|
4686
|
+
group: Object.freeze(out.group),
|
|
4687
|
+
router: Object.freeze(out.router),
|
|
4688
|
+
curve: Object.freeze(out.curve),
|
|
4689
|
+
room: Object.freeze(out.room)
|
|
4690
|
+
});
|
|
4691
|
+
}
|
|
4692
|
+
/** This function gives a new registry with the strategies of `registry` and then `strategies`. It does not change `registry`. */
|
|
4693
|
+
function register(registry, ...strategies) {
|
|
4694
|
+
return createRegistry([
|
|
4695
|
+
...registry.group ?? [],
|
|
4696
|
+
...registry.router,
|
|
4697
|
+
...registry.curve,
|
|
4698
|
+
...registry.room,
|
|
4699
|
+
...strategies
|
|
4700
|
+
]);
|
|
4701
|
+
}
|
|
4702
|
+
/** This function gives the strategies of one step of a registry, in order. */
|
|
4703
|
+
function listStrategies(registry, step) {
|
|
4704
|
+
return registry[step] ?? [];
|
|
4705
|
+
}
|
|
4706
|
+
/** This function gives the strategy of one step with a name, or `undefined` when the registry has no such strategy. */
|
|
4707
|
+
function findStrategy(registry, step, name) {
|
|
4708
|
+
return listStrategies(registry, step).find((s) => s.name === name);
|
|
4709
|
+
}
|
|
4710
|
+
/** This function gives the strategy of one step with a name. When the registry has no such strategy, the function throws an error. */
|
|
4711
|
+
function getStrategy(registry, step, name) {
|
|
4712
|
+
const s = findStrategy(registry, step, name);
|
|
4713
|
+
if (!s) throw new Error(`There is no ${step} "${name}". The registry has ${listStrategies(registry, step).map((t) => t.name).join(", ")}.`);
|
|
4714
|
+
return s;
|
|
4715
|
+
}
|
|
4716
|
+
/** This function gives a `Router` that runs a router strategy with the options. The router keeps the name and the `maxNodes` of the strategy. */
|
|
4717
|
+
function asRouter(strategy, options) {
|
|
4718
|
+
return {
|
|
4719
|
+
name: strategy.name,
|
|
4720
|
+
...strategy.maxNodes === void 0 ? {} : { maxNodes: strategy.maxNodes },
|
|
4721
|
+
route: (graph) => strategy.run(graph, options)
|
|
4722
|
+
};
|
|
4723
|
+
}
|
|
4724
|
+
/** This function gives the router strategies of a registry as `Router` objects, in order. The benchmark races them. */
|
|
4725
|
+
function routersOf(registry) {
|
|
4726
|
+
return listStrategies(registry, "router").map((s) => asRouter(s));
|
|
4727
|
+
}
|
|
4728
|
+
// src/strategies/builtins.ts
|
|
4729
|
+
/** This function makes a router strategy from a router factory and a preset of its options. Each call makes the router of its options. */
|
|
4730
|
+
function routerStrategy(name, describe, maxNodes, defaults, make) {
|
|
4731
|
+
return defineStrategy({
|
|
4732
|
+
step: "router",
|
|
4733
|
+
name,
|
|
4734
|
+
describe,
|
|
4735
|
+
defaults,
|
|
4736
|
+
maxNodes,
|
|
4737
|
+
run: (graph, options) => make({
|
|
4738
|
+
...options,
|
|
4739
|
+
name
|
|
4740
|
+
}).route(graph)
|
|
4741
|
+
});
|
|
4742
|
+
}
|
|
4743
|
+
/** This function makes a router strategy for a router without options. */
|
|
4744
|
+
function fixedRouter(router, describe) {
|
|
4745
|
+
return defineStrategy({
|
|
4746
|
+
step: "router",
|
|
4747
|
+
name: router.name,
|
|
4748
|
+
describe,
|
|
4749
|
+
defaults: {},
|
|
4750
|
+
run: (graph) => router.route(graph)
|
|
4751
|
+
});
|
|
4752
|
+
}
|
|
4753
|
+
/** The built-in router strategies, in the order of the benchmark. They route as the routers of `ROUTERS`. */
|
|
4754
|
+
const routerStrategies = Object.freeze({
|
|
4755
|
+
straight: fixedRouter(straightRouter, "The control. Each edge is one straight line between its plugs."),
|
|
4756
|
+
plugs: fixedRouter(plugsRouter, "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."),
|
|
4757
|
+
corners: routerStrategy("corners", "Each edge goes around the nodes on a shortest path (A*) through a sparse visibility graph of the box corners.", 1e4, {}, cornersRouter),
|
|
4758
|
+
"corners-smooth": routerStrategy("corners-smooth", "The same paths as `corners`. A route in clear view follows the inflection rule, and the other routes get round corners.", 1e4, { smooth: true }, cornersRouter),
|
|
4759
|
+
trunks: routerStrategy("trunks", "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.", 1e4, { rule: byCollection }, trunksRouter),
|
|
4760
|
+
shared: routerStrategy("shared", "The edges of one collection share their ways through the corner graph. A link that the group uses already costs half.", 1e4, { rule: byCollection }, sharedRouter),
|
|
4761
|
+
"shared-all": routerStrategy("shared-all", "The same as `shared`, but all edges share their ways.", 1e4, {}, sharedRouter),
|
|
4762
|
+
sections: routerStrategy("sections", "A tree of sections, with a hub in each section. A long edge goes through the hubs, and a short edge goes directly.", 1e5, {}, sectionsRouter),
|
|
4763
|
+
"sections-tree": routerStrategy("sections-tree", "The same as `sections`, but without the limit of the stretch.", 1e5, { stretch: Infinity }, sectionsRouter)
|
|
4764
|
+
});
|
|
4765
|
+
/** The context of a curve input, without the keys that have no value. */
|
|
4766
|
+
const contextOf = (input) => ({
|
|
4767
|
+
...input.startNormal ? { startNormal: input.startNormal } : {},
|
|
4768
|
+
...input.endNormal ? { endNormal: input.endNormal } : {},
|
|
4769
|
+
...input.space ? { space: input.space } : {}
|
|
4770
|
+
});
|
|
4771
|
+
/** The built-in curve strategies, in the order of `CURVE_TYPES`. Each one gives the same segments as `curveOf` with its type. */
|
|
4772
|
+
const curveStrategies = Object.freeze({
|
|
4773
|
+
straight: defineStrategy({
|
|
4774
|
+
step: "curve",
|
|
4775
|
+
name: "straight",
|
|
4776
|
+
describe: "The polyline as it is.",
|
|
4777
|
+
defaults: {},
|
|
4778
|
+
run: (input) => straightCurve(input.points)
|
|
4779
|
+
}),
|
|
4780
|
+
rounded: defineStrategy({
|
|
4781
|
+
step: "curve",
|
|
4782
|
+
name: "rounded",
|
|
4783
|
+
describe: "A quadratic Bezier curve at each corner, as `roundCorners` draws it. The first and the last legs stay.",
|
|
4784
|
+
defaults: { radius: 12 },
|
|
4785
|
+
run: (input, o) => roundedCurve(input.points, {
|
|
4786
|
+
...contextOf(input),
|
|
4787
|
+
radius: o.radius
|
|
4788
|
+
})
|
|
4789
|
+
}),
|
|
4790
|
+
spline: defineStrategy({
|
|
4791
|
+
step: "curve",
|
|
4792
|
+
name: "spline",
|
|
4793
|
+
describe: "A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece.",
|
|
4794
|
+
defaults: { tension: 0 },
|
|
4795
|
+
run: (input, o) => splineCurve(input.points, {
|
|
4796
|
+
...contextOf(input),
|
|
4797
|
+
tension: o.tension
|
|
4798
|
+
})
|
|
4799
|
+
}),
|
|
4800
|
+
arc: defineStrategy({
|
|
4801
|
+
step: "curve",
|
|
4802
|
+
name: "arc",
|
|
4803
|
+
describe: "A circular arc at each corner, tangent to the two legs. The first and the last legs stay.",
|
|
4804
|
+
defaults: { radius: 12 },
|
|
4805
|
+
run: (input, o) => arcCurve(input.points, {
|
|
4806
|
+
...contextOf(input),
|
|
4807
|
+
radius: o.radius
|
|
4808
|
+
})
|
|
4809
|
+
})
|
|
4810
|
+
});
|
|
4811
|
+
const roomStrategy = (method, describe) => defineStrategy({
|
|
4812
|
+
step: "room",
|
|
4813
|
+
name: method,
|
|
4814
|
+
describe,
|
|
4815
|
+
defaults: {},
|
|
4816
|
+
run: (input, o) => roomDemand(input.items, input.links, {
|
|
4817
|
+
...o,
|
|
4818
|
+
method
|
|
4819
|
+
})
|
|
4820
|
+
});
|
|
4821
|
+
/** The built-in room strategies: the methods of `roomDemand`. */
|
|
4822
|
+
const roomStrategies = Object.freeze({
|
|
4823
|
+
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."),
|
|
4824
|
+
lines: roomStrategy("lines", "The lanes of a gap are the straight lines between the centers of the boxes that cross the gap.")
|
|
4825
|
+
});
|
|
4826
|
+
/** The built-in grouping rules: the strategies of the step `group`. The router `sections` makes its hubs and its zoom levels from their tree. */
|
|
4827
|
+
const groupStrategies = Object.freeze({
|
|
4828
|
+
quadtree: defineStrategy({
|
|
4829
|
+
step: "group",
|
|
4830
|
+
name: "quadtree",
|
|
4831
|
+
describe: "The cells of a quadtree of the plane. A cell with more than `cap` nodes splits in four. It is the default rule of the router `sections`.",
|
|
4832
|
+
defaults: { ...SECTION_DEFAULTS },
|
|
4833
|
+
run: (graph, o) => quadtreeGroups(graph, o)
|
|
4834
|
+
}),
|
|
4835
|
+
keys: defineStrategy({
|
|
4836
|
+
step: "group",
|
|
4837
|
+
name: "keys",
|
|
4838
|
+
describe: "One group for each key of the nodes. The default key is the string `data.group` of the node. A node without a key is a group alone.",
|
|
4839
|
+
defaults: { key: byDataGroup },
|
|
4840
|
+
run: (graph, o) => keyGroups(graph, o)
|
|
4841
|
+
})
|
|
4842
|
+
});
|
|
4843
|
+
/** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
|
|
4844
|
+
const BUILT_IN_STRATEGIES = Object.freeze([
|
|
4845
|
+
...Object.values(groupStrategies),
|
|
4846
|
+
...Object.values(routerStrategies),
|
|
4847
|
+
...Object.values(curveStrategies),
|
|
4848
|
+
...Object.values(roomStrategies)
|
|
4849
|
+
]);
|
|
4850
|
+
/** The registry of the built-in strategies. The benchmark and the site take their lists from it. */
|
|
4851
|
+
const STRATEGIES = createRegistry(BUILT_IN_STRATEGIES);
|
|
4309
4852
|
// src/bench/curves.ts
|
|
4310
|
-
const now$
|
|
4853
|
+
const now$2 = () => globalThis.performance?.now() ?? Date.now();
|
|
4311
4854
|
/**
|
|
4312
4855
|
* This function routes a graph with a router and makes each curve type of each route.
|
|
4313
4856
|
* It counts the curves that go through a box, with and without the free space, and it compares their length to the polylines.
|
|
4314
4857
|
* The type `spline` leaves each plug along the normal of the nearest side of its node.
|
|
4315
4858
|
*/
|
|
4316
4859
|
function benchCurves(router, fixture, graph, options = {}) {
|
|
4860
|
+
return benchCurveStrategies(router, fixture, graph, CURVE_TYPES.map((t) => curveStrategies[t]), options);
|
|
4861
|
+
}
|
|
4862
|
+
/**
|
|
4863
|
+
* This function does the work of `benchCurves` with a list of curve strategies, in place of the curve types.
|
|
4864
|
+
* The option `radius` goes to each strategy. A strategy without such an option does not read it.
|
|
4865
|
+
*/
|
|
4866
|
+
function benchCurveStrategies(router, fixture, graph, curves, options = {}) {
|
|
4317
4867
|
const tolerance = options.tolerance ?? .25;
|
|
4318
4868
|
const result = router.route(graph);
|
|
4319
4869
|
const byId = nodeIndex(graph);
|
|
@@ -4337,27 +4887,27 @@ function benchCurves(router, fixture, graph, options = {}) {
|
|
|
4337
4887
|
});
|
|
4338
4888
|
const base = measureLinks(boxes, routes);
|
|
4339
4889
|
const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
|
|
4340
|
-
|
|
4890
|
+
const radius = { radius: options.radius ?? 12 };
|
|
4891
|
+
return curves.map((curve) => {
|
|
4341
4892
|
const make = (withSpace) => routes.map((r) => ({
|
|
4342
4893
|
source: r.source,
|
|
4343
4894
|
target: r.target,
|
|
4344
|
-
points: sampleCurve(
|
|
4345
|
-
|
|
4346
|
-
radius: options.radius ?? 12,
|
|
4895
|
+
points: sampleCurve(curve.run({
|
|
4896
|
+
points: r.points,
|
|
4347
4897
|
startNormal: r.startNormal,
|
|
4348
4898
|
endNormal: r.endNormal,
|
|
4349
4899
|
...withSpace ? { space } : {}
|
|
4350
|
-
}), tolerance)
|
|
4900
|
+
}, radius), tolerance)
|
|
4351
4901
|
}));
|
|
4352
4902
|
const free = measureLinks(boxes, make(false));
|
|
4353
|
-
const t0 = now$
|
|
4903
|
+
const t0 = now$2();
|
|
4354
4904
|
const clamped = make(true);
|
|
4355
|
-
const ms = now$
|
|
4905
|
+
const ms = now$2() - t0;
|
|
4356
4906
|
const length = clamped.reduce((sum, l) => sum + polylineLength(l.points), 0);
|
|
4357
4907
|
return {
|
|
4358
4908
|
router: router.name,
|
|
4359
4909
|
fixture,
|
|
4360
|
-
type,
|
|
4910
|
+
type: curve.name,
|
|
4361
4911
|
links: routes.length,
|
|
4362
4912
|
through: base.through,
|
|
4363
4913
|
free: free.through,
|
|
@@ -4376,7 +4926,7 @@ function formatCurves(rows) {
|
|
|
4376
4926
|
].join("\n");
|
|
4377
4927
|
}
|
|
4378
4928
|
// src/bench/room.ts
|
|
4379
|
-
const now = () => globalThis.performance?.now() ?? Date.now();
|
|
4929
|
+
const now$1 = () => globalThis.performance?.now() ?? Date.now();
|
|
4380
4930
|
/** This function gives the area of the bounding box of the nodes. */
|
|
4381
4931
|
function areaOf(nodes) {
|
|
4382
4932
|
let x0 = Infinity;
|
|
@@ -4393,11 +4943,13 @@ function areaOf(nodes) {
|
|
|
4393
4943
|
}
|
|
4394
4944
|
/**
|
|
4395
4945
|
* 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.
|
|
4396
|
-
*
|
|
4946
|
+
* With a lane order, each link of the far view gets its own lane in each gap (`gapLanes` at the lane width `spacing`).
|
|
4947
|
+
* Without it, the links of a gap share the lines of the level links.
|
|
4948
|
+
* The function also measures the lane routes of the router `sections` at the lane width `spacing`.
|
|
4397
4949
|
*/
|
|
4398
|
-
function measureBoard(graph, spacing) {
|
|
4950
|
+
function measureBoard(graph, spacing, order) {
|
|
4399
4951
|
const links = roomLinks(graph);
|
|
4400
|
-
const
|
|
4952
|
+
const level = levelLinks(graph.nodes.map((n) => ({
|
|
4401
4953
|
id: n.id,
|
|
4402
4954
|
box: n.box,
|
|
4403
4955
|
nodes: [n.id]
|
|
@@ -4406,6 +4958,10 @@ function measureBoard(graph, spacing) {
|
|
|
4406
4958
|
target: l.target,
|
|
4407
4959
|
count: l.count ?? 1
|
|
4408
4960
|
})));
|
|
4961
|
+
const drawn = order ? gapLanes(graph.nodes, level, {
|
|
4962
|
+
spacing,
|
|
4963
|
+
order
|
|
4964
|
+
}) : level;
|
|
4409
4965
|
const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
|
|
4410
4966
|
const edgeOf = /* @__PURE__ */ new Map();
|
|
4411
4967
|
for (const e of graph.edges) {
|
|
@@ -4427,10 +4983,10 @@ function measureBoard(graph, spacing) {
|
|
|
4427
4983
|
};
|
|
4428
4984
|
}
|
|
4429
4985
|
/**
|
|
4430
|
-
* This function measures a packed board, the board with each gap open to the gutter only, and the board that
|
|
4431
|
-
* The options
|
|
4986
|
+
* This function measures a packed board, the board with each gap open to the gutter only, and the board that each room strategy opens.
|
|
4987
|
+
* The options are the same for each strategy, except `method`. The default strategies are the methods `routes` and `lines` of `roomDemand`.
|
|
4432
4988
|
*/
|
|
4433
|
-
function benchRoom(fixture, graph, options = {}) {
|
|
4989
|
+
function benchRoom(fixture, graph, options = {}, methods = [roomStrategies.routes, roomStrategies.lines]) {
|
|
4434
4990
|
const spacing = options.spacing ?? 4;
|
|
4435
4991
|
const area0 = areaOf(graph.nodes);
|
|
4436
4992
|
const links = roomLinks(graph);
|
|
@@ -4445,28 +5001,129 @@ function benchRoom(fixture, graph, options = {}) {
|
|
|
4445
5001
|
}, spacing)
|
|
4446
5002
|
});
|
|
4447
5003
|
const rows = [row("packed", graph.nodes, 0)];
|
|
4448
|
-
const
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
5004
|
+
const t0 = now$1();
|
|
5005
|
+
const gutter = makeRoom(graph.nodes, links, {
|
|
5006
|
+
...options,
|
|
5007
|
+
spacing: 0,
|
|
5008
|
+
method: "routes"
|
|
5009
|
+
});
|
|
5010
|
+
rows.push(row("gutter", gutter.items, now$1() - t0));
|
|
5011
|
+
const { method: _method, ...preset } = options;
|
|
5012
|
+
for (const s of methods) {
|
|
5013
|
+
const t1 = now$1();
|
|
5014
|
+
const gaps = s.run({
|
|
5015
|
+
items: graph.nodes,
|
|
5016
|
+
links
|
|
5017
|
+
}, preset);
|
|
5018
|
+
const nodes = openGaps(graph.nodes, gaps, options.snap);
|
|
5019
|
+
rows.push(row(s.name, nodes, now$1() - t1));
|
|
5020
|
+
}
|
|
5021
|
+
return rows;
|
|
5022
|
+
}
|
|
5023
|
+
/**
|
|
5024
|
+
* This function measures the board that `makeRoom` opens with each setting, with the method `routes`.
|
|
5025
|
+
* 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`.
|
|
5026
|
+
*/
|
|
5027
|
+
function sweepRoom(fixture, graph, settings, baseline = {
|
|
5028
|
+
gutter: 8,
|
|
5029
|
+
spacing: 4
|
|
5030
|
+
}) {
|
|
5031
|
+
const area0 = areaOf(graph.nodes);
|
|
5032
|
+
const links = roomLinks(graph);
|
|
5033
|
+
const row = (board, nodes, ms, spacing, lanes) => ({
|
|
5034
|
+
fixture,
|
|
5035
|
+
board,
|
|
5036
|
+
area: area0 > 0 ? areaOf(nodes) / area0 : 1,
|
|
5037
|
+
ms,
|
|
5038
|
+
...measureBoard({
|
|
5039
|
+
nodes,
|
|
5040
|
+
edges: graph.edges
|
|
5041
|
+
}, spacing, lanes)
|
|
5042
|
+
});
|
|
5043
|
+
const rows = [row("gutter", makeRoom(graph.nodes, links, {
|
|
5044
|
+
...baseline,
|
|
5045
|
+
spacing: 0,
|
|
5046
|
+
method: "routes",
|
|
5047
|
+
maxGrowth: Infinity,
|
|
5048
|
+
passes: 1
|
|
5049
|
+
}).items, 0, baseline.spacing ?? 4)];
|
|
5050
|
+
for (const s of settings) {
|
|
5051
|
+
const t0 = now$1();
|
|
5052
|
+
const open = makeRoom(graph.nodes, links, {
|
|
5053
|
+
...s.options,
|
|
4456
5054
|
method: "routes"
|
|
4457
|
-
}
|
|
4458
|
-
|
|
4459
|
-
...options,
|
|
4460
|
-
method: "lines"
|
|
4461
|
-
}]
|
|
4462
|
-
];
|
|
4463
|
-
for (const [board, o] of states) {
|
|
4464
|
-
const t0 = now();
|
|
4465
|
-
const room = makeRoom(graph.nodes, links, o);
|
|
4466
|
-
rows.push(row(board, room.items, now() - t0));
|
|
5055
|
+
});
|
|
5056
|
+
rows.push(row(s.name, open.items, now$1() - t0, s.options.spacing ?? 4, s.lanes));
|
|
4467
5057
|
}
|
|
4468
5058
|
return rows;
|
|
4469
5059
|
}
|
|
5060
|
+
/** The values of `maxGrowth` of the sweep of ROOM-A. */
|
|
5061
|
+
const GROWTH_SWEEP = [
|
|
5062
|
+
0,
|
|
5063
|
+
.06,
|
|
5064
|
+
.09,
|
|
5065
|
+
.12,
|
|
5066
|
+
.15,
|
|
5067
|
+
.18,
|
|
5068
|
+
.21,
|
|
5069
|
+
.25,
|
|
5070
|
+
.3,
|
|
5071
|
+
.35,
|
|
5072
|
+
.5,
|
|
5073
|
+
Infinity
|
|
5074
|
+
];
|
|
5075
|
+
/** This function gives the settings of ROOM-A: the area and the faults for each `maxGrowth`, at gutter 8 and spacing 4. */
|
|
5076
|
+
function growthSettings(values = GROWTH_SWEEP) {
|
|
5077
|
+
return values.map((m) => ({
|
|
5078
|
+
name: `maxGrowth ${m}`,
|
|
5079
|
+
options: {
|
|
5080
|
+
gutter: 8,
|
|
5081
|
+
spacing: 4,
|
|
5082
|
+
maxGrowth: m
|
|
5083
|
+
}
|
|
5084
|
+
}));
|
|
5085
|
+
}
|
|
5086
|
+
/**
|
|
5087
|
+
* This function gives the settings of ROOM-C: each lane order of the far view at each spacing, at gutter 8.
|
|
5088
|
+
* The order `shared` is the far view without lanes, as before ROOM-C.
|
|
5089
|
+
*/
|
|
5090
|
+
function laneSettings(spacings = [
|
|
5091
|
+
2,
|
|
5092
|
+
4,
|
|
5093
|
+
6,
|
|
5094
|
+
8
|
|
5095
|
+
], orders = LANE_ORDERS) {
|
|
5096
|
+
return spacings.flatMap((spacing) => [{
|
|
5097
|
+
name: `shared spacing ${spacing}`,
|
|
5098
|
+
options: {
|
|
5099
|
+
gutter: 8,
|
|
5100
|
+
spacing
|
|
5101
|
+
}
|
|
5102
|
+
}, ...orders.map((lanes) => ({
|
|
5103
|
+
name: `${lanes} spacing ${spacing}`,
|
|
5104
|
+
options: {
|
|
5105
|
+
gutter: 8,
|
|
5106
|
+
spacing
|
|
5107
|
+
},
|
|
5108
|
+
lanes
|
|
5109
|
+
}))]);
|
|
5110
|
+
}
|
|
5111
|
+
/** This function gives the settings of ROOM-D: each gutter with each number of passes, at spacing 4. */
|
|
5112
|
+
function passSettings(gutters = [
|
|
5113
|
+
4,
|
|
5114
|
+
8,
|
|
5115
|
+
12,
|
|
5116
|
+
16
|
|
5117
|
+
], passes = [1, 2]) {
|
|
5118
|
+
return gutters.flatMap((gutter) => passes.map((p) => ({
|
|
5119
|
+
name: `gutter ${gutter} passes ${p}`,
|
|
5120
|
+
options: {
|
|
5121
|
+
gutter,
|
|
5122
|
+
spacing: 4,
|
|
5123
|
+
passes: p
|
|
5124
|
+
}
|
|
5125
|
+
})));
|
|
5126
|
+
}
|
|
4470
5127
|
/** This function writes the rows of `benchRoom` as a Markdown table. */
|
|
4471
5128
|
function formatRoom(rows) {
|
|
4472
5129
|
return [
|
|
@@ -4478,6 +5135,118 @@ function formatRoom(rows) {
|
|
|
4478
5135
|
})
|
|
4479
5136
|
].join("\n");
|
|
4480
5137
|
}
|
|
4481
|
-
|
|
5138
|
+
// src/bench/sections.ts
|
|
5139
|
+
const now = () => globalThis.performance?.now() ?? Date.now();
|
|
5140
|
+
/** This function gives the number of items of the first level with more than one item, or 0 for a result without levels. */
|
|
5141
|
+
function farItems(result) {
|
|
5142
|
+
return result.levels?.find((l) => l.items.length > 1)?.items.length ?? 0;
|
|
5143
|
+
}
|
|
5144
|
+
/**
|
|
5145
|
+
* This function measures the router `sections` on one board with each setting. The first row is the fair baseline: the defaults, with no option.
|
|
5146
|
+
* The direct routes come from the hub limit 0, so `hubRoutes` counts the routes that use the hubs.
|
|
5147
|
+
*/
|
|
5148
|
+
function sweepSections(fixture, graph, settings) {
|
|
5149
|
+
return [{
|
|
5150
|
+
name: "defaults",
|
|
5151
|
+
options: {}
|
|
5152
|
+
}, ...settings].map((s) => {
|
|
5153
|
+
const direct = sectionsRouter({
|
|
5154
|
+
...s.options,
|
|
5155
|
+
name: "direct",
|
|
5156
|
+
stretch: 0,
|
|
5157
|
+
stretchPad: 0
|
|
5158
|
+
}).route(graph);
|
|
5159
|
+
const t0 = now();
|
|
5160
|
+
const result = sectionsRouter({
|
|
5161
|
+
...s.options,
|
|
5162
|
+
name: s.name
|
|
5163
|
+
}).route(graph);
|
|
5164
|
+
const ms = now() - t0;
|
|
5165
|
+
const levelFaults = measureLevelFaults(result);
|
|
5166
|
+
return {
|
|
5167
|
+
fixture,
|
|
5168
|
+
setting: s.name,
|
|
5169
|
+
faults: measureRouteFaults(graph, result),
|
|
5170
|
+
levelFaults: sumFaults(levelFaults),
|
|
5171
|
+
overlaps: levelFaults.reduce((n, f) => n + f.overlaps, 0),
|
|
5172
|
+
hubRoutes: result.routes.filter((r, i) => !sameData(r.points, direct.routes[i]?.points)).length,
|
|
5173
|
+
runs: result.runs?.length ?? 0,
|
|
5174
|
+
levels: result.levels?.length ?? 0,
|
|
5175
|
+
farItems: farItems(result),
|
|
5176
|
+
ms
|
|
5177
|
+
};
|
|
5178
|
+
});
|
|
5179
|
+
}
|
|
5180
|
+
/** The values of the hub limit (`stretch`) of the sweep of CUSTOM. The default is 2. */
|
|
5181
|
+
const STRETCH_SWEEP = [
|
|
5182
|
+
1.25,
|
|
5183
|
+
1.5,
|
|
5184
|
+
2,
|
|
5185
|
+
3,
|
|
5186
|
+
Infinity
|
|
5187
|
+
];
|
|
5188
|
+
/** This function gives the settings of the sweep of the hub limit: one setting for each value of `stretch`. */
|
|
5189
|
+
function stretchSettings(values = STRETCH_SWEEP) {
|
|
5190
|
+
return values.map((stretch) => ({
|
|
5191
|
+
name: `stretch ${stretch}`,
|
|
5192
|
+
options: { stretch }
|
|
5193
|
+
}));
|
|
5194
|
+
}
|
|
5195
|
+
/** The values of `hubSlack` of the sweep of CUSTOM. The default is 80. */
|
|
5196
|
+
const SLACK_SWEEP = [
|
|
5197
|
+
0,
|
|
5198
|
+
40,
|
|
5199
|
+
80,
|
|
5200
|
+
160,
|
|
5201
|
+
Infinity
|
|
5202
|
+
];
|
|
5203
|
+
/** This function gives the settings of the sweep of the slack of the hubs: one setting for each value of `hubSlack`. */
|
|
5204
|
+
function slackSettings(values = SLACK_SWEEP) {
|
|
5205
|
+
return values.map((hubSlack) => ({
|
|
5206
|
+
name: `hubSlack ${hubSlack}`,
|
|
5207
|
+
options: { hubSlack }
|
|
5208
|
+
}));
|
|
5209
|
+
}
|
|
5210
|
+
/** The values of the section limit (`cap`) of the sweep of CUSTOM. The default is 8. */
|
|
5211
|
+
const CAP_SWEEP = [
|
|
5212
|
+
4,
|
|
5213
|
+
6,
|
|
5214
|
+
8,
|
|
5215
|
+
12,
|
|
5216
|
+
16
|
|
5217
|
+
];
|
|
5218
|
+
/** This function gives the settings of the sweep of the section limit: one setting for each value of `cap`. */
|
|
5219
|
+
function capSettings(values = CAP_SWEEP) {
|
|
5220
|
+
return values.map((cap) => ({
|
|
5221
|
+
name: `cap ${cap}`,
|
|
5222
|
+
options: { cap }
|
|
5223
|
+
}));
|
|
5224
|
+
}
|
|
5225
|
+
/**
|
|
5226
|
+
* This function gives one setting for each grouping rule: the router `sections` with the strategy as its option `group`.
|
|
5227
|
+
* The map `options` gives the options of a strategy by its name.
|
|
5228
|
+
*/
|
|
5229
|
+
function groupSettings(strategies = Object.values(groupStrategies), options = {}) {
|
|
5230
|
+
return strategies.map((group) => ({
|
|
5231
|
+
name: `group ${group.name}`,
|
|
5232
|
+
options: {
|
|
5233
|
+
group,
|
|
5234
|
+
...options[group.name] ? { groupOptions: options[group.name] } : {}
|
|
5235
|
+
}
|
|
5236
|
+
}));
|
|
5237
|
+
}
|
|
5238
|
+
/** This function writes the rows of `sweepSections` as a Markdown table. */
|
|
5239
|
+
function formatSections(rows) {
|
|
5240
|
+
return [
|
|
5241
|
+
"| fixture | setting | through | out and back | worst detour | outside | hub routes | runs | levels | far items | level through | level out and back | level outside | level overlaps | ms |",
|
|
5242
|
+
"|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
|
|
5243
|
+
...rows.map((r) => {
|
|
5244
|
+
const f = r.faults;
|
|
5245
|
+
const l = r.levelFaults;
|
|
5246
|
+
return `| ${r.fixture} | ${r.setting} | ${f.through} | ${f.backtrack} | ${f.worstDetour.toFixed(2)} | ${f.outside} | ${r.hubRoutes} | ${r.runs} | ${r.levels} | ${r.farItems} | ${l.through} | ${l.backtrack} | ${l.outside} | ${r.overlaps} | ${r.ms.toFixed(1)} |`;
|
|
5247
|
+
})
|
|
5248
|
+
].join("\n");
|
|
5249
|
+
}
|
|
5250
|
+
export { BACKTRACK_MIN, BACKTRACK_SHARE, BUILT_IN_STRATEGIES, CAP_SWEEP, CASES, CLEARANCE_CAP, CURVE_TYPES, CornerGraph, FIXTURES, GROWTH_SWEEP, Grid, LANE_ORDERS, OUTSIDE_SLACK, Obstacles, PICK, ROUTERS, SECTION_DEFAULTS, SIDE_NORMAL, SLACK_SWEEP, STEPS, STRATEGIES, STRETCH_SWEEP, TOLERANCE, WayFinder, allSections, arcCurve, asRouter, backtrackOf, benchCurveStrategies, benchCurves, benchOne, benchRoom, bendCount, boardGaps, boxExit, buildSections, byCollection, byDataGroup, capSettings, center, checkProperties, checkSections, cornersRouter, countCrossings, countOverlaps, createRegistry, cubicAt, curveOf, curveSpace, curveStrategies, defineStrategy, denseGroupsBoard, dist, distinctPoints, findStrategy, fixedPortPoint, formatCurves, formatFaults, formatRoom, formatSections, formatTable, frechet, gapLanes, getStrategy, goesOutAndBack, groupEdges, groupSettings, groupStrategies, growthSettings, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, itemSizes, keyGroups, keyedSections, laneOrder, laneRoutes, laneSettings, listStrategies, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, openGaps, orderLanes, packedGridBoard, passSettings, pathPoints, pickLevel, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, quadtreeGroups, register, rng, roomDemand, roomLinks, roomStrategies, roundCorners, roundedCurve, routerStrategies, routersOf, runBenchmark, runKey, sameData, sampleCurve, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, slackSettings, splineCurve, splitPoint, straightCurve, straightRouter, stretchSettings, sumFaults, sweepRoom, sweepSections, toSvgPath, treeDepth, trunksRouter, turns };
|
|
4482
5251
|
|
|
4483
5252
|
//# sourceMappingURL=index.js.map
|