Spaces:
Configuration error
Configuration error
File size: 10,454 Bytes
d051564 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 |
import type { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js";
import type {
Vector2,
LLink,
INodeInputSlot,
INodeOutputSlot,
LGraphNode as TLGraphNode,
IWidget,
} from "typings/litegraph.js";
import { app } from "scripts/app.js";
import { RgthreeBaseVirtualNode } from "./base_node.js";
import { rgthree } from "./rgthree.js";
import {
PassThroughFollowing,
addConnectionLayoutSupport,
addMenuItem,
getConnectedInputNodes,
getConnectedInputNodesAndFilterPassThroughs,
getConnectedOutputNodes,
getConnectedOutputNodesAndFilterPassThroughs,
} from "./utils.js";
/**
* A Virtual Node that allows any node's output to connect to it.
*/
export class BaseAnyInputConnectedNode extends RgthreeBaseVirtualNode {
override isVirtualNode = true;
/**
* Whether inputs show the immediate nodes, or follow and show connected nodes through
* passthrough nodes.
*/
readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.NONE;
debouncerTempWidth: number = 0;
schedulePromise: Promise<void> | null = null;
constructor(title = BaseAnyInputConnectedNode.title) {
super(title);
}
override onConstructed() {
this.addInput("", "*");
return super.onConstructed();
}
/** Schedules a promise to run a stabilization. */
scheduleStabilizeWidgets(ms = 100) {
if (!this.schedulePromise) {
this.schedulePromise = new Promise((resolve) => {
setTimeout(() => {
this.schedulePromise = null;
this.doStablization();
resolve();
}, ms);
});
}
return this.schedulePromise;
}
override clone() {
const cloned = super.clone();
// Copying to clipboard (and also, creating node templates) work by cloning nodes and, for some
// reason, it manually manipulates the cloned data. So, we want to keep the present input slots
// so if it's pasted/templatized the data is correct. Otherwise, clear the inputs and so the new
// node is ready to go, fresh.
if (!rgthree.canvasCurrentlyCopyingToClipboardWithMultipleNodes) {
while (cloned.inputs.length > 1) {
cloned.removeInput(cloned.inputs.length - 1);
}
if (cloned.inputs[0]) {
cloned.inputs[0].label = "";
}
}
return cloned;
}
/**
* Ensures we have at least one empty input at the end.
*/
stabilizeInputsOutputs() {
const hasEmptyInput = !this.inputs[this.inputs.length - 1]?.link;
if (!hasEmptyInput) {
this.addInput("", "*");
}
for (let index = this.inputs.length - 2; index >= 0; index--) {
const input = this.inputs[index]!;
if (!input.link) {
this.removeInput(index);
} else {
const node = getConnectedInputNodesAndFilterPassThroughs(
this,
this,
index,
this.inputsPassThroughFollowing,
)[0];
input.name = node?.title || "";
}
}
}
/**
* Stabilizes the node's inputs and widgets.
*/
private doStablization() {
if (!this.graph) {
return;
}
// When we add/remove widgets, litegraph is going to mess up the size, so we
// store it so we can retrieve it in computeSize. Hacky..
(this as any)._tempWidth = this.size[0];
const linkedNodes = getConnectedInputNodesAndFilterPassThroughs(this);
this.stabilizeInputsOutputs();
this.handleLinkedNodesStabilization(linkedNodes);
app.graph.setDirtyCanvas(true, true);
// Schedule another stabilization in the future.
this.scheduleStabilizeWidgets(500);
}
handleLinkedNodesStabilization(linkedNodes: TLGraphNode[]) {
linkedNodes; // No-op, but makes overridding in VSCode cleaner.
throw new Error("handleLinkedNodesStabilization should be overridden.");
}
onConnectionsChainChange() {
this.scheduleStabilizeWidgets();
}
override onConnectionsChange(
type: number,
index: number,
connected: boolean,
linkInfo: LLink,
ioSlot: INodeOutputSlot | INodeInputSlot,
) {
super.onConnectionsChange &&
super.onConnectionsChange(type, index, connected, linkInfo, ioSlot);
if (!linkInfo) return;
// Follow outputs to see if we need to trigger an onConnectionChange.
const connectedNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
for (const node of connectedNodes) {
if ((node as BaseAnyInputConnectedNode).onConnectionsChainChange) {
(node as BaseAnyInputConnectedNode).onConnectionsChainChange();
}
}
this.scheduleStabilizeWidgets();
}
override removeInput(slot: number) {
(this as any)._tempWidth = this.size[0];
return super.removeInput(slot);
}
override addInput(name: string, type: string | -1, extra_info?: Partial<INodeInputSlot>) {
(this as any)._tempWidth = this.size[0];
return super.addInput(name, type, extra_info);
}
override addWidget<T extends IWidget>(
type: T["type"],
name: string,
value: T["value"],
callback?: T["callback"] | string,
options?: T["options"],
) {
(this as any)._tempWidth = this.size[0];
return super.addWidget(type, name, value, callback, options);
}
/**
* Guess this doesn't exist in Litegraph...
*/
override removeWidget(widgetOrSlot?: IWidget | number) {
(this as any)._tempWidth = this.size[0];
super.removeWidget(widgetOrSlot);
}
override computeSize(out: Vector2) {
let size = super.computeSize(out);
if ((this as any)._tempWidth) {
size[0] = (this as any)._tempWidth;
// We sometimes get repeated calls to compute size, so debounce before clearing.
this.debouncerTempWidth && clearTimeout(this.debouncerTempWidth);
this.debouncerTempWidth = setTimeout(() => {
(this as any)._tempWidth = null;
}, 32);
}
// If we're collapsed, then subtract the total calculated height of the other input slots.
if (this.properties["collapse_connections"]) {
const rows = Math.max(this.inputs?.length || 0, this.outputs?.length || 0, 1) - 1;
size[1] = size[1] - rows * LiteGraph.NODE_SLOT_HEIGHT;
}
setTimeout(() => {
app.graph.setDirtyCanvas(true, true);
}, 16);
return size;
}
/**
* When we connect our output, check our inputs and make sure we're not trying to connect a loop.
*/
override onConnectOutput(
outputIndex: number,
inputType: string | -1,
inputSlot: INodeInputSlot,
inputNode: TLGraphNode,
inputIndex: number,
): boolean {
let canConnect = true;
if (super.onConnectOutput) {
canConnect = super.onConnectOutput(outputIndex, inputType, inputSlot, inputNode, inputIndex);
}
if (canConnect) {
const nodes = getConnectedInputNodes(this); // We want passthrough nodes, since they will loop.
if (nodes.includes(inputNode)) {
alert(
`Whoa, whoa, whoa. You've just tried to create a connection that loops back on itself, ` +
`a situation that could create a time paradox, the results of which could cause a ` +
`chain reaction that would unravel the very fabric of the space time continuum, ` +
`and destroy the entire universe!`,
);
canConnect = false;
}
}
return canConnect;
}
override onConnectInput(
inputIndex: number,
outputType: string | -1,
outputSlot: INodeOutputSlot,
outputNode: TLGraphNode,
outputIndex: number,
): boolean {
let canConnect = true;
if (super.onConnectInput) {
canConnect = super.onConnectInput(
inputIndex,
outputType,
outputSlot,
outputNode,
outputIndex,
);
}
if (canConnect) {
const nodes = getConnectedOutputNodes(this); // We want passthrough nodes, since they will loop.
if (nodes.includes(outputNode)) {
alert(
`Whoa, whoa, whoa. You've just tried to create a connection that loops back on itself, ` +
`a situation that could create a time paradox, the results of which could cause a ` +
`chain reaction that would unravel the very fabric of the space time continuum, ` +
`and destroy the entire universe!`,
);
canConnect = false;
}
}
return canConnect;
}
/**
* If something is dropped on us, just add it to the bottom. onConnectInput should already cancel
* if it's disallowed.
*/
override connectByTypeOutput<T = any>(
slot: string | number,
sourceNode: TLGraphNode,
sourceSlotType: string,
optsIn: string,
): T | null {
const lastInput = this.inputs[this.inputs.length - 1];
if (!lastInput?.link && lastInput?.type === "*") {
var sourceSlot = sourceNode.findOutputSlotByType(sourceSlotType, false, true);
return sourceNode.connect(sourceSlot, this, slot);
}
return super.connectByTypeOutput(slot, sourceNode, sourceSlotType, optsIn);
}
static override setUp() {
super.setUp();
addConnectionLayoutSupport(this, app, [
["Left", "Right"],
["Right", "Left"],
]);
addMenuItem(this, app, {
name: (node) =>
`${node.properties?.["collapse_connections"] ? "Show" : "Collapse"} Connections`,
property: "collapse_connections",
prepareValue: (_value, node) => !node.properties?.["collapse_connections"],
callback: (_node) => {
app.graph.setDirtyCanvas(true, true);
},
});
}
}
// Ok, hack time! LGraphNode's connectByType is powerful, but for our nodes, that have multiple "*"
// input types, it seems it just takes the first one, and disconnects it. I'd rather we don't do
// that and instead take the next free one. If that doesn't work, then we'll give it to the old
// method.
const oldLGraphNodeConnectByType = LGraphNode.prototype.connectByType;
LGraphNode.prototype.connectByType = function connectByType<T = any>(
slot: string | number,
sourceNode: TLGraphNode,
sourceSlotType: string,
optsIn: string,
): T | null {
// If we're droppiong on a node, and the last input is free and an "*" type, then connect there
// first...
if (sourceNode.inputs) {
for (const [index, input] of sourceNode.inputs.entries()) {
if (!input.link && input.type === "*") {
this.connect(slot, sourceNode, index);
return null;
}
}
}
return ((oldLGraphNodeConnectByType &&
oldLGraphNodeConnectByType.call(this, slot, sourceNode, sourceSlotType, optsIn)) ||
null) as T;
};
|