diff --git a/bzGraflow.js b/bzGraflow.js index b5ae82e..a6ccc80 100644 --- a/bzGraflow.js +++ b/bzGraflow.js @@ -60,7 +60,10 @@ class BZgraflow extends Buildoz{ await this.loadNodes(flowObj.nodesFile) this.flow = flowObj.flow - if(this.hasAnyLoop()) console.warn('This flow has loops.Optimization not available') + if(this.hasAnyLoop()) { + console.warn('This flow has loops.Optimization not available') + console.log('===Loops:===>', this.getLoopingLinks()) + } this.refresh() } @@ -140,19 +143,14 @@ class BZgraflow extends Buildoz{ if(forceAutoplace){ const bb=this.getBoundingClientRect() - //TODO compute tensions from nodes + //TODO compute tensions from ports if(bb.width > bb.height) this.autoPlace('horizontal', 80, 30, 500) else this.autoPlace('vertical', 80, 30, 200) } } bezier(idNode1, idPort1, idNode2, idPort2, tensionMin=60) { - const dirVect = { - n: { x: 0, y: -1 }, - s: { x: 0, y: 1 }, - e: { x: 1, y: 0 }, - w: { x: -1, y: 0 }, - } + const svgRect = this.wiresContainer.getBoundingClientRect() const node1 = this.stagedNodes[idNode1] const port1 = node1.ports[idPort1] @@ -172,18 +170,38 @@ class BZgraflow extends Buildoz{ let tension = dist * 0.4 if(tension < tensionMin) tension = tensionMin - const c1x = x1 + (dirVect[port1.direction].x * tension) - const c1y = y1 + (dirVect[port1.direction].y * tension) - - const c2x = x2 + (dirVect[port2.direction].x * tension) - const c2y = y2 + (dirVect[port2.direction].y * tension) + const dirVect = { + n: { x: 0, y: -1 }, + s: { x: 0, y: 1 }, + e: { x: 1, y: 0 }, + w: { x: -1, y: 0 }, + } + let c1x = x1 + (dirVect[port1.direction].x * tension) + 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: correct intermediary points + if(['n', 's'].includes(port1.direction)) { + c1x += tension + c2x -= tension + } + if(['e', 'w'].includes(port1.direction)){ + c1y += 1*tension + c2y -= 1*tension + } + } return `M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}` } autoPlace(orientation = 'horizontal', gapx = 80, gapy = 30, tween=1000){ + let linksWithoutBackEdges if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){ console.warn('Loop(s) detected... Cannot auto-place !') - return + const backEdges = this.findBackEdges(this.flow.nodes, this.flow.links) + console.log('===BackEdges:===>', backEdges) + linksWithoutBackEdges = this.flow.links.filter((link, idx) => (!backEdges.includes(idx)) && (link.from[0] != link.to[0])) + } else { + linksWithoutBackEdges = this.flow.links } const parents = {} @@ -193,13 +211,16 @@ class BZgraflow extends Buildoz{ adj[n.id] = [] }) - this.flow.links.forEach(link => { + linksWithoutBackEdges.forEach(link => { const from = link.from[0] const to = link.to[0] parents[to].push(from) adj[from].push(to) }) - + console.log('===LinksWithoutBackEdges:===>', linksWithoutBackEdges) + + console.log('===Parents:===>', parents) + console.log('===Adj:===>', adj) const layers = this.computeLayers(this.flow.nodes, parents) let maxHeight = 0; let maxWidth = 0 const layerHeights = []; const layerWidths = []; @@ -363,38 +384,117 @@ class BZgraflow extends Buildoz{ getLoopingLinks(nodes, links) { - const linkIndexes = [] + const loops = [] + + // Handle self-loops (links where from[0] === to[0]) + for(let [idx, link] of this.flow.links.entries()){ + if(link.from[0] === link.to[0]) { + loops.push([idx]) + } + } + + // Build adjacency list with link indexes const adj = {}; this.flow.nodes.forEach(node => adj[node.id] = []) for(let [idx, link] of this.flow.links.entries()){ - if(link.from[0] == link.to[0]) linkIndexes.push(idx) //self loops - adj[link.from[0]].push([link.to[0], idx]) + if(link.from[0] !== link.to[0]) { // Skip self-loops, already handled + adj[link.from[0]].push({ to: link.to[0], linkIdx: idx }) + } } - const visiting = new Set() + // Track visited nodes globally to avoid revisiting completed subtrees const visited = new Set() - const loops = new Set() - const dfs = (nid) => { - if(visiting.has(nid)) { - loops.push() - return(true) - } + + // DFS to find cycles + const dfs = (nid, path, pathLinkIndexes) => { + // If already fully visited, skip + if(visited.has(nid)) return - if(visited.has(nid)) return(false) - visiting.add(nid) - for(const m of adj[nid]) { - if(dfs(m)) { - return(true) + // Add current node to path + path.push(nid) + + // Explore neighbors + for(const neighbor of adj[nid]) { + // Check if this neighbor creates a cycle + const cycleStartIdx = path.indexOf(neighbor.to) + if(cycleStartIdx !== -1) { + // Found a cycle: from path[cycleStartIdx] to path[path.length-1] and back via this link + // The cycle links are: pathLinkIndexes[cycleStartIdx] to pathLinkIndexes[pathLinkIndexes.length-1], plus the current link + const cycleLinkIndexes = [...pathLinkIndexes.slice(cycleStartIdx), neighbor.linkIdx] + // Normalize: represent cycle as sorted array of link indexes for uniqueness + const normalized = [...cycleLinkIndexes].sort((a, b) => a - b) + // Check if we already have this cycle (avoid duplicates) + const alreadyFound = loops.some(loop => { + const loopNormalized = [...loop].sort((a, b) => a - b) + if(loopNormalized.length !== normalized.length) return false + return loopNormalized.every((idx, i) => idx === normalized[i]) + }) + if(!alreadyFound && cycleLinkIndexes.length > 0) { + loops.push(cycleLinkIndexes) + } + continue // Don't recurse into this neighbor } + + pathLinkIndexes.push(neighbor.linkIdx) + dfs(neighbor.to, path, pathLinkIndexes) + pathLinkIndexes.pop() } - visiting.delete(nid) + + // Remove current node from path + path.pop() + + // Mark as fully visited visited.add(nid) - return(false) - } - return(this.flow.nodes.map(n => n.id).some(dfs)) + } + + // Start DFS from each unvisited node + for(const node of this.flow.nodes) { + if(!visited.has(node.id)) { + dfs(node.id, [], []) + } + } + + return loops } + + 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 backEdges = [] + + function dfs(u) { + color[u] = 'gray' + for (const neighbor of adj[u]) { + const v = neighbor.to + const linkIdx = neighbor.linkIdx + if (color[v] === 'gray') { + backEdges.push(linkIdx) // 👈 cycle edge - return link index + } else if (color[v] === 'white') { + dfs(v) + } + } + color[u] = 'black' + } + + nodes.forEach(n => { + if (color[n.id] === 'white') dfs(n.id) + }) + + return backEdges + } + } Buildoz.define('graflow', BZgraflow)