autoplace on looooooops

This commit is contained in:
STEINNI
2026-01-13 17:55:55 +00:00
parent fdf5a8d6c5
commit 6465d38d2c

View File

@@ -60,7 +60,10 @@ class BZgraflow extends Buildoz{
await this.loadNodes(flowObj.nodesFile) await this.loadNodes(flowObj.nodesFile)
this.flow = flowObj.flow 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() this.refresh()
} }
@@ -140,19 +143,14 @@ class BZgraflow extends Buildoz{
if(forceAutoplace){ if(forceAutoplace){
const bb=this.getBoundingClientRect() 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) if(bb.width > bb.height) this.autoPlace('horizontal', 80, 30, 500)
else this.autoPlace('vertical', 80, 30, 200) else this.autoPlace('vertical', 80, 30, 200)
} }
} }
bezier(idNode1, idPort1, idNode2, idPort2, tensionMin=60) { 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 svgRect = this.wiresContainer.getBoundingClientRect()
const node1 = this.stagedNodes[idNode1] const node1 = this.stagedNodes[idNode1]
const port1 = node1.ports[idPort1] const port1 = node1.ports[idPort1]
@@ -172,18 +170,38 @@ class BZgraflow extends Buildoz{
let tension = dist * 0.4 let tension = dist * 0.4
if(tension < tensionMin) tension = tensionMin if(tension < tensionMin) tension = tensionMin
const c1x = x1 + (dirVect[port1.direction].x * tension) const dirVect = {
const c1y = y1 + (dirVect[port1.direction].y * tension) n: { x: 0, y: -1 },
s: { x: 0, y: 1 },
const c2x = x2 + (dirVect[port2.direction].x * tension) e: { x: 1, y: 0 },
const c2y = y2 + (dirVect[port2.direction].y * tension) 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}` return `M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`
} }
autoPlace(orientation = 'horizontal', gapx = 80, gapy = 30, tween=1000){ autoPlace(orientation = 'horizontal', gapx = 80, gapy = 30, tween=1000){
let linksWithoutBackEdges
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){ if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
console.warn('Loop(s) detected... Cannot auto-place !') 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 = {} const parents = {}
@@ -193,13 +211,16 @@ class BZgraflow extends Buildoz{
adj[n.id] = [] adj[n.id] = []
}) })
this.flow.links.forEach(link => { linksWithoutBackEdges.forEach(link => {
const from = link.from[0] const from = link.from[0]
const to = link.to[0] const to = link.to[0]
parents[to].push(from) parents[to].push(from)
adj[from].push(to) adj[from].push(to)
}) })
console.log('===LinksWithoutBackEdges:===>', linksWithoutBackEdges)
console.log('===Parents:===>', parents)
console.log('===Adj:===>', adj)
const layers = this.computeLayers(this.flow.nodes, parents) const layers = this.computeLayers(this.flow.nodes, parents)
let maxHeight = 0; let maxWidth = 0 let maxHeight = 0; let maxWidth = 0
const layerHeights = []; const layerWidths = []; const layerHeights = []; const layerWidths = [];
@@ -363,38 +384,117 @@ class BZgraflow extends Buildoz{
getLoopingLinks(nodes, links) { 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 = {}; const adj = {};
this.flow.nodes.forEach(node => adj[node.id] = []) this.flow.nodes.forEach(node => adj[node.id] = [])
for(let [idx, link] of this.flow.links.entries()){ for(let [idx, link] of this.flow.links.entries()){
if(link.from[0] == link.to[0]) linkIndexes.push(idx) //self loops if(link.from[0] !== link.to[0]) { // Skip self-loops, already handled
adj[link.from[0]].push([link.to[0], idx]) 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 visited = new Set()
const loops = new Set()
const dfs = (nid) => { // DFS to find cycles
if(visiting.has(nid)) { const dfs = (nid, path, pathLinkIndexes) => {
loops.push() // If already fully visited, skip
return(true) if(visited.has(nid)) return
}
if(visited.has(nid)) return(false) // Add current node to path
visiting.add(nid) path.push(nid)
for(const m of adj[nid]) {
if(dfs(m)) { // Explore neighbors
return(true) 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) 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) Buildoz.define('graflow', BZgraflow)