File size: 17,116 Bytes
4450790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
import type { ComfyNodeConstructor, ComfyObjectInfo, NodeMode } from "typings/comfy.js";
import type {
  IWidget,
  SerializedLGraphNode,
  LGraphNode as TLGraphNode,
  LGraphCanvas,
  ContextMenuItem,
  INodeOutputSlot,
  INodeInputSlot,
} from "typings/litegraph.js";
import type { RgthreeBaseServerNodeConstructor, RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js";

import { ComfyWidgets } from "scripts/widgets.js";
import { SERVICE as KEY_EVENT_SERVICE } from "./services/key_events_services.js";
import { app } from "scripts/app.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 } from "rgthree/common/shared_utils.js";

/**

 * A base node with standard methods, directly extending the LGraphNode.

 * This can be used for ui-nodes and a further base for server nodes.

 */
export abstract class RgthreeBaseNode extends LGraphNode {
  /**

   * Action strings that can be exposed and triggered from other nodes, like Fast Actions Button.

   */
  static exposedActions: string[] = [];

  static override title: string = "__NEED_CLASS_TITLE__";
  static category = "rgthree";
  static _category = "rgthree"; // `category` seems to get reset by comfy, so reset to this after.

  /**

   * The comfyClass is property ComfyUI and extensions may care about, even through it is only for

   * server nodes. RgthreeBaseServerNode below overrides this with the expected value and we just

   * set it here so extensions that are none the wiser don't break on some unchecked string method

   * call on an undefined calue.

   */
  comfyClass: string = "__NEED_COMFY_CLASS__";

  /** Used by the ComfyUI-Manager badge. */
  readonly nickname = "rgthree";
  /** Are we a virtual node? */
  readonly isVirtualNode: boolean = false;
  /** Are we able to be dropped on (if config is enabled too). */
  isDropEnabled = false;
  /** A state member determining if we're currently removed. */
  removed = false;
  /** A state member determining if we're currently "configuring."" */
  configuring = false;
  /** A temporary width value that can be used to ensure compute size operates correctly. */
  _tempWidth = 0;

  /** Private Mode member so we can override the setter/getter and call an `onModeChange`. */
  private rgthree_mode: NodeMode;
  /** An internal bool set when `onConstructed` is run. */
  private __constructed__ = false;
  /** The help dialog. */
  private helpDialog: RgthreeHelpDialog | null = null;

  constructor(title = RgthreeBaseNode.title, skipOnConstructedCall = true) {
    super(title);
    if (title == "__NEED_CLASS_TITLE__") {
      throw new Error("RgthreeBaseNode needs overrides.");
    }
    // Ensure these exist since some other extensions will break in their onNodeCreated.
    this.widgets = this.widgets || [];
    this.properties = this.properties || {};

    // Some checks we want to do after we're constructed, looking that data is set correctly and
    // that our base's `onConstructed` was called (if not, set a DEV warning).
    setTimeout(() => {
      // Check we have a comfyClass defined.
      if (this.comfyClass == "__NEED_COMFY_CLASS__") {
        throw new Error("RgthreeBaseNode needs a comfy class override.");
      }
      // Ensure we've called onConstructed before we got here.
      this.checkAndRunOnConstructed();
    });

    defineProperty(this, 'mode', {
      get: () => {
        return this.rgthree_mode;
      },
      set: (mode: NodeMode) => {
        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__;
  }

  onDragOver(e: DragEvent): boolean {
    if (!this.isDropEnabled) return false;
    return importIndividualNodesInnerOnDragOver(this, e);
  }

  async onDragDrop(e: DragEvent): Promise<boolean> {
    if (!this.isDropEnabled) return false;
    return importIndividualNodesInnerOnDragDrop(this, e);
  }

  /**

   * When a node is finished with construction, we must call this. Failure to do so will result in

   * an error message from the timeout in this base class. This is broken out and becomes the

   * responsibility of the child class because

   */
  onConstructed() {
    if (this.__constructed__) return false;
    // This is kinda a hack, but if this.type is still null, then set it to undefined to match.
    this.type = this.type ?? undefined;
    this.__constructed__ = true;
    rgthree.invokeExtensionsAsync("nodeCreated", this);
    return this.__constructed__;
  }

  override configure(info: SerializedLGraphNode<TLGraphNode>): void {
    this.configuring = true;
    super.configure(info);
    // Fix https://github.com/comfyanonymous/ComfyUI/issues/1448 locally.
    // Can removed when fixed and adopted.
    for (const w of this.widgets || []) {
      w.last_y = w.last_y || 0;
    }
    this.configuring = false;
  }

  /**

   * Override clone for, at the least, deep-copying properties.

   */
  override clone() {
    const cloned = super.clone();
    // This is whild, but LiteGraph clone doesn't deep clone data, so we will. We'll use structured
    // clone, which most browsers in 2022 support, but but we'll check.
    if (cloned.properties && !!window.structuredClone) {
      cloned.properties = structuredClone(cloned.properties);
    }
    return cloned;
  }

  /** When a mode change, we want all connected nodes to match. */
  onModeChange(from: NodeMode, to: NodeMode) {
    // Override
  }

  /**

   * Given a string, do something. At the least, handle any `exposedActions` that may be called and

   * passed into from other nodes, like Fast Actions Button

   */
  async handleAction(action: string) {
    action; // No-op. Should be overridden but OK if not.
  }

  /**

   * Guess this doesn't exist in Litegraph...

   */
  removeWidget(widgetOrSlot?: IWidget | number) {
    if (typeof widgetOrSlot === "number") {
      this.widgets.splice(widgetOrSlot, 1);
    } else if (widgetOrSlot) {
      const index = this.widgets.indexOf(widgetOrSlot);
      if (index > -1) {
        this.widgets.splice(index, 1);
      }
    }
  }

  /**

   * A default version of the logive when a node does not set `getSlotMenuOptions`. This is

   * necessary because child nodes may want to define getSlotMenuOptions but LiteGraph then won't do

   * it's default logic. This bakes it so child nodes can call this instead (and this doesn't set

   * getSlotMenuOptions for all child nodes in case it doesn't exist).

   */
  defaultGetSlotMenuOptions(slot: {
    input?: INodeInputSlot;
    output?: INodeOutputSlot;
  }): ContextMenuItem[] | null {
    const menu_info: ContextMenuItem[] = [];
    if (slot?.output?.links?.length) {
      menu_info.push({ content: "Disconnect Links", slot: 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[]) {
    // No-op.
  }

  /**

   * A function to provide help text to be overridden.

   */
  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: ContextMenuItem[]): void {
    // Some other extensions override getExtraMenuOptions on the nodeType as it comes through from
    // the server, so we can call out to that if we don't have our own.
    if (super.getExtraMenuOptions) {
      super.getExtraMenuOptions?.apply(this, [canvas, options]);
    } else if ((this.constructor as any).nodeType?.prototype?.getExtraMenuOptions) {
      (this.constructor as any).nodeType?.prototype?.getExtraMenuOptions?.apply(this, [
        canvas,
        options,
      ]);
    }
    // If we have help content, then add a menu item.
    const help = this.getHelp() || (this.constructor as any).help;
    if (help) {
      addHelpMenuItem(this, help, options);
    }
  }
}

/**

 * A virtual node. Right now, this is just a wrapper for RgthreeBaseNode (which was the initial

 * base virtual node).

 *

 * TODO: Make RgthreeBaseNode private and move all virtual nodes to this class; cleanup

 * RgthreeBaseNode assumptions that its virtual.

 */
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;
    }
  }
}

/**

 * A base node with standard methods, extending the LGraphNode.

 * This is somewhat experimental, but if comfyui is going to keep breaking widgets and inputs, it

 * seems safer than NOT overriding.

 */
export class RgthreeBaseServerNode extends RgthreeBaseNode {
  static nodeData: ComfyObjectInfo | null = null;
  static nodeType: ComfyNodeConstructor | null = null;

  // Drop is enabled by default for server nodes.
  override isDropEnabled = true;

  constructor(title: string) {
    super(title, true);
    this.serialize_widgets = true;
    this.setupFromServerNodeData();
    this.onConstructed();
  }

  getWidgets() {
    return ComfyWidgets;
  }

  /**

   * This takes the server data and builds out the inputs, outputs and widgets. It's similar to the

   * ComfyNode constructor in registerNodes in ComfyUI's app.js, but is more stable and thus

   * shouldn't break as often when it modifyies widgets and types.

   */
  async setupFromServerNodeData() {
    const nodeData = (this.constructor as any).nodeData;
    if (!nodeData) {
      throw Error("No node data");
    }

    // Necessary for serialization so Comfy backend can check types.
    // Serialized as `class_type`. See app.js#graphToPrompt
    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 we're forcing the input, just do it now and forget all that widget stuff.
      // This is one of the differences from ComfyNode and provides smoother experience for inputs
      // that are going to remain inputs anyway.
      // Also, it fixes https://github.com/comfyanonymous/ComfyUI/issues/1404 (for rgthree nodes)
      if (inputData[1]?.forceInput) {
        this.addInput(inputName, type);
      } else {
        let widgetCreated = true;
        if (Array.isArray(type)) {
          // Enums
          Object.assign(config, WIDGETS.COMBO(this, inputName, inputData, app) || {});
        } else if (`${type}:${inputName}` in WIDGETS) {
          // Support custom widgets by Type:Name
          Object.assign(
            config,
            WIDGETS[`${type}:${inputName}`]!(this, inputName, inputData, app) || {},
          );
        } else if (type in WIDGETS) {
          // Standard type widgets
          Object.assign(config, WIDGETS[type]!(this, inputName, inputData, app) || {});
        } else {
          // Node connection inputs
          this.addInput(inputName, type);
          widgetCreated = false;
        }

        // Don't actually need this right now, but ported it over from ComfyWidget.
        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, s[0] * 1.5);
    s[1] = Math.max(config.minHeight, s[1]);
    this.size = s;
    this.serialize_widgets = true;
  }

  static __registeredForOverride__: boolean = false;
  static registerForOverride(

    comfyClass: ComfyNodeConstructor,

    nodeData: ComfyObjectInfo,

    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);
    // Mark the rgthreeClass as `__registeredForOverride__` because ComfyUI will repeatedly call
    // this and certain setups will only want to setup once (like adding context menus, etc).
    if (!rgthreeClass.__registeredForOverride__) {
      rgthreeClass.__registeredForOverride__ = true;
      rgthreeClass.nodeType = comfyClass;
      rgthreeClass.nodeData = nodeData;
      rgthreeClass.onRegisteredForOverride(comfyClass, rgthreeClass);
    }
  }

  static onRegisteredForOverride(comfyClass: any, rgthreeClass: any) {
    // To be overridden
  }
}

/**

 * Keeps track of the rgthree-comfy nodes that come from the server (and want to be ComfyNodes) that

 * we override into a own, more flexible and cleaner nodes.

 */
const OVERRIDDEN_SERVER_NODES = new Map<any, any>();

const oldregisterNodeType = LiteGraph.registerNodeType;
/**

 * ComfyUI calls registerNodeType with its ComfyNode, but we don't trust that will remain stable, so

 * we need to identify it, intercept it, and supply our own class for the node.

 */
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);
    // Note, we don't currently call our rgthree.invokeExtensionsAsync w/ beforeRegisterNodeDef as
    // this runs right after that. However, this does mean that extensions cannot actually change
    // anything about overriden server rgthree nodes in their beforeRegisterNodeDef (as when comfy
    // calls it, it's for the wrong ComfyNode class). Calling it here, however, would re-run
    // everything causing more issues than not. If we wanted to support beforeRegisterNodeDef then
    // it would mean rewriting ComfyUI's registerNodeDef which, frankly, is not worth it.
  }
  return oldregisterNodeType.call(LiteGraph, nodeId, clazz);
};