mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-10-31 10:54:15 +01:00 
			
		
		
		
	#5237 WIP, refactoring, adding
This commit is contained in:
		| @@ -3,8 +3,8 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; | |||||||
| import parser from './parser/stateDiagram.jison'; | import parser from './parser/stateDiagram.jison'; | ||||||
| import db from './stateDb.js'; | import db from './stateDb.js'; | ||||||
| import styles from './styles.js'; | import styles from './styles.js'; | ||||||
| import renderer from './stateRenderer-v2.js'; | // import renderer from './stateRenderer-v2.js'; | ||||||
| // import renderer from './stateRenderer-v3-unified.js'; | import renderer from './stateRenderer-v3-unified.js'; | ||||||
|  |  | ||||||
| export const diagram: DiagramDefinition = { | export const diagram: DiagramDefinition = { | ||||||
|   parser, |   parser, | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js'; | import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js'; | ||||||
| import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js'; | import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js'; | ||||||
| import insertMarkers from './markers.js'; | import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; | ||||||
| import { updateNodeBounds } from './shapes/util.js'; | import insertMarkers from '../../rendering-elements/markers.js'; | ||||||
|  | import { updateNodeBounds } from '../../rendering-elements/shapes/util.js'; | ||||||
| import { | import { | ||||||
|   clear as clearGraphlib, |   clear as clearGraphlib, | ||||||
|   clusterDb, |   clusterDb, | ||||||
| @@ -9,12 +10,22 @@ import { | |||||||
|   findNonClusterChild, |   findNonClusterChild, | ||||||
|   sortNodesByHierarchy, |   sortNodesByHierarchy, | ||||||
| } from './mermaid-graphlib.js'; | } from './mermaid-graphlib.js'; | ||||||
| import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes.js'; | import { | ||||||
| import { insertCluster, clear as clearClusters } from './clusters.js'; |   insertNode, | ||||||
| import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js'; |   positionNode, | ||||||
| import { log } from '../logger.js'; |   clear as clearNodes, | ||||||
| import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js'; |   setNodeElem, | ||||||
| import { getConfig } from '../diagram-api/diagramAPI.js'; | } from '../../rendering-elements/nodes.js'; | ||||||
|  | import { insertCluster, clear as clearClusters } from '../../rendering-elements/clusters.js'; | ||||||
|  | import { | ||||||
|  |   insertEdgeLabel, | ||||||
|  |   positionEdgeLabel, | ||||||
|  |   insertEdge, | ||||||
|  |   clear as clearEdges, | ||||||
|  | } from '../../rendering-elements/edges.js'; | ||||||
|  | import { log } from '$root/logger.js'; | ||||||
|  | import { getSubGraphTitleMargins } from '../../../utils/subGraphTitleMargins.js'; | ||||||
|  | import { getConfig } from '../../../diagram-api/diagramAPI.js'; | ||||||
| 
 | 
 | ||||||
| const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, siteConfig) => { | const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, siteConfig) => { | ||||||
|   log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster); |   log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster); | ||||||
| @@ -161,19 +172,49 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, sit | |||||||
|   return { elem, diff }; |   return { elem, diff }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const render = async (elem, graph, markers, diagramtype, id) => { | export const render = async (data4Layout, svg, element) => { | ||||||
|   insertMarkers(elem, markers, diagramtype, id); |   console.warn('HERERERERERER'); | ||||||
|  |   // Create the input mermaid.graph
 | ||||||
|  |   const graph = new graphlib.Graph({ | ||||||
|  |     multigraph: true, | ||||||
|  |     compound: true, | ||||||
|  |   }) | ||||||
|  |     .setGraph({ | ||||||
|  |       rankdir: data4Layout.direction, | ||||||
|  |       nodesep: data4Layout.nodeSpacing, | ||||||
|  |       ranksep: data4Layout.rankSpacing, | ||||||
|  |       marginx: 8, | ||||||
|  |       marginy: 8, | ||||||
|  |     }) | ||||||
|  |     .setDefaultEdgeLabel(function () { | ||||||
|  |       return {}; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |   // Org
 | ||||||
|  | 
 | ||||||
|  |   insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId); | ||||||
|   clearNodes(); |   clearNodes(); | ||||||
|   clearEdges(); |   clearEdges(); | ||||||
|   clearClusters(); |   clearClusters(); | ||||||
|   clearGraphlib(); |   clearGraphlib(); | ||||||
| 
 | 
 | ||||||
|  |   // Add the nodes and edges to the graph
 | ||||||
|  |   data4Layout.nodes.forEach((node) => { | ||||||
|  |     graph.setNode(node.id, { ...node }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   log.warn('Graph at first:', JSON.stringify(graphlibJson.write(graph))); |   log.warn('Graph at first:', JSON.stringify(graphlibJson.write(graph))); | ||||||
|   adjustClustersAndEdges(graph); |   adjustClustersAndEdges(graph); | ||||||
|   log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph))); |   log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph))); | ||||||
|   // log.warn('Graph ever  after:', graphlibJson.write(graph.node('A').graph));
 |  | ||||||
|   const siteConfig = getConfig(); |   const siteConfig = getConfig(); | ||||||
|   await recursiveRender(elem, graph, diagramtype, id, undefined, siteConfig); |   await recursiveRender( | ||||||
|  |     element, | ||||||
|  |     graph, | ||||||
|  |     data4Layout.type, | ||||||
|  |     data4Layout.diagramId, | ||||||
|  |     undefined, | ||||||
|  |     siteConfig | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // const shapeDefinitions = {};
 | // const shapeDefinitions = {};
 | ||||||
| @@ -0,0 +1,474 @@ | |||||||
|  | /** Decorates with functions required by mermaids dagre-wrapper. */ | ||||||
|  | import { log } from '$root/logger.js'; | ||||||
|  | import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js'; | ||||||
|  | import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; | ||||||
|  |  | ||||||
|  | export let clusterDb = {}; | ||||||
|  | let descendants = {}; | ||||||
|  | let parents = {}; | ||||||
|  |  | ||||||
|  | export const clear = () => { | ||||||
|  |   descendants = {}; | ||||||
|  |   parents = {}; | ||||||
|  |   clusterDb = {}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const isDescendant = (id, ancenstorId) => { | ||||||
|  |   // if (id === ancenstorId) return true; | ||||||
|  |  | ||||||
|  |   log.trace('In isDecendant', ancenstorId, ' ', id, ' = ', descendants[ancenstorId].includes(id)); | ||||||
|  |   if (descendants[ancenstorId].includes(id)) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return false; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const edgeInCluster = (edge, clusterId) => { | ||||||
|  |   log.info('Decendants of ', clusterId, ' is ', descendants[clusterId]); | ||||||
|  |   log.info('Edge is ', edge); | ||||||
|  |   // Edges to/from the cluster is not in the cluster, they are in the parent | ||||||
|  |   if (edge.v === clusterId) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   if (edge.w === clusterId) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!descendants[clusterId]) { | ||||||
|  |     log.debug('Tilt, ', clusterId, ',not in decendants'); | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   return ( | ||||||
|  |     descendants[clusterId].includes(edge.v) || | ||||||
|  |     isDescendant(edge.v, clusterId) || | ||||||
|  |     isDescendant(edge.w, clusterId) || | ||||||
|  |     descendants[clusterId].includes(edge.w) | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const copy = (clusterId, graph, newGraph, rootId) => { | ||||||
|  |   log.warn( | ||||||
|  |     'Copying children of ', | ||||||
|  |     clusterId, | ||||||
|  |     'root', | ||||||
|  |     rootId, | ||||||
|  |     'data', | ||||||
|  |     graph.node(clusterId), | ||||||
|  |     rootId | ||||||
|  |   ); | ||||||
|  |   const nodes = graph.children(clusterId) || []; | ||||||
|  |  | ||||||
|  |   // Include cluster node if it is not the root | ||||||
|  |   if (clusterId !== rootId) { | ||||||
|  |     nodes.push(clusterId); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   log.warn('Copying (nodes) clusterId', clusterId, 'nodes', nodes); | ||||||
|  |  | ||||||
|  |   nodes.forEach((node) => { | ||||||
|  |     if (graph.children(node).length > 0) { | ||||||
|  |       copy(node, graph, newGraph, rootId); | ||||||
|  |     } else { | ||||||
|  |       const data = graph.node(node); | ||||||
|  |       log.info('cp ', node, ' to ', rootId, ' with parent ', clusterId); //,node, data, ' parent is ', clusterId); | ||||||
|  |       newGraph.setNode(node, data); | ||||||
|  |       if (rootId !== graph.parent(node)) { | ||||||
|  |         log.warn('Setting parent', node, graph.parent(node)); | ||||||
|  |         newGraph.setParent(node, graph.parent(node)); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (clusterId !== rootId && node !== clusterId) { | ||||||
|  |         log.debug('Setting parent', node, clusterId); | ||||||
|  |         newGraph.setParent(node, clusterId); | ||||||
|  |       } else { | ||||||
|  |         log.info('In copy ', clusterId, 'root', rootId, 'data', graph.node(clusterId), rootId); | ||||||
|  |         log.debug( | ||||||
|  |           'Not Setting parent for node=', | ||||||
|  |           node, | ||||||
|  |           'cluster!==rootId', | ||||||
|  |           clusterId !== rootId, | ||||||
|  |           'node!==clusterId', | ||||||
|  |           node !== clusterId | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       const edges = graph.edges(node); | ||||||
|  |       log.debug('Copying Edges', edges); | ||||||
|  |       edges.forEach((edge) => { | ||||||
|  |         log.info('Edge', edge); | ||||||
|  |         const data = graph.edge(edge.v, edge.w, edge.name); | ||||||
|  |         log.info('Edge data', data, rootId); | ||||||
|  |         try { | ||||||
|  |           // Do not copy edges in and out of the root cluster, they belong to the parent graph | ||||||
|  |           if (edgeInCluster(edge, rootId)) { | ||||||
|  |             log.info('Copying as ', edge.v, edge.w, data, edge.name); | ||||||
|  |             newGraph.setEdge(edge.v, edge.w, data, edge.name); | ||||||
|  |             log.info('newGraph edges ', newGraph.edges(), newGraph.edge(newGraph.edges()[0])); | ||||||
|  |           } else { | ||||||
|  |             log.info( | ||||||
|  |               'Skipping copy of edge ', | ||||||
|  |               edge.v, | ||||||
|  |               '-->', | ||||||
|  |               edge.w, | ||||||
|  |               ' rootId: ', | ||||||
|  |               rootId, | ||||||
|  |               ' clusterId:', | ||||||
|  |               clusterId | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |         } catch (e) { | ||||||
|  |           log.error(e); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     log.debug('Removing node', node); | ||||||
|  |     graph.removeNode(node); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | export const extractDescendants = (id, graph) => { | ||||||
|  |   // log.debug('Extracting ', id); | ||||||
|  |   const children = graph.children(id); | ||||||
|  |   let res = [...children]; | ||||||
|  |  | ||||||
|  |   for (const child of children) { | ||||||
|  |     parents[child] = id; | ||||||
|  |     res = [...res, ...extractDescendants(child, graph)]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return res; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Validates the graph, checking that all parent child relation points to existing nodes and that | ||||||
|  |  * edges between nodes also ia correct. When not correct the function logs the discrepancies. | ||||||
|  |  * | ||||||
|  |  * @param graph | ||||||
|  |  */ | ||||||
|  | export const validate = (graph) => { | ||||||
|  |   const edges = graph.edges(); | ||||||
|  |   log.trace('Edges: ', edges); | ||||||
|  |   for (const edge of edges) { | ||||||
|  |     if (graph.children(edge.v).length > 0) { | ||||||
|  |       log.trace('The node ', edge.v, ' is part of and edge even though it has children'); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if (graph.children(edge.w).length > 0) { | ||||||
|  |       log.trace('The node ', edge.w, ' is part of and edge even though it has children'); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Finds a child that is not a cluster. When faking an edge between a node and a cluster. | ||||||
|  |  * | ||||||
|  |  * @param id | ||||||
|  |  * @param {any} graph | ||||||
|  |  */ | ||||||
|  | export const findNonClusterChild = (id, graph) => { | ||||||
|  |   // const node = graph.node(id); | ||||||
|  |   log.trace('Searching', id); | ||||||
|  |   // const children = graph.children(id).reverse(); | ||||||
|  |   const children = graph.children(id); //.reverse(); | ||||||
|  |   log.trace('Searching children of id ', id, children); | ||||||
|  |   if (children.length < 1) { | ||||||
|  |     log.trace('This is a valid node', id); | ||||||
|  |     return id; | ||||||
|  |   } | ||||||
|  |   for (const child of children) { | ||||||
|  |     const _id = findNonClusterChild(child, graph); | ||||||
|  |     if (_id) { | ||||||
|  |       log.trace('Found replacement for', id, ' => ', _id); | ||||||
|  |       return _id; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getAnchorId = (id) => { | ||||||
|  |   if (!clusterDb[id]) { | ||||||
|  |     return id; | ||||||
|  |   } | ||||||
|  |   // If the cluster has no external connections | ||||||
|  |   if (!clusterDb[id].externalConnections) { | ||||||
|  |     return id; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Return the replacement node | ||||||
|  |   if (clusterDb[id]) { | ||||||
|  |     return clusterDb[id].id; | ||||||
|  |   } | ||||||
|  |   return id; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const adjustClustersAndEdges = (graph, depth) => { | ||||||
|  |   if (!graph || depth > 10) { | ||||||
|  |     log.debug('Opting out, no graph '); | ||||||
|  |     return; | ||||||
|  |   } else { | ||||||
|  |     log.debug('Opting in, graph '); | ||||||
|  |   } | ||||||
|  |   // Go through the nodes and for each cluster found, save a replacement node, this can be used when | ||||||
|  |   // faking a link to a cluster | ||||||
|  |   graph.nodes().forEach(function (id) { | ||||||
|  |     const children = graph.children(id); | ||||||
|  |     if (children.length > 0) { | ||||||
|  |       log.warn( | ||||||
|  |         'Cluster identified', | ||||||
|  |         id, | ||||||
|  |         ' Replacement id in edges: ', | ||||||
|  |         findNonClusterChild(id, graph) | ||||||
|  |       ); | ||||||
|  |       descendants[id] = extractDescendants(id, graph); | ||||||
|  |       clusterDb[id] = { id: findNonClusterChild(id, graph), clusterData: graph.node(id) }; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Check incoming and outgoing edges for each cluster | ||||||
|  |   graph.nodes().forEach(function (id) { | ||||||
|  |     const children = graph.children(id); | ||||||
|  |     const edges = graph.edges(); | ||||||
|  |     if (children.length > 0) { | ||||||
|  |       log.debug('Cluster identified', id, descendants); | ||||||
|  |       edges.forEach((edge) => { | ||||||
|  |         // log.debug('Edge, descendants: ', edge, descendants[id]); | ||||||
|  |  | ||||||
|  |         // Check if any edge leaves the cluster (not the actual cluster, that's a link from the box) | ||||||
|  |         if (edge.v !== id && edge.w !== id) { | ||||||
|  |           // Any edge where either the one of the nodes is descending to the cluster but not the other | ||||||
|  |           // if (descendants[id].indexOf(edge.v) < 0 && descendants[id].indexOf(edge.w) < 0) { | ||||||
|  |  | ||||||
|  |           const d1 = isDescendant(edge.v, id); | ||||||
|  |           const d2 = isDescendant(edge.w, id); | ||||||
|  |  | ||||||
|  |           // d1 xor d2 - if either d1 is true and d2 is false or the other way around | ||||||
|  |           if (d1 ^ d2) { | ||||||
|  |             log.warn('Edge: ', edge, ' leaves cluster ', id); | ||||||
|  |             log.warn('Decendants of XXX ', id, ': ', descendants[id]); | ||||||
|  |             clusterDb[id].externalConnections = true; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       log.debug('Not a cluster ', id, descendants); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   for (let id of Object.keys(clusterDb)) { | ||||||
|  |     const nonClusterChild = clusterDb[id].id; | ||||||
|  |     const parent = graph.parent(nonClusterChild); | ||||||
|  |  | ||||||
|  |     // Change replacement node of id to parent of current replacement node if valid | ||||||
|  |     if (parent !== id && clusterDb[parent] && !clusterDb[parent].externalConnections) { | ||||||
|  |       clusterDb[id].id = parent; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // For clusters with incoming and/or outgoing edges translate those edges to a real node | ||||||
|  |   // in the cluster in order to fake the edge | ||||||
|  |   graph.edges().forEach(function (e) { | ||||||
|  |     const edge = graph.edge(e); | ||||||
|  |     log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); | ||||||
|  |     log.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e))); | ||||||
|  |  | ||||||
|  |     let v = e.v; | ||||||
|  |     let w = e.w; | ||||||
|  |     // Check if link is either from or to a cluster | ||||||
|  |     log.warn( | ||||||
|  |       'Fix XXX', | ||||||
|  |       clusterDb, | ||||||
|  |       'ids:', | ||||||
|  |       e.v, | ||||||
|  |       e.w, | ||||||
|  |       'Translating: ', | ||||||
|  |       clusterDb[e.v], | ||||||
|  |       ' --- ', | ||||||
|  |       clusterDb[e.w] | ||||||
|  |     ); | ||||||
|  |     if (clusterDb[e.v] && clusterDb[e.w] && clusterDb[e.v] === clusterDb[e.w]) { | ||||||
|  |       log.warn('Fixing and trixing link to self - removing XXX', e.v, e.w, e.name); | ||||||
|  |       log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name); | ||||||
|  |       v = getAnchorId(e.v); | ||||||
|  |       w = getAnchorId(e.w); | ||||||
|  |       graph.removeEdge(e.v, e.w, e.name); | ||||||
|  |       const specialId = e.w + '---' + e.v; | ||||||
|  |       graph.setNode(specialId, { | ||||||
|  |         domId: specialId, | ||||||
|  |         id: specialId, | ||||||
|  |         labelStyle: '', | ||||||
|  |         labelText: edge.label, | ||||||
|  |         padding: 0, | ||||||
|  |         shape: 'labelRect', | ||||||
|  |         style: '', | ||||||
|  |       }); | ||||||
|  |       const edge1 = structuredClone(edge); | ||||||
|  |       const edge2 = structuredClone(edge); | ||||||
|  |       edge1.label = ''; | ||||||
|  |       edge1.arrowTypeEnd = 'none'; | ||||||
|  |       edge2.label = ''; | ||||||
|  |       edge1.fromCluster = e.v; | ||||||
|  |       edge2.toCluster = e.v; | ||||||
|  |  | ||||||
|  |       graph.setEdge(v, specialId, edge1, e.name + '-cyclic-special'); | ||||||
|  |       graph.setEdge(specialId, w, edge2, e.name + '-cyclic-special'); | ||||||
|  |     } else if (clusterDb[e.v] || clusterDb[e.w]) { | ||||||
|  |       log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name); | ||||||
|  |       v = getAnchorId(e.v); | ||||||
|  |       w = getAnchorId(e.w); | ||||||
|  |       graph.removeEdge(e.v, e.w, e.name); | ||||||
|  |       if (v !== e.v) { | ||||||
|  |         const parent = graph.parent(v); | ||||||
|  |         clusterDb[parent].externalConnections = true; | ||||||
|  |         edge.fromCluster = e.v; | ||||||
|  |       } | ||||||
|  |       if (w !== e.w) { | ||||||
|  |         const parent = graph.parent(w); | ||||||
|  |         clusterDb[parent].externalConnections = true; | ||||||
|  |         edge.toCluster = e.w; | ||||||
|  |       } | ||||||
|  |       log.warn('Fix Replacing with XXX', v, w, e.name); | ||||||
|  |       graph.setEdge(v, w, edge, e.name); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   log.warn('Adjusted Graph', graphlibJson.write(graph)); | ||||||
|  |   extractor(graph, 0); | ||||||
|  |  | ||||||
|  |   log.trace(clusterDb); | ||||||
|  |  | ||||||
|  |   // Remove references to extracted cluster | ||||||
|  |   // graph.edges().forEach(edge => { | ||||||
|  |   //   if (isDecendant(edge.v, clusterId) || isDecendant(edge.w, clusterId)) { | ||||||
|  |   //     graph.removeEdge(edge); | ||||||
|  |   //   } | ||||||
|  |   // }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const extractor = (graph, depth) => { | ||||||
|  |   log.warn('extractor - ', depth, graphlibJson.write(graph), graph.children('D')); | ||||||
|  |   if (depth > 10) { | ||||||
|  |     log.error('Bailing out'); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   // For clusters without incoming and/or outgoing edges, create a new cluster-node | ||||||
|  |   // containing the nodes and edges in the custer in a new graph | ||||||
|  |   // for (let i = 0;) | ||||||
|  |   let nodes = graph.nodes(); | ||||||
|  |   let hasChildren = false; | ||||||
|  |   for (const node of nodes) { | ||||||
|  |     const children = graph.children(node); | ||||||
|  |     hasChildren = hasChildren || children.length > 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!hasChildren) { | ||||||
|  |     log.debug('Done, no node has children', graph.nodes()); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   // const clusters = Object.keys(clusterDb); | ||||||
|  |   // clusters.forEach(clusterId => { | ||||||
|  |   log.debug('Nodes = ', nodes, depth); | ||||||
|  |   for (const node of nodes) { | ||||||
|  |     log.debug( | ||||||
|  |       'Extracting node', | ||||||
|  |       node, | ||||||
|  |       clusterDb, | ||||||
|  |       clusterDb[node] && !clusterDb[node].externalConnections, | ||||||
|  |       !graph.parent(node), | ||||||
|  |       graph.node(node), | ||||||
|  |       graph.children('D'), | ||||||
|  |       ' Depth ', | ||||||
|  |       depth | ||||||
|  |     ); | ||||||
|  |     // Note that the node might have been removed after the Object.keys call so better check | ||||||
|  |     // that it still is in the game | ||||||
|  |     if (!clusterDb[node]) { | ||||||
|  |       // Skip if the node is not a cluster | ||||||
|  |       log.debug('Not a cluster', node, depth); | ||||||
|  |       // break; | ||||||
|  |     } else if ( | ||||||
|  |       !clusterDb[node].externalConnections && | ||||||
|  |       // !graph.parent(node) && | ||||||
|  |       graph.children(node) && | ||||||
|  |       graph.children(node).length > 0 | ||||||
|  |     ) { | ||||||
|  |       log.warn( | ||||||
|  |         'Cluster without external connections, without a parent and with children', | ||||||
|  |         node, | ||||||
|  |         depth | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       const graphSettings = graph.graph(); | ||||||
|  |       let dir = graphSettings.rankdir === 'TB' ? 'LR' : 'TB'; | ||||||
|  |       if (clusterDb[node] && clusterDb[node].clusterData && clusterDb[node].clusterData.dir) { | ||||||
|  |         dir = clusterDb[node].clusterData.dir; | ||||||
|  |         log.warn('Fixing dir', clusterDb[node].clusterData.dir, dir); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const clusterGraph = new graphlib.Graph({ | ||||||
|  |         multigraph: true, | ||||||
|  |         compound: true, | ||||||
|  |       }) | ||||||
|  |         .setGraph({ | ||||||
|  |           rankdir: dir, // Todo: set proper spacing | ||||||
|  |           nodesep: 50, | ||||||
|  |           ranksep: 50, | ||||||
|  |           marginx: 8, | ||||||
|  |           marginy: 8, | ||||||
|  |         }) | ||||||
|  |         .setDefaultEdgeLabel(function () { | ||||||
|  |           return {}; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |       log.warn('Old graph before copy', graphlibJson.write(graph)); | ||||||
|  |       copy(node, graph, clusterGraph, node); | ||||||
|  |       graph.setNode(node, { | ||||||
|  |         clusterNode: true, | ||||||
|  |         id: node, | ||||||
|  |         clusterData: clusterDb[node].clusterData, | ||||||
|  |         labelText: clusterDb[node].labelText, | ||||||
|  |         graph: clusterGraph, | ||||||
|  |       }); | ||||||
|  |       log.warn('New graph after copy node: (', node, ')', graphlibJson.write(clusterGraph)); | ||||||
|  |       log.debug('Old graph after copy', graphlibJson.write(graph)); | ||||||
|  |     } else { | ||||||
|  |       log.warn( | ||||||
|  |         'Cluster ** ', | ||||||
|  |         node, | ||||||
|  |         ' **not meeting the criteria !externalConnections:', | ||||||
|  |         !clusterDb[node].externalConnections, | ||||||
|  |         ' no parent: ', | ||||||
|  |         !graph.parent(node), | ||||||
|  |         ' children ', | ||||||
|  |         graph.children(node) && graph.children(node).length > 0, | ||||||
|  |         graph.children('D'), | ||||||
|  |         depth | ||||||
|  |       ); | ||||||
|  |       log.debug(clusterDb); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   nodes = graph.nodes(); | ||||||
|  |   log.warn('New list of nodes', nodes); | ||||||
|  |   for (const node of nodes) { | ||||||
|  |     const data = graph.node(node); | ||||||
|  |     log.warn(' Now next level', node, data); | ||||||
|  |     if (data.clusterNode) { | ||||||
|  |       extractor(data.graph, depth + 1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const sorter = (graph, nodes) => { | ||||||
|  |   if (nodes.length === 0) { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  |   let result = Object.assign(nodes); | ||||||
|  |   nodes.forEach((node) => { | ||||||
|  |     const children = graph.children(node); | ||||||
|  |     const sorted = sorter(graph, children); | ||||||
|  |     result = [...result, ...sorted]; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return result; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const sortNodesByHierarchy = (graph) => sorter(graph, graph.children()); | ||||||
| @@ -0,0 +1,508 @@ | |||||||
|  | import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js'; | ||||||
|  | import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; | ||||||
|  | import { | ||||||
|  |   validate, | ||||||
|  |   adjustClustersAndEdges, | ||||||
|  |   extractDescendants, | ||||||
|  |   sortNodesByHierarchy, | ||||||
|  | } from './mermaid-graphlib.js'; | ||||||
|  | import { setLogLevel, log } from '../logger.js'; | ||||||
|  |  | ||||||
|  | describe('Graphlib decorations', () => { | ||||||
|  |   let g; | ||||||
|  |   beforeEach(function () { | ||||||
|  |     setLogLevel(1); | ||||||
|  |     g = new graphlib.Graph({ | ||||||
|  |       multigraph: true, | ||||||
|  |       compound: true, | ||||||
|  |     }); | ||||||
|  |     g.setGraph({ | ||||||
|  |       rankdir: 'TB', | ||||||
|  |       nodesep: 10, | ||||||
|  |       ranksep: 10, | ||||||
|  |       marginx: 8, | ||||||
|  |       marginy: 8, | ||||||
|  |     }); | ||||||
|  |     g.setDefaultEdgeLabel(function () { | ||||||
|  |       return {}; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('validate', function () { | ||||||
|  |     it('Validate should detect edges between clusters', function () { | ||||||
|  |       /* | ||||||
|  |         subgraph C1 | ||||||
|  |           a --> b | ||||||
|  |         end | ||||||
|  |         subgraph C2 | ||||||
|  |           c | ||||||
|  |         end | ||||||
|  |         C1 --> C2 | ||||||
|  |       */ | ||||||
|  |       g.setNode('a', { data: 1 }); | ||||||
|  |       g.setNode('b', { data: 2 }); | ||||||
|  |       g.setNode('c', { data: 3 }); | ||||||
|  |       g.setParent('a', 'C1'); | ||||||
|  |       g.setParent('b', 'C1'); | ||||||
|  |       g.setParent('c', 'C2'); | ||||||
|  |       g.setEdge('a', 'b'); | ||||||
|  |       g.setEdge('C1', 'C2'); | ||||||
|  |  | ||||||
|  |       expect(validate(g)).toBe(false); | ||||||
|  |     }); | ||||||
|  |     it('Validate should not detect edges between clusters after adjustment', function () { | ||||||
|  |       /* | ||||||
|  |         subgraph C1 | ||||||
|  |           a --> b | ||||||
|  |         end | ||||||
|  |         subgraph C2 | ||||||
|  |           c | ||||||
|  |         end | ||||||
|  |         C1 --> C2 | ||||||
|  |       */ | ||||||
|  |       g.setNode('a', {}); | ||||||
|  |       g.setNode('b', {}); | ||||||
|  |       g.setNode('c', {}); | ||||||
|  |       g.setParent('a', 'C1'); | ||||||
|  |       g.setParent('b', 'C1'); | ||||||
|  |       g.setParent('c', 'C2'); | ||||||
|  |       g.setEdge('a', 'b'); | ||||||
|  |       g.setEdge('C1', 'C2'); | ||||||
|  |  | ||||||
|  |       adjustClustersAndEdges(g); | ||||||
|  |       log.info(g.edges()); | ||||||
|  |       expect(validate(g)).toBe(true); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('Validate should detect edges between clusters and transform clusters GLB4', function () { | ||||||
|  |       /* | ||||||
|  |         a --> b | ||||||
|  |         subgraph C1 | ||||||
|  |           subgraph C2 | ||||||
|  |             a | ||||||
|  |           end | ||||||
|  |           b | ||||||
|  |         end | ||||||
|  |         C1 --> c | ||||||
|  |       */ | ||||||
|  |       g.setNode('a', { data: 1 }); | ||||||
|  |       g.setNode('b', { data: 2 }); | ||||||
|  |       g.setNode('c', { data: 3 }); | ||||||
|  |       g.setNode('C1', { data: 4 }); | ||||||
|  |       g.setNode('C2', { data: 5 }); | ||||||
|  |       g.setParent('a', 'C2'); | ||||||
|  |       g.setParent('b', 'C1'); | ||||||
|  |       g.setParent('C2', 'C1'); | ||||||
|  |       g.setEdge('a', 'b', { name: 'C1-internal-link' }); | ||||||
|  |       g.setEdge('C1', 'c', { name: 'C1-external-link' }); | ||||||
|  |  | ||||||
|  |       adjustClustersAndEdges(g); | ||||||
|  |       log.info(g.nodes()); | ||||||
|  |       expect(g.nodes().length).toBe(2); | ||||||
|  |       expect(validate(g)).toBe(true); | ||||||
|  |     }); | ||||||
|  |     it('Validate should detect edges between clusters and transform clusters GLB5', function () { | ||||||
|  |       /* | ||||||
|  |         a --> b | ||||||
|  |         subgraph C1 | ||||||
|  |           a | ||||||
|  |         end | ||||||
|  |         subgraph C2 | ||||||
|  |           b | ||||||
|  |         end | ||||||
|  |         C1 --> | ||||||
|  |       */ | ||||||
|  |       g.setNode('a', { data: 1 }); | ||||||
|  |       g.setNode('b', { data: 2 }); | ||||||
|  |       g.setParent('a', 'C1'); | ||||||
|  |       g.setParent('b', 'C2'); | ||||||
|  |       // g.setEdge('a', 'b', { name: 'C1-internal-link' }); | ||||||
|  |       g.setEdge('C1', 'C2', { name: 'C1-external-link' }); | ||||||
|  |  | ||||||
|  |       log.info(g.nodes()); | ||||||
|  |       adjustClustersAndEdges(g); | ||||||
|  |       log.info(g.nodes()); | ||||||
|  |       expect(g.nodes().length).toBe(2); | ||||||
|  |       expect(validate(g)).toBe(true); | ||||||
|  |     }); | ||||||
|  |     it('adjustClustersAndEdges GLB6', function () { | ||||||
|  |       /* | ||||||
|  |       subgraph C1 | ||||||
|  |         a | ||||||
|  |       end | ||||||
|  |       C1 --> b | ||||||
|  |     */ | ||||||
|  |       g.setNode('a', { data: 1 }); | ||||||
|  |       g.setNode('b', { data: 2 }); | ||||||
|  |       g.setNode('C1', { data: 3 }); | ||||||
|  |       g.setParent('a', 'C1'); | ||||||
|  |       g.setEdge('C1', 'b', { data: 'link1' }, '1'); | ||||||
|  |  | ||||||
|  |       // log.info(g.edges()) | ||||||
|  |       adjustClustersAndEdges(g); | ||||||
|  |       log.info(g.edges()); | ||||||
|  |       expect(g.nodes()).toEqual(['b', 'C1']); | ||||||
|  |       expect(g.edges().length).toBe(1); | ||||||
|  |       expect(validate(g)).toBe(true); | ||||||
|  |       expect(g.node('C1').clusterNode).toBe(true); | ||||||
|  |  | ||||||
|  |       const C1Graph = g.node('C1').graph; | ||||||
|  |       expect(C1Graph.nodes()).toEqual(['a']); | ||||||
|  |     }); | ||||||
|  |     it('adjustClustersAndEdges GLB7', function () { | ||||||
|  |       /* | ||||||
|  |       subgraph C1 | ||||||
|  |         a | ||||||
|  |       end | ||||||
|  |       C1 --> b | ||||||
|  |       C1 --> c | ||||||
|  |     */ | ||||||
|  |       g.setNode('a', { data: 1 }); | ||||||
|  |       g.setNode('b', { data: 2 }); | ||||||
|  |       g.setNode('c', { data: 3 }); | ||||||
|  |       g.setParent('a', 'C1'); | ||||||
|  |       g.setNode('C1', { data: 4 }); | ||||||
|  |       g.setEdge('C1', 'b', { data: 'link1' }, '1'); | ||||||
|  |       g.setEdge('C1', 'c', { data: 'link2' }, '2'); | ||||||
|  |  | ||||||
|  |       log.info(g.node('C1')); | ||||||
|  |       adjustClustersAndEdges(g); | ||||||
|  |       log.info(g.edges()); | ||||||
|  |       expect(g.nodes()).toEqual(['b', 'c', 'C1']); | ||||||
|  |       expect(g.nodes().length).toBe(3); | ||||||
|  |       expect(g.edges().length).toBe(2); | ||||||
|  |  | ||||||
|  |       expect(g.edges().length).toBe(2); | ||||||
|  |       const edgeData = g.edge(g.edges()[1]); | ||||||
|  |       expect(edgeData.data).toBe('link2'); | ||||||
|  |       expect(validate(g)).toBe(true); | ||||||
|  |  | ||||||
|  |       const C1Graph = g.node('C1').graph; | ||||||
|  |       expect(C1Graph.nodes()).toEqual(['a']); | ||||||
|  |     }); | ||||||
|  |     it('adjustClustersAndEdges GLB8', function () { | ||||||
|  |       /* | ||||||
|  |     subgraph A | ||||||
|  |       a | ||||||
|  |     end | ||||||
|  |     subgraph B | ||||||
|  |       b | ||||||
|  |     end | ||||||
|  |     subgraph C | ||||||
|  |       c | ||||||
|  |     end | ||||||
|  |     A --> B | ||||||
|  |     A --> C | ||||||
|  |     */ | ||||||
|  |       g.setNode('a', { data: 1 }); | ||||||
|  |       g.setNode('b', { data: 2 }); | ||||||
|  |       g.setNode('c', { data: 3 }); | ||||||
|  |       g.setParent('a', 'A'); | ||||||
|  |       g.setParent('b', 'B'); | ||||||
|  |       g.setParent('c', 'C'); | ||||||
|  |       g.setEdge('A', 'B', { data: 'link1' }, '1'); | ||||||
|  |       g.setEdge('A', 'C', { data: 'link2' }, '2'); | ||||||
|  |  | ||||||
|  |       // log.info(g.edges()) | ||||||
|  |       adjustClustersAndEdges(g); | ||||||
|  |       expect(g.nodes()).toEqual(['A', 'B', 'C']); | ||||||
|  |       expect(g.edges().length).toBe(2); | ||||||
|  |  | ||||||
|  |       expect(g.edges().length).toBe(2); | ||||||
|  |       const edgeData = g.edge(g.edges()[1]); | ||||||
|  |       expect(edgeData.data).toBe('link2'); | ||||||
|  |       expect(validate(g)).toBe(true); | ||||||
|  |  | ||||||
|  |       const CGraph = g.node('C').graph; | ||||||
|  |       expect(CGraph.nodes()).toEqual(['c']); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB10', function () { | ||||||
|  |       /* | ||||||
|  |     subgraph C | ||||||
|  |       subgraph D | ||||||
|  |         d | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |     */ | ||||||
|  |  | ||||||
|  |       g.setNode('C', { data: 1 }); | ||||||
|  |       g.setNode('D', { data: 2 }); | ||||||
|  |       g.setNode('d', { data: 3 }); | ||||||
|  |       g.setParent('d', 'D'); | ||||||
|  |       g.setParent('D', 'C'); | ||||||
|  |  | ||||||
|  |       // log.info('Graph before', g.node('D')) | ||||||
|  |       // log.info('Graph before', graphlibJson.write(g)) | ||||||
|  |       adjustClustersAndEdges(g); | ||||||
|  |       // log.info('Graph after', graphlibJson.write(g), g.node('C').graph) | ||||||
|  |  | ||||||
|  |       const CGraph = g.node('C').graph; | ||||||
|  |       const DGraph = CGraph.node('D').graph; | ||||||
|  |  | ||||||
|  |       expect(CGraph.nodes()).toEqual(['D']); | ||||||
|  |       expect(DGraph.nodes()).toEqual(['d']); | ||||||
|  |  | ||||||
|  |       expect(g.nodes()).toEqual(['C']); | ||||||
|  |       expect(g.nodes().length).toBe(1); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('adjustClustersAndEdges the extracted graphs shall contain the correct data GLB11', function () { | ||||||
|  |       /* | ||||||
|  |     subgraph A | ||||||
|  |       a | ||||||
|  |     end | ||||||
|  |     subgraph B | ||||||
|  |       b | ||||||
|  |     end | ||||||
|  |     subgraph C | ||||||
|  |       subgraph D | ||||||
|  |         d | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |     A --> B | ||||||
|  |     A --> C | ||||||
|  |     */ | ||||||
|  |  | ||||||
|  |       g.setNode('C', { data: 1 }); | ||||||
|  |       g.setNode('D', { data: 2 }); | ||||||
|  |       g.setNode('d', { data: 3 }); | ||||||
|  |       g.setNode('B', { data: 4 }); | ||||||
|  |       g.setNode('b', { data: 5 }); | ||||||
|  |       g.setNode('A', { data: 6 }); | ||||||
|  |       g.setNode('a', { data: 7 }); | ||||||
|  |       g.setParent('a', 'A'); | ||||||
|  |       g.setParent('b', 'B'); | ||||||
|  |       g.setParent('d', 'D'); | ||||||
|  |       g.setParent('D', 'C'); | ||||||
|  |       g.setEdge('A', 'B', { data: 'link1' }, '1'); | ||||||
|  |       g.setEdge('A', 'C', { data: 'link2' }, '2'); | ||||||
|  |  | ||||||
|  |       log.info('Graph before', g.node('D')); | ||||||
|  |       log.info('Graph before', graphlibJson.write(g)); | ||||||
|  |       adjustClustersAndEdges(g); | ||||||
|  |       log.trace('Graph after', graphlibJson.write(g)); | ||||||
|  |       expect(g.nodes()).toEqual(['C', 'B', 'A']); | ||||||
|  |       expect(g.nodes().length).toBe(3); | ||||||
|  |       expect(g.edges().length).toBe(2); | ||||||
|  |  | ||||||
|  |       const AGraph = g.node('A').graph; | ||||||
|  |       const BGraph = g.node('B').graph; | ||||||
|  |       const CGraph = g.node('C').graph; | ||||||
|  |       // log.info(CGraph.nodes()); | ||||||
|  |       const DGraph = CGraph.node('D').graph; | ||||||
|  |       // log.info('DG', CGraph.children('D')); | ||||||
|  |  | ||||||
|  |       log.info('A', AGraph.nodes()); | ||||||
|  |       expect(AGraph.nodes().length).toBe(1); | ||||||
|  |       expect(AGraph.nodes()).toEqual(['a']); | ||||||
|  |       log.trace('Nodes', BGraph.nodes()); | ||||||
|  |       expect(BGraph.nodes().length).toBe(1); | ||||||
|  |       expect(BGraph.nodes()).toEqual(['b']); | ||||||
|  |       expect(CGraph.nodes()).toEqual(['D']); | ||||||
|  |       expect(CGraph.nodes().length).toEqual(1); | ||||||
|  |  | ||||||
|  |       expect(AGraph.edges().length).toBe(0); | ||||||
|  |       expect(BGraph.edges().length).toBe(0); | ||||||
|  |       expect(CGraph.edges().length).toBe(0); | ||||||
|  |       expect(DGraph.nodes()).toEqual(['d']); | ||||||
|  |       expect(DGraph.edges().length).toBe(0); | ||||||
|  |       // expect(CGraph.node('D')).toEqual({ data: 2 }); | ||||||
|  |       expect(g.edges().length).toBe(2); | ||||||
|  |  | ||||||
|  |       // expect(g.edges().length).toBe(2); | ||||||
|  |       // const edgeData = g.edge(g.edges()[1]); | ||||||
|  |       // expect(edgeData.data).toBe('link2'); | ||||||
|  |       // expect(validate(g)).toBe(true); | ||||||
|  |     }); | ||||||
|  |     it('adjustClustersAndEdges the extracted graphs shall contain the correct links  GLB20', function () { | ||||||
|  |       /* | ||||||
|  |       a --> b | ||||||
|  |       subgraph b [Test] | ||||||
|  |         c --> d -->e | ||||||
|  |       end | ||||||
|  |     */ | ||||||
|  |       g.setNode('a', { data: 1 }); | ||||||
|  |       g.setNode('b', { data: 2 }); | ||||||
|  |       g.setNode('c', { data: 3 }); | ||||||
|  |       g.setNode('d', { data: 3 }); | ||||||
|  |       g.setNode('e', { data: 3 }); | ||||||
|  |       g.setParent('c', 'b'); | ||||||
|  |       g.setParent('d', 'b'); | ||||||
|  |       g.setParent('e', 'b'); | ||||||
|  |       g.setEdge('a', 'b', { data: 'link1' }, '1'); | ||||||
|  |       g.setEdge('c', 'd', { data: 'link2' }, '2'); | ||||||
|  |       g.setEdge('d', 'e', { data: 'link2' }, '2'); | ||||||
|  |  | ||||||
|  |       log.info('Graph before', graphlibJson.write(g)); | ||||||
|  |       adjustClustersAndEdges(g); | ||||||
|  |       const bGraph = g.node('b').graph; | ||||||
|  |       // log.trace('Graph after', graphlibJson.write(g)) | ||||||
|  |       log.info('Graph after', graphlibJson.write(bGraph)); | ||||||
|  |       expect(bGraph.nodes().length).toBe(3); | ||||||
|  |       expect(bGraph.edges().length).toBe(2); | ||||||
|  |     }); | ||||||
|  |     it('adjustClustersAndEdges the extracted graphs shall contain the correct links  GLB21', function () { | ||||||
|  |       /* | ||||||
|  |     state a { | ||||||
|  |         state b { | ||||||
|  |             state c { | ||||||
|  |                 e | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     */ | ||||||
|  |       g.setNode('a', { data: 1 }); | ||||||
|  |       g.setNode('b', { data: 2 }); | ||||||
|  |       g.setNode('c', { data: 3 }); | ||||||
|  |       g.setNode('e', { data: 3 }); | ||||||
|  |       g.setParent('b', 'a'); | ||||||
|  |       g.setParent('c', 'b'); | ||||||
|  |       g.setParent('e', 'c'); | ||||||
|  |  | ||||||
|  |       log.info('Graph before', graphlibJson.write(g)); | ||||||
|  |       adjustClustersAndEdges(g); | ||||||
|  |       const aGraph = g.node('a').graph; | ||||||
|  |       const bGraph = aGraph.node('b').graph; | ||||||
|  |       log.info('Graph after', graphlibJson.write(aGraph)); | ||||||
|  |       const cGraph = bGraph.node('c').graph; | ||||||
|  |       // log.trace('Graph after', graphlibJson.write(g)) | ||||||
|  |       expect(aGraph.nodes().length).toBe(1); | ||||||
|  |       expect(bGraph.nodes().length).toBe(1); | ||||||
|  |       expect(cGraph.nodes().length).toBe(1); | ||||||
|  |       expect(bGraph.edges().length).toBe(0); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |   it('adjustClustersAndEdges should handle nesting GLB77', function () { | ||||||
|  |     /* | ||||||
|  | flowchart TB | ||||||
|  |   subgraph A | ||||||
|  |     b-->B | ||||||
|  |     a-->c | ||||||
|  |   end | ||||||
|  |   subgraph B | ||||||
|  |     c | ||||||
|  |   end | ||||||
|  |     */ | ||||||
|  |  | ||||||
|  |     const exportedGraph = JSON.parse( | ||||||
|  |       '{"options":{"directed":true,"multigraph":true,"compound":true},"nodes":[{"v":"A","value":{"labelStyle":"","shape":"rect","labelText":"A","rx":0,"ry":0,"class":"default","style":"","id":"A","width":500,"type":"group","padding":15}},{"v":"B","value":{"labelStyle":"","shape":"rect","labelText":"B","rx":0,"ry":0,"class":"default","style":"","id":"B","width":500,"type":"group","padding":15},"parent":"A"},{"v":"b","value":{"labelStyle":"","shape":"rect","labelText":"b","rx":0,"ry":0,"class":"default","style":"","id":"b","padding":15},"parent":"A"},{"v":"c","value":{"labelStyle":"","shape":"rect","labelText":"c","rx":0,"ry":0,"class":"default","style":"","id":"c","padding":15},"parent":"B"},{"v":"a","value":{"labelStyle":"","shape":"rect","labelText":"a","rx":0,"ry":0,"class":"default","style":"","id":"a","padding":15},"parent":"A"}],"edges":[{"v":"b","w":"B","name":"1","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-b-B","classes":"flowchart-link LS-b LE-B"}},{"v":"a","w":"c","name":"2","value":{"minlen":1,"arrowhead":"normal","arrowTypeStart":"arrow_open","arrowTypeEnd":"arrow_point","thickness":"normal","pattern":"solid","style":"fill:none","labelStyle":"","arrowheadStyle":"fill: #333","labelpos":"c","labelType":"text","label":"","id":"L-a-c","classes":"flowchart-link LS-a LE-c"}}],"value":{"rankdir":"TB","nodesep":50,"ranksep":50,"marginx":8,"marginy":8}}' | ||||||
|  |     ); | ||||||
|  |     const gr = graphlibJson.read(exportedGraph); | ||||||
|  |  | ||||||
|  |     log.info('Graph before', graphlibJson.write(gr)); | ||||||
|  |     adjustClustersAndEdges(gr); | ||||||
|  |     const aGraph = gr.node('A').graph; | ||||||
|  |     const bGraph = aGraph.node('B').graph; | ||||||
|  |     log.info('Graph after', graphlibJson.write(aGraph)); | ||||||
|  |     // log.trace('Graph after', graphlibJson.write(g)) | ||||||
|  |     expect(aGraph.parent('c')).toBe('B'); | ||||||
|  |     expect(aGraph.parent('B')).toBe(undefined); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | describe('extractDescendants', function () { | ||||||
|  |   let g; | ||||||
|  |   beforeEach(function () { | ||||||
|  |     setLogLevel(1); | ||||||
|  |     g = new graphlib.Graph({ | ||||||
|  |       multigraph: true, | ||||||
|  |       compound: true, | ||||||
|  |     }); | ||||||
|  |     g.setGraph({ | ||||||
|  |       rankdir: 'TB', | ||||||
|  |       nodesep: 10, | ||||||
|  |       ranksep: 10, | ||||||
|  |       marginx: 8, | ||||||
|  |       marginy: 8, | ||||||
|  |     }); | ||||||
|  |     g.setDefaultEdgeLabel(function () { | ||||||
|  |       return {}; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |   it('Simple case of one level descendants GLB9', function () { | ||||||
|  |     /* | ||||||
|  |     subgraph A | ||||||
|  |       a | ||||||
|  |     end | ||||||
|  |     subgraph B | ||||||
|  |       b | ||||||
|  |     end | ||||||
|  |     subgraph C | ||||||
|  |       c | ||||||
|  |     end | ||||||
|  |     A --> B | ||||||
|  |     A --> C | ||||||
|  |     */ | ||||||
|  |     g.setNode('a', { data: 1 }); | ||||||
|  |     g.setNode('b', { data: 2 }); | ||||||
|  |     g.setNode('c', { data: 3 }); | ||||||
|  |     g.setParent('a', 'A'); | ||||||
|  |     g.setParent('b', 'B'); | ||||||
|  |     g.setParent('c', 'C'); | ||||||
|  |     g.setEdge('A', 'B', { data: 'link1' }, '1'); | ||||||
|  |     g.setEdge('A', 'C', { data: 'link2' }, '2'); | ||||||
|  |  | ||||||
|  |     // log.info(g.edges()) | ||||||
|  |     const d1 = extractDescendants('A', g); | ||||||
|  |     const d2 = extractDescendants('B', g); | ||||||
|  |     const d3 = extractDescendants('C', g); | ||||||
|  |  | ||||||
|  |     expect(d1).toEqual(['a']); | ||||||
|  |     expect(d2).toEqual(['b']); | ||||||
|  |     expect(d3).toEqual(['c']); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | describe('sortNodesByHierarchy', function () { | ||||||
|  |   let g; | ||||||
|  |   beforeEach(function () { | ||||||
|  |     setLogLevel(1); | ||||||
|  |     g = new graphlib.Graph({ | ||||||
|  |       multigraph: true, | ||||||
|  |       compound: true, | ||||||
|  |     }); | ||||||
|  |     g.setGraph({ | ||||||
|  |       rankdir: 'TB', | ||||||
|  |       nodesep: 10, | ||||||
|  |       ranksep: 10, | ||||||
|  |       marginx: 8, | ||||||
|  |       marginy: 8, | ||||||
|  |     }); | ||||||
|  |     g.setDefaultEdgeLabel(function () { | ||||||
|  |       return {}; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |   it('should sort proper en nodes are in reverse order', function () { | ||||||
|  |     /* | ||||||
|  |   a -->b | ||||||
|  |   subgraph B | ||||||
|  |   b | ||||||
|  |   end | ||||||
|  |   subgraph A | ||||||
|  |   B | ||||||
|  |   end | ||||||
|  |     */ | ||||||
|  |     g.setNode('a', { data: 1 }); | ||||||
|  |     g.setNode('b', { data: 2 }); | ||||||
|  |     g.setParent('b', 'B'); | ||||||
|  |     g.setParent('B', 'A'); | ||||||
|  |     g.setEdge('a', 'b', '1'); | ||||||
|  |     expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']); | ||||||
|  |   }); | ||||||
|  |   it('should sort proper en nodes are in correct order', function () { | ||||||
|  |     /* | ||||||
|  |   a -->b | ||||||
|  |   subgraph B | ||||||
|  |   b | ||||||
|  |   end | ||||||
|  |   subgraph A | ||||||
|  |   B | ||||||
|  |   end | ||||||
|  |     */ | ||||||
|  |     g.setNode('a', { data: 1 }); | ||||||
|  |     g.setParent('B', 'A'); | ||||||
|  |     g.setParent('b', 'B'); | ||||||
|  |     g.setNode('b', { data: 2 }); | ||||||
|  |     g.setEdge('a', 'b', '1'); | ||||||
|  |     expect(sortNodesByHierarchy(g)).toEqual(['a', 'A', 'B', 'b']); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -1,6 +1,9 @@ | |||||||
| export const render = async (data4Layout, svg, element) => { | export const render = async (data4Layout, svg, element) => { | ||||||
|   if (data4Layout.layoutAlgorithm === 'dagre-wrapper') { |   if (data4Layout.layoutAlgorithm === 'dagre-wrapper') { | ||||||
|     const layoutRenderer = await import('../dagre-wrapper/index-refactored.js'); |     console.warn('THERERERERERER'); | ||||||
|  |     // const layoutRenderer = await import('../dagre-wrapper/index-refactored.js'); | ||||||
|  |  | ||||||
|  |     const layoutRenderer = await import('./layout-algorithms/dagre/index.js'); | ||||||
|  |  | ||||||
|     return layoutRenderer.render(data4Layout, svg, element); |     return layoutRenderer.render(data4Layout, svg, element); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -0,0 +1,261 @@ | |||||||
|  | import intersectRect from '../rendering-elements/intersect/intersect-rect.js'; | ||||||
|  | import { log } from '$root/logger.js'; | ||||||
|  | import createLabel from './createLabel.js'; | ||||||
|  | import { createText } from '../createText.ts'; | ||||||
|  | import { select } from 'd3'; | ||||||
|  | import { getConfig } from '$root/diagram-api/diagramAPI.js'; | ||||||
|  | import { evaluate } from '$root/diagrams/common/common.js'; | ||||||
|  | import { getSubGraphTitleMargins } from '$root/utils/subGraphTitleMargins.js'; | ||||||
|  |  | ||||||
|  | const rect = (parent, node) => { | ||||||
|  |   log.info('Creating subgraph rect for ', node.id, node); | ||||||
|  |   const siteConfig = getConfig(); | ||||||
|  |  | ||||||
|  |   // Add outer g element | ||||||
|  |   const shapeSvg = parent | ||||||
|  |     .insert('g') | ||||||
|  |     .attr('class', 'cluster' + (node.class ? ' ' + node.class : '')) | ||||||
|  |     .attr('id', node.id); | ||||||
|  |  | ||||||
|  |   // add the rect | ||||||
|  |   const rect = shapeSvg.insert('rect', ':first-child'); | ||||||
|  |  | ||||||
|  |   const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels); | ||||||
|  |  | ||||||
|  |   // Create the label and insert it after the rect | ||||||
|  |   const label = shapeSvg.insert('g').attr('class', 'cluster-label'); | ||||||
|  |  | ||||||
|  |   // const text = label | ||||||
|  |   //   .node() | ||||||
|  |   //   .appendChild(createLabel(node.labelText, node.labelStyle, undefined, true)); | ||||||
|  |   const text = | ||||||
|  |     node.labelType === 'markdown' | ||||||
|  |       ? createText(label, node.labelText, { style: node.labelStyle, useHtmlLabels }) | ||||||
|  |       : label.node().appendChild(createLabel(node.labelText, node.labelStyle, undefined, true)); | ||||||
|  |  | ||||||
|  |   // Get the size of the label | ||||||
|  |   let bbox = text.getBBox(); | ||||||
|  |  | ||||||
|  |   if (evaluate(siteConfig.flowchart.htmlLabels)) { | ||||||
|  |     const div = text.children[0]; | ||||||
|  |     const dv = select(text); | ||||||
|  |     bbox = div.getBoundingClientRect(); | ||||||
|  |     dv.attr('width', bbox.width); | ||||||
|  |     dv.attr('height', bbox.height); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const padding = 0 * node.padding; | ||||||
|  |   const halfPadding = padding / 2; | ||||||
|  |  | ||||||
|  |   const width = node.width <= bbox.width + padding ? bbox.width + padding : node.width; | ||||||
|  |   if (node.width <= bbox.width + padding) { | ||||||
|  |     node.diff = (bbox.width - node.width) / 2 - node.padding / 2; | ||||||
|  |   } else { | ||||||
|  |     node.diff = -node.padding / 2; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   log.trace('Data ', node, JSON.stringify(node)); | ||||||
|  |   // center the rect around its coordinate | ||||||
|  |   rect | ||||||
|  |     .attr('style', node.style) | ||||||
|  |     .attr('rx', node.rx) | ||||||
|  |     .attr('ry', node.ry) | ||||||
|  |     .attr('x', node.x - width / 2) | ||||||
|  |     .attr('y', node.y - node.height / 2 - halfPadding) | ||||||
|  |     .attr('width', width) | ||||||
|  |     .attr('height', node.height + padding); | ||||||
|  |  | ||||||
|  |   const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig); | ||||||
|  |   if (useHtmlLabels) { | ||||||
|  |     label.attr( | ||||||
|  |       'transform', | ||||||
|  |       // This puts the labal on top of the box instead of inside it | ||||||
|  |       `translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})` | ||||||
|  |     ); | ||||||
|  |   } else { | ||||||
|  |     label.attr( | ||||||
|  |       'transform', | ||||||
|  |       // This puts the labal on top of the box instead of inside it | ||||||
|  |       `translate(${node.x}, ${node.y - node.height / 2 + subGraphTitleTopMargin})` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   // Center the label | ||||||
|  |  | ||||||
|  |   const rectBox = rect.node().getBBox(); | ||||||
|  |   node.width = rectBox.width; | ||||||
|  |   node.height = rectBox.height; | ||||||
|  |  | ||||||
|  |   node.intersect = function (point) { | ||||||
|  |     return intersectRect(node, point); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return shapeSvg; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Non visible cluster where the note is group with its | ||||||
|  |  * | ||||||
|  |  * @param {any} parent | ||||||
|  |  * @param {any} node | ||||||
|  |  * @returns {any} ShapeSvg | ||||||
|  |  */ | ||||||
|  | const noteGroup = (parent, node) => { | ||||||
|  |   // Add outer g element | ||||||
|  |   const shapeSvg = parent.insert('g').attr('class', 'note-cluster').attr('id', node.id); | ||||||
|  |  | ||||||
|  |   // add the rect | ||||||
|  |   const rect = shapeSvg.insert('rect', ':first-child'); | ||||||
|  |  | ||||||
|  |   const padding = 0 * node.padding; | ||||||
|  |   const halfPadding = padding / 2; | ||||||
|  |  | ||||||
|  |   // center the rect around its coordinate | ||||||
|  |   rect | ||||||
|  |     .attr('rx', node.rx) | ||||||
|  |     .attr('ry', node.ry) | ||||||
|  |     .attr('x', node.x - node.width / 2 - halfPadding) | ||||||
|  |     .attr('y', node.y - node.height / 2 - halfPadding) | ||||||
|  |     .attr('width', node.width + padding) | ||||||
|  |     .attr('height', node.height + padding) | ||||||
|  |     .attr('fill', 'none'); | ||||||
|  |  | ||||||
|  |   const rectBox = rect.node().getBBox(); | ||||||
|  |   node.width = rectBox.width; | ||||||
|  |   node.height = rectBox.height; | ||||||
|  |  | ||||||
|  |   node.intersect = function (point) { | ||||||
|  |     return intersectRect(node, point); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return shapeSvg; | ||||||
|  | }; | ||||||
|  | const roundedWithTitle = (parent, node) => { | ||||||
|  |   const siteConfig = getConfig(); | ||||||
|  |  | ||||||
|  |   // Add outer g element | ||||||
|  |   const shapeSvg = parent.insert('g').attr('class', node.classes).attr('id', node.id); | ||||||
|  |  | ||||||
|  |   // add the rect | ||||||
|  |   const rect = shapeSvg.insert('rect', ':first-child'); | ||||||
|  |  | ||||||
|  |   // Create the label and insert it after the rect | ||||||
|  |   const label = shapeSvg.insert('g').attr('class', 'cluster-label'); | ||||||
|  |   const innerRect = shapeSvg.append('rect'); | ||||||
|  |  | ||||||
|  |   const text = label | ||||||
|  |     .node() | ||||||
|  |     .appendChild(createLabel(node.labelText, node.labelStyle, undefined, true)); | ||||||
|  |  | ||||||
|  |   // Get the size of the label | ||||||
|  |   let bbox = text.getBBox(); | ||||||
|  |   if (evaluate(siteConfig.flowchart.htmlLabels)) { | ||||||
|  |     const div = text.children[0]; | ||||||
|  |     const dv = select(text); | ||||||
|  |     bbox = div.getBoundingClientRect(); | ||||||
|  |     dv.attr('width', bbox.width); | ||||||
|  |     dv.attr('height', bbox.height); | ||||||
|  |   } | ||||||
|  |   bbox = text.getBBox(); | ||||||
|  |   const padding = 0 * node.padding; | ||||||
|  |   const halfPadding = padding / 2; | ||||||
|  |  | ||||||
|  |   const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width; | ||||||
|  |   if (node.width <= bbox.width + node.padding) { | ||||||
|  |     node.diff = (bbox.width + node.padding * 0 - node.width) / 2; | ||||||
|  |   } else { | ||||||
|  |     node.diff = -node.padding / 2; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // center the rect around its coordinate | ||||||
|  |   rect | ||||||
|  |     .attr('class', 'outer') | ||||||
|  |     .attr('x', node.x - width / 2 - halfPadding) | ||||||
|  |     .attr('y', node.y - node.height / 2 - halfPadding) | ||||||
|  |     .attr('width', width + padding) | ||||||
|  |     .attr('height', node.height + padding); | ||||||
|  |   innerRect | ||||||
|  |     .attr('class', 'inner') | ||||||
|  |     .attr('x', node.x - width / 2 - halfPadding) | ||||||
|  |     .attr('y', node.y - node.height / 2 - halfPadding + bbox.height - 1) | ||||||
|  |     .attr('width', width + padding) | ||||||
|  |     .attr('height', node.height + padding - bbox.height - 3); | ||||||
|  |  | ||||||
|  |   const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig); | ||||||
|  |   // Center the label | ||||||
|  |   label.attr( | ||||||
|  |     'transform', | ||||||
|  |     `translate(${node.x - bbox.width / 2}, ${ | ||||||
|  |       node.y - | ||||||
|  |       node.height / 2 - | ||||||
|  |       node.padding / 3 + | ||||||
|  |       (evaluate(siteConfig.flowchart.htmlLabels) ? 5 : 3) + | ||||||
|  |       subGraphTitleTopMargin | ||||||
|  |     })` | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const rectBox = rect.node().getBBox(); | ||||||
|  |   node.height = rectBox.height; | ||||||
|  |  | ||||||
|  |   node.intersect = function (point) { | ||||||
|  |     return intersectRect(node, point); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return shapeSvg; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const divider = (parent, node) => { | ||||||
|  |   // Add outer g element | ||||||
|  |   const shapeSvg = parent.insert('g').attr('class', node.classes).attr('id', node.id); | ||||||
|  |  | ||||||
|  |   // add the rect | ||||||
|  |   const rect = shapeSvg.insert('rect', ':first-child'); | ||||||
|  |  | ||||||
|  |   const padding = 0 * node.padding; | ||||||
|  |   const halfPadding = padding / 2; | ||||||
|  |  | ||||||
|  |   // center the rect around its coordinate | ||||||
|  |   rect | ||||||
|  |     .attr('class', 'divider') | ||||||
|  |     .attr('x', node.x - node.width / 2 - halfPadding) | ||||||
|  |     .attr('y', node.y - node.height / 2) | ||||||
|  |     .attr('width', node.width + padding) | ||||||
|  |     .attr('height', node.height + padding); | ||||||
|  |  | ||||||
|  |   const rectBox = rect.node().getBBox(); | ||||||
|  |   node.width = rectBox.width; | ||||||
|  |   node.height = rectBox.height; | ||||||
|  |   node.diff = -node.padding / 2; | ||||||
|  |   node.intersect = function (point) { | ||||||
|  |     return intersectRect(node, point); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return shapeSvg; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const shapes = { rect, roundedWithTitle, noteGroup, divider }; | ||||||
|  |  | ||||||
|  | let clusterElems = {}; | ||||||
|  |  | ||||||
|  | export const insertCluster = (elem, node) => { | ||||||
|  |   log.trace('Inserting cluster'); | ||||||
|  |   const shape = node.shape || 'rect'; | ||||||
|  |   clusterElems[node.id] = shapes[shape](elem, node); | ||||||
|  | }; | ||||||
|  | export const getClusterTitleWidth = (elem, node) => { | ||||||
|  |   const label = createLabel(node.labelText, node.labelStyle, undefined, true); | ||||||
|  |   elem.node().appendChild(label); | ||||||
|  |   const width = label.getBBox().width; | ||||||
|  |   elem.node().removeChild(label); | ||||||
|  |   return width; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const clear = () => { | ||||||
|  |   clusterElems = {}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const positionCluster = (node) => { | ||||||
|  |   log.info('Position cluster (' + node.id + ', ' + node.x + ', ' + node.y + ')'); | ||||||
|  |   const el = clusterElems[node.id]; | ||||||
|  |  | ||||||
|  |   el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')'); | ||||||
|  | }; | ||||||
| @@ -0,0 +1,100 @@ | |||||||
|  | import { select } from 'd3'; | ||||||
|  | import { log } from '$root/logger.js'; | ||||||
|  | import { getConfig } from '$root/diagram-api/diagramAPI.js'; | ||||||
|  | import { evaluate } from '$root/diagrams/common/common.js'; | ||||||
|  | import { decodeEntities } from '$root/utils.js'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param dom | ||||||
|  |  * @param styleFn | ||||||
|  |  */ | ||||||
|  | function applyStyle(dom, styleFn) { | ||||||
|  |   if (styleFn) { | ||||||
|  |     dom.attr('style', styleFn); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param {any} node | ||||||
|  |  * @returns {SVGForeignObjectElement} Node | ||||||
|  |  */ | ||||||
|  | function addHtmlLabel(node) { | ||||||
|  |   const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')); | ||||||
|  |   const div = fo.append('xhtml:div'); | ||||||
|  |  | ||||||
|  |   const label = node.label; | ||||||
|  |   const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel'; | ||||||
|  |   div.html( | ||||||
|  |     '<span class="' + | ||||||
|  |       labelClass + | ||||||
|  |       '" ' + | ||||||
|  |       (node.labelStyle ? 'style="' + node.labelStyle + '"' : '') + | ||||||
|  |       '>' + | ||||||
|  |       label + | ||||||
|  |       '</span>' | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   applyStyle(div, node.labelStyle); | ||||||
|  |   div.style('display', 'inline-block'); | ||||||
|  |   // Fix for firefox | ||||||
|  |   div.style('white-space', 'nowrap'); | ||||||
|  |   div.attr('xmlns', 'http://www.w3.org/1999/xhtml'); | ||||||
|  |   return fo.node(); | ||||||
|  | } | ||||||
|  | /** | ||||||
|  |  * @param _vertexText | ||||||
|  |  * @param style | ||||||
|  |  * @param isTitle | ||||||
|  |  * @param isNode | ||||||
|  |  * @deprecated svg-util/createText instead | ||||||
|  |  */ | ||||||
|  | const createLabel = (_vertexText, style, isTitle, isNode) => { | ||||||
|  |   let vertexText = _vertexText || ''; | ||||||
|  |   if (typeof vertexText === 'object') { | ||||||
|  |     vertexText = vertexText[0]; | ||||||
|  |   } | ||||||
|  |   if (evaluate(getConfig().flowchart.htmlLabels)) { | ||||||
|  |     // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? | ||||||
|  |     vertexText = vertexText.replace(/\\n|\n/g, '<br />'); | ||||||
|  |     log.info('vertexText' + vertexText); | ||||||
|  |     const node = { | ||||||
|  |       isNode, | ||||||
|  |       label: decodeEntities(vertexText).replace( | ||||||
|  |         /fa[blrs]?:fa-[\w-]+/g, | ||||||
|  |         (s) => `<i class='${s.replace(':', ' ')}'></i>` | ||||||
|  |       ), | ||||||
|  |       labelStyle: style.replace('fill:', 'color:'), | ||||||
|  |     }; | ||||||
|  |     let vertexNode = addHtmlLabel(node); | ||||||
|  |     // vertexNode.parentNode.removeChild(vertexNode); | ||||||
|  |     return vertexNode; | ||||||
|  |   } else { | ||||||
|  |     const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); | ||||||
|  |     svgLabel.setAttribute('style', style.replace('color:', 'fill:')); | ||||||
|  |     let rows = []; | ||||||
|  |     if (typeof vertexText === 'string') { | ||||||
|  |       rows = vertexText.split(/\\n|\n|<br\s*\/?>/gi); | ||||||
|  |     } else if (Array.isArray(vertexText)) { | ||||||
|  |       rows = vertexText; | ||||||
|  |     } else { | ||||||
|  |       rows = []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (const row of rows) { | ||||||
|  |       const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); | ||||||
|  |       tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); | ||||||
|  |       tspan.setAttribute('dy', '1em'); | ||||||
|  |       tspan.setAttribute('x', '0'); | ||||||
|  |       if (isTitle) { | ||||||
|  |         tspan.setAttribute('class', 'title-row'); | ||||||
|  |       } else { | ||||||
|  |         tspan.setAttribute('class', 'row'); | ||||||
|  |       } | ||||||
|  |       tspan.textContent = row.trim(); | ||||||
|  |       svgLabel.appendChild(tspan); | ||||||
|  |     } | ||||||
|  |     return svgLabel; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default createLabel; | ||||||
| @@ -0,0 +1,79 @@ | |||||||
|  | import type { Mocked } from 'vitest'; | ||||||
|  | import type { SVG } from '../diagram-api/types.js'; | ||||||
|  | import { addEdgeMarkers } from './edgeMarker.js'; | ||||||
|  |  | ||||||
|  | describe('addEdgeMarker', () => { | ||||||
|  |   const svgPath = { | ||||||
|  |     attr: vitest.fn(), | ||||||
|  |   } as unknown as Mocked<SVG>; | ||||||
|  |   const url = 'http://example.com'; | ||||||
|  |   const id = 'test'; | ||||||
|  |   const diagramType = 'test'; | ||||||
|  |  | ||||||
|  |   beforeEach(() => { | ||||||
|  |     svgPath.attr.mockReset(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should add markers for arrow_cross:arrow_point', () => { | ||||||
|  |     const arrowTypeStart = 'arrow_cross'; | ||||||
|  |     const arrowTypeEnd = 'arrow_point'; | ||||||
|  |     addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType); | ||||||
|  |     expect(svgPath.attr).toHaveBeenCalledWith( | ||||||
|  |       'marker-start', | ||||||
|  |       `url(${url}#${id}_${diagramType}-crossStart)` | ||||||
|  |     ); | ||||||
|  |     expect(svgPath.attr).toHaveBeenCalledWith( | ||||||
|  |       'marker-end', | ||||||
|  |       `url(${url}#${id}_${diagramType}-pointEnd)` | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should add markers for aggregation:arrow_point', () => { | ||||||
|  |     const arrowTypeStart = 'aggregation'; | ||||||
|  |     const arrowTypeEnd = 'arrow_point'; | ||||||
|  |     addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType); | ||||||
|  |     expect(svgPath.attr).toHaveBeenCalledWith( | ||||||
|  |       'marker-start', | ||||||
|  |       `url(${url}#${id}_${diagramType}-aggregationStart)` | ||||||
|  |     ); | ||||||
|  |     expect(svgPath.attr).toHaveBeenCalledWith( | ||||||
|  |       'marker-end', | ||||||
|  |       `url(${url}#${id}_${diagramType}-pointEnd)` | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should add markers for arrow_point:aggregation', () => { | ||||||
|  |     const arrowTypeStart = 'arrow_point'; | ||||||
|  |     const arrowTypeEnd = 'aggregation'; | ||||||
|  |     addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType); | ||||||
|  |     expect(svgPath.attr).toHaveBeenCalledWith( | ||||||
|  |       'marker-start', | ||||||
|  |       `url(${url}#${id}_${diagramType}-pointStart)` | ||||||
|  |     ); | ||||||
|  |     expect(svgPath.attr).toHaveBeenCalledWith( | ||||||
|  |       'marker-end', | ||||||
|  |       `url(${url}#${id}_${diagramType}-aggregationEnd)` | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should add markers for aggregation:composition', () => { | ||||||
|  |     const arrowTypeStart = 'aggregation'; | ||||||
|  |     const arrowTypeEnd = 'composition'; | ||||||
|  |     addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType); | ||||||
|  |     expect(svgPath.attr).toHaveBeenCalledWith( | ||||||
|  |       'marker-start', | ||||||
|  |       `url(${url}#${id}_${diagramType}-aggregationStart)` | ||||||
|  |     ); | ||||||
|  |     expect(svgPath.attr).toHaveBeenCalledWith( | ||||||
|  |       'marker-end', | ||||||
|  |       `url(${url}#${id}_${diagramType}-compositionEnd)` | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should not add invalid markers', () => { | ||||||
|  |     const arrowTypeStart = 'this is an invalid marker'; | ||||||
|  |     const arrowTypeEnd = ') url(https://my-malicious-site.example)'; | ||||||
|  |     addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType); | ||||||
|  |     expect(svgPath.attr).not.toHaveBeenCalled(); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -0,0 +1,57 @@ | |||||||
|  | import type { SVG } from '$root/diagram-api/types.js'; | ||||||
|  | import { log } from '$root/logger.js'; | ||||||
|  | import type { EdgeData } from '$root/types.js'; | ||||||
|  | /** | ||||||
|  |  * Adds SVG markers to a path element based on the arrow types specified in the edge. | ||||||
|  |  * | ||||||
|  |  * @param svgPath - The SVG path element to add markers to. | ||||||
|  |  * @param edge - The edge data object containing the arrow types. | ||||||
|  |  * @param url - The URL of the SVG marker definitions. | ||||||
|  |  * @param id - The ID prefix for the SVG marker definitions. | ||||||
|  |  * @param diagramType - The type of diagram being rendered. | ||||||
|  |  */ | ||||||
|  | export const addEdgeMarkers = ( | ||||||
|  |   svgPath: SVG, | ||||||
|  |   edge: Pick<EdgeData, 'arrowTypeStart' | 'arrowTypeEnd'>, | ||||||
|  |   url: string, | ||||||
|  |   id: string, | ||||||
|  |   diagramType: string | ||||||
|  | ) => { | ||||||
|  |   if (edge.arrowTypeStart) { | ||||||
|  |     addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType); | ||||||
|  |   } | ||||||
|  |   if (edge.arrowTypeEnd) { | ||||||
|  |     addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const arrowTypesMap = { | ||||||
|  |   arrow_cross: 'cross', | ||||||
|  |   arrow_point: 'point', | ||||||
|  |   arrow_barb: 'barb', | ||||||
|  |   arrow_circle: 'circle', | ||||||
|  |   aggregation: 'aggregation', | ||||||
|  |   extension: 'extension', | ||||||
|  |   composition: 'composition', | ||||||
|  |   dependency: 'dependency', | ||||||
|  |   lollipop: 'lollipop', | ||||||
|  | } as const; | ||||||
|  |  | ||||||
|  | const addEdgeMarker = ( | ||||||
|  |   svgPath: SVG, | ||||||
|  |   position: 'start' | 'end', | ||||||
|  |   arrowType: string, | ||||||
|  |   url: string, | ||||||
|  |   id: string, | ||||||
|  |   diagramType: string | ||||||
|  | ) => { | ||||||
|  |   const endMarkerType = arrowTypesMap[arrowType as keyof typeof arrowTypesMap]; | ||||||
|  |  | ||||||
|  |   if (!endMarkerType) { | ||||||
|  |     log.warn(`Unknown arrow type: ${arrowType}`); | ||||||
|  |     return; // unknown arrow type, ignore | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const suffix = position === 'start' ? 'Start' : 'End'; | ||||||
|  |   svgPath.attr(`marker-${position}`, `url(${url}#${id}_${diagramType}-${endMarkerType}${suffix})`); | ||||||
|  | }; | ||||||
							
								
								
									
										521
									
								
								packages/mermaid/src/rendering-util/rendering-elements/edges.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										521
									
								
								packages/mermaid/src/rendering-util/rendering-elements/edges.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,521 @@ | |||||||
|  | import { log } from '$root/logger.js'; | ||||||
|  | import createLabel from './createLabel.js'; | ||||||
|  | import { createText } from '$root/rendering-util/createText.ts'; | ||||||
|  | import { line, curveBasis, select } from 'd3'; | ||||||
|  | import { getConfig } from '$root/diagram-api/diagramAPI.js'; | ||||||
|  | import utils from '$root/utils.js'; | ||||||
|  | import { evaluate } from '$root/diagrams/common/common.js'; | ||||||
|  | import { getLineFunctionsWithOffset } from '$root/utils/lineWithOffset.js'; | ||||||
|  | import { getSubGraphTitleMargins } from '$root/utils/subGraphTitleMargins.js'; | ||||||
|  | import { addEdgeMarkers } from './edgeMarker.ts'; | ||||||
|  |  | ||||||
|  | let edgeLabels = {}; | ||||||
|  | let terminalLabels = {}; | ||||||
|  |  | ||||||
|  | export const clear = () => { | ||||||
|  |   edgeLabels = {}; | ||||||
|  |   terminalLabels = {}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const insertEdgeLabel = (elem, edge) => { | ||||||
|  |   const useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels); | ||||||
|  |   // Create the actual text element | ||||||
|  |   const labelElement = | ||||||
|  |     edge.labelType === 'markdown' | ||||||
|  |       ? createText(elem, edge.label, { | ||||||
|  |           style: edge.labelStyle, | ||||||
|  |           useHtmlLabels, | ||||||
|  |           addSvgBackground: true, | ||||||
|  |         }) | ||||||
|  |       : createLabel(edge.label, edge.labelStyle); | ||||||
|  |   log.info('abc82', edge, edge.labelType); | ||||||
|  |  | ||||||
|  |   // Create outer g, edgeLabel, this will be positioned after graph layout | ||||||
|  |   const edgeLabel = elem.insert('g').attr('class', 'edgeLabel'); | ||||||
|  |  | ||||||
|  |   // Create inner g, label, this will be positioned now for centering the text | ||||||
|  |   const label = edgeLabel.insert('g').attr('class', 'label'); | ||||||
|  |   label.node().appendChild(labelElement); | ||||||
|  |  | ||||||
|  |   // Center the label | ||||||
|  |   let bbox = labelElement.getBBox(); | ||||||
|  |   if (useHtmlLabels) { | ||||||
|  |     const div = labelElement.children[0]; | ||||||
|  |     const dv = select(labelElement); | ||||||
|  |     bbox = div.getBoundingClientRect(); | ||||||
|  |     dv.attr('width', bbox.width); | ||||||
|  |     dv.attr('height', bbox.height); | ||||||
|  |   } | ||||||
|  |   label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')'); | ||||||
|  |  | ||||||
|  |   // Make element accessible by id for positioning | ||||||
|  |   edgeLabels[edge.id] = edgeLabel; | ||||||
|  |  | ||||||
|  |   // Update the abstract data of the edge with the new information about its width and height | ||||||
|  |   edge.width = bbox.width; | ||||||
|  |   edge.height = bbox.height; | ||||||
|  |  | ||||||
|  |   let fo; | ||||||
|  |   if (edge.startLabelLeft) { | ||||||
|  |     // Create the actual text element | ||||||
|  |     const startLabelElement = createLabel(edge.startLabelLeft, edge.labelStyle); | ||||||
|  |     const startEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals'); | ||||||
|  |     const inner = startEdgeLabelLeft.insert('g').attr('class', 'inner'); | ||||||
|  |     fo = inner.node().appendChild(startLabelElement); | ||||||
|  |     const slBox = startLabelElement.getBBox(); | ||||||
|  |     inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')'); | ||||||
|  |     if (!terminalLabels[edge.id]) { | ||||||
|  |       terminalLabels[edge.id] = {}; | ||||||
|  |     } | ||||||
|  |     terminalLabels[edge.id].startLeft = startEdgeLabelLeft; | ||||||
|  |     setTerminalWidth(fo, edge.startLabelLeft); | ||||||
|  |   } | ||||||
|  |   if (edge.startLabelRight) { | ||||||
|  |     // Create the actual text element | ||||||
|  |     const startLabelElement = createLabel(edge.startLabelRight, edge.labelStyle); | ||||||
|  |     const startEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals'); | ||||||
|  |     const inner = startEdgeLabelRight.insert('g').attr('class', 'inner'); | ||||||
|  |     fo = startEdgeLabelRight.node().appendChild(startLabelElement); | ||||||
|  |     inner.node().appendChild(startLabelElement); | ||||||
|  |     const slBox = startLabelElement.getBBox(); | ||||||
|  |     inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')'); | ||||||
|  |  | ||||||
|  |     if (!terminalLabels[edge.id]) { | ||||||
|  |       terminalLabels[edge.id] = {}; | ||||||
|  |     } | ||||||
|  |     terminalLabels[edge.id].startRight = startEdgeLabelRight; | ||||||
|  |     setTerminalWidth(fo, edge.startLabelRight); | ||||||
|  |   } | ||||||
|  |   if (edge.endLabelLeft) { | ||||||
|  |     // Create the actual text element | ||||||
|  |     const endLabelElement = createLabel(edge.endLabelLeft, edge.labelStyle); | ||||||
|  |     const endEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals'); | ||||||
|  |     const inner = endEdgeLabelLeft.insert('g').attr('class', 'inner'); | ||||||
|  |     fo = inner.node().appendChild(endLabelElement); | ||||||
|  |     const slBox = endLabelElement.getBBox(); | ||||||
|  |     inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')'); | ||||||
|  |  | ||||||
|  |     endEdgeLabelLeft.node().appendChild(endLabelElement); | ||||||
|  |  | ||||||
|  |     if (!terminalLabels[edge.id]) { | ||||||
|  |       terminalLabels[edge.id] = {}; | ||||||
|  |     } | ||||||
|  |     terminalLabels[edge.id].endLeft = endEdgeLabelLeft; | ||||||
|  |     setTerminalWidth(fo, edge.endLabelLeft); | ||||||
|  |   } | ||||||
|  |   if (edge.endLabelRight) { | ||||||
|  |     // Create the actual text element | ||||||
|  |     const endLabelElement = createLabel(edge.endLabelRight, edge.labelStyle); | ||||||
|  |     const endEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals'); | ||||||
|  |     const inner = endEdgeLabelRight.insert('g').attr('class', 'inner'); | ||||||
|  |  | ||||||
|  |     fo = inner.node().appendChild(endLabelElement); | ||||||
|  |     const slBox = endLabelElement.getBBox(); | ||||||
|  |     inner.attr('transform', 'translate(' + -slBox.width / 2 + ', ' + -slBox.height / 2 + ')'); | ||||||
|  |  | ||||||
|  |     endEdgeLabelRight.node().appendChild(endLabelElement); | ||||||
|  |     if (!terminalLabels[edge.id]) { | ||||||
|  |       terminalLabels[edge.id] = {}; | ||||||
|  |     } | ||||||
|  |     terminalLabels[edge.id].endRight = endEdgeLabelRight; | ||||||
|  |     setTerminalWidth(fo, edge.endLabelRight); | ||||||
|  |   } | ||||||
|  |   return labelElement; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param {any} fo | ||||||
|  |  * @param {any} value | ||||||
|  |  */ | ||||||
|  | function setTerminalWidth(fo, value) { | ||||||
|  |   if (getConfig().flowchart.htmlLabels && fo) { | ||||||
|  |     fo.style.width = value.length * 9 + 'px'; | ||||||
|  |     fo.style.height = '12px'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const positionEdgeLabel = (edge, paths) => { | ||||||
|  |   log.info('Moving label abc78 ', edge.id, edge.label, edgeLabels[edge.id]); | ||||||
|  |   let path = paths.updatedPath ? paths.updatedPath : paths.originalPath; | ||||||
|  |   const siteConfig = getConfig(); | ||||||
|  |   const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig); | ||||||
|  |   if (edge.label) { | ||||||
|  |     const el = edgeLabels[edge.id]; | ||||||
|  |     let x = edge.x; | ||||||
|  |     let y = edge.y; | ||||||
|  |     if (path) { | ||||||
|  |       //   // debugger; | ||||||
|  |       const pos = utils.calcLabelPosition(path); | ||||||
|  |       log.info( | ||||||
|  |         'Moving label ' + edge.label + ' from (', | ||||||
|  |         x, | ||||||
|  |         ',', | ||||||
|  |         y, | ||||||
|  |         ') to (', | ||||||
|  |         pos.x, | ||||||
|  |         ',', | ||||||
|  |         pos.y, | ||||||
|  |         ') abc78' | ||||||
|  |       ); | ||||||
|  |       if (paths.updatedPath) { | ||||||
|  |         x = pos.x; | ||||||
|  |         y = pos.y; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     el.attr('transform', `translate(${x}, ${y + subGraphTitleTotalMargin / 2})`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   //let path = paths.updatedPath ? paths.updatedPath : paths.originalPath; | ||||||
|  |   if (edge.startLabelLeft) { | ||||||
|  |     const el = terminalLabels[edge.id].startLeft; | ||||||
|  |     let x = edge.x; | ||||||
|  |     let y = edge.y; | ||||||
|  |     if (path) { | ||||||
|  |       // debugger; | ||||||
|  |       const pos = utils.calcTerminalLabelPosition(edge.arrowTypeStart ? 10 : 0, 'start_left', path); | ||||||
|  |       x = pos.x; | ||||||
|  |       y = pos.y; | ||||||
|  |     } | ||||||
|  |     el.attr('transform', `translate(${x}, ${y})`); | ||||||
|  |   } | ||||||
|  |   if (edge.startLabelRight) { | ||||||
|  |     const el = terminalLabels[edge.id].startRight; | ||||||
|  |     let x = edge.x; | ||||||
|  |     let y = edge.y; | ||||||
|  |     if (path) { | ||||||
|  |       // debugger; | ||||||
|  |       const pos = utils.calcTerminalLabelPosition( | ||||||
|  |         edge.arrowTypeStart ? 10 : 0, | ||||||
|  |         'start_right', | ||||||
|  |         path | ||||||
|  |       ); | ||||||
|  |       x = pos.x; | ||||||
|  |       y = pos.y; | ||||||
|  |     } | ||||||
|  |     el.attr('transform', `translate(${x}, ${y})`); | ||||||
|  |   } | ||||||
|  |   if (edge.endLabelLeft) { | ||||||
|  |     const el = terminalLabels[edge.id].endLeft; | ||||||
|  |     let x = edge.x; | ||||||
|  |     let y = edge.y; | ||||||
|  |     if (path) { | ||||||
|  |       // debugger; | ||||||
|  |       const pos = utils.calcTerminalLabelPosition(edge.arrowTypeEnd ? 10 : 0, 'end_left', path); | ||||||
|  |       x = pos.x; | ||||||
|  |       y = pos.y; | ||||||
|  |     } | ||||||
|  |     el.attr('transform', `translate(${x}, ${y})`); | ||||||
|  |   } | ||||||
|  |   if (edge.endLabelRight) { | ||||||
|  |     const el = terminalLabels[edge.id].endRight; | ||||||
|  |     let x = edge.x; | ||||||
|  |     let y = edge.y; | ||||||
|  |     if (path) { | ||||||
|  |       // debugger; | ||||||
|  |       const pos = utils.calcTerminalLabelPosition(edge.arrowTypeEnd ? 10 : 0, 'end_right', path); | ||||||
|  |       x = pos.x; | ||||||
|  |       y = pos.y; | ||||||
|  |     } | ||||||
|  |     el.attr('transform', `translate(${x}, ${y})`); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const outsideNode = (node, point) => { | ||||||
|  |   // log.warn('Checking bounds ', node, point); | ||||||
|  |   const x = node.x; | ||||||
|  |   const y = node.y; | ||||||
|  |   const dx = Math.abs(point.x - x); | ||||||
|  |   const dy = Math.abs(point.y - y); | ||||||
|  |   const w = node.width / 2; | ||||||
|  |   const h = node.height / 2; | ||||||
|  |   if (dx >= w || dy >= h) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |   return false; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const intersection = (node, outsidePoint, insidePoint) => { | ||||||
|  |   log.warn(`intersection calc abc89: | ||||||
|  |   outsidePoint: ${JSON.stringify(outsidePoint)} | ||||||
|  |   insidePoint : ${JSON.stringify(insidePoint)} | ||||||
|  |   node        : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`); | ||||||
|  |   const x = node.x; | ||||||
|  |   const y = node.y; | ||||||
|  |  | ||||||
|  |   const dx = Math.abs(x - insidePoint.x); | ||||||
|  |   // const dy = Math.abs(y - insidePoint.y); | ||||||
|  |   const w = node.width / 2; | ||||||
|  |   let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx; | ||||||
|  |   const h = node.height / 2; | ||||||
|  |  | ||||||
|  |   // const edges = { | ||||||
|  |   //   x1: x - w, | ||||||
|  |   //   x2: x + w, | ||||||
|  |   //   y1: y - h, | ||||||
|  |   //   y2: y + h | ||||||
|  |   // }; | ||||||
|  |  | ||||||
|  |   // if ( | ||||||
|  |   //   outsidePoint.x === edges.x1 || | ||||||
|  |   //   outsidePoint.x === edges.x2 || | ||||||
|  |   //   outsidePoint.y === edges.y1 || | ||||||
|  |   //   outsidePoint.y === edges.y2 | ||||||
|  |   // ) { | ||||||
|  |   //   log.warn('abc89 calc equals on edge', outsidePoint, edges); | ||||||
|  |   //   return outsidePoint; | ||||||
|  |   // } | ||||||
|  |  | ||||||
|  |   const Q = Math.abs(outsidePoint.y - insidePoint.y); | ||||||
|  |   const R = Math.abs(outsidePoint.x - insidePoint.x); | ||||||
|  |   // log.warn(); | ||||||
|  |   if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) { | ||||||
|  |     // Intersection is top or bottom of rect. | ||||||
|  |     // let q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y; | ||||||
|  |     let q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y; | ||||||
|  |     r = (R * q) / Q; | ||||||
|  |     const res = { | ||||||
|  |       x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r, | ||||||
|  |       y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (r === 0) { | ||||||
|  |       res.x = outsidePoint.x; | ||||||
|  |       res.y = outsidePoint.y; | ||||||
|  |     } | ||||||
|  |     if (R === 0) { | ||||||
|  |       res.x = outsidePoint.x; | ||||||
|  |     } | ||||||
|  |     if (Q === 0) { | ||||||
|  |       res.y = outsidePoint.y; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     log.warn(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); | ||||||
|  |  | ||||||
|  |     return res; | ||||||
|  |   } else { | ||||||
|  |     // Intersection onn sides of rect | ||||||
|  |     if (insidePoint.x < outsidePoint.x) { | ||||||
|  |       r = outsidePoint.x - w - x; | ||||||
|  |     } else { | ||||||
|  |       // r = outsidePoint.x - w - x; | ||||||
|  |       r = x - w - outsidePoint.x; | ||||||
|  |     } | ||||||
|  |     let q = (Q * r) / R; | ||||||
|  |     //  OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w; | ||||||
|  |     // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; | ||||||
|  |     let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r; | ||||||
|  |     // let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; | ||||||
|  |     let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q; | ||||||
|  |     log.warn(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y }); | ||||||
|  |     if (r === 0) { | ||||||
|  |       _x = outsidePoint.x; | ||||||
|  |       _y = outsidePoint.y; | ||||||
|  |     } | ||||||
|  |     if (R === 0) { | ||||||
|  |       _x = outsidePoint.x; | ||||||
|  |     } | ||||||
|  |     if (Q === 0) { | ||||||
|  |       _y = outsidePoint.y; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { x: _x, y: _y }; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | /** | ||||||
|  |  * This function will page a path and node where the last point(s) in the path is inside the node | ||||||
|  |  * and return an update path ending by the border of the node. | ||||||
|  |  * | ||||||
|  |  * @param {Array} _points | ||||||
|  |  * @param {any} boundryNode | ||||||
|  |  * @returns {Array} Points | ||||||
|  |  */ | ||||||
|  | const cutPathAtIntersect = (_points, boundryNode) => { | ||||||
|  |   log.warn('abc88 cutPathAtIntersect', _points, boundryNode); | ||||||
|  |   let points = []; | ||||||
|  |   let lastPointOutside = _points[0]; | ||||||
|  |   let isInside = false; | ||||||
|  |   _points.forEach((point) => { | ||||||
|  |     // const node = clusterDb[edge.toCluster].node; | ||||||
|  |     log.info('abc88 checking point', point, boundryNode); | ||||||
|  |  | ||||||
|  |     // check if point is inside the boundary rect | ||||||
|  |     if (!outsideNode(boundryNode, point) && !isInside) { | ||||||
|  |       // First point inside the rect found | ||||||
|  |       // Calc the intersection coord between the point anf the last point outside the rect | ||||||
|  |       const inter = intersection(boundryNode, lastPointOutside, point); | ||||||
|  |       log.warn('abc88 inside', point, lastPointOutside, inter); | ||||||
|  |       log.warn('abc88 intersection', inter); | ||||||
|  |  | ||||||
|  |       // // Check case where the intersection is the same as the last point | ||||||
|  |       let pointPresent = false; | ||||||
|  |       points.forEach((p) => { | ||||||
|  |         pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y); | ||||||
|  |       }); | ||||||
|  |       // // if (!pointPresent) { | ||||||
|  |       if (!points.some((e) => e.x === inter.x && e.y === inter.y)) { | ||||||
|  |         points.push(inter); | ||||||
|  |       } else { | ||||||
|  |         log.warn('abc88 no intersect', inter, points); | ||||||
|  |       } | ||||||
|  |       // points.push(inter); | ||||||
|  |       isInside = true; | ||||||
|  |     } else { | ||||||
|  |       // Outside | ||||||
|  |       log.warn('abc88 outside', point, lastPointOutside); | ||||||
|  |       lastPointOutside = point; | ||||||
|  |       // points.push(point); | ||||||
|  |       if (!isInside) { | ||||||
|  |         points.push(point); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   log.warn('abc88 returning points', points); | ||||||
|  |   return points; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph, id) { | ||||||
|  |   let points = edge.points; | ||||||
|  |   let pointsHasChanged = false; | ||||||
|  |   const tail = graph.node(e.v); | ||||||
|  |   var head = graph.node(e.w); | ||||||
|  |  | ||||||
|  |   log.info('abc88 InsertEdge: ', edge); | ||||||
|  |   if (head.intersect && tail.intersect) { | ||||||
|  |     points = points.slice(1, edge.points.length - 1); | ||||||
|  |     points.unshift(tail.intersect(points[0])); | ||||||
|  |     log.info( | ||||||
|  |       'Last point', | ||||||
|  |       points[points.length - 1], | ||||||
|  |       head, | ||||||
|  |       head.intersect(points[points.length - 1]) | ||||||
|  |     ); | ||||||
|  |     points.push(head.intersect(points[points.length - 1])); | ||||||
|  |   } | ||||||
|  |   if (edge.toCluster) { | ||||||
|  |     log.info('to cluster abc88', clusterDb[edge.toCluster]); | ||||||
|  |     points = cutPathAtIntersect(edge.points, clusterDb[edge.toCluster].node); | ||||||
|  |     // log.trace('edge', edge); | ||||||
|  |     // points = []; | ||||||
|  |     // let lastPointOutside; // = edge.points[0]; | ||||||
|  |     // let isInside = false; | ||||||
|  |     // edge.points.forEach(point => { | ||||||
|  |     //   const node = clusterDb[edge.toCluster].node; | ||||||
|  |     //   log.warn('checking from', edge.fromCluster, point, node); | ||||||
|  |  | ||||||
|  |     //   if (!outsideNode(node, point) && !isInside) { | ||||||
|  |     //     log.trace('inside', edge.toCluster, point, lastPointOutside); | ||||||
|  |  | ||||||
|  |     //     // First point inside the rect | ||||||
|  |     //     const inter = intersection(node, lastPointOutside, point); | ||||||
|  |  | ||||||
|  |     //     let pointPresent = false; | ||||||
|  |     //     points.forEach(p => { | ||||||
|  |     //       pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y); | ||||||
|  |     //     }); | ||||||
|  |     //     // if (!pointPresent) { | ||||||
|  |     //     if (!points.find(e => e.x === inter.x && e.y === inter.y)) { | ||||||
|  |     //       points.push(inter); | ||||||
|  |     //     } else { | ||||||
|  |     //       log.warn('no intersect', inter, points); | ||||||
|  |     //     } | ||||||
|  |     //     isInside = true; | ||||||
|  |     // } else { | ||||||
|  |     //   // outside | ||||||
|  |     //   lastPointOutside = point; | ||||||
|  |     //   if (!isInside) points.push(point); | ||||||
|  |     // } | ||||||
|  |     // }); | ||||||
|  |     pointsHasChanged = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (edge.fromCluster) { | ||||||
|  |     log.info('from cluster abc88', clusterDb[edge.fromCluster]); | ||||||
|  |     points = cutPathAtIntersect(points.reverse(), clusterDb[edge.fromCluster].node).reverse(); | ||||||
|  |  | ||||||
|  |     pointsHasChanged = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // The data for our line | ||||||
|  |   const lineData = points.filter((p) => !Number.isNaN(p.y)); | ||||||
|  |  | ||||||
|  |   // This is the accessor function we talked about above | ||||||
|  |   let curve = curveBasis; | ||||||
|  |   // Currently only flowcharts get the curve from the settings, perhaps this should | ||||||
|  |   // be expanded to a common setting? Restricting it for now in order not to cause side-effects that | ||||||
|  |   // have not been thought through | ||||||
|  |   if (edge.curve && (diagramType === 'graph' || diagramType === 'flowchart')) { | ||||||
|  |     curve = edge.curve; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const { x, y } = getLineFunctionsWithOffset(edge); | ||||||
|  |   const lineFunction = line().x(x).y(y).curve(curve); | ||||||
|  |  | ||||||
|  |   // Construct stroke classes based on properties | ||||||
|  |   let strokeClasses; | ||||||
|  |   switch (edge.thickness) { | ||||||
|  |     case 'normal': | ||||||
|  |       strokeClasses = 'edge-thickness-normal'; | ||||||
|  |       break; | ||||||
|  |     case 'thick': | ||||||
|  |       strokeClasses = 'edge-thickness-thick'; | ||||||
|  |       break; | ||||||
|  |     case 'invisible': | ||||||
|  |       strokeClasses = 'edge-thickness-thick'; | ||||||
|  |       break; | ||||||
|  |     default: | ||||||
|  |       strokeClasses = ''; | ||||||
|  |   } | ||||||
|  |   switch (edge.pattern) { | ||||||
|  |     case 'solid': | ||||||
|  |       strokeClasses += ' edge-pattern-solid'; | ||||||
|  |       break; | ||||||
|  |     case 'dotted': | ||||||
|  |       strokeClasses += ' edge-pattern-dotted'; | ||||||
|  |       break; | ||||||
|  |     case 'dashed': | ||||||
|  |       strokeClasses += ' edge-pattern-dashed'; | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const svgPath = elem | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', lineFunction(lineData)) | ||||||
|  |     .attr('id', edge.id) | ||||||
|  |     .attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '')) | ||||||
|  |     .attr('style', edge.style); | ||||||
|  |  | ||||||
|  |   // DEBUG code, adds a red circle at each edge coordinate | ||||||
|  |   // edge.points.forEach((point) => { | ||||||
|  |   //   elem | ||||||
|  |   //     .append('circle') | ||||||
|  |   //     .style('stroke', 'red') | ||||||
|  |   //     .style('fill', 'red') | ||||||
|  |   //     .attr('r', 1) | ||||||
|  |   //     .attr('cx', point.x) | ||||||
|  |   //     .attr('cy', point.y); | ||||||
|  |   // }); | ||||||
|  |  | ||||||
|  |   let url = ''; | ||||||
|  |   // // TODO: Can we load this config only from the rendered graph type? | ||||||
|  |   if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) { | ||||||
|  |     url = | ||||||
|  |       window.location.protocol + | ||||||
|  |       '//' + | ||||||
|  |       window.location.host + | ||||||
|  |       window.location.pathname + | ||||||
|  |       window.location.search; | ||||||
|  |     url = url.replace(/\(/g, '\\('); | ||||||
|  |     url = url.replace(/\)/g, '\\)'); | ||||||
|  |   } | ||||||
|  |   log.info('arrowTypeStart', edge.arrowTypeStart); | ||||||
|  |   log.info('arrowTypeEnd', edge.arrowTypeEnd); | ||||||
|  |  | ||||||
|  |   addEdgeMarkers(svgPath, edge, url, id, diagramType); | ||||||
|  |  | ||||||
|  |   let paths = {}; | ||||||
|  |   if (pointsHasChanged) { | ||||||
|  |     paths.updatedPath = points; | ||||||
|  |   } | ||||||
|  |   paths.originalPath = edge.points; | ||||||
|  |   return paths; | ||||||
|  | }; | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | /* | ||||||
|  |  * Borrowed with love from from dagre-d3. Many thanks to cpettitt! | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import node from './intersect-node.js'; | ||||||
|  | import circle from './intersect-circle.js'; | ||||||
|  | import ellipse from './intersect-ellipse.js'; | ||||||
|  | import polygon from './intersect-polygon.js'; | ||||||
|  | import rect from './intersect-rect.js'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   node, | ||||||
|  |   circle, | ||||||
|  |   ellipse, | ||||||
|  |   polygon, | ||||||
|  |   rect, | ||||||
|  | }; | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | import intersectEllipse from './intersect-ellipse.js'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param node | ||||||
|  |  * @param rx | ||||||
|  |  * @param point | ||||||
|  |  */ | ||||||
|  | function intersectCircle(node, rx, point) { | ||||||
|  |   return intersectEllipse(node, rx, rx, point); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default intersectCircle; | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | /** | ||||||
|  |  * @param node | ||||||
|  |  * @param rx | ||||||
|  |  * @param ry | ||||||
|  |  * @param point | ||||||
|  |  */ | ||||||
|  | function intersectEllipse(node, rx, ry, point) { | ||||||
|  |   // Formulae from: https://mathworld.wolfram.com/Ellipse-LineIntersection.html | ||||||
|  |  | ||||||
|  |   var cx = node.x; | ||||||
|  |   var cy = node.y; | ||||||
|  |  | ||||||
|  |   var px = cx - point.x; | ||||||
|  |   var py = cy - point.y; | ||||||
|  |  | ||||||
|  |   var det = Math.sqrt(rx * rx * py * py + ry * ry * px * px); | ||||||
|  |  | ||||||
|  |   var dx = Math.abs((rx * ry * px) / det); | ||||||
|  |   if (point.x < cx) { | ||||||
|  |     dx = -dx; | ||||||
|  |   } | ||||||
|  |   var dy = Math.abs((rx * ry * py) / det); | ||||||
|  |   if (point.y < cy) { | ||||||
|  |     dy = -dy; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { x: cx + dx, y: cy + dy }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default intersectEllipse; | ||||||
| @@ -0,0 +1,78 @@ | |||||||
|  | /** | ||||||
|  |  * Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect. | ||||||
|  |  * | ||||||
|  |  * @param p1 | ||||||
|  |  * @param p2 | ||||||
|  |  * @param q1 | ||||||
|  |  * @param q2 | ||||||
|  |  */ | ||||||
|  | function intersectLine(p1, p2, q1, q2) { | ||||||
|  |   // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, | ||||||
|  |   // p7 and p473. | ||||||
|  |  | ||||||
|  |   var a1, a2, b1, b2, c1, c2; | ||||||
|  |   var r1, r2, r3, r4; | ||||||
|  |   var denom, offset, num; | ||||||
|  |   var x, y; | ||||||
|  |  | ||||||
|  |   // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + | ||||||
|  |   // b1 y + c1 = 0. | ||||||
|  |   a1 = p2.y - p1.y; | ||||||
|  |   b1 = p1.x - p2.x; | ||||||
|  |   c1 = p2.x * p1.y - p1.x * p2.y; | ||||||
|  |  | ||||||
|  |   // Compute r3 and r4. | ||||||
|  |   r3 = a1 * q1.x + b1 * q1.y + c1; | ||||||
|  |   r4 = a1 * q2.x + b1 * q2.y + c1; | ||||||
|  |  | ||||||
|  |   // Check signs of r3 and r4. If both point 3 and point 4 lie on | ||||||
|  |   // same side of line 1, the line segments do not intersect. | ||||||
|  |   if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) { | ||||||
|  |     return /*DON'T_INTERSECT*/; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0 | ||||||
|  |   a2 = q2.y - q1.y; | ||||||
|  |   b2 = q1.x - q2.x; | ||||||
|  |   c2 = q2.x * q1.y - q1.x * q2.y; | ||||||
|  |  | ||||||
|  |   // Compute r1 and r2 | ||||||
|  |   r1 = a2 * p1.x + b2 * p1.y + c2; | ||||||
|  |   r2 = a2 * p2.x + b2 * p2.y + c2; | ||||||
|  |  | ||||||
|  |   // Check signs of r1 and r2. If both point 1 and point 2 lie | ||||||
|  |   // on same side of second line segment, the line segments do | ||||||
|  |   // not intersect. | ||||||
|  |   if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) { | ||||||
|  |     return /*DON'T_INTERSECT*/; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Line segments intersect: compute intersection point. | ||||||
|  |   denom = a1 * b2 - a2 * b1; | ||||||
|  |   if (denom === 0) { | ||||||
|  |     return /*COLLINEAR*/; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   offset = Math.abs(denom / 2); | ||||||
|  |  | ||||||
|  |   // The denom/2 is to get rounding instead of truncating. It | ||||||
|  |   // is added or subtracted to the numerator, depending upon the | ||||||
|  |   // sign of the numerator. | ||||||
|  |   num = b1 * c2 - b2 * c1; | ||||||
|  |   x = num < 0 ? (num - offset) / denom : (num + offset) / denom; | ||||||
|  |  | ||||||
|  |   num = a2 * c1 - a1 * c2; | ||||||
|  |   y = num < 0 ? (num - offset) / denom : (num + offset) / denom; | ||||||
|  |  | ||||||
|  |   return { x: x, y: y }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param r1 | ||||||
|  |  * @param r2 | ||||||
|  |  */ | ||||||
|  | function sameSign(r1, r2) { | ||||||
|  |   return r1 * r2 > 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default intersectLine; | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | /** | ||||||
|  |  * @param node | ||||||
|  |  * @param point | ||||||
|  |  */ | ||||||
|  | function intersectNode(node, point) { | ||||||
|  |   // console.info('Intersect Node'); | ||||||
|  |   return node.intersect(point); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default intersectNode; | ||||||
| @@ -0,0 +1,70 @@ | |||||||
|  | /* eslint "no-console": off */ | ||||||
|  |  | ||||||
|  | import intersectLine from './intersect-line.js'; | ||||||
|  |  | ||||||
|  | export default intersectPolygon; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns the point ({x, y}) at which the point argument intersects with the node argument assuming | ||||||
|  |  * that it has the shape specified by polygon. | ||||||
|  |  * | ||||||
|  |  * @param node | ||||||
|  |  * @param polyPoints | ||||||
|  |  * @param point | ||||||
|  |  */ | ||||||
|  | function intersectPolygon(node, polyPoints, point) { | ||||||
|  |   var x1 = node.x; | ||||||
|  |   var y1 = node.y; | ||||||
|  |  | ||||||
|  |   var intersections = []; | ||||||
|  |  | ||||||
|  |   var minX = Number.POSITIVE_INFINITY; | ||||||
|  |   var minY = Number.POSITIVE_INFINITY; | ||||||
|  |   if (typeof polyPoints.forEach === 'function') { | ||||||
|  |     polyPoints.forEach(function (entry) { | ||||||
|  |       minX = Math.min(minX, entry.x); | ||||||
|  |       minY = Math.min(minY, entry.y); | ||||||
|  |     }); | ||||||
|  |   } else { | ||||||
|  |     minX = Math.min(minX, polyPoints.x); | ||||||
|  |     minY = Math.min(minY, polyPoints.y); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   var left = x1 - node.width / 2 - minX; | ||||||
|  |   var top = y1 - node.height / 2 - minY; | ||||||
|  |  | ||||||
|  |   for (var i = 0; i < polyPoints.length; i++) { | ||||||
|  |     var p1 = polyPoints[i]; | ||||||
|  |     var p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0]; | ||||||
|  |     var intersect = intersectLine( | ||||||
|  |       node, | ||||||
|  |       point, | ||||||
|  |       { x: left + p1.x, y: top + p1.y }, | ||||||
|  |       { x: left + p2.x, y: top + p2.y } | ||||||
|  |     ); | ||||||
|  |     if (intersect) { | ||||||
|  |       intersections.push(intersect); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!intersections.length) { | ||||||
|  |     // console.log('NO INTERSECTION FOUND, RETURN NODE CENTER', node); | ||||||
|  |     return node; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (intersections.length > 1) { | ||||||
|  |     // More intersections, find the one nearest to edge end point | ||||||
|  |     intersections.sort(function (p, q) { | ||||||
|  |       var pdx = p.x - point.x; | ||||||
|  |       var pdy = p.y - point.y; | ||||||
|  |       var distp = Math.sqrt(pdx * pdx + pdy * pdy); | ||||||
|  |  | ||||||
|  |       var qdx = q.x - point.x; | ||||||
|  |       var qdy = q.y - point.y; | ||||||
|  |       var distq = Math.sqrt(qdx * qdx + qdy * qdy); | ||||||
|  |  | ||||||
|  |       return distp < distq ? -1 : distp === distq ? 0 : 1; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   return intersections[0]; | ||||||
|  | } | ||||||
| @@ -0,0 +1,32 @@ | |||||||
|  | const intersectRect = (node, point) => { | ||||||
|  |   var x = node.x; | ||||||
|  |   var y = node.y; | ||||||
|  |  | ||||||
|  |   // Rectangle intersection algorithm from: | ||||||
|  |   // https://math.stackexchange.com/questions/108113/find-edge-between-two-boxes | ||||||
|  |   var dx = point.x - x; | ||||||
|  |   var dy = point.y - y; | ||||||
|  |   var w = node.width / 2; | ||||||
|  |   var h = node.height / 2; | ||||||
|  |  | ||||||
|  |   var sx, sy; | ||||||
|  |   if (Math.abs(dy) * w > Math.abs(dx) * h) { | ||||||
|  |     // Intersection is top or bottom of rect. | ||||||
|  |     if (dy < 0) { | ||||||
|  |       h = -h; | ||||||
|  |     } | ||||||
|  |     sx = dy === 0 ? 0 : (h * dx) / dy; | ||||||
|  |     sy = h; | ||||||
|  |   } else { | ||||||
|  |     // Intersection is left or right of rect. | ||||||
|  |     if (dx < 0) { | ||||||
|  |       w = -w; | ||||||
|  |     } | ||||||
|  |     sx = w; | ||||||
|  |     sy = dx === 0 ? 0 : (w * dy) / dx; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { x: x + sx, y: y + sy }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default intersectRect; | ||||||
| @@ -0,0 +1,293 @@ | |||||||
|  | /** Setup arrow head and define the marker. The result is appended to the svg. */ | ||||||
|  | import { log } from '$root/logger.js'; | ||||||
|  |  | ||||||
|  | // Only add the number of markers that the diagram needs | ||||||
|  | const insertMarkers = (elem, markerArray, type, id) => { | ||||||
|  |   markerArray.forEach((markerName) => { | ||||||
|  |     markers[markerName](elem, type, id); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const extension = (elem, type, id) => { | ||||||
|  |   log.trace('Making markers for ', id); | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-extensionStart') | ||||||
|  |     .attr('class', 'marker extension ' + type) | ||||||
|  |     .attr('refX', 18) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 190) | ||||||
|  |     .attr('markerHeight', 240) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 1,7 L18,13 V 1 Z'); | ||||||
|  |  | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-extensionEnd') | ||||||
|  |     .attr('class', 'marker extension ' + type) | ||||||
|  |     .attr('refX', 1) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 20) | ||||||
|  |     .attr('markerHeight', 28) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const composition = (elem, type, id) => { | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-compositionStart') | ||||||
|  |     .attr('class', 'marker composition ' + type) | ||||||
|  |     .attr('refX', 18) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 190) | ||||||
|  |     .attr('markerHeight', 240) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); | ||||||
|  |  | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-compositionEnd') | ||||||
|  |     .attr('class', 'marker composition ' + type) | ||||||
|  |     .attr('refX', 1) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 20) | ||||||
|  |     .attr('markerHeight', 28) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); | ||||||
|  | }; | ||||||
|  | const aggregation = (elem, type, id) => { | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-aggregationStart') | ||||||
|  |     .attr('class', 'marker aggregation ' + type) | ||||||
|  |     .attr('refX', 18) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 190) | ||||||
|  |     .attr('markerHeight', 240) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); | ||||||
|  |  | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-aggregationEnd') | ||||||
|  |     .attr('class', 'marker aggregation ' + type) | ||||||
|  |     .attr('refX', 1) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 20) | ||||||
|  |     .attr('markerHeight', 28) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); | ||||||
|  | }; | ||||||
|  | const dependency = (elem, type, id) => { | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-dependencyStart') | ||||||
|  |     .attr('class', 'marker dependency ' + type) | ||||||
|  |     .attr('refX', 6) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 190) | ||||||
|  |     .attr('markerHeight', 240) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z'); | ||||||
|  |  | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-dependencyEnd') | ||||||
|  |     .attr('class', 'marker dependency ' + type) | ||||||
|  |     .attr('refX', 13) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 20) | ||||||
|  |     .attr('markerHeight', 28) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); | ||||||
|  | }; | ||||||
|  | const lollipop = (elem, type, id) => { | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-lollipopStart') | ||||||
|  |     .attr('class', 'marker lollipop ' + type) | ||||||
|  |     .attr('refX', 13) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 190) | ||||||
|  |     .attr('markerHeight', 240) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('circle') | ||||||
|  |     .attr('stroke', 'black') | ||||||
|  |     .attr('fill', 'transparent') | ||||||
|  |     .attr('cx', 7) | ||||||
|  |     .attr('cy', 7) | ||||||
|  |     .attr('r', 6); | ||||||
|  |  | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-lollipopEnd') | ||||||
|  |     .attr('class', 'marker lollipop ' + type) | ||||||
|  |     .attr('refX', 1) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 190) | ||||||
|  |     .attr('markerHeight', 240) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('circle') | ||||||
|  |     .attr('stroke', 'black') | ||||||
|  |     .attr('fill', 'transparent') | ||||||
|  |     .attr('cx', 7) | ||||||
|  |     .attr('cy', 7) | ||||||
|  |     .attr('r', 6); | ||||||
|  | }; | ||||||
|  | const point = (elem, type, id) => { | ||||||
|  |   elem | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-pointEnd') | ||||||
|  |     .attr('class', 'marker ' + type) | ||||||
|  |     .attr('viewBox', '0 0 10 10') | ||||||
|  |     .attr('refX', 6) | ||||||
|  |     .attr('refY', 5) | ||||||
|  |     .attr('markerUnits', 'userSpaceOnUse') | ||||||
|  |     .attr('markerWidth', 12) | ||||||
|  |     .attr('markerHeight', 12) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 0 0 L 10 5 L 0 10 z') | ||||||
|  |     .attr('class', 'arrowMarkerPath') | ||||||
|  |     .style('stroke-width', 1) | ||||||
|  |     .style('stroke-dasharray', '1,0'); | ||||||
|  |   elem | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-pointStart') | ||||||
|  |     .attr('class', 'marker ' + type) | ||||||
|  |     .attr('viewBox', '0 0 10 10') | ||||||
|  |     .attr('refX', 4.5) | ||||||
|  |     .attr('refY', 5) | ||||||
|  |     .attr('markerUnits', 'userSpaceOnUse') | ||||||
|  |     .attr('markerWidth', 12) | ||||||
|  |     .attr('markerHeight', 12) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 0 5 L 10 10 L 10 0 z') | ||||||
|  |     .attr('class', 'arrowMarkerPath') | ||||||
|  |     .style('stroke-width', 1) | ||||||
|  |     .style('stroke-dasharray', '1,0'); | ||||||
|  | }; | ||||||
|  | const circle = (elem, type, id) => { | ||||||
|  |   elem | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-circleEnd') | ||||||
|  |     .attr('class', 'marker ' + type) | ||||||
|  |     .attr('viewBox', '0 0 10 10') | ||||||
|  |     .attr('refX', 11) | ||||||
|  |     .attr('refY', 5) | ||||||
|  |     .attr('markerUnits', 'userSpaceOnUse') | ||||||
|  |     .attr('markerWidth', 11) | ||||||
|  |     .attr('markerHeight', 11) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('circle') | ||||||
|  |     .attr('cx', '5') | ||||||
|  |     .attr('cy', '5') | ||||||
|  |     .attr('r', '5') | ||||||
|  |     .attr('class', 'arrowMarkerPath') | ||||||
|  |     .style('stroke-width', 1) | ||||||
|  |     .style('stroke-dasharray', '1,0'); | ||||||
|  |  | ||||||
|  |   elem | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-circleStart') | ||||||
|  |     .attr('class', 'marker ' + type) | ||||||
|  |     .attr('viewBox', '0 0 10 10') | ||||||
|  |     .attr('refX', -1) | ||||||
|  |     .attr('refY', 5) | ||||||
|  |     .attr('markerUnits', 'userSpaceOnUse') | ||||||
|  |     .attr('markerWidth', 11) | ||||||
|  |     .attr('markerHeight', 11) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('circle') | ||||||
|  |     .attr('cx', '5') | ||||||
|  |     .attr('cy', '5') | ||||||
|  |     .attr('r', '5') | ||||||
|  |     .attr('class', 'arrowMarkerPath') | ||||||
|  |     .style('stroke-width', 1) | ||||||
|  |     .style('stroke-dasharray', '1,0'); | ||||||
|  | }; | ||||||
|  | const cross = (elem, type, id) => { | ||||||
|  |   elem | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-crossEnd') | ||||||
|  |     .attr('class', 'marker cross ' + type) | ||||||
|  |     .attr('viewBox', '0 0 11 11') | ||||||
|  |     .attr('refX', 12) | ||||||
|  |     .attr('refY', 5.2) | ||||||
|  |     .attr('markerUnits', 'userSpaceOnUse') | ||||||
|  |     .attr('markerWidth', 11) | ||||||
|  |     .attr('markerHeight', 11) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     // .attr('stroke', 'black') | ||||||
|  |     .attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9') | ||||||
|  |     .attr('class', 'arrowMarkerPath') | ||||||
|  |     .style('stroke-width', 2) | ||||||
|  |     .style('stroke-dasharray', '1,0'); | ||||||
|  |  | ||||||
|  |   elem | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-crossStart') | ||||||
|  |     .attr('class', 'marker cross ' + type) | ||||||
|  |     .attr('viewBox', '0 0 11 11') | ||||||
|  |     .attr('refX', -1) | ||||||
|  |     .attr('refY', 5.2) | ||||||
|  |     .attr('markerUnits', 'userSpaceOnUse') | ||||||
|  |     .attr('markerWidth', 11) | ||||||
|  |     .attr('markerHeight', 11) | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     // .attr('stroke', 'black') | ||||||
|  |     .attr('d', 'M 1,1 l 9,9 M 10,1 l -9,9') | ||||||
|  |     .attr('class', 'arrowMarkerPath') | ||||||
|  |     .style('stroke-width', 2) | ||||||
|  |     .style('stroke-dasharray', '1,0'); | ||||||
|  | }; | ||||||
|  | const barb = (elem, type, id) => { | ||||||
|  |   elem | ||||||
|  |     .append('defs') | ||||||
|  |     .append('marker') | ||||||
|  |     .attr('id', id + '_' + type + '-barbEnd') | ||||||
|  |     .attr('refX', 19) | ||||||
|  |     .attr('refY', 7) | ||||||
|  |     .attr('markerWidth', 20) | ||||||
|  |     .attr('markerHeight', 14) | ||||||
|  |     .attr('markerUnits', 'strokeWidth') | ||||||
|  |     .attr('orient', 'auto') | ||||||
|  |     .append('path') | ||||||
|  |     .attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // TODO rename the class diagram markers to something shape descriptive and semantic free | ||||||
|  | const markers = { | ||||||
|  |   extension, | ||||||
|  |   composition, | ||||||
|  |   aggregation, | ||||||
|  |   dependency, | ||||||
|  |   lollipop, | ||||||
|  |   point, | ||||||
|  |   circle, | ||||||
|  |   cross, | ||||||
|  |   barb, | ||||||
|  | }; | ||||||
|  | export default insertMarkers; | ||||||
| @@ -0,0 +1,83 @@ | |||||||
|  | import { log } from '$root/logger.js'; | ||||||
|  | import { rect } from './shapes/rect.js'; | ||||||
|  | import { getConfig } from '$root/diagram-api/diagramAPI.js'; | ||||||
|  |  | ||||||
|  | const formatClass = (str) => { | ||||||
|  |   if (str) { | ||||||
|  |     return ' ' + str; | ||||||
|  |   } | ||||||
|  |   return ''; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const shapes = { | ||||||
|  |   rect, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | let nodeElems = {}; | ||||||
|  |  | ||||||
|  | export const insertNode = async (elem, node, dir) => { | ||||||
|  |   let newEl; | ||||||
|  |   let el; | ||||||
|  |  | ||||||
|  |   console.log('insertNode element', elem, elem.node(), rect); | ||||||
|  |   // debugger; | ||||||
|  |   // Add link when appropriate | ||||||
|  |   if (node.link) { | ||||||
|  |     let target; | ||||||
|  |     if (getConfig().securityLevel === 'sandbox') { | ||||||
|  |       target = '_top'; | ||||||
|  |     } else if (node.linkTarget) { | ||||||
|  |       target = node.linkTarget || '_blank'; | ||||||
|  |     } | ||||||
|  |     newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target); | ||||||
|  |     el = await shapes[node.shape](newEl, node, dir); | ||||||
|  |   } else { | ||||||
|  |     el = await shapes[node.shape](elem, node, dir); | ||||||
|  |     newEl = el; | ||||||
|  |   } | ||||||
|  |   if (node.tooltip) { | ||||||
|  |     el.attr('title', node.tooltip); | ||||||
|  |   } | ||||||
|  |   if (node.class) { | ||||||
|  |     el.attr('class', 'node default ' + node.class); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   nodeElems[node.id] = newEl; | ||||||
|  |  | ||||||
|  |   if (node.haveCallback) { | ||||||
|  |     nodeElems[node.id].attr('class', nodeElems[node.id].attr('class') + ' clickable'); | ||||||
|  |   } | ||||||
|  |   return newEl; | ||||||
|  | }; | ||||||
|  | export const setNodeElem = (elem, node) => { | ||||||
|  |   nodeElems[node.id] = elem; | ||||||
|  | }; | ||||||
|  | export const clear = () => { | ||||||
|  |   nodeElems = {}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const positionNode = (node) => { | ||||||
|  |   const el = nodeElems[node.id]; | ||||||
|  |  | ||||||
|  |   log.trace( | ||||||
|  |     'Transforming node', | ||||||
|  |     node.diff, | ||||||
|  |     node, | ||||||
|  |     'translate(' + (node.x - node.width / 2 - 5) + ', ' + node.width / 2 + ')' | ||||||
|  |   ); | ||||||
|  |   const padding = 8; | ||||||
|  |   const diff = node.diff || 0; | ||||||
|  |   if (node.clusterNode) { | ||||||
|  |     el.attr( | ||||||
|  |       'transform', | ||||||
|  |       'translate(' + | ||||||
|  |         (node.x + diff - node.width / 2) + | ||||||
|  |         ', ' + | ||||||
|  |         (node.y - node.height / 2 - padding) + | ||||||
|  |         ')' | ||||||
|  |     ); | ||||||
|  |   } else { | ||||||
|  |     el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')'); | ||||||
|  |   } | ||||||
|  |   return diff; | ||||||
|  | }; | ||||||
| @@ -0,0 +1,126 @@ | |||||||
|  | import { log } from '$root/logger.js'; | ||||||
|  | import { labelHelper, updateNodeBounds } from './util.js'; | ||||||
|  | import intersect from '../intersect/index.js'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * | ||||||
|  |  * @param rect | ||||||
|  |  * @param borders | ||||||
|  |  * @param totalWidth | ||||||
|  |  * @param totalHeight | ||||||
|  |  */ | ||||||
|  | function applyNodePropertyBorders(rect, borders, totalWidth, totalHeight) { | ||||||
|  |   const strokeDashArray = []; | ||||||
|  |   const addBorder = (length) => { | ||||||
|  |     strokeDashArray.push(length, 0); | ||||||
|  |   }; | ||||||
|  |   const skipBorder = (length) => { | ||||||
|  |     strokeDashArray.push(0, length); | ||||||
|  |   }; | ||||||
|  |   if (borders.includes('t')) { | ||||||
|  |     log.debug('add top border'); | ||||||
|  |     addBorder(totalWidth); | ||||||
|  |   } else { | ||||||
|  |     skipBorder(totalWidth); | ||||||
|  |   } | ||||||
|  |   if (borders.includes('r')) { | ||||||
|  |     log.debug('add right border'); | ||||||
|  |     addBorder(totalHeight); | ||||||
|  |   } else { | ||||||
|  |     skipBorder(totalHeight); | ||||||
|  |   } | ||||||
|  |   if (borders.includes('b')) { | ||||||
|  |     log.debug('add bottom border'); | ||||||
|  |     addBorder(totalWidth); | ||||||
|  |   } else { | ||||||
|  |     skipBorder(totalWidth); | ||||||
|  |   } | ||||||
|  |   if (borders.includes('l')) { | ||||||
|  |     log.debug('add left border'); | ||||||
|  |     addBorder(totalHeight); | ||||||
|  |   } else { | ||||||
|  |     skipBorder(totalHeight); | ||||||
|  |   } | ||||||
|  |   rect.attr('stroke-dasharray', strokeDashArray.join(' ')); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const rect = async (parent, node) => { | ||||||
|  |   const { shapeSvg, bbox, halfPadding } = await labelHelper( | ||||||
|  |     parent, | ||||||
|  |     node, | ||||||
|  |     'node ' + node.classes + ' ' + node.class, | ||||||
|  |     true | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   console.log('rect node', node); | ||||||
|  |  | ||||||
|  |   // add the rect | ||||||
|  |   const rect = shapeSvg.insert('rect', ':first-child'); | ||||||
|  |  | ||||||
|  |   const totalWidth = bbox.width + node.padding; | ||||||
|  |   const totalHeight = bbox.height + node.padding; | ||||||
|  |  | ||||||
|  |   rect | ||||||
|  |     .attr('class', 'basic label-container') | ||||||
|  |     .attr('style', node.style) | ||||||
|  |     .attr('rx', node.rx) | ||||||
|  |     .attr('ry', node.ry) | ||||||
|  |     // .attr('x', -bbox.width / 2 - node.padding) | ||||||
|  |     // .attr('y', -bbox.height / 2 - node.padding) | ||||||
|  |     .attr('x', -bbox.width / 2 - halfPadding) | ||||||
|  |     .attr('y', -bbox.height / 2 - halfPadding) | ||||||
|  |     .attr('width', totalWidth) | ||||||
|  |     .attr('height', totalHeight); | ||||||
|  |  | ||||||
|  |   if (node.props) { | ||||||
|  |     const propKeys = new Set(Object.keys(node.props)); | ||||||
|  |     if (node.props.borders) { | ||||||
|  |       applyNodePropertyBorders(rect, node.props.borders, totalWidth, totalHeight); | ||||||
|  |       propKeys.delete('borders'); | ||||||
|  |     } | ||||||
|  |     propKeys.forEach((propKey) => { | ||||||
|  |       log.warn(`Unknown node property ${propKey}`); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   updateNodeBounds(node, rect); | ||||||
|  |  | ||||||
|  |   node.intersect = function (point) { | ||||||
|  |     return intersect.rect(node, point); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return shapeSvg; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const labelRect = async (parent, node) => { | ||||||
|  |   const { shapeSvg } = await labelHelper(parent, node, 'label', true); | ||||||
|  |  | ||||||
|  |   log.trace('Classes = ', node.class); | ||||||
|  |   // add the rect | ||||||
|  |   const rect = shapeSvg.insert('rect', ':first-child'); | ||||||
|  |  | ||||||
|  |   // Hide the rect we are only after the label | ||||||
|  |   const totalWidth = 0; | ||||||
|  |   const totalHeight = 0; | ||||||
|  |   rect.attr('width', totalWidth).attr('height', totalHeight); | ||||||
|  |   shapeSvg.attr('class', 'label edgeLabel'); | ||||||
|  |  | ||||||
|  |   if (node.props) { | ||||||
|  |     const propKeys = new Set(Object.keys(node.props)); | ||||||
|  |     if (node.props.borders) { | ||||||
|  |       applyNodePropertyBorders(rect, node.props.borders, totalWidth, totalHeight); | ||||||
|  |       propKeys.delete('borders'); | ||||||
|  |     } | ||||||
|  |     propKeys.forEach((propKey) => { | ||||||
|  |       log.warn(`Unknown node property ${propKey}`); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   updateNodeBounds(node, rect); | ||||||
|  |  | ||||||
|  |   node.intersect = function (point) { | ||||||
|  |     return intersect.rect(node, point); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return shapeSvg; | ||||||
|  | }; | ||||||
| @@ -0,0 +1,146 @@ | |||||||
|  | import createLabel from '../createLabel.js'; | ||||||
|  | import { createText } from '$root/rendering-util/createText.ts'; | ||||||
|  | import { getConfig } from '$root/diagram-api/diagramAPI.js'; | ||||||
|  | import { select } from 'd3'; | ||||||
|  | import { evaluate, sanitizeText } from '$root/diagrams/common/common.js'; | ||||||
|  | import { decodeEntities } from '$root/utils.js'; | ||||||
|  |  | ||||||
|  | export const labelHelper = async (parent, node, _classes, isNode) => { | ||||||
|  |   let classes; | ||||||
|  |   const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig().flowchart.htmlLabels); | ||||||
|  |   if (!_classes) { | ||||||
|  |     classes = 'node default'; | ||||||
|  |   } else { | ||||||
|  |     classes = _classes; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Add outer g element | ||||||
|  |   const shapeSvg = parent | ||||||
|  |     .insert('g') | ||||||
|  |     .attr('class', classes) | ||||||
|  |     .attr('id', node.domId || node.id); | ||||||
|  |  | ||||||
|  |   // Create the label and insert it after the rect | ||||||
|  |   const label = shapeSvg.insert('g').attr('class', 'label').attr('style', node.labelStyle); | ||||||
|  |  | ||||||
|  |   // Replace labelText with default value if undefined | ||||||
|  |   let labelText; | ||||||
|  |   if (node.labelText === undefined) { | ||||||
|  |     labelText = ''; | ||||||
|  |   } else { | ||||||
|  |     labelText = typeof node.labelText === 'string' ? node.labelText : node.labelText[0]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const textNode = label.node(); | ||||||
|  |   let text; | ||||||
|  |   if (node.labelType === 'markdown') { | ||||||
|  |     // text = textNode; | ||||||
|  |     text = createText(label, sanitizeText(decodeEntities(labelText), getConfig()), { | ||||||
|  |       useHtmlLabels, | ||||||
|  |       width: node.width || getConfig().flowchart.wrappingWidth, | ||||||
|  |       classes: 'markdown-node-label', | ||||||
|  |     }); | ||||||
|  |   } else { | ||||||
|  |     text = textNode.appendChild( | ||||||
|  |       createLabel( | ||||||
|  |         sanitizeText(decodeEntities(labelText), getConfig()), | ||||||
|  |         node.labelStyle, | ||||||
|  |         false, | ||||||
|  |         isNode | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   // Get the size of the label | ||||||
|  |   let bbox = text.getBBox(); | ||||||
|  |   const halfPadding = node.padding / 2; | ||||||
|  |  | ||||||
|  |   if (evaluate(getConfig().flowchart.htmlLabels)) { | ||||||
|  |     const div = text.children[0]; | ||||||
|  |     const dv = select(text); | ||||||
|  |  | ||||||
|  |     // if there are images, need to wait for them to load before getting the bounding box | ||||||
|  |     const images = div.getElementsByTagName('img'); | ||||||
|  |     if (images) { | ||||||
|  |       const noImgText = labelText.replace(/<img[^>]*>/g, '').trim() === ''; | ||||||
|  |  | ||||||
|  |       await Promise.all( | ||||||
|  |         [...images].map( | ||||||
|  |           (img) => | ||||||
|  |             new Promise((res) => { | ||||||
|  |               /** | ||||||
|  |                * | ||||||
|  |                */ | ||||||
|  |               function setupImage() { | ||||||
|  |                 img.style.display = 'flex'; | ||||||
|  |                 img.style.flexDirection = 'column'; | ||||||
|  |  | ||||||
|  |                 if (noImgText) { | ||||||
|  |                   // default size if no text | ||||||
|  |                   const bodyFontSize = getConfig().fontSize | ||||||
|  |                     ? getConfig().fontSize | ||||||
|  |                     : window.getComputedStyle(document.body).fontSize; | ||||||
|  |                   const enlargingFactor = 5; | ||||||
|  |                   const width = parseInt(bodyFontSize, 10) * enlargingFactor + 'px'; | ||||||
|  |                   img.style.minWidth = width; | ||||||
|  |                   img.style.maxWidth = width; | ||||||
|  |                 } else { | ||||||
|  |                   img.style.width = '100%'; | ||||||
|  |                 } | ||||||
|  |                 res(img); | ||||||
|  |               } | ||||||
|  |               setTimeout(() => { | ||||||
|  |                 if (img.complete) { | ||||||
|  |                   setupImage(); | ||||||
|  |                 } | ||||||
|  |               }); | ||||||
|  |               img.addEventListener('error', setupImage); | ||||||
|  |               img.addEventListener('load', setupImage); | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     bbox = div.getBoundingClientRect(); | ||||||
|  |     dv.attr('width', bbox.width); | ||||||
|  |     dv.attr('height', bbox.height); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Center the label | ||||||
|  |   if (useHtmlLabels) { | ||||||
|  |     label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')'); | ||||||
|  |   } else { | ||||||
|  |     label.attr('transform', 'translate(' + 0 + ', ' + -bbox.height / 2 + ')'); | ||||||
|  |   } | ||||||
|  |   if (node.centerLabel) { | ||||||
|  |     label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')'); | ||||||
|  |   } | ||||||
|  |   label.insert('rect', ':first-child'); | ||||||
|  |   return { shapeSvg, bbox, halfPadding, label }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const updateNodeBounds = (node, element) => { | ||||||
|  |   const bbox = element.node().getBBox(); | ||||||
|  |   node.width = bbox.width; | ||||||
|  |   node.height = bbox.height; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param parent | ||||||
|  |  * @param w | ||||||
|  |  * @param h | ||||||
|  |  * @param points | ||||||
|  |  */ | ||||||
|  | export function insertPolygonShape(parent, w, h, points) { | ||||||
|  |   return parent | ||||||
|  |     .insert('polygon', ':first-child') | ||||||
|  |     .attr( | ||||||
|  |       'points', | ||||||
|  |       points | ||||||
|  |         .map(function (d) { | ||||||
|  |           return d.x + ',' + d.y; | ||||||
|  |         }) | ||||||
|  |         .join(' ') | ||||||
|  |     ) | ||||||
|  |     .attr('class', 'label-container') | ||||||
|  |     .attr('transform', 'translate(' + -w / 2 + ',' + h / 2 + ')'); | ||||||
|  | } | ||||||
| @@ -3,7 +3,12 @@ | |||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "rootDir": "./src", |     "rootDir": "./src", | ||||||
|     "outDir": "./dist", |     "outDir": "./dist", | ||||||
|     "types": ["vitest/importMeta", "vitest/globals"] |     "types": ["vitest/importMeta", "vitest/globals"], | ||||||
|   }, |     "baseUrl": ".", // This must be set if "paths" is set | ||||||
|   "include": ["./src/**/*.ts", "./package.json"] |     "paths": { | ||||||
|  |       "$root/*": ["src/*"] | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |   }, | ||||||
|  |   "include": ["./src/**/*.ts", "./package.json"], | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,10 +2,15 @@ import jison from './.vite/jisonPlugin.js'; | |||||||
| import jsonSchemaPlugin from './.vite/jsonSchemaPlugin.js'; | import jsonSchemaPlugin from './.vite/jsonSchemaPlugin.js'; | ||||||
| import typescript from '@rollup/plugin-typescript'; | import typescript from '@rollup/plugin-typescript'; | ||||||
| import { defaultExclude, defineConfig } from 'vitest/config'; | import { defaultExclude, defineConfig } from 'vitest/config'; | ||||||
|  | import path from 'path'; | ||||||
|  |  | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   resolve: { |   resolve: { | ||||||
|     extensions: ['.js'], |     extensions: ['.js'], | ||||||
|  |     alias: { | ||||||
|  |       // Define your alias here | ||||||
|  |       '$root/*': path.resolve(__dirname, 'src/*'), | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   plugins: [ |   plugins: [ | ||||||
|     jison(), |     jison(), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Knut Sveidqvist
					Knut Sveidqvist