@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.
231 lines • 10.8 kB
JavaScript
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 {
static _instance;
static create() {
return new WebXRButtonFactory();
}
static getOrCreate() {
if (!this._instance) {
this._instance = this.create();
}
return this._instance;
}
get isSecureConnection() { return window.location.protocol === "https:"; }
get quicklookButton() { return this._quicklookButton; }
_quicklookButton;
get arButton() { return this._arButton; }
_arButton;
get vrButton() { return this._vrButton; }
_vrButton;
get sendToQuestButton() { return this._sendToQuestButton; }
_sendToQuestButton;
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() {
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 = 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) {
if (this._arButton)
return this._arButton;
const mode = "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) {
if (this._vrButton)
return this._vrButton;
const mode = "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() {
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() {
return ButtonsFactory.getOrCreate().createQRCode();
}
updateSessionSupported(button, mode) {
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);
});
}
hideElementDuringXRSession(element) {
onXRSessionStart(_ => {
element["previous-display"] = element.style.display;
element.style.display = "none";
});
onXRSessionEnd(_ => {
if (element["previous-display"] != undefined)
element.style.display = element["previous-display"];
});
}
listenToXRSessionState(button, mode) {
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"];
});
}
}
}
//# sourceMappingURL=WebXRButtons.js.map