dsh-model-everything 0.5.1 → 0.5.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/lib/client.js CHANGED
@@ -1617,6 +1617,114 @@ window.__ModuleLoader__.load({
1617
1617
  return out;
1618
1618
  }
1619
1619
 
1620
+ // src/render/trunk.ts
1621
+ var TRUNK_MIN_EDGES = 3;
1622
+ function isVerticalSide(side) {
1623
+ return side === "top" || side === "bottom";
1624
+ }
1625
+ function trunkAt(box, side) {
1626
+ if (side === "top") return box.y - ROUTE_STUB;
1627
+ if (side === "bottom") return box.y + box.height + ROUTE_STUB;
1628
+ if (side === "left") return box.x - ROUTE_STUB;
1629
+ return box.x + box.width + ROUTE_STUB;
1630
+ }
1631
+ function alongSide(point, side) {
1632
+ return isVerticalSide(side) ? point.x : point.y;
1633
+ }
1634
+ function alongNormal(point, side) {
1635
+ return isVerticalSide(side) ? point.y : point.x;
1636
+ }
1637
+ function facesTrunk(point, side, at) {
1638
+ const normal = normalOf(side);
1639
+ const outward = isVerticalSide(side) ? normal.y : normal.x;
1640
+ return (at - alongNormal(point, side)) * outward > 0.5;
1641
+ }
1642
+ function keyOf(end) {
1643
+ return end.to + "\0" + end.entry + "\0" + (end.arrow ?? "");
1644
+ }
1645
+ function clearSegment(boxes, skip, x1, y1, x2, y2) {
1646
+ for (const box of boxes) {
1647
+ if (skip.includes(box)) continue;
1648
+ if (crossesBox(x1, y1, x2, y2, box)) return false;
1649
+ }
1650
+ return true;
1651
+ }
1652
+ function onOneLine(group, side, at, low, high) {
1653
+ if (isVerticalSide(group.side) !== isVerticalSide(side)) return false;
1654
+ if (group.at !== at) return false;
1655
+ return Math.min(group.to, high) - Math.max(group.from, low) > 0.5;
1656
+ }
1657
+ function planTrunks(input) {
1658
+ const groups = [];
1659
+ const groupOf = /* @__PURE__ */ new Map();
1660
+ const buckets = /* @__PURE__ */ new Map();
1661
+ for (const end of input.ends) {
1662
+ if (end.runY === void 0) continue;
1663
+ if (isVerticalSide(end.exit) !== isVerticalSide(end.entry)) continue;
1664
+ const source = input.boxOf(end.from);
1665
+ const target = input.boxOf(end.to);
1666
+ if (source === void 0 || target === void 0) continue;
1667
+ if (source === target) continue;
1668
+ const exit = input.exitPortOf(end.index);
1669
+ if (exit === void 0) continue;
1670
+ const at = trunkAt(target, end.entry);
1671
+ if (!facesTrunk(exit, end.exit, at)) continue;
1672
+ const candidate = { end, source, target, at, exit, branch: alongSide(exit, end.exit) };
1673
+ const key = keyOf(end);
1674
+ const list = buckets.get(key);
1675
+ if (list === void 0) buckets.set(key, [candidate]);
1676
+ else list.push(candidate);
1677
+ }
1678
+ for (const list of buckets.values()) {
1679
+ const bySource = /* @__PURE__ */ new Map();
1680
+ for (const candidate of list) {
1681
+ const seen = bySource.get(candidate.end.from);
1682
+ if (seen === void 0) bySource.set(candidate.end.from, [candidate]);
1683
+ else seen.push(candidate);
1684
+ }
1685
+ const members = list.filter((candidate) => bySource.get(candidate.end.from).length === 1);
1686
+ if (members.length < TRUNK_MIN_EDGES) continue;
1687
+ const target = members[0].target;
1688
+ const side = members[0].end.entry;
1689
+ const at = members[0].at;
1690
+ const entryAlong = isVerticalSide(side) ? target.x + target.width / 2 : target.y + target.height / 2;
1691
+ let lead = members[0];
1692
+ for (const candidate of members) {
1693
+ if (Math.abs(candidate.branch - entryAlong) < Math.abs(lead.branch - entryAlong) - 1e-9) lead = candidate;
1694
+ }
1695
+ let low = entryAlong;
1696
+ let high = entryAlong;
1697
+ for (const candidate of members) {
1698
+ low = Math.min(low, candidate.branch);
1699
+ high = Math.max(high, candidate.branch);
1700
+ }
1701
+ const vertical = isVerticalSide(side);
1702
+ const run = vertical ? [low, at, high, at] : [at, low, at, high];
1703
+ if (!clearSegment(input.boxes, [target], run[0], run[1], run[2], run[3])) continue;
1704
+ let blocked = false;
1705
+ for (const candidate of members) {
1706
+ const drop = vertical ? [candidate.branch, candidate.exit.y, candidate.branch, at] : [candidate.exit.x, candidate.branch, at, candidate.branch];
1707
+ if (!clearSegment(input.boxes, [candidate.source, target], drop[0], drop[1], drop[2], drop[3])) blocked = true;
1708
+ }
1709
+ if (blocked) continue;
1710
+ if (groups.some((group) => onOneLine(group, side, at, low, high))) continue;
1711
+ const id = groups.length;
1712
+ groups.push({
1713
+ id,
1714
+ target: members[0].end.to,
1715
+ side,
1716
+ at,
1717
+ entryAlong,
1718
+ from: low,
1719
+ to: high,
1720
+ lead: lead.end.index,
1721
+ members: members.map((candidate) => candidate.end.index)
1722
+ });
1723
+ for (const candidate of members) groupOf.set(candidate.end.index, id);
1724
+ }
1725
+ return { groups, groupOf };
1726
+ }
1727
+
1620
1728
  // src/render/symbol.ts
1621
1729
  function cubicPoint(curve, t) {
1622
1730
  const u = 1 - t;
@@ -1786,7 +1894,7 @@ window.__ModuleLoader__.load({
1786
1894
  var CAPTION_CHAR_WIDTH = 6.2;
1787
1895
  var PORT_SEPARATION = 14;
1788
1896
  var PORT_SIDES = ["top", "right", "bottom", "left"];
1789
- var isVerticalSide = (side) => side === "top" || side === "bottom";
1897
+ var isVerticalSide2 = (side) => side === "top" || side === "bottom";
1790
1898
  var MULTI_EDGE_SPACING = 20;
1791
1899
  function packPorts(wanted, low, high) {
1792
1900
  const placed = [];
@@ -1802,6 +1910,14 @@ window.__ModuleLoader__.load({
1802
1910
  }
1803
1911
  return placed;
1804
1912
  }
1913
+ function centreRun(placed, low, high) {
1914
+ if (placed.length === 0) return placed;
1915
+ const first = placed[0];
1916
+ const last = placed[placed.length - 1];
1917
+ const middle = (low + high) / 2;
1918
+ const shift = Math.min(high - last, Math.max(low - first, middle - (first + last) / 2));
1919
+ return shift === 0 ? placed : placed.map((value) => value + shift);
1920
+ }
1805
1921
  function roleOfNode(node) {
1806
1922
  if (node.role === "error") return "fail";
1807
1923
  if (node.role === "warning") return "findings";
@@ -1835,42 +1951,65 @@ window.__ModuleLoader__.load({
1835
1951
  var LABEL_HEIGHT = 12;
1836
1952
  var LABEL_PAD = 2;
1837
1953
  var LABEL_STEP = 3;
1838
- function placeLabel(label, from, to, boxes, placed, segments) {
1954
+ function runsOf(points) {
1955
+ const runs = [];
1956
+ for (let index = 0; index + 1 < points.length; index += 1) {
1957
+ const from = points[index];
1958
+ const to = points[index + 1];
1959
+ const length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
1960
+ if (length < 1) continue;
1961
+ runs.push({ from, to, length, index });
1962
+ }
1963
+ return runs.sort((left, right) => right.length - left.length || left.index - right.index);
1964
+ }
1965
+ function placeLabel(label, points, boxes, placed, otherRoutes) {
1839
1966
  const width = textWidth(label);
1840
1967
  const height = LABEL_HEIGHT;
1841
1968
  const spotAt = (x, y) => ({ x, y, width, height });
1842
1969
  const hitsBox = (spot, box) => spot.x - spot.width / 2 < box.x + box.width + LABEL_PAD && spot.x + spot.width / 2 > box.x - LABEL_PAD && spot.y - spot.height / 2 < box.y + box.height + LABEL_PAD && spot.y + spot.height / 2 > box.y - LABEL_PAD;
1843
1970
  const hitsSpot = (spot, other) => spot.x - spot.width / 2 < other.x + other.width / 2 + LABEL_PAD && spot.x + spot.width / 2 > other.x - other.width / 2 - LABEL_PAD && spot.y - spot.height / 2 < other.y + other.height / 2 + LABEL_PAD && spot.y + spot.height / 2 > other.y - other.height / 2 - LABEL_PAD;
1844
1971
  const hitsSegment = (spot, segment) => spot.x + spot.width / 2 > Math.min(segment.x1, segment.x2) - LABEL_PAD && spot.x - spot.width / 2 < Math.max(segment.x1, segment.x2) + LABEL_PAD && spot.y + spot.height / 2 > Math.min(segment.y1, segment.y2) - LABEL_PAD && spot.y - spot.height / 2 < Math.max(segment.y1, segment.y2) + LABEL_PAD;
1845
- const clearOfBoxes = (spot) => {
1846
- for (const box of boxes) if (hitsBox(spot, box)) return false;
1847
- for (const earlier of placed) if (hitsSpot(spot, earlier)) return false;
1848
- return true;
1849
- };
1850
- const clearOfEdges = (spot) => {
1851
- for (const segment of segments) if (hitsSegment(spot, segment)) return false;
1852
- return true;
1972
+ const collisionsAt = (spot, run) => {
1973
+ let boxHits = 0;
1974
+ let labelHits = 0;
1975
+ let edgeHits = 0;
1976
+ for (const box of boxes) if (hitsBox(spot, box)) boxHits += 1;
1977
+ for (const earlier of placed) if (hitsSpot(spot, earlier)) labelHits += 1;
1978
+ for (const route of otherRoutes) {
1979
+ for (const segment of route) {
1980
+ if (!hitsSegment(spot, segment)) continue;
1981
+ edgeHits += 1;
1982
+ break;
1983
+ }
1984
+ }
1985
+ return { run, collisions: boxHits + labelHits + edgeHits, boxes: boxHits, labels: labelHits, edges: edgeHits };
1853
1986
  };
1854
- const middle = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
1855
- const dx = to.x - from.x;
1856
- const dy = to.y - from.y;
1857
- const length = Math.abs(dx) + Math.abs(dy);
1858
- const ux = dx / length;
1859
- const uy = dy / length;
1860
- const along = Math.abs(ux) * width / 2 + Math.abs(uy) * height / 2;
1861
- const bound = Math.max(0, length / 2 - along);
1862
- const walk = [0];
1863
- for (let step = LABEL_STEP; step <= bound + 1e-3; step += LABEL_STEP) walk.push(step, -step);
1864
- const at = (offset) => spotAt(middle.x + ux * offset, middle.y + uy * offset);
1865
- for (const offset of walk) {
1866
- const spot = at(offset);
1867
- if (clearOfBoxes(spot) && clearOfEdges(spot)) return spot;
1868
- }
1869
- for (const offset of walk) {
1870
- const spot = at(offset);
1871
- if (clearOfBoxes(spot)) return spot;
1872
- }
1873
- return at(0);
1987
+ const runs = runsOf(points);
1988
+ if (runs.length === 0) {
1989
+ const point = points[0] ?? { x: 0, y: 0 };
1990
+ return { ...spotAt(point.x, point.y), run: -1, collisions: 0, boxes: 0, labels: 0, edges: 0 };
1991
+ }
1992
+ let best;
1993
+ for (const [runIndex, run] of runs.entries()) {
1994
+ const dx = run.to.x - run.from.x;
1995
+ const dy = run.to.y - run.from.y;
1996
+ const ux = dx / run.length;
1997
+ const uy = dy / run.length;
1998
+ const middle = { x: (run.from.x + run.to.x) / 2, y: (run.from.y + run.to.y) / 2 };
1999
+ const along = Math.abs(ux) * width / 2 + Math.abs(uy) * height / 2;
2000
+ const bound = Math.max(0, run.length / 2 - along);
2001
+ const walk = [0];
2002
+ for (let step = LABEL_STEP; step <= bound + 1e-3; step += LABEL_STEP) walk.push(step, -step);
2003
+ for (const offset of walk) {
2004
+ const spot = spotAt(middle.x + ux * offset, middle.y + uy * offset);
2005
+ const placement = collisionsAt(spot, runIndex);
2006
+ if (placement.collisions === 0) return { ...spot, ...placement };
2007
+ if (best === void 0 || placement.collisions < best.collisions || placement.collisions === best.collisions && placement.boxes + placement.labels < best.boxes + best.labels) {
2008
+ best = { ...spot, ...placement };
2009
+ }
2010
+ }
2011
+ }
2012
+ return best;
1874
2013
  }
1875
2014
  function drawnExtent(shapes) {
1876
2015
  let width = 0;
@@ -1898,10 +2037,24 @@ window.__ModuleLoader__.load({
1898
2037
  case "title":
1899
2038
  consider(textWidth(shape.lines?.[0] ?? "") + shape.x, shape.y + 12);
1900
2039
  break;
2040
+ case "junction":
2041
+ break;
1901
2042
  }
1902
2043
  }
1903
2044
  return { width, height };
1904
2045
  }
2046
+ function boxesCrossedBy(points, from, to, boxes) {
2047
+ const crossed = [];
2048
+ for (let index = 0; index + 1 < points.length; index += 1) {
2049
+ const a = points[index];
2050
+ const b = points[index + 1];
2051
+ for (const box of boxes) {
2052
+ if (box === from || box === to) continue;
2053
+ if (crossesBox(a.x, a.y, b.x, b.y, box)) crossed.push(box);
2054
+ }
2055
+ }
2056
+ return crossed;
2057
+ }
1905
2058
  function paintWith(diagram, overrides = {}, options = {}) {
1906
2059
  const colours = palette(overrides, options);
1907
2060
  const shapes = [];
@@ -1965,21 +2118,100 @@ window.__ModuleLoader__.load({
1965
2118
  endsAt.set(edge.from, (endsAt.get(edge.from) ?? 0) + 1);
1966
2119
  endsAt.set(edge.to, (endsAt.get(edge.to) ?? 0) + 1);
1967
2120
  }
2121
+ const spanOf2 = (boxId, side, corner) => {
2122
+ const box = endBox(boxId);
2123
+ const vertical = side === "top" || side === "bottom";
2124
+ const middle = vertical ? box.x + box.width / 2 : box.y + box.height / 2;
2125
+ const within = (low, high) => ({ low, high: Math.max(low, high) });
2126
+ switch (shapeOf(boxId)) {
2127
+ // The four vertices of a diamond, and the four cardinal points of an ellipse, are the middles of its sides.
2128
+ case "diamond":
2129
+ case "circle":
2130
+ return within(middle, middle);
2131
+ case "hexagon":
2132
+ return vertical ? within(box.x + hexagonCut(box.width), box.x + box.width - hexagonCut(box.width)) : within(middle, middle);
2133
+ case "stadium": {
2134
+ const radius = stadiumRadius(box);
2135
+ return vertical ? within(box.x + radius, box.x + box.width - radius) : within(middle, middle);
2136
+ }
2137
+ case "cylinder": {
2138
+ if (vertical) return within(middle, middle);
2139
+ const cap = cylinderCaps(box).cap;
2140
+ return within(box.y + cap, box.y + box.height - cap);
2141
+ }
2142
+ default: {
2143
+ const inset = corner ? 2 : vertical ? PORT_INSET : 8;
2144
+ return vertical ? within(box.x + inset, box.x + box.width - inset) : within(box.y + inset, box.y + box.height - inset);
2145
+ }
2146
+ }
2147
+ };
2148
+ const clampTo = (boxId, side, value, corner) => {
2149
+ const span = spanOf2(boxId, side, corner);
2150
+ return Math.min(span.high, Math.max(span.low, value));
2151
+ };
2152
+ const pointOn = (boxId, side, along) => {
2153
+ const box = endBox(boxId);
2154
+ if (side === "left") return { x: box.x, y: along };
2155
+ if (side === "right") return { x: box.x + box.width, y: along };
2156
+ if (side === "top") return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "top").y : box.y };
2157
+ return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "bottom").y : box.y + box.height };
2158
+ };
1968
2159
  const claimed = /* @__PURE__ */ new Map();
1969
2160
  const claimedAt = /* @__PURE__ */ new Map();
1970
2161
  const claim = (id, index, end, side) => {
1971
2162
  claimed.set(endKey(index, end), side);
1972
- claimedAt.set(id, [...claimedAt.get(id) ?? [], side]);
2163
+ claimedAt.set(id, [...claimedAt.get(id) ?? [], { index, side }]);
2164
+ };
2165
+ const takenSides = (id, except) => (claimedAt.get(id) ?? []).filter((entry) => entry.index !== except).map((entry) => entry.side);
2166
+ const sideMiddle = (box, side) => isVerticalSide2(side) ? box.x + box.width / 2 : box.y + box.height / 2;
2167
+ const sidePoint = (boxId, side) => pointOn(boxId, side, sideMiddle(endBox(boxId), side));
2168
+ const facesAway2 = (box, other, side) => {
2169
+ const out = normalOf(side);
2170
+ const dx = other.x + other.width / 2 - (box.x + box.width / 2);
2171
+ const dy = other.y + other.height / 2 - (box.y + box.height / 2);
2172
+ return out.x * dx + out.y * dy < 0;
1973
2173
  };
1974
- const bestSide = (id, other) => {
2174
+ const stubAlong = (pinnedId, pinnedSide, freeSide) => {
2175
+ const point = sidePoint(pinnedId, pinnedSide);
2176
+ const out = normalOf(pinnedSide);
2177
+ return isVerticalSide2(freeSide) ? point.x + out.x * ROUTE_STUB : point.y + out.y * ROUTE_STUB;
2178
+ };
2179
+ const offers = (boxId, side, value) => {
2180
+ const span = spanOf2(boxId, side, true);
2181
+ return value >= span.low - 0.01 && value <= span.high + 0.01;
2182
+ };
2183
+ const bendBudget = (fromId, toId, exit, entry, index) => {
2184
+ const from = endBox(fromId);
2185
+ const to = endBox(toId);
2186
+ if (facesAway2(from, to, exit) || facesAway2(to, from, entry)) return 4;
2187
+ if (isVerticalSide2(exit) === isVerticalSide2(entry)) {
2188
+ const out = normalOf(exit);
2189
+ const into = normalOf(entry);
2190
+ const sameWay = out.x === -into.x && out.y === -into.y;
2191
+ const fromSpan = spanOf2(fromId, exit, true);
2192
+ const toSpan = spanOf2(toId, entry, true);
2193
+ const meet = fromSpan.low <= toSpan.high + 0.01 && toSpan.low <= fromSpan.high + 0.01;
2194
+ return sameWay && meet ? 0 : 2;
2195
+ }
2196
+ const horizontalId = isVerticalSide2(exit) ? toId : fromId;
2197
+ const horizontalSide = isVerticalSide2(exit) ? entry : exit;
2198
+ const verticalId = isVerticalSide2(exit) ? fromId : toId;
2199
+ const verticalSide = isVerticalSide2(exit) ? exit : entry;
2200
+ if (!offers(verticalId, verticalSide, stubAlong(horizontalId, horizontalSide, verticalSide))) return 2;
2201
+ const crowded = takenSides(fromId, index).includes(exit) || takenSides(toId, index).includes(entry);
2202
+ return crowded ? 2 : 1;
2203
+ };
2204
+ const bestSide = (id, otherId, end, partner, index) => {
1975
2205
  const box = endBox(id);
2206
+ const other = endBox(otherId);
1976
2207
  const centre = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
1977
2208
  const toward = { x: other.x + other.width / 2 - centre.x, y: other.y + other.height / 2 - centre.y };
1978
2209
  const reach = Math.hypot(toward.x, toward.y);
1979
- const taken = claimedAt.get(id) ?? [];
2210
+ const taken = takenSides(id, index);
1980
2211
  let best;
1981
2212
  for (const side of PORT_SIDES) {
1982
2213
  if (taken.includes(side)) continue;
2214
+ const bends = end === "exit" ? bendBudget(id, otherId, side, partner, index) : bendBudget(otherId, id, partner, side, index);
1983
2215
  const normal = normalOf(side);
1984
2216
  const score = reach === 0 ? 0 : (normal.x * toward.x + normal.y * toward.y) / reach;
1985
2217
  const point = anchor(box, side);
@@ -1987,11 +2219,43 @@ window.__ModuleLoader__.load({
1987
2219
  Math.max(other.x - point.x, 0, point.x - (other.x + other.width)),
1988
2220
  Math.max(other.y - point.y, 0, point.y - (other.y + other.height))
1989
2221
  );
1990
- const better = best === void 0 || score > best.score + 1e-9 || Math.abs(score - best.score) <= 1e-9 && near < best.near - 1e-9;
1991
- if (better) best = { side, score, near };
2222
+ const partnerPoint = anchor(other, partner);
2223
+ const ground = Math.abs(point.x - partnerPoint.x) + Math.abs(point.y - partnerPoint.y);
2224
+ const better = best === void 0 || bends < best.bends || bends === best.bends && (score > best.score + 1e-9 || Math.abs(score - best.score) <= 1e-9 && (near < best.near - 1e-9 || Math.abs(near - best.near) <= 1e-9 && ground < best.ground - 1e-9));
2225
+ if (better) best = { side, bends, score, near, ground };
1992
2226
  }
1993
2227
  return best?.side;
1994
2228
  };
2229
+ const bestPair = (fromId, toId, index) => {
2230
+ const approach = approachSides(endBox(fromId), endBox(toId));
2231
+ const pairs = [
2232
+ { exit: "bottom", entry: "top" },
2233
+ { exit: "top", entry: "bottom" },
2234
+ { exit: "right", entry: "left" },
2235
+ { exit: "left", entry: "right" }
2236
+ ];
2237
+ let best = approach;
2238
+ let bestBends = bendBudget(fromId, toId, approach.exit, approach.entry, index);
2239
+ for (const pair of pairs) {
2240
+ const bends = bendBudget(fromId, toId, pair.exit, pair.entry, index);
2241
+ if (bends >= bestBends) continue;
2242
+ best = pair;
2243
+ bestBends = bends;
2244
+ }
2245
+ return best;
2246
+ };
2247
+ const bestEnd = (id, otherId, end, partner, index) => {
2248
+ const approach = end === "exit" ? approachSides(endBox(id), endBox(otherId)).exit : approachSides(endBox(otherId), endBox(id)).entry;
2249
+ let best = approach;
2250
+ let bestBends = end === "exit" ? bendBudget(id, otherId, approach, partner, index) : bendBudget(otherId, id, partner, approach, index);
2251
+ for (const side of PORT_SIDES) {
2252
+ const bends = end === "exit" ? bendBudget(id, otherId, side, partner, index) : bendBudget(otherId, id, partner, side, index);
2253
+ if (bends >= bestBends) continue;
2254
+ best = side;
2255
+ bestBends = bends;
2256
+ }
2257
+ return best;
2258
+ };
1995
2259
  diagram.edges.forEach((edge, index) => {
1996
2260
  if (edge.from === edge.to) return;
1997
2261
  const source = endBox(edge.from);
@@ -2018,7 +2282,9 @@ window.__ModuleLoader__.load({
2018
2282
  const id = end === "exit" ? edge.from : edge.to;
2019
2283
  if ((endsAt.get(id) ?? 0) < 2) continue;
2020
2284
  if (!hasDiscretePorts(shapeOf(id))) continue;
2021
- const side = bestSide(id, end === "exit" ? target : source);
2285
+ const approached = approachSides(source, target);
2286
+ const partner = claimed.get(endKey(index, end === "exit" ? "entry" : "exit")) ?? (end === "exit" ? approached.entry : approached.exit);
2287
+ const side = bestSide(id, end === "exit" ? edge.to : edge.from, end, partner, index);
2022
2288
  if (side === void 0) continue;
2023
2289
  claim(id, index, end, side);
2024
2290
  }
@@ -2027,20 +2293,22 @@ window.__ModuleLoader__.load({
2027
2293
  const source = endBox(edge.from);
2028
2294
  const target = endBox(edge.to);
2029
2295
  if (source === void 0 || target === void 0 || edge.from === edge.to) return void 0;
2030
- const sides2 = placement.across.has(index) ? { exit: "right", entry: "left" } : approachSides(source, target);
2296
+ if (placement.across.has(index)) return { exit: "right", entry: "left" };
2031
2297
  const lane = placement.lanes.get(index);
2032
- if (lane === void 0 || lane.points.length === 0) {
2298
+ const down = target.y > source.y;
2299
+ if (lane !== void 0 && lane.points.length > 0) {
2033
2300
  return {
2034
- exit: claimed.get(endKey(index, "exit")) ?? sides2.exit,
2035
- entry: claimed.get(endKey(index, "entry")) ?? sides2.entry
2301
+ exit: claimed.get(endKey(index, "exit")) ?? (down ? "bottom" : "top"),
2302
+ entry: claimed.get(endKey(index, "entry")) ?? (down ? "top" : "bottom"),
2303
+ at: lane.points[0].x
2036
2304
  };
2037
2305
  }
2038
- const down = target.y > source.y;
2039
- return {
2040
- exit: claimed.get(endKey(index, "exit")) ?? (down ? "bottom" : "top"),
2041
- entry: claimed.get(endKey(index, "entry")) ?? (down ? "top" : "bottom"),
2042
- at: lane.points[0].x
2043
- };
2306
+ const exit = claimed.get(endKey(index, "exit"));
2307
+ const entry = claimed.get(endKey(index, "entry"));
2308
+ if (exit !== void 0 && entry !== void 0) return { exit, entry };
2309
+ if (exit !== void 0) return { exit, entry: bestEnd(edge.to, edge.from, "entry", exit, index) };
2310
+ if (entry !== void 0) return { exit: bestEnd(edge.from, edge.to, "exit", entry, index), entry };
2311
+ return bestPair(edge.from, edge.to, index);
2044
2312
  });
2045
2313
  const laneClear = (points, x, from, to) => {
2046
2314
  for (let at = 0; at + 1 < points.length; at += 1) {
@@ -2079,50 +2347,12 @@ window.__ModuleLoader__.load({
2079
2347
  }
2080
2348
  const wanted = /* @__PURE__ */ new Map();
2081
2349
  const slotKey = (boxId, side, index) => boxId + ":" + side + ":" + String(index);
2082
- const spanOf2 = (boxId, side, corner) => {
2083
- const box = endBox(boxId);
2084
- const vertical = side === "top" || side === "bottom";
2085
- const middle = vertical ? box.x + box.width / 2 : box.y + box.height / 2;
2086
- const within = (low, high) => ({ low, high: Math.max(low, high) });
2087
- switch (shapeOf(boxId)) {
2088
- // The four vertices of a diamond, and the four cardinal points of an ellipse, are the middles of its sides.
2089
- case "diamond":
2090
- case "circle":
2091
- return within(middle, middle);
2092
- case "hexagon":
2093
- return vertical ? within(box.x + hexagonCut(box.width), box.x + box.width - hexagonCut(box.width)) : within(middle, middle);
2094
- case "stadium": {
2095
- const radius = stadiumRadius(box);
2096
- return vertical ? within(box.x + radius, box.x + box.width - radius) : within(middle, middle);
2097
- }
2098
- case "cylinder": {
2099
- if (vertical) return within(middle, middle);
2100
- const cap = cylinderCaps(box).cap;
2101
- return within(box.y + cap, box.y + box.height - cap);
2102
- }
2103
- default: {
2104
- const inset = corner ? 2 : vertical ? PORT_INSET : 8;
2105
- return vertical ? within(box.x + inset, box.x + box.width - inset) : within(box.y + inset, box.y + box.height - inset);
2106
- }
2107
- }
2108
- };
2109
- const clampTo = (boxId, side, value, corner) => {
2110
- const span = spanOf2(boxId, side, corner);
2111
- return Math.min(span.high, Math.max(span.low, value));
2112
- };
2113
- const pointOn = (boxId, side, along) => {
2114
- const box = endBox(boxId);
2115
- if (side === "left") return { x: box.x, y: along };
2116
- if (side === "right") return { x: box.x + box.width, y: along };
2117
- if (side === "top") return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "top").y : box.y };
2118
- return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "bottom").y : box.y + box.height };
2119
- };
2120
2350
  for (const [index, entry] of facing.entries()) {
2121
2351
  if (entry === void 0) continue;
2122
2352
  const edge = diagram.edges[index];
2123
2353
  const from = endBox(edge.from);
2124
2354
  const to = endBox(edge.to);
2125
- const middle = (box, side) => isVerticalSide(side) ? box.x + box.width / 2 : box.y + box.height / 2;
2355
+ const middle = (box, side) => isVerticalSide2(side) ? box.x + box.width / 2 : box.y + box.height / 2;
2126
2356
  const fromMiddle = middle(from, entry.exit);
2127
2357
  const toMiddle = middle(to, entry.entry);
2128
2358
  const corner = entry.at !== void 0;
@@ -2133,14 +2363,14 @@ window.__ModuleLoader__.load({
2133
2363
  const chosen = entry.at + (laneShift.get(index) ?? 0);
2134
2364
  exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
2135
2365
  entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
2136
- } else if (isVerticalSide(entry.exit) === isVerticalSide(entry.entry)) {
2366
+ } else if (isVerticalSide2(entry.exit) === isVerticalSide2(entry.entry)) {
2137
2367
  const target = spanOf2(edge.to, entry.entry, false);
2138
2368
  const chosen = (fromMiddle >= target.low && fromMiddle <= target.high ? fromMiddle : clampTo(edge.from, entry.exit, toMiddle, false)) + offset;
2139
2369
  exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
2140
2370
  entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
2141
2371
  } else {
2142
- exitWanted = hasDiscretePorts(shapeOf(edge.from)) ? fromMiddle : clampTo(edge.from, entry.exit, anchor(to, entry.entry).x + normalOf(entry.entry).x * ROUTE_STUB, false) + offset;
2143
- entryWanted = hasDiscretePorts(shapeOf(edge.to)) ? toMiddle : clampTo(edge.to, entry.entry, anchor(from, entry.exit).x + normalOf(entry.exit).x * ROUTE_STUB, false) + offset;
2372
+ exitWanted = hasDiscretePorts(shapeOf(edge.from)) ? fromMiddle : clampTo(edge.from, entry.exit, stubAlong(edge.to, entry.entry, entry.exit), false) + offset;
2373
+ entryWanted = hasDiscretePorts(shapeOf(edge.to)) ? toMiddle : clampTo(edge.to, entry.entry, stubAlong(edge.from, entry.exit, entry.entry), false) + offset;
2144
2374
  }
2145
2375
  wanted.set(slotKey(edge.from, entry.exit, index), exitWanted);
2146
2376
  wanted.set(slotKey(edge.to, entry.entry, index), entryWanted);
@@ -2189,6 +2419,109 @@ window.__ModuleLoader__.load({
2189
2419
  }
2190
2420
  if (!moved) break;
2191
2421
  }
2422
+ const trunkEnds = [];
2423
+ diagram.edges.forEach((edge, index) => {
2424
+ const entry = facing[index];
2425
+ if (entry === void 0) return;
2426
+ const runY = placement.runs.get(index);
2427
+ trunkEnds.push({
2428
+ index,
2429
+ from: edge.from,
2430
+ to: edge.to,
2431
+ exit: entry.exit,
2432
+ entry: entry.entry,
2433
+ ...edge.arrow === void 0 ? {} : { arrow: edge.arrow },
2434
+ ...runY === void 0 ? {} : { runY }
2435
+ });
2436
+ });
2437
+ const planFor = (exitPortOf) => planTrunks({
2438
+ ends: trunkEnds,
2439
+ // Only a node's own box is a trunk target: an arrow into a boundary points past it, not at it.
2440
+ boxOf: (id) => boxes.get(id),
2441
+ boxes: boxList,
2442
+ exitPortOf
2443
+ });
2444
+ const packedExit = (index) => {
2445
+ const entry = facing[index];
2446
+ if (entry === void 0) return void 0;
2447
+ const edge = diagram.edges[index];
2448
+ const coordinate = placedPort.get(slotKey(edge.from, entry.exit, index));
2449
+ return coordinate === void 0 ? void 0 : pointOn(edge.from, entry.exit, coordinate);
2450
+ };
2451
+ const planned = planFor(packedExit);
2452
+ const onLane = (index, coordinate) => {
2453
+ if (planned.groupOf.has(index)) return false;
2454
+ const lane = placement.lanes.get(index);
2455
+ if (lane === void 0 || lane.points.length === 0) return false;
2456
+ return Math.abs(lane.points[0].x - coordinate) <= 0.5;
2457
+ };
2458
+ const rows = [];
2459
+ const rowOf = /* @__PURE__ */ new Map();
2460
+ for (const [key, list] of sides) {
2461
+ const [boxId, side] = key.split(":");
2462
+ const span = spanOf2(boxId, side, true);
2463
+ const slots = [...list].map((entry) => ({ index: entry.index, at: placedPort.get(slotKey(boxId, side, entry.index)) })).sort((left, right) => left.at - right.at || left.index - right.index);
2464
+ const centred = centreRun(
2465
+ slots.map((slot) => slot.at),
2466
+ span.low,
2467
+ span.high
2468
+ );
2469
+ const first = slots[0];
2470
+ const row = {
2471
+ boxId,
2472
+ side,
2473
+ indexes: slots.map((slot) => slot.index),
2474
+ at: slots.map((slot) => slot.at),
2475
+ position: new Map(slots.map((slot, at) => [slot.index, at])),
2476
+ shift: centred[0] - first.at,
2477
+ allowed: true
2478
+ };
2479
+ rows.push(row);
2480
+ rowOf.set(key, row);
2481
+ }
2482
+ const movedAt = (row, at) => row.at[at] + (row.allowed ? row.shift : 0);
2483
+ const partnerOf = (boxId, side, index) => {
2484
+ const entry = facing[index];
2485
+ if (entry === void 0) return void 0;
2486
+ const edge = diagram.edges[index];
2487
+ const isExit = edge.from === boxId && entry.exit === side;
2488
+ const groupId = planned.groupOf.get(index);
2489
+ if (groupId !== void 0) {
2490
+ if (!isExit) return void 0;
2491
+ const group = planned.groups[groupId];
2492
+ if (isVerticalSide2(side) !== isVerticalSide2(group.side)) return void 0;
2493
+ return { coordinate: group.entryAlong };
2494
+ }
2495
+ const partnerId = isExit ? edge.to : edge.from;
2496
+ const partnerSide = isExit ? entry.entry : entry.exit;
2497
+ if (isVerticalSide2(side) !== isVerticalSide2(partnerSide)) return void 0;
2498
+ const coordinate = placedPort.get(slotKey(partnerId, partnerSide, index));
2499
+ if (coordinate === void 0) return void 0;
2500
+ const row = rowOf.get(partnerId + ":" + partnerSide);
2501
+ return { coordinate, ...row === void 0 ? {} : { row, at: row.position.get(index) } };
2502
+ };
2503
+ for (let pass = 0; pass < rows.length; pass += 1) {
2504
+ let refused = false;
2505
+ for (const row of rows) {
2506
+ if (!row.allowed || row.shift === 0) continue;
2507
+ const pinned = row.indexes.some((index, at) => {
2508
+ if (onLane(index, row.at[at])) return true;
2509
+ const partner = partnerOf(row.boxId, row.side, index);
2510
+ if (partner === void 0) return false;
2511
+ if (Math.abs(row.at[at] - partner.coordinate) > 0.5) return false;
2512
+ const partnerAt = partner.row === void 0 ? partner.coordinate : movedAt(partner.row, partner.at);
2513
+ return Math.abs(movedAt(row, at) - partnerAt) > 0.5;
2514
+ });
2515
+ if (!pinned) continue;
2516
+ row.allowed = false;
2517
+ refused = true;
2518
+ }
2519
+ if (!refused) break;
2520
+ }
2521
+ for (const row of rows) {
2522
+ if (!row.allowed || row.shift === 0) continue;
2523
+ row.indexes.forEach((index, at) => placedPort.set(slotKey(row.boxId, row.side, index), movedAt(row, at)));
2524
+ }
2192
2525
  const portOf = (boxId, side, edgeIndex) => {
2193
2526
  const coordinate = placedPort.get(slotKey(boxId, side, edgeIndex));
2194
2527
  return pointOn(boxId, side, coordinate);
@@ -2204,16 +2537,9 @@ window.__ModuleLoader__.load({
2204
2537
  entrySide: entry.entry
2205
2538
  });
2206
2539
  }
2207
- const longestRun = (points) => {
2208
- let best = { from: points[0], to: points[points.length - 1], length: -1 };
2209
- for (let index = 0; index + 1 < points.length; index += 1) {
2210
- const from = points[index];
2211
- const to = points[index + 1];
2212
- const length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
2213
- if (length > best.length) best = { from, to, length };
2214
- }
2215
- return best;
2216
- };
2540
+ const trunks = planFor((index) => anchorsFor.get(index)?.exit);
2541
+ const sharedEntry = /* @__PURE__ */ new Map();
2542
+ for (const group of trunks.groups) sharedEntry.set(group.id, pointOn(group.target, group.side, group.entryAlong));
2217
2543
  const pointsOfRoute = (shape) => shape.kind === "path" ? shape.points : [{ x: shape.x1, y: shape.y1 }, { x: shape.x2, y: shape.y2 }];
2218
2544
  const segmentsOfRoute = (shape) => {
2219
2545
  const points = pointsOfRoute(shape);
@@ -2225,7 +2551,7 @@ window.__ModuleLoader__.load({
2225
2551
  }
2226
2552
  return out;
2227
2553
  };
2228
- const paintPath = (edge, points, colour) => {
2554
+ const paintPath = (edgeIndex, edge, points, colour) => {
2229
2555
  const shape = {
2230
2556
  kind: "path",
2231
2557
  points,
@@ -2237,11 +2563,11 @@ window.__ModuleLoader__.load({
2237
2563
  ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2238
2564
  };
2239
2565
  shapes.push(shape);
2240
- routes.push(shape);
2566
+ routes.push({ shape, edge: edgeIndex, fixed: false });
2241
2567
  };
2242
- const paintRoute = (edge, points, colour) => {
2568
+ const paintRoute = (edgeIndex, edge, points, colour) => {
2243
2569
  if (points.length > 2) {
2244
- paintPath(edge, points, colour);
2570
+ paintPath(edgeIndex, edge, points, colour);
2245
2571
  return;
2246
2572
  }
2247
2573
  const exit = points[0];
@@ -2260,38 +2586,31 @@ window.__ModuleLoader__.load({
2260
2586
  ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2261
2587
  };
2262
2588
  shapes.push(shape);
2263
- routes.push(shape);
2589
+ routes.push({ shape, edge: edgeIndex, fixed: false });
2264
2590
  };
2591
+ const clearOfBoxes = (points, from, to) => boxesCrossedBy(points, from, to, boxList).length === 0;
2265
2592
  const runRoute = (ports, runY) => {
2266
- const out = isVerticalSide(ports.exitSide) ? 0 : normalOf(ports.exitSide).x * ROUTE_STUB;
2267
- const into = isVerticalSide(ports.entrySide) ? 0 : normalOf(ports.entrySide).x * ROUTE_STUB;
2593
+ const out = isVerticalSide2(ports.exitSide) ? 0 : normalOf(ports.exitSide).x * ROUTE_STUB;
2594
+ const into = isVerticalSide2(ports.entrySide) ? 0 : normalOf(ports.entrySide).x * ROUTE_STUB;
2595
+ const exitRun = out !== 0 && into === 0 && Math.sign(ports.entry.x - ports.exit.x) === Math.sign(out) ? ports.entry.x : ports.exit.x + out;
2596
+ const entryRun = into !== 0 && out === 0 && Math.sign(ports.exit.x - ports.entry.x) === Math.sign(into) ? ports.exit.x : ports.entry.x + into;
2268
2597
  const points = [ports.exit];
2269
- if (out !== 0) points.push({ x: ports.exit.x + out, y: ports.exit.y });
2270
- points.push({ x: ports.exit.x + out, y: runY }, { x: ports.entry.x + into, y: runY });
2271
- if (into !== 0) points.push({ x: ports.entry.x + into, y: ports.entry.y });
2598
+ if (out !== 0) points.push({ x: exitRun, y: ports.exit.y });
2599
+ points.push({ x: exitRun, y: runY }, { x: entryRun, y: runY });
2600
+ if (into !== 0) points.push({ x: entryRun, y: ports.entry.y });
2272
2601
  points.push(ports.entry);
2273
2602
  return simplify(points);
2274
2603
  };
2275
2604
  const runFits = (ports, runY) => {
2276
- if (isVerticalSide(ports.exitSide) && Math.abs(runY - ports.exit.y) > 0.5) {
2605
+ if (isVerticalSide2(ports.exitSide) && Math.abs(runY - ports.exit.y) > 0.5) {
2277
2606
  if (Math.sign(runY - ports.exit.y) !== Math.sign(normalOf(ports.exitSide).y)) return false;
2278
2607
  }
2279
- if (isVerticalSide(ports.entrySide) && Math.abs(ports.entry.y - runY) > 0.5) {
2608
+ if (isVerticalSide2(ports.entrySide) && Math.abs(ports.entry.y - runY) > 0.5) {
2280
2609
  if (Math.sign(ports.entry.y - runY) !== -Math.sign(normalOf(ports.entrySide).y)) return false;
2281
2610
  }
2282
2611
  return true;
2283
2612
  };
2284
- const clearsBoxes = (points, from, to) => {
2285
- for (let at = 0; at + 1 < points.length; at += 1) {
2286
- const a = points[at];
2287
- const b = points[at + 1];
2288
- for (const box of boxList) {
2289
- if (box === from || box === to) continue;
2290
- if (crossesBox(a.x, a.y, b.x, b.y, box)) return false;
2291
- }
2292
- }
2293
- return true;
2294
- };
2613
+ const junctions = [];
2295
2614
  for (const [edgeIndex, edge] of diagram.edges.entries()) {
2296
2615
  const source = endBox(edge.from);
2297
2616
  const target = endBox(edge.to);
@@ -2300,7 +2619,7 @@ window.__ModuleLoader__.load({
2300
2619
  if (edge.from === edge.to) {
2301
2620
  const x = source.x + source.width;
2302
2621
  const y = source.y + source.height / 2;
2303
- shapes.push({
2622
+ const shape = {
2304
2623
  kind: "path",
2305
2624
  points: [
2306
2625
  { x, y: y - 9 },
@@ -2316,10 +2635,25 @@ window.__ModuleLoader__.load({
2316
2635
  colour,
2317
2636
  ...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
2318
2637
  ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2319
- });
2638
+ };
2639
+ shapes.push(shape);
2640
+ routes.push({ shape, edge: edgeIndex, fixed: true });
2320
2641
  continue;
2321
2642
  }
2322
2643
  const anchors = anchorsFor.get(edgeIndex);
2644
+ const groupId = trunks.groupOf.get(edgeIndex);
2645
+ if (groupId !== void 0 && anchors !== void 0) {
2646
+ const group = trunks.groups[groupId];
2647
+ const entry = sharedEntry.get(groupId);
2648
+ const branch = isVerticalSide2(group.side) ? [anchors.exit, { x: anchors.exit.x, y: group.at }, { x: entry.x, y: group.at }, entry] : [anchors.exit, { x: group.at, y: anchors.exit.y }, { x: group.at, y: entry.y }, entry];
2649
+ const points = simplify(branch);
2650
+ if (clearOfBoxes(points, source, target)) {
2651
+ paintRoute(edgeIndex, edge, points, colour);
2652
+ const meet = isVerticalSide2(group.side) ? { x: anchors.exit.x, y: group.at } : { x: group.at, y: anchors.exit.y };
2653
+ junctions.push({ kind: "junction", x: meet.x, y: meet.y, colour, trunk: group.id });
2654
+ continue;
2655
+ }
2656
+ }
2323
2657
  const lane = placement.lanes.get(edgeIndex);
2324
2658
  if (lane !== void 0 && lane.points.length > 0 && anchors !== void 0) {
2325
2659
  const shift = laneShift.get(edgeIndex) ?? 0;
@@ -2334,32 +2668,76 @@ window.__ModuleLoader__.load({
2334
2668
  }
2335
2669
  if (Math.abs(previous.x - anchors.entry.x) > 0.5 && Math.abs(previous.y - anchors.entry.y) > 0.5) points.push({ x: anchors.entry.x, y: previous.y });
2336
2670
  points.push(anchors.entry);
2337
- paintPath(edge, simplify(points), colour);
2338
- continue;
2671
+ const simplified = simplify(points);
2672
+ if (clearOfBoxes(simplified, source, target)) {
2673
+ paintPath(edgeIndex, edge, simplified, colour);
2674
+ continue;
2675
+ }
2339
2676
  }
2340
2677
  const runY = placement.runs.get(edgeIndex);
2341
2678
  if (runY !== void 0 && anchors !== void 0 && runFits(anchors, runY)) {
2342
2679
  const points = runRoute(anchors, runY);
2343
- const mixed = isVerticalSide(anchors.exitSide) !== isVerticalSide(anchors.entrySide);
2344
- if (!mixed || clearsBoxes(points, source, target)) {
2345
- paintRoute(edge, points, colour);
2680
+ if (clearOfBoxes(points, source, target)) {
2681
+ paintRoute(edgeIndex, edge, points, colour);
2346
2682
  continue;
2347
2683
  }
2348
2684
  }
2349
- paintRoute(edge, routeEdge(source, target, boxList, anchors).points, colour);
2685
+ paintRoute(edgeIndex, edge, routeEdge(source, target, boxList, anchors).points, colour);
2350
2686
  }
2351
- const routeSegments = routes.map((shape) => segmentsOfRoute(shape));
2687
+ for (const dot of junctions) shapes.push(dot);
2688
+ const boxEntries = Array.from(boxes.entries());
2689
+ const routingFailures = [];
2690
+ for (const { shape, edge } of routes) {
2691
+ const diagramEdge = diagram.edges[edge];
2692
+ const source = endBox(diagramEdge.from);
2693
+ const target = endBox(diagramEdge.to);
2694
+ if (source === void 0 || target === void 0) continue;
2695
+ for (const segment of segmentsOfRoute(shape)) {
2696
+ for (const [id, box] of boxEntries) {
2697
+ if (box === source || box === target) continue;
2698
+ if (!crossesBox(segment.x1, segment.y1, segment.x2, segment.y2, box)) continue;
2699
+ routingFailures.push({
2700
+ edge,
2701
+ from: diagramEdge.from,
2702
+ to: diagramEdge.to,
2703
+ box: id,
2704
+ x1: segment.x1,
2705
+ y1: segment.y1,
2706
+ x2: segment.x2,
2707
+ y2: segment.y2
2708
+ });
2709
+ }
2710
+ }
2711
+ }
2712
+ const routeSegments = routes.map(({ shape }) => segmentsOfRoute(shape));
2352
2713
  const placedLabels = [];
2353
- routes.forEach((shape, index) => {
2354
- const run = longestRun(pointsOfRoute(shape));
2355
- let spot = { x: (run.from.x + run.to.x) / 2, y: (run.from.y + run.to.y) / 2, width: textWidth(shape.label), height: LABEL_HEIGHT };
2356
- if (shape.label !== "") {
2357
- spot = placeLabel(shape.label, run.from, run.to, boxList, placedLabels, routeSegments.flatMap((list, at) => at === index ? [] : list));
2358
- placedLabels.push(spot);
2359
- }
2360
- shape.labelX = spot.x;
2361
- shape.labelY = spot.y;
2714
+ routes.forEach(({ shape, fixed }, at) => {
2715
+ if (fixed) {
2716
+ if (shape.label !== "") {
2717
+ placedLabels.push({ x: shape.labelX, y: shape.labelY, width: textWidth(shape.label), height: LABEL_HEIGHT });
2718
+ }
2719
+ return;
2720
+ }
2721
+ if (shape.label === "") return;
2722
+ const placement2 = placeLabel(
2723
+ shape.label,
2724
+ pointsOfRoute(shape),
2725
+ boxList,
2726
+ placedLabels,
2727
+ routeSegments.filter((_, other) => other !== at)
2728
+ );
2729
+ shape.labelX = placement2.x;
2730
+ shape.labelY = placement2.y;
2731
+ shape.labelPlacement = {
2732
+ run: placement2.run,
2733
+ collisions: placement2.collisions,
2734
+ boxes: placement2.boxes,
2735
+ labels: placement2.labels,
2736
+ edges: placement2.edges
2737
+ };
2738
+ placedLabels.push({ x: placement2.x, y: placement2.y, width: placement2.width, height: placement2.height });
2362
2739
  });
2740
+ const labelCollisions = routes.reduce((total, { shape }) => total + ((shape.labelPlacement?.collisions ?? 0) > 0 ? 1 : 0), 0);
2363
2741
  shapes.push({ kind: "title", x: MARGIN, y: MARGIN + 12, text: diagram.title, colour: "text" });
2364
2742
  const contentRight = Math.max(
2365
2743
  MARGIN,
@@ -2380,7 +2758,10 @@ window.__ModuleLoader__.load({
2380
2758
  const extent = drawnExtent(shapes);
2381
2759
  const canvasWidth = Math.max(width, extent.width + MARGIN);
2382
2760
  const canvasHeight = Math.max(height, extent.height + MARGIN);
2383
- return { canvas: { width: Math.round(canvasWidth), height: Math.round(canvasHeight), shapes }, colours };
2761
+ return {
2762
+ canvas: { width: Math.round(canvasWidth), height: Math.round(canvasHeight), shapes, routingFailures, labelCollisions },
2763
+ colours
2764
+ };
2384
2765
  }
2385
2766
 
2386
2767
  // src/render/from-mermaid.ts
@@ -3107,6 +3488,9 @@ window.__ModuleLoader__.load({
3107
3488
  const after = shape.points[1] ?? first;
3108
3489
  return '<polyline points="' + points + '" fill="none" stroke="' + stroke + '" stroke-width="1" stroke-linejoin="round"' + dash(shape) + " />" + edgeMarks(first, after, before, last, shape.arrow, stroke, colours.background) + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
3109
3490
  },
3491
+ // A junction dot: the visible answer to "is this a join or a crossing?". Filled in the line's own colour, so it
3492
+ // reads as the line thickening where a branch meets it rather than as a new mark on the page.
3493
+ junction: (shape, colours) => '<circle cx="' + n(shape.x) + '" cy="' + n(shape.y) + '" r="2.5" fill="' + colours[shape.colour] + '" />',
3110
3494
  title: (shape, colours) => {
3111
3495
  return '<text x="' + n(shape.x) + '" y="' + n(shape.y) + '" font-size="12" fill="' + colours[shape.colour] + '">' + escape(shape.text) + "</text>";
3112
3496
  },