From aef991bc49445fb2f89ee32b3c97029b035d981a Mon Sep 17 00:00:00 2001 From: NicolasNewman Date: Sat, 6 Apr 2024 21:12:30 -0500 Subject: [PATCH] feat(arch): XY edges now have a 90deg bend --- .../diagrams/architecture/architectureDb.ts | 2 - .../architecture/architectureRenderer.ts | 175 ++++++++++++++---- .../architecture/architectureTypes.ts | 9 +- .../src/diagrams/architecture/svgDraw.ts | 26 ++- 4 files changed, 155 insertions(+), 57 deletions(-) diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts index 47ccf882c..d8c15afb8 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDb.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -120,7 +120,6 @@ const getGroups = (): ArchitectureGroup[] => { }; const getDataStructures = () => { - console.log('===== createSpatialMap ====='); if (datastructures === undefined) { // Create an adjacency list of the diagram to perform BFS on // Outer reduce applied on all services @@ -169,7 +168,6 @@ const getDataStructures = () => { const [posX, posY] = spatialMap[id]; Object.entries(adj).forEach(([dir, rhsId]) => { if (!visited[rhsId]) { - console.log(`${id} -- ${rhsId}`); spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair( [posX, posY], dir as ArchitectureDirectionPair diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index e38af6927..84638de1e 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -1,4 +1,4 @@ -import cytoscape from 'cytoscape'; +import cytoscape, { Position } from 'cytoscape'; import type { Diagram } from '../../Diagram.js'; import fcose, { FcoseLayoutOptions } from 'cytoscape-fcose'; import type { MermaidConfig } from '../../config.type.js'; @@ -15,6 +15,8 @@ import { ArchitectureDataStructures, ArchitectureDirectionName, getOppositeArchitectureDirection, + isArchitectureDirectionXY, + isArchitectureDirectionY, } from './architectureTypes.js'; import { select } from 'd3'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; @@ -51,6 +53,19 @@ function drawServices( services.forEach((service) => drawService(db, svg, service, conf)); } +function positionServices(db: ArchitectureDB, cy: cytoscape.Core) { + cy.nodes().map((node, id) => { + const data = node.data(); + if (data.type === 'group') return; + data.x = node.position().x; + data.y = node.position().y; + + const nodeElem = db.getElementById(data.id); + nodeElem.attr('transform', 'translate(' + (data.x || 0) + ',' + (data.y || 0) + ')'); + }); +} + + function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) { groups.forEach((group) => { cy.add({ @@ -67,32 +82,41 @@ function addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) { }); } -function positionServices(db: ArchitectureDB, cy: cytoscape.Core) { - cy.nodes().map((node, id) => { - const data = node.data(); - if (data.type === 'group') return; - data.x = node.position().x; - data.y = node.position().y; - console.log(`Position service (${data.id}): (${data.x}, ${data.y})`); - - const nodeElem = db.getElementById(data.id); - nodeElem.attr('transform', 'translate(' + (data.x || 0) + ',' + (data.y || 0) + ')'); - }); -} - function addEdges(lines: ArchitectureLine[], cy: cytoscape.Core) { lines.forEach((line) => { + const { lhs_id, rhs_id, lhs_into, rhs_into, lhs_dir, rhs_dir } = line; + const edgeType = isArchitectureDirectionXY(line.lhs_dir, line.rhs_dir) + ? 'segments' + : 'straight'; + const edge: cytoscape._EdgeSingularData = { + id: `${lhs_id}-${rhs_id}`, + source: lhs_id, + sourceDir: lhs_dir, + sourceArrow: lhs_into, + sourceEndpoint: + lhs_dir === 'L' + ? '0 50%' + : lhs_dir === 'R' + ? '100% 50%' + : lhs_dir === 'T' + ? '50% 0' + : '50% 100%', + target: rhs_id, + targetDir: rhs_dir, + targetArrow: rhs_into, + targetEndpoint: + rhs_dir === 'L' + ? '0 50%' + : rhs_dir === 'R' + ? '100% 50%' + : rhs_dir === 'T' + ? '50% 0' + : '50% 100%', + }; cy.add({ group: 'edges', - data: { - id: `${line.lhs_id}-${line.rhs_id}`, - source: line.lhs_id, - sourceDir: line.lhs_dir, - sourceArrow: line.lhs_into, - target: line.rhs_id, - targetDir: line.rhs_dir, - targetArrow: line.rhs_into, - }, + data: edge, + classes: edgeType, }); }); } @@ -112,8 +136,20 @@ function layoutArchitecture( selector: 'edge', style: { 'curve-style': 'straight', - 'source-endpoint': '50% 50%', - 'target-endpoint': '50% 50%', + 'source-endpoint': 'data(sourceEndpoint)', + 'target-endpoint': 'data(targetEndpoint)', + }, + }, + { + selector: 'edge.segments', + style: { + 'curve-style': 'segments', + 'segment-weights': '0', + 'segment-distances': [0.5], + //@ts-ignore + 'edge-distances': 'endpoints', + 'source-endpoint': 'data(sourceEndpoint)', + 'target-endpoint': 'data(targetEndpoint)', }, }, { @@ -196,8 +232,6 @@ function layoutArchitecture( const invSpatialMap = Object.fromEntries( Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id]) ); - console.log('===== invSpatialMap ====='); - console.log(invSpatialMap); // perform BFS const queue = [posToStr([0, 0])]; @@ -227,7 +261,7 @@ function layoutArchitecture( [ArchitectureDirectionName[ getOppositeArchitectureDirection(dir as ArchitectureDirection) ]]: currId, - gap: 100, + gap: 1.5 * getConfigField('iconSize'), }); } }); @@ -244,12 +278,12 @@ function layoutArchitecture( console.log(`Relative Alignments:`); console.log(relativeConstraints); - cy.layout({ + const layout = cy.layout({ name: 'fcose', quality: 'proof', styleEnabled: false, animate: false, - nodeDimensionsIncludeLabels: true, + nodeDimensionsIncludeLabels: false, // Adjust the edge parameters if it passes through the border of a group // Hacky fix for: https://github.com/iVis-at-Bilkent/cytoscape.js-fcose/issues/67 idealEdgeLength(edge) { @@ -257,14 +291,11 @@ function layoutArchitecture( const { parent: parentA } = nodeA.data(); const { parent: parentB } = nodeB.data(); const elasticity = - parentA === parentB - ? 1.25 * getConfigField('iconSize') - : 0.5 * getConfigField('iconSize'); + parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize'); return elasticity; }, edgeElasticity(edge) { const [nodeA, nodeB] = edge.connectedNodes(); - console.log(nodeA.data()); const { parent: parentA } = nodeA.data(); const { parent: parentB } = nodeB.data(); const elasticity = parentA === parentB ? 0.45 : 0.001; @@ -275,7 +306,79 @@ function layoutArchitecture( vertical: verticalAlignments, }, relativePlacementConstraint: relativeConstraints, - } as FcoseLayoutOptions).run(); + } as FcoseLayoutOptions); + + layout.one('layoutstop', (_event) => { + function getSegmentWeights( + source: Position, + target: Position, + pointX: number, + pointY: number + ) { + let W, D; + const { x: sX, y: sY } = source; + const { x: tX, y: tY } = target; + + D = + (pointY - sY + ((sX - pointX) * (sY - tY)) / (sX - tX)) / + Math.sqrt(1 + Math.pow((sY - tY) / (sX - tX), 2)); + W = Math.sqrt(Math.pow(pointY - sY, 2) + Math.pow(pointX - sX, 2) - Math.pow(D, 2)); + + const distAB = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2)); + W = W / distAB; + + //check whether the point (pointX, pointY) is on right or left of the line src to tgt. for instance : a point C(X, Y) and line (AB). d=(xB-xA)(yC-yA)-(yB-yA)(xC-xA). if d>0, then C is on left of the line. if d<0, it is on right. if d=0, it is on the line. + let delta1 = (tX - sX) * (pointY - sY) - (tY - sY) * (pointX - sX); + switch (true) { + case delta1 >= 0: + delta1 = 1; + break; + case delta1 < 0: + delta1 = -1; + break; + } + //check whether the point (pointX, pointY) is "behind" the line src to tgt + let delta2 = (tX - sX) * (pointX - sX) + (tY - sY) * (pointY - sY); + switch (true) { + case delta2 >= 0: + delta2 = 1; + break; + case delta2 < 0: + delta2 = -1; + break; + } + + D = Math.abs(D) * delta1; //ensure that sign of D is same as sign of delta1. Hence we need to take absolute value of D and multiply by delta1 + W = W * delta2; + + return { + distances: D, + weights: W, + }; + } + cy.startBatch(); + for (let edge of Object.values(cy.edges())) { + if (edge.data) { + let { x: s_x, y: s_y } = edge.source().position(); + let { x: t_x, y: t_y } = edge.target().position(); + if (s_x !== t_x && s_y !== t_y) { + let sEP = edge.sourceEndpoint(); + let tEP = edge.targetEndpoint(); + const { sourceDir } = edge.data(); + const [pointX, pointY] = isArchitectureDirectionY(sourceDir) + ? [sEP.x, tEP.y] + : [tEP.x, sEP.y]; + const { weights, distances } = getSegmentWeights(sEP, tEP, pointX, pointY); + edge.style('segment-distances', distances); + edge.style('segment-weights', weights); + } + } + } + cy.endBatch(); + layout.run(); + }); + layout.run(); + cy.ready((e) => { log.info('Ready', e); resolve(cy); @@ -309,7 +412,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) drawServices(db, servicesElem, services, conf); const cy = await layoutArchitecture(services, groups, lines, ds); - console.log(cy.nodes().map((node) => ({ a: node.data() }))); + // console.log(cy.nodes().map((node) => ({ a: node.data() }))); drawEdges(edgesElem, cy); drawGroups(groupElem, cy); diff --git a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts index fb07d9e37..bf8738e06 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureTypes.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureTypes.ts @@ -21,10 +21,10 @@ export const ArchitectureDirectionArrow = { } as const; export const ArchitectureDirectionArrowShift = { - L: (orig: number, iconSize: number, arrowSize: number) => orig - iconSize / 2 - arrowSize + 2, - R: (orig: number, iconSize: number, arrowSize: number) => orig + iconSize / 2 - 2, - T: (orig: number, iconSize: number, arrowSize: number) => orig - iconSize / 2 - arrowSize + 2, - B: (orig: number, iconSize: number, arrowSize: number) => orig + iconSize / 2 - 2, + L: (orig: number, arrowSize: number) => orig - arrowSize + 2, + R: (orig: number, arrowSize: number) => orig - 2, + T: (orig: number, arrowSize: number) => orig - arrowSize + 2, + B: (orig: number, arrowSize: number) => orig - 2, } as const; export const getOppositeArchitectureDirection = function ( @@ -104,7 +104,6 @@ export const shiftPositionByArchitectureDirectionPair = function ( ): number[] { const lhs = pair[0] as ArchitectureDirection; const rhs = pair[1] as ArchitectureDirection; - console.log(`${pair}: (${x},${y})`); if (isArchitectureDirectionX(lhs)) { if (isArchitectureDirectionY(rhs)) { return [x + (lhs === 'L' ? -1 : 1), y + (rhs === 'T' ? 1 : -1)]; diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts index 8c27550f1..6b1555599 100644 --- a/packages/mermaid/src/diagrams/architecture/svgDraw.ts +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -77,30 +77,29 @@ declare module 'cytoscape' { export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) { const iconSize = getConfigField('iconSize'); - const halfIconSize = iconSize / 2; const arrowSize = iconSize / 6; const halfArrowSize = arrowSize / 2; + cy.edges().map((edge, id) => { - const { source, sourceDir, sourceArrow, target, targetDir, targetArrow } = edge.data(); - if (edge[0]._private.bodyBounds) { + const { sourceDir, sourceArrow, targetDir, targetArrow } = edge.data(); + const { x: startX, y: startY } = edge[0].sourceEndpoint(); + const { x: midX, y: midY } = edge[0].midpoint(); + const { x: endX, y: endY } = edge[0].targetEndpoint(); + if (edge[0]._private.rscratch) { const bounds = edge[0]._private.rscratch; const g = edgesEl.insert('g'); g.insert('path') - .attr( - 'd', - `M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} ` - ) + .attr('d', `M ${startX},${startY} L ${midX},${midY} L${endX},${endY} `) .attr('class', 'edge'); if (sourceArrow) { - console.log(`New source arrow: ${sourceDir} for ${source}`); const xShift = isArchitectureDirectionX(sourceDir) - ? ArchitectureDirectionArrowShift[sourceDir](bounds.startX, iconSize, arrowSize) + ? ArchitectureDirectionArrowShift[sourceDir](bounds.startX, arrowSize) : bounds.startX - halfArrowSize; const yShift = isArchitectureDirectionY(sourceDir) - ? ArchitectureDirectionArrowShift[sourceDir](bounds.startY, iconSize, arrowSize) + ? ArchitectureDirectionArrowShift[sourceDir](bounds.startY, arrowSize) : bounds.startY - halfArrowSize; g.insert('polygon') @@ -109,13 +108,13 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) { .attr('class', 'arrow'); } if (targetArrow) { - console.log(`New target arrow: ${targetDir} for ${target}`); const xShift = isArchitectureDirectionX(targetDir) - ? ArchitectureDirectionArrowShift[targetDir](bounds.endX, iconSize, arrowSize) + ? ArchitectureDirectionArrowShift[targetDir](bounds.endX, arrowSize) : bounds.endX - halfArrowSize; const yShift = isArchitectureDirectionY(targetDir) - ? ArchitectureDirectionArrowShift[targetDir](bounds.endY, iconSize, arrowSize) + ? ArchitectureDirectionArrowShift[targetDir](bounds.endY, arrowSize) : bounds.endY - halfArrowSize; + g.insert('polygon') .attr('points', ArchitectureDirectionArrow[targetDir](arrowSize)) .attr('transform', `translate(${xShift},${yShift})`) @@ -207,7 +206,6 @@ export const drawService = function ( const { width, height } = serviceElem._groups[0][0].getBBox(); service.width = width; service.height = height; - console.log(`Draw service (${service.id})`); db.setElementForId(service.id, serviceElem); return 0; };