diff --git a/cypress/integration/rendering/mindmap.spec.ts b/cypress/integration/rendering/mindmap.spec.ts
index 731f861ee..eedf1da1e 100644
--- a/cypress/integration/rendering/mindmap.spec.ts
+++ b/cypress/integration/rendering/mindmap.spec.ts
@@ -105,6 +105,126 @@ root))bang((
);
});
+ it('Bang and cloud shape with dagre layout', () => {
+ imgSnapshotTest(
+ `---
+ config:
+ layout: dagre
+ ---
+ mindmap
+ root((mindmap))
+ Origins
+ Long history
+ ::icon(fa fa-book)
+ Popularisation
+ British popular psychology author Tony Buzan
+ Research
+ On effectiveness
and features
+ On Automatic creation
+ Uses
+ Creative techniques
+ Strategic planning
+ Argument mapping
+ Tools
+ Pen and paper
+ Mermaid
+ `,
+ {},
+ undefined,
+ shouldHaveRoot
+ );
+ });
+
+ it('Bang and cloud shape with elk layout', () => {
+ imgSnapshotTest(
+ `---
+ config:
+ layout: elk
+ ---
+ mindmap
+ root((mindmap))
+ Origins
+ Long history
+ ::icon(fa fa-book)
+ Popularisation
+ British popular psychology author Tony Buzan
+ Research
+ On effectiveness
and features
+ On Automatic creation
+ Uses
+ Creative techniques
+ Strategic planning
+ Argument mapping
+ Tools
+ Pen and paper
+ Mermaid
+ `,
+ {},
+ undefined,
+ shouldHaveRoot
+ );
+ });
+
+ it('Bang and cloud shape with cose-bilkent layout', () => {
+ imgSnapshotTest(
+ `---
+ config:
+ layout: cose-bilkent
+ ---
+ mindmap
+ root((mindmap))
+ Origins
+ Long history
+ ::icon(fa fa-book)
+ Popularisation
+ British popular psychology author Tony Buzan
+ Research
+ On effectiveness
and features
+ On Automatic creation
+ Uses
+ Creative techniques
+ Strategic planning
+ Argument mapping
+ Tools
+ Pen and paper
+ Mermaid
+ `,
+ {},
+ undefined,
+ shouldHaveRoot
+ );
+ });
+
+ it('Bang and cloud shape with tidy-tree layout', () => {
+ imgSnapshotTest(
+ `---
+ config:
+ layout: tidy-tree
+ ---
+ mindmap
+ root((mindmap))
+ Origins
+ Long history
+ ::icon(fa fa-book)
+ Popularisation
+ British popular psychology author Tony Buzan
+ Research
+ On effectiveness
and features
+ On Automatic creation
+ Uses
+ Creative techniques
+ Strategic planning
+ Argument mapping
+ Tools
+ Pen and paper
+ Mermaid
+ `,
+ {},
+ undefined,
+ shouldHaveRoot
+ );
+ });
+
it('braches', () => {
imgSnapshotTest(
`mindmap
diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
index 75fbfd27b..8ef70d20c 100644
--- a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
+++ b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
@@ -206,9 +206,9 @@ const flattenNodes = (node: MindmapNode, processedNodes: MindmapLayoutNode[]): v
case nodeType.ROUNDED_RECT:
return 'rounded';
case nodeType.CLOUD:
- return 'rounded'; // Map cloud to rounded for now
+ return 'cloud';
case nodeType.BANG:
- return 'circle'; // Map bang to circle for now
+ return 'bang';
case nodeType.HEXAGON:
return 'hexagon';
case nodeType.DEFAULT:
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts
index 829f89a8f..2a25ba559 100644
--- a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts
+++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts
@@ -61,6 +61,8 @@ import { erBox } from './shapes/erBox.js';
import { classBox } from './shapes/classBox.js';
import { requirementBox } from './shapes/requirementBox.js';
import { kanbanItem } from './shapes/kanbanItem.js';
+import { bang } from './shapes/bang.js';
+import { cloud } from './shapes/cloud.js';
type ShapeHandler = (
parent: D3Selection,
@@ -135,6 +137,22 @@ export const shapesDefs = [
aliases: ['circ'],
handler: circle,
},
+ {
+ semanticName: 'Bang',
+ name: 'Bang',
+ shortName: 'bang',
+ description: 'Bang',
+ aliases: ['bang'],
+ handler: bang,
+ },
+ {
+ semanticName: 'Cloud',
+ name: 'Cloud',
+ shortName: 'cloud',
+ description: 'cloud',
+ aliases: ['cloud'],
+ handler: cloud,
+ },
{
semanticName: 'Decision',
name: 'Diamond',
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/bang.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/bang.ts
new file mode 100644
index 000000000..b98917f5a
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/bang.ts
@@ -0,0 +1,77 @@
+import { log } from '../../../logger.js';
+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';
+
+export async function bang(parent: D3Selection, node: Node) {
+ const { labelStyles, nodeStyles } = styles2String(node);
+ node.labelStyle = labelStyles;
+ const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
+ parent,
+ node,
+ getNodeClasses(node)
+ );
+
+ const w = bbox.width + 2 * halfPadding;
+ const h = bbox.height + 2 * halfPadding;
+ const r = 0.15 * w;
+ const { cssStyles } = node;
+
+ // Label centered around (0,0)
+ label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
+
+ let bangElem;
+ const path = `M0 0
+ a${r},${r} 1 0,0 ${w * 0.25},${-1 * h * 0.1}
+ a${r},${r} 1 0,0 ${w * 0.25},${0}
+ a${r},${r} 1 0,0 ${w * 0.25},${0}
+ a${r},${r} 1 0,0 ${w * 0.25},${h * 0.1}
+
+ a${r},${r} 1 0,0 ${w * 0.15},${h * 0.33}
+ a${r * 0.8},${r * 0.8} 1 0,0 0,${h * 0.34}
+ a${r},${r} 1 0,0 ${-1 * w * 0.15},${h * 0.33}
+
+ a${r},${r} 1 0,0 ${-1 * w * 0.25},${h * 0.15}
+ a${r},${r} 1 0,0 ${-1 * w * 0.25},0
+ a${r},${r} 1 0,0 ${-1 * w * 0.25},0
+ a${r},${r} 1 0,0 ${-1 * w * 0.25},${-1 * h * 0.15}
+
+ a${r},${r} 1 0,0 ${-1 * w * 0.1},${-1 * h * 0.33}
+ a${r * 0.8},${r * 0.8} 1 0,0 0,${-1 * h * 0.34}
+ a${r},${r} 1 0,0 ${w * 0.1},${-1 * h * 0.33}
+ H0 V0 Z`;
+
+ if (node.look === 'handDrawn') {
+ // @ts-expect-error -- Passing a D3.Selection seems to work for some reason
+ const rc = rough.svg(shapeSvg);
+ const options = userNodeOverrides(node, {});
+ const roughNode = rc.path(path, options);
+ bangElem = shapeSvg.insert(() => roughNode, ':first-child');
+ bangElem.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
+ } else {
+ bangElem = shapeSvg
+ .insert('path', ':first-child')
+ .attr('class', 'basic label-container')
+ .attr('style', nodeStyles)
+ .attr('d', path);
+ }
+
+ // Translate the path (center the shape)
+ bangElem.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
+
+ updateNodeBounds(node, bangElem);
+ node.calcIntersect = function (bounds: Bounds, point: Point) {
+ return intersect.rect(bounds, point);
+ };
+ node.intersect = function (point) {
+ log.info('Bang intersect', node, point);
+ return intersect.rect(node, point);
+ };
+
+ return shapeSvg;
+}
diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/cloud.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/cloud.ts
new file mode 100644
index 000000000..f7fb36509
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/cloud.ts
@@ -0,0 +1,81 @@
+import { log } from '../../../logger.js';
+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';
+
+export async function cloud(parent: D3Selection, node: Node) {
+ const { labelStyles, nodeStyles } = styles2String(node);
+ node.labelStyle = labelStyles;
+
+ const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
+ parent,
+ node,
+ getNodeClasses(node)
+ );
+
+ const w = bbox.width + 2 * halfPadding;
+ const h = bbox.height + 2 * halfPadding;
+
+ // Cloud radii
+ const r1 = 0.15 * w;
+ const r2 = 0.25 * w;
+ const r3 = 0.35 * w;
+ const r4 = 0.2 * w;
+
+ const { cssStyles } = node;
+ let cloudElem;
+
+ // Cloud path
+ const path = `M0 0
+ a${r1},${r1} 0 0,1 ${w * 0.25},${-1 * w * 0.1}
+ a${r3},${r3} 1 0,1 ${w * 0.4},${-1 * w * 0.1}
+ a${r2},${r2} 1 0,1 ${w * 0.35},${w * 0.2}
+
+ a${r1},${r1} 1 0,1 ${w * 0.15},${h * 0.35}
+ a${r4},${r4} 1 0,1 ${-1 * w * 0.15},${h * 0.65}
+
+ a${r2},${r1} 1 0,1 ${-1 * w * 0.25},${w * 0.15}
+ a${r3},${r3} 1 0,1 ${-1 * w * 0.5},0
+ a${r1},${r1} 1 0,1 ${-1 * w * 0.25},${-1 * w * 0.15}
+
+ a${r1},${r1} 1 0,1 ${-1 * w * 0.1},${-1 * h * 0.35}
+ a${r4},${r4} 1 0,1 ${w * 0.1},${-1 * h * 0.65}
+ H0 V0 Z`;
+
+ if (node.look === 'handDrawn') {
+ // @ts-expect-error -- Passing a D3.Selection seems to work for some reason
+ const rc = rough.svg(shapeSvg);
+ 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(cssStyles));
+ } else {
+ cloudElem = shapeSvg
+ .insert('path', ':first-child')
+ .attr('class', 'basic label-container')
+ .attr('style', nodeStyles)
+ .attr('d', path);
+ }
+
+ label.attr('transform', `translate(${-bbox.width / 2}, ${-bbox.height / 2})`);
+
+ // Center the shape
+ cloudElem.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
+
+ updateNodeBounds(node, cloudElem);
+
+ node.calcIntersect = function (bounds: Bounds, point: Point) {
+ return intersect.rect(bounds, point);
+ };
+ node.intersect = function (point) {
+ log.info('Cloud intersect', node, point);
+ return intersect.rect(node, point);
+ };
+
+ return shapeSvg;
+}