From 51ce3dcd77c5df09d1a4a117db93475981b1e1e3 Mon Sep 17 00:00:00 2001 From: Felix Zhang Date: Mon, 27 Jan 2025 21:51:03 -0800 Subject: [PATCH] offersession support Summary: - Add support for navigator.xr.offerSession - Optimize how devui and sem plugs into IWER - bump version to v2.0.0 because of breaking change to API Reviewed By: cabanier Differential Revision: D68745942 Privacy Context Container: L1233623 fbshipit-source-id: cefdb6738b58b4217200db7e8d6e00f1525e4e27 --- .gitignore | 2 +- package-lock.json | 4 +- package.json | 2 +- src/device/XRController.ts | 6 ++ src/device/XRDevice.ts | 85 ++++++++++++++++--- src/initialization/XRSystem.ts | 147 ++++++++++++++++++++++----------- src/session/XRSession.ts | 34 ++++++-- 7 files changed, 208 insertions(+), 72 deletions(-) diff --git a/.gitignore b/.gitignore index 777478d..a9bd289 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ node_modules/ docs/.vitepress/dist/ docs/.vitepress/cache/ -src/version.ts \ No newline at end of file +**/src/version.ts \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 26f65bb..7e2ed4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "iwer", - "version": "1.1.1", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "iwer", - "version": "1.1.1", + "version": "2.0.0", "license": "MIT", "dependencies": { "gl-matrix": "^3.4.3" diff --git a/package.json b/package.json index dd79e53..71bddeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iwer", - "version": "1.1.1", + "version": "2.0.0", "description": "Javascript WebXR Runtime for Emulation", "type": "module", "main": "lib/index.js", diff --git a/src/device/XRController.ts b/src/device/XRController.ts index 76fb09d..84df5a9 100644 --- a/src/device/XRController.ts +++ b/src/device/XRController.ts @@ -31,6 +31,7 @@ export interface XRControllerConfig { export class XRController extends XRTrackedInput { [P_CONTROLLER]: { + profileId: string; gamepadConfig: GamepadConfig; }; @@ -64,6 +65,7 @@ export class XRController extends XRTrackedInput { super(inputSource); this[P_CONTROLLER] = { + profileId: controllerConfig.profileId, gamepadConfig: controllerConfig.layout[handedness]!.gamepad, }; } @@ -72,6 +74,10 @@ export class XRController extends XRTrackedInput { return this[P_CONTROLLER].gamepadConfig; } + get profileId() { + return this[P_CONTROLLER].profileId; + } + updateButtonValue(id: string, value: number) { if (value > 1 || value < 0) { console.warn(`Out-of-range value ${value} provided for button ${id}.`); diff --git a/src/device/XRDevice.ts b/src/device/XRDevice.ts index d7ddc66..22330e5 100644 --- a/src/device/XRDevice.ts +++ b/src/device/XRDevice.ts @@ -103,8 +103,22 @@ const DEFAULTS = { stereoEnabled: false, }; +export interface DevUIConstructor { + new (xrDevice: XRDevice): DevUI; +} +export interface DevUI { + version: string; + render(time: number): void; + get devUICanvas(): HTMLCanvasElement; + get devUIContainer(): HTMLDivElement; +} + +export interface SEMConstructor { + new (xrDevice: XRDevice): SyntheticEnvironmentModule; +} export interface SyntheticEnvironmentModule { - render(xrDevice: XRDevice): void; + version: string; + render(time: number): void; loadEnvironment(json: any): void; planesVisible: boolean; boundingBoxesVisible: boolean; @@ -115,11 +129,18 @@ export interface SyntheticEnvironmentModule { computeHitTestResults(rayMatrix: mat4): mat4[]; } +const Z_INDEX_SEM_CANVAS = 1; +const Z_INDEX_APP_CANVAS = 2; +const Z_INDEX_DEVUI_CANVAS = 3; +const Z_INDEX_DEVUI_CONTAINER = 4; + /** * XRDevice is not a standard API class outlined in the WebXR Device API Specifications * Instead, it serves as an user-facing interface to control the emulated XR Device */ export class XRDevice { + public readonly version = VERSION; + [P_DEVICE]: { // device config name: string; @@ -158,6 +179,7 @@ export class XRDevice { parent: HTMLElement | null; width: number; height: number; + zIndex: string; }; canvasContainer: HTMLDivElement; @@ -170,8 +192,9 @@ export class XRDevice { // action playback actionPlayer?: ActionPlayer; - // synthetic environment - syntheticEnvironmentModule?: SyntheticEnvironmentModule; + // add-on modules: + devui?: DevUI; + sem?: SyntheticEnvironmentModule; }; constructor( @@ -306,8 +329,17 @@ export class XRDevice { // backup canvas data const canvas = baseLayer.context.canvas as HTMLCanvasElement; if (canvas.parentElement !== this[P_DEVICE].canvasContainer) { - const sem = this[P_DEVICE].syntheticEnvironmentModule; + const devui = this[P_DEVICE].devui; + if (devui) { + const { devUICanvas, devUIContainer } = devui; + devUICanvas.style.zIndex = Z_INDEX_DEVUI_CANVAS.toString(); + devUIContainer.style.zIndex = Z_INDEX_DEVUI_CONTAINER.toString(); + this[P_DEVICE].canvasContainer.appendChild(devui.devUICanvas); + this[P_DEVICE].canvasContainer.appendChild(devui.devUIContainer); + } + const sem = this[P_DEVICE].sem; if (sem) { + sem.environmentCanvas.style.zIndex = Z_INDEX_SEM_CANVAS.toString(); this[P_DEVICE].canvasContainer.appendChild(sem.environmentCanvas); } this[P_DEVICE].canvasData = { @@ -315,7 +347,9 @@ export class XRDevice { parent: canvas.parentElement, width: canvas.width, height: canvas.height, + zIndex: canvas.style.zIndex, }; + canvas.style.zIndex = Z_INDEX_APP_CANVAS.toString(); this[P_DEVICE].canvasContainer.appendChild(canvas); document.body.appendChild(this[P_DEVICE].canvasContainer); } @@ -325,15 +359,22 @@ export class XRDevice { }, onSessionEnd: () => { if (this[P_DEVICE].canvasData) { - const { canvas, parent, width, height } = this[P_DEVICE].canvasData; + const { canvas, parent, width, height, zIndex } = + this[P_DEVICE].canvasData; canvas.width = width; canvas.height = height; + canvas.style.zIndex = zIndex; if (parent) { parent.appendChild(canvas); } else { this[P_DEVICE].canvasContainer.removeChild(canvas); } - const sem = this[P_DEVICE].syntheticEnvironmentModule; + const devui = this[P_DEVICE].devui; + if (devui) { + this[P_DEVICE].canvasContainer.removeChild(devui.devUICanvas); + this[P_DEVICE].canvasContainer.removeChild(devui.devUIContainer); + } + const sem = this[P_DEVICE].sem; if (sem) { this[P_DEVICE].canvasContainer.removeChild(sem.environmentCanvas); } @@ -436,8 +477,12 @@ export class XRDevice { globalObject['XRReferenceSpaceEvent'] = XRReferenceSpaceEvent; } - installSyntheticEnvironmentModule(sem: SyntheticEnvironmentModule) { - this[P_DEVICE].syntheticEnvironmentModule = sem; + installDevUI(devUIConstructor: DevUIConstructor) { + this[P_DEVICE].devui = new devUIConstructor(this); + } + + installSEM(semConstructor: SEMConstructor) { + this[P_DEVICE].sem = new semConstructor(this); } get supportedSessionModes() { @@ -563,6 +608,22 @@ export class XRDevice { return this[P_DEVICE].xrSystem?.[P_SYSTEM].activeSession; } + get sessionOffered(): boolean { + return Boolean(this[P_DEVICE].xrSystem?.[P_SYSTEM].offeredSessionConfig); + } + + get name() { + return this[P_DEVICE].name; + } + + grantOfferedSession(): void { + const pSystem = this[P_DEVICE].xrSystem?.[P_SYSTEM]; + if (pSystem && pSystem.offeredSessionConfig) { + pSystem.grantSession(pSystem.offeredSessionConfig); + pSystem.offeredSessionConfig = undefined; + } + } + recenter() { const deltaVec = new Vector3(-this.position.x, 0, -this.position.z); const forward = new Vector3(0, 0, -1).applyQuaternion(this.quaternion); @@ -625,7 +686,11 @@ export class XRDevice { return this[P_DEVICE].actionPlayer; } - get syntheticEnvironmentModule() { - return this[P_DEVICE].syntheticEnvironmentModule; + get devui() { + return this[P_DEVICE].devui; + } + + get sem() { + return this[P_DEVICE].sem; } } diff --git a/src/initialization/XRSystem.ts b/src/initialization/XRSystem.ts index 6f05b95..9b0ba3a 100644 --- a/src/initialization/XRSystem.ts +++ b/src/initialization/XRSystem.ts @@ -14,15 +14,85 @@ import { import { P_SYSTEM } from '../private.js'; +type SessionGrantConfig = { + resolve: (value: XRSession) => void; + reject: (reason?: any) => void; + mode: XRSessionMode; + options: XRSessionInit; +}; + export class XRSystem extends EventTarget { [P_SYSTEM]: { device: XRDevice; activeSession?: XRSession; + grantSession: (SessionGrantConfig: SessionGrantConfig) => void; + offeredSessionConfig?: SessionGrantConfig; }; constructor(device: XRDevice) { super(); - this[P_SYSTEM] = { device }; + this[P_SYSTEM] = { + device, + grantSession: ({ resolve, reject, mode, options }) => { + // Check for active sessions and other constraints here + if (this[P_SYSTEM].activeSession) { + reject( + new DOMException( + 'An active XRSession already exists.', + 'InvalidStateError', + ), + ); + return; + } + + // Handle required and optional features + const { requiredFeatures = [], optionalFeatures = [] } = options; + const { supportedFeatures } = this[P_SYSTEM].device; + + // Check if all required features are supported + const allRequiredSupported = requiredFeatures.every((feature) => + supportedFeatures.includes(feature), + ); + if (!allRequiredSupported) { + reject( + new Error( + 'One or more required features are not supported by the device.', + ), + ); + return; + } + + // Filter out unsupported optional features + const supportedOptionalFeatures = optionalFeatures.filter((feature) => + supportedFeatures.includes(feature), + ); + + // Combine required and supported optional features into enabled features + const enabledFeatures: WebXRFeature[] = Array.from( + new Set([ + ...requiredFeatures, + ...supportedOptionalFeatures, + 'viewer', + 'local', + ]), + ); + + // Proceed with session creation + const session = new XRSession( + this[P_SYSTEM].device, + mode, + enabledFeatures, + ); + this[P_SYSTEM].activeSession = session; + + // Listen for session end to clear the active session + session.addEventListener('end', () => { + this[P_SYSTEM].activeSession = undefined; + }); + + resolve(session); + }, + }; // Initialize device change monitoring here if applicable } @@ -54,63 +124,42 @@ export class XRSystem extends EventTarget { return; } - // Check for active sessions and other constraints here - if (this[P_SYSTEM].activeSession) { - reject( - new DOMException( - 'An active XRSession already exists.', - 'InvalidStateError', - ), - ); - return; - } + const sessionGrantConfig = { + resolve, + reject, + mode, + options, + }; - // Handle required and optional features - const { requiredFeatures = [], optionalFeatures = [] } = options; - const { supportedFeatures } = this[P_SYSTEM].device; + this[P_SYSTEM].grantSession(sessionGrantConfig); + }) + .catch(reject); + }); + } - // Check if all required features are supported - const allRequiredSupported = requiredFeatures.every((feature) => - supportedFeatures.includes(feature), - ); - if (!allRequiredSupported) { + offerSession( + mode: XRSessionMode, + options: XRSessionInit = {}, + ): Promise { + return new Promise((resolve, reject) => { + this.isSessionSupported(mode) + .then((isSupported) => { + if (!isSupported) { reject( - new Error( - 'One or more required features are not supported by the device.', + new DOMException( + 'The requested XRSession mode is not supported.', + 'NotSupportedError', ), ); return; } - // Filter out unsupported optional features - const supportedOptionalFeatures = optionalFeatures.filter((feature) => - supportedFeatures.includes(feature), - ); - - // Combine required and supported optional features into enabled features - const enabledFeatures: WebXRFeature[] = Array.from( - new Set([ - ...requiredFeatures, - ...supportedOptionalFeatures, - 'viewer', - 'local', - ]), - ); - - // Proceed with session creation - const session = new XRSession( - this[P_SYSTEM].device, + this[P_SYSTEM].offeredSessionConfig = { + resolve, + reject, mode, - enabledFeatures, - ); - this[P_SYSTEM].activeSession = session; - - // Listen for session end to clear the active session - session.addEventListener('end', () => { - this[P_SYSTEM].activeSession = undefined; - }); - - resolve(session); + options, + }; }) .catch(reject); }); diff --git a/src/session/XRSession.ts b/src/session/XRSession.ts index d5e0d28..e1118a0 100644 --- a/src/session/XRSession.ts +++ b/src/session/XRSession.ts @@ -298,11 +298,16 @@ export class XRSession extends EventTarget { performance.now(), ); + const time = performance.now(); + const devui = this[P_SESSION].device[P_DEVICE].devui; + if (devui) { + devui.render(time); + } + if (this[P_SESSION].mode === 'immersive-ar') { - const sem = - this[P_SESSION].device[P_DEVICE].syntheticEnvironmentModule; + const sem = this[P_SESSION].device[P_DEVICE].sem; if (sem) { - sem.render(this[P_SESSION].device); + sem.render(time); } } @@ -408,7 +413,7 @@ export class XRSession extends EventTarget { }, trackedPlanes: new Map(), updateTrackedPlanes: (frame: XRFrame) => { - const sem = this[P_SESSION].device[P_DEVICE].syntheticEnvironmentModule; + const sem = this[P_SESSION].device[P_DEVICE].sem; if (!sem) { return; } @@ -425,7 +430,12 @@ export class XRSession extends EventTarget { this[P_SESSION].device[P_DEVICE].globalSpace, plane.transform.matrix, ); - xrPlane = new XRPlane(plane, planeSpace, plane.polygon); + xrPlane = new XRPlane( + plane, + planeSpace, + plane.polygon, + plane.semanticLabel, + ); this[P_SESSION].trackedPlanes.set(plane, xrPlane); } xrPlane[P_PLANE].lastChangedTime = frame.predictedDisplayTime; @@ -435,7 +445,7 @@ export class XRSession extends EventTarget { }, trackedMeshes: new Map(), updateTrackedMeshes: (frame: XRFrame) => { - const sem = this[P_SESSION].device[P_DEVICE].syntheticEnvironmentModule; + const sem = this[P_SESSION].device[P_DEVICE].sem; if (!sem) { return; } @@ -452,7 +462,13 @@ export class XRSession extends EventTarget { this[P_SESSION].device[P_DEVICE].globalSpace, mesh.transform.matrix, ); - xrMesh = new XRMesh(mesh, meshSpace, mesh.vertices, mesh.indices); + xrMesh = new XRMesh( + mesh, + meshSpace, + mesh.vertices, + mesh.indices, + mesh.semanticLabel, + ); this[P_SESSION].trackedMeshes.set(mesh, xrMesh); } xrMesh[P_MESH].lastChangedTime = frame.predictedDisplayTime; @@ -462,7 +478,7 @@ export class XRSession extends EventTarget { }, hitTestSources: new Set(), computeHitTestResults: (frame) => { - const sem = this[P_SESSION].device[P_DEVICE].syntheticEnvironmentModule; + const sem = this[P_SESSION].device[P_DEVICE].sem; if (!sem) return; const globalSpace = this[P_SESSION].device[P_DEVICE].globalSpace; this[P_SESSION].hitTestSources.forEach((hitTestSource) => { @@ -786,7 +802,7 @@ export class XRSession extends EventTarget { reject( new DOMException('XRSession has already ended.', 'InvalidStateError'), ); - } else if (!this[P_SESSION].device[P_DEVICE].syntheticEnvironmentModule) { + } else if (!this[P_SESSION].device[P_DEVICE].sem) { reject( new DOMException( 'Synthethic Environment Module required for emulating hit-test',