Spaces:
Building
on
Zero
Building
on
Zero
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( | |
"<span class='name'>Name</span><input autofocus type='text'/><button>OK</button>", | |
{}, | |
); | |
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); | |
} | |
}, | |
}); | |