@mark1russell7/highways 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/build/index.d.ts +217 -10
- package/build/index.d.ts.map +1 -1
- package/build/index.js +500 -44
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/curves.ts +35 -8
- package/src/bench/room.ts +83 -15
- package/src/index.ts +2 -0
- package/src/room/index.ts +22 -2
- package/src/room/lanes.ts +175 -0
- package/src/routers/index.ts +1 -1
- package/src/strategies/builtins.ts +169 -0
- package/src/strategies/index.ts +2 -0
- package/src/strategies/model.ts +157 -0
package/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);
|
|
@@ -2625,7 +2625,7 @@ function sumFaults(list) {
|
|
|
2625
2625
|
// src/levels.ts
|
|
2626
2626
|
/** The margin of the second corner graph. A gap between two boxes wider than approximately 1.5 times this margin lets a link through. */
|
|
2627
2627
|
const TIGHT_MARGIN = 1;
|
|
2628
|
-
const EPS = 1e-6;
|
|
2628
|
+
const EPS$1 = 1e-6;
|
|
2629
2629
|
/**
|
|
2630
2630
|
* This function removes the repeated points, the points in the middle of a straight run,
|
|
2631
2631
|
* and the points where the way turns back on itself.
|
|
@@ -2633,15 +2633,15 @@ const EPS = 1e-6;
|
|
|
2633
2633
|
function tidy(p) {
|
|
2634
2634
|
const out = [];
|
|
2635
2635
|
for (const q of p) {
|
|
2636
|
-
if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
|
|
2636
|
+
if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$1) continue;
|
|
2637
2637
|
while (out.length >= 2) {
|
|
2638
2638
|
const a = out[out.length - 2];
|
|
2639
2639
|
const b = out[out.length - 1];
|
|
2640
2640
|
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;
|
|
2641
|
+
if (Math.abs(cross) > EPS$1 * (1 + dist(a, b) * dist(b, q))) break;
|
|
2642
2642
|
out.pop();
|
|
2643
2643
|
}
|
|
2644
|
-
if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
|
|
2644
|
+
if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$1) continue;
|
|
2645
2645
|
out.push(q);
|
|
2646
2646
|
}
|
|
2647
2647
|
return out;
|
|
@@ -2881,7 +2881,7 @@ function freeSpans(lo, hi, covered) {
|
|
|
2881
2881
|
if (x >= hi) break;
|
|
2882
2882
|
}
|
|
2883
2883
|
if (x < hi) out.push([x, hi]);
|
|
2884
|
-
return out.filter(([a, b]) => b - a > EPS);
|
|
2884
|
+
return out.filter(([a, b]) => b - a > EPS$1);
|
|
2885
2885
|
}
|
|
2886
2886
|
/**
|
|
2887
2887
|
* 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 +3013,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
|
|
|
3013
3013
|
out.push(...step.legs);
|
|
3014
3014
|
const end = out[out.length - 1];
|
|
3015
3015
|
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;
|
|
3016
|
+
axis = Math.abs(end.y - before.y) < EPS$1 ? "x" : Math.abs(end.x - before.x) < EPS$1 ? "y" : void 0;
|
|
3017
3017
|
i = step.j;
|
|
3018
3018
|
}
|
|
3019
3019
|
return out;
|
|
@@ -3023,7 +3023,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
|
|
|
3023
3023
|
* Of two clear corners, it picks the corner that continues the axis `from` and arrives along the axis `to`.
|
|
3024
3024
|
*/
|
|
3025
3025
|
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;
|
|
3026
|
+
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
3027
|
const options = [{
|
|
3028
3028
|
c: {
|
|
3029
3029
|
x: b.x,
|
|
@@ -3700,7 +3700,7 @@ function trunksRouter(options) {
|
|
|
3700
3700
|
};
|
|
3701
3701
|
}
|
|
3702
3702
|
// src/routers/index.ts
|
|
3703
|
-
/** The routers
|
|
3703
|
+
/** The built-in routers. The registry `STRATEGIES` holds the same routers as strategies, and the benchmark races the routers of the registry. */
|
|
3704
3704
|
const ROUTERS = [
|
|
3705
3705
|
straightRouter,
|
|
3706
3706
|
plugsRouter,
|
|
@@ -3823,17 +3823,34 @@ function openGaps(items, gaps, snap = DEFAULT_SNAP) {
|
|
|
3823
3823
|
* 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
3824
|
*/
|
|
3825
3825
|
function roomDemand(items, links, options = {}) {
|
|
3826
|
+
const gutter = options.gutter ?? DEFAULT_GUTTER;
|
|
3827
|
+
const snap = options.snap ?? DEFAULT_SNAP;
|
|
3828
|
+
const passes = Math.max(1, Math.floor(options.passes ?? 1));
|
|
3829
|
+
let total = demandOnce(items, links, options);
|
|
3830
|
+
for (let p = 1; p < passes; p++) {
|
|
3831
|
+
const next = demandOnce(openGaps(items, total, snap), links, options);
|
|
3832
|
+
if (next.length !== total.length || next.some((g, i) => g.axis !== total[i].axis)) break;
|
|
3833
|
+
total = total.map((g, i) => ({
|
|
3834
|
+
...g,
|
|
3835
|
+
lanes: Math.max(g.lanes, next[i].lanes),
|
|
3836
|
+
need: g.need + Math.max(0, next[i].need - next[i].width)
|
|
3837
|
+
}));
|
|
3838
|
+
}
|
|
3839
|
+
return capGrowth(items, total, gutter, options.maxGrowth ?? Infinity);
|
|
3840
|
+
}
|
|
3841
|
+
/** This function gives the demand of one pass, without the limit `maxGrowth`. */
|
|
3842
|
+
function demandOnce(items, links, options) {
|
|
3826
3843
|
const gutter = options.gutter ?? DEFAULT_GUTTER;
|
|
3827
3844
|
const spacing = options.spacing ?? DEFAULT_SPACING;
|
|
3828
3845
|
const snap = options.snap ?? DEFAULT_SNAP;
|
|
3829
3846
|
const weight = (l) => options.lanes === "count" ? Math.max(1, l.count ?? 1) : 1;
|
|
3830
3847
|
const gaps = boardGaps(items, snap);
|
|
3831
3848
|
const lanes = (options.method ?? "routes") === "lines" ? lineLanes(items, links, gaps, weight) : routeLanes(items, links, gaps, gutter, snap, weight, options.router);
|
|
3832
|
-
return
|
|
3849
|
+
return gaps.map((g, i) => ({
|
|
3833
3850
|
...g,
|
|
3834
3851
|
lanes: lanes[i],
|
|
3835
3852
|
need: Math.max(g.width, gutter + lanes[i] * spacing)
|
|
3836
|
-
}))
|
|
3853
|
+
}));
|
|
3837
3854
|
}
|
|
3838
3855
|
/** This function gives the demand of the board and the board with each gap open to its need. */
|
|
3839
3856
|
function makeRoom(items, links, options = {}) {
|
|
@@ -4018,6 +4035,141 @@ function capGrowth(items, gaps, gutter, maxGrowth) {
|
|
|
4018
4035
|
}
|
|
4019
4036
|
return out;
|
|
4020
4037
|
}
|
|
4038
|
+
// src/room/lanes.ts
|
|
4039
|
+
/** The lane orders of `gapLanes`, in the order of the benchmark. */
|
|
4040
|
+
const LANE_ORDERS = [
|
|
4041
|
+
"links",
|
|
4042
|
+
"ends",
|
|
4043
|
+
"crossings"
|
|
4044
|
+
];
|
|
4045
|
+
const EPS = 1e-6;
|
|
4046
|
+
const ROUNDS = 8;
|
|
4047
|
+
/**
|
|
4048
|
+
* 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.
|
|
4049
|
+
* A leg of a link moves across its gap only, so the link keeps its plugs and the axes of its legs.
|
|
4050
|
+
* When a move puts a link through more boxes than before, the link keeps its points.
|
|
4051
|
+
*/
|
|
4052
|
+
function gapLanes(items, links, options = {}) {
|
|
4053
|
+
const spacing = Math.max(0, options.spacing ?? 4);
|
|
4054
|
+
const order = options.order ?? "crossings";
|
|
4055
|
+
const moved = links.map((l) => l.points.map((p) => ({
|
|
4056
|
+
x: p.x,
|
|
4057
|
+
y: p.y
|
|
4058
|
+
})));
|
|
4059
|
+
for (const g of boardGaps(items, options.snap)) {
|
|
4060
|
+
if (g.width <= EPS) continue;
|
|
4061
|
+
const across = g.axis;
|
|
4062
|
+
const along = g.axis === "x" ? "y" : "x";
|
|
4063
|
+
const lo = g.at - g.width;
|
|
4064
|
+
const hi = g.at;
|
|
4065
|
+
const pieces = [];
|
|
4066
|
+
links.forEach((l, li) => {
|
|
4067
|
+
const p = l.points;
|
|
4068
|
+
for (let i = 2; i <= p.length - 2; i++) {
|
|
4069
|
+
const a = p[i - 1];
|
|
4070
|
+
const b = p[i];
|
|
4071
|
+
if (Math.abs(a[across] - b[across]) > EPS || a[across] <= lo + EPS || a[across] >= hi - EPS) continue;
|
|
4072
|
+
if (Math.abs(a[along] - b[along]) <= EPS) continue;
|
|
4073
|
+
const side = (q, r) => Math.sign(r[across] - q[across]);
|
|
4074
|
+
pieces.push({
|
|
4075
|
+
link: li,
|
|
4076
|
+
seg: i,
|
|
4077
|
+
lo: Math.min(a[along], b[along]),
|
|
4078
|
+
hi: Math.max(a[along], b[along]),
|
|
4079
|
+
turns: [[a[along], side(a, p[i - 2])], [b[along], side(b, p[i + 1])]]
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
});
|
|
4083
|
+
if (pieces.length === 0) continue;
|
|
4084
|
+
const list = orderPieces(pieces, order);
|
|
4085
|
+
const lane = [];
|
|
4086
|
+
let n = 0;
|
|
4087
|
+
list.forEach((P, i) => {
|
|
4088
|
+
let k = 0;
|
|
4089
|
+
for (let j = 0; j < i; j++) if (overlaps(list[j], P)) k = Math.max(k, lane[j] + 1);
|
|
4090
|
+
lane.push(k);
|
|
4091
|
+
n = Math.max(n, k + 1);
|
|
4092
|
+
});
|
|
4093
|
+
const step = Math.min(spacing, g.width / (n + 1));
|
|
4094
|
+
const mid = (lo + hi) / 2;
|
|
4095
|
+
list.forEach((P, i) => {
|
|
4096
|
+
const v = mid + (lane[i] - (n - 1) / 2) * step;
|
|
4097
|
+
moved[P.link][P.seg - 1][across] = v;
|
|
4098
|
+
moved[P.link][P.seg][across] = v;
|
|
4099
|
+
});
|
|
4100
|
+
}
|
|
4101
|
+
const boxes = new Grid(Math.max(20, meanSide(items)));
|
|
4102
|
+
for (const i of items) boxes.add(i.box, i.box);
|
|
4103
|
+
return links.map((l, i) => hits(boxes, moved[i]) > hits(boxes, l.points) ? l : {
|
|
4104
|
+
...l,
|
|
4105
|
+
points: moved[i]
|
|
4106
|
+
});
|
|
4107
|
+
}
|
|
4108
|
+
const overlaps = (P, Q) => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS;
|
|
4109
|
+
/**
|
|
4110
|
+
* This function counts the crossings when the piece `P` is on the near side of the piece `Q`.
|
|
4111
|
+
* 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.
|
|
4112
|
+
*/
|
|
4113
|
+
function cost(P, Q) {
|
|
4114
|
+
let c = 0;
|
|
4115
|
+
for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS && y < Q.hi - EPS) c++;
|
|
4116
|
+
for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS && y < P.hi - EPS) c++;
|
|
4117
|
+
return c;
|
|
4118
|
+
}
|
|
4119
|
+
/** This function gives the pieces of one gap in lane order, from the near side to the far side. */
|
|
4120
|
+
function orderPieces(pieces, order) {
|
|
4121
|
+
if (order === "links") return [...pieces];
|
|
4122
|
+
const sum = (P) => P.turns.reduce((s, [, side]) => s + side, 0);
|
|
4123
|
+
const list = [...pieces].sort((P, Q) => sum(P) - sum(Q) || P.link - Q.link || P.seg - Q.seg);
|
|
4124
|
+
if (order === "ends") return list;
|
|
4125
|
+
const n = list.length;
|
|
4126
|
+
const index = new Map(list.map((P, i) => [P, i]));
|
|
4127
|
+
const c = list.map((P) => list.map((Q) => P !== Q && overlaps(P, Q) ? cost(P, Q) : 0));
|
|
4128
|
+
const at = (P, Q) => c[index.get(P)][index.get(Q)];
|
|
4129
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
4130
|
+
let better = false;
|
|
4131
|
+
for (let i = 0; i < n; i++) {
|
|
4132
|
+
const x = list[i];
|
|
4133
|
+
let best = 0;
|
|
4134
|
+
let to = i;
|
|
4135
|
+
let d = 0;
|
|
4136
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
4137
|
+
d += at(x, list[k]) - at(list[k], x);
|
|
4138
|
+
if (d < best) [best, to] = [d, k];
|
|
4139
|
+
}
|
|
4140
|
+
d = 0;
|
|
4141
|
+
for (let k = i + 1; k < n; k++) {
|
|
4142
|
+
d += at(list[k], x) - at(x, list[k]);
|
|
4143
|
+
if (d < best) [best, to] = [d, k];
|
|
4144
|
+
}
|
|
4145
|
+
if (to !== i) {
|
|
4146
|
+
list.splice(i, 1);
|
|
4147
|
+
list.splice(to, 0, x);
|
|
4148
|
+
better = true;
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
if (!better) break;
|
|
4152
|
+
}
|
|
4153
|
+
return list;
|
|
4154
|
+
}
|
|
4155
|
+
/** This function gives the mean of the longer side of the boxes. */
|
|
4156
|
+
function meanSide(items) {
|
|
4157
|
+
let s = 0;
|
|
4158
|
+
for (const i of items) s += Math.max(i.box.w, i.box.h);
|
|
4159
|
+
return items.length ? s / items.length : 1;
|
|
4160
|
+
}
|
|
4161
|
+
/** This function counts the legs of a polyline that go through a box, for each box. */
|
|
4162
|
+
function hits(boxes, p) {
|
|
4163
|
+
let n = 0;
|
|
4164
|
+
for (let i = 1; i < p.length; i++) {
|
|
4165
|
+
const a = p[i - 1];
|
|
4166
|
+
const b = p[i];
|
|
4167
|
+
boxes.querySegment(a, b, 0, (box) => {
|
|
4168
|
+
if (segmentHitsBox(a, b, box)) n++;
|
|
4169
|
+
});
|
|
4170
|
+
}
|
|
4171
|
+
return n;
|
|
4172
|
+
}
|
|
4021
4173
|
// src/bench/quality.ts
|
|
4022
4174
|
/** The clearance stops at this distance. */
|
|
4023
4175
|
const CLEARANCE_CAP = 40;
|
|
@@ -4306,6 +4458,196 @@ function formatFaults(rows) {
|
|
|
4306
4458
|
})
|
|
4307
4459
|
].join("\n");
|
|
4308
4460
|
}
|
|
4461
|
+
// src/strategies/model.ts
|
|
4462
|
+
/** The steps that have strategies, in the order of the pipeline. */
|
|
4463
|
+
const STEPS = [
|
|
4464
|
+
"router",
|
|
4465
|
+
"curve",
|
|
4466
|
+
"room"
|
|
4467
|
+
];
|
|
4468
|
+
/**
|
|
4469
|
+
* This function makes a strategy from its definition. The strategy fills the missing options from `defaults`.
|
|
4470
|
+
* When the caller gives no option, the definition gets the `defaults` object itself.
|
|
4471
|
+
*/
|
|
4472
|
+
function defineStrategy(definition) {
|
|
4473
|
+
const { step, name, describe, defaults, maxNodes } = definition;
|
|
4474
|
+
if (name.length === 0) throw new Error(`A strategy of the step "${step}" has no name.`);
|
|
4475
|
+
const strategy = {
|
|
4476
|
+
step,
|
|
4477
|
+
name,
|
|
4478
|
+
describe,
|
|
4479
|
+
defaults: Object.freeze({ ...defaults }),
|
|
4480
|
+
...maxNodes === void 0 ? {} : { maxNodes },
|
|
4481
|
+
run(input, options) {
|
|
4482
|
+
const all = options === void 0 || Object.keys(options).length === 0 ? strategy.defaults : {
|
|
4483
|
+
...strategy.defaults,
|
|
4484
|
+
...options
|
|
4485
|
+
};
|
|
4486
|
+
return definition.run(input, all);
|
|
4487
|
+
}
|
|
4488
|
+
};
|
|
4489
|
+
return Object.freeze(strategy);
|
|
4490
|
+
}
|
|
4491
|
+
/** 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. */
|
|
4492
|
+
function createRegistry(strategies) {
|
|
4493
|
+
const out = {
|
|
4494
|
+
router: [],
|
|
4495
|
+
curve: [],
|
|
4496
|
+
room: []
|
|
4497
|
+
};
|
|
4498
|
+
for (const s of strategies) {
|
|
4499
|
+
const list = out[s.step];
|
|
4500
|
+
if (!list) throw new Error(`There is no step "${String(s.step)}". The steps are ${STEPS.join(", ")}.`);
|
|
4501
|
+
if (list.some((t) => t.name === s.name)) throw new Error(`The step "${s.step}" has two strategies with the name "${s.name}".`);
|
|
4502
|
+
list.push(s);
|
|
4503
|
+
}
|
|
4504
|
+
return Object.freeze({
|
|
4505
|
+
router: Object.freeze(out.router),
|
|
4506
|
+
curve: Object.freeze(out.curve),
|
|
4507
|
+
room: Object.freeze(out.room)
|
|
4508
|
+
});
|
|
4509
|
+
}
|
|
4510
|
+
/** This function gives a new registry with the strategies of `registry` and then `strategies`. It does not change `registry`. */
|
|
4511
|
+
function register(registry, ...strategies) {
|
|
4512
|
+
return createRegistry([
|
|
4513
|
+
...registry.router,
|
|
4514
|
+
...registry.curve,
|
|
4515
|
+
...registry.room,
|
|
4516
|
+
...strategies
|
|
4517
|
+
]);
|
|
4518
|
+
}
|
|
4519
|
+
/** This function gives the strategies of one step of a registry, in order. */
|
|
4520
|
+
function listStrategies(registry, step) {
|
|
4521
|
+
return registry[step];
|
|
4522
|
+
}
|
|
4523
|
+
/** This function gives the strategy of one step with a name, or `undefined` when the registry has no such strategy. */
|
|
4524
|
+
function findStrategy(registry, step, name) {
|
|
4525
|
+
return listStrategies(registry, step).find((s) => s.name === name);
|
|
4526
|
+
}
|
|
4527
|
+
/** This function gives the strategy of one step with a name. When the registry has no such strategy, the function throws an error. */
|
|
4528
|
+
function getStrategy(registry, step, name) {
|
|
4529
|
+
const s = findStrategy(registry, step, name);
|
|
4530
|
+
if (!s) throw new Error(`There is no ${step} "${name}". The registry has ${listStrategies(registry, step).map((t) => t.name).join(", ")}.`);
|
|
4531
|
+
return s;
|
|
4532
|
+
}
|
|
4533
|
+
/** This function gives a `Router` that runs a router strategy with the options. The router keeps the name and the `maxNodes` of the strategy. */
|
|
4534
|
+
function asRouter(strategy, options) {
|
|
4535
|
+
return {
|
|
4536
|
+
name: strategy.name,
|
|
4537
|
+
...strategy.maxNodes === void 0 ? {} : { maxNodes: strategy.maxNodes },
|
|
4538
|
+
route: (graph) => strategy.run(graph, options)
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
/** This function gives the router strategies of a registry as `Router` objects, in order. The benchmark races them. */
|
|
4542
|
+
function routersOf(registry) {
|
|
4543
|
+
return listStrategies(registry, "router").map((s) => asRouter(s));
|
|
4544
|
+
}
|
|
4545
|
+
// src/strategies/builtins.ts
|
|
4546
|
+
/** This function makes a router strategy from a router factory and a preset of its options. Each call makes the router of its options. */
|
|
4547
|
+
function routerStrategy(name, describe, maxNodes, defaults, make) {
|
|
4548
|
+
return defineStrategy({
|
|
4549
|
+
step: "router",
|
|
4550
|
+
name,
|
|
4551
|
+
describe,
|
|
4552
|
+
defaults,
|
|
4553
|
+
maxNodes,
|
|
4554
|
+
run: (graph, options) => make({
|
|
4555
|
+
...options,
|
|
4556
|
+
name
|
|
4557
|
+
}).route(graph)
|
|
4558
|
+
});
|
|
4559
|
+
}
|
|
4560
|
+
/** This function makes a router strategy for a router without options. */
|
|
4561
|
+
function fixedRouter(router, describe) {
|
|
4562
|
+
return defineStrategy({
|
|
4563
|
+
step: "router",
|
|
4564
|
+
name: router.name,
|
|
4565
|
+
describe,
|
|
4566
|
+
defaults: {},
|
|
4567
|
+
run: (graph) => router.route(graph)
|
|
4568
|
+
});
|
|
4569
|
+
}
|
|
4570
|
+
/** The built-in router strategies, in the order of the benchmark. They route as the routers of `ROUTERS`. */
|
|
4571
|
+
const routerStrategies = Object.freeze({
|
|
4572
|
+
straight: fixedRouter(straightRouter, "The control. Each edge is one straight line between its plugs."),
|
|
4573
|
+
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."),
|
|
4574
|
+
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),
|
|
4575
|
+
"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),
|
|
4576
|
+
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),
|
|
4577
|
+
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),
|
|
4578
|
+
"shared-all": routerStrategy("shared-all", "The same as `shared`, but all edges share their ways.", 1e4, {}, sharedRouter),
|
|
4579
|
+
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),
|
|
4580
|
+
"sections-tree": routerStrategy("sections-tree", "The same as `sections`, but without the limit of the stretch.", 1e5, { stretch: Infinity }, sectionsRouter)
|
|
4581
|
+
});
|
|
4582
|
+
/** The context of a curve input, without the keys that have no value. */
|
|
4583
|
+
const contextOf = (input) => ({
|
|
4584
|
+
...input.startNormal ? { startNormal: input.startNormal } : {},
|
|
4585
|
+
...input.endNormal ? { endNormal: input.endNormal } : {},
|
|
4586
|
+
...input.space ? { space: input.space } : {}
|
|
4587
|
+
});
|
|
4588
|
+
/** The built-in curve strategies, in the order of `CURVE_TYPES`. Each one gives the same segments as `curveOf` with its type. */
|
|
4589
|
+
const curveStrategies = Object.freeze({
|
|
4590
|
+
straight: defineStrategy({
|
|
4591
|
+
step: "curve",
|
|
4592
|
+
name: "straight",
|
|
4593
|
+
describe: "The polyline as it is.",
|
|
4594
|
+
defaults: {},
|
|
4595
|
+
run: (input) => straightCurve(input.points)
|
|
4596
|
+
}),
|
|
4597
|
+
rounded: defineStrategy({
|
|
4598
|
+
step: "curve",
|
|
4599
|
+
name: "rounded",
|
|
4600
|
+
describe: "A quadratic Bezier curve at each corner, as `roundCorners` draws it. The first and the last legs stay.",
|
|
4601
|
+
defaults: { radius: 12 },
|
|
4602
|
+
run: (input, o) => roundedCurve(input.points, {
|
|
4603
|
+
...contextOf(input),
|
|
4604
|
+
radius: o.radius
|
|
4605
|
+
})
|
|
4606
|
+
}),
|
|
4607
|
+
spline: defineStrategy({
|
|
4608
|
+
step: "curve",
|
|
4609
|
+
name: "spline",
|
|
4610
|
+
describe: "A centripetal Catmull-Rom spline through each point. It does not overshoot or make loops in a piece.",
|
|
4611
|
+
defaults: { tension: 0 },
|
|
4612
|
+
run: (input, o) => splineCurve(input.points, {
|
|
4613
|
+
...contextOf(input),
|
|
4614
|
+
tension: o.tension
|
|
4615
|
+
})
|
|
4616
|
+
}),
|
|
4617
|
+
arc: defineStrategy({
|
|
4618
|
+
step: "curve",
|
|
4619
|
+
name: "arc",
|
|
4620
|
+
describe: "A circular arc at each corner, tangent to the two legs. The first and the last legs stay.",
|
|
4621
|
+
defaults: { radius: 12 },
|
|
4622
|
+
run: (input, o) => arcCurve(input.points, {
|
|
4623
|
+
...contextOf(input),
|
|
4624
|
+
radius: o.radius
|
|
4625
|
+
})
|
|
4626
|
+
})
|
|
4627
|
+
});
|
|
4628
|
+
const roomStrategy = (method, describe) => defineStrategy({
|
|
4629
|
+
step: "room",
|
|
4630
|
+
name: method,
|
|
4631
|
+
describe,
|
|
4632
|
+
defaults: {},
|
|
4633
|
+
run: (input, o) => roomDemand(input.items, input.links, {
|
|
4634
|
+
...o,
|
|
4635
|
+
method
|
|
4636
|
+
})
|
|
4637
|
+
});
|
|
4638
|
+
/** The built-in room strategies: the methods of `roomDemand`. */
|
|
4639
|
+
const roomStrategies = Object.freeze({
|
|
4640
|
+
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."),
|
|
4641
|
+
lines: roomStrategy("lines", "The lanes of a gap are the straight lines between the centers of the boxes that cross the gap.")
|
|
4642
|
+
});
|
|
4643
|
+
/** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
|
|
4644
|
+
const BUILT_IN_STRATEGIES = Object.freeze([
|
|
4645
|
+
...Object.values(routerStrategies),
|
|
4646
|
+
...Object.values(curveStrategies),
|
|
4647
|
+
...Object.values(roomStrategies)
|
|
4648
|
+
]);
|
|
4649
|
+
/** The registry of the built-in strategies. The benchmark and the site take their lists from it. */
|
|
4650
|
+
const STRATEGIES = createRegistry(BUILT_IN_STRATEGIES);
|
|
4309
4651
|
// src/bench/curves.ts
|
|
4310
4652
|
const now$1 = () => globalThis.performance?.now() ?? Date.now();
|
|
4311
4653
|
/**
|
|
@@ -4314,6 +4656,13 @@ const now$1 = () => globalThis.performance?.now() ?? Date.now();
|
|
|
4314
4656
|
* The type `spline` leaves each plug along the normal of the nearest side of its node.
|
|
4315
4657
|
*/
|
|
4316
4658
|
function benchCurves(router, fixture, graph, options = {}) {
|
|
4659
|
+
return benchCurveStrategies(router, fixture, graph, CURVE_TYPES.map((t) => curveStrategies[t]), options);
|
|
4660
|
+
}
|
|
4661
|
+
/**
|
|
4662
|
+
* This function does the work of `benchCurves` with a list of curve strategies, in place of the curve types.
|
|
4663
|
+
* The option `radius` goes to each strategy. A strategy without such an option does not read it.
|
|
4664
|
+
*/
|
|
4665
|
+
function benchCurveStrategies(router, fixture, graph, curves, options = {}) {
|
|
4317
4666
|
const tolerance = options.tolerance ?? .25;
|
|
4318
4667
|
const result = router.route(graph);
|
|
4319
4668
|
const byId = nodeIndex(graph);
|
|
@@ -4337,17 +4686,17 @@ function benchCurves(router, fixture, graph, options = {}) {
|
|
|
4337
4686
|
});
|
|
4338
4687
|
const base = measureLinks(boxes, routes);
|
|
4339
4688
|
const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
|
|
4340
|
-
|
|
4689
|
+
const radius = { radius: options.radius ?? 12 };
|
|
4690
|
+
return curves.map((curve) => {
|
|
4341
4691
|
const make = (withSpace) => routes.map((r) => ({
|
|
4342
4692
|
source: r.source,
|
|
4343
4693
|
target: r.target,
|
|
4344
|
-
points: sampleCurve(
|
|
4345
|
-
|
|
4346
|
-
radius: options.radius ?? 12,
|
|
4694
|
+
points: sampleCurve(curve.run({
|
|
4695
|
+
points: r.points,
|
|
4347
4696
|
startNormal: r.startNormal,
|
|
4348
4697
|
endNormal: r.endNormal,
|
|
4349
4698
|
...withSpace ? { space } : {}
|
|
4350
|
-
}), tolerance)
|
|
4699
|
+
}, radius), tolerance)
|
|
4351
4700
|
}));
|
|
4352
4701
|
const free = measureLinks(boxes, make(false));
|
|
4353
4702
|
const t0 = now$1();
|
|
@@ -4357,7 +4706,7 @@ function benchCurves(router, fixture, graph, options = {}) {
|
|
|
4357
4706
|
return {
|
|
4358
4707
|
router: router.name,
|
|
4359
4708
|
fixture,
|
|
4360
|
-
type,
|
|
4709
|
+
type: curve.name,
|
|
4361
4710
|
links: routes.length,
|
|
4362
4711
|
through: base.through,
|
|
4363
4712
|
free: free.through,
|
|
@@ -4393,11 +4742,13 @@ function areaOf(nodes) {
|
|
|
4393
4742
|
}
|
|
4394
4743
|
/**
|
|
4395
4744
|
* 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
|
-
*
|
|
4745
|
+
* With a lane order, each link of the far view gets its own lane in each gap (`gapLanes` at the lane width `spacing`).
|
|
4746
|
+
* Without it, the links of a gap share the lines of the level links.
|
|
4747
|
+
* The function also measures the lane routes of the router `sections` at the lane width `spacing`.
|
|
4397
4748
|
*/
|
|
4398
|
-
function measureBoard(graph, spacing) {
|
|
4749
|
+
function measureBoard(graph, spacing, order) {
|
|
4399
4750
|
const links = roomLinks(graph);
|
|
4400
|
-
const
|
|
4751
|
+
const level = levelLinks(graph.nodes.map((n) => ({
|
|
4401
4752
|
id: n.id,
|
|
4402
4753
|
box: n.box,
|
|
4403
4754
|
nodes: [n.id]
|
|
@@ -4406,6 +4757,10 @@ function measureBoard(graph, spacing) {
|
|
|
4406
4757
|
target: l.target,
|
|
4407
4758
|
count: l.count ?? 1
|
|
4408
4759
|
})));
|
|
4760
|
+
const drawn = order ? gapLanes(graph.nodes, level, {
|
|
4761
|
+
spacing,
|
|
4762
|
+
order
|
|
4763
|
+
}) : level;
|
|
4409
4764
|
const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
|
|
4410
4765
|
const edgeOf = /* @__PURE__ */ new Map();
|
|
4411
4766
|
for (const e of graph.edges) {
|
|
@@ -4427,10 +4782,10 @@ function measureBoard(graph, spacing) {
|
|
|
4427
4782
|
};
|
|
4428
4783
|
}
|
|
4429
4784
|
/**
|
|
4430
|
-
* This function measures a packed board, the board with each gap open to the gutter only, and the board that
|
|
4431
|
-
* The options
|
|
4785
|
+
* This function measures a packed board, the board with each gap open to the gutter only, and the board that each room strategy opens.
|
|
4786
|
+
* The options are the same for each strategy, except `method`. The default strategies are the methods `routes` and `lines` of `roomDemand`.
|
|
4432
4787
|
*/
|
|
4433
|
-
function benchRoom(fixture, graph, options = {}) {
|
|
4788
|
+
function benchRoom(fixture, graph, options = {}, methods = [roomStrategies.routes, roomStrategies.lines]) {
|
|
4434
4789
|
const spacing = options.spacing ?? 4;
|
|
4435
4790
|
const area0 = areaOf(graph.nodes);
|
|
4436
4791
|
const links = roomLinks(graph);
|
|
@@ -4445,28 +4800,129 @@ function benchRoom(fixture, graph, options = {}) {
|
|
|
4445
4800
|
}, spacing)
|
|
4446
4801
|
});
|
|
4447
4802
|
const rows = [row("packed", graph.nodes, 0)];
|
|
4448
|
-
const
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
}
|
|
4462
|
-
|
|
4463
|
-
|
|
4803
|
+
const t0 = now();
|
|
4804
|
+
const gutter = makeRoom(graph.nodes, links, {
|
|
4805
|
+
...options,
|
|
4806
|
+
spacing: 0,
|
|
4807
|
+
method: "routes"
|
|
4808
|
+
});
|
|
4809
|
+
rows.push(row("gutter", gutter.items, now() - t0));
|
|
4810
|
+
const { method: _method, ...preset } = options;
|
|
4811
|
+
for (const s of methods) {
|
|
4812
|
+
const t1 = now();
|
|
4813
|
+
const gaps = s.run({
|
|
4814
|
+
items: graph.nodes,
|
|
4815
|
+
links
|
|
4816
|
+
}, preset);
|
|
4817
|
+
const nodes = openGaps(graph.nodes, gaps, options.snap);
|
|
4818
|
+
rows.push(row(s.name, nodes, now() - t1));
|
|
4819
|
+
}
|
|
4820
|
+
return rows;
|
|
4821
|
+
}
|
|
4822
|
+
/**
|
|
4823
|
+
* This function measures the board that `makeRoom` opens with each setting, with the method `routes`.
|
|
4824
|
+
* 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`.
|
|
4825
|
+
*/
|
|
4826
|
+
function sweepRoom(fixture, graph, settings, baseline = {
|
|
4827
|
+
gutter: 8,
|
|
4828
|
+
spacing: 4
|
|
4829
|
+
}) {
|
|
4830
|
+
const area0 = areaOf(graph.nodes);
|
|
4831
|
+
const links = roomLinks(graph);
|
|
4832
|
+
const row = (board, nodes, ms, spacing, lanes) => ({
|
|
4833
|
+
fixture,
|
|
4834
|
+
board,
|
|
4835
|
+
area: area0 > 0 ? areaOf(nodes) / area0 : 1,
|
|
4836
|
+
ms,
|
|
4837
|
+
...measureBoard({
|
|
4838
|
+
nodes,
|
|
4839
|
+
edges: graph.edges
|
|
4840
|
+
}, spacing, lanes)
|
|
4841
|
+
});
|
|
4842
|
+
const rows = [row("gutter", makeRoom(graph.nodes, links, {
|
|
4843
|
+
...baseline,
|
|
4844
|
+
spacing: 0,
|
|
4845
|
+
method: "routes",
|
|
4846
|
+
maxGrowth: Infinity,
|
|
4847
|
+
passes: 1
|
|
4848
|
+
}).items, 0, baseline.spacing ?? 4)];
|
|
4849
|
+
for (const s of settings) {
|
|
4464
4850
|
const t0 = now();
|
|
4465
|
-
const
|
|
4466
|
-
|
|
4851
|
+
const open = makeRoom(graph.nodes, links, {
|
|
4852
|
+
...s.options,
|
|
4853
|
+
method: "routes"
|
|
4854
|
+
});
|
|
4855
|
+
rows.push(row(s.name, open.items, now() - t0, s.options.spacing ?? 4, s.lanes));
|
|
4467
4856
|
}
|
|
4468
4857
|
return rows;
|
|
4469
4858
|
}
|
|
4859
|
+
/** The values of `maxGrowth` of the sweep of ROOM-A. */
|
|
4860
|
+
const GROWTH_SWEEP = [
|
|
4861
|
+
0,
|
|
4862
|
+
.06,
|
|
4863
|
+
.09,
|
|
4864
|
+
.12,
|
|
4865
|
+
.15,
|
|
4866
|
+
.18,
|
|
4867
|
+
.21,
|
|
4868
|
+
.25,
|
|
4869
|
+
.3,
|
|
4870
|
+
.35,
|
|
4871
|
+
.5,
|
|
4872
|
+
Infinity
|
|
4873
|
+
];
|
|
4874
|
+
/** This function gives the settings of ROOM-A: the area and the faults for each `maxGrowth`, at gutter 8 and spacing 4. */
|
|
4875
|
+
function growthSettings(values = GROWTH_SWEEP) {
|
|
4876
|
+
return values.map((m) => ({
|
|
4877
|
+
name: `maxGrowth ${m}`,
|
|
4878
|
+
options: {
|
|
4879
|
+
gutter: 8,
|
|
4880
|
+
spacing: 4,
|
|
4881
|
+
maxGrowth: m
|
|
4882
|
+
}
|
|
4883
|
+
}));
|
|
4884
|
+
}
|
|
4885
|
+
/**
|
|
4886
|
+
* This function gives the settings of ROOM-C: each lane order of the far view at each spacing, at gutter 8.
|
|
4887
|
+
* The order `shared` is the far view without lanes, as before ROOM-C.
|
|
4888
|
+
*/
|
|
4889
|
+
function laneSettings(spacings = [
|
|
4890
|
+
2,
|
|
4891
|
+
4,
|
|
4892
|
+
6,
|
|
4893
|
+
8
|
|
4894
|
+
], orders = LANE_ORDERS) {
|
|
4895
|
+
return spacings.flatMap((spacing) => [{
|
|
4896
|
+
name: `shared spacing ${spacing}`,
|
|
4897
|
+
options: {
|
|
4898
|
+
gutter: 8,
|
|
4899
|
+
spacing
|
|
4900
|
+
}
|
|
4901
|
+
}, ...orders.map((lanes) => ({
|
|
4902
|
+
name: `${lanes} spacing ${spacing}`,
|
|
4903
|
+
options: {
|
|
4904
|
+
gutter: 8,
|
|
4905
|
+
spacing
|
|
4906
|
+
},
|
|
4907
|
+
lanes
|
|
4908
|
+
}))]);
|
|
4909
|
+
}
|
|
4910
|
+
/** This function gives the settings of ROOM-D: each gutter with each number of passes, at spacing 4. */
|
|
4911
|
+
function passSettings(gutters = [
|
|
4912
|
+
4,
|
|
4913
|
+
8,
|
|
4914
|
+
12,
|
|
4915
|
+
16
|
|
4916
|
+
], passes = [1, 2]) {
|
|
4917
|
+
return gutters.flatMap((gutter) => passes.map((p) => ({
|
|
4918
|
+
name: `gutter ${gutter} passes ${p}`,
|
|
4919
|
+
options: {
|
|
4920
|
+
gutter,
|
|
4921
|
+
spacing: 4,
|
|
4922
|
+
passes: p
|
|
4923
|
+
}
|
|
4924
|
+
})));
|
|
4925
|
+
}
|
|
4470
4926
|
/** This function writes the rows of `benchRoom` as a Markdown table. */
|
|
4471
4927
|
function formatRoom(rows) {
|
|
4472
4928
|
return [
|
|
@@ -4478,6 +4934,6 @@ function formatRoom(rows) {
|
|
|
4478
4934
|
})
|
|
4479
4935
|
].join("\n");
|
|
4480
4936
|
}
|
|
4481
|
-
export { BACKTRACK_MIN, BACKTRACK_SHARE, CASES, CLEARANCE_CAP, CURVE_TYPES, CornerGraph, FIXTURES, Grid, OUTSIDE_SLACK, Obstacles, ROUTERS, SIDE_NORMAL, TOLERANCE, WayFinder, allSections, arcCurve, backtrackOf, benchCurves, benchOne, benchRoom, bendCount, boardGaps, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, countOverlaps, cubicAt, curveOf, curveSpace, denseGroupsBoard, dist, distinctPoints, fixedPortPoint, formatCurves, formatFaults, formatRoom, formatTable, frechet, goesOutAndBack, groupEdges, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, openGaps, orderLanes, packedGridBoard, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, rng, roomDemand, roomLinks, roundCorners, roundedCurve, runBenchmark, runKey, sameData, sampleCurve, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splineCurve, splitPoint, straightCurve, straightRouter, sumFaults, toSvgPath, treeDepth, trunksRouter, turns };
|
|
4937
|
+
export { BACKTRACK_MIN, BACKTRACK_SHARE, BUILT_IN_STRATEGIES, CASES, CLEARANCE_CAP, CURVE_TYPES, CornerGraph, FIXTURES, GROWTH_SWEEP, Grid, LANE_ORDERS, OUTSIDE_SLACK, Obstacles, ROUTERS, SIDE_NORMAL, STEPS, STRATEGIES, TOLERANCE, WayFinder, allSections, arcCurve, asRouter, backtrackOf, benchCurveStrategies, benchCurves, benchOne, benchRoom, bendCount, boardGaps, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, countOverlaps, createRegistry, cubicAt, curveOf, curveSpace, curveStrategies, defineStrategy, denseGroupsBoard, dist, distinctPoints, findStrategy, fixedPortPoint, formatCurves, formatFaults, formatRoom, formatTable, frechet, gapLanes, getStrategy, goesOutAndBack, groupEdges, growthSettings, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, laneSettings, listStrategies, makeFixture, makeRoom, manyGroupsBoard, measureBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, openGaps, orderLanes, packedGridBoard, passSettings, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, register, rng, roomDemand, roomLinks, roomStrategies, roundCorners, roundedCurve, routerStrategies, routersOf, runBenchmark, runKey, sameData, sampleCurve, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splineCurve, splitPoint, straightCurve, straightRouter, sumFaults, sweepRoom, toSvgPath, treeDepth, trunksRouter, turns };
|
|
4482
4938
|
|
|
4483
4939
|
//# sourceMappingURL=index.js.map
|