multimodalart's picture
Squashing commit
4450790 verified
import type {INodeInputSlot} from "typings/litegraph.js";
import {BaseContextNode} from "./context.js";
import {ComfyNodeConstructor, ComfyObjectInfo} from "typings/comfy.js";
import {RgthreeBaseServerNode} from "./base_node.js";
import {moveArrayItem, wait} from "rgthree/common/shared_utils.js";
import {RgthreeInvisibleWidget} from "./utils_widgets.js";
import {
getContextOutputName,
InputMutation,
InputMutationOperation,
} from "./services/context_service.js";
import {app} from "scripts/app.js";
import {SERVICE as CONTEXT_SERVICE} from "./services/context_service.js";
const OWNED_PREFIX = "+";
const REGEX_OWNED_PREFIX = /^\+\s*/;
const REGEX_EMPTY_INPUT = /^\+\s*$/;
export type InputLike = {
name: string;
type: string | -1;
label?: string;
link: number | null;
removable?: boolean;
};
/**
* The base context node that contains some shared between DynamicContext nodes. Not labels
* `abstract` so we can reference `this` in static methods.
*/
export class DynamicContextNodeBase extends BaseContextNode {
protected readonly hasShadowInputs: boolean = false;
getContextInputsList(): InputLike[] {
return this.inputs;
}
provideInputsData() {
const inputs = this.getContextInputsList();
return inputs
.map((input, index) => ({
name: this.stripOwnedPrefix(input.name),
type: String(input.type),
index,
}))
.filter((i) => i.type !== "*");
}
addOwnedPrefix(name: string) {
return `+ ${this.stripOwnedPrefix(name)}`;
}
isOwnedInput(inputOrName: string | null | INodeInputSlot) {
const name = typeof inputOrName == "string" ? inputOrName : inputOrName?.name || "";
return REGEX_OWNED_PREFIX.test(name);
}
stripOwnedPrefix(name: string) {
return name.replace(REGEX_OWNED_PREFIX, "");
}
// handleUpstreamMutation(mutation: InputMutation) {
// throw new Error('handleUpstreamMutation not overridden!')
// }
handleUpstreamMutation(mutation: InputMutation) {
console.log(`[node ${this.id}] handleUpstreamMutation`, mutation);
if (mutation.operation === InputMutationOperation.ADDED) {
const slot = mutation.slot;
if (!slot) {
throw new Error("Cannot have an ADDED mutation without a provided slot data.");
}
this.addContextInput(
this.stripOwnedPrefix(slot.name),
slot.type as string,
mutation.slotIndex,
);
return;
}
if (mutation.operation === InputMutationOperation.REMOVED) {
const slot = mutation.slot;
if (!slot) {
throw new Error("Cannot have an REMOVED mutation without a provided slot data.");
}
this.removeContextInput(mutation.slotIndex);
return;
}
if (mutation.operation === InputMutationOperation.RENAMED) {
const slot = mutation.slot;
if (!slot) {
throw new Error("Cannot have an RENAMED mutation without a provided slot data.");
}
this.renameContextInput(mutation.slotIndex, slot.name);
return;
}
}
override clone() {
const cloned = super.clone();
while (cloned.inputs.length > 1) {
cloned.removeInput(cloned.inputs.length - 1);
}
while (cloned.widgets.length > 1) {
cloned.removeWidget(cloned.widgets.length - 1);
}
while (cloned.outputs.length > 1) {
cloned.removeOutput(cloned.outputs.length - 1);
}
return cloned;
}
/**
* Adds the basic output_keys widget. Should be called _after_ specific nodes setup their inputs
* or widgets.
*/
override onNodeCreated() {
const node = this;
this.addCustomWidget(
new RgthreeInvisibleWidget("output_keys", "RGTHREE_DYNAMIC_CONTEXT_OUTPUTS", "", () => {
return (node.outputs || [])
.map((o, i) => i > 0 && o.name)
.filter((n) => n !== false)
.join(",");
}),
);
}
addContextInput(name: string, type: string, slot = -1) {
const inputs = this.getContextInputsList();
if (this.hasShadowInputs) {
inputs.push({name, type, link: null});
} else {
this.addInput(name, type);
}
if (slot > -1) {
moveArrayItem(inputs, inputs.length - 1, slot);
} else {
slot = inputs.length - 1;
}
if (type !== "*") {
const output = this.addOutput(getContextOutputName(name), type);
if (type === "COMBO" || String(type).includes(",") || Array.isArray(type)) {
(output as any).widget = true;
}
if (slot > -1) {
moveArrayItem(this.outputs, this.outputs.length - 1, slot);
}
}
this.fixInputsOutputsLinkSlots();
this.inputsMutated({
operation: InputMutationOperation.ADDED,
node: this,
slotIndex: slot,
slot: inputs[slot]!,
});
}
removeContextInput(slotIndex: number) {
if (this.hasShadowInputs) {
const inputs = this.getContextInputsList();
const input = inputs.splice(slotIndex, 1)[0];
if (this.outputs[slotIndex]) {
this.removeOutput(slotIndex);
}
} else {
this.removeInput(slotIndex);
}
}
renameContextInput(index: number, newName: string, forceOwnBool: boolean | null = null) {
const inputs = this.getContextInputsList();
const input = inputs[index]!;
const oldName = input.name;
newName = this.stripOwnedPrefix(newName.trim() || this.getSlotDefaultInputLabel(index));
if (forceOwnBool === true || (this.isOwnedInput(oldName) && forceOwnBool !== false)) {
newName = this.addOwnedPrefix(newName);
}
if (oldName !== newName) {
input.name = newName;
input.removable = this.isOwnedInput(newName);
this.outputs[index]!.name = getContextOutputName(inputs[index]!.name);
this.inputsMutated({
node: this,
operation: InputMutationOperation.RENAMED,
slotIndex: index,
slot: input,
});
}
}
getSlotDefaultInputLabel(slotIndex: number) {
const inputs = this.getContextInputsList();
const input = inputs[slotIndex]!;
let defaultLabel = this.stripOwnedPrefix(input.name).toLowerCase();
return defaultLabel.toLocaleLowerCase();
}
inputsMutated(mutation: InputMutation) {
CONTEXT_SERVICE.onInputChanges(this, mutation);
}
fixInputsOutputsLinkSlots() {
if (!this.hasShadowInputs) {
const inputs = this.getContextInputsList();
for (let index = inputs.length - 1; index > 0; index--) {
const input = inputs[index]!;
if ((input === null || input === void 0 ? void 0 : input.link) != null) {
app.graph.links[input.link!]!.target_slot = index;
}
}
}
const outputs = this.outputs;
for (let index = outputs.length - 1; index > 0; index--) {
const output = outputs[index];
if (output) {
output.nameLocked = true;
for (const link of output.links || []) {
app.graph.links[link!]!.origin_slot = index;
}
}
}
}
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) {
RgthreeBaseServerNode.registerForOverride(comfyClass, nodeData, this);
// [🤮] 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_DYNAMIC_CONTEXT"] =
LiteGraph.slot_types_default_out["RGTHREE_DYNAMIC_CONTEXT"] || [];
LiteGraph.slot_types_default_out["RGTHREE_DYNAMIC_CONTEXT"].push(comfyClass.comfyClass);
});
}
}