collab/mermaid/packages/mermaid-layout-elk/src/render.tsblame
View source
6dd74de1import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
6dd74de2// @ts-ignore TODO: Investigate D3 issue
6dd74de3import { curveLinear } from 'd3';
6dd74de4import ELK from 'elkjs/lib/elk.bundled.js';
6dd74de5import { type TreeData, findCommonAncestor } from './find-common-ancestor.js';
6dd74de6
6dd74de7import {
6dd74de8 type P,
6dd74de9 type RectLike,
6dd74de10 outsideNode,
6dd74de11 computeNodeIntersection,
6dd74de12 replaceEndpoint,
6dd74de13 onBorder,
6dd74de14} from './geometry.js';
6dd74de15
6dd74de16type Node = LayoutData['nodes'][number];
6dd74de17
6dd74de18// Minimal structural type to avoid depending on d3 Selection typings
6dd74de19interface D3Selection<T extends Element> {
6dd74de20 node(): T | null;
6dd74de21 attr(name: string, value: string): D3Selection<T>;
6dd74de22}
6dd74de23
6dd74de24interface LabelData {
6dd74de25 width: number;
6dd74de26 height: number;
6dd74de27 wrappingWidth?: number;
6dd74de28 labelNode?: SVGGElement | null;
6dd74de29}
6dd74de30
6dd74de31interface NodeWithVertex extends Omit<Node, 'domId'> {
6dd74de32 children?: LayoutData['nodes'];
6dd74de33 labelData?: LabelData;
6dd74de34 domId?: D3Selection<SVGAElement | SVGGElement>;
6dd74de35}
6dd74de36
6dd74de37export const render = async (
6dd74de38 data4Layout: LayoutData,
6dd74de39 svg: SVG,
6dd74de40 {
6dd74de41 common,
6dd74de42 getConfig,
6dd74de43 insertCluster,
6dd74de44 insertEdge,
6dd74de45 insertEdgeLabel,
6dd74de46 insertMarkers,
6dd74de47 insertNode,
6dd74de48 interpolateToCurve,
6dd74de49 labelHelper,
6dd74de50 log,
6dd74de51 positionEdgeLabel,
6dd74de52 }: InternalHelpers,
6dd74de53 { algorithm }: RenderOptions
6dd74de54) => {
6dd74de55 const nodeDb: Record<string, any> = {};
6dd74de56 const clusterDb: Record<string, any> = {};
6dd74de57
6dd74de58 const addVertex = async (
6dd74de59 nodeEl: SVGGroup,
6dd74de60 graph: { children: NodeWithVertex[] },
6dd74de61 nodeArr: Node[],
6dd74de62 node: Node
6dd74de63 ) => {
6dd74de64 const labelData: LabelData = { width: 0, height: 0 };
6dd74de65
6dd74de66 const config = getConfig();
6dd74de67
6dd74de68 // Add the element to the DOM
6dd74de69 if (!node.isGroup) {
6dd74de70 // const child = node as NodeWithVertex;
6dd74de71 const child: NodeWithVertex = {
6dd74de72 id: node.id,
6dd74de73 width: node.width,
6dd74de74 height: node.height,
6dd74de75 // Store the original node data for later use
6dd74de76 label: node.label,
6dd74de77 isGroup: node.isGroup,
6dd74de78 shape: node.shape,
6dd74de79 padding: node.padding,
6dd74de80 cssClasses: node.cssClasses,
6dd74de81 cssStyles: node.cssStyles,
6dd74de82 look: node.look,
6dd74de83 // Include parentId for subgraph processing
6dd74de84 parentId: node.parentId,
6dd74de85 };
6dd74de86 graph.children.push(child);
6dd74de87 nodeDb[node.id] = node;
6dd74de88
6dd74de89 const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
6dd74de90 const boundingBox = childNodeEl.node()!.getBBox();
6dd74de91 // Store the domId separately for rendering, not in the ELK graph
6dd74de92 child.domId = childNodeEl;
6dd74de93 child.width = boundingBox.width;
6dd74de94 child.height = boundingBox.height;
6dd74de95 } else {
6dd74de96 // A subgraph
6dd74de97 const child: NodeWithVertex & { children: NodeWithVertex[] } = {
6dd74de98 ...node,
6dd74de99 domId: undefined,
6dd74de100 children: [],
6dd74de101 };
6dd74de102 // Let elk render with the copy
6dd74de103 graph.children.push(child);
6dd74de104 // Save the original containing the intersection function
6dd74de105 nodeDb[node.id] = child;
6dd74de106 await addVertices(nodeEl, nodeArr, child, node.id);
6dd74de107
6dd74de108 if (node.label) {
6dd74de109 // @ts-ignore TODO: fix this
6dd74de110 const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true);
6dd74de111 labelData.width = bbox.width;
6dd74de112 labelData.wrappingWidth = config.flowchart!.wrappingWidth;
6dd74de113 // Give some padding for elk
6dd74de114 labelData.height = bbox.height - 2;
6dd74de115 labelData.labelNode = shapeSvg.node();
6dd74de116 // We need the label hight to be able to size the subgraph;
6dd74de117 shapeSvg.remove();
6dd74de118 } else {
6dd74de119 // Subgraph without label
6dd74de120 labelData.width = 0;
6dd74de121 labelData.height = 0;
6dd74de122 }
6dd74de123 child.labelData = labelData;
6dd74de124 child.domId = nodeEl;
6dd74de125 }
6dd74de126 };
6dd74de127
6dd74de128 const addVertices = async function (
6dd74de129 nodeEl: SVGGroup,
6dd74de130 nodeArr: Node[],
6dd74de131 graph: { children: NodeWithVertex[] },
6dd74de132 parentId?: string
6dd74de133 ) {
6dd74de134 const siblings = nodeArr.filter((node) => node?.parentId === parentId);
6dd74de135 log.info('addVertices APA12', siblings, parentId);
6dd74de136 // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
6dd74de137 await Promise.all(
6dd74de138 siblings.map(async (node) => {
6dd74de139 await addVertex(nodeEl, graph, nodeArr, node);
6dd74de140 })
6dd74de141 );
6dd74de142 return graph;
6dd74de143 };
6dd74de144
6dd74de145 const drawNodes = async (
6dd74de146 relX: number,
6dd74de147 relY: number,
6dd74de148 nodeArray: any[],
6dd74de149 svg: any,
6dd74de150 subgraphsEl: SVGGroup,
6dd74de151 depth: number
6dd74de152 ) => {
6dd74de153 await Promise.all(
6dd74de154 nodeArray.map(async function (node: {
6dd74de155 id: string | number;
6dd74de156 x: any;
6dd74de157 y: any;
6dd74de158 width: number;
6dd74de159 labels: { width: any }[];
6dd74de160 height: number;
6dd74de161 isGroup: any;
6dd74de162 labelData: any;
6dd74de163 offset: { posX: number; posY: number };
6dd74de164 shape: any;
6dd74de165 domId: { node: () => any; attr: (arg0: string, arg1: string) => void };
6dd74de166 }) {
6dd74de167 if (node) {
6dd74de168 nodeDb[node.id] ??= {};
6dd74de169 nodeDb[node.id].offset = {
6dd74de170 posX: node.x + relX,
6dd74de171 posY: node.y + relY,
6dd74de172 x: relX,
6dd74de173 y: relY,
6dd74de174 depth,
6dd74de175 width: Math.max(node.width, node.labels ? node.labels[0]?.width || 0 : 0),
6dd74de176 height: node.height,
6dd74de177 };
6dd74de178 if (node.isGroup) {
6dd74de179 log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData);
6dd74de180 const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph');
6dd74de181 // TODO use faster way of cloning
6dd74de182 const clusterNode = JSON.parse(JSON.stringify(node));
6dd74de183 clusterNode.x = node.offset.posX + node.width / 2;
6dd74de184 clusterNode.y = node.offset.posY + node.height / 2;
6dd74de185 clusterNode.width = Math.max(clusterNode.width, node.labelData.width);
6dd74de186 await insertCluster(subgraphEl, clusterNode);
6dd74de187
6dd74de188 log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels);
6dd74de189 } else {
6dd74de190 log.info(
6dd74de191 'Id NODE = ',
6dd74de192 node.id,
6dd74de193 node.x,
6dd74de194 node.y,
6dd74de195 relX,
6dd74de196 relY,
6dd74de197 node.domId.node(),
6dd74de198 `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})`
6dd74de199 );
6dd74de200 node.domId.attr(
6dd74de201 'transform',
6dd74de202 `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})`
6dd74de203 );
6dd74de204 }
6dd74de205 }
6dd74de206 })
6dd74de207 );
6dd74de208
6dd74de209 await Promise.all(
6dd74de210 nodeArray.map(async function (node: { isGroup: any; x: any; y: any; children: any }) {
6dd74de211 if (node?.isGroup) {
6dd74de212 await drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, depth + 1);
6dd74de213 }
6dd74de214 })
6dd74de215 );
6dd74de216 };
6dd74de217
6dd74de218 const addSubGraphs = (nodeArr: any[]): TreeData => {
6dd74de219 const parentLookupDb: TreeData = { parentById: {}, childrenById: {} };
6dd74de220 const subgraphs = nodeArr.filter((node: { isGroup: any }) => node.isGroup);
6dd74de221 log.info('Subgraphs - ', subgraphs);
6dd74de222 subgraphs.forEach((subgraph: { id: string }) => {
6dd74de223 const children = nodeArr.filter((node: { parentId: any }) => node.parentId === subgraph.id);
6dd74de224 children.forEach((node: any) => {
6dd74de225 parentLookupDb.parentById[node.id] = subgraph.id;
6dd74de226 if (parentLookupDb.childrenById[subgraph.id] === undefined) {
6dd74de227 parentLookupDb.childrenById[subgraph.id] = [];
6dd74de228 }
6dd74de229 parentLookupDb.childrenById[subgraph.id].push(node);
6dd74de230 });
6dd74de231 });
6dd74de232
6dd74de233 return parentLookupDb;
6dd74de234 };
6dd74de235
6dd74de236 const getEdgeStartEndPoint = (edge: any) => {
6dd74de237 // edge.start and edge.end are IDs (string/number) in our layout data
6dd74de238 const sourceId: string | number = edge.start;
6dd74de239 const targetId: string | number = edge.end;
6dd74de240
6dd74de241 const source = sourceId;
6dd74de242 const target = targetId;
6dd74de243
6dd74de244 const startNode = nodeDb[sourceId];
6dd74de245 const endNode = nodeDb[targetId];
6dd74de246
6dd74de247 if (!startNode || !endNode) {
6dd74de248 return { source, target };
6dd74de249 }
6dd74de250
6dd74de251 // Add the edge to the graph
6dd74de252 return { source, target, sourceId, targetId };
6dd74de253 };
6dd74de254
6dd74de255 const calcOffset = function (src: string, dest: string, parentLookupDb: TreeData) {
6dd74de256 const ancestor = findCommonAncestor(src, dest, parentLookupDb);
6dd74de257 if (ancestor === undefined || ancestor === 'root') {
6dd74de258 return { x: 0, y: 0 };
6dd74de259 }
6dd74de260
6dd74de261 const ancestorOffset = nodeDb[ancestor].offset;
6dd74de262 return { x: ancestorOffset.posX, y: ancestorOffset.posY };
6dd74de263 };
6dd74de264
6dd74de265 /**
6dd74de266 * Add edges to graph based on parsed graph definition
6dd74de267 */
6dd74de268 // Edge helper maps and utilities (de-duplicated)
6dd74de269 const ARROW_MAP: Record<string, [string, string]> = {
6dd74de270 arrow_open: ['arrow_open', 'arrow_open'],
6dd74de271 arrow_cross: ['arrow_open', 'arrow_cross'],
6dd74de272 double_arrow_cross: ['arrow_cross', 'arrow_cross'],
6dd74de273 arrow_point: ['arrow_open', 'arrow_point'],
6dd74de274 double_arrow_point: ['arrow_point', 'arrow_point'],
6dd74de275 arrow_circle: ['arrow_open', 'arrow_circle'],
6dd74de276 double_arrow_circle: ['arrow_circle', 'arrow_circle'],
6dd74de277 };
6dd74de278
6dd74de279 const computeStroke = (
6dd74de280 stroke: string | undefined,
6dd74de281 defaultStyle?: string,
6dd74de282 defaultLabelStyle?: string
6dd74de283 ) => {
6dd74de284 // Defaults correspond to 'normal'
6dd74de285 let thickness = 'normal';
6dd74de286 let pattern = 'solid';
6dd74de287 let style = '';
6dd74de288 let labelStyle = '';
6dd74de289
6dd74de290 if (stroke === 'dotted') {
6dd74de291 pattern = 'dotted';
6dd74de292 style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
6dd74de293 } else if (stroke === 'thick') {
6dd74de294 thickness = 'thick';
6dd74de295 style = 'stroke-width: 3.5px;fill:none;';
6dd74de296 } else {
6dd74de297 // normal
6dd74de298 style = defaultStyle ?? 'fill:none;';
6dd74de299 if (defaultLabelStyle !== undefined) {
6dd74de300 labelStyle = defaultLabelStyle;
6dd74de301 }
6dd74de302 }
6dd74de303 return { thickness, pattern, style, labelStyle };
6dd74de304 };
6dd74de305
6dd74de306 const getCurve = (edgeInterpolate: any, edgesDefaultInterpolate: any, confCurve: any) => {
6dd74de307 if (edgeInterpolate !== undefined) {
6dd74de308 return interpolateToCurve(edgeInterpolate, curveLinear);
6dd74de309 }
6dd74de310 if (edgesDefaultInterpolate !== undefined) {
6dd74de311 return interpolateToCurve(edgesDefaultInterpolate, curveLinear);
6dd74de312 }
6dd74de313 // @ts-ignore TODO: fix this
6dd74de314 return interpolateToCurve(confCurve, curveLinear);
6dd74de315 };
6dd74de316 const buildEdgeData = (
6dd74de317 edge: any,
6dd74de318 defaults: {
6dd74de319 defaultStyle?: string;
6dd74de320 defaultLabelStyle?: string;
6dd74de321 defaultInterpolate?: any;
6dd74de322 confCurve: any;
6dd74de323 },
6dd74de324 common: any
6dd74de325 ) => {
6dd74de326 const edgeData: any = { style: '', labelStyle: '' };
6dd74de327 edgeData.minlen = edge.length || 1;
6dd74de328 // maintain legacy behavior
6dd74de329 edge.text = edge.label;
6dd74de330
6dd74de331 // Arrowhead fill vs none
6dd74de332 edgeData.arrowhead = edge.type === 'arrow_open' ? 'none' : 'normal';
6dd74de333
6dd74de334 // Arrow types
6dd74de335 const arrowMap = ARROW_MAP[edge.type] ?? ARROW_MAP.arrow_open;
6dd74de336 edgeData.arrowTypeStart = arrowMap[0];
6dd74de337 edgeData.arrowTypeEnd = arrowMap[1];
6dd74de338
6dd74de339 // Optional edge label positioning flags
6dd74de340 edgeData.startLabelRight = edge.startLabelRight;
6dd74de341 edgeData.endLabelLeft = edge.endLabelLeft;
6dd74de342
6dd74de343 // Stroke
6dd74de344 const strokeRes = computeStroke(edge.stroke, defaults.defaultStyle, defaults.defaultLabelStyle);
6dd74de345 edgeData.thickness = strokeRes.thickness;
6dd74de346 edgeData.pattern = strokeRes.pattern;
6dd74de347 edgeData.style = (edgeData.style || '') + (strokeRes.style || '');
6dd74de348 edgeData.labelStyle = (edgeData.labelStyle || '') + (strokeRes.labelStyle || '');
6dd74de349
6dd74de350 // Curve
6dd74de351 // @ts-ignore - defaults.confCurve is present at runtime but missing in type
6dd74de352 edgeData.curve = getCurve(edge.interpolate, defaults.defaultInterpolate, defaults.confCurve);
6dd74de353
6dd74de354 // Arrowhead style + labelpos when we have label text
6dd74de355 const hasText = (edge?.text ?? '') !== '';
6dd74de356 if (hasText) {
6dd74de357 edgeData.arrowheadStyle = 'fill: #333';
6dd74de358 edgeData.labelpos = 'c';
6dd74de359 } else if (edge.style !== undefined) {
6dd74de360 edgeData.arrowheadStyle = 'fill: #333';
6dd74de361 }
6dd74de362
6dd74de363 edgeData.labelType = edge.labelType;
6dd74de364 edgeData.label = (edge?.text ?? '').replace(common.lineBreakRegex, '\n');
6dd74de365
6dd74de366 if (edge.style === undefined) {
6dd74de367 edgeData.style = edgeData.style ?? 'stroke: #333; stroke-width: 1.5px;fill:none;';
6dd74de368 }
6dd74de369
6dd74de370 edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
6dd74de371 return edgeData;
6dd74de372 };
6dd74de373
6dd74de374 const addEdges = async function (
6dd74de375 dataForLayout: { edges: any; direction?: string },
6dd74de376 graph: {
6dd74de377 id?: string;
6dd74de378 layoutOptions?: {
6dd74de379 'elk.hierarchyHandling': string;
6dd74de380 'elk.algorithm': any;
6dd74de381 'nodePlacement.strategy': any;
6dd74de382 'elk.layered.mergeEdges': any;
6dd74de383 'elk.direction': string;
6dd74de384 'spacing.baseValue': number;
6dd74de385 };
6dd74de386 children?: never[];
6dd74de387 edges: any;
6dd74de388 },
6dd74de389 svg: SVG
6dd74de390 ) {
6dd74de391 log.info('abc78 DAGA edges = ', dataForLayout);
6dd74de392 const edges = dataForLayout.edges;
6dd74de393 const labelsEl = svg.insert('g').attr('class', 'edgeLabels');
6dd74de394 const linkIdCnt: any = {};
6dd74de395 let defaultStyle: string | undefined;
6dd74de396 let defaultLabelStyle: string | undefined;
6dd74de397
6dd74de398 await Promise.all(
6dd74de399 edges.map(async function (edge: {
6dd74de400 id: string;
6dd74de401 start: string;
6dd74de402 end: string;
6dd74de403 length: number;
6dd74de404 text: undefined;
6dd74de405 label: any;
6dd74de406 type: string;
6dd74de407 stroke: any;
6dd74de408 interpolate: undefined;
6dd74de409 style: undefined;
6dd74de410 labelType: any;
6dd74de411 startLabelRight?: string;
6dd74de412 endLabelLeft?: string;
6dd74de413 }) {
6dd74de414 // Identify Link
6dd74de415 const linkIdBase = edge.id; // 'L-' + edge.start + '-' + edge.end;
6dd74de416 // count the links from+to the same node to give unique id
6dd74de417 if (linkIdCnt[linkIdBase] === undefined) {
6dd74de418 linkIdCnt[linkIdBase] = 0;
6dd74de419 log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
6dd74de420 } else {
6dd74de421 linkIdCnt[linkIdBase]++;
6dd74de422 log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
6dd74de423 }
6dd74de424 const linkId = linkIdBase; // + '_' + linkIdCnt[linkIdBase];
6dd74de425 edge.id = linkId;
6dd74de426 log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]);
6dd74de427 const linkNameStart = 'LS_' + edge.start;
6dd74de428 const linkNameEnd = 'LE_' + edge.end;
6dd74de429
6dd74de430 const conf = getConfig();
6dd74de431 const edgeData = buildEdgeData(
6dd74de432 edge,
6dd74de433 {
6dd74de434 defaultStyle,
6dd74de435 defaultLabelStyle,
6dd74de436 defaultInterpolate: edges.defaultInterpolate,
6dd74de437 // @ts-ignore - conf.curve exists at runtime but is missing from typing
6dd74de438 confCurve: conf.curve,
6dd74de439 },
6dd74de440 common
6dd74de441 );
6dd74de442
6dd74de443 edgeData.id = linkId;
6dd74de444 edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
6dd74de445
6dd74de446 const labelEl = await insertEdgeLabel(labelsEl, edgeData);
6dd74de447
6dd74de448 // calculate start and end points of the edge, note that the source and target
6dd74de449 // can be modified for shapes that have ports
6dd74de450
6dd74de451 const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge);
6dd74de452 log.debug('abc78 source and target', source, target);
6dd74de453 // Add the edge to the graph
6dd74de454 graph.edges.push({
6dd74de455 ...edge,
6dd74de456 sources: [source],
6dd74de457 targets: [target],
6dd74de458 sourceId,
6dd74de459 targetId,
6dd74de460 labelEl: labelEl,
6dd74de461 labels: [
6dd74de462 {
6dd74de463 width: edgeData.width,
6dd74de464 height: edgeData.height,
6dd74de465 orgWidth: edgeData.width,
6dd74de466 orgHeight: edgeData.height,
6dd74de467 text: edgeData.label,
6dd74de468 layoutOptions: {
6dd74de469 'edgeLabels.inline': 'true',
6dd74de470 'edgeLabels.placement': 'CENTER',
6dd74de471 },
6dd74de472 },
6dd74de473 ],
6dd74de474 edgeData,
6dd74de475 });
6dd74de476 })
6dd74de477 );
6dd74de478 return graph;
6dd74de479 };
6dd74de480
6dd74de481 function dir2ElkDirection(dir: any) {
6dd74de482 switch (dir) {
6dd74de483 case 'LR':
6dd74de484 return 'RIGHT';
6dd74de485 case 'RL':
6dd74de486 return 'LEFT';
6dd74de487 case 'TB':
6dd74de488 case 'TD': // TD is an alias for TB in Mermaid
6dd74de489 return 'DOWN';
6dd74de490 case 'BT':
6dd74de491 return 'UP';
6dd74de492 default:
6dd74de493 return 'DOWN';
6dd74de494 }
6dd74de495 }
6dd74de496
6dd74de497 function setIncludeChildrenPolicy(nodeId: string, ancestorId: string) {
6dd74de498 const node = nodeDb[nodeId];
6dd74de499
6dd74de500 if (!node) {
6dd74de501 return;
6dd74de502 }
6dd74de503 if (node?.layoutOptions === undefined) {
6dd74de504 node.layoutOptions = {};
6dd74de505 }
6dd74de506 node.layoutOptions['elk.hierarchyHandling'] = 'INCLUDE_CHILDREN';
6dd74de507 if (node.id !== ancestorId) {
6dd74de508 setIncludeChildrenPolicy(node.parentId, ancestorId);
6dd74de509 }
6dd74de510 }
6dd74de511
6dd74de512 // Node bounds helpers (global)
6dd74de513 const getEffectiveGroupWidth = (node: any): number => {
6dd74de514 const labelW = node?.labels?.[0]?.width ?? 0;
6dd74de515 const padding = node?.padding ?? 0;
6dd74de516 return Math.max(node.width ?? 0, labelW + padding);
6dd74de517 };
6dd74de518
6dd74de519 const boundsFor = (node: any): RectLike => {
6dd74de520 const width = node?.isGroup ? getEffectiveGroupWidth(node) : node.width;
6dd74de521 return {
6dd74de522 x: node.offset.posX + node.width / 2,
6dd74de523 y: node.offset.posY + node.height / 2,
6dd74de524 width,
6dd74de525 height: node.height,
6dd74de526 padding: node.padding,
6dd74de527 };
6dd74de528 };
6dd74de529 // Helper utilities for endpoint handling around cutter2
6dd74de530 type Side = 'start' | 'end';
6dd74de531 const approxEq = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) < eps;
6dd74de532 const isCenterApprox = (pt: P, node: { x: number; y: number }) =>
6dd74de533 approxEq(pt.x, node.x) && approxEq(pt.y, node.y);
6dd74de534
6dd74de535 const getCandidateBorderPoint = (
6dd74de536 points: P[],
6dd74de537 node: any,
6dd74de538 side: Side
6dd74de539 ): { candidate: P; centerApprox: boolean } => {
6dd74de540 if (!points?.length) {
6dd74de541 return { candidate: { x: node.x, y: node.y } as P, centerApprox: true };
6dd74de542 }
6dd74de543 if (side === 'start') {
6dd74de544 const first = points[0];
6dd74de545 const centerApprox = isCenterApprox(first, node);
6dd74de546 const candidate = centerApprox && points.length > 1 ? points[1] : first;
6dd74de547 return { candidate, centerApprox };
6dd74de548 } else {
6dd74de549 const last = points[points.length - 1];
6dd74de550 const centerApprox = isCenterApprox(last, node);
6dd74de551 const candidate = centerApprox && points.length > 1 ? points[points.length - 2] : last;
6dd74de552 return { candidate, centerApprox };
6dd74de553 }
6dd74de554 };
6dd74de555
6dd74de556 const dropAutoCenterPoint = (points: P[], side: Side, doDrop: boolean) => {
6dd74de557 if (!doDrop) {
6dd74de558 return;
6dd74de559 }
6dd74de560 if (side === 'start') {
6dd74de561 if (points.length > 0) {
6dd74de562 points.shift();
6dd74de563 }
6dd74de564 } else {
6dd74de565 if (points.length > 0) {
6dd74de566 points.pop();
6dd74de567 }
6dd74de568 }
6dd74de569 };
6dd74de570
6dd74de571 const applyStartIntersectionIfNeeded = (points: P[], startNode: any, startBounds: RectLike) => {
6dd74de572 let firstOutsideStartIndex = -1;
6dd74de573 for (const [i, p] of points.entries()) {
6dd74de574 if (outsideNode(startBounds, p)) {
6dd74de575 firstOutsideStartIndex = i;
6dd74de576 break;
6dd74de577 }
6dd74de578 }
6dd74de579 if (firstOutsideStartIndex !== -1) {
6dd74de580 const outsidePointForStart = points[firstOutsideStartIndex];
6dd74de581 const startCenter = points[0];
6dd74de582 const startIntersection = computeNodeIntersection(
6dd74de583 startNode,
6dd74de584 startBounds,
6dd74de585 outsidePointForStart,
6dd74de586 startCenter
6dd74de587 );
6dd74de588 replaceEndpoint(points, 'start', startIntersection);
6dd74de589 log.debug('UIO cutter2: start-only intersection applied', { startIntersection });
6dd74de590 }
6dd74de591 };
6dd74de592
6dd74de593 const applyEndIntersectionIfNeeded = (points: P[], endNode: any, endBounds: RectLike) => {
6dd74de594 let outsideIndexForEnd = -1;
6dd74de595 for (let i = points.length - 1; i >= 0; i--) {
6dd74de596 if (outsideNode(endBounds, points[i])) {
6dd74de597 outsideIndexForEnd = i;
6dd74de598 break;
6dd74de599 }
6dd74de600 }
6dd74de601 if (outsideIndexForEnd !== -1) {
6dd74de602 const outsidePointForEnd = points[outsideIndexForEnd];
6dd74de603 const endCenter = points[points.length - 1];
6dd74de604 const endIntersection = computeNodeIntersection(
6dd74de605 endNode,
6dd74de606 endBounds,
6dd74de607 outsidePointForEnd,
6dd74de608 endCenter
6dd74de609 );
6dd74de610 replaceEndpoint(points, 'end', endIntersection);
6dd74de611 log.debug('UIO cutter2: end-only intersection applied', { endIntersection });
6dd74de612 }
6dd74de613 };
6dd74de614
6dd74de615 const cutter2 = (startNode: any, endNode: any, _points: any[]) => {
6dd74de616 const startBounds = boundsFor(startNode);
6dd74de617 const endBounds = boundsFor(endNode);
6dd74de618
6dd74de619 if (_points.length === 0) {
6dd74de620 return [];
6dd74de621 }
6dd74de622
6dd74de623 // Copy the original points array
6dd74de624 const points: P[] = [..._points] as P[];
6dd74de625
6dd74de626 // The first point is the center of sNode, the last point is the center of eNode
6dd74de627 const startCenter = points[0];
6dd74de628 const endCenter = points[points.length - 1];
6dd74de629
6dd74de630 // Minimal, structured logging for diagnostics
6dd74de631 log.debug('PPP cutter2: bounds', { startBounds, endBounds });
6dd74de632 log.debug('PPP cutter2: original points', _points);
6dd74de633
6dd74de634 let firstOutsideStartIndex = -1;
6dd74de635
6dd74de636 // Single iteration through the array
6dd74de637 for (const [i, point] of points.entries()) {
6dd74de638 if (firstOutsideStartIndex === -1 && outsideNode(startBounds, point)) {
6dd74de639 firstOutsideStartIndex = i;
6dd74de640 }
6dd74de641 if (outsideNode(endBounds, point)) {
6dd74de642 // keep scanning; we'll also scan from the end for the last outside point
6dd74de643 }
6dd74de644 }
6dd74de645
6dd74de646 // Calculate intersection with start node if we found a point outside it
6dd74de647 if (firstOutsideStartIndex !== -1) {
6dd74de648 const outsidePointForStart = points[firstOutsideStartIndex];
6dd74de649 const startIntersection = computeNodeIntersection(
6dd74de650 startNode,
6dd74de651 startBounds,
6dd74de652 outsidePointForStart,
6dd74de653 startCenter
6dd74de654 );
6dd74de655 log.debug('UIO cutter2: start intersection', startIntersection);
6dd74de656 replaceEndpoint(points, 'start', startIntersection);
6dd74de657 }
6dd74de658
6dd74de659 // Calculate intersection with end node
6dd74de660 let outsidePointForEnd = null;
6dd74de661 let outsideIndexForEnd = -1;
6dd74de662
6dd74de663 for (let i = points.length - 1; i >= 0; i--) {
6dd74de664 if (outsideNode(endBounds, points[i])) {
6dd74de665 outsidePointForEnd = points[i];
6dd74de666 outsideIndexForEnd = i;
6dd74de667 break;
6dd74de668 }
6dd74de669 }
6dd74de670
6dd74de671 if (!outsidePointForEnd && points.length > 1) {
6dd74de672 outsidePointForEnd = points[points.length - 2];
6dd74de673 outsideIndexForEnd = points.length - 2;
6dd74de674 }
6dd74de675
6dd74de676 if (outsidePointForEnd) {
6dd74de677 const endIntersection = computeNodeIntersection(
6dd74de678 endNode,
6dd74de679 endBounds,
6dd74de680 outsidePointForEnd,
6dd74de681 endCenter
6dd74de682 );
6dd74de683 log.debug('UIO cutter2: end intersection', { endIntersection, outsideIndexForEnd });
6dd74de684 replaceEndpoint(points, 'end', endIntersection);
6dd74de685 }
6dd74de686
6dd74de687 // Final cleanup: Check if the last point is too close to the previous point
6dd74de688 if (points.length > 1) {
6dd74de689 const lastPoint = points[points.length - 1];
6dd74de690 const secondLastPoint = points[points.length - 2];
6dd74de691 const distance = Math.sqrt(
6dd74de692 (lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2
6dd74de693 );
6dd74de694 if (distance < 2) {
6dd74de695 log.debug('UIO cutter2: trimming tail point (too close)', {
6dd74de696 distance,
6dd74de697 lastPoint,
6dd74de698 secondLastPoint,
6dd74de699 });
6dd74de700 points.pop();
6dd74de701 }
6dd74de702 }
6dd74de703
6dd74de704 log.debug('UIO cutter2: final points', points);
6dd74de705
6dd74de706 return points;
6dd74de707 };
6dd74de708
6dd74de709 // @ts-ignore - ELK is not typed
6dd74de710 const elk = new ELK();
6dd74de711 const element = svg.select('g');
6dd74de712 // Add the arrowheads to the svg
6dd74de713 insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
6dd74de714
6dd74de715 // Setup the graph with the layout options and the data for the layout
6dd74de716 let elkGraph: any = {
6dd74de717 id: 'root',
6dd74de718 layoutOptions: {
6dd74de719 'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
6dd74de720 'elk.algorithm': algorithm,
6dd74de721 'nodePlacement.strategy': data4Layout.config.elk?.nodePlacementStrategy,
6dd74de722 'elk.layered.mergeEdges': data4Layout.config.elk?.mergeEdges,
6dd74de723 'elk.direction': 'DOWN',
6dd74de724 'spacing.baseValue': 40,
6dd74de725 'elk.layered.crossingMinimization.forceNodeModelOrder':
6dd74de726 data4Layout.config.elk?.forceNodeModelOrder,
6dd74de727 'elk.layered.considerModelOrder.strategy': data4Layout.config.elk?.considerModelOrder,
6dd74de728 'elk.layered.unnecessaryBendpoints': true,
6dd74de729 'elk.layered.cycleBreaking.strategy': data4Layout.config.elk?.cycleBreakingStrategy,
6dd74de730
6dd74de731 // 'elk.layered.cycleBreaking.strategy': 'GREEDY_MODEL_ORDER',
6dd74de732 // 'elk.layered.cycleBreaking.strategy': 'MODEL_ORDER',
6dd74de733 // 'spacing.nodeNode': 20,
6dd74de734 // 'spacing.nodeNodeBetweenLayers': 25,
6dd74de735 // 'spacing.edgeNode': 20,
6dd74de736 // 'spacing.edgeNodeBetweenLayers': 10,
6dd74de737 // 'spacing.edgeEdge': 10,
6dd74de738 // 'spacing.edgeEdgeBetweenLayers': 20,
6dd74de739 // 'spacing.nodeSelfLoop': 20,
6dd74de740
6dd74de741 // Tweaking options
6dd74de742 // 'nodePlacement.favorStraightEdges': true,
6dd74de743 // 'elk.layered.nodePlacement.favorStraightEdges': true,
6dd74de744 // 'nodePlacement.feedbackEdges': true,
6dd74de745 'elk.layered.wrapping.multiEdge.improveCuts': true,
6dd74de746 'elk.layered.wrapping.multiEdge.improveWrappedEdges': true,
6dd74de747 // 'elk.layered.wrapping.strategy': 'MULTI_EDGE',
6dd74de748 // 'elk.layered.wrapping.strategy': 'SINGLE_EDGE',
6dd74de749 'elk.layered.edgeRouting.selfLoopDistribution': 'EQUALLY',
6dd74de750 'elk.layered.mergeHierarchyEdges': true,
6dd74de751
6dd74de752 // 'elk.layered.feedbackEdges': true,
6dd74de753 // 'elk.layered.crossingMinimization.semiInteractive': true,
6dd74de754 // 'elk.layered.edgeRouting.splines.sloppy.layerSpacingFactor': 1,
6dd74de755 // 'elk.layered.edgeRouting.polyline.slopedEdgeZoneWidth': 4.0,
6dd74de756 // 'elk.layered.wrapping.validify.strategy': 'LOOK_BACK',
6dd74de757 // 'elk.insideSelfLoops.activate': true,
6dd74de758 // 'elk.separateConnectedComponents': true,
6dd74de759 // 'elk.alg.layered.options.EdgeStraighteningStrategy': 'NONE',
6dd74de760 // 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES', // NODES_AND_EDGES
6dd74de761 // 'elk.layered.considerModelOrder.strategy': 'EDGES', // NODES_AND_EDGES
6dd74de762 // 'elk.layered.wrapping.cutting.strategy': 'ARD', // NODES_AND_EDGES
6dd74de763 },
6dd74de764 children: [],
6dd74de765 edges: [],
6dd74de766 };
6dd74de767
6dd74de768 log.info('Drawing flowchart using v4 renderer', elk);
6dd74de769
6dd74de770 // Set the direction of the graph based on the parsed information
6dd74de771 const dir = data4Layout.direction ?? 'DOWN';
6dd74de772 elkGraph.layoutOptions['elk.direction'] = dir2ElkDirection(dir);
6dd74de773
6dd74de774 // Create the lookup db for the subgraphs and their children to used when creating
6dd74de775 // the tree structured graph
6dd74de776 const parentLookupDb: any = addSubGraphs(data4Layout.nodes);
6dd74de777
6dd74de778 // Add elements in the svg to be used to hold the subgraphs container
6dd74de779 // elements and the nodes
6dd74de780 const subGraphsEl = svg.insert('g').attr('class', 'subgraphs');
6dd74de781
6dd74de782 const nodeEl = svg.insert('g').attr('class', 'nodes');
6dd74de783
6dd74de784 // Add the nodes to the graph, this will entail creating the actual nodes
6dd74de785 // in order to get the size of the node. You can't get the size of a node
6dd74de786 // that is not in the dom so we need to add it to the dom, get the size
6dd74de787 // we will position the nodes when we get the layout from elkjs
6dd74de788 elkGraph = await addVertices(nodeEl, data4Layout.nodes, elkGraph);
6dd74de789 // Time for the edges, we start with adding an element in the node to hold the edges
6dd74de790 const edgesEl = svg.insert('g').attr('class', 'edges edgePaths');
6dd74de791
6dd74de792 // Add the edges to the elk graph, this will entail creating the actual edges
6dd74de793 elkGraph = await addEdges(data4Layout, elkGraph, svg);
6dd74de794
6dd74de795 // Iterate through all nodes and add the top level nodes to the graph
6dd74de796 const nodes = data4Layout.nodes;
6dd74de797 nodes.forEach((n: { id: string | number }) => {
6dd74de798 const node = nodeDb[n.id];
6dd74de799
6dd74de800 // Subgraph
6dd74de801 if (parentLookupDb.childrenById[node.id] !== undefined) {
6dd74de802 // Set label and adjust node width separately (avoid side effects in labels array)
6dd74de803 node.labels = [
6dd74de804 {
6dd74de805 text: node.label,
6dd74de806 width: node?.labelData?.width ?? 50,
6dd74de807 height: node?.labelData?.height ?? 50,
6dd74de808 },
6dd74de809 ];
6dd74de810 node.width = node.width + 2 * node.padding;
6dd74de811 log.debug('UIO node label', node?.labelData?.width, node.padding);
6dd74de812 node.layoutOptions = {
6dd74de813 'spacing.baseValue': 30,
6dd74de814 'nodeLabels.placement': '[H_CENTER V_TOP, INSIDE]',
6dd74de815 };
6dd74de816 if (node.dir) {
6dd74de817 node.layoutOptions = {
6dd74de818 ...node.layoutOptions,
6dd74de819 'elk.algorithm': algorithm,
6dd74de820 'elk.direction': dir2ElkDirection(node.dir),
6dd74de821 'nodePlacement.strategy': data4Layout.config.elk?.nodePlacementStrategy,
6dd74de822 'elk.layered.mergeEdges': data4Layout.config.elk?.mergeEdges,
6dd74de823 'elk.hierarchyHandling': 'SEPARATE_CHILDREN',
6dd74de824 };
6dd74de825 }
6dd74de826 delete node.x;
6dd74de827 delete node.y;
6dd74de828 delete node.width;
6dd74de829 delete node.height;
6dd74de830 }
6dd74de831 });
6dd74de832 log.debug('APA01 processing edges, count:', elkGraph.edges.length);
6dd74de833 elkGraph.edges.forEach((edge: any, index: number) => {
6dd74de834 log.debug('APA01 processing edge', index, ':', edge);
6dd74de835 const source = edge.sources[0];
6dd74de836 const target = edge.targets[0];
6dd74de837 log.debug('APA01 source:', source, 'target:', target);
6dd74de838 log.debug('APA01 nodeDb[source]:', nodeDb[source]);
6dd74de839 log.debug('APA01 nodeDb[target]:', nodeDb[target]);
6dd74de840
6dd74de841 if (nodeDb[source] && nodeDb[target] && nodeDb[source].parentId !== nodeDb[target].parentId) {
6dd74de842 const ancestorId = findCommonAncestor(source, target, parentLookupDb);
6dd74de843 // an edge that breaks a subgraph has been identified, set configuration accordingly
6dd74de844 setIncludeChildrenPolicy(source, ancestorId);
6dd74de845 setIncludeChildrenPolicy(target, ancestorId);
6dd74de846 }
6dd74de847 });
6dd74de848
6dd74de849 log.debug('APA01 before');
6dd74de850 log.debug('APA01 elkGraph structure:', JSON.stringify(elkGraph, null, 2));
6dd74de851 log.debug('APA01 elkGraph.children length:', elkGraph.children?.length);
6dd74de852 log.debug('APA01 elkGraph.edges length:', elkGraph.edges?.length);
6dd74de853
6dd74de854 // Validate that all edge references exist as nodes
6dd74de855 elkGraph.edges?.forEach((edge: any, index: number) => {
6dd74de856 log.debug(`APA01 validating edge ${index}:`, edge);
6dd74de857 if (edge.sources) {
6dd74de858 edge.sources.forEach((sourceId: any) => {
6dd74de859 const sourceExists = elkGraph.children?.some((child: any) => child.id === sourceId);
6dd74de860 log.debug(`APA01 source ${sourceId} exists:`, sourceExists);
6dd74de861 });
6dd74de862 }
6dd74de863 if (edge.targets) {
6dd74de864 edge.targets.forEach((targetId: any) => {
6dd74de865 const targetExists = elkGraph.children?.some((child: any) => child.id === targetId);
6dd74de866 log.debug(`APA01 target ${targetId} exists:`, targetExists);
6dd74de867 });
6dd74de868 }
6dd74de869 });
6dd74de870
6dd74de871 let g;
6dd74de872 try {
6dd74de873 g = await elk.layout(elkGraph);
6dd74de874 log.debug('APA01 after - success');
6dd74de875 log.info('APA01 layout result:', JSON.stringify(g, null, 2));
6dd74de876 } catch (error) {
6dd74de877 log.error('APA01 ELK layout error:', error);
6dd74de878 log.error('APA01 elkGraph that caused error:', JSON.stringify(elkGraph, null, 2));
6dd74de879 throw error;
6dd74de880 }
6dd74de881
6dd74de882 // debugger;
6dd74de883 await drawNodes(0, 0, g.children, svg, subGraphsEl, 0);
6dd74de884
6dd74de885 g.edges?.map(
6dd74de886 (edge: {
6dd74de887 sources: (string | number)[];
6dd74de888 targets: (string | number)[];
6dd74de889 start: any;
6dd74de890 end: any;
6dd74de891 sections: { startPoint: any; endPoint: any; bendPoints: any }[];
6dd74de892 points: any[];
6dd74de893 x: any;
6dd74de894 labels: { height: number; width: number; x: number; y: number }[];
6dd74de895 y: any;
6dd74de896 }) => {
6dd74de897 // (elem, edge, clusterDb, diagramType, graph, id)
6dd74de898 const startNode = nodeDb[edge.sources[0]];
6dd74de899 const startCluster = parentLookupDb[edge.sources[0]];
6dd74de900 const endNode = nodeDb[edge.targets[0]];
6dd74de901 const sourceId = edge.start;
6dd74de902 const targetId = edge.end;
6dd74de903
6dd74de904 const offset = calcOffset(sourceId, targetId, parentLookupDb);
6dd74de905 log.debug(
6dd74de906 'APA18 offset',
6dd74de907 offset,
6dd74de908 sourceId,
6dd74de909 ' ==> ',
6dd74de910 targetId,
6dd74de911 'edge:',
6dd74de912 edge,
6dd74de913 'cluster:',
6dd74de914 startCluster,
6dd74de915 startNode
6dd74de916 );
6dd74de917 if (edge.sections) {
6dd74de918 const src = edge.sections[0].startPoint;
6dd74de919 const dest = edge.sections[0].endPoint;
6dd74de920 const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : [];
6dd74de921
6dd74de922 const segPoints = segments.map((segment: { x: any; y: any }) => {
6dd74de923 return { x: segment.x + offset.x, y: segment.y + offset.y };
6dd74de924 });
6dd74de925 edge.points = [
6dd74de926 { x: src.x + offset.x, y: src.y + offset.y },
6dd74de927 ...segPoints,
6dd74de928 { x: dest.x + offset.x, y: dest.y + offset.y },
6dd74de929 ];
6dd74de930
6dd74de931 let sw = startNode.width;
6dd74de932 let ew = endNode.width;
6dd74de933 if (startNode.isGroup) {
6dd74de934 const bbox = startNode.domId.node().getBBox();
6dd74de935 // sw = Math.max(bbox.width, startNode.width, startNode.labels[0].width);
6dd74de936 sw = Math.max(startNode.width, startNode.labels[0].width + startNode.padding);
6dd74de937 // sw = startNode.width;
6dd74de938 log.info(
6dd74de939 'UIO width',
6dd74de940 startNode.id,
6dd74de941 startNode.width,
6dd74de942 'bbox.width=',
6dd74de943 bbox.width,
6dd74de944 'lw=',
6dd74de945 startNode.labels[0].width,
6dd74de946 'node:',
6dd74de947 startNode.width,
6dd74de948 'SW = ',
6dd74de949 sw
6dd74de950 // 'HTML:',
6dd74de951 // startNode.domId.node().innerHTML
6dd74de952 );
6dd74de953 }
6dd74de954 if (endNode.isGroup) {
6dd74de955 const bbox = endNode.domId.node().getBBox();
6dd74de956 ew = Math.max(endNode.width, endNode.labels[0].width + endNode.padding);
6dd74de957
6dd74de958 log.debug(
6dd74de959 'UIO width',
6dd74de960 startNode.id,
6dd74de961 startNode.width,
6dd74de962 bbox.width,
6dd74de963 'EW = ',
6dd74de964 ew,
6dd74de965 'HTML:',
6dd74de966 startNode.innerHTML
6dd74de967 );
6dd74de968 }
6dd74de969 startNode.x = startNode.offset.posX + startNode.width / 2;
6dd74de970 startNode.y = startNode.offset.posY + startNode.height / 2;
6dd74de971 endNode.x = endNode.offset.posX + endNode.width / 2;
6dd74de972 endNode.y = endNode.offset.posY + endNode.height / 2;
6dd74de973
6dd74de974 // Only add center points for non-subgraph nodes or when the edge path doesn't already end near the target
6dd74de975 const shouldAddStartCenter = startNode.shape !== 'rect33';
6dd74de976 const shouldAddEndCenter = endNode.shape !== 'rect33';
6dd74de977
6dd74de978 if (shouldAddStartCenter) {
6dd74de979 edge.points.unshift({
6dd74de980 x: startNode.x,
6dd74de981 y: startNode.y,
6dd74de982 });
6dd74de983 }
6dd74de984
6dd74de985 if (shouldAddEndCenter) {
6dd74de986 edge.points.push({
6dd74de987 x: endNode.x,
6dd74de988 y: endNode.y,
6dd74de989 });
6dd74de990 }
6dd74de991
6dd74de992 // Debug and sanitize points around cutter2
6dd74de993 const prevPoints = Array.isArray(edge.points) ? [...edge.points] : [];
6dd74de994 const endBounds = boundsFor(endNode);
6dd74de995 log.debug(
6dd74de996 'PPP cutter2: Points before cutter2:',
6dd74de997 JSON.stringify(edge.points),
6dd74de998 'endBounds:',
6dd74de999 endBounds,
6dd74de1000 onBorder(endBounds, edge.points[edge.points.length - 1])
6dd74de1001 );
6dd74de1002 // Block for reducing variable scope and guardrails for the cutter function
6dd74de1003 {
6dd74de1004 const startBounds = boundsFor(startNode);
6dd74de1005 const endBounds = boundsFor(endNode);
6dd74de1006
6dd74de1007 const startIsGroup = !!startNode?.isGroup;
6dd74de1008 const endIsGroup = !!endNode?.isGroup;
6dd74de1009
6dd74de1010 const { candidate: startCandidate, centerApprox: startCenterApprox } =
6dd74de1011 getCandidateBorderPoint(prevPoints as P[], startNode, 'start');
6dd74de1012 const { candidate: endCandidate, centerApprox: endCenterApprox } =
6dd74de1013 getCandidateBorderPoint(prevPoints as P[], endNode, 'end');
6dd74de1014
6dd74de1015 const skipStart = startIsGroup && onBorder(startBounds, startCandidate);
6dd74de1016 const skipEnd = endIsGroup && onBorder(endBounds, endCandidate);
6dd74de1017
6dd74de1018 dropAutoCenterPoint(prevPoints as P[], 'start', skipStart && startCenterApprox);
6dd74de1019 dropAutoCenterPoint(prevPoints as P[], 'end', skipEnd && endCenterApprox);
6dd74de1020
6dd74de1021 if (skipStart || skipEnd) {
6dd74de1022 if (!skipStart) {
6dd74de1023 applyStartIntersectionIfNeeded(prevPoints as P[], startNode, startBounds);
6dd74de1024 }
6dd74de1025 if (!skipEnd) {
6dd74de1026 applyEndIntersectionIfNeeded(prevPoints as P[], endNode, endBounds);
6dd74de1027 }
6dd74de1028
6dd74de1029 log.debug('PPP cutter2: skipping cutter2 due to on-border group endpoint(s)', {
6dd74de1030 skipStart,
6dd74de1031 skipEnd,
6dd74de1032 startCenterApprox,
6dd74de1033 endCenterApprox,
6dd74de1034 startCandidate,
6dd74de1035 endCandidate,
6dd74de1036 });
6dd74de1037 edge.points = prevPoints;
6dd74de1038 } else {
6dd74de1039 edge.points = cutter2(startNode, endNode, prevPoints);
6dd74de1040 }
6dd74de1041 }
6dd74de1042 log.debug('PPP cutter2: Points after cutter2:', JSON.stringify(edge.points));
6dd74de1043 const hasNaN = (pts: { x: number; y: number }[]) =>
6dd74de1044 pts?.some((p) => !Number.isFinite(p?.x) || !Number.isFinite(p?.y));
6dd74de1045 if (!Array.isArray(edge.points) || edge.points.length < 2 || hasNaN(edge.points)) {
6dd74de1046 log.warn(
6dd74de1047 'POI cutter2: Invalid points from cutter2, falling back to prevPoints',
6dd74de1048 edge.points
6dd74de1049 );
6dd74de1050 // Fallback to previous points and strip any invalid ones just in case
6dd74de1051 const cleaned = prevPoints.filter((p) => Number.isFinite(p?.x) && Number.isFinite(p?.y));
6dd74de1052 edge.points = cleaned.length >= 2 ? cleaned : prevPoints;
6dd74de1053 }
6dd74de1054 log.debug('UIO cutter2: Points after cutter2 (sanitized):', edge.points);
6dd74de1055 // Remove consecutive duplicate points to avoid zero-length segments in path builders
6dd74de1056 const deduped = edge.points.filter(
6dd74de1057 (p: { x: number; y: number }, i: number, arr: { x: number; y: number }[]) => {
6dd74de1058 if (i === 0) {
6dd74de1059 return true;
6dd74de1060 }
6dd74de1061 const prev = arr[i - 1];
6dd74de1062 return Math.abs(p.x - prev.x) > 1e-6 || Math.abs(p.y - prev.y) > 1e-6;
6dd74de1063 }
6dd74de1064 );
6dd74de1065 if (deduped.length !== edge.points.length) {
6dd74de1066 log.debug('UIO cutter2: removed consecutive duplicate points', {
6dd74de1067 before: edge.points,
6dd74de1068 after: deduped,
6dd74de1069 });
6dd74de1070 }
6dd74de1071 edge.points = deduped;
6dd74de1072 const paths = insertEdge(
6dd74de1073 edgesEl,
6dd74de1074 edge,
6dd74de1075 clusterDb,
6dd74de1076 data4Layout.type,
6dd74de1077 startNode,
6dd74de1078 endNode,
6dd74de1079 data4Layout.diagramId,
6dd74de1080 true
6dd74de1081 );
6dd74de1082 log.info('APA12 edge points after insert', JSON.stringify(edge.points));
6dd74de1083
6dd74de1084 edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2;
6dd74de1085 edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2;
6dd74de1086 positionEdgeLabel(edge, paths);
6dd74de1087 }
6dd74de1088 }
6dd74de1089 );
6dd74de1090};