import type {ComfyNodeConstructor, ComfyObjectInfo} from "typings/comfy.js"; import type {INodeSlot, LGraphNode, LLink, LGraphCanvas} from "typings/litegraph.js"; import {app} from "scripts/app.js"; import {DynamicContextNodeBase, InputLike} from "./dynamic_context_base.js"; import {NodeTypesString} from "./constants.js"; import { InputMutation, SERVICE as CONTEXT_SERVICE, stripContextInputPrefixes, getContextOutputName, } from "./services/context_service.js"; import {getConnectedInputNodesAndFilterPassThroughs} from "./utils.js"; import {debounce, moveArrayItem} from "rgthree/common/shared_utils.js"; import {measureText} from "./utils_canvas.js"; import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js"; type ShadowInputData = { node: LGraphNode; slot: number; shadowIndex: number; shadowIndexIfShownSingularly: number; shadowIndexFull: number; nodeIndex: number; type: string | -1; name: string; key: string; // isDuplicatedBefore: boolean, duplicatesBefore: number[]; duplicatesAfter: number[]; }; /** * The Context Switch node. */ class DynamicContextSwitchNode extends DynamicContextNodeBase { static override title = NodeTypesString.DYNAMIC_CONTEXT_SWITCH; static override type = NodeTypesString.DYNAMIC_CONTEXT_SWITCH; static comfyClass = NodeTypesString.DYNAMIC_CONTEXT_SWITCH; protected override readonly hasShadowInputs = true; // override hasShadowInputs = true; /** * We should be able to assume that `lastInputsList` is the input list after the last, major * synchronous change. Which should mean, if we're handling a change that is currently live, but * not represented in our node (like, an upstream node has already removed an input), then we * should be able to compar the current InputList to this `lastInputsList`. */ lastInputsList: ShadowInputData[] = []; private shadowInputs: (InputLike & {count: number})[] = [ {name: "base_ctx", type: "RGTHREE_DYNAMIC_CONTEXT", link: null, count: 0}, ]; constructor(title = DynamicContextSwitchNode.title) { super(title); } override getContextInputsList() { return this.shadowInputs; } override handleUpstreamMutation(mutation: InputMutation) { this.scheduleHardRefresh(); } 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) { this.scheduleHardRefresh(); } } scheduleHardRefresh(ms = 64) { return debounce(() => { this.refreshInputsAndOutputs(); }, ms); } override onNodeCreated() { this.addInput("ctx_1", "RGTHREE_DYNAMIC_CONTEXT"); this.addInput("ctx_2", "RGTHREE_DYNAMIC_CONTEXT"); this.addInput("ctx_3", "RGTHREE_DYNAMIC_CONTEXT"); this.addInput("ctx_4", "RGTHREE_DYNAMIC_CONTEXT"); this.addInput("ctx_5", "RGTHREE_DYNAMIC_CONTEXT"); super.onNodeCreated(); } override addContextInput(name: string, type: string, slot?: number): void {} /** * This is a "hard" refresh of the list, but looping over the actual context inputs, and * recompiling the shadowInputs and outputs. */ private refreshInputsAndOutputs() { const inputs: (InputLike & {count: number})[] = [ {name: "base_ctx", type: "RGTHREE_DYNAMIC_CONTEXT", link: null, count: 0}, ]; let numConnected = 0; for (let i = 0; i < this.inputs.length; i++) { const childCtxs = getConnectedInputNodesAndFilterPassThroughs( this, this, i, ) as DynamicContextNodeBase[]; if (childCtxs.length > 1) { throw new Error("How is there more than one input?"); } const ctx = childCtxs[0]; if (!ctx) continue; numConnected++; const slotsData = CONTEXT_SERVICE.getDynamicContextInputsData(ctx); console.log(slotsData); for (const slotData of slotsData) { const found = inputs.find( (n) => getContextOutputName(slotData.name) === getContextOutputName(n.name), ); if (found) { found.count += 1; continue; } inputs.push({ name: slotData.name, type: slotData.type, link: null, count: 1, }); } } this.shadowInputs = inputs; // First output is always CONTEXT, so "p" is the offset. let i = 0; for (i; i < this.shadowInputs.length; i++) { const data = this.shadowInputs[i]!; let existing = this.outputs.find( (o) => getContextOutputName(o.name) === getContextOutputName(data.name), ); if (!existing) { existing = this.addOutput(getContextOutputName(data.name), data.type); } moveArrayItem(this.outputs, existing, i); delete existing.rgthree_status; if (data.count !== numConnected) { existing.rgthree_status = "WARN"; } } while (this.outputs[i]) { const output = this.outputs[i]; if (output?.links?.length) { output.rgthree_status = "ERROR"; i++; } else { this.removeOutput(i); } } this.fixInputsOutputsLinkSlots(); } override onDrawForeground(ctx: CanvasRenderingContext2D, canvas: LGraphCanvas): void { const low_quality = (canvas?.ds?.scale ?? 1) < 0.6; if (low_quality || this.size[0] <= 10) { return; } let y = LiteGraph.NODE_SLOT_HEIGHT - 1; const w = this.size[0]; ctx.save(); ctx.font = "normal " + LiteGraph.NODE_SUBTEXT_SIZE + "px Arial"; ctx.textAlign = "right"; for (const output of this.outputs) { if (!output.rgthree_status) { y += LiteGraph.NODE_SLOT_HEIGHT; continue; } const x = w - 20 - measureText(ctx, output.name); if (output.rgthree_status === "ERROR") { ctx.fillText("🛑", x, y); } else if (output.rgthree_status === "WARN") { ctx.fillText("⚠️", x, y); } y += LiteGraph.NODE_SLOT_HEIGHT; } ctx.restore(); } } app.registerExtension({ name: "rgthree.DynamicContextSwitch", async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) { if (!CONFIG_SERVICE.getConfigValue("unreleased.dynamic_context.enabled")) { return; } if (nodeData.name === DynamicContextSwitchNode.type) { DynamicContextSwitchNode.setUp(nodeType, nodeData); } }, });