fix: move mindmap icons logic into a separate file

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
This commit is contained in:
darshanr0107
2025-10-03 16:28:25 +05:30
parent 81a43e3d7c
commit 9fdd34c55c
11 changed files with 412 additions and 231 deletions

View File

@@ -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<SVGGraphicsElement>,
node: Node,
config: MindmapIconConfig
): Promise<void> {
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(`<g>${iconSvg}</g>`);
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;`);
}

View File

@@ -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(

View File

@@ -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<SVGAElement> | Awaited<ReturnType<ShapeHandler>>;
const nodeElems = new Map<string, NodeElement>();
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(`<g>${iconSvg}</g>`);
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);
}

View File

@@ -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<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
@@ -19,14 +22,21 @@ export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>,
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<T extends SVGGraphicsElement>(parent: D3Selection<T>,
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<T extends SVGGraphicsElement>(parent: D3Selection<T>,
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<T extends SVGGraphicsElement>(parent: D3Selection<T>,
// 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);

View File

@@ -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<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
@@ -25,21 +22,19 @@ export async function circle<T extends SVGGraphicsElement>(
);
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<T extends SVGGraphicsElement>(
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')

View File

@@ -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<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
@@ -19,26 +22,26 @@ export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T>
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<T extends SVGGraphicsElement>(parent: D3Selection<T>
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<T extends SVGGraphicsElement>(parent: D3Selection<T>
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<T extends SVGGraphicsElement>(parent: D3Selection<T>
// 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) {

View File

@@ -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<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
@@ -20,24 +22,26 @@ export async function defaultMindmapNode<T extends SVGGraphicsElement>(
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<T extends SVGGraphicsElement>(
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);

View File

@@ -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<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node,
@@ -21,26 +18,17 @@ export async function drawRect<T extends SVGGraphicsElement>(
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;

View File

@@ -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<T extends SVGGraphicsElement>(parent: D3Selection<T>, 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

View File

@@ -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<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
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;
}

View File

@@ -85,9 +85,6 @@ export function generateArcPoints(
return points;
}
const ICON_SIZE = 30;
const ICON_PADDING = 15;
export async function roundedRect<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node
@@ -99,21 +96,12 @@ export async function roundedRect<T extends SVGGraphicsElement>(
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})`);
}