UNPKG

@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.

4,447 lines 267 kB
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, enableDeviceLostSceneRecovery, createSceneContext, createArcRotateCamera, attachControl, onBeforeRender, getContainerMeshes, computeMaxExtents, setEnvironmentBlur, setEnvironmentRotation, loadHdrEnvironment, loadEnvironment, setSceneImageProcessing, rebuildScenePbrPipelines, createDirectionalLight, addToScene, createDisc, createPbrMaterial, setShadowOnly, createEsmDirectionalShadowGenerator, setShadowTaskCasterMeshes, loadGltf, resetVariant, stopAnimation, removeFromScene, disposeMeshGpu, goToFrame, playAnimation, pauseAnimation, getVariantNames, selectVariant, interpolateArcRotateCamera, getEffectiveAspectRatio, getViewProjectionMatrix, getCameraPosition, computeDeformedPositionToRef, unregisterScene, registerScene, registerSceneWithShadowSupport, startEngine, stopEngine, 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: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$2:d>=0?(e.push(a),s.slice(0,d)+h$1+s.slice(d)+o$3+x):s+o$3+(-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$3),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$3)&&(d.push({type:6,index:l}),r.removeAttribute(t));if(y.test(r.tagName)){const t=r.textContent.split(o$3),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$2)d.push({type:2,index:l});else {let t=-1;for(;-1!==(t=r.data.indexOf(o$3,t+1));)d.push({type:7,index:l}),t+=o$3.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$1.litHtmlPolyfillSupport;B?.(S,k),(t$1.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$2=s$1.litElementPolyfillSupport;o$2?.({LitElement:i$1});(s$1.litElementVersions??=[]).push("4.2.2");

/**
 * @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}&nbsp;&nbsp;</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$3()];
            _renderWhenIdle_decorators = [n$3({ attribute: "render-when-idle", type: Boolean })];
            _source_decorators = [n$3()];
            _extension_decorators = [n$3()];
            _useOpenPBR_decorators = [n$3({ attribute: "use-open-pbr", type: Boolean })];
            _set_environment_decorators = [n$3({
                    hasChanged: (newValue, oldValue) => {
                        const environmentUrl = newValue || null;
                        return environmentUrl !== oldValue.lighting || environmentUrl !== oldValue.skybox;
                    },
                })];
            _environmentLighting_decorators = [n$3({ attribute: "environment-lighting" })];
            _environmentSkybox_decorators = [n$3({ attribute: "environment-skybox" })];
            _environmentIntensity_decorators = [n$3({ type: Number, attribute: "environment-intensity" })];
            _environmentRotation_decorators = [n$3({
                    type: Number,
                    attribute: "environment-rotation",
                })];
            _shadowQuality_decorators = [n$3({
                    attribute: "shadow-quality",
                })];
            __loadingProgress_decorators = [r$3()];
            _skyboxBlur_decorators = [n$3({ attribute: "skybox-blur" })];
            _toneMapping_decorators = [n$3({
                    attribute: "tone-mapping",
                    converter: (value) => {
                        if (!value || !IsToneMapping(value)) {
                            return "neutral";
                        }
                        return value;
                    },
                })];
            _contrast_decorators = [n$3()];
            _exposure_decorators = [n$3()];
            _ssao_decorators = [n$3({ type: String })];
            _clearColor_decorators = [n$3({
                    attribute: "clear-color",
                    converter: {
                        fromAttribute: parseColor,
                        toAttribute: (color) => (color ? colorToHex(color) : null),
                    },
                })];
            _cameraAutoOrbit_decorators = [n$3({
                    attribute: "camera-auto-orbit",
                    type: Boolean,
                })];
            _cameraAutoOrbitSpeed_decorators = [n$3({
                    attribute: "camera-auto-orbit-speed",
                    type: Number,
                })];
            _cameraAutoOrbitDelay_decorators = [n$3({
                    attribute: "camera-auto-orbit-delay",
                    type: Number,
                })];
            _hotSpots_decorators = [n$3({
                    attribute: "hotspots",
                    converter: (value) => {
                        if (!value) {
                            return {};
                        }
                        return JSON.parse(value);
                    },
                })];
            _animationAutoPlay_decorators = [n$3({ attribute: "animation-auto-play", type: Boolean })];
            _selectedAnimation_decorators = [n$3({ attribute: "selected-animation", type: Number })];
            _animationSpeed_decorators = [n$3({ attribute: "animation-speed" })];
            _animationProgress_decorators = [n$3({ attribute: false })];
            __animations_decorators = [r$3()];
            __isAnimationPlaying_decorators = [r$3()];
            __showAnimationSlider_decorators = [r$3()];
            _selectedMaterialVariant_decorators = [n$3({ attribute: "material-variant" })];
            _camerasAsHotSpots_decorators = [n$3({ attribute: "cameras-as-hotspots", type: Boolean })];
            _resetMode_decorators = [n$3({ attribute: "reset-mode", converter: coerceResetMode })];
            __canvasContainer_decorators = [e$2("#canvasContainer")];
            __hotSpotSelect_decorators = [e$2("#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;
})();

/**
 * 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() };
}

// ── Defaults ──
/**
 * The default options for the Lite Viewer.
 */
const DefaultViewerOptions = DefaultViewerBaseOptions;
const DefaultCameraAlpha = -Math.PI / 2;
const DefaultCameraBeta = Math.PI / 2.5;
const DefaultCameraRadius = 3;
const DefaultShadowLightDirection = [0.12, -1, 0.05];
// ── Helpers ──
// Reusable scratch vectors for hotspot resolution to keep per-frame querying zero-allocation.
const _tmpHotSpotVectors = {
    a: { x: 0, y: 0, z: 0 },
    b: { x: 0, y: 0, z: 0 },
    c: { x: 0, y: 0, z: 0 },
    worldPos: { x: 0, y: 0, z: 0 },
    worldNormal: { x: 0, y: 0, z: 0 },
};
function getExtension(url, explicitExt) {
    if (explicitExt) {
        return explicitExt.startsWith(".") ? explicitExt : `.${explicitExt}`;
    }
    const dotIdx = url.lastIndexOf(".");
    return dotIdx >= 0 ? url.substring(dotIdx).toLowerCase() : "";
}
/**
 * Map the Viewer's tone-mapping mode to a Babylon Lite {@link LiteToneMapping} value (or `undefined`
 * when tone mapping should be disabled).
 * @param mode - The Viewer tone-mapping mode.
 * @returns The corresponding Lite tone mapping, or `undefined` to disable tone mapping.
 */
function toneMappingToLiteToneMapping(mode) {
    switch (mode) {
        case "standard":
            return StandardToneMapping;
        case "aces":
            return AcesToneMapping;
        case "neutral":
            return NeutralToneMapping;
        case "none":
            return undefined;
    }
}
// ── Viewer ──
/**
 * A lightweight implementation of the {@link IViewer} interface built on the Babylon Lite API.
 *
 * @remarks
 * Babylon Lite is a WebGPU-only engine that provides a subset of the full Babylon.js feature set.
 * Features that are not available in Lite (SSAO, "high" shadow quality, hot spots, File/ArrayBufferView model sources)
 * will log warnings and fall back gracefully.
 *
 * The `autoSuspendRendering` option is silently ignored: Lite has no scene-mutation tracking, so it cannot
 * detect when a scene is idle. Rendering is still suspended while the canvas is offscreen (see
 * {@link CreateViewerForCanvas}), which is the case that dominates cost on pages with multiple viewers.
 */
class Viewer extends ViewerBase {
    /**
     * Creates a new Viewer instance.
     * @param _engine The Babylon Lite engine context.
     * @param _options Optional viewer configuration.
     */
    constructor(_engine, _options) {
        super();
        this._engine = _engine;
        this._options = _options;
        this._detachControl = null;
        /** True while the engine's requestAnimationFrame loop is running. False while suspended or disposed. */
        this._renderLoopRunning = false;
        /**
         * True once the scene has been registered with the engine (deferred builders run, renderables bucketed).
         * Distinct from {@link _renderLoopRunning}: suspending rendering stops the rAF loop but leaves the scene
         * registered, so code that needs to know "has the scene been built" must consult this instead.
         */
        this._sceneRegistered = false;
        /** Number of live suspension handles returned by {@link _suspendRendering}. Rendering runs only at zero. */
        this._suspendRenderCount = 0;
        /**
         * Serializes every engine start/stop and scene (un)registration, so a suspend/resume triggered by the
         * offscreen observer can never interleave with {@link _beginRendering}'s stop, unregister, register,
         * start sequence (which is itself re-entered from model loads, environment loads, and construction).
         */
        this._renderLoopLock = new AsyncLock();
        /**
         * Resolves the pending `startEngine` await when the render loop is stopped before its first frame.
         * Lite's `startEngine` promise only settles from inside the rAF callback, so stopping the loop first
         * (by suspension or disposal) would otherwise leave the await pending forever.
         */
        this._renderLoopStopped = null;
        // Auto-orbit
        this._autoOrbitIdleTime = 0;
        this._lastPointerTime = 0;
        // Environment
        /** The currently-loaded lighting URL ("auto" resolves to the embedded default). null = no lighting loaded. */
        this._currentLightingUrl = null;
        /** The currently-loaded skybox URL ("auto" resolves to the embedded default). null = no skybox loaded. */
        this._currentSkyboxUrl = null;
        // Post processing
        this._toneMapping = this._options?.postProcessing?.toneMapping ?? DefaultViewerOptions.postProcessing.toneMapping;
        this._contrast = this._options?.postProcessing?.contrast ?? DefaultViewerOptions.postProcessing.contrast;
        this._exposure = this._options?.postProcessing?.exposure ?? DefaultViewerOptions.postProcessing.exposure;
        this._ssaoOption = this._options?.postProcessing?.ssao ?? DefaultViewerOptions.postProcessing.ssao;
        /** Serializes the async PBR-pipeline rebuilds triggered by image-processing updates
         *  (`setSceneImageProcessing`) and environment relights (`rebuildScenePbrPipelines`), so overlapping
         *  changes can't run concurrent rebuilds (which race on the scene's renderable list and leak). */
        this._pbrRebuildLock = new AsyncLock();
        // Shadows
        this._shadowGenerator = null;
        // The directional light and ground disc created by `_setupShadows`, tracked so `_unloadCurrentModel`
        // can tear them down instead of accumulating a new light + ground on every model (re)load.
        this._shadowLight = null;
        this._shadowGround = null;
        // Model
        this._container = null;
        /** GPU picker for double-click focus, created lazily on first double-click. Disposed with the viewer. */
        this._picker = null;
        /** The source that was passed to the most recent {@link loadModel} call, for notifications. */
        this._modelSource = null;
        /**
         * True once the first model load has built its Lite material group (via `registerScene`). Because
         * `_scene` is created once and never recreated, and glTF models all share Lite's singleton PBR group
         * builder, later model loads reuse that already-built group: their meshes are enqueued into the
         * per-frame material-swap queue and the running render loop materializes them, so those loads must
         * NOT re-register the scene (re-registration clears the swap queue and would drop the model). See the
         * (re-)registration decision in {@link _loadModelImpl}.
         */
        this._modelMaterialGroupBuilt = false;
        /** Cached animation-aware model bounds for the current model. Reset on unload. See {@link _computeModelBounds}. */
        this._cachedModelBounds = null;
        // Animation
        this._selectedAnimation = -1;
        this._animationSpeed = this._options?.animationSpeed ?? DefaultViewerOptions.animationSpeed;
        this._wasPlaying = false;
        this._lastProgress = -1;
        // Material variants
        this._selectedMaterialVariant = null;
        // Hot spots
        this._camerasAsHotSpots = false;
        /**
         * Aborts the in-flight camera interpolation (from {@link focusHotSpot}) when a new one starts or
         * the viewer is disposed. Null when no interpolation is running.
         */
        this._cameraInterpolationAbort = null;
        this._defaultTarget = { x: 0, y: 0, z: 0 };
        // ── Private Helpers ──
        this._onPointerActivity = () => {
            this._lastPointerTime = performance.now();
        };
        /**
         * Handles a canvas double-click: GPU-picks the model at the cursor and, on a hit, focuses the camera
         * on the picked point; on a miss (background), reframes the camera. Mirrors the full Viewer's
         * `POINTERDOUBLETAP` handler.
         * @param event The double-click mouse event; its offset coordinates locate the pick on the canvas.
         */
        this._onCanvasDoubleClick = (event) => {
            void this._handleDoubleClick(event.offsetX, event.offsetY);
        };
        this._deviceLostRecovery = enableDeviceLostSceneRecovery(_engine, {
            onRecoveryFailed: (error) => {
                const recoveryError = error instanceof Error ? error : new Error(`Babylon Lite device recovery failed: ${String(error)}`, { cause: error });
                if (this._options?.onFaulted) {
                    this._options.onFaulted(recoveryError);
                }
                else {
                    // Prefer the stack: an unhandled recovery failure is only diagnosable from where it was
                    // thrown, and the message alone gives no indication of which rebuild step failed.
                    Logger.Error(recoveryError.stack ?? recoveryError.message);
                }
            },
        });
        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;
        this._autoOrbitEnabled = this._options?.cameraAutoOrbit?.enabled ?? DefaultViewerOptions.cameraAutoOrbit.enabled;
        this._autoOrbitSpeed = this._options?.cameraAutoOrbit?.speed ?? DefaultViewerOptions.cameraAutoOrbit.speed;
        this._autoOrbitDelay = this._options?.cameraAutoOrbit?.delay ?? DefaultViewerOptions.cameraAutoOrbit.delay;
        if (this._options?.hotSpots) {
            this.hotSpots = this._options.hotSpots;
        }
        // Create scene internally (matching how full Viewer owns its scene)
        this._scene = createSceneContext(_engine);
        // Camera — NaN means "auto" (will be recomputed when model loads)
        const orbitAlpha = _options?.cameraOrbit?.[0];
        const orbitBeta = _options?.cameraOrbit?.[1];
        const orbitRadius = _options?.cameraOrbit?.[2];
        const alpha = orbitAlpha != null && !isNaN(orbitAlpha) ? orbitAlpha : DefaultCameraAlpha;
        const beta = orbitBeta != null && !isNaN(orbitBeta) ? orbitBeta : DefaultCameraBeta;
        const radius = orbitRadius != null && !isNaN(orbitRadius) ? orbitRadius : DefaultCameraRadius;
        this._defaultAlpha = alpha;
        this._defaultBeta = beta;
        this._defaultRadius = radius;
        if (_options?.cameraTarget) {
            const tx = _options.cameraTarget[0];
            const ty = _options.cameraTarget[1];
            const tz = _options.cameraTarget[2];
            this._defaultTarget = {
                x: tx != null && !isNaN(tx) ? tx : 0,
                y: ty != null && !isNaN(ty) ? ty : 0,
                z: tz != null && !isNaN(tz) ? tz : 0,
            };
        }
        this._camera = createArcRotateCamera(alpha, beta, radius, this._defaultTarget);
        this._scene.camera = this._camera;
        this._detachControl = attachControl(this._camera, this._engine.canvas, this._scene);
        // Track pointer activity for auto-orbit idle detection. Use the instance method (not a local
        // closure) so `dispose()` can remove the exact same listener references. Seed the last-activity
        // time to now so auto-orbit waits the configured delay before starting rather than treating the
        // freshly-constructed viewer (with `_lastPointerTime` at 0) as already idle.
        this._lastPointerTime = performance.now();
        this._engine.canvas.addEventListener("pointerdown", this._onPointerActivity);
        this._engine.canvas.addEventListener("pointermove", this._onPointerActivity);
        this._engine.canvas.addEventListener("wheel", this._onPointerActivity);
        // Double-click to focus: on the model, focus the picked point; on the background, reframe.
        // Mirrors the full Viewer's POINTERDOUBLETAP handler (viewer.ts).
        this._engine.canvas.addEventListener("dblclick", this._onCanvasDoubleClick);
        // Clear color — initialize base field from options, then push to engine.
        this._clearColor.r = _options?.clearColor?.[0] ?? 0;
        this._clearColor.g = _options?.clearColor?.[1] ?? 0;
        this._clearColor.b = _options?.clearColor?.[2] ?? 0;
        this._clearColor.a = _options?.clearColor?.[3] ?? 0;
        this._applyClearColor();
        // Auto-orbit, environment config, animation speed, and hot spots are initialized
        // inline at the field declarations.
        // Post processing — apply the initial Viewer state (loaded from options at field init
        // time above) to `scene.imageProcessing`, which still holds Lite's defaults until we
        // push our values. We bypass the public setter because the fields are already at the
        // target values, so the setter would dedup and skip the scene apply.
        this._applyImageProcessingToScene();
        // Shadow config — route through updateShadows so unsupported "high" is normalized to "normal"
        if (_options?.shadowConfig?.quality) {
            observePromise(this.updateShadows({ quality: _options.shadowConfig.quality }));
        }
        // Per-frame callback
        onBeforeRender(this._scene, (deltaMs) => {
            if (this._isDisposed) {
                return;
            }
            this._updateAutoOrbit(deltaMs);
            this._pollAnimationState();
            this.onAfterRenderObservable.notifyObservers();
        });
        // Keep the scene unbuilt until an initial shadow-casting model is added. Registering the
        // empty scene first leaves later material groups without their boot-time builders, so the
        // shadow task cannot materialize its caster renderables.
        if (!_options?.source || this._shadowQuality === "none") {
            observePromise(this._beginRendering());
        }
        // Initial loads — each will restart the render loop to pick up new renderables
        const initialLightingUrl = _options?.environmentLighting ?? DefaultViewerOptions.environmentLighting;
        const initialSkyboxUrl = _options?.environmentSkybox ?? DefaultViewerOptions.environmentSkybox;
        // Initial environment + model loads. The model is loaded AFTER the environment so its PBR pipeline is
        // built with the environment (IBL) present in one pass — Lite bakes the environment into the PBR
        // shaders at build time. This is an optimization, not a correctness requirement: an environment added
        // after a model is already built triggers a pixel-identical pipeline rebuild (see
        // `_rebuildModelPbrForEnvironment`). Ordering the initial loads this way just avoids that extra rebuild
        // (and the brief unlit frame before it) on the common path.
        observePromise((async () => {
            const envLoads = [];
            // If lighting and skybox URLs are the same, do one combined load. Otherwise do them separately.
            if (initialLightingUrl === initialSkyboxUrl) {
                if (initialLightingUrl !== "none") {
                    envLoads.push(this.loadEnvironment(initialLightingUrl));
                }
            }
            else {
                if (initialLightingUrl !== "none") {
                    envLoads.push(this.loadEnvironment(initialLightingUrl, { lighting: true, skybox: false }));
                }
                if (initialSkyboxUrl !== "none") {
                    envLoads.push(this.loadEnvironment(initialSkyboxUrl, { lighting: false, skybox: true }));
                }
            }
            // Don't block (or fail) the model load if an environment load rejects (e.g. a bad URL) — the
            // model should still appear. `allSettled` waits for the env textures to be in place first.
            await Promise.allSettled(envLoads);
            if (_options?.source) {
                await this.loadModel(_options.source);
            }
        })());
    }
    // ── Clear Color ──
    /** @internal */
    _applyClearColor() {
        const cc = this._scene.clearColor;
        cc.r = this._clearColor.r;
        cc.g = this._clearColor.g;
        cc.b = this._clearColor.b;
        cc.a = this._clearColor.a;
    }
    // ── Camera ──
    /** @internal Lite stores auto-orbit state on the base class fields and consults them in its idle loop. No engine push needed. */
    _applyCameraAutoOrbitEnabled() { }
    /** @internal Lite stores auto-orbit state on the base class fields. */
    _applyCameraAutoOrbitSpeed() { }
    /** @internal Lite stores auto-orbit state on the base class fields. */
    _applyCameraAutoOrbitDelay() { }
    resetCamera(reframe) {
        // The public reset always animates the transition, matching the full Viewer's `resetCamera`.
        this._resetCameraCore(reframe, true);
    }
    /**
     * Shared implementation of camera reset. Resolves the reframe default (matching the full Viewer:
     * reframe to model bounds when the selected animation differs from the default, otherwise return to
     * the explicit default pose) and moves the camera there, optionally animating the transition.
     * @param reframe Whether to reframe to model bounds; when undefined, decided from animation state.
     * @param interpolate Whether to animate the camera to the reset pose.
     */
    _resetCameraCore(reframe, interpolate) {
        if (reframe === undefined) {
            // Match the full Viewer: when the selected animation differs from the default, the explicit
            // default pose likely won't frame the model (it may even be out of view), so reframe to the
            // model bounds instead. See viewer.ts `resetCamera`.
            reframe = this._selectedAnimation !== (this._options?.selectedAnimation ?? 0);
        }
        if (reframe) {
            this._frameCameraToModel(interpolate);
            return;
        }
        // Non-reframe reset restores the "default" framing: the model bounds pose with any explicit
        // cameraOrbit/cameraTarget option overrides applied. Mirrors the full Viewer's
        // `_resetCamera` -> `_reframeCameraFromBounds`, so with no such options it returns to exactly the
        // load-time framing (fixing "reset moves the camera"). Before any model is loaded there are no
        // bounds, so fall back to the fixed default pose the camera was created with.
        if (this._frameCameraToModel(interpolate, true)) {
            return;
        }
        this._moveCameraTo({
            alpha: this._defaultAlpha,
            beta: this._defaultBeta,
            radius: this._defaultRadius,
            target: this._defaultTarget,
        }, interpolate);
    }
    updateCamera(pose) {
        // Animate to the requested pose, matching the full Viewer's `updateCamera`
        // (which routes through `interpolateTo`). Omitted fields keep the current value.
        const target = pose.targetX !== undefined || pose.targetY !== undefined || pose.targetZ !== undefined
            ? {
                x: pose.targetX ?? this._camera.target.x,
                y: pose.targetY ?? this._camera.target.y,
                z: pose.targetZ ?? this._camera.target.z,
            }
            : undefined;
        this._moveCameraTo({ alpha: pose.alpha, beta: pose.beta, radius: pose.radius, target }, true);
    }
    /**
     * Moves the camera to a goal pose, either by animating (via {@link interpolateArcRotateCamera}) or by
     * snapping directly. Either way, any in-flight interpolation is first canceled so it can't fight the
     * new pose. Omitted or NaN goal fields keep the camera's current value for that channel.
     * @param goal The destination camera pose.
     * @param interpolate Whether to animate the transition.
     */
    _moveCameraTo(goal, interpolate) {
        if (interpolate) {
            this._interpolateCameraTo(goal);
            return;
        }
        // Snap: cancel any running interpolation, then write the pose directly.
        this._cameraInterpolationAbort?.abort(new AbortError("Camera interpolation superseded."));
        this._cameraInterpolationAbort = null;
        if (goal.alpha !== undefined && !isNaN(goal.alpha)) {
            this._camera.alpha = goal.alpha;
        }
        if (goal.beta !== undefined && !isNaN(goal.beta)) {
            this._camera.beta = goal.beta;
        }
        if (goal.radius !== undefined && !isNaN(goal.radius)) {
            this._camera.radius = goal.radius;
        }
        if (goal.target) {
            // Mutate the existing target ObservableVec3 in place — replacing it with a plain object
            // would silently lose Lite's ObservableVec3 dirty-tracking, which is what notifies the
            // camera that its world matrix needs to recompute when target changes.
            this._camera.target.x = goal.target.x;
            this._camera.target.y = goal.target.y;
            this._camera.target.z = goal.target.z;
        }
    }
    /**
     * Frames the camera to the loaded model's bounds, matching the full Viewer's framing math. Near/far
     * planes and zoom limits are applied immediately; the orbit pose is moved (snapped or animated) via
     * {@link _moveCameraTo}.
     *
     * When `applyDefaultPoseOverrides` is true, the bounds-derived orbit pose is overridden per-channel by
     * any explicit `cameraOrbit`/`cameraTarget` options — mirroring the full Viewer's
     * `_resetCamera` -> `_reframeCameraFromBounds`. With no such options this equals the pure bounds
     * framing used on model load, so a reset returns to exactly the load-time framing.
     * @param interpolate Whether to animate the camera to the framing pose.
     * @param applyDefaultPoseOverrides Whether to override the bounds pose with explicit camera options.
     * @returns True if the model had bounds and the camera was framed; false if there is no model to frame.
     */
    _frameCameraToModel(interpolate, applyDefaultPoseOverrides = false) {
        const bounds = this._computeModelBounds();
        if (!bounds) {
            return false;
        }
        // Mirror the full Viewer's framing math (viewer.ts `_getCameraConfig` / `_reframeCameraFromBounds`)
        // so Lite frames identically. The framing radius is the bounding-box diagonal * 1.1. `bounds.radius`
        // is the bounding-sphere radius (half the diagonal), so the diagonal is `bounds.radius * 2`.
        let radius = bounds.radius * 2 * 1.1;
        if (!isFinite(radius) || radius <= 0) {
            radius = 1;
        }
        // Near/far planes and zoom limits scaled to the framing radius, matching the full Viewer
        // (near = radius * 0.001, far = radius * 1000, radius limits = radius * 0.001 .. radius * 5).
        // These are always bounds-derived (never overridden) and applied immediately (not interpolated),
        // matching the full Viewer's `_reframeCameraFromBounds`.
        this._camera.nearPlane = radius * 0.001;
        this._camera.farPlane = radius * 1000;
        this._camera.lowerRadiusLimit = radius * 0.001;
        this._camera.upperRadiusLimit = radius * 5;
        // Default to the bounds framing pose, matching the full Viewer (its reframe always applies a fixed
        // alpha/beta, independent of any cameraOrbit option). This keeps a consistent viewpoint across load
        // and animation switches.
        let alpha = FramingCameraAlpha;
        let beta = FramingCameraBeta;
        let radiusGoal = radius;
        const target = { x: bounds.center[0], y: bounds.center[1], z: bounds.center[2] };
        // For a "default pose" reset, override each channel with the explicit camera option when present,
        // falling back to the bounds framing otherwise — matching the full Viewer's `_reframeCameraFromBounds`.
        if (applyDefaultPoseOverrides) {
            const orbit = this._options?.cameraOrbit;
            if (orbit) {
                if (orbit[0] != null && !isNaN(orbit[0])) {
                    alpha = orbit[0];
                }
                if (orbit[1] != null && !isNaN(orbit[1])) {
                    beta = orbit[1];
                }
                if (orbit[2] != null && !isNaN(orbit[2])) {
                    radiusGoal = orbit[2];
                }
            }
            const cameraTarget = this._options?.cameraTarget;
            if (cameraTarget) {
                if (cameraTarget[0] != null && !isNaN(cameraTarget[0])) {
                    target.x = cameraTarget[0];
                }
                if (cameraTarget[1] != null && !isNaN(cameraTarget[1])) {
                    target.y = cameraTarget[1];
                }
                if (cameraTarget[2] != null && !isNaN(cameraTarget[2])) {
                    target.z = cameraTarget[2];
                }
            }
        }
        this._moveCameraTo({ alpha, beta, radius: radiusGoal, target }, interpolate);
        return true;
    }
    /**
     * Compute the aggregate world-space bounding box of the loaded model, accounting for
     * animation.
     *
     * Delegates to Lite's {@link computeMaxExtents}, which steps through the currently-selected
     * animation group and unions every sampled pose. This captures the full swept volume of
     * node (TRS), skeletal, and morph-target animation — so skinned models like the
     * acrobaticPlane glTF frame correctly instead of reporting their (much smaller) bind-pose
     * AABB. Meshes are gathered with {@link getContainerMeshes} so Viewer-added meshes (e.g. the
     * shadow-receiver disc) are excluded.
     *
     * The result is cached for the lifetime of the loaded model (reset in
     * `_unloadCurrentModel`) so the two consumers — `_frameCameraToModel` (camera target +
     * radius + near/far planes) and `_setupShadows` (light positioning, ground placement,
     * frustum sizing) — share a single animation sweep rather than stepping it twice.
     *
     * @returns aggregate `min`, `max`, `center`, and bounding-sphere `radius`
     *   (= half the diagonal), or `null` if the model has no bounds info.
     */
    _computeModelBounds() {
        if (!this._container) {
            return null;
        }
        if (this._cachedModelBounds) {
            return this._cachedModelBounds;
        }
        const meshes = getContainerMeshes(this._container);
        if (meshes.length === 0) {
            return null;
        }
        // Sample the selected animation so the bounds cover the model's full motion.
        const animationGroup = this._getActiveAnimationGroup();
        const extents = computeMaxExtents(meshes, animationGroup, this._engine);
        let minX = Infinity, minY = Infinity, minZ = Infinity;
        let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
        for (const extent of extents) {
            // Skip meshes that contributed no geometry (inverted extent).
            if (extent.minimum[0] > extent.maximum[0]) {
                continue;
            }
            if (extent.minimum[0] < minX) {
                minX = extent.minimum[0];
            }
            if (extent.minimum[1] < minY) {
                minY = extent.minimum[1];
            }
            if (extent.minimum[2] < minZ) {
                minZ = extent.minimum[2];
            }
            if (extent.maximum[0] > maxX) {
                maxX = extent.maximum[0];
            }
            if (extent.maximum[1] > maxY) {
                maxY = extent.maximum[1];
            }
            if (extent.maximum[2] > maxZ) {
                maxZ = extent.maximum[2];
            }
        }
        if (minX > maxX) {
            return null;
        }
        const dx = maxX - minX;
        const dy = maxY - minY;
        const dz = maxZ - minZ;
        const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
        this._cachedModelBounds = {
            min: [minX, minY, minZ],
            max: [maxX, maxY, maxZ],
            center: [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2],
            radius,
        };
        return this._cachedModelBounds;
    }
    // ── Environment ──
    /** @internal Lite has no engine state for intensity. */
    _applyEnvironmentIntensity() { }
    /** @internal */
    _applyEnvironmentBlur() {
        if (this._currentSkyboxUrl !== null) {
            setEnvironmentBlur(this._scene, this._environmentBlur);
        }
    }
    /** @internal */
    _applyEnvironmentRotation() {
        setEnvironmentRotation(this._scene, this._environmentRotation);
        this._rotateShadowLightWithEnvironment();
    }
    /** @internal */
    async _loadEnvironmentImpl(url, options, abortSignal, compositeAbortSignal) {
        const updateLighting = options.lighting;
        const updateSkybox = options.skybox;
        // Resolve the target URLs for lighting and skybox after applying the update.
        const targetLightingUrl = updateLighting ? (url ?? null) : this._currentLightingUrl;
        const targetSkyboxUrl = updateSkybox ? (url ?? null) : this._currentSkyboxUrl;
        // No-op: nothing actually changes.
        if (targetLightingUrl === this._currentLightingUrl && targetSkyboxUrl === this._currentSkyboxUrl) {
            return;
        }
        try {
            // Babylon Lite's current public API has no way to remove or replace existing env state:
            // - `liteLoadEnvironment` and `loadHdrEnvironment` always set `scene._envTextures` and push
            //   skybox/ground builders, but they don't remove anything that's already there.
            // - There's no PBR-scene-compatible standalone skybox loader.
            // We allow ADDING new state where there was none, but throw on any REPLACEMENT.
            if (this._currentLightingUrl !== null && targetLightingUrl !== this._currentLightingUrl) {
                const action = targetLightingUrl === null ? "remove" : "replace";
                throw new Error(`Babylon Lite cannot ${action} the loaded environment lighting ("${this._currentLightingUrl}"${targetLightingUrl === null ? "" : ` → "${targetLightingUrl}"`}). ` +
                    `Recreate the Viewer to change the environment.`);
            }
            if (this._currentSkyboxUrl !== null && targetSkyboxUrl !== this._currentSkyboxUrl) {
                const action = targetSkyboxUrl === null ? "remove" : "replace";
                throw new Error(`Babylon Lite cannot ${action} the loaded environment skybox ("${this._currentSkyboxUrl}"${targetSkyboxUrl === null ? "" : ` → "${targetSkyboxUrl}"`}). ` +
                    `Recreate the Viewer to change the environment.`);
            }
            // Skybox-only addition with no lighting yet: Lite couples the skybox to the IBL cubemap — a
            // single environment texture drives BOTH the background and the reflections/lighting. (This
            // matches the full viewer's `environment-skybox`, where the model also reflects the skybox.)
            // So a skybox requested with no lighting loaded is satisfied by loading the requested skybox
            // URL AS the environment: it provides the skybox and, unavoidably in Lite, the IBL too.
            const skyboxOnlyBecomesEnv = !updateLighting && updateSkybox && this._currentLightingUrl === null;
            const effectiveLightingUrl = skyboxOnlyBecomesEnv ? (targetSkyboxUrl ?? "auto") : (targetLightingUrl ?? "auto");
            const resolvedLightingUrl = effectiveLightingUrl === "auto" ? (await import('./defaultEnvironment-5jBs1zfd.js')).default : effectiveLightingUrl;
            const ext = getExtension(resolvedLightingUrl, options.extension);
            setEnvironmentRotation(this._scene, this._environmentRotation);
            if (targetSkyboxUrl !== null) {
                setEnvironmentBlur(this._scene, this._environmentBlur);
            }
            // Lite's `loadHdrEnvironment` cannot suppress its skybox build. So:
            // - `lighting: true, skybox: false` with .hdr → would build an unwanted skybox. Throw.
            // - `lighting: true, skybox: true` (or skybox-only with same URL) → fine.
            if (ext === ".hdr" && !updateSkybox) {
                throw new Error("Babylon Lite cannot load only the lighting from a .hdr URL — `loadHdrEnvironment` always builds a skybox. " +
                    "Update lighting and skybox together, or use a .env URL.");
            }
            if (ext === ".hdr") {
                await loadHdrEnvironment(this._scene, resolvedLightingUrl, {
                    useCubemapSkybox: true,
                    skipGround: true,
                    skyboxSize: 20,
                });
                this._currentLightingUrl = effectiveLightingUrl;
                this._currentSkyboxUrl = effectiveLightingUrl;
            }
            else {
                const resolvedSkyboxUrl = targetSkyboxUrl === "auto" ? (await import('./defaultEnvironment-5jBs1zfd.js')).default : (targetSkyboxUrl ?? undefined);
                // Note: when skybox-only is requested but lighting already exists, we still call
                // liteLoadEnvironment with the existing lighting URL — this re-fetches and re-uploads
                // the cubemap (wasteful) but correctly builds the requested skybox. Per the contract
                // ("honoring by re-loading is OK"), this is acceptable.
                await loadEnvironment(this._scene, resolvedLightingUrl, {
                    brdfUrl: (await import('./defaultBRDF-VHOoJ9Eb.js')).default,
                    skyboxUrl: resolvedSkyboxUrl,
                    skipSkybox: !updateSkybox,
                    skipGround: true,
                    skyboxSize: 20,
                });
                this._currentLightingUrl = effectiveLightingUrl;
                if (updateSkybox) {
                    this._currentSkyboxUrl = targetSkyboxUrl;
                }
            }
            // Lite's env loader unconditionally overwrites `scene.imageProcessing` (toneMappingEnabled,
            // exposure, contrast) with its own defaults — clobbering whatever the Viewer set during
            // construction or via subsequent `postProcessing` setter calls. Re-push our committed
            // values so the user-facing post-processing state survives env loads.
            this._applyImageProcessingToScene();
            // Re-register the scene so that the env loader's deferred builders are processed
            // and the new skybox/ground renderables land in the draw buckets. Lite only fills
            // `_opaqueRenderables` etc. at registerScene() time. Gated on `_sceneRegistered` (not the
            // rAF-loop state) so an environment loaded while rendering is suspended still re-registers;
            // otherwise the skybox would never appear, even after rendering resumes.
            if (this._sceneRegistered) {
                await this._beginRendering();
            }
            // Relight an already-built model. Lite bakes the environment (IBL) textures into the PBR
            // shaders when the material group is BUILT; `loadEnvironment` only updates `scene._envTextures`
            // + the skybox and does NOT rebuild existing PBR groups (and the re-registration above only runs
            // deferred builders, not already-built ones). So when the environment finishes loading AFTER the
            // model's pipeline was built — the common concurrent-load race where the model wins, or an
            // environment explicitly added to an already-displayed model — the model keeps its previous
            // (often absent) IBL and renders unlit/black while the skybox looks correct. Force a PBR rebuild
            // so the model picks up the new lighting. No-op when no model is loaded.
            if (this._sceneRegistered && this._container) {
                await this._rebuildModelPbrForEnvironment();
            }
            throwIfAborted(abortSignal, compositeAbortSignal);
            this.onEnvironmentChanged.notifyObservers();
        }
        catch (e) {
            if (!(e instanceof AbortError)) {
                this.onEnvironmentError.notifyObservers(e);
            }
            throw e;
        }
    }
    // ── Post Processing ──
    get postProcessing() {
        return {
            toneMapping: this._toneMapping,
            contrast: this._contrast,
            exposure: this._exposure,
            ssao: this._ssaoOption,
        };
    }
    set postProcessing(value) {
        let changed = false;
        if (value.toneMapping !== undefined && value.toneMapping !== this._toneMapping) {
            this._toneMapping = value.toneMapping;
            changed = true;
        }
        if (value.exposure !== undefined && value.exposure !== this._exposure) {
            this._exposure = value.exposure;
            changed = true;
        }
        if (value.contrast !== undefined && value.contrast !== this._contrast) {
            this._contrast = value.contrast;
            changed = true;
        }
        if (value.ssao !== undefined && value.ssao !== this._ssaoOption) {
            this._ssaoOption = value.ssao;
            if (value.ssao !== "disabled") {
                Logger.Warn("Viewer: SSAO is not supported by Babylon Lite.");
            }
            changed = true;
        }
        if (changed) {
            // Apply the change to the running scene. Exposure/contrast are read live from the scene UBO
            // each frame, but tone mapping (enabled state + algorithm) is baked into the PBR shaders at
            // registration time, so a change there requires a pipeline rebuild. `setSceneImageProcessing`
            // does both: it updates the config and rebuilds the affected PBR pipelines only when the tone
            // mapping actually changed (and no-ops the rebuild before the scene is first registered).
            this._applyImageProcessingDynamic();
            this.onPostProcessingChanged.notifyObservers();
        }
    }
    /**
     * Apply the current committed post-processing state to the running scene via
     * `setSceneImageProcessing`, serialized through {@link _pbrRebuildLock} so overlapping calls
     * never run concurrent PBR-pipeline rebuilds. Each queued apply re-reads the latest committed state
     * when it runs, so a burst of rapid changes collapses to the final state (intermediate updates that
     * no longer differ are no-ops).
     */
    _applyImageProcessingDynamic() {
        observePromise(this._pbrRebuildLock.lockAsync(async () => await setSceneImageProcessing(this._scene, this._liteImageProcessingUpdate())));
    }
    /**
     * Force a rebuild of the loaded model's PBR pipelines so they pick up the scene's current environment
     * (IBL) textures. Needed because Lite bakes the environment into the PBR shaders at build time and
     * `loadEnvironment` doesn't rebuild existing PBR groups, so a model built before its environment loads
     * renders unlit/black. Used when an environment is added or changed AFTER a model is already displayed.
     *
     * `rebuildScenePbrPipelines` re-runs the PBR group builder against the scene's current `_envTextures`,
     * producing pipelines pixel-identical to a model built with the environment present from the start.
     *
     * Serialized through {@link _pbrRebuildLock} with the other image-processing updates (which also
     * rebuild PBR pipelines) so the two can't race.
     * @returns A promise that resolves once the model's PBR pipelines have been rebuilt.
     */
    async _rebuildModelPbrForEnvironment() {
        await this._pbrRebuildLock.lockAsync(async () => {
            await rebuildScenePbrPipelines(this._scene);
        });
    }
    /**
     * Build the Babylon Lite {@link ImageProcessingUpdate} that mirrors the Viewer's committed
     * post-processing state (`_toneMapping`, `_exposure`, `_contrast`). SSAO has no
     * `scene.imageProcessing` slot in Lite — it's tracked in `_ssaoOption` but doesn't render anything
     * yet (Lite has no SSAO support).
     * @returns The Lite image-processing update mirroring the Viewer's committed state.
     */
    _liteImageProcessingUpdate() {
        const toneMapping = toneMappingToLiteToneMapping(this._toneMapping);
        return {
            toneMappingEnabled: toneMapping !== undefined,
            toneMapping,
            exposure: this._exposure,
            contrast: this._contrast,
        };
    }
    /**
     * Push the Viewer's committed post-processing state directly into `scene.imageProcessing`. Used on
     * paths where the scene is not yet registered or is about to be (re-)registered — construction, and
     * after env loads (Lite's env loader overwrites `scene.imageProcessing` with its own defaults, so we
     * re-push our values before re-registration bakes them into the shaders). The dynamic path (the
     * `postProcessing` setter) instead uses `setSceneImageProcessing` for a targeted pipeline rebuild.
     */
    _applyImageProcessingToScene() {
        Object.assign(this._scene.imageProcessing, this._liteImageProcessingUpdate());
    }
    // ── Shadows ──
    /**
     * 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") {
            throw new Error("Babylon Lite does not support 'high' shadow quality. Use 'normal' or 'none'.");
        }
        return await super.updateShadows(value, abortSignal);
    }
    /**
     * @internal
     * Lite cannot cleanly add or remove shadow infrastructure (light, ground disc, shadow generator)
     * after the scene has been registered. Adding meshes post-register requires re-running deferred
     * GPU builders, which corrupts the existing model's pipeline state. Reloading the model breaks
     * for similar reasons (the previous scene state isn't fully torn down).
     *
     * For now, shadow quality is effectively fixed at the value provided in the initial constructor
     * options: `_setupShadows` runs once during `_loadModelImpl` (before `addToScene` and the first
     * `registerScene`), so initial setup works correctly. Subsequent calls to `updateShadows` change
     * the committed `_shadowQuality` field but do not re-run shadow setup. Callers that need to
     * change shadow quality should recreate the viewer.
     */
    async _updateShadowsImpl(
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    quality, 
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    abortSignal, 
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    internalAbortSignal) {
        Logger.Warn("Babylon Lite cannot toggle shadow quality after the model is loaded. " +
            "Set `shadowConfig.quality` in the viewer constructor options instead, or recreate the viewer to change shadow rendering.");
    }
    async _setupShadows(abortSignal, internalAbortSignal) {
        if (!this._container || this._shadowQuality === "none") {
            return;
        }
        // Caster meshes: snapshot the scene's flat mesh list BEFORE we add the ground disc, so the
        // disc isn't itself treated as a shadow caster. (`container.entities` for glTF only
        // contains the root TransformNode, so filtering entities directly always returns empty.)
        const casterMeshes = [...this._scene.meshes];
        if (casterMeshes.length === 0) {
            return;
        }
        // Keep deformation-aware shadow bounds behind one lazy entry so static casters don't pay
        // for the morph-target or skeletal bounds providers.
        const hasMorphTargets = casterMeshes.some((mesh) => !!mesh.morphTargets);
        const hasSkeletons = casterMeshes.some((mesh) => !!mesh.skeleton);
        const deformableShadows = hasMorphTargets || hasSkeletons ? await import('./viewerShadows-DyIW8k9P.js') : undefined;
        throwIfAborted(abortSignal, internalAbortSignal);
        // Bounds for ground placement, ground sizing, and shadow light positioning. Falls back
        // to a unit cube if no mesh has bounds info — shadows still set up sensibly.
        const bounds = this._computeModelBounds() ?? {
            min: [-1, -1, -1],
            center: [0, 0, 0],
            radius: 1,
        };
        const minY = bounds.min[1];
        const [cx, cy, cz] = bounds.center;
        const radius = bounds.radius;
        // Directional light. Position it well outside the model along the negated light direction
        // so Lite's directional shadow camera (placed at light.position looking in light.direction)
        // sees the model in front of it. With the default (0, 0, 0) position, the model — which
        // typically sits with its base at or above origin — would be behind the shadow camera and
        // get clipped, leaving the shadow map empty. (Mirrors the full Viewer's positionFactor
        // logic: light.position = -direction * radius * 3.)
        //
        // The direction is kept nearly vertical (a small horizontal component relative to the -Y
        // drop) so the cast shadow sits centered directly beneath the model rather than being
        // pushed off to one side as an oblique streak. The full Viewer derives its light direction
        // from the IBL's dominant direction, which for the typical diffuse studio environment is
        // close to overhead (e.g. ~(-0.12, -0.99, -0.01)); Lite has no IBL dominant-direction
        // analysis, so this near-overhead fixed direction approximates that soft, grounded look.
        const lightDir = [...DefaultShadowLightDirection];
        const dirLen = Math.sqrt(lightDir[0] ** 2 + lightDir[1] ** 2 + lightDir[2] ** 2);
        const positionFactor = radius * 3;
        const light = createDirectionalLight(lightDir, 1);
        light.position.set(cx - (lightDir[0] / dirLen) * positionFactor, cy - (lightDir[1] / dirLen) * positionFactor, cz - (lightDir[2] / dirLen) * positionFactor);
        addToScene(this._scene, light);
        this._shadowLight = light;
        this._rotateShadowLightWithEnvironment();
        // Shadow ground disc. `setShadowOnly` enables Lite's shadow-only shader path, which mirrors
        // BJS's `BackgroundMaterial.shadowOnly`: the surface is invisible everywhere except where
        // shadow falls on it, where it appears in the configured color (black here). The
        // ground needs `receiveShadows = true` so Lite compiles it on the multi-light path where the
        // per-light `shadowFactors` the shadow-only path reads are actually written. `createPbrMaterial`
        // installs its own 1×1 fallback base/ORM textures, so none need to be supplied here.
        //
        // Disc radius is intentionally enormous (~1000× the model radius) so the disc is
        // effectively an infinite ground plane. Animated models can move/translate well outside
        // a tightly-fit ground without the cast shadow clipping at the disc edge. The disc itself
        // is alpha-zero outside the shadow region, so the visible scene is unchanged.
        const groundRadius = radius * 1000;
        const ground = createDisc(this._engine, { radius: groundRadius, tessellation: 64 });
        ground.rotation.x = Math.PI / 2;
        ground.position.y = minY;
        ground.receiveShadows = true;
        // Shadow-only is a tree-shakeable, opt-in PBR feature: importing and calling its enabler is
        // what pulls in and registers the shader fragment. Apps that never import the enabler pay
        // zero bundle cost. The feature also forces the alpha-blend path, so no separate
        // `alphaBlend: true` is needed for the disc to composite over the scene.
        const groundMaterial = createPbrMaterial({});
        setShadowOnly(groundMaterial, {
            color: [0, 0, 0],
            // Keep the shadow light and translucent so it reads as a soft, subtle grounding shadow
            // similar to the full Viewer (whose directional shadow darkness is only ~0.2–0.8),
            // rather than a heavy pitch-black blob. Paired with the large blurKernel below, this
            // gives a gentle penumbra instead of a hard silhouette.
            opacity: 0.3,
            // Use the natural ESM falloff (falloff = 1) so the penumbra fades smoothly. Values > 1
            // collapse the gradient into a hard aliased edge.
            falloff: 1,
        });
        ground.material = groundMaterial;
        addToScene(this._scene, ground);
        this._shadowGround = ground;
        // Shadow generator. Lite drives shadow rendering from `light.shadowGenerator`, not from a
        // separate scene-level list — so attaching to the light is the load-bearing step here.
        // Casters are registered separately via `setShadowTaskCasterMeshes`: pass only the model
        // meshes, since including the ground disc would cause the disc to occlude itself in the
        // shadow map.
        //
        // ESM (exponential shadow map) FP16 precision and scale invariance both pivot on the
        // caster's NDC-depth fraction. The shader stores `exp(-depthScale * NDC_depth)` per
        // texel; with depthScale=50 (Lite default), values for NDC > ~0.2 underflow to zero in
        // FP16, collapsing the soft penumbra into a binary silhouette → hard aliased shadow
        // edge.
        //
        // Counter-intuitively, the fix is to make orthoMaxZ much LARGER than the model + light
        // distance, not smaller: a wide depth range puts the entire caster at small NDC depth
        // where ESM exp values stay near 1 and FP16 has plenty of precision. The blur kernel
        // (fixed in texels) then produces the visible penumbra by smearing those near-1 values
        // toward zero across the silhouette boundary.
        //
        // Setting orthoMaxZ = positionFactor * 100 makes the caster's NDC-depth fraction (~0.7%)
        // invariant to model scale, so the shadow looks identical for a 16 cm airplane and a
        // 5 m UFO.
        const orthoMinZ = 0;
        const orthoMaxZ = positionFactor * 100;
        this._shadowGenerator = createEsmDirectionalShadowGenerator(this._engine, light, {
            orthoMinZ,
            orthoMaxZ,
            // Wide kernel blur on the ESM shadow map. The default (1) leaves a crisp, hard-edged
            // shadow; a large kernel spreads the penumbra into a soft gradient that matches the full
            // Viewer's soft shadow look (which ramps blurKernel up to 64 for diffuse environments).
            blurKernel: 48,
        });
        if (hasMorphTargets) {
            deformableShadows?.enableMorphTargetShadows(this._shadowGenerator);
        }
        if (hasSkeletons) {
            deformableShadows?.enableSkeletonShadows(this._shadowGenerator);
        }
        setShadowTaskCasterMeshes(this._shadowGenerator, casterMeshes);
        light.shadowGenerator = this._shadowGenerator ?? undefined;
    }
    _rotateShadowLightWithEnvironment() {
        const light = this._shadowLight;
        const bounds = this._computeModelBounds();
        if (!light || !bounds) {
            return;
        }
        const angle = -this._environmentRotation;
        const cosine = Math.cos(angle);
        const sine = Math.sin(angle);
        const directionX = DefaultShadowLightDirection[0] * cosine + DefaultShadowLightDirection[2] * sine;
        const directionY = DefaultShadowLightDirection[1];
        const directionZ = -0.12 * sine + DefaultShadowLightDirection[2] * cosine;
        const directionLength = Math.sqrt(directionX ** 2 + directionY ** 2 + directionZ ** 2);
        const positionFactor = bounds.radius * 3;
        const [centerX, centerY, centerZ] = bounds.center;
        light.direction.set(directionX, directionY, directionZ);
        light.position.set(centerX - (directionX / directionLength) * positionFactor, centerY - (directionY / directionLength) * positionFactor, centerZ - (directionZ / directionLength) * positionFactor);
    }
    // ── Model Loading ──
    /** @internal */
    async _loadModelImpl(source, options, abortSignal, internalAbortSignal) {
        // Source `undefined` flows through from `resetModel`. Treat as "unload, no new load".
        if (source === undefined) {
            const hadModel = this._modelSource !== null;
            this._unloadCurrentModel();
            if (hadModel) {
                this.onModelChanged.notifyObservers(null);
            }
            return;
        }
        const loadOperation = this._beginLoadOperation();
        try {
            if (typeof source !== "string") {
                Logger.Warn("Viewer: Only string URLs are supported for model loading. File and ArrayBufferView sources are not supported.");
                throw new Error("Unsupported model source type");
            }
            // Unload previous model
            this._unloadCurrentModel();
            // Load new model
            const url = source;
            if (options?.pluginExtension) {
                // Append extension hint if provided
                const ext = options.pluginExtension.startsWith(".") ? options.pluginExtension : `.${options.pluginExtension}`;
                if (!url.toLowerCase().endsWith(ext.toLowerCase())) {
                    // The Lite loader determines format from URL extension;
                    // for now we just trust the URL.
                }
            }
            const container = await loadGltf(this._engine, url);
            throwIfAborted(abortSignal, internalAbortSignal);
            this._container = container;
            this._modelSource = source;
            // Set up animation state BEFORE `addToScene` registers the tick callback. This way the
            // very first tick of the new render loop sees only the selected group as eligible to
            // tick — preventing any "wrong animation" flash on the first rendered frame.
            // (Lite's `createAnimationGroups` sets every clip to auto-play; we need exactly one to
            // be active at all times.)
            this._setupAnimations();
            // Add to scene and rebuild renderables
            addToScene(this._scene, container);
            // Frame the camera to the model BEFORE the first rendered frame, so the model never
            // appears briefly at the previous (default) camera position. Snap (no interpolation) here,
            // matching the full Viewer, which loads with `interpolateCamera: false`. Apply the explicit
            // cameraOrbit/cameraTarget option overrides on top of the bounds framing, mirroring the full
            // Viewer's post-load `_reset(false, "camera")` (viewer.ts `_loadModelImpl`) — without this,
            // an explicit `camera-orbit` is ignored on initial load.
            this._frameCameraToModel(false, true);
            // Setup shadows BEFORE the first rendered frame so the shadow ground's deferred GPU
            // builder is processed by the upcoming `registerScene` (Lite only runs deferred builders
            // during `registerScene`; meshes added afterwards stay invisible until re-registration).
            if (this._shadowQuality !== "none") {
                await this._setupShadows(abortSignal, internalAbortSignal);
            }
            // Materialize the newly-added model. There are two paths, and using the wrong one drops the
            // model:
            //
            // - First build of a material group (`addToScene` queued a deferred builder): the renderable
            //   is built only by `registerScene` -> `buildScene`, and that first build also populates the
            //   group's `_rebuildSingle` closure. This happens on the initial model load (the constructor
            //   registers an empty scene, so the model's PBR group is new) and whenever a load introduces a
            //   brand-new material family. Re-register to run the deferred builder.
            //
            // - Re-using an already-built group (`addToScene` only enqueued the meshes into the per-frame
            //   material-swap queue): this is the model-swap case — the previous model was removed, but Lite
            //   retains the (now-empty) group key, so the new meshes reuse it and get a swap-queue entry
            //   instead of a deferred builder. The running render loop drains that queue every frame
            //   (`processMaterialSwaps`), builds each renderable via the group's `_rebuildSingle`, and bumps
            //   `_renderableVersion` so the frame graph re-buckets them. Re-registering here would be wrong:
            //   `buildScene` clears the material-swap queue before the loop can drain it, dropping the model.
            //
            // So only (re-)register when the scene isn't registered yet or the model's material group
            // has not been built before; otherwise let the running loop drain the swap queue.
            //
            // `this._scene` is created once in the constructor and never recreated (it is disposed only in
            // `dispose()`), and the Viewer only loads glTF, whose meshes all share Lite's singleton PBR group
            // builder. So the first successful model load builds that group (populating its per-mesh rebuild
            // closure), and every later load — swap, reload, reset, or clear-then-load — reuses it via the
            // swap queue. `_modelMaterialGroupBuilt` tracks that one-time transition.
            //
            // Shadows are excluded from the swap-queue path: `_setupShadows` adds a fresh light + ground on
            // every load (`_unloadCurrentModel` tears down the previous ones), and the shadow frame-graph task
            // is wired at registration time. For a shadow-enabled viewer we therefore keep the (documented,
            // construction-time) re-registration behavior rather than draining the swap queue, so the shadow
            // task is rebuilt against the new light/ground instead of going stale. Shadow quality is fixed at
            // construction (see `_updateShadowsImpl`), so this only affects the rarely-used shadow + model-swap
            // combination.
            //
            // The condition tests `_sceneRegistered` rather than the rAF-loop state so that a model loaded
            // while rendering is suspended still registers its renderables; the swap queue it may instead
            // enqueue into is drained by the first frame after rendering resumes.
            if (!this._sceneRegistered || this._shadowQuality !== "none" || !this._modelMaterialGroupBuilt) {
                await this._beginRendering();
            }
            this._modelMaterialGroupBuilt = true;
            // Apply clear color from model if present
            if (container.clearColor) {
                this._scene.clearColor = container.clearColor;
                this.onClearColorChanged.notifyObservers();
            }
            // Apply material variant from options
            if (this._options?.selectedMaterialVariant) {
                this.selectedMaterialVariant = this._options.selectedMaterialVariant;
            }
            this.onModelChanged.notifyObservers(source);
        }
        catch (e) {
            if (!(e instanceof AbortError)) {
                this.onModelError.notifyObservers(e);
            }
            throw e;
        }
        finally {
            loadOperation.dispose();
        }
    }
    _unloadCurrentModel() {
        // Reset animation state
        this._selectedAnimation = -1;
        this._wasPlaying = false;
        this._lastProgress = -1;
        if (this._container) {
            // Reset material variant
            if (this._selectedMaterialVariant !== null) {
                resetVariant(this._container);
                this._selectedMaterialVariant = null;
            }
            // Remove the model's renderables from the scene. `addToScene` adds the container's meshes but
            // nothing removes them on unload, so without this the previous model stays rendered after a
            // `clear model` (source removed) or `change model source`. `removeFromScene` fully tears down
            // each mesh (renderables, frame-graph task bindings, GPU buffers) and bumps the renderable
            // version, so the removal is reflected by the live render loop even when the scene is not
            // re-registered (the model-cleared path does not re-register).
            //
            // TODO: Simplify once https://github.com/BabylonJS/Babylon-Lite/pull/337 is merged and picked
            // up in the junctioned Lite build. That PR makes `removeFromScene` accept the same union as
            // `addToScene` (including `AssetContainer`) and undoes the add field-by-field. The entire block
            // below (stop animation groups, splice them out of `scene.animationGroups`, and remove each
            // container mesh) collapses to a single symmetric call:
            //     removeFromScene(this._scene, this._container);
            // That PR also plugs a leak this manual teardown cannot reach: `addToScene` pushes an anonymous
            // `_beforeRender` animation-tick closure with no removal handle. Stopping the groups here keeps
            // it harmless (it ticks stopped clips), but the closure stays in `scene._beforeRender` across
            // loads. The PR stores it as `AssetContainer._beforeRenderHook` so removal can splice it out.
            const groups = this._container.animationGroups;
            if (groups) {
                // Stop the container's clips first so the animation tick callback `addToScene` registered
                // for them does not keep advancing a model whose meshes are being removed.
                for (const group of groups) {
                    stopAnimation(group);
                }
                for (const group of groups) {
                    const index = this._scene.animationGroups.indexOf(group);
                    if (index >= 0) {
                        this._scene.animationGroups.splice(index, 1);
                    }
                }
            }
            for (const mesh of getContainerMeshes(this._container)) {
                removeFromScene(this._scene, mesh);
            }
        }
        // Tear down the shadow infrastructure created by `_setupShadows` so a subsequent (re)load doesn't
        // accumulate duplicate lights/ground discs. The ground disc is a mesh we can fully remove + free;
        // the directional light is detached by removing it from the scene's light list. (Lite has no public
        // shadow-generator dispose API yet — dropping our reference plus removing the light detaches it from
        // the render path, and the shadow frame-graph task is rebuilt on the next `registerScene`.)
        if (this._shadowGround) {
            removeFromScene(this._scene, this._shadowGround);
            disposeMeshGpu(this._shadowGround);
            this._shadowGround = null;
        }
        if (this._shadowLight) {
            const lightIndex = this._scene.lights.indexOf(this._shadowLight);
            if (lightIndex >= 0) {
                this._scene.lights.splice(lightIndex, 1);
            }
            this._shadowLight = null;
        }
        this._shadowGenerator = null;
        this._container = null;
        this._modelSource = null;
        this._cachedModelBounds = null;
    }
    // ── Animation ──
    get animations() {
        const groups = this._container?.animationGroups;
        if (!groups || groups.length === 0) {
            return [];
        }
        return groups.map((g) => g.name);
    }
    get selectedAnimation() {
        return this._selectedAnimation;
    }
    set selectedAnimation(index) {
        const groups = this._container?.animationGroups;
        if (!groups || groups.length === 0) {
            this._selectedAnimation = -1;
            this.onSelectedAnimationChanged.notifyObservers();
            return;
        }
        const newIndex = index >= 0 && index < groups.length ? index : -1;
        if (newIndex === this._selectedAnimation) {
            return;
        }
        // Capture whether the previously-active animation was playing so we can preserve play state
        // across selection (matches full Viewer's behavior).
        const previousActive = this._getActiveAnimationGroup();
        const wasPlaying = previousActive?.isPlaying ?? false;
        this._selectedAnimation = newIndex;
        this._isolateSelectedAnimation();
        // The framing bounds are animation-specific (each clip sweeps a different volume), so drop
        // the cached bounds and reframe the camera to the newly-selected animation. Without this the
        // camera stays framed for the previously-selected clip and the model can animate out of view
        // — e.g. the acrobaticPlane/UFO "flight" clip travels far outside the "hover" bounds. Matches
        // the full Viewer, whose `selectedAnimation` setter calls `_reframeCamera` (which interpolates).
        this._cachedModelBounds = null;
        this._frameCameraToModel(true);
        this.onSelectedAnimationChanged.notifyObservers();
        if (wasPlaying) {
            this.playAnimation();
        }
    }
    get animationSpeed() {
        return this._animationSpeed;
    }
    set animationSpeed(value) {
        this._animationSpeed = value;
        const group = this._getActiveAnimationGroup();
        if (group) {
            group.speedRatio = value;
        }
        this.onAnimationSpeedChanged.notifyObservers();
    }
    get isAnimationPlaying() {
        const group = this._getActiveAnimationGroup();
        return group ? group.isPlaying : false;
    }
    get animationProgress() {
        const group = this._getActiveAnimationGroup();
        if (!group || group.duration <= 0) {
            return 0;
        }
        // currentTime is in seconds; duration is also in seconds
        return Math.min(group.currentTime / group.duration, 1);
    }
    set animationProgress(value) {
        const group = this._getActiveAnimationGroup();
        if (!group || group.duration <= 0) {
            return;
        }
        // goToFrame expects a frame number at 60 fps
        const targetSeconds = value * group.duration;
        const frameAt60fps = targetSeconds * 60;
        goToFrame(group, frameAt60fps);
        this.onAnimationProgressChanged.notifyObservers();
    }
    toggleAnimation() {
        if (this.isAnimationPlaying) {
            observePromise(this.pauseAnimation());
        }
        else {
            this.playAnimation();
        }
    }
    playAnimation() {
        const group = this._getActiveAnimationGroup();
        if (group) {
            group.speedRatio = this._animationSpeed;
            group.loopAnimation = true;
            playAnimation(group);
            this.onIsAnimationPlayingChanged.notifyObservers();
        }
    }
    async pauseAnimation() {
        const group = this._getActiveAnimationGroup();
        if (group) {
            pauseAnimation(group);
            this.onIsAnimationPlayingChanged.notifyObservers();
        }
    }
    _getActiveAnimationGroup() {
        const groups = this._container?.animationGroups;
        if (!groups || this._selectedAnimation < 0 || this._selectedAnimation >= groups.length) {
            return null;
        }
        return groups[this._selectedAnimation];
    }
    /**
     * Enforces the "only the selected animation may be playing" invariant by stopping every
     * non-selected animation group (`stopAnimation` blocks subsequent ticks) and pausing the
     * selected one (so its tick still runs and applies the current-time pose).
     *
     * Lite's `tickAnimation` writes bone TRS every frame regardless of `playing`, so a merely
     * paused non-selected group would still pollute the mesh transforms with its current frame's
     * pose. Only `stopAnimation` blocks tick entirely.
     *
     * The selected group's tick must be allowed to run (so switching between animations updates
     * the pose). Lite's `pauseAnimation` doesn't un-stop a previously-stopped group, so we run
     * `playAnimation` then `pauseAnimation` to clear the stopped flag while ending up paused —
     * the tick fires next frame and applies the time-0 pose.
     */
    _isolateSelectedAnimation() {
        const groups = this._container?.animationGroups;
        if (!groups) {
            return;
        }
        for (let i = 0; i < groups.length; i++) {
            const group = groups[i];
            if (i === this._selectedAnimation) {
                playAnimation(group);
                pauseAnimation(group);
            }
            else {
                stopAnimation(group);
            }
        }
    }
    _setupAnimations() {
        const groups = this._container?.animationGroups;
        if (!groups || groups.length === 0) {
            this._selectedAnimation = -1;
            return;
        }
        // Select the first animation by default, or the one specified in options
        const defaultIndex = this._options?.selectedAnimation ?? 0;
        this._selectedAnimation = defaultIndex >= 0 && defaultIndex < groups.length ? defaultIndex : 0;
        // Stop every non-selected group; pause the selected one. See `_isolateSelectedAnimation`.
        this._isolateSelectedAnimation();
        this.onSelectedAnimationChanged.notifyObservers();
        // Auto-play if configured
        if (this._options?.animationAutoPlay) {
            this.playAnimation();
        }
    }
    _pollAnimationState() {
        const group = this._getActiveAnimationGroup();
        if (!group) {
            return;
        }
        const isPlaying = group.isPlaying;
        if (isPlaying !== this._wasPlaying) {
            this._wasPlaying = isPlaying;
            this.onIsAnimationPlayingChanged.notifyObservers();
        }
        if (isPlaying) {
            const progress = group.duration > 0 ? group.currentTime / group.duration : 0;
            if (progress !== this._lastProgress) {
                this._lastProgress = progress;
                this.onAnimationProgressChanged.notifyObservers();
            }
        }
    }
    // ── Material Variants ──
    get materialVariants() {
        if (!this._container) {
            return [];
        }
        return getVariantNames(this._container);
    }
    get selectedMaterialVariant() {
        return this._selectedMaterialVariant;
    }
    set selectedMaterialVariant(value) {
        if (value === this._selectedMaterialVariant) {
            return;
        }
        this._selectedMaterialVariant = value;
        if (this._container) {
            if (value === null) {
                resetVariant(this._container);
            }
            else {
                selectVariant(this._container, value);
            }
        }
        this.onSelectedMaterialVariantChanged.notifyObservers();
    }
    // ── Hot Spots ──
    get camerasAsHotSpots() {
        return this._camerasAsHotSpots;
    }
    set camerasAsHotSpots(value) {
        if (value === this._camerasAsHotSpots) {
            return;
        }
        this._camerasAsHotSpots = value;
        this.onCamerasAsHotSpotsChanged.notifyObservers();
    }
    queryHotSpot(name, result) {
        return this._queryHotSpot(name, result) != null;
    }
    focusHotSpot(name) {
        const result = new ViewerHotSpotResult();
        const hotSpot = this._queryHotSpot(name, result);
        if (!hotSpot) {
            return false;
        }
        observePromise(this.pauseAnimation());
        // Smoothly move the camera to the hotspot's associated orbit pose (if any), always retargeting
        // to the hotspot's world position — mirroring the full Viewer's `focusHotSpot`, which calls
        // `ArcRotateCamera.interpolateTo`. Omitted/NaN orbit components keep the camera's current value.
        const orbit = hotSpot.cameraOrbit;
        const alpha = orbit?.[0] == null ? undefined : Number(orbit[0]);
        const beta = orbit?.[1] == null ? undefined : Number(orbit[1]);
        const radius = orbit?.[2] == null ? undefined : Number(orbit[2]);
        this._interpolateCameraTo({
            alpha,
            beta,
            radius,
            target: { x: result.worldPosition[0], y: result.worldPosition[1], z: result.worldPosition[2] },
        });
        return true;
    }
    /**
     * Starts a camera interpolation toward the given goal pose, canceling any interpolation already in
     * flight. Lite's arc-rotate camera has no built-in interpolation, so this drives
     * {@link interpolateArcRotateCamera} from the scene render loop. The returned promise is intentionally
     * swallowed: it rejects when the transition is superseded, aborted, or interrupted by user input,
     * none of which are error conditions here.
     * @param goal The destination camera pose; omitted fields keep the current value.
     */
    _interpolateCameraTo(goal) {
        this._cameraInterpolationAbort?.abort(new AbortError("Camera interpolation superseded."));
        const abortController = new AbortController();
        this._cameraInterpolationAbort = abortController;
        void (async () => {
            try {
                await interpolateArcRotateCamera(this._camera, this._scene, goal, abortController.signal);
            }
            catch {
                // Superseded / aborted / interrupted by user input — all expected, not error conditions.
            }
            finally {
                if (this._cameraInterpolationAbort === abortController) {
                    this._cameraInterpolationAbort = null;
                }
            }
        })();
    }
    /**
     * Resolves a named hotspot to its world position, screen position, and visibility, writing the
     * result into `result`. Returns the hotspot definition on success (so callers like
     * {@link focusHotSpot} can read its `cameraOrbit`), or `null` if the hotspot is unknown or cannot
     * be resolved (e.g. an out-of-range surface vertex).
     *
     * Surface hotspots track skeletal + morph animation: the three referenced vertices are deformed
     * for the current frame via {@link computeDeformedPositionToRef} (mesh-local), barycentric-
     * blended, then transformed to world space by the mesh world matrix — mirroring Babylon.js core's
     * `GetHotSpotToRef`. World hotspots use their fixed position/normal.
     * @param name The name of the hotspot to resolve.
     * @param result The result object to write the world position, screen position, and visibility into.
     * @returns The hotspot definition on success, or `null` if it cannot be resolved.
     */
    _queryHotSpot(name, result) {
        const hotSpot = this.hotSpots[name];
        if (!hotSpot) {
            return null;
        }
        const worldPos = _tmpHotSpotVectors.worldPos;
        const worldNormal = _tmpHotSpotVectors.worldNormal;
        if (hotSpot.type === "surface") {
            // Hotspot `meshIndex` values are authored against the full Viewer, whose glTF loader
            // inserts a synthetic `__root__` mesh at `assetContainer.meshes[0]` — so index 1 is the
            // first real mesh. Lite's `getContainerMeshes` returns only renderable meshes (no root),
            // so shift by one to keep shared hotspot configs consistent across both viewers.
            const meshes = this._container ? getContainerMeshes(this._container) : [];
            const mesh = meshes[hotSpot.meshIndex - 1];
            if (!mesh) {
                return null;
            }
            if (!this._getSurfaceHotSpotToRef(mesh, hotSpot.pointIndex, hotSpot.barycentric, worldPos, worldNormal)) {
                return null;
            }
        }
        else {
            worldPos.x = hotSpot.position[0];
            worldPos.y = hotSpot.position[1];
            worldPos.z = hotSpot.position[2];
            worldNormal.x = hotSpot.normal[0];
            worldNormal.y = hotSpot.normal[1];
            worldNormal.z = hotSpot.normal[2];
        }
        // Project the world position to screen space. Aspect matches the rendered drawing buffer;
        // the NDC→pixel mapping uses the canvas CSS size so it aligns with the DOM annotation overlay.
        const canvas = this._engine.canvas;
        const bufferWidth = canvas.width || 1;
        const bufferHeight = canvas.height || 1;
        const cssWidth = canvas.clientWidth || bufferWidth;
        const cssHeight = canvas.clientHeight || bufferHeight;
        const aspect = getEffectiveAspectRatio(this._camera, bufferWidth, bufferHeight);
        const vp = getViewProjectionMatrix(this._camera, aspect);
        // clip = VP * [worldPos, 1] (column-major).
        const cx = vp[0] * worldPos.x + vp[4] * worldPos.y + vp[8] * worldPos.z + vp[12];
        const cy = vp[1] * worldPos.x + vp[5] * worldPos.y + vp[9] * worldPos.z + vp[13];
        const cw = vp[3] * worldPos.x + vp[7] * worldPos.y + vp[11] * worldPos.z + vp[15];
        if (cw <= 0) {
            // Behind the camera — report as invalid (matches an off-screen/back projection).
            result.screenPosition[0] = NaN;
            result.screenPosition[1] = NaN;
        }
        else {
            const ndcX = cx / cw;
            const ndcY = cy / cw;
            // Inverse of Lite's createPickingRay screen→NDC mapping (Y flipped for WebGPU).
            result.screenPosition[0] = ((ndcX + 1) / 2) * cssWidth;
            result.screenPosition[1] = ((1 - ndcY) / 2) * cssHeight;
        }
        result.worldPosition[0] = worldPos.x;
        result.worldPosition[1] = worldPos.y;
        result.worldPosition[2] = worldPos.z;
        // Visibility: dot(eyeToSurface, worldNormal). > 0 front-facing, <= 0 back-facing.
        const eye = getCameraPosition(this._camera);
        let ex = eye.x - worldPos.x;
        let ey = eye.y - worldPos.y;
        let ez = eye.z - worldPos.z;
        const len = Math.hypot(ex, ey, ez) || 1;
        ex /= len;
        ey /= len;
        ez /= len;
        result.visibility = ex * worldNormal.x + ey * worldNormal.y + ez * worldNormal.z;
        return hotSpot;
    }
    /**
     * Computes the world-space position and normal of a surface hotspot on `mesh` from three vertex
     * indices and barycentric weights, applying the mesh's current animation pose. Mirrors core's
     * `GetHotSpotToRef`: deform each vertex to mesh-local space, blend by barycentric, then transform
     * the single blended point (and the local triangle normal) to world space.
     * @param mesh The mesh the hotspot is anchored to.
     * @param pointIndex The three vertex indices defining the hotspot's triangle.
     * @param barycentric The barycentric weights blending the three vertices.
     * @param outPos Receives the world-space hotspot position.
     * @param outNormal Receives the world-space hotspot normal.
     * @returns `true` if the position and normal were computed, or `false` if a vertex is out of range.
     */
    _getSurfaceHotSpotToRef(mesh, pointIndex, barycentric, outPos, outNormal) {
        const a = _tmpHotSpotVectors.a;
        const b = _tmpHotSpotVectors.b;
        const c = _tmpHotSpotVectors.c;
        if (!computeDeformedPositionToRef(mesh, pointIndex[0], a) ||
            !computeDeformedPositionToRef(mesh, pointIndex[1], b) ||
            !computeDeformedPositionToRef(mesh, pointIndex[2], c)) {
            return false;
        }
        // Barycentric blend in mesh-local space.
        const lx = a.x * barycentric[0] + b.x * barycentric[1] + c.x * barycentric[2];
        const ly = a.y * barycentric[0] + b.y * barycentric[1] + c.y * barycentric[2];
        const lz = a.z * barycentric[0] + b.z * barycentric[1] + c.z * barycentric[2];
        // Local triangle normal = (b - a) x (c - a).
        const abx = b.x - a.x;
        const aby = b.y - a.y;
        const abz = b.z - a.z;
        const acx = c.x - a.x;
        const acy = c.y - a.y;
        const acz = c.z - a.z;
        const nx = aby * acz - abz * acy;
        const ny = abz * acx - abx * acz;
        const nz = abx * acy - aby * acx;
        const m = mesh.worldMatrix;
        // Position → world (column-major, with translation).
        outPos.x = m[0] * lx + m[4] * ly + m[8] * lz + m[12];
        outPos.y = m[1] * lx + m[5] * ly + m[9] * lz + m[13];
        outPos.z = m[2] * lx + m[6] * ly + m[10] * lz + m[14];
        // Normal → world (rotation/scale only, no translation), then normalize.
        const wnx = m[0] * nx + m[4] * ny + m[8] * nz;
        const wny = m[1] * nx + m[5] * ny + m[9] * nz;
        const wnz = m[2] * nx + m[6] * ny + m[10] * nz;
        const nlen = Math.hypot(wnx, wny, wnz) || 1;
        outNormal.x = wnx / nlen;
        outNormal.y = wny / nlen;
        outNormal.z = wnz / nlen;
        return true;
    }
    // ── State ──
    get isModelLoaded() {
        return this._container !== null;
    }
    /** @internal */
    _resetEnvironment() {
        // Reset scalar env config to defaults (matches full Viewer's reset hook). The setter fires
        // `onEnvironmentConfigurationChanged` and is independent of the URL state.
        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,
        };
        observePromise(this.resetEnvironment());
        const initialLightingUrl = this._options?.environmentLighting ?? DefaultViewerOptions.environmentLighting;
        const initialSkyboxUrl = this._options?.environmentSkybox ?? DefaultViewerOptions.environmentSkybox;
        if (initialLightingUrl === initialSkyboxUrl) {
            if (initialLightingUrl !== "none") {
                observePromise(this.loadEnvironment(initialLightingUrl));
            }
        }
        else {
            if (initialLightingUrl !== "none") {
                observePromise(this.loadEnvironment(initialLightingUrl, { lighting: true, skybox: false }));
            }
            if (initialSkyboxUrl !== "none") {
                observePromise(this.loadEnvironment(initialSkyboxUrl, { lighting: false, skybox: true }));
            }
        }
    }
    /** @internal */
    _resetAnimation() {
        const groups = this._container?.animationGroups;
        if (groups && groups.length > 0) {
            this._selectedAnimation = this._options?.selectedAnimation ?? 0;
            this._animationSpeed = this._options?.animationSpeed ?? 1;
            this._isolateSelectedAnimation();
            this.onSelectedAnimationChanged.notifyObservers();
            this.onAnimationSpeedChanged.notifyObservers();
            if (this._options?.animationAutoPlay) {
                this.playAnimation();
            }
        }
    }
    /**
     * @internal
     * Resets the camera to its default/framing pose, animating the transition when `interpolate` is true
     * (e.g. a user-initiated reset) and snapping when false (e.g. an initial reset before the first frame).
     */
    _resetCamera(interpolate) {
        this._resetCameraCore(undefined, interpolate);
        this.cameraAutoOrbit = this._options?.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,
            }
            : { ...DefaultViewerOptions.cameraAutoOrbit };
    }
    /** @internal */
    _resetPostProcessing() {
        // Route through the public setter so we get free dedup + observable-notify-on-change
        // semantics, matching what the user-facing API does.
        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,
        };
    }
    /**
     * Registers the scene with the engine and starts the render loop.
     * Safe to call multiple times — stops and re-registers if already running.
     * @remarks
     * Serialized against suspend/resume via {@link _renderLoopLock} so a scroll-driven suspension can never
     * land in the middle of the stop, unregister, register, start sequence below.
     */
    async _beginRendering() {
        await this._renderLoopLock.lockAsync(async () => {
            if (this._isDisposed) {
                return;
            }
            this._stopRenderLoop();
            if (this._sceneRegistered) {
                unregisterScene(this._scene);
                this._sceneRegistered = false;
            }
            // Install the scene-owned frame-graph shadow task only when shadows are enabled, so the
            // shadow-task bundle is tree-shaken out for viewers that never render shadows.
            if (this._shadowQuality === "none") {
                await registerScene(this._scene);
            }
            else {
                await registerSceneWithShadowSupport(this._scene);
            }
            if (this._isDisposed) {
                return;
            }
            this._sceneRegistered = true;
            await this._startRenderLoop();
        });
    }
    /**
     * Starts the engine's render loop, unless rendering is suspended (or the viewer is disposed), and waits
     * for the first frame.
     * @remarks
     * Lite's `startEngine` promise resolves from inside the rAF callback, so it never settles if the loop is
     * stopped before that first frame. {@link _renderLoopStopped} races against it so a suspension or a
     * disposal arriving in that window can't leave this await (and any model load awaiting it) pending forever.
     */
    async _startRenderLoop() {
        if (this._renderLoopRunning || this._isDisposed || this._suspendRenderCount > 0) {
            return;
        }
        this._renderLoopRunning = true;
        const firstFrame = startEngine(this._engine);
        const stopped = new Promise((resolve) => (this._renderLoopStopped = resolve));
        await Promise.race([firstFrame, stopped]);
        this._renderLoopStopped = null;
    }
    /** Stops the engine's render loop (if running) and releases anyone awaiting its first frame. */
    _stopRenderLoop() {
        if (this._renderLoopRunning) {
            stopEngine(this._engine);
            this._renderLoopRunning = false;
        }
        this._renderLoopStopped?.();
        this._renderLoopStopped = null;
    }
    /**
     * Suspends rendering until the returned disposable is disposed.
     * @remarks
     * Reference counted, mirroring the full Viewer: rendering only resumes once every suspension handle has
     * been disposed. The scene stays registered while suspended, so resuming does not rebuild anything.
     * @returns A disposable that resumes rendering (when no other suspensions are outstanding).
     * @internal
     */
    _suspendRendering() {
        if (this._suspendRenderCount++ === 0) {
            observePromise(this._renderLoopLock.lockAsync(() => this._stopRenderLoop()));
        }
        let disposed = false;
        return {
            dispose: () => {
                if (!disposed) {
                    disposed = true;
                    if (--this._suspendRenderCount === 0) {
                        // Not awaited: resuming must not block the caller (typically an IntersectionObserver
                        // callback) on the first rendered frame.
                        observePromise(this._renderLoopLock.lockAsync(async () => await this._startRenderLoop()));
                    }
                }
            },
        };
    }
    dispose() {
        if (this._isDisposed) {
            return;
        }
        // Disable device-lost recovery before any teardown below: everything that follows frees GPU
        // resources (picker, model, scene, engine), and a loss arriving mid-teardown would otherwise
        // kick off a rebuild against resources that are in the process of being destroyed.
        this._deviceLostRecovery.disable();
        // Detach camera controls
        this._detachControl?.();
        this._detachControl = null;
        // Cancel any in-flight camera interpolation so its render-loop callback stops touching the scene.
        this._cameraInterpolationAbort?.abort(new AbortError("Viewer disposed."));
        this._cameraInterpolationAbort = null;
        // Clean up pointer listeners
        this._engine.canvas.removeEventListener("pointerdown", this._onPointerActivity);
        this._engine.canvas.removeEventListener("pointermove", this._onPointerActivity);
        this._engine.canvas.removeEventListener("wheel", this._onPointerActivity);
        this._engine.canvas.removeEventListener("dblclick", this._onCanvasDoubleClick);
        // Dispose the GPU picker (if a double-click ever created it).
        if (this._picker) {
            disposePicker(this._picker);
            this._picker = null;
        }
        // Unload model
        this._unloadCurrentModel();
        // Stop and dispose engine/scene
        this._stopRenderLoop();
        unregisterScene(this._scene);
        this._sceneRegistered = false;
        disposeScene(this._scene);
        disposeEngine(this._engine);
        // Base disposes observables and sets _isDisposed = true
        super.dispose();
    }
    /**
     * Picks the model at the given canvas coordinates and either focuses the picked point (hit) or
     * reframes the camera (miss). Only the loaded model's meshes are pickable, so Viewer-added meshes
     * (e.g. the shadow-receiver disc) never swallow a pick or count as a "model" hit.
     * @param x The canvas-relative CSS x coordinate of the double-click.
     * @param y The canvas-relative CSS y coordinate of the double-click.
     */
    async _handleDoubleClick(x, y) {
        // With no loaded model there is nothing to pick; treat as a background double-click (reframe).
        if (this._container) {
            const pickables = new Set(getContainerMeshes(this._container));
            this._picker ?? (this._picker = createGpuPicker(this._scene));
            const pick = await pickAsync(this._picker, x, y, { filter: (mesh) => pickables.has(mesh) });
            if (this._isDisposed) {
                return;
            }
            if (pick.hit && pick.pickedPoint) {
                this._focusCameraOnPoint(pick.pickedPoint);
                return;
            }
        }
        // Background double-click: reframe the camera to the model bounds (animated).
        this.resetCamera(true);
    }
    /**
     * Focuses the camera on a world-space point, mirroring the full Viewer's double-tap-on-model behavior.
     * The target and radius are first snapped so the point lies on the current view axis at its picked
     * depth — this preserves the camera position and avoids a dolly along the view axis — then the target
     * is interpolated to the actual point (orbit angles and radius held).
     * @param point The world-space point to focus on.
     */
    _focusCameraOnPoint(point) {
        const position = getCameraPosition(this._camera);
        const target = this._camera.target;
        // Forward (view) direction of the ArcRotate camera: from the camera position toward the target.
        let fx = target.x - position.x;
        let fy = target.y - position.y;
        let fz = target.z - position.z;
        const length = Math.hypot(fx, fy, fz) || 1;
        fx /= length;
        fy /= length;
        fz /= length;
        // Distance to the picked point measured along the view axis.
        const distance = (point[0] - position.x) * fx + (point[1] - position.y) * fy + (point[2] - position.z) * fz;
        // Snap the target onto the view axis at the picked depth and set the radius to match. Because
        // position = target - forward * radius, this leaves the camera position unchanged; only the
        // subsequent target interpolation moves the camera. Mutate the ObservableVec3 in place to keep
        // Lite's dirty-tracking. This must precede the interpolation so it starts from this pose.
        target.x = position.x + fx * distance;
        target.y = position.y + fy * distance;
        target.z = position.z + fz * distance;
        this._camera.radius = distance;
        // Interpolate the target to the actual picked point (orbit angles and radius held).
        this._interpolateCameraTo({ target: { x: point[0], y: point[1], z: point[2] } });
    }
    _updateAutoOrbit(deltaMs) {
        if (!this._autoOrbitEnabled || this._cameraInterpolationAbort) {
            this._autoOrbitIdleTime = 0;
            return;
        }
        const now = performance.now();
        const idleMs = now - this._lastPointerTime;
        if (idleMs < this._autoOrbitDelay) {
            this._autoOrbitIdleTime = 0;
            return;
        }
        this._autoOrbitIdleTime += deltaMs;
        // Rotate alpha based on speed (radians per second)
        const rotationAmount = (this._autoOrbitSpeed * deltaMs) / 1000;
        this._camera.alpha += rotationAmount;
    }
}
/**
 * Creates a new {@link Viewer} instance for the given canvas element.
 * @param canvas The HTML canvas element to render into.
 * @param options Optional viewer configuration.
 * @returns A promise that resolves to the initialized Viewer.
 */
async function CreateViewerForCanvas(canvas, options) {
    const engine = await createEngine(canvas, { alphaMode: "premultiplied" });
    const viewer = new Viewer(engine, options);
    // If the canvas is not visible, suspend rendering. Matches the full Viewer, where this is unconditional
    // (the `autoSuspendRendering` option gates idle suspension, not offscreen suspension).
    const offscreenSuspensionObserver = SuspendRenderingWhenOffscreen(canvas, () => viewer._suspendRendering());
    // Override the Viewer's dispose method to add in additional cleanup. Matches the full Viewer's factory:
    // only the observer needs disconnecting — any live suspension handle is dropped along with the Viewer.
    const disposeViewer = viewer.dispose.bind(viewer);
    viewer.dispose = () => {
        offscreenSuspensionObserver.dispose();
        disposeViewer();
    };
    return viewer;
}

/**
 * Viewer custom element backed by the Babylon Lite engine (WebGPU-only).
 * Provides the same `<babylon-viewer>` tag as the full viewer — the two are mutually exclusive.
 */
class ViewerElement extends ViewerElementBase {
    constructor(options = {}) {
        super(options);
    }
    /**
     * Gets the underlying Viewer instance (when the viewer is in a loaded state).
     */
    get viewer() {
        return this._viewer;
    }
    async _createViewer(canvas, options) {
        return await CreateViewerForCanvas(canvas, options);
    }
}
/**
 * Displays a 3D model using the Babylon Lite Viewer (WebGPU-only).
 * @remarks
 * This element registers as `<babylon-viewer>` and is mutually exclusive with the full Babylon.js viewer element.
 * Import `@babylonjs/viewer/lite` instead of `@babylonjs/viewer` to use the Lite viewer.
 */
let HTML3DElement = (() => {
    let _classDecorators = [t$3("babylon-viewer")];
    let _classDescriptor;
    let _classExtraInitializers = [];
    let _classThis;
    let _classSuper = ViewerElement;
    _classThis = class extends _classSuper {
        /**
         * Creates a new HTML3DElement backed by the Lite viewer.
         * @param options The options to use for the viewer.
         */
        constructor(options) {
            super(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$3("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$3({ 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, CreateViewerForCanvas, DefaultViewerOptions, HTML3DAnnotationElement, HTML3DElement, Viewer, ViewerElement, ViewerHotSpotResult };
//# sourceMappingURL=index.js.map