From 979f6eaeedf9a8934522077ee8e430c550e5a701 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Tue, 16 May 2023 12:42:49 -0500 Subject: [PATCH] Work on subgraph support --- litegraph | 2 +- src/lib/ComfyGraph.ts | 6 +- src/lib/api.ts | 12 +-- src/lib/components/ComfyApp.ts | 82 +++++++++++---------- src/lib/components/ComfyPromptSerializer.ts | 2 +- src/lib/nodes/ComfyActionNodes.ts | 1 + src/lib/nodes/ComfyWidgetNodes.ts | 6 +- src/lib/stores/layoutState.ts | 16 ++-- src/lib/stores/queueState.ts | 22 +++--- src/lib/utils.ts | 60 +++++++-------- src/main-desktop.ts | 4 +- src/main-mobile.ts | 8 +- src/tests/ComfyPromptSerializerTests.ts | 38 ++++++++++ 13 files changed, 145 insertions(+), 114 deletions(-) diff --git a/litegraph b/litegraph index 7fb95e5..edc0ccb 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit 7fb95e5b83308c3ec8e3f561a31d99cbaec241ba +Subproject commit edc0ccbb086cf44db847e48f371b2793a19b1340 diff --git a/src/lib/ComfyGraph.ts b/src/lib/ComfyGraph.ts index 63cd1a9..3022664 100644 --- a/src/lib/ComfyGraph.ts +++ b/src/lib/ComfyGraph.ts @@ -26,17 +26,14 @@ export default class ComfyGraph extends LGraph { override onConfigure() { console.debug("Configured"); - this.eventBus.emit("configured", this); } override onBeforeChange(graph: LGraph, info: any) { console.debug("BeforeChange", info); - this.eventBus.emit("beforeChange", graph, info); } override onAfterChange(graph: LGraph, info: any) { console.debug("AfterChange", info); - this.eventBus.emit("afterChange", graph, info); } override onAfterExecute() { @@ -44,6 +41,9 @@ export default class ComfyGraph extends LGraph { } override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) { + if (options.subgraphs && options.subgraphs.length > 0) + return + layoutState.nodeAdded(node, options) // All nodes whether they come from base litegraph or ComfyBox should diff --git a/src/lib/api.ts b/src/lib/api.ts index c457de4..aaf256a 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,4 +1,4 @@ -import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutput, SerializedPromptOutputs } from "./components/ComfyApp"; +import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutputs } from "./components/ComfyApp"; import type TypedEmitter from "typed-emitter"; import EventEmitter from "events"; import type { ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes"; @@ -30,7 +30,7 @@ export type ComfyAPIQueueResponse = { error?: string } -export type NodeID = UUID; +export type ComfyNodeID = UUID; // To distinguish from Litegraph NodeID export type PromptID = UUID; // UUID export type ComfyAPIHistoryItem = [ @@ -38,7 +38,7 @@ export type ComfyAPIHistoryItem = [ PromptID, SerializedPromptInputsAll, ComfyBoxPromptExtraData, - NodeID[] // good outputs + ComfyNodeID[] // good outputs ] export type ComfyAPIPromptResponse = { @@ -76,9 +76,9 @@ type ComfyAPIEvents = { progress: (progress: Progress) => void, reconnecting: () => void, reconnected: () => void, - executing: (promptID: PromptID | null, runningNodeID: NodeID | null) => void, - executed: (promptID: PromptID, nodeID: NodeID, output: SerializedPromptOutput) => void, - execution_cached: (promptID: PromptID, nodes: NodeID[]) => void, + executing: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => void, + executed: (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutputs) => void, + execution_cached: (promptID: PromptID, nodes: ComfyNodeID[]) => void, execution_error: (promptID: PromptID, message: string) => void, } diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 821ef64..fa094b3 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -1,6 +1,6 @@ -import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot } 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 } from "@litegraph-ts/core"; import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core"; -import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type NodeID, type PromptID } from "$lib/api" +import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api" import { getPngMetadata, importA1111 } from "$lib/pnginfo"; import EventEmitter from "events"; import type TypedEmitter from "typed-emitter"; @@ -28,7 +28,7 @@ import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; import { get } from "svelte/store"; import { tick } from "svelte"; import uiState from "$lib/stores/uiState"; -import { download, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils"; +import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils"; import notify from "$lib/notify"; import configState from "$lib/stores/configState"; import { blankGraph } from "$lib/defaultGraph"; @@ -57,7 +57,7 @@ export type SerializedAppState = { } /** [link origin, link index] | value */ -export type SerializedPromptInput = [NodeID, number] | any +export type SerializedPromptInput = [ComfyNodeID, number] | any export type SerializedPromptInputs = { /* property name -> value or link */ @@ -65,14 +65,14 @@ export type SerializedPromptInputs = { class_type: string } -export type SerializedPromptInputsAll = Record +export type SerializedPromptInputsAll = Record export type SerializedPrompt = { workflow: SerializedLGraph, output: SerializedPromptInputsAll } -export type SerializedPromptOutputs = Record +export type SerializedPromptOutputs = Record export type Progress = { value: number, @@ -324,21 +324,21 @@ export default class ComfyApp { this.lGraph.setDirtyCanvas(true, false); }); - this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: NodeID | null) => { + this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: ComfyNodeID | null) => { queueState.executingUpdated(promptID, nodeID); this.lGraph.setDirtyCanvas(true, false); }); - this.api.addEventListener("executed", (promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) => { + this.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => { this.nodeOutputs[nodeID] = output; - const node = this.lGraph.getNodeById(nodeID) as ComfyGraphNode; + const node = this.lGraph.getNodeByIdRecursive(nodeID) as ComfyGraphNode; if (node?.onExecuted) { node.onExecuted(output); } queueState.onExecuted(promptID, nodeID, output) }); - this.api.addEventListener("execution_cached", (promptID: PromptID, nodes: NodeID[]) => { + this.api.addEventListener("execution_cached", (promptID: PromptID, nodes: ComfyNodeID[]) => { queueState.executionCached(promptID, nodes) }); @@ -490,6 +490,7 @@ export default class ComfyApp { } layoutState.onStartConfigure(); + this.lCanvas.closeAllSubgraphs(); this.lGraph.configure(blankGraph) layoutState.initDefaultLayout(); uiState.update(s => { @@ -588,6 +589,7 @@ export default class ComfyApp { const p = this.graphToPrompt(tag); const l = layoutState.serialize(); + console.debug(graphToGraphVis(this.lGraph)) console.debug(promptToGraphVis(p)) const extraData: ComfyBoxPromptExtraData = { @@ -619,19 +621,20 @@ export default class ComfyApp { error = response.error; } catch (err) { - error = err + error = { error: err } } if (error != null) { - const mes = error.response || error.toString() + const mes = error.error notify(`Error queuing prompt:\n${mes}`, { type: "error" }) + console.error(graphToGraphVis(this.lGraph)) console.error(promptToGraphVis(p)) console.error("Error queuing prompt", error, num, p) break; } for (const n of p.workflow.nodes) { - const node = this.lGraph.getNodeById(n.id); + const node = this.lGraph.getNodeByIdRecursive(n.id); if ("afterQueued" in node) { (node as ComfyGraphNode).afterQueued(p, tag); } @@ -689,8 +692,6 @@ export default class ComfyApp { async refreshComboInNodes(flashUI: boolean = false) { const defs = await this.api.getNodeDefs(); - const toUpdate: BackendComboNode[] = [] - const isComfyComboNode = (node: LGraphNode): boolean => { return node && node.type === "ui/combo" @@ -704,14 +705,14 @@ export default class ComfyApp { } // Node IDs of combo widgets attached to a backend node - let backendCombos: Set = new Set() + const backendUpdatedCombos: Record = {} console.debug("[refreshComboInNodes] start") // Figure out which combo nodes to update. They need to be connected to // an input slot on a backend node with a backend config in the input // slot connected to. - for (const node of this.lGraph.iterateNodesInOrder()) { + for (const node of this.lGraph.iterateNodesInOrderRecursive()) { if (!(node as any).isBackendNode) continue; @@ -738,40 +739,43 @@ export default class ComfyApp { const hasBackendConfig = def["input"]["required"][inputSlot.name] !== undefined if (hasBackendConfig) { - backendCombos.add(comboNode.id) - toUpdate.push({ comboNode, inputSlot, backendNode }) + backendUpdatedCombos[comboNode.id] = { comboNode, inputSlot, backendNode } } } } - console.debug("[refreshComboInNodes] found:", toUpdate.length, toUpdate) + console.debug("[refreshComboInNodes] found:", backendUpdatedCombos.length, backendUpdatedCombos) // Mark combo nodes without backend configs as being loaded already. - for (const node of this.lGraph.iterateNodesInOrder()) { - if (isComfyComboNode(node) && !backendCombos.has(node.id)) { - const comboNode = node as nodes.ComfyComboNode; - let values = comboNode.properties.values; - - // Frontend nodes can declare defaultWidgets which creates a - // config inside their own inputs slots too. - const foundInput = range(node.outputs.length) - .flatMap(i => node.getInputSlotsConnectedTo(i)) - .find(inp => "config" in inp && Array.isArray((inp.config as any).values)) - - if (foundInput != null) { - const comfyInput = foundInput as IComfyInputSlot; - console.warn("[refreshComboInNodes] found frontend config:", node.title, node.type, comfyInput.config.values) - values = comfyInput.config.values; - } - - comboNode.formatValues(values); + for (const node of this.lGraph.iterateNodesOfClassRecursive(nodes.ComfyComboNode)) { + if (backendUpdatedCombos[node.id] != null) { + continue; } + + // This node isn't connected to a backend node, so it's configured + // by the frontend instead. + const comboNode = node as nodes.ComfyComboNode; + let values = comboNode.properties.values; + + // Frontend nodes can declare defaultWidgets which creates a + // config inside their own inputs slots too. + const foundInput = range(node.outputs.length) + .flatMap(i => node.getInputSlotsConnectedTo(i)) + .find(inp => "config" in inp && Array.isArray((inp.config as any).values)) + + if (foundInput != null) { + const comfyInput = foundInput as IComfyInputSlot; + console.warn("[refreshComboInNodes] found frontend config:", node.title, node.type, comfyInput.config.values) + values = comfyInput.config.values; + } + + comboNode.formatValues(values); } await tick(); // Load definitions from the backend. - for (const { comboNode, inputSlot, backendNode } of toUpdate) { + for (const { comboNode, inputSlot, backendNode } of Object.values(backendUpdatedCombos)) { const def = defs[backendNode.type]; const rawValues = def["input"]["required"][inputSlot.name][0]; diff --git a/src/lib/components/ComfyPromptSerializer.ts b/src/lib/components/ComfyPromptSerializer.ts index 0b1dc27..b112d4f 100644 --- a/src/lib/components/ComfyPromptSerializer.ts +++ b/src/lib/components/ComfyPromptSerializer.ts @@ -166,7 +166,7 @@ export default class ComfyPromptSerializer { parent = null; } else { - console.debug("[graphToPrompt] Traverse upstream link", parent.id, nextParent?.id, nextParent?.isBackendNode) + console.debug("[graphToPrompt] Traverse upstream link", parent.id, nextParent?.id, (nextParent as any)?.isBackendNode) currentLink = nextLink; parent = nextParent; } diff --git a/src/lib/nodes/ComfyActionNodes.ts b/src/lib/nodes/ComfyActionNodes.ts index a34fa81..df614af 100644 --- a/src/lib/nodes/ComfyActionNodes.ts +++ b/src/lib/nodes/ComfyActionNodes.ts @@ -530,6 +530,7 @@ export class ComfySetNodeModeAdvancedAction extends ComfyGraphNode { } for (const [nodeId, newMode] of Object.entries(nodeChanges)) { + // NOTE: Only applies to this subgraph, not parent/child graphs. this.graph.getNodeById(nodeId).changeMode(newMode); } diff --git a/src/lib/nodes/ComfyWidgetNodes.ts b/src/lib/nodes/ComfyWidgetNodes.ts index 0185bdc..e1b559a 100644 --- a/src/lib/nodes/ComfyWidgetNodes.ts +++ b/src/lib/nodes/ComfyWidgetNodes.ts @@ -18,7 +18,7 @@ import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte"; import RadioWidget from "$lib/widgets/RadioWidget.svelte"; import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte"; import ImageEditorWidget from "$lib/widgets/ImageEditorWidget.svelte"; -import type { NodeID } from "$lib/api"; +import type { ComfyNodeID } from "$lib/api"; export type AutoConfigOptions = { includeProperties?: Set | null, @@ -272,7 +272,7 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { } if (options.setWidgetTitle) { - const widget = layoutState.findLayoutForNode(this.id as NodeID) + const widget = layoutState.findLayoutForNode(this.id as ComfyNodeID) if (widget && input.name !== "") { widget.attrs.title = input.name; } @@ -291,7 +291,7 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { } notifyPropsChanged() { - const layoutEntry = layoutState.findLayoutEntryForNode(this.id as NodeID) + const layoutEntry = layoutState.findLayoutEntryForNode(this.id as ComfyNodeID) if (layoutEntry && layoutEntry.parent) { layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1) } diff --git a/src/lib/stores/layoutState.ts b/src/lib/stores/layoutState.ts index b65d848..0c0f7de 100644 --- a/src/lib/stores/layoutState.ts +++ b/src/lib/stores/layoutState.ts @@ -4,7 +4,7 @@ import type ComfyApp from "$lib/components/ComfyApp" import { type LGraphNode, type IWidget, type LGraph, NodeMode, type LGraphRemoveNodeOptions, type LGraphAddNodeOptions, type UUID } from "@litegraph-ts/core" import { SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import type { ComfyWidgetNode } from '$lib/nodes'; -import type { NodeID } from '$lib/api'; +import type { ComfyNodeID } from '$lib/api'; import { v4 as uuidv4 } from "uuid"; type DragItemEntry = { @@ -60,7 +60,7 @@ export type LayoutState = { * Items indexed by the litegraph node they're bound to * Only contains drag items of type "widget" */ - allItemsByNode: Record, + allItemsByNode: Record, /* * Selected drag items. @@ -663,8 +663,8 @@ type LayoutStateOps = { groupItems: (dragItems: IDragItem[], attrs?: Partial) => ContainerLayout, ungroup: (container: ContainerLayout) => void, getCurrentSelection: () => IDragItem[], - findLayoutEntryForNode: (nodeId: NodeID) => DragItemEntry | null, - findLayoutForNode: (nodeId: NodeID) => IDragItem | null, + findLayoutEntryForNode: (nodeId: ComfyNodeID) => DragItemEntry | null, + findLayoutForNode: (nodeId: ComfyNodeID) => IDragItem | null, serialize: () => SerializedLayoutState, deserialize: (data: SerializedLayoutState, graph: LGraph) => void, initDefaultLayout: () => void, @@ -924,7 +924,7 @@ function ungroup(container: ContainerLayout) { store.set(state) } -function findLayoutEntryForNode(nodeId: NodeID): DragItemEntry | null { +function findLayoutEntryForNode(nodeId: ComfyNodeID): DragItemEntry | null { const state = get(store) const found = Object.entries(state.allItems).find(pair => pair[1].dragItem.type === "widget" @@ -934,7 +934,7 @@ function findLayoutEntryForNode(nodeId: NodeID): DragItemEntry | null { return null; } -function findLayoutForNode(nodeId: NodeID): WidgetLayout | null { +function findLayoutForNode(nodeId: ComfyNodeID): WidgetLayout | null { const found = findLayoutEntryForNode(nodeId); if (!found) return null; @@ -1034,7 +1034,9 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) { if (dragItem.type === "widget") { const widget = dragItem as WidgetLayout; - widget.node = graph.getNodeById(entry.dragItem.nodeId) as ComfyWidgetNode + widget.node = graph.getNodeByIdRecursive(entry.dragItem.nodeId) as ComfyWidgetNode + if (widget.node == null) + throw (`Node in litegraph not found! ${entry.dragItem.nodeId}`) allItemsByNode[entry.dragItem.nodeId] = dragEntry } } diff --git a/src/lib/stores/queueState.ts b/src/lib/stores/queueState.ts index 32a9f4d..339f4b9 100644 --- a/src/lib/stores/queueState.ts +++ b/src/lib/stores/queueState.ts @@ -1,4 +1,4 @@ -import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, NodeID, PromptID } from "$lib/api"; +import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, ComfyNodeID, PromptID } from "$lib/api"; import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp"; import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes"; import notify from "$lib/notify"; @@ -14,12 +14,12 @@ type QueueStateOps = { queueUpdated: (resp: ComfyAPIQueueResponse) => void, historyUpdated: (resp: ComfyAPIHistoryResponse) => void, statusUpdated: (status: ComfyAPIStatusResponse | null) => void, - executingUpdated: (promptID: PromptID | null, runningNodeID: NodeID | null) => void, - executionCached: (promptID: PromptID, nodes: NodeID[]) => void, + executingUpdated: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => void, + 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: NodeID, output: ComfyExecutionResult) => void + onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => void } export type QueueEntry = { @@ -30,7 +30,7 @@ export type QueueEntry = { promptID: PromptID, prompt: SerializedPromptInputsAll, extraData: ComfyBoxPromptExtraData, - goodOutputs: NodeID[], + goodOutputs: ComfyNodeID[], /* Data not sent by ComfyUI's API, lost on page refresh */ @@ -38,8 +38,8 @@ export type QueueEntry = { outputs: SerializedPromptOutputs, /* Nodes in of the workflow that have finished running so far. */ - nodesRan: Set, - cachedNodes: Set + nodesRan: Set, + cachedNodes: Set } export type CompletedQueueEntry = { @@ -54,7 +54,7 @@ export type QueueState = { queuePending: Writable, queueCompleted: Writable, queueRemaining: number | "X" | null; - runningNodeID: NodeID | null; + runningNodeID: ComfyNodeID | null; progress: Progress | null, isInterrupting: boolean } @@ -161,7 +161,7 @@ function moveToCompleted(index: number, queue: Writable, status: Q store.set(state) } -function executingUpdated(promptID: PromptID, runningNodeID: NodeID | null) { +function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null) { console.debug("[queueState] executingUpdated", promptID, runningNodeID) store.update((s) => { s.progress = null; @@ -199,7 +199,7 @@ function executingUpdated(promptID: PromptID, runningNodeID: NodeID | null) { }) } -function executionCached(promptID: PromptID, nodes: NodeID[]) { +function executionCached(promptID: PromptID, nodes: ComfyNodeID[]) { console.debug("[queueState] executionCached", promptID, nodes) store.update(s => { const [index, entry, queue] = findEntryInPending(promptID); @@ -257,7 +257,7 @@ function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromp }) } -function onExecuted(promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) { +function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) { console.debug("[queueState] onExecuted", promptID, nodeID, output) store.update(s => { const [index, entry, queue] = findEntryInPending(promptID) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 31b5c23..993292d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -6,8 +6,9 @@ import { get } from "svelte/store" import layoutState from "$lib/stores/layoutState" import type { SvelteComponentDev } from "svelte/internal"; import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, GraphInput } from "@litegraph-ts/core"; -import type { FileNameOrGalleryData, ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes"; +import type { ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes"; import type { FileData as GradioFileData } from "@gradio/upload"; +import type { ComfyNodeID } from "./api"; export function clamp(n: number, min: number, max: number): number { return Math.min(Math.max(n, min), max) @@ -121,7 +122,7 @@ export function graphToGraphVis(graph: LGraph): string { subgraphs[node.graph._subgraph_node.id][1].push(linkText) subgraphNodes[node.graph._subgraph_node.id] = node.graph._subgraph_node } - else if (!node.is(Subgraph) && !node.graph.getNodeById(link.target_id)?.is(Subgraph)) { + else { links.push(linkText) } } @@ -130,39 +131,23 @@ export function graphToGraphVis(graph: LGraph): string { } let out = "digraph {\n" - out += " node [shape=box];\n" + out += ' fontname="Helvetica,Arial,sans-serif"\n' + out += ' node [fontname="Helvetica,Arial,sans-serif"]\n' + out += ' edge [fontname="Helvetica,Arial,sans-serif"]\n' + out += ' node [shape=box style=filled fillcolor="#DDDDDD"]\n' for (const [subgraph, links] of Object.values(subgraphs)) { // Subgraph name has to be prefixed with "cluster" to show up as a cluster... out += ` subgraph cluster_subgraph_${convId(subgraph.id)} {\n` - out += ` label="${convId(subgraph.id)}: ${subgraph.title}";\n`; + out += ` label="${convId(subgraph.id)}_${subgraph.title}";\n`; out += " color=red;\n"; // out += " style=grey;\n"; - out += " node [style=filled,fillcolor=white];\n"; out += " " + links.join(" ") out += " }\n" } out += links.join("") - for (const subgraphNode of Object.values(subgraphNodes)) { - for (const [index, input] of enumerate(subgraphNode.iterateInputInfo())) { - const link = subgraphNode.getInputLink(index); - if (link) { - const inputNode = subgraphNode.getInputNode(link.origin_slot); - const innerInput = subgraphNode.getInnerGraphInputByIndex(index); - out += ` "${convId(link.origin_id)}_${inputNode.title}" -> "${convId(innerInput.id)}_${innerInput.title}";\n` - } - } - for (const [index, output] of enumerate(subgraphNode.iterateOutputInfo())) { - for (const link of subgraphNode.getOutputLinks(index)) { - const outputNode = subgraphNode.graph.getNodeById(link.target_id) - const innerOutput = subgraphNode.getInnerGraphOutputByIndex(index); - out += ` "${convId(innerOutput.id)}_${innerOutput.title}" -> "${convId(link.origin_id)}_${outputNode.title}";\n` - } - } - } - out += "}" return out } @@ -186,17 +171,21 @@ export function promptToGraphVis(prompt: SerializedPrompt): string { for (const pair of Object.entries(prompt.output)) { const [id, o] = pair; const outNode = prompt.workflow.nodes.find(n => n.id == id) - for (const pair2 of Object.entries(o.inputs)) { - const [inpName, i] = pair2; + if (outNode) { + for (const pair2 of Object.entries(o.inputs)) { + const [inpName, i] = pair2; - if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") { - // Link - const inpNode = prompt.workflow.nodes.find(n => n.id == i[0]) - out += `"${inpNode.title}" -> "${outNode.title}"\n` - } - else { - // Value - out += `"${id}-${inpName}-${i}" -> "${outNode.title}"\n` + if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") { + // Link + const inpNode = prompt.workflow.nodes.find(n => n.id == i[0]) + if (inpNode) { + out += `"${inpNode.title}" -> "${outNode.title}"\n` + } + } + else { + // Value + out += `"${id}-${inpName}-${i}" -> "${outNode.title}"\n` + } } } } @@ -205,12 +194,13 @@ export function promptToGraphVis(prompt: SerializedPrompt): string { return out } -export function getNodeInfo(nodeId: NodeID): string { +export function getNodeInfo(nodeId: ComfyNodeID): string { let app = (window as any).app; if (!app || !app.lGraph) return String(nodeId); - const title = app.lGraph.getNodeById(nodeId)?.title || String(nodeId); + // TODO subgraph support + const title = app.lGraph.getNodeByIdRecursive(nodeId)?.title || String(nodeId); return title + " (" + nodeId + ")" } diff --git a/src/main-desktop.ts b/src/main-desktop.ts index ea2019f..15f8e17 100644 --- a/src/main-desktop.ts +++ b/src/main-desktop.ts @@ -1,7 +1,7 @@ -import { LiteGraph } from '@litegraph-ts/core'; +import { configureLitegraph } from '$lib/init'; import App from './App.svelte'; -LiteGraph.use_uuids = true; +configureLitegraph() const app = new App({ target: document.getElementById('app'), diff --git a/src/main-mobile.ts b/src/main-mobile.ts index 4f30b66..f306861 100644 --- a/src/main-mobile.ts +++ b/src/main-mobile.ts @@ -6,18 +6,14 @@ import ComfyApp from '$lib/components/ComfyApp'; import uiState from '$lib/stores/uiState'; import { LiteGraph } from '@litegraph-ts/core'; import ComfyGraph from '$lib/ComfyGraph'; +import { configureLitegraph } from '$lib/init'; Framework7.use(Framework7Svelte); -LiteGraph.use_uuids = true; -LiteGraph.dialog_close_on_mouse_leave = false; -LiteGraph.search_hide_on_mouse_leave = false; -LiteGraph.pointerevents_method = "pointer"; +configureLitegraph(true); const comfyApp = new ComfyApp(); -uiState.update(s => { s.app = comfyApp; return s; }) - const app = new AppMobile({ target: document.getElementById('app'), props: { app: comfyApp } diff --git a/src/tests/ComfyPromptSerializerTests.ts b/src/tests/ComfyPromptSerializerTests.ts index 9bce926..946afa1 100644 --- a/src/tests/ComfyPromptSerializerTests.ts +++ b/src/tests/ComfyPromptSerializerTests.ts @@ -131,6 +131,44 @@ export default class ComfyPromptSerializerTests extends UnitTest { const result = ser.serialize(graph) + expect(Object.keys(result.output)).toHaveLength(3); + expect(result.output[input.id].inputs["in"]).toBeInstanceOf(Array) + expect(result.output[input.id].inputs["in"][0]).toEqual(link.id) + expect(result.output[link.id].inputs["in"]).toBeInstanceOf(Array) + expect(result.output[link.id].inputs["in"][0]).toEqual(output.id) + expect(result.output[output.id].inputs).toEqual({}) + } + + test__serialize__shouldFollowSubgraphsRecursively() { + const ser = new ComfyPromptSerializer(); + const graph = new ComfyGraph(); + + const output = LiteGraph.createNode(MockBackendOutput) + const link = LiteGraph.createNode(MockBackendLink) + const input = LiteGraph.createNode(MockBackendInput) + + const subgraphA = LiteGraph.createNode(Subgraph) + const subgraphB = LiteGraph.createNode(Subgraph) + const graphInputA = subgraphA.addGraphInput("testIn", "number") + const graphOutputA = subgraphA.addGraphOutput("testOut", "number") + const graphInputB = subgraphB.addGraphInput("testIn", "number") + const graphOutputB = subgraphB.addGraphOutput("testOut", "number") + + graph.add(subgraphA) + subgraphA.subgraph.add(subgraphB) + graph.add(output) + subgraphB.subgraph.add(link) + graph.add(input) + + output.connect(0, subgraphA, 0) + graphInputA.innerNode.connect(0, subgraphB, 0) + graphInputB.innerNode.connect(0, link, 0) + link.connect(0, graphOutputB.innerNode, 0) + subgraphB.connect(0, graphOutputA.innerNode, 0) + subgraphA.connect(0, input, 0) + + const result = ser.serialize(graph) + console.warn(graphToGraphVis(graph)) console.warn(result.output) expect(Object.keys(result.output)).toHaveLength(3);