Skip to content

Commit

Permalink
Add pickingRadius prop (visgl#641)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress authored and Lezhi Li committed May 31, 2017
1 parent abe0ce6 commit 370fc9a
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 44 deletions.
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);

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,
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

0 comments on commit 370fc9a

Please sign in to comment.