@graph-artifact/core 0.1.13 → 0.1.15
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/dist/components/GraphCanvas.d.ts +17 -4
- package/dist/components/GraphCanvas.js +136 -37
- package/dist/components/edges/RoutedEdge.d.ts +3 -4
- package/dist/components/edges/RoutedEdge.js +14 -7
- package/dist/components/exportPng.d.ts +46 -0
- package/dist/components/exportPng.js +156 -0
- package/dist/components/nodes/ClassNode.js +9 -2
- package/dist/components/nodes/EntityNode.js +41 -17
- package/dist/components/nodes/FlowNode.js +22 -7
- package/dist/components/nodes/StateNode.js +13 -17
- package/dist/components/nodes/SubgraphNode.js +2 -2
- package/dist/config.d.ts +1 -1
- package/dist/config.js +7 -7
- package/dist/layout/dagre/index.js +49 -0
- package/dist/layout/dagre/nodeSizing.js +8 -8
- package/dist/layout/edges/buildEdges.js +681 -25
- package/dist/layout/edges/paths.d.ts +4 -0
- package/dist/layout/edges/paths.js +137 -9
- package/dist/layout/index.js +19 -0
- package/dist/layout/sequenceLayout.js +5 -2
- package/dist/parsers/state.js +162 -23
- package/dist/theme/dark.js +11 -11
- package/dist/theme/light.js +10 -10
- package/dist/utils/boxShadow.d.ts +9 -0
- package/dist/utils/boxShadow.js +103 -0
- package/package.json +1 -1
|
@@ -47,6 +47,10 @@ export declare function generateSmoothPath(points: Point[], straightEnds?: boole
|
|
|
47
47
|
/**
|
|
48
48
|
* Basis spline path (d3 curveBasis style): smoother than Catmull-Rom and
|
|
49
49
|
* naturally reduces sharp elbows in orthogonal-ish routes.
|
|
50
|
+
*
|
|
51
|
+
* Starts and ends with straight segments to ensure the path connects exactly
|
|
52
|
+
* to the computed node boundary intersection points, then uses B-spline
|
|
53
|
+
* interpolation for smooth curves through the middle waypoints.
|
|
50
54
|
*/
|
|
51
55
|
export declare function generateBasisPath(points: Point[]): string;
|
|
52
56
|
/**
|
|
@@ -182,9 +182,67 @@ export function generateSmoothPath(points, straightEnds = false) {
|
|
|
182
182
|
}
|
|
183
183
|
return path;
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Attempt to add intermediate points along long segments to create more
|
|
187
|
+
* natural curves through the B-spline. This helps smooth out L-shaped
|
|
188
|
+
* or Z-shaped routes that would otherwise have sharp corners.
|
|
189
|
+
*/
|
|
190
|
+
function subdivideIfNeeded(points, maxSegmentLen = 80) {
|
|
191
|
+
if (points.length < 2)
|
|
192
|
+
return points;
|
|
193
|
+
const result = [points[0]];
|
|
194
|
+
for (let i = 1; i < points.length; i++) {
|
|
195
|
+
const prev = points[i - 1];
|
|
196
|
+
const curr = points[i];
|
|
197
|
+
const dx = curr.x - prev.x;
|
|
198
|
+
const dy = curr.y - prev.y;
|
|
199
|
+
const len = Math.hypot(dx, dy);
|
|
200
|
+
if (len > maxSegmentLen) {
|
|
201
|
+
// Add midpoint(s) to break up long segments
|
|
202
|
+
const subdivisions = Math.ceil(len / maxSegmentLen);
|
|
203
|
+
for (let j = 1; j < subdivisions; j++) {
|
|
204
|
+
const t = j / subdivisions;
|
|
205
|
+
result.push({
|
|
206
|
+
x: prev.x + dx * t,
|
|
207
|
+
y: prev.y + dy * t,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
result.push(curr);
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Check if all points are nearly collinear (forming a straight line).
|
|
217
|
+
* Used to avoid unnecessary B-spline curves on edges that should be straight.
|
|
218
|
+
*/
|
|
219
|
+
function isNearlyStraight(points, threshold = 8) {
|
|
220
|
+
if (points.length <= 2)
|
|
221
|
+
return true;
|
|
222
|
+
const first = points[0];
|
|
223
|
+
const last = points[points.length - 1];
|
|
224
|
+
const dx = last.x - first.x;
|
|
225
|
+
const dy = last.y - first.y;
|
|
226
|
+
const len = Math.hypot(dx, dy);
|
|
227
|
+
if (len < 1e-6)
|
|
228
|
+
return true; // coincident endpoints
|
|
229
|
+
// Check perpendicular distance of each inner point from the first→last line
|
|
230
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
231
|
+
const px = points[i].x - first.x;
|
|
232
|
+
const py = points[i].y - first.y;
|
|
233
|
+
const dist = Math.abs(px * dy - py * dx) / len;
|
|
234
|
+
if (dist > threshold)
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
185
239
|
/**
|
|
186
240
|
* Basis spline path (d3 curveBasis style): smoother than Catmull-Rom and
|
|
187
241
|
* naturally reduces sharp elbows in orthogonal-ish routes.
|
|
242
|
+
*
|
|
243
|
+
* Starts and ends with straight segments to ensure the path connects exactly
|
|
244
|
+
* to the computed node boundary intersection points, then uses B-spline
|
|
245
|
+
* interpolation for smooth curves through the middle waypoints.
|
|
188
246
|
*/
|
|
189
247
|
export function generateBasisPath(points) {
|
|
190
248
|
if (points.length < 2)
|
|
@@ -192,26 +250,96 @@ export function generateBasisPath(points) {
|
|
|
192
250
|
if (points.length === 2) {
|
|
193
251
|
return `M${points[0].x},${points[0].y}L${points[1].x},${points[1].y}`;
|
|
194
252
|
}
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
253
|
+
// If all points are nearly collinear, use a straight line instead of B-spline
|
|
254
|
+
// This avoids unnecessary curves on edges that should be straight (e.g., start→Dashboard)
|
|
255
|
+
if (isNearlyStraight(points)) {
|
|
256
|
+
const first = points[0];
|
|
257
|
+
const last = points[points.length - 1];
|
|
258
|
+
return `M${first.x},${first.y}L${last.x},${last.y}`;
|
|
259
|
+
}
|
|
260
|
+
// For 3-point L-shaped routes, create a smooth curve using cubic bezier
|
|
261
|
+
// that rounds the corner instead of making a sharp turn
|
|
262
|
+
if (points.length === 3) {
|
|
263
|
+
const [p0, corner, p2] = points;
|
|
264
|
+
// Calculate distances to determine curve tightness
|
|
265
|
+
const d1 = Math.hypot(corner.x - p0.x, corner.y - p0.y);
|
|
266
|
+
const d2 = Math.hypot(p2.x - corner.x, p2.y - corner.y);
|
|
267
|
+
const minDist = Math.min(d1, d2);
|
|
268
|
+
// Control point offset - how far along each segment to place control points
|
|
269
|
+
// Larger values = tighter corner, smaller = rounder corner
|
|
270
|
+
const tension = Math.min(0.4, minDist * 0.3 / Math.max(d1, d2, 1));
|
|
271
|
+
const offset1 = Math.max(20, d1 * tension);
|
|
272
|
+
const offset2 = Math.max(20, d2 * tension);
|
|
273
|
+
// Direction vectors
|
|
274
|
+
const dir1x = (corner.x - p0.x) / d1;
|
|
275
|
+
const dir1y = (corner.y - p0.y) / d1;
|
|
276
|
+
const dir2x = (p2.x - corner.x) / d2;
|
|
277
|
+
const dir2y = (p2.y - corner.y) / d2;
|
|
278
|
+
// Control points pulled back from corner along each segment
|
|
279
|
+
const cp1 = {
|
|
280
|
+
x: corner.x - dir1x * offset1,
|
|
281
|
+
y: corner.y - dir1y * offset1,
|
|
282
|
+
};
|
|
283
|
+
const cp2 = {
|
|
284
|
+
x: corner.x + dir2x * offset2,
|
|
285
|
+
y: corner.y + dir2y * offset2,
|
|
286
|
+
};
|
|
287
|
+
// Use cubic bezier: start -> line to approach -> curve through corner -> line to end
|
|
288
|
+
return `M${p0.x},${p0.y}L${cp1.x},${cp1.y}Q${corner.x},${corner.y},${cp2.x},${cp2.y}L${p2.x},${p2.y}`;
|
|
289
|
+
}
|
|
290
|
+
// For 4+ points, subdivide long segments then apply B-spline
|
|
291
|
+
const subdivided = subdivideIfNeeded(points, 60);
|
|
292
|
+
// Start path at exact first point
|
|
293
|
+
const first = subdivided[0];
|
|
294
|
+
const last = subdivided[subdivided.length - 1];
|
|
295
|
+
let path = `M${first.x},${first.y}`;
|
|
296
|
+
// For B-spline middle section, we need at least 4 points.
|
|
297
|
+
// Use inner points (excluding first and last) for the spline.
|
|
298
|
+
const inner = subdivided.slice(1, -1);
|
|
299
|
+
if (inner.length < 2) {
|
|
300
|
+
// Not enough inner points for B-spline, use the 3-point logic
|
|
301
|
+
if (subdivided.length === 3) {
|
|
302
|
+
const [p0, corner, p2] = subdivided;
|
|
303
|
+
const d1 = Math.hypot(corner.x - p0.x, corner.y - p0.y);
|
|
304
|
+
const d2 = Math.hypot(p2.x - corner.x, p2.y - corner.y);
|
|
305
|
+
const offset = Math.min(d1, d2) * 0.3;
|
|
306
|
+
const dir1x = d1 > 0 ? (corner.x - p0.x) / d1 : 0;
|
|
307
|
+
const dir1y = d1 > 0 ? (corner.y - p0.y) / d1 : 0;
|
|
308
|
+
const dir2x = d2 > 0 ? (p2.x - corner.x) / d2 : 0;
|
|
309
|
+
const dir2y = d2 > 0 ? (p2.y - corner.y) / d2 : 0;
|
|
310
|
+
const cp1 = { x: corner.x - dir1x * offset, y: corner.y - dir1y * offset };
|
|
311
|
+
const cp2 = { x: corner.x + dir2x * offset, y: corner.y + dir2y * offset };
|
|
312
|
+
return `M${p0.x},${p0.y}L${cp1.x},${cp1.y}Q${corner.x},${corner.y},${cp2.x},${cp2.y}L${p2.x},${p2.y}`;
|
|
313
|
+
}
|
|
314
|
+
// Fallback to straight lines
|
|
315
|
+
for (let i = 1; i < subdivided.length; i++) {
|
|
316
|
+
path += `L${subdivided[i].x},${subdivided[i].y}`;
|
|
317
|
+
}
|
|
318
|
+
return path;
|
|
319
|
+
}
|
|
320
|
+
// Draw a short straight segment toward the first inner point
|
|
321
|
+
// to ensure smooth transition from exact start point
|
|
322
|
+
const toFirst = inner[0];
|
|
323
|
+
const startBlend = {
|
|
324
|
+
x: first.x + (toFirst.x - first.x) * 0.25,
|
|
325
|
+
y: first.y + (toFirst.y - first.y) * 0.25,
|
|
326
|
+
};
|
|
327
|
+
path += `L${startBlend.x},${startBlend.y}`;
|
|
328
|
+
// B-spline through inner points with repeated endpoints for clamping
|
|
329
|
+
const p = [startBlend, startBlend, ...inner, inner[inner.length - 1], inner[inner.length - 1]];
|
|
199
330
|
for (let i = 0; i <= p.length - 4; i++) {
|
|
200
331
|
const p0 = p[i];
|
|
201
332
|
const p1 = p[i + 1];
|
|
202
333
|
const p2 = p[i + 2];
|
|
203
334
|
const p3 = p[i + 3];
|
|
204
335
|
// 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
336
|
const b1 = { x: (4 * p1.x + 2 * p2.x) / 6, y: (4 * p1.y + 2 * p2.y) / 6 };
|
|
207
337
|
const b2 = { x: (2 * p1.x + 4 * p2.x) / 6, y: (2 * p1.y + 4 * p2.y) / 6 };
|
|
208
338
|
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
339
|
path += `C${b1.x},${b1.y},${b2.x},${b2.y},${b3.x},${b3.y}`;
|
|
214
340
|
}
|
|
341
|
+
// End with straight segment to exact last point
|
|
342
|
+
path += `L${last.x},${last.y}`;
|
|
215
343
|
return path;
|
|
216
344
|
}
|
|
217
345
|
// ─── Rounded Polyline Path (for orthogonal-ish ER routes) ──────────────────
|
package/dist/layout/index.js
CHANGED
|
@@ -95,6 +95,25 @@ export function layoutNodes(parsed, options, nodeSizeOverrides) {
|
|
|
95
95
|
height: bounds.height,
|
|
96
96
|
});
|
|
97
97
|
nodes.unshift(buildSubgraphNode(subgraph, bounds, theme));
|
|
98
|
+
// Center start/end nodes within the subgraph (state diagrams)
|
|
99
|
+
if (parsed.diagramType === 'state') {
|
|
100
|
+
for (const nodeId of subgraph.nodeIds) {
|
|
101
|
+
// Check if this is a start or end node for this subgraph
|
|
102
|
+
if (nodeId.startsWith('[*]_start_') || nodeId.startsWith('[*]_end_')) {
|
|
103
|
+
const nodePos = positions.get(nodeId);
|
|
104
|
+
if (nodePos) {
|
|
105
|
+
// Center horizontally within subgraph
|
|
106
|
+
const centeredX = bounds.x + (bounds.width - nodePos.width) / 2;
|
|
107
|
+
positions.set(nodeId, { ...nodePos, x: centeredX });
|
|
108
|
+
// Update the corresponding React Flow node
|
|
109
|
+
const rfNode = nodes.find(n => n.id === nodeId);
|
|
110
|
+
if (rfNode) {
|
|
111
|
+
rfNode.position.x = centeredX;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
119
|
const nodeShapes = new Map(parsed.nodes.map(n => [n.id, n.shape]));
|
|
@@ -28,7 +28,8 @@ const LABEL_LINE_HEIGHT = 16;
|
|
|
28
28
|
const LABEL_GAP_BELOW = 6;
|
|
29
29
|
const TOP_PADDING = 20;
|
|
30
30
|
const LEFT_PADDING = 40;
|
|
31
|
-
const LIFELINE_START_OFFSET =
|
|
31
|
+
const LIFELINE_START_OFFSET = 0; // Lifelines connect directly to participant bottom
|
|
32
|
+
const FIRST_MESSAGE_PADDING = 50; // Space below participants before first message row
|
|
32
33
|
const BOTTOM_PARTICIPANT_GAP = 40;
|
|
33
34
|
const ANCHOR_WIDTH = 2;
|
|
34
35
|
const ANCHOR_HEIGHT = 2;
|
|
@@ -131,10 +132,12 @@ export function layoutSequenceDiagram(sequence, theme, metadata) {
|
|
|
131
132
|
// ── Compute message Y positions ──
|
|
132
133
|
// Each message row is tall enough for its label above the line.
|
|
133
134
|
// Notes consume their own vertical rows before the message they precede.
|
|
135
|
+
// Lifeline starts at participant bottom (visually connects)
|
|
134
136
|
const lifelineStartY = seq.topPadding + seq.participantHeight + seq.lifelineStartOffset;
|
|
135
137
|
const messageY = [];
|
|
136
138
|
const noteYPositions = new Map();
|
|
137
|
-
|
|
139
|
+
// Messages start with padding below participants (room for labels, numbers, arrows)
|
|
140
|
+
let currentY = lifelineStartY + FIRST_MESSAGE_PADDING;
|
|
138
141
|
const noteSpaceBefore = new Map();
|
|
139
142
|
if (sequence.notes) {
|
|
140
143
|
for (let ni = 0; ni < sequence.notes.length; ni++) {
|
package/dist/parsers/state.js
CHANGED
|
@@ -2,67 +2,206 @@ import { prep } from './helpers.js';
|
|
|
2
2
|
// Matches node IDs: regular words OR the special [*] start/end marker
|
|
3
3
|
const NODE_ID = /(\[\*\]|[\w]+)/;
|
|
4
4
|
const EDGE_RE = new RegExp(`^${NODE_ID.source}\\s*-->\\s*${NODE_ID.source}(?:\\s*:\\s*(.+))?`);
|
|
5
|
+
// Matches composite state declarations: state StateName { or state "Label" as StateName {
|
|
6
|
+
const COMPOSITE_STATE_RE = /^state\s+(?:"([^"]+)"\s+as\s+)?(\w+)\s*\{/i;
|
|
5
7
|
export function parseStateDiagram(syntax) {
|
|
6
8
|
const lines = prep(syntax);
|
|
7
9
|
if (!lines.some(l => /^stateDiagram(-v2)?$/i.test(l)))
|
|
8
10
|
return null;
|
|
11
|
+
// Pre-scan: identify all composite states before parsing edges
|
|
12
|
+
// This is needed because edges like `A --> CompositeState` may appear
|
|
13
|
+
// before the `state CompositeState { }` declaration.
|
|
14
|
+
const compositeStates = new Set();
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const compositeMatch = line.match(COMPOSITE_STATE_RE);
|
|
17
|
+
if (compositeMatch) {
|
|
18
|
+
compositeStates.add(compositeMatch[2]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
9
21
|
const nodeMap = new Map();
|
|
10
22
|
const edges = [];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
const subgraphs = [];
|
|
24
|
+
// Track composite state nesting
|
|
25
|
+
const stateStack = []; // stack of parent state IDs
|
|
26
|
+
const nodeParent = new Map(); // nodeId -> parent composite state ID
|
|
27
|
+
// Track [*] usage per context (global or within composite state)
|
|
28
|
+
const startNodes = new Map(); // context -> start node ID
|
|
29
|
+
const endNodes = new Map(); // context -> end node ID
|
|
30
|
+
function getContext() {
|
|
31
|
+
return stateStack.length > 0 ? stateStack[stateStack.length - 1] : '__global__';
|
|
32
|
+
}
|
|
33
|
+
function ensureStartNode(context) {
|
|
34
|
+
if (!startNodes.has(context)) {
|
|
35
|
+
const id = context === '__global__' ? '[*]_start' : `[*]_start_${context}`;
|
|
36
|
+
startNodes.set(context, id);
|
|
37
|
+
nodeMap.set(id, { id, label: '', shape: 'circle' });
|
|
38
|
+
if (context !== '__global__') {
|
|
39
|
+
nodeParent.set(id, context);
|
|
40
|
+
}
|
|
18
41
|
}
|
|
42
|
+
return startNodes.get(context);
|
|
19
43
|
}
|
|
20
|
-
function ensureEndNode() {
|
|
21
|
-
if (!
|
|
22
|
-
|
|
23
|
-
|
|
44
|
+
function ensureEndNode(context) {
|
|
45
|
+
if (!endNodes.has(context)) {
|
|
46
|
+
const id = context === '__global__' ? '[*]_end' : `[*]_end_${context}`;
|
|
47
|
+
endNodes.set(context, id);
|
|
48
|
+
nodeMap.set(id, { id, label: '', shape: 'doublecircle' });
|
|
49
|
+
if (context !== '__global__') {
|
|
50
|
+
nodeParent.set(id, context);
|
|
51
|
+
}
|
|
24
52
|
}
|
|
53
|
+
return endNodes.get(context);
|
|
25
54
|
}
|
|
26
|
-
function ensureNode(id) {
|
|
55
|
+
function ensureNode(id, context) {
|
|
27
56
|
if (id === '[*]')
|
|
28
57
|
return; // handled per-edge based on position
|
|
58
|
+
if (compositeStates.has(id))
|
|
59
|
+
return; // composite states are subgraphs, not nodes
|
|
29
60
|
if (nodeMap.has(id))
|
|
30
61
|
return;
|
|
31
62
|
nodeMap.set(id, { id, label: id, shape: 'round' });
|
|
63
|
+
if (context !== '__global__') {
|
|
64
|
+
nodeParent.set(id, context);
|
|
65
|
+
}
|
|
32
66
|
}
|
|
33
|
-
for (
|
|
34
|
-
|
|
35
|
-
|
|
67
|
+
for (let i = 0; i < lines.length; i++) {
|
|
68
|
+
const line = lines[i];
|
|
69
|
+
// Skip diagram declaration and direction
|
|
36
70
|
if (/^stateDiagram(-v2)?$/i.test(line))
|
|
37
71
|
continue;
|
|
72
|
+
if (/^direction\s/i.test(line))
|
|
73
|
+
continue;
|
|
74
|
+
if (/^note\s/i.test(line))
|
|
75
|
+
continue;
|
|
76
|
+
// Check for closing brace (end of composite state)
|
|
77
|
+
if (line === '}') {
|
|
78
|
+
stateStack.pop();
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Check for composite state declaration: state StateName {
|
|
82
|
+
const compositeMatch = line.match(COMPOSITE_STATE_RE);
|
|
83
|
+
if (compositeMatch) {
|
|
84
|
+
const label = compositeMatch[1] || compositeMatch[2];
|
|
85
|
+
const id = compositeMatch[2];
|
|
86
|
+
const context = getContext();
|
|
87
|
+
// Create subgraph entry
|
|
88
|
+
subgraphs.push({
|
|
89
|
+
id,
|
|
90
|
+
label,
|
|
91
|
+
nodeIds: [], // will be populated later
|
|
92
|
+
});
|
|
93
|
+
// If nested inside another composite state, track parent
|
|
94
|
+
if (context !== '__global__') {
|
|
95
|
+
nodeParent.set(id, context);
|
|
96
|
+
}
|
|
97
|
+
// Push onto stack for child nodes
|
|
98
|
+
stateStack.push(id);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Skip simple state declarations (state StateName) without braces
|
|
102
|
+
if (/^state\s+/i.test(line) && !line.includes('{'))
|
|
103
|
+
continue;
|
|
104
|
+
// Parse transitions
|
|
38
105
|
const m = line.match(EDGE_RE);
|
|
39
106
|
if (m) {
|
|
40
107
|
const rawSource = m[1];
|
|
41
108
|
const rawTarget = m[2];
|
|
42
109
|
const label = m[3]?.trim();
|
|
110
|
+
const context = getContext();
|
|
43
111
|
// Resolve [*] to start or end based on edge position
|
|
44
112
|
let source;
|
|
45
113
|
let target;
|
|
114
|
+
let sourceSubgraphId;
|
|
115
|
+
let targetSubgraphId;
|
|
46
116
|
if (rawSource === '[*]') {
|
|
47
|
-
ensureStartNode();
|
|
48
|
-
|
|
117
|
+
source = ensureStartNode(context);
|
|
118
|
+
}
|
|
119
|
+
else if (compositeStates.has(rawSource)) {
|
|
120
|
+
// Edge from composite state: use subgraph boundary
|
|
121
|
+
source = rawSource; // temporary, will be replaced
|
|
122
|
+
sourceSubgraphId = rawSource;
|
|
49
123
|
}
|
|
50
124
|
else {
|
|
51
|
-
ensureNode(rawSource);
|
|
125
|
+
ensureNode(rawSource, context);
|
|
52
126
|
source = rawSource;
|
|
53
127
|
}
|
|
54
128
|
if (rawTarget === '[*]') {
|
|
55
|
-
ensureEndNode();
|
|
56
|
-
|
|
129
|
+
target = ensureEndNode(context);
|
|
130
|
+
}
|
|
131
|
+
else if (compositeStates.has(rawTarget)) {
|
|
132
|
+
// Edge to composite state: use subgraph boundary
|
|
133
|
+
target = rawTarget; // temporary, will be replaced
|
|
134
|
+
targetSubgraphId = rawTarget;
|
|
57
135
|
}
|
|
58
136
|
else {
|
|
59
|
-
ensureNode(rawTarget);
|
|
137
|
+
ensureNode(rawTarget, context);
|
|
60
138
|
target = rawTarget;
|
|
61
139
|
}
|
|
62
|
-
edges.push({ source, target, label });
|
|
140
|
+
edges.push({ source, target, label, sourceSubgraphId, targetSubgraphId });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Populate subgraph nodeIds based on nodeParent mapping
|
|
144
|
+
for (const subgraph of subgraphs) {
|
|
145
|
+
subgraph.nodeIds = [];
|
|
146
|
+
for (const [nodeId, parentId] of nodeParent.entries()) {
|
|
147
|
+
if (parentId === subgraph.id) {
|
|
148
|
+
subgraph.nodeIds.push(nodeId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Build maps for incoming vs outgoing edge representative nodes
|
|
153
|
+
// Use start node for incoming edges (so they enter from above)
|
|
154
|
+
// Use end node for outgoing edges ONLY if an explicit end node exists
|
|
155
|
+
// (i.e., there's a [*] transition inside the subgraph). Otherwise use start node.
|
|
156
|
+
// This prevents orphan end nodes that dagre positions randomly.
|
|
157
|
+
const subgraphIncomingRep = new Map(); // for targetSubgraphId
|
|
158
|
+
const subgraphOutgoingRep = new Map(); // for sourceSubgraphId
|
|
159
|
+
for (const subgraph of subgraphs) {
|
|
160
|
+
// For incoming edges: use start node (top of subgraph)
|
|
161
|
+
const startNode = startNodes.get(subgraph.id);
|
|
162
|
+
if (startNode) {
|
|
163
|
+
subgraphIncomingRep.set(subgraph.id, startNode);
|
|
164
|
+
}
|
|
165
|
+
else if (subgraph.nodeIds.length > 0) {
|
|
166
|
+
subgraphIncomingRep.set(subgraph.id, subgraph.nodeIds[0]);
|
|
167
|
+
}
|
|
168
|
+
// For outgoing edges: use end node (now guaranteed to exist for subgraphs with outgoing edges)
|
|
169
|
+
const endNode = endNodes.get(subgraph.id);
|
|
170
|
+
if (endNode) {
|
|
171
|
+
subgraphOutgoingRep.set(subgraph.id, endNode);
|
|
172
|
+
}
|
|
173
|
+
else if (startNode) {
|
|
174
|
+
subgraphOutgoingRep.set(subgraph.id, startNode);
|
|
175
|
+
}
|
|
176
|
+
else if (subgraph.nodeIds.length > 0) {
|
|
177
|
+
subgraphOutgoingRep.set(subgraph.id, subgraph.nodeIds[0]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Fix edges that reference composite states: replace subgraph ID with appropriate representative node
|
|
181
|
+
for (const edge of edges) {
|
|
182
|
+
if (edge.sourceSubgraphId && edge.source === edge.sourceSubgraphId) {
|
|
183
|
+
// Outgoing edge: use end node (bottom) so target renders below subgraph
|
|
184
|
+
const repNode = subgraphOutgoingRep.get(edge.sourceSubgraphId);
|
|
185
|
+
if (repNode) {
|
|
186
|
+
edge.source = repNode;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (edge.targetSubgraphId && edge.target === edge.targetSubgraphId) {
|
|
190
|
+
// Incoming edge: use start node (top) so edge enters from above
|
|
191
|
+
const repNode = subgraphIncomingRep.get(edge.targetSubgraphId);
|
|
192
|
+
if (repNode) {
|
|
193
|
+
edge.target = repNode;
|
|
194
|
+
}
|
|
63
195
|
}
|
|
64
196
|
}
|
|
65
197
|
if (nodeMap.size === 0)
|
|
66
198
|
return null;
|
|
67
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
kind: 'graph',
|
|
201
|
+
direction: 'TB',
|
|
202
|
+
diagramType: 'state',
|
|
203
|
+
nodes: Array.from(nodeMap.values()),
|
|
204
|
+
edges,
|
|
205
|
+
subgraphs: subgraphs.length > 0 ? subgraphs : undefined,
|
|
206
|
+
};
|
|
68
207
|
}
|
package/dist/theme/dark.js
CHANGED
|
@@ -25,14 +25,14 @@ const font = {
|
|
|
25
25
|
family: "'Roboto', sans-serif",
|
|
26
26
|
mono: "'Roboto Mono', monospace",
|
|
27
27
|
size: {
|
|
28
|
-
xxs: '
|
|
29
|
-
xs: '
|
|
30
|
-
sm: '
|
|
31
|
-
md: '
|
|
32
|
-
base: '
|
|
33
|
-
lg: '
|
|
34
|
-
xl: '
|
|
35
|
-
xxl: '
|
|
28
|
+
xxs: '10px',
|
|
29
|
+
xs: '11px',
|
|
30
|
+
sm: '12px',
|
|
31
|
+
md: '13px',
|
|
32
|
+
base: '14px',
|
|
33
|
+
lg: '16px',
|
|
34
|
+
xl: '18px',
|
|
35
|
+
xxl: '20px',
|
|
36
36
|
},
|
|
37
37
|
weight: {
|
|
38
38
|
regular: 400,
|
|
@@ -98,7 +98,7 @@ const edgeDefaults = {
|
|
|
98
98
|
style: { stroke: color.gray3, strokeWidth: 1.5 },
|
|
99
99
|
labelStyle: {
|
|
100
100
|
fill: color.gray3,
|
|
101
|
-
fontSize:
|
|
101
|
+
fontSize: 13,
|
|
102
102
|
fontWeight: font.weight.medium,
|
|
103
103
|
fontFamily: font.family,
|
|
104
104
|
},
|
|
@@ -145,7 +145,7 @@ const nodeBase = {
|
|
|
145
145
|
// ─── Per-Node-Type Sizing ───────────────────────────────────────────────────
|
|
146
146
|
const nodeStyles = {
|
|
147
147
|
flow: {
|
|
148
|
-
diamond: { labelMaxWidth: '
|
|
148
|
+
diamond: { labelMaxWidth: '140px' },
|
|
149
149
|
markdownCode: { backgroundColor: color.darkBg3, padding: `${space[1]} ${space[2]}` },
|
|
150
150
|
},
|
|
151
151
|
state: {
|
|
@@ -165,7 +165,7 @@ const nodeStyles = {
|
|
|
165
165
|
subgraph: {
|
|
166
166
|
pointerEvents: 'none',
|
|
167
167
|
borderStyle: '1px dashed',
|
|
168
|
-
bgOpacity: '
|
|
168
|
+
bgOpacity: '88',
|
|
169
169
|
labelOffset: 28,
|
|
170
170
|
},
|
|
171
171
|
detail: {
|
package/dist/theme/light.js
CHANGED
|
@@ -25,14 +25,14 @@ const font = {
|
|
|
25
25
|
family: "'Roboto', sans-serif",
|
|
26
26
|
mono: "'Roboto Mono', monospace",
|
|
27
27
|
size: {
|
|
28
|
-
xxs: '
|
|
29
|
-
xs: '
|
|
30
|
-
sm: '
|
|
31
|
-
md: '
|
|
32
|
-
base: '
|
|
33
|
-
lg: '
|
|
34
|
-
xl: '
|
|
35
|
-
xxl: '
|
|
28
|
+
xxs: '10px',
|
|
29
|
+
xs: '11px',
|
|
30
|
+
sm: '12px',
|
|
31
|
+
md: '13px',
|
|
32
|
+
base: '14px',
|
|
33
|
+
lg: '16px',
|
|
34
|
+
xl: '18px',
|
|
35
|
+
xxl: '20px',
|
|
36
36
|
},
|
|
37
37
|
weight: {
|
|
38
38
|
regular: 400,
|
|
@@ -98,7 +98,7 @@ const edgeDefaults = {
|
|
|
98
98
|
style: { stroke: color.gray3, strokeWidth: 1.5 },
|
|
99
99
|
labelStyle: {
|
|
100
100
|
fill: color.gray4,
|
|
101
|
-
fontSize:
|
|
101
|
+
fontSize: 13,
|
|
102
102
|
fontWeight: font.weight.medium,
|
|
103
103
|
fontFamily: font.family,
|
|
104
104
|
},
|
|
@@ -145,7 +145,7 @@ const nodeBase = {
|
|
|
145
145
|
// ─── Per-Node-Type Sizing ───────────────────────────────────────────────────
|
|
146
146
|
const nodeStyles = {
|
|
147
147
|
flow: {
|
|
148
|
-
diamond: { labelMaxWidth: '
|
|
148
|
+
diamond: { labelMaxWidth: '140px' },
|
|
149
149
|
markdownCode: { backgroundColor: color.darkBg3, padding: `${space[1]} ${space[2]}` },
|
|
150
150
|
},
|
|
151
151
|
state: {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface BoxShadowExtent {
|
|
2
|
+
left: number;
|
|
3
|
+
right: number;
|
|
4
|
+
top: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
max: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function getBoxShadowExtent(boxShadow: string | null | undefined): BoxShadowExtent;
|
|
9
|
+
export declare function getBoxShadowMaxExtent(boxShadow: string | null | undefined): number;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
function splitOutsideParens(input, delimiter) {
|
|
2
|
+
const out = [];
|
|
3
|
+
let buf = '';
|
|
4
|
+
let depth = 0;
|
|
5
|
+
for (let i = 0; i < input.length; i++) {
|
|
6
|
+
const ch = input[i];
|
|
7
|
+
if (ch === '(')
|
|
8
|
+
depth++;
|
|
9
|
+
if (ch === ')')
|
|
10
|
+
depth = Math.max(0, depth - 1);
|
|
11
|
+
if (depth === 0 && ch === delimiter) {
|
|
12
|
+
const s = buf.trim();
|
|
13
|
+
if (s)
|
|
14
|
+
out.push(s);
|
|
15
|
+
buf = '';
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
buf += ch;
|
|
19
|
+
}
|
|
20
|
+
const s = buf.trim();
|
|
21
|
+
if (s)
|
|
22
|
+
out.push(s);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function splitWhitespaceOutsideParens(input) {
|
|
26
|
+
const out = [];
|
|
27
|
+
let buf = '';
|
|
28
|
+
let depth = 0;
|
|
29
|
+
for (let i = 0; i < input.length; i++) {
|
|
30
|
+
const ch = input[i];
|
|
31
|
+
if (ch === '(')
|
|
32
|
+
depth++;
|
|
33
|
+
if (ch === ')')
|
|
34
|
+
depth = Math.max(0, depth - 1);
|
|
35
|
+
const isWs = ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r';
|
|
36
|
+
if (depth === 0 && isWs) {
|
|
37
|
+
const s = buf.trim();
|
|
38
|
+
if (s)
|
|
39
|
+
out.push(s);
|
|
40
|
+
buf = '';
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
buf += ch;
|
|
44
|
+
}
|
|
45
|
+
const s = buf.trim();
|
|
46
|
+
if (s)
|
|
47
|
+
out.push(s);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function isLengthToken(token) {
|
|
51
|
+
// Keep intentionally conservative: we only care about px/unitless values.
|
|
52
|
+
return /^-?\d*\.?\d+(px)?$/i.test(token);
|
|
53
|
+
}
|
|
54
|
+
function parsePx(token) {
|
|
55
|
+
const t = token.trim().toLowerCase();
|
|
56
|
+
const n = t.endsWith('px') ? parseFloat(t.slice(0, -2)) : parseFloat(t);
|
|
57
|
+
return Number.isFinite(n) ? n : 0;
|
|
58
|
+
}
|
|
59
|
+
export function getBoxShadowExtent(boxShadow) {
|
|
60
|
+
if (!boxShadow)
|
|
61
|
+
return { left: 0, right: 0, top: 0, bottom: 0, max: 0 };
|
|
62
|
+
const src = String(boxShadow).trim();
|
|
63
|
+
if (!src || src === 'none')
|
|
64
|
+
return { left: 0, right: 0, top: 0, bottom: 0, max: 0 };
|
|
65
|
+
let left = 0;
|
|
66
|
+
let right = 0;
|
|
67
|
+
let top = 0;
|
|
68
|
+
let bottom = 0;
|
|
69
|
+
// box-shadow is a comma-separated list, but colors can contain commas (rgba()).
|
|
70
|
+
const shadows = splitOutsideParens(src, ',');
|
|
71
|
+
for (const shadow of shadows) {
|
|
72
|
+
const tokens = splitWhitespaceOutsideParens(shadow);
|
|
73
|
+
if (tokens.length < 2)
|
|
74
|
+
continue;
|
|
75
|
+
if (tokens.includes('inset'))
|
|
76
|
+
continue;
|
|
77
|
+
const lengths = [];
|
|
78
|
+
for (const tok of tokens) {
|
|
79
|
+
if (!isLengthToken(tok))
|
|
80
|
+
continue;
|
|
81
|
+
lengths.push(parsePx(tok));
|
|
82
|
+
if (lengths.length >= 4)
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
if (lengths.length < 2)
|
|
86
|
+
continue;
|
|
87
|
+
const offsetX = lengths[0];
|
|
88
|
+
const offsetY = lengths[1];
|
|
89
|
+
const blur = Math.max(0, lengths[2] ?? 0);
|
|
90
|
+
const spread = lengths[3] ?? 0;
|
|
91
|
+
const base = Math.max(0, blur + spread);
|
|
92
|
+
// Extents outside the element box in each direction.
|
|
93
|
+
left = Math.max(left, Math.max(0, base - offsetX));
|
|
94
|
+
right = Math.max(right, Math.max(0, base + offsetX));
|
|
95
|
+
top = Math.max(top, Math.max(0, base - offsetY));
|
|
96
|
+
bottom = Math.max(bottom, Math.max(0, base + offsetY));
|
|
97
|
+
}
|
|
98
|
+
const max = Math.max(left, right, top, bottom);
|
|
99
|
+
return { left, right, top, bottom, max };
|
|
100
|
+
}
|
|
101
|
+
export function getBoxShadowMaxExtent(boxShadow) {
|
|
102
|
+
return getBoxShadowExtent(boxShadow).max;
|
|
103
|
+
}
|