mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-10-06 07:39:48 +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 type { FilledMindMapNode } from './mindmapTypes.js';
|
||||||
import defaultConfig from '../../defaultConfig.js';
|
import defaultConfig from '../../defaultConfig.js';
|
||||||
import type { MindmapDB } from './mindmapDb.js';
|
import type { MindmapDB } from './mindmapDb.js';
|
||||||
|
import { getMindmapIconConfig, insertMindmapIcon } from './mindmapIconHelper.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the layout data with actual node dimensions after drawing
|
* 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
|
// Use the unified rendering system
|
||||||
await render(data4Layout, svg);
|
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
|
// Setup the view box and size of the svg element using config from data4Layout
|
||||||
setupViewPortForSVG(
|
setupViewPortForSVG(
|
||||||
|
@@ -4,95 +4,12 @@ import type { Node, NonClusterNode, ShapeRenderOptions } from '../types.js';
|
|||||||
import type { SVGGroup } from '../../mermaid.js';
|
import type { SVGGroup } from '../../mermaid.js';
|
||||||
import type { D3Selection } from '../../types.js';
|
import type { D3Selection } from '../../types.js';
|
||||||
import type { graphlib } from 'dagre-d3-es';
|
import type { graphlib } from 'dagre-d3-es';
|
||||||
import { getIconSVG, isIconAvailable } from '../icons.js';
|
|
||||||
|
|
||||||
type ShapeHandler = (typeof shapes)[keyof typeof shapes];
|
type ShapeHandler = (typeof shapes)[keyof typeof shapes];
|
||||||
type NodeElement = D3Selection<SVGAElement> | Awaited<ReturnType<ShapeHandler>>;
|
type NodeElement = D3Selection<SVGAElement> | Awaited<ReturnType<ShapeHandler>>;
|
||||||
|
|
||||||
const nodeElems = new Map<string, NodeElement>();
|
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(
|
export async function insertNode(
|
||||||
elem: SVGGroup,
|
elem: SVGGroup,
|
||||||
node: NonClusterNode,
|
node: NonClusterNode,
|
||||||
@@ -117,6 +34,7 @@ export async function insertNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.link) {
|
if (node.link) {
|
||||||
|
// Add link when appropriate
|
||||||
let target;
|
let target;
|
||||||
if (renderOptions.config.securityLevel === 'sandbox') {
|
if (renderOptions.config.securityLevel === 'sandbox') {
|
||||||
target = '_top';
|
target = '_top';
|
||||||
@@ -132,14 +50,6 @@ export async function insertNode(
|
|||||||
el = await shapeHandler(elem, node, renderOptions);
|
el = await shapeHandler(elem, node, renderOptions);
|
||||||
newEl = el;
|
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) {
|
if (node.tooltip) {
|
||||||
el.attr('title', 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 intersect from '../intersect/index.js';
|
||||||
import type { Node } from '../../types.js';
|
import type { Node } from '../../types.js';
|
||||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||||
|
import rough from 'roughjs';
|
||||||
import type { D3Selection } from '../../../types.js';
|
import type { D3Selection } from '../../../types.js';
|
||||||
import { handleUndefinedAttr } from '../../../utils.js';
|
import { handleUndefinedAttr } from '../../../utils.js';
|
||||||
import type { Bounds, Point } from '../../../types.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) {
|
export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||||
const { labelStyles, nodeStyles } = styles2String(node);
|
const { labelStyles, nodeStyles } = styles2String(node);
|
||||||
node.labelStyle = labelStyles;
|
node.labelStyle = labelStyles;
|
||||||
@@ -19,14 +22,21 @@ export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>,
|
|||||||
getNodeClasses(node)
|
getNodeClasses(node)
|
||||||
);
|
);
|
||||||
|
|
||||||
let w = bbox.width + 10 * halfPadding;
|
const baseWidth = bbox.width + 10 * halfPadding;
|
||||||
let h = bbox.height + 8 * halfPadding;
|
const baseHeight = bbox.height + 8 * halfPadding;
|
||||||
|
|
||||||
if (node.icon) {
|
const iconConfig = getMindmapIconConfig('bang');
|
||||||
const minWidthWithIcon = bbox.width + ICON_SIZE + ICON_PADDING * 2 + 10 * halfPadding;
|
const dimensions = calculateMindmapDimensions(
|
||||||
w = Math.max(w, minWidthWithIcon);
|
node,
|
||||||
h = Math.max(h, ICON_SIZE + 8 * halfPadding);
|
bbox,
|
||||||
}
|
baseWidth,
|
||||||
|
baseHeight,
|
||||||
|
halfPadding,
|
||||||
|
iconConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
const w = dimensions.width;
|
||||||
|
const h = dimensions.height;
|
||||||
|
|
||||||
node.width = w;
|
node.width = w;
|
||||||
node.height = h;
|
node.height = h;
|
||||||
@@ -36,21 +46,14 @@ export async function bang<T extends SVGGraphicsElement>(parent: D3Selection<T>,
|
|||||||
const effectiveWidth = Math.max(w, minWidth);
|
const effectiveWidth = Math.max(w, minWidth);
|
||||||
const effectiveHeight = Math.max(h, minHeight);
|
const effectiveHeight = Math.max(h, minHeight);
|
||||||
|
|
||||||
let labelXOffset = -bbox.width / 2;
|
label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`);
|
||||||
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})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = 0.15 * effectiveWidth;
|
const r = 0.15 * effectiveWidth;
|
||||||
let bangElem;
|
|
||||||
const path = `M0 0
|
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},${-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},${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},${effectiveHeight * 0.1}
|
||||||
|
|
||||||
a${r},${r} 1 0,0 ${effectiveWidth * 0.15},${effectiveHeight * 0.33}
|
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}
|
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}
|
a${r},${r} 1 0,0 ${effectiveWidth * 0.1},${-1 * effectiveHeight * 0.33}
|
||||||
H0 V0 Z`;
|
H0 V0 Z`;
|
||||||
|
|
||||||
|
let bangElem;
|
||||||
if (node.look === 'handDrawn') {
|
if (node.look === 'handDrawn') {
|
||||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
||||||
const rc = rough.svg(shapeSvg);
|
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)
|
// Translate the path (center the shape)
|
||||||
bangElem.attr('transform', `translate(${-effectiveWidth / 2}, ${-effectiveHeight / 2})`);
|
bangElem.attr('transform', `translate(${-effectiveWidth / 2}, ${-effectiveHeight / 2})`);
|
||||||
|
|
||||||
|
if (node.icon) {
|
||||||
|
await insertMindmapIcon(shapeSvg, node, iconConfig);
|
||||||
|
}
|
||||||
|
|
||||||
updateNodeBounds(node, bangElem);
|
updateNodeBounds(node, bangElem);
|
||||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||||
return intersect.rect(bounds, point);
|
return intersect.rect(bounds, point);
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import rough from 'roughjs';
|
||||||
import { log } from '../../../logger.js';
|
import { log } from '../../../logger.js';
|
||||||
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
||||||
import { handleUndefinedAttr } from '../../../utils.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 intersect from '../intersect/index.js';
|
||||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||||
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.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>(
|
export async function circle<T extends SVGGraphicsElement>(
|
||||||
parent: D3Selection<T>,
|
parent: D3Selection<T>,
|
||||||
@@ -25,21 +22,19 @@ export async function circle<T extends SVGGraphicsElement>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const padding = options?.padding ?? halfPadding;
|
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 radius = bbox.width / 2 + padding;
|
||||||
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})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
node.width = radius * 2;
|
node.width = radius * 2;
|
||||||
node.height = 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;
|
let circleElem;
|
||||||
|
const { cssStyles } = node;
|
||||||
|
|
||||||
if (node.look === 'handDrawn') {
|
if (node.look === 'handDrawn') {
|
||||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
// @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);
|
const roughNode = rc.circle(0, 0, radius * 2, options);
|
||||||
|
|
||||||
circleElem = shapeSvg.insert(() => roughNode, ':first-child');
|
circleElem = shapeSvg.insert(() => roughNode, ':first-child');
|
||||||
circleElem
|
circleElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
|
||||||
.attr('class', 'basic label-container')
|
|
||||||
.attr('style', handleUndefinedAttr(node.cssStyles));
|
|
||||||
} else {
|
} else {
|
||||||
circleElem = shapeSvg
|
circleElem = shapeSvg
|
||||||
.insert('circle', ':first-child')
|
.insert('circle', ':first-child')
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import rough from 'roughjs';
|
||||||
import { log } from '../../../logger.js';
|
import { log } from '../../../logger.js';
|
||||||
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
import type { Bounds, D3Selection, Point } from '../../../types.js';
|
||||||
import { handleUndefinedAttr } from '../../../utils.js';
|
import { handleUndefinedAttr } from '../../../utils.js';
|
||||||
@@ -5,10 +6,12 @@ import type { Node } from '../../types.js';
|
|||||||
import intersect from '../intersect/index.js';
|
import intersect from '../intersect/index.js';
|
||||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||||
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.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) {
|
export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||||
const { labelStyles, nodeStyles } = styles2String(node);
|
const { labelStyles, nodeStyles } = styles2String(node);
|
||||||
node.labelStyle = labelStyles;
|
node.labelStyle = labelStyles;
|
||||||
@@ -19,26 +22,26 @@ export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T>
|
|||||||
getNodeClasses(node)
|
getNodeClasses(node)
|
||||||
);
|
);
|
||||||
|
|
||||||
let w = bbox.width + 2 * halfPadding;
|
const baseWidth = bbox.width + 2 * halfPadding;
|
||||||
let h = bbox.height + 2 * halfPadding;
|
const baseHeight = bbox.height + 2 * halfPadding;
|
||||||
|
|
||||||
let labelXOffset = -bbox.width / 2;
|
const iconConfig = getMindmapIconConfig('cloud');
|
||||||
const labelYOffset = -bbox.height / 2;
|
const dimensions = calculateMindmapDimensions(
|
||||||
if (node.icon) {
|
node,
|
||||||
const minWidthWithIcon = bbox.width + ICON_SIZE + ICON_PADDING * 2 + 2 * halfPadding;
|
bbox,
|
||||||
w = Math.max(w, minWidthWithIcon);
|
baseWidth,
|
||||||
h = Math.max(h, ICON_SIZE + 2 * halfPadding);
|
baseHeight,
|
||||||
|
halfPadding,
|
||||||
|
iconConfig
|
||||||
|
);
|
||||||
|
|
||||||
node.width = w;
|
const w = dimensions.width;
|
||||||
node.height = h;
|
const h = dimensions.height;
|
||||||
|
|
||||||
const availableTextSpace = w - ICON_SIZE - ICON_PADDING * 2;
|
node.width = w;
|
||||||
labelXOffset = -w / 2 + ICON_SIZE + ICON_PADDING + availableTextSpace / 2 - bbox.width / 2;
|
node.height = h;
|
||||||
label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`);
|
|
||||||
} else {
|
label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`);
|
||||||
node.width = w;
|
|
||||||
node.height = h;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cloud radii
|
// Cloud radii
|
||||||
const r1 = 0.15 * w;
|
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 r3 = 0.35 * w;
|
||||||
const r4 = 0.2 * w;
|
const r4 = 0.2 * w;
|
||||||
|
|
||||||
|
const { cssStyles } = node;
|
||||||
let cloudElem;
|
let cloudElem;
|
||||||
|
|
||||||
// Cloud path
|
// Cloud path
|
||||||
@@ -71,9 +75,7 @@ export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T>
|
|||||||
const options = userNodeOverrides(node, {});
|
const options = userNodeOverrides(node, {});
|
||||||
const roughNode = rc.path(path, options);
|
const roughNode = rc.path(path, options);
|
||||||
cloudElem = shapeSvg.insert(() => roughNode, ':first-child');
|
cloudElem = shapeSvg.insert(() => roughNode, ':first-child');
|
||||||
cloudElem
|
cloudElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
|
||||||
.attr('class', 'basic label-container')
|
|
||||||
.attr('style', handleUndefinedAttr(node.cssStyles));
|
|
||||||
} else {
|
} else {
|
||||||
cloudElem = shapeSvg
|
cloudElem = shapeSvg
|
||||||
.insert('path', ':first-child')
|
.insert('path', ':first-child')
|
||||||
@@ -85,6 +87,10 @@ export async function cloud<T extends SVGGraphicsElement>(parent: D3Selection<T>
|
|||||||
// Center the shape
|
// Center the shape
|
||||||
cloudElem.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
|
cloudElem.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
|
||||||
|
|
||||||
|
if (node.icon) {
|
||||||
|
await insertMindmapIcon(shapeSvg, node, iconConfig);
|
||||||
|
}
|
||||||
|
|
||||||
updateNodeBounds(node, cloudElem);
|
updateNodeBounds(node, cloudElem);
|
||||||
|
|
||||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||||
|
@@ -3,9 +3,11 @@ import type { Node } from '../../types.js';
|
|||||||
import intersect from '../intersect/index.js';
|
import intersect from '../intersect/index.js';
|
||||||
import { styles2String } from './handDrawnShapeStyles.js';
|
import { styles2String } from './handDrawnShapeStyles.js';
|
||||||
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
|
||||||
|
import {
|
||||||
const ICON_SIZE = 30;
|
getMindmapIconConfig,
|
||||||
const ICON_PADDING = 15;
|
calculateMindmapDimensions,
|
||||||
|
insertMindmapIcon,
|
||||||
|
} from '../../../diagrams/mindmap/mindmapIconHelper.js';
|
||||||
|
|
||||||
export async function defaultMindmapNode<T extends SVGGraphicsElement>(
|
export async function defaultMindmapNode<T extends SVGGraphicsElement>(
|
||||||
parent: D3Selection<T>,
|
parent: D3Selection<T>,
|
||||||
@@ -20,24 +22,26 @@ export async function defaultMindmapNode<T extends SVGGraphicsElement>(
|
|||||||
getNodeClasses(node)
|
getNodeClasses(node)
|
||||||
);
|
);
|
||||||
|
|
||||||
let w = bbox.width + 8 * halfPadding;
|
const baseWidth = bbox.width + 8 * halfPadding;
|
||||||
let h = bbox.height + 2 * halfPadding;
|
const baseHeight = bbox.height + 2 * halfPadding;
|
||||||
|
|
||||||
if (node.icon) {
|
const iconConfig = getMindmapIconConfig('default');
|
||||||
const minWidthWithIcon = bbox.width + ICON_SIZE + ICON_PADDING * 2 + 8 * halfPadding;
|
const dimensions = calculateMindmapDimensions(
|
||||||
w = Math.max(w, minWidthWithIcon);
|
node,
|
||||||
h = Math.max(h, ICON_SIZE + 2 * halfPadding);
|
bbox,
|
||||||
|
baseWidth,
|
||||||
|
baseHeight,
|
||||||
|
halfPadding,
|
||||||
|
iconConfig
|
||||||
|
);
|
||||||
|
|
||||||
node.width = w;
|
const w = dimensions.width;
|
||||||
node.height = h;
|
const h = dimensions.height;
|
||||||
|
|
||||||
const availableTextSpace = w - ICON_SIZE - ICON_PADDING * 2;
|
node.width = w;
|
||||||
const labelXOffset =
|
node.height = h;
|
||||||
-w / 2 + ICON_SIZE + ICON_PADDING + availableTextSpace / 2 - bbox.width / 2;
|
|
||||||
label.attr('transform', `translate(${labelXOffset}, ${-bbox.height / 2})`);
|
label.attr('transform', `translate(${dimensions.labelOffset.x}, ${dimensions.labelOffset.y})`);
|
||||||
} else {
|
|
||||||
label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const RD = 5;
|
const RD = 5;
|
||||||
const rectPath = `M${-w / 2} ${h / 2 - RD}
|
const rectPath = `M${-w / 2} ${h / 2 - RD}
|
||||||
@@ -68,6 +72,10 @@ export async function defaultMindmapNode<T extends SVGGraphicsElement>(
|
|||||||
|
|
||||||
shapeSvg.append(() => label.node());
|
shapeSvg.append(() => label.node());
|
||||||
|
|
||||||
|
if (node.icon) {
|
||||||
|
await insertMindmapIcon(shapeSvg, node, iconConfig);
|
||||||
|
}
|
||||||
|
|
||||||
updateNodeBounds(node, bg);
|
updateNodeBounds(node, bg);
|
||||||
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
node.calcIntersect = function (bounds: Bounds, point: Point) {
|
||||||
return intersect.rect(bounds, point);
|
return intersect.rect(bounds, point);
|
||||||
|
@@ -8,9 +8,6 @@ import type { D3Selection } from '../../../types.js';
|
|||||||
import { handleUndefinedAttr } from '../../../utils.js';
|
import { handleUndefinedAttr } from '../../../utils.js';
|
||||||
import type { Bounds, Point } from '../../../types.js';
|
import type { Bounds, Point } from '../../../types.js';
|
||||||
|
|
||||||
const ICON_SIZE = 30;
|
|
||||||
const ICON_PADDING = 10;
|
|
||||||
|
|
||||||
export async function drawRect<T extends SVGGraphicsElement>(
|
export async function drawRect<T extends SVGGraphicsElement>(
|
||||||
parent: D3Selection<T>,
|
parent: D3Selection<T>,
|
||||||
node: Node,
|
node: Node,
|
||||||
@@ -21,26 +18,17 @@ export async function drawRect<T extends SVGGraphicsElement>(
|
|||||||
|
|
||||||
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
|
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
|
||||||
|
|
||||||
let totalWidth = Math.max(bbox.width + options.labelPaddingX * 2, node?.width || 0);
|
const totalWidth = Math.max(bbox.width + options.labelPaddingX * 2, node?.width || 0);
|
||||||
let totalHeight = Math.max(bbox.height + options.labelPaddingY * 2, node?.height || 0);
|
const totalHeight = Math.max(bbox.height + options.labelPaddingY * 2, node?.height || 0);
|
||||||
let labelXOffset = -bbox.width / 2;
|
|
||||||
|
node.width = totalWidth;
|
||||||
|
node.height = totalHeight;
|
||||||
|
|
||||||
|
const labelXOffset = -bbox.width / 2;
|
||||||
|
|
||||||
const labelYOffset = -bbox.height / 2;
|
const labelYOffset = -bbox.height / 2;
|
||||||
|
|
||||||
if (node.icon) {
|
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})`);
|
label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`);
|
||||||
} else {
|
|
||||||
node.width = totalWidth;
|
|
||||||
node.height = totalHeight;
|
|
||||||
}
|
}
|
||||||
const x = -totalWidth / 2;
|
const x = -totalWidth / 2;
|
||||||
const y = -totalHeight / 2;
|
const y = -totalHeight / 2;
|
||||||
|
@@ -2,11 +2,8 @@ import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } f
|
|||||||
import intersect from '../intersect/index.js';
|
import intersect from '../intersect/index.js';
|
||||||
import type { Node } from '../../types.js';
|
import type { Node } from '../../types.js';
|
||||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||||
import type { D3Selection } from '../../../types.js';
|
|
||||||
import rough from 'roughjs';
|
import rough from 'roughjs';
|
||||||
|
import type { D3Selection } from '../../../types.js';
|
||||||
const ICON_SIZE = 30;
|
|
||||||
const ICON_PADDING = 1;
|
|
||||||
|
|
||||||
export const createHexagonPathD = (
|
export const createHexagonPathD = (
|
||||||
x: number,
|
x: number,
|
||||||
@@ -29,27 +26,21 @@ export const createHexagonPathD = (
|
|||||||
export async function hexagon<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
export async function hexagon<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {
|
||||||
const { labelStyles, nodeStyles } = styles2String(node);
|
const { labelStyles, nodeStyles } = styles2String(node);
|
||||||
node.labelStyle = labelStyles;
|
node.labelStyle = labelStyles;
|
||||||
|
|
||||||
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
|
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
|
||||||
|
|
||||||
let h = bbox.height + (node.padding ?? 0);
|
const h = bbox.height + (node.padding ?? 0);
|
||||||
let w = bbox.width + (node.padding ?? 0) * 2.5;
|
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;
|
const labelYOffset = -bbox.height / 2;
|
||||||
|
|
||||||
if (node.icon) {
|
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})`);
|
label.attr('transform', `translate(${labelXOffset}, ${labelYOffset})`);
|
||||||
} else {
|
|
||||||
node.width = w;
|
|
||||||
node.height = h;
|
|
||||||
}
|
}
|
||||||
const { cssStyles } = node;
|
const { cssStyles } = node;
|
||||||
// @ts-expect-error -- Passing a D3.Selection seems to work for some reason
|
// @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 } from '../../types.js';
|
||||||
import type { Node, MindmapOptions } from '../../types.js';
|
|
||||||
import type { D3Selection } 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>(
|
export async function mindmapCircle<T extends SVGGraphicsElement>(
|
||||||
parent: D3Selection<T>,
|
parent: D3Selection<T>,
|
||||||
node: Node
|
node: Node
|
||||||
) {
|
) {
|
||||||
const options = {
|
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
|
||||||
padding: node.padding ?? 0,
|
const halfPadding = (node.padding ?? 0) / 2;
|
||||||
} as MindmapOptions;
|
|
||||||
return circle(parent, node, options);
|
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;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ICON_SIZE = 30;
|
|
||||||
const ICON_PADDING = 15;
|
|
||||||
|
|
||||||
export async function roundedRect<T extends SVGGraphicsElement>(
|
export async function roundedRect<T extends SVGGraphicsElement>(
|
||||||
parent: D3Selection<T>,
|
parent: D3Selection<T>,
|
||||||
node: Node
|
node: Node
|
||||||
@@ -99,21 +96,12 @@ export async function roundedRect<T extends SVGGraphicsElement>(
|
|||||||
const labelPaddingX = node?.padding ?? 0;
|
const labelPaddingX = node?.padding ?? 0;
|
||||||
const labelPaddingY = node?.padding ?? 0;
|
const labelPaddingY = node?.padding ?? 0;
|
||||||
|
|
||||||
let w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2;
|
const w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2;
|
||||||
let h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2;
|
const h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2;
|
||||||
|
|
||||||
let labelXOffset = -bbox.width / 2;
|
const labelXOffset = -bbox.width / 2;
|
||||||
|
|
||||||
if (node.icon) {
|
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})`);
|
label.attr('transform', `translate(${labelXOffset}, ${-bbox.height / 2})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user