UNPKG

maplibre-gl

Version:

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

1,104 lines (1,037 loc) 35.6 kB
import {describe, expect, test} from 'vitest'; import Point from '@mapbox/point-geometry'; import {EXTENT} from '../data/extent'; import {scanlineTriangulateVertexRing, subdividePolygon, subdivideVertexLine} from './subdivision'; import {CanonicalTileID} from '../source/tile_id'; /** * With this granularity, all geometry should be subdivided along axes divisible by 4. */ const granularityForInterval4 = EXTENT / 4; const granularityForInterval128 = EXTENT / 128; const canonicalDefault = new CanonicalTileID(20, 1, 1); describe('Line geometry subdivision', () => { test('Line inside cell remains unchanged', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(0, 0), new Point(4, 4), ], granularityForInterval4))).toEqual(toSimplePoints([ new Point(0, 0), new Point(4, 4), ])); expect(toSimplePoints(subdivideVertexLine([ new Point(0, 0), new Point(4, 0), ], granularityForInterval4))).toEqual(toSimplePoints([ new Point(0, 0), new Point(4, 0), ])); expect(toSimplePoints(subdivideVertexLine([ new Point(0, 2), new Point(4, 2), ], granularityForInterval4))).toEqual(toSimplePoints([ new Point(0, 2), new Point(4, 2), ])); }); test('Simple line', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(1, 1), new Point(6, 1), ], granularityForInterval4))).toEqual(toSimplePoints([ new Point(1, 1), new Point(4, 1), new Point(6, 1), ])); }); test('Simple ring', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(0, 0), new Point(8, 0), new Point(0, 8), ], granularityForInterval4, true))).toEqual(toSimplePoints([ new Point(0, 0), new Point(4, 0), new Point(8, 0), new Point(4, 4), new Point(0, 8), new Point(0, 4), new Point(0, 0), ])); }); test('Simple ring inside cell', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(0, 0), new Point(8, 0), new Point(0, 8), ], granularityForInterval128, true))).toEqual(toSimplePoints([ new Point(0, 0), new Point(8, 0), new Point(0, 8), new Point(0, 0), ])); }); test('Simple ring is unchanged when granularity=0', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(0, 0), new Point(8, 0), new Point(0, 8), ], 0, true))).toEqual(toSimplePoints([ new Point(0, 0), new Point(8, 0), new Point(0, 8), new Point(0, 0), ])); }); test('Line lies on subdivision axis', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(1, 0), new Point(6, 0), ], granularityForInterval4))).toEqual(toSimplePoints([ new Point(1, 0), new Point(4, 0), new Point(6, 0), ])); }); test('Line circles a subdivision cell', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(0, 0), new Point(4, 0), new Point(4, 4), new Point(0, 4), new Point(0, 0), ], granularityForInterval4))).toEqual(toSimplePoints([ new Point(0, 0), new Point(4, 0), new Point(4, 4), new Point(0, 4), new Point(0, 0), ])); }); test('Line goes through cell vertices', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(0, 0), new Point(4, 4), new Point(8, 4), new Point(8, 8), ], granularityForInterval4))).toEqual(toSimplePoints([ new Point(0, 0), new Point(4, 4), new Point(8, 4), new Point(8, 8), ])); }); test('Line crosses several cells', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(0, 0), new Point(12, 5), ], granularityForInterval4))).toEqual(toSimplePoints([ new Point(0, 0), new Point(4, 2), new Point(8, 3), new Point(10, 4), new Point(12, 5), ])); }); test('Line crosses several cells in negative coordinates', () => { // Same geometry as the previous test, just shifted by -1000 in both axes expect(toSimplePoints(subdivideVertexLine([ new Point(-1000, -1000), new Point(-1012, -1005), ], granularityForInterval4))).toEqual(toSimplePoints([ new Point(-1000, -1000), new Point(-1004, -1002), new Point(-1008, -1003), new Point(-1010, -1004), new Point(-1012, -1005), ])); }); test('Line is unmodified at granularity 1', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(-EXTENT * 4, 0), new Point(EXTENT * 4, 0), ], 1))).toEqual(toSimplePoints([ new Point(-EXTENT * 4, 0), new Point(EXTENT * 4, 0), ])); }); test('Line is unmodified at granularity 0', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(-EXTENT * 4, 0), new Point(EXTENT * 4, 0), ], 0))).toEqual(toSimplePoints([ new Point(-EXTENT * 4, 0), new Point(EXTENT * 4, 0), ])); }); test('Line is unmodified at granularity -2', () => { expect(toSimplePoints(subdivideVertexLine([ new Point(-EXTENT * 4, 0), new Point(EXTENT * 4, 0), ], -2))).toEqual(toSimplePoints([ new Point(-EXTENT * 4, 0), new Point(EXTENT * 4, 0), ])); }); }); describe('Fill subdivision', () => { test('Polygon is unchanged when granularity=1', () => { const result = subdividePolygon( [ [ // x, y new Point(0, 0), new Point(20000, 0), new Point(20000, 20000), new Point(0, 20000), ] ], canonicalDefault, 1 ); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); expect(result.verticesFlattened).toEqual([ 0, 0, 20000, 0, 20000, 20000, 0, 20000 ]); expect(result.indicesTriangles).toEqual([2, 0, 3, 0, 2, 1]); expect(result.indicesLineList).toEqual([ [ 0, 1, 1, 2, 2, 3, 3, 0 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Polygon is unchanged when granularity=1, but winding order is corrected.', () => { const result = subdividePolygon( [ [ // x, y new Point(0, 0), new Point(0, 20000), new Point(20000, 20000), new Point(20000, 0), ] ], canonicalDefault, 1 ); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); expect(result.verticesFlattened).toEqual([ 0, 0, 0, 20000, 20000, 20000, 20000, 0 ]); expect(result.indicesTriangles).toEqual([1, 3, 0, 3, 1, 2]); expect(result.indicesLineList).toEqual([ [ 0, 1, 1, 2, 2, 3, 3, 0 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Polygon inside cell is unchanged', () => { const result = subdividePolygon( [ [ // x, y new Point(0, 0), new Point(2, 0), new Point(2, 2), new Point(0, 2), ] ], canonicalDefault, granularityForInterval4 ); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); expect(result.verticesFlattened).toEqual([ 0, 0, 2, 0, 2, 2, 0, 2 ]); expect(result.indicesTriangles).toEqual([0, 3, 2, 1, 0, 2]); expect(result.indicesLineList).toEqual([ [ 0, 1, 1, 2, 2, 3, 3, 0 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Subdivide a polygon', () => { const result = subdividePolygon([ [ new Point(0, 0), new Point(8, 0), new Point(0, 8), ], [ new Point(1, 1), new Point(5, 1), new Point(1, 5), ] ], canonicalDefault, granularityForInterval4); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); expect(result.verticesFlattened).toEqual([ // // indices: 0, 0, // 0 8, 0, // 1 0, 8, // 2 1, 1, // 3 5, 1, // 4 1, 5, // 5 1, 4, // 6 4, 1, // 7 0, 4, // 8 4, 0, // 9 4, 4, // 10 2, 4, // 11 4, 3, // 12 4, 2 // 13 ]); // X: 0 1 2 3 4 5 6 7 8 // Y: | | | | | | | | | // 0: 0 9 1 // // 1: 3 7 4 // // 2: 13 // // 3: 12 // // 4: 8 6 11 10 // // 5: 5 // // 6: // // 7: // // 8: 2 expect(result.indicesTriangles).toEqual([ 3, 0, 6, 7, 0, 3, 0, 8, 6, 6, 8, 2, 6, 2, 5, 9, 0, 7, 9, 7, 4, 9, 4, 1, 12, 11, 10, 12, 10, 1, 5, 2, 11, 11, 2, 10, 13, 11, 12, 13, 12, 4, 4, 12, 1 ]); // X: 0 1 2 3 4 5 6 7 8 // Y: | | | | | | | | | // 0: 0⎼⎼⎽⎽__---------9\--------------1 // | ⟍ ⎺⎺⎻⎻⎼⎼⎽⎽ | ⟍ _⎼⎼⎻⎻⎺╱ // 1: || 3-----------7---4⎻⎻⎺ ╱╱ // || | ╱ ╱ ╱ // 2: |⎹ | 13╱╱ ╱ ╱ // | ⎹ | ╱ |╱ ╱ ╱ // 3: | || ╱ _12 ╱ // | ⎹| ╱_⎻⎺⎺ | ╱ // 4: 8---6 11------10╱ // | ⎹| ╱ ⎸ ╱ // 5: | ⎹ 5 ⎸ ╱ // | ⎸| ⎸ ╱ // 6: |⎹⎹ ⎸ ╱ // |⎹⎸ ⎸ ╱ // 7: || ⎸ ╱ // |⎸⎸╱ // 8: 2╱ expect(result.indicesLineList).toEqual([ [ 0, 9, 9, 1, 1, 10, 10, 2, 2, 8, 8, 0 ], [ 3, 7, 7, 4, 4, 13, 13, 11, 11, 5, 5, 6, 6, 3 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); describe('Polygon outline line list is correct', () => { test('Subcell polygon', () => { const result = subdividePolygon([ [ new Point(17, 127), new Point(19, 111), new Point(126, 13), ] ], canonicalDefault, granularityForInterval128); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Small polygon', () => { const result = subdividePolygon([ [ new Point(17, 15), new Point(261, 13), new Point(19, 273), ] ], canonicalDefault, granularityForInterval128); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Medium polygon', () => { const result = subdividePolygon([ [ new Point(17, 127), new Point(1029, 13), new Point(127, 1045), ] ], canonicalDefault, granularityForInterval128); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Large polygon', () => { const result = subdividePolygon([ [ new Point(17, 127), new Point(8001, 13), new Point(127, 8003), ] ], canonicalDefault, granularityForInterval128); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Large polygon with hole', () => { const result = subdividePolygon([ [ new Point(17, 127), new Point(8001, 13), new Point(127, 8003), ], [ new Point(1001, 1002), new Point(1502, 1008), new Point(1004, 1523), ] ], canonicalDefault, granularityForInterval128); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Large polygon with hole, granularity=0', () => { const result = subdividePolygon([ [ new Point(17, 127), new Point(8001, 13), new Point(127, 8003), ], [ new Point(1001, 1002), new Point(1502, 1008), new Point(1004, 1523), ] ], canonicalDefault, 0); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Large polygon with hole, finer granularity', () => { const result = subdividePolygon([ [ new Point(17, 1), new Point(347, 13), new Point(19, 453), ], [ new Point(23, 7), new Point(319, 17), new Point(29, 399), ] ], canonicalDefault, EXTENT / 8); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); // This polygon subdivision results in at least one edge that is shared among more than 2 triangles. // This is not ideal, but it is also an edge case of a weird triangle getting subdivided by a very fine grid. // Furthermore, one edge shared by multiple triangles is not a problem for map rendering, // but it should *not* occur when subdividing any simple geometry. //testMeshIntegrity(result.indicesTriangles); // Polygon outline match test also fails for this specific edge case. //testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList); }); test('Polygon with hole inside cell', () => { // 0 // / \ // / 3 \ // / / \ \ // / / \ \ // / 5⎺⎺⎺⎺4 \ // 2⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺1 const result = subdividePolygon( [ [ new Point(0, 0), new Point(3, 4), new Point(-3, 4), ], [ new Point(0, 1), new Point(1, 3), new Point(-1, 3), ] ], canonicalDefault, 0 ); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); expect(result.verticesFlattened).toEqual([ 0, 0, // 0 3, 4, // 1 -3, 4, // 2 0, 1, // 3 1, 3, // 4 -1, 3 // 5 ]); expect(result.indicesTriangles).toEqual([ 2, 4, 5, 3, 2, 5, 1, 4, 2, 3, 0, 2, 0, 4, 1, 4, 0, 3 ]); expect(result.indicesLineList).toEqual([ [ 0, 1, 1, 2, 2, 0 ], [ 3, 4, 4, 5, 5, 3 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Polygon with duplicate vertex with hole inside cell', () => { // 0 // / \ // // \\ // // \\ // /4⎺⎺⎺⎺⎺3\ // 2⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺1 const result = subdividePolygon( [ [ new Point(0, 0), new Point(3, 4), new Point(-3, 4), ], [ new Point(0, 0), new Point(1, 3), new Point(-1, 3), ] ], canonicalDefault, 0 ); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); expect(result.verticesFlattened).toEqual([ 0, 0, // 0 3, 4, // 1 -3, 4, // 2 1, 3, // 3 -1, 3 // 4 ]); expect(result.indicesTriangles).toEqual([ 2, 3, 4, 0, 2, 4, 3, 1, 0, 1, 3, 2 ]); expect(result.indicesLineList).toEqual([ [ 0, 1, 1, 2, 2, 0 ], [ 0, 3, 3, 4, 4, 0 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Polygon with duplicate edge inside cell', () => { // Test a slightly degenerate polygon, where the hole is achieved using a duplicate edge // 0 // /|\ // / 3 \ // / / \ \ // / / \ \ // / 4⎺⎺⎺⎺⎺5 \ // 2⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺1 const result = subdividePolygon( [ [ new Point(0, 0), new Point(3, 4), new Point(-3, 4), new Point(0, 0), new Point(0, 1), new Point(-1, 3), new Point(1, 3), new Point(0, 1), new Point(0, 0), ] ], canonicalDefault, 0 ); expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false); testMeshIntegrity(result.indicesTriangles); expect(result.verticesFlattened).toEqual([ 0, 0, // 0 3, 4, // 1 -3, 4, // 2 0, 1, // 3 -1, 3, // 4 1, 3 // 5 ]); expect(result.indicesTriangles).toEqual([ 3, 1, 0, 2, 3, 0, 5, 1, 3, 2, 4, 3, 4, 1, 5, 1, 4, 2 ]); expect(result.indicesLineList).toEqual([ [ 0, 1, 1, 2, 2, 0, 0, 3, 3, 4, 4, 5, 5, 3, 3, 0 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); }); test('Generates pole geometry for both poles', () => { const result = subdividePolygon( [ [ // x, y new Point(0, 0), new Point(EXTENT, 0), new Point(EXTENT, EXTENT), new Point(0, EXTENT), ] ], new CanonicalTileID(0, 0, 0), 2 ); expect(result.verticesFlattened).toEqual([ 0, 0, // 0 8192, 0, // 1 8192, 8192, // 2 0, 8192, // 3 0, 4096, // 4 4096, 4096, // 5 4096, 8192, // 6 4096, 0, // 7 8192, 4096, // 8 0, 32767, // 9 - South pole - 3 vertices 4096, 32767, // 10 8192, 32767, // 11 4096, -32768, // 12 - North pole - 3 vertices 0, -32768, // 13 8192, -32768 // 14 ]); // 0 4096 8192 // | | | // -32K: 13 12 14 // // 0: 0 7 1 // // 4096: 4 5 8 // // 8192: 3 6 2 // // 32K: 9 10 11 expect(result.indicesTriangles).toEqual([ 0, 4, 5, 4, 3, 5, 5, 3, 6, 5, 6, 2, 7, 0, 5, 7, 5, 1, 1, 5, 8, 8, 5, 2, 6, 3, 9, 10, 6, 9, 2, 6, 10, 11, 2, 10, 0, 7, 12, 13, 0, 12, 7, 1, 14, 12, 7, 14 ]); // The outline intersects the added pole geometry - but that shouldn't be an issue. expect(result.indicesLineList).toEqual([ [ 0, 7, 7, 1, 1, 8, 8, 2, 2, 6, 6, 3, 3, 4, 4, 0 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Generates pole geometry for north pole only (geometry not bordering other pole)', () => { const result = subdividePolygon( [ [ // x, y new Point(0, 0), new Point(EXTENT, 0), new Point(EXTENT, EXTENT), // Note that one of the vertices touches the south edge... new Point(0, EXTENT - 1), // ...the other does not. ] ], new CanonicalTileID(0, 0, 0), 1 ); expect(result.verticesFlattened).toEqual([ 0, 0, 8192, 0, 8192, 8192, 0, 8191, 8192, -32768, 0, -32768 ]); expect(result.indicesTriangles).toEqual([ 2, 0, 3, 0, 2, 1, 0, 1, 4, 5, 0, 4 ]); expect(result.indicesLineList).toEqual([ [ 0, 1, 1, 2, 2, 3, 3, 0 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Generates pole geometry for south pole only (geometry not bordering other pole)', () => { const result = subdividePolygon( [ [ // x, y new Point(0, 0), new Point(EXTENT, 1), new Point(EXTENT, EXTENT), new Point(0, EXTENT), ] ], new CanonicalTileID(0, 0, 0), 1 ); expect(result.verticesFlattened).toEqual([ 0, 0, 8192, 1, 8192, 8192, 0, 8192, 0, 32767, 8192, 32767 ]); expect(result.indicesTriangles).toEqual([ 2, 0, 3, 0, 2, 1, 2, 3, 4, 5, 2, 4 ]); expect(result.indicesLineList).toEqual([ [ 0, 1, 1, 2, 2, 3, 3, 0 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Generates pole geometry for north pole only (tile not bordering other pole)', () => { const result = subdividePolygon( [ [ // x, y new Point(0, 0), new Point(EXTENT, 0), new Point(EXTENT, EXTENT), new Point(0, EXTENT), ] ], new CanonicalTileID(1, 0, 0), 1 ); expect(result.verticesFlattened).toEqual([ 0, 0, 8192, 0, 8192, 8192, 0, 8192, 8192, -32768, 0, -32768 ]); expect(result.indicesTriangles).toEqual([ 2, 0, 3, 0, 2, 1, 0, 1, 4, 5, 0, 4 ]); expect(result.indicesLineList).toEqual([ [ 0, 1, 1, 2, 2, 3, 3, 0 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Generates pole geometry for south pole only (tile not bordering other pole)', () => { const result = subdividePolygon( [ [ // x, y new Point(0, 0), new Point(EXTENT, 0), new Point(EXTENT, EXTENT), new Point(0, EXTENT), ] ], new CanonicalTileID(1, 0, 1), 1 ); expect(result.verticesFlattened).toEqual([ 0, 0, 8192, 0, 8192, 8192, 0, 8192, 0, 32767, 8192, 32767 ]); expect(result.indicesTriangles).toEqual([ 2, 0, 3, 0, 2, 1, 2, 3, 4, 5, 2, 4 ]); expect(result.indicesLineList).toEqual([ [ 0, 1, 1, 2, 2, 3, 3, 0 ] ]); checkWindingOrder(result.verticesFlattened, result.indicesTriangles); }); test('Scanline subdivision ring generation case 1', () => { // Check ring generation on data where it was actually failing const vertices = [ 243, 152, // 0 240, 157, // 1 237, 160, // 2 232, 160, // 3 226, 160, // 4 232, 153, // 5 232, 152, // 6 240, 152 // 7 ]; // This vertex ring is slightly degenerate (4-5-6 is concave) // 226 232 237 240 243 // | | | | | // 152: 6 7 0 // 153: 5 // // // // 157: 1 // // // 160: 4 3 2 const ring = [0, 1, 2, 3, 4, 5, 6, 7]; const finalIndices = []; scanlineTriangulateVertexRing(vertices, ring, finalIndices); checkWindingOrder(vertices, finalIndices); }); test('Scanline subdivision ring generation case 2', () => { // It should pass on this data const vertices = [210, 160, 216, 153, 217, 152, 224, 152, 232, 152, 232, 152, 232, 153, 226, 160, 224, 160, 216, 160]; const ring = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; const finalIndices = []; scanlineTriangulateVertexRing(vertices, ring, finalIndices); checkWindingOrder(vertices, finalIndices); }); }); /** * Converts an array of points into an array of simple \{x, y\} objects. * Jest prints much nicer comparisons on arrays of these simple objects than on * arrays of points. */ function toSimplePoints(a: Array<Point>): Array<{x: number; y: number}> { const result = []; for (let i = 0; i < a.length; i++) { result.push({ x: a[i].x, y: a[i].y, }); } return result; } function getEdgeOccurrencesMap(triangleIndices: Array<number>): Map<string, number> { const edgeOccurrences = new Map<string, number>(); for (let triangleIndex = 0; triangleIndex < triangleIndices.length; triangleIndex += 3) { const i0 = triangleIndices[triangleIndex]; const i1 = triangleIndices[triangleIndex + 1]; const i2 = triangleIndices[triangleIndex + 2]; for (const edge of [[i0, i1], [i1, i2], [i2, i0]]) { const e0 = Math.min(edge[0], edge[1]); const e1 = Math.max(edge[0], edge[1]); const key = `${e0}_${e1}`; if (edgeOccurrences.has(key)) { edgeOccurrences.set(key, edgeOccurrences.get(key) + 1); } else { edgeOccurrences.set(key, 1); } } } return edgeOccurrences; } /** * Checks that the supplied mesh has no edge that is shared by more than 2 triangles. */ function testMeshIntegrity(triangleIndices: Array<number>) { const edgeOccurrences = getEdgeOccurrencesMap(triangleIndices); for (const pair of edgeOccurrences) { if (pair[1] > 2) { throw new Error(`Polygon contains an edge with indices ${pair[0].replace('_', ', ')} that is shared by more than 2 triangles.`); } } } /** * Checks that the lines in `lineIndicesLists` actually match the exposed edges of the triangle mesh in `triangleIndices`. */ function testPolygonOutlineMatches(triangleIndices: Array<number>, lineIndicesLists: Array<Array<number>>): void { const edgeOccurrences = getEdgeOccurrencesMap(triangleIndices); const uncoveredEdges = new Set<string>(); for (const pair of edgeOccurrences) { if (pair[1] === 1) { uncoveredEdges.add(pair[0]); } } const outlineEdges = new Set<string>(); for (const lines of lineIndicesLists) { for (let i = 0; i < lines.length; i += 2) { const i0 = lines[i]; const i1 = lines[i + 1]; const e0 = Math.min(i0, i1); const e1 = Math.max(i0, i1); const key = `${e0}_${e1}`; if (outlineEdges.has(key)) { throw new Error(`Outline line lists contain edge with indices ${e0}, ${e1} multiple times.`); } outlineEdges.add(key); } } if (uncoveredEdges.size !== outlineEdges.size) { throw new Error(`Polygon exposed triangle edge count ${uncoveredEdges.size} and outline line count ${outlineEdges.size} does not match.`); } expect(isSubsetOf(outlineEdges, uncoveredEdges)).toBe(true); expect(isSubsetOf(uncoveredEdges, outlineEdges)).toBe(true); } function isSubsetOf(a: Set<string>, b: Set<string>): boolean { for (const key of b) { if (!a.has(key)) { return false; } } return true; } function hasDuplicateVertices(flattened: Array<number>): boolean { const set = new Set<string>(); for (let i = 0; i < flattened.length; i += 2) { const vx = flattened[i]; const vy = flattened[i + 1]; const key = `${vx}_${vy}`; if (set.has(key)) { return true; } set.add(key); } return false; } /** * Passes if all triangles have the correct winding order, otherwise throws. */ function checkWindingOrder(flattened: Array<number>, indices: Array<number>): void { 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) { // Incorrect throw new Error(`Found triangle with wrong winding order! Indices: [${i0} ${i1} ${i2}] Vertices: [(${v0x} ${v0y}) (${v1x} ${v1y}) (${v2x} ${v2y})]`); } } }