@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
324 lines • 15.8 kB
JavaScript
/* @license
* Copyright 2019 Google LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { property } from 'lit/decorators.js';
import { Vector3 } from 'three';
import { $altDefaulted, $announceModelVisibility, $getModelIsVisible, $isElementInViewport, $progressTracker, $scene, $shouldAttemptPreload, $updateSource, $userInputElement, toVector3D } from '../model-viewer-base.js';
import { $loader, CachingGLTFLoader } from '../three-components/CachingGLTFLoader.js';
import { Renderer } from '../three-components/Renderer.js';
import { throttle } from '../utilities.js';
export const PROGRESS_BAR_UPDATE_THRESHOLD = 100;
const DEFAULT_DRACO_DECODER_LOCATION = 'https://www.gstatic.com/draco/versioned/decoders/1.5.6/';
const DEFAULT_KTX2_TRANSCODER_LOCATION = 'https://www.gstatic.com/basis-universal/versioned/2021-04-15-ba1c3e4/';
const DEFAULT_LOTTIE_LOADER_LOCATION = 'https://cdn.jsdelivr.net/npm/three@0.149.0/examples/jsm/loaders/LottieLoader.js';
const RevealStrategy = {
AUTO: 'auto',
MANUAL: 'manual'
};
const LoadingStrategy = {
AUTO: 'auto',
LAZY: 'lazy',
EAGER: 'eager'
};
export const $defaultProgressBarElement = Symbol('defaultProgressBarElement');
export const $posterContainerElement = Symbol('posterContainerElement');
export const $defaultPosterElement = Symbol('defaultPosterElement');
const $shouldDismissPoster = Symbol('shouldDismissPoster');
const $hidePoster = Symbol('hidePoster');
const $modelIsRevealed = Symbol('modelIsRevealed');
const $updateProgressBar = Symbol('updateProgressBar');
const $ariaLabelCallToAction = Symbol('ariaLabelCallToAction');
const $onProgress = Symbol('onProgress');
/**
* LoadingMixin implements features related to lazy loading, as well as
* presentation details related to the pre-load / pre-render presentation of a
* <model-viewer>
*
* This mixin implements support for models with DRACO-compressed meshes.
* The DRACO decoder will be loaded on-demand if a glTF that uses the DRACO mesh
* compression extension is encountered.
*
* By default, the DRACO decoder will be loaded from a Google CDN. It is
* possible to customize where the decoder is loaded from by defining a global
* configuration option for `<model-viewer>` like so:
*
* ```html
* <script>
* self.ModelViewerElement = self.ModelViewerElement || {};
* self.ModelViewerElement.dracoDecoderLocation =
* 'http://example.com/location/of/draco/decoder/files/';
* </script>
* ```
*
* Note that the above configuration strategy must be performed *before* the
* first `<model-viewer>` element is created in the browser. The configuration
* can be done anywhere, but the easiest way to ensure it is done at the right
* time is to do it in the `<head>` of the HTML document. This is the
* recommended way to set the location because it is most compatible with
* scenarios where the `<model-viewer>` library is lazily loaded.
*
* If you absolutely have to set the DRACO decoder location *after* the first
* `<model-viewer>` element is created, you can do it this way:
*
* ```html
* <script>
* const ModelViewerElement = customElements.get('model-viewer');
* ModelViewerElement.dracoDecoderLocation =
* 'http://example.com/location/of/draco/decoder/files/';
* </script>
* ```
*
* Note that the above configuration approach will not work until *after*
* `<model-viewer>` is defined in the browser. Also note that this configuration
* *must* be set *before* the first DRACO model is fully loaded.
*
* It is recommended that users who intend to take advantage of DRACO mesh
* compression consider whether or not it is acceptable for their use case to
* have code side-loaded from a Google CDN. If it is not acceptable, then the
* location must be customized before loading any DRACO models in order to cause
* the decoder to be loaded from an alternative, acceptable location.
*/
export const LoadingMixin = (ModelViewerElement) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
class LoadingModelViewerElement extends ModelViewerElement {
constructor(...args) {
super(...args);
/**
* A URL pointing to the image to use as a poster in scenarios where the
* <model-viewer> is not ready to reveal a rendered model to the viewer.
*/
this.poster = null;
/**
* An enumerable attribute describing under what conditions the
* <model-viewer> should reveal a model to the viewer.
*
* The default value is "auto". The only supported alternative values is
* "manual".
*/
this.reveal = RevealStrategy.AUTO;
/**
* An enumerable attribute describing under what conditions the
* <model-viewer> should preload a model.
*
* The default value is "auto". The only supported alternative values are
* "lazy" and "eager". Auto is equivalent to lazy, which loads the model
* when it is near the viewport for reveal = "auto", and when interacted
* with for reveal = "interaction". Eager loads the model immediately.
*/
this.loading = LoadingStrategy.AUTO;
this[_a] = false;
this[_b] = false;
// TODO: Add this to the shadow root as part of this mixin's
// implementation:
this[_c] = this.shadowRoot.querySelector('.slot.poster');
this[_d] = this.shadowRoot.querySelector('#default-poster');
this[_e] = this.shadowRoot.querySelector('#default-progress-bar > .bar');
this[_f] = this[$defaultPosterElement].getAttribute('aria-label');
this[_g] = throttle((progress) => {
const parentNode = this[$defaultProgressBarElement].parentNode;
requestAnimationFrame(() => {
this[$defaultProgressBarElement].style.transform =
`scaleX(${progress})`;
if (progress === 0) {
// NOTE(cdata): We remove and re-append the progress bar in this
// condition so that the progress bar does not appear to
// transition backwards from the right when we reset to 0 (or
// otherwise <1) progress after having already reached 1 progress
// previously.
parentNode.removeChild(this[$defaultProgressBarElement]);
parentNode.appendChild(this[$defaultProgressBarElement]);
}
this[$defaultProgressBarElement].classList.toggle('hide', progress === 1.0);
});
}, PROGRESS_BAR_UPDATE_THRESHOLD);
this[_h] = (event) => {
const progress = event.detail.totalProgress;
const reason = event.detail.reason;
if (progress === 1.0) {
this[$updateProgressBar].flush();
if (this.loaded &&
(this[$shouldDismissPoster] ||
this.reveal === RevealStrategy.AUTO)) {
this[$hidePoster]();
}
}
this[$updateProgressBar](progress);
this.dispatchEvent(new CustomEvent('progress', { detail: { totalProgress: progress, reason } }));
};
const ModelViewerElement = self.ModelViewerElement || {};
const dracoDecoderLocation = ModelViewerElement.dracoDecoderLocation ||
DEFAULT_DRACO_DECODER_LOCATION;
CachingGLTFLoader.setDRACODecoderLocation(dracoDecoderLocation);
const ktx2TranscoderLocation = ModelViewerElement.ktx2TranscoderLocation ||
DEFAULT_KTX2_TRANSCODER_LOCATION;
CachingGLTFLoader.setKTX2TranscoderLocation(ktx2TranscoderLocation);
if (ModelViewerElement.meshoptDecoderLocation) {
CachingGLTFLoader.setMeshoptDecoderLocation(ModelViewerElement.meshoptDecoderLocation);
}
const lottieLoaderLocation = ModelViewerElement.lottieLoaderLocation ||
DEFAULT_LOTTIE_LOADER_LOCATION;
Renderer.singleton.textureUtils.lottieLoaderUrl = lottieLoaderLocation;
}
static set dracoDecoderLocation(value) {
CachingGLTFLoader.setDRACODecoderLocation(value);
}
static get dracoDecoderLocation() {
return CachingGLTFLoader.getDRACODecoderLocation();
}
static set ktx2TranscoderLocation(value) {
CachingGLTFLoader.setKTX2TranscoderLocation(value);
}
static get ktx2TranscoderLocation() {
return CachingGLTFLoader.getKTX2TranscoderLocation();
}
static set meshoptDecoderLocation(value) {
CachingGLTFLoader.setMeshoptDecoderLocation(value);
}
static get meshoptDecoderLocation() {
return CachingGLTFLoader.getMeshoptDecoderLocation();
}
static set lottieLoaderLocation(value) {
Renderer.singleton.textureUtils.lottieLoaderUrl = value;
}
static get lottieLoaderLocation() {
return Renderer.singleton.textureUtils.lottieLoaderUrl;
}
/**
* If provided, the callback will be passed each resource URL before a
* request is sent. The callback may return the original URL, or a new URL
* to override loading behavior. This behavior can be used to load assets
* from .ZIP files, drag-and-drop APIs, and Data URIs.
*/
static mapURLs(callback) {
Renderer.singleton.loader[$loader].manager.setURLModifier(callback);
}
/**
* Dismisses the poster, causing the model to load and render if
* necessary. This is currently effectively the same as interacting with
* the poster via user input.
*/
dismissPoster() {
if (this.loaded) {
this[$hidePoster]();
}
else {
this[$shouldDismissPoster] = true;
this[$updateSource]();
}
}
/**
* Displays the poster, hiding the 3D model. If this is called after the 3D
* model has been revealed, then it must be dismissed by a call to
* dismissPoster().
*/
showPoster() {
const posterContainerElement = this[$posterContainerElement];
if (posterContainerElement.classList.contains('show')) {
return;
}
posterContainerElement.classList.add('show');
this[$userInputElement].classList.remove('show');
const defaultPosterElement = this[$defaultPosterElement];
defaultPosterElement.removeAttribute('tabindex');
defaultPosterElement.removeAttribute('aria-hidden');
const oldVisibility = this.modelIsVisible;
this[$modelIsRevealed] = false;
this[$announceModelVisibility](oldVisibility);
}
/**
* Returns the model's bounding box dimensions in meters, independent of
* turntable rotation.
*/
getDimensions() {
return toVector3D(this[$scene].size);
}
getBoundingBoxCenter() {
return toVector3D(this[$scene].boundingBox.getCenter(new Vector3()));
}
connectedCallback() {
super.connectedCallback();
if (!this.loaded) {
this.showPoster();
}
this[$progressTracker].addEventListener('progress', this[$onProgress]);
}
disconnectedCallback() {
super.disconnectedCallback();
this[$progressTracker].removeEventListener('progress', this[$onProgress]);
}
async updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('poster') && this.poster != null) {
this[$defaultPosterElement].style.backgroundImage =
`url(${this.poster})`;
}
if (changedProperties.has('alt')) {
this[$defaultPosterElement].setAttribute('aria-label', this[$altDefaulted]);
}
if (changedProperties.has('reveal') || changedProperties.has('loading')) {
this[$updateSource]();
}
}
[(_a = $modelIsRevealed, _b = $shouldDismissPoster, _c = $posterContainerElement, _d = $defaultPosterElement, _e = $defaultProgressBarElement, _f = $ariaLabelCallToAction, _g = $updateProgressBar, _h = $onProgress, $shouldAttemptPreload)]() {
return !!this.src &&
(this[$shouldDismissPoster] ||
this.loading === LoadingStrategy.EAGER ||
(this.reveal === RevealStrategy.AUTO && this[$isElementInViewport]));
}
[$hidePoster]() {
this[$shouldDismissPoster] = false;
const posterContainerElement = this[$posterContainerElement];
if (!posterContainerElement.classList.contains('show')) {
return;
}
posterContainerElement.classList.remove('show');
this[$userInputElement].classList.add('show');
const oldVisibility = this.modelIsVisible;
this[$modelIsRevealed] = true;
this[$announceModelVisibility](oldVisibility);
const root = this.getRootNode();
// If the <model-viewer> is still focused, forward the focus to
// the canvas that has just been revealed
if (root && root.activeElement === this) {
this[$userInputElement].focus();
}
// Ensure that the poster is no longer focusable or visible to
// screen readers
const defaultPosterElement = this[$defaultPosterElement];
defaultPosterElement.setAttribute('aria-hidden', 'true');
defaultPosterElement.tabIndex = -1;
this.dispatchEvent(new CustomEvent('poster-dismissed'));
}
[$getModelIsVisible]() {
return super[$getModelIsVisible]() && this[$modelIsRevealed];
}
}
__decorate([
property({ type: String })
], LoadingModelViewerElement.prototype, "poster", void 0);
__decorate([
property({ type: String })
], LoadingModelViewerElement.prototype, "reveal", void 0);
__decorate([
property({ type: String })
], LoadingModelViewerElement.prototype, "loading", void 0);
return LoadingModelViewerElement;
};
//# sourceMappingURL=loading.js.map