@deck.gl/extensions
Version:
Plug-and-play functionalities for deck.gl layers
256 lines • 11.3 kB
JavaScript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { _deepEqual as deepEqual, LayerExtension, log } from '@deck.gl/core';
import { dataFilter, dataFilter64 } from "./shader-module.js";
import * as aggregator from "./aggregator.js";
const defaultProps = {
getFilterValue: { type: 'accessor', value: 0 },
getFilterCategory: { type: 'accessor', value: 0 },
onFilteredItemsChange: { type: 'function', value: null, optional: true },
filterEnabled: true,
filterRange: [-1, 1],
filterSoftRange: null,
filterCategories: [0],
filterTransformSize: true,
filterTransformColor: true
};
const defaultOptions = {
categorySize: 0,
filterSize: 1,
fp64: false,
countItems: false
};
const CATEGORY_TYPE_FROM_SIZE = {
1: 'uint',
2: 'uvec2',
3: 'uvec3',
4: 'uvec4'
};
const DATA_TYPE_FROM_SIZE = {
1: 'float',
2: 'vec2',
3: 'vec3',
4: 'vec4'
};
/** Adds GPU-based data filtering functionalities to layers. It allows the layer to show/hide objects based on user-defined properties. */
class DataFilterExtension extends LayerExtension {
constructor(opts = {}) {
super({ ...defaultOptions, ...opts });
}
getShaders(extension) {
const { categorySize, filterSize, fp64 } = extension.opts;
const defines = {};
if (categorySize) {
defines.DATACATEGORY_TYPE = CATEGORY_TYPE_FROM_SIZE[categorySize];
defines.DATACATEGORY_CHANNELS = categorySize;
}
if (filterSize) {
defines.DATAFILTER_TYPE = DATA_TYPE_FROM_SIZE[filterSize];
defines.DATAFILTER_DOUBLE = Boolean(fp64);
}
const module = fp64 ? dataFilter64 : dataFilter;
module.uniformTypes = module.uniformTypesFromOptions(extension.opts);
return { modules: [module], defines };
}
initializeState(context, extension) {
const attributeManager = this.getAttributeManager();
const { categorySize, filterSize, fp64 } = extension.opts;
if (attributeManager) {
if (filterSize) {
attributeManager.add({
filterValues: {
size: filterSize,
type: fp64 ? 'float64' : 'float32',
stepMode: 'dynamic',
accessor: 'getFilterValue'
}
});
}
if (categorySize) {
attributeManager.add({
filterCategoryValues: {
size: categorySize,
stepMode: 'dynamic',
accessor: 'getFilterCategory',
type: 'uint32',
transform: categorySize === 1
? d => extension._getCategoryKey.call(this, d, 0)
: d => d.map((x, i) => extension._getCategoryKey.call(this, x, i))
}
});
}
}
const { device } = this.context;
if (attributeManager && extension.opts.countItems) {
const useFloatTarget = aggregator.supportsFloatTarget(device);
// This attribute is needed for variable-width data, e.g. Path, SolidPolygon, Text
// The vertex shader checks if a vertex has the same "index" as the previous vertex
// so that we only write one count cross multiple vertices of the same object
attributeManager.add({
filterVertexIndices: {
size: useFloatTarget ? 1 : 2,
vertexOffset: 1,
type: 'unorm8',
accessor: (object, { index }) => {
const i = object && object.__source ? object.__source.index : index;
return useFloatTarget ? (i + 1) % 255 : [(i + 1) % 255, Math.floor(i / 255) % 255];
},
shaderAttributes: {
filterPrevIndices: {
vertexOffset: 0
},
filterIndices: {
vertexOffset: 1
}
}
}
});
const filterFBO = aggregator.getFramebuffer(device, useFloatTarget);
const filterModel = aggregator.getModel(device, attributeManager.getBufferLayouts({ isInstanced: false }), extension.getShaders.call(this, extension), useFloatTarget);
this.setState({ filterFBO, filterModel });
}
}
// eslint-disable-next-line complexity
updateState({ props, oldProps, changeFlags }, extension) {
const attributeManager = this.getAttributeManager();
const { categorySize } = extension.opts;
if (this.state.filterModel) {
const filterNeedsUpdate =
// attributeManager must be defined for filterModel to be set
attributeManager.attributes.filterValues?.needsUpdate() ||
attributeManager.attributes.filterCategoryValues?.needsUpdate() ||
props.filterEnabled !== oldProps.filterEnabled ||
props.filterRange !== oldProps.filterRange ||
props.filterSoftRange !== oldProps.filterSoftRange ||
props.filterCategories !== oldProps.filterCategories;
if (filterNeedsUpdate) {
this.setState({ filterNeedsUpdate });
}
}
if (attributeManager?.attributes.filterCategoryValues) {
// Update bitmask if accessor or selected categories has changed
const categoryBitMaskNeedsUpdate = attributeManager.attributes.filterCategoryValues.needsUpdate() ||
!deepEqual(props.filterCategories, oldProps.filterCategories, 2);
if (categoryBitMaskNeedsUpdate) {
this.setState({ categoryBitMask: null });
}
// Need to recreate category map if categorySize has changed
const resetCategories = changeFlags.dataChanged;
if (resetCategories) {
this.setState({
categoryMap: Array(categorySize)
.fill(0)
.map(() => ({}))
});
attributeManager.attributes.filterCategoryValues.setNeedsUpdate('categoryMap');
}
}
}
// eslint-disable-next-line max-statements
draw(params, extension) {
const filterFBO = this.state.filterFBO;
const filterModel = this.state.filterModel;
const filterNeedsUpdate = this.state.filterNeedsUpdate;
if (!this.state.categoryBitMask) {
extension._updateCategoryBitMask.call(this, params, extension);
}
const { onFilteredItemsChange, extensions, filterEnabled, filterRange, filterSoftRange, filterTransformSize, filterTransformColor, filterCategories } = this.props;
const dataFilterProps = {
extensions,
filterEnabled,
filterRange,
filterSoftRange,
filterTransformSize,
filterTransformColor,
filterCategories
};
if (this.state.categoryBitMask) {
dataFilterProps.categoryBitMask = this.state.categoryBitMask;
}
this.setShaderModuleProps({ dataFilter: dataFilterProps });
/* eslint-disable-next-line camelcase */
if (filterNeedsUpdate && onFilteredItemsChange && filterModel) {
const attributeManager = this.getAttributeManager();
const { attributes: { filterValues, filterCategoryValues, filterVertexIndices } } = attributeManager;
filterModel.setVertexCount(this.getNumInstances());
// @ts-expect-error filterValue and filterVertexIndices should always have buffer value
const attributes = {
...filterValues?.getValue(),
...filterCategoryValues?.getValue(),
...filterVertexIndices?.getValue()
};
filterModel.setAttributes(attributes);
filterModel.shaderInputs.setProps({
dataFilter: dataFilterProps
});
const viewport = [0, 0, filterFBO.width, filterFBO.height];
const renderPass = filterModel.device.beginRenderPass({
id: 'data-filter-aggregation',
framebuffer: filterFBO,
parameters: { viewport },
clearColor: [0, 0, 0, 0]
});
filterModel.setParameters(aggregator.parameters);
filterModel.draw(renderPass);
renderPass.end();
const color = filterModel.device.readPixelsToArrayWebGL(filterFBO);
let count = 0;
for (let i = 0; i < color.length; i++) {
count += color[i];
}
onFilteredItemsChange({ id: this.id, count });
this.state.filterNeedsUpdate = false;
}
}
finalizeState() {
const filterFBO = this.state.filterFBO;
const filterModel = this.state.filterModel;
// filterFBO.color.delete();
filterFBO?.destroy();
filterModel?.destroy();
}
/**
* Updates the bitmask used on the GPU to perform the filter based on the
* `filterCategories` prop. The mapping between categories and bit in the bitmask
* is performed by `_getCategoryKey()`
*/
_updateCategoryBitMask(params, extension) {
const { categorySize } = extension.opts;
if (!categorySize)
return;
const { filterCategories } = this.props;
const categoryBitMask = new Uint32Array([0, 0, 0, 0]);
const categoryFilters = (categorySize === 1 ? [filterCategories] : filterCategories);
const maxCategories = categorySize === 1 ? 128 : categorySize === 2 ? 64 : 32;
for (let c = 0; c < categoryFilters.length; c++) {
const categoryFilter = categoryFilters[c];
for (const category of categoryFilter) {
const key = extension._getCategoryKey.call(this, category, c);
if (key < maxCategories) {
const channel = c * (maxCategories / 32) + Math.floor(key / 32);
categoryBitMask[channel] += Math.pow(2, key % 32); // 1 << key fails for key > 30
}
else {
log.warn(`Exceeded maximum number of categories (${maxCategories})`)();
}
}
}
this.state.categoryBitMask = categoryBitMask;
}
/**
* Returns an index of bit in the bitmask for a given category. If the category has
* not yet been assigned a bit, a new one is assigned.
*/
_getCategoryKey(category, channel) {
const categoryMap = this.state.categoryMap[channel];
if (!(category in categoryMap)) {
categoryMap[category] = Object.keys(categoryMap).length;
}
return categoryMap[category];
}
}
DataFilterExtension.defaultProps = defaultProps;
DataFilterExtension.extensionName = 'DataFilterExtension';
export default DataFilterExtension;
//# sourceMappingURL=data-filter-extension.js.map