From 024cadd49b8d81d79cb38eeef71c4d1f6aa2ca52 Mon Sep 17 00:00:00 2001 From: machenmusik Date: Thu, 13 Apr 2017 14:37:57 -0400 Subject: [PATCH] add vive-tracker disable everGotGamepadEvent usage, seeing issues with latest Nightly make sure vive-controls doesn't match tracker bring in line with latest #2513 work around Nightly bug to get both controllers and trackers working at the same time bring vive-tracker in line with #2513 fix tests per separate discussion, use controllers list from system not getGamepadsByPrefix bring in line with #2505 make sure that queryObject values are used even if falsy e.g. index 0 tracked-controls needs to know about hand to filter properly and find vive trackers, given current Nightly bug. since empty string is always changed to default value (even when default is undefined), need to use 'none' to indicate empty-string hand re-clone tracker tests from controls use newly committed vive-tracker model add rotation property, which applies if no rotationOffset and allows full 3DOF rotation correct pose orientation for tracker to be what is generally expected of controllers fix default vive-tracker rotation correction, per discussion updateControllerModel for all tracked controls components; use fixed vive tracker model until published to CDN use new tracker model from CDN add rotation parameter apply rotationOffset the old way, to avoid precision issues add rotation offset test, with EPSILON change sentinel hand value to avoid node test failure zero pivot point update tracked-controls hand docs per discussion on PR fix duplicate play/pause presumably from botched merge --- docs/components/tracked-controls.md | 6 +- docs/components/vive-tracker.md | 58 ++++++ src/components/daydream-controls.js | 4 + src/components/gearvr-controls.js | 4 + src/components/index.js | 1 + src/components/tracked-controls.js | 30 +++- src/components/vive-controls.js | 19 +- src/components/vive-tracker.js | 207 ++++++++++++++++++++++ src/utils/tracked-controls.js | 8 +- tests/components/tracked-controls.test.js | 26 ++- tests/components/vive-tracker.test.js | 203 +++++++++++++++++++++ 11 files changed, 543 insertions(+), 23 deletions(-) create mode 100644 docs/components/vive-tracker.md create mode 100644 src/components/vive-tracker.js create mode 100644 tests/components/vive-tracker.test.js diff --git a/docs/components/tracked-controls.md b/docs/components/tracked-controls.md index e984b6ca335..45ebb3894f4 100644 --- a/docs/components/tracked-controls.md +++ b/docs/components/tracked-controls.md @@ -38,9 +38,11 @@ so using idPrefix for Vive / OpenVR controllers is recommended. | controller | Index of the controller in array returned by the Gamepad API. | 0 | | id | Selects the controller from the Gamepad API using exact match. | | | idPrefix | Selects the controller from the Gamepad API using prefix match. | | -| rotationOffset | Offset to add to model rotation. | 0 | +| rotation | Offset to add to model rotation. | | +| rotationOffset | Offset (around Z axis) to add to model rotation. | 0 | | headElement | Head element for arm model if needed (if not active camera). | | -| hand | Which hand to use, if arm model is needed. (left negates X) | right | +| hand | Which hand to match (and use if arm model needed). | any-or-none | +| armModel | Whether to use arm model to simulate 3DOF controllers position. | true | ## Events diff --git a/docs/components/vive-tracker.md b/docs/components/vive-tracker.md new file mode 100644 index 00000000000..d2b430ac39b --- /dev/null +++ b/docs/components/vive-tracker.md @@ -0,0 +1,58 @@ +--- +title: vive-tracker +type: components +layout: docs +parent_section: components +--- + +[trackedcontrols]: ./tracked-controls.md + +The vive-tracker component interfaces with the HTC Vive Tracker. It +wraps the [tracked-controls component][trackedcontrols] while adding button +mappings, events, and a model. + +## Example + +```html + + + + + +``` + +## Value + +| Property | Description | Default Value | +|----------------------|----------------------------------------------------|----------------------| +| buttonColor | Button colors when not pressed. | #FAFAFA (off-white) | +| buttonHighlightColor | Button colors when pressed and active. | #22D1EE (light blue) | +| index | The index of the tracker that will be tracked. | 0 | +| model | Whether the model is loaded. | true | +| rotationOffset | Offset to apply to model rotation. | 0 | + +## Events + +| Event Name | Description | +| ---------- | ----------- | +| gripdown | Grip button pressed. | +| gripup | Grip button released. | +| gripchanged | Grip button changed. | +| menudown | Menu button pressed. | +| menuup | Menu button released. | +| menuchanged | Menu button changed. | +| systemdown | System button pressed. | +| systemup | System button released. | +| systemchanged | System button changed. | +| trackpaddown | Trackpad pressed. | +| trackpadup | Trackpad released. | +| trackpadchanged | Trackpad button changed. | +| triggerdown | Trigger pressed. | +| triggerup | Trigger released. | +| triggerchanged | Trigger changed. | + +## Assets + +- [Controller OBJ](https://cdn.aframe.io/controllers/vive/vr_controller_vive.obj) +- [Controller MTL](https://cdn.aframe.io/controllers/vive/vr_controller_vive.mtl) + diff --git a/src/components/daydream-controls.js b/src/components/daydream-controls.js index 98142e86c8a..7dfd2abe5e7 100644 --- a/src/components/daydream-controls.js +++ b/src/components/daydream-controls.js @@ -116,6 +116,10 @@ module.exports.Component = registerComponent('daydream-controls', { var el = this.el; var data = this.data; el.setAttribute('tracked-controls', {idPrefix: GAMEPAD_ID_PREFIX, hand: data.hand, rotationOffset: data.rotationOffset, armModel: data.armModel}); + this.updateControllerModel(); + }, + + updateControllerModel: function () { if (!this.data.model) { return; } this.el.setAttribute('obj-model', { obj: DAYDREAM_CONTROLLER_MODEL_OBJ_URL, diff --git a/src/components/gearvr-controls.js b/src/components/gearvr-controls.js index 885e3b2ff53..269e996d479 100644 --- a/src/components/gearvr-controls.js +++ b/src/components/gearvr-controls.js @@ -116,6 +116,10 @@ module.exports.Component = registerComponent('gearvr-controls', { var el = this.el; var data = this.data; el.setAttribute('tracked-controls', {idPrefix: GAMEPAD_ID_PREFIX, rotationOffset: data.rotationOffset, armModel: data.armModel}); + this.updateControllerModel(); + }, + + updateControllerModel: function () { if (!this.data.model) { return; } this.el.setAttribute('obj-model', { obj: GEARVR_CONTROLLER_MODEL_OBJ_URL, diff --git a/src/components/index.js b/src/components/index.js index 97e77168184..cd7834280f1 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -24,6 +24,7 @@ require('./text'); require('./tracked-controls'); require('./visible'); require('./vive-controls'); +require('./vive-tracker'); require('./wasd-controls'); require('./scene/canvas'); diff --git a/src/components/tracked-controls.js b/src/components/tracked-controls.js index 75779e7bcad..73d91c0784f 100644 --- a/src/components/tracked-controls.js +++ b/src/components/tracked-controls.js @@ -1,5 +1,6 @@ var registerComponent = require('../core/component').registerComponent; var THREE = require('../lib/three'); +var DEG2RAD = require('../lib/three').Math.DEG2RAD; var DEFAULT_USER_HEIGHT = require('../constants').DEFAULT_USER_HEIGHT; var DEFAULT_HANDEDNESS = require('../constants').DEFAULT_HANDEDNESS; @@ -18,9 +19,11 @@ var FOREARM = {x: 0, y: 0, z: -0.175}; // vector from eyes to elbow (divided by module.exports.Component = registerComponent('tracked-controls', { schema: { controller: {default: 0}, + hand: {type: 'string', default: 'any-or-none'}, id: {type: 'string', default: ''}, idPrefix: {type: 'string', default: ''}, rotationOffset: {default: 0}, + rotation: {type: 'vec3'}, // Arm model parameters, to use when not 6DOF. (pose hasPosition false, no position) armModel: {default: true}, headElement: {type: 'selector'} @@ -30,6 +33,8 @@ module.exports.Component = registerComponent('tracked-controls', { this.axis = [0, 0, 0]; this.buttonStates = {}; + this.rotationOffset = new THREE.Quaternion(); + this.dolly = new THREE.Object3D(); this.controllerEuler = new THREE.Euler(); this.controllerEuler.order = 'YXZ'; @@ -42,6 +47,14 @@ module.exports.Component = registerComponent('tracked-controls', { this.updateGamepad(); }, + update: function (oldData) { + var data = this.data; + if (data.rotation === oldData.rotation) { return; } + + this.controllerEuler.set(DEG2RAD * data.rotation.x, DEG2RAD * data.rotation.y, DEG2RAD * data.rotation.z); + this.rotationOffset.setFromEuler(this.controllerEuler); + }, + tick: function (time, delta) { var mesh = this.el.getObject3D('mesh'); // Update mesh animations. @@ -70,9 +83,15 @@ module.exports.Component = registerComponent('tracked-controls', { var matchingControllers; // Hand IDs: 0 is right, 1 is left. - matchingControllers = controllers.filter(function hasIdOrPrefix (controller) { - if (data.idPrefix) { return controller.id.indexOf(data.idPrefix) === 0; } - return controller.id === data.id; + matchingControllers = controllers.filter(function hasIdOrPrefixAndHand (controller) { + if (data.idPrefix) { + if (controller.id.indexOf(data.idPrefix) !== 0) { return false; } + } else { + if (controller.id !== data.id) { return false; } + } + + if (data.hand === 'any-or-none') { return true; } + return (controller.hand === undefined ? DEFAULT_HANDEDNESS : controller.hand) === (data.hand === 'none' ? '' : data.hand); }); this.controller = matchingControllers[data.controller]; @@ -129,6 +148,7 @@ module.exports.Component = registerComponent('tracked-controls', { var controller = this.controller; var controllerEuler = this.controllerEuler; var controllerPosition = this.controllerPosition; + var controllerQuaternion = this.controllerQuaternion; var currentPosition; var deltaControllerPosition = this.deltaControllerPosition; var dolly = this.dolly; @@ -175,7 +195,9 @@ module.exports.Component = registerComponent('tracked-controls', { } // Decompose. - controllerEuler.setFromRotationMatrix(dolly.matrix); + controllerQuaternion.setFromRotationMatrix(dolly.matrix); + controllerQuaternion.multiply(this.rotationOffset); + controllerEuler.setFromQuaternion(controllerQuaternion); controllerPosition.setFromMatrixPosition(dolly.matrix); // Apply rotation (as absolute, with rotation offset). diff --git a/src/components/vive-controls.js b/src/components/vive-controls.js index fe50d0dfd08..a30c327483e 100644 --- a/src/components/vive-controls.js +++ b/src/components/vive-controls.js @@ -103,31 +103,28 @@ module.exports.Component = registerComponent('vive-controls', { el.removeEventListener('axismove', this.onAxisMoved); }, - /** - * Once OpenVR returns correct hand data in supporting browsers, we can use hand property. - * var isPresent = this.checkControllerPresentAndSetup(this.el.sceneEl, GAMEPAD_ID_PREFIX, - { hand: data.hand }); - * Until then, use hardcoded index. - */ checkIfControllerPresent: function () { var data = this.data; - var controllerIndex = data.hand === 'right' ? 0 : data.hand === 'left' ? 1 : 2; - this.checkControllerPresentAndSetup(this, GAMEPAD_ID_PREFIX, {index: controllerIndex}); + // Once OpenVR / SteamVR return correct hand data in the supporting browsers, we can use hand property. + this.checkControllerPresentAndSetup(this, GAMEPAD_ID_PREFIX, { hand: data.hand }); }, injectTrackedControls: function () { var el = this.el; var data = this.data; - // If we have an OpenVR Gamepad, use the fixed mapping. + // Use the supplied controller index. el.setAttribute('tracked-controls', { idPrefix: GAMEPAD_ID_PREFIX, - // Hand IDs: 0 = right, 1 = left, 2 = anything else. - controller: data.hand === 'right' ? 0 : data.hand === 'left' ? 1 : 2, + hand: data.hand, rotationOffset: data.rotationOffset }); // Load model. + this.updateControllerModel(); + }, + + updateControllerModel: function () { if (!this.data.model) { return; } this.el.setAttribute('obj-model', { obj: VIVE_CONTROLLER_MODEL_OBJ_URL, diff --git a/src/components/vive-tracker.js b/src/components/vive-tracker.js new file mode 100644 index 00000000000..844bf6ec66d --- /dev/null +++ b/src/components/vive-tracker.js @@ -0,0 +1,207 @@ +var registerComponent = require('../core/component').registerComponent; +var bind = require('../utils/bind'); +var checkControllerPresentAndSetup = require('../utils/tracked-controls').checkControllerPresentAndSetup; +var emitIfAxesChanged = require('../utils/tracked-controls').emitIfAxesChanged; + +var VIVE_TRACKER_MODEL_BASE_URL = 'https://cdn.aframe.io/controllers/vive/vr_tracker-vive'; +var VIVE_TRACKER_MODEL_OBJ_URL = VIVE_TRACKER_MODEL_BASE_URL + '.obj'; +var VIVE_TRACKER_MODEL_OBJ_MTL = VIVE_TRACKER_MODEL_BASE_URL + '.mtl'; + +var GAMEPAD_ID_PREFIX = 'OpenVR '; // should be OpenVR Tracker, but work around Nightly bug + +/** + * Vive Tracker Component + * Interfaces with vive trackers and maps Gamepad events to + * common controller buttons: trackpad, trigger, grip, menu and system + * It loads a model and highlights the pressed buttons + */ +module.exports.Component = registerComponent('vive-tracker', { + schema: { + index: {type: 'number', default: 0}, + buttonColor: {type: 'color', default: '#FAFAFA'}, // Off-white. + buttonHighlightColor: {type: 'color', default: '#22D1EE'}, // Light blue. + model: {default: true}, + rotationOffset: {default: 0}, // use -999 as sentinel value to auto-determine based on hand + rotation: {type: 'string', default: '-90 0 0'} // default rotation to make orientation match controller + }, + + // buttonId + // 0 - trackpad + // 1 - trigger ( intensity value from 0.5 to 1 ) + // 2 - grip + // 3 - menu ( dispatch but better for menu options ) + // 4 - system ( never dispatched on this layer ) + mapping: { + axes: {'trackpad': [0, 1]}, + buttons: ['trackpad', 'trigger', 'grip', 'menu', 'system'] + }, + + // Use these labels for detail on axis events such as thumbstickmoved. + // e.g. for thumbstickmoved detail, the first axis returned is labeled x, and the second is labeled y. + axisLabels: ['x', 'y', 'z', 'w'], + + bindMethods: function () { + this.onModelLoaded = bind(this.onModelLoaded, this); + this.onControllersUpdate = bind(this.onControllersUpdate, this); + this.checkIfControllerPresent = bind(this.checkIfControllerPresent, this); + this.removeControllersUpdateListener = bind(this.removeControllersUpdateListener, this); + this.onAxisMoved = bind(this.onAxisMoved, this); + }, + + init: function () { + var self = this; + this.animationActive = 'pointing'; + this.onButtonChanged = bind(this.onButtonChanged, this); + this.onButtonDown = function (evt) { self.onButtonEvent(evt.detail.id, 'down'); }; + this.onButtonUp = function (evt) { self.onButtonEvent(evt.detail.id, 'up'); }; + this.onButtonTouchStart = function (evt) { self.onButtonEvent(evt.detail.id, 'touchstart'); }; + this.onButtonTouchEnd = function (evt) { self.onButtonEvent(evt.detail.id, 'touchend'); }; + this.onAxisMoved = bind(this.onAxisMoved, this); + this.controllerPresent = false; + this.lastControllerCheck = 0; + this.previousButtonValues = {}; + this.bindMethods(); + this.checkControllerPresentAndSetup = checkControllerPresentAndSetup; // to allow mock + this.emitIfAxesChanged = emitIfAxesChanged; // to allow mock + }, + + addEventListeners: function () { + var el = this.el; + el.addEventListener('buttonchanged', this.onButtonChanged); + el.addEventListener('buttondown', this.onButtonDown); + el.addEventListener('buttonup', this.onButtonUp); + el.addEventListener('touchstart', this.onButtonTouchStart); + el.addEventListener('touchend', this.onButtonTouchEnd); + el.addEventListener('model-loaded', this.onModelLoaded); + el.addEventListener('axismove', this.onAxisMoved); + }, + + removeEventListeners: function () { + var el = this.el; + el.removeEventListener('buttonchanged', this.onButtonChanged); + el.removeEventListener('buttondown', this.onButtonDown); + el.removeEventListener('buttonup', this.onButtonUp); + el.removeEventListener('touchstart', this.onButtonTouchStart); + el.removeEventListener('touchend', this.onButtonTouchEnd); + el.removeEventListener('model-loaded', this.onModelLoaded); + el.removeEventListener('axismove', this.onAxisMoved); + }, + + checkIfControllerPresent: function () { + var data = this.data; + this.checkControllerPresentAndSetup(this, GAMEPAD_ID_PREFIX, { hand: '', index: data.index }); + }, + + play: function () { + this.checkIfControllerPresent(); + this.addControllersUpdateListener(); + // Note that due to gamepadconnected event propagation issues, we don't rely on events. + window.addEventListener('gamepaddisconnected', this.checkIfControllerPresent, false); + }, + + pause: function () { + this.removeEventListeners(); + this.removeControllersUpdateListener(); + // Note that due to gamepadconnected event propagation issues, we don't rely on events. + window.removeEventListener('gamepaddisconnected', this.checkIfControllerPresent, false); + }, + + injectTrackedControls: function () { + var el = this.el; + var data = this.data; + el.setAttribute('tracked-controls', {idPrefix: GAMEPAD_ID_PREFIX, hand: 'none', controller: data.index, rotationOffset: data.rotationOffset, rotation: data.rotation}); + this.updateControllerModel(); + }, + + updateControllerModel: function () { + if (!this.data.model) { return; } + this.el.setAttribute('obj-model', { + obj: VIVE_TRACKER_MODEL_OBJ_URL, + mtl: VIVE_TRACKER_MODEL_OBJ_MTL + }); + }, + + addControllersUpdateListener: function () { + this.el.sceneEl.addEventListener('controllersupdated', this.onControllersUpdate, false); + }, + + removeControllersUpdateListener: function () { + this.el.sceneEl.removeEventListener('controllersupdated', this.onControllersUpdate, false); + }, + + onControllersUpdate: function () { this.checkIfControllerPresent(); }, + + onButtonChanged: function (evt) { + var button = this.mapping.buttons[evt.detail.id]; + var buttonMeshes = this.buttonMeshes; + var analogValue; + if (!button) { return; } + + if (button === 'trigger') { + analogValue = evt.detail.state.value; + // Update button mesh, if any. + if (buttonMeshes && buttonMeshes.trigger) { + buttonMeshes.trigger.rotation.x = -analogValue * (Math.PI / 12); + } + } + + // Pass along changed event with button state, using button mapping for convenience. + this.el.emit(button + 'changed', evt.detail.state); + }, + + onModelLoaded: function (evt) { + var controllerObject3D = evt.detail.model; + var buttonMeshes; + if (!this.data.model) { return; } + buttonMeshes = this.buttonMeshes = {}; + buttonMeshes.grip = { + left: controllerObject3D.getObjectByName('leftgrip'), + right: controllerObject3D.getObjectByName('rightgrip') + }; + buttonMeshes.menu = controllerObject3D.getObjectByName('menubutton'); + buttonMeshes.system = controllerObject3D.getObjectByName('systembutton'); + buttonMeshes.trackpad = controllerObject3D.getObjectByName('touchpad'); + buttonMeshes.trigger = controllerObject3D.getObjectByName('trigger'); + // Offset pivot point + controllerObject3D.position.set(0, 0, 0); + }, + + onAxisMoved: function (evt) { this.emitIfAxesChanged(this, this.mapping.axes, evt); }, + + onButtonEvent: function (id, evtName) { + var buttonName = this.mapping.buttons[id]; + var i; + if (Array.isArray(buttonName)) { + for (i = 0; i < buttonName.length; i++) { + this.el.emit(buttonName[i] + evtName); + } + } else { + this.el.emit(buttonName + evtName); + } + this.updateModel(buttonName, evtName); + }, + + updateModel: function (buttonName, evtName) { + var i; + if (!this.data.model) { return; } + if (Array.isArray(buttonName)) { + for (i = 0; i < buttonName.length; i++) { + this.updateButtonModel(buttonName[i], evtName); + } + } else { + this.updateButtonModel(buttonName, evtName); + } + }, + + updateButtonModel: function (buttonName, state) { + var color = state === 'up' ? this.data.buttonColor : this.data.buttonHighlightColor; + var buttonMeshes = this.buttonMeshes; + if (!buttonMeshes) { return; } + if (buttonName === 'grip') { + buttonMeshes.grip.left.material.color.set(color); + buttonMeshes.grip.right.material.color.set(color); + return; + } + buttonMeshes[buttonName].material.color.set(color); + } +}); diff --git a/src/utils/tracked-controls.js b/src/utils/tracked-controls.js index 3223ca56066..bc373c022f5 100644 --- a/src/utils/tracked-controls.js +++ b/src/utils/tracked-controls.js @@ -81,14 +81,14 @@ function isControllerPresent (component, idPrefix, queryObject) { gamepad = gamepads[i]; isPrefixMatch = (!idPrefix || idPrefix === '' || gamepad.id.indexOf(idPrefix) === 0); isPresent = isPrefixMatch; - if (isPresent && queryObject.hand) { - isPresent = (gamepad.hand || DEFAULT_HANDEDNESS) === queryObject.hand; + if (isPresent && (queryObject.hand !== undefined)) { + isPresent = (gamepad.hand === undefined ? DEFAULT_HANDEDNESS : gamepad.hand) === queryObject.hand; } - if (isPresent && queryObject.index) { + if (isPresent && (queryObject.index !== undefined)) { isPresent = index === queryObject.index; // need to use count of gamepads with idPrefix + index++; // (and correct hand filter if present) } if (isPresent) { break; } - if (isPrefixMatch) { index++; } // update count of gamepads with idPrefix } return isPresent; diff --git a/tests/components/tracked-controls.test.js b/tests/components/tracked-controls.test.js index 3386b6a70f3..8db17d655b1 100644 --- a/tests/components/tracked-controls.test.js +++ b/tests/components/tracked-controls.test.js @@ -205,10 +205,32 @@ suite('tracked-controls', function () { }); test('applies rotation Z-offset', function () { + var rotationOffset = 10; assertVec3(el.getAttribute('rotation'), [0, 0, 0]); - el.setAttribute('tracked-controls', 'rotationOffset', 10); + el.setAttribute('tracked-controls', 'rotationOffset', rotationOffset); component.tick(); - assertVec3(el.getAttribute('rotation'), [0, 0, 10]); + var rotation = el.getAttribute('rotation'); + var EPSILON = 0; + assert.isAtMost(Math.abs(0 - rotation.x), EPSILON, 'rotation X delta is at most EPSILON'); + assert.isAtMost(Math.abs(0 - rotation.y), EPSILON, 'rotation Y delta is at most EPSILON'); + assert.isAtMost(Math.abs(rotationOffset - rotation.z), EPSILON, 'rotation Z delta is at most EPSILON'); + }); + + test('applies rotation offset', function () { + // var rotationOffset = new THREE.Vector3(10, 20, 30); // EPSILON = 1.4E-6 + // var rotationOffset = new THREE.Vector3(90, 0, 180); // comes out 90 180 0 + var rotationOffset = new THREE.Vector3(90, 180, 0); + assertVec3(el.getAttribute('rotation'), [0, 0, 0]); + el.setAttribute('tracked-controls', 'rotationOffset', 0); + el.setAttribute('tracked-controls', 'rotation', rotationOffset); + component.tick(); + // Due to mathematical precision, assertVec3 returns AssertionError + // assertVec3(el.getAttribute('rotation'), [10, 20, 30]); + var rotation = el.getAttribute('rotation'); + var EPSILON = 0; + assert.isAtMost(Math.abs(rotationOffset.x - rotation.x), EPSILON, 'rotation X delta is at most EPSILON'); + assert.isAtMost(Math.abs(rotationOffset.y - rotation.y), EPSILON, 'rotation Y delta is at most EPSILON'); + assert.isAtMost(Math.abs(rotationOffset.z - rotation.z), EPSILON, 'rotation Z delta is at most EPSILON'); }); }); diff --git a/tests/components/vive-tracker.test.js b/tests/components/vive-tracker.test.js new file mode 100644 index 00000000000..e245c455467 --- /dev/null +++ b/tests/components/vive-tracker.test.js @@ -0,0 +1,203 @@ +/* global assert, process, setup, suite, test, CustomEvent, Event */ +var entityFactory = require('../helpers').entityFactory; +var controllerComponentName = 'vive-tracker'; + +suite(controllerComponentName, function () { + setup(function (done) { + var el = this.el = entityFactory(); + el.setAttribute(controllerComponentName, 'index: 0'); // to ensure index = 0 + el.addEventListener('loaded', function () { + var controllerComponent = el.components[controllerComponentName]; + controllerComponent.controllersWhenPresent = [{id: 'OpenVR Tracker', index: 0, hand: '', pose: {}}]; + done(); + }); + }); + + suite('checkIfControllerPresent', function () { + test('first-time, if no controllers, remember not present', function () { + var el = this.el; + var controllerComponent = el.components[controllerComponentName]; + var addEventListenersSpy = this.sinon.spy(controllerComponent, 'addEventListeners'); + var injectTrackedControlsSpy = this.sinon.spy(controllerComponent, 'injectTrackedControls'); + + el.sceneEl.systems['tracked-controls'].controllers = []; + + // reset so we don't think we've looked before + controllerComponent.controllerPresent = false; + // do the check + controllerComponent.checkIfControllerPresent(); + // check assertions + assert.notOk(injectTrackedControlsSpy.called); + assert.notOk(addEventListenersSpy.called); + assert.ok(controllerComponent.controllerPresent === false); // not undefined + }); + + test('if no controllers again, do not remove event listeners', function () { + var el = this.el; + var controllerComponent = el.components[controllerComponentName]; + var addEventListenersSpy = this.sinon.spy(controllerComponent, 'addEventListeners'); + var injectTrackedControlsSpy = this.sinon.spy(controllerComponent, 'injectTrackedControls'); + var removeEventListenersSpy = this.sinon.spy(controllerComponent, 'removeEventListeners'); + + el.sceneEl.systems['tracked-controls'].controllers = []; + + // pretend we've looked before + controllerComponent.controllerPresent = false; + // do the check + controllerComponent.checkIfControllerPresent(); + // check assertions + assert.notOk(injectTrackedControlsSpy.called); + assert.notOk(addEventListenersSpy.called); + assert.notOk(removeEventListenersSpy.called); + assert.ok(controllerComponent.controllerPresent === false); // not undefined + }); + + test('attach events if controller is newly present', function () { + var el = this.el; + var controllerComponent = el.components[controllerComponentName]; + var addEventListenersSpy = this.sinon.spy(controllerComponent, 'addEventListeners'); + var injectTrackedControlsSpy = this.sinon.spy(controllerComponent, 'injectTrackedControls'); + var removeEventListenersSpy = this.sinon.spy(controllerComponent, 'removeEventListeners'); + + el.sceneEl.systems['tracked-controls'].controllers = controllerComponent.controllersWhenPresent; + + // reset so we don't think we've looked before + controllerComponent.controllerPresent = false; + // do the check + controllerComponent.checkIfControllerPresent(); + // check assertions + assert.ok(injectTrackedControlsSpy.called); + assert.ok(addEventListenersSpy.called); + assert.notOk(removeEventListenersSpy.called); + assert.ok(controllerComponent.controllerPresent); + }); + + test('do not inject or attach events again if controller is already present', function () { + var el = this.el; + var controllerComponent = el.components[controllerComponentName]; + var addEventListenersSpy = this.sinon.spy(controllerComponent, 'addEventListeners'); + var injectTrackedControlsSpy = this.sinon.spy(controllerComponent, 'injectTrackedControls'); + var removeEventListenersSpy = this.sinon.spy(controllerComponent, 'removeEventListeners'); + + el.sceneEl.systems['tracked-controls'].controllers = controllerComponent.controllersWhenPresent; + + // pretend we've looked before + controllerComponent.controllerPresent = true; + // do the check + controllerComponent.checkIfControllerPresent(); + // check assertions + assert.notOk(injectTrackedControlsSpy.called); + assert.notOk(addEventListenersSpy.called); + assert.notOk(removeEventListenersSpy.called); + assert.ok(controllerComponent.controllerPresent); + }); + + test('if controller disappears, remove event listeners', function () { + var el = this.el; + var controllerComponent = el.components[controllerComponentName]; + var addEventListenersSpy = this.sinon.spy(controllerComponent, 'addEventListeners'); + var injectTrackedControlsSpy = this.sinon.spy(controllerComponent, 'injectTrackedControls'); + var removeEventListenersSpy = this.sinon.spy(controllerComponent, 'removeEventListeners'); + + el.sceneEl.systems['tracked-controls'].controllers = []; + + // pretend we've looked before + controllerComponent.controllerPresent = true; + // do the check + controllerComponent.checkIfControllerPresent(); + // check assertions + assert.notOk(injectTrackedControlsSpy.called); + assert.notOk(addEventListenersSpy.called); + assert.ok(removeEventListenersSpy.called); + assert.notOk(controllerComponent.controllerPresent); + }); + }); + + suite('axismove', function () { + var name = 'trackpad'; + test('if we get axismove, emit ' + name + 'moved', function (done) { + var el = this.el; + var controllerComponent = el.components[controllerComponentName]; + var evt; + + el.sceneEl.systems['tracked-controls'].controllers = controllerComponent.controllersWhenPresent; + // do the check + controllerComponent.checkIfControllerPresent(); + // install event handler listening for thumbstickmoved + this.el.addEventListener(name + 'moved', function (evt) { + assert.equal(evt.detail.x, 0.1); + assert.equal(evt.detail.y, 0.2); + assert.ok(evt.detail); + done(); + }); + // emit axismove + evt = new CustomEvent('axismove', {'detail': {axis: [0.1, 0.2], changed: [true, false]}}); + this.el.dispatchEvent(evt); + }); + + test('if we get axismove with no changes, do not emit ' + name + 'moved', function (done) { + var el = this.el; + var controllerComponent = el.components[controllerComponentName]; + var evt; + + el.sceneEl.systems['tracked-controls'].controllers = controllerComponent.controllersWhenPresent; + // do the check + controllerComponent.checkIfControllerPresent(); + // install event handler listening for thumbstickmoved + this.el.addEventListener(name + 'moved', function (evt) { + assert.notOk(evt.detail); + }); + // emit axismove + evt = new CustomEvent('axismove', {'detail': {axis: [0.1, 0.2], changed: [false, false]}}); + this.el.dispatchEvent(evt); + // finish next tick + setTimeout(function () { done(); }, 0); + }); + }); + + suite('buttonchanged', function () { + var name = 'trigger'; + var id = 1; + test('if we get buttonchanged, emit ' + name + 'changed', function (done) { + var el = this.el; + var controllerComponent = el.components[controllerComponentName]; + var evt; + + el.sceneEl.systems['tracked-controls'].controllers = controllerComponent.controllersWhenPresent; + // do the check + controllerComponent.checkIfControllerPresent(); + // install event handler listening for triggerchanged + this.el.addEventListener(name + 'changed', function (evt) { + assert.ok(evt.detail); + done(); + }); + // emit buttonchanged + evt = new CustomEvent('buttonchanged', {'detail': {id: id, state: {value: 0.5, pressed: true, touched: true}}}); + this.el.dispatchEvent(evt); + }); + }); + + suite('gamepaddisconnected', function () { + // Due to an apparent bug in FF Nightly + // where only one gamepadconnected / disconnected event is fired, + // which makes it difficult to handle in individual controller entities, + // we no longer remove the controllersupdate listener as a result. + test('if we get gamepaddisconnected, check if present', function () { + var el = this.el; + var controllerComponent = el.components[controllerComponentName]; + var checkIfControllerPresentSpy = this.sinon.spy(controllerComponent, 'checkIfControllerPresent'); + // Because checkIfControllerPresent may be used in bound form, bind and reinstall. + controllerComponent.checkIfControllerPresent = controllerComponent.checkIfControllerPresent.bind(controllerComponent); + controllerComponent.pause(); + controllerComponent.play(); + + el.sceneEl.systems['tracked-controls'].controllers = []; + // reset everGotGamepadEvent so we don't think we've looked before + delete controllerComponent.everGotGamepadEvent; + // fire emulated gamepaddisconnected event + window.dispatchEvent(new Event('gamepaddisconnected')); + // check assertions + assert.ok(checkIfControllerPresentSpy.called); + }); + }); +});