We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add node selection menu.
scanning-pipeline/src/node-graph.js
Line 180 in 16f13b2
/* exported NodeCallback */ class NodeCallback { /** * @param {GraphNodeUI} graphNodeUI */ constructor(graphNodeUI) { this.graphNodeUI = graphNodeUI; /** * @public * @type {boolean} */ this.abortFlag = false; } /** * @public * @param {string} info */ setInfo(info) {} /** * @public * @param {number} progressPercent */ setProgressPercent(progressPercent) { this.graphNodeUI.domProgressElement.hidden = false; if (progressPercent <= 0) { this.graphNodeUI.domProgressElement.removeAttribute("value"); } else if (progressPercent >= 100) { this.graphNodeUI.domProgressElement.hidden = true; } else { this.graphNodeUI.domProgressElement.value = progressPercent; } } } class NodeGraph { /** * @param {HTMLElement} parentElement */ constructor(parentElement) { /** * @protected * @type {GraphNode[]} */ this.registeredNodes = []; /** * @protected * @type {GraphNodeUI[]} */ this.placedNodes = []; this.parentElement = parentElement; this.domCanvas = document.createElement("canvas"); this.domCanvas.style.backgroundColor = "transparent"; this.domCanvas.style.position = "absolute"; this.domCanvas.style.width = "100%"; this.domCanvas.style.height = "100%"; window.addEventListener("resize", this.resizeHandler.bind(this)); this.parentElement.appendChild(this.domCanvas); this.domCanvasContext = this.domCanvas.getContext("2d"); this.currentMousePosition = { x: this.parentElement.clientWidth / 2, y: this.parentElement.clientHeight / 2, }; this.parentElement.addEventListener( "mousemove", this.mousemoveHandler.bind(this) ); this.parentElement.addEventListener( "mouseup", this.mouseUpHandler.bind(this) ); /** * @private * @type {GraphNodeUI} */ this.grabbedNode = null; /** * @private * @type {GraphNodeInputUI | GraphNodeOutputUI} */ this.linkedNodeIO = null; this.resizeHandler(); } /** * @public * @param {Function} nodeExecuter * @returns {GraphNode} */ registerNode(nodeExecuter) { const graphNode = new GraphNode(nodeExecuter, false); this.registeredNodes.push(graphNode); return graphNode; } /** * @public * @param {Function} nodeExecuter * @param {string[]} dependencies * @returns {GraphNode} */ registerNodeAsWorker(nodeExecuter, ...dependencies) { const graphNode = new GraphNode(nodeExecuter, true, ...dependencies); this.registeredNodes.push(graphNode); return graphNode; } /** * @param {GraphNode} graphNode * @param {{x:number, y:number}} position * @returns {GraphNodeUI} */ placeNode(graphNode, position = this.currentMousePosition) { const graphNodeUI = new GraphNodeUI(graphNode, this); this.placedNodes.push(graphNodeUI); graphNodeUI.setPosition(position); this.parentElement.appendChild(graphNodeUI.domElement); return graphNodeUI; } /** * @public * @param {InputGraphNode} inputGraphNode * @param {{x:number, y:number}} position */ placeInputGraphNode(inputGraphNode, position) { this.placedNodes.push(inputGraphNode); if (position) inputGraphNode.setPosition(position); this.parentElement.appendChild(inputGraphNode.domElement); } /** * @public * @param {string} type * @param {{x:number, y:number}} position * @param {any} initValue * @returns {GraphNodeUI} */ createInputNode(type, position, initValue = undefined) { const inputGraphNode = new InputGraphNode(this, type); if (initValue) { inputGraphNode.setValue(initValue); } this.placeInputGraphNode(inputGraphNode, position); return inputGraphNode; } /** * @public * @param {Promise<GraphNodeOutputUI>} output * @param {Promise<GraphNodeInputUI>} input */ async connect(output, input) { const inputResolved = await input; const outputResolved = await output; inputResolved.setConnection(outputResolved); this.updateConnectionUI(); } /** * @param {GraphNodeUI} graphNodeUI */ displaceNode(graphNodeUI) { const graphNodeIndex = this.placedNodes.indexOf(graphNodeUI); this.placedNodes.splice(graphNodeIndex); } doubleClickHandler() { // TODO Add node selection menu. this.placeNode(this.registeredNodes[0]); } /** * @private */ resizeHandler() { this.domCanvas.height = this.parentElement.clientHeight; this.domCanvas.width = this.parentElement.clientWidth; this.updateConnectionUI(); } /** * @public * @param {GraphNodeInputUI | GraphNodeOutputUI} graphNodeIO */ toggleConnection(graphNodeIO) { if (this.linkedNodeIO === null) { this.linkedNodeIO = graphNodeIO; } else if ( graphNodeIO instanceof GraphNodeInputUI && this.linkedNodeIO instanceof GraphNodeOutputUI ) { graphNodeIO.setConnection(this.linkedNodeIO); this.linkedNodeIO = null; } else if ( graphNodeIO instanceof GraphNodeOutputUI && this.linkedNodeIO instanceof GraphNodeInputUI ) { this.linkedNodeIO.setConnection(graphNodeIO); this.linkedNodeIO = null; } this.updateConnectionUI(); } /** * @private * @returns {Promise<{input: GraphNodeInputUI, output: GraphNodeOutputUI}[]>} */ async getConnections() { /** @type {{input: GraphNodeInputUI, output: GraphNodeOutputUI}[]} */ const connections = []; this.placedNodes.forEach(async (node) => { connections.push(...(await node.getConnections())); }); return connections; } /** * @public */ async updateConnectionUI() { this.domCanvasContext.clearRect( 0, 0, this.domCanvasContext.canvas.width, this.domCanvasContext.canvas.height ); this.domCanvasContext.beginPath(); this.domCanvasContext.strokeStyle = "white"; this.domCanvasContext.lineWidth = 2; const connections = await this.getConnections(); connections.forEach((connection) => { if (connection.input && connection.output) { const startRect = connection.input.domElement.getBoundingClientRect(); const start = { x: startRect.left, y: (startRect.top + startRect.bottom) / 2, }; const endRect = connection.output.domElement.getBoundingClientRect(); const end = { x: endRect.right, y: (endRect.top + endRect.bottom) / 2, }; this.domCanvasContext.moveTo(start.x, start.y); this.domCanvasContext.lineTo(end.x, end.y); } }); this.domCanvasContext.stroke(); } mouseUpHandler() { this.grabbedNode = null; this.linkedNodeIO = null; this.updateConnectionUI(); } /** * @param {GraphNodeUI} graphNode */ setGrabbedNode(graphNode) { this.grabbedNode = graphNode; } /** * @param {GraphNodeInputUI | GraphNodeOutputUI} linkedNodeIO */ setLinkedNodeIO(linkedNodeIO) { this.linkedNodeIO = linkedNodeIO; this.updateConnectionUI(); } /** * @private * @param {MouseEvent} mouseEvent */ mousemoveHandler(mouseEvent) { this.currentMousePosition = { x: mouseEvent.pageX - this.parentElement.offsetLeft, y: mouseEvent.pageY - this.parentElement.offsetTop, }; if (this.grabbedNode) { this.grabbedNode.setPosition(this.currentMousePosition); this.updateConnectionUI(); } if (this.linkedNodeIO) { this.domCanvasContext.clearRect( 0, 0, this.domCanvasContext.canvas.width, this.domCanvasContext.canvas.height ); this.domCanvasContext.beginPath(); this.domCanvasContext.strokeStyle = "white"; this.domCanvasContext.lineWidth = 2; const startRect = this.linkedNodeIO.domElement.getBoundingClientRect(); const start = { x: startRect.right, y: (startRect.top + startRect.bottom) / 2, }; const end = this.currentMousePosition; this.domCanvasContext.moveTo(start.x, start.y); this.domCanvasContext.lineTo(end.x, end.y); this.domCanvasContext.stroke(); } } } class GraphNode { /** * @param {Function} executer * @param {boolean} asWorker * @param {string[]} dependencies */ constructor(executer, asWorker, ...dependencies) { /** * @public * @type {Function} */ this.executer = executer; this.asWorker = asWorker; /** * @private * @type {string[]} */ this.dependencies = dependencies; /** * @protected * @type {GraphNodeInput[]} */ this.graphNodeInputs = []; /** * @protected * @type {GraphNodeOutput[]} */ this.graphNodeOutputs = []; /** * @protected * @type {{output: GraphNodeOutput, input: GraphNodeInput}[]} */ this.outputConnections = []; /** * @private * @type {boolean} */ this.initialized = false; /** * @private * @type {boolean} */ this.initializing = false; } /** * @public * @returns {Promise<string>} */ async getDependenciesSource() { let dependenciesSource = ""; for (let i = 0; i < this.dependencies.length; i++) { const dependencySource = await new Promise((resolve) => { window.fetch(this.dependencies[i]).then(async (response) => { resolve(await response.text()); }); }); dependenciesSource += dependencySource; } return dependenciesSource; } /** * @private */ async initialize() { while (this.initializing === true) { await new Promise((resolve) => { setTimeout(resolve, 500); }); } if (this.initialized === false) { this.initializing = true; await this.readFunctionSourceWithDocs(); this.initialized = true; this.initializing = false; } } /** * @public * @returns {Promise<GraphNodeInput[]>} */ async getInputs() { await this.initialize(); return this.graphNodeInputs; } /** * @public * @returns {Promise<GraphNodeOutput[]>} */ async getOutputs() { await this.initialize(); return this.graphNodeOutputs; } /** * @public * @returns {string} */ getName() { return this.executer.name; } /** * @private */ async readFunctionSourceWithDocs() { const scriptElements = Array.from(document.scripts); const functionSource = this.executer.toString(); for (let i = 0, count = scriptElements.length; i < count; i++) { const scriptSource = await new Promise((resolve) => { window.fetch(scriptElements[i].src).then(async (response) => { resolve(await response.text()); }); }); if (scriptSource.includes(functionSource)) { const jsDoc = new RegExp( "(\\/\\*\\*\\s*\\n([^\\*]|(\\*(?!\\/)))*\\*\\/)(\\s*\\n*\\s*)(.*)" + this.getName() + "\\s*\\(", "mg" ) .exec(scriptSource)[0] .replaceAll("\n", "") .replaceAll("*", ""); const jsDocArguments = jsDoc.split("@"); jsDocArguments.shift(); jsDocArguments.forEach((argument) => { const argumentType = argument.split(" ")[0]; if (argumentType === "param") { const argumentVarType = argument .split("{")[1] .split("}")[0] .replace("Promise<", "") .replace(">", ""); const argumentVarName = argument.split("} ")[1].split(" ")[0]; const argumentDescription = argument.split( " " + argumentVarName + " ", 2 )[1]; this.graphNodeInputs.push( new GraphNodeInput( argumentVarName, argumentVarType, argumentDescription ) ); } else if (argumentType === "returns") { const argumentVarType = argument .split("{")[1] .split("}")[0] .replace("Promise<", "") .replace(">", ""); const argumentDescription = argument.split("} ")[1]; this.graphNodeOutputs.push( new GraphNodeOutput(argumentVarType, argumentDescription) ); } }); } } } } class GraphNodeInput { /** * @param {string} name * @param {string} type * @param {string} description */ constructor(name, type, description = undefined) { //this.name = name.replace(/([A-Z])/g, " $1"); this.name = name; this.uiName = this.name.charAt(0).toUpperCase() + this.name.slice(1); this.type = type; this.description = description.replaceAll(/\s\s+/g, " "); } } class GraphNodeOutput { /** * @param {string} type * @param {string} description */ constructor(type, description = undefined) { this.type = type; this.description = description; } } class GraphNodeInputUI extends GraphNodeInput { /** * @param {GraphNodeInput[]} graphNodeInputs * @param {NodeGraph} nodeGraph * @param {GraphNodeUI} graphNode * @returns {GraphNodeInputUI[]} */ static getFromGraphNodeInputs(graphNodeInputs, nodeGraph, graphNode) { /** @type {GraphNodeInputUI[]} */ const graphNodeInputsUI = []; graphNodeInputs.forEach((graphNodeInput) => { graphNodeInputsUI.push( new GraphNodeInputUI( graphNodeInput.name, graphNodeInput.type, graphNodeInput.description, nodeGraph, graphNode ) ); }); return graphNodeInputsUI; } /** * @param {string} name * @param {string} type * @param {string} description * @param {NodeGraph} nodeGraph * @param {GraphNodeUI} graphNodeUI * @param {string} cssClass */ constructor( name, type, description = undefined, nodeGraph, graphNodeUI, cssClass = "graphNodeInput" ) { super(name, type, description); /** * @private * @type {GraphNodeOutputUI} */ this.connection = null; this.nodeGraph = nodeGraph; this.graphNodeUI = graphNodeUI; this.domElement = document.createElement("li"); this.domElement.innerText = name; this.domElement.title = "[" + this.type + "]\n" + this.description; this.domElement.style.textAlign = "left"; this.domElement.classList.add(cssClass); this.domElement.addEventListener("click", this.clickHandler.bind(this)); this.domElement.addEventListener( "mousedown", this.mouseHandler.bind(this) ); this.domElement.addEventListener("mouseup", this.mouseHandler.bind(this)); } /** * @private * @param {MouseEvent} mouseEvent */ clickHandler(mouseEvent) { if (mouseEvent.detail > 1) { this.doubleClickHandler(); } } /** * @private */ mouseHandler() { this.nodeGraph.toggleConnection(this); } /** * @private */ doubleClickHandler() { /*const boundingRect = this.domElement.getBoundingClientRect(); this.nodeGraph.placeInputGraphNode( new InputGraphNode(this.nodeGraph, this), { x: boundingRect.left - 200, y: boundingRect.top - 25 } ); this.nodeGraph.setLinkedNodeIO(null);*/ } /** * @public * @returns {GraphNodeOutputUI} */ getConnection() { return this.connection; } /** * @public * @param {GraphNodeOutputUI} graphNodeOutput */ setConnection(graphNodeOutput) { if (this.connection) { this.connection.removeConnection(this); } graphNodeOutput.addConnection(this); this.connection = graphNodeOutput; this.graphNodeUI.setRefreshFlag(); } /** * @public */ removeConnection() { this.connection = null; } } class GraphNodeOutputUI extends GraphNodeOutput { /** * @param {GraphNodeOutput[]} graphNodeOutputs * @param {NodeGraph} nodeGraph * @param {GraphNodeUI} graphNode * @returns {GraphNodeOutputUI[]} */ static getFromGraphNodeOutputs(graphNodeOutputs, nodeGraph, graphNode) { /** @type {GraphNodeOutputUI[]} */ const graphNodeOutputsUI = []; graphNodeOutputs.forEach((graphNodeOutput) => { graphNodeOutputsUI.push( new GraphNodeOutputUI( graphNodeOutput.type, graphNodeOutput.description, nodeGraph, graphNode ) ); }); return graphNodeOutputsUI; } /** * @param {string} type * @param {string} description * @param {NodeGraph} nodeGraph * @param {GraphNodeUI} graphNode * @param {string} cssClass */ constructor( type, description = undefined, nodeGraph, graphNode, cssClass = "graphNodeInput" ) { super(type, description); /** * @private * @type {any} */ this.value = undefined; this.nodeGraph = nodeGraph; /** * @public * @type {GraphNodeUI} */ this.graphNodeUI = graphNode; /** * @private * @type {GraphNodeInputUI[]} */ this.connections = []; this.domElement = document.createElement("li"); this.domElement.innerText = "▶"; this.domElement.title = "[" + this.type + "]"; this.domElement.style.textAlign = "right"; this.domElement.classList.add(cssClass); this.domElement.addEventListener( "mousedown", this.mouseHandler.bind(this) ); } /** * @public * @param {GraphNodeInputUI} graphNodeInputUI */ addConnection(graphNodeInputUI) { this.connections.push(graphNodeInputUI); } /** * @public * @param {GraphNodeInputUI} graphNodeInputUI */ removeConnection(graphNodeInputUI) { const id = this.connections.indexOf(graphNodeInputUI); this.connections.splice(id); } /** * @public * @returns {GraphNodeInputUI[]} */ getConnections() { return this.connections; } /** * @public * @returns {any} */ getValue() { return this.value; } /** * @public * @param {any} value */ async setValue(value) { this.value = value; const outputNodes = this.graphNodeUI.getOutputNodes(); outputNodes.forEach((outputNode) => { if (outputNode !== this.graphNodeUI) { outputNode.setRefreshFlag(); } }); } /** * @private */ mouseHandler() { this.nodeGraph.toggleConnection(this); } } class GraphNodeUI { /** * @param {GraphNode} graphNode * @param {NodeGraph} nodeGraph * @param {string} cssClass */ constructor(graphNode, nodeGraph, cssClass = "graphNode") { this.graphNode = graphNode; this.nodeGraph = nodeGraph; this.cssClass = cssClass; this.domElement = document.createElement("span"); this.position = { x: this.nodeGraph.parentElement.clientWidth / 2, y: this.nodeGraph.parentElement.clientHeight / 2, }; this.refreshFlag = true; if (this.graphNode) { this.initialize(); } } /** * @public * @param {string} name * @returns {Promise<GraphNodeInputUI>} */ async getInput(name) { await this.initialize(); for (let i = 0; i < this.graphNodeInputs.length; i++) { if (this.graphNodeInputs[i].name === name) return this.graphNodeInputs[i]; } } /** * @public * @param {string} description * @returns {Promise<GraphNodeOutputUI>} */ async getOutput(description = undefined) { await this.initialize(); if (this.graphNodeOutputs.length === 1) { return this.graphNodeOutputs[0]; } else { for (let i = 0; i < this.graphNodeOutputs.length; i++) { if (this.graphNodeOutputs[i].description === description) return this.graphNodeOutputs[i]; } } } /** * @public */ setRefreshFlag() { this.refreshFlag = true; this.execute(); } /** * @public */ async execute() { if (this.graphNode.asWorker) { if (this.worker) { console.log("terminating " + this.graphNode.executer.name + "."); this.worker.terminate(); } } else { if (this.executerCallback) { console.log("aborting " + this.graphNode.executer.name + "."); this.executerCallback.abortFlag = true; } } if (this.refreshFlag) { this.refreshFlag = false; const parameterValues = this.getParameterValues(); if (parameterValues.includes(undefined)) return; console.log( "Calling function '" + this.graphNode.executer.name + "'." ); this.executerCallback = new NodeCallback(this); this.executerCallback.setProgressPercent(0); if (this.graphNode.asWorker) { this.executeAsWorker(parameterValues); } else { this.executeAsPromise(parameterValues); } if (!parameterValues.includes(undefined)) { console.log("Executing " + this.graphNode.executer.name + "."); } else { console.log( "Function '" + this.graphNode.executer.name + "' did not pick up, because at least one parameter is undefined." ); } } } /** * @private * @param {any[]} parameterValues */ async executeAsPromise(parameterValues) { setTimeout(async () => { const result = await this.graphNode.executer( ...parameterValues, this.executerCallback ); this.graphNodeOutputs[0].setValue(result); this.refreshValuePreview(result); this.executerCallback.setProgressPercent(100); }); } /** * @private * @param {any[]} parameterValues */ async executeAsWorker(parameterValues) { const toTransfer = []; const toCopy = []; let parameterValuesString = "("; let pointerCount = 0; let copyCount = 0; parameterValues.forEach((parameterValue) => { if (false && parameterValue instanceof ImageBitmap) { toTransfer.push(parameterValue); parameterValuesString += "messageEvent.data.pointer[" + String(pointerCount) + "]"; pointerCount++; } else { toCopy.push(parameterValue); parameterValuesString += "messageEvent.data.copy[" + String(copyCount) + "]"; copyCount++; } parameterValuesString += ","; }); parameterValuesString += ")"; this.worker = await this.createWorker(parameterValuesString); const cThis = this; this.worker.addEventListener( "message", async function handler(messageEvent) { cThis.worker.removeEventListener(messageEvent.type, handler); const resultValue = messageEvent.data; // TODO Handle multiple outputs. cThis.graphNodeOutputs[0].setValue(resultValue); cThis.refreshValuePreview(resultValue); cThis.worker.terminate(); cThis.worker = undefined; cThis.executerCallback.setProgressPercent(100); } ); this.worker.postMessage( { pointer: toTransfer, copy: toCopy }, toTransfer ); } /** * @private * @param {string} parameterValuesString * @returns {Promise<Worker>} */ async createWorker(parameterValuesString) { const dependenciesSource = await this.graphNode.getDependenciesSource(); const workerSource = dependenciesSource + "\n" + "const cSelf = self;\n" + "self.addEventListener('message', async (messageEvent) => {\n" + "cSelf.postMessage(await " + this.graphNode.executer.name + parameterValuesString + ");\n" + "});"; const blob = new Blob([workerSource], { type: "text/javascript", }); const workerSrc = window.URL.createObjectURL(blob); return new Worker(workerSrc); } /** * @protected * @param {any} value */ refreshValuePreview(value) { this.outputUIElement.innerHTML = ""; if (value instanceof ImageBitmap) { const imageCanvas = document.createElement("canvas"); imageCanvas.width = value.width; imageCanvas.height = value.height; const context = imageCanvas.getContext("2d"); context.drawImage(value, 0, 0, value.width, value.height); const imageElement = new Image(); imageElement.style.maxWidth = "100%"; imageCanvas.style.maxHeight = "5rem"; this.outputUIElement.style.display = "flex"; this.outputUIElement.style.justifyContent = "center"; this.outputUIElement.appendChild(imageElement); imageElement.src = imageCanvas.toDataURL(); } else if (Array.isArray(value) && value[0] instanceof ImageBitmap) { const imageCanvas = document.createElement("canvas"); imageCanvas.width = value[0].width; imageCanvas.height = value[0].height; const context = imageCanvas.getContext("2d"); context.drawImage(value[0], 0, 0, value[0].width, value[0].height); const imageElement = new Image(); imageElement.style.maxWidth = "100%"; imageCanvas.style.maxHeight = "5rem"; this.outputUIElement.style.display = "flex"; this.outputUIElement.style.justifyContent = "center"; this.outputUIElement.appendChild(imageElement); imageElement.src = imageCanvas.toDataURL(); } else if (typeof value === "number") { const numberElement = document.createElement("div"); numberElement.innerText = String(value); numberElement.style.textAlign = "center"; this.outputUIElement.appendChild(numberElement); } else if (typeof value === "string") { const valueImage = new Image(); valueImage.src = value; valueImage.style.maxWidth = "100%"; valueImage.style.maxHeight = "5rem"; this.outputUIElement.style.display = "flex"; this.outputUIElement.style.justifyContent = "center"; this.outputUIElement.appendChild(valueImage); } this.nodeGraph.updateConnectionUI(); } /** * @private * @returns {any[]} */ getParameterValues() { const parameterValues = []; this.graphNodeInputs.forEach((input) => { const connection = input.getConnection(); if (connection) { parameterValues.push(connection.getValue()); } else { parameterValues.push(undefined); } }); return parameterValues; } /** * @public */ async initialize() { while (this.initializing) { await new Promise((resolve) => { setTimeout(resolve, 500); }); } if (this.initialized) { return; } this.initialized = false; this.initializing = true; if (this.graphNode) { /** * @override * @protected * @type {GraphNodeInputUI[]} */ this.graphNodeInputs = GraphNodeInputUI.getFromGraphNodeInputs( await this.graphNode.getInputs(), this.nodeGraph, this ); /** * @override * @protected * @type {GraphNodeOutputUI[]} */ this.graphNodeOutputs = GraphNodeOutputUI.getFromGraphNodeOutputs( await this.graphNode.getOutputs(), this.nodeGraph, this ); } this.domElement.classList.add(this.cssClass); const domTitleElement = document.createElement("h1"); domTitleElement.style.cursor = "grab"; domTitleElement.addEventListener( "mousedown", this.mousedownGrabHandler.bind(this) ); domTitleElement.innerText = this.graphNode.getName(); domTitleElement.style.backgroundColor = "transparent"; this.domElement.appendChild(domTitleElement); this.domProgressElement = document.createElement("progress"); this.domProgressElement.style.width = "100%"; this.domProgressElement.value = 0; this.domProgressElement.max = 100; this.domElement.appendChild(this.domProgressElement); this.domProgressElement.hidden = true; this.outputUIElement = document.createElement("div"); this.domElement.appendChild(this.outputUIElement); const domIOElement = document.createElement("div"); const domInputList = document.createElement("ul"); const domOutputList = document.createElement("ul"); domIOElement.style.display = "flex"; domIOElement.style.justifyContent = "space-between"; domIOElement.style.marginLeft = "-10%"; domIOElement.style.width = "120%"; this.domElement.appendChild(domIOElement); domIOElement.appendChild(domInputList); domIOElement.appendChild(domOutputList); this.graphNodeInputs.forEach((graphNodeInput) => { domInputList.appendChild(graphNodeInput.domElement); }); this.graphNodeOutputs.forEach((graphNodeOutput) => { domOutputList.appendChild(graphNodeOutput.domElement); }); this.execute(); this.initialized = true; this.initializing = false; } /** * @public * @returns {Promise<{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]>} */ async getConnections() { /** @type {{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]} */ const connections = []; this.graphNodeInputs.forEach((graphNodeInput) => { if (graphNodeInput.graphNodeUI === this) { const output = graphNodeInput.getConnection(); if (output) { connections.push({ input: graphNodeInput, output: output, }); } } }); return connections; } /** * @public * @returns {GraphNodeUI[]} */ getOutputNodes() { /** @type {GraphNodeUI[]} */ const outputNodes = []; this.graphNodeOutputs.forEach((graphNodeOutput) => { graphNodeOutput.getConnections().forEach((connection) => { outputNodes.push(connection.graphNodeUI); }); }); return outputNodes; } /** * @protected */ mousedownGrabHandler() { this.nodeGraph.setGrabbedNode(this); } /** * @public * @returns {{x:number, y:number}} */ getPosition() { const boundingRect = this.domElement.getBoundingClientRect(); return { x: boundingRect.left + boundingRect.width / 2, y: boundingRect.top + 5, }; } /** * @param {{x:number, y:number}} position */ setPosition(position) { this.position = position; this.domElement.style.transform = "translate(calc(" + this.position.x + "px - 50%), calc(" + this.position.y + "px - 0.25rem))"; } } class InputGraphNode extends GraphNodeUI { /** * @param {NodeGraph} nodeGraph * @param {GraphNodeInputUI | string} inputNodeOrType * @param {string} cssClass */ constructor(nodeGraph, inputNodeOrType, cssClass = "graphNode") { super(undefined, nodeGraph, cssClass); if (inputNodeOrType instanceof GraphNodeInputUI) { this.type = inputNodeOrType.type; this.inputNode = inputNodeOrType; } else { this.type = inputNodeOrType; } this.initialize(); if (inputNodeOrType instanceof GraphNodeInputUI) this.setConnectionToInputNode(); } /** * @private */ async setConnectionToInputNode() { this.inputNode.setConnection(this.graphNodeOutputs); this.nodeGraph.updateConnectionUI(); } /** * @override */ async initialize() { while (this.initializing) { await new Promise((resolve) => { setTimeout(resolve, 500); }); } if (this.initialized) { return; } this.domElement.classList.add(this.cssClass); const domTitleElement = document.createElement("h1"); domTitleElement.style.cursor = "grab"; domTitleElement.addEventListener( "mousedown", this.mousedownGrabHandler.bind(this) ); domTitleElement.innerText = this.type; domTitleElement.style.backgroundColor = "transparent"; this.domElement.appendChild(domTitleElement); this.domProgressElement = document.createElement("progress"); this.domProgressElement.style.width = "100%"; this.domProgressElement.value = 0; this.domProgressElement.max = 100; this.domElement.appendChild(this.domProgressElement); this.domProgressElement.hidden = true; this.outputUIElement = document.createElement("div"); this.domElement.appendChild(this.outputUIElement); const domIOElement = document.createElement("div"); const domInputList = document.createElement("ul"); const domOutputList = document.createElement("ul"); domIOElement.style.display = "flex"; domIOElement.style.justifyContent = "space-between"; domIOElement.style.marginLeft = "10%"; domIOElement.style.width = "100%"; this.domElement.appendChild(domIOElement); domIOElement.appendChild(domInputList); domIOElement.appendChild(domOutputList); this.inputElement = document.createElement("input"); this.inputElement.style.width = "80%"; this.inputElement.style.overflowWrap = "break-word"; this.inputElement.style.hyphens = "auto"; this.inputElement.style.whiteSpace = "normal"; this.inputElement.multiple = false; if (this.type === "number") { this.inputElement.type = "number"; domInputList.appendChild(this.inputElement); } else if (this.type === "ImageBitmap") { this.inputElement.type = "file"; this.inputElement.accept = "image/*"; } else if (this.type === "ImageBitmap[]") { this.inputElement.type = "file"; this.inputElement.accept = "image/*"; this.inputElement.multiple = true; } else { console.error("Input type '" + this.type + "' not supported."); } this.inputElement.addEventListener( "input", this.inputChangeHandler.bind(this) ); domInputList.appendChild(this.inputElement); this.graphNodeOutputs = [ new GraphNodeOutputUI( this.type, "[" + this.type + "]", this.nodeGraph, this ), ]; domOutputList.appendChild(this.graphNodeOutputs[0].domElement); this.initialized = true; this.initializing = false; } /** * @public * @param {any} value */ async setValue(value) { this.inputElement.value = value; this.inputElement.dispatchEvent(new Event("input")); } /** * @private * @param {InputEvent} inputEvent */ inputChangeHandler(inputEvent) { if (this.type === "number") { let value = Number( /** @type {HTMLInputElement} */ (inputEvent.target).value ); if (!value) { value = 0; } this.graphNodeOutputs[0].setValue(value); this.refreshValuePreview(value); } else if (this.type === "ImageBitmap") { const nodeCallback = new NodeCallback(this); nodeCallback.setProgressPercent(0); const imageLoaderWorker = new Worker("./src/image-loader-worker.js"); imageLoaderWorker.addEventListener("message", async (messageEvent) => { const imageBitmap = messageEvent.data; this.graphNodeOutputs[0].setValue(imageBitmap); this.refreshValuePreview(imageBitmap); nodeCallback.setProgressPercent(100); }); imageLoaderWorker.postMessage(inputEvent.target.files[0]); } else if (this.type === "ImageBitmap[]") { const nodeCallback = new NodeCallback(this); nodeCallback.setProgressPercent(0); const files = Array.from(inputEvent.target.files); const imageCount = files.length; const imageBitmapArray = []; files.forEach((file) => { const imageLoaderWorker = new Worker( "./src/image-loader-worker.js" ); imageLoaderWorker.addEventListener( "message", async (messageEvent) => { const imageBitmap = messageEvent.data; imageBitmapArray.push(imageBitmap); if (imageBitmapArray.length === imageCount) { this.graphNodeOutputs[0].setValue(imageBitmapArray); this.refreshValuePreview(imageBitmap); } nodeCallback.setProgressPercent( (imageBitmapArray.length / imageCount) * 100 ); } ); imageLoaderWorker.postMessage(file); }); } } /** * @override * @public * @returns {Promise<{input: GraphNodeInputUI, output:GraphNodeOutputUI}[]>} */ async getConnections() { // TODO Handle multiple outputs. return [{ input: this.inputNode, output: this.graphNodeOutputs[0] }]; } /** * @override * @public */ async execute() { this.refreshFlag = false; } }
c245f34978705a63aacff73b2449b76e745b0507
The text was updated successfully, but these errors were encountered:
No branches or pull requests
Add node selection menu.
scanning-pipeline/src/node-graph.js
Line 180 in 16f13b2
c245f34978705a63aacff73b2449b76e745b0507
The text was updated successfully, but these errors were encountered: