From 9e8dfcd4439afea3326a2f6e75815a22b543c535 Mon Sep 17 00:00:00 2001 From: Kevin Ngo Date: Wed, 15 Feb 2017 20:07:14 -0800 Subject: [PATCH] unit test and minor fixes to tracked-controls (#2396) --- src/components/tracked-controls.js | 182 ++++++--- src/core/a-node.js | 3 +- tests/components/tracked-controls.test.js | 465 +++++++++++++++++----- 3 files changed, 476 insertions(+), 174 deletions(-) diff --git a/src/components/tracked-controls.js b/src/components/tracked-controls.js index 999e00e8e6e..918a9037469 100644 --- a/src/components/tracked-controls.js +++ b/src/components/tracked-controls.js @@ -3,11 +3,11 @@ var THREE = require('../lib/three'); /** * Tracked controls component. - * Interface with the gamepad API to handled tracked controllers. + * Wrap the gamepad API for pose and button states. * Select the appropriate controller and apply pose to the entity. - * Observe buttons state and emit appropriate events. + * Observe button states and emit appropriate events. * - * @property {number} controller - Index of the controller in array returned by Gamepad API. + * @property {number} controller - Index of controller in array returned by Gamepad API. * @property {string} id - Selected controller among those returned by Gamepad API. */ module.exports.Component = registerComponent('tracked-controls', { @@ -19,28 +19,41 @@ module.exports.Component = registerComponent('tracked-controls', { }, init: function () { + this.axis = [0, 0, 0]; this.buttonStates = {}; - this.previousAxis = []; this.previousControllerPosition = new THREE.Vector3(); - }, - - update: function () { - var controllers = this.system.controllers; - var data = this.data; - controllers = controllers.filter(hasIdOrPrefix); - // handId: 0 - right, 1 - left - this.controller = controllers[data.controller]; - function hasIdOrPrefix (controller) { return data.idPrefix ? controller.id.indexOf(data.idPrefix) === 0 : controller.id === data.id; } + this.updateGamepad(); }, tick: function (time, delta) { var mesh = this.el.getObject3D('mesh'); // Update mesh animations. if (mesh && mesh.update) { mesh.update(delta / 1000); } + this.updateGamepad(); this.updatePose(); this.updateButtons(); }, + /** + * Handle update to `id` or `idPrefix. + */ + updateGamepad: function () { + var controllers = this.system.controllers; + var data = this.data; + 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; + }); + + this.controller = matchingControllers[data.controller]; + }, + + /** + * Read pose from controller (from Gamepad API), apply transforms, apply to entity. + */ updatePose: (function () { var controllerEuler = new THREE.Euler(); var controllerPosition = new THREE.Vector3(); @@ -49,16 +62,19 @@ module.exports.Component = registerComponent('tracked-controls', { var dolly = new THREE.Object3D(); var standingMatrix = new THREE.Matrix4(); controllerEuler.order = 'YXZ'; + return function () { - var controller; - var pose; + var controller = this.controller; + var currentPosition; + var el = this.el; var orientation; + var pose; var position; - var el = this.el; var vrDisplay = this.system.vrDisplay; - this.update(); - controller = this.controller; + if (!controller) { return; } + + // Compose pose from Gamepad. pose = controller.pose; orientation = pose.orientation || [0, 0, 0, 1]; position = pose.position || [0, 0, 0]; @@ -66,22 +82,28 @@ module.exports.Component = registerComponent('tracked-controls', { dolly.quaternion.fromArray(orientation); dolly.position.fromArray(position); dolly.updateMatrix(); + + // Apply transforms. if (vrDisplay && vrDisplay.stageParameters) { standingMatrix.fromArray(vrDisplay.stageParameters.sittingToStandingTransform); dolly.applyMatrix(standingMatrix); } + + // Decompose. controllerEuler.setFromRotationMatrix(dolly.matrix); controllerPosition.setFromMatrixPosition(dolly.matrix); + + // Apply rotation (as absolute, with rotation offset). el.setAttribute('rotation', { x: THREE.Math.radToDeg(controllerEuler.x), y: THREE.Math.radToDeg(controllerEuler.y), z: THREE.Math.radToDeg(controllerEuler.z) + this.data.rotationOffset }); + // Apply position (as delta from previous Gamepad rotation). deltaControllerPosition.copy(controllerPosition).sub(this.previousControllerPosition); this.previousControllerPosition.copy(controllerPosition); - var currentPosition = el.getAttribute('position'); - + currentPosition = el.getAttribute('position'); el.setAttribute('position', { x: currentPosition.x + deltaControllerPosition.x, y: currentPosition.y + deltaControllerPosition.y, @@ -90,60 +112,88 @@ module.exports.Component = registerComponent('tracked-controls', { }; })(), + /** + * Handle button changes including axes, presses, touches, values. + */ updateButtons: function () { - var i; var buttonState; var controller = this.controller; - if (!this.controller) { return; } - for (i = 0; i < controller.buttons.length; ++i) { - buttonState = controller.buttons[i]; - this.handleButton(i, buttonState); + var id; + + if (!controller) { return; } + + // Check every button. + for (id = 0; id < controller.buttons.length; ++id) { + // Initialize button state. + if (!this.buttonStates[id]) { + this.buttonStates[id] = {pressed: false, touched: false, value: 0}; + } + + buttonState = controller.buttons[id]; + this.handleButton(id, buttonState); } - this.handleAxes(controller.axes); + // Check axes. + this.handleAxes(); + }, + + /** + * Handle presses and touches for a single button. + * + * @param {number} id - Index of button in Gamepad button array. + * @param {number} buttonState - Value of button state from 0 to 1. + * @returns {boolean} Whether button has changed in any way. + */ + handleButton: function (id, buttonState) { + var changed = this.handlePress(id, buttonState) || + this.handleTouch(id, buttonState) || + this.handleValue(id, buttonState); + if (!changed) { return false; } + this.el.emit('buttonchanged', {id: id, state: buttonState}); + return true; }, - handleAxes: function (controllerAxes) { - var previousAxis = this.previousAxis; + /** + * An axis is an array of values from -1 (up, left) to 1 (down, right). + * Compare each component of the axis to the previous value to determine change. + * + * @returns {boolean} Whether axes changed. + */ + handleAxes: function () { var changed = false; + var controllerAxes = this.controller.axes; var i; + var previousAxis = this.axis; + + // Check if axis changed. for (i = 0; i < controllerAxes.length; ++i) { if (previousAxis[i] !== controllerAxes[i]) { changed = true; break; } } - if (!changed) { return; } - this.previousAxis = controllerAxes.slice(); - this.el.emit('axismove', {axis: this.previousAxis}); - }, + if (!changed) { return false; } - handleButton: function (id, buttonState) { - var changed = false; - changed = changed || this.handlePress(id, buttonState); - changed = changed || this.handleTouch(id, buttonState); - changed = changed || this.handleValue(id, buttonState); - if (!changed) { return; } - this.el.emit('buttonchanged', {id: id, state: buttonState}); + this.axis = controllerAxes.slice(); + this.el.emit('axismove', {axis: this.axis}); + return true; }, /** * Determine whether a button press has occured and emit events as appropriate. * - * @param {string} id - id of the button to check. - * @param {object} buttonState - state of the button to check. - * @returns {boolean} true if button press state changed, false otherwise. + * @param {string} id - ID of the button to check. + * @param {object} buttonState - State of the button to check. + * @returns {boolean} Whether button press state changed. */ handlePress: function (id, buttonState) { - var buttonStates = this.buttonStates; var evtName; - var previousButtonState = buttonStates[id] = buttonStates[id] || {}; + var previousButtonState = this.buttonStates[id]; + + // Not changed. if (buttonState.pressed === previousButtonState.pressed) { return false; } - if (buttonState.pressed) { - evtName = 'down'; - } else { - evtName = 'up'; - } - this.el.emit('button' + evtName, {id: id}); + + evtName = buttonState.pressed ? 'down' : 'up'; + this.el.emit('button' + evtName, {id: id, state: buttonState}); previousButtonState.pressed = buttonState.pressed; return true; }, @@ -151,36 +201,36 @@ module.exports.Component = registerComponent('tracked-controls', { /** * Determine whether a button touch has occured and emit events as appropriate. * - * @param {string} id - id of the button to check. - * @param {object} buttonState - state of the button to check. - * @returns {boolean} true if button touch state changed, false otherwise. + * @param {string} id - ID of the button to check. + * @param {object} buttonState - State of the button to check. + * @returns {boolean} Whether button touch state changed. */ handleTouch: function (id, buttonState) { - var buttonStates = this.buttonStates; var evtName; - var previousButtonState = buttonStates[id] = buttonStates[id] || {}; + var previousButtonState = this.buttonStates[id]; + + // Not changed. if (buttonState.touched === previousButtonState.touched) { return false; } - if (buttonState.touched) { - evtName = 'start'; - } else { - evtName = 'end'; - } + + evtName = buttonState.touched ? 'start' : 'end'; + this.el.emit('touch' + evtName, {id: id, state: buttonState}); previousButtonState.touched = buttonState.touched; - this.el.emit('touch' + evtName, {id: id, state: previousButtonState}); return true; }, /** * Determine whether a button value has changed. * - * @param {string} id - id of the button to check. - * @param {object} buttonState - state of the button to check. - * @returns {boolean} true if button value changed, false otherwise. + * @param {string} id - Id of the button to check. + * @param {object} buttonState - State of the button to check. + * @returns {boolean} Whether button value changed. */ handleValue: function (id, buttonState) { - var buttonStates = this.buttonStates; - var previousButtonState = buttonStates[id] = buttonStates[id] || {}; + var previousButtonState = this.buttonStates[id]; + + // Not changed. if (buttonState.value === previousButtonState.value) { return false; } + previousButtonState.value = buttonState.value; return true; } diff --git a/src/core/a-node.js b/src/core/a-node.js index a5ef69cb8da..9cb4e3e7f9f 100644 --- a/src/core/a-node.js +++ b/src/core/a-node.js @@ -221,7 +221,8 @@ module.exports = registerElement('a-node', { return name.split(' ').map(function (eventName) { return utils.fireEvent(self, eventName, data); }); - } + }, + writable: window.debug }, /** diff --git a/tests/components/tracked-controls.test.js b/tests/components/tracked-controls.test.js index a8c7666a0fa..ac426dd8c80 100644 --- a/tests/components/tracked-controls.test.js +++ b/tests/components/tracked-controls.test.js @@ -1,158 +1,409 @@ -/* global assert, process, setup, suite, test */ -var entityFactory = require('../helpers').entityFactory; +/* global assert, process, setup, suite, test, THREE */ +const entityFactory = require('../helpers').entityFactory; + +const PI = Math.PI; suite('tracked-controls', function () { + var component; + var controller; + var el; + var system; + setup(function (done) { - var el = this.el = entityFactory(); + el = entityFactory(); el.setAttribute('position', ''); el.setAttribute('tracked-controls', ''); el.addEventListener('loaded', function () { - var trackedControls = el.components['tracked-controls']; - trackedControls.system.controllers = [ - { id: 'OpenVR Gamepad', pose: { position: [0, 0, 0] }, buttons: [], axes: [] } - ]; + component = el.components['tracked-controls']; + system = component.system; + controller = { + id: 'OpenVR Gamepad', + pose: { + position: [0, 0, 0], + orientation: [0, 0, 0, 1] + }, + buttons: [ + {pressed: false, touched: false, value: 0}, + {pressed: false, touched: false, value: 0} + ], + axes: [0, 0, 0] + }; + system.controllers = [controller]; + el.setAttribute('tracked-controls', 'id', 'OpenVR Gamepad'); done(); }); }); - suite('id', function () { - test('do not match controller by default', function () { - var el = this.el; - var trackedControls = el.components['tracked-controls']; - assert.notOk(trackedControls.controller); + suite('updateGamepad', function () { + test('matches controller with same id', function () { + assert.notOk(component.controller); + el.setAttribute('tracked-controls', 'id', 'OpenVR Gamepad'); + component.tick(); + assert.equal(component.controller, controller); }); - test('do not match controller with different id', function () { - var el = this.el; - var trackedControls = el.components['tracked-controls']; - assert.notEqual(trackedControls.data.id, trackedControls.system.controllers[0].id); - assert.notOk(trackedControls.controller); + test('matches controller with prefix', function () { + assert.notOk(component.controller); + el.setAttribute('tracked-controls', 'idPrefix', 'OpenVR'); + component.tick(); + assert.equal(component.controller, controller); }); - test('match controller with same id', function () { - var el = this.el; - var trackedControls = el.components['tracked-controls']; - assert.notOk(trackedControls.controller); - el.setAttribute('tracked-controls', 'id', trackedControls.system.controllers[0].id); - assert.equal(trackedControls.controller, trackedControls.system.controllers[0]); + test('does not match controller by default', function () { + assert.notOk(component.controller); + el.setAttribute('tracked-controls', {}, true); + component.tick(); + assert.notOk(component.controller); }); - }); - suite('tick', function () { - setup(function (done) { - var el = this.el; - var trackedControls = el.components['tracked-controls']; - el.setAttribute('tracked-controls', 'id', trackedControls.system.controllers[0].id); - assert.equal(trackedControls.controller, trackedControls.system.controllers[0]); - done(); + test('does not match controller with different id', function () { + assert.notOk(component.controller); + el.setAttribute('tracked-controls', 'id', 'foo'); + component.tick(); + assert.notOk(component.controller); }); - test('pose and buttons update if mesh is not defined', function () { - var el = this.el; - var trackedControls = el.components['tracked-controls']; - var updateButtonsSpy = this.sinon.spy(trackedControls, 'updateButtons'); - var updatePoseSpy = this.sinon.spy(trackedControls, 'updatePose'); - assert.equal(el.getObject3D('mesh'), undefined); - trackedControls.tick(); + test('does not match controller with different prefix', function () { + assert.notOk(component.controller); + el.setAttribute('tracked-controls', 'idPrefix', 'foo'); + component.tick(); + assert.notOk(component.controller); + }); + }); + + suite('tick', function () { + test('updates pose and buttons even if mesh is not defined', function () { + var updateButtonsSpy = this.sinon.spy(component, 'updateButtons'); + var updatePoseSpy = this.sinon.spy(component, 'updatePose'); + assert.notOk(el.getObject3D('mesh')); + component.tick(); assert.ok(updatePoseSpy.called); assert.ok(updateButtonsSpy.called); }); }); - suite('position', function () { - setup(function (done) { - var el = this.el; - var trackedControls = el.components['tracked-controls']; - el.setAttribute('tracked-controls', 'id', trackedControls.system.controllers[0].id); - assert.equal(trackedControls.controller, trackedControls.system.controllers[0]); - done(); + suite('updatePose (position)', function () { + test('defaults position to zero vector', function () { + controller.pose.position = [0, 0, 0]; + el.setAttribute('position', '0 0 0'); + component.tick(); + assertVec3(component.previousControllerPosition, [0, 0, 0]); + assertVec3(el.getAttribute('position'), [0, 0, 0]); }); - test('defaults position and pose to [0 0 0]', function () { - var el = this.el; - var trackedControls = el.components['tracked-controls']; + test('applies position from gamepad pose', function () { + controller.pose.position = [1, 2, 3]; + component.tick(); + assertVec3(component.previousControllerPosition, [1, 2, 3]); + assertVec3(el.getAttribute('position'), [1, 2, 3]); + }); - trackedControls.system.controllers[0].pose.position = [0, 0, 0]; - el.setAttribute('position', '0 0 0'); - trackedControls.tick(); + test('applies position using deltas', function () { + controller.pose.position = [0, 0, 0]; + el.setAttribute('position', '1 2 3'); + component.tick(); + assertVec3(el.getAttribute('position'), [1, 2, 3]); + + assertVec3(component.previousControllerPosition, [0, 0, 0]); + controller.pose.position = [1, 1, 1]; + component.tick(); + assertVec3(el.getAttribute('position'), [2, 3, 4]); + }); - var previousControllerPos = trackedControls.previousControllerPosition; - assert.equal(previousControllerPos.x, 0); - assert.equal(previousControllerPos.y, 0); - assert.equal(previousControllerPos.z, 0); - assert.deepEqual(el.getAttribute('position'), {x: 0, y: 0, z: 0}); + test('applies position using deltas with non-zero pose', function () { + assertVec3(component.previousControllerPosition, [0, 0, 0]); + controller.pose.position = [4, 5, -6]; + el.setAttribute('position', '-1 2 -3'); + component.tick(); + // A-Frame position + (Gamepad position - previous Gamepad position). + // [-1, 2, -3] + ([4, 5, -6] - [0, 0, 0]). + assertVec3(el.getAttribute('position'), [3, 7, -9]); + }); + + test('handles unchanged Gamepad position', function () { + controller.pose.position = [4, 5, -6]; + component.tick(); + el.setAttribute('position', '-1 2 -3'); + component.tick(); + assertVec3(component.previousControllerPosition, [4, 5, -6]); + // A-Frame position + (Gamepad position - previous Gamepad position). + // [-1, 2, -3] + ([4, 5, -6] - [4, 5, -6]). + assertVec3(el.getAttribute('position'), [-1, 2, -3]); }); - test('position: [0 0 0] and pose: [1 2 3]', function () { - var el = this.el; - var trackedControls = el.components['tracked-controls']; - var previousControllerPos = trackedControls.previousControllerPosition; + test('applies new Gamepad position to manually positioned entity', function () { + controller.pose.position = [1, 2, 3]; + component.tick(); + assertVec3(component.previousControllerPosition, [1, 2, 3]); + assertVec3(el.getAttribute('position'), [1, 2, 3]); - trackedControls.system.controllers[0].pose.position = [1, 2, 3]; - trackedControls.tick(); + el.setAttribute('position', '10 10 10'); + controller.pose.position = [2, 4, 6]; + component.tick(); + assertVec3(component.previousControllerPosition, [2, 4, 6]); - assert.equal(previousControllerPos.x, 1); - assert.equal(previousControllerPos.y, 2); - assert.equal(previousControllerPos.z, 3); - assert.deepEqual(el.getAttribute('position'), {x: 1, y: 2, z: 3}); + // A-Frame position + (Gamepad position - previous Gamepad position). + // [10, 10, 10] + ([2, 4, 6] - [1, 2, 3]). + assertVec3(el.getAttribute('position'), [11, 12, 13]); }); + }); - test('position: [-1 2 -3] and pose: [0 0 0]', function () { - var el = this.el; - var trackedControls = el.components['tracked-controls']; + suite('updatePose (rotation)', function () { + test('defaults rotation to zero', function () { + controller.pose.orientation = toQuaternion(0, 0, 0); + el.setAttribute('rotation', '0 0 0'); + component.tick(); + assert.shallowDeepEqual(el.object3D.quaternion.toArray(), [0, 0, 0, 1]); + }); - trackedControls.system.controllers[0].pose.position = [0, 0, 0]; - el.setAttribute('position', '-1 2 -3'); - trackedControls.tick(); - assert.deepEqual(el.getAttribute('position'), {x: -1, y: 2, z: -3}); + test('applies rotation from Gamepad pose', function () { + controller.pose.orientation = toQuaternion(PI, PI / 2, PI / 3); + component.tick(); + assertQuaternion(el.object3D.quaternion, controller.pose.orientation); }); - test('position: [-1 2 -3] and pose: [4 5 -6]', function () { - var el = this.el; - var trackedControls = el.components['tracked-controls']; + test('applies rotation absolutely', function () { + controller.pose.orientation = toQuaternion(PI, PI / 2, PI / 3); + el.setAttribute('rotation', '180 90 60'); + component.tick(); + assertQuaternion(el.object3D.quaternion, controller.pose.orientation); - trackedControls.system.controllers[0].pose.position = [4, 5, -6]; - el.setAttribute('position', '-1 2 -3'); - trackedControls.tick(); - assert.deepEqual(el.getAttribute('position'), {x: 3, y: 7, z: -9}); + controller.pose.orientation = toQuaternion(PI / 2, PI / 3, PI / 4); + component.tick(); + assertQuaternion(el.object3D.quaternion, controller.pose.orientation); }); - test('position: [-1 2 -3] and current and previous pose: [4 5 -6]', function () { - var el = this.el; - var trackedControls = el.components['tracked-controls']; + test('handles unchanged Gamepad rotation', function () { + controller.pose.orientation = toQuaternion(PI, PI / 2, PI / 3); + component.tick(); + component.tick(); + assertQuaternion(el.object3D.quaternion, controller.pose.orientation); + }); - trackedControls.system.controllers[0].pose.position = [4, 5, -6]; - trackedControls.tick(); - // Previous pose = [4 5 -6] diff with current pose = 0 + test('applies rotation Z-offset', function () { + assertVec3(el.getAttribute('rotation'), [0, 0, 0]); + el.setAttribute('tracked-controls', 'rotationOffset', 10); + component.tick(); + assertVec3(el.getAttribute('rotation'), [0, 0, 10]); + }); + }); - el.setAttribute('position', '-1 2 -3'); - trackedControls.tick(); + suite('handleAxes', function () { + test('does not emit on initial state', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + component.tick(); + assert.notOk(component.handleAxes()); + assert.notOk(emitSpy.called); + }); - assert.deepEqual(el.getAttribute('position'), {x: -1, y: 2, z: -3}); + test('emits axismove on first touch', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + controller.axes = [0.5, 0.5, 0.5]; + assert.deepEqual(component.axis, [0, 0, 0]); + component.tick(); + assert.deepEqual(component.axis, [0.5, 0.5, 0.5]); + assert.equal(emitSpy.getCalls()[0].args[0], 'axismove'); + assert.deepEqual(emitSpy.getCalls()[0].args[1].axis, [0.5, 0.5, 0.5]); }); - test('position: [-1 2 -3] and current [7 -8 9] and previous pose: [4 5 -6]', function () { - var el = this.el; - var trackedControls = el.components['tracked-controls']; + test('emits axismove if axis changed', function () { + controller.axes = [0.5, 0.5, 0.5]; + component.tick(); + assert.deepEqual(component.axis, [0.5, 0.5, 0.5]); - trackedControls.system.controllers[0].pose.position = [4, 5, -6]; - console.log('tick1'); - trackedControls.tick(); + const emitSpy = this.sinon.spy(el, 'emit'); + controller.axes = [1, 1, 1]; + component.tick(); + const emitCall = emitSpy.getCalls()[0]; + assert.equal(emitCall.args[0], 'axismove'); + assert.deepEqual(emitCall.args[1].axis, [1, 1, 1]); + }); + }); - var previousControllerPos = trackedControls.previousControllerPosition; - assert.equal(previousControllerPos.x, 4); - assert.equal(previousControllerPos.y, 5); - assert.equal(previousControllerPos.z, -6); + suite('handleButton', function () { + test('does not emit if button not pressed', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + component.tick(); + assert.notOk(emitSpy.called); + }); - trackedControls.system.controllers[0].pose.position = [7, -8, 9]; - // diff prev & current pos = [3 -13 15] - el.setAttribute('position', '-1 2 -3'); - trackedControls.tick(); + test('emits buttonchanged if button pressed', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + controller.buttons[0].pressed = true; + component.tick(); + + const emitChangedCalls = emitSpy.getCalls().filter( + call => call.args[0] === 'buttonchanged'); + assert.equal(emitChangedCalls.length, 1); + + const emitCall = emitChangedCalls[0]; + assert.equal(emitCall.args[0], 'buttonchanged'); + assert.deepEqual(emitCall.args[1].id, 0); + assert.deepEqual(emitCall.args[1].state, controller.buttons[0]); + }); + + test('emits buttonchanged if button touched', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + controller.buttons[0].touched = true; + component.tick(); + + const emitChangedCalls = emitSpy.getCalls().filter( + call => call.args[0] === 'buttonchanged'); + assert.equal(emitChangedCalls.length, 1); - assert.equal(previousControllerPos.x, 7); - assert.equal(previousControllerPos.y, -8); - assert.equal(previousControllerPos.z, 9); - assert.deepEqual(el.getAttribute('position'), {x: 2, y: -11, z: 12}); + const emitCall = emitChangedCalls[0]; + assert.equal(emitCall.args[0], 'buttonchanged'); + assert.deepEqual(emitCall.args[1].id, 0); + assert.deepEqual(emitCall.args[1].state, controller.buttons[0]); + }); + }); + + suite('handlePress', function () { + test('does not emit anything if button not pressed', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + component.tick(); + assert.notOk(emitSpy.called); + assert.notOk(component.handlePress(0, {pressed: false, touched: false, value: 0})); + }); + + test('emits buttondown if button pressed', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + controller.buttons[0].pressed = true; + component.tick(); + + const emitDownCalls = emitSpy.getCalls().filter( + call => call.args[0] === 'buttondown'); + assert.equal(emitDownCalls.length, 1); + + const emitCall = emitDownCalls[0]; + assert.equal(emitCall.args[0], 'buttondown'); + assert.deepEqual(emitCall.args[1].id, 0); + assert.deepEqual(emitCall.args[1].state, controller.buttons[0]); + }); + + test('emits buttonup if button released', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + component.buttonStates[1] = {pressed: true, touched: false, value: 1}; + controller.buttons[1].pressed = false; + controller.buttons[1].value = 0; + component.tick(); + + const emitUpCalls = emitSpy.getCalls().filter( + call => call.args[0] === 'buttonup'); + assert.equal(emitUpCalls.length, 1); + + const emitCall = emitUpCalls[0]; + assert.equal(emitCall.args[0], 'buttonup'); + assert.deepEqual(emitCall.args[1].id, 1); + assert.deepEqual(emitCall.args[1].state, controller.buttons[1]); + }); + + test('does not emit buttonup if button pressed', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + controller.buttons[0].pressed = true; + component.tick(); + const emitUpCalls = emitSpy.getCalls().filter( + call => call.args[0] === 'buttonup'); + assert.notOk(emitUpCalls.length); + }); + + test('does not emit buttondown if button released', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + component.buttonStates[1] = {pressed: true, touched: false, value: 1}; + controller.buttons[1].pressed = false; + controller.buttons[1].value = 0; + component.tick(); + + const emitDownCalls = emitSpy.getCalls().filter( + call => call.args[0] === 'buttondown'); + assert.notOk(emitDownCalls.length); + }); + }); + + suite('handleTouch', function () { + test('does not do anything if button not touched', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + component.tick(); + assert.notOk(emitSpy.called); + assert.notOk(component.handleTouch(0, {pressed: false, touched: false, value: 0})); + }); + + test('emits touchstart if button touched', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + controller.buttons[0].touched = true; + component.tick(); + + const emitStartCalls = emitSpy.getCalls().filter( + call => call.args[0] === 'touchstart'); + assert.equal(emitStartCalls.length, 1); + + const emitCall = emitStartCalls[0]; + assert.equal(emitCall.args[0], 'touchstart'); + assert.deepEqual(emitCall.args[1].id, 0); + assert.deepEqual(emitCall.args[1].state, controller.buttons[0]); + }); + + test('emits touchend if button no longer touched', function () { + const emitSpy = this.sinon.spy(el, 'emit'); + component.buttonStates[1] = {pressed: false, touched: true, value: 1}; + controller.buttons[1].touched = false; + controller.buttons[1].value = 0; + component.tick(); + + const emitEndCalls = emitSpy.getCalls().filter( + call => call.args[0] === 'touchend'); + assert.equal(emitEndCalls.length, 1); + + const emitCall = emitEndCalls[0]; + assert.equal(emitCall.args[0], 'touchend'); + assert.deepEqual(emitCall.args[1].id, 1); + assert.deepEqual(emitCall.args[1].state, controller.buttons[1]); + }); + }); + + suite('handleValue', function () { + test('stores default button value in button states', function () { + component.tick(); + assert.equal(component.buttonStates[0].value, 0); + assert.equal(component.buttonStates[1].value, 0); + }); + + test('stores changed button value in button states', function () { + controller.buttons[0].value = 0.25; + controller.buttons[1].value = 0.75; + component.tick(); + assert.equal(component.buttonStates[0].value, 0.25); + assert.equal(component.buttonStates[1].value, 0.75); }); }); }); + +function assertVec3 (vec3, arr) { + assert.equal(vec3.x, arr[0]); + assert.equal(vec3.y, arr[1]); + assert.equal(vec3.z, arr[2]); +} + +function assertQuaternion (quaternion, arr) { + quaternion = quaternion.toArray(); + // Compute negative quaternion if necessary. Equivalent rotations. + // eslint-disable-next-line eqeqeq + if (quaternion[0].toFixed(5) * -1 == arr[0].toFixed(5)) { + quaternion = quaternion.map(n => -1 * n); + } + // Round. + quaternion = quaternion.map(n => n.toFixed(5)); + arr = arr.map(n => n.toFixed(5)); + + assert.shallowDeepEqual(quaternion, arr); +} + +function toQuaternion (x, y, z) { + var euler = new THREE.Euler(); + var quaternion = new THREE.Quaternion(); + return (function () { + euler.fromArray([x, y, z]); + quaternion.setFromEuler(euler); + return quaternion.toArray(); + })(); +}