Files

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);
};
},
});