diff --git a/app/assets/html/bzGraflow/nodesTest1.html b/app/assets/html/bzGraflow/nodesTest1.html index c860cb7..912b37a 100644 --- a/app/assets/html/bzGraflow/nodesTest1.html +++ b/app/assets/html/bzGraflow/nodesTest1.html @@ -27,6 +27,7 @@ border-radius: 6px; border: 1px solid #CCC; } + .bzgf-node .body span{ font-size: 12px; text-align: left; line-height: 21px; } .bzgf-node .port{ position: absolute; height: 10px; @@ -48,38 +49,124 @@ } .bzgf-node[data-nodetype="inc"] .title{ background: #090; } + + .bzgf-node[data-nodetype="wadder"]{ + background: #DFD; + border-color: #090; + height:150px + } + .bzgf-node[data-nodetype="wadder"] .body{ display: grid; grid-gap: 4px; margin-left:0.5em; grid-template-columns: 1fr 1fr; align-items: center; } + .bzgf-node[data-nodetype="wadder"] .title{ background: #090; } + .bzgf-node[data-nodetype="wadder"] .port[data-id="inp1"] { top:37px; } + .bzgf-node[data-nodetype="wadder"] .port[data-id="inp2"] { top:63px; } + .bzgf-node[data-nodetype="wadder"] .port[data-id="inp3"] { top:89px; } + .bzgf-node[data-nodetype="wadder"] .port[data-id="inp4"] { top:115px; } + .bzgf-node[data-nodetype="wadder"] .port[data-id="inp5"] { top:141px; } + .bzgf-node[data-nodetype="factor"]{ background: #DDF; border-color: #009; } .bzgf-node[data-nodetype="factor"] .title{ background: #009; } - .bzgf-wire{ - stroke: #0AF; - stroke-width: 2; + .bzgf-node[data-nodetype="multiplier"]{ + background: #DDF; + border-color: #009; + height:110px } + .bzgf-node[data-nodetype="multiplier"] .body{ + font-size:40px; + font-weight: bold; + align-items: center; + display: flex; + justify-content: center; + margin-top: 1em; + } + .bzgf-node[data-nodetype="multiplier"] .title{ background: #009; } + .bzgf-node[data-nodetype="multiplier"] .port[data-id="inp1"] { top:37px; } + .bzgf-node[data-nodetype="multiplier"] .port[data-id="inp2"] { top:63px; } + .bzgf-node[data-nodetype="multiplier"] .port[data-id="inp3"] { top:89px; } + + .bzgf-node[data-nodetype="input"], + .bzgf-node[data-nodetype="console"]{ + background: #CCC; + border-color: #555; + } + .bzgf-node[data-nodetype="input"] .title, + .bzgf-node[data-nodetype="console"] .title{ background: #555; } + + .bzgf-wire{ stroke: #0AF; stroke-width: 2; } + + + + + + + + \ No newline at end of file diff --git a/app/assets/html/test.html b/app/assets/html/test.html index 637b615..715c27d 100644 --- a/app/assets/html/test.html +++ b/app/assets/html/test.html @@ -12,6 +12,6 @@ - + diff --git a/app/assets/json/bzGraflow/testFlow1.json b/app/assets/json/bzGraflow/testFlow1.json index f254f34..369de2d 100644 --- a/app/assets/json/bzGraflow/testFlow1.json +++ b/app/assets/json/bzGraflow/testFlow1.json @@ -4,27 +4,47 @@ "nodes":[ { "nodeType": "inc", "id": "aze", - "coords": { "x": 20, "y": 10} + "coords": { "x": 220, "y": 10} + }, + { "nodeType": "inc", + "id": "aze2", + "coords": { "x": 220, "y": 120} + }, + { "nodeType": "factor", + "id": "qsd2", + "coords": { "x": 470, "y": 170} }, { "nodeType": "factor", "id": "qsd", - "coords": { "x": 270, "y": 50} - }, - { "nodeType": "inc", + "coords": { "x": 470, "y": 50} + }, + { "nodeType": "wadder", "id": "wcx", - "coords": { "x": 520, "y": 50} + "coords": { "x": 720, "y": 50} }, - { "nodeType": "inc", + { "nodeType": "multiplier", "id": "ert", - "coords": { "x": 150, "y": 350} + "coords": { "x": 550, "y": 350} + }, + { "nodeType": "input", + "id": "0000", + "coords": { "x": 20, "y": 350} + }, + { "nodeType": "console", + "id": "9999", + "coords": { "x": 800, "y": 350} } ], "links": [ + { "from": ["0000", "out1"], "to": ["aze", "inp1"] }, + { "from": ["aze2", "out1"], "to": ["qsd2", "inp1"] }, { "from": ["aze", "out1"], "to": ["qsd", "inp1"] }, + { "from": ["0000", "out1"], "to": ["aze2", "inp1"] }, + { "from": ["qsd2", "out1"], "to": ["wcx", "inp2"] }, + { "from": ["wcx", "out1"], "to": ["ert", "inp1"] }, + { "from": ["qsd2", "out1"], "to": ["ert", "inp2"] }, { "from": ["qsd", "out1"], "to": ["wcx", "inp1"] }, - { "from": ["qsd", "out1"], "to": ["ert", "inp2"] }, - { "from": ["ert", "out1"], "to": ["aze", "inp2"] } - + { "from": ["ert", "out1"], "to": ["9999", "inp1"] } ] } } \ No newline at end of file diff --git a/app/assets/styles/app.css b/app/assets/styles/app.css index acc992f..88398d7 100755 --- a/app/assets/styles/app.css +++ b/app/assets/styles/app.css @@ -48,7 +48,9 @@ body[eicapp] { height: 100vh; background: transparent; pointer-events: none; - z-index: 9; + position: absolute; + inset: 0; + z-index: 9; } [eicapp] [eicapptoolbar] { @@ -144,7 +146,8 @@ menu[eicmenu] [menuitem] > a > button, menu[eicmenu] [menuitem] > .nolink button [eicapp] .app-workspace .window > header .controls button.shrink { display: none; } [eicapp] .app-workspace .window > section { cursor: default; - overflow: hidden; + height: auto; + overflow: auto; /* important for windows content to be scrollable */ transition: all 0.5s; flex: 1 1 auto; width: 100%; @@ -336,7 +339,7 @@ menu[eicmenu] [menuitem] a label { color: #4E4; } menu[eicmenu] [menuitem] i[class^="icon-"] { - color:#fdfb93;; + color:#070;; } article[eiccard] > header h1{ text-align: center; } diff --git a/app/thirdparty/buildoz/buildoz.css b/app/thirdparty/buildoz/buildoz.css index 84bc42d..4b999ed 100644 --- a/app/thirdparty/buildoz/buildoz.css +++ b/app/thirdparty/buildoz/buildoz.css @@ -201,4 +201,5 @@ bz-graflow .bzgf-nodes-container{ height: 100%; } bz-graflow .bzgf-nodes-container{ z-index:10; } -bz-graflow .bzgf-wires-container{ z-index:9; } \ No newline at end of file +bz-graflow .bzgf-wires-container{ z-index:9; } +bz-graflow .bzgf-nodes-container .bzgf-node{ position:absolute; } \ No newline at end of file diff --git a/app/thirdparty/buildoz/bzGraflow.js b/app/thirdparty/buildoz/bzGraflow.js index 0e813f5..cbfacc7 100644 --- a/app/thirdparty/buildoz/bzGraflow.js +++ b/app/thirdparty/buildoz/bzGraflow.js @@ -2,7 +2,7 @@ class BZgraflow extends Buildoz{ constructor(){ super() - this.defaultAttrs = { } + this.defaultAttrs = { tension: 100 } this.stagedNodes = { } this.stagedWires = { } } @@ -15,9 +15,7 @@ class BZgraflow extends Buildoz{ console.warn('BZgraflow: No flow URL !?') return } - - this.loadFlow(flowUrl) // Let it load async while we coat - + this.loadFlow(flowUrl) // Let it load async while we coat this.nodesContainer = document.createElement('div') this.nodesContainer.classList.add('bzgf-nodes-container') this.wiresContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg') @@ -42,6 +40,8 @@ class BZgraflow extends Buildoz{ } await this.loadNodes(flowObj.nodesFile) this.flow = flowObj.flow + + if(this.hasAnyLoop()) console.warn('This flow has loops.Optimization not available') this.refresh() } @@ -73,31 +73,20 @@ class BZgraflow extends Buildoz{ addNode(type, id, x, y){ const nodeDef = this.nodesRegistry[type] this.stagedNodes[id] = nodeDef.cloneNode(true) + const portEls = this.stagedNodes[id].querySelectorAll('.port') this.stagedNodes[id].style.left = `${x}px` this.stagedNodes[id].style.top = `${y}px` this.stagedNodes[id].dataset.id = id - const portEls = this.stagedNodes[id].querySelectorAll('.port') this.stagedNodes[id].ports = Object.fromEntries(Array.from(portEls).map(item => ([item.dataset.id, { ...item.dataset, el:item }]))) this.nodesContainer.append(this.stagedNodes[id]) return(this.stagedNodes[id]) } addWire(idNode1, idPort1, idNode2, idPort2){ - const node1 = this.stagedNodes[idNode1] - const port1 = node1.ports[idPort1] - const node2 = this.stagedNodes[idNode2] - const port2 = node2.ports[idPort2] - const id = `${node1.dataset.id}_${node2.dataset.id}` - const bb1 = port1.el.getBoundingClientRect() - const bb2 = port2.el.getBoundingClientRect() - const x1 = Math.floor(bb1.x + (bb1.width/2)) - const y1 = Math.floor(bb1.y + (bb1.height/2)) - const x2 = Math.floor(bb2.x + (bb2.width/2)) - const y2 = Math.floor(bb2.y + (bb2.height/2)) - - console.log('====>', x1, y1, x2, y2) + const path = this.bezier(idNode1, idPort1, idNode2, idPort2, this.getBZAttribute('tension')) + const id = `${idNode1}_${idNode2}` this.stagedWires[id] = document.createElementNS('http://www.w3.org/2000/svg', 'path') - this.stagedWires[id].setAttribute('d', this.bezier(x1, y1, port1.direction , x2, y2, port2.direction, 60)) + this.stagedWires[id].setAttribute('d', path) this.stagedWires[id].setAttribute('fill', 'none') this.stagedWires[id].classList.add('bzgf-wire') this.stagedWires[id].dataset.id = id @@ -118,41 +107,172 @@ class BZgraflow extends Buildoz{ } } - - bezier(x1, y1, dir1, x2, y2, dir2, tensionMin=60) { + bezier(idNode1, idPort1, idNode2, idPort2, tensionMin=60) { const dirVect = { n: { x: 0, y: -1 }, s: { x: 0, y: 1 }, e: { x: 1, y: 0 }, w: { x: -1, y: 0 }, } + const node1 = this.stagedNodes[idNode1] + const port1 = node1.ports[idPort1] + const node2 = this.stagedNodes[idNode2] + const port2 = node2.ports[idPort2] + const bb1 = port1.el.getBoundingClientRect() + const bb2 = port2.el.getBoundingClientRect() + const x1 = Math.floor(bb1.x + (bb1.width/2)) + const y1 = Math.floor(bb1.y + (bb1.height/2)) + const x2 = Math.floor(bb2.x + (bb2.width/2)) + const y2 = Math.floor(bb2.y + (bb2.height/2)) const dist = Math.abs(x2 - x1) + Math.abs(y2 - y1) let tension = dist * 0.4 - if (tension < tensionMin) tension = tensionMin + if(tension < tensionMin) tension = tensionMin - const c1x = x1 + (dirVect[dir1].x * tension) - const c1y = y1 + (dirVect[dir1].y * tension) + const c1x = x1 + (dirVect[port1.direction].x * tension) + const c1y = y1 + (dirVect[port1.direction].y * tension) - const c2x = x2 + (dirVect[dir2].x * tension) - const c2y = y2 + (dirVect[dir2].y * tension) + const c2x = x2 + (dirVect[port2.direction].x * tension) + const c2y = y2 + (dirVect[port2.direction].y * tension) return `M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}` } + + autoPlace(orientation = 'horizontal', gapx = 80, gapy = 30){ + if(!['horizontal', 'vertical'].includes(orientation)) return + + const parents = {} + const adj = {} + + this.flow.nodes.forEach(n => { + parents[n.id] = [] + adj[n.id] = [] + }) + + this.flow.links.forEach(link => { + const from = link.from[0] + const to = link.to[0] + parents[to].push(from) + adj[from].push(to) + }) + const layers = this.computeLayers(this.flow.nodes, parents) +// const maxLayers = Math.max(...layers.map(item=>item.length)) + let maxHeight = 0 + const layerHeights = [] + for(const layer of layers){ + let totHeight = 0 + for(const nid of layer){ + const bb = this.stagedNodes[nid].getBoundingClientRect() + totHeight += bb.height+gapy + } + if(totHeight>maxHeight) maxHeight = totHeight + layerHeights.push(totHeight) + } -/* -portPosition(nodeEl, portEl) { - const nodeRect = nodeEl.getBoundingClientRect() - const portRect = portEl.getBoundingClientRect() - const canvasRect = this.canvas.getBoundingClientRect() + let x = gapx + for(const [idx, layer] of layers.entries()){ + for(const nid of layer){ + // some parents can be in far layers, but at least one is in the prev layer (by definition of layer) + const localParent = parents[nid].find((nid => layers[idx-1].includes(nid))) + if(localParent){ //undefined for 1st node + const ports = this.stagedNodes[localParent].ports + console.log(`parent of ${nid} is ${localParent}`, ports) + } + } - return { - x: portRect.left - canvasRect.left + portRect.width / 2, - y: portRect.top - canvasRect.top + portRect.height / 2 - } -} + let wMax = 0 + let y = ((maxHeight - layerHeights[idx]) / 2) + gapy + for(const nid of layer){ + const bb = this.stagedNodes[nid].getBoundingClientRect() + wMax = (bb.width > wMax) ? bb.width : wMax + this.moveNode(nid, x, y, 1000) + y += gapy + bb.height + } + x += wMax + gapx + } + } -*/ + moveNode(nid, destx, desty, duration = 200) { + const t0 = performance.now() + const bb = this.stagedNodes[nid].getBoundingClientRect() + const x0=bb.x + const y0 = bb.y + function frame(t) { + const p = Math.min((t - t0) / duration, 1) + const k = p * p * (3 - 2 * p) // smoothstep + const x = x0 + (destx - x0) * k + const y = y0 + (desty - y0) * k + + this.stagedNodes[nid].style.left = `${x}px` + this.stagedNodes[nid].style.top = `${y}px` + this.updateWires(nid) + + if(p < 1) requestAnimationFrame(frame.bind(this)) + } + requestAnimationFrame(frame.bind(this)) + } + updateWires(nid){ + const wires = Object.keys(this.stagedWires) + .filter(id => (id.startsWith(nid+'_')||id.endsWith('_'+nid))) + .map(id => this.stagedWires[id]) + for(const wire of wires){ + const [nid1, nid2] = wire.dataset.id.split('_') + const lnk = this.getLink(nid1, nid2) + const path = this.bezier(nid1, lnk.from[1], nid2, lnk.to[1], this.getBZAttribute('tension')) + wire.setAttribute('d', path) + } + } + + getLink(nid1, nid2){ + return(this.flow.links.find(item => ((item.from[0]==nid1) && (item.to[0]==nid2)))) + } + + computeLayers(nodes, parents) { + const layer = {} + const dfs = (id) => { + if (layer[id] !== undefined) return(layer[id]) + + if (parents[id].length === 0) { + layer[id] = 0 + } else { + layer[id] = 1 + Math.max(...parents[id].map(dfs)) + } + return(layer[id]) + } + nodes.forEach(n => dfs(n.id)) + const t = [] + for(const nid in layer) { + if(!t[layer[nid]]) t[layer[nid]]=[] + t[layer[nid]].push(nid) + } + return(t) + } + + hasAnyLoop(nodes, links) { + // self-loops + if(this.flow.links.some(l => l.from[0] === l.to[0])) return(true) + + // multi-node cycles + const adj = {}; + this.flow.nodes.forEach(node => adj[node.id] = []) + this.flow.links.forEach(link => adj[link.from[0]].push(link.to[0])) + + const visiting = new Set(); + const visited = new Set() + const dfs = (nid) => { + if(visiting.has(nid)) return(true) + if(visited.has(nid)) return(false) + visiting.add(nid) + for(const m of adj[nid]) { + if(dfs(m)) return(true) + } + visiting.delete(nid) + visited.add(nid) + return(false) + } + return(this.flow.nodes.map(n => n.id).some(dfs)) + } + } Buildoz.define('graflow', BZgraflow)