@deck.gl/core
Version:
deck.gl core library
1,078 lines (965 loc) • 31.9 kB
text/typescript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {Buffer, Texture} from '@luma.gl/core';
import type {Device} from '@luma.gl/core';
import PickLayersPass, {PickingColorDecoder} from '../passes/pick-layers-pass';
import log from '../utils/log';
import {getClosestObject, getUniqueObjects, PickedPixel} from './picking/query-object';
import {
processPickInfo,
getLayerPickingInfo,
getEmptyPickingInfo,
PickingInfo
} from './picking/pick-info';
import type {RenderStats} from '../passes/layers-pass';
import type {Stats} from '@probe.gl/stats';
import type {Framebuffer} from '@luma.gl/core';
import type {FilterContext, Rect} from '../passes/layers-pass';
import type Layer from './layer';
import type {Effect} from './effect';
import type View from '../views/view';
import type Viewport from '../viewports/viewport';
export type PickByPointOptions = {
x: number;
y: number;
radius?: number;
depth?: number;
mode?: string;
unproject3D?: boolean;
};
export type PickByRectOptions = {
x: number;
y: number;
width?: number;
height?: number;
mode?: string;
maxObjects?: number | null;
};
type PickOperationContext = {
layers: Layer[];
views: Record<string, View>;
viewports: Viewport[];
onViewportActive: (viewport: Viewport) => void;
effects: Effect[];
};
/** Manages picking in a Deck context */
export default class DeckPicker {
device: Device;
pickingFBO?: Framebuffer;
depthFBO?: Framebuffer;
pickLayersPass: PickLayersPass;
layerFilter?: (context: FilterContext) => boolean;
stats?: Stats;
/** Identifiers of the previously picked object, for callback tracking and auto highlight */
lastPickedInfo: {
index: number;
layerId: string | null;
info: PickingInfo | null;
};
_pickable: boolean = true;
constructor(device: Device, opts: {stats?: Stats} = {}) {
this.device = device;
this.stats = opts.stats;
this.pickLayersPass = new PickLayersPass(device);
this.lastPickedInfo = {
index: -1,
layerId: null,
info: null
};
}
setProps(props: any): void {
if ('layerFilter' in props) {
this.layerFilter = props.layerFilter;
}
if ('_pickable' in props) {
this._pickable = props._pickable;
}
}
finalize() {
if (this.pickingFBO) {
this.pickingFBO.destroy();
}
if (this.depthFBO) {
this.depthFBO.destroy();
}
}
/**
* Pick the closest info at given coordinate
* @returns Promise that resolves with picking info
*/
pickObjectAsync(opts: PickByPointOptions & PickOperationContext): Promise<{
result: PickingInfo[];
emptyInfo: PickingInfo;
}> {
return this._pickClosestObjectAsync(opts);
}
/**
* Picks a list of unique infos within a bounding box
* @returns Promise that resolves to all unique infos within a bounding box
*/
pickObjectsAsync(opts: PickByRectOptions & PickOperationContext): Promise<PickingInfo[]> {
return this._pickVisibleObjectsAsync(opts);
}
/**
* Pick the closest info at given coordinate
* @returns picking info
* @note WebGL only - use pickObjectAsync instead
*/
pickObject(opts: PickByPointOptions & PickOperationContext) {
return this._pickClosestObject(opts);
}
/**
* Get all unique infos within a bounding box
* @returns all unique infos within a bounding box
* @note WebGL only - use pickObjectAsync instead
*/
pickObjects(opts: PickByRectOptions & PickOperationContext) {
return this._pickVisibleObjects(opts);
}
// Returns a new picking info object by assuming the last picked object is still picked
getLastPickedObject({x, y, layers, viewports}, lastPickedInfo = this.lastPickedInfo.info) {
const lastPickedLayerId = lastPickedInfo && lastPickedInfo.layer && lastPickedInfo.layer.id;
const lastPickedViewportId =
lastPickedInfo && lastPickedInfo.viewport && lastPickedInfo.viewport.id;
const layer = lastPickedLayerId ? layers.find(l => l.id === lastPickedLayerId) : null;
const viewport =
(lastPickedViewportId && viewports.find(v => v.id === lastPickedViewportId)) || viewports[0];
const coordinate = viewport && viewport.unproject([x - viewport.x, y - viewport.y]);
const info = {
x,
y,
viewport,
coordinate,
layer
};
return {...lastPickedInfo, ...info};
}
// Private
/** Ensures that picking framebuffer exists and matches the canvas size */
_resizeBuffer() {
// Create a frame buffer if not already available
if (!this.pickingFBO) {
const pickingColorTexture = this.device.createTexture({
format: 'rgba8unorm',
width: 1,
height: 1,
usage: Texture.RENDER_ATTACHMENT | Texture.COPY_SRC
});
this.pickingFBO = this.device.createFramebuffer({
colorAttachments: [pickingColorTexture],
depthStencilAttachment: 'depth16unorm'
});
if (this.device.isTextureFormatRenderable('rgba32float')) {
const depthColorTexture = this.device.createTexture({
format: 'rgba32float',
width: 1,
height: 1,
usage: Texture.RENDER_ATTACHMENT | Texture.COPY_SRC
});
const depthFBO = this.device.createFramebuffer({
colorAttachments: [depthColorTexture],
depthStencilAttachment: 'depth16unorm'
});
this.depthFBO = depthFBO;
}
}
// Resize it to current canvas size (this is a noop if size hasn't changed)
const {canvas} = this.device.getDefaultCanvasContext();
this.pickingFBO?.resize({width: canvas.width, height: canvas.height});
this.depthFBO?.resize({width: canvas.width, height: canvas.height});
}
/** Preliminary filtering of the layers list. Skid picking pass if no layer is pickable. */
_getPickable(layers: Layer[]): Layer[] | null {
if (this._pickable === false) {
return null;
}
const pickableLayers = layers.filter(
layer => this.pickLayersPass.shouldDrawLayer(layer) && !layer.isComposite
);
return pickableLayers.length ? pickableLayers : null;
}
/**
* Pick the closest object at the given coordinate
*/
// eslint-disable-next-line max-statements,complexity
async _pickClosestObjectAsync({
layers,
views,
viewports,
x,
y,
radius = 0,
depth = 1,
mode = 'query',
unproject3D,
onViewportActive,
effects
}: PickByPointOptions & PickOperationContext): Promise<{
result: PickingInfo[];
emptyInfo: PickingInfo;
}> {
// @ts-expect-error TODO - assuming WebGL context
const pixelRatio = this.device.canvasContext.cssToDeviceRatio();
const pickableLayers = this._getPickable(layers);
if (!pickableLayers || viewports.length === 0) {
return {
result: [],
emptyInfo: getEmptyPickingInfo({viewports, x, y, pixelRatio})
};
}
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
// @ts-expect-error TODO - assuming WebGL context
const devicePixelRange = this.device.canvasContext.cssToDevicePixels([x, y], true);
const devicePixel = [
devicePixelRange.x + Math.floor(devicePixelRange.width / 2),
devicePixelRange.y + Math.floor(devicePixelRange.height / 2)
];
const deviceRadius = Math.round(radius * pixelRatio);
const {width, height} = this.pickingFBO as Framebuffer;
const deviceRect = this._getPickingRect({
deviceX: devicePixel[0],
deviceY: devicePixel[1],
deviceRadius,
deviceWidth: width,
deviceHeight: height
});
const cullRect: Rect = {
x: x - radius,
y: y - radius,
width: radius * 2 + 1,
height: radius * 2 + 1
};
let infos: Map<string | null, PickingInfo>;
const result: PickingInfo[] = [];
const affectedLayers = new Set<Layer>();
for (let i = 0; i < depth; i++) {
let pickInfo: PickedPixel;
if (deviceRect) {
const pickedResult = await this._drawAndSampleAsync({
layers: pickableLayers,
views,
viewports,
onViewportActive,
deviceRect,
cullRect,
effects,
pass: `picking:${mode}`
});
pickInfo = getClosestObject({
...pickedResult,
deviceX: devicePixel[0],
deviceY: devicePixel[1],
deviceRadius,
deviceRect
});
} else {
pickInfo = {
pickedColor: null,
pickedObjectIndex: -1
};
}
let z;
const depthLayers = this._getDepthLayers(pickInfo, pickableLayers, unproject3D);
if (depthLayers.length > 0) {
const {pickedColors: pickedColors2} = await this._drawAndSampleAsync(
{
layers: depthLayers,
views,
viewports,
onViewportActive,
deviceRect: {
x: pickInfo.pickedX ?? devicePixel[0],
y: pickInfo.pickedY ?? devicePixel[1],
width: 1,
height: 1
},
cullRect,
effects,
pass: `picking:${mode}:z`
},
true
);
// picked value is in common space (pixels) from the camera target (viewport.position)
// convert it to meters from the ground
if (pickedColors2[3]) {
z = pickedColors2[0];
}
}
// Only exclude if we need to run picking again.
// We need to run picking again if an object is detected AND
// we have not exhausted the requested depth.
if (pickInfo.pickedLayer && i + 1 < depth) {
affectedLayers.add(pickInfo.pickedLayer);
pickInfo.pickedLayer.disablePickingIndex(pickInfo.pickedObjectIndex);
}
// This logic needs to run even if no object is picked.
infos = processPickInfo({
pickInfo,
lastPickedInfo: this.lastPickedInfo,
mode,
layers: pickableLayers,
viewports,
x,
y,
z,
pixelRatio
});
for (const info of infos.values()) {
if (info.layer) {
result.push(info);
}
}
// If no object is picked stop.
if (!pickInfo.pickedColor) {
break;
}
}
// reset only affected buffers
for (const layer of affectedLayers) {
layer.restorePickingColors();
}
return {result, emptyInfo: infos!.get(null) as PickingInfo};
}
/**
* Pick the closest object at the given coordinate
* @deprecated WebGL only
*/
// eslint-disable-next-line max-statements,complexity
_pickClosestObject({
layers,
views,
viewports,
x,
y,
radius = 0,
depth = 1,
mode = 'query',
unproject3D,
onViewportActive,
effects
}: PickByPointOptions & PickOperationContext): {
result: PickingInfo[];
emptyInfo: PickingInfo;
} {
// @ts-expect-error TODO - assuming WebGL context
const pixelRatio = this.device.canvasContext.cssToDeviceRatio();
const pickableLayers = this._getPickable(layers);
if (!pickableLayers || viewports.length === 0) {
return {
result: [],
emptyInfo: getEmptyPickingInfo({viewports, x, y, pixelRatio})
};
}
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
// @ts-expect-error TODO - assuming WebGL context
const devicePixelRange = this.device.canvasContext.cssToDevicePixels([x, y], true);
const devicePixel = [
devicePixelRange.x + Math.floor(devicePixelRange.width / 2),
devicePixelRange.y + Math.floor(devicePixelRange.height / 2)
];
const deviceRadius = Math.round(radius * pixelRatio);
const {width, height} = this.pickingFBO as Framebuffer;
const deviceRect = this._getPickingRect({
deviceX: devicePixel[0],
deviceY: devicePixel[1],
deviceRadius,
deviceWidth: width,
deviceHeight: height
});
const cullRect: Rect = {
x: x - radius,
y: y - radius,
width: radius * 2 + 1,
height: radius * 2 + 1
};
let infos: Map<string | null, PickingInfo>;
const result: PickingInfo[] = [];
const affectedLayers = new Set<Layer>();
for (let i = 0; i < depth; i++) {
let pickInfo: PickedPixel;
if (deviceRect) {
const pickedResult = this._drawAndSample({
layers: pickableLayers,
views,
viewports,
onViewportActive,
deviceRect,
cullRect,
effects,
pass: `picking:${mode}`
});
pickInfo = getClosestObject({
...pickedResult,
deviceX: devicePixel[0],
deviceY: devicePixel[1],
deviceRadius,
deviceRect
});
} else {
pickInfo = {
pickedColor: null,
pickedObjectIndex: -1
};
}
let z;
const depthLayers = this._getDepthLayers(pickInfo, pickableLayers, unproject3D);
if (depthLayers.length > 0) {
const {pickedColors: pickedColors2} = this._drawAndSample(
{
layers: depthLayers,
views,
viewports,
onViewportActive,
deviceRect: {
x: pickInfo.pickedX ?? devicePixel[0],
y: pickInfo.pickedY ?? devicePixel[1],
width: 1,
height: 1
},
cullRect,
effects,
pass: `picking:${mode}:z`
},
true
);
// picked value is in common space (pixels) from the camera target (viewport.position)
// convert it to meters from the ground
if (pickedColors2[3]) {
z = pickedColors2[0];
}
}
// Only exclude if we need to run picking again.
// We need to run picking again if an object is detected AND
// we have not exhausted the requested depth.
if (pickInfo.pickedLayer && i + 1 < depth) {
affectedLayers.add(pickInfo.pickedLayer);
pickInfo.pickedLayer.disablePickingIndex(pickInfo.pickedObjectIndex);
}
// This logic needs to run even if no object is picked.
infos = processPickInfo({
pickInfo,
lastPickedInfo: this.lastPickedInfo,
mode,
layers: pickableLayers,
viewports,
x,
y,
z,
pixelRatio
});
for (const info of infos.values()) {
if (info.layer) {
result.push(info);
}
}
// If no object is picked stop.
if (!pickInfo.pickedColor) {
break;
}
}
// reset only affected buffers
for (const layer of affectedLayers) {
layer.restorePickingColors();
}
return {result, emptyInfo: infos!.get(null) as PickingInfo};
}
/**
* Pick all objects within the given bounding box
*/
// eslint-disable-next-line max-statements
async _pickVisibleObjectsAsync({
layers,
views,
viewports,
x,
y,
width = 1,
height = 1,
mode = 'query',
maxObjects = null,
onViewportActive,
effects
}: PickByRectOptions & PickOperationContext): Promise<PickingInfo[]> {
const pickableLayers = this._getPickable(layers);
if (!pickableLayers || viewports.length === 0) {
return [];
}
this._resizeBuffer();
// Convert from canvas top-left to WebGL bottom-left coordinates
// And compensate for pixelRatio
// @ts-expect-error TODO - assuming WebGL context
const pixelRatio = this.device.canvasContext.cssToDeviceRatio();
// @ts-expect-error TODO - assuming WebGL context
const leftTop = this.device.canvasContext.cssToDevicePixels([x, y], true);
// take left and top (y inverted in device pixels) from start location
const deviceLeft = leftTop.x;
const deviceTop = leftTop.y + leftTop.height;
// take right and bottom (y inverted in device pixels) from end location
// @ts-expect-error TODO - assuming WebGL context
const rightBottom = this.device.canvasContext.cssToDevicePixels([x + width, y + height], true);
const deviceRight = rightBottom.x + rightBottom.width;
const deviceBottom = rightBottom.y;
const deviceRect = {
x: deviceLeft,
y: deviceBottom,
// deviceTop and deviceRight represent the first pixel outside the desired rect
width: deviceRight - deviceLeft,
height: deviceTop - deviceBottom
};
const pickedResult = await this._drawAndSampleAsync({
layers: pickableLayers,
views,
viewports,
onViewportActive,
deviceRect,
cullRect: {x, y, width, height},
effects,
pass: `picking:${mode}`
});
const pickInfos = getUniqueObjects(pickedResult);
// `getUniqueObjects` dedup by picked color
// However different picked color may be linked to the same picked object, e.g. stroke and fill of the same polygon
// picked from different sub layers of a GeoJsonLayer
// Here after resolving the picked index with `layer.getPickingInfo`, we need to dedup again by unique picked objects
const uniquePickedObjects = new Map<string, Set<unknown>>();
const uniqueInfos: PickingInfo[] = [];
const limitMaxObjects = Number.isFinite(maxObjects);
for (let i = 0; i < pickInfos.length; i++) {
if (limitMaxObjects && uniqueInfos.length >= maxObjects!) {
break;
}
const pickInfo = pickInfos[i];
let info: PickingInfo = {
color: pickInfo.pickedColor,
layer: null,
index: pickInfo.pickedObjectIndex,
picked: true,
x,
y,
pixelRatio
};
info = getLayerPickingInfo({layer: pickInfo.pickedLayer as Layer, info, mode});
// info.layer is always populated because it's a picked pixel
const pickedLayerId = info.layer!.id;
if (!uniquePickedObjects.has(pickedLayerId)) {
uniquePickedObjects.set(pickedLayerId, new Set<unknown>());
}
const uniqueObjectsInLayer = uniquePickedObjects.get(pickedLayerId) as Set<unknown>;
// info.object may be null if the layer is using non-iterable data.
// Fall back to using index as identifier.
const pickedObjectKey = info.object ?? info.index;
if (!uniqueObjectsInLayer.has(pickedObjectKey)) {
uniqueObjectsInLayer.add(pickedObjectKey);
uniqueInfos.push(info);
}
}
return uniqueInfos;
}
/**
* Pick all objects within the given bounding box
* @deprecated WebGL only
*/
// eslint-disable-next-line max-statements
_pickVisibleObjects({
layers,
views,
viewports,
x,
y,
width = 1,
height = 1,
mode = 'query',
maxObjects = null,
onViewportActive,
effects
}: PickByRectOptions & PickOperationContext): PickingInfo[] {
const pickableLayers = this._getPickable(layers);
if (!pickableLayers || viewports.length === 0) {
return [];
}
this._resizeBuffer();
// Convert from canvas top-left to WebGL bottom-left coordinates
// And compensate for pixelRatio
// @ts-expect-error TODO - assuming WebGL context
const pixelRatio = this.device.canvasContext.cssToDeviceRatio();
// @ts-expect-error TODO - assuming WebGL context
const leftTop = this.device.canvasContext.cssToDevicePixels([x, y], true);
// take left and top (y inverted in device pixels) from start location
const deviceLeft = leftTop.x;
const deviceTop = leftTop.y + leftTop.height;
// take right and bottom (y inverted in device pixels) from end location
// @ts-expect-error TODO - assuming WebGL context
const rightBottom = this.device.canvasContext.cssToDevicePixels([x + width, y + height], true);
const deviceRight = rightBottom.x + rightBottom.width;
const deviceBottom = rightBottom.y;
const deviceRect = {
x: deviceLeft,
y: deviceBottom,
// deviceTop and deviceRight represent the first pixel outside the desired rect
width: deviceRight - deviceLeft,
height: deviceTop - deviceBottom
};
const pickedResult = this._drawAndSample({
layers: pickableLayers,
views,
viewports,
onViewportActive,
deviceRect,
cullRect: {x, y, width, height},
effects,
pass: `picking:${mode}`
});
const pickInfos = getUniqueObjects(pickedResult);
// `getUniqueObjects` dedup by picked color
// However different picked color may be linked to the same picked object, e.g. stroke and fill of the same polygon
// picked from different sub layers of a GeoJsonLayer
// Here after resolving the picked index with `layer.getPickingInfo`, we need to dedup again by unique picked objects
const uniquePickedObjects = new Map<string, Set<unknown>>();
const uniqueInfos: PickingInfo[] = [];
const limitMaxObjects = Number.isFinite(maxObjects);
for (let i = 0; i < pickInfos.length; i++) {
if (limitMaxObjects && uniqueInfos.length >= maxObjects!) {
break;
}
const pickInfo = pickInfos[i];
let info: PickingInfo = {
color: pickInfo.pickedColor,
layer: null,
index: pickInfo.pickedObjectIndex,
picked: true,
x,
y,
pixelRatio
};
info = getLayerPickingInfo({layer: pickInfo.pickedLayer as Layer, info, mode});
// info.layer is always populated because it's a picked pixel
const pickedLayerId = info.layer!.id;
if (!uniquePickedObjects.has(pickedLayerId)) {
uniquePickedObjects.set(pickedLayerId, new Set<unknown>());
}
const uniqueObjectsInLayer = uniquePickedObjects.get(pickedLayerId) as Set<unknown>;
// info.object may be null if the layer is using non-iterable data.
// Fall back to using index as identifier.
const pickedObjectKey = info.object ?? info.index;
if (!uniqueObjectsInLayer.has(pickedObjectKey)) {
uniqueObjectsInLayer.add(pickedObjectKey);
uniqueInfos.push(info);
}
}
return uniqueInfos;
}
/** Renders layers into the picking buffer with picking colors and read the pixels. */
_drawAndSampleAsync(params: {
deviceRect: Rect;
pass: string;
layers: Layer[];
views: Record<string, View>;
viewports: Viewport[];
onViewportActive: (viewport: Viewport) => void;
cullRect?: Rect;
effects: Effect[];
}): Promise<{
pickedColors: Uint8Array;
decodePickingColor: PickingColorDecoder;
}>;
/** Renders layers into the picking buffer with encoded z values and read the pixels. */
_drawAndSampleAsync(
params: {
deviceRect: Rect;
pass: string;
layers: Layer[];
views: Record<string, View>;
viewports: Viewport[];
onViewportActive: (viewport: Viewport) => void;
cullRect?: Rect;
effects: Effect[];
},
pickZ: true
): Promise<{
pickedColors: Float32Array;
decodePickingColor: null;
}>;
// Note: Implementation of the overloaded signatures above, TSDoc is on the signatures
async _drawAndSampleAsync(
{
layers,
views,
viewports,
onViewportActive,
deviceRect,
cullRect,
effects,
pass
}: {
deviceRect: Rect;
pass: string;
layers: Layer[];
views: Record<string, View>;
viewports: Viewport[];
onViewportActive: (viewport: Viewport) => void;
cullRect?: Rect;
effects: Effect[];
},
pickZ: boolean = false
): Promise<{
pickedColors: Uint8Array | Float32Array;
decodePickingColor: PickingColorDecoder | null;
}> {
const pickingFBO = pickZ ? this.depthFBO : this.pickingFBO;
const opts = {
layers,
layerFilter: this.layerFilter,
views,
viewports,
onViewportActive,
pickingFBO,
deviceRect,
cullRect,
effects,
pass,
pickZ,
preRenderStats: {},
isPicking: true
};
for (const effect of effects) {
if (effect.useInPicking) {
opts.preRenderStats[effect.id] = effect.preRender(opts);
}
}
const {decodePickingColor, stats} = this.pickLayersPass.render(opts);
this._updateStats(stats);
const {x, y, width, height} = deviceRect;
const texture = (pickingFBO as Framebuffer).colorAttachments[0]?.texture;
if (!texture) {
throw new Error('Picking framebuffer color attachment is missing');
}
const pickedColors = await this._readTextureDataAsync(
texture,
{x, y, width, height},
pickZ ? Float32Array : Uint8Array
);
if (!pickZ) {
let hasNonZeroAlpha = false;
for (let i = 3; i < pickedColors.length; i += 4) {
if (pickedColors[i] !== 0) {
hasNonZeroAlpha = true;
break;
}
}
if (!hasNonZeroAlpha && pickedColors.length > 0) {
log.warn('Async pick readback returned only zero alpha values', {
deviceRect,
bytes: Array.from(pickedColors.subarray(0, Math.min(pickedColors.length, 16)))
})();
}
}
return {pickedColors, decodePickingColor};
}
private async _readTextureDataAsync<T extends Uint8Array | Float32Array>(
texture: Texture,
options: {x: number; y: number; width: number; height: number},
ArrayType: Uint8ArrayConstructor | Float32ArrayConstructor
): Promise<T> {
const {width, height} = options;
const layout = texture.computeMemoryLayout(options);
const readBuffer = this.device.createBuffer({
byteLength: layout.byteLength,
usage: Buffer.COPY_DST | Buffer.MAP_READ
});
try {
texture.readBuffer(options, readBuffer);
const readData = await readBuffer.readAsync(0, layout.byteLength);
const bytesPerElement = ArrayType.BYTES_PER_ELEMENT;
if (layout.bytesPerRow % bytesPerElement !== 0) {
throw new Error(
`Texture readback row stride ${layout.bytesPerRow} is not aligned to ${bytesPerElement}-byte elements.`
);
}
const source = new ArrayType(
readData.buffer,
readData.byteOffset,
layout.byteLength / bytesPerElement
);
// Picking textures are RGBA. WebGPU rows may be padded to satisfy GPU alignment
// requirements, so repack each row into a tightly packed CPU array before decode.
const packedRowLength = width * 4;
const sourceRowLength = layout.bytesPerRow / bytesPerElement;
if (sourceRowLength < packedRowLength) {
throw new Error(
`Texture readback row stride ${sourceRowLength} is smaller than packed row length ${packedRowLength}.`
);
}
const packed = new ArrayType(width * height * 4);
for (let row = 0; row < height; row++) {
const sourceStart = row * sourceRowLength;
packed.set(
source.subarray(sourceStart, sourceStart + packedRowLength),
row * packedRowLength
);
}
return packed as T;
} finally {
readBuffer.destroy();
}
}
/**
* Renders layers into the picking buffer with picking colors and read the pixels.
* @deprecated WebGL only, use _drawAndSampleAsync instead
*/
_drawAndSample(params: {
deviceRect: Rect;
pass: string;
layers: Layer[];
views: Record<string, View>;
viewports: Viewport[];
onViewportActive: (viewport: Viewport) => void;
cullRect?: Rect;
effects: Effect[];
}): {
pickedColors: Uint8Array;
decodePickingColor: PickingColorDecoder;
};
/**
* Renders layers into the picking buffer with encoded z values and read the pixels.
* @deprecated WebGL only, use _drawAndSampleAsync instead
*/
_drawAndSample(
params: {
deviceRect: Rect;
pass: string;
layers: Layer[];
views: Record<string, View>;
viewports: Viewport[];
onViewportActive: (viewport: Viewport) => void;
cullRect?: Rect;
effects: Effect[];
},
pickZ: true
): {
pickedColors: Float32Array;
decodePickingColor: null;
};
// Note: Implementation of the overloaded signatures above, TSDoc is on the signatures
_drawAndSample(
{
layers,
views,
viewports,
onViewportActive,
deviceRect,
cullRect,
effects,
pass
}: {
deviceRect: Rect;
pass: string;
layers: Layer[];
views: Record<string, View>;
viewports: Viewport[];
onViewportActive: (viewport: Viewport) => void;
cullRect?: Rect;
effects: Effect[];
},
pickZ: boolean = false
): {
pickedColors: Uint8Array | Float32Array;
decodePickingColor: PickingColorDecoder | null;
} {
const pickingFBO = pickZ ? this.depthFBO : this.pickingFBO;
const opts = {
layers,
layerFilter: this.layerFilter,
views,
viewports,
onViewportActive,
pickingFBO,
deviceRect,
cullRect,
effects,
pass,
pickZ,
preRenderStats: {},
isPicking: true
};
for (const effect of effects) {
if (effect.useInPicking) {
opts.preRenderStats[effect.id] = effect.preRender(opts);
}
}
const {decodePickingColor, stats} = this.pickLayersPass.render(opts);
this._updateStats(stats);
// Read from an already rendered picking buffer
// Returns an Uint8ClampedArray of picked pixels
const {x, y, width, height} = deviceRect;
const pickedColors = new (pickZ ? Float32Array : Uint8Array)(width * height * 4);
this.device.readPixelsToArrayWebGL(pickingFBO as Framebuffer, {
sourceX: x,
sourceY: y,
sourceWidth: width,
sourceHeight: height,
target: pickedColors
});
return {pickedColors, decodePickingColor};
}
private _updateStats(source: RenderStats[]) {
if (!this.stats) return;
let layersCount = 0;
for (const {visibleCount} of source) {
layersCount += visibleCount;
}
this.stats.get('Layers picked').addCount(layersCount);
}
/**
* Determine which layers to use for the depth (pickZ) pass.
* - If a non-draped layer was picked, use just that layer.
* - If a draped layer was picked (geometry is at z=0) or no layer was picked
* (e.g. no-FBO tiles at extreme zoom), fall back to terrain layers.
*/
_getDepthLayers(pickInfo: PickedPixel, pickableLayers: Layer[], unproject3D?: boolean): Layer[] {
if (!unproject3D || !this.depthFBO) {
return [];
}
const {pickedLayer} = pickInfo;
const isDraped = pickedLayer?.state?.terrainDrawMode === 'drape';
if (pickedLayer && !isDraped) {
return [pickedLayer];
}
// For draped layers or when no layer was picked, use terrain layers for depth
return pickableLayers.filter(l => l.props.operation.includes('terrain'));
}
/**
* 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
}: {
deviceX: number;
deviceY: number;
deviceRadius: number;
deviceWidth: number;
deviceHeight: number;
}): Rect | null {
// 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(deviceWidth, deviceX + deviceRadius + 1) - x;
const height = Math.min(deviceHeight, deviceY + deviceRadius + 1) - y;
// x, y out of bounds.
if (width <= 0 || height <= 0) {
return null;
}
return {x, y, width, height};
}
}