UNPKG

@wonderlandengine/components

Version:

Wonderland Engine's official component library.

309 lines 12.5 kB
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