// Copyright (c) 2015 - 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import {Tesselator} from '@deck.gl/core'; import {normalizePath} from './path'; import type {TypedArray} from '@math.gl/core'; import type {PathGeometry, FlatPathGeometry, NormalizedPathGeometry} from './path'; const START_CAP = 1; const END_CAP = 2; const INVALID = 4; // This class is set up to allow querying one attribute at a time // the way the AttributeManager expects it export default class PathTesselator extends Tesselator< PathGeometry, NormalizedPathGeometry, { fp64?: boolean; resolution?: number; wrapLongitude?: boolean; loop?: boolean; } > { constructor(opts) { super({ ...opts, attributes: { // Padding covers shaderAttributes for last segment in largest case fp64 // additional vertex + hi & low parts, 3 * 6 positions: { size: 3, padding: 18, initialize: true, type: opts.fp64 ? Float64Array : Float32Array }, segmentTypes: {size: 1, type: Uint8ClampedArray} } }); } /** Get packed attribute by name */ get(attributeName: string): TypedArray | null { return this.attributes[attributeName]; } /* Implement base Tesselator interface */ protected getGeometryFromBuffer(buffer) { if (this.normalize) { return super.getGeometryFromBuffer(buffer); } // we don't need to read the positions if no normalization return null; } /* Implement base Tesselator interface */ protected normalizeGeometry(path: PathGeometry): number[][] | PathGeometry { if (this.normalize) { return normalizePath(path, this.positionSize, this.opts.resolution, this.opts.wrapLongitude); } return path; } /* Implement base Tesselator interface */ protected getGeometrySize(path: NormalizedPathGeometry): number { if (isCut(path)) { let size = 0; for (const subPath of path) { size += this.getGeometrySize(subPath); } return size; } const numPoints = this.getPathLength(path); if (numPoints < 2) { // invalid path return 0; } if (this.isClosed(path)) { // minimum 3 vertices return numPoints < 3 ? 0 : numPoints + 2; } return numPoints; } /* Implement base Tesselator interface */ protected updateGeometryAttributes( path: NormalizedPathGeometry | null, context: { vertexStart: number; geometrySize: number; } ): void { if (context.geometrySize === 0) { return; } if (path && isCut(path)) { for (const subPath of path) { const geometrySize = this.getGeometrySize(subPath); context.geometrySize = geometrySize; this.updateGeometryAttributes(subPath, context); context.vertexStart += geometrySize; } } else { this._updateSegmentTypes(path, context); this._updatePositions(path, context); } } private _updateSegmentTypes( path: FlatPathGeometry | null, context: { vertexStart: number; geometrySize: number; } ) { const segmentTypes = this.attributes.segmentTypes as TypedArray; const isPathClosed = path ? this.isClosed(path) : false; const {vertexStart, geometrySize} = context; // positions -- A0 A1 B0 B1 B2 B3 B0 B1 B2 -- // segmentTypes 3 4 4 0 0 0 0 4 4 segmentTypes.fill(0, vertexStart, vertexStart + geometrySize); if (isPathClosed) { segmentTypes[vertexStart] = INVALID; segmentTypes[vertexStart + geometrySize - 2] = INVALID; } else { segmentTypes[vertexStart] += START_CAP; segmentTypes[vertexStart + geometrySize - 2] += END_CAP; } segmentTypes[vertexStart + geometrySize - 1] = INVALID; } private _updatePositions( path: FlatPathGeometry | null, context: { vertexStart: number; geometrySize: number; } ) { const {positions} = this.attributes; if (!positions || !path) { return; } const {vertexStart, geometrySize} = context; const p = new Array(3); // positions -- A0 A1 B0 B1 B2 B3 B0 B1 B2 -- // segmentTypes 3 4 4 0 0 0 0 4 4 for (let i = vertexStart, ptIndex = 0; ptIndex < geometrySize; i++, ptIndex++) { this.getPointOnPath(path, ptIndex, p); positions[i * 3] = p[0]; positions[i * 3 + 1] = p[1]; positions[i * 3 + 2] = p[2]; } } // Utilities /** Returns the number of points in the path */ private getPathLength(path: FlatPathGeometry): number { return path.length / this.positionSize; } /** Returns a point on the path at the specified index */ private getPointOnPath(path: FlatPathGeometry, index: number, target: number[] = []): number[] { const {positionSize} = this; if (index * positionSize >= path.length) { // loop index += 1 - path.length / positionSize; } const i = index * positionSize; target[0] = path[i]; target[1] = path[i + 1]; target[2] = (positionSize === 3 && path[i + 2]) || 0; return target; } // Returns true if the first and last points are identical private isClosed(path: FlatPathGeometry): boolean { if (!this.normalize) { return Boolean(this.opts.loop); } const {positionSize} = this; const lastPointIndex = path.length - positionSize; return ( path[0] === path[lastPointIndex] && path[1] === path[lastPointIndex + 1] && (positionSize === 2 || path[2] === path[lastPointIndex + 2]) ); } } function isCut(path: NormalizedPathGeometry): path is FlatPathGeometry[] { return Array.isArray(path[0]); }