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.

221 lines (189 loc) • 8.81 kB
import type { Context } from "../engine_context.js"; import { RoomEvents, UserJoinedOrLeftRoomModel } from "../engine_networking.js"; import { getParam } from "../engine_utils.js"; import { NeedleXRController } from "./NeedleXRController.js"; import { NeedleXRSession } from "./NeedleXRSession.js"; const debug = getParam("debugwebxr"); declare type XRControllerType = "hand" | "controller"; declare type XRControllerState = { // adding a guid so it's saved on the server, ideally we have a "room lifetime" store that doesnt save state forever on disc but just until the room is disposed (we have to add support for this in the networking backend tho) guid: string; index: number; handedness: XRHandedness; isTracking: boolean; type: XRControllerType; } class XRUserState { readonly controllerStates: XRControllerState[] = []; readonly userId: string; readonly context: Context; private readonly userStateEvtName: string; constructor(userId: string, context: Context) { this.userId = userId; this.context = context; this.userStateEvtName = "xr-sync-user-state-" + userId; this.context.connection.beginListen(this.userStateEvtName, this.onReceivedControllerState); } dispose() { this.context.connection.stopListen(this.userStateEvtName, this.onReceivedControllerState); } onReceivedControllerState = (state: XRControllerState) => { if (debug) console.log(`XRSync: Received change for ${this.userId}: ${state.type} ${state.handedness}; tracked=${state.isTracking}`); let found = false; for (let i = 0; i < this.controllerStates.length; i++) { const ctrl = this.controllerStates[i]; if (ctrl.index === state.index) { this.controllerStates[i] = state; found = true; break; } } if (!found) { this.controllerStates.push(state); } } update(session: NeedleXRSession) { if (this.context.connection.isConnected == false) return; for (let i = this.controllerStates.length - 1; i >= 0; i--) { const state = this.controllerStates[i]; let foundController = false; for (let i = 0; i < session.controllers.length; i++) { const ctrl = session.controllers[i]; if (ctrl.index === state.index) { foundController = true; } } if (!foundController) { // controller was removed if (debug) console.log(`XRSync: ${state.type} ${state.handedness} removed`, state.index); this.controllerStates.splice(i, 1); this.sendControllerRemoved(state); } } for (const ctrl of session.controllers) { this.updateControllerStates(ctrl); } } onExitXR(_session: NeedleXRSession) { for (const state of this.controllerStates) { this.sendControllerRemoved(state); } this.controllerStates.length = 0; } private sendControllerRemoved(state: XRControllerState) { state.isTracking = false; state.guid = ""; this.context.connection.send(this.userStateEvtName, state); this.context.connection.sendDeleteRemoteState(state.guid); } private updateControllerStates(ctrl: NeedleXRController) { // this.context.connection.send(this.userStateEvtName, {}); const existing = this.controllerStates.find(x => x.index === ctrl.index); if (existing) { let hasChanged = false; hasChanged ||= existing.isTracking != ctrl.isTracking; if (hasChanged) { existing.isTracking = ctrl.isTracking; this.context.connection.send(this.userStateEvtName, existing); } } else { const state: XRControllerState = { guid: this.userId + "-" + ctrl.index, isTracking: ctrl.isTracking, handedness: ctrl.side, index: ctrl.index, type: ctrl.hand ? "hand" : "controller" } this.controllerStates.push(state); this.context.connection.send(this.userStateEvtName, state); if (debug) console.log(`XRSync: ${state.type} ${state.handedness} added`, state.index); } } } export class NeedleXRSync { hasState(userId: string | null | undefined) { if (!userId) return false; return this._states.has(userId); } /** Is the left controller or hand tracked */ isTracking(userId: string | null | undefined, handedness: XRHandedness): boolean | undefined { if (!userId) return undefined; const user = this._states.get(userId); if (!user) return undefined; const ctrl = user.controllerStates.find(x => x.handedness === handedness); return ctrl?.isTracking || false; } /** Is it hand tracking or a controller */ getDeviceType(userId: string, handedness: XRHandedness): XRControllerType | undefined | "unknown" { if (!userId) return undefined; const user = this._states.get(userId); if (!user) return undefined; const ctrl = user.controllerStates.find(x => x.handedness === handedness); return ctrl?.type || "unknown"; } private readonly context: Context; constructor(context: Context) { this.context = context; this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onJoinedRoom); this.context.connection.beginListen(RoomEvents.LeftRoom, this.onLeftRoom) this.context.connection.beginListen(RoomEvents.UserJoinedRoom, this.onOtherUserJoinedRoom); this.context.connection.beginListen(RoomEvents.UserLeftRoom, this.onOtherUserLeftRoom); } destroy() { this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onJoinedRoom); this.context.connection.stopListen(RoomEvents.LeftRoom, this.onLeftRoom) this.context.connection.stopListen(RoomEvents.UserJoinedRoom, this.onOtherUserJoinedRoom); this.context.connection.stopListen(RoomEvents.UserLeftRoom, this.onOtherUserLeftRoom); } private onJoinedRoom = () => { if (this.context.connection.connectionId) { if (!this._states.has(this.context.connection.connectionId)) { if (debug) console.log("XRSync: Local user joined room", this.context.connection.connectionId); this._states.set(this.context.connection.connectionId, new XRUserState(this.context.connection.connectionId, this.context)); } for (const user of this.context.connection.usersInRoom()) { if (!this._states.has(user)) { this._states.set(user, new XRUserState(user, this.context)); } } } } private onLeftRoom = () => { if (this.context.connection.connectionId) { if (!this._states.has(this.context.connection.connectionId)) { const state = this._states.get(this.context.connection.connectionId); state?.dispose(); this._states.delete(this.context.connection.connectionId); } } } private onOtherUserJoinedRoom = (evt: UserJoinedOrLeftRoomModel) => { const userId = evt.userId; if (!this._states.has(userId)) { if (debug) console.log("XRSync: Remote user joined room", userId); this._states.set(userId, new XRUserState(userId, this.context)); } } private onOtherUserLeftRoom = (evt: UserJoinedOrLeftRoomModel) => { const userId = evt.userId; if (!this._states.has(userId)) { const state = this._states.get(userId); state?.dispose(); this._states.delete(userId); } } private _states: Map<string, XRUserState> = new Map(); onUpdate(session: NeedleXRSession) { if (this.context.connection.isConnected && this.context.connection.connectionId) { const localState = this._states.get(this.context.connection.connectionId); localState?.update(session); } } onExitXR(session: NeedleXRSession) { if (this.context.connection.isConnected && this.context.connection.connectionId) { const localState = this._states.get(this.context.connection.connectionId); localState?.onExitXR(session); } } }