mermaid/packages/mermaid/src/diagrams/block/renderHelpers.ts

262 lines
6.7 KiB
TypeScript

import { getStylesFromArray } from '../../utils.js';
import { insertNode, positionNode } from '../../dagre-wrapper/nodes.js';
import { insertEdge, insertEdgeLabel, positionEdgeLabel } from '../../dagre-wrapper/edges.js';
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
import { getConfig } from '../../config.js';
import type { ContainerElement } from 'd3';
import type { Block } from './blockTypes.js';
import type { BlockDB } from './blockDB.js';
interface Node {
classes: string;
}
function getNodeFromBlock(block: Block, db: BlockDB, positioned = false) {
const vertex = block;
let classStr = 'default';
if ((vertex?.classes?.length || 0) > 0) {
classStr = (vertex?.classes || []).join(' ');
}
classStr = classStr + ' flowchart-label';
// We create a SVG label, either by delegating to addHtmlLabel or manually
let radius = 0;
let shape = '';
let layoutOptions = {};
let padding;
// Set the shape based parameters
switch (vertex.type) {
case 'round':
radius = 5;
shape = 'rect';
break;
case 'composite':
radius = 0;
shape = 'composite';
padding = 0;
break;
case 'square':
shape = 'rect';
break;
case 'diamond':
shape = 'question';
layoutOptions = {
portConstraints: 'FIXED_SIDE',
};
break;
case 'hexagon':
shape = 'hexagon';
break;
case 'block_arrow':
shape = 'block_arrow';
break;
case 'odd':
shape = 'rect_left_inv_arrow';
break;
case 'lean_right':
shape = 'lean_right';
break;
case 'lean_left':
shape = 'lean_left';
break;
case 'trapezoid':
shape = 'trapezoid';
break;
case 'inv_trapezoid':
shape = 'inv_trapezoid';
break;
case 'rect_left_inv_arrow':
shape = 'rect_left_inv_arrow';
break;
case 'circle':
shape = 'circle';
break;
case 'ellipse':
shape = 'ellipse';
break;
case 'stadium':
shape = 'stadium';
break;
case 'subroutine':
shape = 'subroutine';
break;
case 'cylinder':
shape = 'cylinder';
break;
case 'group':
shape = 'rect';
break;
case 'doublecircle':
shape = 'doublecircle';
break;
default:
shape = 'rect';
}
const styles = getStylesFromArray(vertex?.styles || []);
// Use vertex id as text in the box if no text is provided by the graph definition
const vertexText = vertex.label;
const bounds = vertex.size || { width: 0, height: 0, x: 0, y: 0 };
// Add the node
const node = {
labelStyle: styles.labelStyle,
shape: shape,
labelText: vertexText,
rx: radius,
ry: radius,
class: classStr,
style: styles.style,
id: vertex.id,
directions: vertex.directions,
width: bounds.width,
height: bounds.height,
x: bounds.x,
y: bounds.y,
positioned,
intersect: undefined,
type: vertex.type,
padding: padding ?? (getConfig()?.block?.padding || 0),
};
return node;
}
async function calculateBlockSize(
elem: d3.Selection<SVGGElement, unknown, HTMLElement, any>,
block: any,
db: any
) {
const node = getNodeFromBlock(block, db, false);
if (node.type === 'group') {
return;
}
// Add the element to the DOM to size it
const nodeEl = await insertNode(elem, node);
const boundingBox = nodeEl.node().getBBox();
const obj = db.getBlock(node.id);
obj.size = { width: boundingBox.width, height: boundingBox.height, x: 0, y: 0, node: nodeEl };
db.setBlock(obj);
nodeEl.remove();
}
type ActionFun = typeof calculateBlockSize;
export async function insertBlockPositioned(elem: any, block: Block, db: any) {
const node = getNodeFromBlock(block, db, true);
// Add the element to the DOM to size it
const obj = db.getBlock(node.id);
if (obj.type !== 'space') {
const nodeEl = await insertNode(elem, node);
block.intersect = node?.intersect;
positionNode(node);
}
}
export async function performOperations(
elem: d3.Selection<SVGGElement, unknown, HTMLElement, any>,
blocks: Block[],
db: BlockDB,
operation: ActionFun
) {
for (const block of blocks) {
await operation(elem, block, db);
if (block.children) {
await performOperations(elem, block.children, db, operation);
}
}
}
export async function calculateBlockSizes(elem: any, blocks: Block[], db: BlockDB) {
await performOperations(elem, blocks, db, calculateBlockSize);
}
export async function insertBlocks(
elem: d3.Selection<SVGGElement, unknown, HTMLElement, any>,
blocks: Block[],
db: BlockDB
) {
await performOperations(elem, blocks, db, insertBlockPositioned);
}
export async function insertEdges(
elem: any,
edges: Block[],
blocks: Block[],
db: BlockDB,
id: string
) {
const g = new graphlib.Graph({
multigraph: true,
compound: true,
});
g.setGraph({
rankdir: 'TB',
nodesep: 10,
ranksep: 10,
marginx: 8,
marginy: 8,
});
for (const block of blocks) {
if (block.size) {
g.setNode(block.id, {
width: block.size.width,
height: block.size.height,
intersect: block.intersect,
});
}
}
for (const edge of edges) {
// elem, e, edge, clusterDb, diagramType, graph;
if (edge.start && edge.end) {
const startBlock = db.getBlock(edge.start);
const endBlock = db.getBlock(edge.end);
if (startBlock?.size && endBlock?.size) {
const start = startBlock.size;
const end = endBlock.size;
const points = [
{ x: start.x, y: start.y },
{ x: start.x + (end.x - start.x) / 2, y: start.y + (end.y - start.y) / 2 },
{ x: end.x, y: end.y },
];
// edge.points = points;
await insertEdge(
elem,
{ v: edge.start, w: edge.end, name: edge.id },
{
...edge,
arrowTypeEnd: edge.arrowTypeEnd,
arrowTypeStart: edge.arrowTypeStart,
points,
classes: 'edge-thickness-normal edge-pattern-solid flowchart-link LS-a1 LE-b1',
},
undefined,
'block',
g,
id
);
if (edge.label) {
await insertEdgeLabel(elem, {
...edge,
label: edge.label,
labelStyle: 'stroke: #333; stroke-width: 1.5px;fill:none;',
arrowTypeEnd: edge.arrowTypeEnd,
arrowTypeStart: edge.arrowTypeStart,
points,
classes: 'edge-thickness-normal edge-pattern-solid flowchart-link LS-a1 LE-b1',
});
await positionEdgeLabel(
{ ...edge, x: points[1].x, y: points[1].y },
{
originalPath: points,
}
);
}
}
}
}
}