From 9008c9a4c5147e569f13a88ff0c48b17d609e9ee Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Tue, 30 Jul 2024 15:44:44 -0700 Subject: [PATCH 01/23] Introduce pattern for extending SceneLoader options by plugins and extensions --- packages/dev/core/src/Loading/sceneLoader.ts | 99 ++++++++++++++++--- .../src/glTF/2.0/Extensions/MSFT_lod.ts | 29 +++++- .../dev/loaders/src/glTF/2.0/glTFLoader.ts | 21 ++-- .../dev/loaders/src/glTF/glTFFileLoader.ts | 50 ++++++++-- 4 files changed, 163 insertions(+), 36 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index a26f5cb79d1..340c4ab61f0 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -244,7 +244,7 @@ export interface ISceneLoaderPlugin extends ISceneLoaderPluginBase { * @param onError The callback when import fails * @returns The loaded asset container */ - loadAssetContainer(scene: Scene, data: unknown, rootUrl: string, onError?: (message: string, exception?: any) => void): AssetContainer; + loadAssetContainer(scene: Scene, data: unknown, rootUrl: string, onError?: (message: string, exception?: any) => void, options?: SceneLoaderPluginOptions): AssetContainer; } /** @@ -290,7 +290,14 @@ export interface ISceneLoaderPluginAsync extends ISceneLoaderPluginBase { * @param fileName Defines the name of the file to load * @returns The loaded asset container */ - loadAssetContainerAsync(scene: Scene, data: unknown, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, fileName?: string): Promise; + loadAssetContainerAsync( + scene: Scene, + data: unknown, + rootUrl: string, + onProgress?: (event: ISceneLoaderProgressEvent) => void, + fileName?: string, + options?: SceneLoaderPluginOptions + ): Promise; } /** @@ -369,6 +376,16 @@ interface IFileInfo { rawData: Nullable; } +export interface SceneLoaderPluginOptions extends Record) | undefined> {} + +export interface SceneLoaderOptions { + rootUrl?: string; + scene?: Scene; + onProgress?: (event: ISceneLoaderProgressEvent) => void; + pluginExtension?: string; + pluginOptions?: SceneLoaderPluginOptions; +} + /** * Class used to load scene from various file formats using registered plugins * @see https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes @@ -542,6 +559,8 @@ export class SceneLoader { ? SceneLoader._GetPluginForDirectLoad(fileInfo.url) : SceneLoader._GetPluginForFilename(fileInfo.url); + // TODO: Check if the extension is disabled + 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."; @@ -713,13 +732,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, @@ -1136,6 +1155,19 @@ export class SceneLoader { * @returns The loaded plugin */ public static LoadAssetContainer( + rootUrl: string, + sceneFilename?: string | File | ArrayBufferView, + 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._LoadAssetContainerCore(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name, {}); + } + + private static _LoadAssetContainerCore( rootUrl: string, sceneFilename: string | File | ArrayBufferView = "", scene: Nullable = EngineStore.LastCreatedScene, @@ -1143,7 +1175,8 @@ export class SceneLoader { onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, - name: string = "" + name: string = "", + pluginOptions: SceneLoaderPluginOptions = {} ): Nullable { if (!scene) { Logger.Error("No scene available to load asset container to"); @@ -1203,7 +1236,7 @@ export class SceneLoader { (plugin, data) => { if ((plugin as ISceneLoaderPlugin).loadAssetContainer) { const syncedPlugin = plugin; - const assetContainer = syncedPlugin.loadAssetContainer(scene, data, fileInfo.rootUrl, errorHandler); + const assetContainer = syncedPlugin.loadAssetContainer(scene, data, fileInfo.rootUrl, errorHandler, pluginOptions); if (!assetContainer) { return; } @@ -1213,7 +1246,7 @@ export class SceneLoader { } else if ((plugin as ISceneLoaderPluginAsync).loadAssetContainerAsync) { const asyncedPlugin = plugin; asyncedPlugin - .loadAssetContainerAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) + .loadAssetContainerAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name, pluginOptions) .then((assetContainer) => { assetContainer.populateRootNodes(); scene.loadingPluginName = plugin.name; @@ -1234,6 +1267,14 @@ export class SceneLoader { ); } + /** + * Load a scene into an asset container + * @param source The concatenation of rootURL and filename (e.g. http://example.com/test.glb) or a File object + * @param options Options to configure aspects of how the scene is loaded + * @returns The loaded asset container + */ + public static LoadAssetContainerAsync(source: string | File, options?: SceneLoaderOptions): 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) @@ -1245,13 +1286,43 @@ export class SceneLoader { */ public static LoadAssetContainerAsync( rootUrl: string, - sceneFilename: string | File = "", - scene: Nullable = EngineStore.LastCreatedScene, - onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, - pluginExtension: Nullable = null + sceneFilename?: string | File, + scene?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable + ): Promise; + + public static LoadAssetContainerAsync( + ...args: + | [ + rootUrl: string, + sceneFilename?: string | File, + scene?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + ] + | [source: string | File, options?: SceneLoaderOptions] ): Promise { + let rootUrl: string; + let sceneFilename: string | File | undefined; + let scene: Nullable | undefined; + let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; + let pluginExtension: Nullable | undefined; + let pluginOptions: SceneLoaderPluginOptions | undefined; + + const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [source: string | File, options?: SceneLoaderOptions] => { + return maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File); + }; + + if (isOptionsArgs(args)) { + sceneFilename = args[0]; + ({ rootUrl = "", scene, onProgress, pluginExtension, pluginOptions } = args[1] ?? {}); + } else { + [rootUrl, sceneFilename, scene, onProgress, pluginExtension] = args; + } + return new Promise((resolve, reject) => { - SceneLoader.LoadAssetContainer( + SceneLoader._LoadAssetContainerCore( rootUrl, sceneFilename, scene, @@ -1262,7 +1333,9 @@ export class SceneLoader { (scene, message, exception) => { reject(exception || new Error(message)); }, - pluginExtension + pluginExtension, + undefined, + pluginOptions ); }); } 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..6542532383f 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 @@ -8,10 +8,24 @@ import type { BaseTexture } from "core/Materials/Textures/baseTexture"; import type { INode, IMaterial, IBuffer, IScene } from "../glTFLoaderInterfaces"; import type { IGLTFLoaderExtension } from "../glTFLoaderExtension"; import { GLTFLoader, ArrayItem } from "../glTFLoader"; +import type { GLTFLoaderExtensionOptions } from "../../glTFFileLoader"; import type { IProperty, IMSFTLOD } from "babylonjs-gltf2interface"; const NAME = "MSFT_lod"; +// eslint-disable-next-line @typescript-eslint/naming-convention +export type MSFT_lodOptions = { + maxLODsToLoad: number; +}; + +declare module "../../glTFFileLoader" { + export interface GLTFLoaderExtensionOptions { + [NAME]?: Partial & { + enabled?: boolean; + }; + } +} + interface IBufferInfo { start: number; end: number; @@ -74,7 +88,10 @@ export class MSFT_lod implements IGLTFLoaderExtension { /** * @internal */ - constructor(loader: GLTFLoader) { + constructor( + loader: GLTFLoader, + private readonly _options: GLTFLoaderExtensionOptions + ) { this._loader = loader; this.enabled = this._loader.isExtensionUsed(NAME); } @@ -354,7 +371,11 @@ export class MSFT_lod implements IGLTFLoaderExtension { * @param ids */ private _getLODs(context: string, property: T, array: ArrayLike | undefined, ids: number[]): T[] { - if (this.maxLODsToLoad <= 0) { + // Options takes precedence. The maxLODsToLoad extension property is retained for back compat. + // For new extensions, they should only use options. + const maxLODsToLoad = this._options.MSFT_lod?.maxLODsToLoad ?? this.maxLODsToLoad; + + if (maxLODsToLoad <= 0) { throw new Error("maxLODsToLoad must be greater than zero"); } @@ -362,7 +383,7 @@ export class MSFT_lod implements IGLTFLoaderExtension { for (let i = ids.length - 1; i >= 0; i--) { properties.push(ArrayItem.Get(`${context}/ids/${ids[i]}`, array, ids[i])); - if (properties.length === this.maxLODsToLoad) { + if (properties.length === maxLODsToLoad) { return properties; } } @@ -414,4 +435,4 @@ export class MSFT_lod implements IGLTFLoaderExtension { } } -GLTFLoader.RegisterExtension(NAME, (loader) => new MSFT_lod(loader)); +GLTFLoader.RegisterExtension(NAME, (loader, options) => new MSFT_lod(loader, options)); diff --git a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts index 807009b1f31..94f30713002 100644 --- a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts +++ b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts @@ -65,7 +65,7 @@ import type { _IAnimationSamplerData, } from "./glTFLoaderInterfaces"; import type { IGLTFLoaderExtension } from "./glTFLoaderExtension"; -import type { IGLTFLoader, IGLTFLoaderData } from "../glTFFileLoader"; +import type { GLTFLoaderOptions, GLTFLoaderExtensionOptions, IGLTFLoader, IGLTFLoaderData } from "../glTFFileLoader"; import { GLTFFileLoader, GLTFLoaderState, GLTFLoaderCoordinateSystemMode, GLTFLoaderAnimationStartMode } from "../glTFFileLoader"; import type { IDataBuffer } from "core/Misc/dataReader"; import { DecodeBase64UrlToBinary, IsBase64DataUrl, LoadFileError } from "core/Misc/fileTools"; @@ -94,7 +94,7 @@ interface ILoaderProperty extends IProperty { } interface IRegisteredExtension { - factory: (loader: GLTFLoader) => IGLTFLoaderExtension; + factory: (loader: GLTFLoader, options: GLTFLoaderExtensionOptions) => IGLTFLoaderExtension; } interface IWithMetadata { @@ -223,7 +223,7 @@ export class GLTFLoader implements IGLTFLoader { * @param name The name of the loader extension. * @param factory The factory function that creates the loader extension. */ - public static RegisterExtension(name: string, factory: (loader: GLTFLoader) => IGLTFLoaderExtension): void { + public static RegisterExtension(name: string, factory: (loader: GLTFLoader, options: GLTFLoaderExtensionOptions) => IGLTFLoaderExtension): void { if (GLTFLoader.UnregisterExtension(name)) { Logger.Warn(`Extension with the name '${name}' already exists`); } @@ -300,7 +300,10 @@ export class GLTFLoader implements IGLTFLoader { /** * @internal */ - constructor(parent: GLTFFileLoader) { + constructor( + parent: GLTFFileLoader, + private readonly options: GLTFLoaderOptions + ) { this._parent = parent; } @@ -419,7 +422,7 @@ export class GLTFLoader implements IGLTFLoader { const oldBlockMaterialDirtyMechanism = this._babylonScene.blockMaterialDirtyMechanism; this._babylonScene.blockMaterialDirtyMechanism = true; - if (!this.parent.loadOnlyMaterials) { + if (!this.options.loadOnlyMaterials) { if (nodes) { promises.push(this.loadSceneAsync("/nodes", { nodes: nodes, index: -1 })); } else if (this._gltf.scene != undefined || (this._gltf.scenes && this._gltf.scenes[0])) { @@ -428,7 +431,7 @@ export class GLTFLoader implements IGLTFLoader { } } - if (!this.parent.skipMaterials && this.parent.loadAllMaterials && this._gltf.materials) { + if (!this.options.skipMaterials && this.parent.loadAllMaterials && this._gltf.materials) { for (let m = 0; m < this._gltf.materials.length; ++m) { const material = this._gltf.materials[m]; const context = "/materials/" + m; @@ -565,7 +568,7 @@ export class GLTFLoader implements IGLTFLoader { private _loadExtensions(): void { for (const name in GLTFLoader._RegisteredExtensions) { - const extension = GLTFLoader._RegisteredExtensions[name].factory(this); + const extension = GLTFLoader._RegisteredExtensions[name].factory(this, this.options.extensionOptions); if (extension.name !== name) { Logger.Warn(`The name of the glTF loader extension instance does not match the registered name: ${extension.name} !== ${name}`); } @@ -1037,7 +1040,7 @@ export class GLTFLoader implements IGLTFLoader { this._defaultBabylonMaterialData[babylonDrawMode] = babylonMaterial; } babylonMesh.material = babylonMaterial; - } else if (!this.parent.skipMaterials) { + } else if (!this.options.skipMaterials) { const material = ArrayItem.Get(`${context}/material`, this._gltf.materials, primitive.material); promises.push( this._loadMaterialAsync(`/materials/${material.index}`, material, babylonMesh, babylonDrawMode, (babylonMaterial) => { @@ -2968,4 +2971,4 @@ export class GLTFLoader implements IGLTFLoader { } } -GLTFFileLoader._CreateGLTF2Loader = (parent) => new GLTFLoader(parent); +GLTFFileLoader._CreateGLTF2Loader = (parent, options) => new GLTFLoader(parent, options); diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index 4c98760987e..f03fad68a49 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,25 @@ import { RuntimeError, ErrorCodes } from "core/Misc/error"; import type { TransformNode } from "core/Meshes/transformNode"; import type { MorphTargetManager } from "core/Morph/morphTargetManager"; +const NAME = "gltf"; + +export interface GLTFLoaderExtensionOptions extends Record) | undefined> {} + +export type GLTFLoaderOptions = { + loadOnlyMaterials: boolean; + skipMaterials: boolean; + coordinateSystemMode: GLTFLoaderCoordinateSystemMode; + extensionOptions: GLTFLoaderExtensionOptions; +}; + +declare module "core/Loading/sceneLoader" { + export interface SceneLoaderPluginOptions { + [NAME]?: Partial & { + enabled?: boolean; + }; + } +} + interface IFileRequestInfo extends IFileRequest { _lengthComputable?: boolean; _loaded?: number; @@ -171,10 +191,10 @@ export interface IGLTFLoader extends IDisposable { */ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISceneLoaderPluginFactory { /** @internal */ - public static _CreateGLTF1Loader: (parent: GLTFFileLoader) => IGLTFLoader; + public static _CreateGLTF1Loader: (parent: GLTFFileLoader, options: GLTFLoaderOptions) => IGLTFLoader; /** @internal */ - public static _CreateGLTF2Loader: (parent: GLTFFileLoader) => IGLTFLoader; + public static _CreateGLTF2Loader: (parent: GLTFFileLoader, options: GLTFLoaderOptions) => IGLTFLoader; // -------------- // Common options @@ -540,7 +560,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc /** * Name of the loader ("gltf") */ - public name = "gltf"; + public name = NAME; /** @internal */ public extensions: ISceneLoaderPluginExtensions = { @@ -723,7 +743,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc this.onParsedObservable.clear(); this._log(`Loading ${fileName || ""}`); - this._loader = this._getLoader(data); + this._loader = this._getLoader(data, null!); return this._loader.importMeshAsync(meshesNames, scene, null, data, rootUrl, onProgress, fileName); }); } @@ -737,7 +757,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc this.onParsedObservable.clear(); this._log(`Loading ${fileName || ""}`); - this._loader = this._getLoader(data); + this._loader = this._getLoader(data, null!); return this._loader.loadAsync(scene, data, rootUrl, onProgress, fileName); }); } @@ -750,14 +770,15 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc data: IGLTFLoaderData, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, - fileName?: string + fileName?: string, + options?: SceneLoaderPluginOptions ): Promise { return Promise.resolve().then(() => { this.onParsedObservable.notifyObservers(data); this.onParsedObservable.clear(); this._log(`Loading ${fileName || ""}`); - this._loader = this._getLoader(data); + this._loader = this._getLoader(data, this._resolveOptions(options)); // Prepare the asset container. const container = new AssetContainer(scene); @@ -922,6 +943,15 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc return request; } + private _resolveOptions(options?: SceneLoaderPluginOptions): GLTFLoaderOptions { + return { + loadOnlyMaterials: options?.[NAME]?.loadOnlyMaterials ?? this.loadOnlyMaterials, + skipMaterials: options?.[NAME]?.skipMaterials ?? this.skipMaterials, + coordinateSystemMode: options?.[NAME]?.coordinateSystemMode ?? this.coordinateSystemMode, + extensionOptions: options?.[NAME]?.extensionOptions ?? {}, + }; + } + private _onProgress(event: ProgressEvent, request: IFileRequestInfo): void { if (!this._progressCallback) { return; @@ -977,7 +1007,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc ); } - private _getLoader(loaderData: IGLTFLoaderData): IGLTFLoader { + private _getLoader(loaderData: IGLTFLoaderData, options: GLTFLoaderOptions): IGLTFLoader { const asset = (loaderData.json).asset || {}; this._log(`Asset version: ${asset.version}`); @@ -1000,7 +1030,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc } } - const createLoaders: { [key: number]: (parent: GLTFFileLoader) => IGLTFLoader } = { + const createLoaders: { [key: number]: (parent: GLTFFileLoader, options: GLTFLoaderOptions) => IGLTFLoader } = { 1: GLTFFileLoader._CreateGLTF1Loader, 2: GLTFFileLoader._CreateGLTF2Loader, }; @@ -1010,7 +1040,7 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc throw new Error("Unsupported version: " + asset.version); } - return createLoader(this); + return createLoader(this, options); } private _parseJson(json: string): Object { From 1bbf8358492c0c5ed8a615bf4c7b5a434ed6c882 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Tue, 30 Jul 2024 16:00:01 -0700 Subject: [PATCH 02/23] Small comment updates --- packages/dev/core/src/Loading/sceneLoader.ts | 2 +- packages/dev/loaders/src/glTF/2.0/glTFLoader.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 340c4ab61f0..a5cd56cdcc6 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -559,7 +559,7 @@ export class SceneLoader { ? SceneLoader._GetPluginForDirectLoad(fileInfo.url) : SceneLoader._GetPluginForFilename(fileInfo.url); - // TODO: Check if the extension is disabled + // TODO: Check if the plugin is disabled? if (fileInfo.rawData && !registeredPlugin.isBinary) { // eslint-disable-next-line no-throw-literal diff --git a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts index 94f30713002..c925acf1c53 100644 --- a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts +++ b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts @@ -568,6 +568,7 @@ export class GLTFLoader implements IGLTFLoader { private _loadExtensions(): void { for (const name in GLTFLoader._RegisteredExtensions) { + // TODO: Check if the extension is disabled? const extension = GLTFLoader._RegisteredExtensions[name].factory(this, this.options.extensionOptions); if (extension.name !== name) { Logger.Warn(`The name of the glTF loader extension instance does not match the registered name: ${extension.name} !== ${name}`); From cc8de5a49e1f6203b32da292699560cdefeab49d Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 1 Aug 2024 14:34:02 -0700 Subject: [PATCH 03/23] Flesh out more of the impl --- packages/dev/core/src/Loading/sceneLoader.ts | 397 +++++++++++++++--- .../dev/loaders/src/glTF/2.0/glTFLoader.ts | 10 +- .../dev/loaders/src/glTF/glTFFileLoader.ts | 30 +- 3 files changed, 373 insertions(+), 64 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index a5cd56cdcc6..93c6895685b 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -223,7 +223,8 @@ export interface ISceneLoaderPlugin extends ISceneLoaderPluginBase { meshes: AbstractMesh[], particleSystems: IParticleSystem[], skeletons: Skeleton[], - onError?: (message: string, exception?: any) => void + onError?: (message: string, exception?: any) => void, + options?: SceneLoaderPluginOptions ): boolean; /** @@ -234,7 +235,7 @@ export interface ISceneLoaderPlugin extends ISceneLoaderPluginBase { * @param onError The callback when import fails * @returns True if successful or false otherwise */ - load(scene: Scene, data: unknown, rootUrl: string, onError?: (message: string, exception?: any) => void): boolean; + load(scene: Scene, data: unknown, rootUrl: string, onError?: (message: string, exception?: any) => void, options?: SceneLoaderPluginOptions): boolean; /** * Load into an asset container. @@ -267,7 +268,8 @@ export interface ISceneLoaderPluginAsync extends ISceneLoaderPluginBase { data: unknown, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, - fileName?: string + fileName?: string, + options?: SceneLoaderPluginOptions ): Promise; /** @@ -279,7 +281,14 @@ export interface ISceneLoaderPluginAsync extends ISceneLoaderPluginBase { * @param fileName Defines the name of the file to load * @returns Nothing */ - loadAsync(scene: Scene, data: unknown, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, fileName?: string): Promise; + loadAsync( + scene: Scene, + data: unknown, + rootUrl: string, + onProgress?: (event: ISceneLoaderProgressEvent) => void, + fileName?: string, + options?: SceneLoaderPluginOptions + ): Promise; /** * Load into an asset container. @@ -376,14 +385,84 @@ interface IFileInfo { rawData: Nullable; } -export interface SceneLoaderPluginOptions extends Record) | undefined> {} +/** + * Defines options for SceneLoader plugins. This interface is extended by specific plugins. + */ +export interface SceneLoaderPluginOptions extends Record | undefined> {} -export interface SceneLoaderOptions { +/** + * 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; - scene?: Scene; + + /** + * A callback with a progress event for each file being loaded + */ onProgress?: (event: ISceneLoaderProgressEvent) => void; + + /** + * The extension used to determine the plugin + */ pluginExtension?: string; - pluginOptions?: SceneLoaderPluginOptions; + + /** + * Defines the filename, if the data is binary + */ + name?: string; + + /** + * Defines options for the registered plugins + */ + pluginOptions?: { [P in keyof SceneLoaderPluginOptions]: SceneLoaderPluginOptions[P] & { enabled?: boolean } }; +} + +/** + * 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; + + /** + * The instance of BABYLON.Scene to append to + */ + scene?: Scene; +} + +/** + * Defines options for LoadAsync. + */ +export interface LoadOptions extends SceneLoaderOptions { + /** + * The instance of BABYLON.Engine to use to create the scene + */ + engine?: AbstractEngine; +} + +/** + * Defines options for AppendAsync. + */ +export interface AppendOptions extends SceneLoaderOptions { + /** + * The instance of BABYLON.Scene to append to + */ + scene?: Scene; +} + +/** + * Defines options for LoadAssetContainerAsync. + */ +export interface LoadAssetContainerOptions extends SceneLoaderOptions { + /** + * The instance of BABYLON.Scene to append to + */ + scene?: Scene; } /** @@ -771,6 +850,21 @@ export class SceneLoader { onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, 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: string | File | ArrayBufferView = "", + 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 = "", + pluginOptions: SceneLoaderPluginOptions = {} ): Nullable { if (!scene) { Logger.Error("No scene available to import mesh to"); @@ -840,7 +934,7 @@ export class SceneLoader { const particleSystems: IParticleSystem[] = []; const skeletons: Skeleton[] = []; - if (!syncedPlugin.importMesh(meshNames, scene, data, fileInfo.rootUrl, meshes, particleSystems, skeletons, errorHandler)) { + if (!syncedPlugin.importMesh(meshNames, scene, data, fileInfo.rootUrl, meshes, particleSystems, skeletons, errorHandler, pluginOptions)) { return; } @@ -849,7 +943,7 @@ export class SceneLoader { } else { const asyncedPlugin = plugin; asyncedPlugin - .importMeshAsync(meshNames, scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) + .importMeshAsync(meshNames, scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name, pluginOptions) .then((result) => { scene.loadingPluginName = plugin.name; successHandler( @@ -876,6 +970,14 @@ export class SceneLoader { ); } + /** + * Import meshes into a scene + * @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 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: string | File | ArrayBufferView, 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 @@ -890,14 +992,56 @@ 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?: string | File | ArrayBufferView, + 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?: string | File | ArrayBufferView, + scene?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + name?: string, + ] + | [source: string | File | ArrayBufferView, options?: ImportMeshOptions] ): Promise { + let meshNames: string | readonly string[] | null | undefined; + let rootUrl: string; + let sceneFilename: string | File | ArrayBufferView | undefined; + let scene: Nullable | undefined; + let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; + let pluginExtension: Nullable | undefined; + let name: string | undefined; + let pluginOptions: SceneLoaderPluginOptions | undefined; + + // 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 [source: string | File | ArrayBufferView, options?: ImportMeshOptions] => { + // If the first argument is a File or an ArrayBufferView, then it must be the options overload. + // If there is only a single string argument, then we should use the legacy overload for back compat. + // If there are more than one arguments, and the second argument is a object, then it must be the options overload. + return maybeOptionsArgs[0] instanceof File || ArrayBuffer.isView(maybeOptionsArgs[0]) || (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object"); + }; + + if (isOptionsArgs(args)) { + // Source is mapped to sceneFileName + sceneFilename = args[0]; + // Options determine the rest of the arguments + ({ meshNames, rootUrl = "", scene, onProgress, pluginExtension, name, pluginOptions } = args[1] ?? {}); + } 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, @@ -919,7 +1063,8 @@ export class SceneLoader { reject(exception || new Error(message)); }, pluginExtension, - name + name, + pluginOptions ); }); } @@ -937,6 +1082,19 @@ export class SceneLoader { * @returns The loaded plugin */ public static Load( + rootUrl: string, + sceneFilename?: string | File | ArrayBufferView, + 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: string | File | ArrayBufferView = "", engine: Nullable = EngineStore.LastCreatedEngine, @@ -944,16 +1102,25 @@ export class SceneLoader { onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, - name: string = "" + name: string = "", + pluginOptions: SceneLoaderPluginOptions = {} ): 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 + * @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 options an object that configures aspects of how the scene is loaded + * @returns The loaded scene + */ + public static LoadAsync(source: string | File | ArrayBufferView, 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) @@ -966,14 +1133,58 @@ 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?: string | File | ArrayBufferView, + engine?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + name?: string + ): Promise; + + public static LoadAsync( + ...args: + | [ + rootUrl: string, + sceneFilename?: string | File | ArrayBufferView, + engine?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + name?: string, + ] + | [source: string | File | ArrayBufferView, options?: LoadOptions] ): Promise { + let rootUrl: string; + let sceneFilename: string | File | ArrayBufferView | undefined; + let engine: Nullable | undefined; + let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; + let pluginExtension: Nullable | undefined; + let name: string | undefined; + let pluginOptions: SceneLoaderPluginOptions | undefined; + + // 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 [source: string | File | ArrayBufferView, options?: LoadOptions] => { + // If the first argument is a File or an ArrayBufferView, then it must be the options overload. + // If there is only a single string argument, then we should use the legacy overload for back compat. + // If there are more than one arguments, and the second argument is a object but not a File, then it must be the options overload. + return ( + maybeOptionsArgs[0] instanceof File || + ArrayBuffer.isView(maybeOptionsArgs[0]) || + (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File)) + ); + }; + + if (isOptionsArgs(args)) { + // Source is mapped to sceneFileName + sceneFilename = args[0]; + // Options determine the rest of the arguments + ({ rootUrl = "", engine, onProgress, pluginExtension, name, pluginOptions } = args[1] ?? {}); + } 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, @@ -985,7 +1196,8 @@ export class SceneLoader { reject(exception || new Error(message)); }, pluginExtension, - name + name, + pluginOptions ); }); } @@ -1003,6 +1215,19 @@ export class SceneLoader { * @returns The loaded plugin */ public static Append( + rootUrl: string, + sceneFilename?: string | File | ArrayBufferView, + 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: string | File | ArrayBufferView = "", scene: Nullable = EngineStore.LastCreatedScene, @@ -1010,7 +1235,8 @@ export class SceneLoader { onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, - name: string = "" + name: string = "", + pluginOptions: SceneLoaderPluginOptions = {} ): Nullable { if (!scene) { Logger.Error("No scene available to append to"); @@ -1079,7 +1305,7 @@ export class SceneLoader { (plugin, data) => { if ((plugin as ISceneLoaderPlugin).load) { const syncedPlugin = plugin; - if (!syncedPlugin.load(scene, data, fileInfo.rootUrl, errorHandler)) { + if (!syncedPlugin.load(scene, data, fileInfo.rootUrl, errorHandler, pluginOptions)) { return; } @@ -1088,7 +1314,7 @@ export class SceneLoader { } else { const asyncedPlugin = plugin; asyncedPlugin - .loadAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) + .loadAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name, pluginOptions) .then(() => { scene.loadingPluginName = plugin.name; successHandler(); @@ -1106,6 +1332,14 @@ export class SceneLoader { ); } + /** + * Append a scene + * @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 options an object that configures aspects of how the scene is loaded + * @returns The given scene + */ + public static AppendAsync(source: string | File | ArrayBufferView, 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) @@ -1118,14 +1352,58 @@ 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?: string | File | ArrayBufferView, + scene?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + name?: string + ): Promise; + + public static AppendAsync( + ...args: + | [ + rootUrl: string, + sceneFilename?: string | File | ArrayBufferView, + scene?: Nullable, + onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, + pluginExtension?: Nullable, + name?: string, + ] + | [source: string | File | ArrayBufferView, options?: AppendOptions] ): Promise { + let rootUrl: string; + let sceneFilename: string | File | ArrayBufferView | undefined; + let scene: Nullable | undefined; + let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; + let pluginExtension: Nullable | undefined; + let name: string | undefined; + let pluginOptions: SceneLoaderPluginOptions | undefined; + + // 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 [source: string | File | ArrayBufferView, options?: AppendOptions] => { + // If the first argument is a File or an ArrayBufferView, then it must be the options overload. + // If there is only a single string argument, then we should use the legacy overload for back compat. + // If there are more than one arguments, and the second argument is a object but not a File, then it must be the options overload. + return ( + maybeOptionsArgs[0] instanceof File || + ArrayBuffer.isView(args[0]) || + (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File)) + ); + }; + + if (isOptionsArgs(args)) { + // Source is mapped to sceneFileName + sceneFilename = args[0]; + // Options determine the rest of the arguments + ({ rootUrl = "", scene, onProgress, pluginExtension, name, pluginOptions } = args[1] ?? {}); + } 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, @@ -1137,7 +1415,8 @@ export class SceneLoader { reject(exception || new Error(message)); }, pluginExtension, - name + name, + pluginOptions ); }); } @@ -1164,10 +1443,10 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string ): Nullable { - return SceneLoader._LoadAssetContainerCore(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name, {}); + return SceneLoader._LoadAssetContainer(rootUrl, sceneFilename, scene, onSuccess, onProgress, onError, pluginExtension, name); } - private static _LoadAssetContainerCore( + private static _LoadAssetContainer( rootUrl: string, sceneFilename: string | File | ArrayBufferView = "", scene: Nullable = EngineStore.LastCreatedScene, @@ -1269,11 +1548,11 @@ export class SceneLoader { /** * Load a scene into an asset container - * @param source The concatenation of rootURL and filename (e.g. http://example.com/test.glb) or a File object - * @param options Options to configure aspects of how the scene is loaded + * @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 options an object that configures aspects of how the scene is loaded * @returns The loaded asset container */ - public static LoadAssetContainerAsync(source: string | File, options?: SceneLoaderOptions): Promise; + public static LoadAssetContainerAsync(source: string | File | ArrayBufferView, options?: LoadAssetContainerOptions): Promise; /** * Load a scene into an asset container @@ -1282,6 +1561,7 @@ 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( @@ -1289,9 +1569,12 @@ export class SceneLoader { sceneFilename?: string | File, scene?: Nullable, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, - pluginExtension?: Nullable + 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: | [ @@ -1300,29 +1583,43 @@ export class SceneLoader { scene?: Nullable, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, + name?: string, ] - | [source: string | File, options?: SceneLoaderOptions] + | [source: string | File | ArrayBufferView, options?: LoadAssetContainerOptions] ): Promise { let rootUrl: string; - let sceneFilename: string | File | undefined; + let sceneFilename: string | File | ArrayBufferView | undefined; let scene: Nullable | undefined; let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; + let name: string | undefined; let pluginOptions: SceneLoaderPluginOptions | undefined; - const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [source: string | File, options?: SceneLoaderOptions] => { - return maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File); + // 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 [source: string | File | ArrayBufferView, options?: LoadAssetContainerOptions] => { + // If the first argument is a File or an ArrayBufferView, then it must be the options overload. + // If there is only a single string argument, then we should use the legacy overload for back compat. + // If there are more than one arguments, and the second argument is a object but not a File, then it must be the options overload. + return ( + maybeOptionsArgs[0] instanceof File || + ArrayBuffer.isView(args[0]) || + (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File)) + ); }; if (isOptionsArgs(args)) { + // Source is mapped to sceneFileName sceneFilename = args[0]; - ({ rootUrl = "", scene, onProgress, pluginExtension, pluginOptions } = args[1] ?? {}); + // Options determine the rest of the arguments + ({ rootUrl = "", scene, onProgress, pluginExtension, name, pluginOptions } = args[1] ?? {}); } else { - [rootUrl, sceneFilename, scene, onProgress, pluginExtension] = args; + // For the legacy signature, we just directly map each argument + [rootUrl, sceneFilename, scene, onProgress, pluginExtension, name] = args; } return new Promise((resolve, reject) => { - SceneLoader._LoadAssetContainerCore( + SceneLoader._LoadAssetContainer( rootUrl, sceneFilename, scene, @@ -1334,7 +1631,7 @@ export class SceneLoader { reject(exception || new Error(message)); }, pluginExtension, - undefined, + name, pluginOptions ); }); diff --git a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts index c925acf1c53..a491934665c 100644 --- a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts +++ b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts @@ -302,7 +302,7 @@ export class GLTFLoader implements IGLTFLoader { */ constructor( parent: GLTFFileLoader, - private readonly options: GLTFLoaderOptions + private readonly _options: GLTFLoaderOptions ) { this._parent = parent; } @@ -422,7 +422,7 @@ export class GLTFLoader implements IGLTFLoader { const oldBlockMaterialDirtyMechanism = this._babylonScene.blockMaterialDirtyMechanism; this._babylonScene.blockMaterialDirtyMechanism = true; - if (!this.options.loadOnlyMaterials) { + if (!this._options.loadOnlyMaterials) { if (nodes) { promises.push(this.loadSceneAsync("/nodes", { nodes: nodes, index: -1 })); } else if (this._gltf.scene != undefined || (this._gltf.scenes && this._gltf.scenes[0])) { @@ -431,7 +431,7 @@ export class GLTFLoader implements IGLTFLoader { } } - if (!this.options.skipMaterials && this.parent.loadAllMaterials && this._gltf.materials) { + if (!this._options.skipMaterials && this.parent.loadAllMaterials && this._gltf.materials) { for (let m = 0; m < this._gltf.materials.length; ++m) { const material = this._gltf.materials[m]; const context = "/materials/" + m; @@ -569,7 +569,7 @@ export class GLTFLoader implements IGLTFLoader { private _loadExtensions(): void { for (const name in GLTFLoader._RegisteredExtensions) { // TODO: Check if the extension is disabled? - const extension = GLTFLoader._RegisteredExtensions[name].factory(this, this.options.extensionOptions); + const extension = GLTFLoader._RegisteredExtensions[name].factory(this, this._options.extensionOptions); if (extension.name !== name) { Logger.Warn(`The name of the glTF loader extension instance does not match the registered name: ${extension.name} !== ${name}`); } @@ -1041,7 +1041,7 @@ export class GLTFLoader implements IGLTFLoader { this._defaultBabylonMaterialData[babylonDrawMode] = babylonMaterial; } babylonMesh.material = babylonMaterial; - } else if (!this.options.skipMaterials) { + } else if (!this._options.skipMaterials) { const material = ArrayItem.Get(`${context}/material`, this._gltf.materials, primitive.material); promises.push( this._loadMaterialAsync(`/materials/${material.index}`, material, babylonMesh, babylonDrawMode, (babylonMaterial) => { diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index f03fad68a49..43700dca9f3 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -34,20 +34,24 @@ import type { MorphTargetManager } from "core/Morph/morphTargetManager"; const NAME = "gltf"; -export interface GLTFLoaderExtensionOptions extends Record) | undefined> {} +/** + * Defines options for glTF loader extensions. This interface is extended by specific extensions. + */ +export interface GLTFLoaderExtensionOptions extends Record | undefined> {} +/** + * Defines options for the glTF loader. + */ export type GLTFLoaderOptions = { loadOnlyMaterials: boolean; skipMaterials: boolean; coordinateSystemMode: GLTFLoaderCoordinateSystemMode; - extensionOptions: GLTFLoaderExtensionOptions; + extensionOptions: { [P in keyof GLTFLoaderExtensionOptions]: GLTFLoaderExtensionOptions[P] & { enabled?: boolean } }; }; declare module "core/Loading/sceneLoader" { export interface SceneLoaderPluginOptions { - [NAME]?: Partial & { - enabled?: boolean; - }; + [NAME]?: Partial; } } @@ -736,14 +740,15 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc data: IGLTFLoaderData, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, - fileName?: string + fileName?: string, + options?: SceneLoaderPluginOptions ): Promise { return Promise.resolve().then(() => { this.onParsedObservable.notifyObservers(data); this.onParsedObservable.clear(); this._log(`Loading ${fileName || ""}`); - this._loader = this._getLoader(data, null!); + this._loader = this._getLoader(data, this._resolveOptions(options)); return this._loader.importMeshAsync(meshesNames, scene, null, data, rootUrl, onProgress, fileName); }); } @@ -751,13 +756,20 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc /** * @internal */ - public loadAsync(scene: Scene, data: IGLTFLoaderData, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, fileName?: string): Promise { + public loadAsync( + scene: Scene, + data: IGLTFLoaderData, + rootUrl: string, + onProgress?: (event: ISceneLoaderProgressEvent) => void, + fileName?: string, + options?: SceneLoaderPluginOptions + ): Promise { return Promise.resolve().then(() => { this.onParsedObservable.notifyObservers(data); this.onParsedObservable.clear(); this._log(`Loading ${fileName || ""}`); - this._loader = this._getLoader(data, null!); + this._loader = this._getLoader(data, this._resolveOptions(options)); return this._loader.loadAsync(scene, data, rootUrl, onProgress, fileName); }); } From 796a46a871b6cae3fde252a5eeb9133c5dd2ad34 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 1 Aug 2024 14:59:54 -0700 Subject: [PATCH 04/23] Add ImportMeshAsync with config overload --- packages/dev/core/src/Loading/sceneLoader.ts | 141 ++++++++++++++++--- 1 file changed, 125 insertions(+), 16 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 93c6895685b..74c8ca0f739 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -465,6 +465,31 @@ export interface LoadAssetContainerOptions extends SceneLoaderOptions { scene?: Scene; } +/** + * Defines options for ImportAnimationsAsync. + */ +export interface ImportAnimationsOptions extends SceneLoaderOptions { + /** + * The instance of BABYLON.Scene to append to + */ + scene?: Scene; + + /** + * 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>; +} + /** * Class used to load scene from various file formats using registered plugins * @see https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes @@ -1652,7 +1677,33 @@ export class SceneLoader { */ public static ImportAnimations( rootUrl: string, - sceneFilename: string | File = "", + sceneFilename?: string | File, + 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 + ): void { + SceneLoader._ImportAnimations( + rootUrl, + sceneFilename, + scene, + overwriteAnimations, + animationGroupLoadingMode, + targetConverter, + onSuccess, + onProgress, + onError, + pluginExtension + ); + } + + private static _ImportAnimations( + rootUrl: string, + sceneFilename: string | File | ArrayBufferView = "", scene: Nullable = EngineStore.LastCreatedScene, overwriteAnimations = true, animationGroupLoadingMode = SceneLoaderAnimationGroupLoadingMode.Clean, @@ -1660,7 +1711,8 @@ 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, + pluginOptions: SceneLoaderPluginOptions = {} ): void { if (!scene) { Logger.Error("No scene available to load animations to"); @@ -1723,9 +1775,17 @@ export class SceneLoader { } }; - this.LoadAssetContainer(rootUrl, sceneFilename, scene, onAssetContainerLoaded, onProgress, onError, pluginExtension); + this._LoadAssetContainer(rootUrl, sceneFilename, scene, onAssetContainerLoaded, onProgress, onError, pluginExtension, undefined, pluginOptions); } + /** + * Import animations from a file into a scene + * @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 options an object that configures aspects of how the scene is loaded + * @returns The loaded asset container + */ + public static ImportAnimationsAsync(source: string | File | ArrayBufferView, 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) @@ -1742,20 +1802,68 @@ export class SceneLoader { */ 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?: string | File | ArrayBufferView, + 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 + ): Promise; + + public static ImportAnimationsAsync( + ...args: + | [ + rootUrl: string, + sceneFilename?: string | File | ArrayBufferView, + 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, + ] + | [source: string | File | ArrayBufferView, options?: ImportAnimationsOptions] ): Promise { + let rootUrl: string; + let sceneFilename: string | File | ArrayBufferView | 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 pluginOptions: SceneLoaderPluginOptions | undefined; + + // 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 [source: string | File | ArrayBufferView, options?: ImportAnimationsOptions] => { + // If the first argument is a File or an ArrayBufferView, then it must be the options overload. + // If there is only a single string argument, then we should use the legacy overload for back compat. + // If there are more than one arguments, and the second argument is a object but not a File, then it must be the options overload. + return ( + maybeOptionsArgs[0] instanceof File || + ArrayBuffer.isView(args[0]) || + (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File)) + ); + }; + + if (isOptionsArgs(args)) { + // Source is mapped to sceneFileName + sceneFilename = args[0]; + // Options determine the rest of the arguments + ({ rootUrl = "", scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, onProgress, pluginExtension, pluginOptions } = args[1] ?? {}); + } else { + // For the legacy signature, we just directly map each argument + [rootUrl, sceneFilename, scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, , onProgress, , pluginExtension] = args; + } + return new Promise((resolve, reject) => { - SceneLoader.ImportAnimations( + SceneLoader._ImportAnimations( rootUrl, sceneFilename, scene, @@ -1769,7 +1877,8 @@ export class SceneLoader { (_scene: Scene, message: string, exception: any) => { reject(exception || new Error(message)); }, - pluginExtension + pluginExtension, + pluginOptions ); }); } From 87b60f934704786303167bb7c1f3f1a892878614 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 1 Aug 2024 15:00:35 -0700 Subject: [PATCH 05/23] Missed a Ctrl+S, oops --- packages/dev/core/src/Loading/sceneLoader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 74c8ca0f739..48b60ebdf2e 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -487,7 +487,7 @@ export interface ImportAnimationsOptions extends SceneLoaderOptions { /** * defines a function used to convert animation targets from loaded scene to current scene (default: search node by name) */ - targetConverter: Nullable<(target: unknown) => unknown>; + targetConverter?: Nullable<(target: unknown) => unknown>; } /** From 2c8e4f12d15f38c8eb517c826de21f51949c54ca Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 1 Aug 2024 15:06:30 -0700 Subject: [PATCH 06/23] Fix logic in ImportMeshAsync for determining correct overload --- packages/dev/core/src/Loading/sceneLoader.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 48b60ebdf2e..5d0800d4276 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -1049,10 +1049,9 @@ export class SceneLoader { // 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 [source: string | File | ArrayBufferView, options?: ImportMeshOptions] => { - // If the first argument is a File or an ArrayBufferView, then it must be the options overload. - // If there is only a single string argument, then we should use the legacy overload for back compat. - // If there are more than one arguments, and the second argument is a object, then it must be the options overload. - return maybeOptionsArgs[0] instanceof File || ArrayBuffer.isView(maybeOptionsArgs[0]) || (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object"); + // If there is only a single argument, then it must be the options overload. + // If the second argument is an object, then it must be the options overload. + return maybeOptionsArgs.length === 1 || typeof maybeOptionsArgs[1] === "object"; }; if (isOptionsArgs(args)) { From 569dd4f590bec2529faad15cb3200bd5aadf9e98 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 1 Aug 2024 15:33:30 -0700 Subject: [PATCH 07/23] More fixes for choosing overload --- packages/dev/core/src/Loading/sceneLoader.ts | 27 +++++++++----------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 5d0800d4276..ba10d4a8662 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -1187,13 +1187,12 @@ export class SceneLoader { // 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 [source: string | File | ArrayBufferView, options?: LoadOptions] => { - // If the first argument is a File or an ArrayBufferView, then it must be the options overload. + // If the first argument is not a string, then it must be the options overload. // If there is only a single string argument, then we should use the legacy overload for back compat. - // If there are more than one arguments, and the second argument is a object but not a File, then it must be the options overload. + // If there are more than one arguments, and the second argument is a object but not a File or an ArrayBuffer, then it must be the options overload. return ( - maybeOptionsArgs[0] instanceof File || - ArrayBuffer.isView(maybeOptionsArgs[0]) || - (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File)) + !(typeof maybeOptionsArgs[0] === "string") || + (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File) && !ArrayBuffer.isView(maybeOptionsArgs[1])) ); }; @@ -1622,13 +1621,12 @@ export class SceneLoader { // 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 [source: string | File | ArrayBufferView, options?: LoadAssetContainerOptions] => { - // If the first argument is a File or an ArrayBufferView, then it must be the options overload. + // If the first argument is not a string, then it must be the options overload. // If there is only a single string argument, then we should use the legacy overload for back compat. - // If there are more than one arguments, and the second argument is a object but not a File, then it must be the options overload. + // If there are more than one arguments, and the second argument is a object but not a File or an ArrayBufferView, then it must be the options overload. return ( - maybeOptionsArgs[0] instanceof File || - ArrayBuffer.isView(args[0]) || - (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File)) + !(typeof maybeOptionsArgs[0] === "string") || + (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File) && !ArrayBuffer.isView(maybeOptionsArgs[1])) ); }; @@ -1841,13 +1839,12 @@ export class SceneLoader { // 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 [source: string | File | ArrayBufferView, options?: ImportAnimationsOptions] => { - // If the first argument is a File or an ArrayBufferView, then it must be the options overload. + // If the first argument is not a string, then it must be the options overload. // If there is only a single string argument, then we should use the legacy overload for back compat. - // If there are more than one arguments, and the second argument is a object but not a File, then it must be the options overload. + // If there are more than one arguments, and the second argument is a object but not a File or an ArrayBufferView, then it must be the options overload. return ( - maybeOptionsArgs[0] instanceof File || - ArrayBuffer.isView(args[0]) || - (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File)) + !(typeof maybeOptionsArgs[0] === "string") || + (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File) && !ArrayBuffer.isView(maybeOptionsArgs[1])) ); }; From e8e257000e4ed660b243a9b309f74b40cc08215b Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 1 Aug 2024 16:13:55 -0700 Subject: [PATCH 08/23] Respect enabled flag on plugins and extensions --- packages/dev/core/src/Loading/sceneLoader.ts | 18 +++++++++----- .../dev/loaders/src/glTF/2.0/glTFLoader.ts | 24 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index ba10d4a8662..898a28b6046 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -648,7 +648,8 @@ export class SceneLoader { onError: (message?: string, exception?: any) => void, onDispose: () => void, pluginExtension: Nullable, - name: string + name: string, + pluginOptions: SceneLoaderPluginOptions ): Nullable { const directLoad = SceneLoader._GetDirectLoad(fileInfo.url); @@ -663,7 +664,9 @@ export class SceneLoader { ? SceneLoader._GetPluginForDirectLoad(fileInfo.url) : SceneLoader._GetPluginForFilename(fileInfo.url); - // TODO: Check if the plugin is disabled? + 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 @@ -673,7 +676,7 @@ export class SceneLoader { const plugin: IRegisteredPlugin["plugin"] = registeredPlugin.plugin.createPlugin?.() ?? 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); @@ -991,7 +994,8 @@ export class SceneLoader { errorHandler, disposeHandler, pluginExtension, - name + name, + pluginOptions ); } @@ -1351,7 +1355,8 @@ export class SceneLoader { errorHandler, disposeHandler, pluginExtension, - name + name, + pluginOptions ); } @@ -1565,7 +1570,8 @@ export class SceneLoader { errorHandler, disposeHandler, pluginExtension, - name + name, + pluginOptions ); } diff --git a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts index a491934665c..4572366928b 100644 --- a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts +++ b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts @@ -568,14 +568,21 @@ export class GLTFLoader implements IGLTFLoader { private _loadExtensions(): void { for (const name in GLTFLoader._RegisteredExtensions) { - // TODO: Check if the extension is disabled? - const extension = GLTFLoader._RegisteredExtensions[name].factory(this, this._options.extensionOptions); - 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._options.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, this._options.extensionOptions); + 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)); @@ -587,6 +594,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._options.extensionOptions[name]?.enabled === false) { + throw new Error(`Required extension ${name} is disabled`); + } throw new Error(`Required extension ${name} is not available`); } } From 22591148b638cbb1cf40196cd420770b18fd21d8 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 2 Aug 2024 11:57:17 -0700 Subject: [PATCH 09/23] Add comments and more glTFFileLoader options --- packages/dev/core/src/Loading/sceneLoader.ts | 9 +- .../src/glTF/2.0/Extensions/MSFT_lod.ts | 11 +- .../dev/loaders/src/glTF/glTFFileLoader.ts | 138 ++++++++++-------- 3 files changed, 94 insertions(+), 64 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 898a28b6046..2e38731e25c 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -417,7 +417,14 @@ interface SceneLoaderOptions { /** * Defines options for the registered plugins */ - pluginOptions?: { [P in keyof SceneLoaderPluginOptions]: SceneLoaderPluginOptions[P] & { enabled?: boolean } }; + pluginOptions?: { + [P in keyof SceneLoaderPluginOptions]: SceneLoaderPluginOptions[P] & { + /** + * Defines if the plugin is enabled + */ + enabled?: boolean; + }; + }; } /** 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 6542532383f..8c894d97518 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 @@ -15,14 +15,19 @@ const NAME = "MSFT_lod"; // eslint-disable-next-line @typescript-eslint/naming-convention export type MSFT_lodOptions = { + /** + * Maximum number of LODs to load, starting from the lowest LOD. + */ maxLODsToLoad: number; }; declare module "../../glTFFileLoader" { + // eslint-disable-next-line jsdoc/require-jsdoc export interface GLTFLoaderExtensionOptions { - [NAME]?: Partial & { - enabled?: boolean; - }; + /** + * Defines options for the MSFT_lod extension. + */ + [NAME]?: Partial; } } diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index 43700dca9f3..635f6d827f8 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -42,15 +42,23 @@ export interface GLTFLoaderExtensionOptions extends Record; } } @@ -190,56 +198,7 @@ export interface IGLTFLoader extends IDisposable { loadAsync: (scene: Scene, data: IGLTFLoaderData, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, fileName?: string) => Promise; } -/** - * File loader for loading glTF files into a scene. - */ -export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISceneLoaderPluginFactory { - /** @internal */ - public static _CreateGLTF1Loader: (parent: GLTFFileLoader, options: GLTFLoaderOptions) => IGLTFLoader; - - /** @internal */ - public static _CreateGLTF2Loader: (parent: GLTFFileLoader, options: GLTFLoaderOptions) => 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; - +class GLTFFileLoaderBaseOptions { // ---------- // V2 options // ---------- @@ -336,6 +295,57 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc * You can also pass null if you don't want a root node to be created. */ public customRootNode?: Nullable; +} + +/** + * File loader for loading glTF files into a scene. + */ +export class GLTFFileLoader extends GLTFFileLoaderBaseOptions implements IDisposable, ISceneLoaderPluginAsync, ISceneLoaderPluginFactory { + /** @internal */ + public static _CreateGLTF1Loader: (parent: GLTFFileLoader, options: GLTFLoaderOptions) => IGLTFLoader; + + /** @internal */ + public static _CreateGLTF2Loader: (parent: GLTFFileLoader, options: GLTFLoaderOptions) => 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. @@ -956,12 +966,20 @@ export class GLTFFileLoader implements IDisposable, ISceneLoaderPluginAsync, ISc } private _resolveOptions(options?: SceneLoaderPluginOptions): GLTFLoaderOptions { - return { - loadOnlyMaterials: options?.[NAME]?.loadOnlyMaterials ?? this.loadOnlyMaterials, - skipMaterials: options?.[NAME]?.skipMaterials ?? this.skipMaterials, - coordinateSystemMode: options?.[NAME]?.coordinateSystemMode ?? this.coordinateSystemMode, + // Start with a GLTFFileLoaderBaseOptions so we can easily iterate over its properties to merge options. + const resolvedOptions = new GLTFFileLoaderBaseOptions(); + + // Loop over options that exist on the class first. Passed in options take precedence over options on the instance. + for (const key in resolvedOptions) { + const typedKey = key as keyof GLTFFileLoaderBaseOptions; + (resolvedOptions satisfies Partial> as Record)[typedKey] = + options?.[NAME]?.[typedKey] ?? this[typedKey]; + } + + // Add on the extension options from the passed in options. + return Object.assign(resolvedOptions, { extensionOptions: options?.[NAME]?.extensionOptions ?? {}, - }; + }); } private _onProgress(event: ProgressEvent, request: IFileRequestInfo): void { From 7f1b3b0375dd207da7a121894e2f95450c96a347 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 2 Aug 2024 12:55:40 -0700 Subject: [PATCH 10/23] Simplify --- packages/dev/core/src/Loading/sceneLoader.ts | 48 ++++------ .../src/glTF/2.0/Extensions/MSFT_lod.ts | 10 +- .../dev/loaders/src/glTF/2.0/glTFLoader.ts | 25 +++-- .../dev/loaders/src/glTF/glTFFileLoader.ts | 94 ++++++++----------- 4 files changed, 70 insertions(+), 107 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 2e38731e25c..88aa7d60a1c 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -126,9 +126,10 @@ export interface ISceneLoaderPluginFactory { /** * 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. @@ -223,8 +224,7 @@ export interface ISceneLoaderPlugin extends ISceneLoaderPluginBase { meshes: AbstractMesh[], particleSystems: IParticleSystem[], skeletons: Skeleton[], - onError?: (message: string, exception?: any) => void, - options?: SceneLoaderPluginOptions + onError?: (message: string, exception?: any) => void ): boolean; /** @@ -235,7 +235,7 @@ export interface ISceneLoaderPlugin extends ISceneLoaderPluginBase { * @param onError The callback when import fails * @returns True if successful or false otherwise */ - load(scene: Scene, data: unknown, rootUrl: string, onError?: (message: string, exception?: any) => void, options?: SceneLoaderPluginOptions): boolean; + load(scene: Scene, data: unknown, rootUrl: string, onError?: (message: string, exception?: any) => void): boolean; /** * Load into an asset container. @@ -245,7 +245,7 @@ export interface ISceneLoaderPlugin extends ISceneLoaderPluginBase { * @param onError The callback when import fails * @returns The loaded asset container */ - loadAssetContainer(scene: Scene, data: unknown, rootUrl: string, onError?: (message: string, exception?: any) => void, options?: SceneLoaderPluginOptions): AssetContainer; + loadAssetContainer(scene: Scene, data: unknown, rootUrl: string, onError?: (message: string, exception?: any) => void): AssetContainer; } /** @@ -268,8 +268,7 @@ export interface ISceneLoaderPluginAsync extends ISceneLoaderPluginBase { data: unknown, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, - fileName?: string, - options?: SceneLoaderPluginOptions + fileName?: string ): Promise; /** @@ -281,14 +280,7 @@ export interface ISceneLoaderPluginAsync extends ISceneLoaderPluginBase { * @param fileName Defines the name of the file to load * @returns Nothing */ - loadAsync( - scene: Scene, - data: unknown, - rootUrl: string, - onProgress?: (event: ISceneLoaderProgressEvent) => void, - fileName?: string, - options?: SceneLoaderPluginOptions - ): Promise; + loadAsync(scene: Scene, data: unknown, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, fileName?: string): Promise; /** * Load into an asset container. @@ -299,14 +291,7 @@ export interface ISceneLoaderPluginAsync extends ISceneLoaderPluginBase { * @param fileName Defines the name of the file to load * @returns The loaded asset container */ - loadAssetContainerAsync( - scene: Scene, - data: unknown, - rootUrl: string, - onProgress?: (event: ISceneLoaderProgressEvent) => void, - fileName?: string, - options?: SceneLoaderPluginOptions - ): Promise; + loadAssetContainerAsync(scene: Scene, data: unknown, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, fileName?: string): Promise; } /** @@ -680,7 +665,10 @@ export class SceneLoader { 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 '${pluginExtension}' file type has not been found. If using es6, please import the plugin you wish to use before.`; @@ -969,7 +957,7 @@ export class SceneLoader { const particleSystems: IParticleSystem[] = []; const skeletons: Skeleton[] = []; - if (!syncedPlugin.importMesh(meshNames, scene, data, fileInfo.rootUrl, meshes, particleSystems, skeletons, errorHandler, pluginOptions)) { + if (!syncedPlugin.importMesh(meshNames, scene, data, fileInfo.rootUrl, meshes, particleSystems, skeletons, errorHandler)) { return; } @@ -978,7 +966,7 @@ export class SceneLoader { } else { const asyncedPlugin = plugin; asyncedPlugin - .importMeshAsync(meshNames, scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name, pluginOptions) + .importMeshAsync(meshNames, scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) .then((result) => { scene.loadingPluginName = plugin.name; successHandler( @@ -1339,7 +1327,7 @@ export class SceneLoader { (plugin, data) => { if ((plugin as ISceneLoaderPlugin).load) { const syncedPlugin = plugin; - if (!syncedPlugin.load(scene, data, fileInfo.rootUrl, errorHandler, pluginOptions)) { + if (!syncedPlugin.load(scene, data, fileInfo.rootUrl, errorHandler)) { return; } @@ -1348,7 +1336,7 @@ export class SceneLoader { } else { const asyncedPlugin = plugin; asyncedPlugin - .loadAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name, pluginOptions) + .loadAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) .then(() => { scene.loadingPluginName = plugin.name; successHandler(); @@ -1550,7 +1538,7 @@ export class SceneLoader { (plugin, data) => { if ((plugin as ISceneLoaderPlugin).loadAssetContainer) { const syncedPlugin = plugin; - const assetContainer = syncedPlugin.loadAssetContainer(scene, data, fileInfo.rootUrl, errorHandler, pluginOptions); + const assetContainer = syncedPlugin.loadAssetContainer(scene, data, fileInfo.rootUrl, errorHandler); if (!assetContainer) { return; } @@ -1560,7 +1548,7 @@ export class SceneLoader { } else if ((plugin as ISceneLoaderPluginAsync).loadAssetContainerAsync) { const asyncedPlugin = plugin; asyncedPlugin - .loadAssetContainerAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name, pluginOptions) + .loadAssetContainerAsync(scene, data, fileInfo.rootUrl, progressHandler, fileInfo.name) .then((assetContainer) => { assetContainer.populateRootNodes(); scene.loadingPluginName = plugin.name; 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 8c894d97518..48fa3fda526 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 @@ -8,7 +8,6 @@ import type { BaseTexture } from "core/Materials/Textures/baseTexture"; import type { INode, IMaterial, IBuffer, IScene } from "../glTFLoaderInterfaces"; import type { IGLTFLoaderExtension } from "../glTFLoaderExtension"; import { GLTFLoader, ArrayItem } from "../glTFLoader"; -import type { GLTFLoaderExtensionOptions } from "../../glTFFileLoader"; import type { IProperty, IMSFTLOD } from "babylonjs-gltf2interface"; const NAME = "MSFT_lod"; @@ -93,10 +92,7 @@ export class MSFT_lod implements IGLTFLoaderExtension { /** * @internal */ - constructor( - loader: GLTFLoader, - private readonly _options: GLTFLoaderExtensionOptions - ) { + constructor(loader: GLTFLoader) { this._loader = loader; this.enabled = this._loader.isExtensionUsed(NAME); } @@ -378,7 +374,7 @@ export class MSFT_lod implements IGLTFLoaderExtension { private _getLODs(context: string, property: T, array: ArrayLike | undefined, ids: number[]): T[] { // Options takes precedence. The maxLODsToLoad extension property is retained for back compat. // For new extensions, they should only use options. - const maxLODsToLoad = this._options.MSFT_lod?.maxLODsToLoad ?? this.maxLODsToLoad; + const maxLODsToLoad = this._loader.parent.extensionOptions[NAME]?.maxLODsToLoad ?? this.maxLODsToLoad; if (maxLODsToLoad <= 0) { throw new Error("maxLODsToLoad must be greater than zero"); @@ -440,4 +436,4 @@ export class MSFT_lod implements IGLTFLoaderExtension { } } -GLTFLoader.RegisterExtension(NAME, (loader, options) => new MSFT_lod(loader, options)); +GLTFLoader.RegisterExtension(NAME, (loader) => new MSFT_lod(loader)); diff --git a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts index 4572366928b..835997321b6 100644 --- a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts +++ b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts @@ -65,7 +65,7 @@ import type { _IAnimationSamplerData, } from "./glTFLoaderInterfaces"; import type { IGLTFLoaderExtension } from "./glTFLoaderExtension"; -import type { GLTFLoaderOptions, GLTFLoaderExtensionOptions, IGLTFLoader, IGLTFLoaderData } from "../glTFFileLoader"; +import type { IGLTFLoader, IGLTFLoaderData } from "../glTFFileLoader"; import { GLTFFileLoader, GLTFLoaderState, GLTFLoaderCoordinateSystemMode, GLTFLoaderAnimationStartMode } from "../glTFFileLoader"; import type { IDataBuffer } from "core/Misc/dataReader"; import { DecodeBase64UrlToBinary, IsBase64DataUrl, LoadFileError } from "core/Misc/fileTools"; @@ -94,7 +94,7 @@ interface ILoaderProperty extends IProperty { } interface IRegisteredExtension { - factory: (loader: GLTFLoader, options: GLTFLoaderExtensionOptions) => IGLTFLoaderExtension; + factory: (loader: GLTFLoader) => IGLTFLoaderExtension; } interface IWithMetadata { @@ -223,7 +223,7 @@ export class GLTFLoader implements IGLTFLoader { * @param name The name of the loader extension. * @param factory The factory function that creates the loader extension. */ - public static RegisterExtension(name: string, factory: (loader: GLTFLoader, options: GLTFLoaderExtensionOptions) => IGLTFLoaderExtension): void { + public static RegisterExtension(name: string, factory: (loader: GLTFLoader) => IGLTFLoaderExtension): void { if (GLTFLoader.UnregisterExtension(name)) { Logger.Warn(`Extension with the name '${name}' already exists`); } @@ -300,10 +300,7 @@ export class GLTFLoader implements IGLTFLoader { /** * @internal */ - constructor( - parent: GLTFFileLoader, - private readonly _options: GLTFLoaderOptions - ) { + constructor(parent: GLTFFileLoader) { this._parent = parent; } @@ -422,7 +419,7 @@ export class GLTFLoader implements IGLTFLoader { const oldBlockMaterialDirtyMechanism = this._babylonScene.blockMaterialDirtyMechanism; this._babylonScene.blockMaterialDirtyMechanism = true; - if (!this._options.loadOnlyMaterials) { + if (!this.parent.loadOnlyMaterials) { if (nodes) { promises.push(this.loadSceneAsync("/nodes", { nodes: nodes, index: -1 })); } else if (this._gltf.scene != undefined || (this._gltf.scenes && this._gltf.scenes[0])) { @@ -431,7 +428,7 @@ export class GLTFLoader implements IGLTFLoader { } } - if (!this._options.skipMaterials && this.parent.loadAllMaterials && this._gltf.materials) { + if (!this.parent.skipMaterials && this.parent.loadAllMaterials && this._gltf.materials) { for (let m = 0; m < this._gltf.materials.length; ++m) { const material = this._gltf.materials[m]; const context = "/materials/" + m; @@ -569,13 +566,13 @@ export class GLTFLoader implements IGLTFLoader { private _loadExtensions(): void { for (const name in GLTFLoader._RegisteredExtensions) { // Don't load explicitly disabled extensions. - if (this._options.extensionOptions[name]?.enabled === false) { + 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, this._options.extensionOptions); + 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}`); } @@ -594,7 +591,7 @@ 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._options.extensionOptions[name]?.enabled === false) { + if (this.parent.extensionOptions[name]?.enabled === false) { throw new Error(`Required extension ${name} is disabled`); } throw new Error(`Required extension ${name} is not available`); @@ -1051,7 +1048,7 @@ export class GLTFLoader implements IGLTFLoader { this._defaultBabylonMaterialData[babylonDrawMode] = babylonMaterial; } babylonMesh.material = babylonMaterial; - } else if (!this._options.skipMaterials) { + } else if (!this.parent.skipMaterials) { const material = ArrayItem.Get(`${context}/material`, this._gltf.materials, primitive.material); promises.push( this._loadMaterialAsync(`/materials/${material.index}`, material, babylonMesh, babylonDrawMode, (babylonMaterial) => { @@ -2982,4 +2979,4 @@ export class GLTFLoader implements IGLTFLoader { } } -GLTFFileLoader._CreateGLTF2Loader = (parent, options) => new GLTFLoader(parent, options); +GLTFFileLoader._CreateGLTF2Loader = (parent) => new GLTFLoader(parent); diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index 635f6d827f8..3c2f686f408 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -39,20 +39,6 @@ const NAME = "gltf"; */ export interface GLTFLoaderExtensionOptions extends Record | undefined> {} -/** - * Defines options for the glTF loader. - */ -export type GLTFLoaderOptions = GLTFFileLoaderBaseOptions & { - extensionOptions: { - [P in keyof GLTFLoaderExtensionOptions]: GLTFLoaderExtensionOptions[P] & { - /** - * Defines if the extension is enabled - */ - enabled?: boolean; - }; - }; -}; - declare module "core/Loading/sceneLoader" { // eslint-disable-next-line jsdoc/require-jsdoc export interface SceneLoaderPluginOptions { @@ -198,7 +184,17 @@ export interface IGLTFLoader extends IDisposable { loadAsync: (scene: Scene, data: IGLTFLoaderData, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, fileName?: string) => Promise; } -class GLTFFileLoaderBaseOptions { +class GLTFLoaderOptions { + // eslint-disable-next-line babylonjs/available + constructor(options?: Partial>) { + if (options) { + for (const key in this) { + const typedKey = key as keyof GLTFLoaderOptions; + (this as Record)[typedKey] = options[typedKey] ?? this[typedKey]; + } + } + } + // ---------- // V2 options // ---------- @@ -233,7 +229,7 @@ class GLTFFileLoaderBaseOptions { * If false, (default) The luminance of each pixel will reduce its opacity to simulate the behaviour of most physical materials. * If true, no extra effects are applied to transparent pixels. */ - public transparencyAsCoverage = false; + public transparencyAsCoverage = false; // TODO: Need to pass the full gltf options to extensions so they can access this /** * Defines if the loader should use range requests when load binary glTF files from HTTP. @@ -295,17 +291,29 @@ class GLTFFileLoaderBaseOptions { * You can also pass null if you don't want a root node to be created. */ public customRootNode?: Nullable; + + /** + * Defines options for glTF extensions. + */ + extensionOptions: { + [P in keyof GLTFLoaderExtensionOptions]: GLTFLoaderExtensionOptions[P] & { + /** + * Defines if the extension is enabled + */ + enabled?: boolean; + }; + } = {}; } /** * File loader for loading glTF files into a scene. */ -export class GLTFFileLoader extends GLTFFileLoaderBaseOptions implements IDisposable, ISceneLoaderPluginAsync, ISceneLoaderPluginFactory { +export class GLTFFileLoader extends GLTFLoaderOptions implements IDisposable, ISceneLoaderPluginAsync, ISceneLoaderPluginFactory { /** @internal */ - public static _CreateGLTF1Loader: (parent: GLTFFileLoader, options: GLTFLoaderOptions) => IGLTFLoader; + public static _CreateGLTF1Loader: (parent: GLTFFileLoader) => IGLTFLoader; /** @internal */ - public static _CreateGLTF2Loader: (parent: GLTFFileLoader, options: GLTFLoaderOptions) => IGLTFLoader; + public static _CreateGLTF2Loader: (parent: GLTFFileLoader) => IGLTFLoader; // -------------- // Common options @@ -750,15 +758,14 @@ export class GLTFFileLoader extends GLTFFileLoaderBaseOptions implements IDispos data: IGLTFLoaderData, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, - fileName?: string, - options?: SceneLoaderPluginOptions + fileName?: string ): Promise { return Promise.resolve().then(() => { this.onParsedObservable.notifyObservers(data); this.onParsedObservable.clear(); this._log(`Loading ${fileName || ""}`); - this._loader = this._getLoader(data, this._resolveOptions(options)); + this._loader = this._getLoader(data); return this._loader.importMeshAsync(meshesNames, scene, null, data, rootUrl, onProgress, fileName); }); } @@ -766,20 +773,13 @@ export class GLTFFileLoader extends GLTFFileLoaderBaseOptions implements IDispos /** * @internal */ - public loadAsync( - scene: Scene, - data: IGLTFLoaderData, - rootUrl: string, - onProgress?: (event: ISceneLoaderProgressEvent) => void, - fileName?: string, - options?: SceneLoaderPluginOptions - ): Promise { + public loadAsync(scene: Scene, data: IGLTFLoaderData, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, fileName?: string): Promise { return Promise.resolve().then(() => { this.onParsedObservable.notifyObservers(data); this.onParsedObservable.clear(); this._log(`Loading ${fileName || ""}`); - this._loader = this._getLoader(data, this._resolveOptions(options)); + this._loader = this._getLoader(data); return this._loader.loadAsync(scene, data, rootUrl, onProgress, fileName); }); } @@ -792,15 +792,14 @@ export class GLTFFileLoader extends GLTFFileLoaderBaseOptions implements IDispos data: IGLTFLoaderData, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, - fileName?: string, - options?: SceneLoaderPluginOptions + fileName?: string ): Promise { return Promise.resolve().then(() => { this.onParsedObservable.notifyObservers(data); this.onParsedObservable.clear(); this._log(`Loading ${fileName || ""}`); - this._loader = this._getLoader(data, this._resolveOptions(options)); + this._loader = this._getLoader(data); // Prepare the asset container. const container = new AssetContainer(scene); @@ -890,8 +889,8 @@ export class GLTFFileLoader extends GLTFFileLoaderBaseOptions implements IDispos public rewriteRootURL?(rootUrl: string, responseURL?: string): string; /** @internal */ - public createPlugin(): ISceneLoaderPluginAsync { - return new GLTFFileLoader(); + public createPlugin(options?: SceneLoaderPluginOptions): ISceneLoaderPluginAsync { + return new GLTFFileLoader(options?.[NAME]); } /** @@ -965,23 +964,6 @@ export class GLTFFileLoader extends GLTFFileLoaderBaseOptions implements IDispos return request; } - private _resolveOptions(options?: SceneLoaderPluginOptions): GLTFLoaderOptions { - // Start with a GLTFFileLoaderBaseOptions so we can easily iterate over its properties to merge options. - const resolvedOptions = new GLTFFileLoaderBaseOptions(); - - // Loop over options that exist on the class first. Passed in options take precedence over options on the instance. - for (const key in resolvedOptions) { - const typedKey = key as keyof GLTFFileLoaderBaseOptions; - (resolvedOptions satisfies Partial> as Record)[typedKey] = - options?.[NAME]?.[typedKey] ?? this[typedKey]; - } - - // Add on the extension options from the passed in options. - return Object.assign(resolvedOptions, { - extensionOptions: options?.[NAME]?.extensionOptions ?? {}, - }); - } - private _onProgress(event: ProgressEvent, request: IFileRequestInfo): void { if (!this._progressCallback) { return; @@ -1037,7 +1019,7 @@ export class GLTFFileLoader extends GLTFFileLoaderBaseOptions implements IDispos ); } - private _getLoader(loaderData: IGLTFLoaderData, options: GLTFLoaderOptions): IGLTFLoader { + private _getLoader(loaderData: IGLTFLoaderData): IGLTFLoader { const asset = (loaderData.json).asset || {}; this._log(`Asset version: ${asset.version}`); @@ -1060,7 +1042,7 @@ export class GLTFFileLoader extends GLTFFileLoaderBaseOptions implements IDispos } } - const createLoaders: { [key: number]: (parent: GLTFFileLoader, options: GLTFLoaderOptions) => IGLTFLoader } = { + const createLoaders: { [key: number]: (parent: GLTFFileLoader) => IGLTFLoader } = { 1: GLTFFileLoader._CreateGLTF1Loader, 2: GLTFFileLoader._CreateGLTF2Loader, }; @@ -1070,7 +1052,7 @@ export class GLTFFileLoader extends GLTFFileLoaderBaseOptions implements IDispos throw new Error("Unsupported version: " + asset.version); } - return createLoader(this, options); + return createLoader(this); } private _parseJson(json: string): Object { From 0eebac23a8952dacf3b15495f81f3a75cbf54c9b Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 2 Aug 2024 13:53:01 -0700 Subject: [PATCH 11/23] Add missing name parameter in ImportAnimations and other minor cleanup --- packages/dev/core/src/Loading/sceneLoader.ts | 43 ++++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 88aa7d60a1c..bcaa3ddc743 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -866,13 +866,13 @@ export class SceneLoader { public static ImportMesh( meshNames: string | readonly string[] | null | undefined, rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", - 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 = "" + sceneFilename?: string | File | ArrayBufferView, + 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); } @@ -886,7 +886,7 @@ export class SceneLoader { onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, - name: string = "", + name = "", pluginOptions: SceneLoaderPluginOptions = {} ): Nullable { if (!scene) { @@ -1125,7 +1125,7 @@ export class SceneLoader { onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, - name: string = "", + name = "", pluginOptions: SceneLoaderPluginOptions = {} ): Nullable { if (!engine) { @@ -1257,7 +1257,7 @@ export class SceneLoader { onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, - name: string = "", + name = "", pluginOptions: SceneLoaderPluginOptions = {} ): Nullable { if (!scene) { @@ -1477,7 +1477,7 @@ export class SceneLoader { onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, - name: string = "", + name = "", pluginOptions: SceneLoaderPluginOptions = {} ): Nullable { if (!scene) { @@ -1672,6 +1672,7 @@ 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, @@ -1683,7 +1684,8 @@ export class SceneLoader { onSuccess?: Nullable<(scene: Scene) => void>, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, - pluginExtension?: Nullable + pluginExtension?: Nullable, + name?: string ): void { SceneLoader._ImportAnimations( rootUrl, @@ -1695,7 +1697,8 @@ export class SceneLoader { onSuccess, onProgress, onError, - pluginExtension + pluginExtension, + name ); } @@ -1710,6 +1713,7 @@ export class SceneLoader { onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, + name = "", pluginOptions: SceneLoaderPluginOptions = {} ): void { if (!scene) { @@ -1773,7 +1777,7 @@ export class SceneLoader { } }; - this._LoadAssetContainer(rootUrl, sceneFilename, scene, onAssetContainerLoaded, onProgress, onError, pluginExtension, undefined, pluginOptions); + this._LoadAssetContainer(rootUrl, sceneFilename, scene, onAssetContainerLoaded, onProgress, onError, pluginExtension, name, pluginOptions); } /** @@ -1796,6 +1800,7 @@ 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( @@ -1808,7 +1813,8 @@ export class SceneLoader { onSuccess?: Nullable<(scene: Scene) => void>, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, - pluginExtension?: Nullable + pluginExtension?: Nullable, + name?: string ): Promise; public static ImportAnimationsAsync( @@ -1824,6 +1830,7 @@ export class SceneLoader { onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, onError?: Nullable<(scene: Scene, message: string, exception?: any) => void>, pluginExtension?: Nullable, + name?: string, ] | [source: string | File | ArrayBufferView, options?: ImportAnimationsOptions] ): Promise { @@ -1835,6 +1842,7 @@ export class SceneLoader { let targetConverter: Nullable<(target: any) => any> | undefined; let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; + let name: string | undefined; let pluginOptions: SceneLoaderPluginOptions | undefined; // This is a user-defined type guard: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards @@ -1853,10 +1861,10 @@ export class SceneLoader { // Source is mapped to sceneFileName sceneFilename = args[0]; // Options determine the rest of the arguments - ({ rootUrl = "", scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, onProgress, pluginExtension, pluginOptions } = args[1] ?? {}); + ({ rootUrl = "", scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, onProgress, pluginExtension, name, pluginOptions } = args[1] ?? {}); } else { // For the legacy signature, we just directly map each argument - [rootUrl, sceneFilename, scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, , onProgress, , pluginExtension] = args; + [rootUrl, sceneFilename, scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, , onProgress, , pluginExtension, name] = args; } return new Promise((resolve, reject) => { @@ -1875,6 +1883,7 @@ export class SceneLoader { reject(exception || new Error(message)); }, pluginExtension, + name, pluginOptions ); }); From 0dddc7f172bfc3de55fcea18cbb35ddf4213fb3b Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 2 Aug 2024 13:53:18 -0700 Subject: [PATCH 12/23] Remove todo comment --- packages/dev/loaders/src/glTF/glTFFileLoader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index 3c2f686f408..ae9586f3bac 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -229,7 +229,7 @@ class GLTFLoaderOptions { * If false, (default) The luminance of each pixel will reduce its opacity to simulate the behaviour of most physical materials. * If true, no extra effects are applied to transparent pixels. */ - public transparencyAsCoverage = false; // TODO: Need to pass the full gltf options to extensions so they can access this + public transparencyAsCoverage = false; /** * Defines if the loader should use range requests when load binary glTF files from HTTP. From aa708fba3352639de684b1ee9d9b9865b503c8aa Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 2 Aug 2024 14:01:27 -0700 Subject: [PATCH 13/23] Add SceneSource type --- packages/dev/core/src/Loading/sceneLoader.ts | 84 ++++++++++---------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index bcaa3ddc743..81fa01808b5 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -482,6 +482,8 @@ export interface ImportAnimationsOptions extends SceneLoaderOptions { targetConverter?: Nullable<(target: unknown) => unknown>; } +type SceneSource = string | File | ArrayBufferView; + /** * Class used to load scene from various file formats using registered plugins * @see https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes @@ -766,7 +768,7 @@ export class SceneLoader { return plugin; } - private static _GetFileInfo(rootUrl: string, sceneFilename: string | File | ArrayBufferView): Nullable { + private static _GetFileInfo(rootUrl: string, sceneFilename: SceneSource): Nullable { let url: string; let name: string; let file: Nullable = null; @@ -866,7 +868,7 @@ 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>, @@ -880,7 +882,7 @@ export class SceneLoader { private static _ImportMesh( meshNames: string | readonly string[] | null | undefined, rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", + sceneFilename: SceneSource = "", scene: Nullable = EngineStore.LastCreatedScene, onSuccess: Nullable = null, onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, @@ -1000,7 +1002,7 @@ export class SceneLoader { * @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: string | File | ArrayBufferView, options?: ImportMeshOptions): Promise; + public static ImportMeshAsync(source: SceneSource, options?: ImportMeshOptions): Promise; /** * Import meshes into a scene @@ -1016,7 +1018,7 @@ export class SceneLoader { public static ImportMeshAsync( meshNames: string | readonly string[] | null | undefined, rootUrl: string, - sceneFilename?: string | File | ArrayBufferView, + sceneFilename?: SceneSource, scene?: Nullable, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, @@ -1028,17 +1030,17 @@ export class SceneLoader { | [ meshNames: string | readonly string[] | null | undefined, rootUrl: string, - sceneFilename?: string | File | ArrayBufferView, + sceneFilename?: SceneSource, scene?: Nullable, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, name?: string, ] - | [source: string | File | ArrayBufferView, options?: ImportMeshOptions] + | [source: SceneSource, options?: ImportMeshOptions] ): Promise { let meshNames: string | readonly string[] | null | undefined; let rootUrl: string; - let sceneFilename: string | File | ArrayBufferView | undefined; + let sceneFilename: SceneSource | undefined; let scene: Nullable | undefined; let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; @@ -1047,7 +1049,7 @@ export class SceneLoader { // 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 [source: string | File | ArrayBufferView, options?: ImportMeshOptions] => { + const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [source: SceneSource, options?: ImportMeshOptions] => { // If there is only a single argument, then it must be the options overload. // If the second argument is an object, then it must be the options overload. return maybeOptionsArgs.length === 1 || typeof maybeOptionsArgs[1] === "object"; @@ -1106,7 +1108,7 @@ 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>, @@ -1119,7 +1121,7 @@ export class SceneLoader { private static _Load( rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", + sceneFilename: SceneSource = "", engine: Nullable = EngineStore.LastCreatedEngine, onSuccess: Nullable<(scene: Scene) => void> = null, onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, @@ -1142,7 +1144,7 @@ export class SceneLoader { * @param options an object that configures aspects of how the scene is loaded * @returns The loaded scene */ - public static LoadAsync(source: string | File | ArrayBufferView, options?: LoadOptions): Promise; + public static LoadAsync(source: SceneSource, options?: LoadOptions): Promise; /** * Load a scene @@ -1156,7 +1158,7 @@ export class SceneLoader { */ public static LoadAsync( rootUrl: string, - sceneFilename?: string | File | ArrayBufferView, + sceneFilename?: SceneSource, engine?: Nullable, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, @@ -1167,16 +1169,16 @@ export class SceneLoader { ...args: | [ rootUrl: string, - sceneFilename?: string | File | ArrayBufferView, + sceneFilename?: SceneSource, engine?: Nullable, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, name?: string, ] - | [source: string | File | ArrayBufferView, options?: LoadOptions] + | [source: SceneSource, options?: LoadOptions] ): Promise { let rootUrl: string; - let sceneFilename: string | File | ArrayBufferView | undefined; + let sceneFilename: SceneSource | undefined; let engine: Nullable | undefined; let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; @@ -1185,7 +1187,7 @@ export class SceneLoader { // 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 [source: string | File | ArrayBufferView, options?: LoadOptions] => { + const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [source: SceneSource, options?: LoadOptions] => { // If the first argument is not a string, then it must be the options overload. // If there is only a single string argument, then we should use the legacy overload for back compat. // If there are more than one arguments, and the second argument is a object but not a File or an ArrayBuffer, then it must be the options overload. @@ -1238,7 +1240,7 @@ 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>, @@ -1251,7 +1253,7 @@ export class SceneLoader { private static _Append( rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", + sceneFilename: SceneSource = "", scene: Nullable = EngineStore.LastCreatedScene, onSuccess: Nullable<(scene: Scene) => void> = null, onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, @@ -1361,7 +1363,7 @@ export class SceneLoader { * @param options an object that configures aspects of how the scene is loaded * @returns The given scene */ - public static AppendAsync(source: string | File | ArrayBufferView, options?: LoadAssetContainerOptions): Promise; + public static AppendAsync(source: SceneSource, options?: LoadAssetContainerOptions): Promise; /** * Append a scene @@ -1375,7 +1377,7 @@ export class SceneLoader { */ public static AppendAsync( rootUrl: string, - sceneFilename?: string | File | ArrayBufferView, + sceneFilename?: SceneSource, scene?: Nullable, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, @@ -1386,16 +1388,16 @@ export class SceneLoader { ...args: | [ rootUrl: string, - sceneFilename?: string | File | ArrayBufferView, + sceneFilename?: SceneSource, scene?: Nullable, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, name?: string, ] - | [source: string | File | ArrayBufferView, options?: AppendOptions] + | [source: SceneSource, options?: AppendOptions] ): Promise { let rootUrl: string; - let sceneFilename: string | File | ArrayBufferView | undefined; + let sceneFilename: SceneSource | undefined; let scene: Nullable | undefined; let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; @@ -1404,7 +1406,7 @@ export class SceneLoader { // 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 [source: string | File | ArrayBufferView, options?: AppendOptions] => { + const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [source: SceneSource, options?: AppendOptions] => { // If the first argument is a File or an ArrayBufferView, then it must be the options overload. // If there is only a single string argument, then we should use the legacy overload for back compat. // If there are more than one arguments, and the second argument is a object but not a File, then it must be the options overload. @@ -1458,7 +1460,7 @@ 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>, @@ -1471,7 +1473,7 @@ export class SceneLoader { private static _LoadAssetContainer( rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", + sceneFilename: SceneSource = "", scene: Nullable = EngineStore.LastCreatedScene, onSuccess: Nullable<(assets: AssetContainer) => void> = null, onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> = null, @@ -1576,7 +1578,7 @@ export class SceneLoader { * @param options an object that configures aspects of how the scene is loaded * @returns The loaded asset container */ - public static LoadAssetContainerAsync(source: string | File | ArrayBufferView, options?: LoadAssetContainerOptions): Promise; + public static LoadAssetContainerAsync(source: SceneSource, options?: LoadAssetContainerOptions): Promise; /** * Load a scene into an asset container @@ -1590,7 +1592,7 @@ export class SceneLoader { */ public static LoadAssetContainerAsync( rootUrl: string, - sceneFilename?: string | File, + sceneFilename?: SceneSource, scene?: Nullable, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, @@ -1603,16 +1605,16 @@ export class SceneLoader { ...args: | [ rootUrl: string, - sceneFilename?: string | File, + sceneFilename?: SceneSource, scene?: Nullable, onProgress?: Nullable<(event: ISceneLoaderProgressEvent) => void>, pluginExtension?: Nullable, name?: string, ] - | [source: string | File | ArrayBufferView, options?: LoadAssetContainerOptions] + | [source: SceneSource, options?: LoadAssetContainerOptions] ): Promise { let rootUrl: string; - let sceneFilename: string | File | ArrayBufferView | undefined; + let sceneFilename: SceneSource | undefined; let scene: Nullable | undefined; let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; @@ -1621,7 +1623,7 @@ export class SceneLoader { // 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 [source: string | File | ArrayBufferView, options?: LoadAssetContainerOptions] => { + const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [source: SceneSource, options?: LoadAssetContainerOptions] => { // If the first argument is not a string, then it must be the options overload. // If there is only a single string argument, then we should use the legacy overload for back compat. // If there are more than one arguments, and the second argument is a object but not a File or an ArrayBufferView, then it must be the options overload. @@ -1676,7 +1678,7 @@ export class SceneLoader { */ public static ImportAnimations( rootUrl: string, - sceneFilename?: string | File, + sceneFilename?: SceneSource, scene?: Nullable, overwriteAnimations?: boolean, animationGroupLoadingMode?: SceneLoaderAnimationGroupLoadingMode, @@ -1704,7 +1706,7 @@ export class SceneLoader { private static _ImportAnimations( rootUrl: string, - sceneFilename: string | File | ArrayBufferView = "", + sceneFilename: SceneSource = "", scene: Nullable = EngineStore.LastCreatedScene, overwriteAnimations = true, animationGroupLoadingMode = SceneLoaderAnimationGroupLoadingMode.Clean, @@ -1786,7 +1788,7 @@ export class SceneLoader { * @param options an object that configures aspects of how the scene is loaded * @returns The loaded asset container */ - public static ImportAnimationsAsync(source: string | File | ArrayBufferView, options?: ImportAnimationsOptions): Promise; + public static ImportAnimationsAsync(source: SceneSource, options?: ImportAnimationsOptions): Promise; /** * Import animations from a file into a scene @@ -1805,7 +1807,7 @@ export class SceneLoader { */ public static ImportAnimationsAsync( rootUrl: string, - sceneFilename?: string | File | ArrayBufferView, + sceneFilename?: SceneSource, scene?: Nullable, overwriteAnimations?: boolean, animationGroupLoadingMode?: SceneLoaderAnimationGroupLoadingMode, @@ -1821,7 +1823,7 @@ export class SceneLoader { ...args: | [ rootUrl: string, - sceneFilename?: string | File | ArrayBufferView, + sceneFilename?: SceneSource, scene?: Nullable, overwriteAnimations?: boolean, animationGroupLoadingMode?: SceneLoaderAnimationGroupLoadingMode, @@ -1832,10 +1834,10 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string, ] - | [source: string | File | ArrayBufferView, options?: ImportAnimationsOptions] + | [source: SceneSource, options?: ImportAnimationsOptions] ): Promise { let rootUrl: string; - let sceneFilename: string | File | ArrayBufferView | undefined; + let sceneFilename: SceneSource | undefined; let scene: Nullable | undefined; let overwriteAnimations: boolean | undefined; let animationGroupLoadingMode: SceneLoaderAnimationGroupLoadingMode | undefined; @@ -1847,7 +1849,7 @@ export class SceneLoader { // 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 [source: string | File | ArrayBufferView, options?: ImportAnimationsOptions] => { + const isOptionsArgs = (maybeOptionsArgs: typeof args): maybeOptionsArgs is [source: SceneSource, options?: ImportAnimationsOptions] => { // If the first argument is not a string, then it must be the options overload. // If there is only a single string argument, then we should use the legacy overload for back compat. // If there are more than one arguments, and the second argument is a object but not a File or an ArrayBufferView, then it must be the options overload. From 27e90de5dca3068afd1d3db52ad52c156da5a7bf Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 2 Aug 2024 14:15:08 -0700 Subject: [PATCH 14/23] SceneLoaderOptions non null --- packages/dev/core/src/Loading/sceneLoader.ts | 2 +- packages/dev/loaders/src/glTF/glTFFileLoader.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 81fa01808b5..fa5978b978a 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -129,7 +129,7 @@ export interface ISceneLoaderPluginFactory { * @param options plugin options that were passed to the SceneLoader operation * @returns the new plugin */ - createPlugin(options?: SceneLoaderPluginOptions): ISceneLoaderPlugin | ISceneLoaderPluginAsync; + createPlugin(options: SceneLoaderPluginOptions): ISceneLoaderPlugin | ISceneLoaderPluginAsync; /** * The callback that returns true if the data can be directly loaded. diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index ae9586f3bac..0afcc279cdf 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -889,8 +889,8 @@ export class GLTFFileLoader extends GLTFLoaderOptions implements IDisposable, IS public rewriteRootURL?(rootUrl: string, responseURL?: string): string; /** @internal */ - public createPlugin(options?: SceneLoaderPluginOptions): ISceneLoaderPluginAsync { - return new GLTFFileLoader(options?.[NAME]); + public createPlugin(options: SceneLoaderPluginOptions): ISceneLoaderPluginAsync { + return new GLTFFileLoader(options[NAME]); } /** From e5be1659242cca01cb99284229f8b8789375870c Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 2 Aug 2024 17:07:44 -0700 Subject: [PATCH 15/23] Type improvements and adding stub options for other loaders --- packages/dev/core/src/Loading/sceneLoader.ts | 17 ++++++++++------ packages/dev/loaders/src/OBJ/objFileLoader.ts | 14 ++++++++++++- .../dev/loaders/src/SPLAT/splatFileLoader.ts | 14 ++++++++++++- packages/dev/loaders/src/STL/stlFileLoader.ts | 14 ++++++++++++- .../src/glTF/2.0/Extensions/MSFT_lod.ts | 15 ++++++-------- .../dev/loaders/src/glTF/glTFFileLoader.ts | 20 +++++++++++++------ 6 files changed, 70 insertions(+), 24 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index fa5978b978a..add15d58d54 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -375,6 +375,13 @@ interface IFileInfo { */ export interface SceneLoaderPluginOptions extends Record | undefined> {} +type WithEnabled = { + /** + * Defines if the plugin is enabled + */ + enabled?: boolean; +} & T; + /** * Defines common options for loading operations performed by SceneLoader. */ @@ -403,12 +410,10 @@ interface SceneLoaderOptions { * Defines options for the registered plugins */ pluginOptions?: { - [P in keyof SceneLoaderPluginOptions]: SceneLoaderPluginOptions[P] & { - /** - * Defines if the plugin is enabled - */ - enabled?: boolean; - }; + // 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 WithEnabled]: WithEnabled[Option] }; }; } diff --git a/packages/dev/loaders/src/OBJ/objFileLoader.ts b/packages/dev/loaders/src/OBJ/objFileLoader.ts index f1ca2febbbb..6fbfc4fc54b 100644 --- a/packages/dev/loaders/src/OBJ/objFileLoader.ts +++ b/packages/dev/loaders/src/OBJ/objFileLoader.ts @@ -13,6 +13,18 @@ import { SolidParser } from "./solidParser"; import type { Mesh } from "core/Meshes/mesh"; import { StandardMaterial } from "core/Materials/standardMaterial"; +const NAME = "obj"; + +declare module "core/Loading/sceneLoader" { + // eslint-disable-next-line jsdoc/require-jsdoc + export interface SceneLoaderPluginOptions { + /** + * Defines options for the glTF loader. + */ + [NAME]?: {}; + } +} + /** * OBJ file type loader. * This is a babylon scene loader plugin. @@ -74,7 +86,7 @@ export class OBJFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlugi /** * Defines the name of the plugin. */ - public name = "obj"; + public name = NAME; /** * Defines the extension the plugin is able to load. */ diff --git a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts index 3f7f1da40f7..ee80d7da261 100644 --- a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts +++ b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts @@ -11,6 +11,18 @@ import { GaussianSplattingMesh } from "core/Meshes/GaussianSplatting/gaussianSpl import type { AssetContainer } from "core/assetContainer"; import type { Scene } from "core/scene"; +const NAME = "splat"; + +declare module "core/Loading/sceneLoader" { + // eslint-disable-next-line jsdoc/require-jsdoc + export interface SceneLoaderPluginOptions { + /** + * Defines options for the glTF loader. + */ + [NAME]?: {}; + } +} + /** * @experimental * SPLAT file type loader. @@ -20,7 +32,7 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu /** * Defines the name of the plugin. */ - public name = "splat"; + public name = NAME; /** * Defines the extensions the splat loader is able to load. diff --git a/packages/dev/loaders/src/STL/stlFileLoader.ts b/packages/dev/loaders/src/STL/stlFileLoader.ts index 43f57cdbdf0..4c7f8e40ad7 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 NAME = "stl"; + +declare module "core/Loading/sceneLoader" { + // eslint-disable-next-line jsdoc/require-jsdoc + export interface SceneLoaderPluginOptions { + /** + * Defines options for the glTF loader. + */ + [NAME]?: {}; + } +} + /** * STL file type loader. * This is a babylon scene loader plugin. @@ -27,7 +39,7 @@ export class STLFileLoader implements ISceneLoaderPlugin { /** * Defines the name of the plugin. */ - public name = "stl"; + public name = NAME; /** * Defines the extensions the stl loader is able to load. 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 48fa3fda526..503b2a0dc9b 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,21 +12,18 @@ import type { IProperty, IMSFTLOD } from "babylonjs-gltf2interface"; const NAME = "MSFT_lod"; -// eslint-disable-next-line @typescript-eslint/naming-convention -export type MSFT_lodOptions = { - /** - * Maximum number of LODs to load, starting from the lowest LOD. - */ - maxLODsToLoad: number; -}; - declare module "../../glTFFileLoader" { // eslint-disable-next-line jsdoc/require-jsdoc export interface GLTFLoaderExtensionOptions { /** * Defines options for the MSFT_lod extension. */ - [NAME]?: Partial; + [NAME]?: Partial<{ + /** + * Maximum number of LODs to load, starting from the lowest LOD. + */ + maxLODsToLoad: number; + }>; } } diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index 0afcc279cdf..97ea30ac0cc 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -45,7 +45,8 @@ declare module "core/Loading/sceneLoader" { /** * Defines options for the glTF loader. */ - [NAME]?: Partial; + // NOTE: This is 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. + [NAME]?: Partial<{ [Option in keyof GLTFLoaderOptions]: GLTFLoaderOptions[Option] }>; } } @@ -184,6 +185,13 @@ export interface IGLTFLoader extends IDisposable { loadAsync: (scene: Scene, data: IGLTFLoaderData, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, fileName?: string) => Promise; } +type WithEnabled = { + /** + * Defines if the extension is enabled + */ + enabled?: boolean; +} & T; + class GLTFLoaderOptions { // eslint-disable-next-line babylonjs/available constructor(options?: Partial>) { @@ -296,11 +304,11 @@ class GLTFLoaderOptions { * Defines options for glTF extensions. */ extensionOptions: { - [P in keyof GLTFLoaderExtensionOptions]: GLTFLoaderExtensionOptions[P] & { - /** - * Defines if the extension is enabled - */ - enabled?: boolean; + // 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 WithEnabled]: WithEnabled[Option]; }; } = {}; } From d256145e237d128175ed8554194d4b894e32c0b9 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 2 Aug 2024 17:12:29 -0700 Subject: [PATCH 16/23] Read MSF_lod options in constructor (simpler, and allows for overrides with existing observables) --- .../dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 503b2a0dc9b..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 @@ -91,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); } @@ -369,11 +372,7 @@ export class MSFT_lod implements IGLTFLoaderExtension { * @param ids */ private _getLODs(context: string, property: T, array: ArrayLike | undefined, ids: number[]): T[] { - // Options takes precedence. The maxLODsToLoad extension property is retained for back compat. - // For new extensions, they should only use options. - const maxLODsToLoad = this._loader.parent.extensionOptions[NAME]?.maxLODsToLoad ?? this.maxLODsToLoad; - - if (maxLODsToLoad <= 0) { + if (this.maxLODsToLoad <= 0) { throw new Error("maxLODsToLoad must be greater than zero"); } @@ -381,7 +380,7 @@ export class MSFT_lod implements IGLTFLoaderExtension { for (let i = ids.length - 1; i >= 0; i--) { properties.push(ArrayItem.Get(`${context}/ids/${ids[i]}`, array, ids[i])); - if (properties.length === maxLODsToLoad) { + if (properties.length === this.maxLODsToLoad) { return properties; } } From e08902d7b54ad499b82aa79f6a3ea995ad8be5a7 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Fri, 2 Aug 2024 18:47:56 -0700 Subject: [PATCH 17/23] Make helper type names unique for UMD Fix some comment copy/paste typos --- packages/dev/core/src/Loading/sceneLoader.ts | 11 ++++++++--- packages/dev/loaders/src/OBJ/objFileLoader.ts | 2 +- packages/dev/loaders/src/SPLAT/splatFileLoader.ts | 2 +- packages/dev/loaders/src/STL/stlFileLoader.ts | 2 +- packages/dev/loaders/src/glTF/glTFFileLoader.ts | 9 ++++++--- packages/tools/viewer-alpha/src/viewer.ts | 4 +++- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index add15d58d54..08c2f64bd40 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -375,12 +375,15 @@ interface IFileInfo { */ export interface SceneLoaderPluginOptions extends Record | undefined> {} -type WithEnabled = { +/** + * Adds default/implicit options to plugin specific options. + */ +type DefaultPluginOptions = { /** * Defines if the plugin is enabled */ enabled?: boolean; -} & T; +} & BasePluginOptions; /** * Defines common options for loading operations performed by SceneLoader. @@ -413,7 +416,9 @@ interface SceneLoaderOptions { // 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 WithEnabled]: WithEnabled[Option] }; + [Plugin in keyof SceneLoaderPluginOptions]: { + [Option in keyof DefaultPluginOptions]: DefaultPluginOptions[Option]; + }; }; } diff --git a/packages/dev/loaders/src/OBJ/objFileLoader.ts b/packages/dev/loaders/src/OBJ/objFileLoader.ts index 6fbfc4fc54b..9f3b0aa8286 100644 --- a/packages/dev/loaders/src/OBJ/objFileLoader.ts +++ b/packages/dev/loaders/src/OBJ/objFileLoader.ts @@ -19,7 +19,7 @@ declare module "core/Loading/sceneLoader" { // eslint-disable-next-line jsdoc/require-jsdoc export interface SceneLoaderPluginOptions { /** - * Defines options for the glTF loader. + * Defines options for the obj loader. */ [NAME]?: {}; } diff --git a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts index ee80d7da261..c673ef31bf6 100644 --- a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts +++ b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts @@ -17,7 +17,7 @@ declare module "core/Loading/sceneLoader" { // eslint-disable-next-line jsdoc/require-jsdoc export interface SceneLoaderPluginOptions { /** - * Defines options for the glTF loader. + * Defines options for the splat loader. */ [NAME]?: {}; } diff --git a/packages/dev/loaders/src/STL/stlFileLoader.ts b/packages/dev/loaders/src/STL/stlFileLoader.ts index 4c7f8e40ad7..b90882c5438 100644 --- a/packages/dev/loaders/src/STL/stlFileLoader.ts +++ b/packages/dev/loaders/src/STL/stlFileLoader.ts @@ -15,7 +15,7 @@ declare module "core/Loading/sceneLoader" { // eslint-disable-next-line jsdoc/require-jsdoc export interface SceneLoaderPluginOptions { /** - * Defines options for the glTF loader. + * Defines options for the stl loader. */ [NAME]?: {}; } diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index 97ea30ac0cc..854f61db403 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -185,12 +185,15 @@ export interface IGLTFLoader extends IDisposable { loadAsync: (scene: Scene, data: IGLTFLoaderData, rootUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void, fileName?: string) => Promise; } -type WithEnabled = { +/** + * Adds default/implicit options to extension specific options. + */ +type DefaultExtensionOptions = { /** * Defines if the extension is enabled */ enabled?: boolean; -} & T; +} & BaseExtensionOptions; class GLTFLoaderOptions { // eslint-disable-next-line babylonjs/available @@ -308,7 +311,7 @@ class GLTFLoaderOptions { // 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 WithEnabled]: WithEnabled[Option]; + [Option in keyof DefaultExtensionOptions]: DefaultExtensionOptions[Option]; }; } = {}; } diff --git a/packages/tools/viewer-alpha/src/viewer.ts b/packages/tools/viewer-alpha/src/viewer.ts index 98d38150161..7b5b2d8f198 100644 --- a/packages/tools/viewer-alpha/src/viewer.ts +++ b/packages/tools/viewer-alpha/src/viewer.ts @@ -132,7 +132,9 @@ 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, { + scene: this._details.scene, + }); this._details.model.addAllToScene(); this._reframeCamera(); }); From b02cfea84c49674386a748b93d99e8ae01c8bd83 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Sat, 3 Aug 2024 14:38:44 -0700 Subject: [PATCH 18/23] Make constants unique for UMD Make more properties and objects immutable --- packages/dev/core/src/Loading/sceneLoader.ts | 8 ++++---- packages/dev/loaders/src/OBJ/objFileLoader.ts | 9 +++++---- packages/dev/loaders/src/SPLAT/splatFileLoader.ts | 11 ++++++----- packages/dev/loaders/src/STL/stlFileLoader.ts | 10 +++++----- packages/dev/loaders/src/glTF/glTFFileLoader.ts | 14 +++++++------- 5 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 08c2f64bd40..b7603dd1da5 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -110,7 +110,7 @@ export interface ISceneLoaderPluginExtensions { /** * Defines the list of supported extensions */ - [extension: string]: { + readonly [extension: string]: { isBinary: boolean; }; } @@ -122,7 +122,7 @@ export interface ISceneLoaderPluginFactory { /** * Defines the name of the factory */ - name: string; + readonly name: string; /** * Function called to create a new plugin @@ -146,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. diff --git a/packages/dev/loaders/src/OBJ/objFileLoader.ts b/packages/dev/loaders/src/OBJ/objFileLoader.ts index 9f3b0aa8286..df83db2dd5c 100644 --- a/packages/dev/loaders/src/OBJ/objFileLoader.ts +++ b/packages/dev/loaders/src/OBJ/objFileLoader.ts @@ -13,7 +13,8 @@ import { SolidParser } from "./solidParser"; import type { Mesh } from "core/Meshes/mesh"; import { StandardMaterial } from "core/Materials/standardMaterial"; -const NAME = "obj"; +// eslint-disable-next-line @typescript-eslint/naming-convention +const PLUGIN_OBJ = "obj"; declare module "core/Loading/sceneLoader" { // eslint-disable-next-line jsdoc/require-jsdoc @@ -21,7 +22,7 @@ declare module "core/Loading/sceneLoader" { /** * Defines options for the obj loader. */ - [NAME]?: {}; + [PLUGIN_OBJ]?: {}; } } @@ -86,11 +87,11 @@ export class OBJFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlugi /** * Defines the name of the plugin. */ - public name = NAME; + 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 c673ef31bf6..55876aa28c7 100644 --- a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts +++ b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts @@ -11,7 +11,8 @@ import { GaussianSplattingMesh } from "core/Meshes/GaussianSplatting/gaussianSpl import type { AssetContainer } from "core/assetContainer"; import type { Scene } from "core/scene"; -const NAME = "splat"; +// eslint-disable-next-line @typescript-eslint/naming-convention +const PLUGIN_SPLAT = "splat"; declare module "core/Loading/sceneLoader" { // eslint-disable-next-line jsdoc/require-jsdoc @@ -19,7 +20,7 @@ declare module "core/Loading/sceneLoader" { /** * Defines options for the splat loader. */ - [NAME]?: {}; + [PLUGIN_SPLAT]?: {}; } } @@ -32,18 +33,18 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu /** * Defines the name of the plugin. */ - public name = NAME; + 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 b90882c5438..6652716b4b2 100644 --- a/packages/dev/loaders/src/STL/stlFileLoader.ts +++ b/packages/dev/loaders/src/STL/stlFileLoader.ts @@ -9,7 +9,7 @@ import { SceneLoader } from "core/Loading/sceneLoader"; import { AssetContainer } from "core/assetContainer"; import type { Scene } from "core/scene"; -const NAME = "stl"; +const PLUGIN_STL = "stl"; declare module "core/Loading/sceneLoader" { // eslint-disable-next-line jsdoc/require-jsdoc @@ -17,7 +17,7 @@ declare module "core/Loading/sceneLoader" { /** * Defines options for the stl loader. */ - [NAME]?: {}; + [PLUGIN_STL]?: {}; } } @@ -39,16 +39,16 @@ export class STLFileLoader implements ISceneLoaderPlugin { /** * Defines the name of the plugin. */ - public name = NAME; + 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/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index 854f61db403..dfff21c7f0b 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -32,7 +32,7 @@ import { RuntimeError, ErrorCodes } from "core/Misc/error"; import type { TransformNode } from "core/Meshes/transformNode"; import type { MorphTargetManager } from "core/Morph/morphTargetManager"; -const NAME = "gltf"; +const PLUGIN_GLTF = "gltf"; /** * Defines options for glTF loader extensions. This interface is extended by specific extensions. @@ -46,7 +46,7 @@ declare module "core/Loading/sceneLoader" { * Defines options for the glTF loader. */ // NOTE: This is 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. - [NAME]?: Partial<{ [Option in keyof GLTFLoaderOptions]: GLTFLoaderOptions[Option] }>; + [PLUGIN_GLTF]?: Partial<{ [Option in keyof GLTFLoaderOptions]: GLTFLoaderOptions[Option] }>; } } @@ -588,18 +588,18 @@ export class GLTFFileLoader extends GLTFLoaderOptions implements IDisposable, IS 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 = NAME; + 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. @@ -901,7 +901,7 @@ export class GLTFFileLoader extends GLTFLoaderOptions implements IDisposable, IS /** @internal */ public createPlugin(options: SceneLoaderPluginOptions): ISceneLoaderPluginAsync { - return new GLTFFileLoader(options[NAME]); + return new GLTFFileLoader(options[PLUGIN_GLTF]); } /** From c8b85a3f98ce8d1a7120095469072a36ff41b038 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Sun, 4 Aug 2024 22:13:13 -0700 Subject: [PATCH 19/23] Add PluginOptions type so that the enabled property of DefaultPluginOptions is included and can be checked in a type safe way --- packages/dev/core/src/Loading/sceneLoader.ts | 35 +++++++++++--------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index b7603dd1da5..7e6bd39065b 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -385,6 +385,13 @@ type DefaultPluginOptions = { 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. */ @@ -492,8 +499,6 @@ export interface ImportAnimationsOptions extends SceneLoaderOptions { targetConverter?: Nullable<(target: unknown) => unknown>; } -type SceneSource = string | File | ArrayBufferView; - /** * Class used to load scene from various file formats using registered plugins * @see https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes @@ -653,7 +658,7 @@ export class SceneLoader { onDispose: () => void, pluginExtension: Nullable, name: string, - pluginOptions: SceneLoaderPluginOptions + pluginOptions: PluginOptions ): Nullable { const directLoad = SceneLoader._GetDirectLoad(fileInfo.url); @@ -668,7 +673,7 @@ export class SceneLoader { ? SceneLoader._GetPluginForDirectLoad(fileInfo.url) : SceneLoader._GetPluginForFilename(fileInfo.url); - if (pluginOptions[registeredPlugin.plugin.name]?.enabled === false) { + 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.`); } @@ -680,7 +685,7 @@ export class SceneLoader { // 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; + 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.`; @@ -899,7 +904,7 @@ export class SceneLoader { onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, name = "", - pluginOptions: SceneLoaderPluginOptions = {} + pluginOptions: PluginOptions = {} ): Nullable { if (!scene) { Logger.Error("No scene available to import mesh to"); @@ -1055,7 +1060,7 @@ export class SceneLoader { let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; let name: string | undefined; - let pluginOptions: SceneLoaderPluginOptions | 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. @@ -1138,7 +1143,7 @@ export class SceneLoader { onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, name = "", - pluginOptions: SceneLoaderPluginOptions = {} + pluginOptions: PluginOptions = {} ): Nullable { if (!engine) { Tools.Error("No engine available"); @@ -1193,7 +1198,7 @@ export class SceneLoader { let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; let name: string | undefined; - let pluginOptions: SceneLoaderPluginOptions | 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. @@ -1270,7 +1275,7 @@ export class SceneLoader { onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, name = "", - pluginOptions: SceneLoaderPluginOptions = {} + pluginOptions: PluginOptions = {} ): Nullable { if (!scene) { Logger.Error("No scene available to append to"); @@ -1412,7 +1417,7 @@ export class SceneLoader { let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; let name: string | undefined; - let pluginOptions: SceneLoaderPluginOptions | 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. @@ -1490,7 +1495,7 @@ export class SceneLoader { onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, name = "", - pluginOptions: SceneLoaderPluginOptions = {} + pluginOptions: PluginOptions = {} ): Nullable { if (!scene) { Logger.Error("No scene available to load asset container to"); @@ -1629,7 +1634,7 @@ export class SceneLoader { let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; let name: string | undefined; - let pluginOptions: SceneLoaderPluginOptions | 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. @@ -1726,7 +1731,7 @@ export class SceneLoader { onError: Nullable<(scene: Scene, message: string, exception?: any) => void> = null, pluginExtension: Nullable = null, name = "", - pluginOptions: SceneLoaderPluginOptions = {} + pluginOptions: PluginOptions = {} ): void { if (!scene) { Logger.Error("No scene available to load animations to"); @@ -1855,7 +1860,7 @@ export class SceneLoader { let onProgress: Nullable<(event: ISceneLoaderProgressEvent) => void> | undefined; let pluginExtension: Nullable | undefined; let name: string | undefined; - let pluginOptions: SceneLoaderPluginOptions | 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. From 103d88631f21929759f11498b8506442ce94c497 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 5 Aug 2024 16:52:06 -0700 Subject: [PATCH 20/23] Improve rootUrl/sceneSource handling --- packages/dev/core/src/Loading/sceneLoader.ts | 33 ++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 7e6bd39065b..e446d0c6510 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -499,6 +499,10 @@ export interface ImportAnimationsOptions extends SceneLoaderOptions { 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 @@ -783,30 +787,29 @@ export class SceneLoader { return plugin; } - private static _GetFileInfo(rootUrl: string, sceneFilename: SceneSource): 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; @@ -814,6 +817,10 @@ export class SceneLoader { url = rootUrl + filename; name = filename; + } else { + url = sceneSource; + name = Tools.GetFilename(sceneSource); + rootUrl = Tools.GetFolderPath(sceneSource); } return { From 8998fb2ecba24ae7c468dca9a33b300068e94331 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 5 Aug 2024 17:33:51 -0700 Subject: [PATCH 21/23] Move scene/engine to be a required parameter --- packages/dev/core/src/Loading/sceneLoader.ts | 123 +++++++------------ packages/tools/viewer-alpha/src/viewer.ts | 4 +- 2 files changed, 44 insertions(+), 83 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index e446d0c6510..da6ba93b384 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 @@ -437,52 +437,27 @@ 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; - - /** - * The instance of BABYLON.Scene to append to - */ - scene?: Scene; } /** * Defines options for LoadAsync. */ -export interface LoadOptions extends SceneLoaderOptions { - /** - * The instance of BABYLON.Engine to use to create the scene - */ - engine?: AbstractEngine; -} +export interface LoadOptions extends SceneLoaderOptions {} /** * Defines options for AppendAsync. */ -export interface AppendOptions extends SceneLoaderOptions { - /** - * The instance of BABYLON.Scene to append to - */ - scene?: Scene; -} +export interface AppendOptions extends SceneLoaderOptions {} /** * Defines options for LoadAssetContainerAsync. */ -export interface LoadAssetContainerOptions extends SceneLoaderOptions { - /** - * The instance of BABYLON.Scene to append to - */ - scene?: Scene; -} +export interface LoadAssetContainerOptions extends SceneLoaderOptions {} /** * Defines options for ImportAnimationsAsync. */ export interface ImportAnimationsOptions extends SceneLoaderOptions { - /** - * The instance of BABYLON.Scene to append to - */ - scene?: Scene; - /** * When true, animations are cleaned before importing new ones. Animations are appended otherwise */ @@ -1021,10 +996,11 @@ export class SceneLoader { /** * Import meshes into a scene * @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, options?: ImportMeshOptions): Promise; + public static ImportMeshAsync(source: SceneSource, scene: Scene, options?: ImportMeshOptions): Promise; /** * Import meshes into a scene @@ -1058,7 +1034,7 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string, ] - | [source: SceneSource, options?: ImportMeshOptions] + | [source: SceneSource, scene: Scene, options?: ImportMeshOptions] ): Promise { let meshNames: string | readonly string[] | null | undefined; let rootUrl: string; @@ -1071,17 +1047,17 @@ export class SceneLoader { // 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 [source: SceneSource, options?: ImportMeshOptions] => { - // If there is only a single argument, then it must be the options overload. + 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 maybeOptionsArgs.length === 1 || typeof maybeOptionsArgs[1] === "object"; + 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 = "", scene, onProgress, pluginExtension, name, pluginOptions } = args[1] ?? {}); + ({ 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; @@ -1163,10 +1139,11 @@ export class SceneLoader { /** * Load a scene * @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, options?: LoadOptions): Promise; + public static LoadAsync(source: SceneSource, engine: AbstractEngine, options?: LoadOptions): Promise; /** * Load a scene @@ -1197,7 +1174,7 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string, ] - | [source: SceneSource, options?: LoadOptions] + | [source: SceneSource, engine: AbstractEngine, options?: LoadOptions] ): Promise { let rootUrl: string; let sceneFilename: SceneSource | undefined; @@ -1209,21 +1186,17 @@ export class SceneLoader { // 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 [source: SceneSource, options?: LoadOptions] => { - // If the first argument is not a string, then it must be the options overload. - // If there is only a single string argument, then we should use the legacy overload for back compat. - // If there are more than one arguments, and the second argument is a object but not a File or an ArrayBuffer, then it must be the options overload. - return ( - !(typeof maybeOptionsArgs[0] === "string") || - (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File) && !ArrayBuffer.isView(maybeOptionsArgs[1])) - ); + 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 = "", engine, onProgress, pluginExtension, name, pluginOptions } = args[1] ?? {}); + ({ 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; @@ -1382,10 +1355,11 @@ export class SceneLoader { /** * Append a scene * @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, options?: LoadAssetContainerOptions): Promise; + public static AppendAsync(source: SceneSource, scene: Scene, options?: LoadAssetContainerOptions): Promise; /** * Append a scene @@ -1416,7 +1390,7 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string, ] - | [source: SceneSource, options?: AppendOptions] + | [source: SceneSource, scene: Scene, options?: AppendOptions] ): Promise { let rootUrl: string; let sceneFilename: SceneSource | undefined; @@ -1428,22 +1402,17 @@ export class SceneLoader { // 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 [source: SceneSource, options?: AppendOptions] => { - // If the first argument is a File or an ArrayBufferView, then it must be the options overload. - // If there is only a single string argument, then we should use the legacy overload for back compat. - // If there are more than one arguments, and the second argument is a object but not a File, then it must be the options overload. - return ( - maybeOptionsArgs[0] instanceof File || - ArrayBuffer.isView(args[0]) || - (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File)) - ); + 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 = "", scene, onProgress, pluginExtension, name, pluginOptions } = args[1] ?? {}); + ({ 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; @@ -1597,10 +1566,11 @@ export class SceneLoader { /** * Load a scene into an asset container * @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, options?: LoadAssetContainerOptions): Promise; + public static LoadAssetContainerAsync(source: SceneSource, scene: Scene, options?: LoadAssetContainerOptions): Promise; /** * Load a scene into an asset container @@ -1633,7 +1603,7 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string, ] - | [source: SceneSource, options?: LoadAssetContainerOptions] + | [source: SceneSource, scene: Scene, options?: LoadAssetContainerOptions] ): Promise { let rootUrl: string; let sceneFilename: SceneSource | undefined; @@ -1645,21 +1615,17 @@ export class SceneLoader { // 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 [source: SceneSource, options?: LoadAssetContainerOptions] => { - // If the first argument is not a string, then it must be the options overload. - // If there is only a single string argument, then we should use the legacy overload for back compat. - // If there are more than one arguments, and the second argument is a object but not a File or an ArrayBufferView, then it must be the options overload. - return ( - !(typeof maybeOptionsArgs[0] === "string") || - (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File) && !ArrayBuffer.isView(maybeOptionsArgs[1])) - ); + 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 = "", scene, onProgress, pluginExtension, name, pluginOptions } = args[1] ?? {}); + ({ 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; @@ -1807,10 +1773,11 @@ export class SceneLoader { /** * Import animations from a file into a scene * @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, options?: ImportAnimationsOptions): Promise; + public static ImportAnimationsAsync(source: SceneSource, scene: Scene, options?: ImportAnimationsOptions): Promise; /** * Import animations from a file into a scene @@ -1856,7 +1823,7 @@ export class SceneLoader { pluginExtension?: Nullable, name?: string, ] - | [source: SceneSource, options?: ImportAnimationsOptions] + | [source: SceneSource, scene: Scene, options?: ImportAnimationsOptions] ): Promise { let rootUrl: string; let sceneFilename: SceneSource | undefined; @@ -1871,21 +1838,17 @@ export class SceneLoader { // 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 [source: SceneSource, options?: ImportAnimationsOptions] => { - // If the first argument is not a string, then it must be the options overload. - // If there is only a single string argument, then we should use the legacy overload for back compat. - // If there are more than one arguments, and the second argument is a object but not a File or an ArrayBufferView, then it must be the options overload. - return ( - !(typeof maybeOptionsArgs[0] === "string") || - (maybeOptionsArgs.length > 1 && typeof maybeOptionsArgs[1] === "object" && !(maybeOptionsArgs[1] instanceof File) && !ArrayBuffer.isView(maybeOptionsArgs[1])) - ); + 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 = "", scene, overwriteAnimations, animationGroupLoadingMode, targetConverter, onProgress, pluginExtension, name, pluginOptions } = args[1] ?? {}); + ({ 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; diff --git a/packages/tools/viewer-alpha/src/viewer.ts b/packages/tools/viewer-alpha/src/viewer.ts index 7b5b2d8f198..6aaf71aa74e 100644 --- a/packages/tools/viewer-alpha/src/viewer.ts +++ b/packages/tools/viewer-alpha/src/viewer.ts @@ -132,9 +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, { - scene: this._details.scene, - }); + this._details.model = await SceneLoader.LoadAssetContainerAsync(finalSource, this._details.scene); this._details.model.addAllToScene(); this._reframeCamera(); }); From 2849fa7a5b32ff044653b34282420c4ba7f0a143 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Tue, 6 Aug 2024 14:43:40 -0700 Subject: [PATCH 22/23] Minor PR feedback --- packages/dev/core/src/Loading/sceneLoader.ts | 2 +- packages/dev/loaders/src/glTF/glTFFileLoader.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index da6ba93b384..130ba6f5390 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -783,7 +783,7 @@ export class SceneLoader { } else if (sceneSource.startsWith("data:")) { url = sceneSource; name = ""; - } else if (rootUrl !== "") { + } else if (rootUrl) { const filename = sceneSource; if (filename.substr(0, 1) === "/") { Tools.Error("Wrong sceneFilename parameter"); diff --git a/packages/dev/loaders/src/glTF/glTFFileLoader.ts b/packages/dev/loaders/src/glTF/glTFFileLoader.ts index 45b6e982a94..b92dfcd997c 100644 --- a/packages/dev/loaders/src/glTF/glTFFileLoader.ts +++ b/packages/dev/loaders/src/glTF/glTFFileLoader.ts @@ -45,8 +45,7 @@ declare module "core/Loading/sceneLoader" { /** * Defines options for the glTF loader. */ - // NOTE: This is 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_GLTF]?: Partial<{ [Option in keyof GLTFLoaderOptions]: GLTFLoaderOptions[Option] }>; + [PLUGIN_GLTF]?: Partial; } } @@ -197,7 +196,7 @@ type DefaultExtensionOptions = { class GLTFLoaderOptions { // eslint-disable-next-line babylonjs/available - constructor(options?: Partial>) { + public constructor(options?: Partial>) { if (options) { for (const key in this) { const typedKey = key as keyof GLTFLoaderOptions; @@ -322,7 +321,7 @@ class GLTFLoaderOptions { /** * Defines options for glTF extensions. */ - extensionOptions: { + 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. From 576d126f289aa7f8ae8914fd17c041452a7cb641 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Tue, 6 Aug 2024 14:54:04 -0700 Subject: [PATCH 23/23] Mark new functions as experimental since we will probably move them to be module level functions --- packages/dev/core/src/Loading/sceneLoader.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/dev/core/src/Loading/sceneLoader.ts b/packages/dev/core/src/Loading/sceneLoader.ts index 130ba6f5390..ca80dd909b3 100644 --- a/packages/dev/core/src/Loading/sceneLoader.ts +++ b/packages/dev/core/src/Loading/sceneLoader.ts @@ -995,6 +995,7 @@ export class SceneLoader { /** * 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 @@ -1138,6 +1139,7 @@ export class SceneLoader { /** * 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 @@ -1354,6 +1356,7 @@ export class SceneLoader { /** * 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 @@ -1565,6 +1568,7 @@ export class SceneLoader { /** * 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 @@ -1772,6 +1776,7 @@ export class SceneLoader { /** * 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