Files
electricty00-ComfyUI-Toggle…/js/multi_ref_encode.js
T

240 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ComfyUI-Toggle-Pass 前端扩展
* 支持动态输入/输出槽:
* - DynamicRefLatentInput: 动态 LATENT 输入槽
* - ImageBatcher: 动态 IMAGE 输入槽
* - DynamicRefImageEncode: 动态 IMAGE 输入槽
* - DynamicRefIndependent: 四项同步增删(IMAGE + prompt + cond + latent)
*
* 架构说明:
* 使用 beforeRegisterNodeDef + onConfigure/onNodeCreated 模式,
* 而非 nodeCreated/nodeAdded + setTimeout。
* 原因:nodeCreated/nodeAdded 触发时 widget 值可能尚未从 JSON 恢复,
* setTimeout 无法可靠保证时序,且切页面时可能与 onConfigure 打架。
* onConfigure 在 widget 值恢复之后触发,可安全读取 w.value。
*/
import { app } from "../../scripts/app.js";
const EXT_NAME = "ComfyUI-Toggle-Pass";
const FIXED_WIDTH = 300;
/* ============ 通用节点(DynamicRefLatentInput / ImageBatcher / DynamicRefImageEncode)============ */
const DYNAMIC_NODES = {
"DynamicRefLatentInput": { widget: "num_latents", inputType: "LATENT", max: 10, prefix: "latent" },
"ImageBatcher": { widget: "num_images", inputType: "IMAGE", max: 10, prefix: "image" },
"DynamicRefImageEncode": { widget: "num_images", inputType: "IMAGE", max: 10, prefix: "image" },
};
/**
* 强制固定节点宽度为 FIXED_WIDTH
* 方法:劫持 setSize,以及用 setTimeout 多轮覆盖
*/
function enforceWidth(node) {
// 劫持 setSize
const origSetSize = node.setSize;
node.setSize = function (size) {
if (Array.isArray(size)) {
origSetSize.call(this, [FIXED_WIDTH, size[1]]);
} else {
origSetSize.call(this, size);
}
};
// 立即设置一次
node.setSize([FIXED_WIDTH, node.size[1]]);
// 多轮 setTimeout 覆盖 Vue 渲染后的修改
for (let i = 0; i < 10; i++) {
setTimeout(() => {
if (node.size[0] !== FIXED_WIDTH) {
node.size[0] = FIXED_WIDTH;
app.graph.change();
}
}, i * 100);
}
}
app.registerExtension({
name: EXT_NAME + "-generic",
beforeRegisterNodeDef(nodeType, nodeData, app) {
const cfg = DYNAMIC_NODES[nodeData.name];
if (!cfg) return;
// 劫持 setSize,强制固定宽度
const origSetSize = nodeType.prototype.setSize;
nodeType.prototype.setSize = function (size) {
if (Array.isArray(size)) {
size = [FIXED_WIDTH, size[1]];
}
return origSetSize.call(this, size);
};
// computeSize 也返回固定宽度
const origComputeSize = nodeType.prototype.computeSize;
nodeType.prototype.computeSize = function () {
const s = origComputeSize ? origComputeSize.call(this) : [FIXED_WIDTH, 100];
return [FIXED_WIDTH, Array.isArray(s) ? s[1] : s];
};
// onConfigure:从 JSON 恢复后触发,widget 值已正确
const origConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
origConfigure?.apply(this, arguments);
requestAnimationFrame(() => {
const w = this.widgets?.find(w => w.name === cfg.widget);
if (w) syncInputs(this, cfg, Math.max(1, Math.min(cfg.max, w.value || 1)));
enforceWidth(this);
});
};
// onNodeCreated:新拖入的节点(无 JSON 恢复),用默认值初始化
const origCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origCreated?.apply(this, arguments);
if (this._tpInit) return;
this._tpInit = true;
this.setSize([FIXED_WIDTH, this.computeSize()[1]]);
enforceWidth(this);
const w = this.widgets?.find(w => w.name === cfg.widget);
if (!w) return;
syncInputs(this, cfg, Math.max(1, Math.min(cfg.max, w.value || 1)));
const prev = w.callback;
w.callback = function (v) {
if (prev) prev.call(this, v);
syncInputs(this._node || this, cfg, Math.max(1, Math.min(cfg.max, v || 1)));
enforceWidth(this._node || this);
};
};
},
});
function syncInputs(node, cfg, target) {
const inputs = node.inputs || [];
let count = 0;
for (const inp of inputs) if (inp.type === cfg.inputType) count++;
if (count === target) { enforceWidth(node); return; }
if (count < target) {
for (let i = count + 1; i <= target; i++) node.addInput(`${cfg.prefix}${i}`, cfg.inputType);
} else {
const idx = [];
for (let i = 0; i < inputs.length; i++) if (inputs[i].type === cfg.inputType) idx.push(i);
for (let i = 0; i < count - target; i++) node.removeInput(idx[idx.length - 1 - i]);
}
requestAnimationFrame(() => {
requestAnimationFrame(() => {
node.setSize([FIXED_WIDTH, node.computeSize()[1]]);
enforceWidth(node);
app.graph.change();
});
});
}
/* ============ Ref Independent(四项同步增删)============ */
app.registerExtension({
name: EXT_NAME + "-refind",
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "DynamicRefIndependent") return;
// 劫持 setSize,强制固定宽度
const origSetSize = nodeType.prototype.setSize;
nodeType.prototype.setSize = function (size) {
if (Array.isArray(size)) {
size = [FIXED_WIDTH, size[1]];
}
return origSetSize.call(this, size);
};
// computeSize 也返回固定宽度
const origComputeSize = nodeType.prototype.computeSize;
nodeType.prototype.computeSize = function () {
const s = origComputeSize ? origComputeSize.call(this) : [FIXED_WIDTH, 100];
return [FIXED_WIDTH, Array.isArray(s) ? s[1] : s];
};
// onConfigure:从 JSON 恢复后触发
const origConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
origConfigure?.apply(this, arguments);
requestAnimationFrame(() => {
const w = this.widgets?.find(w => w.name === "num_images");
if (!w) return;
const n = Math.max(1, Math.min(10, w.value || 1));
applyRefInd(this, n);
enforceWidth(this);
});
};
// onNodeCreated:新拖入的节点(首次创建,无 JSON 恢复)
const origCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origCreated?.apply(this, arguments);
if (this._tpRefInd) return;
this._tpRefInd = true;
this.setSize([FIXED_WIDTH, this.computeSize()[1]]);
enforceWidth(this);
const w = this.widgets?.find(w => w.name === "num_images");
if (!w) return;
const n = Math.max(1, Math.min(10, w.value || 1));
applyRefInd(this, n);
const prev = w.callback;
w.callback = function (v) {
if (prev) prev.call(this, v);
applyRefInd(this._node || this, Math.max(1, Math.min(10, v || 1)));
enforceWidth(this._node || this);
};
};
},
});
/**
* Ref Independent:同步增删
* 1. IMAGE 输入槽:image1 ~ imageN(增删)
* 2. prompt 控件:prompt1 ~ promptN(hidden 显示/隐藏)
* 3. 输出槽:cond_1, latent1, cond_2, latent2, ..., cond_N, latentN(增删,交替排列)
*/
function applyRefInd(node, n) {
// ---- 1. 增删 IMAGE 输入槽 ----
const inputs = node.inputs || [];
let imgCount = 0;
for (const inp of inputs) if (inp.type === "IMAGE") imgCount++;
if (imgCount < n) {
for (let i = imgCount + 1; i <= n; i++) node.addInput(`image${i}`, "IMAGE");
} else if (imgCount > n) {
const idx = [];
for (let i = 0; i < inputs.length; i++) if (inputs[i].type === "IMAGE") idx.push(i);
for (let i = 0; i < imgCount - n; i++) node.removeInput(idx[idx.length - 1 - i]);
}
// ---- 2. 隐藏/显示 prompt 控件 ----
for (const w of (node.widgets || [])) {
if (w.name && w.name.startsWith("prompt")) {
const num = parseInt(w.name.replace("prompt", ""), 10);
w.hidden = num > n;
}
}
// ---- 3. 调整输出槽 ----
const expectedCount = n * 2;
while (node.outputs && node.outputs.length > expectedCount) {
node.removeOutput(node.outputs.length - 1);
}
while (node.outputs && node.outputs.length < expectedCount) {
const i = Math.floor(node.outputs.length / 2) + 1;
if (node.outputs.length % 2 === 0) {
node.addOutput(`cond_${i}`, "CONDITIONING");
} else {
node.addOutput(`latent${i}`, "LATENT");
}
}
requestAnimationFrame(() => {
requestAnimationFrame(() => {
node.setSize([FIXED_WIDTH, node.computeSize()[1]]);
enforceWidth(node);
app.graph.change();
});
});
}