commit 7365e71c5b9f720465c481d8d34d6f58d3d1c302 Author: loficoding Date: Sat Jan 24 12:37:52 2026 -0700 Added an icon diff --git a/.tracking b/.tracking new file mode 100644 index 0000000..000e63f --- /dev/null +++ b/.tracking @@ -0,0 +1,9 @@ +.tracking +LICENSE.txt +README.md +__init__.py +__pycache__/__init__.cpython-313.pyc +mainuse.gif +pyproject.toml +web/css/hud_styles.css +web/node_hud.js \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..63b4b68 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4c853c --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# ComfyUI Node HUD 🛠️ + +**Stop the spaghetti. Keep the data.** + +A lightweight UI extension for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) that adds a smart "Heads-Up Display" (HUD) to your nodes. It allows you to stack nodes behind one another to clean up your workspace, while still seeing exactly what is happening inside the stack via a sleek, tinted-glass dock. + +![Demo GIF](mainuse.gif) + +## ✨ Features + +### 1. Hiding Nodes +Hide utility nodes (like String Primitives, Notes, or multiple LoRAs) behind a single "Parent" node. Clean Layout Dock detects these hidden nodes and generates a live status bar attached to the bottom of the parent. + +### 2. Smart I/O Indicators 🚦 +No more guessing if a hidden node is connected. The dock displays **Smart Dots** that visualize the data flow: +* **Left Dots (Inputs):** Shows active incoming connections. +* **Right Dots (Outputs):** Shows active outgoing data. +* **Color Coded:** The dots match ComfyUI's standard wire colors: + * 🟣 **Purple:** Model + * 🟡 **Yellow:** CLIP + * 🔴 **Red:** VAE + * 🌸 **Pink:** Latent + * 🔵 **Blue:** Image + * 🟢 **Green:** String / Text + +### 3. Live Status Feedback +The dock updates in real-time to reflect the state of your nodes: +* **Active:** Green text / Standard border. +* **Muted:** Gray border & dimmed text. +* **Bypassed:** Purple border. +* **Disconnected:** If a node has no active inputs or outputs, a red error dot 🔴 appears. + +### 4. "Tinted Glass" UI +The dock uses a semi-transparent dark background (85% opacity) to ensure readability without feeling "heavy" or blocking the grid entirely. + +--- + +## 📥 Installation + +### Method 1: Manager (Coming Soon) +*Once indexed, you can install this via the ComfyUI Manager.* + +### Method 2: Manual Install +1. Navigate to your ComfyUI `custom_nodes` directory: + ```bash + cd ComfyUI/custom_nodes/ + ``` +2. Clone this repository: + ```bash + git clone https://github.com/loficoding/ComfyUI-Node-HUD.git + ``` +3. **Restart ComfyUI**. + +--- + +## 🖱️ Usage + +1. **Stack Your Nodes:** Drag utility nodes (like *Primitive* or *Note*) physically behind a larger "Parent" node (like *Prompt Manager* or *KSampler*). +2. **Activate the Dock:** + * Right-click the Parent Node. + * Select **"Toggle Stack Info"** from the menu. +3. **That's it!** The dock will appear. If you move the nodes out from behind the parent, the dock updates instantly. + +--- + +## 🎨 Color Legend + +| Color | Meaning | +| :--- | :--- | +| 🟣 | **Model** (Checkpoint / LoRA) | +| 🟡 | **CLIP** (Conditioning) | +| 🔴 | **VAE** | +| 🌸 | **Latent** | +| 🔵 | **Image** | +| 🟢 | **String** / Text | +| 🔴 (Solid) | **Error** / Disconnected | + +--- + +## 🤝 Contributing + +This extension is pure JavaScript. Feel free to submit PRs for new features or styling tweaks! + +## 📄 License + +MIT License. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..4aee448 --- /dev/null +++ b/__init__.py @@ -0,0 +1,7 @@ +# 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"] \ No newline at end of file diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..4d4ab55 Binary files /dev/null and b/__pycache__/__init__.cpython-313.pyc differ diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..d9f9783 Binary files /dev/null and b/icon.png differ diff --git a/mainuse.gif b/mainuse.gif new file mode 100644 index 0000000..4167dd6 Binary files /dev/null and b/mainuse.gif differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f536f45 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "ComfyUI-Node-HUD" +description = "Adds a smart I/O status dock to stacked nodes for cleaner layouts." +version = "1.2.3" +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 = "icon.png" +includes = [] +# "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility + diff --git a/web/css/hud_styles.css b/web/css/hud_styles.css new file mode 100644 index 0000000..2f67615 --- /dev/null +++ b/web/css/hud_styles.css @@ -0,0 +1,64 @@ +/* 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; +} \ No newline at end of file diff --git a/web/node_hud.js b/web/node_hud.js new file mode 100644 index 0000000..9b39760 --- /dev/null +++ b/web/node_hud.js @@ -0,0 +1,270 @@ +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; +} \ No newline at end of file