From e8018ed779bd1ebc473d7cba2ed5f5fcf1e64741 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Wed, 15 May 2024 11:20:10 +0200 Subject: [PATCH 1/3] #5237 pass css node style like bgColor, borderColor, borderWeight for roughjs --- .../mermaid/src/diagrams/state/stateDb.js | 28 +++++++++++++++++++ .../mermaid/src/rendering-util/types.d.ts | 5 ++++ 2 files changed, 33 insertions(+) diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index b0c436457..8e2ec5b81 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -760,6 +760,34 @@ function insertOrUpdateNode(nodes, nodeData) { if (!nodeData.id || nodeData.id === '' || nodeData.id === '') { return; } + + //Populate node style attributes if nodeData has classes defined + if (nodeData.classes) { + nodeData.classes.split(' ').forEach((cssClass) => { + if (classes[cssClass]) { + classes[cssClass].styles.forEach((style) => { + // Populate nodeData with style attributes specifically to be used by rough.js + if (style && style.startsWith('fill:')) { + nodeData.backgroundColor = style.replace('fill:', ''); + } + if (style && style.startsWith('stroke:')) { + nodeData.borderColor = style.replace('stroke:', ''); + } + if (style && style.startsWith('stroke-width:')) { + nodeData.borderWidth = style.replace('stroke-width:', ''); + } + + nodeData.style += style + ';'; + }); + classes[cssClass].textStyles.forEach((style) => { + nodeData.labelStyle += style + ';'; + if (style && style.startsWith('fill:')) { + nodeData.labelTextColor = style.replace('fill:', ''); + } + }); + } + }); + } const existingNodeData = nodes.find((node) => node.id === nodeData.id); if (existingNodeData) { //update the existing nodeData diff --git a/packages/mermaid/src/rendering-util/types.d.ts b/packages/mermaid/src/rendering-util/types.d.ts index 4d10498fe..b07d84edc 100644 --- a/packages/mermaid/src/rendering-util/types.d.ts +++ b/packages/mermaid/src/rendering-util/types.d.ts @@ -44,6 +44,11 @@ interface Node { useRough?: boolean; useHtmlLabels?: boolean; centerLabel?: boolean; + + //Node style properties + backgroundColor?: string; + borderColor?: string; + labelTextColor?: string; } // Common properties for any edge in the system From 42a12a62ac31e07b16896e69842b279a026f25e2 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 15 May 2024 15:06:09 +0200 Subject: [PATCH 2/3] #5237 Improved Edge Handling --- .../rendering-elements/edges.js | 175 +++++++++++++++++- 1 file changed, 165 insertions(+), 10 deletions(-) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index 63364b969..e4dedefdc 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -406,6 +406,152 @@ function insertMidpoint(p1, p2) { return [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2]; } +/** + * Given an edge, this function will return the corner points of the edge. This is defined as: + * one point that has a previous point and a next point such as the angle between the previous + * point and the next point is 90 degrees. Meaning that the previous point has the same x coordinate + * as the center point and at the same time the next point has the same y coordinate or vice versa. + * @param points + */ +function extractCornerPoints(points) { + // console.log('abc99 extractCornerPoints: ', points); + const cornerPoints = []; + const cornerPointPositions = []; + for (let i = 1; i < points.length - 1; i++) { + const prev = points[i - 1]; + const curr = points[i]; + const next = points[i + 1]; + // console.log('abc99 extractCornerPoints: ', prev, curr, next); + if ( + prev.x === curr.x && + curr.y === next.y && + Math.abs(curr.x - next.x) > 5 && + Math.abs(curr.y - prev.y) > 5 + ) { + // console.log('abc99 extractCornerPoints got one... '); + console.log('abc99 extractCornerPoints got one... '); + cornerPoints.push(curr); + cornerPointPositions.push(i); + } else if ( + prev.y === curr.y && + curr.x === next.x && + Math.abs(curr.x - prev.x) > 5 && + Math.abs(curr.y - next.y) > 5 + ) { + console.log('abc99 extractCornerPoints got one... ', curr.x - prev.x, curr.y - next.y); + cornerPoints.push(curr); + cornerPointPositions.push(i); + } + } + return { cornerPoints, cornerPointPositions }; +} + +const findAdjacentPoint = function (pointA, pointB, distance) { + const xDiff = pointB.x - pointA.x; + const yDiff = pointB.y - pointA.y; + const length = Math.sqrt(xDiff * xDiff + yDiff * yDiff); + const ratio = distance / length; + return { x: pointB.x - ratio * xDiff, y: pointB.y - ratio * yDiff }; +}; + +/** + * Given an array of points, this function will return a new array of points where the cornershave been removed and replaced with + * adjacent points in each direction. SO a corder will be replaced with a point before and the point after the corner. + */ + +const fixCorners = function (lineData) { + const { cornerPoints, cornerPointPositions } = extractCornerPoints(lineData); + const newLineData = []; + let lastCorner = 0; + if (lineData.length > 3) { + console.log('abc99 fixCorners: ', lineData); + } + for (let i = 0; i < lineData.length; i++) { + if (cornerPointPositions.includes(i)) { + const prevPoint = lineData[i - 1]; + const nextPoint = lineData[i + 1]; + const cornerPoint = lineData[i]; + // newLineData.push(lineData[i]); + // Find point 5 points back and push it to the new array + // console.log('abc99 fixCorners git one: ', cornerPointPositions); + // Find a new point on the line point 5 points back and push it to the new array + const newPrevPoint = findAdjacentPoint(prevPoint, cornerPoint, 5); + const newNextPoint = findAdjacentPoint(nextPoint, cornerPoint, 5); + newLineData.push(newPrevPoint); + + const xDiff = newNextPoint.x - newPrevPoint.x; + const yDiff = newNextPoint.y - newPrevPoint.y; + + const a = Math.sqrt(2) * 2; + let newCornerPoint = { x: cornerPoint.x, y: cornerPoint.y }; + if (cornerPoint.x === newPrevPoint.x) { + // if (yDiff > 0) { + newCornerPoint = { + x: xDiff < 0 ? newPrevPoint.x - 5 + a : newPrevPoint.x + 5 - a, + y: yDiff < 0 ? newPrevPoint.y - a : newPrevPoint.y + a, + }; + // } else { + // newCornerPoint = { x: newPrevPoint.x - a, y: newPrevPoint.y + a }; + // } + } else { + // if (yDiff > 0) { + // newCornerPoint = { x: newPrevPoint.x - 5 + a, y: newPrevPoint.y + a }; + // } else { + newCornerPoint = { + x: xDiff < 0 ? newPrevPoint.x - a : newPrevPoint.x + a, + y: yDiff < 0 ? newPrevPoint.y - 5 + a : newPrevPoint.y + 5 - a, + }; + // } + } + if (lineData.length > 3) { + console.log( + '########### abc99\nCorner point', + cornerPoint, + a, + '\n new points prev: ', + newPrevPoint, + 'Next', + newNextPoint, + 'xDiff: ', + xDiff, + 'yDiff', + yDiff, + 'newCornerPoint', + newCornerPoint + ); + } + + // newLineData.push(cornerPoint); + newLineData.push(newCornerPoint, newNextPoint); + } else { + newLineData.push(lineData[i]); + } + } + if (lineData.length > 3) { + console.log('abc99 fixCorners done: ', newLineData); + } + return newLineData; +}; + +/** + * Given a line, this function will return a new line where the corners are rounded. + * @param lineData + */ +function roundedCornersLine(lineData) { + console.log('abc99 roundedCornersLine: ', lineData); + const newLineData = fixCorners(lineData); + let path = ''; + for (let i = 0; i < newLineData.length; i++) { + if (i === 0) { + path += 'M' + newLineData[i].x + ',' + newLineData[i].y; + } else if (i === newLineData.length - 1) { + path += 'L' + newLineData[i].x + ',' + newLineData[i].y; + } else { + path += 'L' + newLineData[i].x + ',' + newLineData[i].y; + } + } + return path; +} export const insertEdge = function (elem, edge, clusterDb, diagramType, graph, id) { const { handdrawnSeed } = getConfig(); console.log('abc88 InsertEdge - edge: ', edge); @@ -444,7 +590,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, graph, i } // The data for our line - const lineData = points.filter((p) => !Number.isNaN(p.y)); + let lineData = points.filter((p) => !Number.isNaN(p.y)); + const { cornerPoints, cornerPointPositions } = extractCornerPoints(lineData); + lineData = fixCorners(lineData); + // console.log('abc99 lineData: ', lineData, points); let lastPoint = lineData[0]; if (lineData.length > 1) { lastPoint = lineData[lineData.length - 1]; @@ -458,13 +607,13 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, graph, i // console.log('abc99 InsertEdge 3: ', lineData); // This is the accessor function we talked about above let curve; - // curve = curveBasis; + curve = curveBasis; // curve = curveCardinal; // curve = curveLinear; // curve = curveNatural; // curve = curveCatmullRom.alpha(0.5); - curve = curveCatmullRom; - // curve = curveCardinal.tension(1); + // curve = curveCatmullRom; + // curve = curveCardinal.tension(0.7); // curve = curveMonotoneY; // let curve = interpolateToCurve([5], curveNatural, 0.01, 10); // Currently only flowcharts get the curve from the settings, perhaps this should @@ -475,6 +624,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, graph, i } const { x, y } = getLineFunctionsWithOffset(edge); + // const lineFunction = edge.curve ? line().x(x).y(y).curve(curve) : roundedCornersLine; const lineFunction = line().x(x).y(y).curve(curve); // Construct stroke classes based on properties @@ -508,15 +658,11 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, graph, i let useRough = edge.useRough; let svgPath; let path = ''; - const pointArr = []; - edge.points.forEach((point) => { - path += point.x + ',' + point.y + ' '; - pointArr.push([point.x, point.y]); - }); if (useRough) { const rc = rough.svg(elem); - const svgPathNode = rc.path(lineFunction(lineData.splice(0, lineData.length - 1)), { + const ld = Object.assign([], lineData); + const svgPathNode = rc.path(lineFunction(ld.splice(0, ld.length - 1)), { roughness: 0.3, seed: handdrawnSeed, }); @@ -542,6 +688,15 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, graph, i .attr('style', edge.style); } // DEBUG code, adds a red circle at each edge coordinate + // cornerPoints.forEach((point) => { + // elem + // .append('circle') + // .style('stroke', 'blue') + // .style('fill', 'blue') + // .attr('r', 3) + // .attr('cx', point.x) + // .attr('cy', point.y); + // }); // lineData.forEach((point) => { // elem // .append('circle') From 55afd8cdb8fbf61d267aa5130c29878cefaf19fa Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 15 May 2024 15:28:39 +0200 Subject: [PATCH 3/3] #5237 Theme support for stateStart, stateEnd, choice and fork/join --- .../rendering-elements/shapes/choice.ts | 5 ++++- .../rendering-elements/shapes/forkJoin.ts | 5 ++++- .../rendering-elements/shapes/handdrawnStyles.ts | 1 + .../rendering-elements/shapes/stateEnd.ts | 16 +++++----------- .../rendering-elements/shapes/stateStart.ts | 6 +++++- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/choice.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/choice.ts index 1b96dd423..c9e27cf8a 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/choice.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/choice.ts @@ -3,8 +3,11 @@ import type { Node } from '$root/rendering-util/types.d.ts'; import type { SVG } from '$root/diagram-api/types.js'; import rough from 'roughjs'; import { solidStateFill } from './handdrawnStyles.js'; +import { getConfig } from '$root/diagram-api/diagramAPI.js'; export const choice = (parent: SVG, node: Node) => { + const { themeVariables } = getConfig(); + const { lineColor } = themeVariables; const shapeSvg = parent .insert('g') .attr('class', 'node default') @@ -24,7 +27,7 @@ export const choice = (parent: SVG, node: Node) => { const pointArr = points.map(function (d) { return [d.x, d.y]; }); - const roughNode = rc.polygon(pointArr, solidStateFill('black')); + const roughNode = rc.polygon(pointArr, solidStateFill(lineColor)); choice = shapeSvg.insert(() => roughNode); } else { choice = shapeSvg.insert('polygon', ':first-child').attr( diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/forkJoin.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/forkJoin.ts index 340cbe536..687f18847 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/forkJoin.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/forkJoin.ts @@ -5,8 +5,11 @@ import type { Node } from '$root/rendering-util/types.d.ts'; import type { SVG } from '$root/diagram-api/types.js'; import rough from 'roughjs'; import { solidStateFill } from './handdrawnStyles.js'; +import { getConfig } from '$root/diagram-api/diagramAPI.js'; export const forkJoin = (parent: SVG, node: Node, dir: string) => { + const { themeVariables } = getConfig(); + const { lineColor } = themeVariables; const shapeSvg = parent .insert('g') .attr('class', 'node default') @@ -25,7 +28,7 @@ export const forkJoin = (parent: SVG, node: Node, dir: string) => { let shape; if (node.useRough) { const rc = rough.svg(shapeSvg); - const roughNode = rc.rectangle(x, y, width, height, solidStateFill('black')); + const roughNode = rc.rectangle(x, y, width, height, solidStateFill(lineColor)); shape = shapeSvg.insert(() => roughNode); } else { shape = shapeSvg diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handdrawnStyles.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handdrawnStyles.ts index 3db0ad5ed..462ce9f3d 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handdrawnStyles.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handdrawnStyles.ts @@ -10,6 +10,7 @@ export const solidStateFill = (color: string) => { hachureGap: 4, fillWeight: 2, roughness: 0.7, + stroke: color, seed: handdrawnSeed, }; }; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateEnd.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateEnd.ts index ecde36efe..47518a576 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateEnd.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateEnd.ts @@ -5,28 +5,22 @@ import type { Node } from '$root/rendering-util/types.d.ts'; import type { SVG } from '$root/diagram-api/types.js'; import rough from 'roughjs'; import { solidStateFill } from './handdrawnStyles.js'; +import { getConfig } from '$root/diagram-api/diagramAPI.js'; export const stateEnd = (parent: SVG, node: Node) => { + const { themeVariables } = getConfig(); + const { lineColor } = themeVariables; const shapeSvg = parent .insert('g') .attr('class', 'node default') .attr('id', node.domId || node.id); - // const roughNode = rc.circle(0, 0, 14, { - // fill: 'white', - // fillStyle: 'solid', - // roughness: 1, - // stroke: 'black', - // strokeWidth: 1, - // }); - - // circle = shapeSvg.insert(() => roughNode); let circle; let innerCircle; if (node.useRough) { const rc = rough.svg(shapeSvg); - const roughNode = rc.circle(0, 0, 14, { ...solidStateFill('black'), roughness: 0.5 }); - const roughInnerNode = rc.circle(0, 0, 5, { ...solidStateFill('black'), fillStyle: 'solid' }); + const roughNode = rc.circle(0, 0, 14, { ...solidStateFill(lineColor), roughness: 0.5 }); + const roughInnerNode = rc.circle(0, 0, 5, { ...solidStateFill(lineColor), fillStyle: 'solid' }); circle = shapeSvg.insert(() => roughNode); innerCircle = shapeSvg.insert(() => roughInnerNode); } else { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateStart.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateStart.ts index 7ad960578..9474902fa 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateStart.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateStart.ts @@ -5,8 +5,12 @@ import type { Node } from '$root/rendering-util/types.d.ts'; import type { SVG } from '$root/diagram-api/types.js'; import rough from 'roughjs'; import { solidStateFill } from './handdrawnStyles.js'; +import { getConfig } from '$root/diagram-api/diagramAPI.js'; export const stateStart = (parent: SVG, node: Node) => { + const { themeVariables } = getConfig(); + const { lineColor } = themeVariables; + const shapeSvg = parent .insert('g') .attr('class', 'node default') @@ -15,7 +19,7 @@ export const stateStart = (parent: SVG, node: Node) => { let circle; if (node.useRough) { const rc = rough.svg(shapeSvg); - const roughNode = rc.circle(0, 0, 14, solidStateFill('black')); + const roughNode = rc.circle(0, 0, 14, solidStateFill(lineColor)); circle = shapeSvg.insert(() => roughNode); } else { circle = shapeSvg.insert('circle', ':first-child');