ComfyUI/web/scripts/app.js

2461 lines
67 KiB
JavaScript
Raw Normal View History

2023-08-04 07:30:01 +00:00
import { ComfyLogging } from "./logging.js";
import { ComfyWidgets, initWidgets } from "./widgets.js";
import { ComfyUI, $el } from "./ui.js";
import { api } from "./api.js";
import { defaultGraph } from "./defaultGraph.js";
import { getPngMetadata, getWebpMetadata, getFlacMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
import { addDomClippingSetting } from "./domWidget.js";
import { createImageHost, calculateImageGrid } from "./ui/imagePreview.js";
import { ComfyAppMenu } from "./ui/menu/index.js";
import { getStorageValue, setStorageValue } from "./utils.js";
import { ComfyWorkflowManager } from "./workflows.js";
export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview";
function sanitizeNodeName(string) {
let entityMap = {
'&': '',
'<': '',
'>': '',
'"': '',
"'": '',
'`': '',
'=': ''
};
return String(string).replace(/[&<>"'`=]/g, function fromEntityMap (s) {
return entityMap[s];
});
}
/**
2023-04-15 20:40:39 +00:00
* @typedef {import("types/comfy").ComfyExtension} ComfyExtension
*/
export class ComfyApp {
/**
* List of entries to queue
* @type {{number: number, batchCount: number}[]}
2023-04-01 17:46:05 +00:00
*/
#queueItems = [];
/**
* If the queue is currently being processed
2023-04-15 20:40:39 +00:00
* @type {boolean}
2023-04-01 17:46:05 +00:00
*/
#processingQueue = false;
/**
* Content Clipboard
* @type {serialized node object}
*/
static clipspace = null;
Clipspace Menu and MaskEditor application. (#548) * Add clipspace feature. * feat: copy content to clipspace * feat: paste content from clipspace Extend validation to allow for validating annotated_path in addition to other parameters. Add support for annotated_filepath in folder_paths function. Generalize the '/upload/image' API to allow for uploading images to the 'input', 'temp', or 'output' directories. * rename contentClipboard -> clipspace * Do deep copy for imgs on copy to clipspace. * mask painting on clipspace * add original_imgs into clipspace * Preserve the original image when 'imgs' are modified * robust patch & refactoring folder_paths about annotated_filepath * wip * Only show the Paste menu if the ComfyApp.clipspace is not empty * clipspace feature added maskeditor feature added * instant refresh on paste force triggering 'changed' on paste action * enhance mask painting smooth drawing add brush_size +/- button * robust patch use mouseup event * robust patch again... * subfolder fix on paste logic attach subfolder if subfolder isn't empty * event listener patch add ], [ key event for brush size remove listener on close * Fix button positioning issue related to window height. Change brush size from button to slider. * clean commit * clean code * various bug fixes * paste action - prevent opening upload popup - ensure rendering after widget_value update * view api update - support annotated_filepath * maskeditor layout - prevent covering button by hidden div * remove dbg message * Add cursor functionality to display brush size * refactor: Replace brush preview feature with missionfloyd implementation * missionfloyd implementation * hiding brush preview off the canvas * change brush size on wheel event * keyup -> keydown event * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * Add support for channel-specific image data retrieval in /view API to fix alpha mask loading issue When loading an image with an alpha mask in JavaScript canvas, there is an issue where the alpha and RGB channels are premultiplied. To avoid reliance on JavaScript canvas, I added support for channel-specific image data retrieval in the "/view" API. This allows us to retrieve data for each channel separately and fix the alpha mask loading issue. The changes have been committed to the repository. * Enable brush preview for key and slider events * optimize * preview fix * robust patch * fix copy (clipspace) action imgs[0] copy -> whole imgs copy * support batch images on clipspace, maskeditor * copy/paste bug fixes for batch images enhance selector preview on clipspace menu add img_paste_mode option into clipspace menu * crash fix * print message if clipspace content cannot editable * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * make default img_paste_mode to 'selected' refactor space -> tab * save clipspace files to input/clipspace instead of temp * show "clipspace/filename.png" instead of 'filename.png [clipspace]' in LoadImage/LoadImageMask * refresh fix related to FILE_COMBO * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * adjust margin based on missionfloyd impelements * mouse event -> pointer event * pen, touch, mouse drawing patched and tested * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * add comment about touch event. --------- Co-authored-by: Lt.Dr.Data <lt.dr.data@gmail.com> Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com>
2023-05-08 18:37:36 +00:00
static clipspace_invalidate_handler = null;
static open_maskeditor = null;
static clipspace_return_node = null;
constructor() {
this.ui = new ComfyUI(this);
2023-08-04 07:30:01 +00:00
this.logging = new ComfyLogging(this);
this.workflowManager = new ComfyWorkflowManager(this);
this.bodyTop = $el("div.comfyui-body-top", { parent: document.body });
this.bodyLeft = $el("div.comfyui-body-left", { parent: document.body });
this.bodyRight = $el("div.comfyui-body-right", { parent: document.body });
this.bodyBottom = $el("div.comfyui-body-bottom", { parent: document.body });
this.menu = new ComfyAppMenu(this);
2023-04-15 20:40:39 +00:00
/**
* List of extensions that are registered with the app
* @type {ComfyExtension[]}
*/
2023-03-03 18:28:34 +00:00
this.extensions = [];
2023-04-15 20:40:39 +00:00
/**
* Stores the execution output data for each node
* @type {Record<string, any>}
*/
this._nodeOutputs = {};
2023-04-15 20:40:39 +00:00
2023-05-31 01:43:29 +00:00
/**
* Stores the preview image data for each node
* @type {Record<string, Image>}
*/
this.nodePreviewImages = {};
2023-04-15 20:40:39 +00:00
/**
* If the shift key on the keyboard is pressed
* @type {boolean}
*/
2023-04-02 14:33:34 +00:00
this.shiftDown = false;
}
get nodeOutputs() {
return this._nodeOutputs;
}
set nodeOutputs(value) {
this._nodeOutputs = value;
this.#invokeExtensions("onNodeOutputsUpdated", value);
}
getPreviewFormatParam() {
let preview_format = this.ui.settings.getSettingValue("Comfy.PreviewFormat");
if(preview_format)
return `&preview=${preview_format}`;
else
return "";
}
2023-11-11 18:56:14 +00:00
getRandParam() {
return "&rand=" + Math.random();
}
static isImageNode(node) {
return node.imgs || (node && node.widgets && node.widgets.findIndex(obj => obj.name === 'image') >= 0);
}
static onClipspaceEditorSave() {
if(ComfyApp.clipspace_return_node) {
ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node);
}
}
static onClipspaceEditorClosed() {
ComfyApp.clipspace_return_node = null;
}
static copyToClipspace(node) {
var widgets = null;
if(node.widgets) {
widgets = node.widgets.map(({ type, name, value }) => ({ type, name, value }));
}
var imgs = undefined;
var orig_imgs = undefined;
if(node.imgs != undefined) {
imgs = [];
orig_imgs = [];
for (let i = 0; i < node.imgs.length; i++) {
imgs[i] = new Image();
imgs[i].src = node.imgs[i].src;
orig_imgs[i] = imgs[i];
}
}
var selectedIndex = 0;
if(node.imageIndex) {
selectedIndex = node.imageIndex;
}
ComfyApp.clipspace = {
'widgets': widgets,
'imgs': imgs,
'original_imgs': orig_imgs,
'images': node.images,
'selectedIndex': selectedIndex,
'img_paste_mode': 'selected' // reset to default im_paste_mode state on copy action
};
ComfyApp.clipspace_return_node = null;
if(ComfyApp.clipspace_invalidate_handler) {
ComfyApp.clipspace_invalidate_handler();
}
}
static pasteFromClipspace(node) {
if(ComfyApp.clipspace) {
// image paste
if(ComfyApp.clipspace.imgs && node.imgs) {
if(node.images && ComfyApp.clipspace.images) {
if(ComfyApp.clipspace['img_paste_mode'] == 'selected') {
2023-06-07 13:56:08 +00:00
node.images = [ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']]];
}
2023-06-07 13:56:08 +00:00
else {
node.images = ComfyApp.clipspace.images;
}
if(app.nodeOutputs[node.id + ""])
app.nodeOutputs[node.id + ""].images = node.images;
}
if(ComfyApp.clipspace.imgs) {
// deep-copy to cut link with clipspace
if(ComfyApp.clipspace['img_paste_mode'] == 'selected') {
const img = new Image();
img.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
node.imgs = [img];
node.imageIndex = 0;
}
else {
const imgs = [];
for(let i=0; i<ComfyApp.clipspace.imgs.length; i++) {
imgs[i] = new Image();
imgs[i].src = ComfyApp.clipspace.imgs[i].src;
node.imgs = imgs;
}
}
}
}
if(node.widgets) {
if(ComfyApp.clipspace.images) {
const clip_image = ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']];
const index = node.widgets.findIndex(obj => obj.name === 'image');
if(index >= 0) {
if(node.widgets[index].type != 'image' && typeof node.widgets[index].value == "string" && clip_image.filename) {
node.widgets[index].value = (clip_image.subfolder?clip_image.subfolder+'/':'') + clip_image.filename + (clip_image.type?` [${clip_image.type}]`:'');
}
else {
node.widgets[index].value = clip_image;
}
}
}
if(ComfyApp.clipspace.widgets) {
ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => {
const prop = Object.values(node.widgets).find(obj => obj.type === type && obj.name === name);
if (prop && prop.type != 'button') {
if(prop.type != 'image' && typeof prop.value == "string" && value.filename) {
2023-06-07 15:06:56 +00:00
prop.value = (value.subfolder?value.subfolder+'/':'') + value.filename + (value.type?` [${value.type}]`:'');
}
else {
prop.value = value;
prop.callback(value);
}
}
});
}
}
app.graph.setDirtyCanvas(true);
}
}
2023-03-03 15:47:33 +00:00
/**
* Invoke an extension callback
2023-04-15 20:40:39 +00:00
* @param {keyof ComfyExtension} method The extension callback to execute
* @param {any[]} args Any arguments to pass to the callback
2023-03-03 18:28:34 +00:00
* @returns
2023-03-03 15:47:33 +00:00
*/
#invokeExtensions(method, ...args) {
let results = [];
for (const ext of this.extensions) {
if (method in ext) {
try {
results.push(ext[method](...args, this));
} catch (error) {
2023-03-03 19:05:39 +00:00
console.error(
`Error calling extension '${ext.name}' method '${method}'`,
{ error },
{ extension: ext },
{ args }
);
}
}
}
return results;
}
2023-03-03 15:47:33 +00:00
/**
* Invoke an async extension callback
* Each callback will be invoked concurrently
* @param {string} method The extension callback to execute
* @param {...any} args Any arguments to pass to the callback
2023-03-03 18:28:34 +00:00
* @returns
2023-03-03 15:47:33 +00:00
*/
async #invokeExtensionsAsync(method, ...args) {
return await Promise.all(
this.extensions.map(async (ext) => {
if (method in ext) {
try {
return await ext[method](...args, this);
} catch (error) {
2023-03-03 19:05:39 +00:00
console.error(
`Error calling extension '${ext.name}' method '${method}'`,
{ error },
{ extension: ext },
{ args }
);
}
}
})
);
}
#addRestoreWorkflowView() {
const serialize = LGraph.prototype.serialize;
const self = this;
LGraph.prototype.serialize = function() {
const workflow = serialize.apply(this, arguments);
// Store the drag & scale info in the serialized workflow if the setting is enabled
if (self.enableWorkflowViewRestore.value) {
if (!workflow.extra) {
workflow.extra = {};
}
workflow.extra.ds = {
scale: self.canvas.ds.scale,
offset: self.canvas.ds.offset,
};
} else if (workflow.extra?.ds) {
// Clear any old view data
delete workflow.extra.ds;
}
return workflow;
}
this.enableWorkflowViewRestore = this.ui.settings.addSetting({
id: "Comfy.EnableWorkflowViewRestore",
name: "Save and restore canvas position and zoom level in workflows",
type: "boolean",
defaultValue: true
});
}
2023-03-03 15:47:33 +00:00
/**
* Adds special context menu handling for nodes
* e.g. this adds Open Image functionality for nodes that show images
* @param {*} node The node to add the menu handler
*/
#addNodeContextMenuHandler(node) {
function getCopyImageOption(img) {
if (typeof window.ClipboardItem === "undefined") return [];
return [
{
content: "Copy Image",
callback: async () => {
const url = new URL(img.src);
url.searchParams.delete("preview");
const writeImage = async (blob) => {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
};
try {
const data = await fetch(url);
const blob = await data.blob();
try {
await writeImage(blob);
} catch (error) {
// Chrome seems to only support PNG on write, convert and try again
if (blob.type !== "image/png") {
const canvas = $el("canvas", {
width: img.naturalWidth,
height: img.naturalHeight,
});
const ctx = canvas.getContext("2d");
let image;
if (typeof window.createImageBitmap === "undefined") {
image = new Image();
const p = new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
}).finally(() => {
URL.revokeObjectURL(image.src);
});
image.src = URL.createObjectURL(blob);
await p;
} else {
image = await createImageBitmap(blob);
}
try {
ctx.drawImage(image, 0, 0);
canvas.toBlob(writeImage, "image/png");
} finally {
if (typeof image.close === "function") {
image.close();
}
}
return;
}
throw error;
}
} catch (error) {
alert("Error copying image: " + (error.message ?? error));
}
},
},
];
}
node.prototype.getExtraMenuOptions = function (_, options) {
if (this.imgs) {
// If this node has images then we add an open in new tab item
let img;
if (this.imageIndex != null) {
// An image is selected so select that
img = this.imgs[this.imageIndex];
} else if (this.overIndex != null) {
// No image is selected but one is hovered
img = this.imgs[this.overIndex];
}
if (img) {
2023-03-21 08:00:13 +00:00
options.unshift(
{
content: "Open Image",
callback: () => {
let url = new URL(img.src);
url.searchParams.delete("preview");
window.open(url, "_blank");
},
2023-03-21 08:00:13 +00:00
},
...getCopyImageOption(img),
2023-03-21 08:00:13 +00:00
{
content: "Save Image",
callback: () => {
const a = document.createElement("a");
let url = new URL(img.src);
url.searchParams.delete("preview");
a.href = url;
a.setAttribute("download", new URLSearchParams(url.search).get("filename"));
2023-03-21 08:00:13 +00:00
document.body.append(a);
a.click();
requestAnimationFrame(() => a.remove());
},
}
);
}
}
options.push({
content: "Bypass",
callback: (obj) => {
if (this.mode === 4) this.mode = 0;
else this.mode = 4;
this.graph.change();
},
});
// prevent conflict of clipspace content
if (!ComfyApp.clipspace_return_node) {
options.push({
content: "Copy (Clipspace)",
callback: (obj) => {
ComfyApp.copyToClipspace(this);
},
});
if (ComfyApp.clipspace != null) {
options.push({
content: "Paste (Clipspace)",
callback: () => {
ComfyApp.pasteFromClipspace(this);
},
});
}
Clipspace Menu and MaskEditor application. (#548) * Add clipspace feature. * feat: copy content to clipspace * feat: paste content from clipspace Extend validation to allow for validating annotated_path in addition to other parameters. Add support for annotated_filepath in folder_paths function. Generalize the '/upload/image' API to allow for uploading images to the 'input', 'temp', or 'output' directories. * rename contentClipboard -> clipspace * Do deep copy for imgs on copy to clipspace. * mask painting on clipspace * add original_imgs into clipspace * Preserve the original image when 'imgs' are modified * robust patch & refactoring folder_paths about annotated_filepath * wip * Only show the Paste menu if the ComfyApp.clipspace is not empty * clipspace feature added maskeditor feature added * instant refresh on paste force triggering 'changed' on paste action * enhance mask painting smooth drawing add brush_size +/- button * robust patch use mouseup event * robust patch again... * subfolder fix on paste logic attach subfolder if subfolder isn't empty * event listener patch add ], [ key event for brush size remove listener on close * Fix button positioning issue related to window height. Change brush size from button to slider. * clean commit * clean code * various bug fixes * paste action - prevent opening upload popup - ensure rendering after widget_value update * view api update - support annotated_filepath * maskeditor layout - prevent covering button by hidden div * remove dbg message * Add cursor functionality to display brush size * refactor: Replace brush preview feature with missionfloyd implementation * missionfloyd implementation * hiding brush preview off the canvas * change brush size on wheel event * keyup -> keydown event * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * Add support for channel-specific image data retrieval in /view API to fix alpha mask loading issue When loading an image with an alpha mask in JavaScript canvas, there is an issue where the alpha and RGB channels are premultiplied. To avoid reliance on JavaScript canvas, I added support for channel-specific image data retrieval in the "/view" API. This allows us to retrieve data for each channel separately and fix the alpha mask loading issue. The changes have been committed to the repository. * Enable brush preview for key and slider events * optimize * preview fix * robust patch * fix copy (clipspace) action imgs[0] copy -> whole imgs copy * support batch images on clipspace, maskeditor * copy/paste bug fixes for batch images enhance selector preview on clipspace menu add img_paste_mode option into clipspace menu * crash fix * print message if clipspace content cannot editable * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * make default img_paste_mode to 'selected' refactor space -> tab * save clipspace files to input/clipspace instead of temp * show "clipspace/filename.png" instead of 'filename.png [clipspace]' in LoadImage/LoadImageMask * refresh fix related to FILE_COMBO * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * adjust margin based on missionfloyd impelements * mouse event -> pointer event * pen, touch, mouse drawing patched and tested * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * add comment about touch event. --------- Co-authored-by: Lt.Dr.Data <lt.dr.data@gmail.com> Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com>
2023-05-08 18:37:36 +00:00
if (ComfyApp.isImageNode(this)) {
options.push({
content: "Open in MaskEditor",
callback: (obj) => {
ComfyApp.copyToClipspace(this);
ComfyApp.clipspace_return_node = this;
ComfyApp.open_maskeditor();
},
});
}
}
};
}
#addNodeKeyHandler(node) {
const app = this;
const origNodeOnKeyDown = node.prototype.onKeyDown;
node.prototype.onKeyDown = function(e) {
if (origNodeOnKeyDown && origNodeOnKeyDown.apply(this, e) === false) {
return false;
}
if (this.flags.collapsed || !this.imgs || this.imageIndex === null) {
return;
}
let handled = false;
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
if (e.key === "ArrowLeft") {
this.imageIndex -= 1;
} else if (e.key === "ArrowRight") {
this.imageIndex += 1;
}
this.imageIndex %= this.imgs.length;
if (this.imageIndex < 0) {
this.imageIndex = this.imgs.length + this.imageIndex;
}
handled = true;
} else if (e.key === "Escape") {
this.imageIndex = null;
handled = true;
}
if (handled === true) {
e.preventDefault();
e.stopImmediatePropagation();
return false;
}
}
}
2023-03-03 15:47:33 +00:00
/**
* Adds Custom drawing logic for nodes
* e.g. Draws images and handles thumbnail navigation on nodes that output images
* @param {*} node The node to add the draw handler
*/
#addDrawBackgroundHandler(node) {
const app = this;
function getImageTop(node) {
let shiftY;
if (node.imageOffset != null) {
shiftY = node.imageOffset;
} else {
if (node.widgets?.length) {
const w = node.widgets[node.widgets.length - 1];
shiftY = w.last_y;
if (w.computeSize) {
shiftY += w.computeSize()[1] + 4;
}
else if(w.computedHeight) {
shiftY += w.computedHeight;
}
else {
shiftY += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
} else {
shiftY = node.computeSize()[1];
}
}
return shiftY;
}
node.prototype.setSizeForImage = function (force) {
if(!force && this.animatedImages) return;
if (this.inputHeight || this.freeWidgetSpace > 210) {
this.setSize(this.size);
return;
}
const minHeight = getImageTop(this) + 220;
if (this.size[1] < minHeight) {
this.setSize([this.size[0], minHeight]);
}
};
node.prototype.onDrawBackground = function (ctx) {
if (!this.flags.collapsed) {
2023-05-31 01:43:29 +00:00
let imgURLs = []
let imagesChanged = false
const output = app.nodeOutputs[this.id + ""];
if (output?.images) {
this.animatedImages = output?.animated?.find(Boolean);
if (this.images !== output.images) {
this.images = output.images;
2023-05-31 01:43:29 +00:00
imagesChanged = true;
imgURLs = imgURLs.concat(
output.images.map((params) => {
return api.apiURL(
"/view?" +
new URLSearchParams(params).toString() +
2023-12-02 08:16:21 +00:00
(this.animatedImages ? "" : app.getPreviewFormatParam()) + app.getRandParam()
);
})
);
2023-05-31 01:43:29 +00:00
}
}
const preview = app.nodePreviewImages[this.id + ""]
if (this.preview !== preview) {
this.preview = preview
imagesChanged = true;
if (preview != null) {
imgURLs.push(preview);
}
}
if (imagesChanged) {
this.imageIndex = null;
if (imgURLs.length > 0) {
Promise.all(
2023-05-31 01:43:29 +00:00
imgURLs.map((src) => {
return new Promise((r) => {
const img = new Image();
img.onload = () => r(img);
2023-03-20 18:55:28 +00:00
img.onerror = () => r(null);
2023-05-31 01:43:29 +00:00
img.src = src
});
})
).then((imgs) => {
2023-05-31 01:43:29 +00:00
if ((!output || this.images === output.images) && (!preview || this.preview === preview)) {
this.imgs = imgs.filter(Boolean);
this.setSizeForImage?.();
app.graph.setDirtyCanvas(true);
}
});
}
2023-05-31 01:43:29 +00:00
else {
this.imgs = null;
}
2023-03-06 15:50:29 +00:00
}
function calculateGrid(w, h, n) {
let columns, rows, cellsize;
if (w > h) {
cellsize = h;
columns = Math.ceil(w / cellsize);
rows = Math.ceil(n / columns);
} else {
cellsize = w;
rows = Math.ceil(h / cellsize);
columns = Math.ceil(n / rows);
}
while (columns * rows < n) {
cellsize++;
if (w >= h) {
columns = Math.ceil(w / cellsize);
rows = Math.ceil(n / columns);
} else {
rows = Math.ceil(h / cellsize);
columns = Math.ceil(n / rows);
}
}
const cell_size = Math.min(w/columns, h/rows);
return {cell_size, columns, rows};
}
function is_all_same_aspect_ratio(imgs) {
// assume: imgs.length >= 2
let ratio = imgs[0].naturalWidth/imgs[0].naturalHeight;
for(let i=1; i<imgs.length; i++) {
let this_ratio = imgs[i].naturalWidth/imgs[i].naturalHeight;
if(ratio != this_ratio)
return false;
}
return true;
}
if (this.imgs?.length) {
const widgetIdx = this.widgets?.findIndex((w) => w.name === ANIM_PREVIEW_WIDGET);
if(this.animatedImages) {
// Instead of using the canvas we'll use a IMG
if(widgetIdx > -1) {
// Replace content
const widget = this.widgets[widgetIdx];
widget.options.host.updateImages(this.imgs);
} else {
const host = createImageHost(this);
this.setSizeForImage(true);
const widget = this.addDOMWidget(ANIM_PREVIEW_WIDGET, "img", host.el, {
host,
getHeight: host.getHeight,
onDraw: host.onDraw,
hideOnZoom: false
});
widget.serializeValue = () => undefined;
widget.options.host.updateImages(this.imgs);
}
return;
}
if (widgetIdx > -1) {
2023-11-23 19:43:55 +00:00
this.widgets[widgetIdx].onRemove?.();
this.widgets.splice(widgetIdx, 1);
}
const canvas = app.graph.list_of_graphcanvas[0];
2023-03-06 15:50:29 +00:00
const mouse = canvas.graph_mouse;
if (!canvas.pointer_is_down && this.pointerDown) {
if (mouse[0] === this.pointerDown.pos[0] && mouse[1] === this.pointerDown.pos[1]) {
this.imageIndex = this.pointerDown.index;
}
2023-03-06 15:50:29 +00:00
this.pointerDown = null;
}
2023-03-06 15:50:29 +00:00
let imageIndex = this.imageIndex;
const numImages = this.imgs.length;
if (numImages === 1 && !imageIndex) {
this.imageIndex = imageIndex = 0;
}
2023-03-14 19:39:49 +00:00
const top = getImageTop(this);
var shiftY = top;
2023-03-14 19:39:49 +00:00
2023-03-06 15:50:29 +00:00
let dw = this.size[0];
let dh = this.size[1];
dh -= shiftY;
2023-03-06 15:50:29 +00:00
if (imageIndex == null) {
var cellWidth, cellHeight, shiftX, cell_padding, cols;
const compact_mode = is_all_same_aspect_ratio(this.imgs);
if(!compact_mode) {
// use rectangle cell style and border line
cell_padding = 2;
const { cell_size, columns, rows } = calculateGrid(dw, dh, numImages);
cols = columns;
cellWidth = cell_size;
cellHeight = cell_size;
shiftX = (dw-cell_size*cols)/2;
shiftY = (dh-cell_size*rows)/2 + top;
}
else {
cell_padding = 0;
({ cellWidth, cellHeight, cols, shiftX } = calculateImageGrid(this.imgs, dw, dh));
2023-03-06 15:50:29 +00:00
}
2023-03-06 15:50:29 +00:00
let anyHovered = false;
this.imageRects = [];
for (let i = 0; i < numImages; i++) {
const img = this.imgs[i];
const row = Math.floor(i / cols);
const col = i % cols;
const x = col * cellWidth + shiftX;
const y = row * cellHeight + shiftY;
if (!anyHovered) {
2023-03-06 15:50:29 +00:00
anyHovered = LiteGraph.isInsideRectangle(
mouse[0],
mouse[1],
x + this.pos[0],
y + this.pos[1],
2023-03-06 15:50:29 +00:00
cellWidth,
cellHeight
);
2023-03-06 15:50:29 +00:00
if (anyHovered) {
this.overIndex = i;
let value = 110;
if (canvas.pointer_is_down) {
2023-03-06 15:50:29 +00:00
if (!this.pointerDown || this.pointerDown.index !== i) {
this.pointerDown = { index: i, pos: [...mouse] };
}
value = 125;
}
2023-03-06 15:50:29 +00:00
ctx.filter = `contrast(${value}%) brightness(${value}%)`;
canvas.canvas.style.cursor = "pointer";
}
}
this.imageRects.push([x, y, cellWidth, cellHeight]);
2023-09-16 11:37:27 +00:00
let wratio = cellWidth/img.width;
let hratio = cellHeight/img.height;
var ratio = Math.min(wratio, hratio);
let imgHeight = ratio * img.height;
let imgY = row * cellHeight + shiftY + (cellHeight - imgHeight)/2;
let imgWidth = ratio * img.width;
let imgX = col * cellWidth + shiftX + (cellWidth - imgWidth)/2;
ctx.drawImage(img, imgX+cell_padding, imgY+cell_padding, imgWidth-cell_padding*2, imgHeight-cell_padding*2);
if(!compact_mode) {
// rectangle cell and border line style
ctx.strokeStyle = "#8F8F8F";
ctx.lineWidth = 1;
ctx.strokeRect(x+cell_padding, y+cell_padding, cellWidth-cell_padding*2, cellHeight-cell_padding*2);
}
2023-03-06 15:50:29 +00:00
ctx.filter = "none";
}
if (!anyHovered) {
this.pointerDown = null;
this.overIndex = null;
}
} else {
// Draw individual
let w = this.imgs[imageIndex].naturalWidth;
let h = this.imgs[imageIndex].naturalHeight;
2023-03-06 15:50:29 +00:00
const scaleX = dw / w;
const scaleY = dh / h;
const scale = Math.min(scaleX, scaleY, 1);
w *= scale;
h *= scale;
let x = (dw - w) / 2;
let y = (dh - h) / 2 + shiftY;
ctx.drawImage(this.imgs[imageIndex], x, y, w, h);
const drawButton = (x, y, sz, text) => {
const hovered = LiteGraph.isInsideRectangle(mouse[0], mouse[1], x + this.pos[0], y + this.pos[1], sz, sz);
let fill = "#333";
let textFill = "#fff";
let isClicking = false;
if (hovered) {
canvas.canvas.style.cursor = "pointer";
if (canvas.pointer_is_down) {
fill = "#1e90ff";
isClicking = true;
} else {
2023-03-06 15:50:29 +00:00
fill = "#eee";
textFill = "#000";
}
2023-03-06 15:50:29 +00:00
} else {
this.pointerWasDown = null;
}
2023-03-06 15:50:29 +00:00
ctx.fillStyle = fill;
ctx.beginPath();
ctx.roundRect(x, y, sz, sz, [4]);
ctx.fill();
ctx.fillStyle = textFill;
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText(text, x + 15, y + 20);
2023-03-06 15:50:29 +00:00
return isClicking;
};
2023-03-06 15:50:29 +00:00
if (numImages > 1) {
if (drawButton(dw - 40, dh + top - 40, 30, `${this.imageIndex + 1}/${numImages}`)) {
2023-03-06 15:50:29 +00:00
let i = this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1;
if (!this.pointerDown || !this.pointerDown.index === i) {
this.pointerDown = { index: i, pos: [...mouse] };
}
2023-03-06 15:50:29 +00:00
}
if (drawButton(dw - 40, top + 10, 30, `x`)) {
2023-03-06 15:50:29 +00:00
if (!this.pointerDown || !this.pointerDown.index === null) {
this.pointerDown = { index: null, pos: [...mouse] };
}
}
}
}
}
}
};
}
2023-03-03 15:47:33 +00:00
/**
* Adds a handler allowing drag+drop of files onto the window to load workflows
*/
#addDropHandler() {
// Get prompt from dropped PNG or json
document.addEventListener("drop", async (event) => {
event.preventDefault();
event.stopPropagation();
const n = this.dragOverNode;
this.dragOverNode = null;
// Node handles file drop, we dont use the built in onDropFile handler as its buggy
// If you drag multiple files it will call it multiple times with the same file
if (n && n.onDragDrop && (await n.onDragDrop(event))) {
return;
}
// Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that
if (event.dataTransfer.files.length && event.dataTransfer.files[0].type !== "image/bmp") {
await this.handleFile(event.dataTransfer.files[0]);
} else {
// Try loading the first URI in the transfer list
const validTypes = ["text/uri-list", "text/x-moz-url"];
const match = [...event.dataTransfer.types].find((t) => validTypes.find(v => t === v));
if (match) {
const uri = event.dataTransfer.getData(match)?.split("\n")?.[0];
if (uri) {
await this.handleFile(await (await fetch(uri)).blob());
}
}
}
2023-03-03 15:20:49 +00:00
});
// Always clear over node on drag leave
this.canvasEl.addEventListener("dragleave", async () => {
if (this.dragOverNode) {
this.dragOverNode = null;
this.graph.setDirtyCanvas(false, true);
}
2023-03-03 15:20:49 +00:00
});
// Add handler for dropping onto a specific node
this.canvasEl.addEventListener(
"dragover",
(e) => {
this.canvas.adjustMouseEvent(e);
const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY);
if (node) {
if (node.onDragOver && node.onDragOver(e)) {
this.dragOverNode = node;
2023-03-14 21:25:52 +00:00
// dragover event is fired very frequently, run this on an animation frame
requestAnimationFrame(() => {
this.graph.setDirtyCanvas(false, true);
});
return;
}
}
this.dragOverNode = null;
},
false
);
2023-03-03 15:20:49 +00:00
}
2023-03-03 15:47:33 +00:00
/**
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
2023-03-03 15:47:33 +00:00
*/
2023-03-03 15:20:49 +00:00
#addPasteHandler() {
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
2023-11-30 19:13:27 +00:00
document.addEventListener("paste", async (e) => {
// ctrl+shift+v is used to paste nodes with connections
// this is handled by litegraph
if(this.shiftDown) return;
let data = (e.clipboardData || window.clipboardData);
const items = data.items;
// Look for image paste data
for (const item of items) {
if (item.type.startsWith('image/')) {
var imageNode = null;
// If an image node is selected, paste into it
if (this.canvas.current_node &&
this.canvas.current_node.is_selected &&
ComfyApp.isImageNode(this.canvas.current_node)) {
imageNode = this.canvas.current_node;
}
// No image node selected: add a new one
if (!imageNode) {
const newNode = LiteGraph.createNode("LoadImage");
newNode.pos = [...this.canvas.graph_mouse];
imageNode = this.graph.add(newNode);
this.graph.change();
}
const blob = item.getAsFile();
imageNode.pasteFile(blob);
return;
}
}
// No image found. Look for node data
data = data.getData("text/plain");
2023-03-03 15:20:49 +00:00
let workflow;
try {
data = data.slice(data.indexOf("{"));
workflow = JSON.parse(data);
} catch (err) {
try {
data = data.slice(data.indexOf("workflow\n"));
data = data.slice(data.indexOf("{"));
workflow = JSON.parse(data);
} catch (error) {}
}
2023-03-03 15:20:49 +00:00
if (workflow && workflow.version && workflow.nodes && workflow.extra) {
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
2023-11-30 19:13:27 +00:00
await this.loadGraphData(workflow);
2023-03-03 15:20:49 +00:00
}
else {
if (e.target.type === "text" || e.target.type === "textarea") {
return;
}
// Litegraph default paste
this.canvas.pasteFromClipboard();
}
});
}
/**
* Adds a handler on copy that serializes selected nodes to JSON
*/
#addCopyHandler() {
document.addEventListener("copy", (e) => {
2023-09-08 15:17:45 +00:00
if (e.target.type === "text" || e.target.type === "textarea") {
// Default system copy
return;
}
2023-09-08 15:17:45 +00:00
// copy nodes and clear clipboard
if (e.target.className === "litegraph" && this.canvas.selected_nodes) {
2023-09-08 15:17:45 +00:00
this.canvas.copyToClipboard();
2023-09-10 15:19:31 +00:00
e.clipboardData.setData('text', ' '); //clearData doesn't remove images from clipboard
2023-09-08 15:17:45 +00:00
e.preventDefault();
e.stopImmediatePropagation();
return false;
}
});
}
2023-03-27 01:53:49 +00:00
/**
* Handle mouse
*
* Move group by header
*/
#addProcessMouseHandler() {
const self = this;
const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown;
LGraphCanvas.prototype.processMouseDown = function(e) {
// prepare for ctrl+shift drag: zoom start
if(e.ctrlKey && e.shiftKey && e.buttons) {
self.zoom_drag_start = [e.x, e.y, this.ds.scale];
return;
}
2023-03-27 01:53:49 +00:00
const res = origProcessMouseDown.apply(this, arguments);
this.selected_group_moving = false;
if (this.selected_group && !this.selected_group_resizing) {
var font_size =
this.selected_group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE;
var height = font_size * 1.4;
// Move group by header
if (LiteGraph.isInsideRectangle(e.canvasX, e.canvasY, this.selected_group.pos[0], this.selected_group.pos[1], this.selected_group.size[0], height)) {
this.selected_group_moving = true;
}
}
return res;
}
const origProcessMouseMove = LGraphCanvas.prototype.processMouseMove;
LGraphCanvas.prototype.processMouseMove = function(e) {
// handle ctrl+shift drag
if(e.ctrlKey && e.shiftKey && self.zoom_drag_start) {
// stop canvas zoom action
if(!e.buttons) {
self.zoom_drag_start = null;
return;
}
// calculate delta
let deltaY = e.y - self.zoom_drag_start[1];
let startScale = self.zoom_drag_start[2];
let scale = startScale - deltaY/100;
this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]);
this.graph.change();
return;
}
2023-03-27 01:53:49 +00:00
const orig_selected_group = this.selected_group;
if (this.selected_group && !this.selected_group_resizing && !this.selected_group_moving) {
this.selected_group = null;
}
const res = origProcessMouseMove.apply(this, arguments);
if (orig_selected_group && !this.selected_group_resizing && !this.selected_group_moving) {
this.selected_group = orig_selected_group;
}
return res;
};
}
2023-03-26 04:45:40 +00:00
/**
* Handle keypress
*
* Ctrl + M mute/unmute selected nodes
*/
#addProcessKeyHandler() {
const self = this;
const origProcessKey = LGraphCanvas.prototype.processKey;
LGraphCanvas.prototype.processKey = function(e) {
if (!this.graph) {
return;
}
var block_default = false;
if (e.target.localName == "input") {
return;
}
if (e.type == "keydown" && !e.repeat) {
2023-03-26 04:45:40 +00:00
// Ctrl + M mute/unmute
if (e.key === 'm' && (e.metaKey || e.ctrlKey)) {
2023-03-26 04:45:40 +00:00
if (this.selected_nodes) {
for (var i in this.selected_nodes) {
if (this.selected_nodes[i].mode === 2) { // never
this.selected_nodes[i].mode = 0; // always
} else {
this.selected_nodes[i].mode = 2; // never
}
}
}
block_default = true;
}
// Ctrl + B bypass
if (e.key === 'b' && (e.metaKey || e.ctrlKey)) {
if (this.selected_nodes) {
for (var i in this.selected_nodes) {
if (this.selected_nodes[i].mode === 4) { // never
this.selected_nodes[i].mode = 0; // always
} else {
this.selected_nodes[i].mode = 4; // never
}
}
}
block_default = true;
}
// Alt + C collapse/uncollapse
if (e.key === 'c' && e.altKey) {
if (this.selected_nodes) {
for (var i in this.selected_nodes) {
this.selected_nodes[i].collapse()
}
}
block_default = true;
}
// Ctrl+C Copy
if ((e.key === 'c') && (e.metaKey || e.ctrlKey)) {
2023-09-10 15:19:31 +00:00
// Trigger onCopy
return true;
}
// Ctrl+V Paste
if ((e.key === 'v' || e.key == 'V') && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
2023-09-10 15:19:31 +00:00
// Trigger onPaste
return true;
}
if((e.key === '+') && e.altKey) {
block_default = true;
let scale = this.ds.scale * 1.1;
this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]);
this.graph.change();
}
if((e.key === '-') && e.altKey) {
block_default = true;
let scale = this.ds.scale * 1 / 1.1;
this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]);
this.graph.change();
}
2023-03-26 04:45:40 +00:00
}
this.graph.change();
if (block_default) {
e.preventDefault();
e.stopImmediatePropagation();
return false;
}
// Fall through to Litegraph defaults
return origProcessKey.apply(this, arguments);
2023-03-26 04:45:40 +00:00
};
}
2023-03-27 01:53:49 +00:00
/**
* Draws group header bar
*/
#addDrawGroupsHandler() {
const self = this;
const origDrawGroups = LGraphCanvas.prototype.drawGroups;
LGraphCanvas.prototype.drawGroups = function(canvas, ctx) {
if (!this.graph) {
return;
}
var groups = this.graph._groups;
ctx.save();
ctx.globalAlpha = 0.7 * this.editor_alpha;
for (var i = 0; i < groups.length; ++i) {
var group = groups[i];
if (!LiteGraph.overlapBounding(this.visible_area, group._bounding)) {
continue;
} //out of the visible area
ctx.fillStyle = group.color || "#335";
ctx.strokeStyle = group.color || "#335";
var pos = group._pos;
var size = group._size;
ctx.globalAlpha = 0.25 * this.editor_alpha;
ctx.beginPath();
var font_size =
group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE;
ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], font_size * 1.4);
ctx.fill();
ctx.globalAlpha = this.editor_alpha;
}
ctx.restore();
const res = origDrawGroups.apply(this, arguments);
return res;
}
}
2023-03-03 15:47:33 +00:00
/**
2023-03-14 21:22:47 +00:00
* Draws node highlights (executing, drag drop) and progress bar
2023-03-03 15:47:33 +00:00
*/
#addDrawNodeHandler() {
2023-03-26 04:45:40 +00:00
const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape;
const self = this;
2023-03-26 04:45:40 +00:00
LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) {
2023-03-26 04:45:40 +00:00
const res = origDrawNodeShape.apply(this, arguments);
const nodeErrors = self.lastNodeErrors?.[node.id];
let color = null;
let lineWidth = 1;
if (node.id === +self.runningNodeId) {
color = "#0f0";
} else if (self.dragOverNode && node.id === self.dragOverNode.id) {
color = "dodgerblue";
}
else if (nodeErrors?.errors) {
color = "red";
lineWidth = 2;
}
else if (self.lastExecutionError && +self.lastExecutionError.node_id === node.id) {
color = "#f0f";
lineWidth = 2;
}
if (color) {
const shape = node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE;
ctx.lineWidth = lineWidth;
ctx.globalAlpha = 0.8;
ctx.beginPath();
if (shape == LiteGraph.BOX_SHAPE)
ctx.rect(-6, -6 - LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT);
else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed))
ctx.roundRect(
-6,
-6 - LiteGraph.NODE_TITLE_HEIGHT,
12 + size[0] + 1,
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
this.round_radius * 2
);
else if (shape == LiteGraph.CARD_SHAPE)
ctx.roundRect(
-6,
-6 - LiteGraph.NODE_TITLE_HEIGHT,
12 + size[0] + 1,
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
2023-05-05 09:34:09 +00:00
[this.round_radius * 2, this.round_radius * 2, 2, 2]
);
else if (shape == LiteGraph.CIRCLE_SHAPE)
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2);
ctx.strokeStyle = color;
ctx.stroke();
ctx.strokeStyle = fgcolor;
ctx.globalAlpha = 1;
}
if (self.progress && node.id === +self.runningNodeId) {
ctx.fillStyle = "green";
ctx.fillRect(0, 0, size[0] * (self.progress.value / self.progress.max), 6);
ctx.fillStyle = bgcolor;
}
// Highlight inputs that failed validation
if (nodeErrors) {
ctx.lineWidth = 2;
ctx.strokeStyle = "red";
for (const error of nodeErrors.errors) {
if (error.extra_info && error.extra_info.input_name) {
const inputIndex = node.findInputSlot(error.extra_info.input_name)
if (inputIndex !== -1) {
let pos = node.getConnectionPos(true, inputIndex);
ctx.beginPath();
ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false)
ctx.stroke();
}
}
}
}
return res;
};
2023-03-26 04:45:40 +00:00
const origDrawNode = LGraphCanvas.prototype.drawNode;
LGraphCanvas.prototype.drawNode = function (node, ctx) {
var editor_alpha = this.editor_alpha;
var old_color = node.bgcolor;
2023-03-26 04:45:40 +00:00
if (node.mode === 2) { // never
this.editor_alpha = 0.4;
}
if (node.mode === 4) { // never
node.bgcolor = "#FF00FF";
this.editor_alpha = 0.2;
}
2023-03-26 04:45:40 +00:00
const res = origDrawNode.apply(this, arguments);
this.editor_alpha = editor_alpha;
node.bgcolor = old_color;
2023-03-26 04:45:40 +00:00
return res;
};
}
2023-03-03 15:47:33 +00:00
/**
* Handles updates from the API socket
*/
#addApiUpdateHandlers() {
2023-03-03 15:20:49 +00:00
api.addEventListener("status", ({ detail }) => {
this.ui.setStatus(detail);
});
2023-03-03 15:20:49 +00:00
api.addEventListener("reconnecting", () => {
this.ui.dialog.show("Reconnecting...");
});
2023-03-03 15:20:49 +00:00
api.addEventListener("reconnected", () => {
this.ui.dialog.close();
});
api.addEventListener("progress", ({ detail }) => {
if (this.workflowManager.activePrompt?.workflow
&& this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return;
this.progress = detail;
this.graph.setDirtyCanvas(true, false);
});
api.addEventListener("executing", ({ detail }) => {
if (this.workflowManager.activePrompt ?.workflow
&& this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return;
this.progress = null;
this.runningNodeId = detail;
this.graph.setDirtyCanvas(true, false);
2023-05-31 01:43:29 +00:00
delete this.nodePreviewImages[this.runningNodeId]
});
2023-03-03 15:20:49 +00:00
api.addEventListener("executed", ({ detail }) => {
if (this.workflowManager.activePrompt ?.workflow
&& this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return;
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
2023-11-30 19:13:27 +00:00
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);
2023-05-31 01:43:29 +00:00
if (node) {
if (node.onExecuted)
node.onExecuted(detail.output);
}
2023-03-03 15:20:49 +00:00
});
api.addEventListener("execution_start", ({ detail }) => {
2023-05-31 01:43:29 +00:00
this.runningNodeId = null;
this.lastExecutionError = null
2023-09-02 21:53:02 +00:00
this.graph._nodes.forEach((node) => {
if (node.onExecutionStart)
node.onExecutionStart()
})
});
api.addEventListener("execution_error", ({ detail }) => {
this.lastExecutionError = detail;
const formattedError = this.#formatExecutionError(detail);
this.ui.dialog.show(formattedError);
this.canvas.draw(true, true);
});
2023-05-31 01:43:29 +00:00
api.addEventListener("b_preview", ({ detail }) => {
const id = this.runningNodeId
if (id == null)
return;
const blob = detail
const blobUrl = URL.createObjectURL(blob)
this.nodePreviewImages[id] = [blobUrl]
});
api.init();
}
2023-03-14 20:29:18 +00:00
#addKeyboardHandler() {
window.addEventListener("keydown", (e) => {
2023-04-02 14:33:34 +00:00
this.shiftDown = e.shiftKey;
2023-03-14 20:29:18 +00:00
});
2023-04-02 14:33:34 +00:00
window.addEventListener("keyup", (e) => {
this.shiftDown = e.shiftKey;
});
2023-03-14 20:29:18 +00:00
}
#addConfigureHandler() {
const app = this;
const configure = LGraph.prototype.configure;
// Flag that the graph is configuring to prevent nodes from running checks while its still loading
LGraph.prototype.configure = function () {
app.configuringGraph = true;
try {
return configure.apply(this, arguments);
} finally {
app.configuringGraph = false;
}
};
}
#addAfterConfigureHandler() {
const app = this;
const onConfigure = app.graph.onConfigure;
app.graph.onConfigure = function () {
// Fire callbacks before the onConfigure, this is used by widget inputs to setup the config
for (const node of app.graph._nodes) {
node.onGraphConfigured?.();
}
const r = onConfigure?.apply(this, arguments);
// Fire after onConfigure, used by primitves to generate widget using input nodes config
for (const node of app.graph._nodes) {
node.onAfterGraphConfigured?.();
}
return r;
};
}
2023-03-03 19:05:39 +00:00
/**
* Loads all extensions from the API into the window in parallel
2023-03-03 19:05:39 +00:00
*/
async #loadExtensions() {
const extensions = await api.getExtensions();
this.logging.addEntry("Comfy.App", "debug", { Extensions: extensions });
const extensionPromises = extensions.map(async ext => {
try {
await import(api.apiURL(ext));
} catch (error) {
console.error("Error loading extension", ext, error);
}
});
await Promise.all(extensionPromises);
try {
this.menu.workflows.registerExtension(this);
} catch (error) {
console.error(error);
}
2023-03-03 19:05:39 +00:00
}
async #migrateSettings() {
this.isNewUserSession = true;
// Store all current settings
const settings = Object.keys(this.ui.settings).reduce((p, n) => {
const v = localStorage[`Comfy.Settings.${n}`];
if (v) {
try {
p[n] = JSON.parse(v);
} catch (error) {}
}
return p;
}, {});
await api.storeSettings(settings);
}
async #setUser() {
const userConfig = await api.getUserConfig();
this.storageLocation = userConfig.storage;
if (typeof userConfig.migrated == "boolean") {
// Single user mode migrated true/false for if the default user is created
if (!userConfig.migrated && this.storageLocation === "server") {
// Default user not created yet
await this.#migrateSettings();
}
return;
}
this.multiUserServer = true;
let user = localStorage["Comfy.userId"];
const users = userConfig.users ?? {};
if (!user || !users[user]) {
// This will rarely be hit so move the loading to on demand
const { UserSelectionScreen } = await import("./ui/userSelection.js");
this.ui.menuContainer.style.display = "none";
const { userId, username, created } = await new UserSelectionScreen().show(users, user);
this.ui.menuContainer.style.display = "";
user = userId;
localStorage["Comfy.userName"] = username;
localStorage["Comfy.userId"] = user;
if (created) {
api.user = user;
await this.#migrateSettings();
}
}
api.user = user;
this.ui.settings.addSetting({
id: "Comfy.SwitchUser",
name: "Switch User",
type: (name) => {
let currentUser = localStorage["Comfy.userName"];
if (currentUser) {
currentUser = ` (${currentUser})`;
}
return $el("tr", [
$el("td", [
$el("label", {
textContent: name,
}),
]),
$el("td", [
$el("button", {
textContent: name + (currentUser ?? ""),
onclick: () => {
delete localStorage["Comfy.userId"];
delete localStorage["Comfy.userName"];
window.location.reload();
},
}),
]),
]);
},
});
}
/**
* Set up the app on the page
*/
async setup() {
await this.#setUser();
2023-03-03 19:05:39 +00:00
// Create and mount the LiteGraph in the DOM
2023-05-16 13:55:00 +00:00
const mainCanvas = document.createElement("canvas")
mainCanvas.style.touchAction = "none"
const canvasEl = (this.canvasEl = Object.assign(mainCanvas, { id: "graph-canvas" }));
canvasEl.tabIndex = "1";
document.body.append(canvasEl);
this.resizeCanvas();
await Promise.all([this.workflowManager.loadWorkflows(), this.ui.settings.load()]);
await this.#loadExtensions();
addDomClippingSetting();
2023-03-27 01:53:49 +00:00
this.#addProcessMouseHandler();
2023-03-26 04:45:40 +00:00
this.#addProcessKeyHandler();
this.#addConfigureHandler();
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
2023-11-30 19:13:27 +00:00
this.#addApiUpdateHandlers();
this.#addRestoreWorkflowView();
2023-03-27 01:53:49 +00:00
this.graph = new LGraph();
this.#addAfterConfigureHandler();
this.canvas = new LGraphCanvas(canvasEl, this.graph);
this.ctx = canvasEl.getContext("2d");
LiteGraph.release_link_on_empty_shows_menu = true;
LiteGraph.alt_drag_do_clone_nodes = true;
this.graph.start();
// Ensure the canvas fills the window
this.resizeCanvas();
window.addEventListener("resize", () => this.resizeCanvas());
const ro = new ResizeObserver(() => this.resizeCanvas());
ro.observe(this.bodyTop);
ro.observe(this.bodyLeft);
ro.observe(this.bodyRight);
ro.observe(this.bodyBottom);
await this.#invokeExtensionsAsync("init");
await this.registerNodes();
initWidgets(this);
// Load previous workflow
let restored = false;
try {
const loadWorkflow = async (json) => {
if (json) {
const workflow = JSON.parse(json);
const workflowName = getStorageValue("Comfy.PreviousWorkflow");
await this.loadGraphData(workflow, true, true, workflowName);
return true;
}
};
const clientId = api.initialClientId ?? api.clientId;
restored =
(clientId && (await loadWorkflow(sessionStorage.getItem(`workflow:${clientId}`)))) ||
(await loadWorkflow(localStorage.getItem("workflow")));
} catch (err) {
console.error("Error loading previous workflow", err);
}
// We failed to restore a workflow so load the default
if (!restored) {
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
2023-11-30 19:13:27 +00:00
await this.loadGraphData();
}
// Save current workflow automatically
setInterval(() => {
const workflow = JSON.stringify(this.graph.serialize());
localStorage.setItem("workflow", workflow);
if (api.clientId) {
sessionStorage.setItem(`workflow:${api.clientId}`, workflow);
}
}, 1000);
this.#addDrawNodeHandler();
2023-03-27 01:53:49 +00:00
this.#addDrawGroupsHandler();
this.#addDropHandler();
this.#addCopyHandler();
2023-03-03 15:20:49 +00:00
this.#addPasteHandler();
2023-03-14 20:29:18 +00:00
this.#addKeyboardHandler();
2023-03-03 15:20:49 +00:00
await this.#invokeExtensionsAsync("setup");
}
resizeCanvas() {
// Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845
const scale = Math.max(window.devicePixelRatio, 1);
// Clear fixed width and height while calculating rect so it uses 100% instead
this.canvasEl.height = this.canvasEl.width = "";
const { width, height } = this.canvasEl.getBoundingClientRect();
this.canvasEl.width = Math.round(width * scale);
this.canvasEl.height = Math.round(height * scale);
this.canvasEl.getContext("2d").scale(scale, scale);
this.canvas?.draw(true, true);
}
2023-03-03 15:47:33 +00:00
/**
* Registers nodes with the graph
*/
async registerNodes() {
const app = this;
// Load node definitions from the backend
const defs = await api.getNodeDefs();
2023-05-31 14:26:56 +00:00
await this.registerNodesFromDefs(defs);
await this.#invokeExtensionsAsync("registerCustomNodes");
}
2023-12-01 09:13:04 +00:00
getWidgetType(inputData, inputName) {
const type = inputData[0];
if (Array.isArray(type)) {
return "COMBO";
} else if (`${type}:${inputName}` in this.widgets) {
return `${type}:${inputName}`;
} else if (type in this.widgets) {
return type;
} else {
return null;
}
}
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
2023-11-30 19:13:27 +00:00
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;
2023-12-01 09:13:04 +00:00
const widgetType = self.getWidgetType(inputData, inputName);
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
2023-11-30 19:13:27 +00:00
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 outputTooltip = nodeData["output_tooltips"]?.[o];
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
2023-11-30 19:13:27 +00:00
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, tooltip: outputTooltip });
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
2023-11-30 19:13:27 +00:00
}
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
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
2023-11-30 19:13:27 +00:00
this.widgets = Object.assign(
{},
ComfyWidgets,
...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean)
);
// Register a node for each definition
for (const nodeId in defs) {
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
2023-11-30 19:13:27 +00:00
this.registerNodeDef(nodeId, defs[nodeId]);
}
}
2023-10-17 20:53:57 +00:00
loadTemplateData(templateData) {
if (!templateData?.templates) {
return;
}
const old = localStorage.getItem("litegrapheditor_clipboard");
var maxY, nodeBottom, node;
for (const template of templateData.templates) {
if (!template?.data) {
continue;
}
localStorage.setItem("litegrapheditor_clipboard", template.data);
app.canvas.pasteFromClipboard();
// Move mouse position down to paste the next template below
maxY = false;
for (const i in app.canvas.selected_nodes) {
node = app.canvas.selected_nodes[i];
nodeBottom = node.pos[1] + node.size[1];
if (maxY === false || nodeBottom > maxY) {
maxY = nodeBottom;
}
}
app.canvas.graph_mouse[1] = maxY + 50;
}
localStorage.setItem("litegrapheditor_clipboard", old);
}
showMissingNodesError(missingNodeTypes, hasAddedNodes = true) {
let seenTypes = new Set();
this.ui.dialog.show(
$el("div.comfy-missing-nodes", [
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
2023-11-30 19:13:27 +00:00
$el("span", { textContent: "When loading the graph, the following node types were not found: " }),
$el(
"ul",
Array.from(new Set(missingNodeTypes)).map((t) => {
let children = [];
if (typeof t === "object") {
if(seenTypes.has(t.type)) return null;
seenTypes.add(t.type);
children.push($el("span", { textContent: t.type }));
if (t.hint) {
children.push($el("span", { textContent: t.hint }));
}
if (t.action) {
children.push($el("button", { onclick: t.action.callback, textContent: t.action.text }));
}
} else {
if(seenTypes.has(t)) return null;
seenTypes.add(t);
children.push($el("span", { textContent: t }));
}
return $el("li", children);
}).filter(Boolean)
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
2023-11-30 19:13:27 +00:00
),
...(hasAddedNodes
? [$el("span", { textContent: "Nodes that have failed to load will show as red on the graph." })]
: []),
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
2023-11-30 19:13:27 +00:00
])
);
this.logging.addEntry("Comfy.App", "warn", {
MissingNodes: missingNodeTypes,
});
}
async changeWorkflow(callback, workflow = null) {
try {
this.workflowManager.activeWorkflow?.changeTracker?.store()
} catch (error) {
console.error(error);
}
await callback();
try {
this.workflowManager.setWorkflow(workflow);
this.workflowManager.activeWorkflow?.track()
} catch (error) {
console.error(error);
}
}
/**
* Populates the graph with the specified workflow data
* @param {*} graphData A serialized graph object
* @param { boolean } clean If the graph state, e.g. images, should be cleared
* @param { boolean } restore_view If the graph position should be restored
* @param { import("./workflows.js").ComfyWorkflowInstance | null } workflow The workflow
*/
async loadGraphData(graphData, clean = true, restore_view = true, workflow = null) {
if (clean !== false) {
this.clean();
}
let reset_invalid_values = false;
2023-03-03 15:20:49 +00:00
if (!graphData) {
graphData = defaultGraph;
reset_invalid_values = true;
2023-03-03 15:20:49 +00:00
}
if (typeof structuredClone === "undefined")
{
graphData = JSON.parse(JSON.stringify(graphData));
}else
{
graphData = structuredClone(graphData);
}
try {
this.workflowManager.setWorkflow(workflow);
} catch (error) {
console.error(error);
}
2023-04-08 13:52:24 +00:00
const missingNodeTypes = [];
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
2023-11-30 19:13:27 +00:00
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";
2023-09-24 17:27:57 +00:00
if (n.type == "ConditioningAverage ") n.type = "ConditioningAverage"; //typo fix
2023-11-24 04:20:07 +00:00
if (n.type == "SDV_img2vid_Conditioning") n.type = "SVD_img2vid_Conditioning"; //typo fix
// Find missing node types
if (!(n.type in LiteGraph.registered_node_types)) {
missingNodeTypes.push(n.type);
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
2023-11-30 19:13:27 +00:00
n.type = sanitizeNodeName(n.type);
}
}
try {
this.graph.configure(graphData);
if (restore_view && this.enableWorkflowViewRestore.value && graphData.extra?.ds) {
this.canvas.ds.offset = graphData.extra.ds.offset;
this.canvas.ds.scale = graphData.extra.ds.scale;
}
try {
this.workflowManager.activeWorkflow?.track()
} catch (error) {
}
} catch (error) {
2023-04-08 15:37:09 +00:00
let errorHint = [];
2023-04-08 15:31:10 +00:00
// Try extracting filename to see if it was caused by an extension script
const filename = error.fileName || (error.stack || "").match(/(\/extensions\/.*\.js)/)?.[1];
const pos = (filename || "").indexOf("/extensions/");
if (pos > -1) {
2023-04-08 15:37:09 +00:00
errorHint.push(
$el("span", { textContent: "This may be due to the following script:" }),
$el("br"),
$el("span", {
style: {
fontWeight: "bold",
},
textContent: filename.substring(pos),
})
);
}
// Show dialog to let the user know something went wrong loading the data
this.ui.dialog.show(
$el("div", [
$el("p", { textContent: "Loading aborted due to error reloading workflow data" }),
$el("pre", {
style: { padding: "5px", backgroundColor: "rgba(255,0,0,0.2)" },
textContent: error.toString(),
}),
$el("pre", {
style: {
padding: "5px",
color: "#ccc",
fontSize: "10px",
maxHeight: "50vh",
overflow: "auto",
2023-04-08 15:37:09 +00:00
backgroundColor: "rgba(0,0,0,0.2)",
},
textContent: error.stack || "No stacktrace available",
}),
2023-04-08 15:37:09 +00:00
...errorHint,
]).outerHTML
);
2023-04-08 15:37:09 +00:00
return;
}
for (const node of this.graph._nodes) {
const size = node.computeSize();
size[0] = Math.max(node.size[0], size[0]);
size[1] = Math.max(node.size[1], size[1]);
node.size = size;
if (node.widgets) {
// If you break something in the backend and want to patch workflows in the frontend
// This is the place to do this
for (let widget of node.widgets) {
if (node.type == "KSampler" || node.type == "KSamplerAdvanced") {
if (widget.name == "sampler_name") {
if (widget.value.startsWith("sample_")) {
2023-03-03 15:20:49 +00:00
widget.value = widget.value.slice(7);
}
if (widget.value === "euler_pp" || widget.value === "euler_ancestral_pp") {
widget.value = widget.value.slice(0, -3);
for (let w of node.widgets) {
if (w.name == "cfg") {
w.value *= 2.0;
}
}
}
}
}
if (node.type == "KSampler" || node.type == "KSamplerAdvanced" || node.type == "PrimitiveNode") {
if (widget.name == "control_after_generate") {
if (widget.value === true) {
widget.value = "randomize";
} else if (widget.value === false) {
widget.value = "fixed";
}
}
}
if (reset_invalid_values) {
if (widget.type == "combo") {
if (!widget.options.values.includes(widget.value) && widget.options.values.length > 0) {
widget.value = widget.options.values[0];
}
}
}
}
}
this.#invokeExtensions("loadedGraphNode", node);
}
if (missingNodeTypes.length) {
this.showMissingNodesError(missingNodeTypes);
}
await this.#invokeExtensionsAsync("afterConfigureGraph", missingNodeTypes);
requestAnimationFrame(() => {
this.graph.setDirtyCanvas(true, true);
});
}
2023-03-03 15:47:33 +00:00
/**
* Converts the current graph workflow for sending to the API
* @returns The workflow and node links
*/
async graphToPrompt(graph = this.graph, clean = true) {
for (const outerNode of graph.computeExecutionOrder(false)) {
if (outerNode.widgets) {
for (const widget of outerNode.widgets) {
// Allow widgets to run callbacks before a prompt has been queued
// e.g. random seed before every gen
widget.beforeQueued?.();
}
}
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
2023-11-30 19:13:27 +00:00
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();
}
}
}
}
const workflow = graph.serialize();
const output = {};
// Process nodes in order of execution
for (const outerNode of graph.computeExecutionOrder(false)) {
2023-12-03 17:04:16 +00:00
const skipNode = outerNode.mode === 2 || outerNode.mode === 4;
const innerNodes = (!skipNode && outerNode.getInnerNodes) ? outerNode.getInnerNodes() : [outerNode];
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
2023-11-30 19:13:27 +00:00
for (const node of innerNodes) {
if (node.isVirtualNode) {
continue;
}
2023-03-03 18:28:34 +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
2023-11-30 19:13:27 +00:00
if (node.mode === 2 || node.mode === 4) {
// Don't serialize muted nodes
continue;
}
2023-03-26 04:45:40 +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
2023-11-30 19:13:27 +00:00
const inputs = {};
const widgets = node.widgets;
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
2023-11-30 19:13:27 +00:00
// 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;
}
}
}
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
2023-11-30 19:13:27 +00:00
// 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;
}
}
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
2023-11-30 19:13:27 +00:00
} 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;
2023-08-03 06:57:40 +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
2023-11-30 19:13:27 +00:00
if (!found) {
break;
}
}
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
2023-11-30 19:13:27 +00:00
if (link) {
if (parent?.updateLink) {
link = parent.updateLink(link);
}
if (link) {
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
}
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
2023-11-30 19:13:27 +00:00
}
2023-03-03 18:28:34 +00:00
}
}
let node_data = {
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
2023-11-30 19:13:27 +00:00
inputs,
class_type: node.comfyClass,
};
if (this.ui.settings.getSettingValue("Comfy.DevMode")) {
// Ignored by the backend.
node_data["_meta"] = {
title: node.title,
}
}
output[String(node.id)] = node_data;
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
2023-11-30 19:13:27 +00:00
}
}
2023-03-26 04:45:40 +00:00
// Remove inputs connected to removed nodes
if(clean) {
for (const o in output) {
for (const i in output[o].inputs) {
if (Array.isArray(output[o].inputs[i])
&& output[o].inputs[i].length === 2
&& !output[output[o].inputs[i][0]]) {
delete output[o].inputs[i];
}
2023-03-26 04:45:40 +00:00
}
}
}
return { workflow, output };
}
#formatPromptError(error) {
if (error == null) {
return "(unknown error)"
}
else if (typeof error === "string") {
return error;
}
else if (error.stack && error.message) {
return error.toString()
}
else if (error.response) {
let message = error.response.error.message;
if (error.response.error.details)
message += ": " + error.response.error.details;
for (const [nodeID, nodeError] of Object.entries(error.response.node_errors)) {
message += "\n" + nodeError.class_type + ":"
for (const errorReason of nodeError.errors) {
message += "\n - " + errorReason.message + ": " + errorReason.details
}
}
return message
}
return "(unknown error)"
}
#formatExecutionError(error) {
if (error == null) {
return "(unknown error)"
}
const traceback = error.traceback.join("")
const nodeId = error.node_id
const nodeType = error.node_type
2023-05-28 14:31:40 +00:00
return `Error occurred when executing ${nodeType}:\n\n${error.exception_message}\n\n${traceback}`
}
2023-03-09 17:02:03 +00:00
async queuePrompt(number, batchCount = 1) {
2023-04-01 17:46:05 +00:00
this.#queueItems.push({ number, batchCount });
2023-04-01 17:46:05 +00:00
// Only have one action process the items so each one gets a unique seed correctly
if (this.#processingQueue) {
return;
}
2023-04-01 17:46:05 +00:00
this.#processingQueue = true;
this.lastNodeErrors = null;
2023-04-01 17:46:05 +00:00
try {
while (this.#queueItems.length) {
({ number, batchCount } = this.#queueItems.pop());
2023-04-01 17:46:05 +00:00
for (let i = 0; i < batchCount; i++) {
const p = await this.graphToPrompt();
try {
const res = await api.queuePrompt(number, p);
this.lastNodeErrors = res.node_errors;
if (this.lastNodeErrors.length > 0) {
this.canvas.draw(true, true);
} else {
try {
this.workflowManager.storePrompt({
id: res.prompt_id,
nodes: Object.keys(p.output)
});
} catch (error) {
}
}
2023-04-01 17:46:05 +00:00
} catch (error) {
const formattedError = this.#formatPromptError(error)
this.ui.dialog.show(formattedError);
if (error.response) {
this.lastNodeErrors = error.response.node_errors;
this.canvas.draw(true, true);
}
2023-04-01 17:46:05 +00:00
break;
}
for (const n of p.workflow.nodes) {
const node = graph.getNodeById(n.id);
if (node.widgets) {
for (const widget of node.widgets) {
// Allow widgets to run callbacks after a prompt has been queued
// e.g. random seed after every gen
if (widget.afterQueued) {
widget.afterQueued();
}
}
2023-03-09 17:02:03 +00:00
}
}
2023-04-01 17:46:05 +00:00
this.canvas.draw(true, true);
await this.ui.queue.update();
}
}
2023-04-01 17:46:05 +00:00
} finally {
this.#processingQueue = false;
2023-03-09 17:02:03 +00:00
}
api.dispatchEvent(new CustomEvent("promptQueued", { detail: { number, batchCount } }));
return !this.lastNodeErrors;
}
2023-03-03 15:27:08 +00:00
showErrorOnFileLoad(file) {
this.ui.dialog.show(
$el("div", [
$el("p", {textContent: `Unable to find workflow in ${file.name}`})
]).outerHTML
);
}
2023-03-03 15:47:33 +00:00
/**
* Loads workflow data from the specified file
2023-03-03 18:28:34 +00:00
* @param {File} file
2023-03-03 15:47:33 +00:00
*/
2023-03-03 15:27:08 +00:00
async handleFile(file) {
const removeExt = f => {
if(!f) return f;
const p = f.lastIndexOf(".");
if(p === -1) return f;
return f.substring(0, p);
};
const fileName = removeExt(file.name);
2023-03-03 15:27:08 +00:00
if (file.type === "image/png") {
const pngInfo = await getPngMetadata(file);
if (pngInfo?.workflow) {
await this.loadGraphData(JSON.parse(pngInfo.workflow), true, true, fileName);
} else if (pngInfo?.prompt) {
this.loadApiJson(JSON.parse(pngInfo.prompt), fileName);
} else if (pngInfo?.parameters) {
this.changeWorkflow(() => {
importA1111(this.graph, pngInfo.parameters);
}, fileName)
} else {
this.showErrorOnFileLoad(file);
2023-03-03 15:27:08 +00:00
}
} else if (file.type === "image/webp") {
const pngInfo = await getWebpMetadata(file);
// Support loading workflows from that webp custom node.
const workflow = pngInfo?.workflow || pngInfo?.Workflow;
const prompt = pngInfo?.prompt || pngInfo?.Prompt;
if (workflow) {
this.loadGraphData(JSON.parse(workflow), true, true, fileName);
} else if (prompt) {
this.loadApiJson(JSON.parse(prompt), fileName);
} else {
this.showErrorOnFileLoad(file);
}
} else if (file.type === "audio/flac" || file.type === "audio/x-flac") {
const pngInfo = await getFlacMetadata(file);
// Support loading workflows from that webp custom node.
const workflow = pngInfo?.workflow;
const prompt = pngInfo?.prompt;
if (workflow) {
this.loadGraphData(JSON.parse(workflow), true, true, fileName);
} else if (prompt) {
this.loadApiJson(JSON.parse(prompt), fileName);
} else {
this.showErrorOnFileLoad(file);
}
} else if (file.type === "application/json" || file.name?.endsWith(".json")) {
2023-03-03 15:27:08 +00:00
const reader = new FileReader();
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
2023-11-30 19:13:27 +00:00
reader.onload = async () => {
const jsonContent = JSON.parse(reader.result);
2023-10-17 20:53:57 +00:00
if (jsonContent?.templates) {
this.loadTemplateData(jsonContent);
} else if(this.isApiJson(jsonContent)) {
this.loadApiJson(jsonContent, fileName);
2023-10-17 20:53:57 +00:00
} else {
2024-07-01 16:10:44 +00:00
await this.loadGraphData(jsonContent, true, true, fileName);
2023-10-17 20:53:57 +00:00
}
2023-03-03 15:27:08 +00:00
};
reader.readAsText(file);
} else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) {
2023-05-18 06:41:21 +00:00
const info = await getLatentMetadata(file);
if (info.workflow) {
2024-07-01 16:10:44 +00:00
await this.loadGraphData(JSON.parse(info.workflow), true, true, fileName);
} else if (info.prompt) {
this.loadApiJson(JSON.parse(info.prompt));
} else {
this.showErrorOnFileLoad(file);
2023-05-18 06:41:21 +00:00
}
} else {
this.showErrorOnFileLoad(file);
2023-03-03 15:27:08 +00:00
}
}
2023-03-03 18:28:34 +00:00
isApiJson(data) {
return Object.values(data).every((v) => v.class_type);
}
loadApiJson(apiData, fileName) {
const missingNodeTypes = Object.values(apiData).filter((n) => !LiteGraph.registered_node_types[n.class_type]);
if (missingNodeTypes.length) {
this.showMissingNodesError(missingNodeTypes.map(t => t.class_type), false);
return;
}
const ids = Object.keys(apiData);
app.graph.clear();
for (const id of ids) {
const data = apiData[id];
const node = LiteGraph.createNode(data.class_type);
node.id = isNaN(+id) ? id : +id;
node.title = data._meta?.title ?? node.title
app.graph.add(node);
}
this.changeWorkflow(() => {
for (const id of ids) {
const data = apiData[id];
const node = app.graph.getNodeById(id);
for (const input in data.inputs ?? {}) {
const value = data.inputs[input];
if (value instanceof Array) {
const [fromId, fromSlot] = value;
const fromNode = app.graph.getNodeById(fromId);
let toSlot = node.inputs?.findIndex((inp) => inp.name === input);
if (toSlot == null || toSlot === -1) {
try {
// Target has no matching input, most likely a converted widget
const widget = node.widgets?.find((w) => w.name === input);
if (widget && node.convertWidgetToInput?.(widget)) {
toSlot = node.inputs?.length - 1;
}
} catch (error) {}
}
if (toSlot != null || toSlot !== -1) {
fromNode.connect(fromSlot, node, toSlot);
}
} else {
const widget = node.widgets?.find((w) => w.name === input);
if (widget) {
widget.value = value;
widget.callback?.(value);
}
}
}
}
app.graph.arrange();
}, fileName);
}
2023-04-15 20:40:39 +00:00
/**
* Registers a Comfy web extension with the app
* @param {ComfyExtension} extension
*/
2023-03-03 18:28:34 +00:00
registerExtension(extension) {
if (!extension.name) {
throw new Error("Extensions must have a 'name' property.");
}
if (this.extensions.find((ext) => ext.name === extension.name)) {
throw new Error(`Extension named '${extension.name}' already registered.`);
}
this.extensions.push(extension);
}
/**
* Refresh combo list on whole nodes
*/
async refreshComboInNodes() {
const defs = await api.getNodeDefs();
for (const nodeId in defs) {
this.registerNodeDef(nodeId, defs[nodeId]);
}
for(let nodeNum in this.graph._nodes) {
const node = this.graph._nodes[nodeNum];
const def = defs[node.type];
// Allow primitive nodes to handle refresh
node.refreshComboInNode?.(defs);
if(!def)
continue;
for(const widgetNum in node.widgets) {
const widget = node.widgets[widgetNum]
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
widget.options.values = def["input"]["required"][widget.name][0];
if(widget.name != 'image' && !widget.options.values.includes(widget.value)) {
widget.value = widget.options.values[0];
Clipspace Menu and MaskEditor application. (#548) * Add clipspace feature. * feat: copy content to clipspace * feat: paste content from clipspace Extend validation to allow for validating annotated_path in addition to other parameters. Add support for annotated_filepath in folder_paths function. Generalize the '/upload/image' API to allow for uploading images to the 'input', 'temp', or 'output' directories. * rename contentClipboard -> clipspace * Do deep copy for imgs on copy to clipspace. * mask painting on clipspace * add original_imgs into clipspace * Preserve the original image when 'imgs' are modified * robust patch & refactoring folder_paths about annotated_filepath * wip * Only show the Paste menu if the ComfyApp.clipspace is not empty * clipspace feature added maskeditor feature added * instant refresh on paste force triggering 'changed' on paste action * enhance mask painting smooth drawing add brush_size +/- button * robust patch use mouseup event * robust patch again... * subfolder fix on paste logic attach subfolder if subfolder isn't empty * event listener patch add ], [ key event for brush size remove listener on close * Fix button positioning issue related to window height. Change brush size from button to slider. * clean commit * clean code * various bug fixes * paste action - prevent opening upload popup - ensure rendering after widget_value update * view api update - support annotated_filepath * maskeditor layout - prevent covering button by hidden div * remove dbg message * Add cursor functionality to display brush size * refactor: Replace brush preview feature with missionfloyd implementation * missionfloyd implementation * hiding brush preview off the canvas * change brush size on wheel event * keyup -> keydown event * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * Add support for channel-specific image data retrieval in /view API to fix alpha mask loading issue When loading an image with an alpha mask in JavaScript canvas, there is an issue where the alpha and RGB channels are premultiplied. To avoid reliance on JavaScript canvas, I added support for channel-specific image data retrieval in the "/view" API. This allows us to retrieve data for each channel separately and fix the alpha mask loading issue. The changes have been committed to the repository. * Enable brush preview for key and slider events * optimize * preview fix * robust patch * fix copy (clipspace) action imgs[0] copy -> whole imgs copy * support batch images on clipspace, maskeditor * copy/paste bug fixes for batch images enhance selector preview on clipspace menu add img_paste_mode option into clipspace menu * crash fix * print message if clipspace content cannot editable * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * make default img_paste_mode to 'selected' refactor space -> tab * save clipspace files to input/clipspace instead of temp * show "clipspace/filename.png" instead of 'filename.png [clipspace]' in LoadImage/LoadImageMask * refresh fix related to FILE_COMBO * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * adjust margin based on missionfloyd impelements * mouse event -> pointer event * pen, touch, mouse drawing patched and tested * Update web/extensions/core/maskeditor.js Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com> * add comment about touch event. --------- Co-authored-by: Lt.Dr.Data <lt.dr.data@gmail.com> Co-authored-by: missionfloyd <missionfloyd@users.noreply.github.com>
2023-05-08 18:37:36 +00:00
widget.callback(widget.value);
}
}
}
}
await this.#invokeExtensionsAsync("refreshComboInNodes", defs);
}
2024-05-10 21:30:52 +00:00
resetView() {
app.canvas.ds.scale = 1;
app.canvas.ds.offset = [0, 0]
app.graph.setDirtyCanvas(true, true);
}
/**
* Clean current state
*/
clean() {
this.nodeOutputs = {};
2023-05-31 01:43:29 +00:00
this.nodePreviewImages = {}
this.lastNodeErrors = null;
this.lastExecutionError = null;
2023-05-31 01:43:29 +00:00
this.runningNodeId = null;
}
}
export const app = new ComfyApp();