UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

295 lines 13.8 kB
/* @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