From 422bd20e11b214e93185d249edd646d5898dce8c Mon Sep 17 00:00:00 2001 From: Ib Green Date: Sat, 17 Aug 2024 10:54:38 -0400 Subject: [PATCH] fix(polygon): Improve speed of getSignedArea() --- .github/workflows/test.yml | 4 ++ modules/polygon/src/polygon-utils.ts | 31 +++++++++++- modules/polygon/src/polygon.ts | 11 +++-- modules/polygon/test/bench.ts | 74 ++++++++++++++++++++-------- test/bench/modules.bench.ts | 6 +-- 5 files changed, 96 insertions(+), 30 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0855f8ad..5efdd208 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,3 +39,7 @@ jobs: uses: coverallsapp/github-action@09b709cf6a16e30b0808ba050c7a6e8a5ef13f8d # v1.2.5 with: github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Bench + run: | + yarn test bench \ No newline at end of file diff --git a/modules/polygon/src/polygon-utils.ts b/modules/polygon/src/polygon-utils.ts index 5d114edc..ccbd49e9 100644 --- a/modules/polygon/src/polygon-utils.ts +++ b/modules/polygon/src/polygon-utils.ts @@ -110,13 +110,16 @@ export const DimIndex: Record = { * @returns Signed area of the polygon. * https://en.wikipedia.org/wiki/Shoelace_formula */ -export function getPolygonSignedArea(points: NumericArray, options: PolygonParams = {}): number { +export function getPolygonSignedArea( + points: NumericArray, + options: PolygonParams & {fast?: boolean} = {} +): 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]]; + let area = 0; 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; @@ -124,6 +127,30 @@ export function getPolygonSignedArea(points: NumericArray, options: PolygonParam return area / 2; } +export function getPolygonSignedAreaFlatFast( + points: NumericArray, + options: PolygonParams = {} +): number { + // const {start = 0} = options; TODO - fast + if (options.start) { + throw new Error('start option is not supported in fast mode'); + } + const {end = points.length, plane = 'xy'} = options; + const dim = options.size || 2; + const i0 = DimIndex[plane[0]]; + const i1 = DimIndex[plane[1]]; + + const Area = function (curr: number, prev: number): number { + return (points[curr + i0] - points[prev + i0]) * (points[curr + i1] - points[prev + i1]); + }; + + let area = Area(0, end - dim); + for (let i = dim; i < end; i += dim) { + area += Area(i, i - dim); + } + return area / 2; +} + /** * Calls the visitor callback for each segment in the polygon. * @param points An array that represents points of the polygon diff --git a/modules/polygon/src/polygon.ts b/modules/polygon/src/polygon.ts index 4030e9dc..3f5eb2e4 100644 --- a/modules/polygon/src/polygon.ts +++ b/modules/polygon/src/polygon.ts @@ -9,6 +9,7 @@ import type {NumericArray} from '@math.gl/core'; import { getPolygonSignedArea, + getPolygonSignedAreaFlatFast, forEachSegmentInPolygon, modifyPolygonWindingDirection, getPolygonSignedAreaPoints, @@ -46,10 +47,12 @@ export class Polygon { * Returns signed area of the polygon. * @returns Signed area of the polygon. */ - getSignedArea(): number { - if (this.isFlatArray) return getPolygonSignedArea(this.points as NumericArray, this.options); - - return getPolygonSignedAreaPoints(this.points as number[][], this.options); + getSignedArea(fast?: boolean): number { + return this.isFlatArray + ? fast + ? getPolygonSignedAreaFlatFast(this.points as NumericArray, this.options) + : getPolygonSignedArea(this.points as NumericArray, this.options) + : getPolygonSignedAreaPoints(this.points as number[][], this.options); } /** diff --git a/modules/polygon/test/bench.ts b/modules/polygon/test/bench.ts index f36ac7d1..61d112e9 100644 --- a/modules/polygon/test/bench.ts +++ b/modules/polygon/test/bench.ts @@ -8,15 +8,23 @@ // @ts-nocheck import {earcut, Polygon, modifyPolygonWindingDirection, WINDING} from '@math.gl/polygon'; import {toNested} from './utils'; +import {fstat} from 'fs'; -const polygonSmall = [0, 0, 1, 1, 0, 2, -1, 1, -1.25, 0.5, 0, 0]; +const flatPolygonSmall = [0, 0, 1, 1, 0, 2, -1, 1, -1.25, 0.5, 0, 0]; -const polygonMedium = [ +const flatPolygonMedium = [ 4.2625, 2.24125, 3.0025, 3.20125, 2.5225, 4.22125, 0.9225, 4.32125, -0.3775, 3.30125, -0.7975, 2.14125, -1.8575, 2.72125, -1.8575, 0.64125, -0.3775, -0.89875, -0.3775, -0.89875, 1.2825, 0.92125, 1.4025, -0.89875, 2.9025, -0.31875, 4.0825, 0.62125, 4.2625, 2.24125 ]; -const polygonMediumNested = toNested(polygonMedium); +const nestedPolygonMedium = toNested(flatPolygonMedium); + +const flatPolygonLarge = new Array(10000) + .fill(0) + .map((_, i) => + i % 2 === 0 ? Math.cos((Math.PI * 2 * i) / 5000) : Math.sin((Math.PI * 2 * i) / 5000) + ); +const nestedPolygonLarge = toNested(flatPolygonLarge); // A helper function to swap winding direction on each iteration. let winding = WINDING.CLOCKWISE; @@ -28,44 +36,68 @@ function nextWinding() { export function polygonBench(suite, addReferenceBenchmarks) { suite .group('Polygon') - .add('Polygon#new()', () => new Polygon(polygonSmall)) - .add('Polygon#modifyWindingDirection() S', () => { - const polygon = new Polygon(polygonSmall); + .add('Polygon#new()', () => new Polygon(flatPolygonSmall)) + .add('Polygon#modifyWindingDirection() flat small', () => { + const polygon = new Polygon(flatPolygonSmall); polygon.modifyWindingDirection(nextWinding()); }) - .add('modifyPolygonWindingDirection() S', () => { - modifyPolygonWindingDirection(polygonSmall, nextWinding(), { + .add('modifyPolygonWindingDirection() flat small', () => { + modifyPolygonWindingDirection(flatPolygonSmall, nextWinding(), { isClosed: true, start: 0, - end: polygonSmall.length, + end: flatPolygonSmall.length, size: 2 }); }) - .add('Polygon#modifyWindingDirection() M', () => { - const polygon = new Polygon(polygonMedium); + .add('Polygon#modifyWindingDirection() flat medium', () => { + const polygon = new Polygon(flatPolygonMedium); polygon.modifyWindingDirection(nextWinding()); }) - .add('modifyPolygonWindingDirection() M', () => { - modifyPolygonWindingDirection(polygonMedium, nextWinding(), { + .add('modifyPolygonWindingDirection() flat medium', () => { + modifyPolygonWindingDirection(flatPolygonMedium, nextWinding(), { isClosed: true, start: 0, - end: polygonMedium.length, + end: flatPolygonMedium.length, size: 2 }); }) - .add('Polygon#getSignedArea()', () => { - const polygon = new Polygon(polygonMedium); + .add('Polygon#getSignedArea() flat medium', () => { + const polygon = new Polygon(flatPolygonMedium); polygon.getSignedArea(); }) - .add('Polygon#getSignedArea() nested', () => { - const polygon = new Polygon(polygonMediumNested); + .add('Polygon#getSignedArea(fast) flat medium', () => { + const polygon = new Polygon(flatPolygonMedium); + polygon.getSignedArea(true); + }) + .add('Polygon#getSignedArea() nested medium', () => { + const polygon = new Polygon(nestedPolygonMedium); + polygon.getSignedArea(); + }) + .add('Polygon#getSignedArea(fast) nested medium', () => { + const polygon = new Polygon(nestedPolygonMedium); + polygon.getSignedArea(true); + }) + .add('Polygon#getSignedArea() flat large', () => { + const polygon = new Polygon(flatPolygonLarge); polygon.getSignedArea(); }) - .add('earcut with precomputed areas ', () => { - earcut(polygonMedium, [], 2, [-21.3664]); + .add('Polygon#getSignedArea(fast) flat large ', () => { + const polygon = new Polygon(flatPolygonLarge); + polygon.getSignedArea(true); + }) + .add('Polygon#getSignedArea() nested large', () => { + const polygon = new Polygon(nestedPolygonLarge); + polygon.getSignedArea(); + }) + .add('Polygon#getSignedArea(fast) nested large', () => { + const polygon = new Polygon(nestedPolygonLarge); + polygon.getSignedArea(true); + }) + .add('earcut with precomputed areas', () => { + earcut(flatPolygonMedium, [], 2, [-21.3664]); }) .add('earcut', () => { - earcut(polygonMedium, [], 2); + earcut(flatPolygonMedium, [], 2); }); return suite; diff --git a/test/bench/modules.bench.ts b/test/bench/modules.bench.ts index 87c6fd78..3f876b37 100644 --- a/test/bench/modules.bench.ts +++ b/test/bench/modules.bench.ts @@ -9,9 +9,9 @@ import {cullingBench} from '../../modules/culling/test/bench'; import {polygonBench} from '../../modules/polygon/test/bench'; export default function addBenchmarks(suite: Bench, addReferenceBenchmarks: boolean): Bench { - coreBench(suite, addReferenceBenchmarks); - geospatialBench(suite, addReferenceBenchmarks); - cullingBench(suite, addReferenceBenchmarks); + // coreBench(suite, addReferenceBenchmarks); + // geospatialBench(suite, addReferenceBenchmarks); + // cullingBench(suite, addReferenceBenchmarks); polygonBench(suite, addReferenceBenchmarks); return suite;