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)