diff --git a/cypress/integration/rendering/newShapes.spec.ts b/cypress/integration/rendering/newShapes.spec.ts index 45e51a09b6..f75d3c5fca 100644 --- a/cypress/integration/rendering/newShapes.spec.ts +++ b/cypress/integration/rendering/newShapes.spec.ts @@ -40,6 +40,7 @@ const newShapesSet5 = [ 'card', 'shadedProcess', ] as const; +const newShapesSet6 = ['curlyBraces']; // Aggregate all shape sets into a single array const newShapesSets = [ @@ -48,6 +49,7 @@ const newShapesSets = [ newShapesSet3, newShapesSet4, newShapesSet5, + newShapesSet6, ] as const; looks.forEach((look) => { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js index e40d0c6fa8..2505bffc53 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js @@ -48,6 +48,7 @@ import { multiWaveEdgedRectangle } from './shapes/multiWaveEdgedRectangle.js'; import { windowPane } from './shapes/windowPane.js'; import { linedWaveEdgedRect } from './shapes/linedWaveEdgedRect.js'; import { taggedWaveEdgedRectangle } from './shapes/taggedWaveEdgedRectangle.js'; +import { curlyBraces } from './shapes/curlyBraces.js'; //Use these names as the left side to render shapes. const shapes = { @@ -212,6 +213,9 @@ const shapes = { lightningBolt, bolt: lightningBolt, 'com-link': lightningBolt, + curlyBraces, + brace: curlyBraces, + comment: curlyBraces, hourglass, odd: rect_left_inv_arrow, labelRect, diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraces.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraces.ts new file mode 100644 index 0000000000..ea294e8623 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraces.ts @@ -0,0 +1,114 @@ +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '$root/rendering-util/types.d.ts'; +import { + styles2String, + userNodeOverrides, +} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +function generateCirclePoints( + centerX: number, + centerY: number, + radius: number, + numPoints = 100, + startAngle = 0, + endAngle = 180 +) { + const points = []; + + // Convert angles to radians + const startAngleRad = (startAngle * Math.PI) / 180; + const endAngleRad = (endAngle * Math.PI) / 180; + + // Calculate the angle range in radians + const angleRange = endAngleRad - startAngleRad; + + // Calculate the angle step + const angleStep = angleRange / (numPoints - 1); + + for (let i = 0; i < numPoints; i++) { + const angle = startAngleRad + i * angleStep; + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + points.push({ x, y }); + } + + return points; +} + +export const curlyBraces = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = bbox.width + (node.padding ?? 0); + const h = bbox.height + (node.padding ?? 0); + const radius = w * 0.05; + + const { cssStyles } = node; + + const points = [ + ...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0), + { x: w / 2 + radius, y: -radius }, + ...generateCirclePoints(w / 2 + w * 0.1, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + w * 0.1, radius, radius, 20, -90, -180), + { x: w / 2 + radius, y: h / 2 }, + ...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90), + ]; + + const rectPoints = [ + { x: -w / 2, y: -h / 2 - radius }, + { x: w / 2, y: -h / 2 - radius }, + ...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0), + { x: w / 2 + radius, y: -radius }, + ...generateCirclePoints(w / 2 + w * 0.1, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + w * 0.1, radius, radius, 20, -90, -180), + { x: w / 2 + radius, y: h / 2 }, + ...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90), + { x: w / 2, y: h / 2 + radius }, + { x: -w / 2, y: h / 2 + radius }, + ]; + + // @ts-ignore - rough is not typed + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { fill: 'none' }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + const bowTieRectPath = createPathFromPoints(points); + const newCurlyBracePath = bowTieRectPath.replace('Z', ''); + const curlyBracePath = rc.path(newCurlyBracePath, options); + const rectPath = createPathFromPoints(rectPoints); + const rectShape = rc.path(rectPath, { ...options }); + const bowTieRectShape = shapeSvg.insert('g', ':first-child'); + bowTieRectShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0); + bowTieRectShape.insert(() => curlyBracePath, ':first-child'); + bowTieRectShape.attr('class', 'text'); + + if (cssStyles && node.look !== 'handDrawn') { + bowTieRectShape.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + bowTieRectShape.selectAll('path').attr('style', nodeStyles); + } + + bowTieRectShape.attr('transform', `translate(${-radius}, 0)`); + + label.attr( + 'transform', + `translate(${-w / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, bowTieRectShape); + + node.intersect = function (point) { + const pos = intersect.polygon(node, rectPoints, point); + + return pos; + }; + + return shapeSvg; +};