Newer
Older
reroad-test / 2020-fuga / aframe-master / tests / components / tracked-controls-webvr.test.js
@fuga sakurai fuga sakurai on 4 Nov 2020 22 KB a-フレームを追加した
/* global assert, process, setup, sinon, suite, teardown, test, THREE */
const entityFactory = require('../helpers').entityFactory;

const PI = Math.PI;

suite('tracked-controls-webvr', function () {
  var component;
  var controller;
  var el;
  var system;
  var standingMatrix = new THREE.Matrix4();

  setup(function (done) {
    standingMatrix.identity();
    el = entityFactory();
    setTimeout(() => {
      el.setAttribute('position', '');
      el.setAttribute('tracked-controls', '');
      el.sceneEl.addEventListener('loaded', function () {
        el.parentNode.renderer.xr.getStandingMatrix = function () {
          return standingMatrix;
        };
        component = el.components['tracked-controls-webvr'];
        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-webvr', 'id', 'OpenVR Gamepad');
        done();
      });
    });
  });

  suite('updateGamepad', function () {
    test('matches controller with same id', function () {
      assert.strictEqual(component.controller, undefined);
      el.setAttribute('tracked-controls-webvr', 'id', 'OpenVR Gamepad');
      component.tick();
      assert.equal(component.controller, controller);
    });

    test('matches controller with prefix', function () {
      assert.strictEqual(component.controller, undefined);
      el.setAttribute('tracked-controls-webvr', 'idPrefix', 'OpenVR');
      component.tick();
      assert.equal(component.controller, controller);
    });

    test('does not match controller by default', function () {
      assert.strictEqual(component.controller, undefined);
      el.setAttribute('tracked-controls-webvr', {}, true);
      component.tick();
      assert.strictEqual(component.controller, undefined);
    });

    test('does not match controller with different id', function () {
      assert.strictEqual(component.controller, undefined);
      el.setAttribute('tracked-controls-webvr', 'id', 'foo');
      component.tick();
      assert.strictEqual(component.controller, undefined);
    });

    test('does not match controller with different prefix', function () {
      assert.strictEqual(component.controller, undefined);
      el.setAttribute('tracked-controls-webvr', 'idPrefix', 'foo');
      component.tick();
      assert.strictEqual(component.controller, undefined);
    });

    test('set controller to undefined if controller not found', function () {
      assert.strictEqual(component.controller, undefined);
      el.setAttribute('tracked-controls-webvr', 'id', 'OpenVR Gamepad');
      component.tick();
      assert.equal(component.controller, controller);
      system.controllers = [];
      component.tick();
      assert.strictEqual(component.controller, undefined);
    });
  });

  suite('tick', function () {
    test('updates pose and buttons even if mesh is not defined', function () {
      var updateButtonsSpy = sinon.spy(component, 'updateButtons');
      var updatePoseSpy = sinon.spy(component, 'updatePose');
      assert.notOk(el.getObject3D('mesh'));
      component.tick();
      sinon.assert.calledOnce(updatePoseSpy);
      sinon.assert.calledOnce(updateButtonsSpy);
    });
  });

  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(el.getAttribute('position'), [0, 0, 0]);
    });

    test('applies position from gamepad pose', function () {
      controller.pose.position = [1, 2, 3];
      el.sceneEl.systems['tracked-controls-webvr'].vrDisplay = true;
      component.tick();
      assertVec3(el.getAttribute('position'), [1, 2, 3]);
    });

    test('handles unchanged Gamepad position', function () {
      controller.pose.position = [4, 5, -6];
      el.sceneEl.systems['tracked-controls-webvr'].vrDisplay = true;
      component.tick();
      el.setAttribute('position', '-1 2 -3');
      component.tick();
      assertVec3(el.getAttribute('position'), [4, 5, -6]);
    });

    test('applies new Gamepad position to manually positioned entity', function () {
      controller.pose.position = [1, 2, 3];
      el.sceneEl.systems['tracked-controls-webvr'].vrDisplay = true;
      component.tick();
      assertVec3(el.getAttribute('position'), [1, 2, 3]);

      el.setAttribute('position', '10 10 10');
      controller.pose.position = [2, 4, 6];
      component.tick();
      assertVec3(el.getAttribute('position'), [2, 4, 6]);
    });

    test('applies standing matrix transform', function () {
      standingMatrix.makeTranslation(1, 0.5, -3);
      controller.pose.position = [1, 2, 3];
      el.sceneEl.systems['tracked-controls-webvr'].vrDisplay = true;
      component.tick();
      assertVec3(el.getAttribute('position'), [2, 2.5, 0]);
    });

    test('does not apply standing matrix transform for 3DoF', function () {
      standingMatrix.makeTranslation(1, 0.5, -3);
      controller.pose.position = null;
      el.setAttribute('tracked-controls-webvr', 'armModel', true);
      el.sceneEl.systems['tracked-controls-webvr'].vrDisplay = true;
      component.tick();
      // assert position after default camera position and arm model are applied
      assertVec3CloseTo(el.getAttribute('position'), [0.28, 1.12, -0.32], 0.01);
    });
  });

  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]);
    });

    test('applies rotation from Gamepad pose', function () {
      controller.pose.orientation = toQuaternion(PI, PI / 2, PI / 3);
      el.sceneEl.systems['tracked-controls-webvr'].vrDisplay = true;
      component.tick();
      assertQuaternion(el.object3D.quaternion, controller.pose.orientation);
    });

    test('applies rotation absolutely', function () {
      controller.pose.orientation = toQuaternion(PI, PI / 2, PI / 3);
      el.sceneEl.systems['tracked-controls-webvr'].vrDisplay = true;
      el.setAttribute('rotation', '180 90 60');
      component.tick();
      assertQuaternion(el.object3D.quaternion, controller.pose.orientation);

      controller.pose.orientation = toQuaternion(PI / 2, PI / 3, PI / 4);
      component.tick();
      assertQuaternion(el.object3D.quaternion, controller.pose.orientation);
    });

    test('handles unchanged Gamepad rotation', function () {
      controller.pose.orientation = toQuaternion(PI, PI / 2, PI / 3);
      el.sceneEl.systems['tracked-controls-webvr'].vrDisplay = true;
      component.tick();
      assertQuaternion(el.object3D.quaternion, controller.pose.orientation);
    });

    test('applies orientation offset', function () {
      el.setAttribute('tracked-controls-webvr', 'orientationOffset', {x: 3, y: 4, z: 5});
      component.tick();
      var rotation = el.getAttribute('rotation');
      rotation.x = Math.round(rotation.x);
      rotation.y = Math.round(rotation.y);
      rotation.z = Math.round(rotation.z);
      assertVec3(rotation, [3, 4, 5]);
    });
  });

  suite('handleAxes', function () {
    test('does not emit on initial state', function () {
      const emitSpy = sinon.spy(el, 'emit');
      component.tick();
      assert.notOk(component.handleAxes());
      sinon.assert.notCalled(emitSpy);
    });

    test('emits axismove on first touch', function () {
      const emitSpy = 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]);
      assert.deepEqual(emitSpy.getCalls()[0].args[1].changed, [true, true, true]);
    });

    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]);

      const emitSpy = 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]);
      assert.deepEqual(emitCall.args[1].changed, [true, true, true]);
    });

    test('emits axismove with correct axis changed flags', function () {
      controller.axes = [0.5, 0.5, 0.5];
      component.tick();
      assert.deepEqual(component.axis, [0.5, 0.5, 0.5]);

      const emitSpy = sinon.spy(el, 'emit');
      controller.axes = [1, 0.5, 0.5];
      component.tick();
      const emitCall = emitSpy.getCalls()[0];
      assert.equal(emitCall.args[0], 'axismove');
      assert.deepEqual(emitCall.args[1].axis, [1, 0.5, 0.5]);
      assert.deepEqual(emitCall.args[1].changed, [true, false, false]);
    });
  });

  suite('handleButton', function () {
    test('does not emit if button not pressed', function () {
      const emitSpy = sinon.spy(el, 'emit');
      component.tick();
      sinon.assert.notCalled(emitSpy);
    });

    test('emits buttonchanged if button pressed', function () {
      const emitSpy = 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);

      assertButtonEvent(emitChangedCalls[0], 'buttonchanged', 0, controller.buttons[0]);
    });

    test('emits buttonchanged if button touched', function () {
      const emitSpy = 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);

      assertButtonEvent(emitChangedCalls[0], 'buttonchanged', 0, controller.buttons[0]);
    });

    test('emits buttonchanged if value changed', function () {
      const emitSpy = sinon.spy(el, 'emit');
      controller.buttons[0].value = 0.5;
      component.tick();

      const emitChangedCalls = emitSpy.getCalls().filter(
        call => call.args[0] === 'buttonchanged');
      assert.equal(emitChangedCalls.length, 1);

      assertButtonEvent(emitChangedCalls[0], 'buttonchanged', 0, controller.buttons[0]);
    });

    test('emits independent streams for buttondown, touchstart and buttonchanged', function () {
      const emitSpy = sinon.spy(el, 'emit');
      controller.buttons[0].pressed = true;
      controller.buttons[0].touched = true;
      component.tick();

      // We should emit button, touch and changed calls.
      assert.equal(emitSpy.getCalls().length, 3);
      const emitButtonCalls = emitSpy.getCalls().filter(
        call => call.args[0] === 'buttondown');
      assert.equal(emitButtonCalls.length, 1);

      const emitTouchCalls = emitSpy.getCalls().filter(
        call => call.args[0] === 'touchstart');
      assert.equal(emitTouchCalls.length, 1);

      const emitChangedCalls = emitSpy.getCalls().filter(
        call => call.args[0] === 'buttonchanged');
      assert.equal(emitChangedCalls.length, 1);

      assertButtonEvent(emitButtonCalls[0], 'buttondown', 0, controller.buttons[0]);
      assertButtonEvent(emitTouchCalls[0], 'touchstart', 0, controller.buttons[0]);
      assertButtonEvent(emitChangedCalls[0], 'buttonchanged', 0, controller.buttons[0]);
    });

    test('emits independent streams for buttonup, touchend and buttonchanged', function () {
      const emitSpy = sinon.spy(el, 'emit');
      component.buttonStates[0] = {pressed: true, touched: true, value: 1};
      controller.buttons[0].pressed = false;
      controller.buttons[0].touched = false;
      controller.buttons[0].value = 0;
      component.tick();

      // Filter down to just our expected events for verification.
      const emitButtonCalls = emitSpy.getCalls().filter(call => call.args[0] === 'buttonup');
      assert.equal(emitButtonCalls.length, 1);

      const emitTouchCalls = emitSpy.getCalls().filter(call => call.args[0] === 'touchend');
      assert.equal(emitTouchCalls.length, 1);

      const emitChangedCalls = emitSpy.getCalls().filter(call => call.args[0] === 'buttonchanged');
      assert.equal(emitChangedCalls.length, 1);

      // Verify each of the 3 events has the correct event name and state.
      assertButtonEvent(emitButtonCalls[0], 'buttonup', 0, controller.buttons[0]);
      assertButtonEvent(emitTouchCalls[0], 'touchend', 0, controller.buttons[0]);
      assertButtonEvent(emitChangedCalls[0], 'buttonchanged', 0, controller.buttons[0]);
    });

    test('emits correct event stream for spaced out interaction.', function () {
      // First round, verify we only see a touchstart and buttonchanged
      let emitSpy = sinon.spy(el, 'emit');
      controller.buttons[0].touched = true;
      component.tick();

      assertEventStream(emitSpy.getCalls(), ['buttonchanged', 'touchstart'], ['buttondown', 'buttonup', 'touchend']);
      emitSpy.restore();

      // Second round, verify we only see a buttondown and buttonchanged since pressed state isn't changing.
      emitSpy = sinon.spy(el, 'emit');
      controller.buttons[0].pressed = true;
      component.tick();

      assertEventStream(emitSpy.getCalls(), ['buttonchanged', 'buttondown'], ['buttonup', 'touchend', 'touchstart']);
      emitSpy.restore();

      // Third round, verify we only see a buttonup and button changed when we release the button.
      emitSpy = sinon.spy(el, 'emit');
      controller.buttons[0].pressed = false;
      component.tick();

      assertEventStream(emitSpy.getCalls(), ['buttonchanged', 'buttonup'], ['buttondown', 'touchend', 'touchstart']);
      emitSpy.restore();

      // Fourth round, verify we only see a touchend and button changed when we lift our touch.
      emitSpy = sinon.spy(el, 'emit');
      controller.buttons[0].touched = false;
      component.tick();

      assertEventStream(emitSpy.getCalls(), ['buttonchanged', 'touchend'], ['buttondown', 'buttonup', 'touchstart']);
      emitSpy.restore();
    });

    test('emits correct event states on a fast click', function () {
      // First round, verify we see all activation events
      let emitSpy = sinon.spy(el, 'emit');
      controller.buttons[0].pressed = true;
      controller.buttons[0].touched = true;
      controller.buttons[0].value = 1;
      component.tick();

      assertEventStream(emitSpy.getCalls(), ['buttonchanged', 'buttondown', 'touchstart'], ['buttonup', 'touchend']);
      emitSpy.restore();

      // Second round, verify we see all deactivation events
      emitSpy = sinon.spy(el, 'emit');
      controller.buttons[0].pressed = false;
      controller.buttons[0].touched = false;
      controller.buttons[0].value = 0;
      component.tick();

      assertEventStream(emitSpy.getCalls(), ['buttonchanged', 'buttonup', 'touchend'], ['buttondown', 'touchstart']);
      emitSpy.restore();

      // Finally verify there are no events backed up because of bad carryover state.
      emitSpy = sinon.spy(el, 'emit');
      component.tick();
      sinon.assert.notCalled(emitSpy);
    });
  });

  suite('handlePress', function () {
    test('does not emit anything if button not pressed', function () {
      const emitSpy = sinon.spy(el, 'emit');
      component.tick();
      sinon.assert.notCalled(emitSpy);
      assert.notOk(component.handlePress(0, {pressed: false, touched: false, value: 0}));
    });

    test('emits buttondown if button pressed', function () {
      const emitSpy = 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);

      assertButtonEvent(emitDownCalls[0], 'buttondown', 0, controller.buttons[0]);
    });

    test('emits buttonup if button released', function () {
      const emitSpy = sinon.spy(el, 'emit');
      component.buttonStates[1] = {pressed: true, touched: false, value: 1};
      component.buttonEventDetails[1] = {id: 1, state: component.buttonStates[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);

      assertButtonEvent(emitUpCalls[0], 'buttonup', 1, controller.buttons[1]);
    });

    test('does not emit buttonup if button pressed', function () {
      const emitSpy = 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 = 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 = sinon.spy(el, 'emit');
      component.tick();
      sinon.assert.notCalled(emitSpy);
      assert.notOk(component.handleTouch(0, {pressed: false, touched: false, value: 0}));
    });

    test('emits touchstart if button touched', function () {
      const emitSpy = 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);

      assertButtonEvent(emitStartCalls[0], 'touchstart', 0, controller.buttons[0]);
    });

    test('emits touchend if button no longer touched', function () {
      const emitSpy = sinon.spy(el, 'emit');
      component.buttonStates[1] = {pressed: false, touched: true, value: 1};
      component.buttonEventDetails[1] = {id: 1, state: component.buttonStates[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);

      assertButtonEvent(emitEndCalls[0], 'touchend', 1, 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);
    });
  });

  suite('armModel', function () {
    setup(function () {
      controller.pose.position = null;
    });

    test('if armModel false, do not apply', function () {
      var applyArmModelSpy = sinon.spy(component, 'applyArmModel');
      component.data.armModel = false;
      component.tick();
      sinon.assert.notCalled(applyArmModelSpy);
    });

    test('if armModel true, apply', function () {
      var applyArmModelSpy = sinon.spy(component, 'applyArmModel');
      component.data.armModel = true;
      component.tick();
      sinon.assert.calledOnce(applyArmModelSpy);
    });

    teardown(function () {
      controller.pose.position = [0, 0, 0];
    });
  });
});

function assertVec3CloseTo (vec3, arr, delta) {
  var debugOutput = `${[vec3.x, vec3.y, vec3.z]} is not close to ${arr}`;
  assert.closeTo(vec3.x, arr[0], delta, debugOutput);
  assert.closeTo(vec3.y, arr[1], delta, debugOutput);
  assert.closeTo(vec3.z, arr[2], delta, debugOutput);
}

function assertVec3 (vec3, arr) {
  var debugOutput = `${[vec3.x, vec3.y, vec3.z]} does not equal ${arr}`;
  assert.equal(vec3.x, arr[0], debugOutput);
  assert.equal(vec3.y, arr[1], debugOutput);
  assert.equal(vec3.z, arr[2], debugOutput);
}

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();
  })();
}

/**
 * Given a button event emit call that has been spied, verify all of the
 * data matches.
 *
 * @param {object} eventCall - The spied call to emit.
 * @param {string} eventName - The expected name of the event.
 * @param {number} eventId - The button id firing the event.
 * @param {object} eventState - The full event state.
 */
function assertButtonEvent (eventCall, eventName, eventId, eventState) {
  assert.equal(eventCall.args[0], eventName);
  assert.equal(eventCall.args[1].id, eventId);
  assert.deepEqual(eventCall.args[1].state, eventState);
}

/**
 * Verifies a stream of events includes and excludes the expected event names.
 *
 * @param {Array} eventCalls - The spied calls to emit for all events.
 * @param {Array} expectedEvents - The expected events in the stream. Must be present.
 * @param {Array} excludedEvents - Unexpected events in the stream. Must be excluded.
 */
function assertEventStream (eventCalls, expectedEvents, excludedEvents) {
  for (var eventCall of eventCalls) {
    const expectedIndex = expectedEvents.indexOf(eventCall.args[0]);
    const discludedIndex = excludedEvents.indexOf(eventCall.args[0]);

    // Ensure we don't have a discluded event.
    assert.equal(discludedIndex, -1);

    // If we found an expected event, then move it to the discluded list
    // since we should only see expected events once.
    if (expectedIndex >= 0) {
      excludedEvents.push(expectedEvents.splice(expectedIndex, 1)[0]);
    }
  }

  assert.equal(expectedEvents.length, 0);
}