mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-09-14 13:35:05 +00:00
Group nodes (#1776)
* setup ui unit tests * Refactoring, adding connections * Few tweaks * Fix type * Add general test * Refactored and extended test * move to describe * for groups * wip group nodes * Relink nodes Fixed widget values Convert to nodes * Reconnect on convert back * add via node menu + canvas refactor * Add ws event handling * fix using wrong node on widget serialize * allow reroute pipe fix control_after_generate configure * allow multiple images * Add test for converted widgets on missing nodes + fix crash * tidy * mores tests + refactor * throw earlier to get less confusing error * support outputs * more test * add ci action * use lts node * Fix? * Prevent connecting non matching combos * update * accidently removed npm i * Disable logging extension * fix naming allow control_after_generate custom name allow convert from reroutes * group node tests * Add executing info, custom node icon Tidy * internal reroute just works * Fix crash on virtual nodes e.g. note * Save group nodes to templates * Fix template nodes not being stored * Fix aborting convert * tidy * Fix reconnecting output links on convert to group * Fix links on convert to nodes * Handle missing internal nodes * Trigger callback on text change * Apply value on connect * Fix converted widgets not reconnecting * Group node updates - persist internal ids in current session - copy widget values when converting to nodes - fix issue serializing converted inputs * Resolve issue with sanitized node name * Fix internal id * allow outputs to be used internally and externally * order widgets on group node various fixes * fix imageupload widget requiring a specific name * groupnode imageupload test give widget unique name * Fix issue with external node links * Add VAE model * Fix internal node id check * fix potential crash * wip widget input support * more wip group widget inputs * Group node refactor Support for primitives/converted widgets * Fix convert to nodes with internal reroutes * fix applying primitive * Fix control widget values * fix test
This commit is contained in:
@@ -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";
|
||||
@@ -779,7 +779,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;
|
||||
@@ -827,7 +827,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") {
|
||||
@@ -1177,7 +1177,19 @@ export class ComfyApp {
|
||||
});
|
||||
|
||||
api.addEventListener("executed", ({ detail }) => {
|
||||
this.nodeOutputs[detail.node] = detail.output;
|
||||
const output = this.nodeOutputs[detail.node];
|
||||
if (detail.merge && output) {
|
||||
for (const k in detail.output ?? {}) {
|
||||
const v = output[k];
|
||||
if (v instanceof Array) {
|
||||
output[k] = v.concat(detail.output[k]);
|
||||
} else {
|
||||
output[k] = detail.output[k];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.nodeOutputs[detail.node] = detail.output;
|
||||
}
|
||||
const node = this.graph.getNodeById(detail.node);
|
||||
if (node) {
|
||||
if (node.onExecuted)
|
||||
@@ -1292,6 +1304,7 @@ export class ComfyApp {
|
||||
this.#addProcessMouseHandler();
|
||||
this.#addProcessKeyHandler();
|
||||
this.#addConfigureHandler();
|
||||
this.#addApiUpdateHandlers();
|
||||
|
||||
this.graph = new LGraph();
|
||||
|
||||
@@ -1328,7 +1341,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) {
|
||||
@@ -1337,7 +1350,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
|
||||
@@ -1345,7 +1358,6 @@ export class ComfyApp {
|
||||
|
||||
this.#addDrawNodeHandler();
|
||||
this.#addDrawGroupsHandler();
|
||||
this.#addApiUpdateHandlers();
|
||||
this.#addDropHandler();
|
||||
this.#addCopyHandler();
|
||||
this.#addPasteHandler();
|
||||
@@ -1365,11 +1377,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)
|
||||
@@ -1377,75 +1459,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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1488,9 +1502,14 @@ export class ComfyApp {
|
||||
|
||||
showMissingNodesError(missingNodeTypes, hasAddedNodes = true) {
|
||||
this.ui.dialog.show(
|
||||
`When loading the graph, the following node types were not found: <ul>${Array.from(new Set(missingNodeTypes)).map(
|
||||
(t) => `<li>${t}</li>`
|
||||
).join("")}</ul>${hasAddedNodes ? "Nodes that have failed to load will show as red on the graph." : ""}`
|
||||
$el("div", [
|
||||
$el("span", { textContent: "When loading the graph, the following node types were not found: " }),
|
||||
$el(
|
||||
"ul",
|
||||
Array.from(new Set(missingNodeTypes)).map((t) => $el("li", { textContent: t }))
|
||||
),
|
||||
...(hasAddedNodes ? [$el("span", { textContent: "Nodes that have failed to load will show as red on the graph." })] : []),
|
||||
])
|
||||
);
|
||||
this.logging.addEntry("Comfy.App", "warn", {
|
||||
MissingNodes: missingNodeTypes,
|
||||
@@ -1501,7 +1520,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;
|
||||
@@ -1519,6 +1538,7 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
const missingNodeTypes = [];
|
||||
await this.#invokeExtensionsAsync("beforeConfigureGraph", graphData, missingNodeTypes);
|
||||
for (let n of graphData.nodes) {
|
||||
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
||||
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
|
||||
@@ -1527,8 +1547,8 @@ export class ComfyApp {
|
||||
|
||||
// Find missing node types
|
||||
if (!(n.type in LiteGraph.registered_node_types)) {
|
||||
n.type = sanitizeNodeName(n.type);
|
||||
missingNodeTypes.push(n.type);
|
||||
n.type = sanitizeNodeName(n.type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1627,92 +1647,98 @@ export class ComfyApp {
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
async graphToPrompt() {
|
||||
for (const node of this.graph.computeExecutionOrder(false)) {
|
||||
if (node.isVirtualNode) {
|
||||
// Don't serialize frontend only nodes but let them make changes
|
||||
if (node.applyToGraph) {
|
||||
node.applyToGraph();
|
||||
for (const outerNode of this.graph.computeExecutionOrder(false)) {
|
||||
const innerNodes = outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode];
|
||||
for (const node of innerNodes) {
|
||||
if (node.isVirtualNode) {
|
||||
// Don't serialize frontend only nodes but let them make changes
|
||||
if (node.applyToGraph) {
|
||||
node.applyToGraph();
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
for (const outerNode of this.graph.computeExecutionOrder(false)) {
|
||||
const innerNodes = outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode];
|
||||
for (const node of innerNodes) {
|
||||
if (node.isVirtualNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.isVirtualNode) {
|
||||
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(node, 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
|
||||
@@ -1832,7 +1858,7 @@ 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);
|
||||
}
|
||||
@@ -1848,21 +1874,21 @@ export class ComfyApp {
|
||||
}
|
||||
} else if (file.type === "application/json" || file.name?.endsWith(".json")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
reader.onload = async () => {
|
||||
const jsonContent = JSON.parse(reader.result);
|
||||
if (jsonContent?.templates) {
|
||||
this.loadTemplateData(jsonContent);
|
||||
} else if(this.isApiJson(jsonContent)) {
|
||||
this.loadApiJson(jsonContent);
|
||||
} else {
|
||||
this.loadGraphData(jsonContent);
|
||||
await this.loadGraphData(jsonContent);
|
||||
}
|
||||
};
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@ function getClipPath(node, element, elRect) {
|
||||
}
|
||||
|
||||
function computeSize(size) {
|
||||
if (this.widgets?.[0].last_y == null) return;
|
||||
if (this.widgets?.[0]?.last_y == null) return;
|
||||
|
||||
let y = this.widgets[0].last_y;
|
||||
let freeSpace = size[1] - y;
|
||||
@@ -195,7 +195,6 @@ export function addDomClippingSetting() {
|
||||
type: "boolean",
|
||||
defaultValue: enableDomClipping,
|
||||
onChange(value) {
|
||||
console.log("enableDomClipping", enableDomClipping);
|
||||
enableDomClipping = !!value;
|
||||
},
|
||||
});
|
||||
|
@@ -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;
|
||||
}
|
||||
@@ -784,9 +784,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()
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
@@ -23,29 +23,73 @@ function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) {
|
||||
return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } };
|
||||
}
|
||||
|
||||
export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values) {
|
||||
const widgets = addValueControlWidgets(node, targetWidget, defaultValue, values, {
|
||||
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, widgetName, inputData) {
|
||||
let name = inputData[1]?.control_after_generate;
|
||||
if(typeof name !== "string") {
|
||||
name = widgetName;
|
||||
}
|
||||
const widgets = addValueControlWidgets(node, targetWidget, defaultValue, {
|
||||
addFilterList: false,
|
||||
});
|
||||
controlAfterGenerateName: name
|
||||
}, inputData);
|
||||
return widgets[0];
|
||||
}
|
||||
|
||||
export function addValueControlWidgets(node, targetWidget, defaultValue = "randomize", values, options) {
|
||||
export function addValueControlWidgets(node, targetWidget, defaultValue = "randomize", options, inputData) {
|
||||
if (!defaultValue) defaultValue = "randomize";
|
||||
if (!options) options = {};
|
||||
|
||||
|
||||
const getName = (defaultName, optionName) => {
|
||||
let name = defaultName;
|
||||
if (options[optionName]) {
|
||||
name = options[optionName];
|
||||
} else if (typeof inputData?.[1]?.[defaultName] === "string") {
|
||||
name = inputData?.[1]?.[defaultName];
|
||||
} else if (inputData?.[1]?.control_prefix) {
|
||||
name = inputData?.[1]?.control_prefix + " " + name
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
const widgets = [];
|
||||
const valueControl = node.addWidget("combo", "control_after_generate", defaultValue, function (v) { }, {
|
||||
values: ["fixed", "increment", "decrement", "randomize"],
|
||||
serialize: false, // Don't include this in prompt.
|
||||
});
|
||||
const valueControl = node.addWidget(
|
||||
"combo",
|
||||
getName("control_after_generate", "controlAfterGenerateName"),
|
||||
defaultValue,
|
||||
function () {},
|
||||
{
|
||||
values: ["fixed", "increment", "decrement", "randomize"],
|
||||
serialize: false, // Don't include this in prompt.
|
||||
}
|
||||
);
|
||||
widgets.push(valueControl);
|
||||
|
||||
const isCombo = targetWidget.type === "combo";
|
||||
let comboFilter;
|
||||
if (isCombo && options.addFilterList !== false) {
|
||||
comboFilter = node.addWidget("string", "control_filter_list", "", function (v) {}, {
|
||||
serialize: false, // Don't include this in prompt.
|
||||
});
|
||||
comboFilter = node.addWidget(
|
||||
"string",
|
||||
getName("control_filter_list", "controlFilterListName"),
|
||||
"",
|
||||
function () {},
|
||||
{
|
||||
serialize: false, // Don't include this in prompt.
|
||||
}
|
||||
);
|
||||
widgets.push(comboFilter);
|
||||
}
|
||||
|
||||
@@ -96,7 +140,8 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
|
||||
targetWidget.value = value;
|
||||
targetWidget.callback(value);
|
||||
}
|
||||
} else { //number
|
||||
} else {
|
||||
//number
|
||||
let min = targetWidget.options.min;
|
||||
let max = targetWidget.options.max;
|
||||
// limit to something that javascript can handle
|
||||
@@ -119,32 +164,54 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
|
||||
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;
|
||||
targetWidget.callback(targetWidget.value);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
return widgets;
|
||||
};
|
||||
|
||||
function seedWidget(node, inputName, inputData, app) {
|
||||
const seed = ComfyWidgets.INT(node, inputName, inputData, app);
|
||||
const seedControl = addValueControlWidget(node, seed.widget, "randomize");
|
||||
function seedWidget(node, inputName, inputData, app, widgetName) {
|
||||
const seed = createIntWidget(node, inputName, inputData, app, true);
|
||||
const seedControl = addValueControlWidget(node, seed.widget, "randomize", undefined, widgetName, inputData);
|
||||
|
||||
seed.widget.linkedWidgets = [seedControl];
|
||||
return seed;
|
||||
}
|
||||
|
||||
function createIntWidget(node, inputName, inputData, app, isSeedInput) {
|
||||
const control = inputData[1]?.control_after_generate;
|
||||
if (!isSeedInput && control) {
|
||||
return seedWidget(node, inputName, inputData, app, typeof control === "string" ? control : undefined);
|
||||
}
|
||||
|
||||
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
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function addMultilineWidget(node, name, opts, app) {
|
||||
const inputEl = document.createElement("textarea");
|
||||
inputEl.className = "comfy-multiline-input";
|
||||
inputEl.value = opts.defaultVal;
|
||||
inputEl.placeholder = opts.placeholder || "";
|
||||
inputEl.placeholder = opts.placeholder || name;
|
||||
|
||||
const widget = node.addDOMWidget(name, "customtext", inputEl, {
|
||||
getValue() {
|
||||
@@ -156,6 +223,10 @@ function addMultilineWidget(node, name, opts, app) {
|
||||
});
|
||||
widget.inputEl = inputEl;
|
||||
|
||||
inputEl.addEventListener("input", () => {
|
||||
widget.callback?.(widget.value);
|
||||
});
|
||||
|
||||
return { minWidth: 400, minHeight: 200, widget };
|
||||
}
|
||||
|
||||
@@ -186,21 +257,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 = false;
|
||||
@@ -245,10 +302,14 @@ export const ComfyWidgets = {
|
||||
if (inputData[1] && inputData[1].default) {
|
||||
defaultValue = inputData[1].default;
|
||||
}
|
||||
return { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
|
||||
const res = { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
|
||||
if (inputData[1]?.control_after_generate) {
|
||||
res.widget.linkedWidgets = addValueControlWidgets(node, res.widget, undefined, undefined, inputData);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
IMAGEUPLOAD(node, inputName, inputData, app) {
|
||||
const imageWidget = node.widgets.find((w) => w.name === "image");
|
||||
const imageWidget = node.widgets.find((w) => w.name === (inputData[1]?.widget ?? "image"));
|
||||
let uploadWidget;
|
||||
|
||||
function showImage(name) {
|
||||
@@ -362,9 +423,10 @@ export const ComfyWidgets = {
|
||||
document.body.append(fileInput);
|
||||
|
||||
// Create the button widget for selecting the files
|
||||
uploadWidget = node.addWidget("button", "choose file to upload", "image", () => {
|
||||
uploadWidget = node.addWidget("button", inputName, "image", () => {
|
||||
fileInput.click();
|
||||
});
|
||||
uploadWidget.label = "choose file to upload";
|
||||
uploadWidget.serialize = false;
|
||||
|
||||
// Add handler to check if an image is being dragged over our node
|
||||
|
Reference in New Issue
Block a user