Files
pekkAi-dev-ComfyUI-LegacyWi…/web/bugfix.js
T

190 lines
5.3 KiB
JavaScript

// Drop-in workaround for ComfyUI_frontend #12443.
//
// WidgetLegacy.vue's draw() unconditionally writes the Vue container's
// clientWidth onto widget.width on every frame. In LiteGraph mode this
// pollutes the widget object and the canvas renderer's
// `widget.width || nodeWidth` fallback then ignores node.size[0],
// so widgets stop tracking node resizes after a move.
//
// We can't edit the compiled Vue code, so we neutralize the *effect*:
// replace widget.width with a getter/setter that drops writes while
// the patch is active and LiteGraph mode is in control.
//
// Patch is installed when a LegacyWidgetWidthFix node is added to the
// graph. New nodes are repaired automatically while it is present, and
// the fix node also exposes a manual repair button as a fallback.
import { app } from "../../scripts/app.js";
const NODE_NAME = "LegacyWidgetWidthFix";
const WIDTH_KEY = "__lwwf_width";
const GUARDED = "__lwwf_guarded";
const REPAIR_BUTTON = "__lwwf_repair_button";
let activeCount = 0;
let hooksInstalled = false;
const origMethods = {};
// addDOMWidget and addCustomWidget bypass addWidget — they push directly
// onto node.widgets. Hook all three so we catch every code path that
// produces a widget, regardless of which custom node pack created it.
const HOOK_METHODS = ["addWidget", "addDOMWidget", "addCustomWidget"];
function inVueMode() {
// LiteGraph.vueNodesMode is the upstream flag the proper fix gates on.
// If it's undefined (older builds, tree-shaken, etc.) we assume LG mode
// and drop the write — that matches the regression we're fixing.
const LG = globalThis.LiteGraph;
return !!(LG && LG.vueNodesMode);
}
function guardWidget(w) {
if (!w || w[GUARDED]) return;
w[WIDTH_KEY] = w.width;
Object.defineProperty(w, "width", {
configurable: true,
enumerable: true,
get() {
return this[WIDTH_KEY];
},
set(v) {
if (!patchActive() || inVueMode()) {
this[WIDTH_KEY] = v;
}
// else: drop the write — this is the bug we're suppressing
},
});
Object.defineProperty(w, GUARDED, {
value: true,
enumerable: false,
configurable: true,
});
}
function repairWidget(w) {
if (!w) return;
guardWidget(w);
// Clear the stale persisted value so renderers fall back to node.size[0].
// Must be `undefined`, not 0 — DomWidgets.vue uses `width ?? nodeWidth`
// (nullish), which would not fall back from 0. Canvas renderer uses
// `width || nodeWidth` (falsy), which falls back from either. undefined
// works for both.
if (!inVueMode()) w[WIDTH_KEY] = undefined;
}
function unguardWidget(w) {
if (!w || !w[GUARDED]) return;
const v = w[WIDTH_KEY];
delete w.width;
delete w[WIDTH_KEY];
delete w[GUARDED];
w.width = v;
}
function patchActive() {
return activeCount > 0;
}
function hookWidgetMethods() {
if (hooksInstalled) return;
const proto = globalThis.LGraphNode?.prototype;
if (!proto) return;
for (const name of HOOK_METHODS) {
const fn = proto[name];
if (typeof fn !== "function") continue;
origMethods[name] = fn;
proto[name] = function patched(...args) {
const w = fn.apply(this, args);
if (w) {
guardWidget(w);
if (patchActive()) repairWidget(w);
}
return w;
};
}
hooksInstalled = true;
}
function forEachWidget(fn) {
const nodes = app.graph?._nodes ?? [];
for (const n of nodes) {
const ws = n.widgets;
if (!ws) continue;
for (const w of ws) fn(w, n);
}
}
function repairNode(node) {
for (const w of node?.widgets ?? []) repairWidget(w);
}
function repairAll() {
if (!patchActive()) return;
forEachWidget((w) => repairWidget(w));
app.graph?.setDirtyCanvas(true, true);
}
function addRepairButton(node) {
if (!node || node[REPAIR_BUTTON]) return;
const button = node.addWidget?.(
"button",
"Repair widget widths now",
null,
() => repairAll(),
{ serialize: false },
);
if (button) node[REPAIR_BUTTON] = button;
}
function isFixNode(node) {
return node?.comfyClass === NODE_NAME
|| node?.constructor?.comfyClass === NODE_NAME
|| node?.type === NODE_NAME;
}
function install() {
hookWidgetMethods();
repairAll();
}
function uninstall() {
forEachWidget((w) => unguardWidget(w));
app.graph?.setDirtyCanvas(true, true);
}
app.registerExtension({
name: "LegacyWidgetWidthFix",
nodeCreated(node) {
if (isFixNode(node)) addRepairButton(node);
if (!patchActive()) return;
repairNode(node);
// Other extensions can append widgets in their own nodeCreated hooks.
// Re-scan once those synchronous hooks and the current render pass finish.
globalThis.requestAnimationFrame?.(() => {
if (patchActive() && app.graph?._nodes?.includes(node)) repairNode(node);
});
},
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== NODE_NAME) return;
const origOnAdded = nodeType.prototype.onAdded;
nodeType.prototype.onAdded = function (...args) {
origOnAdded?.apply(this, args);
addRepairButton(this);
activeCount += 1;
if (activeCount === 1) install();
};
const origOnRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function (...args) {
origOnRemoved?.apply(this, args);
activeCount = Math.max(0, activeCount - 1);
if (activeCount === 0) uninstall();
};
},
});