mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-04 14:49:46 +02:00
fix: move mindmap icons logic into a separate file
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
This commit is contained in:
222
packages/mermaid/src/diagrams/mindmap/mindmapIconHelper.ts
Normal file
222
packages/mermaid/src/diagrams/mindmap/mindmapIconHelper.ts
Normal 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;`);
|
||||
}
|
@@ -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(
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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')
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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})`);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user