import {PermissionsManager} from './PermissionsManager'; import { WebXRSessionEventType, WebXRSessionManager, } from './WebXRSessionManager'; const XRBUTTON_WRAPPER_ID = 'XRButtonWrapper'; const XRBUTTON_CLASS = 'XRButton'; export class XRButton { public domElement = document.createElement('div'); public simulatorButtonElement = document.createElement('button'); public xrButtonElement = document.createElement('button'); private errorElement = document.createElement('p'); private disposed = false; private startingSimulator = false; constructor( private sessionManager: WebXRSessionManager, private permissionsManager: PermissionsManager, private appTitle = '', private appDescription = '', private startText = 'ENTER XR', private endText = 'END XR', private invalidText = 'XR NOT SUPPORTED', private startSimulatorText = 'START SIMULATOR', showEnterSimulatorButton = false, public startSimulator: () => void | Promise = () => {}, private permissions = { geolocation: false, camera: false, microphone: false, } ) { this.domElement.id = XRBUTTON_WRAPPER_ID; this.createXRAppTitle(); this.createXRAppDescription(); this.createXRButtonElement(); if (showEnterSimulatorButton) { this.createSimulatorButton(); } this.createErrorElement(); this.sessionManager.addEventListener( WebXRSessionEventType.UNSUPPORTED, this.onUnsupported ); this.sessionManager.addEventListener( WebXRSessionEventType.READY, this.onReady ); this.sessionManager.addEventListener( WebXRSessionEventType.SESSION_START, this.onSessionStart ); this.sessionManager.addEventListener( WebXRSessionEventType.SESSION_END, this.onSessionEnd ); this.sessionManager.addEventListener( WebXRSessionEventType.SESSION_ERROR, this.onSessionError ); } private onUnsupported = () => this.showXRNotSupported(); private onReady = () => this.onSessionReady(); private onSessionStart = () => this.onSessionStarted(); private onSessionEnd = () => this.onSessionEnded(); private onSessionError = (event: {error: unknown}) => this.showError(event.error); private createErrorElement() { this.errorElement.className = 'XRButtonError'; this.errorElement.setAttribute('role', 'alert'); this.errorElement.style.maxWidth = 'min(90vw, 40rem)'; this.errorElement.hidden = true; this.domElement.appendChild(this.errorElement); } private showError(error: unknown, mode: 'XR' | 'Simulator' = 'XR') { if (this.disposed) return; const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error); this.errorElement.textContent = `${mode} could not start. ${detail}`; this.errorElement.hidden = false; if (mode === 'Simulator') return; this.xrButtonElement.textContent = this.sessionManager.currentSession ? this.endText : this.startText; this.xrButtonElement.disabled = this.startingSimulator; this.simulatorButtonElement.disabled = this.startingSimulator || !!this.sessionManager.currentSession; } private createSimulatorButton() { this.simulatorButtonElement.classList.add(XRBUTTON_CLASS); this.simulatorButtonElement.innerText = this.startSimulatorText; this.simulatorButtonElement.onclick = async () => { if (this.disposed || this.simulatorButtonElement.disabled) return; this.setSimulatorStarting(true); this.errorElement.textContent = ''; this.errorElement.hidden = true; try { await this.startSimulator(); if (!this.disposed) this.domElement.remove(); } catch (error) { if (this.disposed) return; this.setSimulatorStarting(false); this.showError(error, 'Simulator'); } }; this.domElement.appendChild(this.simulatorButtonElement); } private createXRAppTitle() { if (!this.appTitle) { return; } const appTitle = document.createElement('h1'); appTitle.textContent = this.appTitle; this.domElement.appendChild(appTitle); } private createXRAppDescription() { if (!this.appDescription) { return; } const appDescription = document.createElement('h4'); appDescription.textContent = this.appDescription; this.domElement.appendChild(appDescription); } private createXRButtonElement() { this.xrButtonElement.classList.add(XRBUTTON_CLASS); this.xrButtonElement.disabled = true; this.xrButtonElement.textContent = '...'; this.domElement.appendChild(this.xrButtonElement); } private onSessionReady() { this.errorElement.textContent = ''; this.errorElement.hidden = true; const button = this.xrButtonElement; button.style.display = ''; button.innerHTML = this.startText; button.disabled = this.startingSimulator; this.simulatorButtonElement.disabled = this.startingSimulator; const allowsVideoFallback = this.sessionManager .getSessionOptions() ?.optionalFeatures?.includes('camera-access'); button.onclick = () => { if (this.disposed || button.disabled) return; this.errorElement.textContent = ''; this.errorElement.hidden = true; button.textContent = 'ENTERING XR...'; button.disabled = true; this.simulatorButtonElement.disabled = true; this.permissionsManager .checkAndRequestPermissions(this.permissions, { allowVideoFallback: allowsVideoFallback, }) .then((result) => { if (this.disposed) return; if (result.granted) { this.sessionManager.startSession(); } else { this.showError( new Error(result.error || 'Browser permission was not granted.') ); } }) .catch((error) => this.showError(error)); }; } private showXRNotSupported() { this.xrButtonElement.textContent = this.invalidText; this.xrButtonElement.disabled = true; } private async onSessionStarted() { this.errorElement.textContent = ''; this.errorElement.hidden = true; this.xrButtonElement.innerHTML = this.endText; this.xrButtonElement.disabled = this.startingSimulator; this.simulatorButtonElement.disabled = true; this.xrButtonElement.onclick = () => { void this.sessionManager.endSession(); }; } private onSessionEnded() { this.onSessionReady(); } setSimulatorStarting(starting: boolean) { if (this.disposed) return; this.startingSimulator = starting; this.simulatorButtonElement.disabled = starting || !!this.sessionManager.currentSession; this.xrButtonElement.disabled = starting || this.sessionManager.isXRSupported() !== true; } dispose() { if (this.disposed) return; this.disposed = true; this.sessionManager.removeEventListener( WebXRSessionEventType.UNSUPPORTED, this.onUnsupported ); this.sessionManager.removeEventListener( WebXRSessionEventType.READY, this.onReady ); this.sessionManager.removeEventListener( WebXRSessionEventType.SESSION_START, this.onSessionStart ); this.sessionManager.removeEventListener( WebXRSessionEventType.SESSION_END, this.onSessionEnd ); this.sessionManager.removeEventListener( WebXRSessionEventType.SESSION_ERROR, this.onSessionError ); this.simulatorButtonElement.onclick = null; this.xrButtonElement.onclick = null; this.domElement.remove(); } }