From 0b49471ad8e73d91ef75d0e62d74833bd3f28739 Mon Sep 17 00:00:00 2001 From: AEmotionStudio Date: Wed, 31 Dec 2025 17:53:29 -0800 Subject: [PATCH] refactor: migrate magnify glass and info panel to TypeScript with Vite and Vue --- package.json | 25 + playwright.config.ts | 49 + src/components/ComfyPanel.vue | 47 + src/info-panel/EventManager.ts | 181 + src/info-panel/InfoPanel.ts | 146 + src/info-panel/InformationGatherer.ts | 322 ++ src/info-panel/PositionManager.ts | 145 + src/info-panel/StateManager.ts | 404 +++ src/info-panel/UIManager.ts | 839 +++++ src/info-panel/components/InfoPanel.vue | 174 + src/info-panel/index.ts | 7 + src/info-panel/styles.ts | 574 ++++ src/magnify-glass/ConfigManager.ts | 182 + src/magnify-glass/DebugManager.ts | 181 + src/magnify-glass/EventHandler.ts | 232 ++ src/magnify-glass/MagnifierState.ts | 80 + src/magnify-glass/MagnifyGlass.ts | 414 +++ src/magnify-glass/UiManager.ts | 279 ++ src/magnify-glass/WebGLRenderer.ts | 302 ++ src/magnify-glass/index.ts | 13 + src/magnify_glass.ts | 27 + src/magnify_info_panel.ts | 58 + src/shared/constants.ts | 85 + src/shared/index.ts | 3 + src/shared/settings.ts | 700 ++++ src/shared/utils.ts | 161 + src/types/comfy-scripts.d.ts | 11 + src/types/comfyui.d.ts | 264 ++ src/types/vue-shims.d.ts | 5 + tests/e2e/example.spec.ts | 12 + tests/unit/ComfyPanel.test.ts | 29 + tests/unit/InfoPanel.test.ts | 49 + tests/unit/utils.test.ts | 29 + tsconfig.json | 44 + vite.config.ts | 47 + web/_virtual/_plugin-vue_export-helper.js | 11 + web/_virtual/_plugin-vue_export-helper.js.map | 1 + web/info-panel/EventManager.js | 560 +-- web/info-panel/EventManager.js.map | 1 + web/info-panel/InfoPanel.js | 228 +- web/info-panel/InfoPanel.js.map | 1 + web/info-panel/InformationGatherer.js | 560 ++- web/info-panel/InformationGatherer.js.map | 1 + web/info-panel/PositionManager.js | 305 +- web/info-panel/PositionManager.js.map | 1 + web/info-panel/StateManager.js | 659 ++-- web/info-panel/StateManager.js.map | 1 + web/info-panel/UIManager.js | 1387 ++++---- web/info-panel/UIManager.js.map | 1 + web/info-panel/components/InfoPanel.vue.js | 56 + .../components/InfoPanel.vue.js.map | 1 + web/info-panel/components/InfoPanel.vue2.js | 59 + .../components/InfoPanel.vue2.js.map | 1 + web/info-panel/styles.js | 19 +- web/info-panel/styles.js.map | 1 + web/magnify-glass/ConfigManager.js | 246 +- web/magnify-glass/ConfigManager.js.map | 1 + web/magnify-glass/DebugManager.js | 297 +- web/magnify-glass/DebugManager.js.map | 1 + web/magnify-glass/EventHandler.js | 392 +-- web/magnify-glass/EventHandler.js.map | 1 + web/magnify-glass/MagnifierState.js | 106 +- web/magnify-glass/MagnifierState.js.map | 1 + web/magnify-glass/MagnifyGlass.js | 656 ++-- web/magnify-glass/MagnifyGlass.js.map | 1 + web/magnify-glass/UiManager.js | 421 ++- web/magnify-glass/UiManager.js.map | 1 + web/magnify-glass/WebGLRenderer.js | 425 ++- web/magnify-glass/WebGLRenderer.js.map | 1 + web/magnify-glass/index.js | 13 - web/magnify_glass.js | 1720 +--------- web/magnify_glass.js.map | 1 + web/magnify_glass_refactored.js | 58 - web/magnify_info_panel.js | 92 +- web/magnify_info_panel.js.backup | 3035 ----------------- web/magnify_info_panel.js.map | 1 + web/shared/constants.js | 130 +- web/shared/constants.js.map | 1 + web/shared/settings.js | 1083 +++--- web/shared/settings.js.map | 1 + web/shared/utils.js | 165 +- web/shared/utils.js.map | 1 + web/style.css | 80 + 83 files changed, 9628 insertions(+), 9277 deletions(-) create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 src/components/ComfyPanel.vue create mode 100644 src/info-panel/EventManager.ts create mode 100644 src/info-panel/InfoPanel.ts create mode 100644 src/info-panel/InformationGatherer.ts create mode 100644 src/info-panel/PositionManager.ts create mode 100644 src/info-panel/StateManager.ts create mode 100644 src/info-panel/UIManager.ts create mode 100644 src/info-panel/components/InfoPanel.vue create mode 100644 src/info-panel/index.ts create mode 100644 src/info-panel/styles.ts create mode 100644 src/magnify-glass/ConfigManager.ts create mode 100644 src/magnify-glass/DebugManager.ts create mode 100644 src/magnify-glass/EventHandler.ts create mode 100644 src/magnify-glass/MagnifierState.ts create mode 100644 src/magnify-glass/MagnifyGlass.ts create mode 100644 src/magnify-glass/UiManager.ts create mode 100644 src/magnify-glass/WebGLRenderer.ts create mode 100644 src/magnify-glass/index.ts create mode 100644 src/magnify_glass.ts create mode 100644 src/magnify_info_panel.ts create mode 100644 src/shared/constants.ts create mode 100644 src/shared/index.ts create mode 100644 src/shared/settings.ts create mode 100644 src/shared/utils.ts create mode 100644 src/types/comfy-scripts.d.ts create mode 100644 src/types/comfyui.d.ts create mode 100644 src/types/vue-shims.d.ts create mode 100644 tests/e2e/example.spec.ts create mode 100644 tests/unit/ComfyPanel.test.ts create mode 100644 tests/unit/InfoPanel.test.ts create mode 100644 tests/unit/utils.test.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 web/_virtual/_plugin-vue_export-helper.js create mode 100644 web/_virtual/_plugin-vue_export-helper.js.map create mode 100644 web/info-panel/EventManager.js.map create mode 100644 web/info-panel/InfoPanel.js.map create mode 100644 web/info-panel/InformationGatherer.js.map create mode 100644 web/info-panel/PositionManager.js.map create mode 100644 web/info-panel/StateManager.js.map create mode 100644 web/info-panel/UIManager.js.map create mode 100644 web/info-panel/components/InfoPanel.vue.js create mode 100644 web/info-panel/components/InfoPanel.vue.js.map create mode 100644 web/info-panel/components/InfoPanel.vue2.js create mode 100644 web/info-panel/components/InfoPanel.vue2.js.map create mode 100644 web/info-panel/styles.js.map create mode 100644 web/magnify-glass/ConfigManager.js.map create mode 100644 web/magnify-glass/DebugManager.js.map create mode 100644 web/magnify-glass/EventHandler.js.map create mode 100644 web/magnify-glass/MagnifierState.js.map create mode 100644 web/magnify-glass/MagnifyGlass.js.map create mode 100644 web/magnify-glass/UiManager.js.map create mode 100644 web/magnify-glass/WebGLRenderer.js.map delete mode 100644 web/magnify-glass/index.js create mode 100644 web/magnify_glass.js.map delete mode 100644 web/magnify_glass_refactored.js delete mode 100644 web/magnify_info_panel.js.backup create mode 100644 web/magnify_info_panel.js.map create mode 100644 web/shared/constants.js.map create mode 100644 web/shared/settings.js.map create mode 100644 web/shared/utils.js.map create mode 100644 web/style.css diff --git a/package.json b/package.json new file mode 100644 index 0000000..b35e49f --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "comfyui-magnifyglass", + "version": "1.0.0", + "type": "module", + "description": "A professional magnifying glass and info panel for ComfyUI", + "scripts": { + "dev": "vite build --watch", + "build": "vite build", + "typecheck": "tsc --noEmit", + "test:e2e": "playwright test", + "test:unit": "vitest" + }, + "devDependencies": { + "@playwright/test": "^1.57.0", + "@vitejs/plugin-vue": "^6.0.3", + "@vue/test-utils": "^2.4.6", + "happy-dom": "^20.0.11", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "vitest": "^4.0.16" + }, + "dependencies": { + "vue": "^3.5.26" + } +} \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..487223a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,49 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests/e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:8188', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/src/components/ComfyPanel.vue b/src/components/ComfyPanel.vue new file mode 100644 index 0000000..25e0bbb --- /dev/null +++ b/src/components/ComfyPanel.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/src/info-panel/EventManager.ts b/src/info-panel/EventManager.ts new file mode 100644 index 0000000..3f55432 --- /dev/null +++ b/src/info-panel/EventManager.ts @@ -0,0 +1,181 @@ +/** + * ComfyUI MagnifyGlass - Info Panel Event Manager (TypeScript) + * + * Complete Event Manager extracted from magnify_info_panel.js + * Handles all event binding and delegation for the info panel. + */ + +import { StateManager } from './StateManager'; +import { UIManager } from './UIManager'; // Kept for type compatibility if needed, though we use HTMLElement mostly +import { PositionManager } from './PositionManager'; +import { isUserTyping } from '../shared/utils'; + +export interface EventCallbacks { + toggleVisibility: () => void; + togglePin: () => void; + updatePosition: () => void; +} + +/** + * Event Manager class. + * Handles all event listeners for the info panel. + */ +export class EventManager { + stateManager: StateManager; + panelElement: HTMLElement | null; + positionManager: PositionManager; + callbacks: EventCallbacks | null; + + dragCleanup: (() => void) | null; + + boundHandleMouseMove: (e: MouseEvent) => void; + boundHandleKeyDown: (e: KeyboardEvent) => void; + boundHandleKeyUp: (e: KeyboardEvent) => void; + + constructor(stateManager: StateManager, panelElement: HTMLElement | null, positionManager: PositionManager, callbacks?: EventCallbacks) { + this.stateManager = stateManager; + this.panelElement = panelElement; + this.positionManager = positionManager; + this.callbacks = callbacks || null; + this.dragCleanup = null; + + this.boundHandleMouseMove = this.handleMouseMove.bind(this); + this.boundHandleKeyDown = this.handleKeyDown.bind(this); + this.boundHandleKeyUp = this.handleKeyUp.bind(this); + + this.setupEventListeners(); + } + + setupEventListeners(): void { + this.setupPanelEvents(); + this.setupHotkeyEvents(); + this.setupDragEvents(); + this.setupHoverEvents(); + } + + setupPanelEvents(): void { + if (!this.panelElement) return; + + // Vue handles most internal events now. + // We can keep specific global-ish listeners if needed here. + } + + setupHotkeyEvents(): void { + document.addEventListener('keydown', this.boundHandleKeyDown); + } + + setupDragEvents(): void { + if (!this.panelElement) return; + + // Simple drag implementation for the Vue panel + // The dragging logic could be inside Vue, but if we want to update global state/position, we can do it here. + // For now, let's look for the header element. + + // We need to wait for Vue to render the header? + // Since we pass the container, and Vue mounts to it, the header might be inside. + // We can use event delegation on the container. + + const startDrag = (e: MouseEvent) => { + if (!this.panelElement) return; + const target = e.target as HTMLElement; + + // Only allow dragging from header + if (!target.closest('.mag-panel-header')) return; + if (target.closest('button')) return; + + e.preventDefault(); + + const startX = e.clientX; + const startY = e.clientY; + const rect = this.panelElement.getBoundingClientRect(); + const startLeft = rect.left; + const startTop = rect.top; + + // Use requestAnimationFrame for smoother dragging? + const onMouseMove = (moveEvent: MouseEvent) => { + const dx = moveEvent.clientX - startX; + const dy = moveEvent.clientY - startY; + + if (this.panelElement) { + this.panelElement.style.left = `${startLeft + dx}px`; + this.panelElement.style.top = `${startTop + dy}px`; + } + }; + + const onMouseUp = () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + + // Save pinned position + if (this.panelElement) { + const finalRect = this.panelElement.getBoundingClientRect(); + this.stateManager.state.pinnedPosition = { x: finalRect.left, y: finalRect.top }; + // Auto-pin on drag end? + if (!this.stateManager.state.isPanelPinned) { + // callbacks.togglePin? Or just set state? + // Better to let user manually pin, or follow existing logic. + } + } + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; + + this.panelElement.addEventListener('mousedown', startDrag); + + this.dragCleanup = () => { + if (this.panelElement) this.panelElement.removeEventListener('mousedown', startDrag); + }; + } + + setupHoverEvents(): void { + if (!this.panelElement) return; + + this.panelElement.addEventListener('mouseenter', () => { + this.stateManager.state.isPanelHovered = true; + this.stateManager.clearAutoExpandTimer(); + }); + + this.panelElement.addEventListener('mouseleave', () => { + this.stateManager.state.isPanelHovered = false; + if (!this.stateManager.state.isHoveringNode) { + this.stateManager.scheduleAutoCollapse(); + } + }); + } + + handleKeyDown(e: KeyboardEvent): void { + if (isUserTyping()) return; + + const settings = this.stateManager.state.settings; + const key = e.key.toLowerCase(); + // console.log(`[InfoPanel] KeyDown: ${key}`); + + // Toggle visibility + if (key === (settings["🔍MagnifyGlass.ToggleHotkey"] as string).toLowerCase()) { + console.log("[InfoPanel] Toggle visibility hotkey matched!"); + e.preventDefault(); + this.callbacks?.toggleVisibility(); + } + + // Toggle Pin + if (key === (settings["🔍MagnifyGlass.PinPanelHotkey"] as string).toLowerCase()) { + e.preventDefault(); + this.callbacks?.togglePin(); + } + } + + handleMouseMove(e: MouseEvent): void { + // Placeholder + } + + handleKeyUp(e: KeyboardEvent): void { + // Placeholder + } + + cleanup(): void { + document.removeEventListener('keydown', this.boundHandleKeyDown); + if (this.dragCleanup) this.dragCleanup(); + } +} diff --git a/src/info-panel/InfoPanel.ts b/src/info-panel/InfoPanel.ts new file mode 100644 index 0000000..5343414 --- /dev/null +++ b/src/info-panel/InfoPanel.ts @@ -0,0 +1,146 @@ +/** + * ComfyUI MagnifyGlass - Info Panel Orchestrator (TypeScript) + * + * Complete Info Panel Manager extracted from magnify_info_panel.js + * Main class that orchestrates all info panel components. + */ + +import { registerPanelSettings } from '../shared/settings'; +import { StateManager } from './StateManager'; +import { UIManager } from './UIManager'; +import { PositionManager } from './PositionManager'; +import { EventManager } from './EventManager'; +import { InformationGatherer } from './InformationGatherer'; + +/** + * Professional Info Panel Manager. + * Orchestrates all the other components for the info panel. + */ +export class InfoPanel { + magnifyGlass: any; + stateManager: StateManager; + uiManager: UIManager; + positionManager: PositionManager; + eventManager: EventManager; + informationGatherer: InformationGatherer; + + constructor(magnifyGlass: any) { + this.magnifyGlass = magnifyGlass; + + // Initialize Managers + this.stateManager = new StateManager(); + this.uiManager = new UIManager(this.stateManager); + this.informationGatherer = new InformationGatherer(); + + // Position Manager needs the UI elements + this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel!); + + // Event Manager needs state and UI manager + this.eventManager = new EventManager(this.stateManager, this.uiManager.elements.panel!, this.positionManager, { + toggleVisibility: () => { + if (this.stateManager.state.isPanelVisible) { + this.uiManager.hide(); + } else { + this.uiManager.show(); + } + }, + togglePin: () => { + this.stateManager.togglePinning(); + this.uiManager.updatePinnedState(); + }, + updatePosition: () => this.positionManager.positionPanel() + }); + + this.hookIntoMagnifyGlass(); + + // Register settings with ComfyUI + registerPanelSettings(this.stateManager, this.uiManager, this.positionManager); + + console.log("ComfyUI Magnify Info Panel Pro V2: Initialized successfully"); + } + + hookIntoMagnifyGlass(): void { + // Hook into magnify glass update cycle + const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass); + + this.magnifyGlass.updateMagnifiedView = (() => { + originalUpdateMagnifiedView(); + + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.scheduleInfoUpdate(); + } + }).bind(this); + + // Hook into show/hide + const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui); + const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui); + + this.magnifyGlass.ui.show = (() => { + originalShow(); + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + this.uiManager.show(); + // Position controls after showing + setTimeout(() => { + this.positionManager.positionPanel(); + this.positionManager.positionFloatingControls(this.uiManager.elements.controls); + }, 10); + } + }).bind(this); + + this.magnifyGlass.ui.hide = (() => { + originalHide(); + this.uiManager.hide(); + }).bind(this); + } + + scheduleInfoUpdate(): void { + if (this.stateManager.state.updateScheduled) return; + + this.stateManager.state.updateScheduled = true; + requestAnimationFrame(() => { + this.updateInfo(); + this.stateManager.state.updateScheduled = false; + }); + } + + updateInfo(): void { + if (!this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] || !this.magnifyGlass.state.active) return; + + const info = this.informationGatherer.gatherInformation(); + this.stateManager.setCurrentInfo(info); + + // Update the UI + this.uiManager.displayInfo(info); + + this.positionManager.positionPanel(); + this.positionManager.positionFloatingControls(this.uiManager.elements.controls); + + // Handle node hover state logic if needed for internal state + if (info.hoveredNode) { + this.stateManager.state.isHoveringNode = true; + this.stateManager.expandNodeSections(); + if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) { + this.stateManager.state.lastNodeId = info.hoveredNode.id; + } + } else { + this.stateManager.state.isHoveringNode = false; + if (this.stateManager.state.lastNodeId !== null) { + this.stateManager.state.lastNodeId = null; + if (!this.stateManager.state.isPanelHovered) { + this.stateManager.scheduleAutoCollapse(); + } + } + } + } + + updateSettings(): void { + const changes = this.stateManager.updateSettings(); + this.uiManager.applyStyles(); + + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.uiManager.show(); + } else { + this.uiManager.hide(); + } + } +} diff --git a/src/info-panel/InformationGatherer.ts b/src/info-panel/InformationGatherer.ts new file mode 100644 index 0000000..493938e --- /dev/null +++ b/src/info-panel/InformationGatherer.ts @@ -0,0 +1,322 @@ +/** + * ComfyUI MagnifyGlass - Info Panel Information Gatherer (TypeScript) + * + * Complete Information Gatherer extracted from magnify_info_panel.js + * Responsible for collecting information about nodes, widgets, and canvas state. + */ + +// We need to declare the app global variable or import it if the type definition is available +// Assuming types/comfyui.d.ts handles declared const app: ComfyApp; +declare const app: any; + +/** + * Information Gatherer class. + * Collects information about the current state under the cursor. + */ +export class InformationGatherer { + constructor() { } + + gatherInformation(): any { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass) return {}; + + const info: any = { + timestamp: Date.now(), + cursor: { + canvas: { x: magnifyGlass.state.x, y: magnifyGlass.state.y }, + screen: { + x: magnifyGlass.lastKnownMousePosition.x, + y: magnifyGlass.lastKnownMousePosition.y + } + }, + canvas: { + scale: magnifyGlass.state.canvasScale, + offset: { + x: magnifyGlass.state.canvasOffsetX, + y: magnifyGlass.state.canvasOffsetY + } + }, + magnifier: { + zoomFactor: magnifyGlass.config.zoomFactor, + offsetX: magnifyGlass.config.offsetX, + offsetY: magnifyGlass.config.offsetY, + sourceRegion: { + x: magnifyGlass.state.sourceX, + y: magnifyGlass.state.sourceY, + width: magnifyGlass.state.sourceWidth, + height: magnifyGlass.state.sourceHeight + } + }, + hoveredNode: null, + node: null, + widget: null, + connection: null, + media: null + }; + + if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) { + info.media = this.getMediaInfo(magnifyGlass.currentMediaElement); + } + + const nodeUnderCursor = this.getNodeUnderCursor(); + if (nodeUnderCursor) { + info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos); + info.node = this.getNodeInfo(nodeUnderCursor.node); + + const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos); + if (widget) { + info.widget = this.getWidgetInfo(widget); + } + } + + return info; + } + + getNodeUnderCursor(): { node: any, localPos: { x: number, y: number } } | null { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!app.graph || !app.canvas || !magnifyGlass) { + return null; + } + + try { + // Try to get the node from ComfyUI's state + if (app.canvas.node_over) { + const node = app.canvas.node_over; + if (node && node.pos && node.size) { + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; + const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; + + let graphPos: [number, number]; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos = [ + (mouseX / ds.scale) - (ds.offset[0] / ds.scale), + (mouseY / ds.scale) - (ds.offset[1] / ds.scale) + ]; + } + + return { + node: node, + localPos: { + x: graphPos[0] - node.pos[0], + y: graphPos[1] - node.pos[1] + } + }; + } + } + + // Fallback to manual detection + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; + const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; + + let graphPos: [number, number]; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos = [ + (mouseX / ds.scale) - (ds.offset[0] / ds.scale), + (mouseY / ds.scale) - (ds.offset[1] / ds.scale) + ]; + } + + for (let i = app.graph._nodes.length - 1; i >= 0; i--) { + const node = app.graph._nodes[i]; + if (!node.pos || !node.size || (node.flags && node.flags.collapsed)) continue; + + if (graphPos[0] >= node.pos[0] && + graphPos[0] <= node.pos[0] + node.size[0] && + graphPos[1] >= node.pos[1] && + graphPos[1] <= node.pos[1] + node.size[1]) { + + return { + node: node, + localPos: { + x: graphPos[0] - node.pos[0], + y: graphPos[1] - node.pos[1] + } + }; + } + } + } catch (err) { + console.warn("Error in node detection:", err); + } + + return null; + } + + getDetailedNodeInfo(node: any, localPos: { x: number, y: number }): any { + return { + id: node.id, + title: node.title || "Untitled Node", + type: node.type, + mode: this.getNodeModeText(node.mode), + position: { + x: Math.round(node.pos[0]), + y: Math.round(node.pos[1]), + formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` + }, + size: { + width: Math.round(node.size[0]), + height: Math.round(node.size[1]), + formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}` + }, + localPosition: { + x: Math.round(localPos.x), + y: Math.round(localPos.y), + formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`, + percentage: { + x: ((localPos.x / node.size[0]) * 100).toFixed(1), + y: ((localPos.y / node.size[1]) * 100).toFixed(1) + } + }, + counts: { + widgets: node.widgets ? node.widgets.length : 0, + inputs: node.inputs ? node.inputs.length : 0, + outputs: node.outputs ? node.outputs.length : 0, + properties: node.properties ? Object.keys(node.properties).length : 0 + }, + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {}, + hoverRegion: this.detectNodeRegion(localPos, node) + }; + } + + getNodeInfo(node: any): any { + return { + id: node.id, + title: node.title || "Untitled", + type: node.type, + mode: node.mode, + size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : "Unknown", + position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : "Unknown", + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {} + }; + } + + getWidgetUnderCursor(node: any, localPos: { x: number, y: number }): any { + if (!node.widgets || !node.widgets.length) return null; + + const titleHeight = 30; + let currentY = titleHeight; + + for (const widget of node.widgets) { + const widgetHeight = 25; + + if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) { + return widget; + } + + currentY += widgetHeight + 5; + } + + return null; + } + + getWidgetInfo(widget: any): any { + return { + name: widget.name, + type: widget.type, + value: this.formatValue(widget.value), + options: widget.options || null, + min: widget.min, + max: widget.max, + step: widget.step + }; + } + + getMediaInfo(mediaElement: HTMLImageElement | HTMLVideoElement): any { + const info: any = { + tagName: mediaElement.tagName, + src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf('/') + 1) : "No source" + }; + + if (mediaElement instanceof HTMLImageElement) { + info.naturalSize = `${mediaElement.naturalWidth}×${mediaElement.naturalHeight}`; + info.displaySize = `${Math.round(mediaElement.width)}×${Math.round(mediaElement.height)}`; + info.complete = mediaElement.complete; + } else if (mediaElement instanceof HTMLVideoElement) { + info.videoSize = `${mediaElement.videoWidth}×${mediaElement.videoHeight}`; + info.duration = mediaElement.duration ? `${mediaElement.duration.toFixed(2)}s` : "Unknown"; + info.currentTime = `${mediaElement.currentTime.toFixed(2)}s`; + info.paused = mediaElement.paused; + info.readyState = mediaElement.readyState; + } + + return info; + } + + getNodeModeText(mode: number): string { + const modes: { [key: number]: string } = { + 0: "Always", + 1: "On Event", + 2: "Never", + 3: "On Trigger", + 4: "On Request" + }; + return modes[mode] || `Mode ${mode}`; + } + + detectNodeRegion(localPos: { x: number, y: number }, node: any): string { + const titleHeight = 30; + const regions: string[] = []; + + if (localPos.y <= titleHeight) { + regions.push("Title Bar"); + } + + if (localPos.x <= 10) { + regions.push("Left Edge"); + } else if (localPos.x >= node.size[0] - 10) { + regions.push("Right Edge"); + } + + if (localPos.y <= 10) { + regions.push("Top Edge"); + } else if (localPos.y >= node.size[1] - 10) { + regions.push("Bottom Edge"); + } + + if (regions.length === 0) { + if (localPos.y > titleHeight) { + regions.push("Content Area"); + } + } + + if (localPos.x <= 20 && localPos.y > titleHeight) { + regions.push("Input Area"); + } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) { + regions.push("Output Area"); + } + + return regions.length > 0 ? regions.join(", ") : "Unknown"; + } + + formatValue(value: any): string { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") { + return value; // Show full text without truncation + } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); + } +} diff --git a/src/info-panel/PositionManager.ts b/src/info-panel/PositionManager.ts new file mode 100644 index 0000000..05e35af --- /dev/null +++ b/src/info-panel/PositionManager.ts @@ -0,0 +1,145 @@ +/** + * Position Manager class. + * Handles all positioning logic for the info panel. + */ + +import { StateManager } from './StateManager'; + +export class PositionManager { + stateManager: StateManager; + panelElement: HTMLElement; + + constructor(stateManager: StateManager, panelElement: HTMLElement) { + this.stateManager = stateManager; + this.panelElement = panelElement; + } + + positionPanel(): void { + if (!this.panelElement) return; + + // If panel is pinned, keep it at its pinned position + if (this.stateManager.state.isPanelPinned) { + this.applyPinnedPosition(); + return; + } + + // Normal positioning logic + this.calculateNormalPosition(); + } + + applyPinnedPosition(): void { + if (!this.panelElement) return; + + let { x, y } = this.stateManager.state.pinnedPosition; + const panelWidth = this.panelElement.offsetWidth; + const panelHeight = this.panelElement.offsetHeight; + const margin = 10; + + const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin)); + const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin)); + + this.panelElement.style.left = `${boundedX}px`; + this.panelElement.style.top = `${boundedY}px`; + } + + calculateNormalPosition(): void { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass || !this.panelElement) return; + + const settings = this.stateManager.state.settings; + const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; + const panelHeight = this.panelElement.offsetHeight; // Use actual height + + let left: number; + let top: number; + const margin = 15; + + if (!this.stateManager.state.isGlassPreviewVisible) { + left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); + top = magnifyGlass.lastKnownMousePosition.y - 20; + } else { + const glassRect = magnifyGlass.ui.glassDiv?.getBoundingClientRect(); + if (glassRect) { + const position = settings["🔍MagnifyGlass.InfoPanelPosition"]; + switch (position) { + case "Right": + left = glassRect.right + margin; + top = glassRect.top; + break; + case "Left": + left = glassRect.left - panelWidth - margin; + top = glassRect.top; + break; + case "Top": + left = glassRect.left; + top = glassRect.top - panelHeight - margin; + break; + case "Bottom": + left = glassRect.left; + top = glassRect.bottom + margin; + break; + default: + left = glassRect.right + margin; + top = glassRect.top; + break; + } + } else { + left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); + top = magnifyGlass.lastKnownMousePosition.y - 20; + } + } + + left = Math.max(10, Math.min(left, window.innerWidth - panelWidth - 10)); + top = Math.max(10, Math.min(top, window.innerHeight - panelHeight - 10)); + + this.panelElement.style.left = `${left}px`; + this.panelElement.style.top = `${top}px`; + } + + /** + * Position the floating controls relative to the panel. + */ + positionFloatingControls(controlsElement: HTMLElement | null): void { + if (!controlsElement || !this.panelElement) return; + + const panelRect = this.panelElement.getBoundingClientRect(); + const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "right"; + const margin = 8; + + let left: number; + let top: number; + + switch (controlsPosition) { + case "left": + left = panelRect.left - controlsElement.offsetWidth - margin; + top = panelRect.top; + break; + case "right": + left = panelRect.right + margin; + top = panelRect.top; + break; + case "top": + left = panelRect.left; + top = panelRect.top - controlsElement.offsetHeight - margin; + break; + case "bottom": + left = panelRect.left; + top = panelRect.bottom + margin; + break; + case "top-right": + left = panelRect.right - controlsElement.offsetWidth; + top = panelRect.top - controlsElement.offsetHeight - margin; + break; + default: + left = panelRect.right + margin; + top = panelRect.top; + } + + // Keep controls within viewport + left = Math.max(10, Math.min(left, window.innerWidth - controlsElement.offsetWidth - 10)); + top = Math.max(10, Math.min(top, window.innerHeight - controlsElement.offsetHeight - 10)); + + controlsElement.style.left = `${left}px`; + controlsElement.style.top = `${top}px`; + } +} diff --git a/src/info-panel/StateManager.ts b/src/info-panel/StateManager.ts new file mode 100644 index 0000000..7d34969 --- /dev/null +++ b/src/info-panel/StateManager.ts @@ -0,0 +1,404 @@ +/** + * ComfyUI MagnifyGlass - Info Panel State Manager (TypeScript) + * + * Centralized state management for the information panel. + */ + +import { getSettingValue } from '../shared/utils'; +import { DEFAULT_PANEL_SETTINGS } from '../shared/settings'; + +export interface InfoPanelStateData { + // Panel visibility and positioning + isPanelVisible: boolean; + isPanelMinimized: boolean; + isPanelPinned: boolean; + isPanelLocked: boolean; // New lock state + isAutoPinned: boolean; // Track if panel was auto-pinned by glass hide + pinnedPosition: { x: number; y: number }; + lastPinnedPosition: { x: number; y: number } | null; // Remember last pinned location + + // Glass visibility + isGlassPreviewVisible: boolean; + + // Section expansion + expandedSections: Set; + + // Interaction states + isPanelHovered: boolean; + isHoveringNode: boolean; + + // Auto-collapse + autoExpandTimer: ReturnType | null; + lastNodeId: string | number | null; + + // Update scheduling + updateScheduled: boolean; + isInitialLoading: boolean; + + // Current data + currentInfo: any; + + // Settings cache + settings: { [key: string]: any }; + + // Auto-detected theme + currentTheme: string; +} + +/** + * State Manager class. + * Handles all state-related operations and provides clean state transitions. + */ +export class StateManager { + state: InfoPanelStateData; + + constructor() { + this.state = { + // Panel visibility and positioning + isPanelVisible: false, + isPanelMinimized: false, + isPanelPinned: false, + isPanelLocked: false, // New lock state + isAutoPinned: false, // Track if panel was auto-pinned by glass hide + pinnedPosition: { x: 0, y: 0 }, + lastPinnedPosition: null, // Remember last pinned location + + // Glass visibility + isGlassPreviewVisible: true, + + // Section expansion + expandedSections: new Set(['node']), + + // Interaction states + isPanelHovered: false, + isHoveringNode: false, + + // Auto-collapse + autoExpandTimer: null, + lastNodeId: null, + + // Update scheduling + updateScheduled: false, + isInitialLoading: false, + + // Current data + currentInfo: {}, + + // Settings cache + settings: {}, + + // Auto-detected theme + currentTheme: 'dark' // Will be auto-detected + }; + + this.initThemeDetection(); + this.loadSettings(); + } + + initThemeDetection(): void { + // Detect ComfyUI's current theme + this.detectCurrentTheme(); + + // Force an immediate check in case we missed the initial state + setTimeout(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 1000); + + // Set up mutation observer to watch for theme changes + this.setupThemeObserver(); + + // Listen for system theme changes as fallback + if (window.matchMedia) { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); + if (prefersDark.addListener) { + prefersDark.addListener(() => this.detectCurrentTheme()); + } else if (prefersDark.addEventListener) { + prefersDark.addEventListener('change', () => this.detectCurrentTheme()); + } + } + + // Add click listener to theme toggle buttons for immediate detection + this.setupThemeButtonListeners(); + } + + detectCurrentTheme(): boolean { + let detectedTheme = 'dark'; // Default fallback + + const body = document.body; + const html = document.documentElement; + const vueApp = document.querySelector('#vue-app'); + const sidebar = document.querySelector('.comfy-menu, .sidebar, .menu, [class*="sidebar"], [class*="menu"]'); + + // Get all possible theme-related elements + const allElements = [body, html, vueApp, sidebar].filter(Boolean) as Element[]; + + // Check CSS variables + const rootStyles = window.getComputedStyle(html); + const bodyStyles = window.getComputedStyle(body); + + // Method 2: Analyze background colors of multiple elements + const backgrounds = allElements.map(el => { + const styles = window.getComputedStyle(el); + return { + element: el.tagName || el.className, + backgroundColor: styles.backgroundColor, + color: styles.color + }; + }); + + // Method 3: Check for light backgrounds + for (const bg of backgrounds) { + if (bg.backgroundColor && bg.backgroundColor !== 'rgba(0, 0, 0, 0)' && bg.backgroundColor !== 'transparent') { + const rgbMatch = bg.backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + if (brightness > 180) { // Higher threshold for light detection + detectedTheme = 'light'; + break; + } + } + } + } + + // Method 5: Force light theme detection if we see light backgrounds + if (detectedTheme === 'dark') { + // Look for any element with very light background + const lightElements = document.querySelectorAll('*'); + for (let i = 0; i < Math.min(lightElements.length, 50); i++) { // Check first 50 elements + const el = lightElements[i]; + const styles = window.getComputedStyle(el); + const bgColor = styles.backgroundColor; + + if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { + const rgbMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + if (brightness > 200) { // Very light elements + detectedTheme = 'light'; + break; + } + } + } + } + } + + // Update theme if it changed + if (this.state.currentTheme !== detectedTheme) { + this.state.currentTheme = detectedTheme; + return true; // Theme changed + } + + return false; // No change + } + + setupThemeObserver(): void { + // Watch for changes to body/html classes and attributes + const observer = new MutationObserver((mutations) => { + let shouldCheck = false; + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && + (mutation.attributeName === 'class' || + mutation.attributeName === 'data-theme' || + mutation.attributeName === 'style')) { + shouldCheck = true; + } + }); + + if (shouldCheck && this.detectCurrentTheme()) { + // Theme changed, update UI immediately + this.notifyThemeChange(); + } + }); + + // Observe changes to body, html, and vue app + observer.observe(document.body, { attributes: true, attributeOldValue: true }); + observer.observe(document.documentElement, { attributes: true, attributeOldValue: true }); + + const vueApp = document.querySelector('#vue-app'); + if (vueApp) { + observer.observe(vueApp, { attributes: true, attributeOldValue: true }); + } + + // Also watch for changes to any element with theme-related classes + const themeElements = document.querySelectorAll('[class*="theme"], [class*="dark"], [class*="light"], [data-theme]'); + themeElements.forEach(el => { + observer.observe(el, { attributes: true, attributeOldValue: true }); + }); + + // Periodic fallback check every 2 seconds + setInterval(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 2000); + } + + setupThemeButtonListeners(): void { + // Listen for clicks on theme toggle buttons + document.addEventListener('click', (e) => { + // Check if clicked element might be a theme toggle + const target = e.target as HTMLElement; + const isThemeButton = target.textContent?.toLowerCase().includes('theme') || + target.textContent?.toLowerCase().includes('light') || + target.textContent?.toLowerCase().includes('dark') || + target.title?.toLowerCase().includes('theme') || + target.className?.toLowerCase().includes('theme'); + + if (isThemeButton) { + setTimeout(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 100); + } + }); + } + + notifyThemeChange(): void { + // Update settings to reflect new theme + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + + // Notify UI components of theme change immediately + if (window.infoPanelManager && window.infoPanelManager.uiManager) { + window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); + } + } + + loadSettings(): void { + Object.keys(DEFAULT_PANEL_SETTINGS).forEach(key => { + // Skip the theme setting since we auto-detect it + if (key !== "🔍MagnifyGlass.InfoPanelTheme") { + this.state.settings[key] = getSettingValue(key, (DEFAULT_PANEL_SETTINGS as any)[key]); + } + }); + + // Set theme to auto-detected value + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + } + + updateSettings(): { [key: string]: { old: any, new: any } } { + const oldSettings = { ...this.state.settings }; + this.loadSettings(); + + // Return what changed for reactive updates + const changes: { [key: string]: { old: any, new: any } } = {}; + Object.keys(this.state.settings).forEach(key => { + if (oldSettings[key] !== this.state.settings[key]) { + changes[key] = { + old: oldSettings[key], + new: this.state.settings[key] + }; + } + }); + + return changes; + } + + togglePanelVisibility(): boolean { + this.state.isPanelVisible = !this.state.isPanelVisible; + return this.state.isPanelVisible; + } + + toggleGlassPreview(): boolean { + this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible; + return this.state.isGlassPreviewVisible; + } + + togglePinning(): boolean { + const wasPinned = this.state.isPanelPinned; + this.state.isPanelPinned = !this.state.isPanelPinned; + + // When unpinning, also unlock and clear auto-pin flag + if (!this.state.isPanelPinned) { + this.state.isPanelLocked = false; + this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning + } + + if (this.state.isPanelPinned && this.state.lastPinnedPosition) { + // Restore to last pinned position + this.state.pinnedPosition = { ...this.state.lastPinnedPosition }; + } else if (!this.state.isPanelPinned && wasPinned) { + // Save current position when unpinning + this.state.lastPinnedPosition = { ...this.state.pinnedPosition }; + } + + return this.state.isPanelPinned; + } + + toggleLocking(): boolean { + // Only allow locking when pinned + if (!this.state.isPanelPinned) { + return false; + } + + this.state.isPanelLocked = !this.state.isPanelLocked; + return this.state.isPanelLocked; + } + + toggleMinimized(): boolean { + this.state.isPanelMinimized = !this.state.isPanelMinimized; + return this.state.isPanelMinimized; + } + + toggleSection(sectionId: string): boolean { + if (sectionId === 'node') return false; // Node section always expanded + + if (this.state.expandedSections.has(sectionId)) { + this.state.expandedSections.delete(sectionId); + } else { + this.state.expandedSections.add(sectionId); + } + return true; + } + + setPinnedPosition(x: number, y: number): void { + this.state.pinnedPosition = { x, y }; + // Also save as last pinned position for memory + this.state.lastPinnedPosition = { x, y }; + } + + setCurrentInfo(info: any): void { + this.state.currentInfo = info; + } + + scheduleAutoCollapse(): void { + this.clearAutoExpandTimer(); + if (this.state.settings["🔍MagnifyGlass.InfoPanelAnimations"]) { + this.state.autoExpandTimer = setTimeout(() => { + if (!this.state.isPanelHovered) { + this.collapseNodeSections(); + } + }, 1500); + } + } + + clearAutoExpandTimer(): void { + if (this.state.autoExpandTimer) { + clearTimeout(this.state.autoExpandTimer); + this.state.autoExpandTimer = null; + } + } + + expandNodeSections(): void { + this.state.expandedSections.add('hoveredNode'); + this.state.expandedSections.add('node'); + this.state.expandedSections.add('cursor'); + this.state.expandedSections.add('canvas'); + this.state.expandedSections.add('magnifier'); + this.clearAutoExpandTimer(); + } + + collapseNodeSections(): void { + this.state.expandedSections.delete('hoveredNode'); + this.state.expandedSections.delete('node'); + this.state.expandedSections.delete('widget'); + } + + cleanup(): void { + this.clearAutoExpandTimer(); + } +} diff --git a/src/info-panel/UIManager.ts b/src/info-panel/UIManager.ts new file mode 100644 index 0000000..a5f53d5 --- /dev/null +++ b/src/info-panel/UIManager.ts @@ -0,0 +1,839 @@ +/** + * ComfyUI MagnifyGlass - Info Panel UI Manager (TypeScript) + * + * Complete UI Manager extracted from magnify_info_panel.js + * Handles all DOM manipulation and UI creation for the info panel. + */ + +// import { Z_INDEX } from '../shared/constants'; // Not used in original code? Kept for reference if needed +import { getInfoPanelCSS } from './styles'; +import { StateManager } from './StateManager'; + +interface InfoPanelElements { + panel: HTMLDivElement | null; + header: HTMLDivElement | null; + content: HTMLDivElement | null; + controls: HTMLDivElement | null; +} + +/** + * UI Manager class. + * Handles all DOM manipulation and UI creation. + */ +export class UIManager { + stateManager: StateManager; + elements: InfoPanelElements; + + constructor(stateManager: StateManager) { + this.stateManager = stateManager; + this.elements = { + panel: null, + header: null, + content: null, + controls: null + }; + + this.createPanel(); + this.injectStyles(); + } + + /** + * Create the main panel and its components. + */ + createPanel(): void { + // Main panel container + this.elements.panel = document.createElement("div"); + this.elements.panel.id = "comfyui-magnify-info-panel-pro-v2"; + this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`; + + // Header + this.elements.header = document.createElement("div"); + this.elements.header.className = "panel-header"; + this.elements.header.innerHTML = ` +
+
🔍
+
Inspector
+
Real-time analysis
+
+
+ +
+ `; + + // Content container + this.elements.content = document.createElement("div"); + this.elements.content.className = "panel-content"; + + this.elements.panel.appendChild(this.elements.header); + this.elements.panel.appendChild(this.elements.content); + + this.applyStyles(); + document.body.appendChild(this.elements.panel); + + // Create floating controls after panel is in DOM + if (this.stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"]) { + this.createFloatingControls(); + // Update control states immediately after creation + this.updateControlStates(); + } + } + + /** + * Create floating control buttons. + */ + createFloatingControls(): void { + this.elements.controls = document.createElement("div"); + this.elements.controls.className = "floating-controls vertical-layout"; // Default to vertical + this.elements.controls.innerHTML = ` + + + + + `; + + // Insert before the panel in the document body, not as a child + document.body.appendChild(this.elements.controls); + + // Set initial layout based on settings + const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; + this.updateControlsLayout(controlsPosition); + } + + /** + * Update control button states. + */ + updateControlStates(): void { + if (!this.elements.controls) return; + + const pinBtn = this.elements.controls.querySelector('[data-action="pin"]') as HTMLButtonElement; + const lockBtn = this.elements.controls.querySelector('[data-action="lock"]') as HTMLButtonElement; + const visibilityBtn = this.elements.controls.querySelector('[data-action="toggle-panel"]') as HTMLButtonElement; + const glassBtn = this.elements.controls.querySelector('[data-action="toggle-glass"]') as HTMLButtonElement; + + if (pinBtn) { + pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned); + pinBtn.title = this.stateManager.state.isPanelPinned ? "Lock Panel" : "Unlock Panel"; + pinBtn.textContent = this.stateManager.state.isPanelPinned ? "🔒" : "🔓"; + } + + if (lockBtn) { + // Only show/enable lock button when pinned + lockBtn.style.display = this.stateManager.state.isPanelPinned ? 'flex' : 'none'; + lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked); + lockBtn.title = this.stateManager.state.isPanelLocked ? "Upin Panel Position" : "Pin Panel Position"; + lockBtn.disabled = !this.stateManager.state.isPanelPinned; + } + + if (visibilityBtn) { + visibilityBtn.classList.toggle('active', this.stateManager.state.isPanelVisible); + visibilityBtn.title = this.stateManager.state.isPanelVisible ? "Show Panel" : "Hide Panel"; + } + + if (glassBtn) { + glassBtn.classList.toggle('active', this.stateManager.state.isGlassPreviewVisible); + glassBtn.title = this.stateManager.state.isGlassPreviewVisible ? "Hide Glass Preview" : "Show Glass Preview"; + } + } + + /** + * Update controls layout based on position setting. + * @param position + */ + updateControlsLayout(position: string): void { + if (!this.elements.controls) return; + + // Remove existing layout classes + this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout'); + + // Add appropriate layout class + if (['top', 'bottom'].includes(position)) { + this.elements.controls.classList.add('horizontal-layout'); + } else { + this.elements.controls.classList.add('vertical-layout'); + } + } + + /** + * Apply current styles to elements. + */ + applyStyles(): void { + if (!this.elements.panel) return; + + const settings = this.stateManager.state.settings; + + // Apply custom text color via CSS variable if provided + const textColor = settings["🔍MagnifyGlass.InfoPanelTextColor"]; + if (textColor) { + // Ensure color has # symbol + const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`; + this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor); + } + + // Apply custom accent color via CSS variable if provided + const accentColor = settings["🔍MagnifyGlass.InfoPanelAccentColor"]; + if (accentColor) { + // Ensure color has # symbol + const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`; + this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor); + } + + // Apply opacity setting if panel is visible (convert percentage to decimal) + if (this.stateManager.state.isPanelVisible) { + const opacityPercent = settings["🔍MagnifyGlass.InfoPanelOpacity"]; + this.elements.panel.style.opacity = (opacityPercent / 100).toString(); + } + + this.elements.panel.style.cssText = ` + position: absolute; + width: ${settings["🔍MagnifyGlass.InfoPanelWidth"]}px; + max-height: ${settings["🔍MagnifyGlass.InfoPanelMaxHeight"]}px; + z-index: 99999; + display: none; + opacity: 0; + transform: translateY(-10px); + transition: ${settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none'}; + pointer-events: auto; + user-select: none; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + ${textColor ? `--info-panel-text-color: ${textColor.startsWith('#') ? textColor : `#${textColor}`};` : ''} + ${accentColor ? `--info-panel-accent-color: ${accentColor.startsWith('#') ? accentColor : `#${accentColor}`};` : ''} + `; + } + + /** + * Show the panel. + */ + show(): void { + if (!this.elements.panel) return; + + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + this.elements.panel.style.display = "block"; + if (this.elements.controls) { + this.elements.controls.style.display = "flex"; + } + // Apply user's opacity setting when showing (convert percentage to decimal) + const opacityPercent = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"]; + this.elements.panel.style.opacity = (opacityPercent / 100).toString(); + // Trigger reflow + this.elements.panel.offsetHeight; + this.elements.panel.classList.add('visible'); + this.stateManager.state.isPanelVisible = true; + } + } + + /** + * Hide the panel. + */ + hide(): void { + if (!this.elements.panel) return; + + this.elements.panel.classList.remove('visible'); + setTimeout(() => { + if (!this.stateManager.state.isPanelVisible && this.elements.panel) { + this.elements.panel.style.display = "none"; + if (this.elements.controls) { + this.elements.controls.style.display = "none"; + } + } + }, this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 300 : 0); + this.stateManager.state.isPanelVisible = false; + } + + /** + * Update minimized state. + */ + updateMinimizedState(): void { + if (!this.elements.panel || !this.elements.header) return; + + this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized); + const minimizeBtn = this.elements.header.querySelector('.minimize-btn'); + if (minimizeBtn) { + minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−'; + (minimizeBtn as HTMLElement).title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel'; + } + } + + /** + * Update pinned state. + */ + updatePinnedState(): void { + if (!this.elements.panel) return; + + this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned); + this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked); + this.updateControlStates(); + } + + /** + * Update theme class. + * @param newTheme + */ + updateTheme(newTheme: string): void { + if (this.elements.panel) { + this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); + } + } + + /** + * Display information in the panel. + * @param info + */ + displayInfo(info: any): void { + const sections = this.buildSections(info); + this.renderSections(sections); + this.updateSectionStates(); + this.updateHeaderSubtitle(info); + } + + /** + * Build section data from info object. + * @param info + * @returns + */ + buildSections(info: any): any[] { + const sections: any[] = []; + const settings = this.stateManager.state.settings; + + // Inspector section + if (settings["🔍MagnifyGlass.ShowInspectorTab"]) { + const inspectorContent = [ + { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` }, + { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` }, + { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` } + ]; + + sections.push({ + id: 'inspector', + icon: '🔍', + title: 'Inspector', + content: inspectorContent + }); + } + + // Media section + if (info.media) { + const mediaContent = [ + { label: 'Type', value: info.media.tagName }, + { label: 'Source', value: info.media.src } + ]; + + if (info.media.naturalSize) { + mediaContent.push({ label: 'Natural', value: info.media.naturalSize }); + } + + sections.push({ + id: 'media', + icon: '📷', + title: 'Media', + badge: info.media.tagName, + content: mediaContent + }); + } + + // Node Details section + if (info.hoveredNode) { + const nodeContent = [ + { label: 'Title', value: info.hoveredNode.title } + ]; + + // Check if this is a complex node that shows all widgets + const nodeType = info.hoveredNode.type ? info.hoveredNode.type.toLowerCase() : ''; + const isSaveNode = nodeType.includes('save') && + !nodeType.includes('checkpoint') && + !nodeType.includes('model') && + !nodeType.includes('preview'); + + const showAllWidgets = info.hoveredNode.type && ( + nodeType.includes('ksampler') || + nodeType.includes('sampler') || + nodeType.includes('k_samplers') || + nodeType.includes('checkpoint') || + nodeType.includes('model') || + nodeType.includes('lora') || + nodeType.includes('controlnet') || + nodeType.includes('advanced') || + nodeType.includes('detailer') || + nodeType.includes('inpaint') || + nodeType.includes('upscale') || + nodeType.includes('clip') || + nodeType.includes('text') || + nodeType.includes('encode') + ) && !isSaveNode; + + if (!showAllWidgets) { + // Add specific extractions for simple nodes + const checkpointInfo = this.getCheckpointInfo(info.hoveredNode); + if (checkpointInfo) { + nodeContent.push({ label: 'Model', value: checkpointInfo }); + } + + const imageInfo = this.getImageInfo(info.hoveredNode); + if (imageInfo) { + if (typeof imageInfo === 'string') { + nodeContent.push({ label: 'Image', value: imageInfo }); + } else if (typeof imageInfo === 'object' && imageInfo !== null) { + const imgInfoAny = imageInfo as any; + nodeContent.push({ label: 'Image Size', value: `${imgInfoAny.width}×${imgInfoAny.height}` }); + if (imgInfoAny.src) { + nodeContent.push({ label: 'Image Source', value: imgInfoAny.src }); + } + } + } + + const textBoxContent = this.getTextBoxContent(info.hoveredNode); + if (textBoxContent) { + nodeContent.push({ label: 'Text', value: textBoxContent }); + } + } + + const importantParameters = this.getImportantNodeParameters(info.hoveredNode); + nodeContent.push(...importantParameters); + + sections.push({ + id: 'node', + icon: '🎯', + title: 'Node', + badge: info.hoveredNode.type, + content: nodeContent + }); + } + + return sections; + } + + /** + * Render sections to the panel. + * @param sections + */ + renderSections(sections: any[]): void { + if (!this.elements.content) return; + + if (sections.length === 0) { + this.elements.content.innerHTML = ` +
+
📍
+
Empty canvas area
+
+ `; + return; + } + + this.elements.content.innerHTML = sections.map(section => ` +
+
+ ${section.icon} + ${section.title} + ${section.badge ? `${section.badge}` : ''} + ${section.id !== 'node' ? '▶' : ''} +
+
+
+ ${section.content.map((item: any) => { + const value = this.formatValue(item.value, item.label); + const valueClass = this.getValueClass(item.value); + const valueAttributes = this.getValueAttributes(item.value); + return ` +
+ ${item.label} + ${value} +
`; + }).join('')} +
+
+
+ `).join(''); + } + + /** + * Update section expansion states. + */ + updateSectionStates(): void { + if (!this.elements.content) return; + + const sections = this.elements.content.querySelectorAll('.info-section'); + sections.forEach(section => { + const sectionId = (section as HTMLElement).dataset.section; + if (!sectionId) return; + + const header = section.querySelector('.section-header'); + const content = section.querySelector('.section-content'); + + if (header && content) { + if (this.stateManager.state.expandedSections.has(sectionId)) { + header.classList.add('expanded'); + content.classList.add('expanded'); + } else { + header.classList.remove('expanded'); + content.classList.remove('expanded'); + } + } + }); + } + + /** + * Format a value for display. + * @param value + * @param label + * @returns + */ + formatValue(value: any, label?: string): string { + if (value === null || value === undefined) return ''; + + const str = String(value); + + // Show full text for prompts, text content, model names, and file paths + if (label && ( + label.toLowerCase().includes('text') || + label.toLowerCase().includes('prompt') || + label.toLowerCase().includes('model') || + label.toLowerCase().includes('file') || + label.toLowerCase().includes('conditioning') || + label.toLowerCase().includes('positive') || + label.toLowerCase().includes('negative') + )) { + return str; + } + + // Show full text for very long values (no truncation) + return str; + } + + /** + * Get value class for styling. + * @param value + * @returns + */ + getValueClass(value: any): string { + if (!value) return ''; + + const str = String(value); + let classes: string[] = []; + + // Mark text that might benefit from special styling for readability + if (str.length > 100) { + classes.push('long-text'); + } + + return classes.join(' '); + } + + /** + * Get value attributes. + * @param value + * @returns + */ + getValueAttributes(value: any): string { + // Since we show full text now, we don't need title attributes for long text + // Only add title for very long text that might benefit from tooltips + if (!value) return ''; + + const str = String(value); + if (str.length > 500) { // Only for extremely long text + return `title="${str.replace(/"/g, '"')}"`; + } + + return ''; + } + + /** + * Update header subtitle. + * @param info + */ + updateHeaderSubtitle(info: any): void { + if (!this.elements.header) return; + + const subtitleElement = this.elements.header.querySelector('.header-subtitle') as HTMLElement; + if (subtitleElement) { + const accentColor = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"]; + if (info.hoveredNode) { + subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`; + subtitleElement.style.color = accentColor || ''; + } else if (info.media) { + subtitleElement.textContent = `Media: ${info.media.tagName}`; + subtitleElement.style.color = accentColor || ''; + } else if (info.connection) { + subtitleElement.textContent = `Connection: ${info.connection.type}`; + subtitleElement.style.color = accentColor || ''; + } else { + subtitleElement.textContent = 'Real-time analysis'; + subtitleElement.style.color = ''; + } + } + } + + // ============== Helper methods for node information extraction ============== + + /** + * Get checkpoint/model info from a node. + * @param nodeInfo + * @returns + */ + getCheckpointInfo(nodeInfo: any): string | null { + if (nodeInfo.type && ( + nodeInfo.type.includes("CheckpointLoader") || + nodeInfo.type.includes("LoadCheckpoint") || + nodeInfo.type.includes("ModelLoader") || + nodeInfo.type.includes("UNETLoader") || + nodeInfo.type.includes("VAELoader") || + nodeInfo.type.includes("LoraLoader") + )) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + if (widget.name && ( + widget.name.toLowerCase().includes("model") || + widget.name.toLowerCase().includes("checkpoint") || + widget.name.toLowerCase().includes("ckpt") || + widget.name.toLowerCase().includes("lora") || + widget.name.toLowerCase().includes("vae") || + widget.name.toLowerCase().includes("file") + )) { + const value = String(widget.value); + const filename = value.split(/[\/\\]/).pop(); + return filename || value; + } + } + } + } + return null; + } + + /** + * Get image info from a node. + * @param nodeInfo + * @returns + */ + getImageInfo(nodeInfo: any): string | object | null { + if (nodeInfo.type && ( + nodeInfo.type.includes("SaveImage") || + nodeInfo.type.includes("PreviewImage") || + nodeInfo.type.includes("VisionOutput") || + nodeInfo.type.includes("ImageOutput") || + nodeInfo.type.includes("LoadImage") || + nodeInfo.type.includes("Display") + )) { + if (nodeInfo.widgets) { + for (const widget of nodeInfo.widgets) { + if (widget.name && ( + widget.name.toLowerCase().includes("image") || + widget.name.toLowerCase().includes("filename") || + widget.name.toLowerCase().includes("file") + )) { + return widget.value; + } + } + } + + if (nodeInfo.properties && nodeInfo.properties.img) { + const img = nodeInfo.properties.img; + return { + width: img.width || "unknown", + height: img.height || "unknown", + src: img.src ? img.src.split(/[\/\\]/).pop() : "Preview available" + }; + } + + if (nodeInfo.outputs) { + for (const output of nodeInfo.outputs) { + if (output.links && output.links.length > 0) { + return "Image connected to " + output.links.length + " node(s)"; + } + } + } + + return "Image node"; + } + return null; + } + + /** + * Get text box content from a node. + * @param nodeInfo + * @returns + */ + getTextBoxContent(nodeInfo: any): string | null { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + if (nodeInfo.type && nodeInfo.type.includes("CLIPTextEncode")) { + for (const widget of nodeInfo.widgets) { + if (widget.name === "text" && typeof widget.value === 'string') { + return widget.value; + } + } + } + + for (const widget of nodeInfo.widgets) { + if ((widget.name.toLowerCase().includes("prompt") || + widget.name.toLowerCase().includes("conditioning")) && + typeof widget.value === 'string' && widget.value.length > 0) { + return widget.value; + } + } + + for (const widget of nodeInfo.widgets) { + if ((widget.type === 'text' || widget.type === 'textarea' || + widget.type === 'string' || widget.name.toLowerCase().includes('text')) && + typeof widget.value === 'string' && widget.value.length > 0) { + + // Show full text without truncation + return widget.value; + } + } + } + return null; + } + + /** + * Get important node parameters based on node type. + * @param nodeInfo + * @returns + */ + getImportantNodeParameters(nodeInfo: any): any[] { + const parameters: any[] = []; + + // For complex nodes that have many parameters, show ALL widgets + const nodeType = nodeInfo.type ? nodeInfo.type.toLowerCase() : ''; + const isSaveNode = nodeType.includes('save') && + !nodeType.includes('checkpoint') && + !nodeType.includes('model') && + !nodeType.includes('preview'); + + const showAllWidgets = nodeInfo.type && ( + nodeType.includes('ksampler') || + nodeType.includes('sampler') || + nodeType.includes('k_samplers') || + nodeType.includes('checkpoint') || + nodeType.includes('model') || + nodeType.includes('lora') || + nodeType.includes('controlnet') || + nodeType.includes('advanced') || + nodeType.includes('detailer') || + nodeType.includes('inpaint') || + nodeType.includes('upscale') || + nodeType.includes('clip') || + nodeType.includes('text') || + nodeType.includes('encode') + ) && !isSaveNode; + + if (showAllWidgets) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + // Show all widgets for complex nodes, but filter out some duplicates + if (widget.name && widget.name !== '') { + const widgetName = widget.name.toLowerCase(); + + // Skip some generic widget names that might be duplicates + if (widgetName.includes('title') || + widgetName === 'node' || + widgetName === 'id' || + widgetName === 'type' || + widgetName === 'mode') { + continue; + } + + parameters.push({ + label: widget.name, + value: this.formatWidgetValue(widget.value) + }); + } + } + } + return parameters; + } + + // Special handling for Save nodes - only show essential parameters + let importantParams: string[]; + if (isSaveNode) { + importantParams = [ + 'filename_prefix', 'filename', 'directory', 'path', + 'format', 'quality', 'extension' + ]; + } else { + // For other nodes, use the filtered list but avoid duplicates with specific extractions + importantParams = [ + 'seed', 'steps', 'cfg', 'scale', 'sampler', 'scheduler', + 'positive', 'negative', 'width', 'height', 'denoise', 'strength', + 'noise', 'count', 'batch', 'size', 'phase', 'color', 'intensity', + // KSampler specific parameters (for other samplers) + 'control_after_generate', 'control', 'after', 'generate', + 'start_at_step', 'end_at_step', 'start', 'end', + 'return_with_leftover_noise', 'leftover', 'noise_return', + // Additional common parameters (but avoid duplicates with specific extractors) + 'model', 'vae', 'clip', 'lora', 'checkpoint', + 'latent', 'image', 'mask', 'filename', 'directory', + 'prompt', 'conditioning', 'filename_prefix', + // New detection vocabulary + 'resolution', 'num_chunks', 'seconds', 'aspect_ratio', + 'style_type', 'background', 'n', 'human', 'raw', 'guidance', + 'skip_preprocessing', 'movement_amplitude', 'animation', + 'material_type', 'b1', 'b2', 's1', 's2', 'type', 'channel', + 'sigma', 'rho', + // Additional detection vocabulary + 'alpha', 'base_shift', 'shift', 'stretch', 'terminal', + 'spacing', 'style', 'eta', 'norm_threshold', 'momentum', + 'hypernetwork_name', 'reuse_threshold', 'verbose', 'layers', + 'set_cond_area', 'audioui', + // Camera and 3D parameters + 'camera_pose', 'fx', 'cx', 'fy', 'cy' + // Note: 'text' and 'string' removed to avoid duplication with getTextBoxContent + ]; + } + + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + const paramName = widget.name.toLowerCase(); + if (importantParams.some(param => paramName.includes(param))) { + parameters.push({ + label: widget.name, + value: this.formatWidgetValue(widget.value) + }); + } + } + } + + return parameters; + } + + /** + * Format widget value for display. + * @param value + * @returns + */ + formatWidgetValue(value: any): string { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") { + return value; // Show full text without truncation + } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); + } + + /** + * Inject CSS styles. + */ + injectStyles(): void { + if (!document.getElementById('magnify-info-panel-styles-v2')) { + const style = document.createElement('style'); + style.id = 'magnify-info-panel-styles-v2'; + style.textContent = getInfoPanelCSS(); + document.head.appendChild(style); + } + } + + cleanup(): void { + if (this.elements.panel && this.elements.panel.parentNode) { + this.elements.panel.parentNode.removeChild(this.elements.panel); + } + if (this.elements.controls && this.elements.controls.parentNode) { + this.elements.controls.parentNode.removeChild(this.elements.controls); + } + } +} diff --git a/src/info-panel/components/InfoPanel.vue b/src/info-panel/components/InfoPanel.vue new file mode 100644 index 0000000..aec8e0d --- /dev/null +++ b/src/info-panel/components/InfoPanel.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/src/info-panel/index.ts b/src/info-panel/index.ts new file mode 100644 index 0000000..e4698ff --- /dev/null +++ b/src/info-panel/index.ts @@ -0,0 +1,7 @@ +export * from './StateManager'; +export * from './UIManager'; +export * from './PositionManager'; +export * from './InformationGatherer'; +export * from './EventManager'; +export * from './InfoPanel'; +export * from './styles'; diff --git a/src/info-panel/styles.ts b/src/info-panel/styles.ts new file mode 100644 index 0000000..cd70e90 --- /dev/null +++ b/src/info-panel/styles.ts @@ -0,0 +1,574 @@ +/** + * ComfyUI MagnifyGlass - Info Panel Styles (TypeScript) + * + * Complete CSS styles for the info panel component. + */ + +/** + * Get the complete CSS for the info panel. + * @returns CSS string + */ +export function getInfoPanelCSS(): string { + return ` + .magnify-info-panel { + border-radius: 12px; + backdrop-filter: blur(20px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1); + overflow: hidden; + font-size: 16px; + line-height: 1.6; + pointer-events: auto; + } + + .magnify-info-panel * { + pointer-events: auto; + } + + .magnify-info-panel.theme-dark { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96)); + color: var(--info-panel-text-color, #e0e0e0); + border: 1px solid rgba(100, 100, 120, 0.4); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(12px); + } + + .magnify-info-panel.theme-light { + background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96)); + color: var(--info-panel-text-color, #2a2a2a); + border: 1px solid rgba(200, 200, 220, 0.5); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(12px); + } + + .magnify-info-panel.visible { + transform: translateY(0) !important; + } + + .magnify-info-panel.panel-pinned { + cursor: grab; + box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); + } + + .magnify-info-panel.panel-locked { + cursor: default !important; + box-shadow: 0 0 0 2px rgba(255, 69, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); + } + + .magnify-info-panel.panel-locked::after { + content: '📌'; + position: absolute; + top: 8px; + right: 8px; + font-size: 14px; + opacity: 0.7; + pointer-events: none; + z-index: 1; + } + + .magnify-info-panel.panel-pinned.panel-dragging { + cursor: grabbing; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(255, 215, 0, 0.7); + transform: scale(1.02) !important; + z-index: 100001 !important; + transition: none !important; + } + + .magnify-info-panel.panel-pinned::before { + content: ''; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: linear-gradient(45deg, rgba(255, 215, 0, 0.05) 0%, transparent 100%); + pointer-events: none; + border-radius: inherit; + } + + .panel-dragging * { + pointer-events: none !important; + user-select: none !important; + transition: none !important; + } + + .magnify-info-panel.panel-minimized .panel-content { + max-height: 0 !important; + overflow: hidden; + transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + .floating-controls { + position: fixed; + display: flex; + padding: 4px; + background: rgba(40, 40, 50, 0.95); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 4px; + opacity: 1; + z-index: 100000; + backdrop-filter: blur(8px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + } + + .floating-controls.vertical-layout { + flex-direction: column; + gap: 4px; + } + + .floating-controls.horizontal-layout { + flex-direction: row; + gap: 4px; + } + + .control-btn { + background: rgba(60, 60, 70, 0.8); + border: 1px solid rgba(255, 255, 255, 0.2); + width: 28px; + height: 28px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #ddd; + border-radius: 3px; + transition: background-color 0.15s ease; + } + + .control-btn:hover { + background: rgba(80, 80, 90, 0.9); + color: white; + } + + .control-btn:active { + background: rgba(100, 100, 110, 0.9); + } + + .control-btn:disabled { + background: rgba(40, 40, 50, 0.5); + color: #666; + cursor: not-allowed; + opacity: 0.5; + } + + .control-btn.active { + background: rgba(255, 200, 50, 0.3); + border-color: rgba(255, 200, 50, 0.6); + color: #ffcc33; + } + + .control-btn.lock-btn.active { + background: rgba(255, 69, 0, 0.3); + border-color: rgba(255, 69, 0, 0.6); + color: #ff4500; + } + + .panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + background: linear-gradient(135deg, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.05)); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + user-select: none; + } + + .header-content { + display: flex; + align-items: center; + gap: 10px; + } + + .header-icon { + font-size: 18px; + opacity: 0.9; + } + + .header-title { + font-weight: 600; + font-size: 18px; + } + + .header-subtitle { + font-size: 14px; + opacity: 0.7; + font-style: italic; + } + + .header-controls { + display: flex; + gap: 6px; + } + + .minimize-btn { + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 6px; + color: inherit; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + font-weight: bold; + font-size: 16px; + } + + .minimize-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); + } + + .minimize-btn:active { + transform: scale(0.95); + } + + .panel-content { + padding: 0; + max-height: calc(100% - 60px); + overflow-y: auto; + scrollbar-width: thin; + } + + .panel-content::-webkit-scrollbar { + width: 8px; + } + + .panel-content::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + } + + .panel-content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 3px; + } + + .info-section { + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 8px; + border-radius: 10px; + overflow: visible; + background: rgba(255, 255, 255, 0.02); + transition: all 0.3s ease; + } + + .info-section:hover { + background: rgba(255, 255, 255, 0.04); + transform: translateY(-1px); + } + + .section-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-weight: 500; + border-radius: 8px; + margin: 3px; + position: relative; + overflow: hidden; + } + + .section-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.05) 50%, transparent 100%); + transform: translateX(-100%); + transition: transform 0.6s ease; + } + + .section-header:hover::before { + transform: translateX(100%); + } + + .section-header:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateX(2px); + box-shadow: inset 2px 0 0 rgba(116, 185, 255, 0.5); + } + + .section-header:active { + background: rgba(255, 255, 255, 0.12); + transform: translateX(1px); + } + + .section-header.expanded { + background: rgba(116, 185, 255, 0.1); + border-left: 2px solid var(--info-panel-accent-color, #74b9ff); + } + + .info-section[data-section="node"] { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.05), rgba(160, 212, 104, 0.05)); + border: 1px solid rgba(116, 185, 255, 0.2); + border-radius: 8px; + margin: 6px; + } + + .info-section[data-section="node"] .section-header { + background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(160, 212, 104, 0.15)); + border-radius: 8px 8px 0 0; + cursor: default; + } + + .info-section[data-section="node"] .section-header:hover { + transform: none; + box-shadow: none; + } + + .info-section[data-section="node"] .section-title { + color: var(--info-panel-accent-color, #74b9ff); + font-weight: 700; + } + + .info-section[data-section="inspector"] { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.05), rgba(74, 144, 226, 0.05)); + border: 1px solid rgba(100, 150, 255, 0.2); + border-radius: 8px; + margin: 6px; + } + + .info-section[data-section="inspector"] .section-header { + background: linear-gradient(135deg, rgba(100, 150, 255, 0.15), rgba(74, 144, 226, 0.15)); + border-radius: 8px 8px 0 0; + } + + .info-section[data-section="inspector"] .section-title { + color: #6496ff; + font-weight: 700; + } + + .info-section[data-section="media"] { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.05), rgba(255, 215, 0, 0.05)); + border: 1px solid rgba(253, 203, 110, 0.2); + border-radius: 8px; + margin: 6px; + } + + .info-section[data-section="media"] .section-header { + background: linear-gradient(135deg, rgba(253, 203, 110, 0.15), rgba(255, 215, 0, 0.15)); + border-radius: 8px 8px 0 0; + } + + .info-section[data-section="media"] .section-title { + color: var(--info-panel-accent-color, #fdcb6e); + font-weight: 700; + } + + .section-icon { + font-size: 18px; + width: 22px; + text-align: center; + } + + .section-title { + flex: 1; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.3px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + + .section-badge { + background: rgba(100, 150, 255, 0.2); + color: #8bb3ff; + padding: 3px 10px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + } + + .expand-icon { + font-size: 14px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + color: rgba(255, 255, 255, 0.6); + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: rgba(255, 255, 255, 0.05); + margin-left: auto; + } + + .section-header:hover .expand-icon { + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(116, 185, 255, 0.15); + transform: scale(1.1); + } + + .section-header.expanded .expand-icon { + transform: rotate(90deg) scale(1.1); + color: var(--info-panel-accent-color, #74b9ff); + background: rgba(116, 185, 255, 0.2); + } + + .section-content { + max-height: 0; + overflow: visible; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; + opacity: 0; + } + + .section-content.expanded { + max-height: 2000px; + opacity: 1; + overflow: visible; + } + + .section-body { + padding: 0 18px 16px 24px; + } + + .info-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 10px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + transition: all 0.2s ease; + border-radius: 4px; + margin: 0 -8px; + padding-left: 8px; + padding-right: 8px; + } + + .info-row:last-child { + border-bottom: none; + } + + .info-row:hover { + background: rgba(255, 255, 255, 0.03); + border-bottom-color: rgba(255, 255, 255, 0.08); + } + + .info-label { + font-weight: 500; + opacity: 0.85; + font-size: 15px; + min-width: 80px; + padding-right: 12px; + line-height: 1.5; + } + + .info-value { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + color: var(--info-panel-accent-color, #a0d468); + background: rgba(160, 212, 104, 0.12); + padding: 4px 10px; + border-radius: 6px; + max-width: 100%; + word-wrap: break-word; + word-break: break-word; + hyphens: auto; + line-height: 1.4; + text-align: left; + white-space: pre-wrap; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; + } + + .info-value:hover { + background: rgba(160, 212, 104, 0.18); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + } + + .info-value.long-text { + white-space: pre-wrap; + word-wrap: break-word; + max-width: 100%; + line-height: 1.4; + } + + .empty-state { + text-align: center; + padding: 50px 25px; + opacity: 0.6; + } + + .empty-state-icon { + font-size: 36px; + margin-bottom: 15px; + opacity: 0.5; + } + + /* Copy to clipboard functionality */ + .info-value[title]:hover::after { + content: '📋'; + position: absolute; + right: -20px; + top: 50%; + transform: translateY(-50%); + font-size: 12px; + opacity: 0.7; + } + + /* Smooth loading states */ + .section-body.loading { + opacity: 0.5; + pointer-events: none; + } + + .section-body.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + border: 2px solid transparent; + border-top: 2px solid var(--info-panel-accent-color, #74b9ff); + border-radius: 50%; + animation: spin 1s linear infinite; + transform: translate(-50%, -50%); + } + + @keyframes spin { + to { transform: translate(-50%, -50%) rotate(360deg); } + } + + /* Enhanced scrollbar for better UX */ + .section-body::-webkit-scrollbar { + width: 4px; + } + + .section-body::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 2px; + } + + .section-body::-webkit-scrollbar-thumb { + background: var(--info-panel-accent-color, #74b9ff); + border-radius: 2px; + opacity: 0.6; + } + + .section-body::-webkit-scrollbar-thumb:hover { + opacity: 1; + } + + .empty-state-text { + font-size: 16px; + font-style: italic; + } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + + .pulse { + animation: pulse 2s infinite; + } + `; +} diff --git a/src/magnify-glass/ConfigManager.ts b/src/magnify-glass/ConfigManager.ts new file mode 100644 index 0000000..b402f1b --- /dev/null +++ b/src/magnify-glass/ConfigManager.ts @@ -0,0 +1,182 @@ +/** + * ComfyUI MagnifyGlass - ConfigManager (TypeScript) + * + * Manages configuration and settings for the magnifying glass. + */ + +import { getSettingValue } from '../shared/utils'; +import { STORAGE_KEYS } from '../shared/constants'; +import type { GlassPosition, GlassShape, TextureFilter } from '../shared/constants'; + +// Default settings (we'll import these properly later) +const DEFAULT_GLASS_SETTINGS = { + "🔍MagnifyGlass.ZoomFactor": 300, + "🔍MagnifyGlass.GlassSize": 300, + "🔍MagnifyGlass.BorderColor": "#6b7280", + "🔍MagnifyGlass.BorderWidth": 1, + "🔍MagnifyGlass.ActivationKey": "x", + "🔍MagnifyGlass.AltRequired": false, + "🔍MagnifyGlass.FollowCursor": false, + "🔍MagnifyGlass.DebugMode": false, + "🔍MagnifyGlass.OffsetStep": 5, + "🔍MagnifyGlass.GlassPosition": "Top-Right" as GlassPosition, + "🔍MagnifyGlass.ResetKey": "o", + "🔍MagnifyGlass.GlassShape": "Rounded Square" as GlassShape, + "🔍MagnifyGlass.BorderEnabled": true, + "🔍MagnifyGlass.TextureFiltering": "Linear" as TextureFilter, + "🔍MagnifyGlass.AlwaysActiveMode": true, + "🔍MagnifyGlass.ToggleFollowCursorKey": "h", +}; + +/** + * Configuration manager class. + * Reads settings from ComfyUI and manages persistent offsets. + */ +export class ConfigManager { + /** Zoom factor (1.0 = 100%, 3.0 = 300%) */ + zoomFactor: number; + + /** Glass size in pixels */ + glassSize: number; + + /** Border color (CSS color string) */ + borderColor: string; + + /** Border width in pixels */ + borderWidth: number; + + /** Activation key (lowercase) */ + activationKey: string; + + /** Whether Alt key is required for activation */ + altRequired: boolean; + + /** Whether glass follows cursor */ + followCursor: boolean; + + /** Debug mode enabled */ + debugMode: boolean; + + /** Offset step for arrow key adjustments */ + offsetStep: number; + + /** Glass position relative to cursor */ + glassPosition: string; + + /** Reset key (lowercase) */ + resetKey: string; + + /** Glass shape */ + glassShape: string; + + /** Whether border is enabled */ + borderEnabled: boolean; + + /** Texture filtering mode */ + textureFiltering: string; + + /** Always active mode */ + alwaysActiveMode: boolean; + + /** Toggle follow cursor key */ + toggleFollowCursorKey: string; + + /** Manual offset X in graph units */ + offsetX: number; + + /** Manual offset Y in graph units */ + offsetY: number; + + constructor() { + // Initialize properties with defaults + this.zoomFactor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ZoomFactor"] / 100; + this.glassSize = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassSize"]; + this.borderColor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderColor"]; + this.borderWidth = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderWidth"]; + this.activationKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ActivationKey"]; + this.altRequired = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.AltRequired"]; + this.followCursor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.FollowCursor"]; + this.debugMode = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.DebugMode"]; + this.offsetStep = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.OffsetStep"]; + this.glassPosition = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassPosition"]; + this.resetKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ResetKey"]; + this.glassShape = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassShape"]; + this.borderEnabled = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderEnabled"]; + this.textureFiltering = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.TextureFiltering"]; + this.alwaysActiveMode = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.AlwaysActiveMode"]; + this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ToggleFollowCursorKey"]; + + // Alignment adjustment parameters - managed separately + this.offsetX = 0; + this.offsetY = 0; + } + + /** + * Load settings from ComfyUI settings system. + */ + loadSettings(): void { + this.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", this.zoomFactor * 100) / 100; + this.glassSize = getSettingValue("🔍MagnifyGlass.GlassSize", this.glassSize); + this.borderColor = getSettingValue("🔍MagnifyGlass.BorderColor", this.borderColor); + this.borderWidth = getSettingValue("🔍MagnifyGlass.BorderWidth", this.borderWidth); + this.activationKey = getSettingValue("🔍MagnifyGlass.ActivationKey", this.activationKey); + this.altRequired = getSettingValue("🔍MagnifyGlass.AltRequired", this.altRequired); + this.followCursor = getSettingValue("🔍MagnifyGlass.FollowCursor", this.followCursor); + this.debugMode = getSettingValue("🔍MagnifyGlass.DebugMode", this.debugMode); + this.offsetStep = getSettingValue("🔍MagnifyGlass.OffsetStep", this.offsetStep); + this.glassPosition = getSettingValue("🔍MagnifyGlass.GlassPosition", this.glassPosition); + this.resetKey = getSettingValue("🔍MagnifyGlass.ResetKey", this.resetKey); + this.glassShape = getSettingValue("🔍MagnifyGlass.GlassShape", this.glassShape); + this.borderEnabled = getSettingValue("🔍MagnifyGlass.BorderEnabled", this.borderEnabled); + this.textureFiltering = getSettingValue("🔍MagnifyGlass.TextureFiltering", this.textureFiltering); + this.alwaysActiveMode = getSettingValue("🔍MagnifyGlass.AlwaysActiveMode", this.alwaysActiveMode); + this.toggleFollowCursorKey = getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", this.toggleFollowCursorKey); + } + + /** + * Load saved offsets from localStorage. + */ + loadSavedOffsets(): void { + try { + const savedOffsetX = localStorage.getItem(STORAGE_KEYS.OFFSET_X); + const savedOffsetY = localStorage.getItem(STORAGE_KEYS.OFFSET_Y); + + if (savedOffsetX !== null) { + this.offsetX = parseInt(savedOffsetX, 10); + } else { + this.offsetX = 0; + } + + if (savedOffsetY !== null) { + this.offsetY = parseInt(savedOffsetY, 10); + } else { + this.offsetY = 0; + } + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error loading saved offsets:", e); + this.offsetX = 0; + this.offsetY = 0; + } + } + + /** + * Save current offsets to localStorage. + */ + saveOffsets(): void { + try { + localStorage.setItem(STORAGE_KEYS.OFFSET_X, this.offsetX.toString()); + localStorage.setItem(STORAGE_KEYS.OFFSET_Y, this.offsetY.toString()); + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error saving offsets:", e); + } + } + + /** + * Reset offsets to default (zero). + */ + resetOffsets(): void { + this.offsetX = 0; + this.offsetY = 0; + this.saveOffsets(); + } +} diff --git a/src/magnify-glass/DebugManager.ts b/src/magnify-glass/DebugManager.ts new file mode 100644 index 0000000..8b98da7 --- /dev/null +++ b/src/magnify-glass/DebugManager.ts @@ -0,0 +1,181 @@ +/** + * ComfyUI MagnifyGlass - DebugManager (TypeScript) + * + * Handles debug visualization and logging. + */ + +import type { ComfyApp } from '../types/comfyui'; +import type { ConfigManager } from './ConfigManager'; +import type { MagnifierState } from './MagnifierState'; +import type { UiManager } from './UiManager'; + +declare const app: ComfyApp; + +/** + * Debug Manager class. + * Provides debug logging and canvas visualization. + */ +export class DebugManager { + config: ConfigManager; + state: MagnifierState; + ui: UiManager; + + constructor(config: ConfigManager, state: MagnifierState, ui: UiManager) { + this.config = config; + this.state = state; + this.ui = ui; + } + + /** + * Log a message if debug mode is enabled. + */ + log(...args: unknown[]): void { + if (this.config.debugMode) { + console.log("ComfyUI Magnifying Glass:", ...args); + } + } + + /** + * Log an error message. + */ + error(...args: unknown[]): void { + console.error("ComfyUI Magnifying Glass ERROR:", ...args); + } + + /** + * Print detailed canvas information for debugging. + */ + printCanvasInfo(): void { + if (!this.config.debugMode) return; + + try { + // Using any for deep property access on ComfyUI objects that might vary + const canvasManager = (app as any).canvas; + const canvas = canvasManager?.graph_canvas as HTMLCanvasElement; + + if (!canvas) { + this.log("Could not find graph canvas for detailed info"); + return; + } + + this.log("---- Canvas Information ----"); + this.log(`Canvas dimensions: ${canvas.width}x${canvas.height}`); + this.log(`Canvas display size: ${canvas.clientWidth}x${canvas.clientHeight}`); + this.log(`Canvas CSS transform: ${canvas.style.transform || 'none'}`); + + const ds = canvasManager.ds; + if (ds) { + this.log(`Canvas DS scale: ${ds.scale}`); + if (ds.offset) { + this.log(`Canvas DS offset: [${ds.offset[0]}, ${ds.offset[1]}]`); + } else { + this.log("Canvas DS offset not found"); + } + } else { + this.log("Canvas DS object not found"); + } + + const screenWidth = window.innerWidth; + const screenHeight = window.innerHeight; + const canvasWidth = canvas.width; + const canvasHeight = canvas.height; + + this.log(`Window dimensions: ${screenWidth}x${screenHeight}`); + this.log(`Resolution scale factor: X=${canvasWidth / screenWidth}, Y=${canvasHeight / screenHeight}`); + this.log("---- End Canvas Information ----"); + } catch (e) { + this.log("Error in printCanvasInfo:", e); + } + } + + /** + * Update the debug visualization canvas. + */ + updateDebugView(): void { + if (!this.config.debugMode || !this.ui || !this.ui.debugCanvas || !this.ui.debugCtx) return; + + const debugCtx = this.ui.debugCtx; + const debugCanvas = this.ui.debugCanvas; + + debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); + + // Draw background + debugCtx.fillStyle = 'rgba(0,0,0,0.8)'; + debugCtx.fillRect(0, 0, debugCanvas.width, debugCanvas.height); + + // Header + debugCtx.fillStyle = '#FFFFFF'; + debugCtx.font = '14px monospace'; + debugCtx.fillText('Magnify Glass Debug', 10, 20); + + // Position info + debugCtx.font = '12px monospace'; + debugCtx.fillText(`Cursor: (${this.state.x.toFixed(1)}, ${this.state.y.toFixed(1)})`, 10, 50); + debugCtx.fillText(`Source Rect: (${this.state.sourceX.toFixed(1)}, ${this.state.sourceY.toFixed(1)}, w:${this.state.sourceWidth.toFixed(1)}, h:${this.state.sourceHeight.toFixed(1)})`, 10, 70); + + // Canvas transform + debugCtx.fillText(`Canvas Scale: ${this.state.canvasScale.toFixed(2)}`, 10, 90); + debugCtx.fillText(`Canvas Offset: (${this.state.canvasOffsetX.toFixed(1)}, ${this.state.canvasOffsetY.toFixed(1)})`, 10, 110); + + // Offset information + debugCtx.fillStyle = '#FFFF00'; + debugCtx.fillText(`MANUAL OFFSETS: X=${this.config.offsetX}, Y=${this.config.offsetY} (Use arrow keys to adjust)`, 10, 130); + debugCtx.fillStyle = '#FFFFFF'; + + // Visualization + this.drawCanvasVisualization(debugCtx, debugCanvas); + } + + /** + * Draw a scaled visualization of the canvas and source region. + */ + drawCanvasVisualization(debugCtx: CanvasRenderingContext2D, debugCanvas: HTMLCanvasElement): void { + const canvasScale = 0.1; + const canvasVisX = 10; + const canvasVisY = 170; + const canvasVisWidth = 380; + const canvasVisHeight = 150; + + // Draw canvas representation + debugCtx.strokeStyle = '#AAAAAA'; + debugCtx.strokeRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); + debugCtx.fillStyle = '#444444'; + debugCtx.fillRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); + + // Calculate cursor position in visualization + const cursorVisX = canvasVisX + (this.state.x * canvasScale); + const cursorVisY = canvasVisY + (this.state.y * canvasScale); + + // Calculate source rect in visualization + const sourceRectVisX = canvasVisX + (this.state.sourceX * canvasScale); + const sourceRectVisY = canvasVisY + (this.state.sourceY * canvasScale); + const sourceRectVisWidth = this.state.sourceWidth * canvasScale; + const sourceRectVisHeight = this.state.sourceHeight * canvasScale; + + // Draw source rect + debugCtx.strokeStyle = '#FF0000'; + debugCtx.strokeRect(sourceRectVisX, sourceRectVisY, sourceRectVisWidth, sourceRectVisHeight); + + // Draw cursor position + debugCtx.fillStyle = '#FFFF00'; + debugCtx.beginPath(); + debugCtx.arc(cursorVisX, cursorVisY, 3, 0, Math.PI * 2); + debugCtx.fill(); + + // Draw line from cursor to source rect center + debugCtx.strokeStyle = '#00FF00'; + debugCtx.beginPath(); + debugCtx.moveTo(cursorVisX, cursorVisY); + debugCtx.lineTo(sourceRectVisX + sourceRectVisWidth / 2, sourceRectVisY + sourceRectVisHeight / 2); + debugCtx.stroke(); + + // Label for visualization + debugCtx.fillStyle = '#FFFFFF'; + debugCtx.fillText('Canvas Visualization (scaled)', canvasVisX, canvasVisY - 5); + + // Offset values + const offsetX = this.state.sourceX - this.state.x + (this.state.sourceWidth / 2); + const offsetY = this.state.sourceY - this.state.y + (this.state.sourceHeight / 2); + debugCtx.fillText(`Alignment Offset: (${offsetX.toFixed(1)}, ${offsetY.toFixed(1)})`, 10, canvasVisY + canvasVisHeight + 20); + } +} diff --git a/src/magnify-glass/EventHandler.ts b/src/magnify-glass/EventHandler.ts new file mode 100644 index 0000000..f01c39e --- /dev/null +++ b/src/magnify-glass/EventHandler.ts @@ -0,0 +1,232 @@ +/** + * ComfyUI MagnifyGlass - EventHandler (TypeScript) + * + * Handles keyboard and mouse events for the magnifying glass. + */ + +import { isUserTyping } from '../shared/utils'; +import { DEFAULT_PADDING } from '../shared/constants'; +import type { MagnifyGlass } from './MagnifyGlass'; + +/** + * Event Handler class. + * Manages all keyboard and mouse event handling for the magnifier. + */ +export class EventHandler { + magnifyGlass: MagnifyGlass; + + constructor(magnifyGlass: MagnifyGlass) { + this.magnifyGlass = magnifyGlass; + + // Bind methods to maintain context + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleKeyUp = this.handleKeyUp.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + } + + /** + * Attach all event listeners. + */ + attachListeners(): void { + document.addEventListener("keydown", this.handleKeyDown); + document.addEventListener("keyup", this.handleKeyUp); + document.addEventListener("mousemove", this.handleMouseMove); + } + + /** + * Detach all event listeners. + */ + detachListeners(): void { + document.removeEventListener("keydown", this.handleKeyDown); + document.removeEventListener("keyup", this.handleKeyUp); + document.removeEventListener("mousemove", this.handleMouseMove); + } + + /** + * Handle keydown events. + */ + handleKeyDown(e: KeyboardEvent): void { + const config = this.magnifyGlass.config; + const state = this.magnifyGlass.state; + + // Don't handle hotkeys if user is typing in an input field + if (isUserTyping()) { + return; + } + + // Magnifier activation + console.log(`[MagnifyGlass] KeyDown: ${e.key} (config: ${config.activationKey}, active: ${state.active})`); + if (e.key.toLowerCase() === config.activationKey && + (!config.altRequired || e.altKey)) { + console.log("[MagnifyGlass] Activation key matched!"); + if (config.alwaysActiveMode) { + if (state.active) { + state.active = false; + this.magnifyGlass.ui.hide(); + } else { + state.active = true; + this.magnifyGlass.ui.show(); + this.updateInitialPosition(); + } + } else { + if (!state.active) { + state.active = true; + this.magnifyGlass.ui.show(); + this.updateInitialPosition(); + } + } + } + + // Manual offset adjustment keys (only when magnifier is active) + if (state.active) { + let offsetChanged = false; + const stepSize = e.shiftKey ? config.offsetStep * 5 : config.offsetStep; + + if (e.key === "ArrowUp") { + config.offsetY -= stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowDown") { + config.offsetY += stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowLeft") { + config.offsetX -= stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowRight") { + config.offsetX += stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() && + (!config.altRequired || e.altKey)) { + this.magnifyGlass.resetOffsets(); + offsetChanged = true; + e.preventDefault(); + } + + // Toggle Follow Cursor Key + if (e.key.toLowerCase() === config.toggleFollowCursorKey && + (!config.altRequired || e.altKey)) { + config.followCursor = !config.followCursor; + this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? 'ON' : 'OFF'}`); + if (state.active && config.followCursor) { + this.magnifyGlass.ui.positionGlass( + this.magnifyGlass.lastKnownMousePosition.x, + this.magnifyGlass.lastKnownMousePosition.y + ); + } + e.preventDefault(); + } + + if (offsetChanged) { + config.saveOffsets(); + this.magnifyGlass.updateMagnifiedView(); + } + } + } + + /** + * Handle keyup events. + */ + handleKeyUp(e: KeyboardEvent): void { + const config = this.magnifyGlass.config; + const state = this.magnifyGlass.state; + + if (!config.alwaysActiveMode && + (e.key.toLowerCase() === config.activationKey || + (config.altRequired && e.key === "Alt"))) { + + let shouldDeactivate = false; + if (config.altRequired) { + if (e.key === "Alt" || e.key.toLowerCase() === config.activationKey) { + shouldDeactivate = true; + } + } else { + if (e.key.toLowerCase() === config.activationKey) { + shouldDeactivate = true; + } + } + + if (state.active && shouldDeactivate) { + state.active = false; + this.magnifyGlass.ui.hide(); + } + } + } + + /** + * Handle mousemove events. + */ + handleMouseMove(e: MouseEvent): void { + // Always track mouse position + this.magnifyGlass.lastKnownMousePosition.x = e.clientX; + this.magnifyGlass.lastKnownMousePosition.y = e.clientY; + + if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return; + + const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); + const cssMouseXOnCanvas = e.clientX - rect.left; + const cssMouseYOnCanvas = e.clientY - rect.top; + + // Check if the cursor is over the canvas element + if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width && + cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) { + + const canvasElement = this.magnifyGlass.litegraphCanvas; + const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; + const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; + + const pixelX = cssMouseXOnCanvas * scaleX; + const pixelY = cssMouseYOnCanvas * scaleY; + + this.magnifyGlass.state.x = pixelX; + this.magnifyGlass.state.y = pixelY; + + this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY); + this.magnifyGlass.updateMagnifiedView(); + } + } + + /** + * Update the initial position when magnifier is activated. + */ + updateInitialPosition(): void { + if (!this.magnifyGlass.litegraphCanvas) return; + + const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); + const clientX = this.magnifyGlass.lastKnownMousePosition.x; + const clientY = this.magnifyGlass.lastKnownMousePosition.y; + + const isOverCanvas = clientX >= rect.left && clientX <= rect.right && + clientY >= rect.top && clientY <= rect.bottom; + + if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) { + const cssMouseXOnCanvas = clientX - rect.left; + const cssMouseYOnCanvas = clientY - rect.top; + + const canvasElement = this.magnifyGlass.litegraphCanvas; + const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; + const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; + + const pixelX = cssMouseXOnCanvas * scaleX; + const pixelY = cssMouseYOnCanvas * scaleY; + + this.magnifyGlass.state.x = pixelX; + this.magnifyGlass.state.y = pixelY; + + const glassSize = this.magnifyGlass.config.glassSize; + + if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) { + // First activation - position at top right of the window + this.magnifyGlass.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`; + this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_PADDING}px`; + this.magnifyGlass.state.wasActivatedBefore = true; + } else { + this.magnifyGlass.ui.positionGlass(clientX, clientY); + } + + this.magnifyGlass.updateMagnifiedView(); + } + } +} diff --git a/src/magnify-glass/MagnifierState.ts b/src/magnify-glass/MagnifierState.ts new file mode 100644 index 0000000..95ef2e6 --- /dev/null +++ b/src/magnify-glass/MagnifierState.ts @@ -0,0 +1,80 @@ +/** + * ComfyUI MagnifyGlass - MagnifierState (TypeScript) + * + * Manages the state of the magnifying glass. + */ + +/** + * Magnifier state class. + * Tracks activation state, cursor position, and canvas transforms. + */ +export class MagnifierState { + /** Whether the magnifier is currently active */ + active: boolean; + + /** Track if the glass has been activated before */ + wasActivatedBefore: boolean; + + /** Cursor X relative to litegraphCanvas */ + x: number; + + /** Cursor Y relative to litegraphCanvas */ + y: number; + + /** Calculated source area X */ + sourceX: number; + + /** Calculated source area Y */ + sourceY: number; + + /** Calculated source area width */ + sourceWidth: number; + + /** Calculated source area height */ + sourceHeight: number; + + /** Current canvas scale/zoom */ + canvasScale: number; + + /** Canvas translation X */ + canvasOffsetX: number; + + /** Canvas translation Y */ + canvasOffsetY: number; + + /** Flag to manage requestAnimationFrame */ + isRenderScheduled: boolean; + + constructor() { + this.active = false; + this.wasActivatedBefore = false; + this.x = 0; + this.y = 0; + this.sourceX = 0; + this.sourceY = 0; + this.sourceWidth = 0; + this.sourceHeight = 0; + this.canvasScale = 1.0; + this.canvasOffsetX = 0; + this.canvasOffsetY = 0; + this.isRenderScheduled = false; + } + + /** + * Reset state to initial values. + */ + reset(): void { + this.active = false; + this.wasActivatedBefore = false; + this.x = 0; + this.y = 0; + this.sourceX = 0; + this.sourceY = 0; + this.sourceWidth = 0; + this.sourceHeight = 0; + this.canvasScale = 1.0; + this.canvasOffsetX = 0; + this.canvasOffsetY = 0; + this.isRenderScheduled = false; + } +} diff --git a/src/magnify-glass/MagnifyGlass.ts b/src/magnify-glass/MagnifyGlass.ts new file mode 100644 index 0000000..18e1334 --- /dev/null +++ b/src/magnify-glass/MagnifyGlass.ts @@ -0,0 +1,414 @@ +/** + * ComfyUI MagnifyGlass - Main Class (TypeScript) + * + * Orchestrates all components of the magnifying glass. + */ + +import type { ComfyApp, ComfyNode } from '../types/comfyui'; +import { findLiteGraphCanvas, rectsOverlap, Rectangle } from '../shared/utils'; +import { DEFAULT_PADDING } from '../shared/constants'; +import { registerGlassSettings } from '../shared/settings'; +import { ConfigManager } from './ConfigManager'; +import { MagnifierState } from './MagnifierState'; +import { UiManager } from './UiManager'; +import { WebGLRenderer } from './WebGLRenderer'; +import { DebugManager } from './DebugManager'; +import { EventHandler } from './EventHandler'; + +// External ComfyUI globals +declare const app: ComfyApp; +declare const LiteGraph: unknown; + +/** + * Main MagnifyGlass class. + * Orchestrates all components and provides the main API. + */ +export class MagnifyGlass { + config: ConfigManager; + state: MagnifierState; + ui: UiManager; + renderer: WebGLRenderer | null; + debugger: DebugManager; + eventHandler: EventHandler; + + /** The LiteGraph canvas */ + litegraphCanvas: HTMLCanvasElement | null; + + /** Last known mouse position for better initial positioning */ + lastKnownMousePosition: { x: number; y: number }; + + /** Whether we're currently over media (for info panel) */ + isOverMedia: boolean; + + /** Current media element under cursor */ + currentMediaElement: HTMLImageElement | HTMLVideoElement | null; + + constructor() { + this.config = new ConfigManager(); + this.state = new MagnifierState(); + this.ui = new UiManager(this.config, this.state); + this.renderer = null; + this.debugger = new DebugManager(this.config, this.state, this.ui); + this.eventHandler = new EventHandler(this); + + // The LiteGraph canvas + this.litegraphCanvas = null; + + // Last known mouse position for better initial positioning + this.lastKnownMousePosition = { x: 0, y: 0 }; + + // Media tracking + this.isOverMedia = false; + this.currentMediaElement = null; + } + + /** + * Initialize the magnifying glass. + */ + init(): void { + if (typeof LiteGraph === 'undefined' || typeof app === 'undefined' || !app.canvas) { + this.debugger.log("LiteGraph or app not ready, retrying in 100ms."); + setTimeout(() => this.init(), 100); + return; + } + this.debugger.log("LiteGraph and app ready."); + + // Load saved offsets first + this.config.loadSavedOffsets(); + + // Print detailed canvas information + this.debugger.printCanvasInfo(); + + // Create UI elements + this.ui.createElements(); + + // Setup WebGL renderer + this.renderer = new WebGLRenderer(this.config, this.state, this.ui); + if (!this.renderer.isValid()) { + this.ui.cleanup(); + return; + } + + // Find the LiteGraph canvas + this.litegraphCanvas = findLiteGraphCanvas(); + this.debugger.log("LiteGraph canvas found:", this.litegraphCanvas); + if (!this.litegraphCanvas) { + this.debugger.error("Could not find LiteGraph canvas. Magnifier will not work."); + this.ui.cleanup(); + return; + } + + // Attach event handlers + this.eventHandler.attachListeners(); + + // Register settings with ComfyUI + registerGlassSettings(this); + + this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? 'Alt+' : ''}${this.config.activationKey.toUpperCase()} to activate.`); + } + + /** + * Update the magnified view. + */ + updateMagnifiedView(): void { + if (!this.state.active || !this.renderer || !this.litegraphCanvas) { + return; + } + + // Get canvas transformation info + this.updateCanvasTransformation(); + + // Calculate the source region + this.calculateSourceRegion(); + + // Schedule the rendering operation + if (!this.state.isRenderScheduled) { + this.state.isRenderScheduled = true; + requestAnimationFrame(() => { + if (!this.state.active || !this.renderer || !this.litegraphCanvas) { + this.state.isRenderScheduled = false; + return; + } + // Render the magnified view + this.renderer.render(this.litegraphCanvas); + + // Update debug visualization + this.debugger.updateDebugView(); + + // Render HTML overlays + this.renderHtmlOverlays(); + + this.state.isRenderScheduled = false; + }); + } + } + + /** + * Update canvas transformation state. + */ + updateCanvasTransformation(): void { + this.state.canvasScale = 1.0; + this.state.canvasOffsetX = 0; + this.state.canvasOffsetY = 0; + + if (app?.canvas) { + const ds = (app.canvas as any).ds; + if (ds) { + if (typeof ds.scale === 'number') { + this.state.canvasScale = ds.scale; + } + + if (ds.offset) { + this.state.canvasOffsetX = ds.offset[0] || 0; + this.state.canvasOffsetY = ds.offset[1] || 0; + } + } + } + } + + /** + * Calculate the source region for magnification. + */ + calculateSourceRegion(): void { + const cursorPixelX = this.state.x; + const cursorPixelY = this.state.y; + const canvasScale = this.state.canvasScale; + const canvasOffsetX = this.state.canvasOffsetX; + const canvasOffsetY = this.state.canvasOffsetY; + + if (canvasScale === 0) return; + + // Convert cursor canvas pixels to LiteGraph graph coordinates + const cursorGraphX = (cursorPixelX - canvasOffsetX) / canvasScale; + const cursorGraphY = (cursorPixelY - canvasOffsetY) / canvasScale; + + // Apply manual offset + const targetGraphCenterX = cursorGraphX + this.config.offsetX; + const targetGraphCenterY = cursorGraphY + this.config.offsetY; + + // Calculate source dimensions in graph units + const sourceGraphWidth = (this.config.glassSize / this.config.zoomFactor) / canvasScale; + const sourceGraphHeight = (this.config.glassSize / this.config.zoomFactor) / canvasScale; + + // Calculate source top-left corner in graph coordinates + const sourceGraphX = targetGraphCenterX - (sourceGraphWidth / 2); + const sourceGraphY = targetGraphCenterY - (sourceGraphHeight / 2); + + // Convert back to canvas pixel coordinates + this.state.sourceX = (sourceGraphX * canvasScale) + canvasOffsetX; + this.state.sourceY = (sourceGraphY * canvasScale) + canvasOffsetY; + this.state.sourceWidth = sourceGraphWidth * canvasScale; + this.state.sourceHeight = sourceGraphHeight * canvasScale; + } + + /** + * Render HTML overlays for text and media in the magnified view. + */ + renderHtmlOverlays(): void { + const graph = (app as any).graph; + if (!this.state.active || !this.ui.htmlOverlayContainer || !graph || !this.litegraphCanvas) { + if (this.ui.htmlOverlayContainer) this.ui.htmlOverlayContainer.innerHTML = ''; + return; + } + + this.ui.htmlOverlayContainer.innerHTML = ''; + + const magnifyRect: Rectangle = { + x: this.state.sourceX, + y: this.state.sourceY, + width: this.state.sourceWidth, + height: this.state.sourceHeight + }; + + const nodes: ComfyNode[] = graph._nodes; + if (!nodes) return; + + for (const node of nodes) { + const widgets = (node as any).widgets; + if (!widgets) continue; + + for (const widget of widgets) { + let isTextElement = false; + let isVideoElement = false; + let isImageElement = false; + let elementToProcess: HTMLElement | null = null; + + if (widget.element) { + const element = widget.element as HTMLElement; + if (widget.type === "text" || widget.type === "string" || element.tagName === 'TEXTAREA') { + isTextElement = true; + elementToProcess = element; + } else if (element.tagName === 'VIDEO') { + isVideoElement = true; + elementToProcess = element; + } else if (element.tagName === 'IMG') { + isImageElement = true; + elementToProcess = element; + } else { + const potentialVideo = element.querySelector('video'); + if (potentialVideo) { + isVideoElement = true; + elementToProcess = potentialVideo; + } else { + const potentialImage = element.querySelector('img'); + if (potentialImage) { + isImageElement = true; + elementToProcess = potentialImage; + } + } + } + } + + if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) { + const widgetRect = elementToProcess.getBoundingClientRect(); + const canvasRect = this.litegraphCanvas!.getBoundingClientRect(); + + const canvasToViewportScaleX = canvasRect.width > 0 ? (this.litegraphCanvas!.width / canvasRect.width) : 1; + const canvasToViewportScaleY = canvasRect.height > 0 ? (this.litegraphCanvas!.height / canvasRect.height) : 1; + + const widgetCanvasX = (widgetRect.left - canvasRect.left) * canvasToViewportScaleX; + const widgetCanvasY = (widgetRect.top - canvasRect.top) * canvasToViewportScaleY; + const widgetCanvasWidth = widgetRect.width * canvasToViewportScaleX; + const widgetCanvasHeight = widgetRect.height * canvasToViewportScaleY; + + const widgetSourceRect: Rectangle = { + x: widgetCanvasX, + y: widgetCanvasY, + width: widgetCanvasWidth, + height: widgetCanvasHeight + }; + + if (rectsOverlap(magnifyRect, widgetSourceRect)) { + const clonedElement = elementToProcess.cloneNode(true) as HTMLElement; + clonedElement.style.position = 'absolute'; + clonedElement.style.pointerEvents = 'none'; + + if (isTextElement) { + clonedElement.style.backgroundColor = elementToProcess.style.backgroundColor || '#222'; + clonedElement.style.color = elementToProcess.style.color || '#DDD'; + clonedElement.style.border = elementToProcess.style.border || '1px solid #555'; + (clonedElement as HTMLInputElement).disabled = true; + } else if (isVideoElement) { + const video = clonedElement as HTMLVideoElement; + const originalVideo = elementToProcess as HTMLVideoElement; + video.src = originalVideo.src; + video.autoplay = originalVideo.autoplay; + video.loop = originalVideo.loop; + video.preload = originalVideo.preload; + video.crossOrigin = originalVideo.crossOrigin; + video.muted = true; + if (!originalVideo.paused) { + video.play().catch(e => console.warn("Magnify Glass: Cloned video play failed", e)); + } + video.currentTime = originalVideo.currentTime; + } else if (isImageElement) { + const img = clonedElement as HTMLImageElement; + const originalImg = elementToProcess as HTMLImageElement; + img.src = originalImg.src; + img.alt = originalImg.alt; + } + + const relativeX = widgetSourceRect.x - magnifyRect.x; + const relativeY = widgetSourceRect.y - magnifyRect.y; + const magnifiedX = relativeX * this.config.zoomFactor; + const magnifiedY = relativeY * this.config.zoomFactor; + + clonedElement.style.left = `${magnifiedX}px`; + clonedElement.style.top = `${magnifiedY}px`; + clonedElement.style.width = `${widgetSourceRect.width}px`; + clonedElement.style.height = `${widgetSourceRect.height}px`; + clonedElement.style.transformOrigin = 'top left'; + clonedElement.style.transform = `scale(${this.config.zoomFactor})`; + + if (isTextElement && clonedElement.style.fontSize) { + const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize); + clonedElement.style.fontSize = `${originalFontSize}px`; + } + + this.ui.htmlOverlayContainer!.appendChild(clonedElement); + } + } + } + } + } + + /** + * Update config from current settings values. + */ + updateConfigFromSettings(): void { + this.config.loadSettings(); + } + + /** + * Apply UI changes based on current config. + */ + applyUiChanges(): void { + this.ui.applyStyles(); + + if (this.renderer?.gl) { + this.renderer.updateViewport(); + } + } + + /** + * Toggle the magnify glass on/off. + */ + toggle(): void { + this.state.active = !this.state.active; + if (this.state.active) { + this.ui.show(); + } else { + this.ui.hide(); + } + } + + /** + * Reset offsets and panel positions to defaults. + */ + resetOffsets(): void { + this.config.resetOffsets(); + + if (this.state.active) { + this.updateMagnifiedView(); + } + + // Reset magnify glass position to top-right corner + if (this.ui.glassDiv) { + const glassSize = this.config.glassSize; + this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`; + this.ui.glassDiv.style.top = `${DEFAULT_PADDING}px`; + this.state.wasActivatedBefore = false; + } + + // Reset inspector panel position if exists + const extensions = window.comfyUIMagnifyGlassExtensions; + if (extensions && extensions.length > 0) { + extensions.forEach((extension: any) => { + if (extension?.stateManager) { + extension.stateManager.state.isPanelPinned = false; + extension.stateManager.state.isPanelLocked = false; + extension.stateManager.state.isAutoPinned = false; + extension.stateManager.state.pinnedPosition = { x: 0, y: 0 }; + extension.stateManager.state.lastPinnedPosition = null; + + if (extension.positionManager) { + extension.positionManager.positionPanel(); + } + if (extension.uiManager?.updateControlStates) { + extension.uiManager.updateControlStates(); + } + } + }); + } + + this.debugger.log("Reset: Magnify glass and inspector panel positions restored to defaults"); + } + + /** + * Cleanup all resources. + */ + cleanup(): void { + this.eventHandler.detachListeners(); + this.ui.cleanup(); + } +} diff --git a/src/magnify-glass/UiManager.ts b/src/magnify-glass/UiManager.ts new file mode 100644 index 0000000..deec9aa --- /dev/null +++ b/src/magnify-glass/UiManager.ts @@ -0,0 +1,279 @@ +/** + * ComfyUI MagnifyGlass - UiManager (TypeScript) + * + * Handles creation and management of DOM elements for the magnifying glass. + */ + +import { Z_INDEX, DEFAULT_PADDING } from '../shared/constants'; +import type { ConfigManager } from './ConfigManager'; +import type { MagnifierState } from './MagnifierState'; + +/** + * UI Manager class. + * Manages all DOM elements for the magnifying glass. + */ +export class UiManager { + config: ConfigManager; + state: MagnifierState; + glassDiv: HTMLDivElement | null; + glassCanvas: HTMLCanvasElement | null; + debugCanvas: HTMLCanvasElement | null; + debugCtx: CanvasRenderingContext2D | null; + htmlOverlayContainer: HTMLDivElement | null; + + constructor(config: ConfigManager, state: MagnifierState) { + this.config = config; + this.state = state; + this.glassDiv = null; + this.glassCanvas = null; + this.debugCanvas = null; + this.debugCtx = null; + this.htmlOverlayContainer = null; + } + + /** + * Create all DOM elements for the magnifying glass. + */ + createElements(): void { + // Create magnifying glass container + this.glassDiv = document.createElement("div"); + this.glassDiv.id = "comfyui-magnify-glass"; + this.glassDiv.style.cssText = ` + position: absolute; + width: ${this.config.glassSize}px; + height: ${this.config.glassSize}px; + border-radius: ${this.config.glassShape === "Circle" ? "50%" : "0px"}; + border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : 'none'}; + overflow: hidden; + pointer-events: none; + z-index: ${Z_INDEX.GLASS}; + display: none; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); + background-color: rgba(255,255,255,0.1); + `; + + // Create WebGL canvas for the magnifying glass + this.glassCanvas = document.createElement("canvas"); + this.glassCanvas.width = this.config.glassSize; + this.glassCanvas.height = this.config.glassSize; + this.glassCanvas.id = "comfyui-magnify-canvas"; + this.glassDiv.appendChild(this.glassCanvas); + + // Create HTML overlay container + this.htmlOverlayContainer = document.createElement("div"); + this.htmlOverlayContainer.id = "comfyui-magnify-html-overlay"; + this.htmlOverlayContainer.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden; + `; + this.glassDiv.appendChild(this.htmlOverlayContainer); + + document.body.appendChild(this.glassDiv); + + // Create debug canvas if debug mode is enabled + if (this.config.debugMode) { + this.createDebugCanvas(); + } + } + + /** + * Create the debug canvas overlay. + */ + createDebugCanvas(): void { + this.debugCanvas = document.createElement("canvas"); + this.debugCanvas.id = "comfyui-magnify-debug"; + this.debugCanvas.width = 400; + this.debugCanvas.height = 350; + this.debugCanvas.style.cssText = ` + position: fixed; + top: 10px; + right: 10px; + background: rgba(0,0,0,0.7); + border: 1px solid #fff; + z-index: ${Z_INDEX.DEBUG}; + pointer-events: none; + color: white; + font-family: monospace; + display: none; + `; + document.body.appendChild(this.debugCanvas); + this.debugCtx = this.debugCanvas.getContext('2d'); + } + + /** + * Show the magnifying glass. + */ + show(): void { + if (this.glassDiv) { + this.glassDiv.style.display = "block"; + } + if (this.config.debugMode && this.debugCanvas) { + this.debugCanvas.style.display = "block"; + } + } + + /** + * Hide the magnifying glass. + */ + hide(): void { + if (this.glassDiv) { + this.glassDiv.style.display = "none"; + } + if (this.config.debugMode && this.debugCanvas) { + this.debugCanvas.style.display = "none"; + } + // Clear HTML overlays when hiding + if (this.htmlOverlayContainer) { + this.htmlOverlayContainer.innerHTML = ''; + } + } + + /** + * Position the glass relative to cursor. + * @param clientX - Client X coordinate + * @param clientY - Client Y coordinate + */ + positionGlass(clientX: number, clientY: number): void { + if (!this.config.followCursor || !this.glassDiv) return; + + const glassSize = this.config.glassSize; + const offsetAmount = DEFAULT_PADDING; + let newLeft: number; + let newTop: number; + + switch (this.config.glassPosition) { + case "Top": + newLeft = clientX - (glassSize / 2); + newTop = clientY - glassSize - offsetAmount; + break; + case "Bottom": + newLeft = clientX - (glassSize / 2); + newTop = clientY + offsetAmount; + break; + case "Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY - (glassSize / 2); + break; + case "Right": + newLeft = clientX + offsetAmount; + newTop = clientY - (glassSize / 2); + break; + case "Top-Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY - glassSize - offsetAmount; + break; + case "Top-Right": + newLeft = clientX + offsetAmount; + newTop = clientY - glassSize - offsetAmount; + break; + case "Bottom-Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY + offsetAmount; + break; + case "Bottom-Right": + newLeft = clientX + offsetAmount; + newTop = clientY + offsetAmount; + break; + default: + newLeft = clientX - (glassSize / 2); + newTop = clientY + offsetAmount; + break; + } + + this.glassDiv.style.left = `${newLeft}px`; + this.glassDiv.style.top = `${newTop}px`; + + this.adjustForBoundaries(clientX, clientY); + } + + /** + * Adjust glass position to stay within viewport boundaries. + * @param clientX - Client X coordinate + * @param clientY - Client Y coordinate + */ + adjustForBoundaries(clientX: number, clientY: number): void { + if (!this.glassDiv) return; + + const glassRect = this.glassDiv.getBoundingClientRect(); + + // Check right boundary + if (glassRect.right > window.innerWidth) { + this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`; + } + + // Check left boundary + const currentRectLeft = this.glassDiv.getBoundingClientRect(); + if (currentRectLeft.left < 0) { + this.glassDiv.style.left = "10px"; + } + + // Check bottom boundary + if (glassRect.bottom > window.innerHeight) { + this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`; + } + + // Check top boundary + const currentRectTop = this.glassDiv.getBoundingClientRect(); + if (currentRectTop.top < 0) { + this.glassDiv.style.top = "10px"; + } + } + + /** + * Apply current config to UI elements. + */ + applyStyles(): void { + if (this.glassDiv) { + this.glassDiv.style.width = `${this.config.glassSize}px`; + this.glassDiv.style.height = `${this.config.glassSize}px`; + this.glassDiv.style.border = this.config.borderEnabled + ? `${this.config.borderWidth}px solid ${this.config.borderColor}` + : 'none'; + + // Reset clip-path and set default border-radius + this.glassDiv.style.clipPath = 'none'; + this.glassDiv.style.borderRadius = '0px'; + + switch (this.config.glassShape) { + case "Circle": + this.glassDiv.style.borderRadius = "50%"; + break; + case "Square": + // borderRadius is already '0px' + break; + case "Rounded Square": + this.glassDiv.style.borderRadius = "20%"; + break; + default: + this.glassDiv.style.borderRadius = "50%"; + break; + } + } + + if (this.glassCanvas) { + this.glassCanvas.width = this.config.glassSize; + this.glassCanvas.height = this.config.glassSize; + } + + // Handle debug canvas show/hide + if (this.config.debugMode) { + if (!this.debugCanvas) this.createDebugCanvas(); + if (this.state.active && this.debugCanvas) this.debugCanvas.style.display = "block"; + } else { + if (this.debugCanvas) this.debugCanvas.style.display = "none"; + } + } + + /** + * Cleanup DOM elements. + */ + cleanup(): void { + if (this.glassDiv) this.glassDiv.remove(); + if (this.debugCanvas) this.debugCanvas.remove(); + } +} diff --git a/src/magnify-glass/WebGLRenderer.ts b/src/magnify-glass/WebGLRenderer.ts new file mode 100644 index 0000000..f92577b --- /dev/null +++ b/src/magnify-glass/WebGLRenderer.ts @@ -0,0 +1,302 @@ +/** + * ComfyUI MagnifyGlass - WebGLRenderer (TypeScript) + * + * Handles WebGL-based rendering of the magnified view. + */ + +import type { ConfigManager } from './ConfigManager'; +import type { MagnifierState } from './MagnifierState'; +import type { UiManager } from './UiManager'; + +interface UniformLocations { + sourceTexture: WebGLUniformLocation | null; + textureOffset: WebGLUniformLocation | null; + textureRepeat: WebGLUniformLocation | null; + glassSize: WebGLUniformLocation | null; +} + +interface AttributeLocations { + position: number; + texCoord: number; +} + +/** + * WebGL Renderer class. + * Provides efficient WebGL-based magnification rendering. + */ +export class WebGLRenderer { + config: ConfigManager; + state: MagnifierState; + ui: UiManager; + + gl: WebGLRenderingContext | null; + program: WebGLProgram | null; + texture: WebGLTexture | null; + positionBuffer: WebGLBuffer | null; + texCoordBuffer: WebGLBuffer | null; + uniformLocations: UniformLocations | null; + attributeLocations: AttributeLocations | null; + currentFilteringMode: number | null; + + vertexShaderSource: string; + fragmentShaderSource: string; + + constructor(config: ConfigManager, state: MagnifierState, ui: UiManager) { + this.config = config; + this.state = state; + this.ui = ui; + + this.gl = null; + this.program = null; + this.texture = null; + this.positionBuffer = null; + this.texCoordBuffer = null; + this.uniformLocations = null; + this.attributeLocations = null; + this.currentFilteringMode = null; + + this.vertexShaderSource = ` + attribute vec2 a_position; + attribute vec2 a_texCoord; + varying vec2 v_texCoord; + void main() { + gl_Position = vec4(a_position, 0.0, 1.0); + v_texCoord = a_texCoord; + } + `; + + this.fragmentShaderSource = ` + precision mediump float; + varying vec2 v_texCoord; + uniform sampler2D u_sourceTexture; + uniform vec2 u_textureOffset; + uniform vec2 u_textureRepeat; + uniform float u_glassSize; + + void main() { + vec2 sampleCoord = u_textureOffset + v_texCoord * u_textureRepeat; + vec4 color = texture2D(u_sourceTexture, sampleCoord); + gl_FragColor = color; + } + `; + + this.initialize(); + } + + /** + * Initialize WebGL context and resources. + */ + initialize(): void { + // Get WebGL context + if (!this.ui.glassCanvas) return; + + this.gl = this.ui.glassCanvas.getContext("webgl", { preserveDrawingBuffer: true }); + if (!this.gl) { + console.error("ComfyUI Magnifying Glass ERROR: WebGL not supported or context creation failed."); + return; + } + + // Create shader program + this.program = this.createShaderProgram(this.gl, this.vertexShaderSource, this.fragmentShaderSource); + if (!this.program) return; + + // Create position buffer + this.positionBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); + const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; + this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW); + + // Create texture coordinate buffer + this.texCoordBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); + const texCoords = [0, 1, 1, 1, 0, 0, 1, 0]; // Flipped Y for texCoords + this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW); + + // Create texture + this.texture = this.gl.createTexture(); + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR); + this.updateTextureFiltering(this.config.textureFiltering); + + // Get uniform and attribute locations + this.uniformLocations = { + sourceTexture: this.gl.getUniformLocation(this.program, "u_sourceTexture"), + textureOffset: this.gl.getUniformLocation(this.program, "u_textureOffset"), + textureRepeat: this.gl.getUniformLocation(this.program, "u_textureRepeat"), + glassSize: this.gl.getUniformLocation(this.program, "u_glassSize"), + }; + + this.attributeLocations = { + position: this.gl.getAttribLocation(this.program, "a_position"), + texCoord: this.gl.getAttribLocation(this.program, "a_texCoord"), + }; + } + + /** + * Update texture filtering mode. + * @param filteringModeString - "Linear" or "Nearest" + */ + updateTextureFiltering(filteringModeString: string): void { + if (!this.gl || !this.texture) return; + + let glFilterMode: number; + if (filteringModeString === "Nearest") { + glFilterMode = this.gl.NEAREST; + } else { + glFilterMode = this.gl.LINEAR; + } + + if (this.currentFilteringMode === glFilterMode) return; + + this.currentFilteringMode = glFilterMode; + + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, glFilterMode); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, glFilterMode); + this.gl.bindTexture(this.gl.TEXTURE_2D, null); + } + + /** + * Check if renderer is valid and ready. + * @returns Boolean indicating if WebGL context and program are ready + */ + isValid(): boolean { + return this.gl !== null && this.program !== null; + } + + /** + * Create a WebGL shader program. + * @param gl - WebGL context + * @param vsSource - Vertex shader source + * @param fsSource - Fragment shader source + * @returns WebGL program or null on failure + */ + createShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string): WebGLProgram | null { + const vertexShader = gl.createShader(gl.VERTEX_SHADER); + if (!vertexShader) return null; + + gl.shaderSource(vertexShader, vsSource); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + console.error('ComfyUI Magnifying Glass ERROR: Vertex shader compilation error:', gl.getShaderInfoLog(vertexShader)); + gl.deleteShader(vertexShader); + return null; + } + + const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + if (!fragmentShader) { + gl.deleteShader(vertexShader); + return null; + } + + gl.shaderSource(fragmentShader, fsSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + console.error('ComfyUI Magnifying Glass ERROR: Fragment shader compilation error:', gl.getShaderInfoLog(fragmentShader)); + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; + } + + const program = gl.createProgram(); + if (!program) { + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; + } + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error('ComfyUI Magnifying Glass ERROR: Shader program linking error:', gl.getProgramInfoLog(program)); + gl.deleteProgram(program); + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; + } + + return program; + } + + /** + * Render the magnified view. + * @param sourceCanvas - The source canvas to sample from + */ + render(sourceCanvas: HTMLCanvasElement): void { + if (!this.gl || !this.program || !this.texture || !this.uniformLocations || !this.attributeLocations) return; + + // Bind texture and update with source canvas + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + + try { + this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceCanvas); + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error in texImage2D:", e); + return; + } + + // Calculate normalized texture coordinates (UV space: 0-1) + const uvX = this.state.sourceX / sourceCanvas.width; + const uvY = this.state.sourceY / sourceCanvas.height; + const uvWidth = this.state.sourceWidth / sourceCanvas.width; + const uvHeight = this.state.sourceHeight / sourceCanvas.height; + + // Set up rendering + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + this.gl.clearColor(0.0, 0.0, 0.0, 0.0); + this.gl.clear(this.gl.COLOR_BUFFER_BIT); + + this.gl.useProgram(this.program); + + // Set uniforms + this.gl.activeTexture(this.gl.TEXTURE0); + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.uniform1i(this.uniformLocations.sourceTexture, 0); + this.gl.uniform2f(this.uniformLocations.textureOffset, uvX, uvY); + this.gl.uniform2f(this.uniformLocations.textureRepeat, uvWidth, uvHeight); + this.gl.uniform1f(this.uniformLocations.glassSize, this.config.glassSize); + + // Set attributes + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); + this.gl.vertexAttribPointer(this.attributeLocations.position, 2, this.gl.FLOAT, false, 0, 0); + this.gl.enableVertexAttribArray(this.attributeLocations.position); + + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); + this.gl.vertexAttribPointer(this.attributeLocations.texCoord, 2, this.gl.FLOAT, false, 0, 0); + this.gl.enableVertexAttribArray(this.attributeLocations.texCoord); + + // Draw + this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); + } + + /** + * Check for WebGL errors. + * @param label - Debug label + * @returns True if error occurred + */ + checkWebGLError(label: string): boolean { + if (!this.gl) return false; + + const err = this.gl.getError(); + if (err !== this.gl.NO_ERROR) { + console.error(`ComfyUI Magnifying Glass ERROR: WebGL Error (${label}):`, err); + return true; + } + return false; + } + + /** + * Update viewport when glass size changes. + */ + updateViewport(): void { + if (this.gl) { + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + } + } +} diff --git a/src/magnify-glass/index.ts b/src/magnify-glass/index.ts new file mode 100644 index 0000000..f36e1e0 --- /dev/null +++ b/src/magnify-glass/index.ts @@ -0,0 +1,13 @@ +/** + * ComfyUI MagnifyGlass - Module Index (TypeScript) + * + * Re-exports all magnify-glass components. + */ + +export { MagnifyGlass } from './MagnifyGlass'; +export { MagnifierState } from './MagnifierState'; +export { ConfigManager } from './ConfigManager'; +export { UiManager } from './UiManager'; +export { WebGLRenderer } from './WebGLRenderer'; +export { DebugManager } from './DebugManager'; +export { EventHandler } from './EventHandler'; diff --git a/src/magnify_glass.ts b/src/magnify_glass.ts new file mode 100644 index 0000000..1fa3a5a --- /dev/null +++ b/src/magnify_glass.ts @@ -0,0 +1,27 @@ +/** + * ComfyUI Magnify Glass - Main Entry Point + * + * Initializes the Magnify Glass extension. + */ + +import { MagnifyGlass } from './magnify-glass'; +// @ts-ignore +import { app } from "/scripts/app.js"; + +// Global declaration handled in comfyui.d.ts + +// We rely on the global 'app' object provided by ComfyUI +// Register extension +app.registerExtension({ + name: "comfyui.magnify.glass", + init() { + // Initialize the magnify glass + const magnifyGlass = new MagnifyGlass(); + magnifyGlass.init(); + + // Expose to window for other extensions (like info panel) to access + window.comfyUIMagnifyGlass = magnifyGlass; + + console.log("ComfyUI Magnify Glass: Extension initialized"); + } +}); diff --git a/src/magnify_info_panel.ts b/src/magnify_info_panel.ts new file mode 100644 index 0000000..956d05a --- /dev/null +++ b/src/magnify_info_panel.ts @@ -0,0 +1,58 @@ +/** + * ComfyUI Magnify Info Panel - Main Entry Point + * + * Initializes the Info Panel extension. + */ + +import { InfoPanel } from './info-panel'; +// @ts-ignore +import { app } from "/scripts/app.js"; + +// We rely on the global 'app' object provided by ComfyUI +// No need to import it as it's global, but TypeScript needs to know about it. +// We handled this in types/comfyui.d.ts + +app.registerExtension({ + name: "comfyui.magnify.info.panel", + init() { + // Wait for magnify glass to be available + const checkDependencies = () => { + if (window.comfyUIMagnifyGlass) { + initializeInfoPanel(); + } else { + setTimeout(checkDependencies, 100); + } + }; + + checkDependencies(); + } +}); + +function initializeInfoPanel() { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass) { + console.error("ComfyUI Magnify Info Panel: Failed to find MagnifyGlass instance."); + return; + } + + try { + // Initialize the info panel + const infoPanel = new InfoPanel(magnifyGlass); + + // Expose orchestrator globally if needed + window.infoPanelManager = infoPanel as any; + + // Load CSS + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = "extensions/comfyui-magnifyglass/style.css"; + document.head.appendChild(link); + console.log("ComfyUI Magnify Info Panel: CSS loaded"); + + console.log("ComfyUI Magnify Info Panel: Extension initialized"); + } catch (e) { + console.error("ComfyUI Magnify Info Panel: Error during initialization:", e); + } +} + diff --git a/src/shared/constants.ts b/src/shared/constants.ts new file mode 100644 index 0000000..6d0364c --- /dev/null +++ b/src/shared/constants.ts @@ -0,0 +1,85 @@ +/** + * ComfyUI MagnifyGlass - Shared Constants + * + * Configuration constants shared between magnify glass and info panel modules. + */ + +// Glass positioning options +export const GLASS_POSITIONS = [ + "Top", + "Bottom", + "Left", + "Right", + "Top-Left", + "Top-Right", + "Bottom-Left", + "Bottom-Right" +] as const; + +export type GlassPosition = typeof GLASS_POSITIONS[number]; + +// Glass shape options +export const GLASS_SHAPES = [ + "Circle", + "Square", + "Rounded Square" +] as const; + +export type GlassShape = typeof GLASS_SHAPES[number]; + +// Texture filtering modes +export const TEXTURE_FILTERS = [ + "Linear", + "Nearest" +] as const; + +export type TextureFilter = typeof TEXTURE_FILTERS[number]; + +// Info panel positions relative to glass +export const PANEL_POSITIONS = [ + "Top", + "Bottom", + "Left", + "Right" +] as const; + +export type PanelPosition = typeof PANEL_POSITIONS[number]; + +// Available keys for settings +export const ACTIVATION_KEYS = ["x", "z", "m", "q", "v", "c"] as const; +export const RESET_KEYS = ["r", "o", "p", "k", "l"] as const; +export const TOGGLE_FOLLOW_KEYS = ["f", "g", "h", "j", "k"] as const; + +// UI Constants +export const DEFAULT_PADDING = 20; +export const DEFAULT_GLASS_SIZE = 300; +export const DEFAULT_ZOOM_FACTOR = 300; // 3x as percentage +export const DEFAULT_BORDER_WIDTH = 1; +export const DEFAULT_BORDER_COLOR = "#6b7280"; +export const DEFAULT_OFFSET_STEP = 5; + +// Panel Constants +export const DEFAULT_PANEL_WIDTH = 320; +export const DEFAULT_PANEL_MAX_HEIGHT = 1000; +export const DEFAULT_PANEL_OPACITY = 100; + +// Z-Index hierarchy +export const Z_INDEX = { + GLASS: 98999, + DEBUG: 99000, + PANEL: 99999, + CONTROLS: 99998 +} as const; + +// Animation durations (ms) +export const ANIMATION = { + PANEL_TRANSITION: 300, + AUTO_COLLAPSE_DELAY: 1500, + THEME_DETECTION_INTERVAL: 2000 +} as const; + +// LocalStorage keys +export const STORAGE_KEYS = { + OFFSET_X: 'comfyui_magnify_offset_x', + OFFSET_Y: 'comfyui_magnify_offset_y' +} as const; diff --git a/src/shared/index.ts b/src/shared/index.ts new file mode 100644 index 0000000..5c2fdd2 --- /dev/null +++ b/src/shared/index.ts @@ -0,0 +1,3 @@ +export * from './constants'; +export * from './utils'; +export * from './settings'; diff --git a/src/shared/settings.ts b/src/shared/settings.ts new file mode 100644 index 0000000..10af051 --- /dev/null +++ b/src/shared/settings.ts @@ -0,0 +1,700 @@ +/** + * ComfyUI MagnifyGlass - Settings Configuration + * + * Centralized settings definitions and registration functions. + */ + +// @ts-ignore +import { app } from "/scripts/app.js"; +import { + DEFAULT_GLASS_SIZE, + DEFAULT_ZOOM_FACTOR, + DEFAULT_BORDER_WIDTH, + DEFAULT_BORDER_COLOR, + DEFAULT_OFFSET_STEP, + DEFAULT_PANEL_WIDTH, + DEFAULT_PANEL_MAX_HEIGHT, + DEFAULT_PANEL_OPACITY, + GLASS_POSITIONS, + GLASS_SHAPES, + ACTIVATION_KEYS, + RESET_KEYS, + TOGGLE_FOLLOW_KEYS, + PANEL_POSITIONS +} from './constants'; + +// Define the shape of our settings objects for better type safety +export interface GlassSettings { + "🔍MagnifyGlass.ZoomFactor": number; + "🔍MagnifyGlass.GlassSize": number; + "🔍MagnifyGlass.BorderColor": string; + "🔍MagnifyGlass.BorderWidth": number; + "🔍MagnifyGlass.ActivationKey": string; + "🔍MagnifyGlass.AltRequired": boolean; + "🔍MagnifyGlass.FollowCursor": boolean; + "🔍MagnifyGlass.DebugMode": boolean; + "🔍MagnifyGlass.OffsetStep": number; + "🔍MagnifyGlass.GlassPosition": string; + "🔍MagnifyGlass.ResetKey": string; + "🔍MagnifyGlass.GlassShape": string; + "🔍MagnifyGlass.BorderEnabled": boolean; + "🔍MagnifyGlass.TextureFiltering": string; + "🔍MagnifyGlass.AlwaysActiveMode": boolean; + "🔍MagnifyGlass.ToggleFollowCursorKey": string; +} + +export interface PanelSettings { + "🔍MagnifyGlass.InfoPanelEnabled": boolean; + "🔍MagnifyGlass.InfoPanelPosition": string; + "🔍MagnifyGlass.InfoPanelWidth": number; + "🔍MagnifyGlass.InfoPanelOpacity": number; + "🔍MagnifyGlass.InfoPanelMaxHeight": number; + "🔍MagnifyGlass.InfoPanelAnimations": boolean; + "🔍MagnifyGlass.ShowInspectorTab": boolean; + "🔍MagnifyGlass.ToggleHotkey": string; + "🔍MagnifyGlass.GlassPreviewToggleHotkey": string; + "🔍MagnifyGlass.PinPanelHotkey": string; + "🔍MagnifyGlass.ShowHoveringControls": boolean; + "🔍MagnifyGlass.ControlsPosition": string; + "🔍MagnifyGlass.InfoPanelTextColor": string; + "🔍MagnifyGlass.InfoPanelAccentColor": string; + [key: string]: any; // Allow indexing +} + +/** + * Default settings for the magnify glass component. + */ +export const DEFAULT_GLASS_SETTINGS: GlassSettings = { + "🔍MagnifyGlass.ZoomFactor": DEFAULT_ZOOM_FACTOR, + "🔍MagnifyGlass.GlassSize": DEFAULT_GLASS_SIZE, + "🔍MagnifyGlass.BorderColor": DEFAULT_BORDER_COLOR, + "🔍MagnifyGlass.BorderWidth": DEFAULT_BORDER_WIDTH, + "🔍MagnifyGlass.ActivationKey": "x", + "🔍MagnifyGlass.AltRequired": false, + "🔍MagnifyGlass.FollowCursor": false, + "🔍MagnifyGlass.DebugMode": false, + "🔍MagnifyGlass.OffsetStep": DEFAULT_OFFSET_STEP, + "🔍MagnifyGlass.GlassPosition": "Top-Right", + "🔍MagnifyGlass.ResetKey": "o", + "🔍MagnifyGlass.GlassShape": "Rounded Square", + "🔍MagnifyGlass.BorderEnabled": true, + "🔍MagnifyGlass.TextureFiltering": "Linear", + "🔍MagnifyGlass.AlwaysActiveMode": true, + "🔍MagnifyGlass.ToggleFollowCursorKey": "h", +}; + +/** + * Default settings for the info panel component. + */ +export const DEFAULT_PANEL_SETTINGS: PanelSettings = { + "🔍MagnifyGlass.InfoPanelEnabled": true, + "🔍MagnifyGlass.InfoPanelPosition": "Bottom", + "🔍MagnifyGlass.InfoPanelWidth": DEFAULT_PANEL_WIDTH, + "🔍MagnifyGlass.InfoPanelOpacity": DEFAULT_PANEL_OPACITY, + "🔍MagnifyGlass.InfoPanelMaxHeight": DEFAULT_PANEL_MAX_HEIGHT, + "🔍MagnifyGlass.InfoPanelAnimations": false, + "🔍MagnifyGlass.ShowInspectorTab": false, + "🔍MagnifyGlass.ToggleHotkey": "i", + "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g", + "🔍MagnifyGlass.PinPanelHotkey": "u", + "🔍MagnifyGlass.ShowHoveringControls": true, + "🔍MagnifyGlass.ControlsPosition": "bottom", + "🔍MagnifyGlass.InfoPanelTextColor": "#6b7280", + "🔍MagnifyGlass.InfoPanelAccentColor": "#3b82f6", +}; + +/** + * Register all magnify glass settings with ComfyUI. + * @param magnifyGlass - The MagnifyGlass instance + */ +export function registerGlassSettings(magnifyGlass: any): void { + const settings = DEFAULT_GLASS_SETTINGS; + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ZoomFactor", + name: "🔍 Magnify Glass: Zoom Factor (%)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.ZoomFactor"], + min: 100, + max: 1000, + step: 25, + tooltip: "Magnification level as a percentage (e.g., 300 = 3x zoom, 150 = 1.5x zoom).", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.zoomFactor = typeof value === 'number' ? value / 100 : parseFloat(value) / 100; + if (magnifyGlass.state.active) { + magnifyGlass.updateMagnifiedView(); + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.GlassSize", + name: "🔍 Magnify Glass: Size (px)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.GlassSize"], + min: 50, + max: 100, + step: 10, + tooltip: "Diameter of the magnifying glass circle in pixels.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.glassSize = parseInt(String(value), 10); + magnifyGlass.applyUiChanges(); + if (magnifyGlass.state.active) { + magnifyGlass.updateMagnifiedView(); + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.BorderWidth", + name: "🔍 Magnify Glass: Border Width (px)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.BorderWidth"], + min: 0, + max: 10, + step: 0.1, + tooltip: "Width of the border around the magnifying glass.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.borderWidth = parseFloat(String(value)); + magnifyGlass.applyUiChanges(); + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.BorderColor", + name: "🎨 Magnify Glass: Border Color", + type: "color", + defaultValue: settings["🔍MagnifyGlass.BorderColor"], + tooltip: "Color of the border around the magnifying glass.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + const strValue = String(value); + const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue; + magnifyGlass.config.borderColor = normalizedColor; + if (strValue !== normalizedColor) { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.BorderColor", normalizedColor); + } catch (e) { + console.warn('Failed to save normalized border color:', e); + } + } + magnifyGlass.applyUiChanges(); + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ActivationKey", + name: "⌨️ Magnify Glass: Activation Key", + type: "combo", + options: ACTIVATION_KEYS.map(k => ({ value: k, text: k })), // Assuming ACTIVATION_KEYS is simply an array of strings + defaultValue: settings["🔍MagnifyGlass.ActivationKey"], + tooltip: "The key to activate the magnifier.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.activationKey = String(value).toLowerCase(); + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.AltRequired", + name: "⌨️ Magnify Glass: Require Alt/Option Key", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.AltRequired"], + tooltip: "If Yes, Alt (Windows/Linux) or Option (Mac) must be held for activation.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.altRequired = !!value; + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.FollowCursor", + name: "🖱️ Magnify Glass: Follow Cursor Position", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.FollowCursor"], + tooltip: "If Yes, the magnifier window moves with the cursor.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.followCursor = !!value; + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.OffsetStep", + name: "⌨️ Magnify Glass: Offset Adjust Step", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.OffsetStep"], + min: 1, + max: 50, + step: 1, + tooltip: "How many graph units the view shifts when pressing arrow keys.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.offsetStep = parseInt(String(value), 10); + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ResetKey", + name: "⌨️ Magnify Glass: Reset Offset Key", + type: "combo", + options: RESET_KEYS.map(k => ({ value: k, text: k })), + defaultValue: settings["🔍MagnifyGlass.ResetKey"], + tooltip: "The key to reset the magnify glass offset to default.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.resetKey = String(value).toLowerCase(); + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.DebugMode", + name: "🐞 Magnify Glass: Debug Mode", + type: "combo", + options: [{ value: true, text: "Enabled" }, { value: false, text: "Disabled" }], + defaultValue: settings["🔍MagnifyGlass.DebugMode"], + tooltip: "Show detailed logging and the debug visualization overlay.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.debugMode = !!value; + magnifyGlass.applyUiChanges(); + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.GlassPosition", + name: "🖱️ Magnify Glass: Glass Position", + type: "combo", + options: GLASS_POSITIONS.map(p => ({ value: p, text: p === "Bottom" ? "Bottom (Default)" : p })), + defaultValue: settings["🔍MagnifyGlass.GlassPosition"], + tooltip: "Position of the magnifying glass relative to the cursor.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.glassPosition = String(value); + if (magnifyGlass.state.active && !magnifyGlass.config.followCursor) { + const { x, y } = magnifyGlass.lastKnownMousePosition; + magnifyGlass.ui.positionGlass(x, y); + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.GlassShape", + name: "🖼️ Magnify Glass: Shape", + type: "combo", + options: GLASS_SHAPES.map(s => ({ value: s, text: s })), + defaultValue: settings["🔍MagnifyGlass.GlassShape"], + tooltip: "Shape of the magnifying glass.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.glassShape = String(value); + magnifyGlass.applyUiChanges(); + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.BorderEnabled", + name: "🖼️ Magnify Glass: Show Border", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.BorderEnabled"], + tooltip: "Enable or disable the border around the magnifying glass.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.borderEnabled = !!value; + magnifyGlass.applyUiChanges(); + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.TextureFiltering", + name: "🖼️ Magnify Glass: Texture Filtering", + type: "combo", + options: [ + { value: "Linear", text: "Linear (Smooth)" }, + { value: "Nearest", text: "Nearest (Pixelated)" } + ], + defaultValue: settings["🔍MagnifyGlass.TextureFiltering"], + tooltip: "Controls how the magnified image is scaled.", + onChange: (value: any) => { + if (magnifyGlass?.config && magnifyGlass.renderer) { + magnifyGlass.config.textureFiltering = String(value); + magnifyGlass.renderer.updateTextureFiltering(value); + if (magnifyGlass.state.active) { + magnifyGlass.updateMagnifiedView(); + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.AlwaysActiveMode", + name: "🔒 Magnify Glass: Always Active Mode", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.AlwaysActiveMode"], + tooltip: "If Yes, activating the magnifier keeps it on until activated again.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.alwaysActiveMode = !!value; + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ToggleFollowCursorKey", + name: "🔑 Magnify Glass: Toggle Follow Key", + type: "combo", + options: TOGGLE_FOLLOW_KEYS.map(k => ({ value: k, text: k })), + defaultValue: settings["🔍MagnifyGlass.ToggleFollowCursorKey"], + tooltip: "The key to toggle the 'Follow Cursor' behavior.", + onChange: (value: any) => { + if (magnifyGlass?.config) { + magnifyGlass.config.toggleFollowCursorKey = String(value).toLowerCase(); + } + } + }); +} + +/** + * Register all info panel settings with ComfyUI. + * @param stateManager - The StateManager instance for the info panel + * @param uiManager - The UIManager instance for the info panel + * @param positionManager - The PositionManager instance for the info panel + */ +export function registerPanelSettings(stateManager: any, uiManager: any, positionManager: any): void { + const settings = DEFAULT_PANEL_SETTINGS; + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelEnabled", + name: "📊 Info Panel: Enable", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.InfoPanelEnabled"], + tooltip: "Enable or disable the information panel.", + onChange: (value: any) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] = !!value; + if (!value && uiManager) { + uiManager.hide(); + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelPosition", + name: "📊 Info Panel: Position", + type: "combo", + options: PANEL_POSITIONS.map(p => ({ value: p, text: p })), + defaultValue: settings["🔍MagnifyGlass.InfoPanelPosition"], + tooltip: "Position of the info panel relative to the magnifying glass.", + onChange: (value: any) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelPosition"] = String(value); + if (positionManager) { + positionManager.positionPanel(); + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelWidth", + name: "📊 Info Panel: Width (px)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.InfoPanelWidth"], + min: 200, + max: 600, + step: 20, + tooltip: "Width of the information panel in pixels.", + onChange: (value: any) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelWidth"] = parseInt(String(value), 10); + if (uiManager) { + uiManager.applyStyles(); + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelOpacity", + name: "📊 Info Panel: Opacity (%)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.InfoPanelOpacity"], + min: 10, + max: 100, + step: 5, + tooltip: "Opacity of the information panel background.", + onChange: (value: any) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"] = parseInt(String(value), 10); + if (uiManager) { + uiManager.applyStyles(); + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelMaxHeight", + name: "📊 Info Panel: Max Height (px)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.InfoPanelMaxHeight"], + min: 200, + max: 1500, + step: 50, + tooltip: "Maximum height of the information panel in pixels.", + onChange: (value: any) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelMaxHeight"] = parseInt(String(value), 10); + if (uiManager) { + uiManager.applyStyles(); + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelAnimations", + name: "📊 Info Panel: Animations", + type: "combo", + options: [{ value: true, text: "Enabled" }, { value: false, text: "Disabled" }], + defaultValue: settings["🔍MagnifyGlass.InfoPanelAnimations"], + tooltip: "Enable or disable animations for the info panel.", + onChange: (value: any) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] = !!value; + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ShowInspectorTab", + name: "📊 Info Panel: Show Inspector Tab", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.ShowInspectorTab"], + tooltip: "Show or hide the Inspector tab with cursor and canvas information.", + onChange: (value: any) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.ShowInspectorTab"] = !!value; + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ShowHoveringControls", + name: "📊 Info Panel: Show Hover Controls", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.ShowHoveringControls"], + tooltip: "Show or hide hovering UI controls above the info panel.", + onChange: (value: any) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"] = !!value; + if (uiManager && uiManager.elements.controls) { + uiManager.elements.controls.style.display = value ? 'flex' : 'none'; + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelTextColor", + name: "🎨 Info Panel: Text Color", + type: "color", + defaultValue: settings["🔍MagnifyGlass.InfoPanelTextColor"], + tooltip: "Custom text color for the info panel.", + onChange: (value: any) => { + if (stateManager) { + const strValue = String(value); + const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue; + stateManager.state.settings["🔍MagnifyGlass.InfoPanelTextColor"] = normalizedColor; + if (uiManager) { + uiManager.applyStyles(); + } + } + } + }); + + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelAccentColor", + name: "🎨 Info Panel: Accent Color", + type: "color", + defaultValue: settings["🔍MagnifyGlass.InfoPanelAccentColor"], + tooltip: "Custom accent color for the info panel.", + onChange: (value: any) => { + if (stateManager) { + const strValue = String(value); + const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue; + stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"] = normalizedColor; + if (uiManager) { + uiManager.applyStyles(); + } + } + } + }); +} + +/** + * Get a setting configuration object for ComfyUI settings registration. + * This function generates the setting config based on the key and default value. + * + * @param key - The setting key + * @param defaultValue - The default value for the setting + * @returns The setting configuration or null if not found + */ +export function getSettingConfig(key: string, defaultValue: any): any { + const configs: Record = { + "🔍MagnifyGlass.InfoPanelEnabled": { + id: key, + name: "📋 Magnify Glass: Info Panel", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Enable or disable the professional information panel." + }, + "🔍MagnifyGlass.InfoPanelPosition": { + id: key, + name: "📋 Info Panel: Position", + type: "combo", + options: PANEL_POSITIONS.map(pos => ({ value: pos, text: pos })), + defaultValue, + tooltip: "Position of the info panel relative to the magnify glass." + }, + "🔍MagnifyGlass.InfoPanelWidth": { + id: key, + name: "📋 Info Panel: Width (px)", + type: "slider", + defaultValue, + min: 200, + max: 600, + step: 20, + tooltip: "Width of the info panel in pixels." + }, + "🔍MagnifyGlass.InfoPanelMaxHeight": { + id: key, + name: "📋 Info Panel: Max Height (px)", + type: "slider", + defaultValue, + min: 200, + max: 1500, + step: 50, + tooltip: "Maximum height of the info panel." + }, + "🔍MagnifyGlass.InfoPanelOpacity": { + id: key, + name: "📋 Info Panel: Opacity (%)", + type: "slider", + defaultValue, + min: 10, + max: 100, + step: 5, + tooltip: "Opacity of the info panel background." + }, + "🔍MagnifyGlass.InfoPanelAnimations": { + id: key, + name: "📋 Info Panel: Animations", + type: "combo", + options: [ + { value: true, text: "Enabled" }, + { value: false, text: "Disabled" } + ], + defaultValue, + tooltip: "Enable smooth animations for the info panel." + }, + "🔍MagnifyGlass.ShowInspectorTab": { + id: key, + name: "📋 Info Panel: Show Inspector", + type: "combo", + options: [ + { value: true, text: "Yes" }, + { value: false, text: "No" } + ], + defaultValue, + tooltip: "Show the inspector tab with cursor and canvas information." + }, + "🔍MagnifyGlass.ToggleHotkey": { + id: key, + name: "📋 Info Panel: Toggle Hotkey", + type: "combo", + options: ["i", "j", "k", "l", "n", "m"].map(k => ({ value: k, text: k.toUpperCase() })), + defaultValue, + tooltip: "Key to toggle the info panel visibility." + }, + "🔍MagnifyGlass.GlassPreviewToggleHotkey": { + id: key, + name: "📋 Info Panel: Glass Toggle Hotkey", + type: "combo", + options: ["g", "f", "v", "b"].map(k => ({ value: k, text: k.toUpperCase() })), + defaultValue, + tooltip: "Key to toggle the magnify glass preview visibility." + }, + "🔍MagnifyGlass.PinPanelHotkey": { + id: key, + name: "📋 Info Panel: Pin Hotkey", + type: "combo", + options: ["u", "p", "y", "t"].map(k => ({ value: k, text: k.toUpperCase() })), + defaultValue, + tooltip: "Key to pin/unlock the info panel at the current mouse location." + }, + "🔍MagnifyGlass.ShowHoveringControls": { + id: key, + name: "📋 Info Panel: Floating Controls", + type: "combo", + options: [ + { value: true, text: "Show" }, + { value: false, text: "Hide" } + ], + defaultValue, + tooltip: "Show floating control buttons near the info panel." + }, + "🔍MagnifyGlass.ControlsPosition": { + id: key, + name: "📋 Info Panel: Controls Position", + type: "combo", + options: ["top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right"].map(p => ({ value: p, text: p })), + defaultValue, + tooltip: "Position of the floating control buttons." + }, + "🔍MagnifyGlass.InfoPanelTextColor": { + id: key, + name: "🎨 Info Panel: Text Color", + type: "color", + defaultValue, + tooltip: "Color for text in the info panel." + }, + "🔍MagnifyGlass.InfoPanelAccentColor": { + id: key, + name: "🎨 Info Panel: Accent Color", + type: "color", + defaultValue, + tooltip: "Accent color for highlights in the info panel." + } + }; + + return configs[key] || null; +} diff --git a/src/shared/utils.ts b/src/shared/utils.ts new file mode 100644 index 0000000..30c1ed7 --- /dev/null +++ b/src/shared/utils.ts @@ -0,0 +1,161 @@ +/** + * ComfyUI MagnifyGlass - Shared Utilities + * + * Common utility functions shared between magnify glass and info panel modules. + */ + +import type { ComfyApp } from '../types/comfyui'; + +// Reference to ComfyUI's app - will be set at runtime +// @ts-ignore +import { app } from "/scripts/app.js"; + +/** + * Rectangle interface for overlap calculations + */ +export interface Rectangle { + x: number; + y: number; + width: number; + height: number; +} + +/** + * Debug logger interface + */ +export interface DebugLogger { + log(...args: unknown[]): void; + error(...args: unknown[]): void; +} + +/** + * Safely get a setting value from ComfyUI settings with fallback to default. + * @param key - The setting key + * @param defaultValue - Default value if setting not found + * @returns The setting value or default + */ +export function getSettingValue(key: string, defaultValue: T): T { + try { + const value = app.ui.settings.getSettingValue(key); + return value === undefined ? defaultValue : value as T; + } catch (e) { + console.warn(`ComfyUI Magnifying Glass: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`); + return defaultValue; + } +} + +/** + * Check if the user is currently typing in an input field. + * Used for smart input detection to avoid triggering hotkeys while typing. + * @returns True if user is typing in an input field + */ +export function isUserTyping(): boolean { + const activeElement = document.activeElement as HTMLElement | null; + if (!activeElement) return false; + + // Check if the active element is an input, textarea, or contenteditable + const tagName = activeElement.tagName.toLowerCase(); + if (tagName === 'input' || tagName === 'textarea') { + return true; + } + + // Check for contenteditable elements + if (activeElement.contentEditable === 'true') { + return true; + } + + // Check if it's inside a form or has input-like classes + if (activeElement.closest('form') || + activeElement.classList.contains('cm-editor') || // CodeMirror editor + activeElement.classList.contains('monaco-editor') || // Monaco editor + activeElement.closest('.cm-editor') || + activeElement.closest('.monaco-editor')) { + return true; + } + + return false; +} + +/** + * Check if two rectangles overlap. + * @param rect1 - First rectangle + * @param rect2 - Second rectangle + * @returns True if rectangles overlap + */ +export function rectsOverlap(rect1: Rectangle, rect2: Rectangle): boolean { + return rect1.x < rect2.x + rect2.width && + rect1.x + rect1.width > rect2.x && + rect1.y < rect2.y + rect2.height && + rect1.y + rect1.height > rect2.y; +} + +/** + * Find the LiteGraph canvas element in the DOM. + * Tries multiple selectors for compatibility with different ComfyUI versions. + * @returns The canvas element or null if not found + */ +export function findLiteGraphCanvas(): HTMLCanvasElement | null { + // Try multiple selectors for compatibility + const canvas = document.getElementById("graph-canvas") as HTMLCanvasElement | null; + if (canvas) return canvas; + + const graphCanvas = document.querySelector("canvas.graphcanvas") as HTMLCanvasElement | null; + if (graphCanvas) return graphCanvas; + + // Try canvas manager (dynamic access for ComfyUI compatibility) + const appAny = app as any; + if (appAny?.canvas_manager?.container) { + const managerCanvas = appAny.canvas_manager.container.querySelector("canvas") as HTMLCanvasElement | null; + if (managerCanvas) return managerCanvas; + } + + // Try direct canvas reference + if (appAny?.canvas?.graph_canvas) { + return appAny.canvas.graph_canvas; + } + + return null; +} + +/** + * Normalize a color value to include the # symbol for CSS. + * @param color - The color value (with or without #) + * @returns The normalized color with # prefix + */ +export function normalizeColor(color: string | null | undefined): string { + if (!color) return color ?? ''; + return color.startsWith('#') ? color : `#${color}`; +} + +/** + * Clamp a value between min and max bounds. + * @param value - The value to clamp + * @param min - Minimum bound + * @param max - Maximum bound + * @returns The clamped value + */ +export function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +/** + * Create a debug logger that only logs when debug mode is enabled. + * @param prefix - Prefix for log messages + * @param isDebugEnabled - Function that returns whether debug is enabled + * @returns Logger object + */ +export function createDebugLogger( + prefix: string, + isDebugEnabled: () => boolean +): DebugLogger { + return { + log(...args: unknown[]): void { + if (isDebugEnabled()) { + console.log(`${prefix}:`, ...args); + } + }, + error(...args: unknown[]): void { + console.error(`${prefix} ERROR:`, ...args); + } + }; +} diff --git a/src/types/comfy-scripts.d.ts b/src/types/comfy-scripts.d.ts new file mode 100644 index 0000000..167ecd9 --- /dev/null +++ b/src/types/comfy-scripts.d.ts @@ -0,0 +1,11 @@ +declare module "../../scripts/app.js" { + export const app: any; +} + +declare module "../../scripts/ui.js" { + export const ui: any; +} + +declare module "../../scripts/api.js" { + export const api: any; +} diff --git a/src/types/comfyui.d.ts b/src/types/comfyui.d.ts new file mode 100644 index 0000000..d576663 --- /dev/null +++ b/src/types/comfyui.d.ts @@ -0,0 +1,264 @@ +/** + * ComfyUI Type Definitions + * + * Type definitions for ComfyUI's app, canvas, and graph APIs. + * These types are based on the runtime behavior of ComfyUI. + */ + +// ============================================================================ +// ComfyUI App +// ============================================================================ + +export interface ComfyApp { + graph: ComfyGraph; + canvas: ComfyCanvas; + ui: ComfyUI; + extensionManager: ComfyExtensionManager; + registerExtension(extension: ComfyExtension): void; +} + +export interface ComfyExtension { + name: string; + setup?: () => Promise | void; + init?: () => Promise | void; + commands?: ComfyCommand[]; + keybindings?: ComfyKeybinding[]; + menuCommands?: ComfyMenuCommand[]; + settings?: ComfySetting[]; + bottomPanelTabs?: ComfyBottomPanelTab[]; + aboutPageBadges?: ComfyBadge[]; + getSelectionToolboxCommands?: (selectedItem: unknown) => string[]; +} + +export interface ComfyCommand { + id: string; + label?: string; + icon?: string; + function: () => void; +} + +export interface ComfyKeybinding { + combo: { + key: string; + alt?: boolean; + ctrl?: boolean; + shift?: boolean; + meta?: boolean; + }; + commandId: string; +} + +export interface ComfyMenuCommand { + path: string[]; + commands: string[]; +} + +export interface ComfySetting { + id: string; + name: string; + type: 'text' | 'number' | 'slider' | 'combo' | 'color' | 'boolean'; + defaultValue: unknown; + min?: number; + max?: number; + step?: number; + options?: Array<{ value: unknown; text: string }>; + tooltip?: string; + onChange?: (value: unknown) => void; +} + +export interface ComfyBottomPanelTab { + id: string; + title: string; + type: 'custom'; + render: (el: HTMLElement) => void; +} + +export interface ComfyBadge { + label: string; + url: string; + icon: string; +} + +// ============================================================================ +// ComfyUI UI +// ============================================================================ + +export interface ComfyUI { + settings: ComfySettings; +} + +export interface ComfySettings { + addSetting(setting: ComfySetting): void; + getSettingValue(id: string): unknown; + setSettingValue(id: string, value: unknown): void; +} + +// ============================================================================ +// ComfyUI Extension Manager +// ============================================================================ + +export interface ComfyExtensionManager { + setting: { + get(id: string): unknown; + set(id: string, value: unknown): void; + }; + toast: ComfyToast; + dialog: ComfyDialog; + registerSidebarTab(tab: ComfySidebarTab): void; +} + +export interface ComfySidebarTab { + id: string; + icon: string; + title: string; + tooltip: string; + type: 'custom'; + render: (el: HTMLElement) => void; +} + +export interface ComfyToast { + add(options: ComfyToastOptions): void; + addAlert(message: string): void; +} + +export interface ComfyToastOptions { + severity: 'success' | 'info' | 'warn' | 'error'; + summary: string; + detail: string; + life?: number; +} + +export interface ComfyDialog { + prompt(options: { title: string; message: string }): Promise; + confirm(options: { title: string; message: string }): Promise; +} + +// ============================================================================ +// ComfyUI Graph +// ============================================================================ + +export interface ComfyGraph { + _nodes: ComfyNode[]; + getNodeById(id: number): ComfyNode | null; +} + +export interface ComfyNode { + id: number; + title: string; + type: string; + mode: number; + pos: [number, number]; + size: [number, number]; + flags?: { + collapsed?: boolean; + }; + widgets?: ComfyWidget[]; + inputs?: ComfySlot[]; + outputs?: ComfySlot[]; + properties?: Record; +} + +export interface ComfyWidget { + name: string; + type: string; + value: unknown; + options?: Record; + min?: number; + max?: number; + step?: number; +} + +export interface ComfySlot { + name: string; + type: string; + link?: number | null; + links?: number[]; +} + +// ============================================================================ +// ComfyUI Canvas +// ============================================================================ + +export interface ComfyCanvas { + canvas: HTMLCanvasElement; + ds: { + scale: number; + offset: [number, number]; + }; + node_over: ComfyNode | null; + convertOffsetToCanvasPos?(pos: [number, number]): [number, number]; +} + +// ============================================================================ +// Global Window Extensions +// ============================================================================ + +declare global { + interface Window { + comfyUIMagnifyGlass?: MagnifyGlassInstance; + comfyUIMagnifyGlassExtensions?: unknown[]; + infoPanelManager?: { uiManager?: { updateTheme: (theme: string) => void } }; + } + + var app: ComfyApp; +} + +export interface MagnifyGlassInstance { + state: MagnifyGlassState; + config: MagnifyGlassConfig; + ui: MagnifyGlassUI; + renderer: MagnifyGlassRenderer | null; + lastKnownMousePosition: { x: number; y: number }; + isOverMedia: boolean; + currentMediaElement: HTMLImageElement | HTMLVideoElement | null; + updateMagnifiedView(): void; + applyUiChanges(): void; + toggle(): void; +} + +export interface MagnifyGlassState { + active: boolean; + x: number; + y: number; + sourceX: number; + sourceY: number; + sourceWidth: number; + sourceHeight: number; + canvasScale: number; + canvasOffsetX: number; + canvasOffsetY: number; +} + +export interface MagnifyGlassConfig { + zoomFactor: number; + glassSize: number; + borderColor: string; + borderWidth: number; + borderEnabled: boolean; + activationKey: string; + altRequired: boolean; + followCursor: boolean; + debugMode: boolean; + offsetStep: number; + offsetX: number; + offsetY: number; + glassPosition: string; + glassShape: string; + resetKey: string; + textureFiltering: string; + alwaysActiveMode: boolean; + toggleFollowCursorKey: string; +} + +export interface MagnifyGlassUI { + glassDiv: HTMLDivElement | null; + show(): void; + hide(): void; + positionGlass(x: number, y: number): void; +} + +export interface MagnifyGlassRenderer { + updateTextureFiltering(filtering: string): void; +} + +export { }; diff --git a/src/types/vue-shims.d.ts b/src/types/vue-shims.d.ts new file mode 100644 index 0000000..1a9fbc3 --- /dev/null +++ b/src/types/vue-shims.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/tests/e2e/example.spec.ts b/tests/e2e/example.spec.ts new file mode 100644 index 0000000..ba173c5 --- /dev/null +++ b/tests/e2e/example.spec.ts @@ -0,0 +1,12 @@ +import { test, expect } from '@playwright/test'; + +test('magnify glass extension loads', async ({ page }) => { + // This is a placeholder test. + // To test ComfyUI, you would typically need to start the ComfyUI server and navigate to it. + // console.log('This test needs a running ComfyUI instance to verify the extension.'); + + // Example of what we might check if ComfyUI was running locally: + // await page.goto('http://127.0.0.1:8188'); + // await expect(page).toHaveTitle(/ComfyUI/); + // await expect(page.locator('.magnify-glass-icon')).toBeVisible(); // Hypothetical check +}); diff --git a/tests/unit/ComfyPanel.test.ts b/tests/unit/ComfyPanel.test.ts new file mode 100644 index 0000000..f14e220 --- /dev/null +++ b/tests/unit/ComfyPanel.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import ComfyPanel from '../../src/components/ComfyPanel.vue'; + +describe('ComfyPanel.vue', () => { + it('renders title correctly', () => { + const title = 'Test Panel'; + const wrapper = mount(ComfyPanel, { + props: { title } + }); + expect(wrapper.find('h1').text()).toBe(title); + }); + + it('renders slot content', () => { + const wrapper = mount(ComfyPanel, { + slots: { + default: '
Content
' + } + }); + expect(wrapper.find('.test-content').exists()).toBe(true); + }); + + it('applies theme class', () => { + const wrapper = mount(ComfyPanel, { + props: { theme: 'light' } + }); + expect(wrapper.classes()).toContain('theme-light'); + }); +}); diff --git a/tests/unit/InfoPanel.test.ts b/tests/unit/InfoPanel.test.ts new file mode 100644 index 0000000..47560db --- /dev/null +++ b/tests/unit/InfoPanel.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import InfoPanel from '../../src/info-panel/components/InfoPanel.vue'; + +describe('InfoPanel.vue', () => { + it('is initially hidden if initialVisible is false', () => { + const wrapper = mount(InfoPanel, { + props: { initialVisible: false } + }); + expect(wrapper.find('.mag-info-panel').exists()).toBe(false); + }); + + it('renders node info when hoveredNode is present', async () => { + const wrapper = mount(InfoPanel, { + props: { initialVisible: true } + }); + + // Simulate state update (since component uses internal ref, we need to access exposed method or setup state) + // In our component definition, we returned `updateState`. + // However, simpler for this test is to modify the internal state if we can, or refactor component to take props. + // Given the component uses `visible` ref initialized from prop, checking it exists is tricky if it's strictly v-if. + + // Let's rely on the fact that we can interact with the component instance + const vm = wrapper.vm as any; + vm.visible = true; + vm.hoveredNode = { id: 123, title: 'Test Node', type: 'TestType' }; + + await wrapper.vm.$nextTick(); + + expect(wrapper.text()).toContain('Test Node'); + expect(wrapper.text()).toContain('ID:'); + expect(wrapper.text()).toContain('123'); + }); + + it('toggles pin state', async () => { + const wrapper = mount(InfoPanel, { + props: { initialVisible: true } + }); + const vm = wrapper.vm as any; + vm.visible = true; + await wrapper.vm.$nextTick(); + + const pinBtn = wrapper.find('button'); + expect(vm.pinned).toBe(false); + + await pinBtn.trigger('click'); + expect(vm.pinned).toBe(true); + }); +}); diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts new file mode 100644 index 0000000..cbc0138 --- /dev/null +++ b/tests/unit/utils.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { clamp, normalizeColor } from '../../src/shared/utils'; + +describe('Shared Utilities', () => { + describe('clamp', () => { + it('should clamp value within range', () => { + expect(clamp(150, 0, 100)).toBe(100); + expect(clamp(-50, 0, 100)).toBe(0); + expect(clamp(50, 0, 100)).toBe(50); + }); + }); + + describe('normalizeColor', () => { + it('should add # prefix if missing', () => { + expect(normalizeColor('ffffff')).toBe('#ffffff'); + expect(normalizeColor('000')).toBe('#000'); + }); + + it('should keep # prefix if present', () => { + expect(normalizeColor('#ff0000')).toBe('#ff0000'); + }); + + it('should handle falsy values', () => { + expect(normalizeColor('')).toBe(''); + // @ts-ignore + expect(normalizeColor(null)).toBe(null); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1f86930 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "baseUrl": ".", + "paths": { + "@shared/*": [ + "src/shared/*" + ], + "@magnify-glass/*": [ + "src/magnify-glass/*" + ], + "@info-panel/*": [ + "src/info-panel/*" + ] + } + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts" + ], + "exclude": [ + "node_modules", + "dist", + "web" + ] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0e1bbc1 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,47 @@ +/// +import { defineConfig } from 'vite'; +import { resolve } from 'path'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + define: { + 'process.env': {} + }, + plugins: [vue()], + build: { + lib: { + entry: { + 'magnify_glass': resolve(__dirname, 'src/magnify_glass.ts'), + 'magnify_info_panel': resolve(__dirname, 'src/magnify_info_panel.ts') + }, + formats: ['es'], + fileName: (format, entryName) => `${entryName}.js` + }, + outDir: 'web', + emptyOutDir: false, // Keep existing files + rollupOptions: { + external: [ + /^\/scripts\//, // Match /scripts/ (Absolute path) + /^\.\.\/\.\.\/scripts\//, // Match ../../scripts/ (Browser path - deprecated) + ], + output: { + preserveModules: true, + preserveModulesRoot: 'src', + entryFileNames: '[name].js', + } + }, + sourcemap: true, + minify: false // Keep readable for debugging + }, + resolve: { + alias: { + '@shared': resolve(__dirname, 'src/shared'), + '@magnify-glass': resolve(__dirname, 'src/magnify-glass'), + '@info-panel': resolve(__dirname, 'src/info-panel') + } + }, + test: { + environment: 'happy-dom', // or jsdom, useful for component testing + globals: true + } +}); diff --git a/web/_virtual/_plugin-vue_export-helper.js b/web/_virtual/_plugin-vue_export-helper.js new file mode 100644 index 0000000..aa20d46 --- /dev/null +++ b/web/_virtual/_plugin-vue_export-helper.js @@ -0,0 +1,11 @@ +const _export_sfc = (sfc, props) => { + const target = sfc.__vccOpts || sfc; + for (const [key, val] of props) { + target[key] = val; + } + return target; +}; +export { + _export_sfc as default +}; +//# sourceMappingURL=_plugin-vue_export-helper.js.map diff --git a/web/_virtual/_plugin-vue_export-helper.js.map b/web/_virtual/_plugin-vue_export-helper.js.map new file mode 100644 index 0000000..29778a8 --- /dev/null +++ b/web/_virtual/_plugin-vue_export-helper.js.map @@ -0,0 +1 @@ +{"version":3,"file":"_plugin-vue_export-helper.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;"} \ No newline at end of file diff --git a/web/info-panel/EventManager.js b/web/info-panel/EventManager.js index 2f54869..c1153f5 100644 --- a/web/info-panel/EventManager.js +++ b/web/info-panel/EventManager.js @@ -1,455 +1,115 @@ -/** - * ComfyUI MagnifyGlass - Info Panel Event Manager - * - * Complete Event Manager extracted from magnify_info_panel.js - * Handles all event binding and delegation for the info panel. - */ - -import { isUserTyping } from '../shared/utils.js'; - -/** - * Event Manager class. - * Handles all event binding and delegation. - */ -export class EventManager { - /** - * @param {import('./StateManager.js').StateManager} stateManager - * @param {import('./UIManager.js').UIManager} uiManager - * @param {import('./PositionManager.js').PositionManager} positionManager - */ - constructor(stateManager, uiManager, positionManager) { - this.stateManager = stateManager; - this.uiManager = uiManager; - this.positionManager = positionManager; - this.dragCleanup = null; - - this.setupEventListeners(); - } - - setupEventListeners() { - this.setupPanelEvents(); - this.setupHotkeyEvents(); - this.setupDragEvents(); - this.setupHoverEvents(); - } - - setupPanelEvents() { - // Event delegation for panel controls - this.uiManager.elements.panel.addEventListener('click', (e) => { - const action = e.target.dataset.action; - if (action) { - e.stopPropagation(); - e.preventDefault(); - this.handleControlAction(action); - } - - // Section header clicks - const sectionHeader = e.target.closest('.section-header'); - if (sectionHeader && sectionHeader.dataset.section) { - const sectionId = sectionHeader.dataset.section; - if (sectionId !== 'node') { // Node section is always expanded - this.stateManager.toggleSection(sectionId); - this.uiManager.updateSectionStates(); - } - } - }); - - // Event delegation for floating controls (separate since they're not children of panel) - if (this.uiManager.elements.controls) { - this.uiManager.elements.controls.addEventListener('click', (e) => { - const action = e.target.dataset.action; - if (action) { - e.stopPropagation(); - e.preventDefault(); - this.handleControlAction(action); - } - }); - - // Prevent floating controls from propagating events - this.uiManager.elements.controls.addEventListener('mousedown', (e) => { - e.stopPropagation(); - }); - - this.uiManager.elements.controls.addEventListener('mouseup', (e) => { - e.stopPropagation(); - }); +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { isUserTyping } from "../shared/utils.js"; +class EventManager { + constructor(stateManager, panelElement, positionManager, callbacks) { + __publicField(this, "stateManager"); + __publicField(this, "panelElement"); + __publicField(this, "positionManager"); + __publicField(this, "callbacks"); + __publicField(this, "dragCleanup"); + __publicField(this, "boundHandleMouseMove"); + __publicField(this, "boundHandleKeyDown"); + __publicField(this, "boundHandleKeyUp"); + this.stateManager = stateManager; + this.panelElement = panelElement; + this.positionManager = positionManager; + this.callbacks = callbacks || null; + this.dragCleanup = null; + this.boundHandleMouseMove = this.handleMouseMove.bind(this); + this.boundHandleKeyDown = this.handleKeyDown.bind(this); + this.boundHandleKeyUp = this.handleKeyUp.bind(this); + this.setupEventListeners(); + } + setupEventListeners() { + this.setupPanelEvents(); + this.setupHotkeyEvents(); + this.setupDragEvents(); + this.setupHoverEvents(); + } + setupPanelEvents() { + if (!this.panelElement) return; + } + setupHotkeyEvents() { + document.addEventListener("keydown", this.boundHandleKeyDown); + } + setupDragEvents() { + if (!this.panelElement) return; + const startDrag = (e) => { + if (!this.panelElement) return; + const target = e.target; + if (!target.closest(".mag-panel-header")) return; + if (target.closest("button")) return; + e.preventDefault(); + const startX = e.clientX; + const startY = e.clientY; + const rect = this.panelElement.getBoundingClientRect(); + const startLeft = rect.left; + const startTop = rect.top; + const onMouseMove = (moveEvent) => { + const dx = moveEvent.clientX - startX; + const dy = moveEvent.clientY - startY; + if (this.panelElement) { + this.panelElement.style.left = `${startLeft + dx}px`; + this.panelElement.style.top = `${startTop + dy}px`; } - - // Prevent panel interactions from propagating - this.uiManager.elements.panel.addEventListener('mousedown', (e) => { - e.stopPropagation(); - }); - - this.uiManager.elements.panel.addEventListener('mouseup', (e) => { - e.stopPropagation(); - }); - } - - // Helper function to check if user is typing in an input field - isUserTyping() { - const activeElement = document.activeElement; - if (!activeElement) return false; - - // Check if the active element is an input, textarea, or contenteditable - const tagName = activeElement.tagName.toLowerCase(); - if (tagName === 'input' || tagName === 'textarea') { - return true; + }; + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + if (this.panelElement) { + const finalRect = this.panelElement.getBoundingClientRect(); + this.stateManager.state.pinnedPosition = { x: finalRect.left, y: finalRect.top }; + if (!this.stateManager.state.isPanelPinned) ; } - - // Check for contenteditable elements - if (activeElement.contentEditable === 'true') { - return true; - } - - // Check if it's inside a form or has input-like classes - if (activeElement.closest('form') || - activeElement.classList.contains('cm-editor') || // CodeMirror editor - activeElement.classList.contains('monaco-editor') || // Monaco editor - activeElement.closest('.cm-editor') || - activeElement.closest('.monaco-editor')) { - return true; - } - - return false; + }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }; + this.panelElement.addEventListener("mousedown", startDrag); + this.dragCleanup = () => { + if (this.panelElement) this.panelElement.removeEventListener("mousedown", startDrag); + }; + } + setupHoverEvents() { + if (!this.panelElement) return; + this.panelElement.addEventListener("mouseenter", () => { + this.stateManager.state.isPanelHovered = true; + this.stateManager.clearAutoExpandTimer(); + }); + this.panelElement.addEventListener("mouseleave", () => { + this.stateManager.state.isPanelHovered = false; + if (!this.stateManager.state.isHoveringNode) { + this.stateManager.scheduleAutoCollapse(); + } + }); + } + handleKeyDown(e) { + var _a, _b; + if (isUserTyping()) return; + const settings = this.stateManager.state.settings; + const key = e.key.toLowerCase(); + if (key === settings["🔍MagnifyGlass.ToggleHotkey"].toLowerCase()) { + console.log("[InfoPanel] Toggle visibility hotkey matched!"); + e.preventDefault(); + (_a = this.callbacks) == null ? void 0 : _a.toggleVisibility(); } - - setupHotkeyEvents() { - const magnifyGlass = window.comfyUIMagnifyGlass; - - document.addEventListener('keydown', (e) => { - // Don't handle hotkeys if user is typing in an input field (Smart Input Detection) - if (this.isUserTyping()) { - return; - } - - // Only handle hotkeys when magnifying glass is active - const magnifyGlass = window.comfyUIMagnifyGlass; - if (!magnifyGlass || !magnifyGlass.state.active) return; - - const settings = this.stateManager.state.settings; - - // Toggle info panel - if (e.key.toLowerCase() === settings["🔍MagnifyGlass.ToggleHotkey"].toLowerCase() && - (!magnifyGlass.config.altRequired || e.altKey) && !e.repeat) { - e.preventDefault(); - e.stopPropagation(); - this.handleControlAction('toggle-panel'); - } - - // Toggle glass preview - if (e.key.toLowerCase() === settings["🔍MagnifyGlass.GlassPreviewToggleHotkey"].toLowerCase() && - (!magnifyGlass.config.altRequired || e.altKey) && !e.repeat) { - e.preventDefault(); - e.stopPropagation(); - this.handleControlAction('toggle-glass'); - } - - // Unlock panel from magnify glass and move to current mouse location (no Alt required due to Smart Input Detection) - if (e.key.toLowerCase() === settings["🔍MagnifyGlass.PinPanelHotkey"].toLowerCase() && !e.repeat) { - e.preventDefault(); - e.stopPropagation(); - this.handleControlAction('pin-at-mouse'); - } - }); - } - - setupDragEvents() { - let dragState = { - isDragging: false, - startX: 0, - startY: 0, - currentX: 0, - currentY: 0, - offsetX: 0, - offsetY: 0 - }; - - // Start drag - const startDrag = (e) => { - // Only when pinned and not locked - if (!this.stateManager.state.isPanelPinned || this.stateManager.state.isPanelLocked) return; - - // Block on specific elements only - if (e.target.closest('.minimize-btn') || - e.target.closest('.control-btn') || - e.target.tagName === 'BUTTON') { - return; - } - - // Stop all event propagation immediately - e.preventDefault(); - e.stopImmediatePropagation(); - - // Set drag state - dragState.isDragging = true; - dragState.startX = e.clientX; - dragState.startY = e.clientY; - - // Get current panel position - const rect = this.uiManager.elements.panel.getBoundingClientRect(); - dragState.currentX = rect.left; - dragState.currentY = rect.top; - dragState.offsetX = e.clientX - rect.left; - dragState.offsetY = e.clientY - rect.top; - - // Visual feedback - this.uiManager.elements.panel.classList.add('panel-dragging'); - this.uiManager.elements.panel.style.cursor = 'grabbing'; - document.body.style.cursor = 'grabbing'; - document.body.style.userSelect = 'none'; - }; - - // Handle drag movement - const doDrag = (e) => { - if (!dragState.isDragging) return; - - e.preventDefault(); - e.stopImmediatePropagation(); - - // Calculate new position based on mouse movement - const newX = e.clientX - dragState.offsetX; - const newY = e.clientY - dragState.offsetY; - - // Apply boundaries - const panelWidth = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelWidth"]; - const panelHeight = this.uiManager.elements.panel.offsetHeight; - const margin = 10; - - const boundedX = Math.max(margin, Math.min(newX, window.innerWidth - panelWidth - margin)); - const boundedY = Math.max(margin, Math.min(newY, window.innerHeight - panelHeight - margin)); - - // Apply position immediately and forcefully - this.uiManager.elements.panel.style.position = 'fixed'; - this.uiManager.elements.panel.style.left = `${boundedX}px`; - this.uiManager.elements.panel.style.top = `${boundedY}px`; - this.uiManager.elements.panel.style.transform = 'none'; - this.uiManager.elements.panel.style.zIndex = '10010'; - - // Update current position - dragState.currentX = boundedX; - dragState.currentY = boundedY; - - // Update floating controls - if (this.uiManager.elements.controls) { - this.positionManager.positionFloatingControls(); - } - }; - - // End drag - const endDrag = (e) => { - if (!dragState.isDragging) return; - - e.preventDefault(); - e.stopImmediatePropagation(); - - // Calculate if we actually moved significantly - const deltaX = Math.abs(e.clientX - dragState.startX); - const deltaY = Math.abs(e.clientY - dragState.startY); - const moved = deltaX > 5 || deltaY > 5; - - if (moved) { - // Save the final position - this.stateManager.setPinnedPosition(dragState.currentX, dragState.currentY); - } - - // Reset state - dragState.isDragging = false; - - // Remove visual feedback - this.uiManager.elements.panel.classList.remove('panel-dragging'); - this.uiManager.elements.panel.style.cursor = ''; - this.uiManager.elements.panel.style.zIndex = ''; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - }; - - // Cancel drag - const cancelDrag = () => { - if (!dragState.isDragging) return; - - // Restore original position from pinned state - const { x, y } = this.stateManager.state.pinnedPosition; - this.uiManager.elements.panel.style.left = `${x}px`; - this.uiManager.elements.panel.style.top = `${y}px`; - - // Reset state - dragState.isDragging = false; - - // Remove visual feedback - this.uiManager.elements.panel.classList.remove('panel-dragging'); - this.uiManager.elements.panel.style.cursor = ''; - this.uiManager.elements.panel.style.zIndex = ''; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - - // Update controls - if (this.uiManager.elements.controls) { - this.positionManager.positionFloatingControls(); - } - }; - - // Attach events with capture to prevent interference - this.uiManager.elements.panel.addEventListener('mousedown', startDrag, { capture: true, passive: false }); - document.addEventListener('mousemove', doDrag, { capture: true, passive: false }); - document.addEventListener('mouseup', endDrag, { capture: true, passive: false }); - - // Handle escape key - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - cancelDrag(); - } - }); - - // Handle mouse leaving window - document.addEventListener('mouseleave', endDrag); - - // Visual feedback - this.uiManager.elements.panel.addEventListener('mouseenter', () => { - if (this.stateManager.state.isPanelPinned && !this.stateManager.state.isPanelLocked && !dragState.isDragging) { - this.uiManager.elements.panel.style.cursor = 'grab'; - } - }); - - this.uiManager.elements.panel.addEventListener('mouseleave', () => { - if (!dragState.isDragging) { - this.uiManager.elements.panel.style.cursor = ''; - } - }); - - // Store cleanup function - this.dragCleanup = () => { - this.uiManager.elements.panel.removeEventListener('mousedown', startDrag, { capture: true }); - document.removeEventListener('mousemove', doDrag, { capture: true }); - document.removeEventListener('mouseup', endDrag, { capture: true }); - document.removeEventListener('mouseleave', endDrag); - cancelDrag(); - }; - } - - setupHoverEvents() { - this.uiManager.elements.panel.addEventListener('mouseenter', () => { - this.stateManager.state.isPanelHovered = true; - this.stateManager.clearAutoExpandTimer(); - }); - - this.uiManager.elements.panel.addEventListener('mouseleave', () => { - this.stateManager.state.isPanelHovered = false; - if (!this.stateManager.state.isHoveringNode) { - this.stateManager.scheduleAutoCollapse(); - } - }); - } - - captureCurrentPanelPosition() { - // Capture current position when pinning is activated - const panelRect = this.uiManager.elements.panel.getBoundingClientRect(); - this.stateManager.setPinnedPosition(panelRect.left, panelRect.top); - } - - handleControlAction(action) { - const magnifyGlass = window.comfyUIMagnifyGlass; - - switch (action) { - case 'minimize': - this.stateManager.toggleMinimized(); - this.uiManager.updateMinimizedState(); - break; - - case 'pin': - // If not currently pinned, either restore to last position or capture current - if (!this.stateManager.state.isPanelPinned) { - // If we don't have a remembered position, capture current position - if (!this.stateManager.state.lastPinnedPosition) { - this.captureCurrentPanelPosition(); - } - // Note: togglePinning will restore lastPinnedPosition if it exists - } - - this.stateManager.togglePinning(); - this.uiManager.updatePinnedState(); - this.positionManager.positionPanel(); - break; - - case 'lock': - this.stateManager.toggleLocking(); - this.uiManager.updatePinnedState(); // This updates both pinned and locked states - break; - - case 'pin-at-mouse': - if (!magnifyGlass) return; - - // Pin at current mouse location (Alt + configured key) - const settings = this.stateManager.state.settings; - const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; - const panelHeight = Math.min(settings["🔍MagnifyGlass.InfoPanelMaxHeight"], 400); - - // Position panel near mouse with some offset to avoid covering cursor area - const mouseX = magnifyGlass.lastKnownMousePosition.x; - const mouseY = magnifyGlass.lastKnownMousePosition.y; - - let pinX = mouseX + 50; // Offset to the right of mouse - let pinY = mouseY - 100; // Offset above mouse - - // Apply boundary constraints - pinX = Math.max(10, Math.min(pinX, window.innerWidth - panelWidth - 10)); - pinY = Math.max(10, Math.min(pinY, window.innerHeight - panelHeight - 10)); - - // Set the new pin position (this will also save as lastPinnedPosition) - this.stateManager.setPinnedPosition(pinX, pinY); - - // Enable pinning if not already pinned - if (!this.stateManager.state.isPanelPinned) { - this.stateManager.state.isPanelPinned = true; - } - - this.uiManager.updatePinnedState(); - this.positionManager.positionPanel(); - break; - - case 'toggle-panel': - if (this.stateManager.togglePanelVisibility()) { - this.uiManager.show(); - } else { - this.uiManager.hide(); - } - this.uiManager.updateControlStates(); - break; - - case 'toggle-glass': - const isVisible = this.stateManager.toggleGlassPreview(); - this.applyGlassVisibility(isVisible); - - // When the glass preview is hidden, automatically pin the panel at its current location. - if (!isVisible && !this.stateManager.state.isPanelPinned) { - this.captureCurrentPanelPosition(); // Capture current position before pinning - this.stateManager.togglePinning(); - this.stateManager.state.isAutoPinned = true; // Mark as auto-pinned - this.uiManager.updatePinnedState(); - } - - // When the glass preview is shown again, only auto-unlock if NOT manually locked with 📌 button - if (isVisible && this.stateManager.state.isPanelPinned && !this.stateManager.state.isPanelLocked) { - this.stateManager.togglePinning(); // Unpin the panel - this.stateManager.state.isAutoPinned = false; // Clear auto-pin flag - this.uiManager.updatePinnedState(); - } - - this.uiManager.updateControlStates(); - this.positionManager.positionPanel(); // Reposition based on new glass state - break; - } - } - - applyGlassVisibility(isVisible) { - const magnifyGlass = window.comfyUIMagnifyGlass; - if (magnifyGlass && magnifyGlass.ui && magnifyGlass.ui.glassDiv) { - magnifyGlass.ui.glassDiv.style.opacity = isVisible ? "1" : "0"; - } - } - - cleanup() { - if (this.dragCleanup) { - this.dragCleanup(); - } + if (key === settings["🔍MagnifyGlass.PinPanelHotkey"].toLowerCase()) { + e.preventDefault(); + (_b = this.callbacks) == null ? void 0 : _b.togglePin(); } + } + handleMouseMove(e) { + } + handleKeyUp(e) { + } + cleanup() { + document.removeEventListener("keydown", this.boundHandleKeyDown); + if (this.dragCleanup) this.dragCleanup(); + } } +export { + EventManager +}; +//# sourceMappingURL=EventManager.js.map diff --git a/web/info-panel/EventManager.js.map b/web/info-panel/EventManager.js.map new file mode 100644 index 0000000..048f671 --- /dev/null +++ b/web/info-panel/EventManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"EventManager.js","sources":["../../src/info-panel/EventManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel Event Manager (TypeScript)\n * \n * Complete Event Manager extracted from magnify_info_panel.js\n * Handles all event binding and delegation for the info panel.\n */\n\nimport { StateManager } from './StateManager';\nimport { UIManager } from './UIManager'; // Kept for type compatibility if needed, though we use HTMLElement mostly\nimport { PositionManager } from './PositionManager';\nimport { isUserTyping } from '../shared/utils';\n\nexport interface EventCallbacks {\n toggleVisibility: () => void;\n togglePin: () => void;\n updatePosition: () => void;\n}\n\n/**\n * Event Manager class.\n * Handles all event listeners for the info panel.\n */\nexport class EventManager {\n stateManager: StateManager;\n panelElement: HTMLElement | null;\n positionManager: PositionManager;\n callbacks: EventCallbacks | null;\n\n dragCleanup: (() => void) | null;\n\n boundHandleMouseMove: (e: MouseEvent) => void;\n boundHandleKeyDown: (e: KeyboardEvent) => void;\n boundHandleKeyUp: (e: KeyboardEvent) => void;\n\n constructor(stateManager: StateManager, panelElement: HTMLElement | null, positionManager: PositionManager, callbacks?: EventCallbacks) {\n this.stateManager = stateManager;\n this.panelElement = panelElement;\n this.positionManager = positionManager;\n this.callbacks = callbacks || null;\n this.dragCleanup = null;\n\n this.boundHandleMouseMove = this.handleMouseMove.bind(this);\n this.boundHandleKeyDown = this.handleKeyDown.bind(this);\n this.boundHandleKeyUp = this.handleKeyUp.bind(this);\n\n this.setupEventListeners();\n }\n\n setupEventListeners(): void {\n this.setupPanelEvents();\n this.setupHotkeyEvents();\n this.setupDragEvents();\n this.setupHoverEvents();\n }\n\n setupPanelEvents(): void {\n if (!this.panelElement) return;\n\n // Vue handles most internal events now.\n // We can keep specific global-ish listeners if needed here.\n }\n\n setupHotkeyEvents(): void {\n document.addEventListener('keydown', this.boundHandleKeyDown);\n }\n\n setupDragEvents(): void {\n if (!this.panelElement) return;\n\n // Simple drag implementation for the Vue panel\n // The dragging logic could be inside Vue, but if we want to update global state/position, we can do it here.\n // For now, let's look for the header element.\n\n // We need to wait for Vue to render the header?\n // Since we pass the container, and Vue mounts to it, the header might be inside.\n // We can use event delegation on the container.\n\n const startDrag = (e: MouseEvent) => {\n if (!this.panelElement) return;\n const target = e.target as HTMLElement;\n\n // Only allow dragging from header\n if (!target.closest('.mag-panel-header')) return;\n if (target.closest('button')) return;\n\n e.preventDefault();\n\n const startX = e.clientX;\n const startY = e.clientY;\n const rect = this.panelElement.getBoundingClientRect();\n const startLeft = rect.left;\n const startTop = rect.top;\n\n // Use requestAnimationFrame for smoother dragging?\n const onMouseMove = (moveEvent: MouseEvent) => {\n const dx = moveEvent.clientX - startX;\n const dy = moveEvent.clientY - startY;\n\n if (this.panelElement) {\n this.panelElement.style.left = `${startLeft + dx}px`;\n this.panelElement.style.top = `${startTop + dy}px`;\n }\n };\n\n const onMouseUp = () => {\n document.removeEventListener('mousemove', onMouseMove);\n document.removeEventListener('mouseup', onMouseUp);\n\n // Save pinned position\n if (this.panelElement) {\n const finalRect = this.panelElement.getBoundingClientRect();\n this.stateManager.state.pinnedPosition = { x: finalRect.left, y: finalRect.top };\n // Auto-pin on drag end?\n if (!this.stateManager.state.isPanelPinned) {\n // callbacks.togglePin? Or just set state?\n // Better to let user manually pin, or follow existing logic.\n }\n }\n };\n\n document.addEventListener('mousemove', onMouseMove);\n document.addEventListener('mouseup', onMouseUp);\n };\n\n this.panelElement.addEventListener('mousedown', startDrag);\n\n this.dragCleanup = () => {\n if (this.panelElement) this.panelElement.removeEventListener('mousedown', startDrag);\n };\n }\n\n setupHoverEvents(): void {\n if (!this.panelElement) return;\n\n this.panelElement.addEventListener('mouseenter', () => {\n this.stateManager.state.isPanelHovered = true;\n this.stateManager.clearAutoExpandTimer();\n });\n\n this.panelElement.addEventListener('mouseleave', () => {\n this.stateManager.state.isPanelHovered = false;\n if (!this.stateManager.state.isHoveringNode) {\n this.stateManager.scheduleAutoCollapse();\n }\n });\n }\n\n handleKeyDown(e: KeyboardEvent): void {\n if (isUserTyping()) return;\n\n const settings = this.stateManager.state.settings;\n const key = e.key.toLowerCase();\n // console.log(`[InfoPanel] KeyDown: ${key}`);\n\n // Toggle visibility\n if (key === (settings[\"🔍MagnifyGlass.ToggleHotkey\"] as string).toLowerCase()) {\n console.log(\"[InfoPanel] Toggle visibility hotkey matched!\");\n e.preventDefault();\n this.callbacks?.toggleVisibility();\n }\n\n // Toggle Pin\n if (key === (settings[\"🔍MagnifyGlass.PinPanelHotkey\"] as string).toLowerCase()) {\n e.preventDefault();\n this.callbacks?.togglePin();\n }\n }\n\n handleMouseMove(e: MouseEvent): void {\n // Placeholder\n }\n\n handleKeyUp(e: KeyboardEvent): void {\n // Placeholder\n }\n\n cleanup(): void {\n document.removeEventListener('keydown', this.boundHandleKeyDown);\n if (this.dragCleanup) this.dragCleanup();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/InfoPanel.js b/web/info-panel/InfoPanel.js index 857a9de..8064989 100644 --- a/web/info-panel/InfoPanel.js +++ b/web/info-panel/InfoPanel.js @@ -1,130 +1,110 @@ -/** - * ComfyUI MagnifyGlass - Info Panel Orchestrator - * - * Complete Info Panel Manager extracted from magnify_info_panel.js - * Main class that orchestrates all info panel components. - */ - -import { app } from "../../../../scripts/app.js"; -import { StateManager } from './StateManager.js'; -import { UIManager } from './UIManager.js'; -import { PositionManager } from './PositionManager.js'; -import { EventManager } from './EventManager.js'; -import { InformationGatherer } from './InformationGatherer.js'; - -/** - * Professional Info Panel Manager. - * Orchestrates all the other components for the info panel. - */ -export class InfoPanel { - constructor(magnifyGlass) { - this.magnifyGlass = magnifyGlass; - - // Initialize all managers - this.stateManager = new StateManager(); - this.uiManager = new UIManager(this.stateManager); - this.positionManager = new PositionManager(this.stateManager, this.uiManager); - this.eventManager = new EventManager(this.stateManager, this.uiManager, this.positionManager); - this.informationGatherer = new InformationGatherer(); - - this.hookIntoMagnifyGlass(); - - console.log("ComfyUI Magnify Info Panel Pro V2: Initialized successfully"); - } - - hookIntoMagnifyGlass() { - // Hook into magnify glass update cycle - const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass); - - this.magnifyGlass.updateMagnifiedView = (() => { - originalUpdateMagnifiedView(); - - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { - this.scheduleInfoUpdate(); - } - }).bind(this); - - // Hook into show/hide - const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui); - const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui); - - this.magnifyGlass.ui.show = (() => { - originalShow(); - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { - this.uiManager.show(); - // Position controls after showing - setTimeout(() => this.positionManager.positionPanel(), 10); - } - }).bind(this); - - this.magnifyGlass.ui.hide = (() => { - originalHide(); - this.uiManager.hide(); - }).bind(this); - } - - scheduleInfoUpdate() { - if (this.stateManager.state.updateScheduled) return; - - this.stateManager.state.updateScheduled = true; - requestAnimationFrame(() => { - this.updateInfo(); - this.stateManager.state.updateScheduled = false; - }); - } - - updateInfo() { - if (!this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] || !this.magnifyGlass.state.active) return; - - const info = this.informationGatherer.gatherInformation(); - this.stateManager.setCurrentInfo(info); - this.uiManager.displayInfo(info); - this.positionManager.positionPanel(); - - // Handle node hover state - if (info.hoveredNode) { - this.stateManager.state.isHoveringNode = true; - this.stateManager.expandNodeSections(); - - if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) { - this.stateManager.state.lastNodeId = info.hoveredNode.id; - } +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { registerPanelSettings } from "../shared/settings.js"; +import { StateManager } from "./StateManager.js"; +import { UIManager } from "./UIManager.js"; +import { PositionManager } from "./PositionManager.js"; +import { EventManager } from "./EventManager.js"; +import { InformationGatherer } from "./InformationGatherer.js"; +class InfoPanel { + constructor(magnifyGlass) { + __publicField(this, "magnifyGlass"); + __publicField(this, "stateManager"); + __publicField(this, "uiManager"); + __publicField(this, "positionManager"); + __publicField(this, "eventManager"); + __publicField(this, "informationGatherer"); + this.magnifyGlass = magnifyGlass; + this.stateManager = new StateManager(); + this.uiManager = new UIManager(this.stateManager); + this.informationGatherer = new InformationGatherer(); + this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel); + this.eventManager = new EventManager(this.stateManager, this.uiManager.elements.panel, this.positionManager, { + toggleVisibility: () => { + if (this.stateManager.state.isPanelVisible) { + this.uiManager.hide(); } else { - this.stateManager.state.isHoveringNode = false; - - if (this.stateManager.state.lastNodeId !== null) { - this.stateManager.state.lastNodeId = null; - if (!this.stateManager.state.isPanelHovered) { - this.stateManager.scheduleAutoCollapse(); - } - } + this.uiManager.show(); } - } - - updateSettings() { - const changes = this.stateManager.updateSettings(); - - // React to setting changes (theme is now auto-detected) - // Theme changes are handled automatically by the observer - - if (changes["🔍MagnifyGlass.ControlsPosition"]) { - // Update controls layout and position when position setting changes - this.positionManager.positionPanel(); - } - - // Apply new styles - this.uiManager.applyStyles(); - - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { - this.uiManager.show(); - } else { - this.uiManager.hide(); + }, + togglePin: () => { + this.stateManager.togglePinning(); + this.uiManager.updatePinnedState(); + }, + updatePosition: () => this.positionManager.positionPanel() + }); + this.hookIntoMagnifyGlass(); + registerPanelSettings(this.stateManager, this.uiManager, this.positionManager); + console.log("ComfyUI Magnify Info Panel Pro V2: Initialized successfully"); + } + hookIntoMagnifyGlass() { + const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass); + this.magnifyGlass.updateMagnifiedView = (() => { + originalUpdateMagnifiedView(); + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.scheduleInfoUpdate(); + } + }).bind(this); + const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui); + const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui); + this.magnifyGlass.ui.show = (() => { + originalShow(); + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + this.uiManager.show(); + setTimeout(() => { + this.positionManager.positionPanel(); + this.positionManager.positionFloatingControls(this.uiManager.elements.controls); + }, 10); + } + }).bind(this); + this.magnifyGlass.ui.hide = (() => { + originalHide(); + this.uiManager.hide(); + }).bind(this); + } + scheduleInfoUpdate() { + if (this.stateManager.state.updateScheduled) return; + this.stateManager.state.updateScheduled = true; + requestAnimationFrame(() => { + this.updateInfo(); + this.stateManager.state.updateScheduled = false; + }); + } + updateInfo() { + if (!this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] || !this.magnifyGlass.state.active) return; + const info = this.informationGatherer.gatherInformation(); + this.stateManager.setCurrentInfo(info); + this.uiManager.displayInfo(info); + this.positionManager.positionPanel(); + this.positionManager.positionFloatingControls(this.uiManager.elements.controls); + if (info.hoveredNode) { + this.stateManager.state.isHoveringNode = true; + this.stateManager.expandNodeSections(); + if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) { + this.stateManager.state.lastNodeId = info.hoveredNode.id; + } + } else { + this.stateManager.state.isHoveringNode = false; + if (this.stateManager.state.lastNodeId !== null) { + this.stateManager.state.lastNodeId = null; + if (!this.stateManager.state.isPanelHovered) { + this.stateManager.scheduleAutoCollapse(); } + } } - - cleanup() { - this.stateManager.cleanup(); - this.uiManager.cleanup(); - this.eventManager.cleanup(); + } + updateSettings() { + this.stateManager.updateSettings(); + this.uiManager.applyStyles(); + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { + this.uiManager.show(); + } else { + this.uiManager.hide(); } + } } +export { + InfoPanel +}; +//# sourceMappingURL=InfoPanel.js.map diff --git a/web/info-panel/InfoPanel.js.map b/web/info-panel/InfoPanel.js.map new file mode 100644 index 0000000..e8a3cc1 --- /dev/null +++ b/web/info-panel/InfoPanel.js.map @@ -0,0 +1 @@ +{"version":3,"file":"InfoPanel.js","sources":["../../src/info-panel/InfoPanel.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel Orchestrator (TypeScript)\n * \n * Complete Info Panel Manager extracted from magnify_info_panel.js\n * Main class that orchestrates all info panel components.\n */\n\nimport { registerPanelSettings } from '../shared/settings';\nimport { StateManager } from './StateManager';\nimport { UIManager } from './UIManager';\nimport { PositionManager } from './PositionManager';\nimport { EventManager } from './EventManager';\nimport { InformationGatherer } from './InformationGatherer';\n\n/**\n * Professional Info Panel Manager.\n * Orchestrates all the other components for the info panel.\n */\nexport class InfoPanel {\n magnifyGlass: any;\n stateManager: StateManager;\n uiManager: UIManager;\n positionManager: PositionManager;\n eventManager: EventManager;\n informationGatherer: InformationGatherer;\n\n constructor(magnifyGlass: any) {\n this.magnifyGlass = magnifyGlass;\n\n // Initialize Managers\n this.stateManager = new StateManager();\n this.uiManager = new UIManager(this.stateManager);\n this.informationGatherer = new InformationGatherer();\n\n // Position Manager needs the UI elements\n this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel!);\n\n // Event Manager needs state and UI manager\n this.eventManager = new EventManager(this.stateManager, this.uiManager.elements.panel!, this.positionManager, {\n toggleVisibility: () => {\n if (this.stateManager.state.isPanelVisible) {\n this.uiManager.hide();\n } else {\n this.uiManager.show();\n }\n },\n togglePin: () => {\n this.stateManager.togglePinning();\n this.uiManager.updatePinnedState();\n },\n updatePosition: () => this.positionManager.positionPanel()\n });\n\n this.hookIntoMagnifyGlass();\n\n // Register settings with ComfyUI\n registerPanelSettings(this.stateManager, this.uiManager, this.positionManager);\n\n console.log(\"ComfyUI Magnify Info Panel Pro V2: Initialized successfully\");\n }\n\n hookIntoMagnifyGlass(): void {\n // Hook into magnify glass update cycle\n const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass);\n\n this.magnifyGlass.updateMagnifiedView = (() => {\n originalUpdateMagnifiedView();\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] && this.magnifyGlass.state.active) {\n this.scheduleInfoUpdate();\n }\n }).bind(this);\n\n // Hook into show/hide\n const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui);\n const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui);\n\n this.magnifyGlass.ui.show = (() => {\n originalShow();\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"]) {\n this.uiManager.show();\n // Position controls after showing\n setTimeout(() => {\n this.positionManager.positionPanel();\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n }, 10);\n }\n }).bind(this);\n\n this.magnifyGlass.ui.hide = (() => {\n originalHide();\n this.uiManager.hide();\n }).bind(this);\n }\n\n scheduleInfoUpdate(): void {\n if (this.stateManager.state.updateScheduled) return;\n\n this.stateManager.state.updateScheduled = true;\n requestAnimationFrame(() => {\n this.updateInfo();\n this.stateManager.state.updateScheduled = false;\n });\n }\n\n updateInfo(): void {\n if (!this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] || !this.magnifyGlass.state.active) return;\n\n const info = this.informationGatherer.gatherInformation();\n this.stateManager.setCurrentInfo(info);\n\n // Update the UI\n this.uiManager.displayInfo(info);\n\n this.positionManager.positionPanel();\n this.positionManager.positionFloatingControls(this.uiManager.elements.controls);\n\n // Handle node hover state logic if needed for internal state\n if (info.hoveredNode) {\n this.stateManager.state.isHoveringNode = true;\n this.stateManager.expandNodeSections();\n if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) {\n this.stateManager.state.lastNodeId = info.hoveredNode.id;\n }\n } else {\n this.stateManager.state.isHoveringNode = false;\n if (this.stateManager.state.lastNodeId !== null) {\n this.stateManager.state.lastNodeId = null;\n if (!this.stateManager.state.isPanelHovered) {\n this.stateManager.scheduleAutoCollapse();\n }\n }\n }\n }\n\n updateSettings(): void {\n const changes = this.stateManager.updateSettings();\n this.uiManager.applyStyles();\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] && this.magnifyGlass.state.active) {\n this.uiManager.show();\n } else {\n this.uiManager.hide();\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/InformationGatherer.js b/web/info-panel/InformationGatherer.js index c5267c7..f95e5d6 100644 --- a/web/info-panel/InformationGatherer.js +++ b/web/info-panel/InformationGatherer.js @@ -1,320 +1,270 @@ -/** - * ComfyUI MagnifyGlass - Info Panel Information Gatherer - * - * Complete Information Gatherer extracted from magnify_info_panel.js - * Responsible for collecting information about nodes, widgets, and canvas state. - */ - -import { app } from "../../../../scripts/app.js"; - -/** - * Information Gatherer class. - * Collects information about the current state under the cursor. - */ -export class InformationGatherer { - constructor() { } - - gatherInformation() { - const magnifyGlass = window.comfyUIMagnifyGlass; - if (!magnifyGlass) return {}; - - const info = { - timestamp: Date.now(), - cursor: { - canvas: { x: magnifyGlass.state.x, y: magnifyGlass.state.y }, - screen: { - x: magnifyGlass.lastKnownMousePosition.x, - y: magnifyGlass.lastKnownMousePosition.y - } - }, - canvas: { - scale: magnifyGlass.state.canvasScale, - offset: { - x: magnifyGlass.state.canvasOffsetX, - y: magnifyGlass.state.canvasOffsetY - } - }, - magnifier: { - zoomFactor: magnifyGlass.config.zoomFactor, - offsetX: magnifyGlass.config.offsetX, - offsetY: magnifyGlass.config.offsetY, - sourceRegion: { - x: magnifyGlass.state.sourceX, - y: magnifyGlass.state.sourceY, - width: magnifyGlass.state.sourceWidth, - height: magnifyGlass.state.sourceHeight - } - }, - hoveredNode: null, - node: null, - widget: null, - connection: null, - media: null - }; - - if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) { - info.media = this.getMediaInfo(magnifyGlass.currentMediaElement); +class InformationGatherer { + constructor() { + } + gatherInformation() { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass) return {}; + const info = { + timestamp: Date.now(), + cursor: { + canvas: { x: magnifyGlass.state.x, y: magnifyGlass.state.y }, + screen: { + x: magnifyGlass.lastKnownMousePosition.x, + y: magnifyGlass.lastKnownMousePosition.y } - - const nodeUnderCursor = this.getNodeUnderCursor(); - if (nodeUnderCursor) { - info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos); - info.node = this.getNodeInfo(nodeUnderCursor.node); - - const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos); - if (widget) { - info.widget = this.getWidgetInfo(widget); + }, + canvas: { + scale: magnifyGlass.state.canvasScale, + offset: { + x: magnifyGlass.state.canvasOffsetX, + y: magnifyGlass.state.canvasOffsetY + } + }, + magnifier: { + zoomFactor: magnifyGlass.config.zoomFactor, + offsetX: magnifyGlass.config.offsetX, + offsetY: magnifyGlass.config.offsetY, + sourceRegion: { + x: magnifyGlass.state.sourceX, + y: magnifyGlass.state.sourceY, + width: magnifyGlass.state.sourceWidth, + height: magnifyGlass.state.sourceHeight + } + }, + hoveredNode: null, + node: null, + widget: null, + connection: null, + media: null + }; + if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) { + info.media = this.getMediaInfo(magnifyGlass.currentMediaElement); + } + const nodeUnderCursor = this.getNodeUnderCursor(); + if (nodeUnderCursor) { + info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos); + info.node = this.getNodeInfo(nodeUnderCursor.node); + const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos); + if (widget) { + info.widget = this.getWidgetInfo(widget); + } + } + return info; + } + getNodeUnderCursor() { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!app.graph || !app.canvas || !magnifyGlass) { + return null; + } + try { + if (app.canvas.node_over) { + const node = app.canvas.node_over; + if (node && node.pos && node.size) { + const canvasRect2 = app.canvas.canvas.getBoundingClientRect(); + const mouseX2 = magnifyGlass.lastKnownMousePosition.x - canvasRect2.left; + const mouseY2 = magnifyGlass.lastKnownMousePosition.y - canvasRect2.top; + let graphPos2; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos2 = app.canvas.convertOffsetToCanvasPos([mouseX2, mouseY2]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos2 = [ + mouseX2 / ds.scale - ds.offset[0] / ds.scale, + mouseY2 / ds.scale - ds.offset[1] / ds.scale + ]; + } + return { + node, + localPos: { + x: graphPos2[0] - node.pos[0], + y: graphPos2[1] - node.pos[1] } + }; } - - return info; - } - - getNodeUnderCursor() { - const magnifyGlass = window.comfyUIMagnifyGlass; - if (!app.graph || !app.canvas || !magnifyGlass) { - return null; - } - - try { - // Try to get the node from ComfyUI's state - if (app.canvas.node_over) { - const node = app.canvas.node_over; - if (node && node.pos && node.size) { - const canvasRect = app.canvas.canvas.getBoundingClientRect(); - const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; - const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; - - let graphPos; - if (app.canvas.convertOffsetToCanvasPos) { - graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); - } else { - const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; - graphPos = [ - (mouseX / ds.scale) - (ds.offset[0] / ds.scale), - (mouseY / ds.scale) - (ds.offset[1] / ds.scale) - ]; - } - - return { - node: node, - localPos: { - x: graphPos[0] - node.pos[0], - y: graphPos[1] - node.pos[1] - } - }; - } + } + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; + const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; + let graphPos; + if (app.canvas.convertOffsetToCanvasPos) { + graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); + } else { + const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; + graphPos = [ + mouseX / ds.scale - ds.offset[0] / ds.scale, + mouseY / ds.scale - ds.offset[1] / ds.scale + ]; + } + for (let i = app.graph._nodes.length - 1; i >= 0; i--) { + const node = app.graph._nodes[i]; + if (!node.pos || !node.size || node.flags && node.flags.collapsed) continue; + if (graphPos[0] >= node.pos[0] && graphPos[0] <= node.pos[0] + node.size[0] && graphPos[1] >= node.pos[1] && graphPos[1] <= node.pos[1] + node.size[1]) { + return { + node, + localPos: { + x: graphPos[0] - node.pos[0], + y: graphPos[1] - node.pos[1] } - - // Fallback to manual detection - const canvasRect = app.canvas.canvas.getBoundingClientRect(); - const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; - const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; - - let graphPos; - if (app.canvas.convertOffsetToCanvasPos) { - graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); - } else { - const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; - graphPos = [ - (mouseX / ds.scale) - (ds.offset[0] / ds.scale), - (mouseY / ds.scale) - (ds.offset[1] / ds.scale) - ]; - } - - for (let i = app.graph._nodes.length - 1; i >= 0; i--) { - const node = app.graph._nodes[i]; - if (!node.pos || !node.size || node.flags?.collapsed) continue; - - if (graphPos[0] >= node.pos[0] && - graphPos[0] <= node.pos[0] + node.size[0] && - graphPos[1] >= node.pos[1] && - graphPos[1] <= node.pos[1] + node.size[1]) { - - return { - node: node, - localPos: { - x: graphPos[0] - node.pos[0], - y: graphPos[1] - node.pos[1] - } - }; - } - } - } catch (err) { - console.warn("Error in node detection:", err); + }; } - - return null; + } + } catch (err) { + console.warn("Error in node detection:", err); } - - getDetailedNodeInfo(node, localPos) { - return { - id: node.id, - title: node.title || "Untitled Node", - type: node.type, - mode: this.getNodeModeText(node.mode), - position: { - x: Math.round(node.pos[0]), - y: Math.round(node.pos[1]), - formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` - }, - size: { - width: Math.round(node.size[0]), - height: Math.round(node.size[1]), - formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}` - }, - localPosition: { - x: Math.round(localPos.x), - y: Math.round(localPos.y), - formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`, - percentage: { - x: ((localPos.x / node.size[0]) * 100).toFixed(1), - y: ((localPos.y / node.size[1]) * 100).toFixed(1) - } - }, - counts: { - widgets: node.widgets ? node.widgets.length : 0, - inputs: node.inputs ? node.inputs.length : 0, - outputs: node.outputs ? node.outputs.length : 0, - properties: node.properties ? Object.keys(node.properties).length : 0 - }, - widgets: node.widgets || [], - inputs: node.inputs || [], - outputs: node.outputs || [], - properties: node.properties || {}, - hoverRegion: this.detectNodeRegion(localPos, node) - }; + return null; + } + getDetailedNodeInfo(node, localPos) { + return { + id: node.id, + title: node.title || "Untitled Node", + type: node.type, + mode: this.getNodeModeText(node.mode), + position: { + x: Math.round(node.pos[0]), + y: Math.round(node.pos[1]), + formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` + }, + size: { + width: Math.round(node.size[0]), + height: Math.round(node.size[1]), + formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}` + }, + localPosition: { + x: Math.round(localPos.x), + y: Math.round(localPos.y), + formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`, + percentage: { + x: (localPos.x / node.size[0] * 100).toFixed(1), + y: (localPos.y / node.size[1] * 100).toFixed(1) + } + }, + counts: { + widgets: node.widgets ? node.widgets.length : 0, + inputs: node.inputs ? node.inputs.length : 0, + outputs: node.outputs ? node.outputs.length : 0, + properties: node.properties ? Object.keys(node.properties).length : 0 + }, + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {}, + hoverRegion: this.detectNodeRegion(localPos, node) + }; + } + getNodeInfo(node) { + return { + id: node.id, + title: node.title || "Untitled", + type: node.type, + mode: node.mode, + size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : "Unknown", + position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : "Unknown", + widgets: node.widgets || [], + inputs: node.inputs || [], + outputs: node.outputs || [], + properties: node.properties || {} + }; + } + getWidgetUnderCursor(node, localPos) { + if (!node.widgets || !node.widgets.length) return null; + const titleHeight = 30; + let currentY = titleHeight; + for (const widget of node.widgets) { + const widgetHeight = 25; + if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) { + return widget; + } + currentY += widgetHeight + 5; } - - getNodeInfo(node) { - return { - id: node.id, - title: node.title || "Untitled", - type: node.type, - mode: node.mode, - size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : "Unknown", - position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : "Unknown", - widgets: node.widgets || [], - inputs: node.inputs || [], - outputs: node.outputs || [], - properties: node.properties || {} - }; + return null; + } + getWidgetInfo(widget) { + return { + name: widget.name, + type: widget.type, + value: this.formatValue(widget.value), + options: widget.options || null, + min: widget.min, + max: widget.max, + step: widget.step + }; + } + getMediaInfo(mediaElement) { + const info = { + tagName: mediaElement.tagName, + src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf("/") + 1) : "No source" + }; + if (mediaElement instanceof HTMLImageElement) { + info.naturalSize = `${mediaElement.naturalWidth}×${mediaElement.naturalHeight}`; + info.displaySize = `${Math.round(mediaElement.width)}×${Math.round(mediaElement.height)}`; + info.complete = mediaElement.complete; + } else if (mediaElement instanceof HTMLVideoElement) { + info.videoSize = `${mediaElement.videoWidth}×${mediaElement.videoHeight}`; + info.duration = mediaElement.duration ? `${mediaElement.duration.toFixed(2)}s` : "Unknown"; + info.currentTime = `${mediaElement.currentTime.toFixed(2)}s`; + info.paused = mediaElement.paused; + info.readyState = mediaElement.readyState; } - - getWidgetUnderCursor(node, localPos) { - if (!node.widgets || !node.widgets.length) return null; - - const titleHeight = 30; - let currentY = titleHeight; - - for (const widget of node.widgets) { - const widgetHeight = 25; - - if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) { - return widget; - } - - currentY += widgetHeight + 5; - } - - return null; + return info; + } + getNodeModeText(mode) { + const modes = { + 0: "Always", + 1: "On Event", + 2: "Never", + 3: "On Trigger", + 4: "On Request" + }; + return modes[mode] || `Mode ${mode}`; + } + detectNodeRegion(localPos, node) { + const titleHeight = 30; + const regions = []; + if (localPos.y <= titleHeight) { + regions.push("Title Bar"); } - - getWidgetInfo(widget) { - return { - name: widget.name, - type: widget.type, - value: this.formatValue(widget.value), - options: widget.options || null, - min: widget.min, - max: widget.max, - step: widget.step - }; + if (localPos.x <= 10) { + regions.push("Left Edge"); + } else if (localPos.x >= node.size[0] - 10) { + regions.push("Right Edge"); } - - getMediaInfo(mediaElement) { - const info = { - tagName: mediaElement.tagName, - src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf('/') + 1) : "No source" - }; - - if (mediaElement.tagName === 'IMG') { - info.naturalSize = `${mediaElement.naturalWidth}×${mediaElement.naturalHeight}`; - info.displaySize = `${Math.round(mediaElement.width)}×${Math.round(mediaElement.height)}`; - info.complete = mediaElement.complete; - } else if (mediaElement.tagName === 'VIDEO') { - info.videoSize = `${mediaElement.videoWidth}×${mediaElement.videoHeight}`; - info.duration = mediaElement.duration ? `${mediaElement.duration.toFixed(2)}s` : "Unknown"; - info.currentTime = `${mediaElement.currentTime.toFixed(2)}s`; - info.paused = mediaElement.paused; - info.readyState = mediaElement.readyState; - } - - return info; + if (localPos.y <= 10) { + regions.push("Top Edge"); + } else if (localPos.y >= node.size[1] - 10) { + regions.push("Bottom Edge"); } - - getNodeModeText(mode) { - const modes = { - 0: "Always", - 1: "On Event", - 2: "Never", - 3: "On Trigger", - 4: "On Request" - }; - return modes[mode] || `Mode ${mode}`; + if (regions.length === 0) { + if (localPos.y > titleHeight) { + regions.push("Content Area"); + } } - - detectNodeRegion(localPos, node) { - const titleHeight = 30; - const regions = []; - - if (localPos.y <= titleHeight) { - regions.push("Title Bar"); - } - - if (localPos.x <= 10) { - regions.push("Left Edge"); - } else if (localPos.x >= node.size[0] - 10) { - regions.push("Right Edge"); - } - - if (localPos.y <= 10) { - regions.push("Top Edge"); - } else if (localPos.y >= node.size[1] - 10) { - regions.push("Bottom Edge"); - } - - if (regions.length === 0) { - if (localPos.y > titleHeight) { - regions.push("Content Area"); - } - } - - if (localPos.x <= 20 && localPos.y > titleHeight) { - regions.push("Input Area"); - } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) { - regions.push("Output Area"); - } - - return regions.length > 0 ? regions.join(", ") : "Unknown"; + if (localPos.x <= 20 && localPos.y > titleHeight) { + regions.push("Input Area"); + } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) { + regions.push("Output Area"); } - - formatValue(value) { - if (value === null) return "null"; - if (value === undefined) return "undefined"; - if (typeof value === "string") { - return value; // Show full text without truncation - } - if (typeof value === "number") { - return Number.isInteger(value) ? value.toString() : value.toFixed(3); - } - if (typeof value === "boolean") return value.toString(); - if (Array.isArray(value)) { - return `Array(${value.length})`; - } - if (typeof value === "object") { - return "Object"; - } - return String(value); + return regions.length > 0 ? regions.join(", ") : "Unknown"; + } + formatValue(value) { + if (value === null) return "null"; + if (value === void 0) return "undefined"; + if (typeof value === "string") { + return value; } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); + } } +export { + InformationGatherer +}; +//# sourceMappingURL=InformationGatherer.js.map diff --git a/web/info-panel/InformationGatherer.js.map b/web/info-panel/InformationGatherer.js.map new file mode 100644 index 0000000..8c118ae --- /dev/null +++ b/web/info-panel/InformationGatherer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"InformationGatherer.js","sources":["../../src/info-panel/InformationGatherer.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel Information Gatherer (TypeScript)\n * \n * Complete Information Gatherer extracted from magnify_info_panel.js\n * Responsible for collecting information about nodes, widgets, and canvas state.\n */\n\n// We need to declare the app global variable or import it if the type definition is available\n// Assuming types/comfyui.d.ts handles declared const app: ComfyApp;\ndeclare const app: any;\n\n/**\n * Information Gatherer class.\n * Collects information about the current state under the cursor.\n */\nexport class InformationGatherer {\n constructor() { }\n\n gatherInformation(): any {\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (!magnifyGlass) return {};\n\n const info: any = {\n timestamp: Date.now(),\n cursor: {\n canvas: { x: magnifyGlass.state.x, y: magnifyGlass.state.y },\n screen: {\n x: magnifyGlass.lastKnownMousePosition.x,\n y: magnifyGlass.lastKnownMousePosition.y\n }\n },\n canvas: {\n scale: magnifyGlass.state.canvasScale,\n offset: {\n x: magnifyGlass.state.canvasOffsetX,\n y: magnifyGlass.state.canvasOffsetY\n }\n },\n magnifier: {\n zoomFactor: magnifyGlass.config.zoomFactor,\n offsetX: magnifyGlass.config.offsetX,\n offsetY: magnifyGlass.config.offsetY,\n sourceRegion: {\n x: magnifyGlass.state.sourceX,\n y: magnifyGlass.state.sourceY,\n width: magnifyGlass.state.sourceWidth,\n height: magnifyGlass.state.sourceHeight\n }\n },\n hoveredNode: null,\n node: null,\n widget: null,\n connection: null,\n media: null\n };\n\n if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) {\n info.media = this.getMediaInfo(magnifyGlass.currentMediaElement);\n }\n\n const nodeUnderCursor = this.getNodeUnderCursor();\n if (nodeUnderCursor) {\n info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos);\n info.node = this.getNodeInfo(nodeUnderCursor.node);\n\n const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos);\n if (widget) {\n info.widget = this.getWidgetInfo(widget);\n }\n }\n\n return info;\n }\n\n getNodeUnderCursor(): { node: any, localPos: { x: number, y: number } } | null {\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (!app.graph || !app.canvas || !magnifyGlass) {\n return null;\n }\n\n try {\n // Try to get the node from ComfyUI's state\n if (app.canvas.node_over) {\n const node = app.canvas.node_over;\n if (node && node.pos && node.size) {\n const canvasRect = app.canvas.canvas.getBoundingClientRect();\n const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left;\n const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top;\n\n let graphPos: [number, number];\n if (app.canvas.convertOffsetToCanvasPos) {\n graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]);\n } else {\n const ds = app.canvas.ds || { scale: 1, offset: [0, 0] };\n graphPos = [\n (mouseX / ds.scale) - (ds.offset[0] / ds.scale),\n (mouseY / ds.scale) - (ds.offset[1] / ds.scale)\n ];\n }\n\n return {\n node: node,\n localPos: {\n x: graphPos[0] - node.pos[0],\n y: graphPos[1] - node.pos[1]\n }\n };\n }\n }\n\n // Fallback to manual detection\n const canvasRect = app.canvas.canvas.getBoundingClientRect();\n const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left;\n const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top;\n\n let graphPos: [number, number];\n if (app.canvas.convertOffsetToCanvasPos) {\n graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]);\n } else {\n const ds = app.canvas.ds || { scale: 1, offset: [0, 0] };\n graphPos = [\n (mouseX / ds.scale) - (ds.offset[0] / ds.scale),\n (mouseY / ds.scale) - (ds.offset[1] / ds.scale)\n ];\n }\n\n for (let i = app.graph._nodes.length - 1; i >= 0; i--) {\n const node = app.graph._nodes[i];\n if (!node.pos || !node.size || (node.flags && node.flags.collapsed)) continue;\n\n if (graphPos[0] >= node.pos[0] &&\n graphPos[0] <= node.pos[0] + node.size[0] &&\n graphPos[1] >= node.pos[1] &&\n graphPos[1] <= node.pos[1] + node.size[1]) {\n\n return {\n node: node,\n localPos: {\n x: graphPos[0] - node.pos[0],\n y: graphPos[1] - node.pos[1]\n }\n };\n }\n }\n } catch (err) {\n console.warn(\"Error in node detection:\", err);\n }\n\n return null;\n }\n\n getDetailedNodeInfo(node: any, localPos: { x: number, y: number }): any {\n return {\n id: node.id,\n title: node.title || \"Untitled Node\",\n type: node.type,\n mode: this.getNodeModeText(node.mode),\n position: {\n x: Math.round(node.pos[0]),\n y: Math.round(node.pos[1]),\n formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})`\n },\n size: {\n width: Math.round(node.size[0]),\n height: Math.round(node.size[1]),\n formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}`\n },\n localPosition: {\n x: Math.round(localPos.x),\n y: Math.round(localPos.y),\n formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`,\n percentage: {\n x: ((localPos.x / node.size[0]) * 100).toFixed(1),\n y: ((localPos.y / node.size[1]) * 100).toFixed(1)\n }\n },\n counts: {\n widgets: node.widgets ? node.widgets.length : 0,\n inputs: node.inputs ? node.inputs.length : 0,\n outputs: node.outputs ? node.outputs.length : 0,\n properties: node.properties ? Object.keys(node.properties).length : 0\n },\n widgets: node.widgets || [],\n inputs: node.inputs || [],\n outputs: node.outputs || [],\n properties: node.properties || {},\n hoverRegion: this.detectNodeRegion(localPos, node)\n };\n }\n\n getNodeInfo(node: any): any {\n return {\n id: node.id,\n title: node.title || \"Untitled\",\n type: node.type,\n mode: node.mode,\n size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : \"Unknown\",\n position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : \"Unknown\",\n widgets: node.widgets || [],\n inputs: node.inputs || [],\n outputs: node.outputs || [],\n properties: node.properties || {}\n };\n }\n\n getWidgetUnderCursor(node: any, localPos: { x: number, y: number }): any {\n if (!node.widgets || !node.widgets.length) return null;\n\n const titleHeight = 30;\n let currentY = titleHeight;\n\n for (const widget of node.widgets) {\n const widgetHeight = 25;\n\n if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) {\n return widget;\n }\n\n currentY += widgetHeight + 5;\n }\n\n return null;\n }\n\n getWidgetInfo(widget: any): any {\n return {\n name: widget.name,\n type: widget.type,\n value: this.formatValue(widget.value),\n options: widget.options || null,\n min: widget.min,\n max: widget.max,\n step: widget.step\n };\n }\n\n getMediaInfo(mediaElement: HTMLImageElement | HTMLVideoElement): any {\n const info: any = {\n tagName: mediaElement.tagName,\n src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf('/') + 1) : \"No source\"\n };\n\n if (mediaElement instanceof HTMLImageElement) {\n info.naturalSize = `${mediaElement.naturalWidth}×${mediaElement.naturalHeight}`;\n info.displaySize = `${Math.round(mediaElement.width)}×${Math.round(mediaElement.height)}`;\n info.complete = mediaElement.complete;\n } else if (mediaElement instanceof HTMLVideoElement) {\n info.videoSize = `${mediaElement.videoWidth}×${mediaElement.videoHeight}`;\n info.duration = mediaElement.duration ? `${mediaElement.duration.toFixed(2)}s` : \"Unknown\";\n info.currentTime = `${mediaElement.currentTime.toFixed(2)}s`;\n info.paused = mediaElement.paused;\n info.readyState = mediaElement.readyState;\n }\n\n return info;\n }\n\n getNodeModeText(mode: number): string {\n const modes: { [key: number]: string } = {\n 0: \"Always\",\n 1: \"On Event\",\n 2: \"Never\",\n 3: \"On Trigger\",\n 4: \"On Request\"\n };\n return modes[mode] || `Mode ${mode}`;\n }\n\n detectNodeRegion(localPos: { x: number, y: number }, node: any): string {\n const titleHeight = 30;\n const regions: string[] = [];\n\n if (localPos.y <= titleHeight) {\n regions.push(\"Title Bar\");\n }\n\n if (localPos.x <= 10) {\n regions.push(\"Left Edge\");\n } else if (localPos.x >= node.size[0] - 10) {\n regions.push(\"Right Edge\");\n }\n\n if (localPos.y <= 10) {\n regions.push(\"Top Edge\");\n } else if (localPos.y >= node.size[1] - 10) {\n regions.push(\"Bottom Edge\");\n }\n\n if (regions.length === 0) {\n if (localPos.y > titleHeight) {\n regions.push(\"Content Area\");\n }\n }\n\n if (localPos.x <= 20 && localPos.y > titleHeight) {\n regions.push(\"Input Area\");\n } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) {\n regions.push(\"Output Area\");\n }\n\n return regions.length > 0 ? regions.join(\", \") : \"Unknown\";\n }\n\n formatValue(value: any): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"string\") {\n return value; // Show full text without truncation\n }\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? value.toString() : value.toFixed(3);\n }\n if (typeof value === \"boolean\") return value.toString();\n if (Array.isArray(value)) {\n return `Array(${value.length})`;\n }\n if (typeof value === \"object\") {\n return \"Object\";\n }\n return String(value);\n }\n}\n"],"names":["canvasRect","mouseX","mouseY","graphPos"],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/PositionManager.js b/web/info-panel/PositionManager.js index 66e36e1..9816245 100644 --- a/web/info-panel/PositionManager.js +++ b/web/info-panel/PositionManager.js @@ -1,192 +1,123 @@ -/** - * ComfyUI MagnifyGlass - Info Panel Position Manager - * - * Complete Position Manager extracted from magnify_info_panel.js - * Handles all positioning logic including pinning and boundary checking. - */ - -/** - * Position Manager class. - * Handles all positioning logic for the info panel. - */ -export class PositionManager { - /** - * @param {import('./StateManager.js').StateManager} stateManager - * @param {import('./UIManager.js').UIManager} uiManager - */ - constructor(stateManager, uiManager) { - this.stateManager = stateManager; - this.uiManager = uiManager; +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +class PositionManager { + constructor(stateManager, panelElement) { + __publicField(this, "stateManager"); + __publicField(this, "panelElement"); + this.stateManager = stateManager; + this.panelElement = panelElement; + } + positionPanel() { + if (!this.panelElement) return; + if (this.stateManager.state.isPanelPinned) { + this.applyPinnedPosition(); + return; } - - positionPanel() { - if (!this.uiManager.elements.panel) return; - - // If panel is pinned, keep it at its pinned position - if (this.stateManager.state.isPanelPinned) { - this.applyPinnedPosition(); - this.positionFloatingControls(); - return; + this.calculateNormalPosition(); + } + applyPinnedPosition() { + if (!this.panelElement) return; + let { x, y } = this.stateManager.state.pinnedPosition; + const panelWidth = this.panelElement.offsetWidth; + const panelHeight = this.panelElement.offsetHeight; + const margin = 10; + const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin)); + const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin)); + this.panelElement.style.left = `${boundedX}px`; + this.panelElement.style.top = `${boundedY}px`; + } + calculateNormalPosition() { + var _a; + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass || !this.panelElement) return; + const settings = this.stateManager.state.settings; + const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; + const panelHeight = this.panelElement.offsetHeight; + let left; + let top; + const margin = 15; + if (!this.stateManager.state.isGlassPreviewVisible) { + left = magnifyGlass.lastKnownMousePosition.x - panelWidth / 2; + top = magnifyGlass.lastKnownMousePosition.y - 20; + } else { + const glassRect = (_a = magnifyGlass.ui.glassDiv) == null ? void 0 : _a.getBoundingClientRect(); + if (glassRect) { + const position = settings["🔍MagnifyGlass.InfoPanelPosition"]; + switch (position) { + case "Right": + left = glassRect.right + margin; + top = glassRect.top; + break; + case "Left": + left = glassRect.left - panelWidth - margin; + top = glassRect.top; + break; + case "Top": + left = glassRect.left; + top = glassRect.top - panelHeight - margin; + break; + case "Bottom": + left = glassRect.left; + top = glassRect.bottom + margin; + break; + default: + left = glassRect.right + margin; + top = glassRect.top; + break; } - - // Normal positioning logic - this.calculateNormalPosition(); - this.positionFloatingControls(); + } else { + left = magnifyGlass.lastKnownMousePosition.x - panelWidth / 2; + top = magnifyGlass.lastKnownMousePosition.y - 20; + } } - - positionFloatingControls() { - if (!this.uiManager.elements.controls) return; - - const panelRect = this.uiManager.elements.panel.getBoundingClientRect(); - const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; - - let idealX, idealY; - // Determine dimensions based on layout. Fallback values are estimates. - const isHorizontal = ['top', 'bottom'].includes(controlsPosition); - const controlsWidth = this.uiManager.elements.controls.offsetWidth || (isHorizontal ? 160 : 40); - const controlsHeight = this.uiManager.elements.controls.offsetHeight || (isHorizontal ? 40 : 160); - const margin = 5; - - // Determine the ideal, unconstrained position for the controls - switch (controlsPosition) { - case "top-left": - idealX = panelRect.left - controlsWidth - margin; - idealY = panelRect.top; - break; - case "top-right": - idealX = panelRect.right + margin; - idealY = panelRect.top; - break; - case "bottom-left": - idealX = panelRect.left - controlsWidth - margin; - idealY = panelRect.bottom - controlsHeight; - break; - case "bottom-right": - idealX = panelRect.right + margin; - idealY = panelRect.bottom - controlsHeight; - break; - case "top": - idealX = panelRect.left + (panelRect.width - controlsWidth) / 2; - idealY = panelRect.top - controlsHeight - margin; - break; - case "bottom": - idealX = panelRect.left + (panelRect.width - controlsWidth) / 2; - idealY = panelRect.bottom + margin; - break; - case "left": - idealX = panelRect.left - controlsWidth - margin; - idealY = panelRect.top + (panelRect.height - controlsHeight) / 2; - break; - case "right": - idealX = panelRect.right + margin; - idealY = panelRect.top + (panelRect.height - controlsHeight) / 2; - break; - default: // Fallback to top-right - idealX = panelRect.right + margin; - idealY = panelRect.top; - break; - } - - let finalX = idealX; - let finalY = idealY; - - // For side-positioned controls, check if they are forced to move horizontally. - // If so, override their vertical alignment to be "top" instead of "center". - if (['left', 'right'].includes(controlsPosition)) { - if (idealX < margin || (idealX + controlsWidth) > (window.innerWidth - margin)) { - finalY = panelRect.top; // Switch to top alignment - - // Also, place it slightly inside the panel to ensure it's visible. - if (controlsPosition === 'left') { - finalX = margin; - } else { // 'right' - finalX = window.innerWidth - controlsWidth - margin; - } - } - } - - // Apply boundary constraints to the final calculated position - finalX = Math.max(margin, Math.min(finalX, window.innerWidth - controlsWidth - margin)); - finalY = Math.max(margin, Math.min(finalY, window.innerHeight - controlsHeight - margin)); - - this.uiManager.elements.controls.style.left = `${finalX}px`; - this.uiManager.elements.controls.style.top = `${finalY}px`; - - // Update layout class based on the setting - this.uiManager.updateControlsLayout(controlsPosition); - } - - applyPinnedPosition() { - let { x, y } = this.stateManager.state.pinnedPosition; - const panel = this.uiManager.elements.panel; - const panelWidth = panel.offsetWidth; - const panelHeight = panel.offsetHeight; - const margin = 10; - - // Ensure the pinned panel stays within the viewport boundaries, e.g., after content expands. - // This adjustment is now temporary and won't update the stored pinned position. - const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin)); - const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin)); - - panel.style.left = `${boundedX}px`; - panel.style.top = `${boundedY}px`; - } - - calculateNormalPosition() { - const magnifyGlass = window.comfyUIMagnifyGlass; - if (!magnifyGlass) return; - - const settings = this.stateManager.state.settings; - const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; - const panelHeight = Math.min(settings["🔍MagnifyGlass.InfoPanelMaxHeight"], this.uiManager.elements.panel.scrollHeight); - - let left, top; - const margin = 15; - - if (!this.stateManager.state.isGlassPreviewVisible) { - // Position at mouse cursor when glass is hidden - left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); - top = magnifyGlass.lastKnownMousePosition.y - 20; - } else { - // Position relative to magnify glass - const glassRect = magnifyGlass.ui.glassDiv?.getBoundingClientRect(); - if (glassRect) { - const position = settings["🔍MagnifyGlass.InfoPanelPosition"]; - - switch (position) { - case "Right": - left = glassRect.right + margin; - top = glassRect.top; - break; - case "Left": - left = glassRect.left - panelWidth - margin; - top = glassRect.top; - break; - case "Top": - left = glassRect.left; - top = glassRect.top - panelHeight - margin; - break; - case "Bottom": - left = glassRect.left; - top = glassRect.bottom + margin; - break; - default: - left = glassRect.right + margin; - top = glassRect.top; - break; - } - } else { - // Fallback to mouse position - left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); - top = magnifyGlass.lastKnownMousePosition.y - 20; - } - } - - // Apply boundary constraints - left = Math.max(10, Math.min(left, window.innerWidth - panelWidth - 10)); - top = Math.max(10, Math.min(top, window.innerHeight - panelHeight - 10)); - - this.uiManager.elements.panel.style.left = `${left}px`; - this.uiManager.elements.panel.style.top = `${top}px`; + left = Math.max(10, Math.min(left, window.innerWidth - panelWidth - 10)); + top = Math.max(10, Math.min(top, window.innerHeight - panelHeight - 10)); + this.panelElement.style.left = `${left}px`; + this.panelElement.style.top = `${top}px`; + } + /** + * Position the floating controls relative to the panel. + */ + positionFloatingControls(controlsElement) { + if (!controlsElement || !this.panelElement) return; + const panelRect = this.panelElement.getBoundingClientRect(); + const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "right"; + const margin = 8; + let left; + let top; + switch (controlsPosition) { + case "left": + left = panelRect.left - controlsElement.offsetWidth - margin; + top = panelRect.top; + break; + case "right": + left = panelRect.right + margin; + top = panelRect.top; + break; + case "top": + left = panelRect.left; + top = panelRect.top - controlsElement.offsetHeight - margin; + break; + case "bottom": + left = panelRect.left; + top = panelRect.bottom + margin; + break; + case "top-right": + left = panelRect.right - controlsElement.offsetWidth; + top = panelRect.top - controlsElement.offsetHeight - margin; + break; + default: + left = panelRect.right + margin; + top = panelRect.top; } + left = Math.max(10, Math.min(left, window.innerWidth - controlsElement.offsetWidth - 10)); + top = Math.max(10, Math.min(top, window.innerHeight - controlsElement.offsetHeight - 10)); + controlsElement.style.left = `${left}px`; + controlsElement.style.top = `${top}px`; + } } +export { + PositionManager +}; +//# sourceMappingURL=PositionManager.js.map diff --git a/web/info-panel/PositionManager.js.map b/web/info-panel/PositionManager.js.map new file mode 100644 index 0000000..6c5b473 --- /dev/null +++ b/web/info-panel/PositionManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"PositionManager.js","sources":["../../src/info-panel/PositionManager.ts"],"sourcesContent":["/**\n * Position Manager class.\n * Handles all positioning logic for the info panel.\n */\n\nimport { StateManager } from './StateManager';\n\nexport class PositionManager {\n stateManager: StateManager;\n panelElement: HTMLElement;\n\n constructor(stateManager: StateManager, panelElement: HTMLElement) {\n this.stateManager = stateManager;\n this.panelElement = panelElement;\n }\n\n positionPanel(): void {\n if (!this.panelElement) return;\n\n // If panel is pinned, keep it at its pinned position\n if (this.stateManager.state.isPanelPinned) {\n this.applyPinnedPosition();\n return;\n }\n\n // Normal positioning logic\n this.calculateNormalPosition();\n }\n\n applyPinnedPosition(): void {\n if (!this.panelElement) return;\n\n let { x, y } = this.stateManager.state.pinnedPosition;\n const panelWidth = this.panelElement.offsetWidth;\n const panelHeight = this.panelElement.offsetHeight;\n const margin = 10;\n\n const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin));\n const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin));\n\n this.panelElement.style.left = `${boundedX}px`;\n this.panelElement.style.top = `${boundedY}px`;\n }\n\n calculateNormalPosition(): void {\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (!magnifyGlass || !this.panelElement) return;\n\n const settings = this.stateManager.state.settings;\n const panelWidth = settings[\"🔍MagnifyGlass.InfoPanelWidth\"];\n const panelHeight = this.panelElement.offsetHeight; // Use actual height\n\n let left: number;\n let top: number;\n const margin = 15;\n\n if (!this.stateManager.state.isGlassPreviewVisible) {\n left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2);\n top = magnifyGlass.lastKnownMousePosition.y - 20;\n } else {\n const glassRect = magnifyGlass.ui.glassDiv?.getBoundingClientRect();\n if (glassRect) {\n const position = settings[\"🔍MagnifyGlass.InfoPanelPosition\"];\n switch (position) {\n case \"Right\":\n left = glassRect.right + margin;\n top = glassRect.top;\n break;\n case \"Left\":\n left = glassRect.left - panelWidth - margin;\n top = glassRect.top;\n break;\n case \"Top\":\n left = glassRect.left;\n top = glassRect.top - panelHeight - margin;\n break;\n case \"Bottom\":\n left = glassRect.left;\n top = glassRect.bottom + margin;\n break;\n default:\n left = glassRect.right + margin;\n top = glassRect.top;\n break;\n }\n } else {\n left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2);\n top = magnifyGlass.lastKnownMousePosition.y - 20;\n }\n }\n\n left = Math.max(10, Math.min(left, window.innerWidth - panelWidth - 10));\n top = Math.max(10, Math.min(top, window.innerHeight - panelHeight - 10));\n\n this.panelElement.style.left = `${left}px`;\n this.panelElement.style.top = `${top}px`;\n }\n\n /**\n * Position the floating controls relative to the panel.\n */\n positionFloatingControls(controlsElement: HTMLElement | null): void {\n if (!controlsElement || !this.panelElement) return;\n\n const panelRect = this.panelElement.getBoundingClientRect();\n const controlsPosition = this.stateManager.state.settings[\"🔍MagnifyGlass.ControlsPosition\"] || \"right\";\n const margin = 8;\n\n let left: number;\n let top: number;\n\n switch (controlsPosition) {\n case \"left\":\n left = panelRect.left - controlsElement.offsetWidth - margin;\n top = panelRect.top;\n break;\n case \"right\":\n left = panelRect.right + margin;\n top = panelRect.top;\n break;\n case \"top\":\n left = panelRect.left;\n top = panelRect.top - controlsElement.offsetHeight - margin;\n break;\n case \"bottom\":\n left = panelRect.left;\n top = panelRect.bottom + margin;\n break;\n case \"top-right\":\n left = panelRect.right - controlsElement.offsetWidth;\n top = panelRect.top - controlsElement.offsetHeight - margin;\n break;\n default:\n left = panelRect.right + margin;\n top = panelRect.top;\n }\n\n // Keep controls within viewport\n left = Math.max(10, Math.min(left, window.innerWidth - controlsElement.offsetWidth - 10));\n top = Math.max(10, Math.min(top, window.innerHeight - controlsElement.offsetHeight - 10));\n\n controlsElement.style.left = `${left}px`;\n controlsElement.style.top = `${top}px`;\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/StateManager.js b/web/info-panel/StateManager.js index 3f5d0a5..85e4c6d 100644 --- a/web/info-panel/StateManager.js +++ b/web/info-panel/StateManager.js @@ -1,402 +1,273 @@ -/** - * ComfyUI MagnifyGlass - Info Panel State Manager - * - * Centralized state management for the information panel. - * Complete implementation extracted from magnify_info_panel.js - */ - -import { app } from "../../../../scripts/app.js"; -import { getSettingValue } from '../shared/utils.js'; -import { DEFAULT_PANEL_SETTINGS } from '../shared/settings.js'; - -/** - * State Manager class. - * Handles all state-related operations and provides clean state transitions. - */ -export class StateManager { - constructor() { - this.state = { - // Panel visibility and positioning - isPanelVisible: false, - isPanelMinimized: false, - isPanelPinned: false, - isPanelLocked: false, // New lock state - isAutoPinned: false, // Track if panel was auto-pinned by glass hide - pinnedPosition: { x: 0, y: 0 }, - lastPinnedPosition: null, // Remember last pinned location - - // Glass visibility - isGlassPreviewVisible: true, - - // Section expansion - expandedSections: new Set(['node']), - - // Interaction states - isPanelHovered: false, - isHoveringNode: false, - - // Auto-collapse - autoExpandTimer: null, - lastNodeId: null, - - // Update scheduling - updateScheduled: false, - isInitialLoading: false, - - // Current data - currentInfo: {}, - - // Settings cache - settings: {}, - - // Auto-detected theme - currentTheme: 'dark' // Will be auto-detected - }; - - this.initThemeDetection(); - this.loadSettings(); +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { getSettingValue } from "../shared/utils.js"; +import { DEFAULT_PANEL_SETTINGS } from "../shared/settings.js"; +class StateManager { + constructor() { + __publicField(this, "state"); + this.state = { + // Panel visibility and positioning + isPanelVisible: false, + isPanelMinimized: false, + isPanelPinned: false, + isPanelLocked: false, + // New lock state + isAutoPinned: false, + // Track if panel was auto-pinned by glass hide + pinnedPosition: { x: 0, y: 0 }, + lastPinnedPosition: null, + // Remember last pinned location + // Glass visibility + isGlassPreviewVisible: true, + // Section expansion + expandedSections: /* @__PURE__ */ new Set(["node"]), + // Interaction states + isPanelHovered: false, + isHoveringNode: false, + // Auto-collapse + autoExpandTimer: null, + lastNodeId: null, + // Update scheduling + updateScheduled: false, + isInitialLoading: false, + // Current data + currentInfo: {}, + // Settings cache + settings: {}, + // Auto-detected theme + currentTheme: "dark" + // Will be auto-detected + }; + this.initThemeDetection(); + this.loadSettings(); + } + initThemeDetection() { + this.detectCurrentTheme(); + setTimeout(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 1e3); + this.setupThemeObserver(); + if (window.matchMedia) { + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); + if (prefersDark.addListener) { + prefersDark.addListener(() => this.detectCurrentTheme()); + } else if (prefersDark.addEventListener) { + prefersDark.addEventListener("change", () => this.detectCurrentTheme()); + } } - - initThemeDetection() { - // Detect ComfyUI's current theme - this.detectCurrentTheme(); - - // Force an immediate check in case we missed the initial state + this.setupThemeButtonListeners(); + } + detectCurrentTheme() { + let detectedTheme = "dark"; + const body = document.body; + const html = document.documentElement; + const vueApp = document.querySelector("#vue-app"); + const sidebar = document.querySelector('.comfy-menu, .sidebar, .menu, [class*="sidebar"], [class*="menu"]'); + const allElements = [body, html, vueApp, sidebar].filter(Boolean); + window.getComputedStyle(html); + window.getComputedStyle(body); + const backgrounds = allElements.map((el) => { + const styles = window.getComputedStyle(el); + return { + element: el.tagName || el.className, + backgroundColor: styles.backgroundColor, + color: styles.color + }; + }); + for (const bg of backgrounds) { + if (bg.backgroundColor && bg.backgroundColor !== "rgba(0, 0, 0, 0)" && bg.backgroundColor !== "transparent") { + const rgbMatch = bg.backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1e3; + if (brightness > 180) { + detectedTheme = "light"; + break; + } + } + } + } + if (detectedTheme === "dark") { + const lightElements = document.querySelectorAll("*"); + for (let i = 0; i < Math.min(lightElements.length, 50); i++) { + const el = lightElements[i]; + const styles = window.getComputedStyle(el); + const bgColor = styles.backgroundColor; + if (bgColor && bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent") { + const rgbMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (rgbMatch) { + const [, r, g, b] = rgbMatch.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1e3; + if (brightness > 200) { + detectedTheme = "light"; + break; + } + } + } + } + } + if (this.state.currentTheme !== detectedTheme) { + this.state.currentTheme = detectedTheme; + return true; + } + return false; + } + setupThemeObserver() { + const observer = new MutationObserver((mutations) => { + let shouldCheck = false; + mutations.forEach((mutation) => { + if (mutation.type === "attributes" && (mutation.attributeName === "class" || mutation.attributeName === "data-theme" || mutation.attributeName === "style")) { + shouldCheck = true; + } + }); + if (shouldCheck && this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }); + observer.observe(document.body, { attributes: true, attributeOldValue: true }); + observer.observe(document.documentElement, { attributes: true, attributeOldValue: true }); + const vueApp = document.querySelector("#vue-app"); + if (vueApp) { + observer.observe(vueApp, { attributes: true, attributeOldValue: true }); + } + const themeElements = document.querySelectorAll('[class*="theme"], [class*="dark"], [class*="light"], [data-theme]'); + themeElements.forEach((el) => { + observer.observe(el, { attributes: true, attributeOldValue: true }); + }); + setInterval(() => { + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 2e3); + } + setupThemeButtonListeners() { + document.addEventListener("click", (e) => { + var _a, _b, _c, _d, _e; + const target = e.target; + const isThemeButton = ((_a = target.textContent) == null ? void 0 : _a.toLowerCase().includes("theme")) || ((_b = target.textContent) == null ? void 0 : _b.toLowerCase().includes("light")) || ((_c = target.textContent) == null ? void 0 : _c.toLowerCase().includes("dark")) || ((_d = target.title) == null ? void 0 : _d.toLowerCase().includes("theme")) || ((_e = target.className) == null ? void 0 : _e.toLowerCase().includes("theme")); + if (isThemeButton) { setTimeout(() => { - if (this.detectCurrentTheme()) { - this.notifyThemeChange(); - } - }, 1000); - - // Set up mutation observer to watch for theme changes - this.setupThemeObserver(); - - // Listen for system theme changes as fallback - if (window.matchMedia) { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); - prefersDark.addListener(() => this.detectCurrentTheme()); - } - - // Add click listener to theme toggle buttons for immediate detection - this.setupThemeButtonListeners(); + if (this.detectCurrentTheme()) { + this.notifyThemeChange(); + } + }, 100); + } + }); + } + notifyThemeChange() { + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + if (window.infoPanelManager && window.infoPanelManager.uiManager) { + window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); } - - detectCurrentTheme() { - let detectedTheme = 'dark'; // Default fallback - - const body = document.body; - const html = document.documentElement; - const vueApp = document.querySelector('#vue-app'); - const sidebar = document.querySelector('.comfy-menu, .sidebar, .menu, [class*="sidebar"], [class*="menu"]'); - - // Get all possible theme-related elements - const allElements = [body, html, vueApp, sidebar].filter(Boolean); - - // Debug: Log current classes and attributes with more detail - const debugInfo = { - bodyClasses: Array.from(body.classList), - htmlClasses: Array.from(html.classList), - bodyDataTheme: body.getAttribute('data-theme'), - htmlDataTheme: html.getAttribute('data-theme'), - vueAppClasses: vueApp ? Array.from(vueApp.classList) : null, - sidebarClasses: sidebar ? Array.from(sidebar.classList) : null, - bodyBgColor: window.getComputedStyle(body).backgroundColor, - htmlBgColor: window.getComputedStyle(html).backgroundColor, - allComputedVars: {} + } + loadSettings() { + Object.keys(DEFAULT_PANEL_SETTINGS).forEach((key) => { + if (key !== "🔍MagnifyGlass.InfoPanelTheme") { + this.state.settings[key] = getSettingValue(key, DEFAULT_PANEL_SETTINGS[key]); + } + }); + this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; + } + updateSettings() { + const oldSettings = { ...this.state.settings }; + this.loadSettings(); + const changes = {}; + Object.keys(this.state.settings).forEach((key) => { + if (oldSettings[key] !== this.state.settings[key]) { + changes[key] = { + old: oldSettings[key], + new: this.state.settings[key] }; - - // Check CSS variables - const rootStyles = window.getComputedStyle(html); - const bodyStyles = window.getComputedStyle(body); - - // Get all CSS custom properties - for (let i = 0; i < rootStyles.length; i++) { - const prop = rootStyles[i]; - if (prop.startsWith('--')) { - debugInfo.allComputedVars[prop] = rootStyles.getPropertyValue(prop).trim(); - } + } + }); + return changes; + } + togglePanelVisibility() { + this.state.isPanelVisible = !this.state.isPanelVisible; + return this.state.isPanelVisible; + } + toggleGlassPreview() { + this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible; + return this.state.isGlassPreviewVisible; + } + togglePinning() { + const wasPinned = this.state.isPanelPinned; + this.state.isPanelPinned = !this.state.isPanelPinned; + if (!this.state.isPanelPinned) { + this.state.isPanelLocked = false; + this.state.isAutoPinned = false; + } + if (this.state.isPanelPinned && this.state.lastPinnedPosition) { + this.state.pinnedPosition = { ...this.state.lastPinnedPosition }; + } else if (!this.state.isPanelPinned && wasPinned) { + this.state.lastPinnedPosition = { ...this.state.pinnedPosition }; + } + return this.state.isPanelPinned; + } + toggleLocking() { + if (!this.state.isPanelPinned) { + return false; + } + this.state.isPanelLocked = !this.state.isPanelLocked; + return this.state.isPanelLocked; + } + toggleMinimized() { + this.state.isPanelMinimized = !this.state.isPanelMinimized; + return this.state.isPanelMinimized; + } + toggleSection(sectionId) { + if (sectionId === "node") return false; + if (this.state.expandedSections.has(sectionId)) { + this.state.expandedSections.delete(sectionId); + } else { + this.state.expandedSections.add(sectionId); + } + return true; + } + setPinnedPosition(x, y) { + this.state.pinnedPosition = { x, y }; + this.state.lastPinnedPosition = { x, y }; + } + setCurrentInfo(info) { + this.state.currentInfo = info; + } + scheduleAutoCollapse() { + this.clearAutoExpandTimer(); + if (this.state.settings["🔍MagnifyGlass.InfoPanelAnimations"]) { + this.state.autoExpandTimer = setTimeout(() => { + if (!this.state.isPanelHovered) { + this.collapseNodeSections(); } - - // Method 1: Look for specific light theme indicators in CSS variables - const primarySurface = rootStyles.getPropertyValue('--p-primary-color') || - rootStyles.getPropertyValue('--primary-color') || - bodyStyles.getPropertyValue('--p-primary-color') || - bodyStyles.getPropertyValue('--primary-color'); - - const surfaceGround = rootStyles.getPropertyValue('--p-surface-ground') || - rootStyles.getPropertyValue('--surface-ground') || - bodyStyles.getPropertyValue('--p-surface-ground') || - bodyStyles.getPropertyValue('--surface-ground'); - - const textColor = rootStyles.getPropertyValue('--p-text-color') || - rootStyles.getPropertyValue('--text-color') || - bodyStyles.getPropertyValue('--p-text-color') || - bodyStyles.getPropertyValue('--text-color'); - - // Method 2: Analyze background colors of multiple elements - const backgrounds = allElements.map(el => { - const styles = window.getComputedStyle(el); - return { - element: el.tagName || el.className, - backgroundColor: styles.backgroundColor, - color: styles.color - }; - }); - - // Method 3: Check for light backgrounds - for (const bg of backgrounds) { - if (bg.backgroundColor && bg.backgroundColor !== 'rgba(0, 0, 0, 0)' && bg.backgroundColor !== 'transparent') { - const rgbMatch = bg.backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (rgbMatch) { - const [, r, g, b] = rgbMatch.map(Number); - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - if (brightness > 180) { // Higher threshold for light detection - detectedTheme = 'light'; - break; - } - } - } - } - - // Method 4: Check for theme toggle buttons state - const themeButtons = document.querySelectorAll('[class*="theme"], button[title*="theme"], button[title*="Theme"], .p-button[title*="light"], .p-button[title*="Light"]'); - - // Method 5: Force light theme detection if we see light backgrounds - if (detectedTheme === 'dark') { - // Look for any element with very light background - const lightElements = document.querySelectorAll('*'); - for (let i = 0; i < Math.min(lightElements.length, 50); i++) { // Check first 50 elements - const el = lightElements[i]; - const styles = window.getComputedStyle(el); - const bgColor = styles.backgroundColor; - - if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { - const rgbMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (rgbMatch) { - const [, r, g, b] = rgbMatch.map(Number); - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - if (brightness > 200) { // Very light elements - detectedTheme = 'light'; - break; - } - } - } - } - } - - // Update theme if it changed - if (this.state.currentTheme !== detectedTheme) { - this.state.currentTheme = detectedTheme; - return true; // Theme changed - } - - return false; // No change + }, 1500); } - - setupThemeObserver() { - // Watch for changes to body/html classes and attributes - const observer = new MutationObserver((mutations) => { - let shouldCheck = false; - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && - (mutation.attributeName === 'class' || - mutation.attributeName === 'data-theme' || - mutation.attributeName === 'style')) { - shouldCheck = true; - } - }); - - if (shouldCheck && this.detectCurrentTheme()) { - // Theme changed, update UI immediately - this.notifyThemeChange(); - } - }); - - // Observe changes to body, html, and vue app - observer.observe(document.body, { attributes: true, attributeOldValue: true }); - observer.observe(document.documentElement, { attributes: true, attributeOldValue: true }); - - const vueApp = document.querySelector('#vue-app'); - if (vueApp) { - observer.observe(vueApp, { attributes: true, attributeOldValue: true }); - } - - // Also watch for changes to any element with theme-related classes - const themeElements = document.querySelectorAll('[class*="theme"], [class*="dark"], [class*="light"], [data-theme]'); - themeElements.forEach(el => { - observer.observe(el, { attributes: true, attributeOldValue: true }); - }); - - // Periodic fallback check every 2 seconds - setInterval(() => { - if (this.detectCurrentTheme()) { - this.notifyThemeChange(); - } - }, 2000); - } - - setupThemeButtonListeners() { - // Listen for clicks on theme toggle buttons - document.addEventListener('click', (e) => { - // Check if clicked element might be a theme toggle - const target = e.target; - const isThemeButton = target.textContent?.toLowerCase().includes('theme') || - target.textContent?.toLowerCase().includes('light') || - target.textContent?.toLowerCase().includes('dark') || - target.title?.toLowerCase().includes('theme') || - target.className?.toLowerCase().includes('theme'); - - if (isThemeButton) { - setTimeout(() => { - if (this.detectCurrentTheme()) { - this.notifyThemeChange(); - } - }, 100); - } - }); - } - - notifyThemeChange() { - // Update settings to reflect new theme - this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; - - // Notify UI components of theme change immediately - if (window.infoPanelManager && window.infoPanelManager.uiManager) { - window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); - } - } - - loadSettings() { - Object.keys(DEFAULT_PANEL_SETTINGS).forEach(key => { - // Skip the theme setting since we auto-detect it - if (key !== "🔍MagnifyGlass.InfoPanelTheme") { - this.state.settings[key] = getSettingValue(key, DEFAULT_PANEL_SETTINGS[key]); - } - }); - - // Set theme to auto-detected value - this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; - } - - updateSettings() { - const oldSettings = { ...this.state.settings }; - this.loadSettings(); - - // Return what changed for reactive updates - const changes = {}; - Object.keys(this.state.settings).forEach(key => { - if (oldSettings[key] !== this.state.settings[key]) { - changes[key] = { - old: oldSettings[key], - new: this.state.settings[key] - }; - } - }); - - return changes; - } - - togglePanelVisibility() { - this.state.isPanelVisible = !this.state.isPanelVisible; - return this.state.isPanelVisible; - } - - toggleGlassPreview() { - this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible; - return this.state.isGlassPreviewVisible; - } - - togglePinning() { - const wasPinned = this.state.isPanelPinned; - this.state.isPanelPinned = !this.state.isPanelPinned; - - // When unpinning, also unlock and clear auto-pin flag - if (!this.state.isPanelPinned) { - this.state.isPanelLocked = false; - this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning - } - - if (this.state.isPanelPinned && this.state.lastPinnedPosition) { - // Restore to last pinned position - this.state.pinnedPosition = { ...this.state.lastPinnedPosition }; - } else if (!this.state.isPanelPinned && wasPinned) { - // Save current position when unpinning - this.state.lastPinnedPosition = { ...this.state.pinnedPosition }; - } - - return this.state.isPanelPinned; - } - - toggleLocking() { - // Only allow locking when pinned - if (!this.state.isPanelPinned) { - return false; - } - - this.state.isPanelLocked = !this.state.isPanelLocked; - return this.state.isPanelLocked; - } - - toggleMinimized() { - this.state.isPanelMinimized = !this.state.isPanelMinimized; - return this.state.isPanelMinimized; - } - - toggleSection(sectionId) { - if (sectionId === 'node') return false; // Node section always expanded - - if (this.state.expandedSections.has(sectionId)) { - this.state.expandedSections.delete(sectionId); - } else { - this.state.expandedSections.add(sectionId); - } - return true; - } - - setPinnedPosition(x, y) { - this.state.pinnedPosition = { x, y }; - // Also save as last pinned position for memory - this.state.lastPinnedPosition = { x, y }; - } - - setCurrentInfo(info) { - this.state.currentInfo = info; - } - - scheduleAutoCollapse() { - this.clearAutoExpandTimer(); - if (this.state.settings["🔍MagnifyGlass.InfoPanelAnimations"]) { - this.state.autoExpandTimer = setTimeout(() => { - if (!this.state.isPanelHovered) { - this.collapseNodeSections(); - } - }, 1500); - } - } - - clearAutoExpandTimer() { - if (this.state.autoExpandTimer) { - clearTimeout(this.state.autoExpandTimer); - this.state.autoExpandTimer = null; - } - } - - expandNodeSections() { - this.state.expandedSections.add('hoveredNode'); - this.state.expandedSections.add('node'); - this.state.expandedSections.add('cursor'); - this.state.expandedSections.add('canvas'); - this.state.expandedSections.add('magnifier'); - this.clearAutoExpandTimer(); - } - - collapseNodeSections() { - this.state.expandedSections.delete('hoveredNode'); - this.state.expandedSections.delete('node'); - this.state.expandedSections.delete('widget'); - } - - cleanup() { - this.clearAutoExpandTimer(); + } + clearAutoExpandTimer() { + if (this.state.autoExpandTimer) { + clearTimeout(this.state.autoExpandTimer); + this.state.autoExpandTimer = null; } + } + expandNodeSections() { + this.state.expandedSections.add("hoveredNode"); + this.state.expandedSections.add("node"); + this.state.expandedSections.add("cursor"); + this.state.expandedSections.add("canvas"); + this.state.expandedSections.add("magnifier"); + this.clearAutoExpandTimer(); + } + collapseNodeSections() { + this.state.expandedSections.delete("hoveredNode"); + this.state.expandedSections.delete("node"); + this.state.expandedSections.delete("widget"); + } + cleanup() { + this.clearAutoExpandTimer(); + } } +export { + StateManager +}; +//# sourceMappingURL=StateManager.js.map diff --git a/web/info-panel/StateManager.js.map b/web/info-panel/StateManager.js.map new file mode 100644 index 0000000..748ef74 --- /dev/null +++ b/web/info-panel/StateManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"StateManager.js","sources":["../../src/info-panel/StateManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel State Manager (TypeScript)\n * \n * Centralized state management for the information panel.\n */\n\nimport { getSettingValue } from '../shared/utils';\nimport { DEFAULT_PANEL_SETTINGS } from '../shared/settings';\n\nexport interface InfoPanelStateData {\n // Panel visibility and positioning\n isPanelVisible: boolean;\n isPanelMinimized: boolean;\n isPanelPinned: boolean;\n isPanelLocked: boolean; // New lock state\n isAutoPinned: boolean; // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: number; y: number };\n lastPinnedPosition: { x: number; y: number } | null; // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: boolean;\n\n // Section expansion\n expandedSections: Set;\n\n // Interaction states\n isPanelHovered: boolean;\n isHoveringNode: boolean;\n\n // Auto-collapse\n autoExpandTimer: ReturnType | null;\n lastNodeId: string | number | null;\n\n // Update scheduling\n updateScheduled: boolean;\n isInitialLoading: boolean;\n\n // Current data\n currentInfo: any;\n\n // Settings cache\n settings: { [key: string]: any };\n\n // Auto-detected theme\n currentTheme: string;\n}\n\n/**\n * State Manager class.\n * Handles all state-related operations and provides clean state transitions.\n */\nexport class StateManager {\n state: InfoPanelStateData;\n\n constructor() {\n this.state = {\n // Panel visibility and positioning\n isPanelVisible: false,\n isPanelMinimized: false,\n isPanelPinned: false,\n isPanelLocked: false, // New lock state\n isAutoPinned: false, // Track if panel was auto-pinned by glass hide\n pinnedPosition: { x: 0, y: 0 },\n lastPinnedPosition: null, // Remember last pinned location\n\n // Glass visibility\n isGlassPreviewVisible: true,\n\n // Section expansion\n expandedSections: new Set(['node']),\n\n // Interaction states\n isPanelHovered: false,\n isHoveringNode: false,\n\n // Auto-collapse\n autoExpandTimer: null,\n lastNodeId: null,\n\n // Update scheduling\n updateScheduled: false,\n isInitialLoading: false,\n\n // Current data\n currentInfo: {},\n\n // Settings cache\n settings: {},\n\n // Auto-detected theme\n currentTheme: 'dark' // Will be auto-detected\n };\n\n this.initThemeDetection();\n this.loadSettings();\n }\n\n initThemeDetection(): void {\n // Detect ComfyUI's current theme\n this.detectCurrentTheme();\n\n // Force an immediate check in case we missed the initial state\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 1000);\n\n // Set up mutation observer to watch for theme changes\n this.setupThemeObserver();\n\n // Listen for system theme changes as fallback\n if (window.matchMedia) {\n const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');\n if (prefersDark.addListener) {\n prefersDark.addListener(() => this.detectCurrentTheme());\n } else if (prefersDark.addEventListener) {\n prefersDark.addEventListener('change', () => this.detectCurrentTheme());\n }\n }\n\n // Add click listener to theme toggle buttons for immediate detection\n this.setupThemeButtonListeners();\n }\n\n detectCurrentTheme(): boolean {\n let detectedTheme = 'dark'; // Default fallback\n\n const body = document.body;\n const html = document.documentElement;\n const vueApp = document.querySelector('#vue-app');\n const sidebar = document.querySelector('.comfy-menu, .sidebar, .menu, [class*=\"sidebar\"], [class*=\"menu\"]');\n\n // Get all possible theme-related elements\n const allElements = [body, html, vueApp, sidebar].filter(Boolean) as Element[];\n\n // Check CSS variables\n const rootStyles = window.getComputedStyle(html);\n const bodyStyles = window.getComputedStyle(body);\n\n // Method 2: Analyze background colors of multiple elements\n const backgrounds = allElements.map(el => {\n const styles = window.getComputedStyle(el);\n return {\n element: el.tagName || el.className,\n backgroundColor: styles.backgroundColor,\n color: styles.color\n };\n });\n\n // Method 3: Check for light backgrounds\n for (const bg of backgrounds) {\n if (bg.backgroundColor && bg.backgroundColor !== 'rgba(0, 0, 0, 0)' && bg.backgroundColor !== 'transparent') {\n const rgbMatch = bg.backgroundColor.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);\n if (rgbMatch) {\n const [, r, g, b] = rgbMatch.map(Number);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n if (brightness > 180) { // Higher threshold for light detection\n detectedTheme = 'light';\n break;\n }\n }\n }\n }\n\n // Method 5: Force light theme detection if we see light backgrounds\n if (detectedTheme === 'dark') {\n // Look for any element with very light background\n const lightElements = document.querySelectorAll('*');\n for (let i = 0; i < Math.min(lightElements.length, 50); i++) { // Check first 50 elements\n const el = lightElements[i];\n const styles = window.getComputedStyle(el);\n const bgColor = styles.backgroundColor;\n\n if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {\n const rgbMatch = bgColor.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);\n if (rgbMatch) {\n const [, r, g, b] = rgbMatch.map(Number);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n if (brightness > 200) { // Very light elements\n detectedTheme = 'light';\n break;\n }\n }\n }\n }\n }\n\n // Update theme if it changed\n if (this.state.currentTheme !== detectedTheme) {\n this.state.currentTheme = detectedTheme;\n return true; // Theme changed\n }\n\n return false; // No change\n }\n\n setupThemeObserver(): void {\n // Watch for changes to body/html classes and attributes\n const observer = new MutationObserver((mutations) => {\n let shouldCheck = false;\n mutations.forEach((mutation) => {\n if (mutation.type === 'attributes' &&\n (mutation.attributeName === 'class' ||\n mutation.attributeName === 'data-theme' ||\n mutation.attributeName === 'style')) {\n shouldCheck = true;\n }\n });\n\n if (shouldCheck && this.detectCurrentTheme()) {\n // Theme changed, update UI immediately\n this.notifyThemeChange();\n }\n });\n\n // Observe changes to body, html, and vue app\n observer.observe(document.body, { attributes: true, attributeOldValue: true });\n observer.observe(document.documentElement, { attributes: true, attributeOldValue: true });\n\n const vueApp = document.querySelector('#vue-app');\n if (vueApp) {\n observer.observe(vueApp, { attributes: true, attributeOldValue: true });\n }\n\n // Also watch for changes to any element with theme-related classes\n const themeElements = document.querySelectorAll('[class*=\"theme\"], [class*=\"dark\"], [class*=\"light\"], [data-theme]');\n themeElements.forEach(el => {\n observer.observe(el, { attributes: true, attributeOldValue: true });\n });\n\n // Periodic fallback check every 2 seconds\n setInterval(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 2000);\n }\n\n setupThemeButtonListeners(): void {\n // Listen for clicks on theme toggle buttons\n document.addEventListener('click', (e) => {\n // Check if clicked element might be a theme toggle\n const target = e.target as HTMLElement;\n const isThemeButton = target.textContent?.toLowerCase().includes('theme') ||\n target.textContent?.toLowerCase().includes('light') ||\n target.textContent?.toLowerCase().includes('dark') ||\n target.title?.toLowerCase().includes('theme') ||\n target.className?.toLowerCase().includes('theme');\n\n if (isThemeButton) {\n setTimeout(() => {\n if (this.detectCurrentTheme()) {\n this.notifyThemeChange();\n }\n }, 100);\n }\n });\n }\n\n notifyThemeChange(): void {\n // Update settings to reflect new theme\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n\n // Notify UI components of theme change immediately\n if (window.infoPanelManager && window.infoPanelManager.uiManager) {\n window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme);\n }\n }\n\n loadSettings(): void {\n Object.keys(DEFAULT_PANEL_SETTINGS).forEach(key => {\n // Skip the theme setting since we auto-detect it\n if (key !== \"🔍MagnifyGlass.InfoPanelTheme\") {\n this.state.settings[key] = getSettingValue(key, (DEFAULT_PANEL_SETTINGS as any)[key]);\n }\n });\n\n // Set theme to auto-detected value\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n }\n\n updateSettings(): { [key: string]: { old: any, new: any } } {\n const oldSettings = { ...this.state.settings };\n this.loadSettings();\n\n // Return what changed for reactive updates\n const changes: { [key: string]: { old: any, new: any } } = {};\n Object.keys(this.state.settings).forEach(key => {\n if (oldSettings[key] !== this.state.settings[key]) {\n changes[key] = {\n old: oldSettings[key],\n new: this.state.settings[key]\n };\n }\n });\n\n return changes;\n }\n\n togglePanelVisibility(): boolean {\n this.state.isPanelVisible = !this.state.isPanelVisible;\n return this.state.isPanelVisible;\n }\n\n toggleGlassPreview(): boolean {\n this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible;\n return this.state.isGlassPreviewVisible;\n }\n\n togglePinning(): boolean {\n const wasPinned = this.state.isPanelPinned;\n this.state.isPanelPinned = !this.state.isPanelPinned;\n\n // When unpinning, also unlock and clear auto-pin flag\n if (!this.state.isPanelPinned) {\n this.state.isPanelLocked = false;\n this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning\n }\n\n if (this.state.isPanelPinned && this.state.lastPinnedPosition) {\n // Restore to last pinned position\n this.state.pinnedPosition = { ...this.state.lastPinnedPosition };\n } else if (!this.state.isPanelPinned && wasPinned) {\n // Save current position when unpinning\n this.state.lastPinnedPosition = { ...this.state.pinnedPosition };\n }\n\n return this.state.isPanelPinned;\n }\n\n toggleLocking(): boolean {\n // Only allow locking when pinned\n if (!this.state.isPanelPinned) {\n return false;\n }\n\n this.state.isPanelLocked = !this.state.isPanelLocked;\n return this.state.isPanelLocked;\n }\n\n toggleMinimized(): boolean {\n this.state.isPanelMinimized = !this.state.isPanelMinimized;\n return this.state.isPanelMinimized;\n }\n\n toggleSection(sectionId: string): boolean {\n if (sectionId === 'node') return false; // Node section always expanded\n\n if (this.state.expandedSections.has(sectionId)) {\n this.state.expandedSections.delete(sectionId);\n } else {\n this.state.expandedSections.add(sectionId);\n }\n return true;\n }\n\n setPinnedPosition(x: number, y: number): void {\n this.state.pinnedPosition = { x, y };\n // Also save as last pinned position for memory\n this.state.lastPinnedPosition = { x, y };\n }\n\n setCurrentInfo(info: any): void {\n this.state.currentInfo = info;\n }\n\n scheduleAutoCollapse(): void {\n this.clearAutoExpandTimer();\n if (this.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"]) {\n this.state.autoExpandTimer = setTimeout(() => {\n if (!this.state.isPanelHovered) {\n this.collapseNodeSections();\n }\n }, 1500);\n }\n }\n\n clearAutoExpandTimer(): void {\n if (this.state.autoExpandTimer) {\n clearTimeout(this.state.autoExpandTimer);\n this.state.autoExpandTimer = null;\n }\n }\n\n expandNodeSections(): void {\n this.state.expandedSections.add('hoveredNode');\n this.state.expandedSections.add('node');\n this.state.expandedSections.add('cursor');\n this.state.expandedSections.add('canvas');\n this.state.expandedSections.add('magnifier');\n this.clearAutoExpandTimer();\n }\n\n collapseNodeSections(): void {\n this.state.expandedSections.delete('hoveredNode');\n this.state.expandedSections.delete('node');\n this.state.expandedSections.delete('widget');\n }\n\n cleanup(): void {\n this.clearAutoExpandTimer();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/UIManager.js b/web/info-panel/UIManager.js index b402598..08b01bf 100644 --- a/web/info-panel/UIManager.js +++ b/web/info-panel/UIManager.js @@ -1,47 +1,31 @@ -/** - * ComfyUI MagnifyGlass - Info Panel UI Manager - * - * Complete UI Manager extracted from magnify_info_panel.js - * Handles all DOM manipulation and UI creation for the info panel. - */ - -import { Z_INDEX } from '../shared/constants.js'; -import { getInfoPanelCSS } from './styles.js'; - -/** - * UI Manager class. - * Handles all DOM manipulation and UI creation. - */ -export class UIManager { - /** - * @param {import('./StateManager.js').StateManager} stateManager - */ - constructor(stateManager) { - this.stateManager = stateManager; - this.elements = { - panel: null, - header: null, - content: null, - controls: null - }; - - this.createPanel(); - this.injectStyles(); - } - - /** - * Create the main panel and its components. - */ - createPanel() { - // Main panel container - this.elements.panel = document.createElement("div"); - this.elements.panel.id = "comfyui-magnify-info-panel-pro-v2"; - this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`; - - // Header - this.elements.header = document.createElement("div"); - this.elements.header.className = "panel-header"; - this.elements.header.innerHTML = ` +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { getInfoPanelCSS } from "./styles.js"; +class UIManager { + constructor(stateManager) { + __publicField(this, "stateManager"); + __publicField(this, "elements"); + this.stateManager = stateManager; + this.elements = { + panel: null, + header: null, + content: null, + controls: null + }; + this.createPanel(); + this.injectStyles(); + } + /** + * Create the main panel and its components. + */ + createPanel() { + this.elements.panel = document.createElement("div"); + this.elements.panel.id = "comfyui-magnify-info-panel-pro-v2"; + this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`; + this.elements.header = document.createElement("div"); + this.elements.header.className = "panel-header"; + this.elements.header.innerHTML = `
🔍
Inspector
@@ -51,129 +35,96 @@ export class UIManager {
`; - - // Content container - this.elements.content = document.createElement("div"); - this.elements.content.className = "panel-content"; - - this.elements.panel.appendChild(this.elements.header); - this.elements.panel.appendChild(this.elements.content); - - this.applyStyles(); - document.body.appendChild(this.elements.panel); - - // Create floating controls after panel is in DOM - if (this.stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"]) { - this.createFloatingControls(); - // Update control states immediately after creation - this.updateControlStates(); - } + this.elements.content = document.createElement("div"); + this.elements.content.className = "panel-content"; + this.elements.panel.appendChild(this.elements.header); + this.elements.panel.appendChild(this.elements.content); + this.applyStyles(); + document.body.appendChild(this.elements.panel); + if (this.stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"]) { + this.createFloatingControls(); + this.updateControlStates(); } - - /** - * Create floating control buttons. - */ - createFloatingControls() { - this.elements.controls = document.createElement("div"); - this.elements.controls.className = "floating-controls vertical-layout"; // Default to vertical - this.elements.controls.innerHTML = ` + } + /** + * Create floating control buttons. + */ + createFloatingControls() { + this.elements.controls = document.createElement("div"); + this.elements.controls.className = "floating-controls vertical-layout"; + this.elements.controls.innerHTML = ` `; - - // Insert before the panel in the document body, not as a child - document.body.appendChild(this.elements.controls); - - // Set initial layout based on settings - const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; - this.updateControlsLayout(controlsPosition); + document.body.appendChild(this.elements.controls); + const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; + this.updateControlsLayout(controlsPosition); + } + /** + * Update control button states. + */ + updateControlStates() { + if (!this.elements.controls) return; + const pinBtn = this.elements.controls.querySelector('[data-action="pin"]'); + const lockBtn = this.elements.controls.querySelector('[data-action="lock"]'); + const visibilityBtn = this.elements.controls.querySelector('[data-action="toggle-panel"]'); + const glassBtn = this.elements.controls.querySelector('[data-action="toggle-glass"]'); + if (pinBtn) { + pinBtn.classList.toggle("active", this.stateManager.state.isPanelPinned); + pinBtn.title = this.stateManager.state.isPanelPinned ? "Lock Panel" : "Unlock Panel"; + pinBtn.textContent = this.stateManager.state.isPanelPinned ? "🔒" : "🔓"; } - - /** - * Update control button states. - */ - updateControlStates() { - if (!this.elements.controls) return; - - const pinBtn = this.elements.controls.querySelector('[data-action="pin"]'); - const lockBtn = this.elements.controls.querySelector('[data-action="lock"]'); - const visibilityBtn = this.elements.controls.querySelector('[data-action="toggle-panel"]'); - const glassBtn = this.elements.controls.querySelector('[data-action="toggle-glass"]'); - - if (pinBtn) { - pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned); - pinBtn.title = this.stateManager.state.isPanelPinned ? "Lock Panel" : "Unlock Panel"; - pinBtn.textContent = this.stateManager.state.isPanelPinned ? "🔒" : "🔓"; - } - - if (lockBtn) { - // Only show/enable lock button when pinned - lockBtn.style.display = this.stateManager.state.isPanelPinned ? 'flex' : 'none'; - lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked); - lockBtn.title = this.stateManager.state.isPanelLocked ? "Upin Panel Position" : "Pin Panel Position"; - lockBtn.disabled = !this.stateManager.state.isPanelPinned; - } - - if (visibilityBtn) { - visibilityBtn.classList.toggle('active', this.stateManager.state.isPanelVisible); - visibilityBtn.title = this.stateManager.state.isPanelVisible ? "Show Panel" : "Hide Panel"; - } - - if (glassBtn) { - glassBtn.classList.toggle('active', this.stateManager.state.isGlassPreviewVisible); - glassBtn.title = this.stateManager.state.isGlassPreviewVisible ? "Hide Glass Preview" : "Show Glass Preview"; - } + if (lockBtn) { + lockBtn.style.display = this.stateManager.state.isPanelPinned ? "flex" : "none"; + lockBtn.classList.toggle("active", this.stateManager.state.isPanelLocked); + lockBtn.title = this.stateManager.state.isPanelLocked ? "Upin Panel Position" : "Pin Panel Position"; + lockBtn.disabled = !this.stateManager.state.isPanelPinned; } - - /** - * Update controls layout based on position setting. - * @param {string} position - */ - updateControlsLayout(position) { - if (!this.elements.controls) return; - - // Remove existing layout classes - this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout'); - - // Add appropriate layout class - if (['top', 'bottom'].includes(position)) { - this.elements.controls.classList.add('horizontal-layout'); - } else { - this.elements.controls.classList.add('vertical-layout'); - } + if (visibilityBtn) { + visibilityBtn.classList.toggle("active", this.stateManager.state.isPanelVisible); + visibilityBtn.title = this.stateManager.state.isPanelVisible ? "Show Panel" : "Hide Panel"; } - - /** - * Apply current styles to elements. - */ - applyStyles() { - const settings = this.stateManager.state.settings; - - // Apply custom text color via CSS variable if provided - const textColor = settings["🔍MagnifyGlass.InfoPanelTextColor"]; - if (textColor) { - // Ensure color has # symbol - const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`; - this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor); - } - - // Apply custom accent color via CSS variable if provided - const accentColor = settings["🔍MagnifyGlass.InfoPanelAccentColor"]; - if (accentColor) { - // Ensure color has # symbol - const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`; - this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor); - } - - // Apply opacity setting if panel is visible (convert percentage to decimal) - if (this.stateManager.state.isPanelVisible) { - const opacityPercent = settings["🔍MagnifyGlass.InfoPanelOpacity"]; - this.elements.panel.style.opacity = opacityPercent / 100; - } - - this.elements.panel.style.cssText = ` + if (glassBtn) { + glassBtn.classList.toggle("active", this.stateManager.state.isGlassPreviewVisible); + glassBtn.title = this.stateManager.state.isGlassPreviewVisible ? "Hide Glass Preview" : "Show Glass Preview"; + } + } + /** + * Update controls layout based on position setting. + * @param position + */ + updateControlsLayout(position) { + if (!this.elements.controls) return; + this.elements.controls.classList.remove("horizontal-layout", "vertical-layout"); + if (["top", "bottom"].includes(position)) { + this.elements.controls.classList.add("horizontal-layout"); + } else { + this.elements.controls.classList.add("vertical-layout"); + } + } + /** + * Apply current styles to elements. + */ + applyStyles() { + if (!this.elements.panel) return; + const settings = this.stateManager.state.settings; + const textColor = settings["🔍MagnifyGlass.InfoPanelTextColor"]; + if (textColor) { + const normalizedTextColor = textColor.startsWith("#") ? textColor : `#${textColor}`; + this.elements.panel.style.setProperty("--info-panel-text-color", normalizedTextColor); + } + const accentColor = settings["🔍MagnifyGlass.InfoPanelAccentColor"]; + if (accentColor) { + const normalizedAccentColor = accentColor.startsWith("#") ? accentColor : `#${accentColor}`; + this.elements.panel.style.setProperty("--info-panel-accent-color", normalizedAccentColor); + } + if (this.stateManager.state.isPanelVisible) { + const opacityPercent = settings["🔍MagnifyGlass.InfoPanelOpacity"]; + this.elements.panel.style.opacity = (opacityPercent / 100).toString(); + } + this.elements.panel.style.cssText = ` position: absolute; width: ${settings["🔍MagnifyGlass.InfoPanelWidth"]}px; max-height: ${settings["🔍MagnifyGlass.InfoPanelMaxHeight"]}px; @@ -181,639 +132,563 @@ export class UIManager { display: none; opacity: 0; transform: translateY(-10px); - transition: ${settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none'}; + transition: ${settings["🔍MagnifyGlass.InfoPanelAnimations"] ? "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" : "none"}; pointer-events: auto; user-select: none; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - ${textColor ? `--info-panel-text-color: ${textColor.startsWith('#') ? textColor : `#${textColor}`};` : ''} - ${accentColor ? `--info-panel-accent-color: ${accentColor.startsWith('#') ? accentColor : `#${accentColor}`};` : ''} + ${textColor ? `--info-panel-text-color: ${textColor.startsWith("#") ? textColor : `#${textColor}`};` : ""} + ${accentColor ? `--info-panel-accent-color: ${accentColor.startsWith("#") ? accentColor : `#${accentColor}`};` : ""} `; + } + /** + * Show the panel. + */ + show() { + if (!this.elements.panel) return; + if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { + this.elements.panel.style.display = "block"; + if (this.elements.controls) { + this.elements.controls.style.display = "flex"; + } + const opacityPercent = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"]; + this.elements.panel.style.opacity = (opacityPercent / 100).toString(); + this.elements.panel.offsetHeight; + this.elements.panel.classList.add("visible"); + this.stateManager.state.isPanelVisible = true; } - - /** - * Show the panel. - */ - show() { - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { - this.elements.panel.style.display = "block"; - if (this.elements.controls) { - this.elements.controls.style.display = "flex"; + } + /** + * Hide the panel. + */ + hide() { + if (!this.elements.panel) return; + this.elements.panel.classList.remove("visible"); + setTimeout(() => { + if (!this.stateManager.state.isPanelVisible && this.elements.panel) { + this.elements.panel.style.display = "none"; + if (this.elements.controls) { + this.elements.controls.style.display = "none"; + } + } + }, this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 300 : 0); + this.stateManager.state.isPanelVisible = false; + } + /** + * Update minimized state. + */ + updateMinimizedState() { + if (!this.elements.panel || !this.elements.header) return; + this.elements.panel.classList.toggle("panel-minimized", this.stateManager.state.isPanelMinimized); + const minimizeBtn = this.elements.header.querySelector(".minimize-btn"); + if (minimizeBtn) { + minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? "+" : "−"; + minimizeBtn.title = this.stateManager.state.isPanelMinimized ? "Expand Panel" : "Minimize Panel"; + } + } + /** + * Update pinned state. + */ + updatePinnedState() { + if (!this.elements.panel) return; + this.elements.panel.classList.toggle("panel-pinned", this.stateManager.state.isPanelPinned); + this.elements.panel.classList.toggle("panel-locked", this.stateManager.state.isPanelLocked); + this.updateControlStates(); + } + /** + * Update theme class. + * @param newTheme + */ + updateTheme(newTheme) { + if (this.elements.panel) { + this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); + } + } + /** + * Display information in the panel. + * @param info + */ + displayInfo(info) { + const sections = this.buildSections(info); + this.renderSections(sections); + this.updateSectionStates(); + this.updateHeaderSubtitle(info); + } + /** + * Build section data from info object. + * @param info + * @returns + */ + buildSections(info) { + const sections = []; + const settings = this.stateManager.state.settings; + if (settings["🔍MagnifyGlass.ShowInspectorTab"]) { + const inspectorContent = [ + { label: "Cursor Canvas", value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` }, + { label: "Canvas Scale", value: `${(info.canvas.scale * 100).toFixed(1)}%` }, + { label: "Magnifier Zoom", value: `${info.magnifier.zoomFactor}×` } + ]; + sections.push({ + id: "inspector", + icon: "🔍", + title: "Inspector", + content: inspectorContent + }); + } + if (info.media) { + const mediaContent = [ + { label: "Type", value: info.media.tagName }, + { label: "Source", value: info.media.src } + ]; + if (info.media.naturalSize) { + mediaContent.push({ label: "Natural", value: info.media.naturalSize }); + } + sections.push({ + id: "media", + icon: "📷", + title: "Media", + badge: info.media.tagName, + content: mediaContent + }); + } + if (info.hoveredNode) { + const nodeContent = [ + { label: "Title", value: info.hoveredNode.title } + ]; + const nodeType = info.hoveredNode.type ? info.hoveredNode.type.toLowerCase() : ""; + const isSaveNode = nodeType.includes("save") && !nodeType.includes("checkpoint") && !nodeType.includes("model") && !nodeType.includes("preview"); + const showAllWidgets = info.hoveredNode.type && (nodeType.includes("ksampler") || nodeType.includes("sampler") || nodeType.includes("k_samplers") || nodeType.includes("checkpoint") || nodeType.includes("model") || nodeType.includes("lora") || nodeType.includes("controlnet") || nodeType.includes("advanced") || nodeType.includes("detailer") || nodeType.includes("inpaint") || nodeType.includes("upscale") || nodeType.includes("clip") || nodeType.includes("text") || nodeType.includes("encode")) && !isSaveNode; + if (!showAllWidgets) { + const checkpointInfo = this.getCheckpointInfo(info.hoveredNode); + if (checkpointInfo) { + nodeContent.push({ label: "Model", value: checkpointInfo }); + } + const imageInfo = this.getImageInfo(info.hoveredNode); + if (imageInfo) { + if (typeof imageInfo === "string") { + nodeContent.push({ label: "Image", value: imageInfo }); + } else if (typeof imageInfo === "object" && imageInfo !== null) { + const imgInfoAny = imageInfo; + nodeContent.push({ label: "Image Size", value: `${imgInfoAny.width}×${imgInfoAny.height}` }); + if (imgInfoAny.src) { + nodeContent.push({ label: "Image Source", value: imgInfoAny.src }); } - // Apply user's opacity setting when showing (convert percentage to decimal) - const opacityPercent = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"]; - this.elements.panel.style.opacity = opacityPercent / 100; - // Trigger reflow - this.elements.panel.offsetHeight; - this.elements.panel.classList.add('visible'); - this.stateManager.state.isPanelVisible = true; + } } - } - - /** - * Hide the panel. - */ - hide() { - this.elements.panel.classList.remove('visible'); - setTimeout(() => { - if (!this.stateManager.state.isPanelVisible) { - this.elements.panel.style.display = "none"; - if (this.elements.controls) { - this.elements.controls.style.display = "none"; - } - } - }, this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 300 : 0); - this.stateManager.state.isPanelVisible = false; - } - - /** - * Update minimized state. - */ - updateMinimizedState() { - this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized); - const minimizeBtn = this.elements.header.querySelector('.minimize-btn'); - if (minimizeBtn) { - minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−'; - minimizeBtn.title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel'; + const textBoxContent = this.getTextBoxContent(info.hoveredNode); + if (textBoxContent) { + nodeContent.push({ label: "Text", value: textBoxContent }); } + } + const importantParameters = this.getImportantNodeParameters(info.hoveredNode); + nodeContent.push(...importantParameters); + sections.push({ + id: "node", + icon: "🎯", + title: "Node", + badge: info.hoveredNode.type, + content: nodeContent + }); } - - /** - * Update pinned state. - */ - updatePinnedState() { - this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned); - this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked); - this.updateControlStates(); - } - - /** - * Update theme class. - * @param {string} newTheme - */ - updateTheme(newTheme) { - if (this.elements.panel) { - this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); - } - } - - /** - * Display information in the panel. - * @param {object} info - */ - displayInfo(info) { - const sections = this.buildSections(info); - this.renderSections(sections); - this.updateSectionStates(); - this.updateHeaderSubtitle(info); - } - - /** - * Build section data from info object. - * @param {object} info - * @returns {Array} - */ - buildSections(info) { - const sections = []; - const settings = this.stateManager.state.settings; - - // Inspector section - if (settings["🔍MagnifyGlass.ShowInspectorTab"]) { - const inspectorContent = [ - { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` }, - { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` }, - { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` } - ]; - - sections.push({ - id: 'inspector', - icon: '🔍', - title: 'Inspector', - content: inspectorContent - }); - } - - // Media section - if (info.media) { - const mediaContent = [ - { label: 'Type', value: info.media.tagName }, - { label: 'Source', value: info.media.src } - ]; - - if (info.media.naturalSize) { - mediaContent.push({ label: 'Natural', value: info.media.naturalSize }); - } - - sections.push({ - id: 'media', - icon: '📷', - title: 'Media', - badge: info.media.tagName, - content: mediaContent - }); - } - - // Node Details section - if (info.hoveredNode) { - const nodeContent = [ - { label: 'Title', value: info.hoveredNode.title } - ]; - - // Check if this is a complex node that shows all widgets - const nodeType = info.hoveredNode.type.toLowerCase(); - const isSaveNode = nodeType.includes('save') && - !nodeType.includes('checkpoint') && - !nodeType.includes('model') && - !nodeType.includes('preview'); - - const showAllWidgets = info.hoveredNode.type && ( - nodeType.includes('ksampler') || - nodeType.includes('sampler') || - nodeType.includes('k_samplers') || - nodeType.includes('checkpoint') || - nodeType.includes('model') || - nodeType.includes('lora') || - nodeType.includes('controlnet') || - nodeType.includes('advanced') || - nodeType.includes('detailer') || - nodeType.includes('inpaint') || - nodeType.includes('upscale') || - nodeType.includes('clip') || - nodeType.includes('text') || - nodeType.includes('encode') - ) && !isSaveNode; - - if (!showAllWidgets) { - // Add specific extractions for simple nodes - const checkpointInfo = this.getCheckpointInfo(info.hoveredNode); - if (checkpointInfo) { - nodeContent.push({ label: 'Model', value: checkpointInfo }); - } - - const imageInfo = this.getImageInfo(info.hoveredNode); - if (imageInfo) { - if (typeof imageInfo === 'string') { - nodeContent.push({ label: 'Image', value: imageInfo }); - } else if (typeof imageInfo === 'object') { - nodeContent.push({ label: 'Image Size', value: `${imageInfo.width}×${imageInfo.height}` }); - if (imageInfo.src) { - nodeContent.push({ label: 'Image Source', value: imageInfo.src }); - } - } - } - - const textBoxContent = this.getTextBoxContent(info.hoveredNode); - if (textBoxContent) { - nodeContent.push({ label: 'Text', value: textBoxContent }); - } - } - - const importantParameters = this.getImportantNodeParameters(info.hoveredNode); - nodeContent.push(...importantParameters); - - sections.push({ - id: 'node', - icon: '🎯', - title: 'Node', - badge: info.hoveredNode.type, - content: nodeContent - }); - } - - return sections; - } - - /** - * Render sections to the panel. - * @param {Array} sections - */ - renderSections(sections) { - if (sections.length === 0) { - this.elements.content.innerHTML = ` + return sections; + } + /** + * Render sections to the panel. + * @param sections + */ + renderSections(sections) { + if (!this.elements.content) return; + if (sections.length === 0) { + this.elements.content.innerHTML = `
📍
Empty canvas area
`; - return; - } - - this.elements.content.innerHTML = sections.map(section => ` + return; + } + this.elements.content.innerHTML = sections.map((section) => `
${section.icon} ${section.title} - ${section.badge ? `${section.badge}` : ''} - ${section.id !== 'node' ? '▶' : ''} + ${section.badge ? `${section.badge}` : ""} + ${section.id !== "node" ? '▶' : ""}
- ${section.content.map(item => { - const value = this.formatValue(item.value, item.label); - const valueClass = this.getValueClass(item.value); - const valueAttributes = this.getValueAttributes(item.value); - return ` + ${section.content.map((item) => { + const value = this.formatValue(item.value, item.label); + const valueClass = this.getValueClass(item.value); + const valueAttributes = this.getValueAttributes(item.value); + return `
${item.label} ${value}
`; - }).join('')} + }).join("")}
- `).join(''); - } - - /** - * Update section expansion states. - */ - updateSectionStates() { - const sections = this.elements.content.querySelectorAll('.info-section'); - sections.forEach(section => { - const sectionId = section.dataset.section; - const header = section.querySelector('.section-header'); - const content = section.querySelector('.section-content'); - - if (this.stateManager.state.expandedSections.has(sectionId)) { - header.classList.add('expanded'); - content.classList.add('expanded'); - } else { - header.classList.remove('expanded'); - content.classList.remove('expanded'); - } - }); - } - - /** - * Format a value for display. - * @param {*} value - * @param {string} label - * @returns {string} - */ - formatValue(value, label) { - if (!value) return value; - - const str = String(value); - - // Show full text for prompts, text content, model names, and file paths - if (label && ( - label.toLowerCase().includes('text') || - label.toLowerCase().includes('prompt') || - label.toLowerCase().includes('model') || - label.toLowerCase().includes('file') || - label.toLowerCase().includes('conditioning') || - label.toLowerCase().includes('positive') || - label.toLowerCase().includes('negative') - )) { - return str; - } - - // Show full text for very long values (no truncation) - return str; - } - - /** - * Get value class for styling. - * @param {*} value - * @returns {string} - */ - getValueClass(value) { - if (!value) return ''; - - const str = String(value); - let classes = []; - - // Mark text that might benefit from special styling for readability - if (str.length > 100) { - classes.push('long-text'); - } - - return classes.join(' '); - } - - /** - * Get value attributes. - * @param {*} value - * @returns {string} - */ - getValueAttributes(value) { - // Since we show full text now, we don't need title attributes for long text - // Only add title for very long text that might benefit from tooltips - if (!value) return ''; - - const str = String(value); - if (str.length > 500) { // Only for extremely long text - return `title="${str.replace(/"/g, '"')}"`; - } - - return ''; - } - - /** - * Update header subtitle. - * @param {object} info - */ - updateHeaderSubtitle(info) { - const subtitleElement = this.elements.header.querySelector('.header-subtitle'); - if (subtitleElement) { - const accentColor = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"]; - if (info.hoveredNode) { - subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`; - subtitleElement.style.color = accentColor; - } else if (info.media) { - subtitleElement.textContent = `Media: ${info.media.tagName}`; - subtitleElement.style.color = accentColor; - } else if (info.connection) { - subtitleElement.textContent = `Connection: ${info.connection.type}`; - subtitleElement.style.color = accentColor; - } else { - subtitleElement.textContent = 'Real-time analysis'; - subtitleElement.style.color = ''; - } - } - } - - // ============== Helper methods for node information extraction ============== - - /** - * Get checkpoint/model info from a node. - * @param {object} nodeInfo - * @returns {string|null} - */ - getCheckpointInfo(nodeInfo) { - if (nodeInfo.type && ( - nodeInfo.type.includes("CheckpointLoader") || - nodeInfo.type.includes("LoadCheckpoint") || - nodeInfo.type.includes("ModelLoader") || - nodeInfo.type.includes("UNETLoader") || - nodeInfo.type.includes("VAELoader") || - nodeInfo.type.includes("LoraLoader") - )) { - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - for (const widget of nodeInfo.widgets) { - if (widget.name && ( - widget.name.toLowerCase().includes("model") || - widget.name.toLowerCase().includes("checkpoint") || - widget.name.toLowerCase().includes("ckpt") || - widget.name.toLowerCase().includes("lora") || - widget.name.toLowerCase().includes("vae") || - widget.name.toLowerCase().includes("file") - )) { - const value = String(widget.value); - const filename = value.split(/[\/\\]/).pop(); - return filename || value; - } - } - } - } - return null; - } - - /** - * Get image info from a node. - * @param {object} nodeInfo - * @returns {string|object|null} - */ - getImageInfo(nodeInfo) { - if (nodeInfo.type && ( - nodeInfo.type.includes("SaveImage") || - nodeInfo.type.includes("PreviewImage") || - nodeInfo.type.includes("VisionOutput") || - nodeInfo.type.includes("ImageOutput") || - nodeInfo.type.includes("LoadImage") || - nodeInfo.type.includes("Display") - )) { - if (nodeInfo.widgets) { - for (const widget of nodeInfo.widgets) { - if (widget.name && ( - widget.name.toLowerCase().includes("image") || - widget.name.toLowerCase().includes("filename") || - widget.name.toLowerCase().includes("file") - )) { - return widget.value; - } - } - } - - if (nodeInfo.properties && nodeInfo.properties.img) { - const img = nodeInfo.properties.img; - return { - width: img.width || "unknown", - height: img.height || "unknown", - src: img.src ? img.src.split(/[\/\\]/).pop() : "Preview available" - }; - } - - if (nodeInfo.outputs) { - for (const output of nodeInfo.outputs) { - if (output.links && output.links.length > 0) { - return "Image connected to " + output.links.length + " node(s)"; - } - } - } - - return "Image node"; - } - return null; - } - - /** - * Get text box content from a node. - * @param {object} nodeInfo - * @returns {string|null} - */ - getTextBoxContent(nodeInfo) { - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - if (nodeInfo.type && nodeInfo.type.includes("CLIPTextEncode")) { - for (const widget of nodeInfo.widgets) { - if (widget.name === "text" && typeof widget.value === 'string') { - return widget.value; - } - } - } - - for (const widget of nodeInfo.widgets) { - if ((widget.name.toLowerCase().includes("prompt") || - widget.name.toLowerCase().includes("conditioning")) && - typeof widget.value === 'string' && widget.value.length > 0) { - return widget.value; - } - } - - for (const widget of nodeInfo.widgets) { - if ((widget.type === 'text' || widget.type === 'textarea' || - widget.type === 'string' || widget.name.toLowerCase().includes('text')) && - typeof widget.value === 'string' && widget.value.length > 0) { - - // Show full text without truncation - return widget.value; - } - } - } - return null; - } - - /** - * Get important node parameters based on node type. - * @param {object} nodeInfo - * @returns {Array} - */ - getImportantNodeParameters(nodeInfo) { - const parameters = []; - - // For complex nodes that have many parameters, show ALL widgets - const nodeType = nodeInfo.type.toLowerCase(); - const isSaveNode = nodeType.includes('save') && - !nodeType.includes('checkpoint') && - !nodeType.includes('model') && - !nodeType.includes('preview'); - - const showAllWidgets = nodeInfo.type && ( - nodeType.includes('ksampler') || - nodeType.includes('sampler') || - nodeType.includes('k_samplers') || - nodeType.includes('checkpoint') || - nodeType.includes('model') || - nodeType.includes('lora') || - nodeType.includes('controlnet') || - nodeType.includes('advanced') || - nodeType.includes('detailer') || - nodeType.includes('inpaint') || - nodeType.includes('upscale') || - nodeType.includes('clip') || - nodeType.includes('text') || - nodeType.includes('encode') - ) && !isSaveNode; - - if (showAllWidgets) { - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - for (const widget of nodeInfo.widgets) { - // Show all widgets for complex nodes, but filter out some duplicates - if (widget.name && widget.name !== '') { - const widgetName = widget.name.toLowerCase(); - - // Skip some generic widget names that might be duplicates - if (widgetName.includes('title') || - widgetName === 'node' || - widgetName === 'id' || - widgetName === 'type' || - widgetName === 'mode') { - continue; - } - - parameters.push({ - label: widget.name, - value: this.formatWidgetValue(widget.value) - }); - } - } - } - return parameters; - } - - // Special handling for Save nodes - only show essential parameters - let importantParams; - if (isSaveNode) { - importantParams = [ - 'filename_prefix', 'filename', 'directory', 'path', - 'format', 'quality', 'extension' - ]; + `).join(""); + } + /** + * Update section expansion states. + */ + updateSectionStates() { + if (!this.elements.content) return; + const sections = this.elements.content.querySelectorAll(".info-section"); + sections.forEach((section) => { + const sectionId = section.dataset.section; + if (!sectionId) return; + const header = section.querySelector(".section-header"); + const content = section.querySelector(".section-content"); + if (header && content) { + if (this.stateManager.state.expandedSections.has(sectionId)) { + header.classList.add("expanded"); + content.classList.add("expanded"); } else { - // For other nodes, use the filtered list but avoid duplicates with specific extractions - importantParams = [ - 'seed', 'steps', 'cfg', 'scale', 'sampler', 'scheduler', - 'positive', 'negative', 'width', 'height', 'denoise', 'strength', - 'noise', 'count', 'batch', 'size', 'phase', 'color', 'intensity', - // KSampler specific parameters (for other samplers) - 'control_after_generate', 'control', 'after', 'generate', - 'start_at_step', 'end_at_step', 'start', 'end', - 'return_with_leftover_noise', 'leftover', 'noise_return', - // Additional common parameters (but avoid duplicates with specific extractors) - 'model', 'vae', 'clip', 'lora', 'checkpoint', - 'latent', 'image', 'mask', 'filename', 'directory', - 'prompt', 'conditioning', 'filename_prefix', - // New detection vocabulary - 'resolution', 'num_chunks', 'seconds', 'aspect_ratio', - 'style_type', 'background', 'n', 'human', 'raw', 'guidance', - 'skip_preprocessing', 'movement_amplitude', 'animation', - 'material_type', 'b1', 'b2', 's1', 's2', 'type', 'channel', - 'sigma', 'rho', - // Additional detection vocabulary - 'alpha', 'base_shift', 'shift', 'stretch', 'terminal', - 'spacing', 'style', 'eta', 'norm_threshold', 'momentum', - 'hypernetwork_name', 'reuse_threshold', 'verbose', 'layers', - 'set_cond_area', 'audioui', - // Camera and 3D parameters - 'camera_pose', 'fx', 'cx', 'fy', 'cy' - // Note: 'text' and 'string' removed to avoid duplication with getTextBoxContent - ]; + header.classList.remove("expanded"); + content.classList.remove("expanded"); } - - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - for (const widget of nodeInfo.widgets) { - const paramName = widget.name.toLowerCase(); - if (importantParams.some(param => paramName.includes(param))) { - parameters.push({ - label: widget.name, - value: this.formatWidgetValue(widget.value) - }); - } + } + }); + } + /** + * Format a value for display. + * @param value + * @param label + * @returns + */ + formatValue(value, label) { + if (value === null || value === void 0) return ""; + const str = String(value); + if (label && (label.toLowerCase().includes("text") || label.toLowerCase().includes("prompt") || label.toLowerCase().includes("model") || label.toLowerCase().includes("file") || label.toLowerCase().includes("conditioning") || label.toLowerCase().includes("positive") || label.toLowerCase().includes("negative"))) { + return str; + } + return str; + } + /** + * Get value class for styling. + * @param value + * @returns + */ + getValueClass(value) { + if (!value) return ""; + const str = String(value); + let classes = []; + if (str.length > 100) { + classes.push("long-text"); + } + return classes.join(" "); + } + /** + * Get value attributes. + * @param value + * @returns + */ + getValueAttributes(value) { + if (!value) return ""; + const str = String(value); + if (str.length > 500) { + return `title="${str.replace(/"/g, """)}"`; + } + return ""; + } + /** + * Update header subtitle. + * @param info + */ + updateHeaderSubtitle(info) { + if (!this.elements.header) return; + const subtitleElement = this.elements.header.querySelector(".header-subtitle"); + if (subtitleElement) { + const accentColor = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"]; + if (info.hoveredNode) { + subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`; + subtitleElement.style.color = accentColor || ""; + } else if (info.media) { + subtitleElement.textContent = `Media: ${info.media.tagName}`; + subtitleElement.style.color = accentColor || ""; + } else if (info.connection) { + subtitleElement.textContent = `Connection: ${info.connection.type}`; + subtitleElement.style.color = accentColor || ""; + } else { + subtitleElement.textContent = "Real-time analysis"; + subtitleElement.style.color = ""; + } + } + } + // ============== Helper methods for node information extraction ============== + /** + * Get checkpoint/model info from a node. + * @param nodeInfo + * @returns + */ + getCheckpointInfo(nodeInfo) { + if (nodeInfo.type && (nodeInfo.type.includes("CheckpointLoader") || nodeInfo.type.includes("LoadCheckpoint") || nodeInfo.type.includes("ModelLoader") || nodeInfo.type.includes("UNETLoader") || nodeInfo.type.includes("VAELoader") || nodeInfo.type.includes("LoraLoader"))) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + if (widget.name && (widget.name.toLowerCase().includes("model") || widget.name.toLowerCase().includes("checkpoint") || widget.name.toLowerCase().includes("ckpt") || widget.name.toLowerCase().includes("lora") || widget.name.toLowerCase().includes("vae") || widget.name.toLowerCase().includes("file"))) { + const value = String(widget.value); + const filename = value.split(/[\/\\]/).pop(); + return filename || value; + } + } + } + } + return null; + } + /** + * Get image info from a node. + * @param nodeInfo + * @returns + */ + getImageInfo(nodeInfo) { + if (nodeInfo.type && (nodeInfo.type.includes("SaveImage") || nodeInfo.type.includes("PreviewImage") || nodeInfo.type.includes("VisionOutput") || nodeInfo.type.includes("ImageOutput") || nodeInfo.type.includes("LoadImage") || nodeInfo.type.includes("Display"))) { + if (nodeInfo.widgets) { + for (const widget of nodeInfo.widgets) { + if (widget.name && (widget.name.toLowerCase().includes("image") || widget.name.toLowerCase().includes("filename") || widget.name.toLowerCase().includes("file"))) { + return widget.value; + } + } + } + if (nodeInfo.properties && nodeInfo.properties.img) { + const img = nodeInfo.properties.img; + return { + width: img.width || "unknown", + height: img.height || "unknown", + src: img.src ? img.src.split(/[\/\\]/).pop() : "Preview available" + }; + } + if (nodeInfo.outputs) { + for (const output of nodeInfo.outputs) { + if (output.links && output.links.length > 0) { + return "Image connected to " + output.links.length + " node(s)"; + } + } + } + return "Image node"; + } + return null; + } + /** + * Get text box content from a node. + * @param nodeInfo + * @returns + */ + getTextBoxContent(nodeInfo) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + if (nodeInfo.type && nodeInfo.type.includes("CLIPTextEncode")) { + for (const widget of nodeInfo.widgets) { + if (widget.name === "text" && typeof widget.value === "string") { + return widget.value; + } + } + } + for (const widget of nodeInfo.widgets) { + if ((widget.name.toLowerCase().includes("prompt") || widget.name.toLowerCase().includes("conditioning")) && typeof widget.value === "string" && widget.value.length > 0) { + return widget.value; + } + } + for (const widget of nodeInfo.widgets) { + if ((widget.type === "text" || widget.type === "textarea" || widget.type === "string" || widget.name.toLowerCase().includes("text")) && typeof widget.value === "string" && widget.value.length > 0) { + return widget.value; + } + } + } + return null; + } + /** + * Get important node parameters based on node type. + * @param nodeInfo + * @returns + */ + getImportantNodeParameters(nodeInfo) { + const parameters = []; + const nodeType = nodeInfo.type ? nodeInfo.type.toLowerCase() : ""; + const isSaveNode = nodeType.includes("save") && !nodeType.includes("checkpoint") && !nodeType.includes("model") && !nodeType.includes("preview"); + const showAllWidgets = nodeInfo.type && (nodeType.includes("ksampler") || nodeType.includes("sampler") || nodeType.includes("k_samplers") || nodeType.includes("checkpoint") || nodeType.includes("model") || nodeType.includes("lora") || nodeType.includes("controlnet") || nodeType.includes("advanced") || nodeType.includes("detailer") || nodeType.includes("inpaint") || nodeType.includes("upscale") || nodeType.includes("clip") || nodeType.includes("text") || nodeType.includes("encode")) && !isSaveNode; + if (showAllWidgets) { + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + if (widget.name && widget.name !== "") { + const widgetName = widget.name.toLowerCase(); + if (widgetName.includes("title") || widgetName === "node" || widgetName === "id" || widgetName === "type" || widgetName === "mode") { + continue; } + parameters.push({ + label: widget.name, + value: this.formatWidgetValue(widget.value) + }); + } } - - return parameters; + } + return parameters; } - - /** - * Format widget value for display. - * @param {*} value - * @returns {string} - */ - formatWidgetValue(value) { - if (value === null) return "null"; - if (value === undefined) return "undefined"; - if (typeof value === "string") { - return value; // Show full text without truncation - } - if (typeof value === "number") { - return Number.isInteger(value) ? value.toString() : value.toFixed(3); - } - if (typeof value === "boolean") return value.toString(); - if (Array.isArray(value)) { - return `Array(${value.length})`; - } - if (typeof value === "object") { - return "Object"; - } - return String(value); + let importantParams; + if (isSaveNode) { + importantParams = [ + "filename_prefix", + "filename", + "directory", + "path", + "format", + "quality", + "extension" + ]; + } else { + importantParams = [ + "seed", + "steps", + "cfg", + "scale", + "sampler", + "scheduler", + "positive", + "negative", + "width", + "height", + "denoise", + "strength", + "noise", + "count", + "batch", + "size", + "phase", + "color", + "intensity", + // KSampler specific parameters (for other samplers) + "control_after_generate", + "control", + "after", + "generate", + "start_at_step", + "end_at_step", + "start", + "end", + "return_with_leftover_noise", + "leftover", + "noise_return", + // Additional common parameters (but avoid duplicates with specific extractors) + "model", + "vae", + "clip", + "lora", + "checkpoint", + "latent", + "image", + "mask", + "filename", + "directory", + "prompt", + "conditioning", + "filename_prefix", + // New detection vocabulary + "resolution", + "num_chunks", + "seconds", + "aspect_ratio", + "style_type", + "background", + "n", + "human", + "raw", + "guidance", + "skip_preprocessing", + "movement_amplitude", + "animation", + "material_type", + "b1", + "b2", + "s1", + "s2", + "type", + "channel", + "sigma", + "rho", + // Additional detection vocabulary + "alpha", + "base_shift", + "shift", + "stretch", + "terminal", + "spacing", + "style", + "eta", + "norm_threshold", + "momentum", + "hypernetwork_name", + "reuse_threshold", + "verbose", + "layers", + "set_cond_area", + "audioui", + // Camera and 3D parameters + "camera_pose", + "fx", + "cx", + "fy", + "cy" + // Note: 'text' and 'string' removed to avoid duplication with getTextBoxContent + ]; } - - /** - * Inject CSS styles. - */ - injectStyles() { - if (!document.getElementById('magnify-info-panel-styles-v2')) { - const style = document.createElement('style'); - style.id = 'magnify-info-panel-styles-v2'; - style.textContent = getInfoPanelCSS(); - document.head.appendChild(style); + if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { + for (const widget of nodeInfo.widgets) { + const paramName = widget.name.toLowerCase(); + if (importantParams.some((param) => paramName.includes(param))) { + parameters.push({ + label: widget.name, + value: this.formatWidgetValue(widget.value) + }); } + } } - - /** - * Cleanup DOM elements. - */ - cleanup() { - if (this.elements.panel) { - this.elements.panel.remove(); - } - - if (this.elements.controls) { - this.elements.controls.remove(); - } - - const styles = document.getElementById('magnify-info-panel-styles-v2'); - if (styles) { - styles.remove(); - } + return parameters; + } + /** + * Format widget value for display. + * @param value + * @returns + */ + formatWidgetValue(value) { + if (value === null) return "null"; + if (value === void 0) return "undefined"; + if (typeof value === "string") { + return value; } + if (typeof value === "number") { + return Number.isInteger(value) ? value.toString() : value.toFixed(3); + } + if (typeof value === "boolean") return value.toString(); + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + if (typeof value === "object") { + return "Object"; + } + return String(value); + } + /** + * Inject CSS styles. + */ + injectStyles() { + if (!document.getElementById("magnify-info-panel-styles-v2")) { + const style = document.createElement("style"); + style.id = "magnify-info-panel-styles-v2"; + style.textContent = getInfoPanelCSS(); + document.head.appendChild(style); + } + } + cleanup() { + if (this.elements.panel && this.elements.panel.parentNode) { + this.elements.panel.parentNode.removeChild(this.elements.panel); + } + if (this.elements.controls && this.elements.controls.parentNode) { + this.elements.controls.parentNode.removeChild(this.elements.controls); + } + } } +export { + UIManager +}; +//# sourceMappingURL=UIManager.js.map diff --git a/web/info-panel/UIManager.js.map b/web/info-panel/UIManager.js.map new file mode 100644 index 0000000..5b697af --- /dev/null +++ b/web/info-panel/UIManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"UIManager.js","sources":["../../src/info-panel/UIManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel UI Manager (TypeScript)\n * \n * Complete UI Manager extracted from magnify_info_panel.js\n * Handles all DOM manipulation and UI creation for the info panel.\n */\n\n// import { Z_INDEX } from '../shared/constants'; // Not used in original code? Kept for reference if needed\nimport { getInfoPanelCSS } from './styles';\nimport { StateManager } from './StateManager';\n\ninterface InfoPanelElements {\n panel: HTMLDivElement | null;\n header: HTMLDivElement | null;\n content: HTMLDivElement | null;\n controls: HTMLDivElement | null;\n}\n\n/**\n * UI Manager class.\n * Handles all DOM manipulation and UI creation.\n */\nexport class UIManager {\n stateManager: StateManager;\n elements: InfoPanelElements;\n\n constructor(stateManager: StateManager) {\n this.stateManager = stateManager;\n this.elements = {\n panel: null,\n header: null,\n content: null,\n controls: null\n };\n\n this.createPanel();\n this.injectStyles();\n }\n\n /**\n * Create the main panel and its components.\n */\n createPanel(): void {\n // Main panel container\n this.elements.panel = document.createElement(\"div\");\n this.elements.panel.id = \"comfyui-magnify-info-panel-pro-v2\";\n this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`;\n\n // Header\n this.elements.header = document.createElement(\"div\");\n this.elements.header.className = \"panel-header\";\n this.elements.header.innerHTML = `\n
\n
🔍
\n
Inspector
\n
Real-time analysis
\n
\n
\n \n
\n `;\n\n // Content container\n this.elements.content = document.createElement(\"div\");\n this.elements.content.className = \"panel-content\";\n\n this.elements.panel.appendChild(this.elements.header);\n this.elements.panel.appendChild(this.elements.content);\n\n this.applyStyles();\n document.body.appendChild(this.elements.panel);\n\n // Create floating controls after panel is in DOM\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.ShowHoveringControls\"]) {\n this.createFloatingControls();\n // Update control states immediately after creation\n this.updateControlStates();\n }\n }\n\n /**\n * Create floating control buttons.\n */\n createFloatingControls(): void {\n this.elements.controls = document.createElement(\"div\");\n this.elements.controls.className = \"floating-controls vertical-layout\"; // Default to vertical\n this.elements.controls.innerHTML = `\n \n \n \n \n `;\n\n // Insert before the panel in the document body, not as a child\n document.body.appendChild(this.elements.controls);\n\n // Set initial layout based on settings\n const controlsPosition = this.stateManager.state.settings[\"🔍MagnifyGlass.ControlsPosition\"] || \"top-right\";\n this.updateControlsLayout(controlsPosition);\n }\n\n /**\n * Update control button states.\n */\n updateControlStates(): void {\n if (!this.elements.controls) return;\n\n const pinBtn = this.elements.controls.querySelector('[data-action=\"pin\"]') as HTMLButtonElement;\n const lockBtn = this.elements.controls.querySelector('[data-action=\"lock\"]') as HTMLButtonElement;\n const visibilityBtn = this.elements.controls.querySelector('[data-action=\"toggle-panel\"]') as HTMLButtonElement;\n const glassBtn = this.elements.controls.querySelector('[data-action=\"toggle-glass\"]') as HTMLButtonElement;\n\n if (pinBtn) {\n pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned);\n pinBtn.title = this.stateManager.state.isPanelPinned ? \"Lock Panel\" : \"Unlock Panel\";\n pinBtn.textContent = this.stateManager.state.isPanelPinned ? \"🔒\" : \"🔓\";\n }\n\n if (lockBtn) {\n // Only show/enable lock button when pinned\n lockBtn.style.display = this.stateManager.state.isPanelPinned ? 'flex' : 'none';\n lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked);\n lockBtn.title = this.stateManager.state.isPanelLocked ? \"Upin Panel Position\" : \"Pin Panel Position\";\n lockBtn.disabled = !this.stateManager.state.isPanelPinned;\n }\n\n if (visibilityBtn) {\n visibilityBtn.classList.toggle('active', this.stateManager.state.isPanelVisible);\n visibilityBtn.title = this.stateManager.state.isPanelVisible ? \"Show Panel\" : \"Hide Panel\";\n }\n\n if (glassBtn) {\n glassBtn.classList.toggle('active', this.stateManager.state.isGlassPreviewVisible);\n glassBtn.title = this.stateManager.state.isGlassPreviewVisible ? \"Hide Glass Preview\" : \"Show Glass Preview\";\n }\n }\n\n /**\n * Update controls layout based on position setting.\n * @param position \n */\n updateControlsLayout(position: string): void {\n if (!this.elements.controls) return;\n\n // Remove existing layout classes\n this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout');\n\n // Add appropriate layout class\n if (['top', 'bottom'].includes(position)) {\n this.elements.controls.classList.add('horizontal-layout');\n } else {\n this.elements.controls.classList.add('vertical-layout');\n }\n }\n\n /**\n * Apply current styles to elements.\n */\n applyStyles(): void {\n if (!this.elements.panel) return;\n\n const settings = this.stateManager.state.settings;\n\n // Apply custom text color via CSS variable if provided\n const textColor = settings[\"🔍MagnifyGlass.InfoPanelTextColor\"];\n if (textColor) {\n // Ensure color has # symbol\n const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`;\n this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor);\n }\n\n // Apply custom accent color via CSS variable if provided\n const accentColor = settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"];\n if (accentColor) {\n // Ensure color has # symbol\n const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`;\n this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor);\n }\n\n // Apply opacity setting if panel is visible (convert percentage to decimal)\n if (this.stateManager.state.isPanelVisible) {\n const opacityPercent = settings[\"🔍MagnifyGlass.InfoPanelOpacity\"];\n this.elements.panel.style.opacity = (opacityPercent / 100).toString();\n }\n\n this.elements.panel.style.cssText = `\n position: absolute;\n width: ${settings[\"🔍MagnifyGlass.InfoPanelWidth\"]}px;\n max-height: ${settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"]}px;\n z-index: 99999;\n display: none;\n opacity: 0;\n transform: translateY(-10px);\n transition: ${settings[\"🔍MagnifyGlass.InfoPanelAnimations\"] ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none'};\n pointer-events: auto;\n user-select: none;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n ${textColor ? `--info-panel-text-color: ${textColor.startsWith('#') ? textColor : `#${textColor}`};` : ''}\n ${accentColor ? `--info-panel-accent-color: ${accentColor.startsWith('#') ? accentColor : `#${accentColor}`};` : ''}\n `;\n }\n\n /**\n * Show the panel.\n */\n show(): void {\n if (!this.elements.panel) return;\n\n if (this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"]) {\n this.elements.panel.style.display = \"block\";\n if (this.elements.controls) {\n this.elements.controls.style.display = \"flex\";\n }\n // Apply user's opacity setting when showing (convert percentage to decimal)\n const opacityPercent = this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"];\n this.elements.panel.style.opacity = (opacityPercent / 100).toString();\n // Trigger reflow\n this.elements.panel.offsetHeight;\n this.elements.panel.classList.add('visible');\n this.stateManager.state.isPanelVisible = true;\n }\n }\n\n /**\n * Hide the panel.\n */\n hide(): void {\n if (!this.elements.panel) return;\n\n this.elements.panel.classList.remove('visible');\n setTimeout(() => {\n if (!this.stateManager.state.isPanelVisible && this.elements.panel) {\n this.elements.panel.style.display = \"none\";\n if (this.elements.controls) {\n this.elements.controls.style.display = \"none\";\n }\n }\n }, this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"] ? 300 : 0);\n this.stateManager.state.isPanelVisible = false;\n }\n\n /**\n * Update minimized state.\n */\n updateMinimizedState(): void {\n if (!this.elements.panel || !this.elements.header) return;\n\n this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized);\n const minimizeBtn = this.elements.header.querySelector('.minimize-btn');\n if (minimizeBtn) {\n minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−';\n (minimizeBtn as HTMLElement).title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel';\n }\n }\n\n /**\n * Update pinned state.\n */\n updatePinnedState(): void {\n if (!this.elements.panel) return;\n\n this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned);\n this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked);\n this.updateControlStates();\n }\n\n /**\n * Update theme class.\n * @param newTheme \n */\n updateTheme(newTheme: string): void {\n if (this.elements.panel) {\n this.elements.panel.className = this.elements.panel.className.replace(/theme-\\w+/, `theme-${newTheme.toLowerCase()}`);\n }\n }\n\n /**\n * Display information in the panel.\n * @param info \n */\n displayInfo(info: any): void {\n const sections = this.buildSections(info);\n this.renderSections(sections);\n this.updateSectionStates();\n this.updateHeaderSubtitle(info);\n }\n\n /**\n * Build section data from info object.\n * @param info \n * @returns \n */\n buildSections(info: any): any[] {\n const sections: any[] = [];\n const settings = this.stateManager.state.settings;\n\n // Inspector section\n if (settings[\"🔍MagnifyGlass.ShowInspectorTab\"]) {\n const inspectorContent = [\n { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` },\n { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` },\n { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` }\n ];\n\n sections.push({\n id: 'inspector',\n icon: '🔍',\n title: 'Inspector',\n content: inspectorContent\n });\n }\n\n // Media section\n if (info.media) {\n const mediaContent = [\n { label: 'Type', value: info.media.tagName },\n { label: 'Source', value: info.media.src }\n ];\n\n if (info.media.naturalSize) {\n mediaContent.push({ label: 'Natural', value: info.media.naturalSize });\n }\n\n sections.push({\n id: 'media',\n icon: '📷',\n title: 'Media',\n badge: info.media.tagName,\n content: mediaContent\n });\n }\n\n // Node Details section\n if (info.hoveredNode) {\n const nodeContent = [\n { label: 'Title', value: info.hoveredNode.title }\n ];\n\n // Check if this is a complex node that shows all widgets\n const nodeType = info.hoveredNode.type ? info.hoveredNode.type.toLowerCase() : '';\n const isSaveNode = nodeType.includes('save') &&\n !nodeType.includes('checkpoint') &&\n !nodeType.includes('model') &&\n !nodeType.includes('preview');\n\n const showAllWidgets = info.hoveredNode.type && (\n nodeType.includes('ksampler') ||\n nodeType.includes('sampler') ||\n nodeType.includes('k_samplers') ||\n nodeType.includes('checkpoint') ||\n nodeType.includes('model') ||\n nodeType.includes('lora') ||\n nodeType.includes('controlnet') ||\n nodeType.includes('advanced') ||\n nodeType.includes('detailer') ||\n nodeType.includes('inpaint') ||\n nodeType.includes('upscale') ||\n nodeType.includes('clip') ||\n nodeType.includes('text') ||\n nodeType.includes('encode')\n ) && !isSaveNode;\n\n if (!showAllWidgets) {\n // Add specific extractions for simple nodes\n const checkpointInfo = this.getCheckpointInfo(info.hoveredNode);\n if (checkpointInfo) {\n nodeContent.push({ label: 'Model', value: checkpointInfo });\n }\n\n const imageInfo = this.getImageInfo(info.hoveredNode);\n if (imageInfo) {\n if (typeof imageInfo === 'string') {\n nodeContent.push({ label: 'Image', value: imageInfo });\n } else if (typeof imageInfo === 'object' && imageInfo !== null) {\n const imgInfoAny = imageInfo as any;\n nodeContent.push({ label: 'Image Size', value: `${imgInfoAny.width}×${imgInfoAny.height}` });\n if (imgInfoAny.src) {\n nodeContent.push({ label: 'Image Source', value: imgInfoAny.src });\n }\n }\n }\n\n const textBoxContent = this.getTextBoxContent(info.hoveredNode);\n if (textBoxContent) {\n nodeContent.push({ label: 'Text', value: textBoxContent });\n }\n }\n\n const importantParameters = this.getImportantNodeParameters(info.hoveredNode);\n nodeContent.push(...importantParameters);\n\n sections.push({\n id: 'node',\n icon: '🎯',\n title: 'Node',\n badge: info.hoveredNode.type,\n content: nodeContent\n });\n }\n\n return sections;\n }\n\n /**\n * Render sections to the panel.\n * @param sections \n */\n renderSections(sections: any[]): void {\n if (!this.elements.content) return;\n\n if (sections.length === 0) {\n this.elements.content.innerHTML = `\n
\n
📍
\n
Empty canvas area
\n
\n `;\n return;\n }\n\n this.elements.content.innerHTML = sections.map(section => `\n
\n
\n ${section.icon}\n ${section.title}\n ${section.badge ? `${section.badge}` : ''}\n ${section.id !== 'node' ? '▶' : ''}\n
\n
\n
\n ${section.content.map((item: any) => {\n const value = this.formatValue(item.value, item.label);\n const valueClass = this.getValueClass(item.value);\n const valueAttributes = this.getValueAttributes(item.value);\n return `\n
\n ${item.label}\n ${value}\n
`;\n }).join('')}\n
\n
\n
\n `).join('');\n }\n\n /**\n * Update section expansion states.\n */\n updateSectionStates(): void {\n if (!this.elements.content) return;\n\n const sections = this.elements.content.querySelectorAll('.info-section');\n sections.forEach(section => {\n const sectionId = (section as HTMLElement).dataset.section;\n if (!sectionId) return;\n\n const header = section.querySelector('.section-header');\n const content = section.querySelector('.section-content');\n\n if (header && content) {\n if (this.stateManager.state.expandedSections.has(sectionId)) {\n header.classList.add('expanded');\n content.classList.add('expanded');\n } else {\n header.classList.remove('expanded');\n content.classList.remove('expanded');\n }\n }\n });\n }\n\n /**\n * Format a value for display.\n * @param value \n * @param label\n * @returns \n */\n formatValue(value: any, label?: string): string {\n if (value === null || value === undefined) return '';\n\n const str = String(value);\n\n // Show full text for prompts, text content, model names, and file paths\n if (label && (\n label.toLowerCase().includes('text') ||\n label.toLowerCase().includes('prompt') ||\n label.toLowerCase().includes('model') ||\n label.toLowerCase().includes('file') ||\n label.toLowerCase().includes('conditioning') ||\n label.toLowerCase().includes('positive') ||\n label.toLowerCase().includes('negative')\n )) {\n return str;\n }\n\n // Show full text for very long values (no truncation)\n return str;\n }\n\n /**\n * Get value class for styling.\n * @param value \n * @returns \n */\n getValueClass(value: any): string {\n if (!value) return '';\n\n const str = String(value);\n let classes: string[] = [];\n\n // Mark text that might benefit from special styling for readability\n if (str.length > 100) {\n classes.push('long-text');\n }\n\n return classes.join(' ');\n }\n\n /**\n * Get value attributes.\n * @param value \n * @returns \n */\n getValueAttributes(value: any): string {\n // Since we show full text now, we don't need title attributes for long text\n // Only add title for very long text that might benefit from tooltips\n if (!value) return '';\n\n const str = String(value);\n if (str.length > 500) { // Only for extremely long text\n return `title=\"${str.replace(/\"/g, '"')}\"`;\n }\n\n return '';\n }\n\n /**\n * Update header subtitle.\n * @param info \n */\n updateHeaderSubtitle(info: any): void {\n if (!this.elements.header) return;\n\n const subtitleElement = this.elements.header.querySelector('.header-subtitle') as HTMLElement;\n if (subtitleElement) {\n const accentColor = this.stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"];\n if (info.hoveredNode) {\n subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`;\n subtitleElement.style.color = accentColor || '';\n } else if (info.media) {\n subtitleElement.textContent = `Media: ${info.media.tagName}`;\n subtitleElement.style.color = accentColor || '';\n } else if (info.connection) {\n subtitleElement.textContent = `Connection: ${info.connection.type}`;\n subtitleElement.style.color = accentColor || '';\n } else {\n subtitleElement.textContent = 'Real-time analysis';\n subtitleElement.style.color = '';\n }\n }\n }\n\n // ============== Helper methods for node information extraction ==============\n\n /**\n * Get checkpoint/model info from a node.\n * @param nodeInfo \n * @returns \n */\n getCheckpointInfo(nodeInfo: any): string | null {\n if (nodeInfo.type && (\n nodeInfo.type.includes(\"CheckpointLoader\") ||\n nodeInfo.type.includes(\"LoadCheckpoint\") ||\n nodeInfo.type.includes(\"ModelLoader\") ||\n nodeInfo.type.includes(\"UNETLoader\") ||\n nodeInfo.type.includes(\"VAELoader\") ||\n nodeInfo.type.includes(\"LoraLoader\")\n )) {\n if (nodeInfo.widgets && nodeInfo.widgets.length > 0) {\n for (const widget of nodeInfo.widgets) {\n if (widget.name && (\n widget.name.toLowerCase().includes(\"model\") ||\n widget.name.toLowerCase().includes(\"checkpoint\") ||\n widget.name.toLowerCase().includes(\"ckpt\") ||\n widget.name.toLowerCase().includes(\"lora\") ||\n widget.name.toLowerCase().includes(\"vae\") ||\n widget.name.toLowerCase().includes(\"file\")\n )) {\n const value = String(widget.value);\n const filename = value.split(/[\\/\\\\]/).pop();\n return filename || value;\n }\n }\n }\n }\n return null;\n }\n\n /**\n * Get image info from a node.\n * @param nodeInfo \n * @returns \n */\n getImageInfo(nodeInfo: any): string | object | null {\n if (nodeInfo.type && (\n nodeInfo.type.includes(\"SaveImage\") ||\n nodeInfo.type.includes(\"PreviewImage\") ||\n nodeInfo.type.includes(\"VisionOutput\") ||\n nodeInfo.type.includes(\"ImageOutput\") ||\n nodeInfo.type.includes(\"LoadImage\") ||\n nodeInfo.type.includes(\"Display\")\n )) {\n if (nodeInfo.widgets) {\n for (const widget of nodeInfo.widgets) {\n if (widget.name && (\n widget.name.toLowerCase().includes(\"image\") ||\n widget.name.toLowerCase().includes(\"filename\") ||\n widget.name.toLowerCase().includes(\"file\")\n )) {\n return widget.value;\n }\n }\n }\n\n if (nodeInfo.properties && nodeInfo.properties.img) {\n const img = nodeInfo.properties.img;\n return {\n width: img.width || \"unknown\",\n height: img.height || \"unknown\",\n src: img.src ? img.src.split(/[\\/\\\\]/).pop() : \"Preview available\"\n };\n }\n\n if (nodeInfo.outputs) {\n for (const output of nodeInfo.outputs) {\n if (output.links && output.links.length > 0) {\n return \"Image connected to \" + output.links.length + \" node(s)\";\n }\n }\n }\n\n return \"Image node\";\n }\n return null;\n }\n\n /**\n * Get text box content from a node.\n * @param nodeInfo \n * @returns \n */\n getTextBoxContent(nodeInfo: any): string | null {\n if (nodeInfo.widgets && nodeInfo.widgets.length > 0) {\n if (nodeInfo.type && nodeInfo.type.includes(\"CLIPTextEncode\")) {\n for (const widget of nodeInfo.widgets) {\n if (widget.name === \"text\" && typeof widget.value === 'string') {\n return widget.value;\n }\n }\n }\n\n for (const widget of nodeInfo.widgets) {\n if ((widget.name.toLowerCase().includes(\"prompt\") ||\n widget.name.toLowerCase().includes(\"conditioning\")) &&\n typeof widget.value === 'string' && widget.value.length > 0) {\n return widget.value;\n }\n }\n\n for (const widget of nodeInfo.widgets) {\n if ((widget.type === 'text' || widget.type === 'textarea' ||\n widget.type === 'string' || widget.name.toLowerCase().includes('text')) &&\n typeof widget.value === 'string' && widget.value.length > 0) {\n\n // Show full text without truncation\n return widget.value;\n }\n }\n }\n return null;\n }\n\n /**\n * Get important node parameters based on node type.\n * @param nodeInfo \n * @returns \n */\n getImportantNodeParameters(nodeInfo: any): any[] {\n const parameters: any[] = [];\n\n // For complex nodes that have many parameters, show ALL widgets\n const nodeType = nodeInfo.type ? nodeInfo.type.toLowerCase() : '';\n const isSaveNode = nodeType.includes('save') &&\n !nodeType.includes('checkpoint') &&\n !nodeType.includes('model') &&\n !nodeType.includes('preview');\n\n const showAllWidgets = nodeInfo.type && (\n nodeType.includes('ksampler') ||\n nodeType.includes('sampler') ||\n nodeType.includes('k_samplers') ||\n nodeType.includes('checkpoint') ||\n nodeType.includes('model') ||\n nodeType.includes('lora') ||\n nodeType.includes('controlnet') ||\n nodeType.includes('advanced') ||\n nodeType.includes('detailer') ||\n nodeType.includes('inpaint') ||\n nodeType.includes('upscale') ||\n nodeType.includes('clip') ||\n nodeType.includes('text') ||\n nodeType.includes('encode')\n ) && !isSaveNode;\n\n if (showAllWidgets) {\n if (nodeInfo.widgets && nodeInfo.widgets.length > 0) {\n for (const widget of nodeInfo.widgets) {\n // Show all widgets for complex nodes, but filter out some duplicates\n if (widget.name && widget.name !== '') {\n const widgetName = widget.name.toLowerCase();\n\n // Skip some generic widget names that might be duplicates\n if (widgetName.includes('title') ||\n widgetName === 'node' ||\n widgetName === 'id' ||\n widgetName === 'type' ||\n widgetName === 'mode') {\n continue;\n }\n\n parameters.push({\n label: widget.name,\n value: this.formatWidgetValue(widget.value)\n });\n }\n }\n }\n return parameters;\n }\n\n // Special handling for Save nodes - only show essential parameters\n let importantParams: string[];\n if (isSaveNode) {\n importantParams = [\n 'filename_prefix', 'filename', 'directory', 'path',\n 'format', 'quality', 'extension'\n ];\n } else {\n // For other nodes, use the filtered list but avoid duplicates with specific extractions\n importantParams = [\n 'seed', 'steps', 'cfg', 'scale', 'sampler', 'scheduler',\n 'positive', 'negative', 'width', 'height', 'denoise', 'strength',\n 'noise', 'count', 'batch', 'size', 'phase', 'color', 'intensity',\n // KSampler specific parameters (for other samplers)\n 'control_after_generate', 'control', 'after', 'generate',\n 'start_at_step', 'end_at_step', 'start', 'end',\n 'return_with_leftover_noise', 'leftover', 'noise_return',\n // Additional common parameters (but avoid duplicates with specific extractors)\n 'model', 'vae', 'clip', 'lora', 'checkpoint',\n 'latent', 'image', 'mask', 'filename', 'directory',\n 'prompt', 'conditioning', 'filename_prefix',\n // New detection vocabulary\n 'resolution', 'num_chunks', 'seconds', 'aspect_ratio',\n 'style_type', 'background', 'n', 'human', 'raw', 'guidance',\n 'skip_preprocessing', 'movement_amplitude', 'animation',\n 'material_type', 'b1', 'b2', 's1', 's2', 'type', 'channel',\n 'sigma', 'rho',\n // Additional detection vocabulary\n 'alpha', 'base_shift', 'shift', 'stretch', 'terminal',\n 'spacing', 'style', 'eta', 'norm_threshold', 'momentum',\n 'hypernetwork_name', 'reuse_threshold', 'verbose', 'layers',\n 'set_cond_area', 'audioui',\n // Camera and 3D parameters\n 'camera_pose', 'fx', 'cx', 'fy', 'cy'\n // Note: 'text' and 'string' removed to avoid duplication with getTextBoxContent\n ];\n }\n\n if (nodeInfo.widgets && nodeInfo.widgets.length > 0) {\n for (const widget of nodeInfo.widgets) {\n const paramName = widget.name.toLowerCase();\n if (importantParams.some(param => paramName.includes(param))) {\n parameters.push({\n label: widget.name,\n value: this.formatWidgetValue(widget.value)\n });\n }\n }\n }\n\n return parameters;\n }\n\n /**\n * Format widget value for display.\n * @param value \n * @returns \n */\n formatWidgetValue(value: any): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"string\") {\n return value; // Show full text without truncation\n }\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? value.toString() : value.toFixed(3);\n }\n if (typeof value === \"boolean\") return value.toString();\n if (Array.isArray(value)) {\n return `Array(${value.length})`;\n }\n if (typeof value === \"object\") {\n return \"Object\";\n }\n return String(value);\n }\n\n /**\n * Inject CSS styles.\n */\n injectStyles(): void {\n if (!document.getElementById('magnify-info-panel-styles-v2')) {\n const style = document.createElement('style');\n style.id = 'magnify-info-panel-styles-v2';\n style.textContent = getInfoPanelCSS();\n document.head.appendChild(style);\n }\n }\n\n cleanup(): void {\n if (this.elements.panel && this.elements.panel.parentNode) {\n this.elements.panel.parentNode.removeChild(this.elements.panel);\n }\n if (this.elements.controls && this.elements.controls.parentNode) {\n this.elements.controls.parentNode.removeChild(this.elements.controls);\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/info-panel/components/InfoPanel.vue.js b/web/info-panel/components/InfoPanel.vue.js new file mode 100644 index 0000000..5c81c65 --- /dev/null +++ b/web/info-panel/components/InfoPanel.vue.js @@ -0,0 +1,56 @@ +import _sfc_main from "./InfoPanel.vue2.js"; +import "../../node_modules/.pnpm/vue@3.5.26_typescript@5.9.3/node_modules/vue/dist/vue.runtime.esm-bundler.js"; +/* empty css */ +import _export_sfc from "../../_virtual/_plugin-vue_export-helper.js"; +import { createElementBlock, createCommentVNode, createElementVNode as createBaseVNode, openBlock } from "../../node_modules/.pnpm/@vue_runtime-core@3.5.26/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js"; +import { normalizeClass, toDisplayString, normalizeStyle } from "../../node_modules/.pnpm/@vue_shared@3.5.26/node_modules/@vue/shared/dist/shared.esm-bundler.js"; +const _hoisted_1 = { class: "mag-panel-header" }; +const _hoisted_2 = { class: "mag-panel-controls" }; +const _hoisted_3 = { class: "mag-panel-content" }; +const _hoisted_4 = { + key: 0, + class: "mag-node-info" +}; +const _hoisted_5 = { class: "mag-property-grid" }; +const _hoisted_6 = { class: "mag-prop-row" }; +const _hoisted_7 = { class: "mag-prop-value" }; +const _hoisted_8 = { + key: 1, + class: "mag-empty-state" +}; +function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { + return _ctx.isVisible ? (openBlock(), createElementBlock("div", { + key: 0, + class: normalizeClass(["mag-info-panel", _ctx.themeClass]), + style: normalizeStyle(_ctx.panelStyle) + }, [ + createBaseVNode("div", _hoisted_1, [ + _cache[2] || (_cache[2] = createBaseVNode("div", { class: "mag-panel-title" }, "Magnify Info", -1)), + createBaseVNode("div", _hoisted_2, [ + createBaseVNode("button", { + onClick: _cache[0] || (_cache[0] = (...args) => _ctx.togglePin && _ctx.togglePin(...args)), + class: normalizeClass({ active: _ctx.isPinned }) + }, "📌", 2), + createBaseVNode("button", { + onClick: _cache[1] || (_cache[1] = (...args) => _ctx.close && _ctx.close(...args)) + }, "✕") + ]) + ]), + createBaseVNode("div", _hoisted_3, [ + _ctx.nodeInfo ? (openBlock(), createElementBlock("div", _hoisted_4, [ + createBaseVNode("h3", null, toDisplayString(_ctx.nodeInfo.title || _ctx.nodeInfo.type), 1), + createBaseVNode("div", _hoisted_5, [ + createBaseVNode("div", _hoisted_6, [ + _cache[3] || (_cache[3] = createBaseVNode("span", { class: "mag-prop-label" }, "ID:", -1)), + createBaseVNode("span", _hoisted_7, toDisplayString(_ctx.nodeInfo.id), 1) + ]) + ]) + ])) : (openBlock(), createElementBlock("div", _hoisted_8, " Hover over a node to see details ")) + ]) + ], 6)) : createCommentVNode("", true); +} +const InfoPanelComponent = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-687c6e82"]]); +export { + InfoPanelComponent as default +}; +//# sourceMappingURL=InfoPanel.vue.js.map diff --git a/web/info-panel/components/InfoPanel.vue.js.map b/web/info-panel/components/InfoPanel.vue.js.map new file mode 100644 index 0000000..fcc6b16 --- /dev/null +++ b/web/info-panel/components/InfoPanel.vue.js.map @@ -0,0 +1 @@ +{"version":3,"file":"InfoPanel.vue.js","sources":["../../../src/info-panel/components/InfoPanel.vue"],"sourcesContent":["\n\n\n\n\n"],"names":["_openBlock","_createElementBlock","_normalizeClass","_normalizeStyle","_createElementVNode","_toDisplayString"],"mappings":";;;;;;AAES,MAAA,aAAA,EAAA,OAAM,mBAAA;AAEJ,MAAA,aAAA,EAAA,OAAM,qBAAA;AAMR,MAAA,aAAA,EAAA,OAAM,oBAAA;;;EACY,OAAM;;AAEpB,MAAA,aAAA,EAAA,OAAM,oBAAA;AACH,MAAA,aAAA,EAAA,OAAM,eAAA;AAEH,MAAA,aAAA,EAAA,OAAM,iBAAA;;;EAKP,OAAM;;;AApBX,SAAA,KAAA,aAAAA,UAAA,GAAXC,mBAwBM,OAAA;AAAA,IAAA,KAAA;AAAA,IAxBgB,OAAKC,eAAA,CAAC,kBAAyB,KAAA,UAAU,CAAA;AAAA,IAAG,OAAKC,eAAE,KAAA,UAAU;AAAA,EAAA,GAAA;AAAA,IACjFC,gBAMM,OANN,YAMM;AAAA,MAAA,OAAA,CAAA,MAAA,OAAA,CAAA,IALJA,gBAA+C,OAAA,EAA1C,OAAM,kBAAA,GAAkB,gBAAY,EAAA;AAAA,MACzCA,gBAGM,OAHN,YAGM;AAAA,QAFJA,gBAAoE,UAAA;AAAA,UAA3D,SAAK,OAAA,CAAA,MAAA,OAAA,CAAA,IAAA,IAAA,SAAE,KAAA,aAAA,KAAA,UAAA,GAAA,IAAA;AAAA,UAAY,OAAKF,yBAAY,KAAA,SAAA,CAAQ;AAAA,QAAA,GAAI,MAAE,CAAA;AAAA,QAC3DE,gBAAiC,UAAA;AAAA,UAAxB,SAAK,OAAA,CAAA,MAAA,OAAA,CAAA,IAAA,IAAA,SAAE,KAAA,SAAA,KAAA,MAAA,GAAA,IAAA;AAAA,QAAA,GAAO,GAAC;AAAA,MAAA,CAAA;AAAA;IAI5BA,gBAcM,OAdN,YAcM;AAAA,MAbO,KAAA,YAAAJ,UAAA,GAAXC,mBASM,OATN,YASM;AAAA,QARJG,gBAA8C,MAAA,MAAAC,gBAAvC,KAAA,SAAS,SAAS,cAAS,IAAI,GAAA,CAAA;AAAA,QACtCD,gBAMM,OANN,YAMM;AAAA,UALHA,gBAGM,OAHN,YAGM;AAAA,YAAA,OAAA,CAAA,MAAA,OAAA,CAAA,IAFJA,gBAAuC,QAAA,EAAjC,OAAM,iBAAA,GAAiB,OAAG,EAAA;AAAA,YAChCA,gBAAqD,QAArD,YAAqDC,gBAArB,cAAS,EAAE,GAAA,CAAA;AAAA,UAAA,CAAA;AAAA;0BAKlDJ,mBAEM,OAFN,YAAoC,oCAEpC;AAAA,IAAA,CAAA;AAAA;;;"} \ No newline at end of file diff --git a/web/info-panel/components/InfoPanel.vue2.js b/web/info-panel/components/InfoPanel.vue2.js new file mode 100644 index 0000000..604af2d --- /dev/null +++ b/web/info-panel/components/InfoPanel.vue2.js @@ -0,0 +1,59 @@ +import "../../node_modules/.pnpm/vue@3.5.26_typescript@5.9.3/node_modules/vue/dist/vue.runtime.esm-bundler.js"; +import { defineComponent, computed } from "../../node_modules/.pnpm/@vue_runtime-core@3.5.26/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js"; +import { toRefs } from "../../node_modules/.pnpm/@vue_reactivity@3.5.26/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js"; +const _sfc_main = defineComponent({ + name: "InfoPanel", + props: { + state: { + type: Object, + required: true + } + }, + setup(props) { + const { state } = toRefs(props); + const isVisible = computed(() => { + var _a; + return ((_a = state.value) == null ? void 0 : _a.visible) ?? false; + }); + const isPinned = computed(() => { + var _a; + return ((_a = state.value) == null ? void 0 : _a.pinned) ?? false; + }); + const nodeInfo = computed(() => { + var _a; + return ((_a = state.value) == null ? void 0 : _a.hoveredNode) ?? null; + }); + const theme = computed(() => { + var _a; + return ((_a = state.value) == null ? void 0 : _a.theme) ?? "dark"; + }); + const themeClass = computed(() => `theme-${theme.value}`); + const panelStyle = computed(() => ({ + width: "300px", + opacity: 1 + })); + const togglePin = () => { + if (state.value) { + state.value.pinned = !state.value.pinned; + } + }; + const close = () => { + if (state.value) { + state.value.visible = false; + } + }; + return { + isVisible, + isPinned, + nodeInfo, + themeClass, + panelStyle, + togglePin, + close + }; + } +}); +export { + _sfc_main as default +}; +//# sourceMappingURL=InfoPanel.vue2.js.map diff --git a/web/info-panel/components/InfoPanel.vue2.js.map b/web/info-panel/components/InfoPanel.vue2.js.map new file mode 100644 index 0000000..3b6f395 --- /dev/null +++ b/web/info-panel/components/InfoPanel.vue2.js.map @@ -0,0 +1 @@ +{"version":3,"file":"InfoPanel.vue2.js","sources":["../../../src/info-panel/components/InfoPanel.vue"],"sourcesContent":["\n\n\n\n\n"],"names":[],"mappings":";;;AA+BA,MAAA,YAAe,gBAAgB;AAAA,EAC7B,MAAM;AAAA,EACN,OAAO;AAAA,IACL,OAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU;AAAA,IAAA;AAAA,EACZ;AAAA,EAEF,MAAM,OAAO;AAEX,UAAM,EAAE,MAAA,IAAU,OAAO,KAAK;AAE9B,UAAM,YAAY,SAAS,MAAA;;AAAM,0BAAM,UAAN,mBAAa,YAAW;AAAA,KAAK;AAC9D,UAAM,WAAW,SAAS,MAAA;;AAAM,0BAAM,UAAN,mBAAa,WAAU;AAAA,KAAK;AAC5D,UAAM,WAAW,SAAS,MAAA;;AAAM,0BAAM,UAAN,mBAAa,gBAAe;AAAA,KAAI;AAChE,UAAM,QAAQ,SAAS,MAAA;;AAAM,0BAAM,UAAN,mBAAa,UAAS;AAAA,KAAM;AAEzD,UAAM,aAAa,SAAS,MAAM,SAAS,MAAM,KAAK,EAAE;AACxD,UAAM,aAAa,SAAS,OAAO;AAAA,MACjC,OAAO;AAAA,MACP,SAAS;AAAA,IAAA,EACT;AAEF,UAAM,YAAY,MAAM;AACtB,UAAI,MAAM,OAAO;AACf,cAAM,MAAM,SAAS,CAAC,MAAM,MAAM;AAAA,MACpC;AAAA,IACF;AAEA,UAAM,QAAQ,MAAM;AAClB,UAAI,MAAM,OAAO;AACf,cAAM,MAAM,UAAU;AAAA,MACxB;AAAA,IACF;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AACF,CAAC;"} \ No newline at end of file diff --git a/web/info-panel/styles.js b/web/info-panel/styles.js index 60716ec..552f1bf 100644 --- a/web/info-panel/styles.js +++ b/web/info-panel/styles.js @@ -1,16 +1,5 @@ -/** - * ComfyUI MagnifyGlass - Info Panel Styles - * - * Complete CSS styles for the info panel component. - * Extracted from magnify_info_panel.js getCSS() method. - */ - -/** - * Get the complete CSS for the info panel. - * @returns {string} CSS string - */ -export function getInfoPanelCSS() { - return ` +function getInfoPanelCSS() { + return ` .magnify-info-panel { border-radius: 12px; backdrop-filter: blur(20px); @@ -573,3 +562,7 @@ export function getInfoPanelCSS() { } `; } +export { + getInfoPanelCSS +}; +//# sourceMappingURL=styles.js.map diff --git a/web/info-panel/styles.js.map b/web/info-panel/styles.js.map new file mode 100644 index 0000000..1e9cc8e --- /dev/null +++ b/web/info-panel/styles.js.map @@ -0,0 +1 @@ +{"version":3,"file":"styles.js","sources":["../../src/info-panel/styles.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Info Panel Styles (TypeScript)\n * \n * Complete CSS styles for the info panel component.\n */\n\n/**\n * Get the complete CSS for the info panel.\n * @returns CSS string\n */\nexport function getInfoPanelCSS(): string {\n return `\n .magnify-info-panel {\n border-radius: 12px;\n backdrop-filter: blur(20px);\n box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1);\n overflow: hidden;\n font-size: 16px;\n line-height: 1.6;\n pointer-events: auto;\n }\n \n .magnify-info-panel * {\n pointer-events: auto;\n }\n \n .magnify-info-panel.theme-dark {\n background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96));\n color: var(--info-panel-text-color, #e0e0e0);\n border: 1px solid rgba(100, 100, 120, 0.4);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2);\n backdrop-filter: blur(12px);\n }\n \n .magnify-info-panel.theme-light {\n background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96));\n color: var(--info-panel-text-color, #2a2a2a);\n border: 1px solid rgba(200, 200, 220, 0.5);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05);\n backdrop-filter: blur(12px);\n }\n \n .magnify-info-panel.visible {\n transform: translateY(0) !important;\n }\n \n .magnify-info-panel.panel-pinned {\n cursor: grab;\n box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3);\n }\n \n .magnify-info-panel.panel-locked {\n cursor: default !important;\n box-shadow: 0 0 0 2px rgba(255, 69, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3);\n }\n \n .magnify-info-panel.panel-locked::after {\n content: '📌';\n position: absolute;\n top: 8px;\n right: 8px;\n font-size: 14px;\n opacity: 0.7;\n pointer-events: none;\n z-index: 1;\n }\n \n .magnify-info-panel.panel-pinned.panel-dragging {\n cursor: grabbing;\n box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(255, 215, 0, 0.7);\n transform: scale(1.02) !important;\n z-index: 100001 !important;\n transition: none !important;\n }\n \n .magnify-info-panel.panel-pinned::before {\n content: '';\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n background: linear-gradient(45deg, rgba(255, 215, 0, 0.05) 0%, transparent 100%);\n pointer-events: none;\n border-radius: inherit;\n }\n \n .panel-dragging * {\n pointer-events: none !important;\n user-select: none !important;\n transition: none !important;\n }\n \n .magnify-info-panel.panel-minimized .panel-content {\n max-height: 0 !important;\n overflow: hidden;\n transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n }\n \n .floating-controls {\n position: fixed;\n display: flex;\n padding: 4px;\n background: rgba(40, 40, 50, 0.95);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 4px;\n opacity: 1;\n z-index: 100000;\n backdrop-filter: blur(8px);\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n \n .floating-controls.vertical-layout {\n flex-direction: column;\n gap: 4px;\n }\n \n .floating-controls.horizontal-layout {\n flex-direction: row;\n gap: 4px;\n }\n \n .control-btn {\n background: rgba(60, 60, 70, 0.8);\n border: 1px solid rgba(255, 255, 255, 0.2);\n width: 28px;\n height: 28px;\n font-size: 14px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: #ddd;\n border-radius: 3px;\n transition: background-color 0.15s ease;\n }\n \n .control-btn:hover {\n background: rgba(80, 80, 90, 0.9);\n color: white;\n }\n \n .control-btn:active {\n background: rgba(100, 100, 110, 0.9);\n }\n \n .control-btn:disabled {\n background: rgba(40, 40, 50, 0.5);\n color: #666;\n cursor: not-allowed;\n opacity: 0.5;\n }\n \n .control-btn.active {\n background: rgba(255, 200, 50, 0.3);\n border-color: rgba(255, 200, 50, 0.6);\n color: #ffcc33;\n }\n \n .control-btn.lock-btn.active {\n background: rgba(255, 69, 0, 0.3);\n border-color: rgba(255, 69, 0, 0.6);\n color: #ff4500;\n }\n \n .panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 14px 18px;\n background: linear-gradient(135deg, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.05));\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n user-select: none;\n }\n \n .header-content {\n display: flex;\n align-items: center;\n gap: 10px;\n }\n \n .header-icon {\n font-size: 18px;\n opacity: 0.9;\n }\n \n .header-title {\n font-weight: 600;\n font-size: 18px;\n }\n \n .header-subtitle {\n font-size: 14px;\n opacity: 0.7;\n font-style: italic;\n }\n \n .header-controls {\n display: flex;\n gap: 6px;\n }\n \n .minimize-btn {\n background: rgba(255, 255, 255, 0.1);\n border: none;\n border-radius: 6px;\n color: inherit;\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: all 0.2s;\n font-weight: bold;\n font-size: 16px;\n }\n \n .minimize-btn:hover {\n background: rgba(255, 255, 255, 0.2);\n transform: scale(1.05);\n }\n \n .minimize-btn:active {\n transform: scale(0.95);\n }\n \n .panel-content {\n padding: 0;\n max-height: calc(100% - 60px);\n overflow-y: auto;\n scrollbar-width: thin;\n }\n \n .panel-content::-webkit-scrollbar {\n width: 8px;\n }\n \n .panel-content::-webkit-scrollbar-track {\n background: rgba(0, 0, 0, 0.1);\n }\n \n .panel-content::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, 0.3);\n border-radius: 3px;\n }\n \n .info-section {\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n margin-bottom: 8px;\n border-radius: 10px;\n overflow: visible;\n background: rgba(255, 255, 255, 0.02);\n transition: all 0.3s ease;\n }\n \n .info-section:hover {\n background: rgba(255, 255, 255, 0.04);\n transform: translateY(-1px);\n }\n \n .section-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 16px 20px;\n cursor: pointer;\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n font-weight: 500;\n border-radius: 8px;\n margin: 3px;\n position: relative;\n overflow: hidden;\n }\n \n .section-header::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.05) 50%, transparent 100%);\n transform: translateX(-100%);\n transition: transform 0.6s ease;\n }\n \n .section-header:hover::before {\n transform: translateX(100%);\n }\n \n .section-header:hover {\n background: rgba(255, 255, 255, 0.08);\n transform: translateX(2px);\n box-shadow: inset 2px 0 0 rgba(116, 185, 255, 0.5);\n }\n \n .section-header:active {\n background: rgba(255, 255, 255, 0.12);\n transform: translateX(1px);\n }\n \n .section-header.expanded {\n background: rgba(116, 185, 255, 0.1);\n border-left: 2px solid var(--info-panel-accent-color, #74b9ff);\n }\n \n .info-section[data-section=\"node\"] {\n background: linear-gradient(135deg, rgba(116, 185, 255, 0.05), rgba(160, 212, 104, 0.05));\n border: 1px solid rgba(116, 185, 255, 0.2);\n border-radius: 8px;\n margin: 6px;\n }\n \n .info-section[data-section=\"node\"] .section-header {\n background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(160, 212, 104, 0.15));\n border-radius: 8px 8px 0 0;\n cursor: default;\n }\n \n .info-section[data-section=\"node\"] .section-header:hover {\n transform: none;\n box-shadow: none;\n }\n \n .info-section[data-section=\"node\"] .section-title {\n color: var(--info-panel-accent-color, #74b9ff);\n font-weight: 700;\n }\n \n .info-section[data-section=\"inspector\"] {\n background: linear-gradient(135deg, rgba(100, 150, 255, 0.05), rgba(74, 144, 226, 0.05));\n border: 1px solid rgba(100, 150, 255, 0.2);\n border-radius: 8px;\n margin: 6px;\n }\n \n .info-section[data-section=\"inspector\"] .section-header {\n background: linear-gradient(135deg, rgba(100, 150, 255, 0.15), rgba(74, 144, 226, 0.15));\n border-radius: 8px 8px 0 0;\n }\n \n .info-section[data-section=\"inspector\"] .section-title {\n color: #6496ff;\n font-weight: 700;\n }\n \n .info-section[data-section=\"media\"] {\n background: linear-gradient(135deg, rgba(253, 203, 110, 0.05), rgba(255, 215, 0, 0.05));\n border: 1px solid rgba(253, 203, 110, 0.2);\n border-radius: 8px;\n margin: 6px;\n }\n \n .info-section[data-section=\"media\"] .section-header {\n background: linear-gradient(135deg, rgba(253, 203, 110, 0.15), rgba(255, 215, 0, 0.15));\n border-radius: 8px 8px 0 0;\n }\n \n .info-section[data-section=\"media\"] .section-title {\n color: var(--info-panel-accent-color, #fdcb6e);\n font-weight: 700;\n }\n \n .section-icon {\n font-size: 18px;\n width: 22px;\n text-align: center;\n }\n \n .section-title {\n flex: 1;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.3px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n }\n \n .section-badge {\n background: rgba(100, 150, 255, 0.2);\n color: #8bb3ff;\n padding: 3px 10px;\n border-radius: 10px;\n font-size: 13px;\n font-weight: 500;\n }\n \n .expand-icon {\n font-size: 14px;\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n color: rgba(255, 255, 255, 0.6);\n width: 20px;\n height: 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background: rgba(255, 255, 255, 0.05);\n margin-left: auto;\n }\n \n .section-header:hover .expand-icon {\n color: var(--info-panel-accent-color, #74b9ff);\n background: rgba(116, 185, 255, 0.15);\n transform: scale(1.1);\n }\n \n .section-header.expanded .expand-icon {\n transform: rotate(90deg) scale(1.1);\n color: var(--info-panel-accent-color, #74b9ff);\n background: rgba(116, 185, 255, 0.2);\n }\n \n .section-content {\n max-height: 0;\n overflow: visible;\n transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;\n opacity: 0;\n }\n\n .section-content.expanded {\n max-height: 2000px;\n opacity: 1;\n overflow: visible;\n }\n \n .section-body {\n padding: 0 18px 16px 24px;\n }\n \n .info-row {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n padding: 10px 0;\n border-bottom: 1px solid rgba(255, 255, 255, 0.05);\n transition: all 0.2s ease;\n border-radius: 4px;\n margin: 0 -8px;\n padding-left: 8px;\n padding-right: 8px;\n }\n \n .info-row:last-child {\n border-bottom: none;\n }\n \n .info-row:hover {\n background: rgba(255, 255, 255, 0.03);\n border-bottom-color: rgba(255, 255, 255, 0.08);\n }\n \n .info-label {\n font-weight: 500;\n opacity: 0.85;\n font-size: 15px;\n min-width: 80px;\n padding-right: 12px;\n line-height: 1.5;\n }\n \n .info-value {\n font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;\n font-size: 14px;\n color: var(--info-panel-accent-color, #a0d468);\n background: rgba(160, 212, 104, 0.12);\n padding: 4px 10px;\n border-radius: 6px;\n max-width: 100%;\n word-wrap: break-word;\n word-break: break-word;\n hyphens: auto;\n line-height: 1.4;\n text-align: left;\n white-space: pre-wrap;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n transition: all 0.2s ease;\n }\n \n .info-value:hover {\n background: rgba(160, 212, 104, 0.18);\n transform: translateY(-1px);\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n }\n \n .info-value.long-text {\n white-space: pre-wrap;\n word-wrap: break-word;\n max-width: 100%;\n line-height: 1.4;\n }\n \n .empty-state {\n text-align: center;\n padding: 50px 25px;\n opacity: 0.6;\n }\n \n .empty-state-icon {\n font-size: 36px;\n margin-bottom: 15px;\n opacity: 0.5;\n }\n \n /* Copy to clipboard functionality */\n .info-value[title]:hover::after {\n content: '📋';\n position: absolute;\n right: -20px;\n top: 50%;\n transform: translateY(-50%);\n font-size: 12px;\n opacity: 0.7;\n }\n \n /* Smooth loading states */\n .section-body.loading {\n opacity: 0.5;\n pointer-events: none;\n }\n \n .section-body.loading::after {\n content: '';\n position: absolute;\n top: 50%;\n left: 50%;\n width: 20px;\n height: 20px;\n border: 2px solid transparent;\n border-top: 2px solid var(--info-panel-accent-color, #74b9ff);\n border-radius: 50%;\n animation: spin 1s linear infinite;\n transform: translate(-50%, -50%);\n }\n \n @keyframes spin {\n to { transform: translate(-50%, -50%) rotate(360deg); }\n }\n \n /* Enhanced scrollbar for better UX */\n .section-body::-webkit-scrollbar {\n width: 4px;\n }\n \n .section-body::-webkit-scrollbar-track {\n background: rgba(255, 255, 255, 0.05);\n border-radius: 2px;\n }\n \n .section-body::-webkit-scrollbar-thumb {\n background: var(--info-panel-accent-color, #74b9ff);\n border-radius: 2px;\n opacity: 0.6;\n }\n \n .section-body::-webkit-scrollbar-thumb:hover {\n opacity: 1;\n }\n \n .empty-state-text {\n font-size: 16px;\n font-style: italic;\n }\n \n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.7; }\n }\n \n .pulse {\n animation: pulse 2s infinite;\n }\n `;\n}\n"],"names":[],"mappings":"AAUO,SAAS,kBAA0B;AACtC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkjBX;"} \ No newline at end of file diff --git a/web/magnify-glass/ConfigManager.js b/web/magnify-glass/ConfigManager.js index bde5b7e..be578a2 100644 --- a/web/magnify-glass/ConfigManager.js +++ b/web/magnify-glass/ConfigManager.js @@ -1,108 +1,148 @@ -/** - * ComfyUI MagnifyGlass - ConfigManager - * - * Manages configuration and settings for the magnifying glass. - */ - -import { getSettingValue } from '../shared/utils.js'; -import { DEFAULT_GLASS_SETTINGS } from '../shared/settings.js'; -import { STORAGE_KEYS } from '../shared/constants.js'; - -/** - * Configuration manager class. - * Reads settings from ComfyUI and manages persistent offsets. - */ -export class ConfigManager { - constructor() { - // Initialize properties with defaults - this.zoomFactor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ZoomFactor"] / 100; - this.glassSize = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassSize"]; - this.borderColor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderColor"]; - this.borderWidth = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderWidth"]; - this.activationKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ActivationKey"]; - this.altRequired = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.AltRequired"]; - this.followCursor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.FollowCursor"]; - this.debugMode = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.DebugMode"]; - this.offsetStep = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.OffsetStep"]; - this.glassPosition = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassPosition"]; - this.resetKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ResetKey"]; - this.glassShape = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassShape"]; - this.borderEnabled = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderEnabled"]; - this.textureFiltering = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.TextureFiltering"]; - this.alwaysActiveMode = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.AlwaysActiveMode"]; - this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ToggleFollowCursorKey"]; - - // Alignment adjustment parameters - managed separately +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { getSettingValue } from "../shared/utils.js"; +import { STORAGE_KEYS } from "../shared/constants.js"; +const DEFAULT_GLASS_SETTINGS = { + "🔍MagnifyGlass.ZoomFactor": 300, + "🔍MagnifyGlass.GlassSize": 300, + "🔍MagnifyGlass.BorderColor": "#6b7280", + "🔍MagnifyGlass.BorderWidth": 1, + "🔍MagnifyGlass.ActivationKey": "x", + "🔍MagnifyGlass.AltRequired": false, + "🔍MagnifyGlass.FollowCursor": false, + "🔍MagnifyGlass.DebugMode": false, + "🔍MagnifyGlass.OffsetStep": 5, + "🔍MagnifyGlass.GlassPosition": "Top-Right", + "🔍MagnifyGlass.ResetKey": "o", + "🔍MagnifyGlass.GlassShape": "Rounded Square", + "🔍MagnifyGlass.BorderEnabled": true, + "🔍MagnifyGlass.TextureFiltering": "Linear", + "🔍MagnifyGlass.AlwaysActiveMode": true, + "🔍MagnifyGlass.ToggleFollowCursorKey": "h" +}; +class ConfigManager { + constructor() { + /** Zoom factor (1.0 = 100%, 3.0 = 300%) */ + __publicField(this, "zoomFactor"); + /** Glass size in pixels */ + __publicField(this, "glassSize"); + /** Border color (CSS color string) */ + __publicField(this, "borderColor"); + /** Border width in pixels */ + __publicField(this, "borderWidth"); + /** Activation key (lowercase) */ + __publicField(this, "activationKey"); + /** Whether Alt key is required for activation */ + __publicField(this, "altRequired"); + /** Whether glass follows cursor */ + __publicField(this, "followCursor"); + /** Debug mode enabled */ + __publicField(this, "debugMode"); + /** Offset step for arrow key adjustments */ + __publicField(this, "offsetStep"); + /** Glass position relative to cursor */ + __publicField(this, "glassPosition"); + /** Reset key (lowercase) */ + __publicField(this, "resetKey"); + /** Glass shape */ + __publicField(this, "glassShape"); + /** Whether border is enabled */ + __publicField(this, "borderEnabled"); + /** Texture filtering mode */ + __publicField(this, "textureFiltering"); + /** Always active mode */ + __publicField(this, "alwaysActiveMode"); + /** Toggle follow cursor key */ + __publicField(this, "toggleFollowCursorKey"); + /** Manual offset X in graph units */ + __publicField(this, "offsetX"); + /** Manual offset Y in graph units */ + __publicField(this, "offsetY"); + this.zoomFactor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ZoomFactor"] / 100; + this.glassSize = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassSize"]; + this.borderColor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderColor"]; + this.borderWidth = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderWidth"]; + this.activationKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ActivationKey"]; + this.altRequired = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.AltRequired"]; + this.followCursor = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.FollowCursor"]; + this.debugMode = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.DebugMode"]; + this.offsetStep = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.OffsetStep"]; + this.glassPosition = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassPosition"]; + this.resetKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ResetKey"]; + this.glassShape = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassShape"]; + this.borderEnabled = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.BorderEnabled"]; + this.textureFiltering = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.TextureFiltering"]; + this.alwaysActiveMode = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.AlwaysActiveMode"]; + this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ToggleFollowCursorKey"]; + this.offsetX = 0; + this.offsetY = 0; + } + /** + * Load settings from ComfyUI settings system. + */ + loadSettings() { + this.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", this.zoomFactor * 100) / 100; + this.glassSize = getSettingValue("🔍MagnifyGlass.GlassSize", this.glassSize); + this.borderColor = getSettingValue("🔍MagnifyGlass.BorderColor", this.borderColor); + this.borderWidth = getSettingValue("🔍MagnifyGlass.BorderWidth", this.borderWidth); + this.activationKey = getSettingValue("🔍MagnifyGlass.ActivationKey", this.activationKey); + this.altRequired = getSettingValue("🔍MagnifyGlass.AltRequired", this.altRequired); + this.followCursor = getSettingValue("🔍MagnifyGlass.FollowCursor", this.followCursor); + this.debugMode = getSettingValue("🔍MagnifyGlass.DebugMode", this.debugMode); + this.offsetStep = getSettingValue("🔍MagnifyGlass.OffsetStep", this.offsetStep); + this.glassPosition = getSettingValue("🔍MagnifyGlass.GlassPosition", this.glassPosition); + this.resetKey = getSettingValue("🔍MagnifyGlass.ResetKey", this.resetKey); + this.glassShape = getSettingValue("🔍MagnifyGlass.GlassShape", this.glassShape); + this.borderEnabled = getSettingValue("🔍MagnifyGlass.BorderEnabled", this.borderEnabled); + this.textureFiltering = getSettingValue("🔍MagnifyGlass.TextureFiltering", this.textureFiltering); + this.alwaysActiveMode = getSettingValue("🔍MagnifyGlass.AlwaysActiveMode", this.alwaysActiveMode); + this.toggleFollowCursorKey = getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", this.toggleFollowCursorKey); + } + /** + * Load saved offsets from localStorage. + */ + loadSavedOffsets() { + try { + const savedOffsetX = localStorage.getItem(STORAGE_KEYS.OFFSET_X); + const savedOffsetY = localStorage.getItem(STORAGE_KEYS.OFFSET_Y); + if (savedOffsetX !== null) { + this.offsetX = parseInt(savedOffsetX, 10); + } else { this.offsetX = 0; + } + if (savedOffsetY !== null) { + this.offsetY = parseInt(savedOffsetY, 10); + } else { this.offsetY = 0; + } + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error loading saved offsets:", e); + this.offsetX = 0; + this.offsetY = 0; } - - /** - * Load settings from ComfyUI settings system. - */ - loadSettings() { - this.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", this.zoomFactor * 100) / 100; - this.glassSize = getSettingValue("🔍MagnifyGlass.GlassSize", this.glassSize); - this.borderColor = getSettingValue("🔍MagnifyGlass.BorderColor", this.borderColor); - this.borderWidth = getSettingValue("🔍MagnifyGlass.BorderWidth", this.borderWidth); - this.activationKey = getSettingValue("🔍MagnifyGlass.ActivationKey", this.activationKey); - this.altRequired = getSettingValue("🔍MagnifyGlass.AltRequired", this.altRequired); - this.followCursor = getSettingValue("🔍MagnifyGlass.FollowCursor", this.followCursor); - this.debugMode = getSettingValue("🔍MagnifyGlass.DebugMode", this.debugMode); - this.offsetStep = getSettingValue("🔍MagnifyGlass.OffsetStep", this.offsetStep); - this.glassPosition = getSettingValue("🔍MagnifyGlass.GlassPosition", this.glassPosition); - this.resetKey = getSettingValue("🔍MagnifyGlass.ResetKey", this.resetKey); - this.glassShape = getSettingValue("🔍MagnifyGlass.GlassShape", this.glassShape); - this.borderEnabled = getSettingValue("🔍MagnifyGlass.BorderEnabled", this.borderEnabled); - this.textureFiltering = getSettingValue("🔍MagnifyGlass.TextureFiltering", this.textureFiltering); - this.alwaysActiveMode = getSettingValue("🔍MagnifyGlass.AlwaysActiveMode", this.alwaysActiveMode); - this.toggleFollowCursorKey = getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", this.toggleFollowCursorKey); - } - - /** - * Load saved offsets from localStorage. - */ - loadSavedOffsets() { - try { - const savedOffsetX = localStorage.getItem(STORAGE_KEYS.OFFSET_X); - const savedOffsetY = localStorage.getItem(STORAGE_KEYS.OFFSET_Y); - - if (savedOffsetX !== null) { - this.offsetX = parseInt(savedOffsetX, 10); - } else { - this.offsetX = 0; - } - - if (savedOffsetY !== null) { - this.offsetY = parseInt(savedOffsetY, 10); - } else { - this.offsetY = 0; - } - } catch (e) { - console.error("ComfyUI Magnifying Glass ERROR: Error loading saved offsets:", e); - this.offsetX = 0; - this.offsetY = 0; - } - } - - /** - * Save current offsets to localStorage. - */ - saveOffsets() { - try { - localStorage.setItem(STORAGE_KEYS.OFFSET_X, this.offsetX.toString()); - localStorage.setItem(STORAGE_KEYS.OFFSET_Y, this.offsetY.toString()); - } catch (e) { - console.error("ComfyUI Magnifying Glass ERROR: Error saving offsets:", e); - } - } - - /** - * Reset offsets to default (zero). - */ - resetOffsets() { - this.offsetX = 0; - this.offsetY = 0; - this.saveOffsets(); + } + /** + * Save current offsets to localStorage. + */ + saveOffsets() { + try { + localStorage.setItem(STORAGE_KEYS.OFFSET_X, this.offsetX.toString()); + localStorage.setItem(STORAGE_KEYS.OFFSET_Y, this.offsetY.toString()); + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error saving offsets:", e); } + } + /** + * Reset offsets to default (zero). + */ + resetOffsets() { + this.offsetX = 0; + this.offsetY = 0; + this.saveOffsets(); + } } +export { + ConfigManager +}; +//# sourceMappingURL=ConfigManager.js.map diff --git a/web/magnify-glass/ConfigManager.js.map b/web/magnify-glass/ConfigManager.js.map new file mode 100644 index 0000000..862d1b8 --- /dev/null +++ b/web/magnify-glass/ConfigManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ConfigManager.js","sources":["../../src/magnify-glass/ConfigManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - ConfigManager (TypeScript)\n * \n * Manages configuration and settings for the magnifying glass.\n */\n\nimport { getSettingValue } from '../shared/utils';\nimport { STORAGE_KEYS } from '../shared/constants';\nimport type { GlassPosition, GlassShape, TextureFilter } from '../shared/constants';\n\n// Default settings (we'll import these properly later)\nconst DEFAULT_GLASS_SETTINGS = {\n \"🔍MagnifyGlass.ZoomFactor\": 300,\n \"🔍MagnifyGlass.GlassSize\": 300,\n \"🔍MagnifyGlass.BorderColor\": \"#6b7280\",\n \"🔍MagnifyGlass.BorderWidth\": 1,\n \"🔍MagnifyGlass.ActivationKey\": \"x\",\n \"🔍MagnifyGlass.AltRequired\": false,\n \"🔍MagnifyGlass.FollowCursor\": false,\n \"🔍MagnifyGlass.DebugMode\": false,\n \"🔍MagnifyGlass.OffsetStep\": 5,\n \"🔍MagnifyGlass.GlassPosition\": \"Top-Right\" as GlassPosition,\n \"🔍MagnifyGlass.ResetKey\": \"o\",\n \"🔍MagnifyGlass.GlassShape\": \"Rounded Square\" as GlassShape,\n \"🔍MagnifyGlass.BorderEnabled\": true,\n \"🔍MagnifyGlass.TextureFiltering\": \"Linear\" as TextureFilter,\n \"🔍MagnifyGlass.AlwaysActiveMode\": true,\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": \"h\",\n};\n\n/**\n * Configuration manager class.\n * Reads settings from ComfyUI and manages persistent offsets.\n */\nexport class ConfigManager {\n /** Zoom factor (1.0 = 100%, 3.0 = 300%) */\n zoomFactor: number;\n\n /** Glass size in pixels */\n glassSize: number;\n\n /** Border color (CSS color string) */\n borderColor: string;\n\n /** Border width in pixels */\n borderWidth: number;\n\n /** Activation key (lowercase) */\n activationKey: string;\n\n /** Whether Alt key is required for activation */\n altRequired: boolean;\n\n /** Whether glass follows cursor */\n followCursor: boolean;\n\n /** Debug mode enabled */\n debugMode: boolean;\n\n /** Offset step for arrow key adjustments */\n offsetStep: number;\n\n /** Glass position relative to cursor */\n glassPosition: string;\n\n /** Reset key (lowercase) */\n resetKey: string;\n\n /** Glass shape */\n glassShape: string;\n\n /** Whether border is enabled */\n borderEnabled: boolean;\n\n /** Texture filtering mode */\n textureFiltering: string;\n\n /** Always active mode */\n alwaysActiveMode: boolean;\n\n /** Toggle follow cursor key */\n toggleFollowCursorKey: string;\n\n /** Manual offset X in graph units */\n offsetX: number;\n\n /** Manual offset Y in graph units */\n offsetY: number;\n\n constructor() {\n // Initialize properties with defaults\n this.zoomFactor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ZoomFactor\"] / 100;\n this.glassSize = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassSize\"];\n this.borderColor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderColor\"];\n this.borderWidth = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderWidth\"];\n this.activationKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ActivationKey\"];\n this.altRequired = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.AltRequired\"];\n this.followCursor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.FollowCursor\"];\n this.debugMode = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.DebugMode\"];\n this.offsetStep = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.OffsetStep\"];\n this.glassPosition = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassPosition\"];\n this.resetKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ResetKey\"];\n this.glassShape = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassShape\"];\n this.borderEnabled = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderEnabled\"];\n this.textureFiltering = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.TextureFiltering\"];\n this.alwaysActiveMode = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.AlwaysActiveMode\"];\n this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ToggleFollowCursorKey\"];\n\n // Alignment adjustment parameters - managed separately\n this.offsetX = 0;\n this.offsetY = 0;\n }\n\n /**\n * Load settings from ComfyUI settings system.\n */\n loadSettings(): void {\n this.zoomFactor = getSettingValue(\"🔍MagnifyGlass.ZoomFactor\", this.zoomFactor * 100) / 100;\n this.glassSize = getSettingValue(\"🔍MagnifyGlass.GlassSize\", this.glassSize);\n this.borderColor = getSettingValue(\"🔍MagnifyGlass.BorderColor\", this.borderColor);\n this.borderWidth = getSettingValue(\"🔍MagnifyGlass.BorderWidth\", this.borderWidth);\n this.activationKey = getSettingValue(\"🔍MagnifyGlass.ActivationKey\", this.activationKey);\n this.altRequired = getSettingValue(\"🔍MagnifyGlass.AltRequired\", this.altRequired);\n this.followCursor = getSettingValue(\"🔍MagnifyGlass.FollowCursor\", this.followCursor);\n this.debugMode = getSettingValue(\"🔍MagnifyGlass.DebugMode\", this.debugMode);\n this.offsetStep = getSettingValue(\"🔍MagnifyGlass.OffsetStep\", this.offsetStep);\n this.glassPosition = getSettingValue(\"🔍MagnifyGlass.GlassPosition\", this.glassPosition);\n this.resetKey = getSettingValue(\"🔍MagnifyGlass.ResetKey\", this.resetKey);\n this.glassShape = getSettingValue(\"🔍MagnifyGlass.GlassShape\", this.glassShape);\n this.borderEnabled = getSettingValue(\"🔍MagnifyGlass.BorderEnabled\", this.borderEnabled);\n this.textureFiltering = getSettingValue(\"🔍MagnifyGlass.TextureFiltering\", this.textureFiltering);\n this.alwaysActiveMode = getSettingValue(\"🔍MagnifyGlass.AlwaysActiveMode\", this.alwaysActiveMode);\n this.toggleFollowCursorKey = getSettingValue(\"🔍MagnifyGlass.ToggleFollowCursorKey\", this.toggleFollowCursorKey);\n }\n\n /**\n * Load saved offsets from localStorage.\n */\n loadSavedOffsets(): void {\n try {\n const savedOffsetX = localStorage.getItem(STORAGE_KEYS.OFFSET_X);\n const savedOffsetY = localStorage.getItem(STORAGE_KEYS.OFFSET_Y);\n\n if (savedOffsetX !== null) {\n this.offsetX = parseInt(savedOffsetX, 10);\n } else {\n this.offsetX = 0;\n }\n\n if (savedOffsetY !== null) {\n this.offsetY = parseInt(savedOffsetY, 10);\n } else {\n this.offsetY = 0;\n }\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error loading saved offsets:\", e);\n this.offsetX = 0;\n this.offsetY = 0;\n }\n }\n\n /**\n * Save current offsets to localStorage.\n */\n saveOffsets(): void {\n try {\n localStorage.setItem(STORAGE_KEYS.OFFSET_X, this.offsetX.toString());\n localStorage.setItem(STORAGE_KEYS.OFFSET_Y, this.offsetY.toString());\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error saving offsets:\", e);\n }\n }\n\n /**\n * Reset offsets to default (zero).\n */\n resetOffsets(): void {\n this.offsetX = 0;\n this.offsetY = 0;\n this.saveOffsets();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/DebugManager.js b/web/magnify-glass/DebugManager.js index 944ca26..602d387 100644 --- a/web/magnify-glass/DebugManager.js +++ b/web/magnify-glass/DebugManager.js @@ -1,177 +1,128 @@ -/** - * ComfyUI MagnifyGlass - DebugManager - * - * Handles debug visualization and logging. - */ - -import { app } from "../../../../scripts/app.js"; - -/** - * Debug Manager class. - * Provides debug logging and canvas visualization. - */ -export class DebugManager { - /** - * @param {import('./ConfigManager.js').ConfigManager} config - * @param {import('./MagnifierState.js').MagnifierState} state - * @param {import('./UiManager.js').UiManager} ui - */ - constructor(config, state, ui) { - this.config = config; - this.state = state; - this.ui = ui; +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +class DebugManager { + constructor(config, state, ui) { + __publicField(this, "config"); + __publicField(this, "state"); + __publicField(this, "ui"); + this.config = config; + this.state = state; + this.ui = ui; + } + /** + * Log a message if debug mode is enabled. + */ + log(...args) { + if (this.config.debugMode) { + console.log("ComfyUI Magnifying Glass:", ...args); } - - /** - * Log a message if debug mode is enabled. - * @param {...any} args - */ - log(...args) { - if (this.config.debugMode) { - console.log("ComfyUI Magnifying Glass:", ...args); + } + /** + * Log an error message. + */ + error(...args) { + console.error("ComfyUI Magnifying Glass ERROR:", ...args); + } + /** + * Print detailed canvas information for debugging. + */ + printCanvasInfo() { + if (!this.config.debugMode) return; + try { + const canvasManager = app.canvas; + const canvas = canvasManager == null ? void 0 : canvasManager.graph_canvas; + if (!canvas) { + this.log("Could not find graph canvas for detailed info"); + return; + } + this.log("---- Canvas Information ----"); + this.log(`Canvas dimensions: ${canvas.width}x${canvas.height}`); + this.log(`Canvas display size: ${canvas.clientWidth}x${canvas.clientHeight}`); + this.log(`Canvas CSS transform: ${canvas.style.transform || "none"}`); + const ds = canvasManager.ds; + if (ds) { + this.log(`Canvas DS scale: ${ds.scale}`); + if (ds.offset) { + this.log(`Canvas DS offset: [${ds.offset[0]}, ${ds.offset[1]}]`); + } else { + this.log("Canvas DS offset not found"); } + } else { + this.log("Canvas DS object not found"); + } + const screenWidth = window.innerWidth; + const screenHeight = window.innerHeight; + const canvasWidth = canvas.width; + const canvasHeight = canvas.height; + this.log(`Window dimensions: ${screenWidth}x${screenHeight}`); + this.log(`Resolution scale factor: X=${canvasWidth / screenWidth}, Y=${canvasHeight / screenHeight}`); + this.log("---- End Canvas Information ----"); + } catch (e) { + this.log("Error in printCanvasInfo:", e); } - - /** - * Log an error message. - * @param {...any} args - */ - error(...args) { - console.error("ComfyUI Magnifying Glass ERROR:", ...args); - } - - /** - * Print detailed canvas information for debugging. - */ - printCanvasInfo() { - if (!this.config.debugMode) return; - - try { - const canvas = app.canvas.graph_canvas; - if (!canvas) { - this.log("Could not find graph canvas for detailed info"); - return; - } - - this.log("---- Canvas Information ----"); - this.log(`Canvas dimensions: ${canvas.width}x${canvas.height}`); - this.log(`Canvas display size: ${canvas.clientWidth}x${canvas.clientHeight}`); - this.log(`Canvas CSS transform: ${canvas.style.transform || 'none'}`); - - if (app.canvas.ds) { - this.log(`Canvas DS scale: ${app.canvas.ds.scale}`); - if (app.canvas.ds.offset) { - this.log(`Canvas DS offset: [${app.canvas.ds.offset[0]}, ${app.canvas.ds.offset[1]}]`); - } else { - this.log("Canvas DS offset not found"); - } - } else { - this.log("Canvas DS object not found"); - } - - const screenWidth = window.innerWidth; - const screenHeight = window.innerHeight; - const canvasWidth = canvas.width; - const canvasHeight = canvas.height; - - this.log(`Window dimensions: ${screenWidth}x${screenHeight}`); - this.log(`Resolution scale factor: X=${canvasWidth / screenWidth}, Y=${canvasHeight / screenHeight}`); - this.log("---- End Canvas Information ----"); - } catch (e) { - this.log("Error in printCanvasInfo:", e); - } - } - - /** - * Update the debug visualization canvas. - */ - updateDebugView() { - if (!this.config.debugMode || !this.ui || !this.ui.debugCanvas || !this.ui.debugCtx) return; - - const debugCtx = this.ui.debugCtx; - const debugCanvas = this.ui.debugCanvas; - - debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); - - // Draw background - debugCtx.fillStyle = 'rgba(0,0,0,0.8)'; - debugCtx.fillRect(0, 0, debugCanvas.width, debugCanvas.height); - - // Header - debugCtx.fillStyle = '#FFFFFF'; - debugCtx.font = '14px monospace'; - debugCtx.fillText('Magnify Glass Debug', 10, 20); - - // Position info - debugCtx.font = '12px monospace'; - debugCtx.fillText(`Cursor: (${this.state.x.toFixed(1)}, ${this.state.y.toFixed(1)})`, 10, 50); - debugCtx.fillText(`Source Rect: (${this.state.sourceX.toFixed(1)}, ${this.state.sourceY.toFixed(1)}, w:${this.state.sourceWidth.toFixed(1)}, h:${this.state.sourceHeight.toFixed(1)})`, 10, 70); - - // Canvas transform - debugCtx.fillText(`Canvas Scale: ${this.state.canvasScale.toFixed(2)}`, 10, 90); - debugCtx.fillText(`Canvas Offset: (${this.state.canvasOffsetX.toFixed(1)}, ${this.state.canvasOffsetY.toFixed(1)})`, 10, 110); - - // Offset information - debugCtx.fillStyle = '#FFFF00'; - debugCtx.fillText(`MANUAL OFFSETS: X=${this.config.offsetX}, Y=${this.config.offsetY} (Use arrow keys to adjust)`, 10, 130); - debugCtx.fillStyle = '#FFFFFF'; - - // Visualization - this.drawCanvasVisualization(debugCtx, debugCanvas); - } - - /** - * Draw a scaled visualization of the canvas and source region. - * @param {CanvasRenderingContext2D} debugCtx - * @param {HTMLCanvasElement} debugCanvas - */ - drawCanvasVisualization(debugCtx, debugCanvas) { - const canvasScale = 0.1; - const canvasVisX = 10; - const canvasVisY = 170; - const canvasVisWidth = 380; - const canvasVisHeight = 150; - - // Draw canvas representation - debugCtx.strokeStyle = '#AAAAAA'; - debugCtx.strokeRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); - debugCtx.fillStyle = '#444444'; - debugCtx.fillRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); - - // Calculate cursor position in visualization - const cursorVisX = canvasVisX + (this.state.x * canvasScale); - const cursorVisY = canvasVisY + (this.state.y * canvasScale); - - // Calculate source rect in visualization - const sourceRectVisX = canvasVisX + (this.state.sourceX * canvasScale); - const sourceRectVisY = canvasVisY + (this.state.sourceY * canvasScale); - const sourceRectVisWidth = this.state.sourceWidth * canvasScale; - const sourceRectVisHeight = this.state.sourceHeight * canvasScale; - - // Draw source rect - debugCtx.strokeStyle = '#FF0000'; - debugCtx.strokeRect(sourceRectVisX, sourceRectVisY, sourceRectVisWidth, sourceRectVisHeight); - - // Draw cursor position - debugCtx.fillStyle = '#FFFF00'; - debugCtx.beginPath(); - debugCtx.arc(cursorVisX, cursorVisY, 3, 0, Math.PI * 2); - debugCtx.fill(); - - // Draw line from cursor to source rect center - debugCtx.strokeStyle = '#00FF00'; - debugCtx.beginPath(); - debugCtx.moveTo(cursorVisX, cursorVisY); - debugCtx.lineTo(sourceRectVisX + sourceRectVisWidth / 2, sourceRectVisY + sourceRectVisHeight / 2); - debugCtx.stroke(); - - // Label for visualization - debugCtx.fillStyle = '#FFFFFF'; - debugCtx.fillText('Canvas Visualization (scaled)', canvasVisX, canvasVisY - 5); - - // Offset values - const offsetX = this.state.sourceX - this.state.x + (this.state.sourceWidth / 2); - const offsetY = this.state.sourceY - this.state.y + (this.state.sourceHeight / 2); - debugCtx.fillText(`Alignment Offset: (${offsetX.toFixed(1)}, ${offsetY.toFixed(1)})`, 10, canvasVisY + canvasVisHeight + 20); - } + } + /** + * Update the debug visualization canvas. + */ + updateDebugView() { + if (!this.config.debugMode || !this.ui || !this.ui.debugCanvas || !this.ui.debugCtx) return; + const debugCtx = this.ui.debugCtx; + const debugCanvas = this.ui.debugCanvas; + debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); + debugCtx.fillStyle = "rgba(0,0,0,0.8)"; + debugCtx.fillRect(0, 0, debugCanvas.width, debugCanvas.height); + debugCtx.fillStyle = "#FFFFFF"; + debugCtx.font = "14px monospace"; + debugCtx.fillText("Magnify Glass Debug", 10, 20); + debugCtx.font = "12px monospace"; + debugCtx.fillText(`Cursor: (${this.state.x.toFixed(1)}, ${this.state.y.toFixed(1)})`, 10, 50); + debugCtx.fillText(`Source Rect: (${this.state.sourceX.toFixed(1)}, ${this.state.sourceY.toFixed(1)}, w:${this.state.sourceWidth.toFixed(1)}, h:${this.state.sourceHeight.toFixed(1)})`, 10, 70); + debugCtx.fillText(`Canvas Scale: ${this.state.canvasScale.toFixed(2)}`, 10, 90); + debugCtx.fillText(`Canvas Offset: (${this.state.canvasOffsetX.toFixed(1)}, ${this.state.canvasOffsetY.toFixed(1)})`, 10, 110); + debugCtx.fillStyle = "#FFFF00"; + debugCtx.fillText(`MANUAL OFFSETS: X=${this.config.offsetX}, Y=${this.config.offsetY} (Use arrow keys to adjust)`, 10, 130); + debugCtx.fillStyle = "#FFFFFF"; + this.drawCanvasVisualization(debugCtx, debugCanvas); + } + /** + * Draw a scaled visualization of the canvas and source region. + */ + drawCanvasVisualization(debugCtx, debugCanvas) { + const canvasScale = 0.1; + const canvasVisX = 10; + const canvasVisY = 170; + const canvasVisWidth = 380; + const canvasVisHeight = 150; + debugCtx.strokeStyle = "#AAAAAA"; + debugCtx.strokeRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); + debugCtx.fillStyle = "#444444"; + debugCtx.fillRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); + const cursorVisX = canvasVisX + this.state.x * canvasScale; + const cursorVisY = canvasVisY + this.state.y * canvasScale; + const sourceRectVisX = canvasVisX + this.state.sourceX * canvasScale; + const sourceRectVisY = canvasVisY + this.state.sourceY * canvasScale; + const sourceRectVisWidth = this.state.sourceWidth * canvasScale; + const sourceRectVisHeight = this.state.sourceHeight * canvasScale; + debugCtx.strokeStyle = "#FF0000"; + debugCtx.strokeRect(sourceRectVisX, sourceRectVisY, sourceRectVisWidth, sourceRectVisHeight); + debugCtx.fillStyle = "#FFFF00"; + debugCtx.beginPath(); + debugCtx.arc(cursorVisX, cursorVisY, 3, 0, Math.PI * 2); + debugCtx.fill(); + debugCtx.strokeStyle = "#00FF00"; + debugCtx.beginPath(); + debugCtx.moveTo(cursorVisX, cursorVisY); + debugCtx.lineTo(sourceRectVisX + sourceRectVisWidth / 2, sourceRectVisY + sourceRectVisHeight / 2); + debugCtx.stroke(); + debugCtx.fillStyle = "#FFFFFF"; + debugCtx.fillText("Canvas Visualization (scaled)", canvasVisX, canvasVisY - 5); + const offsetX = this.state.sourceX - this.state.x + this.state.sourceWidth / 2; + const offsetY = this.state.sourceY - this.state.y + this.state.sourceHeight / 2; + debugCtx.fillText(`Alignment Offset: (${offsetX.toFixed(1)}, ${offsetY.toFixed(1)})`, 10, canvasVisY + canvasVisHeight + 20); + } } +export { + DebugManager +}; +//# sourceMappingURL=DebugManager.js.map diff --git a/web/magnify-glass/DebugManager.js.map b/web/magnify-glass/DebugManager.js.map new file mode 100644 index 0000000..d09fafb --- /dev/null +++ b/web/magnify-glass/DebugManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"DebugManager.js","sources":["../../src/magnify-glass/DebugManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - DebugManager (TypeScript)\n * \n * Handles debug visualization and logging.\n */\n\nimport type { ComfyApp } from '../types/comfyui';\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\nimport type { UiManager } from './UiManager';\n\ndeclare const app: ComfyApp;\n\n/**\n * Debug Manager class.\n * Provides debug logging and canvas visualization.\n */\nexport class DebugManager {\n config: ConfigManager;\n state: MagnifierState;\n ui: UiManager;\n\n constructor(config: ConfigManager, state: MagnifierState, ui: UiManager) {\n this.config = config;\n this.state = state;\n this.ui = ui;\n }\n\n /**\n * Log a message if debug mode is enabled.\n */\n log(...args: unknown[]): void {\n if (this.config.debugMode) {\n console.log(\"ComfyUI Magnifying Glass:\", ...args);\n }\n }\n\n /**\n * Log an error message.\n */\n error(...args: unknown[]): void {\n console.error(\"ComfyUI Magnifying Glass ERROR:\", ...args);\n }\n\n /**\n * Print detailed canvas information for debugging.\n */\n printCanvasInfo(): void {\n if (!this.config.debugMode) return;\n\n try {\n // Using any for deep property access on ComfyUI objects that might vary\n const canvasManager = (app as any).canvas;\n const canvas = canvasManager?.graph_canvas as HTMLCanvasElement;\n\n if (!canvas) {\n this.log(\"Could not find graph canvas for detailed info\");\n return;\n }\n\n this.log(\"---- Canvas Information ----\");\n this.log(`Canvas dimensions: ${canvas.width}x${canvas.height}`);\n this.log(`Canvas display size: ${canvas.clientWidth}x${canvas.clientHeight}`);\n this.log(`Canvas CSS transform: ${canvas.style.transform || 'none'}`);\n\n const ds = canvasManager.ds;\n if (ds) {\n this.log(`Canvas DS scale: ${ds.scale}`);\n if (ds.offset) {\n this.log(`Canvas DS offset: [${ds.offset[0]}, ${ds.offset[1]}]`);\n } else {\n this.log(\"Canvas DS offset not found\");\n }\n } else {\n this.log(\"Canvas DS object not found\");\n }\n\n const screenWidth = window.innerWidth;\n const screenHeight = window.innerHeight;\n const canvasWidth = canvas.width;\n const canvasHeight = canvas.height;\n\n this.log(`Window dimensions: ${screenWidth}x${screenHeight}`);\n this.log(`Resolution scale factor: X=${canvasWidth / screenWidth}, Y=${canvasHeight / screenHeight}`);\n this.log(\"---- End Canvas Information ----\");\n } catch (e) {\n this.log(\"Error in printCanvasInfo:\", e);\n }\n }\n\n /**\n * Update the debug visualization canvas.\n */\n updateDebugView(): void {\n if (!this.config.debugMode || !this.ui || !this.ui.debugCanvas || !this.ui.debugCtx) return;\n\n const debugCtx = this.ui.debugCtx;\n const debugCanvas = this.ui.debugCanvas;\n\n debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);\n\n // Draw background\n debugCtx.fillStyle = 'rgba(0,0,0,0.8)';\n debugCtx.fillRect(0, 0, debugCanvas.width, debugCanvas.height);\n\n // Header\n debugCtx.fillStyle = '#FFFFFF';\n debugCtx.font = '14px monospace';\n debugCtx.fillText('Magnify Glass Debug', 10, 20);\n\n // Position info\n debugCtx.font = '12px monospace';\n debugCtx.fillText(`Cursor: (${this.state.x.toFixed(1)}, ${this.state.y.toFixed(1)})`, 10, 50);\n debugCtx.fillText(`Source Rect: (${this.state.sourceX.toFixed(1)}, ${this.state.sourceY.toFixed(1)}, w:${this.state.sourceWidth.toFixed(1)}, h:${this.state.sourceHeight.toFixed(1)})`, 10, 70);\n\n // Canvas transform\n debugCtx.fillText(`Canvas Scale: ${this.state.canvasScale.toFixed(2)}`, 10, 90);\n debugCtx.fillText(`Canvas Offset: (${this.state.canvasOffsetX.toFixed(1)}, ${this.state.canvasOffsetY.toFixed(1)})`, 10, 110);\n\n // Offset information\n debugCtx.fillStyle = '#FFFF00';\n debugCtx.fillText(`MANUAL OFFSETS: X=${this.config.offsetX}, Y=${this.config.offsetY} (Use arrow keys to adjust)`, 10, 130);\n debugCtx.fillStyle = '#FFFFFF';\n\n // Visualization\n this.drawCanvasVisualization(debugCtx, debugCanvas);\n }\n\n /**\n * Draw a scaled visualization of the canvas and source region.\n */\n drawCanvasVisualization(debugCtx: CanvasRenderingContext2D, debugCanvas: HTMLCanvasElement): void {\n const canvasScale = 0.1;\n const canvasVisX = 10;\n const canvasVisY = 170;\n const canvasVisWidth = 380;\n const canvasVisHeight = 150;\n\n // Draw canvas representation\n debugCtx.strokeStyle = '#AAAAAA';\n debugCtx.strokeRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight);\n debugCtx.fillStyle = '#444444';\n debugCtx.fillRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight);\n\n // Calculate cursor position in visualization\n const cursorVisX = canvasVisX + (this.state.x * canvasScale);\n const cursorVisY = canvasVisY + (this.state.y * canvasScale);\n\n // Calculate source rect in visualization\n const sourceRectVisX = canvasVisX + (this.state.sourceX * canvasScale);\n const sourceRectVisY = canvasVisY + (this.state.sourceY * canvasScale);\n const sourceRectVisWidth = this.state.sourceWidth * canvasScale;\n const sourceRectVisHeight = this.state.sourceHeight * canvasScale;\n\n // Draw source rect\n debugCtx.strokeStyle = '#FF0000';\n debugCtx.strokeRect(sourceRectVisX, sourceRectVisY, sourceRectVisWidth, sourceRectVisHeight);\n\n // Draw cursor position\n debugCtx.fillStyle = '#FFFF00';\n debugCtx.beginPath();\n debugCtx.arc(cursorVisX, cursorVisY, 3, 0, Math.PI * 2);\n debugCtx.fill();\n\n // Draw line from cursor to source rect center\n debugCtx.strokeStyle = '#00FF00';\n debugCtx.beginPath();\n debugCtx.moveTo(cursorVisX, cursorVisY);\n debugCtx.lineTo(sourceRectVisX + sourceRectVisWidth / 2, sourceRectVisY + sourceRectVisHeight / 2);\n debugCtx.stroke();\n\n // Label for visualization\n debugCtx.fillStyle = '#FFFFFF';\n debugCtx.fillText('Canvas Visualization (scaled)', canvasVisX, canvasVisY - 5);\n\n // Offset values\n const offsetX = this.state.sourceX - this.state.x + (this.state.sourceWidth / 2);\n const offsetY = this.state.sourceY - this.state.y + (this.state.sourceHeight / 2);\n debugCtx.fillText(`Alignment Offset: (${offsetX.toFixed(1)}, ${offsetY.toFixed(1)})`, 10, canvasVisY + canvasVisHeight + 20);\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/EventHandler.js b/web/magnify-glass/EventHandler.js index adf75af..e4829fe 100644 --- a/web/magnify-glass/EventHandler.js +++ b/web/magnify-glass/EventHandler.js @@ -1,233 +1,179 @@ -/** - * ComfyUI MagnifyGlass - EventHandler - * - * Handles keyboard and mouse events for the magnifying glass. - */ - -import { isUserTyping } from '../shared/utils.js'; -import { DEFAULT_PADDING } from '../shared/constants.js'; - -/** - * Event Handler class. - * Manages all keyboard and mouse event handling for the magnifier. - */ -export class EventHandler { - /** - * @param {import('./MagnifyGlass.js').MagnifyGlass} magnifyGlass - */ - constructor(magnifyGlass) { - this.magnifyGlass = magnifyGlass; - - // Bind methods to maintain context - this.handleKeyDown = this.handleKeyDown.bind(this); - this.handleKeyUp = this.handleKeyUp.bind(this); - this.handleMouseMove = this.handleMouseMove.bind(this); +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { isUserTyping } from "../shared/utils.js"; +import { DEFAULT_PADDING } from "../shared/constants.js"; +class EventHandler { + constructor(magnifyGlass) { + __publicField(this, "magnifyGlass"); + this.magnifyGlass = magnifyGlass; + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleKeyUp = this.handleKeyUp.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + } + /** + * Attach all event listeners. + */ + attachListeners() { + document.addEventListener("keydown", this.handleKeyDown); + document.addEventListener("keyup", this.handleKeyUp); + document.addEventListener("mousemove", this.handleMouseMove); + } + /** + * Detach all event listeners. + */ + detachListeners() { + document.removeEventListener("keydown", this.handleKeyDown); + document.removeEventListener("keyup", this.handleKeyUp); + document.removeEventListener("mousemove", this.handleMouseMove); + } + /** + * Handle keydown events. + */ + handleKeyDown(e) { + const config = this.magnifyGlass.config; + const state = this.magnifyGlass.state; + if (isUserTyping()) { + return; } - - /** - * Attach all event listeners. - */ - attachListeners() { - document.addEventListener("keydown", this.handleKeyDown); - document.addEventListener("keyup", this.handleKeyUp); - document.addEventListener("mousemove", this.handleMouseMove); - } - - /** - * Detach all event listeners. - */ - detachListeners() { - document.removeEventListener("keydown", this.handleKeyDown); - document.removeEventListener("keyup", this.handleKeyUp); - document.removeEventListener("mousemove", this.handleMouseMove); - } - - /** - * Handle keydown events. - * @param {KeyboardEvent} e - */ - handleKeyDown(e) { - const config = this.magnifyGlass.config; - const state = this.magnifyGlass.state; - - // Don't handle hotkeys if user is typing in an input field - if (isUserTyping()) { - return; - } - - // Magnifier activation - if (e.key.toLowerCase() === config.activationKey && - (!config.altRequired || e.altKey)) { - if (config.alwaysActiveMode) { - if (state.active) { - state.active = false; - this.magnifyGlass.ui.hide(); - } else { - state.active = true; - this.magnifyGlass.ui.show(); - this.updateInitialPosition(); - } - } else { - if (!state.active) { - state.active = true; - this.magnifyGlass.ui.show(); - this.updateInitialPosition(); - } - } - } - - // Manual offset adjustment keys (only when magnifier is active) + console.log(`[MagnifyGlass] KeyDown: ${e.key} (config: ${config.activationKey}, active: ${state.active})`); + if (e.key.toLowerCase() === config.activationKey && (!config.altRequired || e.altKey)) { + console.log("[MagnifyGlass] Activation key matched!"); + if (config.alwaysActiveMode) { if (state.active) { - let offsetChanged = false; - const stepSize = e.shiftKey ? config.offsetStep * 5 : config.offsetStep; - - if (e.key === "ArrowUp") { - config.offsetY -= stepSize; - offsetChanged = true; - e.preventDefault(); - } else if (e.key === "ArrowDown") { - config.offsetY += stepSize; - offsetChanged = true; - e.preventDefault(); - } else if (e.key === "ArrowLeft") { - config.offsetX -= stepSize; - offsetChanged = true; - e.preventDefault(); - } else if (e.key === "ArrowRight") { - config.offsetX += stepSize; - offsetChanged = true; - e.preventDefault(); - } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() && - (!config.altRequired || e.altKey)) { - this.magnifyGlass.resetOffsets(); - offsetChanged = true; - e.preventDefault(); - } - - // Toggle Follow Cursor Key - if (e.key.toLowerCase() === config.toggleFollowCursorKey && - (!config.altRequired || e.altKey)) { - config.followCursor = !config.followCursor; - this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? 'ON' : 'OFF'}`); - if (state.active && config.followCursor) { - this.magnifyGlass.ui.positionGlass( - this.magnifyGlass.lastKnownMousePosition.x, - this.magnifyGlass.lastKnownMousePosition.y - ); - } - e.preventDefault(); - } - - if (offsetChanged) { - config.saveOffsets(); - this.magnifyGlass.updateMagnifiedView(); - } + state.active = false; + this.magnifyGlass.ui.hide(); + } else { + state.active = true; + this.magnifyGlass.ui.show(); + this.updateInitialPosition(); } - } - - /** - * Handle keyup events. - * @param {KeyboardEvent} e - */ - handleKeyUp(e) { - const config = this.magnifyGlass.config; - const state = this.magnifyGlass.state; - - if (!config.alwaysActiveMode && - (e.key.toLowerCase() === config.activationKey || - (config.altRequired && e.key === "Alt"))) { - - let shouldDeactivate = false; - if (config.altRequired) { - if (e.key === "Alt" || e.key.toLowerCase() === config.activationKey) { - shouldDeactivate = true; - } - } else { - if (e.key.toLowerCase() === config.activationKey) { - shouldDeactivate = true; - } - } - - if (state.active && shouldDeactivate) { - state.active = false; - this.magnifyGlass.ui.hide(); - } + } else { + if (!state.active) { + state.active = true; + this.magnifyGlass.ui.show(); + this.updateInitialPosition(); } + } } - - /** - * Handle mousemove events. - * @param {MouseEvent} e - */ - handleMouseMove(e) { - // Always track mouse position - this.magnifyGlass.lastKnownMousePosition.x = e.clientX; - this.magnifyGlass.lastKnownMousePosition.y = e.clientY; - - if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return; - - const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); - const cssMouseXOnCanvas = e.clientX - rect.left; - const cssMouseYOnCanvas = e.clientY - rect.top; - - // Check if the cursor is over the canvas element - if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width && - cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) { - - const canvasElement = this.magnifyGlass.litegraphCanvas; - const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; - const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; - - const pixelX = cssMouseXOnCanvas * scaleX; - const pixelY = cssMouseYOnCanvas * scaleY; - - this.magnifyGlass.state.x = pixelX; - this.magnifyGlass.state.y = pixelY; - - this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY); - this.magnifyGlass.updateMagnifiedView(); + if (state.active) { + let offsetChanged = false; + const stepSize = e.shiftKey ? config.offsetStep * 5 : config.offsetStep; + if (e.key === "ArrowUp") { + config.offsetY -= stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowDown") { + config.offsetY += stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowLeft") { + config.offsetX -= stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key === "ArrowRight") { + config.offsetX += stepSize; + offsetChanged = true; + e.preventDefault(); + } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() && (!config.altRequired || e.altKey)) { + this.magnifyGlass.resetOffsets(); + offsetChanged = true; + e.preventDefault(); + } + if (e.key.toLowerCase() === config.toggleFollowCursorKey && (!config.altRequired || e.altKey)) { + config.followCursor = !config.followCursor; + this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? "ON" : "OFF"}`); + if (state.active && config.followCursor) { + this.magnifyGlass.ui.positionGlass( + this.magnifyGlass.lastKnownMousePosition.x, + this.magnifyGlass.lastKnownMousePosition.y + ); } + e.preventDefault(); + } + if (offsetChanged) { + config.saveOffsets(); + this.magnifyGlass.updateMagnifiedView(); + } } - - /** - * Update the initial position when magnifier is activated. - */ - updateInitialPosition() { - if (!this.magnifyGlass.litegraphCanvas) return; - - const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); - const clientX = this.magnifyGlass.lastKnownMousePosition.x; - const clientY = this.magnifyGlass.lastKnownMousePosition.y; - - const isOverCanvas = clientX >= rect.left && clientX <= rect.right && - clientY >= rect.top && clientY <= rect.bottom; - - if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) { - const cssMouseXOnCanvas = clientX - rect.left; - const cssMouseYOnCanvas = clientY - rect.top; - - const canvasElement = this.magnifyGlass.litegraphCanvas; - const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; - const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; - - const pixelX = cssMouseXOnCanvas * scaleX; - const pixelY = cssMouseYOnCanvas * scaleY; - - this.magnifyGlass.state.x = pixelX; - this.magnifyGlass.state.y = pixelY; - - const glassSize = this.magnifyGlass.config.glassSize; - - if (!this.magnifyGlass.state.wasActivatedBefore) { - // First activation - position at top right of the window - this.magnifyGlass.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`; - this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_PADDING}px`; - this.magnifyGlass.state.wasActivatedBefore = true; - } else { - this.magnifyGlass.ui.positionGlass(clientX, clientY); - } - - this.magnifyGlass.updateMagnifiedView(); + } + /** + * Handle keyup events. + */ + handleKeyUp(e) { + const config = this.magnifyGlass.config; + const state = this.magnifyGlass.state; + if (!config.alwaysActiveMode && (e.key.toLowerCase() === config.activationKey || config.altRequired && e.key === "Alt")) { + let shouldDeactivate = false; + if (config.altRequired) { + if (e.key === "Alt" || e.key.toLowerCase() === config.activationKey) { + shouldDeactivate = true; } + } else { + if (e.key.toLowerCase() === config.activationKey) { + shouldDeactivate = true; + } + } + if (state.active && shouldDeactivate) { + state.active = false; + this.magnifyGlass.ui.hide(); + } } + } + /** + * Handle mousemove events. + */ + handleMouseMove(e) { + this.magnifyGlass.lastKnownMousePosition.x = e.clientX; + this.magnifyGlass.lastKnownMousePosition.y = e.clientY; + if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return; + const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); + const cssMouseXOnCanvas = e.clientX - rect.left; + const cssMouseYOnCanvas = e.clientY - rect.top; + if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width && cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) { + const canvasElement = this.magnifyGlass.litegraphCanvas; + const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; + const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; + const pixelX = cssMouseXOnCanvas * scaleX; + const pixelY = cssMouseYOnCanvas * scaleY; + this.magnifyGlass.state.x = pixelX; + this.magnifyGlass.state.y = pixelY; + this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY); + this.magnifyGlass.updateMagnifiedView(); + } + } + /** + * Update the initial position when magnifier is activated. + */ + updateInitialPosition() { + if (!this.magnifyGlass.litegraphCanvas) return; + const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); + const clientX = this.magnifyGlass.lastKnownMousePosition.x; + const clientY = this.magnifyGlass.lastKnownMousePosition.y; + const isOverCanvas = clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom; + if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) { + const cssMouseXOnCanvas = clientX - rect.left; + const cssMouseYOnCanvas = clientY - rect.top; + const canvasElement = this.magnifyGlass.litegraphCanvas; + const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; + const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; + const pixelX = cssMouseXOnCanvas * scaleX; + const pixelY = cssMouseYOnCanvas * scaleY; + this.magnifyGlass.state.x = pixelX; + this.magnifyGlass.state.y = pixelY; + const glassSize = this.magnifyGlass.config.glassSize; + if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) { + this.magnifyGlass.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`; + this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_PADDING}px`; + this.magnifyGlass.state.wasActivatedBefore = true; + } else { + this.magnifyGlass.ui.positionGlass(clientX, clientY); + } + this.magnifyGlass.updateMagnifiedView(); + } + } } +export { + EventHandler +}; +//# sourceMappingURL=EventHandler.js.map diff --git a/web/magnify-glass/EventHandler.js.map b/web/magnify-glass/EventHandler.js.map new file mode 100644 index 0000000..2197d1d --- /dev/null +++ b/web/magnify-glass/EventHandler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"EventHandler.js","sources":["../../src/magnify-glass/EventHandler.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - EventHandler (TypeScript)\n * \n * Handles keyboard and mouse events for the magnifying glass.\n */\n\nimport { isUserTyping } from '../shared/utils';\nimport { DEFAULT_PADDING } from '../shared/constants';\nimport type { MagnifyGlass } from './MagnifyGlass';\n\n/**\n * Event Handler class.\n * Manages all keyboard and mouse event handling for the magnifier.\n */\nexport class EventHandler {\n magnifyGlass: MagnifyGlass;\n\n constructor(magnifyGlass: MagnifyGlass) {\n this.magnifyGlass = magnifyGlass;\n\n // Bind methods to maintain context\n this.handleKeyDown = this.handleKeyDown.bind(this);\n this.handleKeyUp = this.handleKeyUp.bind(this);\n this.handleMouseMove = this.handleMouseMove.bind(this);\n }\n\n /**\n * Attach all event listeners.\n */\n attachListeners(): void {\n document.addEventListener(\"keydown\", this.handleKeyDown);\n document.addEventListener(\"keyup\", this.handleKeyUp);\n document.addEventListener(\"mousemove\", this.handleMouseMove);\n }\n\n /**\n * Detach all event listeners.\n */\n detachListeners(): void {\n document.removeEventListener(\"keydown\", this.handleKeyDown);\n document.removeEventListener(\"keyup\", this.handleKeyUp);\n document.removeEventListener(\"mousemove\", this.handleMouseMove);\n }\n\n /**\n * Handle keydown events.\n */\n handleKeyDown(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n // Don't handle hotkeys if user is typing in an input field\n if (isUserTyping()) {\n return;\n }\n\n // Magnifier activation\n console.log(`[MagnifyGlass] KeyDown: ${e.key} (config: ${config.activationKey}, active: ${state.active})`);\n if (e.key.toLowerCase() === config.activationKey &&\n (!config.altRequired || e.altKey)) {\n console.log(\"[MagnifyGlass] Activation key matched!\");\n if (config.alwaysActiveMode) {\n if (state.active) {\n state.active = false;\n this.magnifyGlass.ui.hide();\n } else {\n state.active = true;\n this.magnifyGlass.ui.show();\n this.updateInitialPosition();\n }\n } else {\n if (!state.active) {\n state.active = true;\n this.magnifyGlass.ui.show();\n this.updateInitialPosition();\n }\n }\n }\n\n // Manual offset adjustment keys (only when magnifier is active)\n if (state.active) {\n let offsetChanged = false;\n const stepSize = e.shiftKey ? config.offsetStep * 5 : config.offsetStep;\n\n if (e.key === \"ArrowUp\") {\n config.offsetY -= stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowDown\") {\n config.offsetY += stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowLeft\") {\n config.offsetX -= stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowRight\") {\n config.offsetX += stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() &&\n (!config.altRequired || e.altKey)) {\n this.magnifyGlass.resetOffsets();\n offsetChanged = true;\n e.preventDefault();\n }\n\n // Toggle Follow Cursor Key\n if (e.key.toLowerCase() === config.toggleFollowCursorKey &&\n (!config.altRequired || e.altKey)) {\n config.followCursor = !config.followCursor;\n this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? 'ON' : 'OFF'}`);\n if (state.active && config.followCursor) {\n this.magnifyGlass.ui.positionGlass(\n this.magnifyGlass.lastKnownMousePosition.x,\n this.magnifyGlass.lastKnownMousePosition.y\n );\n }\n e.preventDefault();\n }\n\n if (offsetChanged) {\n config.saveOffsets();\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n }\n\n /**\n * Handle keyup events.\n */\n handleKeyUp(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n if (!config.alwaysActiveMode &&\n (e.key.toLowerCase() === config.activationKey ||\n (config.altRequired && e.key === \"Alt\"))) {\n\n let shouldDeactivate = false;\n if (config.altRequired) {\n if (e.key === \"Alt\" || e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n } else {\n if (e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n }\n\n if (state.active && shouldDeactivate) {\n state.active = false;\n this.magnifyGlass.ui.hide();\n }\n }\n }\n\n /**\n * Handle mousemove events.\n */\n handleMouseMove(e: MouseEvent): void {\n // Always track mouse position\n this.magnifyGlass.lastKnownMousePosition.x = e.clientX;\n this.magnifyGlass.lastKnownMousePosition.y = e.clientY;\n\n if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const cssMouseXOnCanvas = e.clientX - rect.left;\n const cssMouseYOnCanvas = e.clientY - rect.top;\n\n // Check if the cursor is over the canvas element\n if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width &&\n cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) {\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY);\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n\n /**\n * Update the initial position when magnifier is activated.\n */\n updateInitialPosition(): void {\n if (!this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const clientX = this.magnifyGlass.lastKnownMousePosition.x;\n const clientY = this.magnifyGlass.lastKnownMousePosition.y;\n\n const isOverCanvas = clientX >= rect.left && clientX <= rect.right &&\n clientY >= rect.top && clientY <= rect.bottom;\n\n if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) {\n const cssMouseXOnCanvas = clientX - rect.left;\n const cssMouseYOnCanvas = clientY - rect.top;\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n const glassSize = this.magnifyGlass.config.glassSize;\n\n if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) {\n // First activation - position at top right of the window\n this.magnifyGlass.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`;\n this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_PADDING}px`;\n this.magnifyGlass.state.wasActivatedBefore = true;\n } else {\n this.magnifyGlass.ui.positionGlass(clientX, clientY);\n }\n\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/MagnifierState.js b/web/magnify-glass/MagnifierState.js index 830eecc..a418b20 100644 --- a/web/magnify-glass/MagnifierState.js +++ b/web/magnify-glass/MagnifierState.js @@ -1,44 +1,64 @@ -/** - * ComfyUI MagnifyGlass - MagnifierState - * - * Manages the state of the magnifying glass. - */ - -/** - * Magnifier state class. - * Tracks activation state, cursor position, and canvas transforms. - */ -export class MagnifierState { - constructor() { - this.active = false; - this.wasActivatedBefore = false; // Track if the glass has been activated before - this.x = 0; // Cursor X relative to litegraphCanvas - this.y = 0; // Cursor Y relative to litegraphCanvas - this.sourceX = 0; // Calculated source area X - this.sourceY = 0; // Calculated source area Y - this.sourceWidth = 0; // Calculated source area width - this.sourceHeight = 0; // Calculated source area height - this.canvasScale = 1.0; // Current canvas scale/zoom - this.canvasOffsetX = 0; // Canvas translation X - this.canvasOffsetY = 0; // Canvas translation Y - this.isRenderScheduled = false; // Flag to manage requestAnimationFrame - } - - /** - * Reset state to initial values. - */ - reset() { - this.active = false; - this.wasActivatedBefore = false; - this.x = 0; - this.y = 0; - this.sourceX = 0; - this.sourceY = 0; - this.sourceWidth = 0; - this.sourceHeight = 0; - this.canvasScale = 1.0; - this.canvasOffsetX = 0; - this.canvasOffsetY = 0; - this.isRenderScheduled = false; - } +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +class MagnifierState { + constructor() { + /** Whether the magnifier is currently active */ + __publicField(this, "active"); + /** Track if the glass has been activated before */ + __publicField(this, "wasActivatedBefore"); + /** Cursor X relative to litegraphCanvas */ + __publicField(this, "x"); + /** Cursor Y relative to litegraphCanvas */ + __publicField(this, "y"); + /** Calculated source area X */ + __publicField(this, "sourceX"); + /** Calculated source area Y */ + __publicField(this, "sourceY"); + /** Calculated source area width */ + __publicField(this, "sourceWidth"); + /** Calculated source area height */ + __publicField(this, "sourceHeight"); + /** Current canvas scale/zoom */ + __publicField(this, "canvasScale"); + /** Canvas translation X */ + __publicField(this, "canvasOffsetX"); + /** Canvas translation Y */ + __publicField(this, "canvasOffsetY"); + /** Flag to manage requestAnimationFrame */ + __publicField(this, "isRenderScheduled"); + this.active = false; + this.wasActivatedBefore = false; + this.x = 0; + this.y = 0; + this.sourceX = 0; + this.sourceY = 0; + this.sourceWidth = 0; + this.sourceHeight = 0; + this.canvasScale = 1; + this.canvasOffsetX = 0; + this.canvasOffsetY = 0; + this.isRenderScheduled = false; + } + /** + * Reset state to initial values. + */ + reset() { + this.active = false; + this.wasActivatedBefore = false; + this.x = 0; + this.y = 0; + this.sourceX = 0; + this.sourceY = 0; + this.sourceWidth = 0; + this.sourceHeight = 0; + this.canvasScale = 1; + this.canvasOffsetX = 0; + this.canvasOffsetY = 0; + this.isRenderScheduled = false; + } } +export { + MagnifierState +}; +//# sourceMappingURL=MagnifierState.js.map diff --git a/web/magnify-glass/MagnifierState.js.map b/web/magnify-glass/MagnifierState.js.map new file mode 100644 index 0000000..a0e8d03 --- /dev/null +++ b/web/magnify-glass/MagnifierState.js.map @@ -0,0 +1 @@ +{"version":3,"file":"MagnifierState.js","sources":["../../src/magnify-glass/MagnifierState.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - MagnifierState (TypeScript)\n * \n * Manages the state of the magnifying glass.\n */\n\n/**\n * Magnifier state class.\n * Tracks activation state, cursor position, and canvas transforms.\n */\nexport class MagnifierState {\n /** Whether the magnifier is currently active */\n active: boolean;\n\n /** Track if the glass has been activated before */\n wasActivatedBefore: boolean;\n\n /** Cursor X relative to litegraphCanvas */\n x: number;\n\n /** Cursor Y relative to litegraphCanvas */\n y: number;\n\n /** Calculated source area X */\n sourceX: number;\n\n /** Calculated source area Y */\n sourceY: number;\n\n /** Calculated source area width */\n sourceWidth: number;\n\n /** Calculated source area height */\n sourceHeight: number;\n\n /** Current canvas scale/zoom */\n canvasScale: number;\n\n /** Canvas translation X */\n canvasOffsetX: number;\n\n /** Canvas translation Y */\n canvasOffsetY: number;\n\n /** Flag to manage requestAnimationFrame */\n isRenderScheduled: boolean;\n\n constructor() {\n this.active = false;\n this.wasActivatedBefore = false;\n this.x = 0;\n this.y = 0;\n this.sourceX = 0;\n this.sourceY = 0;\n this.sourceWidth = 0;\n this.sourceHeight = 0;\n this.canvasScale = 1.0;\n this.canvasOffsetX = 0;\n this.canvasOffsetY = 0;\n this.isRenderScheduled = false;\n }\n\n /**\n * Reset state to initial values.\n */\n reset(): void {\n this.active = false;\n this.wasActivatedBefore = false;\n this.x = 0;\n this.y = 0;\n this.sourceX = 0;\n this.sourceY = 0;\n this.sourceWidth = 0;\n this.sourceHeight = 0;\n this.canvasScale = 1.0;\n this.canvasOffsetX = 0;\n this.canvasOffsetY = 0;\n this.isRenderScheduled = false;\n }\n}\n"],"names":[],"mappings":";;;AAUO,MAAM,eAAe;AAAA,EAqCxB,cAAc;AAnCd;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGI,SAAK,SAAS;AACd,SAAK,qBAAqB;AAC1B,SAAK,IAAI;AACT,SAAK,IAAI;AACT,SAAK,UAAU;AACf,SAAK,UAAU;AACf,SAAK,cAAc;AACnB,SAAK,eAAe;AACpB,SAAK,cAAc;AACnB,SAAK,gBAAgB;AACrB,SAAK,gBAAgB;AACrB,SAAK,oBAAoB;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACV,SAAK,SAAS;AACd,SAAK,qBAAqB;AAC1B,SAAK,IAAI;AACT,SAAK,IAAI;AACT,SAAK,UAAU;AACf,SAAK,UAAU;AACf,SAAK,cAAc;AACnB,SAAK,eAAe;AACpB,SAAK,cAAc;AACnB,SAAK,gBAAgB;AACrB,SAAK,gBAAgB;AACrB,SAAK,oBAAoB;AAAA,EAC7B;AACJ;"} \ No newline at end of file diff --git a/web/magnify-glass/MagnifyGlass.js b/web/magnify-glass/MagnifyGlass.js index ddc5ee9..c2ae3a3 100644 --- a/web/magnify-glass/MagnifyGlass.js +++ b/web/magnify-glass/MagnifyGlass.js @@ -1,362 +1,322 @@ -/** - * ComfyUI MagnifyGlass - Main Class - * - * Orchestrates all components of the magnifying glass. - */ - -import { app } from "../../../../scripts/app.js"; -import { findLiteGraphCanvas, rectsOverlap } from '../shared/utils.js'; -import { DEFAULT_PADDING } from '../shared/constants.js'; -import { ConfigManager } from './ConfigManager.js'; -import { MagnifierState } from './MagnifierState.js'; -import { UiManager } from './UiManager.js'; -import { WebGLRenderer } from './WebGLRenderer.js'; -import { DebugManager } from './DebugManager.js'; -import { EventHandler } from './EventHandler.js'; - -/** - * Main MagnifyGlass class. - * Orchestrates all components and provides the main API. - */ -export class MagnifyGlass { - constructor() { - this.config = new ConfigManager(); - this.state = new MagnifierState(); - this.ui = new UiManager(this.config, this.state); - this.renderer = null; - this.debugger = new DebugManager(this.config, this.state, this.ui); - this.eventHandler = new EventHandler(this); - - // The LiteGraph canvas - this.litegraphCanvas = null; - - // Last known mouse position for better initial positioning - this.lastKnownMousePosition = { x: 0, y: 0 }; +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { findLiteGraphCanvas, rectsOverlap } from "../shared/utils.js"; +import { DEFAULT_PADDING } from "../shared/constants.js"; +import { registerGlassSettings } from "../shared/settings.js"; +import { ConfigManager } from "./ConfigManager.js"; +import { MagnifierState } from "./MagnifierState.js"; +import { UiManager } from "./UiManager.js"; +import { WebGLRenderer } from "./WebGLRenderer.js"; +import { DebugManager } from "./DebugManager.js"; +import { EventHandler } from "./EventHandler.js"; +class MagnifyGlass { + constructor() { + __publicField(this, "config"); + __publicField(this, "state"); + __publicField(this, "ui"); + __publicField(this, "renderer"); + __publicField(this, "debugger"); + __publicField(this, "eventHandler"); + /** The LiteGraph canvas */ + __publicField(this, "litegraphCanvas"); + /** Last known mouse position for better initial positioning */ + __publicField(this, "lastKnownMousePosition"); + /** Whether we're currently over media (for info panel) */ + __publicField(this, "isOverMedia"); + /** Current media element under cursor */ + __publicField(this, "currentMediaElement"); + this.config = new ConfigManager(); + this.state = new MagnifierState(); + this.ui = new UiManager(this.config, this.state); + this.renderer = null; + this.debugger = new DebugManager(this.config, this.state, this.ui); + this.eventHandler = new EventHandler(this); + this.litegraphCanvas = null; + this.lastKnownMousePosition = { x: 0, y: 0 }; + this.isOverMedia = false; + this.currentMediaElement = null; + } + /** + * Initialize the magnifying glass. + */ + init() { + if (typeof LiteGraph === "undefined" || typeof app === "undefined" || !app.canvas) { + this.debugger.log("LiteGraph or app not ready, retrying in 100ms."); + setTimeout(() => this.init(), 100); + return; } - - /** - * Initialize the magnifying glass. - */ - init() { - if (typeof LiteGraph === 'undefined' || typeof app === 'undefined' || !app.canvas) { - this.debugger.log("LiteGraph or app not ready, retrying in 100ms."); - setTimeout(() => this.init(), 100); - return; - } - this.debugger.log("LiteGraph and app ready."); - - // Load saved offsets first - this.config.loadSavedOffsets(); - - // Print detailed canvas information - this.debugger.printCanvasInfo(); - - // Create UI elements - this.ui.createElements(); - - // Setup WebGL renderer - this.renderer = new WebGLRenderer(this.config, this.state, this.ui); - if (!this.renderer.isValid()) { - this.ui.cleanup(); - return; - } - - // Find the LiteGraph canvas - this.litegraphCanvas = findLiteGraphCanvas(); - this.debugger.log("LiteGraph canvas found:", this.litegraphCanvas); - if (!this.litegraphCanvas) { - this.debugger.error("Could not find LiteGraph canvas. Magnifier will not work."); - this.ui.cleanup(); - return; - } - - // Attach event handlers - this.eventHandler.attachListeners(); - - this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? 'Alt+' : ''}${this.config.activationKey.toUpperCase()} to activate.`); + this.debugger.log("LiteGraph and app ready."); + this.config.loadSavedOffsets(); + this.debugger.printCanvasInfo(); + this.ui.createElements(); + this.renderer = new WebGLRenderer(this.config, this.state, this.ui); + if (!this.renderer.isValid()) { + this.ui.cleanup(); + return; } - - /** - * Update the magnified view. - */ - updateMagnifiedView() { + this.litegraphCanvas = findLiteGraphCanvas(); + this.debugger.log("LiteGraph canvas found:", this.litegraphCanvas); + if (!this.litegraphCanvas) { + this.debugger.error("Could not find LiteGraph canvas. Magnifier will not work."); + this.ui.cleanup(); + return; + } + this.eventHandler.attachListeners(); + registerGlassSettings(this); + this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? "Alt+" : ""}${this.config.activationKey.toUpperCase()} to activate.`); + } + /** + * Update the magnified view. + */ + updateMagnifiedView() { + if (!this.state.active || !this.renderer || !this.litegraphCanvas) { + return; + } + this.updateCanvasTransformation(); + this.calculateSourceRegion(); + if (!this.state.isRenderScheduled) { + this.state.isRenderScheduled = true; + requestAnimationFrame(() => { if (!this.state.active || !this.renderer || !this.litegraphCanvas) { - return; - } - - // Get canvas transformation info - this.updateCanvasTransformation(); - - // Calculate the source region - this.calculateSourceRegion(); - - // Schedule the rendering operation - if (!this.state.isRenderScheduled) { - this.state.isRenderScheduled = true; - requestAnimationFrame(() => { - if (!this.state.active || !this.renderer || !this.litegraphCanvas) { - this.state.isRenderScheduled = false; - return; - } - // Render the magnified view - this.renderer.render(this.litegraphCanvas); - - // Update debug visualization - this.debugger.updateDebugView(); - - // Render HTML overlays - this.renderHtmlOverlays(); - - this.state.isRenderScheduled = false; - }); + this.state.isRenderScheduled = false; + return; } + this.renderer.render(this.litegraphCanvas); + this.debugger.updateDebugView(); + this.renderHtmlOverlays(); + this.state.isRenderScheduled = false; + }); } - - /** - * Update canvas transformation state. - */ - updateCanvasTransformation() { - this.state.canvasScale = 1.0; - this.state.canvasOffsetX = 0; - this.state.canvasOffsetY = 0; - - if (app && app.canvas) { - if (app.canvas.ds) { - if (typeof app.canvas.ds.scale === 'number') { - this.state.canvasScale = app.canvas.ds.scale; - } - - if (app.canvas.ds.offset) { - this.state.canvasOffsetX = app.canvas.ds.offset[0] || 0; - this.state.canvasOffsetY = app.canvas.ds.offset[1] || 0; - } + } + /** + * Update canvas transformation state. + */ + updateCanvasTransformation() { + this.state.canvasScale = 1; + this.state.canvasOffsetX = 0; + this.state.canvasOffsetY = 0; + if (app == null ? void 0 : app.canvas) { + const ds = app.canvas.ds; + if (ds) { + if (typeof ds.scale === "number") { + this.state.canvasScale = ds.scale; + } + if (ds.offset) { + this.state.canvasOffsetX = ds.offset[0] || 0; + this.state.canvasOffsetY = ds.offset[1] || 0; + } + } + } + } + /** + * Calculate the source region for magnification. + */ + calculateSourceRegion() { + const cursorPixelX = this.state.x; + const cursorPixelY = this.state.y; + const canvasScale = this.state.canvasScale; + const canvasOffsetX = this.state.canvasOffsetX; + const canvasOffsetY = this.state.canvasOffsetY; + if (canvasScale === 0) return; + const cursorGraphX = (cursorPixelX - canvasOffsetX) / canvasScale; + const cursorGraphY = (cursorPixelY - canvasOffsetY) / canvasScale; + const targetGraphCenterX = cursorGraphX + this.config.offsetX; + const targetGraphCenterY = cursorGraphY + this.config.offsetY; + const sourceGraphWidth = this.config.glassSize / this.config.zoomFactor / canvasScale; + const sourceGraphHeight = this.config.glassSize / this.config.zoomFactor / canvasScale; + const sourceGraphX = targetGraphCenterX - sourceGraphWidth / 2; + const sourceGraphY = targetGraphCenterY - sourceGraphHeight / 2; + this.state.sourceX = sourceGraphX * canvasScale + canvasOffsetX; + this.state.sourceY = sourceGraphY * canvasScale + canvasOffsetY; + this.state.sourceWidth = sourceGraphWidth * canvasScale; + this.state.sourceHeight = sourceGraphHeight * canvasScale; + } + /** + * Render HTML overlays for text and media in the magnified view. + */ + renderHtmlOverlays() { + const graph = app.graph; + if (!this.state.active || !this.ui.htmlOverlayContainer || !graph || !this.litegraphCanvas) { + if (this.ui.htmlOverlayContainer) this.ui.htmlOverlayContainer.innerHTML = ""; + return; + } + this.ui.htmlOverlayContainer.innerHTML = ""; + const magnifyRect = { + x: this.state.sourceX, + y: this.state.sourceY, + width: this.state.sourceWidth, + height: this.state.sourceHeight + }; + const nodes = graph._nodes; + if (!nodes) return; + for (const node of nodes) { + const widgets = node.widgets; + if (!widgets) continue; + for (const widget of widgets) { + let isTextElement = false; + let isVideoElement = false; + let isImageElement = false; + let elementToProcess = null; + if (widget.element) { + const element = widget.element; + if (widget.type === "text" || widget.type === "string" || element.tagName === "TEXTAREA") { + isTextElement = true; + elementToProcess = element; + } else if (element.tagName === "VIDEO") { + isVideoElement = true; + elementToProcess = element; + } else if (element.tagName === "IMG") { + isImageElement = true; + elementToProcess = element; + } else { + const potentialVideo = element.querySelector("video"); + if (potentialVideo) { + isVideoElement = true; + elementToProcess = potentialVideo; + } else { + const potentialImage = element.querySelector("img"); + if (potentialImage) { + isImageElement = true; + elementToProcess = potentialImage; + } } + } } - } - - /** - * Calculate the source region for magnification. - */ - calculateSourceRegion() { - const cursorPixelX = this.state.x; - const cursorPixelY = this.state.y; - const canvasScale = this.state.canvasScale; - const canvasOffsetX = this.state.canvasOffsetX; - const canvasOffsetY = this.state.canvasOffsetY; - - if (canvasScale === 0) return; - - // Convert cursor canvas pixels to LiteGraph graph coordinates - const cursorGraphX = (cursorPixelX - canvasOffsetX) / canvasScale; - const cursorGraphY = (cursorPixelY - canvasOffsetY) / canvasScale; - - // Apply manual offset - const targetGraphCenterX = cursorGraphX + this.config.offsetX; - const targetGraphCenterY = cursorGraphY + this.config.offsetY; - - // Calculate source dimensions in graph units - const sourceGraphWidth = (this.config.glassSize / this.config.zoomFactor) / canvasScale; - const sourceGraphHeight = (this.config.glassSize / this.config.zoomFactor) / canvasScale; - - // Calculate source top-left corner in graph coordinates - const sourceGraphX = targetGraphCenterX - (sourceGraphWidth / 2); - const sourceGraphY = targetGraphCenterY - (sourceGraphHeight / 2); - - // Convert back to canvas pixel coordinates - this.state.sourceX = (sourceGraphX * canvasScale) + canvasOffsetX; - this.state.sourceY = (sourceGraphY * canvasScale) + canvasOffsetY; - this.state.sourceWidth = sourceGraphWidth * canvasScale; - this.state.sourceHeight = sourceGraphHeight * canvasScale; - } - - /** - * Render HTML overlays for text and media in the magnified view. - */ - renderHtmlOverlays() { - if (!this.state.active || !this.ui.htmlOverlayContainer || !app.graph || !this.litegraphCanvas) { - if (this.ui.htmlOverlayContainer) this.ui.htmlOverlayContainer.innerHTML = ''; - return; - } - - this.ui.htmlOverlayContainer.innerHTML = ''; - - const magnifyRect = { - x: this.state.sourceX, - y: this.state.sourceY, - width: this.state.sourceWidth, - height: this.state.sourceHeight - }; - - const nodes = app.graph._nodes; - if (!nodes) return; - - for (const node of nodes) { - if (!node.widgets) continue; - - for (const widget of node.widgets) { - let isTextElement = false; - let isVideoElement = false; - let isImageElement = false; - let elementToProcess = null; - - if (widget.element) { - if (widget.type === "text" || widget.type === "string" || widget.element.tagName === 'TEXTAREA') { - isTextElement = true; - elementToProcess = widget.element; - } else if (widget.element.tagName === 'VIDEO') { - isVideoElement = true; - elementToProcess = widget.element; - } else if (widget.element.tagName === 'IMG') { - isImageElement = true; - elementToProcess = widget.element; - } else { - const potentialVideo = widget.element.querySelector('video'); - if (potentialVideo) { - isVideoElement = true; - elementToProcess = potentialVideo; - } else { - const potentialImage = widget.element.querySelector('img'); - if (potentialImage) { - isImageElement = true; - elementToProcess = potentialImage; - } - } - } - } - - if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) { - const widgetRect = elementToProcess.getBoundingClientRect(); - const canvasRect = this.litegraphCanvas.getBoundingClientRect(); - - const canvasToViewportScaleX = canvasRect.width > 0 ? (this.litegraphCanvas.width / canvasRect.width) : 1; - const canvasToViewportScaleY = canvasRect.height > 0 ? (this.litegraphCanvas.height / canvasRect.height) : 1; - - const widgetCanvasX = (widgetRect.left - canvasRect.left) * canvasToViewportScaleX; - const widgetCanvasY = (widgetRect.top - canvasRect.top) * canvasToViewportScaleY; - const widgetCanvasWidth = widgetRect.width * canvasToViewportScaleX; - const widgetCanvasHeight = widgetRect.height * canvasToViewportScaleY; - - const widgetSourceRect = { - x: widgetCanvasX, - y: widgetCanvasY, - width: widgetCanvasWidth, - height: widgetCanvasHeight - }; - - if (rectsOverlap(magnifyRect, widgetSourceRect)) { - const clonedElement = elementToProcess.cloneNode(true); - clonedElement.style.position = 'absolute'; - clonedElement.style.pointerEvents = 'none'; - - if (isTextElement) { - clonedElement.style.backgroundColor = elementToProcess.style.backgroundColor || '#222'; - clonedElement.style.color = elementToProcess.style.color || '#DDD'; - clonedElement.style.border = elementToProcess.style.border || '1px solid #555'; - clonedElement.disabled = true; - } else if (isVideoElement) { - clonedElement.src = elementToProcess.src; - clonedElement.autoplay = elementToProcess.autoplay; - clonedElement.loop = elementToProcess.loop; - clonedElement.preload = elementToProcess.preload; - clonedElement.crossOrigin = elementToProcess.crossOrigin; - clonedElement.muted = true; - if (!elementToProcess.paused) { - clonedElement.play().catch(e => console.warn("Magnify Glass: Cloned video play failed", e)); - } - clonedElement.currentTime = elementToProcess.currentTime; - } else if (isImageElement) { - clonedElement.src = elementToProcess.src; - clonedElement.alt = elementToProcess.alt; - } - - const relativeX = widgetSourceRect.x - magnifyRect.x; - const relativeY = widgetSourceRect.y - magnifyRect.y; - const magnifiedX = relativeX * this.config.zoomFactor; - const magnifiedY = relativeY * this.config.zoomFactor; - - clonedElement.style.left = `${magnifiedX}px`; - clonedElement.style.top = `${magnifiedY}px`; - clonedElement.style.width = `${widgetSourceRect.width}px`; - clonedElement.style.height = `${widgetSourceRect.height}px`; - clonedElement.style.transformOrigin = 'top left'; - clonedElement.style.transform = `scale(${this.config.zoomFactor})`; - - if (isTextElement && clonedElement.style.fontSize) { - const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize); - clonedElement.style.fontSize = `${originalFontSize}px`; - } - - this.ui.htmlOverlayContainer.appendChild(clonedElement); - } - } + if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) { + const widgetRect = elementToProcess.getBoundingClientRect(); + const canvasRect = this.litegraphCanvas.getBoundingClientRect(); + const canvasToViewportScaleX = canvasRect.width > 0 ? this.litegraphCanvas.width / canvasRect.width : 1; + const canvasToViewportScaleY = canvasRect.height > 0 ? this.litegraphCanvas.height / canvasRect.height : 1; + const widgetCanvasX = (widgetRect.left - canvasRect.left) * canvasToViewportScaleX; + const widgetCanvasY = (widgetRect.top - canvasRect.top) * canvasToViewportScaleY; + const widgetCanvasWidth = widgetRect.width * canvasToViewportScaleX; + const widgetCanvasHeight = widgetRect.height * canvasToViewportScaleY; + const widgetSourceRect = { + x: widgetCanvasX, + y: widgetCanvasY, + width: widgetCanvasWidth, + height: widgetCanvasHeight + }; + if (rectsOverlap(magnifyRect, widgetSourceRect)) { + const clonedElement = elementToProcess.cloneNode(true); + clonedElement.style.position = "absolute"; + clonedElement.style.pointerEvents = "none"; + if (isTextElement) { + clonedElement.style.backgroundColor = elementToProcess.style.backgroundColor || "#222"; + clonedElement.style.color = elementToProcess.style.color || "#DDD"; + clonedElement.style.border = elementToProcess.style.border || "1px solid #555"; + clonedElement.disabled = true; + } else if (isVideoElement) { + const video = clonedElement; + const originalVideo = elementToProcess; + video.src = originalVideo.src; + video.autoplay = originalVideo.autoplay; + video.loop = originalVideo.loop; + video.preload = originalVideo.preload; + video.crossOrigin = originalVideo.crossOrigin; + video.muted = true; + if (!originalVideo.paused) { + video.play().catch((e) => console.warn("Magnify Glass: Cloned video play failed", e)); + } + video.currentTime = originalVideo.currentTime; + } else if (isImageElement) { + const img = clonedElement; + const originalImg = elementToProcess; + img.src = originalImg.src; + img.alt = originalImg.alt; } + const relativeX = widgetSourceRect.x - magnifyRect.x; + const relativeY = widgetSourceRect.y - magnifyRect.y; + const magnifiedX = relativeX * this.config.zoomFactor; + const magnifiedY = relativeY * this.config.zoomFactor; + clonedElement.style.left = `${magnifiedX}px`; + clonedElement.style.top = `${magnifiedY}px`; + clonedElement.style.width = `${widgetSourceRect.width}px`; + clonedElement.style.height = `${widgetSourceRect.height}px`; + clonedElement.style.transformOrigin = "top left"; + clonedElement.style.transform = `scale(${this.config.zoomFactor})`; + if (isTextElement && clonedElement.style.fontSize) { + const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize); + clonedElement.style.fontSize = `${originalFontSize}px`; + } + this.ui.htmlOverlayContainer.appendChild(clonedElement); + } } + } } - - /** - * Update config from current settings values. - */ - updateConfigFromSettings() { - this.config.loadSettings(); + } + /** + * Update config from current settings values. + */ + updateConfigFromSettings() { + this.config.loadSettings(); + } + /** + * Apply UI changes based on current config. + */ + applyUiChanges() { + var _a; + this.ui.applyStyles(); + if ((_a = this.renderer) == null ? void 0 : _a.gl) { + this.renderer.updateViewport(); } - - /** - * Apply UI changes based on current config. - */ - applyUiChanges() { - this.ui.applyStyles(); - - if (this.renderer && this.renderer.gl) { - this.renderer.updateViewport(); + } + /** + * Toggle the magnify glass on/off. + */ + toggle() { + this.state.active = !this.state.active; + if (this.state.active) { + this.ui.show(); + } else { + this.ui.hide(); + } + } + /** + * Reset offsets and panel positions to defaults. + */ + resetOffsets() { + this.config.resetOffsets(); + if (this.state.active) { + this.updateMagnifiedView(); + } + if (this.ui.glassDiv) { + const glassSize = this.config.glassSize; + this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`; + this.ui.glassDiv.style.top = `${DEFAULT_PADDING}px`; + this.state.wasActivatedBefore = false; + } + const extensions = window.comfyUIMagnifyGlassExtensions; + if (extensions && extensions.length > 0) { + extensions.forEach((extension) => { + var _a; + if (extension == null ? void 0 : extension.stateManager) { + extension.stateManager.state.isPanelPinned = false; + extension.stateManager.state.isPanelLocked = false; + extension.stateManager.state.isAutoPinned = false; + extension.stateManager.state.pinnedPosition = { x: 0, y: 0 }; + extension.stateManager.state.lastPinnedPosition = null; + if (extension.positionManager) { + extension.positionManager.positionPanel(); + } + if ((_a = extension.uiManager) == null ? void 0 : _a.updateControlStates) { + extension.uiManager.updateControlStates(); + } } + }); } - - /** - * Reset offsets and panel positions to defaults. - */ - resetOffsets() { - this.config.resetOffsets(); - - if (this.state.active) { - this.updateMagnifiedView(); - } - - // Reset magnify glass position to top-right corner - if (this.ui.glassDiv) { - const glassSize = this.config.glassSize; - this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`; - this.ui.glassDiv.style.top = `${DEFAULT_PADDING}px`; - this.state.wasActivatedBefore = false; - } - - // Reset inspector panel position if exists - if (window.comfyUIMagnifyGlassExtensions && window.comfyUIMagnifyGlassExtensions.length > 0) { - window.comfyUIMagnifyGlassExtensions.forEach(extension => { - if (extension && extension.stateManager) { - extension.stateManager.state.isPanelPinned = false; - extension.stateManager.state.isPanelLocked = false; - extension.stateManager.state.isAutoPinned = false; - extension.stateManager.state.pinnedPosition = { x: 0, y: 0 }; - extension.stateManager.state.lastPinnedPosition = null; - - if (extension.positionManager) { - extension.positionManager.positionPanel(); - } - if (extension.uiManager && extension.uiManager.updateControlStates) { - extension.uiManager.updateControlStates(); - } - } - }); - } - - this.debugger.log("Reset: Magnify glass and inspector panel positions restored to defaults"); - } - - /** - * Cleanup all resources. - */ - cleanup() { - this.eventHandler.detachListeners(); - this.ui.cleanup(); - } + this.debugger.log("Reset: Magnify glass and inspector panel positions restored to defaults"); + } + /** + * Cleanup all resources. + */ + cleanup() { + this.eventHandler.detachListeners(); + this.ui.cleanup(); + } } +export { + MagnifyGlass +}; +//# sourceMappingURL=MagnifyGlass.js.map diff --git a/web/magnify-glass/MagnifyGlass.js.map b/web/magnify-glass/MagnifyGlass.js.map new file mode 100644 index 0000000..9684373 --- /dev/null +++ b/web/magnify-glass/MagnifyGlass.js.map @@ -0,0 +1 @@ +{"version":3,"file":"MagnifyGlass.js","sources":["../../src/magnify-glass/MagnifyGlass.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Main Class (TypeScript)\n * \n * Orchestrates all components of the magnifying glass.\n */\n\nimport type { ComfyApp, ComfyNode } from '../types/comfyui';\nimport { findLiteGraphCanvas, rectsOverlap, Rectangle } from '../shared/utils';\nimport { DEFAULT_PADDING } from '../shared/constants';\nimport { registerGlassSettings } from '../shared/settings';\nimport { ConfigManager } from './ConfigManager';\nimport { MagnifierState } from './MagnifierState';\nimport { UiManager } from './UiManager';\nimport { WebGLRenderer } from './WebGLRenderer';\nimport { DebugManager } from './DebugManager';\nimport { EventHandler } from './EventHandler';\n\n// External ComfyUI globals\ndeclare const app: ComfyApp;\ndeclare const LiteGraph: unknown;\n\n/**\n * Main MagnifyGlass class.\n * Orchestrates all components and provides the main API.\n */\nexport class MagnifyGlass {\n config: ConfigManager;\n state: MagnifierState;\n ui: UiManager;\n renderer: WebGLRenderer | null;\n debugger: DebugManager;\n eventHandler: EventHandler;\n\n /** The LiteGraph canvas */\n litegraphCanvas: HTMLCanvasElement | null;\n\n /** Last known mouse position for better initial positioning */\n lastKnownMousePosition: { x: number; y: number };\n\n /** Whether we're currently over media (for info panel) */\n isOverMedia: boolean;\n\n /** Current media element under cursor */\n currentMediaElement: HTMLImageElement | HTMLVideoElement | null;\n\n constructor() {\n this.config = new ConfigManager();\n this.state = new MagnifierState();\n this.ui = new UiManager(this.config, this.state);\n this.renderer = null;\n this.debugger = new DebugManager(this.config, this.state, this.ui);\n this.eventHandler = new EventHandler(this);\n\n // The LiteGraph canvas\n this.litegraphCanvas = null;\n\n // Last known mouse position for better initial positioning\n this.lastKnownMousePosition = { x: 0, y: 0 };\n\n // Media tracking\n this.isOverMedia = false;\n this.currentMediaElement = null;\n }\n\n /**\n * Initialize the magnifying glass.\n */\n init(): void {\n if (typeof LiteGraph === 'undefined' || typeof app === 'undefined' || !app.canvas) {\n this.debugger.log(\"LiteGraph or app not ready, retrying in 100ms.\");\n setTimeout(() => this.init(), 100);\n return;\n }\n this.debugger.log(\"LiteGraph and app ready.\");\n\n // Load saved offsets first\n this.config.loadSavedOffsets();\n\n // Print detailed canvas information\n this.debugger.printCanvasInfo();\n\n // Create UI elements\n this.ui.createElements();\n\n // Setup WebGL renderer\n this.renderer = new WebGLRenderer(this.config, this.state, this.ui);\n if (!this.renderer.isValid()) {\n this.ui.cleanup();\n return;\n }\n\n // Find the LiteGraph canvas\n this.litegraphCanvas = findLiteGraphCanvas();\n this.debugger.log(\"LiteGraph canvas found:\", this.litegraphCanvas);\n if (!this.litegraphCanvas) {\n this.debugger.error(\"Could not find LiteGraph canvas. Magnifier will not work.\");\n this.ui.cleanup();\n return;\n }\n\n // Attach event handlers\n this.eventHandler.attachListeners();\n\n // Register settings with ComfyUI\n registerGlassSettings(this);\n\n this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? 'Alt+' : ''}${this.config.activationKey.toUpperCase()} to activate.`);\n }\n\n /**\n * Update the magnified view.\n */\n updateMagnifiedView(): void {\n if (!this.state.active || !this.renderer || !this.litegraphCanvas) {\n return;\n }\n\n // Get canvas transformation info\n this.updateCanvasTransformation();\n\n // Calculate the source region\n this.calculateSourceRegion();\n\n // Schedule the rendering operation\n if (!this.state.isRenderScheduled) {\n this.state.isRenderScheduled = true;\n requestAnimationFrame(() => {\n if (!this.state.active || !this.renderer || !this.litegraphCanvas) {\n this.state.isRenderScheduled = false;\n return;\n }\n // Render the magnified view\n this.renderer.render(this.litegraphCanvas);\n\n // Update debug visualization\n this.debugger.updateDebugView();\n\n // Render HTML overlays\n this.renderHtmlOverlays();\n\n this.state.isRenderScheduled = false;\n });\n }\n }\n\n /**\n * Update canvas transformation state.\n */\n updateCanvasTransformation(): void {\n this.state.canvasScale = 1.0;\n this.state.canvasOffsetX = 0;\n this.state.canvasOffsetY = 0;\n\n if (app?.canvas) {\n const ds = (app.canvas as any).ds;\n if (ds) {\n if (typeof ds.scale === 'number') {\n this.state.canvasScale = ds.scale;\n }\n\n if (ds.offset) {\n this.state.canvasOffsetX = ds.offset[0] || 0;\n this.state.canvasOffsetY = ds.offset[1] || 0;\n }\n }\n }\n }\n\n /**\n * Calculate the source region for magnification.\n */\n calculateSourceRegion(): void {\n const cursorPixelX = this.state.x;\n const cursorPixelY = this.state.y;\n const canvasScale = this.state.canvasScale;\n const canvasOffsetX = this.state.canvasOffsetX;\n const canvasOffsetY = this.state.canvasOffsetY;\n\n if (canvasScale === 0) return;\n\n // Convert cursor canvas pixels to LiteGraph graph coordinates\n const cursorGraphX = (cursorPixelX - canvasOffsetX) / canvasScale;\n const cursorGraphY = (cursorPixelY - canvasOffsetY) / canvasScale;\n\n // Apply manual offset\n const targetGraphCenterX = cursorGraphX + this.config.offsetX;\n const targetGraphCenterY = cursorGraphY + this.config.offsetY;\n\n // Calculate source dimensions in graph units\n const sourceGraphWidth = (this.config.glassSize / this.config.zoomFactor) / canvasScale;\n const sourceGraphHeight = (this.config.glassSize / this.config.zoomFactor) / canvasScale;\n\n // Calculate source top-left corner in graph coordinates\n const sourceGraphX = targetGraphCenterX - (sourceGraphWidth / 2);\n const sourceGraphY = targetGraphCenterY - (sourceGraphHeight / 2);\n\n // Convert back to canvas pixel coordinates\n this.state.sourceX = (sourceGraphX * canvasScale) + canvasOffsetX;\n this.state.sourceY = (sourceGraphY * canvasScale) + canvasOffsetY;\n this.state.sourceWidth = sourceGraphWidth * canvasScale;\n this.state.sourceHeight = sourceGraphHeight * canvasScale;\n }\n\n /**\n * Render HTML overlays for text and media in the magnified view.\n */\n renderHtmlOverlays(): void {\n const graph = (app as any).graph;\n if (!this.state.active || !this.ui.htmlOverlayContainer || !graph || !this.litegraphCanvas) {\n if (this.ui.htmlOverlayContainer) this.ui.htmlOverlayContainer.innerHTML = '';\n return;\n }\n\n this.ui.htmlOverlayContainer.innerHTML = '';\n\n const magnifyRect: Rectangle = {\n x: this.state.sourceX,\n y: this.state.sourceY,\n width: this.state.sourceWidth,\n height: this.state.sourceHeight\n };\n\n const nodes: ComfyNode[] = graph._nodes;\n if (!nodes) return;\n\n for (const node of nodes) {\n const widgets = (node as any).widgets;\n if (!widgets) continue;\n\n for (const widget of widgets) {\n let isTextElement = false;\n let isVideoElement = false;\n let isImageElement = false;\n let elementToProcess: HTMLElement | null = null;\n\n if (widget.element) {\n const element = widget.element as HTMLElement;\n if (widget.type === \"text\" || widget.type === \"string\" || element.tagName === 'TEXTAREA') {\n isTextElement = true;\n elementToProcess = element;\n } else if (element.tagName === 'VIDEO') {\n isVideoElement = true;\n elementToProcess = element;\n } else if (element.tagName === 'IMG') {\n isImageElement = true;\n elementToProcess = element;\n } else {\n const potentialVideo = element.querySelector('video');\n if (potentialVideo) {\n isVideoElement = true;\n elementToProcess = potentialVideo;\n } else {\n const potentialImage = element.querySelector('img');\n if (potentialImage) {\n isImageElement = true;\n elementToProcess = potentialImage;\n }\n }\n }\n }\n\n if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) {\n const widgetRect = elementToProcess.getBoundingClientRect();\n const canvasRect = this.litegraphCanvas!.getBoundingClientRect();\n\n const canvasToViewportScaleX = canvasRect.width > 0 ? (this.litegraphCanvas!.width / canvasRect.width) : 1;\n const canvasToViewportScaleY = canvasRect.height > 0 ? (this.litegraphCanvas!.height / canvasRect.height) : 1;\n\n const widgetCanvasX = (widgetRect.left - canvasRect.left) * canvasToViewportScaleX;\n const widgetCanvasY = (widgetRect.top - canvasRect.top) * canvasToViewportScaleY;\n const widgetCanvasWidth = widgetRect.width * canvasToViewportScaleX;\n const widgetCanvasHeight = widgetRect.height * canvasToViewportScaleY;\n\n const widgetSourceRect: Rectangle = {\n x: widgetCanvasX,\n y: widgetCanvasY,\n width: widgetCanvasWidth,\n height: widgetCanvasHeight\n };\n\n if (rectsOverlap(magnifyRect, widgetSourceRect)) {\n const clonedElement = elementToProcess.cloneNode(true) as HTMLElement;\n clonedElement.style.position = 'absolute';\n clonedElement.style.pointerEvents = 'none';\n\n if (isTextElement) {\n clonedElement.style.backgroundColor = elementToProcess.style.backgroundColor || '#222';\n clonedElement.style.color = elementToProcess.style.color || '#DDD';\n clonedElement.style.border = elementToProcess.style.border || '1px solid #555';\n (clonedElement as HTMLInputElement).disabled = true;\n } else if (isVideoElement) {\n const video = clonedElement as HTMLVideoElement;\n const originalVideo = elementToProcess as HTMLVideoElement;\n video.src = originalVideo.src;\n video.autoplay = originalVideo.autoplay;\n video.loop = originalVideo.loop;\n video.preload = originalVideo.preload;\n video.crossOrigin = originalVideo.crossOrigin;\n video.muted = true;\n if (!originalVideo.paused) {\n video.play().catch(e => console.warn(\"Magnify Glass: Cloned video play failed\", e));\n }\n video.currentTime = originalVideo.currentTime;\n } else if (isImageElement) {\n const img = clonedElement as HTMLImageElement;\n const originalImg = elementToProcess as HTMLImageElement;\n img.src = originalImg.src;\n img.alt = originalImg.alt;\n }\n\n const relativeX = widgetSourceRect.x - magnifyRect.x;\n const relativeY = widgetSourceRect.y - magnifyRect.y;\n const magnifiedX = relativeX * this.config.zoomFactor;\n const magnifiedY = relativeY * this.config.zoomFactor;\n\n clonedElement.style.left = `${magnifiedX}px`;\n clonedElement.style.top = `${magnifiedY}px`;\n clonedElement.style.width = `${widgetSourceRect.width}px`;\n clonedElement.style.height = `${widgetSourceRect.height}px`;\n clonedElement.style.transformOrigin = 'top left';\n clonedElement.style.transform = `scale(${this.config.zoomFactor})`;\n\n if (isTextElement && clonedElement.style.fontSize) {\n const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize);\n clonedElement.style.fontSize = `${originalFontSize}px`;\n }\n\n this.ui.htmlOverlayContainer!.appendChild(clonedElement);\n }\n }\n }\n }\n }\n\n /**\n * Update config from current settings values.\n */\n updateConfigFromSettings(): void {\n this.config.loadSettings();\n }\n\n /**\n * Apply UI changes based on current config.\n */\n applyUiChanges(): void {\n this.ui.applyStyles();\n\n if (this.renderer?.gl) {\n this.renderer.updateViewport();\n }\n }\n\n /**\n * Toggle the magnify glass on/off.\n */\n toggle(): void {\n this.state.active = !this.state.active;\n if (this.state.active) {\n this.ui.show();\n } else {\n this.ui.hide();\n }\n }\n\n /**\n * Reset offsets and panel positions to defaults.\n */\n resetOffsets(): void {\n this.config.resetOffsets();\n\n if (this.state.active) {\n this.updateMagnifiedView();\n }\n\n // Reset magnify glass position to top-right corner\n if (this.ui.glassDiv) {\n const glassSize = this.config.glassSize;\n this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`;\n this.ui.glassDiv.style.top = `${DEFAULT_PADDING}px`;\n this.state.wasActivatedBefore = false;\n }\n\n // Reset inspector panel position if exists\n const extensions = window.comfyUIMagnifyGlassExtensions;\n if (extensions && extensions.length > 0) {\n extensions.forEach((extension: any) => {\n if (extension?.stateManager) {\n extension.stateManager.state.isPanelPinned = false;\n extension.stateManager.state.isPanelLocked = false;\n extension.stateManager.state.isAutoPinned = false;\n extension.stateManager.state.pinnedPosition = { x: 0, y: 0 };\n extension.stateManager.state.lastPinnedPosition = null;\n\n if (extension.positionManager) {\n extension.positionManager.positionPanel();\n }\n if (extension.uiManager?.updateControlStates) {\n extension.uiManager.updateControlStates();\n }\n }\n });\n }\n\n this.debugger.log(\"Reset: Magnify glass and inspector panel positions restored to defaults\");\n }\n\n /**\n * Cleanup all resources.\n */\n cleanup(): void {\n this.eventHandler.detachListeners();\n this.ui.cleanup();\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAyBO,MAAM,aAAa;AAAA,EAoBtB,cAAc;AAnBd;AACA;AACA;AACA;AACA;AACA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGA;AAAA;AAGI,SAAK,SAAS,IAAI,cAAA;AAClB,SAAK,QAAQ,IAAI,eAAA;AACjB,SAAK,KAAK,IAAI,UAAU,KAAK,QAAQ,KAAK,KAAK;AAC/C,SAAK,WAAW;AAChB,SAAK,WAAW,IAAI,aAAa,KAAK,QAAQ,KAAK,OAAO,KAAK,EAAE;AACjE,SAAK,eAAe,IAAI,aAAa,IAAI;AAGzC,SAAK,kBAAkB;AAGvB,SAAK,yBAAyB,EAAE,GAAG,GAAG,GAAG,EAAA;AAGzC,SAAK,cAAc;AACnB,SAAK,sBAAsB;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACT,QAAI,OAAO,cAAc,eAAe,OAAO,QAAQ,eAAe,CAAC,IAAI,QAAQ;AAC/E,WAAK,SAAS,IAAI,gDAAgD;AAClE,iBAAW,MAAM,KAAK,KAAA,GAAQ,GAAG;AACjC;AAAA,IACJ;AACA,SAAK,SAAS,IAAI,0BAA0B;AAG5C,SAAK,OAAO,iBAAA;AAGZ,SAAK,SAAS,gBAAA;AAGd,SAAK,GAAG,eAAA;AAGR,SAAK,WAAW,IAAI,cAAc,KAAK,QAAQ,KAAK,OAAO,KAAK,EAAE;AAClE,QAAI,CAAC,KAAK,SAAS,WAAW;AAC1B,WAAK,GAAG,QAAA;AACR;AAAA,IACJ;AAGA,SAAK,kBAAkB,oBAAA;AACvB,SAAK,SAAS,IAAI,2BAA2B,KAAK,eAAe;AACjE,QAAI,CAAC,KAAK,iBAAiB;AACvB,WAAK,SAAS,MAAM,2DAA2D;AAC/E,WAAK,GAAG,QAAA;AACR;AAAA,IACJ;AAGA,SAAK,aAAa,gBAAA;AAGlB,0BAAsB,IAAI;AAE1B,SAAK,SAAS,IAAI,yDAAyD,KAAK,OAAO,cAAc,SAAS,EAAE,GAAG,KAAK,OAAO,cAAc,YAAA,CAAa,eAAe;AAAA,EAC7K;AAAA;AAAA;AAAA;AAAA,EAKA,sBAA4B;AACxB,QAAI,CAAC,KAAK,MAAM,UAAU,CAAC,KAAK,YAAY,CAAC,KAAK,iBAAiB;AAC/D;AAAA,IACJ;AAGA,SAAK,2BAAA;AAGL,SAAK,sBAAA;AAGL,QAAI,CAAC,KAAK,MAAM,mBAAmB;AAC/B,WAAK,MAAM,oBAAoB;AAC/B,4BAAsB,MAAM;AACxB,YAAI,CAAC,KAAK,MAAM,UAAU,CAAC,KAAK,YAAY,CAAC,KAAK,iBAAiB;AAC/D,eAAK,MAAM,oBAAoB;AAC/B;AAAA,QACJ;AAEA,aAAK,SAAS,OAAO,KAAK,eAAe;AAGzC,aAAK,SAAS,gBAAA;AAGd,aAAK,mBAAA;AAEL,aAAK,MAAM,oBAAoB;AAAA,MACnC,CAAC;AAAA,IACL;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,6BAAmC;AAC/B,SAAK,MAAM,cAAc;AACzB,SAAK,MAAM,gBAAgB;AAC3B,SAAK,MAAM,gBAAgB;AAE3B,QAAI,2BAAK,QAAQ;AACb,YAAM,KAAM,IAAI,OAAe;AAC/B,UAAI,IAAI;AACJ,YAAI,OAAO,GAAG,UAAU,UAAU;AAC9B,eAAK,MAAM,cAAc,GAAG;AAAA,QAChC;AAEA,YAAI,GAAG,QAAQ;AACX,eAAK,MAAM,gBAAgB,GAAG,OAAO,CAAC,KAAK;AAC3C,eAAK,MAAM,gBAAgB,GAAG,OAAO,CAAC,KAAK;AAAA,QAC/C;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,wBAA8B;AAC1B,UAAM,eAAe,KAAK,MAAM;AAChC,UAAM,eAAe,KAAK,MAAM;AAChC,UAAM,cAAc,KAAK,MAAM;AAC/B,UAAM,gBAAgB,KAAK,MAAM;AACjC,UAAM,gBAAgB,KAAK,MAAM;AAEjC,QAAI,gBAAgB,EAAG;AAGvB,UAAM,gBAAgB,eAAe,iBAAiB;AACtD,UAAM,gBAAgB,eAAe,iBAAiB;AAGtD,UAAM,qBAAqB,eAAe,KAAK,OAAO;AACtD,UAAM,qBAAqB,eAAe,KAAK,OAAO;AAGtD,UAAM,mBAAoB,KAAK,OAAO,YAAY,KAAK,OAAO,aAAc;AAC5E,UAAM,oBAAqB,KAAK,OAAO,YAAY,KAAK,OAAO,aAAc;AAG7E,UAAM,eAAe,qBAAsB,mBAAmB;AAC9D,UAAM,eAAe,qBAAsB,oBAAoB;AAG/D,SAAK,MAAM,UAAW,eAAe,cAAe;AACpD,SAAK,MAAM,UAAW,eAAe,cAAe;AACpD,SAAK,MAAM,cAAc,mBAAmB;AAC5C,SAAK,MAAM,eAAe,oBAAoB;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA,EAKA,qBAA2B;AACvB,UAAM,QAAS,IAAY;AAC3B,QAAI,CAAC,KAAK,MAAM,UAAU,CAAC,KAAK,GAAG,wBAAwB,CAAC,SAAS,CAAC,KAAK,iBAAiB;AACxF,UAAI,KAAK,GAAG,qBAAsB,MAAK,GAAG,qBAAqB,YAAY;AAC3E;AAAA,IACJ;AAEA,SAAK,GAAG,qBAAqB,YAAY;AAEzC,UAAM,cAAyB;AAAA,MAC3B,GAAG,KAAK,MAAM;AAAA,MACd,GAAG,KAAK,MAAM;AAAA,MACd,OAAO,KAAK,MAAM;AAAA,MAClB,QAAQ,KAAK,MAAM;AAAA,IAAA;AAGvB,UAAM,QAAqB,MAAM;AACjC,QAAI,CAAC,MAAO;AAEZ,eAAW,QAAQ,OAAO;AACtB,YAAM,UAAW,KAAa;AAC9B,UAAI,CAAC,QAAS;AAEd,iBAAW,UAAU,SAAS;AAC1B,YAAI,gBAAgB;AACpB,YAAI,iBAAiB;AACrB,YAAI,iBAAiB;AACrB,YAAI,mBAAuC;AAE3C,YAAI,OAAO,SAAS;AAChB,gBAAM,UAAU,OAAO;AACvB,cAAI,OAAO,SAAS,UAAU,OAAO,SAAS,YAAY,QAAQ,YAAY,YAAY;AACtF,4BAAgB;AAChB,+BAAmB;AAAA,UACvB,WAAW,QAAQ,YAAY,SAAS;AACpC,6BAAiB;AACjB,+BAAmB;AAAA,UACvB,WAAW,QAAQ,YAAY,OAAO;AAClC,6BAAiB;AACjB,+BAAmB;AAAA,UACvB,OAAO;AACH,kBAAM,iBAAiB,QAAQ,cAAc,OAAO;AACpD,gBAAI,gBAAgB;AAChB,+BAAiB;AACjB,iCAAmB;AAAA,YACvB,OAAO;AACH,oBAAM,iBAAiB,QAAQ,cAAc,KAAK;AAClD,kBAAI,gBAAgB;AAChB,iCAAiB;AACjB,mCAAmB;AAAA,cACvB;AAAA,YACJ;AAAA,UACJ;AAAA,QACJ;AAEA,YAAI,qBAAqB,iBAAiB,kBAAkB,iBAAiB;AACzE,gBAAM,aAAa,iBAAiB,sBAAA;AACpC,gBAAM,aAAa,KAAK,gBAAiB,sBAAA;AAEzC,gBAAM,yBAAyB,WAAW,QAAQ,IAAK,KAAK,gBAAiB,QAAQ,WAAW,QAAS;AACzG,gBAAM,yBAAyB,WAAW,SAAS,IAAK,KAAK,gBAAiB,SAAS,WAAW,SAAU;AAE5G,gBAAM,iBAAiB,WAAW,OAAO,WAAW,QAAQ;AAC5D,gBAAM,iBAAiB,WAAW,MAAM,WAAW,OAAO;AAC1D,gBAAM,oBAAoB,WAAW,QAAQ;AAC7C,gBAAM,qBAAqB,WAAW,SAAS;AAE/C,gBAAM,mBAA8B;AAAA,YAChC,GAAG;AAAA,YACH,GAAG;AAAA,YACH,OAAO;AAAA,YACP,QAAQ;AAAA,UAAA;AAGZ,cAAI,aAAa,aAAa,gBAAgB,GAAG;AAC7C,kBAAM,gBAAgB,iBAAiB,UAAU,IAAI;AACrD,0BAAc,MAAM,WAAW;AAC/B,0BAAc,MAAM,gBAAgB;AAEpC,gBAAI,eAAe;AACf,4BAAc,MAAM,kBAAkB,iBAAiB,MAAM,mBAAmB;AAChF,4BAAc,MAAM,QAAQ,iBAAiB,MAAM,SAAS;AAC5D,4BAAc,MAAM,SAAS,iBAAiB,MAAM,UAAU;AAC7D,4BAAmC,WAAW;AAAA,YACnD,WAAW,gBAAgB;AACvB,oBAAM,QAAQ;AACd,oBAAM,gBAAgB;AACtB,oBAAM,MAAM,cAAc;AAC1B,oBAAM,WAAW,cAAc;AAC/B,oBAAM,OAAO,cAAc;AAC3B,oBAAM,UAAU,cAAc;AAC9B,oBAAM,cAAc,cAAc;AAClC,oBAAM,QAAQ;AACd,kBAAI,CAAC,cAAc,QAAQ;AACvB,sBAAM,OAAO,MAAM,CAAA,MAAK,QAAQ,KAAK,2CAA2C,CAAC,CAAC;AAAA,cACtF;AACA,oBAAM,cAAc,cAAc;AAAA,YACtC,WAAW,gBAAgB;AACvB,oBAAM,MAAM;AACZ,oBAAM,cAAc;AACpB,kBAAI,MAAM,YAAY;AACtB,kBAAI,MAAM,YAAY;AAAA,YAC1B;AAEA,kBAAM,YAAY,iBAAiB,IAAI,YAAY;AACnD,kBAAM,YAAY,iBAAiB,IAAI,YAAY;AACnD,kBAAM,aAAa,YAAY,KAAK,OAAO;AAC3C,kBAAM,aAAa,YAAY,KAAK,OAAO;AAE3C,0BAAc,MAAM,OAAO,GAAG,UAAU;AACxC,0BAAc,MAAM,MAAM,GAAG,UAAU;AACvC,0BAAc,MAAM,QAAQ,GAAG,iBAAiB,KAAK;AACrD,0BAAc,MAAM,SAAS,GAAG,iBAAiB,MAAM;AACvD,0BAAc,MAAM,kBAAkB;AACtC,0BAAc,MAAM,YAAY,SAAS,KAAK,OAAO,UAAU;AAE/D,gBAAI,iBAAiB,cAAc,MAAM,UAAU;AAC/C,oBAAM,mBAAmB,WAAW,OAAO,iBAAiB,gBAAgB,EAAE,QAAQ;AACtF,4BAAc,MAAM,WAAW,GAAG,gBAAgB;AAAA,YACtD;AAEA,iBAAK,GAAG,qBAAsB,YAAY,aAAa;AAAA,UAC3D;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,2BAAiC;AAC7B,SAAK,OAAO,aAAA;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAuB;;AACnB,SAAK,GAAG,YAAA;AAER,SAAI,UAAK,aAAL,mBAAe,IAAI;AACnB,WAAK,SAAS,eAAA;AAAA,IAClB;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,SAAe;AACX,SAAK,MAAM,SAAS,CAAC,KAAK,MAAM;AAChC,QAAI,KAAK,MAAM,QAAQ;AACnB,WAAK,GAAG,KAAA;AAAA,IACZ,OAAO;AACH,WAAK,GAAG,KAAA;AAAA,IACZ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,eAAqB;AACjB,SAAK,OAAO,aAAA;AAEZ,QAAI,KAAK,MAAM,QAAQ;AACnB,WAAK,oBAAA;AAAA,IACT;AAGA,QAAI,KAAK,GAAG,UAAU;AAClB,YAAM,YAAY,KAAK,OAAO;AAC9B,WAAK,GAAG,SAAS,MAAM,OAAO,GAAG,OAAO,aAAa,YAAY,eAAe;AAChF,WAAK,GAAG,SAAS,MAAM,MAAM,GAAG,eAAe;AAC/C,WAAK,MAAM,qBAAqB;AAAA,IACpC;AAGA,UAAM,aAAa,OAAO;AAC1B,QAAI,cAAc,WAAW,SAAS,GAAG;AACrC,iBAAW,QAAQ,CAAC,cAAmB;;AACnC,YAAI,uCAAW,cAAc;AACzB,oBAAU,aAAa,MAAM,gBAAgB;AAC7C,oBAAU,aAAa,MAAM,gBAAgB;AAC7C,oBAAU,aAAa,MAAM,eAAe;AAC5C,oBAAU,aAAa,MAAM,iBAAiB,EAAE,GAAG,GAAG,GAAG,EAAA;AACzD,oBAAU,aAAa,MAAM,qBAAqB;AAElD,cAAI,UAAU,iBAAiB;AAC3B,sBAAU,gBAAgB,cAAA;AAAA,UAC9B;AACA,eAAI,eAAU,cAAV,mBAAqB,qBAAqB;AAC1C,sBAAU,UAAU,oBAAA;AAAA,UACxB;AAAA,QACJ;AAAA,MACJ,CAAC;AAAA,IACL;AAEA,SAAK,SAAS,IAAI,yEAAyE;AAAA,EAC/F;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACZ,SAAK,aAAa,gBAAA;AAClB,SAAK,GAAG,QAAA;AAAA,EACZ;AACJ;"} \ No newline at end of file diff --git a/web/magnify-glass/UiManager.js b/web/magnify-glass/UiManager.js index 6e886ef..081de12 100644 --- a/web/magnify-glass/UiManager.js +++ b/web/magnify-glass/UiManager.js @@ -1,43 +1,36 @@ -/** - * ComfyUI MagnifyGlass - UiManager - * - * Handles creation and management of DOM elements for the magnifying glass. - */ - -import { Z_INDEX, DEFAULT_PADDING } from '../shared/constants.js'; - -/** - * UI Manager class. - * Manages all DOM elements for the magnifying glass. - */ -export class UiManager { - /** - * @param {import('./ConfigManager.js').ConfigManager} config - * @param {import('./MagnifierState.js').MagnifierState} state - */ - constructor(config, state) { - this.config = config; - this.state = state; - this.glassDiv = null; - this.glassCanvas = null; - this.debugCanvas = null; - this.debugCtx = null; - this.htmlOverlayContainer = null; - } - - /** - * Create all DOM elements for the magnifying glass. - */ - createElements() { - // Create magnifying glass container - this.glassDiv = document.createElement("div"); - this.glassDiv.id = "comfyui-magnify-glass"; - this.glassDiv.style.cssText = ` +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { Z_INDEX, DEFAULT_PADDING } from "../shared/constants.js"; +class UiManager { + constructor(config, state) { + __publicField(this, "config"); + __publicField(this, "state"); + __publicField(this, "glassDiv"); + __publicField(this, "glassCanvas"); + __publicField(this, "debugCanvas"); + __publicField(this, "debugCtx"); + __publicField(this, "htmlOverlayContainer"); + this.config = config; + this.state = state; + this.glassDiv = null; + this.glassCanvas = null; + this.debugCanvas = null; + this.debugCtx = null; + this.htmlOverlayContainer = null; + } + /** + * Create all DOM elements for the magnifying glass. + */ + createElements() { + this.glassDiv = document.createElement("div"); + this.glassDiv.id = "comfyui-magnify-glass"; + this.glassDiv.style.cssText = ` position: absolute; width: ${this.config.glassSize}px; height: ${this.config.glassSize}px; border-radius: ${this.config.glassShape === "Circle" ? "50%" : "0px"}; - border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : 'none'}; + border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : "none"}; overflow: hidden; pointer-events: none; z-index: ${Z_INDEX.GLASS}; @@ -45,18 +38,14 @@ export class UiManager { box-shadow: 0 5px 15px rgba(0,0,0,0.3); background-color: rgba(255,255,255,0.1); `; - - // Create WebGL canvas for the magnifying glass - this.glassCanvas = document.createElement("canvas"); - this.glassCanvas.width = this.config.glassSize; - this.glassCanvas.height = this.config.glassSize; - this.glassCanvas.id = "comfyui-magnify-canvas"; - this.glassDiv.appendChild(this.glassCanvas); - - // Create HTML overlay container - this.htmlOverlayContainer = document.createElement("div"); - this.htmlOverlayContainer.id = "comfyui-magnify-html-overlay"; - this.htmlOverlayContainer.style.cssText = ` + this.glassCanvas = document.createElement("canvas"); + this.glassCanvas.width = this.config.glassSize; + this.glassCanvas.height = this.config.glassSize; + this.glassCanvas.id = "comfyui-magnify-canvas"; + this.glassDiv.appendChild(this.glassCanvas); + this.htmlOverlayContainer = document.createElement("div"); + this.htmlOverlayContainer.id = "comfyui-magnify-html-overlay"; + this.htmlOverlayContainer.style.cssText = ` position: absolute; top: 0; left: 0; @@ -65,25 +54,21 @@ export class UiManager { pointer-events: none; overflow: hidden; `; - this.glassDiv.appendChild(this.htmlOverlayContainer); - - document.body.appendChild(this.glassDiv); - - // Create debug canvas if debug mode is enabled - if (this.config.debugMode) { - this.createDebugCanvas(); - } + this.glassDiv.appendChild(this.htmlOverlayContainer); + document.body.appendChild(this.glassDiv); + if (this.config.debugMode) { + this.createDebugCanvas(); } - - /** - * Create the debug canvas overlay. - */ - createDebugCanvas() { - this.debugCanvas = document.createElement("canvas"); - this.debugCanvas.id = "comfyui-magnify-debug"; - this.debugCanvas.width = 400; - this.debugCanvas.height = 350; - this.debugCanvas.style.cssText = ` + } + /** + * Create the debug canvas overlay. + */ + createDebugCanvas() { + this.debugCanvas = document.createElement("canvas"); + this.debugCanvas.id = "comfyui-magnify-debug"; + this.debugCanvas.width = 400; + this.debugCanvas.height = 350; + this.debugCanvas.style.cssText = ` position: fixed; top: 10px; right: 10px; @@ -95,172 +80,154 @@ export class UiManager { font-family: monospace; display: none; `; - document.body.appendChild(this.debugCanvas); - this.debugCtx = this.debugCanvas.getContext('2d'); + document.body.appendChild(this.debugCanvas); + this.debugCtx = this.debugCanvas.getContext("2d"); + } + /** + * Show the magnifying glass. + */ + show() { + if (this.glassDiv) { + this.glassDiv.style.display = "block"; } - - /** - * Show the magnifying glass. - */ - show() { - this.glassDiv.style.display = "block"; - if (this.config.debugMode && this.debugCanvas) { - this.debugCanvas.style.display = "block"; - } + if (this.config.debugMode && this.debugCanvas) { + this.debugCanvas.style.display = "block"; } - - /** - * Hide the magnifying glass. - */ - hide() { - this.glassDiv.style.display = "none"; - if (this.config.debugMode && this.debugCanvas) { - this.debugCanvas.style.display = "none"; - } - // Clear HTML overlays when hiding - if (this.htmlOverlayContainer) { - this.htmlOverlayContainer.innerHTML = ''; - } + } + /** + * Hide the magnifying glass. + */ + hide() { + if (this.glassDiv) { + this.glassDiv.style.display = "none"; } - - /** - * Position the glass relative to cursor. - * @param {number} clientX - Client X coordinate - * @param {number} clientY - Client Y coordinate - */ - positionGlass(clientX, clientY) { - if (!this.config.followCursor) return; - - const glassSize = this.config.glassSize; - const offsetAmount = DEFAULT_PADDING; - let newLeft, newTop; - - switch (this.config.glassPosition) { - case "Top": - newLeft = clientX - (glassSize / 2); - newTop = clientY - glassSize - offsetAmount; - break; - case "Bottom": - newLeft = clientX - (glassSize / 2); - newTop = clientY + offsetAmount; - break; - case "Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY - (glassSize / 2); - break; - case "Right": - newLeft = clientX + offsetAmount; - newTop = clientY - (glassSize / 2); - break; - case "Top-Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY - glassSize - offsetAmount; - break; - case "Top-Right": - newLeft = clientX + offsetAmount; - newTop = clientY - glassSize - offsetAmount; - break; - case "Bottom-Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY + offsetAmount; - break; - case "Bottom-Right": - newLeft = clientX + offsetAmount; - newTop = clientY + offsetAmount; - break; - default: - newLeft = clientX - (glassSize / 2); - newTop = clientY + offsetAmount; - break; - } - - this.glassDiv.style.left = `${newLeft}px`; - this.glassDiv.style.top = `${newTop}px`; - - this.adjustForBoundaries(clientX, clientY); + if (this.config.debugMode && this.debugCanvas) { + this.debugCanvas.style.display = "none"; } - - /** - * Adjust glass position to stay within viewport boundaries. - * @param {number} clientX - Client X coordinate - * @param {number} clientY - Client Y coordinate - */ - adjustForBoundaries(clientX, clientY) { - const glassRect = this.glassDiv.getBoundingClientRect(); - - // Check right boundary - if (glassRect.right > window.innerWidth) { - this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`; - } - - // Check left boundary - const currentRectLeft = this.glassDiv.getBoundingClientRect(); - if (currentRectLeft.left < 0) { - this.glassDiv.style.left = "10px"; - } - - // Check bottom boundary - if (glassRect.bottom > window.innerHeight) { - this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`; - } - - // Check top boundary - const currentRectTop = this.glassDiv.getBoundingClientRect(); - if (currentRectTop.top < 0) { - this.glassDiv.style.top = "10px"; - } + if (this.htmlOverlayContainer) { + this.htmlOverlayContainer.innerHTML = ""; } - - /** - * Apply current config to UI elements. - */ - applyStyles() { - if (this.glassDiv) { - this.glassDiv.style.width = `${this.config.glassSize}px`; - this.glassDiv.style.height = `${this.config.glassSize}px`; - this.glassDiv.style.border = this.config.borderEnabled - ? `${this.config.borderWidth}px solid ${this.config.borderColor}` - : 'none'; - - // Reset clip-path and set default border-radius - this.glassDiv.style.clipPath = 'none'; - this.glassDiv.style.borderRadius = '0px'; - - switch (this.config.glassShape) { - case "Circle": - this.glassDiv.style.borderRadius = "50%"; - break; - case "Square": - // borderRadius is already '0px' - break; - case "Rounded Square": - this.glassDiv.style.borderRadius = "20%"; - break; - default: - this.glassDiv.style.borderRadius = "50%"; - break; - } - } - - if (this.glassCanvas) { - this.glassCanvas.width = this.config.glassSize; - this.glassCanvas.height = this.config.glassSize; - } - - // Handle debug canvas show/hide - if (this.config.debugMode) { - if (!this.debugCanvas) this.createDebugCanvas(); - if (this.state.active) this.debugCanvas.style.display = "block"; - } else { - if (this.debugCanvas) this.debugCanvas.style.display = "none"; - } + } + /** + * Position the glass relative to cursor. + * @param clientX - Client X coordinate + * @param clientY - Client Y coordinate + */ + positionGlass(clientX, clientY) { + if (!this.config.followCursor || !this.glassDiv) return; + const glassSize = this.config.glassSize; + const offsetAmount = DEFAULT_PADDING; + let newLeft; + let newTop; + switch (this.config.glassPosition) { + case "Top": + newLeft = clientX - glassSize / 2; + newTop = clientY - glassSize - offsetAmount; + break; + case "Bottom": + newLeft = clientX - glassSize / 2; + newTop = clientY + offsetAmount; + break; + case "Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY - glassSize / 2; + break; + case "Right": + newLeft = clientX + offsetAmount; + newTop = clientY - glassSize / 2; + break; + case "Top-Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY - glassSize - offsetAmount; + break; + case "Top-Right": + newLeft = clientX + offsetAmount; + newTop = clientY - glassSize - offsetAmount; + break; + case "Bottom-Left": + newLeft = clientX - glassSize - offsetAmount; + newTop = clientY + offsetAmount; + break; + case "Bottom-Right": + newLeft = clientX + offsetAmount; + newTop = clientY + offsetAmount; + break; + default: + newLeft = clientX - glassSize / 2; + newTop = clientY + offsetAmount; + break; } - - /** - * Cleanup DOM elements. - */ - cleanup() { - if (this.glassDiv) this.glassDiv.remove(); - if (this.debugCanvas) this.debugCanvas.remove(); + this.glassDiv.style.left = `${newLeft}px`; + this.glassDiv.style.top = `${newTop}px`; + this.adjustForBoundaries(clientX, clientY); + } + /** + * Adjust glass position to stay within viewport boundaries. + * @param clientX - Client X coordinate + * @param clientY - Client Y coordinate + */ + adjustForBoundaries(clientX, clientY) { + if (!this.glassDiv) return; + const glassRect = this.glassDiv.getBoundingClientRect(); + if (glassRect.right > window.innerWidth) { + this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`; } + const currentRectLeft = this.glassDiv.getBoundingClientRect(); + if (currentRectLeft.left < 0) { + this.glassDiv.style.left = "10px"; + } + if (glassRect.bottom > window.innerHeight) { + this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`; + } + const currentRectTop = this.glassDiv.getBoundingClientRect(); + if (currentRectTop.top < 0) { + this.glassDiv.style.top = "10px"; + } + } + /** + * Apply current config to UI elements. + */ + applyStyles() { + if (this.glassDiv) { + this.glassDiv.style.width = `${this.config.glassSize}px`; + this.glassDiv.style.height = `${this.config.glassSize}px`; + this.glassDiv.style.border = this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : "none"; + this.glassDiv.style.clipPath = "none"; + this.glassDiv.style.borderRadius = "0px"; + switch (this.config.glassShape) { + case "Circle": + this.glassDiv.style.borderRadius = "50%"; + break; + case "Square": + break; + case "Rounded Square": + this.glassDiv.style.borderRadius = "20%"; + break; + default: + this.glassDiv.style.borderRadius = "50%"; + break; + } + } + if (this.glassCanvas) { + this.glassCanvas.width = this.config.glassSize; + this.glassCanvas.height = this.config.glassSize; + } + if (this.config.debugMode) { + if (!this.debugCanvas) this.createDebugCanvas(); + if (this.state.active && this.debugCanvas) this.debugCanvas.style.display = "block"; + } else { + if (this.debugCanvas) this.debugCanvas.style.display = "none"; + } + } + /** + * Cleanup DOM elements. + */ + cleanup() { + if (this.glassDiv) this.glassDiv.remove(); + if (this.debugCanvas) this.debugCanvas.remove(); + } } +export { + UiManager +}; +//# sourceMappingURL=UiManager.js.map diff --git a/web/magnify-glass/UiManager.js.map b/web/magnify-glass/UiManager.js.map new file mode 100644 index 0000000..b9143f4 --- /dev/null +++ b/web/magnify-glass/UiManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"UiManager.js","sources":["../../src/magnify-glass/UiManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - UiManager (TypeScript)\n * \n * Handles creation and management of DOM elements for the magnifying glass.\n */\n\nimport { Z_INDEX, DEFAULT_PADDING } from '../shared/constants';\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\n\n/**\n * UI Manager class.\n * Manages all DOM elements for the magnifying glass.\n */\nexport class UiManager {\n config: ConfigManager;\n state: MagnifierState;\n glassDiv: HTMLDivElement | null;\n glassCanvas: HTMLCanvasElement | null;\n debugCanvas: HTMLCanvasElement | null;\n debugCtx: CanvasRenderingContext2D | null;\n htmlOverlayContainer: HTMLDivElement | null;\n\n constructor(config: ConfigManager, state: MagnifierState) {\n this.config = config;\n this.state = state;\n this.glassDiv = null;\n this.glassCanvas = null;\n this.debugCanvas = null;\n this.debugCtx = null;\n this.htmlOverlayContainer = null;\n }\n\n /**\n * Create all DOM elements for the magnifying glass.\n */\n createElements(): void {\n // Create magnifying glass container\n this.glassDiv = document.createElement(\"div\");\n this.glassDiv.id = \"comfyui-magnify-glass\";\n this.glassDiv.style.cssText = `\n position: absolute;\n width: ${this.config.glassSize}px;\n height: ${this.config.glassSize}px;\n border-radius: ${this.config.glassShape === \"Circle\" ? \"50%\" : \"0px\"};\n border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : 'none'};\n overflow: hidden;\n pointer-events: none;\n z-index: ${Z_INDEX.GLASS};\n display: none;\n box-shadow: 0 5px 15px rgba(0,0,0,0.3);\n background-color: rgba(255,255,255,0.1);\n `;\n\n // Create WebGL canvas for the magnifying glass\n this.glassCanvas = document.createElement(\"canvas\");\n this.glassCanvas.width = this.config.glassSize;\n this.glassCanvas.height = this.config.glassSize;\n this.glassCanvas.id = \"comfyui-magnify-canvas\";\n this.glassDiv.appendChild(this.glassCanvas);\n\n // Create HTML overlay container\n this.htmlOverlayContainer = document.createElement(\"div\");\n this.htmlOverlayContainer.id = \"comfyui-magnify-html-overlay\";\n this.htmlOverlayContainer.style.cssText = `\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n overflow: hidden; \n `;\n this.glassDiv.appendChild(this.htmlOverlayContainer);\n\n document.body.appendChild(this.glassDiv);\n\n // Create debug canvas if debug mode is enabled\n if (this.config.debugMode) {\n this.createDebugCanvas();\n }\n }\n\n /**\n * Create the debug canvas overlay.\n */\n createDebugCanvas(): void {\n this.debugCanvas = document.createElement(\"canvas\");\n this.debugCanvas.id = \"comfyui-magnify-debug\";\n this.debugCanvas.width = 400;\n this.debugCanvas.height = 350;\n this.debugCanvas.style.cssText = `\n position: fixed;\n top: 10px;\n right: 10px;\n background: rgba(0,0,0,0.7);\n border: 1px solid #fff;\n z-index: ${Z_INDEX.DEBUG};\n pointer-events: none;\n color: white;\n font-family: monospace;\n display: none;\n `;\n document.body.appendChild(this.debugCanvas);\n this.debugCtx = this.debugCanvas.getContext('2d');\n }\n\n /**\n * Show the magnifying glass.\n */\n show(): void {\n if (this.glassDiv) {\n this.glassDiv.style.display = \"block\";\n }\n if (this.config.debugMode && this.debugCanvas) {\n this.debugCanvas.style.display = \"block\";\n }\n }\n\n /**\n * Hide the magnifying glass.\n */\n hide(): void {\n if (this.glassDiv) {\n this.glassDiv.style.display = \"none\";\n }\n if (this.config.debugMode && this.debugCanvas) {\n this.debugCanvas.style.display = \"none\";\n }\n // Clear HTML overlays when hiding\n if (this.htmlOverlayContainer) {\n this.htmlOverlayContainer.innerHTML = '';\n }\n }\n\n /**\n * Position the glass relative to cursor.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n positionGlass(clientX: number, clientY: number): void {\n if (!this.config.followCursor || !this.glassDiv) return;\n\n const glassSize = this.config.glassSize;\n const offsetAmount = DEFAULT_PADDING;\n let newLeft: number;\n let newTop: number;\n\n switch (this.config.glassPosition) {\n case \"Top\":\n newLeft = clientX - (glassSize / 2);\n newTop = clientY - glassSize - offsetAmount;\n break;\n case \"Bottom\":\n newLeft = clientX - (glassSize / 2);\n newTop = clientY + offsetAmount;\n break;\n case \"Left\":\n newLeft = clientX - glassSize - offsetAmount;\n newTop = clientY - (glassSize / 2);\n break;\n case \"Right\":\n newLeft = clientX + offsetAmount;\n newTop = clientY - (glassSize / 2);\n break;\n case \"Top-Left\":\n newLeft = clientX - glassSize - offsetAmount;\n newTop = clientY - glassSize - offsetAmount;\n break;\n case \"Top-Right\":\n newLeft = clientX + offsetAmount;\n newTop = clientY - glassSize - offsetAmount;\n break;\n case \"Bottom-Left\":\n newLeft = clientX - glassSize - offsetAmount;\n newTop = clientY + offsetAmount;\n break;\n case \"Bottom-Right\":\n newLeft = clientX + offsetAmount;\n newTop = clientY + offsetAmount;\n break;\n default:\n newLeft = clientX - (glassSize / 2);\n newTop = clientY + offsetAmount;\n break;\n }\n\n this.glassDiv.style.left = `${newLeft}px`;\n this.glassDiv.style.top = `${newTop}px`;\n\n this.adjustForBoundaries(clientX, clientY);\n }\n\n /**\n * Adjust glass position to stay within viewport boundaries.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n adjustForBoundaries(clientX: number, clientY: number): void {\n if (!this.glassDiv) return;\n\n const glassRect = this.glassDiv.getBoundingClientRect();\n\n // Check right boundary\n if (glassRect.right > window.innerWidth) {\n this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`;\n }\n\n // Check left boundary\n const currentRectLeft = this.glassDiv.getBoundingClientRect();\n if (currentRectLeft.left < 0) {\n this.glassDiv.style.left = \"10px\";\n }\n\n // Check bottom boundary\n if (glassRect.bottom > window.innerHeight) {\n this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`;\n }\n\n // Check top boundary\n const currentRectTop = this.glassDiv.getBoundingClientRect();\n if (currentRectTop.top < 0) {\n this.glassDiv.style.top = \"10px\";\n }\n }\n\n /**\n * Apply current config to UI elements.\n */\n applyStyles(): void {\n if (this.glassDiv) {\n this.glassDiv.style.width = `${this.config.glassSize}px`;\n this.glassDiv.style.height = `${this.config.glassSize}px`;\n this.glassDiv.style.border = this.config.borderEnabled\n ? `${this.config.borderWidth}px solid ${this.config.borderColor}`\n : 'none';\n\n // Reset clip-path and set default border-radius\n this.glassDiv.style.clipPath = 'none';\n this.glassDiv.style.borderRadius = '0px';\n\n switch (this.config.glassShape) {\n case \"Circle\":\n this.glassDiv.style.borderRadius = \"50%\";\n break;\n case \"Square\":\n // borderRadius is already '0px'\n break;\n case \"Rounded Square\":\n this.glassDiv.style.borderRadius = \"20%\";\n break;\n default:\n this.glassDiv.style.borderRadius = \"50%\";\n break;\n }\n }\n\n if (this.glassCanvas) {\n this.glassCanvas.width = this.config.glassSize;\n this.glassCanvas.height = this.config.glassSize;\n }\n\n // Handle debug canvas show/hide\n if (this.config.debugMode) {\n if (!this.debugCanvas) this.createDebugCanvas();\n if (this.state.active && this.debugCanvas) this.debugCanvas.style.display = \"block\";\n } else {\n if (this.debugCanvas) this.debugCanvas.style.display = \"none\";\n }\n }\n\n /**\n * Cleanup DOM elements.\n */\n cleanup(): void {\n if (this.glassDiv) this.glassDiv.remove();\n if (this.debugCanvas) this.debugCanvas.remove();\n }\n}\n"],"names":[],"mappings":";;;;AAcO,MAAM,UAAU;AAAA,EASnB,YAAY,QAAuB,OAAuB;AAR1D;AACA;AACA;AACA;AACA;AACA;AACA;AAGI,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,WAAW;AAChB,SAAK,cAAc;AACnB,SAAK,cAAc;AACnB,SAAK,WAAW;AAChB,SAAK,uBAAuB;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAuB;AAEnB,SAAK,WAAW,SAAS,cAAc,KAAK;AAC5C,SAAK,SAAS,KAAK;AACnB,SAAK,SAAS,MAAM,UAAU;AAAA;AAAA,qBAEjB,KAAK,OAAO,SAAS;AAAA,sBACpB,KAAK,OAAO,SAAS;AAAA,6BACd,KAAK,OAAO,eAAe,WAAW,QAAQ,KAAK;AAAA,sBAC1D,KAAK,OAAO,gBAAgB,GAAG,KAAK,OAAO,WAAW,YAAY,KAAK,OAAO,WAAW,KAAK,MAAM;AAAA;AAAA;AAAA,uBAGnG,QAAQ,KAAK;AAAA;AAAA;AAAA;AAAA;AAO5B,SAAK,cAAc,SAAS,cAAc,QAAQ;AAClD,SAAK,YAAY,QAAQ,KAAK,OAAO;AACrC,SAAK,YAAY,SAAS,KAAK,OAAO;AACtC,SAAK,YAAY,KAAK;AACtB,SAAK,SAAS,YAAY,KAAK,WAAW;AAG1C,SAAK,uBAAuB,SAAS,cAAc,KAAK;AACxD,SAAK,qBAAqB,KAAK;AAC/B,SAAK,qBAAqB,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS1C,SAAK,SAAS,YAAY,KAAK,oBAAoB;AAEnD,aAAS,KAAK,YAAY,KAAK,QAAQ;AAGvC,QAAI,KAAK,OAAO,WAAW;AACvB,WAAK,kBAAA;AAAA,IACT;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,oBAA0B;AACtB,SAAK,cAAc,SAAS,cAAc,QAAQ;AAClD,SAAK,YAAY,KAAK;AACtB,SAAK,YAAY,QAAQ;AACzB,SAAK,YAAY,SAAS;AAC1B,SAAK,YAAY,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAMlB,QAAQ,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAM5B,aAAS,KAAK,YAAY,KAAK,WAAW;AAC1C,SAAK,WAAW,KAAK,YAAY,WAAW,IAAI;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACT,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,UAAU;AAAA,IAClC;AACA,QAAI,KAAK,OAAO,aAAa,KAAK,aAAa;AAC3C,WAAK,YAAY,MAAM,UAAU;AAAA,IACrC;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACT,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,UAAU;AAAA,IAClC;AACA,QAAI,KAAK,OAAO,aAAa,KAAK,aAAa;AAC3C,WAAK,YAAY,MAAM,UAAU;AAAA,IACrC;AAEA,QAAI,KAAK,sBAAsB;AAC3B,WAAK,qBAAqB,YAAY;AAAA,IAC1C;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,SAAiB,SAAuB;AAClD,QAAI,CAAC,KAAK,OAAO,gBAAgB,CAAC,KAAK,SAAU;AAEjD,UAAM,YAAY,KAAK,OAAO;AAC9B,UAAM,eAAe;AACrB,QAAI;AACJ,QAAI;AAEJ,YAAQ,KAAK,OAAO,eAAA;AAAA,MAChB,KAAK;AACD,kBAAU,UAAW,YAAY;AACjC,iBAAS,UAAU,YAAY;AAC/B;AAAA,MACJ,KAAK;AACD,kBAAU,UAAW,YAAY;AACjC,iBAAS,UAAU;AACnB;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU,YAAY;AAChC,iBAAS,UAAW,YAAY;AAChC;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU;AACpB,iBAAS,UAAW,YAAY;AAChC;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU,YAAY;AAChC,iBAAS,UAAU,YAAY;AAC/B;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU;AACpB,iBAAS,UAAU,YAAY;AAC/B;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU,YAAY;AAChC,iBAAS,UAAU;AACnB;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU;AACpB,iBAAS,UAAU;AACnB;AAAA,MACJ;AACI,kBAAU,UAAW,YAAY;AACjC,iBAAS,UAAU;AACnB;AAAA,IAAA;AAGR,SAAK,SAAS,MAAM,OAAO,GAAG,OAAO;AACrC,SAAK,SAAS,MAAM,MAAM,GAAG,MAAM;AAEnC,SAAK,oBAAoB,SAAS,OAAO;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,oBAAoB,SAAiB,SAAuB;AACxD,QAAI,CAAC,KAAK,SAAU;AAEpB,UAAM,YAAY,KAAK,SAAS,sBAAA;AAGhC,QAAI,UAAU,QAAQ,OAAO,YAAY;AACrC,WAAK,SAAS,MAAM,OAAO,GAAG,UAAU,UAAU,QAAQ,eAAe;AAAA,IAC7E;AAGA,UAAM,kBAAkB,KAAK,SAAS,sBAAA;AACtC,QAAI,gBAAgB,OAAO,GAAG;AAC1B,WAAK,SAAS,MAAM,OAAO;AAAA,IAC/B;AAGA,QAAI,UAAU,SAAS,OAAO,aAAa;AACvC,WAAK,SAAS,MAAM,MAAM,GAAG,UAAU,UAAU,SAAS,eAAe;AAAA,IAC7E;AAGA,UAAM,iBAAiB,KAAK,SAAS,sBAAA;AACrC,QAAI,eAAe,MAAM,GAAG;AACxB,WAAK,SAAS,MAAM,MAAM;AAAA,IAC9B;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,cAAoB;AAChB,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,QAAQ,GAAG,KAAK,OAAO,SAAS;AACpD,WAAK,SAAS,MAAM,SAAS,GAAG,KAAK,OAAO,SAAS;AACrD,WAAK,SAAS,MAAM,SAAS,KAAK,OAAO,gBACnC,GAAG,KAAK,OAAO,WAAW,YAAY,KAAK,OAAO,WAAW,KAC7D;AAGN,WAAK,SAAS,MAAM,WAAW;AAC/B,WAAK,SAAS,MAAM,eAAe;AAEnC,cAAQ,KAAK,OAAO,YAAA;AAAA,QAChB,KAAK;AACD,eAAK,SAAS,MAAM,eAAe;AACnC;AAAA,QACJ,KAAK;AAED;AAAA,QACJ,KAAK;AACD,eAAK,SAAS,MAAM,eAAe;AACnC;AAAA,QACJ;AACI,eAAK,SAAS,MAAM,eAAe;AACnC;AAAA,MAAA;AAAA,IAEZ;AAEA,QAAI,KAAK,aAAa;AAClB,WAAK,YAAY,QAAQ,KAAK,OAAO;AACrC,WAAK,YAAY,SAAS,KAAK,OAAO;AAAA,IAC1C;AAGA,QAAI,KAAK,OAAO,WAAW;AACvB,UAAI,CAAC,KAAK,YAAa,MAAK,kBAAA;AAC5B,UAAI,KAAK,MAAM,UAAU,KAAK,YAAa,MAAK,YAAY,MAAM,UAAU;AAAA,IAChF,OAAO;AACH,UAAI,KAAK,YAAa,MAAK,YAAY,MAAM,UAAU;AAAA,IAC3D;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACZ,QAAI,KAAK,SAAU,MAAK,SAAS,OAAA;AACjC,QAAI,KAAK,YAAa,MAAK,YAAY,OAAA;AAAA,EAC3C;AACJ;"} \ No newline at end of file diff --git a/web/magnify-glass/WebGLRenderer.js b/web/magnify-glass/WebGLRenderer.js index f543cb2..ffa00d7 100644 --- a/web/magnify-glass/WebGLRenderer.js +++ b/web/magnify-glass/WebGLRenderer.js @@ -1,34 +1,33 @@ -/** - * ComfyUI MagnifyGlass - WebGLRenderer - * - * Handles WebGL-based rendering of the magnified view. - */ - -/** - * WebGL Renderer class. - * Provides efficient WebGL-based magnification rendering. - */ -export class WebGLRenderer { - /** - * @param {import('./ConfigManager.js').ConfigManager} config - * @param {import('./MagnifierState.js').MagnifierState} state - * @param {import('./UiManager.js').UiManager} ui - */ - constructor(config, state, ui) { - this.config = config; - this.state = state; - this.ui = ui; - - this.gl = null; - this.program = null; - this.texture = null; - this.positionBuffer = null; - this.texCoordBuffer = null; - this.uniformLocations = null; - this.attributeLocations = null; - this.currentFilteringMode = null; - - this.vertexShaderSource = ` +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +class WebGLRenderer { + constructor(config, state, ui) { + __publicField(this, "config"); + __publicField(this, "state"); + __publicField(this, "ui"); + __publicField(this, "gl"); + __publicField(this, "program"); + __publicField(this, "texture"); + __publicField(this, "positionBuffer"); + __publicField(this, "texCoordBuffer"); + __publicField(this, "uniformLocations"); + __publicField(this, "attributeLocations"); + __publicField(this, "currentFilteringMode"); + __publicField(this, "vertexShaderSource"); + __publicField(this, "fragmentShaderSource"); + this.config = config; + this.state = state; + this.ui = ui; + this.gl = null; + this.program = null; + this.texture = null; + this.positionBuffer = null; + this.texCoordBuffer = null; + this.uniformLocations = null; + this.attributeLocations = null; + this.currentFilteringMode = null; + this.vertexShaderSource = ` attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; @@ -37,8 +36,7 @@ export class WebGLRenderer { v_texCoord = a_texCoord; } `; - - this.fragmentShaderSource = ` + this.fragmentShaderSource = ` precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_sourceTexture; @@ -52,205 +50,180 @@ export class WebGLRenderer { gl_FragColor = color; } `; - - this.initialize(); + this.initialize(); + } + /** + * Initialize WebGL context and resources. + */ + initialize() { + if (!this.ui.glassCanvas) return; + this.gl = this.ui.glassCanvas.getContext("webgl", { preserveDrawingBuffer: true }); + if (!this.gl) { + console.error("ComfyUI Magnifying Glass ERROR: WebGL not supported or context creation failed."); + return; } - - /** - * Initialize WebGL context and resources. - */ - initialize() { - // Get WebGL context - this.gl = this.ui.glassCanvas.getContext("webgl", { preserveDrawingBuffer: true }); - if (!this.gl) { - console.error("ComfyUI Magnifying Glass ERROR: WebGL not supported or context creation failed."); - return; - } - - // Create shader program - this.program = this.createShaderProgram(this.gl, this.vertexShaderSource, this.fragmentShaderSource); - if (!this.program) return; - - // Create position buffer - this.positionBuffer = this.gl.createBuffer(); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); - const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; - this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW); - - // Create texture coordinate buffer - this.texCoordBuffer = this.gl.createBuffer(); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); - const texCoords = [0, 1, 1, 1, 0, 0, 1, 0]; // Flipped Y for texCoords - this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW); - - // Create texture - this.texture = this.gl.createTexture(); - this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); - this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR); - this.updateTextureFiltering(this.config.textureFiltering); - - // Get uniform and attribute locations - this.uniformLocations = { - sourceTexture: this.gl.getUniformLocation(this.program, "u_sourceTexture"), - textureOffset: this.gl.getUniformLocation(this.program, "u_textureOffset"), - textureRepeat: this.gl.getUniformLocation(this.program, "u_textureRepeat"), - glassSize: this.gl.getUniformLocation(this.program, "u_glassSize"), - }; - - this.attributeLocations = { - position: this.gl.getAttribLocation(this.program, "a_position"), - texCoord: this.gl.getAttribLocation(this.program, "a_texCoord"), - }; + this.program = this.createShaderProgram(this.gl, this.vertexShaderSource, this.fragmentShaderSource); + if (!this.program) return; + this.positionBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); + const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; + this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW); + this.texCoordBuffer = this.gl.createBuffer(); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); + const texCoords = [0, 1, 1, 1, 0, 0, 1, 0]; + this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW); + this.texture = this.gl.createTexture(); + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR); + this.updateTextureFiltering(this.config.textureFiltering); + this.uniformLocations = { + sourceTexture: this.gl.getUniformLocation(this.program, "u_sourceTexture"), + textureOffset: this.gl.getUniformLocation(this.program, "u_textureOffset"), + textureRepeat: this.gl.getUniformLocation(this.program, "u_textureRepeat"), + glassSize: this.gl.getUniformLocation(this.program, "u_glassSize") + }; + this.attributeLocations = { + position: this.gl.getAttribLocation(this.program, "a_position"), + texCoord: this.gl.getAttribLocation(this.program, "a_texCoord") + }; + } + /** + * Update texture filtering mode. + * @param filteringModeString - "Linear" or "Nearest" + */ + updateTextureFiltering(filteringModeString) { + if (!this.gl || !this.texture) return; + let glFilterMode; + if (filteringModeString === "Nearest") { + glFilterMode = this.gl.NEAREST; + } else { + glFilterMode = this.gl.LINEAR; } - - /** - * Update texture filtering mode. - * @param {string} filteringModeString - "Linear" or "Nearest" - */ - updateTextureFiltering(filteringModeString) { - if (!this.gl) return; - - let glFilterMode; - if (filteringModeString === "Nearest") { - glFilterMode = this.gl.NEAREST; - } else { - glFilterMode = this.gl.LINEAR; - } - - if (this.currentFilteringMode === glFilterMode) return; - - this.currentFilteringMode = glFilterMode; - - this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, glFilterMode); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, glFilterMode); - this.gl.bindTexture(this.gl.TEXTURE_2D, null); + if (this.currentFilteringMode === glFilterMode) return; + this.currentFilteringMode = glFilterMode; + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, glFilterMode); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, glFilterMode); + this.gl.bindTexture(this.gl.TEXTURE_2D, null); + } + /** + * Check if renderer is valid and ready. + * @returns Boolean indicating if WebGL context and program are ready + */ + isValid() { + return this.gl !== null && this.program !== null; + } + /** + * Create a WebGL shader program. + * @param gl - WebGL context + * @param vsSource - Vertex shader source + * @param fsSource - Fragment shader source + * @returns WebGL program or null on failure + */ + createShaderProgram(gl, vsSource, fsSource) { + const vertexShader = gl.createShader(gl.VERTEX_SHADER); + if (!vertexShader) return null; + gl.shaderSource(vertexShader, vsSource); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + console.error("ComfyUI Magnifying Glass ERROR: Vertex shader compilation error:", gl.getShaderInfoLog(vertexShader)); + gl.deleteShader(vertexShader); + return null; } - - /** - * Check if renderer is valid and ready. - * @returns {boolean} - */ - isValid() { - return this.gl !== null && this.program !== null; + const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + if (!fragmentShader) { + gl.deleteShader(vertexShader); + return null; } - - /** - * Create a WebGL shader program. - * @param {WebGLRenderingContext} gl - * @param {string} vsSource - * @param {string} fsSource - * @returns {WebGLProgram|null} - */ - createShaderProgram(gl, vsSource, fsSource) { - const vertexShader = gl.createShader(gl.VERTEX_SHADER); - gl.shaderSource(vertexShader, vsSource); - gl.compileShader(vertexShader); - if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { - console.error('ComfyUI Magnifying Glass ERROR: Vertex shader compilation error:', gl.getShaderInfoLog(vertexShader)); - gl.deleteShader(vertexShader); - return null; - } - - const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); - gl.shaderSource(fragmentShader, fsSource); - gl.compileShader(fragmentShader); - if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { - console.error('ComfyUI Magnifying Glass ERROR: Fragment shader compilation error:', gl.getShaderInfoLog(fragmentShader)); - gl.deleteShader(vertexShader); - gl.deleteShader(fragmentShader); - return null; - } - - const program = gl.createProgram(); - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - console.error('ComfyUI Magnifying Glass ERROR: Shader program linking error:', gl.getProgramInfoLog(program)); - gl.deleteProgram(program); - gl.deleteShader(vertexShader); - gl.deleteShader(fragmentShader); - return null; - } - - return program; + gl.shaderSource(fragmentShader, fsSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + console.error("ComfyUI Magnifying Glass ERROR: Fragment shader compilation error:", gl.getShaderInfoLog(fragmentShader)); + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; } - - /** - * Render the magnified view. - * @param {HTMLCanvasElement} sourceCanvas - */ - render(sourceCanvas) { - // Bind texture and update with source canvas - this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); - - try { - this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceCanvas); - } catch (e) { - console.error("ComfyUI Magnifying Glass ERROR: Error in texImage2D:", e); - return; - } - - // Calculate normalized texture coordinates (UV space: 0-1) - const uvX = this.state.sourceX / sourceCanvas.width; - const uvY = this.state.sourceY / sourceCanvas.height; - const uvWidth = this.state.sourceWidth / sourceCanvas.width; - const uvHeight = this.state.sourceHeight / sourceCanvas.height; - - // Set up rendering - this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); - this.gl.clearColor(0.0, 0.0, 0.0, 0.0); - this.gl.clear(this.gl.COLOR_BUFFER_BIT); - - this.gl.useProgram(this.program); - - // Set uniforms - this.gl.activeTexture(this.gl.TEXTURE0); - this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); - this.gl.uniform1i(this.uniformLocations.sourceTexture, 0); - this.gl.uniform2f(this.uniformLocations.textureOffset, uvX, uvY); - this.gl.uniform2f(this.uniformLocations.textureRepeat, uvWidth, uvHeight); - this.gl.uniform1f(this.uniformLocations.glassSize, this.config.glassSize); - - // Set attributes - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); - this.gl.vertexAttribPointer(this.attributeLocations.position, 2, this.gl.FLOAT, false, 0, 0); - this.gl.enableVertexAttribArray(this.attributeLocations.position); - - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); - this.gl.vertexAttribPointer(this.attributeLocations.texCoord, 2, this.gl.FLOAT, false, 0, 0); - this.gl.enableVertexAttribArray(this.attributeLocations.texCoord); - - // Draw - this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); + const program = gl.createProgram(); + if (!program) { + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; } - - /** - * Check for WebGL errors. - * @param {string} label - * @returns {boolean} - */ - checkWebGLError(label) { - const err = this.gl.getError(); - if (err !== this.gl.NO_ERROR) { - console.error(`ComfyUI Magnifying Glass ERROR: WebGL Error (${label}):`, err); - return true; - } - return false; + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error("ComfyUI Magnifying Glass ERROR: Shader program linking error:", gl.getProgramInfoLog(program)); + gl.deleteProgram(program); + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + return null; } - - /** - * Update viewport when glass size changes. - */ - updateViewport() { - if (this.gl) { - this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); - } + return program; + } + /** + * Render the magnified view. + * @param sourceCanvas - The source canvas to sample from + */ + render(sourceCanvas) { + if (!this.gl || !this.program || !this.texture || !this.uniformLocations || !this.attributeLocations) return; + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + try { + this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceCanvas); + } catch (e) { + console.error("ComfyUI Magnifying Glass ERROR: Error in texImage2D:", e); + return; } + const uvX = this.state.sourceX / sourceCanvas.width; + const uvY = this.state.sourceY / sourceCanvas.height; + const uvWidth = this.state.sourceWidth / sourceCanvas.width; + const uvHeight = this.state.sourceHeight / sourceCanvas.height; + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + this.gl.clearColor(0, 0, 0, 0); + this.gl.clear(this.gl.COLOR_BUFFER_BIT); + this.gl.useProgram(this.program); + this.gl.activeTexture(this.gl.TEXTURE0); + this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); + this.gl.uniform1i(this.uniformLocations.sourceTexture, 0); + this.gl.uniform2f(this.uniformLocations.textureOffset, uvX, uvY); + this.gl.uniform2f(this.uniformLocations.textureRepeat, uvWidth, uvHeight); + this.gl.uniform1f(this.uniformLocations.glassSize, this.config.glassSize); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); + this.gl.vertexAttribPointer(this.attributeLocations.position, 2, this.gl.FLOAT, false, 0, 0); + this.gl.enableVertexAttribArray(this.attributeLocations.position); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); + this.gl.vertexAttribPointer(this.attributeLocations.texCoord, 2, this.gl.FLOAT, false, 0, 0); + this.gl.enableVertexAttribArray(this.attributeLocations.texCoord); + this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); + } + /** + * Check for WebGL errors. + * @param label - Debug label + * @returns True if error occurred + */ + checkWebGLError(label) { + if (!this.gl) return false; + const err = this.gl.getError(); + if (err !== this.gl.NO_ERROR) { + console.error(`ComfyUI Magnifying Glass ERROR: WebGL Error (${label}):`, err); + return true; + } + return false; + } + /** + * Update viewport when glass size changes. + */ + updateViewport() { + if (this.gl) { + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + } + } } +export { + WebGLRenderer +}; +//# sourceMappingURL=WebGLRenderer.js.map diff --git a/web/magnify-glass/WebGLRenderer.js.map b/web/magnify-glass/WebGLRenderer.js.map new file mode 100644 index 0000000..16744e8 --- /dev/null +++ b/web/magnify-glass/WebGLRenderer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"WebGLRenderer.js","sources":["../../src/magnify-glass/WebGLRenderer.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - WebGLRenderer (TypeScript)\n * \n * Handles WebGL-based rendering of the magnified view.\n */\n\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\nimport type { UiManager } from './UiManager';\n\ninterface UniformLocations {\n sourceTexture: WebGLUniformLocation | null;\n textureOffset: WebGLUniformLocation | null;\n textureRepeat: WebGLUniformLocation | null;\n glassSize: WebGLUniformLocation | null;\n}\n\ninterface AttributeLocations {\n position: number;\n texCoord: number;\n}\n\n/**\n * WebGL Renderer class.\n * Provides efficient WebGL-based magnification rendering.\n */\nexport class WebGLRenderer {\n config: ConfigManager;\n state: MagnifierState;\n ui: UiManager;\n\n gl: WebGLRenderingContext | null;\n program: WebGLProgram | null;\n texture: WebGLTexture | null;\n positionBuffer: WebGLBuffer | null;\n texCoordBuffer: WebGLBuffer | null;\n uniformLocations: UniformLocations | null;\n attributeLocations: AttributeLocations | null;\n currentFilteringMode: number | null;\n\n vertexShaderSource: string;\n fragmentShaderSource: string;\n\n constructor(config: ConfigManager, state: MagnifierState, ui: UiManager) {\n this.config = config;\n this.state = state;\n this.ui = ui;\n\n this.gl = null;\n this.program = null;\n this.texture = null;\n this.positionBuffer = null;\n this.texCoordBuffer = null;\n this.uniformLocations = null;\n this.attributeLocations = null;\n this.currentFilteringMode = null;\n\n this.vertexShaderSource = `\n attribute vec2 a_position;\n attribute vec2 a_texCoord;\n varying vec2 v_texCoord;\n void main() {\n gl_Position = vec4(a_position, 0.0, 1.0);\n v_texCoord = a_texCoord;\n }\n `;\n\n this.fragmentShaderSource = `\n precision mediump float;\n varying vec2 v_texCoord;\n uniform sampler2D u_sourceTexture;\n uniform vec2 u_textureOffset;\n uniform vec2 u_textureRepeat;\n uniform float u_glassSize;\n\n void main() {\n vec2 sampleCoord = u_textureOffset + v_texCoord * u_textureRepeat;\n vec4 color = texture2D(u_sourceTexture, sampleCoord);\n gl_FragColor = color;\n }\n `;\n\n this.initialize();\n }\n\n /**\n * Initialize WebGL context and resources.\n */\n initialize(): void {\n // Get WebGL context\n if (!this.ui.glassCanvas) return;\n\n this.gl = this.ui.glassCanvas.getContext(\"webgl\", { preserveDrawingBuffer: true });\n if (!this.gl) {\n console.error(\"ComfyUI Magnifying Glass ERROR: WebGL not supported or context creation failed.\");\n return;\n }\n\n // Create shader program\n this.program = this.createShaderProgram(this.gl, this.vertexShaderSource, this.fragmentShaderSource);\n if (!this.program) return;\n\n // Create position buffer\n this.positionBuffer = this.gl.createBuffer();\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);\n const positions = [-1, -1, 1, -1, -1, 1, 1, 1];\n this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW);\n\n // Create texture coordinate buffer\n this.texCoordBuffer = this.gl.createBuffer();\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);\n const texCoords = [0, 1, 1, 1, 0, 0, 1, 0]; // Flipped Y for texCoords\n this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW);\n\n // Create texture\n this.texture = this.gl.createTexture();\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);\n this.updateTextureFiltering(this.config.textureFiltering);\n\n // Get uniform and attribute locations\n this.uniformLocations = {\n sourceTexture: this.gl.getUniformLocation(this.program, \"u_sourceTexture\"),\n textureOffset: this.gl.getUniformLocation(this.program, \"u_textureOffset\"),\n textureRepeat: this.gl.getUniformLocation(this.program, \"u_textureRepeat\"),\n glassSize: this.gl.getUniformLocation(this.program, \"u_glassSize\"),\n };\n\n this.attributeLocations = {\n position: this.gl.getAttribLocation(this.program, \"a_position\"),\n texCoord: this.gl.getAttribLocation(this.program, \"a_texCoord\"),\n };\n }\n\n /**\n * Update texture filtering mode.\n * @param filteringModeString - \"Linear\" or \"Nearest\"\n */\n updateTextureFiltering(filteringModeString: string): void {\n if (!this.gl || !this.texture) return;\n\n let glFilterMode: number;\n if (filteringModeString === \"Nearest\") {\n glFilterMode = this.gl.NEAREST;\n } else {\n glFilterMode = this.gl.LINEAR;\n }\n\n if (this.currentFilteringMode === glFilterMode) return;\n\n this.currentFilteringMode = glFilterMode;\n\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, glFilterMode);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, glFilterMode);\n this.gl.bindTexture(this.gl.TEXTURE_2D, null);\n }\n\n /**\n * Check if renderer is valid and ready.\n * @returns Boolean indicating if WebGL context and program are ready\n */\n isValid(): boolean {\n return this.gl !== null && this.program !== null;\n }\n\n /**\n * Create a WebGL shader program.\n * @param gl - WebGL context\n * @param vsSource - Vertex shader source\n * @param fsSource - Fragment shader source\n * @returns WebGL program or null on failure\n */\n createShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string): WebGLProgram | null {\n const vertexShader = gl.createShader(gl.VERTEX_SHADER);\n if (!vertexShader) return null;\n\n gl.shaderSource(vertexShader, vsSource);\n gl.compileShader(vertexShader);\n if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Vertex shader compilation error:', gl.getShaderInfoLog(vertexShader));\n gl.deleteShader(vertexShader);\n return null;\n }\n\n const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);\n if (!fragmentShader) {\n gl.deleteShader(vertexShader);\n return null;\n }\n\n gl.shaderSource(fragmentShader, fsSource);\n gl.compileShader(fragmentShader);\n if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Fragment shader compilation error:', gl.getShaderInfoLog(fragmentShader));\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n const program = gl.createProgram();\n if (!program) {\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Shader program linking error:', gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n return program;\n }\n\n /**\n * Render the magnified view.\n * @param sourceCanvas - The source canvas to sample from\n */\n render(sourceCanvas: HTMLCanvasElement): void {\n if (!this.gl || !this.program || !this.texture || !this.uniformLocations || !this.attributeLocations) return;\n\n // Bind texture and update with source canvas\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n\n try {\n this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceCanvas);\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error in texImage2D:\", e);\n return;\n }\n\n // Calculate normalized texture coordinates (UV space: 0-1)\n const uvX = this.state.sourceX / sourceCanvas.width;\n const uvY = this.state.sourceY / sourceCanvas.height;\n const uvWidth = this.state.sourceWidth / sourceCanvas.width;\n const uvHeight = this.state.sourceHeight / sourceCanvas.height;\n\n // Set up rendering\n this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);\n this.gl.clearColor(0.0, 0.0, 0.0, 0.0);\n this.gl.clear(this.gl.COLOR_BUFFER_BIT);\n\n this.gl.useProgram(this.program);\n\n // Set uniforms\n this.gl.activeTexture(this.gl.TEXTURE0);\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.uniform1i(this.uniformLocations.sourceTexture, 0);\n this.gl.uniform2f(this.uniformLocations.textureOffset, uvX, uvY);\n this.gl.uniform2f(this.uniformLocations.textureRepeat, uvWidth, uvHeight);\n this.gl.uniform1f(this.uniformLocations.glassSize, this.config.glassSize);\n\n // Set attributes\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);\n this.gl.vertexAttribPointer(this.attributeLocations.position, 2, this.gl.FLOAT, false, 0, 0);\n this.gl.enableVertexAttribArray(this.attributeLocations.position);\n\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);\n this.gl.vertexAttribPointer(this.attributeLocations.texCoord, 2, this.gl.FLOAT, false, 0, 0);\n this.gl.enableVertexAttribArray(this.attributeLocations.texCoord);\n\n // Draw\n this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);\n }\n\n /**\n * Check for WebGL errors.\n * @param label - Debug label\n * @returns True if error occurred\n */\n checkWebGLError(label: string): boolean {\n if (!this.gl) return false;\n\n const err = this.gl.getError();\n if (err !== this.gl.NO_ERROR) {\n console.error(`ComfyUI Magnifying Glass ERROR: WebGL Error (${label}):`, err);\n return true;\n }\n return false;\n }\n\n /**\n * Update viewport when glass size changes.\n */\n updateViewport(): void {\n if (this.gl) {\n this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/index.js b/web/magnify-glass/index.js deleted file mode 100644 index d12ffa3..0000000 --- a/web/magnify-glass/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * ComfyUI MagnifyGlass - Index - * - * Re-exports all magnify-glass module components for convenience. - */ - -export { MagnifyGlass } from './MagnifyGlass.js'; -export { ConfigManager } from './ConfigManager.js'; -export { MagnifierState } from './MagnifierState.js'; -export { UiManager } from './UiManager.js'; -export { WebGLRenderer } from './WebGLRenderer.js'; -export { DebugManager } from './DebugManager.js'; -export { EventHandler } from './EventHandler.js'; diff --git a/web/magnify_glass.js b/web/magnify_glass.js index 165582e..0c963b8 100644 --- a/web/magnify_glass.js +++ b/web/magnify_glass.js @@ -1,1710 +1,12 @@ -import { app } from "../../../scripts/app.js"; - -/** - * ComfyUI Magnifying Glass - * - * This script adds a magnifying glass feature to ComfyUI. - * Press X to activate the magnifying glass and see a zoomed view of the canvas. - */ - +import { MagnifyGlass } from "./magnify-glass/MagnifyGlass.js"; +import { app } from "/scripts/app.js"; app.registerExtension({ - name: "comfyui.magnify.glass", - async setup() { - // Default settings configuration - const DEFAULT_SETTINGS = { - "🔍MagnifyGlass.ZoomFactor": 300, - "🔍MagnifyGlass.GlassSize": 300, - "🔍MagnifyGlass.BorderColor": "#6b7280", - "🔍MagnifyGlass.BorderWidth": 1, - "🔍MagnifyGlass.ActivationKey": "x", - "🔍MagnifyGlass.AltRequired": false, - "🔍MagnifyGlass.FollowCursor": false, - "🔍MagnifyGlass.DebugMode": false, - "🔍MagnifyGlass.OffsetStep": 5, - "🔍MagnifyGlass.GlassPosition": "Top-Right", - "🔍MagnifyGlass.ResetKey": "o", - "🔍MagnifyGlass.GlassShape": "Rounded Square", - "🔍MagnifyGlass.BorderEnabled": true, - "🔍MagnifyGlass.TextureFiltering": "Linear", - "🔍MagnifyGlass.AlwaysActiveMode": true, - "🔍MagnifyGlass.ToggleFollowCursorKey": "h", - }; - - // Function to safely get settings values - const getSettingValue = (key, defaultValue) => { - try { - const value = app.ui.settings.getSettingValue(key); - return value === undefined ? defaultValue : value; - } catch (e) { - console.warn(`ComfyUI Magnifying Glass: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`); - return defaultValue; - } - }; - - // Main MagnifyGlass class - class MagnifyGlass { - constructor() { - this.config = new ConfigManager(); // ConfigManager will now read from settings - this.state = new MagnifierState(); - this.ui = new UiManager(this.config, this.state); - this.renderer = null; - this.debugger = new DebugManager(this.config, this.state, this.ui); - this.eventHandler = new EventHandler(this); - - // The LiteGraph canvas - this.litegraphCanvas = null; - - // Last known mouse position for better initial positioning - this.lastKnownMousePosition = { x: 0, y: 0 }; - } - - init() { - if (typeof LiteGraph === 'undefined' || typeof app === 'undefined' || !app.canvas) { - this.debugger.log("LiteGraph or app not ready, retrying in 100ms."); - setTimeout(() => this.init(), 100); - return; - } - this.debugger.log("LiteGraph and app ready."); - - // Load saved offsets first (before UI creation depends on them) - this.config.loadSavedOffsets(); - - // Print detailed information about the canvas setup (if debug enabled) - this.debugger.printCanvasInfo(); - - // Create UI elements (now depends on config potentially updated by settings) - this.ui.createElements(); - - // Setup WebGL renderer - this.renderer = new WebGLRenderer(this.config, this.state, this.ui); - if (!this.renderer.isValid()) { - this.ui.cleanup(); - return; - } - - // Find the LiteGraph canvas - this.litegraphCanvas = this.findLiteGraphCanvas(); - this.debugger.log("LiteGraph canvas found:", this.litegraphCanvas); - if (!this.litegraphCanvas) { - this.debugger.error("Could not find LiteGraph canvas. Magnifier will not work."); - this.ui.cleanup(); - return; - } - - // Attach event handlers - this.eventHandler.attachListeners(); - - this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? 'Alt+' : ''}${this.config.activationKey.toUpperCase()} to activate. Hotkeys disabled while typing in inputs.`); - } - - findLiteGraphCanvas() { - return document.getElementById("graph-canvas") || - document.querySelector("canvas.graphcanvas") || - app.canvas_manager?.container?.querySelector("canvas") || - (app.canvas && app.canvas.graph_canvas ? app.canvas.graph_canvas : null); - } - - updateMagnifiedView() { - if (!this.state.active || !this.renderer || !this.litegraphCanvas) { - //this.debugger.log("updateMagnifiedView skipped: not active, no renderer, or no litegraphCanvas."); // Too noisy - return; - } - - // Get canvas transformation info - this.updateCanvasTransformation(); - - // Calculate the source region - this.calculateSourceRegion(); - - // Schedule the rendering operation if not already scheduled - if (!this.state.isRenderScheduled) { - this.state.isRenderScheduled = true; - requestAnimationFrame(() => { - if (!this.state.active || !this.renderer || !this.litegraphCanvas) { // Re-check before actual rendering - this.state.isRenderScheduled = false; - return; - } - // Render the magnified view - this.renderer.render(this.litegraphCanvas); - - // Update debug visualization - this.debugger.updateDebugView(); - - // Render HTML overlays - this.renderHtmlOverlays(); - - this.state.isRenderScheduled = false; // Reset the flag - }); - } - } - - updateCanvasTransformation() { - this.state.canvasScale = 1.0; - this.state.canvasOffsetX = 0; - this.state.canvasOffsetY = 0; - - if (app && app.canvas) { - if (app.canvas.ds) { - if (typeof app.canvas.ds.scale === 'number') { - this.state.canvasScale = app.canvas.ds.scale; - } - - if (app.canvas.ds.offset) { - this.state.canvasOffsetX = app.canvas.ds.offset[0] || 0; - this.state.canvasOffsetY = app.canvas.ds.offset[1] || 0; - } - } - //this.debugger.log(`Canvas transform: scale=${this.state.canvasScale}, offsetX=${this.state.canvasOffsetX}, offsetY=${this.state.canvasOffsetY}`); // Too noisy - } - } - - calculateSourceRegion() { - // 1. We have cursor position in canvas pixels (state.x, state.y) - const cursorPixelX = this.state.x; - const cursorPixelY = this.state.y; - - // 2. Get LiteGraph's current transform - const canvasScale = this.state.canvasScale; - const canvasOffsetX = this.state.canvasOffsetX; - const canvasOffsetY = this.state.canvasOffsetY; - - // Avoid division by zero if scale is somehow 0 - if (canvasScale === 0) return; - - // 3. Convert cursor canvas pixels to LiteGraph graph coordinates - const cursorGraphX = (cursorPixelX - canvasOffsetX) / canvasScale; - const cursorGraphY = (cursorPixelY - canvasOffsetY) / canvasScale; - - // 4. Determine the target center in graph coordinates, applying the manual offset - // The manual offset now conceptually works in graph units. - const targetGraphCenterX = cursorGraphX + this.config.offsetX; - const targetGraphCenterY = cursorGraphY + this.config.offsetY; - - // 5. Calculate source dimensions in graph units - // The size on the source canvas (in pixels) that we want to sample is glassSize / zoomFactor. - // Convert this pixel size to graph units by dividing by canvasScale. - const sourceGraphWidth = (this.config.glassSize / this.config.zoomFactor) / canvasScale; - const sourceGraphHeight = (this.config.glassSize / this.config.zoomFactor) / canvasScale; - - // 6. Calculate source top-left corner in graph coordinates - const sourceGraphX = targetGraphCenterX - (sourceGraphWidth / 2); - const sourceGraphY = targetGraphCenterY - (sourceGraphHeight / 2); - - // 7. Convert the source rectangle from graph coordinates back to canvas pixel coordinates - this.state.sourceX = (sourceGraphX * canvasScale) + canvasOffsetX; - this.state.sourceY = (sourceGraphY * canvasScale) + canvasOffsetY; - - // The width/height in pixels is the graph size * scale - this.state.sourceWidth = sourceGraphWidth * canvasScale; - this.state.sourceHeight = sourceGraphHeight * canvasScale; - - // Keep the debug logging (adjust to show graph coords too) - // this.debugger.log(`Cursor (Canvas Pixels): X=${cursorPixelX.toFixed(1)}, Y=${cursorPixelY.toFixed(1)}`); // Too noisy - // this.debugger.log(`Cursor (Graph Coords): X=${cursorGraphX.toFixed(1)}, Y=${cursorGraphY.toFixed(1)}`); // Too noisy - // this.debugger.log(`Target Center (Graph Coords + Manual Offset): X=${targetGraphCenterX.toFixed(1)}, Y=${targetGraphCenterY.toFixed(1)}`); // Too noisy - // this.debugger.log(`Source Rect (Graph Coords): X=${sourceGraphX.toFixed(1)}, Y=${sourceGraphY.toFixed(1)}, W=${sourceGraphWidth.toFixed(1)}, H=${sourceGraphHeight.toFixed(1)}`); // Too noisy - // this.debugger.log(`Source Rect (Canvas Pixels): X=${this.state.sourceX.toFixed(1)}, Y=${this.state.sourceY.toFixed(1)}, W=${this.state.sourceWidth.toFixed(1)}, H=${this.state.sourceHeight.toFixed(1)}`); // Too noisy - // this.debugger.log(`Using Manual Offsets (Graph Units): X=${this.config.offsetX}, Y=${this.config.offsetY}`); // Too noisy - // this.debugger.log(`LiteGraph Canvas Transform: scale=${canvasScale.toFixed(3)}, offset=(${canvasOffsetX.toFixed(1)}, ${canvasOffsetY.toFixed(1)})`); // Too noisy - } - - // Helper function to check if two rectangles overlap - rectsOverlap(rect1, rect2) { - return rect1.x < rect2.x + rect2.width && - rect1.x + rect1.width > rect2.x && - rect1.y < rect2.y + rect2.height && - rect1.y + rect1.height > rect2.y; - } - - renderHtmlOverlays() { - if (!this.state.active || !this.ui.htmlOverlayContainer || !app.graph || !this.litegraphCanvas) { - if (this.ui.htmlOverlayContainer) this.ui.htmlOverlayContainer.innerHTML = ''; - return; - } - - this.ui.htmlOverlayContainer.innerHTML = ''; - - const magnifyRect = { - x: this.state.sourceX, - y: this.state.sourceY, - width: this.state.sourceWidth, - height: this.state.sourceHeight - }; - - const nodes = app.graph._nodes; - if (!nodes) return; - - for (const node of nodes) { - if (!node.widgets) continue; - - for (const widget of node.widgets) { - let isTextElement = false; - let isVideoElement = false; - let isImageElement = false; // New flag for image elements - let elementToProcess = null; - - if (widget.element) { - if (widget.type === "text" || widget.type === "string" || widget.element.tagName === 'TEXTAREA') { - isTextElement = true; - elementToProcess = widget.element; - } else if (widget.element.tagName === 'VIDEO') { - isVideoElement = true; - elementToProcess = widget.element; - } else if (widget.element.tagName === 'IMG') { // Check for IMG tag directly - isImageElement = true; - elementToProcess = widget.element; - } else { - // Try to find a video or image element as a child - const potentialVideo = widget.element.querySelector('video'); - if (potentialVideo) { - isVideoElement = true; - elementToProcess = potentialVideo; - } else { - const potentialImage = widget.element.querySelector('img'); - if (potentialImage) { - isImageElement = true; - elementToProcess = potentialImage; - } - } - } - } - - if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) { - const widgetRect = elementToProcess.getBoundingClientRect(); - const canvasRect = this.litegraphCanvas.getBoundingClientRect(); - - const canvasToViewportScaleX = canvasRect.width > 0 ? (this.litegraphCanvas.width / canvasRect.width) : 1; - const canvasToViewportScaleY = canvasRect.height > 0 ? (this.litegraphCanvas.height / canvasRect.height) : 1; - - const widgetCanvasX = (widgetRect.left - canvasRect.left) * canvasToViewportScaleX; - const widgetCanvasY = (widgetRect.top - canvasRect.top) * canvasToViewportScaleY; - const widgetCanvasWidth = widgetRect.width * canvasToViewportScaleX; - const widgetCanvasHeight = widgetRect.height * canvasToViewportScaleY; - - const widgetSourceRect = { - x: widgetCanvasX, - y: widgetCanvasY, - width: widgetCanvasWidth, - height: widgetCanvasHeight - }; - - if (this.rectsOverlap(magnifyRect, widgetSourceRect)) { - const clonedElement = elementToProcess.cloneNode(true); - clonedElement.style.position = 'absolute'; - clonedElement.style.pointerEvents = 'none'; - - if (isTextElement) { - clonedElement.style.backgroundColor = elementToProcess.style.backgroundColor || '#222'; - clonedElement.style.color = elementToProcess.style.color || '#DDD'; - clonedElement.style.border = elementToProcess.style.border || '1px solid #555'; - clonedElement.disabled = true; - } else if (isVideoElement) { - clonedElement.src = elementToProcess.src; - clonedElement.autoplay = elementToProcess.autoplay; - clonedElement.loop = elementToProcess.loop; - clonedElement.preload = elementToProcess.preload; - clonedElement.crossOrigin = elementToProcess.crossOrigin; - clonedElement.muted = true; - if (!elementToProcess.paused) { - clonedElement.play().catch(e => console.warn("Magnify Glass: Cloned video play failed", e)); - } - clonedElement.currentTime = elementToProcess.currentTime; - } else if (isImageElement) { - // For images, ensure src and alt are copied. - // The browser will handle rendering based on src. - clonedElement.src = elementToProcess.src; - clonedElement.alt = elementToProcess.alt; - // Preserve original display characteristics if needed, but usually not for simple images - // clonedElement.style.objectFit = elementToProcess.style.objectFit || 'contain'; - } - - const relativeX = widgetSourceRect.x - magnifyRect.x; - const relativeY = widgetSourceRect.y - magnifyRect.y; - - const magnifiedX = relativeX * this.config.zoomFactor; - const magnifiedY = relativeY * this.config.zoomFactor; - - clonedElement.style.left = `${magnifiedX}px`; - clonedElement.style.top = `${magnifiedY}px`; - clonedElement.style.width = `${widgetSourceRect.width}px`; - clonedElement.style.height = `${widgetSourceRect.height}px`; - clonedElement.style.transformOrigin = 'top left'; - clonedElement.style.transform = `scale(${this.config.zoomFactor})`; - - if (isTextElement && clonedElement.style.fontSize) { - const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize); - clonedElement.style.fontSize = `${originalFontSize}px`; - } - - this.ui.htmlOverlayContainer.appendChild(clonedElement); - } - } - } - } - } - - // Method to update config from settings (can be called from callbacks) - updateConfigFromSettings() { - this.config.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", DEFAULT_SETTINGS["🔍MagnifyGlass.ZoomFactor"]) / 100; - this.config.glassSize = getSettingValue("🔍MagnifyGlass.GlassSize", DEFAULT_SETTINGS["🔍MagnifyGlass.GlassSize"]); - this.config.borderColor = getSettingValue("🔍MagnifyGlass.BorderColor", DEFAULT_SETTINGS["🔍MagnifyGlass.BorderColor"]); - this.config.borderWidth = getSettingValue("🔍MagnifyGlass.BorderWidth", DEFAULT_SETTINGS["🔍MagnifyGlass.BorderWidth"]); - this.config.activationKey = getSettingValue("🔍MagnifyGlass.ActivationKey", DEFAULT_SETTINGS["🔍MagnifyGlass.ActivationKey"]); - this.config.altRequired = getSettingValue("🔍MagnifyGlass.AltRequired", DEFAULT_SETTINGS["🔍MagnifyGlass.AltRequired"]); - this.config.followCursor = getSettingValue("🔍MagnifyGlass.FollowCursor", DEFAULT_SETTINGS["🔍MagnifyGlass.FollowCursor"]); - this.config.debugMode = getSettingValue("🔍MagnifyGlass.DebugMode", DEFAULT_SETTINGS["🔍MagnifyGlass.DebugMode"]); - this.config.offsetStep = getSettingValue("🔍MagnifyGlass.OffsetStep", DEFAULT_SETTINGS["🔍MagnifyGlass.OffsetStep"]); - this.config.glassPosition = getSettingValue("🔍MagnifyGlass.GlassPosition", DEFAULT_SETTINGS["🔍MagnifyGlass.GlassPosition"]); - this.config.resetKey = getSettingValue("🔍MagnifyGlass.ResetKey", DEFAULT_SETTINGS["🔍MagnifyGlass.ResetKey"]); - this.config.glassShape = getSettingValue("🔍MagnifyGlass.GlassShape", DEFAULT_SETTINGS["🔍MagnifyGlass.GlassShape"]); - this.config.borderEnabled = getSettingValue("🔍MagnifyGlass.BorderEnabled", DEFAULT_SETTINGS["🔍MagnifyGlass.BorderEnabled"]); - this.config.textureFiltering = getSettingValue("🔍MagnifyGlass.TextureFiltering", DEFAULT_SETTINGS["🔍MagnifyGlass.TextureFiltering"]); - this.config.alwaysActiveMode = getSettingValue("🔍MagnifyGlass.AlwaysActiveMode", DEFAULT_SETTINGS["🔍MagnifyGlass.AlwaysActiveMode"]); - this.config.toggleFollowCursorKey = getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", DEFAULT_SETTINGS["🔍MagnifyGlass.ToggleFollowCursorKey"]); - // Offsets X/Y are managed separately via load/save/arrow keys - } - - // Method to apply UI changes based on config - applyUiChanges() { - if (this.ui.glassDiv) { - this.ui.glassDiv.style.width = `${this.config.glassSize}px`; - this.ui.glassDiv.style.height = `${this.config.glassSize}px`; - this.ui.glassDiv.style.border = this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : 'none'; - // this.ui.glassDiv.style.borderRadius = this.config.glassShape === "Circle" ? "50%" : "0px"; - - // Reset clip-path and set default border-radius, then apply shape-specific styles - this.ui.glassDiv.style.clipPath = 'none'; - this.ui.glassDiv.style.borderRadius = '0px'; // Default to square before specific shape styling - - switch (this.config.glassShape) { - case "Circle": - this.ui.glassDiv.style.borderRadius = "50%"; - break; - case "Square": - // borderRadius is already '0px' - break; - case "Rounded Square": - this.ui.glassDiv.style.borderRadius = "20%"; // Adjust as desired - break; - default: // Fallback to Circle if shape is unknown or unsupported for border - this.ui.glassDiv.style.borderRadius = "50%"; - break; - } - } - if (this.ui.glassCanvas) { - this.ui.glassCanvas.width = this.config.glassSize; - this.ui.glassCanvas.height = this.config.glassSize; - // Re-initialize or update viewport if needed for WebGL - if(this.renderer && this.renderer.gl) { - this.renderer.gl.viewport(0, 0, this.renderer.gl.canvas.width, this.renderer.gl.canvas.height); - } - } - // Handle debug canvas show/hide - if (this.config.debugMode) { - if (!this.ui.debugCanvas) this.ui.createDebugCanvas(); - if (this.state.active) this.ui.debugCanvas.style.display = "block"; - } else { - if (this.ui.debugCanvas) this.ui.debugCanvas.style.display = "none"; - } - } - - resetOffsets() { - this.config.offsetX = 0; - this.config.offsetY = 0; - this.config.saveOffsets(); // Save the reset values - if (this.state.active) { - this.updateMagnifiedView(); // Update if active - } - - // Reset magnify glass position to top-right corner - if (this.ui.glassDiv) { - const glassSize = this.config.glassSize; - const padding = 20; // Same padding as initial positioning - this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - padding}px`; - this.ui.glassDiv.style.top = `${padding}px`; - // Reset the activation flag so next time it uses default positioning - this.state.wasActivatedBefore = false; - } - - // Reset inspector panel position to default - if (window.comfyUIMagnifyGlassExtensions && window.comfyUIMagnifyGlassExtensions.length > 0) { - window.comfyUIMagnifyGlassExtensions.forEach(extension => { - if (extension && extension.stateManager) { - // Reset panel to default position - extension.stateManager.state.isPanelPinned = false; - extension.stateManager.state.isPanelLocked = false; - extension.stateManager.state.isAutoPinned = false; - extension.stateManager.state.pinnedPosition = { x: 0, y: 0 }; - extension.stateManager.state.lastPinnedPosition = null; - - // Update panel position and controls - if (extension.positionManager) { - extension.positionManager.positionPanel(); - } - if (extension.uiManager && extension.uiManager.updateControlStates) { - extension.uiManager.updateControlStates(); - } - } - }); - } - - this.debugger.log("Reset: Magnify glass and inspector panel positions restored to top-right defaults"); - } - } - - // Configuration manager - Reads from settings - class ConfigManager { - constructor() { - // Initialize properties that will be set by reading settings - this.zoomFactor = DEFAULT_SETTINGS["🔍MagnifyGlass.ZoomFactor"] / 100; - this.glassSize = DEFAULT_SETTINGS["🔍MagnifyGlass.GlassSize"]; - this.borderColor = DEFAULT_SETTINGS["🔍MagnifyGlass.BorderColor"]; - this.borderWidth = DEFAULT_SETTINGS["🔍MagnifyGlass.BorderWidth"]; - this.activationKey = DEFAULT_SETTINGS["🔍MagnifyGlass.ActivationKey"]; - this.altRequired = DEFAULT_SETTINGS["🔍MagnifyGlass.AltRequired"]; - this.followCursor = DEFAULT_SETTINGS["🔍MagnifyGlass.FollowCursor"]; - this.debugMode = DEFAULT_SETTINGS["🔍MagnifyGlass.DebugMode"]; - this.offsetStep = DEFAULT_SETTINGS["🔍MagnifyGlass.OffsetStep"]; - this.glassPosition = DEFAULT_SETTINGS["🔍MagnifyGlass.GlassPosition"]; - this.resetKey = DEFAULT_SETTINGS["🔍MagnifyGlass.ResetKey"]; - this.glassShape = DEFAULT_SETTINGS["🔍MagnifyGlass.GlassShape"]; - this.borderEnabled = DEFAULT_SETTINGS["🔍MagnifyGlass.BorderEnabled"]; - this.textureFiltering = DEFAULT_SETTINGS["🔍MagnifyGlass.TextureFiltering"]; - this.alwaysActiveMode = DEFAULT_SETTINGS["🔍MagnifyGlass.AlwaysActiveMode"]; - this.toggleFollowCursorKey = DEFAULT_SETTINGS["🔍MagnifyGlass.ToggleFollowCursorKey"]; - - // Alignment adjustment parameters - managed separately - this.offsetX = 0; // Default before loading saved - this.offsetY = 0; // Default before loading saved - } - - // Call this after settings are registered and potentially read by MagnifyGlass - loadSettings() { - this.zoomFactor = getSettingValue("🔍MagnifyGlass.ZoomFactor", this.zoomFactor * 100) / 100; - this.glassSize = getSettingValue("🔍MagnifyGlass.GlassSize", this.glassSize); - this.borderColor = getSettingValue("🔍MagnifyGlass.BorderColor", this.borderColor); - this.borderWidth = getSettingValue("🔍MagnifyGlass.BorderWidth", this.borderWidth); - this.activationKey = getSettingValue("🔍MagnifyGlass.ActivationKey", this.activationKey); - this.altRequired = getSettingValue("🔍MagnifyGlass.AltRequired", this.altRequired); - this.followCursor = getSettingValue("🔍MagnifyGlass.FollowCursor", this.followCursor); - this.debugMode = getSettingValue("🔍MagnifyGlass.DebugMode", this.debugMode); - this.offsetStep = getSettingValue("🔍MagnifyGlass.OffsetStep", this.offsetStep); - this.glassPosition = getSettingValue("🔍MagnifyGlass.GlassPosition", this.glassPosition); - this.resetKey = getSettingValue("🔍MagnifyGlass.ResetKey", this.resetKey); - this.glassShape = getSettingValue("🔍MagnifyGlass.GlassShape", this.glassShape); - this.borderEnabled = getSettingValue("🔍MagnifyGlass.BorderEnabled", this.borderEnabled); - this.textureFiltering = getSettingValue("🔍MagnifyGlass.TextureFiltering", this.textureFiltering); - this.alwaysActiveMode = getSettingValue("🔍MagnifyGlass.AlwaysActiveMode", this.alwaysActiveMode); - this.toggleFollowCursorKey = getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", this.toggleFollowCursorKey); - } - - loadSavedOffsets() { - try { - const savedOffsetX = localStorage.getItem('comfyui_magnify_offset_x'); - const savedOffsetY = localStorage.getItem('comfyui_magnify_offset_y'); - - if (savedOffsetX !== null) { - this.offsetX = parseInt(savedOffsetX, 10); - } else { - this.offsetX = 0; // Ensure reset if not found - } - - if (savedOffsetY !== null) { - this.offsetY = parseInt(savedOffsetY, 10); - } else { - this.offsetY = 0; // Ensure reset if not found - } - - // No initial debug log here, DebugManager handles it based on config.debugMode - } catch (e) { - console.error("ComfyUI Magnifying Glass ERROR: Error loading saved offsets:", e); - this.offsetX = 0; // Reset on error - this.offsetY = 0; - } - } - - saveOffsets() { - try { - localStorage.setItem('comfyui_magnify_offset_x', this.offsetX.toString()); - localStorage.setItem('comfyui_magnify_offset_y', this.offsetY.toString()); - // No log here, EventHandler handles it - } catch (e) { - console.error("ComfyUI Magnifying Glass ERROR: Error saving offsets:", e); - } - } - } - - // Magnifier state - class MagnifierState { - constructor() { - this.active = false; - this.wasActivatedBefore = false; // Track if the glass has been activated before - this.x = 0; // Cursor X relative to litegraphCanvas - this.y = 0; // Cursor Y relative to litegraphCanvas - this.sourceX = 0; // Calculated source area X - this.sourceY = 0; // Calculated source area Y - this.sourceWidth = 0; // Calculated source area width - this.sourceHeight = 0; // Calculated source area height - this.canvasScale = 1.0; // Current canvas scale/zoom - this.canvasOffsetX = 0; // Canvas translation X - this.canvasOffsetY = 0; // Canvas translation Y - this.isRenderScheduled = false; // Flag to manage requestAnimationFrame - } - } - - // UI Manager - handles creation and management of DOM elements (Minor changes for config) - class UiManager { - constructor(config, state) { - this.config = config; - this.state = state; - this.glassDiv = null; - this.glassCanvas = null; - this.debugCanvas = null; - this.debugCtx = null; - this.htmlOverlayContainer = null; // Added for HTML overlays - } - - createElements() { - // Create magnifying glass container - this.glassDiv = document.createElement("div"); - this.glassDiv.id = "comfyui-magnify-glass"; - this.glassDiv.style.cssText = ` - position: absolute; - width: ${this.config.glassSize}px; - height: ${this.config.glassSize}px; - border-radius: ${this.config.glassShape === "Circle" ? "50%" : "0px"}; - border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : 'none'}; - overflow: hidden; - pointer-events: none; - z-index: 98999; - display: none; - box-shadow: 0 5px 15px rgba(0,0,0,0.3); - background-color: rgba(255,255,255,0.1); - `; - - // Create WebGL canvas for the magnifying glass - this.glassCanvas = document.createElement("canvas"); - this.glassCanvas.width = this.config.glassSize; - this.glassCanvas.height = this.config.glassSize; - this.glassCanvas.id = "comfyui-magnify-canvas"; - this.glassDiv.appendChild(this.glassCanvas); - - // Create HTML overlay container - this.htmlOverlayContainer = document.createElement("div"); - this.htmlOverlayContainer.id = "comfyui-magnify-html-overlay"; - this.htmlOverlayContainer.style.cssText = ` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - overflow: hidden; - `; - this.glassDiv.appendChild(this.htmlOverlayContainer); - - document.body.appendChild(this.glassDiv); - - // Create debug canvas if debug mode is enabled - if (this.config.debugMode) { - this.createDebugCanvas(); - } - } - - createDebugCanvas() { - this.debugCanvas = document.createElement("canvas"); - this.debugCanvas.id = "comfyui-magnify-debug"; - this.debugCanvas.width = 400; - this.debugCanvas.height = 350; // Increased height to 350px - this.debugCanvas.style.cssText = ` - position: fixed; - top: 10px; - right: 10px; - background: rgba(0,0,0,0.7); - border: 1px solid #fff; - z-index: 99000; - pointer-events: none; - color: white; - font-family: monospace; - display: none; - `; - document.body.appendChild(this.debugCanvas); - this.debugCtx = this.debugCanvas.getContext('2d'); - } - - show() { - this.glassDiv.style.display = "block"; - if (this.config.debugMode && this.debugCanvas) { - this.debugCanvas.style.display = "block"; - } - } - - hide() { - this.glassDiv.style.display = "none"; - if (this.config.debugMode && this.debugCanvas) { - this.debugCanvas.style.display = "none"; - } - // Clear HTML overlays when hiding - if (this.htmlOverlayContainer) { - this.htmlOverlayContainer.innerHTML = ''; - } - } - - positionGlass(clientX, clientY) { - if (!this.config.followCursor) return; - - // Default position: Horizontally centered with cursor, top of glass 20px below cursor - let newLeft = clientX - (this.config.glassSize / 2); - let newTop = clientY + 20; // Default to Bottom - - const glassSize = this.config.glassSize; - const offsetAmount = 20; // How far from the cursor the glass edge should be - - switch (this.config.glassPosition) { - case "Top": - newLeft = clientX - (glassSize / 2); - newTop = clientY - glassSize - offsetAmount; - break; - case "Bottom": - newLeft = clientX - (glassSize / 2); - newTop = clientY + offsetAmount; - break; - case "Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY - (glassSize / 2); - break; - case "Right": - newLeft = clientX + offsetAmount; - newTop = clientY - (glassSize / 2); - break; - case "Top-Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY - glassSize - offsetAmount; - break; - case "Top-Right": - newLeft = clientX + offsetAmount; - newTop = clientY - glassSize - offsetAmount; - break; - case "Bottom-Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY + offsetAmount; - break; - case "Bottom-Right": - newLeft = clientX + offsetAmount; - newTop = clientY + offsetAmount; - break; - default: // Default to Bottom if something is wrong - newLeft = clientX - (glassSize / 2); - newTop = clientY + offsetAmount; - break; - } - - this.glassDiv.style.left = `${newLeft}px`; - this.glassDiv.style.top = `${newTop}px`; - - this.adjustForBoundaries(clientX, clientY); - } - - adjustForBoundaries(clientX, clientY) { - const glassRect = this.glassDiv.getBoundingClientRect(); - - // Check right boundary - if (glassRect.right > window.innerWidth) { - this.glassDiv.style.left = `${clientX - glassRect.width - 20}px`; - } - - // Check left boundary - const currentRectLeft = this.glassDiv.getBoundingClientRect(); - if (currentRectLeft.left < 0) { - this.glassDiv.style.left = "10px"; - } - - // Check bottom boundary - if (glassRect.bottom > window.innerHeight) { - this.glassDiv.style.top = `${clientY - glassRect.height - 20}px`; - } - - // Check top boundary - const currentRectTop = this.glassDiv.getBoundingClientRect(); - if (currentRectTop.top < 0) { - this.glassDiv.style.top = "10px"; - } - } - - cleanup() { - if (this.glassDiv) this.glassDiv.remove(); - if (this.debugCanvas) this.debugCanvas.remove(); - // No need to explicitly remove htmlOverlayContainer as it's a child of glassDiv - } - } - - // WebGL Renderer - class WebGLRenderer { - constructor(config, state, ui) { - this.config = config; - this.state = state; - this.ui = ui; - - this.gl = null; - this.program = null; - this.texture = null; - this.positionBuffer = null; - this.texCoordBuffer = null; - this.uniformLocations = null; - this.attributeLocations = null; - this.currentFilteringMode = null; // To store the GL constant for filtering - - this.vertexShaderSource = ` - attribute vec2 a_position; - attribute vec2 a_texCoord; - varying vec2 v_texCoord; - void main() { - gl_Position = vec4(a_position, 0.0, 1.0); - v_texCoord = a_texCoord; - } - `; - - this.fragmentShaderSource = ` - precision mediump float; - varying vec2 v_texCoord; - uniform sampler2D u_sourceTexture; - uniform vec2 u_textureOffset; - uniform vec2 u_textureRepeat; - uniform float u_glassSize; - - void main() { - vec2 sampleCoord = u_textureOffset + v_texCoord * u_textureRepeat; - vec4 color = texture2D(u_sourceTexture, sampleCoord); - gl_FragColor = color; - } - `; - - this.initialize(); - } - - initialize() { - // Get WebGL context - this.gl = this.ui.glassCanvas.getContext("webgl", { preserveDrawingBuffer: true }); - if (!this.gl) { - console.error("ComfyUI Magnifying Glass ERROR: WebGL not supported or context creation failed."); - return; - } - - // Create shader program - this.program = this.createShaderProgram(this.gl, this.vertexShaderSource, this.fragmentShaderSource); - if (!this.program) return; - - // Create position buffer - this.positionBuffer = this.gl.createBuffer(); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); - const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; - this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW); - - // Create texture coordinate buffer - this.texCoordBuffer = this.gl.createBuffer(); - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); - const texCoords = [0, 1, 1, 1, 0, 0, 1, 0]; // Flipped Y for texCoords - this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW); - - // Create texture - this.texture = this.gl.createTexture(); - this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); - this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR); - this.updateTextureFiltering(this.config.textureFiltering); // Initialize with config setting - - // Get uniform and attribute locations - this.uniformLocations = { - sourceTexture: this.gl.getUniformLocation(this.program, "u_sourceTexture"), - textureOffset: this.gl.getUniformLocation(this.program, "u_textureOffset"), - textureRepeat: this.gl.getUniformLocation(this.program, "u_textureRepeat"), - glassSize: this.gl.getUniformLocation(this.program, "u_glassSize"), - }; - - this.attributeLocations = { - position: this.gl.getAttribLocation(this.program, "a_position"), - texCoord: this.gl.getAttribLocation(this.program, "a_texCoord"), - }; - } - - updateTextureFiltering(filteringModeString) { - if (!this.gl) return; - - let glFilterMode; - if (filteringModeString === "Nearest") { - glFilterMode = this.gl.NEAREST; - } else { // Default to Linear - glFilterMode = this.gl.LINEAR; - } - - if (this.currentFilteringMode === glFilterMode) return; // No change needed - - this.currentFilteringMode = glFilterMode; - - this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, glFilterMode); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, glFilterMode); - this.gl.bindTexture(this.gl.TEXTURE_2D, null); // Unbind - // console.log(`ComfyUI Magnifying Glass: Texture filtering updated to ${filteringModeString}`); - } - - isValid() { - return this.gl !== null && this.program !== null; - } - - createShaderProgram(gl, vsSource, fsSource) { - const vertexShader = gl.createShader(gl.VERTEX_SHADER); - gl.shaderSource(vertexShader, vsSource); - gl.compileShader(vertexShader); - if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { - console.error('ComfyUI Magnifying Glass ERROR: Vertex shader compilation error:', gl.getShaderInfoLog(vertexShader)); - gl.deleteShader(vertexShader); - return null; - } - - const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); - gl.shaderSource(fragmentShader, fsSource); - gl.compileShader(fragmentShader); - if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { - console.error('ComfyUI Magnifying Glass ERROR: Fragment shader compilation error:', gl.getShaderInfoLog(fragmentShader)); - gl.deleteShader(vertexShader); - gl.deleteShader(fragmentShader); - return null; - } - - const program = gl.createProgram(); - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - console.error('ComfyUI Magnifying Glass ERROR: Shader program linking error:', gl.getProgramInfoLog(program)); - gl.deleteProgram(program); - gl.deleteShader(vertexShader); - gl.deleteShader(fragmentShader); - return null; - } - - return program; - } - - render(sourceCanvas) { - // Bind texture and update with source canvas - this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); - - try { - this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceCanvas); - } catch (e) { - console.error("ComfyUI Magnifying Glass ERROR: Error in texImage2D:", e); - return; - } - - // Calculate normalized texture coordinates (UV space: 0-1) - const uvX = this.state.sourceX / sourceCanvas.width; - const uvY = this.state.sourceY / sourceCanvas.height; - // Corrected: sourceWidth/Height are now *already* in sourceCanvas pixel units - const uvWidth = this.state.sourceWidth / sourceCanvas.width; - const uvHeight = this.state.sourceHeight / sourceCanvas.height; - - // Set up rendering - this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); - this.gl.clearColor(0.0, 0.0, 0.0, 0.0); - this.gl.clear(this.gl.COLOR_BUFFER_BIT); - - this.gl.useProgram(this.program); - - // Set uniforms - this.gl.activeTexture(this.gl.TEXTURE0); - this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture); - this.gl.uniform1i(this.uniformLocations.sourceTexture, 0); - this.gl.uniform2f(this.uniformLocations.textureOffset, uvX, uvY); - this.gl.uniform2f(this.uniformLocations.textureRepeat, uvWidth, uvHeight); - this.gl.uniform1f(this.uniformLocations.glassSize, this.config.glassSize); - - // Set attributes - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); - this.gl.vertexAttribPointer(this.attributeLocations.position, 2, this.gl.FLOAT, false, 0, 0); - this.gl.enableVertexAttribArray(this.attributeLocations.position); - - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer); - this.gl.vertexAttribPointer(this.attributeLocations.texCoord, 2, this.gl.FLOAT, false, 0, 0); - this.gl.enableVertexAttribArray(this.attributeLocations.texCoord); - - // Draw - this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4); - } - - checkWebGLError(label) { - const err = this.gl.getError(); - if (err !== this.gl.NO_ERROR) { - console.error(`ComfyUI Magnifying Glass ERROR: WebGL Error (${label}):`, err); - return true; - } - return false; - } - } - - // Event Handler - class EventHandler { - constructor(magnifyGlass) { - this.magnifyGlass = magnifyGlass; - } - - attachListeners() { - // Keydown event - document.addEventListener("keydown", this.handleKeyDown.bind(this)); - - // Keyup event - document.addEventListener("keyup", this.handleKeyUp.bind(this)); - - // Mousemove event - document.addEventListener("mousemove", this.handleMouseMove.bind(this)); - } - - // Helper function to check if user is typing in an input field - isUserTyping() { - const activeElement = document.activeElement; - if (!activeElement) return false; - - // Check if the active element is an input, textarea, or contenteditable - const tagName = activeElement.tagName.toLowerCase(); - if (tagName === 'input' || tagName === 'textarea') { - return true; - } - - // Check for contenteditable elements - if (activeElement.contentEditable === 'true') { - return true; - } - - // Check if it's inside a form or has input-like classes - if (activeElement.closest('form') || - activeElement.classList.contains('cm-editor') || // CodeMirror editor - activeElement.classList.contains('monaco-editor') || // Monaco editor - activeElement.closest('.cm-editor') || - activeElement.closest('.monaco-editor')) { - return true; - } - - return false; - } - - handleKeyDown(e) { - const config = this.magnifyGlass.config; - const state = this.magnifyGlass.state; - - // Don't handle hotkeys if user is typing in an input field (Smart Input Detection) - if (this.isUserTyping()) { - return; - } - - // Magnifier activation - if (e.key.toLowerCase() === config.activationKey && - (!config.altRequired || e.altKey)) { - if (config.alwaysActiveMode) { - if (state.active) { - state.active = false; - this.magnifyGlass.ui.hide(); - } else { - state.active = true; - this.magnifyGlass.ui.show(); - this.updateInitialPosition(); // Update position when activating - } - } else { - if (!state.active) { - state.active = true; - this.magnifyGlass.ui.show(); - this.updateInitialPosition(); // Update position when activating - } - } - } - - // Manual offset adjustment keys (only when magnifier is active) - if (state.active) { - let offsetChanged = false; - - // Use larger steps when shift is pressed - const stepSize = e.shiftKey ? config.offsetStep * 5 : config.offsetStep; - - // Arrow keys to adjust offset - if (e.key === "ArrowUp") { - config.offsetY -= stepSize; - offsetChanged = true; - e.preventDefault(); - } else if (e.key === "ArrowDown") { - config.offsetY += stepSize; - offsetChanged = true; - e.preventDefault(); - } else if (e.key === "ArrowLeft") { - config.offsetX -= stepSize; - offsetChanged = true; - e.preventDefault(); - } else if (e.key === "ArrowRight") { - config.offsetX += stepSize; - offsetChanged = true; - e.preventDefault(); - } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() && - (!config.altRequired || e.altKey)) { // Use configured reset key & check Alt - // Reset magnify glass offsets to zero - config.offsetX = 0; - config.offsetY = 0; - offsetChanged = true; - - // Reset magnify glass position to top-right corner - if (this.magnifyGlass.ui.glassDiv) { - const glassSize = config.glassSize; - const padding = 20; // Same padding as initial positioning - this.magnifyGlass.ui.glassDiv.style.left = `${window.innerWidth - glassSize - padding}px`; - this.magnifyGlass.ui.glassDiv.style.top = `${padding}px`; - // Reset the activation flag so next time it uses default positioning - this.magnifyGlass.state.wasActivatedBefore = false; - } - - // Reset inspector panel position to default - if (window.comfyUIMagnifyGlassExtensions && window.comfyUIMagnifyGlassExtensions.length > 0) { - window.comfyUIMagnifyGlassExtensions.forEach(extension => { - if (extension && extension.stateManager) { - // Reset panel to default position - extension.stateManager.state.isPanelPinned = false; - extension.stateManager.state.isPanelLocked = false; - extension.stateManager.state.pinnedPosition = { x: 0, y: 0 }; - extension.stateManager.state.lastPinnedPosition = null; - - // Update panel position and controls - if (extension.positionManager) { - extension.positionManager.positionPanel(); - } - if (extension.uiManager && extension.uiManager.updateControlStates) { - extension.uiManager.updateControlStates(); - } - } - }); - } - - this.magnifyGlass.debugger.log("Reset: Magnify glass and inspector panel positions restored to top-right defaults"); - e.preventDefault(); - } - - // Toggle Follow Cursor Key - if (e.key.toLowerCase() === config.toggleFollowCursorKey && - (!config.altRequired || e.altKey)) { - config.followCursor = !config.followCursor; - this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? 'ON' : 'OFF'}`); - if (state.active && config.followCursor) { - // If toggled ON and active, update glass position to current cursor - this.magnifyGlass.ui.positionGlass(this.magnifyGlass.lastKnownMousePosition.x, this.magnifyGlass.lastKnownMousePosition.y); - } - // We might also want to trigger an update of the settings UI if it shows this value directly, but that's more complex. - e.preventDefault(); - } - - // If offset was changed, update the view - if (offsetChanged) { - // Save the changes to localStorage - config.saveOffsets(); - this.magnifyGlass.updateMagnifiedView(); - } - } - } - - handleKeyUp(e) { - const config = this.magnifyGlass.config; - const state = this.magnifyGlass.state; - - // Check if the released key is the activation key OR if Alt was required and Alt was released. - if (!config.alwaysActiveMode && - (e.key.toLowerCase() === config.activationKey || - (config.altRequired && e.key === "Alt"))) { - - // Only deactivate if the *other* key required for activation is NOT still pressed. - // This handles cases like: Alt+X, release X (deactivate), or Alt+X, release Alt (deactivate). - let shouldDeactivate = false; - if (config.altRequired) { - // If Alt is required, releasing either Alt or the activation key deactivates. - if (e.key === "Alt" || e.key.toLowerCase() === config.activationKey) { - shouldDeactivate = true; - } - } else { - // If Alt is not required, only releasing the activation key deactivates. - if (e.key.toLowerCase() === config.activationKey) { - shouldDeactivate = true; - } - } - - if (state.active && shouldDeactivate) { - // Check if the *other* key is still pressed down. If so, don't deactivate yet. - // This is tricky because we don't have a perfect way to know key states globally - // without more complex tracking. The current logic might sometimes deactivate - // if Alt is released but X is still held (if altRequired = true). - // For simplicity, we assume releasing any part of the combo means deactivation. - state.active = false; - this.magnifyGlass.ui.hide(); // This will now also clear HTML overlays - } - } - } - - handleMouseMove(e) { - // Always track mouse position, even when magnifier is inactive - this.magnifyGlass.lastKnownMousePosition.x = e.clientX; - this.magnifyGlass.lastKnownMousePosition.y = e.clientY; - - if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return; - - const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); - const cssMouseXOnCanvas = e.clientX - rect.left; - const cssMouseYOnCanvas = e.clientY - rect.top; - - // Check if the cursor is over the canvas element (using CSS coordinates) - if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width && - cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) { - - // Convert CSS coordinates on canvas to actual canvas pixel coordinates - const canvasElement = this.magnifyGlass.litegraphCanvas; - // Ensure rect.width and rect.height are not zero to prevent division by zero - const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; - const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; - - const pixelX = cssMouseXOnCanvas * scaleX; - const pixelY = cssMouseYOnCanvas * scaleY; - - this.magnifyGlass.state.x = pixelX; - this.magnifyGlass.state.y = pixelY; - - this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY); - this.magnifyGlass.updateMagnifiedView(); - } - } - - updateInitialPosition() { - if (!this.magnifyGlass.litegraphCanvas) return; - - const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect(); - const clientX = this.magnifyGlass.lastKnownMousePosition.x; - const clientY = this.magnifyGlass.lastKnownMousePosition.y; - - // If alwaysActiveMode is true, we want to set the initial position - // even if the cursor is not strictly over the canvas at the moment of activation. - // However, we still need a valid canvas context for calculations. - // The current logic in handleMouseMove which sets state.x/y only when over canvas remains crucial - // for continuous updates. This initial positioning is a one-shot for activation. - const isOverCanvas = clientX >= rect.left && clientX <= rect.right && - clientY >= rect.top && clientY <= rect.bottom; - - if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) { - const cssMouseXOnCanvas = clientX - rect.left; - const cssMouseYOnCanvas = clientY - rect.top; - - const canvasElement = this.magnifyGlass.litegraphCanvas; - const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1; - const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1; - - const pixelX = cssMouseXOnCanvas * scaleX; - const pixelY = cssMouseYOnCanvas * scaleY; - - this.magnifyGlass.state.x = pixelX; - this.magnifyGlass.state.y = pixelY; - - // Position the glass initially at the top right corner of the window instead of at cursor - // when first activated - const glassSize = this.magnifyGlass.config.glassSize; - const padding = 20; // Padding from window edges - - if (!this.magnifyGlass.state.wasActivatedBefore) { - // First activation - position at top right of the window - this.magnifyGlass.ui.glassDiv.style.left = `${window.innerWidth - glassSize - padding}px`; - this.magnifyGlass.ui.glassDiv.style.top = `${padding}px`; - this.magnifyGlass.state.wasActivatedBefore = true; // Mark as activated once - } else { - // For subsequent activations, use normal positioning based on cursor and settings - this.magnifyGlass.ui.positionGlass(clientX, clientY); - } - - this.magnifyGlass.updateMagnifiedView(); - } - } - } - - // Debug Manager - class DebugManager { - constructor(config, state, ui) { - this.config = config; - this.state = state; - this.ui = ui; - } - - log(...args) { - if (this.config.debugMode) console.log("ComfyUI Magnifying Glass:", ...args); - } - - error(...args) { - console.error("ComfyUI Magnifying Glass ERROR:", ...args); - } - - printCanvasInfo() { - if (!this.config.debugMode) return; - - try { - const canvas = app.canvas.graph_canvas; - if (!canvas) { - this.log("Could not find graph canvas for detailed info"); - return; - } - - this.log("---- Canvas Information ----"); - this.log(`Canvas dimensions: ${canvas.width}x${canvas.height}`); - this.log(`Canvas display size: ${canvas.clientWidth}x${canvas.clientHeight}`); - this.log(`Canvas CSS transform: ${canvas.style.transform || 'none'}`); - - if (app.canvas.ds) { - this.log(`Canvas DS scale: ${app.canvas.ds.scale}`); - if (app.canvas.ds.offset) { - this.log(`Canvas DS offset: [${app.canvas.ds.offset[0]}, ${app.canvas.ds.offset[1]}]`); - } else { - this.log("Canvas DS offset not found"); - } - } else { - this.log("Canvas DS object not found"); - } - - // Try to compute the expected offset based on resolution - const screenWidth = window.innerWidth; - const screenHeight = window.innerHeight; - const canvasWidth = canvas.width; - const canvasHeight = canvas.height; - - this.log(`Window dimensions: ${screenWidth}x${screenHeight}`); - this.log(`Resolution scale factor: X=${canvasWidth/screenWidth}, Y=${canvasHeight/screenHeight}`); - this.log("---- End Canvas Information ----"); - } catch (e) { - this.log("Error in printCanvasInfo:", e); - } - } - - updateDebugView() { - if (!this.config.debugMode || !this.ui || !this.ui.debugCanvas || !this.ui.debugCtx) return; - - const debugCtx = this.ui.debugCtx; - const debugCanvas = this.ui.debugCanvas; - - debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); - - // Draw background - debugCtx.fillStyle = 'rgba(0,0,0,0.8)'; - debugCtx.fillRect(0, 0, debugCanvas.width, debugCanvas.height); - - // Header - debugCtx.fillStyle = '#FFFFFF'; - debugCtx.font = '14px monospace'; - debugCtx.fillText('Magnify Glass Debug', 10, 20); - - // Position info - debugCtx.font = '12px monospace'; - debugCtx.fillText(`Cursor: (${this.state.x.toFixed(1)}, ${this.state.y.toFixed(1)})`, 10, 50); - debugCtx.fillText(`Source Rect: (${this.state.sourceX.toFixed(1)}, ${this.state.sourceY.toFixed(1)}, w:${this.state.sourceWidth.toFixed(1)}, h:${this.state.sourceHeight.toFixed(1)})`, 10, 70); - - // Canvas transform - debugCtx.fillText(`Canvas Scale: ${this.state.canvasScale.toFixed(2)}`, 10, 90); - debugCtx.fillText(`Canvas Offset: (${this.state.canvasOffsetX.toFixed(1)}, ${this.state.canvasOffsetY.toFixed(1)})`, 10, 110); - - // Offset information - debugCtx.fillStyle = '#FFFF00'; // Yellow for emphasis - debugCtx.fillText(`MANUAL OFFSETS: X=${this.config.offsetX}, Y=${this.config.offsetY} (Use arrow keys to adjust)`, 10, 130); - debugCtx.fillStyle = '#FFFFFF'; - - // Visualization of canvas and source rect - this.drawCanvasVisualization(debugCtx, debugCanvas); - } - - drawCanvasVisualization(debugCtx, debugCanvas) { - const canvasScale = 0.1; // Scale factor for visualization - const canvasVisX = 10; - const canvasVisY = 170; - const canvasVisWidth = 380; - const canvasVisHeight = 150; - - // Draw canvas representation - debugCtx.strokeStyle = '#AAAAAA'; - debugCtx.strokeRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); - debugCtx.fillStyle = '#444444'; - debugCtx.fillRect(canvasVisX, canvasVisY, canvasVisWidth, canvasVisHeight); - - // Calculate cursor position in visualization - const cursorVisX = canvasVisX + (this.state.x * canvasScale); - const cursorVisY = canvasVisY + (this.state.y * canvasScale); - - // Calculate source rect in visualization - const sourceRectVisX = canvasVisX + (this.state.sourceX * canvasScale); - const sourceRectVisY = canvasVisY + (this.state.sourceY * canvasScale); - const sourceRectVisWidth = this.state.sourceWidth * canvasScale; - const sourceRectVisHeight = this.state.sourceHeight * canvasScale; - - // Draw source rect - debugCtx.strokeStyle = '#FF0000'; - debugCtx.strokeRect(sourceRectVisX, sourceRectVisY, sourceRectVisWidth, sourceRectVisHeight); - - // Draw cursor position - debugCtx.fillStyle = '#FFFF00'; - debugCtx.beginPath(); - debugCtx.arc(cursorVisX, cursorVisY, 3, 0, Math.PI * 2); - debugCtx.fill(); - - // Draw line from cursor to source rect center - debugCtx.strokeStyle = '#00FF00'; - debugCtx.beginPath(); - debugCtx.moveTo(cursorVisX, cursorVisY); - debugCtx.lineTo(sourceRectVisX + sourceRectVisWidth/2, sourceRectVisY + sourceRectVisHeight/2); - debugCtx.stroke(); - - // Label for visualization - debugCtx.fillStyle = '#FFFFFF'; - debugCtx.fillText('Canvas Visualization (scaled)', canvasVisX, canvasVisY - 5); - - // Offset values - const offsetX = this.state.sourceX - this.state.x + (this.state.sourceWidth / 2); - const offsetY = this.state.sourceY - this.state.y + (this.state.sourceHeight / 2); - debugCtx.fillText(`Alignment Offset: (${offsetX.toFixed(1)}, ${offsetY.toFixed(1)})`, 10, canvasVisY + canvasVisHeight + 20); - } - } - - // Instantiate the main class - const magnifyGlass = new MagnifyGlass(); - - // Add settings to the ComfyUI settings dialog - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.ZoomFactor", - name: "🔍 Magnify Glass: Zoom Factor (%)", - type: "slider", - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.ZoomFactor"], - min: 100, - max: 1000, - step: 25, - tooltip: "Magnification level as a percentage (e.g., 300 = 3x zoom, 150 = 1.5x zoom).", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - // Convert percentage to decimal (e.g., 300 -> 3.0) - magnifyGlass.config.zoomFactor = value / 100; - if (magnifyGlass.state.active) { - magnifyGlass.updateMagnifiedView(); - } - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.GlassSize", - name: "🔍 Magnify Glass: Size (px)", - type: "slider", - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.GlassSize"], - min: 50, - max: 100, - step: 10, - tooltip: "Diameter of the magnifying glass circle in pixels. Slider max is 100, but larger values can be manually set if needed.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.glassSize = parseInt(value, 10); - magnifyGlass.applyUiChanges(); // Update div/canvas size - if (magnifyGlass.state.active) { - magnifyGlass.updateMagnifiedView(); // Recalculate source based on new size - } - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.BorderWidth", - name: "🔍 Magnify Glass: Border Width (px)", - type: "slider", - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.BorderWidth"], - min: 0, - max: 10, - step: 0.1, - tooltip: "Width of the border around the magnifying glass (supports decimal values like 0.5, 1.5, etc.).", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.borderWidth = parseFloat(value); - magnifyGlass.applyUiChanges(); // Update border style - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.BorderColor", - name: "🎨 Magnify Glass: Border Color", - type: "color", - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.BorderColor"], - tooltip: "Color of the border around the magnifying glass.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - // Ensure color value includes # symbol for valid CSS - const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; - magnifyGlass.config.borderColor = normalizedColor; - - // Only save back if we actually normalized the color (to prevent recursion) - if (value !== normalizedColor) { - try { - app.ui.settings.setSettingValue("🔍MagnifyGlass.BorderColor", normalizedColor); - } catch (e) { - console.warn('Failed to save normalized border color:', e); - } - } - - magnifyGlass.applyUiChanges(); // Update border style - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.ActivationKey", - name: "⌨️ Magnify Glass: Activation Key", - type: "combo", - options: ["x", "z", "m", "q", "v", "c"], // Limited selection for simplicity - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.ActivationKey"], - tooltip: "The key (case-insensitive) to hold down to activate the magnifier. Works with Alt/Option if 'Require Alt/Option Key' is Yes.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.activationKey = value.toLowerCase(); - magnifyGlass.debugger.log(`Activation key set to ${magnifyGlass.config.altRequired ? 'Alt/Option+' : ''}${magnifyGlass.config.activationKey.toUpperCase()}`); - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.AltRequired", - name: "⌨️ Magnify Glass: Require Alt/Option Key", - type: "combo", - options: [ { value: true, text: "Yes" }, { value: false, text: "No" } ], - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.AltRequired"], - tooltip: "If Yes, Alt (Windows/Linux) or Option (Mac) must be held for activation and for reset. Not needed when 'Smart Input Detection' prevents hotkeys during typing.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.altRequired = value; - magnifyGlass.debugger.log(`Require Alt/Option set to ${value}. Activation: ${magnifyGlass.config.altRequired ? 'Alt/Option+' : ''}${magnifyGlass.config.activationKey.toUpperCase()}, Reset: ${magnifyGlass.config.altRequired ? 'Alt/Option+' : ''}${magnifyGlass.config.resetKey.toUpperCase()}`); - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.FollowCursor", - name: "🖱️ Magnify Glass: Follow Cursor Position", - type: "combo", - options: [ { value: true, text: "Yes" }, { value: false, text: "No" } ], - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.FollowCursor"], - tooltip: "If Yes, the magnifier window moves with the cursor. If No, it stays where activated.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.followCursor = value; - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.OffsetStep", - name: "⌨️ Magnify Glass: Offset Adjust Step (Graph Units)", - type: "slider", - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.OffsetStep"], - min: 1, - max: 50, // Increased max - step: 1, - tooltip: "How many graph units the view shifts when pressing arrow keys (Shift+Arrow = 5x).", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.offsetStep = parseInt(value, 10); - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.ResetKey", - name: "⌨️ Magnify Glass: Reset Offset Key", - type: "combo", - options: ["r", "o", "p", "k", "l"], // Example keys, can be expanded - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.ResetKey"], - tooltip: "The key (case-insensitive) to press to reset the magnify glass offset and inspector panel position to their default locations while the magnifier is active. Works with Alt/Option if 'Require Alt for Reset' is Yes.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.resetKey = value.toLowerCase(); - magnifyGlass.debugger.log(`Reset offset key set to ${magnifyGlass.config.resetAltRequired ? 'Alt/Option+' : ''}${magnifyGlass.config.resetKey.toUpperCase()}`); - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.DebugMode", - name: "🐞 Magnify Glass: Debug Mode", - type: "combo", - options: [ { value: true, text: "Enabled" }, { value: false, text: "Disabled" } ], - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.DebugMode"], - tooltip: "Show detailed logging and the debug visualization overlay.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.debugMode = value; - magnifyGlass.applyUiChanges(); // Show/hide debug canvas - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.GlassPosition", - name: "🖱️ Magnify Glass: Glass Position", - type: "combo", - options: [ - { value: "Bottom", text: "Bottom (Default)" }, - { value: "Top", text: "Top" }, - { value: "Left", text: "Left" }, - { value: "Right", text: "Right" }, - { value: "Top-Left", text: "Top-Left" }, - { value: "Top-Right", text: "Top-Right" }, - { value: "Bottom-Left", text: "Bottom-Left" }, - { value: "Bottom-Right", text: "Bottom-Right" } - ], - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.GlassPosition"], - tooltip: "Position of the magnifying glass relative to the cursor.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.glassPosition = value; - // If glass is active and not following cursor, might need to reposition - if (magnifyGlass.state.active && !magnifyGlass.config.followCursor) { - // To reposition, we need the last known cursor position. - // This assumes lastKnownMousePosition is up-to-date. - // A more robust solution might involve storing the activation point. - const { x, y } = magnifyGlass.lastKnownMousePosition; - magnifyGlass.ui.positionGlass(x, y); - } - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.GlassShape", - name: "🖼️ Magnify Glass: Shape", - type: "combo", - options: [ - { value: "Circle", text: "Circle" }, - { value: "Square", text: "Square" }, - { value: "Rounded Square", text: "Rounded Square" } - ], - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.GlassShape"], - tooltip: "Shape of the magnifying glass (e.g., Circle, Square).", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.glassShape = value; - magnifyGlass.applyUiChanges(); // Update border-radius - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.BorderEnabled", - name: "🖼️ Magnify Glass: Show Border", - type: "combo", - options: [ - { value: true, text: "Yes" }, - { value: false, text: "No" } - ], - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.BorderEnabled"], - tooltip: "Enable or disable the border around the magnifying glass.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.borderEnabled = value; - magnifyGlass.applyUiChanges(); // Update border visibility - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.TextureFiltering", - name: "🖼️ Magnify Glass: Texture Filtering", - type: "combo", - options: [ - { value: "Linear", text: "Linear (Smooth)" }, - { value: "Nearest", text: "Nearest (Pixelated)" } - ], - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.TextureFiltering"], - tooltip: "Controls how the magnified image is scaled. Linear is smoother, Nearest is sharper/pixelated.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config && magnifyGlass.renderer) { - magnifyGlass.config.textureFiltering = value; - magnifyGlass.renderer.updateTextureFiltering(value); - if (magnifyGlass.state.active) { - magnifyGlass.updateMagnifiedView(); // Re-render if active - } - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.AlwaysActiveMode", - name: "🔒 Magnify Glass: Always Active Mode", - type: "combo", - options: [ - { value: true, text: "Yes" }, - { value: false, text: "No" } - ], - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.AlwaysActiveMode"], - tooltip: "If Yes, activating the magnifier keeps it on until activated again. If No, it deactivates on key release.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.alwaysActiveMode = value; - // No immediate UI change needed other than behavior change - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.ToggleFollowCursorKey", - name: "🔑 Magnify Glass: Toggle Follow Key", - type: "combo", - options: ["f", "g", "h", "j", "k"], // Example keys, can be expanded - defaultValue: DEFAULT_SETTINGS["🔍MagnifyGlass.ToggleFollowCursorKey"], - tooltip: "The key (case-insensitive) to toggle the 'Follow Cursor' behavior. Works with Alt/Option if 'Require Alt/Option Key' is Yes.", - onChange: (value) => { - if (magnifyGlass && magnifyGlass.config) { - magnifyGlass.config.toggleFollowCursorKey = value.toLowerCase(); - magnifyGlass.debugger.log(`Toggle Follow Cursor key set to ${magnifyGlass.config.altRequired ? 'Alt/Option+' : ''}${magnifyGlass.config.toggleFollowCursorKey.toUpperCase()}`); - } - } - }); - - // --- Initialization --- - - // Load settings into the config object *after* settings are registered - magnifyGlass.config.loadSettings(); - - // Initialize the magnifier (reads config, creates UI, attaches listeners) - magnifyGlass.init(); - - // Apply UI changes - magnifyGlass.applyUiChanges(); - - // Expose magnifyGlass globally for extensions - window.comfyUIMagnifyGlass = magnifyGlass; - - // Diagnostic Info - console.log("---- Enhanced Magnifier Diagnostic Info ----"); - // Browser Info - console.log("User Agent:", navigator.userAgent); - console.log("Device Pixel Ratio:", window.devicePixelRatio); - // ComfyUI Canvas State - if (typeof app !== 'undefined' && app.canvas && app.canvas.ds) { - console.log("ComfyUI Canvas Scale:", app.canvas.ds.scale); - console.log("ComfyUI Canvas Offset:", app.canvas.ds.offset); - } else { - console.log("ComfyUI Canvas State: Could not access app.canvas.ds"); - } - // Stored Manual Offsets - try { - const offsetX = localStorage.getItem('comfyui_magnify_offset_x') || 'Not Set (Will be 0)'; - const offsetY = localStorage.getItem('comfyui_magnify_offset_y') || 'Not Set (Will be 0)'; - console.log("Stored Manual Offset X:", offsetX); - console.log("Stored Manual Offset Y:", offsetY); - } catch (e) { - console.error("Error reading offsets from localStorage:", e); - } - console.log("---- End Diagnostic Info ----"); - console.log("REMINDER: Please also copy the 'ComfyUI Magnifying Glass:' log lines that appeared *before* this diagnostic block."); - console.log("REMINDER: Please provide Browser Name/Version, Browser Zoom %, and a Screenshot if reporting issues."); - } // End setup() -}); // End app.registerExtension \ No newline at end of file + name: "comfyui.magnify.glass", + init() { + const magnifyGlass = new MagnifyGlass(); + magnifyGlass.init(); + window.comfyUIMagnifyGlass = magnifyGlass; + console.log("ComfyUI Magnify Glass: Extension initialized"); + } +}); +//# sourceMappingURL=magnify_glass.js.map diff --git a/web/magnify_glass.js.map b/web/magnify_glass.js.map new file mode 100644 index 0000000..c277b92 --- /dev/null +++ b/web/magnify_glass.js.map @@ -0,0 +1 @@ +{"version":3,"file":"magnify_glass.js","sources":["../src/magnify_glass.ts"],"sourcesContent":["/**\n * ComfyUI Magnify Glass - Main Entry Point\n * \n * Initializes the Magnify Glass extension.\n */\n\nimport { MagnifyGlass } from './magnify-glass';\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\n\n// Global declaration handled in comfyui.d.ts\n\n// We rely on the global 'app' object provided by ComfyUI\n// Register extension\napp.registerExtension({\n name: \"comfyui.magnify.glass\",\n init() {\n // Initialize the magnify glass\n const magnifyGlass = new MagnifyGlass();\n magnifyGlass.init();\n\n // Expose to window for other extensions (like info panel) to access\n window.comfyUIMagnifyGlass = magnifyGlass;\n\n console.log(\"ComfyUI Magnify Glass: Extension initialized\");\n }\n});\n"],"names":[],"mappings":";;AAcA,IAAI,kBAAkB;AAAA,EAClB,MAAM;AAAA,EACN,OAAO;AAEH,UAAM,eAAe,IAAI,aAAA;AACzB,iBAAa,KAAA;AAGb,WAAO,sBAAsB;AAE7B,YAAQ,IAAI,8CAA8C;AAAA,EAC9D;AACJ,CAAC;"} \ No newline at end of file diff --git a/web/magnify_glass_refactored.js b/web/magnify_glass_refactored.js deleted file mode 100644 index e8f0642..0000000 --- a/web/magnify_glass_refactored.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * ComfyUI Magnifying Glass - Entry Point (Refactored) - * - * This script adds a magnifying glass feature to ComfyUI. - * Press X to activate the magnifying glass and see a zoomed view of the canvas. - * - * This file has been refactored to use modular ES6 imports. - * The actual implementation is split across the magnify-glass/ directory. - */ - -import { app } from "../../../scripts/app.js"; -import { MagnifyGlass } from './magnify-glass/MagnifyGlass.js'; -import { registerGlassSettings, DEFAULT_GLASS_SETTINGS } from './shared/settings.js'; - -app.registerExtension({ - name: "comfyui.magnify.glass", - async setup() { - // Instantiate the main magnify glass - const magnifyGlass = new MagnifyGlass(); - - // Register settings with ComfyUI - registerGlassSettings(magnifyGlass); - - // Load settings into the config object - magnifyGlass.config.loadSettings(); - - // Initialize the magnifier - magnifyGlass.init(); - - // Apply UI changes - magnifyGlass.applyUiChanges(); - - // Expose magnifyGlass globally for extensions - window.comfyUIMagnifyGlass = magnifyGlass; - - // Diagnostic Info - console.log("---- Enhanced Magnifier Diagnostic Info ----"); - console.log("User Agent:", navigator.userAgent); - console.log("Device Pixel Ratio:", window.devicePixelRatio); - - if (typeof app !== 'undefined' && app.canvas && app.canvas.ds) { - console.log("ComfyUI Canvas Scale:", app.canvas.ds.scale); - console.log("ComfyUI Canvas Offset:", app.canvas.ds.offset); - } else { - console.log("ComfyUI Canvas State: Could not access app.canvas.ds"); - } - - try { - const offsetX = localStorage.getItem('comfyui_magnify_offset_x') || 'Not Set (Will be 0)'; - const offsetY = localStorage.getItem('comfyui_magnify_offset_y') || 'Not Set (Will be 0)'; - console.log("Stored Manual Offset X:", offsetX); - console.log("Stored Manual Offset Y:", offsetY); - } catch (e) { - console.error("Error reading offsets from localStorage:", e); - } - console.log("---- End Diagnostic Info ----"); - } -}); diff --git a/web/magnify_info_panel.js b/web/magnify_info_panel.js index dea1ef4..5560b7c 100644 --- a/web/magnify_info_panel.js +++ b/web/magnify_info_panel.js @@ -1,60 +1,36 @@ -/** - * ComfyUI MagnifyGlass - Info Panel (Refactored Entry Point) - * - * This is the refactored version of the info panel that uses modular imports. - * The original magnify_info_panel.js is preserved as a backup. - */ - -import { app } from "../../../scripts/app.js"; -import { InfoPanel } from './info-panel/index.js'; -import { DEFAULT_PANEL_SETTINGS, getSettingConfig } from './shared/settings.js'; - +import { app } from "/scripts/app.js"; +import { InfoPanel } from "./info-panel/InfoPanel.js"; app.registerExtension({ - name: "Comfy.MagnifyGlass.InfoPanel.Refactored", - - async setup() { - // Wait for magnify glass to be ready - const checkForMagnifyGlass = () => { - return new Promise((resolve) => { - const check = () => { - if (window.comfyUIMagnifyGlass) { - resolve(window.comfyUIMagnifyGlass); - } else { - setTimeout(check, 100); - } - }; - check(); - }); - }; - - // Get magnify glass instance - const magnifyGlass = await checkForMagnifyGlass(); - - // Create info panel manager - const infoPanelManager = new InfoPanel(magnifyGlass); - - // Store reference globally for cleanup and theme updates - if (!window.comfyUIMagnifyGlassExtensions) { - window.comfyUIMagnifyGlassExtensions = []; - } - window.comfyUIMagnifyGlassExtensions.push(infoPanelManager); - - // Store main manager reference for theme updates - window.infoPanelManager = infoPanelManager; - - // Register settings with ComfyUI - Object.keys(DEFAULT_PANEL_SETTINGS).forEach(settingKey => { - const settingConfig = getSettingConfig(settingKey, DEFAULT_PANEL_SETTINGS[settingKey]); - if (settingConfig) { - app.ui.settings.addSetting({ - ...settingConfig, - onChange: (value) => { - infoPanelManager.updateSettings(); - } - }); - } - }); - - console.log("ComfyUI Magnify Info Panel (Refactored): Loaded successfully"); - } + name: "comfyui.magnify.info.panel", + init() { + const checkDependencies = () => { + if (window.comfyUIMagnifyGlass) { + initializeInfoPanel(); + } else { + setTimeout(checkDependencies, 100); + } + }; + checkDependencies(); + } }); +function initializeInfoPanel() { + const magnifyGlass = window.comfyUIMagnifyGlass; + if (!magnifyGlass) { + console.error("ComfyUI Magnify Info Panel: Failed to find MagnifyGlass instance."); + return; + } + try { + const infoPanel = new InfoPanel(magnifyGlass); + window.infoPanelManager = infoPanel; + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = "extensions/comfyui-magnifyglass/style.css"; + document.head.appendChild(link); + console.log("ComfyUI Magnify Info Panel: CSS loaded"); + console.log("ComfyUI Magnify Info Panel: Extension initialized"); + } catch (e) { + console.error("ComfyUI Magnify Info Panel: Error during initialization:", e); + } +} +//# sourceMappingURL=magnify_info_panel.js.map diff --git a/web/magnify_info_panel.js.backup b/web/magnify_info_panel.js.backup deleted file mode 100644 index 58cc47f..0000000 --- a/web/magnify_info_panel.js.backup +++ /dev/null @@ -1,3035 +0,0 @@ -import { app } from "../../../scripts/app.js"; - -/** - * ComfyUI Magnifying Glass - Professional Information Panel Extension (Refactored) - * - * This script provides a professional, highly interactive information panel that displays - * detailed analysis of whatever you're hovering over when the magnifying glass is active. - * - * Key Features: - * - Unified state management - * - Smooth mode transitions - * - Improved pin functionality with drag support - * - Lock feature to prevent dragging when pinned - * - Clean control system - * - Better positioning logic - */ - -app.registerExtension({ - name: "comfyui.magnify.glass.info.panel.pro.v2", - async setup() { - // Wait for the main magnifying glass to be ready - let magnifyGlass = null; - let retryCount = 0; - const maxRetries = 50; - - const waitForMagnifyGlass = () => { - if (window.comfyUIMagnifyGlass) { - magnifyGlass = window.comfyUIMagnifyGlass; - console.log("ComfyUI Magnify Info Panel Pro V2: Found magnifying glass instance"); - initializeInfoPanel(); - } else if (retryCount < maxRetries) { - retryCount++; - setTimeout(waitForMagnifyGlass, 100); - } else { - console.warn("ComfyUI Magnify Info Panel Pro V2: Could not find magnifying glass instance after 5 seconds"); - } - }; - - setTimeout(waitForMagnifyGlass, 100); - - function initializeInfoPanel() { - // Default settings configuration - const DEFAULT_SETTINGS = { - "🔍MagnifyGlass.InfoPanelEnabled": true, - "🔍MagnifyGlass.InfoPanelPosition": "Bottom", - "🔍MagnifyGlass.InfoPanelWidth": 320, - "🔍MagnifyGlass.InfoPanelOpacity": 100, - "🔍MagnifyGlass.InfoPanelMaxHeight": 1000, - "🔍MagnifyGlass.InfoPanelAnimations": false, - "🔍MagnifyGlass.ShowInspectorTab": false, - "🔍MagnifyGlass.ToggleHotkey": "i", - "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g", - "🔍MagnifyGlass.PinPanelHotkey": "u", - "🔍MagnifyGlass.ShowHoveringControls": true, - "🔍MagnifyGlass.ControlsPosition": "bottom", - "🔍MagnifyGlass.InfoPanelTextColor": "#6b7280", - "🔍MagnifyGlass.InfoPanelAccentColor": "#3b82f6", - }; - - const getSettingValue = (key, defaultValue) => { - try { - const value = app.ui.settings.getSettingValue(key); - return value === undefined ? defaultValue : value; - } catch (e) { - console.warn(`ComfyUI Magnify Info Panel Pro V2: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`); - return defaultValue; - } - }; - - /** - * Centralized State Manager - * Handles all state-related operations and provides clean state transitions - */ - class StateManager { - constructor() { - this.state = { - // Panel visibility and positioning - isPanelVisible: false, - isPanelMinimized: false, - isPanelPinned: false, - isPanelLocked: false, // New lock state - isAutoPinned: false, // Track if panel was auto-pinned by glass hide - pinnedPosition: { x: 0, y: 0 }, - lastPinnedPosition: null, // Remember last pinned location - - // Glass visibility - isGlassPreviewVisible: true, - - // Section expansion - expandedSections: new Set(['node']), - - // Interaction states - isPanelHovered: false, - isHoveringNode: false, - - // Auto-collapse - autoExpandTimer: null, - lastNodeId: null, - - // Update scheduling - updateScheduled: false, - isInitialLoading: false, - - // Current data - currentInfo: {}, - - // Settings cache - settings: {}, - - // Auto-detected theme - currentTheme: 'dark' // Will be auto-detected - }; - - this.initThemeDetection(); - this.loadSettings(); - } - - initThemeDetection() { - // Immediate forced detection - // console.log('ComfyUI Magnify Glass: Starting theme detection...'); - - // Detect ComfyUI's current theme - this.detectCurrentTheme(); - - // Force an immediate check in case we missed the initial state - setTimeout(() => { - // console.log('ComfyUI Magnify Glass: Delayed theme check...'); - if (this.detectCurrentTheme()) { - this.notifyThemeChange(); - } - }, 1000); - - // Set up mutation observer to watch for theme changes - this.setupThemeObserver(); - - // Listen for system theme changes as fallback - if (window.matchMedia) { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); - prefersDark.addListener(() => this.detectCurrentTheme()); - } - - // Add click listener to theme toggle buttons for immediate detection - this.setupThemeButtonListeners(); - } - - detectCurrentTheme() { - let detectedTheme = 'dark'; // Default fallback - - const body = document.body; - const html = document.documentElement; - const vueApp = document.querySelector('#vue-app'); - const sidebar = document.querySelector('.comfy-menu, .sidebar, .menu, [class*="sidebar"], [class*="menu"]'); - - // Get all possible theme-related elements - const allElements = [body, html, vueApp, sidebar].filter(Boolean); - - // Debug: Log current classes and attributes with more detail - const debugInfo = { - bodyClasses: Array.from(body.classList), - htmlClasses: Array.from(html.classList), - bodyDataTheme: body.getAttribute('data-theme'), - htmlDataTheme: html.getAttribute('data-theme'), - vueAppClasses: vueApp ? Array.from(vueApp.classList) : null, - sidebarClasses: sidebar ? Array.from(sidebar.classList) : null, - bodyBgColor: window.getComputedStyle(body).backgroundColor, - htmlBgColor: window.getComputedStyle(html).backgroundColor, - allComputedVars: {} - }; - - // Check CSS variables - const rootStyles = window.getComputedStyle(html); - const bodyStyles = window.getComputedStyle(body); - - // Get all CSS custom properties - for (let i = 0; i < rootStyles.length; i++) { - const prop = rootStyles[i]; - if (prop.startsWith('--')) { - debugInfo.allComputedVars[prop] = rootStyles.getPropertyValue(prop).trim(); - } - } - - // console.log('ComfyUI Magnify Glass: Comprehensive theme detection debug:', debugInfo); - - // Method 1: Look for specific light theme indicators in CSS variables - const primarySurface = rootStyles.getPropertyValue('--p-primary-color') || - rootStyles.getPropertyValue('--primary-color') || - bodyStyles.getPropertyValue('--p-primary-color') || - bodyStyles.getPropertyValue('--primary-color'); - - const surfaceGround = rootStyles.getPropertyValue('--p-surface-ground') || - rootStyles.getPropertyValue('--surface-ground') || - bodyStyles.getPropertyValue('--p-surface-ground') || - bodyStyles.getPropertyValue('--surface-ground'); - - const textColor = rootStyles.getPropertyValue('--p-text-color') || - rootStyles.getPropertyValue('--text-color') || - bodyStyles.getPropertyValue('--p-text-color') || - bodyStyles.getPropertyValue('--text-color'); - - // console.log('ComfyUI Magnify Glass: CSS Variables:', { primarySurface, surfaceGround, textColor }); - - // Method 2: Analyze background colors of multiple elements - const backgrounds = allElements.map(el => { - const styles = window.getComputedStyle(el); - return { - element: el.tagName || el.className, - backgroundColor: styles.backgroundColor, - color: styles.color - }; - }); - - // console.log('ComfyUI Magnify Glass: Background analysis:', backgrounds); - - // Method 3: Check for light backgrounds - for (const bg of backgrounds) { - if (bg.backgroundColor && bg.backgroundColor !== 'rgba(0, 0, 0, 0)' && bg.backgroundColor !== 'transparent') { - const rgbMatch = bg.backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (rgbMatch) { - const [, r, g, b] = rgbMatch.map(Number); - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - // console.log(`ComfyUI Magnify Glass: Element ${bg.element} brightness: ${brightness}`); - if (brightness > 180) { // Higher threshold for light detection - detectedTheme = 'light'; - // console.log(`ComfyUI Magnify Glass: Light theme detected via background analysis of ${bg.element}`); - break; - } - } - } - } - - // Method 4: Check for theme toggle buttons state (they might indicate current theme) - const themeButtons = document.querySelectorAll('[class*="theme"], button[title*="theme"], button[title*="Theme"], .p-button[title*="light"], .p-button[title*="Light"]'); - themeButtons.forEach(btn => { - // console.log('ComfyUI Magnify Glass: Found theme button:', btn); - }); - - // Method 5: Force light theme detection if we see light backgrounds - if (detectedTheme === 'dark') { - // Look for any element with very light background - const lightElements = document.querySelectorAll('*'); - for (let i = 0; i < Math.min(lightElements.length, 50); i++) { // Check first 50 elements - const el = lightElements[i]; - const styles = window.getComputedStyle(el); - const bgColor = styles.backgroundColor; - - if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { - const rgbMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - if (rgbMatch) { - const [, r, g, b] = rgbMatch.map(Number); - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - if (brightness > 200) { // Very light elements - detectedTheme = 'light'; - // console.log(`ComfyUI Magnify Glass: Light theme forced via element ${el.tagName}.${el.className} with brightness ${brightness}`); - break; - } - } - } - } - } - - // Update theme if it changed - if (this.state.currentTheme !== detectedTheme) { - // console.log(`ComfyUI Magnify Glass: Theme changed from ${this.state.currentTheme} to ${detectedTheme}`); - this.state.currentTheme = detectedTheme; - return true; // Theme changed - } - - // console.log(`ComfyUI Magnify Glass: Current theme remains: ${detectedTheme}`); - return false; // No change - } - - setupThemeObserver() { - // Watch for changes to body/html classes and attributes - const observer = new MutationObserver((mutations) => { - let shouldCheck = false; - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && - (mutation.attributeName === 'class' || - mutation.attributeName === 'data-theme' || - mutation.attributeName === 'style')) { - shouldCheck = true; - // console.log('ComfyUI Magnify Glass: DOM change detected:', { - // target: mutation.target.tagName, - // attribute: mutation.attributeName, - // oldValue: mutation.oldValue, - // newValue: mutation.target.getAttribute(mutation.attributeName) - // }); - } - }); - - if (shouldCheck && this.detectCurrentTheme()) { - // Theme changed, update UI immediately - this.notifyThemeChange(); - } - }); - - // Observe changes to body, html, and vue app - observer.observe(document.body, { attributes: true, attributeOldValue: true }); - observer.observe(document.documentElement, { attributes: true, attributeOldValue: true }); - - const vueApp = document.querySelector('#vue-app'); - if (vueApp) { - observer.observe(vueApp, { attributes: true, attributeOldValue: true }); - } - - // Also watch for changes to any element with theme-related classes - const themeElements = document.querySelectorAll('[class*="theme"], [class*="dark"], [class*="light"], [data-theme]'); - themeElements.forEach(el => { - observer.observe(el, { attributes: true, attributeOldValue: true }); - }); - - // Periodic fallback check every 2 seconds - setInterval(() => { - if (this.detectCurrentTheme()) { - this.notifyThemeChange(); - } - }, 2000); - } - - setupThemeButtonListeners() { - // Listen for clicks on theme toggle buttons - document.addEventListener('click', (e) => { - // Check if clicked element might be a theme toggle - const target = e.target; - const isThemeButton = target.textContent?.toLowerCase().includes('theme') || - target.textContent?.toLowerCase().includes('light') || - target.textContent?.toLowerCase().includes('dark') || - target.title?.toLowerCase().includes('theme') || - target.className?.toLowerCase().includes('theme'); - - if (isThemeButton) { - // console.log('ComfyUI Magnify Glass: Theme button clicked, triggering detection'); - setTimeout(() => { - if (this.detectCurrentTheme()) { - this.notifyThemeChange(); - } - }, 100); - } - }); - } - - notifyThemeChange() { - // Update settings to reflect new theme - this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; - - // Notify UI components of theme change immediately - if (window.infoPanelManager && window.infoPanelManager.uiManager) { - window.infoPanelManager.uiManager.updateTheme(this.state.currentTheme); - } - } - - loadSettings() { - Object.keys(DEFAULT_SETTINGS).forEach(key => { - // Skip the theme setting since we auto-detect it - if (key !== "🔍MagnifyGlass.InfoPanelTheme") { - this.state.settings[key] = getSettingValue(key, DEFAULT_SETTINGS[key]); - } - }); - - // Set theme to auto-detected value - this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; - } - - updateSettings() { - const oldSettings = { ...this.state.settings }; - this.loadSettings(); - - // Return what changed for reactive updates - const changes = {}; - Object.keys(this.state.settings).forEach(key => { - if (oldSettings[key] !== this.state.settings[key]) { - changes[key] = { - old: oldSettings[key], - new: this.state.settings[key] - }; - } - }); - - return changes; - } - - togglePanelVisibility() { - this.state.isPanelVisible = !this.state.isPanelVisible; - // console.log(`Info panel visibility toggled to ${this.state.isPanelVisible ? 'visible' : 'hidden'}`); - return this.state.isPanelVisible; - } - - toggleGlassPreview() { - this.state.isGlassPreviewVisible = !this.state.isGlassPreviewVisible; - // console.log(`Glass preview visibility toggled to ${this.state.isGlassPreviewVisible ? 'visible' : 'hidden'}`); - return this.state.isGlassPreviewVisible; - } - - togglePinning() { - const wasPinned = this.state.isPanelPinned; - this.state.isPanelPinned = !this.state.isPanelPinned; - - // When unpinning, also unlock and clear auto-pin flag - if (!this.state.isPanelPinned) { - this.state.isPanelLocked = false; - this.state.isAutoPinned = false; // Clear auto-pin flag when manually unpinning - } - - if (this.state.isPanelPinned && this.state.lastPinnedPosition) { - // Restore to last pinned position - this.state.pinnedPosition = { ...this.state.lastPinnedPosition }; - // console.log(`Panel pinned at remembered position: (${this.state.pinnedPosition.x}, ${this.state.pinnedPosition.y})`); - } else if (!this.state.isPanelPinned && wasPinned) { - // Save current position when unpinning - this.state.lastPinnedPosition = { ...this.state.pinnedPosition }; - // console.log(`Panel unpinned, position saved: (${this.state.lastPinnedPosition.x}, ${this.state.lastPinnedPosition.y})`); - } - - // console.log(`Panel ${this.state.isPanelPinned ? 'pinned' : 'unpinned'}`); - return this.state.isPanelPinned; - } - - toggleLocking() { - // Only allow locking when pinned - if (!this.state.isPanelPinned) { - // console.log("Cannot lock panel when not pinned"); - return false; - } - - this.state.isPanelLocked = !this.state.isPanelLocked; - // console.log(`Panel ${this.state.isPanelLocked ? 'locked' : 'unlocked'}`); - return this.state.isPanelLocked; - } - - toggleMinimized() { - this.state.isPanelMinimized = !this.state.isPanelMinimized; - return this.state.isPanelMinimized; - } - - toggleSection(sectionId) { - if (sectionId === 'node') return false; // Node section always expanded - - if (this.state.expandedSections.has(sectionId)) { - this.state.expandedSections.delete(sectionId); - } else { - this.state.expandedSections.add(sectionId); - } - return true; - } - - setPinnedPosition(x, y) { - this.state.pinnedPosition = { x, y }; - // Also save as last pinned position for memory - this.state.lastPinnedPosition = { x, y }; - } - - setCurrentInfo(info) { - this.state.currentInfo = info; - } - - scheduleAutoCollapse() { - this.clearAutoExpandTimer(); - if (this.state.settings["🔍MagnifyGlass.InfoPanelAnimations"]) { - this.state.autoExpandTimer = setTimeout(() => { - if (!this.state.isPanelHovered) { - this.collapseNodeSections(); - } - }, 1500); - } - } - - clearAutoExpandTimer() { - if (this.state.autoExpandTimer) { - clearTimeout(this.state.autoExpandTimer); - this.state.autoExpandTimer = null; - } - } - - expandNodeSections() { - this.state.expandedSections.add('hoveredNode'); - this.state.expandedSections.add('node'); - this.state.expandedSections.add('cursor'); - this.state.expandedSections.add('canvas'); - this.state.expandedSections.add('magnifier'); - this.clearAutoExpandTimer(); - } - - collapseNodeSections() { - this.state.expandedSections.delete('hoveredNode'); - this.state.expandedSections.delete('node'); - this.state.expandedSections.delete('widget'); - } - - cleanup() { - this.clearAutoExpandTimer(); - } - } - - /** - * UI Manager - * Handles all DOM manipulation and UI creation - */ - class UIManager { - constructor(stateManager) { - this.stateManager = stateManager; - this.elements = { - panel: null, - header: null, - content: null, - controls: null - }; - - this.createPanel(); - this.injectStyles(); - } - - createPanel() { - // Main panel container - this.elements.panel = document.createElement("div"); - this.elements.panel.id = "comfyui-magnify-info-panel-pro-v2"; - this.elements.panel.className = `magnify-info-panel theme-${this.stateManager.state.currentTheme}`; - - // Header - this.elements.header = document.createElement("div"); - this.elements.header.className = "panel-header"; - this.elements.header.innerHTML = ` -
-
🔍
-
Inspector
-
Real-time analysis
-
-
- -
- `; - - // Content container - this.elements.content = document.createElement("div"); - this.elements.content.className = "panel-content"; - - this.elements.panel.appendChild(this.elements.header); - this.elements.panel.appendChild(this.elements.content); - - this.applyStyles(); - document.body.appendChild(this.elements.panel); - - // Create floating controls after panel is in DOM - if (this.stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"]) { - this.createFloatingControls(); - // Update control states immediately after creation - this.updateControlStates(); - } - } - - createFloatingControls() { - this.elements.controls = document.createElement("div"); - this.elements.controls.className = "floating-controls vertical-layout"; // Default to vertical - this.elements.controls.innerHTML = ` - - - - - `; - - // Insert before the panel in the document body, not as a child - document.body.appendChild(this.elements.controls); - - // Set initial layout based on settings - const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; - this.updateControlsLayout(controlsPosition); - } - - updateControlStates() { - if (!this.elements.controls) return; - - const pinBtn = this.elements.controls.querySelector('[data-action="pin"]'); - const lockBtn = this.elements.controls.querySelector('[data-action="lock"]'); - const visibilityBtn = this.elements.controls.querySelector('[data-action="toggle-panel"]'); - const glassBtn = this.elements.controls.querySelector('[data-action="toggle-glass"]'); - - if (pinBtn) { - pinBtn.classList.toggle('active', this.stateManager.state.isPanelPinned); - pinBtn.title = this.stateManager.state.isPanelPinned ? "Lock Panel" : "Unlock Panel"; - pinBtn.textContent = this.stateManager.state.isPanelPinned ? "🔒" : "🔓"; - } - - if (lockBtn) { - // Only show/enable lock button when pinned - lockBtn.style.display = this.stateManager.state.isPanelPinned ? 'flex' : 'none'; - lockBtn.classList.toggle('active', this.stateManager.state.isPanelLocked); - lockBtn.title = this.stateManager.state.isPanelLocked ? "Upin Panel Position" : "Pin Panel Position"; - lockBtn.disabled = !this.stateManager.state.isPanelPinned; - } - - if (visibilityBtn) { - visibilityBtn.classList.toggle('active', this.stateManager.state.isPanelVisible); - visibilityBtn.title = this.stateManager.state.isPanelVisible ? "Show Panel" : "Hide Panel"; - } - - if (glassBtn) { - glassBtn.classList.toggle('active', this.stateManager.state.isGlassPreviewVisible); - glassBtn.title = this.stateManager.state.isGlassPreviewVisible ? "Hide Glass Preview" : "Show Glass Preview"; - } - } - - updateControlsLayout(position) { - if (!this.elements.controls) return; - - // Remove existing layout classes - this.elements.controls.classList.remove('horizontal-layout', 'vertical-layout'); - - // Add appropriate layout class - if (['top', 'bottom'].includes(position)) { - this.elements.controls.classList.add('horizontal-layout'); - } else { - this.elements.controls.classList.add('vertical-layout'); - } - } - - applyStyles() { - const settings = this.stateManager.state.settings; - - // Apply custom text color via CSS variable if provided - const textColor = settings["🔍MagnifyGlass.InfoPanelTextColor"]; - if (textColor) { - // Ensure color has # symbol - const normalizedTextColor = textColor.startsWith('#') ? textColor : `#${textColor}`; - this.elements.panel.style.setProperty('--info-panel-text-color', normalizedTextColor); - } - - // Apply custom accent color via CSS variable if provided - const accentColor = settings["🔍MagnifyGlass.InfoPanelAccentColor"]; - if (accentColor) { - // Ensure color has # symbol - const normalizedAccentColor = accentColor.startsWith('#') ? accentColor : `#${accentColor}`; - this.elements.panel.style.setProperty('--info-panel-accent-color', normalizedAccentColor); - } - - // Apply opacity setting if panel is visible (convert percentage to decimal) - if (this.stateManager.state.isPanelVisible) { - const opacityPercent = settings["🔍MagnifyGlass.InfoPanelOpacity"]; - this.elements.panel.style.opacity = opacityPercent / 100; - } - - this.elements.panel.style.cssText = ` - position: absolute; - width: ${settings["🔍MagnifyGlass.InfoPanelWidth"]}px; - max-height: ${settings["🔍MagnifyGlass.InfoPanelMaxHeight"]}px; - z-index: 99999; - display: none; - opacity: 0; - transform: translateY(-10px); - transition: ${settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none'}; - pointer-events: auto; - user-select: none; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - ${textColor ? `--info-panel-text-color: ${textColor.startsWith('#') ? textColor : `#${textColor}`};` : ''} - ${accentColor ? `--info-panel-accent-color: ${accentColor.startsWith('#') ? accentColor : `#${accentColor}`};` : ''} - `; - } - - show() { - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { - this.elements.panel.style.display = "block"; - if (this.elements.controls) { - this.elements.controls.style.display = "flex"; - } - // Apply user's opacity setting when showing (convert percentage to decimal) - const opacityPercent = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"]; - this.elements.panel.style.opacity = opacityPercent / 100; - // Trigger reflow - this.elements.panel.offsetHeight; - this.elements.panel.classList.add('visible'); - this.stateManager.state.isPanelVisible = true; - } - } - - hide() { - this.elements.panel.classList.remove('visible'); - setTimeout(() => { - if (!this.stateManager.state.isPanelVisible) { - this.elements.panel.style.display = "none"; - if (this.elements.controls) { - this.elements.controls.style.display = "none"; - } - } - }, this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] ? 300 : 0); - this.stateManager.state.isPanelVisible = false; - } - - updateMinimizedState() { - this.elements.panel.classList.toggle('panel-minimized', this.stateManager.state.isPanelMinimized); - const minimizeBtn = this.elements.header.querySelector('.minimize-btn'); - if (minimizeBtn) { - minimizeBtn.textContent = this.stateManager.state.isPanelMinimized ? '+' : '−'; - minimizeBtn.title = this.stateManager.state.isPanelMinimized ? 'Expand Panel' : 'Minimize Panel'; - } - } - - updatePinnedState() { - this.elements.panel.classList.toggle('panel-pinned', this.stateManager.state.isPanelPinned); - this.elements.panel.classList.toggle('panel-locked', this.stateManager.state.isPanelLocked); - this.updateControlStates(); - } - - updateTheme(newTheme) { - if (this.elements.panel) { - this.elements.panel.className = this.elements.panel.className.replace(/theme-\w+/, `theme-${newTheme.toLowerCase()}`); - // console.log(`ComfyUI Magnify Glass: Panel theme updated to ${newTheme}`); - } - } - - displayInfo(info) { - const sections = this.buildSections(info); - this.renderSections(sections); - this.updateSectionStates(); - this.updateHeaderSubtitle(info); - } - - buildSections(info) { - const sections = []; - const settings = this.stateManager.state.settings; - - // Inspector section - if (settings["🔍MagnifyGlass.ShowInspectorTab"]) { - const inspectorContent = [ - { label: 'Cursor Canvas', value: `(${Math.round(info.cursor.canvas.x)}, ${Math.round(info.cursor.canvas.y)})` }, - { label: 'Canvas Scale', value: `${(info.canvas.scale * 100).toFixed(1)}%` }, - { label: 'Magnifier Zoom', value: `${info.magnifier.zoomFactor}×` } - ]; - - sections.push({ - id: 'inspector', - icon: '🔍', - title: 'Inspector', - content: inspectorContent - }); - } - - // Media section - if (info.media) { - const mediaContent = [ - { label: 'Type', value: info.media.tagName }, - { label: 'Source', value: info.media.src } - ]; - - if (info.media.naturalSize) { - mediaContent.push({ label: 'Natural', value: info.media.naturalSize }); - } - - sections.push({ - id: 'media', - icon: '📷', - title: 'Media', - badge: info.media.tagName, - content: mediaContent - }); - } - - // Node Details section - if (info.hoveredNode) { - const nodeContent = [ - { label: 'Title', value: info.hoveredNode.title } - ]; - - // Check if this is a complex node that shows all widgets - const nodeType = info.hoveredNode.type.toLowerCase(); - const isSaveNode = nodeType.includes('save') && - !nodeType.includes('checkpoint') && - !nodeType.includes('model') && - !nodeType.includes('preview'); - - const showAllWidgets = info.hoveredNode.type && ( - nodeType.includes('ksampler') || - nodeType.includes('sampler') || - nodeType.includes('k_samplers') || - nodeType.includes('checkpoint') || - nodeType.includes('model') || - nodeType.includes('lora') || - nodeType.includes('controlnet') || - nodeType.includes('advanced') || - nodeType.includes('detailer') || - nodeType.includes('inpaint') || - nodeType.includes('upscale') || - nodeType.includes('clip') || - nodeType.includes('text') || - nodeType.includes('encode') - ) && !isSaveNode; - - if (!showAllWidgets) { - // Add specific extractions for simple nodes - const checkpointInfo = this.getCheckpointInfo(info.hoveredNode); - if (checkpointInfo) { - nodeContent.push({ label: 'Model', value: checkpointInfo }); - } - - const imageInfo = this.getImageInfo(info.hoveredNode); - if (imageInfo) { - if (typeof imageInfo === 'string') { - nodeContent.push({ label: 'Image', value: imageInfo }); - } else if (typeof imageInfo === 'object') { - nodeContent.push({ label: 'Image Size', value: `${imageInfo.width}×${imageInfo.height}` }); - if (imageInfo.src) { - nodeContent.push({ label: 'Image Source', value: imageInfo.src }); - } - } - } - - const textBoxContent = this.getTextBoxContent(info.hoveredNode); - if (textBoxContent) { - nodeContent.push({ label: 'Text', value: textBoxContent }); - } - } - - const importantParameters = this.getImportantNodeParameters(info.hoveredNode); - nodeContent.push(...importantParameters); - - sections.push({ - id: 'node', - icon: '🎯', - title: 'Node', - badge: info.hoveredNode.type, - content: nodeContent - }); - } - - return sections; - } - - renderSections(sections) { - if (sections.length === 0) { - this.elements.content.innerHTML = ` -
-
📍
-
Empty canvas area
-
- `; - return; - } - - this.elements.content.innerHTML = sections.map(section => ` -
-
- ${section.icon} - ${section.title} - ${section.badge ? `${section.badge}` : ''} - ${section.id !== 'node' ? '▶' : ''} -
-
-
- ${section.content.map(item => { - const value = this.formatValue(item.value, item.label); - const valueClass = this.getValueClass(item.value); - const valueAttributes = this.getValueAttributes(item.value); - return ` -
- ${item.label} - ${value} -
`; - }).join('')} -
-
-
- `).join(''); - } - - updateSectionStates() { - const sections = this.elements.content.querySelectorAll('.info-section'); - sections.forEach(section => { - const sectionId = section.dataset.section; - const header = section.querySelector('.section-header'); - const content = section.querySelector('.section-content'); - - if (this.stateManager.state.expandedSections.has(sectionId)) { - header.classList.add('expanded'); - content.classList.add('expanded'); - } else { - header.classList.remove('expanded'); - content.classList.remove('expanded'); - } - }); - } - - formatValue(value, label) { - if (!value) return value; - - const str = String(value); - - // Show full text for prompts, text content, model names, and file paths - if (label && ( - label.toLowerCase().includes('text') || - label.toLowerCase().includes('prompt') || - label.toLowerCase().includes('model') || - label.toLowerCase().includes('file') || - label.toLowerCase().includes('conditioning') || - label.toLowerCase().includes('positive') || - label.toLowerCase().includes('negative') - )) { - return str; - } - - // Show full text for very long values (no truncation) - return str; - } - - getValueClass(value) { - if (!value) return ''; - - const str = String(value); - let classes = []; - - // Mark text that might benefit from special styling for readability - if (str.length > 100) { - classes.push('long-text'); - } - - return classes.join(' '); - } - - getValueAttributes(value) { - // Since we show full text now, we don't need title attributes for long text - // Only add title for very long text that might benefit from tooltips - if (!value) return ''; - - const str = String(value); - if (str.length > 500) { // Only for extremely long text - return `title="${str.replace(/"/g, '"')}"`; - } - - return ''; - } - - updateHeaderSubtitle(info) { - const subtitleElement = this.elements.header.querySelector('.header-subtitle'); - if (subtitleElement) { - const accentColor = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"]; - if (info.hoveredNode) { - subtitleElement.textContent = `Analyzing: ${info.hoveredNode.title}`; - subtitleElement.style.color = accentColor; - } else if (info.media) { - subtitleElement.textContent = `Media: ${info.media.tagName}`; - subtitleElement.style.color = accentColor; - } else if (info.connection) { - subtitleElement.textContent = `Connection: ${info.connection.type}`; - subtitleElement.style.color = accentColor; - } else { - subtitleElement.textContent = 'Real-time analysis'; - subtitleElement.style.color = ''; - } - } - } - - // Helper methods for node information extraction - getCheckpointInfo(nodeInfo) { - if (nodeInfo.type && ( - nodeInfo.type.includes("CheckpointLoader") || - nodeInfo.type.includes("LoadCheckpoint") || - nodeInfo.type.includes("ModelLoader") || - nodeInfo.type.includes("UNETLoader") || - nodeInfo.type.includes("VAELoader") || - nodeInfo.type.includes("LoraLoader") - )) { - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - for (const widget of nodeInfo.widgets) { - if (widget.name && ( - widget.name.toLowerCase().includes("model") || - widget.name.toLowerCase().includes("checkpoint") || - widget.name.toLowerCase().includes("ckpt") || - widget.name.toLowerCase().includes("lora") || - widget.name.toLowerCase().includes("vae") || - widget.name.toLowerCase().includes("file") - )) { - const value = String(widget.value); - const filename = value.split(/[\/\\]/).pop(); - return filename || value; - } - } - } - } - return null; - } - - getImageInfo(nodeInfo) { - if (nodeInfo.type && ( - nodeInfo.type.includes("SaveImage") || - nodeInfo.type.includes("PreviewImage") || - nodeInfo.type.includes("VisionOutput") || - nodeInfo.type.includes("ImageOutput") || - nodeInfo.type.includes("LoadImage") || - nodeInfo.type.includes("Display") - )) { - if (nodeInfo.widgets) { - for (const widget of nodeInfo.widgets) { - if (widget.name && ( - widget.name.toLowerCase().includes("image") || - widget.name.toLowerCase().includes("filename") || - widget.name.toLowerCase().includes("file") - )) { - return widget.value; - } - } - } - - if (nodeInfo.properties && nodeInfo.properties.img) { - const img = nodeInfo.properties.img; - return { - width: img.width || "unknown", - height: img.height || "unknown", - src: img.src ? img.src.split(/[\/\\]/).pop() : "Preview available" - }; - } - - if (nodeInfo.outputs) { - for (const output of nodeInfo.outputs) { - if (output.links && output.links.length > 0) { - return "Image connected to " + output.links.length + " node(s)"; - } - } - } - - return "Image node"; - } - return null; - } - - getTextBoxContent(nodeInfo) { - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - if (nodeInfo.type && nodeInfo.type.includes("CLIPTextEncode")) { - for (const widget of nodeInfo.widgets) { - if (widget.name === "text" && typeof widget.value === 'string') { - return widget.value; - } - } - } - - for (const widget of nodeInfo.widgets) { - if ((widget.name.toLowerCase().includes("prompt") || - widget.name.toLowerCase().includes("conditioning")) && - typeof widget.value === 'string' && widget.value.length > 0) { - return widget.value; - } - } - - for (const widget of nodeInfo.widgets) { - if ((widget.type === 'text' || widget.type === 'textarea' || - widget.type === 'string' || widget.name.toLowerCase().includes('text')) && - typeof widget.value === 'string' && widget.value.length > 0) { - - // Show full text without truncation - return widget.value; - } - } - } - return null; - } - - getImportantNodeParameters(nodeInfo) { - const parameters = []; - - // For complex nodes that have many parameters, show ALL widgets - const nodeType = nodeInfo.type.toLowerCase(); - const isSaveNode = nodeType.includes('save') && - !nodeType.includes('checkpoint') && - !nodeType.includes('model') && - !nodeType.includes('preview'); - - const showAllWidgets = nodeInfo.type && ( - nodeType.includes('ksampler') || - nodeType.includes('sampler') || - nodeType.includes('k_samplers') || - nodeType.includes('checkpoint') || - nodeType.includes('model') || - nodeType.includes('lora') || - nodeType.includes('controlnet') || - nodeType.includes('advanced') || - nodeType.includes('detailer') || - nodeType.includes('inpaint') || - nodeType.includes('upscale') || - nodeType.includes('clip') || - nodeType.includes('text') || - nodeType.includes('encode') - ) && !isSaveNode; - - if (showAllWidgets) { - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - for (const widget of nodeInfo.widgets) { - // Show all widgets for complex nodes, but filter out some duplicates - if (widget.name && widget.name !== '') { - const widgetName = widget.name.toLowerCase(); - - // Skip some generic widget names that might be duplicates - // These are handled by specific extraction methods for simple nodes - if (widgetName.includes('title') || - widgetName === 'node' || - widgetName === 'id' || - widgetName === 'type' || - widgetName === 'mode') { - continue; - } - - parameters.push({ - label: widget.name, - value: this.formatValue(widget.value) - }); - } - } - } - return parameters; - } - - // Special handling for Save nodes - only show essential parameters - let importantParams; - if (isSaveNode) { - importantParams = [ - 'filename_prefix', 'filename', 'directory', 'path', - 'format', 'quality', 'extension' - ]; - } else { - // For other nodes, use the filtered list but avoid duplicates with specific extractions - importantParams = [ - 'seed', 'steps', 'cfg', 'scale', 'sampler', 'scheduler', - 'positive', 'negative', 'width', 'height', 'denoise', 'strength', - 'noise', 'count', 'batch', 'size', 'phase', 'color', 'intensity', - // KSampler specific parameters (for other samplers) - 'control_after_generate', 'control', 'after', 'generate', - 'start_at_step', 'end_at_step', 'start', 'end', - 'return_with_leftover_noise', 'leftover', 'noise_return', - // Additional common parameters (but avoid duplicates with specific extractors) - 'model', 'vae', 'clip', 'lora', 'checkpoint', - 'latent', 'image', 'mask', 'filename', 'directory', - 'prompt', 'conditioning', 'filename_prefix', - // New detection vocabulary - 'resolution', 'num_chunks', 'seconds', 'aspect_ratio', - 'style_type', 'background', 'n', 'human', 'raw', 'guidance', - 'skip_preprocessing', 'movement_amplitude', 'animation', - 'material_type', 'b1', 'b2', 's1', 's2', 'type', 'channel', - 'sigma', 'rho', - // Additional detection vocabulary - 'alpha', 'base_shift', 'shift', 'stretch', 'terminal', - 'spacing', 'style', 'eta', 'norm_threshold', 'momentum', - 'hypernetwork_name', 'reuse_threshold', 'verbose', 'layers', - 'set_cond_area', 'audioui', - // Camera and 3D parameters - 'camera_pose', 'fx', 'cx', 'fy', 'cy' - // Note: 'text' and 'string' removed to avoid duplication with getTextBoxContent - ]; - } - - if (nodeInfo.widgets && nodeInfo.widgets.length > 0) { - for (const widget of nodeInfo.widgets) { - const paramName = widget.name.toLowerCase(); - if (importantParams.some(param => paramName.includes(param))) { - parameters.push({ - label: widget.name, - value: this.formatValue(widget.value) - }); - } - } - } - - return parameters; - } - - formatValue(value) { - if (value === null) return "null"; - if (value === undefined) return "undefined"; - if (typeof value === "string") { - return value; // Show full text without truncation - } - if (typeof value === "number") { - return Number.isInteger(value) ? value.toString() : value.toFixed(3); - } - if (typeof value === "boolean") return value.toString(); - if (Array.isArray(value)) { - return `Array(${value.length})`; - } - if (typeof value === "object") { - return "Object"; - } - return String(value); - } - - injectStyles() { - if (!document.getElementById('magnify-info-panel-styles-v2')) { - const style = document.createElement('style'); - style.id = 'magnify-info-panel-styles-v2'; - style.textContent = this.getCSS(); - document.head.appendChild(style); - } - } - - getCSS() { - return ` - .magnify-info-panel { - border-radius: 12px; - backdrop-filter: blur(20px); - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1); - overflow: hidden; - font-size: 16px; - line-height: 1.6; - pointer-events: auto; - } - - .magnify-info-panel * { - pointer-events: auto; - } - - .magnify-info-panel.theme-dark { - background: linear-gradient(135deg, rgba(0, 0, 0, 0.96), rgba(10, 10, 15, 0.96)); - color: var(--info-panel-text-color, #e0e0e0); - border: 1px solid rgba(100, 100, 120, 0.4); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 16px rgba(0, 0, 0, 0.2); - backdrop-filter: blur(12px); - } - - .magnify-info-panel.theme-light { - background: linear-gradient(135deg, rgba(250, 250, 255, 0.96), rgba(240, 240, 250, 0.96)); - color: var(--info-panel-text-color, #2a2a2a); - border: 1px solid rgba(200, 200, 220, 0.5); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 16px rgba(0, 0, 0, 0.05); - backdrop-filter: blur(12px); - } - - .magnify-info-panel.visible { - transform: translateY(0) !important; - } - - .magnify-info-panel.panel-pinned { - cursor: grab; - box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); - } - - .magnify-info-panel.panel-locked { - cursor: default !important; - box-shadow: 0 0 0 2px rgba(255, 69, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.3); - } - - .magnify-info-panel.panel-locked::after { - content: '📌'; - position: absolute; - top: 8px; - right: 8px; - font-size: 14px; - opacity: 0.7; - pointer-events: none; - z-index: 1; - } - - .magnify-info-panel.panel-pinned.panel-dragging { - cursor: grabbing; - box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(255, 215, 0, 0.7); - transform: scale(1.02) !important; - z-index: 100001 !important; - transition: none !important; - } - - .magnify-info-panel.panel-pinned::before { - content: ''; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - background: linear-gradient(45deg, rgba(255, 215, 0, 0.05) 0%, transparent 100%); - pointer-events: none; - border-radius: inherit; - } - - .panel-dragging * { - pointer-events: none !important; - user-select: none !important; - transition: none !important; - } - - .magnify-info-panel.panel-minimized .panel-content { - max-height: 0 !important; - overflow: hidden; - transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); - } - - .floating-controls { - position: fixed; - display: flex; - padding: 4px; - background: rgba(40, 40, 50, 0.95); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 4px; - opacity: 1; - z-index: 100000; - backdrop-filter: blur(8px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); - } - - .floating-controls.vertical-layout { - flex-direction: column; - gap: 4px; - } - - .floating-controls.horizontal-layout { - flex-direction: row; - gap: 4px; - } - - .control-btn { - background: rgba(60, 60, 70, 0.8); - border: 1px solid rgba(255, 255, 255, 0.2); - width: 28px; - height: 28px; - font-size: 14px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - color: #ddd; - border-radius: 3px; - transition: background-color 0.15s ease; - } - - .control-btn:hover { - background: rgba(80, 80, 90, 0.9); - color: white; - } - - .control-btn:active { - background: rgba(100, 100, 110, 0.9); - } - - .control-btn:disabled { - background: rgba(40, 40, 50, 0.5); - color: #666; - cursor: not-allowed; - opacity: 0.5; - } - - .control-btn.active { - background: rgba(255, 200, 50, 0.3); - border-color: rgba(255, 200, 50, 0.6); - color: #ffcc33; - } - - .control-btn.lock-btn.active { - background: rgba(255, 69, 0, 0.3); - border-color: rgba(255, 69, 0, 0.6); - color: #ff4500; - } - - .panel-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 18px; - background: linear-gradient(135deg, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.05)); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - user-select: none; - } - - .header-content { - display: flex; - align-items: center; - gap: 10px; - } - - .header-icon { - font-size: 18px; - opacity: 0.9; - } - - .header-title { - font-weight: 600; - font-size: 18px; - } - - .header-subtitle { - font-size: 14px; - opacity: 0.7; - font-style: italic; - } - - .header-controls { - display: flex; - gap: 6px; - } - - .minimize-btn { - background: rgba(255, 255, 255, 0.1); - border: none; - border-radius: 6px; - color: inherit; - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s; - font-weight: bold; - font-size: 16px; - } - - .minimize-btn:hover { - background: rgba(255, 255, 255, 0.2); - transform: scale(1.05); - } - - .minimize-btn:active { - transform: scale(0.95); - } - - .panel-content { - padding: 0; - max-height: calc(100% - 60px); - overflow-y: auto; - scrollbar-width: thin; - } - - .panel-content::-webkit-scrollbar { - width: 8px; - } - - .panel-content::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.1); - } - - .panel-content::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 3px; - } - - .info-section { - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - margin-bottom: 8px; - border-radius: 10px; - overflow: visible; - background: rgba(255, 255, 255, 0.02); - transition: all 0.3s ease; - } - - .info-section:hover { - background: rgba(255, 255, 255, 0.04); - transform: translateY(-1px); - } - - .section-header { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 20px; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - font-weight: 500; - border-radius: 8px; - margin: 3px; - position: relative; - overflow: hidden; - } - - .section-header::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.05) 50%, transparent 100%); - transform: translateX(-100%); - transition: transform 0.6s ease; - } - - .section-header:hover::before { - transform: translateX(100%); - } - - .section-header:hover { - background: rgba(255, 255, 255, 0.08); - transform: translateX(2px); - box-shadow: inset 2px 0 0 rgba(116, 185, 255, 0.5); - } - - .section-header:active { - background: rgba(255, 255, 255, 0.12); - transform: translateX(1px); - } - - .section-header.expanded { - background: rgba(116, 185, 255, 0.1); - border-left: 2px solid var(--info-panel-accent-color, #74b9ff); - } - - .info-section[data-section="node"] { - background: linear-gradient(135deg, rgba(116, 185, 255, 0.05), rgba(160, 212, 104, 0.05)); - border: 1px solid rgba(116, 185, 255, 0.2); - border-radius: 8px; - margin: 6px; - } - - .info-section[data-section="node"] .section-header { - background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(160, 212, 104, 0.15)); - border-radius: 8px 8px 0 0; - cursor: default; - } - - .info-section[data-section="node"] .section-header:hover { - transform: none; - box-shadow: none; - } - - .info-section[data-section="node"] .section-title { - color: var(--info-panel-accent-color, #74b9ff); - font-weight: 700; - } - - .info-section[data-section="inspector"] { - background: linear-gradient(135deg, rgba(100, 150, 255, 0.05), rgba(74, 144, 226, 0.05)); - border: 1px solid rgba(100, 150, 255, 0.2); - border-radius: 8px; - margin: 6px; - } - - .info-section[data-section="inspector"] .section-header { - background: linear-gradient(135deg, rgba(100, 150, 255, 0.15), rgba(74, 144, 226, 0.15)); - border-radius: 8px 8px 0 0; - } - - .info-section[data-section="inspector"] .section-title { - color: #6496ff; - font-weight: 700; - } - - .info-section[data-section="media"] { - background: linear-gradient(135deg, rgba(253, 203, 110, 0.05), rgba(255, 215, 0, 0.05)); - border: 1px solid rgba(253, 203, 110, 0.2); - border-radius: 8px; - margin: 6px; - } - - .info-section[data-section="media"] .section-header { - background: linear-gradient(135deg, rgba(253, 203, 110, 0.15), rgba(255, 215, 0, 0.15)); - border-radius: 8px 8px 0 0; - } - - .info-section[data-section="media"] .section-title { - color: var(--info-panel-accent-color, #fdcb6e); - font-weight: 700; - } - - .section-icon { - font-size: 18px; - width: 22px; - text-align: center; - } - - .section-title { - flex: 1; - font-size: 16px; - font-weight: 600; - letter-spacing: 0.3px; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - } - - .section-badge { - background: rgba(100, 150, 255, 0.2); - color: #8bb3ff; - padding: 3px 10px; - border-radius: 10px; - font-size: 13px; - font-weight: 500; - } - - .expand-icon { - font-size: 14px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - color: rgba(255, 255, 255, 0.6); - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: rgba(255, 255, 255, 0.05); - margin-left: auto; - } - - .section-header:hover .expand-icon { - color: var(--info-panel-accent-color, #74b9ff); - background: rgba(116, 185, 255, 0.15); - transform: scale(1.1); - } - - .section-header.expanded .expand-icon { - transform: rotate(90deg) scale(1.1); - color: var(--info-panel-accent-color, #74b9ff); - background: rgba(116, 185, 255, 0.2); - } - - .section-content { - max-height: 0; - overflow: visible; - transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; - opacity: 0; - } - - .section-content.expanded { - max-height: 2000px; - opacity: 1; - overflow: visible; - } - - .section-body { - padding: 0 18px 16px 24px; - } - - .info-row { - display: flex; - justify-content: space-between; - align-items: flex-start; - padding: 10px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - transition: all 0.2s ease; - border-radius: 4px; - margin: 0 -8px; - padding-left: 8px; - padding-right: 8px; - } - - .info-row:last-child { - border-bottom: none; - } - - .info-row:hover { - background: rgba(255, 255, 255, 0.03); - border-bottom-color: rgba(255, 255, 255, 0.08); - } - - .info-label { - font-weight: 500; - opacity: 0.85; - font-size: 15px; - min-width: 80px; - padding-right: 12px; - line-height: 1.5; - } - - .info-value { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 14px; - color: var(--info-panel-accent-color, #a0d468); - background: rgba(160, 212, 104, 0.12); - padding: 4px 10px; - border-radius: 6px; - max-width: 100%; - word-wrap: break-word; - word-break: break-word; - hyphens: auto; - line-height: 1.4; - text-align: left; - white-space: pre-wrap; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - transition: all 0.2s ease; - } - - .info-value:hover { - background: rgba(160, 212, 104, 0.18); - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - } - - .info-value.long-text { - white-space: pre-wrap; - word-wrap: break-word; - max-width: 100%; - line-height: 1.4; - } - - .empty-state { - text-align: center; - padding: 50px 25px; - opacity: 0.6; - } - - .empty-state-icon { - font-size: 36px; - margin-bottom: 15px; - opacity: 0.5; - } - - /* Copy to clipboard functionality */ - .info-value[title]:hover::after { - content: '📋'; - position: absolute; - right: -20px; - top: 50%; - transform: translateY(-50%); - font-size: 12px; - opacity: 0.7; - } - - /* Smooth loading states */ - .section-body.loading { - opacity: 0.5; - pointer-events: none; - } - - .section-body.loading::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 20px; - height: 20px; - border: 2px solid transparent; - border-top: 2px solid var(--info-panel-accent-color, #74b9ff); - border-radius: 50%; - animation: spin 1s linear infinite; - transform: translate(-50%, -50%); - } - - @keyframes spin { - to { transform: translate(-50%, -50%) rotate(360deg); } - } - - /* Enhanced scrollbar for better UX */ - .section-body::-webkit-scrollbar { - width: 4px; - } - - .section-body::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 2px; - } - - .section-body::-webkit-scrollbar-thumb { - background: var(--info-panel-accent-color, #74b9ff); - border-radius: 2px; - opacity: 0.6; - } - - .section-body::-webkit-scrollbar-thumb:hover { - opacity: 1; - } - - .empty-state-text { - font-size: 16px; - font-style: italic; - } - - @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } - } - - .pulse { - animation: pulse 2s infinite; - } - `; - } - - cleanup() { - if (this.elements.panel) { - this.elements.panel.remove(); - } - - if (this.elements.controls) { - this.elements.controls.remove(); - } - - const styles = document.getElementById('magnify-info-panel-styles-v2'); - if (styles) { - styles.remove(); - } - } - } - - /** - * Position Manager - * Handles all positioning logic including pinning and boundary checking - */ - class PositionManager { - constructor(stateManager, uiManager) { - this.stateManager = stateManager; - this.uiManager = uiManager; - } - - positionPanel() { - if (!this.uiManager.elements.panel) return; - - // If panel is pinned, keep it at its pinned position - if (this.stateManager.state.isPanelPinned) { - this.applyPinnedPosition(); - this.positionFloatingControls(); - return; - } - - // Normal positioning logic - this.calculateNormalPosition(); - this.positionFloatingControls(); - } - - positionFloatingControls() { - if (!this.uiManager.elements.controls) return; - - const panelRect = this.uiManager.elements.panel.getBoundingClientRect(); - const controlsPosition = this.stateManager.state.settings["🔍MagnifyGlass.ControlsPosition"] || "top-right"; - - let idealX, idealY; - // Determine dimensions based on layout. Fallback values are estimates. - const isHorizontal = ['top', 'bottom'].includes(controlsPosition); - const controlsWidth = this.uiManager.elements.controls.offsetWidth || (isHorizontal ? 160 : 40); - const controlsHeight = this.uiManager.elements.controls.offsetHeight || (isHorizontal ? 40 : 160); - const margin = 5; - - // Determine the ideal, unconstrained position for the controls - switch (controlsPosition) { - case "top-left": - idealX = panelRect.left - controlsWidth - margin; - idealY = panelRect.top; - break; - case "top-right": - idealX = panelRect.right + margin; - idealY = panelRect.top; - break; - case "bottom-left": - idealX = panelRect.left - controlsWidth - margin; - idealY = panelRect.bottom - controlsHeight; - break; - case "bottom-right": - idealX = panelRect.right + margin; - idealY = panelRect.bottom - controlsHeight; - break; - case "top": - idealX = panelRect.left + (panelRect.width - controlsWidth) / 2; - idealY = panelRect.top - controlsHeight - margin; - break; - case "bottom": - idealX = panelRect.left + (panelRect.width - controlsWidth) / 2; - idealY = panelRect.bottom + margin; - break; - case "left": - idealX = panelRect.left - controlsWidth - margin; - idealY = panelRect.top + (panelRect.height - controlsHeight) / 2; - break; - case "right": - idealX = panelRect.right + margin; - idealY = panelRect.top + (panelRect.height - controlsHeight) / 2; - break; - default: // Fallback to top-right - idealX = panelRect.right + margin; - idealY = panelRect.top; - break; - } - - let finalX = idealX; - let finalY = idealY; - - // For side-positioned controls, check if they are forced to move horizontally. - // If so, override their vertical alignment to be "top" instead of "center". - if (['left', 'right'].includes(controlsPosition)) { - if (idealX < margin || (idealX + controlsWidth) > (window.innerWidth - margin)) { - finalY = panelRect.top; // Switch to top alignment - - // Also, place it slightly inside the panel to ensure it's visible. - if (controlsPosition === 'left') { - finalX = margin; - } else { // 'right' - finalX = window.innerWidth - controlsWidth - margin; - } - } - } - - // Apply boundary constraints to the final calculated position - finalX = Math.max(margin, Math.min(finalX, window.innerWidth - controlsWidth - margin)); - finalY = Math.max(margin, Math.min(finalY, window.innerHeight - controlsHeight - margin)); - - this.uiManager.elements.controls.style.left = `${finalX}px`; - this.uiManager.elements.controls.style.top = `${finalY}px`; - - // Update layout class based on the setting - this.uiManager.updateControlsLayout(controlsPosition); - } - - applyPinnedPosition() { - let { x, y } = this.stateManager.state.pinnedPosition; - const panel = this.uiManager.elements.panel; - const panelWidth = panel.offsetWidth; - const panelHeight = panel.offsetHeight; - const margin = 10; - - // Ensure the pinned panel stays within the viewport boundaries, e.g., after content expands. - // This adjustment is now temporary and won't update the stored pinned position. - const boundedX = Math.max(margin, Math.min(x, window.innerWidth - panelWidth - margin)); - const boundedY = Math.max(margin, Math.min(y, window.innerHeight - panelHeight - margin)); - - panel.style.left = `${boundedX}px`; - panel.style.top = `${boundedY}px`; - } - - calculateNormalPosition() { - const settings = this.stateManager.state.settings; - const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; - const panelHeight = Math.min(settings["🔍MagnifyGlass.InfoPanelMaxHeight"], this.uiManager.elements.panel.scrollHeight); - - let left, top; - const margin = 15; - - if (!this.stateManager.state.isGlassPreviewVisible) { - // Position at mouse cursor when glass is hidden - left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); - top = magnifyGlass.lastKnownMousePosition.y - 20; - } else { - // Position relative to magnify glass - const glassRect = magnifyGlass.ui.glassDiv?.getBoundingClientRect(); - if (glassRect) { - const position = settings["🔍MagnifyGlass.InfoPanelPosition"]; - - switch (position) { - case "Right": - left = glassRect.right + margin; - top = glassRect.top; - break; - case "Left": - left = glassRect.left - panelWidth - margin; - top = glassRect.top; - break; - case "Top": - left = glassRect.left; - top = glassRect.top - panelHeight - margin; - break; - case "Bottom": - left = glassRect.left; - top = glassRect.bottom + margin; - break; - default: - left = glassRect.right + margin; - top = glassRect.top; - break; - } - } else { - // Fallback to mouse position - left = magnifyGlass.lastKnownMousePosition.x - (panelWidth / 2); - top = magnifyGlass.lastKnownMousePosition.y - 20; - } - } - - // Apply boundary constraints - left = Math.max(10, Math.min(left, window.innerWidth - panelWidth - 10)); - top = Math.max(10, Math.min(top, window.innerHeight - panelHeight - 10)); - - this.uiManager.elements.panel.style.left = `${left}px`; - this.uiManager.elements.panel.style.top = `${top}px`; - } - } - - /** - * Event Manager - * Handles all event binding and delegation - */ - class EventManager { - constructor(stateManager, uiManager, positionManager) { - this.stateManager = stateManager; - this.uiManager = uiManager; - this.positionManager = positionManager; - - this.setupEventListeners(); - } - - setupEventListeners() { - this.setupPanelEvents(); - this.setupHotkeyEvents(); - this.setupDragEvents(); - this.setupHoverEvents(); - } - - setupPanelEvents() { - // Event delegation for panel controls - this.uiManager.elements.panel.addEventListener('click', (e) => { - const action = e.target.dataset.action; - if (action) { - e.stopPropagation(); - e.preventDefault(); - this.handleControlAction(action); - } - - // Section header clicks - const sectionHeader = e.target.closest('.section-header'); - if (sectionHeader && sectionHeader.dataset.section) { - const sectionId = sectionHeader.dataset.section; - if (sectionId !== 'node') { // Node section is always expanded - this.stateManager.toggleSection(sectionId); - this.uiManager.updateSectionStates(); - } - } - }); - - // Event delegation for floating controls (separate since they're not children of panel) - if (this.uiManager.elements.controls) { - this.uiManager.elements.controls.addEventListener('click', (e) => { - const action = e.target.dataset.action; - if (action) { - e.stopPropagation(); - e.preventDefault(); - this.handleControlAction(action); - } - }); - - // Prevent floating controls from propagating events - this.uiManager.elements.controls.addEventListener('mousedown', (e) => { - e.stopPropagation(); - }); - - this.uiManager.elements.controls.addEventListener('mouseup', (e) => { - e.stopPropagation(); - }); - } - - // Prevent panel interactions from propagating - this.uiManager.elements.panel.addEventListener('mousedown', (e) => { - e.stopPropagation(); - }); - - this.uiManager.elements.panel.addEventListener('mouseup', (e) => { - e.stopPropagation(); - }); - } - - // Helper function to check if user is typing in an input field - isUserTyping() { - const activeElement = document.activeElement; - if (!activeElement) return false; - - // Check if the active element is an input, textarea, or contenteditable - const tagName = activeElement.tagName.toLowerCase(); - if (tagName === 'input' || tagName === 'textarea') { - return true; - } - - // Check for contenteditable elements - if (activeElement.contentEditable === 'true') { - return true; - } - - // Check if it's inside a form or has input-like classes - if (activeElement.closest('form') || - activeElement.classList.contains('cm-editor') || // CodeMirror editor - activeElement.classList.contains('monaco-editor') || // Monaco editor - activeElement.closest('.cm-editor') || - activeElement.closest('.monaco-editor')) { - return true; - } - - return false; - } - - setupHotkeyEvents() { - document.addEventListener('keydown', (e) => { - // Don't handle hotkeys if user is typing in an input field (Smart Input Detection) - if (this.isUserTyping()) { - return; - } - - // Only handle hotkeys when magnifying glass is active - if (!magnifyGlass.state.active) return; - - const settings = this.stateManager.state.settings; - - // Toggle info panel - if (e.key.toLowerCase() === settings["🔍MagnifyGlass.ToggleHotkey"].toLowerCase() && - (!magnifyGlass.config.altRequired || e.altKey) && !e.repeat) { - e.preventDefault(); - e.stopPropagation(); - this.handleControlAction('toggle-panel'); - } - - // Toggle glass preview - if (e.key.toLowerCase() === settings["🔍MagnifyGlass.GlassPreviewToggleHotkey"].toLowerCase() && - (!magnifyGlass.config.altRequired || e.altKey) && !e.repeat) { - e.preventDefault(); - e.stopPropagation(); - this.handleControlAction('toggle-glass'); - } - - // Unlock panel from magnify glass and move to current mouse location (no Alt required due to Smart Input Detection) - if (e.key.toLowerCase() === settings["🔍MagnifyGlass.PinPanelHotkey"].toLowerCase() && !e.repeat) { - e.preventDefault(); - e.stopPropagation(); - this.handleControlAction('pin-at-mouse'); - } - }); - } - - setupDragEvents() { - let dragState = { - isDragging: false, - startX: 0, - startY: 0, - currentX: 0, - currentY: 0, - offsetX: 0, - offsetY: 0 - }; - - // Start drag - const startDrag = (e) => { - // Only when pinned and not locked - if (!this.stateManager.state.isPanelPinned || this.stateManager.state.isPanelLocked) return; - - // Block on specific elements only - if (e.target.closest('.minimize-btn') || - e.target.closest('.control-btn') || - e.target.tagName === 'BUTTON') { - return; - } - - // console.log('[DRAG] Starting drag'); - - // Stop all event propagation immediately - e.preventDefault(); - e.stopImmediatePropagation(); - - // Set drag state - dragState.isDragging = true; - dragState.startX = e.clientX; - dragState.startY = e.clientY; - - // Get current panel position - const rect = this.uiManager.elements.panel.getBoundingClientRect(); - dragState.currentX = rect.left; - dragState.currentY = rect.top; - dragState.offsetX = e.clientX - rect.left; - dragState.offsetY = e.clientY - rect.top; - - // Visual feedback - this.uiManager.elements.panel.classList.add('panel-dragging'); - this.uiManager.elements.panel.style.cursor = 'grabbing'; - document.body.style.cursor = 'grabbing'; - document.body.style.userSelect = 'none'; - - // console.log('[DRAG] Drag started at:', { x: dragState.currentX, y: dragState.currentY }); - }; - - // Handle drag movement - const doDrag = (e) => { - if (!dragState.isDragging) return; - - e.preventDefault(); - e.stopImmediatePropagation(); - - // Calculate new position based on mouse movement - const newX = e.clientX - dragState.offsetX; - const newY = e.clientY - dragState.offsetY; - - // Apply boundaries - const panelWidth = this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelWidth"]; - const panelHeight = this.uiManager.elements.panel.offsetHeight; - const margin = 10; - - const boundedX = Math.max(margin, Math.min(newX, window.innerWidth - panelWidth - margin)); - const boundedY = Math.max(margin, Math.min(newY, window.innerHeight - panelHeight - margin)); - - // Apply position immediately and forcefully - this.uiManager.elements.panel.style.position = 'fixed'; - this.uiManager.elements.panel.style.left = `${boundedX}px`; - this.uiManager.elements.panel.style.top = `${boundedY}px`; - this.uiManager.elements.panel.style.transform = 'none'; - this.uiManager.elements.panel.style.zIndex = '10010'; - - // Update current position - dragState.currentX = boundedX; - dragState.currentY = boundedY; - - // Update floating controls - if (this.uiManager.elements.controls) { - this.positionManager.positionFloatingControls(); - } - - // console.log('[DRAG] Moving to:', { x: boundedX, y: boundedY }); - }; - - // End drag - const endDrag = (e) => { - if (!dragState.isDragging) return; - - // console.log('[DRAG] Ending drag'); - - e.preventDefault(); - e.stopImmediatePropagation(); - - // Calculate if we actually moved significantly - const deltaX = Math.abs(e.clientX - dragState.startX); - const deltaY = Math.abs(e.clientY - dragState.startY); - const moved = deltaX > 5 || deltaY > 5; - - if (moved) { - // Save the final position - this.stateManager.setPinnedPosition(dragState.currentX, dragState.currentY); - // console.log('[DRAG] Position saved:', { x: dragState.currentX, y: dragState.currentY }); - } - - // Reset state - dragState.isDragging = false; - - // Remove visual feedback - this.uiManager.elements.panel.classList.remove('panel-dragging'); - this.uiManager.elements.panel.style.cursor = ''; - this.uiManager.elements.panel.style.zIndex = ''; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - }; - - // Cancel drag - const cancelDrag = () => { - if (!dragState.isDragging) return; - - // console.log('[DRAG] Cancelling drag'); - - // Restore original position from pinned state - const { x, y } = this.stateManager.state.pinnedPosition; - this.uiManager.elements.panel.style.left = `${x}px`; - this.uiManager.elements.panel.style.top = `${y}px`; - - // Reset state - dragState.isDragging = false; - - // Remove visual feedback - this.uiManager.elements.panel.classList.remove('panel-dragging'); - this.uiManager.elements.panel.style.cursor = ''; - this.uiManager.elements.panel.style.zIndex = ''; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - - // Update controls - if (this.uiManager.elements.controls) { - this.positionManager.positionFloatingControls(); - } - }; - - // Attach events with capture to prevent interference - this.uiManager.elements.panel.addEventListener('mousedown', startDrag, { capture: true, passive: false }); - document.addEventListener('mousemove', doDrag, { capture: true, passive: false }); - document.addEventListener('mouseup', endDrag, { capture: true, passive: false }); - - // Handle escape key - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - cancelDrag(); - } - }); - - // Handle mouse leaving window - document.addEventListener('mouseleave', endDrag); - - // Visual feedback - this.uiManager.elements.panel.addEventListener('mouseenter', () => { - if (this.stateManager.state.isPanelPinned && !this.stateManager.state.isPanelLocked && !dragState.isDragging) { - this.uiManager.elements.panel.style.cursor = 'grab'; - } - }); - - this.uiManager.elements.panel.addEventListener('mouseleave', () => { - if (!dragState.isDragging) { - this.uiManager.elements.panel.style.cursor = ''; - } - }); - - // console.log('[DRAG] Event listeners attached with capture mode'); - - // Store cleanup function - this.dragCleanup = () => { - this.uiManager.elements.panel.removeEventListener('mousedown', startDrag, { capture: true }); - document.removeEventListener('mousemove', doDrag, { capture: true }); - document.removeEventListener('mouseup', endDrag, { capture: true }); - document.removeEventListener('mouseleave', endDrag); - cancelDrag(); - }; - } - - setupHoverEvents() { - this.uiManager.elements.panel.addEventListener('mouseenter', () => { - this.stateManager.state.isPanelHovered = true; - this.stateManager.clearAutoExpandTimer(); - }); - - this.uiManager.elements.panel.addEventListener('mouseleave', () => { - this.stateManager.state.isPanelHovered = false; - if (!this.stateManager.state.isHoveringNode) { - this.stateManager.scheduleAutoCollapse(); - } - }); - } - - captureCurrentPanelPosition() { - // Capture current position when pinning is activated - const panelRect = this.uiManager.elements.panel.getBoundingClientRect(); - this.stateManager.setPinnedPosition(panelRect.left, panelRect.top); - // console.log(`Panel pinned at current position: (${panelRect.left}, ${panelRect.top})`); - } - - handleControlAction(action) { - switch (action) { - case 'minimize': - this.stateManager.toggleMinimized(); - this.uiManager.updateMinimizedState(); - break; - - case 'pin': - // If not currently pinned, either restore to last position or capture current - if (!this.stateManager.state.isPanelPinned) { - // If we don't have a remembered position, capture current position - if (!this.stateManager.state.lastPinnedPosition) { - this.captureCurrentPanelPosition(); - } - // Note: togglePinning will restore lastPinnedPosition if it exists - } - - this.stateManager.togglePinning(); - this.uiManager.updatePinnedState(); - this.positionManager.positionPanel(); - break; - - case 'lock': - this.stateManager.toggleLocking(); - this.uiManager.updatePinnedState(); // This updates both pinned and locked states - break; - - case 'pin-at-mouse': - // Pin at current mouse location (Alt + configured key) - const settings = this.stateManager.state.settings; - const panelWidth = settings["🔍MagnifyGlass.InfoPanelWidth"]; - const panelHeight = Math.min(settings["🔍MagnifyGlass.InfoPanelMaxHeight"], 400); - - // Position panel near mouse with some offset to avoid covering cursor area - const mouseX = magnifyGlass.lastKnownMousePosition.x; - const mouseY = magnifyGlass.lastKnownMousePosition.y; - const pinKey = settings["🔍MagnifyGlass.PinPanelHotkey"]; - - let pinX = mouseX + 50; // Offset to the right of mouse - let pinY = mouseY - 100; // Offset above mouse - - // Apply boundary constraints - pinX = Math.max(10, Math.min(pinX, window.innerWidth - panelWidth - 10)); - pinY = Math.max(10, Math.min(pinY, window.innerHeight - panelHeight - 10)); - - // Set the new pin position (this will also save as lastPinnedPosition) - this.stateManager.setPinnedPosition(pinX, pinY); - - // Enable pinning if not already pinned - if (!this.stateManager.state.isPanelPinned) { - this.stateManager.state.isPanelPinned = true; - } - - this.uiManager.updatePinnedState(); - this.positionManager.positionPanel(); - // console.log(`Panel pinned at mouse location: (${pinX}, ${pinY}) using Alt+${pinKey.toUpperCase()}`); - break; - - case 'toggle-panel': - if (this.stateManager.togglePanelVisibility()) { - this.uiManager.show(); - } else { - this.uiManager.hide(); - } - this.uiManager.updateControlStates(); - break; - - case 'toggle-glass': - const isVisible = this.stateManager.toggleGlassPreview(); - this.applyGlassVisibility(isVisible); - - // When the glass preview is hidden, automatically pin the panel at its current location. - if (!isVisible && !this.stateManager.state.isPanelPinned) { - this.captureCurrentPanelPosition(); // Capture current position before pinning - this.stateManager.togglePinning(); - this.stateManager.state.isAutoPinned = true; // Mark as auto-pinned - this.uiManager.updatePinnedState(); - } - - // When the glass preview is shown again, only auto-unlock if NOT manually locked with 📌 button - if (isVisible && this.stateManager.state.isPanelPinned && !this.stateManager.state.isPanelLocked) { - this.stateManager.togglePinning(); // Unpin the panel - this.stateManager.state.isAutoPinned = false; // Clear auto-pin flag - this.uiManager.updatePinnedState(); - } - - this.uiManager.updateControlStates(); - this.positionManager.positionPanel(); // Reposition based on new glass state - break; - } - } - - applyGlassVisibility(isVisible) { - if (magnifyGlass && magnifyGlass.ui && magnifyGlass.ui.glassDiv) { - magnifyGlass.ui.glassDiv.style.opacity = isVisible ? "1" : "0"; - } - } - } - - /** - * Information Gatherer - * Responsible for collecting information about the current state - */ - class InformationGatherer { - constructor() {} - - gatherInformation() { - const info = { - timestamp: Date.now(), - cursor: { - canvas: { x: magnifyGlass.state.x, y: magnifyGlass.state.y }, - screen: { - x: magnifyGlass.lastKnownMousePosition.x, - y: magnifyGlass.lastKnownMousePosition.y - } - }, - canvas: { - scale: magnifyGlass.state.canvasScale, - offset: { - x: magnifyGlass.state.canvasOffsetX, - y: magnifyGlass.state.canvasOffsetY - } - }, - magnifier: { - zoomFactor: magnifyGlass.config.zoomFactor, - offsetX: magnifyGlass.config.offsetX, - offsetY: magnifyGlass.config.offsetY, - sourceRegion: { - x: magnifyGlass.state.sourceX, - y: magnifyGlass.state.sourceY, - width: magnifyGlass.state.sourceWidth, - height: magnifyGlass.state.sourceHeight - } - }, - hoveredNode: null, - node: null, - widget: null, - connection: null, - media: null - }; - - if (magnifyGlass.isOverMedia && magnifyGlass.currentMediaElement) { - info.media = this.getMediaInfo(magnifyGlass.currentMediaElement); - } - - const nodeUnderCursor = this.getNodeUnderCursor(); - if (nodeUnderCursor) { - info.hoveredNode = this.getDetailedNodeInfo(nodeUnderCursor.node, nodeUnderCursor.localPos); - info.node = this.getNodeInfo(nodeUnderCursor.node); - - const widget = this.getWidgetUnderCursor(nodeUnderCursor.node, nodeUnderCursor.localPos); - if (widget) { - info.widget = this.getWidgetInfo(widget); - } - } - - return info; - } - - getNodeUnderCursor() { - if (!app.graph || !app.canvas) { - return null; - } - - try { - // Try to get the node from ComfyUI's state - if (app.canvas.node_over) { - const node = app.canvas.node_over; - if (node && node.pos && node.size) { - const canvasRect = app.canvas.canvas.getBoundingClientRect(); - const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; - const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; - - let graphPos; - if (app.canvas.convertOffsetToCanvasPos) { - graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); - } else { - const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; - graphPos = [ - (mouseX / ds.scale) - (ds.offset[0] / ds.scale), - (mouseY / ds.scale) - (ds.offset[1] / ds.scale) - ]; - } - - return { - node: node, - localPos: { - x: graphPos[0] - node.pos[0], - y: graphPos[1] - node.pos[1] - } - }; - } - } - - // Fallback to manual detection - const canvasRect = app.canvas.canvas.getBoundingClientRect(); - const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; - const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; - - let graphPos; - if (app.canvas.convertOffsetToCanvasPos) { - graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); - } else { - const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; - graphPos = [ - (mouseX / ds.scale) - (ds.offset[0] / ds.scale), - (mouseY / ds.scale) - (ds.offset[1] / ds.scale) - ]; - } - - for (let i = app.graph._nodes.length - 1; i >= 0; i--) { - const node = app.graph._nodes[i]; - if (!node.pos || !node.size || node.flags?.collapsed) continue; - - if (graphPos[0] >= node.pos[0] && - graphPos[0] <= node.pos[0] + node.size[0] && - graphPos[1] >= node.pos[1] && - graphPos[1] <= node.pos[1] + node.size[1]) { - - return { - node: node, - localPos: { - x: graphPos[0] - node.pos[0], - y: graphPos[1] - node.pos[1] - } - }; - } - } - } catch (err) { - console.warn("Error in node detection:", err); - } - - return null; - } - - getDetailedNodeInfo(node, localPos) { - return { - id: node.id, - title: node.title || "Untitled Node", - type: node.type, - mode: this.getNodeModeText(node.mode), - position: { - x: Math.round(node.pos[0]), - y: Math.round(node.pos[1]), - formatted: `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` - }, - size: { - width: Math.round(node.size[0]), - height: Math.round(node.size[1]), - formatted: `${Math.round(node.size[0])}×${Math.round(node.size[1])}` - }, - localPosition: { - x: Math.round(localPos.x), - y: Math.round(localPos.y), - formatted: `(${Math.round(localPos.x)}, ${Math.round(localPos.y)})`, - percentage: { - x: ((localPos.x / node.size[0]) * 100).toFixed(1), - y: ((localPos.y / node.size[1]) * 100).toFixed(1) - } - }, - counts: { - widgets: node.widgets ? node.widgets.length : 0, - inputs: node.inputs ? node.inputs.length : 0, - outputs: node.outputs ? node.outputs.length : 0, - properties: node.properties ? Object.keys(node.properties).length : 0 - }, - widgets: node.widgets || [], - inputs: node.inputs || [], - outputs: node.outputs || [], - properties: node.properties || {}, - hoverRegion: this.detectNodeRegion(localPos, node) - }; - } - - getNodeInfo(node) { - return { - id: node.id, - title: node.title || "Untitled", - type: node.type, - mode: node.mode, - size: node.size ? `${Math.round(node.size[0])}×${Math.round(node.size[1])}` : "Unknown", - position: node.pos ? `(${Math.round(node.pos[0])}, ${Math.round(node.pos[1])})` : "Unknown", - widgets: node.widgets || [], - inputs: node.inputs || [], - outputs: node.outputs || [], - properties: node.properties || {} - }; - } - - getWidgetUnderCursor(node, localPos) { - if (!node.widgets || !node.widgets.length) return null; - - const titleHeight = 30; - let currentY = titleHeight; - - for (const widget of node.widgets) { - const widgetHeight = 25; - - if (localPos.y >= currentY && localPos.y <= currentY + widgetHeight) { - return widget; - } - - currentY += widgetHeight + 5; - } - - return null; - } - - getWidgetInfo(widget) { - return { - name: widget.name, - type: widget.type, - value: this.formatValue(widget.value), - options: widget.options || null, - min: widget.min, - max: widget.max, - step: widget.step - }; - } - - getMediaInfo(mediaElement) { - const info = { - tagName: mediaElement.tagName, - src: mediaElement.src ? mediaElement.src.substring(mediaElement.src.lastIndexOf('/') + 1) : "No source" - }; - - if (mediaElement.tagName === 'IMG') { - info.naturalSize = `${mediaElement.naturalWidth}×${mediaElement.naturalHeight}`; - info.displaySize = `${Math.round(mediaElement.width)}×${Math.round(mediaElement.height)}`; - info.complete = mediaElement.complete; - } else if (mediaElement.tagName === 'VIDEO') { - info.videoSize = `${mediaElement.videoWidth}×${mediaElement.videoHeight}`; - info.duration = mediaElement.duration ? `${mediaElement.duration.toFixed(2)}s` : "Unknown"; - info.currentTime = `${mediaElement.currentTime.toFixed(2)}s`; - info.paused = mediaElement.paused; - info.readyState = mediaElement.readyState; - } - - return info; - } - - getNodeModeText(mode) { - const modes = { - 0: "Always", - 1: "On Event", - 2: "Never", - 3: "On Trigger", - 4: "On Request" - }; - return modes[mode] || `Mode ${mode}`; - } - - detectNodeRegion(localPos, node) { - const titleHeight = 30; - const regions = []; - - if (localPos.y <= titleHeight) { - regions.push("Title Bar"); - } - - if (localPos.x <= 10) { - regions.push("Left Edge"); - } else if (localPos.x >= node.size[0] - 10) { - regions.push("Right Edge"); - } - - if (localPos.y <= 10) { - regions.push("Top Edge"); - } else if (localPos.y >= node.size[1] - 10) { - regions.push("Bottom Edge"); - } - - if (regions.length === 0) { - if (localPos.y > titleHeight) { - regions.push("Content Area"); - } - } - - if (localPos.x <= 20 && localPos.y > titleHeight) { - regions.push("Input Area"); - } else if (localPos.x >= node.size[0] - 20 && localPos.y > titleHeight) { - regions.push("Output Area"); - } - - return regions.length > 0 ? regions.join(", ") : "Unknown"; - } - - formatValue(value) { - if (value === null) return "null"; - if (value === undefined) return "undefined"; - if (typeof value === "string") { - return value; // Show full text without truncation - } - if (typeof value === "number") { - return Number.isInteger(value) ? value.toString() : value.toFixed(3); - } - if (typeof value === "boolean") return value.toString(); - if (Array.isArray(value)) { - return `Array(${value.length})`; - } - if (typeof value === "object") { - return "Object"; - } - return String(value); - } - } - - /** - * Main Professional Info Panel Manager - * Orchestrates all the other components - */ - class ProfessionalInfoPanelManager { - constructor(magnifyGlass) { - this.magnifyGlass = magnifyGlass; - - // Initialize all managers - this.stateManager = new StateManager(); - this.uiManager = new UIManager(this.stateManager); - this.positionManager = new PositionManager(this.stateManager, this.uiManager); - this.eventManager = new EventManager(this.stateManager, this.uiManager, this.positionManager); - this.informationGatherer = new InformationGatherer(); - - this.hookIntoMagnifyGlass(); - - console.log("ComfyUI Magnify Info Panel Pro V2: Initialized successfully"); - } - - hookIntoMagnifyGlass() { - // Hook into magnify glass update cycle - const originalUpdateMagnifiedView = this.magnifyGlass.updateMagnifiedView.bind(this.magnifyGlass); - - this.magnifyGlass.updateMagnifiedView = (() => { - originalUpdateMagnifiedView(); - - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { - this.scheduleInfoUpdate(); - } - }).bind(this); - - // Hook into show/hide - const originalShow = this.magnifyGlass.ui.show.bind(this.magnifyGlass.ui); - const originalHide = this.magnifyGlass.ui.hide.bind(this.magnifyGlass.ui); - - this.magnifyGlass.ui.show = (() => { - originalShow(); - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"]) { - this.uiManager.show(); - // Position controls after showing - setTimeout(() => this.positionManager.positionPanel(), 10); - } - }).bind(this); - - this.magnifyGlass.ui.hide = (() => { - originalHide(); - this.uiManager.hide(); - }).bind(this); - } - - scheduleInfoUpdate() { - if (this.stateManager.state.updateScheduled) return; - - this.stateManager.state.updateScheduled = true; - requestAnimationFrame(() => { - this.updateInfo(); - this.stateManager.state.updateScheduled = false; - }); - } - - updateInfo() { - if (!this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] || !this.magnifyGlass.state.active) return; - - const info = this.informationGatherer.gatherInformation(); - this.stateManager.setCurrentInfo(info); - this.uiManager.displayInfo(info); - this.positionManager.positionPanel(); - - // Handle node hover state - if (info.hoveredNode) { - this.stateManager.state.isHoveringNode = true; - this.stateManager.expandNodeSections(); - - if (this.stateManager.state.lastNodeId !== info.hoveredNode.id) { - this.stateManager.state.lastNodeId = info.hoveredNode.id; - } - } else { - this.stateManager.state.isHoveringNode = false; - - if (this.stateManager.state.lastNodeId !== null) { - this.stateManager.state.lastNodeId = null; - if (!this.stateManager.state.isPanelHovered) { - this.stateManager.scheduleAutoCollapse(); - } - } - } - } - - updateSettings() { - const changes = this.stateManager.updateSettings(); - - // React to setting changes (theme is now auto-detected) - // Theme changes are handled automatically by the observer - - if (changes["🔍MagnifyGlass.ControlsPosition"]) { - // Update controls layout and position when position setting changes - this.positionManager.positionPanel(); - } - - // Apply new styles - this.uiManager.applyStyles(); - - if (this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] && this.magnifyGlass.state.active) { - this.uiManager.show(); - } else { - this.uiManager.hide(); - } - } - - cleanup() { - this.stateManager.cleanup(); - this.uiManager.cleanup(); - } - } - - // Create the main manager instance - const infoPanelManager = new ProfessionalInfoPanelManager(magnifyGlass); - - // Store reference globally for cleanup and theme updates - if (!window.comfyUIMagnifyGlassExtensions) { - window.comfyUIMagnifyGlassExtensions = []; - } - window.comfyUIMagnifyGlassExtensions.push(infoPanelManager); - - // Store main manager reference for theme updates - window.infoPanelManager = infoPanelManager; - - // Register settings with ComfyUI - Object.keys(DEFAULT_SETTINGS).forEach(settingKey => { - const settingConfig = getSettingConfig(settingKey, DEFAULT_SETTINGS[settingKey]); - if (settingConfig) { - app.ui.settings.addSetting({ - ...settingConfig, - onChange: (value) => { - infoPanelManager.updateSettings(); - } - }); - } - }); - - function getSettingConfig(key, defaultValue) { - const configs = { - "🔍MagnifyGlass.InfoPanelEnabled": { - id: key, - name: "📋 Magnify Glass: Info Panel", - type: "combo", - options: [ - { value: true, text: "Enabled" }, - { value: false, text: "Disabled" } - ], - defaultValue, - tooltip: "Enable or disable the professional information panel." - }, - "🔍MagnifyGlass.InfoPanelPosition": { - id: key, - name: "📋 Magnify Glass: Info Panel Position", - type: "combo", - options: [ - { value: "Right", text: "Right" }, - { value: "Left", text: "Left" }, - { value: "Top", text: "Top" }, - { value: "Bottom", text: "Bottom" } - ], - defaultValue, - tooltip: "Position of the info panel relative to the magnifying glass." - }, - "🔍MagnifyGlass.InfoPanelWidth": { - id: key, - name: "📋 Magnify Glass: Info Panel Width", - type: "slider", - defaultValue, - min: 250, - max: 450, - step: 10, - tooltip: "Width of the information panel in pixels." - }, - "🔍MagnifyGlass.InfoPanelOpacity": { - id: key, - name: "📋 Magnify Glass: Info Panel Opacity (%)", - type: "slider", - defaultValue, - min: 10, - max: 100, - step: 5, - tooltip: "Opacity of the information panel background as a percentage (10 = very transparent, 100 = fully opaque).", - onChange: (value) => { - // Convert percentage to decimal and apply opacity immediately - const opacity = value / 100; - if (window.infoPanelManager && window.infoPanelManager.uiManager && window.infoPanelManager.uiManager.elements.panel) { - window.infoPanelManager.uiManager.elements.panel.style.opacity = opacity; - } - } - }, - "🔍MagnifyGlass.InfoPanelMaxHeight": { - id: key, - name: "📋 Magnify Glass: Info Panel Max Height", - type: "slider", - defaultValue, - min: 300, - max: 700, - step: 25, - tooltip: "Maximum height of the information panel in pixels." - }, - - "🔍MagnifyGlass.InfoPanelAnimations": { - id: key, - name: "🎬 Magnify Glass: Info Panel Animations", - type: "combo", - options: [ - { value: true, text: "Enabled" }, - { value: false, text: "Disabled" } - ], - defaultValue, - tooltip: "Enable or disable animations for the info panel." - }, - "🔍MagnifyGlass.ShowInspectorTab": { - id: key, - name: "📋 Magnify Glass: Show Inspector Tab", - type: "combo", - options: [ - { value: true, text: "Enabled" }, - { value: false, text: "Disabled" } - ], - defaultValue, - tooltip: "Show or hide the Inspector tab with cursor and canvas information." - }, - "🔍MagnifyGlass.ToggleHotkey": { - id: key, - name: "🔑 Magnify Glass: Info Panel Toggle Hotkey", - type: "combo", - options: ["i", "p", "o", "l", "k"], - defaultValue, - tooltip: "Key to toggle the info panel visibility while the magnifier is active." - }, - "🔍MagnifyGlass.GlassPreviewToggleHotkey": { - id: key, - name: "🔑 Magnify Glass: Preview Toggle Hotkey", - type: "combo", - options: ["g", "v", "b", "m", "n"], - defaultValue, - tooltip: "Key to toggle the magnifying glass preview visibility." - }, - "🔍MagnifyGlass.PinPanelHotkey": { - id: key, - name: "🔓 Magnify Glass: Unlock Panel to Mouse Location", - type: "combo", - options: ["u", "p", "f", "x", "z"], - defaultValue, - tooltip: "Key to unlock the info panel from the magnify glass and move it to your current mouse position (e.g., U). Panel remains movable - use the pin button to lock it in place. Alt not required due to Smart Input Detection." - }, - "🔍MagnifyGlass.ShowHoveringControls": { - id: key, - name: "🎮 Magnify Glass: Show Hover Controls", - type: "combo", - options: [ - { value: true, text: "Enabled" }, - { value: false, text: "Disabled" } - ], - defaultValue, - tooltip: "Show or hide hovering UI controls above the info panel." - }, - "🔍MagnifyGlass.ControlsPosition": { - id: key, - name: "📍 Magnify Glass: Controls Position", - type: "combo", - options: [ - { value: "top-left", text: "Top-Left" }, - { value: "top-right", text: "Top-Right" }, - { value: "bottom-left", text: "Bottom-Left" }, - { value: "bottom-right", text: "Bottom-Right" }, - { value: "top", text: "Top (Centered)" }, - { value: "bottom", text: "Bottom (Centered)" }, - { value: "left", text: "Left (Centered)" }, - { value: "right", text: "Right (Centered)" } - ], - defaultValue, - tooltip: "Position of the floating control buttons relative to the info panel." - }, - "🔍MagnifyGlass.InfoPanelTextColor": { - id: key, - name: "🎨 Magnify Glass: Info Panel Text Color", - type: "color", - defaultValue, - tooltip: "Color of the text in the info panel. This affects all text content including titles, values, and labels.", - onChange: (value) => { - // Ensure color value includes # symbol for valid CSS and persistence - const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; - - // Only save back if we actually normalized the color (to prevent recursion) - if (value !== normalizedColor) { - try { - app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelTextColor", normalizedColor); - } catch (e) { - console.warn('Failed to save normalized text color:', e); - } - } - - // Apply the color immediately - if (window.infoPanelManager && window.infoPanelManager.uiManager) { - window.infoPanelManager.uiManager.applyStyles(); - } - } - }, - "🔍MagnifyGlass.InfoPanelAccentColor": { - id: key, - name: "🎨 Magnify Glass: Info Panel Accent Color", - type: "color", - defaultValue, - tooltip: "Color used for highlighted elements like values, section titles, and active states in the info panel.", - onChange: (value) => { - // Ensure color value includes # symbol for valid CSS and persistence - const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; - - // Only save back if we actually normalized the color (to prevent recursion) - if (value !== normalizedColor) { - try { - app.ui.settings.setSettingValue("🔍MagnifyGlass.InfoPanelAccentColor", normalizedColor); - } catch (e) { - console.warn('Failed to save normalized accent color:', e); - } - } - - // Apply the color immediately - if (window.infoPanelManager && window.infoPanelManager.uiManager) { - window.infoPanelManager.uiManager.applyStyles(); - } - } - } - }; - - return configs[key] || null; - } - - console.log("ComfyUI Magnify Info Panel Pro V2: All settings registered and initialized"); - } - } -}); \ No newline at end of file diff --git a/web/magnify_info_panel.js.map b/web/magnify_info_panel.js.map new file mode 100644 index 0000000..dd6434a --- /dev/null +++ b/web/magnify_info_panel.js.map @@ -0,0 +1 @@ +{"version":3,"file":"magnify_info_panel.js","sources":["../src/magnify_info_panel.ts"],"sourcesContent":["/**\n * ComfyUI Magnify Info Panel - Main Entry Point\n * \n * Initializes the Info Panel extension.\n */\n\nimport { InfoPanel } from './info-panel';\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\n\n// We rely on the global 'app' object provided by ComfyUI\n// No need to import it as it's global, but TypeScript needs to know about it.\n// We handled this in types/comfyui.d.ts\n\napp.registerExtension({\n name: \"comfyui.magnify.info.panel\",\n init() {\n // Wait for magnify glass to be available\n const checkDependencies = () => {\n if (window.comfyUIMagnifyGlass) {\n initializeInfoPanel();\n } else {\n setTimeout(checkDependencies, 100);\n }\n };\n\n checkDependencies();\n }\n});\n\nfunction initializeInfoPanel() {\n const magnifyGlass = window.comfyUIMagnifyGlass;\n if (!magnifyGlass) {\n console.error(\"ComfyUI Magnify Info Panel: Failed to find MagnifyGlass instance.\");\n return;\n }\n\n try {\n // Initialize the info panel\n const infoPanel = new InfoPanel(magnifyGlass);\n\n // Expose orchestrator globally if needed\n window.infoPanelManager = infoPanel as any;\n\n // Load CSS\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.type = \"text/css\";\n link.href = \"extensions/comfyui-magnifyglass/style.css\";\n document.head.appendChild(link);\n console.log(\"ComfyUI Magnify Info Panel: CSS loaded\");\n\n console.log(\"ComfyUI Magnify Info Panel: Extension initialized\");\n } catch (e) {\n console.error(\"ComfyUI Magnify Info Panel: Error during initialization:\", e);\n }\n}\n\n"],"names":[],"mappings":";;AAcA,IAAI,kBAAkB;AAAA,EAClB,MAAM;AAAA,EACN,OAAO;AAEH,UAAM,oBAAoB,MAAM;AAC5B,UAAI,OAAO,qBAAqB;AAC5B,4BAAA;AAAA,MACJ,OAAO;AACH,mBAAW,mBAAmB,GAAG;AAAA,MACrC;AAAA,IACJ;AAEA,sBAAA;AAAA,EACJ;AACJ,CAAC;AAED,SAAS,sBAAsB;AAC3B,QAAM,eAAe,OAAO;AAC5B,MAAI,CAAC,cAAc;AACf,YAAQ,MAAM,mEAAmE;AACjF;AAAA,EACJ;AAEA,MAAI;AAEA,UAAM,YAAY,IAAI,UAAU,YAAY;AAG5C,WAAO,mBAAmB;AAG1B,UAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,SAAK,MAAM;AACX,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,aAAS,KAAK,YAAY,IAAI;AAC9B,YAAQ,IAAI,wCAAwC;AAEpD,YAAQ,IAAI,mDAAmD;AAAA,EACnE,SAAS,GAAG;AACR,YAAQ,MAAM,4DAA4D,CAAC;AAAA,EAC/E;AACJ;"} \ No newline at end of file diff --git a/web/shared/constants.js b/web/shared/constants.js index f8fc0fa..5e1ee6c 100644 --- a/web/shared/constants.js +++ b/web/shared/constants.js @@ -1,81 +1,61 @@ -/** - * ComfyUI MagnifyGlass - Shared Constants - * - * Configuration constants shared between magnify glass and info panel modules. - */ - -// Glass positioning options -export const GLASS_POSITIONS = [ - "Top", - "Bottom", - "Left", - "Right", - "Top-Left", - "Top-Right", - "Bottom-Left", - "Bottom-Right" +const GLASS_POSITIONS = [ + "Top", + "Bottom", + "Left", + "Right", + "Top-Left", + "Top-Right", + "Bottom-Left", + "Bottom-Right" ]; - -// Glass shape options -export const GLASS_SHAPES = [ - "Circle", - "Square", - "Rounded Square" +const GLASS_SHAPES = [ + "Circle", + "Square", + "Rounded Square" ]; - -// Texture filtering modes -export const TEXTURE_FILTERS = [ - "Linear", - "Nearest" +const PANEL_POSITIONS = [ + "Top", + "Bottom", + "Left", + "Right" ]; - -// Info panel positions relative to glass -export const PANEL_POSITIONS = [ - "Top", - "Bottom", - "Left", - "Right" -]; - -// Available activation keys -export const ACTIVATION_KEYS = ["x", "z", "m", "q", "v", "c"]; - -// Available reset keys -export const RESET_KEYS = ["r", "o", "p", "k", "l"]; - -// Available toggle follow keys -export const TOGGLE_FOLLOW_KEYS = ["f", "g", "h", "j", "k"]; - -// UI Constants -export const DEFAULT_PADDING = 20; -export const DEFAULT_GLASS_SIZE = 300; -export const DEFAULT_ZOOM_FACTOR = 300; // 3x as percentage -export const DEFAULT_BORDER_WIDTH = 1; -export const DEFAULT_BORDER_COLOR = "#6b7280"; -export const DEFAULT_OFFSET_STEP = 5; - -// Panel Constants -export const DEFAULT_PANEL_WIDTH = 320; -export const DEFAULT_PANEL_MAX_HEIGHT = 1000; -export const DEFAULT_PANEL_OPACITY = 100; - -// Z-Index hierarchy -export const Z_INDEX = { - GLASS: 98999, - DEBUG: 99000, - PANEL: 99999, - CONTROLS: 99998 +const ACTIVATION_KEYS = ["x", "z", "m", "q", "v", "c"]; +const RESET_KEYS = ["r", "o", "p", "k", "l"]; +const TOGGLE_FOLLOW_KEYS = ["f", "g", "h", "j", "k"]; +const DEFAULT_PADDING = 20; +const DEFAULT_GLASS_SIZE = 300; +const DEFAULT_ZOOM_FACTOR = 300; +const DEFAULT_BORDER_WIDTH = 1; +const DEFAULT_BORDER_COLOR = "#6b7280"; +const DEFAULT_OFFSET_STEP = 5; +const DEFAULT_PANEL_WIDTH = 320; +const DEFAULT_PANEL_MAX_HEIGHT = 1e3; +const DEFAULT_PANEL_OPACITY = 100; +const Z_INDEX = { + GLASS: 98999, + DEBUG: 99e3 }; - -// Animation durations (ms) -export const ANIMATION = { - PANEL_TRANSITION: 300, - AUTO_COLLAPSE_DELAY: 1500, - THEME_DETECTION_INTERVAL: 2000 +const STORAGE_KEYS = { + OFFSET_X: "comfyui_magnify_offset_x", + OFFSET_Y: "comfyui_magnify_offset_y" }; - -// LocalStorage keys -export const STORAGE_KEYS = { - OFFSET_X: 'comfyui_magnify_offset_x', - OFFSET_Y: 'comfyui_magnify_offset_y' +export { + ACTIVATION_KEYS, + DEFAULT_BORDER_COLOR, + DEFAULT_BORDER_WIDTH, + DEFAULT_GLASS_SIZE, + DEFAULT_OFFSET_STEP, + DEFAULT_PADDING, + DEFAULT_PANEL_MAX_HEIGHT, + DEFAULT_PANEL_OPACITY, + DEFAULT_PANEL_WIDTH, + DEFAULT_ZOOM_FACTOR, + GLASS_POSITIONS, + GLASS_SHAPES, + PANEL_POSITIONS, + RESET_KEYS, + STORAGE_KEYS, + TOGGLE_FOLLOW_KEYS, + Z_INDEX }; +//# sourceMappingURL=constants.js.map diff --git a/web/shared/constants.js.map b/web/shared/constants.js.map new file mode 100644 index 0000000..24b864c --- /dev/null +++ b/web/shared/constants.js.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.js","sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Shared Constants\n * \n * Configuration constants shared between magnify glass and info panel modules.\n */\n\n// Glass positioning options\nexport const GLASS_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\",\n \"Top-Left\",\n \"Top-Right\",\n \"Bottom-Left\",\n \"Bottom-Right\"\n] as const;\n\nexport type GlassPosition = typeof GLASS_POSITIONS[number];\n\n// Glass shape options\nexport const GLASS_SHAPES = [\n \"Circle\",\n \"Square\",\n \"Rounded Square\"\n] as const;\n\nexport type GlassShape = typeof GLASS_SHAPES[number];\n\n// Texture filtering modes\nexport const TEXTURE_FILTERS = [\n \"Linear\",\n \"Nearest\"\n] as const;\n\nexport type TextureFilter = typeof TEXTURE_FILTERS[number];\n\n// Info panel positions relative to glass\nexport const PANEL_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\"\n] as const;\n\nexport type PanelPosition = typeof PANEL_POSITIONS[number];\n\n// Available keys for settings\nexport const ACTIVATION_KEYS = [\"x\", \"z\", \"m\", \"q\", \"v\", \"c\"] as const;\nexport const RESET_KEYS = [\"r\", \"o\", \"p\", \"k\", \"l\"] as const;\nexport const TOGGLE_FOLLOW_KEYS = [\"f\", \"g\", \"h\", \"j\", \"k\"] as const;\n\n// UI Constants\nexport const DEFAULT_PADDING = 20;\nexport const DEFAULT_GLASS_SIZE = 300;\nexport const DEFAULT_ZOOM_FACTOR = 300; // 3x as percentage\nexport const DEFAULT_BORDER_WIDTH = 1;\nexport const DEFAULT_BORDER_COLOR = \"#6b7280\";\nexport const DEFAULT_OFFSET_STEP = 5;\n\n// Panel Constants\nexport const DEFAULT_PANEL_WIDTH = 320;\nexport const DEFAULT_PANEL_MAX_HEIGHT = 1000;\nexport const DEFAULT_PANEL_OPACITY = 100;\n\n// Z-Index hierarchy\nexport const Z_INDEX = {\n GLASS: 98999,\n DEBUG: 99000,\n PANEL: 99999,\n CONTROLS: 99998\n} as const;\n\n// Animation durations (ms)\nexport const ANIMATION = {\n PANEL_TRANSITION: 300,\n AUTO_COLLAPSE_DELAY: 1500,\n THEME_DETECTION_INTERVAL: 2000\n} as const;\n\n// LocalStorage keys\nexport const STORAGE_KEYS = {\n OFFSET_X: 'comfyui_magnify_offset_x',\n OFFSET_Y: 'comfyui_magnify_offset_y'\n} as const;\n"],"names":[],"mappings":"AAOO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,eAAe;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AACJ;AAaO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,kBAAkB,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AACrD,MAAM,aAAa,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAC3C,MAAM,qBAAqB,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAGnD,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAG5B,MAAM,sBAAsB;AAC5B,MAAM,2BAA2B;AACjC,MAAM,wBAAwB;AAG9B,MAAM,UAAU;AAAA,EACnB,OAAO;AAAA,EACP,OAAO;AAGX;AAUO,MAAM,eAAe;AAAA,EACxB,UAAU;AAAA,EACV,UAAU;AACd;"} \ No newline at end of file diff --git a/web/shared/settings.js b/web/shared/settings.js index 1e9b9de..f6ad602 100644 --- a/web/shared/settings.js +++ b/web/shared/settings.js @@ -1,658 +1,459 @@ -/** - * ComfyUI MagnifyGlass - Settings Configuration - * - * Centralized settings definitions and registration functions. - */ - -import { app } from "../../../../scripts/app.js"; -import { - DEFAULT_GLASS_SIZE, - DEFAULT_ZOOM_FACTOR, - DEFAULT_BORDER_WIDTH, - DEFAULT_BORDER_COLOR, - DEFAULT_OFFSET_STEP, - DEFAULT_PANEL_WIDTH, - DEFAULT_PANEL_MAX_HEIGHT, - DEFAULT_PANEL_OPACITY, - GLASS_POSITIONS, - GLASS_SHAPES, - ACTIVATION_KEYS, - RESET_KEYS, - TOGGLE_FOLLOW_KEYS, - PANEL_POSITIONS -} from './constants.js'; - -/** - * Default settings for the magnify glass component. - */ -export const DEFAULT_GLASS_SETTINGS = { - "🔍MagnifyGlass.ZoomFactor": DEFAULT_ZOOM_FACTOR, - "🔍MagnifyGlass.GlassSize": DEFAULT_GLASS_SIZE, - "🔍MagnifyGlass.BorderColor": DEFAULT_BORDER_COLOR, - "🔍MagnifyGlass.BorderWidth": DEFAULT_BORDER_WIDTH, - "🔍MagnifyGlass.ActivationKey": "x", - "🔍MagnifyGlass.AltRequired": false, - "🔍MagnifyGlass.FollowCursor": false, - "🔍MagnifyGlass.DebugMode": false, - "🔍MagnifyGlass.OffsetStep": DEFAULT_OFFSET_STEP, - "🔍MagnifyGlass.GlassPosition": "Top-Right", - "🔍MagnifyGlass.ResetKey": "o", - "🔍MagnifyGlass.GlassShape": "Rounded Square", - "🔍MagnifyGlass.BorderEnabled": true, - "🔍MagnifyGlass.TextureFiltering": "Linear", - "🔍MagnifyGlass.AlwaysActiveMode": true, - "🔍MagnifyGlass.ToggleFollowCursorKey": "h", +import { app } from "/scripts/app.js"; +import { DEFAULT_ZOOM_FACTOR, DEFAULT_GLASS_SIZE, DEFAULT_BORDER_WIDTH, DEFAULT_BORDER_COLOR, ACTIVATION_KEYS, DEFAULT_OFFSET_STEP, RESET_KEYS, GLASS_POSITIONS, GLASS_SHAPES, TOGGLE_FOLLOW_KEYS, DEFAULT_PANEL_MAX_HEIGHT, DEFAULT_PANEL_OPACITY, DEFAULT_PANEL_WIDTH, PANEL_POSITIONS } from "./constants.js"; +const DEFAULT_GLASS_SETTINGS = { + "🔍MagnifyGlass.ZoomFactor": DEFAULT_ZOOM_FACTOR, + "🔍MagnifyGlass.GlassSize": DEFAULT_GLASS_SIZE, + "🔍MagnifyGlass.BorderColor": DEFAULT_BORDER_COLOR, + "🔍MagnifyGlass.BorderWidth": DEFAULT_BORDER_WIDTH, + "🔍MagnifyGlass.ActivationKey": "x", + "🔍MagnifyGlass.AltRequired": false, + "🔍MagnifyGlass.FollowCursor": false, + "🔍MagnifyGlass.DebugMode": false, + "🔍MagnifyGlass.OffsetStep": DEFAULT_OFFSET_STEP, + "🔍MagnifyGlass.GlassPosition": "Top-Right", + "🔍MagnifyGlass.ResetKey": "o", + "🔍MagnifyGlass.GlassShape": "Rounded Square", + "🔍MagnifyGlass.BorderEnabled": true, + "🔍MagnifyGlass.TextureFiltering": "Linear", + "🔍MagnifyGlass.AlwaysActiveMode": true, + "🔍MagnifyGlass.ToggleFollowCursorKey": "h" }; - -/** - * Default settings for the info panel component. - */ -export const DEFAULT_PANEL_SETTINGS = { - "🔍MagnifyGlass.InfoPanelEnabled": true, - "🔍MagnifyGlass.InfoPanelPosition": "Bottom", - "🔍MagnifyGlass.InfoPanelWidth": DEFAULT_PANEL_WIDTH, - "🔍MagnifyGlass.InfoPanelOpacity": DEFAULT_PANEL_OPACITY, - "🔍MagnifyGlass.InfoPanelMaxHeight": DEFAULT_PANEL_MAX_HEIGHT, - "🔍MagnifyGlass.InfoPanelAnimations": false, - "🔍MagnifyGlass.ShowInspectorTab": false, - "🔍MagnifyGlass.ToggleHotkey": "i", - "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g", - "🔍MagnifyGlass.PinPanelHotkey": "u", - "🔍MagnifyGlass.ShowHoveringControls": true, - "🔍MagnifyGlass.ControlsPosition": "bottom", - "🔍MagnifyGlass.InfoPanelTextColor": "#6b7280", - "🔍MagnifyGlass.InfoPanelAccentColor": "#3b82f6", +const DEFAULT_PANEL_SETTINGS = { + "🔍MagnifyGlass.InfoPanelEnabled": true, + "🔍MagnifyGlass.InfoPanelPosition": "Bottom", + "🔍MagnifyGlass.InfoPanelWidth": DEFAULT_PANEL_WIDTH, + "🔍MagnifyGlass.InfoPanelOpacity": DEFAULT_PANEL_OPACITY, + "🔍MagnifyGlass.InfoPanelMaxHeight": DEFAULT_PANEL_MAX_HEIGHT, + "🔍MagnifyGlass.InfoPanelAnimations": false, + "🔍MagnifyGlass.ShowInspectorTab": false, + "🔍MagnifyGlass.ToggleHotkey": "i", + "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g", + "🔍MagnifyGlass.PinPanelHotkey": "u", + "🔍MagnifyGlass.ShowHoveringControls": true, + "🔍MagnifyGlass.ControlsPosition": "bottom", + "🔍MagnifyGlass.InfoPanelTextColor": "#6b7280", + "🔍MagnifyGlass.InfoPanelAccentColor": "#3b82f6" }; - -/** - * Register all magnify glass settings with ComfyUI. - * @param {object} magnifyGlass - The MagnifyGlass instance - */ -export function registerGlassSettings(magnifyGlass) { - const settings = DEFAULT_GLASS_SETTINGS; - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.ZoomFactor", - name: "🔍 Magnify Glass: Zoom Factor (%)", - type: "slider", - defaultValue: settings["🔍MagnifyGlass.ZoomFactor"], - min: 100, - max: 1000, - step: 25, - tooltip: "Magnification level as a percentage (e.g., 300 = 3x zoom, 150 = 1.5x zoom).", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.zoomFactor = value / 100; - if (magnifyGlass.state.active) { - magnifyGlass.updateMagnifiedView(); - } - } +function registerGlassSettings(magnifyGlass) { + const settings = DEFAULT_GLASS_SETTINGS; + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ZoomFactor", + name: "🔍 Magnify Glass: Zoom Factor (%)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.ZoomFactor"], + min: 100, + max: 1e3, + step: 25, + tooltip: "Magnification level as a percentage (e.g., 300 = 3x zoom, 150 = 1.5x zoom).", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.zoomFactor = typeof value === "number" ? value / 100 : parseFloat(value) / 100; + if (magnifyGlass.state.active) { + magnifyGlass.updateMagnifiedView(); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.GlassSize", - name: "🔍 Magnify Glass: Size (px)", - type: "slider", - defaultValue: settings["🔍MagnifyGlass.GlassSize"], - min: 50, - max: 100, - step: 10, - tooltip: "Diameter of the magnifying glass circle in pixels.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.glassSize = parseInt(value, 10); - magnifyGlass.applyUiChanges(); - if (magnifyGlass.state.active) { - magnifyGlass.updateMagnifiedView(); - } - } + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.GlassSize", + name: "🔍 Magnify Glass: Size (px)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.GlassSize"], + min: 50, + max: 100, + step: 10, + tooltip: "Diameter of the magnifying glass circle in pixels.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.glassSize = parseInt(String(value), 10); + magnifyGlass.applyUiChanges(); + if (magnifyGlass.state.active) { + magnifyGlass.updateMagnifiedView(); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.BorderWidth", - name: "🔍 Magnify Glass: Border Width (px)", - type: "slider", - defaultValue: settings["🔍MagnifyGlass.BorderWidth"], - min: 0, - max: 10, - step: 0.1, - tooltip: "Width of the border around the magnifying glass.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.borderWidth = parseFloat(value); - magnifyGlass.applyUiChanges(); - } + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.BorderWidth", + name: "🔍 Magnify Glass: Border Width (px)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.BorderWidth"], + min: 0, + max: 10, + step: 0.1, + tooltip: "Width of the border around the magnifying glass.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.borderWidth = parseFloat(String(value)); + magnifyGlass.applyUiChanges(); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.BorderColor", + name: "🎨 Magnify Glass: Border Color", + type: "color", + defaultValue: settings["🔍MagnifyGlass.BorderColor"], + tooltip: "Color of the border around the magnifying glass.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + const strValue = String(value); + const normalizedColor = strValue && !strValue.startsWith("#") ? `#${strValue}` : strValue; + magnifyGlass.config.borderColor = normalizedColor; + if (strValue !== normalizedColor) { + try { + app.ui.settings.setSettingValue("🔍MagnifyGlass.BorderColor", normalizedColor); + } catch (e) { + console.warn("Failed to save normalized border color:", e); + } } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.BorderColor", - name: "🎨 Magnify Glass: Border Color", - type: "color", - defaultValue: settings["🔍MagnifyGlass.BorderColor"], - tooltip: "Color of the border around the magnifying glass.", - onChange: (value) => { - if (magnifyGlass?.config) { - const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; - magnifyGlass.config.borderColor = normalizedColor; - if (value !== normalizedColor) { - try { - app.ui.settings.setSettingValue("🔍MagnifyGlass.BorderColor", normalizedColor); - } catch (e) { - console.warn('Failed to save normalized border color:', e); - } - } - magnifyGlass.applyUiChanges(); - } + magnifyGlass.applyUiChanges(); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ActivationKey", + name: "⌨️ Magnify Glass: Activation Key", + type: "combo", + options: ACTIVATION_KEYS.map((k) => ({ value: k, text: k })), + // Assuming ACTIVATION_KEYS is simply an array of strings + defaultValue: settings["🔍MagnifyGlass.ActivationKey"], + tooltip: "The key to activate the magnifier.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.activationKey = String(value).toLowerCase(); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.AltRequired", + name: "⌨️ Magnify Glass: Require Alt/Option Key", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.AltRequired"], + tooltip: "If Yes, Alt (Windows/Linux) or Option (Mac) must be held for activation.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.altRequired = !!value; + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.FollowCursor", + name: "🖱️ Magnify Glass: Follow Cursor Position", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.FollowCursor"], + tooltip: "If Yes, the magnifier window moves with the cursor.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.followCursor = !!value; + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.OffsetStep", + name: "⌨️ Magnify Glass: Offset Adjust Step", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.OffsetStep"], + min: 1, + max: 50, + step: 1, + tooltip: "How many graph units the view shifts when pressing arrow keys.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.offsetStep = parseInt(String(value), 10); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ResetKey", + name: "⌨️ Magnify Glass: Reset Offset Key", + type: "combo", + options: RESET_KEYS.map((k) => ({ value: k, text: k })), + defaultValue: settings["🔍MagnifyGlass.ResetKey"], + tooltip: "The key to reset the magnify glass offset to default.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.resetKey = String(value).toLowerCase(); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.DebugMode", + name: "🐞 Magnify Glass: Debug Mode", + type: "combo", + options: [{ value: true, text: "Enabled" }, { value: false, text: "Disabled" }], + defaultValue: settings["🔍MagnifyGlass.DebugMode"], + tooltip: "Show detailed logging and the debug visualization overlay.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.debugMode = !!value; + magnifyGlass.applyUiChanges(); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.GlassPosition", + name: "🖱️ Magnify Glass: Glass Position", + type: "combo", + options: GLASS_POSITIONS.map((p) => ({ value: p, text: p === "Bottom" ? "Bottom (Default)" : p })), + defaultValue: settings["🔍MagnifyGlass.GlassPosition"], + tooltip: "Position of the magnifying glass relative to the cursor.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.glassPosition = String(value); + if (magnifyGlass.state.active && !magnifyGlass.config.followCursor) { + const { x, y } = magnifyGlass.lastKnownMousePosition; + magnifyGlass.ui.positionGlass(x, y); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.ActivationKey", - name: "⌨️ Magnify Glass: Activation Key", - type: "combo", - options: ACTIVATION_KEYS, - defaultValue: settings["🔍MagnifyGlass.ActivationKey"], - tooltip: "The key to activate the magnifier.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.activationKey = value.toLowerCase(); - } + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.GlassShape", + name: "🖼️ Magnify Glass: Shape", + type: "combo", + options: GLASS_SHAPES.map((s) => ({ value: s, text: s })), + defaultValue: settings["🔍MagnifyGlass.GlassShape"], + tooltip: "Shape of the magnifying glass.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.glassShape = String(value); + magnifyGlass.applyUiChanges(); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.BorderEnabled", + name: "🖼️ Magnify Glass: Show Border", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.BorderEnabled"], + tooltip: "Enable or disable the border around the magnifying glass.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.borderEnabled = !!value; + magnifyGlass.applyUiChanges(); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.TextureFiltering", + name: "🖼️ Magnify Glass: Texture Filtering", + type: "combo", + options: [ + { value: "Linear", text: "Linear (Smooth)" }, + { value: "Nearest", text: "Nearest (Pixelated)" } + ], + defaultValue: settings["🔍MagnifyGlass.TextureFiltering"], + tooltip: "Controls how the magnified image is scaled.", + onChange: (value) => { + if ((magnifyGlass == null ? void 0 : magnifyGlass.config) && magnifyGlass.renderer) { + magnifyGlass.config.textureFiltering = String(value); + magnifyGlass.renderer.updateTextureFiltering(value); + if (magnifyGlass.state.active) { + magnifyGlass.updateMagnifiedView(); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.AltRequired", - name: "⌨️ Magnify Glass: Require Alt/Option Key", - type: "combo", - options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], - defaultValue: settings["🔍MagnifyGlass.AltRequired"], - tooltip: "If Yes, Alt (Windows/Linux) or Option (Mac) must be held for activation.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.altRequired = value; - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.FollowCursor", - name: "🖱️ Magnify Glass: Follow Cursor Position", - type: "combo", - options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], - defaultValue: settings["🔍MagnifyGlass.FollowCursor"], - tooltip: "If Yes, the magnifier window moves with the cursor.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.followCursor = value; - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.OffsetStep", - name: "⌨️ Magnify Glass: Offset Adjust Step", - type: "slider", - defaultValue: settings["🔍MagnifyGlass.OffsetStep"], - min: 1, - max: 50, - step: 1, - tooltip: "How many graph units the view shifts when pressing arrow keys.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.offsetStep = parseInt(value, 10); - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.ResetKey", - name: "⌨️ Magnify Glass: Reset Offset Key", - type: "combo", - options: RESET_KEYS, - defaultValue: settings["🔍MagnifyGlass.ResetKey"], - tooltip: "The key to reset the magnify glass offset to default.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.resetKey = value.toLowerCase(); - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.DebugMode", - name: "🐞 Magnify Glass: Debug Mode", - type: "combo", - options: [{ value: true, text: "Enabled" }, { value: false, text: "Disabled" }], - defaultValue: settings["🔍MagnifyGlass.DebugMode"], - tooltip: "Show detailed logging and the debug visualization overlay.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.debugMode = value; - magnifyGlass.applyUiChanges(); - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.GlassPosition", - name: "🖱️ Magnify Glass: Glass Position", - type: "combo", - options: GLASS_POSITIONS.map(p => ({ value: p, text: p === "Bottom" ? "Bottom (Default)" : p })), - defaultValue: settings["🔍MagnifyGlass.GlassPosition"], - tooltip: "Position of the magnifying glass relative to the cursor.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.glassPosition = value; - if (magnifyGlass.state.active && !magnifyGlass.config.followCursor) { - const { x, y } = magnifyGlass.lastKnownMousePosition; - magnifyGlass.ui.positionGlass(x, y); - } - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.GlassShape", - name: "🖼️ Magnify Glass: Shape", - type: "combo", - options: GLASS_SHAPES.map(s => ({ value: s, text: s })), - defaultValue: settings["🔍MagnifyGlass.GlassShape"], - tooltip: "Shape of the magnifying glass.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.glassShape = value; - magnifyGlass.applyUiChanges(); - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.BorderEnabled", - name: "🖼️ Magnify Glass: Show Border", - type: "combo", - options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], - defaultValue: settings["🔍MagnifyGlass.BorderEnabled"], - tooltip: "Enable or disable the border around the magnifying glass.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.borderEnabled = value; - magnifyGlass.applyUiChanges(); - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.TextureFiltering", - name: "🖼️ Magnify Glass: Texture Filtering", - type: "combo", - options: [ - { value: "Linear", text: "Linear (Smooth)" }, - { value: "Nearest", text: "Nearest (Pixelated)" } - ], - defaultValue: settings["🔍MagnifyGlass.TextureFiltering"], - tooltip: "Controls how the magnified image is scaled.", - onChange: (value) => { - if (magnifyGlass?.config && magnifyGlass.renderer) { - magnifyGlass.config.textureFiltering = value; - magnifyGlass.renderer.updateTextureFiltering(value); - if (magnifyGlass.state.active) { - magnifyGlass.updateMagnifiedView(); - } - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.AlwaysActiveMode", - name: "🔒 Magnify Glass: Always Active Mode", - type: "combo", - options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], - defaultValue: settings["🔍MagnifyGlass.AlwaysActiveMode"], - tooltip: "If Yes, activating the magnifier keeps it on until activated again.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.alwaysActiveMode = value; - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.ToggleFollowCursorKey", - name: "🔑 Magnify Glass: Toggle Follow Key", - type: "combo", - options: TOGGLE_FOLLOW_KEYS, - defaultValue: settings["🔍MagnifyGlass.ToggleFollowCursorKey"], - tooltip: "The key to toggle the 'Follow Cursor' behavior.", - onChange: (value) => { - if (magnifyGlass?.config) { - magnifyGlass.config.toggleFollowCursorKey = value.toLowerCase(); - } - } - }); + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.AlwaysActiveMode", + name: "🔒 Magnify Glass: Always Active Mode", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.AlwaysActiveMode"], + tooltip: "If Yes, activating the magnifier keeps it on until activated again.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.alwaysActiveMode = !!value; + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ToggleFollowCursorKey", + name: "🔑 Magnify Glass: Toggle Follow Key", + type: "combo", + options: TOGGLE_FOLLOW_KEYS.map((k) => ({ value: k, text: k })), + defaultValue: settings["🔍MagnifyGlass.ToggleFollowCursorKey"], + tooltip: "The key to toggle the 'Follow Cursor' behavior.", + onChange: (value) => { + if (magnifyGlass == null ? void 0 : magnifyGlass.config) { + magnifyGlass.config.toggleFollowCursorKey = String(value).toLowerCase(); + } + } + }); } - -/** - * Register all info panel settings with ComfyUI. - * @param {object} stateManager - The StateManager instance for the info panel - * @param {object} uiManager - The UIManager instance for the info panel - * @param {object} positionManager - The PositionManager instance for the info panel - */ -export function registerPanelSettings(stateManager, uiManager, positionManager) { - const settings = DEFAULT_PANEL_SETTINGS; - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.InfoPanelEnabled", - name: "📊 Info Panel: Enable", - type: "combo", - options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], - defaultValue: settings["🔍MagnifyGlass.InfoPanelEnabled"], - tooltip: "Enable or disable the information panel.", - onChange: (value) => { - if (stateManager) { - stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] = value; - if (!value && uiManager) { - uiManager.hide(); - } - } +function registerPanelSettings(stateManager, uiManager, positionManager) { + const settings = DEFAULT_PANEL_SETTINGS; + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelEnabled", + name: "📊 Info Panel: Enable", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.InfoPanelEnabled"], + tooltip: "Enable or disable the information panel.", + onChange: (value) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelEnabled"] = !!value; + if (!value && uiManager) { + uiManager.hide(); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.InfoPanelPosition", - name: "📊 Info Panel: Position", - type: "combo", - options: PANEL_POSITIONS.map(p => ({ value: p, text: p })), - defaultValue: settings["🔍MagnifyGlass.InfoPanelPosition"], - tooltip: "Position of the info panel relative to the magnifying glass.", - onChange: (value) => { - if (stateManager) { - stateManager.state.settings["🔍MagnifyGlass.InfoPanelPosition"] = value; - if (positionManager) { - positionManager.positionPanel(); - } - } + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelPosition", + name: "📊 Info Panel: Position", + type: "combo", + options: PANEL_POSITIONS.map((p) => ({ value: p, text: p })), + defaultValue: settings["🔍MagnifyGlass.InfoPanelPosition"], + tooltip: "Position of the info panel relative to the magnifying glass.", + onChange: (value) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelPosition"] = String(value); + if (positionManager) { + positionManager.positionPanel(); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.InfoPanelWidth", - name: "📊 Info Panel: Width (px)", - type: "slider", - defaultValue: settings["🔍MagnifyGlass.InfoPanelWidth"], - min: 200, - max: 600, - step: 20, - tooltip: "Width of the information panel in pixels.", - onChange: (value) => { - if (stateManager) { - stateManager.state.settings["🔍MagnifyGlass.InfoPanelWidth"] = value; - if (uiManager) { - uiManager.applyStyles(); - } - } + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelWidth", + name: "📊 Info Panel: Width (px)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.InfoPanelWidth"], + min: 200, + max: 600, + step: 20, + tooltip: "Width of the information panel in pixels.", + onChange: (value) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelWidth"] = parseInt(String(value), 10); + if (uiManager) { + uiManager.applyStyles(); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.InfoPanelOpacity", - name: "📊 Info Panel: Opacity (%)", - type: "slider", - defaultValue: settings["🔍MagnifyGlass.InfoPanelOpacity"], - min: 10, - max: 100, - step: 5, - tooltip: "Opacity of the information panel background.", - onChange: (value) => { - if (stateManager) { - stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"] = value; - if (uiManager) { - uiManager.applyStyles(); - } - } + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelOpacity", + name: "📊 Info Panel: Opacity (%)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.InfoPanelOpacity"], + min: 10, + max: 100, + step: 5, + tooltip: "Opacity of the information panel background.", + onChange: (value) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelOpacity"] = parseInt(String(value), 10); + if (uiManager) { + uiManager.applyStyles(); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.InfoPanelMaxHeight", - name: "📊 Info Panel: Max Height (px)", - type: "slider", - defaultValue: settings["🔍MagnifyGlass.InfoPanelMaxHeight"], - min: 200, - max: 1500, - step: 50, - tooltip: "Maximum height of the information panel in pixels.", - onChange: (value) => { - if (stateManager) { - stateManager.state.settings["🔍MagnifyGlass.InfoPanelMaxHeight"] = value; - if (uiManager) { - uiManager.applyStyles(); - } - } + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelMaxHeight", + name: "📊 Info Panel: Max Height (px)", + type: "slider", + defaultValue: settings["🔍MagnifyGlass.InfoPanelMaxHeight"], + min: 200, + max: 1500, + step: 50, + tooltip: "Maximum height of the information panel in pixels.", + onChange: (value) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelMaxHeight"] = parseInt(String(value), 10); + if (uiManager) { + uiManager.applyStyles(); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.InfoPanelAnimations", - name: "📊 Info Panel: Animations", - type: "combo", - options: [{ value: true, text: "Enabled" }, { value: false, text: "Disabled" }], - defaultValue: settings["🔍MagnifyGlass.InfoPanelAnimations"], - tooltip: "Enable or disable animations for the info panel.", - onChange: (value) => { - if (stateManager) { - stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] = value; - } + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelAnimations", + name: "📊 Info Panel: Animations", + type: "combo", + options: [{ value: true, text: "Enabled" }, { value: false, text: "Disabled" }], + defaultValue: settings["🔍MagnifyGlass.InfoPanelAnimations"], + tooltip: "Enable or disable animations for the info panel.", + onChange: (value) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.InfoPanelAnimations"] = !!value; + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ShowInspectorTab", + name: "📊 Info Panel: Show Inspector Tab", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.ShowInspectorTab"], + tooltip: "Show or hide the Inspector tab with cursor and canvas information.", + onChange: (value) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.ShowInspectorTab"] = !!value; + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.ShowHoveringControls", + name: "📊 Info Panel: Show Hover Controls", + type: "combo", + options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], + defaultValue: settings["🔍MagnifyGlass.ShowHoveringControls"], + tooltip: "Show or hide hovering UI controls above the info panel.", + onChange: (value) => { + if (stateManager) { + stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"] = !!value; + if (uiManager && uiManager.elements.controls) { + uiManager.elements.controls.style.display = value ? "flex" : "none"; } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.ShowInspectorTab", - name: "📊 Info Panel: Show Inspector Tab", - type: "combo", - options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], - defaultValue: settings["🔍MagnifyGlass.ShowInspectorTab"], - tooltip: "Show or hide the Inspector tab with cursor and canvas information.", - onChange: (value) => { - if (stateManager) { - stateManager.state.settings["🔍MagnifyGlass.ShowInspectorTab"] = value; - } + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelTextColor", + name: "🎨 Info Panel: Text Color", + type: "color", + defaultValue: settings["🔍MagnifyGlass.InfoPanelTextColor"], + tooltip: "Custom text color for the info panel.", + onChange: (value) => { + if (stateManager) { + const strValue = String(value); + const normalizedColor = strValue && !strValue.startsWith("#") ? `#${strValue}` : strValue; + stateManager.state.settings["🔍MagnifyGlass.InfoPanelTextColor"] = normalizedColor; + if (uiManager) { + uiManager.applyStyles(); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.ShowHoveringControls", - name: "📊 Info Panel: Show Hover Controls", - type: "combo", - options: [{ value: true, text: "Yes" }, { value: false, text: "No" }], - defaultValue: settings["🔍MagnifyGlass.ShowHoveringControls"], - tooltip: "Show or hide hovering UI controls above the info panel.", - onChange: (value) => { - if (stateManager) { - stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"] = value; - if (uiManager && uiManager.elements.controls) { - uiManager.elements.controls.style.display = value ? 'flex' : 'none'; - } - } + } + } + }); + app.ui.settings.addSetting({ + id: "🔍MagnifyGlass.InfoPanelAccentColor", + name: "🎨 Info Panel: Accent Color", + type: "color", + defaultValue: settings["🔍MagnifyGlass.InfoPanelAccentColor"], + tooltip: "Custom accent color for the info panel.", + onChange: (value) => { + if (stateManager) { + const strValue = String(value); + const normalizedColor = strValue && !strValue.startsWith("#") ? `#${strValue}` : strValue; + stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"] = normalizedColor; + if (uiManager) { + uiManager.applyStyles(); } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.InfoPanelTextColor", - name: "🎨 Info Panel: Text Color", - type: "color", - defaultValue: settings["🔍MagnifyGlass.InfoPanelTextColor"], - tooltip: "Custom text color for the info panel.", - onChange: (value) => { - if (stateManager) { - const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; - stateManager.state.settings["🔍MagnifyGlass.InfoPanelTextColor"] = normalizedColor; - if (uiManager) { - uiManager.applyStyles(); - } - } - } - }); - - app.ui.settings.addSetting({ - id: "🔍MagnifyGlass.InfoPanelAccentColor", - name: "🎨 Info Panel: Accent Color", - type: "color", - defaultValue: settings["🔍MagnifyGlass.InfoPanelAccentColor"], - tooltip: "Custom accent color for the info panel.", - onChange: (value) => { - if (stateManager) { - const normalizedColor = value && !value.startsWith('#') ? `#${value}` : value; - stateManager.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"] = normalizedColor; - if (uiManager) { - uiManager.applyStyles(); - } - } - } - }); -} - -/** - * Get a setting configuration object for ComfyUI settings registration. - * This function generates the setting config based on the key and default value. - * - * @param {string} key - The setting key - * @param {*} defaultValue - The default value for the setting - * @returns {object|null} - The setting configuration or null if not found - */ -export function getSettingConfig(key, defaultValue) { - const configs = { - "🔍MagnifyGlass.InfoPanelEnabled": { - id: key, - name: "📋 Magnify Glass: Info Panel", - type: "combo", - options: [ - { value: true, text: "Enabled" }, - { value: false, text: "Disabled" } - ], - defaultValue, - tooltip: "Enable or disable the professional information panel." - }, - "🔍MagnifyGlass.InfoPanelPosition": { - id: key, - name: "📋 Info Panel: Position", - type: "combo", - options: PANEL_POSITIONS.map(pos => ({ value: pos, text: pos })), - defaultValue, - tooltip: "Position of the info panel relative to the magnify glass." - }, - "🔍MagnifyGlass.InfoPanelWidth": { - id: key, - name: "📋 Info Panel: Width (px)", - type: "slider", - defaultValue, - min: 200, - max: 600, - step: 20, - tooltip: "Width of the info panel in pixels." - }, - "🔍MagnifyGlass.InfoPanelMaxHeight": { - id: key, - name: "📋 Info Panel: Max Height (px)", - type: "slider", - defaultValue, - min: 200, - max: 1500, - step: 50, - tooltip: "Maximum height of the info panel." - }, - "🔍MagnifyGlass.InfoPanelOpacity": { - id: key, - name: "📋 Info Panel: Opacity (%)", - type: "slider", - defaultValue, - min: 10, - max: 100, - step: 5, - tooltip: "Opacity of the info panel background." - }, - "🔍MagnifyGlass.InfoPanelAnimations": { - id: key, - name: "📋 Info Panel: Animations", - type: "combo", - options: [ - { value: true, text: "Enabled" }, - { value: false, text: "Disabled" } - ], - defaultValue, - tooltip: "Enable smooth animations for the info panel." - }, - "🔍MagnifyGlass.ShowInspectorTab": { - id: key, - name: "📋 Info Panel: Show Inspector", - type: "combo", - options: [ - { value: true, text: "Yes" }, - { value: false, text: "No" } - ], - defaultValue, - tooltip: "Show the inspector tab with cursor and canvas information." - }, - "🔍MagnifyGlass.ToggleHotkey": { - id: key, - name: "📋 Info Panel: Toggle Hotkey", - type: "combo", - options: ["i", "j", "k", "l", "n", "m"].map(k => ({ value: k, text: k.toUpperCase() })), - defaultValue, - tooltip: "Key to toggle the info panel visibility." - }, - "🔍MagnifyGlass.GlassPreviewToggleHotkey": { - id: key, - name: "📋 Info Panel: Glass Toggle Hotkey", - type: "combo", - options: ["g", "f", "v", "b"].map(k => ({ value: k, text: k.toUpperCase() })), - defaultValue, - tooltip: "Key to toggle the magnify glass preview visibility." - }, - "🔍MagnifyGlass.PinPanelHotkey": { - id: key, - name: "📋 Info Panel: Pin Hotkey", - type: "combo", - options: ["u", "p", "y", "t"].map(k => ({ value: k, text: k.toUpperCase() })), - defaultValue, - tooltip: "Key to pin/unlock the info panel at the current mouse location." - }, - "🔍MagnifyGlass.ShowHoveringControls": { - id: key, - name: "📋 Info Panel: Floating Controls", - type: "combo", - options: [ - { value: true, text: "Show" }, - { value: false, text: "Hide" } - ], - defaultValue, - tooltip: "Show floating control buttons near the info panel." - }, - "🔍MagnifyGlass.ControlsPosition": { - id: key, - name: "📋 Info Panel: Controls Position", - type: "combo", - options: ["top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right"].map(p => ({ value: p, text: p })), - defaultValue, - tooltip: "Position of the floating control buttons." - }, - "🔍MagnifyGlass.InfoPanelTextColor": { - id: key, - name: "🎨 Info Panel: Text Color", - type: "color", - defaultValue, - tooltip: "Color for text in the info panel." - }, - "🔍MagnifyGlass.InfoPanelAccentColor": { - id: key, - name: "🎨 Info Panel: Accent Color", - type: "color", - defaultValue, - tooltip: "Accent color for highlights in the info panel." - } - }; - - return configs[key] || null; + } + } + }); } +export { + DEFAULT_GLASS_SETTINGS, + DEFAULT_PANEL_SETTINGS, + registerGlassSettings, + registerPanelSettings +}; +//# sourceMappingURL=settings.js.map diff --git a/web/shared/settings.js.map b/web/shared/settings.js.map new file mode 100644 index 0000000..dc8d86a --- /dev/null +++ b/web/shared/settings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"settings.js","sources":["../../src/shared/settings.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Settings Configuration\n * \n * Centralized settings definitions and registration functions.\n */\n\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\nimport {\n DEFAULT_GLASS_SIZE,\n DEFAULT_ZOOM_FACTOR,\n DEFAULT_BORDER_WIDTH,\n DEFAULT_BORDER_COLOR,\n DEFAULT_OFFSET_STEP,\n DEFAULT_PANEL_WIDTH,\n DEFAULT_PANEL_MAX_HEIGHT,\n DEFAULT_PANEL_OPACITY,\n GLASS_POSITIONS,\n GLASS_SHAPES,\n ACTIVATION_KEYS,\n RESET_KEYS,\n TOGGLE_FOLLOW_KEYS,\n PANEL_POSITIONS\n} from './constants';\n\n// Define the shape of our settings objects for better type safety\nexport interface GlassSettings {\n \"🔍MagnifyGlass.ZoomFactor\": number;\n \"🔍MagnifyGlass.GlassSize\": number;\n \"🔍MagnifyGlass.BorderColor\": string;\n \"🔍MagnifyGlass.BorderWidth\": number;\n \"🔍MagnifyGlass.ActivationKey\": string;\n \"🔍MagnifyGlass.AltRequired\": boolean;\n \"🔍MagnifyGlass.FollowCursor\": boolean;\n \"🔍MagnifyGlass.DebugMode\": boolean;\n \"🔍MagnifyGlass.OffsetStep\": number;\n \"🔍MagnifyGlass.GlassPosition\": string;\n \"🔍MagnifyGlass.ResetKey\": string;\n \"🔍MagnifyGlass.GlassShape\": string;\n \"🔍MagnifyGlass.BorderEnabled\": boolean;\n \"🔍MagnifyGlass.TextureFiltering\": string;\n \"🔍MagnifyGlass.AlwaysActiveMode\": boolean;\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": string;\n}\n\nexport interface PanelSettings {\n \"🔍MagnifyGlass.InfoPanelEnabled\": boolean;\n \"🔍MagnifyGlass.InfoPanelPosition\": string;\n \"🔍MagnifyGlass.InfoPanelWidth\": number;\n \"🔍MagnifyGlass.InfoPanelOpacity\": number;\n \"🔍MagnifyGlass.InfoPanelMaxHeight\": number;\n \"🔍MagnifyGlass.InfoPanelAnimations\": boolean;\n \"🔍MagnifyGlass.ShowInspectorTab\": boolean;\n \"🔍MagnifyGlass.ToggleHotkey\": string;\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": string;\n \"🔍MagnifyGlass.PinPanelHotkey\": string;\n \"🔍MagnifyGlass.ShowHoveringControls\": boolean;\n \"🔍MagnifyGlass.ControlsPosition\": string;\n \"🔍MagnifyGlass.InfoPanelTextColor\": string;\n \"🔍MagnifyGlass.InfoPanelAccentColor\": string;\n [key: string]: any; // Allow indexing\n}\n\n/**\n * Default settings for the magnify glass component.\n */\nexport const DEFAULT_GLASS_SETTINGS: GlassSettings = {\n \"🔍MagnifyGlass.ZoomFactor\": DEFAULT_ZOOM_FACTOR,\n \"🔍MagnifyGlass.GlassSize\": DEFAULT_GLASS_SIZE,\n \"🔍MagnifyGlass.BorderColor\": DEFAULT_BORDER_COLOR,\n \"🔍MagnifyGlass.BorderWidth\": DEFAULT_BORDER_WIDTH,\n \"🔍MagnifyGlass.ActivationKey\": \"x\",\n \"🔍MagnifyGlass.AltRequired\": false,\n \"🔍MagnifyGlass.FollowCursor\": false,\n \"🔍MagnifyGlass.DebugMode\": false,\n \"🔍MagnifyGlass.OffsetStep\": DEFAULT_OFFSET_STEP,\n \"🔍MagnifyGlass.GlassPosition\": \"Top-Right\",\n \"🔍MagnifyGlass.ResetKey\": \"o\",\n \"🔍MagnifyGlass.GlassShape\": \"Rounded Square\",\n \"🔍MagnifyGlass.BorderEnabled\": true,\n \"🔍MagnifyGlass.TextureFiltering\": \"Linear\",\n \"🔍MagnifyGlass.AlwaysActiveMode\": true,\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": \"h\",\n};\n\n/**\n * Default settings for the info panel component.\n */\nexport const DEFAULT_PANEL_SETTINGS: PanelSettings = {\n \"🔍MagnifyGlass.InfoPanelEnabled\": true,\n \"🔍MagnifyGlass.InfoPanelPosition\": \"Bottom\",\n \"🔍MagnifyGlass.InfoPanelWidth\": DEFAULT_PANEL_WIDTH,\n \"🔍MagnifyGlass.InfoPanelOpacity\": DEFAULT_PANEL_OPACITY,\n \"🔍MagnifyGlass.InfoPanelMaxHeight\": DEFAULT_PANEL_MAX_HEIGHT,\n \"🔍MagnifyGlass.InfoPanelAnimations\": false,\n \"🔍MagnifyGlass.ShowInspectorTab\": false,\n \"🔍MagnifyGlass.ToggleHotkey\": \"i\",\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": \"g\",\n \"🔍MagnifyGlass.PinPanelHotkey\": \"u\",\n \"🔍MagnifyGlass.ShowHoveringControls\": true,\n \"🔍MagnifyGlass.ControlsPosition\": \"bottom\",\n \"🔍MagnifyGlass.InfoPanelTextColor\": \"#6b7280\",\n \"🔍MagnifyGlass.InfoPanelAccentColor\": \"#3b82f6\",\n};\n\n/**\n * Register all magnify glass settings with ComfyUI.\n * @param magnifyGlass - The MagnifyGlass instance\n */\nexport function registerGlassSettings(magnifyGlass: any): void {\n const settings = DEFAULT_GLASS_SETTINGS;\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ZoomFactor\",\n name: \"🔍 Magnify Glass: Zoom Factor (%)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.ZoomFactor\"],\n min: 100,\n max: 1000,\n step: 25,\n tooltip: \"Magnification level as a percentage (e.g., 300 = 3x zoom, 150 = 1.5x zoom).\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.zoomFactor = typeof value === 'number' ? value / 100 : parseFloat(value) / 100;\n if (magnifyGlass.state.active) {\n magnifyGlass.updateMagnifiedView();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.GlassSize\",\n name: \"🔍 Magnify Glass: Size (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.GlassSize\"],\n min: 50,\n max: 100,\n step: 10,\n tooltip: \"Diameter of the magnifying glass circle in pixels.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.glassSize = parseInt(String(value), 10);\n magnifyGlass.applyUiChanges();\n if (magnifyGlass.state.active) {\n magnifyGlass.updateMagnifiedView();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.BorderWidth\",\n name: \"🔍 Magnify Glass: Border Width (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.BorderWidth\"],\n min: 0,\n max: 10,\n step: 0.1,\n tooltip: \"Width of the border around the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.borderWidth = parseFloat(String(value));\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.BorderColor\",\n name: \"🎨 Magnify Glass: Border Color\",\n type: \"color\",\n defaultValue: settings[\"🔍MagnifyGlass.BorderColor\"],\n tooltip: \"Color of the border around the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n const strValue = String(value);\n const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue;\n magnifyGlass.config.borderColor = normalizedColor;\n if (strValue !== normalizedColor) {\n try {\n app.ui.settings.setSettingValue(\"🔍MagnifyGlass.BorderColor\", normalizedColor);\n } catch (e) {\n console.warn('Failed to save normalized border color:', e);\n }\n }\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ActivationKey\",\n name: \"⌨️ Magnify Glass: Activation Key\",\n type: \"combo\",\n options: ACTIVATION_KEYS.map(k => ({ value: k, text: k })), // Assuming ACTIVATION_KEYS is simply an array of strings\n defaultValue: settings[\"🔍MagnifyGlass.ActivationKey\"],\n tooltip: \"The key to activate the magnifier.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.activationKey = String(value).toLowerCase();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.AltRequired\",\n name: \"⌨️ Magnify Glass: Require Alt/Option Key\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.AltRequired\"],\n tooltip: \"If Yes, Alt (Windows/Linux) or Option (Mac) must be held for activation.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.altRequired = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.FollowCursor\",\n name: \"🖱️ Magnify Glass: Follow Cursor Position\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.FollowCursor\"],\n tooltip: \"If Yes, the magnifier window moves with the cursor.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.followCursor = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.OffsetStep\",\n name: \"⌨️ Magnify Glass: Offset Adjust Step\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.OffsetStep\"],\n min: 1,\n max: 50,\n step: 1,\n tooltip: \"How many graph units the view shifts when pressing arrow keys.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.offsetStep = parseInt(String(value), 10);\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ResetKey\",\n name: \"⌨️ Magnify Glass: Reset Offset Key\",\n type: \"combo\",\n options: RESET_KEYS.map(k => ({ value: k, text: k })),\n defaultValue: settings[\"🔍MagnifyGlass.ResetKey\"],\n tooltip: \"The key to reset the magnify glass offset to default.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.resetKey = String(value).toLowerCase();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.DebugMode\",\n name: \"🐞 Magnify Glass: Debug Mode\",\n type: \"combo\",\n options: [{ value: true, text: \"Enabled\" }, { value: false, text: \"Disabled\" }],\n defaultValue: settings[\"🔍MagnifyGlass.DebugMode\"],\n tooltip: \"Show detailed logging and the debug visualization overlay.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.debugMode = !!value;\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.GlassPosition\",\n name: \"🖱️ Magnify Glass: Glass Position\",\n type: \"combo\",\n options: GLASS_POSITIONS.map(p => ({ value: p, text: p === \"Bottom\" ? \"Bottom (Default)\" : p })),\n defaultValue: settings[\"🔍MagnifyGlass.GlassPosition\"],\n tooltip: \"Position of the magnifying glass relative to the cursor.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.glassPosition = String(value);\n if (magnifyGlass.state.active && !magnifyGlass.config.followCursor) {\n const { x, y } = magnifyGlass.lastKnownMousePosition;\n magnifyGlass.ui.positionGlass(x, y);\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.GlassShape\",\n name: \"🖼️ Magnify Glass: Shape\",\n type: \"combo\",\n options: GLASS_SHAPES.map(s => ({ value: s, text: s })),\n defaultValue: settings[\"🔍MagnifyGlass.GlassShape\"],\n tooltip: \"Shape of the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.glassShape = String(value);\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.BorderEnabled\",\n name: \"🖼️ Magnify Glass: Show Border\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.BorderEnabled\"],\n tooltip: \"Enable or disable the border around the magnifying glass.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.borderEnabled = !!value;\n magnifyGlass.applyUiChanges();\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.TextureFiltering\",\n name: \"🖼️ Magnify Glass: Texture Filtering\",\n type: \"combo\",\n options: [\n { value: \"Linear\", text: \"Linear (Smooth)\" },\n { value: \"Nearest\", text: \"Nearest (Pixelated)\" }\n ],\n defaultValue: settings[\"🔍MagnifyGlass.TextureFiltering\"],\n tooltip: \"Controls how the magnified image is scaled.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config && magnifyGlass.renderer) {\n magnifyGlass.config.textureFiltering = String(value);\n magnifyGlass.renderer.updateTextureFiltering(value);\n if (magnifyGlass.state.active) {\n magnifyGlass.updateMagnifiedView();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.AlwaysActiveMode\",\n name: \"🔒 Magnify Glass: Always Active Mode\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.AlwaysActiveMode\"],\n tooltip: \"If Yes, activating the magnifier keeps it on until activated again.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.alwaysActiveMode = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ToggleFollowCursorKey\",\n name: \"🔑 Magnify Glass: Toggle Follow Key\",\n type: \"combo\",\n options: TOGGLE_FOLLOW_KEYS.map(k => ({ value: k, text: k })),\n defaultValue: settings[\"🔍MagnifyGlass.ToggleFollowCursorKey\"],\n tooltip: \"The key to toggle the 'Follow Cursor' behavior.\",\n onChange: (value: any) => {\n if (magnifyGlass?.config) {\n magnifyGlass.config.toggleFollowCursorKey = String(value).toLowerCase();\n }\n }\n });\n}\n\n/**\n * Register all info panel settings with ComfyUI.\n * @param stateManager - The StateManager instance for the info panel\n * @param uiManager - The UIManager instance for the info panel\n * @param positionManager - The PositionManager instance for the info panel\n */\nexport function registerPanelSettings(stateManager: any, uiManager: any, positionManager: any): void {\n const settings = DEFAULT_PANEL_SETTINGS;\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelEnabled\",\n name: \"📊 Info Panel: Enable\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelEnabled\"],\n tooltip: \"Enable or disable the information panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelEnabled\"] = !!value;\n if (!value && uiManager) {\n uiManager.hide();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelPosition\",\n name: \"📊 Info Panel: Position\",\n type: \"combo\",\n options: PANEL_POSITIONS.map(p => ({ value: p, text: p })),\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelPosition\"],\n tooltip: \"Position of the info panel relative to the magnifying glass.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelPosition\"] = String(value);\n if (positionManager) {\n positionManager.positionPanel();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelWidth\",\n name: \"📊 Info Panel: Width (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelWidth\"],\n min: 200,\n max: 600,\n step: 20,\n tooltip: \"Width of the information panel in pixels.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelWidth\"] = parseInt(String(value), 10);\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelOpacity\",\n name: \"📊 Info Panel: Opacity (%)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelOpacity\"],\n min: 10,\n max: 100,\n step: 5,\n tooltip: \"Opacity of the information panel background.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"] = parseInt(String(value), 10);\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelMaxHeight\",\n name: \"📊 Info Panel: Max Height (px)\",\n type: \"slider\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"],\n min: 200,\n max: 1500,\n step: 50,\n tooltip: \"Maximum height of the information panel in pixels.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"] = parseInt(String(value), 10);\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelAnimations\",\n name: \"📊 Info Panel: Animations\",\n type: \"combo\",\n options: [{ value: true, text: \"Enabled\" }, { value: false, text: \"Disabled\" }],\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelAnimations\"],\n tooltip: \"Enable or disable animations for the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAnimations\"] = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ShowInspectorTab\",\n name: \"📊 Info Panel: Show Inspector Tab\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.ShowInspectorTab\"],\n tooltip: \"Show or hide the Inspector tab with cursor and canvas information.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.ShowInspectorTab\"] = !!value;\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.ShowHoveringControls\",\n name: \"📊 Info Panel: Show Hover Controls\",\n type: \"combo\",\n options: [{ value: true, text: \"Yes\" }, { value: false, text: \"No\" }],\n defaultValue: settings[\"🔍MagnifyGlass.ShowHoveringControls\"],\n tooltip: \"Show or hide hovering UI controls above the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n stateManager.state.settings[\"🔍MagnifyGlass.ShowHoveringControls\"] = !!value;\n if (uiManager && uiManager.elements.controls) {\n uiManager.elements.controls.style.display = value ? 'flex' : 'none';\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelTextColor\",\n name: \"🎨 Info Panel: Text Color\",\n type: \"color\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelTextColor\"],\n tooltip: \"Custom text color for the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n const strValue = String(value);\n const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue;\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelTextColor\"] = normalizedColor;\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n\n app.ui.settings.addSetting({\n id: \"🔍MagnifyGlass.InfoPanelAccentColor\",\n name: \"🎨 Info Panel: Accent Color\",\n type: \"color\",\n defaultValue: settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"],\n tooltip: \"Custom accent color for the info panel.\",\n onChange: (value: any) => {\n if (stateManager) {\n const strValue = String(value);\n const normalizedColor = strValue && !strValue.startsWith('#') ? `#${strValue}` : strValue;\n stateManager.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] = normalizedColor;\n if (uiManager) {\n uiManager.applyStyles();\n }\n }\n }\n });\n}\n\n/**\n * Get a setting configuration object for ComfyUI settings registration.\n * This function generates the setting config based on the key and default value.\n * \n * @param key - The setting key\n * @param defaultValue - The default value for the setting\n * @returns The setting configuration or null if not found\n */\nexport function getSettingConfig(key: string, defaultValue: any): any {\n const configs: Record = {\n \"🔍MagnifyGlass.InfoPanelEnabled\": {\n id: key,\n name: \"📋 Magnify Glass: Info Panel\",\n type: \"combo\",\n options: [\n { value: true, text: \"Enabled\" },\n { value: false, text: \"Disabled\" }\n ],\n defaultValue,\n tooltip: \"Enable or disable the professional information panel.\"\n },\n \"🔍MagnifyGlass.InfoPanelPosition\": {\n id: key,\n name: \"📋 Info Panel: Position\",\n type: \"combo\",\n options: PANEL_POSITIONS.map(pos => ({ value: pos, text: pos })),\n defaultValue,\n tooltip: \"Position of the info panel relative to the magnify glass.\"\n },\n \"🔍MagnifyGlass.InfoPanelWidth\": {\n id: key,\n name: \"📋 Info Panel: Width (px)\",\n type: \"slider\",\n defaultValue,\n min: 200,\n max: 600,\n step: 20,\n tooltip: \"Width of the info panel in pixels.\"\n },\n \"🔍MagnifyGlass.InfoPanelMaxHeight\": {\n id: key,\n name: \"📋 Info Panel: Max Height (px)\",\n type: \"slider\",\n defaultValue,\n min: 200,\n max: 1500,\n step: 50,\n tooltip: \"Maximum height of the info panel.\"\n },\n \"🔍MagnifyGlass.InfoPanelOpacity\": {\n id: key,\n name: \"📋 Info Panel: Opacity (%)\",\n type: \"slider\",\n defaultValue,\n min: 10,\n max: 100,\n step: 5,\n tooltip: \"Opacity of the info panel background.\"\n },\n \"🔍MagnifyGlass.InfoPanelAnimations\": {\n id: key,\n name: \"📋 Info Panel: Animations\",\n type: \"combo\",\n options: [\n { value: true, text: \"Enabled\" },\n { value: false, text: \"Disabled\" }\n ],\n defaultValue,\n tooltip: \"Enable smooth animations for the info panel.\"\n },\n \"🔍MagnifyGlass.ShowInspectorTab\": {\n id: key,\n name: \"📋 Info Panel: Show Inspector\",\n type: \"combo\",\n options: [\n { value: true, text: \"Yes\" },\n { value: false, text: \"No\" }\n ],\n defaultValue,\n tooltip: \"Show the inspector tab with cursor and canvas information.\"\n },\n \"🔍MagnifyGlass.ToggleHotkey\": {\n id: key,\n name: \"📋 Info Panel: Toggle Hotkey\",\n type: \"combo\",\n options: [\"i\", \"j\", \"k\", \"l\", \"n\", \"m\"].map(k => ({ value: k, text: k.toUpperCase() })),\n defaultValue,\n tooltip: \"Key to toggle the info panel visibility.\"\n },\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": {\n id: key,\n name: \"📋 Info Panel: Glass Toggle Hotkey\",\n type: \"combo\",\n options: [\"g\", \"f\", \"v\", \"b\"].map(k => ({ value: k, text: k.toUpperCase() })),\n defaultValue,\n tooltip: \"Key to toggle the magnify glass preview visibility.\"\n },\n \"🔍MagnifyGlass.PinPanelHotkey\": {\n id: key,\n name: \"📋 Info Panel: Pin Hotkey\",\n type: \"combo\",\n options: [\"u\", \"p\", \"y\", \"t\"].map(k => ({ value: k, text: k.toUpperCase() })),\n defaultValue,\n tooltip: \"Key to pin/unlock the info panel at the current mouse location.\"\n },\n \"🔍MagnifyGlass.ShowHoveringControls\": {\n id: key,\n name: \"📋 Info Panel: Floating Controls\",\n type: \"combo\",\n options: [\n { value: true, text: \"Show\" },\n { value: false, text: \"Hide\" }\n ],\n defaultValue,\n tooltip: \"Show floating control buttons near the info panel.\"\n },\n \"🔍MagnifyGlass.ControlsPosition\": {\n id: key,\n name: \"📋 Info Panel: Controls Position\",\n type: \"combo\",\n options: [\"top-left\", \"top-right\", \"bottom-left\", \"bottom-right\", \"top\", \"bottom\", \"left\", \"right\"].map(p => ({ value: p, text: p })),\n defaultValue,\n tooltip: \"Position of the floating control buttons.\"\n },\n \"🔍MagnifyGlass.InfoPanelTextColor\": {\n id: key,\n name: \"🎨 Info Panel: Text Color\",\n type: \"color\",\n defaultValue,\n tooltip: \"Color for text in the info panel.\"\n },\n \"🔍MagnifyGlass.InfoPanelAccentColor\": {\n id: key,\n name: \"🎨 Info Panel: Accent Color\",\n type: \"color\",\n defaultValue,\n tooltip: \"Accent color for highlights in the info panel.\"\n }\n };\n\n return configs[key] || null;\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/shared/utils.js b/web/shared/utils.js index 17a3785..f8d6f7d 100644 --- a/web/shared/utils.js +++ b/web/shared/utils.js @@ -1,120 +1,53 @@ -/** - * ComfyUI MagnifyGlass - Shared Utilities - * - * Common utility functions shared between magnify glass and info panel modules. - */ - -import { app } from "../../../../scripts/app.js"; - -/** - * Safely get a setting value from ComfyUI settings with fallback to default. - * @param {string} key - The setting key - * @param {*} defaultValue - Default value if setting not found - * @returns {*} The setting value or default - */ -export function getSettingValue(key, defaultValue) { - try { - const value = app.ui.settings.getSettingValue(key); - return value === undefined ? defaultValue : value; - } catch (e) { - console.warn(`ComfyUI Magnifying Glass: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`); - return defaultValue; - } +import { app } from "/scripts/app.js"; +function getSettingValue(key, defaultValue) { + try { + const value = app.ui.settings.getSettingValue(key); + return value === void 0 ? defaultValue : value; + } catch (e) { + console.warn(`ComfyUI Magnifying Glass: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`); + return defaultValue; + } } - -/** - * Check if the user is currently typing in an input field. - * Used for smart input detection to avoid triggering hotkeys while typing. - * @returns {boolean} True if user is typing in an input field - */ -export function isUserTyping() { - const activeElement = document.activeElement; - if (!activeElement) return false; - - // Check if the active element is an input, textarea, or contenteditable - const tagName = activeElement.tagName.toLowerCase(); - if (tagName === 'input' || tagName === 'textarea') { - return true; - } - - // Check for contenteditable elements - if (activeElement.contentEditable === 'true') { - return true; - } - - // Check if it's inside a form or has input-like classes - if (activeElement.closest('form') || - activeElement.classList.contains('cm-editor') || // CodeMirror editor - activeElement.classList.contains('monaco-editor') || // Monaco editor - activeElement.closest('.cm-editor') || - activeElement.closest('.monaco-editor')) { - return true; - } - - return false; +function isUserTyping() { + const activeElement = document.activeElement; + if (!activeElement) return false; + const tagName = activeElement.tagName.toLowerCase(); + if (tagName === "input" || tagName === "textarea") { + return true; + } + if (activeElement.contentEditable === "true") { + return true; + } + if (activeElement.closest("form") || activeElement.classList.contains("cm-editor") || // CodeMirror editor + activeElement.classList.contains("monaco-editor") || // Monaco editor + activeElement.closest(".cm-editor") || activeElement.closest(".monaco-editor")) { + return true; + } + return false; } - -/** - * Check if two rectangles overlap. - * @param {{x: number, y: number, width: number, height: number}} rect1 - * @param {{x: number, y: number, width: number, height: number}} rect2 - * @returns {boolean} True if rectangles overlap - */ -export function rectsOverlap(rect1, rect2) { - return rect1.x < rect2.x + rect2.width && - rect1.x + rect1.width > rect2.x && - rect1.y < rect2.y + rect2.height && - rect1.y + rect1.height > rect2.y; +function rectsOverlap(rect1, rect2) { + return rect1.x < rect2.x + rect2.width && rect1.x + rect1.width > rect2.x && rect1.y < rect2.y + rect2.height && rect1.y + rect1.height > rect2.y; } - -/** - * Find the LiteGraph canvas element in the DOM. - * Tries multiple selectors for compatibility with different ComfyUI versions. - * @returns {HTMLCanvasElement|null} The canvas element or null if not found - */ -export function findLiteGraphCanvas() { - return document.getElementById("graph-canvas") || - document.querySelector("canvas.graphcanvas") || - app.canvas_manager?.container?.querySelector("canvas") || - (app.canvas && app.canvas.graph_canvas ? app.canvas.graph_canvas : null); -} - -/** - * Normalize a color value to include the # symbol for CSS. - * @param {string} color - The color value (with or without #) - * @returns {string} The normalized color with # prefix - */ -export function normalizeColor(color) { - if (!color) return color; - return color.startsWith('#') ? color : `#${color}`; -} - -/** - * Clamp a value between min and max bounds. - * @param {number} value - The value to clamp - * @param {number} min - Minimum bound - * @param {number} max - Maximum bound - * @returns {number} The clamped value - */ -export function clamp(value, min, max) { - return Math.min(Math.max(value, min), max); -} - -/** - * Create a debug logger that only logs when debug mode is enabled. - * @param {string} prefix - Prefix for log messages - * @param {function} isDebugEnabled - Function that returns whether debug is enabled - * @returns {{log: function, error: function}} Logger object - */ -export function createDebugLogger(prefix, isDebugEnabled) { - return { - log(...args) { - if (isDebugEnabled()) { - console.log(`${prefix}:`, ...args); - } - }, - error(...args) { - console.error(`${prefix} ERROR:`, ...args); - } - }; +function findLiteGraphCanvas() { + var _a, _b; + const canvas = document.getElementById("graph-canvas"); + if (canvas) return canvas; + const graphCanvas = document.querySelector("canvas.graphcanvas"); + if (graphCanvas) return graphCanvas; + const appAny = app; + if ((_a = appAny == null ? void 0 : appAny.canvas_manager) == null ? void 0 : _a.container) { + const managerCanvas = appAny.canvas_manager.container.querySelector("canvas"); + if (managerCanvas) return managerCanvas; + } + if ((_b = appAny == null ? void 0 : appAny.canvas) == null ? void 0 : _b.graph_canvas) { + return appAny.canvas.graph_canvas; + } + return null; } +export { + findLiteGraphCanvas, + getSettingValue, + isUserTyping, + rectsOverlap +}; +//# sourceMappingURL=utils.js.map diff --git a/web/shared/utils.js.map b/web/shared/utils.js.map new file mode 100644 index 0000000..ecd339a --- /dev/null +++ b/web/shared/utils.js.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.js","sources":["../../src/shared/utils.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Shared Utilities\n * \n * Common utility functions shared between magnify glass and info panel modules.\n */\n\nimport type { ComfyApp } from '../types/comfyui';\n\n// Reference to ComfyUI's app - will be set at runtime\n// @ts-ignore\nimport { app } from \"/scripts/app.js\";\n\n/**\n * Rectangle interface for overlap calculations\n */\nexport interface Rectangle {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Debug logger interface\n */\nexport interface DebugLogger {\n log(...args: unknown[]): void;\n error(...args: unknown[]): void;\n}\n\n/**\n * Safely get a setting value from ComfyUI settings with fallback to default.\n * @param key - The setting key\n * @param defaultValue - Default value if setting not found\n * @returns The setting value or default\n */\nexport function getSettingValue(key: string, defaultValue: T): T {\n try {\n const value = app.ui.settings.getSettingValue(key);\n return value === undefined ? defaultValue : value as T;\n } catch (e) {\n console.warn(`ComfyUI Magnifying Glass: Could not get setting ${key}, using default ${defaultValue}. Error: ${e}`);\n return defaultValue;\n }\n}\n\n/**\n * Check if the user is currently typing in an input field.\n * Used for smart input detection to avoid triggering hotkeys while typing.\n * @returns True if user is typing in an input field\n */\nexport function isUserTyping(): boolean {\n const activeElement = document.activeElement as HTMLElement | null;\n if (!activeElement) return false;\n\n // Check if the active element is an input, textarea, or contenteditable\n const tagName = activeElement.tagName.toLowerCase();\n if (tagName === 'input' || tagName === 'textarea') {\n return true;\n }\n\n // Check for contenteditable elements\n if (activeElement.contentEditable === 'true') {\n return true;\n }\n\n // Check if it's inside a form or has input-like classes\n if (activeElement.closest('form') ||\n activeElement.classList.contains('cm-editor') || // CodeMirror editor\n activeElement.classList.contains('monaco-editor') || // Monaco editor\n activeElement.closest('.cm-editor') ||\n activeElement.closest('.monaco-editor')) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if two rectangles overlap.\n * @param rect1 - First rectangle\n * @param rect2 - Second rectangle\n * @returns True if rectangles overlap\n */\nexport function rectsOverlap(rect1: Rectangle, rect2: Rectangle): boolean {\n return rect1.x < rect2.x + rect2.width &&\n rect1.x + rect1.width > rect2.x &&\n rect1.y < rect2.y + rect2.height &&\n rect1.y + rect1.height > rect2.y;\n}\n\n/**\n * Find the LiteGraph canvas element in the DOM.\n * Tries multiple selectors for compatibility with different ComfyUI versions.\n * @returns The canvas element or null if not found\n */\nexport function findLiteGraphCanvas(): HTMLCanvasElement | null {\n // Try multiple selectors for compatibility\n const canvas = document.getElementById(\"graph-canvas\") as HTMLCanvasElement | null;\n if (canvas) return canvas;\n\n const graphCanvas = document.querySelector(\"canvas.graphcanvas\") as HTMLCanvasElement | null;\n if (graphCanvas) return graphCanvas;\n\n // Try canvas manager (dynamic access for ComfyUI compatibility)\n const appAny = app as any;\n if (appAny?.canvas_manager?.container) {\n const managerCanvas = appAny.canvas_manager.container.querySelector(\"canvas\") as HTMLCanvasElement | null;\n if (managerCanvas) return managerCanvas;\n }\n\n // Try direct canvas reference\n if (appAny?.canvas?.graph_canvas) {\n return appAny.canvas.graph_canvas;\n }\n\n return null;\n}\n\n/**\n * Normalize a color value to include the # symbol for CSS.\n * @param color - The color value (with or without #)\n * @returns The normalized color with # prefix\n */\nexport function normalizeColor(color: string | null | undefined): string {\n if (!color) return color ?? '';\n return color.startsWith('#') ? color : `#${color}`;\n}\n\n/**\n * Clamp a value between min and max bounds.\n * @param value - The value to clamp\n * @param min - Minimum bound\n * @param max - Maximum bound\n * @returns The clamped value\n */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/**\n * Create a debug logger that only logs when debug mode is enabled.\n * @param prefix - Prefix for log messages\n * @param isDebugEnabled - Function that returns whether debug is enabled\n * @returns Logger object\n */\nexport function createDebugLogger(\n prefix: string,\n isDebugEnabled: () => boolean\n): DebugLogger {\n return {\n log(...args: unknown[]): void {\n if (isDebugEnabled()) {\n console.log(`${prefix}:`, ...args);\n }\n },\n error(...args: unknown[]): void {\n console.error(`${prefix} ERROR:`, ...args);\n }\n };\n}\n"],"names":[],"mappings":";AAoCO,SAAS,gBAAmB,KAAa,cAAoB;AAChE,MAAI;AACA,UAAM,QAAQ,IAAI,GAAG,SAAS,gBAAgB,GAAG;AACjD,WAAO,UAAU,SAAY,eAAe;AAAA,EAChD,SAAS,GAAG;AACR,YAAQ,KAAK,mDAAmD,GAAG,mBAAmB,YAAY,YAAY,CAAC,EAAE;AACjH,WAAO;AAAA,EACX;AACJ;AAOO,SAAS,eAAwB;AACpC,QAAM,gBAAgB,SAAS;AAC/B,MAAI,CAAC,cAAe,QAAO;AAG3B,QAAM,UAAU,cAAc,QAAQ,YAAA;AACtC,MAAI,YAAY,WAAW,YAAY,YAAY;AAC/C,WAAO;AAAA,EACX;AAGA,MAAI,cAAc,oBAAoB,QAAQ;AAC1C,WAAO;AAAA,EACX;AAGA,MAAI,cAAc,QAAQ,MAAM,KAC5B,cAAc,UAAU,SAAS,WAAW;AAAA,EAC5C,cAAc,UAAU,SAAS,eAAe;AAAA,EAChD,cAAc,QAAQ,YAAY,KAClC,cAAc,QAAQ,gBAAgB,GAAG;AACzC,WAAO;AAAA,EACX;AAEA,SAAO;AACX;AAQO,SAAS,aAAa,OAAkB,OAA2B;AACtE,SAAO,MAAM,IAAI,MAAM,IAAI,MAAM,SAC7B,MAAM,IAAI,MAAM,QAAQ,MAAM,KAC9B,MAAM,IAAI,MAAM,IAAI,MAAM,UAC1B,MAAM,IAAI,MAAM,SAAS,MAAM;AACvC;AAOO,SAAS,sBAAgD;;AAE5D,QAAM,SAAS,SAAS,eAAe,cAAc;AACrD,MAAI,OAAQ,QAAO;AAEnB,QAAM,cAAc,SAAS,cAAc,oBAAoB;AAC/D,MAAI,YAAa,QAAO;AAGxB,QAAM,SAAS;AACf,OAAI,sCAAQ,mBAAR,mBAAwB,WAAW;AACnC,UAAM,gBAAgB,OAAO,eAAe,UAAU,cAAc,QAAQ;AAC5E,QAAI,cAAe,QAAO;AAAA,EAC9B;AAGA,OAAI,sCAAQ,WAAR,mBAAgB,cAAc;AAC9B,WAAO,OAAO,OAAO;AAAA,EACzB;AAEA,SAAO;AACX;"} \ No newline at end of file diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..b65efdf --- /dev/null +++ b/web/style.css @@ -0,0 +1,80 @@ + +.mag-info-panel[data-v-687c6e82] { + position: fixed; + bottom: 20px; + left: 20px; + background: var(--comfy-menu-bg, #222); + color: var(--comfy-text-color, #fff); + border: 1px solid var(--border-color, #444); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + overflow: hidden; + font-family: sans-serif; + z-index: 1000; + pointer-events: auto; +} +.mag-panel-header[data-v-687c6e82] { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: rgba(255,255,255,0.05); + border-bottom: 1px solid rgba(255,255,255,0.1); + cursor: grab; +} +.mag-panel-header[data-v-687c6e82]:active { + cursor: grabbing; +} +.mag-panel-title[data-v-687c6e82] { + font-weight: bold; +} +.mag-panel-controls[data-v-687c6e82] { + display: flex; + gap: 4px; +} +.mag-panel-controls button[data-v-687c6e82] { + background: transparent; + border: none; + color: inherit; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; +} +.mag-panel-controls button[data-v-687c6e82]:hover { + background: rgba(255,255,255,0.1); +} +.mag-panel-controls button.active[data-v-687c6e82] { + background: rgba(59, 130, 246, 0.3); +} +.mag-panel-content[data-v-687c6e82] { + padding: 12px; + max-height: 400px; + overflow-y: auto; +} +.mag-node-info h3[data-v-687c6e82] { + margin-top: 0; + margin-bottom: 10px; + border-bottom: 1px solid rgba(255,255,255,0.1); + padding-bottom: 5px; +} +.mag-property-grid[data-v-687c6e82] { + display: grid; + gap: 4px; +} +.mag-prop-row[data-v-687c6e82] { + display: flex; + justify-content: space-between; +} +.mag-prop-label[data-v-687c6e82] { + opacity: 0.7; +} +.mag-empty-state[data-v-687c6e82] { + opacity: 0.6; + text-align: center; + padding: 20px; +} +.theme-light[data-v-687c6e82] { + background: #fff; + color: #333; + border-color: #ccc; +}