@mark1russell7/highways 0.1.1 → 0.1.2
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 +6 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +332 -24
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/properties.ts +27 -2
- package/src/levels.ts +243 -0
- package/src/routers/sections.ts +17 -37
- package/src/visibility.ts +25 -3
package/build/index.d.ts
CHANGED
|
@@ -496,7 +496,10 @@ interface SectionsOptions extends WayOptions, SectionOptions {
|
|
|
496
496
|
* 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.
|
|
497
497
|
* An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
|
|
498
498
|
* Each way from hub to hub is found one time, so all the edges on it share its points.
|
|
499
|
-
* The edges in one section of the lowest level go straight around the nodes.
|
|
499
|
+
* The edges in one section of the lowest level go straight around the nodes.
|
|
500
|
+
*
|
|
501
|
+
* The result gives one zoom level for each depth of the tree.
|
|
502
|
+
* A link of a level goes from the boundary of one item to the boundary of the other, around the other items.
|
|
500
503
|
*/
|
|
501
504
|
export declare function sectionsRouter(options: SectionsOptions): Router;
|
|
502
505
|
/** The options of the trunk router. */
|
|
@@ -542,7 +545,7 @@ export declare function trunksRouter(options: TrunksOptions): Router;
|
|
|
542
545
|
export declare const ROUTERS: readonly Router[];
|
|
543
546
|
/** A broken hard rule. A correct router has none. */
|
|
544
547
|
export interface Violation {
|
|
545
|
-
readonly rule: "complete" | "ends-at-plugs" | "fixed-plugs" | "no-pass-through" | "counts-sum" | "bundle-trunk" | "run-shared";
|
|
548
|
+
readonly rule: "complete" | "ends-at-plugs" | "fixed-plugs" | "no-pass-through" | "counts-sum" | "link-ends" | "bundle-trunk" | "run-shared";
|
|
546
549
|
readonly edge?: string;
|
|
547
550
|
readonly detail: string;
|
|
548
551
|
}
|
|
@@ -551,6 +554,7 @@ export declare const TOLERANCE = .5;
|
|
|
551
554
|
/**
|
|
552
555
|
* This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
|
|
553
556
|
* 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.
|
|
557
|
+
* Each link of a level starts on the box of its source and ends on the box of its target.
|
|
554
558
|
*
|
|
555
559
|
* Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
|
|
556
560
|
* Each run has two edges or more, and the route of each edge holds the points of the run in a row.
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/geom.ts","../src/grid.ts","../src/model.ts","../src/fixtures.ts","../src/plugs.ts","../src/curves.ts","../src/visibility.ts","../src/ways.ts","../src/segments.ts","../src/sections.ts","../src/routers/straight.ts","../src/routers/plugs.ts","../src/routers/corners.ts","../src/routers/shared.ts","../src/routers/sections.ts","../src/routers/trunks.ts","../src/routers/index.ts","../src/bench/properties.ts","../src/bench/quality.ts","../src/bench/run.ts"],"mappings":";iBACiB;WACN;WACA;;;iBAIM;WACN;WACA;WACA;WACA;;;wBAIK,KAAK,GAAG,OAAO,GAAG;;wBAOlB,OAAO,GAAG,MAAM;;wBAKhB,QAAQ,GAAG,KAAK,YAAY;;wBAK5B,eAAe,YAAY;;;;;wBAY3B,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG;;;;;wBAY/C,eAAe,GAAG,OAAO,GAAG,OAAO,KAAK,KAAK;;wBAK7C,gBAAgB,GAAG,OAAO,GAAG,OAAO,YAAY,YAAY,YAAY,YAAY;;wBA2BpF,iBAAiB,GAAG,OAAO,GAAG;;wBAO9B,qBAAqB,GAAG,OAAO,GAAG,OAAO,GAAG;;wBAS5C,mBAAmB,GAAG,OAAO,GAAG,OAAO,KAAK;;;;;wBAiB5C,MAAM,YAAY,SAAS;;wBAgB3B,UAAU,YAAY,SAAS;;;;;wBAQ/B,gBAAgB,YAAY,SAAS;;;;;wBAgBrC,QAAQ,YAAY,SAAS,YAAY;;;;;;qBC1J5C,KAAK;;;EAUhB,YAAY;;EAuEZ,IAAI,MAAM,GAAG,GAAG;;EAKhB,WAAW,MAAM,GAAG,GAAG,OAAO,GAAG;;EAMjC,MAAM,GAAG,KAAK,QAAQ,MAAM;;EAK5B,aAAa,GAAG,OAAO,GAAG,OAAO,gBAAgB,QAAQ,MAAM;;;wBAOjD,cAAc,GAAG,OAAO,GAAG,QAAQ;;YC5GvC;;;;;iBAMK;WACN;;WAEA,KAAK;;WAEL,OAAO;;;iBAID;WACN;WACA,KAAK;WACL,iBAAiB;;;iBAIX;WACN;WACA;;;iBAIM;WACN;WACA,QAAQ;WACR,QAAQ;WACR,OAAO,SAAS;;;iBAIV;WACN,gBAAgB;WAChB,gBAAgB;;;iBAIV;WACN;WACA,iBAAiB;;;;;;iBAOX;WACN;WACA,gBAAgB;WAChB,gBAAgB;;;iBAIV;WACN;WACA,KAAK;WACL;;;iBAIM;WACN;WACA;WACA;WACA,iBAAiB;;;iBAIX;WACN,iBAAiB;WACjB,kBAAkB;WAClB,mBAAmB;;WAEnB,gBAAgB;;;;;;iBAOV;WACN;;WAEA;;WAEA;;WAEA;;WAEA,gBAAgB;;;;;;iBAOV;WACN;;WAEA;WACA,iBAAiB;;WAEjB;;;YAIC,aAAa,MAAM;;iBAGd;WACN;;WAEA;EACT,MAAM,OAAO,QAAQ;;;wBAIP,UAAU,GAAG,QAAQ,YAAY;;wBAOjC,eAAe,MAAM,MAAM,KAAK,MAAM;;wBC/HtC,IAAI;;;;;YAeR;;iBAGK;WACN;WACA,MAAM;WACN;;WAEA;WACA;;WAEA;;WAEA;;;qBAIE,mBAAmB;;;;;wBAqBhB,YAAY,MAAM,cAAc;;iBCtD/B;WACN,OAAO;WACP,QAAQ;;;qBAIN,aAAa,SAAS,OAAO,MAAM;;wBAQhC,YAAY,KAAK,KAAK,GAAG,QAAQ;;;;;wBAgBjC,WAAW,KAAK,KAAK,QAAQ,QAAQ;;;;;wBAYrC,YAAY,KAAK,KAAK,MAAM,MAAM,QAAQ,OAAO,iBAAY;;;;;wBAsB7D,UAAU,MAAM,MAAM,KAAK,KAAK,QAAQ,QAAQ;;iBCjE/C;;WAEN;;WAEA;;WAEA;;;;;;wBAOK,iBAAiB,eAAe;;;;;;;wBAchC,gBAAgB,IAAI,OAAO,QAAQ,OAAO,QAAQ,OAAO,UAAS,oBAAyB;;;;;wBAkC3F,aAAa,YAAY,SAAS,gBAAgB,iBAAY;;qBC3DjE;;;EAIX,YAAY,gBAAgB,QAAQ;;EAMpC,MAAM,GAAG,OAAO,GAAG,OAAO,aAAa,OAAO,YAAY;;EAY1D,UAAU,YAAY,SAAS,aAAa,OAAO,YAAY;;EAM/D,OAAO,GAAG,OAAO,aAAa,OAAO;;;iBAatB;;WAEN;;WAEA;;;;;WAKA;;;;;;;;qBASE;;WACF,QAAQ;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/geom.ts","../src/grid.ts","../src/model.ts","../src/fixtures.ts","../src/plugs.ts","../src/curves.ts","../src/visibility.ts","../src/ways.ts","../src/segments.ts","../src/sections.ts","../src/routers/straight.ts","../src/routers/plugs.ts","../src/routers/corners.ts","../src/routers/shared.ts","../src/routers/sections.ts","../src/routers/trunks.ts","../src/routers/index.ts","../src/bench/properties.ts","../src/bench/quality.ts","../src/bench/run.ts"],"mappings":"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"}
|
package/build/index.js
CHANGED
|
@@ -372,7 +372,7 @@ const COLLECTIONS = [
|
|
|
372
372
|
"scores",
|
|
373
373
|
"teams"
|
|
374
374
|
];
|
|
375
|
-
const SIDES = [
|
|
375
|
+
const SIDES$1 = [
|
|
376
376
|
"north",
|
|
377
377
|
"east",
|
|
378
378
|
"south",
|
|
@@ -441,7 +441,7 @@ function makeFixture(spec) {
|
|
|
441
441
|
clusterOf.push(cluster);
|
|
442
442
|
const id = `n${nodes.length}`;
|
|
443
443
|
if (R() < (spec.fixedPorts ?? 0)) {
|
|
444
|
-
const s = SIDES[Math.floor(R() * 4)];
|
|
444
|
+
const s = SIDES$1[Math.floor(R() * 4)];
|
|
445
445
|
const t = .2 + .6 * R();
|
|
446
446
|
const at = s === "north" ? {
|
|
447
447
|
x: w * t,
|
|
@@ -778,6 +778,7 @@ var CornerGraph = class {
|
|
|
778
778
|
#stamp = 0;
|
|
779
779
|
#tested = /* @__PURE__ */ new Int32Array(0);
|
|
780
780
|
#testStamp = 0;
|
|
781
|
+
#endCache = /* @__PURE__ */ new Map();
|
|
781
782
|
/** This constructor makes the graph of the nodes. */
|
|
782
783
|
constructor(nodes, obstacles, options) {
|
|
783
784
|
this.#obstacles = obstacles;
|
|
@@ -877,6 +878,9 @@ var CornerGraph = class {
|
|
|
877
878
|
}
|
|
878
879
|
/** This method gives at most `k` corners that `p` can see, nearest first. The corner `self` does not count. */
|
|
879
880
|
nearestVisible(p, k, self = -1) {
|
|
881
|
+
return this.#nearestVisible(p, k, self, this.#pad);
|
|
882
|
+
}
|
|
883
|
+
#nearestVisible(p, k, self, pad) {
|
|
880
884
|
const out = [];
|
|
881
885
|
if (this.#tested.length < this.points.length) this.#tested = new Int32Array(this.points.length);
|
|
882
886
|
const tested = this.#tested;
|
|
@@ -895,7 +899,7 @@ var CornerGraph = class {
|
|
|
895
899
|
for (const [d, j] of cands) {
|
|
896
900
|
if (d > r) break;
|
|
897
901
|
tested[j] = stamp;
|
|
898
|
-
if (this.#obstacles.clear(p, this.points[j],
|
|
902
|
+
if (this.#obstacles.clear(p, this.points[j], pad)) out.push(j);
|
|
899
903
|
if (out.length >= k) return out;
|
|
900
904
|
}
|
|
901
905
|
}
|
|
@@ -918,7 +922,7 @@ var CornerGraph = class {
|
|
|
918
922
|
}
|
|
919
923
|
const stamp = ++this.#stamp;
|
|
920
924
|
const goal = /* @__PURE__ */ new Map();
|
|
921
|
-
for (const j of this
|
|
925
|
+
for (const j of this.#ends(to)) goal.set(j, dist(this.points[j], to));
|
|
922
926
|
if (goal.size === 0) return void 0;
|
|
923
927
|
const heap = new MinHeap();
|
|
924
928
|
const h = (i) => floor * dist(this.points[i], to);
|
|
@@ -930,7 +934,7 @@ var CornerGraph = class {
|
|
|
930
934
|
this.#parent[j] = parent;
|
|
931
935
|
heap.push(g + h(j), j);
|
|
932
936
|
};
|
|
933
|
-
for (const j of this
|
|
937
|
+
for (const j of this.#ends(from)) relax(j, dist(from, this.points[j]), -1);
|
|
934
938
|
let best = Infinity;
|
|
935
939
|
let bestEnd = -1;
|
|
936
940
|
while (heap.size > 0) {
|
|
@@ -958,6 +962,21 @@ var CornerGraph = class {
|
|
|
958
962
|
out.push(from);
|
|
959
963
|
return out.reverse();
|
|
960
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* This method gives the corners that an end of a path can see. An end near a box sees no corner with the clearance.
|
|
967
|
+
* Then the end takes the corners that it sees without the clearance, so the path does not cut through the boxes.
|
|
968
|
+
* The graph does not change, so the method keeps the corners of each end.
|
|
969
|
+
*/
|
|
970
|
+
#ends(p) {
|
|
971
|
+
const key = `${p.x},${p.y}`;
|
|
972
|
+
let out = this.#endCache.get(key);
|
|
973
|
+
if (!out) {
|
|
974
|
+
out = this.nearestVisible(p, this.#k * 2);
|
|
975
|
+
if (out.length === 0) out = this.#nearestVisible(p, this.#k * 2, -1, 0);
|
|
976
|
+
this.#endCache.set(key, out);
|
|
977
|
+
}
|
|
978
|
+
return out;
|
|
979
|
+
}
|
|
961
980
|
/** This method removes the vertices that a later vertex makes needless: from each vertex, it goes to the farthest vertex in view. */
|
|
962
981
|
pull(p, lookahead = 16) {
|
|
963
982
|
if (p.length < 3) return [...p];
|
|
@@ -1492,13 +1511,281 @@ const plugsRouter = {
|
|
|
1492
1511
|
return { routes };
|
|
1493
1512
|
}
|
|
1494
1513
|
};
|
|
1514
|
+
// src/levels.ts
|
|
1515
|
+
const EPS = 1e-6;
|
|
1516
|
+
/**
|
|
1517
|
+
* This function removes the repeated points, the points in the middle of a straight run,
|
|
1518
|
+
* and the points where the way turns back on itself.
|
|
1519
|
+
*/
|
|
1520
|
+
function tidy(p) {
|
|
1521
|
+
const out = [];
|
|
1522
|
+
for (const q of p) {
|
|
1523
|
+
if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
|
|
1524
|
+
while (out.length >= 2) {
|
|
1525
|
+
const a = out[out.length - 2];
|
|
1526
|
+
const b = out[out.length - 1];
|
|
1527
|
+
const cross = (b.x - a.x) * (q.y - b.y) - (b.y - a.y) * (q.x - b.x);
|
|
1528
|
+
if (Math.abs(cross) > EPS * (1 + dist(a, b) * dist(b, q))) break;
|
|
1529
|
+
out.pop();
|
|
1530
|
+
}
|
|
1531
|
+
if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS) continue;
|
|
1532
|
+
out.push(q);
|
|
1533
|
+
}
|
|
1534
|
+
return out;
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* This function gives the links of one level. The items of the level are the obstacles.
|
|
1538
|
+
* A link starts on the boundary of the box of its source and ends on the boundary of the box of its target.
|
|
1539
|
+
* It goes around the boxes of the other items. Where there is space, its legs are parallel to the axes.
|
|
1540
|
+
*/
|
|
1541
|
+
function levelLinks(items, pairs, options = {}) {
|
|
1542
|
+
if (pairs.length === 0) return [];
|
|
1543
|
+
const nodes = items.map((i) => ({
|
|
1544
|
+
id: i.id,
|
|
1545
|
+
box: i.box
|
|
1546
|
+
}));
|
|
1547
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
1548
|
+
const ways = levelWays(nodes, options);
|
|
1549
|
+
const inset = Math.min(6, ways.margin);
|
|
1550
|
+
const out = [];
|
|
1551
|
+
for (const p of pairs) {
|
|
1552
|
+
const s = byId.get(p.source);
|
|
1553
|
+
const t = byId.get(p.target);
|
|
1554
|
+
if (!s || !t) continue;
|
|
1555
|
+
const [fs, ft] = facingPlugs(s.box, t.box, inset);
|
|
1556
|
+
let points = isFree(ways, s, fs) && isFree(ways, t, ft) ? quickWay(ways, ends(ways, s, fs, t, ft)) : void 0;
|
|
1557
|
+
points ??= squareWay(ways, ends(ways, s, sidePlug(ways, s, center(t.box)), t, sidePlug(ways, t, center(s.box))));
|
|
1558
|
+
out.push({
|
|
1559
|
+
source: p.source,
|
|
1560
|
+
target: p.target,
|
|
1561
|
+
count: p.count,
|
|
1562
|
+
points
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
return out;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* This function makes the obstacles and the corner graph of the items of a level.
|
|
1569
|
+
* The buckets of the obstacles grow with the items, so a long link visits few buckets.
|
|
1570
|
+
*/
|
|
1571
|
+
function levelWays(nodes, options) {
|
|
1572
|
+
const margin = options.margin ?? 5;
|
|
1573
|
+
let size = 0;
|
|
1574
|
+
for (const n of nodes) size += Math.max(n.box.w, n.box.h);
|
|
1575
|
+
const obstacles = new Obstacles(nodes, Math.max(180, 2 * size / Math.max(1, nodes.length)));
|
|
1576
|
+
return {
|
|
1577
|
+
obstacles,
|
|
1578
|
+
corners: new CornerGraph(nodes, obstacles, {
|
|
1579
|
+
margin,
|
|
1580
|
+
k: options.k ?? 8
|
|
1581
|
+
}),
|
|
1582
|
+
margin,
|
|
1583
|
+
out: (p) => ({
|
|
1584
|
+
x: p.point.x + p.normal.x * margin,
|
|
1585
|
+
y: p.point.y + p.normal.y * margin
|
|
1586
|
+
})
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* This function gives the plugs of a link between two boxes. Each plug is on the side that faces the other box.
|
|
1591
|
+
* When the two sides face each other and their spans overlap, both plugs are at the middle of the overlap, so the link is straight.
|
|
1592
|
+
*/
|
|
1593
|
+
function facingPlugs(a, b, inset) {
|
|
1594
|
+
const sa = sideToward(a, center(b));
|
|
1595
|
+
const sb = sideToward(b, center(a));
|
|
1596
|
+
const plug = (box, side, toward) => ({
|
|
1597
|
+
point: pointOnSide(box, side, toward, inset),
|
|
1598
|
+
normal: SIDE_NORMAL[side]
|
|
1599
|
+
});
|
|
1600
|
+
const across = sa === "east" && sb === "west" || sa === "west" && sb === "east";
|
|
1601
|
+
if (across || sa === "south" && sb === "north" || sa === "north" && sb === "south") {
|
|
1602
|
+
const lo = across ? Math.max(a.y, b.y) + inset : Math.max(a.x, b.x) + inset;
|
|
1603
|
+
const hi = across ? Math.min(a.y + a.h, b.y + b.h) - inset : Math.min(a.x + a.w, b.x + b.w) - inset;
|
|
1604
|
+
if (lo <= hi) {
|
|
1605
|
+
const ca = center(a);
|
|
1606
|
+
const cb = center(b);
|
|
1607
|
+
const m = Math.max(lo, Math.min(hi, across ? (ca.y + cb.y) / 2 : (ca.x + cb.x) / 2));
|
|
1608
|
+
const at = (box, side) => across ? {
|
|
1609
|
+
x: side === "east" ? box.x + box.w : box.x,
|
|
1610
|
+
y: m
|
|
1611
|
+
} : {
|
|
1612
|
+
x: m,
|
|
1613
|
+
y: side === "south" ? box.y + box.h : box.y
|
|
1614
|
+
};
|
|
1615
|
+
return [{
|
|
1616
|
+
point: at(a, sa),
|
|
1617
|
+
normal: SIDE_NORMAL[sa]
|
|
1618
|
+
}, {
|
|
1619
|
+
point: at(b, sb),
|
|
1620
|
+
normal: SIDE_NORMAL[sb]
|
|
1621
|
+
}];
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return [plug(a, sa, center(b)), plug(b, sb, center(a))];
|
|
1625
|
+
}
|
|
1626
|
+
const SIDES = [
|
|
1627
|
+
"north",
|
|
1628
|
+
"east",
|
|
1629
|
+
"south",
|
|
1630
|
+
"west"
|
|
1631
|
+
];
|
|
1632
|
+
/** This function tells if the way out of a plug is free. The plug and the point at `margin` from it are in no other box. */
|
|
1633
|
+
function isFree(ways, node, plug) {
|
|
1634
|
+
return !ways.obstacles.inside(ways.out(plug), 0, node) && !ways.obstacles.inside(plug.point, 0, node);
|
|
1635
|
+
}
|
|
1636
|
+
/** This function gives the plug at the middle of a side of the node: the side with a free way out that faces `toward` most. */
|
|
1637
|
+
function sidePlug(ways, node, toward) {
|
|
1638
|
+
const b = node.box;
|
|
1639
|
+
const c = center(b);
|
|
1640
|
+
const d = {
|
|
1641
|
+
x: toward.x - c.x,
|
|
1642
|
+
y: toward.y - c.y
|
|
1643
|
+
};
|
|
1644
|
+
const facing = (s) => SIDE_NORMAL[s].x * d.x + SIDE_NORMAL[s].y * d.y;
|
|
1645
|
+
const order = [...SIDES].sort((u, v) => facing(v) - facing(u));
|
|
1646
|
+
const plug = (s) => {
|
|
1647
|
+
const n = SIDE_NORMAL[s];
|
|
1648
|
+
return {
|
|
1649
|
+
point: {
|
|
1650
|
+
x: c.x + n.x * b.w / 2,
|
|
1651
|
+
y: c.y + n.y * b.h / 2
|
|
1652
|
+
},
|
|
1653
|
+
normal: n
|
|
1654
|
+
};
|
|
1655
|
+
};
|
|
1656
|
+
for (const s of order) if (isFree(ways, node, plug(s))) return plug(s);
|
|
1657
|
+
return plug(order[0]);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* This function gives the ends of a link. The link leaves each plug along its normal for the margin.
|
|
1661
|
+
* 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
|
+
* Two plugs that face each other across a narrow space make the link turn in the middle of that space.
|
|
1663
|
+
*/
|
|
1664
|
+
function ends(ways, s, ps, t, pt) {
|
|
1665
|
+
let ra = reach(ways, s, ps);
|
|
1666
|
+
let rb = reach(ways, t, pt);
|
|
1667
|
+
if (ps.normal.x === -pt.normal.x && ps.normal.y === -pt.normal.y) {
|
|
1668
|
+
const d = (pt.point.x - ps.point.x) * ps.normal.x + (pt.point.y - ps.point.y) * ps.normal.y;
|
|
1669
|
+
if (d > 0 && ra + rb > d) ra = rb = d / 2;
|
|
1670
|
+
}
|
|
1671
|
+
return {
|
|
1672
|
+
ps,
|
|
1673
|
+
pt,
|
|
1674
|
+
a: along(ps, ra),
|
|
1675
|
+
b: along(pt, rb)
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
const along = (p, d) => ({
|
|
1679
|
+
x: p.point.x + p.normal.x * d,
|
|
1680
|
+
y: p.point.y + p.normal.y * d
|
|
1681
|
+
});
|
|
1682
|
+
/** This function gives the distance that a link goes along the normal of a plug before it turns. */
|
|
1683
|
+
function reach(ways, node, plug) {
|
|
1684
|
+
const skip = /* @__PURE__ */ new Set([node]);
|
|
1685
|
+
const far = 2 * ways.margin;
|
|
1686
|
+
if (ways.obstacles.clear(plug.point, along(plug, far), 0, skip)) return ways.margin;
|
|
1687
|
+
let lo = 0;
|
|
1688
|
+
let hi = far;
|
|
1689
|
+
for (let i = 0; i < 10; i++) {
|
|
1690
|
+
const m = (lo + hi) / 2;
|
|
1691
|
+
if (ways.obstacles.clear(plug.point, along(plug, m), 0, skip)) lo = m;
|
|
1692
|
+
else hi = m;
|
|
1693
|
+
}
|
|
1694
|
+
return lo / 2;
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* This function gives the way between the ends when one or two legs parallel to the axes are clear, or `undefined`.
|
|
1698
|
+
* The legs keep half the margin from the boxes, or less when an end turns nearer to a box.
|
|
1699
|
+
*/
|
|
1700
|
+
function quickWay(ways, e) {
|
|
1701
|
+
const near = Math.min(dist(e.ps.point, e.a), dist(e.pt.point, e.b));
|
|
1702
|
+
const pad = Math.max(0, Math.min(ways.margin / 2, near - 1));
|
|
1703
|
+
const legs = square(e.a, e.b, ways.obstacles, pad, axisOf(e.ps.normal), axisOf(e.pt.normal));
|
|
1704
|
+
return legs && tidy([
|
|
1705
|
+
e.ps.point,
|
|
1706
|
+
e.a,
|
|
1707
|
+
...legs,
|
|
1708
|
+
e.pt.point
|
|
1709
|
+
]);
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* This function gives a way from plug to plug through the corner graph. The way leaves and enters each plug along its normal.
|
|
1713
|
+
* Then it pulls the corners tight with legs that are parallel to the axes, where the obstacles let it.
|
|
1714
|
+
*/
|
|
1715
|
+
function squareWay(ways, e) {
|
|
1716
|
+
const quick = quickWay(ways, e);
|
|
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));
|
|
1719
|
+
return tidy([
|
|
1720
|
+
e.ps.point,
|
|
1721
|
+
...legs,
|
|
1722
|
+
e.pt.point
|
|
1723
|
+
]);
|
|
1724
|
+
}
|
|
1725
|
+
const axisOf = (n) => Math.abs(n.x) > Math.abs(n.y) ? "x" : "y";
|
|
1726
|
+
/**
|
|
1727
|
+
* This function pulls a path tight. From each vertex, it goes to the farthest vertex that one or two legs can reach.
|
|
1728
|
+
* These legs are parallel to the axes and clear of the obstacles by `pad`. It keeps a diagonal leg only where no such legs are clear.
|
|
1729
|
+
* `first` is the axis of the way into the first vertex, and `last` is the axis of the way out of the last vertex.
|
|
1730
|
+
*/
|
|
1731
|
+
function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
|
|
1732
|
+
if (p.length === 0) return [];
|
|
1733
|
+
const out = [p[0]];
|
|
1734
|
+
let axis = first;
|
|
1735
|
+
let i = 0;
|
|
1736
|
+
while (i < p.length - 1) {
|
|
1737
|
+
let step;
|
|
1738
|
+
for (let j = Math.min(p.length - 1, i + lookahead); j > i && !step; j--) {
|
|
1739
|
+
const legs = square(p[i], p[j], obstacles, pad, axis, j === p.length - 1 ? last : void 0);
|
|
1740
|
+
if (legs) step = {
|
|
1741
|
+
j,
|
|
1742
|
+
legs
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
step ??= {
|
|
1746
|
+
j: i + 1,
|
|
1747
|
+
legs: square(p[i], p[i + 1], obstacles, 0, axis, i + 1 === p.length - 1 ? last : void 0) ?? [p[i + 1]]
|
|
1748
|
+
};
|
|
1749
|
+
out.push(...step.legs);
|
|
1750
|
+
const end = out[out.length - 1];
|
|
1751
|
+
const before = out[out.length - 2];
|
|
1752
|
+
axis = Math.abs(end.y - before.y) < EPS ? "x" : Math.abs(end.x - before.x) < EPS ? "y" : void 0;
|
|
1753
|
+
i = step.j;
|
|
1754
|
+
}
|
|
1755
|
+
return out;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* This function gives one or two legs parallel to the axes from `a` to `b`, clear of the obstacles by `pad`, or `undefined`.
|
|
1759
|
+
* Of two clear corners, it picks the corner that continues the axis `from` and arrives along the axis `to`.
|
|
1760
|
+
*/
|
|
1761
|
+
function square(a, b, obstacles, pad, from, to) {
|
|
1762
|
+
if (Math.abs(a.x - b.x) < EPS || Math.abs(a.y - b.y) < EPS) return obstacles.clear(a, b, pad) ? [b] : void 0;
|
|
1763
|
+
const options = [{
|
|
1764
|
+
c: {
|
|
1765
|
+
x: b.x,
|
|
1766
|
+
y: a.y
|
|
1767
|
+
},
|
|
1768
|
+
bends: (from === "y" ? 1 : 0) + (to === "x" ? 1 : 0)
|
|
1769
|
+
}, {
|
|
1770
|
+
c: {
|
|
1771
|
+
x: a.x,
|
|
1772
|
+
y: b.y
|
|
1773
|
+
},
|
|
1774
|
+
bends: (from === "x" ? 1 : 0) + (to === "y" ? 1 : 0)
|
|
1775
|
+
}];
|
|
1776
|
+
options.sort((u, v) => u.bends - v.bends);
|
|
1777
|
+
for (const { c } of options) if (obstacles.clear(a, c, pad) && obstacles.clear(c, b, pad)) return [c, b];
|
|
1778
|
+
}
|
|
1495
1779
|
// src/routers/sections.ts
|
|
1496
1780
|
const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
|
|
1497
1781
|
/**
|
|
1498
1782
|
* 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.
|
|
1499
1783
|
* An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
|
|
1500
1784
|
* Each way from hub to hub is found one time, so all the edges on it share its points.
|
|
1501
|
-
* The edges in one section of the lowest level go straight around the nodes.
|
|
1785
|
+
* The edges in one section of the lowest level go straight around the nodes.
|
|
1786
|
+
*
|
|
1787
|
+
* The result gives one zoom level for each depth of the tree.
|
|
1788
|
+
* A link of a level goes from the boundary of one item to the boundary of the other, around the other items.
|
|
1502
1789
|
*/
|
|
1503
1790
|
function sectionsRouter(options) {
|
|
1504
1791
|
const router = {
|
|
@@ -1559,7 +1846,6 @@ function sectionsRouter(options) {
|
|
|
1559
1846
|
};
|
|
1560
1847
|
const at = (p) => `${p.x},${p.y}`;
|
|
1561
1848
|
const routes = [];
|
|
1562
|
-
const marks = /* @__PURE__ */ new Map();
|
|
1563
1849
|
for (const e of graph.edges) {
|
|
1564
1850
|
const s = byId.get(e.source.node);
|
|
1565
1851
|
const t = byId.get(e.target.node);
|
|
@@ -1585,18 +1871,13 @@ function sectionsRouter(options) {
|
|
|
1585
1871
|
const side = (list) => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
|
|
1586
1872
|
const chain = [...side(a), ...side(b).reverse()];
|
|
1587
1873
|
const out = [ps.point];
|
|
1588
|
-
const mark = /* @__PURE__ */ new Map();
|
|
1589
1874
|
const append = (p) => {
|
|
1590
1875
|
for (const q of p) if (dist(q, out[out.length - 1]) > 1e-9) out.push(q);
|
|
1591
1876
|
};
|
|
1592
1877
|
const used = [];
|
|
1593
1878
|
const start = ways.out(ps);
|
|
1594
1879
|
append(piece(at(start), start, chain[0].id, hub(chain[0]), used));
|
|
1595
|
-
|
|
1596
|
-
for (let i = 1; i < chain.length; i++) {
|
|
1597
|
-
append(piece(chain[i - 1].id, hub(chain[i - 1]), chain[i].id, hub(chain[i]), used));
|
|
1598
|
-
mark.set(chain[i], out.length - 1);
|
|
1599
|
-
}
|
|
1880
|
+
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));
|
|
1600
1881
|
const end = ways.out(pt);
|
|
1601
1882
|
append(piece(chain[chain.length - 1].id, hub(chain[chain.length - 1]), at(end), end, used));
|
|
1602
1883
|
append([pt.point]);
|
|
@@ -1616,7 +1897,6 @@ function sectionsRouter(options) {
|
|
|
1616
1897
|
edge: e.id,
|
|
1617
1898
|
points: out
|
|
1618
1899
|
});
|
|
1619
|
-
marks.set(e.id, mark);
|
|
1620
1900
|
}
|
|
1621
1901
|
const routeOf = new Map(routes.map((r) => [r.edge, r]));
|
|
1622
1902
|
const runs = [];
|
|
@@ -1633,7 +1913,7 @@ function sectionsRouter(options) {
|
|
|
1633
1913
|
return {
|
|
1634
1914
|
routes,
|
|
1635
1915
|
runs,
|
|
1636
|
-
levels: makeLevels(graph, root, routeOf,
|
|
1916
|
+
levels: makeLevels(graph, root, routeOf, options)
|
|
1637
1917
|
};
|
|
1638
1918
|
}
|
|
1639
1919
|
};
|
|
@@ -1645,9 +1925,10 @@ function sectionsRouter(options) {
|
|
|
1645
1925
|
/**
|
|
1646
1926
|
* This function gives one zoom level for each depth of the tree, and a last level with each node alone.
|
|
1647
1927
|
* At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
|
|
1648
|
-
*
|
|
1928
|
+
* In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
|
|
1929
|
+
* In the last level, the points of a link are the route of its first edge.
|
|
1649
1930
|
*/
|
|
1650
|
-
function makeLevels(graph, root, routeOf,
|
|
1931
|
+
function makeLevels(graph, root, routeOf, options) {
|
|
1651
1932
|
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
1652
1933
|
const levels = [];
|
|
1653
1934
|
const deepest = treeDepth(root);
|
|
@@ -1709,21 +1990,30 @@ function makeLevels(graph, root, routeOf, marks) {
|
|
|
1709
1990
|
link.count++;
|
|
1710
1991
|
continue;
|
|
1711
1992
|
}
|
|
1712
|
-
const route = routeOf.get(e.id)?.points ?? [];
|
|
1713
|
-
const mark = marks.get(e.id);
|
|
1714
|
-
const from = typeof a === "number" ? 0 : mark?.get(a) ?? 0;
|
|
1715
|
-
const to = typeof b === "number" ? route.length - 1 : mark?.get(b) ?? route.length - 1;
|
|
1716
1993
|
links.set(key, {
|
|
1717
1994
|
source: ida,
|
|
1718
1995
|
target: idb,
|
|
1719
1996
|
count: 1,
|
|
1720
|
-
|
|
1997
|
+
edge: e.id
|
|
1721
1998
|
});
|
|
1722
1999
|
}
|
|
1723
|
-
|
|
2000
|
+
const pairs = [...links.values()].map(({ source, target, count }) => ({
|
|
2001
|
+
source,
|
|
2002
|
+
target,
|
|
2003
|
+
count
|
|
2004
|
+
}));
|
|
2005
|
+
if (d > deepest) levels.push({
|
|
2006
|
+
depth: d,
|
|
2007
|
+
items,
|
|
2008
|
+
links: [...links.values()].map(({ edge, ...l }) => ({
|
|
2009
|
+
...l,
|
|
2010
|
+
points: routeOf.get(edge)?.points ?? []
|
|
2011
|
+
}))
|
|
2012
|
+
});
|
|
2013
|
+
else levels.push({
|
|
1724
2014
|
depth: d,
|
|
1725
2015
|
items,
|
|
1726
|
-
links:
|
|
2016
|
+
links: levelLinks(items, pairs, options)
|
|
1727
2017
|
});
|
|
1728
2018
|
}
|
|
1729
2019
|
return levels;
|
|
@@ -2162,6 +2452,7 @@ const TOLERANCE = .5;
|
|
|
2162
2452
|
/**
|
|
2163
2453
|
* This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
|
|
2164
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.
|
|
2165
2456
|
*
|
|
2166
2457
|
* Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
|
|
2167
2458
|
* Each run has two edges or more, and the route of each edge holds the points of the run in a row.
|
|
@@ -2275,9 +2566,26 @@ function checkProperties(graph, result, limit = 100) {
|
|
|
2275
2566
|
detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
|
|
2276
2567
|
})) return out;
|
|
2277
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
|
+
}
|
|
2278
2581
|
}
|
|
2279
2582
|
return out;
|
|
2280
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
|
+
}
|
|
2281
2589
|
/** This function tells if the polyline `p` holds the points of `part` in a row. */
|
|
2282
2590
|
function holds(p, part) {
|
|
2283
2591
|
if (part.length === 0) return true;
|