-
-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add user-controlled camera and canvas to cursor component. #4983
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,7 +52,9 @@ module.exports.Component = registerComponent('cursor', { | |
fuseTimeout: {default: 1500, min: 0}, | ||
mouseCursorStylesEnabled: {default: true}, | ||
upEvents: {default: []}, | ||
rayOrigin: {default: 'entity', oneOf: ['mouse', 'entity']} | ||
rayOrigin: {default: 'entity', oneOf: ['mouse', 'entity']}, | ||
canvas: {default: 'auto', oneOf: ['auto', 'user']}, | ||
camera: {default: 'auto', oneOf: ['auto', 'user']} | ||
}, | ||
|
||
init: function () { | ||
|
@@ -64,9 +66,18 @@ module.exports.Component = registerComponent('cursor', { | |
this.canvasBounds = document.body.getBoundingClientRect(); | ||
this.isCursorDown = false; | ||
|
||
// expose camera and cursor to user if required. | ||
if (this.data.camera === 'user') { | ||
this.camera = this.el.sceneEl.camera; | ||
} | ||
if (this.data.canvas === 'user') { | ||
this.canvas = this.el.sceneEl.canvas; | ||
this.canvasBounds = this.canvas.getBoundingClientRect(); | ||
} | ||
|
||
// Debounce. | ||
this.updateCanvasBounds = utils.debounce(function updateCanvasBounds () { | ||
self.canvasBounds = self.el.sceneEl.canvas.getBoundingClientRect(); | ||
self.canvasBounds = self.getCanvas().getBoundingClientRect(); | ||
}, 500); | ||
|
||
this.eventDetail = {}; | ||
|
@@ -79,6 +90,12 @@ module.exports.Component = registerComponent('cursor', { | |
this.onIntersectionCleared = bind(this.onIntersectionCleared, this); | ||
this.onMouseMove = bind(this.onMouseMove, this); | ||
this.onEnterVR = bind(this.onEnterVR, this); | ||
|
||
// Variables used in raycasting. One set of these needed per-cursor | ||
// instance. | ||
this.direction = new THREE.Vector3(); | ||
this.origin = new THREE.Vector3(); | ||
this.rayCasterConfig = {origin: this.origin, direction: this.direction}; | ||
}, | ||
|
||
update: function (oldData) { | ||
|
@@ -109,18 +126,18 @@ module.exports.Component = registerComponent('cursor', { | |
var el = this.el; | ||
var self = this; | ||
|
||
function addCanvasListeners () { | ||
canvas = el.sceneEl.canvas; | ||
const addCanvasListeners = () => { | ||
canvas = this.getCanvas(); | ||
if (data.downEvents.length || data.upEvents.length) { return; } | ||
CANVAS_EVENTS.DOWN.forEach(function (downEvent) { | ||
canvas.addEventListener(downEvent, self.onCursorDown); | ||
}); | ||
CANVAS_EVENTS.UP.forEach(function (upEvent) { | ||
canvas.addEventListener(upEvent, self.onCursorUp); | ||
}); | ||
} | ||
}; | ||
|
||
canvas = el.sceneEl.canvas; | ||
canvas = this.getCanvas(); | ||
if (canvas) { | ||
addCanvasListeners(); | ||
} else { | ||
|
@@ -152,7 +169,7 @@ module.exports.Component = registerComponent('cursor', { | |
var el = this.el; | ||
var self = this; | ||
|
||
canvas = el.sceneEl.canvas; | ||
canvas = this.getCanvas(); | ||
if (canvas && !data.downEvents.length && !data.upEvents.length) { | ||
CANVAS_EVENTS.DOWN.forEach(function (downEvent) { | ||
canvas.removeEventListener(downEvent, self.onCursorDown); | ||
|
@@ -184,7 +201,7 @@ module.exports.Component = registerComponent('cursor', { | |
var canvas; | ||
var el = this.el; | ||
|
||
canvas = el.sceneEl.canvas; | ||
canvas = this.getCanvas(); | ||
canvas.removeEventListener('mousemove', this.onMouseMove); | ||
canvas.removeEventListener('touchmove', this.onMouseMove); | ||
el.setAttribute('raycaster', 'useWorldCoordinates', false); | ||
|
@@ -196,14 +213,11 @@ module.exports.Component = registerComponent('cursor', { | |
}, | ||
|
||
onMouseMove: (function () { | ||
var direction = new THREE.Vector3(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These variables not meant to store state but used for local calculations and cached to prevent unnecessary instantiations. Why do they need to be public now? They should also work for multiple cursors if the variables purpose hasn't changed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The more variables exposed, the larger the API surface area, the greater maintenance burden (can't remove / rename variables without thinking about 3rd parties) and increased possibility of misuse. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They don't need to be public. The issue is that they are not only used transiently, but actually passed by reference into the raycaster and used there.
Result is that if you have two separate cursor components on the same scene, they share these variables, and end up scribblng over each others' raycaster parameters. It was a bit of a surprise to me that this happened, but it definitely did (took me quite a long time to figure out what was going on, as it was so unexpected - can create a repro if you want to take a look as exactly what happens here). This was a simple solution that definitely works, because it gives you a unique instance of origin & direction per cursor instance. I don't know of another way to solve this that doesn't have perf implications (e.g. copying the value of the origin & direction vectors to pass them into the raycaster). Can you suggest a better way to solve this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would giving them names that indicate they are internal, e.g. Or can we make them private? this.#origin, this.#direction? (apparently this is brand new in ES2022, I only just found out about this option) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields (edited - initial reading suggested this was introduced in ECMA2019, but in fact it appears to be new in ECMA2022). |
||
var mouse = new THREE.Vector2(); | ||
var origin = new THREE.Vector3(); | ||
var rayCasterConfig = {origin: origin, direction: direction}; | ||
|
||
return function (evt) { | ||
var bounds = this.canvasBounds; | ||
var camera = this.el.sceneEl.camera; | ||
var camera = this.getCamera(); | ||
var left; | ||
var point; | ||
var top; | ||
|
@@ -224,16 +238,16 @@ module.exports.Component = registerComponent('cursor', { | |
mouse.y = -(top / bounds.height) * 2 + 1; | ||
|
||
if (camera && camera.isPerspectiveCamera) { | ||
origin.setFromMatrixPosition(camera.matrixWorld); | ||
direction.set(mouse.x, mouse.y, 0.5).unproject(camera).sub(origin).normalize(); | ||
this.origin.setFromMatrixPosition(camera.matrixWorld); | ||
this.direction.set(mouse.x, mouse.y, 0.5).unproject(camera).sub(this.origin).normalize(); | ||
} else if (camera && camera.isOrthographicCamera) { | ||
origin.set(mouse.x, mouse.y, (camera.near + camera.far) / (camera.near - camera.far)).unproject(camera); // set origin in plane of camera | ||
direction.set(0, 0, -1).transformDirection(camera.matrixWorld); | ||
this.origin.set(mouse.x, mouse.y, (camera.near + camera.far) / (camera.near - camera.far)).unproject(camera); // set origin in plane of camera | ||
this.direction.set(0, 0, -1).transformDirection(camera.matrixWorld); | ||
} else { | ||
console.error('AFRAME.Raycaster: Unsupported camera type: ' + camera.type); | ||
} | ||
|
||
this.el.setAttribute('raycaster', rayCasterConfig); | ||
this.el.setAttribute('raycaster', this.rayCasterConfig); | ||
if (evt.type === 'touchmove') { evt.preventDefault(); } | ||
}; | ||
})(), | ||
|
@@ -359,7 +373,7 @@ module.exports.Component = registerComponent('cursor', { | |
this.twoWayEmit(EVENTS.MOUSEENTER); | ||
|
||
if (this.data.mouseCursorStylesEnabled && this.data.rayOrigin === 'mouse') { | ||
this.el.sceneEl.canvas.classList.add(CANVAS_HOVER_CLASS); | ||
this.getCanvas().classList.add(CANVAS_HOVER_CLASS); | ||
} | ||
|
||
// Begin fuse if necessary. | ||
|
@@ -388,7 +402,7 @@ module.exports.Component = registerComponent('cursor', { | |
this.twoWayEmit(EVENTS.MOUSELEAVE); | ||
|
||
if (this.data.mouseCursorStylesEnabled && this.data.rayOrigin === 'mouse') { | ||
this.el.sceneEl.canvas.classList.remove(CANVAS_HOVER_CLASS); | ||
this.getCanvas().classList.remove(CANVAS_HOVER_CLASS); | ||
} | ||
|
||
// Unset intersected entity (after emitting the event). | ||
|
@@ -425,5 +439,21 @@ module.exports.Component = registerComponent('cursor', { | |
|
||
this.intersectedEventDetail.intersection = intersection; | ||
intersectedEl.emit(evtName, this.intersectedEventDetail); | ||
}, | ||
|
||
getCanvas: function () { | ||
if (this.data.canvas === 'user') { | ||
return this.canvas; | ||
} else { | ||
return this.el.sceneEl.canvas; | ||
} | ||
}, | ||
|
||
getCamera: function () { | ||
if (this.data.camera === 'user') { | ||
return this.camera; | ||
} else { | ||
return this.el.sceneEl.camera; | ||
} | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This API is strange. Public variables are set or not depending on the value of a component property. Purpose is not clear either reading the schema or the docs. Probably feels too ad-hoc to your use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is a bit ad hoc.
The reason for adding this to the interface was a desire to maintain complete back compatibility.
An example of a problem that could be caused by not putting this control on the interface is the following:
In terms of the documentation, I think it would help a lot if I had an example to link to. As per notes above, I'm hoping to re-use the component I have built as an example, but still TBD whether the owners of that code will be willing to open source it.
In short, I think a config option is necessary if we are to avoid breaking existing apps. OTOH, the fix to make cursor work with an orthographic camera isn't even released yet (in master but not 1.2.0), so another option might be to simply document that fact that if you want to switch which camera is used for a cursor, you need to explicitly update
this.camera
in the cursor component.