mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-16 17:20:51 +08:00
- Added a check so that reroute nodes won't connect widget inputs with the same type but different configs (e.g. seed and steps). - The widget property of widget inputs are distributed among every reroute node in the reroute chain, allowing primitives to connect.
255 lines
8.1 KiB
JavaScript
255 lines
8.1 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
// Node that allows you to redirect connections for cleaner graphs
|
|
|
|
app.registerExtension({
|
|
name: "Comfy.RerouteNode",
|
|
registerCustomNodes() {
|
|
class RerouteNode {
|
|
constructor() {
|
|
if (!this.properties) {
|
|
this.properties = {};
|
|
}
|
|
this.properties.showOutputText = RerouteNode.defaultVisibility;
|
|
this.properties.horizontal = false;
|
|
|
|
this.addInput("", "*");
|
|
this.addOutput(this.properties.showOutputText ? "*" : "", "*");
|
|
|
|
this.onConnectionsChange = function (type, index, connected, link_info) {
|
|
this.applyOrientation();
|
|
|
|
// Prevent multiple connections to different types when we have no input
|
|
if (connected && type === LiteGraph.OUTPUT) {
|
|
const callback = function (l) {
|
|
// Map the link to a type, including the widget config if it exists
|
|
const link = app.graph.links[l];
|
|
const input = app.graph.getNodeById(link.target_id).getInputInfo(link.target_slot);
|
|
return [link.type, input?.widget ? JSON.stringify(input.widget.config) : ""];
|
|
}
|
|
const types = new Set(
|
|
this.outputs[0].links
|
|
// Map each link to a type
|
|
.map((l) => callback(l))
|
|
// Ignore wildcard nodes as these will be updated to real types
|
|
.filter((t) => t[0] !== "*")
|
|
// Concatenate the type and widget config into a single string
|
|
.map((t) => t[0] + t[1])
|
|
);
|
|
if (types.size > 1) {
|
|
const linksToDisconnect = [];
|
|
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
|
|
const linkId = this.outputs[0].links[i];
|
|
const link = app.graph.links[linkId];
|
|
linksToDisconnect.push(link);
|
|
}
|
|
for (const link of linksToDisconnect) {
|
|
const node = app.graph.getNodeById(link.target_id);
|
|
node.disconnectInput(link.target_slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find root input
|
|
let currentNode = this;
|
|
let updateNodes = [];
|
|
let inputType = null;
|
|
let inputNode = null;
|
|
while (currentNode) {
|
|
updateNodes.unshift(currentNode);
|
|
const linkId = currentNode.inputs[0].link;
|
|
if (linkId !== null) {
|
|
const link = app.graph.links[linkId];
|
|
const node = app.graph.getNodeById(link.origin_id);
|
|
const type = node.constructor.type;
|
|
if (type === "Reroute") {
|
|
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 = [this];
|
|
let outputType = null;
|
|
let outputWidget = null;
|
|
while (nodes.length) {
|
|
currentNode = nodes.pop();
|
|
const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
|
|
if (outputs.length) {
|
|
for (const linkId of outputs) {
|
|
const link = app.graph.links[linkId];
|
|
|
|
// When disconnecting sometimes the link is still registered
|
|
if (!link) continue;
|
|
|
|
const node = app.graph.getNodeById(link.target_id);
|
|
const type = node.constructor.type;
|
|
|
|
if (type === "Reroute") {
|
|
// Follow reroute nodes
|
|
nodes.push(node);
|
|
updateNodes.push(node);
|
|
} else {
|
|
// We've found an output
|
|
const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
|
|
outputWidget = node.inputs[link.target_slot]?.widget ?? null;
|
|
if (inputType && nodeOutType !== inputType && inputType !== "*") {
|
|
// The output doesnt match our input and is not a wildcard so disconnect it
|
|
node.disconnectInput(link.target_slot);
|
|
} else {
|
|
outputType = nodeOutType;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// No more outputs for this path
|
|
}
|
|
}
|
|
|
|
const displayType = (inputType === "*" && outputType) || inputType || outputType || "*";
|
|
const color = LGraphCanvas.link_type_colors[displayType];
|
|
|
|
// 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 : "";
|
|
if (outputWidget) {
|
|
// Inherit the widget from the connected input
|
|
node.inputs[0].widget = outputWidget;
|
|
} else {
|
|
delete node.inputs[0].widget;
|
|
}
|
|
node.size = node.computeSize();
|
|
node.applyOrientation();
|
|
|
|
for (const l of node.outputs[0].links || []) {
|
|
const link = app.graph.links[l];
|
|
if (link) {
|
|
link.color = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (inputNode) {
|
|
const link = app.graph.links[inputNode.inputs[0].link];
|
|
if (link) {
|
|
link.color = color;
|
|
}
|
|
}
|
|
};
|
|
|
|
this.clone = function () {
|
|
const cloned = RerouteNode.prototype.clone.apply(this);
|
|
cloned.removeOutput(0);
|
|
cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
|
|
cloned.size = cloned.computeSize();
|
|
return cloned;
|
|
};
|
|
|
|
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
|
this.isVirtualNode = true;
|
|
}
|
|
|
|
getExtraMenuOptions(_, options) {
|
|
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.size = this.computeSize();
|
|
this.applyOrientation();
|
|
app.graph.setDirtyCanvas(true, true);
|
|
},
|
|
},
|
|
{
|
|
content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default",
|
|
callback: () => {
|
|
RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility);
|
|
},
|
|
},
|
|
{
|
|
// naming is inverted with respect to LiteGraphNode.horizontal
|
|
// LiteGraphNode.horizontal == true means that
|
|
// each slot in the inputs and outputs are layed out horizontally,
|
|
// which is the opposite of the visual orientation of the inputs and outputs as a node
|
|
content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"),
|
|
callback: () => {
|
|
this.properties.horizontal = !this.properties.horizontal;
|
|
this.applyOrientation();
|
|
},
|
|
}
|
|
);
|
|
}
|
|
applyOrientation() {
|
|
this.horizontal = this.properties.horizontal;
|
|
if (this.horizontal) {
|
|
// we correct the input position, because LiteGraphNode.horizontal
|
|
// doesn't account for title presence
|
|
// which reroute nodes don't have
|
|
this.inputs[0].pos = [this.size[0] / 2, 0];
|
|
} else {
|
|
delete this.inputs[0].pos;
|
|
}
|
|
app.graph.setDirtyCanvas(true, true);
|
|
}
|
|
|
|
computeSize() {
|
|
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) {
|
|
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";
|
|
},
|
|
});
|