@graph-artifact/core 0.1.0
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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/ThemeContext.d.ts +47 -0
- package/dist/ThemeContext.js +81 -0
- package/dist/components/DiagramCanvas.d.ts +8 -0
- package/dist/components/DiagramCanvas.js +19 -0
- package/dist/components/GraphCanvas.d.ts +9 -0
- package/dist/components/GraphCanvas.js +104 -0
- package/dist/components/NodeDetail.d.ts +9 -0
- package/dist/components/NodeDetail.js +127 -0
- package/dist/components/edges/RoutedEdge.d.ts +11 -0
- package/dist/components/edges/RoutedEdge.js +199 -0
- package/dist/components/nodes/ClassNode.d.ts +5 -0
- package/dist/components/nodes/ClassNode.js +62 -0
- package/dist/components/nodes/EntityNode.d.ts +5 -0
- package/dist/components/nodes/EntityNode.js +57 -0
- package/dist/components/nodes/FlowNode.d.ts +5 -0
- package/dist/components/nodes/FlowNode.js +144 -0
- package/dist/components/nodes/SequenceNodes.d.ts +33 -0
- package/dist/components/nodes/SequenceNodes.js +205 -0
- package/dist/components/nodes/StateNode.d.ts +5 -0
- package/dist/components/nodes/StateNode.js +71 -0
- package/dist/components/nodes/SubgraphNode.d.ts +5 -0
- package/dist/components/nodes/SubgraphNode.js +16 -0
- package/dist/config.d.ts +138 -0
- package/dist/config.js +165 -0
- package/dist/core.d.ts +12 -0
- package/dist/core.js +7 -0
- package/dist/diagrams/detect.d.ts +2 -0
- package/dist/diagrams/detect.js +8 -0
- package/dist/diagrams/plugins.d.ts +2 -0
- package/dist/diagrams/plugins.js +45 -0
- package/dist/diagrams/registry.d.ts +3 -0
- package/dist/diagrams/registry.js +13 -0
- package/dist/diagrams/types.d.ts +7 -0
- package/dist/diagrams/types.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/layout/dagre/index.d.ts +31 -0
- package/dist/layout/dagre/index.js +224 -0
- package/dist/layout/dagre/nodeSizing.d.ts +32 -0
- package/dist/layout/dagre/nodeSizing.js +202 -0
- package/dist/layout/edges/buildEdges.d.ts +18 -0
- package/dist/layout/edges/buildEdges.js +405 -0
- package/dist/layout/edges/classify.d.ts +13 -0
- package/dist/layout/edges/classify.js +36 -0
- package/dist/layout/edges/diamondHandles.d.ts +23 -0
- package/dist/layout/edges/diamondHandles.js +108 -0
- package/dist/layout/edges/index.d.ts +10 -0
- package/dist/layout/edges/index.js +8 -0
- package/dist/layout/edges/paths.d.ts +57 -0
- package/dist/layout/edges/paths.js +279 -0
- package/dist/layout/index.d.ts +59 -0
- package/dist/layout/index.js +131 -0
- package/dist/layout/intersect/circle.d.ts +2 -0
- package/dist/layout/intersect/circle.js +14 -0
- package/dist/layout/intersect/diamond.d.ts +9 -0
- package/dist/layout/intersect/diamond.js +21 -0
- package/dist/layout/intersect/index.d.ts +17 -0
- package/dist/layout/intersect/index.js +28 -0
- package/dist/layout/intersect/rect.d.ts +10 -0
- package/dist/layout/intersect/rect.js +31 -0
- package/dist/layout/intersect/rectRounded.d.ts +20 -0
- package/dist/layout/intersect/rectRounded.js +48 -0
- package/dist/layout/mindmapLayout.d.ts +13 -0
- package/dist/layout/mindmapLayout.js +299 -0
- package/dist/layout/sequenceLayout.d.ts +24 -0
- package/dist/layout/sequenceLayout.js +414 -0
- package/dist/layout/subgraph.d.ts +26 -0
- package/dist/layout/subgraph.js +63 -0
- package/dist/layout/types.d.ts +34 -0
- package/dist/layout/types.js +8 -0
- package/dist/parsers/classDiagram.d.ts +2 -0
- package/dist/parsers/classDiagram.js +105 -0
- package/dist/parsers/er.d.ts +2 -0
- package/dist/parsers/er.js +97 -0
- package/dist/parsers/flowchart.d.ts +2 -0
- package/dist/parsers/flowchart.js +191 -0
- package/dist/parsers/helpers.d.ts +4 -0
- package/dist/parsers/helpers.js +8 -0
- package/dist/parsers/index.d.ts +7 -0
- package/dist/parsers/index.js +19 -0
- package/dist/parsers/mindmap.d.ts +2 -0
- package/dist/parsers/mindmap.js +124 -0
- package/dist/parsers/sequence.d.ts +18 -0
- package/dist/parsers/sequence.js +196 -0
- package/dist/parsers/state.d.ts +2 -0
- package/dist/parsers/state.js +68 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.js +9 -0
- package/dist/reactDefaults.d.ts +5 -0
- package/dist/reactDefaults.js +37 -0
- package/dist/renderMarkdown.d.ts +9 -0
- package/dist/renderMarkdown.js +103 -0
- package/dist/swagger.d.ts +113 -0
- package/dist/swagger.js +551 -0
- package/dist/theme/dark.d.ts +8 -0
- package/dist/theme/dark.js +190 -0
- package/dist/theme/index.d.ts +18 -0
- package/dist/theme/index.js +29 -0
- package/dist/theme/light.d.ts +8 -0
- package/dist/theme/light.js +190 -0
- package/dist/theme/types.d.ts +97 -0
- package/dist/theme/types.js +7 -0
- package/dist/types.d.ts +235 -0
- package/dist/types.js +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { parseFlowchart } from '../parsers/flowchart.js';
|
|
2
|
+
import { parseStateDiagram } from '../parsers/state.js';
|
|
3
|
+
import { parseErDiagram } from '../parsers/er.js';
|
|
4
|
+
import { parseClassDiagram } from '../parsers/classDiagram.js';
|
|
5
|
+
import { parseSequenceDiagram } from '../parsers/sequence.js';
|
|
6
|
+
import { parseMindmap } from '../parsers/mindmap.js';
|
|
7
|
+
const startsWith = (regex) => (syntax) => regex.test(syntax.trimStart());
|
|
8
|
+
export const builtinDiagramPlugins = [
|
|
9
|
+
{
|
|
10
|
+
id: 'mindmap',
|
|
11
|
+
detect: startsWith(/^mindmap\b/i),
|
|
12
|
+
parse: parseMindmap,
|
|
13
|
+
renderer: 'graph',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'flowchart',
|
|
17
|
+
detect: startsWith(/^(graph|flowchart)\s+/i),
|
|
18
|
+
parse: parseFlowchart,
|
|
19
|
+
renderer: 'graph',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'state',
|
|
23
|
+
detect: startsWith(/^stateDiagram(?:-v2)?\b/i),
|
|
24
|
+
parse: parseStateDiagram,
|
|
25
|
+
renderer: 'graph',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'er',
|
|
29
|
+
detect: startsWith(/^erDiagram\b/i),
|
|
30
|
+
parse: parseErDiagram,
|
|
31
|
+
renderer: 'graph',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'class',
|
|
35
|
+
detect: startsWith(/^classDiagram\b/i),
|
|
36
|
+
parse: parseClassDiagram,
|
|
37
|
+
renderer: 'graph',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'sequence',
|
|
41
|
+
detect: startsWith(/^sequenceDiagram\b/i),
|
|
42
|
+
parse: parseSequenceDiagram,
|
|
43
|
+
renderer: 'graph',
|
|
44
|
+
},
|
|
45
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { builtinDiagramPlugins } from './plugins.js';
|
|
2
|
+
const plugins = [...builtinDiagramPlugins];
|
|
3
|
+
export function registerDiagramPlugin(plugin) {
|
|
4
|
+
const index = plugins.findIndex((p) => p.id === plugin.id);
|
|
5
|
+
if (index >= 0) {
|
|
6
|
+
plugins[index] = plugin;
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
plugins.push(plugin);
|
|
10
|
+
}
|
|
11
|
+
export function getDiagramPlugins() {
|
|
12
|
+
return plugins;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './core.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dagre/ — Dagre layout algorithm.
|
|
3
|
+
*
|
|
4
|
+
* Builds a dagre graph from parsed data, runs the layout algorithm,
|
|
5
|
+
* and extracts node positions + edge waypoints for downstream use.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors Mermaid's layout-algorithms/dagre/ pattern:
|
|
8
|
+
* the layout algorithm is isolated behind a clean module boundary.
|
|
9
|
+
*/
|
|
10
|
+
import dagre from '@dagrejs/dagre';
|
|
11
|
+
import type { ParsedGraph, ParsedNode } from '../../types.js';
|
|
12
|
+
import type { GraphConfig } from '../../config.js';
|
|
13
|
+
import type { NodePosition, HandlePositions, EdgeWaypoints } from '../types.js';
|
|
14
|
+
export type { NodePosition, HandlePositions, EdgeWaypoints } from '../types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Maps graph direction to handle positions.
|
|
17
|
+
* Forward edges use primary handles (e.g. Bottom→Top for TB).
|
|
18
|
+
* Back-edges route to side handles to avoid passing through intermediate nodes.
|
|
19
|
+
*/
|
|
20
|
+
export declare function directionToHandles(direction: ParsedGraph['direction']): HandlePositions;
|
|
21
|
+
export declare function createDagreGraph(parsed: ParsedGraph, layoutConfig: GraphConfig['layout']): dagre.graphlib.Graph;
|
|
22
|
+
/**
|
|
23
|
+
* Extracts top-left positions from dagre's center-based coordinates.
|
|
24
|
+
* Returns a Map of nodeId → { x, y, width, height }.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractPositions(graph: dagre.graphlib.Graph, nodes: ParsedNode[]): Map<string, NodePosition>;
|
|
27
|
+
/**
|
|
28
|
+
* Extracts dagre's computed edge waypoints for every edge.
|
|
29
|
+
* These waypoints route around intermediate nodes automatically.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractEdgeWaypoints(graph: dagre.graphlib.Graph, parsed: ParsedGraph): EdgeWaypoints[];
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dagre/ — Dagre layout algorithm.
|
|
3
|
+
*
|
|
4
|
+
* Builds a dagre graph from parsed data, runs the layout algorithm,
|
|
5
|
+
* and extracts node positions + edge waypoints for downstream use.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors Mermaid's layout-algorithms/dagre/ pattern:
|
|
8
|
+
* the layout algorithm is isolated behind a clean module boundary.
|
|
9
|
+
*/
|
|
10
|
+
import dagre from '@dagrejs/dagre';
|
|
11
|
+
import { Position } from '@xyflow/react';
|
|
12
|
+
import { nodeSize } from './nodeSizing.js';
|
|
13
|
+
// ─── Handle Positions by Direction ──────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Maps graph direction to handle positions.
|
|
16
|
+
* Forward edges use primary handles (e.g. Bottom→Top for TB).
|
|
17
|
+
* Back-edges route to side handles to avoid passing through intermediate nodes.
|
|
18
|
+
*/
|
|
19
|
+
export function directionToHandles(direction) {
|
|
20
|
+
switch (direction) {
|
|
21
|
+
case 'LR':
|
|
22
|
+
return {
|
|
23
|
+
sourcePosition: Position.Right,
|
|
24
|
+
targetPosition: Position.Left,
|
|
25
|
+
backEdgeSource: Position.Top,
|
|
26
|
+
backEdgeTarget: Position.Top,
|
|
27
|
+
};
|
|
28
|
+
case 'RL':
|
|
29
|
+
return {
|
|
30
|
+
sourcePosition: Position.Left,
|
|
31
|
+
targetPosition: Position.Right,
|
|
32
|
+
backEdgeSource: Position.Top,
|
|
33
|
+
backEdgeTarget: Position.Top,
|
|
34
|
+
};
|
|
35
|
+
case 'BT':
|
|
36
|
+
return {
|
|
37
|
+
sourcePosition: Position.Top,
|
|
38
|
+
targetPosition: Position.Bottom,
|
|
39
|
+
backEdgeSource: Position.Right,
|
|
40
|
+
backEdgeTarget: Position.Right,
|
|
41
|
+
};
|
|
42
|
+
case 'TB':
|
|
43
|
+
default:
|
|
44
|
+
return {
|
|
45
|
+
sourcePosition: Position.Bottom,
|
|
46
|
+
targetPosition: Position.Top,
|
|
47
|
+
backEdgeSource: Position.Right,
|
|
48
|
+
backEdgeTarget: Position.Right,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// ─── Dagre Graph Construction ───────────────────────────────────────────────
|
|
53
|
+
export function createDagreGraph(parsed, layoutConfig) {
|
|
54
|
+
validateParsedGraph(parsed);
|
|
55
|
+
const g = new dagre.graphlib.Graph({ multigraph: true, compound: true });
|
|
56
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
57
|
+
const rankSepByType = {
|
|
58
|
+
er: layoutConfig.erRankSep,
|
|
59
|
+
state: layoutConfig.stateRankSep,
|
|
60
|
+
flowchart: layoutConfig.rankSep,
|
|
61
|
+
class: layoutConfig.rankSep,
|
|
62
|
+
sequence: layoutConfig.rankSep,
|
|
63
|
+
mindmap: layoutConfig.rankSep,
|
|
64
|
+
gitGraph: layoutConfig.rankSep,
|
|
65
|
+
};
|
|
66
|
+
const nodeSepByType = {
|
|
67
|
+
er: layoutConfig.erNodeSep,
|
|
68
|
+
state: layoutConfig.stateNodeSep,
|
|
69
|
+
flowchart: layoutConfig.nodeSep,
|
|
70
|
+
class: layoutConfig.nodeSep,
|
|
71
|
+
sequence: layoutConfig.nodeSep,
|
|
72
|
+
mindmap: layoutConfig.nodeSep,
|
|
73
|
+
gitGraph: layoutConfig.nodeSep,
|
|
74
|
+
};
|
|
75
|
+
g.setGraph({
|
|
76
|
+
rankdir: parsed.direction,
|
|
77
|
+
ranksep: rankSepByType[parsed.diagramType] ?? layoutConfig.rankSep,
|
|
78
|
+
nodesep: nodeSepByType[parsed.diagramType] ?? layoutConfig.nodeSep,
|
|
79
|
+
marginx: 8,
|
|
80
|
+
marginy: 8,
|
|
81
|
+
ranker: 'network-simplex',
|
|
82
|
+
});
|
|
83
|
+
for (const node of parsed.nodes) {
|
|
84
|
+
const size = nodeSize(node, parsed.diagramType, layoutConfig.nodeSizing);
|
|
85
|
+
g.setNode(node.id, { width: size.width, height: size.height });
|
|
86
|
+
}
|
|
87
|
+
// Defensive: if a parser emits an edge endpoint that is missing from
|
|
88
|
+
// parsed.nodes, dagre creates an implicit node with undefined label data,
|
|
89
|
+
// which can crash rank assignment (network simplex). Materialize a fallback
|
|
90
|
+
// node shape/dimensions so layout remains stable.
|
|
91
|
+
const fallbackNode = {
|
|
92
|
+
width: layoutConfig.nodeSizing.flow.minWidth,
|
|
93
|
+
height: layoutConfig.nodeSizing.flow.height,
|
|
94
|
+
};
|
|
95
|
+
// Register subgraphs as compound parent nodes so dagre clusters
|
|
96
|
+
// their children together and adds spacing between groups.
|
|
97
|
+
if (parsed.subgraphs) {
|
|
98
|
+
const pad = layoutConfig.subgraphPadding;
|
|
99
|
+
for (const subgraph of parsed.subgraphs) {
|
|
100
|
+
g.setNode(subgraph.id, {
|
|
101
|
+
clusterLabelPos: 'top',
|
|
102
|
+
paddingTop: pad + 14, // extra for label
|
|
103
|
+
paddingBottom: pad,
|
|
104
|
+
paddingLeft: pad,
|
|
105
|
+
paddingRight: pad,
|
|
106
|
+
});
|
|
107
|
+
for (const nodeId of subgraph.nodeIds) {
|
|
108
|
+
if (!g.hasNode(nodeId))
|
|
109
|
+
g.setNode(nodeId, fallbackNode);
|
|
110
|
+
g.setParent(nodeId, subgraph.id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
parsed.edges.forEach((edge, i) => {
|
|
115
|
+
// Pass label dimensions to dagre so it spaces edges apart
|
|
116
|
+
// and generates better waypoints. Without this, dagre treats
|
|
117
|
+
// all edges as zero-width and packs waypoints tight together.
|
|
118
|
+
const labelWidth = edge.label ? edge.label.length * 8 + 16 : 0;
|
|
119
|
+
const labelHeight = edge.label ? 20 : 0;
|
|
120
|
+
// For inheritance edges (dagreReversed), reverse the direction in dagre
|
|
121
|
+
// so parents rank above children, while the visual arrow stays child→parent.
|
|
122
|
+
const dagreSrc = edge.dagreReversed ? edge.target : edge.source;
|
|
123
|
+
const dagreTgt = edge.dagreReversed ? edge.source : edge.target;
|
|
124
|
+
if (!g.hasNode(dagreSrc))
|
|
125
|
+
g.setNode(dagreSrc, fallbackNode);
|
|
126
|
+
if (!g.hasNode(dagreTgt))
|
|
127
|
+
g.setNode(dagreTgt, fallbackNode);
|
|
128
|
+
// Use index as edge name for multigraph — prevents overwriting when
|
|
129
|
+
// multiple edges exist between the same source→target pair.
|
|
130
|
+
g.setEdge(dagreSrc, dagreTgt, {
|
|
131
|
+
width: labelWidth,
|
|
132
|
+
height: labelHeight,
|
|
133
|
+
labelpos: 'c',
|
|
134
|
+
}, `e${i}`);
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
dagre.layout(g);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
141
|
+
throw new Error(`Layout failed for diagram type '${parsed.diagramType}'. ${msg}`);
|
|
142
|
+
}
|
|
143
|
+
return g;
|
|
144
|
+
}
|
|
145
|
+
function validateParsedGraph(parsed) {
|
|
146
|
+
const issues = [];
|
|
147
|
+
const nodeIds = new Set();
|
|
148
|
+
for (const node of parsed.nodes) {
|
|
149
|
+
const id = String(node.id ?? '').trim();
|
|
150
|
+
if (!id) {
|
|
151
|
+
issues.push('Found node with empty id.');
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (nodeIds.has(id))
|
|
155
|
+
issues.push(`Duplicate node id '${id}'.`);
|
|
156
|
+
nodeIds.add(id);
|
|
157
|
+
}
|
|
158
|
+
parsed.edges.forEach((edge, i) => {
|
|
159
|
+
const src = String(edge.source ?? '').trim();
|
|
160
|
+
const tgt = String(edge.target ?? '').trim();
|
|
161
|
+
if (!src)
|
|
162
|
+
issues.push(`Edge #${i} has empty source.`);
|
|
163
|
+
if (!tgt)
|
|
164
|
+
issues.push(`Edge #${i} has empty target.`);
|
|
165
|
+
if (src && !nodeIds.has(src))
|
|
166
|
+
issues.push(`Edge #${i} source '${src}' does not exist.`);
|
|
167
|
+
if (tgt && !nodeIds.has(tgt))
|
|
168
|
+
issues.push(`Edge #${i} target '${tgt}' does not exist.`);
|
|
169
|
+
});
|
|
170
|
+
if (parsed.subgraphs) {
|
|
171
|
+
for (const sg of parsed.subgraphs) {
|
|
172
|
+
for (const nid of sg.nodeIds) {
|
|
173
|
+
const id = String(nid ?? '').trim();
|
|
174
|
+
if (!id) {
|
|
175
|
+
issues.push(`Subgraph '${sg.id}' contains empty node id.`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (!nodeIds.has(id))
|
|
179
|
+
issues.push(`Subgraph '${sg.id}' references missing node '${id}'.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (issues.length > 0) {
|
|
184
|
+
throw new Error(`Invalid diagram graph: ${issues.join(' ')}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// ─── Position Extraction ────────────────────────────────────────────────────
|
|
188
|
+
/**
|
|
189
|
+
* Extracts top-left positions from dagre's center-based coordinates.
|
|
190
|
+
* Returns a Map of nodeId → { x, y, width, height }.
|
|
191
|
+
*/
|
|
192
|
+
export function extractPositions(graph, nodes) {
|
|
193
|
+
const positions = new Map();
|
|
194
|
+
for (const node of nodes) {
|
|
195
|
+
const pos = graph.node(node.id);
|
|
196
|
+
positions.set(node.id, {
|
|
197
|
+
x: pos.x - pos.width / 2,
|
|
198
|
+
y: pos.y - pos.height / 2,
|
|
199
|
+
width: pos.width,
|
|
200
|
+
height: pos.height,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return positions;
|
|
204
|
+
}
|
|
205
|
+
// ─── Edge Waypoint Extraction ────────────────────────────────────────────────
|
|
206
|
+
/**
|
|
207
|
+
* Extracts dagre's computed edge waypoints for every edge.
|
|
208
|
+
* These waypoints route around intermediate nodes automatically.
|
|
209
|
+
*/
|
|
210
|
+
export function extractEdgeWaypoints(graph, parsed) {
|
|
211
|
+
return parsed.edges.map((edge, i) => {
|
|
212
|
+
// Use the same source/target order we gave dagre (may be reversed for inheritance edges)
|
|
213
|
+
const dagreSrc = edge.dagreReversed ? edge.target : edge.source;
|
|
214
|
+
const dagreTgt = edge.dagreReversed ? edge.source : edge.target;
|
|
215
|
+
const dagreEdge = graph.edge(dagreSrc, dagreTgt, `e${i}`);
|
|
216
|
+
let points = dagreEdge?.points
|
|
217
|
+
? dagreEdge.points.map((p) => ({ x: p.x, y: p.y }))
|
|
218
|
+
: [];
|
|
219
|
+
// Reverse waypoints back so they flow from visual source → visual target
|
|
220
|
+
if (edge.dagreReversed)
|
|
221
|
+
points = points.reverse();
|
|
222
|
+
return { source: edge.source, target: edge.target, points };
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodeSizing.ts — Per-diagram-type node dimension calculations.
|
|
3
|
+
*
|
|
4
|
+
* Each diagram type has its own sizing function. The dispatcher
|
|
5
|
+
* `nodeSize()` selects the right one based on DiagramType.
|
|
6
|
+
*
|
|
7
|
+
* These dimensions are the SINGLE SOURCE OF TRUTH for node sizes.
|
|
8
|
+
* They feed dagre for positioning, intersection math for edge routing,
|
|
9
|
+
* and are passed to components as layoutWidth/layoutHeight.
|
|
10
|
+
*
|
|
11
|
+
* DIMENSIONAL CONTRACT:
|
|
12
|
+
* Card-based nodes (ER, Class) render at their natural CSS height —
|
|
13
|
+
* no fixed height is set. The estimates here are used ONLY by dagre
|
|
14
|
+
* for node spacing and by intersection math for edge endpoints.
|
|
15
|
+
* Estimates should be generous (round up) to ensure dagre allocates
|
|
16
|
+
* enough room. React Flow measures the actual DOM height after render.
|
|
17
|
+
*
|
|
18
|
+
* Shape-based nodes (Flow, State) use layoutWidth/layoutHeight
|
|
19
|
+
* directly since their geometry is deterministic (circles, diamonds, etc).
|
|
20
|
+
*
|
|
21
|
+
* All sizing reads from config so consumers can tune dimensions
|
|
22
|
+
* via `configure({ layout: { nodeSizing: { ... } } })`.
|
|
23
|
+
*/
|
|
24
|
+
import type { ParsedNode, GraphDiagramType } from '../../types.js';
|
|
25
|
+
import type { GraphConfig } from '../../config.js';
|
|
26
|
+
type NodeSizingConfig = GraphConfig['layout']['nodeSizing'];
|
|
27
|
+
interface Dimensions {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function nodeSize(node: ParsedNode, diagramType: GraphDiagramType, sizing: NodeSizingConfig): Dimensions;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodeSizing.ts — Per-diagram-type node dimension calculations.
|
|
3
|
+
*
|
|
4
|
+
* Each diagram type has its own sizing function. The dispatcher
|
|
5
|
+
* `nodeSize()` selects the right one based on DiagramType.
|
|
6
|
+
*
|
|
7
|
+
* These dimensions are the SINGLE SOURCE OF TRUTH for node sizes.
|
|
8
|
+
* They feed dagre for positioning, intersection math for edge routing,
|
|
9
|
+
* and are passed to components as layoutWidth/layoutHeight.
|
|
10
|
+
*
|
|
11
|
+
* DIMENSIONAL CONTRACT:
|
|
12
|
+
* Card-based nodes (ER, Class) render at their natural CSS height —
|
|
13
|
+
* no fixed height is set. The estimates here are used ONLY by dagre
|
|
14
|
+
* for node spacing and by intersection math for edge endpoints.
|
|
15
|
+
* Estimates should be generous (round up) to ensure dagre allocates
|
|
16
|
+
* enough room. React Flow measures the actual DOM height after render.
|
|
17
|
+
*
|
|
18
|
+
* Shape-based nodes (Flow, State) use layoutWidth/layoutHeight
|
|
19
|
+
* directly since their geometry is deterministic (circles, diamonds, etc).
|
|
20
|
+
*
|
|
21
|
+
* All sizing reads from config so consumers can tune dimensions
|
|
22
|
+
* via `configure({ layout: { nodeSizing: { ... } } })`.
|
|
23
|
+
*/
|
|
24
|
+
import { getConfig } from '../../config.js';
|
|
25
|
+
function clampWidth(px) {
|
|
26
|
+
const { layout } = getConfig();
|
|
27
|
+
return Math.max(layout.nodeSizing.common.minWidth, Math.min(layout.nodeSizing.common.maxWidth, Math.ceil(px)));
|
|
28
|
+
}
|
|
29
|
+
function stripMarkdown(input) {
|
|
30
|
+
return input
|
|
31
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
32
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
33
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
34
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
|
|
35
|
+
.replace(/[_~#>|-]/g, '')
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
function labelMetrics(label) {
|
|
39
|
+
const normalized = label.replace(/\\n/g, '\n');
|
|
40
|
+
const lines = normalized.split('\n');
|
|
41
|
+
const maxChars = Math.max(1, ...lines.map((line) => stripMarkdown(line).length || 1));
|
|
42
|
+
return { maxChars, lines: Math.max(1, lines.length) };
|
|
43
|
+
}
|
|
44
|
+
function estimatedWrappedLineCount(label, contentCharsPerLine) {
|
|
45
|
+
const normalized = label.replace(/\\n/g, '\n');
|
|
46
|
+
const lines = normalized.split('\n');
|
|
47
|
+
let total = 0;
|
|
48
|
+
const charsPerLine = Math.max(8, contentCharsPerLine);
|
|
49
|
+
for (const rawLine of lines) {
|
|
50
|
+
const clean = stripMarkdown(rawLine);
|
|
51
|
+
const len = Math.max(1, clean.length);
|
|
52
|
+
total += Math.max(1, Math.ceil(len / charsPerLine));
|
|
53
|
+
}
|
|
54
|
+
return Math.max(1, total);
|
|
55
|
+
}
|
|
56
|
+
// ─── Flowchart ──────────────────────────────────────────────────────────────
|
|
57
|
+
function flowchartNodeSize(node, sizing) {
|
|
58
|
+
const common = sizing.common;
|
|
59
|
+
const { maxChars } = labelMetrics(node.label);
|
|
60
|
+
const dynamicWidth = Math.max(sizing.flow.minWidth, maxChars * sizing.charWidth + sizing.flow.padding);
|
|
61
|
+
const width = clampWidth(dynamicWidth);
|
|
62
|
+
const contentCharsPerLine = Math.max(8, Math.floor((width - sizing.flow.padding) / Math.max(1, sizing.charWidth)));
|
|
63
|
+
const wrappedLines = estimatedWrappedLineCount(node.label, contentCharsPerLine);
|
|
64
|
+
const dynamicHeight = Math.max(sizing.flow.height, common.lineBaseHeight + wrappedLines * common.lineStepHeight);
|
|
65
|
+
if (node.shape === 'diamond') {
|
|
66
|
+
// The visual diamond is a rotated square — its bounding box is side * sqrt(2)
|
|
67
|
+
const baseSide = Math.max(sizing.flow.diamondSize, dynamicWidth * 0.72, dynamicHeight * 0.72);
|
|
68
|
+
const diagSize = Math.ceil(baseSide * Math.SQRT2);
|
|
69
|
+
return { width: diagSize, height: diagSize };
|
|
70
|
+
}
|
|
71
|
+
if (node.shape === 'circle') {
|
|
72
|
+
const size = Math.max(sizing.flow.circleSize, dynamicWidth * 0.85, dynamicHeight * 0.85);
|
|
73
|
+
return { width: Math.ceil(size), height: Math.ceil(size) };
|
|
74
|
+
}
|
|
75
|
+
if (node.shape === 'cylinder') {
|
|
76
|
+
return {
|
|
77
|
+
width: Math.max(sizing.flow.cylinderWidth, dynamicWidth),
|
|
78
|
+
height: Math.max(sizing.flow.cylinderHeight, dynamicHeight + 10),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
width,
|
|
83
|
+
height: dynamicHeight,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// ─── State Diagram ──────────────────────────────────────────────────────────
|
|
87
|
+
function stateNodeSize(node, sizing) {
|
|
88
|
+
const common = sizing.common;
|
|
89
|
+
const { maxChars, lines } = labelMetrics(node.label);
|
|
90
|
+
if (node.shape === 'circle') {
|
|
91
|
+
return { width: sizing.state.circleSize, height: sizing.state.circleSize };
|
|
92
|
+
}
|
|
93
|
+
if (node.shape === 'doublecircle') {
|
|
94
|
+
// Include border in bounding box so intersection math hits the visual boundary
|
|
95
|
+
const size = sizing.state.circleSize + sizing.state.doublecirclePad
|
|
96
|
+
+ sizing.state.doublecircleBorder * 2;
|
|
97
|
+
return { width: size, height: size };
|
|
98
|
+
}
|
|
99
|
+
// border-box dimensions: content + padding + border extras
|
|
100
|
+
const textWidth = Math.max(sizing.state.minWidth, maxChars * sizing.charWidth + sizing.state.padding);
|
|
101
|
+
const textHeight = Math.max(sizing.state.height, common.lineBaseHeight + lines * common.lineStepHeight);
|
|
102
|
+
return {
|
|
103
|
+
width: textWidth + sizing.state.borderExtra,
|
|
104
|
+
height: textHeight + sizing.state.borderExtra,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// ─── ER Diagram ─────────────────────────────────────────────────────────────
|
|
108
|
+
function erNodeSize(node, sizing) {
|
|
109
|
+
const attrCount = node.attributes?.length ?? 0;
|
|
110
|
+
const attrs = node.attributes ?? [];
|
|
111
|
+
// Height breakdown (mirrors EntityNode CSS):
|
|
112
|
+
// headerHeight: header padding (space[2]*2) + font (size.lg) + line-height
|
|
113
|
+
// sectionPad: attribute wrapper padding (space[2]*2 = 8+8 = 16px)
|
|
114
|
+
// rowHeight: per-row padding (space[1]*2 = 4+4) + font (size.md) * line-height
|
|
115
|
+
// dividers: 1px borderBottom between attribute rows
|
|
116
|
+
// borderExtra: card border (borderWidth.sm * 2 = 1+1 = 2px)
|
|
117
|
+
const sectionPad = attrCount > 0 ? 16 : 0;
|
|
118
|
+
const dividers = attrCount > 1 ? attrCount - 1 : 0;
|
|
119
|
+
const badgeRoom = attrs.some((attr) => {
|
|
120
|
+
const t = attr.trim().split(/\s+/);
|
|
121
|
+
return t[2] === 'PK' || t[2] === 'FK' || t[2] === 'UK';
|
|
122
|
+
}) ? 44 : 0;
|
|
123
|
+
// Width mirrors EntityNode row composition:
|
|
124
|
+
// section horizontal padding (24) + type column (90) + gap (8)
|
|
125
|
+
// + longest name content + optional badge room.
|
|
126
|
+
const headerChars = Math.max(0, node.label.length);
|
|
127
|
+
const maxNameChars = attrs.reduce((max, attr) => {
|
|
128
|
+
const t = attr.trim().split(/\s+/);
|
|
129
|
+
return Math.max(max, (t[1] ?? '').length);
|
|
130
|
+
}, 0);
|
|
131
|
+
const contentChars = Math.max(headerChars, maxNameChars);
|
|
132
|
+
const estimatedWidth = Math.max(sizing.er.width, 24 + 90 + 8 + contentChars * sizing.charWidth + badgeRoom);
|
|
133
|
+
return {
|
|
134
|
+
width: clampWidth(estimatedWidth),
|
|
135
|
+
height: sizing.er.headerHeight
|
|
136
|
+
+ sectionPad
|
|
137
|
+
+ attrCount * sizing.er.rowHeight
|
|
138
|
+
+ dividers
|
|
139
|
+
+ sizing.er.borderExtra,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// ─── Class Diagram ──────────────────────────────────────────────────────────
|
|
143
|
+
function classNodeSize(node, sizing) {
|
|
144
|
+
const props = node.properties ?? [];
|
|
145
|
+
const methods = node.methods ?? [];
|
|
146
|
+
const hasBothSections = props.length > 0 && methods.length > 0;
|
|
147
|
+
const sectionCount = (props.length > 0 ? 1 : 0) + (methods.length > 0 ? 1 : 0);
|
|
148
|
+
const longestRowChars = Math.max(node.label.length, ...props.map((p) => p.length), ...methods.map((m) => m.length), 0);
|
|
149
|
+
const estimatedWidth = Math.max(sizing.class.width, 24 + longestRowChars * 7.2);
|
|
150
|
+
const classWidth = clampWidth(estimatedWidth);
|
|
151
|
+
// Estimate wrapped line count per row. The content area is the node width
|
|
152
|
+
// minus section padding (12px each side) and card border (1px each side).
|
|
153
|
+
const contentWidth = classWidth - 26;
|
|
154
|
+
const monoCharWidth = 6.6; // approximate width of 11px monospace chars
|
|
155
|
+
const charsPerLine = Math.max(8, Math.floor(contentWidth / monoCharWidth));
|
|
156
|
+
// Height breakdown (calibrated to match actual component rendering):
|
|
157
|
+
// header: padding 8+8 + font 14px + line-height ≈ 33px
|
|
158
|
+
// per section: wrapper padding 8 top + 8 bottom = 16px
|
|
159
|
+
// per row: row padding 4 top + 4 bottom = 8px
|
|
160
|
+
// per text line: 11px font * 1.4 lineHeight ≈ 16px
|
|
161
|
+
// divider: 1px border between sections
|
|
162
|
+
// card border: 2px (1px top + 1px bottom)
|
|
163
|
+
const HEADER = sizing.class.headerHeight;
|
|
164
|
+
const SECTION_PAD = 16;
|
|
165
|
+
const ROW_PAD = 8;
|
|
166
|
+
const LINE_H = 16;
|
|
167
|
+
const BORDER = 2;
|
|
168
|
+
function sectionHeight(rows) {
|
|
169
|
+
if (rows.length === 0)
|
|
170
|
+
return 0;
|
|
171
|
+
let totalLines = 0;
|
|
172
|
+
for (const row of rows) {
|
|
173
|
+
totalLines += Math.max(1, Math.ceil(row.length / charsPerLine));
|
|
174
|
+
}
|
|
175
|
+
return SECTION_PAD + rows.length * ROW_PAD + totalLines * LINE_H;
|
|
176
|
+
}
|
|
177
|
+
const height = HEADER
|
|
178
|
+
+ sectionHeight(props)
|
|
179
|
+
+ sectionHeight(methods)
|
|
180
|
+
+ (hasBothSections ? 1 : 0) // divider border
|
|
181
|
+
+ BORDER
|
|
182
|
+
+ (sectionCount === 0 ? 30 : 0); // "(empty)" placeholder
|
|
183
|
+
return { width: classWidth, height };
|
|
184
|
+
}
|
|
185
|
+
// ─── Dispatcher ─────────────────────────────────────────────────────────────
|
|
186
|
+
// Sequence diagrams bypass dagre entirely — they never call nodeSize().
|
|
187
|
+
// This stub exists only to satisfy the DiagramType→function mapping.
|
|
188
|
+
function sequenceNodeSize() {
|
|
189
|
+
return { width: 150, height: 40 };
|
|
190
|
+
}
|
|
191
|
+
const sizingByType = {
|
|
192
|
+
flowchart: flowchartNodeSize,
|
|
193
|
+
state: stateNodeSize,
|
|
194
|
+
er: erNodeSize,
|
|
195
|
+
class: classNodeSize,
|
|
196
|
+
sequence: sequenceNodeSize,
|
|
197
|
+
mindmap: flowchartNodeSize,
|
|
198
|
+
gitGraph: flowchartNodeSize,
|
|
199
|
+
};
|
|
200
|
+
export function nodeSize(node, diagramType, sizing) {
|
|
201
|
+
return sizingByType[diagramType](node, sizing);
|
|
202
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildEdges.ts — Main edge builder.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates: intersection → path generation → handle assignment → styling
|
|
5
|
+
* to produce React Flow Edge objects from parsed edges + dagre waypoints.
|
|
6
|
+
*
|
|
7
|
+
* For each edge:
|
|
8
|
+
* 1. Takes dagre's waypoints (which route around intermediate nodes)
|
|
9
|
+
* 2. Computes node-boundary intersection (rect or diamond) for start/end
|
|
10
|
+
* 3. Shortens the end by the arrow marker size so the tip lands at the boundary
|
|
11
|
+
* 4. Generates a rounded SVG path through all waypoints
|
|
12
|
+
* 5. Passes the pre-computed path + label position as edge data
|
|
13
|
+
*/
|
|
14
|
+
import type { Edge } from '@xyflow/react';
|
|
15
|
+
import type { ParsedEdge, ParsedGraph } from '../../types.js';
|
|
16
|
+
import type { Theme } from '../../ThemeContext.js';
|
|
17
|
+
import type { NodePosition, EdgeWaypoints } from '../types.js';
|
|
18
|
+
export declare function buildEdges(parsedEdges: ParsedEdge[], positions: Map<string, NodePosition>, _handles: unknown, direction: ParsedGraph['direction'], theme: Theme, diamondEdgeHandles?: Map<number, string>, edgeWaypoints?: EdgeWaypoints[], nodeShapes?: Map<string, string>): Edge[];
|