From db0dbc765f73d0c4ac63c2ac54b285b3ce3957d5 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Wed, 4 Dec 2024 12:31:33 -0800 Subject: [PATCH] Add deepMerge (#15943) --- packages/dev/core/src/Misc/deepMerger.ts | 26 +++++++++++++++ packages/dev/core/src/Misc/index.ts | 1 + .../2.0/Extensions/KHR_materials_variants.ts | 32 ++++++++++-------- .../dev/loaders/src/glTF/2.0/glTFLoader.ts | 25 ++------------ packages/tools/viewer-alpha/src/viewer.ts | 33 ++++++++++--------- .../viewer-alpha/test/apps/web/index.html | 5 ++- 6 files changed, 66 insertions(+), 56 deletions(-) create mode 100644 packages/dev/core/src/Misc/deepMerger.ts diff --git a/packages/dev/core/src/Misc/deepMerger.ts b/packages/dev/core/src/Misc/deepMerger.ts new file mode 100644 index 00000000000..6bb0ae46a50 --- /dev/null +++ b/packages/dev/core/src/Misc/deepMerger.ts @@ -0,0 +1,26 @@ +// https://stackoverflow.com/a/48218209 +/** + * Merges a series of objects into a single object, deeply. + * @param objects The objects to merge (objects later in the list take precedence). + * @returns The merged object. + */ +export function deepMerge(...objects: T[]): T { + const isRecord = (obj: unknown): obj is Record => !!obj && typeof obj === "object"; + + return objects.reduce>((prev, obj) => { + Object.keys(obj).forEach((key) => { + const pVal = prev[key]; + const oVal = (obj as Record)[key]; + + if (Array.isArray(pVal) && Array.isArray(oVal)) { + prev[key] = pVal.concat(...oVal); + } else if (isRecord(pVal) && isRecord(oVal)) { + prev[key] = deepMerge(pVal, oVal); + } else { + prev[key] = oVal; + } + }); + + return prev; + }, {}) as T; +} diff --git a/packages/dev/core/src/Misc/index.ts b/packages/dev/core/src/Misc/index.ts index 2652511b017..8c6700d06ae 100644 --- a/packages/dev/core/src/Misc/index.ts +++ b/packages/dev/core/src/Misc/index.ts @@ -33,6 +33,7 @@ export * from "./logger"; export * from "./typeStore"; export * from "./filesInputStore"; export * from "./deepCopier"; +export * from "./deepMerger"; export * from "./pivotTools"; export * from "./precisionDate"; export * from "./screenshotTools"; diff --git a/packages/dev/loaders/src/glTF/2.0/Extensions/KHR_materials_variants.ts b/packages/dev/loaders/src/glTF/2.0/Extensions/KHR_materials_variants.ts index d93b17550ba..d78a2d07953 100644 --- a/packages/dev/loaders/src/glTF/2.0/Extensions/KHR_materials_variants.ts +++ b/packages/dev/loaders/src/glTF/2.0/Extensions/KHR_materials_variants.ts @@ -10,14 +10,28 @@ import type { INode, IMeshPrimitive, IMesh } from "../glTFLoaderInterfaces"; import type { IKHRMaterialVariants_Mapping, IKHRMaterialVariants_Variant, IKHRMaterialVariants_Variants } from "babylonjs-gltf2interface"; import type { TransformNode } from "core/Meshes/transformNode"; import { registerGLTFExtension, unregisterGLTFExtension } from "../glTFLoaderExtensionRegistry"; -import type { GLTFLoaderExtensionOptions } from "../../glTFFileLoader"; +import type { MaterialVariantsController } from "../../glTFFileLoader"; const NAME = "KHR_materials_variants"; -// NOTE: This "backward" type definition is needed due to the way namespaces are handled in the UMD bundle. -export type MaterialVariantsController = Parameters["onLoaded"]>[0]; +export { MaterialVariantsController }; declare module "../../glTFFileLoader" { + // Define options related types here so they can be referenced in the options, + // but export the types at the module level. This ensures the types are in the + // correct namespace for UMD. + type MaterialVariantsController = { + /** + * The list of available variant names for this asset. + */ + readonly variants: readonly string[]; + + /** + * Gets or sets the selected variant. + */ + selectedVariant: string; + }; + // eslint-disable-next-line jsdoc/require-jsdoc export interface GLTFLoaderExtensionOptions { /** @@ -29,17 +43,7 @@ declare module "../../glTFFileLoader" { * Defines a callback that will be called if material variants are loaded. * @experimental */ - onLoaded: (controller: { - /** - * The list of available variant names for this asset. - */ - readonly variants: readonly string[]; - - /** - * Gets or sets the selected variant. - */ - selectedVariant: string; - }) => void; + onLoaded: (controller: MaterialVariantsController) => void; }>; } } diff --git a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts index cd303f4201f..a5e1a2cc42f 100644 --- a/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts +++ b/packages/dev/loaders/src/glTF/2.0/glTFLoader.ts @@ -78,6 +78,7 @@ import { nodeAnimationData } from "./glTFLoaderAnimation"; import type { IObjectInfo } from "core/ObjectModel/objectModelInterfaces"; import { registeredGLTFExtensions, registerGLTFExtension, unregisterGLTFExtension } from "./glTFLoaderExtensionRegistry"; import type { GLTFExtensionFactory } from "./glTFLoaderExtensionRegistry"; +import { deepMerge } from "core/Misc/deepMerger"; export { GLTFFileLoader }; interface TypedArrayLike extends ArrayBufferView { @@ -101,28 +102,6 @@ interface IWithMetadata { _internalMetadata: any; } -// https://stackoverflow.com/a/48218209 -function mergeDeep(...objects: any[]): any { - const isObject = (obj: any) => obj && typeof obj === "object"; - - return objects.reduce((prev, obj) => { - Object.keys(obj).forEach((key) => { - const pVal = prev[key]; - const oVal = obj[key]; - - if (Array.isArray(pVal) && Array.isArray(oVal)) { - prev[key] = pVal.concat(...oVal); - } else if (isObject(pVal) && isObject(oVal)) { - prev[key] = mergeDeep(pVal, oVal); - } else { - prev[key] = oVal; - } - }); - - return prev; - }, {}); -} - /** * Helper class for working with arrays when loading the glTF asset */ @@ -907,7 +886,7 @@ export class GLTFLoader implements IGLTFLoader { const babylonTransformNodeForSkin = node._babylonTransformNodeForSkin!; // Merge the metadata from the skin node to the skinned mesh in case a loader extension added metadata. - babylonTransformNode.metadata = mergeDeep(babylonTransformNodeForSkin.metadata, babylonTransformNode.metadata || {}); + babylonTransformNode.metadata = deepMerge(babylonTransformNodeForSkin.metadata, babylonTransformNode.metadata || {}); const skin = ArrayItem.Get(`${context}/skin`, this._gltf.skins, node.skin); promises.push( diff --git a/packages/tools/viewer-alpha/src/viewer.ts b/packages/tools/viewer-alpha/src/viewer.ts index 18d46978d64..1db7bbce7c0 100644 --- a/packages/tools/viewer-alpha/src/viewer.ts +++ b/packages/tools/viewer-alpha/src/viewer.ts @@ -30,16 +30,17 @@ import { CubeTexture } from "core/Materials/Textures/cubeTexture"; import { Texture } from "core/Materials/Textures/texture"; import { Clamp } from "core/Maths/math.scalar.functions"; import { Matrix, Vector3 } from "core/Maths/math.vector"; +import { Viewport } from "core/Maths/math.viewport"; +import { GetHotSpotToRef } from "core/Meshes/abstractMesh.hotSpot"; import { CreateBox } from "core/Meshes/Builders/boxBuilder"; import { computeMaxExtents } from "core/Meshes/meshUtils"; +import { BuildTuple } from "core/Misc/arrayTools"; import { AsyncLock } from "core/Misc/asyncLock"; +import { deepMerge } from "core/Misc/deepMerger"; import { Observable } from "core/Misc/observable"; +import { SnapshotRenderingHelper } from "core/Misc/snapshotRenderingHelper"; import { Scene } from "core/scene"; import { registerBuiltInLoaders } from "loaders/dynamic"; -import { Viewport } from "core/Maths/math.viewport"; -import { GetHotSpotToRef } from "core/Meshes/abstractMesh.hotSpot"; -import { SnapshotRenderingHelper } from "core/Misc/snapshotRenderingHelper"; -import { BuildTuple } from "core/Misc/arrayTools"; const toneMappingOptions = ["none", "standard", "aces", "neutral"] as const; export type ToneMapping = (typeof toneMappingOptions)[number]; @@ -160,7 +161,7 @@ export type ViewerDetails = { * @param screenY The y coordinate in screen space. * @returns A PickingInfo if an object was picked, otherwise null. */ - pick(screenX: number, screenY: number): Nullable; + pick(screenX: number, screenY: number): Promise>; }; export type ViewerOptions = Partial< @@ -384,8 +385,8 @@ export class Viewer implements IDisposable { const camera = new ArcRotateCamera("Viewer Default Camera", 0, 0, 1, Vector3.Zero(), scene); camera.useInputToRestoreState = false; - scene.onPointerObservable.add((pointerInfo) => { - const pickingInfo = this._pick(pointerInfo.event.offsetX, pointerInfo.event.offsetY); + scene.onPointerObservable.add(async (pointerInfo) => { + const pickingInfo = await this._pick(pointerInfo.event.offsetX, pointerInfo.event.offsetY); if (pickingInfo?.pickedPoint) { const distance = pickingInfo.pickedPoint.subtract(camera.position).dot(camera.getForwardRay().direction); // Immediately reset the target and the radius based on the distance to the picked point. @@ -709,37 +710,36 @@ export class Viewer implements IDisposable { this.onLoadingProgressChanged.notifyObservers(); } }; + delete options?.onProgress; const originalOnMaterialVariantsLoaded = options?.pluginOptions?.gltf?.extensionOptions?.KHR_materials_variants?.onLoaded; const onMaterialVariantsLoaded: typeof originalOnMaterialVariantsLoaded = (controller) => { originalOnMaterialVariantsLoaded?.(controller); this._materialVariantsController = controller; }; + delete options?.pluginOptions?.gltf?.extensionOptions?.KHR_materials_variants?.onLoaded; - options = { - ...options, + const defaultOptions: LoadModelOptions = { + // Pass a progress callback to update the loading progress. + onProgress, pluginOptions: { - ...options?.pluginOptions, gltf: { // Enable transparency as coverage by default to be 3D Commerce compliant by default. // https://doc.babylonjs.com/setup/support/3D_commerce_certif transparencyAsCoverage: true, - ...options?.pluginOptions?.gltf, extensionOptions: { - ...options?.pluginOptions?.gltf?.extensionOptions, // eslint-disable-next-line @typescript-eslint/naming-convention KHR_materials_variants: { - ...options?.pluginOptions?.gltf?.extensionOptions?.KHR_materials_variants, // Capture the material variants controller when it is loaded. onLoaded: onMaterialVariantsLoaded, }, }, }, }, - // Pass a progress callback to update the loading progress. - onProgress, }; + options = deepMerge(defaultOptions, options ?? {}); + this._loadModelAbortController?.abort("New model is being loaded before previous model finished loading."); const abortController = (this._loadModelAbortController = new AbortController()); @@ -1121,7 +1121,8 @@ export class Viewer implements IDisposable { this._details.model?.animationGroups.forEach((group) => (group.speedRatio = this._animationSpeed)); } - private _pick(screenX: number, screenY: number): Nullable { + private async _pick(screenX: number, screenY: number): Promise> { + await import("core/Culling/ray"); if (this._details.model) { const model = this._details.model; // Refresh bounding info to ensure morph target and skeletal animations are taken into account. diff --git a/packages/tools/viewer-alpha/test/apps/web/index.html b/packages/tools/viewer-alpha/test/apps/web/index.html index 9601ae26f39..b6dd28d9b31 100644 --- a/packages/tools/viewer-alpha/test/apps/web/index.html +++ b/packages/tools/viewer-alpha/test/apps/web/index.html @@ -144,8 +144,6 @@