@wonderlandengine/components
Version:
Wonderland Engine's official component library.
309 lines • 12.5 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Component, Emitter } from '@wonderlandengine/api';
import { HandTracking } from './hand-tracking.js';
import { vec3, quat } from 'gl-matrix';
import { property } from '@wonderlandengine/api/decorators.js';
import { VrModeActiveSwitch } from './vr-mode-active-switch.js';
const _tempVec = vec3.create();
const _tempQuat = quat.create();
const _tempRotation1 = new Float32Array(4);
const _tempRotation2 = new Float32Array(4);
const minTemp = new Float32Array(3);
const maxTemp = new Float32Array(3);
const hands = ['left', 'right'];
/**
* Dynamically load and map input profiles for XR controllers.
*/
class InputProfile extends Component {
static TypeName = 'input-profile';
/**
* A cache to store loaded profiles for reuse.
*/
static Cache = new Map();
_gamepadObjects = {};
_controllerModel = null;
_defaultControllerComponents;
_handedness;
_profileJSON = null;
_buttons = [];
_axes = [];
/**
* The XR gamepad associated with the current input source.
*/
gamepad;
/**
* A reference to the emitter which triggered on model lodaed event.
*/
onModelLoaded = new Emitter();
/**
* Returns url of input profile json file
*/
url;
/**
* A set of components to filter during component retrieval.
*/
toFilter = new Set(['vr-mode-active-mode-switch']);
/**
* The index representing the handedness of the controller (0 for left, 1 for right).
*/
handedness = 0;
/**
* The base path where XR input profiles are stored.
*/
defaultBasePath;
/**
* An optional folder path for loading custom XR input profiles.
*/
customBasePath;
/**
* The default 3D controller model used when a custom model fails to load.
*/
defaultController;
/**
* The object which has HandTracking component added to it.
*/
trackedHand;
/**
* If true, the input profile will be mapped to the default controller, and no dynamic 3D model of controller will be loaded.
*/
mapToDefaultController;
/**
* If true, adds a VR mode switch component to the loaded controller model.
*/
addVrModeSwitch;
onActivate() {
this._handedness = hands[this.handedness];
const defaultHandName = 'Hand' + this._handedness.charAt(0).toUpperCase() + this._handedness.slice(1);
this.trackedHand =
this.trackedHand ?? this.object.parent?.findByNameRecursive(defaultHandName)[0];
this.defaultController = this.defaultController || this.object.children[0];
this._defaultControllerComponents = this._getComponents(this.defaultController);
this.engine.onXRSessionStart.add(() => {
this.engine.xr?.session.addEventListener('inputsourceschange', this._onInputSourcesChange.bind(this));
});
}
onDeactivate() {
this.engine.xr?.session?.removeEventListener('inputsourceschange', this._onInputSourcesChange.bind(this));
}
/**
* Sets newly loaded controllers for the HandTracking component to proper switching.
* @param controllerObject The controller object.
* @hidden
*/
_setHandTrackingControllers(controllerObject) {
const handtrackingComponent = this.trackedHand.getComponent(HandTracking);
if (!handtrackingComponent)
return;
handtrackingComponent.controllerToDeactivate = controllerObject;
}
/**
* Retrieves all components from the specified object and its children.
* @param obj The object to retrieve components from.
* @return An array of components.
* @hidden
*/
_getComponents(obj) {
const components = [];
if (obj == null)
return components;
const stack = [obj];
while (stack.length > 0) {
const currentObj = stack.pop();
const comps = currentObj
.getComponents()
.filter((c) => !this.toFilter.has(c.type));
components.push(...comps);
const children = currentObj.children;
// Push children onto the stack in reverse order to maintain the correct order
for (let i = children.length - 1; i >= 0; --i) {
stack.push(children[i]);
}
}
return components;
}
/**
* Activates or deactivates components based on the specified boolean value.
* @param active If true, components are set to active; otherwise, they are set to inactive.
* @hidden
*/
_setComponentsActive(active) {
const comps = this._defaultControllerComponents;
if (comps == undefined)
return;
for (let i = 0; i < comps.length; ++i) {
comps[i].active = active;
}
}
/**
* Event handler triggered when XR input sources change.
* Detects new XR input sources and initiates the loading of input profiles.
* @param event The XR input source change event.
* @hidden
*/
_onInputSourcesChange(event) {
if (this._isModelLoaded() && !this.mapToDefaultController) {
this._setComponentsActive(false);
}
event.added.forEach((xrInputSource) => {
if (xrInputSource.hand != null)
return;
if (this._handedness != xrInputSource.handedness)
return;
this.gamepad = xrInputSource.gamepad;
const profile = this.customBasePath !== ''
? this.customBasePath
: this.defaultBasePath + xrInputSource.profiles[0];
this.url = profile + '/profile.json';
this._profileJSON = InputProfile.Cache.get(this.url) ?? null;
if (this._profileJSON != null)
return;
fetch(this.url)
.then((res) => res.json())
.then((out) => {
this._profileJSON = out;
InputProfile.Cache.set(this.url, this._profileJSON);
if (!this._isModelLoaded())
this._loadAndMapGamepad(profile);
})
.catch((e) => {
console.error(`Failed to load profile from ${this.url}. Reason:`, e);
});
});
}
/**
* Checks if the 3D controller model is loaded.
* @return True if the model is loaded; otherwise, false.
* @hidden
*/
_isModelLoaded() {
return this._controllerModel !== null;
}
/**
* Loads the 3D controller model and caches the mapping to the gamepad.
* @param profile The path to the input profile.
* @hidden
*/
async _loadAndMapGamepad(profile) {
const assetPath = profile + '/' + this._handedness + '.glb';
this._controllerModel = this.defaultController;
if (!this.mapToDefaultController) {
/** load 3d model in the runtime with profile url */
try {
this._controllerModel = (await this.engine.scene.append(assetPath));
}
catch (e) {
console.error(`Failed to load i-p controller model. Reason:`, e, `Continuing with ${this._handedness} default controller.`);
this._setComponentsActive(true);
}
this._controllerModel.parent = this.object;
this._controllerModel.setPositionLocal([0, 0, 0]);
this._setComponentsActive(false);
if (this.addVrModeSwitch)
this._controllerModel.addComponent(VrModeActiveSwitch);
this.onModelLoaded.notify();
}
this._cacheGamepadObjectsFromProfile(this._profileJSON, this._controllerModel);
this._setHandTrackingControllers(this._controllerModel);
this.update = () => this._mapGamepadInput();
}
/**
* Caches gamepad objects (buttons, axes) from the loaded input profile.
* @hidden
*/
_cacheGamepadObjectsFromProfile(profile, obj) {
const components = profile.layouts[this._handedness].components;
if (!components)
return;
this._buttons = [];
this._axes = [];
for (const i in components) {
const visualResponses = components[i].visualResponses;
if (components[i].rootNodeName === 'menu')
continue;
for (const j in visualResponses) {
// update buttons with new interface of current visual response
const visualResponse = visualResponses[j];
const valueNode = visualResponse.valueNodeName;
const minNode = visualResponse.minNodeName;
const maxNode = visualResponse.maxNodeName;
this._gamepadObjects[valueNode] = obj.findByNameRecursive(valueNode)[0];
this._gamepadObjects[minNode] = obj.findByNameRecursive(minNode)[0];
this._gamepadObjects[maxNode] = obj.findByNameRecursive(maxNode)[0];
const prop = visualResponses[j].componentProperty;
const response = {
target: this._gamepadObjects[valueNode],
min: this._gamepadObjects[minNode],
max: this._gamepadObjects[maxNode],
id: components[i].gamepadIndices[prop], // Assign a unique ID
};
switch (prop) {
case 'button':
this._buttons.push(response);
break;
case 'xAxis':
case 'yAxis':
this._axes.push(response);
break;
}
}
}
}
/**
* Assigns a transformed position and rotation to the target based on minimum and maximum values and a normalized input value.
* @param target The target object to be transformed.
* @param min The minimum object providing transformation limits.
* @param max The maximum object providing transformation limits.
* @param value The normalized input value.
* @hidden
*/
_assignTransform(target, min, max, value) {
vec3.lerp(_tempVec, min.getPositionWorld(minTemp), max.getPositionWorld(maxTemp), value);
target.setPositionWorld(_tempVec);
quat.lerp(_tempQuat, min.getRotationWorld(_tempRotation1), max.getRotationWorld(_tempRotation2), value);
quat.normalize(_tempQuat, _tempQuat);
target.setRotationWorld(_tempQuat);
}
/**
* Maps input values (buttons, axes) to the 3D controller model.
* @hidden
*/
_mapGamepadInput() {
for (const button of this._buttons) {
const buttonValue = this.gamepad.buttons[button.id].value;
this._assignTransform(button.target, button.min, button.max, buttonValue);
}
for (const axis of this._axes) {
const axisValue = this.gamepad.axes[axis.id];
const normalizedAxisValue = (axisValue + 1) / 2;
this._assignTransform(axis.target, axis.min, axis.max, normalizedAxisValue);
}
}
}
__decorate([
property.enum(hands, 0)
], InputProfile.prototype, "handedness", void 0);
__decorate([
property.string('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@latest/dist/profiles/')
], InputProfile.prototype, "defaultBasePath", void 0);
__decorate([
property.string()
], InputProfile.prototype, "customBasePath", void 0);
__decorate([
property.object()
], InputProfile.prototype, "defaultController", void 0);
__decorate([
property.object()
], InputProfile.prototype, "trackedHand", void 0);
__decorate([
property.bool(false)
], InputProfile.prototype, "mapToDefaultController", void 0);
__decorate([
property.bool(true)
], InputProfile.prototype, "addVrModeSwitch", void 0);
export { InputProfile };
//# sourceMappingURL=input-profile.js.map