Preserve missing-node widget values

This commit is contained in:
2026-07-04 21:34:16 +02:00
parent dd3e51301c
commit 2603697fe9
4 changed files with 235 additions and 18 deletions
+9 -16
View File
@@ -1,4 +1,5 @@
import { app } from "../../scripts/app.js";
import { applyWidgetTransfers, planWidgetTransfers } from "./utfcn_widget_transfer.js";
/*
* UTFCN — Use The F***ing Core Nodes (frontend).
@@ -20,7 +21,7 @@ import { app } from "../../scripts/app.js";
const EXT = "UTFCN";
let INDEX = null; // { sources, candidates, stats }
const shapeCache = new Map(); // targetType -> { inputs, outputs, widgetNames } | null
const shapeCache = new Map(); // targetType -> { inputs, outputs, widgets, widgetNames } | null
/* -------------------------------------------------------------------------- */
/* data */
@@ -96,18 +97,17 @@ function typeOk(a, b) {
return A.some((x) => B.includes(x));
}
/** A widget the user converted into an input slot — its value lives on the input, not the widget. */
const isConvertedWidget = (w) => w?.type === "converted-widget" || w?.type === "hidden";
/** Inspect a target type's slot/widget layout once (creating a throwaway node) and cache it. */
function targetShape(type) {
if (shapeCache.has(type)) return shapeCache.get(type);
let node = null;
try { node = window.LiteGraph.createNode(type); } catch { /* unregistered */ }
const widgets = (node?.widgets || []).map((w) => ({ name: w.name, type: w.type }));
const shape = node && {
inputs: (node.inputs || []).map((s) => ({ name: s.name, type: s.type })),
outputs: (node.outputs || []).map((s) => ({ name: s.name, type: s.type })),
widgetNames: (node.widgets || []).map((w) => w.name),
widgets,
widgetNames: widgets.map((w) => w.name),
};
shapeCache.set(type, shape || null);
return shape || null;
@@ -146,12 +146,9 @@ function planSwap(node, targetType, rule) {
usedOut.add(j); outMap.push({ src: i, dst: j });
});
(node.widgets || []).forEach((w) => {
if (w.name == null || isConvertedWidget(w)) return;
const want = rule?.widgets?.[w.name] ?? w.name;
if (shape.widgetNames.includes(want)) wMap.push({ from: w.name, to: want });
else if (w.value !== undefined && w.value !== null && w.value !== "") warns.push(`widget “${w.name}” value not carried`);
});
const widgetPlan = planWidgetTransfers(node, shape, rule, { allowSerializedIndexFallback: isMissing(node) });
wMap.push(...widgetPlan.wMap);
warns.push(...widgetPlan.warns);
return { ok: problems.length === 0, problems, warns, inMap, outMap, wMap, targetType };
}
@@ -170,11 +167,7 @@ function applySwap(node, plan, rule) {
if (node.bgcolor) t.bgcolor = node.bgcolor;
// widget values first (setting them may lay out extra widgets)
plan.wMap.forEach((m) => {
const sw = (node.widgets || []).find((w) => w.name === m.from);
const tw = (t.widgets || []).find((w) => w.name === m.to);
if (sw && tw && sw.value !== undefined) { tw.value = sw.value; try { tw.callback?.(tw.value); } catch {} }
});
applyWidgetTransfers(node, t, plan.wMap);
// snapshot link records BEFORE we start mutating the graph
const inLinks = plan.inMap
+121
View File
@@ -0,0 +1,121 @@
/** A widget the user converted into an input slot -- its value lives on the input, not the widget. */
export const isConvertedWidget = (w) => w?.type === "converted-widget" || w?.type === "hidden";
const hasStoredValue = (v) => v !== undefined;
const hasReportableValue = (v) => v !== undefined && v !== null && v !== "";
function shapeWidgets(shape) {
if (Array.isArray(shape?.widgets)) return shape.widgets;
return (shape?.widgetNames || []).map((name) => ({ name }));
}
export function serializedWidgetValues(node) {
const values = node?.last_serialization?.widgets_values;
return Array.isArray(values) ? values : [];
}
function findTargetWidgetIndex(widgets, name, used) {
return widgets.findIndex((w, i) => !used.has(i) && !isConvertedWidget(w) && w.name === name);
}
/**
* Plan widget value transfers.
*
* Installed nodes can match by widget name (plus curated widget remaps). Missing
* nodes often have no live widget objects, only ComfyUI's ordered
* last_serialization.widgets_values array, so they may also opt into same-index
* fallback.
*/
export function planWidgetTransfers(node, shape, rule = {}, options = {}) {
const targetWidgets = shapeWidgets(shape);
const liveWidgets = Array.isArray(node?.widgets) ? node.widgets : [];
const serializedValues = serializedWidgetValues(node);
const allowSerializedIndexFallback = !!options.allowSerializedIndexFallback;
const usedTargets = new Set();
const mappedSources = new Set();
const pendingWarns = [];
const pendingWarnSources = new Set();
const wMap = [];
const warns = [];
liveWidgets.forEach((w, sourceIndex) => {
if (w?.name == null || isConvertedWidget(w)) return;
const want = rule?.widgets?.[w.name] ?? w.name;
const targetIndex = findTargetWidgetIndex(targetWidgets, want, usedTargets);
if (targetIndex >= 0) {
usedTargets.add(targetIndex);
mappedSources.add(sourceIndex);
wMap.push({ from: w.name, fromIndex: sourceIndex, to: targetWidgets[targetIndex].name, toIndex: targetIndex });
} else if (hasReportableValue(w.value)) {
pendingWarns.push({ sourceIndex, message: `widget "${w.name}" value not carried` });
pendingWarnSources.add(sourceIndex);
}
});
if (allowSerializedIndexFallback) {
serializedValues.forEach((value, sourceIndex) => {
if (!hasStoredValue(value) || mappedSources.has(sourceIndex)) return;
const target = targetWidgets[sourceIndex];
if (target && !usedTargets.has(sourceIndex) && !isConvertedWidget(target) && target.name != null) {
usedTargets.add(sourceIndex);
mappedSources.add(sourceIndex);
wMap.push({ fromIndex: sourceIndex, toIndex: sourceIndex, to: target.name, serialized: true });
} else if (hasReportableValue(value) && !pendingWarnSources.has(sourceIndex)) {
warns.push(`serialized widget #${sourceIndex + 1} value not carried`);
}
});
}
pendingWarns.forEach((warn) => {
if (!mappedSources.has(warn.sourceIndex)) warns.push(warn.message);
});
return { wMap, warns };
}
function widgetByName(widgets, name) {
return widgets.find((w) => w?.name === name);
}
function sourceWidgetValue(node, transfer) {
const widgets = Array.isArray(node?.widgets) ? node.widgets : [];
const values = serializedWidgetValues(node);
if (transfer.serialized && Number.isInteger(transfer.fromIndex)) {
if (transfer.fromIndex < values.length && hasStoredValue(values[transfer.fromIndex])) return values[transfer.fromIndex];
}
if (transfer.from != null) {
const widget = widgetByName(widgets, transfer.from);
if (hasStoredValue(widget?.value)) return widget.value;
}
if (Number.isInteger(transfer.fromIndex)) {
const widget = widgets[transfer.fromIndex];
if (hasStoredValue(widget?.value)) return widget.value;
if (transfer.fromIndex < values.length && hasStoredValue(values[transfer.fromIndex])) return values[transfer.fromIndex];
}
return undefined;
}
function targetWidget(node, transfer) {
const widgets = Array.isArray(node?.widgets) ? node.widgets : [];
if (transfer.to != null) {
const widget = widgetByName(widgets, transfer.to);
if (widget) return widget;
}
if (Number.isInteger(transfer.toIndex)) return widgets[transfer.toIndex];
return null;
}
export function applyWidgetTransfers(source, target, transfers) {
let applied = 0;
(transfers || []).forEach((transfer) => {
const value = sourceWidgetValue(source, transfer);
const widget = targetWidget(target, transfer);
if (!widget || !hasStoredValue(value)) return;
widget.value = value;
try { widget.callback?.(widget.value); } catch {}
applied++;
});
return applied;
}