mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-14 06:43:25 +08:00
feat(arch): XY edges now have a 90deg bend
This commit is contained in:
parent
6d5791a63a
commit
aef991bc49
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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)];
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user