Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/components/cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,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 |
| hand | Filter WebXR `selectstart`/`selectend` events by controller handedness (`left` or `right`). When set, the cursor only responds to WebXR events from the matching controller. Used by laser-controls to prevent one controller's trigger from affecting another controller's hovered entity. | '' |
| mouseCursorStylesEnabled | Whether to show pointer cursor in `rayOrigin: mouse` mode when hovering over entity. | true |
| rayOrigin | Where the intersection ray is cast from (i.e. xrselect ,entity or mouse). `rayOrigin: mouse` is extremely useful for VR development on a mouse and keyboard. | entity
| upEvents | Array of additional events on the entity to *listen* to for triggering `mouseup`. | [] |
Expand Down
7 changes: 5 additions & 2 deletions docs/components/laser-controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ the hood, laser-controls sets all of the tracked controller components:
These controller components get activated if its respective controller is
connected and detected via the Gamepad API. Then the model of the actual
controller is used. laser-controls then configures the [cursor
component][cursor] for listen to the appropriate events and configures the
[raycaster component][raycaster] to draw the laser.
component][cursor] to listen to the appropriate events and passes the `hand`
property to ensure WebXR events are filtered by controller handedness. This
prevents one controller's trigger press from affecting another controller's
hovered entity. It also configures the [raycaster component][raycaster] to draw
the laser.

When the laser intersects with an entity, the length of the line gets truncated
to the distance to the intersection point.
Expand Down
12 changes: 12 additions & 0 deletions src/components/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export var Component = registerComponent('cursor', {
downEvents: {default: []},
fuse: {default: utils.device.isMobile()},
fuseTimeout: {default: 1500, min: 0},
hand: {default: ''},
mouseCursorStylesEnabled: {default: true},
upEvents: {default: []},
rayOrigin: {default: 'entity', oneOf: ['mouse', 'entity', 'xrselect']}
Expand Down Expand Up @@ -332,6 +333,12 @@ export var Component = registerComponent('cursor', {
* Trigger mousedown and keep track of the mousedowned entity.
*/
onCursorDown: function (evt) {
// Filter WebXR events by handedness when hand is configured.
if (evt.type === 'selectstart' && this.data.hand &&
evt.inputSource.handedness !== this.data.hand) {
return;
}

this.isCursorDown = true;
// Raycast again for touch.
if (this.data.rayOrigin === 'mouse' && evt.type === 'touchstart') {
Expand Down Expand Up @@ -370,6 +377,11 @@ export var Component = registerComponent('cursor', {
*/
onCursorUp: function (evt) {
if (!this.isCursorDown) { return; }
// Filter WebXR events by handedness when hand is configured.
if (evt && evt.type === 'selectend' && this.data.hand &&
evt.inputSource.handedness !== this.data.hand) {
return;
}
if (this.data.rayOrigin === 'xrselect' && this.activeXRInput !== evt.inputSource) { return; }

this.isCursorDown = false;
Expand Down
3 changes: 2 additions & 1 deletion src/components/laser-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ registerComponent('laser-controls', {
}

el.setAttribute('cursor', utils.extend({
fuse: false
fuse: false,
hand: data.hand
}, controllerConfig.cursor));
}

Expand Down
85 changes: 85 additions & 0 deletions tests/components/cursor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,3 +566,88 @@ suite('cursor + raycaster', function () {
parentEl.innerHTML = '<a-entity cursor raycaster="objects: .clickable"></a-entity>';
});
});

suite('cursor WebXR handedness filtering', function () {
var component;
var el;
var intersectedEl;

setup(function (done) {
var cameraEl = entityFactory();
el = document.createElement('a-entity');
intersectedEl = document.createElement('a-entity');
cameraEl.setAttribute('camera', 'active: true');
el.setAttribute('cursor', 'hand: right');
el.addEventListener('componentinitialized', function (evt) {
if (evt.detail.name !== 'cursor') { return; }
component = el.components.cursor;
done();
});
cameraEl.appendChild(el);
});

suite('onCursorDown', function () {
test('ignores selectstart from non-matching hand', function () {
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
component.onCursorDown({
type: 'selectstart',
inputSource: {handedness: 'left'}
});
assert.isFalse(twoWayEmitSpy.called);
assert.isFalse(component.isCursorDown);
});

test('processes selectstart from matching hand', function () {
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
component.intersectedEl = intersectedEl;
component.onCursorDown({
type: 'selectstart',
inputSource: {handedness: 'right'}
});
assert.isTrue(twoWayEmitSpy.calledWith('mousedown'));
assert.isTrue(component.isCursorDown);
});

test('processes non-WebXR events regardless of hand setting', function () {
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
component.intersectedEl = intersectedEl;
component.onCursorDown({type: 'mousedown'});
assert.isTrue(twoWayEmitSpy.calledWith('mousedown'));
assert.isTrue(component.isCursorDown);
});
});

suite('onCursorUp', function () {
test('ignores selectend from non-matching hand', function () {
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
component.isCursorDown = true;
component.onCursorUp({
type: 'selectend',
inputSource: {handedness: 'left'}
});
assert.isFalse(twoWayEmitSpy.called);
assert.isTrue(component.isCursorDown);
});

test('processes selectend from matching hand', function () {
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
component.isCursorDown = true;
component.intersectedEl = intersectedEl;
component.onCursorUp({
type: 'selectend',
inputSource: {handedness: 'right'}
});
assert.isTrue(twoWayEmitSpy.calledWith('mouseup'));
assert.isFalse(component.isCursorDown);
});

test('processes non-WebXR events regardless of hand setting', function () {
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
component.isCursorDown = true;
component.intersectedEl = intersectedEl;
component.onCursorUp({type: 'mouseup'});
assert.isTrue(twoWayEmitSpy.calledWith('mouseup'));
assert.isFalse(component.isCursorDown);
});
});
});