From 7ddda80cf6e5c6debe1617469dd1815c2329e095 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Fri, 5 May 2023 16:46:28 -0500 Subject: [PATCH] Workflow properties --- src/lib/ComfyGraphCanvas.ts | 8 + src/lib/components/ComfyApp.svelte | 34 +- src/lib/components/ComfyApp.ts | 52 ++- src/lib/components/ComfyComboProperty.svelte | 54 +++ src/lib/components/ComfyNumberProperty.svelte | 45 +++ src/lib/components/ComfyProperties.svelte | 313 ++++++++++-------- src/lib/nodes/ComfyActionNodes.ts | 16 +- src/lib/nodes/ComfyBackendNode.ts | 17 + src/lib/nodes/ComfyWidgetNodes.ts | 29 +- src/lib/stores/layoutState.ts | 46 ++- src/lib/stores/uiState.ts | 6 +- src/lib/utils.ts | 43 ++- src/scss/global.scss | 1 - src/scss/ux.scss | 4 + 14 files changed, 489 insertions(+), 179 deletions(-) create mode 100644 src/lib/components/ComfyComboProperty.svelte create mode 100644 src/lib/components/ComfyNumberProperty.svelte diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index 24279be..cd16148 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -3,6 +3,7 @@ import type ComfyApp from "./components/ComfyApp"; import queueState from "./stores/queueState"; import { get } from "svelte/store"; import uiState from "./stores/uiState"; +import layoutState from "./stores/layoutState"; export type SerializedGraphCanvasState = { offset: Vector2, @@ -230,6 +231,13 @@ export default class ComfyGraphCanvas extends LGraphCanvas { return res; } + override onNodeSelected(node: LGraphNode) { + const ls = get(layoutState) + ls.currentSelectionNodes = [node] + ls.currentSelection = [] + layoutState.set(ls) + } + override onNodeMoved(node: LGraphNode) { if (super.onNodeMoved) super.onNodeMoved(node); diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index f11ce46..38d7be9 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -6,7 +6,7 @@ import { BlockTitle } from "@gradio/atoms"; import ComfyUIPane from "./ComfyUIPane.svelte"; import ComfyApp, { type SerializedAppState } from "./ComfyApp"; - import { Checkbox } from "@gradio/form" + import { Checkbox, TextBox } from "@gradio/form" import uiState from "$lib/stores/uiState"; import layoutState from "$lib/stores/layoutState"; import { ImageViewer } from "$lib/ImageViewer"; @@ -29,6 +29,7 @@ let containerElem: HTMLDivElement; let resizeTimeout: NodeJS.Timeout | null; let hasShownUIHelpToast: boolean = false; + let uiTheme: string = ""; let debugLayout: boolean = false; @@ -46,7 +47,10 @@ function queuePrompt() { console.log("Queuing!"); - app.queuePrompt(0, 1); + let subworkflow = $uiState.subWorkflow; + if (subworkflow === "") + subworkflow = null + app.queuePrompt(0, 1, subworkflow); } $: if (app?.lCanvas) app.lCanvas.allow_dragnodes = !$uiState.nodesLocked; @@ -164,6 +168,12 @@ } + + {#if uiTheme === "anapnoe"} + + {/if} + +
@@ -220,16 +230,24 @@ - - + + + -
diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index b406252..09f38b1 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -25,6 +25,7 @@ import ComfyGraph from "$lib/ComfyGraph"; import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; import { get } from "svelte/store"; import uiState from "$lib/stores/uiState"; +import { promptToGraphVis, toGraphVis } from "$lib/utils"; export const COMFYBOX_SERIAL_VERSION = 1; @@ -54,9 +55,11 @@ export type SerializedPromptInputs = { class_type: string } +export type SerializedPromptOutput = Record + export type SerializedPrompt = { workflow: SerializedLGraph, - output: Record + output: SerializedPromptOutput } export type Progress = { @@ -420,7 +423,7 @@ export default class ComfyApp { * Converts the current graph workflow for sending to the API * @returns The workflow and node links */ - async graphToPrompt(): Promise { + async graphToPrompt(tag: string | null = null): Promise { // Run frontend-only logic this.lGraph.runStep(1) @@ -438,6 +441,11 @@ export default class ComfyApp { const node = node_ as ComfyBackendNode; + if (tag && node.tags.indexOf(tag) === -1) { + console.debug("Skipping tagged node", tag, node.tags) + continue; + } + if (node.mode === NodeMode.NEVER) { // Don't serialize muted nodes continue; @@ -451,6 +459,11 @@ export default class ComfyApp { const inp = node.inputs[i]; const inputLink = node.getInputLink(i) const inputNode = node.getInputNode(i) + + if (inputNode && tag && "tags" in inputNode && (inputNode.tags as string[]).indexOf(tag) === -1) { + continue; + } + if (!inputLink || !inputNode) { if ("config" in inp) { const defaultValue = (inp as IComfyInputSlot).config?.defaultValue @@ -489,17 +502,36 @@ export default class ComfyApp { if (parent) { const seen = {} let link = node.getInputLink(i); - while (parent && !parent.isBackendNode) { + + const isValidParent = (parent: ComfyGraphNode) => { + if (!parent || parent.isBackendNode) + return false; + if ("tags" in parent && (parent.tags as string[]).indexOf(tag) === -1) + return false; + return true; + } + + while (isValidParent(parent)) { link = parent.getInputLink(link.origin_slot); if (link && !seen[link.id]) { seen[link.id] = true - parent = parent.getInputNode(link.origin_slot) as ComfyGraphNode; + const inputNode = parent.getInputNode(link.origin_slot) as ComfyGraphNode; + if (inputNode && "tags" in inputNode && tag && (inputNode.tags as string[]).indexOf(tag) === -1) { + console.debug("Skipping tagged parent node", tag, node.tags) + parent = null; + } + else { + parent = inputNode; + } } else { parent = null; } } if (link && parent && parent.isBackendNode) { + if ("tags" in parent && tag && (parent.tags as string[]).indexOf(tag) === -1) + continue; + const input = node.inputs[i] // TODO can null be a legitimate value in some cases? // Nodes like CLIPLoader will never have a value in the frontend, hence "null". @@ -522,15 +554,19 @@ export default class ComfyApp { if (Array.isArray(output[o].inputs[i]) && output[o].inputs[i].length === 2 && !output[output[o].inputs[i][0]]) { + console.debug("Prune removed node link", o, i, output[o].inputs[i]) delete output[o].inputs[i]; } } } + console.warn({ workflow, output }) + console.warn(promptToGraphVis({ workflow, output })) + return { workflow, output }; } - async queuePrompt(num: number, batchCount: number = 1) { + async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) { this.queueItems.push({ num, batchCount }); // Only have one action process the items so each one gets a unique seed correctly @@ -545,7 +581,7 @@ export default class ComfyApp { console.log(`Queue get! ${num} ${batchCount}`); for (let i = 0; i < batchCount; i++) { - const p = await this.graphToPrompt(); + const p = await this.graphToPrompt(tag); try { await this.api.queuePrompt(num, p); @@ -626,12 +662,8 @@ export default class ComfyApp { if ("config" in input) { const comfyInput = input as IComfyInputSlot; - console.warn("RefreshCombo", comfyInput.defaultWidgetNode, comfyInput) - if (comfyInput.defaultWidgetNode == nodes.ComfyComboNode && def["input"]["required"][comfyInput.name] !== undefined) { comfyInput.config.values = def["input"]["required"][comfyInput.name][0]; - - console.warn("RefreshCombo", comfyInput.config.values, def["input"]["required"][comfyInput.name]) const inputNode = node.getInputNode(index) if (inputNode && "doAutoConfig" in inputNode) { diff --git a/src/lib/components/ComfyComboProperty.svelte b/src/lib/components/ComfyComboProperty.svelte new file mode 100644 index 0000000..2ccc0b3 --- /dev/null +++ b/src/lib/components/ComfyComboProperty.svelte @@ -0,0 +1,54 @@ + + + + + diff --git a/src/lib/components/ComfyNumberProperty.svelte b/src/lib/components/ComfyNumberProperty.svelte new file mode 100644 index 0000000..5e96516 --- /dev/null +++ b/src/lib/components/ComfyNumberProperty.svelte @@ -0,0 +1,45 @@ + + + + + diff --git a/src/lib/components/ComfyProperties.svelte b/src/lib/components/ComfyProperties.svelte index 82b7c03..b5aad70 100644 --- a/src/lib/components/ComfyProperties.svelte +++ b/src/lib/components/ComfyProperties.svelte @@ -2,10 +2,11 @@ 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 } from "$lib/stores/layoutState" + import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState" import { get } from "svelte/store" - import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode"; - import type { ComfyWidgetNode } from "$lib/nodes"; + import type { ComfyWidgetNode } from "$lib/nodes"; + import ComfyNumberProperty from "./ComfyNumberProperty.svelte"; + import ComfyComboProperty from "./ComfyComboProperty.svelte"; let target: IDragItem | null = null; let node: LGraphNode | null = null; @@ -20,6 +21,10 @@ node = null; } } + else if ($layoutState.currentSelectionNodes.length > 0) { + target = null; + node = $layoutState.currentSelectionNodes[0] + } else { target = null node = null; @@ -29,14 +34,14 @@ $: { if (node != null) - targetType = node.type || "Widget" + targetType = node.type || "Node" else if (target) - targetType = "group" + targetType = "Group" else - targetType = "???" + targetType = "" } - function updateAttribute(entry: any, value: any) { + function updateAttribute(entry: AttributesSpec, value: any) { if (target) { const name = entry.name console.warn("updateAttribute", name, value) @@ -51,7 +56,7 @@ } } - function updateProperty(entry: any, value: any) { + function updateProperty(entry: AttributesSpec, value: any) { if (node) { const name = entry.name console.warn("updateProperty", name, value) @@ -64,125 +69,195 @@ } } } + + function getVar(node: LGraphNode, entry: AttributesSpec) { + let value = node[entry.name] + if (entry.serialize) + value = entry.serialize(value) + console.debug("[ComfyProperties] getVar", entry, value, node) + return value + } + + function updateVar(entry: any, value: any) { + if (node) { + const name = entry.name + console.warn("updateProperty", name, value) + + if (entry.deserialize) + value = entry.deserialize(value) + + console.debug("[ComfyProperties] updateVar", entry, value, name, node) + node[name] = value; + + if ("propsChanged" in node) { + const comfyNode = node as ComfyWidgetNode + comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1) + } + } + } + + function updateWorkflowAttribute(entry: AttributesSpec, value: any) { + const name = entry.name + console.warn("updateWorkflowAttribute", name, value) + + $layoutState.attrs[name] = value + $layoutState = $layoutState + }
- {#if target} -
-
- - {target.attrs.title} +
+
+ + {target?.attrs?.title || node?.title || "Workflow"} + {#if targetType !== ""} ({targetType}) + {/if} + +
+
+
+ {#each ALL_ATTRIBUTES as category(category.categoryName)} +
+ + {category.categoryName}
-
-
- {#each ALL_ATTRIBUTES as category(category.categoryName)} -
- - {category.categoryName} - -
- {#each category.specs as spec(spec.name)} - {#if spec.location === "widget" && spec.name in target.attrs} + {#each category.specs as spec(spec.name)} + {#if spec.location === "widget" && target && spec.name in target.attrs} +
+ {#if spec.type === "string"} + updateAttribute(spec, e.detail)} + on:input={(e) => updateAttribute(spec, e.detail)} + label={spec.name} + max_lines={1} + /> + {:else if spec.type === "boolean"} + updateAttribute(spec, e.detail)} + label={spec.name} + /> + {:else if spec.type === "number"} + updateAttribute(spec, e.detail)} + /> + {:else if spec.type === "enum"} + updateAttribute(spec, e.detail)} + /> + {/if} +
+ {:else if node} + {#if spec.location === "nodeProps" && spec.name in node.properties}
{#if spec.type === "string"} updateAttribute(spec, e.detail)} - on:input={(e) => updateAttribute(spec, e.detail)} + value={node.properties[spec.name]} + on:change={(e) => updateProperty(spec, e.detail)} + on:input={(e) => updateProperty(spec, e.detail)} label={spec.name} max_lines={1} /> {:else if spec.type === "boolean"} updateAttribute(spec, e.detail)} + value={node.properties[spec.name]} + label={spec.name} + on:change={(e) => 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 spec.location === "nodeVars" && spec.name in node} +
+ {#if spec.type === "string"} + updateVar(spec, e.detail)} + on:input={(e) => updateVar(spec, e.detail)} + label={spec.name} + max_lines={1} + /> + {:else if spec.type === "boolean"} + updateVar(spec, e.detail)} label={spec.name} /> {:else if spec.type === "number"} - + updateVar(spec, e.detail)} + /> {:else if spec.type === "enum"} - + updateVar(spec, e.detail)} + /> {/if}
- {:else if node} - {#if spec.location === "nodeProps" && spec.name in node.properties} -
- {#if spec.type === "string"} - updateProperty(spec, e.detail)} - on:input={(e) => updateProperty(spec, e.detail)} - label={spec.name} - max_lines={1} - /> - {:else if spec.type === "boolean"} - updateProperty(spec, e.detail)} - label={spec.name} - /> - {:else if spec.type === "number"} - - {:else if spec.type === "enum"} - - {/if} -
- {/if} {/if} - {/each} + {:else if spec.location === "workflow" && spec.name in $layoutState.attrs} +
+ {#if spec.type === "string"} + updateWorkflowAttribute(spec, e.detail)} + on:input={(e) => updateWorkflowAttribute(spec, e.detail)} + label={spec.name} + max_lines={1} + /> + {:else if spec.type === "boolean"} + updateWorkflowAttribute(spec, e.detail)} + label={spec.name} + /> + {:else if spec.type === "number"} + updateWorkflowAttribute(spec, e.detail)} + /> + {:else if spec.type === "enum"} + updateWorkflowAttribute(spec, e.detail)} + /> + {/if} +
+ {/if} {/each} -
- {/if} + {/each} +