@deck.gl/extensions
Version:
Plug-and-play functionalities for deck.gl layers
200 lines • 7.99 kB
JavaScript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { log } from '@deck.gl/core';
import { equals } from '@math.gl/core';
import MaskPass from "./mask-pass.js";
import { joinLayerBounds, getRenderBounds, makeViewport } from "../utils/projection-utils.js";
// Class to manage mask effect
export default class MaskEffect {
constructor() {
this.id = 'mask-effect';
this.props = null;
this.useInPicking = true;
this.order = 0;
this.channels = [];
this.masks = null;
}
setup({ device }) {
this.dummyMaskMap = device.createTexture({
width: 1,
height: 1
});
this.maskPass = new MaskPass(device, { id: 'default-mask' });
this.maskMap = this.maskPass.maskMap;
}
preRender({ layers, layerFilter, viewports, onViewportActive, views, isPicking }) {
let didRender = false;
if (isPicking) {
// Do not update on picking pass
return { didRender };
}
const maskLayers = layers.filter(l => l.props.visible && l.props.operation.includes('mask'));
if (maskLayers.length === 0) {
this.masks = null;
this.channels.length = 0;
return { didRender };
}
this.masks = {};
// Map layers to channels
const channelMap = this._sortMaskChannels(maskLayers);
// TODO - support multiple views
const viewport = viewports[0];
const viewportChanged = !this.lastViewport || !this.lastViewport.equals(viewport);
if (viewport.resolution !== undefined) {
log.warn('MaskExtension is not supported in GlobeView')();
return { didRender };
}
for (const maskId in channelMap) {
const result = this._renderChannel(channelMap[maskId], {
layerFilter,
onViewportActive,
views,
viewport,
viewportChanged
});
didRender || (didRender = result);
}
// debugFBO(this.maskMap, {opaque: true});
return { didRender };
}
/* eslint-disable-next-line complexity */
_renderChannel(channelInfo, { layerFilter, onViewportActive, views, viewport, viewportChanged }) {
let didRender = false;
const oldChannelInfo = this.channels[channelInfo.index];
if (!oldChannelInfo) {
return didRender;
}
const maskChanged =
// If a channel is new
channelInfo === oldChannelInfo ||
// If sublayers have changed
channelInfo.layers.length !== oldChannelInfo.layers.length ||
channelInfo.layers.some((layer, i) =>
// Layer instance is updated
// Layer props might have changed
// Undetermined props could have an effect on the output geometry of a mask layer,
// for example getRadius+updateTriggers, radiusScale, modelMatrix
layer !== oldChannelInfo.layers[i] ||
// Some prop is in transition
layer.props.transitions) ||
// If a sublayer's positions have been updated, the cached bounds will change shallowly
channelInfo.layerBounds.some((b, i) => b !== oldChannelInfo.layerBounds[i]);
channelInfo.bounds = oldChannelInfo.bounds;
channelInfo.maskBounds = oldChannelInfo.maskBounds;
this.channels[channelInfo.index] = channelInfo;
if (maskChanged || viewportChanged) {
// Recalculate mask bounds
this.lastViewport = viewport;
const layerBounds = joinLayerBounds(channelInfo.layers, viewport);
channelInfo.bounds = layerBounds && getRenderBounds(layerBounds, viewport);
if (maskChanged || !equals(channelInfo.bounds, oldChannelInfo.bounds)) {
// Rerender mask FBO
const { maskPass, maskMap } = this;
const maskViewport = layerBounds &&
makeViewport({
bounds: channelInfo.bounds,
viewport,
width: maskMap.width,
height: maskMap.height,
border: 1
});
channelInfo.maskBounds = maskViewport ? maskViewport.getBounds() : [0, 0, 1, 1];
// @ts-ignore (2532) This method is only called from preRender where maskPass is defined
maskPass.render({
pass: 'mask',
channel: channelInfo.index,
layers: channelInfo.layers,
layerFilter,
viewports: maskViewport ? [maskViewport] : [],
onViewportActive,
views,
shaderModuleProps: {
project: {
devicePixelRatio: 1
}
}
});
didRender = true;
}
}
// @ts-ignore (2532) This method is only called from preRender where masks is defined
this.masks[channelInfo.id] = {
index: channelInfo.index,
bounds: channelInfo.maskBounds,
coordinateOrigin: channelInfo.coordinateOrigin,
coordinateSystem: channelInfo.coordinateSystem
};
return didRender;
}
/**
* Find a channel to render each mask into
* If a maskId already exists, diff and update the existing channel
* Otherwise replace a removed mask
* Otherwise create a new channel
* Returns a map from mask layer id to channel info
*/
_sortMaskChannels(maskLayers) {
const channelMap = {};
let channelCount = 0;
for (const layer of maskLayers) {
const { id } = layer.root;
let channelInfo = channelMap[id];
if (!channelInfo) {
if (++channelCount > 4) {
log.warn('Too many mask layers. The max supported is 4')();
continue; // eslint-disable-line no-continue
}
channelInfo = {
id,
index: this.channels.findIndex(c => c?.id === id),
layers: [],
layerBounds: [],
coordinateOrigin: layer.root.props.coordinateOrigin,
coordinateSystem: layer.root.props.coordinateSystem
};
channelMap[id] = channelInfo;
}
channelInfo.layers.push(layer);
channelInfo.layerBounds.push(layer.getBounds());
}
for (let i = 0; i < 4; i++) {
const channelInfo = this.channels[i];
if (!channelInfo || !(channelInfo.id in channelMap)) {
// The mask id at this channel no longer exists
this.channels[i] = null;
}
}
for (const maskId in channelMap) {
const channelInfo = channelMap[maskId];
if (channelInfo.index < 0) {
channelInfo.index = this.channels.findIndex(c => !c);
this.channels[channelInfo.index] = channelInfo;
}
}
return channelMap;
}
getShaderModuleProps() {
return {
mask: {
maskMap: this.masks ? this.maskMap : this.dummyMaskMap,
maskChannels: this.masks
}
};
}
cleanup() {
if (this.dummyMaskMap) {
this.dummyMaskMap.delete();
this.dummyMaskMap = undefined;
}
if (this.maskPass) {
this.maskPass.delete();
this.maskPass = undefined;
this.maskMap = undefined;
}
this.lastViewport = undefined;
this.masks = null;
this.channels.length = 0;
}
}
//# sourceMappingURL=mask-effect.js.map