/** * @author Kai Salmen / https://kaisalmen.de * Development repository: https://github.com/kaisalmen/WWOBJLoader */ import { DefaultLoadingManager, FileLoader, Group } from "../../../build/three.module.js"; import { OBJLoader2Parser } from "./obj2/worker/parallel/OBJLoader2Parser.js"; import { MeshReceiver } from "./obj2/shared/MeshReceiver.js"; import { MaterialHandler } from "./obj2/shared/MaterialHandler.js"; /** * Use this class to load OBJ data from files or to parse OBJ data from an arraybuffer * @class * * @param {DefaultLoadingManager} [manager] The loadingManager for the loader to use. Default is {@link DefaultLoadingManager} */ const OBJLoader2 = function ( manager ) { this.manager = ( manager !== undefined && manager !== null ) ? manager : DefaultLoadingManager; this.logging = { enabled: true, debug: false }; this.modelName = ''; this.instanceNo = 0; this.path = undefined; this.resourcePath = undefined; this.useIndices = false; this.disregardNormals = false; this.materialPerSmoothingGroup = false; this.useOAsMesh = false; this.baseObject3d = new Group(); this.callbacks = { onParseProgress: undefined, genericErrorHandler: undefined }; this.materialHandler = new MaterialHandler(); this.meshReceiver = new MeshReceiver( this.materialHandler ); }; OBJLoader2.OBJLOADER2_VERSION = '3.0.0-beta2'; console.info( 'Using OBJLoader2 version: ' + OBJLoader2.OBJLOADER2_VERSION ); OBJLoader2.prototype = { constructor: OBJLoader2, /** * Enable or disable logging in general (except warn and error), plus enable or disable debug logging. * * @param {boolean} enabled True or false. * @param {boolean} debug True or false. */ setLogging: function ( enabled, debug ) { this.logging.enabled = enabled === true; this.logging.debug = debug === true; return this; }, /** * Set the name of the model. * * @param {string} modelName */ setModelName: function ( modelName ) { this.modelName = modelName ? modelName : this.modelName; return this; }, /** * The URL of the base path. * * @param {string} path URL */ setPath: function ( path ) { this.path = path ? path : this.path; return this; }, /** * Allow to specify resourcePath for dependencies of specified resource. * @param {string} resourcePath */ setResourcePath: function ( resourcePath ) { this.resourcePath = resourcePath ? resourcePath : this.resourcePath; }, /** * Set the node where the loaded objects will be attached directly. * * @param {Object3D} baseObject3d Object already attached to scenegraph where new meshes will be attached to */ setBaseObject3d: function ( baseObject3d ) { this.baseObject3d = ( baseObject3d === undefined || baseObject3d === null ) ? this.baseObject3d : baseObject3d; return this; }, /** * Add materials as associated array. * * @param materials Object with named {@link Material} */ addMaterials: function ( materials ) { this.materialHandler.addMaterials( materials ); }, /** * Instructs loaders to create indexed {@link BufferGeometry}. * * @param {boolean} useIndices=false */ setUseIndices: function ( useIndices ) { this.useIndices = useIndices === true; return this; }, /** * Tells whether normals should be completely disregarded and regenerated. * * @param {boolean} disregardNormals=false */ setDisregardNormals: function ( disregardNormals ) { this.disregardNormals = disregardNormals === true; return this; }, /** * Tells whether a material shall be created per smoothing group. * * @param {boolean} materialPerSmoothingGroup=false */ setMaterialPerSmoothingGroup: function ( materialPerSmoothingGroup ) { this.materialPerSmoothingGroup = materialPerSmoothingGroup === true; return this; }, /** * Usually 'o' is meta-information and does not result in creation of new meshes, but mesh creation on occurrence of "o" can be enforced. * * @param {boolean} useOAsMesh=false */ setUseOAsMesh: function ( useOAsMesh ) { this.useOAsMesh = useOAsMesh === true; return this; }, /** * Register an generic error handler that is called if available instead of throwing an exception * @param {Function} genericErrorHandler */ setGenericErrorHandler: function ( genericErrorHandler ) { if ( genericErrorHandler !== undefined && genericErrorHandler !== null ) { this.callbacks.genericErrorHandler = genericErrorHandler; } }, /** * * @private * * @param {Function} [onParseProgress] * @param {Function} [onMeshAlter] * @param {Function} [onLoadMaterials] * @private */ _setCallbacks: function ( onParseProgress, onMeshAlter, onLoadMaterials ) { if ( onParseProgress !== undefined && onParseProgress !== null ) { this.callbacks.onParseProgress = onParseProgress; } this.meshReceiver._setCallbacks( onParseProgress, onMeshAlter ); this.materialHandler._setCallbacks( onLoadMaterials ); }, /** * Announce feedback which is give to the registered callbacks. * @private * * @param {string} type The type of event * @param {string} text Textual description of the event * @param {number} numericalValue Numerical value describing the progress */ _onProgress: function ( type, text, numericalValue ) { let message = text ? text : ''; let event = { detail: { type: type, modelName: this.modelName, instanceNo: this.instanceNo, text: message, numericalValue: numericalValue } }; if ( this.callbacks.onParseProgress ) { this.callbacks.onParseProgress( event ); } if ( this.logging.enabled && this.logging.debug ) { console.log( message ); } }, /** * Announce error feedback which is given to the generic error handler to the registered callbacks. * @private * * @param {String} errorMessage The event containing the error */ _onError: function ( errorMessage ) { if ( this.callbacks.genericErrorHandler ) { this.callbacks.genericErrorHandler( errorMessage ); } if ( this.logging.enabled && this.logging.debug ) { console.log( errorMessage ); } }, /** * Use this convenient method to load a file at the given URL. By default the fileLoader uses an ArrayBuffer. * * @param {string} url A string containing the path/URL of the file to be loaded. * @param {function} onLoad A function to be called after loading is successfully completed. The function receives loaded Object3D as an argument. * @param {function} [onFileLoadProgress] A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, which contains total and Integer bytes. * @param {function} [onError] A function to be called if an error occurs during loading. The function receives the error as an argument. * @param {function} [onMeshAlter] Called after worker successfully delivered a single mesh */ load: function ( url, onLoad, onFileLoadProgress, onError, onMeshAlter ) { let scope = this; if ( onError === null || onError === undefined ) { onError = function ( event ) { let errorMessage = event; if ( event.currentTarget && event.currentTarget.statusText !== null ) { errorMessage = 'Error occurred while downloading!\nurl: ' + event.currentTarget.responseURL + '\nstatus: ' + event.currentTarget.statusText; } scope._onError( errorMessage ); }; } if ( ! url ) { onError( 'An invalid url was provided. Unable to continue!' ); } let urlFull = new URL( url, window.location.href ).href; let filename = urlFull; let urlParts = urlFull.split( '/' ); if ( urlParts.length > 2 ) { filename = urlParts[ urlParts.length - 1 ]; let urlPartsPath = urlParts.slice( 0, urlParts.length - 1 ).join( '/' ) + '/'; if ( urlPartsPath !== undefined && urlPartsPath !== null ) this.path = urlPartsPath; } if ( onFileLoadProgress === null || onFileLoadProgress === undefined ) { let numericalValueRef = 0; let numericalValue = 0; onFileLoadProgress = function ( event ) { if ( ! event.lengthComputable ) return; numericalValue = event.loaded / event.total; if ( numericalValue > numericalValueRef ) { numericalValueRef = numericalValue; let output = 'Download of "' + url + '": ' + (numericalValue * 100).toFixed( 2 ) + '%'; scope._onProgress( 'progressLoad', output, numericalValue ); } }; } this._setCallbacks( null, onMeshAlter, null ); let fileLoaderOnLoad = function ( content ) { onLoad( scope.parse( content ) ); }; let fileLoader = new FileLoader( this.manager ); fileLoader.setPath( this.path || this.resourcePath ); fileLoader.setResponseType( 'arraybuffer' ); fileLoader.load( filename, fileLoaderOnLoad, onFileLoadProgress, onError ); }, /** * Parses OBJ data synchronously from arraybuffer or string. * * @param {arraybuffer|string} content OBJ data as Uint8Array or String */ parse: function ( content ) { // fast-fail in case of illegal data if ( content === null || content === undefined ) { throw 'Provided content is not a valid ArrayBuffer or String. Unable to continue parsing'; } if ( this.logging.enabled ) { console.time( 'OBJLoader parse: ' + this.modelName ); } let parser = new OBJLoader2Parser(); parser.setLogging( this.logging.enabled, this.logging.debug ); parser.setMaterialPerSmoothingGroup( this.materialPerSmoothingGroup ); parser.setUseOAsMesh( this.useOAsMesh ); parser.setUseIndices( this.useIndices ); parser.setDisregardNormals( this.disregardNormals ); // sync code works directly on the material references parser.setMaterials( this.materialHandler.getMaterials() ); let scope = this; let scopedOnAssetAvailable = function ( payload ) { scope._onAssetAvailable( payload ); }; let onProgressScoped = function ( text, numericalValue ) { scope._onProgress( 'progressParse', text, numericalValue ); }; let onErrorScoped = function ( message ) { scope._onError( message ); }; parser.setCallbackOnAssetAvailable( scopedOnAssetAvailable ); parser.setCallbackOnProgress( onProgressScoped ); parser.setCallbackOnError( onErrorScoped ); if ( content instanceof ArrayBuffer || content instanceof Uint8Array ) { if ( this.logging.enabled ) console.info( 'Parsing arrayBuffer...' ); parser.parse( content ); } else if ( typeof( content ) === 'string' || content instanceof String ) { if ( this.logging.enabled ) console.info( 'Parsing text...' ); parser.parseText( content ); } else { scope._onError( 'Provided content was neither of type String nor Uint8Array! Aborting...' ); } if ( this.logging.enabled ) { console.timeEnd( 'OBJLoader parse: ' + this.modelName ); } return this.baseObject3d; }, _onAssetAvailable: function ( payload ) { if ( payload.cmd !== 'assetAvailable' ) return; if ( payload.type === 'mesh' ) { let meshes = this.meshReceiver.buildMeshes( payload ); for ( let mesh of meshes ) { this.baseObject3d.add( mesh ); } } else if ( payload.type === 'material' ) { this.materialHandler.addPayloadMaterials( payload ); } } }; export { OBJLoader2 };