diff --git a/docs/components/cursor.md b/docs/components/cursor.md
index 6014f574244..12752625042 100644
--- a/docs/components/cursor.md
+++ b/docs/components/cursor.md
@@ -80,6 +80,7 @@ AFRAME.registerComponent('cursor-listener', {
| downEvents | Array of additional events on the entity to *listen* to for triggering `mousedown` (e.g., `triggerdown` for vive-controls). | [] |
| fuse | Whether cursor is fuse-based. | false on desktop, true on mobile |
| fuseTimeout | How long to wait (in milliseconds) before triggering a fuse-based click event. | 1500 |
+| mode | Camera (gaze) or mouse controlled. | gaze
| upEvents | Array of additional events on the entity to *listen* to for triggering `mouseup` (e.g., `trackpadup` for daydream-controls). | [] |
To further customize the cursor component, we configure the cursor's dependency
diff --git a/docs/components/raycaster.md b/docs/components/raycaster.md
index 024f2454e18..e195d006408 100644
--- a/docs/components/raycaster.md
+++ b/docs/components/raycaster.md
@@ -58,6 +58,7 @@ AFRAME.registerComponent('collider-check', {
| origin | Vector3 coordinate of where the ray should originate from relative to the entity's origin. | 0, 0, 0 |
| recursive | Checks all children of objects if set. Else only checks intersections with root objects. | true |
| showLine | Whether or not to display the raycaster visually with the [line component][line]. | false |
+| worldCoordinates | Determine if origin and direction are given in local or world coordinates. | false |
## Events
diff --git a/examples/index.html b/examples/index.html
index 727f59eef0f..747d76589cc 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -187,6 +187,7 @@
Showcase
Composite
Curved Mockups
Dynamic Lights
+ Link Traversal
Tracked Controls
Shopping
Spheres and Fog
diff --git a/examples/showcase/link-traversal/city.html b/examples/showcase/link-traversal/city.html
index edf6e1a69c6..f01fb13002e 100644
--- a/examples/showcase/link-traversal/city.html
+++ b/examples/showcase/link-traversal/city.html
@@ -6,24 +6,17 @@
+
-
+
-
-
diff --git a/examples/showcase/link-traversal/index.html b/examples/showcase/link-traversal/index.html
index fd4086766c6..18833583283 100644
--- a/examples/showcase/link-traversal/index.html
+++ b/examples/showcase/link-traversal/index.html
@@ -2,16 +2,17 @@
- Room Scale
+ Links
+
-
+
diff --git a/examples/showcase/link-traversal/js/components/camera-position.js b/examples/showcase/link-traversal/js/components/camera-position.js
new file mode 100644
index 00000000000..0722a53fa38
--- /dev/null
+++ b/examples/showcase/link-traversal/js/components/camera-position.js
@@ -0,0 +1,34 @@
+/* global AFRAME */
+AFRAME.registerComponent('camera-position', {
+ schema: {
+ mobile: {type: 'vec3', default: '0 1.6 3'},
+ desktop: {type: 'vec3', default: '0 1.6 3'}
+ },
+ init: function () {
+ this.onCameraSetActive = this.onCameraSetActive.bind(this);
+ this.resetCamera = this.resetCamera.bind(this);
+ this.el.addEventListener('camera-set-active', this.onCameraSetActive);
+ this.el.addEventListener('exit-vr', this.onCameraSetActive);
+ this.el.addEventListener('enter-vr', this.resetCamera);
+ },
+
+ onCameraSetActive: function () {
+ var cameraEl = this.el.camera.el;
+ var data = this.data;
+ var isMobile = AFRAME.utils.device.isMobile();
+ var position = isMobile ? data.mobile : data.desktop;
+ var savedPose = cameraEl.components.camera.savedPose;
+ if (savedPose) { savedPose.position.z = position.z; }
+ this.el.camera.el.setAttribute('position', position);
+ },
+
+ resetCamera: function () {
+ var cameraEl = this.el.camera.el;
+ var position = cameraEl.getAttribute('position');
+ cameraEl.setAttribute('position', {
+ x: position.x,
+ y: position.y,
+ z: 0
+ });
+ }
+});
diff --git a/examples/showcase/link-traversal/js/components/link-controls.js b/examples/showcase/link-traversal/js/components/link-controls.js
index 4c25ea959bd..d4258373dff 100644
--- a/examples/showcase/link-traversal/js/components/link-controls.js
+++ b/examples/showcase/link-traversal/js/components/link-controls.js
@@ -163,16 +163,16 @@ AFRAME.registerComponent('link-controls', {
},
play: function () {
- var el = this.el;
- el.addEventListener('mouseenter', this.onMouseEnter);
- el.addEventListener('mouseleave', this.onMouseLeave);
+ var sceneEl = this.el.sceneEl;
+ sceneEl.addEventListener('mouseenter', this.onMouseEnter);
+ sceneEl.addEventListener('mouseleave', this.onMouseLeave);
this.addControllerEventListeners();
},
pause: function () {
- var el = this.el;
- el.removeEventListener('mouseenter', this.onMouseEnter);
- el.removeEventListener('mouseleave', this.onMouseLeave);
+ var sceneEl = this.el.sceneEl;
+ sceneEl.removeEventListener('mouseenter', this.onMouseEnter);
+ sceneEl.removeEventListener('mouseleave', this.onMouseLeave);
this.removeControllerEventListeners();
},
@@ -312,7 +312,7 @@ AFRAME.registerComponent('link-controls', {
var previousSelectedLinkEl = this.selectedLinkEl;
var selectedLinkEl = evt.detail.intersectedEl;
var urlEl = this.urlEl;
- if (previousSelectedLinkEl || selectedLinkEl.components.link === undefined) { return; }
+ if (!selectedLinkEl || previousSelectedLinkEl || selectedLinkEl.components.link === undefined) { return; }
selectedLinkEl.setAttribute('link', 'highlighted', true);
this.selectedLinkElPosition = selectedLinkEl.getAttribute('position');
this.selectedLinkEl = selectedLinkEl;
@@ -325,7 +325,7 @@ AFRAME.registerComponent('link-controls', {
onMouseLeave: function (evt) {
var selectedLinkEl = this.selectedLinkEl;
var urlEl = this.urlEl;
- if (!selectedLinkEl) { return; }
+ if (!selectedLinkEl || !evt.detail.intersectedEl) { return; }
selectedLinkEl.setAttribute('link', 'highlighted', false);
this.selectedLinkEl = undefined;
if (!urlEl) { return; }
diff --git a/examples/showcase/link-traversal/mountains.html b/examples/showcase/link-traversal/mountains.html
index 26533956d81..13b04803742 100644
--- a/examples/showcase/link-traversal/mountains.html
+++ b/examples/showcase/link-traversal/mountains.html
@@ -6,12 +6,13 @@
+
-
+
diff --git a/examples/showcase/link-traversal/sunrise.html b/examples/showcase/link-traversal/sunrise.html
index d5c10887641..97d69f14870 100644
--- a/examples/showcase/link-traversal/sunrise.html
+++ b/examples/showcase/link-traversal/sunrise.html
@@ -6,12 +6,13 @@
+
-
+
diff --git a/src/components/cursor.js b/src/components/cursor.js
index 412d43bb510..cbbf6c7748b 100644
--- a/src/components/cursor.js
+++ b/src/components/cursor.js
@@ -1,3 +1,4 @@
+/* global THREE */
var registerComponent = require('../core/component').registerComponent;
var utils = require('../utils/');
@@ -37,7 +38,8 @@ module.exports.Component = registerComponent('cursor', {
downEvents: {default: []},
fuse: {default: utils.device.isMobile()},
fuseTimeout: {default: 1500, min: 0},
- upEvents: {default: []}
+ upEvents: {default: []},
+ mode: {default: 'gaze', oneOf: ['gaze', 'mouse']}
},
init: function () {
@@ -51,6 +53,11 @@ module.exports.Component = registerComponent('cursor', {
this.onCursorUp = bind(this.onCursorUp, this);
this.onIntersection = bind(this.onIntersection, this);
this.onIntersectionCleared = bind(this.onIntersectionCleared, this);
+ this.onMouseMove = bind(this.onMouseMove, this);
+ },
+
+ update: function () {
+ this.updateMouseEventListeners();
},
play: function () {
@@ -118,8 +125,34 @@ module.exports.Component = registerComponent('cursor', {
});
el.removeEventListener('raycaster-intersection', this.onIntersection);
el.removeEventListener('raycaster-intersection-cleared', this.onIntersectionCleared);
+ window.removeEventListener('mousemove', this.onMouseMove);
+ },
+
+ updateMouseEventListeners: function () {
+ var el = this.el;
+ window.removeEventListener('mousemove', this.onMouseMove);
+ el.setAttribute('raycaster', 'worldCoordinates', false);
+ if (this.data.mode !== 'mouse') { return; }
+ window.addEventListener('mousemove', this.onMouseMove, false);
+ el.setAttribute('raycaster', 'worldCoordinates', true);
},
+ onMouseMove: (function () {
+ var mouse = new THREE.Vector2();
+ var origin = new THREE.Vector3();
+ var direction = new THREE.Vector3();
+ return function (evt) {
+ var camera = this.el.sceneEl.camera;
+ camera.parent.updateMatrixWorld();
+ camera.updateMatrixWorld();
+ mouse.x = (evt.clientX / window.innerWidth) * 2 - 1;
+ mouse.y = -(evt.clientY / window.innerHeight) * 2 + 1;
+ origin.setFromMatrixPosition(camera.matrixWorld);
+ direction.set(mouse.x, mouse.y, 0.5).unproject(camera).sub(origin).normalize();
+ this.el.setAttribute('raycaster', {origin: origin, direction: direction});
+ };
+ })(),
+
/**
* Trigger mousedown and keep track of the mousedowned entity.
*/
diff --git a/src/components/raycaster.js b/src/components/raycaster.js
index 84c003418f8..90d96f8e41b 100644
--- a/src/components/raycaster.js
+++ b/src/components/raycaster.js
@@ -27,7 +27,8 @@ module.exports.Component = registerComponent('raycaster', {
objects: {default: ''},
origin: {type: 'vec3'},
recursive: {default: true},
- showLine: {default: false}
+ showLine: {default: false},
+ worldCoordinates: {default: false}
},
init: function () {
@@ -226,6 +227,11 @@ module.exports.Component = registerComponent('raycaster', {
var el = this.el;
var data = this.data;
+ if (data.worldCoordinates) {
+ this.raycaster.set(data.origin, data.direction);
+ return;
+ }
+
// Grab the position and rotation.
el.object3D.updateMatrixWorld();
el.object3D.matrixWorld.decompose(originVec3, quaternion, dummyVec);
diff --git a/tests/components/cursor.test.js b/tests/components/cursor.test.js
index d2022ce42a8..066aec4f022 100644
--- a/tests/components/cursor.test.js
+++ b/tests/components/cursor.test.js
@@ -76,6 +76,17 @@ suite('cursor', function () {
done();
});
});
+
+ suite('update', function () {
+ test('attach mousemove event listener on mouse mode', function () {
+ var updateSpy = this.sinon.spy(el.components.cursor, 'update');
+ var updateMouseEventListenersSpy = this.sinon.spy(el.components.cursor, 'updateMouseEventListeners');
+ var onMouseMoveSpy = this.sinon.spy(el.components.cursor, 'onMouseMove');
+ el.setAttribute('cursor', 'mode', 'mouse');
+ assert.ok(updateSpy.called);
+ assert.ok(updateMouseEventListenersSpy.called);
+ });
+ });
});
suite('onCursorDown', function () {
diff --git a/tests/components/raycaster.test.js b/tests/components/raycaster.test.js
index f2aff9d55d3..6d9b9e08993 100644
--- a/tests/components/raycaster.test.js
+++ b/tests/components/raycaster.test.js
@@ -332,6 +332,24 @@ suite('raycaster', function () {
direction = raycaster.ray.direction;
assert.equal(direction.y, 1);
});
+
+ test('applies origin and direction without transformation if worldCoordinates enabled', function () {
+ el.setAttribute('raycaster', 'worldCoordinates', 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 () {