(
root: MindMapTreeNode,
options: MindMapSvgExportOptions = {},
)
| 379 | }; |
| 380 | |
| 381 | export const buildMindMapSvg = ( |
| 382 | root: MindMapTreeNode, |
| 383 | options: MindMapSvgExportOptions = {}, |
| 384 | ): string => { |
| 385 | const { nodes, edges } = buildMindMapFlow(root); |
| 386 | const title = options.title?.trim() || root.label || 'Mind Map'; |
| 387 | const subtitle = options.subtitle?.trim() || root.summary?.trim() || ''; |
| 388 | const highlights = (options.highlights || []).map((item) => item.trim()).filter(Boolean).slice(0, 4); |
| 389 | |
| 390 | const bounds = nodes.reduce( |
| 391 | (acc, node) => { |
| 392 | const box = getNodeBounds(node); |
| 393 | return { |
| 394 | minX: Math.min(acc.minX, box.left), |
| 395 | minY: Math.min(acc.minY, box.top), |
| 396 | maxX: Math.max(acc.maxX, box.right), |
| 397 | maxY: Math.max(acc.maxY, box.bottom), |
| 398 | }; |
| 399 | }, |
| 400 | { minX: 0, minY: 0, maxX: 0, maxY: 0 }, |
| 401 | ); |
| 402 | |
| 403 | const contentWidth = bounds.maxX - bounds.minX; |
| 404 | const contentHeight = bounds.maxY - bounds.minY; |
| 405 | const width = options.width || Math.max(1400, Math.ceil(contentWidth + SVG_PADDING_X * 2)); |
| 406 | const height = |
| 407 | options.height || |
| 408 | Math.max(980, Math.ceil(contentHeight + SVG_PADDING_Y * 2 + SVG_HEADER_HEIGHT + SVG_FOOTER_HEIGHT)); |
| 409 | const shiftX = Math.round((width - contentWidth) / 2 - bounds.minX); |
| 410 | const shiftY = Math.round(SVG_HEADER_HEIGHT + Math.max(0, (height - SVG_HEADER_HEIGHT - SVG_FOOTER_HEIGHT - contentHeight) / 2) - bounds.minY); |
| 411 | |
| 412 | const nodeMap = new Map(nodes.map((node) => [node.id, node])); |
| 413 | const branchColor = (branch: MindMapNodeData['branch']) => |
| 414 | branch === 'left' |
| 415 | ? { stroke: '#a78bfa', fill: 'rgba(91, 33, 182, 0.24)' } |
| 416 | : branch === 'right' |
| 417 | ? { stroke: '#22d3ee', fill: 'rgba(14, 165, 233, 0.24)' } |
| 418 | : { stroke: '#67e8f9', fill: 'rgba(6, 182, 212, 0.28)' }; |
| 419 | |
| 420 | const edgeMarkup = edges |
| 421 | .map((edge) => { |
| 422 | const source = nodeMap.get(edge.source); |
| 423 | const target = nodeMap.get(edge.target); |
| 424 | if (!source || !target) return ''; |
| 425 | const sourceBox = getNodeBounds(source); |
| 426 | const targetBox = getNodeBounds(target); |
| 427 | const sourceBranch = target.data.branch === 'left' ? 'left' : 'right'; |
| 428 | const sx = source.data.isRoot |
| 429 | ? sourceBox.left + sourceBox.width / 2 |
| 430 | : sourceBranch === 'left' |
| 431 | ? sourceBox.left |
| 432 | : sourceBox.right; |
| 433 | const tx = target.data.branch === 'left' ? targetBox.right : targetBox.left; |
| 434 | const sy = sourceBox.top + sourceBox.height / 2; |
| 435 | const ty = targetBox.top + targetBox.height / 2; |
| 436 | const midX = (sx + tx) / 2; |
| 437 | const color = sourceBranch === 'left' ? '#a78bfa' : '#22d3ee'; |
| 438 | return `<path d="M ${sx} ${sy} C ${midX} ${sy}, ${midX} ${ty}, ${tx} ${ty}" fill="none" stroke="${color}" stroke-width="${target.data.depth <= 1 ? 3 : 2}" stroke-linecap="round" />`; |
no test coverage detected