maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
1,000 lines (883 loc) • 43.8 kB
text/typescript
import Point from '@mapbox/point-geometry';
import {EXTENT} from '../data/extent';
import {type CanonicalTileID} from '../source/tile_id';
import earcut from 'earcut';
import {SubdivisionGranularityExpression, SubdivisionGranularitySetting} from './subdivision_granularity_settings';
import {register} from '../util/web_worker_transfer';
register('SubdivisionGranularityExpression', SubdivisionGranularityExpression);
register('SubdivisionGranularitySetting', SubdivisionGranularitySetting);
type SubdivisionResult = {
verticesFlattened: Array<number>;
indicesTriangles: Array<number>;
/**
* An array of arrays of indices of subdivided lines for polygon outlines.
* Each array of lines corresponds to one ring of the original polygon.
*/
indicesLineList: Array<Array<number>>;
};
// Special pole vertices have coordinates -32768,-32768 for the north pole and 32767,32767 for the south pole.
// First, find any *non-pole* vertices at those coordinates and move them slightly elsewhere.
export const NORTH_POLE_Y = -32768;
export const SOUTH_POLE_Y = 32767;
class Subdivider {
/**
* Flattened vertex positions (xyxyxy).
*/
private _vertexBuffer: Array<number> = [];
/**
* Map of "vertex x and y coordinate" to "index of such vertex".
*/
private _vertexDictionary: Map<number, number> = new Map<number, number>();
private _used: boolean = false;
private readonly _canonical: CanonicalTileID;
private readonly _granularity;
private readonly _granularityCellSize;
constructor(granularity: number, canonical: CanonicalTileID) {
this._granularity = granularity;
this._granularityCellSize = EXTENT / granularity;
this._canonical = canonical;
}
private _getKey(x: number, y: number) {
// Assumes signed 16 bit positions.
x = x + 32768;
y = y + 32768;
return (x << 16) | (y << 0);
}
/**
* Returns an index into the internal vertex buffer for a vertex at the given coordinates.
* If the internal vertex buffer contains no such vertex, then it is added.
*/
private _vertexToIndex(x: number, y: number): number {
if (x < -32768 || y < -32768 || x > 32767 || y > 32767) {
throw new Error('Vertex coordinates are out of signed 16 bit integer range.');
}
const xInt = Math.round(x) | 0;
const yInt = Math.round(y) | 0;
const key = this._getKey(xInt, yInt);
if (this._vertexDictionary.has(key)) {
return this._vertexDictionary.get(key);
}
const index = this._vertexBuffer.length / 2;
this._vertexDictionary.set(key, index);
this._vertexBuffer.push(xInt, yInt);
return index;
}
/**
* Subdivides a polygon by iterating over rows of granularity subdivision cells and splitting each row along vertical subdivision axes.
* @param inputIndices - Indices into the internal vertex buffer of the triangulated polygon (after running `earcut`).
* @returns Indices into the internal vertex buffer for triangles that are a subdivision of the input geometry.
*/
private _subdivideTrianglesScanline(inputIndices: Array<number>): Array<number> {
// A granularity cell is the square space between axes that subdivide geometry.
// For granularity 8, cells would be 1024 by 1024 units.
// For each triangle, we iterate over all cell rows it intersects, and generate subdivided geometry
// only within one cell row at a time. This way, we implicitly subdivide along the X-parallel axes (cell row boundaries).
// For each cell row, we generate an ordered point ring that describes the subdivided geometry inside this row (an intersection of the triangle and a given cell row).
// Such ordered ring can be trivially triangulated.
// Each ring may consist of sections of triangle edges that lie inside the cell row, and cell boundaries that lie inside the triangle. Both must be further subdivided along Y-parallel axes.
// Most complexity of this function comes from generating correct vertex rings, and from placing the vertices into the ring in the correct order.
if (this._granularity < 2) {
// The actual subdivision code always produces triangles with the correct winding order.
// Also apply winding order correction when skipping subdivision altogether to maintain consistency.
return fixWindingOrder(this._vertexBuffer, inputIndices);
}
const finalIndices = [];
// Iterate over all input triangles
const numIndices = inputIndices.length;
for (let primitiveIndex = 0; primitiveIndex < numIndices; primitiveIndex += 3) {
const triangleIndices: [number, number, number] = [
inputIndices[primitiveIndex + 0], // v0
inputIndices[primitiveIndex + 1], // v1
inputIndices[primitiveIndex + 2], // v2
];
const triangleVertices: [number, number, number, number, number, number] = [
this._vertexBuffer[inputIndices[primitiveIndex + 0] * 2 + 0], // v0.x
this._vertexBuffer[inputIndices[primitiveIndex + 0] * 2 + 1], // v0.y
this._vertexBuffer[inputIndices[primitiveIndex + 1] * 2 + 0], // v1.x
this._vertexBuffer[inputIndices[primitiveIndex + 1] * 2 + 1], // v1.y
this._vertexBuffer[inputIndices[primitiveIndex + 2] * 2 + 0], // v2.x
this._vertexBuffer[inputIndices[primitiveIndex + 2] * 2 + 1], // v2.y
];
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
// Compute AABB
for (let i = 0; i < 3; i++) {
const vx = triangleVertices[i * 2];
const vy = triangleVertices[i * 2 + 1];
minX = Math.min(minX, vx);
maxX = Math.max(maxX, vx);
minY = Math.min(minY, vy);
maxY = Math.max(maxY, vy);
}
if (minX === maxX || minY === maxY) {
continue; // Skip degenerate linear axis-aligned triangles
}
const cellXmin = Math.floor(minX / this._granularityCellSize);
const cellXmax = Math.ceil(maxX / this._granularityCellSize);
const cellYmin = Math.floor(minY / this._granularityCellSize);
const cellYmax = Math.ceil(maxY / this._granularityCellSize);
// Skip subdividing triangles that do not span multiple cells - just add them "as is".
if (cellXmin === cellXmax && cellYmin === cellYmax) {
finalIndices.push(...triangleIndices);
continue;
}
// Iterate over cell rows that intersect this triangle
for (let cellRow = cellYmin; cellRow < cellYmax; cellRow++) {
const ring = this._scanlineGenerateVertexRingForCellRow(cellRow, triangleVertices, triangleIndices);
scanlineTriangulateVertexRing(this._vertexBuffer, ring, finalIndices);
}
}
return finalIndices;
}
/**
* Takes a triangle and a cell row index, returns a subdivided vertex ring of the intersection of the triangle and the cell row.
* @param cellRow - Index of the cell row. A cell row of index `i` covert range from `i * granularityCellSize` to `(i + 1) * granularityCellSize`.
* @param triangleVertices - An array of 6 elements, contains flattened positions of the triangle's vertices: `[v0x, v0y, v1x, v1y, v2x, v2y]`.
* @param triangleIndices - An array of 3 elements, contains the original indices of the triangle's vertices: `[index0, index1, index2]`.
* @returns The resulting ring of vertex indices and the index (to the returned ring array) of the leftmost vertex in the ring.
*/
private _scanlineGenerateVertexRingForCellRow(
cellRow: number,
triangleVertices: [number, number, number, number, number, number],
triangleIndices: [number, number, number]
) {
const cellRowYTop = cellRow * this._granularityCellSize;
const cellRowYBottom = cellRowYTop + this._granularityCellSize;
const ring = [];
// Generate the vertex ring
for (let edgeIndex = 0; edgeIndex < 3; edgeIndex++) {
// Current edge that will be subdivided: a --> b
// The remaining vertex of the triangle: c
const aX = triangleVertices[edgeIndex * 2];
const aY = triangleVertices[edgeIndex * 2 + 1];
const bX = triangleVertices[((edgeIndex + 1) * 2) % 6];
const bY = triangleVertices[((edgeIndex + 1) * 2 + 1) % 6];
const cX = triangleVertices[((edgeIndex + 2) * 2) % 6];
const cY = triangleVertices[((edgeIndex + 2) * 2 + 1) % 6];
// Edge direction
const dirX = bX - aX;
const dirY = bY - aY;
// Edges parallel with either axis will need special handling later.
const isParallelY = dirX === 0;
const isParallelX = dirY === 0;
// Distance along edge where it enters/exits current cell row,
// where distance 0 is the edge start point, 1 the endpoint, 0.5 the mid point, etc.
const tTop = (cellRowYTop - aY) / dirY;
const tBottom = (cellRowYBottom - aY) / dirY;
const tEnter = Math.min(tTop, tBottom);
const tExit = Math.max(tTop, tBottom);
// Determine if edge lies entirely outside this cell row.
// Check entry and exit points, or if edge is parallel with X, check its Y coordinate.
if ((!isParallelX && (tEnter >= 1 || tExit <= 0)) ||
(isParallelX && (aY < cellRowYTop || aY > cellRowYBottom))) {
// Skip this edge
// But make sure to add its endpoint vertex if needed.
if (bY >= cellRowYTop && bY <= cellRowYBottom) {
// The edge endpoint is withing this row, add it to the ring
ring.push(triangleIndices[(edgeIndex + 1) % 3]);
}
continue;
}
// Do not add original triangle vertices now, those are handled separately later
// Special case: edge vertex for entry into cell row
// If edge is parallel with X axis, there is no entry vertex
if (!isParallelX && tEnter > 0) {
const x = aX + dirX * tEnter;
const y = aY + dirY * tEnter;
ring.push(this._vertexToIndex(x, y));
}
// The X coordinates of the points where the edge enters/exits the current cell row,
// or the edge start/endpoint, if the entry/exit happens beyond the edge bounds.
const enterX = aX + dirX * Math.max(tEnter, 0);
const exitX = aX + dirX * Math.min(tExit, 1);
// Generate edge interior vertices
// No need to subdivide (along X) edges that are parallel with Y
if (!isParallelY) {
this._generateIntraEdgeVertices(ring, aX, aY, bX, bY, enterX, exitX);
}
// Special case: edge vertex for exit from cell row
if (!isParallelX && tExit < 1) {
const x = aX + dirX * tExit;
const y = aY + dirY * tExit;
ring.push(this._vertexToIndex(x, y));
}
// When to split inter-edge boundary segments?
// When the boundary doesn't intersect a vertex, its easy. But what if it does?
// a
// /|
// / |
// --c--|--boundary
// \ |
// \|
// b
//
// Inter-edge region should be generated when processing the a-b edge.
// This happens fine for the top row, for the bottom row,
//
// x
// /|
// / |
// --x--x--boundary
//
// Edge that lies on boundary should be subdivided in its edge phase.
// The inter-edge phase will correctly skip it.
// Add endpoint vertex
if (isParallelX || (bY >= cellRowYTop && bY <= cellRowYBottom)) {
ring.push(triangleIndices[(edgeIndex + 1) % 3]);
}
// Any edge that has endpoint outside this row or on its boundary gets
// inter-edge vertices.
// No row boundary to split for edges parallel with X
if (!isParallelX && (bY <= cellRowYTop || bY >= cellRowYBottom)) {
this._generateInterEdgeVertices(ring, aX, aY, bX, bY, cX, cY,
exitX, cellRowYTop, cellRowYBottom);
}
}
return ring;
}
/**
* Generates ring vertices along an edge A-\>B, but only in the part that intersects a given cell row.
* Does not handle adding edge endpoint vertices or edge cell row enter/exit vertices.
* @param ring - Ordered array of vertex indices for the constructed ring. New indices are placed here.
* @param enterX - The X coordinate of the point where edge A-\>B enters the current cell row.
* @param exitX - The X coordinate of the point where edge A-\>B exits the current cell row.
*/
private _generateIntraEdgeVertices(
ring: Array<number>,
aX: number,
aY: number,
bX: number,
bY: number,
enterX: number,
exitX: number
): void {
const dirX = bX - aX;
const dirY = bY - aY;
const isParallelX = dirY === 0;
const leftX = isParallelX ? Math.min(aX, bX) : Math.min(enterX, exitX);
const rightX = isParallelX ? Math.max(aX, bX) : Math.max(enterX, exitX);
const edgeSubdivisionLeftCellX = Math.floor(leftX / this._granularityCellSize) + 1;
const edgeSubdivisionRightCellX = Math.ceil(rightX / this._granularityCellSize) - 1;
const isEdgeLeftToRight = isParallelX ? (aX < bX) : (enterX < exitX);
if (isEdgeLeftToRight) {
// Left to right
for (let cellX = edgeSubdivisionLeftCellX; cellX <= edgeSubdivisionRightCellX; cellX++) {
const x = cellX * this._granularityCellSize;
const y = aY + dirY * (x - aX) / dirX;
ring.push(this._vertexToIndex(x, y));
}
} else {
// Right to left
for (let cellX = edgeSubdivisionRightCellX; cellX >= edgeSubdivisionLeftCellX; cellX--) {
const x = cellX * this._granularityCellSize;
const y = aY + dirY * (x - aX) / dirX;
ring.push(this._vertexToIndex(x, y));
}
}
}
/**
* Generates ring vertices along cell border.
* Call when processing an edge A-\>B that exits the current row (B lies outside the current row).
* Generates vertices along the cell edge between the exit point from cell row
* of edge A-\>B and entry of edge B-\>C, or entry of C-\>A if both A and C lie outside the cell row.
* Does not handle adding edge endpoint vertices or edge cell row enter/exit vertices.
* @param ring - Ordered array of vertex indices for the constructed ring. New indices are placed here.
* @param exitX - The X coordinate of the point where edge A-\>B exits the current cell row.
* @param cellRowYTop - The current cell row top Y coordinate.
* @param cellRowYBottom - The current cell row bottom Y coordinate.
*/
private _generateInterEdgeVertices(
ring: Array<number>,
aX: number,
aY: number,
bX: number,
bY: number,
cX: number,
cY: number,
exitX: number,
cellRowYTop: number,
cellRowYBottom: number
): void {
const dirY = bY - aY;
const dir2X = cX - bX;
const dir2Y = cY - bY;
const t2Top = (cellRowYTop - bY) / dir2Y;
const t2Bottom = (cellRowYBottom - bY) / dir2Y;
// The distance along edge B->C where it enters/exits the current cell row,
// where distance 0 is B, 1 is C, 0.5 is the edge midpoint, etc.
const t2Enter = Math.min(t2Top, t2Bottom);
const t2Exit = Math.max(t2Top, t2Bottom);
const enter2X = bX + dir2X * t2Enter;
let boundarySubdivisionLeftCellX = Math.floor(Math.min(enter2X, exitX) / this._granularityCellSize) + 1;
let boundarySubdivisionRightCellX = Math.ceil(Math.max(enter2X, exitX) / this._granularityCellSize) - 1;
let isBoundaryLeftToRight = exitX < enter2X;
const isParallelX2 = dir2Y === 0;
if (isParallelX2 && (cY === cellRowYTop || cY === cellRowYBottom)) {
// Special case when edge b->c that lies on the cell boundary.
// Do not generate any inter-edge vertices in this case,
// this b->c edge gets subdivided when it is itself processed.
return;
}
if (isParallelX2 || t2Enter >= 1 || t2Exit <= 0) {
// The next edge (b->c) lies entirely outside this cell row
// Find entry point for the edge after that instead (c->a)
// There may be at most 1 edge that is parallel to X in a triangle.
// The main "a->b" edge must not be parallel at this point in the code.
// We know that "a->b" crosses the current cell row boundary, such that point "b" is beyond the boundary.
// If "b->c" is parallel to X, then "c->a" must not be parallel and must cross the cell row boundary back:
// a
// |\
// -----|-\--cell row boundary----
// | \
// c---b
// If "b->c" is not parallel to X and doesn't cross the cell row boundary,
// then c->a must also not be parallel to X and must cross the cell boundary back,
// since points "a" and "c" lie on different sides of the boundary and on different Y coordinates.
//
// Thus there is no need for "parallel with X" checks inside this condition branch.
// Compute the X coordinate where edge C->A enters the current cell row
const dir3X = aX - cX;
const dir3Y = aY - cY;
const t3Top = (cellRowYTop - cY) / dir3Y;
const t3Bottom = (cellRowYBottom - cY) / dir3Y;
const t3Enter = Math.min(t3Top, t3Bottom);
const enter3X = cX + dir3X * t3Enter;
boundarySubdivisionLeftCellX = Math.floor(Math.min(enter3X, exitX) / this._granularityCellSize) + 1;
boundarySubdivisionRightCellX = Math.ceil(Math.max(enter3X, exitX) / this._granularityCellSize) - 1;
isBoundaryLeftToRight = exitX < enter3X;
}
const boundaryY = dirY > 0 ? cellRowYBottom : cellRowYTop;
if (isBoundaryLeftToRight) {
// Left to right
for (let cellX = boundarySubdivisionLeftCellX; cellX <= boundarySubdivisionRightCellX; cellX++) {
const x = cellX * this._granularityCellSize;
ring.push(this._vertexToIndex(x, boundaryY));
}
} else {
// Right to left
for (let cellX = boundarySubdivisionRightCellX; cellX >= boundarySubdivisionLeftCellX; cellX--) {
const x = cellX * this._granularityCellSize;
ring.push(this._vertexToIndex(x, boundaryY));
}
}
}
/**
* Generates an outline for a given polygon, returns a list of arrays of line indices.
*/
private _generateOutline(polygon: Array<Array<Point>>): Array<Array<number>> {
const subdividedLines: Array<Array<number>> = [];
for (const ring of polygon) {
const line = subdivideVertexLine(ring, this._granularity, true);
const pathIndices = this._pointArrayToIndices(line);
// Points returned by subdivideVertexLine are "path" waypoints,
// for example with indices 0 1 2 3 0.
// We need list of individual line segments for rendering,
// for example 0, 1, 1, 2, 2, 3, 3, 0.
const lineIndices: Array<number> = [];
for (let i = 1; i < pathIndices.length; i++) {
lineIndices.push(pathIndices[i - 1]);
lineIndices.push(pathIndices[i]);
}
subdividedLines.push(lineIndices);
}
return subdividedLines;
}
/**
* Adds pole geometry if needed.
* @param subdividedTriangles - Array of generated triangle indices, new pole geometry is appended here.
*/
private _handlePoles(subdividedTriangles: Array<number>) {
// Add pole vertices if the tile is at north/south mercator edge
let north = false;
let south = false;
if (this._canonical) {
if (this._canonical.y === 0) {
north = true;
}
if (this._canonical.y === (1 << this._canonical.z) - 1) {
south = true;
}
}
if (north || south) {
this._fillPoles(subdividedTriangles, north, south);
}
}
/**
* Checks the internal vertex buffer for all vertices that might lie on the special pole coordinates and shifts them by one unit.
* Use for removing unintended pole vertices that might have been created during subdivision. After calling this function, actual pole vertices can be safely generated.
*/
private _ensureNoPoleVertices() {
const flattened = this._vertexBuffer;
for (let i = 0; i < flattened.length; i += 2) {
const vy = flattened[i + 1];
if (vy === NORTH_POLE_Y) {
// Move slightly down
flattened[i + 1] = NORTH_POLE_Y + 1;
}
if (vy === SOUTH_POLE_Y) {
// Move slightly down
flattened[i + 1] = SOUTH_POLE_Y - 1;
}
}
}
/**
* Generates a quad from an edge to a pole with the correct winding order.
* Helper function used inside {@link _fillPoles}.
* @param indices - Index array into which the geometry is generated.
* @param i0 - Index of the first edge vertex.
* @param i1 - Index of the second edge vertex.
* @param v0x - X coordinate of the first edge vertex.
* @param v1x - X coordinate of the second edge vertex.
* @param poleY - The Y coordinate of the desired pole (NORTH_POLE_Y or SOUTH_POLE_Y).
*/
private _generatePoleQuad(indices, i0, i1, v0x, v1x, poleY): void {
const flip = (v0x > v1x) !== (poleY === NORTH_POLE_Y);
if (flip) {
indices.push(i0);
indices.push(i1);
indices.push(this._vertexToIndex(v0x, poleY));
indices.push(i1);
indices.push(this._vertexToIndex(v1x, poleY));
indices.push(this._vertexToIndex(v0x, poleY));
} else {
indices.push(i1);
indices.push(i0);
indices.push(this._vertexToIndex(v0x, poleY));
indices.push(this._vertexToIndex(v1x, poleY));
indices.push(i1);
indices.push(this._vertexToIndex(v0x, poleY));
}
}
/**
* Detects edges that border the north or south tile edge
* and adds triangles that extend those edges to the poles.
* Only run this function on tiles that border the poles.
* Assumes that supplied geometry is clipped to the inclusive range of 0..EXTENT.
* Mutates the supplies vertex and index arrays.
* @param indices - Triangle indices. This array is appended with new primitives.
* @param north - Whether to generate geometry for the north pole.
* @param south - Whether to generate geometry for the south pole.
*/
private _fillPoles(indices: Array<number>, north: boolean, south: boolean): void {
const flattened = this._vertexBuffer;
const northEdge = 0;
const southEdge = EXTENT;
const numIndices = indices.length;
for (let primitiveIndex = 2; primitiveIndex < numIndices; primitiveIndex += 3) {
const i0 = indices[primitiveIndex - 2];
const i1 = indices[primitiveIndex - 1];
const i2 = indices[primitiveIndex];
const v0x = flattened[i0 * 2];
const v0y = flattened[i0 * 2 + 1];
const v1x = flattened[i1 * 2];
const v1y = flattened[i1 * 2 + 1];
const v2x = flattened[i2 * 2];
const v2y = flattened[i2 * 2 + 1];
if (north) {
if (v0y === northEdge && v1y === northEdge) {
this._generatePoleQuad(indices, i0, i1, v0x, v1x, NORTH_POLE_Y);
}
if (v1y === northEdge && v2y === northEdge) {
this._generatePoleQuad(indices, i1, i2, v1x, v2x, NORTH_POLE_Y);
}
if (v2y === northEdge && v0y === northEdge) {
this._generatePoleQuad(indices, i2, i0, v2x, v0x, NORTH_POLE_Y);
}
}
if (south) {
if (v0y === southEdge && v1y === southEdge) {
this._generatePoleQuad(indices, i0, i1, v0x, v1x, SOUTH_POLE_Y);
}
if (v1y === southEdge && v2y === southEdge) {
this._generatePoleQuad(indices, i1, i2, v1x, v2x, SOUTH_POLE_Y);
}
if (v2y === southEdge && v0y === southEdge) {
this._generatePoleQuad(indices, i2, i0, v2x, v0x, SOUTH_POLE_Y);
}
}
}
}
/**
* Adds all vertices in the supplied flattened vertex buffer into the internal vertex buffer.
*/
private _initializeVertices(flattened: Array<number>) {
for (let i = 0; i < flattened.length; i += 2) {
this._vertexToIndex(flattened[i], flattened[i + 1]);
}
}
/**
* Subdivides an input mesh. Imagine a regular square grid with the target granularity overlaid over the mesh - this is the subdivision's result.
* Assumes a mesh of tile features - vertex coordinates are integers, visible range where subdivision happens is 0..8192.
* @param polygon - The input polygon, specified as a list of vertex rings.
* @param generateOutlineLines - When true, also generates line indices for outline of the supplied polygon.
* @returns Vertex and index buffers with subdivision applied.
*/
public subdividePolygonInternal(polygon: Array<Array<Point>>, generateOutlineLines: boolean): SubdivisionResult {
if (this._used) {
throw new Error('Subdivision: multiple use not allowed.');
}
this._used = true;
// Initialize the vertex dictionary with input vertices since we will use all of them anyway
const {flattened, holeIndices} = flatten(polygon);
this._initializeVertices(flattened);
// Subdivide triangles
let subdividedTriangles: Array<number>;
try {
// At this point this._finalVertices is just flattened polygon points
const earcutResult = earcut(flattened, holeIndices);
const cut = this._convertIndices(flattened, earcutResult);
subdividedTriangles = this._subdivideTrianglesScanline(cut);
} catch (e) {
console.error(e);
}
// Subdivide lines
let subdividedLines: Array<Array<number>> = [];
if (generateOutlineLines) {
subdividedLines = this._generateOutline(polygon);
}
// Ensure no vertex has the special value used for pole vertices
this._ensureNoPoleVertices();
// Add pole geometry if needed
this._handlePoles(subdividedTriangles);
return {
verticesFlattened: this._vertexBuffer,
indicesTriangles: subdividedTriangles,
indicesLineList: subdividedLines,
};
}
/**
* Sometimes the supplies vertex and index array has duplicate vertices - same coordinates that are referenced by multiple different indices.
* That is not allowed for purposes of subdivision, duplicates are removed in `this.initializeVertices`.
* This function converts the original index array that indexes into the original vertex array with duplicates
* into an index array that indexes into `this._finalVertices`.
* @param vertices - Flattened vertex array used by the old indices. This may contain duplicate vertices.
* @param oldIndices - Indices into the old vertex array.
* @returns Indices transformed so that they are valid indices into `this._finalVertices` (with duplicates removed).
*/
private _convertIndices(vertices: Array<number>, oldIndices: Array<number>): Array<number> {
const newIndices = [];
for (let i = 0; i < oldIndices.length; i++) {
const x = vertices[oldIndices[i] * 2];
const y = vertices[oldIndices[i] * 2 + 1];
newIndices.push(this._vertexToIndex(x, y));
}
return newIndices;
}
/**
* Converts an array of points into an array of indices into the internal vertex buffer (`_finalVertices`).
*/
private _pointArrayToIndices(array: Array<Point>): Array<number> {
const indices = [];
for (let i = 0; i < array.length; i++) {
const p = array[i];
indices.push(this._vertexToIndex(p.x, p.y));
}
return indices;
}
}
/**
* Subdivides a polygon to a given granularity. Intended for preprocessing geometry for the 'fill' and 'fill-extrusion' layer types.
* All returned triangles have the counter-clockwise winding order.
* @param polygon - An array of point rings that specify the polygon. The first ring is the polygon exterior, all subsequent rings form holes inside the first ring.
* @param canonical - The canonical tile ID of the tile this polygon belongs to. Needed for generating special geometry for tiles that border the poles.
* @param granularity - The subdivision granularity. If we assume tile EXTENT=8192, then a granularity of 2 will result in geometry being "cut" on each axis
* divisible by 4096 (including outside the tile range, so -8192, -4096, or 12288...), granularity of 8 on axes divisible by 1024 and so on.
* Granularity of 1 or lower results in *no* subdivision.
* @param generateOutlineLines - When true, also generates index arrays for subdivided lines that form the outline of the supplied polygon. True by default.
* @returns An object that contains the generated vertex array, triangle index array and, if specified, line index arrays.
*/
export function subdividePolygon(polygon: Array<Array<Point>>, canonical: CanonicalTileID, granularity: number, generateOutlineLines: boolean = true): SubdivisionResult {
const subdivider = new Subdivider(granularity, canonical);
return subdivider.subdividePolygonInternal(polygon, generateOutlineLines);
}
/**
* Subdivides a line represented by an array of points. Mainly intended for preprocessing geometry for the 'line' layer type.
* Assumes a line segment between each two consecutive points in the array.
* Does not assume a line segment from last point to first point, unless `isRing` is set to `true`.
* For example, an array of 4 points describes exactly 3 line segments.
* @param linePoints - An array of points describing the line segments.
* @param granularity - Subdivision granularity.
* @param isRing - When true, an additional line segment is assumed to exist between the input array's last and first point.
* @returns A new array of points of the subdivided line segments. The array may contain some of the original Point objects. If `isRing` is set to `true`, then this also includes the (subdivided) segment from the last point of the input array to the first point.
*
* @example
* ```ts
* const result = subdivideVertexLine([
* new Point(0, 0),
* new Point(8, 0),
* new Point(0, 8),
* ], EXTENT / 4, false);
* // Results in an array of points with these (x, y) coordinates:
* // 0, 0
* // 4, 0
* // 8, 0
* // 4, 4
* // 0, 8
* ```
*
* @example
* ```ts
* const result = subdivideVertexLine([
* new Point(0, 0),
* new Point(8, 0),
* new Point(0, 8),
* ], EXTENT / 4, true);
* // Results in an array of points with these (x, y) coordinates:
* // 0, 0
* // 4, 0
* // 8, 0
* // 4, 4
* // 0, 8
* // 0, 4
* // 0, 0
* ```
*/
export function subdivideVertexLine(linePoints: Array<Point>, granularity: number, isRing: boolean = false): Array<Point> {
if (!linePoints || linePoints.length < 1) {
return [];
}
if (linePoints.length < 2) {
return [];
}
// Generate an extra line segment between the input array's first and last points,
// but only if isRing=true AND the first and last points actually differ.
const first = linePoints[0];
const last = linePoints[linePoints.length - 1];
const addLastToFirstSegment = isRing && (first.x !== last.x || first.y !== last.y);
if (granularity < 2) {
if (addLastToFirstSegment) {
return [...linePoints, linePoints[0]];
} else {
return [...linePoints];
}
}
const cellSize = Math.floor(EXTENT / granularity);
const finalLineVertices: Array<Point> = [];
finalLineVertices.push(new Point(linePoints[0].x, linePoints[0].y));
// Iterate over all input lines
const totalPoints = linePoints.length;
const lastIndex = addLastToFirstSegment ? totalPoints : (totalPoints - 1);
for (let pointIndex = 0; pointIndex < lastIndex; pointIndex++) {
const linePoint0 = linePoints[pointIndex];
const linePoint1 = pointIndex < (totalPoints - 1) ? linePoints[pointIndex + 1] : linePoints[0];
const lineVertex0x = linePoint0.x;
const lineVertex0y = linePoint0.y;
const lineVertex1x = linePoint1.x;
const lineVertex1y = linePoint1.y;
const dirXnonZero = lineVertex0x !== lineVertex1x;
const dirYnonZero = lineVertex0y !== lineVertex1y;
if (!dirXnonZero && !dirYnonZero) {
continue;
}
const dirX = lineVertex1x - lineVertex0x;
const dirY = lineVertex1y - lineVertex0y;
const absDirX = Math.abs(dirX);
const absDirY = Math.abs(dirY);
let lastPointX = lineVertex0x;
let lastPointY = lineVertex0y;
// Walk along the line segment from start to end. In every step,
// find out the distance from start until the line intersects either the X-parallel or Y-parallel subdivision axis.
// Pick the closer intersection, add it to the final line points and consider that point the new start of the line.
// But also make sure the intersection point does not lie beyond the end of the line.
// If none of the intersection points is closer than line end, add the endpoint to the final line and break the loop.
while (true) {
const nextBoundaryX = dirX > 0 ?
((Math.floor(lastPointX / cellSize) + 1) * cellSize) :
((Math.ceil(lastPointX / cellSize) - 1) * cellSize);
const nextBoundaryY = dirY > 0 ?
((Math.floor(lastPointY / cellSize) + 1) * cellSize) :
((Math.ceil(lastPointY / cellSize) - 1) * cellSize);
const axisDistanceToBoundaryX = Math.abs(lastPointX - nextBoundaryX);
const axisDistanceToBoundaryY = Math.abs(lastPointY - nextBoundaryY);
const axisDistanceToEndX = Math.abs(lastPointX - lineVertex1x);
const axisDistanceToEndY = Math.abs(lastPointY - lineVertex1y);
const realDistanceToBoundaryX = dirXnonZero ? axisDistanceToBoundaryX / absDirX : Number.POSITIVE_INFINITY;
const realDistanceToBoundaryY = dirYnonZero ? axisDistanceToBoundaryY / absDirY : Number.POSITIVE_INFINITY;
if ((axisDistanceToEndX <= axisDistanceToBoundaryX || !dirXnonZero) &&
(axisDistanceToEndY <= axisDistanceToBoundaryY || !dirYnonZero)) {
break;
}
if ((realDistanceToBoundaryX < realDistanceToBoundaryY && dirXnonZero) || !dirYnonZero) {
// We hit the X cell boundary first
// Always consider the X cell hit if Y dir is zero
lastPointX = nextBoundaryX;
lastPointY = lastPointY + dirY * realDistanceToBoundaryX;
const next = new Point(lastPointX, Math.round(lastPointY));
// Do not add the next vertex if it is equal to the last added vertex
if (finalLineVertices[finalLineVertices.length - 1].x !== next.x ||
finalLineVertices[finalLineVertices.length - 1].y !== next.y) {
finalLineVertices.push(next);
}
} else {
lastPointX = lastPointX + dirX * realDistanceToBoundaryY;
lastPointY = nextBoundaryY;
const next = new Point(Math.round(lastPointX), lastPointY);
if (finalLineVertices[finalLineVertices.length - 1].x !== next.x ||
finalLineVertices[finalLineVertices.length - 1].y !== next.y) {
finalLineVertices.push(next);
}
}
}
const last = new Point(lineVertex1x, lineVertex1y);
if (finalLineVertices[finalLineVertices.length - 1].x !== last.x ||
finalLineVertices[finalLineVertices.length - 1].y !== last.y) {
finalLineVertices.push(last);
}
}
return finalLineVertices;
}
/**
* Takes a polygon as an array of point rings, returns a flattened array of the X,Y coordinates of these points.
* Also creates an array of hole indices. Both returned arrays are required for `earcut`.
*/
function flatten(polygon: Array<Array<Point>>): {
flattened: Array<number>;
holeIndices: Array<number>;
} {
const holeIndices = [];
const flattened = [];
for (const ring of polygon) {
if (ring.length === 0) {
continue;
}
if (ring !== polygon[0]) {
holeIndices.push(flattened.length / 2);
}
for (let i = 0; i < ring.length; i++) {
flattened.push(ring[i].x);
flattened.push(ring[i].y);
}
}
return {
flattened,
holeIndices
};
}
/**
* Returns a new array of indices where all triangles have the counter-clockwise winding order.
* @param flattened - Flattened vertex buffer.
* @param indices - Triangle indices.
*/
export function fixWindingOrder(flattened: Array<number>, indices: Array<number>): Array<number> {
const corrected = [];
for (let i = 0; i < indices.length; i += 3) {
const i0 = indices[i];
const i1 = indices[i + 1];
const i2 = indices[i + 2];
const v0x = flattened[i0 * 2];
const v0y = flattened[i0 * 2 + 1];
const v1x = flattened[i1 * 2];
const v1y = flattened[i1 * 2 + 1];
const v2x = flattened[i2 * 2];
const v2y = flattened[i2 * 2 + 1];
const e0x = v1x - v0x;
const e0y = v1y - v0y;
const e1x = v2x - v0x;
const e1y = v2y - v0y;
const crossProduct = e0x * e1y - e0y * e1x;
if (crossProduct > 0) {
// Flip
corrected.push(i0);
corrected.push(i2);
corrected.push(i1);
} else {
// Don't flip
corrected.push(i0);
corrected.push(i1);
corrected.push(i2);
}
}
return corrected;
}
/**
* Triangulates a ring of vertex indices. Appends to the supplied array of final triangle indices.
* @param vertexBuffer - Flattened vertex coordinate array.
* @param ring - Ordered ring of vertex indices to triangulate.
* @param leftmostIndex - The index of the leftmost vertex in the supplied ring.
* @param finalIndices - Array of final triangle indices, into where the resulting triangles are appended.
*/
export function scanlineTriangulateVertexRing(vertexBuffer: Array<number>, ring: Array<number>, finalIndices: Array<number>): void {
// Triangulate the ring
// It is guaranteed to be convex and ordered
if (ring.length === 0) {
throw new Error('Subdivision vertex ring is empty.');
}
// Find the leftmost vertex in the ring
let leftmostIndex = 0;
let leftmostX = vertexBuffer[ring[0] * 2];
for (let i = 1; i < ring.length; i++) {
const x = vertexBuffer[ring[i] * 2];
if (x < leftmostX) {
leftmostX = x;
leftmostIndex = i;
}
}
// Traverse the ring in both directions from the leftmost vertex
// Assume ring is in CCW order (to produce CCW triangles)
const ringVertexLength = ring.length;
let lastEdgeA = leftmostIndex;
let lastEdgeB = (lastEdgeA + 1) % ringVertexLength;
while (true) {
const candidateIndexA = (lastEdgeA - 1) >= 0 ? (lastEdgeA - 1) : (ringVertexLength - 1);
const candidateIndexB = (lastEdgeB + 1) % ringVertexLength;
// Pick candidate, move edge
const candidateAx = vertexBuffer[ring[candidateIndexA] * 2];
const candidateAy = vertexBuffer[ring[candidateIndexA] * 2 + 1];
const candidateBx = vertexBuffer[ring[candidateIndexB] * 2];
const candidateBy = vertexBuffer[ring[candidateIndexB] * 2 + 1];
const lastEdgeAx = vertexBuffer[ring[lastEdgeA] * 2];
const lastEdgeAy = vertexBuffer[ring[lastEdgeA] * 2 + 1];
const lastEdgeBx = vertexBuffer[ring[lastEdgeB] * 2];
const lastEdgeBy = vertexBuffer[ring[lastEdgeB] * 2 + 1];
let pickA = false;
if (candidateAx < candidateBx) {
pickA = true;
} else if (candidateAx > candidateBx) {
pickA = false;
} else {
// Pick the candidate that is more "right" of the last edge's line
const ex = lastEdgeBx - lastEdgeAx;
const ey = lastEdgeBy - lastEdgeAy;
const nx = ey;
const ny = -ex;
const sign = (lastEdgeAy < lastEdgeBy) ? 1 : -1;
// dot( (candidateA <-- lastEdgeA), normal )
const aRight = ((candidateAx - lastEdgeAx) * nx + (candidateAy - lastEdgeAy) * ny) * sign;
// dot( (candidateB <-- lastEdgeA), normal )
const bRight = ((candidateBx - lastEdgeAx) * nx + (candidateBy - lastEdgeAy) * ny) * sign;
if (aRight > bRight) {
pickA = true;
}
}
if (pickA) {
// Pick candidate A
const c = ring[candidateIndexA];
const a = ring[lastEdgeA];
const b = ring[lastEdgeB];
if (c !== a && c !== b && a !== b) {
finalIndices.push(b, a, c);
}
lastEdgeA--;
if (lastEdgeA < 0) {
lastEdgeA = ringVertexLength - 1;
}
} else {
// Pick candidate B
const c = ring[candidateIndexB];
const a = ring[lastEdgeA];
const b = ring[lastEdgeB];
if (c !== a && c !== b && a !== b) {
finalIndices.push(b, a, c);
}
lastEdgeB++;
if (lastEdgeB >= ringVertexLength) {
lastEdgeB = 0;
}
}
if (candidateIndexA === candidateIndexB) {
break; // We ran out of ring vertices
}
}
}