From 93c4a868ea81782aa53f9ca5588f4ceef6b1ffb6 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 8 Aug 2024 17:15:02 -0700 Subject: [PATCH] Extract SceneLoader state and functions from static class to module --- packages/dev/core/src/Loading/sceneLoader.ts | 1990 ++++++++--------- packages/dev/loaders/src/OBJ/objFileLoader.ts | 8 +- .../dev/loaders/src/SPLAT/splatFileLoader.ts | 8 +- packages/dev/loaders/src/STL/stlFileLoader.ts | 6 +- .../dev/loaders/src/glTF/glTFFileLoader.ts | 6 +- .../public/@babylonjs/viewer-alpha/readme.md | 17 +- .../viewer-alpha/generateCoverageReports.mjs | 2 +- packages/tools/viewer-alpha/package.json | 4 +- packages/tools/viewer-alpha/src/viewer.ts | 4 +- ...FlameChart.js => folderSizeFlameChart.mjs} | 24 +- scripts/queryRollupStats.js | 35 +- 11 files changed, 995 insertions(+), 1109 deletions(-) rename scripts/{folderSizeFlameChart.js => folderSizeFlameChart.mjs} (89%) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index ca80dd909b3..5d7b300d6c9 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -22,7 +22,7 @@ import { RuntimeError, ErrorCodes } from "../Misc/error"; import type { ISpriteManager } from "../Sprites/spriteManager"; import { RandomGUID } from "../Misc/guid"; import { Engine } from "../Engines/engine"; -import { AbstractEngine } from "../Engines/abstractEngine"; +import type { AbstractEngine } from "../Engines/abstractEngine"; /** * Type used for the success callback of ImportMesh @@ -478,333 +478,1002 @@ function isFile(value: unknown): value is File { return !!(value as File).name; } -/** - * Class used to load scene from various file formats using registered plugins - * @see https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes - */ -export class SceneLoader { - /** - * No logging while loading - */ - public static readonly NO_LOGGING = Constants.SCENELOADER_NO_LOGGING; +const _onPluginActivatedObservable = new Observable(); +const _registeredPlugins: { [extension: string]: IRegisteredPlugin } = {}; +let _showingLoadingScreen = false; - /** - * Minimal logging while loading - */ - public static readonly MINIMAL_LOGGING = Constants.SCENELOADER_MINIMAL_LOGGING; +function _getDefaultPlugin(): IRegisteredPlugin { + return _registeredPlugins[".babylon"]; +} - /** - * Summary logging while loading - */ - public static readonly SUMMARY_LOGGING = Constants.SCENELOADER_SUMMARY_LOGGING; +function _getPluginForExtension(extension: string): IRegisteredPlugin { + const registeredPlugin = _registeredPlugins[extension]; + if (registeredPlugin) { + return registeredPlugin; + } + Logger.Warn( + "Unable to find a plugin to load " + + extension + + " files. Trying to use .babylon default plugin. To load from a specific filetype (eg. gltf) see: https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes" + ); + return _getDefaultPlugin(); +} - /** - * Detailed logging while loading - */ - public static readonly DETAILED_LOGGING = Constants.SCENELOADER_DETAILED_LOGGING; +function _isPluginForExtensionAvailable(extension: string): boolean { + return !!_registeredPlugins[extension]; +} - /** - * Gets or sets a boolean indicating if entire scene must be loaded even if scene contains incremental data - */ - public static get ForceFullSceneLoadingForIncremental() { - return SceneLoaderFlags.ForceFullSceneLoadingForIncremental; +function _getPluginForDirectLoad(data: string): IRegisteredPlugin { + for (const extension in _registeredPlugins) { + const plugin = _registeredPlugins[extension].plugin; + + if (plugin.canDirectLoad && plugin.canDirectLoad(data)) { + return _registeredPlugins[extension]; + } } - public static set ForceFullSceneLoadingForIncremental(value: boolean) { - SceneLoaderFlags.ForceFullSceneLoadingForIncremental = value; + return _getDefaultPlugin(); +} + +function _getPluginForFilename(sceneFilename: string): IRegisteredPlugin { + const queryStringPosition = sceneFilename.indexOf("?"); + + if (queryStringPosition !== -1) { + sceneFilename = sceneFilename.substring(0, queryStringPosition); } - /** - * Gets or sets a boolean indicating if loading screen must be displayed while loading a scene - */ - public static get ShowLoadingScreen(): boolean { - return SceneLoaderFlags.ShowLoadingScreen; + const dotPosition = sceneFilename.lastIndexOf("."); + + const extension = sceneFilename.substring(dotPosition, sceneFilename.length).toLowerCase(); + return _getPluginForExtension(extension); +} + +function _getDirectLoad(sceneFilename: string): Nullable { + if (sceneFilename.substr(0, 5) === "data:") { + return sceneFilename.substr(5); } - public static set ShowLoadingScreen(value: boolean) { - SceneLoaderFlags.ShowLoadingScreen = value; + return null; +} + +function _formatErrorMessage(fileInfo: IFileInfo, message?: string, exception?: any): string { + const fromLoad = fileInfo.rawData ? "binary data" : fileInfo.url; + let errorMessage = "Unable to load from " + fromLoad; + + if (message) { + errorMessage += `: ${message}`; + } else if (exception) { + errorMessage += `: ${exception}`; } - /** - * Defines the current logging level (while loading the scene) - * @ignorenaming - */ - // eslint-disable-next-line @typescript-eslint/naming-convention - public static get loggingLevel(): number { - return SceneLoaderFlags.loggingLevel; + return errorMessage; +} + +function _loadData( + fileInfo: IFileInfo, + scene: Scene, + onSuccess: (plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync, data: unknown, responseURL?: string) => void, + onProgress: ((event: ISceneLoaderProgressEvent) => void) | undefined, + onError: (message?: string, exception?: any) => void, + onDispose: () => void, + pluginExtension: Nullable, + name: string, + pluginOptions: PluginOptions +): Nullable { + const directLoad = _getDirectLoad(fileInfo.url); + + if (fileInfo.rawData && !pluginExtension) { + // eslint-disable-next-line no-throw-literal + throw "When using ArrayBufferView to load data the file extension must be provided."; } - // eslint-disable-next-line @typescript-eslint/naming-convention - public static set loggingLevel(value: number) { - SceneLoaderFlags.loggingLevel = value; + const registeredPlugin = pluginExtension ? _getPluginForExtension(pluginExtension) : directLoad ? _getPluginForDirectLoad(fileInfo.url) : _getPluginForFilename(fileInfo.url); + + if (pluginOptions?.[registeredPlugin.plugin.name]?.enabled === false) { + throw new Error(`The '${registeredPlugin.plugin.name}' plugin is disabled via the loader options passed to the loading operation.`); } - /** - * Gets or set a boolean indicating if matrix weights must be cleaned upon loading - */ - public static get CleanBoneMatrixWeights(): boolean { - return SceneLoaderFlags.CleanBoneMatrixWeights; + if (fileInfo.rawData && !registeredPlugin.isBinary) { + // eslint-disable-next-line no-throw-literal + throw "Loading from ArrayBufferView can not be used with plugins that don't support binary loading."; } - public static set CleanBoneMatrixWeights(value: boolean) { - SceneLoaderFlags.CleanBoneMatrixWeights = value; + // For plugin factories, the plugin is instantiated on each SceneLoader operation. This makes options handling + // much simpler as we can just pass the options to the factory, rather than passing options through to every possible + // plugin call. Given this, options are only supported for plugins that provide a factory function. + const plugin: IRegisteredPlugin["plugin"] = registeredPlugin.plugin.createPlugin?.(pluginOptions ?? {}) ?? registeredPlugin.plugin; + if (!plugin) { + // eslint-disable-next-line no-throw-literal + throw `The loader plugin corresponding to the '${pluginExtension}' file type has not been found. If using es6, please import the plugin you wish to use before.`; } - // Members + _onPluginActivatedObservable.notifyObservers(plugin); + + // Check if we have a direct load url. If the plugin is registered to handle + // it or it's not a base64 data url, then pass it through the direct load path. + if (directLoad && ((plugin.canDirectLoad && plugin.canDirectLoad(fileInfo.url)) || !IsBase64DataUrl(fileInfo.url))) { + if (plugin.directLoad) { + const result = plugin.directLoad(scene, directLoad); + if (result instanceof Promise) { + result + .then((data: unknown) => { + onSuccess(plugin, data); + }) + .catch((error: any) => { + onError("Error in directLoad of _loadData: " + error, error); + }); + } else { + onSuccess(plugin, result); + } + } else { + onSuccess(plugin, directLoad); + } + return plugin; + } - /** - * Event raised when a plugin is used to load a scene - */ - public static OnPluginActivatedObservable = new Observable(); + const useArrayBuffer = registeredPlugin.isBinary; - private static _RegisteredPlugins: { [extension: string]: IRegisteredPlugin } = {}; + const dataCallback = (data: unknown, responseURL?: string) => { + if (scene.isDisposed) { + onError("Scene has been disposed"); + return; + } - private static _ShowingLoadingScreen = false; + onSuccess(plugin, data, responseURL); + }; - /** - * Gets the default plugin (used to load Babylon files) - * @returns the .babylon plugin - */ - public static GetDefaultPlugin(): IRegisteredPlugin { - return SceneLoader._RegisteredPlugins[".babylon"]; - } + let request: Nullable = null; + let pluginDisposed = false; + plugin.onDisposeObservable?.add(() => { + pluginDisposed = true; - private static _GetPluginForExtension(extension: string): IRegisteredPlugin { - const registeredPlugin = SceneLoader._RegisteredPlugins[extension]; - if (registeredPlugin) { - return registeredPlugin; + if (request) { + request.abort(); + request = null; } - Logger.Warn( - "Unable to find a plugin to load " + - extension + - " files. Trying to use .babylon default plugin. To load from a specific filetype (eg. gltf) see: https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes" - ); - return SceneLoader.GetDefaultPlugin(); - } - private static _GetPluginForDirectLoad(data: string): IRegisteredPlugin { - for (const extension in SceneLoader._RegisteredPlugins) { - const plugin = SceneLoader._RegisteredPlugins[extension].plugin; + onDispose(); + }); - if (plugin.canDirectLoad && plugin.canDirectLoad(data)) { - return SceneLoader._RegisteredPlugins[extension]; - } + const manifestChecked = () => { + if (pluginDisposed) { + return; } - return SceneLoader.GetDefaultPlugin(); - } + const errorCallback = (request?: WebRequest, exception?: LoadFileError) => { + onError(request?.statusText, exception); + }; + + if (!plugin.loadFile && fileInfo.rawData) { + // eslint-disable-next-line no-throw-literal + throw "Plugin does not support loading ArrayBufferView."; + } - private static _GetPluginForFilename(sceneFilename: string): IRegisteredPlugin { - const queryStringPosition = sceneFilename.indexOf("?"); + request = plugin.loadFile + ? plugin.loadFile(scene, fileInfo.rawData || fileInfo.file || fileInfo.url, fileInfo.rootUrl, dataCallback, onProgress, useArrayBuffer, errorCallback, name) + : scene._loadFile(fileInfo.file || fileInfo.url, dataCallback, onProgress, true, useArrayBuffer, errorCallback); + }; - if (queryStringPosition !== -1) { - sceneFilename = sceneFilename.substring(0, queryStringPosition); + const engine = scene.getEngine(); + let canUseOfflineSupport = engine.enableOfflineSupport; + if (canUseOfflineSupport) { + // Also check for exceptions + let exceptionFound = false; + for (const regex of scene.disableOfflineSupportExceptionRules) { + if (regex.test(fileInfo.url)) { + exceptionFound = true; + break; + } } - const dotPosition = sceneFilename.lastIndexOf("."); + canUseOfflineSupport = !exceptionFound; + } - const extension = sceneFilename.substring(dotPosition, sceneFilename.length).toLowerCase(); - return SceneLoader._GetPluginForExtension(extension); + if (canUseOfflineSupport && Engine.OfflineProviderFactory) { + // Checking if a manifest file has been set for this scene and if offline mode has been requested + scene.offlineProvider = Engine.OfflineProviderFactory(fileInfo.url, manifestChecked, engine.disableManifestCheck); + } else { + manifestChecked(); } - private static _GetDirectLoad(sceneFilename: string): Nullable { - if (sceneFilename.substr(0, 5) === "data:") { - return sceneFilename.substr(5); + return plugin; +} + +function _getFileInfo(rootUrl: string, sceneSource: SceneSource): Nullable { + let url: string; + let name: string; + let file: Nullable = null; + let rawData: Nullable = null; + + if (!sceneSource) { + url = rootUrl; + name = Tools.GetFilename(rootUrl); + rootUrl = Tools.GetFolderPath(rootUrl); + } else if (isFile(sceneSource)) { + url = `file:${sceneSource.name}`; + name = sceneSource.name; + file = sceneSource; + } else if (ArrayBuffer.isView(sceneSource)) { + url = ""; + name = RandomGUID(); + rawData = sceneSource; + } else if (sceneSource.startsWith("data:")) { + url = sceneSource; + name = ""; + } else if (rootUrl) { + const filename = sceneSource; + if (filename.substr(0, 1) === "/") { + Tools.Error("Wrong sceneFilename parameter"); + return null; } - return null; + url = rootUrl + filename; + name = filename; + } else { + url = sceneSource; + name = Tools.GetFilename(sceneSource); + rootUrl = Tools.GetFolderPath(sceneSource); } - private static _FormatErrorMessage(fileInfo: IFileInfo, message?: string, exception?: any): string { - const fromLoad = fileInfo.rawData ? "binary data" : fileInfo.url; - let errorMessage = "Unable to load from " + fromLoad; + return { + url: url, + rootUrl: rootUrl, + name: name, + file: file, + rawData, + }; +} - if (message) { - errorMessage += `: ${message}`; - } else if (exception) { - errorMessage += `: ${exception}`; - } +/** + * Adds a new plugin to the list of registered plugins + * @param plugin defines the plugin to add + */ +export function registerSceneLoaderPlugin(plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync): void { + if (typeof plugin.extensions === "string") { + const extension = plugin.extensions; + _registeredPlugins[extension.toLowerCase()] = { + plugin: plugin, + isBinary: false, + }; + } else { + const extensions = plugin.extensions; + Object.keys(extensions).forEach((extension) => { + _registeredPlugins[extension.toLowerCase()] = { + plugin: plugin, + isBinary: extensions[extension].isBinary, + }; + }); + } +} - return errorMessage; +function _importMesh( + meshNames: string | readonly string[] | null | undefined, + rootUrl: string, + sceneFilename: SceneSource = "", + scene: Nullable = EngineStore.LastCreatedScene, + onSuccess: Nullable = null, + onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, + onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, + pluginExtension: Nullable = null, + name = "", + pluginOptions: PluginOptions = {} +): Nullable { + if (!scene) { + Logger.Error("No scene available to import mesh to"); + return null; } - private static _LoadData( - fileInfo: IFileInfo, - scene: Scene, - onSuccess: (plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync, data: unknown, responseURL?: string) => void, - onProgress: ((event: ISceneLoaderProgressEvent) => void) | undefined, - onError: (message?: string, exception?: any) => void, - onDispose: () => void, - pluginExtension: Nullable, - name: string, - pluginOptions: PluginOptions - ): Nullable { - const directLoad = SceneLoader._GetDirectLoad(fileInfo.url); + const fileInfo = _getFileInfo(rootUrl, sceneFilename); + if (!fileInfo) { + return null; + } - if (fileInfo.rawData && !pluginExtension) { - // eslint-disable-next-line no-throw-literal - throw "When using ArrayBufferView to load data the file extension must be provided."; - } + const loadingToken = {}; + scene.addPendingData(loadingToken); - const registeredPlugin = pluginExtension - ? SceneLoader._GetPluginForExtension(pluginExtension) - : directLoad - ? SceneLoader._GetPluginForDirectLoad(fileInfo.url) - : SceneLoader._GetPluginForFilename(fileInfo.url); + const disposeHandler = () => { + scene.removePendingData(loadingToken); + }; - if (pluginOptions?.[registeredPlugin.plugin.name]?.enabled === false) { - throw new Error(`The '${registeredPlugin.plugin.name}' plugin is disabled via the loader options passed to the loading operation.`); - } + const errorHandler = (message?: string, exception?: any) => { + const errorMessage = _formatErrorMessage(fileInfo, message, exception); - if (fileInfo.rawData && !registeredPlugin.isBinary) { - // eslint-disable-next-line no-throw-literal - throw "Loading from ArrayBufferView can not be used with plugins that don't support binary loading."; + if (onError) { + onError(scene, errorMessage, new RuntimeError(errorMessage, ErrorCodes.SceneLoaderError, exception)); + } else { + Logger.Error(errorMessage); + // should the exception be thrown? } - // For plugin factories, the plugin is instantiated on each SceneLoader operation. This makes options handling - // much simpler as we can just pass the options to the factory, rather than passing options through to every possible - // plugin call. Given this, options are only supported for plugins that provide a factory function. - const plugin: IRegisteredPlugin["plugin"] = registeredPlugin.plugin.createPlugin?.(pluginOptions ?? {}) ?? registeredPlugin.plugin; - if (!plugin) { - // eslint-disable-next-line no-throw-literal - throw `The loader plugin corresponding to the '${pluginExtension}' file type has not been found. If using es6, please import the plugin you wish to use before.`; + disposeHandler(); + }; + + const progressHandler = onProgress + ? (event: ISceneLoaderProgressEvent) => { + try { + onProgress(event); + } catch (e) { + errorHandler("Error in onProgress callback: " + e, e); + } + } + : undefined; + + const successHandler: SceneLoaderSuccessCallback = (meshes, particleSystems, skeletons, animationGroups, transformNodes, geometries, lights, spriteManagers) => { + scene.importedMeshesFiles.push(fileInfo.url); + + if (onSuccess) { + try { + onSuccess(meshes, particleSystems, skeletons, animationGroups, transformNodes, geometries, lights, spriteManagers); + } catch (e) { + errorHandler("Error in onSuccess callback: " + e, e); + } } - SceneLoader.OnPluginActivatedObservable.notifyObservers(plugin); - - // Check if we have a direct load url. If the plugin is registered to handle - // it or it's not a base64 data url, then pass it through the direct load path. - if (directLoad && ((plugin.canDirectLoad && plugin.canDirectLoad(fileInfo.url)) || !IsBase64DataUrl(fileInfo.url))) { - if (plugin.directLoad) { - const result = plugin.directLoad(scene, directLoad); - if (result instanceof Promise) { - result - .then((data: unknown) => { - onSuccess(plugin, data); - }) - .catch((error: any) => { - onError("Error in directLoad of _loadData: " + error, error); - }); - } else { - onSuccess(plugin, result); + scene.removePendingData(loadingToken); + }; + + return _loadData( + fileInfo, + scene, + (plugin, data, responseURL) => { + if (plugin.rewriteRootURL) { + fileInfo.rootUrl = plugin.rewriteRootURL(fileInfo.rootUrl, responseURL); + } + + if ((plugin as ISceneLoaderPlugin).importMesh) { + const syncedPlugin = plugin; + const meshes: AbstractMesh[] = []; + const particleSystems: IParticleSystem[] = []; + const skeletons: Skeleton[] = []; + + if (!syncedPlugin.importMesh(meshNames, scene, data, fileInfo.rootUrl, meshes, particleSystems, skeletons, errorHandler)) { + return; } + + scene.loadingPluginName = plugin.name; + successHandler(meshes, particleSystems, skeletons, [], [], [], [], []); } else { - onSuccess(plugin, directLoad); + const asyncedPlugin = plugin; + asyncedPlugin + .importMeshAsync(meshNames, scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) + .then((result) => { + scene.loadingPluginName = plugin.name; + successHandler( + result.meshes, + result.particleSystems, + result.skeletons, + result.animationGroups, + result.transformNodes, + result.geometries, + result.lights, + result.spriteManagers + ); + }) + .catch((error) => { + errorHandler(error.message, error); + }); } - return plugin; + }, + progressHandler, + errorHandler, + disposeHandler, + pluginExtension, + name, + pluginOptions + ); +} + +/** + * Import meshes into a scene + * @experimental + * @param source a string that defines the name of the scene file, or starts with "data:" following by the stringified version of the scene, or a File object, or an ArrayBufferView + * @param scene the instance of BABYLON.Scene to append to + * @param options an object that configures aspects of how the scene is loaded + * @returns The loaded list of imported meshes, particle systems, skeletons, and animation groups + */ +export function importMeshAsync(source: SceneSource, scene: Scene, options?: ImportMeshOptions): Promise { + const { meshNames, rootUrl = "", onProgress, pluginExtension, name, pluginOptions } = options ?? {}; + return _importMeshAsync(meshNames, rootUrl, source, scene, onProgress, pluginExtension, name, pluginOptions); +} + +function _importMeshAsync( + meshNames: string | readonly string[] | null | undefined, + rootUrl: string, + sceneFilename?: SceneSource, + scene?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + name?: string, + pluginOptions?: PluginOptions +): Promise { + return new Promise((resolve, reject) => { + _importMesh( + meshNames, + rootUrl, + sceneFilename, + scene, + (meshes, particleSystems, skeletons, animationGroups, transformNodes, geometries, lights, spriteManagers) => { + resolve({ + meshes: meshes, + particleSystems: particleSystems, + skeletons: skeletons, + animationGroups: animationGroups, + transformNodes: transformNodes, + geometries: geometries, + lights: lights, + spriteManagers: spriteManagers, + }); + }, + onProgress, + (scene, message, exception) => { + reject(exception || new Error(message)); + }, + pluginExtension, + name, + pluginOptions + ); + }); +} + +function _load( + rootUrl: string, + sceneFilename: SceneSource = "", + engine: Nullable = EngineStore.LastCreatedEngine, + onSuccess: Nullable<(scene: Scene) => void> = null, + onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, + onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, + pluginExtension: Nullable = null, + name = "", + pluginOptions: PluginOptions = {} +): Nullable { + if (!engine) { + Tools.Error("No engine available"); + return null; + } + + return _append(rootUrl, sceneFilename, new Scene(engine), onSuccess, onProgress, onError, pluginExtension, name, pluginOptions); +} + +/** + * Load a scene + * @experimental + * @param source a string that defines the name of the scene file, or starts with "data:" following by the stringified version of the scene, or a File object, or an ArrayBufferView + * @param engine is the instance of BABYLON.Engine to use to create the scene + * @param options an object that configures aspects of how the scene is loaded + * @returns The loaded scene + */ +export function loadAsync(source: SceneSource, engine: AbstractEngine, options?: LoadOptions): Promise { + const { rootUrl = "", onProgress, pluginExtension, name, pluginOptions } = options ?? {}; + return _loadAsync(rootUrl, source, engine, onProgress, pluginExtension, name, pluginOptions); +} + +function _loadAsync( + rootUrl: string, + sceneFilename?: SceneSource, + engine?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + name?: string, + pluginOptions?: PluginOptions +): Promise { + return new Promise((resolve, reject) => { + _load( + rootUrl, + sceneFilename, + engine, + (scene) => { + resolve(scene); + }, + onProgress, + (scene, message, exception) => { + reject(exception || new Error(message)); + }, + pluginExtension, + name, + pluginOptions + ); + }); +} + +function _append( + rootUrl: string, + sceneFilename: SceneSource = "", + scene: Nullable = EngineStore.LastCreatedScene, + onSuccess: Nullable<(scene: Scene) => void> = null, + onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, + onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, + pluginExtension: Nullable = null, + name = "", + pluginOptions: PluginOptions = {} +): Nullable { + if (!scene) { + Logger.Error("No scene available to append to"); + return null; + } + + const fileInfo = _getFileInfo(rootUrl, sceneFilename); + if (!fileInfo) { + return null; + } + + const loadingToken = {}; + scene.addPendingData(loadingToken); + + const disposeHandler = () => { + scene.removePendingData(loadingToken); + }; + + if (SceneLoaderFlags.ShowLoadingScreen && !_showingLoadingScreen) { + _showingLoadingScreen = true; + scene.getEngine().displayLoadingUI(); + scene.executeWhenReady(() => { + scene.getEngine().hideLoadingUI(); + _showingLoadingScreen = false; + }); + } + + const errorHandler = (message?: string, exception?: any) => { + const errorMessage = _formatErrorMessage(fileInfo, message, exception); + + if (onError) { + onError(scene, errorMessage, new RuntimeError(errorMessage, ErrorCodes.SceneLoaderError, exception)); + } else { + Logger.Error(errorMessage); + // should the exception be thrown? } - const useArrayBuffer = registeredPlugin.isBinary; + disposeHandler(); + }; - const dataCallback = (data: unknown, responseURL?: string) => { - if (scene.isDisposed) { - onError("Scene has been disposed"); - return; + const progressHandler = onProgress + ? (event: ISceneLoaderProgressEvent) => { + try { + onProgress(event); + } catch (e) { + errorHandler("Error in onProgress callback", e); + } + } + : undefined; + + const successHandler = () => { + if (onSuccess) { + try { + onSuccess(scene); + } catch (e) { + errorHandler("Error in onSuccess callback", e); } + } - onSuccess(plugin, data, responseURL); - }; + scene.removePendingData(loadingToken); + }; - let request: Nullable = null; - let pluginDisposed = false; - plugin.onDisposeObservable?.add(() => { - pluginDisposed = true; + return _loadData( + fileInfo, + scene, + (plugin, data) => { + if ((plugin as ISceneLoaderPlugin).load) { + const syncedPlugin = plugin; + if (!syncedPlugin.load(scene, data, fileInfo.rootUrl, errorHandler)) { + return; + } - if (request) { - request.abort(); - request = null; + scene.loadingPluginName = plugin.name; + successHandler(); + } else { + const asyncedPlugin = plugin; + asyncedPlugin + .loadAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) + .then(() => { + scene.loadingPluginName = plugin.name; + successHandler(); + }) + .catch((error) => { + errorHandler(error.message, error); + }); } + }, + progressHandler, + errorHandler, + disposeHandler, + pluginExtension, + name, + pluginOptions + ); +} - onDispose(); - }); +/** + * Append a scene + * @experimental + * @param source a string that defines the name of the scene file, or starts with "data:" following by the stringified version of the scene, or a File object, or an ArrayBufferView + * @param scene is the instance of BABYLON.Scene to append to + * @param options an object that configures aspects of how the scene is loaded + * @returns The given scene + */ +export function appendAsync(source: SceneSource, scene: Scene, options?: LoadAssetContainerOptions): Promise { + const { rootUrl = "", onProgress, pluginExtension, name, pluginOptions } = options ?? {}; + return _appendAsync(rootUrl, source, scene, onProgress, pluginExtension, name, pluginOptions); +} - const manifestChecked = () => { - if (pluginDisposed) { - return; - } +function _appendAsync( + rootUrl: string, + sceneFilename?: SceneSource, + scene?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + name?: string, + pluginOptions?: PluginOptions +): Promise { + return new Promise((resolve, reject) => { + _append( + rootUrl, + sceneFilename, + scene, + (scene) => { + resolve(scene); + }, + onProgress, + (scene, message, exception) => { + reject(exception || new Error(message)); + }, + pluginExtension, + name, + pluginOptions + ); + }); +} - const errorCallback = (request?: WebRequest, exception?: LoadFileError) => { - onError(request?.statusText, exception); - }; +function _loadAssetContainer( + rootUrl: string, + sceneFilename: SceneSource = "", + scene: Nullable = EngineStore.LastCreatedScene, + onSuccess: Nullable<(assets: AssetContainer) => void> = null, + onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, + onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, + pluginExtension: Nullable = null, + name = "", + pluginOptions: PluginOptions = {} +): Nullable { + if (!scene) { + Logger.Error("No scene available to load asset container to"); + return null; + } + + const fileInfo = _getFileInfo(rootUrl, sceneFilename); + if (!fileInfo) { + return null; + } - if (!plugin.loadFile && fileInfo.rawData) { - // eslint-disable-next-line no-throw-literal - throw "Plugin does not support loading ArrayBufferView."; + const loadingToken = {}; + scene.addPendingData(loadingToken); + + const disposeHandler = () => { + scene.removePendingData(loadingToken); + }; + + const errorHandler = (message?: string, exception?: any) => { + const errorMessage = _formatErrorMessage(fileInfo, message, exception); + + if (onError) { + onError(scene, errorMessage, new RuntimeError(errorMessage, ErrorCodes.SceneLoaderError, exception)); + } else { + Logger.Error(errorMessage); + // should the exception be thrown? + } + + disposeHandler(); + }; + + const progressHandler = onProgress + ? (event: ISceneLoaderProgressEvent) => { + try { + onProgress(event); + } catch (e) { + errorHandler("Error in onProgress callback", e); + } + } + : undefined; + + const successHandler = (assets: AssetContainer) => { + if (onSuccess) { + try { + onSuccess(assets); + } catch (e) { + errorHandler("Error in onSuccess callback", e); } + } - request = plugin.loadFile - ? plugin.loadFile(scene, fileInfo.rawData || fileInfo.file || fileInfo.url, fileInfo.rootUrl, dataCallback, onProgress, useArrayBuffer, errorCallback, name) - : scene._loadFile(fileInfo.file || fileInfo.url, dataCallback, onProgress, true, useArrayBuffer, errorCallback); - }; + scene.removePendingData(loadingToken); + }; - const engine = scene.getEngine(); - let canUseOfflineSupport = engine.enableOfflineSupport; - if (canUseOfflineSupport) { - // Also check for exceptions - let exceptionFound = false; - for (const regex of scene.disableOfflineSupportExceptionRules) { - if (regex.test(fileInfo.url)) { - exceptionFound = true; - break; + return _loadData( + fileInfo, + scene, + (plugin, data) => { + if ((plugin as ISceneLoaderPlugin).loadAssetContainer) { + const syncedPlugin = plugin; + const assetContainer = syncedPlugin.loadAssetContainer(scene, data, fileInfo.rootUrl, errorHandler); + if (!assetContainer) { + return; } + assetContainer.populateRootNodes(); + scene.loadingPluginName = plugin.name; + successHandler(assetContainer); + } else if ((plugin as ISceneLoaderPluginAsync).loadAssetContainerAsync) { + const asyncedPlugin = plugin; + asyncedPlugin + .loadAssetContainerAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) + .then((assetContainer) => { + assetContainer.populateRootNodes(); + scene.loadingPluginName = plugin.name; + successHandler(assetContainer); + }) + .catch((error) => { + errorHandler(error.message, error); + }); + } else { + errorHandler("LoadAssetContainer is not supported by this plugin. Plugin did not provide a loadAssetContainer or loadAssetContainerAsync method."); } + }, + progressHandler, + errorHandler, + disposeHandler, + pluginExtension, + name, + pluginOptions + ); +} + +/** + * Load a scene into an asset container + * @experimental + * @param source a string that defines the name of the scene file, or starts with "data:" following by the stringified version of the scene, or a File object, or an ArrayBufferView + * @param scene is the instance of Scene to append to + * @param options an object that configures aspects of how the scene is loaded + * @returns The loaded asset container + */ +export function loadAssetContainerAsync(source: SceneSource, scene: Scene, options?: LoadAssetContainerOptions): Promise { + const { rootUrl = "", onProgress, pluginExtension, name, pluginOptions } = options ?? {}; + return _loadAssetContainerAsync(rootUrl, source, scene, onProgress, pluginExtension, name, pluginOptions); +} + +function _loadAssetContainerAsync( + rootUrl: string, + sceneFilename?: SceneSource, + scene?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + name?: string, + pluginOptions?: PluginOptions +): Promise { + return new Promise((resolve, reject) => { + _loadAssetContainer( + rootUrl, + sceneFilename, + scene, + (assets) => { + resolve(assets); + }, + onProgress, + (scene, message, exception) => { + reject(exception || new Error(message)); + }, + pluginExtension, + name, + pluginOptions + ); + }); +} + +function _importAnimations( + rootUrl: string, + sceneFilename: SceneSource = "", + scene: Nullable = EngineStore.LastCreatedScene, + overwriteAnimations = true, + animationGroupLoadingMode = SceneLoaderAnimationGroupLoadingMode.Clean, + targetConverter: Nullable<(target: any) => any> = null, + onSuccess: Nullable<(scene: Scene) => void> = null, + onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, + onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, + pluginExtension: Nullable = null, + name = "", + pluginOptions: PluginOptions = {} +): void { + if (!scene) { + Logger.Error("No scene available to load animations to"); + return; + } + + if (overwriteAnimations) { + // Reset, stop and dispose all animations before loading new ones + for (const animatable of scene.animatables) { + animatable.reset(); + } + scene.stopAllAnimations(); + scene.animationGroups.slice().forEach((animationGroup) => { + animationGroup.dispose(); + }); + const nodes = scene.getNodes(); + nodes.forEach((node) => { + if (node.animations) { + node.animations = []; + } + }); + } else { + switch (animationGroupLoadingMode) { + case SceneLoaderAnimationGroupLoadingMode.Clean: + scene.animationGroups.slice().forEach((animationGroup) => { + animationGroup.dispose(); + }); + break; + case SceneLoaderAnimationGroupLoadingMode.Stop: + scene.animationGroups.forEach((animationGroup) => { + animationGroup.stop(); + }); + break; + case SceneLoaderAnimationGroupLoadingMode.Sync: + scene.animationGroups.forEach((animationGroup) => { + animationGroup.reset(); + animationGroup.restart(); + }); + break; + case SceneLoaderAnimationGroupLoadingMode.NoSync: + // nothing to do + break; + default: + Logger.Error("Unknown animation group loading mode value '" + animationGroupLoadingMode + "'"); + return; + } + } + + const startingIndexForNewAnimatables = scene.animatables.length; + + const onAssetContainerLoaded = (container: AssetContainer) => { + container.mergeAnimationsTo(scene, scene.animatables.slice(startingIndexForNewAnimatables), targetConverter); + + container.dispose(); + + scene.onAnimationFileImportedObservable.notifyObservers(scene); + + if (onSuccess) { + onSuccess(scene); + } + }; + + _loadAssetContainer(rootUrl, sceneFilename, scene, onAssetContainerLoaded, onProgress, onError, pluginExtension, name, pluginOptions); +} + +/** + * Import animations from a file into a scene + * @experimental + * @param source a string that defines the name of the scene file, or starts with "data:" following by the stringified version of the scene, or a File object, or an ArrayBufferView + * @param scene is the instance of BABYLON.Scene to append to (default: last created scene) + * @param options an object that configures aspects of how the scene is loaded + * @returns The loaded asset container + */ +export function importAnimationsAsync(source: SceneSource, scene: Scene, options?: ImportAnimationsOptions): Promise { + const { rootUrl = "", overwriteAnimations, animationGroupLoadingMode, targetConverter, onProgress, pluginExtension, name, pluginOptions } = options ?? {}; + return _importAnimationsAsync(rootUrl, source, scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, onProgress, pluginExtension, name, pluginOptions); +} + +function _importAnimationsAsync( + rootUrl: string, + sceneFilename?: SceneSource, + scene?: Nullable, + overwriteAnimations?: boolean, + animationGroupLoadingMode?: SceneLoaderAnimationGroupLoadingMode, + targetConverter?: Nullable<(target: any) => any>, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + name?: string, + pluginOptions?: PluginOptions +): Promise { + return new Promise((resolve, reject) => { + _importAnimations( + rootUrl, + sceneFilename, + scene, + overwriteAnimations, + animationGroupLoadingMode, + targetConverter, + (scene) => { + resolve(scene); + }, + onProgress, + (scene, message, exception) => { + reject(exception || new Error(message)); + }, + pluginExtension, + name, + pluginOptions + ); + }); +} + +/** + * Class used to load scene from various file formats using registered plugins + * @see https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes + */ +export class SceneLoader { + /** + * No logging while loading + */ + public static readonly NO_LOGGING = Constants.SCENELOADER_NO_LOGGING; + + /** + * Minimal logging while loading + */ + public static readonly MINIMAL_LOGGING = Constants.SCENELOADER_MINIMAL_LOGGING; + + /** + * Summary logging while loading + */ + public static readonly SUMMARY_LOGGING = Constants.SCENELOADER_SUMMARY_LOGGING; + + /** + * Detailed logging while loading + */ + public static readonly DETAILED_LOGGING = Constants.SCENELOADER_DETAILED_LOGGING; + + /** + * Gets or sets a boolean indicating if entire scene must be loaded even if scene contains incremental data + */ + public static get ForceFullSceneLoadingForIncremental() { + return SceneLoaderFlags.ForceFullSceneLoadingForIncremental; + } + + public static set ForceFullSceneLoadingForIncremental(value: boolean) { + SceneLoaderFlags.ForceFullSceneLoadingForIncremental = value; + } + + /** + * Gets or sets a boolean indicating if loading screen must be displayed while loading a scene + */ + public static get ShowLoadingScreen(): boolean { + return SceneLoaderFlags.ShowLoadingScreen; + } + + public static set ShowLoadingScreen(value: boolean) { + SceneLoaderFlags.ShowLoadingScreen = value; + } + + /** + * Defines the current logging level (while loading the scene) + * @ignorenaming + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + public static get loggingLevel(): number { + return SceneLoaderFlags.loggingLevel; + } - canUseOfflineSupport = !exceptionFound; - } + // eslint-disable-next-line @typescript-eslint/naming-convention + public static set loggingLevel(value: number) { + SceneLoaderFlags.loggingLevel = value; + } - if (canUseOfflineSupport && Engine.OfflineProviderFactory) { - // Checking if a manifest file has been set for this scene and if offline mode has been requested - scene.offlineProvider = Engine.OfflineProviderFactory(fileInfo.url, manifestChecked, engine.disableManifestCheck); - } else { - manifestChecked(); - } + /** + * Gets or set a boolean indicating if matrix weights must be cleaned upon loading + */ + public static get CleanBoneMatrixWeights(): boolean { + return SceneLoaderFlags.CleanBoneMatrixWeights; + } - return plugin; + public static set CleanBoneMatrixWeights(value: boolean) { + SceneLoaderFlags.CleanBoneMatrixWeights = value; } - private static _GetFileInfo(rootUrl: string, sceneSource: SceneSource): Nullable { - let url: string; - let name: string; - let file: Nullable = null; - let rawData: Nullable = null; - - if (!sceneSource) { - url = rootUrl; - name = Tools.GetFilename(rootUrl); - rootUrl = Tools.GetFolderPath(rootUrl); - } else if (isFile(sceneSource)) { - url = `file:${sceneSource.name}`; - name = sceneSource.name; - file = sceneSource; - } else if (ArrayBuffer.isView(sceneSource)) { - url = ""; - name = RandomGUID(); - rawData = sceneSource; - } else if (sceneSource.startsWith("data:")) { - url = sceneSource; - name = ""; - } else if (rootUrl) { - const filename = sceneSource; - if (filename.substr(0, 1) === "/") { - Tools.Error("Wrong sceneFilename parameter"); - return null; - } + // Members - url = rootUrl + filename; - name = filename; - } else { - url = sceneSource; - name = Tools.GetFilename(sceneSource); - rootUrl = Tools.GetFolderPath(sceneSource); - } + /** + * Event raised when a plugin is used to load a scene + */ + public static readonly OnPluginActivatedObservable = _onPluginActivatedObservable; - return { - url: url, - rootUrl: rootUrl, - name: name, - file: file, - rawData, - }; + /** + * Gets the default plugin (used to load Babylon files) + * @returns the .babylon plugin + */ + public static GetDefaultPlugin(): IRegisteredPlugin { + return _getDefaultPlugin(); } // Public functions @@ -815,7 +1484,7 @@ export class SceneLoader { * @returns a plugin or null if none works */ public static GetPluginForExtension(extension: string): ISceneLoaderPlugin | ISceneLoaderPluginAsync | ISceneLoaderPluginFactory { - return SceneLoader._GetPluginForExtension(extension).plugin; + return _getPluginForExtension(extension).plugin; } /** @@ -824,7 +1493,7 @@ export class SceneLoader { * @returns true if the extension is supported */ public static IsPluginForExtensionAvailable(extension: string): boolean { - return !!SceneLoader._RegisteredPlugins[extension]; + return _isPluginForExtensionAvailable(extension); } /** @@ -832,21 +1501,7 @@ export class SceneLoader { * @param plugin defines the plugin to add */ public static RegisterPlugin(plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync): void { - if (typeof plugin.extensions === "string") { - const extension = plugin.extensions; - SceneLoader._RegisteredPlugins[extension.toLowerCase()] = { - plugin: plugin, - isBinary: false, - }; - } else { - const extensions = plugin.extensions; - Object.keys(extensions).forEach((extension) => { - SceneLoader._RegisteredPlugins[extension.toLowerCase()] = { - plugin: plugin, - isBinary: extensions[extension].isBinary, - }; - }); - } + registerSceneLoaderPlugin(plugin); } /** @@ -873,136 +1528,9 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string ): Nullable { - return SceneLoader._ImportMesh(meshNames, rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); - } - - private static _ImportMesh( - meshNames: string | readonly string[] | null | undefined, - rootUrl: string, - sceneFilename: SceneSource = "", - scene: Nullable = EngineStore.LastCreatedScene, - onSuccess: Nullable = null, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, - pluginExtension: Nullable = null, - name = "", - pluginOptions: PluginOptions = {} - ): Nullable { - if (!scene) { - Logger.Error("No scene available to import mesh to"); - return null; - } - - const fileInfo = SceneLoader._GetFileInfo(rootUrl, sceneFilename); - if (!fileInfo) { - return null; - } - - const loadingToken = {}; - scene.addPendingData(loadingToken); - - const disposeHandler = () => { - scene.removePendingData(loadingToken); - }; - - const errorHandler = (message?: string, exception?: any) => { - const errorMessage = SceneLoader._FormatErrorMessage(fileInfo, message, exception); - - if (onError) { - onError(scene, errorMessage, new RuntimeError(errorMessage, ErrorCodes.SceneLoaderError, exception)); - } else { - Logger.Error(errorMessage); - // should the exception be thrown? - } - - disposeHandler(); - }; - - const progressHandler = onProgress - ? (event: ISceneLoaderProgressEvent) => { - try { - onProgress(event); - } catch (e) { - errorHandler("Error in onProgress callback: " + e, e); - } - } - : undefined; - - const successHandler: SceneLoaderSuccessCallback = (meshes, particleSystems, skeletons, animationGroups, transformNodes, geometries, lights, spriteManagers) => { - scene.importedMeshesFiles.push(fileInfo.url); - - if (onSuccess) { - try { - onSuccess(meshes, particleSystems, skeletons, animationGroups, transformNodes, geometries, lights, spriteManagers); - } catch (e) { - errorHandler("Error in onSuccess callback: " + e, e); - } - } - - scene.removePendingData(loadingToken); - }; - - return SceneLoader._LoadData( - fileInfo, - scene, - (plugin, data, responseURL) => { - if (plugin.rewriteRootURL) { - fileInfo.rootUrl = plugin.rewriteRootURL(fileInfo.rootUrl, responseURL); - } - - if ((plugin as ISceneLoaderPlugin).importMesh) { - const syncedPlugin = plugin; - const meshes: AbstractMesh[] = []; - const particleSystems: IParticleSystem[] = []; - const skeletons: Skeleton[] = []; - - if (!syncedPlugin.importMesh(meshNames, scene, data, fileInfo.rootUrl, meshes, particleSystems, skeletons, errorHandler)) { - return; - } - - scene.loadingPluginName = plugin.name; - successHandler(meshes, particleSystems, skeletons, [], [], [], [], []); - } else { - const asyncedPlugin = plugin; - asyncedPlugin - .importMeshAsync(meshNames, scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) - .then((result) => { - scene.loadingPluginName = plugin.name; - successHandler( - result.meshes, - result.particleSystems, - result.skeletons, - result.animationGroups, - result.transformNodes, - result.geometries, - result.lights, - result.spriteManagers - ); - }) - .catch((error) => { - errorHandler(error.message, error); - }); - } - }, - progressHandler, - errorHandler, - disposeHandler, - pluginExtension, - name, - pluginOptions - ); + return _importMesh(meshNames, rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); } - /** - * Import meshes into a scene - * @experimental - * @param source a string that defines the name of the scene file, or starts with "data:" following by the stringified version of the scene, or a File object, or an ArrayBufferView - * @param scene the instance of BABYLON.Scene to append to - * @param options an object that configures aspects of how the scene is loaded - * @returns The loaded list of imported meshes, particle systems, skeletons, and animation groups - */ - public static ImportMeshAsync(source: SceneSource, scene: Scene, options?: ImportMeshOptions): Promise; - /** * Import meshes into a scene * @param meshNames an array of mesh names, a single mesh name, or empty string for all meshes that filter what meshes are imported @@ -1022,75 +1550,8 @@ export class SceneLoader { onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, name?: string - ): Promise; - - public static ImportMeshAsync( - ...args: - | [ - meshNames: string | readonly string[] | null | undefined, - rootUrl: string, - sceneFilename?: SceneSource, - scene?: Nullable, - onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, - pluginExtension?: Nullable, - name?: string, - ] - | [source: SceneSource, scene: Scene, options?: ImportMeshOptions] ): Promise { - let meshNames: string | readonly string[] | null | undefined; - let rootUrl: string; - let sceneFilename: SceneSource | undefined; - let scene: Nullable | undefined; - let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; - let pluginExtension: Nullable | undefined; - let name: string | undefined; - let pluginOptions: PluginOptions; - - // This is a user-defined type guard: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards - // This is the most type safe way to distinguish between the two possible argument arrays. - const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [SceneSource, Scene, ImportMeshOptions?] => { - // If the second argument is an object, then it must be the options overload. - return typeof maybeOptionsArgs[1] === "object"; - }; - - if (isOptionsArgs(args)) { - // Source is mapped to sceneFileName - sceneFilename = args[0]; - scene = args[1]; - // Options determine the rest of the arguments - ({ meshNames, rootUrl = "", onProgress, pluginExtension, name, pluginOptions } = args[2] ?? {}); - } else { - // For the legacy signature, we just directly map each argument - [meshNames, rootUrl, sceneFilename, scene, onProgress, pluginExtension, name] = args; - } - - return new Promise((resolve, reject) => { - SceneLoader._ImportMesh( - meshNames, - rootUrl, - sceneFilename, - scene, - (meshes, particleSystems, skeletons, animationGroups, transformNodes, geometries, lights, spriteManagers) => { - resolve({ - meshes: meshes, - particleSystems: particleSystems, - skeletons: skeletons, - animationGroups: animationGroups, - transformNodes: transformNodes, - geometries: geometries, - lights: lights, - spriteManagers: spriteManagers, - }); - }, - onProgress, - (scene, message, exception) => { - reject(exception || new Error(message)); - }, - pluginExtension, - name, - pluginOptions - ); - }); + return _importMeshAsync(meshNames, rootUrl, sceneFilename, scene, onProgress, pluginExtension, name); } /** @@ -1115,38 +1576,9 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string ): Nullable { - return SceneLoader._Load(rootUrl, sceneFilename, engine, onSuccess, onProgress, onError, pluginExtension, name); - } - - private static _Load( - rootUrl: string, - sceneFilename: SceneSource = "", - engine: Nullable = EngineStore.LastCreatedEngine, - onSuccess: Nullable<(scene: Scene) => void> = null, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, - pluginExtension: Nullable = null, - name = "", - pluginOptions: PluginOptions = {} - ): Nullable { - if (!engine) { - Tools.Error("No engine available"); - return null; - } - - return SceneLoader._Append(rootUrl, sceneFilename, new Scene(engine), onSuccess, onProgress, onError, pluginExtension, name, pluginOptions); + return _load(rootUrl, sceneFilename, engine, onSuccess, onProgress, onError, pluginExtension, name); } - /** - * Load a scene - * @experimental - * @param source a string that defines the name of the scene file, or starts with "data:" following by the stringified version of the scene, or a File object, or an ArrayBufferView - * @param engine is the instance of BABYLON.Engine to use to create the scene - * @param options an object that configures aspects of how the scene is loaded - * @returns The loaded scene - */ - public static LoadAsync(source: SceneSource, engine: AbstractEngine, options?: LoadOptions): Promise; - /** * Load a scene * @param rootUrl a string that defines the root url for the scene and resources or the concatenation of rootURL and filename (e.g. http://example.com/test.glb) @@ -1164,63 +1596,8 @@ export class SceneLoader { onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, name?: string - ): Promise; - - public static LoadAsync( - ...args: - | [ - rootUrl: string, - sceneFilename?: SceneSource, - engine?: Nullable, - onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, - pluginExtension?: Nullable, - name?: string, - ] - | [source: SceneSource, engine: AbstractEngine, options?: LoadOptions] ): Promise { - let rootUrl: string; - let sceneFilename: SceneSource | undefined; - let engine: Nullable | undefined; - let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; - let pluginExtension: Nullable | undefined; - let name: string | undefined; - let pluginOptions: PluginOptions; - - // This is a user-defined type guard: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards - // This is the most type safe way to distinguish between the two possible argument arrays. - const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [SceneSource, AbstractEngine, LoadOptions?] => { - // If the second argument is an engine, then it must be the options overload. - return maybeOptionsArgs[1] instanceof AbstractEngine; - }; - - if (isOptionsArgs(args)) { - // Source is mapped to sceneFileName - sceneFilename = args[0]; - engine = args[1]; - // Options determine the rest of the arguments - ({ rootUrl = "", onProgress, pluginExtension, name, pluginOptions } = args[2] ?? {}); - } else { - // For the legacy signature, we just directly map each argument - [rootUrl, sceneFilename, engine, onProgress, pluginExtension, name] = args; - } - - return new Promise((resolve, reject) => { - SceneLoader._Load( - rootUrl, - sceneFilename, - engine, - (scene) => { - resolve(scene); - }, - onProgress, - (scene, message, exception) => { - reject(exception || new Error(message)); - }, - pluginExtension, - name, - pluginOptions - ); - }); + return _loadAsync(rootUrl, sceneFilename, engine, onProgress, pluginExtension, name); } /** @@ -1245,125 +1622,9 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string ): Nullable { - return SceneLoader._Append(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); - } - - private static _Append( - rootUrl: string, - sceneFilename: SceneSource = "", - scene: Nullable = EngineStore.LastCreatedScene, - onSuccess: Nullable<(scene: Scene) => void> = null, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, - pluginExtension: Nullable = null, - name = "", - pluginOptions: PluginOptions = {} - ): Nullable { - if (!scene) { - Logger.Error("No scene available to append to"); - return null; - } - - const fileInfo = SceneLoader._GetFileInfo(rootUrl, sceneFilename); - if (!fileInfo) { - return null; - } - - const loadingToken = {}; - scene.addPendingData(loadingToken); - - const disposeHandler = () => { - scene.removePendingData(loadingToken); - }; - - if (SceneLoader.ShowLoadingScreen && !this._ShowingLoadingScreen) { - this._ShowingLoadingScreen = true; - scene.getEngine().displayLoadingUI(); - scene.executeWhenReady(() => { - scene.getEngine().hideLoadingUI(); - this._ShowingLoadingScreen = false; - }); - } - - const errorHandler = (message?: string, exception?: any) => { - const errorMessage = SceneLoader._FormatErrorMessage(fileInfo, message, exception); - - if (onError) { - onError(scene, errorMessage, new RuntimeError(errorMessage, ErrorCodes.SceneLoaderError, exception)); - } else { - Logger.Error(errorMessage); - // should the exception be thrown? - } - - disposeHandler(); - }; - - const progressHandler = onProgress - ? (event: ISceneLoaderProgressEvent) => { - try { - onProgress(event); - } catch (e) { - errorHandler("Error in onProgress callback", e); - } - } - : undefined; - - const successHandler = () => { - if (onSuccess) { - try { - onSuccess(scene); - } catch (e) { - errorHandler("Error in onSuccess callback", e); - } - } - - scene.removePendingData(loadingToken); - }; - - return SceneLoader._LoadData( - fileInfo, - scene, - (plugin, data) => { - if ((plugin as ISceneLoaderPlugin).load) { - const syncedPlugin = plugin; - if (!syncedPlugin.load(scene, data, fileInfo.rootUrl, errorHandler)) { - return; - } - - scene.loadingPluginName = plugin.name; - successHandler(); - } else { - const asyncedPlugin = plugin; - asyncedPlugin - .loadAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) - .then(() => { - scene.loadingPluginName = plugin.name; - successHandler(); - }) - .catch((error) => { - errorHandler(error.message, error); - }); - } - }, - progressHandler, - errorHandler, - disposeHandler, - pluginExtension, - name, - pluginOptions - ); + return _append(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); } - /** - * Append a scene - * @experimental - * @param source a string that defines the name of the scene file, or starts with "data:" following by the stringified version of the scene, or a File object, or an ArrayBufferView - * @param scene is the instance of BABYLON.Scene to append to - * @param options an object that configures aspects of how the scene is loaded - * @returns The given scene - */ - public static AppendAsync(source: SceneSource, scene: Scene, options?: LoadAssetContainerOptions): Promise; - /** * Append a scene * @param rootUrl a string that defines the root url for the scene and resources or the concatenation of rootURL and filename (e.g. http://example.com/test.glb) @@ -1381,63 +1642,8 @@ export class SceneLoader { onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, name?: string - ): Promise; - - public static AppendAsync( - ...args: - | [ - rootUrl: string, - sceneFilename?: SceneSource, - scene?: Nullable, - onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, - pluginExtension?: Nullable, - name?: string, - ] - | [source: SceneSource, scene: Scene, options?: AppendOptions] ): Promise { - let rootUrl: string; - let sceneFilename: SceneSource | undefined; - let scene: Nullable | undefined; - let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; - let pluginExtension: Nullable | undefined; - let name: string | undefined; - let pluginOptions: PluginOptions; - - // This is a user-defined type guard: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards - // This is the most type safe way to distinguish between the two possible argument arrays. - const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [SceneSource, Scene, AppendOptions?] => { - // If the second argument is a Scene, then it must be the options overload. - return maybeOptionsArgs[1] instanceof Scene; - }; - - if (isOptionsArgs(args)) { - // Source is mapped to sceneFileName - sceneFilename = args[0]; - scene = args[1]; - // Options determine the rest of the arguments - ({ rootUrl = "", onProgress, pluginExtension, name, pluginOptions } = args[2] ?? {}); - } else { - // For the legacy signature, we just directly map each argument - [rootUrl, sceneFilename, scene, onProgress, pluginExtension, name] = args; - } - - return new Promise((resolve, reject) => { - SceneLoader._Append( - rootUrl, - sceneFilename, - scene, - (scene) => { - resolve(scene); - }, - onProgress, - (scene, message, exception) => { - reject(exception || new Error(message)); - }, - pluginExtension, - name, - pluginOptions - ); - }); + return _appendAsync(rootUrl, sceneFilename, scene, onProgress, pluginExtension, name); } /** @@ -1462,120 +1668,9 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string ): Nullable { - return SceneLoader._LoadAssetContainer(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); - } - - private static _LoadAssetContainer( - rootUrl: string, - sceneFilename: SceneSource = "", - scene: Nullable = EngineStore.LastCreatedScene, - onSuccess: Nullable<(assets: AssetContainer) => void> = null, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, - pluginExtension: Nullable = null, - name = "", - pluginOptions: PluginOptions = {} - ): Nullable { - if (!scene) { - Logger.Error("No scene available to load asset container to"); - return null; - } - - const fileInfo = SceneLoader._GetFileInfo(rootUrl, sceneFilename); - if (!fileInfo) { - return null; - } - - const loadingToken = {}; - scene.addPendingData(loadingToken); - - const disposeHandler = () => { - scene.removePendingData(loadingToken); - }; - - const errorHandler = (message?: string, exception?: any) => { - const errorMessage = SceneLoader._FormatErrorMessage(fileInfo, message, exception); - - if (onError) { - onError(scene, errorMessage, new RuntimeError(errorMessage, ErrorCodes.SceneLoaderError, exception)); - } else { - Logger.Error(errorMessage); - // should the exception be thrown? - } - - disposeHandler(); - }; - - const progressHandler = onProgress - ? (event: ISceneLoaderProgressEvent) => { - try { - onProgress(event); - } catch (e) { - errorHandler("Error in onProgress callback", e); - } - } - : undefined; - - const successHandler = (assets: AssetContainer) => { - if (onSuccess) { - try { - onSuccess(assets); - } catch (e) { - errorHandler("Error in onSuccess callback", e); - } - } - - scene.removePendingData(loadingToken); - }; - - return SceneLoader._LoadData( - fileInfo, - scene, - (plugin, data) => { - if ((plugin as ISceneLoaderPlugin).loadAssetContainer) { - const syncedPlugin = plugin; - const assetContainer = syncedPlugin.loadAssetContainer(scene, data, fileInfo.rootUrl, errorHandler); - if (!assetContainer) { - return; - } - assetContainer.populateRootNodes(); - scene.loadingPluginName = plugin.name; - successHandler(assetContainer); - } else if ((plugin as ISceneLoaderPluginAsync).loadAssetContainerAsync) { - const asyncedPlugin = plugin; - asyncedPlugin - .loadAssetContainerAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) - .then((assetContainer) => { - assetContainer.populateRootNodes(); - scene.loadingPluginName = plugin.name; - successHandler(assetContainer); - }) - .catch((error) => { - errorHandler(error.message, error); - }); - } else { - errorHandler("LoadAssetContainer is not supported by this plugin. Plugin did not provide a loadAssetContainer or loadAssetContainerAsync method."); - } - }, - progressHandler, - errorHandler, - disposeHandler, - pluginExtension, - name, - pluginOptions - ); + return _loadAssetContainer(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); } - /** - * Load a scene into an asset container - * @experimental - * @param source a string that defines the name of the scene file, or starts with "data:" following by the stringified version of the scene, or a File object, or an ArrayBufferView - * @param scene is the instance of Scene to append to - * @param options an object that configures aspects of how the scene is loaded - * @returns The loaded asset container - */ - public static LoadAssetContainerAsync(source: SceneSource, scene: Scene, options?: LoadAssetContainerOptions): Promise; - /** * Load a scene into an asset container * @param rootUrl a string that defines the root url for the scene and resources or the concatenation of rootURL and filename (e.g. http://example.com/test.glb) @@ -1593,65 +1688,8 @@ export class SceneLoader { onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, name?: string - ): Promise; - - // This is the single implementation that handles both the legacy many-parameters overload and the - // new source + config overload. Using a parameters array union is the most type safe way to handle this. - public static LoadAssetContainerAsync( - ...args: - | [ - rootUrl: string, - sceneFilename?: SceneSource, - scene?: Nullable, - onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, - pluginExtension?: Nullable, - name?: string, - ] - | [source: SceneSource, scene: Scene, options?: LoadAssetContainerOptions] ): Promise { - let rootUrl: string; - let sceneFilename: SceneSource | undefined; - let scene: Nullable | undefined; - let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; - let pluginExtension: Nullable | undefined; - let name: string | undefined; - let pluginOptions: PluginOptions; - - // This is a user-defined type guard: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards - // This is the most type safe way to distinguish between the two possible argument arrays. - const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [SceneSource, Scene, LoadAssetContainerOptions?] => { - // If the second argument is a Scene, then it must be the options overload. - return maybeOptionsArgs[1] instanceof Scene; - }; - - if (isOptionsArgs(args)) { - // Source is mapped to sceneFileName - sceneFilename = args[0]; - scene = args[1]; - // Options determine the rest of the arguments - ({ rootUrl = "", onProgress, pluginExtension, name, pluginOptions } = args[2] ?? {}); - } else { - // For the legacy signature, we just directly map each argument - [rootUrl, sceneFilename, scene, onProgress, pluginExtension, name] = args; - } - - return new Promise((resolve, reject) => { - SceneLoader._LoadAssetContainer( - rootUrl, - sceneFilename, - scene, - (assetContainer) => { - resolve(assetContainer); - }, - onProgress, - (scene, message, exception) => { - reject(exception || new Error(message)); - }, - pluginExtension, - name, - pluginOptions - ); - }); + return _loadAssetContainerAsync(rootUrl, sceneFilename, scene, onProgress, pluginExtension, name); } /** @@ -1681,109 +1719,9 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string ): void { - SceneLoader._ImportAnimations( - rootUrl, - sceneFilename, - scene, - overwriteAnimations, - animationGroupLoadingMode, - targetConverter, - onSuccess, - onProgress, - onError, - pluginExtension, - name - ); - } - - private static _ImportAnimations( - rootUrl: string, - sceneFilename: SceneSource = "", - scene: Nullable = EngineStore.LastCreatedScene, - overwriteAnimations = true, - animationGroupLoadingMode = SceneLoaderAnimationGroupLoadingMode.Clean, - targetConverter: Nullable<(target: any) => any> = null, - onSuccess: Nullable<(scene: Scene) => void> = null, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, - pluginExtension: Nullable = null, - name = "", - pluginOptions: PluginOptions = {} - ): void { - if (!scene) { - Logger.Error("No scene available to load animations to"); - return; - } - - if (overwriteAnimations) { - // Reset, stop and dispose all animations before loading new ones - for (const animatable of scene.animatables) { - animatable.reset(); - } - scene.stopAllAnimations(); - scene.animationGroups.slice().forEach((animationGroup) => { - animationGroup.dispose(); - }); - const nodes = scene.getNodes(); - nodes.forEach((node) => { - if (node.animations) { - node.animations = []; - } - }); - } else { - switch (animationGroupLoadingMode) { - case SceneLoaderAnimationGroupLoadingMode.Clean: - scene.animationGroups.slice().forEach((animationGroup) => { - animationGroup.dispose(); - }); - break; - case SceneLoaderAnimationGroupLoadingMode.Stop: - scene.animationGroups.forEach((animationGroup) => { - animationGroup.stop(); - }); - break; - case SceneLoaderAnimationGroupLoadingMode.Sync: - scene.animationGroups.forEach((animationGroup) => { - animationGroup.reset(); - animationGroup.restart(); - }); - break; - case SceneLoaderAnimationGroupLoadingMode.NoSync: - // nothing to do - break; - default: - Logger.Error("Unknown animation group loading mode value '" + animationGroupLoadingMode + "'"); - return; - } - } - - const startingIndexForNewAnimatables = scene.animatables.length; - - const onAssetContainerLoaded = (container: AssetContainer) => { - container.mergeAnimationsTo(scene, scene.animatables.slice(startingIndexForNewAnimatables), targetConverter); - - container.dispose(); - - scene.onAnimationFileImportedObservable.notifyObservers(scene); - - if (onSuccess) { - onSuccess(scene); - } - }; - - this._LoadAssetContainer(rootUrl, sceneFilename, scene, onAssetContainerLoaded, onProgress, onError, pluginExtension, name, pluginOptions); + _importAnimations(rootUrl, sceneFilename, scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, onSuccess, onProgress, onError, pluginExtension, name); } - /** - * Import animations from a file into a scene - * @experimental - * @param source a string that defines the name of the scene file, or starts with "data:" following by the stringified version of the scene, or a File object, or an ArrayBufferView - * @param scene is the instance of BABYLON.Scene to append to (default: last created scene) - * @param options an object that configures aspects of how the scene is loaded - * @returns The loaded asset container - */ - public static ImportAnimationsAsync(source: SceneSource, scene: Scene, options?: ImportAnimationsOptions): Promise; - /** * Import animations from a file into a scene * @param rootUrl a string that defines the root url for the scene and resources or the concatenation of rootURL and filename (e.g. http://example.com/test.glb) @@ -1806,78 +1744,14 @@ export class SceneLoader { overwriteAnimations?: boolean, animationGroupLoadingMode?: SceneLoaderAnimationGroupLoadingMode, targetConverter?: Nullable<(target: any) => any>, + // eslint-disable-next-line @typescript-eslint/no-unused-vars onSuccess?: Nullable<(scene: Scene) => void>, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + // eslint-disable-next-line @typescript-eslint/no-unused-vars onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, pluginExtension?: Nullable, name?: string - ): Promise; - - public static ImportAnimationsAsync( - ...args: - | [ - rootUrl: string, - sceneFilename?: SceneSource, - scene?: Nullable, - overwriteAnimations?: boolean, - animationGroupLoadingMode?: SceneLoaderAnimationGroupLoadingMode, - targetConverter?: Nullable<(target: any) => any>, - onSuccess?: Nullable<(scene: Scene) => void>, - onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, - onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, - pluginExtension?: Nullable, - name?: string, - ] - | [source: SceneSource, scene: Scene, options?: ImportAnimationsOptions] ): Promise { - let rootUrl: string; - let sceneFilename: SceneSource | undefined; - let scene: Nullable | undefined; - let overwriteAnimations: boolean | undefined; - let animationGroupLoadingMode: SceneLoaderAnimationGroupLoadingMode | undefined; - let targetConverter: Nullable<(target: any) => any> | undefined; - let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; - let pluginExtension: Nullable | undefined; - let name: string | undefined; - let pluginOptions: PluginOptions; - - // This is a user-defined type guard: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards - // This is the most type safe way to distinguish between the two possible argument arrays. - const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [SceneSource, Scene, ImportAnimationsOptions?] => { - // If the second argument is a Scene, then it must be the options overload. - return maybeOptionsArgs[1] instanceof Scene; - }; - - if (isOptionsArgs(args)) { - // Source is mapped to sceneFileName - sceneFilename = args[0]; - scene = args[1]; - // Options determine the rest of the arguments - ({ rootUrl = "", overwriteAnimations, animationGroupLoadingMode, targetConverter, onProgress, pluginExtension, name, pluginOptions } = args[2] ?? {}); - } else { - // For the legacy signature, we just directly map each argument - [rootUrl, sceneFilename, scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, , onProgress, , pluginExtension, name] = args; - } - - return new Promise((resolve, reject) => { - SceneLoader._ImportAnimations( - rootUrl, - sceneFilename, - scene, - overwriteAnimations, - animationGroupLoadingMode, - targetConverter, - (_scene: Scene) => { - resolve(_scene); - }, - onProgress, - (_scene: Scene, message: string, exception: any) => { - reject(exception || new Error(message)); - }, - pluginExtension, - name, - pluginOptions - ); - }); + return _importAnimationsAsync(rootUrl, sceneFilename, scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, onProgress, pluginExtension, name); } } diff --git a/packages/dev/loaders/src/OBJ/objFileLoader.ts b/packages/dev/loaders/src/OBJ/objFileLoader.ts index 6ff36bfe4b3..2b1a526a98d 100644 --- a/packages/dev/loaders/src/OBJ/objFileLoader.ts +++ b/packages/dev/loaders/src/OBJ/objFileLoader.ts @@ -3,7 +3,7 @@ import { Vector2 } from "core/Maths/math.vector"; import { Tools } from "core/Misc/tools"; import type { AbstractMesh } from "core/Meshes/abstractMesh"; import type { ISceneLoaderPluginAsync, ISceneLoaderPluginFactory, ISceneLoaderPlugin, ISceneLoaderAsyncResult } from "core/Loading/sceneLoader"; -import { SceneLoader } from "core/Loading/sceneLoader"; +import { registerSceneLoaderPlugin } from "core/Loading/sceneLoader"; import { AssetContainer } from "core/assetContainer"; import type { Scene } from "core/scene"; import type { WebRequest } from "core/Misc/webRequest"; @@ -362,7 +362,5 @@ export class OBJFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlugi } } -if (SceneLoader) { - //Add this loader into the register plugin - SceneLoader.RegisterPlugin(new OBJFileLoader()); -} +//Add this loader into the register plugin +registerSceneLoaderPlugin(new OBJFileLoader()); diff --git a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts index 55876aa28c7..c7505e262ef 100644 --- a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts +++ b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts @@ -6,7 +6,7 @@ import type { ISceneLoaderPluginExtensions, ISceneLoaderProgressEvent, } from "core/Loading/sceneLoader"; -import { SceneLoader } from "core/Loading/sceneLoader"; +import { registerSceneLoaderPlugin } from "core/Loading/sceneLoader"; import { GaussianSplattingMesh } from "core/Meshes/GaussianSplatting/gaussianSplattingMesh"; import type { AssetContainer } from "core/assetContainer"; import type { Scene } from "core/scene"; @@ -125,7 +125,5 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu } } -if (SceneLoader) { - //Add this loader into the register plugin - SceneLoader.RegisterPlugin(new SPLATFileLoader()); -} +//Add this loader into the register plugin +registerSceneLoaderPlugin(new SPLATFileLoader()); diff --git a/packages/dev/loaders/src/STL/stlFileLoader.ts b/packages/dev/loaders/src/STL/stlFileLoader.ts index 6652716b4b2..d1a32f03670 100644 --- a/packages/dev/loaders/src/STL/stlFileLoader.ts +++ b/packages/dev/loaders/src/STL/stlFileLoader.ts @@ -5,7 +5,7 @@ import { VertexBuffer } from "core/Buffers/buffer"; import type { AbstractMesh } from "core/Meshes/abstractMesh"; import { Mesh } from "core/Meshes/mesh"; import type { ISceneLoaderPlugin, ISceneLoaderPluginExtensions } from "core/Loading/sceneLoader"; -import { SceneLoader } from "core/Loading/sceneLoader"; +import { registerSceneLoaderPlugin } from "core/Loading/sceneLoader"; import { AssetContainer } from "core/assetContainer"; import type { Scene } from "core/scene"; @@ -287,6 +287,4 @@ export class STLFileLoader implements ISceneLoaderPlugin { } } -if (SceneLoader) { - SceneLoader.RegisterPlugin(new STLFileLoader()); -} +registerSceneLoaderPlugin(new STLFileLoader()); diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index b92dfcd997c..b86ec9830f3 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -16,7 +16,7 @@ import type { ISceneLoaderPluginExtensions, ISceneLoaderAsyncResult, } from "core/Loading/sceneLoader"; -import { SceneLoader } from "core/Loading/sceneLoader"; +import { registerSceneLoaderPlugin } from "core/Loading/sceneLoader"; import type { SceneLoaderPluginOptions } from "core/Loading/sceneLoader"; import { AssetContainer } from "core/assetContainer"; import type { Scene, IDisposable } from "core/scene"; @@ -1303,6 +1303,4 @@ export class GLTFFileLoader extends GLTFLoaderOptions implements IDisposable, IS private _endPerformanceCounterDisabled(counterName: string): void {} } -if (SceneLoader) { - SceneLoader.RegisterPlugin(new GLTFFileLoader()); -} +registerSceneLoaderPlugin(new GLTFFileLoader()); diff --git a/packages/public/@babylonjs/viewer-alpha/readme.md b/packages/public/@babylonjs/viewer-alpha/readme.md index 55c455a5606..09cfa36c7b7 100644 --- a/packages/public/@babylonjs/viewer-alpha/readme.md +++ b/packages/public/@babylonjs/viewer-alpha/readme.md @@ -31,9 +31,24 @@ To use the higher level `HTML3DElement` you can import the `@babylonjs/viewer` m ``` + +## CDN/Direct usage + +If you want to use the viewer directly in a browser without any build tools, you can use the self-contained ESM bundle (which includes all dependencies) through a CDN such as [UNPKG](https://unpkg.com/) or [jsDelivr](https://www.jsdelivr.com/) like this: + +```html + + + + + + +``` + +See the [codesandbox.io](https://codesandbox.io/p/sandbox/babylon-viewer-ws82xr) example for a live demo. diff --git a/packages/tools/viewer-alpha/generateCoverageReports.mjs b/packages/tools/viewer-alpha/generateCoverageReports.mjs index 78e0f434782..cdb52f0c3a1 100644 --- a/packages/tools/viewer-alpha/generateCoverageReports.mjs +++ b/packages/tools/viewer-alpha/generateCoverageReports.mjs @@ -7,7 +7,7 @@ import { readFileSync } from "fs"; import path from "path"; import open from "open"; import chalk from "chalk"; -import { generateFlameChart } from "../../../scripts/folderSizeFlameChart.js"; +import { generateFlameChart } from "../../../scripts/folderSizeFlameChart.mjs"; const [scriptPath, analyzeDirectory, coverageDirectory, originalDirectory, rawDirectory] = process.argv.slice(1); diff --git a/packages/tools/viewer-alpha/package.json b/packages/tools/viewer-alpha/package.json index 607ab7384bb..8c368614d73 100644 --- a/packages/tools/viewer-alpha/package.json +++ b/packages/tools/viewer-alpha/package.json @@ -14,8 +14,8 @@ "compile": "tsc -b tsconfig.build.json", "bundle:analyze": "rimraf dist/analyze && rollup -c rollup.config.analyze.mjs", "bundle:coverage": "rimraf dist/coverage && rollup -c rollup.config.coverage.mjs", - "analyze": "npm run bundle:analyze && node ../../../scripts/folderSizeFlameChart dist/analyze \"**/*.js\" dist/analyze/flameChart", - "import-chains": "node -e \"require('../../../scripts/queryRollupStats.js').queryRollupStats(process.argv[1], 'dist/analyze/stats.json')\"", + "analyze": "npm run bundle:analyze && node ../../../scripts/folderSizeFlameChart.mjs dist/analyze \"**/*.js\" dist/analyze/flameChart", + "import-chains": "node -e \"require('../../../scripts/queryRollupStats.js').queryRollupStats(process.argv[1], process.argv[2], 'dist/analyze/stats.json')\"", "instrument": "npm run bundle:coverage && nyc --exclude-node-modules=false instrument dist/coverage/original dist/coverage/instrumented", "report-coverage": "node generateCoverageReports.mjs dist/analyze dist/coverage dist/coverage/original dist/coverage/raw", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/packages/tools/viewer-alpha/src/viewer.ts b/packages/tools/viewer-alpha/src/viewer.ts index 6aaf71aa74e..f731d94b311 100644 --- a/packages/tools/viewer-alpha/src/viewer.ts +++ b/packages/tools/viewer-alpha/src/viewer.ts @@ -3,7 +3,7 @@ import type { AbstractEngine, AssetContainer, FramingBehavior, IDisposable, Mesh import { ArcRotateCamera } from "core/Cameras/arcRotateCamera"; import { HemisphericLight } from "core/Lights/hemisphericLight"; -import { SceneLoader } from "core/Loading/sceneLoader"; +import { loadAssetContainerAsync } from "core/Loading/sceneLoader"; import { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; import { CubeTexture } from "core/Materials/Textures/cubeTexture"; import { Texture } from "core/Materials/Textures/texture"; @@ -132,7 +132,7 @@ export class Viewer implements IDisposable { await this._loadModelLock.lockAsync(async () => { this._throwIfDisposedOrAborted(abortSignal, abortController.signal); this._details.model?.dispose(); - this._details.model = await SceneLoader.LoadAssetContainerAsync(finalSource, this._details.scene); + this._details.model = await loadAssetContainerAsync(finalSource, this._details.scene); this._details.model.addAllToScene(); this._reframeCamera(); }); diff --git a/scripts/folderSizeFlameChart.js b/scripts/folderSizeFlameChart.mjs similarity index 89% rename from scripts/folderSizeFlameChart.js rename to scripts/folderSizeFlameChart.mjs index f13132f2b45..669b2fb5089 100644 --- a/scripts/folderSizeFlameChart.js +++ b/scripts/folderSizeFlameChart.mjs @@ -7,14 +7,14 @@ // node folderSizeFlameChart.js [pattern=**/*] [outputFile=FoldersSizes] // Example: node folderSizeFlameChart.js . "**/*.ts,!**/*.d.ts,!**/test/**" -const child_process = require("child_process"); -const fs = require("fs"); -const path = require("path"); -const os = require("os"); -const https = require("https"); -const glob = require("glob"); -const chalk = require("chalk"); -const open = require("open"); +import child_process from "child_process"; +import fs from "fs"; +import path from "path"; +import os from "os"; +import https from "https"; +import { glob } from "glob"; +import chalk from "chalk"; +import open from "open"; async function downloadFlameGraphScript() { // This is the temp path where the flamegraph.pl script will be downloaded @@ -56,7 +56,7 @@ async function downloadFlameGraphScript() { * @param {function} coerceSize A function to coerce the size of a file. If not provided, the actual size is used. * @returns {Promise} A promise that resolves when the flame chart has been generated. */ -async function generateFlameChart(folder, pattern, outputFile, chartSubtitle, coerceSize) { +export async function generateFlameChart(folder, pattern, outputFile, chartSubtitle, coerceSize) { const flameGraphScriptPath = await downloadFlameGraphScript(); // Resolve to an absolute path @@ -103,7 +103,7 @@ async function generateFlameChart(folder, pattern, outputFile, chartSubtitle, co child_process.execSync(flameGraphCommand); } -if (require.main === module) { +if (import.meta.url === `file://${process.argv[1]}`) { const [scriptPath, folder = ".", pattern = "**", outputFile = "FoldersSizes"] = process.argv.slice(1); console.log(chalk.bold(`${path.basename(scriptPath)} ${folder} ${pattern} ${outputFile}`)); @@ -113,7 +113,3 @@ if (require.main === module) { open(`${outputFile}.svg`); }); } - -module.exports = { - generateFlameChart, -}; diff --git a/scripts/queryRollupStats.js b/scripts/queryRollupStats.js index 725989f0bfd..e804479b7ad 100644 --- a/scripts/queryRollupStats.js +++ b/scripts/queryRollupStats.js @@ -27,25 +27,34 @@ function stringToColorHash(str) { return color; } -function queryRollupStats(filter, statsFilePath) { +function queryRollupStats(filter, maxDepth, statsFilePath) { + if (!maxDepth) { + maxDepth = Number.POSITIVE_INFINITY; + } const { nodeMetas } = JSON.parse(fs.readFileSync(statsFilePath, "utf8")); const referenceStack = []; + const seenStacks = new Set(); - function traverse(uid) { + function traverse(uid, depth) { const node = nodeMetas[uid]; referenceStack.push(node.id); - if (node.importedBy.length === 0) { - for (let reference of referenceStack) { - const folder = path.dirname(reference); - const file = path.basename(reference); - reference = chalk.hex(stringToColorHash(reference))(`${folder}/${chalk.bold(file)}`); - console.log(reference); + if (node.importedBy.length === 0 || depth >= maxDepth) { + const stack = referenceStack.join(" -> "); + if (!seenStacks.has(stack)) { + seenStacks.add(stack); + + for (let reference of referenceStack) { + const folder = path.dirname(reference); + const file = path.basename(reference); + reference = chalk.hex(stringToColorHash(reference))(`${folder}/${chalk.bold(file)}`); + console.log(reference); + } + console.log(); } - console.log(); } else { for (const importedBy of node.importedBy) { - traverse(importedBy.uid); + traverse(importedBy.uid, depth + 1); } } @@ -56,12 +65,12 @@ function queryRollupStats(filter, statsFilePath) { .filter(([key, value]) => value.id.toLowerCase().includes(filter.toLocaleLowerCase())) .map(([key, value]) => key); - matches.forEach(traverse); + matches.forEach((match) => traverse(match, 1)); } if (require.main === module) { - const [scriptPath, filter, statsFilePath = "stats.json"] = process.argv.slice(1); - queryRollupStats(filter, statsFilePath); + const [scriptPath, filter, maxDepth, statsFilePath = "stats.json"] = process.argv.slice(1); + queryRollupStats(filter, maxDepth, statsFilePath); } module.exports = {