threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AssetImporter.ts 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. import {Event, EventDispatcher, EventListener, FileLoader, LoaderUtils, LoadingManager} from 'three'
  2. import {
  3. IAssetImporter,
  4. IAssetImporterEventTypes,
  5. IImportResultUserData,
  6. ImportAssetOptions,
  7. ImportFilesOptions,
  8. ImportResult,
  9. LoadFileOptions,
  10. ProcessRawOptions,
  11. } from './IAssetImporter'
  12. import {IAsset, IFile} from './IAsset'
  13. import {IImporter, ILoader} from './IImporter'
  14. import {Importer} from './Importer'
  15. import {SimpleJSONLoader} from './import'
  16. import {parseFileExtension} from 'ts-browser-helpers'
  17. import {IObject3D} from '../core'
  18. export type IAssetImporterEvent = Event&{
  19. type: IAssetImporterEventTypes,
  20. data?: ImportResult, options?: ProcessRawOptions,
  21. path?: string, progress?: number, state?: string, error?: any
  22. files?: Map<string, IFile>
  23. url?: string, loaded?: number, total?: number
  24. loader?: ILoader,
  25. }
  26. /**
  27. * Asset Importer
  28. *
  29. * Utility class to import assets from local files, blobs, urls, etc.
  30. * Used in {@link AssetManager} to import assets.
  31. * Acts as a wrapper over three.js LoadingManager and adds support for dynamically loading loaders, caching assets, better event dispatching and file tracking.
  32. * @category Asset Manager
  33. */
  34. export class AssetImporter extends EventDispatcher<IAssetImporterEvent, IAssetImporterEventTypes> implements IAssetImporter {
  35. private _loadingManager: LoadingManager
  36. private _logger = console.log
  37. // Used when loading multiple files at once.
  38. protected _rootContext?: {path: string, rootUrl: string, /* baseUrl: string;*/}
  39. private _loaderCache: {loader: ILoader, ext: string[], mime: string[]}[] = []
  40. private _fileDatabase: Map<string, IFile> = new Map<string, IFile>()
  41. private _cachedAssets: IAsset[] = []
  42. readonly importers: IImporter[] = [
  43. // new Importer(VideoTextureLoader, ['mp4', 'ogg', 'mov', 'data:video'], false),
  44. new Importer(SimpleJSONLoader, ['json', 'vjson'], ['application/json'], false),
  45. new Importer(FileLoader, ['txt'], ['text/plain'], false),
  46. // new Importer(RGBEPNGLoader, ['rgbe.png', 'hdr.png', 'hdrpng'], ['image/png+rgbe'], false), // todo: not working on windows?
  47. // new Importer(LUTCubeLoader2, ['cube'], false),
  48. ]
  49. constructor(logging = false) {
  50. super()
  51. if (!logging) this._logger = () => {return}
  52. // this._viewer = viewer
  53. this._onLoad = this._onLoad.bind(this)
  54. this._onProgress = this._onProgress.bind(this)
  55. this._onError = this._onError.bind(this)
  56. this._onStart = this._onStart.bind(this)
  57. this._urlModifier = this._urlModifier.bind(this)
  58. this._loadingManager = new LoadingManager(this._onLoad, this._onProgress, this._onError)
  59. this._loadingManager.onStart = this._onStart
  60. this._loadingManager.setURLModifier(this._urlModifier)
  61. }
  62. get loadingManager(): LoadingManager {
  63. return this._loadingManager
  64. }
  65. get cachedAssets(): IAsset[] {
  66. return this._cachedAssets
  67. }
  68. addImporter(...importers: IImporter[]) {
  69. for (const importer of importers) {
  70. if (this.importers.includes(importer)) {
  71. console.warn('AssetImporter: Importer already added', importer)
  72. return
  73. }
  74. this.importers.push(importer)
  75. }
  76. }
  77. removeImporter(...importers: IImporter[]) {
  78. for (const importer of importers) {
  79. const index = this.importers.indexOf(importer)
  80. if (index >= 0) this.importers.splice(index, 1)
  81. }
  82. }
  83. // region import functions
  84. async import<T extends ImportResult|undefined = ImportResult>(assetOrPath?: string | IAsset | IAsset[] | File | File[], options?: ImportAssetOptions): Promise<(T|undefined)[]> {
  85. if (!assetOrPath) return []
  86. if (Array.isArray(assetOrPath)) return (await Promise.all(assetOrPath.map(async a => this.import<T>(a, options)))).flat(1)
  87. if (assetOrPath instanceof File) return await this.importFile<T>(assetOrPath, options)
  88. if (typeof assetOrPath === 'object') return await this.importAsset<T>(assetOrPath, options)
  89. if (typeof assetOrPath === 'string') return await this.importPath<T>(assetOrPath, options)
  90. console.error('AssetImporter: Invalid asset or path', assetOrPath)
  91. return []
  92. }
  93. async importSingle<T extends ImportResult|undefined = ImportResult>(asset?: IAsset | string, options?: ImportAssetOptions): Promise<T|undefined> {
  94. return (await this.import<T>(asset, options))?.[0]
  95. }
  96. async importPath<T extends ImportResult|undefined = ImportResult|undefined>(path: string, options: ImportAssetOptions = {}): Promise<T[]> {
  97. const op = {...options}
  98. delete op.pathOverride
  99. delete op.forceImport
  100. delete op.reimportDisposed
  101. delete op.fileHandler
  102. delete op.importedFile
  103. const opts = JSON.stringify(op)
  104. const cached = this._cachedAssets.find(a => a.path === path && a._options === opts)
  105. let asset: IAsset
  106. if (cached) asset = cached
  107. else asset = {path}
  108. asset._options = opts
  109. if (options.importedFile) asset.file = options.importedFile
  110. return await this.importAsset(asset, options)
  111. }
  112. // import and process an IAsset
  113. async importAsset<T extends ImportResult|undefined = ImportResult|undefined>(asset?: IAsset, options: ImportAssetOptions = {}, onDownloadProgress?: (e:ProgressEvent)=>void): Promise<T[]> {
  114. if (!asset) return []
  115. if (!asset.path && !asset.file && !options.pathOverride) {
  116. return [asset as any] // maybe already imported asset
  117. }
  118. // Cache the asset reference if it is not already cached
  119. if (!this._cachedAssets.includes(asset)) {
  120. if (Object.entries(asset).length === 1 && asset.path) {
  121. const ca = this._cachedAssets.find(value => value.path === asset.path)
  122. if (ca) Object.assign(asset, ca)
  123. }
  124. const ca = this._cachedAssets.findIndex(value => value.path === asset.path)
  125. if (ca >= 0) this._cachedAssets.splice(ca, 1)
  126. this._cachedAssets.push(asset)
  127. }
  128. let result: any = asset?.preImported
  129. if (!result && asset?.preImportedRaw) {
  130. result = await asset.preImportedRaw
  131. }
  132. const path = options.pathOverride || asset.path
  133. // console.log(result)
  134. if (!options.forceImport && result) {
  135. const results = await this.processRaw<T>(result, options, path) // just in case its not processed. Internal check is done to ensure it's not processed twice
  136. // let isDisposed = false // if any of the objects is disposed
  137. // for (const r of results) {
  138. // // todo: check if this is still required.
  139. // if ((r as RootSceneImportResult)?.userData?.rootSceneModelRoot) { // in case processImported is false we need a special case check here
  140. // if (r?.children?.find((c: any) => c.__disposed)) {
  141. // isDisposed = true
  142. // break
  143. // }
  144. // }
  145. // if (r && !r.__disposed) continue // todo add __disposed to object, material, texture, etc
  146. // isDisposed = true
  147. // break
  148. // }
  149. // todo: should we check if any of it's children is disposed ?
  150. // if (!isDisposed || options.reimportDisposed === false)
  151. return results
  152. }
  153. // todo: add support to get cloned asset? if we want to import multiple times and everytime return a cloned asset
  154. asset.preImportedRaw = this._loadFile(path, typeof asset.file?.arrayBuffer === 'function' ? asset.file : undefined, options, onDownloadProgress)
  155. result = await asset.preImportedRaw
  156. if (result) result = await this.processRaw(result, options, path)
  157. if (result) {
  158. if (options.processRaw !== false) asset.preImported = result
  159. const arrs: any[] = []
  160. if (Array.isArray(result)) arrs.push(...result)
  161. else {
  162. if (result.userData?.rootSceneModelRoot) arrs.push(...result.children)
  163. else arrs.push(result)
  164. }
  165. // remove preImportedRaw when any of the assets is disposed. This is to prevent memory leaks
  166. arrs.forEach(r=>r.addEventListener?.('dispose', () => { // todo: recheck after dispose logic change
  167. if (asset?.preImportedRaw) asset.preImportedRaw = undefined
  168. if (asset?.preImported) asset.preImported = undefined
  169. }))
  170. }
  171. return result
  172. }
  173. async importFile<T extends ImportResult|undefined = ImportResult|undefined>(file?: File, options: ImportAssetOptions = {}, onDownloadProgress?: (e:ProgressEvent)=>void): Promise<T[]> {
  174. if (!file) return []
  175. if (!(file instanceof File)) {
  176. console.error('AssetImporter: Invalid file', file)
  177. return []
  178. }
  179. return this.importAsset(this._cachedAssets.find(a=>a.file === file) ?? {
  180. path: file.name || file.webkitRelativePath, file,
  181. }, options, onDownloadProgress)
  182. }
  183. /**
  184. * Import multiple local files/blobs from a map of files, like when a local folder is loaded, or when multiple files are dropped.
  185. * @param files
  186. * @param options
  187. */
  188. async importFiles<T extends ImportResult|undefined=ImportResult|undefined>(files: Map<string, IFile>, options: ImportFilesOptions = {}): Promise<Map<string, T[]>> {
  189. const loaded = new Map<string, any>()
  190. let {allowedExtensions} = options
  191. if (allowedExtensions && allowedExtensions.length < 1) allowedExtensions = undefined
  192. if (files.size === 0) return loaded
  193. this.dispatchEvent({type: 'importFiles', files: files, state: 'start'})
  194. const baseFiles: string[] = []
  195. const altFiles: string[] = []
  196. // Note: mostly path === file.name
  197. files.forEach((file, path) => { // todo: handle only one file at the top
  198. this.registerFile(path, file)
  199. const ext = file.ext
  200. const mime = file.mime
  201. if ((ext || mime) && // todo: files with no extensions are not supported right now. This also includes __MacOSX
  202. (allowedExtensions?.includes((ext || mime || '').toLowerCase()) ?? true)) {
  203. if (this._isRootFile(ext)) baseFiles.push(path)
  204. else altFiles.push(path)
  205. }
  206. })
  207. if (baseFiles.length > 0) {
  208. for (const value of baseFiles) {
  209. let res = await this._loadFile(value, undefined, options)
  210. if (res) res = await this.processRaw(res, options, value)
  211. loaded.set(value, res)
  212. }
  213. } else {
  214. for (const value of altFiles) {
  215. let res = await this._loadFile(value, undefined, options)
  216. if (res) res = await this.processRaw(res, options, value)
  217. loaded.set(value, res)
  218. }
  219. // todo: handle no baseFiles
  220. }
  221. this.dispatchEvent({type: 'importFiles', files: files, state: 'end'})
  222. files.forEach((_, path) => this.unregisterFile(path))
  223. return loaded
  224. }
  225. // load a single file
  226. private async _loadFile(path: string, file?: IFile, options: LoadFileOptions = {}, onDownloadProgress?: (e: ProgressEvent)=>void): Promise<ImportResult | ImportResult[] | undefined> {
  227. if (file?.__loadedAsset) return file.__loadedAsset
  228. this.dispatchEvent({type: 'importFile', path, state:'downloading', progress: 0})
  229. let res: ImportResult | ImportResult[] | undefined
  230. try {
  231. const loader = this.registerFile(path, file)
  232. // const url = this.resolveURL(path) // todo: why is this required? maybe for query string?
  233. // const path2 = path.replace(/\?.*$/, '') // remove query string to find the handler properly
  234. // const loader = (options.fileHandler as ILoader) ?? this._getLoader(path2) ??
  235. // (file ? this._getLoader(file.name, file.ext, file.mime) : undefined)
  236. if (!loader) {
  237. throw new Error('AssetImporter: Unable to find loader for ' + path) // caught below
  238. }
  239. this._rootContext = {
  240. path,
  241. rootUrl: LoaderUtils.extractUrlBase(path),
  242. // baseUrl: LoaderUtils.extractUrlBase(url),
  243. }
  244. res = await loader.loadAsync(path + (options.queryString ? (path.includes('?') ? '&' : '?') + options.queryString : ''), (e)=>{
  245. if (onDownloadProgress) onDownloadProgress(e)
  246. this.dispatchEvent({
  247. type: 'importFile', path,
  248. state:'downloading',
  249. loadedBytes: e.loaded || undefined,
  250. totalBytes: e.total || undefined,
  251. progress: e.total > 0 ? e.loaded / e.total : 1,
  252. })
  253. })
  254. if (loader.transform) res = await loader.transform(res, options)
  255. this._rootContext = undefined
  256. this.dispatchEvent({type: 'importFile', path, state:'downloading', progress: 1})
  257. this.dispatchEvent({type: 'importFile', path, state: 'adding'})
  258. if (file)
  259. this._logger('AssetImporter: loaded', path)
  260. else
  261. this._logger('AssetImporter: downloaded', path)
  262. if (file)
  263. this.unregisterFile(path)
  264. } catch (e: any) {
  265. console.error('AssetImporter: Unable to import file', path, file)
  266. console.error(e)
  267. console.error(e?.stack)
  268. // throw e
  269. this.dispatchEvent({type: 'importFile', path, state: 'error', error: e})
  270. if (file)
  271. this.unregisterFile(path)
  272. return []
  273. }
  274. this.dispatchEvent({type: 'importFile', path, state: 'done'}) // todo: do this after processing?
  275. if (file) {
  276. file.__loadedAsset = res
  277. // todo: recheck below code after dispose logic change
  278. // Clear the reference __loadedAsset when any one asset is disposed.
  279. // it's a bit hacky to do this here, but it works for now. todo: move to a better place
  280. let ress: any[] = []
  281. if (Array.isArray(res)) ress = res.flat(2)
  282. else if ((<IObject3D>res)?.userData?.rootSceneModelRoot) ress.push(...(<IObject3D>res).children)
  283. else ress.push(res)
  284. for (const r of ress) r?.addEventListener?.('dispose', () => file.__loadedAsset = undefined)
  285. }
  286. if (res && typeof res === 'object' && !Array.isArray(res)) {
  287. res.__rootPath = path
  288. const f = file || this._fileDatabase.get(path)
  289. if (f) res.__rootBlob = f
  290. }
  291. return res
  292. }
  293. // endregion
  294. // region file database
  295. /**
  296. * Register a file in the database and return a loader for it. If the loader does not exist, it will be created.
  297. * @param path
  298. * @param file
  299. */
  300. registerFile(path: string, file?: IFile): ILoader | undefined {
  301. const isData = path.startsWith('data:') || false
  302. if (!isData) path = path.replace(/\?.*$/, '') // remove query string
  303. const ext = isData ? undefined : file?.ext ?? parseFileExtension(file?.name ?? path)?.toLowerCase()
  304. const mime = file?.mime ?? isData ? path.slice(0, path.indexOf(';')).split(':')[1] || undefined : undefined
  305. if (file) {
  306. if (file.name === undefined) (file as any).name = path
  307. if (!file.ext) file.ext = ext
  308. if (!file.mime) file.mime = mime
  309. if (this._fileDatabase.has(path)) {
  310. console.warn('AssetImporter: File already registered, replacing', path)
  311. this.unregisterFile(path)
  312. }
  313. this._fileDatabase.set(path, file)
  314. }
  315. return this._getLoader(path) || this._createLoader(path, ext, mime)
  316. }
  317. /**
  318. * Remove a file from the database and revoke the object url if it exists.
  319. * @param path
  320. */
  321. unregisterFile(path: string) {
  322. path = path.replace(/\?.*$/, '') // remove query string
  323. const file = this._fileDatabase.get(path)
  324. if (file?.objectUrl) {
  325. URL.revokeObjectURL(file.objectUrl)
  326. file.objectUrl = undefined
  327. }
  328. if (file) this._fileDatabase.delete(path)
  329. }
  330. // endregion
  331. // region processRaw
  332. public async processRaw<T extends (ImportResult|undefined) = ImportResult>(res: T|T[], options: ProcessRawOptions, path?: string): Promise<T[]> {
  333. if (!res) return []
  334. // legacy
  335. if (options.processImported !== undefined) {
  336. console.error('AssetImporter: processImported is deprecated, use processRaw instead')
  337. options.processRaw = options.processImported
  338. }
  339. if (Array.isArray(res)) {
  340. const r: any[] = []
  341. for (const re of res) { // todo: can we parallelize?
  342. r.push(...await this.processRaw(re, options, path))
  343. }
  344. return r
  345. }
  346. if (options.processRaw === false) return [res]
  347. if (res.assetImporterProcessed && !options.forceImporterReprocess) return [res]
  348. this.dispatchEvent({type: 'processRawStart', data: res, options, path})
  349. // for testing only
  350. if (res.isTexture && options._testDataTextureComplete) {
  351. // if some data textures are not loading correctly, should not ideally be required
  352. if (res.isDataTexture && res.image?.data) res.image.complete = true
  353. if (res.image?.complete) res.needsUpdate = true
  354. }
  355. if (res.userData) {
  356. const userData: IImportResultUserData = res.userData
  357. const rootPath = res.__rootPath
  358. if (!userData.rootPath && rootPath && !rootPath.startsWith('blob:') && !rootPath.startsWith('/'))
  359. userData.rootPath = rootPath
  360. if (res.__rootBlob) {
  361. userData.__sourceBlob = res.__rootBlob
  362. if (userData.__needsSourceBuffer) { // set __sourceBuffer here if required during serialize later on, __needsSourceBuffer can be set in asset loaders
  363. userData.__sourceBuffer = await res.__rootBlob.arrayBuffer()
  364. delete userData.__needsSourceBuffer
  365. }
  366. }
  367. }
  368. // if (res.assetType) // todo: why if?
  369. res.assetImporterProcessed = true // this should not be put in userData
  370. this.dispatchEvent({type: 'processRaw', data: res, options, path})
  371. // special for zip files. ZipLoader gives this
  372. if ((<any>res) instanceof Map && options.autoImportZipContents !== false) {
  373. // todo: should we pass in onProgress from outside?
  374. return [...(await this.importFiles<T>(<any>res, options)).values()].flat()
  375. }
  376. return [res]
  377. }
  378. public async processRawSingle<T extends (ImportResult|undefined) = ImportResult>(res: T, options: ProcessRawOptions, path?: string): Promise<T> {
  379. return (await this.processRaw(res, options, path))[0]
  380. }
  381. // endregion
  382. // region disposal
  383. dispose(): void {
  384. this.clearCache()
  385. // this._processors?.dispose()
  386. // this._loadingManager.dispose // todo
  387. }
  388. /**
  389. * Clear memory asset and loader cache. Browser cache and custom cache storage is not cleared with this.
  390. */
  391. clearCache(): void {
  392. this._cachedAssets = []
  393. this.unregisterAllFiles()
  394. this.clearLoaderCache()
  395. }
  396. unregisterAllFiles(): void {
  397. const keys = [...this._fileDatabase.keys()]
  398. for (const key of keys) {
  399. this.unregisterFile(key)
  400. }
  401. }
  402. clearLoaderCache(): void {
  403. for (const lc of this._loaderCache) {
  404. lc.loader?.dispose && lc.loader?.dispose()
  405. }
  406. this._loaderCache = []
  407. }
  408. // endregion
  409. // region utils
  410. resolveURL(url: string): string {
  411. return this._loadingManager.resolveURL(url)
  412. }
  413. protected _urlModifier(url: string) {
  414. let normalizedURL = decodeURI(url)
  415. const rootUrl = this._rootContext?.rootUrl
  416. if (!normalizedURL.includes('://') && rootUrl && !normalizedURL.startsWith(rootUrl))
  417. normalizedURL = rootUrl + normalizedURL
  418. normalizedURL = normalizedURL.replace('./', '') // remove ./
  419. normalizedURL = normalizedURL.replace(/^(\/\/)/, '/') // fix for start with //
  420. // remove query string
  421. normalizedURL = normalizedURL.replace(/\?.*$/, '')
  422. const file = this._fileDatabase.get(normalizedURL)
  423. if (!file) return url
  424. const ext = file.ext
  425. if (!ext) {
  426. console.error('Unable to determine file extension', file)
  427. return url
  428. }
  429. if (!file.objectUrl) file.objectUrl = URL.createObjectURL(file) + '#' + normalizedURL
  430. return file.objectUrl
  431. }
  432. private _isRootFile(ext?: string, mime?: string) {
  433. mime = mime?.toLowerCase()
  434. ext = ext?.toLowerCase()
  435. return this.importers.find(value => value.root && (
  436. ext && value.ext.includes(ext.toLowerCase()) ||
  437. mime && value.mime.includes(mime.toLowerCase())
  438. )) != null
  439. }
  440. // get an importer that can create a loader
  441. private _getImporter(name:string, ext?:string, mime?: string, isRoot = false): IImporter | undefined {
  442. mime = mime?.toLowerCase()
  443. ext = ext?.toLowerCase()
  444. return this.importers.find(importer => {
  445. if (isRoot && !importer.root) return false
  446. if (mime && importer.mime?.find(m => mime === m)) return true
  447. if (importer.ext.find(iext =>
  448. ext && iext === ext
  449. || name?.toLowerCase()?.endsWith('.' + iext)
  450. || iext?.startsWith('data:') && name?.startsWith(iext))) return true
  451. return false
  452. })
  453. }
  454. // get a loader that can load a file.
  455. private _getLoader(name?:string, ext?:string, mime?: string): ILoader | undefined {
  456. if (!ext && !mime && name) ext = parseFileExtension(name).toLowerCase()
  457. mime = mime?.toLowerCase()
  458. ext = ext?.toLowerCase()
  459. return (name ? this._loadingManager.getHandler(name) as ILoader : undefined)
  460. || this._loaderCache.find((lc)=> ext && lc.ext.includes(ext) || mime && lc.mime.includes(mime))?.loader
  461. }
  462. private _createLoader(name:string, ext?:string, mime?: string): ILoader | undefined { // todo: remove/destroy loader.
  463. const importer = this._getImporter(name, ext, mime)
  464. if (!importer) return undefined
  465. const loader = importer.ctor(this)
  466. if (!loader) return undefined
  467. importer.ext.forEach(iext => {
  468. const regex = new RegExp(iext.startsWith('data:') ? '^' + iext + '\\/' : '\\.' + iext + '$', 'i')
  469. this._loadingManager.addHandler(regex, loader)
  470. })
  471. importer.mime?.forEach(imime => {
  472. const regex = new RegExp('^data:' + imime + '$', 'i')
  473. this._loadingManager.addHandler(regex, loader)
  474. })
  475. this._loaderCache.push({loader, ext: importer.ext, mime: importer.mime})
  476. this.dispatchEvent({type: 'loaderCreate', loader})
  477. return loader
  478. }
  479. addEventListener<T extends IAssetImporterEvent['type'] & IAssetImporterEventTypes>(type: T, listener: EventListener<IAssetImporterEvent, T, this>) {
  480. super.addEventListener(type, listener)
  481. if (type === 'loaderCreate') {
  482. for (const loaderCacheElement of this._loaderCache) {
  483. this.dispatchEvent({type: 'loaderCreate', loader: loaderCacheElement.loader})
  484. }
  485. }
  486. }
  487. // endregion
  488. // region Loader Event Dispatchers
  489. protected _onLoad() {
  490. this.dispatchEvent({type: 'onLoad'})
  491. }
  492. protected _onProgress(url: string, loaded: number, total: number) {
  493. this.dispatchEvent({type: 'onProgress', url, loaded, total})
  494. }
  495. protected _onError(url: string) {
  496. this.dispatchEvent({type: 'onError', url})
  497. }
  498. protected _onStart(url: string, loaded: number, total: number) {
  499. this.dispatchEvent({type: 'onStart', url, loaded, total})
  500. }
  501. // endregion
  502. // region deprecated
  503. /**
  504. * @deprecated use {@link processRaw} instead
  505. * @param res
  506. * @param options
  507. */
  508. public async processImported(res: any, options: ProcessRawOptions, path?: string): Promise<any[]> {
  509. console.error('processImported is deprecated. Use processRaw instead.')
  510. return await this.processRaw(res, options, path)
  511. }
  512. // endregion
  513. }