Skip to content
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

Merged
merged 3 commits into from
May 17, 2017
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Ref: http://keepachangelog.com/en/0.3.0/

## Beta Releases

[TBD]
- NEW: `pickingRadius` prop on the `DeckGL` component, enables more tolerant click and hover interaction.

### deck.gl v4.1.0-alpha.1

- PERFORMANCE: Compiled are now cached for reuse so that same shaders are not recompiled for the same type of layers (#613)
Expand Down
7 changes: 7 additions & 0 deletions docs/api-reference/deckgl.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ updated properties derived from the current application state.

Css styles for the deckgl-canvas.

##### `pickingRadius` (Number, optional)

Extra pixels around the pointer to include while picking.
This is helpful when rendered objects are difficult to target, for example
irregularly shaped icons, small moving circles or interaction by touch.
Default `0`.

##### `pixelRatio` (Number, optional)

Will use device ratio by default.
Expand Down
6 changes: 4 additions & 2 deletions examples/layer-browser/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class App extends PureComponent {
// immutable: false,
// Effects are experimental for now. Will be enabled in the future
// effects: false,
separation: 0
separation: 0,
pickingRadius: 0
// the rotation controls works only for layers in
// meter offset projection mode. They are commented out
// here since layer browser currently only have one layer
Expand Down Expand Up @@ -175,7 +176,7 @@ class App extends PureComponent {
}

_renderMap() {
const {width, height, mapViewState, settings: {effects}} = this.state;
const {width, height, mapViewState, settings: {effects, pickingRadius}} = this.state;
return (
<MapboxGLMap
mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN || 'no_token'}
Expand All @@ -189,6 +190,7 @@ class App extends PureComponent {
id="default-deckgl-overlay"
width={width} height={height}
{...mapViewState}
pickingRadius={pickingRadius}
onWebGLInitialized={ this._onWebGLInitialized }
onLayerHover={ this._onHover }
onLayerClick={ this._onClick }
Expand Down
2 changes: 1 addition & 1 deletion examples/layer-browser/src/components/layer-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default class LayerControls extends PureComponent {
if (propType && Number.isFinite(propType.max)) {
max = propType.max;

} else if (/radiusScale|elevationScale|width|height|pixel|size|miter/i.test(settingName) &&
} else if (/radius|scale|width|height|pixel|size|miter/i.test(settingName) &&

(/^((?!scale).)*$/).test(settingName)) {
max = 100;
Expand Down
111 changes: 78 additions & 33 deletions src/lib/draw-and-pick.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ export function drawLayers({layers, pass}) {
export function pickLayers(gl, {
layers,
pickingFBO,
uniforms = {},
x,
y,
radius,
viewport,
mode,
lastPickedInfo
Expand All @@ -71,8 +71,9 @@ export function pickLayers(gl, {
// And compensate for pixelRatio
const pixelRatio = typeof window !== 'undefined' ?
window.devicePixelRatio : 1;
const deviceX = x * pixelRatio;
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);
Copy link
Contributor

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"?

Copy link
Contributor

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.


let pickedColor;
let pickedLayer;
Expand All @@ -99,12 +100,14 @@ export function pickLayers(gl, {
layers,
pickingFBO,
deviceX,
deviceY
deviceY,
deviceRadius
});

pickedColor = pickInfo.pickedColor;
pickedLayer = pickInfo.pickedLayer;
pickedObjectIndex = pickInfo.pickedObjectIndex;
affectedLayers = pickInfo.affectedLayers;
affectedLayers = pickedLayer ? [pickedLayer] : [];

if (mode === 'hover') {
// only invoke onHover events if picked object has changed
Expand Down Expand Up @@ -152,21 +155,7 @@ export function pickLayers(gl, {
info.picked = true;
}

// Walk up the composite chain and find the owner of the event
// sublayers are never directly exposed to the user
while (layer && info) {
// For a composite layer, sourceLayer will point to the sublayer
// where the event originates from.
// It provides additional context for the composite layer's
// getPickingInfo() method to populate the info object
const sourceLayer = info.layer || layer;
info.layer = layer;
// layer.pickLayer() function requires a non-null ```layer.state```
// object to funtion properly. So the layer refereced here
// must be the "current" layer, not an "out-dated" / "invalidated" layer
info = layer.pickLayer({info, mode, sourceLayer});
layer = layer.parentLayer;
}
info = getLayerPickingInfo({layer, info, mode});

// This guarantees that there will be only one copy of info for
// one composite layer
Expand Down Expand Up @@ -204,22 +193,33 @@ export function pickLayers(gl, {

return unhandledPickInfos;
}
/* eslint-enable max-depth, max-statements */

/**
* Pick at a specified pixel with a tolerance radius
* Returns the closest object to the pixel in shape `{pickedColor, pickedLayer, pickedObjectIndex}`
*/
function pickFromBuffer(gl, {
layers,
pickingFBO,
deviceX,
deviceY
deviceY,
deviceRadius
}) {

// Create a box of size `radius * 2 + 1` centered at [deviceX, deviceY]
const x = Math.max(0, deviceX - deviceRadius);
const y = Math.max(0, deviceY - deviceRadius);
const width = Math.min(pickingFBO.width, deviceX + deviceRadius) - x + 1;
const height = Math.min(pickingFBO.height, deviceY + deviceRadius) - y + 1;

// TODO - just return glContextWithState once luma updates
// Make sure we clear scissor test and fbo bindings in case of exceptions
// We are only interested in one pixel, no need to render anything else
// Note that the callback here is called synchronously.
return glContextWithState(gl, {
frameBuffer: pickingFBO,
framebuffer: pickingFBO,
scissorTest: {x: deviceX, y: deviceY, w: 1, h: 1}
scissorTest: {x, y, w: width, h: height}
}, () => {

// Clear the frame buffer
Expand Down Expand Up @@ -251,23 +251,50 @@ function pickFromBuffer(gl, {
});

// Read color in the central pixel, to be mapped with picking colors
const pickedColor = new Uint8Array(4);
gl.readPixels(deviceX, deviceY, 1, 1, GL.RGBA, GL.UNSIGNED_BYTE, pickedColor);
const pickedColors = new Uint8Array(width * height * 4);
gl.readPixels(x, y, width, height, GL.RGBA, GL.UNSIGNED_BYTE, pickedColors);

// restore blend mode
setBlendMode(gl, oldBlendMode);

// Decode picked color
const pickedLayerIndex = pickedColor[3] - 1;
const pickedLayer = pickedLayerIndex >= 0 ? layers[pickedLayerIndex] : null;
return {
pickedColor,
pickedLayer,
pickedObjectIndex: pickedLayer ? pickedLayer.decodePickingColor(pickedColor) : -1,
affectedLayers: pickedLayer ? [pickedLayer] : []
// Traverse all pixels in picking results and find the one closest to the supplied
// [deviceX, deviceY]
let minSquareDistanceToCenter = deviceRadius * deviceRadius;
let closestResultToCenter = null;
let i = 0;

for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
// Decode picked layer from color
const pickedLayerIndex = pickedColors[i + 3] - 1;

if (pickedLayerIndex >= 0) {
const dx = col + x - deviceX;
const dy = row + y - deviceY;
const d2 = dx * dx + dy * dy;

if (d2 <= minSquareDistanceToCenter) {
minSquareDistanceToCenter = d2;

// Decode picked object index from color
const pickedColor = pickedColors.slice(i, i + 4);
const pickedLayer = layers[pickedLayerIndex];
const pickedObjectIndex = pickedLayer.decodePickingColor(pickedColor);
closestResultToCenter = {pickedColor, pickedLayer, pickedObjectIndex};
}
}
i += 4;
}
}

return closestResultToCenter || {
pickedColor: EMPTY_PIXEL,
pickedLayer: null,
pickedObjectIndex: -1
};
});
}
/* eslint-enable max-depth, max-statements */

function createInfo(pixel, viewport) {
// Assign a number of potentially useful props to the "info" object
Expand All @@ -281,3 +308,21 @@ function createInfo(pixel, viewport) {
lngLat: viewport.unproject(pixel)
};
}

// Walk up the layer composite chain to populate the info object
function getLayerPickingInfo({layer, info, mode}) {
while (layer && info) {
// For a composite layer, sourceLayer will point to the sublayer
// where the event originates from.
// It provides additional context for the composite layer's
// getPickingInfo() method to populate the info object
const sourceLayer = info.layer || layer;
info.layer = layer;
// layer.pickLayer() function requires a non-null ```layer.state```
// object to funtion properly. So the layer refereced here
// must be the "current" layer, not an "out-dated" / "invalidated" layer
info = layer.pickLayer({info, mode, sourceLayer});
layer = layer.parentLayer;
}
return info;
}
7 changes: 2 additions & 5 deletions src/lib/layer-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export default class LayerManager {
return this;
}

pickLayer({x, y, mode}) {
pickLayer({x, y, mode, radius = 0}) {
const {gl, uniforms} = this.context;

// Set up a frame buffer if needed
Expand All @@ -161,10 +161,7 @@ export default class LayerManager {
return pickLayers(gl, {
x,
y,
uniforms: {
renderPickingBuffer: true,
picking_uEnable: true
},
radius,
layers: this.layers,
mode,
viewport: this.context.viewport,
Expand Down
11 changes: 8 additions & 3 deletions src/react/deckgl.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const propTypes = {
effects: PropTypes.arrayOf(PropTypes.instanceOf(Effect)),
gl: PropTypes.object,
debug: PropTypes.bool,
pickingRadius: PropTypes.number,
Copy link
Contributor

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

viewport: PropTypes.instanceOf(Viewport),
onWebGLInitialized: PropTypes.func,
onAfterRender: PropTypes.func,
Expand All @@ -52,6 +53,7 @@ const propTypes = {
const defaultProps = {
id: 'deckgl-overlay',
debug: false,
pickingRadius: 0,
gl: null,
effects: [],
onWebGLInitialized: noop,
Expand Down Expand Up @@ -146,7 +148,8 @@ 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 radius = this.props.pickingRadius;
const selectedInfos = this.layerManager.pickLayer({x, y, radius, mode: 'click'});
if (selectedInfos.length) {
const firstInfo = selectedInfos.find(info => info.index >= 0);
// Event.event holds the original MouseEvent object
Expand All @@ -161,7 +164,8 @@ 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 radius = this.props.pickingRadius;
const selectedInfos = this.layerManager.pickLayer({x, y, radius, mode: 'hover'});
if (selectedInfos.length) {
const firstInfo = selectedInfos.find(info => info.index >= 0);
// Event.event holds the original MouseEvent object
Expand Down Expand Up @@ -201,7 +205,8 @@ export default class DeckGL extends React.Component {
}

if (mode) {
const selectedInfos = this.layerManager.pickLayer({x, y, mode});
const radius = this.props.pickingRadius;
const selectedInfos = this.layerManager.pickLayer({x, y, radius, mode});
if (selectedInfos.length) {
const firstInfo = selectedInfos.find(info => info.index >= 0);
// Event.event holds the original MouseEvent object
Expand Down