diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index a26f5cb79d1..ca80dd909b3 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 type { AbstractEngine } from "../Engines/abstractEngine"; +import { AbstractEngine } from "../Engines/abstractEngine"; /** * Type used for the success callback of ImportMesh @@ -110,7 +110,7 @@ export interface ISceneLoaderPluginExtensions { /** * Defines the list of supported extensions */ - [extension: string]: { + readonly [extension: string]: { isBinary: boolean; }; } @@ -122,13 +122,14 @@ export interface ISceneLoaderPluginFactory { /** * Defines the name of the factory */ - name: string; + readonly name: string; /** * Function called to create a new plugin + * @param options plugin options that were passed to the SceneLoader operation * @returns the new plugin */ - createPlugin(): ISceneLoaderPlugin | ISceneLoaderPluginAsync; + createPlugin(options: SceneLoaderPluginOptions): ISceneLoaderPlugin | ISceneLoaderPluginAsync; /** * The callback that returns true if the data can be directly loaded. @@ -145,12 +146,12 @@ export interface ISceneLoaderPluginBase { /** * The friendly name of this plugin. */ - name: string; + readonly name: string; /** * The file extensions supported by this plugin. */ - extensions: string | ISceneLoaderPluginExtensions; + readonly extensions: string | ISceneLoaderPluginExtensions; /** * The callback called when loading from a url. @@ -369,6 +370,114 @@ interface IFileInfo { rawData: Nullable; } +/** + * Defines options for SceneLoader plugins. This interface is extended by specific plugins. + */ +export interface SceneLoaderPluginOptions extends Record | undefined> {} + +/** + * Adds default/implicit options to plugin specific options. + */ +type DefaultPluginOptions = { + /** + * Defines if the plugin is enabled + */ + enabled?: boolean; +} & BasePluginOptions; + +// This captures the type defined inline for the pluginOptions property, which is just SceneLoaderPluginOptions wrapped with DefaultPluginOptions. +// We do it this way rather than explicitly defining the type here and then using it in SceneLoaderOptions because we want the full expanded type +// to show up in the user's intellisense to make it easier to understand what options are available. +type PluginOptions = SceneLoaderOptions["pluginOptions"]; + +type SceneSource = string | File | ArrayBufferView; + +/** + * Defines common options for loading operations performed by SceneLoader. + */ +interface SceneLoaderOptions { + /** + * 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) + */ + rootUrl?: string; + + /** + * A callback with a progress event for each file being loaded + */ + onProgress?: (event: ISceneLoaderProgressEvent) => void; + + /** + * The extension used to determine the plugin + */ + pluginExtension?: string; + + /** + * Defines the filename, if the data is binary + */ + name?: string; + + /** + * Defines options for the registered plugins + */ + pluginOptions?: { + // NOTE: This type is doing two things: + // 1. Adding an implicit 'enabled' property to the options for each plugin. + // 2. Creating a mapped type of all the options of all the plugins to make it just look like a consolidated plain object in intellisense for the user. + [Plugin in keyof SceneLoaderPluginOptions]: { + [Option in keyof DefaultPluginOptions]: DefaultPluginOptions[Option]; + }; + }; +} + +/** + * Defines options for ImportMeshAsync. + */ +export interface ImportMeshOptions extends SceneLoaderOptions { + /** + * An array of mesh names, a single mesh name, or empty string for all meshes that filter what meshes are imported + */ + meshNames?: string | readonly string[] | null | undefined; +} + +/** + * Defines options for LoadAsync. + */ +export interface LoadOptions extends SceneLoaderOptions {} + +/** + * Defines options for AppendAsync. + */ +export interface AppendOptions extends SceneLoaderOptions {} + +/** + * Defines options for LoadAssetContainerAsync. + */ +export interface LoadAssetContainerOptions extends SceneLoaderOptions {} + +/** + * Defines options for ImportAnimationsAsync. + */ +export interface ImportAnimationsOptions extends SceneLoaderOptions { + /** + * When true, animations are cleaned before importing new ones. Animations are appended otherwise + */ + overwriteAnimations?: boolean; + + /** + * Defines how to handle old animations groups before importing new ones + */ + animationGroupLoadingMode?: SceneLoaderAnimationGroupLoadingMode; + + /** + * defines a function used to convert animation targets from loaded scene to current scene (default: search node by name) + */ + targetConverter?: Nullable<(target: unknown) => unknown>; +} + +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 @@ -527,7 +636,8 @@ export class SceneLoader { onError: (message?: string, exception?: any) => void, onDispose: () => void, pluginExtension: Nullable, - name: string + name: string, + pluginOptions: PluginOptions ): Nullable { const directLoad = SceneLoader._GetDirectLoad(fileInfo.url); @@ -542,15 +652,22 @@ export class SceneLoader { ? SceneLoader._GetPluginForDirectLoad(fileInfo.url) : SceneLoader._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.`); + } + 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."; } - const plugin: IRegisteredPlugin["plugin"] = registeredPlugin.plugin.createPlugin?.() ?? registeredPlugin.plugin; + // 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 file type you are trying to load has not been found. If using es6, please import the plugin you wish to use before."; + 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.`; } SceneLoader.OnPluginActivatedObservable.notifyObservers(plugin); @@ -645,30 +762,29 @@ export class SceneLoader { return plugin; } - private static _GetFileInfo(rootUrl: string, sceneFilename: string | File | ArrayBufferView): Nullable { + private static _GetFileInfo(rootUrl: string, sceneSource: SceneSource): Nullable { let url: string; let name: string; let file: Nullable = null; let rawData: Nullable = null; - if (!sceneFilename) { + if (!sceneSource) { url = rootUrl; name = Tools.GetFilename(rootUrl); rootUrl = Tools.GetFolderPath(rootUrl); - } else if ((sceneFilename as File).name) { - const sceneFile = sceneFilename as File; - url = `file:${sceneFile.name}`; - name = sceneFile.name; - file = sceneFile; - } else if (ArrayBuffer.isView(sceneFilename)) { + } else if (isFile(sceneSource)) { + url = `file:${sceneSource.name}`; + name = sceneSource.name; + file = sceneSource; + } else if (ArrayBuffer.isView(sceneSource)) { url = ""; name = RandomGUID(); - rawData = sceneFilename as ArrayBufferView; - } else if (typeof sceneFilename === "string" && sceneFilename.startsWith("data:")) { - url = sceneFilename; + rawData = sceneSource; + } else if (sceneSource.startsWith("data:")) { + url = sceneSource; name = ""; - } else { - const filename = sceneFilename as string; + } else if (rootUrl) { + const filename = sceneSource; if (filename.substr(0, 1) === "/") { Tools.Error("Wrong sceneFilename parameter"); return null; @@ -676,6 +792,10 @@ export class SceneLoader { url = rootUrl + filename; name = filename; + } else { + url = sceneSource; + name = Tools.GetFilename(sceneSource); + rootUrl = Tools.GetFolderPath(sceneSource); } return { @@ -713,13 +833,13 @@ export class SceneLoader { */ public static RegisterPlugin(plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync): void { if (typeof plugin.extensions === "string") { - const extension = plugin.extensions; + const extension = plugin.extensions; SceneLoader._RegisteredPlugins[extension.toLowerCase()] = { plugin: plugin, isBinary: false, }; } else { - const extensions = plugin.extensions; + const extensions = plugin.extensions; Object.keys(extensions).forEach((extension) => { SceneLoader._RegisteredPlugins[extension.toLowerCase()] = { plugin: plugin, @@ -745,13 +865,28 @@ export class SceneLoader { public static ImportMesh( meshNames: string | readonly string[] | null | undefined, rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", + sceneFilename?: SceneSource, + scene?: Nullable, + onSuccess?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, + 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: string = "" + name = "", + pluginOptions: PluginOptions = {} ): Nullable { if (!scene) { Logger.Error("No scene available to import mesh to"); @@ -853,10 +988,21 @@ export class SceneLoader { errorHandler, disposeHandler, pluginExtension, - name + 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 + */ + 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 @@ -871,14 +1017,55 @@ export class SceneLoader { public static ImportMeshAsync( meshNames: string | readonly string[] | null | undefined, rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", - scene: Nullable = EngineStore.LastCreatedScene, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - pluginExtension: Nullable = null, - name: string = "" + sceneFilename?: SceneSource, + scene?: Nullable, + 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( + SceneLoader._ImportMesh( meshNames, rootUrl, sceneFilename, @@ -900,7 +1087,8 @@ export class SceneLoader { reject(exception || new Error(message)); }, pluginExtension, - name + name, + pluginOptions ); }); } @@ -919,22 +1107,46 @@ export class SceneLoader { */ public static Load( rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", + sceneFilename?: SceneSource, + engine?: Nullable, + onSuccess?: Nullable<(scene: Scene) => void>, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, + 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: string = "" + 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); + return SceneLoader._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 + */ + 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) @@ -947,14 +1159,53 @@ export class SceneLoader { */ public static LoadAsync( rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", - engine: Nullable = EngineStore.LastCreatedEngine, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - pluginExtension: Nullable = null, - name: string = "" + sceneFilename?: SceneSource, + engine?: Nullable, + 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( + SceneLoader._Load( rootUrl, sceneFilename, engine, @@ -966,7 +1217,8 @@ export class SceneLoader { reject(exception || new Error(message)); }, pluginExtension, - name + name, + pluginOptions ); }); } @@ -985,13 +1237,27 @@ export class SceneLoader { */ public static Append( rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", + sceneFilename?: SceneSource, + scene?: Nullable, + onSuccess?: Nullable<(scene: Scene) => void>, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, + 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: string = "" + name = "", + pluginOptions: PluginOptions = {} ): Nullable { if (!scene) { Logger.Error("No scene available to append to"); @@ -1083,10 +1349,21 @@ export class SceneLoader { errorHandler, disposeHandler, pluginExtension, - name + name, + pluginOptions ); } + /** + * 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) @@ -1099,14 +1376,53 @@ export class SceneLoader { */ public static AppendAsync( rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", - scene: Nullable = EngineStore.LastCreatedScene, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - pluginExtension: Nullable = null, - name: string = "" + sceneFilename?: SceneSource, + scene?: Nullable, + 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( + SceneLoader._Append( rootUrl, sceneFilename, scene, @@ -1118,7 +1434,8 @@ export class SceneLoader { reject(exception || new Error(message)); }, pluginExtension, - name + name, + pluginOptions ); }); } @@ -1137,13 +1454,27 @@ export class SceneLoader { */ public static LoadAssetContainer( rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", + sceneFilename?: SceneSource, + scene?: Nullable, + onSuccess?: Nullable<(assets: AssetContainer) => void>, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, + 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: string = "" + name = "", + pluginOptions: PluginOptions = {} ): Nullable { if (!scene) { Logger.Error("No scene available to load asset container to"); @@ -1230,10 +1561,21 @@ export class SceneLoader { errorHandler, disposeHandler, pluginExtension, - name + 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 + */ + 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) @@ -1241,17 +1583,60 @@ export class SceneLoader { * @param scene is the instance of Scene to append to * @param onProgress a callback with a progress event for each file being loaded * @param pluginExtension the extension used to determine the plugin + * @param name defines the filename, if the data is binary * @returns The loaded asset container */ public static LoadAssetContainerAsync( rootUrl: string, - sceneFilename: string | File = "", - scene: Nullable = EngineStore.LastCreatedScene, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - pluginExtension: Nullable = null + sceneFilename?: SceneSource, + scene?: Nullable, + 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( + SceneLoader._LoadAssetContainer( rootUrl, sceneFilename, scene, @@ -1262,7 +1647,9 @@ export class SceneLoader { (scene, message, exception) => { reject(exception || new Error(message)); }, - pluginExtension + pluginExtension, + name, + pluginOptions ); }); } @@ -1279,10 +1666,39 @@ export class SceneLoader { * @param onProgress a callback with a progress event for each file being loaded * @param onError a callback with the scene, a message, and possibly an exception when import fails * @param pluginExtension the extension used to determine the plugin + * @param name defines the filename, if the data is binary */ public static ImportAnimations( rootUrl: string, - sceneFilename: string | File = "", + 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 + ): 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, @@ -1290,7 +1706,9 @@ export class SceneLoader { 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 + pluginExtension: Nullable = null, + name = "", + pluginOptions: PluginOptions = {} ): void { if (!scene) { Logger.Error("No scene available to load animations to"); @@ -1353,9 +1771,19 @@ export class SceneLoader { } }; - this.LoadAssetContainer(rootUrl, sceneFilename, scene, onAssetContainerLoaded, onProgress, onError, pluginExtension); + this._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 + */ + 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) @@ -1368,24 +1796,71 @@ export class SceneLoader { * @param onProgress a callback with a progress event for each file being loaded * @param onError a callback with the scene, a message, and possibly an exception when import fails * @param pluginExtension the extension used to determine the plugin + * @param name defines the filename, if the data is binary * @returns the updated scene with imported animations */ public static ImportAnimationsAsync( rootUrl: string, - sceneFilename: string | File = "", - scene: Nullable = EngineStore.LastCreatedScene, - overwriteAnimations = true, - animationGroupLoadingMode = SceneLoaderAnimationGroupLoadingMode.Clean, - targetConverter: Nullable<(target: any) => any> = null, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onSuccess: Nullable<(scene: Scene) => void> = null, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, - pluginExtension: Nullable = null + 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 + ): 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( + SceneLoader._ImportAnimations( rootUrl, sceneFilename, scene, @@ -1399,7 +1874,9 @@ export class SceneLoader { (_scene: Scene, message: string, exception: any) => { reject(exception || new Error(message)); }, - pluginExtension + pluginExtension, + name, + pluginOptions ); }); } diff --git a/packages/dev/loaders/src/OBJ/objFileLoader.ts b/packages/dev/loaders/src/OBJ/objFileLoader.ts index fb72a21b41b..6ff36bfe4b3 100644 --- a/packages/dev/loaders/src/OBJ/objFileLoader.ts +++ b/packages/dev/loaders/src/OBJ/objFileLoader.ts @@ -13,6 +13,19 @@ import { SolidParser } from "./solidParser"; import type { Mesh } from "core/Meshes/mesh"; import { StandardMaterial } from "core/Materials/standardMaterial"; +// eslint-disable-next-line @typescript-eslint/naming-convention +const PLUGIN_OBJ = "obj"; + +declare module "core/Loading/sceneLoader" { + // eslint-disable-next-line jsdoc/require-jsdoc + export interface SceneLoaderPluginOptions { + /** + * Defines options for the obj loader. + */ + [PLUGIN_OBJ]?: {}; + } +} + /** * OBJ file type loader. * This is a babylon scene loader plugin. @@ -74,11 +87,11 @@ export class OBJFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlugi /** * Defines the name of the plugin. */ - public name = "obj"; + public readonly name = PLUGIN_OBJ; /** * Defines the extension the plugin is able to load. */ - public extensions = ".obj"; + public readonly extensions = ".obj"; private _assetContainer: Nullable = null; diff --git a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts index 3f7f1da40f7..55876aa28c7 100644 --- a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts +++ b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts @@ -11,6 +11,19 @@ import { GaussianSplattingMesh } from "core/Meshes/GaussianSplatting/gaussianSpl import type { AssetContainer } from "core/assetContainer"; import type { Scene } from "core/scene"; +// eslint-disable-next-line @typescript-eslint/naming-convention +const PLUGIN_SPLAT = "splat"; + +declare module "core/Loading/sceneLoader" { + // eslint-disable-next-line jsdoc/require-jsdoc + export interface SceneLoaderPluginOptions { + /** + * Defines options for the splat loader. + */ + [PLUGIN_SPLAT]?: {}; + } +} + /** * @experimental * SPLAT file type loader. @@ -20,18 +33,18 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu /** * Defines the name of the plugin. */ - public name = "splat"; + public readonly name = PLUGIN_SPLAT; /** * Defines the extensions the splat loader is able to load. * force data to come in as an ArrayBuffer */ - public extensions: ISceneLoaderPluginExtensions = { + public readonly extensions = { // eslint-disable-next-line @typescript-eslint/naming-convention ".splat": { isBinary: true }, // eslint-disable-next-line @typescript-eslint/naming-convention ".ply": { isBinary: true }, - }; + } as const satisfies ISceneLoaderPluginExtensions; //private _loadingOptions: SPLATLoadingOptions; /** diff --git a/packages/dev/loaders/src/STL/stlFileLoader.ts b/packages/dev/loaders/src/STL/stlFileLoader.ts index 43f57cdbdf0..6652716b4b2 100644 --- a/packages/dev/loaders/src/STL/stlFileLoader.ts +++ b/packages/dev/loaders/src/STL/stlFileLoader.ts @@ -9,6 +9,18 @@ import { SceneLoader } from "core/Loading/sceneLoader"; import { AssetContainer } from "core/assetContainer"; import type { Scene } from "core/scene"; +const PLUGIN_STL = "stl"; + +declare module "core/Loading/sceneLoader" { + // eslint-disable-next-line jsdoc/require-jsdoc + export interface SceneLoaderPluginOptions { + /** + * Defines options for the stl loader. + */ + [PLUGIN_STL]?: {}; + } +} + /** * STL file type loader. * This is a babylon scene loader plugin. @@ -27,16 +39,16 @@ export class STLFileLoader implements ISceneLoaderPlugin { /** * Defines the name of the plugin. */ - public name = "stl"; + public readonly name = PLUGIN_STL; /** * Defines the extensions the stl loader is able to load. * force data to come in as an ArrayBuffer * we'll convert to string if it looks like it's an ASCII .stl */ - public extensions: ISceneLoaderPluginExtensions = { + public readonly extensions = { ".stl": { isBinary: true }, - }; + } as const satisfies ISceneLoaderPluginExtensions; /** * Defines if Y and Z axes are swapped or not when loading an STL file. diff --git a/packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.ts b/packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.ts index 1f1c399346b..2136c391547 100644 --- a/packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.ts +++ b/packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.ts @@ -12,6 +12,21 @@ import type { IProperty, IMSFTLOD } from "babylonjs-gltf2interface"; const NAME = "MSFT_lod"; +declare module "../../glTFFileLoader" { + // eslint-disable-next-line jsdoc/require-jsdoc + export interface GLTFLoaderExtensionOptions { + /** + * Defines options for the MSFT_lod extension. + */ + [NAME]?: Partial<{ + /** + * Maximum number of LODs to load, starting from the lowest LOD. + */ + maxLODsToLoad: number; + }>; + } +} + interface IBufferInfo { start: number; end: number; @@ -76,6 +91,9 @@ export class MSFT_lod implements IGLTFLoaderExtension { */ constructor(loader: GLTFLoader) { this._loader = loader; + // Options takes precedence. The maxLODsToLoad extension property is retained for back compat. + // For new extensions, they should only use options. + this.maxLODsToLoad = this._loader.parent.extensionOptions[NAME]?.maxLODsToLoad ?? this.maxLODsToLoad; this.enabled = this._loader.isExtensionUsed(NAME); } diff --git a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts index d4aa957f4d7..a9d9e28cbf9 100644 --- a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts +++ b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts @@ -565,13 +565,21 @@ export class GLTFLoader implements IGLTFLoader { private _loadExtensions(): void { for (const name in GLTFLoader._RegisteredExtensions) { - const extension = GLTFLoader._RegisteredExtensions[name].factory(this); - if (extension.name !== name) { - Logger.Warn(`The name of the glTF loader extension instance does not match the registered name: ${extension.name} !== ${name}`); - } + // Don't load explicitly disabled extensions. + if (this.parent.extensionOptions[name]?.enabled === false) { + // But warn if the disabled extension is used by the model. + if (this.isExtensionUsed(name)) { + Logger.Warn(`Extension ${name} is used but has been explicitly disabled.`); + } + } else { + const extension = GLTFLoader._RegisteredExtensions[name].factory(this); + if (extension.name !== name) { + Logger.Warn(`The name of the glTF loader extension instance does not match the registered name: ${extension.name} !== ${name}`); + } - this._extensions.push(extension); - this._parent.onExtensionLoadedObservable.notifyObservers(extension); + this._extensions.push(extension); + this._parent.onExtensionLoadedObservable.notifyObservers(extension); + } } this._extensions.sort((a, b) => (a.order || Number.MAX_VALUE) - (b.order || Number.MAX_VALUE)); @@ -583,6 +591,9 @@ export class GLTFLoader implements IGLTFLoader { for (const name of this._gltf.extensionsRequired) { const available = this._extensions.some((extension) => extension.name === name && extension.enabled); if (!available) { + if (this.parent.extensionOptions[name]?.enabled === false) { + throw new Error(`Required extension ${name} is disabled`); + } throw new Error(`Required extension ${name} is not available`); } } diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index d94e1b717cc..b92dfcd997c 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -17,6 +17,7 @@ import type { ISceneLoaderAsyncResult, } from "core/Loading/sceneLoader"; import { SceneLoader } from "core/Loading/sceneLoader"; +import type { SceneLoaderPluginOptions } from "core/Loading/sceneLoader"; import { AssetContainer } from "core/assetContainer"; import type { Scene, IDisposable } from "core/scene"; import type { WebRequest } from "core/Misc/webRequest"; @@ -31,6 +32,23 @@ import { RuntimeError, ErrorCodes } from "core/Misc/error"; import type { TransformNode } from "core/Meshes/transformNode"; import type { MorphTargetManager } from "core/Morph/morphTargetManager"; +const PLUGIN_GLTF = "gltf"; + +/** + * Defines options for glTF loader extensions. This interface is extended by specific extensions. + */ +export interface GLTFLoaderExtensionOptions extends Record | undefined> {} + +declare module "core/Loading/sceneLoader" { + // eslint-disable-next-line jsdoc/require-jsdoc + export interface SceneLoaderPluginOptions { + /** + * Defines options for the glTF loader. + */ + [PLUGIN_GLTF]?: Partial; + } +} + interface IFileRequestInfo extends IFileRequest { _lengthComputable?: boolean; _loaded?: number; @@ -167,55 +185,26 @@ export interface IGLTFLoader extends IDisposable { } /** - * File loader for loading glTF files into a scene. + * Adds default/implicit options to extension specific options. */ -export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISceneLoaderPluginFactory { - /** @internal */ - public static _CreateGLTF1Loader: (parent: GLTFFileLoader) => IGLTFLoader; - - /** @internal */ - public static _CreateGLTF2Loader: (parent: GLTFFileLoader) => IGLTFLoader; - - // -------------- - // Common options - // -------------- - +type DefaultExtensionOptions = { /** - * Raised when the asset has been parsed + * Defines if the extension is enabled */ - public onParsedObservable = new Observable(); + enabled?: boolean; +} & BaseExtensionOptions; - private _onParsedObserver: Nullable>; - - /** - * Raised when the asset has been parsed - */ - public set onParsed(callback: (loaderData: IGLTFLoaderData) => void) { - if (this._onParsedObserver) { - this.onParsedObservable.remove(this._onParsedObserver); +class GLTFLoaderOptions { + // eslint-disable-next-line babylonjs/available + public constructor(options?: Partial>) { + if (options) { + for (const key in this) { + const typedKey = key as keyof GLTFLoaderOptions; + (this as Record)[typedKey] = options[typedKey] ?? this[typedKey]; + } } - this._onParsedObserver = this.onParsedObservable.add(callback); } - // ---------- - // V1 options - // ---------- - - /** - * Set this property to false to disable incremental loading which delays the loader from calling the success callback until after loading the meshes and shaders. - * Textures always loads asynchronously. For example, the success callback can compute the bounding information of the loaded meshes when incremental loading is disabled. - * Defaults to true. - * @internal - */ - public static IncrementalLoading = true; - - /** - * Set this property to true in order to work with homogeneous coordinates, available with some converters and exporters. - * Defaults to false. See https://en.wikipedia.org/wiki/Homogeneous_coordinates. - * @internal - */ - public static HomogeneousCoordinates = false; - // ---------- // V2 options // ---------- @@ -329,6 +318,69 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc */ public customRootNode?: Nullable; + /** + * Defines options for glTF extensions. + */ + public extensionOptions: { + // NOTE: This type is doing two things: + // 1. Adding an implicit 'enabled' property to the options for each extension. + // 2. Creating a mapped type of all the options of all the extensions to make it just look like a consolidated plain object in intellisense for the user. + [Extension in keyof GLTFLoaderExtensionOptions]: { + [Option in keyof DefaultExtensionOptions]: DefaultExtensionOptions[Option]; + }; + } = {}; +} + +/** + * File loader for loading glTF files into a scene. + */ +export class GLTFFileLoader extends GLTFLoaderOptions implements IDisposable, ISceneLoaderPluginAsync, ISceneLoaderPluginFactory { + /** @internal */ + public static _CreateGLTF1Loader: (parent: GLTFFileLoader) => IGLTFLoader; + + /** @internal */ + public static _CreateGLTF2Loader: (parent: GLTFFileLoader) => IGLTFLoader; + + // -------------- + // Common options + // -------------- + + /** + * Raised when the asset has been parsed + */ + public onParsedObservable = new Observable(); + + private _onParsedObserver: Nullable>; + + /** + * Raised when the asset has been parsed + */ + public set onParsed(callback: (loaderData: IGLTFLoaderData) => void) { + if (this._onParsedObserver) { + this.onParsedObservable.remove(this._onParsedObserver); + } + this._onParsedObserver = this.onParsedObservable.add(callback); + } + + // ---------- + // V1 options + // ---------- + + /** + * Set this property to false to disable incremental loading which delays the loader from calling the success callback until after loading the meshes and shaders. + * Textures always loads asynchronously. For example, the success callback can compute the bounding information of the loaded meshes when incremental loading is disabled. + * Defaults to true. + * @internal + */ + public static IncrementalLoading = true; + + /** + * Set this property to true in order to work with homogeneous coordinates, available with some converters and exporters. + * Defaults to false. See https://en.wikipedia.org/wiki/Homogeneous_coordinates. + * @internal + */ + public static HomogeneousCoordinates = false; + /** * Observable raised when the loader creates a mesh after parsing the glTF properties of the mesh. * Note that the observable is raised as soon as the mesh object is created, meaning some data may not have been setup yet for this mesh (vertex data, morph targets, material, ...) @@ -551,18 +603,18 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc private _progressCallback?: (event: ISceneLoaderProgressEvent) => void; private _requests = new Array(); - private static _MagicBase64Encoded = "Z2xURg"; // "glTF" base64 encoded (without the quotes!) + private static readonly _MagicBase64Encoded = "Z2xURg"; // "glTF" base64 encoded (without the quotes!) /** * Name of the loader ("gltf") */ - public name = "gltf"; + public readonly name = PLUGIN_GLTF; /** @internal */ - public extensions: ISceneLoaderPluginExtensions = { + public readonly extensions = { ".gltf": { isBinary: false }, ".glb": { isBinary: true }, - }; + } as const satisfies ISceneLoaderPluginExtensions; /** * Disposes the loader, releases resources during load, and cancels any outstanding requests. @@ -863,8 +915,8 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc public rewriteRootURL?(rootUrl: string, responseURL?: string): string; /** @internal */ - public createPlugin(): ISceneLoaderPluginAsync { - return new GLTFFileLoader(); + public createPlugin(options: SceneLoaderPluginOptions): ISceneLoaderPluginAsync { + return new GLTFFileLoader(options[PLUGIN_GLTF]); } /** diff --git a/packages/tools/viewer-alpha/src/viewer.ts b/packages/tools/viewer-alpha/src/viewer.ts index 98d38150161..6aaf71aa74e 100644 --- a/packages/tools/viewer-alpha/src/viewer.ts +++ b/packages/tools/viewer-alpha/src/viewer.ts @@ -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 SceneLoader.LoadAssetContainerAsync(finalSource, this._details.scene); this._details.model.addAllToScene(); this._reframeCamera(); });