diff --git a/package.json b/package.json index 1244604ce..65ef9638d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bldrs", - "version": "1.0.1500", + "version": "1.0.1510", "main": "src/index.jsx", "license": "AGPL-3.0", "homepage": "https://github.com/bldrs-ai/Share", @@ -13,7 +13,7 @@ "build-conway-debug": "yarn clean && yarn build-share-conway && yarn build-share-copy-wasm-conway-profile && yarn build-cosmos", "build-webifc": "yarn clean && yarn build-share-webifc && yarn build-cosmos", "build-cosmos": "shx mkdir -p docs && shx rm -rf docs/cosmos && cosmos-export --config .cosmos.config.json && shx mv cosmos-export docs/cosmos", - "build-share": "yarn update-version && node tools/esbuild/build.js && shx mkdir -p docs/static/js && shx cp src/OPFS/OPFS.worker.js docs/ && shx cp src/net/github/Cache.js docs/ && yarn build-share-copy-wasm-conway-MT", + "build-share": "yarn update-version && node tools/esbuild/build.js && shx mkdir -p docs/static/js && shx cp src/OPFS/OPFS.worker.js docs/ && shx cp src/net/github/Cache.js docs/ && yarn build-share-copy-wasm-conway-MT && yarn build-share-copy-draco", "build-share-conway": "run-script-os", "build-share-conway:win32": "set USE_WEBIFC_SHIM=true&& yarn build-share", "build-share-conway:linux:darwin": "USE_WEBIFC_SHIM=true yarn build-share", @@ -23,6 +23,7 @@ "build-share-copy-wasm-webifc": "shx cp node_modules/web-ifc/*.wasm docs/static/js", "build-share-copy-wasm-conway-MT": "shx cp node_modules/@bldrs-ai/conway/compiled/dependencies/conway-geom/Dist/* docs/static/js", "build-share-copy-wasm-conway-profile": "shx cp node_modules/@bldrs-ai/conway-web-ifc-adapter/node_modules/@bldrs-ai/conway/compiled/dependencies/conway-geom/Dist/* docs/static/js", + "build-share-copy-draco": "shx mkdir -p docs/static/js/draco && shx cp -r node_modules/three/examples/js/libs/draco/* docs/static/js/draco/", "build-share-analyze": "ANALYZE=true node tools/esbuild/build.js", "clean": "shx rm -rf docs", "cy": "cypress run --browser chrome", diff --git a/public/static/js/vendor/three/examples/jsm/loaders/DRACOLoader.js b/public/static/js/vendor/three/examples/jsm/loaders/DRACOLoader.js new file mode 100644 index 000000000..53afc5b11 --- /dev/null +++ b/public/static/js/vendor/three/examples/jsm/loaders/DRACOLoader.js @@ -0,0 +1,587 @@ +import { + BufferAttribute, + BufferGeometry, + FileLoader, + Loader +} 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 ) => { + + const taskConfig = { + attributeIDs: this.defaultAttributeIDs, + attributeTypes: this.defaultAttributeTypes, + useUniqueIDs: false + }; + + this.decodeGeometry( buffer, taskConfig ) + .then( onLoad ) + .catch( onError ); + + }, onProgress, onError ); + + } + + /** @deprecated Kept for backward-compatibility with previous DRACOLoader versions. */ + decodeDracoFile( buffer, callback, attributeIDs, attributeTypes ) { + + const taskConfig = { + attributeIDs: attributeIDs || this.defaultAttributeIDs, + attributeTypes: attributeTypes || this.defaultAttributeTypes, + useUniqueIDs: !! attributeIDs + }; + + this.decodeGeometry( buffer, taskConfig ).then( callback ); + + } + + decodeGeometry( buffer, taskConfig ) { + + // TODO: For backward-compatibility, support 'attributeTypes' objects containing + // references (rather than names) to typed array constructors. These must be + // serialized before sending them to the worker. + for ( const attribute in taskConfig.attributeTypes ) { + + const type = taskConfig.attributeTypes[ attribute ]; + + if ( type.BYTES_PER_ELEMENT !== undefined ) { + + taskConfig.attributeTypes[ attribute ] = type.name; + + } + + } + + // + + 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 attribute = geometryData.attributes[ i ]; + const name = attribute.name; + const array = attribute.array; + const itemSize = attribute.itemSize; + + geometry.setAttribute( name, new BufferAttribute( array, itemSize ) ); + + } + + return geometry; + + } + + _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; + + 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(); + const decoderBuffer = new draco.DecoderBuffer(); + decoderBuffer.Init( new Int8Array( buffer ), buffer.byteLength ); + + try { + + const geometry = decodeGeometry( draco, decoder, decoderBuffer, 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( decoderBuffer ); + draco.destroy( decoder ); + + } + + } ); + break; + + } + + }; + + function decodeGeometry( draco, decoder, decoderBuffer, taskConfig ) { + + const attributeIDs = taskConfig.attributeIDs; + const attributeTypes = taskConfig.attributeTypes; + + let dracoGeometry; + let decodingStatus; + + const geometryType = decoder.GetEncodedGeometryType( decoderBuffer ); + + if ( geometryType === draco.TRIANGULAR_MESH ) { + + dracoGeometry = new draco.Mesh(); + decodingStatus = decoder.DecodeBufferToMesh( decoderBuffer, dracoGeometry ); + + } else if ( geometryType === draco.POINT_CLOUD ) { + + dracoGeometry = new draco.PointCloud(); + decodingStatus = decoder.DecodeBufferToPointCloud( decoderBuffer, 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 ); + + } + + geometry.attributes.push( decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) ); + + } + + // 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 }; diff --git a/src/loader/Loader.js b/src/loader/Loader.js index 9c0efe89c..b79043e1c 100644 --- a/src/loader/Loader.js +++ b/src/loader/Loader.js @@ -1,6 +1,6 @@ import axios from 'axios' import {BufferAttribute, Matrix4, Mesh, Object3D} from 'three' -import {DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader' + import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader' import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader' import {OBJLoader} from 'three/examples/jsm/loaders/OBJLoader' @@ -17,7 +17,7 @@ import {parseGitHubPath} from '../utils/location' import {testUuid} from '../utils/strings' import {dereferenceAndProxyDownloadContents} from './urls' import BLDLoader from './BLDLoader' -import glbToThree from './glb' +import {createGltfLoader, glbToThree} from './glb' import objToThree from './obj' import pdbToThree from './pdb' import stlToThree from './stl' @@ -81,7 +81,7 @@ export async function load( } // Find loader can do a head download for content typecheck, but full download is delayed - const [loader, isLoaderAsync, isFormatText, isIfc, fixupCb] = await findLoader(path, viewer) + const [loader, isLoaderAsync, isFormatText, isIfc, fixupCb] = await findLoader(path, viewer, onProgress) debug().log( `Loader#load: loader=${loader.constructor.name} isLoaderAsync=${isLoaderAsync} isFormatText=${isFormatText} path=${path}`) @@ -381,7 +381,7 @@ export async function readModel(loader, modelData, basePath, isLoaderAsync, isIf * @param {string} pathname * @return {Function|undefined} */ -async function findLoader(pathname, viewer) { +async function findLoader(pathname, viewer, onProgress) { let extension try { extension = Filetype.getValidExtension(pathname) @@ -449,7 +449,7 @@ async function findLoader(pathname, viewer) { } case 'glb': case 'gltf': { - loader = newGltfLoader() + loader = createGltfLoader(onProgress) fixupCb = glbToThree break } @@ -485,18 +485,6 @@ async function findLoader(pathname, viewer) { } -/** - * @return {GLTFLoader} With DRACO codec enabled - */ -function newGltfLoader() { - const loader = new GLTFLoader - const dracoLoader = new DRACOLoader - dracoLoader.setDecoderPath('./node_modules/three/examples/jsm/libs/draco/') - loader.setDRACOLoader(dracoLoader) - return loader -} - - /** * Sets up the IFCLoader to use the wasm module and move the model to * the origin on load. diff --git a/src/loader/Loader.test.js b/src/loader/Loader.test.js index 9109d1dbe..3fe1d04e3 100644 --- a/src/loader/Loader.test.js +++ b/src/loader/Loader.test.js @@ -2,6 +2,20 @@ import {Object3D, Mesh, BufferGeometry, Material, BufferAttribute} from 'three' import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader' import {load, readModel} from './Loader' +// Mock DRACOLoader to avoid HTTP requests in tests +jest.mock('three/examples/jsm/loaders/DRACOLoader', () => { + const MockDRACOLoader = jest.fn().mockImplementation(() => ({ + setDecoderPath: jest.fn(), + setDecoderConfig: jest.fn(), + onError: null, + onProgress: null, + preload: jest.fn().mockResolvedValue(), + decodeDracoFile: jest.fn().mockResolvedValue(new ArrayBuffer(0)), + decodeDracoBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0)), + })) + return {DRACOLoader: MockDRACOLoader} +}) + let mathRandomSpy let mockViewer @@ -97,6 +111,67 @@ describe('Loader', () => { } }) + it('detects Draco compression in GLB file', () => { + const testPath = 'glb/cube-draco.glb' + const content = readTestDataFile(testPath, true) // Read as binary (returns Buffer) + const {detectDracoCompression} = require('./glb') + + // Convert Buffer to ArrayBuffer for the detection function + const arrayBuffer = content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength) + + // Test that our Draco detection function can process the file + const isDracoCompressed = detectDracoCompression(arrayBuffer) + + // The file should be detected as Draco compressed + expect(isDracoCompressed).toBe(true) + }) + + it('loads a GLB (draco) model', async () => { + // Mock the createGltfLoader function to avoid HTTP requests for Draco decoder + const {createGltfLoader} = require('./glb') + const originalCreateGltfLoader = createGltfLoader + + // Create a mock loader that simulates successful Draco decompression + const mockLoader = { + parse: jest.fn().mockImplementation((data, path, onLoad) => { + // Simulate successful parsing with a mock model + const mockModel = { + scenes: [{ + children: [{ + geometry: new BufferGeometry(), + material: new Material(), + isObject3D: true, + }], + }], + } + onLoad(mockModel) + }), + setDRACOLoader: jest.fn(), + } + + // Mock the createGltfLoader function + jest.doMock('./glb', () => ({ + ...jest.requireActual('./glb'), + createGltfLoader: jest.fn().mockReturnValue(mockLoader), + })) + + mockViewer.IFC.type = 'glb' + const testPath = 'glb/cube-draco.glb' + const onProgress = jest.fn() + const setOpfsFile = jest.fn() + const restoreArrayBuffer = testPathToContent(testPath) + + try { + const model = await load(testPathToUrl(testPath), mockViewer, onProgress, true, setOpfsFile, '') + expect(model).toBeDefined() + expect(model).toMatchSnapshot() + } finally { + restoreArrayBuffer() + // Restore original function + jest.doMock('./glb', () => jest.requireActual('./glb')) + } + }) + it('loads an OBJ model', async () => { mockViewer.IFC.type = 'obj' const testPath = 'obj/Bunny.obj' diff --git a/src/loader/glb.js b/src/loader/glb.js index 20eb4b821..db683a3e8 100644 --- a/src/loader/glb.js +++ b/src/loader/glb.js @@ -1,10 +1,32 @@ +import {REVISION} from 'three' +import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader' +import {DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader' + + +/** + * Create a GLTFLoader that can load Draco compressed models + * + * @return {GLTFLoader} + */ +export function createGltfLoader() { + console.log('THREE.REVISION:', REVISION) + const loader = new GLTFLoader() + const dracoLoader = new DRACOLoader() + dracoLoader.setDecoderPath('/static/js/draco/') + dracoLoader.setDecoderConfig({type: 'wasm'}) + dracoLoader.preload() + loader.setDRACOLoader(dracoLoader) + return loader +} + + /** * GLTF returns a complex object with lights, animations, etc. * * @param {object} model * @return {object} scene */ -export default function glbToThree(model) { +export function glbToThree(model) { if (model.scenes) { if (model.scenes.length === 1) { return model.scenes[0] diff --git a/src/utils/debug.js b/src/utils/debug.js index 97cb760d2..be2d57de5 100644 --- a/src/utils/debug.js +++ b/src/utils/debug.js @@ -5,7 +5,7 @@ const WARN = 2 // Use this as default for prod. Should never see these messages const INFO = 1 const DEBUG = 0 /* eslint-enable no-unused-vars */ -let DEBUG_LEVEL = WARN +let DEBUG_LEVEL = DEBUG /** diff --git a/testdata/models/glb/cube-draco.glb b/testdata/models/glb/cube-draco.glb new file mode 100644 index 000000000..fea9a63db Binary files /dev/null and b/testdata/models/glb/cube-draco.glb differ diff --git a/tools/esbuild/common.js b/tools/esbuild/common.js index 588af1af3..60818d0a3 100644 --- a/tools/esbuild/common.js +++ b/tools/esbuild/common.js @@ -27,7 +27,7 @@ export default { target: ['chrome64', 'firefox62', 'safari11.1', 'edge79', 'es2021'], bundle: true, external: ['*.woff', '*.woff2'], - minify: (process.env.MINIFY || 'true') === 'true', + minify: false, // (process.env.MINIFY || 'true') === 'true', keepNames: true, // TODO: have had breakage without this splitting: false, metafile: true, diff --git a/tools/esbuild/plugins.js b/tools/esbuild/plugins.js index dfc68266d..e82628c14 100644 --- a/tools/esbuild/plugins.js +++ b/tools/esbuild/plugins.js @@ -29,6 +29,27 @@ export default function makePlugins(root, buildDir) { src: assetsDir, dest: buildDir, }), + { + name: 'external-three-draco-loader', + setup(build) { + build.onResolve({ + filter: /^three\/examples\/jsm\/loaders\/DRACOLoader\.js$/, + }, () => ({ + path: '/vendor/three/examples/jsm/loaders/DRACOLoader.js', + external: true, // emit an ESM import to this URL + })) + }, + }, + { + name: 'external-draco-libs', + setup(b) { + b.onResolve({filter: /node_modules\/three\/examples\/js\/libs\/draco\//}, + () => { + console.log('onResolve draco') + return ({external: true}) + }) + }, + }, ] // Conditionally include webIfcShimAliasPlugin