graflow: added icmp example & fixed grand-slam long-links
This commit is contained in:
125
bzGraflow.js
125
bzGraflow.js
@@ -113,7 +113,7 @@ class BZgraflow extends Buildoz{
|
||||
}
|
||||
|
||||
// Now load styles (once)
|
||||
if(!BZgraflow._loadedNodeStyles.has(url)) {
|
||||
if(!BZgraflow._loadedNodeStyles.has(url) || this.attributes.isolated) {
|
||||
const styles = doc.querySelectorAll('style')
|
||||
styles.forEach(styleEl => {
|
||||
const style = document.createElement('style')
|
||||
@@ -151,39 +151,65 @@ class BZgraflow extends Buildoz{
|
||||
}
|
||||
|
||||
zoomIn(id){
|
||||
console.log('==============================>ZOOM IN:', id)
|
||||
const node = this.stagedNodes[id]
|
||||
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')
|
||||
const nodeEl = this.stagedNodes[id]
|
||||
if(!nodeEl) return
|
||||
|
||||
this.Invade(this, testEl)
|
||||
node.classList.remove('scaler')
|
||||
// Create the child graflow first, place it above (foreground) the current graflow,
|
||||
// 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()
|
||||
oldEl.style.visibility = 'hidden'
|
||||
if(hideOld) oldEl.style.visibility = 'hidden'
|
||||
newEl.style.position = 'fixed'
|
||||
newEl.style.left = r.left + 'px'
|
||||
newEl.style.top = r.top + 'px'
|
||||
newEl.style.width = r.width + 'px'
|
||||
newEl.style.height = r.height + 'px'
|
||||
newEl.style.display = 'block'
|
||||
oldEl.parentNode.appendChild(newEl)
|
||||
}
|
||||
|
||||
@@ -389,21 +415,40 @@ testEl.classList.add('eic')
|
||||
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
|
||||
this.flow.longLinks = this.findLongLinks(this.flow.links)
|
||||
for(const link of this.flow.longLinks){
|
||||
for(const layerIdx of link.skippedLayers){
|
||||
for(const llink of this.flow.longLinks){
|
||||
let fakeParent = llink.link.from[0]
|
||||
for(const layerIdx of llink.skippedLayers){
|
||||
const nid = `longLinkPlaceHolder_${crypto.randomUUID()}`
|
||||
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
|
||||
this.reorderLayers(layers, parents, indexes, orientation)
|
||||
delete this._virtualLinks
|
||||
|
||||
// Finally place everything
|
||||
if(orientation=='horizontal'){
|
||||
const fakeNodeHeight = 10
|
||||
let x = gapx
|
||||
for(const [idx, layer] of layers.entries()){
|
||||
let wMax = this.getMaxWidth(layer)
|
||||
@@ -414,14 +459,15 @@ testEl.classList.add('eic')
|
||||
this.moveNode(nid, x, y, orientation, tween)
|
||||
y += gapy + bb.height
|
||||
} 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)
|
||||
y += gapy + 10 //TODO
|
||||
y += gapy + fakeNodeHeight
|
||||
}
|
||||
}
|
||||
x += wMax + gapx
|
||||
}
|
||||
} else if(orientation=='vertical'){
|
||||
const fakeNodeWidth = 10
|
||||
let y = gapy
|
||||
for(const [idx, layer] of layers.entries()){
|
||||
let hMax = this.getMaxHeight(layer)
|
||||
@@ -432,9 +478,9 @@ testEl.classList.add('eic')
|
||||
this.moveNode(nid, x, y, orientation, tween)
|
||||
x += gapx + bb.width
|
||||
} 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)
|
||||
x += gapx + 10 //TODO
|
||||
x += gapx + fakeNodeWidth
|
||||
}
|
||||
}
|
||||
y += hMax + gapy
|
||||
@@ -461,12 +507,11 @@ testEl.classList.add('eic')
|
||||
computePortOffsets(nid, orientation = 'horizontal'){
|
||||
const node = this.stagedNodes[nid]
|
||||
if(!node || !node.ports) return({})
|
||||
const axis = (orientation === 'vertical') ? 'x' : 'y'
|
||||
const nodeRect = node.getBoundingClientRect()
|
||||
const ports = Object.entries(node.ports)
|
||||
.map(([pid, p]) => {
|
||||
const r = p.el.getBoundingClientRect()
|
||||
const pos = (axis === 'x')
|
||||
const pos = (orientation == 'vertical')
|
||||
? (r.left + (r.width / 2) - nodeRect.left)
|
||||
: (r.top + (r.height / 2) - nodeRect.top)
|
||||
return({ pid, pos })
|
||||
@@ -498,12 +543,10 @@ testEl.classList.add('eic')
|
||||
const toSwap = []
|
||||
for(let i=0; i<layer.length; 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)
|
||||
const pnid1 = parents[nid1].find((nid => layers[lidx-1].includes(nid)))
|
||||
for(let j=i+1; j<layer.length; j++){
|
||||
const nid2 = layer[j]
|
||||
if(nid2.startsWith('longLinkPlaceHolder_')) continue
|
||||
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
|
||||
@@ -511,12 +554,18 @@ testEl.classList.add('eic')
|
||||
const p2 = adjIndex(pnid2, link2?.from?.[1])
|
||||
const c1 = adjIndex(nid1, link1?.to?.[1])
|
||||
const c2 = adjIndex(nid2, link2?.to?.[1])
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,7 +610,11 @@ testEl.classList.add('eic')
|
||||
|
||||
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user