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)
|
// 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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user