diff --git a/buildoz.css b/buildoz.css index 311e0d3..84bc42d 100644 --- a/buildoz.css +++ b/buildoz.css @@ -186,4 +186,19 @@ bz-slidepane[side="right"] div.handle { background: repeating-linear-gradient( to right, rgba(255,255,255,1) 0, rgba(255,255,255,1) 2px, rgba(0,0,0,0.2) 3px, rgba(0,0,0,0.2) 4px ); transform: translateY(-50%); cursor: ew-resize; -} \ No newline at end of file +} +bz-graflow { + position: relative; + display: block; + width: 100vw; + height: 100vh; +} +bz-graflow .bzgf-wires-container, +bz-graflow .bzgf-nodes-container{ + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} +bz-graflow .bzgf-nodes-container{ z-index:10; } +bz-graflow .bzgf-wires-container{ z-index:9; } \ No newline at end of file diff --git a/bzGraflow.js b/bzGraflow.js new file mode 100644 index 0000000..0e813f5 --- /dev/null +++ b/bzGraflow.js @@ -0,0 +1,159 @@ +class BZgraflow extends Buildoz{ + + constructor(){ + super() + this.defaultAttrs = { } + this.stagedNodes = { } + this.stagedWires = { } + } + static _loadedNodeStyles = new Set() // Allow multi instances or re-loadNodes, but avoid reinjecting same styles ! + + connectedCallback() { + super.connectedCallback() + const flowUrl = this.getBZAttribute('flow') + if(!flowUrl) { + console.warn('BZgraflow: No flow URL !?') + return + } + + 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') + this.wiresContainer.classList.add('bzgf-wires-container') + this.append(this.wiresContainer) + this.append(this.nodesContainer) + } + + async loadFlow(url){ + const res = await fetch(url+'?'+crypto.randomUUID()) + const buf = await res.text() + let flowObj + try{ + flowObj = JSON.parse(buf) + } catch(err){ + console.error('Could not parse flow JSON!?', err) + return + } + if(!flowObj.nodesFile){ + console.error('No nodesFile in JSON!?') + return + } + await this.loadNodes(flowObj.nodesFile) + this.flow = flowObj.flow + this.refresh() + } + + async loadNodes(url) { + const res = await fetch(url+'?'+crypto.randomUUID()) + const html = await res.text() + + // Get nodes + const doc = new DOMParser().parseFromString(html, 'text/html') + this.nodesRegistry = {} + for(const tpl of doc.querySelectorAll('template')){ + const rootEl = tpl.content.querySelector('.bzgf-node') + if(!rootEl) continue + this.nodesRegistry[rootEl.dataset.nodetype] = rootEl + } + + // Now load styles (once) + if(!BZgraflow._loadedNodeStyles.has(url)) { + const styles = doc.querySelectorAll('style') + styles.forEach(styleEl => { + const style = document.createElement('style') + style.textContent = styleEl.textContent + document.head.appendChild(style) + }) + BZgraflow._loadedNodeStyles.add(url) + } + } + + addNode(type, id, x, y){ + const nodeDef = this.nodesRegistry[type] + this.stagedNodes[id] = nodeDef.cloneNode(true) + 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) + 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('fill', 'none') + this.stagedWires[id].classList.add('bzgf-wire') + this.stagedWires[id].dataset.id = id + this.wiresContainer.append(this.stagedWires[id]) + return(this.stagedWires[id]) + } + + refresh(){ + let x = 0 + let y = 0 + for(const node of this.flow.nodes){ + const nodeEl = this.addNode(node.nodeType, node.id , node.coords.x, node.coords.y) + } + for(const link of this.flow.links){ + const [nodeId1, portId1] = link.from + const [nodeId2, portId2] = link.to + this.addWire(nodeId1, portId1, nodeId2, portId2) + } + } + + + bezier(x1, y1, dir1, x2, y2, dir2, 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 dist = Math.abs(x2 - x1) + Math.abs(y2 - y1) + let tension = dist * 0.4 + if (tension < tensionMin) tension = tensionMin + + const c1x = x1 + (dirVect[dir1].x * tension) + const c1y = y1 + (dirVect[dir1].y * tension) + + const c2x = x2 + (dirVect[dir2].x * tension) + const c2y = y2 + (dirVect[dir2].y * tension) + return `M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}` + } + + +/* +portPosition(nodeEl, portEl) { + const nodeRect = nodeEl.getBoundingClientRect() + const portRect = portEl.getBoundingClientRect() + const canvasRect = this.canvas.getBoundingClientRect() + + return { + x: portRect.left - canvasRect.left + portRect.width / 2, + y: portRect.top - canvasRect.top + portRect.height / 2 + } +} + +*/ + +} +Buildoz.define('graflow', BZgraflow) + +