UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

338 lines (279 loc) 13.4 kB
import {FillExtrusionLayoutArray, PosArray} from '../array_types.g'; import {members as layoutAttributes, centroidAttributes} from './fill_extrusion_attributes'; import {type Segment, SegmentVector} from '../segment'; import {ProgramConfigurationSet} from '../program_configuration'; import {TriangleIndexArray} from '../index_array_type'; import {EXTENT} from '../extent'; import mvt from '@mapbox/vector-tile'; const vectorTileFeatureTypes = mvt.VectorTileFeature.types; import {classifyRings} from '@maplibre/maplibre-gl-style-spec'; const EARCUT_MAX_RINGS = 500; import {register} from '../../util/web_worker_transfer'; import {hasPattern, addPatternDependencies} from './pattern_bucket_features'; import {loadGeometry} from '../load_geometry'; import {toEvaluationFeature} from '../evaluation_feature'; import {EvaluationParameters} from '../../style/evaluation_parameters'; import type {CanonicalTileID} from '../../source/tile_id'; import type { Bucket, BucketParameters, BucketFeature, IndexedFeature, PopulateParameters } from '../bucket'; import type {FillExtrusionStyleLayer} from '../../style/style_layer/fill_extrusion_style_layer'; import type {Context} from '../../gl/context'; import type {IndexBuffer} from '../../gl/index_buffer'; import type {VertexBuffer} from '../../gl/vertex_buffer'; import type Point from '@mapbox/point-geometry'; import type {FeatureStates} from '../../source/source_state'; import type {ImagePosition} from '../../render/image_atlas'; import type {VectorTileLayer} from '@mapbox/vector-tile'; import {subdividePolygon, subdivideVertexLine} from '../../render/subdivision'; import type {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings'; import {fillLargeMeshArrays} from '../../render/fill_large_mesh_arrays'; const FACTOR = Math.pow(2, 13); function addVertex(vertexArray, x, y, nx, ny, nz, t, e) { vertexArray.emplaceBack( // a_pos x, y, // a_normal_ed: 3-component normal and 1-component edgedistance Math.floor(nx * FACTOR) * 2 + t, ny * FACTOR * 2, nz * FACTOR * 2, // edgedistance (used for wrapping patterns around extrusion sides) Math.round(e) ); } type CentroidAccumulator = { x: number; y: number; sampleCount: number; }; export class FillExtrusionBucket implements Bucket { index: number; zoom: number; globalState: Record<string, any>; overscaling: number; layers: Array<FillExtrusionStyleLayer>; layerIds: Array<string>; stateDependentLayers: Array<FillExtrusionStyleLayer>; stateDependentLayerIds: Array<string>; layoutVertexArray: FillExtrusionLayoutArray; layoutVertexBuffer: VertexBuffer; centroidVertexArray: PosArray; centroidVertexBuffer: VertexBuffer; indexArray: TriangleIndexArray; indexBuffer: IndexBuffer; hasPattern: boolean; programConfigurations: ProgramConfigurationSet<FillExtrusionStyleLayer>; segments: SegmentVector; uploaded: boolean; features: Array<BucketFeature>; constructor(options: BucketParameters<FillExtrusionStyleLayer>) { this.zoom = options.zoom; this.globalState = options.globalState; this.overscaling = options.overscaling; this.layers = options.layers; this.layerIds = this.layers.map(layer => layer.id); this.index = options.index; this.hasPattern = false; this.layoutVertexArray = new FillExtrusionLayoutArray(); this.centroidVertexArray = new PosArray(); this.indexArray = new TriangleIndexArray(); this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom); this.segments = new SegmentVector(); this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); } populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) { this.features = []; this.hasPattern = hasPattern('fill-extrusion', this.layers, options); for (const {feature, id, index, sourceLayerIndex} of features) { const needGeometry = this.layers[0]._featureFilter.needGeometry; const evaluationFeature = toEvaluationFeature(feature, needGeometry); if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom, {globalState: this.globalState}), evaluationFeature, canonical)) continue; const bucketFeature: BucketFeature = { id, sourceLayerIndex, index, geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature), properties: feature.properties, type: feature.type, patterns: {} }; if (this.hasPattern) { this.features.push(addPatternDependencies('fill-extrusion', this.layers, bucketFeature, this.zoom, options)); } else { this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {}, options.subdivisionGranularity); } options.featureIndex.insert(feature, bucketFeature.geometry, index, sourceLayerIndex, this.index, true); } } addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { for (const feature of this.features) { const {geometry} = feature; this.addFeature(feature, geometry, feature.index, canonical, imagePositions, options.subdivisionGranularity); } } update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) { if (!this.stateDependentLayers.length) return; this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, imagePositions); } isEmpty() { return this.layoutVertexArray.length === 0 && this.centroidVertexArray.length === 0; } uploadPending() { return !this.uploaded || this.programConfigurations.needsUpload; } upload(context: Context) { if (!this.uploaded) { this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, layoutAttributes); this.centroidVertexBuffer = context.createVertexBuffer(this.centroidVertexArray, centroidAttributes.members, true); this.indexBuffer = context.createIndexBuffer(this.indexArray); } this.programConfigurations.upload(context); this.uploaded = true; } destroy() { if (!this.layoutVertexBuffer) return; this.layoutVertexBuffer.destroy(); this.indexBuffer.destroy(); this.programConfigurations.destroy(); this.segments.destroy(); this.centroidVertexBuffer.destroy(); } addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, subdivisionGranularity: SubdivisionGranularitySetting) { for (const polygon of classifyRings(geometry, EARCUT_MAX_RINGS)) { // Compute polygon centroid to calculate elevation in GPU const centroid: CentroidAccumulator = {x: 0, y: 0, sampleCount: 0}; const oldVertexCount = this.layoutVertexArray.length; this.processPolygon(centroid, canonical, feature, polygon, subdivisionGranularity); const addedVertices = this.layoutVertexArray.length - oldVertexCount; const centroidX = Math.floor(centroid.x / centroid.sampleCount); const centroidY = Math.floor(centroid.y / centroid.sampleCount); for (let i = 0; i < addedVertices; i++) { this.centroidVertexArray.emplaceBack( centroidX, centroidY ); } } this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, canonical); } private processPolygon( centroid: CentroidAccumulator, canonical: CanonicalTileID, feature: BucketFeature, polygon: Array<Array<Point>>, subdivisionGranularity: SubdivisionGranularitySetting ): void { if (polygon.length < 1) { return; } if (isEntirelyOutside(polygon[0])) { return; } // Only consider the un-subdivided polygon outer ring for centroid calculation for (const ring of polygon) { if (ring.length === 0) { continue; } // Here we don't mind if a hole ring is entirely outside, unlike when generating geometry later. accumulatePointsToCentroid(centroid, ring); } const segmentReference = { segment: this.segments.prepareSegment(4, this.layoutVertexArray, this.indexArray) }; const granularity = subdivisionGranularity.fill.getGranularityForZoomLevel(canonical.z); const isPolygon = vectorTileFeatureTypes[feature.type] === 'Polygon'; for (const ring of polygon) { if (ring.length === 0) { continue; } if (isEntirelyOutside(ring)) { continue; } const subdividedRing = subdivideVertexLine(ring, granularity, isPolygon); this._generateSideFaces(subdividedRing, segmentReference); } // Only triangulate and draw the area of the feature if it is a polygon // Other feature types (e.g. LineString) do not have area, so triangulation is pointless / undefined if (!isPolygon) return; // Do not generate outlines, since outlines already got subdivided earlier. const subdividedPolygon = subdividePolygon(polygon, canonical, granularity, false); const vertexArray = this.layoutVertexArray; fillLargeMeshArrays( (x, y) => { addVertex(vertexArray, x, y, 0, 0, 1, 1, 0); }, this.segments, this.layoutVertexArray, this.indexArray, subdividedPolygon.verticesFlattened, subdividedPolygon.indicesTriangles ); } /** * Generates side faces for the supplied geometry. Assumes `geometry` to be a line string, like the output of {@link subdivideVertexLine}. * For rings, it is assumed that the first and last vertex of `geometry` are equal. */ private _generateSideFaces(geometry: Array<Point>, segmentReference: {segment: Segment}) { let edgeDistance = 0; for (let p = 1; p < geometry.length; p++) { const p1 = geometry[p]; const p2 = geometry[p - 1]; if (isBoundaryEdge(p1, p2)) { continue; } if (segmentReference.segment.vertexLength + 4 > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) { segmentReference.segment = this.segments.prepareSegment(4, this.layoutVertexArray, this.indexArray); } const perp = p1.sub(p2)._perp()._unit(); const dist = p2.dist(p1); if (edgeDistance + dist > 32768) edgeDistance = 0; addVertex(this.layoutVertexArray, p1.x, p1.y, perp.x, perp.y, 0, 0, edgeDistance); addVertex(this.layoutVertexArray, p1.x, p1.y, perp.x, perp.y, 0, 1, edgeDistance); edgeDistance += dist; addVertex(this.layoutVertexArray, p2.x, p2.y, perp.x, perp.y, 0, 0, edgeDistance); addVertex(this.layoutVertexArray, p2.x, p2.y, perp.x, perp.y, 0, 1, edgeDistance); const bottomRight = segmentReference.segment.vertexLength; // ┌──────┐ // │ 0 1 │ Counter-clockwise winding order. // │ │ Triangle 1: 0 => 2 => 1 // │ 2 3 │ Triangle 2: 1 => 2 => 3 // └──────┘ this.indexArray.emplaceBack(bottomRight, bottomRight + 2, bottomRight + 1); this.indexArray.emplaceBack(bottomRight + 1, bottomRight + 2, bottomRight + 3); segmentReference.segment.vertexLength += 4; segmentReference.segment.primitiveLength += 2; } } } /** * Accumulates geometry to centroid. Geometry can be either a polygon ring, a line string or a closed line string. * In case of a polygon ring or line ring, the last vertex is ignored if it is the same as the first vertex. */ function accumulatePointsToCentroid(centroid: CentroidAccumulator, geometry: Array<Point>): void { for (let i = 0; i < geometry.length; i++) { const p = geometry[i]; if (i === geometry.length - 1 && geometry[0].x === p.x && geometry[0].y === p.y) { continue; } centroid.x += p.x; centroid.y += p.y; centroid.sampleCount++; } } register('FillExtrusionBucket', FillExtrusionBucket, {omit: ['layers', 'features']}); function isBoundaryEdge(p1, p2) { return (p1.x === p2.x && (p1.x < 0 || p1.x > EXTENT)) || (p1.y === p2.y && (p1.y < 0 || p1.y > EXTENT)); } function isEntirelyOutside(ring) { return ring.every(p => p.x < 0) || ring.every(p => p.x > EXTENT) || ring.every(p => p.y < 0) || ring.every(p => p.y > EXTENT); }