diff --git a/src/magnify-glass/EventHandler.ts b/src/magnify-glass/EventHandler.ts index 421fff9..3601ccd 100644 --- a/src/magnify-glass/EventHandler.ts +++ b/src/magnify-glass/EventHandler.ts @@ -19,10 +19,10 @@ export class EventHandler { constructor(magnifyGlass: MagnifyGlass) { this.magnifyGlass = magnifyGlass; - // Bind methods to maintain context this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyUp = this.handleKeyUp.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); + this.handleResize = this.handleResize.bind(this); } /** @@ -32,6 +32,7 @@ export class EventHandler { document.addEventListener("keydown", this.handleKeyDown); document.addEventListener("keyup", this.handleKeyUp); document.addEventListener("mousemove", this.handleMouseMove); + window.addEventListener("resize", this.handleResize); } /** @@ -41,6 +42,22 @@ export class EventHandler { document.removeEventListener("keydown", this.handleKeyDown); document.removeEventListener("keyup", this.handleKeyUp); document.removeEventListener("mousemove", this.handleMouseMove); + window.removeEventListener("resize", this.handleResize); + } + + /** + * Handle window resize events. + */ + handleResize(): void { + if (this.magnifyGlass && this.magnifyGlass.ui) { + this.magnifyGlass.ui.updateResponsivePosition(); + // Also update panel position if it exists + const infoPanel = (window as any).infoPanelManager; + if (infoPanel && infoPanel.positionManager) { + // Determine if we should move it + infoPanel.positionManager.positionPanel(); + } + } } /** @@ -230,11 +247,11 @@ export class EventHandler { const glassSize = this.magnifyGlass.config.glassSize; if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) { - // First activation - position at top right of the window - const leftPos = window.innerWidth - glassSize - DEFAULT_PADDING; - console.log(`[MagnifyGlass] Initial position - innerWidth: ${window.innerWidth}, glassSize: ${glassSize}, padding: ${DEFAULT_PADDING}, left: ${leftPos}, yOffset: ${DEFAULT_GLASS_Y_OFFSET}`); - this.magnifyGlass.ui.glassDiv.style.left = `${leftPos}px`; + // First activation - position at top right of the window (Anchored) + // Using style.right ensures it stays on the right even if window resizes before first usage + this.magnifyGlass.ui.glassDiv.style.right = `${DEFAULT_PADDING}px`; this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`; + this.magnifyGlass.ui.glassDiv.style.left = 'auto'; // Ensure left is unset this.magnifyGlass.state.wasActivatedBefore = true; } else { this.magnifyGlass.ui.positionGlass(clientX, clientY); diff --git a/src/magnify-glass/MagnifyGlass.ts b/src/magnify-glass/MagnifyGlass.ts index 13290e9..9cd1571 100644 --- a/src/magnify-glass/MagnifyGlass.ts +++ b/src/magnify-glass/MagnifyGlass.ts @@ -82,6 +82,10 @@ export class MagnifyGlass { // Create UI elements this.ui.createElements(); + // Ensure glass is validly positioned (within viewport) right away + // This handles cases where config has saved positions from a different monitor size + this.ui.updateResponsivePosition(); + // Setup WebGL renderer this.renderer = new WebGLRenderer(this.config, this.state, this.ui); if (!this.renderer.isValid()) { @@ -387,10 +391,24 @@ export class MagnifyGlass { // Reset magnify glass position to top-right corner if (this.ui.glassDiv) { - const glassSize = this.config.glassSize; - this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`; - this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`; - this.state.wasActivatedBefore = false; + // If it's the first activation, position at top right of the window (Anchored) + // Otherwise, reset to the last known position or default if not active. + if (!this.state.wasActivatedBefore) { + const padding = DEFAULT_PADDING; + // Using style.right ensures it stays on the right even if window resizes before first usage + this.ui.glassDiv.style.right = `${padding}px`; + this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`; + this.ui.glassDiv.style.left = 'auto'; // Ensure left is unset + this.state.wasActivatedBefore = true; + } else { + // If it was already activated, and we are resetting, we might want to keep its current position + // or explicitly reset it to the initial anchored position. + // For now, let's re-apply the anchored position on reset if it was already activated. + const padding = DEFAULT_PADDING; + this.ui.glassDiv.style.right = `${padding}px`; + this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`; + this.ui.glassDiv.style.left = 'auto'; + } } // Reset inspector panel position if exists diff --git a/src/magnify-glass/UiManager.ts b/src/magnify-glass/UiManager.ts index e44fae3..ceb41b9 100644 --- a/src/magnify-glass/UiManager.ts +++ b/src/magnify-glass/UiManager.ts @@ -4,7 +4,7 @@ * Handles creation and management of DOM elements for the magnifying glass. */ -import { Z_INDEX, DEFAULT_PADDING } from '../shared/constants'; +import { Z_INDEX, DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants'; import { Icons } from '../shared/icons'; import { Logger } from '../shared/logger'; import type { ConfigManager } from './ConfigManager'; @@ -160,57 +160,95 @@ export class UiManager { * @param clientX - Client X coordinate * @param clientY - Client Y coordinate */ + /** + * Position the glass relative to cursor. + * Uses dynamic anchoring (Left/Right, Top/Bottom) based on screen quadrant. + * @param clientX - Client X coordinate + * @param clientY - Client Y coordinate + */ positionGlass(clientX: number, clientY: number): void { if (!this.config.followCursor || !this.glassDiv) return; const glassSize = this.config.glassSize; const offsetAmount = DEFAULT_PADDING; - let newLeft: number; - let newTop: number; + const vw = window.innerWidth; + const vh = window.innerHeight; + // Determine base coordinates (top-left based) + let targetX: number; + let targetY: number; + + // Calculate standard Top-Left based positions first switch (this.config.glassPosition) { case "Top": - newLeft = clientX - (glassSize / 2); - newTop = clientY - glassSize - offsetAmount; + targetX = clientX - (glassSize / 2); + targetY = clientY - glassSize - offsetAmount; break; case "Bottom": - newLeft = clientX - (glassSize / 2); - newTop = clientY + offsetAmount; + targetX = clientX - (glassSize / 2); + targetY = clientY + offsetAmount; break; case "Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY - (glassSize / 2); + targetX = clientX - glassSize - offsetAmount; + targetY = clientY - (glassSize / 2); break; case "Right": - newLeft = clientX + offsetAmount; - newTop = clientY - (glassSize / 2); + targetX = clientX + offsetAmount; + targetY = clientY - (glassSize / 2); break; case "Top-Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY - glassSize - offsetAmount; + targetX = clientX - glassSize - offsetAmount; + targetY = clientY - glassSize - offsetAmount; break; case "Top-Right": - newLeft = clientX + offsetAmount; - newTop = clientY - glassSize - offsetAmount; + targetX = clientX + offsetAmount; + targetY = clientY - glassSize - offsetAmount; break; case "Bottom-Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY + offsetAmount; + targetX = clientX - glassSize - offsetAmount; + targetY = clientY + offsetAmount; break; case "Bottom-Right": - newLeft = clientX + offsetAmount; - newTop = clientY + offsetAmount; + targetX = clientX + offsetAmount; + targetY = clientY + offsetAmount; break; default: - newLeft = clientX - (glassSize / 2); - newTop = clientY + offsetAmount; + targetX = clientX - (glassSize / 2); + targetY = clientY + offsetAmount; break; } - this.glassDiv.style.left = `${newLeft}px`; - this.glassDiv.style.top = `${newTop}px`; + // Apply Dynamic Anchoring based on resulting position's center relative to screen + const glassCenterX = targetX + (glassSize / 2); + const glassCenterY = targetY + (glassSize / 2); - this.adjustForBoundaries(clientX, clientY); + // Horizontal Anchor + if (glassCenterX > vw / 2) { + // Anchor Right + const rightPos = vw - (targetX + glassSize); + this.glassDiv.style.right = `${rightPos}px`; + this.glassDiv.style.left = 'auto'; + } else { + // Anchor Left + this.glassDiv.style.left = `${targetX}px`; + this.glassDiv.style.right = 'auto'; + } + + // Vertical Anchor + if (glassCenterY > vh / 2) { + // Anchor Bottom + const bottomPos = vh - (targetY + glassSize); + this.glassDiv.style.bottom = `${bottomPos}px`; + this.glassDiv.style.top = 'auto'; + } else { + // Anchor Top + this.glassDiv.style.top = `${targetY}px`; + this.glassDiv.style.bottom = 'auto'; + } + + // We run a lightweight clamp to ensure it doesn't bleed offscreen during movement + // (Though the anchor logic implicitly handles resize, dragging near edges might need clamping) + this.adjustForBoundaries(); } /** @@ -218,32 +256,58 @@ export class UiManager { * @param clientX - Client X coordinate * @param clientY - Client Y coordinate */ - adjustForBoundaries(clientX: number, clientY: number): void { + /** + * Adjust glass position to stay within viewport boundaries. + * Respects the current anchor (Left/Right, Top/Bottom). + */ + adjustForBoundaries(): void { if (!this.glassDiv) return; - const glassRect = this.glassDiv.getBoundingClientRect(); + const glassSize = this.config.glassSize; + const padding = DEFAULT_PADDING; + const vw = window.innerWidth; + const vh = window.innerHeight; - // Check right boundary - if (glassRect.right > window.innerWidth) { - this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`; + // Check if anchored Right or Left + const isAnchoredRight = this.glassDiv.style.right !== 'auto' && this.glassDiv.style.right !== ''; + + if (isAnchoredRight) { + let currentRight = parseFloat(this.glassDiv.style.right) || padding; + // Clamp Right: max(padding, min(current, vw - size - padding)) + // Ensure it doesn't go off left side (vw - size - padding) + currentRight = Math.max(padding, Math.min(currentRight, vw - glassSize - padding)); + this.glassDiv.style.right = `${currentRight}px`; + } else { + // Anchor Left + let currentLeft = parseFloat(this.glassDiv.style.left) || padding; + currentLeft = Math.max(padding, Math.min(currentLeft, vw - glassSize - padding)); + this.glassDiv.style.left = `${currentLeft}px`; } - // Check left boundary - const currentRectLeft = this.glassDiv.getBoundingClientRect(); - if (currentRectLeft.left < 0) { - this.glassDiv.style.left = "10px"; - } + // Check if anchored Bottom or Top + const isAnchoredBottom = this.glassDiv.style.bottom !== 'auto' && this.glassDiv.style.bottom !== ''; - // Check bottom boundary - if (glassRect.bottom > window.innerHeight) { - this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`; + if (isAnchoredBottom) { + let currentBottom = parseFloat(this.glassDiv.style.bottom) || padding; + currentBottom = Math.max(padding, Math.min(currentBottom, vh - glassSize - padding)); + this.glassDiv.style.bottom = `${currentBottom}px`; + } else { + // Anchor Top + let currentTop = parseFloat(this.glassDiv.style.top) || padding; + currentTop = Math.max(padding, Math.min(currentTop, vh - glassSize - padding)); + this.glassDiv.style.top = `${currentTop}px`; } + } - // Check top boundary - const currentRectTop = this.glassDiv.getBoundingClientRect(); - if (currentRectTop.top < 0) { - this.glassDiv.style.top = "10px"; - } + /** + * Update position safely on resize. + * Since we use dynamic anchoring in positionGlass, CSS handles most resize cases. + * This method ensures we verify boundaries (clamping) and fix anchors if we cross thresholds drastically. + */ + updateResponsivePosition(): void { + if (!this.glassDiv) return; + // Re-run boundary adjustments to clamp if resize pushed us offscreen + this.adjustForBoundaries(); } /** diff --git a/web/magnify-glass/EventHandler.js b/web/magnify-glass/EventHandler.js index 703c67c..9fd30a2 100644 --- a/web/magnify-glass/EventHandler.js +++ b/web/magnify-glass/EventHandler.js @@ -11,6 +11,7 @@ class EventHandler { this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyUp = this.handleKeyUp.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); + this.handleResize = this.handleResize.bind(this); } /** * Attach all event listeners. @@ -19,6 +20,7 @@ class EventHandler { document.addEventListener("keydown", this.handleKeyDown); document.addEventListener("keyup", this.handleKeyUp); document.addEventListener("mousemove", this.handleMouseMove); + window.addEventListener("resize", this.handleResize); } /** * Detach all event listeners. @@ -27,6 +29,19 @@ class EventHandler { document.removeEventListener("keydown", this.handleKeyDown); document.removeEventListener("keyup", this.handleKeyUp); document.removeEventListener("mousemove", this.handleMouseMove); + window.removeEventListener("resize", this.handleResize); + } + /** + * Handle window resize events. + */ + handleResize() { + if (this.magnifyGlass && this.magnifyGlass.ui) { + this.magnifyGlass.ui.updateResponsivePosition(); + const infoPanel = window.infoPanelManager; + if (infoPanel && infoPanel.positionManager) { + infoPanel.positionManager.positionPanel(); + } + } } /** * Handle keydown events. @@ -168,12 +183,11 @@ class EventHandler { const pixelY = cssMouseYOnCanvas * scaleY; this.magnifyGlass.state.x = pixelX; this.magnifyGlass.state.y = pixelY; - const glassSize = this.magnifyGlass.config.glassSize; + this.magnifyGlass.config.glassSize; if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) { - const leftPos = window.innerWidth - glassSize - DEFAULT_PADDING; - console.log(`[MagnifyGlass] Initial position - innerWidth: ${window.innerWidth}, glassSize: ${glassSize}, padding: ${DEFAULT_PADDING}, left: ${leftPos}, yOffset: ${DEFAULT_GLASS_Y_OFFSET}`); - this.magnifyGlass.ui.glassDiv.style.left = `${leftPos}px`; + this.magnifyGlass.ui.glassDiv.style.right = `${DEFAULT_PADDING}px`; this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`; + this.magnifyGlass.ui.glassDiv.style.left = "auto"; this.magnifyGlass.state.wasActivatedBefore = true; } else { this.magnifyGlass.ui.positionGlass(clientX, clientY); diff --git a/web/magnify-glass/EventHandler.js.map b/web/magnify-glass/EventHandler.js.map index 9fc54aa..e2e2ae8 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 // Bind methods to maintain context\n this.handleKeyDown = this.handleKeyDown.bind(this);\n this.handleKeyUp = this.handleKeyUp.bind(this);\n this.handleMouseMove = this.handleMouseMove.bind(this);\n }\n\n /**\n * Attach all event listeners.\n */\n attachListeners(): void {\n document.addEventListener(\"keydown\", this.handleKeyDown);\n document.addEventListener(\"keyup\", this.handleKeyUp);\n document.addEventListener(\"mousemove\", this.handleMouseMove);\n }\n\n /**\n * Detach all event listeners.\n */\n detachListeners(): void {\n document.removeEventListener(\"keydown\", this.handleKeyDown);\n document.removeEventListener(\"keyup\", this.handleKeyUp);\n document.removeEventListener(\"mousemove\", this.handleMouseMove);\n }\n\n /**\n * Handle keydown events.\n */\n handleKeyDown(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n // Don't handle hotkeys if user is typing in an input field\n if (isUserTyping()) {\n return;\n }\n\n // Magnifier activation\n 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\n const leftPos = window.innerWidth - glassSize - DEFAULT_PADDING;\n console.log(`[MagnifyGlass] Initial position - innerWidth: ${window.innerWidth}, glassSize: ${glassSize}, padding: ${DEFAULT_PADDING}, left: ${leftPos}, yOffset: ${DEFAULT_GLASS_Y_OFFSET}`);\n this.magnifyGlass.ui.glassDiv.style.left = `${leftPos}px`;\n this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.magnifyGlass.state.wasActivatedBefore = true;\n } else {\n this.magnifyGlass.ui.positionGlass(clientX, clientY);\n }\n\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n}\n"],"names":[],"mappings":";;;;;;AAeO,MAAM,aAAa;AAAA,EAGtB,YAAY,cAA4B;AAFxC;AAGI,SAAK,eAAe;AAGpB,SAAK,gBAAgB,KAAK,cAAc,KAAK,IAAI;AACjD,SAAK,cAAc,KAAK,YAAY,KAAK,IAAI;AAC7C,SAAK,kBAAkB,KAAK,gBAAgB,KAAK,IAAI;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAwB;AACpB,aAAS,iBAAiB,WAAW,KAAK,aAAa;AACvD,aAAS,iBAAiB,SAAS,KAAK,WAAW;AACnD,aAAS,iBAAiB,aAAa,KAAK,eAAe;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAwB;AACpB,aAAS,oBAAoB,WAAW,KAAK,aAAa;AAC1D,aAAS,oBAAoB,SAAS,KAAK,WAAW;AACtD,aAAS,oBAAoB,aAAa,KAAK,eAAe;AAAA,EAClE;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;AAE5B,YAAM,YAAY,KAAK,aAAa,OAAO;AAE3C,UAAI,CAAC,KAAK,aAAa,MAAM,sBAAsB,KAAK,aAAa,GAAG,UAAU;AAE9E,cAAM,UAAU,OAAO,aAAa,YAAY;AAChD,gBAAQ,IAAI,iDAAiD,OAAO,UAAU,gBAAgB,SAAS,cAAc,eAAe,WAAW,OAAO,cAAc,sBAAsB,EAAE;AAC5L,aAAK,aAAa,GAAG,SAAS,MAAM,OAAO,GAAG,OAAO;AACrD,aAAK,aAAa,GAAG,SAAS,MAAM,MAAM,GAAG,sBAAsB;AACnE,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 }\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 8319a3a..791482c 100644 --- a/web/magnify-glass/MagnifyGlass.js +++ b/web/magnify-glass/MagnifyGlass.js @@ -51,6 +51,7 @@ class MagnifyGlass { this.config.loadSavedOffsets(); this.debugger.printCanvasInfo(); this.ui.createElements(); + this.ui.updateResponsivePosition(); this.renderer = new WebGLRenderer(this.config, this.state, this.ui); if (!this.renderer.isValid()) { this.ui.cleanup(); @@ -290,10 +291,18 @@ class MagnifyGlass { this.updateMagnifiedView(); } if (this.ui.glassDiv) { - const glassSize = this.config.glassSize; - this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`; - this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`; - this.state.wasActivatedBefore = false; + if (!this.state.wasActivatedBefore) { + const padding = DEFAULT_PADDING; + this.ui.glassDiv.style.right = `${padding}px`; + this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`; + this.ui.glassDiv.style.left = "auto"; + this.state.wasActivatedBefore = true; + } else { + const padding = DEFAULT_PADDING; + this.ui.glassDiv.style.right = `${padding}px`; + this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`; + this.ui.glassDiv.style.left = "auto"; + } } const extensions = window.comfyUIMagnifyGlassExtensions; if (extensions && extensions.length > 0) { diff --git a/web/magnify-glass/MagnifyGlass.js.map b/web/magnify-glass/MagnifyGlass.js.map index 0bc50d1..6ac9085 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 // 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 const glassSize = this.config.glassSize;\n this.ui.glassDiv.style.left = `${window.innerWidth - glassSize - DEFAULT_PADDING}px`;\n this.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.state.wasActivatedBefore = false;\n }\n\n // Reset inspector panel position if exists\n const extensions = window.comfyUIMagnifyGlassExtensions;\n if (extensions && extensions.length > 0) {\n extensions.forEach((extension: any) => {\n if (extension?.stateManager) {\n extension.stateManager.state.isPanelPinned = false;\n extension.stateManager.state.isPanelLocked = false;\n extension.stateManager.state.isAutoPinned = false;\n extension.stateManager.state.pinnedPosition = { x: 0, y: 0 };\n extension.stateManager.state.lastPinnedPosition = null;\n\n if (extension.positionManager) {\n extension.positionManager.positionPanel();\n }\n if (extension.uiManager?.updateControlStates) {\n extension.uiManager.updateControlStates();\n }\n }\n });\n }\n\n this.debugger.log(\"Reset: Magnify glass and inspector panel positions restored to defaults\");\n }\n\n /**\n * Cleanup all resources.\n */\n cleanup(): void {\n this.eventHandler.detachListeners();\n this.ui.cleanup();\n }\n}\n"],"names":[],"mappings":"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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';\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 diff --git a/web/magnify-glass/UiManager.js b/web/magnify-glass/UiManager.js index c1b6cd2..226d8e9 100644 --- a/web/magnify-glass/UiManager.js +++ b/web/magnify-glass/UiManager.js @@ -129,77 +129,123 @@ class UiManager { * @param clientX - Client X coordinate * @param clientY - Client Y coordinate */ + /** + * Position the glass relative to cursor. + * Uses dynamic anchoring (Left/Right, Top/Bottom) based on screen quadrant. + * @param clientX - Client X coordinate + * @param clientY - Client Y coordinate + */ positionGlass(clientX, clientY) { if (!this.config.followCursor || !this.glassDiv) return; const glassSize = this.config.glassSize; const offsetAmount = DEFAULT_PADDING; - let newLeft; - let newTop; + const vw = window.innerWidth; + const vh = window.innerHeight; + let targetX; + let targetY; switch (this.config.glassPosition) { case "Top": - newLeft = clientX - glassSize / 2; - newTop = clientY - glassSize - offsetAmount; + targetX = clientX - glassSize / 2; + targetY = clientY - glassSize - offsetAmount; break; case "Bottom": - newLeft = clientX - glassSize / 2; - newTop = clientY + offsetAmount; + targetX = clientX - glassSize / 2; + targetY = clientY + offsetAmount; break; case "Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY - glassSize / 2; + targetX = clientX - glassSize - offsetAmount; + targetY = clientY - glassSize / 2; break; case "Right": - newLeft = clientX + offsetAmount; - newTop = clientY - glassSize / 2; + targetX = clientX + offsetAmount; + targetY = clientY - glassSize / 2; break; case "Top-Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY - glassSize - offsetAmount; + targetX = clientX - glassSize - offsetAmount; + targetY = clientY - glassSize - offsetAmount; break; case "Top-Right": - newLeft = clientX + offsetAmount; - newTop = clientY - glassSize - offsetAmount; + targetX = clientX + offsetAmount; + targetY = clientY - glassSize - offsetAmount; break; case "Bottom-Left": - newLeft = clientX - glassSize - offsetAmount; - newTop = clientY + offsetAmount; + targetX = clientX - glassSize - offsetAmount; + targetY = clientY + offsetAmount; break; case "Bottom-Right": - newLeft = clientX + offsetAmount; - newTop = clientY + offsetAmount; + targetX = clientX + offsetAmount; + targetY = clientY + offsetAmount; break; default: - newLeft = clientX - glassSize / 2; - newTop = clientY + offsetAmount; + targetX = clientX - glassSize / 2; + targetY = clientY + offsetAmount; break; } - this.glassDiv.style.left = `${newLeft}px`; - this.glassDiv.style.top = `${newTop}px`; - this.adjustForBoundaries(clientX, clientY); + const glassCenterX = targetX + glassSize / 2; + const glassCenterY = targetY + glassSize / 2; + if (glassCenterX > vw / 2) { + const rightPos = vw - (targetX + glassSize); + this.glassDiv.style.right = `${rightPos}px`; + this.glassDiv.style.left = "auto"; + } else { + this.glassDiv.style.left = `${targetX}px`; + this.glassDiv.style.right = "auto"; + } + if (glassCenterY > vh / 2) { + const bottomPos = vh - (targetY + glassSize); + this.glassDiv.style.bottom = `${bottomPos}px`; + this.glassDiv.style.top = "auto"; + } else { + this.glassDiv.style.top = `${targetY}px`; + this.glassDiv.style.bottom = "auto"; + } + this.adjustForBoundaries(); } /** * Adjust glass position to stay within viewport boundaries. * @param clientX - Client X coordinate * @param clientY - Client Y coordinate */ - adjustForBoundaries(clientX, clientY) { + /** + * Adjust glass position to stay within viewport boundaries. + * Respects the current anchor (Left/Right, Top/Bottom). + */ + adjustForBoundaries() { if (!this.glassDiv) return; - const glassRect = this.glassDiv.getBoundingClientRect(); - if (glassRect.right > window.innerWidth) { - this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`; + const glassSize = this.config.glassSize; + const padding = DEFAULT_PADDING; + const vw = window.innerWidth; + const vh = window.innerHeight; + const isAnchoredRight = this.glassDiv.style.right !== "auto" && this.glassDiv.style.right !== ""; + if (isAnchoredRight) { + let currentRight = parseFloat(this.glassDiv.style.right) || padding; + currentRight = Math.max(padding, Math.min(currentRight, vw - glassSize - padding)); + this.glassDiv.style.right = `${currentRight}px`; + } else { + let currentLeft = parseFloat(this.glassDiv.style.left) || padding; + currentLeft = Math.max(padding, Math.min(currentLeft, vw - glassSize - padding)); + this.glassDiv.style.left = `${currentLeft}px`; } - const currentRectLeft = this.glassDiv.getBoundingClientRect(); - if (currentRectLeft.left < 0) { - this.glassDiv.style.left = "10px"; - } - if (glassRect.bottom > window.innerHeight) { - this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`; - } - const currentRectTop = this.glassDiv.getBoundingClientRect(); - if (currentRectTop.top < 0) { - this.glassDiv.style.top = "10px"; + const isAnchoredBottom = this.glassDiv.style.bottom !== "auto" && this.glassDiv.style.bottom !== ""; + if (isAnchoredBottom) { + let currentBottom = parseFloat(this.glassDiv.style.bottom) || padding; + currentBottom = Math.max(padding, Math.min(currentBottom, vh - glassSize - padding)); + this.glassDiv.style.bottom = `${currentBottom}px`; + } else { + let currentTop = parseFloat(this.glassDiv.style.top) || padding; + currentTop = Math.max(padding, Math.min(currentTop, vh - glassSize - padding)); + this.glassDiv.style.top = `${currentTop}px`; } } + /** + * Update position safely on resize. + * Since we use dynamic anchoring in positionGlass, CSS handles most resize cases. + * This method ensures we verify boundaries (clamping) and fix anchors if we cross thresholds drastically. + */ + updateResponsivePosition() { + if (!this.glassDiv) return; + this.adjustForBoundaries(); + } /** * Apply current config to UI elements. */ diff --git a/web/magnify-glass/UiManager.js.map b/web/magnify-glass/UiManager.js.map index b3645d4..66a895c 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 } 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 positionGlass(clientX: number, clientY: number): void {\n if (!this.config.followCursor || !this.glassDiv) return;\n\n const glassSize = this.config.glassSize;\n const offsetAmount = DEFAULT_PADDING;\n let newLeft: number;\n let newTop: number;\n\n switch (this.config.glassPosition) {\n case \"Top\":\n newLeft = clientX - (glassSize / 2);\n newTop = clientY - glassSize - offsetAmount;\n break;\n case \"Bottom\":\n newLeft = clientX - (glassSize / 2);\n newTop = clientY + offsetAmount;\n break;\n case \"Left\":\n newLeft = clientX - glassSize - offsetAmount;\n newTop = clientY - (glassSize / 2);\n break;\n case \"Right\":\n newLeft = clientX + offsetAmount;\n newTop = clientY - (glassSize / 2);\n break;\n case \"Top-Left\":\n newLeft = clientX - glassSize - offsetAmount;\n newTop = clientY - glassSize - offsetAmount;\n break;\n case \"Top-Right\":\n newLeft = clientX + offsetAmount;\n newTop = clientY - glassSize - offsetAmount;\n break;\n case \"Bottom-Left\":\n newLeft = clientX - glassSize - offsetAmount;\n newTop = clientY + offsetAmount;\n break;\n case \"Bottom-Right\":\n newLeft = clientX + offsetAmount;\n newTop = clientY + offsetAmount;\n break;\n default:\n newLeft = clientX - (glassSize / 2);\n newTop = clientY + offsetAmount;\n break;\n }\n\n this.glassDiv.style.left = `${newLeft}px`;\n this.glassDiv.style.top = `${newTop}px`;\n\n this.adjustForBoundaries(clientX, clientY);\n }\n\n /**\n * Adjust glass position to stay within viewport boundaries.\n * @param clientX - Client X coordinate\n * @param clientY - Client Y coordinate\n */\n adjustForBoundaries(clientX: number, clientY: number): void {\n if (!this.glassDiv) return;\n\n const glassRect = this.glassDiv.getBoundingClientRect();\n\n // Check right boundary\n if (glassRect.right > window.innerWidth) {\n this.glassDiv.style.left = `${clientX - glassRect.width - DEFAULT_PADDING}px`;\n }\n\n // Check left boundary\n const currentRectLeft = this.glassDiv.getBoundingClientRect();\n if (currentRectLeft.left < 0) {\n this.glassDiv.style.left = \"10px\";\n }\n\n // Check bottom boundary\n if (glassRect.bottom > window.innerHeight) {\n this.glassDiv.style.top = `${clientY - glassRect.height - DEFAULT_PADDING}px`;\n }\n\n // Check top boundary\n const currentRectTop = this.glassDiv.getBoundingClientRect();\n if (currentRectTop.top < 0) {\n this.glassDiv.style.top = \"10px\";\n }\n }\n\n /**\n * Apply current config to UI elements.\n */\n applyStyles(): void {\n if (this.glassDiv) {\n this.glassDiv.style.width = `${this.config.glassSize}px`;\n this.glassDiv.style.height = `${this.config.glassSize}px`;\n this.glassDiv.style.border = this.config.borderEnabled\n ? `${this.config.borderWidth}px solid ${this.config.borderColor}`\n : 'none';\n\n // Reset clip-path and set default border-radius\n this.glassDiv.style.clipPath = 'none';\n this.glassDiv.style.borderRadius = '0px';\n\n switch (this.config.glassShape) {\n case \"Circle\":\n this.glassDiv.style.borderRadius = \"50%\";\n break;\n case \"Square\":\n // borderRadius is already '0px'\n break;\n case \"Rounded Square\":\n this.glassDiv.style.borderRadius = \"20%\";\n break;\n default:\n this.glassDiv.style.borderRadius = \"50%\";\n break;\n }\n }\n\n if (this.glassCanvas) {\n this.glassCanvas.width = this.config.glassSize;\n this.glassCanvas.height = this.config.glassSize;\n }\n\n // Handle debug canvas show/hide\n if (this.config.debugMode) {\n if (!this.debugCanvas) this.createDebugCanvas();\n if (this.state.active && this.debugCanvas) this.debugCanvas.style.display = \"block\";\n } else {\n if (this.debugCanvas) this.debugCanvas.style.display = \"none\";\n }\n }\n\n /**\n * Cleanup DOM elements.\n */\n cleanup(): void {\n if (this.glassDiv) this.glassDiv.remove();\n if (this.debugCanvas) this.debugCanvas.remove();\n // 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":";;;;;;AAgBO,MAAM,UAAU;AAAA,EAUnB,YAAY,QAAuB,OAAuB,UAAuB;AATjF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGI,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,WAAW;AAChB,SAAK,cAAc;AACnB,SAAK,cAAc;AACnB,SAAK,WAAW;AAChB,SAAK,uBAAuB;AAC5B,SAAK,WAAW;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAuB;AAEnB,SAAK,WAAW,SAAS,cAAc,KAAK;AAC5C,SAAK,SAAS,KAAK;AACnB,SAAK,SAAS,MAAM,UAAU;AAAA;AAAA;AAAA,qBAGjB,KAAK,OAAO,SAAS;AAAA,sBACpB,KAAK,OAAO,SAAS;AAAA,6BACd,KAAK,OAAO,eAAe,WAAW,QAAQ,KAAK;AAAA,sBAC1D,KAAK,OAAO,gBAAgB,GAAG,KAAK,OAAO,WAAW,YAAY,KAAK,OAAO,WAAW,KAAK,MAAM;AAAA;AAAA;AAAA,uBAGnG,QAAQ,KAAK;AAAA;AAAA;AAAA;AAAA;AAO5B,SAAK,cAAc,SAAS,cAAc,QAAQ;AAClD,SAAK,YAAY,QAAQ,KAAK,OAAO;AACrC,SAAK,YAAY,SAAS,KAAK,OAAO;AACtC,SAAK,YAAY,KAAK;AACtB,SAAK,SAAS,YAAY,KAAK,WAAW;AAG1C,SAAK,uBAAuB,SAAS,cAAc,KAAK;AACxD,SAAK,qBAAqB,KAAK;AAC/B,SAAK,qBAAqB,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS1C,SAAK,SAAS,YAAY,KAAK,oBAAoB;AAEnD,aAAS,KAAK,YAAY,KAAK,QAAQ;AAGvC,QAAI,KAAK,OAAO,WAAW;AACvB,WAAK,kBAAA;AAAA,IACT;AAGA,SAAK,iBAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,oBAA0B;AACtB,SAAK,cAAc,SAAS,cAAc,QAAQ;AAClD,SAAK,YAAY,KAAK;AACtB,SAAK,YAAY,QAAQ;AACzB,SAAK,YAAY,SAAS;AAC1B,SAAK,YAAY,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAMlB,QAAQ,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAM5B,aAAS,KAAK,YAAY,KAAK,WAAW;AAC1C,SAAK,WAAW,KAAK,YAAY,WAAW,IAAI;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACT,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,UAAU;AAAA,IAElC;AACA,QAAI,KAAK,OAAO,aAAa,KAAK,aAAa;AAC3C,WAAK,YAAY,MAAM,UAAU;AAAA,IACrC;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,qBAAqB,SAAwB;AACzC,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,UAAU,UAAU,MAAM;AAAA,IAGlD;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACT,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,UAAU;AAAA,IAClC;AACA,QAAI,KAAK,OAAO,aAAa,KAAK,aAAa;AAC3C,WAAK,YAAY,MAAM,UAAU;AAAA,IACrC;AAEA,QAAI,KAAK,sBAAsB;AAC3B,WAAK,qBAAqB,YAAY;AAAA,IAC1C;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,SAAiB,SAAuB;AAClD,QAAI,CAAC,KAAK,OAAO,gBAAgB,CAAC,KAAK,SAAU;AAEjD,UAAM,YAAY,KAAK,OAAO;AAC9B,UAAM,eAAe;AACrB,QAAI;AACJ,QAAI;AAEJ,YAAQ,KAAK,OAAO,eAAA;AAAA,MAChB,KAAK;AACD,kBAAU,UAAW,YAAY;AACjC,iBAAS,UAAU,YAAY;AAC/B;AAAA,MACJ,KAAK;AACD,kBAAU,UAAW,YAAY;AACjC,iBAAS,UAAU;AACnB;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU,YAAY;AAChC,iBAAS,UAAW,YAAY;AAChC;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU;AACpB,iBAAS,UAAW,YAAY;AAChC;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU,YAAY;AAChC,iBAAS,UAAU,YAAY;AAC/B;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU;AACpB,iBAAS,UAAU,YAAY;AAC/B;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU,YAAY;AAChC,iBAAS,UAAU;AACnB;AAAA,MACJ,KAAK;AACD,kBAAU,UAAU;AACpB,iBAAS,UAAU;AACnB;AAAA,MACJ;AACI,kBAAU,UAAW,YAAY;AACjC,iBAAS,UAAU;AACnB;AAAA,IAAA;AAGR,SAAK,SAAS,MAAM,OAAO,GAAG,OAAO;AACrC,SAAK,SAAS,MAAM,MAAM,GAAG,MAAM;AAEnC,SAAK,oBAAoB,SAAS,OAAO;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,oBAAoB,SAAiB,SAAuB;AACxD,QAAI,CAAC,KAAK,SAAU;AAEpB,UAAM,YAAY,KAAK,SAAS,sBAAA;AAGhC,QAAI,UAAU,QAAQ,OAAO,YAAY;AACrC,WAAK,SAAS,MAAM,OAAO,GAAG,UAAU,UAAU,QAAQ,eAAe;AAAA,IAC7E;AAGA,UAAM,kBAAkB,KAAK,SAAS,sBAAA;AACtC,QAAI,gBAAgB,OAAO,GAAG;AAC1B,WAAK,SAAS,MAAM,OAAO;AAAA,IAC/B;AAGA,QAAI,UAAU,SAAS,OAAO,aAAa;AACvC,WAAK,SAAS,MAAM,MAAM,GAAG,UAAU,UAAU,SAAS,eAAe;AAAA,IAC7E;AAGA,UAAM,iBAAiB,KAAK,SAAS,sBAAA;AACrC,QAAI,eAAe,MAAM,GAAG;AACxB,WAAK,SAAS,MAAM,MAAM;AAAA,IAC9B;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,cAAoB;AAChB,QAAI,KAAK,UAAU;AACf,WAAK,SAAS,MAAM,QAAQ,GAAG,KAAK,OAAO,SAAS;AACpD,WAAK,SAAS,MAAM,SAAS,GAAG,KAAK,OAAO,SAAS;AACrD,WAAK,SAAS,MAAM,SAAS,KAAK,OAAO,gBACnC,GAAG,KAAK,OAAO,WAAW,YAAY,KAAK,OAAO,WAAW,KAC7D;AAGN,WAAK,SAAS,MAAM,WAAW;AAC/B,WAAK,SAAS,MAAM,eAAe;AAEnC,cAAQ,KAAK,OAAO,YAAA;AAAA,QAChB,KAAK;AACD,eAAK,SAAS,MAAM,eAAe;AACnC;AAAA,QACJ,KAAK;AAED;AAAA,QACJ,KAAK;AACD,eAAK,SAAS,MAAM,eAAe;AACnC;AAAA,QACJ;AACI,eAAK,SAAS,MAAM,eAAe;AACnC;AAAA,MAAA;AAAA,IAEZ;AAEA,QAAI,KAAK,aAAa;AAClB,WAAK,YAAY,QAAQ,KAAK,OAAO;AACrC,WAAK,YAAY,SAAS,KAAK,OAAO;AAAA,IAC1C;AAGA,QAAI,KAAK,OAAO,WAAW;AACvB,UAAI,CAAC,KAAK,YAAa,MAAK,kBAAA;AAC5B,UAAI,KAAK,MAAM,UAAU,KAAK,YAAa,MAAK,YAAY,MAAM,UAAU;AAAA,IAChF,OAAO;AACH,UAAI,KAAK,YAAa,MAAK,YAAY,MAAM,UAAU;AAAA,IAC3D;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACZ,QAAI,KAAK,SAAU,MAAK,SAAS,OAAA;AACjC,QAAI,KAAK,YAAa,MAAK,YAAY,OAAA;AAEvC,UAAM,MAAM,SAAS,cAAc,qBAAqB;AACxD,QAAI,SAAS,OAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,mBAAyB;AAErB,UAAM,WAAW,KAAK,IAAA,IAAQ;AAG9B,UAAM,mBAAmB,MAAM;AAC3B,YAAM,UAAU,MAAM,KAAK,SAAS,iBAAiB,QAAQ,CAAC;AAI9D,UAAI,YAAY,QAAQ,KAAK,CAAA,MAAK;AAC9B,cAAM,SAAS,EAAE,SAAS,IAAI,YAAA;AAC9B,cAAM,QAAQ,EAAE,aAAa,YAAY,KAAK,IAAI,YAAA;AAClD,cAAM,QAAQ,EAAE,eAAe,IAAI,YAAA;AACnC,eAAQ,MAAM,SAAS,KAAK,KAAK,CAAC,MAAM,SAAS,MAAM,KAClD,KAAK,SAAS,KAAK,KAAK,CAAC,KAAK,SAAS,MAAM,KAC7C,KAAK,SAAS,KAAK;AAAA,MAC5B,CAAC;AAGD,UAAI,CAAC,WAAW;AACZ,oBAAY,QAAQ,KAAK,CAAA,MAAK;AAC1B,gBAAM,SAAS,EAAE,SAAS,IAAI,YAAA;AAC9B,gBAAM,QAAQ,EAAE,aAAa,YAAY,KAAK,IAAI,YAAA;AAClD,iBAAO,MAAM,SAAS,MAAM,KAAK,KAAK,SAAS,MAAM;AAAA,QACzD,CAAC;AAAA,MACL;AAEA,UAAI,aAAa,UAAU,eAAe;AAEtC,YAAI,UAAU,cAAc,cAAc,qBAAqB,EAAG,QAAO;AAEzE,eAAO,MAAM,sBAAsB,UAAU,SAAS,UAAU,aAAa,YAAY,KAAK,gBAAgB;AAE9G,cAAM,MAAM,SAAS,cAAc,QAAQ;AAE3C,YAAI,YAAY,UAAU,YAAY;AAGtC,cAAM,WAAW,OAAO,iBAAiB,SAAS;AAClD,YAAI,MAAM,SAAS,SAAS;AAC5B,YAAI,MAAM,YAAY,SAAS;AAG/B,YAAI,QAAQ;AACZ,YAAI,YAAY,MAAM;AAGtB,YAAI,MAAM,UAAU;AACpB,YAAI,MAAM,aAAa;AACvB,YAAI,MAAM,iBAAiB;AAC3B,YAAI,MAAM,UAAU;AACpB,YAAI,MAAM,SAAS;AACnB,YAAI,MAAM,SAAS,UAAU,MAAM,UAAU,SAAS;AACtD,YAAI,MAAM,eAAe,UAAU,MAAM,gBAAgB,SAAS;AAElE,YAAI,CAAC,IAAI,MAAM,WAAY,KAAI,MAAM,aAAa,SAAS;AAC3D,YAAI,CAAC,IAAI,MAAM,MAAO,KAAI,MAAM,QAAQ,SAAS;AAGjD,YAAI,iBAAiB,SAAS,MAAM;AAChC,cAAI,KAAK,UAAU;AACf,iBAAK,SAAA;AAEL,gBAAI,UAAU,OAAO,QAAQ;AAC7B,gBAAI,UAAU,OAAO,aAAa;AAClC,gBAAI,UAAU,OAAO,UAAU;AAAA,UACnC;AAAA,QACJ,CAAC;AAGD,cAAM,aAAa,UAAU,SAAS,IAAI,YAAA,EAAc,SAAS,KAAK,MACjE,UAAU,aAAa,YAAY,KAAK,IAAI,YAAA,EAAc,SAAS,KAAK;AAE7E,YAAI,WAAW;AAEX,cAAI,UAAU,aAAa;AACvB,sBAAU,cAAc,aAAa,KAAK,UAAU,WAAW;AAAA,UACnE,OAAO;AACH,sBAAU,cAAc,YAAY,GAAG;AAAA,UAC3C;AAAA,QACJ,OAAO;AAEH,oBAAU,cAAc,aAAa,KAAK,SAAS;AAAA,QACvD;AAEA,eAAO,MAAM,0CAA0C;AACvD,eAAO;AAAA,MACX;AACA,aAAO;AAAA,IACX;AAGA,QAAI,mBAAoB;AAGxB,UAAM,eAAe,YAAY,MAAM;AACnC,UAAI,KAAK,IAAA,IAAQ,UAAU;AACvB,gBAAQ;AAAA,UAAK;AAAA,UACT,MAAM,KAAK,SAAS,iBAAiB,QAAQ,CAAC,EAAE,IAAI,CAAA,MAAK,EAAE,SAAS,EAAE,aAAa,YAAY,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC;AAAA,QAAA;AAChJ,sBAAc,YAAY;AAC1B;AAAA,MACJ;AAEA,UAAI,oBAAoB;AACpB,sBAAc,YAAY;AAAA,MAC9B;AAAA,IACJ,GAAG,GAAG;AAAA,EACV;AACJ;"} \ 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 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