diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 23:47:42 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 23:47:42 +0800 |
| commit | 5d73943e584b1c0c0753dda85fbda34d27726986 (patch) | |
| tree | 5126127692f3cd399345aae8ade5ec66dd247668 /public/vendor/addons/loaders/DRACOLoader.js | |
| parent | 7c3a0728570f84f5c7d5f396c5a5d5c11f5fb661 (diff) | |
| download | somhairles-dream-fsharp-5d73943e584b1c0c0753dda85fbda34d27726986.tar.gz | |
feat(ship): HNX-02 Copernicus rework — continuous hull, few-large-element scale, Draco pipeline, side-on framing
Diffstat (limited to 'public/vendor/addons/loaders/DRACOLoader.js')
| -rw-r--r-- | public/vendor/addons/loaders/DRACOLoader.js | 613 |
1 files changed, 613 insertions, 0 deletions
diff --git a/public/vendor/addons/loaders/DRACOLoader.js b/public/vendor/addons/loaders/DRACOLoader.js new file mode 100644 index 0000000..b176ebf --- /dev/null +++ b/public/vendor/addons/loaders/DRACOLoader.js @@ -0,0 +1,613 @@ +import { + BufferAttribute, + BufferGeometry, + Color, + FileLoader, + Loader, + LinearSRGBColorSpace, + SRGBColorSpace +} from 'three'; + +const _taskCache = new WeakMap(); + +class DRACOLoader extends Loader { + + constructor( manager ) { + + super( manager ); + + this.decoderPath = ''; + this.decoderConfig = {}; + this.decoderBinary = null; + this.decoderPending = null; + + this.workerLimit = 4; + this.workerPool = []; + this.workerNextTaskID = 1; + this.workerSourceURL = ''; + + this.defaultAttributeIDs = { + position: 'POSITION', + normal: 'NORMAL', + color: 'COLOR', + uv: 'TEX_COORD' + }; + this.defaultAttributeTypes = { + position: 'Float32Array', + normal: 'Float32Array', + color: 'Float32Array', + uv: 'Float32Array' + }; + + } + + setDecoderPath( path ) { + + this.decoderPath = path; + + return this; + + } + + setDecoderConfig( config ) { + + this.decoderConfig = config; + + return this; + + } + + setWorkerLimit( workerLimit ) { + + this.workerLimit = workerLimit; + + return this; + + } + + load( url, onLoad, onProgress, onError ) { + + const loader = new FileLoader( this.manager ); + + loader.setPath( this.path ); + loader.setResponseType( 'arraybuffer' ); + loader.setRequestHeader( this.requestHeader ); + loader.setWithCredentials( this.withCredentials ); + + loader.load( url, ( buffer ) => { + + this.parse( buffer, onLoad, onError ); + + }, onProgress, onError ); + + } + + + parse( buffer, onLoad, onError = ()=>{} ) { + + this.decodeDracoFile( buffer, onLoad, null, null, SRGBColorSpace ).catch( onError ); + + } + + decodeDracoFile( buffer, callback, attributeIDs, attributeTypes, vertexColorSpace = LinearSRGBColorSpace, onError = () => {} ) { + + const taskConfig = { + attributeIDs: attributeIDs || this.defaultAttributeIDs, + attributeTypes: attributeTypes || this.defaultAttributeTypes, + useUniqueIDs: !! attributeIDs, + vertexColorSpace: vertexColorSpace, + }; + + return this.decodeGeometry( buffer, taskConfig ).then( callback ).catch( onError ); + + } + + decodeGeometry( buffer, taskConfig ) { + + const taskKey = JSON.stringify( taskConfig ); + + // Check for an existing task using this buffer. A transferred buffer cannot be transferred + // again from this thread. + if ( _taskCache.has( buffer ) ) { + + const cachedTask = _taskCache.get( buffer ); + + if ( cachedTask.key === taskKey ) { + + return cachedTask.promise; + + } else if ( buffer.byteLength === 0 ) { + + // Technically, it would be possible to wait for the previous task to complete, + // transfer the buffer back, and decode again with the second configuration. That + // is complex, and I don't know of any reason to decode a Draco buffer twice in + // different ways, so this is left unimplemented. + throw new Error( + + 'THREE.DRACOLoader: Unable to re-decode a buffer with different ' + + 'settings. Buffer has already been transferred.' + + ); + + } + + } + + // + + let worker; + const taskID = this.workerNextTaskID ++; + const taskCost = buffer.byteLength; + + // Obtain a worker and assign a task, and construct a geometry instance + // when the task completes. + const geometryPending = this._getWorker( taskID, taskCost ) + .then( ( _worker ) => { + + worker = _worker; + + return new Promise( ( resolve, reject ) => { + + worker._callbacks[ taskID ] = { resolve, reject }; + + worker.postMessage( { type: 'decode', id: taskID, taskConfig, buffer }, [ buffer ] ); + + // this.debug(); + + } ); + + } ) + .then( ( message ) => this._createGeometry( message.geometry ) ); + + // Remove task from the task list. + // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416) + geometryPending + .catch( () => true ) + .then( () => { + + if ( worker && taskID ) { + + this._releaseTask( worker, taskID ); + + // this.debug(); + + } + + } ); + + // Cache the task result. + _taskCache.set( buffer, { + + key: taskKey, + promise: geometryPending + + } ); + + return geometryPending; + + } + + _createGeometry( geometryData ) { + + const geometry = new BufferGeometry(); + + if ( geometryData.index ) { + + geometry.setIndex( new BufferAttribute( geometryData.index.array, 1 ) ); + + } + + for ( let i = 0; i < geometryData.attributes.length; i ++ ) { + + const result = geometryData.attributes[ i ]; + const name = result.name; + const array = result.array; + const itemSize = result.itemSize; + + const attribute = new BufferAttribute( array, itemSize ); + + if ( name === 'color' ) { + + this._assignVertexColorSpace( attribute, result.vertexColorSpace ); + + attribute.normalized = ( array instanceof Float32Array ) === false; + + } + + geometry.setAttribute( name, attribute ); + + } + + return geometry; + + } + + _assignVertexColorSpace( attribute, inputColorSpace ) { + + // While .drc files do not specify colorspace, the only 'official' tooling + // is PLY and OBJ converters, which use sRGB. We'll assume sRGB when a .drc + // file is passed into .load() or .parse(). GLTFLoader uses internal APIs + // to decode geometry, and vertex colors are already Linear-sRGB in there. + + if ( inputColorSpace !== SRGBColorSpace ) return; + + const _color = new Color(); + + for ( let i = 0, il = attribute.count; i < il; i ++ ) { + + _color.fromBufferAttribute( attribute, i ).convertSRGBToLinear(); + attribute.setXYZ( i, _color.r, _color.g, _color.b ); + + } + + } + + _loadLibrary( url, responseType ) { + + const loader = new FileLoader( this.manager ); + loader.setPath( this.decoderPath ); + loader.setResponseType( responseType ); + loader.setWithCredentials( this.withCredentials ); + + return new Promise( ( resolve, reject ) => { + + loader.load( url, resolve, undefined, reject ); + + } ); + + } + + preload() { + + this._initDecoder(); + + return this; + + } + + _initDecoder() { + + if ( this.decoderPending ) return this.decoderPending; + + const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'; + const librariesPending = []; + + if ( useJS ) { + + librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) ); + + } else { + + librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) ); + librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) ); + + } + + this.decoderPending = Promise.all( librariesPending ) + .then( ( libraries ) => { + + const jsContent = libraries[ 0 ]; + + if ( ! useJS ) { + + this.decoderConfig.wasmBinary = libraries[ 1 ]; + + } + + const fn = DRACOWorker.toString(); + + const body = [ + '/* draco decoder */', + jsContent, + '', + '/* worker */', + fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) ) + ].join( '\n' ); + + this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) ); + + } ); + + return this.decoderPending; + + } + + _getWorker( taskID, taskCost ) { + + return this._initDecoder().then( () => { + + if ( this.workerPool.length < this.workerLimit ) { + + const worker = new Worker( this.workerSourceURL ); + + worker._callbacks = {}; + worker._taskCosts = {}; + worker._taskLoad = 0; + + worker.postMessage( { type: 'init', decoderConfig: this.decoderConfig } ); + + worker.onmessage = function ( e ) { + + const message = e.data; + + switch ( message.type ) { + + case 'decode': + worker._callbacks[ message.id ].resolve( message ); + break; + + case 'error': + worker._callbacks[ message.id ].reject( message ); + break; + + default: + console.error( 'THREE.DRACOLoader: Unexpected message, "' + message.type + '"' ); + + } + + }; + + this.workerPool.push( worker ); + + } else { + + this.workerPool.sort( function ( a, b ) { + + return a._taskLoad > b._taskLoad ? - 1 : 1; + + } ); + + } + + const worker = this.workerPool[ this.workerPool.length - 1 ]; + worker._taskCosts[ taskID ] = taskCost; + worker._taskLoad += taskCost; + return worker; + + } ); + + } + + _releaseTask( worker, taskID ) { + + worker._taskLoad -= worker._taskCosts[ taskID ]; + delete worker._callbacks[ taskID ]; + delete worker._taskCosts[ taskID ]; + + } + + debug() { + + console.log( 'Task load: ', this.workerPool.map( ( worker ) => worker._taskLoad ) ); + + } + + dispose() { + + for ( let i = 0; i < this.workerPool.length; ++ i ) { + + this.workerPool[ i ].terminate(); + + } + + this.workerPool.length = 0; + + if ( this.workerSourceURL !== '' ) { + + URL.revokeObjectURL( this.workerSourceURL ); + + } + + return this; + + } + +} + +/* WEB WORKER */ + +function DRACOWorker() { + + let decoderConfig; + let decoderPending; + + onmessage = function ( e ) { + + const message = e.data; + + switch ( message.type ) { + + case 'init': + decoderConfig = message.decoderConfig; + decoderPending = new Promise( function ( resolve/*, reject*/ ) { + + decoderConfig.onModuleLoaded = function ( draco ) { + + // Module is Promise-like. Wrap before resolving to avoid loop. + resolve( { draco: draco } ); + + }; + + DracoDecoderModule( decoderConfig ); // eslint-disable-line no-undef + + } ); + break; + + case 'decode': + const buffer = message.buffer; + const taskConfig = message.taskConfig; + decoderPending.then( ( module ) => { + + const draco = module.draco; + const decoder = new draco.Decoder(); + + try { + + const geometry = decodeGeometry( draco, decoder, new Int8Array( buffer ), taskConfig ); + + const buffers = geometry.attributes.map( ( attr ) => attr.array.buffer ); + + if ( geometry.index ) buffers.push( geometry.index.array.buffer ); + + self.postMessage( { type: 'decode', id: message.id, geometry }, buffers ); + + } catch ( error ) { + + console.error( error ); + + self.postMessage( { type: 'error', id: message.id, error: error.message } ); + + } finally { + + draco.destroy( decoder ); + + } + + } ); + break; + + } + + }; + + function decodeGeometry( draco, decoder, array, taskConfig ) { + + const attributeIDs = taskConfig.attributeIDs; + const attributeTypes = taskConfig.attributeTypes; + + let dracoGeometry; + let decodingStatus; + + const geometryType = decoder.GetEncodedGeometryType( array ); + + if ( geometryType === draco.TRIANGULAR_MESH ) { + + dracoGeometry = new draco.Mesh(); + decodingStatus = decoder.DecodeArrayToMesh( array, array.byteLength, dracoGeometry ); + + } else if ( geometryType === draco.POINT_CLOUD ) { + + dracoGeometry = new draco.PointCloud(); + decodingStatus = decoder.DecodeArrayToPointCloud( array, array.byteLength, dracoGeometry ); + + } else { + + throw new Error( 'THREE.DRACOLoader: Unexpected geometry type.' ); + + } + + if ( ! decodingStatus.ok() || dracoGeometry.ptr === 0 ) { + + throw new Error( 'THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg() ); + + } + + const geometry = { index: null, attributes: [] }; + + // Gather all vertex attributes. + for ( const attributeName in attributeIDs ) { + + const attributeType = self[ attributeTypes[ attributeName ] ]; + + let attribute; + let attributeID; + + // A Draco file may be created with default vertex attributes, whose attribute IDs + // are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively, + // a Draco file may contain a custom set of attributes, identified by known unique + // IDs. glTF files always do the latter, and `.drc` files typically do the former. + if ( taskConfig.useUniqueIDs ) { + + attributeID = attributeIDs[ attributeName ]; + attribute = decoder.GetAttributeByUniqueId( dracoGeometry, attributeID ); + + } else { + + attributeID = decoder.GetAttributeId( dracoGeometry, draco[ attributeIDs[ attributeName ] ] ); + + if ( attributeID === - 1 ) continue; + + attribute = decoder.GetAttribute( dracoGeometry, attributeID ); + + } + + const attributeResult = decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ); + + if ( attributeName === 'color' ) { + + attributeResult.vertexColorSpace = taskConfig.vertexColorSpace; + + } + + geometry.attributes.push( attributeResult ); + + } + + // Add index. + if ( geometryType === draco.TRIANGULAR_MESH ) { + + geometry.index = decodeIndex( draco, decoder, dracoGeometry ); + + } + + draco.destroy( dracoGeometry ); + + return geometry; + + } + + function decodeIndex( draco, decoder, dracoGeometry ) { + + const numFaces = dracoGeometry.num_faces(); + const numIndices = numFaces * 3; + const byteLength = numIndices * 4; + + const ptr = draco._malloc( byteLength ); + decoder.GetTrianglesUInt32Array( dracoGeometry, byteLength, ptr ); + const index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice(); + draco._free( ptr ); + + return { array: index, itemSize: 1 }; + + } + + function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) { + + const numComponents = attribute.num_components(); + const numPoints = dracoGeometry.num_points(); + const numValues = numPoints * numComponents; + const byteLength = numValues * attributeType.BYTES_PER_ELEMENT; + const dataType = getDracoDataType( draco, attributeType ); + + const ptr = draco._malloc( byteLength ); + decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dataType, byteLength, ptr ); + const array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice(); + draco._free( ptr ); + + return { + name: attributeName, + array: array, + itemSize: numComponents + }; + + } + + function getDracoDataType( draco, attributeType ) { + + switch ( attributeType ) { + + case Float32Array: return draco.DT_FLOAT32; + case Int8Array: return draco.DT_INT8; + case Int16Array: return draco.DT_INT16; + case Int32Array: return draco.DT_INT32; + case Uint8Array: return draco.DT_UINT8; + case Uint16Array: return draco.DT_UINT16; + case Uint32Array: return draco.DT_UINT32; + + } + + } + +} + +export { DRACOLoader }; |
