mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-06 22:10:14 -03:00
feat(workflow): redirect gen-params updates to connected Primitive nodes (#1026)
When a KSampler marked as 'Send Gen Params Target' has widget inputs wired to Primitive nodes (PrimitiveNode, PrimitiveInt, PrimitiveFloat, etc.), sending gen params from the Lora Manager UI now updates the Primitive node's value instead of the KSampler widget. This is necessary because ComfyUI's execution engine reads from the connected input, ignoring the widget value when a wire is present. Also fix two minor issues found during review: - Remove unnecessary String() wrapping on numeric gen params (seed, steps, cfg) to preserve native types through the JSON/WS path - Correct misleading isNodeEnabled comment: LGraphEventMode values are 0=Always, 2=Never, 4=Bypass (not 'Normal/Enabled')
This commit is contained in:
@@ -254,7 +254,7 @@ function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
|
|||||||
const candidates = PARAM_TO_WIDGET_CANDIDATES[key] || [key];
|
const candidates = PARAM_TO_WIDGET_CANDIDATES[key] || [key];
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (widgetSet.has(candidate.toLowerCase())) {
|
if (widgetSet.has(candidate.toLowerCase())) {
|
||||||
updates.push({ widgetName: candidate, value: String(value) });
|
updates.push({ widgetName: candidate, value });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -605,7 +605,7 @@ function isNodeEnabled(node) {
|
|||||||
if (!node) {
|
if (!node) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// ComfyUI node mode: 0 = Normal/Enabled, others = Always/Never/OnEvent
|
// ComfyUI node mode (LGraphEventMode): 0 = Always, 2 = Never, 4 = Bypass
|
||||||
return node.mode === undefined || node.mode === 0;
|
return node.mode === undefined || node.mode === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,9 +204,9 @@ describe('findMatchingWidgets', () => {
|
|||||||
|
|
||||||
it('matches seed to seed widget', () => {
|
it('matches seed to seed widget', () => {
|
||||||
const updates = findMatchingWidgets(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
|
const updates = findMatchingWidgets(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
|
||||||
expect(updates).toContainEqual({ widgetName: 'seed', value: '42' });
|
expect(updates).toContainEqual({ widgetName: 'seed', value: 42 });
|
||||||
expect(updates).toContainEqual({ widgetName: 'steps', value: '30' });
|
expect(updates).toContainEqual({ widgetName: 'steps', value: 30 });
|
||||||
expect(updates).toContainEqual({ widgetName: 'cfg', value: '7' });
|
expect(updates).toContainEqual({ widgetName: 'cfg', value: 7 });
|
||||||
expect(updates).toContainEqual({ widgetName: 'sampler_name', value: 'euler_ancestral' });
|
expect(updates).toContainEqual({ widgetName: 'sampler_name', value: 'euler_ancestral' });
|
||||||
expect(updates).toContainEqual({ widgetName: 'scheduler', value: 'karras' });
|
expect(updates).toContainEqual({ widgetName: 'scheduler', value: 'karras' });
|
||||||
});
|
});
|
||||||
@@ -221,7 +221,7 @@ describe('findMatchingWidgets', () => {
|
|||||||
const updates = findMatchingWidgets(['noise_seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
|
const updates = findMatchingWidgets(['noise_seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
|
||||||
const seedUpdate = updates.find(u => u.widgetName === 'noise_seed');
|
const seedUpdate = updates.find(u => u.widgetName === 'noise_seed');
|
||||||
expect(seedUpdate).toBeDefined();
|
expect(seedUpdate).toBeDefined();
|
||||||
expect(seedUpdate.value).toBe('42');
|
expect(seedUpdate.value).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches rgthree-style sampler widget name', () => {
|
it('matches rgthree-style sampler widget name', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { getAllGraphNodes, getNodeReference, getNodeFromGraph, chainCallback } from "./utils.js";
|
import { getAllGraphNodes, getNodeReference, getNodeFromGraph, chainCallback, getLinkFromGraph } from "./utils.js";
|
||||||
import { ensureLmStyles } from "./lm_styles_loader.js";
|
import { ensureLmStyles } from "./lm_styles_loader.js";
|
||||||
|
|
||||||
const DEBOUNCE_DELAY = 500;
|
const DEBOUNCE_DELAY = 500;
|
||||||
@@ -76,6 +76,84 @@ function fadeWidgetTextColor(widget, fromColor, toColor, duration) {
|
|||||||
return () => { if (rafId) cancelAnimationFrame(rafId); };
|
return () => { if (rafId) cancelAnimationFrame(rafId); };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Primitive node helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of node type names that represent Primitive value nodes.
|
||||||
|
* Includes both the dynamic PrimitiveNode (created by double-clicking
|
||||||
|
* a widget input) and the static typed primitives from the node library.
|
||||||
|
*/
|
||||||
|
const PRIMITIVE_NODE_TYPES = new Set([
|
||||||
|
"PrimitiveNode", // dynamic (double-click a widget input)
|
||||||
|
"PrimitiveInt",
|
||||||
|
"PrimitiveFloat",
|
||||||
|
"PrimitiveString",
|
||||||
|
"PrimitiveBoolean",
|
||||||
|
"PrimitiveStringMultiline",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true when `node` is any flavour of Primitive node.
|
||||||
|
* @param {Object} node - LiteGraph node instance
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isPrimitiveNodeType(node) {
|
||||||
|
return PRIMITIVE_NODE_TYPES.has(node?.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the 0-based input slot index whose widget name matches `widgetName`.
|
||||||
|
* Returns -1 when no matching input is found.
|
||||||
|
*
|
||||||
|
* Matching strategy (in order):
|
||||||
|
* 1. `input.widget?.name === widgetName` — direct widget ref (preferred)
|
||||||
|
* 2. `input.name === widgetName` — fallback by slot name
|
||||||
|
*
|
||||||
|
* @param {Object} node - LiteGraph node instance
|
||||||
|
* @param {string} widgetName
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function findInputSlotForWidget(node, widgetName) {
|
||||||
|
if (!node || !Array.isArray(node.inputs)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return node.inputs.findIndex(
|
||||||
|
(inp) => inp?.widget?.name === widgetName || inp?.name === widgetName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the input slot that backs `widgetName` on `node` is connected to a
|
||||||
|
* Primitive node, return that Primitive node. Otherwise return null.
|
||||||
|
*
|
||||||
|
* This is the key bridge for the "send gen params → Primitive" flow:
|
||||||
|
* when a KSampler widget (e.g. "steps") has an incoming wire from a
|
||||||
|
* Primitive node, we want to update the Primitive's value instead of the
|
||||||
|
* KSampler widget, because ComfyUI's execution engine reads from the
|
||||||
|
* connected input, not the widget.
|
||||||
|
*
|
||||||
|
* @param {Object} node - the target node (e.g. KSampler)
|
||||||
|
* @param {string} widgetName - e.g. "steps", "cfg", "seed"
|
||||||
|
* @returns {Object|null} - the connected Primitive node, or null
|
||||||
|
*/
|
||||||
|
function tryResolvePrimitiveConnection(node, widgetName) {
|
||||||
|
const slotIndex = findInputSlotForWidget(node, widgetName);
|
||||||
|
if (slotIndex === -1) return null;
|
||||||
|
|
||||||
|
const input = node.inputs[slotIndex];
|
||||||
|
if (input?.link == null) return null;
|
||||||
|
|
||||||
|
const link = getLinkFromGraph(node.graph, input.link);
|
||||||
|
if (!link) return null;
|
||||||
|
|
||||||
|
const originNode = node.graph?.getNodeById?.(link.origin_id);
|
||||||
|
if (!originNode) return null;
|
||||||
|
|
||||||
|
return isPrimitiveNodeType(originNode) ? originNode : null;
|
||||||
|
}
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "LoraManager.WorkflowRegistry",
|
name: "LoraManager.WorkflowRegistry",
|
||||||
|
|
||||||
@@ -309,6 +387,60 @@ app.registerExtension({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Redirect to connected Primitive node when present ----
|
||||||
|
// When a widget input (e.g. "steps", "cfg", "seed" on KSampler)
|
||||||
|
// is wired to a Primitive node, the Primitive's value overrides
|
||||||
|
// the widget value during execution. Update the Primitive
|
||||||
|
// directly so the change actually takes effect.
|
||||||
|
if (widgetName) {
|
||||||
|
const primitiveNode = tryResolvePrimitiveConnection(node, widgetName);
|
||||||
|
if (primitiveNode) {
|
||||||
|
const primWidget = primitiveNode.widgets?.[0];
|
||||||
|
if (primWidget) {
|
||||||
|
let primNewValue = value;
|
||||||
|
if (mode === "append") {
|
||||||
|
const sep =
|
||||||
|
primWidget.value && primWidget.value.length > 0
|
||||||
|
? " "
|
||||||
|
: "";
|
||||||
|
primNewValue = primWidget.value + sep + value;
|
||||||
|
}
|
||||||
|
primWidget.value = primNewValue;
|
||||||
|
if (
|
||||||
|
Array.isArray(primitiveNode.widgets_values) &&
|
||||||
|
primitiveNode.widgets_values.length > 0
|
||||||
|
) {
|
||||||
|
primitiveNode.widgets_values[0] = primNewValue;
|
||||||
|
}
|
||||||
|
if (typeof primWidget.callback === "function") {
|
||||||
|
try {
|
||||||
|
primWidget.callback(primNewValue);
|
||||||
|
} catch (callbackError) {
|
||||||
|
console.error(
|
||||||
|
"LoRA Manager: primitive widget callback failed",
|
||||||
|
callbackError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof primitiveNode.setDirtyCanvas === "function") {
|
||||||
|
primitiveNode.setDirtyCanvas(true);
|
||||||
|
}
|
||||||
|
if (typeof app.graph?.setDirtyCanvas === "function") {
|
||||||
|
app.graph.setDirtyCanvas(true, true);
|
||||||
|
}
|
||||||
|
this.flashWidget(primitiveNode, primWidget);
|
||||||
|
console.debug(
|
||||||
|
"LoRA Manager: redirected widget update to Primitive node %s (id=%d) ← %s = %o",
|
||||||
|
primitiveNode.type,
|
||||||
|
primitiveNode.id,
|
||||||
|
widgetName,
|
||||||
|
primNewValue
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Update widget value ----
|
// ---- Update widget value ----
|
||||||
const widgetIndex = node.widgets.indexOf(targetWidget);
|
const widgetIndex = node.widgets.indexOf(targetWidget);
|
||||||
let newValue = value;
|
let newValue = value;
|
||||||
|
|||||||
Reference in New Issue
Block a user