diff --git a/src/magnify-glass/WebGLRenderer.ts b/src/magnify-glass/WebGLRenderer.ts
index bf77cc0..42d794f 100644
--- a/src/magnify-glass/WebGLRenderer.ts
+++ b/src/magnify-glass/WebGLRenderer.ts
@@ -403,21 +403,36 @@ export class WebGLRenderer {
animation: slideIn 0.3s ease-out;
`;
- toast.innerHTML = `
-
-
⚠️
-
- MagnifyGlass: WebGL Disabled
- ${instructions}
-
-
-
- `;
+ cursor: pointer; font-size: 12px;`;
+ btn.addEventListener('click', () => toast.remove());
+ content.appendChild(btn);
+
+ container.appendChild(content);
+ toast.appendChild(container);
// Add animation keyframes
if (!document.getElementById('magnifyglass-toast-style')) {
diff --git a/tests/unit/WebGLRenderer.test.ts b/tests/unit/WebGLRenderer.test.ts
new file mode 100644
index 0000000..c1388fd
--- /dev/null
+++ b/tests/unit/WebGLRenderer.test.ts
@@ -0,0 +1,55 @@
+import { describe, it, expect, afterEach, vi } from 'vitest';
+import { WebGLRenderer } from '../../src/magnify-glass/WebGLRenderer';
+
+describe('WebGLRenderer Security', () => {
+ afterEach(() => {
+ document.body.innerHTML = '';
+ vi.restoreAllMocks();
+ });
+
+ it('should safely render error instructions without XSS', () => {
+ // Mock dependencies
+ const mockUi = { glassCanvas: null } as any;
+ const mockConfig = {} as any;
+ const mockState = {} as any;
+
+ const renderer = new WebGLRenderer(mockConfig, mockState, mockUi);
+
+ // Payload with XSS vector
+ const maliciousInstruction = 'Failed
';
+
+ renderer.showWebGLError(maliciousInstruction);
+
+ const toast = document.getElementById('magnifyglass-webgl-error');
+ expect(toast).toBeTruthy();
+
+ // Check if the malicious instruction is rendered as text, not HTML
+ // In the vulnerable version, this would be true:
+ // expect(toast?.innerHTML).toContain('
');
+
+ // In the fixed version, we expect:
+ expect(toast?.textContent).toContain(maliciousInstruction);
+ expect(toast?.innerHTML).not.toContain('
');
+ expect(toast?.innerHTML).toContain('<img src=x onerror=alert(1)>');
+ });
+
+ it('should have a working dismiss button using event listener', () => {
+ const mockUi = { glassCanvas: null } as any;
+ const renderer = new WebGLRenderer({} as any, {} as any, mockUi);
+
+ renderer.showWebGLError('Test error');
+ const toast = document.getElementById('magnifyglass-webgl-error');
+ const button = toast?.querySelector('button');
+
+ expect(button).toBeTruthy();
+
+ // Verify inline handler is NOT present (CSP compliance)
+ expect(button?.hasAttribute('onclick')).toBe(false);
+
+ // Click to dismiss
+ button?.click();
+
+ // Verify toast is removed
+ expect(document.getElementById('magnifyglass-webgl-error')).toBeNull();
+ });
+});
diff --git a/web/magnify-glass/WebGLRenderer.js b/web/magnify-glass/WebGLRenderer.js
index 68a5c0e..bb66e65 100644
--- a/web/magnify-glass/WebGLRenderer.js
+++ b/web/magnify-glass/WebGLRenderer.js
@@ -290,21 +290,30 @@ class WebGLRenderer {
z-index: 100000;
animation: slideIn 0.3s ease-out;
`;
- toast.innerHTML = `
-
-
⚠️
-
- MagnifyGlass: WebGL Disabled
- ${instructions}
-
-
-
- `;
+ cursor: pointer; font-size: 12px;`;
+ btn.addEventListener("click", () => toast.remove());
+ content.appendChild(btn);
+ container.appendChild(content);
+ toast.appendChild(container);
if (!document.getElementById("magnifyglass-toast-style")) {
const style = document.createElement("style");
style.id = "magnifyglass-toast-style";
diff --git a/web/magnify-glass/WebGLRenderer.js.map b/web/magnify-glass/WebGLRenderer.js.map
index ba3d157..f82c005 100644
--- a/web/magnify-glass/WebGLRenderer.js.map
+++ b/web/magnify-glass/WebGLRenderer.js.map
@@ -1 +1 @@
-{"version":3,"file":"WebGLRenderer.js","sources":["../../src/magnify-glass/WebGLRenderer.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - WebGLRenderer (TypeScript)\n * \n * Handles WebGL-based rendering of the magnified view.\n */\n\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\nimport type { UiManager } from './UiManager';\nimport { Logger } from '../shared/logger';\n\ninterface UniformLocations {\n sourceTexture: WebGLUniformLocation | null;\n textureOffset: WebGLUniformLocation | null;\n textureRepeat: WebGLUniformLocation | null;\n glassSize: WebGLUniformLocation | null;\n}\n\ninterface AttributeLocations {\n position: number;\n texCoord: number;\n}\n\n/**\n * WebGL Renderer class.\n * Provides efficient WebGL-based magnification rendering.\n */\nexport class WebGLRenderer {\n config: ConfigManager;\n state: MagnifierState;\n ui: UiManager;\n\n gl: WebGLRenderingContext | null;\n program: WebGLProgram | null;\n texture: WebGLTexture | null;\n positionBuffer: WebGLBuffer | null;\n texCoordBuffer: WebGLBuffer | null;\n uniformLocations: UniformLocations | null;\n attributeLocations: AttributeLocations | null;\n currentFilteringMode: number | null;\n\n // Dirty flag optimization\n lastCanvasVersion: number;\n lastSourceX: number;\n lastSourceY: number;\n lastSourceWidth: number;\n lastSourceHeight: number;\n textureUploadSkipCount: number;\n\n vertexShaderSource: string;\n fragmentShaderSource: string;\n\n constructor(config: ConfigManager, state: MagnifierState, ui: UiManager) {\n this.config = config;\n this.state = state;\n this.ui = ui;\n\n this.gl = null;\n this.program = null;\n this.texture = null;\n this.positionBuffer = null;\n this.texCoordBuffer = null;\n this.uniformLocations = null;\n this.attributeLocations = null;\n this.currentFilteringMode = null;\n\n // Initialize dirty flag tracking\n this.lastCanvasVersion = -1;\n this.lastSourceX = -1;\n this.lastSourceY = -1;\n this.lastSourceWidth = -1;\n this.lastSourceHeight = -1;\n this.textureUploadSkipCount = 0;\n\n this.vertexShaderSource = `\n attribute vec2 a_position;\n attribute vec2 a_texCoord;\n varying vec2 v_texCoord;\n void main() {\n gl_Position = vec4(a_position, 0.0, 1.0);\n v_texCoord = a_texCoord;\n }\n `;\n\n this.fragmentShaderSource = `\n precision mediump float;\n varying vec2 v_texCoord;\n uniform sampler2D u_sourceTexture;\n uniform vec2 u_textureOffset;\n uniform vec2 u_textureRepeat;\n uniform float u_glassSize;\n\n void main() {\n vec2 sampleCoord = u_textureOffset + v_texCoord * u_textureRepeat;\n vec4 color = texture2D(u_sourceTexture, sampleCoord);\n gl_FragColor = color;\n }\n `;\n\n this.initialize();\n }\n\n /**\n * Initialize WebGL context and resources.\n */\n initialize(): void {\n // Get WebGL context\n if (!this.ui.glassCanvas) return;\n\n this.gl = this.ui.glassCanvas.getContext(\"webgl\", { preserveDrawingBuffer: true });\n if (!this.gl) {\n // Try webgl2 as fallback\n this.gl = this.ui.glassCanvas.getContext(\"webgl2\", { preserveDrawingBuffer: true }) as WebGLRenderingContext | null;\n }\n\n if (!this.gl) {\n const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');\n const browserInstructions = isFirefox\n ? \"Firefox: Go to about:config, search for 'webgl.disabled' and set it to 'false', then restart Firefox.\"\n : \"Check your browser settings to ensure WebGL is enabled.\";\n\n console.error(\"ComfyUI Magnifying Glass ERROR: WebGL not supported or context creation failed.\");\n console.error(`To fix: ${browserInstructions}`);\n\n // Show user notification\n this.showWebGLError(browserInstructions);\n return;\n }\n\n // Create shader program\n this.program = this.createShaderProgram(this.gl, this.vertexShaderSource, this.fragmentShaderSource);\n if (!this.program) return;\n\n // Create position buffer\n this.positionBuffer = this.gl.createBuffer();\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);\n const positions = [-1, -1, 1, -1, -1, 1, 1, 1];\n this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW);\n\n // Create texture coordinate buffer\n this.texCoordBuffer = this.gl.createBuffer();\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);\n const texCoords = [0, 1, 1, 1, 0, 0, 1, 0]; // Flipped Y for texCoords\n this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW);\n\n // Create texture\n this.texture = this.gl.createTexture();\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);\n this.updateTextureFiltering(this.config.textureFiltering);\n\n // Get uniform and attribute locations\n this.uniformLocations = {\n sourceTexture: this.gl.getUniformLocation(this.program, \"u_sourceTexture\"),\n textureOffset: this.gl.getUniformLocation(this.program, \"u_textureOffset\"),\n textureRepeat: this.gl.getUniformLocation(this.program, \"u_textureRepeat\"),\n glassSize: this.gl.getUniformLocation(this.program, \"u_glassSize\"),\n };\n\n this.attributeLocations = {\n position: this.gl.getAttribLocation(this.program, \"a_position\"),\n texCoord: this.gl.getAttribLocation(this.program, \"a_texCoord\"),\n };\n }\n\n /**\n * Update texture filtering mode.\n * @param filteringModeString - \"Linear\" or \"Nearest\"\n */\n updateTextureFiltering(filteringModeString: string): void {\n if (!this.gl || !this.texture) return;\n\n let glFilterMode: number;\n if (filteringModeString === \"Nearest\") {\n glFilterMode = this.gl.NEAREST;\n } else {\n glFilterMode = this.gl.LINEAR;\n }\n\n if (this.currentFilteringMode === glFilterMode) return;\n\n this.currentFilteringMode = glFilterMode;\n\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, glFilterMode);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, glFilterMode);\n this.gl.bindTexture(this.gl.TEXTURE_2D, null);\n }\n\n /**\n * Check if renderer is valid and ready.\n * @returns Boolean indicating if WebGL context and program are ready\n */\n isValid(): boolean {\n return this.gl !== null && this.program !== null;\n }\n\n /**\n * Create a WebGL shader program.\n * @param gl - WebGL context\n * @param vsSource - Vertex shader source\n * @param fsSource - Fragment shader source\n * @returns WebGL program or null on failure\n */\n createShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string): WebGLProgram | null {\n const vertexShader = gl.createShader(gl.VERTEX_SHADER);\n if (!vertexShader) return null;\n\n gl.shaderSource(vertexShader, vsSource);\n gl.compileShader(vertexShader);\n if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Vertex shader compilation error:', gl.getShaderInfoLog(vertexShader));\n gl.deleteShader(vertexShader);\n return null;\n }\n\n const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);\n if (!fragmentShader) {\n gl.deleteShader(vertexShader);\n return null;\n }\n\n gl.shaderSource(fragmentShader, fsSource);\n gl.compileShader(fragmentShader);\n if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Fragment shader compilation error:', gl.getShaderInfoLog(fragmentShader));\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n const program = gl.createProgram();\n if (!program) {\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Shader program linking error:', gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n return program;\n }\n\n /**\n * Render the magnified view.\n * @param sourceCanvas - The source canvas to sample from\n */\n render(sourceCanvas: HTMLCanvasElement): void {\n if (!this.gl || !this.program || !this.texture || !this.uniformLocations || !this.attributeLocations) return;\n\n // Check if we need to update the texture\n // We track: canvas version (via LiteGraph's change counter), source coordinates, and source dimensions\n const app = (window as any).app;\n const currentCanvasVersion = app?.graph?.change_counter ?? -1;\n\n const sourceChanged =\n this.lastSourceX !== this.state.sourceX ||\n this.lastSourceY !== this.state.sourceY ||\n this.lastSourceWidth !== this.state.sourceWidth ||\n this.lastSourceHeight !== this.state.sourceHeight;\n\n const canvasChanged = currentCanvasVersion !== this.lastCanvasVersion;\n\n // Only upload texture if something changed\n const needsTextureUpdate = canvasChanged || sourceChanged || this.lastCanvasVersion === -1;\n\n // Bind texture\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n\n if (needsTextureUpdate) {\n try {\n this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceCanvas);\n\n // Update tracking variables\n this.lastCanvasVersion = currentCanvasVersion;\n this.lastSourceX = this.state.sourceX;\n this.lastSourceY = this.state.sourceY;\n this.lastSourceWidth = this.state.sourceWidth;\n this.lastSourceHeight = this.state.sourceHeight;\n\n // Reset skip counter\n this.textureUploadSkipCount = 0;\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error in texImage2D:\", e);\n return;\n }\n } else {\n // Skip texture upload - just increment counter\n this.textureUploadSkipCount++;\n }\n\n // Calculate normalized texture coordinates (UV space: 0-1)\n // Check if this is the offscreen canvas (its size matches glassSize)\n const isOffscreenCanvas = sourceCanvas.width === this.config.glassSize &&\n sourceCanvas.height === this.config.glassSize;\n\n let uvX: number, uvY: number, uvWidth: number, uvHeight: number;\n\n if (isOffscreenCanvas) {\n // Offscreen canvas already contains the correctly rendered region\n // Use full texture\n uvX = 0;\n uvY = 0;\n uvWidth = 1;\n uvHeight = 1;\n } else {\n // Main LiteGraph canvas - calculate subrect\n uvX = this.state.sourceX / sourceCanvas.width;\n uvY = this.state.sourceY / sourceCanvas.height;\n uvWidth = this.state.sourceWidth / sourceCanvas.width;\n uvHeight = this.state.sourceHeight / sourceCanvas.height;\n }\n\n // Set up rendering\n this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);\n this.gl.clearColor(0.0, 0.0, 0.0, 0.0);\n this.gl.clear(this.gl.COLOR_BUFFER_BIT);\n\n this.gl.useProgram(this.program);\n\n // Set uniforms\n this.gl.activeTexture(this.gl.TEXTURE0);\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.uniform1i(this.uniformLocations.sourceTexture, 0);\n this.gl.uniform2f(this.uniformLocations.textureOffset, uvX, uvY);\n this.gl.uniform2f(this.uniformLocations.textureRepeat, uvWidth, uvHeight);\n this.gl.uniform1f(this.uniformLocations.glassSize, this.config.glassSize);\n\n // Set attributes\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);\n this.gl.vertexAttribPointer(this.attributeLocations.position, 2, this.gl.FLOAT, false, 0, 0);\n this.gl.enableVertexAttribArray(this.attributeLocations.position);\n\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);\n this.gl.vertexAttribPointer(this.attributeLocations.texCoord, 2, this.gl.FLOAT, false, 0, 0);\n this.gl.enableVertexAttribArray(this.attributeLocations.texCoord);\n\n // Draw\n this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);\n }\n\n /**\n * Check for WebGL errors.\n * @param label - Debug label\n * @returns True if error occurred\n */\n checkWebGLError(label: string): boolean {\n if (!this.gl) return false;\n\n const err = this.gl.getError();\n if (err !== this.gl.NO_ERROR) {\n console.error(`ComfyUI Magnifying Glass ERROR: WebGL Error (${label}):`, err);\n return true;\n }\n return false;\n }\n\n /**\n * Update viewport when glass size changes.\n */\n updateViewport(): void {\n if (this.gl) {\n this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);\n }\n }\n\n /**\n * Show WebGL error notification to user.\n * @param instructions - Browser-specific instructions to fix\n */\n showWebGLError(instructions: string): void {\n // Create a toast notification\n const toast = document.createElement('div');\n toast.id = 'magnifyglass-webgl-error';\n toast.style.cssText = `\n position: fixed;\n bottom: 20px;\n right: 20px;\n max-width: 400px;\n padding: 16px 20px;\n background: linear-gradient(135deg, #ff4444 0%, #cc3333 100%);\n color: white;\n border-radius: 8px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 14px;\n line-height: 1.5;\n z-index: 100000;\n animation: slideIn 0.3s ease-out;\n `;\n\n toast.innerHTML = `\n \n
⚠️\n
\n MagnifyGlass: WebGL Disabled\n ${instructions}\n \n
\n
\n `;\n\n // Add animation keyframes\n if (!document.getElementById('magnifyglass-toast-style')) {\n const style = document.createElement('style');\n style.id = 'magnifyglass-toast-style';\n style.textContent = `\n @keyframes slideIn {\n from { transform: translateX(100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n }\n `;\n document.head.appendChild(style);\n }\n\n document.body.appendChild(toast);\n\n // Auto-dismiss after 15 seconds\n setTimeout(() => {\n if (toast.parentElement) {\n toast.style.animation = 'slideIn 0.3s ease-out reverse';\n setTimeout(() => toast.remove(), 300);\n }\n }, 15000);\n }\n}\n"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"WebGLRenderer.js","sources":["../../src/magnify-glass/WebGLRenderer.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - WebGLRenderer (TypeScript)\n * \n * Handles WebGL-based rendering of the magnified view.\n */\n\nimport type { ConfigManager } from './ConfigManager';\nimport type { MagnifierState } from './MagnifierState';\nimport type { UiManager } from './UiManager';\nimport { Logger } from '../shared/logger';\n\ninterface UniformLocations {\n sourceTexture: WebGLUniformLocation | null;\n textureOffset: WebGLUniformLocation | null;\n textureRepeat: WebGLUniformLocation | null;\n glassSize: WebGLUniformLocation | null;\n}\n\ninterface AttributeLocations {\n position: number;\n texCoord: number;\n}\n\n/**\n * WebGL Renderer class.\n * Provides efficient WebGL-based magnification rendering.\n */\nexport class WebGLRenderer {\n config: ConfigManager;\n state: MagnifierState;\n ui: UiManager;\n\n gl: WebGLRenderingContext | null;\n program: WebGLProgram | null;\n texture: WebGLTexture | null;\n positionBuffer: WebGLBuffer | null;\n texCoordBuffer: WebGLBuffer | null;\n uniformLocations: UniformLocations | null;\n attributeLocations: AttributeLocations | null;\n currentFilteringMode: number | null;\n\n // Dirty flag optimization\n lastCanvasVersion: number;\n lastSourceX: number;\n lastSourceY: number;\n lastSourceWidth: number;\n lastSourceHeight: number;\n textureUploadSkipCount: number;\n\n vertexShaderSource: string;\n fragmentShaderSource: string;\n\n constructor(config: ConfigManager, state: MagnifierState, ui: UiManager) {\n this.config = config;\n this.state = state;\n this.ui = ui;\n\n this.gl = null;\n this.program = null;\n this.texture = null;\n this.positionBuffer = null;\n this.texCoordBuffer = null;\n this.uniformLocations = null;\n this.attributeLocations = null;\n this.currentFilteringMode = null;\n\n // Initialize dirty flag tracking\n this.lastCanvasVersion = -1;\n this.lastSourceX = -1;\n this.lastSourceY = -1;\n this.lastSourceWidth = -1;\n this.lastSourceHeight = -1;\n this.textureUploadSkipCount = 0;\n\n this.vertexShaderSource = `\n attribute vec2 a_position;\n attribute vec2 a_texCoord;\n varying vec2 v_texCoord;\n void main() {\n gl_Position = vec4(a_position, 0.0, 1.0);\n v_texCoord = a_texCoord;\n }\n `;\n\n this.fragmentShaderSource = `\n precision mediump float;\n varying vec2 v_texCoord;\n uniform sampler2D u_sourceTexture;\n uniform vec2 u_textureOffset;\n uniform vec2 u_textureRepeat;\n uniform float u_glassSize;\n\n void main() {\n vec2 sampleCoord = u_textureOffset + v_texCoord * u_textureRepeat;\n vec4 color = texture2D(u_sourceTexture, sampleCoord);\n gl_FragColor = color;\n }\n `;\n\n this.initialize();\n }\n\n /**\n * Initialize WebGL context and resources.\n */\n initialize(): void {\n // Get WebGL context\n if (!this.ui.glassCanvas) return;\n\n this.gl = this.ui.glassCanvas.getContext(\"webgl\", { preserveDrawingBuffer: true });\n if (!this.gl) {\n // Try webgl2 as fallback\n this.gl = this.ui.glassCanvas.getContext(\"webgl2\", { preserveDrawingBuffer: true }) as WebGLRenderingContext | null;\n }\n\n if (!this.gl) {\n const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');\n const browserInstructions = isFirefox\n ? \"Firefox: Go to about:config, search for 'webgl.disabled' and set it to 'false', then restart Firefox.\"\n : \"Check your browser settings to ensure WebGL is enabled.\";\n\n console.error(\"ComfyUI Magnifying Glass ERROR: WebGL not supported or context creation failed.\");\n console.error(`To fix: ${browserInstructions}`);\n\n // Show user notification\n this.showWebGLError(browserInstructions);\n return;\n }\n\n // Create shader program\n this.program = this.createShaderProgram(this.gl, this.vertexShaderSource, this.fragmentShaderSource);\n if (!this.program) return;\n\n // Create position buffer\n this.positionBuffer = this.gl.createBuffer();\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);\n const positions = [-1, -1, 1, -1, -1, 1, 1, 1];\n this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW);\n\n // Create texture coordinate buffer\n this.texCoordBuffer = this.gl.createBuffer();\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);\n const texCoords = [0, 1, 1, 1, 0, 0, 1, 0]; // Flipped Y for texCoords\n this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW);\n\n // Create texture\n this.texture = this.gl.createTexture();\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);\n this.updateTextureFiltering(this.config.textureFiltering);\n\n // Get uniform and attribute locations\n this.uniformLocations = {\n sourceTexture: this.gl.getUniformLocation(this.program, \"u_sourceTexture\"),\n textureOffset: this.gl.getUniformLocation(this.program, \"u_textureOffset\"),\n textureRepeat: this.gl.getUniformLocation(this.program, \"u_textureRepeat\"),\n glassSize: this.gl.getUniformLocation(this.program, \"u_glassSize\"),\n };\n\n this.attributeLocations = {\n position: this.gl.getAttribLocation(this.program, \"a_position\"),\n texCoord: this.gl.getAttribLocation(this.program, \"a_texCoord\"),\n };\n }\n\n /**\n * Update texture filtering mode.\n * @param filteringModeString - \"Linear\" or \"Nearest\"\n */\n updateTextureFiltering(filteringModeString: string): void {\n if (!this.gl || !this.texture) return;\n\n let glFilterMode: number;\n if (filteringModeString === \"Nearest\") {\n glFilterMode = this.gl.NEAREST;\n } else {\n glFilterMode = this.gl.LINEAR;\n }\n\n if (this.currentFilteringMode === glFilterMode) return;\n\n this.currentFilteringMode = glFilterMode;\n\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, glFilterMode);\n this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, glFilterMode);\n this.gl.bindTexture(this.gl.TEXTURE_2D, null);\n }\n\n /**\n * Check if renderer is valid and ready.\n * @returns Boolean indicating if WebGL context and program are ready\n */\n isValid(): boolean {\n return this.gl !== null && this.program !== null;\n }\n\n /**\n * Create a WebGL shader program.\n * @param gl - WebGL context\n * @param vsSource - Vertex shader source\n * @param fsSource - Fragment shader source\n * @returns WebGL program or null on failure\n */\n createShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string): WebGLProgram | null {\n const vertexShader = gl.createShader(gl.VERTEX_SHADER);\n if (!vertexShader) return null;\n\n gl.shaderSource(vertexShader, vsSource);\n gl.compileShader(vertexShader);\n if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Vertex shader compilation error:', gl.getShaderInfoLog(vertexShader));\n gl.deleteShader(vertexShader);\n return null;\n }\n\n const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);\n if (!fragmentShader) {\n gl.deleteShader(vertexShader);\n return null;\n }\n\n gl.shaderSource(fragmentShader, fsSource);\n gl.compileShader(fragmentShader);\n if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Fragment shader compilation error:', gl.getShaderInfoLog(fragmentShader));\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n const program = gl.createProgram();\n if (!program) {\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('ComfyUI Magnifying Glass ERROR: Shader program linking error:', gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return null;\n }\n\n return program;\n }\n\n /**\n * Render the magnified view.\n * @param sourceCanvas - The source canvas to sample from\n */\n render(sourceCanvas: HTMLCanvasElement): void {\n if (!this.gl || !this.program || !this.texture || !this.uniformLocations || !this.attributeLocations) return;\n\n // Check if we need to update the texture\n // We track: canvas version (via LiteGraph's change counter), source coordinates, and source dimensions\n const app = (window as any).app;\n const currentCanvasVersion = app?.graph?.change_counter ?? -1;\n\n const sourceChanged =\n this.lastSourceX !== this.state.sourceX ||\n this.lastSourceY !== this.state.sourceY ||\n this.lastSourceWidth !== this.state.sourceWidth ||\n this.lastSourceHeight !== this.state.sourceHeight;\n\n const canvasChanged = currentCanvasVersion !== this.lastCanvasVersion;\n\n // Only upload texture if something changed\n const needsTextureUpdate = canvasChanged || sourceChanged || this.lastCanvasVersion === -1;\n\n // Bind texture\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n\n if (needsTextureUpdate) {\n try {\n this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceCanvas);\n\n // Update tracking variables\n this.lastCanvasVersion = currentCanvasVersion;\n this.lastSourceX = this.state.sourceX;\n this.lastSourceY = this.state.sourceY;\n this.lastSourceWidth = this.state.sourceWidth;\n this.lastSourceHeight = this.state.sourceHeight;\n\n // Reset skip counter\n this.textureUploadSkipCount = 0;\n } catch (e) {\n console.error(\"ComfyUI Magnifying Glass ERROR: Error in texImage2D:\", e);\n return;\n }\n } else {\n // Skip texture upload - just increment counter\n this.textureUploadSkipCount++;\n }\n\n // Calculate normalized texture coordinates (UV space: 0-1)\n // Check if this is the offscreen canvas (its size matches glassSize)\n const isOffscreenCanvas = sourceCanvas.width === this.config.glassSize &&\n sourceCanvas.height === this.config.glassSize;\n\n let uvX: number, uvY: number, uvWidth: number, uvHeight: number;\n\n if (isOffscreenCanvas) {\n // Offscreen canvas already contains the correctly rendered region\n // Use full texture\n uvX = 0;\n uvY = 0;\n uvWidth = 1;\n uvHeight = 1;\n } else {\n // Main LiteGraph canvas - calculate subrect\n uvX = this.state.sourceX / sourceCanvas.width;\n uvY = this.state.sourceY / sourceCanvas.height;\n uvWidth = this.state.sourceWidth / sourceCanvas.width;\n uvHeight = this.state.sourceHeight / sourceCanvas.height;\n }\n\n // Set up rendering\n this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);\n this.gl.clearColor(0.0, 0.0, 0.0, 0.0);\n this.gl.clear(this.gl.COLOR_BUFFER_BIT);\n\n this.gl.useProgram(this.program);\n\n // Set uniforms\n this.gl.activeTexture(this.gl.TEXTURE0);\n this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);\n this.gl.uniform1i(this.uniformLocations.sourceTexture, 0);\n this.gl.uniform2f(this.uniformLocations.textureOffset, uvX, uvY);\n this.gl.uniform2f(this.uniformLocations.textureRepeat, uvWidth, uvHeight);\n this.gl.uniform1f(this.uniformLocations.glassSize, this.config.glassSize);\n\n // Set attributes\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);\n this.gl.vertexAttribPointer(this.attributeLocations.position, 2, this.gl.FLOAT, false, 0, 0);\n this.gl.enableVertexAttribArray(this.attributeLocations.position);\n\n this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);\n this.gl.vertexAttribPointer(this.attributeLocations.texCoord, 2, this.gl.FLOAT, false, 0, 0);\n this.gl.enableVertexAttribArray(this.attributeLocations.texCoord);\n\n // Draw\n this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);\n }\n\n /**\n * Check for WebGL errors.\n * @param label - Debug label\n * @returns True if error occurred\n */\n checkWebGLError(label: string): boolean {\n if (!this.gl) return false;\n\n const err = this.gl.getError();\n if (err !== this.gl.NO_ERROR) {\n console.error(`ComfyUI Magnifying Glass ERROR: WebGL Error (${label}):`, err);\n return true;\n }\n return false;\n }\n\n /**\n * Update viewport when glass size changes.\n */\n updateViewport(): void {\n if (this.gl) {\n this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);\n }\n }\n\n /**\n * Show WebGL error notification to user.\n * @param instructions - Browser-specific instructions to fix\n */\n showWebGLError(instructions: string): void {\n // Create a toast notification\n const toast = document.createElement('div');\n toast.id = 'magnifyglass-webgl-error';\n toast.style.cssText = `\n position: fixed;\n bottom: 20px;\n right: 20px;\n max-width: 400px;\n padding: 16px 20px;\n background: linear-gradient(135deg, #ff4444 0%, #cc3333 100%);\n color: white;\n border-radius: 8px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 14px;\n line-height: 1.5;\n z-index: 100000;\n animation: slideIn 0.3s ease-out;\n `;\n\n const container = document.createElement('div');\n container.style.cssText = \"display: flex; align-items: flex-start; gap: 12px;\";\n\n const icon = document.createElement('span');\n icon.style.fontSize = \"24px\";\n icon.textContent = \"⚠️\";\n container.appendChild(icon);\n\n const content = document.createElement('div');\n\n const title = document.createElement('strong');\n title.style.cssText = \"display: block; margin-bottom: 6px;\";\n title.textContent = \"MagnifyGlass: WebGL Disabled\";\n content.appendChild(title);\n\n const message = document.createElement('span');\n message.style.opacity = \"0.9\";\n message.textContent = instructions;\n content.appendChild(message);\n\n const btn = document.createElement('button');\n btn.textContent = \"Dismiss\";\n btn.style.cssText = `display: block; margin-top: 10px; padding: 6px 12px; background: rgba(255,255,255,0.2); \n border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; color: white; \n cursor: pointer; font-size: 12px;`;\n btn.addEventListener('click', () => toast.remove());\n content.appendChild(btn);\n\n container.appendChild(content);\n toast.appendChild(container);\n\n // Add animation keyframes\n if (!document.getElementById('magnifyglass-toast-style')) {\n const style = document.createElement('style');\n style.id = 'magnifyglass-toast-style';\n style.textContent = `\n @keyframes slideIn {\n from { transform: translateX(100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n }\n `;\n document.head.appendChild(style);\n }\n\n document.body.appendChild(toast);\n\n // Auto-dismiss after 15 seconds\n setTimeout(() => {\n if (toast.parentElement) {\n toast.style.animation = 'slideIn 0.3s ease-out reverse';\n setTimeout(() => toast.remove(), 300);\n }\n }, 15000);\n }\n}\n"],"names":[],"mappings":"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"}
\ No newline at end of file