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' } })