Files
rgthree-rgthree-comfy/web/comfyui/base_node.js
T
unclehooruandClaude Code 2f6b4a72c5 Fix copy/clone disconnecting links on the original node
On ComfyUI frontend 1.53.1+ (after the ECS migration), slot links are
resolved from the graph's link store by (graph, node id, slot) instead
of `input.link`. A cloned node keeps the original's id, and
RgthreeBaseNode.clone() attached the original graph to the clone as a
workaround for ComfyUI_frontend#5037. As a result:

- BaseAnyInputConnectedNode.clone() trimming extra inputs with
  removeInput() disconnected input slots 1+ of the ORIGINAL node
  (Fast Bypasser/Muter, Mute/Bypass Repeater/Relay, Random Unmuter,
  Node Collector, Fast Actions Button) on single-node copy, clone,
  duplicate, alt+drag clone and "Save Selected as Template".
- DynamicContextNodeBase.clone() also cut the original's outputs 1+.
- The graph-attached clone serialized the original's link ids.

Trim the clone's slot arrays directly (they carry no links) and stop
attaching the graph to clones, so clones behave like stock clones.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 22:03:22 +09:00

323 lines
13 KiB
JavaScript

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, findPrototypePropertyDescriptor, moveArrayItem, } from "../../rgthree/common/shared_utils.js";
export class RgthreeBaseNode extends LGraphNode {
constructor(title = RgthreeBaseNode.title, skipOnConstructedCall = true) {
super(title);
this.comfyClass = "__NEED_COMFY_CLASS__";
this.nickname = "rgthree";
this.isVirtualNode = false;
this.isDropEnabled = false;
this.removed = false;
this.configuring = false;
this._tempWidth = 0;
this.__constructed__ = false;
this.helpDialog = null;
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();
});
const protoMode = findPrototypePropertyDescriptor(this, "mode");
const readMode = () => (protoMode === null || protoMode === void 0 ? void 0 : protoMode.get) ? protoMode.get.call(this) : this.rgthree_mode;
defineProperty(this, "mode", {
get: () => readMode(),
set: (mode) => {
const oldMode = readMode();
if (protoMode === null || protoMode === void 0 ? void 0 : protoMode.set) {
protoMode.set.call(this, mode);
}
else {
this.rgthree_mode = mode;
}
if (oldMode != mode) {
this.onModeChange(oldMode, mode);
}
},
});
}
checkAndRunOnConstructed() {
var _a;
if (!this.__constructed__) {
this.onConstructed();
const [n, v] = rgthree.logger.logParts(LogLevel.DEV, `[RgthreeBaseNode] Child class did not call onConstructed for "${this.type}.`);
(_a = console[n]) === null || _a === void 0 ? void 0 : _a.call(console, ...v);
}
return this.__constructed__;
}
onDragOver(e) {
if (!this.isDropEnabled)
return false;
return importIndividualNodesInnerOnDragOver(this, e);
}
async onDragDrop(e) {
if (!this.isDropEnabled)
return false;
return importIndividualNodesInnerOnDragDrop(this, e);
}
onConstructed() {
var _a;
if (this.__constructed__)
return false;
this.type = (_a = this.type) !== null && _a !== void 0 ? _a : undefined;
this.__constructed__ = true;
rgthree.invokeExtensionsAsync("nodeCreated", this);
return this.__constructed__;
}
configure(info) {
this.configuring = true;
super.configure(info);
for (const w of this.widgets || []) {
w.last_y = w.last_y || 0;
}
this.configuring = false;
}
clone() {
const cloned = super.clone();
if ((cloned === null || cloned === void 0 ? void 0 : cloned.properties) && !!window.structuredClone) {
cloned.properties = structuredClone(cloned.properties);
}
return cloned;
}
onModeChange(from, to) {
}
async handleAction(action) {
action;
}
removeWidget(widget) {
var _a;
if (typeof widget === "number") {
widget = this.widgets[widget];
}
if (!widget)
return;
const canUseComfyUIRemoveWidget = false;
if (canUseComfyUIRemoveWidget && typeof super.removeWidget === "function") {
super.removeWidget(widget);
}
else {
const index = this.widgets.indexOf(widget);
if (index > -1) {
this.widgets.splice(index, 1);
}
(_a = widget.onRemove) === null || _a === void 0 ? void 0 : _a.call(widget);
}
}
replaceWidget(widgetOrSlot, newWidget) {
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) {
var _a, _b;
const menu_info = [];
if ((_b = (_a = slot === null || slot === void 0 ? void 0 : slot.output) === null || _a === void 0 ? void 0 : _a.links) === null || _b === void 0 ? void 0 : _b.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;
}
onRemoved() {
var _a;
(_a = super.onRemoved) === null || _a === void 0 ? void 0 : _a.call(this);
this.removed = true;
}
static setUp(...args) {
}
getHelp() {
return "";
}
showHelp() {
const help = this.getHelp() || this.constructor.help;
if (help) {
this.helpDialog = new RgthreeHelpDialog(this, help).show();
this.helpDialog.addEventListener("close", (e) => {
this.helpDialog = null;
});
}
}
onKeyDown(event) {
KEY_EVENT_SERVICE.handleKeyDownOrUp(event);
if (event.key == "?" && !this.helpDialog) {
this.showHelp();
}
}
onKeyUp(event) {
KEY_EVENT_SERVICE.handleKeyDownOrUp(event);
}
getExtraMenuOptions(canvas, options) {
var _a, _b, _c, _d, _e, _f;
if (super.getExtraMenuOptions) {
(_a = super.getExtraMenuOptions) === null || _a === void 0 ? void 0 : _a.apply(this, [canvas, options]);
}
else if ((_c = (_b = this.constructor.nodeType) === null || _b === void 0 ? void 0 : _b.prototype) === null || _c === void 0 ? void 0 : _c.getExtraMenuOptions) {
(_f = (_e = (_d = this.constructor.nodeType) === null || _d === void 0 ? void 0 : _d.prototype) === null || _e === void 0 ? void 0 : _e.getExtraMenuOptions) === null || _f === void 0 ? void 0 : _f.apply(this, [canvas, options]);
}
const help = this.getHelp() || this.constructor.help;
if (help) {
addHelpMenuItem(this, help, options);
}
return [];
}
}
RgthreeBaseNode.exposedActions = [];
RgthreeBaseNode.title = "__NEED_CLASS_TITLE__";
RgthreeBaseNode.type = "__NEED_CLASS_TYPE__";
RgthreeBaseNode.category = "rgthree";
RgthreeBaseNode._category = "rgthree";
export class RgthreeBaseVirtualNode extends RgthreeBaseNode {
constructor(title = RgthreeBaseNode.title) {
super(title, false);
this.isVirtualNode = true;
}
static 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 {
constructor(title) {
super(title, true);
this.isDropEnabled = true;
this.serialize_widgets = true;
this.setupFromServerNodeData();
this.onConstructed();
}
getWidgets() {
return ComfyWidgets;
}
async setupFromServerNodeData() {
var _a, _b, _c;
var _d, _e;
const nodeDef = this.constructor.nodeDef || this.constructor.nodeData;
if (!nodeDef) {
throw Error("No node data");
}
this.comfyClass = nodeDef.name;
let inputs = nodeDef["input"]["required"];
if (nodeDef["input"]["optional"] != undefined) {
inputs = Object.assign({}, inputs, nodeDef["input"]["optional"]);
}
const WIDGETS = this.getWidgets();
const config = {
minWidth: 1,
minHeight: 1,
widget: null,
};
for (const inputName in inputs) {
const inputData = inputs[inputName];
const type = inputData[0];
if ((_a = inputData[1]) === null || _a === void 0 ? void 0 : _a.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 && ((_b = inputData[1]) === null || _b === void 0 ? void 0 : _b.forceInput) && (config === null || config === void 0 ? void 0 : config.widget)) {
if (!config.widget.options)
config.widget.options = {};
config.widget.options.forceInput = inputData[1].forceInput;
}
if (widgetCreated && ((_c = inputData[1]) === null || _c === void 0 ? void 0 : _c.defaultInput) && (config === null || config === void 0 ? void 0 : config.widget)) {
if (!config.widget.options)
config.widget.options = {};
config.widget.options.defaultInput = inputData[1].defaultInput;
}
}
}
for (const o in nodeDef["output"]) {
let output = nodeDef["output"][o];
if (output instanceof Array)
output = "COMBO";
const outputName = nodeDef["output_name"][o] || output;
const outputShape = nodeDef["output_is_list"][o]
? LiteGraph.GRID_SHAPE
: LiteGraph.CIRCLE_SHAPE;
this.addOutput(outputName, output, { shape: outputShape });
}
const s = this.computeSize();
s[0] = Math.max((_d = config.minWidth) !== null && _d !== void 0 ? _d : 1, s[0] * 1.5);
s[1] = Math.max((_e = config.minHeight) !== null && _e !== void 0 ? _e : 1, s[1]);
this.size = s;
this.serialize_widgets = true;
}
static registerForOverride(comfyClass, nodeData, rgthreeClass) {
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.nodeDef = nodeData;
rgthreeClass.onRegisteredForOverride(comfyClass, rgthreeClass);
}
}
static onRegisteredForOverride(comfyClass, rgthreeClass) {
}
}
RgthreeBaseServerNode.nodeType = null;
RgthreeBaseServerNode.nodeDef = null;
RgthreeBaseServerNode.__registeredForOverride__ = false;
const OVERRIDDEN_SERVER_NODES = new Map();
const oldregisterNodeType = LiteGraph.registerNodeType;
LiteGraph.registerNodeType = async function (nodeId, baseClass) {
var _a;
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.`);
(_a = console[n]) === null || _a === void 0 ? void 0 : _a.call(console, ...v);
}
return oldregisterNodeType.call(LiteGraph, nodeId, clazz);
};