Skip to content

Commit 694c3e2

Browse files
lasselammigithub-actions[bot]
authored andcommitted
[MAPS3D-2108] Fix polygon clipping precision errors causing rendering artifacts (internal-8556)
GitOrigin-RevId: afd8d16d30959cca2964bcaa697e00c91d809b64
1 parent c8816cc commit 694c3e2

File tree

2 files changed

+88
-3
lines changed

2 files changed

+88
-3
lines changed

src/util/polygon_clipping.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,16 +262,42 @@ export function polygonSubdivision(subjectPolygon: Point[][], subdivisionEdges:
262262
polygons = martinez.diff(polygons, clipGeometry) as martinez.MultiPolygon;
263263
}
264264

265-
return fromMultiPolygon(polygons, 1 / scale);
265+
// Snap to grid of 128 in 32bit space to eliminate martinez precision errors.
266+
// This corresponds to ~0.002 tile units (128 / 65536) after scaling back.
267+
return fromMultiPolygon(polygons, 1 / scale, 128);
266268
}
267269

268270
function toMultiPolygon(polygon: Point[][], scale: number = 1.0): martinez.MultiPolygon {
269271
return [polygon.map(ring => ring.map(p => [p.x * scale, p.y * scale]))];
270272
}
271273

272-
function fromMultiPolygon(geometry: martinez.MultiPolygon, scale: number = 1.0): Point[][][] {
274+
/**
275+
* Converts martinez MultiPolygon geometry back to Point arrays.
276+
*
277+
* @param {martinez.MultiPolygon} geometry - The martinez MultiPolygon to convert
278+
* @param {number} scale - Scale factor to apply to coordinates (default 1.0)
279+
* @param {number} [gridSize] - Optional grid size for snapping coordinates before scaling.
280+
* When provided, coordinates are rounded to the nearest multiple
281+
* of this value before scaling. This helps eliminate floating-point
282+
* precision errors from the martinez library, ensuring that adjacent
283+
* polygon edges that should share vertices end up with identical
284+
* coordinates after conversion.
285+
* @returns {Point[][][]}
286+
* @private
287+
*/
288+
function fromMultiPolygon(geometry: martinez.MultiPolygon, scale: number = 1.0, gridSize?: number): Point[][][] {
273289
return geometry.map(poly => poly.map((ring, index) => {
274-
const r = ring.map(p => new Point(p[0] * scale, p[1] * scale).round());
290+
const r = ring.map(p => {
291+
let x = p[0];
292+
let y = p[1];
293+
294+
if (gridSize) {
295+
x = Math.round(x / gridSize) * gridSize;
296+
y = Math.round(y / gridSize) * gridSize;
297+
}
298+
299+
return new Point(x * scale, y * scale)._round();
300+
});
275301
if (index > 0) {
276302
// Reverse holes
277303
r.reverse();

test/unit/util/polygon_clipping.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,4 +564,63 @@ describe('polygon subdivision', () => {
564564
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
565565
expect(result[7][0]).toEqualRing([new Point(90, 47), new Point(100, 50), new Point(100, 60), new Point(90, 58), new Point(90, 47)]);
566566
});
567+
568+
test('grid snapping to fix precision issues', () => {
569+
// Real-world polygon that caused precision issues without grid snapping fix.
570+
// The martinez library can produce nearly-coincident vertices that should be
571+
// identical, causing rendering artifacts.
572+
const subject = [
573+
new Point(1569, 3677), new Point(1737, 3023), new Point(1825, 2634),
574+
new Point(1952, 1864), new Point(2077, 728), new Point(2111, -1),
575+
new Point(4966, -1), new Point(4960, 192), new Point(4843, 1624),
576+
new Point(4681, 2642), new Point(4455, 3748), new Point(4278, 4475),
577+
new Point(1569, 3677)
578+
];
579+
580+
const edges = new MockEdgeIterator([
581+
[new Point(3959, -8562), new Point(868, -7651)],
582+
[new Point(4357, -7209), new Point(1246, -6374)],
583+
[new Point(4703, -5781), new Point(1546, -5138)],
584+
[new Point(4945, -4322), new Point(1752, -3891)],
585+
[new Point(5013, -2859), new Point(1970, -2624)],
586+
[new Point(5086, -1399), new Point(2035, -1346)],
587+
[new Point(5063, 67), new Point(2014, -69)],
588+
[new Point(4954, 1538), new Point(1921, 1195)],
589+
[new Point(4730, 3006), new Point(1731, 2437)],
590+
[new Point(4440, 4288), new Point(1463, 3611)]
591+
]);
592+
593+
const result = polygonSubdivision([subject], edges);
594+
595+
expect(result.length).toBe(5);
596+
597+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
598+
expect(result[0][0]).toEqualRing([
599+
new Point(1569, 3677), new Point(1579, 3637), new Point(4330, 4263),
600+
new Point(4278, 4475), new Point(1569, 3677)
601+
]);
602+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
603+
expect(result[1][0]).toEqualRing([
604+
new Point(1579, 3637), new Point(1737, 3023), new Point(1825, 2634),
605+
new Point(1854, 2460), new Point(4611, 2983), new Point(4455, 3748),
606+
new Point(4330, 4263), new Point(1579, 3637)
607+
]);
608+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
609+
expect(result[2][0]).toEqualRing([
610+
new Point(1854, 2460), new Point(1952, 1864), new Point(2024, 1207),
611+
new Point(4851, 1526), new Point(4843, 1624), new Point(4681, 2642),
612+
new Point(4611, 2983), new Point(1854, 2460)
613+
]);
614+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
615+
expect(result[3][0]).toEqualRing([
616+
new Point(2024, 1207), new Point(2077, 728), new Point(2111, -1),
617+
new Point(3539, -1), new Point(4964, 63), new Point(4960, 192),
618+
new Point(4851, 1526), new Point(2024, 1207)
619+
]);
620+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
621+
expect(result[4][0]).toEqualRing([
622+
new Point(3539, -1), new Point(4966, -1), new Point(4964, 63),
623+
new Point(3539, -1)
624+
]);
625+
});
567626
});

0 commit comments

Comments
 (0)