diff --git a/app/assets/html/test.html b/app/assets/html/test.html
index c8d1baf..d94b6ae 100644
--- a/app/assets/html/test.html
+++ b/app/assets/html/test.html
@@ -30,7 +30,7 @@
padding: 2px;
position: absolute;
top: 2px;
- left: 2px;
+ right: 2px;
width: 10em;
background: #FFFB;
border-radius: 5px;
diff --git a/app/thirdparty/buildoz/bzGraflow.js b/app/thirdparty/buildoz/bzGraflow.js
index 98766be..ac6044a 100644
--- a/app/thirdparty/buildoz/bzGraflow.js
+++ b/app/thirdparty/buildoz/bzGraflow.js
@@ -65,13 +65,6 @@ class BZgraflow extends Buildoz{
right: -1em;
color: #A00;
}
- .bzgf-nodes-container .bzgf-node.scaler {
- transform-origin: top left;
- transform:
- translate(var(--tx, 0), var(--ty, 0))
- scale(var(--sx, 1), var(--sy, 1));
- transition: transform 300ms ease-in-out;
- }
`
this.mainContainer.appendChild(style)
this.nodesContainer = document.createElement('div')
@@ -159,18 +152,18 @@ class BZgraflow extends Buildoz{
this.stagedNodes[id].dataset.id = id
this.stagedNodes[id].ports = Object.fromEntries(Array.from(portEls).map(item => ([item.dataset.id, { ...item.dataset, el:item }])))
if(node.subflow) {
- const btnZoomIn = document.createElement('button')
- btnZoomIn.classList.add('bzgf-zoom-in', 'icon-copy')
- btnZoomIn.addEventListener('click', () => {
- this.zoomIn(id)
+ const btnEnterSubflow = document.createElement('button')
+ btnEnterSubflow.classList.add('bzgf-zoom-in', 'icon-copy')
+ btnEnterSubflow.addEventListener('click', () => {
+ this.EnterSubflow(id)
})
- this.stagedNodes[id].appendChild(btnZoomIn)
+ this.stagedNodes[id].appendChild(btnEnterSubflow)
}
this.nodesContainer.append(this.stagedNodes[id])
return(this.stagedNodes[id])
}
- zoomIn(id){
+ EnterSubflow(id){
const nodeEl = this.stagedNodes[id]
if(!nodeEl) return
@@ -185,10 +178,17 @@ class BZgraflow extends Buildoz{
const childEl = document.createElement('bz-graflow')
childEl.setAttribute('flow', flowUrl)
childEl.setAttribute('tension', this.getBZAttribute('tension') || '60')
- childEl.style.zIndex = '9999'
+ // Match the clicked node's border so the transition feels like we're "expanding" it.
+ const nodeStyle = getComputedStyle(nodeEl)
+ childEl.style.border = nodeStyle.border
+ childEl.style.borderRadius = nodeStyle.borderRadius
// Put the child in the exact same viewport rect as the parent (fixed overlay)
- this.Invade(this, childEl, { hideOld:false })
+ this.Invade(this, childEl)
+
+ // Fade out the current (host) graflow while the child scales up
+ this.hostContainer.style.opacity = '1'
+ this.hostContainer.style.transition = 'opacity 1000ms ease-in-out'
// Initial transform so the full-size child "fits" inside the node
const sx0 = nodeBB.width / parentBB.width
@@ -199,7 +199,7 @@ class BZgraflow extends Buildoz{
// 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.transition = 'transform 1000ms 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')
@@ -213,24 +213,24 @@ class BZgraflow extends Buildoz{
childEl.style.setProperty('--ty', '0px')
childEl.style.setProperty('--sx', 1)
childEl.style.setProperty('--sy', 1)
+ this.hostContainer.style.opacity = '0'
})
childEl.addEventListener('transitionend', (e) => {
if(e.propertyName !== 'transform') return
- this.style.visibility = 'hidden'
+ this.hostContainer.style.visibility = 'hidden'
}, { once:true })
}
- Invade(oldEl, newEl, { hideOld=true } = {}){
+ Invade(oldEl, newEl){
const r = oldEl.getBoundingClientRect()
- 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)
+ oldEl.appendChild(newEl)
}
Evade(oldEl, newEl){
@@ -291,15 +291,39 @@ class BZgraflow extends Buildoz{
if(forceAutoplace){
const bb=this.getBoundingClientRect()
- //TODO compute tensions from ports
- if(bb.width > bb.height) this.autoPlace('horizontal', 80, 30, 500)
- else this.autoPlace('vertical', 80, 30, 200)
+ //TODO compute tensions from ports, height and width
+ if(bb.width > bb.height) this.autoPlace('horizontal', 60, 60)
+ else this.autoPlace('vertical', 60, 60)
}
}
+ // Convert viewport (client) coordinates to this instance's SVG local coordinates.
+ // Required when the whole graflow is CSS-transformed (scale/translate), otherwise wire paths
+ // will be computed in the wrong coordinate space.
+ clientToSvg(x, y){
+ const svg = this.wiresContainer
+ const ctm = svg?.getScreenCTM?.()
+ if(ctm && ctm.inverse){
+ const inv = ctm.inverse()
+ if(svg?.createSVGPoint){
+ const pt = svg.createSVGPoint()
+ pt.x = x
+ pt.y = y
+ const p = pt.matrixTransform(inv)
+ return({ x: p.x, y: p.y })
+ }
+ if(typeof DOMPoint !== 'undefined'){
+ const p = new DOMPoint(x, y).matrixTransform(inv)
+ return({ x: p.x, y: p.y })
+ }
+ }
+ // Fallback: approximate using boundingClientRect (works only at scale=1)
+ const r = svg.getBoundingClientRect()
+ return({ x: x - r.left, y: y - r.top })
+ }
+
bezierNodes(idNode1, idPort1, idNode2, idPort2, tension=60) {
tension = parseInt(tension)
- const svgRect = this.wiresContainer.getBoundingClientRect()
const node1 = this.stagedNodes[idNode1]
const port1 = node1.ports[idPort1]
const node2 = this.stagedNodes[idNode2]
@@ -310,10 +334,12 @@ class BZgraflow extends Buildoz{
}
const bb1 = port1.el.getBoundingClientRect()
const bb2 = port2.el.getBoundingClientRect()
- const x1 = Math.floor(bb1.x + (bb1.width/2)) - svgRect.left
- const y1 = Math.floor(bb1.y + (bb1.height/2)) - svgRect.top
- const x2 = Math.floor(bb2.x + (bb2.width/2)) - svgRect.left
- const y2 = Math.floor(bb2.y + (bb2.height/2)) - svgRect.top
+ const p1 = this.clientToSvg(bb1.x + (bb1.width/2), bb1.y + (bb1.height/2))
+ const p2 = this.clientToSvg(bb2.x + (bb2.width/2), bb2.y + (bb2.height/2))
+ const x1 = Math.floor(p1.x)
+ const y1 = Math.floor(p1.y)
+ const x2 = Math.floor(p2.x)
+ const y2 = Math.floor(p2.y)
const loop = (idNode1==idNode2) && (idPort1==idPort2)
const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1)
@@ -336,7 +362,6 @@ class BZgraflow extends Buildoz{
bezierInterNodes(idNode1, idPort1, idNode2, idPort2, interNodes, orientation='horizontal', tension=60) {
tension = parseInt(tension)
- const svgRect = this.wiresContainer.getBoundingClientRect()
const node1 = this.stagedNodes[idNode1]
let port1 = node1.ports[idPort1]
@@ -365,18 +390,23 @@ class BZgraflow extends Buildoz{
const endPath = directPath.substring(directPath.lastIndexOf(',')+1).trim()
let path = startPath
let [ , x1, y1] = startPath.split(' ')
- x1 = parseInt(x1)
- y1 = parseInt(y1)
+ x1 = parseFloat(x1)
+ y1 = parseFloat(y1)
for(const interNode of interNodes){
const bb = this.stagedNodes[interNode].getBoundingClientRect()
- let x2; let y2;
+ // Entry/exit points on the placeholder box, converted to SVG coords (handles CSS transforms)
+ let entryClient; let exitClient
if(orientation=='horizontal'){
- x2 = bb.x -svgRect.left
- y2 =Math.floor(bb.y + (bb.height/2)) - svgRect.top
+ entryClient = { x: bb.left, y: bb.top + (bb.height/2) }
+ exitClient = { x: bb.right, y: bb.top + (bb.height/2) }
} else {
- x2 = Math.floor(bb.x + (bb.width/2)) - svgRect.left
- y2 = bb.y - svgRect.top
+ entryClient = { x: bb.left + (bb.width/2), y: bb.top }
+ exitClient = { x: bb.left + (bb.width/2), y: bb.bottom }
}
+ const entry = this.clientToSvg(entryClient.x, entryClient.y)
+ const exit = this.clientToSvg(exitClient.x, exitClient.y)
+ let x2 = Math.floor(entry.x)
+ let y2 = Math.floor(entry.y)
if(port1){
path += makeCubicBezier(x1, y1, x2, y2, ['w','e'].includes(port1.direction) ? 'horizontal' : 'vertical', orientation)
@@ -385,26 +415,21 @@ class BZgraflow extends Buildoz{
path += makeCubicBezier(x1, y1, x2, y2, orientation, orientation)
}
- if(orientation=='horizontal'){
- x2 += bb.width
- path += ` L ${x2} ${y2} `
- } else {
- y2 += bb.height
- path += ` L ${x2} ${y2} `
- }
+ const x3 = Math.floor(exit.x)
+ const y3 = Math.floor(exit.y)
+ path += ` L ${x3} ${y3} `
- x1 = x2
- y1 = y2
+ x1 = x3
+ y1 = y3
}
let [x2, y2] = endPath.split(' ')
- x2 = parseInt(x2)
- y2 = parseInt(y2)
+ x2 = parseFloat(x2)
+ y2 = parseFloat(y2)
path += ' '+makeCubicBezier(x1, y1, x2, y2, orientation, orientation)
return(path)
}
- autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=1000, align='center'){
- console.log('autoPlace', orientation, gapx, gapy, tween, align)
+ autoPlace(orientation = 'horizontal', gapx = 80, gapy = 80, tween=500, align='center'){
// Loops create infinite recursion in dfs for getting parents & adjacency lists: Remove them !
let linksWithoutBackEdges
if(this.hasAnyLoop(this.flow.nodes, this.flow.links)){
@@ -425,9 +450,12 @@ class BZgraflow extends Buildoz{
for(const layer of layers){
let totHeight = 0; let totWidth = 0
for(const [idx, nid] of layer.entries()){
+ // Use offset* (not impacted by CSS transforms) to keep autoPlace stable during zoom animations.
const bb = this.stagedNodes[nid].getBoundingClientRect()
- totHeight += bb.height + gapy
- totWidth += bb.width + gapx
+ const h = this.stagedNodes[nid].offsetHeight || bb.height
+ const w = this.stagedNodes[nid].offsetWidth || bb.width
+ totHeight += h + gapy
+ totWidth += w + gapx
indexes[nid] = { base: idx, ports: this.computePortOffsets(nid, orientation) }
}
if(totHeight>maxHeight) maxHeight = totHeight
@@ -488,9 +516,9 @@ class BZgraflow extends Buildoz{
}
for(const nid of layer){
if(!nid.startsWith('longLinkPlaceHolder_')) {
- const bb = this.stagedNodes[nid].getBoundingClientRect()
+ const bb = this.stagedNodes[nid].getBoundingClientRect()
this.moveNode(nid, x, y, orientation, tween)
- y += gapy + bb.height
+ 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)
@@ -509,7 +537,7 @@ class BZgraflow extends Buildoz{
if(!nid.startsWith('longLinkPlaceHolder_')){
const bb = this.stagedNodes[nid].getBoundingClientRect()
this.moveNode(nid, x, y, orientation, tween)
- x += gapx + bb.width
+ 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)
@@ -524,7 +552,8 @@ class BZgraflow extends Buildoz{
getMaxWidth(layer){
return(layer.filter(nid =>
!nid.startsWith('longLinkPlaceHolder_'))
- .map(nid => this.stagedNodes[nid].getBoundingClientRect().width)
+ // Use offsetWidth (not impacted by CSS transforms) to keep autoPlace stable during zoom animations.
+ .map(nid => (this.stagedNodes[nid].offsetWidth || this.stagedNodes[nid].getBoundingClientRect().width))
.reduce((a, b) => a > b ? a : b, 0)
)
}
@@ -532,7 +561,8 @@ class BZgraflow extends Buildoz{
getMaxHeight(layer){
return(layer.filter(nid =>
!nid.startsWith('longLinkPlaceHolder_'))
- .map(nid => this.stagedNodes[nid].getBoundingClientRect().height)
+ // Use offsetHeight (not impacted by CSS transforms) to keep autoPlace stable during zoom animations.
+ .map(nid => (this.stagedNodes[nid].offsetHeight || this.stagedNodes[nid].getBoundingClientRect().height))
.reduce((a, b) => a > b ? a : b, 0)
)
}