commit 3eceaea3855dc31142b1db4bdc444334dbb8cdc4 Author: HAL-XP Date: Fri Nov 28 17:28:17 2025 +0100 Initial Commit diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..c3e13cd --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Alexandre Pelletier + +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..85caf41 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# HALXP-Comfy (ComfyUI Extension) + +![Version](https://img.shields.io/badge/version-1.0.0-blue) ![ComfyUI](https://img.shields.io/badge/ComfyUI-Compatible-green) + +**HALXP-Comfy** (High Availability & Layout eXtension Pack) is a professional UI suite for ComfyUI designed to enhance workflow management. + +This extension is the starting point for a growing collection of UI tools. Currently, it features **HALXP Focus**, a smart navigation utility, with more enhancements currently in development. + +**Verified working on:** Legacy UI, ComfyUI V1 (Nodes 2.0), and Portable Distributions. + +## ⚠️ Prerequisite +**This extension requires [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) to be installed.** +The HALXP UI elements are designed to dock seamlessly alongside the ComfyUI Manager button in the main menu. + +## 🌟 Features + +### 1. HALXP Focus +A smart "Focus" button injected into your main menu bar. +* **Auto-Follow:** Smoothly pans the camera to the currently executing node, allowing you to watch your workflow progress hands-free. +* **Smart Docking:** Automatically detects the ComfyUI Manager button and injects itself seamlessly beside it. +* **Visual Feedback:** Features a status indicator (Green dot = Active) so you always know when auto-follow is engaged. +* **V1 Compatible:** Works natively with the new Vue.js frontend and PrimeVue menus. + +### 2. More Coming Soon +HALXP is built as a modular suite. Expect future updates to include additional layout and debugging tools to further improve your ComfyUI experience. + +## 📦 Installation + +### Via ComfyUI Manager (Recommended) +1. Open ComfyUI Manager. +2. Search for `HALXP-Comfy`. +3. Install and Restart. + +### Manual Installation +1. Navigate to your `ComfyUI/custom_nodes/` directory. +2. Clone this repository: + ```bash + git clone [https://github.com/HAL-XP/halxp-comfy](https://github.com/HAL-XP/halxp-comfy) \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..a19508a --- /dev/null +++ b/__init__.py @@ -0,0 +1,12 @@ +# HALXP - High Availability & Layout eXtension Pack +# Registers the web directory for JS extensions + +WEB_DIRECTORY = "./js" + +# Export empty dictionaries so ComfyUI doesn't warn about missing mappings +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} + +__all__ = ["WEB_DIRECTORY", "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] + +print("\033[34m[HALXP-ComfyUI] JS Extensions Loaded successfully.\033[0m") \ No newline at end of file diff --git a/js/halxp-focus.js b/js/halxp-focus.js new file mode 100644 index 0000000..a6bc8dc --- /dev/null +++ b/js/halxp-focus.js @@ -0,0 +1,217 @@ +// \ComfyUI\custom_nodes\better-focus\js\universal_focus.js +// console.log("%c[HALXP-Comfy-Focus] Loaded", "background: purple; color: white; font-weight: bold;"); + +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +app.registerExtension({ + name: "Comfy.HALXP-ComfyUI.Focus", + async setup() { + // --- CONFIG --- + const SMOOTHNESS = 0.15; + const SETTING_ID = "HALXP-Comfy.Focus.DisplayButton"; + + let isFocusEnabled = false; + let activeNode = null; + let animationFrameId = null; + let focusBtn = null; + let dot = null; + + // --- 0. REGISTER SETTING --- + app.ui.settings.addSetting({ + id: SETTING_ID, + name: "Display Focus Button", + type: "boolean", + defaultValue: true, + // MUST be an array. + // Element 0 = Tab Name (HALXP-Comfy) + // Element 1 = Section Header (Focus) + category: ['HALXP-Comfy', 'Focus'], + tooltip: "Show or hide the HALXP Focus button in the top menu.", + onChange: (value) => { + if (value) { + // Try to inject immediately if turned on + injectNextToManager(); + } else { + // Remove button and disable focus if turned off + if (focusBtn) { + focusBtn.remove(); + // Also force stop focus mode if it was active + if (isFocusEnabled) setFocusState(false); + } + } + } + }); + + // --- 1. CREATE BUTTON --- + function createButton() { + const btn = document.createElement("button"); + btn.textContent = "Focus"; + btn.id = "halxp-focus-btn"; + + // CSS: Flex-safe, visible, matches Manager style + btn.style.cssText = ` + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 10px; + margin: 0 4px; /* Space between Focus and Manager */ + + background-color: var(--comfy-input-bg, #333); + color: var(--fg-color, #fff); + border: 1px solid var(--border-color, #555); + border-radius: 4px; + + cursor: pointer; + font-family: sans-serif; + font-size: 13px; + font-weight: 600; + + /* CRITICAL VISIBILITY SETTINGS */ + flex-shrink: 0; + white-space: nowrap; + z-index: 9999; + pointer-events: auto; + + /* DEBUG: REMOVE THIS RED BORDER LATER IF YOU WANT */ + border: 1px solid #d3c1c1ff; + `; + + const d = document.createElement("span"); + d.style.cssText = ` + width: 8px; height: 8px; border-radius: 50%; + background-color: #666; margin-right: 8px; + display: inline-block; + flex-shrink: 0; + `; + btn.prepend(d); + + btn.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + toggleFocus(); + }; + + dot = d; + return btn; + } + + // --- 2. FIND MANAGER & INJECT --- + function injectNextToManager() { + // Check Settings first. If setting is false, do not inject. + const shouldDisplay = app.ui.settings.getSettingValue(SETTING_ID, true); + if (!shouldDisplay) return; + + // Stop if we already exist + if (document.getElementById("halxp-focus-btn")) return; + + // 1. Find the Manager Button + // We look for any button that strictly contains the text "Manager" + const allButtons = Array.from(document.querySelectorAll("button")); + + const managerBtn = allButtons.find(b => { + // Must be visible + if (!b.offsetParent) return false; + + const text = b.innerText.trim(); + return text === "Manager" || text === "ComfyUI Manager"; + }); + + // 2. Inject + if (managerBtn && managerBtn.parentNode) { + // console.log("[Visual Assist] Found Manager Button. Attaching..."); + + if (!focusBtn) focusBtn = createButton(); + + // Match the height of the Manager button exactly + const h = managerBtn.offsetHeight; + if (h > 0) focusBtn.style.height = `${h}px`; + else focusBtn.style.height = "28px"; + + // Insert BEFORE the Manager button (to its left) + managerBtn.parentNode.insertBefore(focusBtn, managerBtn); + + // Restore state appearance if we were somehow already enabled + if (isFocusEnabled) { + dot.style.backgroundColor = "#4CAF50"; + dot.style.boxShadow = "0 0 6px #4CAF50"; + focusBtn.style.color = "#fff"; + focusBtn.style.borderColor = "#4CAF50"; + } + + return; + } + } + + // --- 3. LOOP --- + // We poll every 500ms because the Manager itself loads slowly. + setInterval(injectNextToManager, 500); + + + // --- 4. FOCUS LOGIC (Standard) --- + function toggleFocus() { + setFocusState(!isFocusEnabled); + } + + function setFocusState(enabled) { + isFocusEnabled = enabled; + // Guard: If button doesn't exist (setting off), we can't style it. + if (!focusBtn || !dot) return; + + if (isFocusEnabled) { + dot.style.backgroundColor = "#4CAF50"; + dot.style.boxShadow = "0 0 6px #4CAF50"; + focusBtn.style.color = "#fff"; + focusBtn.style.borderColor = "#4CAF50"; + if (activeNode) requestAnimationFrame(smoothFollow); + } else { + dot.style.backgroundColor = "#666"; + dot.style.boxShadow = "none"; + focusBtn.style.color = "var(--fg-color, #ccc)"; + focusBtn.style.borderColor = "#d3c1c1ff"; // Back to disabled + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); + animationFrameId = null; + } + } + } + + // Listen for canvas drag + const waitForCanvas = setInterval(() => { + if (app.canvas && app.canvas.canvas) { + clearInterval(waitForCanvas); + app.canvas.canvas.addEventListener("pointerdown", () => { + if (isFocusEnabled) setFocusState(false); + }, { capture: true }); + } + }, 500); + + api.addEventListener("executing", (e) => { + if (e.detail) { + activeNode = app.graph.getNodeById(e.detail); + if (isFocusEnabled && activeNode) { + app.canvas.setDirty(true, true); + if (!animationFrameId) requestAnimationFrame(smoothFollow); + } + } + }); + + function smoothFollow() { + if (isFocusEnabled && activeNode) { + const canvas = app.canvas; + const ds = canvas.ds; + const screenCenterX = canvas.canvas.clientWidth / 2; + const screenCenterY = canvas.canvas.clientHeight / 2; + const nodeCenterX = activeNode.pos[0] + activeNode.size[0] / 2; + const nodeCenterY = activeNode.pos[1] + activeNode.size[1] / 2; + const targetOffsetX = (screenCenterX / ds.scale) - nodeCenterX; + const targetOffsetY = (screenCenterY / ds.scale) - nodeCenterY; + ds.offset[0] += (targetOffsetX - ds.offset[0]) * SMOOTHNESS; + ds.offset[1] += (targetOffsetY - ds.offset[1]) * SMOOTHNESS; + canvas.setDirty(true, true); + animationFrameId = requestAnimationFrame(smoothFollow); + } + } + } +}); \ No newline at end of file