@deck.gl/core
Version:
deck.gl core library
1,055 lines • 45.9 kB
JavaScript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
/* eslint-disable react/no-direct-mutation-state */
import { Buffer } from '@luma.gl/core';
import { WebGLDevice } from '@luma.gl/webgl';
import { COORDINATE_SYSTEM } from "./constants.js";
import AttributeManager from "./attribute/attribute-manager.js";
import UniformTransitionManager from "./uniform-transition-manager.js";
import { diffProps, validateProps } from "../lifecycle/props.js";
import { LIFECYCLE } from "../lifecycle/constants.js";
import { count } from "../utils/count.js";
import log from "../utils/log.js";
import debug from "../debug/index.js";
import assert from "../utils/assert.js";
import memoize from "../utils/memoize.js";
import { mergeShaders } from "../utils/shader.js";
import { projectPosition, getWorldPosition } from "../shaderlib/project/project-functions.js";
import typedArrayManager from "../utils/typed-array-manager.js";
import Component from "../lifecycle/component.js";
import LayerState from "./layer-state.js";
import { worldToPixels } from '@math.gl/web-mercator';
import { load } from '@loaders.gl/core';
const TRACE_CHANGE_FLAG = 'layer.changeFlag';
const TRACE_INITIALIZE = 'layer.initialize';
const TRACE_UPDATE = 'layer.update';
const TRACE_FINALIZE = 'layer.finalize';
const TRACE_MATCHED = 'layer.matched';
const MAX_PICKING_COLOR_CACHE_SIZE = 2 ** 24 - 1;
const EMPTY_ARRAY = Object.freeze([]);
// Only compare the same two viewports once
const areViewportsEqual = memoize(({ oldViewport, viewport }) => {
return oldViewport.equals(viewport);
});
let pickingColorCache = new Uint8ClampedArray(0);
const defaultProps = {
// data: Special handling for null, see below
data: { type: 'data', value: EMPTY_ARRAY, async: true },
dataComparator: { type: 'function', value: null, optional: true },
_dataDiff: {
type: 'function',
// @ts-ignore __diff is not defined on data
value: data => data && data.__diff,
optional: true
},
dataTransform: { type: 'function', value: null, optional: true },
onDataLoad: { type: 'function', value: null, optional: true },
onError: { type: 'function', value: null, optional: true },
fetch: {
type: 'function',
value: (url, { propName, layer, loaders, loadOptions, signal }) => {
const { resourceManager } = layer.context;
loadOptions = loadOptions || layer.getLoadOptions();
loaders = loaders || layer.props.loaders;
if (signal) {
loadOptions = {
...loadOptions,
fetch: {
...loadOptions?.fetch,
signal
}
};
}
let inResourceManager = resourceManager.contains(url);
if (!inResourceManager && !loadOptions) {
// If there is no layer-specific load options, then attempt to cache this resource in the data manager
resourceManager.add({ resourceId: url, data: load(url, loaders), persistent: false });
inResourceManager = true;
}
if (inResourceManager) {
return resourceManager.subscribe({
resourceId: url,
onChange: data => layer.internalState?.reloadAsyncProp(propName, data),
consumerId: layer.id,
requestId: propName
});
}
return load(url, loaders, loadOptions);
}
},
updateTriggers: {}, // Update triggers: a core change detection mechanism in deck.gl
visible: true,
pickable: false,
opacity: { type: 'number', min: 0, max: 1, value: 1 },
operation: 'draw',
onHover: { type: 'function', value: null, optional: true },
onClick: { type: 'function', value: null, optional: true },
onDragStart: { type: 'function', value: null, optional: true },
onDrag: { type: 'function', value: null, optional: true },
onDragEnd: { type: 'function', value: null, optional: true },
coordinateSystem: COORDINATE_SYSTEM.DEFAULT,
coordinateOrigin: { type: 'array', value: [0, 0, 0], compare: true },
modelMatrix: { type: 'array', value: null, compare: true, optional: true },
wrapLongitude: false,
positionFormat: 'XYZ',
colorFormat: 'RGBA',
parameters: { type: 'object', value: {}, optional: true, compare: 2 },
loadOptions: { type: 'object', value: null, optional: true, ignore: true },
transitions: null,
extensions: [],
loaders: { type: 'array', value: [], optional: true, ignore: true },
// Offset depth based on layer index to avoid z-fighting.
// Negative values pull layer towards the camera
// https://www.opengl.org/archives/resources/faq/technical/polygonoffset.htm
getPolygonOffset: {
type: 'function',
value: ({ layerIndex }) => [0, -layerIndex * 100]
},
// Selection/Highlighting
highlightedObjectIndex: null,
autoHighlight: false,
highlightColor: { type: 'accessor', value: [0, 0, 128, 128] }
};
class Layer extends Component {
constructor() {
super(...arguments);
this.internalState = null;
this.lifecycle = LIFECYCLE.NO_STATE; // Helps track and debug the life cycle of the layers
this.parent = null;
}
static get componentName() {
return Object.prototype.hasOwnProperty.call(this, 'layerName') ? this.layerName : '';
}
get root() {
// eslint-disable-next-line
let layer = this;
while (layer.parent) {
layer = layer.parent;
}
return layer;
}
toString() {
const className = this.constructor.layerName || this.constructor.name;
return `${className}({id: '${this.props.id}'})`;
}
// Public API for users
/** Projects a point with current view state from the current layer's coordinate system to screen */
project(xyz) {
assert(this.internalState);
const viewport = this.internalState.viewport || this.context.viewport;
const worldPosition = getWorldPosition(xyz, {
viewport,
modelMatrix: this.props.modelMatrix,
coordinateOrigin: this.props.coordinateOrigin,
coordinateSystem: this.props.coordinateSystem
});
const [x, y, z] = worldToPixels(worldPosition, viewport.pixelProjectionMatrix);
return xyz.length === 2 ? [x, y] : [x, y, z];
}
/** Unprojects a screen pixel to the current view's default coordinate system
Note: this does not reverse `project`. */
unproject(xy) {
assert(this.internalState);
const viewport = this.internalState.viewport || this.context.viewport;
return viewport.unproject(xy);
}
/** Projects a point with current view state from the current layer's coordinate system to the world space */
projectPosition(xyz, params) {
assert(this.internalState);
const viewport = this.internalState.viewport || this.context.viewport;
return projectPosition(xyz, {
viewport,
modelMatrix: this.props.modelMatrix,
coordinateOrigin: this.props.coordinateOrigin,
coordinateSystem: this.props.coordinateSystem,
...params
});
}
// Public API for custom layer implementation
/** `true` if this layer renders other layers */
get isComposite() {
return false;
}
/** `true` if the layer renders to screen */
get isDrawable() {
return true;
}
/** Updates selected state members and marks the layer for redraw */
setState(partialState) {
this.setChangeFlags({ stateChanged: true });
Object.assign(this.state, partialState);
this.setNeedsRedraw();
}
/** Sets the redraw flag for this layer, will trigger a redraw next animation frame */
setNeedsRedraw() {
if (this.internalState) {
this.internalState.needsRedraw = true;
}
}
/** Mark this layer as needs a deep update */
setNeedsUpdate() {
if (this.internalState) {
this.context.layerManager.setNeedsUpdate(String(this));
this.internalState.needsUpdate = true;
}
}
/** Returns true if all async resources are loaded */
get isLoaded() {
return this.internalState ? !this.internalState.isAsyncPropLoading() : false;
}
/** Returns true if using shader-based WGS84 longitude wrapping */
get wrapLongitude() {
return this.props.wrapLongitude;
}
/** @deprecated Returns true if the layer is visible in the picking pass */
isPickable() {
return this.props.pickable && this.props.visible;
}
/** Returns an array of models used by this layer, can be overriden by layer subclass */
getModels() {
const state = this.state;
return (state && (state.models || (state.model && [state.model]))) || [];
}
/** Update shader input parameters */
setShaderModuleProps(...props) {
for (const model of this.getModels()) {
model.shaderInputs.setProps(...props);
}
}
/** Returns the attribute manager of this layer */
getAttributeManager() {
return this.internalState && this.internalState.attributeManager;
}
/** Returns the most recent layer that matched to this state
(When reacting to an async event, this layer may no longer be the latest) */
getCurrentLayer() {
return this.internalState && this.internalState.layer;
}
/** Returns the default parse options for async props */
getLoadOptions() {
return this.props.loadOptions;
}
use64bitPositions() {
const { coordinateSystem } = this.props;
return (coordinateSystem === COORDINATE_SYSTEM.DEFAULT ||
coordinateSystem === COORDINATE_SYSTEM.LNGLAT ||
coordinateSystem === COORDINATE_SYSTEM.CARTESIAN);
}
// Event handling
onHover(info, pickingEvent) {
if (this.props.onHover) {
return this.props.onHover(info, pickingEvent) || false;
}
return false;
}
onClick(info, pickingEvent) {
if (this.props.onClick) {
return this.props.onClick(info, pickingEvent) || false;
}
return false;
}
// Returns the picking color that doesn't match any subfeature
// Use if some graphics do not belong to any pickable subfeature
// @return {Array} - a black color
nullPickingColor() {
return [0, 0, 0];
}
// Returns the picking color that doesn't match any subfeature
// Use if some graphics do not belong to any pickable subfeature
encodePickingColor(i, target = []) {
target[0] = (i + 1) & 255;
target[1] = ((i + 1) >> 8) & 255;
target[2] = (((i + 1) >> 8) >> 8) & 255;
return target;
}
// Returns the index corresponding to a picking color that doesn't match any subfeature
// @param {Uint8Array} color - color array to be decoded
// @return {Array} - the decoded picking color
decodePickingColor(color) {
assert(color instanceof Uint8Array);
const [i1, i2, i3] = color;
// 1 was added to seperate from no selection
const index = i1 + i2 * 256 + i3 * 65536 - 1;
return index;
}
/** Deduces number of instances. Intention is to support:
- Explicit setting of numInstances
- Auto-deduction for ES6 containers that define a size member
- Auto-deduction for Classic Arrays via the built-in length attribute
- Auto-deduction via arrays */
getNumInstances() {
// First Check if app has provided an explicit value
if (Number.isFinite(this.props.numInstances)) {
return this.props.numInstances;
}
// Second check if the layer has set its own value
if (this.state && this.state.numInstances !== undefined) {
return this.state.numInstances;
}
// Use container library to get a count for any ES6 container or object
return count(this.props.data);
}
/** Buffer layout describes how many attribute values are packed for each data object
The default (null) is one value each object.
Some data formats (e.g. paths, polygons) have various length. Their buffer layout
is in the form of [L0, L1, L2, ...] */
getStartIndices() {
// First Check if startIndices is provided as an explicit value
if (this.props.startIndices) {
return this.props.startIndices;
}
// Second check if the layer has set its own value
if (this.state && this.state.startIndices) {
return this.state.startIndices;
}
return null;
}
// Default implementation
getBounds() {
return this.getAttributeManager()?.getBounds(['positions', 'instancePositions']);
}
getShaders(shaders) {
shaders = mergeShaders(shaders, {
disableWarnings: true,
modules: this.context.defaultShaderModules
});
for (const extension of this.props.extensions) {
shaders = mergeShaders(shaders, extension.getShaders.call(this, extension));
}
return shaders;
}
/** Controls if updateState should be called. By default returns true if any prop has changed */
shouldUpdateState(params) {
return params.changeFlags.propsOrDataChanged;
}
/** Default implementation, all attributes will be invalidated and updated when data changes */
// eslint-disable-next-line complexity
updateState(params) {
const attributeManager = this.getAttributeManager();
const { dataChanged } = params.changeFlags;
if (dataChanged && attributeManager) {
if (Array.isArray(dataChanged)) {
// is partial update
for (const dataRange of dataChanged) {
attributeManager.invalidateAll(dataRange);
}
}
else {
attributeManager.invalidateAll();
}
}
// Enable/disable picking buffer
if (attributeManager) {
const { props } = params;
const hasPickingBuffer = this.internalState.hasPickingBuffer;
const needsPickingBuffer = Number.isInteger(props.highlightedObjectIndex) ||
props.pickable ||
props.extensions.some(extension => extension.getNeedsPickingBuffer.call(this, extension));
// Only generate picking buffer if needed
if (hasPickingBuffer !== needsPickingBuffer) {
this.internalState.hasPickingBuffer = needsPickingBuffer;
const { pickingColors, instancePickingColors } = attributeManager.attributes;
const pickingColorsAttribute = pickingColors || instancePickingColors;
if (pickingColorsAttribute) {
if (needsPickingBuffer && pickingColorsAttribute.constant) {
pickingColorsAttribute.constant = false;
attributeManager.invalidate(pickingColorsAttribute.id);
}
if (!pickingColorsAttribute.value && !needsPickingBuffer) {
pickingColorsAttribute.constant = true;
pickingColorsAttribute.value = [0, 0, 0];
}
}
}
}
}
/** Called once when layer is no longer matched and state will be discarded. Layers can destroy WebGL resources here. */
finalizeState(context) {
for (const model of this.getModels()) {
model.destroy();
}
const attributeManager = this.getAttributeManager();
if (attributeManager) {
attributeManager.finalize();
}
if (this.context) {
this.context.resourceManager.unsubscribe({ consumerId: this.id });
}
if (this.internalState) {
this.internalState.uniformTransitions.clear();
this.internalState.finalize();
}
}
// If state has a model, draw it with supplied uniforms
draw(opts) {
for (const model of this.getModels()) {
model.draw(opts.renderPass);
}
}
// called to populate the info object that is passed to the event handler
// @return null to cancel event
getPickingInfo({ info, mode, sourceLayer }) {
const { index } = info;
if (index >= 0) {
// If props.data is an indexable array, get the object
if (Array.isArray(this.props.data)) {
info.object = this.props.data[index];
}
}
return info;
}
// END LIFECYCLE METHODS
// / INTERNAL METHODS - called by LayerManager, DeckRenderer and DeckPicker
/** (Internal) Propagate an error event through the system */
raiseError(error, message) {
if (message) {
// Duplicating error message for backward compatibility, see #7986
// TODO - revisit in v9
error = new Error(`${message}: ${error.message}`, { cause: error });
}
if (!this.props.onError?.(error)) {
this.context?.onError?.(error, this);
}
}
/** (Internal) Checks if this layer needs redraw */
getNeedsRedraw(opts = { clearRedrawFlags: false }) {
return this._getNeedsRedraw(opts);
}
/** (Internal) Checks if this layer needs a deep update */
needsUpdate() {
if (!this.internalState) {
return false;
}
// Call subclass lifecycle method
return (this.internalState.needsUpdate ||
this.hasUniformTransition() ||
this.shouldUpdateState(this._getUpdateParams()));
// End lifecycle method
}
/** Checks if this layer has ongoing uniform transition */
hasUniformTransition() {
return this.internalState?.uniformTransitions.active || false;
}
/** Called when this layer is rendered into the given viewport */
activateViewport(viewport) {
if (!this.internalState) {
return;
}
const oldViewport = this.internalState.viewport;
this.internalState.viewport = viewport;
if (!oldViewport || !areViewportsEqual({ oldViewport, viewport })) {
this.setChangeFlags({ viewportChanged: true });
if (this.isComposite) {
if (this.needsUpdate()) {
// Composite layers may add/remove sublayers on viewport change
// Because we cannot change the layers list during a draw cycle, we don't want to update sublayers right away
// This will not call update immediately, but mark the layerManager as needs update on the next frame
this.setNeedsUpdate();
}
}
else {
this._update();
}
}
}
/** Default implementation of attribute invalidation, can be redefined */
invalidateAttribute(name = 'all') {
const attributeManager = this.getAttributeManager();
if (!attributeManager) {
return;
}
if (name === 'all') {
attributeManager.invalidateAll();
}
else {
attributeManager.invalidate(name);
}
}
/** Send updated attributes to the WebGL model */
updateAttributes(changedAttributes) {
// If some buffer layout changed
let bufferLayoutChanged = false;
for (const id in changedAttributes) {
if (changedAttributes[id].layoutChanged()) {
bufferLayoutChanged = true;
}
}
for (const model of this.getModels()) {
this._setModelAttributes(model, changedAttributes, bufferLayoutChanged);
}
}
/** Recalculate any attributes if needed */
_updateAttributes() {
const attributeManager = this.getAttributeManager();
if (!attributeManager) {
return;
}
const props = this.props;
// Figure out data length
const numInstances = this.getNumInstances();
const startIndices = this.getStartIndices();
attributeManager.update({
data: props.data,
numInstances,
startIndices,
props,
transitions: props.transitions,
// @ts-ignore (TS2339) property attribute is not present on some acceptable data types
buffers: props.data.attributes,
context: this
});
const changedAttributes = attributeManager.getChangedAttributes({ clearChangedFlags: true });
this.updateAttributes(changedAttributes);
}
/** Update attribute transitions. This is called in drawLayer, no model updates required. */
_updateAttributeTransition() {
const attributeManager = this.getAttributeManager();
if (attributeManager) {
attributeManager.updateTransition();
}
}
/** Update uniform (prop) transitions. This is called in updateState, may result in model updates. */
_updateUniformTransition() {
// @ts-ignore (TS2339) internalState is alwasy defined when this method is called
const { uniformTransitions } = this.internalState;
if (uniformTransitions.active) {
// clone props
const propsInTransition = uniformTransitions.update();
const props = Object.create(this.props);
for (const key in propsInTransition) {
Object.defineProperty(props, key, { value: propsInTransition[key] });
}
return props;
}
return this.props;
}
/** Updater for the automatically populated instancePickingColors attribute */
calculateInstancePickingColors(attribute, { numInstances }) {
if (attribute.constant) {
return;
}
// calculateInstancePickingColors always generates the same sequence.
// pickingColorCache saves the largest generated sequence for reuse
const cacheSize = Math.floor(pickingColorCache.length / 4);
// Record when using the picking buffer cache, so that layers can always point at the most recently allocated cache
// @ts-ignore (TS2531) internalState is always defined when this method is called
this.internalState.usesPickingColorCache = true;
if (cacheSize < numInstances) {
if (numInstances > MAX_PICKING_COLOR_CACHE_SIZE) {
log.warn('Layer has too many data objects. Picking might not be able to distinguish all objects.')();
}
pickingColorCache = typedArrayManager.allocate(pickingColorCache, numInstances, {
size: 4,
copy: true,
maxCount: Math.max(numInstances, MAX_PICKING_COLOR_CACHE_SIZE)
});
// If the attribute is larger than the cache, resize the cache and populate the missing chunk
const newCacheSize = Math.floor(pickingColorCache.length / 4);
const pickingColor = [0, 0, 0];
for (let i = cacheSize; i < newCacheSize; i++) {
this.encodePickingColor(i, pickingColor);
pickingColorCache[i * 4 + 0] = pickingColor[0];
pickingColorCache[i * 4 + 1] = pickingColor[1];
pickingColorCache[i * 4 + 2] = pickingColor[2];
pickingColorCache[i * 4 + 3] = 0;
}
}
attribute.value = pickingColorCache.subarray(0, numInstances * 4);
}
/** Apply changed attributes to model */
_setModelAttributes(model, changedAttributes, bufferLayoutChanged = false) {
if (!Object.keys(changedAttributes).length) {
return;
}
if (bufferLayoutChanged) {
// AttributeManager is always defined when this method is called
const attributeManager = this.getAttributeManager();
model.setBufferLayout(attributeManager.getBufferLayouts(model));
// All attributes must be reset after buffer layout change
changedAttributes = attributeManager.getAttributes();
}
// @ts-ignore luma.gl type issue
const excludeAttributes = model.userData?.excludeAttributes || {};
const attributeBuffers = {};
const constantAttributes = {};
for (const name in changedAttributes) {
if (excludeAttributes[name]) {
continue;
}
const values = changedAttributes[name].getValue();
for (const attributeName in values) {
const value = values[attributeName];
if (value instanceof Buffer) {
if (changedAttributes[name].settings.isIndexed) {
model.setIndexBuffer(value);
}
else {
attributeBuffers[attributeName] = value;
}
}
else if (value) {
constantAttributes[attributeName] = value;
}
}
}
// TODO - update buffer map?
model.setAttributes(attributeBuffers);
model.setConstantAttributes(constantAttributes);
}
/** (Internal) Sets the picking color at the specified index to null picking color. Used for multi-depth picking.
This method may be overriden by layer implementations */
disablePickingIndex(objectIndex) {
const data = this.props.data;
if (!('attributes' in data)) {
this._disablePickingIndex(objectIndex);
return;
}
// @ts-ignore (TS2531) this method is only called internally with attributeManager defined
const { pickingColors, instancePickingColors } = this.getAttributeManager().attributes;
const colors = pickingColors || instancePickingColors;
const externalColorAttribute = colors && data.attributes && data.attributes[colors.id];
if (externalColorAttribute && externalColorAttribute.value) {
const values = externalColorAttribute.value;
const objectColor = this.encodePickingColor(objectIndex);
for (let index = 0; index < data.length; index++) {
const i = colors.getVertexOffset(index);
if (values[i] === objectColor[0] &&
values[i + 1] === objectColor[1] &&
values[i + 2] === objectColor[2]) {
this._disablePickingIndex(index);
}
}
}
else {
this._disablePickingIndex(objectIndex);
}
}
// TODO - simplify subclassing interface
_disablePickingIndex(objectIndex) {
// @ts-ignore (TS2531) this method is only called internally with attributeManager defined
const { pickingColors, instancePickingColors } = this.getAttributeManager().attributes;
const colors = pickingColors || instancePickingColors;
if (!colors) {
return;
}
const start = colors.getVertexOffset(objectIndex);
const end = colors.getVertexOffset(objectIndex + 1);
// Fill the sub buffer with 0s, 1 byte per element
colors.buffer.write(new Uint8Array(end - start), start);
}
/** (Internal) Re-enable all picking indices after multi-depth picking */
restorePickingColors() {
// @ts-ignore (TS2531) this method is only called internally with attributeManager defined
const { pickingColors, instancePickingColors } = this.getAttributeManager().attributes;
const colors = pickingColors || instancePickingColors;
if (!colors) {
return;
}
// The picking color cache may have been freed and then reallocated. This ensures we read from the currently allocated cache.
if (
// @ts-ignore (TS2531) this method is only called internally with internalState defined
this.internalState.usesPickingColorCache &&
colors.value.buffer !== pickingColorCache.buffer) {
colors.value = pickingColorCache.subarray(0, colors.value.length);
}
colors.updateSubBuffer({ startOffset: 0 });
}
/* eslint-disable max-statements */
/* (Internal) Called by layer manager when a new layer is found */
_initialize() {
assert(!this.internalState); // finalized layer cannot be reused
assert(Number.isFinite(this.props.coordinateSystem)); // invalid coordinateSystem
debug(TRACE_INITIALIZE, this);
const attributeManager = this._getAttributeManager();
if (attributeManager) {
// All instanced layers get instancePickingColors attribute by default
// Their shaders can use it to render a picking scene
// TODO - this slightly slows down non instanced layers
attributeManager.addInstanced({
instancePickingColors: {
type: 'uint8',
size: 4,
noAlloc: true,
// Updaters are always called with `this` pointing to the layer
// eslint-disable-next-line @typescript-eslint/unbound-method
update: this.calculateInstancePickingColors
}
});
}
this.internalState = new LayerState({
attributeManager,
layer: this
});
this._clearChangeFlags(); // populate this.internalState.changeFlags
this.state = {};
// for backwards compatibility with older layers
// TODO - remove in next release
/* eslint-disable accessor-pairs */
Object.defineProperty(this.state, 'attributeManager', {
get: () => {
log.deprecated('layer.state.attributeManager', 'layer.getAttributeManager()')();
return attributeManager;
}
});
/* eslint-enable accessor-pairs */
this.internalState.uniformTransitions = new UniformTransitionManager(this.context.timeline);
this.internalState.onAsyncPropUpdated = this._onAsyncPropUpdated.bind(this);
// Ensure any async props are updated
this.internalState.setAsyncProps(this.props);
// Call subclass lifecycle methods
this.initializeState(this.context);
// Initialize extensions
for (const extension of this.props.extensions) {
extension.initializeState.call(this, this.context, extension);
}
// End subclass lifecycle methods
// initializeState callback tends to clear state
this.setChangeFlags({
dataChanged: 'init',
propsChanged: 'init',
viewportChanged: true,
extensionsChanged: true
});
this._update();
}
/** (Internal) Called by layer manager to transfer state from an old layer */
_transferState(oldLayer) {
debug(TRACE_MATCHED, this, this === oldLayer);
const { state, internalState } = oldLayer;
if (this === oldLayer) {
return;
}
// Move internalState
this.internalState = internalState;
// Move state
this.state = state;
// We keep the state ref on old layers to support async actions
// oldLayer.state = null;
// Ensure any async props are updated
this.internalState.setAsyncProps(this.props);
this._diffProps(this.props, this.internalState.getOldProps());
}
/** (Internal) Called by layer manager when a new layer is added or an existing layer is matched with a new instance */
_update() {
// Call subclass lifecycle method
const stateNeedsUpdate = this.needsUpdate();
// End lifecycle method
debug(TRACE_UPDATE, this, stateNeedsUpdate);
if (!stateNeedsUpdate) {
return;
}
const currentProps = this.props;
const context = this.context;
const internalState = this.internalState;
const currentViewport = context.viewport;
const propsInTransition = this._updateUniformTransition();
internalState.propsInTransition = propsInTransition;
// Overwrite this.context.viewport during update to use the last activated viewport on this layer
// In multi-view applications, a layer may only be drawn in one of the views
// Which would make the "active" viewport different from the shared context
context.viewport = internalState.viewport || currentViewport;
// Overwrite this.props during update to use in-transition prop values
this.props = propsInTransition;
try {
const updateParams = this._getUpdateParams();
const oldModels = this.getModels();
// Safely call subclass lifecycle methods
if (context.device) {
this.updateState(updateParams);
}
else {
try {
this.updateState(updateParams);
}
catch (error) {
// ignore error if gl context is missing
}
}
// Execute extension updates
for (const extension of this.props.extensions) {
extension.updateState.call(this, updateParams, extension);
}
this.setNeedsRedraw();
// Check if attributes need recalculation
this._updateAttributes();
const modelChanged = this.getModels()[0] !== oldModels[0];
this._postUpdate(updateParams, modelChanged);
// End subclass lifecycle methods
}
finally {
// Restore shared context
context.viewport = currentViewport;
this.props = currentProps;
this._clearChangeFlags();
internalState.needsUpdate = false;
internalState.resetOldProps();
}
}
/* eslint-enable max-statements */
/** (Internal) Called by manager when layer is about to be disposed
Note: not guaranteed to be called on application shutdown */
_finalize() {
debug(TRACE_FINALIZE, this);
// Call subclass lifecycle method
this.finalizeState(this.context);
// Finalize extensions
for (const extension of this.props.extensions) {
extension.finalizeState.call(this, this.context, extension);
}
}
// Calculates uniforms
_drawLayer({ renderPass, shaderModuleProps = null, uniforms = {}, parameters = {} }) {
this._updateAttributeTransition();
const currentProps = this.props;
const context = this.context;
// Overwrite this.props during redraw to use in-transition prop values
// `internalState.propsInTransition` could be missing if `updateState` failed
// @ts-ignore (TS2339) internalState is alwasy defined when this method is called
this.props = this.internalState.propsInTransition || currentProps;
try {
// TODO/ib - hack move to luma Model.draw
if (shaderModuleProps) {
this.setShaderModuleProps(shaderModuleProps);
}
// Apply polygon offset to avoid z-fighting
// TODO - move to draw-layers
const { getPolygonOffset } = this.props;
const offsets = (getPolygonOffset && getPolygonOffset(uniforms)) || [0, 0];
if (context.device instanceof WebGLDevice) {
context.device.setParametersWebGL({ polygonOffset: offsets });
}
for (const model of this.getModels()) {
if (model.device.type === 'webgpu') {
// TODO(ibgreen): model.setParameters currently wipes parameters. Semantics TBD.
model.setParameters({ ...model.parameters, ...parameters });
}
else {
model.setParameters(parameters);
}
}
// Call subclass lifecycle method
if (context.device instanceof WebGLDevice) {
context.device.withParametersWebGL(parameters, () => {
const opts = { renderPass, shaderModuleProps, uniforms, parameters, context };
// extensions
for (const extension of this.props.extensions) {
extension.draw.call(this, opts, extension);
}
this.draw(opts);
});
}
else {
const opts = { renderPass, shaderModuleProps, uniforms, parameters, context };
// extensions
for (const extension of this.props.extensions) {
extension.draw.call(this, opts, extension);
}
this.draw(opts);
}
}
finally {
this.props = currentProps;
}
// End lifecycle method
}
// Helper methods
/** Returns the current change flags */
getChangeFlags() {
return this.internalState?.changeFlags;
}
/* eslint-disable complexity */
/** Dirty some change flags, will be handled by updateLayer */
setChangeFlags(flags) {
if (!this.internalState) {
return;
}
const { changeFlags } = this.internalState;
/* eslint-disable no-fallthrough, max-depth */
for (const key in flags) {
if (flags[key]) {
let flagChanged = false;
switch (key) {
case 'dataChanged':
// changeFlags.dataChanged may be `false`, a string (reason) or an array of ranges
const dataChangedReason = flags[key];
const prevDataChangedReason = changeFlags[key];
if (dataChangedReason && Array.isArray(prevDataChangedReason)) {
// Merge partial updates
changeFlags.dataChanged = Array.isArray(dataChangedReason)
? prevDataChangedReason.concat(dataChangedReason)
: dataChangedReason;
flagChanged = true;
}
default:
if (!changeFlags[key]) {
changeFlags[key] = flags[key];
flagChanged = true;
}
}
if (flagChanged) {
debug(TRACE_CHANGE_FLAG, this, key, flags);
}
}
}
/* eslint-enable no-fallthrough, max-depth */
// Update composite flags
const propsOrDataChanged = Boolean(changeFlags.dataChanged ||
changeFlags.updateTriggersChanged ||
changeFlags.propsChanged ||
changeFlags.extensionsChanged);
changeFlags.propsOrDataChanged = propsOrDataChanged;
changeFlags.somethingChanged =
propsOrDataChanged || changeFlags.viewportChanged || changeFlags.stateChanged;
}
/* eslint-enable complexity */
/** Clear all changeFlags, typically after an update */
_clearChangeFlags() {
// @ts-ignore TS2531 this method can only be called internally with internalState assigned
this.internalState.changeFlags = {
dataChanged: false,
propsChanged: false,
updateTriggersChanged: false,
viewportChanged: false,
stateChanged: false,
extensionsChanged: false,
propsOrDataChanged: false,
somethingChanged: false
};
}
/** Compares the layers props with old props from a matched older layer
and extracts change flags that describe what has change so that state
can be update correctly with minimal effort */
_diffProps(newProps, oldProps) {
const changeFlags = diffProps(newProps, oldProps);
// iterate over changedTriggers
if (changeFlags.updateTriggersChanged) {
for (const key in changeFlags.updateTriggersChanged) {
if (changeFlags.updateTriggersChanged[key]) {
this.invalidateAttribute(key);
}
}
}
// trigger uniform transitions
if (changeFlags.transitionsChanged) {
for (const key in changeFlags.transitionsChanged) {
// prop changed and transition is enabled
// @ts-ignore (TS2531) internalState is always defined when this method is called
this.internalState.uniformTransitions.add(key, oldProps[key], newProps[key], newProps.transitions?.[key]);
}
}
return this.setChangeFlags(changeFlags);
}
/** (Internal) called by layer manager to perform extra props validation (in development only) */
validateProps() {
validateProps(this.props);
}
/** (Internal) Called by deck picker when the hovered object changes to update the auto highlight */
updateAutoHighlight(info) {
if (this.props.autoHighlight && !Number.isInteger(this.props.highlightedObjectIndex)) {
this._updateAutoHighlight(info);
}
}
// May be overriden by subclasses
// TODO - simplify subclassing interface
/** Update picking module parameters to highlight the hovered object */
_updateAutoHighlight(info) {
const picking = {
// @ts-ignore
highlightedObjectColor: info.picked ? info.color : null
};
const { highlightColor } = this.props;
if (info.picked && typeof highlightColor === 'function') {
// @ts-ignore
picking.highlightColor = highlightColor(info);
}
this.setShaderModuleProps({ picking });
// setShaderModuleProps does not trigger redraw
this.setNeedsRedraw();
}
/** Create new attribute manager */
_getAttributeManager() {
const context = this.context;
return new AttributeManager(context.device, {
id: this.props.id,
stats: context.stats,
timeline: context.timeline
});
}
// Private methods
/** Called after updateState to perform common tasks */
// eslint-disable-next-line complexity
_postUpdate(updateParams, forceUpdate) {
const { props, oldProps } = updateParams;
// Note: Automatic instance count update only works for single layers
const model = this.state.model;
if (model?.isInstanced) {
model.setInstanceCount(this.getNumInstances());
}
// Set picking module parameters to match props
const { autoHighlight, highlightedObjectIndex, highlightColor } = props;
if (forceUpdate ||
oldProps.autoHighlight !== autoHighlight ||
oldProps.highlightedObjectIndex !== highlightedObjectIndex ||
oldProps.highlightColor !== highlightColor) {
const picking = {};
if (Array.isArray(highlightColor)) {
picking.highlightColor = highlightColor;
}
// highlightedObjectIndex will overwrite any settings from auto highlighting.
// Do not reset unless the value has changed.
if (forceUpdate ||
oldProps.autoHighlight !== autoHighlight ||
highlightedObjectIndex !== oldProps.highlightedObjectIndex) {
picking.highlightedObjectColor =
Number.isFinite(highlightedObjectIndex) && highlightedObjectIndex >= 0
? this.encodePickingColor(highlightedObjectIndex)
: null;
}
this.setShaderModuleProps({ picking });
}
}
_getUpdateParams() {
return {
props: this.props,
// @ts-ignore TS2531 this method can only be called internally with internalState assigned
oldProps: this.internalState.getOldProps(),
context: this.context,
// @ts-ignore TS2531 this method can only be called internally with internalState assigned
changeFlags: this.internalState.changeFlags
};
}
/** Checks state of attributes and model */
_getNeedsRedraw(opts) {
// this method may be called by the render loop as soon a the layer
// has been created, so guard against uninitialized state
if (!this.internalState) {
return false;
}
let redraw = false;
redraw = redraw || (this.internalState.needsRedraw && this.id);
// TODO - is attribute manager needed? - Model should be enough.
const attributeManager = this.getAttributeManager();
const attributeManagerNeedsRedraw = attributeManager
? attributeManager.getNeedsRedraw(opts)
: false;
redraw = redraw || attributeManagerNeedsRedraw;
if (redraw) {
for (const extension of this.props.extensions) {
extension.onNeedsRedraw.call(this, extension);
}
}
this.internalState.needsRedraw = this.internalState.needsRedraw && !opts.clearRedrawFlags;
return redraw;
}
/** Callback when asyn prop is loaded */
_onAsyncPropUpdated() {
// @ts-ignore TS2531 this method can only be called internally with internalState assigned
this._diffProps(this.props, this.internalState.getOldProps());
this.setNeedsUpdate();
}
}
Layer.defaultProps = defaultProps;
Layer.layerName = 'Layer';
export default Layer;
//# sourceMappingURL=layer.js.map