rerouteNode.js 8.6 KB
Newer Older
1
import { app } from "../../scripts/app.js";
2
import { mergeIfValid, getWidgetConfig, setWidgetConfig } from "./widgetInputs.js";
3
4
5
6
7

// Node that allows you to redirect connections for cleaner graphs

app.registerExtension({
	name: "Comfy.RerouteNode",
8
	registerCustomNodes(app) {
9
10
11
12
13
14
		class RerouteNode {
			constructor() {
				if (!this.properties) {
					this.properties = {};
				}
				this.properties.showOutputText = RerouteNode.defaultVisibility;
15
				this.properties.horizontal = false;
16
17
18

				this.addInput("", "*");
				this.addOutput(this.properties.showOutputText ? "*" : "", "*");
19

20
21
22
23
24
25
				this.onAfterGraphConfigured = function () {
					requestAnimationFrame(() => {
						this.onConnectionsChange(LiteGraph.INPUT, null, true, null);
					});
				};

26
				this.onConnectionsChange = function (type, index, connected, link_info) {
27
28
					this.applyOrientation();

29
30
31
32
33
					// 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) => app.graph.links[l].type).filter((t) => t !== "*"));
						if (types.size > 1) {
34
							const linksToDisconnect = [];
35
36
37
							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];
38
39
40
								linksToDisconnect.push(link);
							}
							for (const link of linksToDisconnect) {
41
42
43
44
45
46
47
48
49
50
								const node = app.graph.getNodeById(link.target_id);
								node.disconnectInput(link.target_slot);
							}
						}
					}

					// Find root input
					let currentNode = this;
					let updateNodes = [];
					let inputType = null;
pythongosssss's avatar
pythongosssss committed
51
					let inputNode = null;
52
53
54
55
56
57
58
59
					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") {
60
61
62
63
								if (node === this) {
									// We've found a circle
									currentNode.disconnectInput(link.target_slot);
									currentNode = null;
64
								} else {
65
66
									// Move the previous node
									currentNode = node;
67
								}
68
69
							} else {
								// We've found the end
pythongosssss's avatar
pythongosssss committed
70
								inputNode = currentNode;
71
								inputType = node.outputs[link.origin_slot]?.type ?? null;
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
								break;
							}
						} else {
							// This path has no input node
							currentNode = null;
							break;
						}
					}

					// Find all outputs
					const nodes = [this];
					let outputType = 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];
pythongosssss's avatar
pythongosssss committed
90

91
92
93
94
95
96
97
98
99
100
101
102
								// 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
103
104
105
106
107
									const nodeOutType =
										node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type
											? node.inputs[link.target_slot].type
											: null;
									if (inputType && inputType !== "*" && nodeOutType !== inputType) {
108
109
110
111
112
113
114
115
116
117
118
119
										// The output doesnt match our input so disconnect it
										node.disconnectInput(link.target_slot);
									} else {
										outputType = nodeOutType;
									}
								}
							}
						} else {
							// No more outputs for this path
						}
					}

pythongosssss's avatar
pythongosssss committed
120
					const displayType = inputType || outputType || "*";
pythongosssss's avatar
pythongosssss committed
121
					const color = LGraphCanvas.link_type_colors[displayType];
pythongosssss's avatar
pythongosssss committed
122

123
124
125
					let widgetConfig;
					let targetWidget;
					let widgetType;
126
127
128
129
130
					// 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 || "*";
pythongosssss's avatar
pythongosssss committed
131
132
						node.__outputType = displayType;
						node.outputs[0].name = node.properties.showOutputText ? displayType : "";
133
						node.size = node.computeSize();
134
						node.applyOrientation();
pythongosssss's avatar
pythongosssss committed
135
136

						for (const l of node.outputs[0].links || []) {
pythongosssss's avatar
pythongosssss committed
137
138
139
							const link = app.graph.links[l];
							if (link) {
								link.color = color;
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158

								if (app.configuringGraph) continue;
								const targetNode = app.graph.getNodeById(link.target_id);
								const targetInput = targetNode.inputs?.[link.target_slot];
								if (targetInput?.widget) {
									const config = getWidgetConfig(targetInput);
									if (!widgetConfig) {
										widgetConfig = config[1] ?? {};
										widgetType = config[0];
									}
									if (!targetWidget) {
										targetWidget = targetNode.widgets?.find((w) => w.name === targetInput.widget.name);
									}

									const merged = mergeIfValid(targetInput, [config[0], widgetConfig]);
									if (merged.customConfig) {
										widgetConfig = merged.customConfig;
									}
								}
pythongosssss's avatar
pythongosssss committed
159
							}
pythongosssss's avatar
pythongosssss committed
160
						}
161
					}
pythongosssss's avatar
pythongosssss committed
162

163
164
165
166
167
168
169
170
171
					for (const node of updateNodes) {
						if (widgetConfig && outputType) {
							node.inputs[0].widget = { name: "value" };
							setWidgetConfig(node.inputs[0], [widgetType ?? displayType, widgetConfig], targetWidget);
						} else {
							setWidgetConfig(node.inputs[0], null);
						}
					}

pythongosssss's avatar
pythongosssss committed
172
					if (inputNode) {
pythongosssss's avatar
pythongosssss committed
173
174
175
176
						const link = app.graph.links[inputNode.inputs[0].link];
						if (link) {
							link.color = color;
						}
pythongosssss's avatar
pythongosssss committed
177
					}
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
				};

				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) {
199
								this.outputs[0].name = this.__outputType || this.outputs[0].type;
200
201
202
203
							} else {
								this.outputs[0].name = "";
							}
							this.size = this.computeSize();
204
							this.applyOrientation();
205
							app.graph.setDirtyCanvas(true, true);
206
207
208
209
210
211
212
						},
					},
					{
						content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default",
						callback: () => {
							RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility);
						},
213
214
215
					},
					{
						// naming is inverted with respect to LiteGraphNode.horizontal
216
217
						// LiteGraphNode.horizontal == true means that
						// each slot in the inputs and outputs are layed out horizontally,
218
219
220
221
222
223
						// 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();
						},
224
225
226
					}
				);
			}
227
228
229
			applyOrientation() {
				this.horizontal = this.properties.horizontal;
				if (this.horizontal) {
230
					// we correct the input position, because LiteGraphNode.horizontal
231
232
233
234
235
236
237
238
					// 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);
			}
239
240
241
242

			computeSize() {
				return [
					this.properties.showOutputText && this.outputs && this.outputs.length
pythongosssss's avatar
pythongosssss committed
243
244
						? Math.max(75, LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40)
						: 75,
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
					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",
pythongosssss's avatar
pythongosssss committed
267
				collapsable: false,
268
269
270
271
272
273
			})
		);

		RerouteNode.category = "utils";
	},
});