diff --git a/src/magnify-glass/MagnifyGlass.ts b/src/magnify-glass/MagnifyGlass.ts index 09305af..ec2cad6 100644 --- a/src/magnify-glass/MagnifyGlass.ts +++ b/src/magnify-glass/MagnifyGlass.ts @@ -47,6 +47,12 @@ export class MagnifyGlass { /** Current media element under cursor */ currentMediaElement: HTMLImageElement | HTMLVideoElement | null; + /** Animation loop ID for continuous video rendering */ + private animationLoopId: number | null; + + /** Whether videos are currently visible in magnified view */ + private hasVisibleVideos: boolean; + constructor() { this.config = new ConfigManager(); this.state = new MagnifierState(); @@ -74,6 +80,10 @@ export class MagnifyGlass { // Media tracking this.isOverMedia = false; this.currentMediaElement = null; + + // Animation loop for video rendering + this.animationLoopId = null; + this.hasVisibleVideos = false; } /** @@ -147,6 +157,8 @@ export class MagnifyGlass { if (this.eventHandler) { this.eventHandler.updateInitialPosition(); } + // Start continuous animation loop for smooth video playback + this.startAnimationLoop(); } } @@ -156,6 +168,8 @@ export class MagnifyGlass { */ private forceHideAllComponents(): void { this.state.active = false; + this.stopAnimationLoop(); + this.hasVisibleVideos = false; this.ui.hide(); // 1. Nuclear option: Direct DOM ID targeting @@ -370,6 +384,9 @@ export class MagnifyGlass { this.ui.htmlOverlayContainer.innerHTML = ''; + // Track if any videos are found for animation loop management + let foundVideos = false; + const magnifyRect: Rectangle = { x: this.state.sourceX, y: this.state.sourceY, @@ -425,7 +442,19 @@ export class MagnifyGlass { let isImageElement = false; let elementToProcess: HTMLElement | null = null; - if (widget.element) { + // Check for VHS-style widgets with videoEl property (VideoHelperSuite pattern) + const widgetName = String(widget.name || '').toLowerCase(); + if (widgetName === 'videopreview' || widgetName === 'audiopreview') { + const videoEl = widget.videoEl as HTMLVideoElement | undefined; + if (videoEl && !videoEl.hidden && videoEl.videoWidth > 0) { + isVideoElement = true; + foundVideos = true; + elementToProcess = videoEl; + } + } + + // Check for standard DOM element widgets (existing logic) + if (!elementToProcess && widget.element) { const element = widget.element as HTMLElement; // Skip text elements - now rendered natively on canvas via OffscreenRenderer if (widget.type === "text" || widget.type === "string" || element.tagName === 'TEXTAREA') { @@ -433,6 +462,7 @@ export class MagnifyGlass { // elementToProcess = element; } else if (element.tagName === 'VIDEO') { isVideoElement = true; + foundVideos = true; elementToProcess = element; } else if (element.tagName === 'IMG') { isImageElement = true; @@ -441,6 +471,7 @@ export class MagnifyGlass { const potentialVideo = element.querySelector('video'); if (potentialVideo) { isVideoElement = true; + foundVideos = true; elementToProcess = potentialVideo; } else { const potentialImage = element.querySelector('img'); @@ -581,6 +612,77 @@ export class MagnifyGlass { } } } + + // Manage animation loop based on video presence + if (foundVideos && !this.hasVisibleVideos) { + this.hasVisibleVideos = true; + this.startAnimationLoop(); + } else if (!foundVideos && this.hasVisibleVideos) { + this.hasVisibleVideos = false; + // Loop will stop itself on next iteration + } + } + + /** + * Start the animation loop for continuous video rendering. + * Only runs when videos are visible in the magnified view. + * This loop directly renders without going through updateMagnifiedView's + * scheduling logic to ensure continuous frame updates for video playback. + */ + private startAnimationLoop(): void { + if (this.animationLoopId !== null) return; // Already running + + const animate = () => { + // Stop if glass is deactivated or renderer unavailable + if (!this.state.active || !this.renderer || !this.litegraphCanvas) { + this.stopAnimationLoop(); + return; + } + + // Skip if glass preview is hidden + if (this.state.isPreviewHidden) { + this.animationLoopId = requestAnimationFrame(animate); + return; + } + + // Update canvas transformation and source region + this.updateCanvasTransformation(); + this.calculateSourceRegion(); + + // Direct render (bypass isRenderScheduled to ensure continuous updates) + let sourceCanvas: HTMLCanvasElement = this.litegraphCanvas; + if (this.offscreenRenderer && this.offscreenRenderer.isAvailable()) { + const highResCanvas = this.offscreenRenderer.renderHighResRegion(this.litegraphCanvas); + if (highResCanvas) { + sourceCanvas = highResCanvas; + } + } + + // Render the magnified view + this.renderer.render(sourceCanvas); + + // Render HTML overlays for video widgets + this.renderHtmlOverlays(); + + // Send frame to pop-out tab if open + if (this.popOutManager.isPopOutOpen() && this.ui.glassCanvas) { + this.popOutManager.sendFrame(this.ui.glassCanvas); + } + + this.animationLoopId = requestAnimationFrame(animate); + }; + + this.animationLoopId = requestAnimationFrame(animate); + } + + /** + * Stop the animation loop. + */ + private stopAnimationLoop(): void { + if (this.animationLoopId !== null) { + cancelAnimationFrame(this.animationLoopId); + this.animationLoopId = null; + } } /** @@ -666,6 +768,7 @@ export class MagnifyGlass { * Cleanup all resources. */ cleanup(): void { + this.stopAnimationLoop(); this.eventHandler.detachListeners(); this.popOutManager.cleanup(); this.ui.cleanup(); diff --git a/web/info-panel/StateManager.js b/web/info-panel/StateManager.js index 6d91f9d..d347387 100644 --- a/web/info-panel/StateManager.js +++ b/web/info-panel/StateManager.js @@ -1,7 +1,7 @@ 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 { getSettingValue, clamp } from "../shared/utils.js"; import { DEFAULT_PANEL_SETTINGS } from "../shared/settings/defaults.js"; import "/scripts/app.js"; class StateManager { @@ -187,13 +187,101 @@ class StateManager { } } loadSettings() { - Object.keys(DEFAULT_PANEL_SETTINGS).forEach((key) => { + const defaults = DEFAULT_PANEL_SETTINGS; + Object.keys(defaults).forEach((key) => { if (key !== "🔍MagnifyGlass.InfoPanelTheme") { - this.state.settings[key] = getSettingValue(key, DEFAULT_PANEL_SETTINGS[key]); + this.state.settings[key] = getSettingValue(key, defaults[key]); } }); + this.state.settings["🔍MagnifyGlass.InfoPanelWidth"] = this.validateNumber( + this.state.settings["🔍MagnifyGlass.InfoPanelWidth"], + 200, + 600, + defaults["🔍MagnifyGlass.InfoPanelWidth"] + ); + this.state.settings["🔍MagnifyGlass.InfoPanelMaxHeight"] = this.validateNumber( + this.state.settings["🔍MagnifyGlass.InfoPanelMaxHeight"], + 200, + 1500, + defaults["🔍MagnifyGlass.InfoPanelMaxHeight"] + ); + this.state.settings["🔍MagnifyGlass.InfoPanelOpacity"] = this.validateNumber( + this.state.settings["🔍MagnifyGlass.InfoPanelOpacity"], + 10, + 100, + defaults["🔍MagnifyGlass.InfoPanelOpacity"] + ); + this.state.settings["🔍MagnifyGlass.InfoPanelFontSize"] = this.validateNumber( + this.state.settings["🔍MagnifyGlass.InfoPanelFontSize"], + 8, + 24, + defaults["🔍MagnifyGlass.InfoPanelFontSize"] + ); + this.state.settings["🔍MagnifyGlass.InfoPanelTextColor"] = this.validateColor( + this.state.settings["🔍MagnifyGlass.InfoPanelTextColor"], + defaults["🔍MagnifyGlass.InfoPanelTextColor"] + ); + this.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"] = this.validateColor( + this.state.settings["🔍MagnifyGlass.InfoPanelAccentColor"], + defaults["🔍MagnifyGlass.InfoPanelAccentColor"] + ); + const validPanelPositions = ["Bottom", "Top", "Left", "Right"]; + this.state.settings["🔍MagnifyGlass.InfoPanelPosition"] = this.validateStringOption( + this.state.settings["🔍MagnifyGlass.InfoPanelPosition"], + validPanelPositions, + defaults["🔍MagnifyGlass.InfoPanelPosition"] + ); + const validControlPositions = ["top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right"]; + this.state.settings["🔍MagnifyGlass.ControlsPosition"] = this.validateStringOption( + this.state.settings["🔍MagnifyGlass.ControlsPosition"], + validControlPositions, + defaults["🔍MagnifyGlass.ControlsPosition"] + ); + const validFonts = [ + "System Default", + "Inter", + "Roboto", + "JetBrains Mono", + "Fira Code", + "IBM Plex Sans", + "Space Grotesk", + "Lexend", + "Outfit", + "monospace", + "system-ui" + ]; + this.state.settings["🔍MagnifyGlass.InfoPanelFontFamily"] = this.validateStringOption( + this.state.settings["🔍MagnifyGlass.InfoPanelFontFamily"], + validFonts, + defaults["🔍MagnifyGlass.InfoPanelFontFamily"] + ); this.state.settings["🔍MagnifyGlass.InfoPanelTheme"] = this.state.currentTheme; } + /** + * Validate numeric input with bounds checking. + */ + validateNumber(value, min, max, fallback) { + const num = Number(value); + if (isNaN(num)) return fallback; + return clamp(num, min, max); + } + /** + * Validate color string (hex). + */ + validateColor(color, fallback) { + if (!color || typeof color !== "string") return fallback; + if (/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(color)) { + return color; + } + return fallback; + } + /** + * Validate string against an allowlist. + */ + validateStringOption(value, options, fallback) { + if (typeof value !== "string") return fallback; + return options.includes(value) ? value : fallback; + } updateSettings() { const oldSettings = { ...this.state.settings }; this.loadSettings(); diff --git a/web/info-panel/StateManager.js.map b/web/info-panel/StateManager.js.map index 9f1722f..00def3a 100644 --- a/web/info-panel/StateManager.js.map +++ b/web/info-panel/StateManager.js.map @@ -1 +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 type { GatheredInfo, SettingsChange } from '../types/comfyui';\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 wasPanelVisibleBeforeHide: boolean; // Persist state across glass toggles\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 // Info freezing\n isInfoHeld: 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: GatheredInfo | null;\n\n // Node selection override (user-selected node from dropdown)\n selectedNodeId: number | null;\n\n // Settings cache - using index signature for dynamic ComfyUI settings\n settings: { [key: string]: string | number | boolean };\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 wasPanelVisibleBeforeHide: 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 isInfoHeld: false,\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: null,\n\n // Node selection override\n selectedNodeId: null,\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 // Method 1: Try to read from ComfyUI's color palette setting\n try {\n // ComfyUI stores theme in 'Comfy.ColorPalette' setting\n const colorPalette = getSettingValue('Comfy.ColorPalette', '');\n\n if (colorPalette) {\n // Extract theme name from various possible formats\n const paletteStr = String(colorPalette).toLowerCase();\n\n if (paletteStr.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (paletteStr.includes('arc')) {\n detectedTheme = 'arc';\n } else if (paletteStr.includes('nord')) {\n detectedTheme = 'nord';\n } else if (paletteStr.includes('github')) {\n detectedTheme = 'github';\n } else if (paletteStr.includes('light')) {\n detectedTheme = 'light';\n } else if (paletteStr.includes('dark')) {\n detectedTheme = 'dark';\n }\n }\n } catch (e) {\n // Fallback to brightness-based detection\n console.warn('ComfyUI Info Panel: Could not read theme from settings, using fallback detection');\n }\n\n // Method 2: Fallback - check body/html for theme classes or data attributes\n if (detectedTheme === 'dark') {\n const body = document.body;\n const html = document.documentElement;\n\n // Check for data-theme attribute\n const dataTheme = body.getAttribute('data-theme') || html.getAttribute('data-theme');\n if (dataTheme) {\n detectedTheme = dataTheme.toLowerCase();\n }\n\n // Check for theme classes\n const classes = (body.className + ' ' + html.className).toLowerCase();\n if (classes.includes('theme-solarized') || classes.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (classes.includes('theme-arc') || classes.includes('arc-theme')) {\n detectedTheme = 'arc';\n } else if (classes.includes('theme-nord') || classes.includes('nord-theme')) {\n detectedTheme = 'nord';\n } else if (classes.includes('theme-github') || classes.includes('github-theme')) {\n detectedTheme = 'github';\n } else if (classes.includes('theme-light') || classes.includes('light-theme')) {\n detectedTheme = 'light';\n } else if (classes.includes('theme-dracula') || classes.includes('dracula-theme')) {\n detectedTheme = 'dracula';\n } else if (classes.includes('theme-gruvbox') || classes.includes('gruvbox-theme')) {\n detectedTheme = 'gruvbox';\n } else if (classes.includes('theme-monokai') || classes.includes('monokai-theme')) {\n detectedTheme = 'monokai';\n }\n }\n\n // Method 3: Last resort - brightness detection for light vs dark\n if (detectedTheme === 'dark') {\n const bodyStyles = window.getComputedStyle(document.body);\n const bgColor = bodyStyles.backgroundColor;\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 > 180) {\n detectedTheme = 'light';\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 (typeof target.className === 'string' && 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 // Notify Pop-Out Manager (if it exists on the main glass instance)\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass && magnifyGlass.popOutManager) {\n magnifyGlass.popOutManager.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(): Record {\n const oldSettings = { ...this.state.settings };\n this.loadSettings();\n\n // Return what changed for reactive updates\n const changes: Record = {};\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 } else {\n // When manually pinning, also clear auto-pin flag (this is a user-initiated pin)\n this.state.isAutoPinned = false;\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 toggleHold(): boolean {\n this.state.isInfoHeld = !this.state.isInfoHeld;\n return this.state.isInfoHeld;\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: GatheredInfo): void {\n this.state.currentInfo = info;\n }\n\n /**\n * Set the selected node ID for manual override.\n * When set, the inspector will display this node instead of the hovered node.\n */\n setSelectedNode(id: number | null): void {\n this.state.selectedNodeId = id;\n }\n\n /**\n * Clear the selected node, returning to hover-based detection.\n */\n clearSelectedNode(): void {\n this.state.selectedNodeId = null;\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 +{"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 type { GatheredInfo, SettingsChange } from '../types/comfyui';\nimport { getSettingValue, clamp } from '../shared/utils';\nimport { DEFAULT_PANEL_SETTINGS } from '../shared/settings';\n\nexport interface InfoPanelStateData {\n // Panel visibility and positioning\n isPanelVisible: boolean;\n wasPanelVisibleBeforeHide: boolean; // Persist state across glass toggles\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 // Info freezing\n isInfoHeld: 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: GatheredInfo | null;\n\n // Node selection override (user-selected node from dropdown)\n selectedNodeId: number | null;\n\n // Settings cache - using index signature for dynamic ComfyUI settings\n settings: { [key: string]: string | number | boolean };\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 wasPanelVisibleBeforeHide: 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 isInfoHeld: false,\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: null,\n\n // Node selection override\n selectedNodeId: null,\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 // Method 1: Try to read from ComfyUI's color palette setting\n try {\n // ComfyUI stores theme in 'Comfy.ColorPalette' setting\n const colorPalette = getSettingValue('Comfy.ColorPalette', '');\n\n if (colorPalette) {\n // Extract theme name from various possible formats\n const paletteStr = String(colorPalette).toLowerCase();\n\n if (paletteStr.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (paletteStr.includes('arc')) {\n detectedTheme = 'arc';\n } else if (paletteStr.includes('nord')) {\n detectedTheme = 'nord';\n } else if (paletteStr.includes('github')) {\n detectedTheme = 'github';\n } else if (paletteStr.includes('light')) {\n detectedTheme = 'light';\n } else if (paletteStr.includes('dark')) {\n detectedTheme = 'dark';\n }\n }\n } catch (e) {\n // Fallback to brightness-based detection\n console.warn('ComfyUI Info Panel: Could not read theme from settings, using fallback detection');\n }\n\n // Method 2: Fallback - check body/html for theme classes or data attributes\n if (detectedTheme === 'dark') {\n const body = document.body;\n const html = document.documentElement;\n\n // Check for data-theme attribute\n const dataTheme = body.getAttribute('data-theme') || html.getAttribute('data-theme');\n if (dataTheme) {\n detectedTheme = dataTheme.toLowerCase();\n }\n\n // Check for theme classes\n const classes = (body.className + ' ' + html.className).toLowerCase();\n if (classes.includes('theme-solarized') || classes.includes('solarized')) {\n detectedTheme = 'solarized';\n } else if (classes.includes('theme-arc') || classes.includes('arc-theme')) {\n detectedTheme = 'arc';\n } else if (classes.includes('theme-nord') || classes.includes('nord-theme')) {\n detectedTheme = 'nord';\n } else if (classes.includes('theme-github') || classes.includes('github-theme')) {\n detectedTheme = 'github';\n } else if (classes.includes('theme-light') || classes.includes('light-theme')) {\n detectedTheme = 'light';\n } else if (classes.includes('theme-dracula') || classes.includes('dracula-theme')) {\n detectedTheme = 'dracula';\n } else if (classes.includes('theme-gruvbox') || classes.includes('gruvbox-theme')) {\n detectedTheme = 'gruvbox';\n } else if (classes.includes('theme-monokai') || classes.includes('monokai-theme')) {\n detectedTheme = 'monokai';\n }\n }\n\n // Method 3: Last resort - brightness detection for light vs dark\n if (detectedTheme === 'dark') {\n const bodyStyles = window.getComputedStyle(document.body);\n const bgColor = bodyStyles.backgroundColor;\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 > 180) {\n detectedTheme = 'light';\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 (typeof target.className === 'string' && 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 // Notify Pop-Out Manager (if it exists on the main glass instance)\n const magnifyGlass = (window as any).comfyUIMagnifyGlass;\n if (magnifyGlass && magnifyGlass.popOutManager) {\n magnifyGlass.popOutManager.updateTheme(this.state.currentTheme);\n }\n }\n\n loadSettings(): void {\n const defaults = DEFAULT_PANEL_SETTINGS;\n\n Object.keys(defaults).forEach(key => {\n // Skip the theme setting since we auto-detect it\n if (key !== \"🔍MagnifyGlass.InfoPanelTheme\") {\n this.state.settings[key] = getSettingValue(key, (defaults as any)[key]);\n }\n });\n\n // Validate numeric settings\n this.state.settings[\"🔍MagnifyGlass.InfoPanelWidth\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelWidth\"], 200, 600, defaults[\"🔍MagnifyGlass.InfoPanelWidth\"] as number\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelMaxHeight\"], 200, 1500, defaults[\"🔍MagnifyGlass.InfoPanelMaxHeight\"] as number\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelOpacity\"], 10, 100, defaults[\"🔍MagnifyGlass.InfoPanelOpacity\"] as number\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontSize\"] = this.validateNumber(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontSize\"], 8, 24, defaults[\"🔍MagnifyGlass.InfoPanelFontSize\"] as number\n );\n\n // Validate colors\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTextColor\"] = this.validateColor(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTextColor\"], defaults[\"🔍MagnifyGlass.InfoPanelTextColor\"] as string\n );\n\n this.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"] = this.validateColor(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelAccentColor\"], defaults[\"🔍MagnifyGlass.InfoPanelAccentColor\"] as string\n );\n\n // Validate Enums\n const validPanelPositions = ['Bottom', 'Top', 'Left', 'Right'];\n this.state.settings[\"🔍MagnifyGlass.InfoPanelPosition\"] = this.validateStringOption(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelPosition\"], validPanelPositions, defaults[\"🔍MagnifyGlass.InfoPanelPosition\"] as string\n );\n\n const validControlPositions = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top', 'bottom', 'left', 'right'];\n this.state.settings[\"🔍MagnifyGlass.ControlsPosition\"] = this.validateStringOption(\n this.state.settings[\"🔍MagnifyGlass.ControlsPosition\"], validControlPositions, defaults[\"🔍MagnifyGlass.ControlsPosition\"] as string\n );\n\n const validFonts = [\n 'System Default', 'Inter', 'Roboto', 'JetBrains Mono', 'Fira Code',\n 'IBM Plex Sans', 'Space Grotesk', 'Lexend', 'Outfit', 'monospace', 'system-ui'\n ];\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontFamily\"] = this.validateStringOption(\n this.state.settings[\"🔍MagnifyGlass.InfoPanelFontFamily\"], validFonts, defaults[\"🔍MagnifyGlass.InfoPanelFontFamily\"] as string\n );\n\n // Set theme to auto-detected value\n this.state.settings[\"🔍MagnifyGlass.InfoPanelTheme\"] = this.state.currentTheme;\n }\n\n /**\n * Validate numeric input with bounds checking.\n */\n private validateNumber(value: unknown, min: number, max: number, fallback: number): number {\n const num = Number(value);\n if (isNaN(num)) return fallback;\n return clamp(num, min, max);\n }\n\n /**\n * Validate color string (hex).\n */\n private validateColor(color: unknown, fallback: string): string {\n if (!color || typeof color !== 'string') return fallback;\n // Basic hex validation: #RGB, #RRGGBB, #RRGGBBAA\n if (/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(color)) {\n return color;\n }\n return fallback;\n }\n\n /**\n * Validate string against an allowlist.\n */\n private validateStringOption(value: unknown, options: string[], fallback: string): string {\n if (typeof value !== 'string') return fallback;\n return options.includes(value) ? value : fallback;\n }\n\n updateSettings(): Record {\n const oldSettings = { ...this.state.settings };\n this.loadSettings();\n\n // Return what changed for reactive updates\n const changes: Record = {};\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 } else {\n // When manually pinning, also clear auto-pin flag (this is a user-initiated pin)\n this.state.isAutoPinned = false;\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 toggleHold(): boolean {\n this.state.isInfoHeld = !this.state.isInfoHeld;\n return this.state.isInfoHeld;\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: GatheredInfo): void {\n this.state.currentInfo = info;\n }\n\n /**\n * Set the selected node ID for manual override.\n * When set, the inspector will display this node instead of the hovered node.\n */\n setSelectedNode(id: number | null): void {\n this.state.selectedNodeId = id;\n }\n\n /**\n * Clear the selected node, returning to hover-based detection.\n */\n clearSelectedNode(): void {\n this.state.selectedNodeId = null;\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/magnify-glass/MagnifyGlass.js b/web/magnify-glass/MagnifyGlass.js index 9729692..95ca290 100644 --- a/web/magnify-glass/MagnifyGlass.js +++ b/web/magnify-glass/MagnifyGlass.js @@ -32,6 +32,10 @@ class MagnifyGlass { __publicField(this, "isOverMedia"); /** Current media element under cursor */ __publicField(this, "currentMediaElement"); + /** Animation loop ID for continuous video rendering */ + __publicField(this, "animationLoopId"); + /** Whether videos are currently visible in magnified view */ + __publicField(this, "hasVisibleVideos"); this.config = new ConfigManager(); this.state = new MagnifierState(); this.popOutManager = new PopOutManager(); @@ -48,6 +52,8 @@ class MagnifyGlass { this.lastKnownMousePosition = { x: 0, y: 0 }; this.isOverMedia = false; this.currentMediaElement = null; + this.animationLoopId = null; + this.hasVisibleVideos = false; } /** * Initialize the magnifying glass. @@ -93,6 +99,7 @@ class MagnifyGlass { if (this.eventHandler) { this.eventHandler.updateInitialPosition(); } + this.startAnimationLoop(); } } /** @@ -101,6 +108,8 @@ class MagnifyGlass { */ forceHideAllComponents() { this.state.active = false; + this.stopAnimationLoop(); + this.hasVisibleVideos = false; this.ui.hide(); const panelEl = document.getElementById(INFO_PANEL_ID); if (panelEl) { @@ -247,6 +256,7 @@ class MagnifyGlass { return; } this.ui.htmlOverlayContainer.innerHTML = ""; + let foundVideos = false; const magnifyRect = { x: this.state.sourceX, y: this.state.sourceY, @@ -290,11 +300,21 @@ class MagnifyGlass { let isVideoElement = false; let isImageElement = false; let elementToProcess = null; - if (widget.element) { + const widgetName = String(widget.name || "").toLowerCase(); + if (widgetName === "videopreview" || widgetName === "audiopreview") { + const videoEl = widget.videoEl; + if (videoEl && !videoEl.hidden && videoEl.videoWidth > 0) { + isVideoElement = true; + foundVideos = true; + elementToProcess = videoEl; + } + } + if (!elementToProcess && widget.element) { const element = widget.element; if (widget.type === "text" || widget.type === "string" || element.tagName === "TEXTAREA") ; else if (element.tagName === "VIDEO") { isVideoElement = true; + foundVideos = true; elementToProcess = element; } else if (element.tagName === "IMG") { isImageElement = true; @@ -303,6 +323,7 @@ class MagnifyGlass { const potentialVideo = element.querySelector("video"); if (potentialVideo) { isVideoElement = true; + foundVideos = true; elementToProcess = potentialVideo; } else { const potentialImage = element.querySelector("img"); @@ -315,16 +336,15 @@ class MagnifyGlass { } if (elementToProcess && (isVideoElement || isImageElement)) { const widgetRect = elementToProcess.getBoundingClientRect(); - const canvasRect = this.litegraphCanvas.getBoundingClientRect(); - const dpr2 = canvasRect.width > 0 ? this.litegraphCanvas.width / canvasRect.width : 1; + const canvasRect = rect; const currentScale = this.state.canvasScale; const isVirtualZoomMode = currentScale < 0.7; const widgetCssX = widgetRect.left - canvasRect.left; const widgetCssY = widgetRect.top - canvasRect.top; const widgetCssWidth = widgetRect.width; const widgetCssHeight = widgetRect.height; - const pivotCssX = this.state.x / dpr2; - const pivotCssY = this.state.y / dpr2; + const pivotCssX = this.state.x / dpr; + const pivotCssY = this.state.y / dpr; let finalWidgetCssX; let finalWidgetCssY; let finalWidgetCssWidth; @@ -340,10 +360,10 @@ class MagnifyGlass { finalWidgetCssWidth = widgetCssWidth; finalWidgetCssHeight = widgetCssHeight; } - const widgetCanvasX = finalWidgetCssX * dpr2; - const widgetCanvasY = finalWidgetCssY * dpr2; - const widgetCanvasWidth = finalWidgetCssWidth * dpr2; - const widgetCanvasHeight = finalWidgetCssHeight * dpr2; + const widgetCanvasX = finalWidgetCssX * dpr; + const widgetCanvasY = finalWidgetCssY * dpr; + const widgetCanvasWidth = finalWidgetCssWidth * dpr; + const widgetCanvasHeight = finalWidgetCssHeight * dpr; const widgetSourceRect = { x: widgetCanvasX, y: widgetCanvasY, @@ -392,6 +412,56 @@ class MagnifyGlass { } } } + if (foundVideos && !this.hasVisibleVideos) { + this.hasVisibleVideos = true; + this.startAnimationLoop(); + } else if (!foundVideos && this.hasVisibleVideos) { + this.hasVisibleVideos = false; + } + } + /** + * Start the animation loop for continuous video rendering. + * Only runs when videos are visible in the magnified view. + * This loop directly renders without going through updateMagnifiedView's + * scheduling logic to ensure continuous frame updates for video playback. + */ + startAnimationLoop() { + if (this.animationLoopId !== null) return; + const animate = () => { + if (!this.state.active || !this.renderer || !this.litegraphCanvas) { + this.stopAnimationLoop(); + return; + } + if (this.state.isPreviewHidden) { + this.animationLoopId = requestAnimationFrame(animate); + return; + } + this.updateCanvasTransformation(); + this.calculateSourceRegion(); + let sourceCanvas = this.litegraphCanvas; + if (this.offscreenRenderer && this.offscreenRenderer.isAvailable()) { + const highResCanvas = this.offscreenRenderer.renderHighResRegion(this.litegraphCanvas); + if (highResCanvas) { + sourceCanvas = highResCanvas; + } + } + this.renderer.render(sourceCanvas); + this.renderHtmlOverlays(); + if (this.popOutManager.isPopOutOpen() && this.ui.glassCanvas) { + this.popOutManager.sendFrame(this.ui.glassCanvas); + } + this.animationLoopId = requestAnimationFrame(animate); + }; + this.animationLoopId = requestAnimationFrame(animate); + } + /** + * Stop the animation loop. + */ + stopAnimationLoop() { + if (this.animationLoopId !== null) { + cancelAnimationFrame(this.animationLoopId); + this.animationLoopId = null; + } } /** * Update config from current settings values. @@ -457,6 +527,7 @@ class MagnifyGlass { * Cleanup all resources. */ cleanup() { + this.stopAnimationLoop(); this.eventHandler.detachListeners(); this.popOutManager.cleanup(); this.ui.cleanup(); diff --git a/web/magnify-glass/MagnifyGlass.js.map b/web/magnify-glass/MagnifyGlass.js.map index 0becd73..74f4b68 100644 --- a/web/magnify-glass/MagnifyGlass.js.map +++ b/web/magnify-glass/MagnifyGlass.js.map @@ -1 +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, DEFAULT_GLASS_Y_OFFSET, INFO_PANEL_ID } from '../shared/constants';\nimport { registerGlassSettings, registerAccessibilitySettings } 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';\nimport { PopOutManager } from './PopOutManager';\nimport { OffscreenRenderer } from './OffscreenRenderer';\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 popOutManager: PopOutManager;\n offscreenRenderer: OffscreenRenderer | null;\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.popOutManager = new PopOutManager();\n this.ui = new UiManager(\n this.config,\n this.state,\n () => this.toggle()\n );\n\n // onStateChange is handled by InfoPanel\n\n\n this.renderer = null;\n this.offscreenRenderer = 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\n\n // Load saved offsets first\n this.config.loadSavedOffsets();\n\n // Print detailed canvas information\n\n\n // Create UI elements\n this.ui.createElements();\n\n // Ensure glass is validly positioned (within viewport) right away\n // This handles cases where config has saved positions from a different monitor size\n this.ui.updateResponsivePosition();\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 // Initialize offscreen renderer for high-res magnification\n this.offscreenRenderer = new OffscreenRenderer(this.config, this.state);\n\n // Attach event handlers\n this.eventHandler.attachListeners();\n\n // Register settings with ComfyUI\n registerGlassSettings(this);\n registerAccessibilitySettings(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 * Toggle the magnifying glass active state.\n */\n toggle(): void {\n const state = this.state;\n\n if (state.active) {\n // TURNING OFF - Force hide everything\n this.forceHideAllComponents();\n } else {\n // TURNING ON\n state.active = true;\n this.ui.show();\n if (this.eventHandler) {\n this.eventHandler.updateInitialPosition();\n }\n }\n }\n\n /**\n * Force hide all components including extensions (Info Panel, etc).\n * This overcomes the \"pinned\" state of the info panel when the main tool is deactivated.\n */\n private forceHideAllComponents(): void {\n this.state.active = false;\n this.ui.hide();\n\n // 1. Nuclear option: Direct DOM ID targeting\n // This is necessary because in some cases object references might be stale or logic might prevent hiding\n const panelEl = document.getElementById(INFO_PANEL_ID);\n if (panelEl) {\n panelEl.style.display = 'none';\n // Also force opacity 0 just in case transition handles display\n panelEl.style.opacity = '0';\n }\n\n // Iterate through all registered extensions and hide them if they have a hide method\n const extensions = window.comfyUIMagnifyGlassExtensions;\n if (extensions && extensions.length > 0) {\n extensions.forEach((extension: any) => {\n // Check basically any likely property for a UI manager or direct hide method\n if (extension && extension.uiManager && typeof extension.uiManager.hide === 'function') {\n extension.uiManager.hide();\n // Explicitly force display none if available to be extra sure\n if (extension.uiManager.elements && extension.uiManager.elements.panel) {\n extension.uiManager.elements.panel.style.display = 'none';\n }\n }\n });\n }\n }\n\n /**\n * Set glass preview visibility WITHOUT affecting info panel.\n * This is used by the toggle-glass hover control button.\n * When hidden, the renderer stops completely to save performance.\n * When shown, the renderer resumes.\n * \n * @param visible - true to show and enable rendering, false to hide and disable\n */\n setGlassPreviewActive(visible: boolean): void {\n // Track this state internally\n this.state.isPreviewHidden = !visible;\n\n if (visible) {\n // Show the glass preview\n this.ui.setPreviewVisibility(true);\n // Resume rendering on next mouse move\n } else {\n // Hide the glass preview visually\n this.ui.setPreviewVisibility(false);\n // The rendering will be skipped in updateMagnifiedView due to isPreviewHidden check\n }\n }\n\n /**\n * Check if the glass preview is visible.\n * Returns false if hidden via hover controls.\n */\n private isGlassPreviewVisible(): boolean {\n // Check info panel extension state for isGlassPreviewVisible\n const extensions = (window as any).comfyUIMagnifyGlassExtensions;\n if (extensions && extensions.length > 0) {\n const infoPanel = extensions[0];\n if (infoPanel?.stateManager?.state) {\n return infoPanel.stateManager.state.isGlassPreviewVisible !== false;\n }\n }\n // Default to visible if we can't find the state\n return true;\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 // Skip rendering if glass preview is hidden via hover controls\n // This prevents double FPS when only the info panel is active\n if (this.state.isPreviewHidden) {\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\n // Try virtual zoom capture for high-res magnification\n // This temporarily sets LiteGraph to 100% zoom, captures clean pixels,\n // then restores the original zoom level\n let sourceCanvas: HTMLCanvasElement = this.litegraphCanvas;\n if (this.offscreenRenderer && this.offscreenRenderer.isAvailable()) {\n const highResCanvas = this.offscreenRenderer.renderHighResRegion(this.litegraphCanvas);\n if (highResCanvas) {\n sourceCanvas = highResCanvas;\n }\n }\n\n // Render the magnified view\n this.renderer.render(sourceCanvas);\n\n // Update debug visualization\n\n\n // Render HTML overlays for text, video, and image widgets\n this.renderHtmlOverlays();\n\n // Send frame to pop-out tab if open\n if (this.popOutManager.isPopOutOpen() && this.ui.glassCanvas) {\n this.popOutManager.sendFrame(this.ui.glassCanvas);\n }\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 if (!this.litegraphCanvas) return;\n\n // Get DPR (Device Pixel Ratio) - relationship between Backing Store pixels and CSS pixels\n const rect = this.litegraphCanvas.getBoundingClientRect();\n const dpr = rect.width > 0 ? this.litegraphCanvas.width / rect.width : 1;\n\n // Convert cursor Backing pixels to CSS pixels (Screen pixels)\n // state.x/y are in Backing Pixels (set in EventHandler)\n const cursorCssX = cursorPixelX / dpr;\n const cursorCssY = cursorPixelY / dpr;\n\n // Convert cursor CSS pixels to LiteGraph graph coordinates\n // ds.offset and ds.scale operate in CSS pixel space\n const cursorGraphX = (cursorCssX - canvasOffsetX) / canvasScale;\n const cursorGraphY = (cursorCssY - canvasOffsetY) / canvasScale;\n\n // Apply manual offset (in Graph Units - arbitrary units)\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 Backing Pixels for the source rectangle\n // 1. Convert Graph -> CSS: (Graph * Scale) + Offset\n // 2. Convert CSS -> Backing: CSS * DPR\n const sourceCssX = (sourceGraphX * canvasScale) + canvasOffsetX;\n const sourceCssY = (sourceGraphY * canvasScale) + canvasOffsetY;\n const sourceCssWidth = sourceGraphWidth * canvasScale;\n const sourceCssHeight = sourceGraphHeight * canvasScale;\n\n this.state.sourceX = sourceCssX * dpr;\n this.state.sourceY = sourceCssY * dpr;\n this.state.sourceWidth = sourceCssWidth * dpr;\n this.state.sourceHeight = sourceCssHeight * dpr;\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 // Calculate source region in graph coordinates for culling\n // This mirrors calculateSourceRegion() but stops at graph coords\n if (this.state.canvasScale === 0) return;\n\n const rect = this.litegraphCanvas.getBoundingClientRect();\n const dpr = rect.width > 0 ? this.litegraphCanvas.width / rect.width : 1;\n const cursorCssX = this.state.x / dpr;\n const cursorCssY = this.state.y / dpr;\n const cursorGraphX = (cursorCssX - this.state.canvasOffsetX) / this.state.canvasScale;\n const cursorGraphY = (cursorCssY - this.state.canvasOffsetY) / this.state.canvasScale;\n const targetGraphCenterX = cursorGraphX + this.config.offsetX;\n const targetGraphCenterY = cursorGraphY + this.config.offsetY;\n const sourceGraphWidth = (this.config.glassSize / this.config.zoomFactor) / this.state.canvasScale;\n const sourceGraphHeight = (this.config.glassSize / this.config.zoomFactor) / this.state.canvasScale;\n\n const sourceGraphRect = {\n x: targetGraphCenterX - (sourceGraphWidth / 2),\n y: targetGraphCenterY - (sourceGraphHeight / 2),\n width: sourceGraphWidth,\n height: sourceGraphHeight\n };\n\n for (const node of nodes) {\n // Optimization: Cull nodes that are not in the source region\n if (node.pos && node.size) {\n const nodeRect = {\n x: node.pos[0],\n y: node.pos[1],\n width: node.size[0],\n height: node.size[1]\n };\n if (!rectsOverlap(sourceGraphRect, nodeRect)) {\n continue;\n }\n }\n\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 // Skip text elements - now rendered natively on canvas via OffscreenRenderer\n if (widget.type === \"text\" || widget.type === \"string\" || element.tagName === 'TEXTAREA') {\n // isTextElement = true; // DISABLED - using native canvas rendering\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 // DPR = backing pixels / CSS pixels\n const dpr = canvasRect.width > 0 ? (this.litegraphCanvas!.width / canvasRect.width) : 1;\n const currentScale = this.state.canvasScale;\n const isVirtualZoomMode = currentScale < 0.7;\n\n // Widget position in CSS coordinates relative to canvas\n const widgetCssX = widgetRect.left - canvasRect.left;\n const widgetCssY = widgetRect.top - canvasRect.top;\n const widgetCssWidth = widgetRect.width;\n const widgetCssHeight = widgetRect.height;\n\n // Pivot point (mouse position) in CSS coordinates\n const pivotCssX = this.state.x / dpr;\n const pivotCssY = this.state.y / dpr;\n\n let finalWidgetCssX: number;\n let finalWidgetCssY: number;\n let finalWidgetCssWidth: number;\n let finalWidgetCssHeight: number;\n\n if (isVirtualZoomMode) {\n // Virtual Zoom: Transform widget position to match virtual 1.0 scale capture\n // Formula: virtualCss = (widgetCss - pivotCss) / currentScale + pivotCss\n finalWidgetCssX = (widgetCssX - pivotCssX) / currentScale + pivotCssX;\n finalWidgetCssY = (widgetCssY - pivotCssY) / currentScale + pivotCssY;\n // Size also scales inversely\n finalWidgetCssWidth = widgetCssWidth / currentScale;\n finalWidgetCssHeight = widgetCssHeight / currentScale;\n } else {\n // Direct Capture: Use actual positions\n finalWidgetCssX = widgetCssX;\n finalWidgetCssY = widgetCssY;\n finalWidgetCssWidth = widgetCssWidth;\n finalWidgetCssHeight = widgetCssHeight;\n }\n\n // Convert to backing pixels for comparison with magnifyRect\n const widgetCanvasX = finalWidgetCssX * dpr;\n const widgetCanvasY = finalWidgetCssY * dpr;\n const widgetCanvasWidth = finalWidgetCssWidth * dpr;\n const widgetCanvasHeight = finalWidgetCssHeight * dpr;\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 // Hide scrollbar while keeping content visible\n clonedElement.style.overflow = 'hidden';\n (clonedElement as HTMLInputElement).disabled = true;\n\n // Dual adaptive font sizing:\n // - When zoomed OUT (below 50%): shrink text to fit all content\n // - When zoomed IN (above 50%): grow text for better readability\n const canvasScale = this.state.canvasScale;\n const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize);\n const threshold = 0.5; // 50% zoom threshold\n\n let adaptedFontSize: number;\n if (canvasScale < threshold) {\n // Zoomed out: shrink text aggressively to fit content\n // Scale from threshold down to minimum\n const shrinkFactor = canvasScale / threshold;\n adaptedFontSize = Math.max(6, originalFontSize * shrinkFactor * 0.8);\n } else {\n // Zoomed in: grow text for readability\n // Scale from 100% at threshold up to 150% at 100%+ zoom\n const growFactor = 1 + ((canvasScale - threshold) / (1 - threshold)) * 0.5;\n adaptedFontSize = originalFontSize * Math.min(1.5, growFactor);\n }\n\n clonedElement.style.fontSize = `${adaptedFontSize}px`;\n clonedElement.style.lineHeight = canvasScale < threshold ? '1.2' : '1.4';\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 => {\n if (e.name !== 'AbortError') {\n console.warn(\"Magnify Glass: Cloned video play failed\", e);\n }\n });\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 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\n /**\n * Reset offsets and panel positions to defaults.\n */\n resetOffsets(): void {\n this.config.resetOffsets();\n\n // Also disable follow cursor\n this.config.followCursor = false;\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 // If it's the first activation, position at top right of the window (Anchored)\n // Otherwise, reset to the last known position or default if not active.\n if (!this.state.wasActivatedBefore) {\n const padding = DEFAULT_PADDING;\n // Using style.right ensures it stays on the right even if window resizes before first usage\n this.ui.glassDiv.style.right = `${padding}px`;\n this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.ui.glassDiv.style.left = 'auto'; // Ensure left is unset\n this.state.wasActivatedBefore = true;\n } else {\n // If it was already activated, and we are resetting, we might want to keep its current position\n // or explicitly reset it to the initial anchored position.\n // For now, let's re-apply the anchored position on reset if it was already activated.\n const padding = DEFAULT_PADDING;\n this.ui.glassDiv.style.right = `${padding}px`;\n this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.ui.glassDiv.style.left = 'auto';\n }\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.popOutManager.cleanup();\n this.ui.cleanup();\n }\n\n}\n"],"names":["dpr"],"mappings":"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"} \ No newline at end of file +{"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, DEFAULT_GLASS_Y_OFFSET, INFO_PANEL_ID } from '../shared/constants';\nimport { registerGlassSettings, registerAccessibilitySettings } 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';\nimport { PopOutManager } from './PopOutManager';\nimport { OffscreenRenderer } from './OffscreenRenderer';\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 popOutManager: PopOutManager;\n offscreenRenderer: OffscreenRenderer | null;\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 /** Animation loop ID for continuous video rendering */\n private animationLoopId: number | null;\n\n /** Whether videos are currently visible in magnified view */\n private hasVisibleVideos: boolean;\n\n constructor() {\n this.config = new ConfigManager();\n this.state = new MagnifierState();\n this.popOutManager = new PopOutManager();\n this.ui = new UiManager(\n this.config,\n this.state,\n () => this.toggle()\n );\n\n // onStateChange is handled by InfoPanel\n\n\n this.renderer = null;\n this.offscreenRenderer = 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 // Animation loop for video rendering\n this.animationLoopId = null;\n this.hasVisibleVideos = false;\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\n\n // Load saved offsets first\n this.config.loadSavedOffsets();\n\n // Print detailed canvas information\n\n\n // Create UI elements\n this.ui.createElements();\n\n // Ensure glass is validly positioned (within viewport) right away\n // This handles cases where config has saved positions from a different monitor size\n this.ui.updateResponsivePosition();\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 // Initialize offscreen renderer for high-res magnification\n this.offscreenRenderer = new OffscreenRenderer(this.config, this.state);\n\n // Attach event handlers\n this.eventHandler.attachListeners();\n\n // Register settings with ComfyUI\n registerGlassSettings(this);\n registerAccessibilitySettings(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 * Toggle the magnifying glass active state.\n */\n toggle(): void {\n const state = this.state;\n\n if (state.active) {\n // TURNING OFF - Force hide everything\n this.forceHideAllComponents();\n } else {\n // TURNING ON\n state.active = true;\n this.ui.show();\n if (this.eventHandler) {\n this.eventHandler.updateInitialPosition();\n }\n // Start continuous animation loop for smooth video playback\n this.startAnimationLoop();\n }\n }\n\n /**\n * Force hide all components including extensions (Info Panel, etc).\n * This overcomes the \"pinned\" state of the info panel when the main tool is deactivated.\n */\n private forceHideAllComponents(): void {\n this.state.active = false;\n this.stopAnimationLoop();\n this.hasVisibleVideos = false;\n this.ui.hide();\n\n // 1. Nuclear option: Direct DOM ID targeting\n // This is necessary because in some cases object references might be stale or logic might prevent hiding\n const panelEl = document.getElementById(INFO_PANEL_ID);\n if (panelEl) {\n panelEl.style.display = 'none';\n // Also force opacity 0 just in case transition handles display\n panelEl.style.opacity = '0';\n }\n\n // Iterate through all registered extensions and hide them if they have a hide method\n const extensions = window.comfyUIMagnifyGlassExtensions;\n if (extensions && extensions.length > 0) {\n extensions.forEach((extension: any) => {\n // Check basically any likely property for a UI manager or direct hide method\n if (extension && extension.uiManager && typeof extension.uiManager.hide === 'function') {\n extension.uiManager.hide();\n // Explicitly force display none if available to be extra sure\n if (extension.uiManager.elements && extension.uiManager.elements.panel) {\n extension.uiManager.elements.panel.style.display = 'none';\n }\n }\n });\n }\n }\n\n /**\n * Set glass preview visibility WITHOUT affecting info panel.\n * This is used by the toggle-glass hover control button.\n * When hidden, the renderer stops completely to save performance.\n * When shown, the renderer resumes.\n * \n * @param visible - true to show and enable rendering, false to hide and disable\n */\n setGlassPreviewActive(visible: boolean): void {\n // Track this state internally\n this.state.isPreviewHidden = !visible;\n\n if (visible) {\n // Show the glass preview\n this.ui.setPreviewVisibility(true);\n // Resume rendering on next mouse move\n } else {\n // Hide the glass preview visually\n this.ui.setPreviewVisibility(false);\n // The rendering will be skipped in updateMagnifiedView due to isPreviewHidden check\n }\n }\n\n /**\n * Check if the glass preview is visible.\n * Returns false if hidden via hover controls.\n */\n private isGlassPreviewVisible(): boolean {\n // Check info panel extension state for isGlassPreviewVisible\n const extensions = (window as any).comfyUIMagnifyGlassExtensions;\n if (extensions && extensions.length > 0) {\n const infoPanel = extensions[0];\n if (infoPanel?.stateManager?.state) {\n return infoPanel.stateManager.state.isGlassPreviewVisible !== false;\n }\n }\n // Default to visible if we can't find the state\n return true;\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 // Skip rendering if glass preview is hidden via hover controls\n // This prevents double FPS when only the info panel is active\n if (this.state.isPreviewHidden) {\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\n // Try virtual zoom capture for high-res magnification\n // This temporarily sets LiteGraph to 100% zoom, captures clean pixels,\n // then restores the original zoom level\n let sourceCanvas: HTMLCanvasElement = this.litegraphCanvas;\n if (this.offscreenRenderer && this.offscreenRenderer.isAvailable()) {\n const highResCanvas = this.offscreenRenderer.renderHighResRegion(this.litegraphCanvas);\n if (highResCanvas) {\n sourceCanvas = highResCanvas;\n }\n }\n\n // Render the magnified view\n this.renderer.render(sourceCanvas);\n\n // Update debug visualization\n\n\n // Render HTML overlays for text, video, and image widgets\n this.renderHtmlOverlays();\n\n // Send frame to pop-out tab if open\n if (this.popOutManager.isPopOutOpen() && this.ui.glassCanvas) {\n this.popOutManager.sendFrame(this.ui.glassCanvas);\n }\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 if (!this.litegraphCanvas) return;\n\n // Get DPR (Device Pixel Ratio) - relationship between Backing Store pixels and CSS pixels\n const rect = this.litegraphCanvas.getBoundingClientRect();\n const dpr = rect.width > 0 ? this.litegraphCanvas.width / rect.width : 1;\n\n // Convert cursor Backing pixels to CSS pixels (Screen pixels)\n // state.x/y are in Backing Pixels (set in EventHandler)\n const cursorCssX = cursorPixelX / dpr;\n const cursorCssY = cursorPixelY / dpr;\n\n // Convert cursor CSS pixels to LiteGraph graph coordinates\n // ds.offset and ds.scale operate in CSS pixel space\n const cursorGraphX = (cursorCssX - canvasOffsetX) / canvasScale;\n const cursorGraphY = (cursorCssY - canvasOffsetY) / canvasScale;\n\n // Apply manual offset (in Graph Units - arbitrary units)\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 Backing Pixels for the source rectangle\n // 1. Convert Graph -> CSS: (Graph * Scale) + Offset\n // 2. Convert CSS -> Backing: CSS * DPR\n const sourceCssX = (sourceGraphX * canvasScale) + canvasOffsetX;\n const sourceCssY = (sourceGraphY * canvasScale) + canvasOffsetY;\n const sourceCssWidth = sourceGraphWidth * canvasScale;\n const sourceCssHeight = sourceGraphHeight * canvasScale;\n\n this.state.sourceX = sourceCssX * dpr;\n this.state.sourceY = sourceCssY * dpr;\n this.state.sourceWidth = sourceCssWidth * dpr;\n this.state.sourceHeight = sourceCssHeight * dpr;\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 // Track if any videos are found for animation loop management\n let foundVideos = false;\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 // Calculate source region in graph coordinates for culling\n // This mirrors calculateSourceRegion() but stops at graph coords\n if (this.state.canvasScale === 0) return;\n\n const rect = this.litegraphCanvas.getBoundingClientRect();\n const dpr = rect.width > 0 ? this.litegraphCanvas.width / rect.width : 1;\n const cursorCssX = this.state.x / dpr;\n const cursorCssY = this.state.y / dpr;\n const cursorGraphX = (cursorCssX - this.state.canvasOffsetX) / this.state.canvasScale;\n const cursorGraphY = (cursorCssY - this.state.canvasOffsetY) / this.state.canvasScale;\n const targetGraphCenterX = cursorGraphX + this.config.offsetX;\n const targetGraphCenterY = cursorGraphY + this.config.offsetY;\n const sourceGraphWidth = (this.config.glassSize / this.config.zoomFactor) / this.state.canvasScale;\n const sourceGraphHeight = (this.config.glassSize / this.config.zoomFactor) / this.state.canvasScale;\n\n const sourceGraphRect = {\n x: targetGraphCenterX - (sourceGraphWidth / 2),\n y: targetGraphCenterY - (sourceGraphHeight / 2),\n width: sourceGraphWidth,\n height: sourceGraphHeight\n };\n\n for (const node of nodes) {\n // Optimization: Cull nodes that are not in the source region\n if (node.pos && node.size) {\n const nodeRect = {\n x: node.pos[0],\n y: node.pos[1],\n width: node.size[0],\n height: node.size[1]\n };\n if (!rectsOverlap(sourceGraphRect, nodeRect)) {\n continue;\n }\n }\n\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 // Check for VHS-style widgets with videoEl property (VideoHelperSuite pattern)\n const widgetName = String(widget.name || '').toLowerCase();\n if (widgetName === 'videopreview' || widgetName === 'audiopreview') {\n const videoEl = widget.videoEl as HTMLVideoElement | undefined;\n if (videoEl && !videoEl.hidden && videoEl.videoWidth > 0) {\n isVideoElement = true;\n foundVideos = true;\n elementToProcess = videoEl;\n }\n }\n\n // Check for standard DOM element widgets (existing logic)\n if (!elementToProcess && widget.element) {\n const element = widget.element as HTMLElement;\n // Skip text elements - now rendered natively on canvas via OffscreenRenderer\n if (widget.type === \"text\" || widget.type === \"string\" || element.tagName === 'TEXTAREA') {\n // isTextElement = true; // DISABLED - using native canvas rendering\n // elementToProcess = element;\n } else if (element.tagName === 'VIDEO') {\n isVideoElement = true;\n foundVideos = 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 foundVideos = 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 // Optimization: Reuse canvasRect and dpr from top level scope\n const canvasRect = rect;\n\n // DPR is already calculated at function start\n const currentScale = this.state.canvasScale;\n const isVirtualZoomMode = currentScale < 0.7;\n\n // Widget position in CSS coordinates relative to canvas\n const widgetCssX = widgetRect.left - canvasRect.left;\n const widgetCssY = widgetRect.top - canvasRect.top;\n const widgetCssWidth = widgetRect.width;\n const widgetCssHeight = widgetRect.height;\n\n // Pivot point (mouse position) in CSS coordinates\n const pivotCssX = this.state.x / dpr;\n const pivotCssY = this.state.y / dpr;\n\n let finalWidgetCssX: number;\n let finalWidgetCssY: number;\n let finalWidgetCssWidth: number;\n let finalWidgetCssHeight: number;\n\n if (isVirtualZoomMode) {\n // Virtual Zoom: Transform widget position to match virtual 1.0 scale capture\n // Formula: virtualCss = (widgetCss - pivotCss) / currentScale + pivotCss\n finalWidgetCssX = (widgetCssX - pivotCssX) / currentScale + pivotCssX;\n finalWidgetCssY = (widgetCssY - pivotCssY) / currentScale + pivotCssY;\n // Size also scales inversely\n finalWidgetCssWidth = widgetCssWidth / currentScale;\n finalWidgetCssHeight = widgetCssHeight / currentScale;\n } else {\n // Direct Capture: Use actual positions\n finalWidgetCssX = widgetCssX;\n finalWidgetCssY = widgetCssY;\n finalWidgetCssWidth = widgetCssWidth;\n finalWidgetCssHeight = widgetCssHeight;\n }\n\n // Convert to backing pixels for comparison with magnifyRect\n const widgetCanvasX = finalWidgetCssX * dpr;\n const widgetCanvasY = finalWidgetCssY * dpr;\n const widgetCanvasWidth = finalWidgetCssWidth * dpr;\n const widgetCanvasHeight = finalWidgetCssHeight * dpr;\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 // Hide scrollbar while keeping content visible\n clonedElement.style.overflow = 'hidden';\n (clonedElement as HTMLInputElement).disabled = true;\n\n // Dual adaptive font sizing:\n // - When zoomed OUT (below 50%): shrink text to fit all content\n // - When zoomed IN (above 50%): grow text for better readability\n const canvasScale = this.state.canvasScale;\n const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize);\n const threshold = 0.5; // 50% zoom threshold\n\n let adaptedFontSize: number;\n if (canvasScale < threshold) {\n // Zoomed out: shrink text aggressively to fit content\n // Scale from threshold down to minimum\n const shrinkFactor = canvasScale / threshold;\n adaptedFontSize = Math.max(6, originalFontSize * shrinkFactor * 0.8);\n } else {\n // Zoomed in: grow text for readability\n // Scale from 100% at threshold up to 150% at 100%+ zoom\n const growFactor = 1 + ((canvasScale - threshold) / (1 - threshold)) * 0.5;\n adaptedFontSize = originalFontSize * Math.min(1.5, growFactor);\n }\n\n clonedElement.style.fontSize = `${adaptedFontSize}px`;\n clonedElement.style.lineHeight = canvasScale < threshold ? '1.2' : '1.4';\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 => {\n if (e.name !== 'AbortError') {\n console.warn(\"Magnify Glass: Cloned video play failed\", e);\n }\n });\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 this.ui.htmlOverlayContainer!.appendChild(clonedElement);\n }\n }\n }\n }\n\n // Manage animation loop based on video presence\n if (foundVideos && !this.hasVisibleVideos) {\n this.hasVisibleVideos = true;\n this.startAnimationLoop();\n } else if (!foundVideos && this.hasVisibleVideos) {\n this.hasVisibleVideos = false;\n // Loop will stop itself on next iteration\n }\n }\n\n /**\n * Start the animation loop for continuous video rendering.\n * Only runs when videos are visible in the magnified view.\n * This loop directly renders without going through updateMagnifiedView's\n * scheduling logic to ensure continuous frame updates for video playback.\n */\n private startAnimationLoop(): void {\n if (this.animationLoopId !== null) return; // Already running\n\n const animate = () => {\n // Stop if glass is deactivated or renderer unavailable\n if (!this.state.active || !this.renderer || !this.litegraphCanvas) {\n this.stopAnimationLoop();\n return;\n }\n\n // Skip if glass preview is hidden\n if (this.state.isPreviewHidden) {\n this.animationLoopId = requestAnimationFrame(animate);\n return;\n }\n\n // Update canvas transformation and source region\n this.updateCanvasTransformation();\n this.calculateSourceRegion();\n\n // Direct render (bypass isRenderScheduled to ensure continuous updates)\n let sourceCanvas: HTMLCanvasElement = this.litegraphCanvas;\n if (this.offscreenRenderer && this.offscreenRenderer.isAvailable()) {\n const highResCanvas = this.offscreenRenderer.renderHighResRegion(this.litegraphCanvas);\n if (highResCanvas) {\n sourceCanvas = highResCanvas;\n }\n }\n\n // Render the magnified view\n this.renderer.render(sourceCanvas);\n\n // Render HTML overlays for video widgets\n this.renderHtmlOverlays();\n\n // Send frame to pop-out tab if open\n if (this.popOutManager.isPopOutOpen() && this.ui.glassCanvas) {\n this.popOutManager.sendFrame(this.ui.glassCanvas);\n }\n\n this.animationLoopId = requestAnimationFrame(animate);\n };\n\n this.animationLoopId = requestAnimationFrame(animate);\n }\n\n /**\n * Stop the animation loop.\n */\n private stopAnimationLoop(): void {\n if (this.animationLoopId !== null) {\n cancelAnimationFrame(this.animationLoopId);\n this.animationLoopId = null;\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\n /**\n * Reset offsets and panel positions to defaults.\n */\n resetOffsets(): void {\n this.config.resetOffsets();\n\n // Also disable follow cursor\n this.config.followCursor = false;\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 // If it's the first activation, position at top right of the window (Anchored)\n // Otherwise, reset to the last known position or default if not active.\n if (!this.state.wasActivatedBefore) {\n const padding = DEFAULT_PADDING;\n // Using style.right ensures it stays on the right even if window resizes before first usage\n this.ui.glassDiv.style.right = `${padding}px`;\n this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.ui.glassDiv.style.left = 'auto'; // Ensure left is unset\n this.state.wasActivatedBefore = true;\n } else {\n // If it was already activated, and we are resetting, we might want to keep its current position\n // or explicitly reset it to the initial anchored position.\n // For now, let's re-apply the anchored position on reset if it was already activated.\n const padding = DEFAULT_PADDING;\n this.ui.glassDiv.style.right = `${padding}px`;\n this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.ui.glassDiv.style.left = 'auto';\n }\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.stopAnimationLoop();\n this.eventHandler.detachListeners();\n this.popOutManager.cleanup();\n this.ui.cleanup();\n }\n\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file