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 () {