-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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 pickingRadius
prop
#641
Conversation
We should not rerender picking buffer on every mouse event. |
We currently do so for hover and click. Hover does update a uniform |
src/lib/draw-and-pick.js
Outdated
pickedColor = pickResult.color; | ||
pickedLayer = pickResult.layer; | ||
pickedObjectIndex = pickResult.objectIndex; | ||
affectedLayers = pickedLayer ? [pickedLayer] : []; |
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.
any reason we move this logical out of pickFromBuffer() function now?
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.
The only logic that has been moved out is affectedLayers
. pickFromBuffer()
and pickFromBufferByBoundingBox()
now return similarly shaped objects.
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.
Yep, I already moved this out in the drag events PR because we don't always pick -- drag events that indicate the continuation of a drag event that started on a specific element don't need to re-pick. In that case, the user is assumed to be dragging an element around and so the element picked on dragstart
is returned without picking.
src/lib/layer-manager.js
Outdated
@@ -173,6 +170,26 @@ export default class LayerManager { | |||
}); | |||
} | |||
|
|||
pickLayerByBoundingBox({x, y, width, height}) { |
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.
I assume this function serves as an extension of the pickLayer() function right above it and it will replace pickLayer(), right?
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.
No. pickLayer()
is used by mouse event picking (returns a single info object) and pickLayerByBoundingBox()
is used by the query API (returns an array of info objects).
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.
Yeah, I knew that. pickLayer() and pickLayerByBoundingBox() share a lot of code, so do pickLayers() and pickLayersByBoundingBox() in draw-and-pick.js.
I'm wondering if mouse event picking can also be implemented via pickLayerByBoundingBox() with width and height both set to 1. We'll need to handle picking "mode" better.
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 is the plan per our last conversation:
add queryRenderedFeatures-style functionality to layer-manager to allow applications to handle events themselves and run picking on an arbitrary point/bbox. deck.gl will (later) use this feature for event-based picking under the hood
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 is overall a very nice improvement code wise and the radius prop should give some relief to picking with "fat fingers".
Main concern (in the RFC and in comments here) is on the bounding box selection @shaojingli will add to agenda for next decision meeting.
Ideally the PR would include
- some test (at least on the pickingColor utils, but we should be able to do a test picking, exercising this important code in a test case would be really valuable)
- a bench that tests picking perf on large areas (CPU traversal)
Those additions would really round out this PR. Regardless, before merging, we should have
- general documentation
- what's new
- CHANGELOG
updates in place as well.
examples/layer-browser/src/app.js
Outdated
@@ -102,6 +102,12 @@ class App extends PureComponent { | |||
setImmutableDataSamples(settings.immutable); | |||
} | |||
|
|||
_onQueryFeatures() { |
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.
Should we call it onQueryElements or onQueryObjects for consistency? We should review or documentation of the data
prop and use the same nomenclature for the individual objects...
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 isn't a handler, is it? I thought the intent here was to respond to an input event by calling queryElements
manually from within the input event handler. In that case, should just be queryElements() {}
, no 'on
'.
We agreed this path would serve as a complement to the onClick
/ onHover
callback-style interaction that we already have, to allow app devs more flexibility with how they handle input events.
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.
Agreed with @ibgreen this should be elements
.
examples/layer-browser/src/app.js
Outdated
@@ -220,6 +227,10 @@ class App extends PureComponent { | |||
{ this._renderMap() } | |||
{ !MAPBOX_ACCESS_TOKEN && this._renderNoTokenWarning() } | |||
<div id="control-panel"> | |||
<div> | |||
<h4 /> | |||
<button onClick={this._onQueryFeatures} >Query Rendered Features</button> |
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.
Features => Elements/Objects etc
src/lib/draw-and-pick.js
Outdated
@@ -206,11 +195,122 @@ export function pickLayers(gl, { | |||
} | |||
/* eslint-enable max-depth, max-statements */ | |||
|
|||
export function pickLayersByBoundingBox(gl, { |
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.
See my concern in the RFC. This will not catch hidden objects which I think it expected in an area selection operation. But maybe I am missing the use case. So depending on what we decide, we should ask ourselves if it is worth including a possibly flawed picking function.
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.
@ibgreen sorry, I'm having trouble finding this in the RFC. What does it mean for an element to be "hidden"? We should try to follow the DOM model for this -- if it's "turned off" / "hidden", it's not selectable. If its opacity is 0, it is selectable. I'm not clear on exactly how this maps to deck.gl (or even if it does), nor am I clear on how we'd handle the opacity === 0
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.
@ibgreen sorry, I'm having trouble finding this in the RFC.
As mentioned, this is discussed in the comments in the RFC. But I will explain again:
Let's say you have 2 objects. But you are so zoomed out that they all render on a single pixel. You select a generous bounding box around them, expected the list of selected objects to include both objects. But only one of them will detected by this picking approach.
Now repeat the thought experiment with more pixels and more objects. As long as there is a risk of one object overlapping with another, there is a risk that this picking method does not correctly identify all objects within the region, as it depends on a pixel representing that object actually being rendered into the framebuffer.
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.
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.
Right, I understand now. Maybe already covered in the RFC (reading now) but this would require us to maintain coordinates/bounds of all elements in model space...
src/lib/draw-and-pick.js
Outdated
|
||
// Traverse all pixels in picking results and find the one closest to the supplied | ||
// [deviceX, deviceY] | ||
let minDistanceToCenter = deviceRadius; |
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.
Would be nice to have a bench that tested the perf on this on a relatively big area.
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 was a concern of mine too...
src/lib/draw-and-pick.js
Outdated
if (pickResult) { | ||
const dx = deviceBox.x + col - deviceX; | ||
const dy = deviceBox.y + row - deviceY; | ||
const d = Math.sqrt(dx * dx + dy * dy); |
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.
Often possible and cheaper to not draw roots and just compare squares. E.g. calling the variable minSquaredDistance
.
src/react/deckgl.js
Outdated
@@ -146,7 +159,12 @@ export default class DeckGL extends React.Component { | |||
return; | |||
} | |||
const {event: {offsetX: x, offsetY: y}} = event; | |||
const selectedInfos = this.layerManager.pickLayer({x, y, mode: 'click'}); | |||
const selectedInfos = this.layerManager.pickLayer({ |
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.
Assign radius before the call and keep as one line?
src/react/deckgl.js
Outdated
@@ -161,7 +179,12 @@ export default class DeckGL extends React.Component { | |||
return; | |||
} | |||
const {event: {offsetX: x, offsetY: y}} = event; | |||
const selectedInfos = this.layerManager.pickLayer({x, y, mode: 'hover'}); | |||
const selectedInfos = this.layerManager.pickLayer({ |
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.
ditto
src/react/deckgl.js
Outdated
@@ -201,7 +224,12 @@ export default class DeckGL extends React.Component { | |||
} | |||
|
|||
if (mode) { | |||
const selectedInfos = this.layerManager.pickLayer({x, y, mode}); | |||
const selectedInfos = this.layerManager.pickLayer({ |
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.
ditto
src/react/deckgl.js
Outdated
@@ -78,6 +80,17 @@ export default class DeckGL extends React.Component { | |||
this._updateLayers(nextProps); | |||
} | |||
|
|||
/* Public API */ | |||
elementsInScope(topLeft, bottomRight) { |
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.
To support the review, write up the doc pages for the public APIs, either as temp comment here or in the doc structure.
src/lib/draw-and-pick.js
Outdated
|
||
for (let i = 0; i < pickedColors.length; i += 4) { | ||
// Decode picked color | ||
const pickedLayerIndex = pickedColors[i + 3] - 1; |
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.
Can we update the utilities to handle this, e.g:
const {pickedLayerIndex, pickedColor} = decodePickingColor(pickedColors[...])
src/lib/draw-and-pick.js
Outdated
const dx = deviceBox.x + col - deviceX; | ||
const dy = deviceBox.y + row - deviceY; | ||
const d = Math.sqrt(dx * dx + dy * dy); | ||
if (d <= minDistanceToCenter) { |
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 comparison should be implemented using distance squared, instead of distance to save that super expansive sqrt operations
src/lib/draw-and-pick.js
Outdated
const dx = deviceBox.x + col - deviceX; | ||
const dy = deviceBox.y + row - deviceY; | ||
const d = Math.sqrt(dx * dx + dy * dy); | ||
if (d <= minDistanceToCenter) { |
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.
distance comparison is better done using squared values to save on those super expansive Math.sqrt() operation
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.
Alternatively, is there an algo we can use that starts at the center of the bounding box and moves outward until it hits a pick, then bail? If we're only trying to find one result, closest to center..
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.
@ericsoco maybe out of the scope of this PR but a voronoi layer would do that, and it is almost free (a few lines tweak based on the scatterplot layer) in deck.gl.
src/lib/draw-and-pick.js
Outdated
if (d <= minDistanceToCenter) { | ||
minDistanceToCenter = d; | ||
closestResultToCenter = pickResult; | ||
pickResult.x = deviceBox.x + col; |
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.
x, y values are the location of the closest pixel of the picked element not the location of mouse pointer. We probably want to have better names for these two variables to help the people reading the code and also have them documented.
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.
Generally a very good PR. Let's present this PR to the team on our next frameworks meeting before we land it. During this time, feel free to add some tests and docs to repo
const deviceY = gl.canvas.height - y * pixelRatio; | ||
const deviceX = Math.round(x * pixelRatio); | ||
const deviceY = Math.round(gl.canvas.height - y * pixelRatio); | ||
const deviceRadius = Math.round(radius * pixelRatio); |
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.
In my local pass at this feature (you made it further than I did), I added deviceW
/ deviceH
, to enable marquee selection. Is there any benefit to a circular selection instead of a rectangular one? What's the specific use case for circular other than "fat-finger"?
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.
Ok, reading further I see you're using a box despite the name radius
here. Maybe note here that radius
really means "half-size"? Or maybe it's fine.
src/lib/draw-and-pick.js
Outdated
} | ||
/* eslint-enable max-depth */ | ||
|
||
return closestResultToCenter || { |
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.
So if I'm reading this correctly, we only return one picked element? What if the intent is to marquee-select all elements within bounds?
@@ -38,6 +38,7 @@ const propTypes = { | |||
effects: PropTypes.arrayOf(PropTypes.instanceOf(Effect)), | |||
gl: PropTypes.object, | |||
debug: PropTypes.bool, | |||
pickingRadius: PropTypes.number, |
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.
Out of curiosity, what use cases do you imagine for exposing this? Couldn't hurt, I suppose, just thinking out loud
Would you consider to support lasso selection with this elementsInScope() API later?
How do we handle picking for mouse position within the pickingRadius for more than one point now? |
pickingRadius
prop toDeckGL
component. The old picking behavior is equivalent topickingRadius: 0
. The closest object to the pointer withinpickingRadius
pixels is picked.