240 lines
9.1 KiB
JavaScript
240 lines
9.1 KiB
JavaScript
/**
|
||
* 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();
|
||
});
|
||
});
|
||
}
|