@tldraw/mermaid 4.6.0-internal.c7df3c92455a
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-cjs/blueprint.js +17 -0
- package/dist-cjs/blueprint.js.map +7 -0
- package/dist-cjs/colors.js +173 -0
- package/dist-cjs/colors.js.map +7 -0
- package/dist-cjs/createMermaidDiagram.js +144 -0
- package/dist-cjs/createMermaidDiagram.js.map +7 -0
- package/dist-cjs/flowchartDiagram.js +202 -0
- package/dist-cjs/flowchartDiagram.js.map +7 -0
- package/dist-cjs/index.d.ts +114 -0
- package/dist-cjs/index.js +34 -0
- package/dist-cjs/index.js.map +7 -0
- package/dist-cjs/renderBlueprint.js +314 -0
- package/dist-cjs/renderBlueprint.js.map +7 -0
- package/dist-cjs/sequenceDiagram.js +686 -0
- package/dist-cjs/sequenceDiagram.js.map +7 -0
- package/dist-cjs/stateDiagram.js +373 -0
- package/dist-cjs/stateDiagram.js.map +7 -0
- package/dist-cjs/svgParsing.js +187 -0
- package/dist-cjs/svgParsing.js.map +7 -0
- package/dist-cjs/utils.js +75 -0
- package/dist-cjs/utils.js.map +7 -0
- package/dist-esm/blueprint.mjs +1 -0
- package/dist-esm/blueprint.mjs.map +7 -0
- package/dist-esm/colors.mjs +153 -0
- package/dist-esm/colors.mjs.map +7 -0
- package/dist-esm/createMermaidDiagram.mjs +114 -0
- package/dist-esm/createMermaidDiagram.mjs.map +7 -0
- package/dist-esm/flowchartDiagram.mjs +188 -0
- package/dist-esm/flowchartDiagram.mjs.map +7 -0
- package/dist-esm/index.d.mts +114 -0
- package/dist-esm/index.mjs +14 -0
- package/dist-esm/index.mjs.map +7 -0
- package/dist-esm/renderBlueprint.mjs +298 -0
- package/dist-esm/renderBlueprint.mjs.map +7 -0
- package/dist-esm/sequenceDiagram.mjs +666 -0
- package/dist-esm/sequenceDiagram.mjs.map +7 -0
- package/dist-esm/stateDiagram.mjs +359 -0
- package/dist-esm/stateDiagram.mjs.map +7 -0
- package/dist-esm/svgParsing.mjs +167 -0
- package/dist-esm/svgParsing.mjs.map +7 -0
- package/dist-esm/utils.mjs +55 -0
- package/dist-esm/utils.mjs.map +7 -0
- package/package.json +62 -0
- package/src/blueprint.ts +75 -0
- package/src/colors.ts +215 -0
- package/src/createMermaidDiagram.test.ts +31 -0
- package/src/createMermaidDiagram.ts +155 -0
- package/src/flowchartDiagram.ts +232 -0
- package/src/index.ts +18 -0
- package/src/mermaidDiagrams.test.ts +880 -0
- package/src/renderBlueprint.ts +373 -0
- package/src/sequenceDiagram.ts +851 -0
- package/src/stateDiagram.ts +477 -0
- package/src/svgParsing.ts +240 -0
- package/src/utils.ts +73 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
import type { SequenceDB } from 'mermaid/dist/diagrams/sequence/sequenceDb.d.ts'
|
|
2
|
+
import type { Actor, Message } from 'mermaid/dist/diagrams/sequence/types.js'
|
|
3
|
+
import { TLArrowShapeArrowheadStyle, TLDefaultDashStyle, TLGeoShape } from 'tldraw'
|
|
4
|
+
import type {
|
|
5
|
+
DiagramMermaidBlueprint,
|
|
6
|
+
MermaidBlueprintEdge,
|
|
7
|
+
MermaidBlueprintGeoNode,
|
|
8
|
+
MermaidBlueprintLineNode,
|
|
9
|
+
} from './blueprint'
|
|
10
|
+
import { parseRgbToTldrawColor } from './colors'
|
|
11
|
+
import { getAccumulatedTranslate } from './svgParsing'
|
|
12
|
+
|
|
13
|
+
export interface SvgRect {
|
|
14
|
+
x: number
|
|
15
|
+
y: number
|
|
16
|
+
w: number
|
|
17
|
+
h: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ActorLayout {
|
|
21
|
+
x: number
|
|
22
|
+
y: number
|
|
23
|
+
w: number
|
|
24
|
+
h: number
|
|
25
|
+
bottomY: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ParsedSequenceLayout {
|
|
29
|
+
actorLayouts: ActorLayout[]
|
|
30
|
+
noteRects: SvgRect[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const LINETYPE = {
|
|
34
|
+
SOLID: 0,
|
|
35
|
+
DOTTED: 1,
|
|
36
|
+
NOTE: 2,
|
|
37
|
+
SOLID_CROSS: 3,
|
|
38
|
+
DOTTED_CROSS: 4,
|
|
39
|
+
SOLID_OPEN: 5,
|
|
40
|
+
DOTTED_OPEN: 6,
|
|
41
|
+
LOOP_START: 10,
|
|
42
|
+
LOOP_END: 11,
|
|
43
|
+
ALT_START: 12,
|
|
44
|
+
ALT_ELSE: 13,
|
|
45
|
+
ALT_END: 14,
|
|
46
|
+
OPT_START: 15,
|
|
47
|
+
OPT_END: 16,
|
|
48
|
+
ACTIVE_START: 17,
|
|
49
|
+
ACTIVE_END: 18,
|
|
50
|
+
PAR_START: 19,
|
|
51
|
+
PAR_AND: 20,
|
|
52
|
+
PAR_END: 21,
|
|
53
|
+
RECT_START: 22,
|
|
54
|
+
RECT_END: 23,
|
|
55
|
+
SOLID_POINT: 24,
|
|
56
|
+
DOTTED_POINT: 25,
|
|
57
|
+
AUTONUMBER: 26,
|
|
58
|
+
CRITICAL_START: 27,
|
|
59
|
+
CRITICAL_OPTION: 28,
|
|
60
|
+
CRITICAL_END: 29,
|
|
61
|
+
BREAK_START: 30,
|
|
62
|
+
BREAK_END: 31,
|
|
63
|
+
PAR_OVER_START: 32,
|
|
64
|
+
BIDIRECTIONAL_SOLID: 33,
|
|
65
|
+
BIDIRECTIONAL_DOTTED: 34,
|
|
66
|
+
} as const satisfies SequenceDB['LINETYPE']
|
|
67
|
+
|
|
68
|
+
const PLACEMENT = {
|
|
69
|
+
LEFTOF: 0,
|
|
70
|
+
RIGHTOF: 1,
|
|
71
|
+
OVER: 2,
|
|
72
|
+
} as const satisfies SequenceDB['PLACEMENT']
|
|
73
|
+
|
|
74
|
+
const signalTypes: number[] = [
|
|
75
|
+
LINETYPE.SOLID,
|
|
76
|
+
LINETYPE.DOTTED,
|
|
77
|
+
LINETYPE.SOLID_CROSS,
|
|
78
|
+
LINETYPE.DOTTED_CROSS,
|
|
79
|
+
LINETYPE.SOLID_OPEN,
|
|
80
|
+
LINETYPE.DOTTED_OPEN,
|
|
81
|
+
LINETYPE.SOLID_POINT,
|
|
82
|
+
LINETYPE.DOTTED_POINT,
|
|
83
|
+
LINETYPE.BIDIRECTIONAL_SOLID,
|
|
84
|
+
LINETYPE.BIDIRECTIONAL_DOTTED,
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
function isSignalMessage(type: number | undefined): boolean {
|
|
88
|
+
if (type === undefined) return false
|
|
89
|
+
return signalTypes.includes(type)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isNoteMessage(type: number | undefined): boolean {
|
|
93
|
+
return type === LINETYPE.NOTE
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isActiveStart(type: number | undefined): boolean {
|
|
97
|
+
return type === LINETYPE.ACTIVE_START
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isActiveEnd(type: number | undefined): boolean {
|
|
101
|
+
return type === LINETYPE.ACTIVE_END
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isAutonumber(type: number | undefined): boolean {
|
|
105
|
+
return type === LINETYPE.AUTONUMBER
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Returns the fragment keyword (e.g. "loop", "opt") if this message starts a combined fragment, or null. */
|
|
109
|
+
function getFragmentStartKeyword(type: number | undefined): string | null {
|
|
110
|
+
if (type === undefined) return null
|
|
111
|
+
if (type === LINETYPE.LOOP_START) return 'loop'
|
|
112
|
+
if (type === LINETYPE.ALT_START) return 'alt'
|
|
113
|
+
if (type === LINETYPE.OPT_START) return 'opt'
|
|
114
|
+
if (type === LINETYPE.PAR_START) return 'par'
|
|
115
|
+
if (type === LINETYPE.RECT_START) return 'rect'
|
|
116
|
+
if (type === LINETYPE.CRITICAL_START) return 'critical'
|
|
117
|
+
if (type === LINETYPE.BREAK_START) return 'break'
|
|
118
|
+
if (type === LINETYPE.PAR_OVER_START) return 'par'
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isFragmentEnd(type: number | undefined): boolean {
|
|
123
|
+
if (type === undefined) return false
|
|
124
|
+
const endTypes: number[] = [
|
|
125
|
+
LINETYPE.LOOP_END,
|
|
126
|
+
LINETYPE.ALT_END,
|
|
127
|
+
LINETYPE.OPT_END,
|
|
128
|
+
LINETYPE.PAR_END,
|
|
129
|
+
LINETYPE.RECT_END,
|
|
130
|
+
LINETYPE.CRITICAL_END,
|
|
131
|
+
LINETYPE.BREAK_END,
|
|
132
|
+
]
|
|
133
|
+
return endTypes.includes(type)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Returns a keyword if this message is a section separator within a combined fragment, or null. */
|
|
137
|
+
function getFragmentSeparatorKeyword(type: number | undefined): string | null {
|
|
138
|
+
if (type === LINETYPE.ALT_ELSE) return 'else'
|
|
139
|
+
if (type === LINETYPE.PAR_AND) return 'and'
|
|
140
|
+
if (type === LINETYPE.CRITICAL_OPTION) return 'option'
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function mapParticipantTypeToGeo(type: string): TLGeoShape['props']['geo'] {
|
|
145
|
+
switch (type) {
|
|
146
|
+
case 'actor':
|
|
147
|
+
return 'ellipse'
|
|
148
|
+
case 'database':
|
|
149
|
+
return 'oval'
|
|
150
|
+
default:
|
|
151
|
+
return 'rectangle'
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Map a Mermaid LINETYPE value to tldraw arrow props. */
|
|
156
|
+
function mapLineTypeToArrowProps(type: number): {
|
|
157
|
+
dash: TLDefaultDashStyle
|
|
158
|
+
arrowheadEnd: TLArrowShapeArrowheadStyle
|
|
159
|
+
} {
|
|
160
|
+
switch (type) {
|
|
161
|
+
case LINETYPE.SOLID:
|
|
162
|
+
return { dash: 'solid', arrowheadEnd: 'arrow' }
|
|
163
|
+
case LINETYPE.DOTTED:
|
|
164
|
+
return { dash: 'dotted', arrowheadEnd: 'arrow' }
|
|
165
|
+
case LINETYPE.SOLID_CROSS:
|
|
166
|
+
return { dash: 'solid', arrowheadEnd: 'bar' }
|
|
167
|
+
case LINETYPE.DOTTED_CROSS:
|
|
168
|
+
return { dash: 'dotted', arrowheadEnd: 'bar' }
|
|
169
|
+
case LINETYPE.SOLID_OPEN:
|
|
170
|
+
return { dash: 'solid', arrowheadEnd: 'none' }
|
|
171
|
+
case LINETYPE.DOTTED_OPEN:
|
|
172
|
+
return { dash: 'dotted', arrowheadEnd: 'none' }
|
|
173
|
+
case LINETYPE.SOLID_POINT:
|
|
174
|
+
return { dash: 'solid', arrowheadEnd: 'arrow' }
|
|
175
|
+
case LINETYPE.DOTTED_POINT:
|
|
176
|
+
return { dash: 'dotted', arrowheadEnd: 'arrow' }
|
|
177
|
+
case LINETYPE.BIDIRECTIONAL_SOLID:
|
|
178
|
+
return { dash: 'solid', arrowheadEnd: 'arrow' }
|
|
179
|
+
case LINETYPE.BIDIRECTIONAL_DOTTED:
|
|
180
|
+
return { dash: 'dotted', arrowheadEnd: 'arrow' }
|
|
181
|
+
default:
|
|
182
|
+
return { dash: 'solid', arrowheadEnd: 'arrow' }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isBidirectional(type: number): boolean {
|
|
187
|
+
return type === LINETYPE.BIDIRECTIONAL_SOLID || type === LINETYPE.BIDIRECTIONAL_DOTTED
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const TARGET_ACTOR_SPACING = 300
|
|
191
|
+
const MIN_VERTICAL_GAP = 400
|
|
192
|
+
const FALLBACK_ACTOR_WIDTH = 200
|
|
193
|
+
const FALLBACK_ACTOR_HEIGHT = 70
|
|
194
|
+
const FALLBACK_ACTOR_SPACING = 100
|
|
195
|
+
const FALLBACK_EVENT_SPACING = 80
|
|
196
|
+
const FALLBACK_NOTE_WIDTH = 120
|
|
197
|
+
const FALLBACK_NOTE_HEIGHT = 50
|
|
198
|
+
const NOTE_PADDING = 5
|
|
199
|
+
// tldraw's hand-drawn font is wider than Mermaid's default, so we estimate
|
|
200
|
+
// the minimum note width from the label text to prevent wrapping.
|
|
201
|
+
const NOTE_CHAR_WIDTH = 11
|
|
202
|
+
const NOTE_TEXT_PADDING = 40
|
|
203
|
+
const FRAGMENT_PADDING_X = 30
|
|
204
|
+
const FRAGMENT_PADDING_TOP = 50
|
|
205
|
+
const FRAGMENT_PADDING_BOTTOM = 25
|
|
206
|
+
const ACTOR_PADDING_WIDTH = 30
|
|
207
|
+
const ACTOR_PADDING_HEIGHT = 10
|
|
208
|
+
const SELF_MSG_Y_OFFSET = 0.04
|
|
209
|
+
const SELF_MSG_BEND = -80
|
|
210
|
+
const ACTIVATION_BOX_WIDTH = 20
|
|
211
|
+
const ACTIVATION_NEST_OFFSET = 6
|
|
212
|
+
const ACTIVATION_PAD_RATIO = 0.15
|
|
213
|
+
const FRAGMENT_SECTION_LABEL_HEIGHT = 25
|
|
214
|
+
const FRAGMENT_SECTION_LABEL_PADDING = 5
|
|
215
|
+
|
|
216
|
+
interface FragmentSection {
|
|
217
|
+
title: string
|
|
218
|
+
firstEventIndex: number
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface OpenFragment {
|
|
222
|
+
keyword: string
|
|
223
|
+
sections: FragmentSection[]
|
|
224
|
+
firstEventIndex: number
|
|
225
|
+
actorKeys: Set<string>
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface FragmentSpan extends OpenFragment {
|
|
229
|
+
lastEventIndex: number
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
interface ActivationSpan {
|
|
233
|
+
participantKey: string
|
|
234
|
+
startEventIndex: number
|
|
235
|
+
endEventIndex: number
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function parseSvgRects(root: Element, selector: string): SvgRect[] {
|
|
239
|
+
const results: SvgRect[] = []
|
|
240
|
+
for (const rect of root.querySelectorAll(selector)) {
|
|
241
|
+
const ancestor = getAccumulatedTranslate(rect)
|
|
242
|
+
const x = parseFloat(rect.getAttribute('x') || '0')
|
|
243
|
+
const y = parseFloat(rect.getAttribute('y') || '0')
|
|
244
|
+
const w = parseFloat(rect.getAttribute('width') || '0')
|
|
245
|
+
const h = parseFloat(rect.getAttribute('height') || '0')
|
|
246
|
+
if (w > 0 && h > 0) {
|
|
247
|
+
results.push({
|
|
248
|
+
x: Number.isFinite(ancestor.x + x) ? ancestor.x + x : 0,
|
|
249
|
+
y: Number.isFinite(ancestor.y + y) ? ancestor.y + y : 0,
|
|
250
|
+
w,
|
|
251
|
+
h,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return results
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Stick-figure (actor) participants use SVG <line> elements instead of <rect>.
|
|
260
|
+
* We derive bounding rectangles from the min/max coordinates of each actor-man group.
|
|
261
|
+
*/
|
|
262
|
+
function parseActorManRects(root: Element): SvgRect[] {
|
|
263
|
+
const results: SvgRect[] = []
|
|
264
|
+
for (const group of root.querySelectorAll('g.actor-man')) {
|
|
265
|
+
const ancestor = getAccumulatedTranslate(group)
|
|
266
|
+
let minX = Infinity
|
|
267
|
+
let minY = Infinity
|
|
268
|
+
let maxX = -Infinity
|
|
269
|
+
let maxY = -Infinity
|
|
270
|
+
for (const line of group.querySelectorAll('line')) {
|
|
271
|
+
for (const attr of ['x1', 'x2']) {
|
|
272
|
+
const coord = parseFloat(line.getAttribute(attr) || '0')
|
|
273
|
+
if (coord < minX) minX = coord
|
|
274
|
+
if (coord > maxX) maxX = coord
|
|
275
|
+
}
|
|
276
|
+
for (const attr of ['y1', 'y2']) {
|
|
277
|
+
const coord = parseFloat(line.getAttribute(attr) || '0')
|
|
278
|
+
if (coord < minY) minY = coord
|
|
279
|
+
if (coord > maxY) maxY = coord
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (Number.isFinite(minX) && Number.isFinite(minY)) {
|
|
283
|
+
results.push({
|
|
284
|
+
x: ancestor.x + minX,
|
|
285
|
+
y: ancestor.y + minY,
|
|
286
|
+
w: maxX - minX || FALLBACK_ACTOR_WIDTH,
|
|
287
|
+
h: maxY - minY || FALLBACK_ACTOR_HEIGHT,
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return results
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function computeActorLayouts(root: Element, actorCount: number, eventCount: number): ActorLayout[] {
|
|
295
|
+
const byX = (a: SvgRect, b: SvgRect) => a.x - b.x
|
|
296
|
+
let top = parseSvgRects(root, 'rect.actor-top').sort(byX)
|
|
297
|
+
let bottom = parseSvgRects(root, 'rect.actor-bottom').sort(byX)
|
|
298
|
+
|
|
299
|
+
const actorManRects = parseActorManRects(root).sort(byX)
|
|
300
|
+
if (actorManRects.length > 0 && (top.length < actorCount || bottom.length < actorCount)) {
|
|
301
|
+
const midY =
|
|
302
|
+
top.length > 0 && bottom.length > 0
|
|
303
|
+
? (Math.max(...top.map((r) => r.y + r.h)) + Math.min(...bottom.map((r) => r.y))) / 2
|
|
304
|
+
: actorManRects.length >= 2
|
|
305
|
+
? (actorManRects[0].y + actorManRects[actorManRects.length - 1].y) / 2
|
|
306
|
+
: 0
|
|
307
|
+
for (const rect of actorManRects) {
|
|
308
|
+
if (rect.y < midY) top.push(rect)
|
|
309
|
+
else bottom.push(rect)
|
|
310
|
+
}
|
|
311
|
+
top.sort(byX)
|
|
312
|
+
bottom.sort(byX)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (top.length < actorCount || bottom.length < actorCount) {
|
|
316
|
+
const all = parseSvgRects(root, 'rect[class*="actor"]').sort((a, b) => a.y - b.y)
|
|
317
|
+
if (all.length >= 2 * actorCount) {
|
|
318
|
+
let maxGap = 0
|
|
319
|
+
let splitAt = actorCount
|
|
320
|
+
for (let i = 1; i < all.length; i++) {
|
|
321
|
+
const gap = all[i].y - all[i - 1].y
|
|
322
|
+
if (gap > maxGap) {
|
|
323
|
+
maxGap = gap
|
|
324
|
+
splitAt = i
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
top = all.slice(0, splitAt).sort(byX).slice(0, actorCount)
|
|
328
|
+
bottom = all.slice(splitAt).sort(byX).slice(0, actorCount)
|
|
329
|
+
} else {
|
|
330
|
+
top = []
|
|
331
|
+
bottom = []
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
top = top.slice(0, actorCount)
|
|
335
|
+
bottom = bottom.slice(0, actorCount)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (actorManRects.length > 0) {
|
|
339
|
+
const actorManSet = new Set<SvgRect>(actorManRects)
|
|
340
|
+
const regularTops = top.filter((r) => !actorManSet.has(r))
|
|
341
|
+
const regularBottoms = bottom.filter((r) => !actorManSet.has(r))
|
|
342
|
+
const refHeight =
|
|
343
|
+
regularTops.length > 0 ? Math.max(...regularTops.map((r) => r.h)) : FALLBACK_ACTOR_HEIGHT
|
|
344
|
+
const refWidth =
|
|
345
|
+
regularTops.length > 0 ? Math.max(...regularTops.map((r) => r.w)) : FALLBACK_ACTOR_WIDTH
|
|
346
|
+
const refTopY = regularTops.length > 0 ? Math.min(...regularTops.map((r) => r.y)) : undefined
|
|
347
|
+
const refBottomEndY =
|
|
348
|
+
regularBottoms.length > 0 ? Math.max(...regularBottoms.map((r) => r.y + r.h)) : undefined
|
|
349
|
+
|
|
350
|
+
for (const rect of top) {
|
|
351
|
+
if (!actorManSet.has(rect)) continue
|
|
352
|
+
const centerY = rect.y + rect.h / 2
|
|
353
|
+
rect.h = refHeight
|
|
354
|
+
rect.w = Math.max(rect.w, refWidth)
|
|
355
|
+
rect.y = refTopY !== undefined ? refTopY : centerY - refHeight / 2
|
|
356
|
+
}
|
|
357
|
+
for (const rect of bottom) {
|
|
358
|
+
if (!actorManSet.has(rect)) continue
|
|
359
|
+
const centerY = rect.y + rect.h / 2
|
|
360
|
+
rect.h = refHeight
|
|
361
|
+
rect.w = Math.max(rect.w, refWidth)
|
|
362
|
+
rect.y = refBottomEndY !== undefined ? refBottomEndY - refHeight : centerY - refHeight / 2
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (top.length >= actorCount && bottom.length >= actorCount) {
|
|
367
|
+
const svgCenters = top.map((r) => r.x + r.w / 2)
|
|
368
|
+
const spacings: number[] = []
|
|
369
|
+
for (let i = 1; i < svgCenters.length; i++) {
|
|
370
|
+
spacings.push(svgCenters[i] - svgCenters[i - 1])
|
|
371
|
+
}
|
|
372
|
+
const minSpacing = spacings.length > 0 ? Math.min(...spacings) : 1
|
|
373
|
+
const scale = Math.max(1, TARGET_ACTOR_SPACING / minSpacing)
|
|
374
|
+
|
|
375
|
+
const xCenters = svgCenters.map((c) => (c - svgCenters[0]) * scale)
|
|
376
|
+
const totalSpan = xCenters.length > 1 ? xCenters[xCenters.length - 1] : 0
|
|
377
|
+
|
|
378
|
+
const topRowBottom = Math.max(...top.map((r) => r.y + r.h))
|
|
379
|
+
const bottomRowTop = Math.min(...bottom.map((r) => r.y))
|
|
380
|
+
const yStretch = Math.max(0, MIN_VERTICAL_GAP - (bottomRowTop - topRowBottom))
|
|
381
|
+
const topMinY = Math.min(...top.map((r) => r.y))
|
|
382
|
+
const bottomMaxY = Math.max(...bottom.map((r) => r.y + r.h))
|
|
383
|
+
const originY = -(bottomMaxY + yStretch + topMinY) / 2
|
|
384
|
+
|
|
385
|
+
return top.map((topRect, i) => {
|
|
386
|
+
const w = topRect.w + ACTOR_PADDING_WIDTH
|
|
387
|
+
const h = topRect.h + ACTOR_PADDING_HEIGHT
|
|
388
|
+
return {
|
|
389
|
+
x: xCenters[i] - totalSpan / 2 - w / 2,
|
|
390
|
+
y: originY + topRect.y,
|
|
391
|
+
w,
|
|
392
|
+
h,
|
|
393
|
+
bottomY: originY + bottom[i].y + yStretch,
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const fallbackLifelineHeight = Math.max(300, eventCount * FALLBACK_EVENT_SPACING)
|
|
399
|
+
const totalWidth = actorCount * FALLBACK_ACTOR_WIDTH + (actorCount - 1) * FALLBACK_ACTOR_SPACING
|
|
400
|
+
const totalHeight = FALLBACK_ACTOR_HEIGHT * 2 + fallbackLifelineHeight
|
|
401
|
+
const startX = -totalWidth / 2
|
|
402
|
+
const startY = -totalHeight / 2
|
|
403
|
+
return Array.from({ length: actorCount }, (_, i) => ({
|
|
404
|
+
x: startX + i * (FALLBACK_ACTOR_WIDTH + FALLBACK_ACTOR_SPACING),
|
|
405
|
+
y: startY,
|
|
406
|
+
w: FALLBACK_ACTOR_WIDTH,
|
|
407
|
+
h: FALLBACK_ACTOR_HEIGHT,
|
|
408
|
+
bottomY: startY + totalHeight - FALLBACK_ACTOR_HEIGHT,
|
|
409
|
+
}))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function getMessageLabel(msg: Message): string | undefined {
|
|
413
|
+
return typeof msg.message === 'string' ? msg.message : undefined
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Count how many renderable events (signals + notes) a message list contains. */
|
|
417
|
+
export function countSequenceEvents(messages: Message[]): number {
|
|
418
|
+
let count = 0
|
|
419
|
+
for (const msg of messages) {
|
|
420
|
+
if (isAutonumber(msg.type)) continue
|
|
421
|
+
if (getFragmentStartKeyword(msg.type)) continue
|
|
422
|
+
if (isFragmentEnd(msg.type)) continue
|
|
423
|
+
if (getFragmentSeparatorKeyword(msg.type)) continue
|
|
424
|
+
if (isActiveStart(msg.type) || isActiveEnd(msg.type)) continue
|
|
425
|
+
const isEvent =
|
|
426
|
+
(isSignalMessage(msg.type) && msg.from && msg.to) || (isNoteMessage(msg.type) && msg.from)
|
|
427
|
+
if (isEvent) count++
|
|
428
|
+
}
|
|
429
|
+
return count
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Parse sequence-diagram SVG layout data for use by {@link sequenceToBlueprint}. */
|
|
433
|
+
export function parseSequenceLayout(
|
|
434
|
+
root: Element,
|
|
435
|
+
actorCount: number,
|
|
436
|
+
eventCount: number
|
|
437
|
+
): ParsedSequenceLayout {
|
|
438
|
+
return {
|
|
439
|
+
actorLayouts: computeActorLayouts(root, actorCount, eventCount),
|
|
440
|
+
noteRects: parseSvgRects(root, 'rect.note'),
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Build a complete blueprint for a sequence diagram:
|
|
446
|
+
* top actors, lifelines, bottom actors, fragments, notes, and signal edges.
|
|
447
|
+
*/
|
|
448
|
+
export function sequenceToBlueprint(
|
|
449
|
+
layout: ParsedSequenceLayout,
|
|
450
|
+
actors: Map<string, Actor>,
|
|
451
|
+
actorKeys: string[],
|
|
452
|
+
messages: Message[],
|
|
453
|
+
createdActors: Map<string, number> = new Map(),
|
|
454
|
+
destroyedActors: Map<string, number> = new Map()
|
|
455
|
+
): DiagramMermaidBlueprint {
|
|
456
|
+
const actorCount = actorKeys.length
|
|
457
|
+
const keyIndex = new Map(actorKeys.map((key, i) => [key, i]))
|
|
458
|
+
|
|
459
|
+
const fragments: FragmentSpan[] = []
|
|
460
|
+
const fragmentStack: OpenFragment[] = []
|
|
461
|
+
const events: Message[] = []
|
|
462
|
+
const activationStack = new Map<string, number[]>()
|
|
463
|
+
const activationSpans: ActivationSpan[] = []
|
|
464
|
+
|
|
465
|
+
let autonumberStart = 0
|
|
466
|
+
let autonumberStep = 0
|
|
467
|
+
let autonumberVisible = false
|
|
468
|
+
|
|
469
|
+
for (const msg of messages) {
|
|
470
|
+
if (isAutonumber(msg.type)) {
|
|
471
|
+
autonumberStart = 1
|
|
472
|
+
autonumberStep = 1
|
|
473
|
+
autonumberVisible = true
|
|
474
|
+
continue
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const keyword = getFragmentStartKeyword(msg.type)
|
|
478
|
+
if (keyword) {
|
|
479
|
+
fragmentStack.push({
|
|
480
|
+
keyword,
|
|
481
|
+
sections: [{ title: getMessageLabel(msg) ?? '', firstEventIndex: events.length }],
|
|
482
|
+
firstEventIndex: events.length,
|
|
483
|
+
actorKeys: new Set(),
|
|
484
|
+
})
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (isFragmentEnd(msg.type)) {
|
|
489
|
+
const frag = fragmentStack.pop()
|
|
490
|
+
if (frag) fragments.push({ ...frag, lastEventIndex: events.length - 1 })
|
|
491
|
+
continue
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (getFragmentSeparatorKeyword(msg.type)) {
|
|
495
|
+
const current = fragmentStack[fragmentStack.length - 1]
|
|
496
|
+
if (current) {
|
|
497
|
+
current.sections.push({
|
|
498
|
+
title: getMessageLabel(msg) ?? '',
|
|
499
|
+
firstEventIndex: events.length,
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
continue
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (isActiveStart(msg.type)) {
|
|
506
|
+
const key = msg.from ?? msg.to
|
|
507
|
+
if (key) {
|
|
508
|
+
if (!activationStack.has(key)) activationStack.set(key, [])
|
|
509
|
+
// Explicit `activate` follows the triggering arrow,
|
|
510
|
+
// so the activation starts at the previous event.
|
|
511
|
+
activationStack.get(key)!.push(Math.max(0, events.length - 1))
|
|
512
|
+
}
|
|
513
|
+
continue
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (isActiveEnd(msg.type)) {
|
|
517
|
+
const key = msg.from ?? msg.to
|
|
518
|
+
if (key) {
|
|
519
|
+
const startIdx = activationStack.get(key)?.pop()
|
|
520
|
+
if (startIdx !== undefined) {
|
|
521
|
+
activationSpans.push({
|
|
522
|
+
participantKey: key,
|
|
523
|
+
startEventIndex: startIdx,
|
|
524
|
+
endEventIndex: Math.max(events.length - 1, startIdx),
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const isEvent =
|
|
532
|
+
(isSignalMessage(msg.type) && msg.from && msg.to) || (isNoteMessage(msg.type) && msg.from)
|
|
533
|
+
if (!isEvent) continue
|
|
534
|
+
|
|
535
|
+
for (const frag of fragmentStack) {
|
|
536
|
+
if (msg.from) frag.actorKeys.add(msg.from)
|
|
537
|
+
if (msg.to) frag.actorKeys.add(msg.to)
|
|
538
|
+
}
|
|
539
|
+
events.push(msg)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const layouts = layout.actorLayouts
|
|
543
|
+
|
|
544
|
+
// Pre-compute lifecycle event indices for created/destroyed actors.
|
|
545
|
+
// We scan events for the first signal targeting each created actor
|
|
546
|
+
// and the first signal from each destroyed actor.
|
|
547
|
+
const creationEventIndex = new Map<string, number>()
|
|
548
|
+
const destructionEventIndex = new Map<string, number>()
|
|
549
|
+
for (let i = 0; i < events.length; i++) {
|
|
550
|
+
const ev = events[i]
|
|
551
|
+
if (!isSignalMessage(ev.type)) continue
|
|
552
|
+
|
|
553
|
+
if (ev.to && createdActors.has(ev.to) && !creationEventIndex.has(ev.to)) {
|
|
554
|
+
creationEventIndex.set(ev.to, i)
|
|
555
|
+
}
|
|
556
|
+
if (ev.from && destroyedActors.has(ev.from) && !destructionEventIndex.has(ev.from)) {
|
|
557
|
+
destructionEventIndex.set(ev.from, i)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Build blueprint
|
|
562
|
+
const svgNoteRects = layout.noteRects
|
|
563
|
+
let svgNoteIndex = 0
|
|
564
|
+
const nodes: MermaidBlueprintGeoNode[] = []
|
|
565
|
+
const lines: MermaidBlueprintLineNode[] = []
|
|
566
|
+
const edges: MermaidBlueprintEdge[] = []
|
|
567
|
+
|
|
568
|
+
const { y: firstY, h: firstH, bottomY: firstBottomY } = layouts[0]
|
|
569
|
+
const lifelineTop = firstY + firstH
|
|
570
|
+
const eventStep = (firstBottomY - lifelineTop) / (events.length + 1)
|
|
571
|
+
|
|
572
|
+
// --- Z-order: lifelines -> activations -> fragments -> actor boxes -> notes/arrows ---
|
|
573
|
+
|
|
574
|
+
// 1. Lifelines (behind everything)
|
|
575
|
+
for (let i = 0; i < actorCount; i++) {
|
|
576
|
+
const key = actorKeys[i]
|
|
577
|
+
const { x, y, w, h, bottomY } = layouts[i]
|
|
578
|
+
|
|
579
|
+
const isCreated = creationEventIndex.has(key)
|
|
580
|
+
const isDestroyed = destructionEventIndex.has(key)
|
|
581
|
+
const eventY = isCreated ? lifelineTop + eventStep * (creationEventIndex.get(key)! + 1) : 0
|
|
582
|
+
const topY = isCreated ? eventY + h / 2 : y + h
|
|
583
|
+
const botY = isDestroyed
|
|
584
|
+
? lifelineTop + eventStep * (destructionEventIndex.get(key)! + 1)
|
|
585
|
+
: bottomY
|
|
586
|
+
|
|
587
|
+
const lifelineHeight = botY - topY
|
|
588
|
+
if (lifelineHeight > 0) {
|
|
589
|
+
lines.push({ id: `lifeline-${key}`, x: x + w / 2, y: topY, endY: lifelineHeight })
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 2. Activation boxes (just after lifelines)
|
|
594
|
+
const activationPad = eventStep * ACTIVATION_PAD_RATIO
|
|
595
|
+
const sortedSpans = activationSpans
|
|
596
|
+
.map((span, origIdx) => ({ ...span, origIdx }))
|
|
597
|
+
.sort((a, b) => {
|
|
598
|
+
const sizeA = a.endEventIndex - a.startEventIndex
|
|
599
|
+
const sizeB = b.endEventIndex - b.startEventIndex
|
|
600
|
+
return sizeB - sizeA || a.origIdx - b.origIdx
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
for (let i = 0; i < sortedSpans.length; i++) {
|
|
604
|
+
const span = sortedSpans[i]
|
|
605
|
+
const actorIdx = keyIndex.get(span.participantKey)
|
|
606
|
+
if (actorIdx === undefined) continue
|
|
607
|
+
|
|
608
|
+
const spanSize = span.endEventIndex - span.startEventIndex
|
|
609
|
+
let depth = 0
|
|
610
|
+
for (const other of sortedSpans) {
|
|
611
|
+
if (other === span) continue
|
|
612
|
+
const sameParticipant = other.participantKey === span.participantKey
|
|
613
|
+
const containsSpan =
|
|
614
|
+
other.startEventIndex <= span.startEventIndex && other.endEventIndex >= span.endEventIndex
|
|
615
|
+
const strictlyLarger = other.endEventIndex - other.startEventIndex > spanSize
|
|
616
|
+
if (sameParticipant && containsSpan && strictlyLarger) depth++
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const layout = layouts[actorIdx]
|
|
620
|
+
const lifelineCenterX = layout.x + layout.w / 2
|
|
621
|
+
const boxTop = lifelineTop + eventStep * (span.startEventIndex + 1) - activationPad
|
|
622
|
+
const boxBottom = lifelineTop + eventStep * (span.endEventIndex + 1) + activationPad
|
|
623
|
+
|
|
624
|
+
nodes.push({
|
|
625
|
+
id: `activation-${span.origIdx}`,
|
|
626
|
+
x: lifelineCenterX - ACTIVATION_BOX_WIDTH / 2 + depth * ACTIVATION_NEST_OFFSET,
|
|
627
|
+
y: boxTop,
|
|
628
|
+
w: ACTIVATION_BOX_WIDTH,
|
|
629
|
+
h: boxBottom - boxTop,
|
|
630
|
+
geo: 'rectangle',
|
|
631
|
+
fill: 'solid',
|
|
632
|
+
color: 'light-violet',
|
|
633
|
+
size: 's',
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// 3. Fragments
|
|
638
|
+
for (let fragmentIndex = 0; fragmentIndex < fragments.length; fragmentIndex++) {
|
|
639
|
+
const fragment = fragments[fragmentIndex]
|
|
640
|
+
if (fragment.lastEventIndex < fragment.firstEventIndex) continue
|
|
641
|
+
|
|
642
|
+
const fragTop = lifelineTop + eventStep * (fragment.firstEventIndex + 1) - FRAGMENT_PADDING_TOP
|
|
643
|
+
const fragBottom =
|
|
644
|
+
lifelineTop + eventStep * (fragment.lastEventIndex + 1) + FRAGMENT_PADDING_BOTTOM
|
|
645
|
+
const indices = [...fragment.actorKeys].map((k) => keyIndex.get(k)!).filter((idx) => idx >= 0)
|
|
646
|
+
if (indices.length === 0) continue
|
|
647
|
+
|
|
648
|
+
const minIndex = Math.min(...indices)
|
|
649
|
+
const maxIndex = Math.max(...indices)
|
|
650
|
+
const leftX = layouts[minIndex].x - FRAGMENT_PADDING_X
|
|
651
|
+
const fragW = layouts[maxIndex].x + layouts[maxIndex].w + FRAGMENT_PADDING_X - leftX
|
|
652
|
+
const fragH = fragBottom - fragTop
|
|
653
|
+
|
|
654
|
+
const rgbColor =
|
|
655
|
+
fragment.keyword === 'rect' ? parseRgbToTldrawColor(fragment.sections[0].title) : null
|
|
656
|
+
if (rgbColor) {
|
|
657
|
+
nodes.push({
|
|
658
|
+
id: `fragment-${fragmentIndex}`,
|
|
659
|
+
x: leftX,
|
|
660
|
+
y: fragTop,
|
|
661
|
+
w: fragW,
|
|
662
|
+
h: fragH,
|
|
663
|
+
geo: 'rectangle',
|
|
664
|
+
fill: rgbColor.hasAlpha ? 'semi' : 'solid',
|
|
665
|
+
color: rgbColor.color,
|
|
666
|
+
size: 's',
|
|
667
|
+
})
|
|
668
|
+
} else {
|
|
669
|
+
nodes.push({
|
|
670
|
+
id: `fragment-${fragmentIndex}`,
|
|
671
|
+
x: leftX,
|
|
672
|
+
y: fragTop,
|
|
673
|
+
w: fragW,
|
|
674
|
+
h: fragH,
|
|
675
|
+
geo: 'rectangle',
|
|
676
|
+
dash: 'dashed',
|
|
677
|
+
fill: 'none',
|
|
678
|
+
color: 'light-blue',
|
|
679
|
+
size: 's',
|
|
680
|
+
align: 'start',
|
|
681
|
+
verticalAlign: 'start',
|
|
682
|
+
label: `${fragment.keyword} [${fragment.sections[0].title}]`,
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
for (let s = 1; s < fragment.sections.length; s++) {
|
|
686
|
+
const section = fragment.sections[s]
|
|
687
|
+
const sepY = lifelineTop + eventStep * (section.firstEventIndex + 0.5)
|
|
688
|
+
|
|
689
|
+
lines.push({
|
|
690
|
+
id: `fragment-${fragmentIndex}-sep-${s}`,
|
|
691
|
+
x: leftX,
|
|
692
|
+
y: sepY,
|
|
693
|
+
endX: fragW,
|
|
694
|
+
endY: 0,
|
|
695
|
+
dash: 'dashed',
|
|
696
|
+
color: 'light-blue',
|
|
697
|
+
size: 's',
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
nodes.push({
|
|
701
|
+
id: `fragment-${fragmentIndex}-section-${s}`,
|
|
702
|
+
x: leftX + FRAGMENT_SECTION_LABEL_PADDING,
|
|
703
|
+
y: sepY + FRAGMENT_SECTION_LABEL_PADDING,
|
|
704
|
+
w: fragW - FRAGMENT_SECTION_LABEL_PADDING * 2,
|
|
705
|
+
h: FRAGMENT_SECTION_LABEL_HEIGHT,
|
|
706
|
+
geo: 'rectangle',
|
|
707
|
+
fill: 'none',
|
|
708
|
+
dash: 'dashed',
|
|
709
|
+
color: 'light-blue',
|
|
710
|
+
size: 's',
|
|
711
|
+
align: 'start',
|
|
712
|
+
verticalAlign: 'start',
|
|
713
|
+
label: `[${section.title}]`,
|
|
714
|
+
})
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// 4. Actor boxes (top and bottom)
|
|
720
|
+
for (let i = 0; i < actorCount; i++) {
|
|
721
|
+
const key = actorKeys[i]
|
|
722
|
+
const actor = actors.get(key)
|
|
723
|
+
if (!actor) continue
|
|
724
|
+
const { x, y, w, h, bottomY } = layouts[i]
|
|
725
|
+
const isCreated = creationEventIndex.has(key)
|
|
726
|
+
const isDestroyed = destructionEventIndex.has(key)
|
|
727
|
+
const shared = {
|
|
728
|
+
geo: mapParticipantTypeToGeo(actor.type),
|
|
729
|
+
label: actor.description || actor.name || key,
|
|
730
|
+
align: 'middle' as const,
|
|
731
|
+
verticalAlign: 'middle' as const,
|
|
732
|
+
size: 's' as const,
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const creationY = isCreated ? lifelineTop + eventStep * (creationEventIndex.get(key)! + 1) : 0
|
|
736
|
+
const topY = isCreated ? creationY - h / 2 : y
|
|
737
|
+
nodes.push({ id: `actor-top-${key}`, x, y: topY, w, h, ...shared })
|
|
738
|
+
|
|
739
|
+
if (!isDestroyed) {
|
|
740
|
+
nodes.push({ id: `actor-bottom-${key}`, x, y: bottomY, w, h, ...shared })
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// 5. Events: signals and notes
|
|
745
|
+
const pendingCreations = new Set(createdActors.keys())
|
|
746
|
+
let sequenceNumber = autonumberStart
|
|
747
|
+
|
|
748
|
+
for (let eventIndex = 0; eventIndex < events.length; eventIndex++) {
|
|
749
|
+
const msg = events[eventIndex]
|
|
750
|
+
const anchor = (eventIndex + 1) / (events.length + 1)
|
|
751
|
+
|
|
752
|
+
if (isSignalMessage(msg.type)) {
|
|
753
|
+
const fromKey = msg.from!
|
|
754
|
+
const toKey = msg.to!
|
|
755
|
+
if (!keyIndex.has(fromKey) || !keyIndex.has(toKey)) continue
|
|
756
|
+
|
|
757
|
+
const isCreationMessage = pendingCreations.has(toKey)
|
|
758
|
+
if (isCreationMessage) pendingCreations.delete(toKey)
|
|
759
|
+
|
|
760
|
+
const msgType = msg.type ?? LINETYPE.SOLID
|
|
761
|
+
const { dash, arrowheadEnd } = mapLineTypeToArrowProps(msgType)
|
|
762
|
+
const isSelf = fromKey === toKey
|
|
763
|
+
const bidir = !isSelf && isBidirectional(msgType)
|
|
764
|
+
|
|
765
|
+
const edge: MermaidBlueprintEdge = {
|
|
766
|
+
startNodeId: `lifeline-${fromKey}`,
|
|
767
|
+
endNodeId: isCreationMessage ? `actor-top-${toKey}` : `lifeline-${toKey}`,
|
|
768
|
+
label: getMessageLabel(msg),
|
|
769
|
+
bend: isSelf ? SELF_MSG_BEND : 0,
|
|
770
|
+
dash,
|
|
771
|
+
arrowheadEnd,
|
|
772
|
+
arrowheadStart: bidir ? 'arrow' : 'none',
|
|
773
|
+
size: 's',
|
|
774
|
+
anchorStartY: isSelf ? anchor - SELF_MSG_Y_OFFSET : anchor,
|
|
775
|
+
anchorEndY: isCreationMessage ? 0.5 : isSelf ? anchor + SELF_MSG_Y_OFFSET : anchor,
|
|
776
|
+
isExact: true,
|
|
777
|
+
isPrecise: true,
|
|
778
|
+
...(isCreationMessage && { isExactEnd: false, isPreciseEnd: false }),
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (autonumberVisible) {
|
|
782
|
+
edge.decoration = { type: 'autonumber', value: String(sequenceNumber) }
|
|
783
|
+
sequenceNumber += autonumberStep
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
edges.push(edge)
|
|
787
|
+
} else if (isNoteMessage(msg.type)) {
|
|
788
|
+
const eventY = lifelineTop + eventStep * (eventIndex + 1)
|
|
789
|
+
const fromKey = msg.from!
|
|
790
|
+
const fromIdx = keyIndex.get(fromKey)
|
|
791
|
+
const toIdx = keyIndex.get(msg.to ?? fromKey)
|
|
792
|
+
if (fromIdx === undefined) continue
|
|
793
|
+
|
|
794
|
+
const label = getMessageLabel(msg)
|
|
795
|
+
// Mermaid types `placement` as a string enum but the runtime value is numeric
|
|
796
|
+
const msgPlacement = msg.placement as unknown as number | undefined
|
|
797
|
+
const fromCenterX = layouts[fromIdx].x + layouts[fromIdx].w / 2
|
|
798
|
+
const toCenterX = toIdx !== undefined ? layouts[toIdx].x + layouts[toIdx].w / 2 : fromCenterX
|
|
799
|
+
const isSpanning =
|
|
800
|
+
msgPlacement === PLACEMENT.OVER && msg.from !== msg.to && toIdx !== undefined
|
|
801
|
+
|
|
802
|
+
const svgNote = svgNoteRects[svgNoteIndex++]
|
|
803
|
+
const noteHeight = svgNote?.h ?? FALLBACK_NOTE_HEIGHT
|
|
804
|
+
const textWidth = label ? label.length * NOTE_CHAR_WIDTH + NOTE_TEXT_PADDING : 0
|
|
805
|
+
const baseWidth = Math.max(svgNote?.w ?? FALLBACK_NOTE_WIDTH, textWidth)
|
|
806
|
+
const noteWidth = isSpanning
|
|
807
|
+
? Math.max(baseWidth, Math.abs(toCenterX - fromCenterX) + NOTE_PADDING)
|
|
808
|
+
: baseWidth
|
|
809
|
+
|
|
810
|
+
let noteX: number
|
|
811
|
+
if (msgPlacement === PLACEMENT.LEFTOF) {
|
|
812
|
+
noteX = fromCenterX - noteWidth - NOTE_PADDING
|
|
813
|
+
} else if (msgPlacement === PLACEMENT.RIGHTOF) {
|
|
814
|
+
noteX = fromCenterX + NOTE_PADDING
|
|
815
|
+
} else if (isSpanning) {
|
|
816
|
+
noteX = (fromCenterX + toCenterX) / 2 - noteWidth / 2
|
|
817
|
+
} else {
|
|
818
|
+
noteX = fromCenterX - noteWidth / 2
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
nodes.push({
|
|
822
|
+
id: `note-${eventIndex}`,
|
|
823
|
+
x: noteX,
|
|
824
|
+
y: eventY - noteHeight / 2,
|
|
825
|
+
w: noteWidth,
|
|
826
|
+
h: noteHeight,
|
|
827
|
+
geo: 'rectangle',
|
|
828
|
+
fill: 'solid',
|
|
829
|
+
color: 'yellow',
|
|
830
|
+
dash: 'draw',
|
|
831
|
+
size: 's',
|
|
832
|
+
align: 'middle',
|
|
833
|
+
verticalAlign: 'middle',
|
|
834
|
+
label,
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return {
|
|
840
|
+
nodes,
|
|
841
|
+
edges,
|
|
842
|
+
lines,
|
|
843
|
+
groups: actorKeys.map((key) => {
|
|
844
|
+
const group = [`actor-top-${key}`, `lifeline-${key}`]
|
|
845
|
+
if (!destructionEventIndex.has(key)) {
|
|
846
|
+
group.push(`actor-bottom-${key}`)
|
|
847
|
+
}
|
|
848
|
+
return group
|
|
849
|
+
}),
|
|
850
|
+
}
|
|
851
|
+
}
|