Unverified Commit 7f469203 authored by pythongosssss's avatar pythongosssss Committed by GitHub
Browse files

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
parent d19de275
{
"path-intellisense.mappings": {
"../": "${workspaceFolder}/web/extensions/core"
},
"[python]": {
"editor.defaultFormatter": "ms-python.autopep8"
},
"python.formatting.provider": "none"
}
...@@ -20,6 +20,7 @@ async function setup() { ...@@ -20,6 +20,7 @@ async function setup() {
// Modify the response data to add some checkpoints // Modify the response data to add some checkpoints
const objectInfo = JSON.parse(data); const objectInfo = JSON.parse(data);
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name[0] = ["model1.safetensors", "model2.ckpt"]; objectInfo.CheckpointLoaderSimple.input.required.ckpt_name[0] = ["model1.safetensors", "model2.ckpt"];
objectInfo.VAELoader.input.required.vae_name[0] = ["vae1.safetensors", "vae2.ckpt"];
data = JSON.stringify(objectInfo, undefined, "\t"); data = JSON.stringify(objectInfo, undefined, "\t");
......
This diff is collapsed.
...@@ -202,8 +202,8 @@ describe("widget inputs", () => { ...@@ -202,8 +202,8 @@ describe("widget inputs", () => {
}); });
expect(dialogShow).toBeCalledTimes(1); expect(dialogShow).toBeCalledTimes(1);
expect(dialogShow.mock.calls[0][0]).toContain("the following node types were not found"); expect(dialogShow.mock.calls[0][0].innerHTML).toContain("the following node types were not found");
expect(dialogShow.mock.calls[0][0]).toContain("TestNode"); expect(dialogShow.mock.calls[0][0].innerHTML).toContain("TestNode");
}); });
test("defaultInput widgets can be converted back to inputs", async () => { test("defaultInput widgets can be converted back to inputs", async () => {
......
...@@ -150,7 +150,7 @@ export class EzNodeMenuItem { ...@@ -150,7 +150,7 @@ export class EzNodeMenuItem {
if (selectNode) { if (selectNode) {
this.node.select(); this.node.select();
} }
this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node); return this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node);
} }
} }
...@@ -240,8 +240,12 @@ export class EzNode { ...@@ -240,8 +240,12 @@ export class EzNode {
return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem); return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
} }
select() { get isRemoved() {
this.app.canvas.selectNode(this.node); return !this.app.graph.getNodeById(this.id);
}
select(addToSelection = false) {
this.app.canvas.selectNode(this.node, addToSelection);
} }
// /** // /**
...@@ -275,12 +279,17 @@ export class EzNode { ...@@ -275,12 +279,17 @@ export class EzNode {
if (!s) return p; if (!s) return p;
const name = s[nameProperty]; const name = s[nameProperty];
const item = new ctor(this, i, s);
// @ts-ignore // @ts-ignore
if (!name || name in p) { p.push(item);
throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`); if (name) {
// @ts-ignore
if (name in p) {
throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`);
}
} }
// @ts-ignore // @ts-ignore
p.push((p[name] = new ctor(this, i, s))); p[name] = item;
return p; return p;
}, Object.assign([], { $: this })); }, Object.assign([], { $: this }));
} }
...@@ -348,6 +357,19 @@ export class EzGraph { ...@@ -348,6 +357,19 @@ export class EzGraph {
}, 10); }, 10);
}); });
} }
/**
* @returns { Promise<{
* workflow: {},
* output: Record<string, {
* class_name: string,
* inputs: Record<string, [string, number] | unknown>
* }>}> }
*/
toPrompt() {
// @ts-ignore
return this.app.graphToPrompt();
}
} }
export const Ez = { export const Ez = {
...@@ -356,12 +378,12 @@ export const Ez = { ...@@ -356,12 +378,12 @@ export const Ez = {
* @example * @example
* const { ez, graph } = Ez.graph(app); * const { ez, graph } = Ez.graph(app);
* graph.clear(); * graph.clear();
* const [model, clip, vae] = ez.CheckpointLoaderSimple(); * const [model, clip, vae] = ez.CheckpointLoaderSimple().outputs;
* const [pos] = ez.CLIPTextEncode(clip, { text: "positive" }); * const [pos] = ez.CLIPTextEncode(clip, { text: "positive" }).outputs;
* const [neg] = ez.CLIPTextEncode(clip, { text: "negative" }); * const [neg] = ez.CLIPTextEncode(clip, { text: "negative" }).outputs;
* const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage()); * const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage().outputs).outputs;
* const [image] = ez.VAEDecode(latent, vae); * const [image] = ez.VAEDecode(latent, vae).outputs;
* const saveNode = ez.SaveImage(image).node; * const saveNode = ez.SaveImage(image);
* console.log(saveNode); * console.log(saveNode);
* graph.arrange(); * graph.arrange();
* @param { app } app * @param { app } app
......
const { mockApi } = require("./setup"); const { mockApi } = require("./setup");
const { Ez } = require("./ezgraph"); const { Ez } = require("./ezgraph");
const lg = require("./litegraph");
/** /**
* *
* @param { Parameters<mockApi>[0] } config * @param { Parameters<mockApi>[0] & { resetEnv?: boolean } } config
* @returns * @returns
*/ */
export async function start(config = undefined) { export async function start(config = undefined) {
if(config?.resetEnv) {
jest.resetModules();
jest.resetAllMocks();
lg.setup(global);
}
mockApi(config); mockApi(config);
const { app } = require("../../web/scripts/app"); const { app } = require("../../web/scripts/app");
await app.setup(); await app.setup();
return Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]); return { ...Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]), app };
} }
/** /**
* @param { ReturnType<Ez["graph"]>["graph"] } graph * @param { ReturnType<Ez["graph"]>["graph"] } graph
* @param { (hasReloaded: boolean) => (Promise<void> | void) } cb * @param { (hasReloaded: boolean) => (Promise<void> | void) } cb
*/ */
export async function checkBeforeAndAfterReload(graph, cb) { export async function checkBeforeAndAfterReload(graph, cb) {
await cb(false); await cb(false);
...@@ -24,10 +31,10 @@ export async function checkBeforeAndAfterReload(graph, cb) { ...@@ -24,10 +31,10 @@ export async function checkBeforeAndAfterReload(graph, cb) {
} }
/** /**
* @param { string } name * @param { string } name
* @param { Record<string, string | [string | string[], any]> } input * @param { Record<string, string | [string | string[], any]> } input
* @param { (string | string[])[] | Record<string, string | string[]> } output * @param { (string | string[])[] | Record<string, string | string[]> } output
* @returns { Record<string, import("../../web/types/comfy").ComfyObjectInfo> } * @returns { Record<string, import("../../web/types/comfy").ComfyObjectInfo> }
*/ */
export function makeNodeDef(name, input, output = {}) { export function makeNodeDef(name, input, output = {}) {
const nodeDef = { const nodeDef = {
...@@ -37,19 +44,19 @@ export function makeNodeDef(name, input, output = {}) { ...@@ -37,19 +44,19 @@ export function makeNodeDef(name, input, output = {}) {
output_name: [], output_name: [],
output_is_list: [], output_is_list: [],
input: { input: {
required: {} required: {},
}, },
}; };
for(const k in input) { for (const k in input) {
nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]]; nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]];
} }
if(output instanceof Array) { if (output instanceof Array) {
output = output.reduce((p, c) => { output = output.reduce((p, c) => {
p[c] = c; p[c] = c;
return p; return p;
}, {}) }, {});
} }
for(const k in output) { for (const k in output) {
nodeDef.output.push(output[k]); nodeDef.output.push(output[k]);
nodeDef.output_name.push(k); nodeDef.output_name.push(k);
nodeDef.output_is_list.push(false); nodeDef.output_is_list.push(false);
...@@ -68,4 +75,31 @@ export function assertNotNullOrUndefined(x) { ...@@ -68,4 +75,31 @@ export function assertNotNullOrUndefined(x) {
expect(x).not.toEqual(null); expect(x).not.toEqual(null);
expect(x).not.toEqual(undefined); expect(x).not.toEqual(undefined);
return true; return true;
} }
\ No newline at end of file
/**
*
* @param { ReturnType<Ez["graph"]>["ez"] } ez
* @param { ReturnType<Ez["graph"]>["graph"] } graph
*/
export function createDefaultWorkflow(ez, graph) {
graph.clear();
const ckpt = ez.CheckpointLoaderSimple();
const pos = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "positive" });
const neg = ez.CLIPTextEncode(ckpt.outputs.CLIP, { text: "negative" });
const empty = ez.EmptyLatentImage();
const sampler = ez.KSampler(
ckpt.outputs.MODEL,
pos.outputs.CONDITIONING,
neg.outputs.CONDITIONING,
empty.outputs.LATENT
);
const decode = ez.VAEDecode(sampler.outputs.LATENT, ckpt.outputs.VAE);
const save = ez.SaveImage(decode.outputs.IMAGE);
graph.arrange();
return { ckpt, pos, neg, empty, sampler, decode, save };
}
...@@ -30,16 +30,20 @@ export function mockApi({ mockExtensions, mockNodeDefs } = {}) { ...@@ -30,16 +30,20 @@ export function mockApi({ mockExtensions, mockNodeDefs } = {}) {
mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./data/object_info.json"))); mockNodeDefs = JSON.parse(fs.readFileSync(path.resolve("./data/object_info.json")));
} }
const events = new EventTarget();
const mockApi = {
addEventListener: events.addEventListener.bind(events),
removeEventListener: events.removeEventListener.bind(events),
dispatchEvent: events.dispatchEvent.bind(events),
getSystemStats: jest.fn(),
getExtensions: jest.fn(() => mockExtensions),
getNodeDefs: jest.fn(() => mockNodeDefs),
init: jest.fn(),
apiURL: jest.fn((x) => "../../web/" + x),
};
jest.mock("../../web/scripts/api", () => ({ jest.mock("../../web/scripts/api", () => ({
get api() { get api() {
return { return mockApi;
addEventListener: jest.fn(),
getSystemStats: jest.fn(),
getExtensions: jest.fn(() => mockExtensions),
getNodeDefs: jest.fn(() => mockNodeDefs),
init: jest.fn(),
apiURL: jest.fn((x) => "../../web/" + x),
};
}, },
})); }));
} }
This diff is collapsed.
import { app } from "../../scripts/app.js"; import { app } from "../../scripts/app.js";
import { ComfyDialog, $el } from "../../scripts/ui.js"; import { ComfyDialog, $el } from "../../scripts/ui.js";
import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js";
// Adds the ability to save and add multiple nodes as a template // Adds the ability to save and add multiple nodes as a template
// To save: // To save:
...@@ -34,7 +35,7 @@ class ManageTemplates extends ComfyDialog { ...@@ -34,7 +35,7 @@ class ManageTemplates extends ComfyDialog {
type: "file", type: "file",
accept: ".json", accept: ".json",
multiple: true, multiple: true,
style: {display: "none"}, style: { display: "none" },
parent: document.body, parent: document.body,
onchange: () => this.importAll(), onchange: () => this.importAll(),
}); });
...@@ -109,13 +110,13 @@ class ManageTemplates extends ComfyDialog { ...@@ -109,13 +110,13 @@ class ManageTemplates extends ComfyDialog {
return; return;
} }
const json = JSON.stringify({templates: this.templates}, null, 2); // convert the data to a JSON string const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string
const blob = new Blob([json], {type: "application/json"}); const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = $el("a", { const a = $el("a", {
href: url, href: url,
download: "node_templates.json", download: "node_templates.json",
style: {display: "none"}, style: { display: "none" },
parent: document.body, parent: document.body,
}); });
a.click(); a.click();
...@@ -291,11 +292,11 @@ app.registerExtension({ ...@@ -291,11 +292,11 @@ app.registerExtension({
setup() { setup() {
const manage = new ManageTemplates(); const manage = new ManageTemplates();
const clipboardAction = (cb) => { const clipboardAction = async (cb) => {
// We use the clipboard functions but dont want to overwrite the current user clipboard // We use the clipboard functions but dont want to overwrite the current user clipboard
// Restore it after we've run our callback // Restore it after we've run our callback
const old = localStorage.getItem("litegrapheditor_clipboard"); const old = localStorage.getItem("litegrapheditor_clipboard");
cb(); await cb();
localStorage.setItem("litegrapheditor_clipboard", old); localStorage.setItem("litegrapheditor_clipboard", old);
}; };
...@@ -309,13 +310,31 @@ app.registerExtension({ ...@@ -309,13 +310,31 @@ app.registerExtension({
disabled: !Object.keys(app.canvas.selected_nodes || {}).length, disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
callback: () => { callback: () => {
const name = prompt("Enter name"); const name = prompt("Enter name");
if (!name || !name.trim()) return; if (!name?.trim()) return;
clipboardAction(() => { clipboardAction(() => {
app.canvas.copyToClipboard(); app.canvas.copyToClipboard();
let data = localStorage.getItem("litegrapheditor_clipboard");
data = JSON.parse(data);
const nodeIds = Object.keys(app.canvas.selected_nodes);
for (let i = 0; i < nodeIds.length; i++) {
const node = app.graph.getNodeById(nodeIds[i]);
const nodeData = node?.constructor.nodeData;
let groupData = GroupNodeHandler.getGroupData(node);
if (groupData) {
groupData = groupData.nodeData;
if (!data.groupNodes) {
data.groupNodes = {};
}
data.groupNodes[nodeData.name] = groupData;
data.nodes[i].type = nodeData.name;
}
}
manage.templates.push({ manage.templates.push({
name, name,
data: localStorage.getItem("litegrapheditor_clipboard"), data: JSON.stringify(data),
}); });
manage.store(); manage.store();
}); });
...@@ -323,15 +342,19 @@ app.registerExtension({ ...@@ -323,15 +342,19 @@ app.registerExtension({
}); });
// Map each template to a menu item // Map each template to a menu item
const subItems = manage.templates.map((t) => ({ const subItems = manage.templates.map((t) => {
content: t.name, return {
callback: () => { content: t.name,
clipboardAction(() => { callback: () => {
localStorage.setItem("litegrapheditor_clipboard", t.data); clipboardAction(async () => {
app.canvas.pasteFromClipboard(); const data = JSON.parse(t.data);
}); await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
}, localStorage.setItem("litegrapheditor_clipboard", t.data);
})); app.canvas.pasteFromClipboard();
});
},
};
});
subItems.push(null, { subItems.push(null, {
content: "Manage", content: "Manage",
......
...@@ -121,6 +121,110 @@ function isValidCombo(combo, obj) { ...@@ -121,6 +121,110 @@ function isValidCombo(combo, obj) {
return true; return true;
} }
export function mergeIfValid(output, config2, forceUpdate, recreateWidget, config1) {
if (!config1) {
config1 = output.widget[CONFIG] ?? output.widget[GET_CONFIG]();
}
if (config1[0] instanceof Array) {
if (!isValidCombo(config1[0], config2[0])) return false;
} else if (config1[0] !== config2[0]) {
// Types dont match
console.log(`connection rejected: types dont match`, config1[0], config2[0]);
return false;
}
const keys = new Set([...Object.keys(config1[1] ?? {}), ...Object.keys(config2[1] ?? {})]);
let customConfig;
const getCustomConfig = () => {
if (!customConfig) {
if (typeof structuredClone === "undefined") {
customConfig = JSON.parse(JSON.stringify(config1[1] ?? {}));
} else {
customConfig = structuredClone(config1[1] ?? {});
}
}
return customConfig;
};
const isNumber = config1[0] === "INT" || config1[0] === "FLOAT";
for (const k of keys.values()) {
if (k !== "default" && k !== "forceInput" && k !== "defaultInput") {
let v1 = config1[1][k];
let v2 = config2[1]?.[k];
if (v1 === v2 || (!v1 && !v2)) continue;
if (isNumber) {
if (k === "min") {
const theirMax = config2[1]?.["max"];
if (theirMax != null && v1 > theirMax) {
console.log("connection rejected: min > max", v1, theirMax);
return false;
}
getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.max(v1, v2);
continue;
} else if (k === "max") {
const theirMin = config2[1]?.["min"];
if (theirMin != null && v1 < theirMin) {
console.log("connection rejected: max < min", v1, theirMin);
return false;
}
getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2);
continue;
} else if (k === "step") {
let step;
if (v1 == null) {
// No current step
step = v2;
} else if (v2 == null) {
// No new step
step = v1;
} else {
if (v1 < v2) {
// Ensure v1 is larger for the mod
const a = v2;
v2 = v1;
v1 = a;
}
if (v1 % v2) {
console.log("connection rejected: steps not divisible", "current:", v1, "new:", v2);
return false;
}
step = v1;
}
getCustomConfig()[k] = step;
continue;
}
}
console.log(`connection rejected: config ${k} values dont match`, v1, v2);
return false;
}
}
if (customConfig || forceUpdate) {
if (customConfig) {
output.widget[CONFIG] = [config1[0], customConfig];
}
const widget = recreateWidget?.call(this);
// When deleting a node this can be null
if (widget) {
const min = widget.options.min;
const max = widget.options.max;
if (min != null && widget.value < min) widget.value = min;
if (max != null && widget.value > max) widget.value = max;
widget.callback(widget.value);
}
}
return { customConfig };
}
app.registerExtension({ app.registerExtension({
name: "Comfy.WidgetInputs", name: "Comfy.WidgetInputs",
async beforeRegisterNodeDef(nodeType, nodeData, app) { async beforeRegisterNodeDef(nodeType, nodeData, app) {
...@@ -308,7 +412,7 @@ app.registerExtension({ ...@@ -308,7 +412,7 @@ app.registerExtension({
this.isVirtualNode = true; this.isVirtualNode = true;
} }
applyToGraph() { applyToGraph(extraLinks = []) {
if (!this.outputs[0].links?.length) return; if (!this.outputs[0].links?.length) return;
function get_links(node) { function get_links(node) {
...@@ -325,10 +429,9 @@ app.registerExtension({ ...@@ -325,10 +429,9 @@ app.registerExtension({
return links; return links;
} }
let links = get_links(this); let links = [...get_links(this).map((l) => app.graph.links[l]), ...extraLinks];
// For each output link copy our value over the original widget value // For each output link copy our value over the original widget value
for (const l of links) { for (const linkInfo of links) {
const linkInfo = app.graph.links[l];
const node = this.graph.getNodeById(linkInfo.target_id); const node = this.graph.getNodeById(linkInfo.target_id);
const input = node.inputs[linkInfo.target_slot]; const input = node.inputs[linkInfo.target_slot];
const widgetName = input.widget.name; const widgetName = input.widget.name;
...@@ -405,7 +508,12 @@ app.registerExtension({ ...@@ -405,7 +508,12 @@ app.registerExtension({
} }
if (this.outputs[slot].links?.length) { if (this.outputs[slot].links?.length) {
return this.#isValidConnection(input); const valid = this.#isValidConnection(input);
if (valid) {
// On connect of additional outputs, copy our value to their widget
this.applyToGraph([{ target_id: target_node.id, target_slot }]);
}
return valid;
} }
} }
...@@ -462,12 +570,12 @@ app.registerExtension({ ...@@ -462,12 +570,12 @@ app.registerExtension({
} }
} }
if (widget.type === "number" || widget.type === "combo") { if (!inputData?.[1]?.control_after_generate && (widget.type === "number" || widget.type === "combo")) {
let control_value = this.widgets_values?.[1]; let control_value = this.widgets_values?.[1];
if (!control_value) { if (!control_value) {
control_value = "fixed"; control_value = "fixed";
} }
addValueControlWidgets(this, widget, control_value); addValueControlWidgets(this, widget, control_value, undefined, inputData);
let filter = this.widgets_values?.[2]; let filter = this.widgets_values?.[2];
if(filter && this.widgets.length === 3) { if(filter && this.widgets.length === 3) {
this.widgets[2].value = filter; this.widgets[2].value = filter;
...@@ -507,6 +615,7 @@ app.registerExtension({ ...@@ -507,6 +615,7 @@ app.registerExtension({
this.#removeWidgets(); this.#removeWidgets();
this.#onFirstConnection(true); this.#onFirstConnection(true);
for (let i = 0; i < this.widgets?.length; i++) this.widgets[i].value = values[i]; for (let i = 0; i < this.widgets?.length; i++) this.widgets[i].value = values[i];
return this.widgets[0];
} }
#mergeWidgetConfig() { #mergeWidgetConfig() {
...@@ -547,108 +656,8 @@ app.registerExtension({ ...@@ -547,108 +656,8 @@ app.registerExtension({
#isValidConnection(input, forceUpdate) { #isValidConnection(input, forceUpdate) {
// Only allow connections where the configs match // Only allow connections where the configs match
const output = this.outputs[0]; const output = this.outputs[0];
const config1 = output.widget[CONFIG] ?? output.widget[GET_CONFIG]();
const config2 = input.widget[GET_CONFIG](); const config2 = input.widget[GET_CONFIG]();
return !!mergeIfValid.call(this, output, config2, forceUpdate, this.#recreateWidget);
if (config1[0] instanceof Array) {
if (!isValidCombo(config1[0], config2[0])) return false;
} else if (config1[0] !== config2[0]) {
// Types dont match
console.log(`connection rejected: types dont match`, config1[0], config2[0]);
return false;
}
const keys = new Set([...Object.keys(config1[1] ?? {}), ...Object.keys(config2[1] ?? {})]);
let customConfig;
const getCustomConfig = () => {
if (!customConfig) {
if (typeof structuredClone === "undefined") {
customConfig = JSON.parse(JSON.stringify(config1[1] ?? {}));
} else {
customConfig = structuredClone(config1[1] ?? {});
}
}
return customConfig;
};
const isNumber = config1[0] === "INT" || config1[0] === "FLOAT";
for (const k of keys.values()) {
if (k !== "default" && k !== "forceInput" && k !== "defaultInput") {
let v1 = config1[1][k];
let v2 = config2[1][k];
if (v1 === v2 || (!v1 && !v2)) continue;
if (isNumber) {
if (k === "min") {
const theirMax = config2[1]["max"];
if (theirMax != null && v1 > theirMax) {
console.log("connection rejected: min > max", v1, theirMax);
return false;
}
getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.max(v1, v2);
continue;
} else if (k === "max") {
const theirMin = config2[1]["min"];
if (theirMin != null && v1 < theirMin) {
console.log("connection rejected: max < min", v1, theirMin);
return false;
}
getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2);
continue;
} else if (k === "step") {
let step;
if (v1 == null) {
// No current step
step = v2;
} else if (v2 == null) {
// No new step
step = v1;
} else {
if (v1 < v2) {
// Ensure v1 is larger for the mod
const a = v2;
v2 = v1;
v1 = a;
}
if (v1 % v2) {
console.log("connection rejected: steps not divisible", "current:", v1, "new:", v2);
return false;
}
step = v1;
}
getCustomConfig()[k] = step;
continue;
}
}
console.log(`connection rejected: config ${k} values dont match`, v1, v2);
return false;
}
}
if (customConfig || forceUpdate) {
if (customConfig) {
output.widget[CONFIG] = [config1[0], customConfig];
}
this.#recreateWidget();
const widget = this.widgets[0];
// When deleting a node this can be null
if (widget) {
const min = widget.options.min;
const max = widget.options.max;
if (min != null && widget.value < min) widget.value = min;
if (max != null && widget.value > max) widget.value = max;
widget.callback(widget.value);
}
}
return true;
} }
#removeWidgets() { #removeWidgets() {
......
import { ComfyLogging } from "./logging.js"; import { ComfyLogging } from "./logging.js";
import { ComfyWidgets } from "./widgets.js"; import { ComfyWidgets, getWidgetType } from "./widgets.js";
import { ComfyUI, $el } from "./ui.js"; import { ComfyUI, $el } from "./ui.js";
import { api } from "./api.js"; import { api } from "./api.js";
import { defaultGraph } from "./defaultGraph.js"; import { defaultGraph } from "./defaultGraph.js";
...@@ -779,7 +779,7 @@ export class ComfyApp { ...@@ -779,7 +779,7 @@ export class ComfyApp {
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data * Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
*/ */
#addPasteHandler() { #addPasteHandler() {
document.addEventListener("paste", (e) => { document.addEventListener("paste", async (e) => {
// ctrl+shift+v is used to paste nodes with connections // ctrl+shift+v is used to paste nodes with connections
// this is handled by litegraph // this is handled by litegraph
if(this.shiftDown) return; if(this.shiftDown) return;
...@@ -827,7 +827,7 @@ export class ComfyApp { ...@@ -827,7 +827,7 @@ export class ComfyApp {
} }
if (workflow && workflow.version && workflow.nodes && workflow.extra) { if (workflow && workflow.version && workflow.nodes && workflow.extra) {
this.loadGraphData(workflow); await this.loadGraphData(workflow);
} }
else { else {
if (e.target.type === "text" || e.target.type === "textarea") { if (e.target.type === "text" || e.target.type === "textarea") {
...@@ -1177,7 +1177,19 @@ export class ComfyApp { ...@@ -1177,7 +1177,19 @@ export class ComfyApp {
}); });
api.addEventListener("executed", ({ detail }) => { 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); const node = this.graph.getNodeById(detail.node);
if (node) { if (node) {
if (node.onExecuted) if (node.onExecuted)
...@@ -1292,6 +1304,7 @@ export class ComfyApp { ...@@ -1292,6 +1304,7 @@ export class ComfyApp {
this.#addProcessMouseHandler(); this.#addProcessMouseHandler();
this.#addProcessKeyHandler(); this.#addProcessKeyHandler();
this.#addConfigureHandler(); this.#addConfigureHandler();
this.#addApiUpdateHandlers();
this.graph = new LGraph(); this.graph = new LGraph();
...@@ -1328,7 +1341,7 @@ export class ComfyApp { ...@@ -1328,7 +1341,7 @@ export class ComfyApp {
const json = localStorage.getItem("workflow"); const json = localStorage.getItem("workflow");
if (json) { if (json) {
const workflow = JSON.parse(json); const workflow = JSON.parse(json);
this.loadGraphData(workflow); await this.loadGraphData(workflow);
restored = true; restored = true;
} }
} catch (err) { } catch (err) {
...@@ -1337,7 +1350,7 @@ export class ComfyApp { ...@@ -1337,7 +1350,7 @@ export class ComfyApp {
// We failed to restore a workflow so load the default // We failed to restore a workflow so load the default
if (!restored) { if (!restored) {
this.loadGraphData(); await this.loadGraphData();
} }
// Save current workflow automatically // Save current workflow automatically
...@@ -1345,7 +1358,6 @@ export class ComfyApp { ...@@ -1345,7 +1358,6 @@ export class ComfyApp {
this.#addDrawNodeHandler(); this.#addDrawNodeHandler();
this.#addDrawGroupsHandler(); this.#addDrawGroupsHandler();
this.#addApiUpdateHandlers();
this.#addDropHandler(); this.#addDropHandler();
this.#addCopyHandler(); this.#addCopyHandler();
this.#addPasteHandler(); this.#addPasteHandler();
...@@ -1365,11 +1377,81 @@ export class ComfyApp { ...@@ -1365,11 +1377,81 @@ export class ComfyApp {
await this.#invokeExtensionsAsync("registerCustomNodes"); 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) { async registerNodesFromDefs(defs) {
await this.#invokeExtensionsAsync("addCustomNodeDefs", defs); await this.#invokeExtensionsAsync("addCustomNodeDefs", defs);
// Generate list of known widgets // Generate list of known widgets
const widgets = Object.assign( this.widgets = Object.assign(
{}, {},
ComfyWidgets, ComfyWidgets,
...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean) ...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean)
...@@ -1377,75 +1459,7 @@ export class ComfyApp { ...@@ -1377,75 +1459,7 @@ export class ComfyApp {
// Register a node for each definition // Register a node for each definition
for (const nodeId in defs) { for (const nodeId in defs) {
const nodeData = defs[nodeId]; this.registerNodeDef(nodeId, 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;
} }
} }
...@@ -1488,9 +1502,14 @@ export class ComfyApp { ...@@ -1488,9 +1502,14 @@ export class ComfyApp {
showMissingNodesError(missingNodeTypes, hasAddedNodes = true) { showMissingNodesError(missingNodeTypes, hasAddedNodes = true) {
this.ui.dialog.show( this.ui.dialog.show(
`When loading the graph, the following node types were not found: <ul>${Array.from(new Set(missingNodeTypes)).map( $el("div", [
(t) => `<li>${t}</li>` $el("span", { textContent: "When loading the graph, the following node types were not found: " }),
).join("")}</ul>${hasAddedNodes ? "Nodes that have failed to load will show as red on the graph." : ""}` $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", { this.logging.addEntry("Comfy.App", "warn", {
MissingNodes: missingNodeTypes, MissingNodes: missingNodeTypes,
...@@ -1501,7 +1520,7 @@ export class ComfyApp { ...@@ -1501,7 +1520,7 @@ export class ComfyApp {
* Populates the graph with the specified workflow data * Populates the graph with the specified workflow data
* @param {*} graphData A serialized graph object * @param {*} graphData A serialized graph object
*/ */
loadGraphData(graphData) { async loadGraphData(graphData) {
this.clean(); this.clean();
let reset_invalid_values = false; let reset_invalid_values = false;
...@@ -1519,6 +1538,7 @@ export class ComfyApp { ...@@ -1519,6 +1538,7 @@ export class ComfyApp {
} }
const missingNodeTypes = []; const missingNodeTypes = [];
await this.#invokeExtensionsAsync("beforeConfigureGraph", graphData, missingNodeTypes);
for (let n of graphData.nodes) { for (let n of graphData.nodes) {
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader"; if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
...@@ -1527,8 +1547,8 @@ export class ComfyApp { ...@@ -1527,8 +1547,8 @@ export class ComfyApp {
// Find missing node types // Find missing node types
if (!(n.type in LiteGraph.registered_node_types)) { if (!(n.type in LiteGraph.registered_node_types)) {
n.type = sanitizeNodeName(n.type);
missingNodeTypes.push(n.type); missingNodeTypes.push(n.type);
n.type = sanitizeNodeName(n.type);
} }
} }
...@@ -1627,92 +1647,98 @@ export class ComfyApp { ...@@ -1627,92 +1647,98 @@ export class ComfyApp {
* @returns The workflow and node links * @returns The workflow and node links
*/ */
async graphToPrompt() { async graphToPrompt() {
for (const node of this.graph.computeExecutionOrder(false)) { for (const outerNode of this.graph.computeExecutionOrder(false)) {
if (node.isVirtualNode) { const innerNodes = outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode];
// Don't serialize frontend only nodes but let them make changes for (const node of innerNodes) {
if (node.applyToGraph) { if (node.isVirtualNode) {
node.applyToGraph(); // Don't serialize frontend only nodes but let them make changes
if (node.applyToGraph) {
node.applyToGraph();
}
} }
continue;
} }
} }
const workflow = this.graph.serialize(); const workflow = this.graph.serialize();
const output = {}; const output = {};
// Process nodes in order of execution // Process nodes in order of execution
for (const node of this.graph.computeExecutionOrder(false)) { for (const outerNode of this.graph.computeExecutionOrder(false)) {
const n = workflow.nodes.find((n) => n.id === node.id); const innerNodes = outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode];
for (const node of innerNodes) {
if (node.isVirtualNode) { if (node.isVirtualNode) {
continue; continue;
} }
if (node.mode === 2 || node.mode === 4) { if (node.mode === 2 || node.mode === 4) {
// Don't serialize muted nodes // Don't serialize muted nodes
continue; continue;
} }
const inputs = {}; const inputs = {};
const widgets = node.widgets; const widgets = node.widgets;
// Store all widget values // Store all widget values
if (widgets) { if (widgets) {
for (const i in widgets) { for (const i in widgets) {
const widget = widgets[i]; const widget = widgets[i];
if (!widget.options || widget.options.serialize !== false) { if (!widget.options || widget.options.serialize !== false) {
inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value; inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(node, i) : widget.value;
}
} }
} }
}
// Store all node links // Store all node links
for (let i in node.inputs) { for (let i in node.inputs) {
let parent = node.getInputNode(i); let parent = node.getInputNode(i);
if (parent) { if (parent) {
let link = node.getInputLink(i); let link = node.getInputLink(i);
while (parent.mode === 4 || parent.isVirtualNode) { while (parent.mode === 4 || parent.isVirtualNode) {
let found = false; let found = false;
if (parent.isVirtualNode) { if (parent.isVirtualNode) {
link = parent.getInputLink(link.origin_slot); link = parent.getInputLink(link.origin_slot);
if (link) { if (link) {
parent = parent.getInputNode(link.target_slot); parent = parent.getInputNode(link.target_slot);
if (parent) { if (parent) {
found = true; found = true;
}
} }
} } else if (link && parent.mode === 4) {
} else if (link && parent.mode === 4) { let all_inputs = [link.origin_slot];
let all_inputs = [link.origin_slot]; if (parent.inputs) {
if (parent.inputs) { all_inputs = all_inputs.concat(Object.keys(parent.inputs))
all_inputs = all_inputs.concat(Object.keys(parent.inputs)) for (let parent_input in all_inputs) {
for (let parent_input in all_inputs) { parent_input = all_inputs[parent_input];
parent_input = all_inputs[parent_input]; if (parent.inputs[parent_input]?.type === node.inputs[i].type) {
if (parent.inputs[parent_input]?.type === node.inputs[i].type) { link = parent.getInputLink(parent_input);
link = parent.getInputLink(parent_input); if (link) {
if (link) { parent = parent.getInputNode(parent_input);
parent = parent.getInputNode(parent_input); }
found = true;
break;
} }
found = true;
break;
} }
} }
} }
}
if (!found) { if (!found) {
break; break;
}
} }
}
if (link) { if (link) {
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)]; if (parent?.updateLink) {
link = parent.updateLink(link);
}
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
}
} }
} }
}
output[String(node.id)] = { output[String(node.id)] = {
inputs, inputs,
class_type: node.comfyClass, class_type: node.comfyClass,
}; };
}
} }
// Remove inputs connected to removed nodes // Remove inputs connected to removed nodes
...@@ -1832,7 +1858,7 @@ export class ComfyApp { ...@@ -1832,7 +1858,7 @@ export class ComfyApp {
const pngInfo = await getPngMetadata(file); const pngInfo = await getPngMetadata(file);
if (pngInfo) { if (pngInfo) {
if (pngInfo.workflow) { if (pngInfo.workflow) {
this.loadGraphData(JSON.parse(pngInfo.workflow)); await this.loadGraphData(JSON.parse(pngInfo.workflow));
} else if (pngInfo.parameters) { } else if (pngInfo.parameters) {
importA1111(this.graph, pngInfo.parameters); importA1111(this.graph, pngInfo.parameters);
} }
...@@ -1848,21 +1874,21 @@ export class ComfyApp { ...@@ -1848,21 +1874,21 @@ export class ComfyApp {
} }
} else if (file.type === "application/json" || file.name?.endsWith(".json")) { } else if (file.type === "application/json" || file.name?.endsWith(".json")) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = async () => {
const jsonContent = JSON.parse(reader.result); const jsonContent = JSON.parse(reader.result);
if (jsonContent?.templates) { if (jsonContent?.templates) {
this.loadTemplateData(jsonContent); this.loadTemplateData(jsonContent);
} else if(this.isApiJson(jsonContent)) { } else if(this.isApiJson(jsonContent)) {
this.loadApiJson(jsonContent); this.loadApiJson(jsonContent);
} else { } else {
this.loadGraphData(jsonContent); await this.loadGraphData(jsonContent);
} }
}; };
reader.readAsText(file); reader.readAsText(file);
} else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) { } else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) {
const info = await getLatentMetadata(file); const info = await getLatentMetadata(file);
if (info.workflow) { 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) { ...@@ -44,7 +44,7 @@ function getClipPath(node, element, elRect) {
} }
function computeSize(size) { 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 y = this.widgets[0].last_y;
let freeSpace = size[1] - y; let freeSpace = size[1] - y;
...@@ -195,7 +195,6 @@ export function addDomClippingSetting() { ...@@ -195,7 +195,6 @@ export function addDomClippingSetting() {
type: "boolean", type: "boolean",
defaultValue: enableDomClipping, defaultValue: enableDomClipping,
onChange(value) { onChange(value) {
console.log("enableDomClipping", enableDomClipping);
enableDomClipping = !!value; enableDomClipping = !!value;
}, },
}); });
......
...@@ -462,8 +462,8 @@ class ComfyList { ...@@ -462,8 +462,8 @@ class ComfyList {
return $el("div", {textContent: item.prompt[0] + ": "}, [ return $el("div", {textContent: item.prompt[0] + ": "}, [
$el("button", { $el("button", {
textContent: "Load", textContent: "Load",
onclick: () => { onclick: async () => {
app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); await app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
if (item.outputs) { if (item.outputs) {
app.nodeOutputs = item.outputs; app.nodeOutputs = item.outputs;
} }
...@@ -784,9 +784,9 @@ export class ComfyUI { ...@@ -784,9 +784,9 @@ export class ComfyUI {
} }
}), }),
$el("button", { $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?")) { if (!confirmClear.value || confirm("Load default workflow?")) {
app.loadGraphData() await app.loadGraphData()
} }
} }
}), }),
......
...@@ -23,29 +23,73 @@ function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) { ...@@ -23,29 +23,73 @@ function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) {
return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } }; return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } };
} }
export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values) { export function getWidgetType(inputData, inputName) {
const widgets = addValueControlWidgets(node, targetWidget, defaultValue, values, { 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, addFilterList: false,
}); controlAfterGenerateName: name
}, inputData);
return widgets[0]; 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 = {}; 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 widgets = [];
const valueControl = node.addWidget("combo", "control_after_generate", defaultValue, function (v) { }, { const valueControl = node.addWidget(
values: ["fixed", "increment", "decrement", "randomize"], "combo",
serialize: false, // Don't include this in prompt. getName("control_after_generate", "controlAfterGenerateName"),
}); defaultValue,
function () {},
{
values: ["fixed", "increment", "decrement", "randomize"],
serialize: false, // Don't include this in prompt.
}
);
widgets.push(valueControl); widgets.push(valueControl);
const isCombo = targetWidget.type === "combo"; const isCombo = targetWidget.type === "combo";
let comboFilter; let comboFilter;
if (isCombo && options.addFilterList !== false) { if (isCombo && options.addFilterList !== false) {
comboFilter = node.addWidget("string", "control_filter_list", "", function (v) {}, { comboFilter = node.addWidget(
serialize: false, // Don't include this in prompt. "string",
}); getName("control_filter_list", "controlFilterListName"),
"",
function () {},
{
serialize: false, // Don't include this in prompt.
}
);
widgets.push(comboFilter); widgets.push(comboFilter);
} }
...@@ -96,7 +140,8 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando ...@@ -96,7 +140,8 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
targetWidget.value = value; targetWidget.value = value;
targetWidget.callback(value); targetWidget.callback(value);
} }
} else { //number } else {
//number
let min = targetWidget.options.min; let min = targetWidget.options.min;
let max = targetWidget.options.max; let max = targetWidget.options.max;
// limit to something that javascript can handle // limit to something that javascript can handle
...@@ -119,32 +164,54 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando ...@@ -119,32 +164,54 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
default: default:
break; break;
} }
/*check if values are over or under their respective /*check if values are over or under their respective
* ranges and set them to min or max.*/ * ranges and set them to min or max.*/
if (targetWidget.value < min) if (targetWidget.value < min) targetWidget.value = min;
targetWidget.value = min;
if (targetWidget.value > max) if (targetWidget.value > max)
targetWidget.value = max; targetWidget.value = max;
targetWidget.callback(targetWidget.value); targetWidget.callback(targetWidget.value);
} }
} };
return widgets; return widgets;
}; };
function seedWidget(node, inputName, inputData, app) { function seedWidget(node, inputName, inputData, app, widgetName) {
const seed = ComfyWidgets.INT(node, inputName, inputData, app); const seed = createIntWidget(node, inputName, inputData, app, true);
const seedControl = addValueControlWidget(node, seed.widget, "randomize"); const seedControl = addValueControlWidget(node, seed.widget, "randomize", undefined, widgetName, inputData);
seed.widget.linkedWidgets = [seedControl]; seed.widget.linkedWidgets = [seedControl];
return seed; 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) { function addMultilineWidget(node, name, opts, app) {
const inputEl = document.createElement("textarea"); const inputEl = document.createElement("textarea");
inputEl.className = "comfy-multiline-input"; inputEl.className = "comfy-multiline-input";
inputEl.value = opts.defaultVal; inputEl.value = opts.defaultVal;
inputEl.placeholder = opts.placeholder || ""; inputEl.placeholder = opts.placeholder || name;
const widget = node.addDOMWidget(name, "customtext", inputEl, { const widget = node.addDOMWidget(name, "customtext", inputEl, {
getValue() { getValue() {
...@@ -156,6 +223,10 @@ function addMultilineWidget(node, name, opts, app) { ...@@ -156,6 +223,10 @@ function addMultilineWidget(node, name, opts, app) {
}); });
widget.inputEl = inputEl; widget.inputEl = inputEl;
inputEl.addEventListener("input", () => {
widget.callback?.(widget.value);
});
return { minWidth: 400, minHeight: 200, widget }; return { minWidth: 400, minHeight: 200, widget };
} }
...@@ -186,21 +257,7 @@ export const ComfyWidgets = { ...@@ -186,21 +257,7 @@ export const ComfyWidgets = {
}, config) }; }, config) };
}, },
INT(node, inputName, inputData, app) { INT(node, inputName, inputData, app) {
let widgetType = isSlider(inputData[1]["display"], app); return createIntWidget(node, inputName, inputData, 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
),
};
}, },
BOOLEAN(node, inputName, inputData) { BOOLEAN(node, inputName, inputData) {
let defaultVal = false; let defaultVal = false;
...@@ -245,10 +302,14 @@ export const ComfyWidgets = { ...@@ -245,10 +302,14 @@ export const ComfyWidgets = {
if (inputData[1] && inputData[1].default) { if (inputData[1] && inputData[1].default) {
defaultValue = 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) { 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; let uploadWidget;
function showImage(name) { function showImage(name) {
...@@ -362,9 +423,10 @@ export const ComfyWidgets = { ...@@ -362,9 +423,10 @@ export const ComfyWidgets = {
document.body.append(fileInput); document.body.append(fileInput);
// Create the button widget for selecting the files // 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(); fileInput.click();
}); });
uploadWidget.label = "choose file to upload";
uploadWidget.serialize = false; uploadWidget.serialize = false;
// Add handler to check if an image is being dragged over our node // Add handler to check if an image is being dragged over our node
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment