3 Commits
Author SHA1 Message Date
loficoding 990a683ab8 removed no longer needed html file for readme. 2026-01-21 16:49:26 -07:00
loficoding c5bc00e812 Updated README 2026-01-21 16:43:57 -07:00
loficoding 84149c7886 Initial release of Node HUD 2026-01-21 16:34:39 -07:00
6 changed files with 213 additions and 377 deletions
+2 -7
View File
@@ -1,7 +1,2 @@
# Mappings required for ComfyUI to acknowledge the node folder
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
WEB_DIRECTORY = "./web"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
WEB_DIRECTORY = "./web/js"
__all__ = ["WEB_DIRECTORY"]
Binary file not shown.
-36
View File
@@ -1,36 +0,0 @@
[project]
name = "node-hudComfyUI-Node-HUD"
description = "Adds a smart I/O status dock to stacked nodes for cleaner layouts."
version = "1.2.1"
license = {file = "LICENSE"}
# classifiers = [
# # For OS-independent nodes (works on all operating systems)
# "Operating System :: OS Independent",
#
# # OR for OS-specific nodes, specify the supported systems:
# "Operating System :: Microsoft :: Windows", # Windows specific
# "Operating System :: POSIX :: Linux", # Linux specific
# "Operating System :: MacOS", # macOS specific
#
# # GPU Accelerator support. Pick the ones that are supported by your extension.
# "Environment :: GPU :: NVIDIA CUDA", # NVIDIA CUDA support
# "Environment :: GPU :: AMD ROCm", # AMD ROCm support
# "Environment :: GPU :: Intel Arc", # Intel Arc support
# "Environment :: NPU :: Huawei Ascend", # Huawei Ascend support
# "Environment :: GPU :: Apple Metal", # Apple Metal support
# ]
[project.urls]
Repository = "https://github.com/loficoding/ComfyUI-Node-HUD"
# Used by Comfy Registry https://registry.comfy.org
Documentation = "https://github.com/loficoding/ComfyUI-Node-HUD/wiki"
"Bug Tracker" = "https://github.com/loficoding/ComfyUI-Node-HUD/issues"
[tool.comfy]
PublisherId = "loficoding"
DisplayName = "ComfyUI-Node-HUD"
Icon = ""
includes = []
# "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility
-64
View File
@@ -1,64 +0,0 @@
/* main dock container */
.hud-dock {
/* VISUALS ONLY - NO POSITIONS OR MARGINS */
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
box-sizing: border-box;
pointer-events: none;
/* Decoration */
background-color: rgba(20, 20, 20, 0.85);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 8px;
backdrop-filter: blur(4px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.hud-dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.5);
transition: transform 0.1s ease;
cursor: help;
}
.hud-dot:hover {
transform: scale(1.3);
border-color: white;
}
.hud-chip {
display: flex;
align-items: center;
gap: 4px;
background-color: #222;
border: 1px solid #444;
border-radius: 8px;
padding: 2px 6px;
height: 20px;
white-space: nowrap;
color: #ccc;
font-family: Arial, sans-serif;
font-size: 10px;
pointer-events: auto;
}
/* --- New Zoomed Out State --- */
.hud-dock-zoomed-out {
/* When chips are hidden, the container would collapse.
We force it to "1 chip height" (approx 20-24px)
so the user still sees the dock exists.
*/
min-height: 24px;
/* Optional: Make it slightly subtler when zoomed out */
background-color: rgba(20, 20, 20, 0.6);
border-color: rgba(255, 255, 255, 0.1);
}
.hud-dock-zoomed-out .hud-chip {
display: none;
}
+211
View File
@@ -0,0 +1,211 @@
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "Comfy.CleanLayoutDock",
// 1. Toggle Menu
async beforeRegisterNodeDef(nodeType, nodeData, app) {
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function(_, options) {
if (getExtraMenuOptions) getExtraMenuOptions.apply(this, arguments);
options.push({
content: "Toggle Stack Info",
callback: () => {
this.show_stack_info = !this.show_stack_info;
app.graph.setDirtyCanvas(true, true);
}
});
};
// 2. Draw Foreground
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
if (onDrawForeground) onDrawForeground.apply(this, arguments);
if (!this.show_stack_info) return;
const stackList = getHiddenNodes(this, app.graph);
if (stackList.length === 0) return;
ctx.save();
drawDock(this, ctx, stackList, app.graph);
ctx.restore();
};
}
});
// --- HELPER: Find Hidden Nodes ---
function getHiddenNodes(parent, graph) {
const stackList = [];
if (!graph || !graph._nodes) return stackList;
const pLeft = parent.pos[0];
const pRight = parent.pos[0] + parent.size[0];
const pTop = parent.pos[1];
const pBottom = parent.pos[1] + parent.size[1];
for (const node of graph._nodes) {
if (node.id === parent.id) continue;
const nx = node.pos[0];
const ny = node.pos[1];
if (nx >= pLeft && nx < pRight && ny >= pTop && ny < pBottom) {
stackList.push(node);
}
}
return stackList;
}
// --- HELPER: Get Link Color ---
function getLinkColor(link) {
const type = link.type;
const colorMap = LGraphCanvas.link_type_colors;
// 1. Check Comfy Registry first
if (colorMap[type]) return colorMap[type];
// 2. Common Fallbacks
if (type === "STRING") return "#66ff66"; // <--- FIXED: Standard Green
if (type === "INT") return "#22cc22"; // Integer Green
if (type === "FLOAT") return "#22cc22"; // Float Green
if (type === "LATENT") return "#ff99aa";
if (type === "IMAGE") return "#66aaff";
if (type === "MODEL") return "#6666aa";
if (type === "VAE") return "#ff4444";
if (type === "CLIP") return "#ffff66";
if (type === "CONDITIONING") return "#aa6666";
return "#aaaaff"; // The "Lint Blue" Default
}
// --- DRAWER ---
function drawDock(node, ctx, stackList, graph) {
if (!node.size) return;
const dockHeight = 26;
const chipHeight = 16;
const padding = 6;
const dotRadius = 3;
const dotSpacing = 8;
const x = 0;
const y = node.size[1];
/*
// Background
ctx.fillStyle = "#000";
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(x, y, node.size[0], dockHeight, [0, 0, 6, 6]);
else ctx.rect(x, y, node.size[0], dockHeight);
ctx.fill();
*/
// Background (Tinted Glass Style)
ctx.fillStyle = "rgba(20, 20, 20, 0.85)"; // 85% Opaque Dark Grey
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(x, y, node.size[0], dockHeight, [0, 0, 6, 6]);
else ctx.rect(x, y, node.size[0], dockHeight);
ctx.fill();
// Optional: Add a subtle glass border for definition
ctx.strokeStyle = "rgba(255, 255, 255, 0.1)";
ctx.lineWidth = 1;
ctx.stroke();
let currentX = x + padding;
ctx.font = "10px Arial";
ctx.textBaseline = "middle";
for (const item of stackList) {
// 1. Gather Dots
const leftDots = [];
const rightDots = [];
// Check Inputs
if (item.inputs) {
for (const input of item.inputs) {
if (input.link) {
const link = graph.links[input.link];
if (link) leftDots.push(getLinkColor(link));
}
}
}
// Check Outputs
if (item.outputs) {
for (const output of item.outputs) {
if (output.links && output.links.length > 0) {
// Use color of first link
const link = graph.links[output.links[0]];
if (link) rightDots.push(getLinkColor(link));
else if (LGraphCanvas.link_type_colors[output.type]) {
rightDots.push(LGraphCanvas.link_type_colors[output.type]);
} else {
// Manual check for disconnected outputs that usually have colors
if (output.type === "STRING") rightDots.push("#66ff66");
else rightDots.push("#cccccc");
}
}
}
}
// Fallback: Disconnected
if (leftDots.length === 0 && rightDots.length === 0) {
leftDots.push("#ff4444"); // Red error dot
}
// 2. Calculate Size
let text = item.title || item.type;
if (text.length > 20) text = text.substring(0, 18) + "..";
const textWidth = ctx.measureText(text).width;
const leftSpace = 6 + (leftDots.length * dotSpacing);
const rightSpace = 6 + (rightDots.length * dotSpacing);
const chipWidth = leftSpace + textWidth + rightSpace;
if (currentX + chipWidth > node.size[0]) break;
// 3. Determine Border Color
let borderColor = "#444";
if (item.mode === 2) borderColor = "#888888"; // Mute
else if (item.mode === 4) borderColor = "#cc66ff"; // Bypass
const centerY = y + (dockHeight/2);
// 4. Draw Chip
ctx.fillStyle = "#222";
ctx.strokeStyle = borderColor;
ctx.lineWidth = 1;
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(currentX, y + (dockHeight - chipHeight)/2, chipWidth, chipHeight, 8);
else ctx.rect(currentX, y + (dockHeight - chipHeight)/2, chipWidth, chipHeight);
ctx.fill();
ctx.stroke();
// 5. Draw Left Dots
let dotX = currentX + 6;
for (const color of leftDots) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(dotX + dotRadius, centerY, dotRadius, 0, Math.PI * 2);
ctx.fill();
dotX += dotSpacing;
}
// 6. Draw Text
ctx.fillStyle = "#ccc";
ctx.fillText(text, currentX + leftSpace, centerY);
// 7. Draw Right Dots
dotX = currentX + leftSpace + textWidth + 6;
for (const color of rightDots) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(dotX + dotRadius, centerY, dotRadius, 0, Math.PI * 2);
ctx.fill();
dotX += dotSpacing;
}
currentX += chipWidth + padding;
}
}
-270
View File
@@ -1,270 +0,0 @@
import { app } from "../../scripts/app.js";
// 1. Inject CSS
const link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = "extensions/ComfyUI-Node-HUD/css/hud_styles.css";
document.head.appendChild(link);
app.registerExtension({
name: "Comfy.NodeHUD",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// --- A. Toggle Menu Option ---
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function(_, options) {
if (getExtraMenuOptions) getExtraMenuOptions.apply(this, arguments);
options.push({
content: "Toggle Stack Info",
callback: () => {
this.show_stack_info = !this.show_stack_info;
// Bring node to front of the array so it renders on top of everything
if (this.show_stack_info){
const nodes = app.graph._nodes;
const index = nodes.indexOf(this);
if (index > -1){
nodes.splice(index, 1);
nodes.push(this);
}
}
app.graph.setDirtyCanvas(true, true);
}
});
};
// --- B. The Main Loop (Replaces Draw) ---
// We hook into onDrawForeground to handle Position updates
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
if (onDrawForeground) onDrawForeground.apply(this, arguments);
// 1. Cleanup: If toggled off, empty, OR COLLAPSED, remove the dock
const stackList = getHiddenNodes(this, app.graph);
// ADDED: Check !this.flags.collapsed so HUD hides when node is collapsed
const shouldShow = this.show_stack_info && stackList.length > 0 && !this.flags.collapsed;
if (!shouldShow) {
if (this.hudElement) {
this.hudElement.remove();
this.hudElement = null;
}
return;
}
// 2. Update the Dock (Position & Content)
updateDock(this, stackList, app.graph);
// If movement is laggy this ensures the HUD follows mouse during drag
if(this.is_selected){
app.graph.setDirtyCanvas(true, false);
}
};
}
});
// --- HELPER: Find Hidden Nodes ---
function getHiddenNodes(parent, graph) {
const stackList = [];
if (!graph || !graph._nodes) return stackList;
const parentIndex = graph._nodes.indexOf(parent);
const pLeft = parent.pos[0];
const pRight = parent.pos[0] + parent.size[0];
const pTop = parent.pos[1];
const pBottom = parent.pos[1] + parent.size[1];
for (let i = 0; i < graph._nodes.length; i++) {
const node = graph._nodes[i];
if (node.id === parent.id) continue;
// Only include nodes that are "below" the parent in the stack
if (i > parentIndex) continue;
const nx = node.pos[0];
const ny = node.pos[1];
// Check if center of node is inside parent (simple hit test)
if (nx >= pLeft && nx < pRight && ny >= pTop && ny < pBottom) {
stackList.push(node);
}
}
return stackList;
}
// --- HELPER: Get Link Color ---
function getLinkColor(link) {
if (!link) return "#aaaaff";
const type = link.type;
const colorMap = LGraphCanvas.link_type_colors;
if (colorMap[type]) return colorMap[type];
if (type === "STRING") return "#66ff66";
if (type === "INT") return "#22cc22";
if (type === "FLOAT") return "#22cc22";
if (type === "LATENT") return "#ff99aa";
if (type === "IMAGE") return "#66aaff";
if (type === "MODEL") return "#6666aa";
if (type === "VAE") return "#ff4444";
if (type === "CLIP") return "#ffff66";
if (type === "CONDITIONING") return "#aa6666";
return "#aaaaff";
}
// --- THE NEW DOM BUILDER ---
function updateDock(node, stackList, graph) {
// 1. Create Element if missing
if (!node.hudElement) {
node.hudElement = document.createElement("div");
node.hudElement.className = "hud-dock";
// Fix Zooming/Scrolling Issue:
// Intercept wheel events to prevent browser page zoom, but manually trigger canvas zoom.
node.hudElement.addEventListener("wheel", (e) => {
// Stop the event from reaching the window (prevents whole page zoom/scroll)
e.preventDefault();
e.stopPropagation();
// Manually apply zoom to the canvas so the user can still zoom the graph
if (app.canvas && app.canvas.ds) {
// Determine zoom direction and factor (standard LiteGraph logic)
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
app.canvas.ds.changeScale(app.canvas.ds.scale * zoomFactor, [e.clientX, e.clientY]);
app.canvas.setDirty(true, true);
}
}, { passive: false });
// We append to document.body to ensure getBoundingClientRect + scroll math works reliably
document.body.appendChild(node.hudElement);
node.lastStackSignature = "";
}
const dock = node.hudElement;
// ... [Content Update Section remains exactly the same] ...
const currentSignature = stackList.map(n => n.id).join(",");
if (currentSignature !== node.lastStackSignature) {
dock.innerHTML = "";
node.lastStackSignature = currentSignature;
// Build Chips (Keep existing logic)
for (const item of stackList) {
const chip = document.createElement("div");
chip.className = "hud-chip";
if (item.mode === 2) chip.style.borderColor = "#888888";
else if (item.mode === 4) chip.style.borderColor = "#cc66ff";
const leftDots = [];
const rightDots = [];
if (item.inputs) {
for (const input of item.inputs) {
if (input.link) {
const link = graph.links[input.link];
if (link) leftDots.push(getLinkColor(link));
}
}
}
if (item.outputs) {
for (const output of item.outputs) {
if (output.links && output.links.length > 0) {
const link = graph.links[output.links[0]];
if (link) rightDots.push(getLinkColor(link));
else if (LGraphCanvas.link_type_colors[output.type]) {
rightDots.push(LGraphCanvas.link_type_colors[output.type]);
} else {
if (output.type === "STRING") rightDots.push("#66ff66");
else rightDots.push("#cccccc");
}
}
}
}
if (leftDots.length === 0 && rightDots.length === 0) {
leftDots.push("#ff4444");
}
leftDots.forEach(c => {
const d = document.createElement("div");
d.className = "hud-dot";
d.style.backgroundColor = c;
chip.appendChild(d);
});
const span = document.createElement("span");
let text = item.title || item.type;
if (text.length > 20) text = text.substring(0, 18) + "..";
span.innerText = text;
chip.appendChild(span);
rightDots.forEach(c => {
const d = document.createElement("div");
d.className = "hud-dot";
d.style.backgroundColor = c;
chip.appendChild(d);
});
dock.appendChild(chip);
}
}
// 3. Position Update (Run every frame)
const canvas = app.canvas;
const scale = canvas.ds.scale;
const offset = canvas.ds.offset;
// Get the DOM position of the canvas element itself (fixes window offset issues)
const canvasRect = canvas.canvas.getBoundingClientRect();
const scrollX = window.scrollX || window.pageXOffset;
const scrollY = window.scrollY || window.pageYOffset;
// Graph Coordinates
const graphX = node.pos[0];
const graphY = node.pos[1] + node.size[1] + 2;
// --- CORRECTION: MATCH LITEGRAPH TRANSFORM ---
const screenX = (graphX + offset[0]) * scale + canvasRect.left + scrollX;
const screenY = (graphY + offset[1]) * scale + canvasRect.top + scrollY;
// GEOMETRY ONLY
dock.style.position = "absolute";
dock.style.left = screenX + "px";
dock.style.top = screenY + "px";
dock.style.width = (node.size[0] * scale) + "px";
dock.style.transform = "none";
dock.style.margin = "0";
// --- UPDATES: Zoom Class & Z-Index ---
// 1. Zoom Logic (Around 57% zoom, text usually disappears)
if (scale < 0.58) {
dock.classList.add("hud-dock-zoomed-out");
} else {
dock.classList.remove("hud-dock-zoomed-out");
}
// 2. Z-Index Logic (Refined)
// We base the z-index on the node's position in the graph stack (0, 1, 2...).
// Standard UI widgets (like active text boxes) usually have high z-indices (1000+).
// By keeping this low, we allow other DOM elements to sit on top if needed.
let zIndex = graph._nodes.indexOf(node);
// Safety check: Ensure we don't accidentally get -1 or weird values
if (zIndex < 0) zIndex = 0;
if (node.zIndex !== undefined) {
zIndex = node.zIndex;
} else if (node.style && node.style.zIndex) {
zIndex = node.style.zIndex;
}
// Important: We assign this dynamically every frame to ensure it updates if node order changes
dock.style.zIndex = zIndex;
}