import {app} from "scripts/app.js";
import {
IoDirection,
followConnectionUntilType,
getConnectedInputInfosAndFilterPassThroughs,
} from "./utils.js";
import {rgthree} from "./rgthree.js";
import {
SERVICE as CONTEXT_SERVICE,
InputMutation,
InputMutationOperation,
} from "./services/context_service.js";
import {NodeTypesString} from "./constants.js";
import {removeUnusedInputsFromEnd} from "./utils_inputs_outputs.js";
import {INodeInputSlot, INodeOutputSlot, INodeSlot, LGraphNode, LLink} from "typings/litegraph.js";
import {ComfyNodeConstructor, ComfyObjectInfo} from "typings/comfy.js";
import {DynamicContextNodeBase} from "./dynamic_context_base.js";
import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js";
const OWNED_PREFIX = "+";
const REGEX_OWNED_PREFIX = /^\+\s*/;
const REGEX_EMPTY_INPUT = /^\+\s*$/;
/**
* The Dynamic Context node.
*/
export class DynamicContextNode extends DynamicContextNodeBase {
static override title = NodeTypesString.DYNAMIC_CONTEXT;
static override type = NodeTypesString.DYNAMIC_CONTEXT;
static comfyClass = NodeTypesString.DYNAMIC_CONTEXT;
constructor(title = DynamicContextNode.title) {
super(title);
}
override onNodeCreated() {
this.addInput("base_ctx", "RGTHREE_DYNAMIC_CONTEXT");
this.ensureOneRemainingNewInputSlot();
super.onNodeCreated();
}
override onConnectionsChange(
type: number,
slotIndex: number,
isConnected: boolean,
link: LLink,
ioSlot: INodeSlot,
): void {
super.onConnectionsChange?.call(this, type, slotIndex, isConnected, link, ioSlot);
if (this.configuring) {
return;
}
if (type === LiteGraph.INPUT) {
if (isConnected) {
this.handleInputConnected(slotIndex);
} else {
this.handleInputDisconnected(slotIndex);
}
}
}
override onConnectInput(
inputIndex: number,
outputType: INodeOutputSlot["type"],
outputSlot: INodeOutputSlot,
outputNode: LGraphNode,
outputIndex: number,
): boolean {
let canConnect = true;
if (super.onConnectInput) {
canConnect = super.onConnectInput.apply(this, [...arguments] as any);
}
if (
canConnect &&
outputNode instanceof DynamicContextNode &&
outputIndex === 0 &&
inputIndex !== 0
) {
const [n, v] = rgthree.logger.warnParts(
"Currently, you can only connect a context node in the first slot.",
);
console[n]?.call(console, ...v);
canConnect = false;
}
return canConnect;
}
handleInputConnected(slotIndex: number) {
const ioSlot = this.inputs[slotIndex];
const connectedIndexes = [];
if (slotIndex === 0) {
let baseNodeInfos = getConnectedInputInfosAndFilterPassThroughs(this, this, 0);
const baseNodes = baseNodeInfos.map((n) => n.node)!;
const baseNodesDynamicCtx = baseNodes[0] as DynamicContextNodeBase;
if (baseNodesDynamicCtx?.provideInputsData) {
const inputsData = CONTEXT_SERVICE.getDynamicContextInputsData(baseNodesDynamicCtx);
console.log("inputsData", inputsData);
for (const input of baseNodesDynamicCtx.provideInputsData()) {
if (input.name === "base_ctx" || input.name === "+") {
continue;
}
this.addContextInput(input.name, input.type, input.index);
this.stabilizeNames();
}
}
} else if (this.isInputSlotForNewInput(slotIndex)) {
this.handleNewInputConnected(slotIndex);
}
}
isInputSlotForNewInput(slotIndex: number) {
const ioSlot = this.inputs[slotIndex];
return ioSlot && ioSlot.name === "+" && ioSlot.type === "*";
}
handleNewInputConnected(slotIndex: number) {
if (!this.isInputSlotForNewInput(slotIndex)) {
throw new Error('Expected the incoming slot index to be the "new input" input.');
}
const ioSlot = this.inputs[slotIndex]!;
let cxn = null;
if (ioSlot.link != null) {
cxn = followConnectionUntilType(this, IoDirection.INPUT, slotIndex, true);
}
if (cxn?.type && cxn?.name) {
let name = this.addOwnedPrefix(this.getNextUniqueNameForThisNode(cxn.name));
if (name.match(/^\+\s*[A-Z_]+(\.\d+)?$/)) {
name = name.toLowerCase();
}
ioSlot.name = name;
ioSlot.type = cxn.type as string;
ioSlot.removable = true;
while (!this.outputs[slotIndex]) {
this.addOutput("*", "*");
}
this.outputs[slotIndex]!.type = cxn.type as string;
this.outputs[slotIndex]!.name = this.stripOwnedPrefix(name).toLocaleUpperCase();
// This is a dumb override for ComfyUI's widgetinputs issues.
if (cxn.type === "COMBO" || cxn.type.includes(",") || Array.isArray(cxn.type)) {
(this.outputs[slotIndex] as any).widget = true;
}
this.inputsMutated({
operation: InputMutationOperation.ADDED,
node: this,
slotIndex,
slot: ioSlot,
});
this.stabilizeNames();
this.ensureOneRemainingNewInputSlot();
}
}
handleInputDisconnected(slotIndex: number) {
const inputs = this.getContextInputsList();
if (slotIndex === 0) {
for (let index = inputs.length - 1; index > 0; index--) {
if (index === 0 || index === inputs.length - 1) {
continue;
}
const input = inputs[index]!;
if (!this.isOwnedInput(input.name)) {
if (input.link || this.outputs[index]?.links?.length) {
this.renameContextInput(index, input.name, true);
} else {
this.removeContextInput(index);
}
}
}
this.setSize(this.computeSize());
this.setDirtyCanvas(true, true);
}
}
ensureOneRemainingNewInputSlot() {
removeUnusedInputsFromEnd(this, 1, REGEX_EMPTY_INPUT);
this.addInput(OWNED_PREFIX, "*");
}
getNextUniqueNameForThisNode(desiredName: string) {
const inputs = this.getContextInputsList();
const allExistingKeys = inputs.map((i) => this.stripOwnedPrefix(i.name).toLocaleUpperCase());
desiredName = this.stripOwnedPrefix(desiredName);
let newName = desiredName;
let n = 0;
while (allExistingKeys.includes(newName.toLocaleUpperCase())) {
newName = `${desiredName}.${++n}`;
}
return newName;
}
override removeInput(slotIndex: number) {
const slot = this.inputs[slotIndex]!;
super.removeInput(slotIndex);
if (this.outputs[slotIndex]) {
this.removeOutput(slotIndex);
}
this.inputsMutated({operation: InputMutationOperation.REMOVED, node: this, slotIndex, slot});
this.stabilizeNames();
}
stabilizeNames() {
const inputs = this.getContextInputsList();
const names: string[] = [];
for (const [index, input] of inputs.entries()) {
if (index === 0 || index === inputs.length - 1) {
continue;
}
input.label = undefined;
this.outputs[index]!.label = undefined;
let origName = this.stripOwnedPrefix(input.name).replace(/\.\d+$/, "");
let name = input.name;
if (!this.isOwnedInput(name)) {
names.push(name.toLocaleUpperCase());
} else {
let n = 0;
name = this.addOwnedPrefix(origName);
while (names.includes(this.stripOwnedPrefix(name).toLocaleUpperCase())) {
name = `${this.addOwnedPrefix(origName)}.${++n}`;
}
names.push(this.stripOwnedPrefix(name).toLocaleUpperCase());
if (input.name !== name) {
this.renameContextInput(index, name);
}
}
}
}
override getSlotMenuOptions(slot: {
slot: number;
input?: INodeInputSlot | undefined;
output?: INodeOutputSlot | undefined;
}) {
const editable = this.isOwnedInput(slot.input!.name) && this.type !== "*";
return [
{
content: "✏️ Rename Input",
disabled: !editable,
callback: () => {
var dialog = app.canvas.createDialog(
"Name",
{},
);
var dialogInput = dialog.querySelector("input")!;
if (dialogInput) {
dialogInput.value = this.stripOwnedPrefix(slot.input!.name || "");
}
var inner = () => {
this.handleContextMenuRenameInputDialog(slot.slot, dialogInput.value);
dialog.close();
};
dialog.querySelector("button")!.addEventListener("click", inner);
dialogInput.addEventListener("keydown", (e) => {
dialog.is_modified = true;
if (e.keyCode == 27) {
dialog.close();
} else if (e.keyCode == 13) {
inner();
} else if (e.keyCode != 13 && (e.target as HTMLElement)?.localName != "textarea") {
return;
}
e.preventDefault();
e.stopPropagation();
});
dialogInput.focus();
},
},
{
content: "🗑️ Delete Input",
disabled: !editable,
callback: () => {
this.removeInput(slot.slot);
},
},
];
}
handleContextMenuRenameInputDialog(slotIndex: number, value: string) {
app.graph.beforeChange();
this.renameContextInput(slotIndex, value);
this.stabilizeNames();
this.setDirtyCanvas(true, true);
app.graph.afterChange();
}
}
const contextDynamicNodes = [DynamicContextNode];
app.registerExtension({
name: "rgthree.DynamicContext",
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
if (!CONFIG_SERVICE.getConfigValue("unreleased.dynamic_context.enabled")) {
return;
}
if (nodeData.name === DynamicContextNode.type) {
DynamicContextNode.setUp(nodeType, nodeData);
}
},
});