Skip to content

Commit 0d5e1e2

Browse files
Fix cursor component triggering clicks on wrong controller's hovered entity in VR (#5782)
1 parent d07b7f9 commit 0d5e1e2

File tree

5 files changed

+105
-3
lines changed

5 files changed

+105
-3
lines changed

docs/components/cursor.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ AFRAME.registerComponent('cursor-listener', {
8787
| downEvents | Array of additional events on the entity to *listen* to for triggering `mousedown` (e.g., `triggerdown` for vive-controls). | [] |
8888
| fuse | Whether cursor is fuse-based. | false on desktop, true on mobile |
8989
| fuseTimeout | How long to wait (in milliseconds) before triggering a fuse-based click event. | 1500 |
90+
| 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. | '' |
9091
| mouseCursorStylesEnabled | Whether to show pointer cursor in `rayOrigin: mouse` mode when hovering over entity. | true |
9192
| 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
9293
| upEvents | Array of additional events on the entity to *listen* to for triggering `mouseup`. | [] |

docs/components/laser-controls.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ the hood, laser-controls sets all of the tracked controller components:
3333
These controller components get activated if its respective controller is
3434
connected and detected via the Gamepad API. Then the model of the actual
3535
controller is used. laser-controls then configures the [cursor
36-
component][cursor] for listen to the appropriate events and configures the
37-
[raycaster component][raycaster] to draw the laser.
36+
component][cursor] to listen to the appropriate events and passes the `hand`
37+
property to ensure WebXR events are filtered by controller handedness. This
38+
prevents one controller's trigger press from affecting another controller's
39+
hovered entity. It also configures the [raycaster component][raycaster] to draw
40+
the laser.
3841

3942
When the laser intersects with an entity, the length of the line gets truncated
4043
to the distance to the intersection point.

src/components/cursor.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export var Component = registerComponent('cursor', {
4949
downEvents: {default: []},
5050
fuse: {default: utils.device.isMobile()},
5151
fuseTimeout: {default: 1500, min: 0},
52+
hand: {default: ''},
5253
mouseCursorStylesEnabled: {default: true},
5354
upEvents: {default: []},
5455
rayOrigin: {default: 'entity', oneOf: ['mouse', 'entity', 'xrselect']}
@@ -332,6 +333,12 @@ export var Component = registerComponent('cursor', {
332333
* Trigger mousedown and keep track of the mousedowned entity.
333334
*/
334335
onCursorDown: function (evt) {
336+
// Filter WebXR events by handedness when hand is configured.
337+
if (evt.type === 'selectstart' && this.data.hand &&
338+
evt.inputSource.handedness !== this.data.hand) {
339+
return;
340+
}
341+
335342
this.isCursorDown = true;
336343
// Raycast again for touch.
337344
if (this.data.rayOrigin === 'mouse' && evt.type === 'touchstart') {
@@ -370,6 +377,11 @@ export var Component = registerComponent('cursor', {
370377
*/
371378
onCursorUp: function (evt) {
372379
if (!this.isCursorDown) { return; }
380+
// Filter WebXR events by handedness when hand is configured.
381+
if (evt && evt.type === 'selectend' && this.data.hand &&
382+
evt.inputSource.handedness !== this.data.hand) {
383+
return;
384+
}
373385
if (this.data.rayOrigin === 'xrselect' && this.activeXRInput !== evt.inputSource) { return; }
374386

375387
this.isCursorDown = false;

src/components/laser-controls.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ registerComponent('laser-controls', {
6363
}
6464

6565
el.setAttribute('cursor', utils.extend({
66-
fuse: false
66+
fuse: false,
67+
hand: data.hand
6768
}, controllerConfig.cursor));
6869
}
6970

tests/components/cursor.test.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,3 +566,88 @@ suite('cursor + raycaster', function () {
566566
parentEl.innerHTML = '<a-entity cursor raycaster="objects: .clickable"></a-entity>';
567567
});
568568
});
569+
570+
suite('cursor WebXR handedness filtering', function () {
571+
var component;
572+
var el;
573+
var intersectedEl;
574+
575+
setup(function (done) {
576+
var cameraEl = entityFactory();
577+
el = document.createElement('a-entity');
578+
intersectedEl = document.createElement('a-entity');
579+
cameraEl.setAttribute('camera', 'active: true');
580+
el.setAttribute('cursor', 'hand: right');
581+
el.addEventListener('componentinitialized', function (evt) {
582+
if (evt.detail.name !== 'cursor') { return; }
583+
component = el.components.cursor;
584+
done();
585+
});
586+
cameraEl.appendChild(el);
587+
});
588+
589+
suite('onCursorDown', function () {
590+
test('ignores selectstart from non-matching hand', function () {
591+
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
592+
component.onCursorDown({
593+
type: 'selectstart',
594+
inputSource: {handedness: 'left'}
595+
});
596+
assert.isFalse(twoWayEmitSpy.called);
597+
assert.isFalse(component.isCursorDown);
598+
});
599+
600+
test('processes selectstart from matching hand', function () {
601+
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
602+
component.intersectedEl = intersectedEl;
603+
component.onCursorDown({
604+
type: 'selectstart',
605+
inputSource: {handedness: 'right'}
606+
});
607+
assert.isTrue(twoWayEmitSpy.calledWith('mousedown'));
608+
assert.isTrue(component.isCursorDown);
609+
});
610+
611+
test('processes non-WebXR events regardless of hand setting', function () {
612+
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
613+
component.intersectedEl = intersectedEl;
614+
component.onCursorDown({type: 'mousedown'});
615+
assert.isTrue(twoWayEmitSpy.calledWith('mousedown'));
616+
assert.isTrue(component.isCursorDown);
617+
});
618+
});
619+
620+
suite('onCursorUp', function () {
621+
test('ignores selectend from non-matching hand', function () {
622+
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
623+
component.isCursorDown = true;
624+
component.onCursorUp({
625+
type: 'selectend',
626+
inputSource: {handedness: 'left'}
627+
});
628+
assert.isFalse(twoWayEmitSpy.called);
629+
assert.isTrue(component.isCursorDown);
630+
});
631+
632+
test('processes selectend from matching hand', function () {
633+
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
634+
component.isCursorDown = true;
635+
component.intersectedEl = intersectedEl;
636+
component.onCursorUp({
637+
type: 'selectend',
638+
inputSource: {handedness: 'right'}
639+
});
640+
assert.isTrue(twoWayEmitSpy.calledWith('mouseup'));
641+
assert.isFalse(component.isCursorDown);
642+
});
643+
644+
test('processes non-WebXR events regardless of hand setting', function () {
645+
var twoWayEmitSpy = this.sinon.spy(component, 'twoWayEmit');
646+
component.isCursorDown = true;
647+
component.intersectedEl = intersectedEl;
648+
component.onCursorUp({type: 'mouseup'});
649+
assert.isTrue(twoWayEmitSpy.calledWith('mouseup'));
650+
assert.isFalse(component.isCursorDown);
651+
});
652+
});
653+
});

0 commit comments

Comments
 (0)