Newer
Older
reroad-test / 2020-ryusei / aframe-master / tests / components / raycaster.test.js
@ryusei ryusei on 22 Oct 2020 20 KB パノラマ表示
/* global assert, process, setup, suite, test, THREE */
var entityFactory = require('../helpers').entityFactory;

suite('raycaster', function () {
  var component;
  var el;
  var parentEl;
  var raycaster;
  var sceneEl;

  setup(function (done) {
    parentEl = entityFactory();
    el = document.createElement('a-entity');
    el.setAttribute('raycaster', '');
    el.addEventListener('componentinitialized', evt => {
      if (evt.detail.name !== 'raycaster') { return; }
      component = el.components.raycaster;
      raycaster = component.raycaster;
      sceneEl = el.sceneEl;
      done();
    });
    parentEl.appendChild(el);
  });

  suite('init', function () {
    test('initializes raycaster', function () {
      assert.ok(raycaster);
    });
  });

  suite('update', function () {
    test('can update far', function () {
      el.setAttribute('raycaster', 'far', 50);
      assert.equal(raycaster.far, 50);
    });

    test('can update near', function () {
      el.setAttribute('raycaster', 'near', 5);
      assert.equal(raycaster.near, 5);
    });

    test('defaults to intersecting all objects', function (done) {
      const wrapper = document.createElement('a-entity');
      const el2 = document.createElement('a-entity');
      const el3 = document.createElement('a-entity');

      // Add geometry to test raycast and wait for them to be loaded.
      el2.setAttribute('geometry', 'primitive: box');
      el3.setAttribute('geometry', 'primitive: box');

      el3.addEventListener('loaded', () => {
        component.refreshObjects();
        // Check that groups are not the raycast targets.
        for (let i = 0; i < component.objects.length; i++) {
          assert.notEqual(component.objects[i], sceneEl.object3D.children[i].object3D);
        }
        assert.ok(component.objects.includes(el2.getObject3D('mesh')));
        assert.ok(component.objects.includes(el3.getObject3D('mesh')));
        done();
      });
      wrapper.appendChild(el2);
      wrapper.appendChild(el3);
      sceneEl.appendChild(wrapper);
    });

    test('does not include non-object3DMap children in objects', function (done) {
      var dummyObject3D;
      var el2 = document.createElement('a-entity');

      // Add geometry to test raycast and wait for them to be loaded.
      el2.setAttribute('geometry', 'primitive: box');

      dummyObject3D = new THREE.Object3D();
      el2.object3D.add(dummyObject3D);

      el2.addEventListener('loaded', function () {
        component.refreshObjects();
        assert.equal(component.objects.indexOf(dummyObject3D), -1);
        assert.notEqual(component.objects.indexOf(el2.getObject3D('mesh')), -1);
        done();
      });
      sceneEl.appendChild(el2);
    });

    test('cannot have redundant objects', function (done) {
      const el1 = document.createElement('a-entity');
      el1.setAttribute('geometry', 'primitive: box');
      const el2 = document.createElement('a-entity');
      el2.setAttribute('geometry', 'primitive: box');
      el1.appendChild(el2);

      el1.addEventListener('loaded', function () {
        component.refreshObjects();
        const mesh = el2.getObject3D('mesh');
        let count = 0;
        component.objects.forEach(obj => {
          if (obj === mesh) { count++; }
        });
        assert.equal(count, 1);
        done();
      });
      sceneEl.appendChild(el1);
    });

    test('can whitelist objects to intersect', function (done) {
      var el2 = document.createElement('a-entity');
      var el3 = document.createElement('a-entity');
      el2.setAttribute('class', 'clickable');
      el2.setAttribute('geometry', 'primitive: box');
      el2.addEventListener('loaded', function () {
        el.setAttribute('raycaster', 'objects', '.clickable');
        component.tick();
        assert.equal(component.objects.length, 1);
        assert.equal(component.objects[0], el2.object3D.children[0]);
        assert.equal(el2, el2.object3D.children[0].el);
        done();
      });
      sceneEl.appendChild(el2);
      sceneEl.appendChild(el3);
    });
  });

  suite('tick', function () {
    test('is throttled by interval', function () {
      var intersectSpy = this.sinon.spy(raycaster, 'intersectObjects');
      el.setAttribute('raycaster', 'interval', 1000);
      component.prevCheckTime = 1000;
      component.tick(1500);
      assert.notOk(intersectSpy.called);

      el.setAttribute('raycaster', 'interval', 499);
      component.tick(1500);
      assert.ok(intersectSpy.called);
    });
  });

  suite('remove', function () {
    test('removes line', function () {
      el.setAttribute('raycaster', 'showLine', true);
      assert.ok(el.getObject3D('line'));
      el.removeAttribute('raycaster');
      assert.notOk(el.getObject3D('line'));
    });
  });

  suite('refreshObjects', function () {
    setup(function (done) {
      var light;
      // Define camera and light before tests to avoid injection.
      sceneEl.appendChild(document.createElement('a-camera'));
      light = document.createElement('a-light');
      light.addEventListener('loaded', function () {
        // Wait for raycaster to pick them up.
        setTimeout(() => { done(); });
      });
      sceneEl.appendChild(light);
    });

    test('refresh objects when new entities are added to the scene', function (done) {
      var newEl = document.createElement('a-entity');
      component.tick();
      var numObjects = component.objects.length;
      newEl.setAttribute('geometry', 'primitive: box');
      newEl.addEventListener('loaded', function () {
        component.tick();
        assert.equal(component.objects.length, numObjects + 1);
        done();
      });
      sceneEl.appendChild(newEl);
    });

    test('refresh objects when new entities are removed from the scene', function (done) {
      var newEl = document.createElement('a-entity');
      component.tick();
      var numObjects = component.objects.length;
      newEl.setAttribute('geometry', 'primitive: box');
      sceneEl.addEventListener('child-detached', function doAssert () {
        component.tick();
        assert.equal(component.objects.length, numObjects);
        sceneEl.removeEventListener('child-detached', doAssert);
        done();
      });
      newEl.addEventListener('loaded', function () {
        sceneEl.removeChild(newEl);
      });
      sceneEl.appendChild(newEl);
    });

    test('refresh objects when entities are modified', function (done) {
      el.setAttribute('raycaster', {objects: '[ray-target]'});
      var newEl = document.createElement('a-entity');
      newEl.setAttribute('geometry', 'primitive: box');
      newEl.addEventListener('loaded', function doAssert () {
        component.tick();
        assert.equal(component.objects.length, 0);
        newEl.setAttribute('ray-target', '');
        setTimeout(function () {
          component.tick();
          assert.equal(component.objects.length, 1);
          sceneEl.removeEventListener('child-attached', doAssert);
          done();
        }, 0);
      });
      sceneEl.appendChild(newEl);
    });

    test('refresh objects when setObject3D() or removeObject3D() is called', function () {
      el.setAttribute('raycaster', {objects: '[ray-target]'});
      component.tick();
      assert.equal(component.dirty, false);
      sceneEl.emit('object3dset');
      assert.equal(component.dirty, true);
      component.tick();
      assert.equal(component.dirty, false);
      sceneEl.emit('object3dremove');
      assert.equal(component.dirty, true);
    });
  });

  suite('raycaster', function () {
    var targetEl;

    setup(function (done) {
      targetEl = document.createElement('a-entity');

      el.setAttribute('position', '0 0 1');
      el.setAttribute('raycaster', {near: 0.1, far: 10});

      targetEl.setAttribute('id', 'target');
      targetEl.setAttribute('geometry', 'primitive: box');
      targetEl.setAttribute('position', '0 0 -1');
      targetEl.addEventListener('loaded', function () {
        setTimeout(() => { done(); });
      });
      sceneEl.appendChild(targetEl);
    });

    test('can catch basic intersection', function (done) {
      targetEl.addEventListener('raycaster-intersected', function (evt) {
        assert.ok(evt.detail.getIntersection(targetEl));
        done();
      });
      component.tick();
    });

    test('updates intersectedEls', function (done) {
      var raycasterEl = el;
      assert.equal(component.intersectedEls.length, 0);
      raycasterEl.addEventListener('raycaster-intersection', function () {
        assert.equal(component.intersectedEls[0], targetEl);
        done();
      });
      component.tick();
    });

    test('emits event on raycaster entity with details', function (done) {
      var raycasterEl = el;
      raycasterEl.addEventListener('raycaster-intersection', function (evt) {
        assert.equal(evt.detail.els[0], targetEl);
        assert.equal(evt.detail.intersections[0].object.el, targetEl);
        done();
      });
      component.tick();
    });

    test('does not re-emit raycaster-intersection if no new intersections', function (done) {
      var count = 0;
      var raycasterEl = el;
      raycasterEl.addEventListener('raycaster-intersection', function () {
        count++;
      });
      component.tick();
      component.tick();
      setTimeout(() => {
        assert.equal(count, 1);
        done();
      });
    });

    test('does not re-emit raycaster-intersected if previously intersecting', function (done) {
      var count = 0;
      targetEl.addEventListener('raycaster-intersected', function (evt) {
        count++;
      });
      component.tick();
      component.tick();
      component.tick();
      setTimeout(() => {
        // 2 because the raycaster hits the box in two points.
        assert.equal(count, 2);
        done();
      });
    });

    test('emits event on intersected entity with details', function (done) {
      var raycasterEl = el;
      targetEl.addEventListener('raycaster-intersected', function (evt) {
        assert.equal(evt.detail.el, raycasterEl);
        done();
      });
      component.tick();
    });

    test('emits event on raycaster entity when clearing intersection', function (done) {
      var raycasterEl = el;
      raycasterEl.addEventListener('raycaster-intersection', function () {
        // Point raycaster somewhere else.
        raycasterEl.setAttribute('rotation', '90 0 0');
        raycasterEl.addEventListener('raycaster-intersection-cleared', function cb (evt) {
          assert.notEqual(component.clearedIntersectedEls.indexOf(targetEl), -1);
          raycasterEl.removeEventListener('raycaster-intersection-cleared', cb);
          done();
        });
        component.tick();
      });
      component.tick();
    });

    test('emits event on intersected entity when clearing intersection', function (done) {
      var raycasterEl = el;
      targetEl.addEventListener('raycaster-intersected', function () {
        // Point raycaster somewhere else.
        raycasterEl.setAttribute('rotation', '-90 0 0');
        targetEl.addEventListener('raycaster-intersected-cleared', function (evt) {
          assert.equal(evt.detail.el, raycasterEl);
          done();
        });
        component.tick();
      });
      component.tick();
    });

    test('clears intersections when disabled', function (done) {
      targetEl.addEventListener('raycaster-intersected', function () {
        targetEl.addEventListener('raycaster-intersected-cleared', function () {
          done();
        });
        assert.equal(component.intersectedEls.length, 2);
        assert.equal(component.clearedIntersectedEls.length, 0);
        el.setAttribute('raycaster', 'enabled', false);
        assert.equal(component.intersectedEls.length, 0);
        assert.equal(component.intersections.length, 0);
        assert.equal(component.clearedIntersectedEls.length, 2);
      });
      component.tick();
    });

    test('emits intersectioncleared when disabled', function (done) {
      targetEl.addEventListener('raycaster-intersected', function () {
        el.addEventListener('raycaster-intersection-cleared', function () {
          done();
        });
        el.setAttribute('raycaster', 'enabled', false);
      });
      component.tick();
    });
  });

  suite('updateOriginDirection', function () {
    test('updates ray origin if position changes', function () {
      var origin;
      el.setAttribute('position', '1 2 3');
      sceneEl.object3D.updateMatrixWorld();  // Normally handled by renderer.
      component.tick();
      origin = raycaster.ray.origin;
      assert.equal(origin.x, 1);
      assert.equal(origin.y, 2);
      assert.equal(origin.z, 3);
    });

    test('updates ray origin if parent position changes', function () {
      var origin;
      parentEl.setAttribute('position', '1 2 3');
      sceneEl.object3D.updateMatrixWorld();  // Normally handled by renderer.
      component.tick();
      origin = raycaster.ray.origin;
      assert.equal(origin.x, 1);
      assert.equal(origin.y, 2);
      assert.equal(origin.z, 3);
    });

    test('defaults ray direction to 0 0 -1', function () {
      var direction = raycaster.ray.direction;
      assert.equal(direction.x, 0);
      assert.equal(direction.y, 0);
      assert.equal(direction.z, -1);
    });

    test('updates ray direction if rotation changes', function () {
      var direction;
      el.setAttribute('rotation', '180 0 0');
      component.tick();
      direction = raycaster.ray.direction;
      assert.equal(Math.floor(direction.x), 0);
      assert.equal(Math.floor(direction.y), 0);
      assert.equal(direction.z, 1);
    });

    test('updates ray direction if parent rotation changes', function () {
      var direction;
      parentEl.setAttribute('rotation', '180 0 0');
      sceneEl.object3D.updateMatrixWorld();
      component.tick();
      direction = raycaster.ray.direction;
      assert.equal(Math.floor(direction.x), 0);
      assert.equal(Math.floor(direction.y), 0);
      assert.equal(direction.z, 1);
    });

    test('can specify origin', function () {
      var origin;
      el.setAttribute('position', '5 5 5');
      el.setAttribute('raycaster', 'origin', '0 -1 1');
      sceneEl.object3D.updateMatrixWorld();
      component.tick();
      origin = raycaster.ray.origin;
      assert.equal(origin.x, 5);
      assert.equal(origin.y, 4);
      assert.equal(origin.z, 6);

      el.setAttribute('rotation', '180 0 0');
      sceneEl.object3D.updateMatrixWorld();
      component.tick();
      assert.equal(origin.x, 5);
      assert.equal(origin.y, 6);
      assert.equal(origin.z, 4);
    });

    test('can specify direction', function () {
      var direction;
      el.setAttribute('raycaster', 'direction', '0 -1 0');
      sceneEl.object3D.updateMatrixWorld();
      component.tick();
      direction = raycaster.ray.direction;
      assert.equal(direction.x, 0);
      assert.equal(direction.y, -1);
      assert.equal(direction.z, 0);

      el.setAttribute('rotation', '0 0 180');
      component.tick();
      direction = raycaster.ray.direction;
      assert.equal(direction.y, 1);
    });

    test('applies origin and direction without transformation if worldCoordinates enabled', function () {
      el.setAttribute('raycaster', 'useWorldCoordinates', true);
      el.setAttribute('raycaster', 'origin', '1 1 1');
      el.setAttribute('raycaster', 'direction', '2 2 2');
      el.setAttribute('position', '5 5 5');
      el.setAttribute('rotation', '30 45 90');
      sceneEl.object3D.updateMatrixWorld();  // Normally handled by renderer.
      component.tick();
      var origin = raycaster.ray.origin;
      var direction = raycaster.ray.direction;
      assert.equal(origin.x, 1);
      assert.equal(origin.y, 1);
      assert.equal(origin.z, 1);
      assert.equal(direction.x, 2);
      assert.equal(direction.y, 2);
      assert.equal(direction.z, 2);
    });
  });

  suite('line', function () {
    setup(function () {
      el.setAttribute('raycaster', 'showLine', true);
    });

    test('creates line', function () {
      var lineData;
      assert.ok(el.getObject3D('line'));
      lineData = el.getAttribute('line');
      assert.shallowDeepEqual(lineData.start, {x: 0, y: 0, z: 0});
      assert.shallowDeepEqual(lineData.end, {x: 0, y: 0, z: -1000});
    });

    test('can remove line', function () {
      el.setAttribute('raycaster', 'showLine', false);
      assert.notOk(el.getObject3D('line'));
    });

    test('matches direction', function () {
      var lineData;
      el.setAttribute('raycaster', 'direction', '0 -1 -1');
      lineData = el.getAttribute('line');
      assert.equal(Math.round(lineData.end.y), -707);
      assert.equal(Math.round(lineData.end.z), -707);
    });

    test('matches origin', function () {
      var lineData;
      el.setAttribute('raycaster', 'origin', '5 10 -20');
      lineData = el.getAttribute('line');
      assert.shallowDeepEqual(lineData.start, {x: 5, y: 10, z: -20});
    });

    test('truncates length to point of intersection', function (done) {
      var box;
      var line;

      el.setAttribute('raycaster', {direction: '0 0 -1', origin: '0 0 0'});
      line = el.getAttribute('line');
      assert.equal(new THREE.Vector3().copy(line.start).sub(line.end).length(), 1000);

      box = document.createElement('a-entity');
      box.setAttribute('geometry', {primitive: 'box', width: 1, height: 1, depth: 1});
      box.setAttribute('position', '0 0 -25');
      sceneEl.appendChild(box);

      box.addEventListener('loaded', function () {
        el.sceneEl.object3D.updateMatrixWorld();
        component.refreshObjects();
        component.tick();
        setTimeout(() => {
          line = el.getAttribute('line');
          assert.equal(new THREE.Vector3().copy(line.start).sub(line.end).length(), 24.5);
          box.parentNode.removeChild(box);
          setTimeout(() => {
            component.tick();
            setTimeout(() => {
              line = el.getAttribute('line');
              assert.equal(new THREE.Vector3().copy(line.start).sub(line.end).length(), 1000);
              done();
            });
          });
        });
      });
    });

    test('truncates line length with two raycasters', function (done) {
      var box;
      var rayEl2;
      var line;
      var lineArray;
      var lineStart = new THREE.Vector3();
      var lineEnd = new THREE.Vector3();

      el.setAttribute('raycaster', {direction: '0 0 -1', origin: '0 0 0', objects: '#target'});
      lineArray = el.components.line.geometry.attributes.position.array;

      rayEl2 = document.createElement('a-entity');
      rayEl2.setAttribute('raycaster', {
        direction: '0 0 -1',
        origin: '0 0 0',
        showLine: true,
        objects: '#target'
      });
      sceneEl.appendChild(rayEl2);

      line = el.getAttribute('line');
      assert.equal(new THREE.Vector3().copy(line.start).sub(line.end).length(), 1000);
      lineEnd.fromArray(lineArray, 3);
      assert.equal(lineStart.fromArray(lineArray).sub(lineEnd).length(), 1000);

      box = document.createElement('a-entity');
      box.id = 'target';
      box.setAttribute('geometry', {primitive: 'box', width: 1, height: 1, depth: 1});
      box.setAttribute('position', '0 0 -25');
      sceneEl.appendChild(box);

      box.addEventListener('loaded', function () {
        el.sceneEl.object3D.updateMatrixWorld();
        component.refreshObjects();
        component.tick();
        rayEl2.components.raycaster.tick();
        rayEl2.components.raycaster.tick();
        setTimeout(() => {
          // Ensure component and geometry are unaffected by other raycaster
          line = el.getAttribute('line');
          assert.equal(new THREE.Vector3().copy(line.start).sub(line.end).length(), 24.5);
          lineEnd.fromArray(lineArray, 3);
          assert.equal(lineStart.fromArray(lineArray).sub(lineEnd).length(), 24.5);
          box.parentNode.removeChild(box);
          setTimeout(() => {
            component.tick();
            setTimeout(() => {
              line = el.getAttribute('line');
              assert.equal(new THREE.Vector3().copy(line.start).sub(line.end).length(), 1000);
              lineEnd.fromArray(lineArray, 3);
              assert.equal(lineStart.fromArray(lineArray).sub(lineEnd).length(), 1000);
              done();
            });
          });
        });
      });
    });
  });
});