@tldraw/mermaid 4.6.0-canary.00a8c03b5687

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +195 -0
  2. package/dist-cjs/blueprint.js +17 -0
  3. package/dist-cjs/blueprint.js.map +7 -0
  4. package/dist-cjs/colors.js +173 -0
  5. package/dist-cjs/colors.js.map +7 -0
  6. package/dist-cjs/createMermaidDiagram.js +157 -0
  7. package/dist-cjs/createMermaidDiagram.js.map +7 -0
  8. package/dist-cjs/flowchartDiagram.js +202 -0
  9. package/dist-cjs/flowchartDiagram.js.map +7 -0
  10. package/dist-cjs/index.d.ts +114 -0
  11. package/dist-cjs/index.js +34 -0
  12. package/dist-cjs/index.js.map +7 -0
  13. package/dist-cjs/mindmapDiagram.js +139 -0
  14. package/dist-cjs/mindmapDiagram.js.map +7 -0
  15. package/dist-cjs/renderBlueprint.js +314 -0
  16. package/dist-cjs/renderBlueprint.js.map +7 -0
  17. package/dist-cjs/sequenceDiagram.js +686 -0
  18. package/dist-cjs/sequenceDiagram.js.map +7 -0
  19. package/dist-cjs/stateDiagram.js +373 -0
  20. package/dist-cjs/stateDiagram.js.map +7 -0
  21. package/dist-cjs/svgParsing.js +187 -0
  22. package/dist-cjs/svgParsing.js.map +7 -0
  23. package/dist-cjs/utils.js +75 -0
  24. package/dist-cjs/utils.js.map +7 -0
  25. package/dist-esm/blueprint.mjs +1 -0
  26. package/dist-esm/blueprint.mjs.map +7 -0
  27. package/dist-esm/colors.mjs +153 -0
  28. package/dist-esm/colors.mjs.map +7 -0
  29. package/dist-esm/createMermaidDiagram.mjs +127 -0
  30. package/dist-esm/createMermaidDiagram.mjs.map +7 -0
  31. package/dist-esm/flowchartDiagram.mjs +188 -0
  32. package/dist-esm/flowchartDiagram.mjs.map +7 -0
  33. package/dist-esm/index.d.mts +114 -0
  34. package/dist-esm/index.mjs +14 -0
  35. package/dist-esm/index.mjs.map +7 -0
  36. package/dist-esm/mindmapDiagram.mjs +119 -0
  37. package/dist-esm/mindmapDiagram.mjs.map +7 -0
  38. package/dist-esm/renderBlueprint.mjs +298 -0
  39. package/dist-esm/renderBlueprint.mjs.map +7 -0
  40. package/dist-esm/sequenceDiagram.mjs +666 -0
  41. package/dist-esm/sequenceDiagram.mjs.map +7 -0
  42. package/dist-esm/stateDiagram.mjs +359 -0
  43. package/dist-esm/stateDiagram.mjs.map +7 -0
  44. package/dist-esm/svgParsing.mjs +167 -0
  45. package/dist-esm/svgParsing.mjs.map +7 -0
  46. package/dist-esm/utils.mjs +55 -0
  47. package/dist-esm/utils.mjs.map +7 -0
  48. package/package.json +64 -0
  49. package/src/blueprint.ts +75 -0
  50. package/src/colors.ts +215 -0
  51. package/src/createMermaidDiagram.test.ts +31 -0
  52. package/src/createMermaidDiagram.ts +169 -0
  53. package/src/flowchartDiagram.ts +232 -0
  54. package/src/index.ts +18 -0
  55. package/src/mermaidDiagrams.test.ts +1157 -0
  56. package/src/mindmapDiagram.ts +169 -0
  57. package/src/renderBlueprint.ts +373 -0
  58. package/src/sequenceDiagram.ts +851 -0
  59. package/src/stateDiagram.ts +477 -0
  60. package/src/svgParsing.ts +240 -0
  61. package/src/utils.ts +73 -0
@@ -0,0 +1,169 @@
1
+ import type { MindmapNode } from 'mermaid/dist/diagrams/mindmap/mindmapTypes.js'
2
+ import type { TLDefaultColorStyle, TLDefaultSizeStyle, TLGeoShapeGeoStyle } from 'tldraw'
3
+ import type {
4
+ DiagramMermaidBlueprint,
5
+ MermaidBlueprintEdge,
6
+ MermaidBlueprintGeoNode,
7
+ } from './blueprint'
8
+ import { parseRgbToTldrawColor } from './colors'
9
+ import type { ParsedNode } from './svgParsing'
10
+ import { parseNodesFromSvg, scaleLayout } from './svgParsing'
11
+ import { LAYOUT_SCALE } from './utils'
12
+
13
+ const MINDMAP_NODE_TYPE = {
14
+ DEFAULT: 0,
15
+ ROUNDED_RECT: 1,
16
+ RECT: 2,
17
+ CIRCLE: 3,
18
+ CLOUD: 4,
19
+ BANG: 5,
20
+ HEXAGON: 6,
21
+ } as const
22
+
23
+ function mapMindmapTypeToGeo(type: number): TLGeoShapeGeoStyle {
24
+ switch (type) {
25
+ case MINDMAP_NODE_TYPE.CIRCLE:
26
+ return 'ellipse'
27
+ case MINDMAP_NODE_TYPE.CLOUD:
28
+ return 'cloud'
29
+ case MINDMAP_NODE_TYPE.HEXAGON:
30
+ return 'hexagon'
31
+ case MINDMAP_NODE_TYPE.BANG:
32
+ return 'star'
33
+ case MINDMAP_NODE_TYPE.RECT:
34
+ case MINDMAP_NODE_TYPE.ROUNDED_RECT:
35
+ case MINDMAP_NODE_TYPE.DEFAULT:
36
+ default:
37
+ return 'rectangle'
38
+ }
39
+ }
40
+
41
+ function getEdgeSizeForLevel(parentLevel: number): TLDefaultSizeStyle {
42
+ if (parentLevel <= 0) return 'l'
43
+ if (parentLevel === 1) return 'm'
44
+ return 's'
45
+ }
46
+
47
+ interface FlatNode {
48
+ id: string
49
+ label: string
50
+ type: number
51
+ level: number
52
+ parentId: string | undefined
53
+ section: number | undefined
54
+ isRoot: boolean
55
+ }
56
+
57
+ function flattenMindmapTree(
58
+ node: MindmapNode,
59
+ parentId: string | undefined,
60
+ out: FlatNode[]
61
+ ): void {
62
+ out.push({
63
+ id: String(node.id),
64
+ label: node.descr,
65
+ type: node.type,
66
+ level: node.level,
67
+ parentId,
68
+ section: node.section,
69
+ isRoot: !!node.isRoot,
70
+ })
71
+ for (const child of node.children) {
72
+ flattenMindmapTree(child, String(node.id), out)
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Pre-parsed SVG layout for mindmap diagram converters.
78
+ * Contains already-scaled node positions extracted from the SVG.
79
+ */
80
+ export interface ParsedMindmapLayout {
81
+ nodes: Map<string, ParsedNode>
82
+ }
83
+
84
+ function parseMindmapNodeId(domId: string): string {
85
+ const match = domId.match(/^node_(\d+)$/)
86
+ return match ? match[1] : domId
87
+ }
88
+
89
+ /** Parse mindmap-specific SVG layout data for use by {@link mindmapToBlueprint}. */
90
+ export function parseMindmapLayout(root: Element): ParsedMindmapLayout {
91
+ const nodes = parseNodesFromSvg(root, '.node', parseMindmapNodeId)
92
+ scaleLayout(nodes, new Map(), [], LAYOUT_SCALE)
93
+ return { nodes }
94
+ }
95
+
96
+ /** Convert a parsed Mermaid mindmap into a tldraw blueprint of nodes and edges. */
97
+ export function mindmapToBlueprint(
98
+ layout: ParsedMindmapLayout,
99
+ mindmapTree: MindmapNode,
100
+ svgRoot: Element
101
+ ): DiagramMermaidBlueprint {
102
+ const flatNodes: FlatNode[] = []
103
+ flattenMindmapTree(mindmapTree, undefined, flatNodes)
104
+
105
+ const { nodes: svgNodes } = layout
106
+
107
+ const nodeColors = new Map<string, TLDefaultColorStyle>()
108
+ for (const el of svgRoot.querySelectorAll('.node')) {
109
+ const rawId = el.getAttribute('id') || ''
110
+ const id = parseMindmapNodeId(rawId)
111
+ const shape =
112
+ el.querySelector('rect, circle, ellipse, polygon, path') ??
113
+ el.querySelector('.label-container')
114
+ if (shape) {
115
+ const parsed = parseRgbToTldrawColor(getComputedStyle(shape as Element).fill)
116
+ if (parsed) nodeColors.set(id, parsed.color)
117
+ }
118
+ }
119
+
120
+ const nodes: MermaidBlueprintGeoNode[] = []
121
+ const edges: MermaidBlueprintEdge[] = []
122
+ const levelById = new Map(flatNodes.map((n) => [n.id, n.level]))
123
+
124
+ for (const flatNode of flatNodes) {
125
+ const svgNode = svgNodes.get(flatNode.id)
126
+ if (!svgNode) continue
127
+
128
+ const geo = mapMindmapTypeToGeo(flatNode.type)
129
+ const color = nodeColors.get(flatNode.id) ?? 'black'
130
+
131
+ let { width: w, height: h } = svgNode
132
+ if (flatNode.type === MINDMAP_NODE_TYPE.CIRCLE) {
133
+ w = h = Math.max(w, h)
134
+ }
135
+
136
+ nodes.push({
137
+ id: flatNode.id,
138
+ x: svgNode.center.x - w / 2,
139
+ y: svgNode.center.y - h / 2,
140
+ w,
141
+ h,
142
+ geo,
143
+ label: flatNode.label || undefined,
144
+ fill: 'solid',
145
+ color,
146
+ size: flatNode.isRoot ? 'l' : 'm',
147
+ align: 'middle',
148
+ verticalAlign: 'middle',
149
+ })
150
+
151
+ // Edge from parent to this node
152
+ if (flatNode.parentId) {
153
+ const parentLevel = levelById.get(flatNode.parentId) ?? 0
154
+ edges.push({
155
+ startNodeId: flatNode.parentId,
156
+ endNodeId: flatNode.id,
157
+ bend: 0,
158
+ arrowheadEnd: 'none',
159
+ arrowheadStart: 'none',
160
+ size: getEdgeSizeForLevel(parentLevel),
161
+ color,
162
+ })
163
+ }
164
+ }
165
+
166
+ const nodeIds = new Set(nodes.map((n) => n.id))
167
+ const validEdges = edges.filter((e) => nodeIds.has(e.startNodeId) && nodeIds.has(e.endNodeId))
168
+ return { nodes, edges: validEdges }
169
+ }
@@ -0,0 +1,373 @@
1
+ import {
2
+ createShapeId,
3
+ Editor,
4
+ IndexKey,
5
+ TLGeoShape,
6
+ TLLineShape,
7
+ TLShapeId,
8
+ toRichText,
9
+ Vec,
10
+ } from 'tldraw'
11
+ import type {
12
+ DiagramMermaidBlueprint,
13
+ MermaidBlueprintEdge,
14
+ MermaidBlueprintGeoNode,
15
+ MermaidBlueprintLineNode,
16
+ } from './blueprint'
17
+ import { orderTopDown, sanitizeDiagramText } from './utils'
18
+
19
+ /** @public */
20
+ export interface BlueprintRenderingOptions {
21
+ centerOnPosition?: boolean
22
+ position?: { x: number; y: number }
23
+ }
24
+
25
+ const defaultBlueprintRenderingOptions = {
26
+ centerOnPosition: true,
27
+ }
28
+
29
+ /** @public */
30
+ export function renderBlueprint(
31
+ editor: Editor,
32
+ blueprint: DiagramMermaidBlueprint,
33
+ opts?: BlueprintRenderingOptions
34
+ ) {
35
+ const options = { ...defaultBlueprintRenderingOptions, ...(opts || {}) }
36
+ const { nodes, edges, lines } = blueprint
37
+
38
+ const bounds = computeBlueprintBounds(nodes, lines)
39
+ const center = options.position
40
+ ? options.position
41
+ : editor.user.getIsPasteAtCursorMode()
42
+ ? editor.inputs.getCurrentPagePoint()
43
+ : editor.getViewportPageBounds().center
44
+ const offsetX = options.centerOnPosition
45
+ ? center.x - (bounds.maxX + bounds.minX) / 2
46
+ : center.x - bounds.minX
47
+ const offsetY = options.centerOnPosition
48
+ ? center.y - (bounds.maxY + bounds.minY) / 2
49
+ : center.y - bounds.minY
50
+
51
+ const ordered = orderTopDown(
52
+ nodes,
53
+ (n) => n.id,
54
+ (n) => n.parentId
55
+ )
56
+ const nodeById = new Map(nodes.map((node) => [node.id, node]))
57
+
58
+ const shapeIds = new Map<string, TLShapeId>()
59
+
60
+ // Lines first so nodes render on top (z-order = creation order in tldraw)
61
+ if (lines) {
62
+ for (const line of lines) {
63
+ const lineId = createShapeId()
64
+ shapeIds.set(line.id, lineId)
65
+ editor.createShape<TLLineShape>({
66
+ id: lineId,
67
+ type: 'line',
68
+ x: offsetX + line.x,
69
+ y: offsetY + line.y,
70
+ props: {
71
+ dash: line.dash ?? 'solid',
72
+ size: line.size ?? 's',
73
+ color: line.color ?? 'black',
74
+ spline: 'line',
75
+ points: {
76
+ a1: { id: 'a1', index: 'a1' as IndexKey, x: 0, y: 0 },
77
+ a2: { id: 'a2', index: 'a2' as IndexKey, x: line.endX ?? 0, y: line.endY },
78
+ },
79
+ },
80
+ })
81
+ }
82
+ }
83
+
84
+ for (const node of ordered) {
85
+ const shapeId = createShapeId()
86
+ shapeIds.set(node.id, shapeId)
87
+
88
+ const parent = node.parentId ? nodeById.get(node.parentId) : undefined
89
+ const parentShapeId = node.parentId ? shapeIds.get(node.parentId) : undefined
90
+
91
+ const absoluteX = offsetX + node.x
92
+ const absoluteY = offsetY + node.y
93
+ const x = parent ? absoluteX - (offsetX + parent.x) : absoluteX
94
+ const y = parent ? absoluteY - (offsetY + parent.y) : absoluteY
95
+
96
+ editor.createShape<TLGeoShape>({
97
+ id: shapeId,
98
+ type: 'geo',
99
+ x,
100
+ y,
101
+ parentId: parentShapeId,
102
+ props: {
103
+ geo: node.geo,
104
+ w: node.w,
105
+ h: node.h,
106
+ fill: node.fill ?? 'none',
107
+ color: node.color ?? 'black',
108
+ dash: node.dash ?? 'draw',
109
+ size: node.size ?? 'm',
110
+ ...(node.label && { richText: toRichText(sanitizeDiagramText(node.label)) }),
111
+ ...(node.align && { align: node.align }),
112
+ ...(node.verticalAlign && { verticalAlign: node.verticalAlign }),
113
+ },
114
+ })
115
+ }
116
+
117
+ const arrowIds: TLShapeId[] = []
118
+ for (const edge of edges) {
119
+ const arrowId = createArrowFromEdge(editor, edge, shapeIds)
120
+ if (arrowId) arrowIds.push(arrowId)
121
+ }
122
+
123
+ // Create sub-groups and track which shape IDs are consumed by a group
124
+ const groupedIds = new Set<TLShapeId>()
125
+ const subGroupIds: TLShapeId[] = []
126
+ if (blueprint.groups) {
127
+ for (const group of blueprint.groups) {
128
+ const members: TLShapeId[] = []
129
+ for (const blueprintId of group) {
130
+ const memberShapeId = shapeIds.get(blueprintId)
131
+ if (memberShapeId) {
132
+ members.push(memberShapeId)
133
+ groupedIds.add(memberShapeId)
134
+ }
135
+ }
136
+ if (members.length > 1) {
137
+ editor.groupShapes(members)
138
+ const first = editor.getShape(members[0])
139
+ if (first && first.parentId !== editor.getCurrentPageId()) {
140
+ subGroupIds.push(first.parentId as TLShapeId)
141
+ } else {
142
+ subGroupIds.push(members[0])
143
+ }
144
+ } else if (members.length === 1) {
145
+ subGroupIds.push(members[0])
146
+ }
147
+ }
148
+ }
149
+
150
+ // Collect ungrouped top-level IDs
151
+ const topLevelIds: TLShapeId[] = [...subGroupIds]
152
+ for (const node of nodes) {
153
+ if (!node.parentId) {
154
+ const nodeShapeId = shapeIds.get(node.id)
155
+ if (nodeShapeId && !groupedIds.has(nodeShapeId)) topLevelIds.push(nodeShapeId)
156
+ }
157
+ }
158
+ if (lines) {
159
+ for (const line of lines) {
160
+ const lineShapeId = shapeIds.get(line.id)
161
+ if (lineShapeId && !groupedIds.has(lineShapeId)) topLevelIds.push(lineShapeId)
162
+ }
163
+ }
164
+ topLevelIds.push(...arrowIds)
165
+
166
+ let rootShapeId: TLShapeId | undefined
167
+ if (topLevelIds.length > 1) {
168
+ editor.groupShapes(topLevelIds)
169
+ const first = editor.getShape(topLevelIds[0])
170
+ if (first && first.parentId !== editor.getCurrentPageId()) {
171
+ rootShapeId = first.parentId as TLShapeId
172
+ }
173
+ } else if (topLevelIds.length === 1) {
174
+ rootShapeId = topLevelIds[0]
175
+ }
176
+
177
+ if (rootShapeId) {
178
+ const actualBounds = editor.getShapePageBounds(rootShapeId)
179
+ if (actualBounds) {
180
+ const desiredX = options.centerOnPosition ? center.x - actualBounds.w / 2 : center.x
181
+ const desiredY = options.centerOnPosition ? center.y - actualBounds.h / 2 : center.y
182
+ const dx = desiredX - actualBounds.x
183
+ const dy = desiredY - actualBounds.y
184
+ if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
185
+ const shape = editor.getShape(rootShapeId)!
186
+ editor.updateShape({
187
+ id: rootShapeId,
188
+ type: shape.type,
189
+ x: shape.x + dx,
190
+ y: shape.y + dy,
191
+ })
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ function makeArrowBinding(
198
+ arrowId: TLShapeId,
199
+ targetId: TLShapeId,
200
+ terminal: 'start' | 'end',
201
+ anchor: { x: number; y: number },
202
+ isExact: boolean,
203
+ isPrecise: boolean
204
+ ) {
205
+ return {
206
+ fromId: arrowId,
207
+ toId: targetId,
208
+ type: 'arrow' as const,
209
+ props: { terminal, normalizedAnchor: anchor, isExact, isPrecise },
210
+ }
211
+ }
212
+
213
+ function createArrowFromEdge(
214
+ editor: Editor,
215
+ edge: MermaidBlueprintEdge,
216
+ shapeIds: Map<string, TLShapeId>
217
+ ): TLShapeId | undefined {
218
+ const startShapeId = shapeIds.get(edge.startNodeId)
219
+ const endShapeId = shapeIds.get(edge.endNodeId)
220
+ if (!startShapeId || !endShapeId) return undefined
221
+
222
+ const startBounds = editor.getShapePageBounds(startShapeId)
223
+ const endBounds = editor.getShapePageBounds(endShapeId)
224
+ if (!startBounds || !endBounds) return undefined
225
+
226
+ const arrowId = createShapeId()
227
+ const isSelfLoop = startShapeId === endShapeId
228
+ const hasPreciseAnchors = edge.anchorStartY !== undefined || edge.anchorEndY !== undefined
229
+
230
+ let labelText = edge.label
231
+ if (edge.decoration?.type === 'autonumber') {
232
+ const num = edge.decoration.value
233
+ labelText = labelText ? `${num} ${labelText}` : num
234
+ }
235
+
236
+ const baseProps = {
237
+ dash: edge.dash ?? ('solid' as const),
238
+ size: edge.size ?? ('s' as const),
239
+ arrowheadEnd: edge.arrowheadEnd ?? ('arrow' as const),
240
+ ...(edge.arrowheadStart && { arrowheadStart: edge.arrowheadStart }),
241
+ color: edge.color ?? ('black' as const),
242
+ ...(labelText && { richText: toRichText(sanitizeDiagramText(labelText)) }),
243
+ }
244
+
245
+ if (hasPreciseAnchors) {
246
+ const startAnchorY = edge.anchorStartY ?? 0.5
247
+ const endAnchorY = edge.anchorEndY ?? 0.5
248
+ const exactStart = edge.isExact ?? true
249
+ const preciseStart = edge.isPrecise ?? true
250
+ const exactEnd = edge.isExactEnd ?? exactStart
251
+ const preciseEnd = edge.isPreciseEnd ?? preciseStart
252
+
253
+ const startPoint = {
254
+ x: startBounds.x + startBounds.w * 0.5,
255
+ y: startBounds.y + startBounds.h * startAnchorY,
256
+ }
257
+ const endPoint = {
258
+ x: endBounds.x + endBounds.w * 0.5,
259
+ y: endBounds.y + endBounds.h * endAnchorY,
260
+ }
261
+ const origin = {
262
+ x: Math.min(startPoint.x, endPoint.x),
263
+ y: Math.min(startPoint.y, endPoint.y),
264
+ }
265
+
266
+ editor.run(() => {
267
+ editor.createShape({
268
+ id: arrowId,
269
+ type: 'arrow',
270
+ x: origin.x,
271
+ y: origin.y,
272
+ props: {
273
+ ...baseProps,
274
+ start: { x: startPoint.x - origin.x, y: startPoint.y - origin.y },
275
+ end: { x: endPoint.x - origin.x, y: endPoint.y - origin.y },
276
+ bend: edge.bend,
277
+ },
278
+ })
279
+ editor.createBindings([
280
+ makeArrowBinding(
281
+ arrowId,
282
+ startShapeId,
283
+ 'start',
284
+ { x: 0.5, y: startAnchorY },
285
+ exactStart,
286
+ preciseStart
287
+ ),
288
+ makeArrowBinding(
289
+ arrowId,
290
+ endShapeId,
291
+ 'end',
292
+ { x: 0.5, y: endAnchorY },
293
+ exactEnd,
294
+ preciseEnd
295
+ ),
296
+ ])
297
+ })
298
+ return arrowId
299
+ }
300
+
301
+ if (isSelfLoop) {
302
+ editor.run(() => {
303
+ editor.createShape({
304
+ id: arrowId,
305
+ type: 'arrow',
306
+ x: startBounds.x,
307
+ y: startBounds.y,
308
+ props: {
309
+ ...baseProps,
310
+ start: { x: startBounds.w / 2, y: 0 },
311
+ end: { x: startBounds.w, y: startBounds.h / 2 },
312
+ bend: -80,
313
+ },
314
+ })
315
+ editor.createBindings([
316
+ makeArrowBinding(arrowId, startShapeId, 'start', { x: 0.9, y: 0.5 }, false, false),
317
+ makeArrowBinding(arrowId, endShapeId, 'end', { x: 0.85, y: 0.8 }, false, false),
318
+ ])
319
+ })
320
+ return arrowId
321
+ }
322
+
323
+ const startCenter = startBounds.center
324
+ const endCenter = endBounds.center
325
+ const arrowOrigin = Vec.Min(startCenter, endCenter)
326
+
327
+ editor.run(() => {
328
+ editor.createShape({
329
+ id: arrowId,
330
+ type: 'arrow',
331
+ x: arrowOrigin.x,
332
+ y: arrowOrigin.y,
333
+ props: {
334
+ ...baseProps,
335
+ start: { x: startCenter.x - arrowOrigin.x, y: startCenter.y - arrowOrigin.y },
336
+ end: { x: endCenter.x - arrowOrigin.x, y: endCenter.y - arrowOrigin.y },
337
+ bend: edge.bend,
338
+ },
339
+ })
340
+ editor.createBindings([
341
+ makeArrowBinding(arrowId, startShapeId, 'start', { x: 0.5, y: 0.5 }, false, false),
342
+ makeArrowBinding(arrowId, endShapeId, 'end', { x: 0.5, y: 0.5 }, false, false),
343
+ ])
344
+ })
345
+
346
+ return arrowId
347
+ }
348
+
349
+ function computeBlueprintBounds(
350
+ nodes: MermaidBlueprintGeoNode[],
351
+ lines?: MermaidBlueprintLineNode[]
352
+ ): { minX: number; minY: number; maxX: number; maxY: number } {
353
+ let minX = Infinity,
354
+ minY = Infinity,
355
+ maxX = -Infinity,
356
+ maxY = -Infinity
357
+ for (const node of nodes) {
358
+ if (node.parentId) continue
359
+ minX = Math.min(minX, node.x)
360
+ minY = Math.min(minY, node.y)
361
+ maxX = Math.max(maxX, node.x + node.w)
362
+ maxY = Math.max(maxY, node.y + node.h)
363
+ }
364
+ if (lines) {
365
+ for (const line of lines) {
366
+ minX = Math.min(minX, line.x)
367
+ minY = Math.min(minY, line.y)
368
+ maxX = Math.max(maxX, line.x)
369
+ maxY = Math.max(maxY, line.y + line.endY)
370
+ }
371
+ }
372
+ return { minX, minY, maxX, maxY }
373
+ }