maplibre-gl 6.0.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/build/readme.md +0 -2
  2. package/dist/maplibre-gl-dev.mjs +433 -573
  3. package/dist/maplibre-gl-dev.mjs.map +1 -1
  4. package/dist/maplibre-gl-shared-dev.mjs +984 -850
  5. package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
  6. package/dist/maplibre-gl-shared.mjs +2 -2
  7. package/dist/maplibre-gl-shared.mjs.map +1 -1
  8. package/dist/maplibre-gl-worker-dev.mjs +14 -12
  9. package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
  10. package/dist/maplibre-gl-worker.mjs +2 -2
  11. package/dist/maplibre-gl-worker.mjs.map +1 -1
  12. package/dist/maplibre-gl.css +1 -1
  13. package/dist/maplibre-gl.d.ts +119 -42
  14. package/dist/maplibre-gl.mjs +22 -22
  15. package/dist/maplibre-gl.mjs.map +1 -1
  16. package/package.json +20 -22
  17. package/src/css/maplibre-gl.css +4 -0
  18. package/src/data/bucket/circle_bucket.ts +3 -2
  19. package/src/data/bucket/fill_bucket.ts +3 -2
  20. package/src/data/bucket/fill_extrusion_bucket.test.ts +22 -0
  21. package/src/data/bucket/fill_extrusion_bucket.ts +17 -4
  22. package/src/data/bucket/line_bucket.test.ts +5 -5
  23. package/src/data/bucket/line_bucket.ts +3 -2
  24. package/src/data/bucket/round_polygon_corners.test.ts +159 -0
  25. package/src/data/bucket/round_polygon_corners.ts +121 -0
  26. package/src/data/bucket/symbol_bucket.test.ts +7 -7
  27. package/src/data/dem_data.test.ts +23 -23
  28. package/src/data/feature_position_map.test.ts +1 -1
  29. package/src/data/segment.test.ts +14 -14
  30. package/src/geo/bounds.test.ts +4 -4
  31. package/src/geo/lng_lat_bounds.test.ts +1 -1
  32. package/src/geo/projection/covering_tiles.bench.ts +39 -0
  33. package/src/geo/projection/covering_tiles.test.ts +3 -7
  34. package/src/geo/projection/covering_tiles.ts +37 -2
  35. package/src/geo/projection/globe_covering_tiles_details_provider.ts +4 -4
  36. package/src/geo/projection/globe_projection.test.ts +2 -2
  37. package/src/geo/projection/globe_projection.ts +6 -15
  38. package/src/geo/projection/globe_transform.test.ts +3 -6
  39. package/src/geo/projection/globe_transform.ts +4 -7
  40. package/src/geo/projection/mercator_covering_tiles_details_provider.ts +4 -4
  41. package/src/geo/projection/mercator_projection.ts +1 -13
  42. package/src/geo/projection/mercator_transform.test.ts +1 -1
  43. package/src/geo/projection/mercator_transform.ts +2 -1
  44. package/src/geo/projection/mercator_utils.test.ts +8 -8
  45. package/src/geo/projection/projection.ts +0 -27
  46. package/src/geo/projection/projection_data.ts +8 -0
  47. package/src/geo/projection/projection_factory.ts +3 -3
  48. package/src/geo/projection/vertical_perspective_projection.ts +3 -60
  49. package/src/geo/projection/vertical_perspective_transform.ts +16 -23
  50. package/src/geo/transform_interface.ts +1 -2
  51. package/src/index.test.ts +7 -12
  52. package/src/index.ts +4 -2
  53. package/src/render/glyph_manager.test.ts +7 -22
  54. package/src/render/painter.test.ts +2 -0
  55. package/src/render/painter.ts +8 -11
  56. package/src/render/subdivision.bench.ts +39 -0
  57. package/src/render/subdivision.test.ts +22 -23
  58. package/src/render/subdivision.ts +46 -0
  59. package/src/render/terrain.test.ts +68 -34
  60. package/src/render/terrain.ts +69 -26
  61. package/src/shaders/glsl/_projection_globe.fragment.glsl +12 -0
  62. package/src/shaders/glsl/_projection_globe.fragment.glsl.g.ts +2 -0
  63. package/src/shaders/glsl/_projection_globe.vertex.glsl +29 -10
  64. package/src/shaders/glsl/_projection_globe.vertex.glsl.g.ts +1 -1
  65. package/src/shaders/glsl/_projection_mercator.fragment.glsl +4 -0
  66. package/src/shaders/glsl/_projection_mercator.fragment.glsl.g.ts +2 -0
  67. package/src/shaders/glsl/line.fragment.glsl +2 -0
  68. package/src/shaders/glsl/line.fragment.glsl.g.ts +1 -1
  69. package/src/shaders/glsl/line_gradient.fragment.glsl +2 -0
  70. package/src/shaders/glsl/line_gradient.fragment.glsl.g.ts +1 -1
  71. package/src/shaders/glsl/line_gradient_sdf.fragment.glsl +2 -0
  72. package/src/shaders/glsl/line_gradient_sdf.fragment.glsl.g.ts +1 -1
  73. package/src/shaders/glsl/line_pattern.fragment.glsl +2 -0
  74. package/src/shaders/glsl/line_pattern.fragment.glsl.g.ts +1 -1
  75. package/src/shaders/glsl/line_sdf.fragment.glsl +2 -0
  76. package/src/shaders/glsl/line_sdf.fragment.glsl.g.ts +1 -1
  77. package/src/shaders/shaders.ts +4 -6
  78. package/src/source/canvas_source.test.ts +1 -1
  79. package/src/source/geojson_source.test.ts +32 -4
  80. package/src/source/geojson_source.ts +41 -0
  81. package/src/source/geojson_source_diff.test.ts +2 -2
  82. package/src/source/geojson_worker_source.test.ts +9 -5
  83. package/src/source/image_source.test.ts +137 -62
  84. package/src/source/image_source.ts +49 -12
  85. package/src/source/raster_dem_tile_source.test.ts +2 -2
  86. package/src/source/raster_tile_source.test.ts +64 -9
  87. package/src/source/raster_tile_source.ts +2 -1
  88. package/src/source/rtl_text_plugin_main_thread.test.ts +14 -14
  89. package/src/source/source.test.ts +8 -8
  90. package/src/source/vector_tile_source.test.ts +1 -1
  91. package/src/source/vector_tile_worker_source.test.ts +7 -5
  92. package/src/source/video_source.test.ts +1 -1
  93. package/src/source/worker.test.ts +1 -1
  94. package/src/source/worker_tile.test.ts +10 -8
  95. package/src/style/light.test.ts +9 -9
  96. package/src/style/light.ts +3 -3
  97. package/src/style/load_glyph_range.test.ts +8 -8
  98. package/src/style/load_sprite.test.ts +1 -1
  99. package/src/style/properties.test.ts +94 -2
  100. package/src/style/properties.ts +30 -1
  101. package/src/style/sky.test.ts +9 -9
  102. package/src/style/sky.ts +2 -2
  103. package/src/style/style.test.ts +38 -36
  104. package/src/style/style.ts +3 -3
  105. package/src/style/style_layer/color_relief_style_layer.test.ts +1 -1
  106. package/src/style/style_layer/fill_extrusion_style_layer.ts +6 -3
  107. package/src/style/style_layer/fill_extrusion_style_layer_properties.g.ts +14 -1
  108. package/src/style/style_layer/raster_style_layer.test.ts +2 -2
  109. package/src/style/style_layer.test.ts +5 -51
  110. package/src/style/validate_style.test.ts +1 -1
  111. package/src/symbol/collision_index.test.ts +1 -1
  112. package/src/symbol/grid_index.test.ts +1 -1
  113. package/src/tile/terrain_tile_manager.test.ts +2 -2
  114. package/src/tile/tile_id.test.ts +5 -5
  115. package/src/tile/tile_manager.test.ts +113 -72
  116. package/src/tile/tile_manager.ts +2 -2
  117. package/src/tile/tile_manager_in_view_tiles.test.ts +2 -2
  118. package/src/ui/camera.test.ts +138 -135
  119. package/src/ui/control/geolocate_control.test.ts +7 -30
  120. package/src/ui/handler/dblclick_zoom.test.ts +2 -2
  121. package/src/ui/handler/mouse.ts +15 -7
  122. package/src/ui/handler/mouse_handler_interface.test.ts +21 -1
  123. package/src/ui/handler/scroll_zoom.test.ts +2 -0
  124. package/src/ui/handler_manager.test.ts +7 -7
  125. package/src/ui/hash.test.ts +1 -0
  126. package/src/ui/map.ts +45 -20
  127. package/src/ui/map_tests/map_basic.test.ts +11 -23
  128. package/src/ui/map_tests/map_calculate_camera_options.test.ts +6 -6
  129. package/src/ui/map_tests/map_events.test.ts +5 -7
  130. package/src/ui/map_tests/map_global_state.test.ts +3 -3
  131. package/src/ui/map_tests/map_images.test.ts +4 -3
  132. package/src/ui/map_tests/map_is_moving.test.ts +1 -1
  133. package/src/ui/map_tests/map_is_rotating.test.ts +1 -1
  134. package/src/ui/map_tests/map_layer.test.ts +2 -1
  135. package/src/ui/map_tests/map_options.test.ts +1 -5
  136. package/src/ui/map_tests/map_pitch.test.ts +6 -6
  137. package/src/ui/map_tests/map_refresh_tiles.test.ts +2 -2
  138. package/src/ui/map_tests/map_render.test.ts +1 -1
  139. package/src/ui/map_tests/map_set_lod_params.test.ts +1 -1
  140. package/src/ui/map_tests/map_style.test.ts +9 -1
  141. package/src/ui/map_tests/map_terrain.test.ts +41 -1
  142. package/src/ui/map_tests/map_zoom.test.ts +15 -15
  143. package/src/ui/marker.test.ts +71 -14
  144. package/src/ui/marker.ts +30 -6
  145. package/src/ui/popup.test.ts +3 -3
  146. package/src/util/actor.test.ts +6 -6
  147. package/src/util/ajax.test.ts +27 -39
  148. package/src/util/browser.test.ts +7 -9
  149. package/src/util/create_tile_mesh.test.ts +2 -2
  150. package/src/util/dispatcher.test.ts +1 -1
  151. package/src/util/evented.test.ts +4 -1
  152. package/src/util/fast_maths.test.ts +4 -4
  153. package/src/util/image_request.test.ts +6 -4
  154. package/src/util/primitives/bounding_volume_cache.ts +3 -1
  155. package/src/util/struct_array.test.ts +2 -2
  156. package/src/util/task_queue.test.ts +1 -1
  157. package/src/util/test/util.ts +1 -0
  158. package/src/util/time_control.test.ts +2 -2
  159. package/src/util/transferable_grid_index.test.ts +1 -1
  160. package/src/util/util.test.ts +27 -13
  161. package/src/util/verticalize_punctuation.test.ts +4 -4
  162. package/src/util/web_worker_transfer.test.ts +5 -5
  163. package/src/webgl/draw/draw_custom.test.ts +13 -13
  164. package/src/webgl/draw/draw_debug.test.ts +1 -1
  165. package/src/webgl/draw/draw_fill.test.ts +13 -12
  166. package/src/webgl/draw/draw_symbol.test.ts +17 -16
  167. package/src/webgl/program/program_uniforms.ts +0 -3
  168. package/src/webgl/program/projection_program.ts +12 -10
  169. package/src/webgl/program/terrain_program.test.ts +2 -2
  170. package/src/webgl/program.ts +6 -4
  171. package/src/webgl/render_to_texture.test.ts +5 -5
  172. package/src/webgl/state.test.ts +1 -1
  173. package/src/webgl/texture.test.ts +3 -3
  174. package/src/webgl/vertex_buffer.test.ts +1 -1
  175. package/build/rolldown_plugins.ts +0 -18
  176. package/src/geo/projection/globe_projection_error_measurement.ts +0 -229
  177. package/src/shaders/glsl/projection_error_measurement.fragment.glsl +0 -5
  178. package/src/shaders/glsl/projection_error_measurement.fragment.glsl.g.ts +0 -2
  179. package/src/shaders/glsl/projection_error_measurement.vertex.glsl +0 -22
  180. package/src/shaders/glsl/projection_error_measurement.vertex.glsl.g.ts +0 -2
  181. package/src/webgl/program/projection_error_measurement_program.ts +0 -23
@@ -0,0 +1,39 @@
1
+ import {bench} from 'vitest';
2
+ import Point from '@mapbox/point-geometry';
3
+ import {CanonicalTileID} from '../tile/tile_id.ts';
4
+ import {EXTENT} from '../data/extent.ts';
5
+ import {subdividePolygon} from './subdivision.ts';
6
+
7
+ function generateRing(cx: number, cy: number, radius: number, vertexCount: number): Point[] {
8
+ const ring = [];
9
+
10
+ for (let i = 0; i < vertexCount; i++) {
11
+ const angle = i / vertexCount * 2.0 * Math.PI;
12
+ ring.push(new Point(
13
+ Math.round(cx + Math.cos(angle) * radius),
14
+ Math.round(cy + Math.sin(angle) * radius)
15
+ ));
16
+ }
17
+
18
+ return ring;
19
+ }
20
+
21
+ const vertexCountMultiplier = 11;
22
+ const granularity = 64;
23
+ const tileID = new CanonicalTileID(2, 1, 1);
24
+
25
+ const polygon: Point[][] = [generateRing(EXTENT / 2, EXTENT / 2, EXTENT * 1.1 / 2, 81 * vertexCountMultiplier)];
26
+
27
+ function generateHole(cx: number, cy: number, r: number, vertexCount: number) {
28
+ polygon.push(generateRing(cx * EXTENT, cy * EXTENT, r * EXTENT, vertexCount));
29
+ }
30
+
31
+ generateHole(0.25, 0.5, 0.15, 16 * vertexCountMultiplier);
32
+ generateHole(0.75, 0.5, 0.15, 2 * vertexCountMultiplier);
33
+ generateHole(0.5, 0.1, 0.05, 4 * vertexCountMultiplier);
34
+
35
+ bench('subdividePolygon', () => {
36
+ for (let i = 0; i < 10; i++) {
37
+ subdividePolygon(polygon, tileID, granularity, true);
38
+ }
39
+ });
@@ -224,7 +224,7 @@ describe('Fill subdivision', () => {
224
224
  3, 0
225
225
  ]
226
226
  ]);
227
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
227
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
228
228
  });
229
229
 
230
230
  test('Polygon is unchanged when granularity=1, but winding order is corrected.', () => {
@@ -259,7 +259,7 @@ describe('Fill subdivision', () => {
259
259
  3, 0
260
260
  ]
261
261
  ]);
262
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
262
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
263
263
  });
264
264
 
265
265
  test('Polygon inside cell is unchanged', () => {
@@ -294,7 +294,7 @@ describe('Fill subdivision', () => {
294
294
  3, 0
295
295
  ]
296
296
  ]);
297
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
297
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
298
298
  });
299
299
 
300
300
  test('Subdivide a polygon', () => {
@@ -349,7 +349,6 @@ describe('Fill subdivision', () => {
349
349
  // 7:
350
350
  //
351
351
  // 8: 2
352
- console.log(result.indicesTriangles);
353
352
  expect(result.indicesTriangles).toEqual([
354
353
  3, 0, 6,
355
354
  7, 0, 3,
@@ -405,7 +404,7 @@ describe('Fill subdivision', () => {
405
404
  6, 3
406
405
  ]
407
406
  ]);
408
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
407
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
409
408
  });
410
409
 
411
410
  describe('Polygon outline line list is correct', () => {
@@ -420,7 +419,7 @@ describe('Fill subdivision', () => {
420
419
  expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false);
421
420
  testMeshIntegrity(result.indicesTriangles);
422
421
  testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList);
423
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
422
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
424
423
  });
425
424
 
426
425
  test('Small polygon', () => {
@@ -434,7 +433,7 @@ describe('Fill subdivision', () => {
434
433
  expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false);
435
434
  testMeshIntegrity(result.indicesTriangles);
436
435
  testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList);
437
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
436
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
438
437
  });
439
438
 
440
439
  test('Medium polygon', () => {
@@ -448,7 +447,7 @@ describe('Fill subdivision', () => {
448
447
  expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false);
449
448
  testMeshIntegrity(result.indicesTriangles);
450
449
  testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList);
451
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
450
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
452
451
  });
453
452
 
454
453
  test('Large polygon', () => {
@@ -462,7 +461,7 @@ describe('Fill subdivision', () => {
462
461
  expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false);
463
462
  testMeshIntegrity(result.indicesTriangles);
464
463
  testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList);
465
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
464
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
466
465
  });
467
466
 
468
467
  test('Large polygon with hole', () => {
@@ -481,7 +480,7 @@ describe('Fill subdivision', () => {
481
480
  expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false);
482
481
  testMeshIntegrity(result.indicesTriangles);
483
482
  testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList);
484
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
483
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
485
484
  });
486
485
 
487
486
  test('Large polygon with hole, granularity=0', () => {
@@ -500,7 +499,7 @@ describe('Fill subdivision', () => {
500
499
  expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false);
501
500
  testMeshIntegrity(result.indicesTriangles);
502
501
  testPolygonOutlineMatches(result.indicesTriangles, result.indicesLineList);
503
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
502
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
504
503
  });
505
504
 
506
505
  test('Large polygon with hole, finer granularity', () => {
@@ -518,7 +517,7 @@ describe('Fill subdivision', () => {
518
517
  ], canonicalDefault, EXTENT / 8);
519
518
 
520
519
  expect(hasDuplicateVertices(result.verticesFlattened)).toBe(false);
521
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
520
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
522
521
 
523
522
  // This polygon subdivision results in at least one edge that is shared among more than 2 triangles.
524
523
  // This is not ideal, but it is also an edge case of a weird triangle getting subdivided by a very fine grid.
@@ -587,7 +586,7 @@ describe('Fill subdivision', () => {
587
586
  5, 3
588
587
  ]
589
588
  ]);
590
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
589
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
591
590
  });
592
591
 
593
592
  test('Polygon with duplicate vertex with hole inside cell', () => {
@@ -641,7 +640,7 @@ describe('Fill subdivision', () => {
641
640
  4, 0
642
641
  ]
643
642
  ]);
644
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
643
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
645
644
  });
646
645
 
647
646
  test('Polygon with duplicate edge inside cell', () => {
@@ -701,7 +700,7 @@ describe('Fill subdivision', () => {
701
700
  3, 0
702
701
  ]
703
702
  ]);
704
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
703
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
705
704
  });
706
705
  });
707
706
 
@@ -778,7 +777,7 @@ describe('Fill subdivision', () => {
778
777
  4, 0
779
778
  ]
780
779
  ]);
781
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
780
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
782
781
  });
783
782
 
784
783
  test('Generates pole geometry for north pole only (geometry not bordering other pole)', () => {
@@ -814,7 +813,7 @@ describe('Fill subdivision', () => {
814
813
  2, 3, 3, 0
815
814
  ]
816
815
  ]);
817
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
816
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
818
817
  });
819
818
 
820
819
  test('Generates pole geometry for south pole only (geometry not bordering other pole)', () => {
@@ -850,7 +849,7 @@ describe('Fill subdivision', () => {
850
849
  2, 3, 3, 0
851
850
  ]
852
851
  ]);
853
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
852
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
854
853
  });
855
854
 
856
855
  test('Generates pole geometry for north pole only (tile not bordering other pole)', () => {
@@ -886,7 +885,7 @@ describe('Fill subdivision', () => {
886
885
  2, 3, 3, 0
887
886
  ]
888
887
  ]);
889
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
888
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
890
889
  });
891
890
 
892
891
  test('Generates pole geometry for south pole only (tile not bordering other pole)', () => {
@@ -922,7 +921,7 @@ describe('Fill subdivision', () => {
922
921
  2, 3, 3, 0
923
922
  ]
924
923
  ]);
925
- checkWindingOrder(result.verticesFlattened, result.indicesTriangles);
924
+ expectWindingOrder(result.verticesFlattened, result.indicesTriangles);
926
925
  });
927
926
 
928
927
  test('Scanline subdivision ring generation case 1', () => {
@@ -952,7 +951,7 @@ describe('Fill subdivision', () => {
952
951
  const ring = [0, 1, 2, 3, 4, 5, 6, 7];
953
952
  const finalIndices = [];
954
953
  scanlineTriangulateVertexRing(vertices, ring, finalIndices);
955
- checkWindingOrder(vertices, finalIndices);
954
+ expectWindingOrder(vertices, finalIndices);
956
955
  });
957
956
 
958
957
  test('Scanline subdivision ring generation case 2', () => {
@@ -961,7 +960,7 @@ describe('Fill subdivision', () => {
961
960
  const ring = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
962
961
  const finalIndices = [];
963
962
  scanlineTriangulateVertexRing(vertices, ring, finalIndices);
964
- checkWindingOrder(vertices, finalIndices);
963
+ expectWindingOrder(vertices, finalIndices);
965
964
  });
966
965
  });
967
966
 
@@ -1076,7 +1075,7 @@ function hasDuplicateVertices(flattened: number[]): boolean {
1076
1075
  /**
1077
1076
  * Passes if all triangles have the correct winding order, otherwise throws.
1078
1077
  */
1079
- function checkWindingOrder(flattened: number[], indices: number[]): void {
1078
+ function expectWindingOrder(flattened: number[], indices: number[]): void {
1080
1079
  for (let i = 0; i < indices.length; i += 3) {
1081
1080
  const i0 = indices[i];
1082
1081
  const i1 = indices[i + 1];
@@ -612,6 +612,11 @@ class Subdivider {
612
612
  // Add pole geometry if needed
613
613
  this._handlePoles(subdividedTriangles);
614
614
 
615
+ if (this._granularity >= 2 && this._canonical?.z === 0) {
616
+ subdividedTriangles = this._removeTrianglesOutsideTileX(subdividedTriangles);
617
+ subdividedLines = subdividedLines.map(lines => this._removeLinesOutsideTileX(lines));
618
+ }
619
+
615
620
  return {
616
621
  verticesFlattened: this._vertexBuffer,
617
622
  indicesTriangles: subdividedTriangles,
@@ -619,6 +624,47 @@ class Subdivider {
619
624
  };
620
625
  }
621
626
 
627
+ private _vertexOutsideTileX(index: number): boolean {
628
+ const x = this._vertexBuffer[index * 2];
629
+ return x < 0 || x > EXTENT;
630
+ }
631
+
632
+ /**
633
+ * Drops all triangles that reach beyond the tile's X extent.
634
+ *
635
+ * On globe the z0 tile's buffer wraps around the planet onto the tile itself, drawing buffered geometry twice.
636
+ * Only globe uses subdivision (`granularity >= 2`), so mercator is never affected.
637
+ * @param indices - Triangle indices into `this._vertexBuffer`.
638
+ * @returns The indices with every triangle that has a vertex outside the tile's X extent removed.
639
+ */
640
+ private _removeTrianglesOutsideTileX(indices: number[]): number[] {
641
+ const filtered: number[] = [];
642
+ for (let i = 0; i < indices.length; i += 3) {
643
+ if (this._vertexOutsideTileX(indices[i]) || this._vertexOutsideTileX(indices[i + 1]) || this._vertexOutsideTileX(indices[i + 2])) {
644
+ continue;
645
+ }
646
+ filtered.push(indices[i], indices[i + 1], indices[i + 2]);
647
+ }
648
+ return filtered;
649
+ }
650
+
651
+ /**
652
+ * Drops all outline line segments that reach beyond the tile's X extent,
653
+ * for the same reason as {@link Subdivider._removeTrianglesOutsideTileX}.
654
+ * @param indices - Line segment indices into `this._vertexBuffer`.
655
+ * @returns The indices with every segment that has a vertex outside the tile's X extent removed.
656
+ */
657
+ private _removeLinesOutsideTileX(indices: number[]): number[] {
658
+ const filtered: number[] = [];
659
+ for (let i = 0; i < indices.length; i += 2) {
660
+ if (this._vertexOutsideTileX(indices[i]) || this._vertexOutsideTileX(indices[i + 1])) {
661
+ continue;
662
+ }
663
+ filtered.push(indices[i], indices[i + 1]);
664
+ }
665
+ return filtered;
666
+ }
667
+
622
668
  /**
623
669
  * Sometimes the supplies vertex and index array has duplicate vertices - same coordinates that are referenced by multiple different indices.
624
670
  * That is not allowed for purposes of subdivision, duplicates are removed in `this.initializeVertices`.
@@ -1,6 +1,5 @@
1
1
  import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest';
2
2
  import Point from '@mapbox/point-geometry';
3
- import {mat4} from 'gl-matrix';
4
3
  import {Terrain} from './terrain.ts';
5
4
  import {Context} from '../webgl/context.ts';
6
5
  import {RGBAImage} from '../util/image.ts';
@@ -248,31 +247,66 @@ describe('Terrain', () => {
248
247
  expect(actualVertexArray).toStrictEqual([0, 0, 0, 2048, 0, 0, 4096, 0, 0, 6144, 0, 0, 8192, 0, 0, 0, 2048, 0, 2048, 2048, 0, 4096, 2048, 0, 6144, 2048, 0, 8192, 2048, 0, 0, 4096, 0, 2048, 4096, 0, 4096, 4096, 0, 6144, 4096, 0, 8192, 4096, 0, 0, 6144, 0, 2048, 6144, 0, 4096, 6144, 0, 6144, 6144, 0, 8192, 6144, 0, 0, 8192, 0, 2048, 8192, 0, 4096, 8192, 0, 6144, 8192, 0, 8192, 8192, 0, 0, 0, 1, 2048, 0, 1, 4096, 0, 1, 6144, 0, 1, 8192, 0, 1, 0, 8192, 1, 2048, 8192, 1, 4096, 8192, 1, 6144, 8192, 1, 8192, 8192, 1, 0, 0, 0, 0, 0, 1, 0, 2048, 0, 0, 2048, 1, 0, 4096, 0, 0, 4096, 1, 0, 6144, 0, 0, 6144, 1, 0, 8192, 0, 0, 8192, 1, 8192, 0, 0, 8192, 0, 1, 8192, 2048, 0, 8192, 2048, 1, 8192, 4096, 0, 8192, 4096, 1, 8192, 6144, 0, 8192, 6144, 1, 8192, 8192, 0, 8192, 8192, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
249
248
  });
250
249
 
251
- test('interpolation works', () => {
252
- const mockTerrain = {
253
- getDEMElevation: Terrain.prototype.getDEMElevation,
254
- getTerrainData() {
255
- return {
256
- u_terrain_matrix: mat4.create(),
257
- tile: {
258
- dem: {
259
- dim: 1,
260
- sampleBilinear(x: number, y: number) {
261
- return 100 * x + 10 * y;
262
- }
263
- }
264
- }
265
- };
250
+ test('getElevation interpolates and reuses DEM sampling setup until reset', () => {
251
+ const terrain = new Terrain(null, {_source: {tileSize: 512}} as any, {exaggeration: 2} as any);
252
+ const tileID = new OverscaledTileID(1, 0, 1, 0, 0);
253
+ const sourceTile = {
254
+ tileID,
255
+ dem: {
256
+ dim: 1,
257
+ sampleBilinear: (x: number, y: number) => 100 * x + 10 * y
266
258
  }
267
- };
268
- const tileID = new OverscaledTileID(0, 0, 0, 0, 0);
269
- expect(mockTerrain.getDEMElevation(tileID, 0, 0)).toBeCloseTo(0);
270
- expect(mockTerrain.getDEMElevation(tileID, 1, 1)).toBeCloseTo(110);
271
- expect(mockTerrain.getDEMElevation(tileID, 0, 0.5)).toBeCloseTo(5);
272
- expect(mockTerrain.getDEMElevation(tileID, 1, 0.5)).toBeCloseTo(105);
273
- expect(mockTerrain.getDEMElevation(tileID, 0.5, 0)).toBeCloseTo(50);
274
- expect(mockTerrain.getDEMElevation(tileID, 0.5, 1)).toBeCloseTo(60);
275
- expect(mockTerrain.getDEMElevation(tileID, 0.4, 0.2)).toBeCloseTo(42);
259
+ } as any as Tile;
260
+ terrain.tileManager.getSourceTile = vi.fn(() => sourceTile);
261
+ terrain.tileManager.getSource = vi.fn(() => ({minzoom: 0, maxzoom: 22}) as any);
262
+
263
+ expect(terrain.getDEMElevation(tileID, EXTENT * 0.4, EXTENT * 0.2)).toBeCloseTo(42);
264
+ expect(terrain.getElevation(tileID, EXTENT / 2, EXTENT / 2)).toBeCloseTo(110);
265
+ expect(terrain.getElevation(tileID, EXTENT / 2, EXTENT / 2)).toBeCloseTo(110);
266
+ expect(terrain.tileManager.getSourceTile).toHaveBeenCalledTimes(1);
267
+
268
+ terrain.resetElevationCache();
269
+ expect(terrain.getElevation(tileID, EXTENT / 2, EXTENT / 2)).toBeCloseTo(110);
270
+
271
+ expect(terrain.tileManager.getSourceTile).toHaveBeenCalledTimes(2);
272
+ });
273
+
274
+ test('getDEMElevation samples the correct part of a parent DEM tile', () => {
275
+ const terrain = new Terrain(null, {_source: {tileSize: 512}} as any, {} as any);
276
+ const childTileID = new OverscaledTileID(2, 0, 2, 3, 3);
277
+ const parentTileID = new OverscaledTileID(1, 0, 1, 1, 1);
278
+ const sampleBilinear = vi.fn(() => 42);
279
+
280
+ terrain.tileManager.getSourceTile = vi.fn(() => ({
281
+ tileID: parentTileID,
282
+ dem: {dim: 4, sampleBilinear}
283
+ }) as any as Tile);
284
+ terrain.tileManager.getSource = vi.fn(() => ({maxzoom: 22}) as any);
285
+
286
+ expect(terrain.getDEMElevation(childTileID, EXTENT / 2, EXTENT / 2)).toBe(42);
287
+ // The center of the bottom-right child is at 75% of both parent axes.
288
+ expect(sampleBilinear).toHaveBeenCalledWith(3, 3);
289
+ });
290
+
291
+ test('getElevation retries sampling setup when DEM data becomes available', () => {
292
+ const terrain = new Terrain(null, {_source: {tileSize: 512}} as any, {exaggeration: 1} as any);
293
+ const tileID = new OverscaledTileID(1, 0, 1, 0, 0);
294
+ let tileHasDem = false;
295
+
296
+ terrain.tileManager.getSourceTile = vi.fn(() => ({
297
+ tileID,
298
+ dem: tileHasDem ? {
299
+ dim: 1,
300
+ sampleBilinear: (x: number, y: number) => 100 * x + 10 * y
301
+ } : undefined
302
+ }) as any as Tile);
303
+ terrain.tileManager.getSource = vi.fn(() => ({minzoom: 0, maxzoom: 22}) as any);
304
+
305
+ expect(terrain.getElevation(tileID, EXTENT / 2, EXTENT / 2)).toBe(0);
306
+
307
+ tileHasDem = true;
308
+ expect(terrain.getElevation(tileID, EXTENT / 2, EXTENT / 2)).toBeCloseTo(55);
309
+ expect(terrain.tileManager.getSourceTile).toHaveBeenCalledTimes(2);
276
310
  });
277
311
 
278
312
  test('getElevationForLngLat uses covering tiles to get the right zoom', () => {
@@ -323,15 +357,15 @@ describe('Terrain', () => {
323
357
 
324
358
  test('getDEMElevation normalizes out-of-bounds coordinates to neighbor tile', () => {
325
359
  const terrain = new Terrain(null, {_source: {tileSize: 512}} as any, {} as any);
326
- const spy = vi.fn().mockReturnValue({tile: null});
327
- terrain.getTerrainData = spy;
360
+ const getSourceTile = vi.fn();
361
+ terrain.tileManager.getSourceTile = getSourceTile;
328
362
 
329
363
  // tile (0,0,1) with x beyond EXTENT should normalize to tile (1,0,1)
330
364
  const tileID = new OverscaledTileID(1, 0, 1, 0, 0);
331
365
  terrain.getDEMElevation(tileID, EXTENT + 100, 50);
332
366
 
333
- expect(spy).toHaveBeenCalledOnce();
334
- const [calledTileID] = spy.mock.calls[0];
367
+ expect(getSourceTile).toHaveBeenCalledTimes(1);
368
+ const [calledTileID] = getSourceTile.mock.calls[0];
335
369
  expect(calledTileID.canonical.x).toBe(1);
336
370
  expect(calledTileID.canonical.y).toBe(0);
337
371
  expect(calledTileID.canonical.z).toBe(1);
@@ -339,15 +373,15 @@ describe('Terrain', () => {
339
373
 
340
374
  test('getDEMElevation returns 0 for coordinates beyond tile grid', () => {
341
375
  const terrain = new Terrain(null, {_source: {tileSize: 512}} as any, {} as any);
342
- const spy = vi.fn();
343
- terrain.getTerrainData = spy;
376
+ const getSourceTile = vi.fn();
377
+ terrain.tileManager.getSourceTile = getSourceTile;
344
378
 
345
379
  // tile (0,0,0) with y beyond EXTENT — no tile exists below at z=0
346
380
  const tileID = new OverscaledTileID(0, 0, 0, 0, 0);
347
381
  const result = terrain.getDEMElevation(tileID, 100, EXTENT + 100);
348
382
 
349
383
  expect(result).toBe(0);
350
- expect(spy).not.toHaveBeenCalled();
384
+ expect(getSourceTile).not.toHaveBeenCalled();
351
385
  });
352
386
 
353
387
  describe('getElevationForLngLatZoom returns 0 for out of bounds', () => {
@@ -361,11 +395,11 @@ describe('Terrain', () => {
361
395
  expect(terrain.getElevationForLngLatZoom(new LngLat(0, 88), 0)).toBe(0);
362
396
  });
363
397
 
364
- test('zoom', () => {
398
+ test('zoom below the minimum', () => {
365
399
  expect(terrain.getElevationForLngLatZoom(new LngLat(0, 0), MIN_TILE_ZOOM - 1)).toBe(0);
366
400
  });
367
401
 
368
- test('zoom', () => {
402
+ test('zoom above the maximum', () => {
369
403
  expect(terrain.getElevationForLngLatZoom(new LngLat(0, 0), MAX_TILE_ZOOM + 1)).toBe(0);
370
404
  });
371
405
  });
@@ -1,4 +1,4 @@
1
- import {mat4, vec2} from 'gl-matrix';
1
+ import {mat4} from 'gl-matrix';
2
2
  import {OverscaledTileID} from '../tile/tile_id.ts';
3
3
  import {RGBAImage} from '../util/image.ts';
4
4
  import {warnOnce} from '../util/util.ts';
@@ -38,6 +38,8 @@ export type TerrainData = {
38
38
  tile: Tile;
39
39
  };
40
40
 
41
+ type TerrainElevationSampler = (x: number, y: number, extent: number) => number;
42
+
41
43
  /**
42
44
  * @internal
43
45
  * This is the main class which handles most of the 3D Terrain logic. It has the following topics:
@@ -135,7 +137,13 @@ export class Terrain {
135
137
  * as of overzooming of raster-dem tiles in high zoomlevels, this cache contains
136
138
  * matrices to transform from vector-tile coords to raster-dem-tile coords.
137
139
  */
138
- _demMatrixCache: {[_: string]: { matrix: mat4; coord: OverscaledTileID }};
140
+ _demMatrixCache: Map<string, mat4>;
141
+ /**
142
+ * Per-render cache of resolved CPU elevation samplers. It is cleared after
143
+ * terrain tile selection updates and whenever the terrain source changes.
144
+ * Missing DEM data is deliberately not cached so a later sample can retry.
145
+ */
146
+ _elevationSamplerCache: Map<string, TerrainElevationSampler>;
139
147
  /**
140
148
  * Controls how terrain skirt length is calculated.
141
149
  * @see {@link MapOptions.terrainSkirtLength}
@@ -149,7 +157,8 @@ export class Terrain {
149
157
  this._terrainSkirtLength = terrainSkirtLength;
150
158
  this.qualityFactor = 2;
151
159
  this.meshSize = 128;
152
- this._demMatrixCache = {};
160
+ this._demMatrixCache = new Map();
161
+ this._elevationSamplerCache = new Map();
153
162
  this.coordsIndex = [];
154
163
  this._coordsTextureSize = 1024;
155
164
  }
@@ -200,13 +209,8 @@ export class Terrain {
200
209
  const normalized = tileID.normalizeCoordinates(x, y, extent);
201
210
  if (!normalized) return 0;
202
211
 
203
- const terrain = this.getTerrainData(normalized.tileID);
204
- const dem = terrain.tile?.dem;
205
- if (!dem) return 0;
206
-
207
- const pos = vec2.transformMat4([], [normalized.x / extent * EXTENT, normalized.y / extent * EXTENT], terrain.u_terrain_matrix);
208
- const coord = [pos[0] * dem.dim, pos[1] * dem.dim];
209
- return dem.sampleBilinear(coord[0], coord[1]);
212
+ const sampler = this._getElevationSampler(normalized.tileID);
213
+ return sampler ? sampler(normalized.x, normalized.y, extent) : 0;
210
214
  }
211
215
 
212
216
  /**
@@ -250,6 +254,59 @@ export class Terrain {
250
254
  return this.getDEMElevation(tileID, x, y, extent) * this.exaggeration;
251
255
  }
252
256
 
257
+ /**
258
+ * Clear CPU elevation samplers that may retain a previously selected DEM tile.
259
+ * @internal
260
+ */
261
+ resetElevationCache(): void {
262
+ this._elevationSamplerCache.clear();
263
+ }
264
+
265
+ _getElevationSampler(tileID: OverscaledTileID): TerrainElevationSampler | null {
266
+ const key = tileID.key;
267
+ const cachedSampler = this._elevationSamplerCache.get(key);
268
+ if (cachedSampler) return cachedSampler;
269
+
270
+ const sourceTile = this.tileManager.getSourceTile(tileID, true);
271
+ const dem = sourceTile?.dem;
272
+ if (!sourceTile || !dem) return null;
273
+
274
+ const matrix = this._getDEMTileMatrix(tileID, sourceTile);
275
+ // Store the vector-tile to DEM-pixel transform once for the hot sampling loop.
276
+ const demPixelScaleX = matrix[0] * dem.dim;
277
+ const demPixelScaleY = matrix[5] * dem.dim;
278
+ const demPixelOffsetX = matrix[12] * dem.dim;
279
+ const demPixelOffsetY = matrix[13] * dem.dim;
280
+ const sampler = (x: number, y: number, extent: number): number => {
281
+ const extentScale = extent === EXTENT ? 1 : EXTENT / extent;
282
+ return dem.sampleBilinear(
283
+ x * extentScale * demPixelScaleX + demPixelOffsetX,
284
+ y * extentScale * demPixelScaleY + demPixelOffsetY
285
+ );
286
+ };
287
+ this._elevationSamplerCache.set(key, sampler);
288
+ return sampler;
289
+ }
290
+
291
+ _getDEMTileMatrix(tileID: OverscaledTileID, sourceTile: Tile): mat4 {
292
+ const matrixKey = `${sourceTile.tileID.key}/${tileID.key}`;
293
+ const cachedMatrix = this._demMatrixCache.get(matrixKey);
294
+ if (cachedMatrix) return cachedMatrix;
295
+
296
+ const maxzoom = this.tileManager.getSource().maxzoom;
297
+ let dz = tileID.canonical.z - sourceTile.tileID.canonical.z;
298
+ if (tileID.overscaledZ > tileID.canonical.z) {
299
+ if (tileID.canonical.z >= maxzoom) dz = tileID.canonical.z - maxzoom;
300
+ else warnOnce('cannot calculate elevation if elevation maxzoom > source.maxzoom');
301
+ }
302
+ const dx = tileID.canonical.x - (tileID.canonical.x >> dz << dz);
303
+ const dy = tileID.canonical.y - (tileID.canonical.y >> dz << dz);
304
+ const demMatrix = mat4.fromScaling(new Float64Array(16), [1 / (EXTENT << dz), 1 / (EXTENT << dz), 0]);
305
+ mat4.translate(demMatrix, demMatrix, [dx * EXTENT, dy * EXTENT, 0]);
306
+ this._demMatrixCache.set(matrixKey, demMatrix);
307
+ return demMatrix;
308
+ }
309
+
253
310
  /**
254
311
  * returns a Terrain Object for a tile. Unless the tile corresponds to data (e.g. tile is loading), return a flat dem object
255
312
  * @param tileID - the tile to get the terrain for
@@ -277,27 +334,13 @@ export class Terrain {
277
334
  sourceTile.demTexture.bind(context.gl.NEAREST, context.gl.CLAMP_TO_EDGE);
278
335
  sourceTile.needsTerrainPrepare = false;
279
336
  }
280
- // create matrix for lookup in dem data
281
- const matrixKey = sourceTile && sourceTile.toString() + sourceTile.tileID.key + tileID.key;
282
- if (matrixKey && !this._demMatrixCache[matrixKey]) {
283
- const maxzoom = this.tileManager.getSource().maxzoom;
284
- let dz = tileID.canonical.z - sourceTile.tileID.canonical.z;
285
- if (tileID.overscaledZ > tileID.canonical.z) {
286
- if (tileID.canonical.z >= maxzoom) dz = tileID.canonical.z - maxzoom;
287
- else warnOnce('cannot calculate elevation if elevation maxzoom > source.maxzoom');
288
- }
289
- const dx = tileID.canonical.x - (tileID.canonical.x >> dz << dz);
290
- const dy = tileID.canonical.y - (tileID.canonical.y >> dz << dz);
291
- const demMatrix = mat4.fromScaling(new Float64Array(16), [1 / (EXTENT << dz), 1 / (EXTENT << dz), 0]);
292
- mat4.translate(demMatrix, demMatrix, [dx * EXTENT, dy * EXTENT, 0]);
293
- this._demMatrixCache[matrixKey] = {matrix: demMatrix, coord: tileID};
294
- }
337
+ const terrainMatrix = sourceTile ? this._getDEMTileMatrix(tileID, sourceTile) : this._emptyDemMatrix;
295
338
  // return uniform values & textures
296
339
  return {
297
340
  'u_depth': 2,
298
341
  'u_terrain': 3,
299
342
  'u_terrain_dim': sourceTile?.dem?.dim || 1,
300
- 'u_terrain_matrix': matrixKey ? this._demMatrixCache[matrixKey].matrix : this._emptyDemMatrix,
343
+ 'u_terrain_matrix': terrainMatrix,
301
344
  'u_terrain_unpack': sourceTile?.dem?.getUnpackVector() || this._emptyDemUnpack,
302
345
  'u_terrain_exaggeration': this.exaggeration,
303
346
  texture: (sourceTile?.demTexture || this._emptyDemTexture).texture,
@@ -0,0 +1,12 @@
1
+ uniform bool u_projection_clip_antimeridian;
2
+
3
+ in highp float v_projection_tile_x;
4
+
5
+ // On globe the z0 tile's buffer wraps around the planet onto the tile itself, drawing geometry near the antimeridian twice.
6
+ // To avoid this, we discard fragments beyond the tile's X extent (0..8192) when enabled for the tile.
7
+ // The range test is half-open so that both sides of the antimeridian tile the seam exactly, without overlap.
8
+ void clipAntimeridian() {
9
+ if (u_projection_clip_antimeridian && (v_projection_tile_x < 0.0 || v_projection_tile_x >= 8192.0)) {
10
+ discard;
11
+ }
12
+ }
@@ -0,0 +1,2 @@
1
+ // This file is generated. Edit build/generate-shaders.ts, then run `npm run codegen`.
2
+ export default 'uniform bool u_projection_clip_antimeridian;in highp float v_projection_tile_x;void clipAntimeridian() {if (u_projection_clip_antimeridian && (v_projection_tile_x < 0.0 || v_projection_tile_x >=8192.0)) {discard;}}';