fix: enable 3D thumbnail support for cloud environments

This commit is contained in:
Terry Jia
2026-03-16 17:02:07 -04:00
parent b3ebf1418a
commit ac21fd77cb
4 changed files with 72 additions and 23 deletions

View File

@@ -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(

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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,