mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-08-03 07:26:31 +08:00
New Menu & Workflow Management (#3112)
* menu * wip * wip * wip * wip * wip * workflow saving/loading * Support inserting workflows Move buttosn to top of lists * fix session storage implement renaming * temp * refactor, better workflow instance management * wip * progress on progress * added send to workflow various fixes * Support multiple image loaders * Support dynamic size breakpoints based on content * various fixes add close unsaved warning * Add filtering tree * prevent renaming unsaved * fix zindex on hover * fix top offset * use filename as workflow name * resize on setting change * hide element until it is drawn * remove glow * Fix export name * Fix test, revert accidental changes to groupNode * Fix colors on all themes * show hover items on smaller screen (mobile) * remove debugging code * dialog fix * Dont reorder open workflows Allow elements around canvas * Toggle body display on setting change * Fix menu disappearing on chrome * Increase delay when typing, remove margin on Safari, fix dialog location * Fix overflow issue on iOS * Add reset view button Prevent view changes causing history entries * Bottom menu wip * Various fixes * Fix merge * Fix breaking old menu position * Fix merge adding restore view to loadGraphData
This commit is contained in:
@@ -5,9 +5,11 @@ import { api } from "./api.js";
|
||||
import { defaultGraph } from "./defaultGraph.js";
|
||||
import { getPngMetadata, getWebpMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
|
||||
import { addDomClippingSetting } from "./domWidget.js";
|
||||
import { createImageHost, calculateImageGrid } from "./ui/imagePreview.js"
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview"
|
||||
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 = {
|
||||
@@ -52,6 +54,12 @@ export class ComfyApp {
|
||||
constructor() {
|
||||
this.ui = new ComfyUI(this);
|
||||
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);
|
||||
|
||||
/**
|
||||
* List of extensions that are registered with the app
|
||||
@@ -1313,11 +1321,15 @@ export class ComfyApp {
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -1325,6 +1337,8 @@ export class ComfyApp {
|
||||
});
|
||||
|
||||
api.addEventListener("executed", ({ detail }) => {
|
||||
if (this.workflowManager.activePrompt ?.workflow
|
||||
&& this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return;
|
||||
const output = this.nodeOutputs[detail.node];
|
||||
if (detail.merge && output) {
|
||||
for (const k in detail.output ?? {}) {
|
||||
@@ -1433,6 +1447,11 @@ export class ComfyApp {
|
||||
});
|
||||
|
||||
await Promise.all(extensionPromises);
|
||||
try {
|
||||
this.menu.workflows.registerExtension(this);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async #migrateSettings() {
|
||||
@@ -1520,15 +1539,17 @@ export class ComfyApp {
|
||||
*/
|
||||
async setup() {
|
||||
await this.#setUser();
|
||||
await this.ui.settings.load();
|
||||
await this.#loadExtensions();
|
||||
|
||||
// Create and mount the LiteGraph in the DOM
|
||||
const mainCanvas = document.createElement("canvas")
|
||||
mainCanvas.style.touchAction = "none"
|
||||
const canvasEl = (this.canvasEl = Object.assign(mainCanvas, { id: "graph-canvas" }));
|
||||
canvasEl.tabIndex = "1";
|
||||
document.body.prepend(canvasEl);
|
||||
document.body.append(canvasEl);
|
||||
this.resizeCanvas();
|
||||
|
||||
await Promise.all([this.workflowManager.loadWorkflows(), this.ui.settings.load()]);
|
||||
await this.#loadExtensions();
|
||||
|
||||
addDomClippingSetting();
|
||||
this.#addProcessMouseHandler();
|
||||
@@ -1541,7 +1562,7 @@ export class ComfyApp {
|
||||
|
||||
this.#addAfterConfigureHandler();
|
||||
|
||||
const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph));
|
||||
this.canvas = new LGraphCanvas(canvasEl, this.graph);
|
||||
this.ctx = canvasEl.getContext("2d");
|
||||
|
||||
LiteGraph.release_link_on_empty_shows_menu = true;
|
||||
@@ -1549,19 +1570,14 @@ export class ComfyApp {
|
||||
|
||||
this.graph.start();
|
||||
|
||||
function resizeCanvas() {
|
||||
// Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845
|
||||
const scale = Math.max(window.devicePixelRatio, 1);
|
||||
const { width, height } = canvasEl.getBoundingClientRect();
|
||||
canvasEl.width = Math.round(width * scale);
|
||||
canvasEl.height = Math.round(height * scale);
|
||||
canvasEl.getContext("2d").scale(scale, scale);
|
||||
canvas.draw(true, true);
|
||||
}
|
||||
|
||||
// Ensure the canvas fills the window
|
||||
resizeCanvas();
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
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();
|
||||
@@ -1573,7 +1589,8 @@ export class ComfyApp {
|
||||
const loadWorkflow = async (json) => {
|
||||
if (json) {
|
||||
const workflow = JSON.parse(json);
|
||||
await this.loadGraphData(workflow);
|
||||
const workflowName = getStorageValue("Comfy.PreviousWorkflow");
|
||||
await this.loadGraphData(workflow, true, workflowName);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -1609,6 +1626,19 @@ export class ComfyApp {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers nodes with the graph
|
||||
*/
|
||||
@@ -1795,12 +1825,29 @@ export class ComfyApp {
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
async loadGraphData(graphData, clean = true, restore_view = true, workflow = null) {
|
||||
if (clean !== false) {
|
||||
this.clean();
|
||||
}
|
||||
@@ -1818,6 +1865,12 @@ export class ComfyApp {
|
||||
{
|
||||
graphData = structuredClone(graphData);
|
||||
}
|
||||
|
||||
try {
|
||||
this.workflowManager.setWorkflow(workflow);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
const missingNodeTypes = [];
|
||||
await this.#invokeExtensionsAsync("beforeConfigureGraph", graphData, missingNodeTypes);
|
||||
@@ -1840,6 +1893,11 @@ export class ComfyApp {
|
||||
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) {
|
||||
let errorHint = [];
|
||||
// Try extracting filename to see if it was caused by an extension script
|
||||
@@ -1927,14 +1985,17 @@ export class ComfyApp {
|
||||
this.showMissingNodesError(missingNodeTypes);
|
||||
}
|
||||
await this.#invokeExtensionsAsync("afterConfigureGraph", missingNodeTypes);
|
||||
requestAnimationFrame(() => {
|
||||
this.graph.setDirtyCanvas(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
async graphToPrompt() {
|
||||
for (const outerNode of this.graph.computeExecutionOrder(false)) {
|
||||
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
|
||||
@@ -1954,10 +2015,10 @@ export class ComfyApp {
|
||||
}
|
||||
}
|
||||
|
||||
const workflow = this.graph.serialize();
|
||||
const workflow = graph.serialize();
|
||||
const output = {};
|
||||
// Process nodes in order of execution
|
||||
for (const outerNode of this.graph.computeExecutionOrder(false)) {
|
||||
for (const outerNode of graph.computeExecutionOrder(false)) {
|
||||
const skipNode = outerNode.mode === 2 || outerNode.mode === 4;
|
||||
const innerNodes = (!skipNode && outerNode.getInnerNodes) ? outerNode.getInnerNodes() : [outerNode];
|
||||
for (const node of innerNodes) {
|
||||
@@ -2049,13 +2110,14 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
// Remove inputs connected to removed nodes
|
||||
|
||||
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];
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2123,6 +2185,14 @@ export class ComfyApp {
|
||||
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) {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const formattedError = this.#formatPromptError(error)
|
||||
@@ -2155,6 +2225,7 @@ export class ComfyApp {
|
||||
this.#processingQueue = false;
|
||||
}
|
||||
api.dispatchEvent(new CustomEvent("promptQueued", { detail: { number, batchCount } }));
|
||||
return !this.lastNodeErrors;
|
||||
}
|
||||
|
||||
showErrorOnFileLoad(file) {
|
||||
@@ -2170,14 +2241,24 @@ export class ComfyApp {
|
||||
* @param {File} file
|
||||
*/
|
||||
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);
|
||||
if (file.type === "image/png") {
|
||||
const pngInfo = await getPngMetadata(file);
|
||||
if (pngInfo?.workflow) {
|
||||
await this.loadGraphData(JSON.parse(pngInfo.workflow));
|
||||
await this.loadGraphData(JSON.parse(pngInfo.workflow), true, true, fileName);
|
||||
} else if (pngInfo?.prompt) {
|
||||
this.loadApiJson(JSON.parse(pngInfo.prompt));
|
||||
this.loadApiJson(JSON.parse(pngInfo.prompt), fileName);
|
||||
} else if (pngInfo?.parameters) {
|
||||
importA1111(this.graph, pngInfo.parameters);
|
||||
this.changeWorkflow(() => {
|
||||
importA1111(this.graph, pngInfo.parameters);
|
||||
}, fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file);
|
||||
}
|
||||
@@ -2188,9 +2269,9 @@ export class ComfyApp {
|
||||
const prompt = pngInfo?.prompt || pngInfo?.Prompt;
|
||||
|
||||
if (workflow) {
|
||||
this.loadGraphData(JSON.parse(workflow));
|
||||
this.loadGraphData(JSON.parse(workflow), true, true, fileName);
|
||||
} else if (prompt) {
|
||||
this.loadApiJson(JSON.parse(prompt));
|
||||
this.loadApiJson(JSON.parse(prompt), fileName);
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file);
|
||||
}
|
||||
@@ -2201,16 +2282,16 @@ export class ComfyApp {
|
||||
if (jsonContent?.templates) {
|
||||
this.loadTemplateData(jsonContent);
|
||||
} else if(this.isApiJson(jsonContent)) {
|
||||
this.loadApiJson(jsonContent);
|
||||
this.loadApiJson(jsonContent, fileName);
|
||||
} else {
|
||||
await this.loadGraphData(jsonContent);
|
||||
await this.loadGraphData(jsonContent, true, fileName);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) {
|
||||
const info = await getLatentMetadata(file);
|
||||
if (info.workflow) {
|
||||
await this.loadGraphData(JSON.parse(info.workflow));
|
||||
await this.loadGraphData(JSON.parse(info.workflow), true, fileName);
|
||||
} else if (info.prompt) {
|
||||
this.loadApiJson(JSON.parse(info.prompt));
|
||||
} else {
|
||||
@@ -2225,7 +2306,7 @@ export class ComfyApp {
|
||||
return Object.values(data).every((v) => v.class_type);
|
||||
}
|
||||
|
||||
loadApiJson(apiData) {
|
||||
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);
|
||||
@@ -2240,40 +2321,42 @@ export class ComfyApp {
|
||||
node.id = isNaN(+id) ? id : +id;
|
||||
node.title = data._meta?.title ?? node.title
|
||||
app.graph.add(node);
|
||||
graph.add(node);
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
app.graph.arrange();
|
||||
}, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user