| 6dd74de | | | 1 | import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid'; |
| 6dd74de | | | 2 | // @ts-ignore TODO: Investigate D3 issue |
| 6dd74de | | | 3 | import { curveLinear } from 'd3'; |
| 6dd74de | | | 4 | import ELK from 'elkjs/lib/elk.bundled.js'; |
| 6dd74de | | | 5 | import { type TreeData, findCommonAncestor } from './find-common-ancestor.js'; |
| 6dd74de | | | 6 | |
| 6dd74de | | | 7 | import { |
| 6dd74de | | | 8 | type P, |
| 6dd74de | | | 9 | type RectLike, |
| 6dd74de | | | 10 | outsideNode, |
| 6dd74de | | | 11 | computeNodeIntersection, |
| 6dd74de | | | 12 | replaceEndpoint, |
| 6dd74de | | | 13 | onBorder, |
| 6dd74de | | | 14 | } from './geometry.js'; |
| 6dd74de | | | 15 | |
| 6dd74de | | | 16 | type Node = LayoutData['nodes'][number]; |
| 6dd74de | | | 17 | |
| 6dd74de | | | 18 | // Minimal structural type to avoid depending on d3 Selection typings |
| 6dd74de | | | 19 | interface D3Selection<T extends Element> { |
| 6dd74de | | | 20 | node(): T | null; |
| 6dd74de | | | 21 | attr(name: string, value: string): D3Selection<T>; |
| 6dd74de | | | 22 | } |
| 6dd74de | | | 23 | |
| 6dd74de | | | 24 | interface LabelData { |
| 6dd74de | | | 25 | width: number; |
| 6dd74de | | | 26 | height: number; |
| 6dd74de | | | 27 | wrappingWidth?: number; |
| 6dd74de | | | 28 | labelNode?: SVGGElement | null; |
| 6dd74de | | | 29 | } |
| 6dd74de | | | 30 | |
| 6dd74de | | | 31 | interface NodeWithVertex extends Omit<Node, 'domId'> { |
| 6dd74de | | | 32 | children?: LayoutData['nodes']; |
| 6dd74de | | | 33 | labelData?: LabelData; |
| 6dd74de | | | 34 | domId?: D3Selection<SVGAElement | SVGGElement>; |
| 6dd74de | | | 35 | } |
| 6dd74de | | | 36 | |
| 6dd74de | | | 37 | export const render = async ( |
| 6dd74de | | | 38 | data4Layout: LayoutData, |
| 6dd74de | | | 39 | svg: SVG, |
| 6dd74de | | | 40 | { |
| 6dd74de | | | 41 | common, |
| 6dd74de | | | 42 | getConfig, |
| 6dd74de | | | 43 | insertCluster, |
| 6dd74de | | | 44 | insertEdge, |
| 6dd74de | | | 45 | insertEdgeLabel, |
| 6dd74de | | | 46 | insertMarkers, |
| 6dd74de | | | 47 | insertNode, |
| 6dd74de | | | 48 | interpolateToCurve, |
| 6dd74de | | | 49 | labelHelper, |
| 6dd74de | | | 50 | log, |
| 6dd74de | | | 51 | positionEdgeLabel, |
| 6dd74de | | | 52 | }: InternalHelpers, |
| 6dd74de | | | 53 | { algorithm }: RenderOptions |
| 6dd74de | | | 54 | ) => { |
| 6dd74de | | | 55 | const nodeDb: Record<string, any> = {}; |
| 6dd74de | | | 56 | const clusterDb: Record<string, any> = {}; |
| 6dd74de | | | 57 | |
| 6dd74de | | | 58 | const addVertex = async ( |
| 6dd74de | | | 59 | nodeEl: SVGGroup, |
| 6dd74de | | | 60 | graph: { children: NodeWithVertex[] }, |
| 6dd74de | | | 61 | nodeArr: Node[], |
| 6dd74de | | | 62 | node: Node |
| 6dd74de | | | 63 | ) => { |
| 6dd74de | | | 64 | const labelData: LabelData = { width: 0, height: 0 }; |
| 6dd74de | | | 65 | |
| 6dd74de | | | 66 | const config = getConfig(); |
| 6dd74de | | | 67 | |
| 6dd74de | | | 68 | // Add the element to the DOM |
| 6dd74de | | | 69 | if (!node.isGroup) { |
| 6dd74de | | | 70 | // const child = node as NodeWithVertex; |
| 6dd74de | | | 71 | const child: NodeWithVertex = { |
| 6dd74de | | | 72 | id: node.id, |
| 6dd74de | | | 73 | width: node.width, |
| 6dd74de | | | 74 | height: node.height, |
| 6dd74de | | | 75 | // Store the original node data for later use |
| 6dd74de | | | 76 | label: node.label, |
| 6dd74de | | | 77 | isGroup: node.isGroup, |
| 6dd74de | | | 78 | shape: node.shape, |
| 6dd74de | | | 79 | padding: node.padding, |
| 6dd74de | | | 80 | cssClasses: node.cssClasses, |
| 6dd74de | | | 81 | cssStyles: node.cssStyles, |
| 6dd74de | | | 82 | look: node.look, |
| 6dd74de | | | 83 | // Include parentId for subgraph processing |
| 6dd74de | | | 84 | parentId: node.parentId, |
| 6dd74de | | | 85 | }; |
| 6dd74de | | | 86 | graph.children.push(child); |
| 6dd74de | | | 87 | nodeDb[node.id] = node; |
| 6dd74de | | | 88 | |
| 6dd74de | | | 89 | const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir }); |
| 6dd74de | | | 90 | const boundingBox = childNodeEl.node()!.getBBox(); |
| 6dd74de | | | 91 | // Store the domId separately for rendering, not in the ELK graph |
| 6dd74de | | | 92 | child.domId = childNodeEl; |
| 6dd74de | | | 93 | child.width = boundingBox.width; |
| 6dd74de | | | 94 | child.height = boundingBox.height; |
| 6dd74de | | | 95 | } else { |
| 6dd74de | | | 96 | // A subgraph |
| 6dd74de | | | 97 | const child: NodeWithVertex & { children: NodeWithVertex[] } = { |
| 6dd74de | | | 98 | ...node, |
| 6dd74de | | | 99 | domId: undefined, |
| 6dd74de | | | 100 | children: [], |
| 6dd74de | | | 101 | }; |
| 6dd74de | | | 102 | // Let elk render with the copy |
| 6dd74de | | | 103 | graph.children.push(child); |
| 6dd74de | | | 104 | // Save the original containing the intersection function |
| 6dd74de | | | 105 | nodeDb[node.id] = child; |
| 6dd74de | | | 106 | await addVertices(nodeEl, nodeArr, child, node.id); |
| 6dd74de | | | 107 | |
| 6dd74de | | | 108 | if (node.label) { |
| 6dd74de | | | 109 | // @ts-ignore TODO: fix this |
| 6dd74de | | | 110 | const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true); |
| 6dd74de | | | 111 | labelData.width = bbox.width; |
| 6dd74de | | | 112 | labelData.wrappingWidth = config.flowchart!.wrappingWidth; |
| 6dd74de | | | 113 | // Give some padding for elk |
| 6dd74de | | | 114 | labelData.height = bbox.height - 2; |
| 6dd74de | | | 115 | labelData.labelNode = shapeSvg.node(); |
| 6dd74de | | | 116 | // We need the label hight to be able to size the subgraph; |
| 6dd74de | | | 117 | shapeSvg.remove(); |
| 6dd74de | | | 118 | } else { |
| 6dd74de | | | 119 | // Subgraph without label |
| 6dd74de | | | 120 | labelData.width = 0; |
| 6dd74de | | | 121 | labelData.height = 0; |
| 6dd74de | | | 122 | } |
| 6dd74de | | | 123 | child.labelData = labelData; |
| 6dd74de | | | 124 | child.domId = nodeEl; |
| 6dd74de | | | 125 | } |
| 6dd74de | | | 126 | }; |
| 6dd74de | | | 127 | |
| 6dd74de | | | 128 | const addVertices = async function ( |
| 6dd74de | | | 129 | nodeEl: SVGGroup, |
| 6dd74de | | | 130 | nodeArr: Node[], |
| 6dd74de | | | 131 | graph: { children: NodeWithVertex[] }, |
| 6dd74de | | | 132 | parentId?: string |
| 6dd74de | | | 133 | ) { |
| 6dd74de | | | 134 | const siblings = nodeArr.filter((node) => node?.parentId === parentId); |
| 6dd74de | | | 135 | log.info('addVertices APA12', siblings, parentId); |
| 6dd74de | | | 136 | // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition |
| 6dd74de | | | 137 | await Promise.all( |
| 6dd74de | | | 138 | siblings.map(async (node) => { |
| 6dd74de | | | 139 | await addVertex(nodeEl, graph, nodeArr, node); |
| 6dd74de | | | 140 | }) |
| 6dd74de | | | 141 | ); |
| 6dd74de | | | 142 | return graph; |
| 6dd74de | | | 143 | }; |
| 6dd74de | | | 144 | |
| 6dd74de | | | 145 | const drawNodes = async ( |
| 6dd74de | | | 146 | relX: number, |
| 6dd74de | | | 147 | relY: number, |
| 6dd74de | | | 148 | nodeArray: any[], |
| 6dd74de | | | 149 | svg: any, |
| 6dd74de | | | 150 | subgraphsEl: SVGGroup, |
| 6dd74de | | | 151 | depth: number |
| 6dd74de | | | 152 | ) => { |
| 6dd74de | | | 153 | await Promise.all( |
| 6dd74de | | | 154 | nodeArray.map(async function (node: { |
| 6dd74de | | | 155 | id: string | number; |
| 6dd74de | | | 156 | x: any; |
| 6dd74de | | | 157 | y: any; |
| 6dd74de | | | 158 | width: number; |
| 6dd74de | | | 159 | labels: { width: any }[]; |
| 6dd74de | | | 160 | height: number; |
| 6dd74de | | | 161 | isGroup: any; |
| 6dd74de | | | 162 | labelData: any; |
| 6dd74de | | | 163 | offset: { posX: number; posY: number }; |
| 6dd74de | | | 164 | shape: any; |
| 6dd74de | | | 165 | domId: { node: () => any; attr: (arg0: string, arg1: string) => void }; |
| 6dd74de | | | 166 | }) { |
| 6dd74de | | | 167 | if (node) { |
| 6dd74de | | | 168 | nodeDb[node.id] ??= {}; |
| 6dd74de | | | 169 | nodeDb[node.id].offset = { |
| 6dd74de | | | 170 | posX: node.x + relX, |
| 6dd74de | | | 171 | posY: node.y + relY, |
| 6dd74de | | | 172 | x: relX, |
| 6dd74de | | | 173 | y: relY, |
| 6dd74de | | | 174 | depth, |
| 6dd74de | | | 175 | width: Math.max(node.width, node.labels ? node.labels[0]?.width || 0 : 0), |
| 6dd74de | | | 176 | height: node.height, |
| 6dd74de | | | 177 | }; |
| 6dd74de | | | 178 | if (node.isGroup) { |
| 6dd74de | | | 179 | log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData); |
| 6dd74de | | | 180 | const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph'); |
| 6dd74de | | | 181 | // TODO use faster way of cloning |
| 6dd74de | | | 182 | const clusterNode = JSON.parse(JSON.stringify(node)); |
| 6dd74de | | | 183 | clusterNode.x = node.offset.posX + node.width / 2; |
| 6dd74de | | | 184 | clusterNode.y = node.offset.posY + node.height / 2; |
| 6dd74de | | | 185 | clusterNode.width = Math.max(clusterNode.width, node.labelData.width); |
| 6dd74de | | | 186 | await insertCluster(subgraphEl, clusterNode); |
| 6dd74de | | | 187 | |
| 6dd74de | | | 188 | log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels); |
| 6dd74de | | | 189 | } else { |
| 6dd74de | | | 190 | log.info( |
| 6dd74de | | | 191 | 'Id NODE = ', |
| 6dd74de | | | 192 | node.id, |
| 6dd74de | | | 193 | node.x, |
| 6dd74de | | | 194 | node.y, |
| 6dd74de | | | 195 | relX, |
| 6dd74de | | | 196 | relY, |
| 6dd74de | | | 197 | node.domId.node(), |
| 6dd74de | | | 198 | `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` |
| 6dd74de | | | 199 | ); |
| 6dd74de | | | 200 | node.domId.attr( |
| 6dd74de | | | 201 | 'transform', |
| 6dd74de | | | 202 | `translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})` |
| 6dd74de | | | 203 | ); |
| 6dd74de | | | 204 | } |
| 6dd74de | | | 205 | } |
| 6dd74de | | | 206 | }) |
| 6dd74de | | | 207 | ); |
| 6dd74de | | | 208 | |
| 6dd74de | | | 209 | await Promise.all( |
| 6dd74de | | | 210 | nodeArray.map(async function (node: { isGroup: any; x: any; y: any; children: any }) { |
| 6dd74de | | | 211 | if (node?.isGroup) { |
| 6dd74de | | | 212 | await drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, depth + 1); |
| 6dd74de | | | 213 | } |
| 6dd74de | | | 214 | }) |
| 6dd74de | | | 215 | ); |
| 6dd74de | | | 216 | }; |
| 6dd74de | | | 217 | |
| 6dd74de | | | 218 | const addSubGraphs = (nodeArr: any[]): TreeData => { |
| 6dd74de | | | 219 | const parentLookupDb: TreeData = { parentById: {}, childrenById: {} }; |
| 6dd74de | | | 220 | const subgraphs = nodeArr.filter((node: { isGroup: any }) => node.isGroup); |
| 6dd74de | | | 221 | log.info('Subgraphs - ', subgraphs); |
| 6dd74de | | | 222 | subgraphs.forEach((subgraph: { id: string }) => { |
| 6dd74de | | | 223 | const children = nodeArr.filter((node: { parentId: any }) => node.parentId === subgraph.id); |
| 6dd74de | | | 224 | children.forEach((node: any) => { |
| 6dd74de | | | 225 | parentLookupDb.parentById[node.id] = subgraph.id; |
| 6dd74de | | | 226 | if (parentLookupDb.childrenById[subgraph.id] === undefined) { |
| 6dd74de | | | 227 | parentLookupDb.childrenById[subgraph.id] = []; |
| 6dd74de | | | 228 | } |
| 6dd74de | | | 229 | parentLookupDb.childrenById[subgraph.id].push(node); |
| 6dd74de | | | 230 | }); |
| 6dd74de | | | 231 | }); |
| 6dd74de | | | 232 | |
| 6dd74de | | | 233 | return parentLookupDb; |
| 6dd74de | | | 234 | }; |
| 6dd74de | | | 235 | |
| 6dd74de | | | 236 | const getEdgeStartEndPoint = (edge: any) => { |
| 6dd74de | | | 237 | // edge.start and edge.end are IDs (string/number) in our layout data |
| 6dd74de | | | 238 | const sourceId: string | number = edge.start; |
| 6dd74de | | | 239 | const targetId: string | number = edge.end; |
| 6dd74de | | | 240 | |
| 6dd74de | | | 241 | const source = sourceId; |
| 6dd74de | | | 242 | const target = targetId; |
| 6dd74de | | | 243 | |
| 6dd74de | | | 244 | const startNode = nodeDb[sourceId]; |
| 6dd74de | | | 245 | const endNode = nodeDb[targetId]; |
| 6dd74de | | | 246 | |
| 6dd74de | | | 247 | if (!startNode || !endNode) { |
| 6dd74de | | | 248 | return { source, target }; |
| 6dd74de | | | 249 | } |
| 6dd74de | | | 250 | |
| 6dd74de | | | 251 | // Add the edge to the graph |
| 6dd74de | | | 252 | return { source, target, sourceId, targetId }; |
| 6dd74de | | | 253 | }; |
| 6dd74de | | | 254 | |
| 6dd74de | | | 255 | const calcOffset = function (src: string, dest: string, parentLookupDb: TreeData) { |
| 6dd74de | | | 256 | const ancestor = findCommonAncestor(src, dest, parentLookupDb); |
| 6dd74de | | | 257 | if (ancestor === undefined || ancestor === 'root') { |
| 6dd74de | | | 258 | return { x: 0, y: 0 }; |
| 6dd74de | | | 259 | } |
| 6dd74de | | | 260 | |
| 6dd74de | | | 261 | const ancestorOffset = nodeDb[ancestor].offset; |
| 6dd74de | | | 262 | return { x: ancestorOffset.posX, y: ancestorOffset.posY }; |
| 6dd74de | | | 263 | }; |
| 6dd74de | | | 264 | |
| 6dd74de | | | 265 | /** |
| 6dd74de | | | 266 | * Add edges to graph based on parsed graph definition |
| 6dd74de | | | 267 | */ |
| 6dd74de | | | 268 | // Edge helper maps and utilities (de-duplicated) |
| 6dd74de | | | 269 | const ARROW_MAP: Record<string, [string, string]> = { |
| 6dd74de | | | 270 | arrow_open: ['arrow_open', 'arrow_open'], |
| 6dd74de | | | 271 | arrow_cross: ['arrow_open', 'arrow_cross'], |
| 6dd74de | | | 272 | double_arrow_cross: ['arrow_cross', 'arrow_cross'], |
| 6dd74de | | | 273 | arrow_point: ['arrow_open', 'arrow_point'], |
| 6dd74de | | | 274 | double_arrow_point: ['arrow_point', 'arrow_point'], |
| 6dd74de | | | 275 | arrow_circle: ['arrow_open', 'arrow_circle'], |
| 6dd74de | | | 276 | double_arrow_circle: ['arrow_circle', 'arrow_circle'], |
| 6dd74de | | | 277 | }; |
| 6dd74de | | | 278 | |
| 6dd74de | | | 279 | const computeStroke = ( |
| 6dd74de | | | 280 | stroke: string | undefined, |
| 6dd74de | | | 281 | defaultStyle?: string, |
| 6dd74de | | | 282 | defaultLabelStyle?: string |
| 6dd74de | | | 283 | ) => { |
| 6dd74de | | | 284 | // Defaults correspond to 'normal' |
| 6dd74de | | | 285 | let thickness = 'normal'; |
| 6dd74de | | | 286 | let pattern = 'solid'; |
| 6dd74de | | | 287 | let style = ''; |
| 6dd74de | | | 288 | let labelStyle = ''; |
| 6dd74de | | | 289 | |
| 6dd74de | | | 290 | if (stroke === 'dotted') { |
| 6dd74de | | | 291 | pattern = 'dotted'; |
| 6dd74de | | | 292 | style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; |
| 6dd74de | | | 293 | } else if (stroke === 'thick') { |
| 6dd74de | | | 294 | thickness = 'thick'; |
| 6dd74de | | | 295 | style = 'stroke-width: 3.5px;fill:none;'; |
| 6dd74de | | | 296 | } else { |
| 6dd74de | | | 297 | // normal |
| 6dd74de | | | 298 | style = defaultStyle ?? 'fill:none;'; |
| 6dd74de | | | 299 | if (defaultLabelStyle !== undefined) { |
| 6dd74de | | | 300 | labelStyle = defaultLabelStyle; |
| 6dd74de | | | 301 | } |
| 6dd74de | | | 302 | } |
| 6dd74de | | | 303 | return { thickness, pattern, style, labelStyle }; |
| 6dd74de | | | 304 | }; |
| 6dd74de | | | 305 | |
| 6dd74de | | | 306 | const getCurve = (edgeInterpolate: any, edgesDefaultInterpolate: any, confCurve: any) => { |
| 6dd74de | | | 307 | if (edgeInterpolate !== undefined) { |
| 6dd74de | | | 308 | return interpolateToCurve(edgeInterpolate, curveLinear); |
| 6dd74de | | | 309 | } |
| 6dd74de | | | 310 | if (edgesDefaultInterpolate !== undefined) { |
| 6dd74de | | | 311 | return interpolateToCurve(edgesDefaultInterpolate, curveLinear); |
| 6dd74de | | | 312 | } |
| 6dd74de | | | 313 | // @ts-ignore TODO: fix this |
| 6dd74de | | | 314 | return interpolateToCurve(confCurve, curveLinear); |
| 6dd74de | | | 315 | }; |
| 6dd74de | | | 316 | const buildEdgeData = ( |
| 6dd74de | | | 317 | edge: any, |
| 6dd74de | | | 318 | defaults: { |
| 6dd74de | | | 319 | defaultStyle?: string; |
| 6dd74de | | | 320 | defaultLabelStyle?: string; |
| 6dd74de | | | 321 | defaultInterpolate?: any; |
| 6dd74de | | | 322 | confCurve: any; |
| 6dd74de | | | 323 | }, |
| 6dd74de | | | 324 | common: any |
| 6dd74de | | | 325 | ) => { |
| 6dd74de | | | 326 | const edgeData: any = { style: '', labelStyle: '' }; |
| 6dd74de | | | 327 | edgeData.minlen = edge.length || 1; |
| 6dd74de | | | 328 | // maintain legacy behavior |
| 6dd74de | | | 329 | edge.text = edge.label; |
| 6dd74de | | | 330 | |
| 6dd74de | | | 331 | // Arrowhead fill vs none |
| 6dd74de | | | 332 | edgeData.arrowhead = edge.type === 'arrow_open' ? 'none' : 'normal'; |
| 6dd74de | | | 333 | |
| 6dd74de | | | 334 | // Arrow types |
| 6dd74de | | | 335 | const arrowMap = ARROW_MAP[edge.type] ?? ARROW_MAP.arrow_open; |
| 6dd74de | | | 336 | edgeData.arrowTypeStart = arrowMap[0]; |
| 6dd74de | | | 337 | edgeData.arrowTypeEnd = arrowMap[1]; |
| 6dd74de | | | 338 | |
| 6dd74de | | | 339 | // Optional edge label positioning flags |
| 6dd74de | | | 340 | edgeData.startLabelRight = edge.startLabelRight; |
| 6dd74de | | | 341 | edgeData.endLabelLeft = edge.endLabelLeft; |
| 6dd74de | | | 342 | |
| 6dd74de | | | 343 | // Stroke |
| 6dd74de | | | 344 | const strokeRes = computeStroke(edge.stroke, defaults.defaultStyle, defaults.defaultLabelStyle); |
| 6dd74de | | | 345 | edgeData.thickness = strokeRes.thickness; |
| 6dd74de | | | 346 | edgeData.pattern = strokeRes.pattern; |
| 6dd74de | | | 347 | edgeData.style = (edgeData.style || '') + (strokeRes.style || ''); |
| 6dd74de | | | 348 | edgeData.labelStyle = (edgeData.labelStyle || '') + (strokeRes.labelStyle || ''); |
| 6dd74de | | | 349 | |
| 6dd74de | | | 350 | // Curve |
| 6dd74de | | | 351 | // @ts-ignore - defaults.confCurve is present at runtime but missing in type |
| 6dd74de | | | 352 | edgeData.curve = getCurve(edge.interpolate, defaults.defaultInterpolate, defaults.confCurve); |
| 6dd74de | | | 353 | |
| 6dd74de | | | 354 | // Arrowhead style + labelpos when we have label text |
| 6dd74de | | | 355 | const hasText = (edge?.text ?? '') !== ''; |
| 6dd74de | | | 356 | if (hasText) { |
| 6dd74de | | | 357 | edgeData.arrowheadStyle = 'fill: #333'; |
| 6dd74de | | | 358 | edgeData.labelpos = 'c'; |
| 6dd74de | | | 359 | } else if (edge.style !== undefined) { |
| 6dd74de | | | 360 | edgeData.arrowheadStyle = 'fill: #333'; |
| 6dd74de | | | 361 | } |
| 6dd74de | | | 362 | |
| 6dd74de | | | 363 | edgeData.labelType = edge.labelType; |
| 6dd74de | | | 364 | edgeData.label = (edge?.text ?? '').replace(common.lineBreakRegex, '\n'); |
| 6dd74de | | | 365 | |
| 6dd74de | | | 366 | if (edge.style === undefined) { |
| 6dd74de | | | 367 | edgeData.style = edgeData.style ?? 'stroke: #333; stroke-width: 1.5px;fill:none;'; |
| 6dd74de | | | 368 | } |
| 6dd74de | | | 369 | |
| 6dd74de | | | 370 | edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); |
| 6dd74de | | | 371 | return edgeData; |
| 6dd74de | | | 372 | }; |
| 6dd74de | | | 373 | |
| 6dd74de | | | 374 | const addEdges = async function ( |
| 6dd74de | | | 375 | dataForLayout: { edges: any; direction?: string }, |
| 6dd74de | | | 376 | graph: { |
| 6dd74de | | | 377 | id?: string; |
| 6dd74de | | | 378 | layoutOptions?: { |
| 6dd74de | | | 379 | 'elk.hierarchyHandling': string; |
| 6dd74de | | | 380 | 'elk.algorithm': any; |
| 6dd74de | | | 381 | 'nodePlacement.strategy': any; |
| 6dd74de | | | 382 | 'elk.layered.mergeEdges': any; |
| 6dd74de | | | 383 | 'elk.direction': string; |
| 6dd74de | | | 384 | 'spacing.baseValue': number; |
| 6dd74de | | | 385 | }; |
| 6dd74de | | | 386 | children?: never[]; |
| 6dd74de | | | 387 | edges: any; |
| 6dd74de | | | 388 | }, |
| 6dd74de | | | 389 | svg: SVG |
| 6dd74de | | | 390 | ) { |
| 6dd74de | | | 391 | log.info('abc78 DAGA edges = ', dataForLayout); |
| 6dd74de | | | 392 | const edges = dataForLayout.edges; |
| 6dd74de | | | 393 | const labelsEl = svg.insert('g').attr('class', 'edgeLabels'); |
| 6dd74de | | | 394 | const linkIdCnt: any = {}; |
| 6dd74de | | | 395 | let defaultStyle: string | undefined; |
| 6dd74de | | | 396 | let defaultLabelStyle: string | undefined; |
| 6dd74de | | | 397 | |
| 6dd74de | | | 398 | await Promise.all( |
| 6dd74de | | | 399 | edges.map(async function (edge: { |
| 6dd74de | | | 400 | id: string; |
| 6dd74de | | | 401 | start: string; |
| 6dd74de | | | 402 | end: string; |
| 6dd74de | | | 403 | length: number; |
| 6dd74de | | | 404 | text: undefined; |
| 6dd74de | | | 405 | label: any; |
| 6dd74de | | | 406 | type: string; |
| 6dd74de | | | 407 | stroke: any; |
| 6dd74de | | | 408 | interpolate: undefined; |
| 6dd74de | | | 409 | style: undefined; |
| 6dd74de | | | 410 | labelType: any; |
| 6dd74de | | | 411 | startLabelRight?: string; |
| 6dd74de | | | 412 | endLabelLeft?: string; |
| 6dd74de | | | 413 | }) { |
| 6dd74de | | | 414 | // Identify Link |
| 6dd74de | | | 415 | const linkIdBase = edge.id; // 'L-' + edge.start + '-' + edge.end; |
| 6dd74de | | | 416 | // count the links from+to the same node to give unique id |
| 6dd74de | | | 417 | if (linkIdCnt[linkIdBase] === undefined) { |
| 6dd74de | | | 418 | linkIdCnt[linkIdBase] = 0; |
| 6dd74de | | | 419 | log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); |
| 6dd74de | | | 420 | } else { |
| 6dd74de | | | 421 | linkIdCnt[linkIdBase]++; |
| 6dd74de | | | 422 | log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); |
| 6dd74de | | | 423 | } |
| 6dd74de | | | 424 | const linkId = linkIdBase; // + '_' + linkIdCnt[linkIdBase]; |
| 6dd74de | | | 425 | edge.id = linkId; |
| 6dd74de | | | 426 | log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); |
| 6dd74de | | | 427 | const linkNameStart = 'LS_' + edge.start; |
| 6dd74de | | | 428 | const linkNameEnd = 'LE_' + edge.end; |
| 6dd74de | | | 429 | |
| 6dd74de | | | 430 | const conf = getConfig(); |
| 6dd74de | | | 431 | const edgeData = buildEdgeData( |
| 6dd74de | | | 432 | edge, |
| 6dd74de | | | 433 | { |
| 6dd74de | | | 434 | defaultStyle, |
| 6dd74de | | | 435 | defaultLabelStyle, |
| 6dd74de | | | 436 | defaultInterpolate: edges.defaultInterpolate, |
| 6dd74de | | | 437 | // @ts-ignore - conf.curve exists at runtime but is missing from typing |
| 6dd74de | | | 438 | confCurve: conf.curve, |
| 6dd74de | | | 439 | }, |
| 6dd74de | | | 440 | common |
| 6dd74de | | | 441 | ); |
| 6dd74de | | | 442 | |
| 6dd74de | | | 443 | edgeData.id = linkId; |
| 6dd74de | | | 444 | edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd; |
| 6dd74de | | | 445 | |
| 6dd74de | | | 446 | const labelEl = await insertEdgeLabel(labelsEl, edgeData); |
| 6dd74de | | | 447 | |
| 6dd74de | | | 448 | // calculate start and end points of the edge, note that the source and target |
| 6dd74de | | | 449 | // can be modified for shapes that have ports |
| 6dd74de | | | 450 | |
| 6dd74de | | | 451 | const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge); |
| 6dd74de | | | 452 | log.debug('abc78 source and target', source, target); |
| 6dd74de | | | 453 | // Add the edge to the graph |
| 6dd74de | | | 454 | graph.edges.push({ |
| 6dd74de | | | 455 | ...edge, |
| 6dd74de | | | 456 | sources: [source], |
| 6dd74de | | | 457 | targets: [target], |
| 6dd74de | | | 458 | sourceId, |
| 6dd74de | | | 459 | targetId, |
| 6dd74de | | | 460 | labelEl: labelEl, |
| 6dd74de | | | 461 | labels: [ |
| 6dd74de | | | 462 | { |
| 6dd74de | | | 463 | width: edgeData.width, |
| 6dd74de | | | 464 | height: edgeData.height, |
| 6dd74de | | | 465 | orgWidth: edgeData.width, |
| 6dd74de | | | 466 | orgHeight: edgeData.height, |
| 6dd74de | | | 467 | text: edgeData.label, |
| 6dd74de | | | 468 | layoutOptions: { |
| 6dd74de | | | 469 | 'edgeLabels.inline': 'true', |
| 6dd74de | | | 470 | 'edgeLabels.placement': 'CENTER', |
| 6dd74de | | | 471 | }, |
| 6dd74de | | | 472 | }, |
| 6dd74de | | | 473 | ], |
| 6dd74de | | | 474 | edgeData, |
| 6dd74de | | | 475 | }); |
| 6dd74de | | | 476 | }) |
| 6dd74de | | | 477 | ); |
| 6dd74de | | | 478 | return graph; |
| 6dd74de | | | 479 | }; |
| 6dd74de | | | 480 | |
| 6dd74de | | | 481 | function dir2ElkDirection(dir: any) { |
| 6dd74de | | | 482 | switch (dir) { |
| 6dd74de | | | 483 | case 'LR': |
| 6dd74de | | | 484 | return 'RIGHT'; |
| 6dd74de | | | 485 | case 'RL': |
| 6dd74de | | | 486 | return 'LEFT'; |
| 6dd74de | | | 487 | case 'TB': |
| 6dd74de | | | 488 | case 'TD': // TD is an alias for TB in Mermaid |
| 6dd74de | | | 489 | return 'DOWN'; |
| 6dd74de | | | 490 | case 'BT': |
| 6dd74de | | | 491 | return 'UP'; |
| 6dd74de | | | 492 | default: |
| 6dd74de | | | 493 | return 'DOWN'; |
| 6dd74de | | | 494 | } |
| 6dd74de | | | 495 | } |
| 6dd74de | | | 496 | |
| 6dd74de | | | 497 | function setIncludeChildrenPolicy(nodeId: string, ancestorId: string) { |
| 6dd74de | | | 498 | const node = nodeDb[nodeId]; |
| 6dd74de | | | 499 | |
| 6dd74de | | | 500 | if (!node) { |
| 6dd74de | | | 501 | return; |
| 6dd74de | | | 502 | } |
| 6dd74de | | | 503 | if (node?.layoutOptions === undefined) { |
| 6dd74de | | | 504 | node.layoutOptions = {}; |
| 6dd74de | | | 505 | } |
| 6dd74de | | | 506 | node.layoutOptions['elk.hierarchyHandling'] = 'INCLUDE_CHILDREN'; |
| 6dd74de | | | 507 | if (node.id !== ancestorId) { |
| 6dd74de | | | 508 | setIncludeChildrenPolicy(node.parentId, ancestorId); |
| 6dd74de | | | 509 | } |
| 6dd74de | | | 510 | } |
| 6dd74de | | | 511 | |
| 6dd74de | | | 512 | // Node bounds helpers (global) |
| 6dd74de | | | 513 | const getEffectiveGroupWidth = (node: any): number => { |
| 6dd74de | | | 514 | const labelW = node?.labels?.[0]?.width ?? 0; |
| 6dd74de | | | 515 | const padding = node?.padding ?? 0; |
| 6dd74de | | | 516 | return Math.max(node.width ?? 0, labelW + padding); |
| 6dd74de | | | 517 | }; |
| 6dd74de | | | 518 | |
| 6dd74de | | | 519 | const boundsFor = (node: any): RectLike => { |
| 6dd74de | | | 520 | const width = node?.isGroup ? getEffectiveGroupWidth(node) : node.width; |
| 6dd74de | | | 521 | return { |
| 6dd74de | | | 522 | x: node.offset.posX + node.width / 2, |
| 6dd74de | | | 523 | y: node.offset.posY + node.height / 2, |
| 6dd74de | | | 524 | width, |
| 6dd74de | | | 525 | height: node.height, |
| 6dd74de | | | 526 | padding: node.padding, |
| 6dd74de | | | 527 | }; |
| 6dd74de | | | 528 | }; |
| 6dd74de | | | 529 | // Helper utilities for endpoint handling around cutter2 |
| 6dd74de | | | 530 | type Side = 'start' | 'end'; |
| 6dd74de | | | 531 | const approxEq = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) < eps; |
| 6dd74de | | | 532 | const isCenterApprox = (pt: P, node: { x: number; y: number }) => |
| 6dd74de | | | 533 | approxEq(pt.x, node.x) && approxEq(pt.y, node.y); |
| 6dd74de | | | 534 | |
| 6dd74de | | | 535 | const getCandidateBorderPoint = ( |
| 6dd74de | | | 536 | points: P[], |
| 6dd74de | | | 537 | node: any, |
| 6dd74de | | | 538 | side: Side |
| 6dd74de | | | 539 | ): { candidate: P; centerApprox: boolean } => { |
| 6dd74de | | | 540 | if (!points?.length) { |
| 6dd74de | | | 541 | return { candidate: { x: node.x, y: node.y } as P, centerApprox: true }; |
| 6dd74de | | | 542 | } |
| 6dd74de | | | 543 | if (side === 'start') { |
| 6dd74de | | | 544 | const first = points[0]; |
| 6dd74de | | | 545 | const centerApprox = isCenterApprox(first, node); |
| 6dd74de | | | 546 | const candidate = centerApprox && points.length > 1 ? points[1] : first; |
| 6dd74de | | | 547 | return { candidate, centerApprox }; |
| 6dd74de | | | 548 | } else { |
| 6dd74de | | | 549 | const last = points[points.length - 1]; |
| 6dd74de | | | 550 | const centerApprox = isCenterApprox(last, node); |
| 6dd74de | | | 551 | const candidate = centerApprox && points.length > 1 ? points[points.length - 2] : last; |
| 6dd74de | | | 552 | return { candidate, centerApprox }; |
| 6dd74de | | | 553 | } |
| 6dd74de | | | 554 | }; |
| 6dd74de | | | 555 | |
| 6dd74de | | | 556 | const dropAutoCenterPoint = (points: P[], side: Side, doDrop: boolean) => { |
| 6dd74de | | | 557 | if (!doDrop) { |
| 6dd74de | | | 558 | return; |
| 6dd74de | | | 559 | } |
| 6dd74de | | | 560 | if (side === 'start') { |
| 6dd74de | | | 561 | if (points.length > 0) { |
| 6dd74de | | | 562 | points.shift(); |
| 6dd74de | | | 563 | } |
| 6dd74de | | | 564 | } else { |
| 6dd74de | | | 565 | if (points.length > 0) { |
| 6dd74de | | | 566 | points.pop(); |
| 6dd74de | | | 567 | } |
| 6dd74de | | | 568 | } |
| 6dd74de | | | 569 | }; |
| 6dd74de | | | 570 | |
| 6dd74de | | | 571 | const applyStartIntersectionIfNeeded = (points: P[], startNode: any, startBounds: RectLike) => { |
| 6dd74de | | | 572 | let firstOutsideStartIndex = -1; |
| 6dd74de | | | 573 | for (const [i, p] of points.entries()) { |
| 6dd74de | | | 574 | if (outsideNode(startBounds, p)) { |
| 6dd74de | | | 575 | firstOutsideStartIndex = i; |
| 6dd74de | | | 576 | break; |
| 6dd74de | | | 577 | } |
| 6dd74de | | | 578 | } |
| 6dd74de | | | 579 | if (firstOutsideStartIndex !== -1) { |
| 6dd74de | | | 580 | const outsidePointForStart = points[firstOutsideStartIndex]; |
| 6dd74de | | | 581 | const startCenter = points[0]; |
| 6dd74de | | | 582 | const startIntersection = computeNodeIntersection( |
| 6dd74de | | | 583 | startNode, |
| 6dd74de | | | 584 | startBounds, |
| 6dd74de | | | 585 | outsidePointForStart, |
| 6dd74de | | | 586 | startCenter |
| 6dd74de | | | 587 | ); |
| 6dd74de | | | 588 | replaceEndpoint(points, 'start', startIntersection); |
| 6dd74de | | | 589 | log.debug('UIO cutter2: start-only intersection applied', { startIntersection }); |
| 6dd74de | | | 590 | } |
| 6dd74de | | | 591 | }; |
| 6dd74de | | | 592 | |
| 6dd74de | | | 593 | const applyEndIntersectionIfNeeded = (points: P[], endNode: any, endBounds: RectLike) => { |
| 6dd74de | | | 594 | let outsideIndexForEnd = -1; |
| 6dd74de | | | 595 | for (let i = points.length - 1; i >= 0; i--) { |
| 6dd74de | | | 596 | if (outsideNode(endBounds, points[i])) { |
| 6dd74de | | | 597 | outsideIndexForEnd = i; |
| 6dd74de | | | 598 | break; |
| 6dd74de | | | 599 | } |
| 6dd74de | | | 600 | } |
| 6dd74de | | | 601 | if (outsideIndexForEnd !== -1) { |
| 6dd74de | | | 602 | const outsidePointForEnd = points[outsideIndexForEnd]; |
| 6dd74de | | | 603 | const endCenter = points[points.length - 1]; |
| 6dd74de | | | 604 | const endIntersection = computeNodeIntersection( |
| 6dd74de | | | 605 | endNode, |
| 6dd74de | | | 606 | endBounds, |
| 6dd74de | | | 607 | outsidePointForEnd, |
| 6dd74de | | | 608 | endCenter |
| 6dd74de | | | 609 | ); |
| 6dd74de | | | 610 | replaceEndpoint(points, 'end', endIntersection); |
| 6dd74de | | | 611 | log.debug('UIO cutter2: end-only intersection applied', { endIntersection }); |
| 6dd74de | | | 612 | } |
| 6dd74de | | | 613 | }; |
| 6dd74de | | | 614 | |
| 6dd74de | | | 615 | const cutter2 = (startNode: any, endNode: any, _points: any[]) => { |
| 6dd74de | | | 616 | const startBounds = boundsFor(startNode); |
| 6dd74de | | | 617 | const endBounds = boundsFor(endNode); |
| 6dd74de | | | 618 | |
| 6dd74de | | | 619 | if (_points.length === 0) { |
| 6dd74de | | | 620 | return []; |
| 6dd74de | | | 621 | } |
| 6dd74de | | | 622 | |
| 6dd74de | | | 623 | // Copy the original points array |
| 6dd74de | | | 624 | const points: P[] = [..._points] as P[]; |
| 6dd74de | | | 625 | |
| 6dd74de | | | 626 | // The first point is the center of sNode, the last point is the center of eNode |
| 6dd74de | | | 627 | const startCenter = points[0]; |
| 6dd74de | | | 628 | const endCenter = points[points.length - 1]; |
| 6dd74de | | | 629 | |
| 6dd74de | | | 630 | // Minimal, structured logging for diagnostics |
| 6dd74de | | | 631 | log.debug('PPP cutter2: bounds', { startBounds, endBounds }); |
| 6dd74de | | | 632 | log.debug('PPP cutter2: original points', _points); |
| 6dd74de | | | 633 | |
| 6dd74de | | | 634 | let firstOutsideStartIndex = -1; |
| 6dd74de | | | 635 | |
| 6dd74de | | | 636 | // Single iteration through the array |
| 6dd74de | | | 637 | for (const [i, point] of points.entries()) { |
| 6dd74de | | | 638 | if (firstOutsideStartIndex === -1 && outsideNode(startBounds, point)) { |
| 6dd74de | | | 639 | firstOutsideStartIndex = i; |
| 6dd74de | | | 640 | } |
| 6dd74de | | | 641 | if (outsideNode(endBounds, point)) { |
| 6dd74de | | | 642 | // keep scanning; we'll also scan from the end for the last outside point |
| 6dd74de | | | 643 | } |
| 6dd74de | | | 644 | } |
| 6dd74de | | | 645 | |
| 6dd74de | | | 646 | // Calculate intersection with start node if we found a point outside it |
| 6dd74de | | | 647 | if (firstOutsideStartIndex !== -1) { |
| 6dd74de | | | 648 | const outsidePointForStart = points[firstOutsideStartIndex]; |
| 6dd74de | | | 649 | const startIntersection = computeNodeIntersection( |
| 6dd74de | | | 650 | startNode, |
| 6dd74de | | | 651 | startBounds, |
| 6dd74de | | | 652 | outsidePointForStart, |
| 6dd74de | | | 653 | startCenter |
| 6dd74de | | | 654 | ); |
| 6dd74de | | | 655 | log.debug('UIO cutter2: start intersection', startIntersection); |
| 6dd74de | | | 656 | replaceEndpoint(points, 'start', startIntersection); |
| 6dd74de | | | 657 | } |
| 6dd74de | | | 658 | |
| 6dd74de | | | 659 | // Calculate intersection with end node |
| 6dd74de | | | 660 | let outsidePointForEnd = null; |
| 6dd74de | | | 661 | let outsideIndexForEnd = -1; |
| 6dd74de | | | 662 | |
| 6dd74de | | | 663 | for (let i = points.length - 1; i >= 0; i--) { |
| 6dd74de | | | 664 | if (outsideNode(endBounds, points[i])) { |
| 6dd74de | | | 665 | outsidePointForEnd = points[i]; |
| 6dd74de | | | 666 | outsideIndexForEnd = i; |
| 6dd74de | | | 667 | break; |
| 6dd74de | | | 668 | } |
| 6dd74de | | | 669 | } |
| 6dd74de | | | 670 | |
| 6dd74de | | | 671 | if (!outsidePointForEnd && points.length > 1) { |
| 6dd74de | | | 672 | outsidePointForEnd = points[points.length - 2]; |
| 6dd74de | | | 673 | outsideIndexForEnd = points.length - 2; |
| 6dd74de | | | 674 | } |
| 6dd74de | | | 675 | |
| 6dd74de | | | 676 | if (outsidePointForEnd) { |
| 6dd74de | | | 677 | const endIntersection = computeNodeIntersection( |
| 6dd74de | | | 678 | endNode, |
| 6dd74de | | | 679 | endBounds, |
| 6dd74de | | | 680 | outsidePointForEnd, |
| 6dd74de | | | 681 | endCenter |
| 6dd74de | | | 682 | ); |
| 6dd74de | | | 683 | log.debug('UIO cutter2: end intersection', { endIntersection, outsideIndexForEnd }); |
| 6dd74de | | | 684 | replaceEndpoint(points, 'end', endIntersection); |
| 6dd74de | | | 685 | } |
| 6dd74de | | | 686 | |
| 6dd74de | | | 687 | // Final cleanup: Check if the last point is too close to the previous point |
| 6dd74de | | | 688 | if (points.length > 1) { |
| 6dd74de | | | 689 | const lastPoint = points[points.length - 1]; |
| 6dd74de | | | 690 | const secondLastPoint = points[points.length - 2]; |
| 6dd74de | | | 691 | const distance = Math.sqrt( |
| 6dd74de | | | 692 | (lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2 |
| 6dd74de | | | 693 | ); |
| 6dd74de | | | 694 | if (distance < 2) { |
| 6dd74de | | | 695 | log.debug('UIO cutter2: trimming tail point (too close)', { |
| 6dd74de | | | 696 | distance, |
| 6dd74de | | | 697 | lastPoint, |
| 6dd74de | | | 698 | secondLastPoint, |
| 6dd74de | | | 699 | }); |
| 6dd74de | | | 700 | points.pop(); |
| 6dd74de | | | 701 | } |
| 6dd74de | | | 702 | } |
| 6dd74de | | | 703 | |
| 6dd74de | | | 704 | log.debug('UIO cutter2: final points', points); |
| 6dd74de | | | 705 | |
| 6dd74de | | | 706 | return points; |
| 6dd74de | | | 707 | }; |
| 6dd74de | | | 708 | |
| 6dd74de | | | 709 | // @ts-ignore - ELK is not typed |
| 6dd74de | | | 710 | const elk = new ELK(); |
| 6dd74de | | | 711 | const element = svg.select('g'); |
| 6dd74de | | | 712 | // Add the arrowheads to the svg |
| 6dd74de | | | 713 | insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId); |
| 6dd74de | | | 714 | |
| 6dd74de | | | 715 | // Setup the graph with the layout options and the data for the layout |
| 6dd74de | | | 716 | let elkGraph: any = { |
| 6dd74de | | | 717 | id: 'root', |
| 6dd74de | | | 718 | layoutOptions: { |
| 6dd74de | | | 719 | 'elk.hierarchyHandling': 'INCLUDE_CHILDREN', |
| 6dd74de | | | 720 | 'elk.algorithm': algorithm, |
| 6dd74de | | | 721 | 'nodePlacement.strategy': data4Layout.config.elk?.nodePlacementStrategy, |
| 6dd74de | | | 722 | 'elk.layered.mergeEdges': data4Layout.config.elk?.mergeEdges, |
| 6dd74de | | | 723 | 'elk.direction': 'DOWN', |
| 6dd74de | | | 724 | 'spacing.baseValue': 40, |
| 6dd74de | | | 725 | 'elk.layered.crossingMinimization.forceNodeModelOrder': |
| 6dd74de | | | 726 | data4Layout.config.elk?.forceNodeModelOrder, |
| 6dd74de | | | 727 | 'elk.layered.considerModelOrder.strategy': data4Layout.config.elk?.considerModelOrder, |
| 6dd74de | | | 728 | 'elk.layered.unnecessaryBendpoints': true, |
| 6dd74de | | | 729 | 'elk.layered.cycleBreaking.strategy': data4Layout.config.elk?.cycleBreakingStrategy, |
| 6dd74de | | | 730 | |
| 6dd74de | | | 731 | // 'elk.layered.cycleBreaking.strategy': 'GREEDY_MODEL_ORDER', |
| 6dd74de | | | 732 | // 'elk.layered.cycleBreaking.strategy': 'MODEL_ORDER', |
| 6dd74de | | | 733 | // 'spacing.nodeNode': 20, |
| 6dd74de | | | 734 | // 'spacing.nodeNodeBetweenLayers': 25, |
| 6dd74de | | | 735 | // 'spacing.edgeNode': 20, |
| 6dd74de | | | 736 | // 'spacing.edgeNodeBetweenLayers': 10, |
| 6dd74de | | | 737 | // 'spacing.edgeEdge': 10, |
| 6dd74de | | | 738 | // 'spacing.edgeEdgeBetweenLayers': 20, |
| 6dd74de | | | 739 | // 'spacing.nodeSelfLoop': 20, |
| 6dd74de | | | 740 | |
| 6dd74de | | | 741 | // Tweaking options |
| 6dd74de | | | 742 | // 'nodePlacement.favorStraightEdges': true, |
| 6dd74de | | | 743 | // 'elk.layered.nodePlacement.favorStraightEdges': true, |
| 6dd74de | | | 744 | // 'nodePlacement.feedbackEdges': true, |
| 6dd74de | | | 745 | 'elk.layered.wrapping.multiEdge.improveCuts': true, |
| 6dd74de | | | 746 | 'elk.layered.wrapping.multiEdge.improveWrappedEdges': true, |
| 6dd74de | | | 747 | // 'elk.layered.wrapping.strategy': 'MULTI_EDGE', |
| 6dd74de | | | 748 | // 'elk.layered.wrapping.strategy': 'SINGLE_EDGE', |
| 6dd74de | | | 749 | 'elk.layered.edgeRouting.selfLoopDistribution': 'EQUALLY', |
| 6dd74de | | | 750 | 'elk.layered.mergeHierarchyEdges': true, |
| 6dd74de | | | 751 | |
| 6dd74de | | | 752 | // 'elk.layered.feedbackEdges': true, |
| 6dd74de | | | 753 | // 'elk.layered.crossingMinimization.semiInteractive': true, |
| 6dd74de | | | 754 | // 'elk.layered.edgeRouting.splines.sloppy.layerSpacingFactor': 1, |
| 6dd74de | | | 755 | // 'elk.layered.edgeRouting.polyline.slopedEdgeZoneWidth': 4.0, |
| 6dd74de | | | 756 | // 'elk.layered.wrapping.validify.strategy': 'LOOK_BACK', |
| 6dd74de | | | 757 | // 'elk.insideSelfLoops.activate': true, |
| 6dd74de | | | 758 | // 'elk.separateConnectedComponents': true, |
| 6dd74de | | | 759 | // 'elk.alg.layered.options.EdgeStraighteningStrategy': 'NONE', |
| 6dd74de | | | 760 | // 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES', // NODES_AND_EDGES |
| 6dd74de | | | 761 | // 'elk.layered.considerModelOrder.strategy': 'EDGES', // NODES_AND_EDGES |
| 6dd74de | | | 762 | // 'elk.layered.wrapping.cutting.strategy': 'ARD', // NODES_AND_EDGES |
| 6dd74de | | | 763 | }, |
| 6dd74de | | | 764 | children: [], |
| 6dd74de | | | 765 | edges: [], |
| 6dd74de | | | 766 | }; |
| 6dd74de | | | 767 | |
| 6dd74de | | | 768 | log.info('Drawing flowchart using v4 renderer', elk); |
| 6dd74de | | | 769 | |
| 6dd74de | | | 770 | // Set the direction of the graph based on the parsed information |
| 6dd74de | | | 771 | const dir = data4Layout.direction ?? 'DOWN'; |
| 6dd74de | | | 772 | elkGraph.layoutOptions['elk.direction'] = dir2ElkDirection(dir); |
| 6dd74de | | | 773 | |
| 6dd74de | | | 774 | // Create the lookup db for the subgraphs and their children to used when creating |
| 6dd74de | | | 775 | // the tree structured graph |
| 6dd74de | | | 776 | const parentLookupDb: any = addSubGraphs(data4Layout.nodes); |
| 6dd74de | | | 777 | |
| 6dd74de | | | 778 | // Add elements in the svg to be used to hold the subgraphs container |
| 6dd74de | | | 779 | // elements and the nodes |
| 6dd74de | | | 780 | const subGraphsEl = svg.insert('g').attr('class', 'subgraphs'); |
| 6dd74de | | | 781 | |
| 6dd74de | | | 782 | const nodeEl = svg.insert('g').attr('class', 'nodes'); |
| 6dd74de | | | 783 | |
| 6dd74de | | | 784 | // Add the nodes to the graph, this will entail creating the actual nodes |
| 6dd74de | | | 785 | // in order to get the size of the node. You can't get the size of a node |
| 6dd74de | | | 786 | // that is not in the dom so we need to add it to the dom, get the size |
| 6dd74de | | | 787 | // we will position the nodes when we get the layout from elkjs |
| 6dd74de | | | 788 | elkGraph = await addVertices(nodeEl, data4Layout.nodes, elkGraph); |
| 6dd74de | | | 789 | // Time for the edges, we start with adding an element in the node to hold the edges |
| 6dd74de | | | 790 | const edgesEl = svg.insert('g').attr('class', 'edges edgePaths'); |
| 6dd74de | | | 791 | |
| 6dd74de | | | 792 | // Add the edges to the elk graph, this will entail creating the actual edges |
| 6dd74de | | | 793 | elkGraph = await addEdges(data4Layout, elkGraph, svg); |
| 6dd74de | | | 794 | |
| 6dd74de | | | 795 | // Iterate through all nodes and add the top level nodes to the graph |
| 6dd74de | | | 796 | const nodes = data4Layout.nodes; |
| 6dd74de | | | 797 | nodes.forEach((n: { id: string | number }) => { |
| 6dd74de | | | 798 | const node = nodeDb[n.id]; |
| 6dd74de | | | 799 | |
| 6dd74de | | | 800 | // Subgraph |
| 6dd74de | | | 801 | if (parentLookupDb.childrenById[node.id] !== undefined) { |
| 6dd74de | | | 802 | // Set label and adjust node width separately (avoid side effects in labels array) |
| 6dd74de | | | 803 | node.labels = [ |
| 6dd74de | | | 804 | { |
| 6dd74de | | | 805 | text: node.label, |
| 6dd74de | | | 806 | width: node?.labelData?.width ?? 50, |
| 6dd74de | | | 807 | height: node?.labelData?.height ?? 50, |
| 6dd74de | | | 808 | }, |
| 6dd74de | | | 809 | ]; |
| 6dd74de | | | 810 | node.width = node.width + 2 * node.padding; |
| 6dd74de | | | 811 | log.debug('UIO node label', node?.labelData?.width, node.padding); |
| 6dd74de | | | 812 | node.layoutOptions = { |
| 6dd74de | | | 813 | 'spacing.baseValue': 30, |
| 6dd74de | | | 814 | 'nodeLabels.placement': '[H_CENTER V_TOP, INSIDE]', |
| 6dd74de | | | 815 | }; |
| 6dd74de | | | 816 | if (node.dir) { |
| 6dd74de | | | 817 | node.layoutOptions = { |
| 6dd74de | | | 818 | ...node.layoutOptions, |
| 6dd74de | | | 819 | 'elk.algorithm': algorithm, |
| 6dd74de | | | 820 | 'elk.direction': dir2ElkDirection(node.dir), |
| 6dd74de | | | 821 | 'nodePlacement.strategy': data4Layout.config.elk?.nodePlacementStrategy, |
| 6dd74de | | | 822 | 'elk.layered.mergeEdges': data4Layout.config.elk?.mergeEdges, |
| 6dd74de | | | 823 | 'elk.hierarchyHandling': 'SEPARATE_CHILDREN', |
| 6dd74de | | | 824 | }; |
| 6dd74de | | | 825 | } |
| 6dd74de | | | 826 | delete node.x; |
| 6dd74de | | | 827 | delete node.y; |
| 6dd74de | | | 828 | delete node.width; |
| 6dd74de | | | 829 | delete node.height; |
| 6dd74de | | | 830 | } |
| 6dd74de | | | 831 | }); |
| 6dd74de | | | 832 | log.debug('APA01 processing edges, count:', elkGraph.edges.length); |
| 6dd74de | | | 833 | elkGraph.edges.forEach((edge: any, index: number) => { |
| 6dd74de | | | 834 | log.debug('APA01 processing edge', index, ':', edge); |
| 6dd74de | | | 835 | const source = edge.sources[0]; |
| 6dd74de | | | 836 | const target = edge.targets[0]; |
| 6dd74de | | | 837 | log.debug('APA01 source:', source, 'target:', target); |
| 6dd74de | | | 838 | log.debug('APA01 nodeDb[source]:', nodeDb[source]); |
| 6dd74de | | | 839 | log.debug('APA01 nodeDb[target]:', nodeDb[target]); |
| 6dd74de | | | 840 | |
| 6dd74de | | | 841 | if (nodeDb[source] && nodeDb[target] && nodeDb[source].parentId !== nodeDb[target].parentId) { |
| 6dd74de | | | 842 | const ancestorId = findCommonAncestor(source, target, parentLookupDb); |
| 6dd74de | | | 843 | // an edge that breaks a subgraph has been identified, set configuration accordingly |
| 6dd74de | | | 844 | setIncludeChildrenPolicy(source, ancestorId); |
| 6dd74de | | | 845 | setIncludeChildrenPolicy(target, ancestorId); |
| 6dd74de | | | 846 | } |
| 6dd74de | | | 847 | }); |
| 6dd74de | | | 848 | |
| 6dd74de | | | 849 | log.debug('APA01 before'); |
| 6dd74de | | | 850 | log.debug('APA01 elkGraph structure:', JSON.stringify(elkGraph, null, 2)); |
| 6dd74de | | | 851 | log.debug('APA01 elkGraph.children length:', elkGraph.children?.length); |
| 6dd74de | | | 852 | log.debug('APA01 elkGraph.edges length:', elkGraph.edges?.length); |
| 6dd74de | | | 853 | |
| 6dd74de | | | 854 | // Validate that all edge references exist as nodes |
| 6dd74de | | | 855 | elkGraph.edges?.forEach((edge: any, index: number) => { |
| 6dd74de | | | 856 | log.debug(`APA01 validating edge ${index}:`, edge); |
| 6dd74de | | | 857 | if (edge.sources) { |
| 6dd74de | | | 858 | edge.sources.forEach((sourceId: any) => { |
| 6dd74de | | | 859 | const sourceExists = elkGraph.children?.some((child: any) => child.id === sourceId); |
| 6dd74de | | | 860 | log.debug(`APA01 source ${sourceId} exists:`, sourceExists); |
| 6dd74de | | | 861 | }); |
| 6dd74de | | | 862 | } |
| 6dd74de | | | 863 | if (edge.targets) { |
| 6dd74de | | | 864 | edge.targets.forEach((targetId: any) => { |
| 6dd74de | | | 865 | const targetExists = elkGraph.children?.some((child: any) => child.id === targetId); |
| 6dd74de | | | 866 | log.debug(`APA01 target ${targetId} exists:`, targetExists); |
| 6dd74de | | | 867 | }); |
| 6dd74de | | | 868 | } |
| 6dd74de | | | 869 | }); |
| 6dd74de | | | 870 | |
| 6dd74de | | | 871 | let g; |
| 6dd74de | | | 872 | try { |
| 6dd74de | | | 873 | g = await elk.layout(elkGraph); |
| 6dd74de | | | 874 | log.debug('APA01 after - success'); |
| 6dd74de | | | 875 | log.info('APA01 layout result:', JSON.stringify(g, null, 2)); |
| 6dd74de | | | 876 | } catch (error) { |
| 6dd74de | | | 877 | log.error('APA01 ELK layout error:', error); |
| 6dd74de | | | 878 | log.error('APA01 elkGraph that caused error:', JSON.stringify(elkGraph, null, 2)); |
| 6dd74de | | | 879 | throw error; |
| 6dd74de | | | 880 | } |
| 6dd74de | | | 881 | |
| 6dd74de | | | 882 | // debugger; |
| 6dd74de | | | 883 | await drawNodes(0, 0, g.children, svg, subGraphsEl, 0); |
| 6dd74de | | | 884 | |
| 6dd74de | | | 885 | g.edges?.map( |
| 6dd74de | | | 886 | (edge: { |
| 6dd74de | | | 887 | sources: (string | number)[]; |
| 6dd74de | | | 888 | targets: (string | number)[]; |
| 6dd74de | | | 889 | start: any; |
| 6dd74de | | | 890 | end: any; |
| 6dd74de | | | 891 | sections: { startPoint: any; endPoint: any; bendPoints: any }[]; |
| 6dd74de | | | 892 | points: any[]; |
| 6dd74de | | | 893 | x: any; |
| 6dd74de | | | 894 | labels: { height: number; width: number; x: number; y: number }[]; |
| 6dd74de | | | 895 | y: any; |
| 6dd74de | | | 896 | }) => { |
| 6dd74de | | | 897 | // (elem, edge, clusterDb, diagramType, graph, id) |
| 6dd74de | | | 898 | const startNode = nodeDb[edge.sources[0]]; |
| 6dd74de | | | 899 | const startCluster = parentLookupDb[edge.sources[0]]; |
| 6dd74de | | | 900 | const endNode = nodeDb[edge.targets[0]]; |
| 6dd74de | | | 901 | const sourceId = edge.start; |
| 6dd74de | | | 902 | const targetId = edge.end; |
| 6dd74de | | | 903 | |
| 6dd74de | | | 904 | const offset = calcOffset(sourceId, targetId, parentLookupDb); |
| 6dd74de | | | 905 | log.debug( |
| 6dd74de | | | 906 | 'APA18 offset', |
| 6dd74de | | | 907 | offset, |
| 6dd74de | | | 908 | sourceId, |
| 6dd74de | | | 909 | ' ==> ', |
| 6dd74de | | | 910 | targetId, |
| 6dd74de | | | 911 | 'edge:', |
| 6dd74de | | | 912 | edge, |
| 6dd74de | | | 913 | 'cluster:', |
| 6dd74de | | | 914 | startCluster, |
| 6dd74de | | | 915 | startNode |
| 6dd74de | | | 916 | ); |
| 6dd74de | | | 917 | if (edge.sections) { |
| 6dd74de | | | 918 | const src = edge.sections[0].startPoint; |
| 6dd74de | | | 919 | const dest = edge.sections[0].endPoint; |
| 6dd74de | | | 920 | const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : []; |
| 6dd74de | | | 921 | |
| 6dd74de | | | 922 | const segPoints = segments.map((segment: { x: any; y: any }) => { |
| 6dd74de | | | 923 | return { x: segment.x + offset.x, y: segment.y + offset.y }; |
| 6dd74de | | | 924 | }); |
| 6dd74de | | | 925 | edge.points = [ |
| 6dd74de | | | 926 | { x: src.x + offset.x, y: src.y + offset.y }, |
| 6dd74de | | | 927 | ...segPoints, |
| 6dd74de | | | 928 | { x: dest.x + offset.x, y: dest.y + offset.y }, |
| 6dd74de | | | 929 | ]; |
| 6dd74de | | | 930 | |
| 6dd74de | | | 931 | let sw = startNode.width; |
| 6dd74de | | | 932 | let ew = endNode.width; |
| 6dd74de | | | 933 | if (startNode.isGroup) { |
| 6dd74de | | | 934 | const bbox = startNode.domId.node().getBBox(); |
| 6dd74de | | | 935 | // sw = Math.max(bbox.width, startNode.width, startNode.labels[0].width); |
| 6dd74de | | | 936 | sw = Math.max(startNode.width, startNode.labels[0].width + startNode.padding); |
| 6dd74de | | | 937 | // sw = startNode.width; |
| 6dd74de | | | 938 | log.info( |
| 6dd74de | | | 939 | 'UIO width', |
| 6dd74de | | | 940 | startNode.id, |
| 6dd74de | | | 941 | startNode.width, |
| 6dd74de | | | 942 | 'bbox.width=', |
| 6dd74de | | | 943 | bbox.width, |
| 6dd74de | | | 944 | 'lw=', |
| 6dd74de | | | 945 | startNode.labels[0].width, |
| 6dd74de | | | 946 | 'node:', |
| 6dd74de | | | 947 | startNode.width, |
| 6dd74de | | | 948 | 'SW = ', |
| 6dd74de | | | 949 | sw |
| 6dd74de | | | 950 | // 'HTML:', |
| 6dd74de | | | 951 | // startNode.domId.node().innerHTML |
| 6dd74de | | | 952 | ); |
| 6dd74de | | | 953 | } |
| 6dd74de | | | 954 | if (endNode.isGroup) { |
| 6dd74de | | | 955 | const bbox = endNode.domId.node().getBBox(); |
| 6dd74de | | | 956 | ew = Math.max(endNode.width, endNode.labels[0].width + endNode.padding); |
| 6dd74de | | | 957 | |
| 6dd74de | | | 958 | log.debug( |
| 6dd74de | | | 959 | 'UIO width', |
| 6dd74de | | | 960 | startNode.id, |
| 6dd74de | | | 961 | startNode.width, |
| 6dd74de | | | 962 | bbox.width, |
| 6dd74de | | | 963 | 'EW = ', |
| 6dd74de | | | 964 | ew, |
| 6dd74de | | | 965 | 'HTML:', |
| 6dd74de | | | 966 | startNode.innerHTML |
| 6dd74de | | | 967 | ); |
| 6dd74de | | | 968 | } |
| 6dd74de | | | 969 | startNode.x = startNode.offset.posX + startNode.width / 2; |
| 6dd74de | | | 970 | startNode.y = startNode.offset.posY + startNode.height / 2; |
| 6dd74de | | | 971 | endNode.x = endNode.offset.posX + endNode.width / 2; |
| 6dd74de | | | 972 | endNode.y = endNode.offset.posY + endNode.height / 2; |
| 6dd74de | | | 973 | |
| 6dd74de | | | 974 | // Only add center points for non-subgraph nodes or when the edge path doesn't already end near the target |
| 6dd74de | | | 975 | const shouldAddStartCenter = startNode.shape !== 'rect33'; |
| 6dd74de | | | 976 | const shouldAddEndCenter = endNode.shape !== 'rect33'; |
| 6dd74de | | | 977 | |
| 6dd74de | | | 978 | if (shouldAddStartCenter) { |
| 6dd74de | | | 979 | edge.points.unshift({ |
| 6dd74de | | | 980 | x: startNode.x, |
| 6dd74de | | | 981 | y: startNode.y, |
| 6dd74de | | | 982 | }); |
| 6dd74de | | | 983 | } |
| 6dd74de | | | 984 | |
| 6dd74de | | | 985 | if (shouldAddEndCenter) { |
| 6dd74de | | | 986 | edge.points.push({ |
| 6dd74de | | | 987 | x: endNode.x, |
| 6dd74de | | | 988 | y: endNode.y, |
| 6dd74de | | | 989 | }); |
| 6dd74de | | | 990 | } |
| 6dd74de | | | 991 | |
| 6dd74de | | | 992 | // Debug and sanitize points around cutter2 |
| 6dd74de | | | 993 | const prevPoints = Array.isArray(edge.points) ? [...edge.points] : []; |
| 6dd74de | | | 994 | const endBounds = boundsFor(endNode); |
| 6dd74de | | | 995 | log.debug( |
| 6dd74de | | | 996 | 'PPP cutter2: Points before cutter2:', |
| 6dd74de | | | 997 | JSON.stringify(edge.points), |
| 6dd74de | | | 998 | 'endBounds:', |
| 6dd74de | | | 999 | endBounds, |
| 6dd74de | | | 1000 | onBorder(endBounds, edge.points[edge.points.length - 1]) |
| 6dd74de | | | 1001 | ); |
| 6dd74de | | | 1002 | // Block for reducing variable scope and guardrails for the cutter function |
| 6dd74de | | | 1003 | { |
| 6dd74de | | | 1004 | const startBounds = boundsFor(startNode); |
| 6dd74de | | | 1005 | const endBounds = boundsFor(endNode); |
| 6dd74de | | | 1006 | |
| 6dd74de | | | 1007 | const startIsGroup = !!startNode?.isGroup; |
| 6dd74de | | | 1008 | const endIsGroup = !!endNode?.isGroup; |
| 6dd74de | | | 1009 | |
| 6dd74de | | | 1010 | const { candidate: startCandidate, centerApprox: startCenterApprox } = |
| 6dd74de | | | 1011 | getCandidateBorderPoint(prevPoints as P[], startNode, 'start'); |
| 6dd74de | | | 1012 | const { candidate: endCandidate, centerApprox: endCenterApprox } = |
| 6dd74de | | | 1013 | getCandidateBorderPoint(prevPoints as P[], endNode, 'end'); |
| 6dd74de | | | 1014 | |
| 6dd74de | | | 1015 | const skipStart = startIsGroup && onBorder(startBounds, startCandidate); |
| 6dd74de | | | 1016 | const skipEnd = endIsGroup && onBorder(endBounds, endCandidate); |
| 6dd74de | | | 1017 | |
| 6dd74de | | | 1018 | dropAutoCenterPoint(prevPoints as P[], 'start', skipStart && startCenterApprox); |
| 6dd74de | | | 1019 | dropAutoCenterPoint(prevPoints as P[], 'end', skipEnd && endCenterApprox); |
| 6dd74de | | | 1020 | |
| 6dd74de | | | 1021 | if (skipStart || skipEnd) { |
| 6dd74de | | | 1022 | if (!skipStart) { |
| 6dd74de | | | 1023 | applyStartIntersectionIfNeeded(prevPoints as P[], startNode, startBounds); |
| 6dd74de | | | 1024 | } |
| 6dd74de | | | 1025 | if (!skipEnd) { |
| 6dd74de | | | 1026 | applyEndIntersectionIfNeeded(prevPoints as P[], endNode, endBounds); |
| 6dd74de | | | 1027 | } |
| 6dd74de | | | 1028 | |
| 6dd74de | | | 1029 | log.debug('PPP cutter2: skipping cutter2 due to on-border group endpoint(s)', { |
| 6dd74de | | | 1030 | skipStart, |
| 6dd74de | | | 1031 | skipEnd, |
| 6dd74de | | | 1032 | startCenterApprox, |
| 6dd74de | | | 1033 | endCenterApprox, |
| 6dd74de | | | 1034 | startCandidate, |
| 6dd74de | | | 1035 | endCandidate, |
| 6dd74de | | | 1036 | }); |
| 6dd74de | | | 1037 | edge.points = prevPoints; |
| 6dd74de | | | 1038 | } else { |
| 6dd74de | | | 1039 | edge.points = cutter2(startNode, endNode, prevPoints); |
| 6dd74de | | | 1040 | } |
| 6dd74de | | | 1041 | } |
| 6dd74de | | | 1042 | log.debug('PPP cutter2: Points after cutter2:', JSON.stringify(edge.points)); |
| 6dd74de | | | 1043 | const hasNaN = (pts: { x: number; y: number }[]) => |
| 6dd74de | | | 1044 | pts?.some((p) => !Number.isFinite(p?.x) || !Number.isFinite(p?.y)); |
| 6dd74de | | | 1045 | if (!Array.isArray(edge.points) || edge.points.length < 2 || hasNaN(edge.points)) { |
| 6dd74de | | | 1046 | log.warn( |
| 6dd74de | | | 1047 | 'POI cutter2: Invalid points from cutter2, falling back to prevPoints', |
| 6dd74de | | | 1048 | edge.points |
| 6dd74de | | | 1049 | ); |
| 6dd74de | | | 1050 | // Fallback to previous points and strip any invalid ones just in case |
| 6dd74de | | | 1051 | const cleaned = prevPoints.filter((p) => Number.isFinite(p?.x) && Number.isFinite(p?.y)); |
| 6dd74de | | | 1052 | edge.points = cleaned.length >= 2 ? cleaned : prevPoints; |
| 6dd74de | | | 1053 | } |
| 6dd74de | | | 1054 | log.debug('UIO cutter2: Points after cutter2 (sanitized):', edge.points); |
| 6dd74de | | | 1055 | // Remove consecutive duplicate points to avoid zero-length segments in path builders |
| 6dd74de | | | 1056 | const deduped = edge.points.filter( |
| 6dd74de | | | 1057 | (p: { x: number; y: number }, i: number, arr: { x: number; y: number }[]) => { |
| 6dd74de | | | 1058 | if (i === 0) { |
| 6dd74de | | | 1059 | return true; |
| 6dd74de | | | 1060 | } |
| 6dd74de | | | 1061 | const prev = arr[i - 1]; |
| 6dd74de | | | 1062 | return Math.abs(p.x - prev.x) > 1e-6 || Math.abs(p.y - prev.y) > 1e-6; |
| 6dd74de | | | 1063 | } |
| 6dd74de | | | 1064 | ); |
| 6dd74de | | | 1065 | if (deduped.length !== edge.points.length) { |
| 6dd74de | | | 1066 | log.debug('UIO cutter2: removed consecutive duplicate points', { |
| 6dd74de | | | 1067 | before: edge.points, |
| 6dd74de | | | 1068 | after: deduped, |
| 6dd74de | | | 1069 | }); |
| 6dd74de | | | 1070 | } |
| 6dd74de | | | 1071 | edge.points = deduped; |
| 6dd74de | | | 1072 | const paths = insertEdge( |
| 6dd74de | | | 1073 | edgesEl, |
| 6dd74de | | | 1074 | edge, |
| 6dd74de | | | 1075 | clusterDb, |
| 6dd74de | | | 1076 | data4Layout.type, |
| 6dd74de | | | 1077 | startNode, |
| 6dd74de | | | 1078 | endNode, |
| 6dd74de | | | 1079 | data4Layout.diagramId, |
| 6dd74de | | | 1080 | true |
| 6dd74de | | | 1081 | ); |
| 6dd74de | | | 1082 | log.info('APA12 edge points after insert', JSON.stringify(edge.points)); |
| 6dd74de | | | 1083 | |
| 6dd74de | | | 1084 | edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2; |
| 6dd74de | | | 1085 | edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2; |
| 6dd74de | | | 1086 | positionEdgeLabel(edge, paths); |
| 6dd74de | | | 1087 | } |
| 6dd74de | | | 1088 | } |
| 6dd74de | | | 1089 | ); |
| 6dd74de | | | 1090 | }; |