UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

261 lines (227 loc) 11.3 kB
import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.js"; import { isDevEnvironment, showBalloonMessage } from "../debug/index.js"; import { findObjectOfType } from "../engine_components.js"; import { Context } from "../engine_setup.js"; import { DeviceUtilities } from "../engine_utils.js"; import { NeedleXRSession } from "../engine_xr.js"; import { onXRSessionEnd, onXRSessionStart } from "../xr/events.js"; import { ButtonsFactory } from "./buttons.js"; import { getIconElement } from "./icons.js"; // TODO: move these buttons into their own web components so their logic is encapsulated (e.g. the CSS animation when a xr session is requested) /** * Factory to create WebXR buttons for AR, VR, Quicklook and Send to Quest * The buttons are created as HTMLButtonElements and can be added to the DOM. * The buttons will automatically hide when a XR session is started and show again when the session ends. */ export class WebXRButtonFactory { private static _instance: WebXRButtonFactory; private static create() { return new WebXRButtonFactory(); } static getOrCreate() { if (!this._instance) { this._instance = this.create(); } return this._instance; } private get isSecureConnection() { return window.location.protocol === "https:"; } get quicklookButton() { return this._quicklookButton } private _quicklookButton?: HTMLButtonElement; get arButton() { return this._arButton; } private _arButton?: HTMLButtonElement; get vrButton() { return this._vrButton } private _vrButton?: HTMLButtonElement; get sendToQuestButton() { return this._sendToQuestButton; } private _sendToQuestButton?: HTMLButtonElement; get qrButton() { return ButtonsFactory.getOrCreate().createQRCode(); } /** get or create the quicklook button * Behaviour of the button: * - if the button is clicked a USDZExporter component will be searched for in the scene and if found, it will be used to export the scene to USDZ / Quicklook */ createQuicklookButton(): HTMLButtonElement { if (this._quicklookButton) return this._quicklookButton; const button = document.createElement("button"); this._quicklookButton = button; button.dataset["needle"] = "quicklook-button"; const supportsQuickLook = DeviceUtilities.supportsQuickLookAR(); // we can immediately enter this scene, because the platform supports rel="ar" links if (supportsQuickLook) { button.innerText = "View in AR"; } else { button.innerText = "View in AR"; } button.prepend(getIconElement("view_in_ar")); let createdExporter = false; let usdzExporter: USDZExporter | null = null; button.addEventListener("click", () => { usdzExporter = findObjectOfType(USDZExporter); // if the scene doesnt have an USDZExporter component, create one if (!usdzExporter) { createdExporter = true; usdzExporter = new USDZExporter(); } // if we have created a USDZExporter if (createdExporter) usdzExporter.objectToExport = Context.Current.scene; if (usdzExporter) { button.classList.add("this-mode-is-requested"); usdzExporter.exportAndOpen().then(() => { button.classList.remove("this-mode-is-requested"); }).catch(err => { button.classList.remove("this-mode-is-requested"); console.error(err); }); } else { console.warn("No USDZExporter component found in the scene"); } }); this.hideElementDuringXRSession(button); return button; } /** get or create the WebXR AR button * @param init optional session init options * Behaviour of the button: * - if the device supports AR, the button will be visible and clickable * - if the device does not support AR, the button will be hidden * - if the device changes and now supports AR, the button will be visible */ createARButton(init?: XRSessionInit): HTMLButtonElement { if (this._arButton) return this._arButton; const mode: XRSessionMode = "immersive-ar"; const button = document.createElement("button"); this._arButton = button; button.classList.add("webxr-button"); button.dataset["needle"] = "webxr-ar-button"; button.innerText = "Enter AR"; button.prepend(getIconElement("view_in_ar")) button.title = "Click to start an AR session"; button.addEventListener("click", () => NeedleXRSession.start(mode, init)); this.updateSessionSupported(button, mode); this.listenToXRSessionState(button, mode); this.hideElementDuringXRSession(button); if (!this.isSecureConnection) { button.disabled = true; button.title = "WebXR requires a secure connection (HTTPS)"; } if (!DeviceUtilities.isMozillaXR()) // WebXR Viewer can't attach events before session start navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode)); return button; } /** get or create the WebXR VR button * @param init optional session init options * Behaviour of the button: * - if the device supports VR, the button will be visible and clickable * - if the device does not support VR, the button will be hidden * - if the device changes and now supports VR, the button will be visible */ createVRButton(init?: XRSessionInit): HTMLButtonElement { if (this._vrButton) return this._vrButton; const mode: XRSessionMode = "immersive-vr"; const button = document.createElement("button"); this._vrButton = button; button.classList.add("webxr-button"); button.dataset["needle"] = "webxr-vr-button"; button.innerText = "Enter VR"; button.prepend(getIconElement("panorama_photosphere")); button.title = "Click to start a VR session"; button.addEventListener("click", () => NeedleXRSession.start(mode, init)); this.updateSessionSupported(button, mode); this.listenToXRSessionState(button, mode); this.hideElementDuringXRSession(button); if (!this.isSecureConnection) { button.disabled = true; button.title = "WebXR requires a secure connection (HTTPS)"; } if (!DeviceUtilities.isMozillaXR()) // WebXR Viewer can't attach events before session start navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode)); return button; } /** get or create the Send To Quest button * Behaviour of the button: * - if the button is clicked, the current URL will be sent to the Oculus Browser on the Quest */ createSendToQuestButton(): HTMLButtonElement { if (this._sendToQuestButton) return this._sendToQuestButton; const baseUrl = `https://oculus.com/open_url/?url=` const button = document.createElement("button"); this._sendToQuestButton = button; button.dataset["needle"] = "webxr-sendtoquest-button"; button.innerText = "Open on Quest"; button.prepend(getIconElement("share_windows")); button.title = "Click to send this page to the Oculus Browser on your Quest"; button.addEventListener("click", () => { const urlParameter = encodeURIComponent(window.location.href); const url = baseUrl + urlParameter; if (window.open(url) == null) { showBalloonMessage("This page doesn't allow popups. Please paste " + url + " into your browser."); } }); this.listenToXRSessionState(button); this.hideElementDuringXRSession(button); // make sure to hide the button when we have VR support directly on the device if (!DeviceUtilities.isMozillaXR()) { // WebXR Viewer can't attach events before session start navigator.xr?.addEventListener("devicechange", () => { if (navigator.xr?.isSessionSupported("immersive-vr")) { button.style.display = "none"; } else { button.style.display = ""; } }); } return button; } /** * @deprecated please use ButtonsFactory.getOrCreate().createQRCode(). This method will be removed in a future update */ createQRCode(): HTMLButtonElement { return ButtonsFactory.getOrCreate().createQRCode(); } private updateSessionSupported(button: HTMLButtonElement, mode: XRSessionMode) { if (!("xr" in navigator)) { button.style.display = "none"; return; } NeedleXRSession.isSessionSupported(mode).then(supported => { button.style.display = !supported ? "none" : ""; if (isDevEnvironment() && !supported) console.log("[WebXR] \"" + mode + "\" is not supported on this device – make sure your server runs using HTTPS and you have a device connected that supports " + mode); }); } private hideElementDuringXRSession(element: HTMLElement) { onXRSessionStart(_ => { element["previous-display"] = element.style.display; element.style.display = "none"; }); onXRSessionEnd(_ => { if (element["previous-display"] != undefined) element.style.display = element["previous-display"]; }); } private listenToXRSessionState(button: HTMLButtonElement, mode?: XRSessionMode) { if (mode) { NeedleXRSession.onSessionRequestStart(args => { if (args.mode === mode) { button.classList.add("this-mode-is-requested"); // button["original-text"] = button.innerText; // let modeText = mode === "immersive-vr" ? "VR" : "AR"; // button.innerText = "Starting " + modeText + "..."; } else { button["was-disabled"] = button.disabled; button.disabled = true; button.classList.add("other-mode-is-requested"); } }); NeedleXRSession.onSessionRequestEnd(_ => { button.classList.remove("this-mode-is-requested"); button.classList.remove("other-mode-is-requested"); button.disabled = button["was-disabled"]; // button.innerText = button["original-text"]; }); } } } /** @deprecated please use WebXRButtonFactory. This type will be removed in a future update */ export type NeedleWebXRHtmlElement = WebXRButtonFactory;