autoplace on looooooops
This commit is contained in:
168
bzGraflow.js
168
bzGraflow.js
@@ -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,37 +384,116 @@ 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
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
if(visited.has(nid)) return(false)
|
pathLinkIndexes.push(neighbor.linkIdx)
|
||||||
visiting.add(nid)
|
dfs(neighbor.to, path, pathLinkIndexes)
|
||||||
for(const m of adj[nid]) {
|
pathLinkIndexes.pop()
|
||||||
if(dfs(m)) {
|
|
||||||
return(true)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user