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 +524 -140
- package/lib/render/paint.js +668 -207
- package/lib/render/svg.js +3 -0
- package/lib/render/trunk.js +213 -0
- package/lib/types/render/paint.d.ts +120 -6
- package/lib/types/render/place.d.ts +8 -3
- package/lib/types/render/trunk.d.ts +104 -0
- package/package.json +1 -1
- package/skills/model-everything-diagram/SKILL.md +1 -0
- package/src/render/paint.ts +769 -205
- package/src/render/place.ts +8 -3
- package/src/render/svg.ts +4 -0
- package/src/render/trunk.ts +275 -0
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
|
|
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
|
|
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
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
for (const
|
|
1852
|
-
|
|
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
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
const
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
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
|
|
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 =
|
|
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
|
|
1991
|
-
|
|
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
|
|
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
|
-
|
|
2296
|
+
if (placement.across.has(index)) return { exit: "right", entry: "left" };
|
|
2031
2297
|
const lane = placement.lanes.get(index);
|
|
2032
|
-
|
|
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")) ??
|
|
2035
|
-
entry: claimed.get(endKey(index, "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
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
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) =>
|
|
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 (
|
|
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,
|
|
2143
|
-
entryWanted = hasDiscretePorts(shapeOf(edge.to)) ? toMiddle : clampTo(edge.to, entry.entry,
|
|
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
|
|
2208
|
-
|
|
2209
|
-
|
|
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 =
|
|
2267
|
-
const into =
|
|
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:
|
|
2270
|
-
points.push({ x:
|
|
2271
|
-
if (into !== 0) points.push({ x:
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2338
|
-
|
|
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
|
-
|
|
2344
|
-
|
|
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
|
|
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,
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
}
|
|
2360
|
-
shape.
|
|
2361
|
-
|
|
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 {
|
|
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
|
},
|