ngx-open-map-wrapper/node_modules/maplibre-gl/src/render/subdivision.test.ts

1104 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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})]`);
}
}
}