diff --git a/app/assets/html/bzGraflow/nodesTest2.html b/app/assets/html/bzGraflow/nodesTest2.html index 404aa15..53ddd4e 100644 --- a/app/assets/html/bzGraflow/nodesTest2.html +++ b/app/assets/html/bzGraflow/nodesTest2.html @@ -161,14 +161,14 @@ diff --git a/app/assets/html/test.html b/app/assets/html/test.html index e5a23c6..aa9ef7f 100644 --- a/app/assets/html/test.html +++ b/app/assets/html/test.html @@ -46,6 +46,7 @@ } bz-graflow{ overflow: scroll; + border: 2px solid black; } bz-graflow.compunet{ grid-column: 1 / -1; width: 80vw; height: 40vh; background:black; } bz-graflow.eic{ grid-column: 1 / -1; width: 80vw; height: 30vh; background: var(--eicui-base-color-grey-10); } @@ -77,6 +78,14 @@ (evt) => { grflw3.autoPlace('vertical', 80, 50, 1000) } ) + const grflw4 = document.querySelector('bz-graflow.icmp') + document.querySelector('[data-trigger="onAutoplace4H"]').addEventListener('click', + (evt) => { grflw4.autoPlace('horizontal', 80, 80, 1000) } + ) + document.querySelector('[data-trigger="onAutoplace4V"]').addEventListener('click', + (evt) => { grflw4.autoPlace('vertical', 80, 80, 1000) } + ) + document.querySelector('[data-id="compunet"]').addEventListener('change', (evt) => { grflw1.setAttribute('tension', evt.target.value); grflw1.refresh() } ) @@ -86,12 +95,16 @@ document.querySelector('[data-id="organi"]').addEventListener('change', (evt) => { grflw3.setAttribute('tension', evt.target.value); grflw3.refresh() } ) + + document.querySelector('[data-id="icmp"]').addEventListener('change', + (evt) => { grflw4.setAttribute('tension', evt.target.value); grflw4.refresh() } + ) }) - +
@@ -99,7 +112,7 @@
- +
@@ -107,13 +120,22 @@
- +
- +
+ + +
+ + +
+
+
+ diff --git a/app/assets/json/bzGraflow/testFlow1.json b/app/assets/json/bzGraflow/testFlow1.json index c8e49ec..faf18ff 100644 --- a/app/assets/json/bzGraflow/testFlow1.json +++ b/app/assets/json/bzGraflow/testFlow1.json @@ -7,7 +7,7 @@ "coords": { "x": 220, "y": 120} }, { "nodeType": "inc", - "subflow": "/app/assets/json/bzGraflow/testSubFlow1.json", + "subflow": "/app/assets/json/bzGraflow/testFlowEic.json", "portLinks": [ { "parentPort": ["in1"], "subflowPort": ["inp1"] }, { "parentPort": ["out1"], "subflowPort": ["out1"] } diff --git a/app/assets/json/bzGraflow/testFlow2.json b/app/assets/json/bzGraflow/testFlow2.json index dc7d332..f086855 100644 --- a/app/assets/json/bzGraflow/testFlow2.json +++ b/app/assets/json/bzGraflow/testFlow2.json @@ -2,36 +2,36 @@ "nodesFile": "/app/assets/html/bzGraflow/nodesTest2.html", "flow": { "nodes":[ - { "nodeType": "start", - "id": "aze", - "coords": { "x": 220, "y": 20}, - "markup": { "text": "Start" } - }, { "nodeType": "process", "id": "aze2", - "coords": { "x": 220, "y": 120}, + "xcoords": { "x": 220, "y": 120}, "markup": { "text": "x = alph - 1" } }, { "nodeType": "condition", "id": "qsd", - "coords": { "x": 250, "y": 270}, + "xcoords": { "x": 250, "y": 270}, "markup": { "text": "x > 0" } }, { "nodeType": "preparation", "id": "qsd2", - "coords": { "x": 250, "y": 470}, + "xcoords": { "x": 250, "y": 470}, "markup": { "text": "prepare SQL" } }, { "nodeType": "database", "id": "wcx", - "coords": { "x": 500, "y": 450}, + "xcoords": { "x": 500, "y": 450}, "markup": { "text": "MySQL
Store" } }, { "nodeType": "end", "id": "ert", - "coords": { "x": 250, "y": 650}, + "xcoords": { "x": 250, "y": 650}, "markup": { "text": "End" } - } + }, + { "nodeType": "start", + "id": "aze", + "xcoords": { "x": 220, "y": 20}, + "markup": { "text": "StartMike" } + } ], "links": [ { "from": ["aze", "out1"], "to": ["aze2", "inout1"], "endArrow":true }, diff --git a/app/assets/json/bzGraflow/testFlowICMP.json b/app/assets/json/bzGraflow/testFlowICMP.json new file mode 100644 index 0000000..bd16d4d --- /dev/null +++ b/app/assets/json/bzGraflow/testFlowICMP.json @@ -0,0 +1,150 @@ +{ + "nodesFile": "/app/assets/html/bzGraflow/nodesEIC.html", + "flow": { + "nodes":[ + { "nodeType": "eicBasic", + "id": "eval", + "markup": { + "title": "Evaluations", + "subtitle": "...", + "severity": "secondary" + }, + "data": { "node": "eval", "nodeId":null} + }, + { "nodeType": "eicBasic", + "id": "gap", + "ncoords": { "x": 100, "y": 220}, + "markup": { + "title": "GAP", + "subtitle": "...", + "severity": "secondary" + }, + "data": { "a": "a2", "b":"b2"} + }, + { "nodeType": "eicBasic", + "id": "cid", + "ncoords": { "x": 150, "y": 320}, + "markup": { + "title": "CID", + "subtitle": "...", + "severity": "secondary" + }, + "data": { "a": "a3", "b":"b3"} + }, + { + "nodeType": "eicBasic", + "id": "allocation", + "markup": { + "title": "Case Allocation", + "subtitle": "...", + "severity": "secondary" + }, + "data": { + "track": "equity" + } + }, + { + "nodeType": "eicBasic", + "id": "signature", + "markup": { + "title": "Grant Signature", + "subtitle": "...", + "severity": "secondary" + }, + "data": { + "track": "grant" + } + }, + { + "nodeType": "eicBasic", + "id": "progress-meeting", + "markup": { + "title": "Progress Meetings", + "subtitle": "...", + "severity": "secondary" + }, + "data": { + "track": "grant", + "instanciable": true + } + }, + { + "nodeType": "eicBasic", + "id": "techdd", + "markup": { + "title": "Tech Due Diligences", + "subtitle": "...", + "severity": "secondary" + }, + "data": { + "track": "equity" + } + }, + { + "nodeType": "eicBasic", + "id": "kyc", + "markup": { + "title": "KYC", + "subtitle": "...", + "severity": "secondary" + }, + "data": { + "track": "equity" + } + }, + { + "nodeType": "eicBasic", + "id": "aifm-advisory", + "markup": { + "title": "AIFM Advisory Commitee", + "subtitle": "...", + "severity": "secondary" + }, + "data": { + "track": "equity" + } + }, + { + "nodeType": "eicBasic", + "id": "aifm-investment", + "markup": { + "title": "AIFM Investment Commitee", + "subtitle": "...", + "severity": "secondary" + }, + "data": { + "track": "equity", + "parent": "aifm-advisory" + } + }, + { + "nodeType": "eicBasic", + "id": "agreement", + "markup": { + "title": "Investment Agreement", + "subtitle": "...", + "severity": "secondary" + }, + "data": { + "track": "equity", + "parent": "aifm-investment" + } + } + ], + "links": [ + { "from": ["eval", "out2"], "to": ["gap", "in1"] }, + { "from": ["eval", "out1"], "to": ["cid", "in1"] }, + { "from": ["eval", "out3"], "to": ["allocation", "in1"] }, + { "from": ["gap", "out1"], "to": ["signature", "in1"] }, + { "from": ["signature", "out1"], "to": ["progress-meeting", "in1"] }, + { "from": ["cid", "out1"], "to": ["techdd", "in1"] }, + { "from": ["allocation", "out1"], "to": ["techdd", "in3"] }, + { "from": ["allocation", "out2"], "to": ["kyc", "in2"] }, + { "from": ["techdd", "out1"], "to": ["aifm-advisory", "in1"] }, + { "from": ["kyc", "out1"], "to": ["aifm-advisory", "in3"] }, + { "from": ["aifm-advisory", "out1"], "to": ["aifm-investment", "in1"] }, + { "from": ["aifm-investment", "out1"], "to": ["agreement", "in1"] }, + { "from": ["gap", "out3"], "to": ["aifm-investment", "in1"] } + ] + } +} \ No newline at end of file diff --git a/app/thirdparty/buildoz/bzGraflow.js b/app/thirdparty/buildoz/bzGraflow.js index eb698b4..cf60522 100644 --- a/app/thirdparty/buildoz/bzGraflow.js +++ b/app/thirdparty/buildoz/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 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 @@ -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) {