graflow, autoplacement 1.0 no layer-ordering

This commit is contained in:
STEINNI
2025-12-24 11:13:13 +00:00
parent 5b00367dd6
commit 26ff467015
2 changed files with 159 additions and 38 deletions

View File

@@ -202,3 +202,4 @@ bz-graflow .bzgf-nodes-container{
} }
bz-graflow .bzgf-nodes-container{ z-index:10; } bz-graflow .bzgf-nodes-container{ z-index:10; }
bz-graflow .bzgf-wires-container{ z-index:9; } bz-graflow .bzgf-wires-container{ z-index:9; }
bz-graflow .bzgf-nodes-container .bzgf-node{ position:absolute; }

View File

@@ -2,7 +2,7 @@ class BZgraflow extends Buildoz{
constructor(){ constructor(){
super() super()
this.defaultAttrs = { } this.defaultAttrs = { tension: 100 }
this.stagedNodes = { } this.stagedNodes = { }
this.stagedWires = { } this.stagedWires = { }
} }
@@ -15,9 +15,7 @@ class BZgraflow extends Buildoz{
console.warn('BZgraflow: No flow URL !?') console.warn('BZgraflow: No flow URL !?')
return return
} }
this.loadFlow(flowUrl) // Let it load async while we coat this.loadFlow(flowUrl) // Let it load async while we coat
this.nodesContainer = document.createElement('div') this.nodesContainer = document.createElement('div')
this.nodesContainer.classList.add('bzgf-nodes-container') this.nodesContainer.classList.add('bzgf-nodes-container')
this.wiresContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg') this.wiresContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
@@ -42,6 +40,8 @@ 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')
this.refresh() this.refresh()
} }
@@ -73,31 +73,20 @@ class BZgraflow extends Buildoz{
addNode(type, id, x, y){ addNode(type, id, x, y){
const nodeDef = this.nodesRegistry[type] const nodeDef = this.nodesRegistry[type]
this.stagedNodes[id] = nodeDef.cloneNode(true) this.stagedNodes[id] = nodeDef.cloneNode(true)
const portEls = this.stagedNodes[id].querySelectorAll('.port')
this.stagedNodes[id].style.left = `${x}px` this.stagedNodes[id].style.left = `${x}px`
this.stagedNodes[id].style.top = `${y}px` this.stagedNodes[id].style.top = `${y}px`
this.stagedNodes[id].dataset.id = id this.stagedNodes[id].dataset.id = id
const portEls = this.stagedNodes[id].querySelectorAll('.port')
this.stagedNodes[id].ports = Object.fromEntries(Array.from(portEls).map(item => ([item.dataset.id, { ...item.dataset, el:item }]))) this.stagedNodes[id].ports = Object.fromEntries(Array.from(portEls).map(item => ([item.dataset.id, { ...item.dataset, el:item }])))
this.nodesContainer.append(this.stagedNodes[id]) this.nodesContainer.append(this.stagedNodes[id])
return(this.stagedNodes[id]) return(this.stagedNodes[id])
} }
addWire(idNode1, idPort1, idNode2, idPort2){ addWire(idNode1, idPort1, idNode2, idPort2){
const node1 = this.stagedNodes[idNode1] const path = this.bezier(idNode1, idPort1, idNode2, idPort2, this.getBZAttribute('tension'))
const port1 = node1.ports[idPort1] const id = `${idNode1}_${idNode2}`
const node2 = this.stagedNodes[idNode2]
const port2 = node2.ports[idPort2]
const id = `${node1.dataset.id}_${node2.dataset.id}`
const bb1 = port1.el.getBoundingClientRect()
const bb2 = port2.el.getBoundingClientRect()
const x1 = Math.floor(bb1.x + (bb1.width/2))
const y1 = Math.floor(bb1.y + (bb1.height/2))
const x2 = Math.floor(bb2.x + (bb2.width/2))
const y2 = Math.floor(bb2.y + (bb2.height/2))
console.log('====>', x1, y1, x2, y2)
this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path') this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path')
this.stagedWires[id].setAttribute('d', this.bezier(x1, y1, port1.direction , x2, y2, port2.direction, 60)) this.stagedWires[id].setAttribute('d', path)
this.stagedWires[id].setAttribute('fill', 'none') this.stagedWires[id].setAttribute('fill', 'none')
this.stagedWires[id].classList.add('bzgf-wire') this.stagedWires[id].classList.add('bzgf-wire')
this.stagedWires[id].dataset.id = id this.stagedWires[id].dataset.id = id
@@ -118,40 +107,171 @@ class BZgraflow extends Buildoz{
} }
} }
bezier(idNode1, idPort1, idNode2, idPort2, tensionMin=60) {
bezier(x1, y1, dir1, x2, y2, dir2, tensionMin=60) {
const dirVect = { const dirVect = {
n: { x: 0, y: -1 }, n: { x: 0, y: -1 },
s: { x: 0, y: 1 }, s: { x: 0, y: 1 },
e: { x: 1, y: 0 }, e: { x: 1, y: 0 },
w: { x: -1, y: 0 }, w: { x: -1, y: 0 },
} }
const node1 = this.stagedNodes[idNode1]
const port1 = node1.ports[idPort1]
const node2 = this.stagedNodes[idNode2]
const port2 = node2.ports[idPort2]
const bb1 = port1.el.getBoundingClientRect()
const bb2 = port2.el.getBoundingClientRect()
const x1 = Math.floor(bb1.x + (bb1.width/2))
const y1 = Math.floor(bb1.y + (bb1.height/2))
const x2 = Math.floor(bb2.x + (bb2.width/2))
const y2 = Math.floor(bb2.y + (bb2.height/2))
const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1) const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1)
let tension = dist * 0.4 let tension = dist * 0.4
if (tension < tensionMin) tension = tensionMin if(tension < tensionMin) tension = tensionMin
const c1x = x1 + (dirVect[dir1].x * tension) const c1x = x1 + (dirVect[port1.direction].x * tension)
const c1y = y1 + (dirVect[dir1].y * tension) const c1y = y1 + (dirVect[port1.direction].y * tension)
const c2x = x2 + (dirVect[dir2].x * tension) const c2x = x2 + (dirVect[port2.direction].x * tension)
const c2y = y2 + (dirVect[dir2].y * tension) const c2y = y2 + (dirVect[port2.direction].y * 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){
if(!['horizontal', 'vertical'].includes(orientation)) return
/* const parents = {}
portPosition(nodeEl, portEl) { const adj = {}
const nodeRect = nodeEl.getBoundingClientRect()
const portRect = portEl.getBoundingClientRect()
const canvasRect = this.canvas.getBoundingClientRect()
return { this.flow.nodes.forEach(n => {
x: portRect.left - canvasRect.left + portRect.width / 2, parents[n.id] = []
y: portRect.top - canvasRect.top + portRect.height / 2 adj[n.id] = []
})
this.flow.links.forEach(link => {
const from = link.from[0]
const to = link.to[0]
parents[to].push(from)
adj[from].push(to)
})
const layers = this.computeLayers(this.flow.nodes, parents)
// const maxLayers = Math.max(...layers.map(item=>item.length))
let maxHeight = 0
const layerHeights = []
for(const layer of layers){
let totHeight = 0
for(const nid of layer){
const bb = this.stagedNodes[nid].getBoundingClientRect()
totHeight += bb.height+gapy
}
if(totHeight>maxHeight) maxHeight = totHeight
layerHeights.push(totHeight)
} }
}
*/ let x = gapx
for(const [idx, layer] of layers.entries()){
for(const nid of layer){
// some parents can be in far layers, but at least one is in the prev layer (by definition of layer)
const localParent = parents[nid].find((nid => layers[idx-1].includes(nid)))
if(localParent){ //undefined for 1st node
const ports = this.stagedNodes[localParent].ports
console.log(`parent of ${nid} is ${localParent}`, ports)
}
}
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, 1000)
y += gapy + bb.height
}
x += wMax + gapx
}
}
moveNode(nid, destx, desty, duration = 200) {
const t0 = performance.now()
const bb = this.stagedNodes[nid].getBoundingClientRect()
const x0=bb.x
const y0 = bb.y
function frame(t) {
const p = Math.min((t - t0) / duration, 1)
const k = p * p * (3 - 2 * p) // smoothstep
const x = x0 + (destx - x0) * k
const y = y0 + (desty - y0) * k
this.stagedNodes[nid].style.left = `${x}px`
this.stagedNodes[nid].style.top = `${y}px`
this.updateWires(nid)
if(p < 1) requestAnimationFrame(frame.bind(this))
}
requestAnimationFrame(frame.bind(this))
}
updateWires(nid){
const wires = Object.keys(this.stagedWires)
.filter(id => (id.startsWith(nid+'_')||id.endsWith('_'+nid)))
.map(id => this.stagedWires[id])
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)
}
}
getLink(nid1, nid2){
return(this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2))))
}
computeLayers(nodes, parents) {
const layer = {}
const dfs = (id) => {
if (layer[id] !== undefined) return(layer[id])
if (parents[id].length === 0) {
layer[id] = 0
} else {
layer[id] = 1 + Math.max(...parents[id].map(dfs))
}
return(layer[id])
}
nodes.forEach(n => dfs(n.id))
const t = []
for(const nid in layer) {
if(!t[layer[nid]]) t[layer[nid]]=[]
t[layer[nid]].push(nid)
}
return(t)
}
hasAnyLoop(nodes, links) {
// self-loops
if(this.flow.links.some(l => l.from[0] === l.to[0])) return(true)
// 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 visiting = new Set();
const visited = new Set()
const dfs = (nid) => {
if(visiting.has(nid)) return(true)
if(visited.has(nid)) return(false)
visiting.add(nid)
for(const m of adj[nid]) {
if(dfs(m)) return(true)
}
visiting.delete(nid)
visited.add(nid)
return(false)
}
return(this.flow.nodes.map(n => n.id).some(dfs))
}
} }
Buildoz.define('graflow', BZgraflow) Buildoz.define('graflow', BZgraflow)