Files

472 lines
16 KiB
JavaScript

import { app } from "../../scripts/app.js";
// CONSTANTS
const GLOBAL_HIDE_SETTING_ID = "SATA_UtilityNode.GlobalPreviewHide";
const PREVIEW_MACHINE_CLASS = "Preview_Machine";
const SAVE_MACHINE_CLASS = "Save_Machine";
// Track hover state
const hoveredNodeIds = new Set();
// Inject stylesheet for Nodes 2.0 (Vue DOM) and Legacy V1 DOM preview masking
function injectHiderCSS() {
if (document.getElementById("sata-preview-hider-styles")) return;
const style = document.createElement("style");
style.id = "sata-preview-hider-styles";
style.textContent = `
/* Mask only the image container itself in-place */
.sata-image-masked {
position: relative !important;
min-height: 100px !important;
background-color: #1a1a1a !important;
border-radius: 6px !important;
border: 1px dashed #444 !important;
box-sizing: border-box !important;
overflow: hidden !important;
transition: background-color 0.15s ease;
}
/* Hide all img/canvas/video children inside the masked image container */
.sata-image-masked img,
.sata-image-masked canvas,
.sata-image-masked video {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
transition: opacity 0.15s ease;
}
/* Center placeholder text cleanly over the masked image container */
.sata-image-masked::after {
content: "Hidden (Hover to Reveal)" !important;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
color: #888 !important;
font-size: 13px !important;
font-weight: bold !important;
font-family: sans-serif !important;
text-align: center !important;
white-space: nowrap !important;
pointer-events: none !important;
opacity: 1 !important;
visibility: visible !important;
z-index: 10 !important;
}
/* NATIVE CSS HOVER REVEAL: Works across Nodes 2.0 Vue Nodes & Legacy LiteGraph DOM */
.sata-image-masked:hover img,
.sata-image-masked:hover canvas,
.sata-image-masked:hover video,
[data-node-id]:hover .sata-image-masked img,
[data-node-id]:hover .sata-image-masked canvas,
[data-node-id]:hover .sata-image-masked video,
.comfy-vue-node:hover .sata-image-masked img,
.comfy-vue-node:hover .sata-image-masked canvas,
.comfy-vue-node:hover .sata-image-masked video,
.node:hover .sata-image-masked img,
.node:hover .sata-image-masked canvas,
.node:hover .sata-image-masked video,
.lgraphnode:hover .sata-image-masked img,
.lgraphnode:hover .sata-image-masked canvas,
.lgraphnode:hover .sata-image-masked video {
opacity: 1 !important;
visibility: visible !important;
pointer-events: auto !important;
}
.sata-image-masked:hover::after,
[data-node-id]:hover .sata-image-masked::after,
.comfy-vue-node:hover .sata-image-masked::after,
.node:hover .sata-image-masked::after,
.lgraphnode:hover .sata-image-masked::after {
display: none !important;
opacity: 0 !important;
visibility: hidden !important;
}
.sata-image-masked:hover,
[data-node-id]:hover .sata-image-masked,
.comfy-vue-node:hover .sata-image-masked,
.node:hover .sata-image-masked,
.lgraphnode:hover .sata-image-masked {
background-color: transparent !important;
border: none !important;
}
`;
document.head.appendChild(style);
}
// Helper to get settings for both ComfyUI V1 and V2
function getSettingValue(id, defaultValue) {
if (app.extensionManager && app.extensionManager.setting) {
const val = app.extensionManager.setting.get(id);
return val !== undefined ? val : defaultValue;
} else if (app.ui && app.ui.settings) {
return app.ui.settings.getSettingValue(id, defaultValue);
}
return defaultValue;
}
app.registerExtension({
name: "SATA_UtilityNode.Preview_Machine",
settings: [
{
id: GLOBAL_HIDE_SETTING_ID,
name: "SATA Utility: Global Hide Previews",
type: "boolean",
defaultValue: false,
onChange: (value) => {
if (app.graph) app.graph.setDirtyCanvas(true, true);
}
}
],
async setup() {
injectHiderCSS();
setupGlobalMouseTracker();
},
async nodeCreated(node) {
if (isTargetNode(node)) {
setupNodeHider(node);
}
},
async loadedGraphNode(node) {
if (isTargetNode(node)) {
setupNodeHider(node);
}
}
});
function isTargetNode(node) {
if (!node || !node.comfyClass) return false;
return node.comfyClass === "PreviewImage" ||
node.comfyClass === "SaveImage" ||
node.comfyClass === PREVIEW_MACHINE_CLASS ||
node.comfyClass === SAVE_MACHINE_CLASS;
}
function shouldHideNode(node) {
const globalHide = getSettingValue(GLOBAL_HIDE_SETTING_ID, false);
if (globalHide) return true;
const hideWidget = node.widgets?.find(w => w.name === "hide_preview");
if (hideWidget) {
return !!hideWidget.value;
}
if (node.comfyClass === PREVIEW_MACHINE_CLASS || node.comfyClass === SAVE_MACHINE_CLASS) {
return true;
}
return false;
}
function isMouseOverNode(node) {
if (!node || !node.pos || !node.size) return false;
if (app.canvas && app.canvas.node_over === node) return true;
if (!app.canvas || !app.canvas.graph_mouse) return false;
const mouse = app.canvas.graph_mouse;
const x = mouse[0];
const y = mouse[1];
return x >= node.pos[0] &&
x <= (node.pos[0] + node.size[0]) &&
y >= node.pos[1] &&
y <= (node.pos[1] + node.size[1]);
}
function isNodeHovered(node) {
if (!node) return false;
if (app.canvas && app.canvas.node_over === node) return true;
if (hoveredNodeIds.has(node.id)) return true;
return isMouseOverNode(node);
}
function shouldHideNow(node) {
const shouldHide = shouldHideNode(node);
const hovered = isNodeHovered(node);
return shouldHide && !hovered;
}
function getNodeDOMElement(node) {
if (!node) return null;
if (node.DOMelement) return node.DOMelement;
if (node.element) return node.element;
const id = String(node.id);
return document.querySelector(`[data-node-id="${id}"]`) ||
document.querySelector(`[data-id="${id}"]`) ||
document.getElementById(`node-${id}`) ||
document.querySelector(`.node-${id}`) ||
document.querySelector(`.comfy-vue-node[data-id="${id}"]`);
}
function hasImages(node) {
if (!node) return false;
if (node.imgs && node.imgs.length > 0) return true;
const el = getNodeDOMElement(node);
if (el) {
const img = el.querySelector("img, canvas.comfy-img-preview, .comfy-img-preview, .node-image-preview, .comfy-vue-image-preview");
if (img) return true;
}
if (node.widgets) {
for (const w of node.widgets) {
if (w.type === "image" || w.name === "preview" || w.imageEl || w.element?.querySelector?.("img")) {
return true;
}
}
}
return false;
}
function getImageContainers(node) {
const containers = new Set();
const el = getNodeDOMElement(node);
if (el) {
// Find existing image preview containers inside the node DOM
const previewEls = el.querySelectorAll(
".comfy-img-preview, .node-image-preview, .comfy-vue-image-preview, .node-widget-image, [class*='ImagePreview'], [class*='image-preview'], [class*='node-image']"
);
previewEls.forEach(p => containers.add(p));
// Direct img elements parent wrappers
const imgs = el.querySelectorAll("img, canvas.comfy-img-preview");
imgs.forEach(img => {
if (img.parentElement && img.parentElement !== el) {
containers.add(img.parentElement);
} else {
containers.add(img);
}
});
}
if (node.widgets) {
for (const widget of node.widgets) {
if (widget.element && (widget.type === "image" || widget.name === "preview" || widget.element.querySelector("img"))) {
containers.add(widget.element);
}
if (widget.imageEl) {
containers.add(widget.imageEl.parentElement || widget.imageEl);
}
}
}
return Array.from(containers);
}
function setupGlobalMouseTracker() {
let rafPending = false;
const checkAndUpdate = () => {
if (!app.graph || !app.graph._nodes) return;
let changed = false;
for (const node of app.graph._nodes) {
if (!isTargetNode(node)) continue;
const currentlyHovered = isMouseOverNode(node);
const wasHovered = hoveredNodeIds.has(node.id);
if (currentlyHovered !== wasHovered) {
if (currentlyHovered) {
hoveredNodeIds.add(node.id);
} else {
hoveredNodeIds.delete(node.id);
}
changed = true;
}
syncNodeDOMVisibility(node);
}
if (changed && app.graph) {
app.graph.setDirtyCanvas(true, true);
}
};
window.addEventListener("mousemove", () => {
if (!rafPending) {
rafPending = true;
requestAnimationFrame(() => {
rafPending = false;
checkAndUpdate();
});
}
});
window.addEventListener("mouseout", (e) => {
if (e.relatedTarget === null) {
hoveredNodeIds.clear();
if (app.graph) app.graph.setDirtyCanvas(true, true);
}
});
}
function syncNodeDOMVisibility(node) {
if (!node) return;
const el = getNodeDOMElement(node);
if (el && !el._sata_mouse_bound) {
el._sata_mouse_bound = true;
el.addEventListener("mouseenter", () => {
hoveredNodeIds.add(node.id);
syncNodeDOMVisibility(node);
if (app.graph) app.graph.setDirtyCanvas(true, true);
});
el.addEventListener("mouseleave", () => {
hoveredNodeIds.delete(node.id);
syncNodeDOMVisibility(node);
if (app.graph) app.graph.setDirtyCanvas(true, true);
});
}
// Apply DOM masking when images exist AND hide_preview is active
const nodeHasImages = hasImages(node);
const hide = nodeHasImages && shouldHideNode(node);
const containers = getImageContainers(node);
containers.forEach(container => {
if (hide) {
container.classList.add("sata-image-masked");
} else {
container.classList.remove("sata-image-masked");
}
});
}
function setupNodeHider(node) {
if (node._sata_hider_v12) return;
node._sata_hider_v12 = true;
injectHiderCSS();
const patchWidgets = () => {
if (!node.widgets) return;
for (const widget of node.widgets) {
if (widget._sata_patched) continue;
if (widget.type === "image" || widget.name === "preview") {
if (widget.draw && typeof widget.draw === 'function') {
const origDraw = widget.draw.bind(widget);
widget.draw = function (ctx, nodeRef, widgetWidth, y, widgetHeight) {
const targetNode = nodeRef || node;
if (hasImages(targetNode) && shouldHideNow(targetNode)) {
ctx.save();
ctx.fillStyle = "#1a1a1a";
ctx.fillRect(0, y, widgetWidth, widgetHeight);
ctx.restore();
return;
}
return origDraw(ctx, targetNode, widgetWidth, y, widgetHeight);
};
widget._sata_patched = true;
}
if (widget.drawWidget && typeof widget.drawWidget === 'function') {
const origDrawWidget = widget.drawWidget.bind(widget);
widget.drawWidget = function (...args) {
if (hasImages(node) && shouldHideNow(node)) {
return;
}
return origDrawWidget(...args);
};
widget._sata_patched = true;
}
}
}
syncNodeDOMVisibility(node);
};
patchWidgets();
const origOnDrawBackground = node.onDrawBackground;
node.onDrawBackground = function (ctx) {
patchWidgets();
syncNodeDOMVisibility(node);
if (origOnDrawBackground) {
origOnDrawBackground.apply(this, arguments);
}
};
const origOnDrawForeground = node.onDrawForeground;
node.onDrawForeground = function (ctx) {
if (origOnDrawForeground) {
origOnDrawForeground.apply(this, arguments);
}
syncNodeDOMVisibility(this);
if (hasImages(this) && shouldHideNow(this)) {
// Find where settings widgets end so we only mask the image area below widgets
let startY = 30;
if (this.widgets && this.widgets.length > 0) {
const lastWidget = this.widgets[this.widgets.length - 1];
if (lastWidget.last_y !== undefined) {
const h = (lastWidget.computeSize ? lastWidget.computeSize(this.size[0])[1] : 20) || 20;
startY = lastWidget.last_y + h + 6;
} else {
startY = 30 + this.widgets.length * 24;
}
}
const maskHeight = Math.max(0, this.size[1] - startY);
if (maskHeight > 10) {
ctx.save();
ctx.fillStyle = "#1a1a1a";
ctx.fillRect(4, startY, this.size[0] - 8, maskHeight - 4);
// Fallback for pure LiteGraph 2D Canvas rendering (when no DOM element exists)
const el = getNodeDOMElement(this);
if (!el || !el.querySelector(".sata-image-masked")) {
ctx.fillStyle = "#888";
ctx.font = "bold 13px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Hidden", this.size[0] / 2, startY + maskHeight / 2 - 8);
ctx.font = "10px Arial";
ctx.fillStyle = "#666";
ctx.fillText("(Hover to Reveal)", this.size[0] / 2, startY + maskHeight / 2 + 10);
}
ctx.restore();
}
}
};
const origOnExecuted = node.onExecuted;
node.onExecuted = function (message) {
if (origOnExecuted) {
origOnExecuted.apply(this, arguments);
}
patchWidgets();
syncNodeDOMVisibility(node);
setTimeout(() => syncNodeDOMVisibility(node), 20);
setTimeout(() => syncNodeDOMVisibility(node), 100);
setTimeout(() => syncNodeDOMVisibility(node), 300);
if (app.graph) app.graph.setDirtyCanvas(true, true);
};
const hideWidget = node.widgets?.find(w => w.name === "hide_preview");
if (hideWidget) {
const origCallback = hideWidget.callback;
hideWidget.callback = function (value) {
if (origCallback) origCallback.call(this, value);
syncNodeDOMVisibility(node);
if (app.graph) app.graph.setDirtyCanvas(true, true);
};
}
}