@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.
5,037 lines • 288 kB
JavaScript
import '@babylonjs/core/Misc/symbolMetadataPolyfill.js';
import { AbortError } from '@babylonjs/core/Misc/error.js';
import { AsyncLock } from '@babylonjs/core/Misc/asyncLock.js';
import { Logger } from '@babylonjs/core/Misc/logger.js';
import { Observable } from '@babylonjs/core/Misc/observable.js';
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 { DirectionalLight } from '@babylonjs/core/Lights/directionalLight.js';
import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight.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 { 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 { IsGaussianSplattingClassName } from '@babylonjs/core/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.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 { deepMerge } from '@babylonjs/core/Misc/deepMerger.js';
import { Lazy } from '@babylonjs/core/Misc/lazy.js';
import { SceneOptimizerOptions, HardwareScalingOptimization, SceneOptimizer } from '@babylonjs/core/Misc/sceneOptimizer.js';
import { SnapshotRenderingHelper } from '@babylonjs/core/Misc/snapshotRenderingHelper.js';
import { _RetryWithInterval } from '@babylonjs/core/Misc/timingTools.js';
import { GetExtensionFromUrl } from '@babylonjs/core/Misc/urlTools.js';
import { Scene } from '@babylonjs/core/scene.js';
import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic.js';
import { Deferred } from '@babylonjs/core/Misc/deferred.js';
/**
* Throws if any of the supplied abort signals is aborted.
* @param abortSignals The signals to check.
*/
function throwIfAborted(...abortSignals) {
for (const signal of abortSignals) {
signal?.throwIfAborted();
}
}
/**
* Fire-and-forget wrapper for a promise that logs non-abort errors.
* @param promise The promise to observe.
*/
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);
}
}
})();
}
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);
}
/**
* 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;
}
}
/**
* The default values for {@link ViewerBaseOptions}. Used as fallbacks by both the full Viewer
* and the Lite Viewer; each re-exports this as its own `DefaultViewerOptions`.
*/
const DefaultViewerBaseOptions = {
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,
ssao: "auto",
},
useRightHandedSystem: false,
useOpenPBR: false,
};
/**
* @internal
* Orbit angle (radians) applied to the arc-rotate camera on every automatic reframe (model load
* and animation switch). Shared by the full Viewer and ViewerLite so both frame to the same default
* viewpoint. Exported from `viewerBase` for internal sharing only — intentionally not re-exported
* from the package index, so it is not part of the public API.
*/
const FramingCameraAlpha = Math.PI / 2;
/**
* @internal
* Elevation angle (radians) applied to the arc-rotate camera on every automatic reframe. Shared by
* the full Viewer and ViewerLite. Exported from `viewerBase` for internal sharing only —
* intentionally not re-exported from the package index, so it is not part of the public API.
*/
const FramingCameraBeta = Math.PI / 2.4;
/**
* Common base for the full Babylon.js {@link Viewer} and the lite Viewer.
*
* Encapsulates the pieces that are identical between both engine backends:
* - The 18 public observables exposed by the viewer surface area
* - In-flight load operation tracking (used to compute aggregate `loadingProgress`)
* - The `_throwIfDisposedOrAborted` helper used at the start of every async operation
* - The disposed flag and observable teardown in `dispose()`
*
* Subclasses are responsible for everything engine-specific (scene/engine creation,
* model + environment loading orchestration, camera, post-processing, shadows, etc.)
* and for declaring `implements IViewer` themselves so the public API contract is
* verified at the leaf class level.
*/
class ViewerBase {
constructor() {
// ── Observables ──
// Concrete `Observable<T>` is exposed publicly (it satisfies `IReadonlyObservable<T>`
// for the `IViewer` interface contract). Subclasses notify directly via `this.onXxx.notifyObservers()`.
/**
* 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();
/**
* Fired after each frame is rendered.
*/
this.onAfterRenderObservable = new Observable();
/**
* Fired when the clear color changes.
*/
this.onClearColorChanged = new Observable();
/**
* @internal Tracks in-flight load operations (model + environment + shadows) so that
* `loadingProgress` can return either an aggregate progress number, `true` (indeterminate),
* or `false` (no operations in flight).
*/
this._loadOperations = new Set();
/** @internal True after `dispose()` has been called. */
this._isDisposed = false;
// ──────────────────────────────────────────────────────────────────────────
// Environment loading orchestration
// ──────────────────────────────────────────────────────────────────────────
/** Lock guarding lighting-side environment loads. */
this._loadEnvironmentLightingLock = new AsyncLock();
/** Abort controller for the currently in-flight lighting-side load (null when none). */
this._loadEnvironmentLightingAbortController = null;
/** Lock guarding skybox-side environment loads. */
this._loadEnvironmentSkyboxLock = new AsyncLock();
/** Abort controller for the currently in-flight skybox-side load (null when none). */
this._loadEnvironmentSkyboxAbortController = null;
// ── Environment configuration (intensity / blur / rotation) ──
/** @internal Current environment intensity. Initialized from options in subclass constructors. */
this._environmentIntensity = DefaultViewerBaseOptions.environmentConfig.intensity;
/** @internal Current environment skybox blur. Initialized from options in subclass constructors. */
this._environmentBlur = DefaultViewerBaseOptions.environmentConfig.blur;
/** @internal Current environment rotation in radians. Initialized from options in subclass constructors. */
this._environmentRotation = DefaultViewerBaseOptions.environmentConfig.rotation;
// ── Camera auto-orbit ──
/** @internal Initialized from options in subclass constructors. */
this._autoOrbitEnabled = DefaultViewerBaseOptions.cameraAutoOrbit.enabled;
/** @internal Initialized from options in subclass constructors. */
this._autoOrbitSpeed = DefaultViewerBaseOptions.cameraAutoOrbit.speed;
/** @internal Initialized from options in subclass constructors. */
this._autoOrbitDelay = DefaultViewerBaseOptions.cameraAutoOrbit.delay;
// ── Clear color ──
/**
* @internal The current scene clear color, stored as a stable mutable record so consumers
* holding a reference returned by the `clearColor` getter see updates from the setter and from
* `reset("environment")`. The setter mutates this object in-place rather than replacing it.
*/
this._clearColor = { r: 0, g: 0, b: 0, a: 0 };
// ── Hot spots ──
/** @internal Pure state — no engine state. Subclasses initialize via the public `hotSpots` setter in their constructor body. */
this._hotSpots = {};
// ──────────────────────────────────────────────────────────────────────────
// Model loading orchestration
// ──────────────────────────────────────────────────────────────────────────
/** Lock guarding model loads (and resets). */
this._loadModelLock = new AsyncLock();
/** Abort controller for the currently in-flight model load (null when none). */
this._loadModelAbortController = null;
// ──────────────────────────────────────────────────────────────────────────
// Shadow update orchestration
// ──────────────────────────────────────────────────────────────────────────
/** Lock guarding shadow updates. */
this._updateShadowsLock = new AsyncLock();
/** Abort controller for the currently in-flight shadow update (null when none). */
this._shadowsAbortController = null;
/**
* @internal The currently committed shadow quality. Subclasses initialize this from their
* options in their constructor and read it in their `_loadModelImpl` etc. The base class
* commits a new value here only after `_updateShadowsImpl` succeeds, so failed/aborted
* shadow updates don't leave this field out of sync with engine state.
*/
this._shadowQuality = DefaultViewerBaseOptions.shadowConfig.quality;
}
/**
* The current loading progress. False when no load is in flight, true when at least one
* load is in flight with indeterminate progress, or a number between 0 and 1 representing
* the average of all in-flight loads' progress.
*/
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;
}
/**
* Begin tracking a new load operation. Subclasses call this at the start of an async load
* and dispose the returned handle when it completes (or fails). The handle exposes a
* `progress` setter that, when assigned, fires `onLoadingProgressChanged`.
* @returns A handle that can be disposed when the operation completes; the `progress` setter
* updates the aggregate `loadingProgress` as the operation runs.
*/
_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;
}
/**
* @internal Throws if the viewer has been disposed or any of the supplied abort signals
* are aborted. Used at the start of every async operation to bail out early.
* @param abortSignals Optional abort signals to check.
*/
_throwIfDisposedOrAborted(...abortSignals) {
if (this._isDisposed) {
throw new Error("Viewer is disposed.");
}
throwIfAborted(...abortSignals);
}
/**
* @internal The abort signal of the currently in-flight lighting-side load, or `undefined` if
* none. Subclasses can use this in `_throwIfDisposedOrAborted` to bail out of dependent async
* work (shadows, post-processing, etc.) when the user starts a new lighting load.
*/
get _loadEnvironmentLightingAbortSignal() {
return this._loadEnvironmentLightingAbortController?.signal;
}
/**
* @internal The abort signal of the currently in-flight skybox-side load, or `undefined` if
* none. Subclasses can use this in `_throwIfDisposedOrAborted` to bail out of dependent async
* work (shadows, post-processing, etc.) when the user starts a new skybox load.
*/
get _loadEnvironmentSkyboxAbortSignal() {
return this._loadEnvironmentSkyboxAbortController?.signal;
}
/**
* Loads an environment from the specified URL. The lighting and skybox sides have
* independent locks and abort controllers so concurrent requests for one side don't
* cancel an in-flight load for the other.
* @param url The URL of the environment to load.
* @param options Selects which sides to update (defaults to both) and forwards engine-specific extras.
* @param abortSignal Optional signal that can be used to abort the load externally.
* @returns A promise that resolves when the environment has finished loading.
*/
async loadEnvironment(url, options, abortSignal) {
return await this._updateEnvironment(url, options, abortSignal);
}
/**
* Removes the loaded environment. By default removes both lighting and skybox; pass `options`
* to remove only one side. Subclasses (notably the full Viewer) may override to add backend-specific
* fallback behavior such as substituting a default environment for lighting when the scene contains
* PBR materials.
* @param options Selects which sides to remove (defaults to both).
* @param abortSignal Optional signal that can be used to abort the operation externally.
* @returns A promise that resolves when the environment has finished resetting.
*/
async resetEnvironment(options, abortSignal) {
return await this._updateEnvironment(undefined, options, abortSignal);
}
/**
* @internal Internal helper exposing the dual-lock orchestration with a nullable URL.
* Used by the public `loadEnvironment` (with a string URL) and by subclass `resetEnvironment`
* implementations (which pass `undefined` to clear or `"auto"` to load defaults).
*
* Subclasses should NOT override this — override the abstract `_loadEnvironmentImpl` instead.
*/
async _updateEnvironment(url, options, abortSignal) {
this._throwIfDisposedOrAborted(abortSignal);
// Default semantics: omitting `options` updates both; passing `options` updates only
// the sides whose flag is truthy. `{ lighting: true }` alone updates only lighting,
// `{ skybox: true }` alone updates only skybox.
const updateLighting = options ? !!options.lighting : true;
const updateSkybox = options ? !!options.skybox : true;
if (!updateLighting && !updateSkybox) {
return;
}
// Resolved options object — `lighting` and `skybox` are guaranteed booleans here, while
// engine-specific extras (e.g. `extension`) are forwarded as-is. Passed to the impl so
// it can use both the resolved flags and the original extras without duplicating the
// default-resolution logic.
const resolvedOptions = { ...options, lighting: updateLighting, skybox: updateSkybox };
const locks = [];
const internalAbortControllers = [];
if (updateLighting) {
this._loadEnvironmentLightingAbortController?.abort(new AbortError("New environment lighting is being loaded before previous environment lighting finished loading."));
const lightingAbortController = (this._loadEnvironmentLightingAbortController = new AbortController());
locks.push(this._loadEnvironmentLightingLock);
internalAbortControllers.push(lightingAbortController);
}
if (updateSkybox) {
this._loadEnvironmentSkyboxAbortController?.abort(new AbortError("New environment skybox is being loaded before previous environment skybox finished loading."));
const skyboxAbortController = (this._loadEnvironmentSkyboxAbortController = new AbortController());
locks.push(this._loadEnvironmentSkyboxLock);
internalAbortControllers.push(skyboxAbortController);
}
// Composite abort fires only when ALL relevant internal aborts fire — a skybox-only
// re-request shouldn't cancel an in-progress lighting load when both were requested
// together.
const compositeAbortController = new AbortController();
const checkAllAborted = () => {
if (internalAbortControllers.every((c) => c.signal.aborted)) {
compositeAbortController.abort(new AbortError(internalAbortControllers.map((controller) => controller.signal.reason).join(" | ")));
}
};
for (const controller of internalAbortControllers) {
controller.signal.addEventListener("abort", checkAllAborted);
}
try {
await AsyncLock.LockAsync(async () => {
throwIfAborted(abortSignal, compositeAbortController.signal);
await this._loadEnvironmentImpl(url?.trim() ?? url, resolvedOptions, abortSignal, compositeAbortController.signal);
}, locks);
}
finally {
for (const controller of internalAbortControllers) {
controller.signal.removeEventListener("abort", checkAllAborted);
}
}
}
get environmentConfig() {
return {
intensity: this._environmentIntensity,
blur: this._environmentBlur,
rotation: this._environmentRotation,
};
}
set environmentConfig(value) {
if (value.blur !== undefined && value.blur !== this._environmentBlur) {
this._environmentBlur = value.blur;
this._applyEnvironmentBlur();
}
if (value.intensity !== undefined && value.intensity !== this._environmentIntensity) {
this._environmentIntensity = value.intensity;
this._applyEnvironmentIntensity();
}
if (value.rotation !== undefined && value.rotation !== this._environmentRotation) {
this._environmentRotation = value.rotation;
this._applyEnvironmentRotation();
}
this.onEnvironmentConfigurationChanged.notifyObservers();
}
get cameraAutoOrbit() {
return {
enabled: this._autoOrbitEnabled,
speed: this._autoOrbitSpeed,
delay: this._autoOrbitDelay,
};
}
set cameraAutoOrbit(value) {
let changed = false;
if (value.enabled !== undefined && value.enabled !== this._autoOrbitEnabled) {
this._autoOrbitEnabled = value.enabled;
this._applyCameraAutoOrbitEnabled();
changed = true;
}
if (value.speed !== undefined && value.speed !== this._autoOrbitSpeed) {
this._autoOrbitSpeed = value.speed;
this._applyCameraAutoOrbitSpeed();
changed = true;
}
if (value.delay !== undefined && value.delay !== this._autoOrbitDelay) {
this._autoOrbitDelay = value.delay;
this._applyCameraAutoOrbitDelay();
changed = true;
}
if (changed) {
this.onCameraAutoOrbitChanged.notifyObservers();
}
}
/**
* The viewer clear color (e.g. background).
*/
get clearColor() {
return this._clearColor;
}
set clearColor(value) {
this._clearColor.r = value.r;
this._clearColor.g = value.g;
this._clearColor.b = value.b;
this._clearColor.a = value.a;
this._applyClearColor();
this.onClearColorChanged.notifyObservers();
}
/**
* The set of defined hotspots.
*/
get hotSpots() {
return this._hotSpots;
}
set hotSpots(value) {
this._hotSpots = value;
this.onHotSpotsChanged.notifyObservers();
}
/**
* @internal The abort signal of the currently in-flight model load, or `undefined` if none.
* Subclasses can use this in `_throwIfDisposedOrAborted` to bail out of dependent async work
* (shadows, environment fallback, etc.) when the user starts a new model load.
*/
get _loadModelAbortSignal() {
return this._loadModelAbortController?.signal;
}
/**
* Loads a 3D model from the specified source.
* @param source The source of the model to load.
* @param options Engine-specific options for loading the model.
* @param abortSignal Optional signal that can be used to abort the load externally.
* @returns A promise that resolves when the model has finished loading.
*/
async loadModel(source, options, abortSignal) {
return await this._updateModel(source, options, abortSignal);
}
/**
* Unloads the current 3D model if one is loaded.
* @param abortSignal Optional signal that can be used to abort the reset.
* @returns A promise that resolves when the current model has been unloaded.
*/
async resetModel(abortSignal) {
return await this._updateModel(undefined, undefined, abortSignal);
}
/**
* @internal Internal helper exposing the model load orchestration with `source: undefined` meaning
* "unload the current model". Subclasses should NOT override this — override `_loadModelImpl` instead.
*/
async _updateModel(source, options, abortSignal) {
this._throwIfDisposedOrAborted(abortSignal);
this._loadModelAbortController?.abort(new AbortError("New model is being loaded before previous model finished loading."));
const internalAbortController = (this._loadModelAbortController = new AbortController());
await this._loadModelLock.lockAsync(async () => {
throwIfAborted(abortSignal, internalAbortController.signal);
await this._loadModelImpl(source, options, abortSignal, internalAbortController.signal);
});
// Post-lock follow-up (e.g. operations that need other locks). Skip if a newer model load
// has already aborted us — the new load owns the post-load work for its own state.
if (!internalAbortController.signal.aborted) {
await this._afterLoadModel(source, options, abortSignal, internalAbortController.signal);
}
}
/**
* @internal Optional post-lock hook invoked AFTER the model load lock is released, allowing
* subclasses to do follow-up work that needs other locks (e.g. environment fallback). The base
* skips this hook if the load was superseded by a newer one before we got here.
*
* Implementations that await additional work should re-check `internalAbortSignal.aborted`
* after each await to avoid acting on stale state (a newer load may have started during the
* await window).
*
* Default: no-op.
*/
async _afterLoadModel(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
source,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
abortSignal,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
internalAbortSignal) {
// Default: no-op.
}
/**
* @internal The abort signal of the currently in-flight shadow update, or `undefined` if none.
* Subclasses can use this in `_throwIfDisposedOrAborted` to bail out of dependent async work
* when the user starts a new shadow update.
*/
get _shadowsAbortSignal() {
return this._shadowsAbortController?.signal;
}
/**
* Gets the current shadow configuration.
*/
get shadowConfig() {
return { quality: this._shadowQuality };
}
/**
* Updates the shadow configuration. Skips work if the requested value matches the currently
* committed one. Subclasses can override this to validate the requested quality (e.g. throw on
* unsupported combinations) before delegating to `super.updateShadows(value, abortSignal)`.
* @param value The new shadow configuration.
* @param abortSignal Optional signal that can be used to abort the update externally.
* @returns A promise that resolves when the shadow update completes.
*/
async updateShadows(value, abortSignal) {
if (value.quality === undefined || value.quality === this._shadowQuality) {
return;
}
// Commit `_shadowQuality` BEFORE running the impl: the engine-specific shadow setup (and
// other handlers that may fire while shadows are being reconfigured, e.g. environment
// rotation) read `this._shadowQuality` as the target value, so it needs to be visible
// during the impl. On failure/abort we roll back below so the public `shadowConfig` stays
// in sync with the actually-applied engine state and a retry with the same quality isn't
// short-circuited by the early-return above.
const previousQuality = this._shadowQuality;
this._shadowQuality = value.quality;
try {
await this._updateShadows(this._shadowQuality, abortSignal);
}
catch (error) {
// Only roll back if a newer `updateShadows` hasn't already committed its own value on
// top of ours; otherwise we'd clobber the newer caller's intent.
if (this._shadowQuality === value.quality) {
this._shadowQuality = previousQuality;
}
throw error;
}
this.onShadowsConfigurationChanged.notifyObservers();
}
/**
* Runs the engine-specific shadow update at the given quality under the shared lock, with
* abort-prev semantics. Subclasses should call this (rather than `_updateShadowsImpl` directly)
* when they need to re-run the shadow setup (e.g. after a model change or environment change).
* The public `updateShadows` also routes through this helper.
* @param quality The shadow quality to apply. Defaults to the currently committed quality
* (`this._shadowQuality`), which is the right choice for re-running shadow setup without
* changing the committed quality. The public `updateShadows` passes a resolved new quality.
* @param abortSignal Optional external abort signal.
* @returns A promise that resolves when the shadow update completes.
*/
async _updateShadows(quality = this._shadowQuality, abortSignal) {
this._throwIfDisposedOrAborted(abortSignal);
this._shadowsAbortController?.abort(new AbortError("Shadows quality is being changed before previous shadows finished initializing."));
const internalAbortController = (this._shadowsAbortController = new AbortController());
await this._updateShadowsLock.lockAsync(async () => {
throwIfAborted(abortSignal, internalAbortController.signal);
await this._updateShadowsImpl(quality, abortSignal, internalAbortController.signal);
});
}
/**
* Disposes the viewer and releases shared resources (observables, disposed flag).
* Subclasses MUST override this method to dispose their own engine-specific state
* (engine, scene, abort controllers, models, etc.) and call `super.dispose()` last
* so observable consumers see the engine-specific notifications before observables clear.
*
* Subclasses should also early-return if `_isDisposed` is already true.
*/
dispose() {
this._loadEnvironmentLightingAbortController?.abort(new AbortError("The viewer is being disposed."));
this._loadEnvironmentLightingAbortController = null;
this._loadEnvironmentSkyboxAbortController?.abort(new AbortError("The viewer is being disposed."));
this._loadEnvironmentSkyboxAbortController = null;
this._loadModelAbortController?.abort(new AbortError("The viewer is being disposed."));
this._loadModelAbortController = null;
this._shadowsAbortController?.abort(new AbortError("The viewer is being disposed."));
this._shadowsAbortController = null;
this.onEnvironmentChanged.clear();
this.onEnvironmentConfigurationChanged.clear();
this.onEnvironmentError.clear();
this.onShadowsConfigurationChanged.clear();
this.onPostProcessingChanged.clear();
this.onModelChanged.clear();
this.onModelError.clear();
this.onLoadingProgressChanged.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.onAfterRenderObservable.clear();
this.onClearColorChanged.clear();
this._isDisposed = true;
}
// ── Reset orchestration ──
/**
* 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.
* - "shadow": Reset shadow 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);
}
/**
* @internal
* Orchestrates the reset operation in canonical flag order. The {@link interpolate} parameter is
* forwarded to per-flag hooks (currently only `_resetCamera`) so internal callers can reset
* without camera animation.
*/
_reset(interpolate, ...flags) {
const all = flags.length === 0;
if (all || flags.includes("source")) {
this._resetModel();
}
if (all || flags.includes("environment")) {
this._resetEnvironment();
}
if (all || flags.includes("shadow")) {
this._resetShadows();
}
if (all || flags.includes("animation")) {
this._resetAnimation();
}
if (all || flags.includes("camera")) {
this._resetCamera(interpolate);
}
if (all || flags.includes("post-processing")) {
this._resetPostProcessing();
}
if (all || flags.includes("material-variant")) {
this._resetMaterialVariant();
}
}
/**
* @internal Resets the loaded model to the source specified at construction (or no model if no source was specified).
*/
_resetModel() {
observePromise(this._updateModel(this._options?.source));
}
/**
* @internal Resets the shadow configuration to the value specified at construction.
*/
_resetShadows() {
observePromise(this.updateShadows({ quality: this._options?.shadowConfig?.quality ?? DefaultViewerBaseOptions.shadowConfig.quality }));
}
/**
* @internal Resets the selected material variant to the value specified at construction (or null if not specified).
*/
_resetMaterialVariant() {
this.selectedMaterialVariant = this._options?.selectedMaterialVariant ?? null;
}
}
// eslint-disable-next-line @typescript-eslint/promise-function-async
const LazySSAODependenciesPromise = new Lazy(() => Promise.all([
import('@babylonjs/core/PostProcesses/RenderPipeline/Pipelines/ssao2RenderingPipeline.js'),
import('@babylonjs/core/Rendering/prePassRendererSceneComponent.js'),
import('@babylonjs/core/Rendering/geometryBufferRendererSceneComponent.js'),
import('@babylonjs/core/Engines/Extensions/engine.multiRender.js'),
import('@babylonjs/core/Engines/WebGPU/Extensions/engine.multiRender.js'),
]));
const WebGPUSnapshotRenderingLoggingEnabled = false;
// Logger.LogLevels = Logger.AllLogLevel;
// TODO: Consider moving this to core after the 9.0 release.
async function WhenNext(observable, abortSignal) {
return await new Promise((resolve, reject) => {
if (abortSignal.aborted) {
reject(new AbortError("Aborted"));
return;
}
const observer = observable.addOnce((payload) => {
abortSignal.removeEventListener("abort", onAbort);
resolve(payload);
});
const onAbort = () => {
observer.remove();
reject(new AbortError("Aborted"));
};
abortSignal.addEventListener("abort", onAbort, { once: true });
});
}
function IsGaussianSplattingMesh(mesh) {
const className = mesh.getClassName();
return IsGaussianSplattingClassName(className) || className === "GaussianSplattingPartProxyMesh";
}
function IsPBRMaterial(material) {
const cn = material.getClassName();
return cn.startsWith("PBR") || cn === "OpenPBRMaterial";
}
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);
}
/**
* 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 = DefaultViewerBaseOptions;
/**
* 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 extends ViewerBase {
/**
* Gets or sets the clear color (background color) of the viewer.
*/
/** @internal */
_applyClearColor() {
this._scene.clearColor.r = this._clearColor.r;
this._scene.clearColor.g = this._clearColor.g;
this._scene.clearColor.b = this._clearColor.b;
this._scene.clearColor.a = this._clearColor.a;
this._markSceneMutated();
}
/**
* True if a model is currently loaded.
*/
get isModelLoaded() {
return this._activeModelBacking !== null;
}
/**
* Called whenever the filtered object lists managed by an active frame graph are recomputed.
* Each entry in the array corresponds to the filter at the same index passed to
* {@link _setActiveFrameGraph}. Called once immediately when _setActiveFrameGraph is called,
* and again after every model change. Override in a subclass to wire ObjectList input blocks
* or perform other per-model setup without managing observer lifetimes manually.
* @param _objectLists The filtered lists of meshes corresponding to the filters passed to _setActiveFrameGraph.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_onObjectListsUpdated(_objectLists) { }
constructor(_engine, _options) {
super();
this._engine = _engine;
this._options = _options;
/**
* When enabled, the Viewer will emit additional diagnostic logs to the console.
*/
this.showDebugLogs = false;
this._snapshotHelper = null;
// Lazily created, viewer-scoped "clay" material assigned to loaded meshes that have no material
// (e.g. an OBJ with no MTL, or an STL). It is created on first use and disposed with the scene.
// We cache the promise (not the material) so that concurrent callers are de-duplicated and only
// a single material is ever created, without relying on any external lock.
this._defaultMaterialPromise = null;
this._renderedLastFrame = null;
this._isIdle = false;
this._sceneOptimizer = null;
this._tempVectors = BuildTuple(4, Vector3.Zero);
this._meshDataCache = new Map();
this._beforeRenderObserver = null;
this._renderLoopController = null;
this._loadedModelsBacking = [];
this._activeModelBacking = null;
this._environmentSkyboxMode = "none";
this._environmentLightingMode = "none";
this._skybox = null;
this._skyboxTexture = null;
this._reflectionTexture = null;
this._light = null;
this._ssaoOption = this._options?.postProcessing?.ssao ?? DefaultViewerOptions.postProcessing.ssao;
this._ssaoPipeline = null;
this._autoSuspendRendering = this._options?.autoSuspendRendering ?? DefaultViewerOptions.autoSuspendRendering;
this._sceneMutated = false;
this._suspendRenderCount = 0;
this._camerasAsHotSpotsAbortController = null;
this._updateSSAOLock = new AsyncLock();
this._ssaoAbortController = null;
this._activeAnimationObservers = [];
this._animationSpeed = this._options?.animationSpeed ?? DefaultViewerOptions.animationSpeed;
this._camerasAsHotSpots = false;
this._shadowState = {};
this._iblShadowsAnimationObserver = null;
this._objectListFilters = [];
this._objectListModelChangedObserver = null;
if (this._options?.shadowConfig?.quality === "high" && this._options?.postProcessing?.ssao === "enabled") {
throw new Error("High quality shadows are not compatible with SSAO. Please choose either high quality shadows or SSAO.");
}
this._shadowQuality = this._options?.shadowConfig?.quality ?? DefaultViewerOptions.shadowConfig.quality;
this._environmentIntensity = this._options?.environmentConfig?.intensity ?? DefaultViewerOptions.environmentConfig.intensity;
this._environmentBlur = this._options?.environmentConfig?.blur ?? DefaultViewerOptions.environmentConfig.blur;
this._environmentRotation = this._options?.environmentConfig?.rotation ?? DefaultViewerOptions.environmentConfig.rotation;
if (this._options?.hotSpots) {
this.hotSpots = this._options.hotSpots;
}
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();
this._clearColor.a = scene.clearColor.a;
this._clearColor.r = scene.clearColor.r;
this._clearColor.g = scene.clearColor.g;
this._clearColor.b = scene.clearColor.b;
this.onClearColorChanged.notifyObservers();
});
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 = WebGPUSnapshotRenderingLoggingEnabled;
this._beforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => {
this._snapshotHelper?.updateMesh(this._scene.meshes);
});
}
this._camera.attachControl();
this._autoRotationBehavior = this._camera.getBehaviorByName("AutoRotation");
// Sync engine state to base field defaults. The subsequent `_reset(false, "camera")` will then
// apply user-provided option overrides via the cameraAutoOrbit setter (with change-detection).
this._applyCameraAutoOrbitEnabled();
this._applyCameraAutoOrbitSpeed();
this._applyCameraAutoOrbitDelay();
this._scene.onAfterRenderObservable.add(() => {
this.onAfterRenderObservable.notifyObservers();
});
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),
get isIdle() {
return viewer._isIdle;
},
});
this._reset(false, "source", "environment", "post-processing");
}
/**
* The camera auto orbit configuration.
*/
/** @internal */
_applyCameraAutoOrbitEnabled() {
if (this._autoOrbitEnabled) {
this._camera.addBehavior(this._autoRotationBehavior);
}
else {
this._camera.removeBehavior(this._autoRotationBehavior);
}
}
/** @internal */
_applyCameraAutoOrbitSpeed() {
this._autoRotationBehavior.idleRotationSpeed = this._autoOrbitSpeed;
}
/** @internal */
_applyCameraAutoOrbitDelay() {
this._autoRotationBehavior.idleRotationWaitTime = this._autoOrbitDelay;
}
/**
* Get the current environment configuration.
*/
/** @internal */
_applyEnvironmentBlur() {
if (this._skybox) {
const material = this._skybox.material;
if (material instanceof BackgroundMaterial) {
this._snapshotHelper?.disableSnapshotRendering();
material.reflectionBlur = this._environmentBlur;
this._snapshotHelper?.enableSnapshotRendering();
this._markSceneMutated();
}
}
}
/** @internal */
_applyEnvironmentRotation() {
this._snapshotHelper?.disableSnapshotRendering();
if (this._skyboxTexture) {
this._skyboxTexture.rotationY = this._environmentRotation;
}
if (this._reflectionTexture) {
this._reflectionTexture.rotationY = this._environmentRotation;
}
this._snapshotHelper?.enableSnapshotRendering();
this._markSceneMutated();
// Side effect: shadow light follows environment rotation in normal/high modes.
this._rotateShadowLightWithEnvironment();
}
/** @internal */
_applyEnvironmentIntensity() {
this._snapshotHelper?.disableSnapshotRendering();
if (this._skyboxTexture) {
this._skyboxTexture.level = this._environmentIntensity;
}
if (this._reflectionTexture) {
this._reflectionTexture.level = this._environmentIntensity;
}
this._snapshotHelper?.enableSnapshotRendering();
this._markSceneMutated();
// Side effect: high-quality (IBL) shadows reset their accumulation when intensity changes.
this._changeShadowLightIntensity();
}
/**
* Updates the shadow configuration.
* @param value The new shadow configuration.
* @param abortSignal Optional signal that can be used to abort the update externally.
* @returns A promise that resolves when the shadow update completes.
*/
async updateShadows(value, abortSignal) {
if (value.quality === "high" && this._ssaoOption === "enabled") {
throw new Error("Shadows quality cannot be set to high when SSAO is enabled.");
}
return await super.updateShadows(value, abortSignal);
}
_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,
ssao: this._ssaoOption,
};
}
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;
}
if (value.ssao && this._ssaoOption !== value.ssao) {
if (value.ssao === "enabled" && this._shadowQuality === "high") {
throw new Error("SSAO cannot be enabled when shadows quality is set to high.");
}
this._ssaoOption = value.ssao;
this._updateSSAOPipeline();
}
this._scene.imageProcessingConfiguration.isEnabled = this._toneMappingEnabled || this._contrast !== 1 || this._exposure !== 1 || this._ssaoPipeline !== null;
this._snapshotHelper?.enableSnapshotRendering();
this._markSceneMutated();
}
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._shadowQuality));
this._updateSSAOPipeline();
this._applyAnimationSpeed();
this._selectAnimation(0, false);
this.onSelectedMaterialVariantChanged.notifyObservers();
this._reframeCamera(true, model ? [model] : undefined);
this.onModelChanged.notifyObservers(options?.source ?? null);
}
}
async _enableSSAOPipeline(abortSignal) {
if (!this._ssaoPipeline) {
const [{ SSAO2RenderingPipeline }] = await LazySSAODependenciesPromise.value;
this._throwIfDisposedOrAborted(abortSignal);
this._scene.postProcessRenderPipelineManager.onNewPipelineAddedObservable.addOnce((pipeline) => {
if (pipeline.name === "ssao") {
this.onPostProcessingChanged.notifyObservers();
}
});
this._scene.postProcessRenderPipelineManager.onPipelineRemovedObservable.addOnce((pipeline) => {
if (pipeline.name === "ssao") {
this.onPostProcessingChanged.notifyObservers();
}
});
const ssaoRatio = {
ssaoRatio: 1,
blurRatio: 1,
};
let ssaoPipeline = null;
try {
ssaoPipeline = new SSAO2RenderingPipeline("ssao", this._scene, ssaoRatio);
const worldBounds = this._getWorldBounds(this._loadedModels);
if (worldBounds) {
const size = Vector3.FromArray(worldBounds.size).length();
ssaoPipeline.expensiveBlur = true;
ssaoPipeline.maxZ = size * 2;
// arbitrary max size to cap SSAO settings
const maxSceneSize = 50;
ssaoPipeline.radius = Clamp(Lerp(1, 5, Clamp((size - 1) / maxSceneSize, 0, 1)), 1, 5);
ssaoPipeline.totalStrength = Clamp(Lerp(0.3, 1.0, Clamp((size - 1) / maxSceneSize, 0, 1)), 0.3, 1.0);
ssaoPipeline.samples = Math.round(Clamp(Lerp(8, 32, Clamp((size - 1) / maxSceneSize, 0, 1)), 8, 32));
}
// Wait for the SSAO pipeline to be ready before attaching it to the camera.
while (!ssaoPipeline.isReady()) {
// eslint-disable-next-line no-await-in-loop
await WhenNext(this._scene.onAfterRenderObservable, abortSignal);
}
this._throwIfDisposedOrAborted(abortSignal);
this._ssaoPipeline = ssaoPipeline;
this._scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline("ssao", this._camera);
}
catch (error) {
ssaoPipeline?.dispose();
throw error;
}
}
}
_disableSSAOPipeline() {
if (this._ssaoPipeline) {
this._scene.postProcessRenderPipelineManager.detachCamerasFromRenderPipeline("ssao", this._camera);
this._scene.postProcessRenderPipelineManager.removePipeline("ssao");
this._ssaoPipeline?.dispose();
this._ssaoPipeline = null;
}
}
_updateSSAOPipeline() {
// Always abort any in-flight SSAO init first, regardless of frame graph state.
this._ssaoAbortController?.abort(new AbortError("SSAO pipeline is being updated."));
this._ssaoAbortController = null;
observePromise((async () => {
this._ssaoAbortController = new AbortController();
const abortSignal = this._ssaoAbortController.signal;
await this._updateSSAOLock.lockAsync(async () => {
let shouldEnable = this._ssaoOption === "enabled" && !this._scene.frameGraph;
if (this._ssaoOption === "auto" && !this._scene.frameGraph) {
const hasModels = this._loadedModels.length > 0;
const hasMaterials = this._loadedModels.some((model) => model.assetContainer.materials.length > 0);
const iblShadowsEnabled = this._shadowQuality === "high";
const allMeshesAreSplats = hasModels &&
this._loadedModels.every((model) => {
const meshes = model.assetContainer.meshes;
return meshes.length > 0 && meshes.every(IsGaussianSplattingMesh);
});
shouldEnable = hasModels && !hasMaterials && !iblShadowsEnabled && !allMeshesAreSplats;
}
if (shouldEnable) {
await this._enableSSAOPipeline(abortSignal);
}
else {
this._disableSSAOPipeline();
}
});
})());
}
/**
* Applies the specified FrameGraph to the scene, or clears the active frame graph when null.
* Call this from a derived class after constructing and building a FrameGraph.
*
* Optionally accepts an array of mesh filter predicates. For each predicate, the viewer
* maintains a filtered list of scene meshes and calls {@link _onObjectListsUpdated} with all
* lists whenever the active model changes. The method is also called immediately on this call
* so the caller can perform initial wiring without a separate code path.
*
* @param frameGraph The FrameGraph to activate, or null to revert to default rendering.
* @param filters Optional array of predicates — one per ObjectList input block to populate.
*/
_setActiveFrameGraph(frameGraph, filters = []) {
// Tear down any existing object-list tracking.
this.onModelChanged.remove(this._objectListModelChangedObserver);
this._objectListModelChangedObserver = null;
this._objectListFilters = [];
this._scene.frameGraph = frameGraph;
// Always re-evaluate SSAO: tear it down when a frame graph is active,
// rebuild it when the frame graph is cleared.
this._updateSSAOPipeline();
if (!frameGraph) {
return;
}
this._objectListFilters = filters;
const notify = () => {
this._onObjectListsUpdated(this._objectListFilters.map((filter) => this._scene.meshes.filter(filter)));
};
// Fire immediately so the caller can wire up ObjectList blocks in one place.
notify();
// Re-fire after every model change so lists stay current as models are swapped.
this._objectListModelChangedObserver = this.onModelChanged.add(notify);
}
/**
* 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();
this._triggerIblShadowsVoxelization();
}
}
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?.selectedMaterialVariant ?? null;
}
set selectedMaterialVariant(value) {
if (this._activeModel && value) {
this._activeModel.selectedMaterialVariant = value;
}
}
/**
* 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();
}
}
/**
* Lazily creates (on first use) and returns a viewer-scoped "clay" PBR material used for loaded meshes
* that have no material of their own. The material is created in the viewer's scene and is therefore
* disposed together with the scene when the viewer is disposed. PBRMaterial is dynamically imported so
* that it is only pulled into the bundle when a material-less model is actually loaded.
* @returns The default "clay" material.
*/
async _getDefaultMaterialAsync() {
// Cache the promise so concurrent callers share a single in-flight creation and we never
// create (and leak) more than one material.
this._defaultMaterialPromise ?? (this._defaultMaterialPromise = (async () => {
const { PBRMaterial } = await import('@babylonjs/core/Materials/PBR/pbrMaterial.js');
const defaultMaterial = new PBRMaterial("Viewer Default Material", this._scene);
defaultMaterial.albedoColor = new Color3(0.4, 0.4, 0.4);
defaultMaterial.metallic = 0;
defaultMaterial.roughness = 1;
defaultMaterial.baseDiffuseRoughness = 1;
defaultMaterial.microSurface = 0;
return defaultMaterial;
})());
return await this._defaultMaterialPromise;
}
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;
// Fall back to the viewer-level plugin extension (e.g. the <babylon-viewer extension="..."> attribute)
// when a per-load extension isn't provided. This is needed for the construction-time model load and for
// sources whose extension cannot be inferred from the URL (e.g. data URLs or extension-less URLs).
if (!options?.pluginExtension && this._options?.pluginExtension) {
options = options ?? {};
options.pluginExtension = this._options.pluginExtension;
}
// Detect SPZ files and set the appropriate plugin extension and options.
if (!options?.pluginExtension) {
let isSpz = false;
if (typeof source === "string") {
const extension = GetExtensionFromUrl(source);
if (extension && extension.toLowerCase() === ".spz") {
isSpz = true;
}
}
else if (source instanceof File) {
if (source.name.toLowerCase().endsWith(".spz")) {
isSpz = true;
}
}
if (isSpz) {
options = options ?? {};
options.pluginExtension = ".spz";
}
}
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,
useOpenPBR: options?.useOpenPBR ?? this._options?.useOpenPBR ?? DefaultViewerOptions.useOpenPBR,
extensionOptions: {
KHR_materials_variants: {
// Capture the material variants controller when it is loaded.
onLoaded: onMaterialVariantsLoaded,
},
},
},
// SPZ files are authored in RUB (Y-up) convention. SPLATFileLoader normally inverts Y
// when flipY is falsy, so we set flipY: true here to prevent that default inversion and
// keep the content Y-up as authored.
...(options?.pluginExtension === ".spz" ? { splat: { flipY: true } } : {}),
},
};
options = deepMerge(defaultOptions, options ?? {});
this._snapshotHelper?.disableSnapshotRendering();
try {
const assetContainer = await LoadAssetContainerAsync(source, this._scene, options);
RemoveUnreferencedVerticesData(assetContainer.meshes.filter((mesh) => mesh instanceof Mesh));
// Meshes with no material (e.g. an OBJ with no MTL, or an STL) would otherwise fall back to the
// engine's default StandardMaterial, which the viewer does not light (it relies on image-based
// lighting). Assign a lazily-created "clay" PBR material so these meshes are shaded consistently.
const materiallessMeshes = assetContainer.meshes.filter((mesh) => !mesh.material);
if (materiallessMeshes.length > 0) {
const defaultMaterial = await this._getDefaultMaterialAsync();
for (const mesh of materiallessMeshes) {
mesh.material = defaultMaterial;
}
}
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(viewer._shadowQuality));
if (activeAnimation) {
activeAnimation.goToFrame(0);
activeAnimation.play(true);
if (!startAnimation) {
activeAnimation.pause();
}
}
},
makeActive: (options) => {
this._setActiveModel(model, options);
},
set selectedMaterialVariant(variantName) {
if (materialVariantsController) {
let value = variantName;
if (!value) {
value = materialVariantsController.variants[0];
}
if (value !== materialVariantsController.selectedVariant && materialVariantsController.variants.includes(value)) {
viewer._snapshotHelper?.disableSnapshotRendering();
materialVariantsController.selectedVariant = value;
viewer._snapshotHelper?.enableSnapshotRendering();
viewer._markSceneMutated();
viewer.onSelectedMaterialVariantChanged.notifyObservers();
}
}
},
get selectedMaterialVariant() {
if (materialVariantsController) {
return materialVariantsController.selectedVariant;
}
return null;
},
};
this._loadedModelsBacking.push(model);
return model;
}
catch (e) {
this.onModelError.notifyObservers(e);
throw e;
}
finally {
loadOperation.dispose();
this._snapshotHelper?.enableSnapshotRendering();
this._markSceneMutated();
}
}
/**
* Loads a 3D model from the specified source.
* @param source The source of the model to load.
* @param options Options for loading the model. See {@link LoadModelOptions}.
* @param abortSignal Optional signal that can be used to abort the load externally.
* @returns A promise that resolves when the model has finished loading.
*/
async loadModel(source, options, abortSignal) {
return await super.loadModel(source, options, abortSignal);
}
/** @internal */
async _loadModelImpl(source, options, abortSignal, internalAbortSignal) {
this._activeModel?.dispose();
this._activeModelBacking = null;
this.selectedAnimation = -1;
if (source) {
const model = await this._loadModel(source, options, internalAbortSignal);
// Re-check abort after the long-running load — a newer loadModel may have superseded us.
throwIfAborted(abortSignal, internalAbortSignal);
model.makeActive(Object.assign({ source, interpolateCamera: false }, options));
this._reset(false, "camera", "animation", "material-variant");
}
}
/** @internal */
async _afterLoadModel(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
source,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options, abortSignal, internalAbortSignal) {
// If PBR is used and an environment texture is not already loaded, then load the default environment.
// This includes the implicit case where a mesh had no material and was assigned the viewer's default
// "clay" PBR material during load, which is why we also inspect the meshes' assigned materials (that
// material is viewer-scoped and therefore not part of any model's assetContainer.materials).
const hasPBRMaterials = this._loadedModels.some((model) => model.assetContainer.materials.some(IsPBRMaterial) || model.assetContainer.meshes.some((mesh) => mesh.material != null && IsPBRMaterial(mesh.material)));
if (!this._scene.environmentTexture && hasPBRMaterials) {
// Combine the caller's external `abortSignal` with our `internalAbortSignal` so the
// fallback environment load aborts on either: caller cancellation OR a newer loadModel
// superseding us. Without this OR, a stale fallback could mutate the scene before the
// post-await `internalAbortSignal.aborted` check runs.
const fallbackAbortSignal = abortSignal ? AbortSignal.any([abortSignal, internalAbortSignal]) : internalAbortSignal;
await this.resetEnvironment({ lighting: true }, fallbackAbortSignal);
if (internalAbortSignal.aborted) {
throw new AbortError(internalAbortSignal.reason);
}
}
this._startSceneOptimizer(true);
}
/** @internal */
async _updateShadowsImpl(quality, abortSignal, internalAbortSignal) {
this._snapshotHelper?.disableSnapshotRendering();
try {
if (quality === "none") {
this._disposeShadows();
}
else {
// make sure there is an env light before creating shadows
if (!this._reflectionTexture) {
// Combine the caller's external `abortSignal` with `internalAbortSignal` so this
// auto-environment load aborts if either the caller cancels or a newer shadow
// update supersedes us.
const envAbortSignal = abortSignal ? AbortSignal.any([abortSignal, internalAbortSignal]) : internalAbortSignal;
await this.loadEnvironment("auto", { lighting: true, skybox: false }, envAbortSignal);
}
if (quality === "normal") {
await this._updateShadowMap(internalAbortSignal);
}
else if (quality === "high") {
await this._updateEnvShadow(internalAbortSignal);
}
}
}
finally {
this._snapshotHelper?.enableSnapshotRendering();
this._markSceneMutated();
}
}
_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._environmentRotation);
}
}
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) {
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._loadModelAbortSignal, this._loadEnvironmentLightingAbortSignal, this._loadEnvironmentSkyboxAbortSignal);
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);
this._shadowState.high = high;
this._startIblShadowsRenderTime();
// Start the per-frame update loop if an animation is already playing.
if (this.isAnimationPlaying) {
this._startIblShadowsAnimationUpdate();
}
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) {
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._loadModelAbortSignal, this._loadEnvironmentLightingAbortSignal, this._loadEnvironmentSkyboxAbortSignal);
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._loadModelAbortSignal, this._loadEnvironmentLightingAbortSignal, this._loadEnvironmentSkyboxAbortSignal);
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;
shadowMap.renderList = this._scene.meshes.slice();
}
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;
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());
},
});
await new Promise((resolve, reject) => {
_RetryWithInterval(() => shadowMap.isReadyForRendering(), () => resolve(void 0), () => reject(new Error("Failed to get shadow map generator ready")));
});
// 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._environmentRotation);
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._stopIblShadowsAnimationUpdate();
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();
}
/**
* 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(IsPBRMaterial)) {
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();
}
/** @internal */
async _loadEnvironmentImpl(url, options, abortSignal, compositeAbortSignal) {
const getDefaultEnvironmentUrlAsync = async () => (await import('./defaultEnvironment-5jBs1zfd.js')).default;
const whenTextureLoadedAsync = async (cubeTexture) => {
await new Promise((resolve, reject) => {
const successObserver = cubeTexture.onLoadObservable.addOnce(() => {
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) {
if (lightingUrl) {
// Load the new reflection texture before disposing the old one.
const oldReflectionTexture = this._reflectionTexture;
if (lightingUrl === this._skyboxTexture?.url) {
// If the lighting url is the same as the skybox url, clone the skybox texture.
const environmentTexture = this._skyboxTexture.clone();
environmentTexture.coordinatesMode = Texture.CUBIC_MODE;
this._setEnvironmentLighting(environmentTexture);
}
else {
// Otherwise, create a new cube texture from the lighting url.
let lightingOptions = options;
if (this._environmentLightingMode === "auto") {
lightingOptions = { ...lightingOptions, extension: ".env" };
}
const lightingTexture = await createCubeTexture(lightingUrl, this._scene, lightingOptions.extension);
newTexturePromises.push(whenTextureLoadedAsync(lightingTexture));
this._setEnvironmentLighting(lightingTexture);
}
oldReflectionTexture?.dispose();
}
else {
// No new lighting url — dispose the old texture and clear.
this._reflectionTexture?.dispose();
this._reflectionTexture = null;
this._scene.environmentTexture = null;
}
}
// If the skybox url is not the same as the current skybox url, load the new skybox texture.
if (skyboxUrl !== this._skyboxTexture?.url) {
if (skyboxUrl) {
// Load the new skybox texture before disposing the old one.
const oldSkybox = this._skybox;
const oldSkyboxTexture = this._skyboxTexture;
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.
let skyboxOptions = options;
if (this._environmentSkyboxMode === "auto") {
skyboxOptions = { ...skyboxOptions, extension: ".env" };
}
const skyboxTexture = await createCubeTexture(skyboxUrl, this._scene, skyboxOptions.extension);
newTexturePromises.push(whenTextureLoadedAsync(skyboxTexture));
this._setEnvironmentSkybox(skyboxTexture);
}
oldSkybox?.dispose(undefined, true);
oldSkyboxTexture?.dispose();
}
else {
// No new skybox url — dispose and clear.
this._skybox?.dispose(undefined, true);
this._skyboxTexture = null;
this._skybox = null;
this._updateAutoClear();
}
}
await Promise.all(newTexturePromises);
throwIfAborted(abortSignal, compositeAbortSignal);
this._updateLight();
observePromise(this._updateShadows(this._shadowQuality));
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);
this._startIblShadowsAnimationUpdate();
}
/**
* Pauses the selected animation if there is one.
*/
async pauseAnimation() {
this._activeAnimation?.pause();
this._stopIblShadowsAnimationUpdate();
this._triggerIblShadowsVoxelization();
}
/**
* Triggers a single IBL shadows voxelization pass.
* If a voxelization is already in progress, a dirty flag is set and a new pass
* will automatically run once the current one completes, ensuring the final state
* is always rendered regardless of how many requests arrive in the meantime.
*/
_triggerIblShadowsVoxelization() {
if (this._shadowState.high) {
if (this._shadowState.high.voxelizationInProgress) {
this._shadowState.high.voxelizationDirty = true;
return;
}
this._shadowState.high.voxelizationInProgress = true;
this._shadowState.high.pipeline.updateSceneBounds();
this._shadowState.high.pipeline.updateVoxelization();
this._shadowState.high.pipeline.onVoxelizationCompleteObservable.addOnce(() => {
if (this._shadowState.high) {
this._shadowState.high.voxelizationInProgress = false;
this._shadowState.high.pipeline.resetAccumulation();
if (this._shadowState.high.voxelizationDirty) {
this._shadowState.high.voxelizationDirty = false;
this._triggerIblShadowsVoxelization();
return;
}
}
this._startIblShadowsRenderTime();
});
}
}
/**
* Starts the per-frame update loop for IBL shadows while an animation is playing.
*/
_startIblShadowsAnimationUpdate() {
if (this._shadowState.high && !this._iblShadowsAnimationObserver) {
let frame = 0;
this._iblShadowsAnimationObserver = this._scene.onAfterAnimationsObservable.add(() => {
const highState = this._shadowState.high;
if (!highState) {
return;
}
if (frame++ % 2 !== 0) {
return;
}
if (highState.voxelizationInProgress) {
return;
}
highState.voxelizationInProgress = true;
highState.pipeline.updateVoxelization();
highState.pipeline.onVoxelizationCompleteObservable.addOnce(() => {
// Ensure the high shadow state is still valid and matches the one we started with
if (!this._shadowState.high || this._shadowState.high !== highState) {
return;
}
highState.voxelizationInProgress = false;
highState.pipeline.resetAccumulation();
this._startIblShadowsRenderTime();
});
});
}
}
_stopIblShadowsAnimationUpdate() {
if (this._iblShadowsAnimationObserver) {
this._scene.onAfterAnimationsObservable.remove(this._iblShadowsAnimationObserver);
this._iblShadowsAnimationObserver = null;
}
}
/**
* 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);
}
/** @internal */
_resetEnvironment() {
const cc = this._options?.clearColor ?? DefaultViewerOptions.clearColor;
this.clearColor = { r: cc[0], g: cc[1], b: cc[2], a: cc[3] ?? 1 };
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 }));
}
}
/** @internal */
_resetAnimation() {
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();
}
}
/** @internal */
_resetCamera(interpolate) {
// 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,
};
}
/** @internal */
_resetPostProcessing() {
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,
ssao: this._options?.postProcessing?.ssao ?? DefaultViewerOptions.postProcessing.ssao,
};
}
/**
* Disposes of the resources held by the Viewer.
*/
dispose() {
if (this._isDisposed) {
return;
}
this.selectedAnimation = -1;
this.animationProgress = 0;
this._camerasAsHotSpotsAbortController?.abort(new AbortError("The viewer is being disposed."));
this._ssaoAbortController?.abort(new AbortError("The viewer is being disposed."));
this._renderLoopController?.dispose();
this._activeModel?.dispose();
this._loadedModelsBacking.forEach((model) => model.dispose());
this._disposeShadows();
this._scene.dispose();
this._imageProcessingConfigurationObserver.remove();
this._beforeRenderObserver?.remove();
this._snapshotHelper?.dispose();
this.onModelChanged.remove(this._objectListModelChangedObserver);
// Base disposes observables and sets _isDisposed = true
super.dispose();
}
/**
* 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._getHotSpotToRef(this._loadedModels.flatMap((model) => model.assetContainer.meshes), query, result);
}
_getHotSpotToRef(...args) {
const [meshesOrAssetContainer, query, result] = args;
const meshes = Array.isArray(meshesOrAssetContainer) ? meshesOrAssetContainer : meshesOrAssetContainer?.meshes;
const worldNormal = this._tempVectors[2];
const worldPos = this._tempVectors[1];
const screenPos = this._tempVectors[0];
if (query.type === "surface") {
const mesh = 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. The SSAO pipeline is not yet in a ready state.
// 7. At least one model should render (playing animations).
// 8. A model (or other asset) load is in flight.
return (!this._autoSuspendRendering ||
this._sceneMutated ||
this._snapshotHelper?.isReady === false ||
this._shadowState.normal?.shouldRender ||
this._shadowState.high?.shouldRender ||
this._ssaoPipeline?.isReady() === false ||
this._loadedModelsBacking.some((model) => model._shouldRender()) ||
this._loadOperations.size > 0);
}
_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");
this._isIdle = false;
// 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._isIdle = true;
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();
// NOTE: this logic to adjust camera parameters based on radius is copied in renderingZone.tsx (for sandbox).
// Please keep them in sync.
// Update the camera panning sensitivity based on the camera's distance from the target.
this._camera.panningSensibility = 5000 / this._camera.radius;
// Update the camera speed based on the camera's distance from the target.
// TODO: This makes mouse wheel zooming behave well, but makes mouse based rotation a bit worse.
this._camera.speed = this._camera.radius * 0.2;
// Update the keyboard zooming sensitivity based on the camera's distance from the target.
this._camera.inputs.attached["keyboard"].zoomingSensibility = 500 / this._camera.radius;
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) {
const goalTarget = Vector3.Zero();
let goalAlpha = FramingCameraAlpha;
let goalBeta = FramingCameraBeta;
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;
const 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 hasNonPBRMaterials = this._loadedModels.some((model) => model.assetContainer.materials.some((material) => !IsPBRMaterial(material)));
if (hasModelProvidedLights) {
shouldHaveDefaultLight = false;
}
else {
shouldHaveDefaultLight = !hasImageBasedLighting || 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);
}
}
}
(() => {
registerBuiltInLoaders();
})();
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {};
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
}
function __runInitializers(thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
}
function __setFunctionName(f, name, prefix) {
if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
}
function __classPrivateFieldGet(receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
}
function __classPrivateFieldSet(receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* Suspends rendering while a canvas is scrolled out of the viewport.
* @remarks
* Shared by the full and Lite Viewer canvas factories. The two flavors implement suspension very differently
* (full disposes a render loop controller synchronously, Lite stops the engine's rAF loop under an async
* lock), but the observer wiring and the pairing of suspend/resume against intersection changes are identical,
* so only the `suspendRendering` callback differs.
* @param canvas The canvas whose visibility should drive rendering.
* @param suspendRendering Called when the canvas leaves the viewport; the returned disposable is disposed when it comes back.
* @returns A disposable that stops observing the canvas. Must be disposed along with the Viewer.
*/
function SuspendRenderingWhenOffscreen(canvas, suspendRendering) {
let offscreenRenderingSuspension = null;
const intersectionObserver = new IntersectionObserver((entries) => {
if (entries.length > 0) {
if (entries[entries.length - 1].isIntersecting) {
offscreenRenderingSuspension?.dispose();
offscreenRenderingSuspension = null;
}
else {
// `??=` rather than `=`: repeated non-intersecting records must not acquire a second
// suspension, which would leak a reference and leave rendering suspended forever.
offscreenRenderingSuspension ?? (offscreenRenderingSuspension = suspendRendering());
}
}
});
intersectionObserver.observe(canvas);
return { dispose: () => intersectionObserver.disconnect() };
}
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() {
// 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 "WebGPU": {
const { WebGPUEngine } = await import('@babylonjs/core/Engines/webgpuEngine.js');
const webGPUEngine = new WebGPUEngine(canvas, options);
try {
await webGPUEngine.initAsync();
engine = webGPUEngine;
break;
}
catch {
Logger.Warn("Failed to initialize WebGPU engine. Falling back to WebGL.");
}
}
// eslint-disable-next-line no-fallthrough
case "WebGL": {
const { Engine } = await import('@babylonjs/core/Engines/engine.js');
engine = new Engine(canvas, undefined, options);
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.
disposeActions.push(SuspendRenderingWhenOffscreen(canvas, () => details.suspendRendering()).dispose);
}
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;
}
/**
* @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$6=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$6(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$5,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$2=(t,s)=>!i$3(t,s),b$1={attribute:true,type:String,converter:u$1,reflect:false,useDefault:false,hasChanged:f$2};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$1){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$1}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$5(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;const r=h.fromAttribute(s,t.type);this[e]=r??this._$Ej?.get(e)??r,this._$Em=null;}}requestUpdate(t,s,i,e=false,h){if(void 0!==t){const r=this.constructor;if(false===e&&(h=this[t]),i??=r.getPropertyOptions(t),!((i.hasChanged??f$2)(h,s)||i.useDefault&&i.reflect&&h===this._$Ej?.get(t)&&!this.hasAttribute(r._$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.2");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$2=globalThis,i$2=t=>t,s$2=t$2.trustedTypes,e$3=s$2?s$2.createPolicy("lit-html",{createHTML:t=>t}):void 0,h$1="$lit$",o$4=`lit$${Math.random().toFixed(9).slice(2)}$`,n$3="?"+o$4,r$4=`<${n$3}>`,l=document,c$1=()=>l.createComment(""),a=t=>null===t||"object"!=typeof t&&"function"!=typeof t,u=Array.isArray,d=t=>u(t)||"function"==typeof t?.[Symbol.iterator],f$1="[ \t\n\f\r]",v=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,_=/-->/g,m=/>/g,p=RegExp(`>|${f$1}(?:([^\\s"'>=/]+)(${f$1}*=${f$1}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),g=/'/g,$=/"/g,y=/^(?:script|style|textarea|title)$/i,x=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),b=x(1),E=Symbol.for("lit-noChange"),A=Symbol.for("lit-nothing"),C=new WeakMap,P=l.createTreeWalker(l,129);function V(t,i){if(!u(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==e$3?e$3.createHTML(i):i}const N=(t,i)=>{const s=t.length-1,e=[];let n,l=2===i?"<svg>":3===i?"<math>":"",c=v;for(let i=0;i<s;i++){const s=t[i];let a,u,d=-1,f=0;for(;f<s.length&&(c.lastIndex=f,u=c.exec(s),null!==u);)f=c.lastIndex,c===v?"!--"===u[1]?c=_:void 0!==u[1]?c=m:void 0!==u[2]?(y.test(u[2])&&(n=RegExp("</"+u[2],"g")),c=p):void 0!==u[3]&&(c=p):c===p?">"===u[0]?(c=n??v,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?p:'"'===u[3]?$:g):c===$||c===g?c=p:c===_||c===m?c=v:(c=p,n=void 0);const x=c===p&&t[i+1].startsWith("/>")?" ":"";l+=c===v?s+r$4:d>=0?(e.push(a),s.slice(0,d)+h$1+s.slice(d)+o$4+x):s+o$4+(-2===d?i:x);}return [V(t,l+(t[s]||"<?>")+(2===i?"</svg>":3===i?"</math>":"")),e]};class S{constructor({strings:t,_$litType$:i},e){let r;this.parts=[];let l=0,a=0;const u=t.length-1,d=this.parts,[f,v]=N(t,i);if(this.el=S.createElement(f,e),P.currentNode=this.el.content,2===i||3===i){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes);}for(;null!==(r=P.nextNode())&&d.length<u;){if(1===r.nodeType){if(r.hasAttributes())for(const t of r.getAttributeNames())if(t.endsWith(h$1)){const i=v[a++],s=r.getAttribute(t).split(o$4),e=/([.?@])?(.*)/.exec(i);d.push({type:1,index:l,name:e[2],strings:s,ctor:"."===e[1]?I:"?"===e[1]?L:"@"===e[1]?z:H}),r.removeAttribute(t);}else t.startsWith(o$4)&&(d.push({type:6,index:l}),r.removeAttribute(t));if(y.test(r.tagName)){const t=r.textContent.split(o$4),i=t.length-1;if(i>0){r.textContent=s$2?s$2.emptyScript:"";for(let s=0;s<i;s++)r.append(t[s],c$1()),P.nextNode(),d.push({type:2,index:++l});r.append(t[i],c$1());}}}else if(8===r.nodeType)if(r.data===n$3)d.push({type:2,index:l});else {let t=-1;for(;-1!==(t=r.data.indexOf(o$4,t+1));)d.push({type:7,index:l}),t+=o$4.length-1;}l++;}}static createElement(t,i){const s=l.createElement("template");return s.innerHTML=t,s}}function M(t,i,s=t,e){if(i===E)return i;let h=void 0!==e?s._$Co?.[e]:s._$Cl;const o=a(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=M(t,h._$AS(t,i.values),h,e)),i}class R{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??l).importNode(i,true);P.currentNode=e;let h=P.nextNode(),o=0,n=0,r=s[0];for(;void 0!==r;){if(o===r.index){let i;2===r.type?i=new k(h,h.nextSibling,this,t):1===r.type?i=new r.ctor(h,r.name,r.strings,this,t):6===r.type&&(i=new Z(h,this,t)),this._$AV.push(i),r=s[++n];}o!==r?.index&&(h=P.nextNode(),o++);}return P.currentNode=l,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 k{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,i,s,e){this.type=2,this._$AH=A,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=M(this,t,i),a(t)?t===A||null==t||""===t?(this._$AH!==A&&this._$AR(),this._$AH=A):t!==this._$AH&&t!==E&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):d(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!==A&&a(this._$AH)?this._$AA.nextSibling.data=t:this.T(l.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=S.createElement(V(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===e)this._$AH.p(i);else {const t=new R(e,this),s=t.u(this.options);t.p(i),this.T(s),this._$AH=t;}}_$AC(t){let i=C.get(t.strings);return void 0===i&&C.set(t.strings,i=new S(t)),i}k(t){u(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 k(this.O(c$1()),this.O(c$1()),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,s){for(this._$AP?.(false,true,s);t!==this._$AB;){const s=i$2(t).nextSibling;i$2(t).remove(),t=s;}}setConnected(t){ void 0===this._$AM&&(this._$Cv=t,this._$AP?.(t));}}class H{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,i,s,e,h){this.type=1,this._$AH=A,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=A;}_$AI(t,i=this,s,e){const h=this.strings;let o=false;if(void 0===h)t=M(this,t,i,0),o=!a(t)||t!==this._$AH&&t!==E,o&&(this._$AH=t);else {const e=t;let n,r;for(t=h[0],n=0;n<h.length-1;n++)r=M(this,e[s+n],i,n),r===E&&(r=this._$AH[n]),o||=!a(r)||r!==this._$AH[n],r===A?t=A:t!==A&&(t+=(r??"")+h[n+1]),this._$AH[n]=r;}o&&!e&&this.j(t);}j(t){t===A?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"");}}class I extends H{constructor(){super(...arguments),this.type=3;}j(t){this.element[this.name]=t===A?void 0:t;}}class L extends H{constructor(){super(...arguments),this.type=4;}j(t){this.element.toggleAttribute(this.name,!!t&&t!==A);}}class z extends H{constructor(t,i,s,e,h){super(t,i,s,e,h),this.type=5;}_$AI(t,i=this){if((t=M(this,t,i,0)??A)===E)return;const s=this._$AH,e=t===A&&s!==A||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,h=t!==A&&(s===A||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){M(this,t);}}const B=t$2.litHtmlPolyfillSupport;B?.(S,k),(t$2.litHtmlVersions??=[]).push("3.3.2");const D=(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 k(i.insertBefore(c$1(),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=D(r,this.renderRoot,this.renderOptions);}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(true);}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(false);}render(){return E}};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.2");
/**
* @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$2},r$3=(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,true,r);},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,true,r);}}throw Error("Unsupported decorator location: "+n)};function n$2(t){return (e,o)=>"object"==typeof o?r$3(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$2(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 r$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(r$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 A}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)),A}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);}});
// 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 an IColor4Like 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 { r: data[0] / 255, g: data[1] / 255, b: data[2] / 255, a: data[3] / 255 };
}
function colorToHex(color) {
const toHex = (v) => Math.round(v * 255)
.toString(16)
.padStart(2, "0");
return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}${toHex(color.a)}`;
}
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+/);
}
/**
* Abstract base class for viewer custom elements.
* Contains all shared UI logic and depends only on IViewer.
*/
let ViewerElementBase = (() => {
var _a, _ViewerElementBase__isFaultedBacking_accessor_storage, _ViewerElementBase_renderWhenIdle_accessor_storage, _ViewerElementBase_source_accessor_storage, _ViewerElementBase_extension_accessor_storage, _ViewerElementBase_useOpenPBR_accessor_storage, _ViewerElementBase_environmentLighting_accessor_storage, _ViewerElementBase_environmentSkybox_accessor_storage, _ViewerElementBase_environmentIntensity_accessor_storage, _ViewerElementBase_environmentRotation_accessor_storage, _ViewerElementBase_shadowQuality_accessor_storage, _ViewerElementBase__loadingProgress_accessor_storage, _ViewerElementBase_skyboxBlur_accessor_storage, _ViewerElementBase_toneMapping_accessor_storage, _ViewerElementBase_contrast_accessor_storage, _ViewerElementBase_exposure_accessor_storage, _ViewerElementBase_ssao_accessor_storage, _ViewerElementBase_clearColor_accessor_storage, _ViewerElementBase_cameraAutoOrbit_accessor_storage, _ViewerElementBase_cameraAutoOrbitSpeed_accessor_storage, _ViewerElementBase_cameraAutoOrbitDelay_accessor_storage, _ViewerElementBase_hotSpots_accessor_storage, _ViewerElementBase_animationAutoPlay_accessor_storage, _ViewerElementBase_selectedAnimation_accessor_storage, _ViewerElementBase_animationSpeed_accessor_storage, _ViewerElementBase_animationProgress_accessor_storage, _ViewerElementBase__animations_accessor_storage, _ViewerElementBase__isAnimationPlaying_accessor_storage, _ViewerElementBase__showAnimationSlider_accessor_storage, _ViewerElementBase_selectedMaterialVariant_accessor_storage, _ViewerElementBase_camerasAsHotSpots_accessor_storage, _ViewerElementBase_resetMode_accessor_storage, _ViewerElementBase__canvasContainer_accessor_storage, _ViewerElementBase__hotSpotSelect_accessor_storage;
let _classSuper = i$1;
let _instanceExtraInitializers = [];
let __isFaultedBacking_decorators;
let __isFaultedBacking_initializers = [];
let __isFaultedBacking_extraInitializers = [];
let _renderWhenIdle_decorators;
let _renderWhenIdle_initializers = [];
let _renderWhenIdle_extraInitializers = [];
let _source_decorators;
let _source_initializers = [];
let _source_extraInitializers = [];
let _extension_decorators;
let _extension_initializers = [];
let _extension_extraInitializers = [];
let _useOpenPBR_decorators;
let _useOpenPBR_initializers = [];
let _useOpenPBR_extraInitializers = [];
let _set_environment_decorators;
let _environmentLighting_decorators;
let _environmentLighting_initializers = [];
let _environmentLighting_extraInitializers = [];
let _environmentSkybox_decorators;
let _environmentSkybox_initializers = [];
let _environmentSkybox_extraInitializers = [];
let _environmentIntensity_decorators;
let _environmentIntensity_initializers = [];
let _environmentIntensity_extraInitializers = [];
let _environmentRotation_decorators;
let _environmentRotation_initializers = [];
let _environmentRotation_extraInitializers = [];
let _shadowQuality_decorators;
let _shadowQuality_initializers = [];
let _shadowQuality_extraInitializers = [];
let __loadingProgress_decorators;
let __loadingProgress_initializers = [];
let __loadingProgress_extraInitializers = [];
let _skyboxBlur_decorators;
let _skyboxBlur_initializers = [];
let _skyboxBlur_extraInitializers = [];
let _toneMapping_decorators;
let _toneMapping_initializers = [];
let _toneMapping_extraInitializers = [];
let _contrast_decorators;
let _contrast_initializers = [];
let _contrast_extraInitializers = [];
let _exposure_decorators;
let _exposure_initializers = [];
let _exposure_extraInitializers = [];
let _ssao_decorators;
let _ssao_initializers = [];
let _ssao_extraInitializers = [];
let _clearColor_decorators;
let _clearColor_initializers = [];
let _clearColor_extraInitializers = [];
let _cameraAutoOrbit_decorators;
let _cameraAutoOrbit_initializers = [];
let _cameraAutoOrbit_extraInitializers = [];
let _cameraAutoOrbitSpeed_decorators;
let _cameraAutoOrbitSpeed_initializers = [];
let _cameraAutoOrbitSpeed_extraInitializers = [];
let _cameraAutoOrbitDelay_decorators;
let _cameraAutoOrbitDelay_initializers = [];
let _cameraAutoOrbitDelay_extraInitializers = [];
let _hotSpots_decorators;
let _hotSpots_initializers = [];
let _hotSpots_extraInitializers = [];
let _animationAutoPlay_decorators;
let _animationAutoPlay_initializers = [];
let _animationAutoPlay_extraInitializers = [];
let _selectedAnimation_decorators;
let _selectedAnimation_initializers = [];
let _selectedAnimation_extraInitializers = [];
let _animationSpeed_decorators;
let _animationSpeed_initializers = [];
let _animationSpeed_extraInitializers = [];
let _animationProgress_decorators;
let _animationProgress_initializers = [];
let _animationProgress_extraInitializers = [];
let __animations_decorators;
let __animations_initializers = [];
let __animations_extraInitializers = [];
let __isAnimationPlaying_decorators;
let __isAnimationPlaying_initializers = [];
let __isAnimationPlaying_extraInitializers = [];
let __showAnimationSlider_decorators;
let __showAnimationSlider_initializers = [];
let __showAnimationSlider_extraInitializers = [];
let _selectedMaterialVariant_decorators;
let _selectedMaterialVariant_initializers = [];
let _selectedMaterialVariant_extraInitializers = [];
let _camerasAsHotSpots_decorators;
let _camerasAsHotSpots_initializers = [];
let _camerasAsHotSpots_extraInitializers = [];
let _resetMode_decorators;
let _resetMode_initializers = [];
let _resetMode_extraInitializers = [];
let __canvasContainer_decorators;
let __canvasContainer_initializers = [];
let __canvasContainer_extraInitializers = [];
let __hotSpotSelect_decorators;
let __hotSpotSelect_initializers = [];
let __hotSpotSelect_extraInitializers = [];
return _a = class ViewerElementBase extends _classSuper {
/**
* Creates an instance of a ViewerElementBase subclass.
* @param _options The options to use when creating the Viewer.
*/
constructor(_options = {}) {
super();
this._options = (__runInitializers(this, _instanceExtraInitializers), _options);
this._viewerLock = new AsyncLock();
this._animationSliderResizeObserver = null;
// Bindings for properties that are synchronized both ways between the lower level Viewer and the ViewerElementBase.
this._propertyBindings = [
this._createPropertyBinding("clearColor", (viewer) => viewer.onClearColorChanged, (viewer) => (viewer.clearColor = this.clearColor ?? { r: 0, g: 0, b: 0, a: 0 }), (viewer) => (this.clearColor = viewer.clearColor)),
this._createPropertyBinding("skyboxBlur", (viewer) => viewer.onEnvironmentConfigurationChanged, (viewer) => (viewer.environmentConfig = { blur: this.skyboxBlur ?? viewer.environmentConfig.blur }), (viewer) => (this.skyboxBlur = viewer.environmentConfig.blur)),
this._createPropertyBinding("environmentIntensity", (viewer) => viewer.onEnvironmentConfigurationChanged, (viewer) => (viewer.environmentConfig = { intensity: this.environmentIntensity ?? viewer.environmentConfig.intensity }), (viewer) => (this.environmentIntensity = viewer.environmentConfig.intensity)),
this._createPropertyBinding("environmentRotation", (viewer) => viewer.onEnvironmentConfigurationChanged, (viewer) => (viewer.environmentConfig = { rotation: this.environmentRotation ?? viewer.environmentConfig.rotation }), (viewer) => (this.environmentRotation = viewer.environmentConfig.rotation)),
this._createPropertyBinding("toneMapping", (viewer) => viewer.onPostProcessingChanged, (viewer) => {
if (this.toneMapping) {
viewer.postProcessing = { toneMapping: this.toneMapping };
}
}, (viewer) => (this.toneMapping = viewer.postProcessing?.toneMapping)),
this._createPropertyBinding("contrast", (viewer) => viewer.onPostProcessingChanged, (viewer) => (viewer.postProcessing = { contrast: this.contrast ?? undefined }), (viewer) => (this.contrast = viewer.postProcessing.contrast)),
this._createPropertyBinding("exposure", (viewer) => viewer.onPostProcessingChanged, (viewer) => (viewer.postProcessing = { exposure: this.exposure ?? undefined }), (viewer) => (this.exposure = viewer.postProcessing.exposure)),
this._createPropertyBinding("ssao", (viewer) => viewer.onPostProcessingChanged, (viewer) => (viewer.postProcessing = { ssao: this.ssao ?? undefined }), (viewer) => (this.ssao = viewer.postProcessing.ssao)),
this._createPropertyBinding("cameraAutoOrbit", (viewer) => viewer.onCameraAutoOrbitChanged, (viewer) => (viewer.cameraAutoOrbit = { enabled: this.cameraAutoOrbit }), (viewer) => (this.cameraAutoOrbit = viewer.cameraAutoOrbit.enabled)),
this._createPropertyBinding("cameraAutoOrbitSpeed", (viewer) => viewer.onCameraAutoOrbitChanged, (viewer) => (viewer.cameraAutoOrbit = { speed: this.cameraAutoOrbitSpeed ?? undefined }), (viewer) => (this.cameraAutoOrbitSpeed = viewer.cameraAutoOrbit.speed)),
this._createPropertyBinding("cameraAutoOrbitDelay", (viewer) => viewer.onCameraAutoOrbitChanged, (viewer) => (viewer.cameraAutoOrbit = { delay: this.cameraAutoOrbitDelay ?? undefined }), (viewer) => (this.cameraAutoOrbitDelay = viewer.cameraAutoOrbit.delay)),
this._createPropertyBinding("animationSpeed", (viewer) => viewer.onAnimationSpeedChanged, (viewer) => (viewer.animationSpeed = this.animationSpeed), (viewer) => {
let speed = 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", (viewer) => viewer.onSelectedAnimationChanged, (viewer) => (viewer.selectedAnimation = this.selectedAnimation ?? viewer.selectedAnimation), (viewer) => (this.selectedAnimation = viewer.selectedAnimation)),
this._createPropertyBinding("selectedMaterialVariant", (viewer) => viewer.onSelectedMaterialVariantChanged, (viewer) => (viewer.selectedMaterialVariant = this.selectedMaterialVariant ?? viewer.selectedMaterialVariant ?? ""), (viewer) => (this.selectedMaterialVariant = viewer.selectedMaterialVariant)),
this._createPropertyBinding("hotSpots", (viewer) => viewer.onHotSpotsChanged, (viewer) => (viewer.hotSpots = this.hotSpots ?? viewer.hotSpots), (viewer) => (this.hotSpots = viewer.hotSpots)),
this._createPropertyBinding("camerasAsHotSpots", (viewer) => viewer.onCamerasAsHotSpotsChanged, (viewer) => (viewer.camerasAsHotSpots = this.camerasAsHotSpots ?? viewer.camerasAsHotSpots), (viewer) => (this.camerasAsHotSpots = viewer.camerasAsHotSpots)),
];
_ViewerElementBase__isFaultedBacking_accessor_storage.set(this, __runInitializers(this, __isFaultedBacking_initializers, false));
_ViewerElementBase_renderWhenIdle_accessor_storage.set(this, (__runInitializers(this, __isFaultedBacking_extraInitializers), __runInitializers(this, _renderWhenIdle_initializers, this._options.autoSuspendRendering === false)));
_ViewerElementBase_source_accessor_storage.set(this, (__runInitializers(this, _renderWhenIdle_extraInitializers), __runInitializers(this, _source_initializers, this._options.source ?? null)));
_ViewerElementBase_extension_accessor_storage.set(this, (__runInitializers(this, _source_extraInitializers), __runInitializers(this, _extension_initializers, null)));
_ViewerElementBase_useOpenPBR_accessor_storage.set(this, (__runInitializers(this, _extension_extraInitializers), __runInitializers(this, _useOpenPBR_initializers, this._options.useOpenPBR ?? false)));
_ViewerElementBase_environmentLighting_accessor_storage.set(this, (__runInitializers(this, _useOpenPBR_extraInitializers), __runInitializers(this, _environmentLighting_initializers, this._options.environmentLighting ?? null)));
_ViewerElementBase_environmentSkybox_accessor_storage.set(this, (__runInitializers(this, _environmentLighting_extraInitializers), __runInitializers(this, _environmentSkybox_initializers, this._options.environmentSkybox ?? null)));
_ViewerElementBase_environmentIntensity_accessor_storage.set(this, (__runInitializers(this, _environmentSkybox_extraInitializers), __runInitializers(this, _environmentIntensity_initializers, this._options.environmentConfig?.intensity ?? null)));
_ViewerElementBase_environmentRotation_accessor_storage.set(this, (__runInitializers(this, _environmentIntensity_extraInitializers), __runInitializers(this, _environmentRotation_initializers, this._options.environmentConfig?.rotation ?? null)));
_ViewerElementBase_shadowQuality_accessor_storage.set(this, (__runInitializers(this, _environmentRotation_extraInitializers), __runInitializers(this, _shadowQuality_initializers, null)));
_ViewerElementBase__loadingProgress_accessor_storage.set(this, (__runInitializers(this, _shadowQuality_extraInitializers), __runInitializers(this, __loadingProgress_initializers, false)));
_ViewerElementBase_skyboxBlur_accessor_storage.set(this, (__runInitializers(this, __loadingProgress_extraInitializers), __runInitializers(this, _skyboxBlur_initializers, this._options.environmentConfig?.blur ?? null)));
_ViewerElementBase_toneMapping_accessor_storage.set(this, (__runInitializers(this, _skyboxBlur_extraInitializers), __runInitializers(this, _toneMapping_initializers, this._options.postProcessing?.toneMapping ?? null)));
_ViewerElementBase_contrast_accessor_storage.set(this, (__runInitializers(this, _toneMapping_extraInitializers), __runInitializers(this, _contrast_initializers, this._options.postProcessing?.contrast ?? null)));
_ViewerElementBase_exposure_accessor_storage.set(this, (__runInitializers(this, _contrast_extraInitializers), __runInitializers(this, _exposure_initializers, this._options.postProcessing?.exposure ?? null)));
_ViewerElementBase_ssao_accessor_storage.set(this, (__runInitializers(this, _exposure_extraInitializers), __runInitializers(this, _ssao_initializers, this._options.postProcessing?.ssao ?? null)));
_ViewerElementBase_clearColor_accessor_storage.set(this, (__runInitializers(this, _ssao_extraInitializers), __runInitializers(this, _clearColor_initializers, this._options.clearColor
? { r: this._options.clearColor[0], g: this._options.clearColor[1], b: this._options.clearColor[2], a: this._options.clearColor[3] ?? 1 }
: null)));
_ViewerElementBase_cameraAutoOrbit_accessor_storage.set(this, (__runInitializers(this, _clearColor_extraInitializers), __runInitializers(this, _cameraAutoOrbit_initializers, this._options.cameraAutoOrbit?.enabled ?? false)));
_ViewerElementBase_cameraAutoOrbitSpeed_accessor_storage.set(this, (__runInitializers(this, _cameraAutoOrbit_extraInitializers), __runInitializers(this, _cameraAutoOrbitSpeed_initializers, this._options.cameraAutoOrbit?.speed ?? null)));
_ViewerElementBase_cameraAutoOrbitDelay_accessor_storage.set(this, (__runInitializers(this, _cameraAutoOrbitSpeed_extraInitializers), __runInitializers(this, _cameraAutoOrbitDelay_initializers, this._options.cameraAutoOrbit?.delay ?? null)));
_ViewerElementBase_hotSpots_accessor_storage.set(this, (__runInitializers(this, _cameraAutoOrbitDelay_extraInitializers), __runInitializers(this, _hotSpots_initializers, this._options.hotSpots ?? {})));
_ViewerElementBase_animationAutoPlay_accessor_storage.set(this, (__runInitializers(this, _hotSpots_extraInitializers), __runInitializers(this, _animationAutoPlay_initializers, !!this._options.animationAutoPlay)));
_ViewerElementBase_selectedAnimation_accessor_storage.set(this, (__runInitializers(this, _animationAutoPlay_extraInitializers), __runInitializers(this, _selectedAnimation_initializers, this._options.selectedAnimation ?? null)));
_ViewerElementBase_animationSpeed_accessor_storage.set(this, (__runInitializers(this, _selectedAnimation_extraInitializers), __runInitializers(this, _animationSpeed_initializers, this._options.animationSpeed ?? 1)));
_ViewerElementBase_animationProgress_accessor_storage.set(this, (__runInitializers(this, _animationSpeed_extraInitializers), __runInitializers(this, _animationProgress_initializers, 0)));
_ViewerElementBase__animations_accessor_storage.set(this, (__runInitializers(this, _animationProgress_extraInitializers), __runInitializers(this, __animations_initializers, [])));
_ViewerElementBase__isAnimationPlaying_accessor_storage.set(this, (__runInitializers(this, __animations_extraInitializers), __runInitializers(this, __isAnimationPlaying_initializers, false)));
_ViewerElementBase__showAnimationSlider_accessor_storage.set(this, (__runInitializers(this, __isAnimationPlaying_extraInitializers), __runInitializers(this, __showAnimationSlider_initializers, true)));
_ViewerElementBase_selectedMaterialVariant_accessor_storage.set(this, (__runInitializers(this, __showAnimationSlider_extraInitializers), __runInitializers(this, _selectedMaterialVariant_initializers, this._options.selectedMaterialVariant ?? null)));
_ViewerElementBase_camerasAsHotSpots_accessor_storage.set(this, (__runInitializers(this, _selectedMaterialVariant_extraInitializers), __runInitializers(this, _camerasAsHotSpots_initializers, false)));
_ViewerElementBase_resetMode_accessor_storage.set(this, (__runInitializers(this, _camerasAsHotSpots_extraInitializers), __runInitializers(this, _resetMode_initializers, "auto")));
_ViewerElementBase__canvasContainer_accessor_storage.set(this, (__runInitializers(this, _resetMode_extraInitializers), __runInitializers(this, __canvasContainer_initializers, void 0)));
_ViewerElementBase__hotSpotSelect_accessor_storage.set(this, (__runInitializers(this, __canvasContainer_extraInitializers), __runInitializers(this, __hotSpotSelect_initializers, void 0)));
__runInitializers(this, __hotSpotSelect_extraInitializers);
this._options = _options;
}
/** @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"];
}
/**
* 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._viewer) {
return this._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._viewer) {
return this._viewer.focusHotSpot(name);
}
return false;
}
get _isFaultedBacking() { return __classPrivateFieldGet(this, _ViewerElementBase__isFaultedBacking_accessor_storage, "f"); }
set _isFaultedBacking(value) { __classPrivateFieldSet(this, _ViewerElementBase__isFaultedBacking_accessor_storage, value, "f"); }
get _isFaulted() {
return this._isFaultedBacking;
}
/**
* When true, the scene will be rendered even if no scene state has changed.
*/
get renderWhenIdle() { return __classPrivateFieldGet(this, _ViewerElementBase_renderWhenIdle_accessor_storage, "f"); }
set renderWhenIdle(value) { __classPrivateFieldSet(this, _ViewerElementBase_renderWhenIdle_accessor_storage, value, "f"); }
/**
* The model URL.
*/
get source() { return __classPrivateFieldGet(this, _ViewerElementBase_source_accessor_storage, "f"); }
set source(value) { __classPrivateFieldSet(this, _ViewerElementBase_source_accessor_storage, value, "f"); }
/**
* 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.
*/
get extension() { return __classPrivateFieldGet(this, _ViewerElementBase_extension_accessor_storage, "f"); }
set extension(value) { __classPrivateFieldSet(this, _ViewerElementBase_extension_accessor_storage, value, "f"); }
/**
* If true, load glTF files using the OpenPBR material instead of the default PBR material.
* @experimental
*/
get useOpenPBR() { return __classPrivateFieldGet(this, _ViewerElementBase_useOpenPBR_accessor_storage, "f"); }
set useOpenPBR(value) { __classPrivateFieldSet(this, _ViewerElementBase_useOpenPBR_accessor_storage, value, "f"); }
/**
* 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;
}
/**
* The texture URL for lighting.
*/
get environmentLighting() { return __classPrivateFieldGet(this, _ViewerElementBase_environmentLighting_accessor_storage, "f"); }
set environmentLighting(value) { __classPrivateFieldSet(this, _ViewerElementBase_environmentLighting_accessor_storage, value, "f"); }
/**
* The texture URL for the skybox.
*/
get environmentSkybox() { return __classPrivateFieldGet(this, _ViewerElementBase_environmentSkybox_accessor_storage, "f"); }
set environmentSkybox(value) { __classPrivateFieldSet(this, _ViewerElementBase_environmentSkybox_accessor_storage, value, "f"); }
/**
* A value between 0 and 2 that specifies the intensity of the environment lighting.
*/
get environmentIntensity() { return __classPrivateFieldGet(this, _ViewerElementBase_environmentIntensity_accessor_storage, "f"); }
set environmentIntensity(value) { __classPrivateFieldSet(this, _ViewerElementBase_environmentIntensity_accessor_storage, value, "f"); }
/**
* A value in radians that specifies the rotation of the environment.
*/
get environmentRotation() { return __classPrivateFieldGet(this, _ViewerElementBase_environmentRotation_accessor_storage, "f"); }
set environmentRotation(value) { __classPrivateFieldSet(this, _ViewerElementBase_environmentRotation_accessor_storage, value, "f"); }
/**
* The type of shadows to use.
*/
get shadowQuality() { return __classPrivateFieldGet(this, _ViewerElementBase_shadowQuality_accessor_storage, "f"); }
set shadowQuality(value) { __classPrivateFieldSet(this, _ViewerElementBase_shadowQuality_accessor_storage, value, "f"); }
get _loadingProgress() { return __classPrivateFieldGet(this, _ViewerElementBase__loadingProgress_accessor_storage, "f"); }
set _loadingProgress(value) { __classPrivateFieldSet(this, _ViewerElementBase__loadingProgress_accessor_storage, value, "f"); }
/**
* 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;
}
/**
* A value between 0 and 1 that specifies how much to blur the skybox.
*/
get skyboxBlur() { return __classPrivateFieldGet(this, _ViewerElementBase_skyboxBlur_accessor_storage, "f"); }
set skyboxBlur(value) { __classPrivateFieldSet(this, _ViewerElementBase_skyboxBlur_accessor_storage, value, "f"); }
/**
* The tone mapping to use for rendering the scene.
*/
get toneMapping() { return __classPrivateFieldGet(this, _ViewerElementBase_toneMapping_accessor_storage, "f"); }
set toneMapping(value) { __classPrivateFieldSet(this, _ViewerElementBase_toneMapping_accessor_storage, value, "f"); }
/**
* The contrast applied to the scene.
*/
get contrast() { return __classPrivateFieldGet(this, _ViewerElementBase_contrast_accessor_storage, "f"); }
set contrast(value) { __classPrivateFieldSet(this, _ViewerElementBase_contrast_accessor_storage, value, "f"); }
/**
* The exposure applied to the scene.
*/
get exposure() { return __classPrivateFieldGet(this, _ViewerElementBase_exposure_accessor_storage, "f"); }
set exposure(value) { __classPrivateFieldSet(this, _ViewerElementBase_exposure_accessor_storage, value, "f"); }
/**
* Enables or disables screen space ambient occlusion (SSAO).
*/
get ssao() { return __classPrivateFieldGet(this, _ViewerElementBase_ssao_accessor_storage, "f"); }
set ssao(value) { __classPrivateFieldSet(this, _ViewerElementBase_ssao_accessor_storage, value, "f"); }
/**
* The clear color (e.g. background color) for the viewer.
*/
get clearColor() { return __classPrivateFieldGet(this, _ViewerElementBase_clearColor_accessor_storage, "f"); }
set clearColor(value) { __classPrivateFieldSet(this, _ViewerElementBase_clearColor_accessor_storage, value, "f"); }
/**
* Enables or disables camera auto-orbit.
*/
get cameraAutoOrbit() { return __classPrivateFieldGet(this, _ViewerElementBase_cameraAutoOrbit_accessor_storage, "f"); }
set cameraAutoOrbit(value) { __classPrivateFieldSet(this, _ViewerElementBase_cameraAutoOrbit_accessor_storage, value, "f"); }
/**
* The speed at which the camera auto-orbits around the target.
*/
get cameraAutoOrbitSpeed() { return __classPrivateFieldGet(this, _ViewerElementBase_cameraAutoOrbitSpeed_accessor_storage, "f"); }
set cameraAutoOrbitSpeed(value) { __classPrivateFieldSet(this, _ViewerElementBase_cameraAutoOrbitSpeed_accessor_storage, value, "f"); }
/**
* The delay in milliseconds before the camera starts auto-orbiting.
*/
get cameraAutoOrbitDelay() { return __classPrivateFieldGet(this, _ViewerElementBase_cameraAutoOrbitDelay_accessor_storage, "f"); }
set cameraAutoOrbitDelay(value) { __classPrivateFieldSet(this, _ViewerElementBase_cameraAutoOrbitDelay_accessor_storage, value, "f"); }
/**
* The set of defined hot spots.
*/
get hotSpots() { return __classPrivateFieldGet(this, _ViewerElementBase_hotSpots_accessor_storage, "f"); }
set hotSpots(value) { __classPrivateFieldSet(this, _ViewerElementBase_hotSpots_accessor_storage, value, "f"); }
/**
* True if the viewer has any hotspots.
*/
get _hasHotSpots() {
return Object.keys(this.hotSpots).length > 0;
}
/**
* True if the default animation should play automatically when a model is loaded.
*/
get animationAutoPlay() { return __classPrivateFieldGet(this, _ViewerElementBase_animationAutoPlay_accessor_storage, "f"); }
set animationAutoPlay(value) { __classPrivateFieldSet(this, _ViewerElementBase_animationAutoPlay_accessor_storage, value, "f"); }
/**
* The list of animation names for the currently loaded model.
*/
get animations() {
return this._animations;
}
/**
* True if the loaded model has any animations.
*/
get _hasAnimations() {
return this._animations.length > 0;
}
/**
* The currently selected animation index.
*/
get selectedAnimation() { return __classPrivateFieldGet(this, _ViewerElementBase_selectedAnimation_accessor_storage, "f"); }
set selectedAnimation(value) { __classPrivateFieldSet(this, _ViewerElementBase_selectedAnimation_accessor_storage, value, "f"); }
/**
* True if an animation is currently playing.
*/
get isAnimationPlaying() {
return this._isAnimationPlaying;
}
/**
* The speed scale at which animations are played.
*/
get animationSpeed() { return __classPrivateFieldGet(this, _ViewerElementBase_animationSpeed_accessor_storage, "f"); }
set animationSpeed(value) { __classPrivateFieldSet(this, _ViewerElementBase_animationSpeed_accessor_storage, value, "f"); }
/**
* The current point on the selected animation timeline, normalized between 0 and 1.
*/
get animationProgress() { return __classPrivateFieldGet(this, _ViewerElementBase_animationProgress_accessor_storage, "f"); }
set animationProgress(value) { __classPrivateFieldSet(this, _ViewerElementBase_animationProgress_accessor_storage, value, "f"); }
get _animations() { return __classPrivateFieldGet(this, _ViewerElementBase__animations_accessor_storage, "f"); }
set _animations(value) { __classPrivateFieldSet(this, _ViewerElementBase__animations_accessor_storage, value, "f"); }
get _isAnimationPlaying() { return __classPrivateFieldGet(this, _ViewerElementBase__isAnimationPlaying_accessor_storage, "f"); }
set _isAnimationPlaying(value) { __classPrivateFieldSet(this, _ViewerElementBase__isAnimationPlaying_accessor_storage, value, "f"); }
get _showAnimationSlider() { return __classPrivateFieldGet(this, _ViewerElementBase__showAnimationSlider_accessor_storage, "f"); }
set _showAnimationSlider(value) { __classPrivateFieldSet(this, _ViewerElementBase__showAnimationSlider_accessor_storage, value, "f"); }
/**
* The list of material variants for the currently loaded model.
*/
get materialVariants() {
return this._viewer?.materialVariants ?? [];
}
/**
* The currently selected material variant.
*/
get selectedMaterialVariant() { return __classPrivateFieldGet(this, _ViewerElementBase_selectedMaterialVariant_accessor_storage, "f"); }
set selectedMaterialVariant(value) { __classPrivateFieldSet(this, _ViewerElementBase_selectedMaterialVariant_accessor_storage, value, "f"); }
/**
* True if scene cameras should be used as hotspots.
*/
get camerasAsHotSpots() { return __classPrivateFieldGet(this, _ViewerElementBase_camerasAsHotSpots_accessor_storage, "f"); }
set camerasAsHotSpots(value) { __classPrivateFieldSet(this, _ViewerElementBase_camerasAsHotSpots_accessor_storage, value, "f"); }
/**
* 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.
*/
get resetMode() { return __classPrivateFieldGet(this, _ViewerElementBase_resetMode_accessor_storage, "f"); }
set resetMode(value) { __classPrivateFieldSet(this, _ViewerElementBase_resetMode_accessor_storage, value, "f"); }
get _canvasContainer() { return __classPrivateFieldGet(this, _ViewerElementBase__canvasContainer_accessor_storage, "f"); }
set _canvasContainer(value) { __classPrivateFieldSet(this, _ViewerElementBase__canvasContainer_accessor_storage, value, "f"); }
get _hotSpotSelect() { return __classPrivateFieldGet(this, _ViewerElementBase__hotSpotSelect_accessor_storage, "f"); }
set _hotSpotSelect(value) { __classPrivateFieldSet(this, _ViewerElementBase__hotSpotSelect_accessor_storage, value, "f"); }
/**
* Toggles the play/pause animation state if there is a selected animation.
*/
toggleAnimation() {
this._viewer?.toggleAnimation();
}
/**
* Resets the Viewer state based on the @see resetMode property.
*/
reset() {
this._reset(this.resetMode);
}
_reset(mode) {
switch (mode) {
case "auto":
this._viewer?.resetCamera(undefined);
break;
case "reframe":
this._viewer?.resetCamera(true);
break;
default:
this._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._viewer?.updateCamera({ alpha: value[0], beta: value[1], radius: value[2] });
}
else {
this._viewer?.resetCamera(false);
}
}
else if (name == "camera-target") {
const value = coerceCameraOrbitOrTarget(newValue);
if (value) {
this._viewer?.updateCamera({ targetX: value[0], targetY: value[1], targetZ: value[2] });
}
else {
this._viewer?.resetCamera(false);
}
}
}
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
update(changedProperties) {
super.update(changedProperties);
if (this._hotSpotSelect) {
this._hotSpotSelect.value = "";
}
if (this._needsReload(changedProperties)) {
this._tearDownViewer();
this._setupViewer();
}
else {
this._propertyBindings.filter((binding) => changedProperties.has(binding.property)).forEach((binding) => binding.updateViewer());
if (changedProperties.has("source") || changedProperties.has("useOpenPBR")) {
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);
}
}
}
/**
* Determines whether a full viewer reload is required for the given property changes.
* Subclasses can override to add additional reload triggers.
* @param changedProperties The properties that have changed.
* @returns True if the viewer needs to be reloaded.
*/
_needsReload(changedProperties) {
return changedProperties.get("renderWhenIdle") != null;
}
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
render() {
return b `
<div class="full-size">
<div id="canvasContainer" class="full-size"></div>
${this._renderOverlay()}
</div>
`;
}
/**
* 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 b `
<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>
`;
}
/**
* Renders the toolbar.
* @returns The template result for the toolbar.
*/
_renderToolbar() {
let toolbarControls = [];
if (this._viewer?.isModelLoaded) {
// If the model has animations, add animation controls.
if (this._hasAnimations) {
toolbarControls.push(b `
<div class="animation-timeline">
<button aria-label="${this.isAnimationPlaying ? "Pause" : "Play"}" @click="${this.toggleAnimation}">
${!this.isAnimationPlaying
? b `
<svg viewBox="0 0 24 24">
<path d="${playFilledIcon}" fill="currentColor"></path>
</svg>
`
: b `
<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) => b `<option value="${speed}" .selected="${this.animationSpeed === speed}">${speed}x</option> `)}
</select>
${this.animations.length > 1
? b `<select aria-label="Select Animation" @change="${this._onSelectedAnimationChanged}">
${this.animations.map((name, index) => b `<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(b `
<select aria-label="Select Material Variant" @change="${this._onMaterialVariantChanged}">
${this.materialVariants.map((name) => b `<option value="${name}" .selected="${this.selectedMaterialVariant === name}">${name}</option>`)}
</select>
`);
}
// Always include a button to reset the camera pose.
toolbarControls.push(b `
<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(b `
<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) => b `<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 = b `<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 b ` <div part="tool-bar" class="bar ${this._hasAnimations ? "" : "bar-min"} tool-bar">${toolbarControls}</div>`;
}
else {
return b ``;
}
}
/**
* Renders the reload button.
* @returns The template result for the reload button.
*/
_renderReloadButton() {
return b `${this._isFaulted
? b `
<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>
`
: ""}`;
}
/**
* 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 b `
<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>
`;
}
/**
* 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));
}
/**
* Handles changes to the selected animation.
* @param event The change event.
*/
_onSelectedAnimationChanged(event) {
const selectElement = event.target;
this.selectedAnimation = Number(selectElement.value);
}
/**
* Handles changes to the animation speed.
* @param event The change event.
*/
_onAnimationSpeedChanged(event) {
const selectElement = event.target;
this.animationSpeed = Number(selectElement.value);
}
/**
* Handles changes to the animation timeline.
* @param event The change event.
*/
_onAnimationTimelineChanged(event) {
if (this._viewer) {
const input = event.target;
const value = Number(input.value);
if (value !== this.animationProgress) {
this._viewer.animationProgress = value;
}
}
}
/**
* Handles pointer down events on the animation timeline.
* @param event The pointer down event.
*/
_onAnimationTimelinePointerDown(event) {
if (this._viewer?.isAnimationPlaying) {
this._viewer.pauseAnimation();
const input = event.target;
input.addEventListener("pointerup", () => this._viewer?.playAnimation(), { once: true });
}
}
/**
* Handles changes to the selected material variant.
* @param event The change event.
*/
_onMaterialVariantChanged(event) {
const selectElement = event.target;
this.selectedMaterialVariant = selectElement.value;
}
/**
* 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 ViewerElementBase properties.
_createPropertyBinding(property, getObservable, updateViewer, updateElement) {
const tryUpdateViewer = (viewer) => {
try {
updateViewer(viewer);
}
catch (error) {
Logger.Error(error);
}
};
return {
property,
onInitialized: (viewer) => {
getObservable(viewer).add(() => {
updateElement(viewer);
});
tryUpdateViewer(viewer);
},
updateViewer: () => {
if (this._viewer) {
tryUpdateViewer(this._viewer);
}
},
};
}
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._viewer) {
const canvas = document.createElement("canvas");
canvas.className = "full-size canvas";
canvas.setAttribute("touch-action", "none");
this._canvasContainer.appendChild(canvas);
// Proxy intercepts option reads so that current HTML attribute values
// take precedence over the initial options passed to the constructor.
// eslint-disable-next-line @typescript-eslint/no-this-alias
const viewerElement = this;
const options = new Proxy(this._options, {
get(target, prop) {
switch (prop) {
case "autoSuspendRendering":
return !(viewerElement.hasAttribute("render-when-idle") || target.autoSuspendRendering === false);
case "source":
return viewerElement.getAttribute("source") ?? target.source;
case "pluginExtension":
return viewerElement.extension ?? target.pluginExtension;
case "useOpenPBR":
return viewerElement.hasAttribute("use-open-pbr") ? true : (viewerElement.useOpenPBR ?? target.useOpenPBR);
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,
ssao: viewerElement.hasAttribute("ssao") || target.postProcessing?.ssao,
};
case "selectedMaterialVariant":
return viewerElement.getAttribute("material-variant") ?? target.selectedMaterialVariant;
case "onFaulted":
return (error) => {
viewerElement._isFaultedBacking = true;
target.onFaulted?.(error);
viewerElement._tearDownViewer();
};
default:
return target[prop];
}
},
});
const viewer = await this._createViewer(canvas, options);
this._viewer = viewer;
viewer.onEnvironmentChanged.add(() => {
this._dispatchCustomEvent("environmentchange", (type) => new Event(type));
});
viewer.onEnvironmentConfigurationChanged.add(() => {
this._dispatchCustomEvent("environmentconfigurationchange", (type) => new Event(type));
});
viewer.onEnvironmentError.add((error) => {
this._dispatchCustomEvent("environmenterror", (type) => new ErrorEvent(type, { error }));
});
viewer.onShadowsConfigurationChanged.add(() => {
this._dispatchCustomEvent("shadowsconfigurationchange", (type) => new Event(type));
});
viewer.onModelChanged.add((source) => {
this._animations = [...viewer.animations];
this._dispatchCustomEvent("modelchange", (type) => new CustomEvent(type, { detail: source }));
});
viewer.onModelError.add((error) => {
this._animations = [...viewer.animations];
this._dispatchCustomEvent("modelerror", (type) => new ErrorEvent(type, { error }));
});
viewer.onLoadingProgressChanged.add(() => {
this._loadingProgress = viewer.loadingProgress;
this._dispatchCustomEvent("loadingprogresschange", (type) => new Event(type));
});
viewer.onSelectedAnimationChanged.add(() => {
this._dispatchCustomEvent("selectedanimationchange", (type) => new Event(type));
});
viewer.onIsAnimationPlayingChanged.add(() => {
this._isAnimationPlaying = viewer.isAnimationPlaying ?? false;
this._dispatchCustomEvent("animationplayingchange", (type) => new Event(type));
});
viewer.onAnimationProgressChanged.add(() => {
this.animationProgress = viewer.animationProgress ?? 0;
this._dispatchCustomEvent("animationprogresschange", (type) => new Event(type));
});
viewer.onSelectedMaterialVariantChanged.add(() => {
this._dispatchCustomEvent("selectedmaterialvariantchange", (type) => new Event(type));
});
viewer.onAfterRenderObservable.add(() => {
this._dispatchCustomEvent("viewerrender", (type) => new Event(type));
});
this._propertyBindings.forEach((binding) => binding.onInitialized(viewer));
this._dispatchCustomEvent("viewerready", (type) => new Event(type));
}
this._isFaultedBacking = false;
});
}
async _tearDownViewer() {
await this._viewerLock.lockAsync(async () => {
if (this._viewer) {
this._viewer.dispose();
this._viewer = undefined;
}
this._onViewerTornDown();
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);
}
});
}
/**
* Called during teardown after the viewer has been disposed.
* Subclasses can override to clean up additional state.
*/
_onViewerTornDown() {
// Intentionally empty — subclasses override as needed.
}
async _updateModel() {
if (this._viewer) {
try {
if (this.source) {
await this._viewer.loadModel(this.source, {
pluginExtension: this.extension ?? undefined,
useOpenPBR: this.useOpenPBR,
});
}
else {
await this._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._viewer) {
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._viewer?.loadEnvironment(url, options);
}
else {
await this._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._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);
}
}
}
},
_ViewerElementBase__isFaultedBacking_accessor_storage = new WeakMap(),
_ViewerElementBase_renderWhenIdle_accessor_storage = new WeakMap(),
_ViewerElementBase_source_accessor_storage = new WeakMap(),
_ViewerElementBase_extension_accessor_storage = new WeakMap(),
_ViewerElementBase_useOpenPBR_accessor_storage = new WeakMap(),
_ViewerElementBase_environmentLighting_accessor_storage = new WeakMap(),
_ViewerElementBase_environmentSkybox_accessor_storage = new WeakMap(),
_ViewerElementBase_environmentIntensity_accessor_storage = new WeakMap(),
_ViewerElementBase_environmentRotation_accessor_storage = new WeakMap(),
_ViewerElementBase_shadowQuality_accessor_storage = new WeakMap(),
_ViewerElementBase__loadingProgress_accessor_storage = new WeakMap(),
_ViewerElementBase_skyboxBlur_accessor_storage = new WeakMap(),
_ViewerElementBase_toneMapping_accessor_storage = new WeakMap(),
_ViewerElementBase_contrast_accessor_storage = new WeakMap(),
_ViewerElementBase_exposure_accessor_storage = new WeakMap(),
_ViewerElementBase_ssao_accessor_storage = new WeakMap(),
_ViewerElementBase_clearColor_accessor_storage = new WeakMap(),
_ViewerElementBase_cameraAutoOrbit_accessor_storage = new WeakMap(),
_ViewerElementBase_cameraAutoOrbitSpeed_accessor_storage = new WeakMap(),
_ViewerElementBase_cameraAutoOrbitDelay_accessor_storage = new WeakMap(),
_ViewerElementBase_hotSpots_accessor_storage = new WeakMap(),
_ViewerElementBase_animationAutoPlay_accessor_storage = new WeakMap(),
_ViewerElementBase_selectedAnimation_accessor_storage = new WeakMap(),
_ViewerElementBase_animationSpeed_accessor_storage = new WeakMap(),
_ViewerElementBase_animationProgress_accessor_storage = new WeakMap(),
_ViewerElementBase__animations_accessor_storage = new WeakMap(),
_ViewerElementBase__isAnimationPlaying_accessor_storage = new WeakMap(),
_ViewerElementBase__showAnimationSlider_accessor_storage = new WeakMap(),
_ViewerElementBase_selectedMaterialVariant_accessor_storage = new WeakMap(),
_ViewerElementBase_camerasAsHotSpots_accessor_storage = new WeakMap(),
_ViewerElementBase_resetMode_accessor_storage = new WeakMap(),
_ViewerElementBase__canvasContainer_accessor_storage = new WeakMap(),
_ViewerElementBase__hotSpotSelect_accessor_storage = new WeakMap(),
(() => {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
__isFaultedBacking_decorators = [r$2()];
_renderWhenIdle_decorators = [n$2({ attribute: "render-when-idle", type: Boolean })];
_source_decorators = [n$2()];
_extension_decorators = [n$2()];
_useOpenPBR_decorators = [n$2({ attribute: "use-open-pbr", type: Boolean })];
_set_environment_decorators = [n$2({
hasChanged: (newValue, oldValue) => {
const environmentUrl = newValue || null;
return environmentUrl !== oldValue.lighting || environmentUrl !== oldValue.skybox;
},
})];
_environmentLighting_decorators = [n$2({ attribute: "environment-lighting" })];
_environmentSkybox_decorators = [n$2({ attribute: "environment-skybox" })];
_environmentIntensity_decorators = [n$2({ type: Number, attribute: "environment-intensity" })];
_environmentRotation_decorators = [n$2({
type: Number,
attribute: "environment-rotation",
})];
_shadowQuality_decorators = [n$2({
attribute: "shadow-quality",
})];
__loadingProgress_decorators = [r$2()];
_skyboxBlur_decorators = [n$2({ attribute: "skybox-blur" })];
_toneMapping_decorators = [n$2({
attribute: "tone-mapping",
converter: (value) => {
if (!value || !IsToneMapping(value)) {
return "neutral";
}
return value;
},
})];
_contrast_decorators = [n$2()];
_exposure_decorators = [n$2()];
_ssao_decorators = [n$2({ type: String })];
_clearColor_decorators = [n$2({
attribute: "clear-color",
converter: {
fromAttribute: parseColor,
toAttribute: (color) => (color ? colorToHex(color) : null),
},
})];
_cameraAutoOrbit_decorators = [n$2({
attribute: "camera-auto-orbit",
type: Boolean,
})];
_cameraAutoOrbitSpeed_decorators = [n$2({
attribute: "camera-auto-orbit-speed",
type: Number,
})];
_cameraAutoOrbitDelay_decorators = [n$2({
attribute: "camera-auto-orbit-delay",
type: Number,
})];
_hotSpots_decorators = [n$2({
attribute: "hotspots",
converter: (value) => {
if (!value) {
return {};
}
return JSON.parse(value);
},
})];
_animationAutoPlay_decorators = [n$2({ attribute: "animation-auto-play", type: Boolean })];
_selectedAnimation_decorators = [n$2({ attribute: "selected-animation", type: Number })];
_animationSpeed_decorators = [n$2({ attribute: "animation-speed" })];
_animationProgress_decorators = [n$2({ attribute: false })];
__animations_decorators = [r$2()];
__isAnimationPlaying_decorators = [r$2()];
__showAnimationSlider_decorators = [r$2()];
_selectedMaterialVariant_decorators = [n$2({ attribute: "material-variant" })];
_camerasAsHotSpots_decorators = [n$2({ attribute: "cameras-as-hotspots", type: Boolean })];
_resetMode_decorators = [n$2({ attribute: "reset-mode", converter: coerceResetMode })];
__canvasContainer_decorators = [e$1("#canvasContainer")];
__hotSpotSelect_decorators = [e$1("#hotSpotSelect")];
__esDecorate(_a, null, __isFaultedBacking_decorators, { kind: "accessor", name: "_isFaultedBacking", static: false, private: false, access: { has: obj => "_isFaultedBacking" in obj, get: obj => obj._isFaultedBacking, set: (obj, value) => { obj._isFaultedBacking = value; } }, metadata: _metadata }, __isFaultedBacking_initializers, __isFaultedBacking_extraInitializers);
__esDecorate(_a, null, _renderWhenIdle_decorators, { kind: "accessor", name: "renderWhenIdle", static: false, private: false, access: { has: obj => "renderWhenIdle" in obj, get: obj => obj.renderWhenIdle, set: (obj, value) => { obj.renderWhenIdle = value; } }, metadata: _metadata }, _renderWhenIdle_initializers, _renderWhenIdle_extraInitializers);
__esDecorate(_a, null, _source_decorators, { kind: "accessor", name: "source", static: false, private: false, access: { has: obj => "source" in obj, get: obj => obj.source, set: (obj, value) => { obj.source = value; } }, metadata: _metadata }, _source_initializers, _source_extraInitializers);
__esDecorate(_a, null, _extension_decorators, { kind: "accessor", name: "extension", static: false, private: false, access: { has: obj => "extension" in obj, get: obj => obj.extension, set: (obj, value) => { obj.extension = value; } }, metadata: _metadata }, _extension_initializers, _extension_extraInitializers);
__esDecorate(_a, null, _useOpenPBR_decorators, { kind: "accessor", name: "useOpenPBR", static: false, private: false, access: { has: obj => "useOpenPBR" in obj, get: obj => obj.useOpenPBR, set: (obj, value) => { obj.useOpenPBR = value; } }, metadata: _metadata }, _useOpenPBR_initializers, _useOpenPBR_extraInitializers);
__esDecorate(_a, null, _set_environment_decorators, { kind: "setter", name: "environment", static: false, private: false, access: { has: obj => "environment" in obj, set: (obj, value) => { obj.environment = value; } }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(_a, null, _environmentLighting_decorators, { kind: "accessor", name: "environmentLighting", static: false, private: false, access: { has: obj => "environmentLighting" in obj, get: obj => obj.environmentLighting, set: (obj, value) => { obj.environmentLighting = value; } }, metadata: _metadata }, _environmentLighting_initializers, _environmentLighting_extraInitializers);
__esDecorate(_a, null, _environmentSkybox_decorators, { kind: "accessor", name: "environmentSkybox", static: false, private: false, access: { has: obj => "environmentSkybox" in obj, get: obj => obj.environmentSkybox, set: (obj, value) => { obj.environmentSkybox = value; } }, metadata: _metadata }, _environmentSkybox_initializers, _environmentSkybox_extraInitializers);
__esDecorate(_a, null, _environmentIntensity_decorators, { kind: "accessor", name: "environmentIntensity", static: false, private: false, access: { has: obj => "environmentIntensity" in obj, get: obj => obj.environmentIntensity, set: (obj, value) => { obj.environmentIntensity = value; } }, metadata: _metadata }, _environmentIntensity_initializers, _environmentIntensity_extraInitializers);
__esDecorate(_a, null, _environmentRotation_decorators, { kind: "accessor", name: "environmentRotation", static: false, private: false, access: { has: obj => "environmentRotation" in obj, get: obj => obj.environmentRotation, set: (obj, value) => { obj.environmentRotation = value; } }, metadata: _metadata }, _environmentRotation_initializers, _environmentRotation_extraInitializers);
__esDecorate(_a, null, _shadowQuality_decorators, { kind: "accessor", name: "shadowQuality", static: false, private: false, access: { has: obj => "shadowQuality" in obj, get: obj => obj.shadowQuality, set: (obj, value) => { obj.shadowQuality = value; } }, metadata: _metadata }, _shadowQuality_initializers, _shadowQuality_extraInitializers);
__esDecorate(_a, null, __loadingProgress_decorators, { kind: "accessor", name: "_loadingProgress", static: false, private: false, access: { has: obj => "_loadingProgress" in obj, get: obj => obj._loadingProgress, set: (obj, value) => { obj._loadingProgress = value; } }, metadata: _metadata }, __loadingProgress_initializers, __loadingProgress_extraInitializers);
__esDecorate(_a, null, _skyboxBlur_decorators, { kind: "accessor", name: "skyboxBlur", static: false, private: false, access: { has: obj => "skyboxBlur" in obj, get: obj => obj.skyboxBlur, set: (obj, value) => { obj.skyboxBlur = value; } }, metadata: _metadata }, _skyboxBlur_initializers, _skyboxBlur_extraInitializers);
__esDecorate(_a, null, _toneMapping_decorators, { kind: "accessor", name: "toneMapping", static: false, private: false, access: { has: obj => "toneMapping" in obj, get: obj => obj.toneMapping, set: (obj, value) => { obj.toneMapping = value; } }, metadata: _metadata }, _toneMapping_initializers, _toneMapping_extraInitializers);
__esDecorate(_a, null, _contrast_decorators, { kind: "accessor", name: "contrast", static: false, private: false, access: { has: obj => "contrast" in obj, get: obj => obj.contrast, set: (obj, value) => { obj.contrast = value; } }, metadata: _metadata }, _contrast_initializers, _contrast_extraInitializers);
__esDecorate(_a, null, _exposure_decorators, { kind: "accessor", name: "exposure", static: false, private: false, access: { has: obj => "exposure" in obj, get: obj => obj.exposure, set: (obj, value) => { obj.exposure = value; } }, metadata: _metadata }, _exposure_initializers, _exposure_extraInitializers);
__esDecorate(_a, null, _ssao_decorators, { kind: "accessor", name: "ssao", static: false, private: false, access: { has: obj => "ssao" in obj, get: obj => obj.ssao, set: (obj, value) => { obj.ssao = value; } }, metadata: _metadata }, _ssao_initializers, _ssao_extraInitializers);
__esDecorate(_a, null, _clearColor_decorators, { kind: "accessor", name: "clearColor", static: false, private: false, access: { has: obj => "clearColor" in obj, get: obj => obj.clearColor, set: (obj, value) => { obj.clearColor = value; } }, metadata: _metadata }, _clearColor_initializers, _clearColor_extraInitializers);
__esDecorate(_a, null, _cameraAutoOrbit_decorators, { kind: "accessor", name: "cameraAutoOrbit", static: false, private: false, access: { has: obj => "cameraAutoOrbit" in obj, get: obj => obj.cameraAutoOrbit, set: (obj, value) => { obj.cameraAutoOrbit = value; } }, metadata: _metadata }, _cameraAutoOrbit_initializers, _cameraAutoOrbit_extraInitializers);
__esDecorate(_a, null, _cameraAutoOrbitSpeed_decorators, { kind: "accessor", name: "cameraAutoOrbitSpeed", static: false, private: false, access: { has: obj => "cameraAutoOrbitSpeed" in obj, get: obj => obj.cameraAutoOrbitSpeed, set: (obj, value) => { obj.cameraAutoOrbitSpeed = value; } }, metadata: _metadata }, _cameraAutoOrbitSpeed_initializers, _cameraAutoOrbitSpeed_extraInitializers);
__esDecorate(_a, null, _cameraAutoOrbitDelay_decorators, { kind: "accessor", name: "cameraAutoOrbitDelay", static: false, private: false, access: { has: obj => "cameraAutoOrbitDelay" in obj, get: obj => obj.cameraAutoOrbitDelay, set: (obj, value) => { obj.cameraAutoOrbitDelay = value; } }, metadata: _metadata }, _cameraAutoOrbitDelay_initializers, _cameraAutoOrbitDelay_extraInitializers);
__esDecorate(_a, null, _hotSpots_decorators, { kind: "accessor", name: "hotSpots", static: false, private: false, access: { has: obj => "hotSpots" in obj, get: obj => obj.hotSpots, set: (obj, value) => { obj.hotSpots = value; } }, metadata: _metadata }, _hotSpots_initializers, _hotSpots_extraInitializers);
__esDecorate(_a, null, _animationAutoPlay_decorators, { kind: "accessor", name: "animationAutoPlay", static: false, private: false, access: { has: obj => "animationAutoPlay" in obj, get: obj => obj.animationAutoPlay, set: (obj, value) => { obj.animationAutoPlay = value; } }, metadata: _metadata }, _animationAutoPlay_initializers, _animationAutoPlay_extraInitializers);
__esDecorate(_a, null, _selectedAnimation_decorators, { kind: "accessor", name: "selectedAnimation", static: false, private: false, access: { has: obj => "selectedAnimation" in obj, get: obj => obj.selectedAnimation, set: (obj, value) => { obj.selectedAnimation = value; } }, metadata: _metadata }, _selectedAnimation_initializers, _selectedAnimation_extraInitializers);
__esDecorate(_a, null, _animationSpeed_decorators, { kind: "accessor", name: "animationSpeed", static: false, private: false, access: { has: obj => "animationSpeed" in obj, get: obj => obj.animationSpeed, set: (obj, value) => { obj.animationSpeed = value; } }, metadata: _metadata }, _animationSpeed_initializers, _animationSpeed_extraInitializers);
__esDecorate(_a, null, _animationProgress_decorators, { kind: "accessor", name: "animationProgress", static: false, private: false, access: { has: obj => "animationProgress" in obj, get: obj => obj.animationProgress, set: (obj, value) => { obj.animationProgress = value; } }, metadata: _metadata }, _animationProgress_initializers, _animationProgress_extraInitializers);
__esDecorate(_a, null, __animations_decorators, { kind: "accessor", name: "_animations", static: false, private: false, access: { has: obj => "_animations" in obj, get: obj => obj._animations, set: (obj, value) => { obj._animations = value; } }, metadata: _metadata }, __animations_initializers, __animations_extraInitializers);
__esDecorate(_a, null, __isAnimationPlaying_decorators, { kind: "accessor", name: "_isAnimationPlaying", static: false, private: false, access: { has: obj => "_isAnimationPlaying" in obj, get: obj => obj._isAnimationPlaying, set: (obj, value) => { obj._isAnimationPlaying = value; } }, metadata: _metadata }, __isAnimationPlaying_initializers, __isAnimationPlaying_extraInitializers);
__esDecorate(_a, null, __showAnimationSlider_decorators, { kind: "accessor", name: "_showAnimationSlider", static: false, private: false, access: { has: obj => "_showAnimationSlider" in obj, get: obj => obj._showAnimationSlider, set: (obj, value) => { obj._showAnimationSlider = value; } }, metadata: _metadata }, __showAnimationSlider_initializers, __showAnimationSlider_extraInitializers);
__esDecorate(_a, null, _selectedMaterialVariant_decorators, { kind: "accessor", name: "selectedMaterialVariant", static: false, private: false, access: { has: obj => "selectedMaterialVariant" in obj, get: obj => obj.selectedMaterialVariant, set: (obj, value) => { obj.selectedMaterialVariant = value; } }, metadata: _metadata }, _selectedMaterialVariant_initializers, _selectedMaterialVariant_extraInitializers);
__esDecorate(_a, null, _camerasAsHotSpots_decorators, { kind: "accessor", name: "camerasAsHotSpots", static: false, private: false, access: { has: obj => "camerasAsHotSpots" in obj, get: obj => obj.camerasAsHotSpots, set: (obj, value) => { obj.camerasAsHotSpots = value; } }, metadata: _metadata }, _camerasAsHotSpots_initializers, _camerasAsHotSpots_extraInitializers);
__esDecorate(_a, null, _resetMode_decorators, { kind: "accessor", name: "resetMode", static: false, private: false, access: { has: obj => "resetMode" in obj, get: obj => obj.resetMode, set: (obj, value) => { obj.resetMode = value; } }, metadata: _metadata }, _resetMode_initializers, _resetMode_extraInitializers);
__esDecorate(_a, null, __canvasContainer_decorators, { kind: "accessor", name: "_canvasContainer", static: false, private: false, access: { has: obj => "_canvasContainer" in obj, get: obj => obj._canvasContainer, set: (obj, value) => { obj._canvasContainer = value; } }, metadata: _metadata }, __canvasContainer_initializers, __canvasContainer_extraInitializers);
__esDecorate(_a, null, __hotSpotSelect_decorators, { kind: "accessor", name: "_hotSpotSelect", static: false, private: false, access: { has: obj => "_hotSpotSelect" in obj, get: obj => obj._hotSpotSelect, set: (obj, value) => { obj._hotSpotSelect = value; } }, metadata: _metadata }, __hotSpotSelect_initializers, __hotSpotSelect_extraInitializers);
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
})(),
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
_a.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);
}
`,
_a;
})();
function coerceEngineAttribute(value) {
if (value === "WebGL" || value === "WebGPU") {
return value;
}
return undefined;
}
// Converts any standard html color string to a Color4 object.
function parseColorAsColor4(color) {
const parsed = parseColor(color);
return parsed ? new Color4(parsed.r, parsed.g, parsed.b, parsed.a) : null;
}
/**
* Viewer custom element backed by the full Babylon.js engine.
* Extends ViewerElementBase with Color4-typed clearColor, viewerDetails, and engine selection.
*/
let ViewerElement = (() => {
var _a, _ViewerElement_clearColor_accessor_storage, _ViewerElement_engine_accessor_storage;
let _classSuper = ViewerElementBase;
let _clearColor_decorators;
let _clearColor_initializers = [];
let _clearColor_extraInitializers = [];
let _engine_decorators;
let _engine_initializers = [];
let _engine_extraInitializers = [];
return _a = class ViewerElement extends _classSuper {
/**
* 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(options);
this._viewerClass = _viewerClass;
_ViewerElement_clearColor_accessor_storage.set(this, __runInitializers(this, _clearColor_initializers, this._options.clearColor
? new Color4(this._options.clearColor[0], this._options.clearColor[1], this._options.clearColor[2], this._options.clearColor[3] ?? 1)
: null));
_ViewerElement_engine_accessor_storage.set(this, (__runInitializers(this, _clearColor_extraInitializers), __runInitializers(this, _engine_initializers, this._options.engine)));
__runInitializers(this, _engine_extraInitializers);
this._viewerClass = _viewerClass;
}
/**
* 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;
}
/**
* The clear color (e.g. background color) for the viewer.
*/
get clearColor() { return __classPrivateFieldGet(this, _ViewerElement_clearColor_accessor_storage, "f"); }
set clearColor(value) { __classPrivateFieldSet(this, _ViewerElement_clearColor_accessor_storage, value, "f"); }
/**
* The engine to use for rendering.
*/
get engine() { return __classPrivateFieldGet(this, _ViewerElement_engine_accessor_storage, "f"); }
set engine(value) { __classPrivateFieldSet(this, _ViewerElement_engine_accessor_storage, value, "f"); }
_needsReload(changedProperties) {
if (super._needsReload(changedProperties)) {
return true;
}
if (changedProperties.has("engine")) {
const previous = changedProperties.get("engine");
if (previous && this.engine !== previous) {
return true;
}
}
return false;
}
async _createViewer(canvas, options) {
const detailsDeferred = new Deferred();
// Wrap the base class's proxied options to add engine and onInitialized interception.
const fullOptions = new Proxy(options, {
get: (target, prop) => {
switch (prop) {
case "engine":
return this.engine ?? target.engine;
case "onInitialized":
return (details) => {
target.onInitialized?.(details);
detailsDeferred.resolve(details);
};
default:
return target[prop];
}
},
});
const viewer = (await CreateViewerForCanvas(canvas, Object.assign(fullOptions, { viewerClass: this._viewerClass })));
const details = await detailsDeferred.promise;
this._viewerDetails = Object.assign(details, { viewer });
return viewer;
}
_onViewerTornDown() {
this._viewerDetails = undefined;
}
},
_ViewerElement_clearColor_accessor_storage = new WeakMap(),
_ViewerElement_engine_accessor_storage = new WeakMap(),
(() => {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
_clearColor_decorators = [n$2({
attribute: "clear-color",
converter: {
fromAttribute: parseColorAsColor4,
toAttribute: (color) => (color ? color.toHexString() : null),
},
})];
_engine_decorators = [n$2({ converter: coerceEngineAttribute })];
__esDecorate(_a, null, _clearColor_decorators, { kind: "accessor", name: "clearColor", static: false, private: false, access: { has: obj => "clearColor" in obj, get: obj => obj.clearColor, set: (obj, value) => { obj.clearColor = value; } }, metadata: _metadata }, _clearColor_initializers, _clearColor_extraInitializers);
__esDecorate(_a, null, _engine_decorators, { kind: "accessor", name: "engine", static: false, private: false, access: { has: obj => "engine" in obj, get: obj => obj.engine, set: (obj, value) => { obj.engine = value; } }, metadata: _metadata }, _engine_initializers, _engine_extraInitializers);
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
})(),
_a;
})();
/**
* Displays a 3D model using the Babylon.js Viewer.
*/
let HTML3DElement = (() => {
let _classDecorators = [t$1("babylon-viewer")];
let _classDescriptor;
let _classExtraInitializers = [];
let _classThis;
let _classSuper = ViewerElement;
_classThis = class extends _classSuper {
/**
* 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);
}
};
__setFunctionName(_classThis, "HTML3DElement");
(() => {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
_classThis = _classDescriptor.value;
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
__runInitializers(_classThis, _classExtraInitializers);
})();
return _classThis;
})();
/**
* 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 = (() => {
var _HTML3DAnnotationElement_hotSpot_accessor_storage;
let _classDecorators = [t$1("babylon-viewer-annotation")];
let _classDescriptor;
let _classExtraInitializers = [];
let _classThis;
let _classSuper = i$1;
let _hotSpot_decorators;
let _hotSpot_initializers = [];
let _hotSpot_extraInitializers = [];
_classThis = class extends _classSuper {
/**
* The name of the hotspot to track.
*/
get hotSpot() { return __classPrivateFieldGet(this, _HTML3DAnnotationElement_hotSpot_accessor_storage, "f"); }
set hotSpot(value) { __classPrivateFieldSet(this, _HTML3DAnnotationElement_hotSpot_accessor_storage, value, "f"); }
/** @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 ViewerElementBase)) {
// 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 b ` <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 = "";
}
}
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;
_HTML3DAnnotationElement_hotSpot_accessor_storage.set(this, __runInitializers(this, _hotSpot_initializers, ""));
__runInitializers(this, _hotSpot_extraInitializers);
}
};
_HTML3DAnnotationElement_hotSpot_accessor_storage = new WeakMap();
__setFunctionName(_classThis, "HTML3DAnnotationElement");
(() => {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
_hotSpot_decorators = [n$2({ attribute: "hotspot" })];
__esDecorate(_classThis, null, _hotSpot_decorators, { kind: "accessor", name: "hotSpot", static: false, private: false, access: { has: obj => "hotSpot" in obj, get: obj => obj.hotSpot, set: (obj, value) => { obj.hotSpot = value; } }, metadata: _metadata }, _hotSpot_initializers, _hotSpot_extraInitializers);
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
_classThis = _classDescriptor.value;
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
})();
/** @internal */
// eslint-disable-next-line @typescript-eslint/naming-convention
_classThis.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);
}
`;
(() => {
__runInitializers(_classThis, _classExtraInitializers);
})();
return _classThis;
})();
export { ConfigureCustomViewerElement, CreateHotSpotFromCamera, CreateViewerForCanvas, DefaultViewerOptions, HTML3DAnnotationElement, HTML3DElement, Viewer, ViewerElement, ViewerHotSpotResult };
//# sourceMappingURL=index.js.map