From 13b863115e88f8c0ccbaed67a9b3c4037b1e4b74 Mon Sep 17 00:00:00 2001 From: STEINNI Date: Wed, 21 Jan 2026 13:49:15 +0000 Subject: [PATCH 1/2] keep if changing mind... --- bzGraflow.js | 139 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 89 insertions(+), 50 deletions(-) diff --git a/bzGraflow.js b/bzGraflow.js index ca7ffbc..64b27e5 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -73,7 +73,6 @@ class BZgraflow extends Buildoz{ for(const tpl of doc.querySelectorAll('template')){ if(tpl.id=='svg-arrows'){ this.arrowDefs = tpl.querySelector('defs').cloneNode(true) - console.log(this.arrowDefs) this.wiresContainer.appendChild(this.arrowDefs) } else { const rootEl = tpl.content.querySelector('.bzgf-node') @@ -157,15 +156,15 @@ class BZgraflow extends Buildoz{ bezier(idNode1, idPort1, idNode2, idPort2, tensionMin=60) { const svgRect = this.wiresContainer.getBoundingClientRect() const node1 = this.stagedNodes[idNode1] - const port1 = node1.ports[idPort1] + const port1 = idPort1 ? node1.ports[idPort1] : null const node2 = this.stagedNodes[idNode2] - const port2 = node2.ports[idPort2] - if(!node1 || !node2 || !port1 || !port2) { - console.warn('Link on bad node / port!', idNode1, idPort1, idNode2, idPort2) + const port2 = idPort2 ? node2.ports[idPort2] : null + if(!node1 || !node2) { + console.warn('Link on bad node ', idNode1, idNode2) return('') } - const bb1 = port1.el.getBoundingClientRect() - const bb2 = port2.el.getBoundingClientRect() + const bb1 = port1 ? port1.el.getBoundingClientRect() : node1.getBoundingClientRect() + const bb2 = port2 ? port2.el.getBoundingClientRect() : node2.getBoundingClientRect() const x1 = Math.floor(bb1.x + (bb1.width/2)) - svgRect.left const y1 = Math.floor(bb1.y + (bb1.height/2)) - svgRect.top const x2 = Math.floor(bb2.x + (bb2.width/2)) - svgRect.left @@ -194,7 +193,7 @@ class BZgraflow extends Buildoz{ c2y -= 1*tension } } - return `M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}` + return(`M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`) } autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=1000){ @@ -227,6 +226,16 @@ class BZgraflow extends Buildoz{ layerWidths.push(totWidth) } + this.flow.longLinks = this.findLongLinks(this.flow.links) + console.log('==============================>longLinks:', this.flow.longLinks) + for(const link of this.flow.longLinks){ + console.log('==============================>longLink:', link) + for(const layerIdx of link.skippedLayers){ + const nid = `longLinkPlaceHolder_${crypto.randomUUID()}` + layers[layerIdx].push(nid) + link.interNodes.push(nid) + } + } this.reorderLayers(layers, parents, indexes, orientation) if(orientation=='horizontal'){ @@ -235,10 +244,17 @@ class BZgraflow extends Buildoz{ let wMax = 0 let y = ((maxHeight - layerHeights[idx]) / 2) + gapy for(const nid of layer){ - const bb = this.stagedNodes[nid].getBoundingClientRect() - wMax = (bb.width > wMax) ? bb.width : wMax - this.moveNode(nid, x, y, tween) - y += gapy + bb.height + if(!nid.startsWith('longLinkPlaceHolder_')){ + const bb = this.stagedNodes[nid].getBoundingClientRect() + wMax = (bb.width > wMax) ? bb.width : wMax + this.moveNode(nid, x, y, tween) + y += gapy + bb.height + } else { + console.log('longLinkPlaceHolder', layer) + // Maybe center x here (but we don't have full wmax yet) + this.moveNode(nid, x, y, tween) + y += gapy + 50 // TODO: fix this + } } x += wMax + gapx } @@ -248,10 +264,17 @@ class BZgraflow extends Buildoz{ let hMax = 0 let x = ((maxWidth - layerWidths[idx]) / 2) + gapx for(const nid of layer){ - const bb = this.stagedNodes[nid].getBoundingClientRect() - hMax = (bb.height > hMax) ? bb.height : hMax - this.moveNode(nid, x, y, tween) - x += gapx + bb.width + if(!nid.startsWith('longLinkPlaceHolder_')){ + const bb = this.stagedNodes[nid].getBoundingClientRect() + hMax = (bb.height > hMax) ? bb.height : hMax + this.moveNode(nid, x, y, tween) + x += gapx + bb.width + } else { + console.log('longLinkPlaceHolder', layer) + // Maybe center y here (but we don't have full hmax yet) + this.moveNode(nid, x, y, tween) + x += gapx + 50 // TODO: fix this + } } y += hMax + gapy } @@ -298,10 +321,12 @@ class BZgraflow extends Buildoz{ const toSwap = [] for(let i=0; i layers[lidx-1].includes(nid))) for(let j=i+1; j layers[lidx-1].includes(nid))) const link1 = (pnid1) ? this.getLink(pnid1, nid1) : null const link2 = (pnid2) ? this.getLink(pnid2, nid2) : null @@ -345,8 +370,22 @@ class BZgraflow extends Buildoz{ for(const wire of wires){ const [nid1, nid2] = wire.dataset.id.split('_') const lnk = this.getLink(nid1, nid2) - const path = this.bezier(nid1, lnk.from[1], nid2, lnk.to[1], this.getBZAttribute('tension')) - wire.setAttribute('d', path) + const longLink = this.flow.longLinks.find(item => (item.link.from[0] == lnk.from[0] && item.link.from[1] == lnk.from[1] && item.link.to[0] == lnk.to[0] && item.link.to[1] == lnk.to[1])) + if(longLink) { + console.log('==============================>WIRE:', wire) + let lastNid = nid1; let lastPort = lnk.from[1]; + for(const interNode of longLink.interNodes){ + const path = this.bezier(lastNid, lastPort, interNode, null, this.getBZAttribute('tension')) + wire.setAttribute('d', path) + // TODO: add/draw intermediary wire + lastNid = interNode; lastPort = null; + } + const path = this.bezier(lastNid, lastPort, nid2, lnk.to[1], this.getBZAttribute('tension')) + wire.setAttribute('d', path) + } else { + const path = this.bezier(nid1, lnk.from[1], nid2, lnk.to[1], this.getBZAttribute('tension')) + wire.setAttribute('d', path) + } } } @@ -354,7 +393,7 @@ class BZgraflow extends Buildoz{ return(this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2)))) } - buildGraphStructures(nodes, links) { + buildGraphStructures(nodes, links, includeLinkIndexes = false) { const parents = {} const adj = {} nodes.forEach(n => { @@ -362,14 +401,19 @@ class BZgraflow extends Buildoz{ adj[n.id] = [] }) - links.forEach(link => { + links.forEach((link, idx) => { const from = link.from[0] - const to = link.to[0] - parents[to].push(from) - adj[from].push(to) + const to = link.to[0] + if(link.from[0] !== link.to[0]) { // Skip self-loops + parents[to].push(from) + if(includeLinkIndexes) { + adj[from].push({ to, linkIdx: idx }) + } else { + adj[from].push(to) + } + } }) - - return { parents, adj } + return({ parents, adj }) } computeLayers(nodes, parents) { @@ -394,14 +438,9 @@ class BZgraflow extends Buildoz{ } hasAnyLoop(nodes, links) { - // self-loops - if(this.flow.links.some(l => l.from[0] === l.to[0])) return(true) + if(links.some(l => l.from[0] === l.to[0])) return(true) // self-loops - // multi-node cycles - const adj = {}; - this.flow.nodes.forEach(node => adj[node.id] = []) - this.flow.links.forEach(link => adj[link.from[0]].push(link.to[0])) - + const { adj } = this.buildGraphStructures(nodes, links) const visiting = new Set(); const visited = new Set() const dfs = (nid) => { @@ -420,22 +459,12 @@ class BZgraflow extends Buildoz{ visited.add(nid) return(false) } - return(this.flow.nodes.map(n => n.id).some(dfs)) + return(nodes.map(n => n.id).some(dfs)) } findBackEdges(nodes, links) { - // Build adjacency list with link indexes - const adj = {}; - nodes.forEach(node => adj[node.id] = []) - for(let [idx, link] of links.entries()){ - if(link.from[0] !== link.to[0]) { // Skip self-loops - adj[link.from[0]].push({ to: link.to[0], linkIdx: idx }) - } - } - - const color = {} - nodes.forEach(n => color[n.id] = 'white') - + const { adj } = this.buildGraphStructures(nodes, links, true) + const color = {}; nodes.forEach(n => color[n.id] = 'white') const backEdges = [] function dfs(u) { @@ -456,22 +485,32 @@ class BZgraflow extends Buildoz{ if (color[n.id] === 'white') dfs(n.id) }) - return backEdges + return(backEdges) } - findCrossLayerLinks(links) { + findLongLinks(links) { const { parents } = this.buildGraphStructures(this.flow.nodes, links) const layers = this.computeLayers(this.flow.nodes, parents) const crossLayerLinks = [] for(const link of links){ const from = link.from[0] const to = link.to[0] - if(layers[from] !== layers[to]) { - crossLayerLinks.push(link) + const idx1 = layers.findIndex(layer => layer.includes(from)) + const idx2 = layers.findIndex(layer => layer.includes(to)) + if(Math.abs(idx1-idx2)>1) { + const lowerIdx = idx1idx2 ? idx1 : idx2 + crossLayerLinks.push({ + link, + linkIdx: link.linkIdx, + interNodes: [], + skippedLayers: Array.from({ length: higherIdx - lowerIdx - 1 }, (_, i) => lowerIdx + i + 1), + }) } } - return crossLayerLinks + return(crossLayerLinks) } } + Buildoz.define('graflow', BZgraflow) From ad86da66276a56f4f7ee3b325e1cb329b84aae1f Mon Sep 17 00:00:00 2001 From: STEINNI Date: Sat, 24 Jan 2026 18:24:17 +0000 Subject: [PATCH 2/2] longlinks fixed --- buildoz.css | 8 ++- bzGraflow.js | 178 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 142 insertions(+), 44 deletions(-) diff --git a/buildoz.css b/buildoz.css index 6a43343..0a556b4 100644 --- a/buildoz.css +++ b/buildoz.css @@ -67,8 +67,7 @@ bz-select option:hover{ color: #FFF; } - - +/************************************************************************************/ bz-toggler{ outline: none; @@ -138,6 +137,8 @@ bz-toggler div.toggle-switch span.toggle-thumb.turned-on { left : 1em; } +/************************************************************************************/ + bz-slidepane { display: block; position: absolute; @@ -187,6 +188,9 @@ bz-slidepane[side="right"] div.handle { transform: translateY(-50%); cursor: ew-resize; } + +/************************************************************************************/ + bz-graflow { position: relative; display: block; diff --git a/bzGraflow.js b/bzGraflow.js index 64b27e5..a17cb32 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -24,6 +24,13 @@ class BZgraflow extends Buildoz{ .bzgf-wires-container, .bzgf-nodes-container{ position: absolute; inset: 0; width: 100%; height: 100%; } .bzgf-nodes-container .bzgf-node{ position:absolute; } + .bzgf-nodes-container .bzgf-fake-node{ + position: absolute; + width: 5px; + height: 5px; + backgrround: transparent; + border-style: none; + } ` this.shadow.appendChild(style) this.nodesContainer = document.createElement('div') @@ -111,10 +118,22 @@ class BZgraflow extends Buildoz{ return(this.stagedNodes[id]) } + addFakeNode(nid, x, y, w, h){ + this.stagedNodes[nid] = document.createElement('div') + this.stagedNodes[nid].classList.add('bzgf-fake-node') + this.stagedNodes[nid].style.left = `${x}px` + this.stagedNodes[nid].style.top = `${y}px` + this.stagedNodes[nid].style.width = `${w}px` + this.stagedNodes[nid].style.height = `${h}px` + this.stagedNodes[nid].dataset.id = nid + this.nodesContainer.append(this.stagedNodes[nid]) + return(this.stagedNodes[nid]) + } + addWire(link){ const [idNode1, idPort1] = link.from const [idNode2, idPort2] = link.to - const path = this.bezier(idNode1, idPort1, idNode2, idPort2, this.getBZAttribute('tension')) + const path = this.bezierNodes(idNode1, idPort1, idNode2, idPort2, this.getBZAttribute('tension')) const id = `${idNode1}_${idNode2}` this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path') this.stagedWires[id].setAttribute('d', path) @@ -153,25 +172,26 @@ class BZgraflow extends Buildoz{ } } - bezier(idNode1, idPort1, idNode2, idPort2, tensionMin=60) { + bezierNodes(idNode1, idPort1, idNode2, idPort2, tensionMin=60) { const svgRect = this.wiresContainer.getBoundingClientRect() const node1 = this.stagedNodes[idNode1] - const port1 = idPort1 ? node1.ports[idPort1] : null + const port1 = node1.ports[idPort1] const node2 = this.stagedNodes[idNode2] - const port2 = idPort2 ? node2.ports[idPort2] : null - if(!node1 || !node2) { - console.warn('Link on bad node ', idNode1, idNode2) + const port2 = node2.ports[idPort2] + if(!node1 || !node2 || !port1 || !port2) { + console.warn('Link on bad node / port ', idNode1, idPort1, idNode2, idPort2) return('') } - const bb1 = port1 ? port1.el.getBoundingClientRect() : node1.getBoundingClientRect() - const bb2 = port2 ? port2.el.getBoundingClientRect() : node2.getBoundingClientRect() + const bb1 = port1.el.getBoundingClientRect() + const bb2 = port2.el.getBoundingClientRect() const x1 = Math.floor(bb1.x + (bb1.width/2)) - svgRect.left const y1 = Math.floor(bb1.y + (bb1.height/2)) - svgRect.top const x2 = Math.floor(bb2.x + (bb2.width/2)) - svgRect.left const y2 = Math.floor(bb2.y + (bb2.height/2)) - svgRect.top + const loop = (idNode1==idNode2) && (idPort1==idPort2) const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1) let tension = dist * 0.4 - if(tension < tensionMin) tension = tensionMin + if(tension < tensionMin) tension = parseInt(tensionMin) const dirVect = { n: { x: 0, y: -1 }, @@ -183,7 +203,7 @@ class BZgraflow extends Buildoz{ let c1y = y1 + (dirVect[port1.direction].y * tension) let c2x = x2 + (dirVect[port2.direction].x * tension) let c2y = y2 + (dirVect[port2.direction].y * tension) - if((idNode1==idNode2) && (idPort1==idPort2)){ // Special case of self-loop: spread a bit intermediary points + if(loop){ if(['n', 's'].includes(port1.direction)) { c1x += tension c2x -= tension @@ -196,7 +216,62 @@ class BZgraflow extends Buildoz{ return(`M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`) } + bezierInterNodes(idNode1, idPort1, idNode2, idPort2, interNodes, orientation='horizontal', tensionMin=60) { + const svgRect = this.wiresContainer.getBoundingClientRect() + const makeCubicBezier = (x1, y1, x2, y2, orientation) => { + const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1) + let tension = dist * 0.4 + if(tension < tensionMin) tension = parseInt(tensionMin) + let c1x, c1y, c2x, c2y + if(orientation=='horizontal'){ + c1x = x1 + tension + c1y = y1 + c2x = x2 - tension + c2y = y2 + } else { + c1x = x1 + c1y = y1 + tension + c2x = x2 + c2y = y2 - tension + } + return(`C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`) + } + const directPath = this.bezierNodes(idNode1, idPort1, idNode2, idPort2, tensionMin) + const startPath = directPath.substring(0,directPath.indexOf('C')) + const endPath = directPath.substring(directPath.lastIndexOf(',')+1).trim() + let path = startPath + let [ , x1, y1] = startPath.split(' ') + x1 = parseInt(x1) + y1 = parseInt(y1) + for(const interNode of interNodes){ + const bb = this.stagedNodes[interNode].getBoundingClientRect() + let x2; let y2; + if(orientation=='horizontal'){ + x2 = bb.x -svgRect.left + y2 =Math.floor(bb.y + (bb.height/2)) - svgRect.top + path += makeCubicBezier(x1, y1, x2, y2, orientation) + x2 += bb.width + path += ` L ${x2} ${y2} ` + } else { + x2 = Math.floor(bb.x + (bb.width/2)) - svgRect.left + y2 = bb.y - svgRect.top + path += makeCubicBezier(x1, y1, x2, y2, orientation) + y2 += bb.height + path += ` L ${x2} ${y2} ` + } + x1 = x2 + y1 = y2 + } + let [x2, y2] = endPath.split(' ') + x2 = parseInt(x2) + y2 = parseInt(y2) + path += ' '+makeCubicBezier(x1, y1, x2, y2, orientation) + return(path) + } + autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=1000){ + + // Loops create infinite recursion in dfs for getting parents & adjacency lists: Remove them ! let linksWithoutBackEdges if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){ console.warn('Loop(s) detected... Cannot auto-place !') @@ -205,10 +280,12 @@ class BZgraflow extends Buildoz{ } else { linksWithoutBackEdges = this.flow.links } - const { parents, adj } = this.buildGraphStructures(this.flow.nodes, linksWithoutBackEdges) const layers = this.computeLayers(this.flow.nodes, parents) + + // Compute indexes for each layer (int part) & add sub-index for ports + // Also compute max width/height for each layer let maxHeight = 0; let maxWidth = 0 const layerHeights = []; const layerWidths = []; const indexes = {} // indexes[nid] = { base: , ports: { [portId]: } } @@ -226,34 +303,34 @@ class BZgraflow extends Buildoz{ layerWidths.push(totWidth) } + // If any long-links, create placeholders for skipped layers this.flow.longLinks = this.findLongLinks(this.flow.links) - console.log('==============================>longLinks:', this.flow.longLinks) for(const link of this.flow.longLinks){ - console.log('==============================>longLink:', link) for(const layerIdx of link.skippedLayers){ const nid = `longLinkPlaceHolder_${crypto.randomUUID()}` layers[layerIdx].push(nid) link.interNodes.push(nid) } } + + // Reorder layers to avoid crossings thanks to indexes this.reorderLayers(layers, parents, indexes, orientation) + // Finally place everything if(orientation=='horizontal'){ let x = gapx for(const [idx, layer] of layers.entries()){ - let wMax = 0 + let wMax = this.getMaxWidth(layer) let y = ((maxHeight - layerHeights[idx]) / 2) + gapy for(const nid of layer){ - if(!nid.startsWith('longLinkPlaceHolder_')){ + if(!nid.startsWith('longLinkPlaceHolder_')) { const bb = this.stagedNodes[nid].getBoundingClientRect() - wMax = (bb.width > wMax) ? bb.width : wMax - this.moveNode(nid, x, y, tween) + this.moveNode(nid, x, y, orientation, tween) y += gapy + bb.height } else { - console.log('longLinkPlaceHolder', layer) - // Maybe center x here (but we don't have full wmax yet) - this.moveNode(nid, x, y, tween) - y += gapy + 50 // TODO: fix this + this.addFakeNode(nid, x, y, wMax, 10) + this.moveNode(nid, x, y, orientation, tween) + y += gapy + 10 //TODO } } x += wMax + gapx @@ -261,19 +338,17 @@ class BZgraflow extends Buildoz{ } else if(orientation=='vertical'){ let y = gapy for(const [idx, layer] of layers.entries()){ - let hMax = 0 + let hMax = this.getMaxHeight(layer) let x = ((maxWidth - layerWidths[idx]) / 2) + gapx for(const nid of layer){ if(!nid.startsWith('longLinkPlaceHolder_')){ const bb = this.stagedNodes[nid].getBoundingClientRect() - hMax = (bb.height > hMax) ? bb.height : hMax - this.moveNode(nid, x, y, tween) + this.moveNode(nid, x, y, orientation, tween) x += gapx + bb.width } else { - console.log('longLinkPlaceHolder', layer) - // Maybe center y here (but we don't have full hmax yet) - this.moveNode(nid, x, y, tween) - x += gapx + 50 // TODO: fix this + this.addFakeNode(nid, x, y, 10, hMax) + this.moveNode(nid, x, y, orientation, tween) + x += gapx + 10 //TODO } } y += hMax + gapy @@ -281,6 +356,22 @@ class BZgraflow extends Buildoz{ } } + getMaxWidth(layer){ + return(layer.filter(nid => + !nid.startsWith('longLinkPlaceHolder_')) + .map(nid => this.stagedNodes[nid].getBoundingClientRect().width) + .reduce((a, b) => a > b ? a : b, 0) + ) + } + + getMaxHeight(layer){ + return(layer.filter(nid => + !nid.startsWith('longLinkPlaceHolder_')) + .map(nid => this.stagedNodes[nid].getBoundingClientRect().height) + .reduce((a, b) => a > b ? a : b, 0) + ) + } + computePortOffsets(nid, orientation = 'horizontal'){ const node = this.stagedNodes[nid] if(!node || !node.ports) return({}) @@ -343,7 +434,7 @@ class BZgraflow extends Buildoz{ } } - moveNode(nid, destx, desty, duration = 200, cb) { + moveNode(nid, destx, desty, orientation, duration = 200, cb) { const t0 = performance.now() const bb = this.stagedNodes[nid].getBoundingClientRect() const parentbb = this.stagedNodes[nid].parentElement.getBoundingClientRect() @@ -356,14 +447,14 @@ class BZgraflow extends Buildoz{ const y = y0 + (desty - y0) * k this.stagedNodes[nid].style.left = `${x}px` this.stagedNodes[nid].style.top = `${y}px` - this.updateWires(nid) + this.updateWires(nid, orientation) if(p < 1) requestAnimationFrame(frame.bind(this)) } requestAnimationFrame(frame.bind(this)) } - updateWires(nid){ + updateWires(nid, orientation){ const wires = Object.keys(this.stagedWires) .filter(id => (id.startsWith(nid+'_')||id.endsWith('_'+nid))) .map(id => this.stagedWires[id]) @@ -372,23 +463,17 @@ class BZgraflow extends Buildoz{ const lnk = this.getLink(nid1, nid2) const longLink = this.flow.longLinks.find(item => (item.link.from[0] == lnk.from[0] && item.link.from[1] == lnk.from[1] && item.link.to[0] == lnk.to[0] && item.link.to[1] == lnk.to[1])) if(longLink) { - console.log('==============================>WIRE:', wire) - let lastNid = nid1; let lastPort = lnk.from[1]; - for(const interNode of longLink.interNodes){ - const path = this.bezier(lastNid, lastPort, interNode, null, this.getBZAttribute('tension')) - wire.setAttribute('d', path) - // TODO: add/draw intermediary wire - lastNid = interNode; lastPort = null; - } - const path = this.bezier(lastNid, lastPort, nid2, lnk.to[1], this.getBZAttribute('tension')) + const path = this.bezierInterNodes(nid1, lnk.from[1], nid2, lnk.to[1], longLink.interNodes, orientation, this.getBZAttribute('tension')) wire.setAttribute('d', path) } else { - const path = this.bezier(nid1, lnk.from[1], nid2, lnk.to[1], this.getBZAttribute('tension')) + const path = this.bezierNodes(nid1, lnk.from[1], nid2, lnk.to[1], this.getBZAttribute('tension')) wire.setAttribute('d', path) } } } + + getLink(nid1, nid2){ return(this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2)))) } @@ -489,7 +574,16 @@ class BZgraflow extends Buildoz{ } findLongLinks(links) { - const { parents } = this.buildGraphStructures(this.flow.nodes, links) + let linksWithoutBackEdges + if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){ + console.warn('Loop(s) detected... Cannot auto-place !') + const backEdges = this.findBackEdges(this.flow.nodes, this.flow.links) + linksWithoutBackEdges = this.flow.links.filter((link, idx) => (!backEdges.includes(idx)) && (link.from[0] != link.to[0])) + } else { + linksWithoutBackEdges = this.flow.links + } + /// Yes that means we ignore long & back links ! + const { parents } = this.buildGraphStructures(this.flow.nodes, linksWithoutBackEdges) const layers = this.computeLayers(this.flow.nodes, parents) const crossLayerLinks = [] for(const link of links){