@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 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. The result gives one zoom level for each depth of the tree.
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.
@@ -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":"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"}
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], this.#pad)) out.push(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.nearestVisible(to, this.#k * 2)) goal.set(j, dist(this.points[j], to));
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.nearestVisible(from, this.#k * 2)) relax(j, dist(from, this.points[j]), -1);
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. The result gives one zoom level for each depth of the tree.
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
- mark.set(chain[0], out.length - 1);
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, marks)
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
- * Its points are the part of the route of its first edge between the two items.
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, marks) {
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
- points: route.slice(from, to + 1)
1997
+ edge: e.id
1721
1998
  });
1722
1999
  }
1723
- levels.push({
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: [...links.values()]
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;