From 4d8e5192980469305c2aa5625449847cda57431d Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Mon, 22 Jul 2024 10:59:17 +0200 Subject: [PATCH] Adding new-shapes and picking up node data from node data statement --- .../mermaid/src/diagrams/flowchart/flowDb.ts | 29 ++++- .../mermaid/src/diagrams/flowchart/styles.ts | 14 +- .../rendering-elements/nodes.js | 8 ++ .../rendering-elements/shapes/anchor.ts | 61 +++++++++ .../rendering-elements/shapes/card.ts | 76 +++++++++++ .../rendering-elements/shapes/document.ts | 121 ++++++++++++++++++ .../rendering-elements/shapes/drawRect.ts | 2 - .../shapes/shadedProcess.ts | 66 ++++++++++ .../rendering-elements/shapes/text.ts | 41 ++++++ .../rendering-elements/shapes/util.js | 6 + 10 files changed, 417 insertions(+), 7 deletions(-) create mode 100644 packages/mermaid/src/rendering-util/rendering-elements/shapes/anchor.ts create mode 100644 packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts create mode 100644 packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts create mode 100644 packages/mermaid/src/rendering-util/rendering-elements/shapes/shadedProcess.ts create mode 100644 packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts index bf6b8f5eb..4a4e13799 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts @@ -120,10 +120,31 @@ export const addVertex = function ( } if (shapeData !== undefined) { - console.log('HERE - shapeData: ', shapeData); - // const doc = yaml.load(shapeData); - // console.log('doc', doc); - // vertex.type = shapeData?.shape || 'roundedRect'; + let yamlData; + // detect if shapeData contains a newline character + + if (!shapeData.includes('\n')) { + console.log('yamlData shapeData has no new lines', shapeData); + yamlData = '{\n' + shapeData + '\n'; + } else { + console.log('yamlData shapeData has new lines', shapeData); + yamlData = shapeData + '\n'; + // Find the position of the last } and replace it with a newline + const lastPos = yamlData.lastIndexOf('}'); + if (lastPos !== -1) { + yamlData = yamlData.substring(0, lastPos) + '\n'; + } + } + + console.log('yamlData', yamlData); + const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }); + console.log('yamlData doc', doc); + if (doc?.shape) { + vertex.type = doc?.shape; + } + if (doc?.label) { + vertex.text = doc?.label; + } } }; diff --git a/packages/mermaid/src/diagrams/flowchart/styles.ts b/packages/mermaid/src/diagrams/flowchart/styles.ts index 549fddec1..f4bc82e56 100644 --- a/packages/mermaid/src/diagrams/flowchart/styles.ts +++ b/packages/mermaid/src/diagrams/flowchart/styles.ts @@ -75,13 +75,20 @@ const getStyles = (options: FlowChartStyleOptions) => stroke-width: 1px; } - .node .label { + .rough-node .label,.node .label { text-align: center; } .node.clickable { cursor: pointer; } + + .root .anchor path { + fill: ${options.lineColor} !important; + stroke-width: 0; + stroke: ${options.lineColor}; + } + .arrowheadPath { fill: ${options.arrowheadColor}; } @@ -151,6 +158,11 @@ const getStyles = (options: FlowChartStyleOptions) => font-size: 18px; fill: ${options.textColor}; } + + rect.text { + fill: none; + stroke-width: 0; + } `; export default getStyles; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js index 54d4ddf3e..664164fa3 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js @@ -17,6 +17,10 @@ import { doublecircle } from './shapes/doubleCircle.js'; import { rect_left_inv_arrow } from './shapes/rectLeftInvArrow.js'; import { question } from './shapes/question.js'; import { hexagon } from './shapes/hexagon.js'; +import { text } from './shapes/text.js'; +import { card } from './shapes/card.js'; +import { shadedProcess } from './shapes/shadedProcess.js'; +import { anchor } from './shapes/anchor.js'; import { lean_right } from './shapes/leanRight.js'; import { lean_left } from './shapes/leanLeft.js'; import { trapezoid } from './shapes/trapezoid.js'; @@ -47,6 +51,10 @@ const shapes = { trapezoid, inv_trapezoid, labelRect, + text, + card, + shadedProcess, + anchor, }; const nodeElems = new Map(); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/anchor.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/anchor.ts new file mode 100644 index 000000000..adaaf3fde --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/anchor.ts @@ -0,0 +1,61 @@ +import { log } from '$root/logger.js'; +import { labelHelper, updateNodeBounds, getNodeClasses } 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/handdrawnStyles.js'; +import rough from 'roughjs'; + +export const anchor = async (parent: SVGAElement, node: Node): Promise => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + // const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node)); + const classes = getNodeClasses(node); + let cssClasses = classes; + if (!classes) { + cssClasses = 'anchor'; + } + const shapeSvg = parent + .insert('g') + .attr('class', cssClasses) + .attr('id', node.domId || node.id); + + const radius = 1; + let circleElem; + const { cssStyles } = node; + + // if (node.look === 'handdrawn') { + // @ts-ignore - rough is not typed + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { fill: 'black', stroke: 'none', fillStyle: 'solid' }); + + if (node.look !== 'handdrawn') { + options.roughness = 0; + } + const roughNode = rc.circle(0, 0, radius * 2, options); + + console.log('IPI roughNode:', options); + + circleElem = shapeSvg.insert(() => roughNode, ':first-child'); + circleElem.attr('class', 'anchor').attr('style', cssStyles); + // } else { + // circleElem = shapeSvg + // .insert('circle', ':first-child') + // .attr('class', 'basic label-container') + // .attr('style', nodeStyles) + // .attr('r', radius) + // .attr('cx', 0) + // .attr('cy', 0); + // } + + updateNodeBounds(node, circleElem); + + node.intersect = function (point) { + log.info('Circle intersect', node, radius, point); + return intersect.circle(node, radius, point); + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts new file mode 100644 index 000000000..9dd2a3f81 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts @@ -0,0 +1,76 @@ +import { labelHelper, updateNodeBounds, getNodeClasses } 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/handdrawnStyles.js'; +import rough from 'roughjs'; + +import { insertPolygonShape } from './insertPolygonShape.js'; +import { createPathFromPoints } from './util.js'; + +// const createPathFromPoints = (points: { x: number; y: number }[]): string => { +// const pointStrings = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`); +// pointStrings.push('Z'); +// return pointStrings.join(' '); +// }; + +export async function card(parent: SVGAElement, node: Node): Promise { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + + const f = 4; + const h = bbox.height + node.padding; + // const m = h / f; + const m = 10; + const padding = 12; + const w = bbox.width + node.padding + padding; + const left = 0; + const right = w; + const top = -h; + const bottom = 0; + // const w = bbox.width + 2 * m + node.padding; + const points = [ + { x: left + padding, y: top }, + { x: right, y: top }, + { x: right, y: bottom }, + { x: left, y: bottom }, + { x: left, y: top + padding }, + { x: left + padding, y: top }, + ]; + + let polygon: d3.Selection; + const { cssStyles } = node; + + if (node.look === 'handdrawn') { + // @ts-ignore - rough is not typed + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + const pathData = createPathFromPoints(points); + const roughNode = rc.path(pathData, options); + + polygon = shapeSvg + .insert(() => roughNode, ':first-child') + .attr('transform', `translate(${-w / 2}, ${h / 2})`); + + if (cssStyles) { + polygon.attr('style', cssStyles); + } + } else { + polygon = insertPolygonShape(shapeSvg, w, h, points); + } + + if (nodeStyles) { + polygon.attr('style', nodeStyles); + } + + updateNodeBounds(node, polygon); + + node.intersect = function (point) { + return intersect.polygon(node, points, point); + }; + + return shapeSvg; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts new file mode 100644 index 000000000..86329f2f3 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts @@ -0,0 +1,121 @@ +import { labelHelper, updateNodeBounds, getNodeClasses } 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/handdrawnStyles.js'; +import rough from 'roughjs'; + +export const createCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number +): string => { + return [ + `M${x},${y + ry}`, + `a${rx},${ry} 0,0,0 ${width},0`, + `a${rx},${ry} 0,0,0 ${-width},0`, + `l0,${height}`, + `a${rx},${ry} 0,0,0 ${width},0`, + `l0,${-height}`, + ].join(' '); +}; +export const createOuterCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number +): string => { + return [ + `M${x},${y + ry}`, + `M${x + width},${y + ry}`, + `a${rx},${ry} 0,0,0 ${-width},0`, + `l0,${height}`, + `a${rx},${ry} 0,0,0 ${width},0`, + `l0,${-height}`, + ].join(' '); +}; +export const createInnerCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number +): string => { + return [`M${x - width / 2},${-height / 2}`, `a${rx},${ry} 0,0,0 ${width},0`].join(' '); +}; +export const cylinder = 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 rx = w / 2; + const ry = rx / (2.5 + w / 50); + const h = bbox.height + ry + node.padding; + + let cylinder: d3.Selection; + const { cssStyles } = node; + + if (node.look === 'handdrawn') { + // @ts-ignore - rough is not typed + const rc = rough.svg(shapeSvg); + const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry); + const innerPathData = createInnerCylinderPathD(0, ry, w, h, rx, ry); + const outerNode = rc.path(outerPathData, userNodeOverrides(node, {})); + const innerLine = rc.path(innerPathData, userNodeOverrides(node, { fill: 'none' })); + + cylinder = shapeSvg.insert(() => innerLine, ':first-child'); + cylinder = shapeSvg.insert(() => outerNode, ':first-child'); + cylinder.attr('class', 'basic label-container'); + if (cssStyles) { + cylinder.attr('style', cssStyles); + } + } else { + const pathData = createCylinderPathD(0, 0, w, h, rx, ry); + cylinder = shapeSvg + .insert('path', ':first-child') + .attr('d', pathData) + .attr('class', 'basic label-container') + .attr('style', cssStyles) + .attr('style', nodeStyles); + } + + cylinder.attr('label-offset-y', ry); + cylinder.attr('transform', `translate(${-w / 2}, ${-(h / 2 + ry)})`); + + updateNodeBounds(node, cylinder); + + node.intersect = function (point) { + const pos = intersect.rect(node, point); + const x = pos.x - (node.x ?? 0); + + if ( + rx != 0 && + (Math.abs(x) < (node.width ?? 0) / 2 || + (Math.abs(x) == (node.width ?? 0) / 2 && + Math.abs(pos.y - (node.y ?? 0)) > (node.height ?? 0) / 2 - ry)) + ) { + let y = ry * ry * (1 - (x * x) / (rx * rx)); + if (y != 0) { + y = Math.sqrt(y); + } + y = ry - y; + if (point.y - (node.y ?? 0) > 0) { + y = -y; + } + + pos.y += y; + } + + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts index 0509ed334..793a3ad30 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/drawRect.ts @@ -50,8 +50,6 @@ export const drawRect = async (parent: SVGAElement, node: Node, options: RectOpt .attr('class', 'basic label-container') .attr('style', nodeStyles) .attr('rx', rx) - .attr('data-id', 'abc') - .attr('data-et', 'node') .attr('ry', ry) .attr('x', x) .attr('y', y) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/shadedProcess.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/shadedProcess.ts new file mode 100644 index 000000000..a943b7696 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/shadedProcess.ts @@ -0,0 +1,66 @@ +import { labelHelper, updateNodeBounds, getNodeClasses } 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/handdrawnStyles.js'; +import rough from 'roughjs'; +import { insertPolygonShape } from './insertPolygonShape.js'; +import { createPathFromPoints } from './util.js'; + +export const shadedProcess = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + const halfPadding = (node?.padding || 0) / 2; + const w = bbox.width + node.padding; + const h = bbox.height + node.padding; + const x = -bbox.width / 2 - halfPadding; + const y = -bbox.height / 2 - halfPadding; + let rect; + const { cssStyles } = node; + const points = [ + { x: 0, y: 0 }, + { x: w, y: 0 }, + { x: w, y: -h }, + { x: 0, y: -h }, + { x: 0, y: 0 }, + { x: -8, y: 0 }, + { x: w + 8, y: 0 }, + { x: w + 8, y: -h }, + { x: -8, y: -h }, + { x: -8, y: 0 }, + ]; + + // @ts-ignore - rough is not typed + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handdrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const roughNode = rc.rectangle(x - 8, y, w + 16, h, options); + const l1 = rc.line(x, y, x, y + h, options); + + shapeSvg.insert(() => l1, ':first-child'); + rect = shapeSvg.insert(() => roughNode, ':first-child'); + + rect.attr('class', 'basic label-container').attr('style', cssStyles); + + if (nodeStyles) { + rect.attr('style', nodeStyles); + } + + updateNodeBounds(node, rect); + + node.intersect = function (point) { + return intersect.rect(node, point); + }; + + return shapeSvg; +}; + +export default shadedProcess; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts new file mode 100644 index 000000000..e8c8b8ead --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts @@ -0,0 +1,41 @@ +import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '$root/rendering-util/types.d.ts'; +import { styles2String } from '$root/rendering-util/rendering-elements/shapes/handdrawnStyles.js'; + +export async function text(parent: SVGAElement, node: Node): Promise { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + // console.log('IPI labelStyles:', labelStyles); + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + + const totalWidth = Math.max(bbox.width + node.padding, node?.width || 0); + const totalHeight = Math.max(bbox.height + node.padding, node?.height || 0); + const x = -totalWidth / 2; + const y = -totalHeight / 2; + + // log.info('IPI node = ', node); + + let rect; + const { cssStyles } = node; + + rect = shapeSvg.insert('rect', ':first-child'); + + rect + .attr('class', 'text') + .attr('style', nodeStyles) + .attr('rx', 0) + .attr('ry', 0) + .attr('x', x) + .attr('y', y) + .attr('width', totalWidth) + .attr('height', totalHeight); + + updateNodeBounds(node, rect); + + node.intersect = function (point) { + return intersect.rect(node, point); + }; + + return shapeSvg; +} 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 075286be7..9e260e67e 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js @@ -134,3 +134,9 @@ export function insertPolygonShape(parent, w, h, points) { export const getNodeClasses = (node, extra) => (node.look === 'handdrawn' ? 'rough-node' : 'node') + ' ' + node.cssClasses + ' ' + (extra || ''); + +export function createPathFromPoints(points) { + const pointStrings = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`); + pointStrings.push('Z'); + return pointStrings.join(' '); +}