diff --git a/examples/index.html b/examples/index.html
index 47cc3675243..abc2f9c8a81 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 85dfd5aa32d..89d753b62a1 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 00000000000..7aa2462c303
--- /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 d05dc00899c..39bd49fe5ee 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 45414e1e342..c26370a2ea3 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) {