mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
fix: enable 3D thumbnail support for cloud environments
This commit is contained in:
@@ -5,7 +5,10 @@ import { nextTick, ref, toRaw, watch } from 'vue'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { persistThumbnail } from '@/platform/assets/utils/assetPreviewUtil'
|
||||
import {
|
||||
isAssetPreviewSupported,
|
||||
persistThumbnail
|
||||
} from '@/platform/assets/utils/assetPreviewUtil'
|
||||
import type {
|
||||
AnimationItem,
|
||||
CameraConfig,
|
||||
@@ -515,7 +518,7 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
// Reset skeleton visibility when loading new model
|
||||
modelConfig.value.showSkeleton = false
|
||||
|
||||
if (load3d && api.getServerFeature('assets', false)) {
|
||||
if (load3d && isAssetPreviewSupported()) {
|
||||
const node = nodeRef.value
|
||||
|
||||
const modelWidget = node?.widgets?.find(
|
||||
|
||||
@@ -7,14 +7,16 @@ import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
||||
import type { NodeOutputWith, ResultItem } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
|
||||
type SaveMeshOutput = NodeOutputWith<{
|
||||
'3d'?: ResultItem[]
|
||||
}>
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { persistThumbnail } from '@/platform/assets/utils/assetPreviewUtil'
|
||||
import {
|
||||
isAssetPreviewSupported,
|
||||
persistThumbnail
|
||||
} from '@/platform/assets/utils/assetPreviewUtil'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
@@ -103,14 +105,15 @@ useExtensionService().registerExtension({
|
||||
|
||||
config.configureForSaveMesh(loadFolder, filePath)
|
||||
|
||||
if (api.getServerFeature('assets', false)) {
|
||||
if (isAssetPreviewSupported()) {
|
||||
const filename = fileInfo.filename ?? ''
|
||||
const displayName = fileInfo.display_name
|
||||
const onModelLoaded = () => {
|
||||
load3d.removeEventListener('modelLoadingEnd', onModelLoaded)
|
||||
load3d
|
||||
.captureThumbnail(256, 256)
|
||||
.then((dataUrl) => fetch(dataUrl).then((r) => r.blob()))
|
||||
.then((blob) => persistThumbnail(filename, blob))
|
||||
.then((blob) => persistThumbnail(filename, blob, displayName))
|
||||
.catch(() => {})
|
||||
}
|
||||
load3d.addEventListener('modelLoadingEnd', onModelLoaded)
|
||||
|
||||
@@ -22,10 +22,11 @@
|
||||
import { useIntersectionObserver } from '@vueuse/core'
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import { findServerPreviewUrl } from '../utils/assetPreviewUtil'
|
||||
import {
|
||||
findServerPreviewUrl,
|
||||
isAssetPreviewSupported
|
||||
} from '../utils/assetPreviewUtil'
|
||||
|
||||
const { asset } = defineProps<{ asset: AssetMeta }>()
|
||||
|
||||
@@ -48,8 +49,11 @@ async function loadThumbnail() {
|
||||
|
||||
if (!asset?.src) return
|
||||
|
||||
if (asset.name && api.getServerFeature('assets', false)) {
|
||||
const serverPreviewUrl = await findServerPreviewUrl(asset.name)
|
||||
if (asset.name && isAssetPreviewSupported()) {
|
||||
const serverPreviewUrl = await findServerPreviewUrl(
|
||||
asset.name,
|
||||
asset.display_name
|
||||
)
|
||||
if (serverPreviewUrl) {
|
||||
thumbnailSrc.value = serverPreviewUrl
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
import { assetService } from '../services/assetService'
|
||||
|
||||
interface AssetRecord {
|
||||
id: string
|
||||
name: string
|
||||
@@ -9,6 +8,21 @@ interface AssetRecord {
|
||||
preview_id?: string
|
||||
}
|
||||
|
||||
export function isAssetPreviewSupported(): boolean {
|
||||
return (
|
||||
assetService.isAssetAPIEnabled() || api.getServerFeature('assets', false)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the filename portion from a path that may include subdirectories.
|
||||
* e.g. "mesh/ComfyUI_00003_.glb" -> "ComfyUI_00003_.glb"
|
||||
*/
|
||||
function basename(path: string): string {
|
||||
const i = path.lastIndexOf('/')
|
||||
return i >= 0 ? path.slice(i + 1) : path
|
||||
}
|
||||
|
||||
async function fetchAssetsByName(name: string): Promise<AssetRecord[]> {
|
||||
const params = new URLSearchParams({ name_contains: name })
|
||||
const res = await api.fetchApi(`/assets?${params}`)
|
||||
@@ -17,19 +31,44 @@ async function fetchAssetsByName(name: string): Promise<AssetRecord[]> {
|
||||
return data.assets ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for the model asset record by name.
|
||||
* On local, `name` (the raw filename) matches directly.
|
||||
* On cloud, `name` is a hash; fall back to `displayName` (original filename)
|
||||
* which may include a subfolder prefix.
|
||||
*/
|
||||
async function findModelAsset(
|
||||
name: string,
|
||||
displayName?: string
|
||||
): Promise<AssetRecord | undefined> {
|
||||
let assets = await fetchAssetsByName(name)
|
||||
let modelAsset = assets.find((a) => a.name === name)
|
||||
|
||||
if (!modelAsset && displayName) {
|
||||
const base = basename(displayName)
|
||||
assets = await fetchAssetsByName(base)
|
||||
modelAsset =
|
||||
assets.find((a) => a.name === base) ??
|
||||
assets.find((a) => a.name === displayName)
|
||||
}
|
||||
|
||||
return modelAsset
|
||||
}
|
||||
|
||||
export async function findServerPreviewUrl(
|
||||
name: string
|
||||
name: string,
|
||||
displayName?: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const assets = await fetchAssetsByName(name)
|
||||
const modelAsset = await findModelAsset(name, displayName)
|
||||
|
||||
const modelAsset = assets.find((a) => a.name === name)
|
||||
if (!modelAsset?.preview_id) return null
|
||||
|
||||
const previewAsset = assets.find((a) => a.id === modelAsset.preview_id)
|
||||
if (!previewAsset?.preview_url) return null
|
||||
// Local API computes preview_url from the linked preview asset
|
||||
if (modelAsset.preview_url) return api.api_base + modelAsset.preview_url
|
||||
|
||||
return api.api_base + previewAsset.preview_url
|
||||
// Cloud list API may omit preview_url; construct from preview_id directly
|
||||
return api.apiURL(`/assets/${modelAsset.preview_id}/content`)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
@@ -37,14 +76,14 @@ export async function findServerPreviewUrl(
|
||||
|
||||
export async function persistThumbnail(
|
||||
modelName: string,
|
||||
blob: Blob
|
||||
blob: Blob,
|
||||
displayName?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const assets = await fetchAssetsByName(modelName)
|
||||
const modelAsset = assets.find((a) => a.name === modelName)
|
||||
const modelAsset = await findModelAsset(modelName, displayName)
|
||||
if (!modelAsset || modelAsset.preview_id) return
|
||||
|
||||
const previewFilename = `${modelName}_preview.png`
|
||||
const previewFilename = `${modelAsset.name}_preview.png`
|
||||
const uploaded = await assetService.uploadAssetFromBase64({
|
||||
data: await blobToDataUrl(blob),
|
||||
name: previewFilename,
|
||||
|
||||
Reference in New Issue
Block a user