diff --git a/src/composables/useLoad3d.ts b/src/composables/useLoad3d.ts index c8312051a..b07daba2e 100644 --- a/src/composables/useLoad3d.ts +++ b/src/composables/useLoad3d.ts @@ -511,6 +511,22 @@ export const useLoad3d = (nodeOrRef: MaybeRef) => { hasSkeleton.value = load3d?.hasSkeleton() ?? false // Reset skeleton visibility when loading new model modelConfig.value.showSkeleton = false + + if (load3d) { + const node = nodeRef.value + + const modelWidget = node?.widgets?.find( + (w) => w.name === 'model_file' || w.name === 'image' + ) + const value = modelWidget?.value + if (typeof value === 'string') { + void Load3dUtils.generateThumbnailIfNeeded( + load3d, + value, + isPreview.value ? 'output' : 'input' + ) + } + } }, skeletonVisibilityChange: (value: boolean) => { modelConfig.value.showSkeleton = value diff --git a/src/extensions/core/load3d/Load3d.ts b/src/extensions/core/load3d/Load3d.ts index 657a1796b..60690e4f8 100644 --- a/src/extensions/core/load3d/Load3d.ts +++ b/src/extensions/core/load3d/Load3d.ts @@ -754,6 +754,60 @@ class Load3d { this.forceRender() } + public async captureThumbnail( + width: number = 256, + height: number = 256 + ): Promise { + if (!this.modelManager.currentModel) { + throw new Error('No model loaded for thumbnail capture') + } + + const savedState = this.cameraManager.getCameraState() + const savedCameraType = this.cameraManager.getCurrentCameraType() + const savedGridVisible = this.sceneManager.gridHelper.visible + + try { + this.sceneManager.gridHelper.visible = false + + if (savedCameraType !== 'perspective') { + this.cameraManager.toggleCamera('perspective') + } + + const box = new THREE.Box3().setFromObject(this.modelManager.currentModel) + const size = box.getSize(new THREE.Vector3()) + const center = box.getCenter(new THREE.Vector3()) + + const maxDim = Math.max(size.x, size.y, size.z) + const distance = maxDim * 1.5 + + const cameraPosition = new THREE.Vector3( + center.x - distance * 0.8, + center.y + distance * 0.4, + center.z + distance * 0.3 + ) + + this.cameraManager.perspectiveCamera.position.copy(cameraPosition) + this.cameraManager.perspectiveCamera.lookAt(center) + this.cameraManager.perspectiveCamera.updateProjectionMatrix() + + if (this.controlsManager.controls) { + this.controlsManager.controls.target.copy(center) + this.controlsManager.controls.update() + } + + const result = await this.sceneManager.captureScene(width, height) + return result.scene + } finally { + this.sceneManager.gridHelper.visible = savedGridVisible + + if (savedCameraType !== 'perspective') { + this.cameraManager.toggleCamera(savedCameraType) + } + this.cameraManager.setCameraState(savedState) + this.controlsManager.controls?.update() + } + } + public remove(): void { if (this.contextMenuAbortController) { this.contextMenuAbortController.abort() diff --git a/src/extensions/core/load3d/Load3dUtils.ts b/src/extensions/core/load3d/Load3dUtils.ts index 13095ac96..ba7c36e55 100644 --- a/src/extensions/core/load3d/Load3dUtils.ts +++ b/src/extensions/core/load3d/Load3dUtils.ts @@ -1,9 +1,34 @@ +import type Load3d from '@/extensions/core/load3d/Load3d' import { t } from '@/i18n' import { useToastStore } from '@/platform/updates/common/toastStore' import { api } from '@/scripts/api' import { app } from '@/scripts/app' class Load3dUtils { + static async generateThumbnailIfNeeded( + load3d: Load3d, + modelPath: string, + folderType: 'input' | 'output' + ): Promise { + const [subfolder, filename] = this.splitFilePath(modelPath) + const thumbnailFilename = this.getThumbnailFilename(filename) + + const exists = await this.fileExists( + subfolder, + thumbnailFilename, + folderType + ) + if (exists) return + + const imageData = await load3d.captureThumbnail(256, 256) + await this.uploadThumbnail( + imageData, + subfolder, + thumbnailFilename, + folderType + ) + } + static async uploadTempImage( imageData: string, prefix: string, @@ -122,6 +147,46 @@ class Load3dUtils { await Promise.all(uploadPromises) } + + static getThumbnailFilename(modelFilename: string): string { + return `${modelFilename}.png` + } + + static async fileExists( + subfolder: string, + filename: string, + type: string = 'input' + ): Promise { + try { + const url = api.apiURL(this.getResourceURL(subfolder, filename, type)) + const response = await fetch(url, { method: 'HEAD' }) + return response.ok + } catch { + return false + } + } + + static async uploadThumbnail( + imageData: string, + subfolder: string, + filename: string, + type: string = 'input' + ): Promise { + const blob = await fetch(imageData).then((r) => r.blob()) + const file = new File([blob], filename, { type: 'image/png' }) + + const body = new FormData() + body.append('image', file) + body.append('subfolder', subfolder) + body.append('type', type) + + const resp = await api.fetchApi('/upload/image', { + method: 'POST', + body + }) + + return resp.status === 200 + } } export default Load3dUtils diff --git a/src/extensions/core/saveMesh.ts b/src/extensions/core/saveMesh.ts index ae94a8609..947120467 100644 --- a/src/extensions/core/saveMesh.ts +++ b/src/extensions/core/saveMesh.ts @@ -4,6 +4,7 @@ import Load3D from '@/components/load3d/Load3D.vue' import { useLoad3d } from '@/composables/useLoad3d' import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper' import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration' +import Load3dUtils from '@/extensions/core/load3d/Load3dUtils' import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces' import type { NodeOutputWith, ResultItem } from '@/schemas/apiSchema' @@ -94,6 +95,17 @@ useExtensionService().registerExtension({ const config = new Load3DConfiguration(load3d, node.properties) const loadFolder = fileInfo.type as 'input' | 'output' + + const onModelLoaded = () => { + load3d.removeEventListener('modelLoadingEnd', onModelLoaded) + void Load3dUtils.generateThumbnailIfNeeded( + load3d, + filePath, + loadFolder + ) + } + load3d.addEventListener('modelLoadingEnd', onModelLoaded) + config.configureForSaveMesh(loadFolder, filePath) } }) diff --git a/src/platform/assets/components/Media3DTop.vue b/src/platform/assets/components/Media3DTop.vue index a4cc141db..b5b7f0c60 100644 --- a/src/platform/assets/components/Media3DTop.vue +++ b/src/platform/assets/components/Media3DTop.vue @@ -1,12 +1,35 @@ + +