From 59b3cf03eadb306d93fd6e7dc104f57ace550e6f Mon Sep 17 00:00:00 2001 From: machenmusik Date: Tue, 16 May 2017 15:17:24 -0400 Subject: [PATCH] enable non-recursive raycaster (fixes #2329) (#2331) * change raycaster to be non-recursive since there appears to be a wrapper around an entity's object3D, use its children instead * add simple example of non-recursive raycaster and dynamically created box add geometry so that raycaster actually adds something wait until next tick to satisfy Firefox; remove extra tick * changes per discussion on PR * changes per discussion on PR --- examples/index.html | 1 + examples/test/raycaster/index.html | 2 +- examples/test/raycaster/simple.html | 39 +++++++ src/components/raycaster.js | 53 +++++++-- tests/components/raycaster.test.js | 164 +++++++++++++++++++++++++--- 5 files changed, 231 insertions(+), 28 deletions(-) create mode 100644 examples/test/raycaster/simple.html diff --git a/examples/index.html b/examples/index.html index 47cc367524..abc2f9c8a8 100644 --- a/examples/index.html +++ b/examples/index.html @@ -235,6 +235,7 @@

Tests

  • Physically-Based Materials
  • Pivot
  • Raycaster
  • +
  • Raycaster (Simple)
  • Shaders
  • Shadows
  • Text
  • diff --git a/examples/test/raycaster/index.html b/examples/test/raycaster/index.html index 85dfd5aa32..89d753b62a 100644 --- a/examples/test/raycaster/index.html +++ b/examples/test/raycaster/index.html @@ -20,7 +20,7 @@ intersect-color-change> - diff --git a/examples/test/raycaster/simple.html b/examples/test/raycaster/simple.html new file mode 100644 index 0000000000..7aa2462c30 --- /dev/null +++ b/examples/test/raycaster/simple.html @@ -0,0 +1,39 @@ + + + + + Raycaster + + + + + + + + + + + + + + diff --git a/src/components/raycaster.js b/src/components/raycaster.js index d05dc00899..39bd49fe5e 100644 --- a/src/components/raycaster.js +++ b/src/components/raycaster.js @@ -33,15 +33,18 @@ module.exports.Component = registerComponent('raycaster', { this.raycaster = new THREE.Raycaster(); this.updateOriginDirection(); this.refreshObjects = bind(this.refreshObjects, this); + this.refreshOnceChildLoaded = bind(this.refreshOnceChildLoaded, this); }, play: function () { - this.el.sceneEl.addEventListener('child-attached', this.refreshObjects); + this.el.sceneEl.addEventListener('loaded', this.refreshObjects); + this.el.sceneEl.addEventListener('child-attached', this.refreshOnceChildLoaded); this.el.sceneEl.addEventListener('child-detached', this.refreshObjects); }, pause: function () { - this.el.sceneEl.removeEventListener('child-attached', this.refreshObjects); + this.el.sceneEl.removeEventListener('loaded', this.refreshObjects); + this.el.sceneEl.removeEventListener('child-attached', this.refreshOnceChildLoaded); this.el.sceneEl.removeEventListener('child-detached', this.refreshObjects); }, @@ -59,26 +62,54 @@ module.exports.Component = registerComponent('raycaster', { this.refreshObjects(); }, + /** + * Update list of objects to test for intersection once child is loaded. + */ + refreshOnceChildLoaded: function (evt) { + var self = this; + var childEl = evt.detail.el; + if (!childEl) { return; } + if (childEl.hasLoaded) { + this.refreshObjects(); + } else { + childEl.addEventListener('loaded', function nowRefresh (evt) { + childEl.removeEventListener('loaded', nowRefresh); + self.refreshObjects(); + }); + } + }, + /** * Update list of objects to test for intersection. */ refreshObjects: function () { + var children; var data = this.data; var i; - var objectEls; + var objects; + var objectsAreEls = data.objects ? this.el.sceneEl.querySelectorAll(data.objects) : null; // Push meshes onto list of objects to intersect. - if (data.objects) { - objectEls = this.el.sceneEl.querySelectorAll(data.objects); - this.objects = []; - for (i = 0; i < objectEls.length; i++) { - this.objects.push(objectEls[i].object3D); + if (objectsAreEls) { + objects = []; + for (i = 0; i < objectsAreEls.length; i++) { + objects.push(objectsAreEls[i].object3D); } - return; + } else { + // If objects not defined, intersect with everything. + objects = this.el.sceneEl.object3D.children; } - // If objects not defined, intersect with everything. - this.objects = this.el.sceneEl.object3D.children; + this.objects = []; + for (i = 0; i < objects.length; i++) { + // A-Frame wraps everything (e.g. in a Group) so we want children. + children = objects[i].children; + + // Add the object3D's children so non-recursive raycasting will work correctly. + // If there aren't any children, then until a refresh after geometry loads, + // raycast won't see this object... but that should happen automatically. + if (children) { this.objects.push.apply(this.objects, children); } + } }, /** diff --git a/tests/components/raycaster.test.js b/tests/components/raycaster.test.js index 45414e1e34..c26370a2ea 100644 --- a/tests/components/raycaster.test.js +++ b/tests/components/raycaster.test.js @@ -31,30 +31,46 @@ suite('raycaster', function () { assert.equal(el.components.raycaster.raycaster.near, 5); }); - test('defaults to intersecting all objects', function () { + test('defaults to intersecting all objects', function (done) { var el = this.el; var el2 = document.createElement('a-entity'); var el3 = document.createElement('a-entity'); + var i; var objects; + // Add some geometry so raycast will actually work, and wait for it to be loaded. + el2.setAttribute('geometry', 'primitive: box'); + el3.setAttribute('geometry', 'primitive: box'); + el3.addEventListener('loaded', function finishSetup () { + el.components.raycaster.refreshObjects(); + objects = el.components.raycaster.objects; + // The object to check raycast against isn't the object3D (which is a wrapper), but the child. + for (i = 0; i < objects.length; i++) { + assert.equal(objects[i], el.sceneEl.object3D.children[i].children[0]); + } + done(); + }); el.sceneEl.appendChild(el2); el.sceneEl.appendChild(el3); - - el.components.raycaster.refreshObjects(); - objects = el.components.raycaster.objects; - assert.equal(objects, el.sceneEl.object3D.children); }); - test('can set objects to intersect', function () { + test('can set objects to intersect', function (done) { var el = this.el; var el2 = document.createElement('a-entity'); var el3 = document.createElement('a-entity'); el2.setAttribute('class', 'clickable'); + // add some geometry so raycast will actually work, and wait for it to be loaded + el2.setAttribute('geometry', 'primitive: box'); + el2.addEventListener('loaded', function finishSetup () { + el.setAttribute('raycaster', 'objects', '.clickable'); + assert.equal(el.components.raycaster.objects.length, 1); + // The object to check raycast against isn't the object3D (which is a wrapper), but the child. + assert.equal(el.components.raycaster.objects[0], el2.object3D.children[0]); + // The object to check raycast against should reference the entity. + assert.equal(el2, el2.object3D.children[0].el); + done(); + }); el.sceneEl.appendChild(el2); el.sceneEl.appendChild(el3); - - el.setAttribute('raycaster', 'objects', '.clickable'); - assert.equal(el.components.raycaster.objects.length, 1); - assert.equal(el.components.raycaster.objects[0], el2.object3D); }); }); @@ -75,16 +91,35 @@ suite('raycaster', function () { }); suite('refreshObjects', function () { + setup(function createRaycasterAndTarget (done) { + // Define camera and light before tests to avoid injection. + this.el.sceneEl.appendChild(document.createElement('a-camera')); + var waitForMe = document.createElement('a-light'); + waitForMe.addEventListener('loaded', function finishSetup () { setTimeout(function () { done(); }); }); + this.el.sceneEl.appendChild(waitForMe); + }); + test('refresh objects when new entities are added to the scene', function (done) { var el = this.el; var newEl = document.createElement('a-entity'); var numObjects = el.components.raycaster.objects.length; var sceneEl = this.el.sceneEl; - sceneEl.addEventListener('child-attached', doAssert); + // add some geometry so raycast will actually work + newEl.setAttribute('geometry', 'primitive: box'); + sceneEl.addEventListener('child-attached', eventuallyDoAssert); sceneEl.appendChild(newEl); + + function eventuallyDoAssert () { + if (newEl.hasLoaded) { + doAssert(); + } else { + newEl.addEventListener('loaded', doAssert); + } + } function doAssert () { + sceneEl.removeEventListener('child-attached', eventuallyDoAssert); + newEl.removeEventListener('loaded', doAssert); assert.equal(el.components.raycaster.objects.length, numObjects + 1); - sceneEl.removeEventListener('child-attached', doAssert); done(); } }); @@ -94,14 +129,19 @@ suite('raycaster', function () { var newEl = document.createElement('a-entity'); var numObjects = el.components.raycaster.objects.length; var sceneEl = this.el.sceneEl; + // add some geometry so raycast will actually work + newEl.setAttribute('geometry', 'primitive: box'); sceneEl.addEventListener('child-detached', doAssert); - sceneEl.addEventListener('child-attached', doRemove); + sceneEl.addEventListener('child-attached', eventuallyDoRemove); sceneEl.appendChild(newEl); + function eventuallyDoRemove () { + if (newEl.hasLoaded) { doRemove(); } else { newEl.addEventListener('loaded', doRemove); } + } function doRemove () { sceneEl.removeChild(newEl); } function doAssert () { assert.equal(el.components.raycaster.objects.length, numObjects); - sceneEl.removeEventListener('child-attached', doRemove); + sceneEl.removeEventListener('child-attached', eventuallyDoRemove); sceneEl.removeEventListener('child-detached', doAssert); done(); } @@ -120,12 +160,104 @@ suite('raycaster', function () { }); targetEl.setAttribute('geometry', 'primitive: box; depth: 1; height: 1; width: 1;'); - targetEl.setAttribute('material', ''); targetEl.setAttribute('position', '0 0 -1'); - targetEl.addEventListener('loaded', function finishSetup () { + el.sceneEl.appendChild(targetEl); + function finishSetup () { setTimeout(function () { done(); }, 0); } + if (targetEl.hasLoaded) { finishSetup(); } else { targetEl.addEventListener('loaded', finishSetup); } + }); + + test('can catch basic intersection', function (done) { + this.targetEl.addEventListener('raycaster-intersected', function () { done(); }); + this.el.sceneEl.tick(); + }); + + test('updates intersectedEls', function (done) { + var raycasterEl = this.el; + var targetEl = this.targetEl; + assert.equal(raycasterEl.components.raycaster.intersectedEls.length, 0); + raycasterEl.addEventListener('raycaster-intersection', function () { + assert.equal(raycasterEl.components.raycaster.intersectedEls[0], targetEl); + done(); + }); + raycasterEl.sceneEl.tick(); + }); + + test('emits event on raycaster entity with details', function (done) { + var targetEl = this.targetEl; + var raycasterEl = this.el; + raycasterEl.addEventListener('raycaster-intersection', function (evt) { + assert.equal(evt.detail.els[0], targetEl); + assert.equal(evt.detail.intersections[0].object.el, targetEl); + done(); + }); + raycasterEl.sceneEl.tick(); + }); + + test('emits event on intersected entity with details', function (done) { + var targetEl = this.targetEl; + var raycasterEl = this.el; + targetEl.addEventListener('raycaster-intersected', function (evt) { + assert.equal(evt.detail.el, raycasterEl); done(); }); + raycasterEl.sceneEl.tick(); + }); + + test('emits event on raycaster entity when clearing intersection', function (done) { + var targetEl = this.targetEl; + var raycasterEl = this.el; + raycasterEl.addEventListener('raycaster-intersection', function () { + // Point raycaster somewhere else. + raycasterEl.setAttribute('rotation', '90 0 0'); + raycasterEl.addEventListener('raycaster-intersection-cleared', function (evt) { + assert.equal(evt.detail.el, targetEl); + done(); + }); + raycasterEl.sceneEl.tick(); + }); + raycasterEl.sceneEl.tick(); + }); + + test('emits event on intersected entity when clearing intersection', function (done) { + var targetEl = this.targetEl; + var raycasterEl = this.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(); + }); + raycasterEl.sceneEl.tick(); + }); + raycasterEl.sceneEl.tick(); + }); + }); + + suite('non-recursive raycaster', function () { + setup(function createRaycasterAndTarget (done) { + var el = this.el; + var targetEl = this.targetEl = document.createElement('a-entity'); + + el.setAttribute('position', '0 0 1'); + el.setAttribute('raycaster', { + recursive: false, + near: 0.1, + far: 10 + }); + + targetEl.setAttribute('geometry', 'primitive: box; depth: 1; height: 1; width: 1;'); + targetEl.setAttribute('position', '0 0 -1'); el.sceneEl.appendChild(targetEl); + // `npm run test:forefox` needs the timeout for the tests to succeed. + function finishSetup () { + setTimeout(function () { + // The object to check raycast against should reference the entity. + assert.equal(targetEl, targetEl.object3D.children[0].el); + done(); + }, 0); + } + if (targetEl.hasLoaded) { finishSetup(); } else { targetEl.addEventListener('loaded', finishSetup); } }); test('can catch basic intersection', function (done) {