diff --git a/web/extensions/core/subflow.js b/web/extensions/core/subflow.js index 81e0c8105..4e901f573 100644 --- a/web/extensions/core/subflow.js +++ b/web/extensions/core/subflow.js @@ -32,16 +32,17 @@ app.registerExtension({ if (!input) continue; const { name, type, link, slot_index, ...extras } = input; - node.addInput(input.name, input.type, extras ); - inputSlots.push([subflowNode.id, pinNum]); + node.addInput(input.name, input.type, extras); + inputSlots.push([subflowNode, pinNum]); pinNum++; } + pinNum = 0; for (const exportedOutput of exports.outputs) { const output = subflowNode.outputs.find(q => q.name === exportedOutput.name); if (!output) continue; node.addOutput(output.name, output.type); - outputSlots.push([subflowNode.id, pinNum]); + outputSlots.push([subflowNode, pinNum]); pinNum++; } } @@ -54,20 +55,61 @@ app.registerExtension({ if (!subflow) return; - if (node.widgets) { - // Allow widgets to cleanup - let subflowWidget; - for (const w of node.widgets) { - if (w.type == "button") { - subflowWidget = w; - } else if (w.onRemove) { - w.onRemove(); + // Allow widgets to cleanup + for (let i = 1; i < node.widgets.length; ++i) { + if (node.widgets[i].onRemove) { + node.widgets[i].onRemove(); + } + } + node.widgets = [node.widgets[0]]; + + // Map widgets + subflow.extras.widgetNodes = []; + const resolveWidgetPath = (thisNode, path, widgetIndex) => { + const subflowNodes = thisNode.subflow.nodes; + + let q = 0; // get what would be the q-th exported widget + console.log(path); + for (const subflowNode of subflowNodes) { + const exportedWidgets = subflowNode.properties?.exports?.widgets; + console.log("has nodes", q, widgetIndex); + if (exportedWidgets) { + console.log("in exports"); + const childPath = `${path}${subflowNode.id}/`; + for (const i in exportedWidgets) { + console.log("exported Widgets",q, widgetIndex); + if (widgetIndex == q) { + console.log(subflowNode); + if (subflowNode.subflow) { + console.log("widget is inside subflow!") + return resolveWidgetPath(subflowNode, childPath, i); + } + return `${childPath}${subflowNode.id}/`; + } + q++; + } + } + } + console.warn("couldn't export a widget"); + }; + const subflowNodes = subflow.nodes; + + for (const subflowNode of subflowNodes) { + const exportedWidgets = subflowNode.properties?.exports?.widgets; + if (exportedWidgets) { + const childPath = `/${subflowNode.id}/`; + for (const i in exportedWidgets) { + console.log("exporting", exportedWidgets[i].name); + if (subflowNode.subflow) { + subflow.extras.widgetNodes.push(resolveWidgetPath(subflowNode, childPath, i)); + } else { + subflow.extras.widgetNodes.push(childPath); + } } } - node.widgets = [subflowWidget]; } - const subflowNodes = subflow.nodes; + console.log(subflow.extras.widgetNodes); let widgetIndex = 1; for (const subflowNode of subflowNodes) { const exports = subflowNode.properties?.exports; @@ -90,7 +132,6 @@ app.registerExtension({ if (v !== null && node.widgets_values) { node.widgets_values[widgetIndex] = v; } - return subflowNode.id; } }; let value = exportedWidget.value; @@ -103,21 +144,27 @@ app.registerExtension({ } } + console.log(subflow.extras.widgetNodes); + }; - const refreshNode = (node, subflow) => { + const refreshNode = (node, subflow, filename) => { if (!subflow) return; node.subflow = subflow; + node.title = `File Subflow (Loaded: ${filename})`; refreshPins(node, subflow); refreshWidgets(node, subflow, false); + + + console.log("HAS", node.subflow.extras.inputSlots); - node.size[0] = Math.max(100, LiteGraph.NODE_TEXT_SIZE * node.title.length * 0.45 + 160); + node.size[0] = Math.max(100, LiteGraph.NODE_TEXT_SIZE * node.title.length * 0.45 + 100); }; if (nodeData.name == "FileSubflow") { nodeType.prototype.onConfigure = function() { refreshWidgets(this, this.subflow, true); }; - nodeType.prototype.refreshNode = function(subflow) { refreshNode(this, subflow); }; + nodeType.prototype.refreshNode = function(subflow, filename) { refreshNode(this, subflow, filename); }; nodeType.prototype.getExportedOutput = function(slot) { return this.subflow.extras.outputSlots[slot]; } nodeType.prototype.getExportedInput = function(slot) { return this.subflow.extras.inputSlots[slot]; } diff --git a/web/lib/litegraph.core.js b/web/lib/litegraph.core.js index e05f5fca5..5e9169692 100644 --- a/web/lib/litegraph.core.js +++ b/web/lib/litegraph.core.js @@ -14073,7 +14073,6 @@ LGraphNode.prototype.executeAction = function(action) !options.ignore_item_callbacks && value.disabled !== true ) { - console.log("value", value); //item callback var r = value.callback.call( this, diff --git a/web/scripts/app.js b/web/scripts/app.js index 12c492829..4f6193e25 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -1588,12 +1588,12 @@ export class ComfyApp { * Converts the current graph workflow for sending to the API * @returns The workflow and node links */ - async graphToPrompt(graph=this.graph) { - let subflowNodeIdOffset = graph.last_node_id; - const subflowIdOffsets = {}; + async graphToPrompt(graph=this.graph, nodeIdOffset=0, path="/", globalMappings={}) { + console.log("MY PATH IS ", path) const workflow = graph.serialize(); const output = {}; + let childNodeIdOffset = nodeIdOffset + graph.last_node_id; // Process nodes in order of execution for (const node of graph.computeExecutionOrder(false)) { const n = workflow.nodes.find((n) => n.id === node.id); @@ -1613,24 +1613,32 @@ export class ComfyApp { if (node.subflow) { const subgraph = new LGraph(); + console.log("configuring subflow"); subgraph.configure(node.subflow); - const subgraphPrompt = (await this.graphToPrompt(subgraph)).output; + console.log("subgraph last node id is ", subgraph.last_node_id); + const subgraphPrompt = (await this.graphToPrompt(subgraph, childNodeIdOffset, path+String(node.id)+"/", globalMappings)); + const subgraphPromptOutput = subgraphPrompt.output; + const subgraphGlobalMappings = subgraphPrompt.globalMappings; // replace ids to not conflict with existing ids - for ( const [key, value] of Object.entries(subgraphPrompt) ) { - for ( const [inputKey, inputValue] of Object.entries(value.inputs) ) { - if (Array.isArray(inputValue)) { - value.inputs[inputKey][0] = String(Number(value.inputs[inputKey][0]) + subflowNodeIdOffset); - } - } - output[String(Number(key) + subflowNodeIdOffset)] = { + for ( const [key, value] of Object.entries(subgraphPromptOutput) ) { + // for ( const [inputKey, inputValue] of Object.entries(value.inputs) ) { + // if (Array.isArray(inputValue)) { + // value.inputs[inputKey][0] = String(Number(value.inputs[inputKey][0]) + subflowNodeIdOffset); + // } + // } + output[key] = { ...value, - for_subflow: String(node.id) // keep reference of parent node + for_subflow: String(node.id) // keep reference of root level subflow node }; + // subflowNodes[node.id][key] = value; } - subflowIdOffsets[node.id] = subflowNodeIdOffset; + - subflowNodeIdOffset += Object.keys(node.subflow).length; + // subflowIdOffsets[node.id] = subflowNodeIdOffset; + + childNodeIdOffset += subgraph.last_node_id; + Object.assign(globalMappings, subgraphGlobalMappings); } const inputs = {}; @@ -1644,9 +1652,13 @@ export class ComfyApp { if (node.subflow) { if (widget.type !== "button") { // use the callback to obtain node reference - const forNode = widget.callback(null); - const widgetNode = subflowIdOffsets[node.id] + forNode; - output[ widgetNode ].inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value; + console.log(node.subflow.extras.widgetNodes); + const globalMappingKey = node.subflow.extras.widgetNodes[i]; + const globalId = globalMappings[globalMappingKey]; + if (!output[globalId]) { + console.log("couldn't find reference with global mapping key", globalMappingKey); + } + output[ globalId ].inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value; } } else { inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value; @@ -1655,15 +1667,37 @@ export class ComfyApp { } } - const getInputRef = (inputNode, inputSlot) => { + const getOutputRef = (inputNode, inputSlot, inputPath) => { if (inputNode.subflow) { // input should be mapped to inner node - const [ localOriginId, originSlot ] = inputNode.getExportedOutput(inputSlot); - const originId = subflowIdOffsets[inputNode.id] + localOriginId; - return [String(originId), parseInt(originSlot)]; + // const [ , originSlot ] = inputNode.getExportedOutput(inputSlot); + const [ underlyingNode, underlyingSlot ] = inputNode.subflow.extras.inputSlots[inputSlot]; + console.log("GOT UNDERLYING NODE", underlyingNode, "from input of", inputSlot, "calling", inputNode); + return getOutputRef(underlyingNode, underlyingSlot, `${inputPath}${String(inputNode.id)}/` ); + + // const originId = nodeIdOffset + localOriginId; + // return [String(originId), parseInt(originSlot)]; } - return [String(inputNode.id), parseInt(inputSlot)]; + const globalId = globalMappings[`${inputPath}${inputNode.id}/`]; + console.log("outputRef", inputNode, `${inputPath}${inputNode.id}/`, globalId); + return [globalId, parseInt(inputSlot)]; + }; + + const getInputRef = (inputNode, inputSlot, inputPath) => { + if (inputNode.subflow) { + // input should be mapped to inner node + // const [ , originSlot ] = inputNode.getExportedOutput(inputSlot); + const [ underlyingNode, underlyingSlot ] = inputNode.subflow.extras.outputSlots[inputSlot]; + return getInputRef(underlyingNode, underlyingSlot, `${inputPath}${String(inputNode.id)}/`); + + // const originId = nodeIdOffset + localOriginId; + // return [String(originId), parseInt(originSlot)]; + } + const globalId = globalMappings[`${inputPath}${inputNode.id}/`]; + console.log(globalMappings); + console.log("inputRef", inputNode,`${inputPath}${inputNode.id}/`, globalId); + return [globalId, parseInt(inputSlot)]; }; // Store all node links @@ -1707,18 +1741,23 @@ export class ComfyApp { if (link) { if (node.subflow) { // inner node's input should be used - const [ localTargetId, targetSlot ] = node.getExportedInput(link.target_slot); - const targetId = subflowIdOffsets[node.id] + localTargetId; - output[ String(targetId) ].inputs[ node.inputs[targetSlot].name ] = getInputRef(parent, link.origin_slot); + // const [ targetNode, targetSlot ] = node.getExportedInput(link.target_slot); + const [targetId, targetSlot] = getOutputRef(node, link.target_slot, path); + console.log("received targetId", targetId, link); + output[ targetId ].inputs[ node.inputs[targetSlot].name ] = getInputRef(parent, link.origin_slot, path); } - inputs[node.inputs[i].name] = getInputRef(parent, link.origin_slot); + console.log("handling", node.type, node.id, node.inputs[i].name ); + inputs[node.inputs[i].name] = getInputRef(parent, link.origin_slot, path); } } } if (!node.subflow) { - output[String(node.id)] = { + const globalId = String(node.id + nodeIdOffset); + console.log("setting globalMapping", path+String(node.id)+"/", globalId); + globalMappings[path+String(node.id)+"/"] = globalId; + output[globalId] = { inputs, class_type: node.comfyClass, }; @@ -1737,7 +1776,8 @@ export class ComfyApp { } } - return { workflow, output }; + console.log(output); + return { workflow, output, globalMappings }; } #formatPromptError(error) { diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index 65cc1c6aa..8f4d5d250 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -547,10 +547,10 @@ export const ComfyWidgets = { const uploadFile = async (file) => { const reader = new FileReader(); + const filename = file.name; reader.onload = (e) => { const subflow = JSON.parse(e.target.result); - console.log(node); - node.refreshNode(subflow); + node.refreshNode(subflow, filename); }; reader.readAsText(file); };