Spaces:
Configuration error
Configuration error
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; | |
}; | |