UNPKG

@google/model-viewer

Version:

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

293 lines 14.5 kB
/* * Copyright 2018 Google Inc. 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_ANDROID, IS_AR_QUICKLOOK_CANDIDATE, IS_IOS, IS_IOS_CHROME, IS_IOS_SAFARI, IS_WEBXR_AR_CANDIDATE } from '../constants.js'; import { enumerationDeserializer } from '../conversions.js'; import { $container, $renderer, $scene } from '../model-viewer-base.js'; import { deserializeUrl } from '../utilities.js'; /** * Takes a URL to a USDZ file and sets the appropriate fields so that Safari * iOS can intent to their AR Quick Look. */ export const openIOSARQuickLook = (() => { const anchor = document.createElement('a'); anchor.setAttribute('rel', 'ar'); anchor.appendChild(document.createElement('img')); return (usdzSrc) => { anchor.setAttribute('href', usdzSrc); anchor.click(); }; })(); export const openARViewer = (() => { const anchor = document.createElement('a'); const noArViewerSigil = '#model-viewer-no-ar-fallback'; let fallbackInvoked = false; return (gltfSrc, title) => { // If the fallback has ever been invoked this session, bounce early: if (fallbackInvoked) { return; } const location = self.location.toString(); const locationUrl = new URL(location); const modelUrl = new URL(gltfSrc); const link = encodeURIComponent(location); const scheme = modelUrl.protocol.replace(':', ''); locationUrl.hash = noArViewerSigil; title = encodeURIComponent(title); modelUrl.protocol = 'intent://'; const intent = `${modelUrl.toString()}?link=${link}&title=${title}#Intent;scheme=${scheme};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 && !fallbackInvoked) { fallbackInvoked = 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(); } }; self.addEventListener('hashchange', undoHashChange, { once: true }); anchor.setAttribute('href', intent); anchor.click(); }; })(); const deserializeQuickLookBrowsers = enumerationDeserializer(['safari', 'chrome']); const ARMode = { QUICK_LOOK: 'quick-look', AR_VIEWER: 'ar-viewer', UNSTABLE_WEBXR: 'unstable-webxr', NONE: 'none' }; const $exitFullscreenButtonContainer = Symbol('exitFullscreenButtonContainer'); const $arButtonContainer = Symbol('arButtonContainer'); const $defaultExitFullscreenButton = Symbol('defaultExitFullscreenButton'); const $enterARWithWebXR = Symbol('enterARWithWebXR'); const $canActivateAR = Symbol('canActivateAR'); const $arMode = Symbol('arMode'); const $canLaunchQuickLook = Symbol('canLaunchQuickLook'); const $quickLookBrowsers = Symbol('quickLookBrowsers'); const $arButtonContainerFallbackClickHandler = Symbol('arButtonContainerFallbackClickHandler'); const $onARButtonContainerFallbackClick = Symbol('onARButtonContainerFallbackClick'); const $arButtonContainerClickHandler = Symbol('arButtonContainerClickHandler'); const $onARButtonContainerClick = Symbol('onARButtonContainerClick'); const $exitFullscreenButtonContainerClickHandler = Symbol('exitFullscreenButtonContainerClickHandler'); const $onExitFullscreenButtonClick = Symbol('onExitFullscreenButtonClick'); const $fullscreenchangeHandler = Symbol('fullscreenHandler'); const $onFullscreenchange = Symbol('onFullscreen'); export const ARMixin = (ModelViewerElement) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; class ARModelViewerElement extends ModelViewerElement { constructor() { super(...arguments); this.ar = false; this.unstableWebxr = false; this.iosSrc = null; this.quickLookBrowsers = 'safari'; 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] = this.shadowRoot.querySelector('.slot.exit-fullscreen-button'); this[_d] = this.shadowRoot.querySelector('#default-exit-fullscreen-button'); // NOTE(cdata): We use a second, separate "fallback" click handler in // order to work around a regression in how Chrome on Android behaves // when requesting fullscreen at the same time as triggering an intent. // As of m76, intents could no longer be triggered successfully if they // were dispatched in the same handler as the fullscreen request. The // workaround is to split both effects into their own event handlers. // @see https://github.com/GoogleWebComponents/model-viewer/issues/693 this[_e] = (event) => this[$onARButtonContainerFallbackClick](event); this[_f] = (event) => this[$onARButtonContainerClick](event); this[_g] = () => this[$onExitFullscreenButtonClick](); this[_h] = () => this[$onFullscreenchange](); this[_j] = ARMode.NONE; this[_k] = new Set(); } get canActivateAR() { return this[$arMode] !== ARMode.NONE; } /** * 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: openIOSARQuickLook(this.iosSrc); break; case ARMode.UNSTABLE_WEBXR: await this[$enterARWithWebXR](); break; case ARMode.AR_VIEWER: openARViewer(this.src, this.alt || ''); break; default: console.warn('No AR Mode can be activated. This is probably due to missing \ configuration or device capabilities'); break; } } connectedCallback() { super.connectedCallback(); document.addEventListener('fullscreenchange', this[$fullscreenchangeHandler]); } disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener('fullscreenchange', this[$fullscreenchangeHandler]); } [(_a = $canActivateAR, _b = $arButtonContainer, _c = $exitFullscreenButtonContainer, _d = $defaultExitFullscreenButton, _e = $arButtonContainerFallbackClickHandler, _f = $arButtonContainerClickHandler, _g = $exitFullscreenButtonContainerClickHandler, _h = $fullscreenchangeHandler, _j = $arMode, _k = $quickLookBrowsers, $onExitFullscreenButtonClick)]() { if (document.fullscreenElement === this) { document.exitFullscreen(); } } [$onFullscreenchange]() { const renderer = this[$renderer]; const scene = this[$scene]; const isFullscreen = document.fullscreenElement === this; if (isFullscreen) { this[$container].classList.add('fullscreen'); } else { this[$container].classList.remove('fullscreen'); } if (document.fullscreenElement !== this && renderer.presentedScene === scene) { try { renderer.stopPresenting(); } catch (error) { console.warn('Unexpected error while stopping AR presentation'); console.error(error); } } } async [$enterARWithWebXR]() { const renderer = this[$renderer]; console.log('Attempting to enter fullscreen and present in AR...'); try { const enterFullscreen = this.requestFullscreen(); try { const outputElement = await renderer.present(this[$scene]); this.shadowRoot.appendChild(outputElement); await enterFullscreen; } catch (error) { console.warn('Error while trying to present to AR'); console.error(error); await enterFullscreen; if (document.fullscreenElement === this) { console.warn('Exiting fullscreen under dire circumstances'); document.exitFullscreen(); } } } catch (error) { console.error(error); console.warn('AR will not activate without fullscreen permission'); } } async update(changedProperties) { super.update(changedProperties); if (changedProperties.has('quickLookBrowsers')) { this[$quickLookBrowsers] = deserializeQuickLookBrowsers(this.quickLookBrowsers); } if (!changedProperties.has('unstableWebxr') && !changedProperties.has('iosSrc') && !changedProperties.has('ar') && !changedProperties.has('src') && !changedProperties.has('alt')) { return; } const renderer = this[$renderer]; const unstableWebxrCandidate = this.unstableWebxr && IS_WEBXR_AR_CANDIDATE && await renderer.supportsPresentation(); const arViewerCandidate = IS_ANDROID && this.ar; const iosQuickLookCandidate = IS_IOS && IS_AR_QUICKLOOK_CANDIDATE && this[$canLaunchQuickLook] && !!this.iosSrc; const showArButton = unstableWebxrCandidate || arViewerCandidate || iosQuickLookCandidate; if (unstableWebxrCandidate) { this[$arMode] = ARMode.UNSTABLE_WEBXR; } else if (arViewerCandidate) { this[$arMode] = ARMode.AR_VIEWER; } else if (iosQuickLookCandidate) { this[$arMode] = ARMode.QUICK_LOOK; } else { this[$arMode] = ARMode.NONE; } if (showArButton) { this[$arButtonContainer].classList.add('enabled'); // NOTE(cdata): The order of the two click handlers on the "ar // button container" is important, vital to the workaround described // earlier in this file. Reversing their order will cause our Scene // Viewer integration to break. // @see https://github.com/GoogleWebComponents/model-viewer/issues/693 this[$arButtonContainer].addEventListener('click', this[$arButtonContainerClickHandler]); this[$arButtonContainer].addEventListener('click', this[$arButtonContainerFallbackClickHandler]); this[$exitFullscreenButtonContainer].addEventListener('click', this[$exitFullscreenButtonContainerClickHandler]); } else { this[$arButtonContainer].removeEventListener('click', this[$arButtonContainerClickHandler]); this[$arButtonContainer].removeEventListener('click', this[$arButtonContainerFallbackClickHandler]); this[$exitFullscreenButtonContainer].removeEventListener('click', this[$exitFullscreenButtonContainerClickHandler]); this[$arButtonContainer].classList.remove('enabled'); } } [$onARButtonContainerFallbackClick](_event) { if (this[$arMode] === ARMode.AR_VIEWER) { this.requestFullscreen(); } } [$onARButtonContainerClick](event) { event.preventDefault(); this.activateAR(); } get [$canLaunchQuickLook]() { if (IS_IOS_CHROME) { return this[$quickLookBrowsers].has('chrome'); } else if (IS_IOS_SAFARI) { return this[$quickLookBrowsers].has('safari'); } return false; } } __decorate([ property({ type: Boolean, attribute: 'ar' }) ], ARModelViewerElement.prototype, "ar", void 0); __decorate([ property({ type: Boolean, attribute: 'unstable-webxr' }) ], ARModelViewerElement.prototype, "unstableWebxr", void 0); __decorate([ property({ converter: { fromAttribute: deserializeUrl }, attribute: 'ios-src' }) ], ARModelViewerElement.prototype, "iosSrc", void 0); __decorate([ property({ type: String, attribute: 'quick-look-browsers' }) ], ARModelViewerElement.prototype, "quickLookBrowsers", void 0); return ARModelViewerElement; }; //# sourceMappingURL=ar.js.map