- js/broadcast_toggle.js: per-node global broadcast toggle, drawn on the node top row next to the toggle switch (state kept in node.properties.tp_broadcast, serialize:false so old workflows are untouched) - js/selection_rect_fix.js: work around a clipped .graph-canvas-panel so the drag-select rectangle follows the pointer (opt out via localStorage.tp_selrect_fix=off) - js/canvas_menu.js, js/sampler_widgets.js, sampler.py: ship modules that were referenced but missing from the repo - multi_ref_encode.py, js/multi_ref_encode.js, unified_loader.py, __init__.py: multi-reference encoding rework plus Krea2 CLIPType support - example_workflows: rename Toggle-Pass-Test.json to 'All-in-One Demo.json', drop the three obsolete preview PNGs - README: bilingual docs, example workflow table, changelog
169 lines
6.0 KiB
JavaScript
169 lines
6.0 KiB
JavaScript
/**
|
||
* Privacy Toggle - 隐私遮罩(Toggle-Pass 图片节点)
|
||
*
|
||
* 适用于 SaveLoadImage、ToggleLoadImage、TogglePreviewImage
|
||
* 核心思路:用 Object.defineProperty 在实例上拦截 imgs 的读写
|
||
* 隐私开启时,任何代码读 this.imgs 都返回 [],写操作保存到 _hiddenImgs
|
||
* 标题栏按钮切换,不影响节点尺寸
|
||
*
|
||
* 按钮含义:
|
||
* 👁 = 显示图片(隐私关闭)
|
||
* 🫣 = 隐藏图片(隐私开启)
|
||
*/
|
||
|
||
import { app } from "../../scripts/app.js";
|
||
|
||
const PRIVACY_KEY = "privacy_hidden";
|
||
const TARGET_NODES = ["SaveLoadImage", "ToggleLoadImage", "TogglePreviewImage"];
|
||
const BTN_SIZE = 14;
|
||
const BTN_MARGIN = 6;
|
||
|
||
/** 开启隐私:拦截实例的 imgs 属性 */
|
||
function enablePrivacy(node) {
|
||
// 保存真实图片
|
||
if (node.imgs && node.imgs.length > 0) {
|
||
node._hiddenImgs = node.imgs;
|
||
}
|
||
|
||
// 在实例上定义 imgs,拦截所有读写
|
||
Object.defineProperty(node, 'imgs', {
|
||
get() { return []; },
|
||
set(val) {
|
||
// 写入时保存到 _hiddenImgs,但不让外部读到
|
||
if (val && val.length > 0) {
|
||
this._hiddenImgs = val;
|
||
}
|
||
},
|
||
configurable: true,
|
||
enumerable: true,
|
||
});
|
||
|
||
node[PRIVACY_KEY] = true;
|
||
}
|
||
|
||
/** 关闭隐私:删除实例拦截,恢复正常 */
|
||
function disablePrivacy(node) {
|
||
// 删除实例上的拦截,让 imgs 回到正常行为
|
||
delete node.imgs;
|
||
|
||
// 恢复图片
|
||
if (node._hiddenImgs && node._hiddenImgs.length > 0) {
|
||
node.imgs = node._hiddenImgs;
|
||
}
|
||
|
||
node[PRIVACY_KEY] = false;
|
||
node._hiddenImgs = null;
|
||
}
|
||
|
||
app.registerExtension({
|
||
name: "ComfyUI-Toggle-Pass-privacy",
|
||
|
||
beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||
if (!TARGET_NODES.includes(nodeData.name)) return;
|
||
|
||
// ===== 新建节点时兜一个正常尺寸 =====
|
||
// TogglePreviewImage 只有 1 个 IMAGE 输入、没有 widget,computeSize() 只算出
|
||
// 约 26px 高 —— 添加后看起来像被"折叠"成一条。这里仅在「新建」时兜底;
|
||
// 加载已有工作流时 onConfigure 会把用户自己存的 size 恢复回来,不受影响。
|
||
if (nodeData.name === "TogglePreviewImage") {
|
||
const origCreated = nodeType.prototype.onNodeCreated;
|
||
nodeType.prototype.onNodeCreated = function () {
|
||
origCreated?.apply(this, arguments);
|
||
const cs = this.computeSize();
|
||
// 与 Toggle Load Image 的初始尺寸保持一致(实测 270×102)
|
||
const w = Math.max(cs[0], 270);
|
||
const h = Math.max(cs[1], 102);
|
||
const sw = Number(this.size?.[0]);
|
||
const sh = Number(this.size?.[1]);
|
||
if (!Number.isFinite(sw) || !Number.isFinite(sh) || sw < w - 0.5 || sh < h - 0.5) {
|
||
this.setSize([w, h]);
|
||
this.setDirtyCanvas?.(true, true);
|
||
}
|
||
};
|
||
}
|
||
|
||
// ===== 序列化 =====
|
||
const origSerialize = nodeType.prototype.serialize;
|
||
nodeType.prototype.serialize = function () {
|
||
const data = origSerialize ? origSerialize.apply(this, arguments) : {};
|
||
if (this[PRIVACY_KEY]) data[PRIVACY_KEY] = true;
|
||
return data;
|
||
};
|
||
|
||
// ===== 反序列化 =====
|
||
const origConfigure = nodeType.prototype.onConfigure;
|
||
nodeType.prototype.onConfigure = function (info) {
|
||
origConfigure?.apply(this, arguments);
|
||
if (info?.[PRIVACY_KEY]) {
|
||
this[PRIVACY_KEY] = true;
|
||
enablePrivacy(this);
|
||
}
|
||
};
|
||
|
||
// ===== onExecuted:执行后若隐私开启则拦截 =====
|
||
const origExecuted = nodeType.prototype.onExecuted;
|
||
nodeType.prototype.onExecuted = function (message) {
|
||
origExecuted?.apply(this, arguments);
|
||
if (this[PRIVACY_KEY]) {
|
||
// 已经在 enablePrivacy 里拦截了,这里不需要再做任何事
|
||
// 因为 set imgs 会被拦截,图片自动保存到 _hiddenImgs
|
||
} else {
|
||
this._hiddenImgs = [];
|
||
}
|
||
app.graph.change();
|
||
};
|
||
|
||
// ===== onDrawForeground:只负责绘制标题栏图标 =====
|
||
const origDrawFg = nodeType.prototype.onDrawForeground;
|
||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||
origDrawFg?.apply(this, arguments);
|
||
|
||
// ===== 绘制标题栏图标 =====
|
||
const [w] = this.size;
|
||
const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30;
|
||
const icon = this[PRIVACY_KEY] ? "🫣" : "👁";
|
||
|
||
let iconX;
|
||
if (this.flags?.collapsed) {
|
||
const textW = ctx.measureText(this.title || "").width;
|
||
iconX = textW + 30 - BTN_MARGIN;
|
||
} else {
|
||
iconX = w - BTN_MARGIN;
|
||
}
|
||
this._privacyBtnRight = iconX;
|
||
|
||
ctx.save();
|
||
ctx.font = `${BTN_SIZE}px Arial`;
|
||
ctx.textAlign = "right";
|
||
ctx.textBaseline = "middle";
|
||
ctx.fillText(icon, iconX, -titleH / 2);
|
||
ctx.restore();
|
||
};
|
||
|
||
// ===== 点击标题栏图标切换 =====
|
||
const origMouseDown = nodeType.prototype.onMouseDown;
|
||
nodeType.prototype.onMouseDown = function (e, pos, canvas) {
|
||
const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30;
|
||
const iconRight = this._privacyBtnRight || this.size[0];
|
||
|
||
if (
|
||
pos[1] >= -titleH &&
|
||
pos[1] <= 0 &&
|
||
pos[0] >= iconRight - 28 &&
|
||
pos[0] <= iconRight
|
||
) {
|
||
if (this[PRIVACY_KEY]) {
|
||
disablePrivacy(this);
|
||
} else {
|
||
enablePrivacy(this);
|
||
}
|
||
this.setDirtyCanvas(true, true);
|
||
app.graph.change();
|
||
return true;
|
||
}
|
||
|
||
return origMouseDown?.apply(this, arguments);
|
||
};
|
||
},
|
||
});
|