Account for upstream links when refreshing combo widgets
This commit is contained in:
@@ -53,8 +53,6 @@
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
};
|
||||
|
||||
const tt = "asd\nasdlkj"
|
||||
</script>
|
||||
|
||||
{#if container && children}
|
||||
|
||||
@@ -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 } 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 } 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";
|
||||
@@ -34,7 +34,7 @@ import notify from "$lib/notify";
|
||||
import configState from "$lib/stores/configState";
|
||||
import { blankGraph } from "$lib/defaultGraph";
|
||||
import type { ComfyExecutionResult } from "$lib/utils";
|
||||
import ComfyPromptSerializer from "./ComfyPromptSerializer";
|
||||
import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer";
|
||||
import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef";
|
||||
import { ComfyComboNode } from "$lib/nodes/widgets";
|
||||
|
||||
@@ -80,7 +80,7 @@ export type Progress = {
|
||||
|
||||
type BackendComboNode = {
|
||||
comboNode: ComfyComboNode,
|
||||
inputSlot: IComfyInputSlot,
|
||||
comfyInput: IComfyInputSlot,
|
||||
backendNode: ComfyBackendNode
|
||||
}
|
||||
|
||||
@@ -709,13 +709,13 @@ export default class ComfyApp {
|
||||
async refreshComboInNodes(flashUI: boolean = false) {
|
||||
const defs = await this.api.getNodeDefs();
|
||||
|
||||
const isComfyComboNode = (node: LGraphNode): boolean => {
|
||||
const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => {
|
||||
return node
|
||||
&& node.type === "ui/combo"
|
||||
&& "doAutoConfig" in node;
|
||||
}
|
||||
|
||||
const isComfyComboInput = (input: INodeInputSlot) => {
|
||||
const isComfyComboInput = (input: INodeInputSlot): input is IComfyInputSlot => {
|
||||
return "config" in input
|
||||
&& "widgetNodeType" in input
|
||||
&& input.widgetNodeType === "ui/combo";
|
||||
@@ -729,34 +729,42 @@ export default class ComfyApp {
|
||||
// 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.
|
||||
const nodeLocator = new UpstreamNodeLocator(isComfyComboNode)
|
||||
|
||||
const findComfyInputAndAttachedCombo = (node: LGraphNode, i: SlotIndex): [IComfyInputSlot, ComfyComboNode] | null => {
|
||||
const input = node.inputs[i]
|
||||
|
||||
// Does this input autocreate a combo box on creation?
|
||||
const isComfyInput = isComfyComboInput(input)
|
||||
if (!isComfyInput)
|
||||
return null;
|
||||
|
||||
// Find an attached combo node even if it's inside/outside of a
|
||||
// subgraph, linked after several nodes, etc.
|
||||
const [comboNode, _link] = nodeLocator.locateUpstream(node, i, null);
|
||||
|
||||
if (comboNode == null)
|
||||
return null;
|
||||
|
||||
const result: [IComfyInputSlot, ComfyComboNode] = [input, comboNode as ComfyComboNode]
|
||||
return result
|
||||
}
|
||||
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
if (!(node as any).isBackendNode)
|
||||
if (!isActiveBackendNode(node))
|
||||
continue;
|
||||
|
||||
const backendNode = (node as ComfyBackendNode)
|
||||
const found = range(backendNode.inputs.length)
|
||||
.filter(i => {
|
||||
const input = backendNode.inputs[i]
|
||||
const inputNode = backendNode.getInputNode(i)
|
||||
const found = range(node.inputs.length)
|
||||
.map((i) => findComfyInputAndAttachedCombo(node, i))
|
||||
.filter(Boolean);
|
||||
|
||||
// Does this input autocreate a combo box on creation?
|
||||
const isComfyInput = isComfyComboInput(input)
|
||||
const isComfyCombo = isComfyComboNode(inputNode)
|
||||
for (const [comfyInput, comboNode] of found) {
|
||||
const def = defs[node.type];
|
||||
|
||||
// console.debug("[refreshComboInNodes] CHECK", backendNode.type, input.name, "isComfyCombo", isComfyCombo, "isComfyInput", isComfyInput)
|
||||
|
||||
return isComfyCombo && isComfyInput
|
||||
});
|
||||
|
||||
for (const inputIndex of found) {
|
||||
const comboNode = backendNode.getInputNode(inputIndex) as ComfyComboNode
|
||||
const inputSlot = backendNode.inputs[inputIndex] as IComfyInputSlot;
|
||||
const def = defs[backendNode.type];
|
||||
|
||||
const hasBackendConfig = def["input"]["required"][inputSlot.name] !== undefined
|
||||
const hasBackendConfig = def["input"]["required"][comfyInput.name] !== undefined
|
||||
|
||||
if (hasBackendConfig) {
|
||||
backendUpdatedCombos[comboNode.id] = { comboNode, inputSlot, backendNode }
|
||||
backendUpdatedCombos[comboNode.id] = { comboNode, comfyInput, backendNode: node }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -792,12 +800,12 @@ export default class ComfyApp {
|
||||
await tick();
|
||||
|
||||
// Load definitions from the backend.
|
||||
for (const { comboNode, inputSlot, backendNode } of Object.values(backendUpdatedCombos)) {
|
||||
for (const { comboNode, comfyInput, backendNode } of Object.values(backendUpdatedCombos)) {
|
||||
const def = defs[backendNode.type];
|
||||
const rawValues = def["input"]["required"][inputSlot.name][0];
|
||||
const rawValues = def["input"]["required"][comfyInput.name][0];
|
||||
|
||||
console.debug("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, rawValues.length)
|
||||
comboNode.doAutoConfig(inputSlot, { includeProperties: new Set(["values"]), setWidgetTitle: false })
|
||||
comboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false })
|
||||
|
||||
comboNode.formatValues(rawValues as string[])
|
||||
if (!rawValues?.includes(get(comboNode.value))) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type ComfyGraph from "$lib/ComfyGraph";
|
||||
import type { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import { GraphInput, GraphOutput, LGraph, LGraphNode, LLink, NodeMode, Subgraph } from "@litegraph-ts/core";
|
||||
import { GraphInput, GraphOutput, LGraph, LGraphNode, LLink, NodeMode, Subgraph, type SlotIndex } from "@litegraph-ts/core";
|
||||
import type { SerializedPrompt, SerializedPromptInput, SerializedPromptInputs, SerializedPromptInputsAll } from "./ComfyApp";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
|
||||
@@ -9,8 +9,8 @@ function hasTag(node: LGraphNode, tag: string): boolean {
|
||||
return "tags" in node.properties && node.properties.tags.indexOf(tag) !== -1
|
||||
}
|
||||
|
||||
function isActiveBackendNode(node: ComfyGraphNode, tag: string | null): node is ComfyBackendNode {
|
||||
if (!node.isBackendNode)
|
||||
export function isActiveNode(node: LGraphNode, tag: string | null = null): boolean {
|
||||
if (!node)
|
||||
return false;
|
||||
|
||||
if (tag && !hasTag(node, tag)) {
|
||||
@@ -18,7 +18,7 @@ function isActiveBackendNode(node: ComfyGraphNode, tag: string | null): node is
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.mode === NodeMode.NEVER) {
|
||||
if (node.mode !== NodeMode.ALWAYS) {
|
||||
// Don't serialize muted nodes
|
||||
return false;
|
||||
}
|
||||
@@ -26,48 +26,122 @@ function isActiveBackendNode(node: ComfyGraphNode, tag: string | null): node is
|
||||
return true;
|
||||
}
|
||||
|
||||
function followSubgraph(subgraph: Subgraph, link: LLink): [LGraph | null, LLink | null] {
|
||||
if (link.origin_id != subgraph.id)
|
||||
throw new Error("A!")
|
||||
export function isActiveBackendNode(node: LGraphNode, tag: string | null = null): node is ComfyBackendNode {
|
||||
if (!(node as any).isBackendNode)
|
||||
return false;
|
||||
|
||||
const innerGraphOutput = subgraph.getInnerGraphOutputByIndex(link.origin_slot)
|
||||
if (innerGraphOutput == null)
|
||||
throw new Error("No inner graph input!")
|
||||
|
||||
const nextLink = innerGraphOutput.getInputLink(0)
|
||||
return [innerGraphOutput.graph, nextLink];
|
||||
return isActiveNode(node, tag);
|
||||
}
|
||||
|
||||
function followGraphInput(graphInput: GraphInput, link: LLink): [LGraph | null, LLink | null] {
|
||||
if (link.origin_id != graphInput.id)
|
||||
throw new Error("A!")
|
||||
|
||||
const outerSubgraph = graphInput.getParentSubgraph();
|
||||
if (outerSubgraph == null)
|
||||
throw new Error("No outer subgraph!")
|
||||
|
||||
const outerInputIndex = outerSubgraph.inputs.findIndex(i => i.name === graphInput.nameInGraph)
|
||||
if (outerInputIndex == null)
|
||||
throw new Error("No outer input slot!")
|
||||
|
||||
const nextLink = outerSubgraph.getInputLink(outerInputIndex)
|
||||
return [outerSubgraph.graph, nextLink];
|
||||
}
|
||||
|
||||
function getUpstreamLink(parent: LGraphNode, currentLink: LLink): [LGraph | null, LLink | null] {
|
||||
if (parent.is(Subgraph)) {
|
||||
console.warn("FollowSubgraph")
|
||||
return followSubgraph(parent, currentLink);
|
||||
export class UpstreamNodeLocator {
|
||||
constructor(private isTheTargetNode: (node: LGraphNode) => boolean) {
|
||||
}
|
||||
else if (parent.is(GraphInput)) {
|
||||
console.warn("FollowGraphInput")
|
||||
return followGraphInput(parent, currentLink);
|
||||
|
||||
private followSubgraph(subgraph: Subgraph, link: LLink): [LGraph | null, LLink | null] {
|
||||
if (link.origin_id != subgraph.id)
|
||||
throw new Error("Invalid link and graph output!")
|
||||
|
||||
const innerGraphOutput = subgraph.getInnerGraphOutputByIndex(link.origin_slot)
|
||||
if (innerGraphOutput == null)
|
||||
throw new Error("No inner graph input!")
|
||||
|
||||
const nextLink = innerGraphOutput.getInputLink(0)
|
||||
return [innerGraphOutput.graph, nextLink];
|
||||
}
|
||||
else if ("getUpstreamLink" in parent) {
|
||||
return [parent.graph, (parent as ComfyGraphNode).getUpstreamLink()];
|
||||
|
||||
private followGraphInput(graphInput: GraphInput, link: LLink): [LGraph | null, LLink | null] {
|
||||
if (link.origin_id != graphInput.id)
|
||||
throw new Error("Invalid link and graph input!")
|
||||
|
||||
const outerSubgraph = graphInput.getParentSubgraph();
|
||||
if (outerSubgraph == null)
|
||||
throw new Error("No outer subgraph!")
|
||||
|
||||
const outerInputIndex = outerSubgraph.inputs.findIndex(i => i.name === graphInput.nameInGraph)
|
||||
if (outerInputIndex == null)
|
||||
throw new Error("No outer input slot!")
|
||||
|
||||
const nextLink = outerSubgraph.getInputLink(outerInputIndex)
|
||||
return [outerSubgraph.graph, nextLink];
|
||||
}
|
||||
|
||||
private getUpstreamLink(parent: LGraphNode, currentLink: LLink): [LGraph | null, LLink | null] {
|
||||
if (parent.is(Subgraph)) {
|
||||
console.debug("FollowSubgraph")
|
||||
return this.followSubgraph(parent, currentLink);
|
||||
}
|
||||
else if (parent.is(GraphInput)) {
|
||||
console.debug("FollowGraphInput")
|
||||
return this.followGraphInput(parent, currentLink);
|
||||
}
|
||||
else if ("getUpstreamLink" in parent) {
|
||||
return [parent.graph, (parent as ComfyGraphNode).getUpstreamLink()];
|
||||
}
|
||||
else if (parent.inputs.length === 1) {
|
||||
// Only one input, so assume we can follow it backwards.
|
||||
const link = parent.getInputLink(0);
|
||||
if (link) {
|
||||
return [parent.graph, link]
|
||||
}
|
||||
}
|
||||
console.warn("[graphToPrompt] Frontend node does not support getUpstreamLink", parent.type)
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
/*
|
||||
* Traverses the graph upstream from outputs towards inputs across
|
||||
* a sequence of nodes dependent on a condition.
|
||||
*
|
||||
* Returns the node and the output link attached to it that leads to the
|
||||
* starting node if any.
|
||||
*/
|
||||
locateUpstream(fromNode: LGraphNode, inputIndex: SlotIndex, tag: string | null): [LGraphNode | null, LLink | null] {
|
||||
let parent = fromNode.getInputNode(inputIndex);
|
||||
if (!parent)
|
||||
return [null, null];
|
||||
|
||||
const seen = {}
|
||||
let currentLink = fromNode.getInputLink(inputIndex);
|
||||
|
||||
const shouldFollowParent = (parent: LGraphNode) => {
|
||||
return isActiveNode(parent, tag) && !this.isTheTargetNode(parent);
|
||||
}
|
||||
|
||||
// If there are non-target nodes between us and another
|
||||
// backend node, we have to traverse them first. This
|
||||
// behavior is dependent on the type of node. Reroute nodes
|
||||
// will simply follow their single input, while branching
|
||||
// nodes have conditional logic that determines which link
|
||||
// to follow backwards.
|
||||
while (shouldFollowParent(parent)) {
|
||||
const [nextGraph, nextLink] = this.getUpstreamLink(parent, currentLink);
|
||||
|
||||
if (nextLink == null) {
|
||||
console.warn("[graphToPrompt] No upstream link found in frontend node", parent)
|
||||
break;
|
||||
}
|
||||
|
||||
if (nextLink && !seen[nextLink.id]) {
|
||||
seen[nextLink.id] = true
|
||||
const nextParent = nextGraph.getNodeById(nextLink.origin_id);
|
||||
if (!isActiveNode(parent, tag)) {
|
||||
parent = null;
|
||||
}
|
||||
else {
|
||||
console.debug("[graphToPrompt] Traverse upstream link", parent.id, nextParent?.id, (nextParent as any)?.isBackendNode)
|
||||
currentLink = nextLink;
|
||||
parent = nextParent;
|
||||
}
|
||||
} else {
|
||||
parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isActiveNode(parent, tag) || !this.isTheTargetNode(parent) || currentLink == null)
|
||||
return [null, null];
|
||||
|
||||
return [parent, currentLink]
|
||||
}
|
||||
console.warn("[graphToPrompt] Node does not support getUpstreamLink", parent.type)
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
export default class ComfyPromptSerializer {
|
||||
@@ -129,66 +203,21 @@ export default class ComfyPromptSerializer {
|
||||
serializeBackendLinks(node: ComfyBackendNode, tag: string | null): Record<string, SerializedPromptInput> {
|
||||
const inputs = {}
|
||||
|
||||
// Find a backend node upstream following before any number of frontend nodes
|
||||
const test = (node: LGraphNode) => (node as any).isBackendNode
|
||||
const nodeLocator = new UpstreamNodeLocator(test)
|
||||
|
||||
// Store links between backend-only and hybrid nodes
|
||||
for (let i = 0; i < node.inputs.length; i++) {
|
||||
let parent = node.getInputNode(i);
|
||||
if (parent) {
|
||||
const seen = {}
|
||||
let currentLink = node.getInputLink(i);
|
||||
|
||||
const isFrontendParent = (parent: LGraphNode) => {
|
||||
if (!parent || (parent as any).isBackendNode)
|
||||
return false;
|
||||
if (tag && !hasTag(parent, tag))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If there are frontend-only nodes between us and another
|
||||
// backend node, we have to traverse them first. This
|
||||
// behavior is dependent on the type of node. Reroute nodes
|
||||
// will simply follow their single input, while branching
|
||||
// nodes have conditional logic that determines which link
|
||||
// to follow backwards.
|
||||
while (isFrontendParent(parent)) {
|
||||
const [nextGraph, nextLink] = getUpstreamLink(parent, currentLink);
|
||||
|
||||
if (nextLink == null) {
|
||||
console.warn("[graphToPrompt] No upstream link found in frontend node", parent)
|
||||
break;
|
||||
}
|
||||
|
||||
if (nextLink && !seen[nextLink.id]) {
|
||||
seen[nextLink.id] = true
|
||||
const nextParent = nextGraph.getNodeById(nextLink.origin_id);
|
||||
if (nextParent && tag && !hasTag(nextParent, tag)) {
|
||||
console.debug("[graphToPrompt] Skipping tagged intermediate frontend node", tag, node.properties.tags)
|
||||
parent = null;
|
||||
}
|
||||
else {
|
||||
console.debug("[graphToPrompt] Traverse upstream link", parent.id, nextParent?.id, (nextParent as any)?.isBackendNode)
|
||||
currentLink = nextLink;
|
||||
parent = nextParent;
|
||||
}
|
||||
} else {
|
||||
parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLink && parent && (parent as any).isBackendNode) {
|
||||
if (tag && !hasTag(parent, tag))
|
||||
continue;
|
||||
|
||||
console.debug("[graphToPrompt] final link", parent.id, node.id)
|
||||
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".
|
||||
if (!(input.name in inputs))
|
||||
inputs[input.name] = [String(currentLink.origin_id), currentLink.origin_slot];
|
||||
}
|
||||
else {
|
||||
console.warn("[graphToPrompt] Didn't find upstream link!", currentLink, parent?.id)
|
||||
}
|
||||
const [backendNode, linkLeadingTo] = nodeLocator.locateUpstream(node, i, tag)
|
||||
if (backendNode) {
|
||||
console.debug("[graphToPrompt] final link", backendNode.id, "-->", node.id)
|
||||
const input = node.inputs[i]
|
||||
if (!(input.name in inputs))
|
||||
inputs[input.name] = [String(linkLeadingTo.origin_id), linkLeadingTo.origin_slot];
|
||||
}
|
||||
else {
|
||||
console.warn("[graphToPrompt] Didn't find upstream link!", node.id, node.type, node.title)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user