|
|
import type {
|
|
|
LGraphCanvas as TLGraphCanvas,
|
|
|
LGraphNode,
|
|
|
LGraphGroup as TLGraphGroup,
|
|
|
SerializedLGraphNode,
|
|
|
serializedLGraph,
|
|
|
LiteGraph as TLiteGraph,
|
|
|
ContextMenuItem,
|
|
|
LGraph as TLGraph,
|
|
|
AdjustedMouseEvent,
|
|
|
} from "typings/litegraph.js";
|
|
|
import type { ComfyApiFormat, ComfyApiPrompt, ComfyApp } from "typings/comfy.js";
|
|
|
|
|
|
import { app } from "../../scripts/app.js";
|
|
|
|
|
|
import { api } from "../../scripts/api.js";
|
|
|
import { SERVICE as CONFIG_SERVICE } from "./config_service.js";
|
|
|
import { fixBadLinks } from "rgthree/common/link_fixer.js";
|
|
|
import { wait } from "rgthree/common/shared_utils.js";
|
|
|
import { replaceNode, waitForCanvas, waitForGraph } from "./utils.js";
|
|
|
import { NodeTypesString } from "./constants.js";
|
|
|
import { RgthreeProgressBar } from "rgthree/common/progress_bar.js";
|
|
|
import { RgthreeConfigDialog } from "./config.js";
|
|
|
import { iconGear, iconReplace, iconStarFilled, logoRgthree } from "rgthree/common/media/svgs.js";
|
|
|
import type { Bookmark } from "./bookmark";
|
|
|
import { createElement, query } from "rgthree/common/utils_dom.js";
|
|
|
|
|
|
declare const LiteGraph: typeof TLiteGraph;
|
|
|
declare const LGraphCanvas: typeof TLGraphCanvas;
|
|
|
declare const LGraph: typeof TLGraph;
|
|
|
declare const LGraphGroup: typeof TLGraphGroup;
|
|
|
|
|
|
export enum LogLevel {
|
|
|
IMPORTANT = 1,
|
|
|
ERROR,
|
|
|
WARN,
|
|
|
INFO,
|
|
|
DEBUG,
|
|
|
DEV,
|
|
|
}
|
|
|
|
|
|
const LogLevelKeyToLogLevel: { [key: string]: LogLevel } = {
|
|
|
IMPORTANT: LogLevel.IMPORTANT,
|
|
|
ERROR: LogLevel.ERROR,
|
|
|
WARN: LogLevel.WARN,
|
|
|
INFO: LogLevel.INFO,
|
|
|
DEBUG: LogLevel.DEBUG,
|
|
|
DEV: LogLevel.DEV,
|
|
|
};
|
|
|
|
|
|
type ConsoleLogFns = "log" | "error" | "warn" | "debug" | "info";
|
|
|
const LogLevelToMethod: { [key in LogLevel]: ConsoleLogFns } = {
|
|
|
[LogLevel.IMPORTANT]: "log",
|
|
|
[LogLevel.ERROR]: "error",
|
|
|
[LogLevel.WARN]: "warn",
|
|
|
[LogLevel.INFO]: "info",
|
|
|
[LogLevel.DEBUG]: "log",
|
|
|
[LogLevel.DEV]: "log",
|
|
|
};
|
|
|
const LogLevelToCSS: { [key in LogLevel]: string } = {
|
|
|
[LogLevel.IMPORTANT]: "font-weight: bold; color: blue;",
|
|
|
[LogLevel.ERROR]: "",
|
|
|
[LogLevel.WARN]: "",
|
|
|
[LogLevel.INFO]: "font-style: italic; color: blue;",
|
|
|
[LogLevel.DEBUG]: "font-style: italic; color: #444;",
|
|
|
[LogLevel.DEV]: "color: #004b68;",
|
|
|
};
|
|
|
|
|
|
let GLOBAL_LOG_LEVEL = LogLevel.ERROR;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const INVOKE_EXTENSIONS_BLOCKLIST = [
|
|
|
{
|
|
|
name: "Comfy.WidgetInputs",
|
|
|
reason:
|
|
|
"Major conflict with rgthree-comfy nodes' inputs causing instability and " +
|
|
|
"repeated link disconnections.",
|
|
|
},
|
|
|
{
|
|
|
name: "efficiency.widgethider",
|
|
|
reason:
|
|
|
"Overrides value getter before widget getter is prepared. Can be lifted if/when "+
|
|
|
"https://github.com/jags111/efficiency-nodes-comfyui/pull/203 is pulled."
|
|
|
|
|
|
}
|
|
|
];
|
|
|
|
|
|
|
|
|
class Logger {
|
|
|
|
|
|
log(level: LogLevel, message: string, ...args: any[]) {
|
|
|
const [n, v] = this.logParts(level, message, ...args);
|
|
|
console[n]?.(...v);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logParts(level: LogLevel, message: string, ...args: any[]): [ConsoleLogFns, any[]] {
|
|
|
if (level <= GLOBAL_LOG_LEVEL) {
|
|
|
const css = LogLevelToCSS[level] || "";
|
|
|
if (level === LogLevel.DEV) {
|
|
|
message = `🔧 ${message}`;
|
|
|
}
|
|
|
return [LogLevelToMethod[level], [`%c${message}`, css, ...args]];
|
|
|
}
|
|
|
return ["none" as "info", []];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LogSession {
|
|
|
readonly logger = new Logger();
|
|
|
readonly logsCache: { [key: string]: { lastShownTime: number } } = {};
|
|
|
|
|
|
constructor(readonly name?: string) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logParts(level: LogLevel, message?: string, ...args: any[]): [ConsoleLogFns, any[]] {
|
|
|
message = `${this.name || ""}${message ? " " + message : ""}`;
|
|
|
return this.logger.logParts(level, message, ...args);
|
|
|
}
|
|
|
|
|
|
logPartsOnceForTime(
|
|
|
level: LogLevel,
|
|
|
time: number,
|
|
|
message?: string,
|
|
|
...args: any[]
|
|
|
): [ConsoleLogFns, any[]] {
|
|
|
message = `${this.name || ""}${message ? " " + message : ""}`;
|
|
|
const cacheKey = `${level}:${message}`;
|
|
|
const cacheEntry = this.logsCache[cacheKey];
|
|
|
const now = +new Date();
|
|
|
if (cacheEntry && cacheEntry.lastShownTime + time > now) {
|
|
|
return ["none" as "info", []];
|
|
|
}
|
|
|
const parts = this.logger.logParts(level, message, ...args);
|
|
|
if (console[parts[0]]) {
|
|
|
this.logsCache[cacheKey] = this.logsCache[cacheKey] || ({} as { lastShownTime: number });
|
|
|
this.logsCache[cacheKey]!.lastShownTime = now;
|
|
|
}
|
|
|
return parts;
|
|
|
}
|
|
|
|
|
|
debugParts(message?: string, ...args: any[]) {
|
|
|
return this.logParts(LogLevel.DEBUG, message, ...args);
|
|
|
}
|
|
|
|
|
|
infoParts(message?: string, ...args: any[]) {
|
|
|
return this.logParts(LogLevel.INFO, message, ...args);
|
|
|
}
|
|
|
|
|
|
warnParts(message?: string, ...args: any[]) {
|
|
|
return this.logParts(LogLevel.WARN, message, ...args);
|
|
|
}
|
|
|
|
|
|
newSession(name?: string) {
|
|
|
return new LogSession(`${this.name}${name}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export type RgthreeUiMessage = {
|
|
|
id: string;
|
|
|
message: string;
|
|
|
type?: "warn" | "info" | "success" | null;
|
|
|
timeout?: number;
|
|
|
|
|
|
actions?: Array<{
|
|
|
label: string;
|
|
|
href?: string;
|
|
|
callback?: (event: MouseEvent) => void;
|
|
|
}>;
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Rgthree extends EventTarget {
|
|
|
|
|
|
readonly api = api;
|
|
|
private settingsDialog: RgthreeConfigDialog | null = null;
|
|
|
private progressBarEl: RgthreeProgressBar | null = null;
|
|
|
|
|
|
|
|
|
private queueNodeIds: number[] | null = null;
|
|
|
|
|
|
|
|
|
ctrlKey = false;
|
|
|
altKey = false;
|
|
|
metaKey = false;
|
|
|
shiftKey = false;
|
|
|
readonly downKeys: { [key: string]: boolean } = {};
|
|
|
|
|
|
logger = new LogSession("[rgthree]");
|
|
|
|
|
|
monitorBadLinksAlerted = false;
|
|
|
monitorLinkTimeout: number | null = null;
|
|
|
|
|
|
processingQueue = false;
|
|
|
loadingApiJson = false;
|
|
|
replacingReroute: number | null = null;
|
|
|
processingMouseDown = false;
|
|
|
processingMouseUp = false;
|
|
|
processingMouseMove = false;
|
|
|
lastAdjustedMouseEvent: AdjustedMouseEvent | null = null;
|
|
|
|
|
|
|
|
|
canvasCurrentlyCopyingToClipboard = false;
|
|
|
canvasCurrentlyCopyingToClipboardWithMultipleNodes = false;
|
|
|
initialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff: any = null;
|
|
|
|
|
|
private elDebugKeydowns: HTMLDivElement | null = null;
|
|
|
|
|
|
private readonly isMac: boolean = !!(
|
|
|
navigator.platform?.toLocaleUpperCase().startsWith("MAC") ||
|
|
|
(navigator as any).userAgentData?.platform?.toLocaleUpperCase().startsWith("MAC")
|
|
|
);
|
|
|
|
|
|
constructor() {
|
|
|
super();
|
|
|
|
|
|
const logLevel =
|
|
|
LogLevelKeyToLogLevel[CONFIG_SERVICE.getConfigValue("log_level")] ?? GLOBAL_LOG_LEVEL;
|
|
|
this.setLogLevel(logLevel);
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener("visibilitychange", (e) => {
|
|
|
this.clearKeydowns();
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("blur", (e) => {
|
|
|
this.clearKeydowns();
|
|
|
});
|
|
|
|
|
|
this.initializeGraphAndCanvasHooks();
|
|
|
this.initializeComfyUIHooks();
|
|
|
this.initializeContextMenu();
|
|
|
|
|
|
wait(100).then(() => {
|
|
|
this.injectRgthreeCss();
|
|
|
});
|
|
|
|
|
|
this.initializeProgressBar();
|
|
|
this.initializeDebugShit();
|
|
|
|
|
|
CONFIG_SERVICE.addEventListener("config-change", ((e: CustomEvent) => {
|
|
|
if (e.detail?.key?.includes("features.progress_bar")) {
|
|
|
this.initializeProgressBar();
|
|
|
}
|
|
|
}) as EventListener);
|
|
|
}
|
|
|
|
|
|
initializeDebugShit() {
|
|
|
if (!this.isDebugMode()) {
|
|
|
return;
|
|
|
}
|
|
|
this.elDebugKeydowns = createElement<HTMLDivElement>("div.rgthree-debug-keydowns", {
|
|
|
parent: document.body,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
private debugRenderKeys() {
|
|
|
if (!this.elDebugKeydowns) {
|
|
|
return;
|
|
|
}
|
|
|
this.elDebugKeydowns.innerText = Object.keys(this.downKeys).join(" ");
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
initializeProgressBar() {
|
|
|
if (CONFIG_SERVICE.getConfigValue("features.progress_bar.enabled")) {
|
|
|
if (!this.progressBarEl) {
|
|
|
this.progressBarEl = RgthreeProgressBar.create();
|
|
|
|
|
|
this.progressBarEl.addEventListener("contextmenu", async (e) => {
|
|
|
e.stopPropagation();
|
|
|
e.preventDefault();
|
|
|
});
|
|
|
|
|
|
this.progressBarEl.addEventListener("pointerdown", async (e) => {
|
|
|
LiteGraph.closeAllContextMenus();
|
|
|
if (e.button == 2) {
|
|
|
const canvas = await waitForCanvas();
|
|
|
new LiteGraph.ContextMenu(
|
|
|
this.getRgthreeContextMenuItems(),
|
|
|
{
|
|
|
title: `<div class="rgthree-contextmenu-item rgthree-contextmenu-title-rgthree-comfy">${logoRgthree} rgthree-comfy</div>`,
|
|
|
left: e.clientX,
|
|
|
top: 5,
|
|
|
},
|
|
|
canvas.getCanvasWindow(),
|
|
|
);
|
|
|
return;
|
|
|
}
|
|
|
if (e.button == 0) {
|
|
|
const nodeId = this.progressBarEl?.currentNodeId;
|
|
|
if (nodeId) {
|
|
|
const [canvas, graph] = await Promise.all([waitForCanvas(), waitForGraph()]);
|
|
|
const node = graph.getNodeById(Number(nodeId));
|
|
|
if (node) {
|
|
|
canvas.centerOnNode(node);
|
|
|
e.stopPropagation();
|
|
|
e.preventDefault();
|
|
|
}
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
if (!this.progressBarEl!.parentElement) {
|
|
|
document.body.appendChild(this.progressBarEl);
|
|
|
}
|
|
|
const height = CONFIG_SERVICE.getConfigValue("features.progress_bar.height") || 14;
|
|
|
this.progressBarEl.style.height = `${height}px`;
|
|
|
const fontSize = Math.max(10, Number(height) - 10);
|
|
|
this.progressBarEl.style.fontSize = `${fontSize}px`;
|
|
|
this.progressBarEl.style.fontWeight = fontSize <= 12 ? "bold" : "normal";
|
|
|
if (CONFIG_SERVICE.getConfigValue("features.progress_bar.position") === "bottom") {
|
|
|
this.progressBarEl.style.bottom = `0px`;
|
|
|
this.progressBarEl.style.top = `auto`;
|
|
|
} else {
|
|
|
this.progressBarEl.style.top = `0px`;
|
|
|
this.progressBarEl.style.bottom = `auto`;
|
|
|
}
|
|
|
} else {
|
|
|
this.progressBarEl?.remove();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async initializeGraphAndCanvasHooks() {
|
|
|
const rgthree = this;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const graphSerialize = LGraph.prototype.serialize;
|
|
|
LGraph.prototype.serialize = function () {
|
|
|
const response = graphSerialize.apply(this, [...arguments] as any) as any;
|
|
|
rgthree.initialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff = response;
|
|
|
return response;
|
|
|
};
|
|
|
|
|
|
|
|
|
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
|
|
|
LGraphCanvas.prototype.processMouseDown = function (e: AdjustedMouseEvent) {
|
|
|
rgthree.processingMouseDown = true;
|
|
|
const returnVal = processMouseDown.apply(this, [...arguments] as any);
|
|
|
rgthree.dispatchCustomEvent("on-process-mouse-down", { originalEvent: e });
|
|
|
rgthree.processingMouseDown = false;
|
|
|
return returnVal;
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const adjustMouseEvent = LGraphCanvas.prototype.adjustMouseEvent;
|
|
|
LGraphCanvas.prototype.adjustMouseEvent = function (e: PointerEvent) {
|
|
|
adjustMouseEvent.apply(this, [...arguments] as any);
|
|
|
rgthree.lastAdjustedMouseEvent = e as AdjustedMouseEvent;
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const copyToClipboard = LGraphCanvas.prototype.copyToClipboard;
|
|
|
LGraphCanvas.prototype.copyToClipboard = function (nodes: LGraphNode[]) {
|
|
|
rgthree.canvasCurrentlyCopyingToClipboard = true;
|
|
|
rgthree.canvasCurrentlyCopyingToClipboardWithMultipleNodes =
|
|
|
Object.values(nodes || this.selected_nodes || []).length > 1;
|
|
|
copyToClipboard.apply(this, [...arguments] as any);
|
|
|
rgthree.canvasCurrentlyCopyingToClipboard = false;
|
|
|
rgthree.canvasCurrentlyCopyingToClipboardWithMultipleNodes = false;
|
|
|
};
|
|
|
|
|
|
|
|
|
const onGroupAdd = LGraphCanvas.onGroupAdd;
|
|
|
LGraphCanvas.onGroupAdd = function (...args: any[]) {
|
|
|
const graph = app.graph as TLGraph;
|
|
|
onGroupAdd.apply(this, [...args] as any);
|
|
|
LGraphCanvas.onShowPropertyEditor(
|
|
|
{},
|
|
|
null,
|
|
|
null,
|
|
|
null,
|
|
|
graph._groups[graph._groups.length - 1],
|
|
|
);
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const processKey = LGraphCanvas.prototype.processKey;
|
|
|
LGraphCanvas.prototype.processKey = function (e: KeyboardEvent) {
|
|
|
if (e.type === "keydown") {
|
|
|
rgthree.handleKeydown(e);
|
|
|
} else if (e.type === "keyup") {
|
|
|
rgthree.handleKeyup(e);
|
|
|
}
|
|
|
return processKey.apply(this, [...arguments] as any) as any;
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async invokeExtensionsAsync(method: "nodeCreated", ...args: any[]) {
|
|
|
const comfyapp = app as ComfyApp;
|
|
|
if (CONFIG_SERVICE.getConfigValue("features.invoke_extensions_async.node_created") === false) {
|
|
|
const [m, a] = this.logParts(
|
|
|
LogLevel.INFO,
|
|
|
`Skipping invokeExtensionsAsync for applicable rgthree-comfy nodes`,
|
|
|
);
|
|
|
console[m]?.(...a);
|
|
|
return Promise.resolve();
|
|
|
}
|
|
|
return await Promise.all(
|
|
|
comfyapp.extensions.map(async (ext) => {
|
|
|
if (ext?.[method]) {
|
|
|
try {
|
|
|
const blocked = INVOKE_EXTENSIONS_BLOCKLIST.find((block) =>
|
|
|
ext.name.toLowerCase().startsWith(block.name.toLowerCase()),
|
|
|
);
|
|
|
if (blocked) {
|
|
|
const [n, v] = this.logger.logPartsOnceForTime(
|
|
|
LogLevel.WARN,
|
|
|
5000,
|
|
|
`Blocked extension '${ext.name}' method '${method}' for rgthree-nodes because: ${blocked.reason}`,
|
|
|
);
|
|
|
console[n]?.(...v);
|
|
|
return Promise.resolve();
|
|
|
}
|
|
|
|
|
|
return await ext[method]!(...args, comfyapp);
|
|
|
} catch (error) {
|
|
|
const [n, v] = this.logParts(
|
|
|
LogLevel.ERROR,
|
|
|
`Error calling extension '${ext.name}' method '${method}' for rgthree-node.`,
|
|
|
{ error },
|
|
|
{ extension: ext },
|
|
|
{ args },
|
|
|
);
|
|
|
console[n]?.(...v);
|
|
|
}
|
|
|
}
|
|
|
}),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private dispatchCustomEvent(event: string, detail?: any) {
|
|
|
if (detail != null) {
|
|
|
return this.dispatchEvent(new CustomEvent(event, { detail }));
|
|
|
}
|
|
|
return this.dispatchEvent(new CustomEvent(event));
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async initializeContextMenu() {
|
|
|
const that = this;
|
|
|
setTimeout(async () => {
|
|
|
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
|
|
|
LGraphCanvas.prototype.getCanvasMenuOptions = function (...args: any[]) {
|
|
|
const options = getCanvasMenuOptions.apply(this, [...args] as any);
|
|
|
|
|
|
options.push(null);
|
|
|
options.push({
|
|
|
content: logoRgthree + `rgthree-comfy`,
|
|
|
className: "rgthree-contextmenu-item rgthree-contextmenu-main-item-rgthree-comfy",
|
|
|
submenu: {
|
|
|
options: that.getRgthreeContextMenuItems(),
|
|
|
},
|
|
|
});
|
|
|
|
|
|
return options;
|
|
|
};
|
|
|
}, 1000);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private getRgthreeContextMenuItems(): ContextMenuItem[] {
|
|
|
const [canvas, graph] = [app.canvas as TLGraphCanvas, app.graph as TLGraph];
|
|
|
const selectedNodes = Object.values(canvas.selected_nodes || {});
|
|
|
let rerouteNodes: LGraphNode[] = [];
|
|
|
if (selectedNodes.length) {
|
|
|
rerouteNodes = selectedNodes.filter((n) => n.type === "Reroute");
|
|
|
} else {
|
|
|
rerouteNodes = graph._nodes.filter((n) => n.type == "Reroute");
|
|
|
}
|
|
|
const rerouteLabel = selectedNodes.length ? "selected" : "all";
|
|
|
|
|
|
const showBookmarks = CONFIG_SERVICE.getFeatureValue("menu_bookmarks.enabled");
|
|
|
const bookmarkMenuItems = showBookmarks ? getBookmarks() : [];
|
|
|
|
|
|
return [
|
|
|
{
|
|
|
content: "Actions",
|
|
|
disabled: true,
|
|
|
className: "rgthree-contextmenu-item rgthree-contextmenu-label",
|
|
|
},
|
|
|
{
|
|
|
content: iconGear + "Settings (rgthree-comfy)",
|
|
|
disabled: !!this.settingsDialog,
|
|
|
className: "rgthree-contextmenu-item",
|
|
|
callback: (...args: any[]) => {
|
|
|
this.settingsDialog = new RgthreeConfigDialog().show();
|
|
|
this.settingsDialog.addEventListener("close", (e) => {
|
|
|
this.settingsDialog = null;
|
|
|
});
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
content: iconReplace + ` Convert ${rerouteLabel} Reroutes`,
|
|
|
disabled: !rerouteNodes.length,
|
|
|
className: "rgthree-contextmenu-item",
|
|
|
callback: (...args: any[]) => {
|
|
|
const msg =
|
|
|
`Convert ${rerouteLabel} ComfyUI Reroutes to Reroute (rgthree) nodes? \n` +
|
|
|
`(First save a copy of your workflow & check reroute connections afterwards)`;
|
|
|
if (!window.confirm(msg)) {
|
|
|
return;
|
|
|
}
|
|
|
(async () => {
|
|
|
for (const node of [...rerouteNodes]) {
|
|
|
if (node.type == "Reroute") {
|
|
|
this.replacingReroute = node.id;
|
|
|
await replaceNode(node, NodeTypesString.REROUTE);
|
|
|
this.replacingReroute = null;
|
|
|
}
|
|
|
}
|
|
|
})();
|
|
|
},
|
|
|
},
|
|
|
...bookmarkMenuItems,
|
|
|
{
|
|
|
content: "More...",
|
|
|
disabled: true,
|
|
|
className: "rgthree-contextmenu-item rgthree-contextmenu-label",
|
|
|
},
|
|
|
{
|
|
|
content: iconStarFilled + "Star on Github",
|
|
|
className: "rgthree-contextmenu-item rgthree-contextmenu-github",
|
|
|
callback: (...args: any[]) => {
|
|
|
window.open("https://github.com/rgthree/rgthree-comfy", "_blank");
|
|
|
},
|
|
|
},
|
|
|
];
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async queueOutputNodes(nodeIds: number[]) {
|
|
|
try {
|
|
|
this.queueNodeIds = nodeIds;
|
|
|
await app.queuePrompt();
|
|
|
} catch (e) {
|
|
|
const [n, v] = this.logParts(
|
|
|
LogLevel.ERROR,
|
|
|
`There was an error queuing nodes ${nodeIds}`,
|
|
|
e,
|
|
|
);
|
|
|
console[n]?.(...v);
|
|
|
} finally {
|
|
|
this.queueNodeIds = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private recursiveAddNodes(nodeId: string, oldOutput: ComfyApiFormat, newOutput: ComfyApiFormat) {
|
|
|
let currentId = nodeId;
|
|
|
let currentNode = oldOutput[currentId]!;
|
|
|
if (newOutput[currentId] == null) {
|
|
|
newOutput[currentId] = currentNode;
|
|
|
for (const inputValue of Object.values(currentNode.inputs || [])) {
|
|
|
if (Array.isArray(inputValue)) {
|
|
|
this.recursiveAddNodes(inputValue[0], oldOutput, newOutput);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return newOutput;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private initializeComfyUIHooks() {
|
|
|
const rgthree = this;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const queuePrompt = app.queuePrompt as Function;
|
|
|
app.queuePrompt = async function () {
|
|
|
rgthree.processingQueue = true;
|
|
|
rgthree.dispatchCustomEvent("queue");
|
|
|
try {
|
|
|
await queuePrompt.apply(app, [...arguments]);
|
|
|
} finally {
|
|
|
rgthree.processingQueue = false;
|
|
|
rgthree.dispatchCustomEvent("queue-end");
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
const loadApiJson = app.loadApiJson as Function;
|
|
|
app.loadApiJson = async function () {
|
|
|
rgthree.loadingApiJson = true;
|
|
|
try {
|
|
|
loadApiJson.apply(app, [...arguments]);
|
|
|
} finally {
|
|
|
rgthree.loadingApiJson = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
const graphToPrompt = app.graphToPrompt as Function;
|
|
|
app.graphToPrompt = async function () {
|
|
|
rgthree.dispatchCustomEvent("graph-to-prompt");
|
|
|
let promise = graphToPrompt.apply(app, [...arguments]);
|
|
|
await promise;
|
|
|
rgthree.dispatchCustomEvent("graph-to-prompt-end");
|
|
|
return promise;
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const apiQueuePrompt = api.queuePrompt as Function;
|
|
|
api.queuePrompt = async function (index: number, prompt: ComfyApiPrompt) {
|
|
|
if (rgthree.queueNodeIds?.length && prompt.output) {
|
|
|
const oldOutput = prompt.output;
|
|
|
let newOutput = {};
|
|
|
for (const queueNodeId of rgthree.queueNodeIds) {
|
|
|
rgthree.recursiveAddNodes(String(queueNodeId), oldOutput, newOutput);
|
|
|
}
|
|
|
prompt.output = newOutput;
|
|
|
}
|
|
|
rgthree.dispatchCustomEvent("comfy-api-queue-prompt-before", {
|
|
|
workflow: prompt.workflow,
|
|
|
output: prompt.output,
|
|
|
});
|
|
|
const response = apiQueuePrompt.apply(app, [index, prompt]);
|
|
|
rgthree.dispatchCustomEvent("comfy-api-queue-prompt-end");
|
|
|
return response;
|
|
|
};
|
|
|
|
|
|
|
|
|
const clean = app.clean;
|
|
|
app.clean = function () {
|
|
|
rgthree.clearAllMessages();
|
|
|
clean && clean.call(app, ...arguments);
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const loadGraphData = app.loadGraphData;
|
|
|
app.loadGraphData = function (graph: serializedLGraph) {
|
|
|
if (rgthree.monitorLinkTimeout) {
|
|
|
clearTimeout(rgthree.monitorLinkTimeout);
|
|
|
rgthree.monitorLinkTimeout = null;
|
|
|
}
|
|
|
rgthree.clearAllMessages();
|
|
|
|
|
|
let graphCopy: serializedLGraph | null;
|
|
|
try {
|
|
|
graphCopy = JSON.parse(JSON.stringify(graph));
|
|
|
} catch (e) {
|
|
|
graphCopy = null;
|
|
|
}
|
|
|
setTimeout(() => {
|
|
|
const wasLoadingAborted = document
|
|
|
.querySelector(".comfy-modal-content")
|
|
|
?.textContent?.includes("Loading aborted due");
|
|
|
const graphToUse = wasLoadingAborted ? graphCopy || graph : app.graph;
|
|
|
const fixBadLinksResult = fixBadLinks(graphToUse);
|
|
|
if (fixBadLinksResult.hasBadLinks) {
|
|
|
const [n, v] = rgthree.logParts(
|
|
|
LogLevel.WARN,
|
|
|
`The workflow you've loaded has corrupt linking data. Open ${
|
|
|
new URL(location.href).origin
|
|
|
}/rgthree/link_fixer to try to fix.`,
|
|
|
);
|
|
|
console[n]?.(...v);
|
|
|
if (CONFIG_SERVICE.getConfigValue("features.show_alerts_for_corrupt_workflows")) {
|
|
|
rgthree.showMessage({
|
|
|
id: "bad-links",
|
|
|
type: "warn",
|
|
|
message:
|
|
|
"The workflow you've loaded has corrupt linking data that may be able to be fixed.",
|
|
|
actions: [
|
|
|
{
|
|
|
label: "Open fixer",
|
|
|
href: "/rgthree/link_fixer",
|
|
|
},
|
|
|
{
|
|
|
label: "Fix in place",
|
|
|
href: "/rgthree/link_fixer",
|
|
|
callback: (event) => {
|
|
|
event.stopPropagation();
|
|
|
event.preventDefault();
|
|
|
if (
|
|
|
confirm(
|
|
|
"This will attempt to fix in place. Please make sure to have a saved copy of your workflow.",
|
|
|
)
|
|
|
) {
|
|
|
try {
|
|
|
const fixBadLinksResult = fixBadLinks(graphToUse, true);
|
|
|
if (!fixBadLinksResult.hasBadLinks) {
|
|
|
rgthree.hideMessage("bad-links");
|
|
|
alert(
|
|
|
"Success! It's possible some valid links may have been affected. Please check and verify your workflow.",
|
|
|
);
|
|
|
wasLoadingAborted && app.loadGraphData(fixBadLinksResult.graph);
|
|
|
if (
|
|
|
CONFIG_SERVICE.getConfigValue("features.monitor_for_corrupt_links") ||
|
|
|
CONFIG_SERVICE.getConfigValue("features.monitor_bad_links")
|
|
|
) {
|
|
|
rgthree.monitorLinkTimeout = setTimeout(() => {
|
|
|
rgthree.monitorBadLinks();
|
|
|
}, 5000);
|
|
|
}
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error(e);
|
|
|
alert("Unsuccessful at fixing corrupt data. :(");
|
|
|
rgthree.hideMessage("bad-links");
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
},
|
|
|
],
|
|
|
});
|
|
|
}
|
|
|
} else if (
|
|
|
CONFIG_SERVICE.getConfigValue("features.monitor_for_corrupt_links") ||
|
|
|
CONFIG_SERVICE.getConfigValue("features.monitor_bad_links")
|
|
|
) {
|
|
|
rgthree.monitorLinkTimeout = setTimeout(() => {
|
|
|
rgthree.monitorBadLinks();
|
|
|
}, 5000);
|
|
|
}
|
|
|
}, 100);
|
|
|
return loadGraphData && loadGraphData.call(app, ...arguments);
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getNodeFromInitialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff(
|
|
|
node: LGraphNode,
|
|
|
): SerializedLGraphNode | null {
|
|
|
return (
|
|
|
this.initialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff?.nodes?.find(
|
|
|
(n: SerializedLGraphNode) => n.id === node.id,
|
|
|
) ?? null
|
|
|
);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async showMessage(data: RgthreeUiMessage) {
|
|
|
let container = document.querySelector(".rgthree-top-messages-container");
|
|
|
if (!container) {
|
|
|
container = document.createElement("div");
|
|
|
container.classList.add("rgthree-top-messages-container");
|
|
|
document.body.appendChild(container);
|
|
|
}
|
|
|
|
|
|
|
|
|
const dialogs = query<HTMLDialogElement>("dialog[open]");
|
|
|
if (dialogs.length) {
|
|
|
let dialog = dialogs[dialogs.length - 1]!;
|
|
|
dialog.appendChild(container);
|
|
|
dialog.addEventListener("close", (e) => {
|
|
|
document.body.appendChild(container!);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
await this.hideMessage(data.id);
|
|
|
|
|
|
const messageContainer = document.createElement("div");
|
|
|
messageContainer.setAttribute("type", data.type || "info");
|
|
|
|
|
|
const message = document.createElement("span");
|
|
|
message.innerHTML = data.message;
|
|
|
messageContainer.appendChild(message);
|
|
|
|
|
|
for (let a = 0; a < (data.actions || []).length; a++) {
|
|
|
const action = data.actions![a]!;
|
|
|
if (a > 0) {
|
|
|
const sep = document.createElement("span");
|
|
|
sep.innerHTML = " | ";
|
|
|
messageContainer.appendChild(sep);
|
|
|
}
|
|
|
|
|
|
const actionEl = document.createElement("a");
|
|
|
actionEl.innerText = action.label;
|
|
|
if (action.href) {
|
|
|
actionEl.target = "_blank";
|
|
|
actionEl.href = action.href;
|
|
|
}
|
|
|
if (action.callback) {
|
|
|
actionEl.onclick = (e) => {
|
|
|
return action.callback!(e);
|
|
|
};
|
|
|
}
|
|
|
messageContainer.appendChild(actionEl);
|
|
|
}
|
|
|
|
|
|
const messageAnimContainer = document.createElement("div");
|
|
|
messageAnimContainer.setAttribute("msg-id", data.id);
|
|
|
messageAnimContainer.appendChild(messageContainer);
|
|
|
container.appendChild(messageAnimContainer);
|
|
|
|
|
|
|
|
|
await wait(64);
|
|
|
messageAnimContainer.style.marginTop = `-${messageAnimContainer.offsetHeight}px`;
|
|
|
await wait(64);
|
|
|
messageAnimContainer.classList.add("-show");
|
|
|
|
|
|
if (data.timeout) {
|
|
|
await wait(data.timeout);
|
|
|
this.hideMessage(data.id);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async hideMessage(id: string) {
|
|
|
const msg = document.querySelector(`.rgthree-top-messages-container > [msg-id="${id}"]`);
|
|
|
if (msg?.classList.contains("-show")) {
|
|
|
msg.classList.remove("-show");
|
|
|
await wait(750);
|
|
|
}
|
|
|
msg && msg.remove();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async clearAllMessages() {
|
|
|
let container = document.querySelector(".rgthree-top-messages-container");
|
|
|
container && (container.innerHTML = "");
|
|
|
}
|
|
|
|
|
|
private clearKeydowns() {
|
|
|
this.ctrlKey = false;
|
|
|
this.altKey = false;
|
|
|
this.metaKey = false;
|
|
|
this.shiftKey = false;
|
|
|
for (const key in this.downKeys) delete this.downKeys[key];
|
|
|
this.debugRenderKeys();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleKeydown(e: KeyboardEvent) {
|
|
|
this.ctrlKey = !!e.ctrlKey;
|
|
|
this.altKey = !!e.altKey;
|
|
|
this.metaKey = !!e.metaKey;
|
|
|
this.shiftKey = !!e.shiftKey;
|
|
|
this.downKeys[e.key.toLocaleUpperCase()] = true;
|
|
|
this.debugRenderKeys();
|
|
|
this.dispatchCustomEvent("keydown", { originalEvent: e });
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleKeyup(e: KeyboardEvent) {
|
|
|
this.ctrlKey = !!e.ctrlKey;
|
|
|
this.altKey = !!e.altKey;
|
|
|
this.metaKey = !!e.metaKey;
|
|
|
this.shiftKey = !!e.shiftKey;
|
|
|
const key = e.key.toLocaleUpperCase();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (key === "META" && this.isMac) {
|
|
|
this.clearKeydowns();
|
|
|
} else {
|
|
|
delete this.downKeys[e.key.toLocaleUpperCase()];
|
|
|
this.debugRenderKeys();
|
|
|
}
|
|
|
this.dispatchCustomEvent("keyup", { originalEvent: e });
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private getKeysFromShortcut(shortcut: string | string[]) {
|
|
|
let keys;
|
|
|
if (typeof shortcut === "string") {
|
|
|
|
|
|
|
|
|
shortcut = shortcut.replace(/\s/g, "");
|
|
|
|
|
|
shortcut = shortcut.replace(/^\+/, "__PLUS__").replace(/\+\+/, "+__PLUS__");
|
|
|
keys = shortcut.split("+").map((i) => i.replace("__PLUS__", "+"));
|
|
|
} else {
|
|
|
keys = [...shortcut];
|
|
|
}
|
|
|
return keys.map((k) => k.toLocaleUpperCase());
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
areAllKeysDown(keys: string | string[]) {
|
|
|
keys = this.getKeysFromShortcut(keys);
|
|
|
return keys.every((k) => {
|
|
|
return rgthree.downKeys[k];
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
areOnlyKeysDown(keys: string | string[], alsoAllowShift = false) {
|
|
|
keys = this.getKeysFromShortcut(keys);
|
|
|
const allKeysDown = this.areAllKeysDown(keys);
|
|
|
const downKeysLength = Object.values(rgthree.downKeys).length;
|
|
|
|
|
|
if (allKeysDown && keys.length === downKeysLength) {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (alsoAllowShift && !keys.includes("SHIFT") && keys.length === downKeysLength - 1) {
|
|
|
|
|
|
|
|
|
return allKeysDown && this.areAllKeysDown(["SHIFT"]);
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private injectRgthreeCss() {
|
|
|
let link = document.createElement("link");
|
|
|
link.rel = "stylesheet";
|
|
|
link.type = "text/css";
|
|
|
link.href = "extensions/rgthree-comfy/rgthree.css";
|
|
|
document.head.appendChild(link);
|
|
|
}
|
|
|
|
|
|
setLogLevel(level?: LogLevel | string) {
|
|
|
if (typeof level === "string") {
|
|
|
level = LogLevelKeyToLogLevel[CONFIG_SERVICE.getConfigValue("log_level")];
|
|
|
}
|
|
|
if (level != null) {
|
|
|
GLOBAL_LOG_LEVEL = level;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
logParts(level: LogLevel, message?: string, ...args: any[]) {
|
|
|
return this.logger.logParts(level, message, ...args);
|
|
|
}
|
|
|
|
|
|
newLogSession(name?: string) {
|
|
|
return this.logger.newSession(name);
|
|
|
}
|
|
|
|
|
|
isDevMode() {
|
|
|
if (window.location.href.includes("rgthree-dev=false")) {
|
|
|
return false;
|
|
|
}
|
|
|
return GLOBAL_LOG_LEVEL >= LogLevel.DEBUG || window.location.href.includes("rgthree-dev");
|
|
|
}
|
|
|
|
|
|
isDebugMode() {
|
|
|
if (!this.isDevMode() || window.location.href.includes("rgthree-debug=false")) {
|
|
|
return false;
|
|
|
}
|
|
|
return window.location.href.includes("rgthree-debug");
|
|
|
}
|
|
|
|
|
|
monitorBadLinks() {
|
|
|
const badLinksFound = fixBadLinks(app.graph);
|
|
|
if (badLinksFound.hasBadLinks && !this.monitorBadLinksAlerted) {
|
|
|
this.monitorBadLinksAlerted = true;
|
|
|
alert(
|
|
|
`Problematic links just found in live data. Can you save your workflow and file a bug with ` +
|
|
|
`the last few steps you took to trigger this at ` +
|
|
|
`https://github.com/rgthree/rgthree-comfy/issues. Thank you!`,
|
|
|
);
|
|
|
} else if (!badLinksFound.hasBadLinks) {
|
|
|
|
|
|
this.monitorBadLinksAlerted = false;
|
|
|
}
|
|
|
this.monitorLinkTimeout = setTimeout(() => {
|
|
|
this.monitorBadLinks();
|
|
|
}, 5000);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function getBookmarks(): ContextMenuItem[] {
|
|
|
const graph: TLGraph = app.graph;
|
|
|
|
|
|
|
|
|
|
|
|
const bookmarks = graph._nodes
|
|
|
.filter((n): n is Bookmark => n.type === NodeTypesString.BOOKMARK)
|
|
|
.sort((a, b) => a.title.localeCompare(b.title))
|
|
|
.map((n) => ({
|
|
|
content: `[${n.shortcutKey}] ${n.title}`,
|
|
|
className: "rgthree-contextmenu-item",
|
|
|
callback: () => {
|
|
|
n.canvasToBookmark();
|
|
|
},
|
|
|
}));
|
|
|
|
|
|
return !bookmarks.length
|
|
|
? []
|
|
|
: [
|
|
|
{
|
|
|
content: "🔖 Bookmarks",
|
|
|
disabled: true,
|
|
|
className: "rgthree-contextmenu-item rgthree-contextmenu-label",
|
|
|
},
|
|
|
...bookmarks,
|
|
|
];
|
|
|
}
|
|
|
|
|
|
export const rgthree = new Rgthree();
|
|
|
|
|
|
window.rgthree = rgthree;
|
|
|
|