Skip to content

Commit

Permalink
enable non-recursive raycaster (fixes #2329) (#2331)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
machenmusik authored and ngokevin committed May 16, 2017
1 parent f8db222 commit 59b3cf0
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 28 deletions.
1 change: 1 addition & 0 deletions examples/index.html
Expand Up @@ -235,6 +235,7 @@ <h2>Tests</h2>
<li><a href="test/physical/">Physically-Based Materials</a></li>
<li><a href="test/pivot/">Pivot</a></li>
<li><a href="test/raycaster/">Raycaster</a></li>
<li><a href="test/raycaster/simple.html">Raycaster (Simple)</a></li>
<li><a href="test/shaders/">Shaders</a></li>
<li><a href="test/shadows/">Shadows</a></li>
<li><a href="test/text/">Text</a></li>
Expand Down
2 changes: 1 addition & 1 deletion examples/test/raycaster/index.html
Expand Up @@ -20,7 +20,7 @@
intersect-color-change></a-mixin>
<a-mixin id="finger" geometry="primitive: box; height: 0.02; width: 0.02; depth: 0.5"
material="side: double"></a-mixin>
<a-mixin id="raycaster" raycaster="objects: [mixin~='box']"
<a-mixin id="raycaster" raycaster="objects: [mixin~='box']; recursive: false"
raycaster-helper material="opacity: 0.8"></a-mixin>

<a-mixin id="color1" material="color: #0B88A8"></a-mixin>
Expand Down
39 changes: 39 additions & 0 deletions examples/test/raycaster/simple.html
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Raycaster</title>
<meta name="description" content="Raycaster - A-Frame">
<script src="../../../dist/aframe-master.js"></script>
</head>
<body>

<script>

AFRAME.registerComponent('raycast-blab', {
init: function () {
this.el.addEventListener('raycaster-intersected', function (evt) {
var el = evt.detail.target;
// May get two intersection events per tick; same element, different faces.
console.log('raycaster-intersected ' + el.outerHTML);
el.setAttribute('material', 'color', '#7f7');
});

this.el.addEventListener('raycaster-intersected-cleared', function (evt) {
var el = evt.detail.target;
// May get two intersection events per tick; same element, different faces.
console.log('raycaster-intersected-cleared ' + el.outerHTML);
el.setAttribute('material', 'color', '#f77');
});
}
});

</script>

<a-scene>
<a-entity camera look-controls wasd-controls raycaster="recursive: false; interval: 1000"></a-entity>
<a-entity geometry="primitive: box" material="color: #000" position="0 0 -2" raycast-blab></a-entity>
</a-scene>

</body>
</html>
53 changes: 42 additions & 11 deletions src/components/raycaster.js
Expand Up @@ -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);
},

Expand All @@ -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); }
}
},

/**
Expand Down
164 changes: 148 additions & 16 deletions tests/components/raycaster.test.js
Expand Up @@ -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);
});
});

Expand All @@ -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();
}
});
Expand All @@ -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();
}
Expand All @@ -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) {
Expand Down

0 comments on commit 59b3cf0

Please sign in to comment.