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)
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)