From c253e0956f8888a3f853425d1cb26c1ee768a77e Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 20 May 2023 14:58:35 -0500 Subject: [PATCH 01/15] Application sidebar --- src/lib/components/ComfyApp.svelte | 306 ++++++++++++++++++-------- src/lib/components/Sidebar.svelte | 154 +++++++++++++ src/lib/components/SidebarItem.svelte | 45 ++++ 3 files changed, 412 insertions(+), 93 deletions(-) create mode 100644 src/lib/components/Sidebar.svelte create mode 100644 src/lib/components/SidebarItem.svelte diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index e3f11e2..83bc0ef 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -1,4 +1,5 @@ @@ -222,86 +228,114 @@
- - - -
-
- {#if $layoutState.attrs.queuePromptButtonName != ""} - - {/if} - - - - - - - - - - - - - - - UI Edit mode - - - - Theme - - -
-
- -
+ + + +
@@ -309,37 +343,123 @@ diff --git a/src/lib/components/SidebarItem.svelte b/src/lib/components/SidebarItem.svelte new file mode 100644 index 0000000..40246e3 --- /dev/null +++ b/src/lib/components/SidebarItem.svelte @@ -0,0 +1,45 @@ + + +
+ + + +
+ + From 3b9017dfadd08d7f0b9ab906f97d2384d8dddb03 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 20 May 2023 15:42:43 -0500 Subject: [PATCH 02/15] Actual loading screen --- src/lib/components/ComfyApp.svelte | 479 +----------------- src/lib/components/ComfyApp.ts | 11 + src/lib/components/ComfyWorkflowsView.svelte | 484 +++++++++++++++++++ src/lib/components/SidebarItem.svelte | 13 +- 4 files changed, 528 insertions(+), 459 deletions(-) create mode 100644 src/lib/components/ComfyWorkflowsView.svelte diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index 83bc0ef..9da5eb7 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -1,46 +1,22 @@ @@ -214,312 +58,39 @@ {/if} - ($a1111Prompt = null)}> -
-

A1111 Prompt Details

-
- -
- -
-
+
-
+
-
- - - - - - - - - - - - - - - - - - -
-
- txt2img - -
-
- img2img - -
-
- asdflkj - -
-
- asdkajw - -
-
-
-
-
- {#if $layoutState.attrs.queuePromptButtonName != ""} - - {/if} - - - - - - - - - - - - - - - UI Edit mode - - - - Theme - - -
-
- -
-
-
-
+
-
diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index d49fbe2..ebf3381 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -128,6 +128,17 @@ type BackendComboNode = { backendNode: ComfyBackendNode } +type CanvasState = { + canvasEl: HTMLCanvasElement, + canvasCtx: CanvasRenderingContext2D, + canvas: ComfyGraphCanvas, +} + +type WorkflowState = { + title: string, + graph: ComfyGraph, +} + export default class ComfyApp { api: ComfyAPI; rootEl: HTMLDivElement | null = null; diff --git a/src/lib/components/ComfyWorkflowsView.svelte b/src/lib/components/ComfyWorkflowsView.svelte new file mode 100644 index 0000000..ed6dcbc --- /dev/null +++ b/src/lib/components/ComfyWorkflowsView.svelte @@ -0,0 +1,484 @@ + + +
+ + + + + + + + + + + + + + + + + + +
+
+ txt2img + +
+
+ img2img + +
+
+ asdflkj + +
+
+ asdkajw + +
+
+
+
+
+ {#if $layoutState.attrs.queuePromptButtonName != ""} + + {/if} + + + + + + + + + + + + + + + UI Edit mode + + + + Theme + + +
+
+ +
+
+
+
+ + +{#if appSetupPromise} + {#await appSetupPromise} +
+ Loading... +
+ {:catch error} +
+
+ Error loading app +
+
{error}
+
+ {/await} +{/if} + + diff --git a/src/lib/components/SidebarItem.svelte b/src/lib/components/SidebarItem.svelte index 40246e3..9c71fa4 100644 --- a/src/lib/components/SidebarItem.svelte +++ b/src/lib/components/SidebarItem.svelte @@ -28,18 +28,21 @@
- +
- +
From a29afab9a75fc56b992583ce7418d00508c49ffb Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 20 May 2023 17:09:00 -0500 Subject: [PATCH 03/15] Prepare for multiple workflows --- src/lib/ComfyGraphCanvas.ts | 3 +- src/lib/ComfyNodeDef.ts | 13 +- src/lib/components/ComfyApp.ts | 391 +++++++++++++------ src/lib/components/ComfyWorkflowsView.svelte | 6 + src/lib/defaultGraph.ts | 11 +- src/lib/nodes/ComfyActionNodes.ts | 8 +- src/lib/nodes/ComfyBackendNode.ts | 4 +- src/lib/nodes/ComfyGraphNode.ts | 4 +- src/lib/stores/queueState.ts | 47 ++- src/lib/utils.ts | 17 +- src/lib/widgets.ts | 19 +- src/mobile/routes/graph.svelte | 6 +- 12 files changed, 375 insertions(+), 154 deletions(-) diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index f09953e..17e23ba 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -20,6 +20,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas { constructor( app: ComfyApp, + graph: LGraph, canvas: HTMLCanvasElement | string, options: { skip_render?: boolean; @@ -28,7 +29,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas { viewport?: Vector4; } = {} ) { - super(canvas, app.lGraph, options); + super(canvas, graph, options); this.app = app; this._unsubscribe = selectionState.subscribe(ss => { for (const node of Object.values(this.selected_nodes)) { diff --git a/src/lib/ComfyNodeDef.ts b/src/lib/ComfyNodeDef.ts index de281ab..7a9a25d 100644 --- a/src/lib/ComfyNodeDef.ts +++ b/src/lib/ComfyNodeDef.ts @@ -27,7 +27,18 @@ export type ComfyNodeDefInput = [ComfyNodeDefInputType, ComfyNodeDefInputOptions export type ComfyNodeDefInputType = any[] | keyof typeof ComfyWidgets | string export type ComfyNodeDefInputOptions = { - forceInput?: boolean + forceInput?: boolean; + + // NOTE: For COMBO type inputs, the default value is always the first entry the list. + default?: any, + + // INT/FLOAT options + min?: number, + max?: number, + step?: number, + + // STRING options + multiline?: boolean, } // TODO if/when comfy refactors diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index ebf3381..7a78573 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -1,4 +1,4 @@ -import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex } from "@litegraph-ts/core"; +import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex, type UUID } from "@litegraph-ts/core"; import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core"; import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api" import { getPngMetadata, importA1111 } from "$lib/pnginfo"; @@ -21,7 +21,7 @@ import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode"; import queueState from "$lib/stores/queueState"; import { type SvelteComponentDev } from "svelte/internal"; import type IComfyInputSlot from "$lib/IComfyInputSlot"; -import type { SerializedLayoutState } from "$lib/stores/layoutState"; +import type { LayoutState, SerializedLayoutState, WritableLayoutStateStore } from "$lib/stores/layoutState"; import layoutState from "$lib/stores/layoutState"; import { toast } from '@zerodevx/svelte-toast' import ComfyGraph from "$lib/ComfyGraph"; @@ -33,7 +33,7 @@ import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, wor import notify from "$lib/notify"; import configState from "$lib/stores/configState"; import { blankGraph } from "$lib/defaultGraph"; -import type { ComfyExecutionResult } from "$lib/utils"; +import type { SerializedPromptOutput } from "$lib/utils"; import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer"; import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef"; import { ComfyComboNode } from "$lib/nodes/widgets"; @@ -41,6 +41,7 @@ import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111"; import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt"; import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt"; import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer"; +import { v4 as uuidv4 } from "uuid"; export const COMFYBOX_SERIAL_VERSION = 1; @@ -51,12 +52,11 @@ if (typeof window !== "undefined") { /* * Queued prompt that hasn't been sent to the backend yet. - * TODO: Assumes the currently active graph will be serialized, needs to change - * for multiple loaded workflow support */ -type QueueItem = { +type PromptQueueItem = { num: number, batchCount: number + workflow: ComfyWorkflow } export type A1111PromptAndInfo = { @@ -111,7 +111,7 @@ export type SerializedPrompt = { /* * Outputs for each node. */ -export type SerializedPromptOutputs = Record +export type SerializedPromptOutputs = Record export type Progress = { value: number, @@ -134,20 +134,156 @@ type CanvasState = { canvas: ComfyGraphCanvas, } -type WorkflowState = { - title: string, - graph: ComfyGraph, +type ActiveCanvas = { + canvas: LGraphCanvas | null; + canvasHandler: () => void | null; + state: SerializedGraphCanvasState; +} + +export type SerializedWorkflowState = { + graph: SerializedLGraph, + layout: SerializedLayoutState +} + +/* + * ID for an opened workflow. + * + * Unlike NodeID and PromptID, these are *not* saved to the workflow itself. + * They are only used for identifying an open workflow in the program. If the + * workflow is closed and reopened, a different workflow ID will be assigned to + * it. + */ +export type WorkflowInstID = UUID; + +export class ComfyWorkflow { + /* + * Used for uniquely identifying the instance of the opened workflow in the frontend. + */ + id: WorkflowInstID; + title: string; + graph: ComfyGraph; + layout: WritableLayoutStateStore; + + canvases: Record = {}; + + constructor(title: string, graph: ComfyGraph, layout: WritableLayoutStateStore) { + this.id = uuidv4(); + this.title = title; + this.layout = layout; + this.graph = graph; + } + + start(key: string, canvas: ComfyGraphCanvas) { + if (this.canvases[key] != null) + throw new Error(`This workflow is already being displayed on canvas ${key}`) + + const canvasHandler = () => canvas.draw(true); + + this.canvases[key] = { + canvas, + canvasHandler, + state: { + // TODO + offset: [0, 0], + scale: 1 + } + } + + this.graph.attachCanvas(canvas); + this.graph.eventBus.on("afterExecute", canvasHandler) + + if (Object.keys(this.canvases).length === 1) + this.graph.start(); + } + + stop(key: string) { + const canvas = this.canvases[key] + if (canvas == null) + throw new Error(`This workflow is not being displayed on canvas ${key}`) + + this.graph.detachCanvas(canvas.canvas); + this.graph.eventBus.removeListener("afterExecute", canvas.canvasHandler) + + delete this.canvases[key] + + if (Object.keys(this.canvases).length === 0) + this.graph.stop(); + } + + stopAll() { + for (const key of Object.keys(this.canvases)) + this.stop(key) + this.graph.stop() + } + + serialize(): SerializedWorkflowState { + const graph = this.graph; + + const serializedGraph = graph.serialize() + const serializedLayout = this.layout.serialize() + + return { + graph: serializedGraph, + layout: serializedLayout + } + } + + static deserialize(data: SerializedWorkflowState): ComfyWorkflow { + const layout = layoutState; // TODO + // Ensure loadGraphData does not trigger any state changes in layoutState + // (isConfiguring is set to true here) + // lGraph.configure will add new nodes, triggering onNodeAdded, but we + // want to restore the layoutState ourselves + layout.onStartConfigure(); + + // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now + for (let n of data.graph.nodes) { + if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader"; + } + + const graph = new ComfyGraph(); + graph.configure(data.graph); + + for (const node of 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; + // this.#invokeExtensions("loadedGraphNode", node); + } + + // Now restore the layout + // Subsequent added nodes will add the UI data to layoutState + // TODO + layout.deserialize(data.layout, graph) + + return new ComfyWorkflow("Workflow X", graph, layout); + } } export default class ComfyApp { api: ComfyAPI; + rootEl: HTMLDivElement | null = null; canvasEl: HTMLCanvasElement | null = null; canvasCtx: CanvasRenderingContext2D | null = null; - lGraph: ComfyGraph | null = null; lCanvas: ComfyGraphCanvas | null = null; - dropZone: HTMLElement | null = null; - nodeOutputs: Record = {}; + + openedWorkflows: ComfyWorkflow[] = []; + openedWorkflowsByID: Record = {}; + activeWorkflowIdx: number = -1; + + get activeWorkflow(): ComfyWorkflow | null { + return this.openedWorkflows[this.activeWorkflowIdx] + } + + get activeGraph(): ComfyGraph | null { + return this.activeWorkflow?.graph; + } + + getWorkflow(id: WorkflowInstID): ComfyWorkflow | null { + return this.openedWorkflowsByID[id]; + } shiftDown: boolean = false; ctrlDown: boolean = false; @@ -155,7 +291,7 @@ export default class ComfyApp { alreadySetup: Writable = writable(false); a1111Prompt: Writable = writable(null); - private queueItems: QueueItem[] = []; + private queueItems: PromptQueueItem[] = []; private processingQueue: boolean = false; private promptSerializer: ComfyPromptSerializer; private stdPromptSerializer: ComfyBoxStdPromptSerializer; @@ -176,8 +312,7 @@ export default class ComfyApp { this.rootEl = document.getElementById("app-root") as HTMLDivElement; this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement; - this.lGraph = new ComfyGraph(); - this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl); + this.lCanvas = new ComfyGraphCanvas(this, null, this.canvasEl); this.canvasCtx = this.canvasEl.getContext("2d"); const uiUnlocked = get(uiState).uiUnlocked; @@ -193,7 +328,7 @@ export default class ComfyApp { const json = localStorage.getItem("workflow"); if (json) { const state = JSON.parse(json) as SerializedAppState; - await this.deserialize(state) + await this.openWorkflow(state) restored = true; } } catch (err) { @@ -236,6 +371,24 @@ export default class ComfyApp { this.lCanvas.draw(true, true); } + serialize(): SerializedAppState { + const workflow = this.activeWorkflow + if (workflow == null) + throw new Error("No workflow active!") + + const { graph, layout } = workflow.serialize(); + const canvas = this.lCanvas.serialize(); + + return { + createdBy: "ComfyBox", + version: COMFYBOX_SERIAL_VERSION, + commitHash: __GIT_COMMIT_HASH__, + workflow: graph, + layout, + canvas + } + } + saveStateToLocalStorage() { try { uiState.update(s => { s.isSavingToLocalStorage = true; return s; }) @@ -362,8 +515,12 @@ export default class ComfyApp { } catch (error) { } } - if (workflow && workflow.version && workflow.nodes && workflow.extra) { - this.loadGraphData(workflow); + if (workflow && workflow.createdBy === "ComfyBox") { + this.openWorkflow(workflow); + } + else { + // TODO handle vanilla workflows + throw new Error("Workflow was not in ComfyBox format!") } }); } @@ -386,21 +543,27 @@ export default class ComfyApp { this.api.addEventListener("progress", (progress: Progress) => { queueState.progressUpdated(progress); - this.lGraph.setDirtyCanvas(true, false); + this.activeGraph?.setDirtyCanvas(true, false); // TODO PromptID }); this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: ComfyNodeID | null) => { - queueState.executingUpdated(promptID, nodeID); - this.lGraph.setDirtyCanvas(true, false); + const queueEntry = queueState.executingUpdated(promptID, nodeID); + if (queueEntry != null) { + const workflow = this.getWorkflow(queueEntry.workflowID) + workflow?.graph.setDirtyCanvas(true, false); + } }); - this.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => { - this.nodeOutputs[nodeID] = output; - const node = this.lGraph.getNodeByIdRecursive(nodeID) as ComfyGraphNode; - if (node?.onExecuted) { - node.onExecuted(output); + this.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutput) => { + const queueEntry = queueState.onExecuted(promptID, nodeID, output) + if (queueEntry != null) { + const workflow = this.getWorkflow(queueEntry.workflowID) + workflow?.graph.setDirtyCanvas(true, false); + const node = workflow?.graph.getNodeByIdRecursive(nodeID) as ComfyGraphNode; + if (node?.onExecuted) { + node.onExecuted(output); + } } - queueState.onExecuted(promptID, nodeID, output) }); this.api.addEventListener("execution_start", (promptID: PromptID) => { @@ -467,46 +630,68 @@ export default class ComfyApp { setColor(BuiltInSlotType.ACTION, "lightseagreen") } - serialize(): SerializedAppState { - const graph = this.lGraph; - - const serializedGraph = graph.serialize() - const serializedLayout = layoutState.serialize() - const serializedCanvas = this.lCanvas.serialize(); - - return { - createdBy: "ComfyBox", - version: COMFYBOX_SERIAL_VERSION, - workflow: serializedGraph, - layout: serializedLayout, - canvas: serializedCanvas - } + createNewWorkflow(): ComfyWorkflow { + // TODO remove + const workflow = ComfyWorkflow.deserialize({ graph: blankGraph.workflow, layout: blankGraph.layout }) + this.openedWorkflows.push(workflow); + this.setActiveWorkflow(this.openedWorkflows.length - 1) + return workflow; } - async deserialize(data: SerializedAppState) { + async openWorkflow(data: SerializedAppState): Promise { if (data.version !== COMFYBOX_SERIAL_VERSION) { throw `Invalid ComfyBox saved data format: ${data.version}` } - // Ensure loadGraphData does not trigger any state changes in layoutState - // (isConfiguring is set to true here) - // lGraph.configure will add new nodes, triggering onNodeAdded, but we - // want to restore the layoutState ourselves - layoutState.onStartConfigure(); + this.clean(); - this.loadGraphData(data.workflow) - - // Now restore the layout - // Subsequent added nodes will add the UI data to layoutState - layoutState.deserialize(data.layout, this.lGraph) + const workflow = ComfyWorkflow.deserialize({ graph: data.workflow, layout: data.layout }) // Restore canvas offset/zoom this.lCanvas.deserialize(data.canvas) - await this.refreshComboInNodes(); + await this.refreshComboInNodes(workflow); - this.lGraph.start(); - this.lGraph.eventBus.on("afterExecute", () => this.lCanvas.draw(true)) + this.openedWorkflows.push(workflow); + this.setActiveWorkflow(this.openedWorkflows.length - 1) + + return workflow; + } + + closeWorkflow(index: number) { + if (index < 0 || index >= this.openedWorkflows.length) + return; + + const workflow = this.openedWorkflows[index]; + workflow.stopAll(); + + this.openedWorkflows.splice(index, 1) + this.setActiveWorkflow(0); + } + + closeAllWorkflows() { + while (this.openedWorkflows.length > 0) + this.closeWorkflow(0) + } + + setActiveWorkflow(index: number) { + if (this.openedWorkflows.length === 0) { + this.activeWorkflowIdx = -1; + return; + } + + if (index < 0 || index >= this.openedWorkflows.length || this.activeWorkflowIdx === index) + return; + + if (this.activeWorkflow != null) + this.activeWorkflow.stop("app") + + const workflow = this.openedWorkflows[index] + this.activeWorkflowIdx = index; + + console.warn("START") + workflow.start("app", this.lCanvas); + this.lCanvas.deserialize(workflow.canvases["app"].state) } async initDefaultGraph() { @@ -520,50 +705,14 @@ export default class ComfyApp { notify(`Failed to load default graph: ${error}`, { type: "error" }) state = structuredClone(blankGraph) } - await this.deserialize(state) - } - - /** - * Populates the graph with the specified workflow data - * @param {*} graphData A serialized graph object - */ - loadGraphData(graphData: SerializedLGraph) { - this.clean(); - - // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now - for (let n of graphData.nodes) { - if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader"; - } - - this.lGraph.configure(graphData); - - for (const node of this.lGraph._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; - // this.#invokeExtensions("loadedGraphNode", node); - } + await this.openWorkflow(state) } clear() { this.clean(); - const blankGraph: SerializedLGraph = { - last_node_id: 0, - last_link_id: 0, - nodes: [], - links: [], - groups: [], - config: {}, - extra: {}, - version: 0 - } - - layoutState.onStartConfigure(); this.lCanvas.closeAllSubgraphs(); - this.lGraph.configure(blankGraph) - layoutState.initDefaultLayout(); + this.closeAllWorkflows(); uiState.update(s => { s.uiUnlocked = true; s.uiEditMode = "widgets"; @@ -572,7 +721,10 @@ export default class ComfyApp { } runDefaultQueueAction() { - for (const node of this.lGraph.iterateNodesInOrderRecursive()) { + if (this.activeWorkflow == null) + return; + + for (const node of this.activeGraph.iterateNodesInOrderRecursive()) { if ("onDefaultQueueAction" in node) { (node as ComfyGraphNode).onDefaultQueueAction() } @@ -614,12 +766,17 @@ export default class ComfyApp { * Converts the current graph workflow for sending to the API * @returns The workflow and node links */ - graphToPrompt(tag: string | null = null): SerializedPrompt { - return this.promptSerializer.serialize(this.lGraph, tag) + graphToPrompt(workflow: ComfyWorkflow, tag: string | null = null): SerializedPrompt { + return this.promptSerializer.serialize(workflow.graph, tag) } async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) { - this.queueItems.push({ num, batchCount }); + if (this.activeWorkflow === null) { + notify("No workflow is opened!", { type: "error" }) + return; + } + + this.queueItems.push({ num, batchCount, workflow: this.activeWorkflow }); // Only have one action process the items so each one gets a unique seed correctly if (this.processingQueue) { @@ -630,13 +787,15 @@ export default class ComfyApp { tag = null; this.processingQueue = true; + let workflow; + try { while (this.queueItems.length) { - ({ num, batchCount } = this.queueItems.pop()); + ({ num, batchCount, workflow } = this.queueItems.pop()); console.debug(`Queue get! ${num} ${batchCount} ${tag}`); const thumbnails = [] - for (const node of this.lGraph.iterateNodesInOrderRecursive()) { + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { if (node.mode !== NodeMode.ALWAYS || (tag != null && Array.isArray(node.properties.tags) @@ -651,7 +810,7 @@ export default class ComfyApp { } for (let i = 0; i < batchCount; i++) { - for (const node of this.lGraph.iterateNodesInOrderRecursive()) { + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { if (node.mode !== NodeMode.ALWAYS) continue; @@ -660,9 +819,9 @@ export default class ComfyApp { } } - const p = this.graphToPrompt(tag); + const p = this.graphToPrompt(workflow, tag); const l = layoutState.serialize(); - console.debug(graphToGraphVis(this.lGraph)) + console.debug(graphToGraphVis(workflow.graph)) console.debug(promptToGraphVis(p)) const stdPrompt = this.stdPromptSerializer.serialize(p); @@ -692,7 +851,7 @@ export default class ComfyApp { error = response.error; } else { - queueState.afterQueued(response.promptID, num, p.output, extraData) + queueState.afterQueued(workflow.id, response.promptID, num, p.output, extraData) } } catch (err) { error = err?.toString(); @@ -701,13 +860,13 @@ export default class ComfyApp { if (error != null) { const mes: string = error; notify(`Error queuing prompt:\n${mes}`, { type: "error" }) - console.error(graphToGraphVis(this.lGraph)) + console.error(graphToGraphVis(workflow.graph)) console.error(promptToGraphVis(p)) console.error("Error queuing prompt", error, num, p) break; } - for (const node of this.lGraph.iterateNodesInOrderRecursive()) { + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { if ("afterQueued" in node) { (node as ComfyGraphNode).afterQueued(p, tag); } @@ -730,7 +889,7 @@ export default class ComfyApp { const pngInfo = await getPngMetadata(file); if (pngInfo) { if (pngInfo.comfyBoxConfig) { - this.deserialize(JSON.parse(pngInfo.comfyBoxConfig)); + await this.openWorkflow(JSON.parse(pngInfo.comfyBoxConfig)); } else if (pngInfo.parameters) { const parsed = parseA1111(pngInfo.parameters) if ("error" in parsed) { @@ -752,8 +911,8 @@ export default class ComfyApp { } } else if (file.type === "application/json" || file.name.endsWith(".json")) { const reader = new FileReader(); - reader.onload = () => { - this.deserialize(JSON.parse(reader.result as string)); + reader.onload = async () => { + await this.openWorkflow(JSON.parse(reader.result as string)); }; reader.readAsText(file); } @@ -772,7 +931,7 @@ export default class ComfyApp { /** * Refresh combo list on whole nodes */ - async refreshComboInNodes(flashUI: boolean = false) { + async refreshComboInNodes(workflow: ComfyWorkflow, flashUI: boolean = false) { const defs = await this.api.getNodeDefs(); const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => { @@ -816,7 +975,7 @@ export default class ComfyApp { return result } - for (const node of this.lGraph.iterateNodesInOrderRecursive()) { + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { if (!isActiveBackendNode(node)) continue; @@ -838,7 +997,7 @@ export default class ComfyApp { console.debug("[refreshComboInNodes] found:", backendUpdatedCombos.length, backendUpdatedCombos) // Mark combo nodes without backend configs as being loaded already. - for (const node of this.lGraph.iterateNodesOfClassRecursive(ComfyComboNode)) { + for (const node of workflow.graph.iterateNodesOfClassRecursive(ComfyComboNode)) { if (backendUpdatedCombos[node.id] != null) { continue; } @@ -870,12 +1029,15 @@ export default class ComfyApp { // Load definitions from the backend. for (const { comboNode, comfyInput, backendNode } of Object.values(backendUpdatedCombos)) { const def = defs[backendNode.type]; - const rawValues = def["input"]["required"][comfyInput.name][0]; + const [rawValues, opts] = def.input.required[comfyInput.name]; console.debug("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, rawValues.length) comboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false }) - comboNode.formatValues(rawValues as string[], true) + const values = rawValues as string[] + const defaultValue = rawValues[0]; + + comboNode.formatValues(values, defaultValue, true) if (!rawValues?.includes(get(comboNode.value))) { comboNode.setValue(rawValues[0], comfyInput.config.defaultValue) } @@ -886,7 +1048,6 @@ export default class ComfyApp { * Clean current state */ clean() { - this.nodeOutputs = {}; this.a1111Prompt.set(null); } } diff --git a/src/lib/components/ComfyWorkflowsView.svelte b/src/lib/components/ComfyWorkflowsView.svelte index ed6dcbc..8604dcc 100644 --- a/src/lib/components/ComfyWorkflowsView.svelte +++ b/src/lib/components/ComfyWorkflowsView.svelte @@ -275,6 +275,12 @@ Error loading app
{error}
+ {#if error.stack} + {@const lines = error.stack.split("\n")} + {#each lines as line} +
{line}
+ {/each} + {/if}
{/await} {/if} diff --git a/src/lib/defaultGraph.ts b/src/lib/defaultGraph.ts index a296a76..9becb14 100644 --- a/src/lib/defaultGraph.ts +++ b/src/lib/defaultGraph.ts @@ -1,4 +1,4 @@ -import type SerializedAppState from "./ComfyApp" +import type { SerializedAppState } from "./components/ComfyApp" const blankGraph: SerializedAppState = { createdBy: "ComfyBox", @@ -13,7 +13,14 @@ const blankGraph: SerializedAppState = { extra: {}, version: 0 }, - panes: {} + layout: { + root: null, + allItems: {}, + attrs: { + queuePromptButtonName: "Queue Prompt", + queuePromptButtonRunWorkflow: true + } + } } export { blankGraph } diff --git a/src/lib/nodes/ComfyActionNodes.ts b/src/lib/nodes/ComfyActionNodes.ts index 9e083a3..ba08046 100644 --- a/src/lib/nodes/ComfyActionNodes.ts +++ b/src/lib/nodes/ComfyActionNodes.ts @@ -8,7 +8,7 @@ import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode" import type { ComfyWidgetNode } from "$lib/nodes/widgets"; import type { NotifyOptions } from "$lib/notify"; import type { FileData as GradioFileData } from "@gradio/upload"; -import { type ComfyExecutionResult, type ComfyImageLocation, convertComfyOutputToGradio, type ComfyUploadImageAPIResponse, parseWhateverIntoComfyImageLocations } from "$lib/utils"; +import { type SerializedPromptOutput, type ComfyImageLocation, convertComfyOutputToGradio, type ComfyUploadImageAPIResponse, parseWhateverIntoComfyImageLocations } from "$lib/utils"; export class ComfyQueueEvents extends ComfyGraphNode { static slotLayout: SlotLayout = { @@ -63,7 +63,7 @@ LiteGraph.registerNodeType({ }) export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties { - images: ComfyExecutionResult | null + images: SerializedPromptOutput | null } export class ComfyStoreImagesAction extends ComfyGraphNode { @@ -90,7 +90,7 @@ export class ComfyStoreImagesAction extends ComfyGraphNode { if (action !== "store" || !param || !("images" in param)) return; - this.setProperty("images", param as ComfyExecutionResult) + this.setProperty("images", param as SerializedPromptOutput) this.setOutputData(0, this.properties.images) } } @@ -223,7 +223,7 @@ export class ComfyNotifyAction extends ComfyGraphNode { // native notifications. if (param != null && typeof param === "object") { if ("images" in param) { - const output = param as ComfyExecutionResult; + const output = param as SerializedPromptOutput; const converted = convertComfyOutputToGradio(output); if (converted.length > 0) options.imageUrl = converted[0].data; diff --git a/src/lib/nodes/ComfyBackendNode.ts b/src/lib/nodes/ComfyBackendNode.ts index c899b19..4c8ac2f 100644 --- a/src/lib/nodes/ComfyBackendNode.ts +++ b/src/lib/nodes/ComfyBackendNode.ts @@ -6,7 +6,7 @@ import { BuiltInSlotShape, BuiltInSlotType, type SerializedLGraphNode } from "@l import type IComfyInputSlot from "$lib/IComfyInputSlot"; import type { ComfyInputConfig } from "$lib/IComfyInputSlot"; import { iterateNodeDefOutputs, type ComfyNodeDef, iterateNodeDefInputs } from "$lib/ComfyNodeDef"; -import type { ComfyExecutionResult } from "$lib/utils"; +import type { SerializedPromptOutput } from "$lib/utils"; /* * Base class for any node with configuration sent by the backend. @@ -111,7 +111,7 @@ export class ComfyBackendNode extends ComfyGraphNode { } } - override onExecuted(outputData: ComfyExecutionResult) { + override onExecuted(outputData: SerializedPromptOutput) { console.warn("onExecuted outputs", outputData) this.triggerSlot(0, outputData) } diff --git a/src/lib/nodes/ComfyGraphNode.ts b/src/lib/nodes/ComfyGraphNode.ts index 1fb1883..6670a73 100644 --- a/src/lib/nodes/ComfyGraphNode.ts +++ b/src/lib/nodes/ComfyGraphNode.ts @@ -3,7 +3,7 @@ import type { SerializedPrompt } from "$lib/components/ComfyApp"; import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core"; import type { SvelteComponentDev } from "svelte/internal"; import type { ComfyWidgetNode } from "$lib/nodes/widgets"; -import type { ComfyExecutionResult, ComfyImageLocation } from "$lib/utils" +import type { SerializedPromptOutput, ComfyImageLocation } from "$lib/utils" import type IComfyInputSlot from "$lib/IComfyInputSlot"; import uiState from "$lib/stores/uiState"; import { get } from "svelte/store"; @@ -48,7 +48,7 @@ export default class ComfyGraphNode extends LGraphNode { * Triggered when the backend sends a finished output back with this node's ID. * Valid for output nodes like SaveImage and PreviewImage. */ - onExecuted?(output: ComfyExecutionResult): void; + onExecuted?(output: SerializedPromptOutput): void; /* * When a prompt is queued, this will be called on the node if it can diff --git a/src/lib/stores/queueState.ts b/src/lib/stores/queueState.ts index ebe8a3d..6c085f2 100644 --- a/src/lib/stores/queueState.ts +++ b/src/lib/stores/queueState.ts @@ -1,5 +1,5 @@ import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, ComfyNodeID, PromptID } from "$lib/api"; -import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp"; +import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs, WorkflowInstID } from "$lib/components/ComfyApp"; import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes"; import notify from "$lib/notify"; import { get, writable, type Writable } from "svelte/store"; @@ -11,12 +11,13 @@ type QueueStateOps = { historyUpdated: (resp: ComfyAPIHistoryResponse) => void, statusUpdated: (status: ComfyAPIStatusResponse | null) => void, executionStart: (promptID: PromptID) => void, - executingUpdated: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => void, + executingUpdated: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => QueueEntry | null; executionCached: (promptID: PromptID, nodes: ComfyNodeID[]) => void, executionError: (promptID: PromptID, message: string) => void, progressUpdated: (progress: Progress) => void - afterQueued: (promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void - onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => void + getQueueEntry: (promptID: PromptID) => QueueEntry | null; + afterQueued: (workflowID: WorkflowInstID, promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void + onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => QueueEntry | null } /* @@ -36,6 +37,8 @@ export type QueueEntry = { /*** Data not sent by ComfyUI's API, lost on page refresh ***/ + /* Workflow tab that sent the prompt. */ + workflowID?: WorkflowInstID, /* Prompt outputs, collected while the prompt is still executing */ outputs: SerializedPromptOutputs, /* Nodes of the workflow that have finished running so far. */ @@ -150,6 +153,21 @@ function statusUpdated(status: ComfyAPIStatusResponse | null) { }) } +function getQueueEntry(promptID: PromptID): QueueEntry | null { + const state = get(store); + + let found = get(state.queuePending).find(e => e.promptID === promptID) + if (found != null) return found; + + found = get(state.queueRunning).find(e => e.promptID === promptID) + if (found != null) return found; + + let foundCompleted = get(state.queueCompleted).find(e => e.entry.promptID === promptID) + if (foundCompleted != null) return foundCompleted.entry; + + return null; +} + function findEntryInPending(promptID: PromptID): [number, QueueEntry | null, Writable | null] { const state = get(store); let index = get(state.queuePending).findIndex(e => e.promptID === promptID) @@ -180,8 +198,10 @@ function moveToCompleted(index: number, queue: Writable, status: Q store.set(state) } -function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null) { +function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null): QueueEntry | null { console.debug("[queueState] executingUpdated", promptID, runningNodeID) + let entry_ = null; + store.update((s) => { s.progress = null; @@ -214,8 +234,11 @@ function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null) s.progress = null; s.runningNodeID = null; } + entry_ = entry; return s }) + + return entry_; } function executionCached(promptID: PromptID, nodes: ComfyNodeID[]) { @@ -283,16 +306,18 @@ function executionStart(promptID: PromptID) { }) } -function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) { +function afterQueued(workflowID: WorkflowInstID, promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) { console.debug("[queueState] afterQueued", promptID, Object.keys(prompt)) store.update(s => { const [index, entry, queue] = findEntryInPending(promptID); if (entry == null) { const entry = createNewQueueEntry(promptID, number, prompt, extraData); + entry.workflowID = workflowID; s.queuePending.update(qp => { qp.push(entry); return qp }) console.debug("[queueState] ADD PROMPT", promptID) } else { + entry.workflowID = workflowID; entry.number = number; entry.prompt = prompt entry.extraData = extraData @@ -304,19 +329,22 @@ function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromp }) } -function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) { - console.debug("[queueState] onExecuted", promptID, nodeID, output) +function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, outputs: ComfyExecutionResult): QueueEntry | null { + console.debug("[queueState] onExecuted", promptID, nodeID, outputs) + let entry_ = null; store.update(s => { const [index, entry, queue] = findEntryInPending(promptID) if (entry != null) { - entry.outputs[nodeID] = output; + entry.outputs[nodeID] = outputs; queue.set(get(queue)) } else { console.error("[queueState] Could not find in pending! (onExecuted)", promptID) } + entry_ = entry; return s }) + return entry; } const queueStateStore: WritableQueueStateStore = @@ -331,6 +359,7 @@ const queueStateStore: WritableQueueStateStore = executionCached, executionError, afterQueued, + getQueueEntry, onExecuted } export default queueStateStore; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 4509436..79fc7f0 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -203,12 +203,12 @@ export function promptToGraphVis(prompt: SerializedPrompt): string { export function getNodeInfo(nodeId: ComfyNodeID): string { let app = (window as any).app; - if (!app || !app.lGraph) + if (!app?.activeGraph) return String(nodeId); const displayNodeID = nodeId ? (nodeId.split("-")[0]) : String(nodeId); - const title = app.lGraph.getNodeByIdRecursive(nodeId)?.title || String(nodeId); + const title = app.activeGraph.getNodeByIdRecursive(nodeId)?.title || String(nodeId); return title + " (" + displayNodeID + ")" } @@ -222,7 +222,7 @@ export const debounce = (callback: Function, wait = 250) => { }; }; -export function convertComfyOutputToGradio(output: ComfyExecutionResult): GradioFileData[] { +export function convertComfyOutputToGradio(output: SerializedPromptOutput): GradioFileData[] { return output.images.map(convertComfyOutputEntryToGradio); } @@ -329,11 +329,16 @@ export async function uploadImageToComfyUI(blob: Blob, filename: string, type: C } /** Raw output as received from ComfyUI's backend */ -export interface ComfyExecutionResult { +export interface SerializedPromptOutput { // Technically this response can contain arbitrary data, but "images" is the // most frequently used as it's output by LoadImage and PreviewImage, the // only two output nodes in base ComfyUI. images: ComfyImageLocation[] | null, + + /* + * Other data + */ + [key: string]: any } /** Raw output entry as received from ComfyUI's backend */ @@ -375,7 +380,7 @@ export function isComfyBoxImageMetadataArray(value: any): value is ComfyBoxImage return Array.isArray(value) && value.every(isComfyBoxImageMetadata); } -export function isComfyExecutionResult(value: any): value is ComfyExecutionResult { +export function isComfyExecutionResult(value: any): value is SerializedPromptOutput { return value && typeof value === "object" && Array.isArray(value.images) } @@ -415,7 +420,7 @@ export function comfyFileToAnnotatedFilepath(comfyUIFile: ComfyImageLocation): s return path; } -export function executionResultToImageMetadata(result: ComfyExecutionResult): ComfyBoxImageMetadata[] { +export function executionResultToImageMetadata(result: SerializedPromptOutput): ComfyBoxImageMetadata[] { return result.images.map(comfyFileToComfyBoxMetadata) } diff --git a/src/lib/widgets.ts b/src/lib/widgets.ts index 8a67428..3635d20 100644 --- a/src/lib/widgets.ts +++ b/src/lib/widgets.ts @@ -2,11 +2,12 @@ import { LGraphNode, LiteGraph } from "@litegraph-ts/core"; import type IComfyInputSlot from "./IComfyInputSlot"; import type { ComfyInputConfig } from "./IComfyInputSlot"; import { ComfyComboNode, ComfyNumberNode, ComfyTextNode } from "./nodes/widgets"; +import type { ComfyNodeDefInput } from "./ComfyNodeDef"; -type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any) => IComfyInputSlot; +type WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput) => IComfyInputSlot; -function getNumberDefaults(inputData: any, defaultStep: number): ComfyInputConfig { - let defaultValue = inputData[1]["default"]; +function getNumberDefaults(inputData: ComfyNodeDefInput, defaultStep: number): ComfyInputConfig { + let defaultValue = inputData[1].default; let { min, max, step } = inputData[1]; if (defaultValue == undefined) defaultValue = 0; @@ -33,25 +34,25 @@ function addComfyInput(node: LGraphNode, inputName: string, extraInfo: Partial { +const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => { const config = getNumberDefaults(inputData, 0.5); return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode }) } -const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => { +const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => { const config = getNumberDefaults(inputData, 1); return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode }) }; -const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => { +const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => { const defaultValue = inputData[1].default || ""; const multiline = !!inputData[1].multiline; return addComfyInput(node, inputName, { type: "string", config: { defaultValue, multiline }, defaultWidgetNode: ComfyTextNode }) }; -const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => { - const type = inputData[0]; +const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => { + const type = inputData[0] as string[]; let defaultValue = type[0]; if (inputData[1] && inputData[1].default) { defaultValue = inputData[1].default; @@ -59,7 +60,7 @@ const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: an return addComfyInput(node, inputName, { type: "string", config: { values: type, defaultValue }, defaultWidgetNode: ComfyComboNode }) } -const IMAGEUPLOAD: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => { +const IMAGEUPLOAD: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => { return addComfyInput(node, inputName, { type: "number", config: {} }) } diff --git a/src/mobile/routes/graph.svelte b/src/mobile/routes/graph.svelte index 170c018..ba0bbb0 100644 --- a/src/mobile/routes/graph.svelte +++ b/src/mobile/routes/graph.svelte @@ -18,11 +18,11 @@ lCanvas.draw(true, true); } - $: if (app != null && app.lGraph && canvasEl != null) { + $: if (app?.activeGraph != null && canvasEl != null) { if (!lCanvas) { - lCanvas = new ComfyGraphCanvas(app, canvasEl); + lCanvas = new ComfyGraphCanvas(app, app.activeGraph, canvasEl); lCanvas.allow_interaction = false; - app.lGraph.eventBus.on("afterExecute", () => lCanvas.draw(true)) + app.activeGraph.eventBus.on("afterExecute", () => lCanvas.draw(true)) } resizeCanvas(); } From a631d97efba8cb993a51662c776c0efb5b82bec9 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 20 May 2023 17:34:13 -0500 Subject: [PATCH 04/15] Workflow tabs --- src/lib/components/ComfyApp.ts | 18 +++++++++++- src/lib/components/ComfyWorkflowsView.svelte | 31 +++++--------------- src/lib/components/PromptDisplay.svelte | 4 +++ src/mobile/GenToolbar.svelte | 11 +++---- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 7a78573..d611768 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -390,6 +390,11 @@ export default class ComfyApp { } saveStateToLocalStorage() { + if (this.activeWorkflow == null) { + notify("No active workflow!", { type: "error" }) + return; + } + try { uiState.update(s => { s.isSavingToLocalStorage = true; return s; }) const savedWorkflow = this.serialize(); @@ -738,6 +743,11 @@ export default class ComfyApp { } querySave() { + if (this.activeWorkflow == null) { + notify("No active workflow!", { type: "error" }) + return; + } + const promptFilename = get(configState).promptForWorkflowName; let filename = "workflow.json"; @@ -931,7 +941,13 @@ export default class ComfyApp { /** * Refresh combo list on whole nodes */ - async refreshComboInNodes(workflow: ComfyWorkflow, flashUI: boolean = false) { + async refreshComboInNodes(workflow?: ComfyWorkflow, flashUI: boolean = false) { + workflow ||= this.activeWorkflow; + if (workflow == null) { + notify("No active workflow!", { type: "error" }) + return + } + const defs = await this.api.getNodeDefs(); const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => { diff --git a/src/lib/components/ComfyWorkflowsView.svelte b/src/lib/components/ComfyWorkflowsView.svelte index 8604dcc..92fee8d 100644 --- a/src/lib/components/ComfyWorkflowsView.svelte +++ b/src/lib/components/ComfyWorkflowsView.svelte @@ -41,7 +41,7 @@ } async function doRefreshCombos() { - await app.refreshComboInNodes(true) + await app.refreshComboInNodes(undefined, true) } function refreshView(event?: Event) { @@ -120,14 +120,11 @@ } function doSave(): void { - if (!app?.lGraph) - return; - app.querySave() } function doLoad(): void { - if (!app?.lGraph || !fileInput) + if (!fileInput) return; fileInput.click(); @@ -139,9 +136,6 @@ } function doSaveLocal(): void { - if (!app?.lGraph) - return; - app.saveStateToLocalStorage(); } @@ -184,22 +178,11 @@
-
- txt2img - -
-
- img2img - -
-
- asdflkj - -
-
- asdkajw - -
+ {#each app.openedWorkflows as workflow, index} + + {/each}
diff --git a/src/lib/components/PromptDisplay.svelte b/src/lib/components/PromptDisplay.svelte index 9bb539e..a11e245 100644 --- a/src/lib/components/PromptDisplay.svelte +++ b/src/lib/components/PromptDisplay.svelte @@ -162,6 +162,10 @@ position: relative; flex: 1 1 0%; max-width: 30vw; + + > :global(.block) { + height: 100%; + } } .copy-button { diff --git a/src/mobile/GenToolbar.svelte b/src/mobile/GenToolbar.svelte index fb2c9ac..38ca67d 100644 --- a/src/mobile/GenToolbar.svelte +++ b/src/mobile/GenToolbar.svelte @@ -21,13 +21,13 @@ app.runDefaultQueueAction() } - function refreshCombos() { + async function refreshCombos() { navigator.vibrate(20) - app.refreshComboInNodes() + await app.refreshComboInNodes() } function doSave(): void { - if (!app?.lGraph || !fileInput) + if (!fileInput) return; navigator.vibrate(20) @@ -35,7 +35,7 @@ } function doLoad(): void { - if (!app?.lGraph || !fileInput) + if (!fileInput) return; navigator.vibrate(20) @@ -48,9 +48,6 @@ } function doSaveLocal(): void { - if (!app?.lGraph) - return; - navigator.vibrate(20) app.saveStateToLocalStorage(); } From 61d9803e1783a0defe9f710aad5a0b7928ed70b6 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 20 May 2023 19:18:01 -0500 Subject: [PATCH 05/15] Huge refactoring for multiple workflows --- src/lib/ComfyGraph.ts | 47 +- src/lib/ComfyGraphCanvas.ts | 12 +- src/lib/api.ts | 2 +- src/lib/components/AccordionContainer.svelte | 27 +- src/lib/components/BlockContainer.svelte | 25 +- src/lib/components/ComfyApp.svelte | 9 - src/lib/components/ComfyApp.ts | 260 +---- src/lib/components/ComfyProperties.svelte | 294 ++--- ...UIPane.svelte => ComfyWorkflowView.svelte} | 37 +- src/lib/components/ComfyWorkflowsView.svelte | 24 +- src/lib/components/Container.svelte | 9 +- src/lib/components/TabsContainer.svelte | 37 +- src/lib/components/WidgetContainer.svelte | 19 +- src/lib/init.ts | 4 +- src/lib/nodes/ComfyActionNodes.ts | 4 +- .../nodes/ComfyConfigureQueuePromptButton.ts | 19 +- src/lib/nodes/ComfyGraphNode.ts | 6 + src/lib/nodes/widgets/ComfyWidgetNode.ts | 5 +- .../{layoutState.ts => layoutStates.ts} | 1001 +++++++++-------- src/lib/stores/selectionState.ts | 2 +- src/lib/stores/workflowState.ts | 261 +++++ src/lib/utils.ts | 12 +- src/lib/widgets/ButtonWidget.svelte | 12 +- src/lib/widgets/CheckboxWidget.svelte | 2 +- src/lib/widgets/ComboWidget.svelte | 2 +- src/lib/widgets/GalleryWidget.svelte | 2 +- src/lib/widgets/ImageUploadWidget.svelte | 2 +- src/lib/widgets/NumberWidget.svelte | 2 +- src/lib/widgets/RadioWidget.svelte | 2 +- src/lib/widgets/TextWidget.svelte | 2 +- src/lib/widgets/utils.ts | 3 +- src/mobile/GenToolbar.svelte | 9 +- src/mobile/routes/graph.svelte | 19 +- src/mobile/routes/subworkflow.svelte | 18 +- src/tests/ComfyGraphTests.ts | 11 +- 35 files changed, 1228 insertions(+), 974 deletions(-) rename src/lib/components/{ComfyUIPane.svelte => ComfyWorkflowView.svelte} (85%) rename src/lib/stores/{layoutState.ts => layoutStates.ts} (54%) create mode 100644 src/lib/stores/workflowState.ts diff --git a/src/lib/ComfyGraph.ts b/src/lib/ComfyGraph.ts index 578a6fd..bab45cb 100644 --- a/src/lib/ComfyGraph.ts +++ b/src/lib/ComfyGraph.ts @@ -2,7 +2,6 @@ import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, Li import GraphSync from "./GraphSync"; import EventEmitter from "events"; import type TypedEmitter from "typed-emitter"; -import layoutState from "./stores/layoutState"; import uiState from "./stores/uiState"; import { get } from "svelte/store"; import type ComfyGraphNode from "./nodes/ComfyGraphNode"; @@ -10,6 +9,9 @@ import type IComfyInputSlot from "./IComfyInputSlot"; import type { ComfyBackendNode } from "./nodes/ComfyBackendNode"; import type { ComfyComboNode, ComfyWidgetNode } from "./nodes/widgets"; import selectionState from "./stores/selectionState"; +import type { WritableLayoutStateStore } from "./stores/layoutStates"; +import type { WorkflowInstID } from "./components/ComfyApp"; +import layoutStates from "./stores/layoutStates"; type ComfyGraphEvents = { configured: (graph: LGraph) => void @@ -25,6 +27,13 @@ type ComfyGraphEvents = { export default class ComfyGraph extends LGraph { eventBus: TypedEmitter = new EventEmitter() as TypedEmitter; + workflowID: WorkflowInstID | null = null; + + constructor(workflowID?: WorkflowInstID) { + super(); + this.workflowID = workflowID; + } + override onConfigure() { console.debug("Configured"); } @@ -50,19 +59,24 @@ export default class ComfyGraph extends LGraph { override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) { // Don't add nodes in subgraphs until this callback reaches the root // graph - if (node.getRootGraph() == null || this._is_subgraph) - return; + // Only root graphs will have a workflow ID, so we don't mind subgraphs + // missing it + if (node.getRootGraph() != null && !this._is_subgraph && this.workflowID != null) { + const layoutState = get(layoutStates).all[this.workflowID] + if (layoutState === null) { + throw new Error(`LGraph with workflow missing layout! ${this.workflowID}`) + } - this.doAddNode(node, options); + this.doAddNode(node, layoutState, options); + } - // console.debug("Added", node); this.eventBus.emit("nodeAdded", node); } /* * Add widget UI/groups for newly added nodes. */ - private doAddNode(node: LGraphNode, options: LGraphAddNodeOptions) { + private doAddNode(node: LGraphNode, layoutState: WritableLayoutStateStore, options: LGraphAddNodeOptions) { layoutState.nodeAdded(node, options) // All nodes whether they come from base litegraph or ComfyBox should @@ -144,7 +158,7 @@ export default class ComfyGraph extends LGraph { // ************** RECURSION ALERT ! ************** if (node.is(Subgraph)) { for (const child of node.subgraph.iterateNodesInOrder()) { - this.doAddNode(child, options) + this.doAddNode(child, layoutState, options) } } // ************** RECURSION ALERT ! ************** @@ -152,16 +166,23 @@ export default class ComfyGraph extends LGraph { override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) { selectionState.clear(); // safest option - layoutState.nodeRemoved(node, options); - // Handle subgraphs being removed - if (node.is(Subgraph)) { - for (const child of node.subgraph.iterateNodesInOrder()) { - this.onNodeRemoved(child, options) + if (node.getRootGraph() != null && !this._is_subgraph && this.workflowID != null) { + const layoutState = get(layoutStates).all[this.workflowID] + if (layoutState === null) { + throw new Error(`LGraph with workflow missing layout! ${this.workflowID}`) + } + + layoutState.nodeRemoved(node, options); + + // Handle subgraphs being removed + if (node.is(Subgraph)) { + for (const child of node.subgraph.iterateNodesInOrder()) { + this.onNodeRemoved(child, options) + } } } - // console.debug("Removed", node); this.eventBus.emit("nodeRemoved", node); } diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index 17e23ba..00cc87b 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -3,11 +3,11 @@ import type ComfyApp from "./components/ComfyApp"; import queueState from "./stores/queueState"; import { get, type Unsubscriber } from "svelte/store"; import uiState from "./stores/uiState"; -import layoutState from "./stores/layoutState"; import { Watch } from "@litegraph-ts/nodes-basic"; import { ComfyReroute } from "./nodes"; import type { Progress } from "./components/ComfyApp"; import selectionState from "./stores/selectionState"; +import type ComfyGraph from "./ComfyGraph"; export type SerializedGraphCanvasState = { offset: Vector2, @@ -18,10 +18,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas { app: ComfyApp | null; private _unsubscribe: Unsubscriber; + get comfyGraph(): ComfyGraph | null { + return this.graph as ComfyGraph; + } + constructor( app: ComfyApp, - graph: LGraph, canvas: HTMLCanvasElement | string, + graph?: ComfyGraph, options: { skip_render?: boolean; skip_events?: boolean; @@ -282,7 +286,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas { selectionState.update(ss => { ss.currentSelectionNodes = Object.values(nodes) ss.currentSelection = [] - const ls = get(layoutState) + const ls = get(this.comfyGraph.layoutState) for (const node of ss.currentSelectionNodes) { const widget = ls.allItemsByNode[node.id] if (widget) @@ -299,7 +303,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas { ss.currentHoveredNodes.add(node.id) } ss.currentHovered.clear() - const ls = get(layoutState) + const ls = get(this.comfyGraph.layoutState) for (const nodeID of ss.currentHoveredNodes) { const widget = ls.allItemsByNode[nodeID] if (widget) diff --git a/src/lib/api.ts b/src/lib/api.ts index 78ea676..3d0a960 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -3,7 +3,7 @@ import type TypedEmitter from "typed-emitter"; import EventEmitter from "events"; import type { ComfyImageLocation } from "$lib/utils"; import type { SerializedLGraph, UUID } from "@litegraph-ts/core"; -import type { SerializedLayoutState } from "./stores/layoutState"; +import type { SerializedLayoutState } from "./stores/layoutStates"; import type { ComfyNodeDef } from "./ComfyNodeDef"; export type ComfyPromptRequest = { diff --git a/src/lib/components/AccordionContainer.svelte b/src/lib/components/AccordionContainer.svelte index 279aafe..cde9290 100644 --- a/src/lib/components/AccordionContainer.svelte +++ b/src/lib/components/AccordionContainer.svelte @@ -11,11 +11,12 @@ // notice - fade in works fine but don't add svelte's fade-out (known issue) import {cubicIn} from 'svelte/easing'; import { flip } from 'svelte/animate'; - import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; + import { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutStates"; import { startDrag, stopDrag } from "$lib/utils" import { writable, type Writable } from "svelte/store"; import { isHidden } from "$lib/widgets/utils"; + export let layoutState: WritableLayoutStateStore; export let container: ContainerLayout | null = null; export let zIndex: number = 0; export let classes: string[] = []; @@ -59,6 +60,14 @@ navigator.vibrate(20) $isOpen = e.detail } + + function _startDrag(e: MouseEvent | TouchEvent) { + startDrag(e, layoutState) + } + + function _stopDrag(e: MouseEvent | TouchEvent) { + stopDrag(e, layoutState) + } {#if container} @@ -93,7 +102,7 @@ animate:flip={{duration:flipDurationMs}} style={item?.attrs?.style || ""} > - + {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
{/if} @@ -101,10 +110,18 @@ {/each}
{#if isHidden(container) && edit} -
+
{/if} {#if showHandles} -
+
{/if} @@ -112,7 +129,7 @@ {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} - + {/each} diff --git a/src/lib/components/BlockContainer.svelte b/src/lib/components/BlockContainer.svelte index 80b85ae..5d230b5 100644 --- a/src/lib/components/BlockContainer.svelte +++ b/src/lib/components/BlockContainer.svelte @@ -10,11 +10,12 @@ // notice - fade in works fine but don't add svelte's fade-out (known issue) import {cubicIn} from 'svelte/easing'; import { flip } from 'svelte/animate'; - import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; + import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates"; import { startDrag, stopDrag } from "$lib/utils" import type { Writable } from "svelte/store"; import { isHidden } from "$lib/widgets/utils"; + export let layoutState: WritableLayoutStateStore; export let container: ContainerLayout | null = null; export let zIndex: number = 0; export let classes: string[] = []; @@ -53,6 +54,14 @@ children = layoutState.updateChildren(container, evt.detail.items) // Ensure dragging is stopped on drag finish }; + + function _startDrag(e: MouseEvent | TouchEvent) { + startDrag(e, layoutState) + } + + function _stopDrag(e: MouseEvent | TouchEvent) { + stopDrag(e, layoutState) + } {#if container} @@ -92,7 +101,7 @@ animate:flip={{duration:flipDurationMs}} style={item?.attrs?.style || ""} > - + {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
{/if} @@ -100,10 +109,18 @@ {/each}
{#if isHidden(container) && edit} -
+
{/if} {#if showHandles} -
+
{/if}
diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index 9da5eb7..28e77bb 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -2,7 +2,6 @@ import { ListIcon as List, ImageIcon as Image, SettingsIcon as Settings } from "svelte-feather-icons"; import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp"; import uiState from "$lib/stores/uiState"; - import layoutState from "$lib/stores/layoutState"; import { SvelteToast, toast } from '@zerodevx/svelte-toast' import LightboxModal from "./LightboxModal.svelte"; @@ -18,8 +17,6 @@ let hasShownUIHelpToast: boolean = false; let uiTheme: string = "gradio-dark"; - let debugLayout: boolean = false; - const toastOptions = { intro: { duration: 200 }, theme: { @@ -32,12 +29,6 @@ notify("Right-click to open context menu.") } - if (debugLayout) { - layoutState.subscribe(s => { - console.warn("UPDATESTATE", s) - }) - } - $: if (uiTheme === "gradio-dark") { document.getElementById("app-root").classList.add("dark") } diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index d611768..904501b 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -21,8 +21,7 @@ import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode"; import queueState from "$lib/stores/queueState"; import { type SvelteComponentDev } from "svelte/internal"; import type IComfyInputSlot from "$lib/IComfyInputSlot"; -import type { LayoutState, SerializedLayoutState, WritableLayoutStateStore } from "$lib/stores/layoutState"; -import layoutState from "$lib/stores/layoutState"; +import type { LayoutState, SerializedLayoutState, WritableLayoutStateStore } from "$lib/stores/layoutStates"; import { toast } from '@zerodevx/svelte-toast' import ComfyGraph from "$lib/ComfyGraph"; import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; @@ -41,7 +40,10 @@ import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111"; import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt"; import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt"; import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer"; -import { v4 as uuidv4 } from "uuid"; +import selectionState from "$lib/stores/selectionState"; +import layoutStates from "$lib/stores/layoutStates"; +import { ComfyWorkflow } from "$lib/stores/workflowState"; +import workflowState from "$lib/stores/workflowState"; export const COMFYBOX_SERIAL_VERSION = 1; @@ -134,133 +136,6 @@ type CanvasState = { canvas: ComfyGraphCanvas, } -type ActiveCanvas = { - canvas: LGraphCanvas | null; - canvasHandler: () => void | null; - state: SerializedGraphCanvasState; -} - -export type SerializedWorkflowState = { - graph: SerializedLGraph, - layout: SerializedLayoutState -} - -/* - * ID for an opened workflow. - * - * Unlike NodeID and PromptID, these are *not* saved to the workflow itself. - * They are only used for identifying an open workflow in the program. If the - * workflow is closed and reopened, a different workflow ID will be assigned to - * it. - */ -export type WorkflowInstID = UUID; - -export class ComfyWorkflow { - /* - * Used for uniquely identifying the instance of the opened workflow in the frontend. - */ - id: WorkflowInstID; - title: string; - graph: ComfyGraph; - layout: WritableLayoutStateStore; - - canvases: Record = {}; - - constructor(title: string, graph: ComfyGraph, layout: WritableLayoutStateStore) { - this.id = uuidv4(); - this.title = title; - this.layout = layout; - this.graph = graph; - } - - start(key: string, canvas: ComfyGraphCanvas) { - if (this.canvases[key] != null) - throw new Error(`This workflow is already being displayed on canvas ${key}`) - - const canvasHandler = () => canvas.draw(true); - - this.canvases[key] = { - canvas, - canvasHandler, - state: { - // TODO - offset: [0, 0], - scale: 1 - } - } - - this.graph.attachCanvas(canvas); - this.graph.eventBus.on("afterExecute", canvasHandler) - - if (Object.keys(this.canvases).length === 1) - this.graph.start(); - } - - stop(key: string) { - const canvas = this.canvases[key] - if (canvas == null) - throw new Error(`This workflow is not being displayed on canvas ${key}`) - - this.graph.detachCanvas(canvas.canvas); - this.graph.eventBus.removeListener("afterExecute", canvas.canvasHandler) - - delete this.canvases[key] - - if (Object.keys(this.canvases).length === 0) - this.graph.stop(); - } - - stopAll() { - for (const key of Object.keys(this.canvases)) - this.stop(key) - this.graph.stop() - } - - serialize(): SerializedWorkflowState { - const graph = this.graph; - - const serializedGraph = graph.serialize() - const serializedLayout = this.layout.serialize() - - return { - graph: serializedGraph, - layout: serializedLayout - } - } - - static deserialize(data: SerializedWorkflowState): ComfyWorkflow { - const layout = layoutState; // TODO - // Ensure loadGraphData does not trigger any state changes in layoutState - // (isConfiguring is set to true here) - // lGraph.configure will add new nodes, triggering onNodeAdded, but we - // want to restore the layoutState ourselves - layout.onStartConfigure(); - - // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now - for (let n of data.graph.nodes) { - if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader"; - } - - const graph = new ComfyGraph(); - graph.configure(data.graph); - - for (const node of 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; - // this.#invokeExtensions("loadedGraphNode", node); - } - - // Now restore the layout - // Subsequent added nodes will add the UI data to layoutState - // TODO - layout.deserialize(data.layout, graph) - - return new ComfyWorkflow("Workflow X", graph, layout); - } -} - export default class ComfyApp { api: ComfyAPI; @@ -269,22 +144,6 @@ export default class ComfyApp { canvasCtx: CanvasRenderingContext2D | null = null; lCanvas: ComfyGraphCanvas | null = null; - openedWorkflows: ComfyWorkflow[] = []; - openedWorkflowsByID: Record = {}; - activeWorkflowIdx: number = -1; - - get activeWorkflow(): ComfyWorkflow | null { - return this.openedWorkflows[this.activeWorkflowIdx] - } - - get activeGraph(): ComfyGraph | null { - return this.activeWorkflow?.graph; - } - - getWorkflow(id: WorkflowInstID): ComfyWorkflow | null { - return this.openedWorkflowsByID[id]; - } - shiftDown: boolean = false; ctrlDown: boolean = false; selectedGroupMoving: boolean = false; @@ -312,7 +171,7 @@ export default class ComfyApp { this.rootEl = document.getElementById("app-root") as HTMLDivElement; this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement; - this.lCanvas = new ComfyGraphCanvas(this, null, this.canvasEl); + this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl); this.canvasCtx = this.canvasEl.getContext("2d"); const uiUnlocked = get(uiState).uiUnlocked; @@ -371,12 +230,12 @@ export default class ComfyApp { this.lCanvas.draw(true, true); } - serialize(): SerializedAppState { - const workflow = this.activeWorkflow - if (workflow == null) - throw new Error("No workflow active!") + serialize(workflow: ComfyWorkflow): SerializedAppState { + const layoutState = layoutStates.getLayout(workflow.id); + if (layoutState == null) + throw new Error("Workflow has no layout!") - const { graph, layout } = workflow.serialize(); + const { graph, layout } = workflow.serialize(layoutState); const canvas = this.lCanvas.serialize(); return { @@ -390,14 +249,15 @@ export default class ComfyApp { } saveStateToLocalStorage() { - if (this.activeWorkflow == null) { + const workflow = workflowState.getActiveWorkflow(); + if (workflow == null) { notify("No active workflow!", { type: "error" }) return; } try { uiState.update(s => { s.isSavingToLocalStorage = true; return s; }) - const savedWorkflow = this.serialize(); + const savedWorkflow = this.serialize(workflow); const json = JSON.stringify(savedWorkflow); localStorage.setItem("workflow", json) notify("Saved to local storage.") @@ -548,25 +408,27 @@ export default class ComfyApp { this.api.addEventListener("progress", (progress: Progress) => { queueState.progressUpdated(progress); - this.activeGraph?.setDirtyCanvas(true, false); // TODO PromptID + workflowState.getActiveWorkflow()?.graph?.setDirtyCanvas(true, false); // TODO PromptID }); this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: ComfyNodeID | null) => { const queueEntry = queueState.executingUpdated(promptID, nodeID); if (queueEntry != null) { - const workflow = this.getWorkflow(queueEntry.workflowID) - workflow?.graph.setDirtyCanvas(true, false); + const workflow = workflowState.getWorkflow(queueEntry.workflowID); + workflow?.graph?.setDirtyCanvas(true, false); } }); this.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutput) => { const queueEntry = queueState.onExecuted(promptID, nodeID, output) if (queueEntry != null) { - const workflow = this.getWorkflow(queueEntry.workflowID) - workflow?.graph.setDirtyCanvas(true, false); - const node = workflow?.graph.getNodeByIdRecursive(nodeID) as ComfyGraphNode; - if (node?.onExecuted) { - node.onExecuted(output); + const workflow = workflowState.getWorkflow(queueEntry.workflowID); + if (workflow != null) { + workflow.graph.setDirtyCanvas(true, false); + const node = workflow.graph.getNodeByIdRecursive(nodeID) as ComfyGraphNode; + if (node?.onExecuted) { + node.onExecuted(output); + } } } }); @@ -635,68 +497,31 @@ export default class ComfyApp { setColor(BuiltInSlotType.ACTION, "lightseagreen") } - createNewWorkflow(): ComfyWorkflow { - // TODO remove - const workflow = ComfyWorkflow.deserialize({ graph: blankGraph.workflow, layout: blankGraph.layout }) - this.openedWorkflows.push(workflow); - this.setActiveWorkflow(this.openedWorkflows.length - 1) - return workflow; - } - async openWorkflow(data: SerializedAppState): Promise { if (data.version !== COMFYBOX_SERIAL_VERSION) { throw `Invalid ComfyBox saved data format: ${data.version}` } - this.clean(); - const workflow = ComfyWorkflow.deserialize({ graph: data.workflow, layout: data.layout }) + const workflow = workflowState.openWorkflow(data); // Restore canvas offset/zoom this.lCanvas.deserialize(data.canvas) await this.refreshComboInNodes(workflow); - this.openedWorkflows.push(workflow); - this.setActiveWorkflow(this.openedWorkflows.length - 1) - return workflow; } - closeWorkflow(index: number) { - if (index < 0 || index >= this.openedWorkflows.length) - return; - - const workflow = this.openedWorkflows[index]; - workflow.stopAll(); - - this.openedWorkflows.splice(index, 1) - this.setActiveWorkflow(0); - } - - closeAllWorkflows() { - while (this.openedWorkflows.length > 0) - this.closeWorkflow(0) - } - setActiveWorkflow(index: number) { - if (this.openedWorkflows.length === 0) { - this.activeWorkflowIdx = -1; - return; + const workflow = workflowState.setActiveWorkflow(index); + + if (workflow != null) { + workflow.start("app", this.lCanvas); + this.lCanvas.deserialize(workflow.canvases["app"].state) } - if (index < 0 || index >= this.openedWorkflows.length || this.activeWorkflowIdx === index) - return; - - if (this.activeWorkflow != null) - this.activeWorkflow.stop("app") - - const workflow = this.openedWorkflows[index] - this.activeWorkflowIdx = index; - - console.warn("START") - workflow.start("app", this.lCanvas); - this.lCanvas.deserialize(workflow.canvases["app"].state) + selectionState.clear(); } async initDefaultGraph() { @@ -717,7 +542,7 @@ export default class ComfyApp { this.clean(); this.lCanvas.closeAllSubgraphs(); - this.closeAllWorkflows(); + workflowState.closeAllWorkflows(); uiState.update(s => { s.uiUnlocked = true; s.uiEditMode = "widgets"; @@ -726,16 +551,17 @@ export default class ComfyApp { } runDefaultQueueAction() { - if (this.activeWorkflow == null) + const workflow = workflowState.getActiveWorkflow(); + if (workflow == null) return; - for (const node of this.activeGraph.iterateNodesInOrderRecursive()) { + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { if ("onDefaultQueueAction" in node) { (node as ComfyGraphNode).onDefaultQueueAction() } } - if (get(layoutState).attrs.queuePromptButtonRunWorkflow) { + if (get(workflow.layout).attrs.queuePromptButtonRunWorkflow) { // Hold control to queue at the front const num = this.ctrlDown ? -1 : 0; this.queuePrompt(num, 1); @@ -743,7 +569,8 @@ export default class ComfyApp { } querySave() { - if (this.activeWorkflow == null) { + const workflow = workflowState.getActiveWorkflow(); + if (workflow == null) { notify("No active workflow!", { type: "error" }) return; } @@ -765,7 +592,7 @@ export default class ComfyApp { } const indent = 2 - const json = JSON.stringify(this.serialize(), null, indent) + const json = JSON.stringify(this.serialize(workflow), null, indent) download(filename, json, "application/json") @@ -781,12 +608,13 @@ export default class ComfyApp { } async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) { - if (this.activeWorkflow === null) { + const activeWorkflow = workflowState.getActiveWorkflow(); + if (activeWorkflow == null) { notify("No workflow is opened!", { type: "error" }) return; } - this.queueItems.push({ num, batchCount, workflow: this.activeWorkflow }); + this.queueItems.push({ num, batchCount, workflow: activeWorkflow }); // Only have one action process the items so each one gets a unique seed correctly if (this.processingQueue) { @@ -830,7 +658,7 @@ export default class ComfyApp { } const p = this.graphToPrompt(workflow, tag); - const l = layoutState.serialize(); + const l = workflow.layout.serialize(); console.debug(graphToGraphVis(workflow.graph)) console.debug(promptToGraphVis(p)) @@ -942,7 +770,7 @@ export default class ComfyApp { * Refresh combo list on whole nodes */ async refreshComboInNodes(workflow?: ComfyWorkflow, flashUI: boolean = false) { - workflow ||= this.activeWorkflow; + workflow ||= workflowState.getActiveWorkflow(); if (workflow == null) { notify("No active workflow!", { type: "error" }) return diff --git a/src/lib/components/ComfyProperties.svelte b/src/lib/components/ComfyProperties.svelte index c3efcb4..86d9019 100644 --- a/src/lib/components/ComfyProperties.svelte +++ b/src/lib/components/ComfyProperties.svelte @@ -2,43 +2,47 @@ import { Block, BlockTitle } from "@gradio/atoms"; import { TextBox, Checkbox } from "@gradio/form"; import { LGraphNode } from "@litegraph-ts/core" - import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState" + import { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec, type WritableLayoutStateStore } from "$lib/stores/layoutStates" import uiState from "$lib/stores/uiState" + import layoutStates from "$lib/stores/layoutStates" import selectionState from "$lib/stores/selectionState" import { get, type Writable, writable } from "svelte/store" import ComfyNumberProperty from "./ComfyNumberProperty.svelte"; import ComfyComboProperty from "./ComfyComboProperty.svelte"; - import type { ComfyWidgetNode } from "$lib/nodes/widgets"; + import type { ComfyWidgetNode } from "$lib/nodes/widgets"; + import type { ComfyWorkflow } from "$lib/stores/workflowState"; + + export let workflow: ComfyWorkflow | null; + + let layoutState: WritableLayoutStateStore | null = null let target: IDragItem | null = null; let node: LGraphNode | null = null; - let attrsChanged: Writable | null = null; - let refreshPropsPanel: Writable | null - - $: refreshPropsPanel = $layoutState.refreshPropsPanel; - - $: if ($selectionState.currentSelection.length > 0) { - node = null; - const targetId = $selectionState.currentSelection.slice(-1)[0] - const entry = $layoutState.allItems[targetId] - if (entry != null) { - target = entry.dragItem - attrsChanged = target.attrsChanged; - if (target.type === "widget") { - node = (target as WidgetLayout).node + $: if (layoutState) { + if ($selectionState.currentSelection.length > 0) { + node = null; + const targetId = $selectionState.currentSelection.slice(-1)[0] + const entry = $layoutState.allItems[targetId] + if (entry != null) { + target = entry.dragItem + if (target.type === "widget") { + node = (target as WidgetLayout).node + } } } - } - else if ($selectionState.currentSelectionNodes.length > 0) { - target = null; - node = $selectionState.currentSelectionNodes[0] - attrsChanged = null; + else if ($selectionState.currentSelectionNodes.length > 0) { + target = null; + node = $selectionState.currentSelectionNodes[0] + } + else { + target = null + node = null; + } } else { - target = null + target = null; node = null; - attrsChanged = null; } $: if (target) { @@ -55,7 +59,7 @@ let value = spec.defaultValue; target.attrs[spec.name] = value; if (spec.refreshPanelOnChange) - $refreshPropsPanel += 1; + doRefreshPanel(); } } } @@ -265,7 +269,7 @@ function doRefreshPanel() { console.warn("[ComfyProperties] doRefreshPanel") - $refreshPropsPanel += 1; + $layoutStates.refreshPropsPanel += 1; } @@ -282,172 +286,174 @@
- {#key $refreshPropsPanel} - {#each ALL_ATTRIBUTES as category(category.categoryName)} -
- - {category.categoryName} - -
- {#each category.specs as spec(spec.id)} - {#if validWidgetAttribute(spec, target)} -
- {#if spec.type === "string"} - updateAttribute(spec, target, e.detail)} - on:input={(e) => updateAttribute(spec, target, e.detail)} - disabled={!$uiState.uiUnlocked || !spec.editable} - label={spec.name} - max_lines={spec.multiline ? 5 : 1} - /> - {:else if spec.type === "boolean"} - + + {category.categoryName} + +
+ {#each category.specs as spec(spec.id)} + {#if validWidgetAttribute(spec, target)} +
+ {#if spec.type === "string"} + updateAttribute(spec, target, e.detail)} + on:input={(e) => updateAttribute(spec, target, e.detail)} disabled={!$uiState.uiUnlocked || !spec.editable} label={spec.name} + max_lines={spec.multiline ? 5 : 1} /> - {:else if spec.type === "number"} - updateAttribute(spec, target, e.detail)} - /> - {:else if spec.type === "enum"} - updateAttribute(spec, target, e.detail)} + disabled={!$uiState.uiUnlocked || !spec.editable} + label={spec.name} + /> + {:else if spec.type === "number"} + updateAttribute(spec, target, e.detail)} /> - {/if} -
- {:else if node} - {#if validNodeProperty(spec, node)} -
- {#if spec.type === "string"} - updateProperty(spec, e.detail)} - on:input={(e) => updateProperty(spec, e.detail)} - label={spec.name} - disabled={!$uiState.uiUnlocked || !spec.editable} - max_lines={spec.multiline ? 5 : 1} - /> - {:else if spec.type === "boolean"} - updateProperty(spec, e.detail)} - /> - {:else if spec.type === "number"} - updateProperty(spec, e.detail)} - /> {:else if spec.type === "enum"} updateProperty(spec, e.detail)} + on:change={(e) => updateAttribute(spec, target, e.detail)} /> {/if}
- {:else if validNodeVar(spec, node)} + {:else if node} + {#if validNodeProperty(spec, node)} +
+ {#if spec.type === "string"} + updateProperty(spec, e.detail)} + on:input={(e) => updateProperty(spec, e.detail)} + label={spec.name} + disabled={!$uiState.uiUnlocked || !spec.editable} + max_lines={spec.multiline ? 5 : 1} + /> + {:else if spec.type === "boolean"} + updateProperty(spec, e.detail)} + /> + {:else if spec.type === "number"} + updateProperty(spec, e.detail)} + /> + {:else if spec.type === "enum"} + updateProperty(spec, e.detail)} + /> + {/if} +
+ {:else if validNodeVar(spec, node)} +
+ {#if spec.type === "string"} + updateVar(spec, e.detail)} + on:input={(e) => updateVar(spec, e.detail)} + label={spec.name} + disabled={!$uiState.uiUnlocked || !spec.editable} + max_lines={spec.multiline ? 5 : 1} + /> + {:else if spec.type === "boolean"} + updateVar(spec, e.detail)} + disabled={!$uiState.uiUnlocked || !spec.editable} + label={spec.name} + /> + {:else if spec.type === "number"} + updateVar(spec, e.detail)} + /> + {:else if spec.type === "enum"} + updateVar(spec, e.detail)} + /> + {/if} +
+ {/if} + {:else if !node && !target && validWorkflowAttribute(spec)}
{#if spec.type === "string"} updateVar(spec, e.detail)} - on:input={(e) => updateVar(spec, e.detail)} + value={getWorkflowAttribute(spec)} + on:change={(e) => updateWorkflowAttribute(spec, e.detail)} + on:input={(e) => updateWorkflowAttribute(spec, e.detail)} label={spec.name} disabled={!$uiState.uiUnlocked || !spec.editable} - max_lines={spec.multiline ? 5 : 1} + max_lines={spec.multiline ? 5 : 1} /> {:else if spec.type === "boolean"} updateVar(spec, e.detail)} + value={getWorkflowAttribute(spec)} + on:change={(e) => updateWorkflowAttribute(spec, e.detail)} disabled={!$uiState.uiUnlocked || !spec.editable} label={spec.name} /> {:else if spec.type === "number"} updateVar(spec, e.detail)} + on:change={(e) => updateWorkflowAttribute(spec, e.detail)} /> {:else if spec.type === "enum"} updateVar(spec, e.detail)} + on:change={(e) => updateWorkflowAttribute(spec, e.detail)} /> {/if}
{/if} - {:else if !node && !target && validWorkflowAttribute(spec)} -
- {#if spec.type === "string"} - updateWorkflowAttribute(spec, e.detail)} - on:input={(e) => updateWorkflowAttribute(spec, e.detail)} - label={spec.name} - disabled={!$uiState.uiUnlocked || !spec.editable} - max_lines={spec.multiline ? 5 : 1} - /> - {:else if spec.type === "boolean"} - updateWorkflowAttribute(spec, e.detail)} - disabled={!$uiState.uiUnlocked || !spec.editable} - label={spec.name} - /> - {:else if spec.type === "number"} - updateWorkflowAttribute(spec, e.detail)} - /> - {:else if spec.type === "enum"} - updateWorkflowAttribute(spec, e.detail)} - /> - {/if} -
- {/if} + {/each} {/each} - {/each} - {/key} + {/key} + {/if}
diff --git a/src/lib/components/ComfyUIPane.svelte b/src/lib/components/ComfyWorkflowView.svelte similarity index 85% rename from src/lib/components/ComfyUIPane.svelte rename to src/lib/components/ComfyWorkflowView.svelte index b4a2bc8..2338857 100644 --- a/src/lib/components/ComfyUIPane.svelte +++ b/src/lib/components/ComfyWorkflowView.svelte @@ -1,34 +1,23 @@ -
- -
+{#if layoutState != null} +
+ +
+{/if} {#if showMenu} @@ -188,7 +179,7 @@ {/if} diff --git a/src/lib/stores/layoutStates.ts b/src/lib/stores/layoutStates.ts index 78dd4a2..3d0af3f 100644 --- a/src/lib/stores/layoutStates.ts +++ b/src/lib/stores/layoutStates.ts @@ -548,6 +548,13 @@ const ALL_ATTRIBUTES: AttributesSpecList = [ }, // Workflow + { + name: "title", + type: "string", + location: "workflow", + editable: true, + defaultValue: "New Workflow" + }, { name: "queuePromptButtonName", type: "string", diff --git a/src/lib/stores/workflowState.ts b/src/lib/stores/workflowState.ts index 3b6b2e3..0b8d1f2 100644 --- a/src/lib/stores/workflowState.ts +++ b/src/lib/stores/workflowState.ts @@ -197,7 +197,7 @@ export class ComfyWorkflow { export type WorkflowState = { openedWorkflows: ComfyWorkflow[], openedWorkflowsByID: Record, - activeWorkflowIdx: number, + activeWorkflowID: WorkflowInstID | null, activeWorkflow: ComfyWorkflow | null, } @@ -219,7 +219,7 @@ const store: Writable = writable( { openedWorkflows: [], openedWorkflowsByID: {}, - activeWorkflowIdx: -1, + activeWorkflowID: null, activeWorkflow: null }) @@ -245,9 +245,9 @@ function getWorkflowByNodeID(id: NodeID): ComfyWorkflow | null { function getActiveWorkflow(): ComfyWorkflow | null { const state = get(store); - if (state.activeWorkflowIdx === -1) + if (state.activeWorkflowID == null) return null; - return state.openedWorkflows[state.activeWorkflowIdx]; + return state.openedWorkflowsByID[state.activeWorkflowID]; } function createNewWorkflow(canvas: ComfyGraphCanvas, title: string = "New Workflow", setActive: boolean = false): ComfyWorkflow { @@ -292,9 +292,10 @@ function closeWorkflow(canvas: ComfyGraphCanvas, index: number) { layoutStates.remove(workflow.id) + state.openedWorkflows.splice(index, 1) delete state.openedWorkflowsByID[workflow.id] - const newIndex = clamp(state.activeWorkflowIdx, 0, state.openedWorkflows.length - 1); + let newIndex = clamp(index, 0, state.openedWorkflows.length - 1) setActiveWorkflow(canvas, newIndex); store.set(state); @@ -310,19 +311,22 @@ function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number): ComfyWorkfl const state = get(store); if (state.openedWorkflows.length === 0) { - state.activeWorkflowIdx = -1; + state.activeWorkflowID = null; state.activeWorkflow = null return null; } - if (index < 0 || index >= state.openedWorkflows.length || state.activeWorkflowIdx === index) + if (index < 0 || index >= state.openedWorkflows.length) return state.activeWorkflow; + const workflow = state.openedWorkflows[index] + if (workflow.id === state.activeWorkflowID) + return; + if (state.activeWorkflow != null) state.activeWorkflow.stop("app") - const workflow = state.openedWorkflows[index] - state.activeWorkflowIdx = index; + state.activeWorkflowID = workflow.id; state.activeWorkflow = workflow; workflow.start("app", canvas); From 1014afc9ab38cbf6ddfc7f9571a1610cb041f5f7 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 20 May 2023 23:22:56 -0500 Subject: [PATCH 13/15] Fix set workflow attribute in node --- src/lib/ComfyGraph.ts | 5 +++-- src/lib/nodes/ComfyConfigureQueuePromptButton.ts | 4 ++-- src/lib/stores/workflowState.ts | 13 ++++++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/lib/ComfyGraph.ts b/src/lib/ComfyGraph.ts index 776051e..17a0382 100644 --- a/src/lib/ComfyGraph.ts +++ b/src/lib/ComfyGraph.ts @@ -32,9 +32,10 @@ export default class ComfyGraph extends LGraph { workflowID: WorkflowInstID | null = null; get workflow(): ComfyWorkflow | null { - if (this.workflowID == null) + const workflowID = (this.getRootGraph() as ComfyGraph)?.workflowID; + if (workflowID == null) return null; - return workflowState.getWorkflow(this.workflowID) + return workflowState.getWorkflow(workflowID) } constructor(workflowID?: WorkflowInstID) { diff --git a/src/lib/nodes/ComfyConfigureQueuePromptButton.ts b/src/lib/nodes/ComfyConfigureQueuePromptButton.ts index 1edd66f..c10edd5 100644 --- a/src/lib/nodes/ComfyConfigureQueuePromptButton.ts +++ b/src/lib/nodes/ComfyConfigureQueuePromptButton.ts @@ -30,9 +30,9 @@ export default class ComfyConfigureQueuePromptButton extends ComfyGraphNode { } if (typeof param === "string") - this.workflow.attrs.queuePromptButtonName = param || "" + this.workflow.setAttribute("queuePromptButtonName", param || "") else if (typeof param === "object" && "buttonName" in param) - this.workflow.attrs.queuePromptButtonName = param.buttonName || "" + this.workflow.setAttribute("queuePromptButtonName", param.buttonName || "") } } } diff --git a/src/lib/stores/workflowState.ts b/src/lib/stores/workflowState.ts index 0b8d1f2..b119593 100644 --- a/src/lib/stores/workflowState.ts +++ b/src/lib/stores/workflowState.ts @@ -99,6 +99,11 @@ export class ComfyWorkflow { store.set(get(store)); } + setAttribute(key: K, value: WorkflowAttributes[K]) { + this.attrs[key] = value; + this.notifyModified(); + } + start(key: string, canvas: ComfyGraphCanvas) { if (this.canvases[key] != null) throw new Error(`This workflow is already being displayed on canvas ${key}`) @@ -176,6 +181,7 @@ export class ComfyWorkflow { } this.graph.configure(data.graph); + this.graph.workflowID = this.id; for (const node of this.graph._nodes) { const size = node.computeSize(); @@ -228,9 +234,10 @@ function getWorkflow(id: WorkflowInstID): ComfyWorkflow | null { } function getWorkflowByGraph(graph: LGraph): ComfyWorkflow | null { - if ("workflowID" in graph && graph.workflowID != null) - return getWorkflow((graph as ComfyGraph).workflowID); - return null; + const workflowID = (graph.getRootGraph() as ComfyGraph)?.workflowID; + if (workflowID == null) + return null; + return getWorkflow(workflowID); } function getWorkflowByNode(node: LGraphNode): ComfyWorkflow | null { From 8990050ae0bb48b7515e8d4cc25c22e609b516b9 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 20 May 2023 23:34:29 -0500 Subject: [PATCH 14/15] Better add tab button --- src/lib/components/ComfyWorkflowsView.svelte | 65 +++++--------------- 1 file changed, 15 insertions(+), 50 deletions(-) diff --git a/src/lib/components/ComfyWorkflowsView.svelte b/src/lib/components/ComfyWorkflowsView.svelte index 80c5856..8dfa784 100644 --- a/src/lib/components/ComfyWorkflowsView.svelte +++ b/src/lib/components/ComfyWorkflowsView.svelte @@ -1,5 +1,6 @@