diff --git a/src/composables/node/useNodeCanvasImagePreview.ts b/src/composables/node/useNodeCanvasImagePreview.ts index e0e4ded564..dccfc7f430 100644 --- a/src/composables/node/useNodeCanvasImagePreview.ts +++ b/src/composables/node/useNodeCanvasImagePreview.ts @@ -2,7 +2,11 @@ import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import { useImagePreviewWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget' export const CANVAS_IMAGE_PREVIEW_WIDGET = '$$canvas-image-preview' -const CANVAS_IMAGE_PREVIEW_NODE_TYPES = new Set(['PreviewImage', 'SaveImage']) +const CANVAS_IMAGE_PREVIEW_NODE_TYPES = new Set([ + 'PreviewImage', + 'SaveImage', + 'GLSLShader' +]) export function supportsVirtualCanvasImagePreview(node: LGraphNode): boolean { return CANVAS_IMAGE_PREVIEW_NODE_TYPES.has(node.type) diff --git a/src/core/graph/subgraph/promotedWidgetView.ts b/src/core/graph/subgraph/promotedWidgetView.ts index 5ec6077849..aae35d5b0a 100644 --- a/src/core/graph/subgraph/promotedWidgetView.ts +++ b/src/core/graph/subgraph/promotedWidgetView.ts @@ -201,7 +201,8 @@ class PromotedWidgetView implements IPromotedWidgetView { projected.drawWidget(ctx, { width: widgetWidth, showText: !lowQuality, - suppressPromotedOutline: true + suppressPromotedOutline: true, + previewImages: resolved.node.imgs }) projected.y = originalY diff --git a/src/core/graph/subgraph/promotionUtils.test.ts b/src/core/graph/subgraph/promotionUtils.test.ts index 9b321de895..ff96cc98bc 100644 --- a/src/core/graph/subgraph/promotionUtils.test.ts +++ b/src/core/graph/subgraph/promotionUtils.test.ts @@ -184,6 +184,17 @@ describe('getPromotableWidgets', () => { ).toBe(true) }) + it('adds virtual canvas preview widget for GLSLShader nodes', () => { + const node = new LGraphNode('GLSLShader') + node.type = 'GLSLShader' + + const widgets = getPromotableWidgets(node) + + expect( + widgets.some((widget) => widget.name === CANVAS_IMAGE_PREVIEW_WIDGET) + ).toBe(true) + }) + it('does not add virtual canvas preview widget for non-image nodes', () => { const node = new LGraphNode('TextNode') node.addOutput('TEXT', 'STRING') @@ -232,4 +243,25 @@ describe('promoteRecommendedWidgets', () => { expect(updatePreviewsMock).not.toHaveBeenCalled() }) + + it('eagerly promotes virtual preview widget for CANVAS_IMAGE_PREVIEW nodes', () => { + const subgraph = createTestSubgraph() + const subgraphNode = createTestSubgraphNode(subgraph) + const glslNode = new LGraphNode('GLSLShader') + glslNode.type = 'GLSLShader' + subgraph.add(glslNode) + + promoteRecommendedWidgets(subgraphNode) + + const store = usePromotionStore() + expect( + store.isPromoted( + subgraphNode.rootGraph.id, + subgraphNode.id, + String(glslNode.id), + CANVAS_IMAGE_PREVIEW_WIDGET + ) + ).toBe(true) + expect(updatePreviewsMock).not.toHaveBeenCalled() + }) }) diff --git a/src/core/graph/subgraph/promotionUtils.ts b/src/core/graph/subgraph/promotionUtils.ts index 8fa048b2e1..532c49920b 100644 --- a/src/core/graph/subgraph/promotionUtils.ts +++ b/src/core/graph/subgraph/promotionUtils.ts @@ -227,6 +227,29 @@ export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) { // defer. Core $$ preview widgets are the lazy path that needs updatePreviews. if (hasPreviewWidget()) continue + // Nodes in CANVAS_IMAGE_PREVIEW_NODE_TYPES support a virtual $$ + // preview widget. Eagerly promote it so getPseudoWidgetPreviewTargets + // includes this node and onDrawBackground can call updatePreviews on it + // once execution outputs arrive. + if (supportsVirtualCanvasImagePreview(node)) { + if ( + !store.isPromoted( + subgraphNode.rootGraph.id, + subgraphNode.id, + String(node.id), + CANVAS_IMAGE_PREVIEW_WIDGET + ) + ) { + store.promote( + subgraphNode.rootGraph.id, + subgraphNode.id, + String(node.id), + CANVAS_IMAGE_PREVIEW_WIDGET + ) + } + continue + } + // Also schedule a deferred check: core $$ widgets are created lazily by // updatePreviews when node outputs are first loaded. requestAnimationFrame(() => updatePreviews(node, promotePreviewWidget)) diff --git a/src/lib/litegraph/src/widgets/BaseWidget.ts b/src/lib/litegraph/src/widgets/BaseWidget.ts index 973e7976da..b8138af707 100644 --- a/src/lib/litegraph/src/widgets/BaseWidget.ts +++ b/src/lib/litegraph/src/widgets/BaseWidget.ts @@ -27,6 +27,8 @@ export interface DrawWidgetOptions { showText?: boolean /** When true, suppresses the promoted outline color (e.g. for projected copies on SubgraphNode). */ suppressPromotedOutline?: boolean + /** Transient image source for preview widgets rendered on behalf of another node (e.g. subgraph promotion). */ + previewImages?: HTMLImageElement[] } interface DrawTruncatingTextOptions extends DrawWidgetOptions { diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget.ts b/src/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget.ts index a76a3cb0f6..bf7fd1064a 100644 --- a/src/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget.ts +++ b/src/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget.ts @@ -4,6 +4,7 @@ import type { IBaseWidget, IWidgetOptions } from '@/lib/litegraph/src/types/widgets' +import type { DrawWidgetOptions } from '@/lib/litegraph/src/widgets/BaseWidget' import { useSettingStore } from '@/platform/settings/settingStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' @@ -77,7 +78,9 @@ const renderPreview = ( ctx: CanvasRenderingContext2D, node: LGraphNode, shiftY: number, - computedHeight: number | undefined + computedHeight: number | undefined, + imgs: HTMLImageElement[], + width: number ) => { if (!node.size) return @@ -99,7 +102,6 @@ const renderPreview = ( node.pointerDown = null } - const imgs = node.imgs ?? [] if (imgs.length === 0) return let { imageIndex } = node @@ -112,7 +114,7 @@ const renderPreview = ( const settingStore = useSettingStore() const allowImageSizeDraw = settingStore.get('Comfy.Node.AllowImageSizeDraw') const IMAGE_TEXT_SIZE_TEXT_HEIGHT = allowImageSizeDraw ? 15 : 0 - const dw = node.size[0] + const dw = width const dh = computedHeight ? computedHeight - IMAGE_TEXT_SIZE_TEXT_HEIGHT : 0 if (imageIndex == null) { @@ -358,8 +360,29 @@ class ImagePreviewWidget extends BaseWidget { this.serialize = false } - override drawWidget(ctx: CanvasRenderingContext2D): void { - renderPreview(ctx, this.node, this.y, this.computedHeight) + override drawWidget( + ctx: CanvasRenderingContext2D, + options: DrawWidgetOptions + ): void { + const imgs = options.previewImages ?? this.node.imgs ?? [] + renderPreview( + ctx, + this.node, + this.y, + this.computedHeight, + imgs, + options.width + ) + } + + override createCopyForNode(node: LGraphNode): this { + const copy = new ImagePreviewWidget( + node, + this.name, + this.options as IWidgetOptions + ) as this + copy.value = this.value + return copy } override onPointerDown(pointer: CanvasPointer, node: LGraphNode): boolean {