| import type {
|
| IWidget,
|
| LGraphCanvas,
|
| IContextMenuValue,
|
| IFoundSlot,
|
| LGraphEventMode,
|
| LGraphNodeConstructor,
|
| ISerialisedNode,
|
| IBaseWidget,
|
| } from "@comfyorg/frontend";
|
| import type {ComfyNodeDef} from "typings/comfy.js";
|
| import type {RgthreeBaseServerNodeConstructor} from "typings/rgthree.js";
|
|
|
| import {app} from "scripts/app.js";
|
| import {ComfyWidgets} from "scripts/widgets.js";
|
| import {SERVICE as KEY_EVENT_SERVICE} from "./services/key_events_services.js";
|
| import {LogLevel, rgthree} from "./rgthree.js";
|
| import {addHelpMenuItem} from "./utils.js";
|
| import {RgthreeHelpDialog} from "rgthree/common/dialog.js";
|
| import {
|
| importIndividualNodesInnerOnDragDrop,
|
| importIndividualNodesInnerOnDragOver,
|
| } from "./feature_import_individual_nodes.js";
|
| import {defineProperty, moveArrayItem} from "rgthree/common/shared_utils.js";
|
|
|
| |
| |
| |
|
|
| export abstract class RgthreeBaseNode extends LGraphNode {
|
| |
| |
|
|
| static exposedActions: string[] = [];
|
|
|
| static override title: string = "__NEED_CLASS_TITLE__";
|
| static override type: string = "__NEED_CLASS_TYPE__";
|
| static override category = "rgthree";
|
| static _category = "rgthree";
|
|
|
|
|
| override widgets!: IWidget[];
|
|
|
| |
| |
| |
| |
| |
|
|
| override comfyClass: string = "__NEED_COMFY_CLASS__";
|
|
|
|
|
| readonly nickname = "rgthree";
|
|
|
| override readonly isVirtualNode: boolean = false;
|
|
|
| isDropEnabled = false;
|
|
|
| removed = false;
|
|
|
| configuring = false;
|
|
|
| _tempWidth = 0;
|
|
|
|
|
| private rgthree_mode?: LGraphEventMode;
|
|
|
|
|
| private __constructed__ = false;
|
|
|
| private helpDialog: RgthreeHelpDialog | null = null;
|
|
|
| constructor(title = RgthreeBaseNode.title, skipOnConstructedCall = true) {
|
| super(title);
|
| if (title == "__NEED_CLASS_TITLE__") {
|
| throw new Error("RgthreeBaseNode needs overrides.");
|
| }
|
|
|
| this.widgets = this.widgets || [];
|
| this.properties = this.properties || {};
|
|
|
|
|
|
|
| setTimeout(() => {
|
|
|
| if (this.comfyClass == "__NEED_COMFY_CLASS__") {
|
| throw new Error("RgthreeBaseNode needs a comfy class override.");
|
| }
|
| if (this.constructor.type == "__NEED_CLASS_TYPE__") {
|
| throw new Error("RgthreeBaseNode needs overrides.");
|
| }
|
|
|
| this.checkAndRunOnConstructed();
|
| });
|
|
|
| defineProperty(this, "mode", {
|
| get: () => {
|
| return this.rgthree_mode;
|
| },
|
| set: (mode: LGraphEventMode) => {
|
| if (this.rgthree_mode != mode) {
|
| const oldMode = this.rgthree_mode;
|
| this.rgthree_mode = mode;
|
| this.onModeChange(oldMode, mode);
|
| }
|
| },
|
| });
|
| }
|
|
|
| private checkAndRunOnConstructed() {
|
| if (!this.__constructed__) {
|
| this.onConstructed();
|
| const [n, v] = rgthree.logger.logParts(
|
| LogLevel.DEV,
|
| `[RgthreeBaseNode] Child class did not call onConstructed for "${this.type}.`,
|
| );
|
| console[n]?.(...v);
|
| }
|
| return this.__constructed__;
|
| }
|
|
|
| override onDragOver(e: DragEvent): boolean {
|
| if (!this.isDropEnabled) return false;
|
| return importIndividualNodesInnerOnDragOver(this, e);
|
| }
|
|
|
| override async onDragDrop(e: DragEvent): Promise<boolean> {
|
| if (!this.isDropEnabled) return false;
|
| return importIndividualNodesInnerOnDragDrop(this, e);
|
| }
|
|
|
| |
| |
| |
| |
|
|
| onConstructed() {
|
| if (this.__constructed__) return false;
|
|
|
| this.type = this.type ?? undefined;
|
| this.__constructed__ = true;
|
| rgthree.invokeExtensionsAsync("nodeCreated", this);
|
| return this.__constructed__;
|
| }
|
|
|
| override configure(info: ISerialisedNode): void {
|
| this.configuring = true;
|
| super.configure(info);
|
|
|
|
|
| for (const w of this.widgets || []) {
|
| w.last_y = w.last_y || 0;
|
| }
|
| this.configuring = false;
|
| }
|
|
|
| |
| |
|
|
| override clone() {
|
| const cloned = super.clone()!;
|
|
|
|
|
| if (cloned?.properties && !!window.structuredClone) {
|
| cloned.properties = structuredClone(cloned.properties);
|
| }
|
|
|
|
|
|
|
|
|
|
|
| cloned.graph = this.graph;
|
| return cloned;
|
| }
|
|
|
|
|
| onModeChange(from: LGraphEventMode | undefined, to: LGraphEventMode) {
|
|
|
| }
|
|
|
| |
| |
| |
|
|
| async handleAction(action: string) {
|
| action;
|
| }
|
|
|
| |
| |
|
|
| override removeWidget(widget: IBaseWidget | IWidget | number | undefined): void {
|
| if (typeof widget === "number") {
|
| widget = this.widgets[widget];
|
| }
|
| if (!widget) return;
|
|
|
|
|
|
|
|
|
|
|
| const canUseComfyUIRemoveWidget = false;
|
| if (canUseComfyUIRemoveWidget && typeof super.removeWidget === 'function') {
|
| super.removeWidget(widget as IBaseWidget);
|
| } else {
|
| const index = this.widgets.indexOf(widget as IWidget);
|
| if (index > -1) {
|
| this.widgets.splice(index, 1);
|
| }
|
| widget.onRemove?.();
|
| }
|
| }
|
|
|
| |
| |
|
|
| replaceWidget(widgetOrSlot: IWidget | number | undefined, newWidget: IWidget) {
|
| let index = null;
|
| if (widgetOrSlot) {
|
| index = typeof widgetOrSlot === "number" ? widgetOrSlot : this.widgets.indexOf(widgetOrSlot);
|
| this.removeWidget(this.widgets[index]!);
|
| }
|
| index = index != null ? index : this.widgets.length - 1;
|
| if (this.widgets.includes(newWidget)) {
|
| moveArrayItem(this.widgets, newWidget, index);
|
| } else {
|
| this.widgets.splice(index, 0, newWidget);
|
| }
|
| }
|
|
|
| |
| |
| |
| |
| |
|
|
| defaultGetSlotMenuOptions(slot: IFoundSlot): IContextMenuValue[] {
|
| const menu_info: IContextMenuValue[] = [];
|
| if (slot?.output?.links?.length) {
|
| menu_info.push({content: "Disconnect Links", slot});
|
| }
|
| let inputOrOutput = slot.input || slot.output;
|
| if (inputOrOutput) {
|
| if (inputOrOutput.removable) {
|
| menu_info.push(
|
| inputOrOutput.locked ? {content: "Cannot remove"} : {content: "Remove Slot", slot},
|
| );
|
| }
|
| if (!inputOrOutput.nameLocked) {
|
| menu_info.push({content: "Rename Slot", slot});
|
| }
|
| }
|
| return menu_info;
|
| }
|
|
|
| override onRemoved(): void {
|
| super.onRemoved?.();
|
| this.removed = true;
|
| }
|
|
|
| static setUp<T extends RgthreeBaseNode>(...args: any[]) {
|
|
|
| }
|
|
|
| |
| |
|
|
| getHelp() {
|
| return "";
|
| }
|
|
|
| showHelp() {
|
| const help = this.getHelp() || (this.constructor as any).help;
|
| if (help) {
|
| this.helpDialog = new RgthreeHelpDialog(this, help).show();
|
| this.helpDialog.addEventListener("close", (e) => {
|
| this.helpDialog = null;
|
| });
|
| }
|
| }
|
|
|
| override onKeyDown(event: KeyboardEvent): void {
|
| KEY_EVENT_SERVICE.handleKeyDownOrUp(event);
|
| if (event.key == "?" && !this.helpDialog) {
|
| this.showHelp();
|
| }
|
| }
|
|
|
| override onKeyUp(event: KeyboardEvent): void {
|
| KEY_EVENT_SERVICE.handleKeyDownOrUp(event);
|
| }
|
|
|
| override getExtraMenuOptions(
|
| canvas: LGraphCanvas,
|
| options: (IContextMenuValue<unknown> | null)[],
|
| ): (IContextMenuValue<unknown> | null)[] {
|
|
|
|
|
| if (super.getExtraMenuOptions) {
|
| super.getExtraMenuOptions?.apply(this, [canvas, options]);
|
| } else if (this.constructor.nodeType?.prototype?.getExtraMenuOptions) {
|
| this.constructor.nodeType?.prototype?.getExtraMenuOptions?.apply(this, [canvas, options]);
|
| }
|
|
|
| const help = this.getHelp() || (this.constructor as any).help;
|
| if (help) {
|
| addHelpMenuItem(this, help, options);
|
| }
|
| return [];
|
| }
|
| }
|
|
|
| |
| |
| |
|
|
| export class RgthreeBaseVirtualNode extends RgthreeBaseNode {
|
| override isVirtualNode = true;
|
|
|
| constructor(title = RgthreeBaseNode.title) {
|
| super(title, false);
|
| }
|
|
|
| static override setUp() {
|
| if (!this.type) {
|
| throw new Error(`Missing type for RgthreeBaseVirtualNode: ${this.title}`);
|
| }
|
| LiteGraph.registerNodeType(this.type, this);
|
| if (this._category) {
|
| this.category = this._category;
|
| }
|
| }
|
| }
|
|
|
| |
| |
| |
| |
|
|
| export class RgthreeBaseServerNode extends RgthreeBaseNode {
|
| static nodeType: LGraphNodeConstructor | null = null;
|
| static nodeData: ComfyNodeDef | null = null;
|
|
|
|
|
| override isDropEnabled = true;
|
|
|
| constructor(title: string) {
|
| super(title, true);
|
| this.serialize_widgets = true;
|
| this.setupFromServerNodeData();
|
| this.onConstructed();
|
| }
|
|
|
| getWidgets() {
|
| return ComfyWidgets;
|
| }
|
|
|
| |
| |
| |
| |
|
|
| async setupFromServerNodeData() {
|
| const nodeData = (this.constructor as any).nodeData;
|
| if (!nodeData) {
|
| throw Error("No node data");
|
| }
|
|
|
|
|
|
|
| this.comfyClass = nodeData.name;
|
|
|
| let inputs = nodeData["input"]["required"];
|
| if (nodeData["input"]["optional"] != undefined) {
|
| inputs = Object.assign({}, inputs, nodeData["input"]["optional"]);
|
| }
|
|
|
| const WIDGETS = this.getWidgets();
|
|
|
| const config: {minWidth: number; minHeight: number; widget?: null | {options: any}} = {
|
| minWidth: 1,
|
| minHeight: 1,
|
| widget: null,
|
| };
|
| for (const inputName in inputs) {
|
| const inputData = inputs[inputName];
|
| const type = inputData[0];
|
|
|
|
|
|
|
|
|
| if (inputData[1]?.forceInput) {
|
| this.addInput(inputName, type);
|
| } else {
|
| let widgetCreated = true;
|
| if (Array.isArray(type)) {
|
|
|
| Object.assign(config, WIDGETS.COMBO(this, inputName, inputData, app) || {});
|
| } else if (`${type}:${inputName}` in WIDGETS) {
|
|
|
| Object.assign(
|
| config,
|
| WIDGETS[`${type}:${inputName}`]!(this, inputName, inputData, app) || {},
|
| );
|
| } else if (type in WIDGETS) {
|
|
|
| Object.assign(config, WIDGETS[type]!(this, inputName, inputData, app) || {});
|
| } else {
|
|
|
| this.addInput(inputName, type);
|
| widgetCreated = false;
|
| }
|
|
|
|
|
| if (widgetCreated && inputData[1]?.forceInput && config?.widget) {
|
| if (!config.widget.options) config.widget.options = {};
|
| config.widget.options.forceInput = inputData[1].forceInput;
|
| }
|
| if (widgetCreated && inputData[1]?.defaultInput && config?.widget) {
|
| if (!config.widget.options) config.widget.options = {};
|
| config.widget.options.defaultInput = inputData[1].defaultInput;
|
| }
|
| }
|
| }
|
|
|
| for (const o in nodeData["output"]) {
|
| let output = nodeData["output"][o];
|
| if (output instanceof Array) output = "COMBO";
|
| const outputName = nodeData["output_name"][o] || output;
|
| const outputShape = nodeData["output_is_list"][o]
|
| ? LiteGraph.GRID_SHAPE
|
| : LiteGraph.CIRCLE_SHAPE;
|
| this.addOutput(outputName, output, {shape: outputShape});
|
| }
|
|
|
| const s = this.computeSize();
|
|
|
|
|
|
|
|
|
| s[0] = Math.max(config.minWidth ?? 1, s[0] * 1.5);
|
| s[1] = Math.max(config.minHeight ?? 1, s[1]);
|
| this.size = s;
|
| this.serialize_widgets = true;
|
| }
|
|
|
| static __registeredForOverride__: boolean = false;
|
| static registerForOverride(
|
| comfyClass: typeof LGraphNode,
|
| nodeData: ComfyNodeDef,
|
| rgthreeClass: RgthreeBaseServerNodeConstructor,
|
| ) {
|
| if (OVERRIDDEN_SERVER_NODES.has(comfyClass)) {
|
| throw Error(
|
| `Already have a class to override ${
|
| comfyClass.type || comfyClass.name || comfyClass.title
|
| }`,
|
| );
|
| }
|
| OVERRIDDEN_SERVER_NODES.set(comfyClass, rgthreeClass);
|
|
|
|
|
| if (!rgthreeClass.__registeredForOverride__) {
|
| rgthreeClass.__registeredForOverride__ = true;
|
| rgthreeClass.nodeType = comfyClass;
|
| rgthreeClass.nodeData = nodeData;
|
| rgthreeClass.onRegisteredForOverride(comfyClass, rgthreeClass);
|
| }
|
| }
|
|
|
| static onRegisteredForOverride(comfyClass: any, rgthreeClass: any) {
|
|
|
| }
|
| }
|
|
|
| |
| |
| |
|
|
| const OVERRIDDEN_SERVER_NODES = new Map<any, any>();
|
|
|
| const oldregisterNodeType = LiteGraph.registerNodeType;
|
| |
| |
| |
|
|
| LiteGraph.registerNodeType = async function (nodeId: string, baseClass: any) {
|
| const clazz = OVERRIDDEN_SERVER_NODES.get(baseClass) || baseClass;
|
| if (clazz !== baseClass) {
|
| const classLabel = clazz.type || clazz.name || clazz.title;
|
| const [n, v] = rgthree.logger.logParts(
|
| LogLevel.DEBUG,
|
| `${nodeId}: replacing default ComfyNode implementation with custom ${classLabel} class.`,
|
| );
|
| console[n]?.(...v);
|
|
|
|
|
|
|
|
|
|
|
|
|
| }
|
| return oldregisterNodeType.call(LiteGraph, nodeId, clazz);
|
| };
|
|
|