From 22a62d7a8af74aabcf4d660f0750fc629ac9b146 Mon Sep 17 00:00:00 2001 From: Sammy Franklin Date: Wed, 11 Oct 2023 23:29:29 -0700 Subject: [PATCH] file subflows --- comfy_extras/nodes_subflow.py | 28 ++- web/extensions/core/subflow.js | 279 +++++++++++++++++----------- web/extensions/core/widgetInputs.js | 2 +- web/scripts/widgets.js | 117 ++++++++++++ 4 files changed, 310 insertions(+), 116 deletions(-) diff --git a/comfy_extras/nodes_subflow.py b/comfy_extras/nodes_subflow.py index 71616ce56..37787f309 100644 --- a/comfy_extras/nodes_subflow.py +++ b/comfy_extras/nodes_subflow.py @@ -1,6 +1,6 @@ import folder_paths -class Subflow: +class LoadSubflow: @classmethod def INPUT_TYPES(s): return {"required": { "subflow_name": (folder_paths.get_filename_list("subflows"), ), }} @@ -8,11 +8,33 @@ class Subflow: FUNCTION = "" CATEGORY = "loaders" + +class FileSubflow: + @classmethod + def INPUT_TYPES(s): + return {"required": { "subflow_name": (folder_paths.get_filename_list("subflows"), )} } + RETURN_TYPES = () + FUNCTION = "" + + CATEGORY = "utils" + +class InMemorySubflow: + @classmethod + def INPUT_TYPES(s): + return {"required": {} } + RETURN_TYPES = () + FUNCTION = "" + + CATEGORY = "" NODE_CLASS_MAPPINGS = { - "Subflow": Subflow, + "LoadSubflow": LoadSubflow, + "FileSubflow": FileSubflow, + "InMemorySubflow": InMemorySubflow } NODE_DISPLAY_NAME_MAPPINGS = { - "Subflow": "Load Subflow" + "Subflow": "Load Subflow", + "FileSubflow": "File Subflow", + "InMemorySubflow": "In Memory Subflow" } diff --git a/web/extensions/core/subflow.js b/web/extensions/core/subflow.js index 363b7d8ab..d93fa8058 100644 --- a/web/extensions/core/subflow.js +++ b/web/extensions/core/subflow.js @@ -1,101 +1,146 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; +import { ComfyWidgets } from "/scripts/widgets.js"; +const CONFIG = Symbol(); +// const GET_CONFIG = Symbol(); +import { GET_CONFIG } from "./widgetInputs.js"; -class InMemorySubflow { - constructor() {} - - onConfigure() { - console.log(this); - } - - getExportedOutput(slot) { - return this.subflow.extras.outputSlots[slot]; - }; - - getExportedInput(slot) { - return this.subflow.extras.inputSlots[slot]; - }; - - async refreshNode(subflow) { - if (!subflow) return; - this.updateSubflowPrompt(subflow); - this.refreshPins(subflow); - } - - refreshPins(subflow) { - if(!subflow) - return; - - subflow.extras = { inputSlots: [], outputSlots: [] }; - const { inputSlots } = subflow.extras; - const { outputSlots } = subflow.extras; - - // remove all existing pins - const numInputs = this.inputs?.length ?? 0; - const numOutputs = this.outputs?.length ?? 0; - for(let i = numInputs-1; i > -1; i--) { - this.removeInput(i); - } - for(let i = numOutputs-1; i > -1; i--) { - this.removeOutput(i); - } - - const subflowNodes = subflow.nodes; - // add the new pins and keep track of where the exported vars go to within the inner nodes - for (const subflowNode of subflowNodes) { - const exports = subflowNode.properties?.exports; - if (exports) { - let pinNum = 0; - for (const inputRef of exports.inputs) { - const input = subflowNode.inputs.find(q => q.name === inputRef); - if (!input) continue; - this.addInput(input.name, input.type); - inputSlots.push([subflowNode.id, pinNum]); - pinNum++; - } - pinNum = 0; - for (const outputRef of exports.outputs) { - const output = subflowNode.outputs.find(q => q.name === outputRef); - if (!output) continue; - this.addOutput(output.name, output.type); - outputSlots.push([subflowNode.id, pinNum]); - pinNum++; - } - } - } - - this.size[0] = 180; - }; - - updateSubflowPrompt(subflow) { - this.subflow = subflow; - }; +function getConfig(widgetName) { + const { nodeData } = this.constructor; + return nodeData?.input?.required[widgetName] ?? nodeData?.input?.optional?.[widgetName]; } +// class InMemorySubflow extends LGraphNode { +// constructor(title) { +// super(title ?? "InMemorySubflow"); +// } + +// onConfigure() { +// console.log(this); +// } + +// getExportedOutput(slot) { +// return this.subflow.extras.outputSlots[slot]; +// }; + +// getExportedInput(slot) { +// return this.subflow.extras.inputSlots[slot]; +// }; + +// async refreshNode(subflow) { +// if (!subflow) return; +// this.updateSubflowPrompt(subflow); +// this.refreshPins(subflow); +// } + +// refreshPins(subflow) { +// if(!subflow) +// return; + +// subflow.extras = { inputSlots: [], outputSlots: [] }; +// const { inputSlots } = subflow.extras; +// const { outputSlots } = subflow.extras; + +// // remove all existing pins +// const numInputs = this.inputs?.length ?? 0; +// const numOutputs = this.outputs?.length ?? 0; +// for(let i = numInputs-1; i > -1; i--) { +// this.removeInput(i); +// } +// for(let i = numOutputs-1; i > -1; i--) { +// this.removeOutput(i); +// } + +// const subflowNodes = subflow.nodes; +// // add the new pins and keep track of where the exported vars go to within the inner nodes +// for (const subflowNode of subflowNodes) { +// const exports = subflowNode.properties?.exports; +// if (exports) { +// let pinNum = 0; +// for (const inputRef of exports.inputs) { +// console.log(subflowNode.inputs); +// const input = subflowNode.inputs.find(q => q.name === inputRef); +// if (!input) continue; +// const { name, type, link, slot_index, ...extras } = input; +// console.log("Input"); +// console.log(input); +// console.log(extras); +// if (extras.widget) { +// extras.widget[GET_CONFIG] = () => config; +// } +// this.addInput(input.name, input.type, extras ); +// inputSlots.push([subflowNode.id, pinNum]); +// pinNum++; +// } +// pinNum = 0; +// for (const outputRef of exports.outputs) { +// const output = subflowNode.outputs.find(q => q.name === outputRef); +// if (!output) continue; +// this.addOutput(output.name, output.type); +// outputSlots.push([subflowNode.id, pinNum]); +// pinNum++; +// } +// } +// } + +// this.size[0] = 180; +// }; + +// updateSubflowPrompt(subflow) { +// this.subflow = subflow; +// }; +// } + +// class FileSubflow extends InMemorySubflow { +// constructor() { +// super("FileSubflow"); + +// console.log("constructor called"); +// // ComfyWidgets.SUBFLOWUPLOAD(this, null, null, app); +// } + +// async refreshNode(subflow) { +// if (!subflow) return; + +// this.updateSubflowPrompt(subflow); +// this.refreshPins(subflow); + +// this.size[0] = this.computeSizeX(); +// } + +// computeSizeX() { +// return Math.max(100, LiteGraph.NODE_TEXT_SIZE * this.title.length * 0.45 + 160); +// } +// } + app.registerExtension({ name: "Comfy.Subflow", - async nodeCreated(node) { - if (!node.widgets) return; - if (node.widgets[0].name !== "subflow_name") return; + beforeRegisterNodeDef(nodeType, nodeData, app) { - let outputSlots = []; // (int (node), int (slot)) - let inputSlots = []; + // console.log("in init") + // LiteGraph.registerNodeType( + // "InMemorySubflow", + // Object.assign(InMemorySubflow, { + // title: "InMemorySubflow", + // }) + // ); - const refreshNode = async (subflowName) => { - const subflowData = await api.getSubflow(subflowName); - if (!subflowData.subflow) return; - - const subflowNodes = subflowData.subflow.nodes - updateSubflowPrompt(subflowData.subflow); - refreshPins(subflowNodes); - - node.size[0] = computeSizeX(subflowName); - }; - - const refreshPins = (subflowNodes) => { - inputSlots = []; - outputSlots = []; + // LiteGraph.registerNodeType( + // "FileSubflow", + // Object.assign(FileSubflow, { + // title: "FileSubflow", + // }) + // ); + // FileSubflow.category = "utils"; + const refreshPins = (node, subflow) => { + if(!subflow) + return; + + subflow.extras = { inputSlots: [], outputSlots: [] }; + const { inputSlots } = subflow.extras; + const { outputSlots } = subflow.extras; + // remove all existing pins const numInputs = node.inputs?.length ?? 0; const numOutputs = node.outputs?.length ?? 0; @@ -105,16 +150,29 @@ app.registerExtension({ for(let i = numOutputs-1; i > -1; i--) { node.removeOutput(i); } - + + const subflowNodes = subflow.nodes; // add the new pins and keep track of where the exported vars go to within the inner nodes for (const subflowNode of subflowNodes) { - const exports = subflowNode.properties.exports; + const exports = subflowNode.properties?.exports; if (exports) { let pinNum = 0; for (const inputRef of exports.inputs) { + console.log(subflowNode.inputs); const input = subflowNode.inputs.find(q => q.name === inputRef); if (!input) continue; - node.addInput(input.name, input.type); + const { name, type, link, slot_index, ...extras } = input; + console.log("Input"); + console.log(input); + console.log(extras); + if (extras.widget) { + const w = extras.widget; + const config = getConfig.call(this, input.name) ?? [input.type, w.options || {}]; + extras.widget[GET_CONFIG] = () => config; + console.log(extras); + console.log(input.type); + } + node.addInput(input.name, input.type, extras ); inputSlots.push([subflowNode.id, pinNum]); pinNum++; } @@ -128,32 +186,29 @@ app.registerExtension({ } } } + + node.size[0] = 180; }; - const updateSubflowPrompt = (subflow) => { + const refreshNode = async (node, subflow) => { + if (!subflow) return; + node.subflow = subflow; + refreshPins(node, subflow); + + node.size[0] = Math.max(100, LiteGraph.NODE_TEXT_SIZE * node.title.length * 0.45 + 160); }; - const computeSizeX = (subflowName) => { - return Math.max(100, LiteGraph.NODE_TEXT_SIZE * subflowName.length * 0.45 + 160); - }; + // if (nodeData.name == "InMemorySubflow") { + // Object.assign(nodeData, new InMemorySubflow()); + // } - node.widgets[0].callback = (subflowName) => refreshNode(subflowName); + if (nodeData.name == "FileSubflow") { + nodeType.prototype.refreshNode = function(subflow) { refreshNode(this, subflow); }; + nodeType.prototype.getExportedOutput = function(slot) { return this.subflow.extras.outputSlots[slot]; } + nodeType.prototype.getExportedInput = function(slot) { return this.subflow.extras.inputSlots[slot]; } - node.getExportedOutput = (slot) => { - return outputSlots[slot]; - }; - - node.getExportedInput = (slot) => { - return inputSlots[slot]; - }; - }, - registerCustomNodes() { - LiteGraph.registerNodeType( - "InMemorySubflow", - Object.assign(InMemorySubflow, { - title: "InMemorySubflow", - }) - ); - }, + nodeData.input.required = { subflow: ["SUBFLOWUPLOAD"] }; + } + } }); diff --git a/web/extensions/core/widgetInputs.js b/web/extensions/core/widgetInputs.js index 3c9da458d..00809906c 100644 --- a/web/extensions/core/widgetInputs.js +++ b/web/extensions/core/widgetInputs.js @@ -4,7 +4,7 @@ import { app } from "../../scripts/app.js"; const CONVERTED_TYPE = "converted-widget"; const VALID_TYPES = ["STRING", "combo", "number", "BOOLEAN"]; const CONFIG = Symbol(); -const GET_CONFIG = Symbol(); +export const GET_CONFIG = Symbol(); function getConfig(widgetName) { const { nodeData } = this.constructor; diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index 2b0239374..e9c906542 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -495,4 +495,121 @@ export const ComfyWidgets = { return { widget: uploadWidget }; }, + SUBFLOWUPLOAD(node, inputName, inputData, app) { + // const subflowWidget = node.widgets.find((w) => w.name === "subflow"); + let uploadWidget; + + // var default_value = subflowWidget.value; + // Object.defineProperty(subflowWidget, "value", { + // set : function(value) { + // this._real_value = value; + // }, + + // get : function() { + // let value = ""; + // if (this._real_value) { + // value = this._real_value; + // } else { + // return default_value; + // } + + // if (value.filename) { + // let real_value = value; + // value = ""; + // if (real_value.subfolder) { + // value = real_value.subfolder + "/"; + // } + + // value += real_value.filename; + + // if(real_value.type && real_value.type !== "input") + // value += ` [${real_value.type}]`; + // } + // return value; + // } + // }); + + // Add our own callback to the combo widget to render an image when it changes + // const cb = node.callback; + // imageWidget.callback = function () { + // if (cb) { + // return cb.apply(this, arguments); + // } + // }; + + // On load if we have a value then render the image + // The value isnt set immediately so we need to wait a moment + // No change callbacks seem to be fired on initial setting of the value + // requestAnimationFrame(() => { + + // }); + + const uploadFile = async (file) => { + const reader = new FileReader(); + reader.onload = (e) => { + const subflow = JSON.parse(e.target.result); + console.log(node); + node.refreshNode(subflow); + }; + reader.readAsText(file); + }; + + + const fileInput = document.createElement("input"); + Object.assign(fileInput, { + type: "file", + accept: "image/png,application/json", + style: "display: none", + onchange: async () => { + if (fileInput.files.length) { + await uploadFile(fileInput.files[0], true); + } + }, + }); + document.body.append(fileInput); + + // Create the button widget for selecting the files + console.log("adding widge") + uploadWidget = node.addWidget("button", "choose file with subflow", "subflow", () => { + fileInput.click(); + }); + console.log(node.widgets); + uploadWidget.serialize = false; + + // Add handler to check if an image is being dragged over our node + node.onDragOver = function (e) { + if (e.dataTransfer && e.dataTransfer.items) { + const image = [...e.dataTransfer.items].find((f) => f.kind === "file"); + return !!image; + } + + return false; + }; + + // On drop upload files + node.onDragDrop = function (e) { + console.log("onDragDrop called"); + let handled = false; + for (const file of e.dataTransfer.files) { + if (file.type === "image/png" || file.type === "application/json") { + uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one + handled = true; + } + } + + return handled; + }; + + node.pasteFile = function(file) { + if (file.type === "image/png" || file.type === "application/json") { + const is_pasted = (file.name === "image.png") && + (file.lastModified - Date.now() < 2000); + uploadFile(file, true, is_pasted); + return true; + } + return false; + } + + return { widget: uploadWidget }; + } };