@ktrysmt/beautiful-mermaid 1.4.2 → 1.4.4
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/README.md +19 -28
- package/dist/index.js +203 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/fan-in-layout.test.ts +109 -0
- package/src/__tests__/level-assignment.test.ts +103 -0
- package/src/__tests__/parser.test.ts +87 -0
- package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +7 -15
- package/src/__tests__/testdata/ascii/backlink_from_top.txt +13 -13
- package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +11 -12
- package/src/__tests__/testdata/ascii/comments.txt +7 -15
- package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +21 -21
- package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +7 -15
- package/src/__tests__/testdata/unicode/backlink_from_top.txt +13 -13
- package/src/__tests__/testdata/unicode/comments.txt +7 -15
- package/src/ascii/draw.ts +110 -12
- package/src/ascii/edge-routing.ts +77 -26
- package/src/ascii/grid.ts +53 -3
- package/src/ascii/pathfinder.ts +8 -1
- package/src/ascii/types.ts +2 -0
- package/src/multiline-utils.ts +13 -3
- package/src/parser.ts +66 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
# beautiful-mermaid
|
|
3
|
+
# @ktrysmt/beautiful-mermaid
|
|
4
4
|
|
|
5
5
|
**Render Mermaid diagrams as beautiful SVGs or ASCII art**
|
|
6
6
|
|
|
@@ -8,32 +8,23 @@ Ultra-fast, fully themeable, zero DOM dependencies. Built for the AI era.
|
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
|
-
[](https://www.npmjs.com/package/beautiful-mermaid)
|
|
11
|
+
[](https://www.npmjs.com/package/@ktrysmt/beautiful-mermaid)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
|
|
14
|
-
[**Live Demo & Samples**](https://agents.craft.do/mermaid)
|
|
15
|
-
|
|
16
|
-
**[→ Use it live in Craft Agents](https://agents.craft.do)**
|
|
17
|
-
|
|
18
14
|
</div>
|
|
19
15
|
|
|
20
16
|
---
|
|
21
17
|
|
|
22
|
-
##
|
|
23
|
-
|
|
24
|
-
Diagrams are essential for AI-assisted programming. When you're working with an AI coding assistant, being able to visualize data flows, state machines, and system architecture—directly in your terminal or chat interface—makes complex concepts instantly graspable.
|
|
25
|
-
|
|
26
|
-
[Mermaid](https://mermaid.js.org/) is the de facto standard for text-based diagrams. It's brilliant. But the default renderer has problems:
|
|
27
|
-
|
|
28
|
-
- **Aesthetics** — Might be personal preference, but wished they looked more professional
|
|
29
|
-
- **Complex theming** — Customizing colors requires wrestling with CSS classes
|
|
30
|
-
- **No terminal output** — Can't render to ASCII for CLI tools
|
|
31
|
-
- **Heavy dependencies** — Pulls in a lot of code for simple diagrams
|
|
18
|
+
## About This Fork
|
|
32
19
|
|
|
33
|
-
|
|
20
|
+
This is a fork of [beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid), originally built by the team at [Craft](https://craft.do). This fork is independently maintained by [@ktrysmt](https://github.com/ktrysmt) with additional bug fixes and improvements including:
|
|
34
21
|
|
|
22
|
+
- CJK/Unicode label support for subgraphs
|
|
23
|
+
- Subgraph layout fixes (vertical stacking, containment, label width)
|
|
24
|
+
- Edge routing improvements
|
|
25
|
+
- Scoped npm package (`@ktrysmt/beautiful-mermaid`)
|
|
35
26
|
|
|
36
|
-
The ASCII rendering engine is based on [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) by Alexander Grooff
|
|
27
|
+
The ASCII rendering engine is based on [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) by Alexander Grooff, ported from Go to TypeScript. Thank you Alexander for the excellent foundation!
|
|
37
28
|
|
|
38
29
|
## Features
|
|
39
30
|
|
|
@@ -50,11 +41,11 @@ The ASCII rendering engine is based on [mermaid-ascii](https://github.com/Alexan
|
|
|
50
41
|
## Installation
|
|
51
42
|
|
|
52
43
|
```bash
|
|
53
|
-
npm install beautiful-mermaid
|
|
44
|
+
npm install @ktrysmt/beautiful-mermaid
|
|
54
45
|
# or
|
|
55
|
-
bun add beautiful-mermaid
|
|
46
|
+
bun add @ktrysmt/beautiful-mermaid
|
|
56
47
|
# or
|
|
57
|
-
pnpm add beautiful-mermaid
|
|
48
|
+
pnpm add @ktrysmt/beautiful-mermaid
|
|
58
49
|
```
|
|
59
50
|
|
|
60
51
|
## Quick Start
|
|
@@ -62,7 +53,7 @@ pnpm add beautiful-mermaid
|
|
|
62
53
|
### SVG Output
|
|
63
54
|
|
|
64
55
|
```typescript
|
|
65
|
-
import { renderMermaidSVG } from 'beautiful-mermaid'
|
|
56
|
+
import { renderMermaidSVG } from '@ktrysmt/beautiful-mermaid'
|
|
66
57
|
|
|
67
58
|
const svg = renderMermaidSVG(`
|
|
68
59
|
graph TD
|
|
@@ -79,7 +70,7 @@ Need async? Use `renderMermaidSVGAsync()` — same output, returns a `Promise<st
|
|
|
79
70
|
### ASCII Output
|
|
80
71
|
|
|
81
72
|
```typescript
|
|
82
|
-
import { renderMermaidASCII } from 'beautiful-mermaid'
|
|
73
|
+
import { renderMermaidASCII } from '@ktrysmt/beautiful-mermaid'
|
|
83
74
|
|
|
84
75
|
const ascii = renderMermaidASCII(`graph LR; A --> B --> C`)
|
|
85
76
|
```
|
|
@@ -99,7 +90,7 @@ const ascii = renderMermaidASCII(`graph LR; A --> B --> C`)
|
|
|
99
90
|
Because rendering is synchronous, you can use `useMemo()` for zero-flash diagram rendering:
|
|
100
91
|
|
|
101
92
|
```tsx
|
|
102
|
-
import { renderMermaidSVG } from 'beautiful-mermaid'
|
|
93
|
+
import { renderMermaidSVG } from '@ktrysmt/beautiful-mermaid'
|
|
103
94
|
|
|
104
95
|
function MermaidDiagram({ code }: { code: string }) {
|
|
105
96
|
const { svg, error } = React.useMemo(() => {
|
|
@@ -224,7 +215,7 @@ const svg = renderMermaidSVG(diagram, {
|
|
|
224
215
|
| `one-dark` | Dark | `#282c34` | `#c678dd` |
|
|
225
216
|
|
|
226
217
|
```typescript
|
|
227
|
-
import { renderMermaidSVG, THEMES } from 'beautiful-mermaid'
|
|
218
|
+
import { renderMermaidSVG, THEMES } from '@ktrysmt/beautiful-mermaid'
|
|
228
219
|
|
|
229
220
|
const svg = renderMermaidSVG(diagram, THEMES['tokyo-night'])
|
|
230
221
|
```
|
|
@@ -259,7 +250,7 @@ Use **any VS Code theme** directly via Shiki integration. This gives you access
|
|
|
259
250
|
|
|
260
251
|
```typescript
|
|
261
252
|
import { getSingletonHighlighter } from 'shiki'
|
|
262
|
-
import { renderMermaidSVG, fromShikiTheme } from 'beautiful-mermaid'
|
|
253
|
+
import { renderMermaidSVG, fromShikiTheme } from '@ktrysmt/beautiful-mermaid'
|
|
263
254
|
|
|
264
255
|
// Load any theme from Shiki's registry
|
|
265
256
|
const highlighter = await getSingletonHighlighter({
|
|
@@ -437,7 +428,7 @@ The chart renderer follows a clean, minimal design philosophy inspired by Apple
|
|
|
437
428
|
For terminal environments, CLI tools, or anywhere you need plain text, render to ASCII or Unicode box-drawing characters:
|
|
438
429
|
|
|
439
430
|
```typescript
|
|
440
|
-
import { renderMermaidASCII } from 'beautiful-mermaid'
|
|
431
|
+
import { renderMermaidASCII } from '@ktrysmt/beautiful-mermaid'
|
|
441
432
|
|
|
442
433
|
// Unicode mode (default) — prettier box drawing
|
|
443
434
|
const unicode = renderMermaidASCII(`graph LR; A --> B`)
|
|
@@ -580,6 +571,6 @@ MIT — see [LICENSE](LICENSE) for details.
|
|
|
580
571
|
|
|
581
572
|
<div align="center">
|
|
582
573
|
|
|
583
|
-
|
|
574
|
+
Originally built by the team at [Craft](https://craft.do). Fork maintained by [@ktrysmt](https://github.com/ktrysmt).
|
|
584
575
|
|
|
585
576
|
</div>
|
package/dist/index.js
CHANGED
|
@@ -263,8 +263,11 @@ function measureMultilineText(text, fontSize, fontWeight) {
|
|
|
263
263
|
|
|
264
264
|
// src/multiline-utils.ts
|
|
265
265
|
function normalizeBrTags(label) {
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
let result = label.replace(/<br\s*\/?>/gi, "\n").replace(/\\n/g, "\n").trim();
|
|
267
|
+
if (result.startsWith('"') && result.endsWith('"')) {
|
|
268
|
+
result = result.slice(1, -1);
|
|
269
|
+
}
|
|
270
|
+
return result.replace(/<\/?(?:sub|sup|small|mark)\s*>/gi, "").replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g, "<i>$1</i>").replace(/~~(.+?)~~/g, "<s>$1</s>").trim();
|
|
268
271
|
}
|
|
269
272
|
function escapeXml(text) {
|
|
270
273
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -336,8 +339,43 @@ ${textEl}`;
|
|
|
336
339
|
}
|
|
337
340
|
|
|
338
341
|
// src/parser.ts
|
|
342
|
+
function joinMultilineDefinitions(lines) {
|
|
343
|
+
const result = [];
|
|
344
|
+
let buffer = "";
|
|
345
|
+
let bracketDepth = 0;
|
|
346
|
+
let parenDepth = 0;
|
|
347
|
+
let inQuote = false;
|
|
348
|
+
for (const line of lines) {
|
|
349
|
+
if (bracketDepth === 0 && parenDepth === 0 && !inQuote) {
|
|
350
|
+
buffer = line;
|
|
351
|
+
} else {
|
|
352
|
+
if (/<br\s*\/?>\s*$/i.test(buffer)) {
|
|
353
|
+
buffer += line;
|
|
354
|
+
} else {
|
|
355
|
+
buffer += "<br>" + line;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
for (const ch of line) {
|
|
359
|
+
if (ch === '"') {
|
|
360
|
+
inQuote = !inQuote;
|
|
361
|
+
} else if (!inQuote) {
|
|
362
|
+
if (ch === "[") bracketDepth++;
|
|
363
|
+
else if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
364
|
+
else if (ch === "(") parenDepth++;
|
|
365
|
+
else if (ch === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (bracketDepth === 0 && parenDepth === 0 && !inQuote) {
|
|
369
|
+
result.push(buffer);
|
|
370
|
+
buffer = "";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (buffer) result.push(buffer);
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
339
376
|
function parseMermaid(text) {
|
|
340
|
-
const
|
|
377
|
+
const rawLines = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("%%"));
|
|
378
|
+
const lines = joinMultilineDefinitions(rawLines);
|
|
341
379
|
if (lines.length === 0) {
|
|
342
380
|
throw new Error("Empty mermaid diagram");
|
|
343
381
|
}
|
|
@@ -1691,7 +1729,7 @@ function isFreeInGrid(grid, c) {
|
|
|
1691
1729
|
return !grid.has(gridKey(c));
|
|
1692
1730
|
}
|
|
1693
1731
|
var MAX_ITERATIONS = 5e4;
|
|
1694
|
-
function getPath(grid, from, to) {
|
|
1732
|
+
function getPath(grid, from, to, congestion) {
|
|
1695
1733
|
const pq = new MinHeap();
|
|
1696
1734
|
pq.push({ coord: from, priority: 0 });
|
|
1697
1735
|
const costSoFar = /* @__PURE__ */ new Map();
|
|
@@ -1719,7 +1757,8 @@ function getPath(grid, from, to) {
|
|
|
1719
1757
|
if (!isFreeInGrid(grid, next) && !gridCoordEquals(next, to)) {
|
|
1720
1758
|
continue;
|
|
1721
1759
|
}
|
|
1722
|
-
const
|
|
1760
|
+
const congestionPenalty = congestion?.get(gridKey(next)) ?? 0;
|
|
1761
|
+
const newCost = currentCost + 1 + congestionPenalty * 3;
|
|
1723
1762
|
const nextKey = gridKey(next);
|
|
1724
1763
|
const existingCost = costSoFar.get(nextKey);
|
|
1725
1764
|
if (existingCost === void 0 || newCost < existingCost) {
|
|
@@ -1863,17 +1902,17 @@ function determineStartAndEndDir(edge, graphDirection) {
|
|
|
1863
1902
|
}
|
|
1864
1903
|
return [preferredDir, preferredOppositeDir, alternativeDir, alternativeOppositeDir];
|
|
1865
1904
|
}
|
|
1866
|
-
function determinePath(graph, edge) {
|
|
1905
|
+
function determinePath(graph, edge, congestion) {
|
|
1867
1906
|
const sourceSg = getNodeSubgraph(graph, edge.from);
|
|
1868
1907
|
const targetSg = getNodeSubgraph(graph, edge.to);
|
|
1869
1908
|
const effectiveDir = sourceSg && sourceSg === targetSg && sourceSg.direction ? sourceSg.direction : graph.config.graphDirection;
|
|
1870
1909
|
const [preferredDir, preferredOppositeDir, alternativeDir, alternativeOppositeDir] = determineStartAndEndDir(edge, effectiveDir);
|
|
1871
1910
|
const prefFrom = gridCoordDirection(edge.from.gridCoord, preferredDir);
|
|
1872
1911
|
const prefTo = gridCoordDirection(edge.to.gridCoord, preferredOppositeDir);
|
|
1873
|
-
let preferredPath = getPath(graph.grid, prefFrom, prefTo);
|
|
1912
|
+
let preferredPath = getPath(graph.grid, prefFrom, prefTo, congestion);
|
|
1874
1913
|
const altFrom = gridCoordDirection(edge.from.gridCoord, alternativeDir);
|
|
1875
1914
|
const altTo = gridCoordDirection(edge.to.gridCoord, alternativeOppositeDir);
|
|
1876
|
-
let alternativePath = getPath(graph.grid, altFrom, altTo);
|
|
1915
|
+
let alternativePath = getPath(graph.grid, altFrom, altTo, congestion);
|
|
1877
1916
|
if (preferredPath !== null && alternativePath !== null) {
|
|
1878
1917
|
preferredPath = mergePath(preferredPath);
|
|
1879
1918
|
alternativePath = mergePath(alternativePath);
|
|
@@ -1904,11 +1943,41 @@ function determinePath(graph, edge) {
|
|
|
1904
1943
|
edge.endDir = preferredOppositeDir;
|
|
1905
1944
|
edge.path = [prefFrom, prefTo];
|
|
1906
1945
|
}
|
|
1946
|
+
function pathToCells(path) {
|
|
1947
|
+
const cells = [];
|
|
1948
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1949
|
+
const p1 = path[i];
|
|
1950
|
+
const p2 = path[i + 1];
|
|
1951
|
+
if (p1.x === p2.x) {
|
|
1952
|
+
const minY = Math.min(p1.y, p2.y);
|
|
1953
|
+
const maxY = Math.max(p1.y, p2.y);
|
|
1954
|
+
for (let y = minY; y <= maxY; y++) {
|
|
1955
|
+
cells.push({ x: p1.x, y });
|
|
1956
|
+
}
|
|
1957
|
+
} else {
|
|
1958
|
+
const minX = Math.min(p1.x, p2.x);
|
|
1959
|
+
const maxX = Math.max(p1.x, p2.x);
|
|
1960
|
+
for (let x = minX; x <= maxX; x++) {
|
|
1961
|
+
cells.push({ x, y: p1.y });
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return cells;
|
|
1966
|
+
}
|
|
1967
|
+
function segmentMidpointKey(line) {
|
|
1968
|
+
const isVertical = line[0].x === line[1].x;
|
|
1969
|
+
if (isVertical) {
|
|
1970
|
+
const mid = Math.floor((line[0].y + line[1].y) / 2);
|
|
1971
|
+
return `v:${line[0].x}:${mid}`;
|
|
1972
|
+
} else {
|
|
1973
|
+
const mid = Math.floor((line[0].x + line[1].x) / 2);
|
|
1974
|
+
return `h:${line[0].y}:${mid}`;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1907
1977
|
function determineLabelLine(graph, edge) {
|
|
1908
1978
|
if (edge.text.length === 0) return;
|
|
1909
1979
|
const lenLabel = maxLineWidth(edge.text);
|
|
1910
1980
|
const pathLen = edge.path.length;
|
|
1911
|
-
const isVerticalFlow = graph.config.graphDirection === "TD";
|
|
1912
1981
|
const segments = [];
|
|
1913
1982
|
for (let i = 1; i < pathLen; i++) {
|
|
1914
1983
|
const p1 = edge.path[i - 1];
|
|
@@ -1918,21 +1987,26 @@ function determineLabelLine(graph, edge) {
|
|
|
1918
1987
|
const isVertical = p1.x === p2.x;
|
|
1919
1988
|
segments.push({ line, width, index: i, isVertical });
|
|
1920
1989
|
}
|
|
1921
|
-
const
|
|
1990
|
+
const used = graph.usedLabelMidpoints ?? /* @__PURE__ */ new Set();
|
|
1991
|
+
const middleIdx = Math.floor(pathLen / 2);
|
|
1992
|
+
const scoreSeg = (s) => {
|
|
1993
|
+
const key = segmentMidpointKey(s.line);
|
|
1994
|
+
let score = 0;
|
|
1995
|
+
if (!used.has(key)) score += 1e3;
|
|
1996
|
+
if (s.index > 1) score += 100;
|
|
1997
|
+
score -= Math.abs(s.index - middleIdx);
|
|
1998
|
+
return score;
|
|
1999
|
+
};
|
|
2000
|
+
const candidates = segments.filter((s) => s.width >= lenLabel);
|
|
1922
2001
|
let largestLine;
|
|
1923
|
-
if (
|
|
1924
|
-
|
|
1925
|
-
largestLine =
|
|
2002
|
+
if (candidates.length > 0) {
|
|
2003
|
+
candidates.sort((a, b) => scoreSeg(b) - scoreSeg(a));
|
|
2004
|
+
largestLine = candidates[0].line;
|
|
1926
2005
|
} else {
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
fallbackSegments.sort((a, b) => b.index - a.index);
|
|
1930
|
-
largestLine = fallbackSegments[0].line;
|
|
1931
|
-
} else {
|
|
1932
|
-
segments.sort((a, b) => b.width - a.width);
|
|
1933
|
-
largestLine = segments[0]?.line ?? [edge.path[0], edge.path[1]];
|
|
1934
|
-
}
|
|
2006
|
+
segments.sort((a, b) => b.width - a.width);
|
|
2007
|
+
largestLine = segments[0]?.line ?? [edge.path[0], edge.path[1]];
|
|
1935
2008
|
}
|
|
2009
|
+
used.add(segmentMidpointKey(largestLine));
|
|
1936
2010
|
const minX = Math.min(largestLine[0].x, largestLine[1].x);
|
|
1937
2011
|
const maxX = Math.max(largestLine[0].x, largestLine[1].x);
|
|
1938
2012
|
const middleX = minX + Math.floor((maxX - minX) / 2);
|
|
@@ -2853,8 +2927,9 @@ function drawArrow(graph, edge) {
|
|
|
2853
2927
|
return [empty, empty, empty, empty, empty, empty];
|
|
2854
2928
|
}
|
|
2855
2929
|
const labelCanvas = drawArrowLabel(graph, edge);
|
|
2856
|
-
const
|
|
2857
|
-
const
|
|
2930
|
+
const endCoordOverride = computeEndCoordOverride(graph, edge);
|
|
2931
|
+
const [pathCanvas, linesDrawn, lineDirs] = drawPath(graph, edge.path, edge.style, void 0, endCoordOverride);
|
|
2932
|
+
const boxStartCanvas = drawBoxStart(graph, edge.path, linesDrawn[0], edge.from);
|
|
2858
2933
|
let arrowHeadEndCanvas;
|
|
2859
2934
|
if (edge.hasArrowEnd) {
|
|
2860
2935
|
arrowHeadEndCanvas = drawArrowHead(
|
|
@@ -2883,6 +2958,38 @@ function drawArrow(graph, edge) {
|
|
|
2883
2958
|
const cornersCanvas = drawCorners(graph, edge.path);
|
|
2884
2959
|
return [pathCanvas, boxStartCanvas, arrowHeadEndCanvas, arrowHeadStartCanvas, cornersCanvas, labelCanvas];
|
|
2885
2960
|
}
|
|
2961
|
+
function computeEndCoordOverride(graph, edge) {
|
|
2962
|
+
if (edge.path.length < 2) return void 0;
|
|
2963
|
+
const lastPathCoord = edge.path[edge.path.length - 1];
|
|
2964
|
+
const prevPathCoord = edge.path[edge.path.length - 2];
|
|
2965
|
+
const dir = determineDirection(prevPathCoord, lastPathCoord);
|
|
2966
|
+
const defaultDC = gridToDrawingCoord(graph, lastPathCoord);
|
|
2967
|
+
const targetDC = edge.to.drawingCoord;
|
|
2968
|
+
const targetGC = edge.to.gridCoord;
|
|
2969
|
+
if (!targetDC || !targetGC) return void 0;
|
|
2970
|
+
let boxW = 0;
|
|
2971
|
+
for (let i = 0; i < 2; i++) boxW += graph.columnWidth.get(targetGC.x + i) ?? 0;
|
|
2972
|
+
let boxH = 0;
|
|
2973
|
+
for (let i = 0; i < 2; i++) boxH += graph.rowHeight.get(targetGC.y + i) ?? 0;
|
|
2974
|
+
if (dirEquals(dir, Left)) {
|
|
2975
|
+
const borderX = targetDC.x + boxW;
|
|
2976
|
+
if (borderX === defaultDC.x) return void 0;
|
|
2977
|
+
return { x: borderX, y: defaultDC.y };
|
|
2978
|
+
} else if (dirEquals(dir, Right)) {
|
|
2979
|
+
const borderX = targetDC.x;
|
|
2980
|
+
if (borderX === defaultDC.x) return void 0;
|
|
2981
|
+
return { x: borderX, y: defaultDC.y };
|
|
2982
|
+
} else if (dirEquals(dir, Up)) {
|
|
2983
|
+
const borderY = targetDC.y + boxH;
|
|
2984
|
+
if (borderY === defaultDC.y) return void 0;
|
|
2985
|
+
return { x: defaultDC.x, y: borderY };
|
|
2986
|
+
} else if (dirEquals(dir, Down)) {
|
|
2987
|
+
const borderY = targetDC.y;
|
|
2988
|
+
if (borderY === defaultDC.y) return void 0;
|
|
2989
|
+
return { x: defaultDC.x, y: borderY };
|
|
2990
|
+
}
|
|
2991
|
+
return void 0;
|
|
2992
|
+
}
|
|
2886
2993
|
function reverseDirection(dir) {
|
|
2887
2994
|
if (dirEquals(dir, Up)) return Down;
|
|
2888
2995
|
if (dirEquals(dir, Down)) return Up;
|
|
@@ -2894,15 +3001,16 @@ function reverseDirection(dir) {
|
|
|
2894
3001
|
if (dirEquals(dir, LowerRight)) return UpperLeft;
|
|
2895
3002
|
return Middle;
|
|
2896
3003
|
}
|
|
2897
|
-
function drawPath(graph, path, style = "solid") {
|
|
3004
|
+
function drawPath(graph, path, style = "solid", startCoordOverride, endCoordOverride) {
|
|
2898
3005
|
const canvas = copyCanvas(graph.canvas);
|
|
2899
3006
|
let previousCoord = path[0];
|
|
2900
3007
|
const linesDrawn = [];
|
|
2901
3008
|
const lineDirs = [];
|
|
3009
|
+
const lastIdx = path.length - 1;
|
|
2902
3010
|
for (let i = 1; i < path.length; i++) {
|
|
2903
3011
|
const nextCoord = path[i];
|
|
2904
|
-
const prevDC = gridToDrawingCoord(graph, previousCoord);
|
|
2905
|
-
const nextDC = gridToDrawingCoord(graph, nextCoord);
|
|
3012
|
+
const prevDC = i === 1 && startCoordOverride ? startCoordOverride : gridToDrawingCoord(graph, previousCoord);
|
|
3013
|
+
const nextDC = i === lastIdx && endCoordOverride ? endCoordOverride : gridToDrawingCoord(graph, nextCoord);
|
|
2906
3014
|
if (drawingCoordEquals(prevDC, nextDC)) {
|
|
2907
3015
|
previousCoord = nextCoord;
|
|
2908
3016
|
continue;
|
|
@@ -2916,18 +3024,45 @@ function drawPath(graph, path, style = "solid") {
|
|
|
2916
3024
|
}
|
|
2917
3025
|
return [canvas, linesDrawn, lineDirs];
|
|
2918
3026
|
}
|
|
2919
|
-
function drawBoxStart(graph, path, firstLine,
|
|
3027
|
+
function drawBoxStart(graph, path, firstLine, sourceNode) {
|
|
2920
3028
|
const canvas = copyCanvas(graph.canvas);
|
|
2921
3029
|
if (graph.config.useAscii) return canvas;
|
|
2922
|
-
if (
|
|
3030
|
+
if (sourceNode.shape === "state-start" || sourceNode.shape === "state-end") {
|
|
2923
3031
|
return canvas;
|
|
2924
3032
|
}
|
|
2925
3033
|
const from = firstLine[0];
|
|
2926
3034
|
const dir = determineDirection(path[0], path[1]);
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
3035
|
+
const dc = sourceNode.drawingCoord;
|
|
3036
|
+
const gc = sourceNode.gridCoord;
|
|
3037
|
+
if (dirEquals(dir, Right)) {
|
|
3038
|
+
let boxW = 0;
|
|
3039
|
+
for (let i = 0; i < 2; i++) boxW += graph.columnWidth.get(gc.x + i) ?? 0;
|
|
3040
|
+
const borderX = dc.x + boxW;
|
|
3041
|
+
canvas[borderX][from.y] = "\u251C";
|
|
3042
|
+
for (let x = borderX + 1; x < from.x; x++) {
|
|
3043
|
+
canvas[x][from.y] = "\u2500";
|
|
3044
|
+
}
|
|
3045
|
+
} else if (dirEquals(dir, Left)) {
|
|
3046
|
+
const borderX = dc.x;
|
|
3047
|
+
canvas[borderX][from.y] = "\u2524";
|
|
3048
|
+
for (let x = from.x + 1; x < borderX; x++) {
|
|
3049
|
+
canvas[x][from.y] = "\u2500";
|
|
3050
|
+
}
|
|
3051
|
+
} else if (dirEquals(dir, Down)) {
|
|
3052
|
+
let boxH = 0;
|
|
3053
|
+
for (let i = 0; i < 2; i++) boxH += graph.rowHeight.get(gc.y + i) ?? 0;
|
|
3054
|
+
const borderY = dc.y + boxH;
|
|
3055
|
+
canvas[from.x][borderY] = "\u252C";
|
|
3056
|
+
for (let y = borderY + 1; y < from.y; y++) {
|
|
3057
|
+
canvas[from.x][y] = "\u2502";
|
|
3058
|
+
}
|
|
3059
|
+
} else if (dirEquals(dir, Up)) {
|
|
3060
|
+
const borderY = dc.y;
|
|
3061
|
+
canvas[from.x][borderY] = "\u2534";
|
|
3062
|
+
for (let y = from.y + 1; y < borderY; y++) {
|
|
3063
|
+
canvas[from.x][y] = "\u2502";
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
2931
3066
|
return canvas;
|
|
2932
3067
|
}
|
|
2933
3068
|
function drawArrowHead(graph, lastLine, fallbackDir) {
|
|
@@ -3815,7 +3950,14 @@ function createMapping(graph) {
|
|
|
3815
3950
|
const parentSg = getNodeSubgraph(graph, node);
|
|
3816
3951
|
const childSg = getNodeSubgraph(graph, child);
|
|
3817
3952
|
const edgeDir = parentSg && parentSg === childSg && parentSg.direction ? parentSg.direction : graph.config.graphDirection;
|
|
3818
|
-
let
|
|
3953
|
+
let maxPredLevel = edgeDir === "LR" ? gc.x : gc.y;
|
|
3954
|
+
for (const e of graph.edges) {
|
|
3955
|
+
if (e.to.name === child.name && e.from.gridCoord !== null) {
|
|
3956
|
+
const predLevel = edgeDir === "LR" ? e.from.gridCoord.x : e.from.gridCoord.y;
|
|
3957
|
+
if (predLevel > maxPredLevel) maxPredLevel = predLevel;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
let childLevel = maxPredLevel + 4;
|
|
3819
3961
|
if (childSg && parentSg !== childSg && deferredRoots.size > 0) {
|
|
3820
3962
|
let placedDeferred = false;
|
|
3821
3963
|
for (const dr of [...deferredRoots]) {
|
|
@@ -3880,17 +4022,44 @@ function createMapping(graph) {
|
|
|
3880
4022
|
for (const node of graph.nodes) {
|
|
3881
4023
|
setColumnWidth(graph, node);
|
|
3882
4024
|
}
|
|
4025
|
+
const labeledInDegree = /* @__PURE__ */ new Map();
|
|
4026
|
+
for (const edge of graph.edges) {
|
|
4027
|
+
if (edge.text.length > 0) {
|
|
4028
|
+
labeledInDegree.set(edge.to.name, (labeledInDegree.get(edge.to.name) ?? 0) + 1);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
for (const node of graph.nodes) {
|
|
4032
|
+
const deg = labeledInDegree.get(node.name) ?? 0;
|
|
4033
|
+
if (deg >= 3 && node.gridCoord) {
|
|
4034
|
+
const approachRow = node.gridCoord.y - 1;
|
|
4035
|
+
if (approachRow >= 0) {
|
|
4036
|
+
const extra = Math.min((deg - 2) * 2, 10);
|
|
4037
|
+
const current = graph.rowHeight.get(approachRow) ?? 0;
|
|
4038
|
+
graph.rowHeight.set(approachRow, Math.max(current, current + extra));
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
3883
4042
|
graph.bundles = analyzeEdgeBundles(graph);
|
|
3884
4043
|
processBundles(graph);
|
|
4044
|
+
graph.usedLabelMidpoints = /* @__PURE__ */ new Set();
|
|
4045
|
+
const congestion = /* @__PURE__ */ new Map();
|
|
3885
4046
|
for (const edge of graph.edges) {
|
|
3886
4047
|
if (edge.bundle && edge.path.length > 0) {
|
|
3887
4048
|
increaseGridSizeForPath(graph, edge.path);
|
|
3888
4049
|
determineLabelLine(graph, edge);
|
|
4050
|
+
for (const cell of pathToCells(edge.path)) {
|
|
4051
|
+
const k = gridKey(cell);
|
|
4052
|
+
congestion.set(k, (congestion.get(k) ?? 0) + 1);
|
|
4053
|
+
}
|
|
3889
4054
|
continue;
|
|
3890
4055
|
}
|
|
3891
|
-
determinePath(graph, edge);
|
|
4056
|
+
determinePath(graph, edge, congestion);
|
|
3892
4057
|
increaseGridSizeForPath(graph, edge.path);
|
|
3893
4058
|
determineLabelLine(graph, edge);
|
|
4059
|
+
for (const cell of pathToCells(edge.path)) {
|
|
4060
|
+
const k = gridKey(cell);
|
|
4061
|
+
congestion.set(k, (congestion.get(k) ?? 0) + 1);
|
|
4062
|
+
}
|
|
3894
4063
|
}
|
|
3895
4064
|
for (const node of graph.nodes) {
|
|
3896
4065
|
node.drawingCoord = gridToDrawingCoord(graph, node.gridCoord);
|