summaryrefslogtreecommitdiff
path: root/public/vendor/addons/loaders/DRACOLoader.js
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 23:47:42 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 23:47:42 +0800
commit5d73943e584b1c0c0753dda85fbda34d27726986 (patch)
tree5126127692f3cd399345aae8ade5ec66dd247668 /public/vendor/addons/loaders/DRACOLoader.js
parent7c3a0728570f84f5c7d5f396c5a5d5c11f5fb661 (diff)
downloadsomhairles-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.js613
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 };