diff --git a/web/extensions/core/groupNode.js b/web/extensions/core/groupNode.js new file mode 100644 index 000000000..eb374ecc5 --- /dev/null +++ b/web/extensions/core/groupNode.js @@ -0,0 +1,375 @@ +import { app } from "../../scripts/app.js"; +import { getWidgetType } from "../../scripts/widgets.js"; + +const IS_GROUP_NODE = Symbol(); +const GROUP_DATA = Symbol(); +const GROUP_SLOTS = Symbol(); + +function getLinks(config) { + const linksFrom = {}; + const linksTo = {}; + + // Extract links for easy lookup + for (const l of config.links) { + const [outputNodeId, outputNodeSlot, inputNodeId, inputNodeSlot] = l; + + // Skip links outside the copy config + if (outputNodeId == null) continue; + + if (!linksFrom[outputNodeId]) { + linksFrom[outputNodeId] = {}; + } + linksFrom[outputNodeId][outputNodeSlot] = l; + + if (!linksTo[inputNodeId]) { + linksTo[inputNodeId] = {}; + } + linksTo[inputNodeId][inputNodeSlot] = l; + } + return { linksTo, linksFrom }; +} + +// function getInnerLinkType(config, link) { +// const [outputNodeId, outputNodeSlot] = link; +// return config.nodes[outputNodeId].outputs[outputNodeSlot].type; +// } + +function buildNodeDef(config, nodeName, defs, workflow) { + const slots = { + inputs: {}, + widgets: {}, + outputs: {}, + }; + + const newDef = { + output: [], + output_name: [], + output_is_list: [], + name: nodeName, + display_name: nodeName, + category: "group nodes" + (workflow ? "/workflow" : ""), + input: { required: {} }, + + [IS_GROUP_NODE]: true, + [GROUP_DATA]: config, + [GROUP_SLOTS]: slots, + }; + const links = getLinks(config); + + console.log( + "Building group node", + nodeName, + config.nodes.map((n) => n.type) + ); + + let inputCount = 0; + for (let nodeId = 0; nodeId < config.nodes.length; nodeId++) { + const node = config.nodes[nodeId]; + console.log("Processing inner node", nodeId, node.type); + let def = defs[node.type]; + + const linksTo = links.linksTo[nodeId]; + const linksFrom = links.linksFrom[nodeId]; + + if (!def) { + // Special handling for reroutes to allow them to be used as inputs/outputs + if (node.type === "Reroute") { + if (linksTo && linksFrom) { + // Being used internally + // TODO: does anything actually need doing here? + continue; + } + + let rerouteType; + if (linksFrom) { + const [, , id, slot] = linksFrom["0"]; + rerouteType = config.nodes[id].inputs[slot].type; + } else { + const [id, slot] = linksTo["0"]; + rerouteType = config.nodes[id].outputs[slot].type; + } + + def = { + input: { + required: { + [rerouteType]: [rerouteType, {}], + }, + }, + output: [rerouteType], + output_name: [], + output_is_list: [], + }; + } else { + // Front end only node + // TODO: check these should all be ignored + debugger; + continue; + } + } + + const inputs = { ...def.input?.required, ...def.input?.optional }; + + // Add inputs / widgets + const inputNames = Object.keys(inputs); + let linkInputId = 0; + for (let inputId = 0; inputId < inputNames.length; inputId++) { + const inputName = inputNames[inputId]; + console.log("\t", "> Processing input", inputId, inputName); + const widgetType = getWidgetType(inputs[inputName], inputName); + let name = nodeId + ":" + inputName; + if (widgetType) { + console.log("\t\t", "Widget", widgetType); + + // Store mapping to get a group widget name from an inner id + name + if (!slots.widgets[nodeId]) slots.widgets[nodeId] = {}; + slots.widgets[nodeId][inputName] = name; + } else { + if (linksTo?.[linkInputId]) { + linkInputId++; + console.info("\t\t", "Link skipped as has internal connection"); + continue; + } + + console.info("\t\t", "Link", linkInputId + " -> outer input " + inputCount); + + // Store a mapping to let us get the group node input for a specific slot on an inner node + if (!slots.inputs[nodeId]) slots.inputs[nodeId] = {}; + slots.inputs[nodeId][linkInputId++] = inputCount++; + } + + let inputDef = inputs[inputName]; + if (inputName === "seed" || inputName === "noise_seed") { + inputDef = [...inputDef]; + inputDef[1] = { control_after_generate: true, ...inputDef[1] }; + } + newDef.input.required[name] = inputDef; + } + + // Add outputs + for (let outputId = 0; outputId < def.output.length; outputId++) { + console.log("\t", "< Processing output", outputId, def.output_name?.[outputId] ?? def.output[outputId]); + + if (linksFrom?.[outputId]) { + console.info("\t\t", "Skipping as has internal connection"); + continue; + } + + slots.outputs[newDef.output.length] = { + node: nodeId, + slot: outputId, + }; + + newDef.output.push(def.output[outputId]); + newDef.output_is_list.push(def.output_is_list[outputId]); + newDef.output_name.push(nodeId + ":" + (def.output_name?.[outputId] ?? def.output[outputId])); + } + } + + return newDef; +} + +const id = "Comfy.GroupNode"; +let globalDefs; +const ext = { + name: id, + setup() { + const orig = LGraphCanvas.prototype.getCanvasMenuOptions; + LGraphCanvas.prototype.getCanvasMenuOptions = function () { + const options = orig.apply(this, arguments); + + options.push(null); + options.push({ + content: `Convert to Group Node`, + disabled: !Object.keys(app.canvas.selected_nodes || {}).length, + callback: async () => { + const name = prompt("Enter group name"); + if (!name) return; + + let extra = app.graph.extra; + if (!extra) app.graph.extra = extra = {}; + let groupNodes = extra.groupNodes; + if (!groupNodes) extra.groupNodes = groupNodes = {}; + + if (name in groupNodes) { + if (app.graph._nodes.find((n) => n.type === name)) { + alert( + "An in use group node with this name already exists embedded in this workflow, please remove any instances or use a new name." + ); + return; + } else if ( + !confirm( + "An group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?" + ) + ) { + return; + } + } + + // Use the built in copyToClipboard function to generate the node data we need + const backup = localStorage.getItem("litegrapheditor_clipboard"); + app.canvas.copyToClipboard(); + const config = JSON.parse(localStorage.getItem("litegrapheditor_clipboard")); + localStorage.setItem("litegrapheditor_clipboard", backup); + const def = buildNodeDef(config, name, globalDefs, true); + await app.registerNodeDef("workflow/" + name, def); + + groupNodes[name] = config; + + let top; + let left; + for (const id in app.canvas.selected_nodes) { + const node = app.graph.getNodeById(id); + if (left == null || node.pos[0] < left) { + left = node.pos[0]; + } + if (top == null || node.pos[1] < top) { + top = node.pos[1]; + } + app.graph.remove(node); + } + + const newNode = LiteGraph.createNode("workflow/" + name); + newNode.pos = [left, top]; + app.graph.add(newNode); + }, + }); + + return options; + }; + }, + async beforeConfigureGraph(graphData) { + const groupNodes = graphData?.extra?.groupNodes; + if (!groupNodes) return; + + for (const name in groupNodes) { + const def = buildNodeDef(groupNodes[name], name, globalDefs, true); + await app.registerNodeDef("workflow/" + name, def); + } + }, + addCustomNodeDefs(defs) { + globalDefs = defs; + }, + nodeCreated(node) { + if (node.constructor.nodeData?.[IS_GROUP_NODE]) { + const config = node.constructor.nodeData[GROUP_DATA]; + const slots = node.constructor.nodeData[GROUP_SLOTS]; + + const onNodeCreated = node.onNodeCreated; + node.onNodeCreated = function () { + for (let innerNodeId = 0; innerNodeId < config.nodes.length; innerNodeId++) { + const values = config.nodes[innerNodeId].widgets_values; + if (!values) continue; + const widgets = slots.widgets?.[innerNodeId]; + if (!widgets) continue; + + const names = Object.values(widgets); + for (let i = 0; i < names.length; i++) { + if (values[i] == null) continue; + const widget = this.widgets.find((w) => w.name === names[i]); + if (widget) { + widget.value = values[i]; + } + } + } + + return onNodeCreated?.apply(this, arguments); + }; + + node.updateLink = function (link, innerNodes) { + // Replace the group node reference with the internal node + link = { ...link }; + const output = slots.outputs[link.origin_slot]; + let innerNode = this.innerNodes[output.node]; + let l; + while (innerNode.type === "Reroute") { + l = innerNode.getInputLink(0); + innerNode = innerNode.getInputNode(0); + } + + link.origin_id = innerNode.id; + link.origin_slot = l?.origin_slot ?? output.slot; + return link; + }; + + node.getInnerNodes = function () { + console.log("Expanding group node", this.comfyClass, this.id); + const links = getLinks(config); + + const innerNodes = config.nodes.map((n, i) => { + const innerNode = LiteGraph.createNode(n.type); + innerNode.configure(n); + + for (const innerWidget of innerNode.widgets ?? []) { + const groupWidgetName = slots.widgets[i][innerWidget.name]; + const groupWidget = node.widgets.find((w) => w.name === groupWidgetName); + if (groupWidget) { + console.log("Set widget value", groupWidgetName + " -> " + innerWidget.name, groupWidget.value); + innerWidget.value = groupWidget.value; + } + } + + innerNode.id = node.id + ":" + i; + innerNode.getInputNode = function (slot) { + if (!innerNode.comfyClass) slot = 0; + console.log("Get input node", innerNode.comfyClass, slot, innerNode.inputs[slot]?.name); + const outerSlot = slots.inputs[i]?.[slot]; + if (outerSlot != null) { + // Our inner node has a mapping to the group node inputs + // return the input node from there + console.log("\t", "Getting from group node input", outerSlot); + const inputNode = node.getInputNode(outerSlot); + console.log("\t", "Result", inputNode?.id, inputNode?.comfyClass); + return inputNode; + } + + // Internal link + const innerLink = links.linksTo[i][slot]; + console.log("\t", "Internal link", innerLink); + const inputNode = innerNodes[innerLink[0]]; + console.log("\t", "Result", inputNode?.id, inputNode?.comfyClass); + return inputNode; + }; + innerNode.getInputLink = function (slot) { + console.log("Get input link", innerNode.comfyClass, slot, innerNode.inputs[slot]?.name); + const outerSlot = slots.inputs[i]?.[slot]; + if (outerSlot != null) { + // The inner node is connected via the group node inputs + console.log("\t", "Getting from group node input", outerSlot); + const linkId = node.inputs[outerSlot].link; + let link = app.graph.links[linkId]; + + // Use the outer link, but update the target to the inner node + link = { + target_id: innerNode.id, + target_slot: slot, + ...link, + }; + console.log("\t", "Result", link); + return link; + } + + let link = links.linksTo[i][slot]; + // Use the inner link, but update the origin node to be inner node id + link = { + origin_id: node.id + ":" + link[0], + origin_slot: link[1], + target_id: node.id + ":" + i, + target_slot: slot, + }; + console.log("\t", "Internal link", link); + + return link; + }; + + return innerNode; + }); + + this.innerNodes = innerNodes; + + return innerNodes; + }; + } + }, +}; + +app.registerExtension(ext); diff --git a/web/extensions/core/widgetInputs.js b/web/extensions/core/widgetInputs.js index ce05a29e9..3cf755a8d 100644 --- a/web/extensions/core/widgetInputs.js +++ b/web/extensions/core/widgetInputs.js @@ -416,7 +416,7 @@ app.registerExtension({ } } - if (widget.type === "number" || widget.type === "combo") { + if ((widget.type === "number" && !inputData?.[1]?.control_after_generate) || widget.type === "combo") { addValueControlWidget(this, widget, "fixed"); } diff --git a/web/scripts/app.js b/web/scripts/app.js index 7698d0f11..5f87edcc1 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -1,5 +1,5 @@ import { ComfyLogging } from "./logging.js"; -import { ComfyWidgets } from "./widgets.js"; +import { ComfyWidgets, getWidgetType } from "./widgets.js"; import { ComfyUI, $el } from "./ui.js"; import { api } from "./api.js"; import { defaultGraph } from "./defaultGraph.js"; @@ -747,7 +747,7 @@ export class ComfyApp { * Adds a handler on paste that extracts and loads images or workflows from pasted JSON data */ #addPasteHandler() { - document.addEventListener("paste", (e) => { + document.addEventListener("paste", async (e) => { // ctrl+shift+v is used to paste nodes with connections // this is handled by litegraph if(this.shiftDown) return; @@ -795,7 +795,7 @@ export class ComfyApp { } if (workflow && workflow.version && workflow.nodes && workflow.extra) { - this.loadGraphData(workflow); + await this.loadGraphData(workflow); } else { if (e.target.type === "text" || e.target.type === "textarea") { @@ -1285,7 +1285,7 @@ export class ComfyApp { const json = localStorage.getItem("workflow"); if (json) { const workflow = JSON.parse(json); - this.loadGraphData(workflow); + await this.loadGraphData(workflow); restored = true; } } catch (err) { @@ -1294,7 +1294,7 @@ export class ComfyApp { // We failed to restore a workflow so load the default if (!restored) { - this.loadGraphData(); + await this.loadGraphData(); } // Save current workflow automatically @@ -1322,11 +1322,81 @@ export class ComfyApp { await this.#invokeExtensionsAsync("registerCustomNodes"); } + async registerNodeDef(nodeId, nodeData) { + const self = this; + const node = Object.assign( + function ComfyNode() { + var inputs = nodeData["input"]["required"]; + if (nodeData["input"]["optional"] != undefined) { + inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]); + } + const config = { minWidth: 1, minHeight: 1 }; + for (const inputName in inputs) { + const inputData = inputs[inputName]; + const type = inputData[0]; + + let widgetCreated = true; + const widgetType = getWidgetType(inputData, inputName); + if(widgetType) { + if(widgetType === "COMBO") { + Object.assign(config, self.widgets.COMBO(this, inputName, inputData, app) || {}); + } else { + Object.assign(config, self.widgets[widgetType](this, inputName, inputData, app) || {}); + } + } else { + // Node connection inputs + this.addInput(inputName, type); + widgetCreated = false; + } + + if(widgetCreated && inputData[1]?.forceInput && config?.widget) { + if (!config.widget.options) config.widget.options = {}; + config.widget.options.forceInput = inputData[1].forceInput; + } + if(widgetCreated && inputData[1]?.defaultInput && config?.widget) { + if (!config.widget.options) config.widget.options = {}; + config.widget.options.defaultInput = inputData[1].defaultInput; + } + } + + for (const o in nodeData["output"]) { + let output = nodeData["output"][o]; + if(output instanceof Array) output = "COMBO"; + const outputName = nodeData["output_name"][o] || output; + const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ; + this.addOutput(outputName, output, { shape: outputShape }); + } + + const s = this.computeSize(); + s[0] = Math.max(config.minWidth, s[0] * 1.5); + s[1] = Math.max(config.minHeight, s[1]); + this.size = s; + this.serialize_widgets = true; + + app.#invokeExtensionsAsync("nodeCreated", this); + }, + { + title: nodeData.display_name || nodeData.name, + comfyClass: nodeData.name, + nodeData + } + ); + node.prototype.comfyClass = nodeData.name; + + this.#addNodeContextMenuHandler(node); + this.#addDrawBackgroundHandler(node, app); + this.#addNodeKeyHandler(node); + + await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData); + LiteGraph.registerNodeType(nodeId, node); + node.category = nodeData.category; + } + async registerNodesFromDefs(defs) { await this.#invokeExtensionsAsync("addCustomNodeDefs", defs); // Generate list of known widgets - const widgets = Object.assign( + this.widgets = Object.assign( {}, ComfyWidgets, ...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean) @@ -1334,75 +1404,7 @@ export class ComfyApp { // Register a node for each definition for (const nodeId in defs) { - const nodeData = defs[nodeId]; - const node = Object.assign( - function ComfyNode() { - var inputs = nodeData["input"]["required"]; - if (nodeData["input"]["optional"] != undefined){ - inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]) - } - const config = { minWidth: 1, minHeight: 1 }; - for (const inputName in inputs) { - const inputData = inputs[inputName]; - const type = inputData[0]; - - let widgetCreated = true; - if (Array.isArray(type)) { - // Enums - Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {}); - } else if (`${type}:${inputName}` in widgets) { - // Support custom widgets by Type:Name - Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {}); - } else if (type in widgets) { - // Standard type widgets - Object.assign(config, widgets[type](this, inputName, inputData, app) || {}); - } else { - // Node connection inputs - this.addInput(inputName, type); - widgetCreated = false; - } - - if(widgetCreated && inputData[1]?.forceInput && config?.widget) { - if (!config.widget.options) config.widget.options = {}; - config.widget.options.forceInput = inputData[1].forceInput; - } - if(widgetCreated && inputData[1]?.defaultInput && config?.widget) { - if (!config.widget.options) config.widget.options = {}; - config.widget.options.defaultInput = inputData[1].defaultInput; - } - } - - for (const o in nodeData["output"]) { - let output = nodeData["output"][o]; - if(output instanceof Array) output = "COMBO"; - const outputName = nodeData["output_name"][o] || output; - const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ; - this.addOutput(outputName, output, { shape: outputShape }); - } - - const s = this.computeSize(); - s[0] = Math.max(config.minWidth, s[0] * 1.5); - s[1] = Math.max(config.minHeight, s[1]); - this.size = s; - this.serialize_widgets = true; - - app.#invokeExtensionsAsync("nodeCreated", this); - }, - { - title: nodeData.display_name || nodeData.name, - comfyClass: nodeData.name, - nodeData - } - ); - node.prototype.comfyClass = nodeData.name; - - this.#addNodeContextMenuHandler(node); - this.#addDrawBackgroundHandler(node, app); - this.#addNodeKeyHandler(node); - - await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData); - LiteGraph.registerNodeType(nodeId, node); - node.category = nodeData.category; + this.registerNodeDef(nodeId, defs[nodeId]); } } @@ -1410,7 +1412,7 @@ export class ComfyApp { * Populates the graph with the specified workflow data * @param {*} graphData A serialized graph object */ - loadGraphData(graphData) { + async loadGraphData(graphData) { this.clean(); let reset_invalid_values = false; @@ -1425,6 +1427,7 @@ export class ComfyApp { reset_invalid_values = true; } + await this.#invokeExtensionsAsync("beforeConfigureGraph", graphData); const missingNodeTypes = []; for (let n of graphData.nodes) { // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now @@ -1542,83 +1545,89 @@ export class ComfyApp { const workflow = this.graph.serialize(); const output = {}; // Process nodes in order of execution - for (const node of this.graph.computeExecutionOrder(false)) { - const n = workflow.nodes.find((n) => n.id === node.id); - - if (node.isVirtualNode) { - // Don't serialize frontend only nodes but let them make changes - if (node.applyToGraph) { - node.applyToGraph(workflow); + for (const outerNode of this.graph.computeExecutionOrder(false)) { + const n = workflow.nodes.find((n) => n.id === outerNode.id); + const innerNodes = outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode]; + for (const node of innerNodes) { + console.log(node.id, node.title ?? node.comfyClass); + if (node.isVirtualNode) { + // Don't serialize frontend only nodes but let them make changes + if (node.applyToGraph) { + node.applyToGraph(workflow); + } + continue; } - continue; - } - if (node.mode === 2 || node.mode === 4) { - // Don't serialize muted nodes - continue; - } + if (node.mode === 2 || node.mode === 4) { + // Don't serialize muted nodes + continue; + } - const inputs = {}; - const widgets = node.widgets; + const inputs = {}; + const widgets = node.widgets; - // Store all widget values - if (widgets) { - for (const i in widgets) { - const widget = widgets[i]; - if (!widget.options || widget.options.serialize !== false) { - inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value; + // Store all widget values + if (widgets) { + for (const i in widgets) { + const widget = widgets[i]; + if (!widget.options || widget.options.serialize !== false) { + inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value; + } } } - } - // Store all node links - for (let i in node.inputs) { - let parent = node.getInputNode(i); - if (parent) { - let link = node.getInputLink(i); - while (parent.mode === 4 || parent.isVirtualNode) { - let found = false; - if (parent.isVirtualNode) { - link = parent.getInputLink(link.origin_slot); - if (link) { - parent = parent.getInputNode(link.target_slot); - if (parent) { - found = true; - } - } - } else if (link && parent.mode === 4) { - let all_inputs = [link.origin_slot]; - if (parent.inputs) { - all_inputs = all_inputs.concat(Object.keys(parent.inputs)) - for (let parent_input in all_inputs) { - parent_input = all_inputs[parent_input]; - if (parent.inputs[parent_input].type === node.inputs[i].type) { - link = parent.getInputLink(parent_input); - if (link) { - parent = parent.getInputNode(parent_input); - } + // Store all node links + for (let i in node.inputs) { + let parent = node.getInputNode(i); + if (parent) { + let link = node.getInputLink(i); + while (parent.mode === 4 || parent.isVirtualNode) { + let found = false; + if (parent.isVirtualNode) { + link = parent.getInputLink(link.origin_slot); + if (link) { + parent = parent.getInputNode(link.target_slot); + if (parent) { found = true; - break; + } + } + } else if (link && parent.mode === 4) { + let all_inputs = [link.origin_slot]; + if (parent.inputs) { + all_inputs = all_inputs.concat(Object.keys(parent.inputs)) + for (let parent_input in all_inputs) { + parent_input = all_inputs[parent_input]; + if (parent.inputs[parent_input].type === node.inputs[i].type) { + link = parent.getInputLink(parent_input); + if (link) { + parent = parent.getInputNode(parent_input); + } + found = true; + break; + } } } } + + if (!found) { + break; + } } - if (!found) { - break; + if (link) { + if (parent.updateLink) { + link = parent.updateLink(link); + } + inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)]; } } - - if (link) { - inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)]; - } } - } - output[String(node.id)] = { - inputs, - class_type: node.comfyClass, - }; + output[String(node.id)] = { + inputs, + class_type: node.comfyClass, + }; + } } // Remove inputs connected to removed nodes @@ -1738,21 +1747,21 @@ export class ComfyApp { const pngInfo = await getPngMetadata(file); if (pngInfo) { if (pngInfo.workflow) { - this.loadGraphData(JSON.parse(pngInfo.workflow)); + await this.loadGraphData(JSON.parse(pngInfo.workflow)); } else if (pngInfo.parameters) { importA1111(this.graph, pngInfo.parameters); } } } else if (file.type === "application/json" || file.name?.endsWith(".json")) { const reader = new FileReader(); - reader.onload = () => { - this.loadGraphData(JSON.parse(reader.result)); + reader.onload = async () => { + await this.loadGraphData(JSON.parse(reader.result)); }; reader.readAsText(file); } else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) { const info = await getLatentMetadata(file); if (info.workflow) { - this.loadGraphData(JSON.parse(info.workflow)); + await this.loadGraphData(JSON.parse(info.workflow)); } } } diff --git a/web/scripts/ui.js b/web/scripts/ui.js index 1e7920167..6d6b63c4f 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -462,8 +462,8 @@ class ComfyList { return $el("div", {textContent: item.prompt[0] + ": "}, [ $el("button", { textContent: "Load", - onclick: () => { - app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); + onclick: async () => { + await app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); if (item.outputs) { app.nodeOutputs = item.outputs; } @@ -782,9 +782,9 @@ export class ComfyUI { } }), $el("button", { - id: "comfy-load-default-button", textContent: "Load Default", onclick: () => { + id: "comfy-load-default-button", textContent: "Load Default", onclick: async () => { if (!confirmClear.value || confirm("Load default workflow?")) { - app.loadGraphData() + await app.loadGraphData() } } }), diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index 2b0239374..fa9a38ef9 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -22,13 +22,26 @@ function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) { return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } }; } +export function getWidgetType(inputData, inputName) { + const type = inputData[0]; + + if (Array.isArray(type)) { + return "COMBO"; + } else if (`${type}:${inputName}` in ComfyWidgets) { + return `${type}:${inputName}`; + } else if (type in ComfyWidgets) { + return type; + } else { + return null; + } +} + export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values) { const valueControl = node.addWidget("combo", "control_after_generate", defaultValue, function (v) { }, { values: ["fixed", "increment", "decrement", "randomize"], serialize: false, // Don't include this in prompt. }); valueControl.afterQueued = () => { - var v = valueControl.value; if (targetWidget.type == "combo" && v !== "fixed") { @@ -77,26 +90,46 @@ export function addValueControlWidget(node, targetWidget, defaultValue = "random default: break; } - /*check if values are over or under their respective - * ranges and set them to min or max.*/ - if (targetWidget.value < min) - targetWidget.value = min; + /*check if values are over or under their respective + * ranges and set them to min or max.*/ + if (targetWidget.value < min) targetWidget.value = min; - if (targetWidget.value > max) - targetWidget.value = max; + if (targetWidget.value > max) targetWidget.value = max; } - } + }; return valueControl; -}; +} function seedWidget(node, inputName, inputData, app) { - const seed = ComfyWidgets.INT(node, inputName, inputData, app); + const seed = createIntWidget(node, inputName, inputData, app, true); const seedControl = addValueControlWidget(node, seed.widget, "randomize"); seed.widget.linkedWidgets = [seedControl]; return seed; } +function createIntWidget(node, inputName, inputData, app, isSeedInput) { + if (!isSeedInput && inputData[1]?.control_after_generate) { + return seedWidget(node, inputName, inputData, app); + } + + let widgetType = isSlider(inputData[1]["display"], app); + const { val, config } = getNumberDefaults(inputData, 1, 0, true); + Object.assign(config, { precision: 0 }); + return { + widget: node.addWidget( + widgetType, + inputName, + val, + function (v) { + const s = this.options.step / 10; + this.value = Math.round(v / s) * s; + }, + config + ), + }; +} + const MultilineSymbol = Symbol(); const MultilineResizeSymbol = Symbol(); @@ -175,22 +208,22 @@ function addMultilineWidget(node, name, opts, app) { .multiplySelf(ctx.getTransform()) .translateSelf(margin, margin + y); - const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) - Object.assign(this.inputEl.style, { - transformOrigin: "0 0", - transform: scale, - left: `${transform.a + transform.e}px`, - top: `${transform.d + transform.f}px`, - width: `${widgetWidth - (margin * 2)}px`, - height: `${this.parent.inputHeight - (margin * 2)}px`, - position: "absolute", - background: (!node.color)?'':node.color, - color: (!node.color)?'':'white', - zIndex: app.graph._nodes.indexOf(node), - }); - this.inputEl.hidden = !visible; - }, - }; + const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) + Object.assign(this.inputEl.style, { + transformOrigin: "0 0", + transform: scale, + left: `${transform.a + transform.e}px`, + top: `${transform.d + transform.f}px`, + width: `${widgetWidth - (margin * 2)}px`, + height: `${this.parent.inputHeight - (margin * 2)}px`, + position: "absolute", + background: (!node.color)?'':node.color, + color: (!node.color)?'':'white', + zIndex: app.graph._nodes.indexOf(node), + }); + this.inputEl.hidden = !visible; + }, + }; widget.inputEl = document.createElement("textarea"); widget.inputEl.className = "comfy-multiline-input"; widget.inputEl.value = opts.defaultVal; @@ -287,21 +320,7 @@ export const ComfyWidgets = { }, config) }; }, INT(node, inputName, inputData, app) { - let widgetType = isSlider(inputData[1]["display"], app); - const { val, config } = getNumberDefaults(inputData, 1, 0, true); - Object.assign(config, { precision: 0 }); - return { - widget: node.addWidget( - widgetType, - inputName, - val, - function (v) { - const s = this.options.step / 10; - this.value = Math.round(v / s) * s; - }, - config - ), - }; + return createIntWidget(node, inputName, inputData, app); }, BOOLEAN(node, inputName, inputData) { let defaultVal = inputData[1]["default"];