@babylonjs/viewer
Version:
The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.
3,784 lines • 188 kB
JavaScript
import { ArcRotateCamera, ComputeAlpha, ComputeBeta } from '@babylonjs/core/Cameras/arcRotateCamera.js';
import { Constants } from '@babylonjs/core/Engines/constants.js';
import { PointerEventTypes } from '@babylonjs/core/Events/pointerEvents.js';
import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight.js';
import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight.js';
import { LoadAssetContainerAsync } from '@babylonjs/core/Loading/sceneLoader.js';
import { BackgroundMaterial } from '@babylonjs/core/Materials/Background/backgroundMaterial.js';
import { ImageProcessingConfiguration } from '@babylonjs/core/Materials/imageProcessingConfiguration.js';
import { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial.js';
import { Texture } from '@babylonjs/core/Materials/Textures/texture.js';
import { Color3, Color4 } from '@babylonjs/core/Maths/math.color.js';
import { Clamp, Lerp } from '@babylonjs/core/Maths/math.scalar.functions.js';
import { Vector3, Matrix, Vector2 } from '@babylonjs/core/Maths/math.vector.js';
import { Viewport } from '@babylonjs/core/Maths/math.viewport.js';
import { GetHotSpotToRef } from '@babylonjs/core/Meshes/abstractMesh.hotSpot.js';
import { CreateBox } from '@babylonjs/core/Meshes/Builders/boxBuilder.js';
import { Mesh } from '@babylonjs/core/Meshes/mesh.js';
import { RemoveUnreferencedVerticesData, computeMaxExtents } from '@babylonjs/core/Meshes/meshUtils.js';
import { BuildTuple } from '@babylonjs/core/Misc/arrayTools.js';
import { AsyncLock } from '@babylonjs/core/Misc/asyncLock.js';
import { deepMerge } from '@babylonjs/core/Misc/deepMerger.js';
import { AbortError } from '@babylonjs/core/Misc/error.js';
import { Logger } from '@babylonjs/core/Misc/logger.js';
import { Observable } from '@babylonjs/core/Misc/observable.js';
import { SceneOptimizerOptions, HardwareScalingOptimization, SceneOptimizer } from '@babylonjs/core/Misc/sceneOptimizer.js';
import { SnapshotRenderingHelper } from '@babylonjs/core/Misc/snapshotRenderingHelper.js';
import { GetExtensionFromUrl } from '@babylonjs/core/Misc/urlTools.js';
import { Scene } from '@babylonjs/core/scene.js';
import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic.js';
import { __decorate } from '@babylonjs/core/tslib.es6.js';
import { Deferred } from '@babylonjs/core/Misc/deferred.js';
const shadowQualityOptions = ["none", "normal", "high"];
const toneMappingOptions = ["none", "standard", "aces", "neutral"];
/**
* Checks if the given value is a valid tone mapping option.
* @param value The value to check.
* @returns True if the value is a valid tone mapping option, otherwise false.
*/
function IsToneMapping(value) {
return toneMappingOptions.includes(value);
}
/**
* Checks if the given value is a valid shadow quality option.
* @param value The value to check.
* @returns True if the value is a valid shadow quality option, otherwise false.
*/
function IsShadowQuality(value) {
return shadowQualityOptions.includes(value);
}
function throwIfAborted(...abortSignals) {
for (const signal of abortSignals) {
signal?.throwIfAborted();
}
}
async function createCubeTexture(url, scene, extension) {
extension = extension ?? GetExtensionFromUrl(url);
const instantiateTexture = await (async () => {
if (extension === ".hdr") {
const { HDRCubeTexture } = await import('@babylonjs/core/Materials/Textures/hdrCubeTexture.js');
return () => new HDRCubeTexture(url, scene, 256, false, true, false, true, undefined, undefined, undefined, true, true);
}
else {
const { CubeTexture } = await import('@babylonjs/core/Materials/Textures/cubeTexture.js');
return () => new CubeTexture(url, scene, null, false, null, null, null, undefined, true, extension, true);
}
})();
const originalUseDelayedTextureLoading = scene.useDelayedTextureLoading;
try {
scene.useDelayedTextureLoading = false;
return instantiateTexture();
}
finally {
scene.useDelayedTextureLoading = originalUseDelayedTextureLoading;
}
}
function createSkybox(scene, camera, reflectionTexture, blur) {
const originalBlockMaterialDirtyMechanism = scene.blockMaterialDirtyMechanism;
scene.blockMaterialDirtyMechanism = true;
try {
const hdrSkybox = CreateBox("hdrSkyBox", { sideOrientation: Mesh.BACKSIDE }, scene);
const hdrSkyboxMaterial = new BackgroundMaterial("skyBox", scene);
// Use the default image processing configuration on the skybox (e.g. don't apply tone mapping, contrast, or exposure).
hdrSkyboxMaterial.imageProcessingConfiguration = new ImageProcessingConfiguration();
hdrSkyboxMaterial.reflectionTexture = reflectionTexture;
reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE;
hdrSkyboxMaterial.reflectionBlur = blur;
hdrSkybox.material = hdrSkyboxMaterial;
hdrSkybox.isPickable = false;
hdrSkybox.infiniteDistance = true;
hdrSkybox.applyFog = false;
updateSkybox(hdrSkybox, camera);
return hdrSkybox;
}
finally {
scene.blockMaterialDirtyMechanism = originalBlockMaterialDirtyMechanism;
}
}
function updateSkybox(skybox, camera) {
skybox?.scaling.setAll((camera.maxZ - camera.minZ) / 2);
}
function computeModelsMaxExtents(models) {
return models.flatMap((model) => {
return computeMaxExtents(model.assetContainer.meshes, model.assetContainer.animationGroups[model.selectedAnimation]);
});
}
function reduceMeshesExtendsToBoundingInfo(maxExtents) {
if (maxExtents.length === 0) {
return null;
}
const min = new Vector3(Math.min(...maxExtents.map((e) => e.minimum.x)), Math.min(...maxExtents.map((e) => e.minimum.y)), Math.min(...maxExtents.map((e) => e.minimum.z)));
const max = new Vector3(Math.max(...maxExtents.map((e) => e.maximum.x)), Math.max(...maxExtents.map((e) => e.maximum.y)), Math.max(...maxExtents.map((e) => e.maximum.z)));
const size = max.subtract(min);
const center = min.add(size.scale(0.5));
return {
extents: {
min: min.asArray(),
max: max.asArray(),
},
size: size.asArray(),
center: center.asArray(),
};
}
/**
* Adjusts the light's target direction to ensure it's not too flat and points downwards.
* @param targetDirection The target direction of the light.
* @returns The adjusted target direction of the light.
*/
function adjustLightTargetDirection(targetDirection) {
const lightSteepnessThreshold = -0.01; // threshold to trigger steepness adjustment
const lightSteepnessFactor = 10; // the factor to multiply Y by if it's too flat
const minLightDirectionY = -0.05; // the minimum steepness for light direction Y
const adjustedDirection = targetDirection.clone();
// ensure light points downwards
if (adjustedDirection.y > 0) {
adjustedDirection.y *= -1;
}
// if light is too flat (pointing almost horizontally or very slightly down), make it steeper
if (adjustedDirection.y > lightSteepnessThreshold) {
adjustedDirection.y = Math.min(adjustedDirection.y * lightSteepnessFactor, minLightDirectionY);
}
return adjustedDirection;
}
/**
* Compute the bounding info for the models by computing their maximum extents, size, and center considering animation, skeleton, and morph targets.
* @param models The models to consider when computing the bounding info
* @returns The computed bounding info for the models or null
*/
function computeModelsBoundingInfos(models) {
const maxExtents = computeModelsMaxExtents(models);
return reduceMeshesExtendsToBoundingInfo(maxExtents);
}
// This helper function is used in functions that are naturally void returning, but need to call an async Promise returning function.
// If there is any error (other than AbortError) in the async function, it will be logged.
function observePromise(promise) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
try {
await promise;
}
catch (error) {
if (!(error instanceof AbortError)) {
Logger.Error(error);
}
}
})();
}
/**
* Generates a HotSpot from a camera by computing its spherical coordinates (alpha, beta, radius) relative to a target point.
*
* The target point is determined using the camera's forward ray:
* - If the ray intersects with a mesh in the model, the intersection point is used as the target.
* - If no intersection is found, a fallback target is calculated by projecting the distance
* between the camera and the model's center along the camera's forward direction.
*
* @param model The reference model used to determine the target point.
* @param camera The camera from which the HotSpot is generated.
* @returns A HotSpot object.
*/
async function CreateHotSpotFromCamera(model, camera) {
await import('@babylonjs/core/Culling/ray.js');
const scene = model.assetContainer.scene;
const ray = camera.getForwardRay(100, camera.getWorldMatrix(), camera.globalPosition); // Set starting point to camera global position
const camGlobalPos = camera.globalPosition.clone();
// Target
let radius = 0.0001; // Just to avoid division by zero
const targetPoint = Vector3.Zero();
const pickingInfo = scene.pickWithRay(ray, (mesh) => model.assetContainer.meshes.includes(mesh));
if (pickingInfo && pickingInfo.hit) {
targetPoint.copyFrom(pickingInfo.pickedPoint); // Use intersection point as target
}
else {
const worldBounds = model.getWorldBounds();
const centerArray = worldBounds ? worldBounds.center : [0, 0, 0];
const distancePoint = Vector3.FromArray(centerArray);
const direction = ray.direction.clone();
targetPoint.copyFrom(camGlobalPos);
radius = Vector3.Distance(camGlobalPos, distancePoint);
direction.scaleAndAddToRef(radius, targetPoint); // Compute fallback target
}
const computationVector = Vector3.Zero();
camGlobalPos.subtractToRef(targetPoint, computationVector);
// Radius
if (pickingInfo && pickingInfo.hit) {
radius = computationVector.length();
}
// Alpha and Beta
const alpha = ComputeAlpha(computationVector);
const beta = ComputeBeta(computationVector.y, radius);
const targetArray = targetPoint.asArray();
return { type: "world", position: targetArray, normal: targetArray, cameraOrbit: [alpha, beta, radius] };
}
/**
* The default options for the Viewer.
*/
const DefaultViewerOptions = {
clearColor: [0, 0, 0, 0],
autoSuspendRendering: true,
environmentConfig: {
intensity: 1,
blur: 0.3,
rotation: 0,
},
environmentLighting: "auto",
environmentSkybox: "none",
cameraAutoOrbit: {
enabled: false,
delay: 2000,
speed: 0.05,
},
animationAutoPlay: false,
animationSpeed: 1,
shadowConfig: {
quality: "none",
},
postProcessing: {
toneMapping: "neutral",
contrast: 1,
exposure: 1,
},
useRightHandedSystem: false,
};
const defaultLoadEnvironmentOptions = {
lighting: true,
skybox: true,
};
/**
* Provides the result of a hot spot query.
*/
class ViewerHotSpotResult {
constructor() {
/**
* 2D canvas position in pixels
*/
this.screenPosition = [NaN, NaN];
/**
* 3D world coordinates
*/
this.worldPosition = [NaN, NaN, NaN];
/**
* visibility range is [-1..1]. A value of 0 means camera eye is on the plane.
*/
this.visibility = NaN;
}
}
/**
* @experimental
* Provides an experience for viewing a single 3D model.
* @remarks
* The Viewer is not tied to a specific UI framework and can be used with Babylon.js in a browser or with Babylon Native.
*/
class Viewer {
constructor(_engine, _options) {
this._engine = _engine;
this._options = _options;
/**
* When enabled, the Viewer will emit additional diagnostic logs to the console.
*/
this.showDebugLogs = false;
/**
* Fired when the environment has changed.
*/
this.onEnvironmentChanged = new Observable();
/**
* Fired when the environment configuration has changed.
*/
this.onEnvironmentConfigurationChanged = new Observable();
/**
* Fired when an error occurs while loading the environment.
*/
this.onEnvironmentError = new Observable();
/**
* Fired when the shadows configuration changes.
*/
this.onShadowsConfigurationChanged = new Observable();
/**
* Fired when the post processing state changes.
*/
this.onPostProcessingChanged = new Observable();
/**
* Fired when a model is loaded into the viewer (or unloaded from the viewer).
* @remarks
* The event argument is the source that was loaded, or null if no model is loaded.
*/
this.onModelChanged = new Observable();
/**
* Fired when an error occurs while loading a model.
*/
this.onModelError = new Observable();
/**
* Fired when progress changes on loading activity.
*/
this.onLoadingProgressChanged = new Observable();
/**
* Fired when the camera auto orbit state changes.
*/
this.onCameraAutoOrbitChanged = new Observable();
/**
* Fired when the selected animation changes.
*/
this.onSelectedAnimationChanged = new Observable();
/**
* Fired when the animation speed changes.
*/
this.onAnimationSpeedChanged = new Observable();
/**
* Fired when the selected animation is playing or paused.
*/
this.onIsAnimationPlayingChanged = new Observable();
/**
* Fired when the current point on the selected animation timeline changes.
*/
this.onAnimationProgressChanged = new Observable();
/**
* Fired when the selected material variant changes.
*/
this.onSelectedMaterialVariantChanged = new Observable();
/**
* Fired when the hot spots object changes to a complete new object instance.
*/
this.onHotSpotsChanged = new Observable();
/**
* Fired when the cameras as hot spots property changes.
*/
this.onCamerasAsHotSpotsChanged = new Observable();
this._renderedLastFrame = null;
this._sceneOptimizer = null;
this._tempVectors = BuildTuple(4, Vector3.Zero);
this._meshDataCache = new Map();
this._renderLoopController = null;
this._loadedModelsBacking = [];
this._activeModelBacking = null;
this._environmentSkyboxMode = "none";
this._environmentLightingMode = "none";
this._skybox = null;
this._skyboxBlur = this._options?.environmentConfig?.blur ?? DefaultViewerOptions.environmentConfig.blur;
this._skyboxTexture = null;
this._reflectionTexture = null;
this._reflectionsIntensity = this._options?.environmentConfig?.intensity ?? DefaultViewerOptions.environmentConfig.intensity;
this._reflectionsRotation = this._options?.environmentConfig?.rotation ?? DefaultViewerOptions.environmentConfig.rotation;
this._light = null;
this._autoSuspendRendering = this._options?.autoSuspendRendering ?? DefaultViewerOptions.autoSuspendRendering;
this._sceneMutated = false;
this._suspendRenderCount = 0;
this._isDisposed = false;
this._loadModelLock = new AsyncLock();
this._loadModelAbortController = null;
this._loadEnvironmentLock = new AsyncLock();
this._loadEnvironmentAbortController = null;
this._camerasAsHotSpotsAbortController = null;
this._updateShadowsLock = new AsyncLock();
this._shadowsAbortController = null;
this._loadOperations = new Set();
this._activeAnimationObservers = [];
this._animationSpeed = this._options?.animationSpeed ?? DefaultViewerOptions.animationSpeed;
this._camerasAsHotSpots = false;
this._hotSpots = this._options?.hotSpots ?? {};
this._shadowQuality = this._options?.shadowConfig?.quality ?? DefaultViewerOptions.shadowConfig.quality;
this._shadowState = {};
this._defaultHardwareScalingLevel = this._lastHardwareScalingLevel = this._engine.getHardwareScalingLevel();
{
const scene = new Scene(this._engine);
scene.useRightHandedSystem = this._options?.useRightHandedSystem ?? DefaultViewerOptions.useRightHandedSystem;
// Deduce tone mapping, contrast, and exposure from the scene (so the viewer stays in sync if anything mutates these values directly on the scene).
this._toneMappingEnabled = scene.imageProcessingConfiguration.toneMappingEnabled;
this._toneMappingType = scene.imageProcessingConfiguration.toneMappingType;
this._contrast = scene.imageProcessingConfiguration.contrast;
this._exposure = scene.imageProcessingConfiguration.exposure;
this._imageProcessingConfigurationObserver = scene.imageProcessingConfiguration.onUpdateParameters.add(() => {
let hasChanged = false;
if (this._toneMappingEnabled !== scene.imageProcessingConfiguration.toneMappingEnabled) {
this._toneMappingEnabled = scene.imageProcessingConfiguration.toneMappingEnabled;
hasChanged = true;
}
if (this._toneMappingType !== scene.imageProcessingConfiguration.toneMappingType) {
this._toneMappingType = scene.imageProcessingConfiguration.toneMappingType;
hasChanged = true;
}
if (this._contrast !== scene.imageProcessingConfiguration.contrast) {
this._contrast = scene.imageProcessingConfiguration.contrast;
hasChanged = true;
}
if (this._exposure !== scene.imageProcessingConfiguration.exposure) {
this._exposure = scene.imageProcessingConfiguration.exposure;
hasChanged = true;
}
if (hasChanged) {
this.onPostProcessingChanged.notifyObservers();
}
});
const camera = new ArcRotateCamera("Viewer Default Camera", 0, 0, 1, Vector3.Zero(), scene);
camera.useInputToRestoreState = false;
camera.useAutoRotationBehavior = true;
camera.onViewMatrixChangedObservable.add(() => {
this._markSceneMutated();
});
scene.onClearColorChangedObservable.add(() => {
this._markSceneMutated();
});
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.
// This eliminates unnecessary camera movement on the local z-axis when interpolating.
camera.target = camera.position.add(camera.getForwardRay().direction.scale(distance));
camera.radius = distance;
camera.interpolateTo(undefined, undefined, undefined, pickingInfo.pickedPoint);
}
else {
this.resetCamera(true);
}
}, PointerEventTypes.POINTERDOUBLETAP);
scene.onNewCameraAddedObservable.add((camera) => {
if (this.camerasAsHotSpots) {
observePromise(this._addCameraHotSpot(camera, this._camerasAsHotSpotsAbortController?.signal));
}
});
scene.onCameraRemovedObservable.add((camera) => {
this._removeCameraHotSpot(camera);
});
this._scene = scene;
this._camera = camera;
}
this._scene.skipFrustumClipping = true;
this._scene.skipPointerDownPicking = true;
this._scene.skipPointerUpPicking = true;
this._scene.skipPointerMovePicking = true;
this._snapshotHelper = new SnapshotRenderingHelper(this._scene, { morphTargetsNumMaxInfluences: 30 });
// this._snapshotHelper.showDebugLogs = true;
this._beforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => {
this._snapshotHelper.updateMesh(this._scene.meshes);
});
this._camera.attachControl();
this._autoRotationBehavior = this._camera.getBehaviorByName("AutoRotation");
this._reset(false, "camera");
// Load a default light, but ignore errors as the user might be immediately loading their own environment.
observePromise(this.resetEnvironment());
this._beginRendering();
// eslint-disable-next-line @typescript-eslint/no-this-alias
const viewer = this;
this._options?.onInitialized?.({
scene: viewer._scene,
camera: viewer._camera,
get model() {
return viewer._activeModel ?? null;
},
suspendRendering: () => this._suspendRendering(),
markSceneMutated: () => this._markSceneMutated(),
pick: async (screenX, screenY) => await this._pick(screenX, screenY),
});
this._reset(false, "source", "environment", "post-processing");
}
/**
* The camera auto orbit configuration.
*/
get cameraAutoOrbit() {
return {
enabled: this._camera.behaviors.includes(this._autoRotationBehavior),
speed: this._autoRotationBehavior.idleRotationSpeed,
delay: this._autoRotationBehavior.idleRotationWaitTime,
};
}
set cameraAutoOrbit(value) {
if (value.enabled !== undefined && value.enabled !== this.cameraAutoOrbit.enabled) {
if (value.enabled) {
this._camera.addBehavior(this._autoRotationBehavior);
}
else {
this._camera.removeBehavior(this._autoRotationBehavior);
}
}
if (value.delay !== undefined) {
this._autoRotationBehavior.idleRotationWaitTime = value.delay;
}
if (value.speed !== undefined) {
this._autoRotationBehavior.idleRotationSpeed = value.speed;
}
this.onCameraAutoOrbitChanged.notifyObservers();
}
/**
* Get the current environment configuration.
*/
get environmentConfig() {
return {
intensity: this._reflectionsIntensity,
blur: this._skyboxBlur,
rotation: this._reflectionsRotation,
};
}
set environmentConfig(value) {
if (value.blur !== undefined) {
this._changeSkyboxBlur(value.blur);
}
if (value.intensity !== undefined) {
this._changeEnvironmentIntensity(value.intensity);
this._changeShadowLightIntensity();
}
if (value.rotation !== undefined) {
this._changeEnvironmentRotation(value.rotation);
this._rotateShadowLightWithEnvironment();
}
this.onEnvironmentConfigurationChanged.notifyObservers();
}
/**
* Get the current shadow configuration.
*/
get shadowConfig() {
return {
quality: this._shadowQuality,
};
}
/**
* Update the shadow configuration.
* @param value The new shadow configuration.
*/
async updateShadows(value) {
if (value.quality && this._shadowQuality !== value.quality) {
this._shadowQuality = value.quality;
await this._updateShadows();
this.onShadowsConfigurationChanged.notifyObservers();
}
}
_changeSkyboxBlur(value) {
if (value !== this._skyboxBlur) {
this._skyboxBlur = value;
if (this._skybox) {
const material = this._skybox.material;
if (material instanceof BackgroundMaterial) {
this._snapshotHelper.disableSnapshotRendering();
material.reflectionBlur = this._skyboxBlur;
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
}
}
}
}
/**
* Change the environment rotation.
* @param value the rotation in radians
*/
_changeEnvironmentRotation(value) {
if (value !== this._reflectionsRotation) {
this._reflectionsRotation = value;
this._snapshotHelper.disableSnapshotRendering();
if (this._skyboxTexture) {
this._skyboxTexture.rotationY = this._reflectionsRotation;
}
if (this._reflectionTexture) {
this._reflectionTexture.rotationY = this._reflectionsRotation;
}
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
}
}
_changeEnvironmentIntensity(value) {
if (value !== this._reflectionsIntensity) {
this._reflectionsIntensity = value;
this._snapshotHelper.disableSnapshotRendering();
if (this._skyboxTexture) {
this._skyboxTexture.level = this._reflectionsIntensity;
}
if (this._reflectionTexture) {
this._reflectionTexture.level = this._reflectionsIntensity;
}
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
}
}
_updateAutoClear() {
// NOTE: Not clearing (even when every pixel is rendered with an opaque color) results in rendering
// artifacts in Chromium browsers on Intel-based Macs (see https://issues.chromium.org/issues/396612322).
// The performance impact of clearing when not necessary is very small, so for now just always auto clear.
//this._scene.autoClear = !this._skybox || !this._skybox.isEnabled() || !this._skyboxVisible;
this._scene.autoClear = true;
this._markSceneMutated();
}
/**
* The post processing configuration.
*/
get postProcessing() {
let toneMapping = "none";
if (this._toneMappingEnabled) {
switch (this._toneMappingType) {
case ImageProcessingConfiguration.TONEMAPPING_STANDARD:
toneMapping = "standard";
break;
case ImageProcessingConfiguration.TONEMAPPING_ACES:
toneMapping = "aces";
break;
case ImageProcessingConfiguration.TONEMAPPING_KHR_PBR_NEUTRAL:
toneMapping = "neutral";
break;
}
}
return {
toneMapping,
contrast: this._contrast,
exposure: this._exposure,
};
}
set postProcessing(value) {
this._snapshotHelper.disableSnapshotRendering();
if (value.toneMapping !== undefined) {
if (value.toneMapping === "none") {
this._scene.imageProcessingConfiguration.toneMappingEnabled = false;
}
else {
switch (value.toneMapping) {
case "standard":
this._scene.imageProcessingConfiguration.toneMappingType = ImageProcessingConfiguration.TONEMAPPING_STANDARD;
break;
case "aces":
this._scene.imageProcessingConfiguration.toneMappingType = ImageProcessingConfiguration.TONEMAPPING_ACES;
break;
case "neutral":
this._scene.imageProcessingConfiguration.toneMappingType = ImageProcessingConfiguration.TONEMAPPING_KHR_PBR_NEUTRAL;
break;
}
this._scene.imageProcessingConfiguration.toneMappingEnabled = true;
}
}
if (value.contrast !== undefined) {
this._scene.imageProcessingConfiguration.contrast = value.contrast;
}
if (value.exposure !== undefined) {
this._scene.imageProcessingConfiguration.exposure = value.exposure;
}
this._scene.imageProcessingConfiguration.isEnabled = this._toneMappingEnabled || this._contrast !== 1 || this._exposure !== 1;
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
}
/**
* Gets information about loading activity.
* @remarks
* false indicates no loading activity.
* true indicates loading activity with no progress information.
* A number between 0 and 1 indicates loading activity with progress information.
*/
get loadingProgress() {
if (this._loadOperations.size > 0) {
let totalProgress = 0;
for (const operation of this._loadOperations) {
if (operation.progress == null) {
return true;
}
totalProgress += operation.progress;
}
return totalProgress / this._loadOperations.size;
}
return false;
}
get _loadedModels() {
return this._loadedModelsBacking;
}
get _activeModel() {
return this._activeModelBacking;
}
_setActiveModel(...args) {
const [model, options] = args;
if (model !== this._activeModelBacking) {
this._activeModelBacking = model;
this._updateLight();
observePromise(this._updateShadows());
this._applyAnimationSpeed();
this._selectAnimation(0, false);
this.onSelectedMaterialVariantChanged.notifyObservers();
this._reframeCamera(true, model ? [model] : undefined);
this.onModelChanged.notifyObservers(options?.source ?? null);
}
}
/**
* The list of animation names for the currently loaded model.
*/
get animations() {
return this._activeModel?.assetContainer.animationGroups.map((group) => group.name) ?? [];
}
/**
* The currently selected animation index.
*/
get selectedAnimation() {
return this._activeModel?.selectedAnimation ?? -1;
}
set selectedAnimation(value) {
this._selectAnimation(value, this._loadOperations.size === 0);
}
_selectAnimation(index, interpolateCamera = true) {
index = Math.round(Clamp(index, -1, this.animations.length - 1));
if (this._activeModel && index !== this._activeModel.selectedAnimation) {
this._activeAnimationObservers.forEach((observer) => observer.remove());
this._activeAnimationObservers = [];
this._activeModel.selectedAnimation = index;
if (this._activeAnimation) {
this._activeAnimationObservers = [
this._activeAnimation.onAnimationGroupPlayObservable.add(() => {
this.onIsAnimationPlayingChanged.notifyObservers();
}),
this._activeAnimation.onAnimationGroupPauseObservable.add(() => {
this.onIsAnimationPlayingChanged.notifyObservers();
}),
this._activeAnimation.onAnimationGroupEndObservable.add(() => {
this.onIsAnimationPlayingChanged.notifyObservers();
this.onAnimationProgressChanged.notifyObservers();
}),
];
this._reframeCamera(interpolateCamera);
}
this.onSelectedAnimationChanged.notifyObservers();
this.onAnimationProgressChanged.notifyObservers();
}
}
/**
* True if an animation is currently playing.
*/
get isAnimationPlaying() {
return this._activeModelBacking?._animationPlaying() ?? false;
}
/**
* The speed scale at which animations are played.
*/
get animationSpeed() {
return this._animationSpeed;
}
set animationSpeed(value) {
this._animationSpeed = value;
this._applyAnimationSpeed();
this.onAnimationSpeedChanged.notifyObservers();
}
/**
* The current point on the selected animation timeline, normalized between 0 and 1.
*/
get animationProgress() {
if (this._activeAnimation) {
return this._activeAnimation.getCurrentFrame() / (this._activeAnimation.to - this._activeAnimation.from);
}
return 0;
}
set animationProgress(value) {
if (this._activeAnimation) {
this._activeAnimation.goToFrame(value * (this._activeAnimation.to - this._activeAnimation.from));
this.onAnimationProgressChanged.notifyObservers();
this._autoRotationBehavior.resetLastInteractionTime();
this._markSceneMutated();
}
}
get _activeAnimation() {
return this._activeModel?.assetContainer.animationGroups[this._activeModel?.selectedAnimation] ?? null;
}
/**
* The list of material variant names for the currently loaded model.
*/
get materialVariants() {
return this._activeModel?.materialVariantsController?.variants ?? [];
}
/**
* The currently selected material variant.
*/
get selectedMaterialVariant() {
return this._activeModel?.materialVariantsController?.selectedVariant ?? null;
}
set selectedMaterialVariant(value) {
if (this._activeModel?.materialVariantsController) {
if (!value) {
value = this._activeModel.materialVariantsController.variants[0];
}
if (value !== this.selectedMaterialVariant && this._activeModel.materialVariantsController.variants.includes(value)) {
this._snapshotHelper.disableSnapshotRendering();
this._activeModel.materialVariantsController.selectedVariant = value;
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
this.onSelectedMaterialVariantChanged.notifyObservers();
}
}
}
/**
* The set of defined hotspots.
*/
get hotSpots() {
return this._hotSpots;
}
set hotSpots(value) {
this._hotSpots = value;
this.onHotSpotsChanged.notifyObservers();
}
/**
* True if scene cameras should be used as hotspots.
*/
get camerasAsHotSpots() {
return this._camerasAsHotSpots;
}
set camerasAsHotSpots(value) {
if (this._camerasAsHotSpots !== value) {
this._camerasAsHotSpots = value;
this._toggleCamerasAsHotSpots();
this.onCamerasAsHotSpotsChanged.notifyObservers();
}
}
_beginLoadOperation() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const viewer = this;
let progress = null;
const loadOperation = {
get progress() {
return progress;
},
set progress(value) {
progress = value;
viewer.onLoadingProgressChanged.notifyObservers();
},
dispose: () => {
viewer._loadOperations.delete(loadOperation);
viewer.onLoadingProgressChanged.notifyObservers();
},
};
this._loadOperations.add(loadOperation);
this.onLoadingProgressChanged.notifyObservers();
return loadOperation;
}
/**
* Loads a 3D model from the specified URL.
* @remarks
* If a model is already loaded, it will be unloaded before loading the new model.
* @param source A url or File or ArrayBufferView that points to the model to load.
* @param options The options to use when loading the model.
* @param abortSignal An optional signal that can be used to abort the loading process.
*/
async loadModel(source, options, abortSignal) {
await this._updateModel(source, options, abortSignal);
}
/**
* Unloads the current 3D model if one is loaded.
* @param abortSignal An optional signal that can be used to abort the reset.
*/
async resetModel(abortSignal) {
await this._updateModel(undefined, undefined, abortSignal);
}
async _loadModel(source, options, abortSignal) {
this._throwIfDisposedOrAborted(abortSignal);
const loadOperation = this._beginLoadOperation();
const originalOnProgress = options?.onProgress;
const onProgress = (event) => {
originalOnProgress?.(event);
loadOperation.progress = event.lengthComputable ? event.loaded / event.total : null;
};
delete options?.onProgress;
let materialVariantsController = null;
const originalOnMaterialVariantsLoaded = options?.pluginOptions?.gltf?.extensionOptions?.KHR_materials_variants?.onLoaded;
const onMaterialVariantsLoaded = (controller) => {
originalOnMaterialVariantsLoaded?.(controller);
materialVariantsController = controller;
};
delete options?.pluginOptions?.gltf?.extensionOptions?.KHR_materials_variants?.onLoaded;
const defaultOptions = {
// Pass a progress callback to update the loading progress.
onProgress,
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,
extensionOptions: {
// eslint-disable-next-line @typescript-eslint/naming-convention
KHR_materials_variants: {
// Capture the material variants controller when it is loaded.
onLoaded: onMaterialVariantsLoaded,
},
},
},
},
};
options = deepMerge(defaultOptions, options ?? {});
this._snapshotHelper.disableSnapshotRendering();
try {
const assetContainer = await LoadAssetContainerAsync(source, this._scene, options);
RemoveUnreferencedVerticesData(assetContainer.meshes.filter((mesh) => mesh instanceof Mesh));
assetContainer.animationGroups.forEach((group) => {
group.start(true, this.animationSpeed);
group.pause();
});
assetContainer.addAllToScene();
this._snapshotHelper.fixMeshes(assetContainer.meshes);
let selectedAnimation = -1;
const cachedWorldBounds = [];
// eslint-disable-next-line @typescript-eslint/no-this-alias
const viewer = this;
const model = {
assetContainer,
materialVariantsController,
_animationPlaying: () => {
const activeAnimation = assetContainer.animationGroups[selectedAnimation];
return activeAnimation?.isPlaying ?? false;
},
_shouldRender: () => {
const stillTransitioning = model?.assetContainer.animationGroups.some((group) => group.animatables.some((animatable) => animatable.animationStarted));
// Should render if :
// 1. An animation is playing.
// 2. Animation is paused, but any individual animatable hasn't transitioned to a paused state yet.
return model._animationPlaying() || stillTransitioning;
},
getHotSpotToRef: (query, result) => {
return this._getHotSpotToRef(assetContainer, query, result);
},
dispose: () => {
this._snapshotHelper.disableSnapshotRendering();
assetContainer.meshes.forEach((mesh) => this._meshDataCache.delete(mesh));
assetContainer.dispose();
const index = this._loadedModelsBacking.indexOf(model);
if (index !== -1) {
this._loadedModelsBacking.splice(index, 1);
if (model === this._activeModel) {
this._setActiveModel(null);
}
}
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
},
getWorldBounds: (animationIndex = selectedAnimation) => {
let worldBounds = cachedWorldBounds[animationIndex];
if (!worldBounds) {
worldBounds = computeModelsBoundingInfos([model]);
if (worldBounds) {
cachedWorldBounds[animationIndex] = worldBounds;
}
}
return worldBounds;
},
resetWorldBounds: () => {
cachedWorldBounds.length = 0;
},
get selectedAnimation() {
return selectedAnimation;
},
set selectedAnimation(index) {
let activeAnimation = assetContainer.animationGroups[selectedAnimation];
const startAnimation = activeAnimation?.isPlaying ?? false;
if (activeAnimation) {
activeAnimation.pause();
activeAnimation.goToFrame(0);
}
selectedAnimation = index;
activeAnimation = assetContainer.animationGroups[selectedAnimation];
observePromise(viewer._updateShadows());
if (activeAnimation) {
activeAnimation.goToFrame(0);
activeAnimation.play(true);
if (!startAnimation) {
activeAnimation.pause();
}
}
},
makeActive: (options) => {
this._setActiveModel(model, options);
},
};
this._loadedModelsBacking.push(model);
return model;
}
catch (e) {
this.onModelError.notifyObservers(e);
throw e;
}
finally {
loadOperation.dispose();
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
}
}
async _updateModel(source, options, abortSignal) {
this._throwIfDisposedOrAborted(abortSignal);
this._loadModelAbortController?.abort(new AbortError("New model is being loaded before previous model finished loading."));
const abortController = (this._loadModelAbortController = new AbortController());
await this._loadModelLock.lockAsync(async () => {
throwIfAborted(abortSignal, abortController.signal);
this._activeModel?.dispose();
this._activeModelBacking = null;
this.selectedAnimation = -1;
if (source) {
const model = await this._loadModel(source, options, abortController.signal);
model.makeActive(Object.assign({ source, interpolateCamera: false }, options));
this._reset(false, "camera", "animation", "material-variant");
}
});
// If there are PBR materials after the model load operation and an environment texture is not loaded, load the default environment.
if (!this._scene.environmentTexture && this._scene.materials.some((material) => material instanceof PBRMaterial)) {
await this.resetEnvironment({ lighting: true }, abortSignal);
}
this._startSceneOptimizer(true);
}
async _updateShadows() {
this._shadowsAbortController?.abort(new AbortError("Shadows quality is being change before previous shadows finished initializing."));
const abortController = (this._shadowsAbortController = new AbortController());
await this._updateShadowsLock.lockAsync(async () => {
if (this._shadowQuality === "none") {
this._disposeShadows();
}
else {
// make sure there is an env light before creating shadows
if (!this._reflectionTexture) {
await this.loadEnvironment("auto", { lighting: true, skybox: false });
}
if (this._shadowQuality === "normal") {
await this._updateShadowMap(abortController.signal);
}
else if (this._shadowQuality === "high") {
await this._updateEnvShadow(abortController.signal);
}
}
});
}
_changeShadowLightIntensity() {
if (this._shadowState.high) {
this._shadowState.high.pipeline.resetAccumulation();
this._startIblShadowsRenderTime();
}
}
_rotateShadowLightWithEnvironment() {
if (this._shadowQuality === "normal" && this._shadowState.normal) {
if (this._shadowState.normal.light) {
this._shadowState.normal.refreshLightPositionDirection(this._reflectionsRotation);
}
}
else if (this._shadowQuality === "high" && this._shadowState.high) {
this._shadowState.high.pipeline?.resetAccumulation();
this._startIblShadowsRenderTime();
}
}
// maybe move this into shadow state
_startIblShadowsRenderTime() {
if (this._shadowState.high) {
if (this._shadowState.high.renderTimer != null) {
clearTimeout(this._shadowState.high.renderTimer);
}
else {
// Only disable if a timeout is not pending, otherwise it has already been called without a paired enable call.
this._snapshotHelper.disableSnapshotRendering();
}
this._shadowState.high.shouldRender = true;
const onRenderTimeout = () => {
if (this._shadowState.high) {
this._shadowState.high.shouldRender = false;
this._shadowState.high.renderTimer = null;
}
this._snapshotHelper.enableSnapshotRendering();
};
this._shadowState.high.renderTimer = setTimeout(onRenderTimeout,
// based on the shadow remanence as we can't estimate the time it takes to accumulate the shadows
this._shadowState.high.pipeline.shadowRemanence * 4000);
}
}
async _updateEnvShadow(abortSignal) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const [{ ShaderMaterial }, { ShaderLanguage }, { CreateDisc }, { IblShadowsRenderPipeline }] = await Promise.all([
import('@babylonjs/core/Materials/shaderMaterial.js'),
import('@babylonjs/core/Materials/shaderLanguage.js'),
import('@babylonjs/core/Meshes/Builders/discBuilder.js'),
import('@babylonjs/core/Rendering/IBLShadows/iblShadowsRenderPipeline.js'),
import('@babylonjs/core/Engines/Extensions/engine.multiRender.js'),
import('@babylonjs/core/Engines/WebGPU/Extensions/engine.multiRender.js'),
import('@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipelineManagerSceneComponent.js'),
]);
// cancel if the model is unloaded before the shadows are created
this._throwIfDisposedOrAborted(abortSignal, this._loadModelAbortController?.signal, this._loadEnvironmentAbortController?.signal);
let high = this._shadowState.high;
const worldBounds = computeModelsBoundingInfos(this._loadedModelsBacking);
if (!worldBounds) {
high?.ground.setEnabled(false);
this._log("No models loaded, cannot create shadows.");
return;
}
const groundFactor = 4;
const radius = Vector3.FromArray(worldBounds.size).length();
const groundSize = groundFactor * radius;
const updateMaterial = () => {
if (this._shadowState.high) {
this._snapshotHelper.disableSnapshotRendering();
const { pipeline, groundMaterial, ground } = this._shadowState.high;
groundMaterial?.setVector2("renderTargetSize", new Vector2(this._scene.getEngine().getRenderWidth(), this._scene.getEngine().getRenderHeight()));
groundMaterial?.setFloat("shadowOpacity", pipeline.shadowOpacity);
groundMaterial?.setTexture("shadowTexture", pipeline._getAccumulatedTexture());
const groundSize = groundFactor * pipeline?.voxelGridSize;
ground?.scaling.set(groundSize, groundSize, groundSize);
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
}
};
this._snapshotHelper.disableSnapshotRendering();
if (!high) {
const pipeline = new IblShadowsRenderPipeline("ibl shadows", this._scene, {
resolutionExp: 6,
sampleDirections: 3,
ssShadowsEnabled: true,
shadowRemanence: 0.7,
triPlanarVoxelization: true,
}, [this._camera]);
pipeline.toggleShadow(false);
// Useful for debugging, but not needed in production
// pipeline.allowDebugPasses = false;
// pipeline.gbufferDebugEnabled = false;
// pipeline.voxelDebugEnabled = false;
// pipeline.accumulationPassDebugEnabled = false;
const isWebGPU = this._scene.getEngine().isWebGPU;
const shaderLanguage = isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */;
const options = {
attributes: ["position", "uv"],
uniforms: ["world", "worldView", "worldViewProjection", "view", "projection", "renderTargetSize", "shadowOpacity"],
samplers: ["shadowTexture"],
shaderLanguage,
extraInitializationsAsync: async () => {
if (shaderLanguage === 1 /* ShaderLanguage.WGSL */) {
await Promise.all([import('./envShadowGround.vertex-oRlsSdho.js'), import('./envShadowGround.fragment-CB8lNguw.js')]);
}
else {
await Promise.all([import('./envShadowGround.vertex-6NXvyWvr.js'), import('./envShadowGround.fragment-L1sPCORS.js')]);
}
},
};
const groundMaterial = new ShaderMaterial("envShadowGroundMaterial", this._scene, "envShadowGround", options);
groundMaterial.alphaMode = Constants.ALPHA_MULTIPLY;
groundMaterial.alpha = 0.99;
updateMaterial();
pipeline.onShadowTextureReadyObservable.addOnce(updateMaterial);
const resizeObserver = this._engine.onResizeObservable.add(() => {
updateMaterial();
pipeline?.resetAccumulation();
this._startIblShadowsRenderTime();
});
this._camera.onViewMatrixChangedObservable.add(() => {
this._startIblShadowsRenderTime();
});
const ground = CreateDisc("envShadowGround", { radius: groundSize, tessellation: 64 }, this._scene);
ground.setEnabled(false);
ground.rotation.x = Math.PI / 2;
ground.position.y = worldBounds.extents.min[1];
ground.material = groundMaterial;
high = {
pipeline: pipeline,
groundMaterial: groundMaterial,
resizeObserver: resizeObserver,
shouldRender: true,
ground: ground,
};
}
// Remove previous meshes and materials.
high.pipeline.clearShadowCastingMeshes();
high.pipeline.clearShadowReceivingMaterials();
for (const model of this._loadedModelsBacking) {
const meshes = model.assetContainer.meshes;
for (const mesh of meshes) {
if (mesh instanceof Mesh) {
high.pipeline.addShadowCastingMesh(mesh);
if (mesh.material) {
high.pipeline.addShadowReceivingMaterial(mesh.material);
}
}
}
}
high.pipeline.onVoxelizationCompleteObservable.addOnce(() => {
this._snapshotHelper.disableSnapshotRendering();
updateMaterial();
high.pipeline.toggleShadow(true);
high.ground.setEnabled(true);
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
});
high.ground.position.y = worldBounds.extents.min[1];
// call the update now because a model might be loaded before the shadows are created
high.pipeline.updateSceneBounds();
high.pipeline.updateVoxelization();
high.pipeline.resetAccumulation();
// shadow map
this._shadowState.normal?.ground.setEnabled(false);
high.pipeline.toggleShadow(true);
high.ground.setEnabled(true);
this._startIblShadowsRenderTime();
this._shadowState.high = high;
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
}
/**
* Finds the light direction the environment (IBL).
* If the environment changes, it will explicitly trigger the generation of CDF maps.
* @param iblCdfGenerator The IblCdfGenerator to use for finding the dominant direction.
* @returns A promise that resolves to the dominant direction vector.
*/
async _findIblDominantDirection(iblCdfGenerator) {
if (this._reflectionTexture && iblCdfGenerator.iblSource !== this._reflectionTexture) {
iblCdfGenerator.iblSource = this._reflectionTexture;
await iblCdfGenerator.renderWhenReady();
}
return await iblCdfGenerator.findDominantDirection();
}
async _updateShadowMap(abortSignal) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const [{ CreateDisc }, { RenderTargetTexture }, { ShadowGenerator }, { IblCdfGenerator }] = await Promise.all([
import('@babylonjs/core/Meshes/Builders/discBuilder.js'),
import('@babylonjs/core/Materials/Textures/renderTargetTexture.js'),
import('@babylonjs/core/Lights/Shadows/shadowGenerator.js'),
import('@babylonjs/core/Rendering/iblCdfGenerator.js'),
import('@babylonjs/core/Rendering/iblCdfGeneratorSceneComponent.js'),
import('@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent.js'),
]);
// cancel if the model is unloaded before the shadows are created
this._throwIfDisposedOrAborted(abortSignal, this._loadModelAbortController?.signal, this._loadEnvironmentAbortController?.signal);
let normal = this._shadowState.normal;
const worldBounds = computeModelsBoundingInfos(this._loadedModelsBacking);
if (!worldBounds) {
normal?.ground.setEnabled(false);
this._log("No models loaded, cannot create shadows.");
return;
}
const radius = Vector3.FromArray(worldBounds.size).length();
if (this._shadowQuality !== "normal") {
return;
}
const iblCdfGenerator = normal?.iblDirection.iblCdfGenerator ? normal?.iblDirection.iblCdfGenerator : new IblCdfGenerator(this._engine);
const iblDirection = await this._findIblDominantDirection(iblCdfGenerator);
this._throwIfDisposedOrAborted(abortSignal, this._loadModelAbortController?.signal, this._loadEnvironmentAbortController?.signal);
this._snapshotHelper.disableSnapshotRendering();
const size = 4096;
const groundFactor = 20;
const groundSize = radius * groundFactor;
const positionFactor = radius * 3;
const iblLightStrength = iblDirection ? Clamp(iblDirection.length(), 0.0, 1.0) : 0.5;
if (!normal) {
const light = new DirectionalLight("shadowMapDirectionalLight", Vector3.Zero(), this._scene);
light.autoUpdateExtends = false;
const generator = new ShadowGenerator(size, light);
generator.setDarkness(Lerp(0.8, 0.2, iblLightStrength));
generator.setTransparencyShadow(true);
generator.filteringQuality = ShadowGenerator.QUALITY_HIGH;
generator.useBlurExponentialShadowMap = true;
generator.enableSoftTransparentShadow = true;
generator.bias = radius / 1000;
generator.useKernelBlur = true;
generator.blurKernel = Math.floor(Lerp(64, 8, iblLightStrength));
const shadowMap = generator.getShadowMap();
if (shadowMap) {
shadowMap.refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONEVERYFRAME;
}
const shadowMaterial = new BackgroundMaterial("shadowMapGroundMaterial", this._scene);
shadowMaterial.shadowOnly = true;
shadowMaterial.primaryColor = Color3.Black();
const ground = CreateDisc("shadowMapGround", { radius: groundSize, tessellation: 64 }, this._scene);
ground.rotation.x = Math.PI / 2;
ground.receiveShadows = true;
ground.position.y = worldBounds.extents.min[1];
ground.material = shadowMaterial;
light.includedOnlyMeshes = [ground];
const newNormal = (normal = {
light: light,
generator: generator,
ground: ground,
shouldRender: true,
iblDirection: {
iblCdfGenerator: iblCdfGenerator,
positionFactor: positionFactor,
direction: iblDirection,
},
refreshLightPositionDirection(reflectionRotation) {
let effectiveSourceDir = this.iblDirection.direction.normalizeToNew();
if (this.light.getScene().useRightHandedSystem) {
effectiveSourceDir.z *= -1;
}
const rotationYMatrix = Matrix.RotationY(reflectionRotation * -1);
effectiveSourceDir = Vector3.TransformCoordinates(effectiveSourceDir, rotationYMatrix);
this.light.position = effectiveSourceDir.scale(this.iblDirection.positionFactor);
this.light.direction = adjustLightTargetDirection(effectiveSourceDir.negate());
},
});
// Since the light is not applied to the meshes of the model (we only want shadows, not lighting),
// the ShadowGenerator's isReady will think everything is ready before it actually is. To account
// for this, explicitly wait for the first shadow map render to consider shadows in a ready state.
generator.onAfterShadowMapRenderObservable.addOnce(() => {
newNormal.shouldRender = false;
});
}
normal.iblDirection.direction = iblDirection;
normal.iblDirection.positionFactor = positionFactor;
normal.refreshLightPositionDirection(this._reflectionsRotation);
normal.light.shadowFrustumSize = radius * 4;
for (const model of this._loadedModelsBacking) {
for (const mesh of model.assetContainer.meshes) {
normal.generator.addShadowCaster(mesh, false);
mesh.receiveShadows = true;
}
}
normal.ground.position.y = worldBounds.extents.min[1];
normal.ground.scaling.set(groundSize, groundSize, groundSize);
this._shadowState.high?.ground.setEnabled(false);
this._shadowState.high?.pipeline.toggleShadow(false);
normal.ground.setEnabled(true);
this._shadowState.normal = normal;
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
}
_disposeShadows() {
this._snapshotHelper.disableSnapshotRendering();
if (!this._shadowState) {
return;
}
for (const model of this._loadedModelsBacking) {
const meshes = model.assetContainer.meshes;
const mesh = model.assetContainer.meshes[0];
this._shadowState.normal?.generator.removeShadowCaster(mesh, true);
mesh.receiveShadows = false;
for (const mesh of meshes) {
if (mesh instanceof Mesh) {
this._shadowState.high?.pipeline.removeShadowCastingMesh(mesh);
if (mesh.material) {
this._shadowState.high?.pipeline.removeShadowReceivingMaterial(mesh.material);
}
}
}
}
const highShadow = this._shadowState.high;
const normalShadow = this._shadowState.normal;
if (normalShadow) {
normalShadow.generator.dispose();
normalShadow.light.dispose();
normalShadow.ground.dispose(true, true);
normalShadow.iblDirection.iblCdfGenerator.dispose();
this._scene.removeMesh(normalShadow.ground);
}
if (highShadow) {
highShadow.resizeObserver.remove();
highShadow.pipeline.dispose();
highShadow.ground.dispose(true, true);
this._scene.removeMesh(highShadow.ground);
if (highShadow.renderTimer) {
clearTimeout(highShadow.renderTimer);
}
}
delete this._shadowState.normal;
delete this._shadowState.high;
this.onShadowsConfigurationChanged.clear();
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
}
/**
* Loads an environment texture from the specified url and sets up a corresponding skybox.
* @remarks
* If an environment is already loaded, it will be unloaded before loading the new environment.
* @param url The url of the environment texture to load.
* @param options The options to use when loading the environment.
* @param abortSignal An optional signal that can be used to abort the loading process.
*/
async loadEnvironment(url, options, abortSignal) {
await this._updateEnvironment(url, options, abortSignal);
}
/**
* Resets the environment to its default state.
* @param options The options to use when resetting the environment.
* @param abortSignal An optional signal that can be used to abort the reset.
*/
async resetEnvironment(options, abortSignal) {
const promises = [];
// When there are PBR materials, the default environment should be used for lighting.
if (options?.lighting && this._scene.materials.some((material) => material instanceof PBRMaterial)) {
const lightingOptions = { ...options, skybox: false };
options = { ...options, lighting: false };
promises.push(this._updateEnvironment("auto", lightingOptions, abortSignal));
}
promises.push(this._updateEnvironment(undefined, options, abortSignal));
await Promise.all(promises);
}
_setEnvironmentLighting(cubeTexture) {
this._reflectionTexture = cubeTexture;
this._scene.environmentTexture = this._reflectionTexture;
this._reflectionTexture.level = this.environmentConfig.intensity;
this._reflectionTexture.rotationY = this.environmentConfig.rotation;
}
_setEnvironmentSkybox(cubeTexture) {
this._skyboxTexture = cubeTexture;
this._skyboxTexture.level = this.environmentConfig.intensity;
this._skyboxTexture.rotationY = this.environmentConfig.rotation;
this._skybox = createSkybox(this._scene, this._camera, this._skyboxTexture, this.environmentConfig.blur);
this._skybox.setEnabled(true);
this._snapshotHelper.fixMeshes([this._skybox]);
this._updateAutoClear();
}
async _updateEnvironment(url, options = defaultLoadEnvironmentOptions, abortSignal) {
this._throwIfDisposedOrAborted(abortSignal);
if (!options.lighting && !options.skybox) {
return;
}
url = url?.trim();
if (url === "auto") {
options = { ...options, extension: ".env" };
}
this._loadEnvironmentAbortController?.abort(new AbortError("New environment is being loaded before previous environment finished loading."));
const loadEnvironmentAbortController = (this._loadEnvironmentAbortController = new AbortController());
await this._loadEnvironmentLock.lockAsync(async () => {
throwIfAborted(abortSignal, loadEnvironmentAbortController.signal);
const getDefaultEnvironmentUrlAsync = async () => (await import('./defaultEnvironment-5jBs1zfd.js')).default;
const whenTextureLoadedAsync = async (cubeTexture) => {
await new Promise((resolve, reject) => {
const successObserver = cubeTexture.onLoadObservable.addOnce(() => {
successObserver.remove();
errorObserver.remove();
resolve();
});
const errorObserver = Texture.OnTextureLoadErrorObservable.add((texture) => {
if (texture === cubeTexture) {
successObserver.remove();
errorObserver.remove();
reject(new Error("Failed to load environment texture."));
}
});
});
};
const mode = !url ? "none" : url === "auto" ? "auto" : "url";
this._environmentLightingMode = options.lighting ? mode : this._environmentLightingMode;
this._environmentSkyboxMode = options.skybox ? mode : this._environmentSkyboxMode;
let lightingUrl = this._reflectionTexture?.url;
let skyboxUrl = this._skyboxTexture?.url;
this._snapshotHelper.disableSnapshotRendering();
try {
// If both modes are auto, use the default environment.
if (this._environmentLightingMode === "auto" && this._environmentSkyboxMode === "auto") {
lightingUrl = skyboxUrl = await getDefaultEnvironmentUrlAsync();
}
else {
// If the lighting mode is not auto and we are updating the lighting, use the provided url.
if (this._environmentLightingMode !== "auto" && options.lighting) {
lightingUrl = url;
}
// If the skybox mode is not auto and we are updating the skybox, use the provided url.
if (this._environmentSkyboxMode !== "auto" && options.skybox) {
skyboxUrl = url;
}
// If the lighting mode is auto, use the skybox texture if there is one, otherwise use the default environment.
if (this._environmentLightingMode === "auto") {
lightingUrl = skyboxUrl ?? (await getDefaultEnvironmentUrlAsync());
}
// If the skybox mode is auto, use the lighting texture if there is one, otherwise use the default environment.
if (this._environmentSkyboxMode === "auto") {
skyboxUrl = lightingUrl ?? (await getDefaultEnvironmentUrlAsync());
}
}
const newTexturePromises = [];
// If the lighting url is not the same as the current lighting url, load the new lighting texture.
if (lightingUrl !== this._reflectionTexture?.url) {
// Dispose the existing lighting texture if it exists.
this._reflectionTexture?.dispose();
this._reflectionTexture = null;
this._scene.environmentTexture = null;
// Load the new lighting texture if there is a target url.
if (lightingUrl) {
if (lightingUrl === this._skyboxTexture?.url) {
// If the lighting url is the same as the skybox url, clone the skybox texture.
this._setEnvironmentLighting(this._skyboxTexture.clone());
}
else {
// Otherwise, create a new cube texture from the lighting url.
const lightingTexture = await createCubeTexture(lightingUrl, this._scene, options.extension);
newTexturePromises.push(whenTextureLoadedAsync(lightingTexture));
this._setEnvironmentLighting(lightingTexture);
}
}
}
// If the skybox url is not the same as the current skybox url, load the new skybox texture.
if (skyboxUrl !== this._skyboxTexture?.url) {
// Dispose the existing skybox texture if it exists.
this._skybox?.dispose(undefined, true);
this._skyboxTexture = null;
this._skybox = null;
this._updateAutoClear();
// Load the new skybox texture if there is a target url.
if (skyboxUrl) {
if (skyboxUrl === this._reflectionTexture?.url) {
// If the skybox url is the same as the lighting url, clone the lighting texture.
this._setEnvironmentSkybox(this._reflectionTexture.clone());
}
else {
// Otherwise, create a new cube texture from the skybox url.
const skyboxTexture = await createCubeTexture(skyboxUrl, this._scene, options.extension);
newTexturePromises.push(whenTextureLoadedAsync(skyboxTexture));
this._setEnvironmentSkybox(skyboxTexture);
}
}
}
await Promise.all(newTexturePromises);
this._updateLight();
observePromise(this._updateShadows());
this.onEnvironmentChanged.notifyObservers();
}
catch (e) {
this.onEnvironmentError.notifyObservers(e);
throw e;
}
finally {
this._snapshotHelper.enableSnapshotRendering();
this._markSceneMutated();
}
});
}
/**
* Toggles the play/pause animation state if there is a selected animation.
*/
toggleAnimation() {
if (this.isAnimationPlaying) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.pauseAnimation();
}
else {
this.playAnimation();
}
}
/**
* Plays the selected animation if there is one.
*/
playAnimation() {
this._activeAnimation?.play(true);
}
/**
* Pauses the selected animation if there is one.
*/
async pauseAnimation() {
this._activeAnimation?.pause();
}
/**
* Resets the camera to its initial pose.
* @param reframe If true, the camera will be reframed to fit the model bounds. If false, it will use the default camera pose passed in with the options to the constructor (if present).
* If undefined, default to false if other viewer state matches the default state (such as the selected animation), otherwise true.
*/
resetCamera(reframe) {
if (reframe == undefined) {
// If the selected animation is different from the default, there is a good chance the default explicit camera framing won't make sense
// and the model may not even be in view. So when this is the case, by default we reframe the camera.
reframe = this.selectedAnimation !== (this._options?.selectedAnimation ?? 0);
}
if (reframe) {
this._reframeCamera(true);
}
else {
this._reset(true, "camera");
}
}
/**
* Updates the camera pose.
* @param pose The new pose of the camera.
* @remarks Any unspecified values are left unchanged.
*/
updateCamera(pose) {
// undefined means default for _resetCameraFromBounds, so convert to NaN if needed.
this._reframeCameraFromBounds(true, this._loadedModels, pose.alpha ?? NaN, pose.beta ?? NaN, pose.radius ?? NaN, pose.targetX ?? NaN, pose.targetY ?? NaN, pose.targetZ ?? NaN);
}
/**
* Resets the viewer to its initial state based on the options passed in to the constructor.
* @param flags The flags that specify which parts of the viewer to reset. If no flags are provided, all parts will be reset.
* - "source": Reset the loaded model.
* - "environment": Reset environment related state.
* - "animation": Reset animation related state.
* - "camera": Reset camera related state.
* - "post-processing": Reset post-processing related state.
* - "material-variant": Reset material variant related state.
*/
reset(...flags) {
this._reset(true, ...flags);
}
_reset(interpolate, ...flags) {
if (flags.length === 0 || flags.includes("source")) {
observePromise(this._updateModel(this._options?.source));
}
if (flags.length === 0 || flags.includes("environment")) {
this._scene.clearColor = new Color4(...(this._options?.clearColor ?? DefaultViewerOptions.clearColor));
this.environmentConfig = {
intensity: this._options?.environmentConfig?.intensity ?? DefaultViewerOptions.environmentConfig.intensity,
blur: this._options?.environmentConfig?.blur ?? DefaultViewerOptions.environmentConfig.blur,
rotation: this._options?.environmentConfig?.rotation ?? DefaultViewerOptions.environmentConfig.rotation,
};
if (this._options?.environmentLighting === this._options?.environmentSkybox) {
observePromise(this._updateEnvironment(this._options?.environmentLighting, { lighting: true, skybox: true }));
}
else {
observePromise(this._updateEnvironment(this._options?.environmentLighting, { lighting: true }));
observePromise(this._updateEnvironment(this._options?.environmentSkybox, { skybox: true }));
}
}
if (flags.length === 0 || flags.includes("shadow")) {
this._shadowQuality = this._options?.shadowConfig?.quality ?? DefaultViewerOptions.shadowConfig.quality;
observePromise(this.updateShadows({ quality: this._shadowQuality }));
}
if (flags.length === 0 || flags.includes("animation")) {
this.animationSpeed = this._options?.animationSpeed ?? DefaultViewerOptions.animationSpeed;
this.selectedAnimation = this._options?.selectedAnimation ?? 0;
if (this._options?.animationAutoPlay) {
this.playAnimation();
}
else {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.pauseAnimation();
}
}
if (flags.length === 0 || flags.includes("camera")) {
// In the case of resetting the camera, we always want to restore default states, so convert NaN to undefined.
const alpha = Number(this._options?.cameraOrbit?.[0]);
const beta = Number(this._options?.cameraOrbit?.[1]);
const radius = Number(this._options?.cameraOrbit?.[2]);
const targetX = Number(this._options?.cameraTarget?.[0]);
const targetY = Number(this._options?.cameraTarget?.[1]);
const targetZ = Number(this._options?.cameraTarget?.[2]);
this._reframeCameraFromBounds(interpolate, this._loadedModels, isNaN(alpha) ? undefined : alpha, isNaN(beta) ? undefined : beta, isNaN(radius) ? undefined : radius, isNaN(targetX) ? undefined : targetX, isNaN(targetY) ? undefined : targetY, isNaN(targetZ) ? undefined : targetZ);
this.cameraAutoOrbit = {
enabled: this._options?.cameraAutoOrbit?.enabled ?? DefaultViewerOptions.cameraAutoOrbit.enabled,
speed: this._options?.cameraAutoOrbit?.speed ?? DefaultViewerOptions.cameraAutoOrbit.speed,
delay: this._options?.cameraAutoOrbit?.delay ?? DefaultViewerOptions.cameraAutoOrbit.delay,
};
}
if (flags.length === 0 || flags.includes("post-processing")) {
this.postProcessing = {
toneMapping: this._options?.postProcessing?.toneMapping ?? DefaultViewerOptions.postProcessing.toneMapping,
contrast: this._options?.postProcessing?.contrast ?? DefaultViewerOptions.postProcessing.contrast,
exposure: this._options?.postProcessing?.exposure ?? DefaultViewerOptions.postProcessing.exposure,
};
}
if (flags.length === 0 || flags.includes("material-variant")) {
this.selectedMaterialVariant = this._options?.selectedMaterialVariant ?? null;
}
}
/**
* Disposes of the resources held by the Viewer.
*/
dispose() {
this.selectedAnimation = -1;
this.animationProgress = 0;
this._loadEnvironmentAbortController?.abort(new AbortError("Thew viewer is being disposed."));
this._loadModelAbortController?.abort(new AbortError("Thew viewer is being disposed."));
this._camerasAsHotSpotsAbortController?.abort(new AbortError("Thew viewer is being disposed."));
this._shadowsAbortController?.abort(new AbortError("Thew viewer is being disposed."));
this._renderLoopController?.dispose();
this._activeModel?.dispose();
this._loadedModelsBacking.forEach((model) => model.dispose());
this._disposeShadows();
this._scene.dispose();
this.onEnvironmentChanged.clear();
this.onEnvironmentError.clear();
this.onEnvironmentConfigurationChanged.clear();
this.onPostProcessingChanged.clear();
this.onModelChanged.clear();
this.onModelError.clear();
this.onCameraAutoOrbitChanged.clear();
this.onSelectedAnimationChanged.clear();
this.onAnimationSpeedChanged.clear();
this.onIsAnimationPlayingChanged.clear();
this.onAnimationProgressChanged.clear();
this.onSelectedMaterialVariantChanged.clear();
this.onHotSpotsChanged.clear();
this.onCamerasAsHotSpotsChanged.clear();
this.onLoadingProgressChanged.clear();
this._imageProcessingConfigurationObserver.remove();
this._beforeRenderObserver.remove();
this._isDisposed = true;
}
/**
* Return world and canvas coordinates of an hot spot
* @param query mesh index and surface information to query the hot spot positions
* @param result Query a Hot Spot and does the conversion for Babylon Hot spot to a more generic HotSpotPositions, without Vector types
* @returns true if hotspot found
*/
getHotSpotToRef(query, result) {
return this._activeModel?.getHotSpotToRef(query, result) ?? false;
}
_getHotSpotToRef(assetContainer, query, result) {
const worldNormal = this._tempVectors[2];
const worldPos = this._tempVectors[1];
const screenPos = this._tempVectors[0];
if (query.type === "surface") {
const mesh = assetContainer?.meshes[query.meshIndex];
if (!mesh) {
return false;
}
if (!GetHotSpotToRef(mesh, query, worldPos, worldNormal)) {
return false;
}
}
else {
worldPos.copyFromFloats(query.position[0], query.position[1], query.position[2]);
worldNormal.copyFromFloats(query.normal[0], query.normal[1], query.normal[2]);
}
const viewportWidth = this._camera.viewport.width * this._engine.getRenderWidth() * this._engine.getHardwareScalingLevel();
const viewportHeight = this._camera.viewport.height * this._engine.getRenderHeight() * this._engine.getHardwareScalingLevel();
const scene = this._scene;
Vector3.ProjectToRef(worldPos, Matrix.IdentityReadOnly, scene.getTransformMatrix(), new Viewport(0, 0, viewportWidth, viewportHeight), screenPos);
result.screenPosition[0] = screenPos.x;
result.screenPosition[1] = screenPos.y;
result.worldPosition[0] = worldPos.x;
result.worldPosition[1] = worldPos.y;
result.worldPosition[2] = worldPos.z;
// visibility
const eyeToSurface = this._tempVectors[3];
eyeToSurface.copyFrom(this._camera.globalPosition);
eyeToSurface.subtractInPlace(worldPos);
eyeToSurface.normalize();
result.visibility = Vector3.Dot(eyeToSurface, worldNormal);
return true;
}
/**
* Get hotspot world and screen values from a named hotspot
* @param name slot of the hot spot
* @param result resulting world and screen positions
* @returns world position, world normal and screen space coordinates
*/
queryHotSpot(name, result) {
return this._queryHotSpot(name, result) != null;
}
/**
* Updates the camera to focus on a named hotspot.
* @param name The name of the hotspot to focus on.
* @returns true if the hotspot was found and the camera was updated, false otherwise.
*/
focusHotSpot(name) {
const result = new ViewerHotSpotResult();
const query = this._queryHotSpot(name, result);
if (query) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.pauseAnimation();
const cameraOrbit = query.cameraOrbit ?? [undefined, undefined, undefined];
this._camera.interpolateTo(cameraOrbit[0], cameraOrbit[1], cameraOrbit[2], new Vector3(result.worldPosition[0], result.worldPosition[1], result.worldPosition[2]));
return true;
}
return false;
}
_queryHotSpot(name, result) {
const hotSpot = this.hotSpots?.[name];
if (hotSpot) {
if (this.getHotSpotToRef(hotSpot, result)) {
return hotSpot;
}
}
return null;
}
async _addCameraHotSpot(camera, signal) {
if (camera !== this._camera) {
const hotSpot = await this._createHotSpotFromCamera(camera);
if (hotSpot && !signal?.aborted) {
this.hotSpots = {
...this.hotSpots,
[`camera-${camera.name}`]: hotSpot,
};
}
}
}
_removeCameraHotSpot(camera) {
delete this.hotSpots[`camera-${camera.name}`];
this.hotSpots = { ...this.hotSpots };
}
_toggleCamerasAsHotSpots() {
if (!this.camerasAsHotSpots) {
this._camerasAsHotSpotsAbortController?.abort();
this._camerasAsHotSpotsAbortController = null;
this._scene.cameras.forEach((camera) => this._removeCameraHotSpot(camera));
}
else {
const abortController = (this._camerasAsHotSpotsAbortController = new AbortController());
this._scene.cameras.forEach(async (camera) => await this._addCameraHotSpot(camera, abortController.signal));
}
}
/**
* Creates a world HotSpot from a camera.
* @param camera The camera to create a HotSpot from.
* @returns A HotSpot created from the camera.
*/
async _createHotSpotFromCamera(camera) {
if (camera instanceof ArcRotateCamera) {
const targetArray = camera.target.asArray();
return { type: "world", position: targetArray, normal: targetArray, cameraOrbit: [camera.alpha, camera.beta, camera.radius] };
}
if (this._activeModel) {
return await CreateHotSpotFromCamera(this._activeModel, camera);
}
return null;
}
get _shouldRender() {
// We should render if:
// 1. Auto suspend rendering is disabled.
// 2. The scene has been mutated.
// 3. The snapshot helper is not yet in a ready state.
// 4. The classic shadows are not yet in a ready state.
// 5. The environment shadows are not yet in a ready state.
// 6. At least one model should render (playing animations).
return (!this._autoSuspendRendering ||
this._sceneMutated ||
!this._snapshotHelper.isReady ||
this._shadowState.normal?.shouldRender ||
this._shadowState.high?.shouldRender ||
this._loadedModelsBacking.some((model) => model._shouldRender()));
}
_markSceneMutated() {
this._sceneMutated = true;
}
_suspendRendering() {
this._renderLoopController?.dispose();
this._suspendRenderCount++;
let disposed = false;
return {
dispose: () => {
if (!disposed) {
disposed = true;
this._suspendRenderCount--;
if (this._suspendRenderCount === 0) {
this._beginRendering();
}
}
},
};
}
_beginRendering() {
if (!this._renderLoopController) {
let renderedReadyFrame = false;
const onRenderingResumed = () => {
this._log("Viewer Resumed Rendering");
// Resume rendering with the hardware scaling level from prior to suspending.
this._engine.setHardwareScalingLevel(this._lastHardwareScalingLevel);
this._engine.performanceMonitor.enable();
this._snapshotHelper.enableSnapshotRendering();
this._startSceneOptimizer();
};
const onRenderingSuspended = () => {
this._log("Viewer Suspended Rendering");
this._renderedLastFrame = false;
renderedReadyFrame = false;
// Take note of the current hardware scaling level for when rendering is resumed.
this._lastHardwareScalingLevel = this._engine.getHardwareScalingLevel();
this._stopSceneOptimizer();
this._snapshotHelper.disableSnapshotRendering();
// We want a high quality render right before suspending, so set the hardware scaling level back to the default,
// disable the performance monitor (so the SceneOptimizer doesn't take into account this potentially slower frame),
// and then render the scene once.
this._engine.performanceMonitor.disable();
this._engine.setHardwareScalingLevel(this._defaultHardwareScalingLevel);
this._engine.beginFrame();
this._scene.render();
this._engine.endFrame();
};
const render = () => {
// First check if we have indicators that we should render.
let shouldRender = this._shouldRender;
// If we don't have indicators that we should render (e.g. nothing has changed since the last frame),
// we still need to ensure that we render at least one frame after any mutations. Scene.isReady does
// a bunch of the same work that happens when we actually render a frame, so we don't want to check
// this unless we know we are in a state where there were mutations and now we are waiting for a frame
// to render after the scene is ready.
if (!shouldRender && this._renderedLastFrame && !renderedReadyFrame) {
renderedReadyFrame = this._scene.isReady(true);
shouldRender = true;
}
if (shouldRender) {
if (!this._renderedLastFrame) {
if (this._renderedLastFrame !== null) {
onRenderingResumed();
}
this._renderedLastFrame = true;
}
this._sceneMutated = false;
this._scene.render();
// Update the camera panning sensitivity related properties based on the camera's distance from the target.
this._camera.panningSensibility = 5000 / this._camera.radius;
this._camera.speed = this._camera.radius * 0.2;
if (this.isAnimationPlaying) {
this.onAnimationProgressChanged.notifyObservers();
this._autoRotationBehavior.resetLastInteractionTime();
}
}
else {
this._camera.update();
if (this._renderedLastFrame) {
onRenderingSuspended();
}
}
};
this._engine.runRenderLoop(render);
let disposed = false;
this._renderLoopController = {
dispose: () => {
if (!disposed) {
disposed = true;
this._engine.stopRenderLoop(render);
this._renderLoopController = null;
if (this._renderedLastFrame) {
onRenderingSuspended();
}
}
},
};
}
}
_reframeCamera(interpolate = false, models = this._loadedModelsBacking) {
this._reframeCameraFromBounds(interpolate, models);
}
_getWorldBounds(models) {
return computeModelsBoundingInfos(models);
}
_getCameraConfig(models) {
let radius = 1;
let target = Vector3.Zero();
const worldBounds = this._getWorldBounds(models);
if (worldBounds) {
// get bounds and prepare framing/camera radius from its values
this._camera.lowerRadiusLimit = null;
radius = Vector3.FromArray(worldBounds.size).length() * 1.1;
target = Vector3.FromArray(worldBounds.center);
if (!isFinite(radius)) {
radius = 1;
target.copyFromFloats(0, 0, 0);
}
}
const lowerRadiusLimit = radius * 0.001;
const upperRadiusLimit = radius * 5;
const minZ = radius * 0.001;
const maxZ = radius * 1000;
return {
radius,
target,
lowerRadiusLimit,
upperRadiusLimit,
minZ,
maxZ,
};
}
// For rotation/radius/target, undefined means default framing, NaN means keep current value.
_reframeCameraFromBounds(interpolate, models, alpha, beta, radius, targetX, targetY, targetZ) {
let goalRadius = 1;
const goalTarget = Vector3.Zero();
let goalAlpha = Math.PI / 2;
let goalBeta = Math.PI / 2.4;
const { radius: sceneRadius, target: sceneTarget, lowerRadiusLimit, upperRadiusLimit, minZ, maxZ } = this._getCameraConfig(models);
this._camera.lowerRadiusLimit = lowerRadiusLimit;
this._camera.upperRadiusLimit = upperRadiusLimit;
this._camera.minZ = minZ;
this._camera.maxZ = maxZ;
goalAlpha = alpha ?? goalAlpha;
goalBeta = beta ?? goalBeta;
goalRadius = radius ?? sceneRadius;
goalTarget.x = targetX ?? sceneTarget.x;
goalTarget.y = targetY ?? sceneTarget.y;
goalTarget.z = targetZ ?? sceneTarget.z;
if (interpolate) {
this._camera.interpolateTo(goalAlpha, goalBeta, goalRadius, goalTarget, undefined, 0.1);
}
else {
this._camera.stopInterpolation();
if (!isNaN(goalAlpha)) {
this._camera.alpha = goalAlpha;
}
if (!isNaN(goalBeta)) {
this._camera.beta = goalBeta;
}
if (!isNaN(goalRadius)) {
this._camera.radius = goalRadius;
}
this._camera.setTarget(new Vector3(isNaN(goalTarget.x) ? this._camera.target.x : goalTarget.x, isNaN(goalTarget.y) ? this._camera.target.y : goalTarget.y, isNaN(goalTarget.z) ? this._camera.target.z : goalTarget.z), undefined, undefined, true);
}
this._camera.wheelDeltaPercentage = 0.01;
this._camera.useNaturalPinchZoom = true;
updateSkybox(this._skybox, this._camera);
}
_updateLight() {
let shouldHaveDefaultLight;
if (this._loadedModels.length === 0) {
shouldHaveDefaultLight = false;
}
else {
const hasModelProvidedLights = this._loadedModels.some((model) => model.assetContainer.lights.length > 0);
const hasImageBasedLighting = !!this._reflectionTexture;
const hasMaterials = this._loadedModels.some((model) => model.assetContainer.materials.length > 0);
const hasNonPBRMaterials = this._loadedModels.some((model) => model.assetContainer.materials.some((material) => !(material instanceof PBRMaterial)));
if (hasModelProvidedLights) {
shouldHaveDefaultLight = false;
}
else {
shouldHaveDefaultLight = !hasImageBasedLighting || !hasMaterials || hasNonPBRMaterials;
}
}
if (shouldHaveDefaultLight) {
if (!this._light) {
this._light = new HemisphericLight("defaultLight", Vector3.Up(), this._scene);
}
}
else {
this._light?.dispose();
this._light = null;
}
}
_applyAnimationSpeed() {
this._activeModel?.assetContainer.animationGroups.forEach((group) => (group.speedRatio = this._animationSpeed));
}
async _pick(screenX, screenY) {
await import('@babylonjs/core/Culling/ray.js');
if (this._loadedModels.length > 0) {
const meshes = this._loadedModelsBacking.flatMap((model) => model.assetContainer.meshes);
// Refresh bounding info to ensure morph target and skeletal animations are taken into account.
meshes.forEach((mesh) => {
let cache = this._meshDataCache.get(mesh);
if (!cache) {
cache = {};
this._meshDataCache.set(mesh, cache);
}
mesh.refreshBoundingInfo({ applyMorph: true, applySkeleton: true, cache });
});
const pickingInfo = this._scene.pick(screenX, screenY, (mesh) => meshes.includes(mesh));
if (pickingInfo.hit) {
return pickingInfo;
}
}
return null;
}
_startSceneOptimizer(reset = false) {
this._stopSceneOptimizer();
if (reset) {
this._engine.setHardwareScalingLevel(this._defaultHardwareScalingLevel);
}
const sceneOptimizerOptions = new SceneOptimizerOptions(60, 1000);
const hardwareScalingOptimization = new HardwareScalingOptimization(undefined, 1);
sceneOptimizerOptions.addOptimization(hardwareScalingOptimization);
this._sceneOptimizer = new SceneOptimizer(this._scene, sceneOptimizerOptions);
this._sceneOptimizer.start();
}
_stopSceneOptimizer() {
this._sceneOptimizer?.dispose();
this._sceneOptimizer = null;
}
_log(message) {
if (this.showDebugLogs) {
Logger.Log(message);
}
}
/**
* Check for disposed or aborted state (basically everything that can interrupt an async operation).
* @param abortSignals A set of optional AbortSignals to also check.
*/
_throwIfDisposedOrAborted(...abortSignals) {
if (this._isDisposed) {
throw new Error("Viewer is disposed.");
}
throwIfAborted(...abortSignals);
}
}
(() => {
registerBuiltInLoaders();
})();
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$3=globalThis,e$5=t$3.ShadowRoot&&(void 0===t$3.ShadyCSS||t$3.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s$3=Symbol(),o$6=new WeakMap;let n$5 = class n{constructor(t,e,o){if(this._$cssResult$=true,o!==s$3)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e;}get styleSheet(){let t=this.o;const s=this.t;if(e$5&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=o$6.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&o$6.set(s,t));}return t}toString(){return this.cssText}};const r$5=t=>new n$5("string"==typeof t?t:t+"",void 0,s$3),i$4=(t,...e)=>{const o=1===t.length?t[0]:e.reduce(((e,s,o)=>e+(t=>{if(true===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+t[o+1]),t[0]);return new n$5(o,t,s$3)},S$1=(s,o)=>{if(e$5)s.adoptedStyleSheets=o.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet));else for(const e of o){const o=document.createElement("style"),n=t$3.litNonce;void 0!==n&&o.setAttribute("nonce",n),o.textContent=e.cssText,s.appendChild(o);}},c$3=e$5?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return r$5(e)})(t):t;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/const{is:i$3,defineProperty:e$4,getOwnPropertyDescriptor:h$2,getOwnPropertyNames:r$4,getOwnPropertySymbols:o$5,getPrototypeOf:n$4}=Object,a$1=globalThis,c$2=a$1.trustedTypes,l$1=c$2?c$2.emptyScript:"",p$1=a$1.reactiveElementPolyfillSupport,d$1=(t,s)=>t,u$1={toAttribute(t,s){switch(s){case Boolean:t=t?l$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t);}return t},fromAttribute(t,s){let i=t;switch(s){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t);}catch(t){i=null;}}return i}},f$3=(t,s)=>!i$3(t,s),b={attribute:true,type:String,converter:u$1,reflect:false,useDefault:false,hasChanged:f$3};Symbol.metadata??=Symbol("metadata"),a$1.litPropertyMetadata??=new WeakMap;let y$1 = class y extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t);}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,s=b){if(s.state&&(s.attribute=false),this._$Ei(),this.prototype.hasOwnProperty(t)&&((s=Object.create(s)).wrapped=true),this.elementProperties.set(t,s),!s.noAccessor){const i=Symbol(),h=this.getPropertyDescriptor(t,i,s);void 0!==h&&e$4(this.prototype,t,h);}}static getPropertyDescriptor(t,s,i){const{get:e,set:r}=h$2(this.prototype,t)??{get(){return this[s]},set(t){this[s]=t;}};return {get:e,set(s){const h=e?.call(this);r?.call(this,s),this.requestUpdate(t,h,i);},configurable:true,enumerable:true}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(d$1("elementProperties")))return;const t=n$4(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties);}static finalize(){if(this.hasOwnProperty(d$1("finalized")))return;if(this.finalized=true,this._$Ei(),this.hasOwnProperty(d$1("properties"))){const t=this.properties,s=[...r$4(t),...o$5(t)];for(const i of s)this.createProperty(i,t[i]);}const t=this[Symbol.metadata];if(null!==t){const s=litPropertyMetadata.get(t);if(void 0!==s)for(const[t,i]of s)this.elementProperties.set(t,i);}this._$Eh=new Map;for(const[t,s]of this.elementProperties){const i=this._$Eu(t,s);void 0!==i&&this._$Eh.set(i,t);}this.elementStyles=this.finalizeStyles(this.styles);}static finalizeStyles(s){const i=[];if(Array.isArray(s)){const e=new Set(s.flat(1/0).reverse());for(const s of e)i.unshift(c$3(s));}else void 0!==s&&i.push(c$3(s));return i}static _$Eu(t,s){const i=s.attribute;return false===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=false,this.hasUpdated=false,this._$Em=null,this._$Ev();}_$Ev(){this._$ES=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((t=>t(this)));}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.();}removeController(t){this._$EO?.delete(t);}_$E_(){const t=new Map,s=this.constructor.elementProperties;for(const i of s.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t);}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return S$1(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(true),this._$EO?.forEach((t=>t.hostConnected?.()));}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach((t=>t.hostDisconnected?.()));}attributeChangedCallback(t,s,i){this._$AK(t,i);}_$ET(t,s){const i=this.constructor.elementProperties.get(t),e=this.constructor._$Eu(t,i);if(void 0!==e&&true===i.reflect){const h=(void 0!==i.converter?.toAttribute?i.converter:u$1).toAttribute(s,i.type);this._$Em=t,null==h?this.removeAttribute(e):this.setAttribute(e,h),this._$Em=null;}}_$AK(t,s){const i=this.constructor,e=i._$Eh.get(t);if(void 0!==e&&this._$Em!==e){const t=i.getPropertyOptions(e),h="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:u$1;this._$Em=e,this[e]=h.fromAttribute(s,t.type)??this._$Ej?.get(e)??null,this._$Em=null;}}requestUpdate(t,s,i){if(void 0!==t){const e=this.constructor,h=this[t];if(i??=e.getPropertyOptions(t),!((i.hasChanged??f$3)(h,s)||i.useDefault&&i.reflect&&h===this._$Ej?.get(t)&&!this.hasAttribute(e._$Eu(t,i))))return;this.C(t,s,i);} false===this.isUpdatePending&&(this._$ES=this._$EP());}C(t,s,{useDefault:i,reflect:e,wrapped:h},r){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??s??this[t]),true!==h||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||i||(s=void 0),this._$AL.set(t,s)),true===e&&this._$Em!==t&&(this._$Eq??=new Set).add(t));}async _$EP(){this.isUpdatePending=true;try{await this._$ES;}catch(t){Promise.reject(t);}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,s]of this._$Ep)this[t]=s;this._$Ep=void 0;}const t=this.constructor.elementProperties;if(t.size>0)for(const[s,i]of t){const{wrapped:t}=i,e=this[s];true!==t||this._$AL.has(s)||void 0===e||this.C(s,void 0,i,e);}}let t=false;const s=this._$AL;try{t=this.shouldUpdate(s),t?(this.willUpdate(s),this._$EO?.forEach((t=>t.hostUpdate?.())),this.update(s)):this._$EM();}catch(s){throw t=false,this._$EM(),s}t&&this._$AE(s);}willUpdate(t){}_$AE(t){this._$EO?.forEach((t=>t.hostUpdated?.())),this.hasUpdated||(this.hasUpdated=true,this.firstUpdated(t)),this.updated(t);}_$EM(){this._$AL=new Map,this.isUpdatePending=false;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return true}update(t){this._$Eq&&=this._$Eq.forEach((t=>this._$ET(t,this[t]))),this._$EM();}updated(t){}firstUpdated(t){}};y$1.elementStyles=[],y$1.shadowRootOptions={mode:"open"},y$1[d$1("elementProperties")]=new Map,y$1[d$1("finalized")]=new Map,p$1?.({ReactiveElement:y$1}),(a$1.reactiveElementVersions??=[]).push("2.1.0");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$2=globalThis,i$2=t$2.trustedTypes,s$2=i$2?i$2.createPolicy("lit-html",{createHTML:t=>t}):void 0,e$3="$lit$",h$1=`lit$${Math.random().toFixed(9).slice(2)}$`,o$4="?"+h$1,n$3=`<${o$4}>`,r$3=document,l=()=>r$3.createComment(""),c$1=t=>null===t||"object"!=typeof t&&"function"!=typeof t,a=Array.isArray,u=t=>a(t)||"function"==typeof t?.[Symbol.iterator],d="[ \t\n\f\r]",f$2=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,_=/>/g,m=RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),p=/'/g,g=/"/g,$=/^(?:script|style|textarea|title)$/i,y=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),x=y(1),T=Symbol.for("lit-noChange"),E=Symbol.for("lit-nothing"),A=new WeakMap,C=r$3.createTreeWalker(r$3,129);function P(t,i){if(!a(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==s$2?s$2.createHTML(i):i}const V=(t,i)=>{const s=t.length-1,o=[];let r,l=2===i?"<svg>":3===i?"<math>":"",c=f$2;for(let i=0;i<s;i++){const s=t[i];let a,u,d=-1,y=0;for(;y<s.length&&(c.lastIndex=y,u=c.exec(s),null!==u);)y=c.lastIndex,c===f$2?"!--"===u[1]?c=v:void 0!==u[1]?c=_:void 0!==u[2]?($.test(u[2])&&(r=RegExp("</"+u[2],"g")),c=m):void 0!==u[3]&&(c=m):c===m?">"===u[0]?(c=r??f$2,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?m:'"'===u[3]?g:p):c===g||c===p?c=m:c===v||c===_?c=f$2:(c=m,r=void 0);const x=c===m&&t[i+1].startsWith("/>")?" ":"";l+=c===f$2?s+n$3:d>=0?(o.push(a),s.slice(0,d)+e$3+s.slice(d)+h$1+x):s+h$1+(-2===d?i:x);}return [P(t,l+(t[s]||"<?>")+(2===i?"</svg>":3===i?"</math>":"")),o]};class N{constructor({strings:t,_$litType$:s},n){let r;this.parts=[];let c=0,a=0;const u=t.length-1,d=this.parts,[f,v]=V(t,s);if(this.el=N.createElement(f,n),C.currentNode=this.el.content,2===s||3===s){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes);}for(;null!==(r=C.nextNode())&&d.length<u;){if(1===r.nodeType){if(r.hasAttributes())for(const t of r.getAttributeNames())if(t.endsWith(e$3)){const i=v[a++],s=r.getAttribute(t).split(h$1),e=/([.?@])?(.*)/.exec(i);d.push({type:1,index:c,name:e[2],strings:s,ctor:"."===e[1]?H:"?"===e[1]?I:"@"===e[1]?L:k}),r.removeAttribute(t);}else t.startsWith(h$1)&&(d.push({type:6,index:c}),r.removeAttribute(t));if($.test(r.tagName)){const t=r.textContent.split(h$1),s=t.length-1;if(s>0){r.textContent=i$2?i$2.emptyScript:"";for(let i=0;i<s;i++)r.append(t[i],l()),C.nextNode(),d.push({type:2,index:++c});r.append(t[s],l());}}}else if(8===r.nodeType)if(r.data===o$4)d.push({type:2,index:c});else {let t=-1;for(;-1!==(t=r.data.indexOf(h$1,t+1));)d.push({type:7,index:c}),t+=h$1.length-1;}c++;}}static createElement(t,i){const s=r$3.createElement("template");return s.innerHTML=t,s}}function S(t,i,s=t,e){if(i===T)return i;let h=void 0!==e?s._$Co?.[e]:s._$Cl;const o=c$1(i)?void 0:i._$litDirective$;return h?.constructor!==o&&(h?._$AO?.(false),void 0===o?h=void 0:(h=new o(t),h._$AT(t,s,e)),void 0!==e?(s._$Co??=[])[e]=h:s._$Cl=h),void 0!==h&&(i=S(t,h._$AS(t,i.values),h,e)),i}class M{constructor(t,i){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=i;}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){const{el:{content:i},parts:s}=this._$AD,e=(t?.creationScope??r$3).importNode(i,true);C.currentNode=e;let h=C.nextNode(),o=0,n=0,l=s[0];for(;void 0!==l;){if(o===l.index){let i;2===l.type?i=new R(h,h.nextSibling,this,t):1===l.type?i=new l.ctor(h,l.name,l.strings,this,t):6===l.type&&(i=new z(h,this,t)),this._$AV.push(i),l=s[++n];}o!==l?.index&&(h=C.nextNode(),o++);}return C.currentNode=r$3,e}p(t){let i=0;for(const s of this._$AV) void 0!==s&&(void 0!==s.strings?(s._$AI(t,s,i),i+=s.strings.length-2):s._$AI(t[i])),i++;}}class R{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,i,s,e){this.type=2,this._$AH=E,this._$AN=void 0,this._$AA=t,this._$AB=i,this._$AM=s,this.options=e,this._$Cv=e?.isConnected??true;}get parentNode(){let t=this._$AA.parentNode;const i=this._$AM;return void 0!==i&&11===t?.nodeType&&(t=i.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,i=this){t=S(this,t,i),c$1(t)?t===E||null==t||""===t?(this._$AH!==E&&this._$AR(),this._$AH=E):t!==this._$AH&&t!==T&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):u(t)?this.k(t):this._(t);}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t));}_(t){this._$AH!==E&&c$1(this._$AH)?this._$AA.nextSibling.data=t:this.T(r$3.createTextNode(t)),this._$AH=t;}$(t){const{values:i,_$litType$:s}=t,e="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=N.createElement(P(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===e)this._$AH.p(i);else {const t=new M(e,this),s=t.u(this.options);t.p(i),this.T(s),this._$AH=t;}}_$AC(t){let i=A.get(t.strings);return void 0===i&&A.set(t.strings,i=new N(t)),i}k(t){a(this._$AH)||(this._$AH=[],this._$AR());const i=this._$AH;let s,e=0;for(const h of t)e===i.length?i.push(s=new R(this.O(l()),this.O(l()),this,this.options)):s=i[e],s._$AI(h),e++;e<i.length&&(this._$AR(s&&s._$AB.nextSibling,e),i.length=e);}_$AR(t=this._$AA.nextSibling,i){for(this._$AP?.(false,true,i);t&&t!==this._$AB;){const i=t.nextSibling;t.remove(),t=i;}}setConnected(t){ void 0===this._$AM&&(this._$Cv=t,this._$AP?.(t));}}class k{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,i,s,e,h){this.type=1,this._$AH=E,this._$AN=void 0,this.element=t,this.name=i,this._$AM=e,this.options=h,s.length>2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=E;}_$AI(t,i=this,s,e){const h=this.strings;let o=false;if(void 0===h)t=S(this,t,i,0),o=!c$1(t)||t!==this._$AH&&t!==T,o&&(this._$AH=t);else {const e=t;let n,r;for(t=h[0],n=0;n<h.length-1;n++)r=S(this,e[s+n],i,n),r===T&&(r=this._$AH[n]),o||=!c$1(r)||r!==this._$AH[n],r===E?t=E:t!==E&&(t+=(r??"")+h[n+1]),this._$AH[n]=r;}o&&!e&&this.j(t);}j(t){t===E?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"");}}class H extends k{constructor(){super(...arguments),this.type=3;}j(t){this.element[this.name]=t===E?void 0:t;}}class I extends k{constructor(){super(...arguments),this.type=4;}j(t){this.element.toggleAttribute(this.name,!!t&&t!==E);}}class L extends k{constructor(t,i,s,e,h){super(t,i,s,e,h),this.type=5;}_$AI(t,i=this){if((t=S(this,t,i,0)??E)===T)return;const s=this._$AH,e=t===E&&s!==E||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,h=t!==E&&(s===E||e);e&&this.element.removeEventListener(this.name,this,s),h&&this.element.addEventListener(this.name,this,t),this._$AH=t;}handleEvent(t){"function"==typeof this._$AH?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t);}}class z{constructor(t,i,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=i,this.options=s;}get _$AU(){return this._$AM._$AU}_$AI(t){S(this,t);}}const j=t$2.litHtmlPolyfillSupport;j?.(N,R),(t$2.litHtmlVersions??=[]).push("3.3.0");const B=(t,i,s)=>{const e=s?.renderBefore??i;let h=e._$litPart$;if(void 0===h){const t=s?.renderBefore??null;e._$litPart$=h=new R(i.insertBefore(l(),t),t,void 0,s??{});}return h._$AI(t),h};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/const s$1=globalThis;let i$1 = class i extends y$1{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0;}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const r=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=B(r,this.renderRoot,this.renderOptions);}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(true);}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(false);}render(){return T}};i$1._$litElement$=true,i$1["finalized"]=true,s$1.litElementHydrateSupport?.({LitElement:i$1});const o$3=s$1.litElementPolyfillSupport;o$3?.({LitElement:i$1});(s$1.litElementVersions??=[]).push("4.2.0");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$1=t=>(e,o)=>{ void 0!==o?o.addInitializer((()=>{customElements.define(t,e);})):customElements.define(t,e);};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/const o$2={attribute:true,type:String,converter:u$1,reflect:false,hasChanged:f$3},r$2=(t=o$2,e,r)=>{const{kind:n,metadata:i}=r;let s=globalThis.litPropertyMetadata.get(i);if(void 0===s&&globalThis.litPropertyMetadata.set(i,s=new Map),"setter"===n&&((t=Object.create(t)).wrapped=true),s.set(r.name,t),"accessor"===n){const{name:o}=r;return {set(r){const n=e.get.call(this);e.set.call(this,r),this.requestUpdate(o,n,t);},init(e){return void 0!==e&&this.C(o,void 0,t,e),e}}}if("setter"===n){const{name:o}=r;return function(r){const n=this[o];e.call(this,r),this.requestUpdate(o,n,t);}}throw Error("Unsupported decorator location: "+n)};function n$2(t){return (e,o)=>"object"==typeof o?r$2(t,e,o):((t,e,o)=>{const r=e.hasOwnProperty(o);return e.constructor.createProperty(o,t),r?Object.getOwnPropertyDescriptor(e,o):void 0})(t,e,o)}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/function r$1(r){return n$2({...r,state:true,attribute:false})}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const e$2=(e,t,c)=>(c.configurable=true,c.enumerable=true,Reflect.decorate&&"object"!=typeof t&&Object.defineProperty(e,t,c),c);
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/function e$1(e,r){return (n,s,i)=>{const o=t=>t.renderRoot?.querySelector(e)??null;return e$2(n,s,{get(){return o(this)}})}}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/const f$1=o=>void 0===o.strings;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t={CHILD:2},e=t=>(...e)=>({_$litDirective$:t,values:e});class i{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i;}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/const s=(i,t)=>{const e=i._$AN;if(void 0===e)return false;for(const i of e)i._$AO?.(t,false),s(i,t);return true},o$1=i=>{let t,e;do{if(void 0===(t=i._$AM))break;e=t._$AN,e.delete(i),i=t;}while(0===e?.size)},r=i=>{for(let t;t=i._$AM;i=t){let e=t._$AN;if(void 0===e)t._$AN=e=new Set;else if(e.has(i))break;e.add(i),c(t);}};function h(i){ void 0!==this._$AN?(o$1(this),this._$AM=i,r(this)):this._$AM=i;}function n$1(i,t=false,e=0){const r=this._$AH,h=this._$AN;if(void 0!==h&&0!==h.size)if(t)if(Array.isArray(r))for(let i=e;i<r.length;i++)s(r[i],false),o$1(r[i]);else null!=r&&(s(r,false),o$1(r));else s(this,i);}const c=i=>{i.type==t.CHILD&&(i._$AP??=n$1,i._$AQ??=h);};class f extends i{constructor(){super(...arguments),this._$AN=void 0;}_$AT(i,t,e){super._$AT(i,t,e),r(this),this.isConnected=i._$AU;}_$AO(i,t=true){i!==this.isConnected&&(this.isConnected=i,i?this.reconnected?.():this.disconnected?.()),t&&(s(this,i),o$1(this));}setValue(t){if(f$1(this._$Ct))this._$Ct._$AI(t,this);else {const i=[...this._$Ct._$AH];i[this._$Ci]=t,this._$Ct._$AI(i,this,0);}}disconnected(){}reconnected(){}}
const o=new WeakMap,n=e(class extends f{render(i){return E}update(i,[s]){const e=s!==this.G;return e&&void 0!==this.G&&this.rt(void 0),(e||this.lt!==this.ct)&&(this.G=s,this.ht=i.options?.host,this.rt(this.ct=i.element)),E}rt(t){if(this.isConnected||(t=void 0),"function"==typeof this.G){const i=this.ht??globalThis;let s=o.get(i);void 0===s&&(s=new WeakMap,o.set(i,s)),void 0!==s.get(this.G)&&this.G.call(this.ht,void 0),s.set(this.G,t),void 0!==t&&this.G.call(this.ht,t);}else this.G.value=t;}get lt(){return "function"==typeof this.G?o.get(this.ht??globalThis)?.get(this.G):this.G?.value}disconnected(){this.lt===this.ct&&this.rt(void 0);}reconnected(){this.rt(this.ct);}});
const DefaultCanvasViewerOptions = {
antialias: true,
adaptToDeviceRatio: true,
enableAllFeatures: true,
setMaximumLimits: true,
};
/**
* Chooses a default engine for the current browser environment.
* @returns The default engine to use.
*/
function GetDefaultEngine() {
// TODO: There are some difficult to repro timing issues with WebGPU + snapshot rendering.
// We need to do deeper diagnosis to understand the issues and make this more reliable.
// For now, we will default to WebGL.
// // First check for WebGPU support.
// if ("gpu" in navigator) {
// // For now, only use WebGPU with chromium-based browsers.
// // WebGPU can be enabled in other browsers once they are fully functional and the performance is at least as good as WebGL.
// if ("chrome" in window) {
// return "WebGPU";
// }
// }
return "WebGL";
}
/**
* @internal
*/
async function CreateViewerForCanvas(canvas, options = {}) {
const detailsDeferred = new Deferred();
options = new Proxy(options, {
get(target, prop) {
switch (prop) {
case "antialias":
return target.antialias ?? DefaultCanvasViewerOptions.antialias;
case "adaptToDeviceRatio":
return target.adaptToDeviceRatio ?? DefaultCanvasViewerOptions.adaptToDeviceRatio;
case "enableAllFeatures":
return target.enableAllFeatures ?? DefaultCanvasViewerOptions.enableAllFeatures;
case "setMaximumLimits":
return target.setMaximumLimits ?? DefaultCanvasViewerOptions.setMaximumLimits;
case "onInitialized":
return (details) => {
target.onInitialized?.(details);
detailsDeferred.resolve(details);
};
default:
return target[prop];
}
},
});
const disposeActions = [];
// Create an engine instance.
let engine;
switch (options.engine ?? GetDefaultEngine()) {
case "WebGL": {
const { Engine } = await import('@babylonjs/core/Engines/engine.js');
engine = new Engine(canvas, undefined, options);
break;
}
case "WebGPU": {
const { WebGPUEngine } = await import('@babylonjs/core/Engines/webgpuEngine.js');
const webGPUEngine = new WebGPUEngine(canvas, options);
await webGPUEngine.initAsync();
engine = webGPUEngine;
break;
}
}
if (options.onFaulted) {
const onFaulted = options.onFaulted;
const contextLostObserver = engine.onContextLostObservable.addOnce(() => {
onFaulted(new Error("The engine context was lost."));
});
disposeActions.push(() => contextLostObserver.remove());
}
// Instantiate the Viewer with the engine and options.
const viewerClass = options?.viewerClass ?? Viewer;
const viewer = new viewerClass(engine, options);
{
const details = await detailsDeferred.promise;
// If the canvas is resized, note that the engine needs a resize, but don't resize it here as it will result in flickering.
let needsResize = false;
const resizeObserver = new ResizeObserver(() => {
needsResize = true;
details.markSceneMutated();
});
resizeObserver.observe(canvas);
disposeActions.push(() => resizeObserver.disconnect());
// Resize if needed right before rendering the Viewer scene to avoid any flickering.
const beforeRenderObserver = details.scene.onBeforeRenderObservable.add(() => {
if (needsResize) {
engine.resize();
needsResize = false;
}
});
disposeActions.push(() => beforeRenderObserver.remove());
// If the canvas is not visible, suspend rendering.
let offscreenRenderingSuspension = null;
const intersectionObserver = new IntersectionObserver((entries) => {
if (entries.length > 0) {
if (entries[entries.length - 1].isIntersecting) {
offscreenRenderingSuspension?.dispose();
offscreenRenderingSuspension = null;
}
else {
offscreenRenderingSuspension = details.suspendRendering();
}
}
});
intersectionObserver.observe(canvas);
disposeActions.push(() => intersectionObserver.disconnect());
}
disposeActions.push(viewer.dispose.bind(viewer));
disposeActions.push(() => engine.dispose());
// Override the Viewer's dispose method to add in additional cleanup.
viewer.dispose = () => disposeActions.forEach((dispose) => dispose());
return viewer;
}
// Icon SVG is pulled from https://iconcloud.design
const playFilledIcon = "M5 5.27368C5 3.56682 6.82609 2.48151 8.32538 3.2973L20.687 10.0235C22.2531 10.8756 22.2531 13.124 20.687 13.9762L8.32538 20.7024C6.82609 21.5181 5 20.4328 5 18.726V5.27368Z";
const pauseFilledIcon = "M5.74609 3C4.7796 3 3.99609 3.7835 3.99609 4.75V19.25C3.99609 20.2165 4.7796 21 5.74609 21H9.24609C10.2126 21 10.9961 20.2165 10.9961 19.25V4.75C10.9961 3.7835 10.2126 3 9.24609 3H5.74609ZM14.7461 3C13.7796 3 12.9961 3.7835 12.9961 4.75V19.25C12.9961 20.2165 13.7796 21 14.7461 21H18.2461C19.2126 21 19.9961 20.2165 19.9961 19.25V4.75C19.9961 3.7835 19.2126 3 18.2461 3H14.7461Z";
const arrowResetFilledIcon = "M7.20711 2.54289C7.59763 2.93342 7.59763 3.56658 7.20711 3.95711L5.41421 5.75H13.25C17.6683 5.75 21.25 9.33172 21.25 13.75C21.25 18.1683 17.6683 21.75 13.25 21.75C8.83172 21.75 5.25 18.1683 5.25 13.75C5.25 13.1977 5.69772 12.75 6.25 12.75C6.80228 12.75 7.25 13.1977 7.25 13.75C7.25 17.0637 9.93629 19.75 13.25 19.75C16.5637 19.75 19.25 17.0637 19.25 13.75C19.25 10.4363 16.5637 7.75 13.25 7.75H5.41421L7.20711 9.54289C7.59763 9.93342 7.59763 10.5666 7.20711 10.9571C6.81658 11.3476 6.18342 11.3476 5.79289 10.9571L2.29289 7.45711C1.90237 7.06658 1.90237 6.43342 2.29289 6.04289L5.79289 2.54289C6.18342 2.15237 6.81658 2.15237 7.20711 2.54289Z";
const targetFilledIcon = "M12 14C13.1046 14 14 13.1046 14 12C14 10.8954 13.1046 10 12 10C10.8954 10 10 10.8954 10 12C10 13.1046 10.8954 14 12 14ZM6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12ZM12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4Z";
const arrowClockwiseFilledIcon = "M5 12C5 8.13401 8.13401 5 12 5C13.32 5 14.5542 5.36484 15.608 6H15C14.4477 6 14 6.44772 14 7C14 7.55228 14.4477 8 15 8H18C18.5523 8 19 7.55228 19 7C19 6 19 5 19 4C19 3.44772 18.5523 3 18 3C17.4477 3 17 3.44772 17 4V4.51575C15.5702 3.5588 13.85 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 11.6199 20.9764 11.2448 20.9304 10.8763C20.8621 10.3282 20.3624 9.93935 19.8144 10.0077C19.2663 10.076 18.8775 10.5757 18.9458 11.1237C18.9815 11.4104 19 11.7028 19 12C19 15.866 15.866 19 12 19C8.13401 19 5 15.866 5 12Z";
const allowedAnimationSpeeds = [0.5, 1, 1.5, 2];
// Converts any standard html color string to a Color4 object.
function parseColor(color) {
if (!color) {
return null;
}
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 1;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Unable to get 2d context for parseColor");
}
context.clearRect(0, 0, 1, 1);
context.fillStyle = color;
context.fillRect(0, 0, 1, 1);
const data = context.getImageData(0, 0, 1, 1).data;
return new Color4(data[0] / 255, data[1] / 255, data[2] / 255, data[3] / 255);
}
function coerceEngineAttribute(value) {
if (value === "WebGL" || value === "WebGPU") {
return value;
}
return undefined;
}
function coerceNumericAttribute(value) {
return value == null ? null : Number(value);
}
function coerceCameraOrbitOrTarget(value) {
if (!value) {
return null;
}
const array = value.trim().split(/\s+/);
if (array.length !== 3) {
throw new Error(`Camera orbit and target should be defined as three space separated numbers, but was specified as "${value}".`);
}
return array.map((value) => Number(value));
}
function coerceToneMapping(value) {
if (!value || !IsToneMapping(value)) {
return null;
}
return value;
}
function coerceShadowQuality(value) {
if (!value || !IsShadowQuality(value)) {
return null;
}
return value;
}
function coerceResetMode(value) {
if (!value || value === "auto") {
return "auto";
}
if (value === "reframe") {
return "reframe";
}
return value.trim().split(/\s+/);
}
/**
* @experimental
* Base class for the viewer custom element.
*/
class ViewerElement extends i$1 {
/**
* @experimental
* Creates an instance of a ViewerElement subclass.
* @param _viewerClass The Viewer subclass to use when creating the Viewer instance.
* @param _options The options to use when creating the Viewer and binding it to the specified canvas.
*/
constructor(_viewerClass, _options = {}) {
super();
this._viewerClass = _viewerClass;
this._options = _options;
this._viewerLock = new AsyncLock();
this._animationSliderResizeObserver = null;
// Bindings for properties that are synchronized both ways between the lower level Viewer and the HTML3DElement.
this._propertyBindings = [
this._createPropertyBinding("clearColor", (details) => details.scene.onClearColorChangedObservable, (details) => (details.scene.clearColor = this.clearColor ?? new Color4(0, 0, 0, 0)), (details) => (this.clearColor = details.scene.clearColor)),
this._createPropertyBinding("skyboxBlur", (details) => details.viewer.onEnvironmentConfigurationChanged, (details) => (details.viewer.environmentConfig = { blur: this.skyboxBlur ?? details.viewer.environmentConfig.blur }), (details) => (this.skyboxBlur = details.viewer.environmentConfig.blur)),
this._createPropertyBinding("environmentIntensity", (details) => details.viewer.onEnvironmentConfigurationChanged, (details) => (details.viewer.environmentConfig = { intensity: this.environmentIntensity ?? details.viewer.environmentConfig.intensity }), (details) => (this.environmentIntensity = details.viewer.environmentConfig.intensity)),
this._createPropertyBinding("environmentRotation", (details) => details.viewer.onEnvironmentConfigurationChanged, (details) => (details.viewer.environmentConfig = { rotation: this.environmentRotation ?? details.viewer.environmentConfig.rotation }), (details) => (this.environmentRotation = details.viewer.environmentConfig.rotation)),
this._createPropertyBinding("toneMapping", (details) => details.viewer.onPostProcessingChanged, (details) => {
if (this.toneMapping) {
details.viewer.postProcessing = { toneMapping: this.toneMapping };
}
}, (details) => (this.toneMapping = details.viewer.postProcessing?.toneMapping)),
this._createPropertyBinding("contrast", (details) => details.viewer.onPostProcessingChanged, (details) => (details.viewer.postProcessing = { contrast: this.contrast ?? undefined }), (details) => (this.contrast = details.viewer.postProcessing.contrast)),
this._createPropertyBinding("exposure", (details) => details.viewer.onPostProcessingChanged, (details) => (details.viewer.postProcessing = { exposure: this.exposure ?? undefined }), (details) => (this.exposure = details.viewer.postProcessing.exposure)),
this._createPropertyBinding("cameraAutoOrbit", (details) => details.viewer.onCameraAutoOrbitChanged, (details) => (details.viewer.cameraAutoOrbit = { enabled: this.cameraAutoOrbit }), (details) => (this.cameraAutoOrbit = details.viewer.cameraAutoOrbit.enabled)),
this._createPropertyBinding("cameraAutoOrbitSpeed", (details) => details.viewer.onCameraAutoOrbitChanged, (details) => (details.viewer.cameraAutoOrbit = { speed: this.cameraAutoOrbitSpeed ?? undefined }), (details) => (this.cameraAutoOrbitSpeed = details.viewer.cameraAutoOrbit.speed)),
this._createPropertyBinding("cameraAutoOrbitDelay", (details) => details.viewer.onCameraAutoOrbitChanged, (details) => (details.viewer.cameraAutoOrbit = { delay: this.cameraAutoOrbitDelay ?? undefined }), (details) => (this.cameraAutoOrbitDelay = details.viewer.cameraAutoOrbit.delay)),
this._createPropertyBinding("animationSpeed", (details) => details.viewer.onAnimationSpeedChanged, (details) => (details.viewer.animationSpeed = this.animationSpeed), (details) => {
let speed = details.viewer.animationSpeed;
speed = allowedAnimationSpeeds.reduce((prev, curr) => (Math.abs(curr - speed) < Math.abs(prev - speed) ? curr : prev));
this.animationSpeed = speed;
this._dispatchCustomEvent("animationspeedchange", (type) => new Event(type));
}),
this._createPropertyBinding("selectedAnimation", (details) => details.viewer.onSelectedAnimationChanged, (details) => (details.viewer.selectedAnimation = this.selectedAnimation ?? details.viewer.selectedAnimation), (details) => (this.selectedAnimation = details.viewer.selectedAnimation)),
this._createPropertyBinding("selectedMaterialVariant", (details) => details.viewer.onSelectedMaterialVariantChanged, (details) => (details.viewer.selectedMaterialVariant = this.selectedMaterialVariant ?? details.viewer.selectedMaterialVariant ?? ""), (details) => (this.selectedMaterialVariant = details.viewer.selectedMaterialVariant)),
this._createPropertyBinding("hotSpots", (details) => details.viewer.onHotSpotsChanged, (details) => (details.viewer.hotSpots = this.hotSpots ?? details.viewer.hotSpots), (details) => (this.hotSpots = details.viewer.hotSpots)),
this._createPropertyBinding("camerasAsHotSpots", (details) => details.viewer.onCamerasAsHotSpotsChanged, (details) => (details.viewer.camerasAsHotSpots = this.camerasAsHotSpots ?? details.viewer.camerasAsHotSpots), (details) => (this.camerasAsHotSpots = details.viewer.camerasAsHotSpots)),
];
this._isFaultedBacking = false;
/**
* The engine to use for rendering.
*/
this.engine = this._options.engine;
/**
* When true, the scene will be rendered even if no scene state has changed.
*/
this.renderWhenIdle = this._options.autoSuspendRendering === false;
/**
* The model URL.
*/
this.source = this._options.source ?? null;
/**
* Forces the model to be loaded with the specified extension.
* @remarks
* If this property is not set, the extension will be inferred from the model URL when possible.
*/
this.extension = null;
/**
* The texture URL for lighting.
*/
this.environmentLighting = this._options.environmentLighting ?? null;
/**
* The texture URL for the skybox.
*/
this.environmentSkybox = this._options.environmentSkybox ?? null;
/**
* A value between 0 and 2 that specifies the intensity of the environment lighting.
*/
this.environmentIntensity = this._options.environmentConfig?.intensity ?? null;
/**
* A value in radians that specifies the rotation of the environment.
*/
this.environmentRotation = this._options.environmentConfig?.rotation ?? null;
/**
* The type of shadows to use.
*/
this.shadowQuality = null;
this._loadingProgress = false;
/**
* A value between 0 and 1 that specifies how much to blur the skybox.
*/
this.skyboxBlur = this._options.environmentConfig?.blur ?? null;
/**
* The tone mapping to use for rendering the scene.
*/
this.toneMapping = this._options.postProcessing?.toneMapping ?? null;
/**
* The contrast applied to the scene.
*/
this.contrast = this._options.postProcessing?.contrast ?? null;
/**
* The exposure applied to the scene.
*/
this.exposure = this._options.postProcessing?.exposure ?? null;
/**
* The clear color (e.g. background color) for the viewer.
*/
this.clearColor = this._options.clearColor
? new Color4(this._options.clearColor[0], this._options.clearColor[1], this._options.clearColor[2], this._options.clearColor[3] ?? 1)
: null;
/**
* Enables or disables camera auto-orbit.
*/
this.cameraAutoOrbit = this._options.cameraAutoOrbit?.enabled ?? false;
/**
* The speed at which the camera auto-orbits around the target.
*/
this.cameraAutoOrbitSpeed = this._options.cameraAutoOrbit?.speed ?? null;
/**
* The delay in milliseconds before the camera starts auto-orbiting.
*/
this.cameraAutoOrbitDelay = this._options.cameraAutoOrbit?.delay ?? null;
/**
* The set of defined hot spots.
*/
this.hotSpots = this._options.hotSpots ?? {};
/**
* True if the default animation should play automatically when a model is loaded.
*/
this.animationAutoPlay = !!this._options.animationAutoPlay;
/**
* The currently selected animation index.
*/
this.selectedAnimation = this._options.selectedAnimation ?? null;
/**
* The speed scale at which animations are played.
*/
this.animationSpeed = this._options.animationSpeed ?? 1;
/**
* The current point on the selected animation timeline, normalized between 0 and 1.
*/
this.animationProgress = 0;
this._animations = [];
this._isAnimationPlaying = false;
this._showAnimationSlider = true;
/**
* The currently selected material variant.
*/
this.selectedMaterialVariant = this._options.selectedMaterialVariant ?? null;
/**
* True if scene cameras should be used as hotspots.
*/
this.camerasAsHotSpots = false;
/**
* Determines the behavior of the reset function, and the associated default reset button.
* @remarks
* - "auto" - Resets the camera to the initial pose if it makes sense given other viewer state, such as the selected animation.
* - "reframe" - Reframes the camera based on the current viewer state (ignores the initial pose).
* - [ResetFlag] - A space separated list of reset flags that reset various aspects of the viewer state.
*/
this.resetMode = "auto";
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
static get observedAttributes() {
// These attributes don't have corresponding properties, so they are managed directly.
return [...super.observedAttributes, "camera-orbit", "camera-target"];
}
/**
* Gets the underlying viewer details (when the underlying viewer is in a loaded state).
* This is useful for advanced scenarios where direct access to the viewer or Babylon scene is needed.
*/
get viewerDetails() {
return this._viewerDetails;
}
/**
* Get hotspot world and screen values from a named hotspot
* @param name slot of the hot spot
* @param result resulting world and screen positions
* @returns world position, world normal and screen space coordinates
*/
queryHotSpot(name, result) {
if (this._viewerDetails) {
return this._viewerDetails.viewer.queryHotSpot(name, result);
}
return false;
}
/**
* Updates the camera to focus on a named hotspot.
* @param name The name of the hotspot to focus on.
* @returns true if the hotspot was found and the camera was updated, false otherwise.
*/
focusHotSpot(name) {
if (this._viewerDetails) {
return this._viewerDetails.viewer.focusHotSpot(name);
}
return false;
}
get _isFaulted() {
return this._isFaultedBacking;
}
/**
* The texture URLs used for lighting and skybox. Setting this property will set both environmentLighting and environmentSkybox.
*/
get environment() {
return { lighting: this.environmentLighting, skybox: this.environmentSkybox };
}
set environment(url) {
this.environmentLighting = url || null;
this.environmentSkybox = url || null;
}
/**
* Gets information about loading activity.
* @remarks
* false indicates no loading activity.
* true indicates loading activity with no progress information.
* A number between 0 and 1 indicates loading activity with progress information.
*/
get loadingProgress() {
return this._loadingProgress;
}
/**
* @experimental
* True if the viewer has any hotspots.
*/
get _hasHotSpots() {
return Object.keys(this.hotSpots).length > 0;
}
/**
* The list of animation names for the currently loaded model.
*/
get animations() {
return this._animations;
}
/**
* @experimental
* True if the loaded model has any animations.
*/
get _hasAnimations() {
return this._animations.length > 0;
}
/**
* True if an animation is currently playing.
*/
get isAnimationPlaying() {
return this._isAnimationPlaying;
}
/**
* The list of material variants for the currently loaded model.
*/
get materialVariants() {
return this._viewerDetails?.viewer.materialVariants ?? [];
}
/**
* Toggles the play/pause animation state if there is a selected animation.
*/
toggleAnimation() {
this._viewerDetails?.viewer.toggleAnimation();
}
/**
* Resets the Viewer state based on the @see resetMode property.
*/
reset() {
this._reset(this.resetMode);
}
_reset(mode) {
switch (mode) {
case "auto":
this._viewerDetails?.viewer.resetCamera(undefined);
break;
case "reframe":
this._viewerDetails?.viewer.resetCamera(true);
break;
default:
this._viewerDetails?.viewer.reset(...mode);
break;
}
}
/**
* Resets the camera to its initial pose.
*/
resetCamera() {
this._reset("reframe");
}
/**
* Reloads the viewer. This is typically only needed when the viewer is in a faulted state (e.g. due to the context being lost).
*/
reload() {
this._tearDownViewer();
this._setupViewer();
}
/** @internal */
connectedCallback() {
super.connectedCallback();
this._setupViewer();
}
/** @internal */
disconnectedCallback() {
super.disconnectedCallback();
this._tearDownViewer();
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
attributeChangedCallback(name, oldValue, newValue) {
super.attributeChangedCallback(name, oldValue, newValue);
if (this.hasUpdated) {
if (name == "camera-orbit") {
const value = coerceCameraOrbitOrTarget(newValue);
if (value) {
this._viewerDetails?.viewer.updateCamera({ alpha: value[0], beta: value[1], radius: value[2] });
}
else {
this._viewerDetails?.viewer.resetCamera(false);
}
}
else if (name == "camera-target") {
const value = coerceCameraOrbitOrTarget(newValue);
if (value) {
this._viewerDetails?.viewer.updateCamera({ targetX: value[0], targetY: value[1], targetZ: value[2] });
}
else {
this._viewerDetails?.viewer.resetCamera(false);
}
}
}
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
update(changedProperties) {
super.update(changedProperties);
if (this._hotSpotSelect) {
this._hotSpotSelect.value = "";
}
let needsReload = false;
if (changedProperties.get("renderWhenIdle") != null) {
needsReload = true;
}
else if (changedProperties.has("engine")) {
const previous = changedProperties.get("engine");
if (previous && this.engine !== previous) {
needsReload = true;
}
}
if (needsReload) {
this._tearDownViewer();
this._setupViewer();
}
else {
this._propertyBindings.filter((binding) => changedProperties.has(binding.property)).forEach((binding) => binding.updateViewer());
if (changedProperties.has("source")) {
this._updateModel();
}
if (changedProperties.has("environmentLighting") || changedProperties.has("environmentSkybox")) {
this._updateEnv({
lighting: changedProperties.has("environmentLighting"),
skybox: changedProperties.has("environmentSkybox"),
});
}
if (changedProperties.has("shadowQuality")) {
this._updateShadows(this.shadowQuality);
}
}
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
render() {
return x `
<div class="full-size">
<div id="canvasContainer" class="full-size"></div>
${this._renderOverlay()}
</div>
`;
}
/**
* @experimental
* Renders the progress bar.
* @returns The template result for the progress bar.
*/
_renderProgressBar() {
const showProgressBar = this.loadingProgress !== false;
// If loadingProgress is true, then the progress bar is indeterminate so the value doesn't matter.
const progressValue = typeof this.loadingProgress === "boolean" ? 0 : this.loadingProgress * 100;
const isIndeterminate = this.loadingProgress === true;
return x `
<div part="progress-bar" class="bar loading-progress-outer ${showProgressBar ? "" : "loading-progress-outer-inactive"}" aria-label="Loading Progress">
<div
class="loading-progress-inner ${isIndeterminate ? "loading-progress-inner-indeterminate" : ""}"
style="${isIndeterminate ? "" : `width: ${progressValue}%`}"
></div>
</div>
`;
}
/**
* @experimental
* Renders the toolbar.
* @returns The template result for the toolbar.
*/
_renderToolbar() {
let toolbarControls = [];
if (this._viewerDetails?.model != null) {
// If the model has animations, add animation controls.
if (this._hasAnimations) {
toolbarControls.push(x `
<div class="animation-timeline">
<button aria-label="${this.isAnimationPlaying ? "Pause" : "Play"}" @click="${this.toggleAnimation}">
${!this.isAnimationPlaying
? x `
<svg viewBox="0 0 24 24">
<path d="${playFilledIcon}" fill="currentColor"></path>
</svg>
`
: x `
<svg viewBox="0 0 24 24">
<path d="${pauseFilledIcon}" fill="currentColor"></path>
</svg>
`}
</button>
<input
${n(this._onAnimationSliderChanged)}
aria-label="Animation Progress"
class="animation-timeline-input"
style="${this._showAnimationSlider ? "" : "visibility: hidden"}"
type="range"
min="0"
max="1"
step="0.0001"
.value="${this.animationProgress}"
@input="${this._onAnimationTimelineChanged}"
@pointerdown="${this._onAnimationTimelinePointerDown}"
/>
</div>
<select aria-label="Select Animation Speed" @change="${this._onAnimationSpeedChanged}">
${allowedAnimationSpeeds.map((speed) => x `<option value="${speed}" .selected="${this.animationSpeed === speed}">${speed}x</option> `)}
</select>
${this.animations.length > 1
? x `<select aria-label="Select Animation" @change="${this._onSelectedAnimationChanged}">
${this.animations.map((name, index) => x `<option value="${index}" .selected="${this.selectedAnimation === index}">${name}</option>`)}
</select>`
: ""}
`);
}
// If the model has material variants, add material variant controls.
if (this.materialVariants.length > 1) {
toolbarControls.push(x `
<select aria-label="Select Material Variant" @change="${this._onMaterialVariantChanged}">
${this.materialVariants.map((name) => x `<option value="${name}" .selected="${this.selectedMaterialVariant === name}">${name}</option>`)}
</select>
`);
}
// Always include a button to reset the camera pose.
toolbarControls.push(x `
<button aria-label="Reset Camera Pose" @click="${this.reset}">
<svg viewBox="0 0 24 24">
<path d="${arrowResetFilledIcon}" fill="currentColor"></path>
</svg>
</button>
`);
// If hotspots have been defined, add hotspot controls.
if (this._hasHotSpots) {
toolbarControls.push(x `
<div class="select-container">
<select id="hotSpotSelect" aria-label="Select HotSpot" @change="${this._onHotSpotsChanged}">
<!-- When the select is forced to be less wide than the options, padding on the right is lost. Pad with white space. -->
${Object.keys(this.hotSpots).map((name) => x `<option value="${name}">${name} </option>`)}
</select>
<!-- This button is not actually interactive, we want input to pass through to the select below. -->
<button style="pointer-events: none">
<svg viewBox="0 0 24 24">
<path d="${targetFilledIcon}" fill="currentColor"></path>
</svg>
</button>
</div>
`);
}
// Add a vertical divider between each toolbar control.
const controlCount = toolbarControls.length;
const separator = x `<div class="divider"></div>`;
toolbarControls = toolbarControls.reduce((toolbarControls, toolbarControl, index) => {
if (index < controlCount - 1) {
return [...toolbarControls, toolbarControl, separator];
}
else {
return [...toolbarControls, toolbarControl];
}
}, new Array());
}
if (toolbarControls.length > 0) {
return x ` <div part="tool-bar" class="bar ${this._hasAnimations ? "" : "bar-min"} tool-bar">${toolbarControls}</div>`;
}
else {
return x ``;
}
}
/**
* @experimental
* Renders the reload button.
* @returns The template result for the reload button.
*/
_renderReloadButton() {
return x `${this._isFaulted
? x `
<button aria-label="Reload" part="reload-button" class="reload-button" @click="${this.reload}">
<svg viewBox="0 0 24 24">
<path d="${arrowClockwiseFilledIcon}" fill="currentColor"></path>
</svg>
</button>
`
: ""}`;
}
/**
* @experimental
* Renders UI elements that overlay the viewer.
* Override this method to provide additional rendering for the component.
* @returns TemplateResult The rendered template result.
*/
_renderOverlay() {
// NOTE: The unnamed 'slot' element holds all child elements of the <babylon-viewer> that do not specify a 'slot' attribute.
return x `
<slot class="full-size children-slot"></slot>
<slot name="progress-bar">${this._renderProgressBar()}</slot>
<slot name="tool-bar">${this._renderToolbar()}</slot>
<slot name="reload-button">${this._renderReloadButton()}</slot>
`;
}
/**
* @experimental
* Dispatches a custom event.
* @param type The type of the event.
* @param event A function that creates the event.
*/
_dispatchCustomEvent(type, event) {
this.dispatchEvent(event(type));
}
/**
* @experimental
* Handles changes to the selected animation.
* @param event The change event.
*/
_onSelectedAnimationChanged(event) {
const selectElement = event.target;
this.selectedAnimation = Number(selectElement.value);
}
/**
* @experimental
* Handles changes to the animation speed.
* @param event The change event.
*/
_onAnimationSpeedChanged(event) {
const selectElement = event.target;
this.animationSpeed = Number(selectElement.value);
}
/**
* @experimental
* Handles changes to the animation timeline.
* @param event The change event.
*/
_onAnimationTimelineChanged(event) {
if (this._viewerDetails) {
const input = event.target;
const value = Number(input.value);
if (value !== this.animationProgress) {
this._viewerDetails.viewer.animationProgress = value;
}
}
}
/**
* @experimental
* Handles pointer down events on the animation timeline.
* @param event The pointer down event.
*/
_onAnimationTimelinePointerDown(event) {
if (this._viewerDetails?.viewer.isAnimationPlaying) {
this._viewerDetails.viewer.pauseAnimation();
const input = event.target;
input.addEventListener("pointerup", () => this._viewerDetails?.viewer.playAnimation(), { once: true });
}
}
/**
* @experimental
* Handles changes to the selected material variant.
* @param event The change event.
*/
_onMaterialVariantChanged(event) {
const selectElement = event.target;
this.selectedMaterialVariant = selectElement.value;
}
/**
* @experimental
* Handles changes to the hot spot list.
* @param event The change event.
*/
_onHotSpotsChanged(event) {
const selectElement = event.target;
const hotSpotName = selectElement.value;
// We don't actually want a selected value, this is just a one time trigger.
selectElement.value = "";
this.focusHotSpot(hotSpotName);
}
_onAnimationSliderChanged(element) {
this._animationSliderResizeObserver?.disconnect();
if (element) {
this._animationSliderResizeObserver = new ResizeObserver(() => {
this._showAnimationSlider = element.clientWidth >= 80;
});
this._animationSliderResizeObserver.observe(element);
}
}
// Helper function to simplify keeping Viewer properties in sync with HTML3DElement properties.
_createPropertyBinding(property, getObservable, updateViewer, updateElement) {
return {
property,
// Called each time a Viewer instance is created.
onInitialized: (viewerDetails) => {
getObservable(viewerDetails).add(() => {
updateElement(viewerDetails);
});
updateViewer(viewerDetails);
},
// Called when the HTML3DElement property should be propagated to the Viewer.
updateViewer: () => {
if (this._viewerDetails) {
updateViewer(this._viewerDetails);
}
},
};
}
async _setupViewer() {
await this._viewerLock.lockAsync(async () => {
// The first time the element is connected, the canvas container may not be available yet.
// Wait for the first update if needed.
if (!this._canvasContainer) {
await this.updateComplete;
}
if (this._canvasContainer && !this._viewerDetails) {
const canvas = document.createElement("canvas");
canvas.className = "full-size canvas";
canvas.setAttribute("touch-action", "none");
this._canvasContainer.appendChild(canvas);
{
const detailsDeferred = new Deferred();
// eslint-disable-next-line @typescript-eslint/no-this-alias
const viewerElement = this;
const viewer = await this._createViewer(canvas, new Proxy(this._options, {
get(target, prop) {
switch (prop) {
case "engine":
return viewerElement.engine ?? target.engine;
case "autoSuspendRendering":
return !(viewerElement.hasAttribute("render-when-idle") || target.autoSuspendRendering === false);
case "source":
return viewerElement.getAttribute("source") ?? target.source;
case "environmentLighting":
return viewerElement.getAttribute("environment-lighting") ?? viewerElement.getAttribute("environment") ?? target.environmentLighting;
case "environmentSkybox":
return viewerElement.getAttribute("environment-skybox") ?? viewerElement.getAttribute("environment") ?? target.environmentSkybox;
case "environmentConfig":
return {
intensity: coerceNumericAttribute(viewerElement.getAttribute("environment-intensity")) ?? target.environmentConfig?.intensity,
blur: coerceNumericAttribute(viewerElement.getAttribute("skybox-blur")) ?? target.environmentConfig?.blur,
rotation: coerceNumericAttribute(viewerElement.getAttribute("environment-rotation")) ?? target.environmentConfig?.rotation,
};
case "shadowConfig":
return {
quality: coerceShadowQuality(viewerElement.getAttribute("shadow-quality")) ?? target.shadowConfig?.quality,
};
case "cameraOrbit":
return coerceCameraOrbitOrTarget(viewerElement.getAttribute("camera-orbit")) ?? target.cameraOrbit;
case "cameraTarget":
return coerceCameraOrbitOrTarget(viewerElement.getAttribute("camera-target")) ?? target.cameraTarget;
case "cameraAutoOrbit":
return {
enabled: viewerElement.hasAttribute("camera-auto-orbit") || target.cameraAutoOrbit?.enabled,
speed: coerceNumericAttribute(viewerElement.getAttribute("camera-auto-orbit-speed")) ?? target.cameraAutoOrbit?.speed,
delay: coerceNumericAttribute(viewerElement.getAttribute("camera-auto-orbit-delay")) ?? target.cameraAutoOrbit?.delay,
};
case "animationAutoPlay":
return viewerElement.hasAttribute("animation-auto-play") || target.animationAutoPlay;
case "animationSpeed":
return coerceNumericAttribute(viewerElement.getAttribute("animation-speed")) ?? target.animationSpeed;
case "selectedAnimation":
return coerceNumericAttribute(viewerElement.getAttribute("selected-animation")) ?? target.selectedAnimation;
case "postProcessing":
return {
toneMapping: coerceToneMapping(viewerElement.getAttribute("tone-mapping")) ?? target.postProcessing?.toneMapping,
contrast: coerceNumericAttribute(viewerElement.getAttribute("contrast")) ?? target.postProcessing?.contrast,
exposure: coerceNumericAttribute(viewerElement.getAttribute("exposure")) ?? target.postProcessing?.exposure,
};
case "selectedMaterialVariant":
return viewerElement.getAttribute("material-variant") ?? target.selectedMaterialVariant;
case "onInitialized":
return (details) => {
target.onInitialized?.(details);
detailsDeferred.resolve(details);
};
case "onFaulted":
return (error) => {
viewerElement._isFaultedBacking = true;
target.onFaulted?.(error);
viewerElement._tearDownViewer();
};
default:
return target[prop];
}
},
}));
const details = await detailsDeferred.promise;
this._viewerDetails = Object.assign(details, { viewer });
}
const details = this._viewerDetails;
details.viewer.onEnvironmentChanged.add(() => {
this._dispatchCustomEvent("environmentchange", (type) => new Event(type));
});
details.viewer.onEnvironmentConfigurationChanged.add(() => {
this._dispatchCustomEvent("environmentconfigurationchange", (type) => new Event(type));
});
details.viewer.onEnvironmentError.add((error) => {
this._dispatchCustomEvent("environmenterror", (type) => new ErrorEvent(type, { error }));
});
details.viewer.onShadowsConfigurationChanged.add(() => {
this._dispatchCustomEvent("shadowsconfigurationchange", (type) => new Event(type));
});
details.viewer.onModelChanged.add((source) => {
this._animations = [...details.viewer.animations];
this._dispatchCustomEvent("modelchange", (type) => new CustomEvent(type, { detail: source }));
});
details.viewer.onModelError.add((error) => {
this._animations = [...details.viewer.animations];
this._dispatchCustomEvent("modelerror", (type) => new ErrorEvent(type, { error }));
});
details.viewer.onLoadingProgressChanged.add(() => {
this._loadingProgress = details.viewer.loadingProgress;
this._dispatchCustomEvent("loadingprogresschange", (type) => new Event(type));
});
details.viewer.onSelectedAnimationChanged.add(() => {
this._dispatchCustomEvent("selectedanimationchange", (type) => new Event(type));
});
details.viewer.onIsAnimationPlayingChanged.add(() => {
this._isAnimationPlaying = details.viewer.isAnimationPlaying ?? false;
this._dispatchCustomEvent("animationplayingchange", (type) => new Event(type));
});
details.viewer.onAnimationProgressChanged.add(() => {
this.animationProgress = details.viewer.animationProgress ?? 0;
this._dispatchCustomEvent("animationprogresschange", (type) => new Event(type));
});
details.viewer.onSelectedMaterialVariantChanged.add(() => {
this._dispatchCustomEvent("selectedmaterialvariantchange", (type) => new Event(type));
});
details.scene.onAfterRenderCameraObservable.add(() => {
this._dispatchCustomEvent("viewerrender", (type) => new Event(type));
});
this._propertyBindings.forEach((binding) => binding.onInitialized(details));
this._dispatchCustomEvent("viewerready", (type) => new Event(type));
}
this._isFaultedBacking = false;
});
}
/**
* @experimental
* Creates a viewer for the specified canvas.
* @param canvas The canvas to create the viewer for.
* @param options The options to use for the viewer.
* @returns The created viewer.
*/
async _createViewer(canvas, options) {
return await CreateViewerForCanvas(canvas, Object.assign(options, { viewerClass: this._viewerClass }));
}
async _tearDownViewer() {
await this._viewerLock.lockAsync(async () => {
if (this._viewerDetails) {
this._viewerDetails.viewer.dispose();
this._viewerDetails = undefined;
}
this._loadingProgress = false;
// We want to replace the canvas for two reasons:
// 1. When the viewer element is reconnected to the DOM, we don't want to briefly see the last frame of the previous model.
// 2. If we are changing engines (e.g. WebGL to WebGPU), we need to create a new canvas for the new engine.
if (this._canvasContainer && this._canvasContainer.firstElementChild) {
this._canvasContainer.removeChild(this._canvasContainer.firstElementChild);
}
});
}
async _updateModel() {
if (this._viewerDetails) {
try {
if (this.source) {
await this._viewerDetails.viewer.loadModel(this.source, {
pluginExtension: this.extension ?? undefined,
});
}
else {
await this._viewerDetails.viewer.resetModel();
}
}
catch (error) {
// If loadModel was aborted (e.g. because a new model load was requested before this one finished), we can just ignore the error.
if (!(error instanceof AbortError)) {
Logger.Error(error);
}
}
}
}
async _updateEnv(options) {
if (this._viewerDetails) {
try {
const updates = [];
if (options.lighting && options.skybox && this.environmentLighting === this.environmentSkybox) {
updates.push([this.environmentLighting, { lighting: true, skybox: true }]);
}
else {
if (options.lighting) {
updates.push([this.environmentLighting, { lighting: true }]);
}
if (options.skybox) {
updates.push([this.environmentSkybox, { skybox: true }]);
}
}
const promises = updates.map(async ([url, options]) => {
if (url) {
await this._viewerDetails?.viewer.loadEnvironment(url, options);
}
else {
await this._viewerDetails?.viewer.resetEnvironment(options);
}
});
await Promise.all(promises);
}
catch (error) {
// If loadEnvironment was aborted (e.g. because a new environment load was requested before this one finished), we can just ignore the error.
if (!(error instanceof AbortError)) {
Logger.Error(error);
}
}
}
}
async _updateShadows(quality) {
if (!quality) {
return;
}
try {
const options = { quality };
await this._viewerDetails?.viewer.updateShadows(options);
}
catch (error) {
// If loadEnvironment was aborted (e.g. because a new environment load was requested before this one finished), we can just ignore the error.
if (!(error instanceof AbortError)) {
Logger.Error(error);
}
}
}
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
ViewerElement.styles = i$4 `
:host {
--ui-foreground-color: white;
--ui-background-hue: 233;
--ui-background-saturation: 8%;
--ui-background-lightness: 39%;
--ui-background-opacity: 0.75;
--ui-background-color: hsla(var(--ui-background-hue), var(--ui-background-saturation), var(--ui-background-lightness), var(--ui-background-opacity));
--ui-background-color-hover: hsla(
var(--ui-background-hue),
var(--ui-background-saturation),
calc(var(--ui-background-lightness) - 10%),
calc(var(--ui-background-opacity) - 0.1)
);
all: inherit;
overflow: hidden;
}
.full-size {
display: block;
position: relative;
width: 100%;
height: 100%;
}
.canvas {
outline: none;
}
.children-slot {
position: absolute;
top: 0;
background: transparent;
pointer-events: none;
}
.reload-button {
position: absolute;
top: 50%;
left: 50%;
width: 25%;
transform: translate(-50%, -50%);
color: var(--ui-foreground-color);
background-color: var(--ui-background-color);
border: 1px solid transparent;
border-radius: 24px;
padding: 0;
cursor: pointer;
outline: none;
}
.reload-button:hover {
background-color: var(--ui-background-color-hover);
}
.bar {
position: absolute;
width: calc(100% - 24px);
min-width: 370px;
max-width: 1280px;
left: 50%;
transform: translateX(-50%);
background-color: var(--ui-background-color);
}
.bar-min {
width: unset;
min-width: unset;
max-width: unset;
}
.loading-progress-outer {
height: 4px;
border-radius: 4px;
border: 1px solid var(--ui-background-color);
outline: none;
top: 12px;
pointer-events: none;
transition: opacity 0.5s ease;
}
.loading-progress-outer-inactive {
opacity: 0;
/* Set the background color to the foreground color while in the inactive state so that the color seen is correct while fading out the opacity. */
background-color: var(--ui-foreground-color);
}
.loading-progress-inner {
width: 0;
height: 100%;
border-radius: inherit;
background-color: var(--ui-foreground-color);
transition: width 0.3s linear;
}
/* The right side of the inner progress bar starts aligned with the left side of the outer progress bar (container).
So, if the width is 30%, then the left side of the inner progress bar moves a total of 130% of the width of the container.
This is why the first keyframe is at 23% ((100/130)*30).
*/
@keyframes indeterminate {
0% {
left: 0%;
width: 0%;
}
23% {
left: 0%;
width: 30%;
}
77% {
left: 70%;
width: 30%;
}
100% {
left: 100%;
width: 0%;
}
}
.loading-progress-inner-indeterminate {
position: absolute;
animation: indeterminate 1.5s infinite;
animation-timing-function: linear;
}
.tool-bar {
display: flex;
flex-direction: row;
align-items: center;
border-radius: 12px;
border-color: var(--ui-foreground-color);
height: 48px;
bottom: 12px;
color: var(--ui-foreground-color);
-webkit-tap-highlight-color: transparent;
}
.tool-bar * {
height: 100%;
min-width: 48px;
}
.tool-bar .divider {
min-width: 1px;
margin: 0px 6px;
height: 66%;
background-color: var(--ui-foreground-color);
}
.tool-bar select {
background: none;
min-width: 52px;
max-width: 128px;
border: 1px solid transparent;
border-radius: inherit;
color: inherit;
font-size: 14px;
padding: 0px 12px;
cursor: pointer;
outline: none;
appearance: none; /* Remove default styling */
-webkit-appearance: none; /* Remove default styling for Safari */
}
.tool-bar .select-container {
position: relative;
display: flex;
border-radius: inherit;
border-width: 0;
padding: 0;
}
.tool-bar .select-container select {
position: absolute;
min-width: 0;
width: 100%;
}
.tool-bar .select-container button {
position: absolute;
border-width: 0;
}
.tool-bar select:hover,
.tool-bar select:focus {
background-color: var(--ui-background-color-hover);
}
.tool-bar select option {
background-color: var(--ui-background-color);
color: var(--ui-foreground-color);
}
.tool-bar select:focus-visible {
border-color: inherit;
}
.tool-bar button {
background: none;
border: 1px solid transparent;
border-radius: inherit;
color: inherit;
padding: 0;
cursor: pointer;
outline: none;
}
.tool-bar button:hover {
background-color: var(--ui-background-color-hover);
}
.tool-bar button:focus-visible {
border-color: inherit;
}
.tool-bar button svg {
width: 32px;
height: 32px;
}
.animation-timeline {
display: flex;
flex: 1;
position: relative;
overflow: hidden;
cursor: pointer;
align-items: center;
border-radius: inherit;
border-color: inherit;
}
.animation-timeline-input {
-webkit-appearance: none;
cursor: pointer;
width: 100%;
height: 100%;
outline: none;
border: 1px solid transparent;
border-radius: inherit;
padding: 0 12px;
background-color: transparent;
}
.animation-timeline-input:focus-visible {
border-color: inherit;
}
/*Chrome -webkit */
.animation-timeline-input::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border: 2px solid;
color: var(--ui-foreground-color);
border-radius: 50%;
background: hsla(var(--ui-background-hue), var(--ui-background-saturation), var(--ui-background-lightness), 1);
margin-top: -10px;
}
.animation-timeline-input::-webkit-slider-runnable-track {
height: 2px;
-webkit-appearance: none;
background-color: var(--ui-foreground-color);
}
/** FireFox -moz */
.animation-timeline-input::-moz-range-progress {
height: 2px;
background-color: var(--ui-foreground-color);
}
.animation-timeline-input::-moz-range-thumb {
width: 16px;
height: 16px;
border: 2px solid var(--ui-foreground-color);
border-radius: 50%;
background: hsla(var(--ui-background-hue), var(--ui-background-saturation), var(--ui-background-lightness), 1);
}
.animation-timeline-input::-moz-range-track {
height: 2px;
background: var(--ui-foreground-color);
}
`;
__decorate([
r$1()
], ViewerElement.prototype, "_isFaultedBacking", void 0);
__decorate([
n$2({ converter: coerceEngineAttribute })
], ViewerElement.prototype, "engine", void 0);
__decorate([
n$2({ attribute: "render-when-idle", type: Boolean })
], ViewerElement.prototype, "renderWhenIdle", void 0);
__decorate([
n$2()
], ViewerElement.prototype, "source", void 0);
__decorate([
n$2()
], ViewerElement.prototype, "extension", void 0);
__decorate([
n$2({
hasChanged: (newValue, oldValue) => {
return newValue.lighting !== oldValue.lighting || newValue.skybox !== oldValue.skybox;
},
})
], ViewerElement.prototype, "environment", null);
__decorate([
n$2({ attribute: "environment-lighting" })
], ViewerElement.prototype, "environmentLighting", void 0);
__decorate([
n$2({ attribute: "environment-skybox" })
], ViewerElement.prototype, "environmentSkybox", void 0);
__decorate([
n$2({ type: Number, attribute: "environment-intensity" })
], ViewerElement.prototype, "environmentIntensity", void 0);
__decorate([
n$2({
type: Number,
attribute: "environment-rotation",
})
], ViewerElement.prototype, "environmentRotation", void 0);
__decorate([
n$2({
attribute: "shadow-quality",
})
], ViewerElement.prototype, "shadowQuality", void 0);
__decorate([
r$1()
], ViewerElement.prototype, "_loadingProgress", void 0);
__decorate([
n$2({ attribute: "skybox-blur" })
], ViewerElement.prototype, "skyboxBlur", void 0);
__decorate([
n$2({
attribute: "tone-mapping",
converter: (value) => {
if (!value || !IsToneMapping(value)) {
return "neutral";
}
return value;
},
})
], ViewerElement.prototype, "toneMapping", void 0);
__decorate([
n$2()
], ViewerElement.prototype, "contrast", void 0);
__decorate([
n$2()
], ViewerElement.prototype, "exposure", void 0);
__decorate([
n$2({
attribute: "clear-color",
converter: {
fromAttribute: parseColor,
toAttribute: (color) => (color ? color.toHexString() : null),
},
})
], ViewerElement.prototype, "clearColor", void 0);
__decorate([
n$2({
attribute: "camera-auto-orbit",
type: Boolean,
})
], ViewerElement.prototype, "cameraAutoOrbit", void 0);
__decorate([
n$2({
attribute: "camera-auto-orbit-speed",
type: Number,
})
], ViewerElement.prototype, "cameraAutoOrbitSpeed", void 0);
__decorate([
n$2({
attribute: "camera-auto-orbit-delay",
type: Number,
})
], ViewerElement.prototype, "cameraAutoOrbitDelay", void 0);
__decorate([
n$2({
attribute: "hotspots",
converter: (value) => {
if (!value) {
return {};
}
return JSON.parse(value);
},
})
], ViewerElement.prototype, "hotSpots", void 0);
__decorate([
n$2({ attribute: "animation-auto-play", type: Boolean })
], ViewerElement.prototype, "animationAutoPlay", void 0);
__decorate([
n$2({ attribute: "selected-animation", type: Number })
], ViewerElement.prototype, "selectedAnimation", void 0);
__decorate([
n$2({ attribute: "animation-speed" })
], ViewerElement.prototype, "animationSpeed", void 0);
__decorate([
n$2({ attribute: false })
], ViewerElement.prototype, "animationProgress", void 0);
__decorate([
r$1()
], ViewerElement.prototype, "_animations", void 0);
__decorate([
r$1()
], ViewerElement.prototype, "_isAnimationPlaying", void 0);
__decorate([
r$1()
], ViewerElement.prototype, "_showAnimationSlider", void 0);
__decorate([
n$2({ attribute: "material-variant" })
], ViewerElement.prototype, "selectedMaterialVariant", void 0);
__decorate([
n$2({ attribute: "cameras-as-hotspots", type: Boolean })
], ViewerElement.prototype, "camerasAsHotSpots", void 0);
__decorate([
n$2({ attribute: "reset-mode", converter: coerceResetMode })
], ViewerElement.prototype, "resetMode", void 0);
__decorate([
e$1("#canvasContainer")
], ViewerElement.prototype, "_canvasContainer", void 0);
__decorate([
e$1("#hotSpotSelect")
], ViewerElement.prototype, "_hotSpotSelect", void 0);
/**
* Displays a 3D model using the Babylon.js Viewer.
*/
let HTML3DElement = class HTML3DElement extends ViewerElement {
/**
* Creates a new HTML3DElement.
* @param options The options to use for the viewer. This is optional, and is only used when programmatically creating a viewer element.
*/
constructor(options) {
super(Viewer, options);
}
};
HTML3DElement = __decorate([
t$1("babylon-viewer")
], HTML3DElement);
/**
* Creates a custom HTML element that creates an HTML3DElement with the specified name and configuration.
* @param elementName The name of the custom element.
* @param options The options to use for the viewer.
*/
function ConfigureCustomViewerElement(elementName, options) {
customElements.define(elementName,
// eslint-disable-next-line jsdoc/require-jsdoc
class extends HTML3DElement {
constructor() {
super(options);
}
});
}
/**
* Displays child elements at the screen space location of a hotspot in a babylon-viewer.
* @remarks
* The babylon-viewer-annotation element must be a child of a babylon-viewer element.
*/
let HTML3DAnnotationElement = class HTML3DAnnotationElement extends i$1 {
constructor() {
super(...arguments);
this._internals = this.attachInternals();
this._mutationObserver = new MutationObserver((mutations) => {
if (mutations.some((mutation) => mutation.type === "childList")) {
this._sanitizeInnerHTML();
}
});
this._viewerAttachment = null;
this._connectingAbortController = null;
this._updateAnnotation = null;
/**
* The name of the hotspot to track.
*/
this.hotSpot = "";
}
/** @internal */
connectedCallback() {
super.connectedCallback();
this._internals.states?.add("invalid");
this._connectingAbortController?.abort();
this._connectingAbortController = new AbortController();
const abortSignal = this._connectingAbortController.signal;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
// Custom element registration can happen at any time via a call to customElements.define, which means it is possible
// the parent custom element hasn't been defined yet. This especially can happen if the order of imports and exports
// results in the parent element being defined after the HTML3DAnnotationElement within the final JS bundle.
if (this.parentElement?.matches(":not(:defined)")) {
await customElements.whenDefined(this.parentElement?.tagName.toLowerCase());
// If the element has since been disconnected or reconnected, abort this connection process.
if (abortSignal.aborted) {
return;
}
}
if (!(this.parentElement instanceof ViewerElement)) {
// eslint-disable-next-line no-console
console.warn("The babylon-viewer-annotation element must be a child of a babylon-viewer element.");
return;
}
this._mutationObserver.observe(this, { childList: true, characterData: true });
this._sanitizeInnerHTML();
const viewerElement = this.parentElement;
const hotSpotResult = new ViewerHotSpotResult();
const updateAnnotation = (this._updateAnnotation = () => {
if (this.hotSpot) {
if (viewerElement.queryHotSpot(this.hotSpot, hotSpotResult)) {
const [screenX, screenY] = hotSpotResult.screenPosition;
this.style.transform = `translate(${screenX}px, ${screenY}px)`;
this._internals.states?.delete("invalid");
if (hotSpotResult.visibility <= 0) {
this._internals.states?.add("back-facing");
}
else {
this._internals.states?.delete("back-facing");
}
}
else {
this._internals.states?.add("invalid");
}
}
});
this._updateAnnotation();
viewerElement.addEventListener("viewerrender", updateAnnotation);
this._viewerAttachment = {
dispose() {
viewerElement.removeEventListener("viewerrender", updateAnnotation);
},
};
})();
}
/** @internal */
disconnectedCallback() {
super.disconnectedCallback();
this._connectingAbortController?.abort();
this._connectingAbortController = null;
this._viewerAttachment?.dispose();
this._viewerAttachment = null;
this._internals.states?.add("invalid");
this._updateAnnotation = null;
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
render() {
return x ` <slot><div aria-label="${this.hotSpot} annotation" part="annotation" class="annotation">${this.hotSpot}</div></slot> `;
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
update(changedProperties) {
super.update(changedProperties);
if (changedProperties.has("hotSpot")) {
this._updateAnnotation?.();
}
}
_sanitizeInnerHTML() {
if (this.innerHTML.trim().length === 0) {
this.innerHTML = "";
}
}
};
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
HTML3DAnnotationElement.styles = i$4 `
:host {
--annotation-foreground-color: black;
--annotation-background-color: white;
display: inline-block;
position: absolute;
transition: opacity 0.25s;
}
:host([hidden]) {
display: none;
}
:host(:state(back-facing)) {
opacity: 0.2;
}
:host(:state(invalid)) {
display: none;
}
.annotation {
transform: translate(-50%, -135%);
font-size: 14px;
padding: 0px 6px;
border-radius: 6px;
color: var(--annotation-foreground-color);
background-color: var(--annotation-background-color);
}
.annotation::after {
content: "";
position: absolute;
left: 50%;
height: 60%;
aspect-ratio: 1;
transform: translate(-50%, 110%) rotate(-45deg);
background-color: inherit;
clip-path: polygon(0 0, 100% 100%, 0 100%, 0 0);
}
`;
__decorate([
n$2({ attribute: "hotspot" })
], HTML3DAnnotationElement.prototype, "hotSpot", void 0);
HTML3DAnnotationElement = __decorate([
t$1("babylon-viewer-annotation")
], HTML3DAnnotationElement);
export { ConfigureCustomViewerElement, CreateHotSpotFromCamera, CreateViewerForCanvas, DefaultViewerOptions, HTML3DAnnotationElement, HTML3DElement, Viewer, ViewerElement, ViewerHotSpotResult };
//# sourceMappingURL=index.js.map