mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 15:40:10 +00:00
Adds the framework for a system to automate display of a curated list of recommended widgets to the node. As part of this, a return to display of "image previews" was made. This code is causing lots of problems. Much of the logic is dependent upon the actual node going through the draw loop. As nodes in the subgraph don't receive redraws, there's lots of issues with managing the initial display and ensuring that an initial draw occurs. This commit includes support for updating previews, but is more brittle than I would like.
281 lines
8.3 KiB
TypeScript
281 lines
8.3 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
|
|
import { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import {
|
|
ExecutedWsMessage,
|
|
ResultItem,
|
|
ResultItemType
|
|
} from '@/schemas/apiSchema'
|
|
import { api } from '@/scripts/api'
|
|
import { app } from '@/scripts/app'
|
|
import { useExecutionStore } from '@/stores/executionStore'
|
|
import { useWorkflowStore } from '@/stores/workflowStore'
|
|
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
|
import { parseFilePath } from '@/utils/formatUtil'
|
|
import { isVideoNode } from '@/utils/litegraphUtil'
|
|
|
|
const createOutputs = (
|
|
filenames: string[],
|
|
type: ResultItemType,
|
|
isAnimated: boolean
|
|
): ExecutedWsMessage['output'] => {
|
|
return {
|
|
images: filenames.map((image) => ({ type, ...parseFilePath(image) })),
|
|
animated: filenames.map(
|
|
(image) =>
|
|
isAnimated && (image.endsWith('.webp') || image.endsWith('.png'))
|
|
)
|
|
}
|
|
}
|
|
|
|
interface SetOutputOptions {
|
|
merge?: boolean
|
|
}
|
|
|
|
export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
|
const { nodeIdToNodeLocatorId, nodeToNodeLocatorId } = useWorkflowStore()
|
|
const { executionIdToNodeLocatorId } = useExecutionStore()
|
|
|
|
function getNodeOutputs(
|
|
node: LGraphNode
|
|
): ExecutedWsMessage['output'] | undefined {
|
|
return app.nodeOutputs[nodeToNodeLocatorId(node)]
|
|
}
|
|
|
|
function getNodePreviews(node: LGraphNode): string[] | undefined {
|
|
return app.nodePreviewImages[nodeToNodeLocatorId(node)]
|
|
}
|
|
|
|
/**
|
|
* Check if a node's outputs includes images that should/can be loaded normally
|
|
* by PIL.
|
|
*/
|
|
const isImageOutputs = (
|
|
node: LGraphNode,
|
|
outputs: ExecutedWsMessage['output']
|
|
): boolean => {
|
|
// If animated webp/png or video outputs, return false
|
|
if (node.animatedImages || isVideoNode(node)) return false
|
|
|
|
// If no images, return false
|
|
if (!outputs?.images?.length) return false
|
|
|
|
// If svg images, return false
|
|
if (outputs.images.some((image) => image.filename?.endsWith('svg')))
|
|
return false
|
|
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Get the preview param for the node's outputs.
|
|
*
|
|
* If the output is an image, use the user's preferred format (from settings).
|
|
* For non-image outputs, return an empty string, as including the preview param
|
|
* will force the server to load the output file as an image.
|
|
*/
|
|
function getPreviewParam(
|
|
node: LGraphNode,
|
|
outputs: ExecutedWsMessage['output']
|
|
): string {
|
|
return isImageOutputs(node, outputs) ? app.getPreviewFormatParam() : ''
|
|
}
|
|
|
|
function getNodeImageUrls(node: LGraphNode): string[] | undefined {
|
|
const previews = getNodePreviews(node)
|
|
if (previews?.length) return previews
|
|
|
|
const outputs = getNodeOutputs(node)
|
|
if (!outputs?.images?.length) return
|
|
|
|
const rand = app.getRandParam()
|
|
const previewParam = getPreviewParam(node, outputs)
|
|
|
|
return outputs.images.map((image) => {
|
|
const imgUrlPart = new URLSearchParams(image)
|
|
return api.apiURL(`/view?${imgUrlPart}${previewParam}${rand}`)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Internal function to set outputs by NodeLocatorId.
|
|
* Handles the merge logic when needed.
|
|
*/
|
|
function setOutputsByLocatorId(
|
|
nodeLocatorId: NodeLocatorId,
|
|
outputs: ExecutedWsMessage['output'] | ResultItem,
|
|
options: SetOutputOptions = {}
|
|
) {
|
|
if (options.merge) {
|
|
const existingOutput = app.nodeOutputs[nodeLocatorId]
|
|
if (existingOutput && outputs) {
|
|
for (const k in outputs) {
|
|
const existingValue = existingOutput[k]
|
|
const newValue = (outputs as Record<NodeLocatorId, any>)[k]
|
|
|
|
if (Array.isArray(existingValue) && Array.isArray(newValue)) {
|
|
existingOutput[k] = existingValue.concat(newValue)
|
|
} else {
|
|
existingOutput[k] = newValue
|
|
}
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
app.nodeOutputs[nodeLocatorId] = outputs
|
|
}
|
|
|
|
function setNodeOutputs(
|
|
node: LGraphNode,
|
|
filenames: string | string[] | ResultItem,
|
|
{
|
|
folder = 'input',
|
|
isAnimated = false
|
|
}: { folder?: ResultItemType; isAnimated?: boolean } = {}
|
|
) {
|
|
if (!filenames || !node) return
|
|
|
|
const locatorId = nodeToNodeLocatorId(node)
|
|
if (!locatorId) return
|
|
if (typeof filenames === 'string') {
|
|
setOutputsByLocatorId(
|
|
locatorId,
|
|
createOutputs([filenames], folder, isAnimated)
|
|
)
|
|
} else if (!Array.isArray(filenames)) {
|
|
setOutputsByLocatorId(locatorId, filenames)
|
|
} else {
|
|
const resultItems = createOutputs(filenames, folder, isAnimated)
|
|
if (!resultItems?.images?.length) return
|
|
setOutputsByLocatorId(locatorId, resultItems)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set node outputs by execution ID (hierarchical ID from backend).
|
|
* Converts the execution ID to a NodeLocatorId before storing.
|
|
*
|
|
* @param executionId - The execution ID (e.g., "123:456:789" or "789")
|
|
* @param outputs - The outputs to store
|
|
* @param options - Options for setting outputs
|
|
* @param options.merge - If true, merge with existing outputs (arrays are concatenated)
|
|
*/
|
|
function setNodeOutputsByExecutionId(
|
|
executionId: string,
|
|
outputs: ExecutedWsMessage['output'] | ResultItem,
|
|
options: SetOutputOptions = {}
|
|
) {
|
|
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
|
|
if (!nodeLocatorId) return
|
|
|
|
setOutputsByLocatorId(nodeLocatorId, outputs, options)
|
|
}
|
|
|
|
/**
|
|
* Set node preview images by execution ID (hierarchical ID from backend).
|
|
* Converts the execution ID to a NodeLocatorId before storing.
|
|
*
|
|
* @param executionId - The execution ID (e.g., "123:456:789" or "789")
|
|
* @param previewImages - Array of preview image URLs to store
|
|
*/
|
|
function setNodePreviewsByExecutionId(
|
|
executionId: string,
|
|
previewImages: string[]
|
|
) {
|
|
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
|
|
if (!nodeLocatorId) return
|
|
|
|
app.nodePreviewImages[nodeLocatorId] = previewImages
|
|
}
|
|
|
|
/**
|
|
* Set node preview images by node ID.
|
|
* Uses the current graph context to create the appropriate NodeLocatorId.
|
|
*
|
|
* @param nodeId - The node ID
|
|
* @param previewImages - Array of preview image URLs to store
|
|
*/
|
|
function setNodePreviewsByNodeId(
|
|
nodeId: string | number,
|
|
previewImages: string[]
|
|
) {
|
|
const nodeLocatorId = nodeIdToNodeLocatorId(nodeId)
|
|
app.nodePreviewImages[nodeLocatorId] = previewImages
|
|
}
|
|
|
|
/**
|
|
* Revoke preview images by execution ID.
|
|
* Frees memory allocated to image preview blobs by revoking the URLs.
|
|
*
|
|
* @param executionId - The execution ID
|
|
*/
|
|
function revokePreviewsByExecutionId(executionId: string) {
|
|
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
|
|
if (!nodeLocatorId) return
|
|
|
|
revokePreviewsByLocatorId(nodeLocatorId)
|
|
}
|
|
|
|
/**
|
|
* Revoke preview images by node locator ID.
|
|
* Frees memory allocated to image preview blobs by revoking the URLs.
|
|
*
|
|
* @param nodeLocatorId - The node locator ID
|
|
*/
|
|
function revokePreviewsByLocatorId(nodeLocatorId: NodeLocatorId) {
|
|
const previews = app.nodePreviewImages[nodeLocatorId]
|
|
if (!previews?.[Symbol.iterator]) return
|
|
|
|
for (const url of previews) {
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
|
|
delete app.nodePreviewImages[nodeLocatorId]
|
|
}
|
|
|
|
/**
|
|
* Revoke all preview images.
|
|
* Frees memory allocated to all image preview blobs.
|
|
*/
|
|
function revokeAllPreviews() {
|
|
for (const nodeLocatorId of Object.keys(app.nodePreviewImages)) {
|
|
const previews = app.nodePreviewImages[nodeLocatorId]
|
|
if (!previews?.[Symbol.iterator]) continue
|
|
|
|
for (const url of previews) {
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
}
|
|
app.nodePreviewImages = {}
|
|
}
|
|
|
|
/**
|
|
* Revoke all preview of a subgraph node and the graph it contains.
|
|
* Does not recurse to contents of nested subgraphs.
|
|
*/
|
|
function revokeSubgraphPreviews(subgraphNode: SubgraphNode) {
|
|
const graphId = subgraphNode.graph.isRootGraph
|
|
? ''
|
|
: subgraphNode.graph.id + ':'
|
|
revokePreviewsByLocatorId(graphId + subgraphNode.id)
|
|
for (const node of subgraphNode.subgraph.nodes) {
|
|
revokePreviewsByLocatorId(subgraphNode.subgraph.id + node.id)
|
|
}
|
|
}
|
|
|
|
return {
|
|
getNodeOutputs,
|
|
getNodeImageUrls,
|
|
getNodePreviews,
|
|
setNodeOutputs,
|
|
setNodeOutputsByExecutionId,
|
|
setNodePreviewsByExecutionId,
|
|
setNodePreviewsByNodeId,
|
|
revokePreviewsByExecutionId,
|
|
revokeAllPreviews,
|
|
revokeSubgraphPreviews,
|
|
getPreviewParam
|
|
}
|
|
})
|