From 9fdd34c55c47de093cd917868d862e40021b9c4a Mon Sep 17 00:00:00 2001 From: darshanr0107 Date: Fri, 3 Oct 2025 16:28:25 +0530 Subject: [PATCH] fix: move mindmap icons logic into a separate file on-behalf-of: @Mermaid-Chart --- .../src/diagrams/mindmap/mindmapIconHelper.ts | 222 ++++++++++++++++++ .../src/diagrams/mindmap/mindmapRenderer.ts | 23 ++ .../rendering-elements/nodes.ts | 92 +------- .../rendering-elements/shapes/bang.ts | 52 ++-- .../rendering-elements/shapes/circle.ts | 27 +-- .../rendering-elements/shapes/cloud.ts | 52 ++-- .../shapes/defaultMindmapNode.ts | 44 ++-- .../rendering-elements/shapes/drawRect.ts | 28 +-- .../rendering-elements/shapes/hexagon.ts | 29 +-- .../shapes/mindmapCircle.ts | 56 ++++- .../rendering-elements/shapes/roundedRect.ts | 18 +- 11 files changed, 412 insertions(+), 231 deletions(-) create mode 100644 packages/mermaid/src/diagrams/mindmap/mindmapIconHelper.ts diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapIconHelper.ts b/packages/mermaid/src/diagrams/mindmap/mindmapIconHelper.ts new file mode 100644 index 000000000..9d89b9c56 --- /dev/null +++ b/packages/mermaid/src/diagrams/mindmap/mindmapIconHelper.ts @@ -0,0 +1,222 @@ +import { log } from '../../logger.js'; +import type { D3Selection } from '../../types.js'; +import type { Node } from '../../rendering-util/types.js'; +import { getIconSVG, isIconAvailable } from '../../rendering-util/icons.js'; + +export interface MindmapIconConfig { + iconSize: number; + iconPadding: number; + shapeType: 'circle' | 'rect' | 'rounded' | 'bang' | 'cloud' | 'hexagon' | 'default'; +} + +export interface MindmapDimensions { + width: number; + height: number; + labelOffset: { x: number; y: number }; +} + +/** + * Get icon configuration for different mindmap shape types + */ +export function getMindmapIconConfig(shapeType: string): MindmapIconConfig { + const baseConfig = { + iconSize: 30, + iconPadding: 15, + shapeType: shapeType as MindmapIconConfig['shapeType'], + }; + + switch (shapeType) { + case 'bang': + return { ...baseConfig, iconPadding: 1 }; + case 'rect': + case 'default': + return { ...baseConfig, iconPadding: 10 }; + default: + return baseConfig; + } +} + +/** + * Calculate dimensions and label positioning for mindmap nodes with icons + */ +export function calculateMindmapDimensions( + node: Node, + bbox: any, + baseWidth: number, + baseHeight: number, + basePadding: number, + config: MindmapIconConfig +): MindmapDimensions { + const hasIcon = Boolean(node.icon); + + if (!hasIcon) { + return { + width: baseWidth, + height: baseHeight, + labelOffset: { x: -bbox.width / 2, y: -bbox.height / 2 }, + }; + } + + const { iconSize, iconPadding, shapeType } = config; + let width = baseWidth; + let height = baseHeight; + let labelXOffset = -bbox.width / 2; + const labelYOffset = -bbox.height / 2; + + switch (shapeType) { + case 'circle': { + const totalWidthNeeded = bbox.width + iconSize + iconPadding * 2; + const minRadiusWithIcon = totalWidthNeeded / 2 + basePadding; + const radius = Math.max(baseWidth / 2, minRadiusWithIcon); + width = radius * 2; + height = radius * 2; + labelXOffset = -radius + iconSize + iconPadding; + break; + } + + case 'rect': + case 'rounded': + case 'default': { + const minWidthWithIcon = bbox.width + iconSize + iconPadding * 2 + basePadding * 2; + width = Math.max(baseWidth, minWidthWithIcon); + height = Math.max(baseHeight, iconSize + basePadding * 2); + + const availableTextSpace = width - iconSize - iconPadding * 2; + labelXOffset = -width / 2 + iconSize + iconPadding + availableTextSpace / 2 - bbox.width / 2; + break; + } + + case 'bang': + case 'cloud': { + const minWidthWithIcon = bbox.width + iconSize + iconPadding * 2 + basePadding; + width = Math.max(baseWidth, minWidthWithIcon); + height = Math.max(baseHeight, iconSize + basePadding); + + const availableTextSpace = width - iconSize - iconPadding * 2; + labelXOffset = -width / 2 + iconSize + iconPadding + availableTextSpace / 2 - bbox.width / 2; + break; + } + + default: { + const minWidthWithIcon = bbox.width + iconSize + iconPadding * 2 + basePadding * 2; + width = Math.max(baseWidth, minWidthWithIcon); + height = Math.max(baseHeight, iconSize + basePadding * 2); + + const availableTextSpace = width - iconSize - iconPadding * 2; + labelXOffset = -width / 2 + iconSize + iconPadding + availableTextSpace / 2 - bbox.width / 2; + break; + } + } + + return { + width, + height, + labelOffset: { x: labelXOffset, y: labelYOffset }, + }; +} + +/** + * Insert mindmap icon into the shape SVG element + */ +export async function insertMindmapIcon( + parentElement: D3Selection, + node: Node, + config: MindmapIconConfig +): Promise { + if (!node.icon) { + return; + } + + const { iconSize, iconPadding, shapeType } = config; + const section = node.section ?? 0; + + let iconName = node.icon; + const isCssFormat = iconName.includes(' '); + + if (isCssFormat) { + iconName = iconName.replace(' ', ':'); + } + + try { + if (await isIconAvailable(iconName)) { + const iconSvg = await getIconSVG( + iconName, + { + height: iconSize, + width: iconSize, + }, + { class: 'label-icon' } + ); + + const iconElem = parentElement.append('g'); + iconElem.html(`${iconSvg}`); + + let iconX = 0; + let iconY = 0; + + switch (shapeType) { + case 'circle': { + const nodeWidth = node.width || 100; + const radius = nodeWidth / 2; + iconX = -radius + iconSize / 2 + iconPadding; + iconY = 0; + break; + } + default: { + const nodeWidth = node.width || 100; + iconX = -nodeWidth / 2 + iconSize / 2 + iconPadding; + iconY = 0; + break; + } + } + + iconElem.attr('transform', `translate(${iconX}, ${iconY})`); + iconElem.attr('style', 'color: currentColor;'); + return; + } + } catch (error) { + log.debug('SVG icon rendering failed, falling back to CSS:', error); + } + + // Fallback to CSS approach (original mindmap behavior) + const iconClass = isCssFormat ? node.icon : node.icon.replace(':', ' '); + + let iconX = 0; + const iconY = -iconSize / 2; + + switch (shapeType) { + case 'circle': { + const nodeWidth = node.width || 100; + const radius = nodeWidth / 2; + iconX = -radius + iconPadding; + break; + } + default: { + const nodeWidth = node.width || 100; + iconX = -nodeWidth / 2 + iconPadding; + break; + } + } + + const icon = parentElement + .append('foreignObject') + .attr('height', `${iconSize}px`) + .attr('width', `${iconSize}px`) + .attr('x', iconX) + .attr('y', iconY) + .attr( + 'style', + 'text-align: center; display: flex; align-items: center; justify-content: center;' + ); + + icon + .append('div') + .attr('class', 'icon-container') + .attr( + 'style', + 'width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;' + ) + .append('i') + .attr('class', `node-icon-${section} ${iconClass}`) + .attr('style', `font-size: ${iconSize}px;`); +} diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts b/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts index a962dc924..820ec5e26 100644 --- a/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts +++ b/packages/mermaid/src/diagrams/mindmap/mindmapRenderer.ts @@ -7,6 +7,7 @@ import type { LayoutData } from '../../rendering-util/types.js'; import type { FilledMindMapNode } from './mindmapTypes.js'; import defaultConfig from '../../defaultConfig.js'; import type { MindmapDB } from './mindmapDb.js'; +import { getMindmapIconConfig, insertMindmapIcon } from './mindmapIconHelper.js'; /** * Update the layout data with actual node dimensions after drawing @@ -71,6 +72,28 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj) => { // Use the unified rendering system await render(data4Layout, svg); + const genericShapes = ['hexagon', 'rect', 'rounded', 'squareRect']; + const nodesWithIcons = data4Layout.nodes.filter( + (node) => node.icon && genericShapes.includes(node.shape || '') + ); + + if (nodesWithIcons.length > 0) { + for (const node of nodesWithIcons) { + const nodeId = node.domId || node.id; + const nodeElement = svg.select(`g[id="${nodeId}"]`); + + if (!nodeElement.empty()) { + try { + const iconConfig = getMindmapIconConfig(node.shape || 'default'); + await insertMindmapIcon(nodeElement, node, iconConfig); + } catch (error) { + log.warn(`Failed to add icon to ${nodeId}:`, error); + } + } else { + log.warn(`Could not find DOM element for node ${nodeId}`); + } + } + } // Setup the view box and size of the svg element using config from data4Layout setupViewPortForSVG( diff --git a/packages/mermaid/src/rendering-util/rendering-elements/nodes.ts b/packages/mermaid/src/rendering-util/rendering-elements/nodes.ts index 7d9626ed6..5af6cd17a 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/nodes.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/nodes.ts @@ -4,95 +4,12 @@ import type { Node, NonClusterNode, ShapeRenderOptions } from '../types.js'; import type { SVGGroup } from '../../mermaid.js'; import type { D3Selection } from '../../types.js'; import type { graphlib } from 'dagre-d3-es'; -import { getIconSVG, isIconAvailable } from '../icons.js'; type ShapeHandler = (typeof shapes)[keyof typeof shapes]; type NodeElement = D3Selection | Awaited>; const nodeElems = new Map(); -async function renderNodeIcon( - parentElement: NodeElement, - node: Node, - isCircle: boolean, - section: number -) { - if (!node.icon) { - return; - } - - let iconName = node.icon; - const isCssFormat = iconName.includes(' '); - if (isCssFormat) { - iconName = iconName.replace(' ', ':'); - } - - const iconSize = 30; - const nodeWidth = node.width || 100; - - const getTransform = () => { - if (isCircle) { - const iconPadding = isCssFormat ? 15 : 20; // fallback padding - const radius = nodeWidth / 2; - return `translate(${-radius + iconSize / 2 + iconPadding}, 0)`; - } else { - return `translate(${-nodeWidth / 2 + iconSize / 2 + 10}, 0)`; - } - }; - - const createForeignObject = (x: number, y: number) => { - const foreignObjectElem = parentElement - .append('foreignObject') - .attr('width', `${iconSize}px`) - .attr('height', `${iconSize}px`) - .attr('x', x) - .attr('y', y) - .attr( - 'style', - 'display: flex; align-items: center; justify-content: center; text-align: center;' - ); - - foreignObjectElem - .append('div') - .attr('class', 'icon-container') - .attr( - 'style', - 'width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;' - ) - .append('i') - .attr( - 'class', - `node-icon-${section} ${isCssFormat ? (node.icon ?? '') : (node.icon ?? '').replace(':', ' ')}` - ) - .attr('style', `font-size: ${iconSize}px;`); - - return foreignObjectElem; - }; - - try { - if (await isIconAvailable(iconName)) { - const iconSvg = await getIconSVG( - iconName, - { width: iconSize, height: iconSize }, - { class: 'label-icon' } - ); - const iconElem = parentElement.append('g').html(`${iconSvg}`); - iconElem.attr('transform', getTransform()); - iconElem.attr('style', 'color: currentColor;'); - return; - } - } catch (error) { - log.debug('SVG icon rendering failed, falling back to CSS:', error); - } - - if (isCircle) { - const radius = nodeWidth / 2; - createForeignObject(-radius + 15, -iconSize / 2); - } else { - createForeignObject(-nodeWidth / 2 + 30, -iconSize / 2); - } -} - export async function insertNode( elem: SVGGroup, node: NonClusterNode, @@ -117,6 +34,7 @@ export async function insertNode( } if (node.link) { + // Add link when appropriate let target; if (renderOptions.config.securityLevel === 'sandbox') { target = '_top'; @@ -132,14 +50,6 @@ export async function insertNode( el = await shapeHandler(elem, node, renderOptions); newEl = el; } - - if (node.icon && newEl) { - const section = node.section || 0; - const isCircle = node.shape === 'circle' || node.shape === 'mindmapCircle'; - - await renderNodeIcon(newEl, node, isCircle, section); - } - if (node.tooltip) { el.attr('title', node.tooltip); } diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/bang.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/bang.ts index 09d163d1a..3fa424d44 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/bang.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/bang.ts @@ -3,13 +3,16 @@ import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; import intersect from '../intersect/index.js'; import type { Node } from '../../types.js'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; import type { D3Selection } from '../../../types.js'; import { handleUndefinedAttr } from '../../../utils.js'; import type { Bounds, Point } from '../../../types.js'; -import rough from 'roughjs'; +import { + calculateMindmapDimensions, + getMindmapIconConfig, + insertMindmapIcon, +} from '../../../diagrams/mindmap/mindmapIconHelper.js'; -const ICON_SIZE = 30; -const ICON_PADDING = 1; export async function bang(parent: D3Selection, node: Node) { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; @@ -19,14 +22,21 @@ export async function bang(parent: D3Selection, getNodeClasses(node) ); - let w = bbox.width + 10 * halfPadding; - let h = bbox.height + 8 * halfPadding; + const baseWidth = bbox.width + 10 * halfPadding; + const baseHeight = bbox.height + 8 * halfPadding; - if (node.icon) { - const minWidthWithIcon = bbox.width + ICON_SIZE + ICON_PADDING * 2 + 10 * halfPadding; - w = Math.max(w, minWidthWithIcon); - h = Math.max(h, ICON_SIZE + 8 * halfPadding); - } + const iconConfig = getMindmapIconConfig('bang'); + const dimensions = calculateMindmapDimensions( + node, + bbox, + baseWidth, + baseHeight, + halfPadding, + iconConfig + ); + + const w = dimensions.width; + const h = dimensions.height; node.width = w; node.height = h; @@ -36,21 +46,14 @@ export async function bang(parent: D3Selection, const effectiveWidth = Math.max(w, minWidth); const effectiveHeight = Math.max(h, minHeight); - let labelXOffset = -bbox.width / 2; - if (node.icon) { - const iconSpace = ICON_SIZE + ICON_PADDING; - const remainingWidth = effectiveWidth - iconSpace; - labelXOffset = -effectiveWidth / 2 + iconSpace + (remainingWidth - bbox.width) / 2; - label.attr('transform', `translate(${labelXOffset}, ${-bbox.height / 2})`); - } + label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`); const r = 0.15 * effectiveWidth; - let bangElem; const path = `M0 0 - a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${-1 * effectiveHeight * 0.1} - a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0} - a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0} - a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${effectiveHeight * 0.1} + a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${-1 * effectiveHeight * 0.1} + a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0} + a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${0} + a${r},${r} 1 0,0 ${effectiveWidth * 0.25},${effectiveHeight * 0.1} a${r},${r} 1 0,0 ${effectiveWidth * 0.15},${effectiveHeight * 0.33} a${r * 0.8},${r * 0.8} 1 0,0 0,${effectiveHeight * 0.34} @@ -66,6 +69,7 @@ export async function bang(parent: D3Selection, a${r},${r} 1 0,0 ${effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33} H0 V0 Z`; + let bangElem; if (node.look === 'handDrawn') { // @ts-expect-error -- Passing a D3.Selection seems to work for some reason const rc = rough.svg(shapeSvg); @@ -86,6 +90,10 @@ export async function bang(parent: D3Selection, // Translate the path (center the shape) bangElem.attr('transform', `translate(${-effectiveWidth / 2}, ${-effectiveHeight / 2})`); + if (node.icon) { + await insertMindmapIcon(shapeSvg, node, iconConfig); + } + updateNodeBounds(node, bangElem); node.calcIntersect = function (bounds: Bounds, point: Point) { return intersect.rect(bounds, point); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts index 1c9b8aae1..749c0c578 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts @@ -1,3 +1,4 @@ +import rough from 'roughjs'; import { log } from '../../../logger.js'; import type { Bounds, D3Selection, Point } from '../../../types.js'; import { handleUndefinedAttr } from '../../../utils.js'; @@ -5,10 +6,6 @@ import type { MindmapOptions, Node, ShapeRenderOptions } from '../../types.js'; import intersect from '../intersect/index.js'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; -import rough from 'roughjs'; - -const ICON_SIZE = 30; -const ICON_PADDING = 20; export async function circle( parent: D3Selection, @@ -25,21 +22,19 @@ export async function circle( ); const padding = options?.padding ?? halfPadding; - let radius = bbox.width / 2 + padding; - let labelXOffset = -bbox.width / 2; - const labelYOffset = -bbox.height / 2; - if (node.icon) { - const totalWidthNeeded = bbox.width + ICON_SIZE + ICON_PADDING * 2; - const minRadiusWithIcon = totalWidthNeeded / 2 + padding; - radius = Math.max(radius, minRadiusWithIcon); - labelXOffset = -radius + ICON_SIZE + ICON_PADDING; - label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`); - } + const radius = bbox.width / 2 + padding; node.width = radius * 2; node.height = radius * 2; + + const labelXOffset = -bbox.width / 2; + const labelYOffset = -bbox.height / 2; + if (node.icon) { + label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`); + } let circleElem; + const { cssStyles } = node; if (node.look === 'handDrawn') { // @ts-expect-error -- Passing a D3.Selection seems to work for some reason @@ -48,9 +43,7 @@ export async function circle( const roughNode = rc.circle(0, 0, radius * 2, options); circleElem = shapeSvg.insert(() => roughNode, ':first-child'); - circleElem - .attr('class', 'basic label-container') - .attr('style', handleUndefinedAttr(node.cssStyles)); + circleElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles)); } else { circleElem = shapeSvg .insert('circle', ':first-child') diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/cloud.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/cloud.ts index 7c46a7bfe..040cc8569 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/cloud.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/cloud.ts @@ -1,3 +1,4 @@ +import rough from 'roughjs'; import { log } from '../../../logger.js'; import type { Bounds, D3Selection, Point } from '../../../types.js'; import { handleUndefinedAttr } from '../../../utils.js'; @@ -5,10 +6,12 @@ import type { Node } from '../../types.js'; import intersect from '../intersect/index.js'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; -import rough from 'roughjs'; +import { + getMindmapIconConfig, + calculateMindmapDimensions, + insertMindmapIcon, +} from '../../../diagrams/mindmap/mindmapIconHelper.js'; -const ICON_SIZE = 30; -const ICON_PADDING = 15; export async function cloud(parent: D3Selection, node: Node) { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; @@ -19,26 +22,26 @@ export async function cloud(parent: D3Selection getNodeClasses(node) ); - let w = bbox.width + 2 * halfPadding; - let h = bbox.height + 2 * halfPadding; + const baseWidth = bbox.width + 2 * halfPadding; + const baseHeight = bbox.height + 2 * halfPadding; - let labelXOffset = -bbox.width / 2; - const labelYOffset = -bbox.height / 2; - if (node.icon) { - const minWidthWithIcon = bbox.width + ICON_SIZE + ICON_PADDING * 2 + 2 * halfPadding; - w = Math.max(w, minWidthWithIcon); - h = Math.max(h, ICON_SIZE + 2 * halfPadding); + const iconConfig = getMindmapIconConfig('cloud'); + const dimensions = calculateMindmapDimensions( + node, + bbox, + baseWidth, + baseHeight, + halfPadding, + iconConfig + ); - node.width = w; - node.height = h; + const w = dimensions.width; + const h = dimensions.height; - const availableTextSpace = w - ICON_SIZE - ICON_PADDING * 2; - labelXOffset = -w / 2 + ICON_SIZE + ICON_PADDING + availableTextSpace / 2 - bbox.width / 2; - label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`); - } else { - node.width = w; - node.height = h; - } + node.width = w; + node.height = h; + + label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`); // Cloud radii const r1 = 0.15 * w; @@ -46,6 +49,7 @@ export async function cloud(parent: D3Selection const r3 = 0.35 * w; const r4 = 0.2 * w; + const { cssStyles } = node; let cloudElem; // Cloud path @@ -71,9 +75,7 @@ export async function cloud(parent: D3Selection const options = userNodeOverrides(node, {}); const roughNode = rc.path(path, options); cloudElem = shapeSvg.insert(() => roughNode, ':first-child'); - cloudElem - .attr('class', 'basic label-container') - .attr('style', handleUndefinedAttr(node.cssStyles)); + cloudElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles)); } else { cloudElem = shapeSvg .insert('path', ':first-child') @@ -85,6 +87,10 @@ export async function cloud(parent: D3Selection // Center the shape cloudElem.attr('transform', `translate(${-w / 2}, ${-h / 2})`); + if (node.icon) { + await insertMindmapIcon(shapeSvg, node, iconConfig); + } + updateNodeBounds(node, cloudElem); node.calcIntersect = function (bounds: Bounds, point: Point) { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/defaultMindmapNode.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/defaultMindmapNode.ts index 75ae7306c..6a3a7bf9e 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/defaultMindmapNode.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/defaultMindmapNode.ts @@ -3,9 +3,11 @@ import type { Node } from '../../types.js'; import intersect from '../intersect/index.js'; import { styles2String } from './handDrawnShapeStyles.js'; import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; - -const ICON_SIZE = 30; -const ICON_PADDING = 15; +import { + getMindmapIconConfig, + calculateMindmapDimensions, + insertMindmapIcon, +} from '../../../diagrams/mindmap/mindmapIconHelper.js'; export async function defaultMindmapNode( parent: D3Selection, @@ -20,24 +22,26 @@ export async function defaultMindmapNode( getNodeClasses(node) ); - let w = bbox.width + 8 * halfPadding; - let h = bbox.height + 2 * halfPadding; + const baseWidth = bbox.width + 8 * halfPadding; + const baseHeight = bbox.height + 2 * halfPadding; - if (node.icon) { - const minWidthWithIcon = bbox.width + ICON_SIZE + ICON_PADDING * 2 + 8 * halfPadding; - w = Math.max(w, minWidthWithIcon); - h = Math.max(h, ICON_SIZE + 2 * halfPadding); + const iconConfig = getMindmapIconConfig('default'); + const dimensions = calculateMindmapDimensions( + node, + bbox, + baseWidth, + baseHeight, + halfPadding, + iconConfig + ); - node.width = w; - node.height = h; + const w = dimensions.width; + const h = dimensions.height; - const availableTextSpace = w - ICON_SIZE - ICON_PADDING * 2; - const labelXOffset = - -w / 2 + ICON_SIZE + ICON_PADDING + availableTextSpace / 2 - bbox.width / 2; - label.attr('transform', `translate(${labelXOffset}, ${-bbox.height / 2})`); - } else { - label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`); - } + node.width = w; + node.height = h; + + label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`); const RD = 5; const rectPath = `M${-w / 2} ${h / 2 - RD} @@ -68,6 +72,10 @@ export async function defaultMindmapNode( shapeSvg.append(() => label.node()); + if (node.icon) { + await insertMindmapIcon(shapeSvg, node, iconConfig); + } + updateNodeBounds(node, bg); node.calcIntersect = function (bounds: Bounds, point: Point) { return intersect.rect(bounds, point); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts index f7bb3e99d..9780a6cd8 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts @@ -8,9 +8,6 @@ import type { D3Selection } from '../../../types.js'; import { handleUndefinedAttr } from '../../../utils.js'; import type { Bounds, Point } from '../../../types.js'; -const ICON_SIZE = 30; -const ICON_PADDING = 10; - export async function drawRect( parent: D3Selection, node: Node, @@ -21,26 +18,17 @@ export async function drawRect( const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); - let totalWidth = Math.max(bbox.width + options.labelPaddingX * 2, node?.width || 0); - let totalHeight = Math.max(bbox.height + options.labelPaddingY * 2, node?.height || 0); - let labelXOffset = -bbox.width / 2; + const totalWidth = Math.max(bbox.width + options.labelPaddingX * 2, node?.width || 0); + const totalHeight = Math.max(bbox.height + options.labelPaddingY * 2, node?.height || 0); + + node.width = totalWidth; + node.height = totalHeight; + + const labelXOffset = -bbox.width / 2; + const labelYOffset = -bbox.height / 2; - if (node.icon) { - const minWidthWithIcon = bbox.width + ICON_SIZE + ICON_PADDING * 2 + options.labelPaddingX * 2; - totalWidth = Math.max(totalWidth, minWidthWithIcon); - totalHeight = Math.max(totalHeight, ICON_SIZE + options.labelPaddingY * 2); - - node.width = totalWidth; - node.height = totalHeight; - - const availableTextSpace = totalWidth - ICON_SIZE - ICON_PADDING * 2; - labelXOffset = - -totalWidth / 2 + ICON_SIZE + ICON_PADDING + availableTextSpace / 2 - bbox.width / 2; label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`); - } else { - node.width = totalWidth; - node.height = totalHeight; } const x = -totalWidth / 2; const y = -totalHeight / 2; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts index 4952169dd..2ab2483f0 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts @@ -2,11 +2,8 @@ import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } f import intersect from '../intersect/index.js'; import type { Node } from '../../types.js'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; -import type { D3Selection } from '../../../types.js'; import rough from 'roughjs'; - -const ICON_SIZE = 30; -const ICON_PADDING = 1; +import type { D3Selection } from '../../../types.js'; export const createHexagonPathD = ( x: number, @@ -29,27 +26,21 @@ export const createHexagonPathD = ( export async function hexagon(parent: D3Selection, node: Node) { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); - let h = bbox.height + (node.padding ?? 0); - let w = bbox.width + (node.padding ?? 0) * 2.5; + const h = bbox.height + (node.padding ?? 0); + const w = bbox.width + (node.padding ?? 0) * 2.5; + + node.width = w; + node.height = h; + + const labelXOffset = -bbox.width / 2; - let labelXOffset = -bbox.width / 2; const labelYOffset = -bbox.height / 2; + if (node.icon) { - const minWidthWithIcon = bbox.width + ICON_SIZE + ICON_PADDING * 2 + (node.padding ?? 0) * 2.5; - w = Math.max(w, minWidthWithIcon); - h = Math.max(h, ICON_SIZE + (node.padding ?? 0)); - - node.width = w; - node.height = h; - - const availableTextSpace = w - ICON_SIZE - ICON_PADDING * 2; - labelXOffset = -w / 2 + ICON_SIZE + ICON_PADDING + availableTextSpace / 2 - bbox.width / 2; label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`); - } else { - node.width = w; - node.height = h; } const { cssStyles } = node; // @ts-expect-error -- Passing a D3.Selection seems to work for some reason diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/mindmapCircle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/mindmapCircle.ts index 5b9dab3fd..7df30bea8 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/mindmapCircle.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/mindmapCircle.ts @@ -1,13 +1,57 @@ -import { circle } from './circle.js'; -import type { Node, MindmapOptions } from '../../types.js'; +import type { Node } from '../../types.js'; import type { D3Selection } from '../../../types.js'; +import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; +import { styles2String } from './handDrawnShapeStyles.js'; +import intersect from '../intersect/index.js'; +import { + getMindmapIconConfig, + calculateMindmapDimensions, + insertMindmapIcon, +} from '../../../diagrams/mindmap/mindmapIconHelper.js'; export async function mindmapCircle( parent: D3Selection, node: Node ) { - const options = { - padding: node.padding ?? 0, - } as MindmapOptions; - return circle(parent, node, options); + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const halfPadding = (node.padding ?? 0) / 2; + + const iconConfig = getMindmapIconConfig('circle'); + const baseRadius = bbox.width / 2 + halfPadding; + const baseDiameter = baseRadius * 2; + + const dimensions = calculateMindmapDimensions( + node, + bbox, + baseDiameter, + baseDiameter, + halfPadding, + iconConfig + ); + + const radius = dimensions.width / 2; + node.width = dimensions.width; + node.height = dimensions.height; + + label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`); + + const circle = shapeSvg + .insert('circle', ':first-child') + .attr('r', radius) + .attr('class', 'basic label-container') + .attr('style', styles2String(node).nodeStyles); + + shapeSvg.append(() => label.node()); + + if (node.icon) { + await insertMindmapIcon(shapeSvg, node, iconConfig); + } + + updateNodeBounds(node, circle); + + node.intersect = function (point) { + return intersect.circle(node, radius, point); + }; + + return shapeSvg; } diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/roundedRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/roundedRect.ts index 0502589dc..741def133 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/roundedRect.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/roundedRect.ts @@ -85,9 +85,6 @@ export function generateArcPoints( return points; } -const ICON_SIZE = 30; -const ICON_PADDING = 15; - export async function roundedRect( parent: D3Selection, node: Node @@ -99,21 +96,12 @@ export async function roundedRect( const labelPaddingX = node?.padding ?? 0; const labelPaddingY = node?.padding ?? 0; - let w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2; - let h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2; + const w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2; + const h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2; - let labelXOffset = -bbox.width / 2; + const labelXOffset = -bbox.width / 2; if (node.icon) { - const minWidthWithIcon = bbox.width + ICON_SIZE + ICON_PADDING * 2 + labelPaddingX * 2; - w = Math.max(w, minWidthWithIcon); - h = Math.max(h, ICON_SIZE + labelPaddingY * 2); - - node.width = w; - node.height = h; - - const availableTextSpace = w - ICON_SIZE - ICON_PADDING * 2; - labelXOffset = -w / 2 + ICON_SIZE + ICON_PADDING + availableTextSpace / 2 - bbox.width / 2; label.attr('transform', `translate(${labelXOffset}, ${-bbox.height / 2})`); }