Work on subgraph support

This commit is contained in:
space-nuko
2023-05-16 12:42:49 -05:00
parent 96033e7628
commit 979f6eaeed
13 changed files with 145 additions and 114 deletions

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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<NodeID, SerializedPromptInputs>
export type SerializedPromptInputsAll = Record<ComfyNodeID, SerializedPromptInputs>
export type SerializedPrompt = {
workflow: SerializedLGraph,
output: SerializedPromptInputsAll
}
export type SerializedPromptOutputs = Record<NodeID, ComfyExecutionResult>
export type SerializedPromptOutputs = Record<ComfyNodeID, ComfyExecutionResult>
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<number> = new Set()
const backendUpdatedCombos: Record<NodeID, BackendComboNode> = {}
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];

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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<string> | null,
@@ -272,7 +272,7 @@ export abstract class ComfyWidgetNode<T = any> 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<T = any> 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)
}

View File

@@ -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<NodeID, DragItemEntry>,
allItemsByNode: Record<ComfyNodeID, DragItemEntry>,
/*
* Selected drag items.
@@ -663,8 +663,8 @@ type LayoutStateOps = {
groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => 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
}
}

View File

@@ -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<NodeID>,
cachedNodes: Set<NodeID>
nodesRan: Set<ComfyNodeID>,
cachedNodes: Set<ComfyNodeID>
}
export type CompletedQueueEntry = {
@@ -54,7 +54,7 @@ export type QueueState = {
queuePending: Writable<QueueEntry[]>,
queueCompleted: Writable<CompletedQueueEntry[]>,
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<QueueEntry[]>, 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)

View File

@@ -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 + ")"
}

View File

@@ -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'),

View File

@@ -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 }

View File

@@ -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);