@deck.gl/core
Version:
deck.gl core library
264 lines (228 loc) • 9.18 kB
text/typescript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {createIterable, getAccessorFromBuffer} from './iterable-utils';
import defaultTypedArrayManager from './typed-array-manager';
import assert from './assert';
import {Buffer} from '@luma.gl/core';
import type {BinaryAttribute} from '../lib/attribute/attribute';
import type {TypedArray} from '../types/types';
import type {AccessorFunction} from '../types/layer-props';
import type {TypedArrayManager} from './typed-array-manager';
type ExternalBuffer = TypedArray | Buffer | BinaryAttribute;
type TesselatorOptions<GeometryT, ExtraOptionsT> = ExtraOptionsT & {
attributes?: Record<string, any>;
getGeometry?: AccessorFunction<any, GeometryT>;
data?: any;
buffers?: Record<string, ExternalBuffer>;
geometryBuffer?: ExternalBuffer;
positionFormat?: 'XY' | 'XYZ';
dataChanged?: {startRow: number; endRow?: number}[] | string | false;
normalize?: boolean;
};
export type GeometryUpdateContext = {
vertexStart: number;
indexStart: number;
geometrySize: number;
geometryIndex: number;
};
export default abstract class Tesselator<GeometryT, NormalizedGeometryT, ExtraOptionsT> {
opts: TesselatorOptions<GeometryT, ExtraOptionsT>;
typedArrayManager: TypedArrayManager;
indexStarts: number[] = [0];
vertexStarts: number[] = [0];
vertexCount: number = 0;
instanceCount: number = 0;
attributes: Record<string, TypedArray | null>;
protected _attributeDefs: any;
protected data: any;
protected getGeometry?: AccessorFunction<any, GeometryT> | null;
protected geometryBuffer?: ExternalBuffer;
protected buffers!: Record<string, ExternalBuffer>;
protected positionSize!: number;
protected normalize!: boolean;
constructor(opts: TesselatorOptions<GeometryT, ExtraOptionsT>) {
const {attributes = {}} = opts;
this.typedArrayManager = defaultTypedArrayManager;
this.attributes = {};
this._attributeDefs = attributes;
this.opts = opts;
this.updateGeometry(opts);
}
/* Public methods */
updateGeometry(opts: TesselatorOptions<GeometryT, ExtraOptionsT>): void {
Object.assign(this.opts, opts);
const {
data,
buffers = {},
getGeometry,
geometryBuffer,
positionFormat,
dataChanged,
normalize = true
} = this.opts;
this.data = data;
this.getGeometry = getGeometry;
this.positionSize =
// @ts-ignore (2339) when geometryBuffer is a luma Buffer, size falls back to positionFormat
(geometryBuffer && geometryBuffer.size) || (positionFormat === 'XY' ? 2 : 3);
this.buffers = buffers;
this.normalize = normalize;
// Handle external logical value
if (geometryBuffer) {
assert(data.startIndices); // binary data missing startIndices
this.getGeometry = this.getGeometryFromBuffer(geometryBuffer);
if (!normalize) {
// skip packing and set attribute value directly
// TODO - avoid mutating user-provided object
buffers.vertexPositions = geometryBuffer;
}
}
this.geometryBuffer = buffers.vertexPositions;
if (Array.isArray(dataChanged)) {
// is partial update
for (const dataRange of dataChanged as {startRow: number; endRow?: number}[]) {
this._rebuildGeometry(dataRange);
}
} else {
this._rebuildGeometry();
}
}
updatePartialGeometry({startRow, endRow}: {startRow: number; endRow: number}): void {
this._rebuildGeometry({startRow, endRow});
}
// Subclass interface
/** Convert geometry to a uniform shape */
protected abstract normalizeGeometry(geometry: GeometryT): NormalizedGeometryT;
/** Update the positions buffer of a single geometry */
protected abstract updateGeometryAttributes(
geometry: NormalizedGeometryT | null,
context: GeometryUpdateContext
);
/** Get the number of vertices in a geometry */
protected abstract getGeometrySize(geometry: NormalizedGeometryT): number;
protected getGeometryFromBuffer(
geometryBuffer: ExternalBuffer
): AccessorFunction<any, GeometryT> | null {
const value = (geometryBuffer as BinaryAttribute).value || geometryBuffer;
if (!ArrayBuffer.isView(value)) {
// Cannot read binary geometries
return null;
}
// @ts-ignore (2322) NumericArray not assignable to GeometryT
return getAccessorFromBuffer(value, {
size: this.positionSize,
offset: (geometryBuffer as BinaryAttribute).offset,
stride: (geometryBuffer as BinaryAttribute).stride,
startIndices: this.data.startIndices
});
}
/* Private utility methods */
private _allocate(instanceCount: number, copy: boolean): void {
// allocate attributes
const {attributes, buffers, _attributeDefs, typedArrayManager} = this;
for (const name in _attributeDefs) {
if (name in buffers) {
// Use external buffer
typedArrayManager.release(attributes[name]);
attributes[name] = null;
} else {
const def = _attributeDefs[name];
// If dataRange is supplied, this is a partial update.
// In case we need to reallocate the typed array, it will need the old values copied
// before performing partial update.
def.copy = copy;
attributes[name] = typedArrayManager.allocate(attributes[name], instanceCount, def);
}
}
}
/**
* Visit all objects
* `data` is expected to be an iterable consistent with the base Layer expectation
*/
private _forEachGeometry(
visitor: (geometry: GeometryT | null, index: number) => void,
startRow: number,
endRow: number
): void {
const {data, getGeometry} = this;
const {iterable, objectInfo} = createIterable(data, startRow, endRow);
for (const object of iterable) {
objectInfo.index++;
const geometry = getGeometry ? getGeometry(object, objectInfo) : null;
visitor(geometry, objectInfo.index);
}
}
/* eslint-disable complexity,max-statements */
private _rebuildGeometry(dataRange?: {startRow: number; endRow?: number}): void {
if (!this.data) {
return;
}
let {indexStarts, vertexStarts, instanceCount} = this;
const {data, geometryBuffer} = this;
const {startRow = 0, endRow = Infinity} = dataRange || {};
const normalizedData: Record<number, NormalizedGeometryT | null> = {};
if (!dataRange) {
// Full update - regenerate buffer layout from scratch
indexStarts = [0];
vertexStarts = [0];
}
if (this.normalize || !geometryBuffer) {
this._forEachGeometry(
(geometry: GeometryT | null, dataIndex: number) => {
const normalizedGeometry = geometry && this.normalizeGeometry(geometry);
normalizedData[dataIndex] = normalizedGeometry;
vertexStarts[dataIndex + 1] =
vertexStarts[dataIndex] +
(normalizedGeometry ? this.getGeometrySize(normalizedGeometry) : 0);
},
startRow,
endRow
);
// count instances
instanceCount = vertexStarts[vertexStarts.length - 1];
} else {
// assume user provided data is already normalized
vertexStarts = data.startIndices;
instanceCount = vertexStarts[data.length] || 0;
if (ArrayBuffer.isView(geometryBuffer)) {
instanceCount = instanceCount || geometryBuffer.length / this.positionSize;
} else if (geometryBuffer instanceof Buffer) {
const byteStride = this.positionSize * 4;
instanceCount = instanceCount || geometryBuffer.byteLength / byteStride;
} else if (geometryBuffer.buffer) {
const byteStride = geometryBuffer.stride || this.positionSize * 4;
instanceCount = instanceCount || geometryBuffer.buffer.byteLength / byteStride;
} else if (geometryBuffer.value) {
const bufferValue = geometryBuffer.value;
const elementStride =
// @ts-ignore (2339) if stride is not specified, will fall through to positionSize
geometryBuffer.stride / bufferValue.BYTES_PER_ELEMENT || this.positionSize;
instanceCount = instanceCount || bufferValue.length / elementStride;
}
}
// allocate attributes
this._allocate(instanceCount, Boolean(dataRange));
this.indexStarts = indexStarts;
this.vertexStarts = vertexStarts;
this.instanceCount = instanceCount;
// @ts-ignore (2739) context will be populated in the loop
const context: GeometryUpdateContext = {};
this._forEachGeometry(
(geometry: GeometryT | null, dataIndex: number) => {
const normalizedGeometry =
normalizedData[dataIndex] || (geometry as unknown as NormalizedGeometryT);
context.vertexStart = vertexStarts[dataIndex];
context.indexStart = indexStarts[dataIndex];
const vertexEnd =
dataIndex < vertexStarts.length - 1 ? vertexStarts[dataIndex + 1] : instanceCount;
context.geometrySize = vertexEnd - vertexStarts[dataIndex];
context.geometryIndex = dataIndex;
this.updateGeometryAttributes(normalizedGeometry, context);
},
startRow,
endRow
);
this.vertexCount = indexStarts[indexStarts.length - 1];
}
}