diff --git a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js index 75e1f0ba7..542539133 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js @@ -49,6 +49,8 @@ import { windowPane } from './shapes/windowPane.js'; import { linedWaveEdgedRect } from './shapes/linedWaveEdgedRect.js'; import { taggedWaveEdgedRectangle } from './shapes/taggedWaveEdgedRectangle.js'; import { curlyBraceLeft } from './shapes/curlyBraceLeft.js'; +import { curlyBraceRight } from './shapes/curlyBraceRight.js'; +import { curlyBraces } from './shapes/curlyBraces.js'; //Use these names as the left side to render shapes. const shapes = { @@ -214,11 +216,14 @@ const shapes = { bolt: lightningBolt, 'com-link': lightningBolt, curlyBraceLeft, - brace: curlyBraceLeft, + 'brace-l': curlyBraceLeft, comment: curlyBraceLeft, hourglass, odd: rect_left_inv_arrow, labelRect, + curlyBraceRight, + 'brace-r': curlyBraceRight, + curlyBraces, }; const nodeElems = new Map(); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceLeft.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceLeft.ts index d68afc91d..cd9d7f14e 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceLeft.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceLeft.ts @@ -43,15 +43,15 @@ export const curlyBraceLeft = async (parent: SVGAElement, node: Node) => { 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 radius = Math.max(5, h * 0.1); const { cssStyles } = node; const points = [ - ...generateCirclePoints(w / 2, -h / 2, 10, 30, -90, 0), + ...generateCirclePoints(w / 2, -h / 2, radius, 30, -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), + ...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180), { x: -w / 2 - radius, y: -h / 2 }, ...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90), ]; @@ -77,32 +77,32 @@ export const curlyBraceLeft = async (parent: SVGAElement, node: Node) => { options.roughness = 0; options.fillStyle = 'solid'; } - const bowTieRectPath = createPathFromPoints(points); - const newCurlyBracePath = bowTieRectPath.replace('Z', ''); - const curlyBracePath = rc.path(newCurlyBracePath, options); + const curlyBraceLeftPath = createPathFromPoints(points); + const newCurlyBracePath = curlyBraceLeftPath.replace('Z', ''); + const curlyBraceLeftNode = 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'); + const curlyBraceLeftShape = shapeSvg.insert('g', ':first-child'); + curlyBraceLeftShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0); + curlyBraceLeftShape.insert(() => curlyBraceLeftNode, ':first-child'); + curlyBraceLeftShape.attr('class', 'text'); if (cssStyles && node.look !== 'handDrawn') { - bowTieRectShape.selectAll('path').attr('style', cssStyles); + curlyBraceLeftShape.selectAll('path').attr('style', cssStyles); } if (nodeStyles && node.look !== 'handDrawn') { - bowTieRectShape.selectAll('path').attr('style', nodeStyles); + curlyBraceLeftShape.selectAll('path').attr('style', nodeStyles); } - bowTieRectShape.attr('transform', `translate(${radius}, 0)`); + curlyBraceLeftShape.attr('transform', `translate(${radius}, 0)`); label.attr( 'transform', `translate(${-w / 2 + radius - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})` ); - updateNodeBounds(node, bowTieRectShape); + updateNodeBounds(node, curlyBraceLeftShape); node.intersect = function (point) { const pos = intersect.polygon(node, rectPoints, point); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceRight.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceRight.ts new file mode 100644 index 000000000..87787b311 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceRight.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 curlyBraceRight = 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 = Math.max(5, h * 0.1); + + const { cssStyles } = node; + + const points = [ + ...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0), + { x: w / 2 + radius, y: -radius }, + ...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + radius * 2, 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 + radius * 2, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + radius * 2, 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 curlyBraceRightPath = createPathFromPoints(points); + const newCurlyBracePath = curlyBraceRightPath.replace('Z', ''); + const curlyBraceRightNode = rc.path(newCurlyBracePath, options); + const rectPath = createPathFromPoints(rectPoints); + const rectShape = rc.path(rectPath, { ...options }); + const curlyBraceRightShape = shapeSvg.insert('g', ':first-child'); + curlyBraceRightShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0); + curlyBraceRightShape.insert(() => curlyBraceRightNode, ':first-child'); + curlyBraceRightShape.attr('class', 'text'); + + if (cssStyles && node.look !== 'handDrawn') { + curlyBraceRightShape.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + curlyBraceRightShape.selectAll('path').attr('style', nodeStyles); + } + + curlyBraceRightShape.attr('transform', `translate(${-radius}, 0)`); + + label.attr( + 'transform', + `translate(${-w / 2 + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, curlyBraceRightShape); + + node.intersect = function (point) { + const pos = intersect.polygon(node, rectPoints, point); + + return pos; + }; + + return shapeSvg; +}; 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 000000000..384c05e8a --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraces.ts @@ -0,0 +1,133 @@ +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: -x, y: -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 = Math.max(5, h * 0.1); + + const { cssStyles } = node; + + const leftCurlyBracePoints = [ + ...generateCirclePoints(w / 2, -h / 2, radius, 30, -90, 0), + { x: -w / 2 - radius, y: radius }, + ...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180), + { x: -w / 2 - radius, y: -h / 2 }, + ...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90), + ]; + + const rightCurlyBracePoints = [ + ...generateCirclePoints(-w / 2 + radius + radius / 2, -h / 2, radius, 20, -90, -180), + { x: w / 2 - radius / 2, y: radius }, + ...generateCirclePoints(-w / 2 - radius / 2, -radius, radius, 20, 0, 90), + ...generateCirclePoints(-w / 2 - radius / 2, radius, radius, 20, -90, 0), + { x: w / 2 - radius / 2, y: -radius }, + ...generateCirclePoints(-w / 2 + radius + radius / 2, h / 2, radius, 30, -180, -270), + ]; + + 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 + radius * 2, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + radius * 2, 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 - radius - radius / 2, y: h / 2 + radius }, + ...generateCirclePoints(-w / 2 + radius + radius / 2, -h / 2, radius, 20, -90, -180), + { x: w / 2 - radius / 2, y: radius }, + ...generateCirclePoints(-w / 2 - radius / 2, -radius, radius, 20, 0, 90), + ...generateCirclePoints(-w / 2 - radius / 2, radius, radius, 20, -90, 0), + { x: w / 2 - radius / 2, y: -radius }, + ...generateCirclePoints(-w / 2 + radius + radius / 2, h / 2, radius, 30, -180, -270), + ]; + + // @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 leftCurlyBracePath = createPathFromPoints(leftCurlyBracePoints); + const newLeftCurlyBracePath = leftCurlyBracePath.replace('Z', ''); + const leftCurlyBraceNode = rc.path(newLeftCurlyBracePath, options); + const rightCurlyBracePath = createPathFromPoints(rightCurlyBracePoints); + const newRightCurlyBracePath = rightCurlyBracePath.replace('Z', ''); + const rightCurlyBraceNode = rc.path(newRightCurlyBracePath, options); + const rectPath = createPathFromPoints(rectPoints); + const rectShape = rc.path(rectPath, { ...options }); + const curlyBracesShape = shapeSvg.insert('g', ':first-child'); + curlyBracesShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0); + curlyBracesShape.insert(() => leftCurlyBraceNode, ':first-child'); + curlyBracesShape.insert(() => rightCurlyBraceNode, ':first-child'); + curlyBracesShape.attr('class', 'text'); + + if (cssStyles && node.look !== 'handDrawn') { + curlyBracesShape.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + curlyBracesShape.selectAll('path').attr('style', nodeStyles); + } + + curlyBracesShape.attr('transform', `translate(${radius - radius / 4}, 0)`); + + label.attr( + 'transform', + `translate(${-w / 2 + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, curlyBracesShape); + + node.intersect = function (point) { + const pos = intersect.polygon(node, rectPoints, point); + + return pos; + }; + + return shapeSvg; +};