diff --git a/app/assets/html/test.html b/app/assets/html/test.html
index 096f89f..7145435 100644
--- a/app/assets/html/test.html
+++ b/app/assets/html/test.html
@@ -56,10 +56,16 @@
window.addEventListener('load',()=>{
const grflw1 = document.querySelector('bz-graflow.compunet')
grflw1.addEventListener('subflowLoaded',
- (evt) => { evt.target.querySelector('.demooptions').style.display = 'none'; }
+ (evt) => {
+ const demoPanel = evt.target.querySelector('.demooptions')
+ if(demoPanel) demoPanel.style.display = 'none';
+ }
)
grflw1.addEventListener('subflowExited',
- (evt) => { evt.target.querySelector('.demooptions').style.display = 'block'; }
+ (evt) => {
+ const demoPanel = evt.target.querySelector('.demooptions')
+ if(demoPanel) demoPanel.style.display = 'block';
+ }
)
document.querySelector('[data-trigger="onAutoplace1H"]').addEventListener('click',
(evt) => { grflw1.autoPlace('horizontal', 80, 30, 1000, document.querySelector('[data-id="compunet"]').value) }
diff --git a/app/thirdparty/buildoz/bzGraflow.js b/app/thirdparty/buildoz/bzGraflow.js
index 4c634f7..8f61618 100644
--- a/app/thirdparty/buildoz/bzGraflow.js
+++ b/app/thirdparty/buildoz/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 layers[lidx-1].includes(nid)))
- for(let j=i+1; j 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 (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)
}