Files
electricty00 2c1b08a1cb feat: add Qwen Layered mode to DynamicRefImageEncode, add remove LoRA button
- DynamicRefImageEncode: add "Qwen Layered" model type with layers parameter
- Latent output matches EmptyQwenImageLayeredLatentImage format ([1,16,layers+1,h/8,w/8])
- ToggleUnifiedLoader: add "- 移除 LoRA" button to remove LoRA slots
- README: update node docs and changelog 2026-05-12
2026-05-12 21:07:54 +08:00

349 lines
14 KiB
JavaScript
Raw Permalink 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)
*/
import { app } from "../../scripts/app.js";
const EXT_NAME = "ComfyUI-Toggle-Pass";
/* ============ 通用节点 ============ */
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" },
};
app.registerExtension({
name: EXT_NAME + "-generic",
beforeRegisterNodeDef(nodeType, nodeData, app) {
const cfg = DYNAMIC_NODES[nodeData.name];
if (!cfg) return;
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)));
// 恢复「图层数」显隐状态
if (nodeData.name === "DynamicRefImageEncode") {
const modelW = this.widgets?.find(w => w.name === "模型类型");
const layerW = this.widgets?.find(w => w.name === "图层数");
if (modelW && layerW) {
layerW.hidden = modelW.value !== "Qwen Layered";
}
}
});
};
const origCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origCreated?.apply(this, arguments);
if (this._tpInit) return;
this._tpInit = true;
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)));
};
// 为 DynamicRefImageEncode 设置最小尺寸 + 模型类型联动
if (nodeData.name === "DynamicRefImageEncode") {
this._tpMinSize = [270, 310];
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this.setSize([270, 310]);
app.graph.change();
});
});
// 监听「模型类型」下拉,控制「图层数」显隐
const modelW = this.widgets?.find(w => w.name === "模型类型");
const layerW = this.widgets?.find(w => w.name === "图层数");
if (modelW && layerW) {
layerW.hidden = modelW.value !== "Qwen Layered";
if (!modelW._tpBound) {
modelW._tpBound = true;
const prevModel = modelW.callback;
modelW.callback = function (v) {
if (prevModel) prevModel.call(this, v);
layerW.hidden = v !== "Qwen Layered";
// 用最小尺寸约束刷新
const s = (this._node || this).computeSize();
const min = (this._node || this)._tpMinSize || [0, 0];
(this._node || this).setSize([Math.max(s[0], min[0]), Math.max(s[1], min[1])]);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const s2 = (this._node || this).computeSize();
(this._node || this).setSize([Math.max(s2[0], min[0]), Math.max(s2[1], min[1])]);
app.graph.change();
});
});
};
}
}
}
};
},
});
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) 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(() => {
// 用最小尺寸约束,而非固定尺寸
const s = node.computeSize();
const min = node._tpMinSize || [0, 0];
node.setSize([Math.max(s[0], min[0]), Math.max(s[1], min[1])]);
app.graph.change();
});
});
}
/* ============ Ref Independent ============ */
app.registerExtension({
name: EXT_NAME + "-refind",
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "DynamicRefIndependent") return;
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;
applyRefInd(this, Math.max(1, Math.min(10, w.value || 1)));
});
};
const origCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origCreated?.apply(this, arguments);
if (this._tpRefInd) return;
this._tpRefInd = true;
const w = this.widgets?.find(w => w.name === "num_images");
if (!w) return;
applyRefInd(this, Math.max(1, Math.min(10, w.value || 1)));
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)));
};
// 为 DynamicRefIndependent 设置最小尺寸(宽270, 高310)
this._tpMinSize = [270, 310];
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this.setSize([270, 310]);
app.graph.change();
});
});
};
},
});
function applyRefInd(node, n) {
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]);
}
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;
}
}
const expected = n * 2;
while (node.outputs && node.outputs.length > expected) {
node.removeOutput(node.outputs.length - 1);
}
while (node.outputs && node.outputs.length < expected) {
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");
}
}
// 用最小尺寸约束,而非固定尺寸
const min = node._tpMinSize || [0, 0];
const s = node.computeSize();
node.setSize([Math.max(s[0], min[0]), Math.max(s[1], min[1])]);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const s2 = node.computeSize();
node.setSize([Math.max(s2[0], min[0]), Math.max(s2[1], min[1])]);
app.graph.change();
});
});
}
/* ============ Unified Loader(按钮逐个添加 LoRA)============ */
app.registerExtension({
name: EXT_NAME + "-unified",
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "ToggleUnifiedLoader") return;
const MAX = 10;
function getGroup(node, i) {
const s = String(i);
return {
name: node.widgets.find(w => w.name === `lora_name_${s}`),
model: node.widgets.find(w => w.name === `lora_strength_model_${s}`),
clip: node.widgets.find(w => w.name === `lora_strength_clip_${s}`),
};
}
function refreshSize(node) {
node.setSize(node.computeSize());
requestAnimationFrame(() => {
requestAnimationFrame(() => {
node.setSize(node.computeSize());
app.graph.change();
});
});
}
function showGroup(node, i) {
const g = getGroup(node, i);
if (!g.name) return;
g.name.hidden = false;
const active = g.name.value !== "none";
if (g.model) g.model.hidden = !active;
if (g.clip) g.clip.hidden = !active;
if (!g.name._tpBound) {
g.name._tpBound = true;
const prev = g.name.callback;
g.name.callback = function (v) {
if (prev) prev.call(this, v);
const a = v !== "none";
if (g.model) g.model.hidden = !a;
if (g.clip) g.clip.hidden = !a;
refreshSize(node);
};
}
refreshSize(node);
}
function hideGroup(node, i) {
const g = getGroup(node, i);
if (!g.name) return;
g.name.hidden = true;
if (g.model) g.model.hidden = true;
if (g.clip) g.clip.hidden = true;
}
function countVisible(node) {
let c = 0;
for (let i = 1; i <= MAX; i++) {
const g = getGroup(node, i);
if (g.name && !g.name.hidden) c++;
}
return c;
}
const origConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
origConfigure?.apply(this, arguments);
requestAnimationFrame(() => {
// 隐藏全部
for (let i = 1; i <= MAX; i++) hideGroup(this, i);
// 恢复:maxActive 之前的所有组都显示
let maxActive = 0;
for (let i = 1; i <= MAX; i++) {
const g = getGroup(this, i);
if (g.name && g.name.value !== "none") maxActive = i;
}
for (let i = 1; i <= maxActive; i++) showGroup(this, i);
// 更新按钮文字
const addBtn = this.widgets?.find(w => w.name === "_tp_add_lora_btn");
const rmBtn = this.widgets?.find(w => w.name === "_tp_rm_lora_btn");
const visible = countVisible(this);
if (addBtn) addBtn.label = visible >= MAX ? "(已到上限 10)" : "+ 添加 LoRA";
if (rmBtn) rmBtn.label = visible <= 0 ? "(没有可移除的)" : "- 移除 LoRA";
});
};
const origCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origCreated?.apply(this, arguments);
if (this._tpUnified) return;
this._tpUnified = true;
const node = this; // 闭包捕获,确保回调里 this 正确
// 初始隐藏全部 LoRA 组
for (let i = 1; i <= MAX; i++) hideGroup(node, i);
// 添加按钮 widget
const btn = node.addWidget("button", "+ 添加 LoRA", null, () => {
for (let i = 1; i <= MAX; i++) {
const g = getGroup(node, i);
if (g.name && g.name.hidden) {
showGroup(node, i);
break;
}
}
updateLabels(node);
});
btn.name = "_tp_add_lora_btn";
btn.label = "+ 添加 LoRA";
// 移除按钮 widget
const rmBtn = node.addWidget("button", "- 移除 LoRA", null, () => {
// 从最后一组开始移除
for (let i = MAX; i >= 1; i--) {
const g = getGroup(node, i);
if (g.name && !g.name.hidden) {
hideGroup(node, i);
g.name.value = "none"; // 重置选择
if (g.model) g.model.value = 0;
if (g.clip) g.clip.value = 0;
break;
}
}
updateLabels(node);
});
rmBtn.name = "_tp_rm_lora_btn";
rmBtn.label = "- 移除 LoRA";
function updateLabels(n) {
const add = n.widgets?.find(w => w.name === "_tp_add_lora_btn");
const rm = n.widgets?.find(w => w.name === "_tp_rm_lora_btn");
const visible = countVisible(n);
if (add) add.label = visible >= MAX ? "(已到上限 10)" : "+ 添加 LoRA";
if (rm) rm.label = visible <= 0 ? "(没有可移除的)" : "- 移除 LoRA";
}
// 设置初始尺寸 宽270,高220
node.setSize([270, 220]);
app.graph.change();
};
},
});