@mark1russell7/highways 0.1.2 → 0.1.3
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/build/index.d.ts +147 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +965 -264
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/links.ts +191 -0
- package/src/bench/run.ts +28 -0
- package/src/cases.ts +111 -0
- package/src/grid.ts +61 -2
- package/src/index.ts +2 -0
- package/src/levels.ts +150 -14
- package/src/routers/sections.ts +33 -6
- package/src/visibility.ts +309 -92
package/build/index.js
CHANGED
|
@@ -228,6 +228,53 @@ var Grid = class {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* This method gives each item in the buckets along the segment `ab`, widened by `margin`, to `visit`, one time each.
|
|
233
|
+
* The buckets are those of `#segment`, in the same order, so the items come in the same order.
|
|
234
|
+
* A bucket in the range of the step before has no new item, so the walk skips it.
|
|
235
|
+
* The walk stops when `visit` gives true, and then the method gives true.
|
|
236
|
+
*/
|
|
237
|
+
#walk(a, b, margin, visit) {
|
|
238
|
+
const stamp = ++this.#stamp;
|
|
239
|
+
const c = this.#cell;
|
|
240
|
+
const items = this.#items;
|
|
241
|
+
const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / (c / 2)));
|
|
242
|
+
let px = a.x;
|
|
243
|
+
let py = a.y;
|
|
244
|
+
let pi0 = 1;
|
|
245
|
+
let pi1 = 0;
|
|
246
|
+
let pj0 = 1;
|
|
247
|
+
let pj1 = 0;
|
|
248
|
+
for (let k = 1; k <= n; k++) {
|
|
249
|
+
const qx = k === n ? b.x : a.x + (b.x - a.x) * k / n;
|
|
250
|
+
const qy = k === n ? b.y : a.y + (b.y - a.y) * k / n;
|
|
251
|
+
const i0 = Math.floor((Math.min(px, qx) - margin) / c);
|
|
252
|
+
const j0 = Math.floor((Math.min(py, qy) - margin) / c);
|
|
253
|
+
const i1 = Math.floor((Math.max(px, qx) + margin) / c);
|
|
254
|
+
const j1 = Math.floor((Math.max(py, qy) + margin) / c);
|
|
255
|
+
const seen = this.#seen;
|
|
256
|
+
for (let i = i0; i <= i1; i++) {
|
|
257
|
+
const before = i >= pi0 && i <= pi1;
|
|
258
|
+
for (let j = j0; j <= j1; j++) {
|
|
259
|
+
if (before && j >= pj0 && j <= pj1) continue;
|
|
260
|
+
const list = this.#buckets.get(this.#key(i, j));
|
|
261
|
+
if (list === void 0) continue;
|
|
262
|
+
for (const id of list) {
|
|
263
|
+
if (seen[id] === stamp) continue;
|
|
264
|
+
seen[id] = stamp;
|
|
265
|
+
if (visit(items[id]) === true) return true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
pi0 = i0;
|
|
270
|
+
pi1 = i1;
|
|
271
|
+
pj0 = j0;
|
|
272
|
+
pj1 = j1;
|
|
273
|
+
px = qx;
|
|
274
|
+
py = qy;
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
231
278
|
#segment(a, b, margin, each) {
|
|
232
279
|
const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y) / (this.#cell / 2)));
|
|
233
280
|
let px = a.x;
|
|
@@ -255,8 +302,16 @@ var Grid = class {
|
|
|
255
302
|
}
|
|
256
303
|
/** This method gives each item whose buckets come within `margin` of the segment `ab` to `visit`, one time each. */
|
|
257
304
|
querySegment(a, b, margin, visit) {
|
|
258
|
-
|
|
259
|
-
|
|
305
|
+
this.#walk(a, b, margin, (item) => {
|
|
306
|
+
visit(item);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* This method tells if `test` gives true for an item whose buckets come within `margin` of the segment `ab`.
|
|
311
|
+
* It gives the items in the order of `querySegment`, and it stops at the first item that passes the test.
|
|
312
|
+
*/
|
|
313
|
+
someSegment(a, b, margin, test) {
|
|
314
|
+
return this.#walk(a, b, margin, test);
|
|
260
315
|
}
|
|
261
316
|
};
|
|
262
317
|
/** This function gives the bounding box of the segment `ab`. */
|
|
@@ -546,6 +601,125 @@ function makeEdges(spec, nodes, clusterOf, R) {
|
|
|
546
601
|
}
|
|
547
602
|
return edges;
|
|
548
603
|
}
|
|
604
|
+
// src/cases.ts
|
|
605
|
+
/** A board while it grows: the functions add nodes and edges in order. */
|
|
606
|
+
var Board = class {
|
|
607
|
+
nodes = [];
|
|
608
|
+
edges = [];
|
|
609
|
+
#pairs = /* @__PURE__ */ new Set();
|
|
610
|
+
node(id, x, y, w, h) {
|
|
611
|
+
this.nodes.push({
|
|
612
|
+
id,
|
|
613
|
+
box: {
|
|
614
|
+
x,
|
|
615
|
+
y,
|
|
616
|
+
w,
|
|
617
|
+
h
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
return id;
|
|
621
|
+
}
|
|
622
|
+
/** This method adds an edge. It adds nothing for a loop or for a second edge between the same two nodes. */
|
|
623
|
+
link(source, target) {
|
|
624
|
+
const key = source < target ? `${source}|${target}` : `${target}|${source}`;
|
|
625
|
+
if (source === target || this.#pairs.has(key)) return;
|
|
626
|
+
this.#pairs.add(key);
|
|
627
|
+
this.edges.push({
|
|
628
|
+
id: `e${this.edges.length}`,
|
|
629
|
+
source: { node: source },
|
|
630
|
+
target: { node: target }
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
graph() {
|
|
634
|
+
return {
|
|
635
|
+
nodes: this.nodes,
|
|
636
|
+
edges: this.edges
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
/**
|
|
641
|
+
* This function makes a dense board: 150 nodes in tight clusters. At the far view, the boxes of the groups overlap.
|
|
642
|
+
* Then a level link starts or ends in a group that another group covers.
|
|
643
|
+
*/
|
|
644
|
+
function denseGroupsBoard() {
|
|
645
|
+
return makeFixture({
|
|
646
|
+
name: "dense-groups",
|
|
647
|
+
kind: "clustered",
|
|
648
|
+
nodes: 150,
|
|
649
|
+
edgesPerNode: 2,
|
|
650
|
+
seed: 3
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* This function makes a board of three rows of 32 cards. Each card links to the next card of its row.
|
|
655
|
+
* Forty long links join cards far apart. The hub of a section is near the middle of its cards.
|
|
656
|
+
* So a long link climbs to hubs behind its source or past its target, and comes back.
|
|
657
|
+
*/
|
|
658
|
+
function hubChainBoard() {
|
|
659
|
+
const R = rng(3);
|
|
660
|
+
const b = new Board();
|
|
661
|
+
const rows = 3;
|
|
662
|
+
const cols = 32;
|
|
663
|
+
const id = (r, c) => `c${r}-${c}`;
|
|
664
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) b.node(id(r, c), c * 128, r * 84, 80, 36);
|
|
665
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c + 1 < cols; c++) b.link(id(r, c), id(r, c + 1));
|
|
666
|
+
for (let n = 0; n < 40; n++) {
|
|
667
|
+
const r = Math.floor(R() * rows);
|
|
668
|
+
const c = Math.floor(R() * cols);
|
|
669
|
+
const span = Math.floor(cols / 8 + R() * cols / 2);
|
|
670
|
+
if (c + span < cols) b.link(id(r, c), id((r + 1) % rows, c + span));
|
|
671
|
+
}
|
|
672
|
+
return b.graph();
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* This function makes a board of 32 groups in 8 columns and 4 rows. Each group has 4 cards in a square.
|
|
676
|
+
* The cards of a group link in a chain, and 96 random links join cards of two groups.
|
|
677
|
+
* On a level with many groups, some level links go around the outer edge of the board.
|
|
678
|
+
*/
|
|
679
|
+
function manyGroupsBoard() {
|
|
680
|
+
const R = rng(7);
|
|
681
|
+
const b = new Board();
|
|
682
|
+
const cols = 8;
|
|
683
|
+
const rows = 4;
|
|
684
|
+
const per = 4;
|
|
685
|
+
const w = 60;
|
|
686
|
+
const h = 30;
|
|
687
|
+
const cardGap = 12;
|
|
688
|
+
const side = Math.ceil(Math.sqrt(per));
|
|
689
|
+
Math.ceil(per / side) * h + (Math.ceil(per / side) - 1) * cardGap;
|
|
690
|
+
const groups = [];
|
|
691
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
|
|
692
|
+
const list = [];
|
|
693
|
+
for (let i = 0; i < per; i++) {
|
|
694
|
+
const x = c * 192 + i % side * 72;
|
|
695
|
+
const y = r * 132 + Math.floor(i / side) * 42;
|
|
696
|
+
list.push(b.node(`g${r}-${c}-${i}`, x, y, w, h));
|
|
697
|
+
}
|
|
698
|
+
groups.push(list);
|
|
699
|
+
}
|
|
700
|
+
for (const list of groups) for (let i = 1; i < list.length; i++) b.link(list[i - 1], list[i]);
|
|
701
|
+
const pick = (list) => list[Math.floor(R() * list.length)];
|
|
702
|
+
for (let n = 0; n < groups.length * 3; n++) b.link(pick(groups[Math.floor(R() * groups.length)]), pick(groups[Math.floor(R() * groups.length)]));
|
|
703
|
+
return b.graph();
|
|
704
|
+
}
|
|
705
|
+
/** The case boards of the benchmark. Each board has a few hundred nodes at most, so the benchmark races them on each run. */
|
|
706
|
+
const CASES = [
|
|
707
|
+
{
|
|
708
|
+
name: "case-dense-groups",
|
|
709
|
+
defect: "Groups overlap at the far view, and level links go through other groups.",
|
|
710
|
+
make: denseGroupsBoard
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
name: "case-hub-chain",
|
|
714
|
+
defect: "A route climbs a chain of hubs away from its target and comes back.",
|
|
715
|
+
make: hubChainBoard
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
name: "case-many-groups",
|
|
719
|
+
defect: "On a level with many groups, links go around the outer edge of the board.",
|
|
720
|
+
make: manyGroupsBoard
|
|
721
|
+
}
|
|
722
|
+
];
|
|
549
723
|
// src/plugs.ts
|
|
550
724
|
/** The outward normal of each side, in a frame with the y axis down. */
|
|
551
725
|
const SIDE_NORMAL = {
|
|
@@ -725,20 +899,30 @@ var Obstacles = class {
|
|
|
725
899
|
}
|
|
726
900
|
/** This method tells if the segment `ab` stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
|
|
727
901
|
clear(a, b, pad, skip) {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
902
|
+
return this.hit(a, b, pad, skip) === void 0;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* This method gives a node whose box, made larger by `pad`, the segment `ab` goes into, or `undefined` when the segment is clear.
|
|
906
|
+
* The nodes in `skip` do not count. The test stops at the first such node.
|
|
907
|
+
*/
|
|
908
|
+
hit(a, b, pad, skip) {
|
|
909
|
+
let found;
|
|
910
|
+
this.#grid.someSegment(a, b, pad, (n) => {
|
|
911
|
+
if (skip?.has(n) || !hits(a, b, n, pad)) return false;
|
|
912
|
+
found = n;
|
|
913
|
+
return true;
|
|
734
914
|
});
|
|
735
|
-
return
|
|
915
|
+
return found;
|
|
736
916
|
}
|
|
737
917
|
/** This method tells if the polyline stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
|
|
738
918
|
clearPath(p, pad, skip) {
|
|
739
919
|
for (let i = 1; i < p.length; i++) if (!this.clear(p[i - 1], p[i], pad, skip)) return false;
|
|
740
920
|
return true;
|
|
741
921
|
}
|
|
922
|
+
/** This method gives each node whose bucket the box touches to `visit`. A node can be outside the box. */
|
|
923
|
+
query(box, visit) {
|
|
924
|
+
this.#grid.query(box, visit);
|
|
925
|
+
}
|
|
742
926
|
/** This method tells if the point is inside a box, made larger by `pad`. The node `skip` does not count. */
|
|
743
927
|
inside(p, pad, skip) {
|
|
744
928
|
let hit = false;
|
|
@@ -756,6 +940,11 @@ var Obstacles = class {
|
|
|
756
940
|
return hit;
|
|
757
941
|
}
|
|
758
942
|
};
|
|
943
|
+
/** This function tells if the segment `ab` goes into the box of the node, made larger by `pad`. */
|
|
944
|
+
function hits(a, b, n, pad) {
|
|
945
|
+
const x = n.box;
|
|
946
|
+
return segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad);
|
|
947
|
+
}
|
|
759
948
|
/**
|
|
760
949
|
* A sparse visibility graph. Its vertices are the corners of the boxes, made larger by `margin`.
|
|
761
950
|
* Each corner links to its `k` nearest corners that it can see. A lattice of free points links the groups of nodes.
|
|
@@ -776,9 +965,12 @@ var CornerGraph = class {
|
|
|
776
965
|
#open = /* @__PURE__ */ new Int32Array(0);
|
|
777
966
|
#closed = /* @__PURE__ */ new Int32Array(0);
|
|
778
967
|
#stamp = 0;
|
|
779
|
-
#
|
|
780
|
-
#
|
|
968
|
+
#buckets = /* @__PURE__ */ new Map();
|
|
969
|
+
#dist = /* @__PURE__ */ new Float64Array(0);
|
|
970
|
+
#near = new MinHeap();
|
|
781
971
|
#endCache = /* @__PURE__ */ new Map();
|
|
972
|
+
#fewCache = /* @__PURE__ */ new Map();
|
|
973
|
+
#part;
|
|
782
974
|
/** This constructor makes the graph of the nodes. */
|
|
783
975
|
constructor(nodes, obstacles, options) {
|
|
784
976
|
this.#obstacles = obstacles;
|
|
@@ -841,12 +1033,17 @@ var CornerGraph = class {
|
|
|
841
1033
|
});
|
|
842
1034
|
}
|
|
843
1035
|
#addPoint(c) {
|
|
844
|
-
this
|
|
1036
|
+
const j = this.points.length;
|
|
1037
|
+
this.#pointGrid.add(j, {
|
|
845
1038
|
x: c.x,
|
|
846
1039
|
y: c.y,
|
|
847
1040
|
w: 0,
|
|
848
1041
|
h: 0
|
|
849
1042
|
});
|
|
1043
|
+
const key = bucketKey(Math.floor(c.x / this.#cell), Math.floor(c.y / this.#cell));
|
|
1044
|
+
const list = this.#buckets.get(key);
|
|
1045
|
+
if (list === void 0) this.#buckets.set(key, [j]);
|
|
1046
|
+
else list.push(j);
|
|
850
1047
|
this.points.push(c);
|
|
851
1048
|
}
|
|
852
1049
|
/** This method gives the number of links of the graph. */
|
|
@@ -880,28 +1077,70 @@ var CornerGraph = class {
|
|
|
880
1077
|
nearestVisible(p, k, self = -1) {
|
|
881
1078
|
return this.#nearestVisible(p, k, self, this.#pad);
|
|
882
1079
|
}
|
|
1080
|
+
/**
|
|
1081
|
+
* This method tests the corners in rings of radius `r` = 1, 2, 4 and 8 cells. In each ring, it tests the corners
|
|
1082
|
+
* within `r` that an earlier ring did not test, nearest first, and equal distances by index.
|
|
1083
|
+
* The ring of `r` takes the corners of the buckets that the square of side `2r` around `p` touches.
|
|
1084
|
+
* A corner of these buckets beyond `r` waits for the next ring. So the method visits each bucket one time.
|
|
1085
|
+
*/
|
|
883
1086
|
#nearestVisible(p, k, self, pad) {
|
|
884
1087
|
const out = [];
|
|
885
|
-
if (this.#
|
|
886
|
-
const
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1088
|
+
if (this.#dist.length < this.points.length) this.#dist = new Float64Array(this.points.length);
|
|
1089
|
+
const dd = this.#dist;
|
|
1090
|
+
const points = this.points;
|
|
1091
|
+
const cell = this.#cell;
|
|
1092
|
+
const blockers = [];
|
|
1093
|
+
const now = this.#near;
|
|
1094
|
+
let wait = [];
|
|
1095
|
+
let pi0 = 1;
|
|
1096
|
+
let pi1 = 0;
|
|
1097
|
+
let pj0 = 1;
|
|
1098
|
+
let pj1 = 0;
|
|
1099
|
+
for (let r = cell; r <= cell * 8; r *= 2) {
|
|
1100
|
+
const x0 = p.x - r;
|
|
1101
|
+
const y0 = p.y - r;
|
|
1102
|
+
const i0 = Math.floor(x0 / cell);
|
|
1103
|
+
const j0 = Math.floor(y0 / cell);
|
|
1104
|
+
const i1 = Math.floor((x0 + 2 * r) / cell);
|
|
1105
|
+
const j1 = Math.floor((y0 + 2 * r) / cell);
|
|
1106
|
+
const later = [];
|
|
1107
|
+
now.clear();
|
|
1108
|
+
for (const j of wait) if (dd[j] > r) later.push(j);
|
|
1109
|
+
else now.add(dd[j], j);
|
|
1110
|
+
for (let bi = i0; bi <= i1; bi++) {
|
|
1111
|
+
const before = bi >= pi0 && bi <= pi1;
|
|
1112
|
+
for (let bj = j0; bj <= j1; bj++) {
|
|
1113
|
+
if (before && bj >= pj0 && bj <= pj1) continue;
|
|
1114
|
+
const list = this.#buckets.get(bucketKey(bi, bj));
|
|
1115
|
+
if (list === void 0) continue;
|
|
1116
|
+
for (const j of list) {
|
|
1117
|
+
if (j === self) continue;
|
|
1118
|
+
const d = dist(p, points[j]);
|
|
1119
|
+
dd[j] = d;
|
|
1120
|
+
if (d > r) later.push(j);
|
|
1121
|
+
else now.add(d, j);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
now.build();
|
|
1126
|
+
while (now.size > 0) {
|
|
1127
|
+
const j = now.pop();
|
|
1128
|
+
const q = points[j];
|
|
1129
|
+
let blocked = false;
|
|
1130
|
+
for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits(p, q, blockers[b], pad);
|
|
1131
|
+
if (blocked) continue;
|
|
1132
|
+
const hit = this.#obstacles.hit(p, q, pad);
|
|
1133
|
+
if (hit !== void 0) blockers.push(hit);
|
|
1134
|
+
else {
|
|
1135
|
+
out.push(j);
|
|
1136
|
+
if (out.length >= k) return out;
|
|
1137
|
+
}
|
|
904
1138
|
}
|
|
1139
|
+
wait = later;
|
|
1140
|
+
pi0 = i0;
|
|
1141
|
+
pi1 = i1;
|
|
1142
|
+
pj0 = j0;
|
|
1143
|
+
pj1 = j1;
|
|
905
1144
|
}
|
|
906
1145
|
return out;
|
|
907
1146
|
}
|
|
@@ -913,6 +1152,35 @@ var CornerGraph = class {
|
|
|
913
1152
|
*/
|
|
914
1153
|
path(from, to, cost, floor = 1) {
|
|
915
1154
|
if (this.#obstacles.clear(from, to, this.#pad)) return [from, to];
|
|
1155
|
+
return this.#search([{
|
|
1156
|
+
point: from,
|
|
1157
|
+
cost: 0
|
|
1158
|
+
}], [{
|
|
1159
|
+
point: to,
|
|
1160
|
+
cost: 0
|
|
1161
|
+
}], cost, floor, this.#k * 2)?.points;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* This method gives the shortest path from a point of `from` to a point of `to`, or `undefined` when the graph has none.
|
|
1165
|
+
* The cost of a path includes the costs of its two points. One search finds the path, so this method is faster than a call of `path` for each pair.
|
|
1166
|
+
*/
|
|
1167
|
+
pathAmong(from, to) {
|
|
1168
|
+
const found = this.#search(from, to, void 0, 1, this.#k);
|
|
1169
|
+
const lines = [];
|
|
1170
|
+
from.forEach((a, i) => to.forEach((b, k) => {
|
|
1171
|
+
const c = a.cost + dist(a.point, b.point) + b.cost;
|
|
1172
|
+
if (!found || c < found.cost) lines.push({
|
|
1173
|
+
points: [a.point, b.point],
|
|
1174
|
+
from: i,
|
|
1175
|
+
to: k,
|
|
1176
|
+
cost: c
|
|
1177
|
+
});
|
|
1178
|
+
}));
|
|
1179
|
+
lines.sort((u, v) => u.cost - v.cost);
|
|
1180
|
+
return lines.find((l) => this.#obstacles.clear(l.points[0], l.points[1], this.#pad)) ?? found;
|
|
1181
|
+
}
|
|
1182
|
+
/** This method is the search (A*) of `path` and `pathAmong`. */
|
|
1183
|
+
#search(from, to, cost, floor, count) {
|
|
916
1184
|
const n = this.points.length;
|
|
917
1185
|
if (this.#g.length < n) {
|
|
918
1186
|
this.#g = new Float64Array(n);
|
|
@@ -921,28 +1189,51 @@ var CornerGraph = class {
|
|
|
921
1189
|
this.#closed = new Int32Array(n);
|
|
922
1190
|
}
|
|
923
1191
|
const stamp = ++this.#stamp;
|
|
1192
|
+
const points = this.points;
|
|
924
1193
|
const goal = /* @__PURE__ */ new Map();
|
|
925
|
-
|
|
1194
|
+
const goalOf = /* @__PURE__ */ new Map();
|
|
1195
|
+
to.forEach((b, k) => {
|
|
1196
|
+
for (const j of this.#ends(b.point, count)) {
|
|
1197
|
+
const tail = dist(points[j], b.point) + b.cost;
|
|
1198
|
+
const now = goal.get(j);
|
|
1199
|
+
if (now === void 0 || tail < now) {
|
|
1200
|
+
goal.set(j, tail);
|
|
1201
|
+
goalOf.set(j, k);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
926
1205
|
if (goal.size === 0) return void 0;
|
|
1206
|
+
const starts = from.map((a) => this.#ends(a.point, count));
|
|
1207
|
+
if (!starts.some((list) => this.#joined(list, goal))) return void 0;
|
|
927
1208
|
const heap = new MinHeap();
|
|
928
|
-
const
|
|
1209
|
+
const G = this.#g;
|
|
1210
|
+
const parentOf = this.#parent;
|
|
1211
|
+
const open = this.#open;
|
|
1212
|
+
const closed = this.#closed;
|
|
1213
|
+
const estimate = (p) => {
|
|
1214
|
+
let h = Infinity;
|
|
1215
|
+
for (const b of to) h = Math.min(h, dist(p, b.point));
|
|
1216
|
+
return floor * h;
|
|
1217
|
+
};
|
|
929
1218
|
const relax = (j, g, parent) => {
|
|
930
|
-
if (
|
|
931
|
-
if (
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
heap.push(g +
|
|
1219
|
+
if (closed[j] === stamp) return;
|
|
1220
|
+
if (open[j] === stamp && G[j] <= g) return;
|
|
1221
|
+
open[j] = stamp;
|
|
1222
|
+
G[j] = g;
|
|
1223
|
+
parentOf[j] = parent;
|
|
1224
|
+
heap.push(g + estimate(points[j]), j);
|
|
936
1225
|
};
|
|
937
|
-
|
|
1226
|
+
from.forEach((a, i) => {
|
|
1227
|
+
for (const j of starts[i]) relax(j, a.cost + dist(a.point, points[j]), -1 - i);
|
|
1228
|
+
});
|
|
938
1229
|
let best = Infinity;
|
|
939
1230
|
let bestEnd = -1;
|
|
940
1231
|
while (heap.size > 0) {
|
|
941
1232
|
if (heap.topKey >= best) break;
|
|
942
1233
|
const i = heap.pop();
|
|
943
|
-
if (
|
|
944
|
-
|
|
945
|
-
const gi =
|
|
1234
|
+
if (closed[i] === stamp) continue;
|
|
1235
|
+
closed[i] = stamp;
|
|
1236
|
+
const gi = G[i];
|
|
946
1237
|
const tail = goal.get(i);
|
|
947
1238
|
if (tail !== void 0 && gi + tail < best) {
|
|
948
1239
|
best = gi + tail;
|
|
@@ -952,28 +1243,67 @@ var CornerGraph = class {
|
|
|
952
1243
|
const len = this.#len[i];
|
|
953
1244
|
for (let m = 0; m < adj.length; m++) {
|
|
954
1245
|
const j = adj[m];
|
|
1246
|
+
if (closed[j] === stamp) continue;
|
|
955
1247
|
const d = len[m];
|
|
956
1248
|
relax(j, gi + (cost ? cost(i, j, d) : d), i);
|
|
957
1249
|
}
|
|
958
1250
|
}
|
|
959
1251
|
if (bestEnd < 0) return void 0;
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1252
|
+
const k = goalOf.get(bestEnd);
|
|
1253
|
+
const out = [to[k].point];
|
|
1254
|
+
for (let i = bestEnd;; i = parentOf[i]) {
|
|
1255
|
+
out.push(points[i]);
|
|
1256
|
+
if (parentOf[i] < 0) {
|
|
1257
|
+
const f = -1 - parentOf[i];
|
|
1258
|
+
out.push(from[f].point);
|
|
1259
|
+
return {
|
|
1260
|
+
points: out.reverse(),
|
|
1261
|
+
from: f,
|
|
1262
|
+
to: k,
|
|
1263
|
+
cost: best
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/** This method tells if a corner of `starts` and a corner of `goal` are in one connected part of the graph. */
|
|
1269
|
+
#joined(starts, goal) {
|
|
1270
|
+
const part = this.#part ??= this.#parts();
|
|
1271
|
+
const goals = /* @__PURE__ */ new Set();
|
|
1272
|
+
for (const j of goal.keys()) goals.add(part[j]);
|
|
1273
|
+
for (const j of starts) if (goals.has(part[j])) return true;
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
/** This method gives the number of the connected part of each corner. */
|
|
1277
|
+
#parts() {
|
|
1278
|
+
const part = new Int32Array(this.points.length).fill(-1);
|
|
1279
|
+
const stack = [];
|
|
1280
|
+
let count = 0;
|
|
1281
|
+
for (let i = 0; i < part.length; i++) {
|
|
1282
|
+
if (part[i] !== -1) continue;
|
|
1283
|
+
part[i] = count;
|
|
1284
|
+
stack.push(i);
|
|
1285
|
+
while (stack.length > 0) for (const j of this.#adj[stack.pop()]) {
|
|
1286
|
+
if (part[j] !== -1) continue;
|
|
1287
|
+
part[j] = count;
|
|
1288
|
+
stack.push(j);
|
|
1289
|
+
}
|
|
1290
|
+
count++;
|
|
1291
|
+
}
|
|
1292
|
+
return part;
|
|
964
1293
|
}
|
|
965
1294
|
/**
|
|
966
|
-
* This method gives
|
|
1295
|
+
* This method gives at most `count` corners that an end of a path can see. An end near a box sees no corner with the clearance.
|
|
967
1296
|
* Then the end takes the corners that it sees without the clearance, so the path does not cut through the boxes.
|
|
968
1297
|
* The graph does not change, so the method keeps the corners of each end.
|
|
969
1298
|
*/
|
|
970
|
-
#ends(p) {
|
|
1299
|
+
#ends(p, count) {
|
|
1300
|
+
const cache = count === this.#k * 2 ? this.#endCache : this.#fewCache;
|
|
971
1301
|
const key = `${p.x},${p.y}`;
|
|
972
|
-
let out =
|
|
1302
|
+
let out = cache.get(key);
|
|
973
1303
|
if (!out) {
|
|
974
|
-
out = this.nearestVisible(p,
|
|
975
|
-
if (out.length === 0) out = this.#nearestVisible(p,
|
|
976
|
-
|
|
1304
|
+
out = this.nearestVisible(p, count);
|
|
1305
|
+
if (out.length === 0) out = this.#nearestVisible(p, count, -1, 0);
|
|
1306
|
+
cache.set(key, out);
|
|
977
1307
|
}
|
|
978
1308
|
return out;
|
|
979
1309
|
}
|
|
@@ -991,61 +1321,85 @@ var CornerGraph = class {
|
|
|
991
1321
|
return out;
|
|
992
1322
|
}
|
|
993
1323
|
};
|
|
994
|
-
/**
|
|
1324
|
+
/** This function packs the two indices of a bucket into one number. Each index stays within ±2^20. */
|
|
1325
|
+
const bucketKey = (i, j) => (i + 1048576) * 2097152 + (j + 1048576);
|
|
1326
|
+
/**
|
|
1327
|
+
* A binary min-heap of (key, value) pairs in typed arrays. The pairs come out by key, and equal keys by value, so the order is deterministic.
|
|
1328
|
+
* The method `push` adds a pair to the heap. The method `add` only appends a pair, and the method `build` then makes the heap of all the pairs.
|
|
1329
|
+
*/
|
|
995
1330
|
var MinHeap = class {
|
|
996
|
-
#
|
|
997
|
-
#
|
|
998
|
-
|
|
999
|
-
return this.#k.length;
|
|
1000
|
-
}
|
|
1001
|
-
#less(a, b) {
|
|
1002
|
-
return this.#k[a] < this.#k[b] || this.#k[a] === this.#k[b] && this.#v[a] < this.#v[b];
|
|
1003
|
-
}
|
|
1004
|
-
#swap(a, b) {
|
|
1005
|
-
const k = this.#k[a];
|
|
1006
|
-
this.#k[a] = this.#k[b];
|
|
1007
|
-
this.#k[b] = k;
|
|
1008
|
-
const v = this.#v[a];
|
|
1009
|
-
this.#v[a] = this.#v[b];
|
|
1010
|
-
this.#v[b] = v;
|
|
1011
|
-
}
|
|
1331
|
+
#d = /* @__PURE__ */ new Float64Array(64);
|
|
1332
|
+
#j = /* @__PURE__ */ new Int32Array(64);
|
|
1333
|
+
size = 0;
|
|
1012
1334
|
/** The smallest key. */
|
|
1013
1335
|
get topKey() {
|
|
1014
|
-
return this.#
|
|
1336
|
+
return this.#d[0];
|
|
1015
1337
|
}
|
|
1016
1338
|
push(key, value) {
|
|
1017
|
-
this
|
|
1018
|
-
this.#
|
|
1019
|
-
|
|
1339
|
+
this.add(key, value);
|
|
1340
|
+
const d = this.#d;
|
|
1341
|
+
const jj = this.#j;
|
|
1342
|
+
let i = this.size - 1;
|
|
1020
1343
|
while (i > 0) {
|
|
1021
1344
|
const p = i - 1 >> 1;
|
|
1022
|
-
if (!
|
|
1023
|
-
|
|
1345
|
+
if (!(key < d[p] || key === d[p] && value < jj[p])) break;
|
|
1346
|
+
d[i] = d[p];
|
|
1347
|
+
jj[i] = jj[p];
|
|
1024
1348
|
i = p;
|
|
1025
1349
|
}
|
|
1350
|
+
d[i] = key;
|
|
1351
|
+
jj[i] = value;
|
|
1352
|
+
}
|
|
1353
|
+
clear() {
|
|
1354
|
+
this.size = 0;
|
|
1355
|
+
}
|
|
1356
|
+
add(d, j) {
|
|
1357
|
+
if (this.size === this.#d.length) {
|
|
1358
|
+
const dd = new Float64Array(2 * this.size);
|
|
1359
|
+
dd.set(this.#d);
|
|
1360
|
+
this.#d = dd;
|
|
1361
|
+
const jj = new Int32Array(2 * this.size);
|
|
1362
|
+
jj.set(this.#j);
|
|
1363
|
+
this.#j = jj;
|
|
1364
|
+
}
|
|
1365
|
+
this.#d[this.size] = d;
|
|
1366
|
+
this.#j[this.size] = j;
|
|
1367
|
+
this.size++;
|
|
1368
|
+
}
|
|
1369
|
+
build() {
|
|
1370
|
+
for (let i = (this.size >> 1) - 1; i >= 0; i--) this.#down(i);
|
|
1026
1371
|
}
|
|
1027
1372
|
/** This method removes the pair with the smallest key and gives its value. */
|
|
1028
1373
|
pop() {
|
|
1029
|
-
const top = this.#
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
this.#
|
|
1034
|
-
this.#
|
|
1035
|
-
let i = 0;
|
|
1036
|
-
for (;;) {
|
|
1037
|
-
const l = 2 * i + 1;
|
|
1038
|
-
const r = l + 1;
|
|
1039
|
-
let m = i;
|
|
1040
|
-
if (l < this.#k.length && this.#less(l, m)) m = l;
|
|
1041
|
-
if (r < this.#k.length && this.#less(r, m)) m = r;
|
|
1042
|
-
if (m === i) break;
|
|
1043
|
-
this.#swap(i, m);
|
|
1044
|
-
i = m;
|
|
1045
|
-
}
|
|
1374
|
+
const top = this.#j[0];
|
|
1375
|
+
this.size--;
|
|
1376
|
+
if (this.size > 0) {
|
|
1377
|
+
this.#d[0] = this.#d[this.size];
|
|
1378
|
+
this.#j[0] = this.#j[this.size];
|
|
1379
|
+
this.#down(0);
|
|
1046
1380
|
}
|
|
1047
1381
|
return top;
|
|
1048
1382
|
}
|
|
1383
|
+
#down(i) {
|
|
1384
|
+
const d = this.#d;
|
|
1385
|
+
const jj = this.#j;
|
|
1386
|
+
const n = this.size;
|
|
1387
|
+
const di = d[i];
|
|
1388
|
+
const ji = jj[i];
|
|
1389
|
+
for (;;) {
|
|
1390
|
+
const l = 2 * i + 1;
|
|
1391
|
+
if (l >= n) break;
|
|
1392
|
+
const r = l + 1;
|
|
1393
|
+
let m = l;
|
|
1394
|
+
if (r < n && (d[r] < d[l] || d[r] === d[l] && jj[r] < jj[l])) m = r;
|
|
1395
|
+
if (!(d[m] < di || d[m] === di && jj[m] < ji)) break;
|
|
1396
|
+
d[i] = d[m];
|
|
1397
|
+
jj[i] = jj[m];
|
|
1398
|
+
i = m;
|
|
1399
|
+
}
|
|
1400
|
+
d[i] = di;
|
|
1401
|
+
jj[i] = ji;
|
|
1402
|
+
}
|
|
1049
1403
|
};
|
|
1050
1404
|
// src/ways.ts
|
|
1051
1405
|
/**
|
|
@@ -1511,7 +1865,296 @@ const plugsRouter = {
|
|
|
1511
1865
|
return { routes };
|
|
1512
1866
|
}
|
|
1513
1867
|
};
|
|
1868
|
+
// src/bench/properties.ts
|
|
1869
|
+
/** The tolerance of the hard rules, in units of the plane. */
|
|
1870
|
+
const TOLERANCE = .5;
|
|
1871
|
+
/**
|
|
1872
|
+
* This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
|
|
1873
|
+
* Fixed plugs stay in place. No route goes through a node that is not one of its ends. The counts of each level add up.
|
|
1874
|
+
* Each link of a level starts on the box of its source and ends on the box of its target.
|
|
1875
|
+
*
|
|
1876
|
+
* Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
|
|
1877
|
+
* Each run has two edges or more, and the route of each edge holds the points of the run in a row.
|
|
1878
|
+
* The function gives at most `limit` violations.
|
|
1879
|
+
*/
|
|
1880
|
+
function checkProperties(graph, result, limit = 100) {
|
|
1881
|
+
const out = [];
|
|
1882
|
+
const add = (v) => {
|
|
1883
|
+
out.push(v);
|
|
1884
|
+
return out.length >= limit;
|
|
1885
|
+
};
|
|
1886
|
+
const byId = nodeIndex(graph);
|
|
1887
|
+
const grid = new Grid(180);
|
|
1888
|
+
for (const n of graph.nodes) grid.add(n, n.box);
|
|
1889
|
+
const routes = /* @__PURE__ */ new Map();
|
|
1890
|
+
for (const r of result.routes) routes.set(r.edge, r);
|
|
1891
|
+
for (const e of graph.edges) {
|
|
1892
|
+
const r = routes.get(e.id);
|
|
1893
|
+
const s = byId.get(e.source.node);
|
|
1894
|
+
const t = byId.get(e.target.node);
|
|
1895
|
+
if (!s || !t) continue;
|
|
1896
|
+
if (!r || r.points.length < 2) {
|
|
1897
|
+
if (add({
|
|
1898
|
+
rule: "complete",
|
|
1899
|
+
edge: e.id,
|
|
1900
|
+
detail: "no route"
|
|
1901
|
+
})) return out;
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
const a = r.points[0];
|
|
1905
|
+
const b = r.points[r.points.length - 1];
|
|
1906
|
+
if (pointBoxDistance(a, s.box) > .5 || pointBoxDistance(b, t.box) > .5) {
|
|
1907
|
+
if (add({
|
|
1908
|
+
rule: "ends-at-plugs",
|
|
1909
|
+
edge: e.id,
|
|
1910
|
+
detail: "an end of the route is away from its node"
|
|
1911
|
+
})) return out;
|
|
1912
|
+
}
|
|
1913
|
+
const fs = fixedPortPoint(s, e.source);
|
|
1914
|
+
const ft = fixedPortPoint(t, e.target);
|
|
1915
|
+
if (fs && dist(fs, a) > .5 || ft && dist(ft, b) > .5) {
|
|
1916
|
+
if (add({
|
|
1917
|
+
rule: "fixed-plugs",
|
|
1918
|
+
edge: e.id,
|
|
1919
|
+
detail: "the route does not start or end at its fixed plug"
|
|
1920
|
+
})) return out;
|
|
1921
|
+
}
|
|
1922
|
+
for (let i = 1; i < r.points.length; i++) {
|
|
1923
|
+
const p = r.points[i - 1];
|
|
1924
|
+
const q = r.points[i];
|
|
1925
|
+
let hit;
|
|
1926
|
+
grid.querySegment(p, q, 0, (n) => {
|
|
1927
|
+
if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -.5))) hit = n;
|
|
1928
|
+
});
|
|
1929
|
+
if (hit) {
|
|
1930
|
+
if (add({
|
|
1931
|
+
rule: "no-pass-through",
|
|
1932
|
+
edge: e.id,
|
|
1933
|
+
detail: `segment ${i} goes through ${hit.id}`
|
|
1934
|
+
})) return out;
|
|
1935
|
+
break;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
const bundled = /* @__PURE__ */ new Set();
|
|
1940
|
+
for (const b of result.bundles ?? []) for (const id of b.edges) {
|
|
1941
|
+
const r = routes.get(id);
|
|
1942
|
+
const shares = r !== void 0 && r.points.length >= b.trunk.length && b.trunk.every((p, i) => dist(p, r.points[i]) <= .5);
|
|
1943
|
+
if (bundled.has(id) || !shares) {
|
|
1944
|
+
if (add({
|
|
1945
|
+
rule: "bundle-trunk",
|
|
1946
|
+
edge: id,
|
|
1947
|
+
detail: `bundle ${b.id}: the route is in two bundles or does not start with the trunk`
|
|
1948
|
+
})) return out;
|
|
1949
|
+
}
|
|
1950
|
+
bundled.add(id);
|
|
1951
|
+
}
|
|
1952
|
+
for (const run of result.runs ?? []) {
|
|
1953
|
+
if (run.edges.length < 2) {
|
|
1954
|
+
if (add({
|
|
1955
|
+
rule: "run-shared",
|
|
1956
|
+
detail: `run ${run.id} has fewer than two edges`
|
|
1957
|
+
})) return out;
|
|
1958
|
+
}
|
|
1959
|
+
for (const id of run.edges) {
|
|
1960
|
+
const r = routes.get(id);
|
|
1961
|
+
if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse()))) {
|
|
1962
|
+
if (add({
|
|
1963
|
+
rule: "run-shared",
|
|
1964
|
+
edge: id,
|
|
1965
|
+
detail: `run ${run.id}: the route does not hold the points of the run in a row`
|
|
1966
|
+
})) return out;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
for (const level of result.levels ?? []) {
|
|
1971
|
+
const itemOf = /* @__PURE__ */ new Map();
|
|
1972
|
+
for (const it of level.items) for (const n of it.nodes) itemOf.set(n, it.id);
|
|
1973
|
+
if (itemOf.size !== graph.nodes.length) {
|
|
1974
|
+
if (add({
|
|
1975
|
+
rule: "counts-sum",
|
|
1976
|
+
detail: `level ${level.depth}: the items hold ${itemOf.size} of ${graph.nodes.length} nodes`
|
|
1977
|
+
})) return out;
|
|
1978
|
+
}
|
|
1979
|
+
let between = 0;
|
|
1980
|
+
for (const e of graph.edges) if (itemOf.get(e.source.node) !== itemOf.get(e.target.node)) between++;
|
|
1981
|
+
const sum = level.links.reduce((s, l) => s + l.count, 0);
|
|
1982
|
+
if (sum !== between) {
|
|
1983
|
+
if (add({
|
|
1984
|
+
rule: "counts-sum",
|
|
1985
|
+
detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
|
|
1986
|
+
})) return out;
|
|
1987
|
+
}
|
|
1988
|
+
const boxOf = new Map(level.items.map((it) => [it.id, it.box]));
|
|
1989
|
+
for (const l of level.links) {
|
|
1990
|
+
const s = boxOf.get(l.source);
|
|
1991
|
+
const t = boxOf.get(l.target);
|
|
1992
|
+
const p = l.points;
|
|
1993
|
+
if (!s || !t || p.length < 2 || !onBoundary(p[0], s) || !onBoundary(p[p.length - 1], t)) {
|
|
1994
|
+
if (add({
|
|
1995
|
+
rule: "link-ends",
|
|
1996
|
+
detail: `level ${level.depth}: the link ${l.source} to ${l.target} does not start and end on the boundaries of its items`
|
|
1997
|
+
})) return out;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
return out;
|
|
2002
|
+
}
|
|
2003
|
+
/** This function tells if the point is on the boundary of the box, within `TOLERANCE`. */
|
|
2004
|
+
function onBoundary(p, b) {
|
|
2005
|
+
const inner = inflate(b, -.5);
|
|
2006
|
+
return !(p.x > inner.x && p.x < inner.x + inner.w && p.y > inner.y && p.y < inner.y + inner.h) && pointBoxDistance(p, b) <= .5;
|
|
2007
|
+
}
|
|
2008
|
+
/** This function tells if the polyline `p` holds the points of `part` in a row. */
|
|
2009
|
+
function holds(p, part) {
|
|
2010
|
+
if (part.length === 0) return true;
|
|
2011
|
+
for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
|
|
2012
|
+
return false;
|
|
2013
|
+
}
|
|
2014
|
+
// src/bench/links.ts
|
|
2015
|
+
/** A link goes out and back when its distance to its target grows by more than this share of the distance between its ends. */
|
|
2016
|
+
const BACKTRACK_SHARE = .25;
|
|
2017
|
+
/** A link goes out and back only when its distance to its target grows by more than this number of units. */
|
|
2018
|
+
const BACKTRACK_MIN = 40;
|
|
2019
|
+
/** The default `slack` of `LinkFaultOptions`. */
|
|
2020
|
+
const OUTSIDE_SLACK = 24;
|
|
2021
|
+
/**
|
|
2022
|
+
* This function gives the largest growth of the distance to the last point of a polyline.
|
|
2023
|
+
* The growth at a vertex is its distance to the last point, less the nearest approach of the polyline before that vertex.
|
|
2024
|
+
*/
|
|
2025
|
+
function backtrackOf(p) {
|
|
2026
|
+
if (p.length < 3) return 0;
|
|
2027
|
+
const end = p[p.length - 1];
|
|
2028
|
+
let near = dist(p[0], end);
|
|
2029
|
+
let worst = 0;
|
|
2030
|
+
for (let i = 1; i < p.length; i++) {
|
|
2031
|
+
worst = Math.max(worst, dist(p[i], end) - near);
|
|
2032
|
+
near = Math.min(near, pointSegmentDistance(end, p[i - 1], p[i]));
|
|
2033
|
+
}
|
|
2034
|
+
return worst;
|
|
2035
|
+
}
|
|
2036
|
+
/** This function tells if the polyline `p` goes out and back: `backtrackOf(p)` is more than `BACKTRACK_MIN` and more than `BACKTRACK_SHARE` of the span. */
|
|
2037
|
+
function goesOutAndBack(p) {
|
|
2038
|
+
if (p.length < 3) return false;
|
|
2039
|
+
const span = dist(p[0], p[p.length - 1]);
|
|
2040
|
+
return backtrackOf(p) > Math.max(40, BACKTRACK_SHARE * span);
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* This function measures the faults of links between boxes. The map `boxes` gives the box of each end.
|
|
2044
|
+
* The bounding box of `outside` is the bounding box of all the boxes of the map.
|
|
2045
|
+
*/
|
|
2046
|
+
function measureLinks(boxes, links, options = {}) {
|
|
2047
|
+
const slack = options.slack ?? 24;
|
|
2048
|
+
let size = 0;
|
|
2049
|
+
let x0 = Infinity;
|
|
2050
|
+
let y0 = Infinity;
|
|
2051
|
+
let x1 = -Infinity;
|
|
2052
|
+
let y1 = -Infinity;
|
|
2053
|
+
for (const b of boxes.values()) {
|
|
2054
|
+
size += Math.max(b.w, b.h);
|
|
2055
|
+
x0 = Math.min(x0, b.x);
|
|
2056
|
+
y0 = Math.min(y0, b.y);
|
|
2057
|
+
x1 = Math.max(x1, b.x + b.w);
|
|
2058
|
+
y1 = Math.max(y1, b.y + b.h);
|
|
2059
|
+
}
|
|
2060
|
+
const grid = new Grid(Math.max(180, 2 * size / Math.max(1, boxes.size)));
|
|
2061
|
+
for (const [id, b] of boxes) grid.add(id, b);
|
|
2062
|
+
let through = 0;
|
|
2063
|
+
let backtrack = 0;
|
|
2064
|
+
let worstDetour = 0;
|
|
2065
|
+
let outside = 0;
|
|
2066
|
+
for (const l of links) {
|
|
2067
|
+
const p = l.points;
|
|
2068
|
+
if (p.length === 0) continue;
|
|
2069
|
+
let hit = false;
|
|
2070
|
+
for (let i = 1; i < p.length && !hit; i++) {
|
|
2071
|
+
const a = p[i - 1];
|
|
2072
|
+
const b = p[i];
|
|
2073
|
+
grid.querySegment(a, b, 0, (id) => {
|
|
2074
|
+
if (!hit && id !== l.source && id !== l.target && segmentHitsBox(a, b, inflate(boxes.get(id), -.5), .5)) hit = true;
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
if (hit) through++;
|
|
2078
|
+
if (goesOutAndBack(p)) backtrack++;
|
|
2079
|
+
const span = dist(p[0], p[p.length - 1]);
|
|
2080
|
+
if (span > 1) worstDetour = Math.max(worstDetour, polylineLength(p) / span);
|
|
2081
|
+
if (p.some((q) => q.x < x0 - slack || q.x > x1 + slack || q.y < y0 - slack || q.y > y1 + slack)) outside++;
|
|
2082
|
+
}
|
|
2083
|
+
return {
|
|
2084
|
+
links: links.length,
|
|
2085
|
+
through,
|
|
2086
|
+
backtrack,
|
|
2087
|
+
worstDetour,
|
|
2088
|
+
outside
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
/** This function measures the faults of the routes of a result. The ends of a route are the nodes of its edge. */
|
|
2092
|
+
function measureRouteFaults(graph, result, options = {}) {
|
|
2093
|
+
const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
|
|
2094
|
+
const edges = new Map(graph.edges.map((e) => [e.id, e]));
|
|
2095
|
+
const links = [];
|
|
2096
|
+
for (const r of result.routes) {
|
|
2097
|
+
const e = edges.get(r.edge);
|
|
2098
|
+
if (e) links.push({
|
|
2099
|
+
source: e.source.node,
|
|
2100
|
+
target: e.target.node,
|
|
2101
|
+
points: r.points
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
return measureLinks(boxes, links, options);
|
|
2105
|
+
}
|
|
2106
|
+
/** This function counts the pairs of boxes that overlap by more than `TOLERANCE` on each axis. */
|
|
2107
|
+
function countOverlaps(boxes) {
|
|
2108
|
+
let size = 0;
|
|
2109
|
+
for (const b of boxes) size += Math.max(b.w, b.h);
|
|
2110
|
+
const grid = new Grid(Math.max(180, 2 * size / Math.max(1, boxes.length)));
|
|
2111
|
+
boxes.forEach((b, i) => grid.add(i, b));
|
|
2112
|
+
let n = 0;
|
|
2113
|
+
boxes.forEach((a, i) => {
|
|
2114
|
+
grid.query(a, (j) => {
|
|
2115
|
+
const b = boxes[j];
|
|
2116
|
+
if (j > i && Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > .5 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > .5) n++;
|
|
2117
|
+
});
|
|
2118
|
+
});
|
|
2119
|
+
return n;
|
|
2120
|
+
}
|
|
2121
|
+
/** This function measures the faults of the links of one zoom level. The items of the level are the boxes. */
|
|
2122
|
+
function measureLevel(level, options = {}) {
|
|
2123
|
+
const boxes = new Map(level.items.map((i) => [i.id, i.box]));
|
|
2124
|
+
return {
|
|
2125
|
+
depth: level.depth,
|
|
2126
|
+
items: level.items.length,
|
|
2127
|
+
groups: level.items.filter((i) => i.nodes.length > 1).length,
|
|
2128
|
+
overlaps: countOverlaps(level.items.map((i) => i.box)),
|
|
2129
|
+
...measureLinks(boxes, level.links, options)
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* This function measures the faults of each zoom level of a result that has groups: the levels of the far view.
|
|
2134
|
+
* A level of single nodes only is the near view. Its links are routes, and `measureRouteFaults` measures them.
|
|
2135
|
+
*/
|
|
2136
|
+
function measureLevelFaults(result, options = {}) {
|
|
2137
|
+
return (result.levels ?? []).filter((l) => l.items.some((i) => i.nodes.length > 1)).map((l) => measureLevel(l, options));
|
|
2138
|
+
}
|
|
2139
|
+
/** This function adds the faults of several levels: it sums the counts and keeps the largest `worstDetour`. */
|
|
2140
|
+
function sumFaults(list) {
|
|
2141
|
+
return list.reduce((s, f) => ({
|
|
2142
|
+
links: s.links + f.links,
|
|
2143
|
+
through: s.through + f.through,
|
|
2144
|
+
backtrack: s.backtrack + f.backtrack,
|
|
2145
|
+
worstDetour: Math.max(s.worstDetour, f.worstDetour),
|
|
2146
|
+
outside: s.outside + f.outside
|
|
2147
|
+
}), {
|
|
2148
|
+
links: 0,
|
|
2149
|
+
through: 0,
|
|
2150
|
+
backtrack: 0,
|
|
2151
|
+
worstDetour: 0,
|
|
2152
|
+
outside: 0
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
1514
2155
|
// src/levels.ts
|
|
2156
|
+
/** The margin of the second corner graph. A gap between two boxes wider than approximately 1.5 times this margin lets a link through. */
|
|
2157
|
+
const TIGHT_MARGIN = 1;
|
|
1515
2158
|
const EPS = 1e-6;
|
|
1516
2159
|
/**
|
|
1517
2160
|
* This function removes the repeated points, the points in the middle of a straight run,
|
|
@@ -1554,7 +2197,7 @@ function levelLinks(items, pairs, options = {}) {
|
|
|
1554
2197
|
if (!s || !t) continue;
|
|
1555
2198
|
const [fs, ft] = facingPlugs(s.box, t.box, inset);
|
|
1556
2199
|
let points = isFree(ways, s, fs) && isFree(ways, t, ft) ? quickWay(ways, ends(ways, s, fs, t, ft)) : void 0;
|
|
1557
|
-
points ??=
|
|
2200
|
+
points ??= sideWay(ways, s, t, inset);
|
|
1558
2201
|
out.push({
|
|
1559
2202
|
source: p.source,
|
|
1560
2203
|
target: p.target,
|
|
@@ -1565,25 +2208,49 @@ function levelLinks(items, pairs, options = {}) {
|
|
|
1565
2208
|
return out;
|
|
1566
2209
|
}
|
|
1567
2210
|
/**
|
|
2211
|
+
* This function gives a way from plug to plug between the plugs at the middle of a side of the two nodes.
|
|
2212
|
+
* Without a way in the corner graph, the link goes straight between the ends, with legs parallel to the axes where they are clear.
|
|
2213
|
+
* When that line goes through another box, the link goes between the free plugs of the two nodes (`freeWay`) instead.
|
|
2214
|
+
*/
|
|
2215
|
+
function sideWay(ways, s, t, inset) {
|
|
2216
|
+
const e = ends(ways, s, sidePlug(ways, s, center(t.box)), t, sidePlug(ways, t, center(s.box)));
|
|
2217
|
+
const quick = quickWay(ways, e);
|
|
2218
|
+
if (quick) return quick;
|
|
2219
|
+
const path = ways.corners.path(e.a, e.b);
|
|
2220
|
+
const way = pulled(ways, e.ps, path ?? [e.a, e.b], e.pt);
|
|
2221
|
+
if (path || ways.obstacles.clearPath(way, 0, /* @__PURE__ */ new Set([s, t]))) return way;
|
|
2222
|
+
return freeWay(ways, s, t, inset) ?? way;
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
1568
2225
|
* This function makes the obstacles and the corner graph of the items of a level.
|
|
1569
2226
|
* The buckets of the obstacles grow with the items, so a long link visits few buckets.
|
|
1570
2227
|
*/
|
|
1571
2228
|
function levelWays(nodes, options) {
|
|
1572
2229
|
const margin = options.margin ?? 5;
|
|
2230
|
+
const k = options.k ?? 8;
|
|
1573
2231
|
let size = 0;
|
|
1574
2232
|
for (const n of nodes) size += Math.max(n.box.w, n.box.h);
|
|
1575
2233
|
const obstacles = new Obstacles(nodes, Math.max(180, 2 * size / Math.max(1, nodes.length)));
|
|
2234
|
+
const corners = new CornerGraph(nodes, obstacles, {
|
|
2235
|
+
margin,
|
|
2236
|
+
k
|
|
2237
|
+
});
|
|
2238
|
+
const out = (p) => ({
|
|
2239
|
+
x: p.point.x + p.normal.x * margin,
|
|
2240
|
+
y: p.point.y + p.normal.y * margin
|
|
2241
|
+
});
|
|
2242
|
+
let tight;
|
|
2243
|
+
const tightGraph = () => tight ??= new CornerGraph(nodes, obstacles, {
|
|
2244
|
+
margin: Math.min(margin, TIGHT_MARGIN),
|
|
2245
|
+
k
|
|
2246
|
+
});
|
|
1576
2247
|
return {
|
|
1577
2248
|
obstacles,
|
|
1578
|
-
corners
|
|
1579
|
-
margin,
|
|
1580
|
-
k: options.k ?? 8
|
|
1581
|
-
}),
|
|
2249
|
+
corners,
|
|
1582
2250
|
margin,
|
|
1583
|
-
out
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
})
|
|
2251
|
+
out,
|
|
2252
|
+
tight: tightGraph,
|
|
2253
|
+
free: /* @__PURE__ */ new Map()
|
|
1587
2254
|
};
|
|
1588
2255
|
}
|
|
1589
2256
|
/**
|
|
@@ -1657,13 +2324,139 @@ function sidePlug(ways, node, toward) {
|
|
|
1657
2324
|
return plug(order[0]);
|
|
1658
2325
|
}
|
|
1659
2326
|
/**
|
|
2327
|
+
* This function gives the free plugs of a node: on each side, the free point nearest to the middle of the side.
|
|
2328
|
+
* A point is free when it and the point at `margin` from it are in no other box. The function keeps the plugs of each node.
|
|
2329
|
+
*/
|
|
2330
|
+
function freePlugs(ways, node, inset) {
|
|
2331
|
+
const known = ways.free.get(node);
|
|
2332
|
+
if (known) return known;
|
|
2333
|
+
const b = node.box;
|
|
2334
|
+
const out = [];
|
|
2335
|
+
for (const side of SIDES) {
|
|
2336
|
+
const n = SIDE_NORMAL[side];
|
|
2337
|
+
const across = n.x === 0;
|
|
2338
|
+
const lo = across ? b.x : b.y;
|
|
2339
|
+
const hi = across ? b.x + b.w : b.y + b.h;
|
|
2340
|
+
const at = across ? n.y > 0 ? b.y + b.h : b.y : n.x > 0 ? b.x + b.w : b.x;
|
|
2341
|
+
const far = at + (across ? n.y : n.x) * ways.margin;
|
|
2342
|
+
const covered = [];
|
|
2343
|
+
const strip = across ? {
|
|
2344
|
+
x: lo,
|
|
2345
|
+
y: Math.min(at, far),
|
|
2346
|
+
w: hi - lo,
|
|
2347
|
+
h: Math.abs(far - at)
|
|
2348
|
+
} : {
|
|
2349
|
+
x: Math.min(at, far),
|
|
2350
|
+
y: lo,
|
|
2351
|
+
w: Math.abs(far - at),
|
|
2352
|
+
h: hi - lo
|
|
2353
|
+
};
|
|
2354
|
+
ways.obstacles.query(strip, (m) => {
|
|
2355
|
+
if (m === node) return;
|
|
2356
|
+
const o = m.box;
|
|
2357
|
+
const [o0, o1, p0, p1] = across ? [
|
|
2358
|
+
o.x,
|
|
2359
|
+
o.x + o.w,
|
|
2360
|
+
o.y,
|
|
2361
|
+
o.y + o.h
|
|
2362
|
+
] : [
|
|
2363
|
+
o.y,
|
|
2364
|
+
o.y + o.h,
|
|
2365
|
+
o.x,
|
|
2366
|
+
o.x + o.w
|
|
2367
|
+
];
|
|
2368
|
+
if (at > p0 && at < p1 || far > p0 && far < p1) covered.push([o0, o1]);
|
|
2369
|
+
});
|
|
2370
|
+
const mid = (lo + hi) / 2;
|
|
2371
|
+
let best;
|
|
2372
|
+
for (const [f0, f1] of freeSpans(lo, hi, covered)) {
|
|
2373
|
+
const room = Math.min(inset, (f1 - f0) / 2);
|
|
2374
|
+
const u = Math.max(f0 + room, Math.min(f1 - room, mid));
|
|
2375
|
+
if (best === void 0 || Math.abs(u - mid) < Math.abs(best - mid)) best = u;
|
|
2376
|
+
}
|
|
2377
|
+
if (best !== void 0) out.push({
|
|
2378
|
+
point: across ? {
|
|
2379
|
+
x: best,
|
|
2380
|
+
y: at
|
|
2381
|
+
} : {
|
|
2382
|
+
x: at,
|
|
2383
|
+
y: best
|
|
2384
|
+
},
|
|
2385
|
+
normal: n
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
const free = {
|
|
2389
|
+
plugs: out,
|
|
2390
|
+
reach: out.map((p) => reach(ways, node, p))
|
|
2391
|
+
};
|
|
2392
|
+
ways.free.set(node, free);
|
|
2393
|
+
return free;
|
|
2394
|
+
}
|
|
2395
|
+
/** This function gives the indices of the plugs, and the plugs that face `toward` most come first. */
|
|
2396
|
+
function facingOrder(plugs, from, toward) {
|
|
2397
|
+
const facing = (p) => p.normal.x * (toward.x - from.x) + p.normal.y * (toward.y - from.y);
|
|
2398
|
+
return plugs.map((_, i) => i).sort((u, v) => facing(plugs[v]) - facing(plugs[u]) || u - v);
|
|
2399
|
+
}
|
|
2400
|
+
/** This function gives the parts of the span from `lo` to `hi` outside each open interval of `covered`. A part of zero length does not count. */
|
|
2401
|
+
function freeSpans(lo, hi, covered) {
|
|
2402
|
+
covered.sort((u, v) => u[0] - v[0]);
|
|
2403
|
+
const out = [];
|
|
2404
|
+
let x = lo;
|
|
2405
|
+
for (const [c0, c1] of covered) {
|
|
2406
|
+
if (c0 > x) out.push([x, Math.min(c0, hi)]);
|
|
2407
|
+
x = Math.max(x, c1);
|
|
2408
|
+
if (x >= hi) break;
|
|
2409
|
+
}
|
|
2410
|
+
if (x < hi) out.push([x, hi]);
|
|
2411
|
+
return out.filter(([a, b]) => b - a > EPS);
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* This function gives the shortest way between the free plugs of the two nodes, or `undefined` when the second corner graph has none.
|
|
2415
|
+
* A link uses it when its plugs at the middle of a side find no way. Other boxes can cover such a plug, or close the space in front of it.
|
|
2416
|
+
*/
|
|
2417
|
+
function freeWay(ways, s, t, inset) {
|
|
2418
|
+
const fs = freePlugs(ways, s, inset);
|
|
2419
|
+
const ft = freePlugs(ways, t, inset);
|
|
2420
|
+
const cs = center(s.box);
|
|
2421
|
+
const ct = center(t.box);
|
|
2422
|
+
const is = facingOrder(fs.plugs, cs, ct);
|
|
2423
|
+
const it = facingOrder(ft.plugs, ct, cs);
|
|
2424
|
+
const from = is.map((i) => fs.plugs[i]);
|
|
2425
|
+
const to = it.map((k) => ft.plugs[k]);
|
|
2426
|
+
const ra = is.map((i) => fs.reach[i]);
|
|
2427
|
+
const rb = it.map((k) => ft.reach[k]);
|
|
2428
|
+
let best;
|
|
2429
|
+
let bestLength = Infinity;
|
|
2430
|
+
from.forEach((ps, i) => to.forEach((pt, k) => {
|
|
2431
|
+
const way = quickWay(ways, endsAt(ps, ra[i], pt, rb[k]));
|
|
2432
|
+
const length = way ? polylineLength(way) : Infinity;
|
|
2433
|
+
if (length < bestLength) {
|
|
2434
|
+
best = way;
|
|
2435
|
+
bestLength = length;
|
|
2436
|
+
}
|
|
2437
|
+
}));
|
|
2438
|
+
if (best) return best;
|
|
2439
|
+
const a = from.map((ps, i) => ({
|
|
2440
|
+
point: along(ps, ra[i]),
|
|
2441
|
+
cost: ra[i]
|
|
2442
|
+
}));
|
|
2443
|
+
const b = to.map((pt, k) => ({
|
|
2444
|
+
point: along(pt, rb[k]),
|
|
2445
|
+
cost: rb[k]
|
|
2446
|
+
}));
|
|
2447
|
+
const found = ways.tight().pathAmong(a, b);
|
|
2448
|
+
return found && pulled(ways, from[found.from], found.points, to[found.to]);
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
1660
2451
|
* This function gives the ends of a link. The link leaves each plug along its normal for the margin.
|
|
1661
2452
|
* A box can be nearer than twice the margin in front of a plug. Then the link turns in the middle of the space before that box.
|
|
1662
2453
|
* Two plugs that face each other across a narrow space make the link turn in the middle of that space.
|
|
1663
2454
|
*/
|
|
1664
2455
|
function ends(ways, s, ps, t, pt) {
|
|
1665
|
-
|
|
1666
|
-
|
|
2456
|
+
return endsAt(ps, reach(ways, s, ps), pt, reach(ways, t, pt));
|
|
2457
|
+
}
|
|
2458
|
+
/** This function gives the ends of a link from the plug `ps` to the plug `pt`, with the reach of each plug. */
|
|
2459
|
+
function endsAt(ps, ra, pt, rb) {
|
|
1667
2460
|
if (ps.normal.x === -pt.normal.x && ps.normal.y === -pt.normal.y) {
|
|
1668
2461
|
const d = (pt.point.x - ps.point.x) * ps.normal.x + (pt.point.y - ps.point.y) * ps.normal.y;
|
|
1669
2462
|
if (d > 0 && ra + rb > d) ra = rb = d / 2;
|
|
@@ -1709,17 +2502,15 @@ function quickWay(ways, e) {
|
|
|
1709
2502
|
]);
|
|
1710
2503
|
}
|
|
1711
2504
|
/**
|
|
1712
|
-
* This function gives a way from plug
|
|
1713
|
-
* Then it pulls the corners tight with legs that are parallel to the axes, where the obstacles let it.
|
|
2505
|
+
* This function gives a way from the plug `ps` through the points of `path` to the plug `pt`.
|
|
2506
|
+
* The way leaves and enters each plug along its normal. Then it pulls the corners tight with legs that are parallel to the axes, where the obstacles let it.
|
|
1714
2507
|
*/
|
|
1715
|
-
function
|
|
1716
|
-
const
|
|
1717
|
-
if (quick) return quick;
|
|
1718
|
-
const legs = squarePull(ways.corners.path(e.a, e.b) ?? [e.a, e.b], ways.obstacles, ways.margin / 2, axisOf(e.ps.normal), axisOf(e.pt.normal));
|
|
2508
|
+
function pulled(ways, ps, path, pt) {
|
|
2509
|
+
const legs = squarePull(path, ways.obstacles, ways.margin / 2, axisOf(ps.normal), axisOf(pt.normal));
|
|
1719
2510
|
return tidy([
|
|
1720
|
-
|
|
2511
|
+
ps.point,
|
|
1721
2512
|
...legs,
|
|
1722
|
-
|
|
2513
|
+
pt.point
|
|
1723
2514
|
]);
|
|
1724
2515
|
}
|
|
1725
2516
|
const axisOf = (n) => Math.abs(n.x) > Math.abs(n.y) ? "x" : "y";
|
|
@@ -1779,11 +2570,18 @@ function square(a, b, obstacles, pad, from, to) {
|
|
|
1779
2570
|
// src/routers/sections.ts
|
|
1780
2571
|
const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
|
|
1781
2572
|
/**
|
|
2573
|
+
* A hub can be this many units farther from the target than the nearest approach of the way before it.
|
|
2574
|
+
* A hub farther away goes out and back, so the way skips it.
|
|
2575
|
+
*/
|
|
2576
|
+
const HUB_SLACK = 80;
|
|
2577
|
+
/**
|
|
1782
2578
|
* 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.
|
|
1783
2579
|
* An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
|
|
1784
2580
|
* Each way from hub to hub is found one time, so all the edges on it share its points.
|
|
1785
2581
|
* The edges in one section of the lowest level go straight around the nodes.
|
|
1786
2582
|
*
|
|
2583
|
+
* An edge skips a hub that takes it away from its target. A way that still goes out and back gives way to the direct way.
|
|
2584
|
+
*
|
|
1787
2585
|
* The result gives one zoom level for each depth of the tree.
|
|
1788
2586
|
* A link of a level goes from the boundary of one item to the boundary of the other, around the other items.
|
|
1789
2587
|
*/
|
|
@@ -1824,6 +2622,19 @@ function sectionsRouter(options) {
|
|
|
1824
2622
|
}
|
|
1825
2623
|
return h;
|
|
1826
2624
|
};
|
|
2625
|
+
const forward = (list, from, to) => {
|
|
2626
|
+
const kept = [];
|
|
2627
|
+
let near = dist(from, to);
|
|
2628
|
+
let last = from;
|
|
2629
|
+
for (const c of list) {
|
|
2630
|
+
const h = hub(c);
|
|
2631
|
+
if (dist(h, to) > near + HUB_SLACK) continue;
|
|
2632
|
+
near = Math.min(near, pointSegmentDistance(to, last, h));
|
|
2633
|
+
last = h;
|
|
2634
|
+
kept.push(c);
|
|
2635
|
+
}
|
|
2636
|
+
return kept;
|
|
2637
|
+
};
|
|
1827
2638
|
const up = (s) => {
|
|
1828
2639
|
const out = [];
|
|
1829
2640
|
for (let x = s; x; x = parent.get(x)) out.push(x);
|
|
@@ -1869,19 +2680,26 @@ function sectionsRouter(options) {
|
|
|
1869
2680
|
const lca = a.find((x) => common.has(x));
|
|
1870
2681
|
const keep = options.hubs ?? Infinity;
|
|
1871
2682
|
const side = (list) => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
|
|
1872
|
-
const
|
|
2683
|
+
const end = ways.out(pt);
|
|
2684
|
+
const start = ways.out(ps);
|
|
2685
|
+
const chain = forward([...side(a), ...side(b).reverse()], start, pt.point);
|
|
2686
|
+
if (chain.length === 0) {
|
|
2687
|
+
routes.push({
|
|
2688
|
+
edge: e.id,
|
|
2689
|
+
points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
|
|
2690
|
+
});
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
1873
2693
|
const out = [ps.point];
|
|
1874
2694
|
const append = (p) => {
|
|
1875
2695
|
for (const q of p) if (dist(q, out[out.length - 1]) > 1e-9) out.push(q);
|
|
1876
2696
|
};
|
|
1877
2697
|
const used = [];
|
|
1878
|
-
const start = ways.out(ps);
|
|
1879
2698
|
append(piece(at(start), start, chain[0].id, hub(chain[0]), used));
|
|
1880
2699
|
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));
|
|
1881
|
-
const end = ways.out(pt);
|
|
1882
2700
|
append(piece(chain[chain.length - 1].id, hub(chain[chain.length - 1]), at(end), end, used));
|
|
1883
2701
|
append([pt.point]);
|
|
1884
|
-
if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin) {
|
|
2702
|
+
if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin || goesOutAndBack(out)) {
|
|
1885
2703
|
routes.push({
|
|
1886
2704
|
edge: e.id,
|
|
1887
2705
|
points: ways.between(ps, pt, /* @__PURE__ */ new Set([s, t]))
|
|
@@ -2446,152 +3264,6 @@ const ROUTERS = [
|
|
|
2446
3264
|
maxNodes: 1e5
|
|
2447
3265
|
})
|
|
2448
3266
|
];
|
|
2449
|
-
// src/bench/properties.ts
|
|
2450
|
-
/** The tolerance of the hard rules, in units of the plane. */
|
|
2451
|
-
const TOLERANCE = .5;
|
|
2452
|
-
/**
|
|
2453
|
-
* This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
|
|
2454
|
-
* Fixed plugs stay in place. No route goes through a node that is not one of its ends. The counts of each level add up.
|
|
2455
|
-
* Each link of a level starts on the box of its source and ends on the box of its target.
|
|
2456
|
-
*
|
|
2457
|
-
* Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
|
|
2458
|
-
* Each run has two edges or more, and the route of each edge holds the points of the run in a row.
|
|
2459
|
-
* The function gives at most `limit` violations.
|
|
2460
|
-
*/
|
|
2461
|
-
function checkProperties(graph, result, limit = 100) {
|
|
2462
|
-
const out = [];
|
|
2463
|
-
const add = (v) => {
|
|
2464
|
-
out.push(v);
|
|
2465
|
-
return out.length >= limit;
|
|
2466
|
-
};
|
|
2467
|
-
const byId = nodeIndex(graph);
|
|
2468
|
-
const grid = new Grid(180);
|
|
2469
|
-
for (const n of graph.nodes) grid.add(n, n.box);
|
|
2470
|
-
const routes = /* @__PURE__ */ new Map();
|
|
2471
|
-
for (const r of result.routes) routes.set(r.edge, r);
|
|
2472
|
-
for (const e of graph.edges) {
|
|
2473
|
-
const r = routes.get(e.id);
|
|
2474
|
-
const s = byId.get(e.source.node);
|
|
2475
|
-
const t = byId.get(e.target.node);
|
|
2476
|
-
if (!s || !t) continue;
|
|
2477
|
-
if (!r || r.points.length < 2) {
|
|
2478
|
-
if (add({
|
|
2479
|
-
rule: "complete",
|
|
2480
|
-
edge: e.id,
|
|
2481
|
-
detail: "no route"
|
|
2482
|
-
})) return out;
|
|
2483
|
-
continue;
|
|
2484
|
-
}
|
|
2485
|
-
const a = r.points[0];
|
|
2486
|
-
const b = r.points[r.points.length - 1];
|
|
2487
|
-
if (pointBoxDistance(a, s.box) > .5 || pointBoxDistance(b, t.box) > .5) {
|
|
2488
|
-
if (add({
|
|
2489
|
-
rule: "ends-at-plugs",
|
|
2490
|
-
edge: e.id,
|
|
2491
|
-
detail: "an end of the route is away from its node"
|
|
2492
|
-
})) return out;
|
|
2493
|
-
}
|
|
2494
|
-
const fs = fixedPortPoint(s, e.source);
|
|
2495
|
-
const ft = fixedPortPoint(t, e.target);
|
|
2496
|
-
if (fs && dist(fs, a) > .5 || ft && dist(ft, b) > .5) {
|
|
2497
|
-
if (add({
|
|
2498
|
-
rule: "fixed-plugs",
|
|
2499
|
-
edge: e.id,
|
|
2500
|
-
detail: "the route does not start or end at its fixed plug"
|
|
2501
|
-
})) return out;
|
|
2502
|
-
}
|
|
2503
|
-
for (let i = 1; i < r.points.length; i++) {
|
|
2504
|
-
const p = r.points[i - 1];
|
|
2505
|
-
const q = r.points[i];
|
|
2506
|
-
let hit;
|
|
2507
|
-
grid.querySegment(p, q, 0, (n) => {
|
|
2508
|
-
if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -.5))) hit = n;
|
|
2509
|
-
});
|
|
2510
|
-
if (hit) {
|
|
2511
|
-
if (add({
|
|
2512
|
-
rule: "no-pass-through",
|
|
2513
|
-
edge: e.id,
|
|
2514
|
-
detail: `segment ${i} goes through ${hit.id}`
|
|
2515
|
-
})) return out;
|
|
2516
|
-
break;
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
const bundled = /* @__PURE__ */ new Set();
|
|
2521
|
-
for (const b of result.bundles ?? []) for (const id of b.edges) {
|
|
2522
|
-
const r = routes.get(id);
|
|
2523
|
-
const shares = r !== void 0 && r.points.length >= b.trunk.length && b.trunk.every((p, i) => dist(p, r.points[i]) <= .5);
|
|
2524
|
-
if (bundled.has(id) || !shares) {
|
|
2525
|
-
if (add({
|
|
2526
|
-
rule: "bundle-trunk",
|
|
2527
|
-
edge: id,
|
|
2528
|
-
detail: `bundle ${b.id}: the route is in two bundles or does not start with the trunk`
|
|
2529
|
-
})) return out;
|
|
2530
|
-
}
|
|
2531
|
-
bundled.add(id);
|
|
2532
|
-
}
|
|
2533
|
-
for (const run of result.runs ?? []) {
|
|
2534
|
-
if (run.edges.length < 2) {
|
|
2535
|
-
if (add({
|
|
2536
|
-
rule: "run-shared",
|
|
2537
|
-
detail: `run ${run.id} has fewer than two edges`
|
|
2538
|
-
})) return out;
|
|
2539
|
-
}
|
|
2540
|
-
for (const id of run.edges) {
|
|
2541
|
-
const r = routes.get(id);
|
|
2542
|
-
if (!r || !(holds(r.points, run.points) || holds(r.points, [...run.points].reverse()))) {
|
|
2543
|
-
if (add({
|
|
2544
|
-
rule: "run-shared",
|
|
2545
|
-
edge: id,
|
|
2546
|
-
detail: `run ${run.id}: the route does not hold the points of the run in a row`
|
|
2547
|
-
})) return out;
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
for (const level of result.levels ?? []) {
|
|
2552
|
-
const itemOf = /* @__PURE__ */ new Map();
|
|
2553
|
-
for (const it of level.items) for (const n of it.nodes) itemOf.set(n, it.id);
|
|
2554
|
-
if (itemOf.size !== graph.nodes.length) {
|
|
2555
|
-
if (add({
|
|
2556
|
-
rule: "counts-sum",
|
|
2557
|
-
detail: `level ${level.depth}: the items hold ${itemOf.size} of ${graph.nodes.length} nodes`
|
|
2558
|
-
})) return out;
|
|
2559
|
-
}
|
|
2560
|
-
let between = 0;
|
|
2561
|
-
for (const e of graph.edges) if (itemOf.get(e.source.node) !== itemOf.get(e.target.node)) between++;
|
|
2562
|
-
const sum = level.links.reduce((s, l) => s + l.count, 0);
|
|
2563
|
-
if (sum !== between) {
|
|
2564
|
-
if (add({
|
|
2565
|
-
rule: "counts-sum",
|
|
2566
|
-
detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
|
|
2567
|
-
})) return out;
|
|
2568
|
-
}
|
|
2569
|
-
const boxOf = new Map(level.items.map((it) => [it.id, it.box]));
|
|
2570
|
-
for (const l of level.links) {
|
|
2571
|
-
const s = boxOf.get(l.source);
|
|
2572
|
-
const t = boxOf.get(l.target);
|
|
2573
|
-
const p = l.points;
|
|
2574
|
-
if (!s || !t || p.length < 2 || !onBoundary(p[0], s) || !onBoundary(p[p.length - 1], t)) {
|
|
2575
|
-
if (add({
|
|
2576
|
-
rule: "link-ends",
|
|
2577
|
-
detail: `level ${level.depth}: the link ${l.source} to ${l.target} does not start and end on the boundaries of its items`
|
|
2578
|
-
})) return out;
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
return out;
|
|
2583
|
-
}
|
|
2584
|
-
/** This function tells if the point is on the boundary of the box, within `TOLERANCE`. */
|
|
2585
|
-
function onBoundary(p, b) {
|
|
2586
|
-
const inner = inflate(b, -.5);
|
|
2587
|
-
return !(p.x > inner.x && p.x < inner.x + inner.w && p.y > inner.y && p.y < inner.y + inner.h) && pointBoxDistance(p, b) <= .5;
|
|
2588
|
-
}
|
|
2589
|
-
/** This function tells if the polyline `p` holds the points of `part` in a row. */
|
|
2590
|
-
function holds(p, part) {
|
|
2591
|
-
if (part.length === 0) return true;
|
|
2592
|
-
for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]) <= .5)) return true;
|
|
2593
|
-
return false;
|
|
2594
|
-
}
|
|
2595
3267
|
// src/bench/quality.ts
|
|
2596
3268
|
/** The clearance stops at this distance. */
|
|
2597
3269
|
const CLEARANCE_CAP = 40;
|
|
@@ -2834,7 +3506,9 @@ function benchOne(router, fixture, graph, repeat = 3) {
|
|
|
2834
3506
|
deterministic,
|
|
2835
3507
|
violations: all.length,
|
|
2836
3508
|
sample: all.slice(0, 5),
|
|
2837
|
-
quality: measureQuality(graph, result)
|
|
3509
|
+
quality: measureQuality(graph, result),
|
|
3510
|
+
faults: measureRouteFaults(graph, result),
|
|
3511
|
+
levelFaults: measureLevelFaults(result)
|
|
2838
3512
|
};
|
|
2839
3513
|
}
|
|
2840
3514
|
/** This function writes the rows as a Markdown table. */
|
|
@@ -2851,6 +3525,33 @@ function formatTable(rows) {
|
|
|
2851
3525
|
})
|
|
2852
3526
|
].join("\n");
|
|
2853
3527
|
}
|
|
2854
|
-
|
|
3528
|
+
/**
|
|
3529
|
+
* This function writes the faults of the rows as a Markdown table: the routes first, then the sum of the levels with groups.
|
|
3530
|
+
* A router without such levels gets a dash in the columns of the levels.
|
|
3531
|
+
*/
|
|
3532
|
+
function formatFaults(rows) {
|
|
3533
|
+
return [
|
|
3534
|
+
"| router | fixture | through | out and back | worst detour | outside | level links | level overlaps | level through | level out and back | level worst detour | level outside |",
|
|
3535
|
+
"|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
|
|
3536
|
+
...rows.map((r) => {
|
|
3537
|
+
const f = r.faults;
|
|
3538
|
+
const cells = [
|
|
3539
|
+
r.router,
|
|
3540
|
+
r.fixture,
|
|
3541
|
+
f.through,
|
|
3542
|
+
f.backtrack,
|
|
3543
|
+
f.worstDetour.toFixed(2),
|
|
3544
|
+
f.outside
|
|
3545
|
+
];
|
|
3546
|
+
if (r.levelFaults.length === 0) cells.push("-", "-", "-", "-", "-", "-");
|
|
3547
|
+
else {
|
|
3548
|
+
const l = sumFaults(r.levelFaults);
|
|
3549
|
+
cells.push(l.links, r.levelFaults.reduce((s, x) => s + x.overlaps, 0), l.through, l.backtrack, l.worstDetour.toFixed(2), l.outside);
|
|
3550
|
+
}
|
|
3551
|
+
return `| ${cells.join(" | ")} |`;
|
|
3552
|
+
})
|
|
3553
|
+
].join("\n");
|
|
3554
|
+
}
|
|
3555
|
+
export { BACKTRACK_MIN, BACKTRACK_SHARE, CASES, CLEARANCE_CAP, CornerGraph, FIXTURES, Grid, OUTSIDE_SLACK, Obstacles, ROUTERS, SIDE_NORMAL, TOLERANCE, WayFinder, allSections, backtrackOf, benchOne, bendCount, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, countOverlaps, denseGroupsBoard, dist, fixedPortPoint, formatFaults, formatTable, frechet, goesOutAndBack, groupEdges, hubChainBoard, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, makeFixture, manyGroupsBoard, measureLevel, measureLevelFaults, measureLinks, measureQuality, measureRouteFaults, measureStability, nearestSide, nodeIndex, offsetPolyline, orderLanes, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, rng, roundCorners, runBenchmark, runKey, sameData, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splitPoint, straightRouter, sumFaults, treeDepth, trunksRouter, turns };
|
|
2855
3556
|
|
|
2856
3557
|
//# sourceMappingURL=index.js.map
|