multimodalart's picture
Squashing commit
4450790 verified
import type {
INodeInputSlot,
INodeOutputSlot,
LGraphCanvas as TLGraphCanvas,
LGraphNode as TLGraphNode,
LLink,
} from "typings/litegraph.js";
import type { ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
import { app } from "scripts/app.js";
import {
IoDirection,
addConnectionLayoutSupport,
addMenuItem,
matchLocalSlotsToServer,
replaceNode,
} from "./utils.js";
import { RgthreeBaseServerNode } from "./base_node.js";
import { SERVICE as KEY_EVENT_SERVICE } from "./services/key_events_services.js";
import { RgthreeBaseServerNodeConstructor } from "typings/rgthree.js";
import { debounce, wait } from "rgthree/common/shared_utils.js";
import { removeUnusedInputsFromEnd } from "./utils_inputs_outputs.js";
import { NodeTypesString } from "./constants.js";
/**
* Takes a non-context node and determins for its input or output slot, if there is a valid
* connection for an opposite context output or input slot.
*/
function findMatchingIndexByTypeOrName(
otherNode: TLGraphNode,
otherSlot: INodeInputSlot | INodeOutputSlot,
ctxSlots: INodeInputSlot[] | INodeOutputSlot[],
) {
const otherNodeType = (otherNode.type || "").toUpperCase();
const otherNodeName = (otherNode.title || "").toUpperCase();
let otherSlotType = otherSlot.type as string;
if (Array.isArray(otherSlotType) || otherSlotType.includes(",")) {
otherSlotType = "COMBO";
}
const otherSlotName = otherSlot.name.toUpperCase().replace("OPT_", "").replace("_NAME", "");
let ctxSlotIndex = -1;
if (["CONDITIONING", "INT", "STRING", "FLOAT", "COMBO"].includes(otherSlotType)) {
ctxSlotIndex = ctxSlots.findIndex((ctxSlot) => {
const ctxSlotName = ctxSlot.name.toUpperCase().replace("OPT_", "").replace("_NAME", "");
let ctxSlotType = ctxSlot.type as string;
if (Array.isArray(ctxSlotType) || ctxSlotType.includes(",")) {
ctxSlotType = "COMBO";
}
if (ctxSlotType !== otherSlotType) {
return false;
}
// Straightforward matches.
if (
ctxSlotName === otherSlotName ||
(ctxSlotName === "SEED" && otherSlotName.includes("SEED")) ||
(ctxSlotName === "STEP_REFINER" && otherSlotName.includes("AT_STEP")) ||
(ctxSlotName === "STEP_REFINER" && otherSlotName.includes("REFINER_STEP"))
) {
return true;
}
// If postive other node, try to match conditining and text.
if (
(otherNodeType.includes("POSITIVE") || otherNodeName.includes("POSITIVE")) &&
((ctxSlotName === "POSITIVE" && otherSlotType === "CONDITIONING") ||
(ctxSlotName === "TEXT_POS_G" && otherSlotName.includes("TEXT_G")) ||
(ctxSlotName === "TEXT_POS_L" && otherSlotName.includes("TEXT_L")))
) {
return true;
}
if (
(otherNodeType.includes("NEGATIVE") || otherNodeName.includes("NEGATIVE")) &&
((ctxSlotName === "NEGATIVE" && otherSlotType === "CONDITIONING") ||
(ctxSlotName === "TEXT_NEG_G" && otherSlotName.includes("TEXT_G")) ||
(ctxSlotName === "TEXT_NEG_L" && otherSlotName.includes("TEXT_L")))
) {
return true;
}
return false;
});
} else {
ctxSlotIndex = ctxSlots.map((s) => s.type).indexOf(otherSlotType);
}
return ctxSlotIndex;
}
/**
* A Base Context node for other context based nodes to extend.
*/
export class BaseContextNode extends RgthreeBaseServerNode {
constructor(title: string) {
super(title);
}
// LiteGraph adds more spacing than we want when calculating a nodes' `_collapsed_width`, so we'll
// override it with a setter and re-set it measured exactly as we want.
___collapsed_width: number = 0;
//@ts-ignore - TS Doesn't like us overriding a property with accessors but, too bad.
override get _collapsed_width() {
return this.___collapsed_width;
}
override set _collapsed_width(width: number) {
const canvas = app.canvas as TLGraphCanvas;
const ctx = canvas.canvas.getContext("2d")!;
const oldFont = ctx.font;
ctx.font = canvas.title_text_font;
let title = this.title.trim();
this.___collapsed_width = 30 + (title ? 10 + ctx.measureText(title).width : 0);
ctx.font = oldFont;
}
override connectByType<T = any>(
slot: string | number,
sourceNode: TLGraphNode,
sourceSlotType: string,
optsIn: string,
): T | null {
let canConnect =
super.connectByType &&
super.connectByType.call(this, slot, sourceNode, sourceSlotType, optsIn);
if (!super.connectByType) {
canConnect = LGraphNode.prototype.connectByType.call(
this,
slot,
sourceNode,
sourceSlotType,
optsIn,
);
}
if (!canConnect && slot === 0) {
const ctrlKey = KEY_EVENT_SERVICE.ctrlKey;
// Okay, we've dragged a context and it can't connect.. let's connect all the other nodes.
// Unfortunately, we don't know which are null now, so we'll just connect any that are
// not already connected.
for (const [index, input] of (sourceNode.inputs || []).entries()) {
if (input.link && !ctrlKey) {
continue;
}
const thisOutputSlot = findMatchingIndexByTypeOrName(sourceNode, input, this.outputs);
if (thisOutputSlot > -1) {
this.connect(thisOutputSlot, sourceNode, index);
}
}
}
return null;
}
override connectByTypeOutput<T = any>(
slot: string | number,
sourceNode: TLGraphNode,
sourceSlotType: string,
optsIn: string,
): T | null {
let canConnect =
super.connectByTypeOutput &&
super.connectByTypeOutput.call(this, slot, sourceNode, sourceSlotType, optsIn);
if (!super.connectByType) {
canConnect = LGraphNode.prototype.connectByTypeOutput.call(
this,
slot,
sourceNode,
sourceSlotType,
optsIn,
);
}
if (!canConnect && slot === 0) {
const ctrlKey = KEY_EVENT_SERVICE.ctrlKey;
// Okay, we've dragged a context and it can't connect.. let's connect all the other nodes.
// Unfortunately, we don't know which are null now, so we'll just connect any that are
// not already connected.
for (const [index, output] of (sourceNode.outputs || []).entries()) {
if (output.links?.length && !ctrlKey) {
continue;
}
const thisInputSlot = findMatchingIndexByTypeOrName(sourceNode, output, this.inputs);
if (thisInputSlot > -1) {
sourceNode.connect(index, this, thisInputSlot);
}
}
}
return null;
}
static override setUp(
comfyClass: ComfyNodeConstructor,
nodeData: ComfyObjectInfo,
ctxClass: RgthreeBaseServerNodeConstructor,
) {
RgthreeBaseServerNode.registerForOverride(comfyClass, nodeData, ctxClass);
// [🤮] ComfyUI only adds "required" inputs to the outputs list when dragging an output to
// empty space, but since RGTHREE_CONTEXT is optional, it doesn't get added to the menu because
// ...of course. So, we'll manually add it. Of course, we also have to do this in a timeout
// because ComfyUI clears out `LiteGraph.slot_types_default_out` in its own 'Comfy.SlotDefaults'
// extension and we need to wait for that to happen.
wait(500).then(() => {
LiteGraph.slot_types_default_out["RGTHREE_CONTEXT"] =
LiteGraph.slot_types_default_out["RGTHREE_CONTEXT"] || [];
LiteGraph.slot_types_default_out["RGTHREE_CONTEXT"].push(comfyClass.comfyClass);
});
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
addConnectionLayoutSupport(ctxClass, app, [
["Left", "Right"],
["Right", "Left"],
]);
setTimeout(() => {
ctxClass.category = comfyClass.category;
});
}
}
/**
* The original Context node.
*/
class ContextNode extends BaseContextNode {
static override title = NodeTypesString.CONTEXT;
static override type = NodeTypesString.CONTEXT;
static comfyClass = NodeTypesString.CONTEXT;
constructor(title = ContextNode.title) {
super(title);
}
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
BaseContextNode.setUp(comfyClass, nodeData, ContextNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextNode, app, {
name: "Convert To Context Big",
callback: (node) => {
replaceNode(node, ContextBigNode.type);
},
});
}
}
/**
* The Context Big node.
*/
class ContextBigNode extends BaseContextNode {
static override title = NodeTypesString.CONTEXT_BIG;
static override type = NodeTypesString.CONTEXT_BIG;
static comfyClass = NodeTypesString.CONTEXT_BIG;
constructor(title = ContextBigNode.title) {
super(title);
}
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
BaseContextNode.setUp(comfyClass, nodeData, ContextBigNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextBigNode, app, {
name: "Convert To Context (Original)",
callback: (node) => {
replaceNode(node, ContextNode.type);
},
});
}
}
/**
* A base node for Context Switche nodes and Context Merges nodes that will always add another empty
* ctx input, no less than five.
*/
class BaseContextMultiCtxInputNode extends BaseContextNode {
private stabilizeBound = this.stabilize.bind(this);
constructor(title: string) {
super(title);
// Adding five. Note, configure will add as many as was in the stored workflow automatically.
this.addContextInput(5);
}
private addContextInput(num = 1) {
for (let i = 0; i < num; i++) {
this.addInput(`ctx_${String(this.inputs.length + 1).padStart(2, "0")}`, "RGTHREE_CONTEXT");
}
}
override onConnectionsChange(
type: number,
slotIndex: number,
isConnected: boolean,
link: LLink,
ioSlot: INodeInputSlot | INodeOutputSlot,
): void {
super.onConnectionsChange?.apply(this, [...arguments] as any);
if (type === LiteGraph.INPUT) {
this.scheduleStabilize();
}
}
private scheduleStabilize(ms = 64) {
return debounce(this.stabilizeBound, 64);
}
/**
* Stabilizes the inputs; removing any disconnected ones from the bottom, then adding an empty
* one to the end so we always have one empty one to expand.
*/
private stabilize() {
removeUnusedInputsFromEnd(this, 4);
this.addContextInput();
}
}
/**
* The Context Switch (original) node.
*/
class ContextSwitchNode extends BaseContextMultiCtxInputNode {
static override title = NodeTypesString.CONTEXT_SWITCH;
static override type = NodeTypesString.CONTEXT_SWITCH;
static comfyClass = NodeTypesString.CONTEXT_SWITCH;
constructor(title = ContextSwitchNode.title) {
super(title);
}
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
BaseContextNode.setUp(comfyClass, nodeData, ContextSwitchNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextSwitchNode, app, {
name: "Convert To Context Switch Big",
callback: (node) => {
replaceNode(node, ContextSwitchBigNode.type);
},
});
}
}
/**
* The Context Switch Big node.
*/
class ContextSwitchBigNode extends BaseContextMultiCtxInputNode {
static override title = NodeTypesString.CONTEXT_SWITCH_BIG;
static override type = NodeTypesString.CONTEXT_SWITCH_BIG;
static comfyClass = NodeTypesString.CONTEXT_SWITCH_BIG;
constructor(title = ContextSwitchBigNode.title) {
super(title);
}
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
BaseContextNode.setUp(comfyClass, nodeData, ContextSwitchBigNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextSwitchBigNode, app, {
name: "Convert To Context Switch",
callback: (node) => {
replaceNode(node, ContextSwitchNode.type);
},
});
}
}
/**
* The Context Merge (original) node.
*/
class ContextMergeNode extends BaseContextMultiCtxInputNode {
static override title = NodeTypesString.CONTEXT_MERGE;
static override type = NodeTypesString.CONTEXT_MERGE;
static comfyClass = NodeTypesString.CONTEXT_MERGE;
constructor(title = ContextMergeNode.title) {
super(title);
}
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
BaseContextNode.setUp(comfyClass, nodeData, ContextMergeNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextMergeNode, app, {
name: "Convert To Context Merge Big",
callback: (node) => {
replaceNode(node, ContextMergeBigNode.type);
},
});
}
}
/**
* The Context Switch Big node.
*/
class ContextMergeBigNode extends BaseContextMultiCtxInputNode {
static override title = NodeTypesString.CONTEXT_MERGE_BIG;
static override type = NodeTypesString.CONTEXT_MERGE_BIG;
static comfyClass = NodeTypesString.CONTEXT_MERGE_BIG;
constructor(title = ContextMergeBigNode.title) {
super(title);
}
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
BaseContextNode.setUp(comfyClass, nodeData, ContextMergeBigNode);
}
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) {
BaseContextNode.onRegisteredForOverride(comfyClass, ctxClass);
addMenuItem(ContextMergeBigNode, app, {
name: "Convert To Context Switch",
callback: (node) => {
replaceNode(node, ContextMergeNode.type);
},
});
}
}
const contextNodes = [
ContextNode,
ContextBigNode,
ContextSwitchNode,
ContextSwitchBigNode,
ContextMergeNode,
ContextMergeBigNode,
];
const contextTypeToServerDef: { [type: string]: ComfyObjectInfo } = {};
function fixBadConfigs(node: ContextNode) {
// Dumb mistake, but let's fix our mispelling. This will probably need to stay in perpetuity to
// keep any old workflows operating.
const wrongName = node.outputs.find((o, i) => o.name === "CLIP_HEIGTH");
if (wrongName) {
wrongName.name = "CLIP_HEIGHT";
}
}
app.registerExtension({
name: "rgthree.Context",
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
// Loop over out context nodes and see if any match the server data.
for (const ctxClass of contextNodes) {
if (nodeData.name === ctxClass.type) {
contextTypeToServerDef[ctxClass.type] = nodeData;
ctxClass.setUp(nodeType, nodeData);
break;
}
}
},
async nodeCreated(node: TLGraphNode) {
const type = node.type || (node.constructor as any).type;
const serverDef = type && contextTypeToServerDef[type];
if (serverDef) {
fixBadConfigs(node as ContextNode);
matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef);
// Switches don't need to change inputs, only context outputs
if (!type!.includes("Switch") && !type!.includes("Merge")) {
matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef);
}
// }, 100);
}
},
/**
* When we're loaded from the server, check if we're using an out of date version and update our
* inputs / outputs to match.
*/
async loadedGraphNode(node: TLGraphNode) {
const type = node.type || (node.constructor as any).type;
const serverDef = type && contextTypeToServerDef[type];
if (serverDef) {
fixBadConfigs(node as ContextNode);
matchLocalSlotsToServer(node, IoDirection.OUTPUT, serverDef);
// Switches don't need to change inputs, only context outputs
if (!type!.includes("Switch") && !type!.includes("Merge")) {
matchLocalSlotsToServer(node, IoDirection.INPUT, serverDef);
}
}
},
});