mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 14:54:37 +00:00
This PR implements front end logic to handle MatchType inputs and outputs. See comfyanonymous/ComfyUI#10644 This allows for the implementation of nodes such as a "switch node" where input types change based on the connections made.  As part of this implementation, significant cleanup is being performed in the reroute code. Extra testing will be required to make sure these changes don't introduce regressions. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6582-Add-front-end-support-for-type-matching-2a16d73d36508189b042cd23f82a332e) by [Unito](https://www.unito.io)
296 lines
9.5 KiB
TypeScript
296 lines
9.5 KiB
TypeScript
import type { IContextMenuValue } from '@/lib/litegraph/src/litegraph'
|
|
import {
|
|
LGraphCanvas,
|
|
LGraphNode,
|
|
LiteGraph
|
|
} from '@/lib/litegraph/src/litegraph'
|
|
import type { ISlotType } from '@/lib/litegraph/src/interfaces'
|
|
|
|
import { app } from '../../scripts/app'
|
|
import { getWidgetConfig, mergeIfValid, setWidgetConfig } from './widgetInputs'
|
|
|
|
// Node that allows you to redirect connections for cleaner graphs
|
|
|
|
app.registerExtension({
|
|
name: 'Comfy.RerouteNode',
|
|
registerCustomNodes(app) {
|
|
interface RerouteNode extends LGraphNode {
|
|
__outputType?: string | number
|
|
}
|
|
|
|
class RerouteNode extends LGraphNode {
|
|
static override category: string | undefined
|
|
static defaultVisibility = false
|
|
|
|
constructor(title?: string) {
|
|
super(title ?? '')
|
|
if (!this.properties) {
|
|
this.properties = {}
|
|
}
|
|
this.properties.showOutputText = RerouteNode.defaultVisibility
|
|
this.properties.horizontal = false
|
|
|
|
this.addInput('', '*')
|
|
this.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
|
|
|
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
|
this.isVirtualNode = true
|
|
}
|
|
override onAfterGraphConfigured() {
|
|
requestAnimationFrame(() => {
|
|
this.onConnectionsChange(LiteGraph.INPUT, undefined, true)
|
|
})
|
|
}
|
|
override clone(): LGraphNode | null {
|
|
const cloned = super.clone()
|
|
if (!cloned) return cloned
|
|
cloned.removeOutput(0)
|
|
cloned.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
|
cloned.setSize(cloned.computeSize())
|
|
return cloned
|
|
}
|
|
override onConnectionsChange(
|
|
type: ISlotType,
|
|
_index: number | undefined,
|
|
connected: boolean
|
|
) {
|
|
const { graph } = this
|
|
if (!graph) return
|
|
if (app.configuringGraph) return
|
|
|
|
// Prevent multiple connections to different types when we have no input
|
|
if (connected && type === LiteGraph.OUTPUT) {
|
|
// Ignore wildcard nodes as these will be updated to real types
|
|
const types = new Set(
|
|
this.outputs[0].links
|
|
?.map((l) => graph.links[l]?.type)
|
|
?.filter((t) => t && t !== '*') ?? []
|
|
)
|
|
if (types.size > 1) {
|
|
const linksToDisconnect = []
|
|
for (const linkId of this.outputs[0].links ?? []) {
|
|
const link = graph.links[linkId]
|
|
linksToDisconnect.push(link)
|
|
}
|
|
linksToDisconnect.pop()
|
|
for (const link of linksToDisconnect) {
|
|
const node = graph.getNodeById(link.target_id)
|
|
node?.disconnectInput(link.target_slot)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find root input
|
|
let currentNode: RerouteNode | null = this
|
|
let updateNodes: RerouteNode[] = []
|
|
let inputType = null
|
|
let inputNode = null
|
|
while (currentNode) {
|
|
updateNodes.unshift(currentNode)
|
|
const linkId = currentNode.inputs[0].link
|
|
if (linkId !== null) {
|
|
const link = graph.links[linkId]
|
|
if (!link) return
|
|
const node = graph.getNodeById(link.origin_id)
|
|
if (!node) return
|
|
if (node instanceof RerouteNode) {
|
|
if (node === this) {
|
|
// We've found a circle
|
|
currentNode.disconnectInput(link.target_slot)
|
|
currentNode = null
|
|
} else {
|
|
// Move the previous node
|
|
currentNode = node
|
|
}
|
|
} else {
|
|
// We've found the end
|
|
inputNode = currentNode
|
|
inputType = node.outputs[link.origin_slot]?.type ?? null
|
|
break
|
|
}
|
|
} else {
|
|
// This path has no input node
|
|
currentNode = null
|
|
break
|
|
}
|
|
}
|
|
|
|
// Find all outputs
|
|
const nodes: RerouteNode[] = [this]
|
|
let outputType = null
|
|
while (nodes.length) {
|
|
currentNode = nodes.pop()!
|
|
const outputs = currentNode.outputs?.[0]?.links ?? []
|
|
for (const linkId of outputs) {
|
|
const link = graph.links[linkId]
|
|
|
|
// When disconnecting sometimes the link is still registered
|
|
if (!link) continue
|
|
|
|
const node = graph.getNodeById(link.target_id)
|
|
if (!node) continue
|
|
if (node instanceof RerouteNode) {
|
|
// Follow reroute nodes
|
|
nodes.push(node)
|
|
updateNodes.push(node)
|
|
} else {
|
|
// We've found an output
|
|
const nodeInput = node.inputs[link.target_slot]
|
|
const nodeOutType = nodeInput.type
|
|
const keep =
|
|
!inputType ||
|
|
!nodeOutType ||
|
|
LiteGraph.isValidConnection(inputType, nodeOutType)
|
|
if (!keep) {
|
|
// The output doesnt match our input so disconnect it
|
|
node.disconnectInput(link.target_slot)
|
|
continue
|
|
}
|
|
node.onConnectionsChange?.(
|
|
LiteGraph.INPUT,
|
|
link.target_slot,
|
|
keep,
|
|
link,
|
|
nodeInput
|
|
)
|
|
outputType = node.inputs[link.target_slot].type
|
|
}
|
|
}
|
|
}
|
|
|
|
const displayType = inputType || outputType || '*'
|
|
const color = LGraphCanvas.link_type_colors[displayType]
|
|
|
|
let widgetConfig
|
|
let widgetType
|
|
// Update the types of each node
|
|
for (const node of updateNodes) {
|
|
// If we dont have an input type we are always wildcard but we'll show the output type
|
|
// This lets you change the output link to a different type and all nodes will update
|
|
node.outputs[0].type = inputType || '*'
|
|
node.__outputType = displayType
|
|
node.outputs[0].name = node.properties.showOutputText
|
|
? `${displayType}`
|
|
: ''
|
|
node.setSize(node.computeSize())
|
|
|
|
for (const l of node.outputs[0].links || []) {
|
|
const link = graph.links[l]
|
|
if (!link) continue
|
|
link.color = color
|
|
|
|
if (app.configuringGraph) continue
|
|
const targetNode = graph.getNodeById(link.target_id)
|
|
if (!targetNode) continue
|
|
const targetInput = targetNode.inputs?.[link.target_slot]
|
|
if (targetInput?.widget) {
|
|
const config = getWidgetConfig(targetInput)
|
|
if (!widgetConfig) {
|
|
widgetConfig = config[1] ?? {}
|
|
widgetType = config[0]
|
|
}
|
|
|
|
const merged = mergeIfValid(targetInput, [
|
|
config[0],
|
|
widgetConfig
|
|
])
|
|
if (merged.customConfig) {
|
|
widgetConfig = merged.customConfig
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const node of updateNodes) {
|
|
if (widgetConfig && outputType) {
|
|
node.inputs[0].widget = { name: 'value' }
|
|
setWidgetConfig(node.inputs[0], [
|
|
widgetType ?? `${displayType}`,
|
|
widgetConfig
|
|
])
|
|
} else {
|
|
setWidgetConfig(node.inputs[0], undefined)
|
|
}
|
|
}
|
|
|
|
if (inputNode?.inputs?.[0]?.link) {
|
|
const link = graph.links[inputNode.inputs[0].link]
|
|
if (link) {
|
|
link.color = color
|
|
}
|
|
}
|
|
}
|
|
|
|
override getExtraMenuOptions(
|
|
_: unknown,
|
|
options: (IContextMenuValue | null)[]
|
|
): IContextMenuValue[] {
|
|
options.unshift(
|
|
{
|
|
content:
|
|
(this.properties.showOutputText ? 'Hide' : 'Show') + ' Type',
|
|
callback: () => {
|
|
this.properties.showOutputText = !this.properties.showOutputText
|
|
if (this.properties.showOutputText) {
|
|
this.outputs[0].name = `${this.__outputType || this.outputs[0].type}`
|
|
} else {
|
|
this.outputs[0].name = ''
|
|
}
|
|
this.setSize(this.computeSize())
|
|
app.canvas.setDirty(true, true)
|
|
}
|
|
},
|
|
{
|
|
content:
|
|
(RerouteNode.defaultVisibility ? 'Hide' : 'Show') +
|
|
' Type By Default',
|
|
callback: () => {
|
|
RerouteNode.setDefaultTextVisibility(
|
|
!RerouteNode.defaultVisibility
|
|
)
|
|
}
|
|
}
|
|
)
|
|
return []
|
|
}
|
|
override computeSize(): [number, number] {
|
|
return [
|
|
this.properties.showOutputText && this.outputs && this.outputs.length
|
|
? Math.max(
|
|
75,
|
|
LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 +
|
|
40
|
|
)
|
|
: 75,
|
|
26
|
|
]
|
|
}
|
|
|
|
static setDefaultTextVisibility(visible: boolean) {
|
|
RerouteNode.defaultVisibility = visible
|
|
if (visible) {
|
|
localStorage['Comfy.RerouteNode.DefaultVisibility'] = 'true'
|
|
} else {
|
|
delete localStorage['Comfy.RerouteNode.DefaultVisibility']
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load default visibility
|
|
RerouteNode.setDefaultTextVisibility(
|
|
!!localStorage['Comfy.RerouteNode.DefaultVisibility']
|
|
)
|
|
|
|
LiteGraph.registerNodeType(
|
|
'Reroute',
|
|
Object.assign(RerouteNode, {
|
|
title_mode: LiteGraph.NO_TITLE,
|
|
title: 'Reroute',
|
|
collapsable: false
|
|
})
|
|
)
|
|
|
|
RerouteNode.category = 'utils'
|
|
}
|
|
})
|