@math.gl/polygon
Version:
Polygon/polyline processing utilities
267 lines (241 loc) • 7.7 kB
text/typescript
// math.gl
// SPDX-License-Identifier: MIT and ISC
// Copyright (c) vis.gl contributors
/* eslint-disable max-statements, max-depth, complexity, no-unused-expressions */
import {equals} from '@math.gl/core';
import type {NumericArray} from '@math.gl/core';
export const WINDING = {
CLOCKWISE: 1,
COUNTER_CLOCKWISE: -1
} as const;
/** Polygon representation where each point is represented as a separate array of positions. */
type PointsArray = NumericArray[];
/** Segment visitor callback type for polygons defined with flat arrays, */
type SegmentVisitorFlat = (
p1x: number,
p1y: number,
p2x: number,
p2y: number,
i1: number,
i2: number
) => void;
/** Segment visitor callback type for polygons defined with array of points. */
export type SegmentVisitorPoints = (
p1: NumericArray,
p2: NumericArray,
i1: number,
i2: number
) => void;
export type Plane2D = 'xy' | 'yz' | 'xz';
/** Parameters of a polygon. */
type PolygonParams = {
/**
* Start index of the polygon in the array of positions.
* @default `0`
*/
start?: number;
/**
* End index of the polygon in the array of positions.
* @default number of positions
*/
end?: number;
/**
* Size of a point, 2 (XZ) or 3 (XYZ). Affects only polygons stored in flat arrays.
* @default `2`
*/
size?: number;
/**
* Indicates that the first point of the polygon is equal to the last point, and additional checks should be ommited.
*/
isClosed?: boolean;
/**
* The 2D projection plane on which to calculate the area of a 3D polygon.
* @default `'xy'`
*/
plane?: Plane2D;
};
/**
* Checks winding direction of the polygon and reverses the polygon in case of opposite winding direction.
* Note: points are modified in-place.
* @param points An array that represents points of the polygon.
* @param direction Requested winding direction. 1 is for clockwise, -1 for counterclockwise winding direction.
* @param options Parameters of the polygon.
* @return Returns true if the winding direction was changed.
*/
export function modifyPolygonWindingDirection(
points: NumericArray,
direction: number,
options: PolygonParams = {}
): boolean {
const windingDirection = getPolygonWindingDirection(points, options);
if (windingDirection !== direction) {
reversePolygon(points, options);
return true;
}
return false;
}
/**
* Returns winding direction of the polygon.
* @param points An array that represents points of the polygon.
* @param options Parameters of the polygon.
* @returns Winding direction of the polygon.
*/
export function getPolygonWindingDirection(
points: NumericArray,
options: PolygonParams = {}
): number {
return Math.sign(getPolygonSignedArea(points, options));
}
export const DimIndex: Record<string, number> = {
x: 0,
y: 1,
z: 2
} as const;
/**
* Returns signed area of the polygon.
* @param points An array that represents points of the polygon.
* @param options Parameters of the polygon.
* @returns Signed area of the polygon.
* https://en.wikipedia.org/wiki/Shoelace_formula
*/
export function getPolygonSignedArea(points: NumericArray, options: PolygonParams = {}): number {
const {start = 0, end = points.length, plane = 'xy'} = options;
const dim = options.size || 2;
let area = 0;
const i0 = DimIndex[plane[0]];
const i1 = DimIndex[plane[1]];
for (let i = start, j = end - dim; i < end; i += dim) {
area += (points[i + i0] - points[j + i0]) * (points[i + i1] + points[j + i1]);
j = i;
}
return area / 2;
}
/**
* Calls the visitor callback for each segment in the polygon.
* @param points An array that represents points of the polygon
* @param visitor A callback to call for each segment.
* @param options Parameters of the polygon.
*/
export function forEachSegmentInPolygon(
points: NumericArray,
visitor: SegmentVisitorFlat,
options: PolygonParams = {}
): void {
const {start = 0, end = points.length, size = 2, isClosed} = options;
const numPoints = (end - start) / size;
for (let i = 0; i < numPoints - 1; ++i) {
visitor(
points[start + i * size],
points[start + i * size + 1],
points[start + (i + 1) * size],
points[start + (i + 1) * size + 1],
i,
i + 1
);
}
const endPointIndex = start + (numPoints - 1) * size;
const isClosedEx =
isClosed ||
(equals(points[start], points[endPointIndex]) &&
equals(points[start + 1], points[endPointIndex + 1]));
if (!isClosedEx) {
visitor(
points[endPointIndex],
points[endPointIndex + 1],
points[start],
points[start + 1],
numPoints - 1,
0
);
}
}
function reversePolygon(
points: NumericArray,
options: {start?: number; end?: number; size?: number}
): void {
const {start = 0, end = points.length, size = 2} = options;
const numPoints = (end - start) / size;
const numSwaps = Math.floor(numPoints / 2);
for (let i = 0; i < numSwaps; ++i) {
const b1 = start + i * size;
const b2 = start + (numPoints - 1 - i) * size;
for (let j = 0; j < size; ++j) {
const tmp = points[b1 + j];
points[b1 + j] = points[b2 + j];
points[b2 + j] = tmp;
}
}
}
/**
* Checks winding direction of the polygon and reverses the polygon in case of opposite winding direction.
* Note: points are modified in-place.
* @param points Array of points that represent the polygon.
* @param direction Requested winding direction. 1 is for clockwise, -1 for counterclockwise winding direction.
* @param options Parameters of the polygon.
* @return Returns true if the winding direction was changed.
*/
export function modifyPolygonWindingDirectionPoints(
points: PointsArray,
direction: number,
options: PolygonParams = {}
): boolean {
const currentDirection = getPolygonWindingDirectionPoints(points, options);
if (currentDirection !== direction) {
points.reverse();
return true;
}
return false;
}
/**
* Returns winding direction of the polygon.
* @param points Array of points that represent the polygon.
* @param options Parameters of the polygon.
* @returns Winding direction of the polygon.
*/
export function getPolygonWindingDirectionPoints(
points: PointsArray,
options: PolygonParams = {}
): number {
return Math.sign(getPolygonSignedAreaPoints(points, options));
}
/**
* Returns signed area of the polygon.
* @param points Array of points that represent the polygon.
* @param options Parameters of the polygon.
* @returns Signed area of the polygon.
*/
export function getPolygonSignedAreaPoints(
points: PointsArray,
options: PolygonParams = {}
): number {
// https://en.wikipedia.org/wiki/Shoelace_formula
const {start = 0, end = points.length, plane = 'xy'} = options;
let area = 0;
const i0 = DimIndex[plane[0]];
const i1 = DimIndex[plane[1]];
for (let i = start, j = end - 1; i < end; ++i) {
area += (points[i][i0] - points[j][i0]) * (points[i][i1] + points[j][i1]);
j = i;
}
return area / 2;
}
/**
* Calls visitor callback for each segment in the polygon.
* @param points Array of points that represent the polygon.
* @param visitor A callback to call for each segment.
* @param options Parameters of the polygon.
*/
export function forEachSegmentInPolygonPoints(
points: PointsArray,
visitor: SegmentVisitorPoints,
options: PolygonParams = {}
): void {
const {start = 0, end = points.length, isClosed} = options;
for (let i = start; i < end - 1; ++i) {
visitor(points[i], points[i + 1], i, i + 1);
}
const isClosedEx = isClosed || equals(points[end - 1], points[0]);
if (!isClosedEx) {
visitor(points[end - 1], points[0], end - 1, 0);
}
}