Support Right Click -> Save for animated webp (#6095)

Allow for Right Click -> Save Image to work for the "SaveAnimatedWEBP"
node.

Fixing this revealed other existing issues:
- Attempting to resize the node causes runaway scaling
- Right clicking on the image directly causes a browser context menu
without a save option.

Significant rewriting has been performed to resolve both of these
issues.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6095-Support-Right-Click-Save-for-animated-webp-28e6d73d3650818e85a2ec58c38c2aae)
by [Unito](https://www.unito.io)
This commit is contained in:
AustinMroz
2025-10-16 18:15:23 -07:00
committed by GitHub
parent 984ebef416
commit 1234e1c56d
2 changed files with 33 additions and 32 deletions

View File

@@ -1,8 +1,7 @@
import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { IWidget } from '@/lib/litegraph/src/types/widgets' import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { ANIM_PREVIEW_WIDGET } from '@/scripts/app' import { ANIM_PREVIEW_WIDGET } from '@/scripts/app'
import { createImageHost } from '@/scripts/ui/imagePreview' import { isDOMWidget } from '@/scripts/domWidget'
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'
/** /**
* Composable for handling animated image previews in nodes * Composable for handling animated image previews in nodes
@@ -20,38 +19,38 @@ export function useNodeAnimatedImage() {
(w) => w.name === ANIM_PREVIEW_WIDGET (w) => w.name === ANIM_PREVIEW_WIDGET
) )
node.imgs[0].classList.value = 'block size-full object-contain'
if (widgetIdx > -1) { if (widgetIdx > -1) {
// Replace content in existing widget // Replace content in existing widget
const widget = node.widgets[widgetIdx] as IWidget & { const widget = node.widgets[widgetIdx]
options: { host: ReturnType<typeof createImageHost> } if (!isDOMWidget(widget)) return
} widget.element.replaceChildren(node.imgs[0])
widget.options.host.updateImages(node.imgs)
} else { } else {
// Create new widget // Create new widget
const host = createImageHost(node) const element = document.createElement('div')
// @ts-expect-error host is not a standard DOM widget option. element.appendChild(node.imgs[0])
const widget = node.addDOMWidget(ANIM_PREVIEW_WIDGET, 'img', host.el, { const widget = node.addDOMWidget(ANIM_PREVIEW_WIDGET, 'img', element, {
host,
// @ts-expect-error `getHeight` of image host returns void instead of number.
getHeight: host.getHeight,
onDraw: host.onDraw,
hideOnZoom: false hideOnZoom: false
}) as IWidget & { })
options: { host: ReturnType<typeof createImageHost> } node.overIndex = 0
}
// Add event listeners for canvas interactions
const { handleWheel, handlePointer, forwardEventToCanvas } =
useCanvasInteractions()
node.imgs[0].style.pointerEvents = 'none'
element.addEventListener('wheel', handleWheel)
element.addEventListener('pointermove', handlePointer)
element.addEventListener('pointerup', handlePointer)
element.addEventListener(
'pointerdown',
(e) => {
return e.button !== 2 ? handlePointer(e) : forwardEventToCanvas(e)
},
true
)
widget.serialize = false widget.serialize = false
widget.serializeValue = () => undefined widget.serializeValue = () => undefined
widget.options.host.updateImages(node.imgs)
widget.computeLayoutSize = () => {
const img = widget.options.host.getCurrentImage()
if (!img) return { minHeight: 0, minWidth: 0 }
return fitDimensionsToNodeWidth(
img.naturalWidth,
img.naturalHeight,
node.size?.[0] || 0
)
}
} }
} }

View File

@@ -1,3 +1,5 @@
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { app } from '../app' import { app } from '../app'
import { $el } from '../ui' import { $el } from '../ui'
@@ -48,8 +50,8 @@ export function calculateImageGrid(
return { cellWidth, cellHeight, cols, rows, shiftX } return { cellWidth, cellHeight, cols, rows, shiftX }
} }
// @ts-expect-error fixme ts strict error /** @knipIgnoreUnusedButUsedByCustomNodes */
export function createImageHost(node) { export function createImageHost(node: LGraphNode) {
const el = $el('div.comfy-img-preview') const el = $el('div.comfy-img-preview')
// @ts-expect-error fixme ts strict error // @ts-expect-error fixme ts strict error
let currentImgs let currentImgs
@@ -108,8 +110,8 @@ export function createImageHost(node) {
} }
el.replaceChildren(...imgs) el.replaceChildren(...imgs)
currentImgs = imgs currentImgs = imgs
node.onResize(node.size) node.onResize?.(node.size)
node.graph.setDirtyCanvas(true, true) node.graph?.setDirtyCanvas(true, true)
} }
}, },
getHeight() { getHeight() {