472 lines
16 KiB
JavaScript
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);
|
|
};
|
|
}
|
|
}
|