diff --git a/web/extensions/core/subflow.js b/web/extensions/core/subflow.js index 2a3aa3947..cfa740b76 100644 --- a/web/extensions/core/subflow.js +++ b/web/extensions/core/subflow.js @@ -8,10 +8,21 @@ app.registerExtension({ if (!node.widgets) return; if (node.widgets[0].name !== "subflow_name") return; - const refreshPins = (subflowNodes) => { + let outputSlots = []; // (int (node), int (slot)) + let inputSlots = []; + + const refreshPins = async (subflowName) => { + const subflowData = await api.getSubflow(subflowName); + if (!subflowData.subflow) return; + + inputSlots = []; + outputSlots = []; + + const subflowNodes = subflowData.subflow.nodes + updateSubflowPrompt(subflowData.subflow); // remove all existing pins - const numInputs = node.inputs.length; - const numOutputs = node.outputs.length; + const numInputs = node.inputs?.length ?? 0; + const numOutputs = node.outputs?.length ?? 0; for(let i = numInputs-1; i > -1; i--) { node.removeInput(i); } @@ -22,33 +33,41 @@ app.registerExtension({ 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; node.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; node.addOutput(output.name, output.type); + outputSlots.push([subflowNode.id, pinNum]); + pinNum++; } } } }; - node.onConfigure = async function () { - const subflowData = await api.getSubflow(node.widgets[0].value); - if (subflowData.subflow) { - refreshPins(subflowData.subflow.nodes); - } - }; + const updateSubflowPrompt = (subflow) => { + node.subflow = subflow; + }; - node.widgets[0].callback = async function (subflowName) { - const subflowData = await api.getSubflow(subflowName); - if (subflowData.subflow) { - refreshPins(subflowData.subflow.nodes); - } - }; + // node.onSerialize = () => + node.onConfigure = () => refreshPins(node.widgets[0].value); + node.widgets[0].callback = (subflowName) => refreshPins(subflowName); + + node.getExportedOutput = (slot) => { + return outputSlots[slot]; + }; + + node.getExportedInput = (slot) => { + return inputSlots[slot]; + }; } }); diff --git a/web/lib/litegraph.core.js b/web/lib/litegraph.core.js index df07cc220..b9fc9ed97 100644 --- a/web/lib/litegraph.core.js +++ b/web/lib/litegraph.core.js @@ -2618,6 +2618,9 @@ if (this.shape) { o.shape = this.shape; } + if (this.subflow) { + o.subflow = this.subflow; + } if (this.onSerialize) { if (this.onSerialize(o)) { diff --git a/web/scripts/app.js b/web/scripts/app.js index 5b9e76580..92032608d 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -1467,15 +1467,21 @@ export class ComfyApp { } } + + + /** * Converts the current graph workflow for sending to the API * @returns The workflow and node links */ - async graphToPrompt() { - const workflow = this.graph.serialize(); + async graphToPrompt(graph=this.graph) { + let subflowNodeIdOffset = graph.last_node_id; + const subflowIdMapping = {}; + const workflow = graph.serialize(); const output = {}; + // Process nodes in order of execution - for (const node of this.graph.computeExecutionOrder(false)) { + for (const node of graph.computeExecutionOrder(false)) { const n = workflow.nodes.find((n) => n.id === node.id); if (node.isVirtualNode) { @@ -1491,6 +1497,28 @@ export class ComfyApp { continue; } + if (node.subflow) { + const subgraph = new LGraph(); + subgraph.configure(node.subflow); + const subgraphPrompt = (await this.graphToPrompt(subgraph)).output; + + // 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)] = { + ...value, + for_subflow: String(node.id) // keep reference of parent node + }; + } + subflowIdMapping[node.id] = subflowNodeIdOffset; + + subflowNodeIdOffset += Object.keys(node.subflow).length; + } + const inputs = {}; const widgets = node.widgets; @@ -1504,6 +1532,17 @@ export class ComfyApp { } } + const getInputRef = (inputNode, inputSlot) => { + if (inputNode.type == "Subflow") { + // input should be mapped to inner node + const [ localOriginId, originSlot ] = inputNode.getExportedOutput(inputSlot); + const originId = subflowIdMapping[inputNode.id] + localOriginId; + return [String(originId), parseInt(originSlot)]; + } + + return [String(inputNode.id), parseInt(inputSlot)]; + }; + // Store all node links for (let i in node.inputs) { let parent = node.getInputNode(i); @@ -1543,15 +1582,24 @@ export class ComfyApp { } if (link) { - inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)]; + if (node.type == "Subflow") { + // inner node's input should be used + const [ localTargetId, targetSlot ] = node.getExportedInput(link.target_slot); + const targetId = subflowIdMapping[node.id] + localTargetId; + output[ String(targetId) ].inputs[ node.inputs[targetSlot].name ] = getInputRef(parent, link.origin_slot); + } + + inputs[node.inputs[i].name] = getInputRef(parent, link.origin_slot); } } } - output[String(node.id)] = { - inputs, - class_type: node.comfyClass, - }; + if (node.type != "Subflow") { + output[String(node.id)] = { + inputs, + class_type: node.comfyClass, + }; + } } // Remove inputs connected to removed nodes