248 lines
7.6 KiB
JavaScript
248 lines
7.6 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
const PATTERN_SIZE = 40;
|
|
const PATTERN_LINE_WIDTH = 10;
|
|
|
|
let spoilerPattern = null;
|
|
|
|
/**
|
|
* Checks if the given mouse position (in graph coordinates) is within the node bounds.
|
|
* Used as a fallback when DOM widgets intercept mouse events and `node_over` becomes unreliable.
|
|
*/
|
|
function isMouseOverNode(mousePos, node) {
|
|
if (!mousePos) return false;
|
|
const [mx, my] = mousePos;
|
|
const [nx, ny] = node.pos;
|
|
const [nw, nh] = node.size;
|
|
return mx >= nx && mx <= nx + nw && my >= ny && my <= ny + nh;
|
|
}
|
|
|
|
/**
|
|
* Creates (or returns cached) a diagonal-stripe pattern used for the spoiler overlay.
|
|
*/
|
|
function createSpoilerPattern(ctx) {
|
|
if (spoilerPattern) return spoilerPattern;
|
|
|
|
const c = document.createElement("canvas");
|
|
c.width = PATTERN_SIZE;
|
|
c.height = PATTERN_SIZE;
|
|
const cx = c.getContext("2d");
|
|
|
|
cx.fillStyle = "#1e1e1e";
|
|
cx.fillRect(0, 0, PATTERN_SIZE, PATTERN_SIZE);
|
|
cx.strokeStyle = "#2a2a2a";
|
|
cx.lineWidth = PATTERN_LINE_WIDTH;
|
|
cx.beginPath();
|
|
cx.moveTo(-10, 50); cx.lineTo(50, -10);
|
|
cx.moveTo(-10, 10); cx.lineTo(10, -10);
|
|
cx.moveTo(30, 50); cx.lineTo(50, 30);
|
|
cx.stroke();
|
|
|
|
spoilerPattern = ctx.createPattern(c, "repeat");
|
|
return spoilerPattern;
|
|
}
|
|
|
|
/**
|
|
* Hides DOM-based widget elements by setting CSS opacity and pointer-events.
|
|
*/
|
|
function hideDomWidgets(widgets, hidden) {
|
|
if (!widgets) return;
|
|
for (const w of widgets) {
|
|
const el = w.element ?? w.inputEl ?? w.textareaEl ?? w.options?.element;
|
|
if (el?.style) {
|
|
// Если скрываем - ставим "0"/"none". Иначе очищаем, чтобы ComfyUI сам управлял видимостью.
|
|
el.style.opacity = hidden ? "0" : "";
|
|
el.style.pointerEvents = hidden ? "none" : "";
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Identifies canvas-drawn image preview widgets and temporarily replaces their
|
|
* `drawWidget` method with a no-op to prevent image/video rendering on the canvas.
|
|
* Returns a restore function.
|
|
*/
|
|
function suppressCanvasPreviews(widgets, suppressed) {
|
|
if (!widgets || !suppressed) return () => {};
|
|
|
|
const saved = [];
|
|
for (const w of widgets) {
|
|
const isImagePreview =
|
|
w.name === "$$canvas-image-preview" ||
|
|
w.type === "image-preview" ||
|
|
typeof w.renderPreview === "function";
|
|
|
|
if (isImagePreview && typeof w.drawWidget === "function") {
|
|
saved.push({ widget: w, original: w.drawWidget });
|
|
w.drawWidget = () => {};
|
|
}
|
|
}
|
|
|
|
return () => {
|
|
for (const { widget, original } of saved) {
|
|
widget.drawWidget = original;
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Draws the spoiler overlay covering everything below the visible slots.
|
|
*/
|
|
function drawSpoilerOverlay(node, ctx) {
|
|
const [w, h] = node.size;
|
|
let startY = -1;
|
|
|
|
// Primary method: Find the exact Y coordinate where the first widget is drawn
|
|
if (node.widgets && node.widgets.length > 0) {
|
|
for (const widget of node.widgets) {
|
|
if (typeof widget.last_y === "number" && widget.last_y >= 0) {
|
|
// Shift up by a tiny amount to cover the padding gap perfectly
|
|
startY = Math.max(0, widget.last_y - 2);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback method: Calculate based strictly on visible slots
|
|
if (startY === -1) {
|
|
let visibleInputs = 0;
|
|
if (node.inputs) {
|
|
for (const inp of node.inputs) {
|
|
if (!inp.hidden) visibleInputs++;
|
|
}
|
|
}
|
|
|
|
let visibleOutputs = 0;
|
|
if (node.outputs) {
|
|
for (const out of node.outputs) {
|
|
if (!out.hidden) visibleOutputs++;
|
|
}
|
|
}
|
|
|
|
const slotHeight = LiteGraph?.NODE_SLOT_HEIGHT ?? 20;
|
|
startY = Math.max(visibleInputs, visibleOutputs) * slotHeight;
|
|
|
|
if (startY > 0) {
|
|
startY += 4;
|
|
}
|
|
}
|
|
|
|
if (startY >= h) {
|
|
startY = Math.max(0, h - 10);
|
|
}
|
|
|
|
const spoilerHeight = h - startY;
|
|
if (spoilerHeight <= 0) return;
|
|
|
|
ctx.save();
|
|
|
|
// Fill background
|
|
ctx.fillStyle = createSpoilerPattern(ctx);
|
|
ctx.beginPath();
|
|
|
|
if (ctx.roundRect) {
|
|
ctx.roundRect(0, startY, w, spoilerHeight, [0, 0, 8, 8]);
|
|
} else {
|
|
ctx.rect(0, startY, w, spoilerHeight);
|
|
}
|
|
ctx.fill();
|
|
|
|
// Outline
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeStyle = "#000000";
|
|
ctx.stroke();
|
|
|
|
// Labels
|
|
const centerY = startY + (spoilerHeight / 2);
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.font = "bold 20px Arial";
|
|
|
|
if (spoilerHeight > 50) {
|
|
ctx.fillText("SPOILER", w / 2, centerY - 10);
|
|
ctx.fillStyle = "#aaaaaa";
|
|
ctx.font = "12px Arial";
|
|
ctx.fillText("Hover to reveal", w / 2, centerY + 15);
|
|
} else if (spoilerHeight > 20) {
|
|
ctx.fillText("SPOILER", w / 2, centerY);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "Comfy.UniversalSpoiler",
|
|
|
|
/**
|
|
* Adds the spoiler toggle to every node's context menu.
|
|
* This hook is natively supported by ComfyUI and runs for all nodes,
|
|
* including custom nodes, group nodes, and subgraphs.
|
|
*/
|
|
getNodeMenuItems(node) {
|
|
const isHidden = !!node.properties?.spoiler_hidden;
|
|
return [
|
|
{
|
|
content: isHidden ? "🔓 Remove spoiler" : "🔒 Add spoiler",
|
|
callback: () => {
|
|
node.properties = node.properties || {};
|
|
node.properties.spoiler_hidden = !node.properties.spoiler_hidden;
|
|
app.canvas?.setDirty(true, true);
|
|
},
|
|
}
|
|
];
|
|
},
|
|
|
|
/**
|
|
* Overrides the canvas drawNode method to inject spoiler behavior.
|
|
*/
|
|
setup() {
|
|
const origDrawNode = app.canvas.drawNode;
|
|
|
|
app.canvas.drawNode = function (node, ctx) {
|
|
const isHidden = !!node.properties?.spoiler_hidden;
|
|
const isCollapsed = !!node.flags?.collapsed; // <-- Проверяем, свернута ли нода
|
|
const isHovered =
|
|
this.node_over === node || isMouseOverNode(this.graph_mouse, node);
|
|
|
|
// Скрываем контент только если включен спойлер, нет ховера и нода НЕ свернута
|
|
const shouldHide = isHidden && !isHovered && !isCollapsed;
|
|
|
|
// Hide DOM widget elements
|
|
hideDomWidgets(node.widgets, shouldHide);
|
|
|
|
// Suppress canvas-drawn image/video previews
|
|
const restorePreviews = suppressCanvasPreviews(node.widgets, shouldHide);
|
|
|
|
// Draw the original node (ports, slots, widgets, etc.)
|
|
origDrawNode.apply(this, arguments);
|
|
|
|
// Restore canvas previews
|
|
restorePreviews();
|
|
|
|
// Draw spoiler overlay on top
|
|
if (shouldHide) {
|
|
drawSpoilerOverlay(node, ctx);
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Triggers a canvas redraw when the mouse enters or leaves a spoiler-hidden node.
|
|
*/
|
|
nodeCreated(node) {
|
|
const origOnMouseEnter = node.onMouseEnter;
|
|
node.onMouseEnter = function (...args) {
|
|
origOnMouseEnter?.apply(this, args);
|
|
if (this.properties?.spoiler_hidden) app.canvas.setDirty(true, false);
|
|
};
|
|
|
|
const origOnMouseLeave = node.onMouseLeave;
|
|
node.onMouseLeave = function (...args) {
|
|
origOnMouseLeave?.apply(this, args);
|
|
if (this.properties?.spoiler_hidden) app.canvas.setDirty(true, false);
|
|
};
|
|
},
|
|
});
|