From 4b007919a2b7df6930bb79828d50da39f6f7719b Mon Sep 17 00:00:00 2001 From: AEmotionStudio Date: Sun, 4 Jan 2026 22:24:04 -0800 Subject: [PATCH] feat: Implement button rendering and refine preview widget detection logic. --- src/magnify-glass/OffscreenRenderer.ts | 92 ++++++++++++++++------ web/magnify-glass/OffscreenRenderer.js | 41 +++++++--- web/magnify-glass/OffscreenRenderer.js.map | 2 +- 3 files changed, 102 insertions(+), 33 deletions(-) diff --git a/src/magnify-glass/OffscreenRenderer.ts b/src/magnify-glass/OffscreenRenderer.ts index e0b1619..f1819e7 100644 --- a/src/magnify-glass/OffscreenRenderer.ts +++ b/src/magnify-glass/OffscreenRenderer.ts @@ -262,8 +262,9 @@ export class OffscreenRenderer { node.widgets = node.widgets.filter((w: any) => { const wName = String(w.name || '').toLowerCase(); const wType = String(w.type || '').toLowerCase(); - const isPreview = wName.includes('preview') || wName.includes('image') || wName.includes('gallery') || wName.includes('upload') || - wType.includes('preview') || wType.includes('image'); + // Only hide explicit preview widgets. Inputs (image, upload) should remain visible. + const isPreview = wName.includes('preview') || wName.includes('gallery') || + wType.includes('preview'); return !isPreview; }); } @@ -420,40 +421,40 @@ export class OffscreenRenderer { // - type 'string' with no computedHeight or small computedHeight // - These are already rendered on the canvas by ComfyUI const computedHeight = (widget as any).computedHeight || 0; - const isMarkdownWidget = widgetType === 'markdown'; + const isMarkdownWidget = widgetType === 'markdown'; // ComfyUI standard type + const isMultiLineWidget = isMarkdownWidget || widgetType === 'customtext' || ((widgetType === 'text' || widgetType === 'textarea') && computedHeight >= 40); + // Common calculations for positioning + const widgetCssX = nodeCssX + (PADDING * scale); + const widgetCssY = nodeCssY + (widgetLocalY * scale); + const widgetCssWidth = nodeCssWidth - (PADDING * 2 * scale); + + // Convert from CSS to offscreen canvas coordinates + const canvasX = (widgetCssX - sourceCssX) * actualDpr * captureScale; + const canvasY = (widgetCssY - sourceCssY) * actualDpr * captureScale; + const widgetWidth = widgetCssWidth * actualDpr * captureScale; + + // Calculate font size + const baseFontSize = 13; + const fontSize = Math.max(10, Math.min(28, baseFontSize * scale * actualDpr * captureScale)); + const lineHeight = fontSize * 1.4; + + // ---------------------------------------------------------------- + // RENDER TEXT / MARKDOWN + // ---------------------------------------------------------------- if (isMultiLineWidget && widget.value !== undefined && widget.value !== null) { const textValue = String(widget.value); if (textValue.length > 0) { - // Widget position in CSS pixels (relative to canvas) - const widgetCssX = nodeCssX + (PADDING * scale); - const widgetCssY = nodeCssY + (widgetLocalY * scale); - const widgetCssWidth = nodeCssWidth - (PADDING * 2 * scale); - - // Convert from CSS to offscreen canvas coordinates - // Position relative to source region, scaled to output size - const canvasX = (widgetCssX - sourceCssX) * actualDpr * captureScale; - const canvasY = (widgetCssY - sourceCssY) * actualDpr * captureScale; - const widgetWidth = widgetCssWidth * actualDpr * captureScale; - - // Calculate font size based on scale - const baseFontSize = 13; - const fontSize = Math.max(10, Math.min(28, baseFontSize * scale * actualDpr * captureScale)); - const lineHeight = fontSize * 1.4; - // Calculate required height based on content let contentHeight = 0; if (isMarkdownWidget) { // Rough estimation of height needed - // We don't have ctx context here easily for precise measurement without setting font - // So we do a generous estimation or try to measure ctx.save(); ctx.font = `500 ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif`; const maxWidth = widgetWidth - 12; - // reuse wrap logic estimation const lines = textValue.split('\n'); for (const line of lines) { const words = line.split(/(\s+)/); @@ -864,6 +865,53 @@ export class OffscreenRenderer { } } + + + /** + * Render a button widget. + * Draws a rounded rectangle with the label centered. + */ + private drawButton( + ctx: CanvasRenderingContext2D, + label: string, + x: number, + y: number, + width: number, + height: number, + fontSize: number + ): void { + const radius = 6; + + ctx.save(); + + // Button background + ctx.beginPath(); + ctx.roundRect(x, y, width, height, radius); + ctx.fillStyle = '#222'; // Dark button background + ctx.fill(); + + // Button border + ctx.strokeStyle = '#555'; + ctx.lineWidth = 1; + ctx.stroke(); + + // Button label + ctx.font = `600 ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = '#ccc'; // Light grey text + + // Center text in button + const fileUploadLabel = "choose file to upload"; + // If it's the long upload label, we might need smaller font or wrapping, + // but typically buttons are single line. + // We'll just truncate if too long or let it clip via context clipping if we added any (we didn't yet). + + ctx.fillText(label, x + width / 2, y + height / 2); + + ctx.restore(); + } + isAvailable(): boolean { return this.offscreenCanvas !== null && this.offscreenCtx !== null; } diff --git a/web/magnify-glass/OffscreenRenderer.js b/web/magnify-glass/OffscreenRenderer.js index 75bd4ac..6c484d4 100644 --- a/web/magnify-glass/OffscreenRenderer.js +++ b/web/magnify-glass/OffscreenRenderer.js @@ -186,7 +186,7 @@ const _OffscreenRenderer = class _OffscreenRenderer { node.widgets = node.widgets.filter((w) => { const wName = String(w.name || "").toLowerCase(); const wType = String(w.type || "").toLowerCase(); - const isPreview = wName.includes("preview") || wName.includes("image") || wName.includes("gallery") || wName.includes("upload") || wType.includes("preview") || wType.includes("image"); + const isPreview = wName.includes("preview") || wName.includes("gallery") || wType.includes("preview"); return !isPreview; }); } @@ -289,18 +289,18 @@ const _OffscreenRenderer = class _OffscreenRenderer { const computedHeight = widget.computedHeight || 0; const isMarkdownWidget = widgetType === "markdown"; const isMultiLineWidget = isMarkdownWidget || widgetType === "customtext" || (widgetType === "text" || widgetType === "textarea") && computedHeight >= 40; + const widgetCssX = nodeCssX + PADDING * scale; + const widgetCssY = nodeCssY + widgetLocalY * scale; + const widgetCssWidth = nodeCssWidth - PADDING * 2 * scale; + const canvasX = (widgetCssX - sourceCssX) * actualDpr * captureScale; + const canvasY = (widgetCssY - sourceCssY) * actualDpr * captureScale; + const widgetWidth = widgetCssWidth * actualDpr * captureScale; + const baseFontSize = 13; + const fontSize = Math.max(10, Math.min(28, baseFontSize * scale * actualDpr * captureScale)); + const lineHeight = fontSize * 1.4; if (isMultiLineWidget && widget.value !== void 0 && widget.value !== null) { const textValue = String(widget.value); if (textValue.length > 0) { - const widgetCssX = nodeCssX + PADDING * scale; - const widgetCssY = nodeCssY + widgetLocalY * scale; - const widgetCssWidth = nodeCssWidth - PADDING * 2 * scale; - const canvasX = (widgetCssX - sourceCssX) * actualDpr * captureScale; - const canvasY = (widgetCssY - sourceCssY) * actualDpr * captureScale; - const widgetWidth = widgetCssWidth * actualDpr * captureScale; - const baseFontSize = 13; - const fontSize = Math.max(10, Math.min(28, baseFontSize * scale * actualDpr * captureScale)); - const lineHeight = fontSize * 1.4; let contentHeight = 0; if (isMarkdownWidget) { ctx.save(); @@ -585,6 +585,27 @@ const _OffscreenRenderer = class _OffscreenRenderer { } } } + /** + * Render a button widget. + * Draws a rounded rectangle with the label centered. + */ + drawButton(ctx, label, x, y, width, height, fontSize) { + const radius = 6; + ctx.save(); + ctx.beginPath(); + ctx.roundRect(x, y, width, height, radius); + ctx.fillStyle = "#222"; + ctx.fill(); + ctx.strokeStyle = "#555"; + ctx.lineWidth = 1; + ctx.stroke(); + ctx.font = `600 ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = "#ccc"; + ctx.fillText(label, x + width / 2, y + height / 2); + ctx.restore(); + } isAvailable() { return this.offscreenCanvas !== null && this.offscreenCtx !== null; } diff --git a/web/magnify-glass/OffscreenRenderer.js.map b/web/magnify-glass/OffscreenRenderer.js.map index b1e2e3d..d80f4e4 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 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 const isPreview = wName.includes('preview') || wName.includes('image') || wName.includes('gallery') || wName.includes('upload') ||\n wType.includes('preview') || wType.includes('image');\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 // Restore original zoom state using setZoom for consistency\n if (typeof lgCanvas.setZoom === 'function') {\n lgCanvas.setZoom(origScale, [pivotCssX, pivotCssY]);\n } else {\n lgCanvas.ds.scale = origScale;\n lgCanvas.ds.offset[0] = origOffsetX;\n lgCanvas.ds.offset[1] = origOffsetY;\n }\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';\n const isMultiLineWidget = isMarkdownWidget || widgetType === 'customtext' ||\n ((widgetType === 'text' || widgetType === 'textarea') && computedHeight >= 40);\n\n if (isMultiLineWidget && widget.value !== undefined && widget.value !== null) {\n const textValue = String(widget.value);\n if (textValue.length > 0) {\n // Widget position in CSS pixels (relative to canvas)\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 // Position relative to source region, scaled to output size\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 based on scale\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 // Calculate required height based on content\n let contentHeight = 0;\n if (isMarkdownWidget) {\n // Rough estimation of height needed\n // We don't have ctx context here easily for precise measurement without setting font\n // So we do a generous estimation or try to measure\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 // reuse wrap logic estimation\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 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 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 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 // Restore original zoom state using setZoom for consistency\n if (typeof lgCanvas.setZoom === 'function') {\n lgCanvas.setZoom(origScale, [pivotCssX, pivotCssY]);\n } else {\n lgCanvas.ds.scale = origScale;\n lgCanvas.ds.offset[0] = origOffsetX;\n lgCanvas.ds.offset[1] = origOffsetY;\n }\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 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