diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js b/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js index 3e82d4751..0f58fa131 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js @@ -140,3 +140,25 @@ export function createPathFromPoints(points) { pointStrings.push('Z'); return pointStrings.join(' '); } + +export function generateFullSineWavePoints(x1, y1, x2, y2, amplitude, numCycles) { + const points = []; + const steps = 50; // Number of segments to create a smooth curve + const deltaX = x2 - x1; + const deltaY = y2 - y1; + const cycleLength = deltaX / numCycles; + + // Calculate frequency and phase shift + const frequency = (2 * Math.PI) / cycleLength; + const midY = y1 + deltaY / 2; + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const x = x1 + t * deltaX; + const y = midY + amplitude * Math.sin(frequency * (x - x1)); + + points.push({ x, y }); + } + + return points; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveEdgedRectangle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveEdgedRectangle.ts index c5296a4c9..966615f2d 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveEdgedRectangle.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveEdgedRectangle.ts @@ -1,52 +1,25 @@ -import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import { + labelHelper, + updateNodeBounds, + getNodeClasses, + generateFullSineWavePoints, + createPathFromPoints, +} from './util.js'; import intersect from '../intersect/index.js'; import type { Node } from '$root/rendering-util/types.d.ts'; import rough from 'roughjs'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; -export function createWaveEdgedRectanglePathD(width: number, height: number) { - // Calculate control points - const rightX = width; - const midX = width / 2; - const controlY1 = height * 0.8; - const controlY2 = height * 1.15; - const endY = height * 0.94; - - // Construct the path - const path = `M0 0 - H${rightX} - V${controlY1} - C${midX} ${controlY1}, ${midX} ${controlY2}, 0 ${endY} - Z`; - - return path; -} - export const waveEdgedRectangle = async (parent: SVGAElement, node: Node) => { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; - const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const w = bbox.width + node.padding; - const h = bbox.height + node.padding + 20; - + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const waveAmplitude = h / 4; + const finalH = h + waveAmplitude; const { cssStyles } = node; - const rightX = w; - const midX = w / 2; - const controlY1 = h * 0.8; - const controlY2 = h * 1.15; - const endY = h * 0.94; - - const points = [ - { x: 0, y: 0 }, - { x: rightX, y: 0 }, - { x: rightX, y: controlY1 }, - { x: midX, y: controlY1 }, - { x: midX, y: controlY2 * 0.8 }, - { x: 0, y: endY }, - ]; - const pathData = createWaveEdgedRectanglePathD(w, h); - // @ts-ignore - rough is not typed const rc = rough.svg(shapeSvg); const options = userNodeOverrides(node, {}); @@ -55,22 +28,36 @@ export const waveEdgedRectangle = async (parent: SVGAElement, node: Node) => { options.roughness = 0; options.fillStyle = 'solid'; } - const shapeNode = rc.path(pathData, options); - const shape = shapeSvg.insert(() => shapeNode, ':first-child'); - shape.attr('class', 'basic label-container'); - if (cssStyles) { - shape.attr('style', cssStyles); + const points = [ + { x: -w / 2, y: finalH / 2 }, + ...generateFullSineWavePoints(-w / 2, finalH / 2, w / 2, finalH / 2, waveAmplitude, 0.8), + { x: w / 2, y: -finalH / 2 }, + { x: -w / 2, y: -finalH / 2 }, + ]; + + const waveEdgeRectPath = createPathFromPoints(points); + const waveEdgeRectNode = rc.path(waveEdgeRectPath, options); + + const waveEdgeRect = shapeSvg.insert(() => waveEdgeRectNode, ':first-child'); + + waveEdgeRect.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + waveEdgeRect.selectAll('path').attr('style', cssStyles); } - if (nodeStyles) { - shape.attr('style', nodeStyles); + if (nodeStyles && node.look !== 'handDrawn') { + waveEdgeRect.selectAll('path').attr('style', nodeStyles); } - shape.attr('transform', `translate(${-w / 2}, ${-h / 2})`); - - updateNodeBounds(node, shape); + waveEdgeRect.attr('transform', `translate(0,${-waveAmplitude / 2})`); + label.attr( + 'transform', + `translate(${-w / 2 + (node.padding ?? 0)},${-h / 2 + (node.padding ?? 0) - waveAmplitude / 2})` + ); + updateNodeBounds(node, waveEdgeRect); node.intersect = function (point) { const pos = intersect.polygon(node, points, point); return pos; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveRectangle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveRectangle.ts index f4b43890b..08e531798 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveRectangle.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveRectangle.ts @@ -1,4 +1,10 @@ -import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import { + labelHelper, + updateNodeBounds, + getNodeClasses, + createPathFromPoints, + generateFullSineWavePoints, +} from './util.js'; import intersect from '../intersect/index.js'; import type { Node } from '$root/rendering-util/types.d.ts'; import { @@ -7,58 +13,50 @@ import { } from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js'; import rough from 'roughjs'; -function createWaveRectanglePathD(x: number, y: number, width: number, height: number) { - const halfWidth = width / 2; - const halfHeight = height / 2; - - const pathData = `M ${x} ${y} - Q ${x + halfWidth / 2} ${y + halfHeight}, ${x + halfWidth} ${y} - Q ${x + (3 * halfWidth) / 2} ${y - halfHeight}, ${x + width} ${y} - L ${x + width} ${y + height} - Q ${x + (3 * halfWidth) / 2} ${y + halfHeight}, ${x + halfWidth} ${y + height} - Q ${x + halfWidth / 2} ${y + 3 * halfHeight}, ${x} ${y + height} - L ${x} ${y} - Z`; - return pathData; -} - export const waveRectangle = async (parent: SVGAElement, node: Node) => { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const w = bbox.width + node.padding; - const h = bbox.height + node.padding + 20; - - let shape: d3.Selection; + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const waveAmplitude = h / 4; + const finalH = h + waveAmplitude; const { cssStyles } = node; - if (node.look === 'handDrawn') { - // @ts-ignore - rough is not typed - const rc = rough.svg(shapeSvg); - const pathData = createWaveRectanglePathD(0, 0, w, h); - const shapeNode = rc.path(pathData, userNodeOverrides(node, {})); + // @ts-ignore - rough is not typed + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); - shape = shapeSvg.insert(() => shapeNode, ':first-child'); - shape.attr('class', 'basic label-container'); - if (cssStyles) { - shape.attr('style', cssStyles); - } - } else { - const pathData = createWaveRectanglePathD(0, 0, w, h); - shape = shapeSvg - .insert('path', ':first-child') - .attr('d', pathData) - .attr('class', 'basic label-container') - .attr('style', cssStyles) - .attr('style', nodeStyles); + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; } - shape.attr('transform', `translate(${-w / 2}, ${-h / 2})`); + const points = [ + { x: -w / 2, y: finalH / 2 }, + ...generateFullSineWavePoints(-w / 2, finalH / 2, w / 2, finalH / 2, waveAmplitude, 1), + { x: w / 2, y: -finalH / 2 }, + ...generateFullSineWavePoints(w / 2, -finalH / 2, -w / 2, -finalH / 2, waveAmplitude, -1), + ]; - updateNodeBounds(node, shape); + const waveRectPath = createPathFromPoints(points); + const waveRectNode = rc.path(waveRectPath, options); + const waveRect = shapeSvg.insert(() => waveRectNode, ':first-child'); + + waveRect.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + waveRect.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + waveRect.selectAll('path').attr('style', nodeStyles); + } + + updateNodeBounds(node, waveRect); node.intersect = function (point) { - const pos = intersect.polygon(node, point); + const pos = intersect.polygon(node, points, point); return pos; };