diff --git a/app/assets/html/test.html b/app/assets/html/test.html index 0438f5e..d6cefd0 100644 --- a/app/assets/html/test.html +++ b/app/assets/html/test.html @@ -8,10 +8,11 @@ diff --git a/app/assets/html/test1.html b/app/assets/html/test1.html index d2137a1..7b56418 100644 --- a/app/assets/html/test1.html +++ b/app/assets/html/test1.html @@ -74,7 +74,7 @@ - +
diff --git a/app/assets/html/test5.html b/app/assets/html/test5.html new file mode 100644 index 0000000..0994574 --- /dev/null +++ b/app/assets/html/test5.html @@ -0,0 +1,92 @@ + + + + graflow + + + + + + + + + + + + +
+ + + +
+
+
+ + + diff --git a/app/assets/json/bzGraflow/testFlow1.1.json b/app/assets/json/bzGraflow/testFlow1.1.json new file mode 100644 index 0000000..eb8c1fe --- /dev/null +++ b/app/assets/json/bzGraflow/testFlow1.1.json @@ -0,0 +1,65 @@ +{ + "nodesFile": "/app/assets/html/bzGraflow/nodesTest1.html", + "flow": { + "nodes":[ + { "nodeType": "inc", + "id": "aze", + "coords": { "x": 220, "y": 120} + }, + { "nodeType": "inc", + "subflow": { + "url": "/app/assets/json/bzGraflow/testFlowEic.json", + "portLinks": [ + { "refNodeType": "refnodein", "refnodePort": "out1", + "parentPort": "in1", + "subflowNode":"aze2", "subflowPort": "in1", + "direction": "in" + }, + { "refNodeType": "refnodeout", "refnodePort": "in1", + "parentPort": "out1", + "subflowNode":"aze5", "subflowPort": "out1", + "direction": "out" + } + ] + }, + "id": "aze2", + "coords": { "x": 220, "y": 10} + }, + { "nodeType": "factor", + "id": "qsd", + "coords": { "x": 470, "y": 170} + }, + { "nodeType": "factor", + "id": "qsd2", + "coords": { "x": 470, "y": 50} + }, + { "nodeType": "wadder", + "id": "wcx", + "coords": { "x": 720, "y": 50} + }, + { "nodeType": "multiplier", + "id": "ert", + "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": ["qsd2", "out1"], "to": ["ert", "inp2"] }, + { "from": ["0000", "out1"], "to": ["aze2", "inp1"] }, + { "from": ["qsd2", "out1"], "to": ["wcx", "inp2"] }, + { "from": ["wcx", "out1"], "to": ["ert", "inp1"] }, + { "from": ["qsd", "out1"], "to": ["wcx", "inp1"] }, + { "from": ["ert", "out1"], "to": ["9999", "inp1"] } + ] + } +} \ No newline at end of file diff --git a/app/assets/json/bzGraflow/testFlow1.json b/app/assets/json/bzGraflow/testFlow1.json index eb8c1fe..8c8968b 100644 --- a/app/assets/json/bzGraflow/testFlow1.json +++ b/app/assets/json/bzGraflow/testFlow1.json @@ -7,21 +7,6 @@ "coords": { "x": 220, "y": 120} }, { "nodeType": "inc", - "subflow": { - "url": "/app/assets/json/bzGraflow/testFlowEic.json", - "portLinks": [ - { "refNodeType": "refnodein", "refnodePort": "out1", - "parentPort": "in1", - "subflowNode":"aze2", "subflowPort": "in1", - "direction": "in" - }, - { "refNodeType": "refnodeout", "refnodePort": "in1", - "parentPort": "out1", - "subflowNode":"aze5", "subflowPort": "out1", - "direction": "out" - } - ] - }, "id": "aze2", "coords": { "x": 220, "y": 10} }, diff --git a/app/thirdparty/buildoz/buildoz.css b/app/thirdparty/buildoz/buildoz.css index 0a556b4..8916695 100644 --- a/app/thirdparty/buildoz/buildoz.css +++ b/app/thirdparty/buildoz/buildoz.css @@ -204,3 +204,38 @@ bz-graflow .bzgf-main-container{ position: relative; box-sizing: border-box; } + +/* BZGRAFLOW_CORE_START */ +/* bz-graflow internal layout rules (used in light DOM, and injected into shadow DOM when isolated) */ +bz-graflow .bzgf-wires-container, +bz-graflow .bzgf-nodes-container{ + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} +bz-graflow .bzgf-nodes-container .bzgf-node{ position:absolute; } +bz-graflow .bzgf-nodes-container .bzgf-fake-node{ + position: absolute; + width: 5px; + height: 5px; + background: transparent; + border-style: none; +} +bz-graflow .bzgf-nodes-container button.bzgf-zoom-in{ + z-index: 999; + position: absolute; + top: -0.5em; + right: -1em; + color: black; + width: 2em; + height: 2em; + padding: 0; +} +bz-graflow button.bzgf-zoom-out{ + z-index: 999; + position: absolute; + left: 5px; + top: 5px; +} +/* BZGRAFLOW_CORE_END */ diff --git a/app/thirdparty/buildoz/bzGraflow.js b/app/thirdparty/buildoz/bzGraflow.js index b7f7176..249bffd 100644 --- a/app/thirdparty/buildoz/bzGraflow.js +++ b/app/thirdparty/buildoz/bzGraflow.js @@ -34,6 +34,32 @@ class BZgraflow extends Buildoz{ this.currentOrientation = null } + static _coreCssPromise = null + + static async getCoreCss(){ + if(BZgraflow._coreCssPromise) return(await BZgraflow._coreCssPromise) + BZgraflow._coreCssPromise = (async() => { + const res = await fetch('/app/thirdparty/buildoz/buildoz.css') + const css = await res.text() + const m = css.match(/\/\*\s*BZGRAFLOW_CORE_START\s*\*\/([\s\S]*?)\/\*\s*BZGRAFLOW_CORE_END\s*\*\//) + const core = m ? m[1] : '' + return(core) + })() + return(await BZgraflow._coreCssPromise) + } + + async ensureIsolatedCoreStyles(){ + if(!this.hasAttribute('isolated')) return + if(this._isolatedCoreInjected) return + this._isolatedCoreInjected = true + const core = await BZgraflow.getCoreCss() + // Convert light-dom selectors (`bz-graflow ...`) to shadow-dom selectors (`:host ...`) + const shadowCss = core.replaceAll('bz-graflow', ':host') + const style = document.createElement('style') + style.textContent = shadowCss + this.mainContainer.appendChild(style) + } + addIcon(el, name) { el.innerHTML = `` } @@ -41,10 +67,7 @@ class BZgraflow extends Buildoz{ connectedCallback() { super.connectedCallback() const flowUrl = this.getBZAttribute('flow') - if(!flowUrl) { - console.warn('BZgraflow: No flow URL !?') - return - } + if(!flowUrl) return // Be tolerant: maybe injected later from JS above // If attribute "isolated" is present, render inside a shadow root. // Otherwise, render in light DOM (no shadow DOM). this.hostContainer = document.createElement('div') @@ -52,37 +75,7 @@ class BZgraflow extends Buildoz{ this.mainContainer = this.hasAttribute('isolated') ? this.hostContainer.attachShadow({ mode: 'open' }) : this.hostContainer - const style = document.createElement('style') - //TODO kick this wart somewhere under a carpet - style.textContent = ` - .bzgf-wires-container, - .bzgf-nodes-container{ position: absolute; inset: 0; width: 100%; height: 100%; } - .bzgf-nodes-container .bzgf-node{ position:absolute; } - .bzgf-nodes-container .bzgf-fake-node{ - position: absolute; - width: 5px; - height: 5px; - backgrround: transparent; - border-style: none; - } - .bzgf-nodes-container button.bzgf-zoom-in{ - z-index: 999; - position: absolute; - top: -0.5em; - right: -1em; - color: black; - width: 2em; - height: 2em; - padding: 0; - } - bz-graflow button.bzgf-zoom-out{ - z-index: 999; - position: absolute; - left: 5px; - top: 5px; - } - ` - this.mainContainer.appendChild(style) + this.ensureIsolatedCoreStyles() this.nodesContainer = document.createElement('div') this.nodesContainer.classList.add('bzgf-nodes-container') this.wiresContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg') @@ -91,9 +84,13 @@ class BZgraflow extends Buildoz{ this.mainContainer.append(this.wiresContainer) this.mainContainer.append(this.nodesContainer) this.append(this.hostContainer) - this.loadFlow(flowUrl) // Let it load async + this.loadFlow(flowUrl).then(() => { + if(this.hasAttribute('editable')){ + this.dnd = new MovingNodes(this) + this.dnd.enableMovingNodes('.bzgf-node') } + }) // Let it load async } - + error(msg, err){ this.innerHTML = `
${msg}
` if(err) console.error(msg, err) @@ -290,11 +287,7 @@ class BZgraflow extends Buildoz{ childEl.addEventListener('transitionend', (e) => { if(e.propertyName !== 'transform') return this.hostContainer.style.visibility = 'hidden' - // Important for nested subflows: - // A non-'none' transform on this element creates a containing block, which would make - // any nested `position:fixed` subflow overlay position relative to this element instead - // of the viewport (showing up as an extra offset like 8px). - childEl.style.transform = 'none' + childEl.style.transform = 'none' // Important for nested subflows to position correctly childEl.style.willChange = '' this.dispatchEvent(new CustomEvent('subflowLoaded', { detail: { subflow: childEl }, @@ -1031,7 +1024,69 @@ class BZgraflow extends Buildoz{ } return(crossLayerLinks) } -} +} Buildoz.define('graflow', BZgraflow) +class MovingNodes{ + constructor(graflow){ + this.graflow = graflow + this.nodesContainer = this.graflow.mainContainer.querySelector('.bzgf-nodes-container') + this.state = null + } + + enableMovingNodes(itemSelector, handleSelector = itemSelector) { + this.itemSelector = itemSelector + this.handleSelector = handleSelector + if(!this._handleCursorStyle){ + const style = document.createElement('style') + style.textContent = `${handleSelector}{ cursor: move }` + this.nodesContainer.appendChild(style) + this._handleCursorStyle = style + } + + this.nodesContainer.addEventListener('pointerdown', this.pointerDown.bind(this)) + this.nodesContainer.addEventListener('pointermove', this.pointerMove.bind(this)) + this.nodesContainer.addEventListener('pointerup', this.pointerUp.bind(this)) + } + + pointerDown(e){ + const node = (e.target.classList.contains(this.itemSelector)) ? e.target : e.target.closest(this.itemSelector) + const handle = (node.classList.contains(this.handleSelector)) ? node : node.querySelector(this.handleSelector) + + const rect = node.getBoundingClientRect() + + this.state = { + node, + handle, + startX: e.clientX, + startY: e.clientY, + offsetX: rect.left, + offsetY: rect.top + } + const x = e.clientX - this.state.startX + this.state.offsetX + const y = e.clientY - this.state.startY + this.state.offsetY + node.setPointerCapture(e.pointerId) + node.style.position = 'absolute' + node.style.left = `${x}px` + node.style.top = `${y}px` + node.style.margin = '0' + node.style.zIndex = '9999' + } + + pointerMove(e){ + if(!this.state) return + const { node, startX, startY, offsetX, offsetY } = this.state + const x = e.clientX - startX + offsetX + const y = e.clientY - startY + offsetY + node.style.left = `${x}px` + node.style.top = `${y}px` + this.graflow.updateWires(node.dataset.id, this.graflow.currentOrientation) + } + + pointerUp(e){ + if(!this.state) return + this.state.node.releasePointerCapture(e.pointerId) + this.state = null + } +} \ No newline at end of file