@ktrysmt/beautiful-mermaid 1.4.4 → 1.5.1
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 +0 -2
- package/dist/index.js +309 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/integration.test.ts +3 -3
- package/src/__tests__/parser.test.ts +37 -0
- package/src/__tests__/renderer.test.ts +5 -5
- package/src/layout-engine.ts +396 -0
- package/src/parser.ts +62 -0
- package/src/renderer.ts +57 -5
- package/src/theme.ts +3 -3
package/README.md
CHANGED
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
Ultra-fast, fully themeable, zero DOM dependencies. Built for the AI era.
|
|
8
8
|
|
|
9
|
-

|
|
10
|
-
|
|
11
9
|
[](https://www.npmjs.com/package/@ktrysmt/beautiful-mermaid)
|
|
12
10
|
[](LICENSE)
|
|
13
11
|
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ var MIX = {
|
|
|
7
7
|
/** Primary text: near-full fg */
|
|
8
8
|
text: 100,
|
|
9
9
|
// just use --fg directly
|
|
10
|
-
/** Secondary text (group headers): fg mixed at
|
|
11
|
-
textSec:
|
|
10
|
+
/** Secondary text (group headers, edge labels): fg mixed at 75% */
|
|
11
|
+
textSec: 75,
|
|
12
12
|
/** Muted text (edge labels, notes): fg mixed at 40% */
|
|
13
13
|
textMuted: 40,
|
|
14
14
|
/** Faint text (de-emphasized): fg mixed at 25% */
|
|
@@ -153,7 +153,7 @@ function buildStyleBlock(font, hasMonoFont) {
|
|
|
153
153
|
const derivedVars = `
|
|
154
154
|
/* Derived from --bg and --fg (overridable via --line, --accent, etc.) */
|
|
155
155
|
--_text: var(--fg);
|
|
156
|
-
--_text-sec:
|
|
156
|
+
--_text-sec: color-mix(in srgb, var(--fg) ${MIX.textSec}%, var(--bg));
|
|
157
157
|
--_text-muted: var(--muted, color-mix(in srgb, var(--fg) ${MIX.textMuted}%, var(--bg)));
|
|
158
158
|
--_text-faint: color-mix(in srgb, var(--fg) ${MIX.textFaint}%, var(--bg));
|
|
159
159
|
--_line: var(--line, color-mix(in srgb, var(--fg) ${MIX.line}%, var(--bg)));
|
|
@@ -481,8 +481,43 @@ function parseFlowchart(lines) {
|
|
|
481
481
|
}
|
|
482
482
|
parseEdgeLine(line, graph, subgraphStack);
|
|
483
483
|
}
|
|
484
|
+
resolveSubgraphEdgeEndpoints(graph);
|
|
484
485
|
return graph;
|
|
485
486
|
}
|
|
487
|
+
function resolveSubgraphEdgeEndpoints(graph) {
|
|
488
|
+
const representatives = /* @__PURE__ */ new Map();
|
|
489
|
+
function pickRepresentative(sg) {
|
|
490
|
+
for (const id of sg.nodeIds) {
|
|
491
|
+
if (graph.nodes.has(id)) return id;
|
|
492
|
+
}
|
|
493
|
+
for (const child of sg.children) {
|
|
494
|
+
const childRep = representatives.get(child.id) ?? pickRepresentative(child);
|
|
495
|
+
if (childRep) return childRep;
|
|
496
|
+
}
|
|
497
|
+
return void 0;
|
|
498
|
+
}
|
|
499
|
+
function visit(sg) {
|
|
500
|
+
for (const child of sg.children) visit(child);
|
|
501
|
+
const rep = pickRepresentative(sg);
|
|
502
|
+
if (rep) representatives.set(sg.id, rep);
|
|
503
|
+
}
|
|
504
|
+
for (const sg of graph.subgraphs) visit(sg);
|
|
505
|
+
if (representatives.size === 0) return;
|
|
506
|
+
for (const edge of graph.edges) {
|
|
507
|
+
const newSource = representatives.get(edge.source);
|
|
508
|
+
if (newSource) edge.source = newSource;
|
|
509
|
+
const newTarget = representatives.get(edge.target);
|
|
510
|
+
if (newTarget) edge.target = newTarget;
|
|
511
|
+
}
|
|
512
|
+
for (const sgId of representatives.keys()) {
|
|
513
|
+
graph.nodes.delete(sgId);
|
|
514
|
+
}
|
|
515
|
+
function scrubMembership(sg) {
|
|
516
|
+
sg.nodeIds = sg.nodeIds.filter((id) => !representatives.has(id));
|
|
517
|
+
for (const child of sg.children) scrubMembership(child);
|
|
518
|
+
}
|
|
519
|
+
for (const sg of graph.subgraphs) scrubMembership(sg);
|
|
520
|
+
}
|
|
486
521
|
function parseStateDiagram(lines) {
|
|
487
522
|
const graph = {
|
|
488
523
|
direction: "TD",
|
|
@@ -6594,6 +6629,7 @@ function mermaidToElk(graph, opts) {
|
|
|
6594
6629
|
"elk.layered.highDegreeNodes.threshold": "8",
|
|
6595
6630
|
"elk.layered.compaction.postCompaction.strategy": "LEFT_RIGHT_CONSTRAINT_LOCKING",
|
|
6596
6631
|
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
|
|
6632
|
+
"elk.layered.cycleBreaking.strategy": "DEPTH_FIRST",
|
|
6597
6633
|
"elk.layered.wrapping.strategy": "OFF",
|
|
6598
6634
|
// Use SEPARATE when subgraphs have direction overrides (enables proper direction handling)
|
|
6599
6635
|
// Use INCLUDE_CHILDREN otherwise (simpler cross-hierarchy edge routing)
|
|
@@ -6698,6 +6734,7 @@ function subgraphToElk(sg, graph, opts, edgesBySubgraph, subgraphPorts) {
|
|
|
6698
6734
|
"elk.layered.spacing.edgeEdgeBetweenLayers": "12",
|
|
6699
6735
|
"elk.layered.spacing.edgeNodeBetweenLayers": "12",
|
|
6700
6736
|
"elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED",
|
|
6737
|
+
"elk.layered.cycleBreaking.strategy": "DEPTH_FIRST",
|
|
6701
6738
|
"elk.layered.spacing.nodeNodeBetweenLayers": String(opts.layerSpacing),
|
|
6702
6739
|
"elk.spacing.nodeNode": String(opts.nodeSpacing)
|
|
6703
6740
|
};
|
|
@@ -6802,6 +6839,7 @@ function elkToPositioned(elkResult, graph, mergeEdges = false) {
|
|
|
6802
6839
|
collectAllSubgraphIds(sg, subgraphIds);
|
|
6803
6840
|
}
|
|
6804
6841
|
extractNodesAndGroups(elkResult, graph, subgraphIds, nodes, groups, 0, 0);
|
|
6842
|
+
expandGroupsForLabels(groups, nodes);
|
|
6805
6843
|
const allBounds = flattenGroupBounds(groups);
|
|
6806
6844
|
const margins = allBounds.length > 0 ? {
|
|
6807
6845
|
leftX: Math.min(...allBounds.map((b) => b.x)) - 20,
|
|
@@ -6812,6 +6850,7 @@ function elkToPositioned(elkResult, graph, mergeEdges = false) {
|
|
|
6812
6850
|
if (mergeEdges) {
|
|
6813
6851
|
bundleEdgePaths(edges, nodes, groups, graph.direction);
|
|
6814
6852
|
}
|
|
6853
|
+
rerouteBackEdges(edges, nodes, groups, graph.direction);
|
|
6815
6854
|
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
6816
6855
|
for (const edge of edges) {
|
|
6817
6856
|
const sourceNode = nodeMap.get(edge.source);
|
|
@@ -6845,6 +6884,34 @@ function elkToPositioned(elkResult, graph, mergeEdges = false) {
|
|
|
6845
6884
|
groups
|
|
6846
6885
|
};
|
|
6847
6886
|
}
|
|
6887
|
+
function expandGroupsForLabels(groups, nodes) {
|
|
6888
|
+
for (const group of groups) {
|
|
6889
|
+
if (group.children.length > 0) {
|
|
6890
|
+
expandGroupsForLabels(group.children, nodes);
|
|
6891
|
+
}
|
|
6892
|
+
if (!group.label) continue;
|
|
6893
|
+
const metrics = measureMultilineText(group.label, FONT_SIZES.groupHeader, FONT_WEIGHTS.groupHeader);
|
|
6894
|
+
const needed = metrics.width + 12 + 16;
|
|
6895
|
+
if (needed <= group.width) continue;
|
|
6896
|
+
const extra = needed - group.width;
|
|
6897
|
+
const shift = extra / 2;
|
|
6898
|
+
for (const node of nodes) {
|
|
6899
|
+
if (node.x >= group.x && node.x + node.width <= group.x + group.width + extra && node.y >= group.y && node.y + node.height <= group.y + group.height) {
|
|
6900
|
+
node.x += shift;
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
for (const child of group.children) {
|
|
6904
|
+
shiftGroupX(child, shift);
|
|
6905
|
+
}
|
|
6906
|
+
group.width = needed;
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
function shiftGroupX(group, dx) {
|
|
6910
|
+
group.x += dx;
|
|
6911
|
+
for (const child of group.children) {
|
|
6912
|
+
shiftGroupX(child, dx);
|
|
6913
|
+
}
|
|
6914
|
+
}
|
|
6848
6915
|
function extractNodesAndGroups(elkNode, graph, subgraphIds, nodes, groups, offsetX, offsetY) {
|
|
6849
6916
|
if (!elkNode.children) return;
|
|
6850
6917
|
for (const child of elkNode.children) {
|
|
@@ -6997,6 +7064,99 @@ function orthogonalizeEdgePoints(points, margins, edgeIndex = 0) {
|
|
|
6997
7064
|
}
|
|
6998
7065
|
return result;
|
|
6999
7066
|
}
|
|
7067
|
+
function rerouteBackEdges(edges, nodes, groups, direction) {
|
|
7068
|
+
const isVertical = direction === "TD" || direction === "TB" || direction === "BT";
|
|
7069
|
+
if (!isVertical) return;
|
|
7070
|
+
const isDownward = direction === "TD" || direction === "TB";
|
|
7071
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
7072
|
+
const backEdgeIndices = [];
|
|
7073
|
+
for (let i = 0; i < edges.length; i++) {
|
|
7074
|
+
const edge = edges[i];
|
|
7075
|
+
const src = nodeMap.get(edge.source);
|
|
7076
|
+
const tgt = nodeMap.get(edge.target);
|
|
7077
|
+
if (!src || !tgt || edge.points.length < 2) continue;
|
|
7078
|
+
const isBackEdge = isDownward ? src.y > tgt.y : src.y + src.height < tgt.y + tgt.height;
|
|
7079
|
+
if (isBackEdge) backEdgeIndices.push(i);
|
|
7080
|
+
}
|
|
7081
|
+
if (backEdgeIndices.length === 0) return;
|
|
7082
|
+
const nodeExitCount = /* @__PURE__ */ new Map();
|
|
7083
|
+
const nodeEnterCount = /* @__PURE__ */ new Map();
|
|
7084
|
+
for (const idx of backEdgeIndices) {
|
|
7085
|
+
const edge = edges[idx];
|
|
7086
|
+
nodeExitCount.set(edge.source, (nodeExitCount.get(edge.source) ?? 0) + 1);
|
|
7087
|
+
nodeEnterCount.set(edge.target, (nodeEnterCount.get(edge.target) ?? 0) + 1);
|
|
7088
|
+
}
|
|
7089
|
+
const nodeTotalPorts = /* @__PURE__ */ new Map();
|
|
7090
|
+
const allNodeIds = /* @__PURE__ */ new Set([...nodeExitCount.keys(), ...nodeEnterCount.keys()]);
|
|
7091
|
+
for (const id of allNodeIds) {
|
|
7092
|
+
nodeTotalPorts.set(id, (nodeExitCount.get(id) ?? 0) + (nodeEnterCount.get(id) ?? 0));
|
|
7093
|
+
}
|
|
7094
|
+
const nodeNextExitSlot = /* @__PURE__ */ new Map();
|
|
7095
|
+
const nodeNextEnterSlot = /* @__PURE__ */ new Map();
|
|
7096
|
+
const allBounds = flattenGroupBounds(groups);
|
|
7097
|
+
for (const n of nodes) {
|
|
7098
|
+
allBounds.push({ x: n.x, y: n.y, right: n.x + n.width, bottom: n.y + n.height });
|
|
7099
|
+
}
|
|
7100
|
+
const diagramRight = Math.max(...allBounds.map((b) => b.right));
|
|
7101
|
+
const EDGE_SPACING = 12;
|
|
7102
|
+
const PORT_MARGIN = 6;
|
|
7103
|
+
function portY(node, slotIndex, totalSlots) {
|
|
7104
|
+
if (totalSlots === 1) return node.y + node.height / 2;
|
|
7105
|
+
const usable = node.height - PORT_MARGIN * 2;
|
|
7106
|
+
return node.y + PORT_MARGIN + usable * (slotIndex + 0.5) / totalSlots;
|
|
7107
|
+
}
|
|
7108
|
+
for (let bi = 0; bi < backEdgeIndices.length; bi++) {
|
|
7109
|
+
const edge = edges[backEdgeIndices[bi]];
|
|
7110
|
+
const src = nodeMap.get(edge.source);
|
|
7111
|
+
const tgt = nodeMap.get(edge.target);
|
|
7112
|
+
const srcExitIdx = nodeNextExitSlot.get(edge.source) ?? 0;
|
|
7113
|
+
nodeNextExitSlot.set(edge.source, srcExitIdx + 1);
|
|
7114
|
+
const srcExitY = portY(src, srcExitIdx, nodeTotalPorts.get(edge.source));
|
|
7115
|
+
const tgtEnterIdx = nodeNextEnterSlot.get(edge.target) ?? 0;
|
|
7116
|
+
nodeNextEnterSlot.set(edge.target, tgtEnterIdx + 1);
|
|
7117
|
+
const tgtExitOffset = nodeExitCount.get(edge.target) ?? 0;
|
|
7118
|
+
const tgtEntryY = portY(tgt, tgtExitOffset + tgtEnterIdx, nodeTotalPorts.get(edge.target));
|
|
7119
|
+
const srcRight = src.x + src.width;
|
|
7120
|
+
const tgtRight = tgt.x + tgt.width;
|
|
7121
|
+
const marginX = diagramRight + 20 + bi * EDGE_SPACING;
|
|
7122
|
+
edge.points = [
|
|
7123
|
+
{ x: srcRight, y: srcExitY },
|
|
7124
|
+
{ x: marginX, y: srcExitY },
|
|
7125
|
+
{ x: marginX, y: tgtEntryY },
|
|
7126
|
+
{ x: tgtRight, y: tgtEntryY }
|
|
7127
|
+
];
|
|
7128
|
+
}
|
|
7129
|
+
const LABEL_PADDING = 8;
|
|
7130
|
+
const placedLabels = [];
|
|
7131
|
+
for (const idx of backEdgeIndices) {
|
|
7132
|
+
const edge = edges[idx];
|
|
7133
|
+
if (!edge.label) continue;
|
|
7134
|
+
const metrics = measureMultilineText(edge.label, FONT_SIZES.edgeLabel, FONT_WEIGHTS.edgeLabel);
|
|
7135
|
+
const halfW = (metrics.width + LABEL_PADDING * 2) / 2;
|
|
7136
|
+
const halfH = (metrics.height + LABEL_PADDING * 2) / 2;
|
|
7137
|
+
const marginX = edge.points[1].x;
|
|
7138
|
+
const topY = Math.min(edge.points[1].y, edge.points[2].y);
|
|
7139
|
+
const botY = Math.max(edge.points[1].y, edge.points[2].y);
|
|
7140
|
+
let labelX = marginX;
|
|
7141
|
+
let labelY = (topY + botY) / 2;
|
|
7142
|
+
for (let attempt = 0; attempt < placedLabels.length + 1; attempt++) {
|
|
7143
|
+
let overlap = false;
|
|
7144
|
+
for (const prev of placedLabels) {
|
|
7145
|
+
const overlapX = Math.abs(labelX - prev.x) < halfW + prev.hw;
|
|
7146
|
+
const overlapY = Math.abs(labelY - prev.y) < halfH + prev.hh;
|
|
7147
|
+
if (overlapX && overlapY) {
|
|
7148
|
+
labelY = prev.y + prev.hh + halfH + 2;
|
|
7149
|
+
overlap = true;
|
|
7150
|
+
break;
|
|
7151
|
+
}
|
|
7152
|
+
}
|
|
7153
|
+
if (!overlap) break;
|
|
7154
|
+
}
|
|
7155
|
+
labelY = Math.max(topY + halfH, Math.min(botY - halfH, labelY));
|
|
7156
|
+
edge.labelPosition = { x: labelX, y: labelY };
|
|
7157
|
+
placedLabels.push({ x: labelX, y: labelY, hw: halfW, hh: halfH });
|
|
7158
|
+
}
|
|
7159
|
+
}
|
|
7000
7160
|
function collectEdgeSegments(elkNode, segments, offsetX, offsetY) {
|
|
7001
7161
|
if (elkNode.edges) {
|
|
7002
7162
|
for (const elkEdge of elkNode.edges) {
|
|
@@ -7337,9 +7497,121 @@ function bundleEdgePaths(edges, nodes, groups, direction) {
|
|
|
7337
7497
|
}
|
|
7338
7498
|
}
|
|
7339
7499
|
}
|
|
7500
|
+
function buildFlatElkGraph(compoundElk) {
|
|
7501
|
+
const flatNodes = [];
|
|
7502
|
+
const flatEdges = [];
|
|
7503
|
+
function collectNodes(node) {
|
|
7504
|
+
if (!node.children) return;
|
|
7505
|
+
for (const child of node.children) {
|
|
7506
|
+
if (child.children) {
|
|
7507
|
+
if (child.children.length > 0) collectNodes(child);
|
|
7508
|
+
} else {
|
|
7509
|
+
flatNodes.push({ id: child.id, width: child.width, height: child.height, labels: child.labels });
|
|
7510
|
+
}
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
collectNodes(compoundElk);
|
|
7514
|
+
const flatNodeIds = new Set(flatNodes.map((n) => n.id));
|
|
7515
|
+
function collectEdges(node) {
|
|
7516
|
+
if (node.edges) {
|
|
7517
|
+
for (const e of node.edges) {
|
|
7518
|
+
if (e.id.endsWith("_internal")) continue;
|
|
7519
|
+
const src = e.sources[0];
|
|
7520
|
+
const tgt = e.targets[0];
|
|
7521
|
+
if (src && tgt && flatNodeIds.has(src) && flatNodeIds.has(tgt)) {
|
|
7522
|
+
flatEdges.push({ id: e.id, sources: [...e.sources], targets: [...e.targets] });
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7525
|
+
}
|
|
7526
|
+
if (node.children) {
|
|
7527
|
+
for (const child of node.children) collectEdges(child);
|
|
7528
|
+
}
|
|
7529
|
+
}
|
|
7530
|
+
collectEdges(compoundElk);
|
|
7531
|
+
return {
|
|
7532
|
+
id: "flat_root",
|
|
7533
|
+
layoutOptions: {
|
|
7534
|
+
"elk.algorithm": "layered",
|
|
7535
|
+
"elk.direction": compoundElk.layoutOptions?.["elk.direction"] ?? "DOWN",
|
|
7536
|
+
"elk.spacing.nodeNode": compoundElk.layoutOptions?.["elk.spacing.nodeNode"] ?? "28",
|
|
7537
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": compoundElk.layoutOptions?.["elk.layered.spacing.nodeNodeBetweenLayers"] ?? "48",
|
|
7538
|
+
"elk.spacing.edgeEdge": "12",
|
|
7539
|
+
"elk.edgeRouting": "ORTHOGONAL",
|
|
7540
|
+
"elk.layered.cycleBreaking.strategy": "DEPTH_FIRST",
|
|
7541
|
+
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
|
|
7542
|
+
"elk.padding": compoundElk.layoutOptions?.["elk.padding"] ?? "[top=40,left=40,bottom=40,right=40]"
|
|
7543
|
+
},
|
|
7544
|
+
children: flatNodes,
|
|
7545
|
+
edges: flatEdges
|
|
7546
|
+
};
|
|
7547
|
+
}
|
|
7548
|
+
function extractFlatLayers(flatResult, layerSpacing, direction) {
|
|
7549
|
+
const isHorizontal = direction === "LR" || direction === "RL";
|
|
7550
|
+
const positions = /* @__PURE__ */ new Map();
|
|
7551
|
+
for (const child of flatResult.children ?? []) {
|
|
7552
|
+
positions.set(child.id, isHorizontal ? child.x ?? 0 : child.y ?? 0);
|
|
7553
|
+
}
|
|
7554
|
+
const sorted = [...positions.entries()].sort((a, b) => a[1] - b[1]);
|
|
7555
|
+
if (sorted.length === 0) return /* @__PURE__ */ new Map();
|
|
7556
|
+
const nodeLayer = /* @__PURE__ */ new Map();
|
|
7557
|
+
const threshold = layerSpacing * 0.6;
|
|
7558
|
+
let layerIdx = 0;
|
|
7559
|
+
let prevPos = sorted[0][1];
|
|
7560
|
+
nodeLayer.set(sorted[0][0], 0);
|
|
7561
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
7562
|
+
if (sorted[i][1] - prevPos > threshold) layerIdx++;
|
|
7563
|
+
nodeLayer.set(sorted[i][0], layerIdx);
|
|
7564
|
+
prevPos = sorted[i][1];
|
|
7565
|
+
}
|
|
7566
|
+
return nodeLayer;
|
|
7567
|
+
}
|
|
7568
|
+
function addLayerConstraintEdges(elkGraph, nodeLayer, subgraphs) {
|
|
7569
|
+
const sgNodeMap = /* @__PURE__ */ new Map();
|
|
7570
|
+
function findSgNodes(node) {
|
|
7571
|
+
if (!node.children) return;
|
|
7572
|
+
for (const child of node.children) {
|
|
7573
|
+
if (child.children && child.children.length > 0) {
|
|
7574
|
+
sgNodeMap.set(child.id, child);
|
|
7575
|
+
findSgNodes(child);
|
|
7576
|
+
}
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
findSgNodes(elkGraph);
|
|
7580
|
+
for (const sg of subgraphs) {
|
|
7581
|
+
const elkSg = sgNodeMap.get(sg.id);
|
|
7582
|
+
if (!elkSg || !elkSg.edges) continue;
|
|
7583
|
+
const members = sg.nodeIds.filter((id) => nodeLayer.has(id)).sort((a, b) => nodeLayer.get(a) - nodeLayer.get(b));
|
|
7584
|
+
if (members.length < 2) continue;
|
|
7585
|
+
const firstLayer = nodeLayer.get(members[0]);
|
|
7586
|
+
const lastLayer = nodeLayer.get(members[members.length - 1]);
|
|
7587
|
+
if (firstLayer === lastLayer) continue;
|
|
7588
|
+
for (let i = 0; i < members.length - 1; i++) {
|
|
7589
|
+
const srcLayer = nodeLayer.get(members[i]);
|
|
7590
|
+
const tgtLayer = nodeLayer.get(members[i + 1]);
|
|
7591
|
+
if (srcLayer < tgtLayer) {
|
|
7592
|
+
elkSg.edges.push({
|
|
7593
|
+
id: `lc_${sg.id}_${i}`,
|
|
7594
|
+
sources: [members[i]],
|
|
7595
|
+
targets: [members[i + 1]]
|
|
7596
|
+
});
|
|
7597
|
+
}
|
|
7598
|
+
}
|
|
7599
|
+
if (sg.children.length > 0) {
|
|
7600
|
+
addLayerConstraintEdges(elkGraph, nodeLayer, sg.children);
|
|
7601
|
+
}
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7340
7604
|
function layoutGraphSync(graph, options = {}) {
|
|
7341
7605
|
const opts = { ...DEFAULTS2, ...options };
|
|
7342
7606
|
const elkGraph = mermaidToElk(graph, opts);
|
|
7607
|
+
if (graph.subgraphs.length > 0) {
|
|
7608
|
+
const flatElk = buildFlatElkGraph(elkGraph);
|
|
7609
|
+
if ((flatElk.children?.length ?? 0) >= 2 && (flatElk.edges?.length ?? 0) > 0) {
|
|
7610
|
+
const flatResult = elkLayoutSync(flatElk);
|
|
7611
|
+
const nodeLayer = extractFlatLayers(flatResult, opts.layerSpacing, graph.direction);
|
|
7612
|
+
addLayerConstraintEdges(elkGraph, nodeLayer, graph.subgraphs);
|
|
7613
|
+
}
|
|
7614
|
+
}
|
|
7343
7615
|
const result = elkLayoutSync(elkGraph);
|
|
7344
7616
|
return elkToPositioned(result, graph, DEFAULTS2.mergeEdges);
|
|
7345
7617
|
}
|
|
@@ -7439,7 +7711,6 @@ function renderGroup(group, font) {
|
|
|
7439
7711
|
}
|
|
7440
7712
|
function renderEdge(edge) {
|
|
7441
7713
|
if (edge.points.length < 2) return "";
|
|
7442
|
-
const pathData = pointsToPolylinePath(edge.points);
|
|
7443
7714
|
const dashArray = edge.style === "dotted" ? ' stroke-dasharray="4 4"' : "";
|
|
7444
7715
|
const baseStrokeWidth = edge.style === "thick" ? STROKE_WIDTHS.connector * 2 : STROKE_WIDTHS.connector;
|
|
7445
7716
|
const strokeColor = escapeAttr(edge.inlineStyle?.stroke ?? "var(--_line)");
|
|
@@ -7459,10 +7730,41 @@ function renderEdge(edge) {
|
|
|
7459
7730
|
if (edge.label) {
|
|
7460
7731
|
dataAttrs.push(`data-label="${escapeAttr(edge.label)}"`);
|
|
7461
7732
|
}
|
|
7462
|
-
return `<
|
|
7733
|
+
return `<path ${dataAttrs.join(" ")} d="${pointsToRoundedPath(edge.points)}" fill="none" stroke="${strokeColor}" stroke-width="${strokeWidth}"${dashArray}${markers} />`;
|
|
7463
7734
|
}
|
|
7464
|
-
function
|
|
7465
|
-
|
|
7735
|
+
function pointsToRoundedPath(points, radius = 6) {
|
|
7736
|
+
if (points.length < 2) return "";
|
|
7737
|
+
if (points.length === 2 || radius <= 0) {
|
|
7738
|
+
return `M${points[0].x},${points[0].y}` + points.slice(1).map((p) => `L${p.x},${p.y}`).join("");
|
|
7739
|
+
}
|
|
7740
|
+
const parts = [`M${points[0].x},${points[0].y}`];
|
|
7741
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
7742
|
+
const prev = points[i - 1];
|
|
7743
|
+
const curr = points[i];
|
|
7744
|
+
const next = points[i + 1];
|
|
7745
|
+
const lenIn = Math.abs(curr.x - prev.x) + Math.abs(curr.y - prev.y);
|
|
7746
|
+
const lenOut = Math.abs(next.x - curr.x) + Math.abs(next.y - curr.y);
|
|
7747
|
+
const r2 = Math.min(radius, lenIn / 2, lenOut / 2);
|
|
7748
|
+
if (r2 < 1) {
|
|
7749
|
+
parts.push(`L${curr.x},${curr.y}`);
|
|
7750
|
+
continue;
|
|
7751
|
+
}
|
|
7752
|
+
const dx1 = curr.x - prev.x;
|
|
7753
|
+
const dy1 = curr.y - prev.y;
|
|
7754
|
+
const d1 = Math.sqrt(dx1 * dx1 + dy1 * dy1) || 1;
|
|
7755
|
+
const startX = curr.x - dx1 / d1 * r2;
|
|
7756
|
+
const startY = curr.y - dy1 / d1 * r2;
|
|
7757
|
+
const dx2 = next.x - curr.x;
|
|
7758
|
+
const dy2 = next.y - curr.y;
|
|
7759
|
+
const d2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
|
|
7760
|
+
const endX = curr.x + dx2 / d2 * r2;
|
|
7761
|
+
const endY = curr.y + dy2 / d2 * r2;
|
|
7762
|
+
parts.push(`L${startX},${startY}`);
|
|
7763
|
+
parts.push(`Q${curr.x},${curr.y} ${endX},${endY}`);
|
|
7764
|
+
}
|
|
7765
|
+
const last = points[points.length - 1];
|
|
7766
|
+
parts.push(`L${last.x},${last.y}`);
|
|
7767
|
+
return parts.join("");
|
|
7466
7768
|
}
|
|
7467
7769
|
function renderEdgeLabel(edge, font) {
|
|
7468
7770
|
const mid = edge.labelPosition ?? edgeMidpoint(edge.points);
|