graflow: several bigfixes, for subflows, longlinks-fakenodes cleanup, multipass layerordering
This commit is contained in:
171
bzGraflow.js
171
bzGraflow.js
@@ -31,6 +31,7 @@ class BZgraflow extends Buildoz{
|
||||
this.stagedNodes = { }
|
||||
this.stagedWires = { }
|
||||
this.arrowDefs = null
|
||||
this.currentOrientation = null
|
||||
}
|
||||
|
||||
addIcon(el, name) {
|
||||
@@ -205,6 +206,8 @@ class BZgraflow extends Buildoz{
|
||||
const flowUrl = flowNode.subflow.url
|
||||
|
||||
const childEl = document.createElement('bz-graflow')
|
||||
childEl.isSubflow = true
|
||||
childEl.currentOrientation = this.currentOrientation
|
||||
childEl.setAttribute('flow', flowUrl)
|
||||
childEl.setAttribute('tension', this.getBZAttribute('tension') || '60')
|
||||
// Remember which node we "came from" so exitSubflow() can animate back to it.
|
||||
@@ -239,8 +242,9 @@ class BZgraflow extends Buildoz{
|
||||
})
|
||||
}
|
||||
}
|
||||
childEl.autoPlace('horizontal', 60, 60)
|
||||
})
|
||||
|
||||
childEl.autoPlace(this.currentOrientation, 60, 60)
|
||||
}, { once:true })
|
||||
|
||||
// Fade out the current (host) graflow while the child scales up
|
||||
this.hostContainer.style.opacity = '1'
|
||||
@@ -291,7 +295,7 @@ class BZgraflow extends Buildoz{
|
||||
|
||||
invade(oldEl, newEl){
|
||||
const r = oldEl.getBoundingClientRect()
|
||||
newEl.style.position = 'fixed'
|
||||
newEl.style.position = 'fixed' //TODO This is bad: not scroll-proof !!
|
||||
newEl.style.left = r.left + 'px'
|
||||
newEl.style.top = r.top + 'px'
|
||||
newEl.style.width = r.width + 'px'
|
||||
@@ -414,13 +418,12 @@ class BZgraflow extends Buildoz{
|
||||
for(const link of this.flow.links){
|
||||
this.addWire(link)
|
||||
}
|
||||
|
||||
if(forceAutoplace){
|
||||
if(!this.currentOrientation) {
|
||||
const bb=this.getBoundingClientRect()
|
||||
//TODO compute tensions from ports, height and width
|
||||
if(bb.width > bb.height) this.autoPlace('horizontal', 60, 60)
|
||||
else this.autoPlace('vertical', 60, 60)
|
||||
}
|
||||
if(bb.width > bb.height) this.currentOrientation = 'horizontal'
|
||||
else this.currentOrientation = 'vertical'
|
||||
}
|
||||
if(forceAutoplace) this.autoPlace(this.currentOrientation, 60, 60)
|
||||
}
|
||||
|
||||
// Convert viewport (client) coordinates to this instance's SVG local coordinates.
|
||||
@@ -556,6 +559,21 @@ class BZgraflow extends Buildoz{
|
||||
}
|
||||
|
||||
autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=500, align='center'){
|
||||
this.currentOrientation = orientation
|
||||
// Cancel any previous autoPlace() animations by bumping a token.
|
||||
// moveNode() checks this token each frame and will no-op if superseded.
|
||||
this._autoPlaceToken = (this._autoPlaceToken || 0) + 1
|
||||
const token = this._autoPlaceToken
|
||||
|
||||
// Cleanup placeholders from previous autoPlace() runs.
|
||||
// Each run creates new longLinkPlaceHolder_* IDs; without cleanup they accumulate in the DOM.
|
||||
for(const nid of Object.keys(this.stagedNodes || {})){
|
||||
if(!nid.startsWith('longLinkPlaceHolder_')) continue
|
||||
const el = this.stagedNodes[nid]
|
||||
if(el?.parentNode) el.parentNode.removeChild(el)
|
||||
delete this.stagedNodes[nid]
|
||||
}
|
||||
|
||||
// Loops create infinite recursion in dfs for getting parents & adjacency lists: Remove them !
|
||||
let linksWithoutBackEdges
|
||||
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
|
||||
@@ -568,6 +586,27 @@ class BZgraflow extends Buildoz{
|
||||
|
||||
const layers = this.computeLayers(this.flow.nodes, parents)
|
||||
|
||||
// Layer-0 nodes have no parents, so reorderLayers() (which uses parent ordering) cannot
|
||||
// improve their order. This shows up strongly for "entry reference nodes" that are added
|
||||
// programmatically: they all end up in layer 0 and keep insertion order, creating crossings.
|
||||
// Pre-sort layer 0 by the average position of their children in layer 1.
|
||||
if(layers.length > 1){
|
||||
const next = layers[1]
|
||||
const nextPos = Object.fromEntries(next.map((nid, idx) => ([nid, idx])))
|
||||
const curPos0 = Object.fromEntries(layers[0].map((nid, idx) => ([nid, idx])))
|
||||
const key0 = (nid) => {
|
||||
const kids = (adj?.[nid] || []).filter(k => (k in nextPos))
|
||||
if(kids.length === 0) return(curPos0[nid] ?? 0)
|
||||
const sum = kids.reduce((acc, k) => acc + nextPos[k], 0)
|
||||
return(sum / kids.length)
|
||||
}
|
||||
layers[0].sort((a, b) => {
|
||||
const ka = key0(a), kb = key0(b)
|
||||
if(ka !== kb) return(ka - kb)
|
||||
return((curPos0[a] ?? 0) - (curPos0[b] ?? 0))
|
||||
})
|
||||
}
|
||||
|
||||
// Compute indexes for each layer (int part) & add sub-index for ports
|
||||
// Also compute max width/height for each layer
|
||||
let maxHeight = 0; let maxWidth = 0
|
||||
@@ -643,11 +682,11 @@ class BZgraflow extends Buildoz{
|
||||
for(const nid of layer){
|
||||
if(!nid.startsWith('longLinkPlaceHolder_')) {
|
||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||
this.moveNode(nid, x, y, orientation, tween)
|
||||
this.moveNode(nid, x, y, orientation, tween, null, token)
|
||||
y += gapy + (this.stagedNodes[nid].offsetHeight || bb.height)
|
||||
} else {
|
||||
this.addFakeNode(nid, x, y, wMax*0.75, fakeNodeHeight)
|
||||
this.moveNode(nid, x, y, orientation, tween)
|
||||
this.moveNode(nid, x, y, orientation, tween, null, token)
|
||||
y += gapy + fakeNodeHeight
|
||||
}
|
||||
}
|
||||
@@ -662,11 +701,11 @@ class BZgraflow extends Buildoz{
|
||||
for(const nid of layer){
|
||||
if(!nid.startsWith('longLinkPlaceHolder_')){
|
||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||
this.moveNode(nid, x, y, orientation, tween)
|
||||
this.moveNode(nid, x, y, orientation, tween, null, token)
|
||||
x += gapx + (this.stagedNodes[nid].offsetWidth || bb.width)
|
||||
} else {
|
||||
this.addFakeNode(nid, x, y, fakeNodeWidth, hMax*0.75)
|
||||
this.moveNode(nid, x, y, orientation, tween)
|
||||
this.moveNode(nid, x, y, orientation, tween, null, token)
|
||||
x += gapx + fakeNodeWidth
|
||||
}
|
||||
}
|
||||
@@ -727,53 +766,89 @@ class BZgraflow extends Buildoz{
|
||||
const off = (portId && info?.ports?.[portId] !== undefined) ? info.ports[portId] : 0
|
||||
return(base + off)
|
||||
}
|
||||
// For nodes with multiple parents in the previous layer (common for "exit" ref nodes),
|
||||
// using only the first parent can produce bad swaps. Use the average index over all
|
||||
// parents that are actually in the previous layer.
|
||||
const avgParentIndex = (nid, prevLayer) => {
|
||||
const ps = (parents?.[nid] || []).filter(p => prevLayer.includes(p))
|
||||
if(ps.length === 0) return(adjIndex(nid))
|
||||
const sum = ps.reduce((acc, p) => {
|
||||
const lnk = this.getLink(p, nid)
|
||||
return(acc + adjIndex(p, lnk?.from?.[1]))
|
||||
}, 0)
|
||||
return(sum / ps.length)
|
||||
}
|
||||
const avgChildIndex = (nid, prevLayer) => {
|
||||
const ps = (parents?.[nid] || []).filter(p => prevLayer.includes(p))
|
||||
if(ps.length === 0) return(adjIndex(nid))
|
||||
const sum = ps.reduce((acc, p) => {
|
||||
const lnk = this.getLink(p, nid)
|
||||
return(acc + adjIndex(nid, lnk?.to?.[1]))
|
||||
}, 0)
|
||||
return(sum / ps.length)
|
||||
}
|
||||
for(const [lidx, layer] of layers.entries()){
|
||||
if(lidx==0) continue
|
||||
const toSwap = []
|
||||
for(let i=0; i<layer.length; i++){
|
||||
const nid1 = layer[i]
|
||||
// some parents can be in far layers, but at least one is in the prev layer (by definition of layer)
|
||||
const pnid1 = parents[nid1].find((nid => layers[lidx-1].includes(nid)))
|
||||
for(let j=i+1; j<layer.length; j++){
|
||||
const nid2 = layer[j]
|
||||
const pnid2 = parents[nid2].find((nid => layers[lidx-1].includes(nid)))
|
||||
const link1 = (pnid1) ? this.getLink(pnid1, nid1) : null
|
||||
const link2 = (pnid2) ? this.getLink(pnid2, nid2) : null
|
||||
const p1 = adjIndex(pnid1, link1?.from?.[1])
|
||||
const p2 = adjIndex(pnid2, link2?.from?.[1])
|
||||
const c1 = adjIndex(nid1, link1?.to?.[1])
|
||||
const c2 = adjIndex(nid2, link2?.to?.[1])
|
||||
const prevLayer = layers[lidx-1]
|
||||
|
||||
if(((p1 - p2) * (c1 - c2)) < 0) { // crossing (now refined by per-port ordering)
|
||||
toSwap.push([i, j])
|
||||
// Single-pass swapping is very sensitive to insertion order (especially with long-link
|
||||
// placeholders and reference nodes). Do a few relaxation passes until stable.
|
||||
const maxPasses = 8
|
||||
for(let pass = 0; pass < maxPasses; pass++){
|
||||
const toSwap = []
|
||||
for(let i=0; i<layer.length; i++){
|
||||
const nid1 = layer[i]
|
||||
for(let j=i+1; j<layer.length; j++){
|
||||
const nid2 = layer[j]
|
||||
const p1 = avgParentIndex(nid1, prevLayer)
|
||||
const p2 = avgParentIndex(nid2, prevLayer)
|
||||
const c1 = avgChildIndex(nid1, prevLayer)
|
||||
const c2 = avgChildIndex(nid2, prevLayer)
|
||||
|
||||
if(((p1 - p2) * (c1 - c2)) < 0) { // crossing (now refined by per-port ordering)
|
||||
toSwap.push([i, j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
swap(layer, toSwap)
|
||||
// Keep bases in sync with the new order so later layers use the updated positions
|
||||
for(const [idx, nid] of layer.entries()){
|
||||
if(!indexes[nid]) indexes[nid] = { base: idx, ports: {} }
|
||||
else indexes[nid].base = idx
|
||||
if(toSwap.length === 0) break
|
||||
swap(layer, toSwap)
|
||||
// Keep bases in sync with the new order so later passes/layers use updated positions
|
||||
for(const [idx, nid] of layer.entries()){
|
||||
if(!indexes[nid]) indexes[nid] = { base: idx, ports: {} }
|
||||
else indexes[nid].base = idx
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveNode(nid, destx, desty, orientation, duration = 200, cb) {
|
||||
moveNode(nid, destx, desty, orientation, duration = 200, autoPlaceToken = null) {
|
||||
const t0 = performance.now()
|
||||
const bb = this.stagedNodes[nid].getBoundingClientRect()
|
||||
const parentbb = this.stagedNodes[nid].parentElement.getBoundingClientRect()
|
||||
const el0 = this.stagedNodes?.[nid]
|
||||
if(!el0) return
|
||||
const bb = el0.getBoundingClientRect()
|
||||
const parentbb = el0.parentElement.getBoundingClientRect()
|
||||
const x0=bb.x - parentbb.x
|
||||
const y0 = bb.y - parentbb.y
|
||||
function frame(t) {
|
||||
if(autoPlaceToken && autoPlaceToken !== this._autoPlaceToken) return
|
||||
const el = this.stagedNodes?.[nid]
|
||||
if(!el) return
|
||||
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`
|
||||
el.style.left = `${x}px`
|
||||
el.style.top = `${y}px`
|
||||
this.updateWires(nid, orientation)
|
||||
|
||||
if(p < 1) requestAnimationFrame(frame.bind(this))
|
||||
else{
|
||||
this.dispatchEvent(new CustomEvent('nodeMoved', {
|
||||
detail: { nid, x, y },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(frame.bind(this))
|
||||
}
|
||||
@@ -785,6 +860,8 @@ class BZgraflow extends Buildoz{
|
||||
for(const wire of wires){
|
||||
const [nid1, nid2] = wire.dataset.id.split('_')
|
||||
const lnk = this.getLink(nid1, nid2)
|
||||
if(!lnk) continue
|
||||
if(!this.flow?.longLinks) this.flow.longLinks = []
|
||||
const longLink = this.flow.longLinks.find(item => (item.link.from[0] == lnk.from[0] && item.link.from[1] == lnk.from[1] && item.link.to[0] == lnk.to[0] && item.link.to[1] == lnk.to[1]))
|
||||
if(longLink) {
|
||||
const path = this.bezierInterNodes(nid1, lnk.from[1], nid2, lnk.to[1], longLink.interNodes, orientation, this.getBZAttribute('tension'))
|
||||
@@ -840,12 +917,18 @@ class BZgraflow extends Buildoz{
|
||||
}
|
||||
return(layer[id])
|
||||
}
|
||||
// Compute all layer indices
|
||||
nodes.forEach(n => dfs(n.id))
|
||||
|
||||
// Build layers in the same order as `nodes` to keep results stable when nodes are appended
|
||||
// programmatically (e.g. reference nodes). Avoid relying on object key enumeration order.
|
||||
const t = []
|
||||
for(const nid in layer) {
|
||||
if(!t[layer[nid]]) t[layer[nid]]=[]
|
||||
t[layer[nid]].push(nid)
|
||||
}
|
||||
nodes.forEach(n => {
|
||||
const l = layer[n.id]
|
||||
if(l === undefined) return
|
||||
if(!t[l]) t[l] = []
|
||||
t[l].push(n.id)
|
||||
})
|
||||
return(t)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user