@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,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequenceLayout.ts — Grid-based layout for sequence diagrams.
|
|
3
|
+
*
|
|
4
|
+
* Sequence diagrams don't use dagre. Instead we lay out participants
|
|
5
|
+
* in a fixed horizontal row and create invisible anchor nodes along
|
|
6
|
+
* each lifeline at each message row. Edges connect anchor-to-anchor.
|
|
7
|
+
*
|
|
8
|
+
* Message labels are rendered as positioned node components (above the line),
|
|
9
|
+
* not as React Flow edge labels. Autonumber indicators appear as numbered
|
|
10
|
+
* circles on the source lifeline.
|
|
11
|
+
*
|
|
12
|
+
* Row heights are computed dynamically based on content — multi-line labels
|
|
13
|
+
* and notes get more vertical space so nothing overlaps.
|
|
14
|
+
*
|
|
15
|
+
* Notes span the full width between the participants they're attached to,
|
|
16
|
+
* matching standard sequence diagram rendering.
|
|
17
|
+
*/
|
|
18
|
+
import { MarkerType } from '@xyflow/react';
|
|
19
|
+
import { getConfig } from '../config.js';
|
|
20
|
+
// ─── Layout Constants ──────────────────────────────────────────────────────
|
|
21
|
+
const PARTICIPANT_WIDTH = 150;
|
|
22
|
+
const PARTICIPANT_HEIGHT = 40;
|
|
23
|
+
const PARTICIPANT_GAP = 60;
|
|
24
|
+
const BASE_MESSAGE_ROW_HEIGHT = 55;
|
|
25
|
+
const SELF_MESSAGE_EXTRA = 30;
|
|
26
|
+
const LABEL_LINE_HEIGHT = 16;
|
|
27
|
+
const LABEL_GAP_BELOW = 6;
|
|
28
|
+
const TOP_PADDING = 20;
|
|
29
|
+
const LEFT_PADDING = 40;
|
|
30
|
+
const LIFELINE_START_OFFSET = 40;
|
|
31
|
+
const BOTTOM_PARTICIPANT_GAP = 40;
|
|
32
|
+
const ANCHOR_WIDTH = 2;
|
|
33
|
+
const ANCHOR_HEIGHT = 2;
|
|
34
|
+
const NOTE_BASE_HEIGHT = 36;
|
|
35
|
+
const NOTE_LINE_HEIGHT = 16;
|
|
36
|
+
const NOTE_OFFSET_X = 10;
|
|
37
|
+
const NOTE_VERTICAL_GAP = 20;
|
|
38
|
+
const NOTE_PADDING_INNER = 12;
|
|
39
|
+
const BLOCK_PADDING_X = 20;
|
|
40
|
+
const BLOCK_PADDING_TOP = 30;
|
|
41
|
+
const BLOCK_PADDING_BOTTOM = 15;
|
|
42
|
+
const NUMBER_CIRCLE_SIZE = 22;
|
|
43
|
+
const CHAR_WIDTH_ESTIMATE = 6;
|
|
44
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
45
|
+
function cleanBrTags(text) {
|
|
46
|
+
return text.replace(/<br\s*\/?>/gi, '\n');
|
|
47
|
+
}
|
|
48
|
+
function estimateTextHeight(text, containerWidth, lineHeight, charWidthEstimate) {
|
|
49
|
+
const lines = text.split('\n');
|
|
50
|
+
let totalLines = 0;
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const charsPerLine = Math.max(1, Math.floor(containerWidth / charWidthEstimate));
|
|
53
|
+
totalLines += Math.max(1, Math.ceil(line.length / charsPerLine));
|
|
54
|
+
}
|
|
55
|
+
return totalLines * lineHeight;
|
|
56
|
+
}
|
|
57
|
+
function participantX(index, leftPadding, participantWidth, participantGap) {
|
|
58
|
+
return leftPadding + index * (participantWidth + participantGap);
|
|
59
|
+
}
|
|
60
|
+
function participantCenterX(index, leftPadding, participantWidth, participantGap) {
|
|
61
|
+
return participantX(index, leftPadding, participantWidth, participantGap) + participantWidth / 2;
|
|
62
|
+
}
|
|
63
|
+
function computeNoteGeometry(text, placement, participantIds, participantIndex, seq) {
|
|
64
|
+
const firstPIdx = participantIndex.get(participantIds[0]) ?? 0;
|
|
65
|
+
let x;
|
|
66
|
+
let width;
|
|
67
|
+
if (placement === 'over') {
|
|
68
|
+
if (participantIds.length >= 2) {
|
|
69
|
+
const lastPIdx = participantIndex.get(participantIds[participantIds.length - 1]) ?? 0;
|
|
70
|
+
const minIdx = Math.min(firstPIdx, lastPIdx);
|
|
71
|
+
const maxIdx = Math.max(firstPIdx, lastPIdx);
|
|
72
|
+
// Span between lifeline centers + small margin
|
|
73
|
+
x = participantCenterX(minIdx, seq.leftPadding, seq.participantWidth, seq.participantGap) - 10;
|
|
74
|
+
width =
|
|
75
|
+
participantCenterX(maxIdx, seq.leftPadding, seq.participantWidth, seq.participantGap)
|
|
76
|
+
- participantCenterX(minIdx, seq.leftPadding, seq.participantWidth, seq.participantGap)
|
|
77
|
+
+ 20;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
width = seq.participantWidth + 20;
|
|
81
|
+
x = participantCenterX(firstPIdx, seq.leftPadding, seq.participantWidth, seq.participantGap) - width / 2;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (placement === 'right') {
|
|
85
|
+
width = seq.participantWidth;
|
|
86
|
+
x = participantX(firstPIdx, seq.leftPadding, seq.participantWidth, seq.participantGap)
|
|
87
|
+
+ seq.participantWidth + seq.noteOffsetX;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
width = seq.participantWidth;
|
|
91
|
+
x = participantX(firstPIdx, seq.leftPadding, seq.participantWidth, seq.participantGap)
|
|
92
|
+
- seq.participantWidth - seq.noteOffsetX;
|
|
93
|
+
}
|
|
94
|
+
const textH = estimateTextHeight(text, width - seq.notePaddingInner * 2, seq.noteLineHeight, seq.textCharWidthEstimate);
|
|
95
|
+
const height = Math.max(seq.noteBaseHeight, textH + 12);
|
|
96
|
+
return { x, width, height };
|
|
97
|
+
}
|
|
98
|
+
// ─── Main Layout ───────────────────────────────────────────────────────────
|
|
99
|
+
export function layoutSequenceDiagram(sequence, theme, metadata) {
|
|
100
|
+
const seq = getConfig().layout.sequence;
|
|
101
|
+
const nodes = [];
|
|
102
|
+
const edges = [];
|
|
103
|
+
const participantIndex = new Map();
|
|
104
|
+
sequence.participants.forEach((p, i) => {
|
|
105
|
+
participantIndex.set(p.id, i);
|
|
106
|
+
});
|
|
107
|
+
// ── Pre-compute cleaned text ──
|
|
108
|
+
const cleanedLabels = sequence.messages.map(m => cleanBrTags(m.label));
|
|
109
|
+
const cleanedNotes = sequence.notes
|
|
110
|
+
? sequence.notes.map(n => cleanBrTags(n.text))
|
|
111
|
+
: [];
|
|
112
|
+
// ── Label heights ──
|
|
113
|
+
const labelHeights = sequence.messages.map((msg, mi) => {
|
|
114
|
+
const label = cleanedLabels[mi];
|
|
115
|
+
if (!label)
|
|
116
|
+
return 0;
|
|
117
|
+
const fromIdx = participantIndex.get(msg.from) ?? 0;
|
|
118
|
+
const toIdx = participantIndex.get(msg.to) ?? 0;
|
|
119
|
+
if (msg.from === msg.to) {
|
|
120
|
+
return estimateTextHeight(label, 120, seq.labelLineHeight, seq.textCharWidthEstimate);
|
|
121
|
+
}
|
|
122
|
+
const availableWidth = Math.abs(participantCenterX(toIdx, seq.leftPadding, seq.participantWidth, seq.participantGap)
|
|
123
|
+
- participantCenterX(fromIdx, seq.leftPadding, seq.participantWidth, seq.participantGap)) - 20;
|
|
124
|
+
return estimateTextHeight(label, Math.max(availableWidth, 80), seq.labelLineHeight, seq.textCharWidthEstimate);
|
|
125
|
+
});
|
|
126
|
+
// ── Note geometries ──
|
|
127
|
+
const noteGeos = sequence.notes
|
|
128
|
+
? sequence.notes.map((note, ni) => computeNoteGeometry(cleanedNotes[ni], note.placement, note.participants, participantIndex, seq))
|
|
129
|
+
: [];
|
|
130
|
+
// ── Compute message Y positions ──
|
|
131
|
+
// Each message row is tall enough for its label above the line.
|
|
132
|
+
// Notes consume their own vertical rows before the message they precede.
|
|
133
|
+
const lifelineStartY = seq.topPadding + seq.participantHeight + seq.lifelineStartOffset;
|
|
134
|
+
const messageY = [];
|
|
135
|
+
const noteYPositions = new Map();
|
|
136
|
+
let currentY = lifelineStartY;
|
|
137
|
+
const noteSpaceBefore = new Map();
|
|
138
|
+
if (sequence.notes) {
|
|
139
|
+
for (let ni = 0; ni < sequence.notes.length; ni++) {
|
|
140
|
+
const group = noteSpaceBefore.get(sequence.notes[ni].messageIndex) ?? [];
|
|
141
|
+
group.push(ni);
|
|
142
|
+
noteSpaceBefore.set(sequence.notes[ni].messageIndex, group);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (let mi = 0; mi < sequence.messages.length; mi++) {
|
|
146
|
+
// Notes that appear before this message
|
|
147
|
+
const notesBefore = noteSpaceBefore.get(mi);
|
|
148
|
+
if (notesBefore) {
|
|
149
|
+
for (const ni of notesBefore) {
|
|
150
|
+
noteYPositions.set(ni, currentY);
|
|
151
|
+
currentY += noteGeos[ni].height + seq.noteVerticalGap;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// The label sits ABOVE the arrow. The arrow sits at `messageY[mi]`.
|
|
155
|
+
// We need enough space above the arrow for the label, so we push
|
|
156
|
+
// the arrow down by the label height.
|
|
157
|
+
const labelH = labelHeights[mi];
|
|
158
|
+
if (labelH > seq.labelLineHeight) {
|
|
159
|
+
// Multi-line label — add extra space so the label doesn't overlap
|
|
160
|
+
// things above (like participant headers or previous message)
|
|
161
|
+
currentY += labelH - seq.labelLineHeight;
|
|
162
|
+
}
|
|
163
|
+
messageY.push(currentY);
|
|
164
|
+
const isSelf = sequence.messages[mi].from === sequence.messages[mi].to;
|
|
165
|
+
const rowHeight = Math.max(seq.baseMessageRowHeight, seq.labelLineHeight + seq.labelGapBelow + 20 + (isSelf ? seq.selfMessageExtra : 0));
|
|
166
|
+
currentY += rowHeight;
|
|
167
|
+
}
|
|
168
|
+
// Trailing notes
|
|
169
|
+
const trailingNotes = noteSpaceBefore.get(sequence.messages.length);
|
|
170
|
+
if (trailingNotes) {
|
|
171
|
+
for (const ni of trailingNotes) {
|
|
172
|
+
noteYPositions.set(ni, currentY);
|
|
173
|
+
currentY += noteGeos[ni].height + seq.noteVerticalGap;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const lifelineEndY = currentY + seq.bottomParticipantGap;
|
|
177
|
+
const bottomParticipantY = lifelineEndY;
|
|
178
|
+
// ── Block overlay nodes ──
|
|
179
|
+
if (sequence.blocks) {
|
|
180
|
+
for (let bi = 0; bi < sequence.blocks.length; bi++) {
|
|
181
|
+
const block = sequence.blocks[bi];
|
|
182
|
+
const startY = (messageY[block.startIndex] ?? lifelineStartY) - 25;
|
|
183
|
+
const endY = (messageY[Math.max(0, block.endIndex - 1)] ?? startY) + 25;
|
|
184
|
+
const involvedIndices = new Set();
|
|
185
|
+
for (let mi = block.startIndex; mi < block.endIndex; mi++) {
|
|
186
|
+
const msg = sequence.messages[mi];
|
|
187
|
+
if (!msg)
|
|
188
|
+
continue;
|
|
189
|
+
const fi = participantIndex.get(msg.from);
|
|
190
|
+
const ti = participantIndex.get(msg.to);
|
|
191
|
+
if (fi !== undefined)
|
|
192
|
+
involvedIndices.add(fi);
|
|
193
|
+
if (ti !== undefined)
|
|
194
|
+
involvedIndices.add(ti);
|
|
195
|
+
}
|
|
196
|
+
const indices = involvedIndices.size > 0
|
|
197
|
+
? Array.from(involvedIndices)
|
|
198
|
+
: sequence.participants.map((_, i) => i);
|
|
199
|
+
const firstX = Math.min(...indices.map((i) => participantX(i, seq.leftPadding, seq.participantWidth, seq.participantGap)));
|
|
200
|
+
const lastX = Math.max(...indices.map((i) => participantX(i, seq.leftPadding, seq.participantWidth, seq.participantGap) + seq.participantWidth));
|
|
201
|
+
const blockX = firstX - seq.blockPaddingX;
|
|
202
|
+
const blockY = startY - seq.blockPaddingTop;
|
|
203
|
+
const blockW = lastX - firstX + seq.blockPaddingX * 2;
|
|
204
|
+
const blockH = endY - startY + seq.blockPaddingTop + seq.blockPaddingBottom;
|
|
205
|
+
nodes.push({
|
|
206
|
+
id: `__block_${bi}`,
|
|
207
|
+
position: { x: blockX, y: blockY },
|
|
208
|
+
data: {
|
|
209
|
+
label: block.type,
|
|
210
|
+
blockLabel: block.label,
|
|
211
|
+
blockType: block.type,
|
|
212
|
+
sections: block.sections,
|
|
213
|
+
blockWidth: blockW,
|
|
214
|
+
blockHeight: blockH,
|
|
215
|
+
messageY,
|
|
216
|
+
blockStartY: blockY,
|
|
217
|
+
},
|
|
218
|
+
type: 'sequenceBlock',
|
|
219
|
+
draggable: false,
|
|
220
|
+
selectable: false,
|
|
221
|
+
style: { zIndex: -1 },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ── Lifeline nodes ──
|
|
226
|
+
for (const p of sequence.participants) {
|
|
227
|
+
const idx = participantIndex.get(p.id);
|
|
228
|
+
nodes.push({
|
|
229
|
+
id: `__lifeline_${p.id}`,
|
|
230
|
+
position: { x: participantCenterX(idx, seq.leftPadding, seq.participantWidth, seq.participantGap) - 1, y: lifelineStartY },
|
|
231
|
+
data: { lifelineHeight: lifelineEndY - lifelineStartY },
|
|
232
|
+
type: 'sequenceLifeline',
|
|
233
|
+
draggable: false,
|
|
234
|
+
selectable: false,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// ── Top participant nodes ──
|
|
238
|
+
for (const p of sequence.participants) {
|
|
239
|
+
const idx = participantIndex.get(p.id);
|
|
240
|
+
nodes.push({
|
|
241
|
+
id: p.id,
|
|
242
|
+
position: { x: participantX(idx, seq.leftPadding, seq.participantWidth, seq.participantGap), y: seq.topPadding },
|
|
243
|
+
data: {
|
|
244
|
+
label: cleanBrTags(p.label),
|
|
245
|
+
participantType: p.type,
|
|
246
|
+
meta: metadata?.[p.id],
|
|
247
|
+
layoutWidth: seq.participantWidth,
|
|
248
|
+
layoutHeight: seq.participantHeight,
|
|
249
|
+
},
|
|
250
|
+
type: 'sequenceParticipant',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
// ── Bottom participant nodes ──
|
|
254
|
+
for (const p of sequence.participants) {
|
|
255
|
+
const idx = participantIndex.get(p.id);
|
|
256
|
+
nodes.push({
|
|
257
|
+
id: `__bottom_${p.id}`,
|
|
258
|
+
position: { x: participantX(idx, seq.leftPadding, seq.participantWidth, seq.participantGap), y: bottomParticipantY },
|
|
259
|
+
data: {
|
|
260
|
+
label: cleanBrTags(p.label),
|
|
261
|
+
participantType: p.type,
|
|
262
|
+
layoutWidth: seq.participantWidth,
|
|
263
|
+
layoutHeight: seq.participantHeight,
|
|
264
|
+
},
|
|
265
|
+
type: 'sequenceParticipant',
|
|
266
|
+
draggable: false,
|
|
267
|
+
selectable: false,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// ── Anchors + edges + labels + numbers ──
|
|
271
|
+
for (let mi = 0; mi < sequence.messages.length; mi++) {
|
|
272
|
+
const msg = sequence.messages[mi];
|
|
273
|
+
const fromIdx = participantIndex.get(msg.from);
|
|
274
|
+
const toIdx = participantIndex.get(msg.to);
|
|
275
|
+
if (fromIdx === undefined || toIdx === undefined)
|
|
276
|
+
continue;
|
|
277
|
+
const y = messageY[mi];
|
|
278
|
+
const isSelf = msg.from === msg.to;
|
|
279
|
+
const label = cleanedLabels[mi];
|
|
280
|
+
const labelH = labelHeights[mi];
|
|
281
|
+
const sourceAnchorId = `__anchor_${msg.from}_${mi}_src`;
|
|
282
|
+
nodes.push({
|
|
283
|
+
id: sourceAnchorId,
|
|
284
|
+
position: {
|
|
285
|
+
x: participantCenterX(fromIdx, seq.leftPadding, seq.participantWidth, seq.participantGap) - seq.anchorWidth / 2,
|
|
286
|
+
y: y - seq.anchorHeight / 2,
|
|
287
|
+
},
|
|
288
|
+
data: { anchor: true },
|
|
289
|
+
type: 'sequenceAnchor',
|
|
290
|
+
draggable: false,
|
|
291
|
+
selectable: false,
|
|
292
|
+
});
|
|
293
|
+
if (isSelf) {
|
|
294
|
+
const loopRightX = participantCenterX(fromIdx, seq.leftPadding, seq.participantWidth, seq.participantGap) + 40;
|
|
295
|
+
const loopBottomY = y + seq.selfMessageExtra + 10;
|
|
296
|
+
const cornerAnchorId = `__anchor_${msg.from}_${mi}_corner`;
|
|
297
|
+
const returnAnchorId = `__anchor_${msg.from}_${mi}_return`;
|
|
298
|
+
const cornerBottomId = `__anchor_${msg.from}_${mi}_cornerbot`;
|
|
299
|
+
nodes.push({ id: cornerAnchorId, position: { x: loopRightX - 1, y: y - 1 }, data: { anchor: true }, type: 'sequenceAnchor', draggable: false, selectable: false }, {
|
|
300
|
+
id: returnAnchorId,
|
|
301
|
+
position: {
|
|
302
|
+
x: participantCenterX(fromIdx, seq.leftPadding, seq.participantWidth, seq.participantGap) - 1,
|
|
303
|
+
y: loopBottomY - 1,
|
|
304
|
+
},
|
|
305
|
+
data: { anchor: true },
|
|
306
|
+
type: 'sequenceAnchor',
|
|
307
|
+
draggable: false,
|
|
308
|
+
selectable: false,
|
|
309
|
+
}, { id: cornerBottomId, position: { x: loopRightX - 1, y: loopBottomY - 1 }, data: { anchor: true }, type: 'sequenceAnchor', draggable: false, selectable: false });
|
|
310
|
+
edges.push(buildSequenceEdge(`e-self-${mi}-a`, sourceAnchorId, cornerAnchorId, msg.arrowType, theme, false), buildSequenceEdge(`e-self-${mi}-b`, cornerAnchorId, cornerBottomId, msg.arrowType, theme, false), buildSequenceEdge(`e-self-${mi}-c`, cornerBottomId, returnAnchorId, msg.arrowType, theme, true));
|
|
311
|
+
if (label) {
|
|
312
|
+
nodes.push({
|
|
313
|
+
id: `__msglabel_${mi}`,
|
|
314
|
+
position: { x: loopRightX + 6, y: y - 2 },
|
|
315
|
+
data: { label, labelWidth: 120 },
|
|
316
|
+
type: 'sequenceMessageLabel',
|
|
317
|
+
draggable: false,
|
|
318
|
+
selectable: false,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
const targetAnchorId = `__anchor_${msg.to}_${mi}_tgt`;
|
|
324
|
+
nodes.push({
|
|
325
|
+
id: targetAnchorId,
|
|
326
|
+
position: {
|
|
327
|
+
x: participantCenterX(toIdx, seq.leftPadding, seq.participantWidth, seq.participantGap) - seq.anchorWidth / 2,
|
|
328
|
+
y: y - seq.anchorHeight / 2,
|
|
329
|
+
},
|
|
330
|
+
data: { anchor: true },
|
|
331
|
+
type: 'sequenceAnchor',
|
|
332
|
+
draggable: false,
|
|
333
|
+
selectable: false,
|
|
334
|
+
});
|
|
335
|
+
edges.push(buildSequenceEdge(`e-msg-${mi}`, sourceAnchorId, targetAnchorId, msg.arrowType, theme, true));
|
|
336
|
+
if (label) {
|
|
337
|
+
const fromCx = participantCenterX(fromIdx, seq.leftPadding, seq.participantWidth, seq.participantGap);
|
|
338
|
+
const toCx = participantCenterX(toIdx, seq.leftPadding, seq.participantWidth, seq.participantGap);
|
|
339
|
+
const midX = (fromCx + toCx) / 2;
|
|
340
|
+
const labelWidth = Math.max(Math.abs(toCx - fromCx) - 20, 80);
|
|
341
|
+
nodes.push({
|
|
342
|
+
id: `__msglabel_${mi}`,
|
|
343
|
+
position: {
|
|
344
|
+
x: midX - labelWidth / 2,
|
|
345
|
+
y: y - labelH - LABEL_GAP_BELOW,
|
|
346
|
+
},
|
|
347
|
+
data: { label, labelWidth },
|
|
348
|
+
type: 'sequenceMessageLabel',
|
|
349
|
+
draggable: false,
|
|
350
|
+
selectable: false,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (sequence.autonumber) {
|
|
355
|
+
nodes.push({
|
|
356
|
+
id: `__number_${mi}`,
|
|
357
|
+
position: {
|
|
358
|
+
x: participantCenterX(fromIdx, seq.leftPadding, seq.participantWidth, seq.participantGap) - seq.numberCircleSize / 2,
|
|
359
|
+
y: y - seq.numberCircleSize / 2,
|
|
360
|
+
},
|
|
361
|
+
data: { number: mi + 1 },
|
|
362
|
+
type: 'sequenceNumber',
|
|
363
|
+
draggable: false,
|
|
364
|
+
selectable: false,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ── Note nodes ──
|
|
369
|
+
// Notes span the full width between the participants they're attached to.
|
|
370
|
+
if (sequence.notes) {
|
|
371
|
+
for (let ni = 0; ni < sequence.notes.length; ni++) {
|
|
372
|
+
const note = sequence.notes[ni];
|
|
373
|
+
const y = noteYPositions.get(ni) ?? ((messageY[note.messageIndex] ?? lifelineStartY) - seq.noteBaseHeight / 2);
|
|
374
|
+
const geo = noteGeos[ni];
|
|
375
|
+
nodes.push({
|
|
376
|
+
id: `__note_${ni}`,
|
|
377
|
+
position: { x: geo.x, y },
|
|
378
|
+
data: {
|
|
379
|
+
label: cleanedNotes[ni],
|
|
380
|
+
noteWidth: geo.width,
|
|
381
|
+
noteHeight: geo.height,
|
|
382
|
+
},
|
|
383
|
+
type: 'sequenceNote',
|
|
384
|
+
draggable: false,
|
|
385
|
+
selectable: false,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { nodes, edges };
|
|
390
|
+
}
|
|
391
|
+
// ─── Edge Builder ──────────────────────────────────────────────────────────
|
|
392
|
+
function buildSequenceEdge(id, source, target, arrowType, theme, showArrow) {
|
|
393
|
+
const isDashed = arrowType === 'dashed' || arrowType === 'dashed_open';
|
|
394
|
+
const isOpen = arrowType === 'solid_open' || arrowType === 'dashed_open';
|
|
395
|
+
return {
|
|
396
|
+
id,
|
|
397
|
+
source,
|
|
398
|
+
target,
|
|
399
|
+
type: 'straight',
|
|
400
|
+
style: {
|
|
401
|
+
stroke: theme.color.gray3,
|
|
402
|
+
strokeWidth: 1.5,
|
|
403
|
+
...(isDashed ? { strokeDasharray: '6,4' } : {}),
|
|
404
|
+
},
|
|
405
|
+
...(showArrow && !isOpen ? {
|
|
406
|
+
markerEnd: {
|
|
407
|
+
type: MarkerType.ArrowClosed,
|
|
408
|
+
color: theme.color.gray3,
|
|
409
|
+
width: 10,
|
|
410
|
+
height: 10,
|
|
411
|
+
},
|
|
412
|
+
} : {}),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subgraph.ts — Subgraph bounding box computation and node construction.
|
|
3
|
+
*
|
|
4
|
+
* When dagre compound clustering is used, cluster bounds come directly
|
|
5
|
+
* from dagre's computed layout. Falls back to manual bounding box
|
|
6
|
+
* calculation when cluster data isn't available.
|
|
7
|
+
*/
|
|
8
|
+
import dagre from '@dagrejs/dagre';
|
|
9
|
+
import type { Node } from '@xyflow/react';
|
|
10
|
+
import type { ParsedSubgraph } from '../types.js';
|
|
11
|
+
import type { Theme } from '../ThemeContext.js';
|
|
12
|
+
import type { NodePosition } from './types.js';
|
|
13
|
+
interface SubgraphBounds {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extracts subgraph bounds from dagre's compound cluster layout.
|
|
21
|
+
* Dagre computes cluster position/size automatically when setParent is used.
|
|
22
|
+
* Falls back to manual bounding box if dagre cluster data isn't available.
|
|
23
|
+
*/
|
|
24
|
+
export declare function computeSubgraphBounds(subgraph: ParsedSubgraph, nodePositions: Map<string, NodePosition>, padding: number, labelOffset: number, dagreGraph?: dagre.graphlib.Graph): SubgraphBounds | null;
|
|
25
|
+
export declare function buildSubgraphNode(subgraph: ParsedSubgraph, bounds: SubgraphBounds, theme: Theme): Node;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subgraph.ts — Subgraph bounding box computation and node construction.
|
|
3
|
+
*
|
|
4
|
+
* When dagre compound clustering is used, cluster bounds come directly
|
|
5
|
+
* from dagre's computed layout. Falls back to manual bounding box
|
|
6
|
+
* calculation when cluster data isn't available.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Extracts subgraph bounds from dagre's compound cluster layout.
|
|
10
|
+
* Dagre computes cluster position/size automatically when setParent is used.
|
|
11
|
+
* Falls back to manual bounding box if dagre cluster data isn't available.
|
|
12
|
+
*/
|
|
13
|
+
export function computeSubgraphBounds(subgraph, nodePositions, padding, labelOffset, dagreGraph) {
|
|
14
|
+
// Try dagre's computed cluster bounds first
|
|
15
|
+
if (dagreGraph) {
|
|
16
|
+
const clusterNode = dagreGraph.node(subgraph.id);
|
|
17
|
+
if (clusterNode && clusterNode.width && clusterNode.height) {
|
|
18
|
+
return {
|
|
19
|
+
x: clusterNode.x - clusterNode.width / 2,
|
|
20
|
+
y: clusterNode.y - clusterNode.height / 2 - labelOffset,
|
|
21
|
+
width: clusterNode.width,
|
|
22
|
+
height: clusterNode.height + labelOffset,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Fallback: manual bounding box from child node positions
|
|
27
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
28
|
+
for (const nodeId of subgraph.nodeIds) {
|
|
29
|
+
const pos = nodePositions.get(nodeId);
|
|
30
|
+
if (!pos)
|
|
31
|
+
continue;
|
|
32
|
+
minX = Math.min(minX, pos.x);
|
|
33
|
+
minY = Math.min(minY, pos.y);
|
|
34
|
+
maxX = Math.max(maxX, pos.x + pos.width);
|
|
35
|
+
maxY = Math.max(maxY, pos.y + pos.height);
|
|
36
|
+
}
|
|
37
|
+
if (!isFinite(minX))
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
x: minX - padding,
|
|
41
|
+
y: minY - padding - labelOffset,
|
|
42
|
+
width: maxX - minX + padding * 2,
|
|
43
|
+
height: maxY - minY + padding * 2 + labelOffset,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// ─── Subgraph Node Builder ──────────────────────────────────────────────────
|
|
47
|
+
export function buildSubgraphNode(subgraph, bounds, theme) {
|
|
48
|
+
const subgraphStyles = theme.nodeStyles.subgraph;
|
|
49
|
+
return {
|
|
50
|
+
id: subgraph.id,
|
|
51
|
+
position: { x: bounds.x, y: bounds.y },
|
|
52
|
+
data: { label: subgraph.label },
|
|
53
|
+
type: 'subgraph',
|
|
54
|
+
style: {
|
|
55
|
+
width: bounds.width,
|
|
56
|
+
height: bounds.height,
|
|
57
|
+
backgroundColor: `${theme.color.darkBg3}${subgraphStyles.bgOpacity}`,
|
|
58
|
+
border: `${subgraphStyles.borderStyle} ${theme.color.gray3}`,
|
|
59
|
+
borderRadius: theme.radius.lg,
|
|
60
|
+
padding: theme.space[2],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — Shared type definitions for the layout engine.
|
|
3
|
+
*
|
|
4
|
+
* Centralized here (following Mermaid's rendering-util/types.ts pattern)
|
|
5
|
+
* so that any layout module can import types without pulling in
|
|
6
|
+
* implementation dependencies.
|
|
7
|
+
*/
|
|
8
|
+
export interface NodePosition {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
export interface HandlePositions {
|
|
15
|
+
sourcePosition: import('@xyflow/react').Position;
|
|
16
|
+
targetPosition: import('@xyflow/react').Position;
|
|
17
|
+
backEdgeSource: import('@xyflow/react').Position;
|
|
18
|
+
backEdgeTarget: import('@xyflow/react').Position;
|
|
19
|
+
}
|
|
20
|
+
export interface Point {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
}
|
|
24
|
+
export interface EdgeWaypoints {
|
|
25
|
+
source: string;
|
|
26
|
+
target: string;
|
|
27
|
+
points: Point[];
|
|
28
|
+
}
|
|
29
|
+
export interface NodeBounds {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { prep } from './helpers.js';
|
|
2
|
+
export function parseClassDiagram(syntax) {
|
|
3
|
+
const lines = prep(syntax);
|
|
4
|
+
if (!lines.some(l => /^classDiagram$/i.test(l)))
|
|
5
|
+
return null;
|
|
6
|
+
const nodeMap = new Map();
|
|
7
|
+
const edges = [];
|
|
8
|
+
const classProps = new Map();
|
|
9
|
+
const classMethods = new Map();
|
|
10
|
+
let currentClass = null;
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
if (/^classDiagram$/i.test(line))
|
|
13
|
+
continue;
|
|
14
|
+
// Class block: class ClassName {
|
|
15
|
+
const classBlock = line.match(/^class\s+(\w+)\s*\{?$/);
|
|
16
|
+
if (classBlock) {
|
|
17
|
+
const name = classBlock[1];
|
|
18
|
+
ensureClass(name);
|
|
19
|
+
currentClass = line.includes('{') ? name : null;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (line === '}') {
|
|
23
|
+
currentClass = null;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
// Member inside class block
|
|
27
|
+
if (currentClass) {
|
|
28
|
+
// Skip annotations like <<abstract>>, <<interface>>, <<enumeration>>
|
|
29
|
+
if (/^<<\w+>>$/.test(line))
|
|
30
|
+
continue;
|
|
31
|
+
if (line.includes('(')) {
|
|
32
|
+
classMethods.get(currentClass)?.push(line);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
classProps.get(currentClass)?.push(line);
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// Relationships
|
|
40
|
+
// Each pattern captures (left, right, label?). The `swap` flag indicates
|
|
41
|
+
// whether source/target should be reversed so the arrowhead points at the
|
|
42
|
+
// correct end. In Mermaid syntax, `A <|-- B` means B inherits from A,
|
|
43
|
+
// so the arrow should go from B → A (arrowhead at A = the parent).
|
|
44
|
+
const relPatterns = [
|
|
45
|
+
// Inheritance (solid line, hollow arrowhead)
|
|
46
|
+
{ pattern: /^(\w+)\s+<\|--\s+(\w+)(?:\s*:\s*(.+))?/, swap: true }, // A <|-- B → B extends A
|
|
47
|
+
{ pattern: /^(\w+)\s+--\|>\s+(\w+)(?:\s*:\s*(.+))?/, swap: false }, // A --|> B → A extends B
|
|
48
|
+
// Implementation (dashed line, hollow arrowhead)
|
|
49
|
+
{ pattern: /^(\w+)\s+<\|\.\.\s+(\w+)(?:\s*:\s*(.+))?/, swap: true }, // A <|.. B → B implements A
|
|
50
|
+
{ pattern: /^(\w+)\s+\.\.\|>\s+(\w+)(?:\s*:\s*(.+))?/, swap: false }, // A ..|> B → A implements B
|
|
51
|
+
// Cardinality (association with labels)
|
|
52
|
+
{ pattern: /^(\w+)\s+"[^"]*"\s*-->\s*"[^"]*"\s+(\w+)(?:\s*:\s*(.+))?/, swap: false },
|
|
53
|
+
// Association (solid line, arrow)
|
|
54
|
+
{ pattern: /^(\w+)\s+-->\s+(\w+)(?:\s*:\s*(.+))?/, swap: false },
|
|
55
|
+
// Dependency (dashed line, arrow)
|
|
56
|
+
{ pattern: /^(\w+)\s+\.\.>\s+(\w+)(?:\s*:\s*(.+))?/, swap: false },
|
|
57
|
+
// Dashed dependency (no arrow)
|
|
58
|
+
{ pattern: /^(\w+)\s+\.\.\s+(\w+)(?:\s*:\s*(.+))?/, swap: false },
|
|
59
|
+
// Aggregation (hollow diamond)
|
|
60
|
+
{ pattern: /^(\w+)\s+<--o\s+(\w+)(?:\s*:\s*(.+))?/, swap: true },
|
|
61
|
+
{ pattern: /^(\w+)\s+o-->\s+(\w+)(?:\s*:\s*(.+))?/, swap: false },
|
|
62
|
+
{ pattern: /^(\w+)\s+o--\s+(\w+)(?:\s*:\s*(.+))?/, swap: false },
|
|
63
|
+
// Composition (filled diamond)
|
|
64
|
+
{ pattern: /^(\w+)\s+<--\*\s+(\w+)(?:\s*:\s*(.+))?/, swap: true },
|
|
65
|
+
{ pattern: /^(\w+)\s+\*-->\s+(\w+)(?:\s*:\s*(.+))?/, swap: false },
|
|
66
|
+
{ pattern: /^(\w+)\s+\*--\s+(\w+)(?:\s*:\s*(.+))?/, swap: false },
|
|
67
|
+
// Plain association (solid line, no arrow)
|
|
68
|
+
{ pattern: /^(\w+)\s+--\s+(\w+)(?:\s*:\s*(.+))?/, swap: false },
|
|
69
|
+
];
|
|
70
|
+
for (const { pattern, swap } of relPatterns) {
|
|
71
|
+
const m = line.match(pattern);
|
|
72
|
+
if (m) {
|
|
73
|
+
ensureClass(m[1]);
|
|
74
|
+
ensureClass(m[2]);
|
|
75
|
+
const src = swap ? m[2] : m[1];
|
|
76
|
+
const tgt = swap ? m[1] : m[2];
|
|
77
|
+
edges.push({ source: src, target: tgt, label: m[3]?.trim(), dagreReversed: swap || undefined });
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (nodeMap.size === 0)
|
|
83
|
+
return null;
|
|
84
|
+
// Attach members to nodes
|
|
85
|
+
for (const [id, props] of classProps) {
|
|
86
|
+
const node = nodeMap.get(id);
|
|
87
|
+
if (node)
|
|
88
|
+
node.properties = props;
|
|
89
|
+
}
|
|
90
|
+
for (const [id, methods] of classMethods) {
|
|
91
|
+
const node = nodeMap.get(id);
|
|
92
|
+
if (node)
|
|
93
|
+
node.methods = methods;
|
|
94
|
+
}
|
|
95
|
+
return { kind: 'graph', direction: 'TB', diagramType: 'class', nodes: Array.from(nodeMap.values()), edges };
|
|
96
|
+
function ensureClass(name) {
|
|
97
|
+
if (nodeMap.has(name))
|
|
98
|
+
return;
|
|
99
|
+
nodeMap.set(name, { id: name, label: name, shape: 'rect', properties: [], methods: [] });
|
|
100
|
+
if (!classProps.has(name))
|
|
101
|
+
classProps.set(name, []);
|
|
102
|
+
if (!classMethods.has(name))
|
|
103
|
+
classMethods.set(name, []);
|
|
104
|
+
}
|
|
105
|
+
}
|