@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
295 lines • 13.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-element';
import { IS_AR_QUICKLOOK_CANDIDATE, IS_SCENEVIEWER_CANDIDATE, IS_WEBXR_AR_CANDIDATE } from '../constants.js';
import { $loaded, $needsRender, $renderer, $scene, $shouldAttemptPreload, $updateSource } from '../model-viewer-base.js';
import { enumerationDeserializer } from '../styles/deserializers.js';
import { ARStatus } from '../three-components/ARRenderer.js';
import { waitForEvent } from '../utilities.js';
let isWebXRBlocked = false;
let isSceneViewerBlocked = false;
const noArViewerSigil = '#model-viewer-no-ar-fallback';
const deserializeARModes = enumerationDeserializer(['quick-look', 'scene-viewer', 'webxr', 'none']);
const DEFAULT_AR_MODES = 'webxr scene-viewer quick-look';
const ARMode = {
QUICK_LOOK: 'quick-look',
SCENE_VIEWER: 'scene-viewer',
WEBXR: 'webxr',
NONE: 'none'
};
const $arButtonContainer = Symbol('arButtonContainer');
const $enterARWithWebXR = Symbol('enterARWithWebXR');
export const $openSceneViewer = Symbol('openSceneViewer');
export const $openIOSARQuickLook = Symbol('openIOSARQuickLook');
const $canActivateAR = Symbol('canActivateAR');
const $arMode = Symbol('arMode');
const $arModes = Symbol('arModes');
const $arAnchor = Symbol('arAnchor');
const $preload = Symbol('preload');
const $onARButtonContainerClick = Symbol('onARButtonContainerClick');
const $onARStatus = Symbol('onARStatus');
const $onARTap = Symbol('onARTap');
const $selectARMode = Symbol('selectARMode');
export const ARMixin = (ModelViewerElement) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
class ARModelViewerElement extends ModelViewerElement {
constructor() {
super(...arguments);
this.ar = false;
this.arScale = 'auto';
this.arPlacement = 'floor';
this.arModes = DEFAULT_AR_MODES;
this.iosSrc = null;
this[_a] = false;
// TODO: Add this to the shadow root as part of this mixin's
// implementation:
this[_b] = this.shadowRoot.querySelector('.ar-button');
this[_c] = document.createElement('a');
this[_d] = new Set();
this[_e] = ARMode.NONE;
this[_f] = false;
this[_g] = (event) => {
event.preventDefault();
this.activateAR();
};
this[_h] = ({ status }) => {
if (status === ARStatus.NOT_PRESENTING ||
this[$renderer].arRenderer.presentedScene === this[$scene]) {
this.setAttribute('ar-status', status);
this.dispatchEvent(new CustomEvent('ar-status', { detail: { status } }));
}
};
this[_j] = (event) => {
if (event.data == '_apple_ar_quicklook_button_tapped') {
this.dispatchEvent(new CustomEvent('quick-look-button-tapped'));
}
};
}
get canActivateAR() {
return this[$arMode] !== ARMode.NONE;
}
connectedCallback() {
super.connectedCallback();
this[$renderer].arRenderer.addEventListener('status', this[$onARStatus]);
this.setAttribute('ar-status', ARStatus.NOT_PRESENTING);
this[$arAnchor].addEventListener('message', this[$onARTap]);
}
disconnectedCallback() {
super.disconnectedCallback();
this[$renderer].arRenderer.removeEventListener('status', this[$onARStatus]);
this[$arAnchor].removeEventListener('message', this[$onARTap]);
}
async update(changedProperties) {
super.update(changedProperties);
if (changedProperties.has('arScale')) {
this[$scene].canScale = this.arScale !== 'fixed';
}
if (changedProperties.has('arPlacement')) {
this[$scene].setShadowIntensity(this[$scene].shadowIntensity);
this[$needsRender]();
}
if (!changedProperties.has('ar') && !changedProperties.has('arModes') &&
!changedProperties.has('iosSrc')) {
return;
}
if (changedProperties.has('arModes')) {
this[$arModes] = deserializeARModes(this.arModes);
}
this[$selectARMode]();
}
/**
* Activates AR. Note that for any mode that is not WebXR-based, this
* method most likely has to be called synchronous from a user
* interaction handler. Otherwise, attempts to activate modes that
* require user interaction will most likely be ignored.
*/
async activateAR() {
switch (this[$arMode]) {
case ARMode.QUICK_LOOK:
this[$openIOSARQuickLook]();
break;
case ARMode.WEBXR:
await this[$enterARWithWebXR]();
break;
case ARMode.SCENE_VIEWER:
this[$openSceneViewer]();
break;
default:
console.warn('No AR Mode can be activated. This is probably due to missing \
configuration or device capabilities');
break;
}
}
async [(_a = $canActivateAR, _b = $arButtonContainer, _c = $arAnchor, _d = $arModes, _e = $arMode, _f = $preload, _g = $onARButtonContainerClick, _h = $onARStatus, _j = $onARTap, $selectARMode)]() {
this[$arMode] = ARMode.NONE;
if (this.ar) {
const arModes = [];
this[$arModes].forEach((value) => {
arModes.push(value);
});
for (const value of arModes) {
if (value === 'webxr' && IS_WEBXR_AR_CANDIDATE && !isWebXRBlocked &&
await this[$renderer].arRenderer.supportsPresentation()) {
this[$arMode] = ARMode.WEBXR;
break;
}
else if (value === 'scene-viewer' && IS_SCENEVIEWER_CANDIDATE &&
!isSceneViewerBlocked) {
this[$arMode] = ARMode.SCENE_VIEWER;
break;
}
else if (value === 'quick-look' && !!this.iosSrc &&
IS_AR_QUICKLOOK_CANDIDATE) {
this[$arMode] = ARMode.QUICK_LOOK;
break;
}
}
}
if (this.canActivateAR) {
this[$arButtonContainer].classList.add('enabled');
this[$arButtonContainer].addEventListener('click', this[$onARButtonContainerClick]);
}
else if (this[$arButtonContainer].classList.contains('enabled')) {
this[$arButtonContainer].removeEventListener('click', this[$onARButtonContainerClick]);
this[$arButtonContainer].classList.remove('enabled');
// If AR went from working to not, notify the element.
const status = ARStatus.FAILED;
this.setAttribute('ar-status', status);
this.dispatchEvent(new CustomEvent('ar-status', { detail: { status } }));
}
}
async [$enterARWithWebXR]() {
console.log('Attempting to present in AR...');
if (!this[$loaded]) {
this[$preload] = true;
this[$updateSource]();
await waitForEvent(this, 'load');
this[$preload] = false;
}
try {
this[$arButtonContainer].removeEventListener('click', this[$onARButtonContainerClick]);
const { arRenderer } = this[$renderer];
arRenderer.placeOnWall = this.arPlacement === 'wall';
await arRenderer.present(this[$scene]);
}
catch (error) {
console.warn('Error while trying to present to AR');
console.error(error);
await this[$renderer].arRenderer.stopPresenting();
isWebXRBlocked = true;
await this[$selectARMode]();
this.activateAR();
}
finally {
this[$selectARMode]();
}
}
[$shouldAttemptPreload]() {
return super[$shouldAttemptPreload]() || this[$preload];
}
/**
* Takes a URL and a title string, and attempts to launch Scene Viewer on
* the current device.
*/
[$openSceneViewer]() {
const location = self.location.toString();
const locationUrl = new URL(location);
const modelUrl = new URL(this.src, location);
const params = new URLSearchParams(modelUrl.search);
locationUrl.hash = noArViewerSigil;
// modelUrl can contain title/link/sound etc.
params.set('mode', 'ar_only');
if (!params.has('disable_occlusion')) {
params.set('disable_occlusion', 'true');
}
if (this.arScale === 'fixed') {
params.set('resizable', 'false');
}
if (this.arPlacement === 'wall') {
params.set('enable_vertical_placement', 'true');
}
if (params.has('sound')) {
const soundUrl = new URL(params.get('sound'), location);
params.set('sound', soundUrl.toString());
}
if (params.has('link')) {
const linkUrl = new URL(params.get('link'), location);
params.set('link', linkUrl.toString());
}
const intent = `intent://arvr.google.com/scene-viewer/1.0?${params
.toString() + '&file=' + encodeURIComponent(modelUrl.toString())}#Intent;scheme=https;package=com.google.ar.core;action=android.intent.action.VIEW;S.browser_fallback_url=${encodeURIComponent(locationUrl.toString())};end;`;
const undoHashChange = () => {
if (self.location.hash === noArViewerSigil) {
isSceneViewerBlocked = true;
// The new history will be the current URL with a new hash.
// Go back one step so that we reset to the expected URL.
// NOTE(cdata): this should not invoke any browser-level navigation
// because hash-only changes modify the URL in-place without
// navigating:
self.history.back();
this[$selectARMode]();
// Would be nice to activateAR() here, but webXR fails due to not
// seeing a user activation.
}
};
self.addEventListener('hashchange', undoHashChange, { once: true });
this[$arAnchor].setAttribute('href', intent);
this[$arAnchor].click();
}
/**
* Takes a URL to a USDZ file and sets the appropriate fields so that Safari
* iOS can intent to their AR Quick Look.
*/
[$openIOSARQuickLook]() {
const modelUrl = new URL(this.iosSrc, self.location.toString());
if (this.arScale === 'fixed') {
if (modelUrl.hash) {
modelUrl.hash += '&';
}
modelUrl.hash += 'allowsContentScaling=0';
}
const anchor = this[$arAnchor];
anchor.setAttribute('rel', 'ar');
const img = document.createElement('img');
anchor.appendChild(img);
anchor.setAttribute('href', modelUrl.toString());
anchor.click();
anchor.removeChild(img);
}
}
__decorate([
property({ type: Boolean, attribute: 'ar' })
], ARModelViewerElement.prototype, "ar", void 0);
__decorate([
property({ type: String, attribute: 'ar-scale' })
], ARModelViewerElement.prototype, "arScale", void 0);
__decorate([
property({ type: String, attribute: 'ar-placement' })
], ARModelViewerElement.prototype, "arPlacement", void 0);
__decorate([
property({ type: String, attribute: 'ar-modes' })
], ARModelViewerElement.prototype, "arModes", void 0);
__decorate([
property({ type: String, attribute: 'ios-src' })
], ARModelViewerElement.prototype, "iosSrc", void 0);
return ARModelViewerElement;
};
//# sourceMappingURL=ar.js.map