Skip to content

Commit

Permalink
Clean up picking code
Browse files Browse the repository at this point in the history
  • Loading branch information
Xiaoji Chen committed Oct 1, 2019
1 parent 2f7eb47 commit 807721d
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 165 deletions.
126 changes: 29 additions & 97 deletions modules/core/src/lib/deck-picker.js
Expand Up @@ -30,7 +30,7 @@ import GL from '@luma.gl/constants';
import assert from '../utils/assert';
import PickLayersPass from '../passes/pick-layers-pass';
import {getClosestObject, getUniqueObjects} from './picking/query-object';
import {processPickInfo, getLayerPickingInfo} from './picking/pick-info';
import {processPickInfo, callLayerPickingCallbacks, getLayerPickingInfo} from './picking/pick-info';

export default class DeckPicker {
constructor(gl) {
Expand Down Expand Up @@ -67,51 +67,19 @@ export default class DeckPicker {
}

// Pick the closest info at given coordinate
pickObject({
x,
y,
mode,
radius = 0,
layers,
viewports,
activateViewport,
unproject3D,
depth = 1,
event = null
}) {
pickObject(opts) {
// Allow layers to access the event
this.pickingEvent = event;
const result = this.pickClosestObject({
// User params
x,
y,
radius,
layers,
mode,
depth,
unproject3D,
// Injected params
viewports,
onViewportActive: activateViewport
});
this.pickingEvent = opts.event;
const result = this._pickClosestObject(opts);

// Clear the current event
this.pickingEvent = null;
return result;
}

// Get all unique infos within a bounding box
pickObjects({x, y, width, height, layers, viewports, activateViewport}) {
return this.pickVisibleObjects({
x,
y,
width,
height,
layers,
mode: 'pickObjects',
viewports,
onViewportActive: activateViewport
});
pickObjects(opts) {
return this._pickVisibleObjects(opts);
}

// Returns a new picking info object by assuming the last picked object is still picked
Expand All @@ -136,7 +104,7 @@ export default class DeckPicker {
}

// Private
updatePickingBuffer() {
_resizeBuffer() {
const {gl} = this;
// Create a frame buffer if not already available
if (!this.pickingFBO) {
Expand All @@ -159,18 +127,18 @@ export default class DeckPicker {

// Pick the closest object at the given (x,y) coordinate
// eslint-disable-next-line max-statements,complexity
pickClosestObject({
_pickClosestObject({
layers,
viewports,
x,
y,
radius,
radius = 0,
depth = 1,
mode,
mode = 'query',
unproject3D,
onViewportActive
}) {
this.updatePickingBuffer();
this._resizeBuffer();
// Convert from canvas top-left to WebGL bottom-left coordinates
// Top-left coordinates [x, y] to bottom-left coordinates [deviceX, deviceY]
// And compensate for pixelRatio
Expand All @@ -183,7 +151,7 @@ export default class DeckPicker {

const deviceRadius = Math.round(radius * pixelRatio);
const {width, height} = this.pickingFBO;
const deviceRect = this.getPickingRect({
const deviceRect = this._getPickingRect({
deviceX: devicePixel[0],
deviceY: devicePixel[1],
deviceRadius,
Expand All @@ -198,7 +166,7 @@ export default class DeckPicker {
for (let i = 0; i < depth; i++) {
const pickedColors =
deviceRect &&
this.drawAndSamplePickingBuffer({
this._drawAndSample({
layers,
viewports,
onViewportActive,
Expand All @@ -217,7 +185,7 @@ export default class DeckPicker {

let z;
if (pickInfo.pickedLayer && unproject3D && this.depthFBO) {
const zValues = this.drawAndSamplePickingBuffer({
const zValues = this._drawAndSample({
layers: [pickInfo.pickedLayer],
viewports,
onViewportActive,
Expand Down Expand Up @@ -251,7 +219,7 @@ export default class DeckPicker {
pixelRatio
});

const processedPickInfos = this.callLayerPickingCallbacks(infos, mode);
const processedPickInfos = callLayerPickingCallbacks(infos, mode, this.pickingEvent);

if (processedPickInfos) {
processedPickInfos.forEach(info => result.push(info));
Expand All @@ -272,8 +240,17 @@ export default class DeckPicker {
}

// Pick all objects within the given bounding box
pickVisibleObjects({layers, viewports, x, y, width, height, mode, onViewportActive}) {
this.updatePickingBuffer();
_pickVisibleObjects({
layers,
viewports,
x,
y,
width = 1,
height = 1,
mode = 'query',
onViewportActive
}) {
this._resizeBuffer();
// Convert from canvas top-left to WebGL bottom-left coordinates
// And compensate for pixelRatio
const pixelRatio = cssToDeviceRatio(this.gl);
Expand All @@ -296,7 +273,7 @@ export default class DeckPicker {
height: deviceTop - deviceBottom
};

const pickedColors = this.drawAndSamplePickingBuffer({
const pickedColors = this._drawAndSample({
layers,
viewports,
onViewportActive,
Expand Down Expand Up @@ -332,14 +309,7 @@ export default class DeckPicker {
}

// returns pickedColor or null if no pickable layers found.
drawAndSamplePickingBuffer({
layers,
viewports,
onViewportActive,
deviceRect,
redrawReason,
pickZ
}) {
_drawAndSample({layers, viewports, onViewportActive, deviceRect, redrawReason, pickZ}) {
assert(deviceRect);
assert(Number.isFinite(deviceRect.width) && deviceRect.width > 0, '`width` must be > 0');
assert(Number.isFinite(deviceRect.height) && deviceRect.height > 0, '`height` must be > 0');
Expand Down Expand Up @@ -382,7 +352,7 @@ export default class DeckPicker {

// Calculate a picking rect centered on deviceX and deviceY and clipped to device
// Returns null if pixel is outside of device
getPickingRect({deviceX, deviceY, deviceRadius, deviceWidth, deviceHeight}) {
_getPickingRect({deviceX, deviceY, deviceRadius, deviceWidth, deviceHeight}) {
// 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);
Expand All @@ -396,42 +366,4 @@ export default class DeckPicker {

return {x, y, width, height};
}

// Per-layer event handlers (e.g. onClick, onHover) are provided by the
// user and out of deck.gl's control. It's very much possible that
// the user calls React lifecycle methods in these function, such as
// ReactComponent.setState(). React lifecycle methods sometimes induce
// a re-render and re-generation of props of deck.gl and its layers,
// which invalidates all layers currently passed to this very function.

// Therefore, per-layer event handlers must be invoked at the end
// of the picking operation. NO operation that relies on the states of current
// layers should be called after this code.
callLayerPickingCallbacks(infos, mode) {
const unhandledPickInfos = [];
const pickingEvent = this.pickingEvent;

infos.forEach(info => {
if (!info.layer) {
return;
}

let handled = false;
switch (mode) {
case 'hover':
handled = info.layer.onHover(info, pickingEvent);
break;
case 'query':
break;
default:
throw new Error('unknown pick type');
}

if (!handled) {
unhandledPickInfos.push(info);
}
});

return unhandledPickInfos;
}
}
105 changes: 38 additions & 67 deletions modules/core/src/lib/deck.js
Expand Up @@ -356,66 +356,47 @@ export default class Deck {
return this.viewManager.getViewports(rect);
}

pickObject({x, y, radius = 0, layerIds = null, unproject3D}) {
this.stats.get('Pick Count').incrementCount();
this.stats.get('pickObject Time').timeStart();
const layers = this.layerManager.getLayers({layerIds});
const activateViewport = this.layerManager.activateViewport;
const selectedInfos = this.deckPicker.pickObject({
x,
y,
radius,
unproject3D,
layers,
viewports: this.getViewports({x, y}),
activateViewport,
mode: 'query',
depth: 1
}).result;
this.stats.get('pickObject Time').timeEnd();
return selectedInfos.length ? selectedInfos[0] : null;
}

pickMultipleObjects({x, y, radius = 0, layerIds = null, unproject3D, depth = 10}) {
this.stats.get('Pick Count').incrementCount();
this.stats.get('pickMultipleObjects Time').timeStart();
const layers = this.layerManager.getLayers({layerIds});
const activateViewport = this.layerManager.activateViewport;
const selectedInfos = this.deckPicker.pickObject({
x,
y,
radius,
unproject3D,
layers,
viewports: this.getViewports({x, y}),
activateViewport,
mode: 'query',
depth
}).result;
this.stats.get('pickMultipleObjects Time').timeEnd();
return selectedInfos;
}

pickObjects({x, y, width = 1, height = 1, layerIds = null}) {
this.stats.get('Pick Count').incrementCount();
this.stats.get('pickObjects Time').timeStart();
const layers = this.layerManager.getLayers({layerIds});
const activateViewport = this.layerManager.activateViewport;
const infos = this.deckPicker.pickObjects({
x,
y,
width,
height,
layers,
viewports: this.getViewports({x, y, width, height}),
activateViewport
});
this.stats.get('pickObjects Time').timeEnd();
return infos;
/* {x, y, radius = 0, layerIds = null, unproject3D} */
pickObject(opts) {
const infos = this._pick('pickObject', 'pickObject Time', opts).result;
return infos.length ? infos[0] : null;
}

/* {x, y, radius = 0, layerIds = null, unproject3D, depth = 10} */
pickMultipleObjects(opts) {
opts.depth = opts.depth || 10;
return this._pick('pickObject', 'pickMultipleObjects Time', opts).result;
}

/* {x, y, width = 1, height = 1, layerIds = null} */
pickObjects(opts) {
return this._pick('pickObjects', 'pickObjects Time', opts);
}

// Private Methods

_pick(method, statKey, opts) {
const {stats} = this;

stats.get('Pick Count').incrementCount();
stats.get(statKey).timeStart();

const infos = this.deckPicker[method](
Object.assign(
{
layers: this.layerManager.getLayers(opts),
viewports: this.getViewports(opts),
onViewportActive: this.layerManager.activateViewport
},
opts
)
);

stats.get(statKey).timeEnd();

return infos;
}

// canvas, either string, canvas or `null`
_createCanvas(props) {
let canvas = props.canvas;
Expand Down Expand Up @@ -561,17 +542,7 @@ export default class Deck {

if (_pickRequest.mode) {
// perform picking
const {result, emptyInfo} = this.deckPicker.pickObject(
Object.assign(
{
layers: this.layerManager.getLayers(),
viewports: this.getViewports(_pickRequest),
activateViewport: this.layerManager.activateViewport,
depth: 1
},
_pickRequest
)
);
const {result, emptyInfo} = this._pick('pickObject', 'pickObject Time', _pickRequest);
const shouldGenerateInfo = _pickRequest.callback || this.props.getTooltip;
const pickedInfo = shouldGenerateInfo && (result.find(info => info.index >= 0) || emptyInfo);
if (this.props.getTooltip) {
Expand Down
35 changes: 35 additions & 0 deletions modules/core/src/lib/picking/pick-info.js
Expand Up @@ -152,3 +152,38 @@ function getViewportFromCoordinates({viewports}) {
const viewport = viewports[0];
return viewport;
}

// Per-layer event handlers (e.g. onClick, onHover) are provided by the
// user and out of deck.gl's control. It's very much possible that
// the user calls React lifecycle methods in these function, such as
// ReactComponent.setState(). React lifecycle methods sometimes induce
// a re-render and re-generation of props of deck.gl and its layers,
// which invalidates all layers currently passed to this very function.

// Therefore, per-layer event handlers must be invoked at the end
// of the picking operation. NO operation that relies on the states of current
// layers should be called after this code.
export function callLayerPickingCallbacks(infos, mode, event) {
const unhandledPickInfos = [];

infos.forEach(info => {
if (!info.layer) {
return;
}

let handled = false;
switch (mode) {
case 'hover':
handled = info.layer.onHover(info, event);
break;
case 'query':
default:
}

if (!handled) {
unhandledPickInfos.push(info);
}
});

return unhandledPickInfos;
}

0 comments on commit 807721d

Please sign in to comment.