@mark1russell7/highways 0.1.0 → 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
@@ -391,6 +391,17 @@ export interface PullApartOptions {
391
391
  * The lanes keep the lane order, so they do not cross in a segment. The two ends of each route stay at their plugs.
392
392
  */
393
393
  export declare function pullApart(graph: SegmentGraph, options: PullApartOptions): Route[];
394
+ /** This function gives the key of a shared run: the ids of its edges, sorted and joined with commas. */
395
+ export declare function runKey(edges: readonly string[]): string;
396
+ /**
397
+ * This function opens the shared segments whose key is in `open`: each edge of an open segment gets its own lane.
398
+ * The lanes are `laneWidth` apart, and an open segment is at most `maxWidth` wide. The other segments stay as one way.
399
+ *
400
+ * Lane 0 is on the left of the segment, from its first point to its last, with the y axis down.
401
+ * The function takes a result or its segments. It gives a route for each edge, with the ends at their plugs.
402
+ * The key of a segment is `runKey` of its edges.
403
+ */
404
+ export declare function laneRoutes(source: Result | SegmentGraph, open: ReadonlySet<string>, laneWidth: number, maxWidth?: number): Route[];
394
405
  /**
395
406
  * This function gives the lane order of the edges on a shared way `run` (metro-line order). Two routes go on together
396
407
  * past the last point of the run until they part. The route that turns more to the left there gets the lane more to the left.
@@ -485,7 +496,10 @@ interface SectionsOptions extends WayOptions, SectionOptions {
485
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.
486
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.
487
498
  * Each way from hub to hub is found one time, so all the edges on it share its points.
488
- * 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.
489
503
  */
490
504
  export declare function sectionsRouter(options: SectionsOptions): Router;
491
505
  /** The options of the trunk router. */
@@ -531,7 +545,7 @@ export declare function trunksRouter(options: TrunksOptions): Router;
531
545
  export declare const ROUTERS: readonly Router[];
532
546
  /** A broken hard rule. A correct router has none. */
533
547
  export interface Violation {
534
- 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";
535
549
  readonly edge?: string;
536
550
  readonly detail: string;
537
551
  }
@@ -540,6 +554,7 @@ export declare const TOLERANCE = .5;
540
554
  /**
541
555
  * This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
542
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.
543
558
  *
544
559
  * Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
545
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];
@@ -1240,6 +1259,30 @@ function offsetPolyline(p, d) {
1240
1259
  * The lanes keep the lane order, so they do not cross in a segment. The two ends of each route stay at their plugs.
1241
1260
  */
1242
1261
  function pullApart(graph, options) {
1262
+ return lanePaths(graph, (s) => s.count > 1 ? Math.min(options.spacing, options.maxWidth / (s.count - 1)) : 0);
1263
+ }
1264
+ /** This function gives the key of a shared run: the ids of its edges, sorted and joined with commas. */
1265
+ function runKey(edges) {
1266
+ return [...edges].sort().join(",");
1267
+ }
1268
+ /**
1269
+ * This function opens the shared segments whose key is in `open`: each edge of an open segment gets its own lane.
1270
+ * The lanes are `laneWidth` apart, and an open segment is at most `maxWidth` wide. The other segments stay as one way.
1271
+ *
1272
+ * Lane 0 is on the left of the segment, from its first point to its last, with the y axis down.
1273
+ * The function takes a result or its segments. It gives a route for each edge, with the ends at their plugs.
1274
+ * The key of a segment is `runKey` of its edges.
1275
+ */
1276
+ function laneRoutes(source, open, laneWidth, maxWidth = Infinity) {
1277
+ const graph = "paths" in source ? source : segmentsOf(source);
1278
+ if (open.size === 0) return graph.paths.map((path) => ({
1279
+ edge: path.edge,
1280
+ points: pathPoints(graph, path)
1281
+ }));
1282
+ return lanePaths(graph, (s) => s.count > 1 && open.has(runKey(s.edges)) ? Math.min(laneWidth, maxWidth / (s.count - 1)) : 0);
1283
+ }
1284
+ /** This function gives the route of each path, with the lanes of each segment at the spacing that `spacingOf` gives. */
1285
+ function lanePaths(graph, spacingOf) {
1243
1286
  const byId = new Map(graph.segments.map((s) => [s.id, s]));
1244
1287
  const routes = [];
1245
1288
  for (const path of graph.paths) {
@@ -1248,9 +1291,8 @@ function pullApart(graph, options) {
1248
1291
  for (const ref of path.segments) {
1249
1292
  const s = byId.get(ref.segment);
1250
1293
  if (!s) continue;
1251
- const n = s.count;
1252
- const spacing = n > 1 ? Math.min(options.spacing, options.maxWidth / (n - 1)) : 0;
1253
- const moved = offsetPolyline(s.points, ((n - 1) / 2 - ref.lane) * spacing);
1294
+ const spacing = spacingOf(s);
1295
+ const moved = offsetPolyline(s.points, ((s.count - 1) / 2 - ref.lane) * spacing);
1254
1296
  for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]) > SAME) out.push(q);
1255
1297
  }
1256
1298
  if (out.length >= 2 && ends.length >= 2) {
@@ -1469,13 +1511,281 @@ const plugsRouter = {
1469
1511
  return { routes };
1470
1512
  }
1471
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
+ }
1472
1779
  // src/routers/sections.ts
1473
1780
  const dedupe$1 = (p) => p.filter((q, i) => i === 0 || dist(q, p[i - 1]) > 1e-9);
1474
1781
  /**
1475
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.
1476
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.
1477
1784
  * Each way from hub to hub is found one time, so all the edges on it share its points.
1478
- * 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.
1479
1789
  */
1480
1790
  function sectionsRouter(options) {
1481
1791
  const router = {
@@ -1536,7 +1846,6 @@ function sectionsRouter(options) {
1536
1846
  };
1537
1847
  const at = (p) => `${p.x},${p.y}`;
1538
1848
  const routes = [];
1539
- const marks = /* @__PURE__ */ new Map();
1540
1849
  for (const e of graph.edges) {
1541
1850
  const s = byId.get(e.source.node);
1542
1851
  const t = byId.get(e.target.node);
@@ -1562,18 +1871,13 @@ function sectionsRouter(options) {
1562
1871
  const side = (list) => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
1563
1872
  const chain = [...side(a), ...side(b).reverse()];
1564
1873
  const out = [ps.point];
1565
- const mark = /* @__PURE__ */ new Map();
1566
1874
  const append = (p) => {
1567
1875
  for (const q of p) if (dist(q, out[out.length - 1]) > 1e-9) out.push(q);
1568
1876
  };
1569
1877
  const used = [];
1570
1878
  const start = ways.out(ps);
1571
1879
  append(piece(at(start), start, chain[0].id, hub(chain[0]), used));
1572
- mark.set(chain[0], out.length - 1);
1573
- for (let i = 1; i < chain.length; i++) {
1574
- append(piece(chain[i - 1].id, hub(chain[i - 1]), chain[i].id, hub(chain[i]), used));
1575
- mark.set(chain[i], out.length - 1);
1576
- }
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));
1577
1881
  const end = ways.out(pt);
1578
1882
  append(piece(chain[chain.length - 1].id, hub(chain[chain.length - 1]), at(end), end, used));
1579
1883
  append([pt.point]);
@@ -1593,7 +1897,6 @@ function sectionsRouter(options) {
1593
1897
  edge: e.id,
1594
1898
  points: out
1595
1899
  });
1596
- marks.set(e.id, mark);
1597
1900
  }
1598
1901
  const routeOf = new Map(routes.map((r) => [r.edge, r]));
1599
1902
  const runs = [];
@@ -1610,7 +1913,7 @@ function sectionsRouter(options) {
1610
1913
  return {
1611
1914
  routes,
1612
1915
  runs,
1613
- levels: makeLevels(graph, root, routeOf, marks)
1916
+ levels: makeLevels(graph, root, routeOf, options)
1614
1917
  };
1615
1918
  }
1616
1919
  };
@@ -1622,9 +1925,10 @@ function sectionsRouter(options) {
1622
1925
  /**
1623
1926
  * This function gives one zoom level for each depth of the tree, and a last level with each node alone.
1624
1927
  * At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
1625
- * 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.
1626
1930
  */
1627
- function makeLevels(graph, root, routeOf, marks) {
1931
+ function makeLevels(graph, root, routeOf, options) {
1628
1932
  const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
1629
1933
  const levels = [];
1630
1934
  const deepest = treeDepth(root);
@@ -1686,21 +1990,30 @@ function makeLevels(graph, root, routeOf, marks) {
1686
1990
  link.count++;
1687
1991
  continue;
1688
1992
  }
1689
- const route = routeOf.get(e.id)?.points ?? [];
1690
- const mark = marks.get(e.id);
1691
- const from = typeof a === "number" ? 0 : mark?.get(a) ?? 0;
1692
- const to = typeof b === "number" ? route.length - 1 : mark?.get(b) ?? route.length - 1;
1693
1993
  links.set(key, {
1694
1994
  source: ida,
1695
1995
  target: idb,
1696
1996
  count: 1,
1697
- points: route.slice(from, to + 1)
1997
+ edge: e.id
1698
1998
  });
1699
1999
  }
1700
- 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({
1701
2014
  depth: d,
1702
2015
  items,
1703
- links: [...links.values()]
2016
+ links: levelLinks(items, pairs, options)
1704
2017
  });
1705
2018
  }
1706
2019
  return levels;
@@ -2139,6 +2452,7 @@ const TOLERANCE = .5;
2139
2452
  /**
2140
2453
  * This function checks the hard rules of a result. Each edge has one route, and each route starts and ends at its nodes.
2141
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.
2142
2456
  *
2143
2457
  * Each route of a bundle starts with its trunk, and an edge is in one bundle at most.
2144
2458
  * Each run has two edges or more, and the route of each edge holds the points of the run in a row.
@@ -2252,9 +2566,26 @@ function checkProperties(graph, result, limit = 100) {
2252
2566
  detail: `level ${level.depth}: the links count ${sum} edges, not ${between}`
2253
2567
  })) return out;
2254
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
+ }
2255
2581
  }
2256
2582
  return out;
2257
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
+ }
2258
2589
  /** This function tells if the polyline `p` holds the points of `part` in a row. */
2259
2590
  function holds(p, part) {
2260
2591
  if (part.length === 0) return true;
@@ -2520,6 +2851,6 @@ function formatTable(rows) {
2520
2851
  })
2521
2852
  ].join("\n");
2522
2853
  }
2523
- export { CLEARANCE_CAP, CornerGraph, FIXTURES, Grid, Obstacles, ROUTERS, SIDE_NORMAL, TOLERANCE, WayFinder, allSections, benchOne, bendCount, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, dist, fixedPortPoint, formatTable, frechet, groupEdges, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, makeFixture, measureQuality, measureStability, nearestSide, nodeIndex, offsetPolyline, orderLanes, pathPoints, placePlug, plugsRouter, pointBoxDistance, pointOnSide, pointSegmentDistance, polylineLength, pullApart, rng, roundCorners, runBenchmark, sameData, sectionsRouter, segmentBounds, segmentBoxDistance, segmentHitsBox, segmentHitsRect, segmentsCross, segmentsOf, sharedRouter, sideAngle, sideToward, splitPoint, straightRouter, treeDepth, trunksRouter, turns };
2854
+ export { CLEARANCE_CAP, CornerGraph, FIXTURES, Grid, Obstacles, ROUTERS, SIDE_NORMAL, TOLERANCE, WayFinder, allSections, benchOne, bendCount, boxExit, buildSections, byCollection, center, checkProperties, cornersRouter, countCrossings, dist, fixedPortPoint, formatTable, frechet, groupEdges, inflate, inflectionCount, inflectionCurve, inflectionSector, laneOrder, laneRoutes, makeFixture, measureQuality, 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, treeDepth, trunksRouter, turns };
2524
2855
 
2525
2856
  //# sourceMappingURL=index.js.map