@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.
865 lines (849 loc) • 257 kB
JavaScript
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 { createEngine, createSceneContext, createArcRotateCamera, attachControl, onBeforeRender, getContainerMeshes, computeMaxExtents, loadHdrEnvironment, loadEnvironment, setSceneImageProcessing, rebuildScenePbrPipelines, createDirectionalLight, addToScene, createDisc, createPbrMaterial, createEsmDirectionalShadowGenerator, setShadowTaskCasterMeshes, loadGltf, resetVariant, stopAnimation, removeFromScene, disposeMeshGpu, goToFrame, playAnimation, pauseAnimation, getVariantNames, selectVariant, interpolateArcRotateCamera, getEffectiveAspectRatio, getViewProjectionMatrix, getCameraPosition, computeDeformedPositionToRef, stopEngine, unregisterScene, registerScene, registerSceneWithShadowSupport, startEngine, disposePicker, disposeScene, disposeEngine, createGpuPicker, pickAsync, NeutralToneMapping, AcesToneMapping, StandardToneMapping } from '@babylonjs/lite';
/**
* 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;
}
}
/******************************************************************************
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;
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$3=t=>(e,o)=>{ void 0!==o?o.addInitializer(()=>{customElements.define(t,e);}):customElements.define(t,e);};
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$2=globalThis,e$5=t$2.ShadowRoot&&(void 0===t$2.ShadyCSS||t$2.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$2.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 o$4={attribute:true,type:String,converter:u$1,reflect:false,hasChanged:f$2},r$4=(t=o$4,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$3(t){return (e,o)=>"object"==typeof o?r$4(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$3(r){return n$3({...r,state:true,attribute:false})}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const e$3=(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$2(e,r){return (n,s,i)=>{const o=t=>t.renderRoot?.querySelector(e)??null;return e$3(n,s,{get(){return o(this)}})}}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$1=globalThis,i$2=t=>t,s$2=t$1.trustedTypes,e$1=s$2?s$2.createPolicy("lit-html",{createHTML:t=>t}):void 0,h$1="$lit$",o$3=`lit$${Math.random().toFixed(9).slice(2)}$`,n$2="?"+o$3,r$2=`<${n$2}>`,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$1?e$1.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