@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.
- package/README.md +195 -0
- 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 +157 -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/mindmapDiagram.js +139 -0
- package/dist-cjs/mindmapDiagram.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 +127 -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/mindmapDiagram.mjs +119 -0
- package/dist-esm/mindmapDiagram.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 +64 -0
- package/src/blueprint.ts +75 -0
- package/src/colors.ts +215 -0
- package/src/createMermaidDiagram.test.ts +31 -0
- package/src/createMermaidDiagram.ts +169 -0
- package/src/flowchartDiagram.ts +232 -0
- package/src/index.ts +18 -0
- package/src/mermaidDiagrams.test.ts +1157 -0
- package/src/mindmapDiagram.ts +169 -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,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
|
+
}
|