playcanvas
Version:
PlayCanvas WebGL game engine
281 lines (278 loc) • 10.2 kB
JavaScript
import { EventHandler } from '../../core/event-handler.js';
import { platform } from '../../core/platform.js';
import { XrInputSource } from './xr-input-source.js';
/**
* @import { XrManager } from './xr-manager.js'
*/ /**
* Provides access to input sources for WebXR.
*
* Input sources represent:
*
* - hand held controllers - and their optional capabilities: gamepad and vibration
* - hands - with their individual joints
* - transient sources - such as touch screen taps and voice commands
*
* @category XR
*/ class XrInput extends EventHandler {
static{
/**
* Fired when a new {@link XrInputSource} is added to the list. The handler is passed the
* {@link XrInputSource} that has been added.
*
* @event
* @example
* app.xr.input.on('add', (inputSource) => {
* // new input source is added
* });
*/ this.EVENT_ADD = 'add';
}
static{
/**
* Fired when an {@link XrInputSource} is removed from the list. The handler is passed the
* {@link XrInputSource} that has been removed.
*
* @event
* @example
* app.xr.input.on('remove', (inputSource) => {
* // input source is removed
* });
*/ this.EVENT_REMOVE = 'remove';
}
static{
/**
* Fired when {@link XrInputSource} has triggered primary action. This could be pressing a
* trigger button, or touching a screen. The handler is passed the {@link XrInputSource} that
* triggered the `select` event and the XRInputSourceEvent event from the WebXR API.
*
* @event
* @example
* const ray = new pc.Ray();
* app.xr.input.on('select', (inputSource, evt) => {
* ray.set(inputSource.getOrigin(), inputSource.getDirection());
* if (obj.intersectsRay(ray)) {
* // selected an object with input source
* }
* });
*/ this.EVENT_SELECT = 'select';
}
static{
/**
* Fired when {@link XrInputSource} has started to trigger primary action. The handler is
* passed the {@link XrInputSource} that triggered the `selectstart` event and the
* XRInputSourceEvent event from the WebXR API.
*
* @event
* @example
* app.xr.input.on('selectstart', (inputSource, evt) => {
* console.log('Select started');
* });
*/ this.EVENT_SELECTSTART = 'selectstart';
}
static{
/**
* Fired when {@link XrInputSource} has ended triggering primary action. The handler is passed
* the {@link XrInputSource} that triggered the `selectend` event and the XRInputSourceEvent
* event from the WebXR API.
*
* @event
* @example
* app.xr.input.on('selectend', (inputSource, evt) => {
* console.log('Select ended');
* });
*/ this.EVENT_SELECTEND = 'selectend';
}
static{
/**
* Fired when {@link XrInputSource} has triggered squeeze action. This is associated with
* "grabbing" action on the controllers. The handler is passed the {@link XrInputSource} that
* triggered the `squeeze` event and the XRInputSourceEvent event from the WebXR API.
*
* @event
* @example
* app.xr.input.on('squeeze', (inputSource, evt) => {
* console.log('Squeeze');
* });
*/ this.EVENT_SQUEEZE = 'squeeze';
}
static{
/**
* Fired when {@link XrInputSource} has started to trigger squeeze action. The handler is
* passed the {@link XrInputSource} that triggered the `squeezestart` event and the
* XRInputSourceEvent event from the WebXR API.
*
* @event
* @example
* app.xr.input.on('squeezestart', (inputSource, evt) => {
* if (obj.containsPoint(inputSource.getPosition())) {
* // grabbed an object
* }
* });
*/ this.EVENT_SQUEEZESTART = 'squeezestart';
}
static{
/**
* Fired when {@link XrInputSource} has ended triggering squeeze action. The handler is passed
* the {@link XrInputSource} that triggered the `squeezeend` event and the XRInputSourceEvent
* event from the WebXR API.
*
* @event
* @example
* app.xr.input.on('squeezeend', (inputSource, evt) => {
* console.log('Squeeze ended');
* });
*/ this.EVENT_SQUEEZEEND = 'squeezeend';
}
/**
* Create a new XrInput instance.
*
* @param {XrManager} manager - WebXR Manager.
* @ignore
*/ constructor(manager){
super(), /**
* @type {XrInputSource[]}
* @private
*/ this._inputSources = [], /**
* @type {boolean}
* @ignore
*/ this.velocitiesSupported = false;
this.manager = manager;
this.velocitiesSupported = !!(platform.browser && window.XRPose?.prototype?.hasOwnProperty('linearVelocity'));
this._onInputSourcesChangeEvt = (evt)=>{
this._onInputSourcesChange(evt);
};
this.manager.on('start', this._onSessionStart, this);
this.manager.on('end', this._onSessionEnd, this);
}
/** @private */ _onSessionStart() {
const session = this.manager.session;
session.addEventListener('inputsourceschange', this._onInputSourcesChangeEvt);
session.addEventListener('select', (evt)=>{
const inputSource = this._getByInputSource(evt.inputSource);
inputSource.update(evt.frame);
inputSource.fire('select', evt);
this.fire('select', inputSource, evt);
});
session.addEventListener('selectstart', (evt)=>{
const inputSource = this._getByInputSource(evt.inputSource);
inputSource.update(evt.frame);
inputSource._selecting = true;
inputSource.fire('selectstart', evt);
this.fire('selectstart', inputSource, evt);
});
session.addEventListener('selectend', (evt)=>{
const inputSource = this._getByInputSource(evt.inputSource);
inputSource.update(evt.frame);
inputSource._selecting = false;
inputSource.fire('selectend', evt);
this.fire('selectend', inputSource, evt);
});
session.addEventListener('squeeze', (evt)=>{
const inputSource = this._getByInputSource(evt.inputSource);
inputSource.update(evt.frame);
inputSource.fire('squeeze', evt);
this.fire('squeeze', inputSource, evt);
});
session.addEventListener('squeezestart', (evt)=>{
const inputSource = this._getByInputSource(evt.inputSource);
inputSource.update(evt.frame);
inputSource._squeezing = true;
inputSource.fire('squeezestart', evt);
this.fire('squeezestart', inputSource, evt);
});
session.addEventListener('squeezeend', (evt)=>{
const inputSource = this._getByInputSource(evt.inputSource);
inputSource.update(evt.frame);
inputSource._squeezing = false;
inputSource.fire('squeezeend', evt);
this.fire('squeezeend', inputSource, evt);
});
// add input sources
const inputSources = session.inputSources;
for(let i = 0; i < inputSources.length; i++){
this._addInputSource(inputSources[i]);
}
}
/** @private */ _onSessionEnd() {
let i = this._inputSources.length;
while(i--){
const inputSource = this._inputSources[i];
this._inputSources.splice(i, 1);
inputSource.fire('remove');
this.fire('remove', inputSource);
}
const session = this.manager.session;
session.removeEventListener('inputsourceschange', this._onInputSourcesChangeEvt);
}
/**
* @param {XRInputSourcesChangeEvent} evt - WebXR input sources change event.
* @private
*/ _onInputSourcesChange(evt) {
// remove
for(let i = 0; i < evt.removed.length; i++){
this._removeInputSource(evt.removed[i]);
}
// add
for(let i = 0; i < evt.added.length; i++){
this._addInputSource(evt.added[i]);
}
}
/**
* @param {XRInputSource} xrInputSource - Input source to search for.
* @returns {XrInputSource|null} The input source that matches the given WebXR input source or
* null if no match is found.
* @private
*/ _getByInputSource(xrInputSource) {
for(let i = 0; i < this._inputSources.length; i++){
if (this._inputSources[i].inputSource === xrInputSource) {
return this._inputSources[i];
}
}
return null;
}
/**
* @param {XRInputSource} xrInputSource - Input source to add.
* @private
*/ _addInputSource(xrInputSource) {
if (this._getByInputSource(xrInputSource)) {
return;
}
const inputSource = new XrInputSource(this.manager, xrInputSource);
this._inputSources.push(inputSource);
this.fire('add', inputSource);
}
/**
* @param {XRInputSource} xrInputSource - Input source to remove.
* @private
*/ _removeInputSource(xrInputSource) {
for(let i = 0; i < this._inputSources.length; i++){
if (this._inputSources[i].inputSource !== xrInputSource) {
continue;
}
const inputSource = this._inputSources[i];
this._inputSources.splice(i, 1);
let h = inputSource.hitTestSources.length;
while(h--){
inputSource.hitTestSources[h].remove();
}
inputSource.fire('remove');
this.fire('remove', inputSource);
return;
}
}
/**
* @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
* @ignore
*/ update(frame) {
for(let i = 0; i < this._inputSources.length; i++){
this._inputSources[i].update(frame);
}
}
/**
* List of active {@link XrInputSource} instances.
*
* @type {XrInputSource[]}
*/ get inputSources() {
return this._inputSources;
}
}
export { XrInput };