diff --git a/CHANGELOG.md b/CHANGELOG.md index d7af22a..f5bb08e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog + +## [0.56.0] - 2025-04-3 +- 增加Resource monitor + ## [0.51.0] - 2025-03-31 - node align 新增快捷键`Shift+WASD` diff --git a/README.md b/README.md index dc3af01..6a9d685 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,15 @@ This is a custom node utility package developed for ComfyUI. Initially created f **📌 所有 KayTool 节点包用到的资源都在 `ComfyUI/custom_nodes/kaytool` 文件夹内。| All KayTool node packages use resources in the `ComfyUI/custom_nodes/kaytool` folder.** +## 资源监视器 Resource Monitor + +- 实时显示当前 ComfyUI 资源使用情况,包括内存占用、显存占用、CPU 占用等。 + Displays real-time resource usage information, including memory usage, GPU memory usage, and CPU usage. +- 支持曲线统计表以精确评估工作流资源消耗情况。 + Supports curve statistics for accurate evaluation of workflow resource consumption. +- 支持自定义颜色设置,可根据个人喜好调整显示效果。 + Supports custom color settings, allowing personal preference adjustments. + ## 节点排版工具栏 Node Align Toolbar - 支持节点多种对齐方式的工具栏 diff --git a/__init__.py b/__init__.py index c06e05b..fff709a 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,8 @@ import os import json from aiohttp import web +from .api import resources + from .nodes.custom_save_image import CustomSaveImage from .nodes.color_adjustment import ColorAdjustment from .nodes.strong_prompt import StrongPrompt @@ -90,28 +92,3 @@ NODE_DISPLAY_NAME_MAPPINGS = { } WEB_DIRECTORY = "web" -SETTINGS_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "settings.json") - -# async def load_settings(request): -# if os.path.exists(SETTINGS_FILE): -# with open(SETTINGS_FILE, "r") as f: -# return web.json_response(json.load(f)) -# return web.json_response({"ShowRunOption": True, "ShowSetGetOptions": True}) - -# async def save_settings(request): -# data = await request.json() -# settings = {} -# if os.path.exists(SETTINGS_FILE): -# with open(SETTINGS_FILE, "r") as f: -# settings = json.load(f) -# settings.update(data) -# with open(SETTINGS_FILE, "w") as f: -# json.dump(settings, f, indent=2) -# return web.Response(status=200) - -# import server -# app = server.PromptServer.instance -# app.app.add_routes([ -# web.get("/kaytool/load_settings", load_settings), -# web.post("/kaytool/save_settings", save_settings), -# ]) \ No newline at end of file diff --git a/api/resources.py b/api/resources.py new file mode 100644 index 0000000..b4c556b --- /dev/null +++ b/api/resources.py @@ -0,0 +1,84 @@ +from server import PromptServer +import psutil +import asyncio +import json +from aiohttp import web +from cpuinfo import get_cpu_info +import pynvml +import platform + +PLATFORM = platform.system() +IS_MACOS = PLATFORM == "Darwin" +IS_LINUX = PLATFORM == "Linux" +IS_WINDOWS = PLATFORM == "Windows" + +PYNVML_AVAILABLE = False +if IS_LINUX or IS_WINDOWS: + try: + pynvml.nvmlInit() + PYNVML_AVAILABLE = True + except pynvml.NVMLError: + PYNVML_AVAILABLE = False + +CPU_NAME = get_cpu_info().get('brand_raw', "Unknown") +GPU_INFO_CACHE = None +LAST_GPU_UPDATE = 0 +GPU_CACHE_INTERVAL = 0.5 + +async def get_gpu_info(): + global GPU_INFO_CACHE, LAST_GPU_UPDATE + current_time = asyncio.get_event_loop().time() + if GPU_INFO_CACHE is None or (current_time - LAST_GPU_UPDATE) >= GPU_CACHE_INTERVAL: + if IS_MACOS: + GPU_INFO_CACHE = "GPU monitoring not supported on macOS" + elif PYNVML_AVAILABLE: + try: + device_count = pynvml.nvmlDeviceGetCount() + gpu_info = [] + for i in range(device_count): + handle = pynvml.nvmlDeviceGetHandleByIndex(i) + name = pynvml.nvmlDeviceGetName(handle) + if isinstance(name, bytes): + name = name.decode('utf-8') + util = pynvml.nvmlDeviceGetUtilizationRates(handle) + mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle) + gpu_info.append({ + "index": i, + "name": name, + "load": float(util.gpu), + "memory_used": float(mem_info.used) / (1024 ** 3), + "memory_total": float(mem_info.total) / (1024 ** 3), + "memory_percent": (float(mem_info.used) / float(mem_info.total)) * 100 + }) + GPU_INFO_CACHE = gpu_info + except Exception: + GPU_INFO_CACHE = "GPU information unavailable" + else: + GPU_INFO_CACHE = "GPU information unavailable" + LAST_GPU_UPDATE = current_time + return GPU_INFO_CACHE + +@PromptServer.instance.routes.get("/kaytool/resources") +async def fetch_resources(request): + try: + cpu_percent = psutil.cpu_percent(interval=0.1) + cpu_count = psutil.cpu_count() + ram = psutil.virtual_memory() + ram_total = round(ram.total / (1024 ** 3), 1) + ram_used = round(ram.used / (1024 ** 3), 1) + ram_percent = ram.percent + + gpu_info = await get_gpu_info() + + data = { + "cpu_percent": cpu_percent, + "cpu_count": cpu_count, + "cpu_name": CPU_NAME, + "ram_total": ram_total, + "ram_used": ram_used, + "ram_percent": ram_percent, + "gpu": gpu_info + } + return web.Response(text=json.dumps(data), content_type='application/json') + except Exception as e: + return web.Response(text=json.dumps({"error": str(e)}), status=500) \ No newline at end of file diff --git a/locales/zh/main.json b/locales/zh/main.json index 9413eec..80193ff 100644 --- a/locales/zh/main.json +++ b/locales/zh/main.json @@ -5,7 +5,8 @@ "KayTool Menu": "右键菜单", "Custom Web Logo (BETA)": "自定义网页徽标", "Workflow PNG": "Workflow PNG", - "NodeAlignment": "节点对齐工具栏" + "NodeAlignment": "节点对齐工具栏", + "Resource Monitor": "资源监视器" }, "nodeCategories": { "Image": "图像", diff --git a/locales/zh/settings.json b/locales/zh/settings.json index 93d9119..646b097 100644 --- a/locales/zh/settings.json +++ b/locales/zh/settings.json @@ -18,11 +18,20 @@ "name": "Workflow PNG 导出的边距大小", "tooltip": "建议设置 100" }, + "KayTool_EnableResourceMonitor": { + "name": "启用资源监视器" + }, + "KayTool_ResourceMonitorEdgeFade": { + "name": "统计表边缘渐隐" + }, + "KayTool_ResourceMonitorBackgroundOpacity": { + "name": "资源监视器背景不透明度" + }, "KayTool_SetGetForegroundColor": { - "name": " '🛜 Set/Get' 节点颜色" + "name": "节点颜色" }, "KayTool_SetGetBackgroundColor": { - "name": " '🛜 Set/Get' 标题颜色" + "name": "标题颜色" }, "KayTool_CustomWebLogo": { "name": "文件必须 < 1MB", diff --git a/nodes/ab_images.py b/nodes/ab_images.py index d4a4196..3cb07af 100644 --- a/nodes/ab_images.py +++ b/nodes/ab_images.py @@ -14,7 +14,6 @@ class ABImages(PreviewImage): "a": ("IMAGE",), }, "optional": { - "b": ("IMAGE",), }, "hidden": { diff --git a/pyproject.toml b/pyproject.toml index 3c3efdf..91b499e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "kaytool" -description ="KayTool enhances ComfyUI workflow efficiency with practical tools, including: image processing (resize, color, blur, expand, merge, crop), background/mask tasks via BiRefNet/RemBG, wireless data transfer (Get & Out), AI translation (Tencent/Baidu), dynamic math operations, flexible text handling, precision sliders, metadata/color profile-supported image saving, batch processing via image folder loading, canvas workflow node map export as PNG with embedded metadata, quick Run options via right-click (Run Group/Node), node alignment toolbar with custom styled styles, and custom ComfyUI logo integration." -version = "0.52.2" +description ="KayTool enhances ComfyUI workflow efficiency with practical tools, including: image processing (resize, color, blur, expand, merge, crop), background/mask tasks via BiRefNet/RemBG, wireless data transfer (Get & Out), AI translation (Tencent/Baidu),Professional high-precision resource monitor and statistics table, dynamic math operations, flexible text handling, precision sliders, metadata/color profile-supported image saving, batch processing via image folder loading, canvas workflow node map export as PNG with embedded metadata, quick Run options via right-click (Run Group/Node), node alignment toolbar with custom styled styles, and custom ComfyUI logo integration." +version = "0.56.0" license = {file = "LICENSE"} -dependencies = ["rembg, timm, opencv-python, requests"] +dependencies = ["rembg, timm, opencv-python, requests, py-cpuinfo, pynvml"] [project.urls] Repository = "https://github.com/kk8bit/KayTool" diff --git a/requirements.txt b/requirements.txt index f3a3f29..2145c7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ rembg timm opencv-python -requests \ No newline at end of file +requests +py-cpuinfo +pynvml \ No newline at end of file diff --git a/web/resourcemonitor.js b/web/resourcemonitor.js new file mode 100644 index 0000000..596b8eb --- /dev/null +++ b/web/resourcemonitor.js @@ -0,0 +1,563 @@ +import { app } from "../../../scripts/app.js"; + +const KayResourceMonitor = { + toolbar: null, + chartCanvas: null, + dataDisplay: null, + menuButton: null, + + dataHistory: { cpu: [], ram: [], gpu: [], gpuMem: [] }, + displayHistory: { cpu: [], ram: [], gpu: [], gpuMem: [] }, + current: { + cpu: 0, ram: 0, gpu: 0, gpuMem: 0, + ramUsed: 0, ramTotal: 0, gpuMemUsed: 0, gpuMemTotal: 0 + }, + target: { + cpu: 0, ram: 0, gpu: 0, gpuMem: 0, + ramTotal: 0, ramUsed: 0, gpuMemTotal: 0, gpuMemUsed: 0, + cpuName: "Unknown", gpuName: "Unknown" + }, + + cpuCores: 4, + dragState: { isDragging: false, offsetX: 0, offsetY: 0 }, + resizeState: { isResizing: false, direction: null, startX: 0, startY: 0, startWidth: 0, startHeight: 0 }, + position: { left: 0, top: 0 }, + size: { width: 250, height: 200 }, + lastUpdate: 0, lastCurveUpdate: 0, lastDisplayUpdate: 0, + maxPoints: 6000, + fetchInterval: 500, + curveInterval: 0, + displayInterval: 0, + smoothFactor: 0.01, + minWidth: 150, + minHeight: 100, + maxHeight: 200, + edgeThreshold: 10, + + colors: { + cpu: 'rgb(232, 166, 0)', + ram: 'rgb(88, 207, 3)', + gpu: 'rgb(224, 0, 153)', + gpuMem: 'rgb(0, 115, 255)' + }, + + styles: { + toolbar: `position: fixed; display: flex; flex-direction: column; color: rgba(186, 186, 186, 0.8); padding: 10px; border-radius: 5px; z-index: 10000; user-select: none; pointer-events: auto; max-height: 200px;`, + header: `cursor: grab; margin-bottom: 10px; font-weight: bold; font-size: 10px;`, + canvas: `margin-bottom: 10px; width: 100%; height: 100px;`, + display: `display: flex; flex-direction: column; font-size: 10px; flex-grow: 1; overflow: hidden;`, + dot: (color) => `display: inline-block; width: 3px; height: 3px; border-radius: 50%; background: ${color}; margin-right: 5px; flex-shrink: 0;`, + bar: (width) => `display: inline-block; width: ${width}px; height: 3px; background: rgba(200, 200, 200, 0.3); margin-right: 5px; position: relative; flex-shrink: 0;`, + fill: (color, percent) => `position: absolute; top: 0; left: 0; width: ${percent}%; height: 100%; background: ${color};`, + row: `display: flex; align-items: center; height: 12px; white-space: nowrap;`, + text: `overflow: hidden; text-overflow: ellipsis;` + }, + + isVisible: true, + animationFrameId: null, + isEnabled: true, + isInitialized: false, + edgeFadeEnabled: true, + + async init() { + if (this.isInitialized) return; + this.isInitialized = true; + + this.loadVisibility(); + this.startMonitoring(); + this.bindEvents(); + + await this.loadSettings(); + this.setupUI(); + }, + + async loadSettings() { + this.isEnabled = app.ui.settings.getSettingValue("KayTool.EnableResourceMonitor"); + this.edgeFadeEnabled = app.ui.settings.getSettingValue("KayTool.ResourceMonitorEdgeFade"); + }, + + loadVisibility() { + const visible = localStorage.getItem('KayResourceMonitorVisible'); + this.isVisible = visible === null ? true : visible === 'true'; + }, + + saveVisibility() { + localStorage.setItem('KayResourceMonitorVisible', this.isVisible.toString()); + }, + + setupUI() { + this.toolbar = document.createElement('div'); + this.toolbar.id = 'kay-resource-monitor'; + const opacity = app.ui.settings.getSettingValue("KayTool.ResourceMonitorBackgroundOpacity") / 100; + this.toolbar.style.cssText = `${this.styles.toolbar} width: ${this.size.width}px; height: ${this.size.height}px; left: ${this.position.left}px; top: ${this.position.top}px; display: ${this.isVisible && this.isEnabled ? 'flex' : 'none'}; background: rgba(0, 0, 0, ${opacity});`; + + const header = document.createElement('div'); + header.style.cssText = this.styles.header; + header.textContent = "𝙆 Resource Monitor"; + this.toolbar.appendChild(header); + + this.chartCanvas = document.createElement('canvas'); + this.chartCanvas.style.cssText = this.styles.canvas; + this.chartCanvas.width = this.size.width - 20; + this.chartCanvas.height = 100; + this.toolbar.appendChild(this.chartCanvas); + + this.dataDisplay = document.createElement('div'); + this.dataDisplay.style.cssText = this.styles.display; + this.toolbar.appendChild(this.dataDisplay); + + document.body.appendChild(this.toolbar); + this.loadPosition(); + this.loadSize(); + this.onResize(); + this.bindToolbarEvents(); + + if (this.isEnabled && this.isVisible) { + this.startAnimation(); + } + }, + + show() { + this.isVisible = true; + if (this.toolbar && this.isEnabled) { + this.toolbar.style.display = 'flex'; + this.refreshCanvas(); + this.startAnimation(); + } + this.saveVisibility(); + }, + + hide() { + this.isVisible = false; + if (this.toolbar) { + this.toolbar.style.display = 'none'; + this.stopAnimation(); + } + this.saveVisibility(); + }, + + updateEnabledState(enabled) { + this.isEnabled = enabled; + if (this.menuButton) { + this.menuButton.style.display = enabled ? '' : 'none'; + } + if (this.toolbar) { + this.toolbar.style.display = enabled && this.isVisible ? 'flex' : 'none'; + if (enabled && this.isVisible) { + this.refreshCanvas(); + this.startAnimation(); + } else { + this.stopAnimation(); + } + } + }, + + refreshCanvas() { + if (this.chartCanvas) { + const headerHeight = this.toolbar.firstChild.getBoundingClientRect().height + 10; + const dataHeight = this.dataDisplay.clientHeight; + this.chartCanvas.width = this.toolbar.clientWidth - 20; + this.chartCanvas.height = this.toolbar.clientHeight - headerHeight - dataHeight - 10; + } + }, + + async fetchResources() { + try { + const response = await fetch('/kaytool/resources', { timeout: 5000 }); + if (!response.ok) throw new Error(`HTTP error: ${response.status}`); + const data = await response.json(); + this.updateTarget(data); + } catch (error) { + this.updateTarget({ + cpu_percent: 0, + cpu_count: this.cpuCores, + cpu_name: "Unknown", + ram_total: 0, + ram_used: 0, + ram_percent: 0, + gpu: "Fetch failed" + }); + } + }, + + updateTarget(data) { + this.target.cpu = data.cpu_percent || 0; + this.cpuCores = data.cpu_count || 4; + this.target.ram = data.ram_percent || 0; + this.target.ramTotal = data.ram_total || 0; + this.target.ramUsed = data.ram_used || 0; + this.target.cpuName = data.cpu_name || "Unknown"; + this.dataHistory.cpu.push(this.target.cpu); + this.dataHistory.ram.push(this.target.ram); + + if (data.gpu && typeof data.gpu !== 'string' && data.gpu.length > 0) { + this.target.gpu = data.gpu[0].load || 0; + this.target.gpuMem = data.gpu[0].memory_percent || 0; + this.target.gpuMemTotal = data.gpu[0].memory_total || 0; + this.target.gpuMemUsed = data.gpu[0].memory_used || 0; + this.target.gpuName = data.gpu[0].name || "Unknown"; + this.dataHistory.gpu.push(this.target.gpu); + this.dataHistory.gpuMem.push(this.target.gpuMem); + } else { + this.target.gpu = 0; + this.target.gpuMem = 0; + this.target.gpuMemTotal = 0; + this.target.gpuMemUsed = 0; + this.target.gpuName = "Unknown"; + this.dataHistory.gpu.push(0); + this.dataHistory.gpuMem.push(0); + } + + if (this.dataHistory.cpu.length > this.maxPoints) { + this.dataHistory.cpu.shift(); + this.dataHistory.ram.shift(); + this.dataHistory.gpu.shift(); + this.dataHistory.gpuMem.shift(); + } + }, + + updateDisplay() { + const { cpu, ram, gpu, gpuMem, ramUsed, ramTotal, gpuMemUsed, gpuMemTotal } = this.current; + const { styles, colors } = this; + this.dataDisplay.innerHTML = [ + this.renderRow('CPU', colors.cpu, cpu, `${cpu.toFixed(1)}% (${this.cpuCores} cores) - ${this.target.cpuName}`), + this.renderRow('RAM', colors.ram, ram, `${ramUsed.toFixed(1)}/${ramTotal.toFixed(1)}GB (${ram.toFixed(1)}%)`), + this.target.gpu || this.target.gpuMem + ? [ + this.renderRow('GPU', colors.gpu, gpu, `${gpu.toFixed(1)}% - ${this.target.gpuName}`), + this.renderRow('VRAM', colors.gpuMem, gpuMem, `${gpuMemUsed.toFixed(1)}/${gpuMemTotal.toFixed(1)}GB (${gpuMem.toFixed(1)}%)`) + ].join('') + : [ + this.renderRow('GPU', colors.gpu, 0, 'N/A'), + this.renderRow('VRAM', colors.gpuMem, 0, 'N/A') + ].join('') + ].join(''); + }, + + renderRow(label, color, value, text) { + const { styles } = this; + const containerWidth = this.toolbar.clientWidth - 20; + const minBarWidth = 50; + const maxBarWidth = 200; + const fixedSpacing = 3 + 5 + 5; + const availableWidth = containerWidth - fixedSpacing; + const barWidth = Math.min(maxBarWidth, Math.max(minBarWidth, availableWidth * 0.3)); + + const textAvailableWidth = containerWidth - fixedSpacing - barWidth; + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + ctx.font = '10px sans-serif'; + const textWidth = ctx.measureText(`${label}: ${text}`).width; + let displayText = `${label}: ${text}`; + if (textWidth > textAvailableWidth) { + const ellipsisWidth = ctx.measureText('...').width; + let truncatedText = `${label}: `; + let remainingWidth = textAvailableWidth - ctx.measureText(truncatedText).width - ellipsisWidth; + for (let i = 0; i < text.length; i++) { + const charWidth = ctx.measureText(text[i]).width; + if (remainingWidth < charWidth) { + truncatedText += '...'; + break; + } + truncatedText += text[i]; + remainingWidth -= charWidth; + } + displayText = truncatedText; + } + return `
+ + + ${displayText} +
`; + }, + + animate() { + if (!this.isVisible || !this.isEnabled || !this.toolbar) return; + + const now = performance.now(); + + if (now - this.lastUpdate >= this.fetchInterval) { + this.fetchResources(); + this.lastUpdate = now; + } + + for (const key in this.current) { + if (this.current[key] === 0 && this.target[key] !== 0) { + this.current[key] = this.target[key]; + } else { + this.current[key] += (this.target[key] - this.current[key]) * this.smoothFactor; + } + } + + if (now - this.lastCurveUpdate >= this.curveInterval) { + for (const key in this.displayHistory) { + this.displayHistory[key].push(this.current[key]); + if (this.displayHistory[key].length > this.maxPoints) this.displayHistory[key].shift(); + } + this.lastCurveUpdate = now; + } + + if (now - this.lastDisplayUpdate >= this.displayInterval) { + this.updateDisplay(); + this.lastDisplayUpdate = now; + } + + if (typeof this.drawCurves === 'function') { + this.drawCurves(); + } + + this.animationFrameId = requestAnimationFrame(() => this.animate()); + }, + + startAnimation() { + if (!this.animationFrameId && this.isEnabled && this.isVisible && this.toolbar) { + this.lastUpdate = performance.now(); + this.lastCurveUpdate = performance.now(); + this.lastDisplayUpdate = performance.now(); + this.animationFrameId = requestAnimationFrame(() => this.animate()); + } + }, + + stopAnimation() { + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = null; + } + }, + + drawCurves() { + const ctx = this.chartCanvas.getContext('2d'); + const w = this.chartCanvas.width; + const h = this.chartCanvas.height; + + ctx.clearRect(0, 0, w, h); + + ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; + ctx.lineWidth = 0.5; + ctx.beginPath(); + for (let y = 0; y <= h; y += h / 4) { + ctx.moveTo(0, y); + ctx.lineTo(w, y); + } + ctx.stroke(); + + const step = w / (this.maxPoints - 1); + const { colors } = this; + [ + { data: this.displayHistory.cpu, color: colors.cpu }, + { data: this.displayHistory.ram, color: colors.ram }, + { data: this.displayHistory.gpu, color: colors.gpu }, + { data: this.displayHistory.gpuMem, color: colors.gpuMem } + ].forEach(({ data, color }) => { + ctx.strokeStyle = color; + ctx.lineWidth = 0.8; + ctx.beginPath(); + data.forEach((v, i) => { + const x = i * step; + const y = h - (v / 100) * h; + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + }); + ctx.stroke(); + }); + + if (this.edgeFadeEnabled) { + const gradient = ctx.createLinearGradient(0, 0, w, 0); + gradient.addColorStop(0, 'rgba(255, 255, 255, 0)'); + gradient.addColorStop(0.1, 'rgba(255, 255, 255, 1)'); + gradient.addColorStop(0.9, 'rgba(255, 255, 255, 1)'); + gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); + + ctx.save(); + ctx.globalCompositeOperation = 'destination-in'; + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, w, h); + ctx.restore(); + } + }, + + startMonitoring() { + for (const key in this.displayHistory) { + this.displayHistory[key] = Array(this.maxPoints).fill(0); + } + }, + + bindEvents() { + document.addEventListener('mousemove', (e) => { + if (this.dragState.isDragging) this.onDrag(e); + if (this.resizeState.isResizing) this.onResizeDrag(e); + }); + document.addEventListener('mouseup', (e) => { + if (this.dragState.isDragging) this.endDrag(); + if (this.resizeState.isResizing) this.endResize(); + }); + document.addEventListener('selectstart', (e) => (this.dragState.isDragging || this.resizeState.isResizing) && e.preventDefault()); + window.addEventListener('resize', () => this.onResize()); + }, + + bindToolbarEvents() { + if (this.toolbar) { + this.toolbar.addEventListener('mousemove', (e) => this.onMouseMove(e)); + this.toolbar.addEventListener('mousedown', (e) => { + if (e.target.style.cursor === 'grab') this.startDrag(e); + else if (['e-resize', 's-resize', 'se-resize'].includes(e.target.style.cursor)) this.startResize(e, e.target.style.cursor.split('-')[0]); + }); + } + }, + + loadPosition() { + const pos = JSON.parse(localStorage.getItem('KayResourceMonitorPosition')) || {}; + this.position.left = pos.left || 0; + this.position.top = pos.top || 0; + this.toolbar.style.left = `${this.position.left}px`; + this.toolbar.style.top = `${this.position.top}px`; + }, + + loadSize() { + const size = JSON.parse(localStorage.getItem('KayResourceMonitorSize')) || {}; + this.size.width = Math.max(size.width || 250, this.minWidth); + this.size.height = Math.max(size.height || 200, this.minHeight); + this.toolbar.style.width = `${this.size.width}px`; + this.toolbar.style.height = `${this.size.height}px`; + }, + + savePositionAndSize() { + localStorage.setItem('KayResourceMonitorPosition', JSON.stringify(this.position)); + localStorage.setItem('KayResourceMonitorSize', JSON.stringify(this.size)); + }, + + onResize() { + if (!this.toolbar) return; + const winW = window.innerWidth; + const winH = window.innerHeight; + this.position.left = Math.max(0, Math.min(this.position.left, winW - this.size.width)); + this.position.top = Math.max(0, Math.min(this.position.top, winH - this.size.height)); + this.toolbar.style.left = `${this.position.left}px`; + this.toolbar.style.top = `${this.position.top}px`; + this.chartCanvas.width = this.toolbar.clientWidth - 20; + const headerHeight = this.toolbar.firstChild.getBoundingClientRect().height + 10; + const dataHeight = this.dataDisplay.clientHeight; + this.chartCanvas.height = this.toolbar.clientHeight - headerHeight - dataHeight - 10; + this.updateDisplay(); + }, + + onMouseMove(e) { + if (!this.toolbar || !this.toolbar.contains(e.target)) return; + + if (this.dragState.isDragging) { + this.onDrag(e); + return; + } + if (this.resizeState.isResizing) { + this.onResizeDrag(e); + return; + } + + const rect = this.toolbar.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const nearRight = x >= rect.width - this.edgeThreshold && x <= rect.width; + const nearBottom = y >= rect.height - this.edgeThreshold && y <= rect.height; + + if (e.target.style.cursor === 'grab') { + this.toolbar.style.cursor = 'grab'; + } else if (nearRight && nearBottom) { + this.toolbar.style.cursor = 'se-resize'; + } else if (nearRight) { + this.toolbar.style.cursor = 'e-resize'; + } else if (nearBottom) { + this.toolbar.style.cursor = 's-resize'; + } else { + this.toolbar.style.cursor = 'default'; + } + }, + + startDrag(e) { + e.preventDefault(); + const rect = this.toolbar.getBoundingClientRect(); + this.dragState.isDragging = true; + this.dragState.offsetX = e.clientX - rect.left; + this.dragState.offsetY = e.clientY - rect.top; + this.toolbar.style.cursor = 'grabbing'; + }, + + onDrag(e) { + if (!this.dragState.isDragging) return; + const winW = window.innerWidth; + const winH = window.innerHeight; + this.position.left = Math.max(0, Math.min(e.clientX - this.dragState.offsetX, winW - this.size.width)); + this.position.top = Math.max(0, Math.min(e.clientY - this.dragState.offsetY, winH - this.size.height)); + this.toolbar.style.left = `${this.position.left}px`; + this.toolbar.style.top = `${this.position.top}px`; + }, + + endDrag() { + if (!this.dragState.isDragging) return; + this.dragState.isDragging = false; + this.toolbar.style.cursor = 'default'; + this.savePositionAndSize(); + }, + + startResize(e, direction) { + e.preventDefault(); + this.resizeState.isResizing = true; + this.resizeState.direction = direction; + this.resizeState.startX = e.clientX; + this.resizeState.startY = e.clientY; + this.resizeState.startWidth = this.toolbar.clientWidth; + this.resizeState.startHeight = this.toolbar.clientHeight; + this.toolbar.style.cursor = `${direction}-resize`; + }, + + onResizeDrag(e) { + if (!this.resizeState.isResizing) return; + const dx = e.clientX - this.resizeState.startX; + const dy = e.clientY - this.resizeState.startY; + + if (this.resizeState.direction === 'e' || this.resizeState.direction === 'se') { + this.size.width = Math.max(this.minWidth, this.resizeState.startWidth + dx); + this.toolbar.style.width = `${this.size.width}px`; + } + if (this.resizeState.direction === 's' || this.resizeState.direction === 'se') { + this.size.height = Math.min(this.maxHeight, Math.max(this.minHeight, this.resizeState.startHeight + dy)); + this.toolbar.style.height = `${this.size.height}px`; + } + this.onResize(); + }, + + endResize() { + if (!this.resizeState.isResizing) return; + this.resizeState.isResizing = false; + this.resizeState.direction = null; + this.toolbar.style.cursor = 'default'; + this.savePositionAndSize(); + } +}; + +app.registerExtension({ + name: "KayTool.ResourceMonitor", + async setup() { + await KayResourceMonitor.init(); + + const showMenuButton = new (await import("/scripts/ui/components/button.js")).ComfyButton({ + content: "𝙆 Monitor", + action: () => { + if (KayResourceMonitor.isVisible) { + KayResourceMonitor.hide(); + showMenuButton.tooltip = "Show Monitor"; + } else { + KayResourceMonitor.show(); + showMenuButton.tooltip = "Hide Monitor"; + } + }, + tooltip: KayResourceMonitor.isVisible ? "Hide Monitor" : "Show Monitor" + }); + + KayResourceMonitor.menuButton = showMenuButton.element; + if (app.menu?.settingsGroup) { + app.menu.settingsGroup.append(showMenuButton); + } + KayResourceMonitor.updateEnabledState(KayResourceMonitor.isEnabled); + window.KayResourceMonitor = KayResourceMonitor; + } +}); \ No newline at end of file diff --git a/web/settings.js b/web/settings.js index de52547..7055769 100644 --- a/web/settings.js +++ b/web/settings.js @@ -1,204 +1,243 @@ import { app } from "../../../scripts/app.js"; - + app.registerExtension({ name: "KayTool.Settings", - setup() { - app.ui.settings.addSetting({ - id: "KayTool.ShowRunOption", - name: "Show '▶️ Run' option in node context menu", - type: "boolean", - defaultValue: true, - category: ["KayTool", "▶️ Run", "ShowRunOption"], - }); - - app.ui.settings.addSetting({ - id: "KayTool.ShowSetGetOptions", - name: "Show 'Set/Get' options in node context menu", - type: "boolean", - defaultValue: true, - category: ["KayTool", "🛜 Set/Get", "ShowSetGetOptions"], - tooltip: "Toggles visibility of 'Set/Get' options in node context menus." - }); - - app.ui.settings.addSetting({ - id: "KayTool.ShiftR", - name: "Use 'Shift+R' to quickly run selected node", - type: "boolean", - defaultValue: true, - category: ["KayTool", "▶️ Run", "ShiftR"], - }); - - app.ui.settings.addSetting({ - id: "KayTool.ShowWorkflowPNG", - name: "Show 'Workflow PNG' option in KayTool menu", - type: "boolean", - defaultValue: true, - category: ["KayTool", "Workflow PNG", "ShowWorkflowPNG"], - }); - - app.ui.settings.addSetting({ - id: "KayTool.ShowStarToMe", - name: "Show 'Star to me' option in KayTool menu", - type: "boolean", - defaultValue: true, - category: ["KayTool", "Menu", "ShowStarToMe"], - }); - - app.ui.settings.addSetting({ - id: "KayTool.WorkflowPNG", - name: "Margin size for Workflow PNG export", - type: "slider", - defaultValue: 100, - attrs: { min: 0, max: 200, step: 10 }, - category: ["KayTool", "Workflow PNG", "MarginSize"], - tooltip: "Default value: 100." - }); - - app.ui.settings.addSetting({ - id: "KayTool.SetGetForegroundColor", - name: "Set/Get Node Foreground Color", - type: "color", - defaultValue: "000000", - category: ["KayTool", "🛜 Set/Get", "ForegroundColor"], - onChange: (newVal) => { - if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { - updateSetGetNodeColors(); - if (app.graph) app.graph.setDirtyCanvas(true, true); + async setup() { + const settings = [ + { + id: "KayTool.ShowRunOption", + name: "Show '▶️ Run' option in node context menu", + type: "boolean", + defaultValue: true, + category: ["KayTool", "▶️ Run", "ShowRunOption"], + }, + { + id: "KayTool.ShowSetGetOptions", + name: "Show 'Set/Get' options in node context menu", + type: "boolean", + defaultValue: true, + category: ["KayTool", "🛜 Set/Get", "ShowSetGetOptions"], + tooltip: "Toggles visibility of 'Set/Get' options in node context menus." + }, + { + id: "KayTool.ShiftR", + name: "Use 'Shift+R' to quickly run selected node", + type: "boolean", + defaultValue: true, + category: ["KayTool", "▶️ Run", "ShiftR"], + }, + { + id: "KayTool.ShowWorkflowPNG", + name: "Show 'Workflow PNG' option in KayTool menu", + type: "boolean", + defaultValue: true, + category: ["KayTool", "Workflow PNG", "ShowWorkflowPNG"], + }, + { + id: "KayTool.ShowStarToMe", + name: "Show 'Star to me' option in KayTool menu", + type: "boolean", + defaultValue: true, + category: ["KayTool", "Menu", "ShowStarToMe"], + }, + { + id: "KayTool.WorkflowPNG", + name: "Margin size for Workflow PNG export", + type: "slider", + defaultValue: 100, + attrs: { min: 0, max: 200, step: 10 }, + category: ["KayTool", "Workflow PNG", "MarginSize"], + tooltip: "Default value: 100." + }, + { + id: "KayTool.SetGetForegroundColor", + name: "Set/Get Node Foreground Color", + type: "color", + defaultValue: "000000", + category: ["KayTool", "🛜 Set/Get", "ForegroundColor"], + onChange: (newVal) => { + if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { + updateSetGetNodeColors(); + if (app.graph) app.graph.setDirtyCanvas(true, true); + } } - } - }); - - app.ui.settings.addSetting({ - id: "KayTool.SetGetBackgroundColor", - name: "Set/Get Node Background Color", - type: "color", - defaultValue: "000000", - category: ["KayTool", "🛜 Set/Get", "BackgroundColor"], - onChange: (newVal) => { - if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { - updateSetGetNodeColors(); - if (app.graph) app.graph.setDirtyCanvas(true, true); + }, + { + id: "KayTool.SetGetBackgroundColor", + name: "Set/Get Node Background Color", + type: "color", + defaultValue: "000000", + category: ["KayTool", "🛜 Set/Get", "BackgroundColor"], + onChange: (newVal) => { + if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { + updateSetGetNodeColors(); + if (app.graph) app.graph.setDirtyCanvas(true, true); + } } - } - }); + }, + { + id: "KayTool.NodeAlignDisplayMode", + name: "Node Alignment Toolbar Display Mode", + type: "combo", + options: [ + { value: "permanent", text: "Permanent" }, + { value: "on-select", text: "Show on Node Selection" }, + { value: "disabled", text: "Disabled" } + ], + defaultValue: "permanent", + category: ["KayTool", "NodeAlignment", "DisplayMode"], + onChange: (value) => { + const manager = window.KayNodeAlignmentManager; + const initFn = window.initializeKayNodeAlignment; + if (!manager || !initFn) return; - app.ui.settings.addSetting({ - id: "KayTool.NodeAlignDisplayMode", - name: "Node Alignment Toolbar Display Mode", - type: "combo", - options: [ - { value: "permanent", text: "Permanent" }, - { value: "on-select", text: "Show on Node Selection" }, - { value: "disabled", text: "Disabled" } - ], - defaultValue: "permanent", - category: ["KayTool", "NodeAlignment", "DisplayMode"], - onChange: (value) => { - const manager = window.KayNodeAlignmentManager; - const initFn = window.initializeKayNodeAlignment; - if (!manager || !initFn) return; - - if (!manager.isInitialized) { - initFn(); + if (!manager.isInitialized) { + initFn(); + } + manager.updateDisplayMode(value); } - manager.updateDisplayMode(value); // - } - }); - - app.ui.settings.addSetting({ - id: "KayTool.EnableAlignmentShortcuts", - name: "Enable Shift+WASD for Quick Alignment (Up/Left/Down/Right)", - type: "boolean", - defaultValue: true, - category: ["KayTool", "NodeAlignment", "Shortcuts"], - tooltip: "Use Shift+W (Up), Shift+A (Left), Shift+S (Down), Shift+D (Right) to align selected nodes." - }); - - app.ui.settings.addSetting({ - id: "KayTool.NodeAlignBackgroundOpacity", - name: "Node Alignment Toolbar Background Opacity", - type: "slider", - defaultValue: 100, - attrs: { min: 0, max: 100, step: 1 }, - category: ["KayTool", "NodeAlignment", "BackgroundOpacity"], - onChange: (newVal) => { - const toolbar = document.getElementById('kay-node-alignment-toolbar'); - if (toolbar) { - const bgColor = app.ui.settings.getSettingValue("KayTool.NodeAlignBackgroundColor"); + }, + { + id: "KayTool.EnableAlignmentShortcuts", + name: "Enable Shift+WASD for Quick Alignment (Up/Left/Down/Right)", + type: "boolean", + defaultValue: true, + category: ["KayTool", "NodeAlignment", "Shortcuts"], + tooltip: "Use Shift+W (Up), Shift+A (Left), Shift+S (Down), Shift+D (Right) to align selected nodes." + }, + { + id: "KayTool.NodeAlignBackgroundOpacity", + name: "Node Alignment Toolbar Background Opacity", + type: "slider", + defaultValue: 100, + attrs: { min: 0, max: 100, step: 1 }, + category: ["KayTool", "NodeAlignment", "BackgroundOpacity"], + onChange: (newVal) => { + const toolbar = document.getElementById('kay-node-alignment-toolbar'); + if (toolbar) { + const bgColor = app.ui.settings.getSettingValue("KayTool.NodeAlignBackgroundColor"); + const opacity = newVal / 100; + if (opacity > 0 && /^[0-9A-Fa-f]{6}$/.test(bgColor)) { + toolbar.style.background = `rgba(${parseInt(bgColor.substr(0, 2), 16)}, ${parseInt(bgColor.substr(2, 2), 16)}, ${parseInt(bgColor.substr(4, 2), 16)}, ${opacity})`; + } else { + toolbar.style.background = ''; + } + } + } + }, + { + id: "KayTool.NodeAlignBackgroundColor", + name: "Node Alignment Toolbar Background Color", + type: "color", + defaultValue: "000000", + category: ["KayTool", "NodeAlignment", "BackgroundColor"], + onChange: (newVal) => { + const toolbar = document.getElementById('kay-node-alignment-toolbar'); + if (toolbar && /^[0-9A-Fa-f]{6}$/.test(newVal)) { + const opacity = app.ui.settings.getSettingValue("KayTool.NodeAlignBackgroundOpacity") / 100; + if (opacity > 0) { + toolbar.style.background = `rgba(${parseInt(newVal.substr(0, 2), 16)}, ${parseInt(newVal.substr(2, 2), 16)}, ${parseInt(newVal.substr(4, 2), 16)}, ${opacity})`; + } else { + toolbar.style.background = ''; + } + } + } + }, + { + id: "KayTool.NodeAlignIconBackgroundColor", + name: "Node Alignment Icon Background Color", + type: "color", + defaultValue: "2b2b2b", + category: ["KayTool", "NodeAlignment", "IconBackgroundColor"], + tooltip: "Use parameter “363636” to match ComfyUI menu bar color for an icon-free background effect", + onChange: (newVal) => { + if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { + const buttons = document.querySelectorAll('.kay-align-button'); + buttons.forEach(btn => btn.style.backgroundColor = `#${newVal}`); + } + } + }, + { + id: "KayTool.NodeAlignIconColor", + name: "Node Alignment Icon Color", + type: "color", + defaultValue: "666666", + category: ["KayTool", "NodeAlignment", "IconColor"], + onChange: (newVal) => { + if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { + const buttons = document.querySelectorAll('.kay-align-button'); + buttons.forEach(btn => { + const svg = btn.querySelector('svg'); + if (svg) svg.querySelectorAll('path').forEach(path => path.setAttribute('fill', `#${newVal}`)); + }); + } + } + }, + { + id: "KayTool.NodeAlignDividerColor", + name: "Node Alignment Divider Color", + type: "color", + defaultValue: "1e1e1e", + category: ["KayTool", "NodeAlignment", "DividerColor"], + onChange: (newVal) => { + if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { + const dividers = document.querySelectorAll('.kay-toolbar-divider'); + dividers.forEach(divider => divider.style.background = `#${newVal}`); + } + } + }, + { + id: "KayTool.EnableResourceMonitor", + name: "Enable Resource Monitor button in menu", + type: "boolean", + defaultValue: true, + category: ["KayTool", "Resource Monitor", "EnableResourceMonitor"], + onChange: (newVal) => { + if (window.KayResourceMonitor) { + window.KayResourceMonitor.updateEnabledState(newVal); + } + } + }, + { + id: "KayTool.ResourceMonitorEdgeFade", + name: "Enable Resource Monitor Chart Edge Fade", + type: "boolean", + defaultValue: true, + category: ["KayTool", "Resource Monitor", "EdgeFade"], + onChange: (newVal) => { + if (window.KayResourceMonitor) { + window.KayResourceMonitor.edgeFadeEnabled = newVal; + if (window.KayResourceMonitor.isVisible && window.KayResourceMonitor.isEnabled) { + window.KayResourceMonitor.drawCurves(); + } + } + } + }, + { + id: "KayTool.ResourceMonitorBackgroundOpacity", + name: "Resource Monitor Background Opacity", + type: "slider", + defaultValue: 80, + attrs: { min: 0, max: 100, step: 1 }, + category: ["KayTool", "Resource Monitor", "BackgroundOpacity"], + onChange: (newVal) => { const opacity = newVal / 100; - if (opacity > 0 && /^[0-9A-Fa-f]{6}$/.test(bgColor)) { - toolbar.style.background = `rgba(${parseInt(bgColor.substr(0, 2), 16)}, ${parseInt(bgColor.substr(2, 2), 16)}, ${parseInt(bgColor.substr(4, 2), 16)}, ${opacity})`; - } else { - toolbar.style.background = ''; + if (window.KayResourceMonitor && window.KayResourceMonitor.toolbar) { + window.KayResourceMonitor.toolbar.style.background = `rgba(0, 0, 0, ${opacity})`; } } } - }); + ]; - app.ui.settings.addSetting({ - id: "KayTool.NodeAlignBackgroundColor", - name: "Node Alignment Toolbar Background Color", - type: "color", - defaultValue: "000000", - category: ["KayTool", "NodeAlignment", "BackgroundColor"], - onChange: (newVal) => { - const toolbar = document.getElementById('kay-node-alignment-toolbar'); - if (toolbar && /^[0-9A-Fa-f]{6}$/.test(newVal)) { - const opacity = app.ui.settings.getSettingValue("KayTool.NodeAlignBackgroundOpacity") / 100; - if (opacity > 0) { - toolbar.style.background = `rgba(${parseInt(newVal.substr(0, 2), 16)}, ${parseInt(newVal.substr(2, 2), 16)}, ${parseInt(newVal.substr(4, 2), 16)}, ${opacity})`; - } else { - toolbar.style.background = ''; - } - } - } - }); - app.ui.settings.addSetting({ - id: "KayTool.NodeAlignIconBackgroundColor", - name: "Node Alignment Icon Background Color", - type: "color", - defaultValue: "2b2b2b", - category: ["KayTool", "NodeAlignment", "IconBackgroundColor"], - tooltip: "Use parameter “363636” to match ComfyUI menu bar color for an icon-free background effect", - onChange: (newVal) => { - if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { - const buttons = document.querySelectorAll('.kay-align-button'); - buttons.forEach(btn => btn.style.backgroundColor = `#${newVal}`); - } - } - }); + for (const setting of settings) { + app.ui.settings.addSetting(setting); - app.ui.settings.addSetting({ - id: "KayTool.NodeAlignIconColor", - name: "Node Alignment Icon Color", - type: "color", - defaultValue: "666666", - category: ["KayTool", "NodeAlignment", "IconColor"], - onChange: (newVal) => { - if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { - const buttons = document.querySelectorAll('.kay-align-button'); - buttons.forEach(btn => { - const svg = btn.querySelector('svg'); - if (svg) svg.querySelectorAll('path').forEach(path => path.setAttribute('fill', `#${newVal}`)); - }); - } + const currentValue = app.ui.settings.getSettingValue(setting.id); + if (currentValue === undefined) { + await app.ui.settings.setSettingValue(setting.id, setting.defaultValue); } - }); - - app.ui.settings.addSetting({ - id: "KayTool.NodeAlignDividerColor", - name: "Node Alignment Divider Color", - type: "color", - defaultValue: "1e1e1e", - category: ["KayTool", "NodeAlignment", "DividerColor"], - onChange: (newVal) => { - if (/^[0-9A-Fa-f]{6}$/.test(newVal)) { - const dividers = document.querySelectorAll('.kay-toolbar-divider'); - dividers.forEach(divider => divider.style.background = `#${newVal}`); - } - } - }); + } }, init() { if (!app.graph) { @@ -222,7 +261,7 @@ function updateSetGetNodeColors() { if (node.type === "KaySetNode" || node.type === "KayGetNode" || node.type === "KayGet多元Node") { node.color = fgColor; node.bgcolor = bgColor; - node.updateColors(); + node.updateColors(); } }); app.graph.setDirtyCanvas(true, true);