diff --git a/package.json b/package.json index c7efeda67..04ad90613 100644 --- a/package.json +++ b/package.json @@ -187,6 +187,7 @@ "vue-i18n": "catalog:", "vue-router": "catalog:", "vuefire": "catalog:", + "wwobjloader2": "catalog:", "yjs": "catalog:", "zod": "catalog:", "zod-validation-error": "catalog:" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d253e7e73..6eea18564 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -309,6 +309,9 @@ catalogs: vuefire: specifier: ^3.2.1 version: 3.2.1 + wwobjloader2: + specifier: ^6.2.1 + version: 6.2.1 yjs: specifier: ^13.6.27 version: 13.6.27 @@ -497,6 +500,9 @@ importers: vuefire: specifier: 'catalog:' version: 3.2.1(consola@3.4.2)(firebase@11.6.0)(vue@3.5.13(typescript@5.9.3)) + wwobjloader2: + specifier: 'catalog:' + version: 6.2.1(three@0.170.0) yjs: specifier: 'catalog:' version: 13.6.27 @@ -8225,6 +8231,19 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} + wtd-core@3.0.0: + resolution: {integrity: sha512-LSPfAQ5ULSV5vPhipcjdQvV5xOx25QesYK23jwUOF99xOx6fuulk7CMQerERRwA4uoQooNmRd8AT6IPBwORlWQ==} + + wtd-three-ext@3.0.0: + resolution: {integrity: sha512-PLZJipCAiinot8D1uB4A7+XHxPAYeZXDhczbbazK7pKdqpE77zMizQH4rSZsaNbzktgnIfpgK/ODqhJTdrUjUw==} + peerDependencies: + three: '>= 0.137.5 < 1' + + wwobjloader2@6.2.1: + resolution: {integrity: sha512-/v/sfUX0PMQAI8souzCs6xsO9LR3RyL+ujnOiS/1pngUlakKyHYC5XMQvu77pTeWzY3rzNyt5Q/bg5O3RukA+g==} + peerDependencies: + three: '>= 0.137.5 < 1' + xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} @@ -17125,6 +17144,19 @@ snapshots: dependencies: is-wsl: 3.1.0 + wtd-core@3.0.0: {} + + wtd-three-ext@3.0.0(three@0.170.0): + dependencies: + three: 0.170.0 + wtd-core: 3.0.0 + + wwobjloader2@6.2.1(three@0.170.0): + dependencies: + three: 0.170.0 + wtd-core: 3.0.0 + wtd-three-ext: 3.0.0(three@0.170.0) + xdg-basedir@5.1.0: {} xml-name-validator@4.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9f4500f3a..bd4638b0d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -104,6 +104,7 @@ catalog: vue-router: ^4.4.3 vue-tsc: ^3.2.1 vuefire: ^3.2.1 + wwobjloader2: ^6.2.1 yjs: ^13.6.27 zod: ^3.23.8 zod-to-json-schema: ^3.24.1 diff --git a/src/extensions/core/load3d/Load3dUtils.ts b/src/extensions/core/load3d/Load3dUtils.ts index 4a63e6b5a..13095ac96 100644 --- a/src/extensions/core/load3d/Load3dUtils.ts +++ b/src/extensions/core/load3d/Load3dUtils.ts @@ -34,9 +34,26 @@ class Load3dUtils { return await resp.json() } + static readonly MAX_UPLOAD_SIZE_MB = 100 + static async uploadFile(file: File, subfolder: string) { let uploadPath + const fileSizeMB = file.size / 1024 / 1024 + if (fileSizeMB > this.MAX_UPLOAD_SIZE_MB) { + const message = t('toastMessages.fileTooLarge', { + size: fileSizeMB.toFixed(1), + maxSize: this.MAX_UPLOAD_SIZE_MB + }) + console.warn( + '[Load3D] uploadFile: file too large', + fileSizeMB.toFixed(2), + 'MB' + ) + useToastStore().addAlert(message) + return undefined + } + try { const body = new FormData() body.append('image', file) @@ -61,7 +78,7 @@ class Load3dUtils { useToastStore().addAlert(resp.status + ' - ' + resp.statusText) } } catch (error) { - console.error('Upload error:', error) + console.error('[Load3D] uploadFile: exception', error) useToastStore().addAlert( error instanceof Error ? error.message diff --git a/src/extensions/core/load3d/LoaderManager.ts b/src/extensions/core/load3d/LoaderManager.ts index 8fbc31767..76e294d80 100644 --- a/src/extensions/core/load3d/LoaderManager.ts +++ b/src/extensions/core/load3d/LoaderManager.ts @@ -3,9 +3,10 @@ import * as THREE from 'three' import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader' -import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader' import { STLLoader } from 'three/examples/jsm/loaders/STLLoader' +import { MtlObjBridge, OBJLoader2Parallel } from 'wwobjloader2' +import OBJLoader2WorkerUrl from 'wwobjloader2/worker?url' import { t } from '@/i18n' import { useSettingStore } from '@/platform/settings/settingStore' @@ -22,7 +23,7 @@ import { FastPLYLoader } from './loader/FastPLYLoader' export class LoaderManager implements LoaderManagerInterface { gltfLoader: GLTFLoader - objLoader: OBJLoader + objLoader: OBJLoader2Parallel mtlLoader: MTLLoader fbxLoader: FBXLoader stlLoader: STLLoader @@ -41,7 +42,12 @@ export class LoaderManager implements LoaderManagerInterface { this.eventManager = eventManager this.gltfLoader = new GLTFLoader() - this.objLoader = new OBJLoader() + this.objLoader = new OBJLoader2Parallel() + // Set worker URL for Vite compatibility + this.objLoader.setWorkerUrl( + true, + new URL(OBJLoader2WorkerUrl, import.meta.url) + ) this.mtlLoader = new MTLLoader() this.fbxLoader = new FBXLoader() this.stlLoader = new STLLoader() @@ -173,7 +179,9 @@ export class LoaderManager implements LoaderManagerInterface { const materials = await this.mtlLoader.loadAsync(mtlFileName) materials.preload() - this.objLoader.setMaterials(materials) + const materialsFromMtl = + MtlObjBridge.addMaterialsFromMtlLoader(materials) + this.objLoader.setMaterials(materialsFromMtl) } catch (e) { console.log( 'No MTL file found or error loading it, continuing without materials' @@ -181,8 +189,10 @@ export class LoaderManager implements LoaderManagerInterface { } } - this.objLoader.setPath(path) - model = await this.objLoader.loadAsync(filename) + // OBJLoader2Parallel uses Web Worker for parsing (non-blocking) + const objUrl = path + encodeURIComponent(filename) + model = await this.objLoader.loadAsync(objUrl) + model.traverse((child) => { if (child instanceof THREE.Mesh) { this.modelManager.originalMaterials.set(child, child.material) @@ -193,7 +203,6 @@ export class LoaderManager implements LoaderManagerInterface { case 'gltf': case 'glb': this.gltfLoader.setPath(path) - const gltf = await this.gltfLoader.loadAsync(filename) this.modelManager.setOriginalModel(gltf) diff --git a/src/extensions/core/load3d/interfaces.ts b/src/extensions/core/load3d/interfaces.ts index 8d32c54a7..7954f6bfe 100644 --- a/src/extensions/core/load3d/interfaces.ts +++ b/src/extensions/core/load3d/interfaces.ts @@ -4,8 +4,8 @@ import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper' import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader' -import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' import { STLLoader } from 'three/examples/jsm/loaders/STLLoader' +import { type OBJLoader2Parallel } from 'wwobjloader2' export type MaterialMode = | 'original' @@ -179,7 +179,7 @@ export interface ModelManagerInterface { export interface LoaderManagerInterface { gltfLoader: GLTFLoader - objLoader: OBJLoader + objLoader: OBJLoader2Parallel mtlLoader: MTLLoader fbxLoader: FBXLoader stlLoader: STLLoader diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 14ef297c7..f9f7c2efa 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1702,6 +1702,7 @@ "pleaseSelectNodesToGroup": "Please select the nodes (or other groups) to create a group for", "emptyCanvas": "Empty canvas", "fileUploadFailed": "File upload failed", + "fileTooLarge": "File too large ({size} MB). Maximum supported size is {maxSize} MB", "unableToGetModelFilePath": "Unable to get model file path", "couldNotDetermineFileType": "Could not determine file type", "errorLoadingModel": "Error loading model",