diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd87f5..a7bc9db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.0] - 2026-01-02 + +### Added +- **Multi-Monitor Pop-Out Tab**: Open the magnified view in a separate browser tab that can be detached and moved to a secondary monitor. + - Pop-out button appears on hover in the top-right corner of the glass. + - Keyboard shortcut `Shift+P` to toggle pop-out when magnifier is active. + - Real-time sync at 30fps using BroadcastChannel API. + - Professional dark-themed viewer with connection status indicator. + - Viewer page automatically syncs glass shape and border styling. + +--- + ## [1.6.0] - 2025-01-02 ### Added diff --git a/src/magnify-glass/EventHandler.ts b/src/magnify-glass/EventHandler.ts index 3601ccd..1e156a9 100644 --- a/src/magnify-glass/EventHandler.ts +++ b/src/magnify-glass/EventHandler.ts @@ -152,6 +152,12 @@ export class EventHandler { config.saveOffsets(); this.magnifyGlass.updateMagnifiedView(); } + + // Pop-out tab toggle (Shift+P) + if (e.shiftKey && e.key.toLowerCase() === 'p') { + this.magnifyGlass.popOutManager.toggle(); + e.preventDefault(); + } } } diff --git a/src/magnify-glass/MagnifyGlass.ts b/src/magnify-glass/MagnifyGlass.ts index 9cd1571..7dfcca2 100644 --- a/src/magnify-glass/MagnifyGlass.ts +++ b/src/magnify-glass/MagnifyGlass.ts @@ -14,6 +14,7 @@ import { UiManager } from './UiManager'; import { WebGLRenderer } from './WebGLRenderer'; import { DebugManager } from './DebugManager'; import { EventHandler } from './EventHandler'; +import { PopOutManager } from './PopOutManager'; // External ComfyUI globals declare const app: ComfyApp; @@ -30,6 +31,7 @@ export class MagnifyGlass { renderer: WebGLRenderer | null; debugger: DebugManager; eventHandler: EventHandler; + popOutManager: PopOutManager; /** The LiteGraph canvas */ litegraphCanvas: HTMLCanvasElement | null; @@ -46,7 +48,13 @@ export class MagnifyGlass { constructor() { this.config = new ConfigManager(); this.state = new MagnifierState(); - this.ui = new UiManager(this.config, this.state, () => this.toggle()); + this.popOutManager = new PopOutManager(); + this.ui = new UiManager( + this.config, + this.state, + () => this.toggle(), + () => this.popOutManager.toggle() + ); this.renderer = null; this.debugger = new DebugManager(this.config, this.state, this.ui); this.eventHandler = new EventHandler(this); @@ -162,6 +170,11 @@ export class MagnifyGlass { // Render HTML overlays this.renderHtmlOverlays(); + // Send frame to pop-out tab if open + if (this.popOutManager.isPopOutOpen() && this.ui.glassCanvas) { + this.popOutManager.sendFrame(this.ui.glassCanvas); + } + this.state.isRenderScheduled = false; }); } @@ -440,6 +453,7 @@ export class MagnifyGlass { */ cleanup(): void { this.eventHandler.detachListeners(); + this.popOutManager.cleanup(); this.ui.cleanup(); } } diff --git a/src/magnify-glass/PopOutManager.ts b/src/magnify-glass/PopOutManager.ts new file mode 100644 index 0000000..709759b --- /dev/null +++ b/src/magnify-glass/PopOutManager.ts @@ -0,0 +1,245 @@ +/** + * ComfyUI MagnifyGlass - PopOutManager (TypeScript) + * + * Manages pop-out tab functionality for multi-monitor viewing. + * Uses BroadcastChannel API for efficient cross-tab communication. + */ + +import { BROADCAST_CHANNEL_NAME } from '../shared/constants'; +import { Logger } from '../shared/logger'; + +/** + * Message types for BroadcastChannel communication + */ +interface PopOutMessage { + type: 'frame' | 'config' | 'close' | 'ping' | 'pong'; + data?: string | PopOutConfig; + timestamp?: number; +} + +interface PopOutConfig { + glassSize: number; + borderColor: string; + borderWidth: number; + glassShape: string; +} + +/** + * PopOutManager class. + * Handles opening, closing, and syncing with an external browser tab. + */ +export class PopOutManager { + private channel: BroadcastChannel | null = null; + private isOpen: boolean = false; + private lastPongTime: number = 0; + private pingInterval: number | null = null; + private viewerUrl: string; + + /** Throttle frame sending to ~30fps */ + private lastFrameTime: number = 0; + private readonly FRAME_INTERVAL = 33; // ~30fps + + /** Connection timeout (ms) */ + private readonly CONNECTION_TIMEOUT = 5000; + + constructor() { + // Get the viewer URL relative to the extension + // The viewer HTML will be in the web directory + this.viewerUrl = this.getViewerUrl(); + this.initChannel(); + } + + /** + * Get the URL for the pop-out viewer page. + */ + private getViewerUrl(): string { + // Find the extension's base URL from the loaded scripts + const scripts = document.querySelectorAll('script[src*="magnify"]'); + if (scripts.length > 0) { + const src = (scripts[0] as HTMLScriptElement).src; + const baseUrl = src.substring(0, src.lastIndexOf('/')); + return `${baseUrl}/popout-viewer.html`; + } + // Fallback: assume standard ComfyUI extension path + return '/extensions/comfyui-magnifyglass/popout-viewer.html'; + } + + /** + * Initialize the BroadcastChannel. + */ + private initChannel(): void { + try { + this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME); + this.channel.onmessage = (event: MessageEvent) => { + this.handleMessage(event.data); + }; + Logger.debug('[PopOut] BroadcastChannel initialized'); + } catch (e) { + Logger.error('[PopOut] Failed to create BroadcastChannel:', e); + } + } + + /** + * Handle incoming messages from the viewer tab. + */ + private handleMessage(message: PopOutMessage): void { + switch (message.type) { + case 'pong': + this.lastPongTime = Date.now(); + if (!this.isOpen) { + this.isOpen = true; + Logger.debug('[PopOut] Viewer tab connected'); + } + break; + case 'close': + this.isOpen = false; + this.stopPing(); + Logger.debug('[PopOut] Viewer tab closed'); + break; + } + } + + /** + * Open the pop-out viewer in a new tab. + */ + open(): void { + if (this.isOpen) { + Logger.debug('[PopOut] Tab already open'); + return; + } + + // Open new tab + const newTab = window.open(this.viewerUrl, '_blank'); + if (!newTab) { + Logger.error('[PopOut] Failed to open new tab - popup may be blocked'); + return; + } + + // Start pinging to detect when viewer is ready + this.startPing(); + Logger.debug('[PopOut] Opening viewer tab...'); + } + + /** + * Close the pop-out viewer tab. + */ + close(): void { + if (!this.isOpen) return; + + this.sendMessage({ type: 'close' }); + this.isOpen = false; + this.stopPing(); + Logger.debug('[PopOut] Sent close message to viewer'); + } + + /** + * Toggle the pop-out state. + */ + toggle(): void { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + /** + * Check if the pop-out tab is currently open. + */ + isPopOutOpen(): boolean { + return this.isOpen; + } + + /** + * Send a frame to the pop-out viewer. + * Throttled to ~30fps for performance. + * @param canvas - The source canvas to send + */ + sendFrame(canvas: HTMLCanvasElement): void { + if (!this.isOpen || !this.channel) return; + + // Throttle frame rate + const now = Date.now(); + if (now - this.lastFrameTime < this.FRAME_INTERVAL) { + return; + } + this.lastFrameTime = now; + + try { + // Convert canvas to data URL (efficient for small canvases) + const dataUrl = canvas.toDataURL('image/png'); + this.sendMessage({ + type: 'frame', + data: dataUrl, + timestamp: now + }); + } catch (e) { + Logger.error('[PopOut] Failed to send frame:', e); + } + } + + /** + * Send configuration to the pop-out viewer. + */ + sendConfig(config: PopOutConfig): void { + if (!this.channel) return; + + this.sendMessage({ + type: 'config', + data: config + }); + } + + /** + * Send a message through the BroadcastChannel. + */ + private sendMessage(message: PopOutMessage): void { + if (!this.channel) return; + + try { + this.channel.postMessage(message); + } catch (e) { + Logger.error('[PopOut] Failed to send message:', e); + } + } + + /** + * Start pinging to check connection. + */ + private startPing(): void { + this.stopPing(); + this.pingInterval = setInterval(() => { + this.sendMessage({ type: 'ping', timestamp: Date.now() }); + + // Check for connection timeout + if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) { + Logger.debug('[PopOut] Connection timeout, marking as closed'); + this.isOpen = false; + this.stopPing(); + } + }, 1000); + } + + /** + * Stop pinging. + */ + private stopPing(): void { + if (this.pingInterval) { + clearInterval(this.pingInterval); + this.pingInterval = null; + } + } + + /** + * Cleanup resources. + */ + cleanup(): void { + this.close(); + this.stopPing(); + if (this.channel) { + this.channel.close(); + this.channel = null; + } + Logger.debug('[PopOut] Cleaned up'); + } +} diff --git a/src/magnify-glass/UiManager.ts b/src/magnify-glass/UiManager.ts index ceb41b9..4084d4e 100644 --- a/src/magnify-glass/UiManager.ts +++ b/src/magnify-glass/UiManager.ts @@ -22,9 +22,11 @@ export class UiManager { debugCanvas: HTMLCanvasElement | null; debugCtx: CanvasRenderingContext2D | null; htmlOverlayContainer: HTMLDivElement | null; + popOutButton: HTMLButtonElement | null; onToggle: (() => void) | undefined; + onPopOut: (() => void) | undefined; - constructor(config: ConfigManager, state: MagnifierState, onToggle?: () => void) { + constructor(config: ConfigManager, state: MagnifierState, onToggle?: () => void, onPopOut?: () => void) { this.config = config; this.state = state; this.glassDiv = null; @@ -32,7 +34,9 @@ export class UiManager { this.debugCanvas = null; this.debugCtx = null; this.htmlOverlayContainer = null; + this.popOutButton = null; this.onToggle = onToggle; + this.onPopOut = onPopOut; } /** @@ -78,6 +82,63 @@ export class UiManager { `; this.glassDiv.appendChild(this.htmlOverlayContainer); + // Create pop-out button + this.popOutButton = document.createElement("button"); + this.popOutButton.id = "comfyui-magnify-popout-btn"; + this.popOutButton.title = "Open in New Tab (Shift+P)"; + this.popOutButton.innerHTML = Icons.externalLink; + this.popOutButton.style.cssText = ` + position: absolute; + top: 6px; + right: 6px; + width: 24px; + height: 24px; + padding: 4px; + border: none; + border-radius: 4px; + background: rgba(0, 0, 0, 0.4); + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + pointer-events: auto; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease; + z-index: 10; + `; + this.popOutButton.addEventListener('click', (e) => { + e.stopPropagation(); + if (this.onPopOut) { + this.onPopOut(); + } + }); + this.popOutButton.addEventListener('mouseenter', () => { + if (this.popOutButton) { + this.popOutButton.style.background = 'rgba(99, 102, 241, 0.6)'; + this.popOutButton.style.color = '#fff'; + } + }); + this.popOutButton.addEventListener('mouseleave', () => { + if (this.popOutButton) { + this.popOutButton.style.background = 'rgba(0, 0, 0, 0.4)'; + this.popOutButton.style.color = 'rgba(255, 255, 255, 0.7)'; + } + }); + this.glassDiv.appendChild(this.popOutButton); + + // Show pop-out button on glass hover + this.glassDiv.addEventListener('mouseenter', () => { + if (this.popOutButton) { + this.popOutButton.style.opacity = '1'; + } + }); + this.glassDiv.addEventListener('mouseleave', () => { + if (this.popOutButton) { + this.popOutButton.style.opacity = '0'; + } + }); + document.body.appendChild(this.glassDiv); // Create debug canvas if debug mode is enabled diff --git a/src/shared/constants.ts b/src/shared/constants.ts index d127abd..f356cf9 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -84,3 +84,7 @@ export const STORAGE_KEYS = { OFFSET_X: 'comfyui_magnify_offset_x', OFFSET_Y: 'comfyui_magnify_offset_y' } as const; + +// BroadcastChannel for pop-out tab communication +export const BROADCAST_CHANNEL_NAME = 'comfyui-magnifyglass-popout'; + diff --git a/src/shared/icons.ts b/src/shared/icons.ts index 040dcd6..4a0b0a8 100644 --- a/src/shared/icons.ts +++ b/src/shared/icons.ts @@ -82,6 +82,13 @@ export const Icons = { // Magnet icon - persist/sticky mode magnet: ` + `, + + // External link icon - pop out to new tab + externalLink: ` + + + ` }; diff --git a/web/magnify-glass/EventHandler.js b/web/magnify-glass/EventHandler.js index 9fd30a2..37a86ff 100644 --- a/web/magnify-glass/EventHandler.js +++ b/web/magnify-glass/EventHandler.js @@ -117,6 +117,10 @@ class EventHandler { config.saveOffsets(); this.magnifyGlass.updateMagnifiedView(); } + if (e.shiftKey && e.key.toLowerCase() === "p") { + this.magnifyGlass.popOutManager.toggle(); + e.preventDefault(); + } } } /** diff --git a/web/magnify-glass/EventHandler.js.map b/web/magnify-glass/EventHandler.js.map index e2e2ae8..63352f2 100644 --- a/web/magnify-glass/EventHandler.js.map +++ b/web/magnify-glass/EventHandler.js.map @@ -1 +1 @@ -{"version":3,"file":"EventHandler.js","sources":["../../src/magnify-glass/EventHandler.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - EventHandler (TypeScript)\n * \n * Handles keyboard and mouse events for the magnifying glass.\n */\n\nimport { isUserTyping } from '../shared/utils';\nimport { Logger } from '../shared/logger';\nimport { DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants';\nimport type { MagnifyGlass } from './MagnifyGlass';\n\n/**\n * Event Handler class.\n * Manages all keyboard and mouse event handling for the magnifier.\n */\nexport class EventHandler {\n magnifyGlass: MagnifyGlass;\n\n constructor(magnifyGlass: MagnifyGlass) {\n this.magnifyGlass = magnifyGlass;\n\n this.handleKeyDown = this.handleKeyDown.bind(this);\n this.handleKeyUp = this.handleKeyUp.bind(this);\n this.handleMouseMove = this.handleMouseMove.bind(this);\n this.handleResize = this.handleResize.bind(this);\n }\n\n /**\n * Attach all event listeners.\n */\n attachListeners(): void {\n document.addEventListener(\"keydown\", this.handleKeyDown);\n document.addEventListener(\"keyup\", this.handleKeyUp);\n document.addEventListener(\"mousemove\", this.handleMouseMove);\n window.addEventListener(\"resize\", this.handleResize);\n }\n\n /**\n * Detach all event listeners.\n */\n detachListeners(): void {\n document.removeEventListener(\"keydown\", this.handleKeyDown);\n document.removeEventListener(\"keyup\", this.handleKeyUp);\n document.removeEventListener(\"mousemove\", this.handleMouseMove);\n window.removeEventListener(\"resize\", this.handleResize);\n }\n\n /**\n * Handle window resize events.\n */\n handleResize(): void {\n if (this.magnifyGlass && this.magnifyGlass.ui) {\n this.magnifyGlass.ui.updateResponsivePosition();\n // Also update panel position if it exists\n const infoPanel = (window as any).infoPanelManager;\n if (infoPanel && infoPanel.positionManager) {\n // Determine if we should move it\n infoPanel.positionManager.positionPanel();\n }\n }\n }\n\n /**\n * Handle keydown events.\n */\n handleKeyDown(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n // Don't handle hotkeys if user is typing in an input field\n if (isUserTyping()) {\n return;\n }\n\n // Magnifier activation\n Logger.debug(`KeyDown: ${e.key} (config: ${config.activationKey}, active: ${state.active})`);\n if (e.key.toLowerCase() === config.activationKey &&\n (!config.altRequired || e.altKey)) {\n Logger.debug('Activation key matched!');\n if (config.alwaysActiveMode) {\n this.magnifyGlass.toggle();\n } else {\n if (!state.active) {\n this.magnifyGlass.toggle();\n }\n }\n }\n\n // Manual offset adjustment keys (only when magnifier is active)\n if (state.active) {\n let offsetChanged = false;\n const stepSize = e.shiftKey ? config.offsetStep * 5 : config.offsetStep;\n\n if (e.key === \"ArrowUp\") {\n config.offsetY -= stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowDown\") {\n config.offsetY += stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowLeft\") {\n config.offsetX -= stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowRight\") {\n config.offsetX += stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() &&\n (!config.altRequired || e.altKey)) {\n this.magnifyGlass.resetOffsets();\n offsetChanged = true;\n e.preventDefault();\n }\n\n // Toggle Follow Cursor Key\n if (e.key.toLowerCase() === config.toggleFollowCursorKey &&\n (!config.altRequired || e.altKey)) {\n config.followCursor = !config.followCursor;\n this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? 'ON' : 'OFF'}`);\n if (state.active && config.followCursor) {\n this.magnifyGlass.ui.positionGlass(\n this.magnifyGlass.lastKnownMousePosition.x,\n this.magnifyGlass.lastKnownMousePosition.y\n );\n }\n e.preventDefault();\n }\n\n // Toggle Glass Preview Key (G) - mimics the hover controls button\n if (e.key.toLowerCase() === config.toggleGlassPreviewKey &&\n (!config.altRequired || e.altKey)) {\n // Find and click the toggle-glass button in the info panel hover controls\n const toggleGlassBtn = document.querySelector('[data-action=\"toggle-glass\"]') as HTMLElement;\n if (toggleGlassBtn) {\n toggleGlassBtn.click();\n this.magnifyGlass.debugger.log(`Glass Preview Toggled (via button)`);\n } else {\n // Fallback: toggle via UI directly if button not found\n const glassDiv = this.magnifyGlass.ui.glassDiv;\n if (glassDiv) {\n const isVisible = glassDiv.style.display !== 'none' && glassDiv.style.opacity !== '0';\n this.magnifyGlass.ui.setPreviewVisibility(!isVisible);\n this.magnifyGlass.debugger.log(`Glass Preview Toggled (fallback): ${!isVisible ? 'ON' : 'OFF'}`);\n }\n }\n e.preventDefault();\n }\n\n if (offsetChanged) {\n config.saveOffsets();\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n }\n\n /**\n * Handle keyup events.\n */\n handleKeyUp(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n if (!config.alwaysActiveMode &&\n (e.key.toLowerCase() === config.activationKey ||\n (config.altRequired && e.key === \"Alt\"))) {\n\n let shouldDeactivate = false;\n if (config.altRequired) {\n if (e.key === \"Alt\" || e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n } else {\n if (e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n }\n\n if (state.active && shouldDeactivate) {\n state.active = false;\n this.magnifyGlass.ui.hide();\n }\n }\n }\n\n /**\n * Handle mousemove events.\n */\n handleMouseMove(e: MouseEvent): void {\n // Always track mouse position\n this.magnifyGlass.lastKnownMousePosition.x = e.clientX;\n this.magnifyGlass.lastKnownMousePosition.y = e.clientY;\n\n if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const cssMouseXOnCanvas = e.clientX - rect.left;\n const cssMouseYOnCanvas = e.clientY - rect.top;\n\n // Check if the cursor is over the canvas element\n if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width &&\n cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) {\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY);\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n\n /**\n * Update the initial position when magnifier is activated.\n */\n updateInitialPosition(): void {\n if (!this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const clientX = this.magnifyGlass.lastKnownMousePosition.x;\n const clientY = this.magnifyGlass.lastKnownMousePosition.y;\n\n const isOverCanvas = clientX >= rect.left && clientX <= rect.right &&\n clientY >= rect.top && clientY <= rect.bottom;\n\n if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) {\n const cssMouseXOnCanvas = clientX - rect.left;\n const cssMouseYOnCanvas = clientY - rect.top;\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n const glassSize = this.magnifyGlass.config.glassSize;\n\n if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) {\n // First activation - position at top right of the window (Anchored)\n // Using style.right ensures it stays on the right even if window resizes before first usage\n this.magnifyGlass.ui.glassDiv.style.right = `${DEFAULT_PADDING}px`;\n this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.magnifyGlass.ui.glassDiv.style.left = 'auto'; // Ensure left is unset\n this.magnifyGlass.state.wasActivatedBefore = true;\n } else {\n this.magnifyGlass.ui.positionGlass(clientX, clientY);\n }\n\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n}\n"],"names":[],"mappings":";;;;;;AAeO,MAAM,aAAa;AAAA,EAGtB,YAAY,cAA4B;AAFxC;AAGI,SAAK,eAAe;AAEpB,SAAK,gBAAgB,KAAK,cAAc,KAAK,IAAI;AACjD,SAAK,cAAc,KAAK,YAAY,KAAK,IAAI;AAC7C,SAAK,kBAAkB,KAAK,gBAAgB,KAAK,IAAI;AACrD,SAAK,eAAe,KAAK,aAAa,KAAK,IAAI;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAwB;AACpB,aAAS,iBAAiB,WAAW,KAAK,aAAa;AACvD,aAAS,iBAAiB,SAAS,KAAK,WAAW;AACnD,aAAS,iBAAiB,aAAa,KAAK,eAAe;AAC3D,WAAO,iBAAiB,UAAU,KAAK,YAAY;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAwB;AACpB,aAAS,oBAAoB,WAAW,KAAK,aAAa;AAC1D,aAAS,oBAAoB,SAAS,KAAK,WAAW;AACtD,aAAS,oBAAoB,aAAa,KAAK,eAAe;AAC9D,WAAO,oBAAoB,UAAU,KAAK,YAAY;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA,EAKA,eAAqB;AACjB,QAAI,KAAK,gBAAgB,KAAK,aAAa,IAAI;AAC3C,WAAK,aAAa,GAAG,yBAAA;AAErB,YAAM,YAAa,OAAe;AAClC,UAAI,aAAa,UAAU,iBAAiB;AAExC,kBAAU,gBAAgB,cAAA;AAAA,MAC9B;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,GAAwB;AAClC,UAAM,SAAS,KAAK,aAAa;AACjC,UAAM,QAAQ,KAAK,aAAa;AAGhC,QAAI,gBAAgB;AAChB;AAAA,IACJ;AAGA,WAAO,MAAM,YAAY,EAAE,GAAG,aAAa,OAAO,aAAa,aAAa,MAAM,MAAM,GAAG;AAC3F,QAAI,EAAE,IAAI,YAAA,MAAkB,OAAO,kBAC9B,CAAC,OAAO,eAAe,EAAE,SAAS;AACnC,aAAO,MAAM,yBAAyB;AACtC,UAAI,OAAO,kBAAkB;AACzB,aAAK,aAAa,OAAA;AAAA,MACtB,OAAO;AACH,YAAI,CAAC,MAAM,QAAQ;AACf,eAAK,aAAa,OAAA;AAAA,QACtB;AAAA,MACJ;AAAA,IACJ;AAGA,QAAI,MAAM,QAAQ;AACd,UAAI,gBAAgB;AACpB,YAAM,WAAW,EAAE,WAAW,OAAO,aAAa,IAAI,OAAO;AAE7D,UAAI,EAAE,QAAQ,WAAW;AACrB,eAAO,WAAW;AAClB,wBAAgB;AAChB,UAAE,eAAA;AAAA,MACN,WAAW,EAAE,QAAQ,aAAa;AAC9B,eAAO,WAAW;AAClB,wBAAgB;AAChB,UAAE,eAAA;AAAA,MACN,WAAW,EAAE,QAAQ,aAAa;AAC9B,eAAO,WAAW;AAClB,wBAAgB;AAChB,UAAE,eAAA;AAAA,MACN,WAAW,EAAE,QAAQ,cAAc;AAC/B,eAAO,WAAW;AAClB,wBAAgB;AAChB,UAAE,eAAA;AAAA,MACN,WAAW,EAAE,IAAI,YAAA,MAAkB,OAAO,SAAS,YAAA,MAC9C,CAAC,OAAO,eAAe,EAAE,SAAS;AACnC,aAAK,aAAa,aAAA;AAClB,wBAAgB;AAChB,UAAE,eAAA;AAAA,MACN;AAGA,UAAI,EAAE,IAAI,YAAA,MAAkB,OAAO,0BAC9B,CAAC,OAAO,eAAe,EAAE,SAAS;AACnC,eAAO,eAAe,CAAC,OAAO;AAC9B,aAAK,aAAa,SAAS,IAAI,0BAA0B,OAAO,eAAe,OAAO,KAAK,EAAE;AAC7F,YAAI,MAAM,UAAU,OAAO,cAAc;AACrC,eAAK,aAAa,GAAG;AAAA,YACjB,KAAK,aAAa,uBAAuB;AAAA,YACzC,KAAK,aAAa,uBAAuB;AAAA,UAAA;AAAA,QAEjD;AACA,UAAE,eAAA;AAAA,MACN;AAGA,UAAI,EAAE,IAAI,YAAA,MAAkB,OAAO,0BAC9B,CAAC,OAAO,eAAe,EAAE,SAAS;AAEnC,cAAM,iBAAiB,SAAS,cAAc,8BAA8B;AAC5E,YAAI,gBAAgB;AAChB,yBAAe,MAAA;AACf,eAAK,aAAa,SAAS,IAAI,oCAAoC;AAAA,QACvE,OAAO;AAEH,gBAAM,WAAW,KAAK,aAAa,GAAG;AACtC,cAAI,UAAU;AACV,kBAAM,YAAY,SAAS,MAAM,YAAY,UAAU,SAAS,MAAM,YAAY;AAClF,iBAAK,aAAa,GAAG,qBAAqB,CAAC,SAAS;AACpD,iBAAK,aAAa,SAAS,IAAI,qCAAqC,CAAC,YAAY,OAAO,KAAK,EAAE;AAAA,UACnG;AAAA,QACJ;AACA,UAAE,eAAA;AAAA,MACN;AAEA,UAAI,eAAe;AACf,eAAO,YAAA;AACP,aAAK,aAAa,oBAAA;AAAA,MACtB;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,GAAwB;AAChC,UAAM,SAAS,KAAK,aAAa;AACjC,UAAM,QAAQ,KAAK,aAAa;AAEhC,QAAI,CAAC,OAAO,qBACP,EAAE,IAAI,YAAA,MAAkB,OAAO,iBAC3B,OAAO,eAAe,EAAE,QAAQ,QAAS;AAE9C,UAAI,mBAAmB;AACvB,UAAI,OAAO,aAAa;AACpB,YAAI,EAAE,QAAQ,SAAS,EAAE,IAAI,YAAA,MAAkB,OAAO,eAAe;AACjE,6BAAmB;AAAA,QACvB;AAAA,MACJ,OAAO;AACH,YAAI,EAAE,IAAI,YAAA,MAAkB,OAAO,eAAe;AAC9C,6BAAmB;AAAA,QACvB;AAAA,MACJ;AAEA,UAAI,MAAM,UAAU,kBAAkB;AAClC,cAAM,SAAS;AACf,aAAK,aAAa,GAAG,KAAA;AAAA,MACzB;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,GAAqB;AAEjC,SAAK,aAAa,uBAAuB,IAAI,EAAE;AAC/C,SAAK,aAAa,uBAAuB,IAAI,EAAE;AAE/C,QAAI,CAAC,KAAK,aAAa,MAAM,UAAU,CAAC,KAAK,aAAa,gBAAiB;AAE3E,UAAM,OAAO,KAAK,aAAa,gBAAgB,sBAAA;AAC/C,UAAM,oBAAoB,EAAE,UAAU,KAAK;AAC3C,UAAM,oBAAoB,EAAE,UAAU,KAAK;AAG3C,QAAI,qBAAqB,KAAK,qBAAqB,KAAK,SACpD,qBAAqB,KAAK,qBAAqB,KAAK,QAAQ;AAE5D,YAAM,gBAAgB,KAAK,aAAa;AACxC,YAAM,SAAS,KAAK,QAAQ,IAAI,cAAc,QAAQ,KAAK,QAAQ;AACnE,YAAM,SAAS,KAAK,SAAS,IAAI,cAAc,SAAS,KAAK,SAAS;AAEtE,YAAM,SAAS,oBAAoB;AACnC,YAAM,SAAS,oBAAoB;AAEnC,WAAK,aAAa,MAAM,IAAI;AAC5B,WAAK,aAAa,MAAM,IAAI;AAE5B,WAAK,aAAa,GAAG,cAAc,EAAE,SAAS,EAAE,OAAO;AACvD,WAAK,aAAa,oBAAA;AAAA,IACtB;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,wBAA8B;AAC1B,QAAI,CAAC,KAAK,aAAa,gBAAiB;AAExC,UAAM,OAAO,KAAK,aAAa,gBAAgB,sBAAA;AAC/C,UAAM,UAAU,KAAK,aAAa,uBAAuB;AACzD,UAAM,UAAU,KAAK,aAAa,uBAAuB;AAEzD,UAAM,eAAe,WAAW,KAAK,QAAQ,WAAW,KAAK,SACzD,WAAW,KAAK,OAAO,WAAW,KAAK;AAE3C,QAAI,KAAK,aAAa,OAAO,oBAAoB,cAAc;AAC3D,YAAM,oBAAoB,UAAU,KAAK;AACzC,YAAM,oBAAoB,UAAU,KAAK;AAEzC,YAAM,gBAAgB,KAAK,aAAa;AACxC,YAAM,SAAS,KAAK,QAAQ,IAAI,cAAc,QAAQ,KAAK,QAAQ;AACnE,YAAM,SAAS,KAAK,SAAS,IAAI,cAAc,SAAS,KAAK,SAAS;AAEtE,YAAM,SAAS,oBAAoB;AACnC,YAAM,SAAS,oBAAoB;AAEnC,WAAK,aAAa,MAAM,IAAI;AAC5B,WAAK,aAAa,MAAM,IAAI;AAEV,WAAK,aAAa,OAAO;AAE3C,UAAI,CAAC,KAAK,aAAa,MAAM,sBAAsB,KAAK,aAAa,GAAG,UAAU;AAG9E,aAAK,aAAa,GAAG,SAAS,MAAM,QAAQ,GAAG,eAAe;AAC9D,aAAK,aAAa,GAAG,SAAS,MAAM,MAAM,GAAG,sBAAsB;AACnE,aAAK,aAAa,GAAG,SAAS,MAAM,OAAO;AAC3C,aAAK,aAAa,MAAM,qBAAqB;AAAA,MACjD,OAAO;AACH,aAAK,aAAa,GAAG,cAAc,SAAS,OAAO;AAAA,MACvD;AAEA,WAAK,aAAa,oBAAA;AAAA,IACtB;AAAA,EACJ;AACJ;"} \ No newline at end of file +{"version":3,"file":"EventHandler.js","sources":["../../src/magnify-glass/EventHandler.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - EventHandler (TypeScript)\n * \n * Handles keyboard and mouse events for the magnifying glass.\n */\n\nimport { isUserTyping } from '../shared/utils';\nimport { Logger } from '../shared/logger';\nimport { DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants';\nimport type { MagnifyGlass } from './MagnifyGlass';\n\n/**\n * Event Handler class.\n * Manages all keyboard and mouse event handling for the magnifier.\n */\nexport class EventHandler {\n magnifyGlass: MagnifyGlass;\n\n constructor(magnifyGlass: MagnifyGlass) {\n this.magnifyGlass = magnifyGlass;\n\n this.handleKeyDown = this.handleKeyDown.bind(this);\n this.handleKeyUp = this.handleKeyUp.bind(this);\n this.handleMouseMove = this.handleMouseMove.bind(this);\n this.handleResize = this.handleResize.bind(this);\n }\n\n /**\n * Attach all event listeners.\n */\n attachListeners(): void {\n document.addEventListener(\"keydown\", this.handleKeyDown);\n document.addEventListener(\"keyup\", this.handleKeyUp);\n document.addEventListener(\"mousemove\", this.handleMouseMove);\n window.addEventListener(\"resize\", this.handleResize);\n }\n\n /**\n * Detach all event listeners.\n */\n detachListeners(): void {\n document.removeEventListener(\"keydown\", this.handleKeyDown);\n document.removeEventListener(\"keyup\", this.handleKeyUp);\n document.removeEventListener(\"mousemove\", this.handleMouseMove);\n window.removeEventListener(\"resize\", this.handleResize);\n }\n\n /**\n * Handle window resize events.\n */\n handleResize(): void {\n if (this.magnifyGlass && this.magnifyGlass.ui) {\n this.magnifyGlass.ui.updateResponsivePosition();\n // Also update panel position if it exists\n const infoPanel = (window as any).infoPanelManager;\n if (infoPanel && infoPanel.positionManager) {\n // Determine if we should move it\n infoPanel.positionManager.positionPanel();\n }\n }\n }\n\n /**\n * Handle keydown events.\n */\n handleKeyDown(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n // Don't handle hotkeys if user is typing in an input field\n if (isUserTyping()) {\n return;\n }\n\n // Magnifier activation\n Logger.debug(`KeyDown: ${e.key} (config: ${config.activationKey}, active: ${state.active})`);\n if (e.key.toLowerCase() === config.activationKey &&\n (!config.altRequired || e.altKey)) {\n Logger.debug('Activation key matched!');\n if (config.alwaysActiveMode) {\n this.magnifyGlass.toggle();\n } else {\n if (!state.active) {\n this.magnifyGlass.toggle();\n }\n }\n }\n\n // Manual offset adjustment keys (only when magnifier is active)\n if (state.active) {\n let offsetChanged = false;\n const stepSize = e.shiftKey ? config.offsetStep * 5 : config.offsetStep;\n\n if (e.key === \"ArrowUp\") {\n config.offsetY -= stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowDown\") {\n config.offsetY += stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowLeft\") {\n config.offsetX -= stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowRight\") {\n config.offsetX += stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() &&\n (!config.altRequired || e.altKey)) {\n this.magnifyGlass.resetOffsets();\n offsetChanged = true;\n e.preventDefault();\n }\n\n // Toggle Follow Cursor Key\n if (e.key.toLowerCase() === config.toggleFollowCursorKey &&\n (!config.altRequired || e.altKey)) {\n config.followCursor = !config.followCursor;\n this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? 'ON' : 'OFF'}`);\n if (state.active && config.followCursor) {\n this.magnifyGlass.ui.positionGlass(\n this.magnifyGlass.lastKnownMousePosition.x,\n this.magnifyGlass.lastKnownMousePosition.y\n );\n }\n e.preventDefault();\n }\n\n // Toggle Glass Preview Key (G) - mimics the hover controls button\n if (e.key.toLowerCase() === config.toggleGlassPreviewKey &&\n (!config.altRequired || e.altKey)) {\n // Find and click the toggle-glass button in the info panel hover controls\n const toggleGlassBtn = document.querySelector('[data-action=\"toggle-glass\"]') as HTMLElement;\n if (toggleGlassBtn) {\n toggleGlassBtn.click();\n this.magnifyGlass.debugger.log(`Glass Preview Toggled (via button)`);\n } else {\n // Fallback: toggle via UI directly if button not found\n const glassDiv = this.magnifyGlass.ui.glassDiv;\n if (glassDiv) {\n const isVisible = glassDiv.style.display !== 'none' && glassDiv.style.opacity !== '0';\n this.magnifyGlass.ui.setPreviewVisibility(!isVisible);\n this.magnifyGlass.debugger.log(`Glass Preview Toggled (fallback): ${!isVisible ? 'ON' : 'OFF'}`);\n }\n }\n e.preventDefault();\n }\n\n if (offsetChanged) {\n config.saveOffsets();\n this.magnifyGlass.updateMagnifiedView();\n }\n\n // Pop-out tab toggle (Shift+P)\n if (e.shiftKey && e.key.toLowerCase() === 'p') {\n this.magnifyGlass.popOutManager.toggle();\n e.preventDefault();\n }\n }\n }\n\n /**\n * Handle keyup events.\n */\n handleKeyUp(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n if (!config.alwaysActiveMode &&\n (e.key.toLowerCase() === config.activationKey ||\n (config.altRequired && e.key === \"Alt\"))) {\n\n let shouldDeactivate = false;\n if (config.altRequired) {\n if (e.key === \"Alt\" || e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n } else {\n if (e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n }\n\n if (state.active && shouldDeactivate) {\n state.active = false;\n this.magnifyGlass.ui.hide();\n }\n }\n }\n\n /**\n * Handle mousemove events.\n */\n handleMouseMove(e: MouseEvent): void {\n // Always track mouse position\n this.magnifyGlass.lastKnownMousePosition.x = e.clientX;\n this.magnifyGlass.lastKnownMousePosition.y = e.clientY;\n\n if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const cssMouseXOnCanvas = e.clientX - rect.left;\n const cssMouseYOnCanvas = e.clientY - rect.top;\n\n // Check if the cursor is over the canvas element\n if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width &&\n cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) {\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY);\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n\n /**\n * Update the initial position when magnifier is activated.\n */\n updateInitialPosition(): void {\n if (!this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const clientX = this.magnifyGlass.lastKnownMousePosition.x;\n const clientY = this.magnifyGlass.lastKnownMousePosition.y;\n\n const isOverCanvas = clientX >= rect.left && clientX <= rect.right &&\n clientY >= rect.top && clientY <= rect.bottom;\n\n if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) {\n const cssMouseXOnCanvas = clientX - rect.left;\n const cssMouseYOnCanvas = clientY - rect.top;\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n const glassSize = this.magnifyGlass.config.glassSize;\n\n if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) {\n // First activation - position at top right of the window (Anchored)\n // Using style.right ensures it stays on the right even if window resizes before first usage\n this.magnifyGlass.ui.glassDiv.style.right = `${DEFAULT_PADDING}px`;\n this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.magnifyGlass.ui.glassDiv.style.left = 'auto'; // Ensure left is unset\n this.magnifyGlass.state.wasActivatedBefore = true;\n } else {\n this.magnifyGlass.ui.positionGlass(clientX, clientY);\n }\n\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/MagnifyGlass.js b/web/magnify-glass/MagnifyGlass.js index 791482c..85cee94 100644 --- a/web/magnify-glass/MagnifyGlass.js +++ b/web/magnify-glass/MagnifyGlass.js @@ -11,6 +11,7 @@ import { UiManager } from "./UiManager.js"; import { WebGLRenderer } from "./WebGLRenderer.js"; import { DebugManager } from "./DebugManager.js"; import { EventHandler } from "./EventHandler.js"; +import { PopOutManager } from "./PopOutManager.js"; class MagnifyGlass { constructor() { __publicField(this, "config"); @@ -19,6 +20,7 @@ class MagnifyGlass { __publicField(this, "renderer"); __publicField(this, "debugger"); __publicField(this, "eventHandler"); + __publicField(this, "popOutManager"); /** The LiteGraph canvas */ __publicField(this, "litegraphCanvas"); /** Last known mouse position for better initial positioning */ @@ -29,7 +31,13 @@ class MagnifyGlass { __publicField(this, "currentMediaElement"); this.config = new ConfigManager(); this.state = new MagnifierState(); - this.ui = new UiManager(this.config, this.state, () => this.toggle()); + this.popOutManager = new PopOutManager(); + this.ui = new UiManager( + this.config, + this.state, + () => this.toggle(), + () => this.popOutManager.toggle() + ); this.renderer = null; this.debugger = new DebugManager(this.config, this.state, this.ui); this.eventHandler = new EventHandler(this); @@ -104,6 +112,9 @@ class MagnifyGlass { this.renderer.render(this.litegraphCanvas); this.debugger.updateDebugView(); this.renderHtmlOverlays(); + if (this.popOutManager.isPopOutOpen() && this.ui.glassCanvas) { + this.popOutManager.sendFrame(this.ui.glassCanvas); + } this.state.isRenderScheduled = false; }); } @@ -330,6 +341,7 @@ class MagnifyGlass { */ cleanup() { 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 6ac9085..f791132 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 } from '../shared/constants';\nimport { registerGlassSettings } from '../shared/settings';\nimport { ConfigManager } from './ConfigManager';\nimport { MagnifierState } from './MagnifierState';\nimport { UiManager } from './UiManager';\nimport { WebGLRenderer } from './WebGLRenderer';\nimport { DebugManager } from './DebugManager';\nimport { EventHandler } from './EventHandler';\n\n// External ComfyUI globals\ndeclare const app: ComfyApp;\ndeclare const LiteGraph: unknown;\n\n/**\n * Main MagnifyGlass class.\n * Orchestrates all components and provides the main API.\n */\nexport class MagnifyGlass {\n config: ConfigManager;\n state: MagnifierState;\n ui: UiManager;\n renderer: WebGLRenderer | null;\n debugger: DebugManager;\n eventHandler: EventHandler;\n\n /** The LiteGraph canvas */\n litegraphCanvas: HTMLCanvasElement | null;\n\n /** Last known mouse position for better initial positioning */\n lastKnownMousePosition: { x: number; y: number };\n\n /** Whether we're currently over media (for info panel) */\n isOverMedia: boolean;\n\n /** Current media element under cursor */\n currentMediaElement: HTMLImageElement | HTMLVideoElement | null;\n\n constructor() {\n this.config = new ConfigManager();\n this.state = new MagnifierState();\n this.ui = new UiManager(this.config, this.state, () => this.toggle());\n this.renderer = null;\n this.debugger = new DebugManager(this.config, this.state, this.ui);\n this.eventHandler = new EventHandler(this);\n\n // The LiteGraph canvas\n this.litegraphCanvas = null;\n\n // Last known mouse position for better initial positioning\n this.lastKnownMousePosition = { x: 0, y: 0 };\n\n // Media tracking\n this.isOverMedia = false;\n this.currentMediaElement = null;\n }\n\n /**\n * Initialize the magnifying glass.\n */\n init(): void {\n if (typeof LiteGraph === 'undefined' || typeof app === 'undefined' || !app.canvas) {\n this.debugger.log(\"LiteGraph or app not ready, retrying in 100ms.\");\n setTimeout(() => this.init(), 100);\n return;\n }\n this.debugger.log(\"LiteGraph and app ready.\");\n\n // Load saved offsets first\n this.config.loadSavedOffsets();\n\n // Print detailed canvas information\n this.debugger.printCanvasInfo();\n\n // Create UI elements\n this.ui.createElements();\n\n // 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 // Attach event handlers\n this.eventHandler.attachListeners();\n\n // Register settings with ComfyUI\n registerGlassSettings(this);\n\n this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? 'Alt+' : ''}${this.config.activationKey.toUpperCase()} to activate.`);\n }\n\n /**\n * Toggle the magnifying glass active state.\n */\n toggle(): void {\n const state = this.state;\n const config = this.config;\n\n if (state.active) {\n state.active = false;\n this.ui.hide();\n } else {\n state.active = true;\n this.ui.show();\n // Update initial position if needed\n if (this.eventHandler) {\n this.eventHandler.updateInitialPosition();\n }\n }\n }\n\n /**\n * Update the magnified view.\n */\n updateMagnifiedView(): void {\n if (!this.state.active || !this.renderer || !this.litegraphCanvas) {\n return;\n }\n\n // Get canvas transformation info\n this.updateCanvasTransformation();\n\n // Calculate the source region\n this.calculateSourceRegion();\n\n // Schedule the rendering operation\n if (!this.state.isRenderScheduled) {\n this.state.isRenderScheduled = true;\n requestAnimationFrame(() => {\n if (!this.state.active || !this.renderer || !this.litegraphCanvas) {\n this.state.isRenderScheduled = false;\n return;\n }\n // Render the magnified view\n this.renderer.render(this.litegraphCanvas);\n\n // Update debug visualization\n this.debugger.updateDebugView();\n\n // Render HTML overlays\n this.renderHtmlOverlays();\n\n this.state.isRenderScheduled = false;\n });\n }\n }\n\n /**\n * Update canvas transformation state.\n */\n updateCanvasTransformation(): void {\n this.state.canvasScale = 1.0;\n this.state.canvasOffsetX = 0;\n this.state.canvasOffsetY = 0;\n\n if (app?.canvas) {\n const ds = (app.canvas as any).ds;\n if (ds) {\n if (typeof ds.scale === 'number') {\n this.state.canvasScale = ds.scale;\n }\n\n if (ds.offset) {\n this.state.canvasOffsetX = ds.offset[0] || 0;\n this.state.canvasOffsetY = ds.offset[1] || 0;\n }\n }\n }\n }\n\n /**\n * Calculate the source region for magnification.\n */\n calculateSourceRegion(): void {\n const cursorPixelX = this.state.x;\n const cursorPixelY = this.state.y;\n const canvasScale = this.state.canvasScale;\n const canvasOffsetX = this.state.canvasOffsetX;\n const canvasOffsetY = this.state.canvasOffsetY;\n\n if (canvasScale === 0) return;\n\n // Convert cursor canvas pixels to LiteGraph graph coordinates\n const cursorGraphX = (cursorPixelX - canvasOffsetX) / canvasScale;\n const cursorGraphY = (cursorPixelY - canvasOffsetY) / canvasScale;\n\n // Apply manual offset\n const targetGraphCenterX = cursorGraphX + this.config.offsetX;\n const targetGraphCenterY = cursorGraphY + this.config.offsetY;\n\n // Calculate source dimensions in graph units\n const sourceGraphWidth = (this.config.glassSize / this.config.zoomFactor) / canvasScale;\n const sourceGraphHeight = (this.config.glassSize / this.config.zoomFactor) / canvasScale;\n\n // Calculate source top-left corner in graph coordinates\n const sourceGraphX = targetGraphCenterX - (sourceGraphWidth / 2);\n const sourceGraphY = targetGraphCenterY - (sourceGraphHeight / 2);\n\n // Convert back to canvas pixel coordinates\n this.state.sourceX = (sourceGraphX * canvasScale) + canvasOffsetX;\n this.state.sourceY = (sourceGraphY * canvasScale) + canvasOffsetY;\n this.state.sourceWidth = sourceGraphWidth * canvasScale;\n this.state.sourceHeight = sourceGraphHeight * canvasScale;\n }\n\n /**\n * Render HTML overlays for text and media in the magnified view.\n */\n renderHtmlOverlays(): void {\n const graph = (app as any).graph;\n if (!this.state.active || !this.ui.htmlOverlayContainer || !graph || !this.litegraphCanvas) {\n if (this.ui.htmlOverlayContainer) this.ui.htmlOverlayContainer.innerHTML = '';\n return;\n }\n\n this.ui.htmlOverlayContainer.innerHTML = '';\n\n const magnifyRect: Rectangle = {\n x: this.state.sourceX,\n y: this.state.sourceY,\n width: this.state.sourceWidth,\n height: this.state.sourceHeight\n };\n\n const nodes: ComfyNode[] = graph._nodes;\n if (!nodes) return;\n\n for (const node of nodes) {\n const widgets = (node as any).widgets;\n if (!widgets) continue;\n\n for (const widget of widgets) {\n let isTextElement = false;\n let isVideoElement = false;\n let isImageElement = false;\n let elementToProcess: HTMLElement | null = null;\n\n if (widget.element) {\n const element = widget.element as HTMLElement;\n if (widget.type === \"text\" || widget.type === \"string\" || element.tagName === 'TEXTAREA') {\n isTextElement = true;\n elementToProcess = element;\n } else if (element.tagName === 'VIDEO') {\n isVideoElement = true;\n elementToProcess = element;\n } else if (element.tagName === 'IMG') {\n isImageElement = true;\n elementToProcess = element;\n } else {\n const potentialVideo = element.querySelector('video');\n if (potentialVideo) {\n isVideoElement = true;\n elementToProcess = potentialVideo;\n } else {\n const potentialImage = element.querySelector('img');\n if (potentialImage) {\n isImageElement = true;\n elementToProcess = potentialImage;\n }\n }\n }\n }\n\n if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) {\n const widgetRect = elementToProcess.getBoundingClientRect();\n const canvasRect = this.litegraphCanvas!.getBoundingClientRect();\n\n const canvasToViewportScaleX = canvasRect.width > 0 ? (this.litegraphCanvas!.width / canvasRect.width) : 1;\n const canvasToViewportScaleY = canvasRect.height > 0 ? (this.litegraphCanvas!.height / canvasRect.height) : 1;\n\n const widgetCanvasX = (widgetRect.left - canvasRect.left) * canvasToViewportScaleX;\n const widgetCanvasY = (widgetRect.top - canvasRect.top) * canvasToViewportScaleY;\n const widgetCanvasWidth = widgetRect.width * canvasToViewportScaleX;\n const widgetCanvasHeight = widgetRect.height * canvasToViewportScaleY;\n\n const widgetSourceRect: Rectangle = {\n x: widgetCanvasX,\n y: widgetCanvasY,\n width: widgetCanvasWidth,\n height: widgetCanvasHeight\n };\n\n if (rectsOverlap(magnifyRect, widgetSourceRect)) {\n const clonedElement = elementToProcess.cloneNode(true) as HTMLElement;\n clonedElement.style.position = 'absolute';\n clonedElement.style.pointerEvents = 'none';\n\n if (isTextElement) {\n clonedElement.style.backgroundColor = elementToProcess.style.backgroundColor || '#222';\n clonedElement.style.color = elementToProcess.style.color || '#DDD';\n clonedElement.style.border = elementToProcess.style.border || '1px solid #555';\n (clonedElement as HTMLInputElement).disabled = true;\n } else if (isVideoElement) {\n const video = clonedElement as HTMLVideoElement;\n const originalVideo = elementToProcess as HTMLVideoElement;\n video.src = originalVideo.src;\n video.autoplay = originalVideo.autoplay;\n video.loop = originalVideo.loop;\n video.preload = originalVideo.preload;\n video.crossOrigin = originalVideo.crossOrigin;\n video.muted = true;\n if (!originalVideo.paused) {\n video.play().catch(e => console.warn(\"Magnify Glass: Cloned video play failed\", e));\n }\n video.currentTime = originalVideo.currentTime;\n } else if (isImageElement) {\n const img = clonedElement as HTMLImageElement;\n const originalImg = elementToProcess as HTMLImageElement;\n img.src = originalImg.src;\n img.alt = originalImg.alt;\n }\n\n const relativeX = widgetSourceRect.x - magnifyRect.x;\n const relativeY = widgetSourceRect.y - magnifyRect.y;\n const magnifiedX = relativeX * this.config.zoomFactor;\n const magnifiedY = relativeY * this.config.zoomFactor;\n\n clonedElement.style.left = `${magnifiedX}px`;\n clonedElement.style.top = `${magnifiedY}px`;\n clonedElement.style.width = `${widgetSourceRect.width}px`;\n clonedElement.style.height = `${widgetSourceRect.height}px`;\n clonedElement.style.transformOrigin = 'top left';\n clonedElement.style.transform = `scale(${this.config.zoomFactor})`;\n\n if (isTextElement && clonedElement.style.fontSize) {\n const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize);\n clonedElement.style.fontSize = `${originalFontSize}px`;\n }\n\n this.ui.htmlOverlayContainer!.appendChild(clonedElement);\n }\n }\n }\n }\n }\n\n /**\n * Update config from current settings values.\n */\n updateConfigFromSettings(): void {\n this.config.loadSettings();\n }\n\n /**\n * Apply UI changes based on current config.\n */\n applyUiChanges(): void {\n this.ui.applyStyles();\n\n if (this.renderer?.gl) {\n this.renderer.updateViewport();\n }\n }\n\n\n\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.ui.cleanup();\n }\n}\n"],"names":[],"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 } from '../shared/constants';\nimport { registerGlassSettings } from '../shared/settings';\nimport { ConfigManager } from './ConfigManager';\nimport { MagnifierState } from './MagnifierState';\nimport { UiManager } from './UiManager';\nimport { WebGLRenderer } from './WebGLRenderer';\nimport { DebugManager } from './DebugManager';\nimport { EventHandler } from './EventHandler';\nimport { PopOutManager } from './PopOutManager';\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\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 () => this.popOutManager.toggle()\n );\n this.renderer = null;\n this.debugger = new DebugManager(this.config, this.state, this.ui);\n this.eventHandler = new EventHandler(this);\n\n // The LiteGraph canvas\n this.litegraphCanvas = null;\n\n // Last known mouse position for better initial positioning\n this.lastKnownMousePosition = { x: 0, y: 0 };\n\n // Media tracking\n this.isOverMedia = false;\n this.currentMediaElement = null;\n }\n\n /**\n * Initialize the magnifying glass.\n */\n init(): void {\n if (typeof LiteGraph === 'undefined' || typeof app === 'undefined' || !app.canvas) {\n this.debugger.log(\"LiteGraph or app not ready, retrying in 100ms.\");\n setTimeout(() => this.init(), 100);\n return;\n }\n this.debugger.log(\"LiteGraph and app ready.\");\n\n // Load saved offsets first\n this.config.loadSavedOffsets();\n\n // Print detailed canvas information\n this.debugger.printCanvasInfo();\n\n // Create UI elements\n this.ui.createElements();\n\n // 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 // Attach event handlers\n this.eventHandler.attachListeners();\n\n // Register settings with ComfyUI\n registerGlassSettings(this);\n\n this.debugger.log(`Initialized (WebGL) with Smart Input Detection. Press ${this.config.altRequired ? 'Alt+' : ''}${this.config.activationKey.toUpperCase()} to activate.`);\n }\n\n /**\n * Toggle the magnifying glass active state.\n */\n toggle(): void {\n const state = this.state;\n const config = this.config;\n\n if (state.active) {\n state.active = false;\n this.ui.hide();\n } else {\n state.active = true;\n this.ui.show();\n // Update initial position if needed\n if (this.eventHandler) {\n this.eventHandler.updateInitialPosition();\n }\n }\n }\n\n /**\n * Update the magnified view.\n */\n updateMagnifiedView(): void {\n if (!this.state.active || !this.renderer || !this.litegraphCanvas) {\n return;\n }\n\n // Get canvas transformation info\n this.updateCanvasTransformation();\n\n // Calculate the source region\n this.calculateSourceRegion();\n\n // Schedule the rendering operation\n if (!this.state.isRenderScheduled) {\n this.state.isRenderScheduled = true;\n requestAnimationFrame(() => {\n if (!this.state.active || !this.renderer || !this.litegraphCanvas) {\n this.state.isRenderScheduled = false;\n return;\n }\n // Render the magnified view\n this.renderer.render(this.litegraphCanvas);\n\n // Update debug visualization\n this.debugger.updateDebugView();\n\n // Render HTML overlays\n this.renderHtmlOverlays();\n\n // 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\n // Convert cursor canvas pixels to LiteGraph graph coordinates\n const cursorGraphX = (cursorPixelX - canvasOffsetX) / canvasScale;\n const cursorGraphY = (cursorPixelY - canvasOffsetY) / canvasScale;\n\n // Apply manual offset\n const targetGraphCenterX = cursorGraphX + this.config.offsetX;\n const targetGraphCenterY = cursorGraphY + this.config.offsetY;\n\n // Calculate source dimensions in graph units\n const sourceGraphWidth = (this.config.glassSize / this.config.zoomFactor) / canvasScale;\n const sourceGraphHeight = (this.config.glassSize / this.config.zoomFactor) / canvasScale;\n\n // Calculate source top-left corner in graph coordinates\n const sourceGraphX = targetGraphCenterX - (sourceGraphWidth / 2);\n const sourceGraphY = targetGraphCenterY - (sourceGraphHeight / 2);\n\n // Convert back to canvas pixel coordinates\n this.state.sourceX = (sourceGraphX * canvasScale) + canvasOffsetX;\n this.state.sourceY = (sourceGraphY * canvasScale) + canvasOffsetY;\n this.state.sourceWidth = sourceGraphWidth * canvasScale;\n this.state.sourceHeight = sourceGraphHeight * canvasScale;\n }\n\n /**\n * Render HTML overlays for text and media in the magnified view.\n */\n renderHtmlOverlays(): void {\n const graph = (app as any).graph;\n if (!this.state.active || !this.ui.htmlOverlayContainer || !graph || !this.litegraphCanvas) {\n if (this.ui.htmlOverlayContainer) this.ui.htmlOverlayContainer.innerHTML = '';\n return;\n }\n\n this.ui.htmlOverlayContainer.innerHTML = '';\n\n const magnifyRect: Rectangle = {\n x: this.state.sourceX,\n y: this.state.sourceY,\n width: this.state.sourceWidth,\n height: this.state.sourceHeight\n };\n\n const nodes: ComfyNode[] = graph._nodes;\n if (!nodes) return;\n\n for (const node of nodes) {\n const widgets = (node as any).widgets;\n if (!widgets) continue;\n\n for (const widget of widgets) {\n let isTextElement = false;\n let isVideoElement = false;\n let isImageElement = false;\n let elementToProcess: HTMLElement | null = null;\n\n if (widget.element) {\n const element = widget.element as HTMLElement;\n if (widget.type === \"text\" || widget.type === \"string\" || element.tagName === 'TEXTAREA') {\n isTextElement = true;\n elementToProcess = element;\n } else if (element.tagName === 'VIDEO') {\n isVideoElement = true;\n elementToProcess = element;\n } else if (element.tagName === 'IMG') {\n isImageElement = true;\n elementToProcess = element;\n } else {\n const potentialVideo = element.querySelector('video');\n if (potentialVideo) {\n isVideoElement = true;\n elementToProcess = potentialVideo;\n } else {\n const potentialImage = element.querySelector('img');\n if (potentialImage) {\n isImageElement = true;\n elementToProcess = potentialImage;\n }\n }\n }\n }\n\n if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) {\n const widgetRect = elementToProcess.getBoundingClientRect();\n const canvasRect = this.litegraphCanvas!.getBoundingClientRect();\n\n const canvasToViewportScaleX = canvasRect.width > 0 ? (this.litegraphCanvas!.width / canvasRect.width) : 1;\n const canvasToViewportScaleY = canvasRect.height > 0 ? (this.litegraphCanvas!.height / canvasRect.height) : 1;\n\n const widgetCanvasX = (widgetRect.left - canvasRect.left) * canvasToViewportScaleX;\n const widgetCanvasY = (widgetRect.top - canvasRect.top) * canvasToViewportScaleY;\n const widgetCanvasWidth = widgetRect.width * canvasToViewportScaleX;\n const widgetCanvasHeight = widgetRect.height * canvasToViewportScaleY;\n\n const widgetSourceRect: Rectangle = {\n x: widgetCanvasX,\n y: widgetCanvasY,\n width: widgetCanvasWidth,\n height: widgetCanvasHeight\n };\n\n if (rectsOverlap(magnifyRect, widgetSourceRect)) {\n const clonedElement = elementToProcess.cloneNode(true) as HTMLElement;\n clonedElement.style.position = 'absolute';\n clonedElement.style.pointerEvents = 'none';\n\n if (isTextElement) {\n clonedElement.style.backgroundColor = elementToProcess.style.backgroundColor || '#222';\n clonedElement.style.color = elementToProcess.style.color || '#DDD';\n clonedElement.style.border = elementToProcess.style.border || '1px solid #555';\n (clonedElement as HTMLInputElement).disabled = true;\n } else if (isVideoElement) {\n const video = clonedElement as HTMLVideoElement;\n const originalVideo = elementToProcess as HTMLVideoElement;\n video.src = originalVideo.src;\n video.autoplay = originalVideo.autoplay;\n video.loop = originalVideo.loop;\n video.preload = originalVideo.preload;\n video.crossOrigin = originalVideo.crossOrigin;\n video.muted = true;\n if (!originalVideo.paused) {\n video.play().catch(e => console.warn(\"Magnify Glass: Cloned video play failed\", e));\n }\n video.currentTime = originalVideo.currentTime;\n } else if (isImageElement) {\n const img = clonedElement as HTMLImageElement;\n const originalImg = elementToProcess as HTMLImageElement;\n img.src = originalImg.src;\n img.alt = originalImg.alt;\n }\n\n const relativeX = widgetSourceRect.x - magnifyRect.x;\n const relativeY = widgetSourceRect.y - magnifyRect.y;\n const magnifiedX = relativeX * this.config.zoomFactor;\n const magnifiedY = relativeY * this.config.zoomFactor;\n\n clonedElement.style.left = `${magnifiedX}px`;\n clonedElement.style.top = `${magnifiedY}px`;\n clonedElement.style.width = `${widgetSourceRect.width}px`;\n clonedElement.style.height = `${widgetSourceRect.height}px`;\n clonedElement.style.transformOrigin = 'top left';\n clonedElement.style.transform = `scale(${this.config.zoomFactor})`;\n\n if (isTextElement && clonedElement.style.fontSize) {\n const originalFontSize = parseFloat(window.getComputedStyle(elementToProcess).fontSize);\n clonedElement.style.fontSize = `${originalFontSize}px`;\n }\n\n this.ui.htmlOverlayContainer!.appendChild(clonedElement);\n }\n }\n }\n }\n }\n\n /**\n * Update config from current settings values.\n */\n updateConfigFromSettings(): void {\n this.config.loadSettings();\n }\n\n /**\n * Apply UI changes based on current config.\n */\n applyUiChanges(): void {\n this.ui.applyStyles();\n\n if (this.renderer?.gl) {\n this.renderer.updateViewport();\n }\n }\n\n\n\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"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/PopOutManager.js b/web/magnify-glass/PopOutManager.js new file mode 100644 index 0000000..e932f24 --- /dev/null +++ b/web/magnify-glass/PopOutManager.js @@ -0,0 +1,192 @@ +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 { BROADCAST_CHANNEL_NAME } from "../shared/constants.js"; +import { Logger } from "../shared/logger.js"; +class PopOutManager { + constructor() { + __publicField(this, "channel", null); + __publicField(this, "isOpen", false); + __publicField(this, "lastPongTime", 0); + __publicField(this, "pingInterval", null); + __publicField(this, "viewerUrl"); + /** Throttle frame sending to ~30fps */ + __publicField(this, "lastFrameTime", 0); + __publicField(this, "FRAME_INTERVAL", 33); + // ~30fps + /** Connection timeout (ms) */ + __publicField(this, "CONNECTION_TIMEOUT", 5e3); + this.viewerUrl = this.getViewerUrl(); + this.initChannel(); + } + /** + * Get the URL for the pop-out viewer page. + */ + getViewerUrl() { + const scripts = document.querySelectorAll('script[src*="magnify"]'); + if (scripts.length > 0) { + const src = scripts[0].src; + const baseUrl = src.substring(0, src.lastIndexOf("/")); + return `${baseUrl}/popout-viewer.html`; + } + return "/extensions/comfyui-magnifyglass/popout-viewer.html"; + } + /** + * Initialize the BroadcastChannel. + */ + initChannel() { + try { + this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME); + this.channel.onmessage = (event) => { + this.handleMessage(event.data); + }; + Logger.debug("[PopOut] BroadcastChannel initialized"); + } catch (e) { + Logger.error("[PopOut] Failed to create BroadcastChannel:", e); + } + } + /** + * Handle incoming messages from the viewer tab. + */ + handleMessage(message) { + switch (message.type) { + case "pong": + this.lastPongTime = Date.now(); + if (!this.isOpen) { + this.isOpen = true; + Logger.debug("[PopOut] Viewer tab connected"); + } + break; + case "close": + this.isOpen = false; + this.stopPing(); + Logger.debug("[PopOut] Viewer tab closed"); + break; + } + } + /** + * Open the pop-out viewer in a new tab. + */ + open() { + if (this.isOpen) { + Logger.debug("[PopOut] Tab already open"); + return; + } + const newTab = window.open(this.viewerUrl, "_blank"); + if (!newTab) { + Logger.error("[PopOut] Failed to open new tab - popup may be blocked"); + return; + } + this.startPing(); + Logger.debug("[PopOut] Opening viewer tab..."); + } + /** + * Close the pop-out viewer tab. + */ + close() { + if (!this.isOpen) return; + this.sendMessage({ type: "close" }); + this.isOpen = false; + this.stopPing(); + Logger.debug("[PopOut] Sent close message to viewer"); + } + /** + * Toggle the pop-out state. + */ + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + /** + * Check if the pop-out tab is currently open. + */ + isPopOutOpen() { + return this.isOpen; + } + /** + * Send a frame to the pop-out viewer. + * Throttled to ~30fps for performance. + * @param canvas - The source canvas to send + */ + sendFrame(canvas) { + if (!this.isOpen || !this.channel) return; + const now = Date.now(); + if (now - this.lastFrameTime < this.FRAME_INTERVAL) { + return; + } + this.lastFrameTime = now; + try { + const dataUrl = canvas.toDataURL("image/png"); + this.sendMessage({ + type: "frame", + data: dataUrl, + timestamp: now + }); + } catch (e) { + Logger.error("[PopOut] Failed to send frame:", e); + } + } + /** + * Send configuration to the pop-out viewer. + */ + sendConfig(config) { + if (!this.channel) return; + this.sendMessage({ + type: "config", + data: config + }); + } + /** + * Send a message through the BroadcastChannel. + */ + sendMessage(message) { + if (!this.channel) return; + try { + this.channel.postMessage(message); + } catch (e) { + Logger.error("[PopOut] Failed to send message:", e); + } + } + /** + * Start pinging to check connection. + */ + startPing() { + this.stopPing(); + this.pingInterval = setInterval(() => { + this.sendMessage({ type: "ping", timestamp: Date.now() }); + if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) { + Logger.debug("[PopOut] Connection timeout, marking as closed"); + this.isOpen = false; + this.stopPing(); + } + }, 1e3); + } + /** + * Stop pinging. + */ + stopPing() { + if (this.pingInterval) { + clearInterval(this.pingInterval); + this.pingInterval = null; + } + } + /** + * Cleanup resources. + */ + cleanup() { + this.close(); + this.stopPing(); + if (this.channel) { + this.channel.close(); + this.channel = null; + } + Logger.debug("[PopOut] Cleaned up"); + } +} +export { + PopOutManager +}; +//# sourceMappingURL=PopOutManager.js.map diff --git a/web/magnify-glass/PopOutManager.js.map b/web/magnify-glass/PopOutManager.js.map new file mode 100644 index 0000000..6b06949 --- /dev/null +++ b/web/magnify-glass/PopOutManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"PopOutManager.js","sources":["../../src/magnify-glass/PopOutManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - PopOutManager (TypeScript)\n * \n * Manages pop-out tab functionality for multi-monitor viewing.\n * Uses BroadcastChannel API for efficient cross-tab communication.\n */\n\nimport { BROADCAST_CHANNEL_NAME } from '../shared/constants';\nimport { Logger } from '../shared/logger';\n\n/**\n * Message types for BroadcastChannel communication\n */\ninterface PopOutMessage {\n type: 'frame' | 'config' | 'close' | 'ping' | 'pong';\n data?: string | PopOutConfig;\n timestamp?: number;\n}\n\ninterface PopOutConfig {\n glassSize: number;\n borderColor: string;\n borderWidth: number;\n glassShape: string;\n}\n\n/**\n * PopOutManager class.\n * Handles opening, closing, and syncing with an external browser tab.\n */\nexport class PopOutManager {\n private channel: BroadcastChannel | null = null;\n private isOpen: boolean = false;\n private lastPongTime: number = 0;\n private pingInterval: number | null = null;\n private viewerUrl: string;\n\n /** Throttle frame sending to ~30fps */\n private lastFrameTime: number = 0;\n private readonly FRAME_INTERVAL = 33; // ~30fps\n\n /** Connection timeout (ms) */\n private readonly CONNECTION_TIMEOUT = 5000;\n\n constructor() {\n // Get the viewer URL relative to the extension\n // The viewer HTML will be in the web directory\n this.viewerUrl = this.getViewerUrl();\n this.initChannel();\n }\n\n /**\n * Get the URL for the pop-out viewer page.\n */\n private getViewerUrl(): string {\n // Find the extension's base URL from the loaded scripts\n const scripts = document.querySelectorAll('script[src*=\"magnify\"]');\n if (scripts.length > 0) {\n const src = (scripts[0] as HTMLScriptElement).src;\n const baseUrl = src.substring(0, src.lastIndexOf('/'));\n return `${baseUrl}/popout-viewer.html`;\n }\n // Fallback: assume standard ComfyUI extension path\n return '/extensions/comfyui-magnifyglass/popout-viewer.html';\n }\n\n /**\n * Initialize the BroadcastChannel.\n */\n private initChannel(): void {\n try {\n this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);\n this.channel.onmessage = (event: MessageEvent) => {\n this.handleMessage(event.data);\n };\n Logger.debug('[PopOut] BroadcastChannel initialized');\n } catch (e) {\n Logger.error('[PopOut] Failed to create BroadcastChannel:', e);\n }\n }\n\n /**\n * Handle incoming messages from the viewer tab.\n */\n private handleMessage(message: PopOutMessage): void {\n switch (message.type) {\n case 'pong':\n this.lastPongTime = Date.now();\n if (!this.isOpen) {\n this.isOpen = true;\n Logger.debug('[PopOut] Viewer tab connected');\n }\n break;\n case 'close':\n this.isOpen = false;\n this.stopPing();\n Logger.debug('[PopOut] Viewer tab closed');\n break;\n }\n }\n\n /**\n * Open the pop-out viewer in a new tab.\n */\n open(): void {\n if (this.isOpen) {\n Logger.debug('[PopOut] Tab already open');\n return;\n }\n\n // Open new tab\n const newTab = window.open(this.viewerUrl, '_blank');\n if (!newTab) {\n Logger.error('[PopOut] Failed to open new tab - popup may be blocked');\n return;\n }\n\n // Start pinging to detect when viewer is ready\n this.startPing();\n Logger.debug('[PopOut] Opening viewer tab...');\n }\n\n /**\n * Close the pop-out viewer tab.\n */\n close(): void {\n if (!this.isOpen) return;\n\n this.sendMessage({ type: 'close' });\n this.isOpen = false;\n this.stopPing();\n Logger.debug('[PopOut] Sent close message to viewer');\n }\n\n /**\n * Toggle the pop-out state.\n */\n toggle(): void {\n if (this.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /**\n * Check if the pop-out tab is currently open.\n */\n isPopOutOpen(): boolean {\n return this.isOpen;\n }\n\n /**\n * Send a frame to the pop-out viewer.\n * Throttled to ~30fps for performance.\n * @param canvas - The source canvas to send\n */\n sendFrame(canvas: HTMLCanvasElement): void {\n if (!this.isOpen || !this.channel) return;\n\n // Throttle frame rate\n const now = Date.now();\n if (now - this.lastFrameTime < this.FRAME_INTERVAL) {\n return;\n }\n this.lastFrameTime = now;\n\n try {\n // Convert canvas to data URL (efficient for small canvases)\n const dataUrl = canvas.toDataURL('image/png');\n this.sendMessage({\n type: 'frame',\n data: dataUrl,\n timestamp: now\n });\n } catch (e) {\n Logger.error('[PopOut] Failed to send frame:', e);\n }\n }\n\n /**\n * Send configuration to the pop-out viewer.\n */\n sendConfig(config: PopOutConfig): void {\n if (!this.channel) return;\n\n this.sendMessage({\n type: 'config',\n data: config\n });\n }\n\n /**\n * Send a message through the BroadcastChannel.\n */\n private sendMessage(message: PopOutMessage): void {\n if (!this.channel) return;\n\n try {\n this.channel.postMessage(message);\n } catch (e) {\n Logger.error('[PopOut] Failed to send message:', e);\n }\n }\n\n /**\n * Start pinging to check connection.\n */\n private startPing(): void {\n this.stopPing();\n this.pingInterval = setInterval(() => {\n this.sendMessage({ type: 'ping', timestamp: Date.now() });\n\n // Check for connection timeout\n if (this.isOpen && Date.now() - this.lastPongTime > this.CONNECTION_TIMEOUT) {\n Logger.debug('[PopOut] Connection timeout, marking as closed');\n this.isOpen = false;\n this.stopPing();\n }\n }, 1000);\n }\n\n /**\n * Stop pinging.\n */\n private stopPing(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Cleanup resources.\n */\n cleanup(): void {\n this.close();\n this.stopPing();\n if (this.channel) {\n this.channel.close();\n this.channel = null;\n }\n Logger.debug('[PopOut] Cleaned up');\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/UiManager.js b/web/magnify-glass/UiManager.js index 226d8e9..0e14408 100644 --- a/web/magnify-glass/UiManager.js +++ b/web/magnify-glass/UiManager.js @@ -5,7 +5,7 @@ import { Z_INDEX, DEFAULT_PADDING } from "../shared/constants.js"; import { Icons } from "../shared/icons.js"; import { Logger } from "../shared/logger.js"; class UiManager { - constructor(config, state, onToggle) { + constructor(config, state, onToggle, onPopOut) { __publicField(this, "config"); __publicField(this, "state"); __publicField(this, "glassDiv"); @@ -13,7 +13,9 @@ class UiManager { __publicField(this, "debugCanvas"); __publicField(this, "debugCtx"); __publicField(this, "htmlOverlayContainer"); + __publicField(this, "popOutButton"); __publicField(this, "onToggle"); + __publicField(this, "onPopOut"); this.config = config; this.state = state; this.glassDiv = null; @@ -21,7 +23,9 @@ class UiManager { this.debugCanvas = null; this.debugCtx = null; this.htmlOverlayContainer = null; + this.popOutButton = null; this.onToggle = onToggle; + this.onPopOut = onPopOut; } /** * Create all DOM elements for the magnifying glass. @@ -60,6 +64,59 @@ class UiManager { overflow: hidden; `; this.glassDiv.appendChild(this.htmlOverlayContainer); + this.popOutButton = document.createElement("button"); + this.popOutButton.id = "comfyui-magnify-popout-btn"; + this.popOutButton.title = "Open in New Tab (Shift+P)"; + this.popOutButton.innerHTML = Icons.externalLink; + this.popOutButton.style.cssText = ` + position: absolute; + top: 6px; + right: 6px; + width: 24px; + height: 24px; + padding: 4px; + border: none; + border-radius: 4px; + background: rgba(0, 0, 0, 0.4); + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + pointer-events: auto; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease; + z-index: 10; + `; + this.popOutButton.addEventListener("click", (e) => { + e.stopPropagation(); + if (this.onPopOut) { + this.onPopOut(); + } + }); + this.popOutButton.addEventListener("mouseenter", () => { + if (this.popOutButton) { + this.popOutButton.style.background = "rgba(99, 102, 241, 0.6)"; + this.popOutButton.style.color = "#fff"; + } + }); + this.popOutButton.addEventListener("mouseleave", () => { + if (this.popOutButton) { + this.popOutButton.style.background = "rgba(0, 0, 0, 0.4)"; + this.popOutButton.style.color = "rgba(255, 255, 255, 0.7)"; + } + }); + this.glassDiv.appendChild(this.popOutButton); + this.glassDiv.addEventListener("mouseenter", () => { + if (this.popOutButton) { + this.popOutButton.style.opacity = "1"; + } + }); + this.glassDiv.addEventListener("mouseleave", () => { + if (this.popOutButton) { + this.popOutButton.style.opacity = "0"; + } + }); document.body.appendChild(this.glassDiv); if (this.config.debugMode) { this.createDebugCanvas(); diff --git a/web/magnify-glass/UiManager.js.map b/web/magnify-glass/UiManager.js.map index 66a895c..b6bb260 100644 --- a/web/magnify-glass/UiManager.js.map +++ b/web/magnify-glass/UiManager.js.map @@ -1 +1 @@ -{"version":3,"file":"UiManager.js","sources":["../../src/magnify-glass/UiManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - UiManager (TypeScript)\n * \n * Handles creation and management of DOM elements for the magnifying glass.\n */\n\nimport { Z_INDEX, DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants';\nimport { Icons } from '../shared/icons';\nimport { Logger } from '../shared/logger';\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\n\n/**\n * UI Manager class.\n * Manages all DOM elements for the magnifying glass.\n */\nexport class UiManager {\n config: ConfigManager;\n state: MagnifierState;\n glassDiv: HTMLDivElement | null;\n glassCanvas: HTMLCanvasElement | null;\n debugCanvas: HTMLCanvasElement | null;\n debugCtx: CanvasRenderingContext2D | null;\n htmlOverlayContainer: HTMLDivElement | null;\n onToggle: (() => void) | undefined;\n\n constructor(config: ConfigManager, state: MagnifierState, onToggle?: () => void) {\n this.config = config;\n this.state = state;\n this.glassDiv = null;\n this.glassCanvas = null;\n this.debugCanvas = null;\n this.debugCtx = null;\n this.htmlOverlayContainer = null;\n this.onToggle = onToggle;\n }\n\n /**\n * Create all DOM elements for the magnifying glass.\n */\n createElements(): void {\n // Create magnifying glass container\n this.glassDiv = document.createElement(\"div\");\n this.glassDiv.id = \"comfyui-magnify-glass\";\n this.glassDiv.style.cssText = `\n position: absolute;\n box-sizing: border-box;\n width: ${this.config.glassSize}px;\n height: ${this.config.glassSize}px;\n border-radius: ${this.config.glassShape === \"Circle\" ? \"50%\" : \"0px\"};\n border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : 'none'};\n overflow: hidden;\n pointer-events: none;\n z-index: ${Z_INDEX.GLASS};\n display: none;\n box-shadow: 0 5px 15px rgba(0,0,0,0.3);\n background-color: rgba(255,255,255,0.1);\n `;\n\n // Create WebGL canvas for the magnifying glass\n this.glassCanvas = document.createElement(\"canvas\");\n this.glassCanvas.width = this.config.glassSize;\n this.glassCanvas.height = this.config.glassSize;\n this.glassCanvas.id = \"comfyui-magnify-canvas\";\n this.glassDiv.appendChild(this.glassCanvas);\n\n // Create HTML overlay container\n this.htmlOverlayContainer = document.createElement(\"div\");\n this.htmlOverlayContainer.id = \"comfyui-magnify-html-overlay\";\n this.htmlOverlayContainer.style.cssText = `\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n overflow: hidden; \n `;\n this.glassDiv.appendChild(this.htmlOverlayContainer);\n\n document.body.appendChild(this.glassDiv);\n\n // Create debug canvas if debug mode is enabled\n if (this.config.debugMode) {\n this.createDebugCanvas();\n }\n\n // Inject menu button\n this.injectMenuButton();\n }\n\n /**\n * Create the debug canvas overlay.\n */\n createDebugCanvas(): void {\n this.debugCanvas = document.createElement(\"canvas\");\n this.debugCanvas.id = \"comfyui-magnify-debug\";\n this.debugCanvas.width = 400;\n this.debugCanvas.height = 350;\n this.debugCanvas.style.cssText = `\n position: fixed;\n top: 10px;\n right: 10px;\n background: rgba(0,0,0,0.7);\n border: 1px solid #fff;\n z-index: ${Z_INDEX.DEBUG};\n pointer-events: none;\n color: white;\n font-family: monospace;\n display: none;\n `;\n document.body.appendChild(this.debugCanvas);\n this.debugCtx = this.debugCanvas.getContext('2d');\n }\n\n /**\n * Show the magnifying glass.\n */\n show(): void {\n if (this.glassDiv) {\n this.glassDiv.style.display = 'block';\n // We do NOT modify opacity here, allowing it to be controlled independently\n }\n if (this.config.debugMode && this.debugCanvas) {\n this.debugCanvas.style.display = \"block\";\n }\n }\n\n /**\n * Set the visual visibility of the glass preview (opacity).\n * This allows the tool to remain \"Active\" (tracking mouse) but invisible,\n * so that the Inspector Panel can be used in \"Inspector Only\" mode.\n */\n setPreviewVisibility(visible: boolean): void {\n if (this.glassDiv) {\n this.glassDiv.style.opacity = visible ? '1' : '0';\n // Disable pointer events when hidden to be safe, though usually they are none anyway\n // this.glassDiv.style.pointerEvents = visible ? 'none' : 'none'; \n }\n }\n\n /**\n * Hide the magnifying glass.\n */\n hide(): void {\n if (this.glassDiv) {\n this.glassDiv.style.display = 'none';\n }\n if (this.config.debugMode && this.debugCanvas) {\n this.debugCanvas.style.display = \"none\";\n }\n // Clear HTML overlays when hiding\n if (this.htmlOverlayContainer) {\n this.htmlOverlayContainer.innerHTML = '';\n }\n }\n\n /**\n * Position the glass relative to cursor.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n /**\n * Position the glass relative to cursor.\n * Uses dynamic anchoring (Left/Right, Top/Bottom) based on screen quadrant.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n positionGlass(clientX: number, clientY: number): void {\n if (!this.config.followCursor || !this.glassDiv) return;\n\n const glassSize = this.config.glassSize;\n const offsetAmount = DEFAULT_PADDING;\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n // Determine base coordinates (top-left based)\n let targetX: number;\n let targetY: number;\n\n // Calculate standard Top-Left based positions first\n switch (this.config.glassPosition) {\n case \"Top\":\n targetX = clientX - (glassSize / 2);\n targetY = clientY - glassSize - offsetAmount;\n break;\n case \"Bottom\":\n targetX = clientX - (glassSize / 2);\n targetY = clientY + offsetAmount;\n break;\n case \"Left\":\n targetX = clientX - glassSize - offsetAmount;\n targetY = clientY - (glassSize / 2);\n break;\n case \"Right\":\n targetX = clientX + offsetAmount;\n targetY = clientY - (glassSize / 2);\n break;\n case \"Top-Left\":\n targetX = clientX - glassSize - offsetAmount;\n targetY = clientY - glassSize - offsetAmount;\n break;\n case \"Top-Right\":\n targetX = clientX + offsetAmount;\n targetY = clientY - glassSize - offsetAmount;\n break;\n case \"Bottom-Left\":\n targetX = clientX - glassSize - offsetAmount;\n targetY = clientY + offsetAmount;\n break;\n case \"Bottom-Right\":\n targetX = clientX + offsetAmount;\n targetY = clientY + offsetAmount;\n break;\n default:\n targetX = clientX - (glassSize / 2);\n targetY = clientY + offsetAmount;\n break;\n }\n\n // Apply Dynamic Anchoring based on resulting position's center relative to screen\n const glassCenterX = targetX + (glassSize / 2);\n const glassCenterY = targetY + (glassSize / 2);\n\n // Horizontal Anchor\n if (glassCenterX > vw / 2) {\n // Anchor Right\n const rightPos = vw - (targetX + glassSize);\n this.glassDiv.style.right = `${rightPos}px`;\n this.glassDiv.style.left = 'auto';\n } else {\n // Anchor Left\n this.glassDiv.style.left = `${targetX}px`;\n this.glassDiv.style.right = 'auto';\n }\n\n // Vertical Anchor\n if (glassCenterY > vh / 2) {\n // Anchor Bottom\n const bottomPos = vh - (targetY + glassSize);\n this.glassDiv.style.bottom = `${bottomPos}px`;\n this.glassDiv.style.top = 'auto';\n } else {\n // Anchor Top\n this.glassDiv.style.top = `${targetY}px`;\n this.glassDiv.style.bottom = 'auto';\n }\n\n // We run a lightweight clamp to ensure it doesn't bleed offscreen during movement\n // (Though the anchor logic implicitly handles resize, dragging near edges might need clamping)\n this.adjustForBoundaries();\n }\n\n /**\n * Adjust glass position to stay within viewport boundaries.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n /**\n * Adjust glass position to stay within viewport boundaries.\n * Respects the current anchor (Left/Right, Top/Bottom).\n */\n adjustForBoundaries(): void {\n if (!this.glassDiv) return;\n\n const glassSize = this.config.glassSize;\n const padding = DEFAULT_PADDING;\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n // Check if anchored Right or Left\n const isAnchoredRight = this.glassDiv.style.right !== 'auto' && this.glassDiv.style.right !== '';\n\n if (isAnchoredRight) {\n let currentRight = parseFloat(this.glassDiv.style.right) || padding;\n // Clamp Right: max(padding, min(current, vw - size - padding))\n // Ensure it doesn't go off left side (vw - size - padding)\n currentRight = Math.max(padding, Math.min(currentRight, vw - glassSize - padding));\n this.glassDiv.style.right = `${currentRight}px`;\n } else {\n // Anchor Left\n let currentLeft = parseFloat(this.glassDiv.style.left) || padding;\n currentLeft = Math.max(padding, Math.min(currentLeft, vw - glassSize - padding));\n this.glassDiv.style.left = `${currentLeft}px`;\n }\n\n // Check if anchored Bottom or Top\n const isAnchoredBottom = this.glassDiv.style.bottom !== 'auto' && this.glassDiv.style.bottom !== '';\n\n if (isAnchoredBottom) {\n let currentBottom = parseFloat(this.glassDiv.style.bottom) || padding;\n currentBottom = Math.max(padding, Math.min(currentBottom, vh - glassSize - padding));\n this.glassDiv.style.bottom = `${currentBottom}px`;\n } else {\n // Anchor Top\n let currentTop = parseFloat(this.glassDiv.style.top) || padding;\n currentTop = Math.max(padding, Math.min(currentTop, vh - glassSize - padding));\n this.glassDiv.style.top = `${currentTop}px`;\n }\n }\n\n /**\n * Update position safely on resize.\n * Since we use dynamic anchoring in positionGlass, CSS handles most resize cases.\n * This method ensures we verify boundaries (clamping) and fix anchors if we cross thresholds drastically.\n */\n updateResponsivePosition(): void {\n if (!this.glassDiv) return;\n // Re-run boundary adjustments to clamp if resize pushed us offscreen\n this.adjustForBoundaries();\n }\n\n /**\n * Apply current config to UI elements.\n */\n applyStyles(): void {\n if (this.glassDiv) {\n this.glassDiv.style.width = `${this.config.glassSize}px`;\n this.glassDiv.style.height = `${this.config.glassSize}px`;\n this.glassDiv.style.border = this.config.borderEnabled\n ? `${this.config.borderWidth}px solid ${this.config.borderColor}`\n : 'none';\n\n // Reset clip-path and set default border-radius\n this.glassDiv.style.clipPath = 'none';\n this.glassDiv.style.borderRadius = '0px';\n\n switch (this.config.glassShape) {\n case \"Circle\":\n this.glassDiv.style.borderRadius = \"50%\";\n break;\n case \"Square\":\n // borderRadius is already '0px'\n break;\n case \"Rounded Square\":\n this.glassDiv.style.borderRadius = \"20%\";\n break;\n default:\n this.glassDiv.style.borderRadius = \"50%\";\n break;\n }\n }\n\n if (this.glassCanvas) {\n this.glassCanvas.width = this.config.glassSize;\n this.glassCanvas.height = this.config.glassSize;\n }\n\n // Handle debug canvas show/hide\n if (this.config.debugMode) {\n if (!this.debugCanvas) this.createDebugCanvas();\n if (this.state.active && this.debugCanvas) this.debugCanvas.style.display = \"block\";\n } else {\n if (this.debugCanvas) this.debugCanvas.style.display = \"none\";\n }\n }\n\n /**\n * Cleanup DOM elements.\n */\n cleanup(): void {\n if (this.glassDiv) this.glassDiv.remove();\n if (this.debugCanvas) this.debugCanvas.remove();\n // Remove menu button if needed (though hard to track references here without saving it)\n const btn = document.querySelector('.magnify-toggle-btn');\n if (btn) btn.remove();\n }\n\n /**\n * Inject a quick toggle button into the ComfyUI menu.\n */\n /**\n * Inject a quick toggle button into the ComfyUI menu.\n */\n injectMenuButton(): void {\n // Stop after 30 seconds\n const stopTime = Date.now() + 30000;\n\n // Define check function\n const attemptInjection = () => {\n const buttons = Array.from(document.querySelectorAll('button'));\n\n // Try to find an anchor button (Minimap or Links)\n // Priority 1: Minimap\n let anchorBtn = buttons.find(b => {\n const title = (b.title || '').toLowerCase();\n const aria = (b.getAttribute('aria-label') || '').toLowerCase();\n const text = (b.textContent || '').toLowerCase();\n return (title.includes('map') && !title.includes('open')) ||\n (aria.includes('map') && !aria.includes('open')) ||\n (text.includes('map'));\n });\n\n // Priority 2: Link Visibility\n if (!anchorBtn) {\n anchorBtn = buttons.find(b => {\n const title = (b.title || '').toLowerCase();\n const aria = (b.getAttribute('aria-label') || '').toLowerCase();\n return title.includes('link') || aria.includes('link');\n });\n }\n\n if (anchorBtn && anchorBtn.parentElement) {\n // Determine success BEFORE creating elements to allow early return if duplicate\n if (anchorBtn.parentElement.querySelector('.magnify-toggle-btn')) return true;\n\n Logger.debug('Found menu anchor:', anchorBtn.title || anchorBtn.getAttribute('aria-label') || 'Unknown Button');\n\n const btn = document.createElement('button');\n // Copy all classes to match theme/library (e.g. p-button, comfy-btn, etc)\n btn.className = anchorBtn.className + ' magnify-toggle-btn';\n\n // Copy key styles that might define shape/size\n const computed = window.getComputedStyle(anchorBtn);\n btn.style.height = computed.height;\n btn.style.minHeight = computed.minHeight;\n // btn.style.width = computed.width; // Don't copy width, let it adapt\n\n btn.title = \"Toggle Magnify Glass\";\n btn.innerHTML = Icons.magnifyGlass;\n\n // Ensure specific display props\n btn.style.display = \"inline-flex\";\n btn.style.alignItems = \"center\";\n btn.style.justifyContent = \"center\";\n btn.style.padding = \"0 8px\";\n btn.style.cursor = \"pointer\";\n btn.style.border = anchorBtn.style.border || computed.border;\n btn.style.borderRadius = anchorBtn.style.borderRadius || computed.borderRadius;\n // Use cssText if available for background/color specific overrides\n if (!btn.style.background) btn.style.background = computed.background;\n if (!btn.style.color) btn.style.color = computed.color;\n\n // Add active state tracking\n btn.addEventListener('click', () => {\n if (this.onToggle) {\n this.onToggle();\n // Try to apply common active classes found in modern UI frameworks\n btn.classList.toggle('active');\n btn.classList.toggle('p-highlight'); // PrimeVue\n btn.classList.toggle('selected');\n }\n });\n\n // Insert logic:\n const isMinimap = (anchorBtn.title || '').toLowerCase().includes('map') ||\n (anchorBtn.getAttribute('aria-label') || '').toLowerCase().includes('map');\n\n if (isMinimap) {\n // Insert AFTER Minimap\n if (anchorBtn.nextSibling) {\n anchorBtn.parentElement.insertBefore(btn, anchorBtn.nextSibling);\n } else {\n anchorBtn.parentElement.appendChild(btn);\n }\n } else {\n // It's likely links, Insert BEFORE\n anchorBtn.parentElement.insertBefore(btn, anchorBtn);\n }\n\n Logger.debug('Menu toggle button injected successfully');\n return true;\n }\n return false;\n };\n\n // Run immediately first\n if (attemptInjection()) return;\n\n // Then poll rapidly\n const checkForMenu = setInterval(() => {\n if (Date.now() > stopTime) {\n console.warn(\"[MagnifyGlass] Menu injection timed out. Found buttons:\",\n Array.from(document.querySelectorAll('button')).map(b => b.title || b.getAttribute('aria-label') || b.textContent || b.className).slice(0, 5));\n clearInterval(checkForMenu);\n return;\n }\n\n if (attemptInjection()) {\n clearInterval(checkForMenu);\n }\n }, 100);\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"UiManager.js","sources":["../../src/magnify-glass/UiManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - UiManager (TypeScript)\n * \n * Handles creation and management of DOM elements for the magnifying glass.\n */\n\nimport { Z_INDEX, DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants';\nimport { Icons } from '../shared/icons';\nimport { Logger } from '../shared/logger';\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\n\n/**\n * UI Manager class.\n * Manages all DOM elements for the magnifying glass.\n */\nexport class UiManager {\n config: ConfigManager;\n state: MagnifierState;\n glassDiv: HTMLDivElement | null;\n glassCanvas: HTMLCanvasElement | null;\n debugCanvas: HTMLCanvasElement | null;\n debugCtx: CanvasRenderingContext2D | null;\n htmlOverlayContainer: HTMLDivElement | null;\n popOutButton: HTMLButtonElement | null;\n onToggle: (() => void) | undefined;\n onPopOut: (() => void) | undefined;\n\n constructor(config: ConfigManager, state: MagnifierState, onToggle?: () => void, onPopOut?: () => void) {\n this.config = config;\n this.state = state;\n this.glassDiv = null;\n this.glassCanvas = null;\n this.debugCanvas = null;\n this.debugCtx = null;\n this.htmlOverlayContainer = null;\n this.popOutButton = null;\n this.onToggle = onToggle;\n this.onPopOut = onPopOut;\n }\n\n /**\n * Create all DOM elements for the magnifying glass.\n */\n createElements(): void {\n // Create magnifying glass container\n this.glassDiv = document.createElement(\"div\");\n this.glassDiv.id = \"comfyui-magnify-glass\";\n this.glassDiv.style.cssText = `\n position: absolute;\n box-sizing: border-box;\n width: ${this.config.glassSize}px;\n height: ${this.config.glassSize}px;\n border-radius: ${this.config.glassShape === \"Circle\" ? \"50%\" : \"0px\"};\n border: ${this.config.borderEnabled ? `${this.config.borderWidth}px solid ${this.config.borderColor}` : 'none'};\n overflow: hidden;\n pointer-events: none;\n z-index: ${Z_INDEX.GLASS};\n display: none;\n box-shadow: 0 5px 15px rgba(0,0,0,0.3);\n background-color: rgba(255,255,255,0.1);\n `;\n\n // Create WebGL canvas for the magnifying glass\n this.glassCanvas = document.createElement(\"canvas\");\n this.glassCanvas.width = this.config.glassSize;\n this.glassCanvas.height = this.config.glassSize;\n this.glassCanvas.id = \"comfyui-magnify-canvas\";\n this.glassDiv.appendChild(this.glassCanvas);\n\n // Create HTML overlay container\n this.htmlOverlayContainer = document.createElement(\"div\");\n this.htmlOverlayContainer.id = \"comfyui-magnify-html-overlay\";\n this.htmlOverlayContainer.style.cssText = `\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n overflow: hidden; \n `;\n this.glassDiv.appendChild(this.htmlOverlayContainer);\n\n // Create pop-out button\n this.popOutButton = document.createElement(\"button\");\n this.popOutButton.id = \"comfyui-magnify-popout-btn\";\n this.popOutButton.title = \"Open in New Tab (Shift+P)\";\n this.popOutButton.innerHTML = Icons.externalLink;\n this.popOutButton.style.cssText = `\n position: absolute;\n top: 6px;\n right: 6px;\n width: 24px;\n height: 24px;\n padding: 4px;\n border: none;\n border-radius: 4px;\n background: rgba(0, 0, 0, 0.4);\n color: rgba(255, 255, 255, 0.7);\n cursor: pointer;\n pointer-events: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease;\n z-index: 10;\n `;\n this.popOutButton.addEventListener('click', (e) => {\n e.stopPropagation();\n if (this.onPopOut) {\n this.onPopOut();\n }\n });\n this.popOutButton.addEventListener('mouseenter', () => {\n if (this.popOutButton) {\n this.popOutButton.style.background = 'rgba(99, 102, 241, 0.6)';\n this.popOutButton.style.color = '#fff';\n }\n });\n this.popOutButton.addEventListener('mouseleave', () => {\n if (this.popOutButton) {\n this.popOutButton.style.background = 'rgba(0, 0, 0, 0.4)';\n this.popOutButton.style.color = 'rgba(255, 255, 255, 0.7)';\n }\n });\n this.glassDiv.appendChild(this.popOutButton);\n\n // Show pop-out button on glass hover\n this.glassDiv.addEventListener('mouseenter', () => {\n if (this.popOutButton) {\n this.popOutButton.style.opacity = '1';\n }\n });\n this.glassDiv.addEventListener('mouseleave', () => {\n if (this.popOutButton) {\n this.popOutButton.style.opacity = '0';\n }\n });\n\n document.body.appendChild(this.glassDiv);\n\n // Create debug canvas if debug mode is enabled\n if (this.config.debugMode) {\n this.createDebugCanvas();\n }\n\n // Inject menu button\n this.injectMenuButton();\n }\n\n /**\n * Create the debug canvas overlay.\n */\n createDebugCanvas(): void {\n this.debugCanvas = document.createElement(\"canvas\");\n this.debugCanvas.id = \"comfyui-magnify-debug\";\n this.debugCanvas.width = 400;\n this.debugCanvas.height = 350;\n this.debugCanvas.style.cssText = `\n position: fixed;\n top: 10px;\n right: 10px;\n background: rgba(0,0,0,0.7);\n border: 1px solid #fff;\n z-index: ${Z_INDEX.DEBUG};\n pointer-events: none;\n color: white;\n font-family: monospace;\n display: none;\n `;\n document.body.appendChild(this.debugCanvas);\n this.debugCtx = this.debugCanvas.getContext('2d');\n }\n\n /**\n * Show the magnifying glass.\n */\n show(): void {\n if (this.glassDiv) {\n this.glassDiv.style.display = 'block';\n // We do NOT modify opacity here, allowing it to be controlled independently\n }\n if (this.config.debugMode && this.debugCanvas) {\n this.debugCanvas.style.display = \"block\";\n }\n }\n\n /**\n * Set the visual visibility of the glass preview (opacity).\n * This allows the tool to remain \"Active\" (tracking mouse) but invisible,\n * so that the Inspector Panel can be used in \"Inspector Only\" mode.\n */\n setPreviewVisibility(visible: boolean): void {\n if (this.glassDiv) {\n this.glassDiv.style.opacity = visible ? '1' : '0';\n // Disable pointer events when hidden to be safe, though usually they are none anyway\n // this.glassDiv.style.pointerEvents = visible ? 'none' : 'none'; \n }\n }\n\n /**\n * Hide the magnifying glass.\n */\n hide(): void {\n if (this.glassDiv) {\n this.glassDiv.style.display = 'none';\n }\n if (this.config.debugMode && this.debugCanvas) {\n this.debugCanvas.style.display = \"none\";\n }\n // Clear HTML overlays when hiding\n if (this.htmlOverlayContainer) {\n this.htmlOverlayContainer.innerHTML = '';\n }\n }\n\n /**\n * Position the glass relative to cursor.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n /**\n * Position the glass relative to cursor.\n * Uses dynamic anchoring (Left/Right, Top/Bottom) based on screen quadrant.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n positionGlass(clientX: number, clientY: number): void {\n if (!this.config.followCursor || !this.glassDiv) return;\n\n const glassSize = this.config.glassSize;\n const offsetAmount = DEFAULT_PADDING;\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n // Determine base coordinates (top-left based)\n let targetX: number;\n let targetY: number;\n\n // Calculate standard Top-Left based positions first\n switch (this.config.glassPosition) {\n case \"Top\":\n targetX = clientX - (glassSize / 2);\n targetY = clientY - glassSize - offsetAmount;\n break;\n case \"Bottom\":\n targetX = clientX - (glassSize / 2);\n targetY = clientY + offsetAmount;\n break;\n case \"Left\":\n targetX = clientX - glassSize - offsetAmount;\n targetY = clientY - (glassSize / 2);\n break;\n case \"Right\":\n targetX = clientX + offsetAmount;\n targetY = clientY - (glassSize / 2);\n break;\n case \"Top-Left\":\n targetX = clientX - glassSize - offsetAmount;\n targetY = clientY - glassSize - offsetAmount;\n break;\n case \"Top-Right\":\n targetX = clientX + offsetAmount;\n targetY = clientY - glassSize - offsetAmount;\n break;\n case \"Bottom-Left\":\n targetX = clientX - glassSize - offsetAmount;\n targetY = clientY + offsetAmount;\n break;\n case \"Bottom-Right\":\n targetX = clientX + offsetAmount;\n targetY = clientY + offsetAmount;\n break;\n default:\n targetX = clientX - (glassSize / 2);\n targetY = clientY + offsetAmount;\n break;\n }\n\n // Apply Dynamic Anchoring based on resulting position's center relative to screen\n const glassCenterX = targetX + (glassSize / 2);\n const glassCenterY = targetY + (glassSize / 2);\n\n // Horizontal Anchor\n if (glassCenterX > vw / 2) {\n // Anchor Right\n const rightPos = vw - (targetX + glassSize);\n this.glassDiv.style.right = `${rightPos}px`;\n this.glassDiv.style.left = 'auto';\n } else {\n // Anchor Left\n this.glassDiv.style.left = `${targetX}px`;\n this.glassDiv.style.right = 'auto';\n }\n\n // Vertical Anchor\n if (glassCenterY > vh / 2) {\n // Anchor Bottom\n const bottomPos = vh - (targetY + glassSize);\n this.glassDiv.style.bottom = `${bottomPos}px`;\n this.glassDiv.style.top = 'auto';\n } else {\n // Anchor Top\n this.glassDiv.style.top = `${targetY}px`;\n this.glassDiv.style.bottom = 'auto';\n }\n\n // We run a lightweight clamp to ensure it doesn't bleed offscreen during movement\n // (Though the anchor logic implicitly handles resize, dragging near edges might need clamping)\n this.adjustForBoundaries();\n }\n\n /**\n * Adjust glass position to stay within viewport boundaries.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n /**\n * Adjust glass position to stay within viewport boundaries.\n * Respects the current anchor (Left/Right, Top/Bottom).\n */\n adjustForBoundaries(): void {\n if (!this.glassDiv) return;\n\n const glassSize = this.config.glassSize;\n const padding = DEFAULT_PADDING;\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n // Check if anchored Right or Left\n const isAnchoredRight = this.glassDiv.style.right !== 'auto' && this.glassDiv.style.right !== '';\n\n if (isAnchoredRight) {\n let currentRight = parseFloat(this.glassDiv.style.right) || padding;\n // Clamp Right: max(padding, min(current, vw - size - padding))\n // Ensure it doesn't go off left side (vw - size - padding)\n currentRight = Math.max(padding, Math.min(currentRight, vw - glassSize - padding));\n this.glassDiv.style.right = `${currentRight}px`;\n } else {\n // Anchor Left\n let currentLeft = parseFloat(this.glassDiv.style.left) || padding;\n currentLeft = Math.max(padding, Math.min(currentLeft, vw - glassSize - padding));\n this.glassDiv.style.left = `${currentLeft}px`;\n }\n\n // Check if anchored Bottom or Top\n const isAnchoredBottom = this.glassDiv.style.bottom !== 'auto' && this.glassDiv.style.bottom !== '';\n\n if (isAnchoredBottom) {\n let currentBottom = parseFloat(this.glassDiv.style.bottom) || padding;\n currentBottom = Math.max(padding, Math.min(currentBottom, vh - glassSize - padding));\n this.glassDiv.style.bottom = `${currentBottom}px`;\n } else {\n // Anchor Top\n let currentTop = parseFloat(this.glassDiv.style.top) || padding;\n currentTop = Math.max(padding, Math.min(currentTop, vh - glassSize - padding));\n this.glassDiv.style.top = `${currentTop}px`;\n }\n }\n\n /**\n * Update position safely on resize.\n * Since we use dynamic anchoring in positionGlass, CSS handles most resize cases.\n * This method ensures we verify boundaries (clamping) and fix anchors if we cross thresholds drastically.\n */\n updateResponsivePosition(): void {\n if (!this.glassDiv) return;\n // Re-run boundary adjustments to clamp if resize pushed us offscreen\n this.adjustForBoundaries();\n }\n\n /**\n * Apply current config to UI elements.\n */\n applyStyles(): void {\n if (this.glassDiv) {\n this.glassDiv.style.width = `${this.config.glassSize}px`;\n this.glassDiv.style.height = `${this.config.glassSize}px`;\n this.glassDiv.style.border = this.config.borderEnabled\n ? `${this.config.borderWidth}px solid ${this.config.borderColor}`\n : 'none';\n\n // Reset clip-path and set default border-radius\n this.glassDiv.style.clipPath = 'none';\n this.glassDiv.style.borderRadius = '0px';\n\n switch (this.config.glassShape) {\n case \"Circle\":\n this.glassDiv.style.borderRadius = \"50%\";\n break;\n case \"Square\":\n // borderRadius is already '0px'\n break;\n case \"Rounded Square\":\n this.glassDiv.style.borderRadius = \"20%\";\n break;\n default:\n this.glassDiv.style.borderRadius = \"50%\";\n break;\n }\n }\n\n if (this.glassCanvas) {\n this.glassCanvas.width = this.config.glassSize;\n this.glassCanvas.height = this.config.glassSize;\n }\n\n // Handle debug canvas show/hide\n if (this.config.debugMode) {\n if (!this.debugCanvas) this.createDebugCanvas();\n if (this.state.active && this.debugCanvas) this.debugCanvas.style.display = \"block\";\n } else {\n if (this.debugCanvas) this.debugCanvas.style.display = \"none\";\n }\n }\n\n /**\n * Cleanup DOM elements.\n */\n cleanup(): void {\n if (this.glassDiv) this.glassDiv.remove();\n if (this.debugCanvas) this.debugCanvas.remove();\n // Remove menu button if needed (though hard to track references here without saving it)\n const btn = document.querySelector('.magnify-toggle-btn');\n if (btn) btn.remove();\n }\n\n /**\n * Inject a quick toggle button into the ComfyUI menu.\n */\n /**\n * Inject a quick toggle button into the ComfyUI menu.\n */\n injectMenuButton(): void {\n // Stop after 30 seconds\n const stopTime = Date.now() + 30000;\n\n // Define check function\n const attemptInjection = () => {\n const buttons = Array.from(document.querySelectorAll('button'));\n\n // Try to find an anchor button (Minimap or Links)\n // Priority 1: Minimap\n let anchorBtn = buttons.find(b => {\n const title = (b.title || '').toLowerCase();\n const aria = (b.getAttribute('aria-label') || '').toLowerCase();\n const text = (b.textContent || '').toLowerCase();\n return (title.includes('map') && !title.includes('open')) ||\n (aria.includes('map') && !aria.includes('open')) ||\n (text.includes('map'));\n });\n\n // Priority 2: Link Visibility\n if (!anchorBtn) {\n anchorBtn = buttons.find(b => {\n const title = (b.title || '').toLowerCase();\n const aria = (b.getAttribute('aria-label') || '').toLowerCase();\n return title.includes('link') || aria.includes('link');\n });\n }\n\n if (anchorBtn && anchorBtn.parentElement) {\n // Determine success BEFORE creating elements to allow early return if duplicate\n if (anchorBtn.parentElement.querySelector('.magnify-toggle-btn')) return true;\n\n Logger.debug('Found menu anchor:', anchorBtn.title || anchorBtn.getAttribute('aria-label') || 'Unknown Button');\n\n const btn = document.createElement('button');\n // Copy all classes to match theme/library (e.g. p-button, comfy-btn, etc)\n btn.className = anchorBtn.className + ' magnify-toggle-btn';\n\n // Copy key styles that might define shape/size\n const computed = window.getComputedStyle(anchorBtn);\n btn.style.height = computed.height;\n btn.style.minHeight = computed.minHeight;\n // btn.style.width = computed.width; // Don't copy width, let it adapt\n\n btn.title = \"Toggle Magnify Glass\";\n btn.innerHTML = Icons.magnifyGlass;\n\n // Ensure specific display props\n btn.style.display = \"inline-flex\";\n btn.style.alignItems = \"center\";\n btn.style.justifyContent = \"center\";\n btn.style.padding = \"0 8px\";\n btn.style.cursor = \"pointer\";\n btn.style.border = anchorBtn.style.border || computed.border;\n btn.style.borderRadius = anchorBtn.style.borderRadius || computed.borderRadius;\n // Use cssText if available for background/color specific overrides\n if (!btn.style.background) btn.style.background = computed.background;\n if (!btn.style.color) btn.style.color = computed.color;\n\n // Add active state tracking\n btn.addEventListener('click', () => {\n if (this.onToggle) {\n this.onToggle();\n // Try to apply common active classes found in modern UI frameworks\n btn.classList.toggle('active');\n btn.classList.toggle('p-highlight'); // PrimeVue\n btn.classList.toggle('selected');\n }\n });\n\n // Insert logic:\n const isMinimap = (anchorBtn.title || '').toLowerCase().includes('map') ||\n (anchorBtn.getAttribute('aria-label') || '').toLowerCase().includes('map');\n\n if (isMinimap) {\n // Insert AFTER Minimap\n if (anchorBtn.nextSibling) {\n anchorBtn.parentElement.insertBefore(btn, anchorBtn.nextSibling);\n } else {\n anchorBtn.parentElement.appendChild(btn);\n }\n } else {\n // It's likely links, Insert BEFORE\n anchorBtn.parentElement.insertBefore(btn, anchorBtn);\n }\n\n Logger.debug('Menu toggle button injected successfully');\n return true;\n }\n return false;\n };\n\n // Run immediately first\n if (attemptInjection()) return;\n\n // Then poll rapidly\n const checkForMenu = setInterval(() => {\n if (Date.now() > stopTime) {\n console.warn(\"[MagnifyGlass] Menu injection timed out. Found buttons:\",\n Array.from(document.querySelectorAll('button')).map(b => b.title || b.getAttribute('aria-label') || b.textContent || b.className).slice(0, 5));\n clearInterval(checkForMenu);\n return;\n }\n\n if (attemptInjection()) {\n clearInterval(checkForMenu);\n }\n }, 100);\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/popout-viewer.html b/web/popout-viewer.html new file mode 100644 index 0000000..7e31bea --- /dev/null +++ b/web/popout-viewer.html @@ -0,0 +1,293 @@ + + + + + + 🔍 MagnifyGlass Viewer + + + +
+
+ + + + + MagnifyGlass Viewer +
+
+ + Connecting... +
+
+ +
+ +
+ 🔍 +

Not Connected

+

Activate MagnifyGlass in ComfyUI to start viewing

+
+
+ + + + diff --git a/web/shared/constants.js b/web/shared/constants.js index bd933d1..7b0f73a 100644 --- a/web/shared/constants.js +++ b/web/shared/constants.js @@ -40,8 +40,10 @@ const STORAGE_KEYS = { OFFSET_X: "comfyui_magnify_offset_x", OFFSET_Y: "comfyui_magnify_offset_y" }; +const BROADCAST_CHANNEL_NAME = "comfyui-magnifyglass-popout"; export { ACTIVATION_KEYS, + BROADCAST_CHANNEL_NAME, DEFAULT_BORDER_COLOR, DEFAULT_BORDER_WIDTH, DEFAULT_GLASS_SIZE, diff --git a/web/shared/constants.js.map b/web/shared/constants.js.map index d03447c..eb5e623 100644 --- a/web/shared/constants.js.map +++ b/web/shared/constants.js.map @@ -1 +1 @@ -{"version":3,"file":"constants.js","sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Shared Constants\n * \n * Configuration constants shared between magnify glass and info panel modules.\n */\n\n// Glass positioning options\nexport const GLASS_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\",\n \"Top-Left\",\n \"Top-Right\",\n \"Bottom-Left\",\n \"Bottom-Right\"\n] as const;\n\nexport type GlassPosition = typeof GLASS_POSITIONS[number];\n\n// Glass shape options\nexport const GLASS_SHAPES = [\n \"Circle\",\n \"Square\",\n \"Rounded Square\"\n] as const;\n\nexport type GlassShape = typeof GLASS_SHAPES[number];\n\n// Texture filtering modes\nexport const TEXTURE_FILTERS = [\n \"Linear\",\n \"Nearest\"\n] as const;\n\nexport type TextureFilter = typeof TEXTURE_FILTERS[number];\n\n// Info panel positions relative to glass\nexport const PANEL_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\"\n] as const;\n\nexport type PanelPosition = typeof PANEL_POSITIONS[number];\n\n// Available keys for settings\nexport const ACTIVATION_KEYS = [\"x\", \"z\", \"m\", \"q\", \"v\", \"c\"] as const;\nexport const RESET_KEYS = [\"r\", \"o\", \"p\", \"k\", \"l\"] as const;\nexport const TOGGLE_FOLLOW_KEYS = [\"f\", \"g\", \"h\", \"j\", \"k\"] as const;\n\n// UI Constants\nexport const DEFAULT_PADDING = 4;\nexport const DEFAULT_GLASS_Y_OFFSET = 110; // Starting Y position offset (below top bar with 3px gap)\nexport const DEFAULT_GLASS_SIZE = 300;\nexport const DEFAULT_ZOOM_FACTOR = 300; // 3x as percentage\nexport const DEFAULT_BORDER_WIDTH = 1;\nexport const DEFAULT_BORDER_COLOR = \"#6b7280\";\nexport const DEFAULT_OFFSET_STEP = 5;\n\n// Panel Constants\nexport const DEFAULT_PANEL_WIDTH = 300;\nexport const DEFAULT_PANEL_MAX_HEIGHT = 300;\nexport const DEFAULT_PANEL_OPACITY = 100;\n\n// Z-Index hierarchy\nexport const Z_INDEX = {\n GLASS: 98999,\n DEBUG: 99000,\n PANEL: 99999,\n CONTROLS: 99998\n} as const;\n\n// Animation durations (ms)\nexport const ANIMATION = {\n PANEL_TRANSITION: 300,\n AUTO_COLLAPSE_DELAY: 1500,\n THEME_DETECTION_INTERVAL: 2000\n} as const;\n\n// LocalStorage keys\nexport const STORAGE_KEYS = {\n OFFSET_X: 'comfyui_magnify_offset_x',\n OFFSET_Y: 'comfyui_magnify_offset_y'\n} as const;\n"],"names":[],"mappings":"AAOO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,eAAe;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AACJ;AAaO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,kBAAkB,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AACrD,MAAM,aAAa,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAC3C,MAAM,qBAAqB,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAGnD,MAAM,kBAAkB;AACxB,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAG5B,MAAM,sBAAsB;AAC5B,MAAM,2BAA2B;AACjC,MAAM,wBAAwB;AAG9B,MAAM,UAAU;AAAA,EACnB,OAAO;AAAA,EACP,OAAO;AAGX;AAUO,MAAM,eAAe;AAAA,EACxB,UAAU;AAAA,EACV,UAAU;AACd;"} \ No newline at end of file +{"version":3,"file":"constants.js","sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Shared Constants\n * \n * Configuration constants shared between magnify glass and info panel modules.\n */\n\n// Glass positioning options\nexport const GLASS_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\",\n \"Top-Left\",\n \"Top-Right\",\n \"Bottom-Left\",\n \"Bottom-Right\"\n] as const;\n\nexport type GlassPosition = typeof GLASS_POSITIONS[number];\n\n// Glass shape options\nexport const GLASS_SHAPES = [\n \"Circle\",\n \"Square\",\n \"Rounded Square\"\n] as const;\n\nexport type GlassShape = typeof GLASS_SHAPES[number];\n\n// Texture filtering modes\nexport const TEXTURE_FILTERS = [\n \"Linear\",\n \"Nearest\"\n] as const;\n\nexport type TextureFilter = typeof TEXTURE_FILTERS[number];\n\n// Info panel positions relative to glass\nexport const PANEL_POSITIONS = [\n \"Top\",\n \"Bottom\",\n \"Left\",\n \"Right\"\n] as const;\n\nexport type PanelPosition = typeof PANEL_POSITIONS[number];\n\n// Available keys for settings\nexport const ACTIVATION_KEYS = [\"x\", \"z\", \"m\", \"q\", \"v\", \"c\"] as const;\nexport const RESET_KEYS = [\"r\", \"o\", \"p\", \"k\", \"l\"] as const;\nexport const TOGGLE_FOLLOW_KEYS = [\"f\", \"g\", \"h\", \"j\", \"k\"] as const;\n\n// UI Constants\nexport const DEFAULT_PADDING = 4;\nexport const DEFAULT_GLASS_Y_OFFSET = 110; // Starting Y position offset (below top bar with 3px gap)\nexport const DEFAULT_GLASS_SIZE = 300;\nexport const DEFAULT_ZOOM_FACTOR = 300; // 3x as percentage\nexport const DEFAULT_BORDER_WIDTH = 1;\nexport const DEFAULT_BORDER_COLOR = \"#6b7280\";\nexport const DEFAULT_OFFSET_STEP = 5;\n\n// Panel Constants\nexport const DEFAULT_PANEL_WIDTH = 300;\nexport const DEFAULT_PANEL_MAX_HEIGHT = 300;\nexport const DEFAULT_PANEL_OPACITY = 100;\n\n// Z-Index hierarchy\nexport const Z_INDEX = {\n GLASS: 98999,\n DEBUG: 99000,\n PANEL: 99999,\n CONTROLS: 99998\n} as const;\n\n// Animation durations (ms)\nexport const ANIMATION = {\n PANEL_TRANSITION: 300,\n AUTO_COLLAPSE_DELAY: 1500,\n THEME_DETECTION_INTERVAL: 2000\n} as const;\n\n// LocalStorage keys\nexport const STORAGE_KEYS = {\n OFFSET_X: 'comfyui_magnify_offset_x',\n OFFSET_Y: 'comfyui_magnify_offset_y'\n} as const;\n\n// BroadcastChannel for pop-out tab communication\nexport const BROADCAST_CHANNEL_NAME = 'comfyui-magnifyglass-popout';\n\n"],"names":[],"mappings":"AAOO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,eAAe;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AACJ;AAaO,MAAM,kBAAkB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAKO,MAAM,kBAAkB,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AACrD,MAAM,aAAa,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAC3C,MAAM,qBAAqB,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAGnD,MAAM,kBAAkB;AACxB,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAG5B,MAAM,sBAAsB;AAC5B,MAAM,2BAA2B;AACjC,MAAM,wBAAwB;AAG9B,MAAM,UAAU;AAAA,EACnB,OAAO;AAAA,EACP,OAAO;AAGX;AAUO,MAAM,eAAe;AAAA,EACxB,UAAU;AAAA,EACV,UAAU;AACd;AAGO,MAAM,yBAAyB;"} \ No newline at end of file diff --git a/web/shared/icons.js b/web/shared/icons.js index 48df5be..03df2ba 100644 --- a/web/shared/icons.js +++ b/web/shared/icons.js @@ -57,6 +57,12 @@ const Icons = { // Magnet icon - persist/sticky mode magnet: ` + `, + // External link icon - pop out to new tab + externalLink: ` + + + ` }; export { diff --git a/web/shared/icons.js.map b/web/shared/icons.js.map index ebaf62d..712e730 100644 --- a/web/shared/icons.js.map +++ b/web/shared/icons.js.map @@ -1 +1 @@ -{"version":3,"file":"icons.js","sources":["../../src/shared/icons.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - SVG Icons\n * \n * Inline SVG icons to replace emoji icons for a more professional look.\n * All icons are designed to be 16x16 or scalable.\n */\n\nexport const Icons = {\n // Magnify glass icon - used for header and glass toggle\n magnifyGlass: `\n \n \n `,\n\n // Unlock icon - panel is floating/following cursor\n unlock: `\n \n \n `,\n\n // Lock icon - panel is pinned\n lock: `\n \n \n `,\n\n // Pin icon - lock panel position\n pin: `\n \n \n `,\n\n // Eye icon - toggle visibility\n eye: `\n \n \n `,\n\n // Camera/image icon - media section\n camera: `\n \n \n `,\n\n // Target/crosshair icon - node section\n target: `\n \n \n \n `,\n\n // Location/map pin icon - empty state\n mapPin: `\n \n \n `,\n\n // Chevron right - expand icon\n chevronRight: `\n \n `,\n\n // Minus icon - minimize button\n minus: `\n \n `,\n\n // Inspector/info icon\n info: `\n \n \n \n `,\n\n // Box/node icon\n box: `\n \n \n \n `,\n\n // Magnet icon - persist/sticky mode\n magnet: `\n \n `\n};\n\n/**\n * Get an icon wrapped in a span for proper sizing\n */\nexport function getIcon(name: keyof typeof Icons, className: string = ''): string {\n const icon = Icons[name] || '';\n return `${icon}`;\n}\n"],"names":[],"mappings":"AAOO,MAAM,QAAQ;AAAA;AAAA,EAEjB,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA,EAMd,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMR,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAMN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,EAML,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,EAML,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAaR,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMR,cAAc;AAAA;AAAA;AAAA;AAAA,EAKd,OAAO;AAAA;AAAA;AAAA;AAAA,EAKP,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAON,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOL,QAAQ;AAAA;AAAA;AAGZ;"} \ No newline at end of file +{"version":3,"file":"icons.js","sources":["../../src/shared/icons.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - SVG Icons\n * \n * Inline SVG icons to replace emoji icons for a more professional look.\n * All icons are designed to be 16x16 or scalable.\n */\n\nexport const Icons = {\n // Magnify glass icon - used for header and glass toggle\n magnifyGlass: `\n \n \n `,\n\n // Unlock icon - panel is floating/following cursor\n unlock: `\n \n \n `,\n\n // Lock icon - panel is pinned\n lock: `\n \n \n `,\n\n // Pin icon - lock panel position\n pin: `\n \n \n `,\n\n // Eye icon - toggle visibility\n eye: `\n \n \n `,\n\n // Camera/image icon - media section\n camera: `\n \n \n `,\n\n // Target/crosshair icon - node section\n target: `\n \n \n \n `,\n\n // Location/map pin icon - empty state\n mapPin: `\n \n \n `,\n\n // Chevron right - expand icon\n chevronRight: `\n \n `,\n\n // Minus icon - minimize button\n minus: `\n \n `,\n\n // Inspector/info icon\n info: `\n \n \n \n `,\n\n // Box/node icon\n box: `\n \n \n \n `,\n\n // Magnet icon - persist/sticky mode\n magnet: `\n \n `,\n\n // External link icon - pop out to new tab\n externalLink: `\n \n \n \n `\n};\n\n/**\n * Get an icon wrapped in a span for proper sizing\n */\nexport function getIcon(name: keyof typeof Icons, className: string = ''): string {\n const icon = Icons[name] || '';\n return `${icon}`;\n}\n"],"names":[],"mappings":"AAOO,MAAM,QAAQ;AAAA;AAAA,EAEjB,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA,EAMd,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMR,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAMN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,EAML,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,EAML,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAaR,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMR,cAAc;AAAA;AAAA;AAAA;AAAA,EAKd,OAAO;AAAA;AAAA;AAAA;AAAA,EAKP,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAON,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOL,QAAQ;AAAA;AAAA;AAAA;AAAA,EAKR,cAAc;AAAA;AAAA;AAAA;AAAA;AAKlB;"} \ No newline at end of file