graflow: added icmp example & fixed grand-slam long-links

This commit is contained in:
STEINNI
2026-02-21 21:32:53 +00:00
parent 2ac276da6d
commit 9e411c88bf

View File

@@ -113,7 +113,7 @@ class BZgraflow extends Buildoz{
} }
// Now load styles (once) // Now load styles (once)
if(!BZgraflow._loadedNodeStyles.has(url)) { if(!BZgraflow._loadedNodeStyles.has(url) || this.attributes.isolated) {
const styles = doc.querySelectorAll('style') const styles = doc.querySelectorAll('style')
styles.forEach(styleEl => { styles.forEach(styleEl => {
const style = document.createElement('style') const style = document.createElement('style')
@@ -151,39 +151,65 @@ class BZgraflow extends Buildoz{
} }
zoomIn(id){ zoomIn(id){
console.log('==============================>ZOOM IN:', id) const nodeEl = this.stagedNodes[id]
const node = this.stagedNodes[id] if(!nodeEl) return
const nodeBB = node.getBoundingClientRect()
const parentBB = this.nodesContainer.getBoundingClientRect()
const sx = parentBB.width / nodeBB.width
const sy = parentBB.height / nodeBB.height
const tx = parentBB.left - nodeBB.left + this.nodesContainer.scrollLeft // TODO Should have a meth to accumulate scrolls in ancestors
const ty = parentBB.top - nodeBB.top + this.nodesContainer.scrollTop // TODO Should have a meth to accumulate scrolls in ancestors
node.style.setProperty('--tx', tx + 'px')
node.style.setProperty('--ty', ty + 'px')
node.style.setProperty('--sx', sx)
node.style.setProperty('--sy', sy)
node.style.zIndex = '9999'
node.classList.add('scaler')
Promise.all(node.getAnimations().map(a => a.finished)).then((transitions) => {
const testEl = document.createElement('bz-graflow')
testEl.setAttribute('flow', '/app/assets/json/bzGraflow/testFlowEic.json')
testEl.setAttribute('tension', '60')
testEl.classList.add('eic')
this.Invade(this, testEl) // Create the child graflow first, place it above (foreground) the current graflow,
node.classList.remove('scaler') // scaled down so it fits exactly inside the clicked node, then animate it to full size.
const nodeBB = nodeEl.getBoundingClientRect()
const parentBB = this.getBoundingClientRect()
const flowNode = this.flow?.nodes?.find(n => n.id === id)
const flowUrl = flowNode?.subflow
const childEl = document.createElement('bz-graflow')
childEl.setAttribute('flow', flowUrl)
childEl.setAttribute('tension', this.getBZAttribute('tension') || '60')
childEl.style.zIndex = '9999'
// Put the child in the exact same viewport rect as the parent (fixed overlay)
this.Invade(this, childEl, { hideOld:false })
// Initial transform so the full-size child "fits" inside the node
const sx0 = nodeBB.width / parentBB.width
const sy0 = nodeBB.height / parentBB.height
const tx0 = nodeBB.left - parentBB.left
const ty0 = nodeBB.top - parentBB.top
// Inline "scaler" (shadow styles don't apply to the child element)
childEl.style.transformOrigin = 'top left'
childEl.style.willChange = 'transform'
childEl.style.transition = 'transform 300ms ease-in-out'
childEl.style.transform = 'translate(var(--tx, 0px), var(--ty, 0px)) scale(var(--sx, 1), var(--sy, 1))'
childEl.style.setProperty('--tx', tx0 + 'px')
childEl.style.setProperty('--ty', ty0 + 'px')
childEl.style.setProperty('--sx', sx0)
childEl.style.setProperty('--sy', sy0)
// Force style flush, then animate back to identity (full parent size)
childEl.getBoundingClientRect()
requestAnimationFrame(() => {
childEl.style.setProperty('--tx', '0px')
childEl.style.setProperty('--ty', '0px')
childEl.style.setProperty('--sx', 1)
childEl.style.setProperty('--sy', 1)
}) })
childEl.addEventListener('transitionend', (e) => {
if(e.propertyName !== 'transform') return
this.style.visibility = 'hidden'
}, { once:true })
} }
Invade(oldEl, newEl){ Invade(oldEl, newEl, { hideOld=true } = {}){
const r = oldEl.getBoundingClientRect() const r = oldEl.getBoundingClientRect()
oldEl.style.visibility = 'hidden' if(hideOld) oldEl.style.visibility = 'hidden'
newEl.style.position = 'fixed' newEl.style.position = 'fixed'
newEl.style.left = r.left + 'px' newEl.style.left = r.left + 'px'
newEl.style.top = r.top + 'px' newEl.style.top = r.top + 'px'
newEl.style.width = r.width + 'px' newEl.style.width = r.width + 'px'
newEl.style.height = r.height + 'px' newEl.style.height = r.height + 'px'
newEl.style.display = 'block'
oldEl.parentNode.appendChild(newEl) oldEl.parentNode.appendChild(newEl)
} }
@@ -389,21 +415,40 @@ testEl.classList.add('eic')
layerWidths.push(totWidth) layerWidths.push(totWidth)
} }
// Temporary "virtual" links used only during autoPlace() to let reorderLayers()
// reason about placeholder nodes as if they were part of the original long-link.
// This prevents bogus swaps caused by missing port info on placeholders.
this._virtualLinks = new Map()
// If any long-links, create placeholders for skipped layers // If any long-links, create placeholders for skipped layers
this.flow.longLinks = this.findLongLinks(this.flow.links) this.flow.longLinks = this.findLongLinks(this.flow.links)
for(const link of this.flow.longLinks){ for(const llink of this.flow.longLinks){
for(const layerIdx of link.skippedLayers){ let fakeParent = llink.link.from[0]
for(const layerIdx of llink.skippedLayers){
const nid = `longLinkPlaceHolder_${crypto.randomUUID()}` const nid = `longLinkPlaceHolder_${crypto.randomUUID()}`
layers[layerIdx].push(nid) layers[layerIdx].push(nid)
link.interNodes.push(nid) llink.interNodes.push(nid)
// Placeholders are added after initial index computation; give them an index
// so reorderLayers() can take them into account (otherwise they default to base=0).
indexes[nid] = { base: layers[layerIdx].length - 1, ports: {} }
// Virtual link: treat placeholder as receiving the same "from port" as the original long-link.
// (Child port doesn't matter for placeholders since they have no ports.)
this._virtualLinks.set(`${fakeParent}__${nid}`, {
from: [fakeParent, llink.link.from[1]],
to: [nid, llink.link.to[1]],
})
parents[nid] = [fakeParent]
fakeParent = nid
} }
} }
// Reorder layers to avoid crossings thanks to indexes // Reorder layers to avoid crossings thanks to indexes
this.reorderLayers(layers, parents, indexes, orientation) this.reorderLayers(layers, parents, indexes, orientation)
delete this._virtualLinks
// Finally place everything // Finally place everything
if(orientation=='horizontal'){ if(orientation=='horizontal'){
const fakeNodeHeight = 10
let x = gapx let x = gapx
for(const [idx, layer] of layers.entries()){ for(const [idx, layer] of layers.entries()){
let wMax = this.getMaxWidth(layer) let wMax = this.getMaxWidth(layer)
@@ -414,14 +459,15 @@ testEl.classList.add('eic')
this.moveNode(nid, x, y, orientation, tween) this.moveNode(nid, x, y, orientation, tween)
y += gapy + bb.height y += gapy + bb.height
} else { } else {
this.addFakeNode(nid, x, y, wMax*0.75, 10) this.addFakeNode(nid, x, y, wMax*0.75, fakeNodeHeight)
this.moveNode(nid, x, y, orientation, tween) this.moveNode(nid, x, y, orientation, tween)
y += gapy + 10 //TODO y += gapy + fakeNodeHeight
} }
} }
x += wMax + gapx x += wMax + gapx
} }
} else if(orientation=='vertical'){ } else if(orientation=='vertical'){
const fakeNodeWidth = 10
let y = gapy let y = gapy
for(const [idx, layer] of layers.entries()){ for(const [idx, layer] of layers.entries()){
let hMax = this.getMaxHeight(layer) let hMax = this.getMaxHeight(layer)
@@ -432,9 +478,9 @@ testEl.classList.add('eic')
this.moveNode(nid, x, y, orientation, tween) this.moveNode(nid, x, y, orientation, tween)
x += gapx + bb.width x += gapx + bb.width
} else { } else {
this.addFakeNode(nid, x, y, 10, hMax*0.75) this.addFakeNode(nid, x, y, fakeNodeWidth, hMax*0.75)
this.moveNode(nid, x, y, orientation, tween) this.moveNode(nid, x, y, orientation, tween)
x += gapx + 10 //TODO x += gapx + fakeNodeWidth
} }
} }
y += hMax + gapy y += hMax + gapy
@@ -461,12 +507,11 @@ testEl.classList.add('eic')
computePortOffsets(nid, orientation = 'horizontal'){ computePortOffsets(nid, orientation = 'horizontal'){
const node = this.stagedNodes[nid] const node = this.stagedNodes[nid]
if(!node || !node.ports) return({}) if(!node || !node.ports) return({})
const axis = (orientation === 'vertical') ? 'x' : 'y'
const nodeRect = node.getBoundingClientRect() const nodeRect = node.getBoundingClientRect()
const ports = Object.entries(node.ports) const ports = Object.entries(node.ports)
.map(([pid, p]) => { .map(([pid, p]) => {
const r = p.el.getBoundingClientRect() const r = p.el.getBoundingClientRect()
const pos = (axis === 'x') const pos = (orientation == 'vertical')
? (r.left + (r.width / 2) - nodeRect.left) ? (r.left + (r.width / 2) - nodeRect.left)
: (r.top + (r.height / 2) - nodeRect.top) : (r.top + (r.height / 2) - nodeRect.top)
return({ pid, pos }) return({ pid, pos })
@@ -498,12 +543,10 @@ testEl.classList.add('eic')
const toSwap = [] const toSwap = []
for(let i=0; i<layer.length; i++){ for(let i=0; i<layer.length; i++){
const nid1 = layer[i] const nid1 = layer[i]
if(nid1.startsWith('longLinkPlaceHolder_')) continue
// some parents can be in far layers, but at least one is in the prev layer (by definition of layer) // 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))) const pnid1 = parents[nid1].find((nid => layers[lidx-1].includes(nid)))
for(let j=i+1; j<layer.length; j++){ for(let j=i+1; j<layer.length; j++){
const nid2 = layer[j] const nid2 = layer[j]
if(nid2.startsWith('longLinkPlaceHolder_')) continue
const pnid2 = parents[nid2].find((nid => layers[lidx-1].includes(nid))) const pnid2 = parents[nid2].find((nid => layers[lidx-1].includes(nid)))
const link1 = (pnid1) ? this.getLink(pnid1, nid1) : null const link1 = (pnid1) ? this.getLink(pnid1, nid1) : null
const link2 = (pnid2) ? this.getLink(pnid2, nid2) : null const link2 = (pnid2) ? this.getLink(pnid2, nid2) : null
@@ -511,12 +554,18 @@ testEl.classList.add('eic')
const p2 = adjIndex(pnid2, link2?.from?.[1]) const p2 = adjIndex(pnid2, link2?.from?.[1])
const c1 = adjIndex(nid1, link1?.to?.[1]) const c1 = adjIndex(nid1, link1?.to?.[1])
const c2 = adjIndex(nid2, link2?.to?.[1]) const c2 = adjIndex(nid2, link2?.to?.[1])
if(((p1 - p2) * (c1 - c2)) < 0) { // crossing (now refined by per-port ordering) if(((p1 - p2) * (c1 - c2)) < 0) { // crossing (now refined by per-port ordering)
toSwap.push([i, j]) toSwap.push([i, j])
} }
} }
} }
swap(layer, toSwap) 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
}
} }
} }
@@ -561,7 +610,11 @@ testEl.classList.add('eic')
getLink(nid1, nid2){ getLink(nid1, nid2){
return(this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2)))) const real = this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2)))
if(real) return(real)
const v = this._virtualLinks?.get(`${nid1}__${nid2}`)
if(v) return(v)
return(null)
} }
buildGraphStructures(nodes, links, includeLinkIndexes = false) { buildGraphStructures(nodes, links, includeLinkIndexes = false) {