From 5b05cd973dab1becec84977da55d83de3f59d9de Mon Sep 17 00:00:00 2001 From: AEmotionStudio Date: Sun, 4 Jan 2026 23:25:00 -0800 Subject: [PATCH] feat: Implement hybrid rendering for magnify glass with direct capture and virtual zoom. --- README.md | 13 ++++++++++--- src/magnify-glass/ConfigManager.ts | 13 ++++++++++++- src/magnify-glass/EventHandler.ts | 9 +++++++++ src/magnify-glass/OffscreenRenderer.ts | 3 ++- src/shared/settings/defaults.ts | 1 + src/shared/settings/types.ts | 1 + web/magnify-glass/ConfigManager.js | 11 ++++++++++- web/magnify-glass/ConfigManager.js.map | 2 +- web/magnify-glass/EventHandler.js | 6 ++++++ web/magnify-glass/EventHandler.js.map | 2 +- web/magnify-glass/OffscreenRenderer.js | 2 +- web/magnify-glass/OffscreenRenderer.js.map | 2 +- web/shared/settings/defaults.js.map | 2 +- 13 files changed, 56 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f974457..9af8272 100644 --- a/README.md +++ b/README.md @@ -46,17 +46,17 @@ This release brings a complete overhaul to the rendering engine for **native fid * **WebGL-Powered**: Smooth, high-performance rendering at any zoom level. * **Smart Interactions**: Follows your cursor or stays fixed. Toggles transparently for click-through. * **Customizable**: Adjust zoom (up to 10x), size, border, and shape (Circle/Square/Rounded). - +[HERE] ### ℹ️ Inspector Panel * **Deep Analysis**: View node parameters (Seed, CFG, Steps), text content, and image details on hover. * **Dockable Interface**: Pin the panel to keep it stable, or let it follow the glass. * **Themed**: Automatically matches any ComfyUI theme (Dark, Light, Solarized, Arc, Nord, GitHub). - +[HERE] ### ⚙️ Sidebar Integration * **Organized Settings**: All Magnify Glass and Info Panel settings in one place. * **Live Previews**: Changes apply instantly without needing to refresh. * **Reset Options**: Quickly restore defaults with individual or global reset buttons. - +[HERE]x ### 🖥️ Multi-Monitor Pop-Out * **Detachable Viewer**: Open the magnified view in a separate browser tab. * **Inspector Sidebar**: Real-time node details, cursor position, and canvas scale. @@ -90,6 +90,7 @@ git clone https://github.com/AEmotionStudio/ComfyUI-MagnifyGlass.git | **`Shift+P`** | Open Pop-Out Viewer in New Tab | | **`U`** | Pin/Unpin Inspector Panel | | **`O`** | Reset Offsets | +| **`D`** | Force Direct Capture (accurate cursor at low zoom) | | **Arrows** | Nudge Glass Position | > **Pro Tip:** Toggle the **Glass Preview (`G`)** off to keep the Inspector Panel active but hide the magnifying preview. @@ -124,6 +125,12 @@ This project is licensed under the [GPL-3.0](LICENSE) License - see the [LICENSE --- +## 🐛 Known Issues + +* **Virtual Zoom Cursor Alignment**: When using "Virtual Zoom" (canvas zoom < 100%), the mini cursor preview in the glass may not perfectly align with the actual content due to a fundamental drift in the coordinate capture system. **Workaround**: Press `D` to toggle Direct Capture mode for accurate cursor alignment (at the cost of lower magnification quality). + +--- + ## 🤝 Contributing Contributions are welcome! Please submit a Pull Request or open an Issue on GitHub. diff --git a/src/magnify-glass/ConfigManager.ts b/src/magnify-glass/ConfigManager.ts index 4dea6f3..a2ed808 100644 --- a/src/magnify-glass/ConfigManager.ts +++ b/src/magnify-glass/ConfigManager.ts @@ -1,4 +1,4 @@ -/** +x/** * ComfyUI MagnifyGlass - ConfigManager (TypeScript) * * Manages configuration and settings for the magnifying glass. @@ -26,6 +26,8 @@ const DEFAULT_GLASS_SETTINGS = { "🔍MagnifyGlass.AlwaysActiveMode": true, "🔍MagnifyGlass.ToggleFollowCursorKey": "h", "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g", + "🔍MagnifyGlass.ShowCursorPreview": true, + "🔍MagnifyGlass.ForceDirectCaptureKey": "d", }; /** @@ -86,6 +88,12 @@ export class ConfigManager { /** Show cursor preview in glass */ showCursorPreview: boolean; + /** Force Direct Capture key */ + forceDirectCaptureKey: string; + + /** Runtime flag: Force Direct Capture mode (not persisted) */ + forceDirectCapture: boolean; + /** Manual offset X in graph units */ offsetX: number; @@ -112,6 +120,8 @@ export class ConfigManager { this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ToggleFollowCursorKey"]; this.toggleGlassPreviewKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassPreviewToggleHotkey"]; this.showCursorPreview = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ShowCursorPreview"]; + this.forceDirectCaptureKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ForceDirectCaptureKey"]; + this.forceDirectCapture = false; // Runtime flag, not persisted // Alignment adjustment parameters - managed separately this.offsetX = 0; @@ -139,6 +149,7 @@ export class ConfigManager { this.alwaysActiveMode = getSettingValue("🔍MagnifyGlass.AlwaysActiveMode", this.alwaysActiveMode); this.toggleFollowCursorKey = getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", this.toggleFollowCursorKey); this.showCursorPreview = getSettingValue("🔍MagnifyGlass.ShowCursorPreview", this.showCursorPreview); + this.forceDirectCaptureKey = getSettingValue("🔍MagnifyGlass.ForceDirectCaptureKey", this.forceDirectCaptureKey); this.toggleGlassPreviewKey = getSettingValue("🔍MagnifyGlass.GlassPreviewToggleHotkey", this.toggleGlassPreviewKey); } diff --git a/src/magnify-glass/EventHandler.ts b/src/magnify-glass/EventHandler.ts index ef1eaca..870de3d 100644 --- a/src/magnify-glass/EventHandler.ts +++ b/src/magnify-glass/EventHandler.ts @@ -153,6 +153,15 @@ export class EventHandler { this.magnifyGlass.updateMagnifiedView(); } + // Force Direct Capture Toggle Key + if (e.key.toLowerCase() === config.forceDirectCaptureKey && + (!config.altRequired || e.altKey)) { + config.forceDirectCapture = !config.forceDirectCapture; + this.magnifyGlass.debugger.log(`Force Direct Capture: ${config.forceDirectCapture ? 'ON' : 'OFF'}`); + this.magnifyGlass.updateMagnifiedView(); + e.preventDefault(); + } + // Pop-out tab toggle (Shift+P) if (e.shiftKey && e.key.toLowerCase() === 'p') { this.magnifyGlass.popOutManager.toggle(); diff --git a/src/magnify-glass/OffscreenRenderer.ts b/src/magnify-glass/OffscreenRenderer.ts index 294f7ff..73a9bbe 100644 --- a/src/magnify-glass/OffscreenRenderer.ts +++ b/src/magnify-glass/OffscreenRenderer.ts @@ -63,7 +63,8 @@ export class OffscreenRenderer { // and it guarantees perfect alignment with zero extra draws. // If we are zoomed out (< 100%), we use High Res Virtual Zoom for crisp details. // Note: Image preview nodes are handled by drawImagePreviewsNatively() during Virtual Zoom. - const useDirectCapture = currentScale >= 1.0; + // If forceDirectCapture is enabled (via hotkey), always use Direct Capture for accurate cursor alignment. + const useDirectCapture = currentScale >= 1.0 || this.config.forceDirectCapture; if (useDirectCapture) { // Direct Capture: Full speed, no extra draws diff --git a/src/shared/settings/defaults.ts b/src/shared/settings/defaults.ts index 4575abf..353e2f0 100644 --- a/src/shared/settings/defaults.ts +++ b/src/shared/settings/defaults.ts @@ -36,6 +36,7 @@ export const DEFAULT_GLASS_SETTINGS: GlassSettings = { "🔍MagnifyGlass.AlwaysActiveMode": true, "🔍MagnifyGlass.ToggleFollowCursorKey": "h", "🔍MagnifyGlass.ShowCursorPreview": true, + "🔍MagnifyGlass.ForceDirectCaptureKey": "d", }; /** diff --git a/src/shared/settings/types.ts b/src/shared/settings/types.ts index 7f94d9b..8a7d7c4 100644 --- a/src/shared/settings/types.ts +++ b/src/shared/settings/types.ts @@ -24,6 +24,7 @@ export interface GlassSettings { "🔍MagnifyGlass.AlwaysActiveMode": boolean; "🔍MagnifyGlass.ToggleFollowCursorKey": string; "🔍MagnifyGlass.ShowCursorPreview": boolean; + "🔍MagnifyGlass.ForceDirectCaptureKey": string; } /** diff --git a/web/magnify-glass/ConfigManager.js b/web/magnify-glass/ConfigManager.js index c0bd839..cda88b5 100644 --- a/web/magnify-glass/ConfigManager.js +++ b/web/magnify-glass/ConfigManager.js @@ -19,7 +19,9 @@ const DEFAULT_GLASS_SETTINGS = { "🔍MagnifyGlass.TextureFiltering": "Linear", "🔍MagnifyGlass.AlwaysActiveMode": true, "🔍MagnifyGlass.ToggleFollowCursorKey": "h", - "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g" + "🔍MagnifyGlass.GlassPreviewToggleHotkey": "g", + "🔍MagnifyGlass.ShowCursorPreview": true, + "🔍MagnifyGlass.ForceDirectCaptureKey": "d" }; class ConfigManager { constructor() { @@ -57,6 +59,10 @@ class ConfigManager { __publicField(this, "toggleGlassPreviewKey"); /** Show cursor preview in glass */ __publicField(this, "showCursorPreview"); + /** Force Direct Capture key */ + __publicField(this, "forceDirectCaptureKey"); + /** Runtime flag: Force Direct Capture mode (not persisted) */ + __publicField(this, "forceDirectCapture"); /** Manual offset X in graph units */ __publicField(this, "offsetX"); /** Manual offset Y in graph units */ @@ -78,6 +84,8 @@ class ConfigManager { this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ToggleFollowCursorKey"]; this.toggleGlassPreviewKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.GlassPreviewToggleHotkey"]; this.showCursorPreview = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ShowCursorPreview"]; + this.forceDirectCaptureKey = DEFAULT_GLASS_SETTINGS["🔍MagnifyGlass.ForceDirectCaptureKey"]; + this.forceDirectCapture = false; this.offsetX = 0; this.offsetY = 0; } @@ -101,6 +109,7 @@ class ConfigManager { this.alwaysActiveMode = getSettingValue("🔍MagnifyGlass.AlwaysActiveMode", this.alwaysActiveMode); this.toggleFollowCursorKey = getSettingValue("🔍MagnifyGlass.ToggleFollowCursorKey", this.toggleFollowCursorKey); this.showCursorPreview = getSettingValue("🔍MagnifyGlass.ShowCursorPreview", this.showCursorPreview); + this.forceDirectCaptureKey = getSettingValue("🔍MagnifyGlass.ForceDirectCaptureKey", this.forceDirectCaptureKey); this.toggleGlassPreviewKey = getSettingValue("🔍MagnifyGlass.GlassPreviewToggleHotkey", this.toggleGlassPreviewKey); } /** diff --git a/web/magnify-glass/ConfigManager.js.map b/web/magnify-glass/ConfigManager.js.map index 03dcf70..0e613c9 100644 --- a/web/magnify-glass/ConfigManager.js.map +++ b/web/magnify-glass/ConfigManager.js.map @@ -1 +1 @@ -{"version":3,"file":"ConfigManager.js","sources":["../../src/magnify-glass/ConfigManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - ConfigManager (TypeScript)\n * \n * Manages configuration and settings for the magnifying glass.\n */\n\nimport { getSettingValue } from '../shared/utils';\nimport { STORAGE_KEYS } from '../shared/constants';\nimport type { GlassPosition, GlassShape, TextureFilter } from '../shared/constants';\n\n// Default settings (we'll import these properly later)\nconst DEFAULT_GLASS_SETTINGS = {\n \"🔍MagnifyGlass.ZoomFactor\": 300,\n \"🔍MagnifyGlass.GlassSize\": 300,\n \"🔍MagnifyGlass.BorderColor\": \"#6b7280\",\n \"🔍MagnifyGlass.BorderWidth\": 1,\n \"🔍MagnifyGlass.ActivationKey\": \"x\",\n \"🔍MagnifyGlass.AltRequired\": false,\n \"🔍MagnifyGlass.FollowCursor\": false,\n \"🔍MagnifyGlass.OffsetStep\": 5,\n \"🔍MagnifyGlass.GlassPosition\": \"Top-Right\" as GlassPosition,\n \"🔍MagnifyGlass.ResetKey\": \"o\",\n \"🔍MagnifyGlass.GlassShape\": \"Rounded Square\" as GlassShape,\n \"🔍MagnifyGlass.BorderEnabled\": true,\n \"🔍MagnifyGlass.TextureFiltering\": \"Linear\" as TextureFilter,\n \"🔍MagnifyGlass.AlwaysActiveMode\": true,\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": \"h\",\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": \"g\",\n};\n\n/**\n * Configuration manager class.\n * Reads settings from ComfyUI and manages persistent offsets.\n */\nexport class ConfigManager {\n /** Zoom factor (1.0 = 100%, 3.0 = 300%) */\n zoomFactor: number;\n\n /** Glass size in pixels */\n glassSize: number;\n\n /** Border color (CSS color string) */\n borderColor: string;\n\n /** Border width in pixels */\n borderWidth: number;\n\n /** Activation key (lowercase) */\n activationKey: string;\n\n /** Whether Alt key is required for activation */\n altRequired: boolean;\n\n /** Whether glass follows cursor */\n followCursor: boolean;\n\n\n\n /** Offset step for arrow key adjustments */\n offsetStep: number;\n\n /** Glass position relative to cursor */\n glassPosition: string;\n\n /** Reset key (lowercase) */\n resetKey: string;\n\n /** Glass shape */\n glassShape: string;\n\n /** Whether border is enabled */\n borderEnabled: boolean;\n\n /** Texture filtering mode */\n textureFiltering: string;\n\n /** Always active mode */\n alwaysActiveMode: boolean;\n\n /** Toggle follow cursor key */\n toggleFollowCursorKey: string;\n\n /** Glass preview toggle key */\n toggleGlassPreviewKey: string;\n\n /** Show cursor preview in glass */\n showCursorPreview: boolean;\n\n /** Manual offset X in graph units */\n offsetX: number;\n\n /** Manual offset Y in graph units */\n offsetY: number;\n\n constructor() {\n // Initialize properties with defaults\n this.zoomFactor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ZoomFactor\"] / 100;\n this.glassSize = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassSize\"];\n this.borderColor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderColor\"];\n this.borderWidth = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderWidth\"];\n this.activationKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ActivationKey\"];\n this.altRequired = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.AltRequired\"];\n this.followCursor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.FollowCursor\"];\n\n this.offsetStep = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.OffsetStep\"];\n this.glassPosition = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassPosition\"];\n this.resetKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ResetKey\"];\n this.glassShape = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassShape\"];\n this.borderEnabled = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderEnabled\"];\n this.textureFiltering = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.TextureFiltering\"];\n this.alwaysActiveMode = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.AlwaysActiveMode\"];\n this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ToggleFollowCursorKey\"];\n this.toggleGlassPreviewKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassPreviewToggleHotkey\"];\n this.showCursorPreview = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ShowCursorPreview\"];\n\n // Alignment adjustment parameters - managed separately\n this.offsetX = 0;\n this.offsetY = 0;\n }\n\n /**\n * Load settings from ComfyUI settings system.\n */\n loadSettings(): void {\n this.zoomFactor = getSettingValue(\"🔍MagnifyGlass.ZoomFactor\", this.zoomFactor * 100) / 100;\n this.glassSize = getSettingValue(\"🔍MagnifyGlass.GlassSize\", this.glassSize);\n this.borderColor = getSettingValue(\"🔍MagnifyGlass.BorderColor\", this.borderColor);\n this.borderWidth = getSettingValue(\"🔍MagnifyGlass.BorderWidth\", this.borderWidth);\n this.activationKey = getSettingValue(\"🔍MagnifyGlass.ActivationKey\", this.activationKey);\n this.altRequired = getSettingValue(\"🔍MagnifyGlass.AltRequired\", this.altRequired);\n this.followCursor = getSettingValue(\"🔍MagnifyGlass.FollowCursor\", this.followCursor);\n\n this.offsetStep = getSettingValue(\"🔍MagnifyGlass.OffsetStep\", this.offsetStep);\n this.glassPosition = getSettingValue(\"🔍MagnifyGlass.GlassPosition\", this.glassPosition);\n this.resetKey = getSettingValue(\"🔍MagnifyGlass.ResetKey\", this.resetKey);\n this.glassShape = getSettingValue(\"🔍MagnifyGlass.GlassShape\", this.glassShape);\n this.borderEnabled = getSettingValue(\"🔍MagnifyGlass.BorderEnabled\", this.borderEnabled);\n this.textureFiltering = getSettingValue(\"🔍MagnifyGlass.TextureFiltering\", this.textureFiltering);\n this.alwaysActiveMode = getSettingValue(\"🔍MagnifyGlass.AlwaysActiveMode\", this.alwaysActiveMode);\n this.toggleFollowCursorKey = getSettingValue(\"🔍MagnifyGlass.ToggleFollowCursorKey\", this.toggleFollowCursorKey);\n this.showCursorPreview = getSettingValue(\"🔍MagnifyGlass.ShowCursorPreview\", this.showCursorPreview);\n this.toggleGlassPreviewKey = getSettingValue(\"🔍MagnifyGlass.GlassPreviewToggleHotkey\", this.toggleGlassPreviewKey);\n }\n\n /**\n * Load saved offsets from localStorage.\n */\n loadSavedOffsets(): void {\n try {\n const savedOffsetX = localStorage.getItem(STORAGE_KEYS.OFFSET_X);\n const savedOffsetY = localStorage.getItem(STORAGE_KEYS.OFFSET_Y);\n\n if (savedOffsetX !== null) {\n this.offsetX = parseInt(savedOffsetX, 10);\n } else {\n this.offsetX = 0;\n }\n\n if (savedOffsetY !== null) {\n this.offsetY = parseInt(savedOffsetY, 10);\n } else {\n this.offsetY = 0;\n }\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error loading saved offsets:\", e);\n this.offsetX = 0;\n this.offsetY = 0;\n }\n }\n\n /**\n * Save current offsets to localStorage.\n */\n saveOffsets(): void {\n try {\n localStorage.setItem(STORAGE_KEYS.OFFSET_X, this.offsetX.toString());\n localStorage.setItem(STORAGE_KEYS.OFFSET_Y, this.offsetY.toString());\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error saving offsets:\", e);\n }\n }\n\n /**\n * Reset offsets to default (zero).\n */\n resetOffsets(): void {\n this.offsetX = 0;\n this.offsetY = 0;\n this.saveOffsets();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ConfigManager.js","sources":["../../src/magnify-glass/ConfigManager.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - ConfigManager (TypeScript)\n * \n * Manages configuration and settings for the magnifying glass.\n */\n\nimport { getSettingValue } from '../shared/utils';\nimport { STORAGE_KEYS } from '../shared/constants';\nimport type { GlassPosition, GlassShape, TextureFilter } from '../shared/constants';\n\n// Default settings (we'll import these properly later)\nconst DEFAULT_GLASS_SETTINGS = {\n \"🔍MagnifyGlass.ZoomFactor\": 300,\n \"🔍MagnifyGlass.GlassSize\": 300,\n \"🔍MagnifyGlass.BorderColor\": \"#6b7280\",\n \"🔍MagnifyGlass.BorderWidth\": 1,\n \"🔍MagnifyGlass.ActivationKey\": \"x\",\n \"🔍MagnifyGlass.AltRequired\": false,\n \"🔍MagnifyGlass.FollowCursor\": false,\n \"🔍MagnifyGlass.OffsetStep\": 5,\n \"🔍MagnifyGlass.GlassPosition\": \"Top-Right\" as GlassPosition,\n \"🔍MagnifyGlass.ResetKey\": \"o\",\n \"🔍MagnifyGlass.GlassShape\": \"Rounded Square\" as GlassShape,\n \"🔍MagnifyGlass.BorderEnabled\": true,\n \"🔍MagnifyGlass.TextureFiltering\": \"Linear\" as TextureFilter,\n \"🔍MagnifyGlass.AlwaysActiveMode\": true,\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": \"h\",\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": \"g\",\n \"🔍MagnifyGlass.ShowCursorPreview\": true,\n \"🔍MagnifyGlass.ForceDirectCaptureKey\": \"d\",\n};\n\n/**\n * Configuration manager class.\n * Reads settings from ComfyUI and manages persistent offsets.\n */\nexport class ConfigManager {\n /** Zoom factor (1.0 = 100%, 3.0 = 300%) */\n zoomFactor: number;\n\n /** Glass size in pixels */\n glassSize: number;\n\n /** Border color (CSS color string) */\n borderColor: string;\n\n /** Border width in pixels */\n borderWidth: number;\n\n /** Activation key (lowercase) */\n activationKey: string;\n\n /** Whether Alt key is required for activation */\n altRequired: boolean;\n\n /** Whether glass follows cursor */\n followCursor: boolean;\n\n\n\n /** Offset step for arrow key adjustments */\n offsetStep: number;\n\n /** Glass position relative to cursor */\n glassPosition: string;\n\n /** Reset key (lowercase) */\n resetKey: string;\n\n /** Glass shape */\n glassShape: string;\n\n /** Whether border is enabled */\n borderEnabled: boolean;\n\n /** Texture filtering mode */\n textureFiltering: string;\n\n /** Always active mode */\n alwaysActiveMode: boolean;\n\n /** Toggle follow cursor key */\n toggleFollowCursorKey: string;\n\n /** Glass preview toggle key */\n toggleGlassPreviewKey: string;\n\n /** Show cursor preview in glass */\n showCursorPreview: boolean;\n\n /** Force Direct Capture key */\n forceDirectCaptureKey: string;\n\n /** Runtime flag: Force Direct Capture mode (not persisted) */\n forceDirectCapture: boolean;\n\n /** Manual offset X in graph units */\n offsetX: number;\n\n /** Manual offset Y in graph units */\n offsetY: number;\n\n constructor() {\n // Initialize properties with defaults\n this.zoomFactor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ZoomFactor\"] / 100;\n this.glassSize = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassSize\"];\n this.borderColor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderColor\"];\n this.borderWidth = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderWidth\"];\n this.activationKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ActivationKey\"];\n this.altRequired = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.AltRequired\"];\n this.followCursor = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.FollowCursor\"];\n\n this.offsetStep = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.OffsetStep\"];\n this.glassPosition = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassPosition\"];\n this.resetKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ResetKey\"];\n this.glassShape = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassShape\"];\n this.borderEnabled = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.BorderEnabled\"];\n this.textureFiltering = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.TextureFiltering\"];\n this.alwaysActiveMode = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.AlwaysActiveMode\"];\n this.toggleFollowCursorKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ToggleFollowCursorKey\"];\n this.toggleGlassPreviewKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.GlassPreviewToggleHotkey\"];\n this.showCursorPreview = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ShowCursorPreview\"];\n this.forceDirectCaptureKey = DEFAULT_GLASS_SETTINGS[\"🔍MagnifyGlass.ForceDirectCaptureKey\"];\n this.forceDirectCapture = false; // Runtime flag, not persisted\n\n // Alignment adjustment parameters - managed separately\n this.offsetX = 0;\n this.offsetY = 0;\n }\n\n /**\n * Load settings from ComfyUI settings system.\n */\n loadSettings(): void {\n this.zoomFactor = getSettingValue(\"🔍MagnifyGlass.ZoomFactor\", this.zoomFactor * 100) / 100;\n this.glassSize = getSettingValue(\"🔍MagnifyGlass.GlassSize\", this.glassSize);\n this.borderColor = getSettingValue(\"🔍MagnifyGlass.BorderColor\", this.borderColor);\n this.borderWidth = getSettingValue(\"🔍MagnifyGlass.BorderWidth\", this.borderWidth);\n this.activationKey = getSettingValue(\"🔍MagnifyGlass.ActivationKey\", this.activationKey);\n this.altRequired = getSettingValue(\"🔍MagnifyGlass.AltRequired\", this.altRequired);\n this.followCursor = getSettingValue(\"🔍MagnifyGlass.FollowCursor\", this.followCursor);\n\n this.offsetStep = getSettingValue(\"🔍MagnifyGlass.OffsetStep\", this.offsetStep);\n this.glassPosition = getSettingValue(\"🔍MagnifyGlass.GlassPosition\", this.glassPosition);\n this.resetKey = getSettingValue(\"🔍MagnifyGlass.ResetKey\", this.resetKey);\n this.glassShape = getSettingValue(\"🔍MagnifyGlass.GlassShape\", this.glassShape);\n this.borderEnabled = getSettingValue(\"🔍MagnifyGlass.BorderEnabled\", this.borderEnabled);\n this.textureFiltering = getSettingValue(\"🔍MagnifyGlass.TextureFiltering\", this.textureFiltering);\n this.alwaysActiveMode = getSettingValue(\"🔍MagnifyGlass.AlwaysActiveMode\", this.alwaysActiveMode);\n this.toggleFollowCursorKey = getSettingValue(\"🔍MagnifyGlass.ToggleFollowCursorKey\", this.toggleFollowCursorKey);\n this.showCursorPreview = getSettingValue(\"🔍MagnifyGlass.ShowCursorPreview\", this.showCursorPreview);\n this.forceDirectCaptureKey = getSettingValue(\"🔍MagnifyGlass.ForceDirectCaptureKey\", this.forceDirectCaptureKey);\n this.toggleGlassPreviewKey = getSettingValue(\"🔍MagnifyGlass.GlassPreviewToggleHotkey\", this.toggleGlassPreviewKey);\n }\n\n /**\n * Load saved offsets from localStorage.\n */\n loadSavedOffsets(): void {\n try {\n const savedOffsetX = localStorage.getItem(STORAGE_KEYS.OFFSET_X);\n const savedOffsetY = localStorage.getItem(STORAGE_KEYS.OFFSET_Y);\n\n if (savedOffsetX !== null) {\n this.offsetX = parseInt(savedOffsetX, 10);\n } else {\n this.offsetX = 0;\n }\n\n if (savedOffsetY !== null) {\n this.offsetY = parseInt(savedOffsetY, 10);\n } else {\n this.offsetY = 0;\n }\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error loading saved offsets:\", e);\n this.offsetX = 0;\n this.offsetY = 0;\n }\n }\n\n /**\n * Save current offsets to localStorage.\n */\n saveOffsets(): void {\n try {\n localStorage.setItem(STORAGE_KEYS.OFFSET_X, this.offsetX.toString());\n localStorage.setItem(STORAGE_KEYS.OFFSET_Y, this.offsetY.toString());\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error saving offsets:\", e);\n }\n }\n\n /**\n * Reset offsets to default (zero).\n */\n resetOffsets(): void {\n this.offsetX = 0;\n this.offsetY = 0;\n this.saveOffsets();\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/EventHandler.js b/web/magnify-glass/EventHandler.js index b1cd786..4b255d9 100644 --- a/web/magnify-glass/EventHandler.js +++ b/web/magnify-glass/EventHandler.js @@ -117,6 +117,12 @@ class EventHandler { config.saveOffsets(); this.magnifyGlass.updateMagnifiedView(); } + if (e.key.toLowerCase() === config.forceDirectCaptureKey && (!config.altRequired || e.altKey)) { + config.forceDirectCapture = !config.forceDirectCapture; + this.magnifyGlass.debugger.log(`Force Direct Capture: ${config.forceDirectCapture ? "ON" : "OFF"}`); + this.magnifyGlass.updateMagnifiedView(); + e.preventDefault(); + } if (e.shiftKey && e.key.toLowerCase() === "p") { this.magnifyGlass.popOutManager.toggle(); e.preventDefault(); diff --git a/web/magnify-glass/EventHandler.js.map b/web/magnify-glass/EventHandler.js.map index 68f2f3d..d617ac4 100644 --- a/web/magnify-glass/EventHandler.js.map +++ b/web/magnify-glass/EventHandler.js.map @@ -1 +1 @@ -{"version":3,"file":"EventHandler.js","sources":["../../src/magnify-glass/EventHandler.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - EventHandler (TypeScript)\n * \n * Handles keyboard and mouse events for the magnifying glass.\n */\n\nimport { isUserTyping } from '../shared/utils';\nimport { Logger } from '../shared/logger';\nimport { DEFAULT_PADDING, DEFAULT_GLASS_Y_OFFSET } from '../shared/constants';\nimport type { MagnifyGlass } from './MagnifyGlass';\n\n/**\n * Event Handler class.\n * Manages all keyboard and mouse event handling for the magnifier.\n */\nexport class EventHandler {\n magnifyGlass: MagnifyGlass;\n\n constructor(magnifyGlass: MagnifyGlass) {\n this.magnifyGlass = magnifyGlass;\n\n this.handleKeyDown = this.handleKeyDown.bind(this);\n this.handleKeyUp = this.handleKeyUp.bind(this);\n this.handleMouseMove = this.handleMouseMove.bind(this);\n this.handleResize = this.handleResize.bind(this);\n }\n\n /**\n * Attach all event listeners.\n */\n attachListeners(): void {\n document.addEventListener(\"keydown\", this.handleKeyDown);\n document.addEventListener(\"keyup\", this.handleKeyUp);\n document.addEventListener(\"mousemove\", this.handleMouseMove);\n window.addEventListener(\"resize\", this.handleResize);\n }\n\n /**\n * Detach all event listeners.\n */\n detachListeners(): void {\n document.removeEventListener(\"keydown\", this.handleKeyDown);\n document.removeEventListener(\"keyup\", this.handleKeyUp);\n document.removeEventListener(\"mousemove\", this.handleMouseMove);\n window.removeEventListener(\"resize\", this.handleResize);\n }\n\n /**\n * Handle window resize events.\n */\n handleResize(): void {\n if (this.magnifyGlass && this.magnifyGlass.ui) {\n this.magnifyGlass.ui.updateResponsivePosition();\n // Also update panel position if it exists\n const infoPanel = (window as any).infoPanelManager;\n if (infoPanel && infoPanel.positionManager) {\n // Determine if we should move it\n infoPanel.positionManager.positionPanel();\n }\n }\n }\n\n /**\n * Handle keydown events.\n */\n handleKeyDown(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n // Don't handle hotkeys if user is typing in an input field\n if (isUserTyping()) {\n return;\n }\n\n // Magnifier activation\n Logger.debug(`KeyDown: ${e.key} (config: ${config.activationKey}, active: ${state.active})`);\n if (e.key.toLowerCase() === config.activationKey &&\n (!config.altRequired || e.altKey)) {\n Logger.debug('Activation key matched!');\n if (config.alwaysActiveMode) {\n this.magnifyGlass.toggle();\n } else {\n if (!state.active) {\n this.magnifyGlass.toggle();\n }\n }\n }\n\n // Manual offset adjustment keys (only when magnifier is active)\n if (state.active) {\n let offsetChanged = false;\n const stepSize = config.offsetStep;\n\n if (e.key === \"ArrowUp\") {\n config.offsetY -= stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowDown\") {\n config.offsetY += stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowLeft\") {\n config.offsetX -= stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key === \"ArrowRight\") {\n config.offsetX += stepSize;\n offsetChanged = true;\n e.preventDefault();\n } else if (e.key.toLowerCase() === config.resetKey.toLowerCase() &&\n (!config.altRequired || e.altKey)) {\n this.magnifyGlass.resetOffsets();\n offsetChanged = true;\n e.preventDefault();\n }\n\n // Toggle Follow Cursor Key\n if (e.key.toLowerCase() === config.toggleFollowCursorKey &&\n (!config.altRequired || e.altKey)) {\n config.followCursor = !config.followCursor;\n this.magnifyGlass.debugger.log(`Follow Cursor Toggled: ${config.followCursor ? 'ON' : 'OFF'}`);\n if (state.active && config.followCursor) {\n this.magnifyGlass.ui.positionGlass(\n this.magnifyGlass.lastKnownMousePosition.x,\n this.magnifyGlass.lastKnownMousePosition.y\n );\n }\n e.preventDefault();\n }\n\n // Toggle Glass Preview Key (G) - mimics the hover controls button\n if (e.key.toLowerCase() === config.toggleGlassPreviewKey &&\n (!config.altRequired || e.altKey)) {\n // Find and click the toggle-glass button in the info panel hover controls\n const toggleGlassBtn = document.querySelector('[data-action=\"toggle-glass\"]') as HTMLElement;\n if (toggleGlassBtn) {\n toggleGlassBtn.click();\n this.magnifyGlass.debugger.log(`Glass Preview Toggled (via button)`);\n } else {\n // Fallback: toggle via UI directly if button not found\n const glassDiv = this.magnifyGlass.ui.glassDiv;\n if (glassDiv) {\n const isVisible = glassDiv.style.display !== 'none' && glassDiv.style.opacity !== '0';\n this.magnifyGlass.ui.setPreviewVisibility(!isVisible);\n this.magnifyGlass.debugger.log(`Glass Preview Toggled (fallback): ${!isVisible ? 'ON' : 'OFF'}`);\n }\n }\n e.preventDefault();\n }\n\n if (offsetChanged) {\n config.saveOffsets();\n this.magnifyGlass.updateMagnifiedView();\n }\n\n // Pop-out tab toggle (Shift+P)\n if (e.shiftKey && e.key.toLowerCase() === 'p') {\n this.magnifyGlass.popOutManager.toggle();\n e.preventDefault();\n }\n }\n }\n\n /**\n * Handle keyup events.\n */\n handleKeyUp(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n if (!config.alwaysActiveMode &&\n (e.key.toLowerCase() === config.activationKey ||\n (config.altRequired && e.key === \"Alt\"))) {\n\n let shouldDeactivate = false;\n if (config.altRequired) {\n if (e.key === \"Alt\" || e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n } else {\n if (e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n }\n\n if (state.active && shouldDeactivate) {\n state.active = false;\n this.magnifyGlass.ui.hide();\n }\n }\n }\n\n /**\n * Handle mousemove events.\n */\n handleMouseMove(e: MouseEvent): void {\n // Always track mouse position\n this.magnifyGlass.lastKnownMousePosition.x = e.clientX;\n this.magnifyGlass.lastKnownMousePosition.y = e.clientY;\n\n if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const cssMouseXOnCanvas = e.clientX - rect.left;\n const cssMouseYOnCanvas = e.clientY - rect.top;\n\n // Check if the cursor is over the canvas element\n if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width &&\n cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) {\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY);\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n\n /**\n * Update the initial position when magnifier is activated.\n */\n updateInitialPosition(): void {\n if (!this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const clientX = this.magnifyGlass.lastKnownMousePosition.x;\n const clientY = this.magnifyGlass.lastKnownMousePosition.y;\n\n const isOverCanvas = clientX >= rect.left && clientX <= rect.right &&\n clientY >= rect.top && clientY <= rect.bottom;\n\n if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) {\n const cssMouseXOnCanvas = clientX - rect.left;\n const cssMouseYOnCanvas = clientY - rect.top;\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n const glassSize = this.magnifyGlass.config.glassSize;\n\n if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) {\n // First activation - position at top right of the window (Anchored)\n // Using style.right ensures it stays on the right even if window resizes before first usage\n this.magnifyGlass.ui.glassDiv.style.right = `${DEFAULT_PADDING}px`;\n this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.magnifyGlass.ui.glassDiv.style.left = 'auto'; // Ensure left is unset\n this.magnifyGlass.state.wasActivatedBefore = true;\n } else {\n this.magnifyGlass.ui.positionGlass(clientX, clientY);\n }\n\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"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 = 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 // Force Direct Capture Toggle Key\n if (e.key.toLowerCase() === config.forceDirectCaptureKey &&\n (!config.altRequired || e.altKey)) {\n config.forceDirectCapture = !config.forceDirectCapture;\n this.magnifyGlass.debugger.log(`Force Direct Capture: ${config.forceDirectCapture ? 'ON' : 'OFF'}`);\n this.magnifyGlass.updateMagnifiedView();\n e.preventDefault();\n }\n\n // Pop-out tab toggle (Shift+P)\n if (e.shiftKey && e.key.toLowerCase() === 'p') {\n this.magnifyGlass.popOutManager.toggle();\n e.preventDefault();\n }\n }\n }\n\n /**\n * Handle keyup events.\n */\n handleKeyUp(e: KeyboardEvent): void {\n const config = this.magnifyGlass.config;\n const state = this.magnifyGlass.state;\n\n if (!config.alwaysActiveMode &&\n (e.key.toLowerCase() === config.activationKey ||\n (config.altRequired && e.key === \"Alt\"))) {\n\n let shouldDeactivate = false;\n if (config.altRequired) {\n if (e.key === \"Alt\" || e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n } else {\n if (e.key.toLowerCase() === config.activationKey) {\n shouldDeactivate = true;\n }\n }\n\n if (state.active && shouldDeactivate) {\n state.active = false;\n this.magnifyGlass.ui.hide();\n }\n }\n }\n\n /**\n * Handle mousemove events.\n */\n handleMouseMove(e: MouseEvent): void {\n // Always track mouse position\n this.magnifyGlass.lastKnownMousePosition.x = e.clientX;\n this.magnifyGlass.lastKnownMousePosition.y = e.clientY;\n\n if (!this.magnifyGlass.state.active || !this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const cssMouseXOnCanvas = e.clientX - rect.left;\n const cssMouseYOnCanvas = e.clientY - rect.top;\n\n // Check if the cursor is over the canvas element\n if (cssMouseXOnCanvas >= 0 && cssMouseXOnCanvas <= rect.width &&\n cssMouseYOnCanvas >= 0 && cssMouseYOnCanvas <= rect.height) {\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n this.magnifyGlass.ui.positionGlass(e.clientX, e.clientY);\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n\n /**\n * Update the initial position when magnifier is activated.\n */\n updateInitialPosition(): void {\n if (!this.magnifyGlass.litegraphCanvas) return;\n\n const rect = this.magnifyGlass.litegraphCanvas.getBoundingClientRect();\n const clientX = this.magnifyGlass.lastKnownMousePosition.x;\n const clientY = this.magnifyGlass.lastKnownMousePosition.y;\n\n const isOverCanvas = clientX >= rect.left && clientX <= rect.right &&\n clientY >= rect.top && clientY <= rect.bottom;\n\n if (this.magnifyGlass.config.alwaysActiveMode || isOverCanvas) {\n const cssMouseXOnCanvas = clientX - rect.left;\n const cssMouseYOnCanvas = clientY - rect.top;\n\n const canvasElement = this.magnifyGlass.litegraphCanvas;\n const scaleX = rect.width > 0 ? canvasElement.width / rect.width : 1;\n const scaleY = rect.height > 0 ? canvasElement.height / rect.height : 1;\n\n const pixelX = cssMouseXOnCanvas * scaleX;\n const pixelY = cssMouseYOnCanvas * scaleY;\n\n this.magnifyGlass.state.x = pixelX;\n this.magnifyGlass.state.y = pixelY;\n\n const glassSize = this.magnifyGlass.config.glassSize;\n\n if (!this.magnifyGlass.state.wasActivatedBefore && this.magnifyGlass.ui.glassDiv) {\n // First activation - position at top right of the window (Anchored)\n // Using style.right ensures it stays on the right even if window resizes before first usage\n this.magnifyGlass.ui.glassDiv.style.right = `${DEFAULT_PADDING}px`;\n this.magnifyGlass.ui.glassDiv.style.top = `${DEFAULT_GLASS_Y_OFFSET}px`;\n this.magnifyGlass.ui.glassDiv.style.left = 'auto'; // Ensure left is unset\n this.magnifyGlass.state.wasActivatedBefore = true;\n } else {\n this.magnifyGlass.ui.positionGlass(clientX, clientY);\n }\n\n this.magnifyGlass.updateMagnifiedView();\n }\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/web/magnify-glass/OffscreenRenderer.js b/web/magnify-glass/OffscreenRenderer.js index 0fce8b9..cd6c6f2 100644 --- a/web/magnify-glass/OffscreenRenderer.js +++ b/web/magnify-glass/OffscreenRenderer.js @@ -35,7 +35,7 @@ const _OffscreenRenderer = class _OffscreenRenderer { const currentScale = lgCanvas.ds.scale; const rect = targetCanvas.getBoundingClientRect(); const dpr = rect.width > 0 ? targetCanvas.width / rect.width : 1; - const useDirectCapture = currentScale >= 1; + const useDirectCapture = currentScale >= 1 || this.config.forceDirectCapture; if (useDirectCapture) { this.cachedVirtualZoomResult = null; return this.renderDirectCapture(targetCanvas, renderSize, dpr); diff --git a/web/magnify-glass/OffscreenRenderer.js.map b/web/magnify-glass/OffscreenRenderer.js.map index d5382ec..5b4cbac 100644 --- a/web/magnify-glass/OffscreenRenderer.js.map +++ b/web/magnify-glass/OffscreenRenderer.js.map @@ -1 +1 @@ -{"version":3,"file":"OffscreenRenderer.js","sources":["../../src/magnify-glass/OffscreenRenderer.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Offscreen Renderer\n * \n * Hybrid Renderer:\n * 1. Direct Capture (Nuclear) for near-100% zoom (Perfect Alignment, Good Quality).\n * 2. Virtual Zoom (High Res) for zoomed-out views (High Detail, sensitive alignment).\n */\n\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\n\ndeclare const app: any;\n\nexport class OffscreenRenderer {\n private config: ConfigManager;\n private state: MagnifierState;\n private offscreenCanvas: HTMLCanvasElement | null = null;\n private offscreenCtx: CanvasRenderingContext2D | null = null;\n private lastRenderWidth: number = 0;\n private lastRenderHeight: number = 0;\n private isCapturing: boolean = false;\n\n // Virtual Zoom Throttling (60 FPS = ~16ms per frame)\n private static readonly VIRTUAL_ZOOM_MIN_INTERVAL_MS = 16;\n private lastVirtualZoomTime: number = 0;\n private cachedVirtualZoomResult: HTMLCanvasElement | null = null;\n\n constructor(config: ConfigManager, state: MagnifierState) {\n this.config = config;\n this.state = state;\n this.initOffscreenCanvas();\n }\n\n private initOffscreenCanvas(): void {\n this.offscreenCanvas = document.createElement('canvas');\n this.offscreenCtx = this.offscreenCanvas.getContext('2d');\n }\n\n renderHighResRegion(targetCanvas: HTMLCanvasElement): HTMLCanvasElement | null {\n if (!this.offscreenCanvas || !this.offscreenCtx) return null;\n if (this.isCapturing) return null;\n\n const lgCanvas = app?.canvas;\n if (!lgCanvas || !lgCanvas.ds) return null;\n\n const renderSize = this.config.glassSize;\n\n // Resize offscreen canvas\n if (this.lastRenderWidth !== renderSize || this.lastRenderHeight !== renderSize) {\n this.offscreenCanvas.width = renderSize;\n this.offscreenCanvas.height = renderSize;\n this.lastRenderWidth = renderSize;\n this.lastRenderHeight = renderSize;\n }\n\n const currentScale = lgCanvas.ds.scale;\n const rect = targetCanvas.getBoundingClientRect();\n // Calculate DPR exactly as EventHandler does (Canvas Pixels / CSS Pixels)\n const dpr = rect.width > 0 ? targetCanvas.width / rect.width : 1;\n\n // Threshold for Hybrid Mode\n // If we are zoomed in enough (>= 100%), Direct Capture quality is perfect,\n // and it guarantees perfect alignment with zero extra draws.\n // If we are zoomed out (< 100%), we use High Res Virtual Zoom for crisp details.\n // Note: Image preview nodes are handled by drawImagePreviewsNatively() during Virtual Zoom.\n const useDirectCapture = currentScale >= 1.0;\n\n if (useDirectCapture) {\n // Direct Capture: Full speed, no extra draws\n this.cachedVirtualZoomResult = null; // Invalidate cache when switching modes\n return this.renderDirectCapture(targetCanvas, renderSize, dpr);\n } else {\n // Virtual Zoom: Throttled to 60 FPS to prevent FPS counter inflation\n const now = performance.now();\n const elapsed = now - this.lastVirtualZoomTime;\n\n if (elapsed < OffscreenRenderer.VIRTUAL_ZOOM_MIN_INTERVAL_MS && this.cachedVirtualZoomResult) {\n // Return cached result if within throttle window\n return this.cachedVirtualZoomResult;\n }\n\n // Perform the expensive virtual zoom render\n const result = this.renderVirtualZoom(lgCanvas, targetCanvas, renderSize, currentScale, dpr);\n this.lastVirtualZoomTime = now;\n this.cachedVirtualZoomResult = result;\n return result;\n }\n }\n\n /**\n * Detect if there are nodes with image previews in the capture region.\n * These nodes (Save Image, Preview Image, etc.) have images that are drawn\n * via onDrawBackground/onDrawForeground and cause artifacts during Virtual Zoom.\n */\n private detectImagePreviewNodes(dpr: number, scale: number, offset: number[]): boolean {\n const graph = app?.graph;\n if (!graph || !graph._nodes) return false;\n\n const renderSize = this.config.glassSize;\n const sourceSizeCss = renderSize / this.config.zoomFactor;\n\n // Mouse position in CSS pixels\n const pivotCssX = this.state.x / dpr;\n const pivotCssY = this.state.y / dpr;\n\n // Capture region in CSS pixels\n const captureLeft = pivotCssX - (sourceSizeCss / 2);\n const captureTop = pivotCssY - (sourceSizeCss / 2);\n const captureRight = captureLeft + sourceSizeCss;\n const captureBottom = captureTop + sourceSizeCss;\n\n // Node types that have image previews\n const imagePreviewTypes = [\n 'SaveImage', 'PreviewImage', 'LoadImage', 'LoadImageMask',\n 'VHS_LoadVideo', 'VHS_VideoCombine', // Video nodes\n 'ImagePreview', 'ShowImage' // Common variants\n ];\n\n for (const node of graph._nodes) {\n if (!node.pos || !node.size) continue;\n if (node.flags?.collapsed) continue;\n\n // Check if this node type has image previews\n const nodeType = node.type || node.comfyClass || '';\n const hasImagePreview = imagePreviewTypes.some(t =>\n nodeType.toLowerCase().includes(t.toLowerCase())\n ) || node.imgs?.length > 0; // Also check for imgs array directly\n\n if (!hasImagePreview) continue;\n\n // Calculate node position in CSS pixels\n const nodeCssX = node.pos[0] * scale + offset[0];\n const nodeCssY = node.pos[1] * scale + offset[1];\n const nodeCssWidth = node.size[0] * scale;\n const nodeCssHeight = node.size[1] * scale;\n\n // Check if node overlaps with capture region\n const nodeRight = nodeCssX + nodeCssWidth;\n const nodeBottom = nodeCssY + nodeCssHeight;\n\n const overlaps = !(nodeRight < captureLeft || nodeCssX > captureRight ||\n nodeBottom < captureTop || nodeCssY > captureBottom);\n\n if (overlaps) {\n return true; // Found an image preview node in capture region\n }\n }\n\n return false;\n }\n\n /**\n * Mode 1: Direct Capture (Nuclear Fix)\n * Copies pixels directly from screen. Zero drift.\n */\n private renderDirectCapture(targetCanvas: HTMLCanvasElement, renderSize: number, dpr: number): HTMLCanvasElement | null {\n try {\n this.isCapturing = true;\n\n // state.x/y are Backing Pixels. pivotCss is Backing / DPR.\n const pivotCssX = this.state.x / dpr;\n const pivotCssY = this.state.y / dpr;\n\n const sourceSizeCss = renderSize / this.config.zoomFactor;\n\n // sourceRect in Backing Pixels\n const sourceWidth = sourceSizeCss * dpr;\n const sourceHeight = sourceSizeCss * dpr;\n const sourceX = (pivotCssX - (sourceSizeCss / 2)) * dpr;\n const sourceY = (pivotCssY - (sourceSizeCss / 2)) * dpr;\n\n this.offscreenCtx!.clearRect(0, 0, renderSize, renderSize);\n this.offscreenCtx!.drawImage(\n targetCanvas,\n sourceX, sourceY, sourceWidth, sourceHeight,\n 0, 0, renderSize, renderSize\n );\n\n // Draw native widget text on top of the captured canvas\n const lgCanvas = app?.canvas;\n const currentScale = lgCanvas?.ds?.scale ?? 1.0;\n const currentOffset: [number, number] = lgCanvas?.ds?.offset ? [lgCanvas.ds.offset[0], lgCanvas.ds.offset[1]] : [0, 0];\n this.drawWidgetTextNatively(sourceX, sourceY, sourceWidth, sourceHeight, renderSize, currentScale, currentOffset);\n\n // NOTE: We do NOT call drawImagePreviewsNatively() here.\n // Direct Capture copies pixels from the screen which already has correctly-drawn images.\n // Native image drawing is only needed during Virtual Zoom where ComfyUI's rendering fails.\n\n // Draw cursor preview overlay if enabled\n if (this.config.showCursorPreview) {\n this.drawCursorPreview(renderSize);\n }\n\n this.isCapturing = false;\n return this.offscreenCanvas;\n } catch (e) {\n console.warn(\"DirectCapture failed\", e);\n this.isCapturing = false;\n return null;\n }\n }\n\n /**\n * Mode 2: Virtual Zoom (High Res)\n * Temporarily sets scale to 1.0 to render details.\n * Uses LiteGraph's setZoom() API for correct pivot handling.\n */\n private renderVirtualZoom(lgCanvas: any, targetCanvas: HTMLCanvasElement, renderSize: number, currentScale: number, dpr: number): HTMLCanvasElement | null {\n try {\n this.isCapturing = true;\n // Set global flag so node renderers can check and skip expensive operations\n (window as any).__magnifyGlassCapturing = true;\n\n // Pivot = Mouse in CSS Pixels (relative to canvas element)\n const pivotCssX = this.state.x / dpr;\n const pivotCssY = this.state.y / dpr;\n\n // Target Scale (1.0 for high-res capture)\n const targetScale = Math.max(1.0, currentScale);\n\n // Store original state for restoration\n const origScale = lgCanvas.ds.scale;\n const origOffsetX = lgCanvas.ds.offset[0];\n const origOffsetY = lgCanvas.ds.offset[1];\n\n // Use LiteGraph's setZoom API - this properly handles the pivot point\n // The center parameter tells LiteGraph to zoom around that screen position\n if (typeof lgCanvas.setZoom === 'function') {\n lgCanvas.setZoom(targetScale, [pivotCssX, pivotCssY]);\n } else {\n // Fallback: manual calculation (legacy, may drift)\n const pivotGraphX = (pivotCssX - origOffsetX) / currentScale;\n const pivotGraphY = (pivotCssY - origOffsetY) / currentScale;\n const newOffsetX = pivotCssX - (pivotGraphX * targetScale);\n const newOffsetY = pivotCssY - (pivotGraphY * targetScale);\n lgCanvas.ds.scale = targetScale;\n lgCanvas.ds.offset[0] = newOffsetX;\n lgCanvas.ds.offset[1] = newOffsetY;\n }\n\n // Hide images AND preview widgets from nodes to prevent them from being drawn on the captured canvas\n // This prevents \"ghosting\" artifacts and crashes (since some custom widgets fail in Virtual Zoom)\n const hiddenNodeImages = new Map();\n const hiddenNodeWidgets = new Map();\n\n if (app?.graph?._nodes) {\n for (const node of app.graph._nodes) {\n // 1. Hide node.imgs\n if (node.imgs) {\n hiddenNodeImages.set(node, node.imgs);\n node.imgs = null;\n }\n\n // 2. Hide preview widgets\n if (node.widgets && Array.isArray(node.widgets)) {\n const hasPreviewWidget = node.widgets.some((w: any) => {\n const wName = String(w.name || '').toLowerCase();\n const wType = String(w.type || '').toLowerCase();\n return wName.includes('preview') || wName.includes('image') || wName.includes('gallery') || wName.includes('upload') ||\n wType.includes('preview') || wType.includes('image');\n });\n\n if (hasPreviewWidget) {\n // Backup original widgets array\n hiddenNodeWidgets.set(node, node.widgets);\n // Filter out the preview widgets for this draw call\n node.widgets = node.widgets.filter((w: any) => {\n const wName = String(w.name || '').toLowerCase();\n const wType = String(w.type || '').toLowerCase();\n // Only hide explicit preview widgets. Inputs (image, upload) should remain visible.\n const isPreview = wName.includes('preview') || wName.includes('gallery') ||\n wType.includes('preview');\n return !isPreview;\n });\n }\n }\n }\n }\n\n try {\n lgCanvas.draw(true, true);\n } finally {\n // Restore images\n for (const [node, imgs] of hiddenNodeImages.entries()) {\n node.imgs = imgs;\n }\n // Restore widgets\n for (const [node, widgets] of hiddenNodeWidgets.entries()) {\n node.widgets = widgets;\n }\n }\n\n // Capture pixels around the pivot point\n const sourceSizeCss = renderSize / this.config.zoomFactor;\n const sourceWidth = sourceSizeCss * dpr;\n const sourceHeight = sourceSizeCss * dpr;\n const sourceX = (pivotCssX - (sourceSizeCss / 2)) * dpr;\n const sourceY = (pivotCssY - (sourceSizeCss / 2)) * dpr;\n\n this.offscreenCtx!.clearRect(0, 0, renderSize, renderSize);\n this.offscreenCtx!.drawImage(\n targetCanvas,\n sourceX, sourceY, sourceWidth, sourceHeight,\n 0, 0, renderSize, renderSize\n );\n\n // Draw native widget text on top of the captured canvas\n // Note: For virtual zoom, we use targetScale and the CURRENT offset (after setZoom)\n // because lgCanvas.setZoom() modified the offset to zoom around the pivot\n const captureOffset: [number, number] = [lgCanvas.ds.offset[0], lgCanvas.ds.offset[1]];\n this.drawWidgetTextNatively(sourceX, sourceY, sourceWidth, sourceHeight, renderSize, targetScale, captureOffset);\n\n // Draw image/video previews natively on top of the captured canvas\n this.drawImagePreviewsNatively(sourceX, sourceY, sourceWidth, sourceHeight, renderSize, targetScale, captureOffset);\n\n // Draw cursor preview overlay if enabled\n if (this.config.showCursorPreview) {\n this.drawCursorPreview(renderSize);\n }\n\n // Restore original zoom state by directly setting the saved values.\n // IMPORTANT: Do NOT use setZoom() here! setZoom() recalculates offset from a pivot point,\n // which introduces floating-point drift (1-2px per call) causing vertical/horizontal drift.\n // We must restore the EXACT original offset values to prevent cumulative drift.\n lgCanvas.ds.scale = origScale;\n lgCanvas.ds.offset[0] = origOffsetX;\n lgCanvas.ds.offset[1] = origOffsetY;\n lgCanvas.draw(true, true);\n\n this.isCapturing = false;\n (window as any).__magnifyGlassCapturing = false;\n return this.offscreenCanvas;\n\n } catch (e) {\n console.warn(\"VirtualZoom failed\", e);\n // Emergency restore\n lgCanvas.ds.scale = currentScale;\n lgCanvas.draw(true, true);\n this.isCapturing = false;\n (window as any).__magnifyGlassCapturing = false;\n return null;\n }\n }\n\n /**\n * Draw widget text natively on the offscreen canvas.\n * This renders text content that would otherwise be lost since widgets are DOM elements.\n * \n * @param sourceX - Source X position in backing pixels\n * @param sourceY - Source Y position in backing pixels\n * @param sourceWidth - Source width in backing pixels\n * @param sourceHeight - Source height in backing pixels\n * @param renderSize - Output render size in pixels\n * @param scale - Canvas scale used during capture\n * @param offset - Canvas offset [x, y] used during capture\n */\n private drawWidgetTextNatively(\n sourceX: number,\n sourceY: number,\n sourceWidth: number,\n sourceHeight: number,\n renderSize: number,\n scale: number,\n offset: [number, number]\n ): void {\n const graph = app?.graph;\n if (!graph || !graph._nodes || !this.offscreenCtx) return;\n\n const ctx = this.offscreenCtx;\n const lgCanvas = app?.canvas;\n if (!lgCanvas || !lgCanvas.ds) return;\n\n // LiteGraph layout constants\n const TITLE_HEIGHT = 30;\n const WIDGET_HEIGHT = 20;\n const WIDGET_MARGIN = 4;\n const PADDING = 15;\n\n // Use the passed offset (capture-time offset), not current lgCanvas offset\n // const ds = lgCanvas.ds;\n // const offset = ds.offset || [0, 0]; // OLD: This was wrong\n\n // The offset from LiteGraph is in CSS pixels\n // The sourceX/Y/Width/Height are in backing pixels\n // We need to convert properly\n\n // Calculate DPR from the source parameters\n const sourceSizeCss = renderSize / this.config.zoomFactor;\n const actualDpr = sourceWidth / sourceSizeCss;\n const captureScale = renderSize / sourceWidth;\n\n // Source region in CSS pixels\n const sourceCssX = sourceX / actualDpr;\n const sourceCssY = sourceY / actualDpr;\n const sourceCssWidth = sourceWidth / actualDpr;\n const sourceCssHeight = sourceHeight / actualDpr;\n\n for (const node of graph._nodes) {\n if (!node.widgets || !node.pos || !node.size) continue;\n if (node.flags?.collapsed) continue;\n\n // Calculate node position in CSS pixels\n // node.pos is in graph coordinates, transform: screenCss = graphPos * scale + offset\n const nodeCssX = node.pos[0] * scale + offset[0];\n const nodeCssY = node.pos[1] * scale + offset[1];\n const nodeCssWidth = node.size[0] * scale;\n const nodeCssHeight = node.size[1] * scale;\n\n // Check if node is in captured region (CSS coordinates)\n if (nodeCssX + nodeCssWidth < sourceCssX || nodeCssX > sourceCssX + sourceCssWidth) continue;\n if (nodeCssY + nodeCssHeight < sourceCssY || nodeCssY > sourceCssY + sourceCssHeight) continue;\n\n // Calculate widget positions\n let widgetY = TITLE_HEIGHT;\n\n for (const widget of node.widgets) {\n // Use widget.last_y if available, otherwise use calculated position\n const widgetLocalY = (widget as any).last_y ?? widgetY;\n\n // Check if widget type contains text AND is a multi-line text area\n // We only want to draw our custom rendering for multi-line text widgets (like CLIP prompts)\n // NOT for simple single-line string inputs (like filename_prefix) which are already canvas-rendered\n const widgetType = String(widget.type || '').toLowerCase();\n\n // Check if this is a multi-line text widget that needs our native rendering\n // Multi-line text widgets typically have:\n // - type 'customtext' (CLIP text encode uses this)\n // - computedHeight >= 40 (multi-line areas are taller)\n // Single-line string inputs typically have:\n // - type 'string' with no computedHeight or small computedHeight\n // - These are already rendered on the canvas by ComfyUI\n const computedHeight = (widget as any).computedHeight || 0;\n const isMarkdownWidget = widgetType === 'markdown'; // ComfyUI standard type\n\n const isMultiLineWidget = isMarkdownWidget || widgetType === 'customtext' ||\n ((widgetType === 'text' || widgetType === 'textarea') && computedHeight >= 40);\n\n // Common calculations for positioning\n const widgetCssX = nodeCssX + (PADDING * scale);\n const widgetCssY = nodeCssY + (widgetLocalY * scale);\n const widgetCssWidth = nodeCssWidth - (PADDING * 2 * scale);\n\n // Convert from CSS to offscreen canvas coordinates\n const canvasX = (widgetCssX - sourceCssX) * actualDpr * captureScale;\n const canvasY = (widgetCssY - sourceCssY) * actualDpr * captureScale;\n const widgetWidth = widgetCssWidth * actualDpr * captureScale;\n\n // Calculate font size\n const baseFontSize = 13;\n const fontSize = Math.max(10, Math.min(28, baseFontSize * scale * actualDpr * captureScale));\n const lineHeight = fontSize * 1.4;\n\n // ----------------------------------------------------------------\n // RENDER TEXT / MARKDOWN\n // ----------------------------------------------------------------\n if (isMultiLineWidget && widget.value !== undefined && widget.value !== null) {\n const textValue = String(widget.value);\n if (textValue.length > 0) {\n // Calculate required height based on content\n let contentHeight = 0;\n if (isMarkdownWidget) {\n // Rough estimation of height needed\n ctx.save();\n ctx.font = `500 ${fontSize}px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif`;\n const maxWidth = widgetWidth - 12;\n\n const lines = textValue.split('\\n');\n for (const line of lines) {\n const words = line.split(/(\\s+)/);\n let currentLine = '';\n let lineCount = 1;\n for (const word of words) {\n if (ctx.measureText(currentLine + word).width > maxWidth) {\n lineCount++;\n currentLine = word;\n } else {\n currentLine += word;\n }\n }\n contentHeight += lineCount * lineHeight;\n }\n ctx.restore();\n\n // Add padding\n contentHeight += 16;\n }\n\n // Calculate available height in canvas pixels to prevent spilling\n // Node bottom in CSS pixels\n const nodeBottomCss = nodeCssY + nodeCssHeight;\n // Max height for this widget in CSS pixels (stay inside node, minus padding)\n const maxWidgetHeightCss = Math.max(0, nodeBottomCss - widgetCssY - (PADDING * scale));\n // Convert to canvas pixels\n const maxWidgetHeight = maxWidgetHeightCss * actualDpr * captureScale;\n\n // Calculate widget height - Render full content for markdown up to node bounds\n const widgetHeight = (isMarkdownWidget)\n ? Math.min(Math.max((widget as any).computedHeight * scale * captureScale || 0, contentHeight), maxWidgetHeight)\n : ((widget as any).computedHeight\n ? (widget as any).computedHeight * scale * captureScale\n : Math.max(80, fontSize * 5));\n\n // Check if widget is at least partially visible (more lenient bounds check)\n const widgetRight = canvasX + widgetWidth;\n const widgetBottom = canvasY + widgetHeight;\n const isVisible = widgetRight > 0 && canvasX < renderSize &&\n widgetBottom > 0 && canvasY < renderSize;\n\n if (isVisible) {\n ctx.save();\n\n // Container bounds\n const containerX = canvasX - 4;\n const containerY = canvasY - 4;\n const containerWidth = widgetWidth + 8;\n const containerHeight = widgetHeight + 8;\n const borderRadius = 8;\n\n // Draw background container with rounded corners\n ctx.beginPath();\n ctx.roundRect(containerX, containerY, containerWidth, containerHeight, borderRadius);\n ctx.fillStyle = '#1e1e1e'; // Slightly lighter dark background\n ctx.fill();\n\n // Draw border\n ctx.strokeStyle = '#3a3a3a';\n ctx.lineWidth = 1;\n ctx.stroke();\n\n // Clip to container bounds to prevent text spill\n ctx.beginPath();\n ctx.roundRect(containerX, containerY, containerWidth, containerHeight, borderRadius);\n ctx.clip();\n\n // Use advanced markdown rendering instead of simple text\n this.drawMarkdown(ctx, textValue, canvasX + 4, canvasY + 4, widgetWidth - 12, widgetHeight - 8, fontSize);\n\n ctx.restore();\n }\n }\n }\n\n widgetY += WIDGET_HEIGHT + WIDGET_MARGIN;\n }\n }\n }\n\n /**\n * Draw image and video previews natively on the offscreen canvas.\n * This renders preview content that would otherwise cause errors during Virtual Zoom,\n * because ComfyUI's ImagePreviewWidget fails when the canvas scale is manipulated.\n * \n * Handles:\n * - node.imgs[] array (standard ComfyUI SaveImage/PreviewImage nodes)\n * - VHS-style DOM widget previews (video/image elements)\n * \n * @param sourceX - Source X position in backing pixels\n * @param sourceY - Source Y position in backing pixels\n * @param sourceWidth - Source width in backing pixels\n * @param sourceHeight - Source height in backing pixels\n * @param renderSize - Output render size in pixels\n * @param scale - Canvas scale used during capture\n * @param offset - Canvas offset [x, y] used during capture\n */\n private drawImagePreviewsNatively(\n sourceX: number,\n sourceY: number,\n sourceWidth: number,\n sourceHeight: number,\n renderSize: number,\n scale: number,\n offset: [number, number]\n ): void {\n const graph = app?.graph;\n if (!graph || !graph._nodes || !this.offscreenCtx) return;\n\n const ctx = this.offscreenCtx;\n\n // Calculate coordinate transforms (same as drawWidgetTextNatively)\n const sourceSizeCss = renderSize / this.config.zoomFactor;\n const actualDpr = sourceWidth / sourceSizeCss;\n const captureScale = renderSize / sourceWidth;\n\n // Source region in CSS pixels\n const sourceCssX = sourceX / actualDpr;\n const sourceCssY = sourceY / actualDpr;\n const sourceCssWidth = sourceWidth / actualDpr;\n const sourceCssHeight = sourceHeight / actualDpr;\n\n // LiteGraph constants\n const TITLE_HEIGHT = 30;\n const WIDGET_MARGIN = 4;\n\n for (const node of graph._nodes) {\n if (!node.pos || !node.size) continue;\n if (node.flags?.collapsed) continue;\n\n // Calculate node position in CSS pixels\n const nodeCssX = node.pos[0] * scale + offset[0];\n const nodeCssY = node.pos[1] * scale + offset[1];\n const nodeCssWidth = node.size[0] * scale;\n const nodeCssHeight = node.size[1] * scale;\n\n // Check if node is in captured region\n if (nodeCssX + nodeCssWidth < sourceCssX || nodeCssX > sourceCssX + sourceCssWidth) continue;\n if (nodeCssY + nodeCssHeight < sourceCssY || nodeCssY > sourceCssY + sourceCssHeight) continue;\n\n // Method 1: Handle node.imgs[] array (standard ComfyUI preview)\n if (node.imgs && Array.isArray(node.imgs) && node.imgs.length > 0) {\n this.drawNodeImages(\n ctx, node, nodeCssX, nodeCssY, nodeCssWidth, nodeCssHeight,\n sourceCssX, sourceCssY, actualDpr, captureScale, renderSize, scale\n );\n }\n\n // Method 2: Handle VHS-style DOM widget previews\n if (node.widgets) {\n for (const widget of node.widgets) {\n const widgetName = String(widget.name || '').toLowerCase();\n if (widgetName === 'videopreview' || widgetName === 'audiopreview') {\n this.drawDomWidgetPreview(\n ctx, widget, node, nodeCssX, nodeCssY, nodeCssWidth,\n sourceCssX, sourceCssY, actualDpr, captureScale, renderSize, scale\n );\n }\n }\n }\n }\n }\n\n /**\n * Draw images from node.imgs[] array onto the offscreen canvas.\n */\n private drawNodeImages(\n ctx: CanvasRenderingContext2D,\n node: any,\n nodeCssX: number,\n nodeCssY: number,\n nodeCssWidth: number,\n nodeCssHeight: number,\n sourceCssX: number,\n sourceCssY: number,\n actualDpr: number,\n captureScale: number,\n renderSize: number,\n scale: number\n ): void {\n const TITLE_HEIGHT = 30;\n const PADDING = 10;\n\n // Calculate image area (below title, with padding)\n const imageAreaY = nodeCssY + (TITLE_HEIGHT * scale);\n const imageAreaWidth = nodeCssWidth - (PADDING * 2 * scale);\n const imageAreaHeight = nodeCssHeight - (TITLE_HEIGHT * scale) - (PADDING * scale);\n\n if (imageAreaWidth <= 0 || imageAreaHeight <= 0) return;\n\n // For simplicity, draw the first image that fits\n // ComfyUI typically shows images in a grid, but for the magnifier we'll show the first/selected one\n const imgs = node.imgs;\n const imageIndex = node.imageIndex ?? 0;\n const img = imgs[Math.min(imageIndex, imgs.length - 1)];\n\n if (!img || !(img instanceof HTMLImageElement) || !img.complete || img.naturalWidth === 0) {\n return;\n }\n\n try {\n // Calculate aspect-ratio-preserving dimensions\n const imgAspect = img.naturalWidth / img.naturalHeight;\n const areaAspect = imageAreaWidth / imageAreaHeight;\n\n let drawWidth = imageAreaWidth;\n let drawHeight = imageAreaHeight;\n\n if (imgAspect > areaAspect) {\n // Image is wider - fit to width\n drawHeight = drawWidth / imgAspect;\n } else {\n // Image is taller - fit to height\n drawWidth = drawHeight * imgAspect;\n }\n\n // Center the image in the area\n const drawX = nodeCssX + (PADDING * scale) + (imageAreaWidth - drawWidth) / 2;\n const drawY = imageAreaY + (imageAreaHeight - drawHeight) / 2;\n\n // Convert to offscreen canvas coordinates\n const canvasX = (drawX - sourceCssX) * actualDpr * captureScale;\n const canvasY = (drawY - sourceCssY) * actualDpr * captureScale;\n const canvasWidth = drawWidth * actualDpr * captureScale;\n const canvasHeight = drawHeight * actualDpr * captureScale;\n\n // Check if at least partially visible\n if (canvasX + canvasWidth > 0 && canvasX < renderSize &&\n canvasY + canvasHeight > 0 && canvasY < renderSize) {\n\n ctx.save();\n ctx.imageSmoothingEnabled = true;\n ctx.imageSmoothingQuality = 'high';\n ctx.drawImage(img, canvasX, canvasY, canvasWidth, canvasHeight);\n ctx.restore();\n }\n } catch (e) {\n // Silently fail for individual images\n }\n }\n\n /**\n * Draw VHS-style DOM widget video/image preview onto the offscreen canvas.\n */\n private drawDomWidgetPreview(\n ctx: CanvasRenderingContext2D,\n widget: any,\n node: any,\n nodeCssX: number,\n nodeCssY: number,\n nodeCssWidth: number,\n sourceCssX: number,\n sourceCssY: number,\n actualDpr: number,\n captureScale: number,\n renderSize: number,\n scale: number\n ): void {\n // VHS widgets have videoEl and imgEl properties\n const videoEl = widget.videoEl as HTMLVideoElement | undefined;\n const imgEl = widget.imgEl as HTMLImageElement | undefined;\n\n // Determine which element to draw\n let sourceElement: HTMLVideoElement | HTMLImageElement | undefined;\n let sourceWidth = 0;\n let sourceHeight = 0;\n\n if (videoEl && !videoEl.hidden && videoEl.videoWidth > 0) {\n sourceElement = videoEl;\n sourceWidth = videoEl.videoWidth;\n sourceHeight = videoEl.videoHeight;\n } else if (imgEl && !imgEl.hidden && imgEl.naturalWidth > 0) {\n sourceElement = imgEl;\n sourceWidth = imgEl.naturalWidth;\n sourceHeight = imgEl.naturalHeight;\n }\n\n if (!sourceElement || sourceWidth === 0 || sourceHeight === 0) return;\n\n try {\n // Widget positioning - VHS uses computedHeight and last_y\n const widgetY = (widget as any).last_y ?? 30;\n const widgetHeight = widget.computedHeight ?? 100;\n\n const widgetCssY = nodeCssY + (widgetY * scale);\n const widgetCssWidth = nodeCssWidth - 20 * scale; // VHS padding\n const widgetCssHeight = widgetHeight * scale;\n\n if (widgetCssWidth <= 0 || widgetCssHeight <= 0) return;\n\n // Calculate aspect-ratio-preserving dimensions\n const srcAspect = sourceWidth / sourceHeight;\n const areaAspect = widgetCssWidth / widgetCssHeight;\n\n let drawWidth = widgetCssWidth;\n let drawHeight = widgetCssHeight;\n\n if (srcAspect > areaAspect) {\n drawHeight = drawWidth / srcAspect;\n } else {\n drawWidth = drawHeight * srcAspect;\n }\n\n // Center in widget area\n const drawX = nodeCssX + 10 * scale + (widgetCssWidth - drawWidth) / 2;\n const drawY = widgetCssY + (widgetCssHeight - drawHeight) / 2;\n\n // Convert to offscreen canvas coordinates\n const canvasX = (drawX - sourceCssX) * actualDpr * captureScale;\n const canvasY = (drawY - sourceCssY) * actualDpr * captureScale;\n const canvasWidth = drawWidth * actualDpr * captureScale;\n const canvasHeight = drawHeight * actualDpr * captureScale;\n\n // Check if at least partially visible\n if (canvasX + canvasWidth > 0 && canvasX < renderSize &&\n canvasY + canvasHeight > 0 && canvasY < renderSize) {\n\n ctx.save();\n ctx.imageSmoothingEnabled = true;\n ctx.imageSmoothingQuality = 'high';\n ctx.drawImage(sourceElement, canvasX, canvasY, canvasWidth, canvasHeight);\n ctx.restore();\n }\n } catch (e) {\n // Silently fail for individual previews\n }\n }\n\n /**\n * Render markdown text onto the context.\n * Supports basic headers (#) and lists (-).\n */\n private drawMarkdown(\n ctx: CanvasRenderingContext2D,\n text: string,\n x: number,\n y: number,\n maxWidth: number,\n maxHeight: number,\n fontSize: number\n ): void {\n const lines = text.split('\\n');\n const lineHeight = fontSize * 1.4;\n let currentY = y;\n const maxY = y + maxHeight;\n\n ctx.textBaseline = 'top';\n ctx.fillStyle = '#e0e0e0';\n\n // Word wrap helper\n const wrapText = (text: string, maxWidth: number, font: string): string[] => {\n ctx.font = font;\n const words = text.split(/(\\s+)/);\n const wrappedLines: string[] = [];\n let currentLine = '';\n\n for (const word of words) {\n const testLine = currentLine + word;\n const testWidth = ctx.measureText(testLine).width;\n if (testWidth > maxWidth && currentLine.length > 0) {\n wrappedLines.push(currentLine.trimEnd());\n currentLine = word.trimStart();\n } else {\n currentLine = testLine;\n }\n }\n if (currentLine.length > 0) wrappedLines.push(currentLine.trimEnd());\n return wrappedLines;\n };\n\n for (const line of lines) {\n if (currentY >= maxY) break;\n\n let renderText = line;\n let currentFont = `500 ${fontSize}px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif`;\n let indent = 0;\n let color = '#e0e0e0';\n\n // 1. Headers\n if (line.startsWith('#')) {\n const level = line.match(/^#+/)?.[0].length || 0;\n renderText = line.substring(level).trim();\n const headerScale = Math.max(1.1, 1.8 - (level * 0.15)); // H1=1.8x, H2=1.65x\n const headerSize = fontSize * headerScale;\n currentFont = `700 ${headerSize}px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif`;\n // Add a little margin before headers\n currentY += fontSize * 0.5;\n }\n // 2. Lists\n else if (line.trim().startsWith('- ') || line.trim().startsWith('* ')) {\n renderText = '• ' + line.trim().substring(2);\n indent = fontSize; // Indent list items\n }\n\n const wrapped = wrapText(renderText, maxWidth - indent, currentFont);\n\n ctx.font = currentFont;\n ctx.fillStyle = color;\n\n for (const wrap of wrapped) {\n if (currentY >= maxY) break;\n ctx.fillText(wrap, x + indent, currentY);\n // Headers get taller line height\n const currentLineHeight = line.startsWith('#') ? (parseFloat(currentFont.split(' ')[1]) * 1.4) : lineHeight;\n currentY += currentLineHeight;\n }\n }\n }\n\n\n\n /**\n * Render a button widget.\n * Draws a rounded rectangle with the label centered.\n */\n private drawButton(\n ctx: CanvasRenderingContext2D,\n label: string,\n x: number,\n y: number,\n width: number,\n height: number,\n fontSize: number\n ): void {\n const radius = 6;\n\n ctx.save();\n\n // Button background\n ctx.beginPath();\n ctx.roundRect(x, y, width, height, radius);\n ctx.fillStyle = '#222'; // Dark button background\n ctx.fill();\n\n // Button border\n ctx.strokeStyle = '#555';\n ctx.lineWidth = 1;\n ctx.stroke();\n\n // Button label\n ctx.font = `600 ${fontSize}px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillStyle = '#ccc'; // Light grey text\n\n // Center text in button\n const fileUploadLabel = \"choose file to upload\";\n // If it's the long upload label, we might need smaller font or wrapping, \n // but typically buttons are single line.\n // We'll just truncate if too long or let it clip via context clipping if we added any (we didn't yet).\n\n ctx.fillText(label, x + width / 2, y + height / 2);\n\n ctx.restore();\n }\n\n /**\n * Draw a mini cursor preview at the center of the glass.\n * The cursor is drawn as a classic arrow pointer with a contrasting outline.\n * @param renderSize - The render size of the glass in pixels\n */\n private drawCursorPreview(renderSize: number): void {\n if (!this.offscreenCtx) return;\n const ctx = this.offscreenCtx;\n\n // Cursor size scales with glass size (roughly 8-12% of glass size)\n const cursorSize = Math.max(16, Math.min(32, renderSize * 0.1));\n\n // Center position of the glass\n const centerX = renderSize / 2;\n const centerY = renderSize / 2;\n\n ctx.save();\n\n // Translate to center (cursor tip will be at center)\n ctx.translate(centerX, centerY);\n\n // Scale the cursor shape\n const scale = cursorSize / 24; // Base cursor is 24px\n ctx.scale(scale, scale);\n\n // Classic arrow cursor shape (pointing up-left)\n ctx.beginPath();\n ctx.moveTo(0, 0); // Tip\n ctx.lineTo(0, 17); // Down\n ctx.lineTo(4, 13); // Notch right\n ctx.lineTo(7, 20); // Handle bottom-right\n ctx.lineTo(10, 19); // Handle outer\n ctx.lineTo(7, 12); // Handle top\n ctx.lineTo(12, 12); // Arrow right point\n ctx.closePath();\n\n // Draw white fill\n ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';\n ctx.fill();\n\n // Draw black outline for contrast\n ctx.strokeStyle = 'rgba(0, 0, 0, 0.9)';\n ctx.lineWidth = 1.5;\n ctx.stroke();\n\n ctx.restore();\n }\n\n isAvailable(): boolean {\n return this.offscreenCanvas !== null && this.offscreenCtx !== null;\n }\n\n getCanvas(): HTMLCanvasElement | null {\n return this.offscreenCanvas;\n }\n}\n"],"names":["text","maxWidth"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"OffscreenRenderer.js","sources":["../../src/magnify-glass/OffscreenRenderer.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Offscreen Renderer\n * \n * Hybrid Renderer:\n * 1. Direct Capture (Nuclear) for near-100% zoom (Perfect Alignment, Good Quality).\n * 2. Virtual Zoom (High Res) for zoomed-out views (High Detail, sensitive alignment).\n */\n\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\n\ndeclare const app: any;\n\nexport class OffscreenRenderer {\n private config: ConfigManager;\n private state: MagnifierState;\n private offscreenCanvas: HTMLCanvasElement | null = null;\n private offscreenCtx: CanvasRenderingContext2D | null = null;\n private lastRenderWidth: number = 0;\n private lastRenderHeight: number = 0;\n private isCapturing: boolean = false;\n\n // Virtual Zoom Throttling (60 FPS = ~16ms per frame)\n private static readonly VIRTUAL_ZOOM_MIN_INTERVAL_MS = 16;\n private lastVirtualZoomTime: number = 0;\n private cachedVirtualZoomResult: HTMLCanvasElement | null = null;\n\n constructor(config: ConfigManager, state: MagnifierState) {\n this.config = config;\n this.state = state;\n this.initOffscreenCanvas();\n }\n\n private initOffscreenCanvas(): void {\n this.offscreenCanvas = document.createElement('canvas');\n this.offscreenCtx = this.offscreenCanvas.getContext('2d');\n }\n\n renderHighResRegion(targetCanvas: HTMLCanvasElement): HTMLCanvasElement | null {\n if (!this.offscreenCanvas || !this.offscreenCtx) return null;\n if (this.isCapturing) return null;\n\n const lgCanvas = app?.canvas;\n if (!lgCanvas || !lgCanvas.ds) return null;\n\n const renderSize = this.config.glassSize;\n\n // Resize offscreen canvas\n if (this.lastRenderWidth !== renderSize || this.lastRenderHeight !== renderSize) {\n this.offscreenCanvas.width = renderSize;\n this.offscreenCanvas.height = renderSize;\n this.lastRenderWidth = renderSize;\n this.lastRenderHeight = renderSize;\n }\n\n const currentScale = lgCanvas.ds.scale;\n const rect = targetCanvas.getBoundingClientRect();\n // Calculate DPR exactly as EventHandler does (Canvas Pixels / CSS Pixels)\n const dpr = rect.width > 0 ? targetCanvas.width / rect.width : 1;\n\n // Threshold for Hybrid Mode\n // If we are zoomed in enough (>= 100%), Direct Capture quality is perfect,\n // and it guarantees perfect alignment with zero extra draws.\n // If we are zoomed out (< 100%), we use High Res Virtual Zoom for crisp details.\n // Note: Image preview nodes are handled by drawImagePreviewsNatively() during Virtual Zoom.\n // If forceDirectCapture is enabled (via hotkey), always use Direct Capture for accurate cursor alignment.\n const useDirectCapture = currentScale >= 1.0 || this.config.forceDirectCapture;\n\n if (useDirectCapture) {\n // Direct Capture: Full speed, no extra draws\n this.cachedVirtualZoomResult = null; // Invalidate cache when switching modes\n return this.renderDirectCapture(targetCanvas, renderSize, dpr);\n } else {\n // Virtual Zoom: Throttled to 60 FPS to prevent FPS counter inflation\n const now = performance.now();\n const elapsed = now - this.lastVirtualZoomTime;\n\n if (elapsed < OffscreenRenderer.VIRTUAL_ZOOM_MIN_INTERVAL_MS && this.cachedVirtualZoomResult) {\n // Return cached result if within throttle window\n return this.cachedVirtualZoomResult;\n }\n\n // Perform the expensive virtual zoom render\n const result = this.renderVirtualZoom(lgCanvas, targetCanvas, renderSize, currentScale, dpr);\n this.lastVirtualZoomTime = now;\n this.cachedVirtualZoomResult = result;\n return result;\n }\n }\n\n /**\n * Detect if there are nodes with image previews in the capture region.\n * These nodes (Save Image, Preview Image, etc.) have images that are drawn\n * via onDrawBackground/onDrawForeground and cause artifacts during Virtual Zoom.\n */\n private detectImagePreviewNodes(dpr: number, scale: number, offset: number[]): boolean {\n const graph = app?.graph;\n if (!graph || !graph._nodes) return false;\n\n const renderSize = this.config.glassSize;\n const sourceSizeCss = renderSize / this.config.zoomFactor;\n\n // Mouse position in CSS pixels\n const pivotCssX = this.state.x / dpr;\n const pivotCssY = this.state.y / dpr;\n\n // Capture region in CSS pixels\n const captureLeft = pivotCssX - (sourceSizeCss / 2);\n const captureTop = pivotCssY - (sourceSizeCss / 2);\n const captureRight = captureLeft + sourceSizeCss;\n const captureBottom = captureTop + sourceSizeCss;\n\n // Node types that have image previews\n const imagePreviewTypes = [\n 'SaveImage', 'PreviewImage', 'LoadImage', 'LoadImageMask',\n 'VHS_LoadVideo', 'VHS_VideoCombine', // Video nodes\n 'ImagePreview', 'ShowImage' // Common variants\n ];\n\n for (const node of graph._nodes) {\n if (!node.pos || !node.size) continue;\n if (node.flags?.collapsed) continue;\n\n // Check if this node type has image previews\n const nodeType = node.type || node.comfyClass || '';\n const hasImagePreview = imagePreviewTypes.some(t =>\n nodeType.toLowerCase().includes(t.toLowerCase())\n ) || node.imgs?.length > 0; // Also check for imgs array directly\n\n if (!hasImagePreview) continue;\n\n // Calculate node position in CSS pixels\n const nodeCssX = node.pos[0] * scale + offset[0];\n const nodeCssY = node.pos[1] * scale + offset[1];\n const nodeCssWidth = node.size[0] * scale;\n const nodeCssHeight = node.size[1] * scale;\n\n // Check if node overlaps with capture region\n const nodeRight = nodeCssX + nodeCssWidth;\n const nodeBottom = nodeCssY + nodeCssHeight;\n\n const overlaps = !(nodeRight < captureLeft || nodeCssX > captureRight ||\n nodeBottom < captureTop || nodeCssY > captureBottom);\n\n if (overlaps) {\n return true; // Found an image preview node in capture region\n }\n }\n\n return false;\n }\n\n /**\n * Mode 1: Direct Capture (Nuclear Fix)\n * Copies pixels directly from screen. Zero drift.\n */\n private renderDirectCapture(targetCanvas: HTMLCanvasElement, renderSize: number, dpr: number): HTMLCanvasElement | null {\n try {\n this.isCapturing = true;\n\n // state.x/y are Backing Pixels. pivotCss is Backing / DPR.\n const pivotCssX = this.state.x / dpr;\n const pivotCssY = this.state.y / dpr;\n\n const sourceSizeCss = renderSize / this.config.zoomFactor;\n\n // sourceRect in Backing Pixels\n const sourceWidth = sourceSizeCss * dpr;\n const sourceHeight = sourceSizeCss * dpr;\n const sourceX = (pivotCssX - (sourceSizeCss / 2)) * dpr;\n const sourceY = (pivotCssY - (sourceSizeCss / 2)) * dpr;\n\n this.offscreenCtx!.clearRect(0, 0, renderSize, renderSize);\n this.offscreenCtx!.drawImage(\n targetCanvas,\n sourceX, sourceY, sourceWidth, sourceHeight,\n 0, 0, renderSize, renderSize\n );\n\n // Draw native widget text on top of the captured canvas\n const lgCanvas = app?.canvas;\n const currentScale = lgCanvas?.ds?.scale ?? 1.0;\n const currentOffset: [number, number] = lgCanvas?.ds?.offset ? [lgCanvas.ds.offset[0], lgCanvas.ds.offset[1]] : [0, 0];\n this.drawWidgetTextNatively(sourceX, sourceY, sourceWidth, sourceHeight, renderSize, currentScale, currentOffset);\n\n // NOTE: We do NOT call drawImagePreviewsNatively() here.\n // Direct Capture copies pixels from the screen which already has correctly-drawn images.\n // Native image drawing is only needed during Virtual Zoom where ComfyUI's rendering fails.\n\n // Draw cursor preview overlay if enabled\n if (this.config.showCursorPreview) {\n this.drawCursorPreview(renderSize);\n }\n\n this.isCapturing = false;\n return this.offscreenCanvas;\n } catch (e) {\n console.warn(\"DirectCapture failed\", e);\n this.isCapturing = false;\n return null;\n }\n }\n\n /**\n * Mode 2: Virtual Zoom (High Res)\n * Temporarily sets scale to 1.0 to render details.\n * Uses LiteGraph's setZoom() API for correct pivot handling.\n */\n private renderVirtualZoom(lgCanvas: any, targetCanvas: HTMLCanvasElement, renderSize: number, currentScale: number, dpr: number): HTMLCanvasElement | null {\n try {\n this.isCapturing = true;\n // Set global flag so node renderers can check and skip expensive operations\n (window as any).__magnifyGlassCapturing = true;\n\n // Pivot = Mouse in CSS Pixels (relative to canvas element)\n const pivotCssX = this.state.x / dpr;\n const pivotCssY = this.state.y / dpr;\n\n // Target Scale (1.0 for high-res capture)\n const targetScale = Math.max(1.0, currentScale);\n\n // Store original state for restoration\n const origScale = lgCanvas.ds.scale;\n const origOffsetX = lgCanvas.ds.offset[0];\n const origOffsetY = lgCanvas.ds.offset[1];\n\n // Use LiteGraph's setZoom API - this properly handles the pivot point\n // The center parameter tells LiteGraph to zoom around that screen position\n if (typeof lgCanvas.setZoom === 'function') {\n lgCanvas.setZoom(targetScale, [pivotCssX, pivotCssY]);\n } else {\n // Fallback: manual calculation (legacy, may drift)\n const pivotGraphX = (pivotCssX - origOffsetX) / currentScale;\n const pivotGraphY = (pivotCssY - origOffsetY) / currentScale;\n const newOffsetX = pivotCssX - (pivotGraphX * targetScale);\n const newOffsetY = pivotCssY - (pivotGraphY * targetScale);\n lgCanvas.ds.scale = targetScale;\n lgCanvas.ds.offset[0] = newOffsetX;\n lgCanvas.ds.offset[1] = newOffsetY;\n }\n\n // Hide images AND preview widgets from nodes to prevent them from being drawn on the captured canvas\n // This prevents \"ghosting\" artifacts and crashes (since some custom widgets fail in Virtual Zoom)\n const hiddenNodeImages = new Map();\n const hiddenNodeWidgets = new Map();\n\n if (app?.graph?._nodes) {\n for (const node of app.graph._nodes) {\n // 1. Hide node.imgs\n if (node.imgs) {\n hiddenNodeImages.set(node, node.imgs);\n node.imgs = null;\n }\n\n // 2. Hide preview widgets\n if (node.widgets && Array.isArray(node.widgets)) {\n const hasPreviewWidget = node.widgets.some((w: any) => {\n const wName = String(w.name || '').toLowerCase();\n const wType = String(w.type || '').toLowerCase();\n return wName.includes('preview') || wName.includes('image') || wName.includes('gallery') || wName.includes('upload') ||\n wType.includes('preview') || wType.includes('image');\n });\n\n if (hasPreviewWidget) {\n // Backup original widgets array\n hiddenNodeWidgets.set(node, node.widgets);\n // Filter out the preview widgets for this draw call\n node.widgets = node.widgets.filter((w: any) => {\n const wName = String(w.name || '').toLowerCase();\n const wType = String(w.type || '').toLowerCase();\n // Only hide explicit preview widgets. Inputs (image, upload) should remain visible.\n const isPreview = wName.includes('preview') || wName.includes('gallery') ||\n wType.includes('preview');\n return !isPreview;\n });\n }\n }\n }\n }\n\n try {\n lgCanvas.draw(true, true);\n } finally {\n // Restore images\n for (const [node, imgs] of hiddenNodeImages.entries()) {\n node.imgs = imgs;\n }\n // Restore widgets\n for (const [node, widgets] of hiddenNodeWidgets.entries()) {\n node.widgets = widgets;\n }\n }\n\n // Capture pixels around the pivot point\n const sourceSizeCss = renderSize / this.config.zoomFactor;\n const sourceWidth = sourceSizeCss * dpr;\n const sourceHeight = sourceSizeCss * dpr;\n const sourceX = (pivotCssX - (sourceSizeCss / 2)) * dpr;\n const sourceY = (pivotCssY - (sourceSizeCss / 2)) * dpr;\n\n this.offscreenCtx!.clearRect(0, 0, renderSize, renderSize);\n this.offscreenCtx!.drawImage(\n targetCanvas,\n sourceX, sourceY, sourceWidth, sourceHeight,\n 0, 0, renderSize, renderSize\n );\n\n // Draw native widget text on top of the captured canvas\n // Note: For virtual zoom, we use targetScale and the CURRENT offset (after setZoom)\n // because lgCanvas.setZoom() modified the offset to zoom around the pivot\n const captureOffset: [number, number] = [lgCanvas.ds.offset[0], lgCanvas.ds.offset[1]];\n this.drawWidgetTextNatively(sourceX, sourceY, sourceWidth, sourceHeight, renderSize, targetScale, captureOffset);\n\n // Draw image/video previews natively on top of the captured canvas\n this.drawImagePreviewsNatively(sourceX, sourceY, sourceWidth, sourceHeight, renderSize, targetScale, captureOffset);\n\n // Draw cursor preview overlay if enabled\n if (this.config.showCursorPreview) {\n this.drawCursorPreview(renderSize);\n }\n\n // Restore original zoom state by directly setting the saved values.\n // IMPORTANT: Do NOT use setZoom() here! setZoom() recalculates offset from a pivot point,\n // which introduces floating-point drift (1-2px per call) causing vertical/horizontal drift.\n // We must restore the EXACT original offset values to prevent cumulative drift.\n lgCanvas.ds.scale = origScale;\n lgCanvas.ds.offset[0] = origOffsetX;\n lgCanvas.ds.offset[1] = origOffsetY;\n lgCanvas.draw(true, true);\n\n this.isCapturing = false;\n (window as any).__magnifyGlassCapturing = false;\n return this.offscreenCanvas;\n\n } catch (e) {\n console.warn(\"VirtualZoom failed\", e);\n // Emergency restore\n lgCanvas.ds.scale = currentScale;\n lgCanvas.draw(true, true);\n this.isCapturing = false;\n (window as any).__magnifyGlassCapturing = false;\n return null;\n }\n }\n\n /**\n * Draw widget text natively on the offscreen canvas.\n * This renders text content that would otherwise be lost since widgets are DOM elements.\n * \n * @param sourceX - Source X position in backing pixels\n * @param sourceY - Source Y position in backing pixels\n * @param sourceWidth - Source width in backing pixels\n * @param sourceHeight - Source height in backing pixels\n * @param renderSize - Output render size in pixels\n * @param scale - Canvas scale used during capture\n * @param offset - Canvas offset [x, y] used during capture\n */\n private drawWidgetTextNatively(\n sourceX: number,\n sourceY: number,\n sourceWidth: number,\n sourceHeight: number,\n renderSize: number,\n scale: number,\n offset: [number, number]\n ): void {\n const graph = app?.graph;\n if (!graph || !graph._nodes || !this.offscreenCtx) return;\n\n const ctx = this.offscreenCtx;\n const lgCanvas = app?.canvas;\n if (!lgCanvas || !lgCanvas.ds) return;\n\n // LiteGraph layout constants\n const TITLE_HEIGHT = 30;\n const WIDGET_HEIGHT = 20;\n const WIDGET_MARGIN = 4;\n const PADDING = 15;\n\n // Use the passed offset (capture-time offset), not current lgCanvas offset\n // const ds = lgCanvas.ds;\n // const offset = ds.offset || [0, 0]; // OLD: This was wrong\n\n // The offset from LiteGraph is in CSS pixels\n // The sourceX/Y/Width/Height are in backing pixels\n // We need to convert properly\n\n // Calculate DPR from the source parameters\n const sourceSizeCss = renderSize / this.config.zoomFactor;\n const actualDpr = sourceWidth / sourceSizeCss;\n const captureScale = renderSize / sourceWidth;\n\n // Source region in CSS pixels\n const sourceCssX = sourceX / actualDpr;\n const sourceCssY = sourceY / actualDpr;\n const sourceCssWidth = sourceWidth / actualDpr;\n const sourceCssHeight = sourceHeight / actualDpr;\n\n for (const node of graph._nodes) {\n if (!node.widgets || !node.pos || !node.size) continue;\n if (node.flags?.collapsed) continue;\n\n // Calculate node position in CSS pixels\n // node.pos is in graph coordinates, transform: screenCss = graphPos * scale + offset\n const nodeCssX = node.pos[0] * scale + offset[0];\n const nodeCssY = node.pos[1] * scale + offset[1];\n const nodeCssWidth = node.size[0] * scale;\n const nodeCssHeight = node.size[1] * scale;\n\n // Check if node is in captured region (CSS coordinates)\n if (nodeCssX + nodeCssWidth < sourceCssX || nodeCssX > sourceCssX + sourceCssWidth) continue;\n if (nodeCssY + nodeCssHeight < sourceCssY || nodeCssY > sourceCssY + sourceCssHeight) continue;\n\n // Calculate widget positions\n let widgetY = TITLE_HEIGHT;\n\n for (const widget of node.widgets) {\n // Use widget.last_y if available, otherwise use calculated position\n const widgetLocalY = (widget as any).last_y ?? widgetY;\n\n // Check if widget type contains text AND is a multi-line text area\n // We only want to draw our custom rendering for multi-line text widgets (like CLIP prompts)\n // NOT for simple single-line string inputs (like filename_prefix) which are already canvas-rendered\n const widgetType = String(widget.type || '').toLowerCase();\n\n // Check if this is a multi-line text widget that needs our native rendering\n // Multi-line text widgets typically have:\n // - type 'customtext' (CLIP text encode uses this)\n // - computedHeight >= 40 (multi-line areas are taller)\n // Single-line string inputs typically have:\n // - type 'string' with no computedHeight or small computedHeight\n // - These are already rendered on the canvas by ComfyUI\n const computedHeight = (widget as any).computedHeight || 0;\n const isMarkdownWidget = widgetType === 'markdown'; // ComfyUI standard type\n\n const isMultiLineWidget = isMarkdownWidget || widgetType === 'customtext' ||\n ((widgetType === 'text' || widgetType === 'textarea') && computedHeight >= 40);\n\n // Common calculations for positioning\n const widgetCssX = nodeCssX + (PADDING * scale);\n const widgetCssY = nodeCssY + (widgetLocalY * scale);\n const widgetCssWidth = nodeCssWidth - (PADDING * 2 * scale);\n\n // Convert from CSS to offscreen canvas coordinates\n const canvasX = (widgetCssX - sourceCssX) * actualDpr * captureScale;\n const canvasY = (widgetCssY - sourceCssY) * actualDpr * captureScale;\n const widgetWidth = widgetCssWidth * actualDpr * captureScale;\n\n // Calculate font size\n const baseFontSize = 13;\n const fontSize = Math.max(10, Math.min(28, baseFontSize * scale * actualDpr * captureScale));\n const lineHeight = fontSize * 1.4;\n\n // ----------------------------------------------------------------\n // RENDER TEXT / MARKDOWN\n // ----------------------------------------------------------------\n if (isMultiLineWidget && widget.value !== undefined && widget.value !== null) {\n const textValue = String(widget.value);\n if (textValue.length > 0) {\n // Calculate required height based on content\n let contentHeight = 0;\n if (isMarkdownWidget) {\n // Rough estimation of height needed\n ctx.save();\n ctx.font = `500 ${fontSize}px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif`;\n const maxWidth = widgetWidth - 12;\n\n const lines = textValue.split('\\n');\n for (const line of lines) {\n const words = line.split(/(\\s+)/);\n let currentLine = '';\n let lineCount = 1;\n for (const word of words) {\n if (ctx.measureText(currentLine + word).width > maxWidth) {\n lineCount++;\n currentLine = word;\n } else {\n currentLine += word;\n }\n }\n contentHeight += lineCount * lineHeight;\n }\n ctx.restore();\n\n // Add padding\n contentHeight += 16;\n }\n\n // Calculate available height in canvas pixels to prevent spilling\n // Node bottom in CSS pixels\n const nodeBottomCss = nodeCssY + nodeCssHeight;\n // Max height for this widget in CSS pixels (stay inside node, minus padding)\n const maxWidgetHeightCss = Math.max(0, nodeBottomCss - widgetCssY - (PADDING * scale));\n // Convert to canvas pixels\n const maxWidgetHeight = maxWidgetHeightCss * actualDpr * captureScale;\n\n // Calculate widget height - Render full content for markdown up to node bounds\n const widgetHeight = (isMarkdownWidget)\n ? Math.min(Math.max((widget as any).computedHeight * scale * captureScale || 0, contentHeight), maxWidgetHeight)\n : ((widget as any).computedHeight\n ? (widget as any).computedHeight * scale * captureScale\n : Math.max(80, fontSize * 5));\n\n // Check if widget is at least partially visible (more lenient bounds check)\n const widgetRight = canvasX + widgetWidth;\n const widgetBottom = canvasY + widgetHeight;\n const isVisible = widgetRight > 0 && canvasX < renderSize &&\n widgetBottom > 0 && canvasY < renderSize;\n\n if (isVisible) {\n ctx.save();\n\n // Container bounds\n const containerX = canvasX - 4;\n const containerY = canvasY - 4;\n const containerWidth = widgetWidth + 8;\n const containerHeight = widgetHeight + 8;\n const borderRadius = 8;\n\n // Draw background container with rounded corners\n ctx.beginPath();\n ctx.roundRect(containerX, containerY, containerWidth, containerHeight, borderRadius);\n ctx.fillStyle = '#1e1e1e'; // Slightly lighter dark background\n ctx.fill();\n\n // Draw border\n ctx.strokeStyle = '#3a3a3a';\n ctx.lineWidth = 1;\n ctx.stroke();\n\n // Clip to container bounds to prevent text spill\n ctx.beginPath();\n ctx.roundRect(containerX, containerY, containerWidth, containerHeight, borderRadius);\n ctx.clip();\n\n // Use advanced markdown rendering instead of simple text\n this.drawMarkdown(ctx, textValue, canvasX + 4, canvasY + 4, widgetWidth - 12, widgetHeight - 8, fontSize);\n\n ctx.restore();\n }\n }\n }\n\n widgetY += WIDGET_HEIGHT + WIDGET_MARGIN;\n }\n }\n }\n\n /**\n * Draw image and video previews natively on the offscreen canvas.\n * This renders preview content that would otherwise cause errors during Virtual Zoom,\n * because ComfyUI's ImagePreviewWidget fails when the canvas scale is manipulated.\n * \n * Handles:\n * - node.imgs[] array (standard ComfyUI SaveImage/PreviewImage nodes)\n * - VHS-style DOM widget previews (video/image elements)\n * \n * @param sourceX - Source X position in backing pixels\n * @param sourceY - Source Y position in backing pixels\n * @param sourceWidth - Source width in backing pixels\n * @param sourceHeight - Source height in backing pixels\n * @param renderSize - Output render size in pixels\n * @param scale - Canvas scale used during capture\n * @param offset - Canvas offset [x, y] used during capture\n */\n private drawImagePreviewsNatively(\n sourceX: number,\n sourceY: number,\n sourceWidth: number,\n sourceHeight: number,\n renderSize: number,\n scale: number,\n offset: [number, number]\n ): void {\n const graph = app?.graph;\n if (!graph || !graph._nodes || !this.offscreenCtx) return;\n\n const ctx = this.offscreenCtx;\n\n // Calculate coordinate transforms (same as drawWidgetTextNatively)\n const sourceSizeCss = renderSize / this.config.zoomFactor;\n const actualDpr = sourceWidth / sourceSizeCss;\n const captureScale = renderSize / sourceWidth;\n\n // Source region in CSS pixels\n const sourceCssX = sourceX / actualDpr;\n const sourceCssY = sourceY / actualDpr;\n const sourceCssWidth = sourceWidth / actualDpr;\n const sourceCssHeight = sourceHeight / actualDpr;\n\n // LiteGraph constants\n const TITLE_HEIGHT = 30;\n const WIDGET_MARGIN = 4;\n\n for (const node of graph._nodes) {\n if (!node.pos || !node.size) continue;\n if (node.flags?.collapsed) continue;\n\n // Calculate node position in CSS pixels\n const nodeCssX = node.pos[0] * scale + offset[0];\n const nodeCssY = node.pos[1] * scale + offset[1];\n const nodeCssWidth = node.size[0] * scale;\n const nodeCssHeight = node.size[1] * scale;\n\n // Check if node is in captured region\n if (nodeCssX + nodeCssWidth < sourceCssX || nodeCssX > sourceCssX + sourceCssWidth) continue;\n if (nodeCssY + nodeCssHeight < sourceCssY || nodeCssY > sourceCssY + sourceCssHeight) continue;\n\n // Method 1: Handle node.imgs[] array (standard ComfyUI preview)\n if (node.imgs && Array.isArray(node.imgs) && node.imgs.length > 0) {\n this.drawNodeImages(\n ctx, node, nodeCssX, nodeCssY, nodeCssWidth, nodeCssHeight,\n sourceCssX, sourceCssY, actualDpr, captureScale, renderSize, scale\n );\n }\n\n // Method 2: Handle VHS-style DOM widget previews\n if (node.widgets) {\n for (const widget of node.widgets) {\n const widgetName = String(widget.name || '').toLowerCase();\n if (widgetName === 'videopreview' || widgetName === 'audiopreview') {\n this.drawDomWidgetPreview(\n ctx, widget, node, nodeCssX, nodeCssY, nodeCssWidth,\n sourceCssX, sourceCssY, actualDpr, captureScale, renderSize, scale\n );\n }\n }\n }\n }\n }\n\n /**\n * Draw images from node.imgs[] array onto the offscreen canvas.\n */\n private drawNodeImages(\n ctx: CanvasRenderingContext2D,\n node: any,\n nodeCssX: number,\n nodeCssY: number,\n nodeCssWidth: number,\n nodeCssHeight: number,\n sourceCssX: number,\n sourceCssY: number,\n actualDpr: number,\n captureScale: number,\n renderSize: number,\n scale: number\n ): void {\n const TITLE_HEIGHT = 30;\n const PADDING = 10;\n\n // Calculate image area (below title, with padding)\n const imageAreaY = nodeCssY + (TITLE_HEIGHT * scale);\n const imageAreaWidth = nodeCssWidth - (PADDING * 2 * scale);\n const imageAreaHeight = nodeCssHeight - (TITLE_HEIGHT * scale) - (PADDING * scale);\n\n if (imageAreaWidth <= 0 || imageAreaHeight <= 0) return;\n\n // For simplicity, draw the first image that fits\n // ComfyUI typically shows images in a grid, but for the magnifier we'll show the first/selected one\n const imgs = node.imgs;\n const imageIndex = node.imageIndex ?? 0;\n const img = imgs[Math.min(imageIndex, imgs.length - 1)];\n\n if (!img || !(img instanceof HTMLImageElement) || !img.complete || img.naturalWidth === 0) {\n return;\n }\n\n try {\n // Calculate aspect-ratio-preserving dimensions\n const imgAspect = img.naturalWidth / img.naturalHeight;\n const areaAspect = imageAreaWidth / imageAreaHeight;\n\n let drawWidth = imageAreaWidth;\n let drawHeight = imageAreaHeight;\n\n if (imgAspect > areaAspect) {\n // Image is wider - fit to width\n drawHeight = drawWidth / imgAspect;\n } else {\n // Image is taller - fit to height\n drawWidth = drawHeight * imgAspect;\n }\n\n // Center the image in the area\n const drawX = nodeCssX + (PADDING * scale) + (imageAreaWidth - drawWidth) / 2;\n const drawY = imageAreaY + (imageAreaHeight - drawHeight) / 2;\n\n // Convert to offscreen canvas coordinates\n const canvasX = (drawX - sourceCssX) * actualDpr * captureScale;\n const canvasY = (drawY - sourceCssY) * actualDpr * captureScale;\n const canvasWidth = drawWidth * actualDpr * captureScale;\n const canvasHeight = drawHeight * actualDpr * captureScale;\n\n // Check if at least partially visible\n if (canvasX + canvasWidth > 0 && canvasX < renderSize &&\n canvasY + canvasHeight > 0 && canvasY < renderSize) {\n\n ctx.save();\n ctx.imageSmoothingEnabled = true;\n ctx.imageSmoothingQuality = 'high';\n ctx.drawImage(img, canvasX, canvasY, canvasWidth, canvasHeight);\n ctx.restore();\n }\n } catch (e) {\n // Silently fail for individual images\n }\n }\n\n /**\n * Draw VHS-style DOM widget video/image preview onto the offscreen canvas.\n */\n private drawDomWidgetPreview(\n ctx: CanvasRenderingContext2D,\n widget: any,\n node: any,\n nodeCssX: number,\n nodeCssY: number,\n nodeCssWidth: number,\n sourceCssX: number,\n sourceCssY: number,\n actualDpr: number,\n captureScale: number,\n renderSize: number,\n scale: number\n ): void {\n // VHS widgets have videoEl and imgEl properties\n const videoEl = widget.videoEl as HTMLVideoElement | undefined;\n const imgEl = widget.imgEl as HTMLImageElement | undefined;\n\n // Determine which element to draw\n let sourceElement: HTMLVideoElement | HTMLImageElement | undefined;\n let sourceWidth = 0;\n let sourceHeight = 0;\n\n if (videoEl && !videoEl.hidden && videoEl.videoWidth > 0) {\n sourceElement = videoEl;\n sourceWidth = videoEl.videoWidth;\n sourceHeight = videoEl.videoHeight;\n } else if (imgEl && !imgEl.hidden && imgEl.naturalWidth > 0) {\n sourceElement = imgEl;\n sourceWidth = imgEl.naturalWidth;\n sourceHeight = imgEl.naturalHeight;\n }\n\n if (!sourceElement || sourceWidth === 0 || sourceHeight === 0) return;\n\n try {\n // Widget positioning - VHS uses computedHeight and last_y\n const widgetY = (widget as any).last_y ?? 30;\n const widgetHeight = widget.computedHeight ?? 100;\n\n const widgetCssY = nodeCssY + (widgetY * scale);\n const widgetCssWidth = nodeCssWidth - 20 * scale; // VHS padding\n const widgetCssHeight = widgetHeight * scale;\n\n if (widgetCssWidth <= 0 || widgetCssHeight <= 0) return;\n\n // Calculate aspect-ratio-preserving dimensions\n const srcAspect = sourceWidth / sourceHeight;\n const areaAspect = widgetCssWidth / widgetCssHeight;\n\n let drawWidth = widgetCssWidth;\n let drawHeight = widgetCssHeight;\n\n if (srcAspect > areaAspect) {\n drawHeight = drawWidth / srcAspect;\n } else {\n drawWidth = drawHeight * srcAspect;\n }\n\n // Center in widget area\n const drawX = nodeCssX + 10 * scale + (widgetCssWidth - drawWidth) / 2;\n const drawY = widgetCssY + (widgetCssHeight - drawHeight) / 2;\n\n // Convert to offscreen canvas coordinates\n const canvasX = (drawX - sourceCssX) * actualDpr * captureScale;\n const canvasY = (drawY - sourceCssY) * actualDpr * captureScale;\n const canvasWidth = drawWidth * actualDpr * captureScale;\n const canvasHeight = drawHeight * actualDpr * captureScale;\n\n // Check if at least partially visible\n if (canvasX + canvasWidth > 0 && canvasX < renderSize &&\n canvasY + canvasHeight > 0 && canvasY < renderSize) {\n\n ctx.save();\n ctx.imageSmoothingEnabled = true;\n ctx.imageSmoothingQuality = 'high';\n ctx.drawImage(sourceElement, canvasX, canvasY, canvasWidth, canvasHeight);\n ctx.restore();\n }\n } catch (e) {\n // Silently fail for individual previews\n }\n }\n\n /**\n * Render markdown text onto the context.\n * Supports basic headers (#) and lists (-).\n */\n private drawMarkdown(\n ctx: CanvasRenderingContext2D,\n text: string,\n x: number,\n y: number,\n maxWidth: number,\n maxHeight: number,\n fontSize: number\n ): void {\n const lines = text.split('\\n');\n const lineHeight = fontSize * 1.4;\n let currentY = y;\n const maxY = y + maxHeight;\n\n ctx.textBaseline = 'top';\n ctx.fillStyle = '#e0e0e0';\n\n // Word wrap helper\n const wrapText = (text: string, maxWidth: number, font: string): string[] => {\n ctx.font = font;\n const words = text.split(/(\\s+)/);\n const wrappedLines: string[] = [];\n let currentLine = '';\n\n for (const word of words) {\n const testLine = currentLine + word;\n const testWidth = ctx.measureText(testLine).width;\n if (testWidth > maxWidth && currentLine.length > 0) {\n wrappedLines.push(currentLine.trimEnd());\n currentLine = word.trimStart();\n } else {\n currentLine = testLine;\n }\n }\n if (currentLine.length > 0) wrappedLines.push(currentLine.trimEnd());\n return wrappedLines;\n };\n\n for (const line of lines) {\n if (currentY >= maxY) break;\n\n let renderText = line;\n let currentFont = `500 ${fontSize}px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif`;\n let indent = 0;\n let color = '#e0e0e0';\n\n // 1. Headers\n if (line.startsWith('#')) {\n const level = line.match(/^#+/)?.[0].length || 0;\n renderText = line.substring(level).trim();\n const headerScale = Math.max(1.1, 1.8 - (level * 0.15)); // H1=1.8x, H2=1.65x\n const headerSize = fontSize * headerScale;\n currentFont = `700 ${headerSize}px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif`;\n // Add a little margin before headers\n currentY += fontSize * 0.5;\n }\n // 2. Lists\n else if (line.trim().startsWith('- ') || line.trim().startsWith('* ')) {\n renderText = '• ' + line.trim().substring(2);\n indent = fontSize; // Indent list items\n }\n\n const wrapped = wrapText(renderText, maxWidth - indent, currentFont);\n\n ctx.font = currentFont;\n ctx.fillStyle = color;\n\n for (const wrap of wrapped) {\n if (currentY >= maxY) break;\n ctx.fillText(wrap, x + indent, currentY);\n // Headers get taller line height\n const currentLineHeight = line.startsWith('#') ? (parseFloat(currentFont.split(' ')[1]) * 1.4) : lineHeight;\n currentY += currentLineHeight;\n }\n }\n }\n\n\n\n /**\n * Render a button widget.\n * Draws a rounded rectangle with the label centered.\n */\n private drawButton(\n ctx: CanvasRenderingContext2D,\n label: string,\n x: number,\n y: number,\n width: number,\n height: number,\n fontSize: number\n ): void {\n const radius = 6;\n\n ctx.save();\n\n // Button background\n ctx.beginPath();\n ctx.roundRect(x, y, width, height, radius);\n ctx.fillStyle = '#222'; // Dark button background\n ctx.fill();\n\n // Button border\n ctx.strokeStyle = '#555';\n ctx.lineWidth = 1;\n ctx.stroke();\n\n // Button label\n ctx.font = `600 ${fontSize}px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillStyle = '#ccc'; // Light grey text\n\n // Center text in button\n const fileUploadLabel = \"choose file to upload\";\n // If it's the long upload label, we might need smaller font or wrapping, \n // but typically buttons are single line.\n // We'll just truncate if too long or let it clip via context clipping if we added any (we didn't yet).\n\n ctx.fillText(label, x + width / 2, y + height / 2);\n\n ctx.restore();\n }\n\n /**\n * Draw a mini cursor preview at the center of the glass.\n * The cursor is drawn as a classic arrow pointer with a contrasting outline.\n * @param renderSize - The render size of the glass in pixels\n */\n private drawCursorPreview(renderSize: number): void {\n if (!this.offscreenCtx) return;\n const ctx = this.offscreenCtx;\n\n // Cursor size scales with glass size (roughly 8-12% of glass size)\n const cursorSize = Math.max(16, Math.min(32, renderSize * 0.1));\n\n // Center position of the glass\n const centerX = renderSize / 2;\n const centerY = renderSize / 2;\n\n ctx.save();\n\n // Translate to center (cursor tip will be at center)\n ctx.translate(centerX, centerY);\n\n // Scale the cursor shape\n const scale = cursorSize / 24; // Base cursor is 24px\n ctx.scale(scale, scale);\n\n // Classic arrow cursor shape (pointing up-left)\n ctx.beginPath();\n ctx.moveTo(0, 0); // Tip\n ctx.lineTo(0, 17); // Down\n ctx.lineTo(4, 13); // Notch right\n ctx.lineTo(7, 20); // Handle bottom-right\n ctx.lineTo(10, 19); // Handle outer\n ctx.lineTo(7, 12); // Handle top\n ctx.lineTo(12, 12); // Arrow right point\n ctx.closePath();\n\n // Draw white fill\n ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';\n ctx.fill();\n\n // Draw black outline for contrast\n ctx.strokeStyle = 'rgba(0, 0, 0, 0.9)';\n ctx.lineWidth = 1.5;\n ctx.stroke();\n\n ctx.restore();\n }\n\n isAvailable(): boolean {\n return this.offscreenCanvas !== null && this.offscreenCtx !== null;\n }\n\n getCanvas(): HTMLCanvasElement | null {\n return this.offscreenCanvas;\n }\n}\n"],"names":["text","maxWidth"],"mappings":";;;AAaO,MAAM,qBAAN,MAAM,mBAAkB;AAAA,EAc3B,YAAY,QAAuB,OAAuB;AAblD;AACA;AACA,2CAA4C;AAC5C,wCAAgD;AAChD,2CAA0B;AAC1B,4CAA2B;AAC3B,uCAAuB;AAIvB,+CAA8B;AAC9B,mDAAoD;AAGxD,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,oBAAA;AAAA,EACT;AAAA,EAEQ,sBAA4B;AAChC,SAAK,kBAAkB,SAAS,cAAc,QAAQ;AACtD,SAAK,eAAe,KAAK,gBAAgB,WAAW,IAAI;AAAA,EAC5D;AAAA,EAEA,oBAAoB,cAA2D;AAC3E,QAAI,CAAC,KAAK,mBAAmB,CAAC,KAAK,aAAc,QAAO;AACxD,QAAI,KAAK,YAAa,QAAO;AAE7B,UAAM,WAAW,2BAAK;AACtB,QAAI,CAAC,YAAY,CAAC,SAAS,GAAI,QAAO;AAEtC,UAAM,aAAa,KAAK,OAAO;AAG/B,QAAI,KAAK,oBAAoB,cAAc,KAAK,qBAAqB,YAAY;AAC7E,WAAK,gBAAgB,QAAQ;AAC7B,WAAK,gBAAgB,SAAS;AAC9B,WAAK,kBAAkB;AACvB,WAAK,mBAAmB;AAAA,IAC5B;AAEA,UAAM,eAAe,SAAS,GAAG;AACjC,UAAM,OAAO,aAAa,sBAAA;AAE1B,UAAM,MAAM,KAAK,QAAQ,IAAI,aAAa,QAAQ,KAAK,QAAQ;AAQ/D,UAAM,mBAAmB,gBAAgB,KAAO,KAAK,OAAO;AAE5D,QAAI,kBAAkB;AAElB,WAAK,0BAA0B;AAC/B,aAAO,KAAK,oBAAoB,cAAc,YAAY,GAAG;AAAA,IACjE,OAAO;AAEH,YAAM,MAAM,YAAY,IAAA;AACxB,YAAM,UAAU,MAAM,KAAK;AAE3B,UAAI,UAAU,mBAAkB,gCAAgC,KAAK,yBAAyB;AAE1F,eAAO,KAAK;AAAA,MAChB;AAGA,YAAM,SAAS,KAAK,kBAAkB,UAAU,cAAc,YAAY,cAAc,GAAG;AAC3F,WAAK,sBAAsB;AAC3B,WAAK,0BAA0B;AAC/B,aAAO;AAAA,IACX;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,wBAAwB,KAAa,OAAe,QAA2B;AAlFpF;AAmFC,UAAM,QAAQ,2BAAK;AACnB,QAAI,CAAC,SAAS,CAAC,MAAM,OAAQ,QAAO;AAEpC,UAAM,aAAa,KAAK,OAAO;AAC/B,UAAM,gBAAgB,aAAa,KAAK,OAAO;AAG/C,UAAM,YAAY,KAAK,MAAM,IAAI;AACjC,UAAM,YAAY,KAAK,MAAM,IAAI;AAGjC,UAAM,cAAc,YAAa,gBAAgB;AACjD,UAAM,aAAa,YAAa,gBAAgB;AAChD,UAAM,eAAe,cAAc;AACnC,UAAM,gBAAgB,aAAa;AAGnC,UAAM,oBAAoB;AAAA,MACtB;AAAA,MAAa;AAAA,MAAgB;AAAA,MAAa;AAAA,MAC1C;AAAA,MAAiB;AAAA;AAAA,MACjB;AAAA,MAAgB;AAAA;AAAA,IAAA;AAGpB,eAAW,QAAQ,MAAM,QAAQ;AAC7B,UAAI,CAAC,KAAK,OAAO,CAAC,KAAK,KAAM;AAC7B,WAAI,UAAK,UAAL,mBAAY,UAAW;AAG3B,YAAM,WAAW,KAAK,QAAQ,KAAK,cAAc;AACjD,YAAM,kBAAkB,kBAAkB;AAAA,QAAK,OAC3C,SAAS,YAAA,EAAc,SAAS,EAAE,aAAa;AAAA,MAAA,OAC9C,UAAK,SAAL,mBAAW,UAAS;AAEzB,UAAI,CAAC,gBAAiB;AAGtB,YAAM,WAAW,KAAK,IAAI,CAAC,IAAI,QAAQ,OAAO,CAAC;AAC/C,YAAM,WAAW,KAAK,IAAI,CAAC,IAAI,QAAQ,OAAO,CAAC;AAC/C,YAAM,eAAe,KAAK,KAAK,CAAC,IAAI;AACpC,YAAM,gBAAgB,KAAK,KAAK,CAAC,IAAI;AAGrC,YAAM,YAAY,WAAW;AAC7B,YAAM,aAAa,WAAW;AAE9B,YAAM,WAAW,EAAE,YAAY,eAAe,WAAW,gBACrD,aAAa,cAAc,WAAW;AAE1C,UAAI,UAAU;AACV,eAAO;AAAA,MACX;AAAA,IACJ;AAEA,WAAO;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,oBAAoB,cAAiC,YAAoB,KAAuC;AA/IrH;AAgJC,QAAI;AACA,WAAK,cAAc;AAGnB,YAAM,YAAY,KAAK,MAAM,IAAI;AACjC,YAAM,YAAY,KAAK,MAAM,IAAI;AAEjC,YAAM,gBAAgB,aAAa,KAAK,OAAO;AAG/C,YAAM,cAAc,gBAAgB;AACpC,YAAM,eAAe,gBAAgB;AACrC,YAAM,WAAW,YAAa,gBAAgB,KAAM;AACpD,YAAM,WAAW,YAAa,gBAAgB,KAAM;AAEpD,WAAK,aAAc,UAAU,GAAG,GAAG,YAAY,UAAU;AACzD,WAAK,aAAc;AAAA,QACf;AAAA,QACA;AAAA,QAAS;AAAA,QAAS;AAAA,QAAa;AAAA,QAC/B;AAAA,QAAG;AAAA,QAAG;AAAA,QAAY;AAAA,MAAA;AAItB,YAAM,WAAW,2BAAK;AACtB,YAAM,iBAAe,0CAAU,OAAV,mBAAc,UAAS;AAC5C,YAAM,kBAAkC,0CAAU,OAAV,mBAAc,UAAS,CAAC,SAAS,GAAG,OAAO,CAAC,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;AACrH,WAAK,uBAAuB,SAAS,SAAS,aAAa,cAAc,YAAY,cAAc,aAAa;AAOhH,UAAI,KAAK,OAAO,mBAAmB;AAC/B,aAAK,kBAAkB,UAAU;AAAA,MACrC;AAEA,WAAK,cAAc;AACnB,aAAO,KAAK;AAAA,IAChB,SAAS,GAAG;AACR,cAAQ,KAAK,wBAAwB,CAAC;AACtC,WAAK,cAAc;AACnB,aAAO;AAAA,IACX;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,kBAAkB,UAAe,cAAiC,YAAoB,cAAsB,KAAuC;AAnMxJ;AAoMC,QAAI;AACA,WAAK,cAAc;AAElB,aAAe,0BAA0B;AAG1C,YAAM,YAAY,KAAK,MAAM,IAAI;AACjC,YAAM,YAAY,KAAK,MAAM,IAAI;AAGjC,YAAM,cAAc,KAAK,IAAI,GAAK,YAAY;AAG9C,YAAM,YAAY,SAAS,GAAG;AAC9B,YAAM,cAAc,SAAS,GAAG,OAAO,CAAC;AACxC,YAAM,cAAc,SAAS,GAAG,OAAO,CAAC;AAIxC,UAAI,OAAO,SAAS,YAAY,YAAY;AACxC,iBAAS,QAAQ,aAAa,CAAC,WAAW,SAAS,CAAC;AAAA,MACxD,OAAO;AAEH,cAAM,eAAe,YAAY,eAAe;AAChD,cAAM,eAAe,YAAY,eAAe;AAChD,cAAM,aAAa,YAAa,cAAc;AAC9C,cAAM,aAAa,YAAa,cAAc;AAC9C,iBAAS,GAAG,QAAQ;AACpB,iBAAS,GAAG,OAAO,CAAC,IAAI;AACxB,iBAAS,GAAG,OAAO,CAAC,IAAI;AAAA,MAC5B;AAIA,YAAM,uCAAuB,IAAA;AAC7B,YAAM,wCAAwB,IAAA;AAE9B,WAAI,gCAAK,UAAL,mBAAY,QAAQ;AACpB,mBAAW,QAAQ,IAAI,MAAM,QAAQ;AAEjC,cAAI,KAAK,MAAM;AACX,6BAAiB,IAAI,MAAM,KAAK,IAAI;AACpC,iBAAK,OAAO;AAAA,UAChB;AAGA,cAAI,KAAK,WAAW,MAAM,QAAQ,KAAK,OAAO,GAAG;AAC7C,kBAAM,mBAAmB,KAAK,QAAQ,KAAK,CAAC,MAAW;AACnD,oBAAM,QAAQ,OAAO,EAAE,QAAQ,EAAE,EAAE,YAAA;AACnC,oBAAM,QAAQ,OAAO,EAAE,QAAQ,EAAE,EAAE,YAAA;AACnC,qBAAO,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,QAAQ,KAC/G,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,OAAO;AAAA,YAC3D,CAAC;AAED,gBAAI,kBAAkB;AAElB,gCAAkB,IAAI,MAAM,KAAK,OAAO;AAExC,mBAAK,UAAU,KAAK,QAAQ,OAAO,CAAC,MAAW;AAC3C,sBAAM,QAAQ,OAAO,EAAE,QAAQ,EAAE,EAAE,YAAA;AACnC,sBAAM,QAAQ,OAAO,EAAE,QAAQ,EAAE,EAAE,YAAA;AAEnC,sBAAM,YAAY,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,SAAS,KACnE,MAAM,SAAS,SAAS;AAC5B,uBAAO,CAAC;AAAA,cACZ,CAAC;AAAA,YACL;AAAA,UACJ;AAAA,QACJ;AAAA,MACJ;AAEA,UAAI;AACA,iBAAS,KAAK,MAAM,IAAI;AAAA,MAC5B,UAAA;AAEI,mBAAW,CAAC,MAAM,IAAI,KAAK,iBAAiB,WAAW;AACnD,eAAK,OAAO;AAAA,QAChB;AAEA,mBAAW,CAAC,MAAM,OAAO,KAAK,kBAAkB,WAAW;AACvD,eAAK,UAAU;AAAA,QACnB;AAAA,MACJ;AAGA,YAAM,gBAAgB,aAAa,KAAK,OAAO;AAC/C,YAAM,cAAc,gBAAgB;AACpC,YAAM,eAAe,gBAAgB;AACrC,YAAM,WAAW,YAAa,gBAAgB,KAAM;AACpD,YAAM,WAAW,YAAa,gBAAgB,KAAM;AAEpD,WAAK,aAAc,UAAU,GAAG,GAAG,YAAY,UAAU;AACzD,WAAK,aAAc;AAAA,QACf;AAAA,QACA;AAAA,QAAS;AAAA,QAAS;AAAA,QAAa;AAAA,QAC/B;AAAA,QAAG;AAAA,QAAG;AAAA,QAAY;AAAA,MAAA;AAMtB,YAAM,gBAAkC,CAAC,SAAS,GAAG,OAAO,CAAC,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC;AACrF,WAAK,uBAAuB,SAAS,SAAS,aAAa,cAAc,YAAY,aAAa,aAAa;AAG/G,WAAK,0BAA0B,SAAS,SAAS,aAAa,cAAc,YAAY,aAAa,aAAa;AAGlH,UAAI,KAAK,OAAO,mBAAmB;AAC/B,aAAK,kBAAkB,UAAU;AAAA,MACrC;AAMA,eAAS,GAAG,QAAQ;AACpB,eAAS,GAAG,OAAO,CAAC,IAAI;AACxB,eAAS,GAAG,OAAO,CAAC,IAAI;AACxB,eAAS,KAAK,MAAM,IAAI;AAExB,WAAK,cAAc;AAClB,aAAe,0BAA0B;AAC1C,aAAO,KAAK;AAAA,IAEhB,SAAS,GAAG;AACR,cAAQ,KAAK,sBAAsB,CAAC;AAEpC,eAAS,GAAG,QAAQ;AACpB,eAAS,KAAK,MAAM,IAAI;AACxB,WAAK,cAAc;AAClB,aAAe,0BAA0B;AAC1C,aAAO;AAAA,IACX;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcQ,uBACJ,SACA,SACA,aACA,cACA,YACA,OACA,QACI;AAhWL;AAiWC,UAAM,QAAQ,2BAAK;AACnB,QAAI,CAAC,SAAS,CAAC,MAAM,UAAU,CAAC,KAAK,aAAc;AAEnD,UAAM,MAAM,KAAK;AACjB,UAAM,WAAW,2BAAK;AACtB,QAAI,CAAC,YAAY,CAAC,SAAS,GAAI;AAG/B,UAAM,eAAe;AACrB,UAAM,gBAAgB;AACtB,UAAM,gBAAgB;AACtB,UAAM,UAAU;AAWhB,UAAM,gBAAgB,aAAa,KAAK,OAAO;AAC/C,UAAM,YAAY,cAAc;AAChC,UAAM,eAAe,aAAa;AAGlC,UAAM,aAAa,UAAU;AAC7B,UAAM,aAAa,UAAU;AAC7B,UAAM,iBAAiB,cAAc;AACrC,UAAM,kBAAkB,eAAe;AAEvC,eAAW,QAAQ,MAAM,QAAQ;AAC7B,UAAI,CAAC,KAAK,WAAW,CAAC,KAAK,OAAO,CAAC,KAAK,KAAM;AAC9C,WAAI,UAAK,UAAL,mBAAY,UAAW;AAI3B,YAAM,WAAW,KAAK,IAAI,CAAC,IAAI,QAAQ,OAAO,CAAC;AAC/C,YAAM,WAAW,KAAK,IAAI,CAAC,IAAI,QAAQ,OAAO,CAAC;AAC/C,YAAM,eAAe,KAAK,KAAK,CAAC,IAAI;AACpC,YAAM,gBAAgB,KAAK,KAAK,CAAC,IAAI;AAGrC,UAAI,WAAW,eAAe,cAAc,WAAW,aAAa,eAAgB;AACpF,UAAI,WAAW,gBAAgB,cAAc,WAAW,aAAa,gBAAiB;AAGtF,UAAI,UAAU;AAEd,iBAAW,UAAU,KAAK,SAAS;AAE/B,cAAM,eAAgB,OAAe,UAAU;AAK/C,cAAM,aAAa,OAAO,OAAO,QAAQ,EAAE,EAAE,YAAA;AAS7C,cAAM,iBAAkB,OAAe,kBAAkB;AACzD,cAAM,mBAAmB,eAAe;AAExC,cAAM,oBAAoB,oBAAoB,eAAe,iBACvD,eAAe,UAAU,eAAe,eAAe,kBAAkB;AAG/E,cAAM,aAAa,WAAY,UAAU;AACzC,cAAM,aAAa,WAAY,eAAe;AAC9C,cAAM,iBAAiB,eAAgB,UAAU,IAAI;AAGrD,cAAM,WAAW,aAAa,cAAc,YAAY;AACxD,cAAM,WAAW,aAAa,cAAc,YAAY;AACxD,cAAM,cAAc,iBAAiB,YAAY;AAGjD,cAAM,eAAe;AACrB,cAAM,WAAW,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,eAAe,QAAQ,YAAY,YAAY,CAAC;AAC3F,cAAM,aAAa,WAAW;AAK9B,YAAI,qBAAqB,OAAO,UAAU,UAAa,OAAO,UAAU,MAAM;AAC1E,gBAAM,YAAY,OAAO,OAAO,KAAK;AACrC,cAAI,UAAU,SAAS,GAAG;AAEtB,gBAAI,gBAAgB;AACpB,gBAAI,kBAAkB;AAElB,kBAAI,KAAA;AACJ,kBAAI,OAAO,OAAO,QAAQ;AAC1B,oBAAM,WAAW,cAAc;AAE/B,oBAAM,QAAQ,UAAU,MAAM,IAAI;AAClC,yBAAW,QAAQ,OAAO;AACtB,sBAAM,QAAQ,KAAK,MAAM,OAAO;AAChC,oBAAI,cAAc;AAClB,oBAAI,YAAY;AAChB,2BAAW,QAAQ,OAAO;AACtB,sBAAI,IAAI,YAAY,cAAc,IAAI,EAAE,QAAQ,UAAU;AACtD;AACA,kCAAc;AAAA,kBAClB,OAAO;AACH,mCAAe;AAAA,kBACnB;AAAA,gBACJ;AACA,iCAAiB,YAAY;AAAA,cACjC;AACA,kBAAI,QAAA;AAGJ,+BAAiB;AAAA,YACrB;AAIA,kBAAM,gBAAgB,WAAW;AAEjC,kBAAM,qBAAqB,KAAK,IAAI,GAAG,gBAAgB,aAAc,UAAU,KAAM;AAErF,kBAAM,kBAAkB,qBAAqB,YAAY;AAGzD,kBAAM,eAAgB,mBAChB,KAAK,IAAI,KAAK,IAAK,OAAe,iBAAiB,QAAQ,gBAAgB,GAAG,aAAa,GAAG,eAAe,IAC3G,OAAe,iBACZ,OAAe,iBAAiB,QAAQ,eACzC,KAAK,IAAI,IAAI,WAAW,CAAC;AAGnC,kBAAM,cAAc,UAAU;AAC9B,kBAAM,eAAe,UAAU;AAC/B,kBAAM,YAAY,cAAc,KAAK,UAAU,cAC3C,eAAe,KAAK,UAAU;AAElC,gBAAI,WAAW;AACX,kBAAI,KAAA;AAGJ,oBAAM,aAAa,UAAU;AAC7B,oBAAM,aAAa,UAAU;AAC7B,oBAAM,iBAAiB,cAAc;AACrC,oBAAM,kBAAkB,eAAe;AACvC,oBAAM,eAAe;AAGrB,kBAAI,UAAA;AACJ,kBAAI,UAAU,YAAY,YAAY,gBAAgB,iBAAiB,YAAY;AACnF,kBAAI,YAAY;AAChB,kBAAI,KAAA;AAGJ,kBAAI,cAAc;AAClB,kBAAI,YAAY;AAChB,kBAAI,OAAA;AAGJ,kBAAI,UAAA;AACJ,kBAAI,UAAU,YAAY,YAAY,gBAAgB,iBAAiB,YAAY;AACnF,kBAAI,KAAA;AAGJ,mBAAK,aAAa,KAAK,WAAW,UAAU,GAAG,UAAU,GAAG,cAAc,IAAI,eAAe,GAAG,QAAQ;AAExG,kBAAI,QAAA;AAAA,YACR;AAAA,UACJ;AAAA,QACJ;AAEA,mBAAW,gBAAgB;AAAA,MAC/B;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBQ,0BACJ,SACA,SACA,aACA,cACA,YACA,OACA,QACI;AAhjBL;AAijBC,UAAM,QAAQ,2BAAK;AACnB,QAAI,CAAC,SAAS,CAAC,MAAM,UAAU,CAAC,KAAK,aAAc;AAEnD,UAAM,MAAM,KAAK;AAGjB,UAAM,gBAAgB,aAAa,KAAK,OAAO;AAC/C,UAAM,YAAY,cAAc;AAChC,UAAM,eAAe,aAAa;AAGlC,UAAM,aAAa,UAAU;AAC7B,UAAM,aAAa,UAAU;AAC7B,UAAM,iBAAiB,cAAc;AACrC,UAAM,kBAAkB,eAAe;AAMvC,eAAW,QAAQ,MAAM,QAAQ;AAC7B,UAAI,CAAC,KAAK,OAAO,CAAC,KAAK,KAAM;AAC7B,WAAI,UAAK,UAAL,mBAAY,UAAW;AAG3B,YAAM,WAAW,KAAK,IAAI,CAAC,IAAI,QAAQ,OAAO,CAAC;AAC/C,YAAM,WAAW,KAAK,IAAI,CAAC,IAAI,QAAQ,OAAO,CAAC;AAC/C,YAAM,eAAe,KAAK,KAAK,CAAC,IAAI;AACpC,YAAM,gBAAgB,KAAK,KAAK,CAAC,IAAI;AAGrC,UAAI,WAAW,eAAe,cAAc,WAAW,aAAa,eAAgB;AACpF,UAAI,WAAW,gBAAgB,cAAc,WAAW,aAAa,gBAAiB;AAGtF,UAAI,KAAK,QAAQ,MAAM,QAAQ,KAAK,IAAI,KAAK,KAAK,KAAK,SAAS,GAAG;AAC/D,aAAK;AAAA,UACD;AAAA,UAAK;AAAA,UAAM;AAAA,UAAU;AAAA,UAAU;AAAA,UAAc;AAAA,UAC7C;AAAA,UAAY;AAAA,UAAY;AAAA,UAAW;AAAA,UAAc;AAAA,UAAY;AAAA,QAAA;AAAA,MAErE;AAGA,UAAI,KAAK,SAAS;AACd,mBAAW,UAAU,KAAK,SAAS;AAC/B,gBAAM,aAAa,OAAO,OAAO,QAAQ,EAAE,EAAE,YAAA;AAC7C,cAAI,eAAe,kBAAkB,eAAe,gBAAgB;AAChE,iBAAK;AAAA,cACD;AAAA,cAAK;AAAA,cAAQ;AAAA,cAAM;AAAA,cAAU;AAAA,cAAU;AAAA,cACvC;AAAA,cAAY;AAAA,cAAY;AAAA,cAAW;AAAA,cAAc;AAAA,cAAY;AAAA,YAAA;AAAA,UAErE;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKQ,eACJ,KACA,MACA,UACA,UACA,cACA,eACA,YACA,YACA,WACA,cACA,YACA,OACI;AACJ,UAAM,eAAe;AACrB,UAAM,UAAU;AAGhB,UAAM,aAAa,WAAY,eAAe;AAC9C,UAAM,iBAAiB,eAAgB,UAAU,IAAI;AACrD,UAAM,kBAAkB,gBAAiB,eAAe,QAAU,UAAU;AAE5E,QAAI,kBAAkB,KAAK,mBAAmB,EAAG;AAIjD,UAAM,OAAO,KAAK;AAClB,UAAM,aAAa,KAAK,cAAc;AACtC,UAAM,MAAM,KAAK,KAAK,IAAI,YAAY,KAAK,SAAS,CAAC,CAAC;AAEtD,QAAI,CAAC,OAAO,EAAE,eAAe,qBAAqB,CAAC,IAAI,YAAY,IAAI,iBAAiB,GAAG;AACvF;AAAA,IACJ;AAEA,QAAI;AAEA,YAAM,YAAY,IAAI,eAAe,IAAI;AACzC,YAAM,aAAa,iBAAiB;AAEpC,UAAI,YAAY;AAChB,UAAI,aAAa;AAEjB,UAAI,YAAY,YAAY;AAExB,qBAAa,YAAY;AAAA,MAC7B,OAAO;AAEH,oBAAY,aAAa;AAAA,MAC7B;AAGA,YAAM,QAAQ,WAAY,UAAU,SAAU,iBAAiB,aAAa;AAC5E,YAAM,QAAQ,cAAc,kBAAkB,cAAc;AAG5D,YAAM,WAAW,QAAQ,cAAc,YAAY;AACnD,YAAM,WAAW,QAAQ,cAAc,YAAY;AACnD,YAAM,cAAc,YAAY,YAAY;AAC5C,YAAM,eAAe,aAAa,YAAY;AAG9C,UAAI,UAAU,cAAc,KAAK,UAAU,cACvC,UAAU,eAAe,KAAK,UAAU,YAAY;AAEpD,YAAI,KAAA;AACJ,YAAI,wBAAwB;AAC5B,YAAI,wBAAwB;AAC5B,YAAI,UAAU,KAAK,SAAS,SAAS,aAAa,YAAY;AAC9D,YAAI,QAAA;AAAA,MACR;AAAA,IACJ,SAAS,GAAG;AAAA,IAEZ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKQ,qBACJ,KACA,QACA,MACA,UACA,UACA,cACA,YACA,YACA,WACA,cACA,YACA,OACI;AAEJ,UAAM,UAAU,OAAO;AACvB,UAAM,QAAQ,OAAO;AAGrB,QAAI;AACJ,QAAI,cAAc;AAClB,QAAI,eAAe;AAEnB,QAAI,WAAW,CAAC,QAAQ,UAAU,QAAQ,aAAa,GAAG;AACtD,sBAAgB;AAChB,oBAAc,QAAQ;AACtB,qBAAe,QAAQ;AAAA,IAC3B,WAAW,SAAS,CAAC,MAAM,UAAU,MAAM,eAAe,GAAG;AACzD,sBAAgB;AAChB,oBAAc,MAAM;AACpB,qBAAe,MAAM;AAAA,IACzB;AAEA,QAAI,CAAC,iBAAiB,gBAAgB,KAAK,iBAAiB,EAAG;AAE/D,QAAI;AAEA,YAAM,UAAW,OAAe,UAAU;AAC1C,YAAM,eAAe,OAAO,kBAAkB;AAE9C,YAAM,aAAa,WAAY,UAAU;AACzC,YAAM,iBAAiB,eAAe,KAAK;AAC3C,YAAM,kBAAkB,eAAe;AAEvC,UAAI,kBAAkB,KAAK,mBAAmB,EAAG;AAGjD,YAAM,YAAY,cAAc;AAChC,YAAM,aAAa,iBAAiB;AAEpC,UAAI,YAAY;AAChB,UAAI,aAAa;AAEjB,UAAI,YAAY,YAAY;AACxB,qBAAa,YAAY;AAAA,MAC7B,OAAO;AACH,oBAAY,aAAa;AAAA,MAC7B;AAGA,YAAM,QAAQ,WAAW,KAAK,SAAS,iBAAiB,aAAa;AACrE,YAAM,QAAQ,cAAc,kBAAkB,cAAc;AAG5D,YAAM,WAAW,QAAQ,cAAc,YAAY;AACnD,YAAM,WAAW,QAAQ,cAAc,YAAY;AACnD,YAAM,cAAc,YAAY,YAAY;AAC5C,YAAM,eAAe,aAAa,YAAY;AAG9C,UAAI,UAAU,cAAc,KAAK,UAAU,cACvC,UAAU,eAAe,KAAK,UAAU,YAAY;AAEpD,YAAI,KAAA;AACJ,YAAI,wBAAwB;AAC5B,YAAI,wBAAwB;AAC5B,YAAI,UAAU,eAAe,SAAS,SAAS,aAAa,YAAY;AACxE,YAAI,QAAA;AAAA,MACR;AAAA,IACJ,SAAS,GAAG;AAAA,IAEZ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,aACJ,KACA,MACA,GACA,GACA,UACA,WACA,UACI;AA3xBL;AA4xBC,UAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,UAAM,aAAa,WAAW;AAC9B,QAAI,WAAW;AACf,UAAM,OAAO,IAAI;AAEjB,QAAI,eAAe;AACnB,QAAI,YAAY;AAGhB,UAAM,WAAW,CAACA,OAAcC,WAAkB,SAA2B;AACzE,UAAI,OAAO;AACX,YAAM,QAAQD,MAAK,MAAM,OAAO;AAChC,YAAM,eAAyB,CAAA;AAC/B,UAAI,cAAc;AAElB,iBAAW,QAAQ,OAAO;AACtB,cAAM,WAAW,cAAc;AAC/B,cAAM,YAAY,IAAI,YAAY,QAAQ,EAAE;AAC5C,YAAI,YAAYC,aAAY,YAAY,SAAS,GAAG;AAChD,uBAAa,KAAK,YAAY,SAAS;AACvC,wBAAc,KAAK,UAAA;AAAA,QACvB,OAAO;AACH,wBAAc;AAAA,QAClB;AAAA,MACJ;AACA,UAAI,YAAY,SAAS,gBAAgB,KAAK,YAAY,SAAS;AACnE,aAAO;AAAA,IACX;AAEA,eAAW,QAAQ,OAAO;AACtB,UAAI,YAAY,KAAM;AAEtB,UAAI,aAAa;AACjB,UAAI,cAAc,OAAO,QAAQ;AACjC,UAAI,SAAS;AACb,UAAI,QAAQ;AAGZ,UAAI,KAAK,WAAW,GAAG,GAAG;AACtB,cAAM,UAAQ,UAAK,MAAM,KAAK,MAAhB,mBAAoB,GAAG,WAAU;AAC/C,qBAAa,KAAK,UAAU,KAAK,EAAE,KAAA;AACnC,cAAM,cAAc,KAAK,IAAI,KAAK,MAAO,QAAQ,IAAK;AACtD,cAAM,aAAa,WAAW;AAC9B,sBAAc,OAAO,UAAU;AAE/B,oBAAY,WAAW;AAAA,MAC3B,WAES,KAAK,KAAA,EAAO,WAAW,IAAI,KAAK,KAAK,KAAA,EAAO,WAAW,IAAI,GAAG;AACnE,qBAAa,OAAO,KAAK,KAAA,EAAO,UAAU,CAAC;AAC3C,iBAAS;AAAA,MACb;AAEA,YAAM,UAAU,SAAS,YAAY,WAAW,QAAQ,WAAW;AAEnE,UAAI,OAAO;AACX,UAAI,YAAY;AAEhB,iBAAW,QAAQ,SAAS;AACxB,YAAI,YAAY,KAAM;AACtB,YAAI,SAAS,MAAM,IAAI,QAAQ,QAAQ;AAEvC,cAAM,oBAAoB,KAAK,WAAW,GAAG,IAAK,WAAW,YAAY,MAAM,GAAG,EAAE,CAAC,CAAC,IAAI,MAAO;AACjG,oBAAY;AAAA,MAChB;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,WACJ,KACA,OACA,GACA,GACA,OACA,QACA,UACI;AACJ,UAAM,SAAS;AAEf,QAAI,KAAA;AAGJ,QAAI,UAAA;AACJ,QAAI,UAAU,GAAG,GAAG,OAAO,QAAQ,MAAM;AACzC,QAAI,YAAY;AAChB,QAAI,KAAA;AAGJ,QAAI,cAAc;AAClB,QAAI,YAAY;AAChB,QAAI,OAAA;AAGJ,QAAI,OAAO,OAAO,QAAQ;AAC1B,QAAI,YAAY;AAChB,QAAI,eAAe;AACnB,QAAI,YAAY;AAQhB,QAAI,SAAS,OAAO,IAAI,QAAQ,GAAG,IAAI,SAAS,CAAC;AAEjD,QAAI,QAAA;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,kBAAkB,YAA0B;AAChD,QAAI,CAAC,KAAK,aAAc;AACxB,UAAM,MAAM,KAAK;AAGjB,UAAM,aAAa,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,aAAa,GAAG,CAAC;AAG9D,UAAM,UAAU,aAAa;AAC7B,UAAM,UAAU,aAAa;AAE7B,QAAI,KAAA;AAGJ,QAAI,UAAU,SAAS,OAAO;AAG9B,UAAM,QAAQ,aAAa;AAC3B,QAAI,MAAM,OAAO,KAAK;AAGtB,QAAI,UAAA;AACJ,QAAI,OAAO,GAAG,CAAC;AACf,QAAI,OAAO,GAAG,EAAE;AAChB,QAAI,OAAO,GAAG,EAAE;AAChB,QAAI,OAAO,GAAG,EAAE;AAChB,QAAI,OAAO,IAAI,EAAE;AACjB,QAAI,OAAO,GAAG,EAAE;AAChB,QAAI,OAAO,IAAI,EAAE;AACjB,QAAI,UAAA;AAGJ,QAAI,YAAY;AAChB,QAAI,KAAA;AAGJ,QAAI,cAAc;AAClB,QAAI,YAAY;AAChB,QAAI,OAAA;AAEJ,QAAI,QAAA;AAAA,EACR;AAAA,EAEA,cAAuB;AACnB,WAAO,KAAK,oBAAoB,QAAQ,KAAK,iBAAiB;AAAA,EAClE;AAAA,EAEA,YAAsC;AAClC,WAAO,KAAK;AAAA,EAChB;AACJ;AAAA;AA57BI,cAVS,oBAUe,gCAA+B;AAVpD,IAAM,oBAAN;"} \ No newline at end of file diff --git a/web/shared/settings/defaults.js.map b/web/shared/settings/defaults.js.map index f9e139e..b2e8703 100644 --- a/web/shared/settings/defaults.js.map +++ b/web/shared/settings/defaults.js.map @@ -1 +1 @@ -{"version":3,"file":"defaults.js","sources":["../../../src/shared/settings/defaults.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Default Settings Values\n * \n * Default values for all magnify glass and info panel settings.\n */\n\nimport {\n DEFAULT_GLASS_SIZE,\n DEFAULT_ZOOM_FACTOR,\n DEFAULT_BORDER_WIDTH,\n DEFAULT_BORDER_COLOR,\n DEFAULT_OFFSET_STEP,\n DEFAULT_PANEL_WIDTH,\n DEFAULT_PANEL_MAX_HEIGHT,\n DEFAULT_PANEL_OPACITY\n} from '../constants';\nimport type { GlassSettings, PanelSettings } from './types';\n\n/**\n * Default settings for the magnify glass component.\n */\nexport const DEFAULT_GLASS_SETTINGS: GlassSettings = {\n \"🔍MagnifyGlass.ZoomFactor\": DEFAULT_ZOOM_FACTOR,\n \"🔍MagnifyGlass.GlassSize\": DEFAULT_GLASS_SIZE,\n \"🔍MagnifyGlass.BorderColor\": DEFAULT_BORDER_COLOR,\n \"🔍MagnifyGlass.BorderWidth\": DEFAULT_BORDER_WIDTH,\n \"🔍MagnifyGlass.ActivationKey\": \"x\",\n \"🔍MagnifyGlass.AltRequired\": false,\n \"🔍MagnifyGlass.FollowCursor\": false,\n \"🔍MagnifyGlass.OffsetStep\": DEFAULT_OFFSET_STEP,\n \"🔍MagnifyGlass.GlassPosition\": \"Top-Right\",\n \"🔍MagnifyGlass.ResetKey\": \"o\",\n \"🔍MagnifyGlass.GlassShape\": \"Rounded Square\",\n \"🔍MagnifyGlass.BorderEnabled\": true,\n \"🔍MagnifyGlass.TextureFiltering\": \"Linear\",\n \"🔍MagnifyGlass.AlwaysActiveMode\": true,\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": \"h\",\n \"🔍MagnifyGlass.ShowCursorPreview\": true,\n};\n\n/**\n * Default settings for the info panel component.\n */\nexport const DEFAULT_PANEL_SETTINGS: PanelSettings = {\n \"🔍MagnifyGlass.InfoPanelEnabled\": true,\n \"🔍MagnifyGlass.InfoPanelPosition\": \"Bottom\",\n \"🔍MagnifyGlass.InfoPanelWidth\": DEFAULT_PANEL_WIDTH,\n \"🔍MagnifyGlass.InfoPanelOpacity\": DEFAULT_PANEL_OPACITY,\n \"🔍MagnifyGlass.InfoPanelMaxHeight\": DEFAULT_PANEL_MAX_HEIGHT,\n \"🔍MagnifyGlass.InfoPanelAnimations\": false,\n \"🔍MagnifyGlass.ShowInspectorTab\": false,\n \"🔍MagnifyGlass.ToggleHotkey\": \"i\",\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": \"g\",\n \"🔍MagnifyGlass.PinPanelHotkey\": \"u\",\n \"🔍MagnifyGlass.ShowHoveringControls\": true,\n \"🔍MagnifyGlass.ControlsPosition\": \"left\",\n \"🔍MagnifyGlass.InfoPanelTextColor\": \"#6b7280\",\n \"🔍MagnifyGlass.InfoPanelAccentColor\": \"#3b82f6\",\n \"🔍MagnifyGlass.InfoPanelFontSize\": 14,\n \"🔍MagnifyGlass.InfoPanelFontFamily\": \"System Default\",\n \"🔍MagnifyGlass.InfoPanelPersist\": false,\n};\n"],"names":[],"mappings":";AAqBO,MAAM,yBAAwC;AAAA,EACjD,6BAA6B;AAAA,EAC7B,4BAA4B;AAAA,EAC5B,8BAA8B;AAAA,EAC9B,8BAA8B;AAAA,EAC9B,gCAAgC;AAAA,EAChC,8BAA8B;AAAA,EAC9B,+BAA+B;AAAA,EAC/B,6BAA6B;AAAA,EAC7B,gCAAgC;AAAA,EAChC,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,gCAAgC;AAAA,EAChC,mCAAmC;AAAA,EACnC,mCAAmC;AAAA,EACnC,wCAAwC;AAAA,EACxC,oCAAoC;AACxC;AAKO,MAAM,yBAAwC;AAAA,EACjD,mCAAmC;AAAA,EACnC,oCAAoC;AAAA,EACpC,iCAAiC;AAAA,EACjC,mCAAmC;AAAA,EACnC,qCAAqC;AAAA,EACrC,sCAAsC;AAAA,EACtC,mCAAmC;AAAA,EACnC,+BAA+B;AAAA,EAC/B,2CAA2C;AAAA,EAC3C,iCAAiC;AAAA,EACjC,uCAAuC;AAAA,EACvC,mCAAmC;AAAA,EACnC,qCAAqC;AAAA,EACrC,uCAAuC;AAAA,EACvC,oCAAoC;AAAA,EACpC,sCAAsC;AAAA,EACtC,mCAAmC;AACvC;"} \ No newline at end of file +{"version":3,"file":"defaults.js","sources":["../../../src/shared/settings/defaults.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Default Settings Values\n * \n * Default values for all magnify glass and info panel settings.\n */\n\nimport {\n DEFAULT_GLASS_SIZE,\n DEFAULT_ZOOM_FACTOR,\n DEFAULT_BORDER_WIDTH,\n DEFAULT_BORDER_COLOR,\n DEFAULT_OFFSET_STEP,\n DEFAULT_PANEL_WIDTH,\n DEFAULT_PANEL_MAX_HEIGHT,\n DEFAULT_PANEL_OPACITY\n} from '../constants';\nimport type { GlassSettings, PanelSettings } from './types';\n\n/**\n * Default settings for the magnify glass component.\n */\nexport const DEFAULT_GLASS_SETTINGS: GlassSettings = {\n \"🔍MagnifyGlass.ZoomFactor\": DEFAULT_ZOOM_FACTOR,\n \"🔍MagnifyGlass.GlassSize\": DEFAULT_GLASS_SIZE,\n \"🔍MagnifyGlass.BorderColor\": DEFAULT_BORDER_COLOR,\n \"🔍MagnifyGlass.BorderWidth\": DEFAULT_BORDER_WIDTH,\n \"🔍MagnifyGlass.ActivationKey\": \"x\",\n \"🔍MagnifyGlass.AltRequired\": false,\n \"🔍MagnifyGlass.FollowCursor\": false,\n \"🔍MagnifyGlass.OffsetStep\": DEFAULT_OFFSET_STEP,\n \"🔍MagnifyGlass.GlassPosition\": \"Top-Right\",\n \"🔍MagnifyGlass.ResetKey\": \"o\",\n \"🔍MagnifyGlass.GlassShape\": \"Rounded Square\",\n \"🔍MagnifyGlass.BorderEnabled\": true,\n \"🔍MagnifyGlass.TextureFiltering\": \"Linear\",\n \"🔍MagnifyGlass.AlwaysActiveMode\": true,\n \"🔍MagnifyGlass.ToggleFollowCursorKey\": \"h\",\n \"🔍MagnifyGlass.ShowCursorPreview\": true,\n \"🔍MagnifyGlass.ForceDirectCaptureKey\": \"d\",\n};\n\n/**\n * Default settings for the info panel component.\n */\nexport const DEFAULT_PANEL_SETTINGS: PanelSettings = {\n \"🔍MagnifyGlass.InfoPanelEnabled\": true,\n \"🔍MagnifyGlass.InfoPanelPosition\": \"Bottom\",\n \"🔍MagnifyGlass.InfoPanelWidth\": DEFAULT_PANEL_WIDTH,\n \"🔍MagnifyGlass.InfoPanelOpacity\": DEFAULT_PANEL_OPACITY,\n \"🔍MagnifyGlass.InfoPanelMaxHeight\": DEFAULT_PANEL_MAX_HEIGHT,\n \"🔍MagnifyGlass.InfoPanelAnimations\": false,\n \"🔍MagnifyGlass.ShowInspectorTab\": false,\n \"🔍MagnifyGlass.ToggleHotkey\": \"i\",\n \"🔍MagnifyGlass.GlassPreviewToggleHotkey\": \"g\",\n \"🔍MagnifyGlass.PinPanelHotkey\": \"u\",\n \"🔍MagnifyGlass.ShowHoveringControls\": true,\n \"🔍MagnifyGlass.ControlsPosition\": \"left\",\n \"🔍MagnifyGlass.InfoPanelTextColor\": \"#6b7280\",\n \"🔍MagnifyGlass.InfoPanelAccentColor\": \"#3b82f6\",\n \"🔍MagnifyGlass.InfoPanelFontSize\": 14,\n \"🔍MagnifyGlass.InfoPanelFontFamily\": \"System Default\",\n \"🔍MagnifyGlass.InfoPanelPersist\": false,\n};\n"],"names":[],"mappings":";AAqBO,MAAM,yBAAwC;AAAA,EACjD,6BAA6B;AAAA,EAC7B,4BAA4B;AAAA,EAC5B,8BAA8B;AAAA,EAC9B,8BAA8B;AAAA,EAC9B,gCAAgC;AAAA,EAChC,8BAA8B;AAAA,EAC9B,+BAA+B;AAAA,EAC/B,6BAA6B;AAAA,EAC7B,gCAAgC;AAAA,EAChC,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,gCAAgC;AAAA,EAChC,mCAAmC;AAAA,EACnC,mCAAmC;AAAA,EACnC,wCAAwC;AAAA,EACxC,oCAAoC;AAExC;AAKO,MAAM,yBAAwC;AAAA,EACjD,mCAAmC;AAAA,EACnC,oCAAoC;AAAA,EACpC,iCAAiC;AAAA,EACjC,mCAAmC;AAAA,EACnC,qCAAqC;AAAA,EACrC,sCAAsC;AAAA,EACtC,mCAAmC;AAAA,EACnC,+BAA+B;AAAA,EAC/B,2CAA2C;AAAA,EAC3C,iCAAiC;AAAA,EACjC,uCAAuC;AAAA,EACvC,mCAAmC;AAAA,EACnC,qCAAqC;AAAA,EACrC,uCAAuC;AAAA,EACvC,oCAAoC;AAAA,EACpC,sCAAsC;AAAA,EACtC,mCAAmC;AACvC;"} \ No newline at end of file