<!-- SOLUV · part 230 -->
se ) return;

		if ( isBlobURL( key ) ) return;

		// log( 'Cache', 'Adding key:', key );

		this.files[ key ] = file;

	},

	/**
	 * Gets the cached value for the given key.
	 *
	 * @static
	 * @param {string} key - The key to reference the cached file.
	 * @return {Object|undefined} The cached file. If the key does not exist `undefined` is returned.
	 */
	get: function ( key ) {

		if ( this.enabled === false ) return;

		if ( isBlobURL( key ) ) return;

		// log( 'Cache', 'Checking key:', key );

		return this.files[ key ];

	},

	/**
	 * Removes the cached file associated with the given key.
	 *
	 * @static
	 * @param {string} key - The key to reference the cached file.
	 */
	remove: function ( key ) {

		delete this.files[ key ];

	},

	/**
	 * Remove all values from the cache.
	 *
	 * @static
	 */
	clear: function () {

		this.files = {};

	}

};

/**
 * Returns true if the given cache key contains the blob: scheme.
 *
 * @private
 * @param {string} key - The cache key.
 * @return {boolean} Whether the given cache key contains the blob: scheme or not.
 */
function isBlobURL( key ) {

	try {

		const urlString = key.slice( key.indexOf( ':' ) + 1 ); // remove type identifier

		const url = new URL( urlString );
		return url.protocol === 'blob:';

	} catch ( e ) {

		// If the string is not a valid URL, it throws an error
		return false;

	}

}

/**
 * Handles and keeps track of loaded and pending data. A default global
 * instance of this class is created and used by loaders if not supplied
 * manually.
 *
 * In general that should be sufficient, however there are times when it can
 * be useful to have separate loaders - for example if you want to show
 * separate loading bars for objects and textures.
 *
 * ```js
 * const manager = new THREE.LoadingManager();
 * manager.onLoad = () => console.log( 'Loading complete!' );
 *
 * const loader1 = new OBJLoader( manager );
 * const loader2 = new ColladaLoader( manager );
 * ```
 */
class LoadingManager {

	/**
	 * Constructs a new loading manager.
	 *
	 * @param {Function} [onLoad] - Executes when all items have been loaded.
	 * @param {Function} [onProgress] - Executes when single items have been loaded.
	 * @param {Function} [onError] - Executes when an error occurs.
	 */
	constructor( onLoad, onProgress, onError ) {

		const scope = this;

		let isLoading = false;
		let itemsLoaded = 0;
		let itemsTotal = 0;
		let urlModifier = undefined;
		const handlers = [];

		// Refer to #5689 for the reason why we don't set .onStart
		// in the constructor

		/**
		 * Executes when an item starts loading.
		 *
		 * @type {Function|undefined}
		 * @default undefined
		 */
		this.onStart = undefined;

		/**
		 * Executes when all items have been loaded.
		 *
		 * @type {Function|undefined}
		 * @default undefined
		 */
		this.onLoad = onLoad;

		/**
		 * Executes when single items have been loaded.
		 *
		 * @type {Function|undefined}
		 * @default undefined
		 */
		this.onProgress = onProgress;

		/**
		 * Executes when an error occurs.
		 *
		 * @type {Function|undefined}
		 * @default undefined
		 */
		this.onError = onError;

		/**
		 * Used for aborting ongoing requests in loaders using this manager.
		 *
		 * @private
		 * @type {AbortController | null}
		 */
		this._abortController = null;

		/**
		 * This should be called by any loader using the manager when the loader
		 * starts loading an item.
		 *
		 * @param {string} url - The URL to load.
		 */
		this.itemStart = function ( url ) {

			itemsTotal ++;

			if ( isLoading === false ) {

				if ( scope.onStart !== undefined ) {

					scope.onStart( url, itemsLoaded, itemsTotal );

				}

			}

			isLoading = true;

		};

		/**
		 * This should be called by any loader using the manager when the loader
		 * ended loading an item.
		 *
		 * @param {string} url - The URL of the loaded item.
		 */
		this.itemEnd = function ( url ) {

			itemsLoaded ++;

			if ( scope.onProgress !== undefined ) {

				scope.onProgress( url, itemsLoaded, itemsTotal );

			}

			if ( itemsLoaded === itemsTotal ) {

				isLoading = false;

				if ( scope.onLoad !== undefined ) {

					scope.onLoad();

				}

			}

		};

		/**
		 * This should be called by any loader using the manager when the loader
		 * encounters an error when loading an item.
		 *
		 * @param {string} url - The URL of the item that produces an error.
		 */
		this.itemError = function ( url ) {

			if ( scope.onError !== undefined ) {

				scope.onError( url );

			}

		};

		/**
		 * Given a URL, uses the URL modifier callback (if any) and returns a
		 * resolved URL. If no URL modifier is set, returns the original URL.
		 *
		 * @param {string} url - The URL to load.
		 * @return {string} The resolved URL.
		 */
		this.resolveURL = function ( url ) {

			// Normalize to NFC so that Unicode URIs (e.g. from glTF)
			// are percent-encoded correctly per RFC 3987.

			url = url.normalize( 'NFC' );

			if ( urlModifier ) {

				return urlM