@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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/ThemeContext.d.ts +47 -0
  4. package/dist/ThemeContext.js +81 -0
  5. package/dist/components/DiagramCanvas.d.ts +8 -0
  6. package/dist/components/DiagramCanvas.js +19 -0
  7. package/dist/components/GraphCanvas.d.ts +9 -0
  8. package/dist/components/GraphCanvas.js +104 -0
  9. package/dist/components/NodeDetail.d.ts +9 -0
  10. package/dist/components/NodeDetail.js +127 -0
  11. package/dist/components/edges/RoutedEdge.d.ts +11 -0
  12. package/dist/components/edges/RoutedEdge.js +199 -0
  13. package/dist/components/nodes/ClassNode.d.ts +5 -0
  14. package/dist/components/nodes/ClassNode.js +62 -0
  15. package/dist/components/nodes/EntityNode.d.ts +5 -0
  16. package/dist/components/nodes/EntityNode.js +57 -0
  17. package/dist/components/nodes/FlowNode.d.ts +5 -0
  18. package/dist/components/nodes/FlowNode.js +144 -0
  19. package/dist/components/nodes/SequenceNodes.d.ts +33 -0
  20. package/dist/components/nodes/SequenceNodes.js +205 -0
  21. package/dist/components/nodes/StateNode.d.ts +5 -0
  22. package/dist/components/nodes/StateNode.js +71 -0
  23. package/dist/components/nodes/SubgraphNode.d.ts +5 -0
  24. package/dist/components/nodes/SubgraphNode.js +16 -0
  25. package/dist/config.d.ts +138 -0
  26. package/dist/config.js +165 -0
  27. package/dist/core.d.ts +12 -0
  28. package/dist/core.js +7 -0
  29. package/dist/diagrams/detect.d.ts +2 -0
  30. package/dist/diagrams/detect.js +8 -0
  31. package/dist/diagrams/plugins.d.ts +2 -0
  32. package/dist/diagrams/plugins.js +45 -0
  33. package/dist/diagrams/registry.d.ts +3 -0
  34. package/dist/diagrams/registry.js +13 -0
  35. package/dist/diagrams/types.d.ts +7 -0
  36. package/dist/diagrams/types.js +1 -0
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +3 -0
  39. package/dist/layout/dagre/index.d.ts +31 -0
  40. package/dist/layout/dagre/index.js +224 -0
  41. package/dist/layout/dagre/nodeSizing.d.ts +32 -0
  42. package/dist/layout/dagre/nodeSizing.js +202 -0
  43. package/dist/layout/edges/buildEdges.d.ts +18 -0
  44. package/dist/layout/edges/buildEdges.js +405 -0
  45. package/dist/layout/edges/classify.d.ts +13 -0
  46. package/dist/layout/edges/classify.js +36 -0
  47. package/dist/layout/edges/diamondHandles.d.ts +23 -0
  48. package/dist/layout/edges/diamondHandles.js +108 -0
  49. package/dist/layout/edges/index.d.ts +10 -0
  50. package/dist/layout/edges/index.js +8 -0
  51. package/dist/layout/edges/paths.d.ts +57 -0
  52. package/dist/layout/edges/paths.js +279 -0
  53. package/dist/layout/index.d.ts +59 -0
  54. package/dist/layout/index.js +131 -0
  55. package/dist/layout/intersect/circle.d.ts +2 -0
  56. package/dist/layout/intersect/circle.js +14 -0
  57. package/dist/layout/intersect/diamond.d.ts +9 -0
  58. package/dist/layout/intersect/diamond.js +21 -0
  59. package/dist/layout/intersect/index.d.ts +17 -0
  60. package/dist/layout/intersect/index.js +28 -0
  61. package/dist/layout/intersect/rect.d.ts +10 -0
  62. package/dist/layout/intersect/rect.js +31 -0
  63. package/dist/layout/intersect/rectRounded.d.ts +20 -0
  64. package/dist/layout/intersect/rectRounded.js +48 -0
  65. package/dist/layout/mindmapLayout.d.ts +13 -0
  66. package/dist/layout/mindmapLayout.js +299 -0
  67. package/dist/layout/sequenceLayout.d.ts +24 -0
  68. package/dist/layout/sequenceLayout.js +414 -0
  69. package/dist/layout/subgraph.d.ts +26 -0
  70. package/dist/layout/subgraph.js +63 -0
  71. package/dist/layout/types.d.ts +34 -0
  72. package/dist/layout/types.js +8 -0
  73. package/dist/parsers/classDiagram.d.ts +2 -0
  74. package/dist/parsers/classDiagram.js +105 -0
  75. package/dist/parsers/er.d.ts +2 -0
  76. package/dist/parsers/er.js +97 -0
  77. package/dist/parsers/flowchart.d.ts +2 -0
  78. package/dist/parsers/flowchart.js +191 -0
  79. package/dist/parsers/helpers.d.ts +4 -0
  80. package/dist/parsers/helpers.js +8 -0
  81. package/dist/parsers/index.d.ts +7 -0
  82. package/dist/parsers/index.js +19 -0
  83. package/dist/parsers/mindmap.d.ts +2 -0
  84. package/dist/parsers/mindmap.js +124 -0
  85. package/dist/parsers/sequence.d.ts +18 -0
  86. package/dist/parsers/sequence.js +196 -0
  87. package/dist/parsers/state.d.ts +2 -0
  88. package/dist/parsers/state.js +68 -0
  89. package/dist/react.d.ts +7 -0
  90. package/dist/react.js +9 -0
  91. package/dist/reactDefaults.d.ts +5 -0
  92. package/dist/reactDefaults.js +37 -0
  93. package/dist/renderMarkdown.d.ts +9 -0
  94. package/dist/renderMarkdown.js +103 -0
  95. package/dist/swagger.d.ts +113 -0
  96. package/dist/swagger.js +551 -0
  97. package/dist/theme/dark.d.ts +8 -0
  98. package/dist/theme/dark.js +190 -0
  99. package/dist/theme/index.d.ts +18 -0
  100. package/dist/theme/index.js +29 -0
  101. package/dist/theme/light.d.ts +8 -0
  102. package/dist/theme/light.js +190 -0
  103. package/dist/theme/types.d.ts +97 -0
  104. package/dist/theme/types.js +7 -0
  105. package/dist/types.d.ts +235 -0
  106. package/dist/types.js +1 -0
  107. package/package.json +74 -0
@@ -0,0 +1,279 @@
1
+ /**
2
+ * paths.ts — Pure path math utilities.
3
+ *
4
+ * Functions for path generation, shortening, deviation detection,
5
+ * and label placement. No side effects, no DOM, no React — just geometry.
6
+ */
7
+ // ─── Waypoint Deviation Check ────────────────────────────────────────────────
8
+ /**
9
+ * Checks whether any waypoint deviates from the straight line between the
10
+ * first and last point by more than `threshold` pixels.
11
+ *
12
+ * Used to distinguish simple straight edges (where dagre's waypoints are
13
+ * essentially collinear) from routed edges (where dagre bent the path
14
+ * around obstacles). Back-edges and lateral edges skip this check entirely.
15
+ */
16
+ export function waypointsDeviate(points, threshold) {
17
+ if (points.length <= 2)
18
+ return false;
19
+ const first = points[0];
20
+ const last = points[points.length - 1];
21
+ const dx = last.x - first.x;
22
+ const dy = last.y - first.y;
23
+ const len = Math.hypot(dx, dy);
24
+ if (len < 1e-6)
25
+ return false; // coincident endpoints
26
+ // Check perpendicular distance of each inner point from the first→last line
27
+ for (let i = 1; i < points.length - 1; i++) {
28
+ const px = points[i].x - first.x;
29
+ const py = points[i].y - first.y;
30
+ const dist = Math.abs(px * dy - py * dx) / len;
31
+ if (dist > threshold)
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+ // ─── Path Shortening for Arrow Markers ──────────────────────────────────────
37
+ /**
38
+ * Shortens a path by moving the last point `distance` pixels back along
39
+ * the direction from the second-to-last point to the last point.
40
+ * This prevents the arrowhead marker from overshooting the node boundary.
41
+ */
42
+ export function shortenPathEnd(points, distance) {
43
+ if (points.length < 2 || distance <= 0)
44
+ return points;
45
+ const result = points.slice();
46
+ let remaining = distance;
47
+ while (remaining > 0 && result.length >= 2) {
48
+ const last = result[result.length - 1];
49
+ const prev = result[result.length - 2];
50
+ const dx = last.x - prev.x;
51
+ const dy = last.y - prev.y;
52
+ const len = Math.hypot(dx, dy);
53
+ if (len < 1e-6) {
54
+ result.pop();
55
+ continue;
56
+ }
57
+ if (len <= remaining) {
58
+ if (result.length <= 2) {
59
+ // Can't shorten further without collapsing the path.
60
+ result[result.length - 1] = { ...prev };
61
+ break;
62
+ }
63
+ result.pop();
64
+ remaining -= len;
65
+ continue;
66
+ }
67
+ result[result.length - 1] = {
68
+ x: last.x - (dx / len) * remaining,
69
+ y: last.y - (dy / len) * remaining,
70
+ };
71
+ remaining = 0;
72
+ }
73
+ return result.length >= 2 ? result : points;
74
+ }
75
+ /**
76
+ * Shortens a path by moving the first point `distance` pixels forward along
77
+ * the direction from the first point to the second point.
78
+ * Used for ER start markers so they don't overflow into the source node.
79
+ */
80
+ export function shortenPathStart(points, distance) {
81
+ if (points.length < 2 || distance <= 0)
82
+ return points;
83
+ const result = points.slice();
84
+ let remaining = distance;
85
+ while (remaining > 0 && result.length >= 2) {
86
+ const first = result[0];
87
+ const next = result[1];
88
+ const dx = next.x - first.x;
89
+ const dy = next.y - first.y;
90
+ const len = Math.hypot(dx, dy);
91
+ if (len < 1e-6) {
92
+ result.shift();
93
+ continue;
94
+ }
95
+ if (len <= remaining) {
96
+ if (result.length <= 2) {
97
+ // Can't shorten further without collapsing the path.
98
+ result[0] = { ...next };
99
+ break;
100
+ }
101
+ result.shift();
102
+ remaining -= len;
103
+ continue;
104
+ }
105
+ result[0] = {
106
+ x: first.x + (dx / len) * remaining,
107
+ y: first.y + (dy / len) * remaining,
108
+ };
109
+ remaining = 0;
110
+ }
111
+ return result.length >= 2 ? result : points;
112
+ }
113
+ // ─── Smooth Path Generation ─────────────────────────────────────────────────
114
+ /**
115
+ * Generates an SVG path using Catmull-Rom interpolation through all points.
116
+ *
117
+ * Unlike B-splines (which approximate), Catmull-Rom passes THROUGH every
118
+ * control point while producing smooth curves between them. This gives us
119
+ * the flowing look of Mermaid's curveBasis without the overshoot problem.
120
+ *
121
+ * For edges with only 2 points (straight lines), produces a simple L command.
122
+ * For edges with bends, produces smooth cubic bezier curves that naturally
123
+ * flow through dagre's computed waypoints.
124
+ *
125
+ * When `straightEnds` is true, the first and last segments use straight lines
126
+ * instead of curves. This ensures markers at path endpoints align perfectly
127
+ * with the node boundary (used for ER cardinality markers).
128
+ *
129
+ * Alpha = 0.5 (centripetal) prevents cusps and self-intersections.
130
+ */
131
+ export function generateSmoothPath(points, straightEnds = false) {
132
+ if (points.length < 2)
133
+ return '';
134
+ if (points.length === 2) {
135
+ return `M${points[0].x},${points[0].y}L${points[1].x},${points[1].y}`;
136
+ }
137
+ // If straightEnds and only 3 points, just use line segments
138
+ if (straightEnds && points.length === 3) {
139
+ return `M${points[0].x},${points[0].y}L${points[1].x},${points[1].y}L${points[2].x},${points[2].y}`;
140
+ }
141
+ const alpha = 0.5; // centripetal Catmull-Rom
142
+ let path = `M${points[0].x},${points[0].y}`;
143
+ for (let i = 0; i < points.length - 1; i++) {
144
+ const isFirstSegment = i === 0;
145
+ const isLastSegment = i === points.length - 2;
146
+ // Straight line for first/last segments when requested (ER markers)
147
+ if (straightEnds && (isFirstSegment || isLastSegment)) {
148
+ path += `L${points[i + 1].x},${points[i + 1].y}`;
149
+ continue;
150
+ }
151
+ const p0 = points[Math.max(0, i - 1)];
152
+ const p1 = points[i];
153
+ const p2 = points[i + 1];
154
+ const p3 = points[Math.min(points.length - 1, i + 2)];
155
+ // Knot parameterization
156
+ const d1 = Math.hypot(p1.x - p0.x, p1.y - p0.y);
157
+ const d2 = Math.hypot(p2.x - p1.x, p2.y - p1.y);
158
+ const d3 = Math.hypot(p3.x - p2.x, p3.y - p2.y);
159
+ const d1a = Math.pow(d1, alpha);
160
+ const d2a = Math.pow(d2, alpha);
161
+ const d3a = Math.pow(d3, alpha);
162
+ // Avoid division by zero for coincident points
163
+ const eps = 1e-6;
164
+ const d1a2 = Math.pow(d1, 2 * alpha) || eps;
165
+ const d2a2 = Math.pow(d2, 2 * alpha) || eps;
166
+ const d3a2 = Math.pow(d3, 2 * alpha) || eps;
167
+ // Control points
168
+ const b1x = (d1a2 * p2.x - d2a2 * p0.x + (2 * d1a2 + 3 * d1a * d2a + d2a2) * p1.x) /
169
+ (3 * d1a * (d1a + d2a));
170
+ const b1y = (d1a2 * p2.y - d2a2 * p0.y + (2 * d1a2 + 3 * d1a * d2a + d2a2) * p1.y) /
171
+ (3 * d1a * (d1a + d2a));
172
+ const b2x = (d3a2 * p1.x - d2a2 * p3.x + (2 * d3a2 + 3 * d3a * d2a + d2a2) * p2.x) /
173
+ (3 * d3a * (d3a + d2a));
174
+ const b2y = (d3a2 * p1.y - d2a2 * p3.y + (2 * d3a2 + 3 * d3a * d2a + d2a2) * p2.y) /
175
+ (3 * d3a * (d3a + d2a));
176
+ // Handle edge cases where control points are NaN (coincident points)
177
+ const cp1x = isFinite(b1x) ? b1x : p1.x;
178
+ const cp1y = isFinite(b1y) ? b1y : p1.y;
179
+ const cp2x = isFinite(b2x) ? b2x : p2.x;
180
+ const cp2y = isFinite(b2y) ? b2y : p2.y;
181
+ path += `C${cp1x},${cp1y},${cp2x},${cp2y},${p2.x},${p2.y}`;
182
+ }
183
+ return path;
184
+ }
185
+ /**
186
+ * Basis spline path (d3 curveBasis style): smoother than Catmull-Rom and
187
+ * naturally reduces sharp elbows in orthogonal-ish routes.
188
+ */
189
+ export function generateBasisPath(points) {
190
+ if (points.length < 2)
191
+ return '';
192
+ if (points.length === 2) {
193
+ return `M${points[0].x},${points[0].y}L${points[1].x},${points[1].y}`;
194
+ }
195
+ // Repeat endpoints to mirror open basis behavior.
196
+ const p = [points[0], points[0], ...points, points[points.length - 1], points[points.length - 1]];
197
+ let path = '';
198
+ let started = false;
199
+ for (let i = 0; i <= p.length - 4; i++) {
200
+ const p0 = p[i];
201
+ const p1 = p[i + 1];
202
+ const p2 = p[i + 2];
203
+ const p3 = p[i + 3];
204
+ // Cubic Bézier equivalent of uniform cubic B-spline segment.
205
+ const b0 = { x: (p0.x + 4 * p1.x + p2.x) / 6, y: (p0.y + 4 * p1.y + p2.y) / 6 };
206
+ const b1 = { x: (4 * p1.x + 2 * p2.x) / 6, y: (4 * p1.y + 2 * p2.y) / 6 };
207
+ const b2 = { x: (2 * p1.x + 4 * p2.x) / 6, y: (2 * p1.y + 4 * p2.y) / 6 };
208
+ const b3 = { x: (p1.x + 4 * p2.x + p3.x) / 6, y: (p1.y + 4 * p2.y + p3.y) / 6 };
209
+ if (!started) {
210
+ path = `M${b0.x},${b0.y}`;
211
+ started = true;
212
+ }
213
+ path += `C${b1.x},${b1.y},${b2.x},${b2.y},${b3.x},${b3.y}`;
214
+ }
215
+ return path;
216
+ }
217
+ // ─── Rounded Polyline Path (for orthogonal-ish ER routes) ──────────────────
218
+ /**
219
+ * Generates a polyline path with rounded corners using quadratic curves.
220
+ * End segments stay straight; only interior corners are rounded.
221
+ */
222
+ export function generateRoundedPath(points, radius = 12) {
223
+ if (points.length < 2)
224
+ return '';
225
+ if (points.length === 2) {
226
+ return `M${points[0].x},${points[0].y}L${points[1].x},${points[1].y}`;
227
+ }
228
+ const safeRadius = Math.max(0, radius);
229
+ let path = `M${points[0].x},${points[0].y}`;
230
+ for (let i = 1; i < points.length - 1; i++) {
231
+ const prev = points[i - 1];
232
+ const curr = points[i];
233
+ const next = points[i + 1];
234
+ const inDx = curr.x - prev.x;
235
+ const inDy = curr.y - prev.y;
236
+ const outDx = next.x - curr.x;
237
+ const outDy = next.y - curr.y;
238
+ const inLen = Math.hypot(inDx, inDy);
239
+ const outLen = Math.hypot(outDx, outDy);
240
+ if (inLen < 1e-6 || outLen < 1e-6) {
241
+ path += `L${curr.x},${curr.y}`;
242
+ continue;
243
+ }
244
+ const uxIn = inDx / inLen;
245
+ const uyIn = inDy / inLen;
246
+ const uxOut = outDx / outLen;
247
+ const uyOut = outDy / outLen;
248
+ // Collinear segments: keep as a straight join.
249
+ const cross = uxIn * uyOut - uyIn * uxOut;
250
+ if (Math.abs(cross) < 1e-3 || safeRadius === 0) {
251
+ path += `L${curr.x},${curr.y}`;
252
+ continue;
253
+ }
254
+ const corner = Math.min(safeRadius, inLen / 2, outLen / 2);
255
+ const inX = curr.x - uxIn * corner;
256
+ const inY = curr.y - uyIn * corner;
257
+ const outX = curr.x + uxOut * corner;
258
+ const outY = curr.y + uyOut * corner;
259
+ path += `L${inX},${inY}Q${curr.x},${curr.y},${outX},${outY}`;
260
+ }
261
+ const last = points[points.length - 1];
262
+ path += `L${last.x},${last.y}`;
263
+ return path;
264
+ }
265
+ // ─── Label Placement ────────────────────────────────────────────────────────
266
+ export function computeLabelPosition(points) {
267
+ if (points.length === 0)
268
+ return { x: 0, y: 0 };
269
+ if (points.length === 1)
270
+ return points[0];
271
+ const mid = (points.length - 1) / 2;
272
+ const lo = Math.floor(mid);
273
+ const hi = Math.ceil(mid);
274
+ const t = mid - lo;
275
+ return {
276
+ x: points[lo].x + (points[hi].x - points[lo].x) * t,
277
+ y: points[lo].y + (points[hi].y - points[lo].y) * t,
278
+ };
279
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * layout/index.ts — Entry point for the layout engine.
3
+ *
4
+ * Two-phase architecture:
5
+ * Phase 1 (layoutNodes): Run dagre for node positions. Return nodes + a
6
+ * LayoutContext that holds everything needed to build edges later.
7
+ * Phase 2 (buildLayoutEdges): Build edges using actual measured dimensions
8
+ * from React Flow, so edge paths match the real node boundaries.
9
+ *
10
+ * GraphCanvas calls Phase 1 first, renders nodes, waits for React Flow to
11
+ * measure them, then calls Phase 2 with actual dimensions. This eliminates
12
+ * height estimation drift for card-based nodes (ER, Class).
13
+ *
14
+ * `layoutParsedGraph()` is a backward-compatible wrapper that does both
15
+ * phases in one call (using dagre-estimated dimensions for edges).
16
+ */
17
+ import type { Node, Edge } from '@xyflow/react';
18
+ import type { ParsedDiagram, ParsedGraph, ParsedEdge, ParsedCustomDiagram, NodeMetadata } from '../types.js';
19
+ import type { Theme } from '../ThemeContext.js';
20
+ import type { NodePosition, HandlePositions, EdgeWaypoints } from './types.js';
21
+ export interface LayoutOptions {
22
+ theme: Theme;
23
+ metadata?: Record<string, NodeMetadata>;
24
+ }
25
+ export interface LayoutContext {
26
+ parsedEdges: ParsedEdge[];
27
+ estimatedPositions: Map<string, NodePosition>;
28
+ handles: HandlePositions;
29
+ direction: ParsedGraph['direction'];
30
+ diamondEdgeHandles: Map<number, string>;
31
+ edgeWaypoints: EdgeWaypoints[];
32
+ nodeShapes: Map<string, string>;
33
+ }
34
+ export interface GraphLayoutResult {
35
+ kind: 'graph';
36
+ nodes: Node[];
37
+ edges: Edge[];
38
+ context?: LayoutContext;
39
+ }
40
+ export interface CustomLayoutResult {
41
+ kind: 'custom';
42
+ diagramType: ParsedCustomDiagram['diagramType'];
43
+ data: ParsedCustomDiagram['data'];
44
+ }
45
+ export type LayoutResult = GraphLayoutResult | CustomLayoutResult;
46
+ export declare function layoutNodes(parsed: ParsedGraph, options: LayoutOptions): {
47
+ nodes: Node[];
48
+ context: LayoutContext;
49
+ } | {
50
+ nodes: Node[];
51
+ edges: Edge[];
52
+ context?: undefined;
53
+ };
54
+ export declare function layoutDiagram(parsed: ParsedDiagram, options: LayoutOptions): LayoutResult;
55
+ export declare function buildLayoutEdges(context: LayoutContext, theme: Theme, measuredPositions?: Map<string, NodePosition>): Edge[];
56
+ export declare function layoutParsedGraph(parsed: ParsedGraph, options: LayoutOptions): {
57
+ nodes: Node[];
58
+ edges: Edge[];
59
+ };
@@ -0,0 +1,131 @@
1
+ /**
2
+ * layout/index.ts — Entry point for the layout engine.
3
+ *
4
+ * Two-phase architecture:
5
+ * Phase 1 (layoutNodes): Run dagre for node positions. Return nodes + a
6
+ * LayoutContext that holds everything needed to build edges later.
7
+ * Phase 2 (buildLayoutEdges): Build edges using actual measured dimensions
8
+ * from React Flow, so edge paths match the real node boundaries.
9
+ *
10
+ * GraphCanvas calls Phase 1 first, renders nodes, waits for React Flow to
11
+ * measure them, then calls Phase 2 with actual dimensions. This eliminates
12
+ * height estimation drift for card-based nodes (ER, Class).
13
+ *
14
+ * `layoutParsedGraph()` is a backward-compatible wrapper that does both
15
+ * phases in one call (using dagre-estimated dimensions for edges).
16
+ */
17
+ import { getConfig, getLayoutMapping } from '../config.js';
18
+ import { createDagreGraph, extractPositions, extractEdgeWaypoints, directionToHandles } from './dagre/index.js';
19
+ import { buildEdges, computeForwardEdgeCounts, computeDiamondAssignments } from './edges/index.js';
20
+ import { computeSubgraphBounds, buildSubgraphNode } from './subgraph.js';
21
+ import { layoutSequenceDiagram } from './sequenceLayout.js';
22
+ import { layoutMindmapGraph } from './mindmapLayout.js';
23
+ // ─── Phase 1: Layout Nodes ──────────────────────────────────────────────────
24
+ export function layoutNodes(parsed, options) {
25
+ const { theme, metadata } = options;
26
+ // Mindmap uses dedicated radial layout (not dagre).
27
+ if (parsed.diagramType === 'mindmap') {
28
+ const mm = layoutMindmapGraph(parsed, theme, metadata);
29
+ const mindmapEdges = parsed.edges.map((e) => ({ ...e, noArrow: true }));
30
+ return {
31
+ nodes: mm.nodes,
32
+ context: {
33
+ parsedEdges: mindmapEdges,
34
+ estimatedPositions: mm.positions,
35
+ handles: mm.handles,
36
+ direction: 'LR',
37
+ diamondEdgeHandles: mm.diamondEdgeHandles,
38
+ edgeWaypoints: mm.edgeWaypoints,
39
+ nodeShapes: new Map(mm.nodes.map((n) => [n.id, String(n.data?.shape ?? 'rect')])),
40
+ },
41
+ };
42
+ }
43
+ // Sequence diagrams use their own grid-based layout (no dagre, no two-phase)
44
+ if (parsed.diagramType === 'sequence' && parsed.sequence) {
45
+ return layoutSequenceDiagram(parsed.sequence, theme, metadata);
46
+ }
47
+ const { layout } = getConfig();
48
+ // 1. Build dagre graph and run layout
49
+ const graph = createDagreGraph(parsed, layout);
50
+ // 2. Extract positioned coordinates and edge waypoints
51
+ const positions = extractPositions(graph, parsed.nodes);
52
+ const edgeWaypoints = extractEdgeWaypoints(graph, parsed);
53
+ // 3. Compute forward edge counts per node (for handle spreading)
54
+ const fwCounts = computeForwardEdgeCounts(parsed.edges, positions, parsed.direction);
55
+ // 3.5 Compute diamond handle assignments (position-aware)
56
+ const diamondAssignments = computeDiamondAssignments(parsed.nodes, parsed.edges, positions, parsed.direction);
57
+ // 4. Build React Flow nodes
58
+ const rfNodeType = getLayoutMapping(parsed.diagramType);
59
+ const handles = directionToHandles(parsed.direction);
60
+ const nodes = parsed.nodes.map((node) => {
61
+ const pos = positions.get(node.id);
62
+ return {
63
+ id: node.id,
64
+ position: { x: pos.x, y: pos.y },
65
+ data: {
66
+ label: node.label,
67
+ shape: node.shape,
68
+ attributes: node.attributes,
69
+ properties: node.properties,
70
+ methods: node.methods,
71
+ meta: metadata?.[node.id],
72
+ handles,
73
+ fwIn: fwCounts.incoming.get(node.id) ?? 0,
74
+ fwOut: fwCounts.outgoing.get(node.id) ?? 0,
75
+ diamondHandleConfig: diamondAssignments.handleConfigs.get(node.id),
76
+ layoutWidth: pos.width,
77
+ layoutHeight: pos.height,
78
+ },
79
+ type: rfNodeType,
80
+ };
81
+ });
82
+ // 5. Build subgraph container nodes (inserted at front so they render behind)
83
+ if (parsed.subgraphs && parsed.subgraphs.length > 0) {
84
+ for (const subgraph of parsed.subgraphs) {
85
+ const bounds = computeSubgraphBounds(subgraph, positions, layout.subgraphPadding, theme.nodeStyles.subgraph.labelOffset, graph);
86
+ if (!bounds)
87
+ continue;
88
+ nodes.unshift(buildSubgraphNode(subgraph, bounds, theme));
89
+ }
90
+ }
91
+ const nodeShapes = new Map(parsed.nodes.map(n => [n.id, n.shape]));
92
+ return {
93
+ nodes,
94
+ context: {
95
+ parsedEdges: parsed.edges,
96
+ estimatedPositions: positions,
97
+ handles,
98
+ direction: parsed.direction,
99
+ diamondEdgeHandles: diamondAssignments.edgeHandles,
100
+ edgeWaypoints,
101
+ nodeShapes,
102
+ },
103
+ };
104
+ }
105
+ export function layoutDiagram(parsed, options) {
106
+ if (parsed.kind === 'custom') {
107
+ return {
108
+ kind: 'custom',
109
+ diagramType: parsed.diagramType,
110
+ data: parsed.data,
111
+ };
112
+ }
113
+ const graphResult = layoutParsedGraph(parsed, options);
114
+ return { kind: 'graph', nodes: graphResult.nodes, edges: graphResult.edges };
115
+ }
116
+ // ─── Phase 2: Build Edges with Actual Dimensions ────────────────────────────
117
+ export function buildLayoutEdges(context, theme, measuredPositions) {
118
+ // Use measured positions if available, fall back to dagre estimates
119
+ const positions = measuredPositions ?? context.estimatedPositions;
120
+ return buildEdges(context.parsedEdges, positions, context.handles, context.direction, theme, context.diamondEdgeHandles, context.edgeWaypoints, context.nodeShapes);
121
+ }
122
+ // ─── Backward-Compatible Wrapper ────────────────────────────────────────────
123
+ export function layoutParsedGraph(parsed, options) {
124
+ const result = layoutNodes(parsed, options);
125
+ // Sequence diagrams return edges directly (no two-phase)
126
+ if (!result.context) {
127
+ return result;
128
+ }
129
+ const edges = buildLayoutEdges(result.context, options.theme);
130
+ return { nodes: result.nodes, edges };
131
+ }
@@ -0,0 +1,2 @@
1
+ import type { NodeBounds, Point } from '../types.js';
2
+ export declare function intersectCircle(node: NodeBounds, point: Point): Point;
@@ -0,0 +1,14 @@
1
+ export function intersectCircle(node, point) {
2
+ const cx = node.x;
3
+ const cy = node.y;
4
+ const dx = point.x - cx;
5
+ const dy = point.y - cy;
6
+ const r = Math.min(node.width, node.height) / 2;
7
+ const len = Math.hypot(dx, dy);
8
+ if (len < 1e-6)
9
+ return { x: cx, y: cy };
10
+ return {
11
+ x: cx + (dx / len) * r,
12
+ y: cy + (dy / len) * r,
13
+ };
14
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * diamond.ts — Diamond (rhombus) boundary intersection.
3
+ *
4
+ * A diamond with center (cx, cy) and half-widths (w, h) has its vertices at
5
+ * (cx, cy-h), (cx+w, cy), (cx, cy+h), (cx-w, cy). The boundary follows
6
+ * |dx/w| + |dy/h| = 1.
7
+ */
8
+ import type { NodeBounds, Point } from '../types.js';
9
+ export declare function intersectDiamond(node: NodeBounds, point: Point): Point;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * diamond.ts — Diamond (rhombus) boundary intersection.
3
+ *
4
+ * A diamond with center (cx, cy) and half-widths (w, h) has its vertices at
5
+ * (cx, cy-h), (cx+w, cy), (cx, cy+h), (cx-w, cy). The boundary follows
6
+ * |dx/w| + |dy/h| = 1.
7
+ */
8
+ export function intersectDiamond(node, point) {
9
+ const cx = node.x;
10
+ const cy = node.y;
11
+ const dx = point.x - cx;
12
+ const dy = point.y - cy;
13
+ const w = node.width / 2;
14
+ const h = node.height / 2;
15
+ if (dx === 0 && dy === 0)
16
+ return { x: cx, y: cy - h };
17
+ // Scale factor: the diamond boundary is |dx/w| + |dy/h| = 1
18
+ // A point (t*dx, t*dy) is on the boundary when t * (|dx|/w + |dy|/h) = 1
19
+ const t = 1 / (Math.abs(dx) / w + Math.abs(dy) / h);
20
+ return { x: cx + t * dx, y: cy + t * dy };
21
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * intersect/ — Node-boundary intersection math.
3
+ *
4
+ * One file per geometric shape, following Mermaid's intersect/ pattern.
5
+ * The dispatcher `intersectNode` selects the correct function based on shape.
6
+ */
7
+ import type { NodeBounds, Point } from '../types.js';
8
+ export { intersectRect } from './rect.js';
9
+ export { intersectRectRounded } from './rectRounded.js';
10
+ export { intersectDiamond } from './diamond.js';
11
+ export { intersectCircle } from './circle.js';
12
+ /**
13
+ * Dispatches to the correct intersection function based on node shape.
14
+ * When `borderRadius` is provided (> 0), uses the rounded-corner
15
+ * compensation to push intersection points flush with the visual boundary.
16
+ */
17
+ export declare function intersectNode(node: NodeBounds, point: Point, shape: string, borderRadius?: number): Point;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * intersect/ — Node-boundary intersection math.
3
+ *
4
+ * One file per geometric shape, following Mermaid's intersect/ pattern.
5
+ * The dispatcher `intersectNode` selects the correct function based on shape.
6
+ */
7
+ import { intersectRect } from './rect.js';
8
+ import { intersectRectRounded } from './rectRounded.js';
9
+ import { intersectDiamond } from './diamond.js';
10
+ import { intersectCircle } from './circle.js';
11
+ export { intersectRect } from './rect.js';
12
+ export { intersectRectRounded } from './rectRounded.js';
13
+ export { intersectDiamond } from './diamond.js';
14
+ export { intersectCircle } from './circle.js';
15
+ /**
16
+ * Dispatches to the correct intersection function based on node shape.
17
+ * When `borderRadius` is provided (> 0), uses the rounded-corner
18
+ * compensation to push intersection points flush with the visual boundary.
19
+ */
20
+ export function intersectNode(node, point, shape, borderRadius = 0) {
21
+ if (shape === 'diamond')
22
+ return intersectDiamond(node, point);
23
+ if (shape === 'circle' || shape === 'doublecircle')
24
+ return intersectCircle(node, point);
25
+ if (borderRadius > 0)
26
+ return intersectRectRounded(node, point, borderRadius);
27
+ return intersectRect(node, point);
28
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * rect.ts — Rectangle boundary intersection.
3
+ *
4
+ * Computes where a line from the node center toward a point
5
+ * intersects the rectangular boundary of the node.
6
+ *
7
+ * Adapted from Mermaid's intersect-rect.js.
8
+ */
9
+ import type { NodeBounds, Point } from '../types.js';
10
+ export declare function intersectRect(node: NodeBounds, point: Point): Point;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * rect.ts — Rectangle boundary intersection.
3
+ *
4
+ * Computes where a line from the node center toward a point
5
+ * intersects the rectangular boundary of the node.
6
+ *
7
+ * Adapted from Mermaid's intersect-rect.js.
8
+ */
9
+ export function intersectRect(node, point) {
10
+ const cx = node.x;
11
+ const cy = node.y;
12
+ const dx = point.x - cx;
13
+ const dy = point.y - cy;
14
+ const w = node.width / 2;
15
+ const h = node.height / 2;
16
+ if (dx === 0 && dy === 0)
17
+ return { x: cx, y: cy };
18
+ let sx;
19
+ let sy;
20
+ if (Math.abs(dy) * w > Math.abs(dx) * h) {
21
+ const hSign = dy < 0 ? -h : h;
22
+ sx = dy === 0 ? 0 : (hSign * dx) / dy;
23
+ sy = hSign;
24
+ }
25
+ else {
26
+ const wSign = dx < 0 ? -w : w;
27
+ sx = wSign;
28
+ sy = dx === 0 ? 0 : (wSign * dy) / dx;
29
+ }
30
+ return { x: cx + sx, y: cy + sy };
31
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * rectRounded.ts — Rectangle intersection with border-radius compensation.
3
+ *
4
+ * The standard intersectRect returns the point where a line from center
5
+ * hits the sharp rectangle boundary. But when the rendered node has
6
+ * rounded corners (border-radius), the actual visual boundary at diagonal
7
+ * angles is INSIDE that sharp corner — creating a gap.
8
+ *
9
+ * This function pushes the intersection point outward along the line
10
+ * from center → point, proportional to how close the intersection is
11
+ * to a corner. At a flat edge (top/bottom/side center), compensation
12
+ * is zero. At a 45° corner, it's maximal.
13
+ *
14
+ * The compensation formula: at corners, the rounded boundary is inset
15
+ * by r - r*cos(θ) where θ is the angle from the corner axis. We
16
+ * approximate this by measuring how "cornery" the intersection is
17
+ * (both x and y are close to their maximums) and pushing outward.
18
+ */
19
+ import type { NodeBounds, Point } from '../types.js';
20
+ export declare function intersectRectRounded(node: NodeBounds, point: Point, borderRadius: number): Point;