-
Notifications
You must be signed in to change notification settings - Fork 491
add thumbnail for 3d generation #8129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -754,6 +754,60 @@ class Load3d { | |
| this.forceRender() | ||
| } | ||
|
|
||
| public async captureThumbnail( | ||
| width: number = 256, | ||
| height: number = 256 | ||
| ): Promise<string> { | ||
| 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() | ||
|
Comment on lines
+772
to
+807
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep controls/view helper in sync when switching cameras.
🐛 Proposed fix (minimal side effects)- if (savedCameraType !== 'perspective') {
- this.cameraManager.toggleCamera('perspective')
- }
+ if (savedCameraType !== 'perspective') {
+ this.cameraManager.toggleCamera('perspective')
+ this.controlsManager.updateCamera(this.cameraManager.activeCamera)
+ this.viewHelperManager.recreateViewHelper()
+ }
@@
- if (savedCameraType !== 'perspective') {
- this.cameraManager.toggleCamera(savedCameraType)
- }
+ if (savedCameraType !== 'perspective') {
+ this.cameraManager.toggleCamera(savedCameraType)
+ this.controlsManager.updateCamera(this.cameraManager.activeCamera)
+ this.viewHelperManager.recreateViewHelper()
+ }🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| public remove(): void { | ||
| if (this.contextMenuAbortController) { | ||
| this.contextMenuAbortController.abort() | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent unhandled rejections from fire‑and‑forget thumbnail generation. Callers invoke this with 🐛 Proposed fix static async generateThumbnailIfNeeded(
load3d: Load3d,
modelPath: string,
folderType: 'input' | 'output'
): Promise<void> {
- 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
- )
+ try {
+ 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
+ )
+ } catch (error) {
+ console.warn('[Load3D] generateThumbnailIfNeeded failed:', error)
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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<boolean> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we re-use the helpers that already exist -- e.g., the one that is 100 lines above this
Or this
Not really for this PR, but after looking, we seem to have a lot of duplication across the codebase for the upload endpoint usage:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai make an Issue to centralize all the upload helpers listed above into a singlar shared util or service that these modules then import from. Allows easier refactoring later whenever we adjust the upload logic or parameters.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain
🏁 Script executed: Length of output: 57
🧬 Code Graph Analysis ResultsFile: src/scripts/api.ts (lines 1304-1304)File: src/scripts/app.ts (lines 1746-1746)File: src/platform/updates/common/toastStore.ts (lines 8-39)File: src/stores/queueStore.ts (lines 83-85)🧠 Learnings used |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imageData: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subfolder: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filename: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: string = 'input' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<boolean> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
jtydhr88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default Load3dUtils | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,35 @@ | ||
| <template> | ||
| <div class="relative size-full overflow-hidden rounded"> | ||
| <img | ||
| v-if="!thumbnailError" | ||
| :src="thumbnailSrc" | ||
| :alt="asset?.name" | ||
| class="size-full object-contain transition-transform duration-300 group-hover:scale-105 group-data-[selected=true]:scale-105" | ||
| @error="thumbnailError = true" | ||
| /> | ||
| <div | ||
| v-else | ||
| class="flex size-full flex-col items-center justify-center gap-2 bg-modal-card-placeholder-background transition-transform duration-300 group-hover:scale-105 group-data-[selected=true]:scale-105" | ||
| > | ||
| <i class="icon-[lucide--box] text-3xl text-muted-foreground" /> | ||
| <span class="text-sm text-base-foreground">{{ | ||
| $t('assetBrowser.media.threeDModelPlaceholder') | ||
| }}</span> | ||
| <span class="text-sm text-base-foreground"> | ||
| {{ $t('assetBrowser.media.threeDModelPlaceholder') }} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import { computed, ref } from 'vue' | ||
|
|
||
| import type { AssetMeta } from '../schemas/mediaAssetSchema' | ||
|
|
||
| const { asset } = defineProps<{ asset: AssetMeta }>() | ||
|
|
||
| const thumbnailError = ref(false) | ||
|
|
||
| const thumbnailSrc = computed(() => { | ||
| if (!asset?.src) return '' | ||
| return asset.src.replace(/([?&]filename=)([^&]*)/, '$1$2.png') | ||
| }) | ||
jtydhr88 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </script> | ||
Uh oh!
There was an error while loading. Please reload this page.