@tangram.gl/tangram-renderer 0.0.0-stage → 0.0.1

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 (321) hide show
  1. package/README.md +60 -2
  2. package/dist/core.js +36923 -0
  3. package/dist/core.js.map +6 -0
  4. package/dist/index.js +32 -0
  5. package/dist/loaders-gl-worker.js +82 -0
  6. package/dist/loaders-gl-worker.js.map +6 -0
  7. package/dist/map-logic.js +2 -0
  8. package/dist/map-logic.js.map +6 -0
  9. package/dist/projected-basemaps-worker.js +3 -0
  10. package/dist/projected-basemaps-worker.js.map +6 -0
  11. package/dist/shader-modules.js +141 -0
  12. package/dist/shader-modules.js.map +6 -0
  13. package/dist/style-schema.js +126 -0
  14. package/dist/style-schema.js.map +6 -0
  15. package/dist/tangram-style.schema.json +882 -0
  16. package/dist/tangram.debug.mjs +37139 -0
  17. package/dist/tangram.debug.mjs.map +6 -0
  18. package/dist/tangram.min.mjs +4950 -0
  19. package/dist/types/builders/earcut.d.ts +15 -0
  20. package/dist/types/core.d.ts +29 -0
  21. package/dist/types/gl/constants.d.ts +2 -0
  22. package/dist/types/gl/globe_mesh.d.ts +48 -0
  23. package/dist/types/gl/glsl.d.ts +17 -0
  24. package/dist/types/gl/vertex-constants.d.ts +4 -0
  25. package/dist/types/gl/vertex-types.d.ts +34 -0
  26. package/dist/types/gl/vertex_data.d.ts +32 -0
  27. package/dist/types/gl/vertex_elements.d.ts +8 -0
  28. package/dist/types/gl/vertex_layout.d.ts +32 -0
  29. package/dist/types/glsl.d.ts +14 -0
  30. package/dist/types/gpu/luma_device_renderer.d.ts +66 -0
  31. package/dist/types/gpu/resource_lifecycle.d.ts +4 -0
  32. package/dist/types/gpu/tangram_gpu_backend.d.ts +151 -0
  33. package/dist/types/index.d.ts +155 -0
  34. package/dist/types/lights/light-definitions.d.ts +121 -0
  35. package/dist/types/map-logic/draped-terrain-bounds.d.ts +41 -0
  36. package/dist/types/map-logic/globe-terrain-preparation.d.ts +85 -0
  37. package/dist/types/map-logic/globe-terrain.d.ts +54 -0
  38. package/dist/types/map-logic/index.d.ts +33 -0
  39. package/dist/types/map-logic/label-identity.d.ts +22 -0
  40. package/dist/types/map-logic/label-placement.d.ts +32 -0
  41. package/dist/types/map-logic/screen-bounds.d.ts +15 -0
  42. package/dist/types/map-logic/terrain-data.d.ts +55 -0
  43. package/dist/types/map-logic/terrain-error.d.ts +51 -0
  44. package/dist/types/map-logic/terrain-ground-topology.d.ts +26 -0
  45. package/dist/types/map-logic/terrain-height-field.d.ts +59 -0
  46. package/dist/types/map-logic/terrain-navigation.d.ts +27 -0
  47. package/dist/types/map-logic/terrain-neighborhood.d.ts +5 -0
  48. package/dist/types/map-logic/terrain-query.d.ts +33 -0
  49. package/dist/types/map-logic/terrain-resource-budget.d.ts +51 -0
  50. package/dist/types/map-logic/terrain-transition-topology.d.ts +23 -0
  51. package/dist/types/map-logic/tile-build-queue.d.ts +51 -0
  52. package/dist/types/map-logic/tile-cache-policy.d.ts +46 -0
  53. package/dist/types/map-logic/tile-residency.d.ts +17 -0
  54. package/dist/types/map-logic/triangle-query-index.d.ts +87 -0
  55. package/dist/types/procedures/js-yaml.d.ts +15 -0
  56. package/dist/types/procedures/mesh-projector.d.ts +107 -0
  57. package/dist/types/procedures/mvt-dependencies.d.ts +20 -0
  58. package/dist/types/procedures/projected-coordinate-transform.d.ts +27 -0
  59. package/dist/types/procedures/projection-batch-executor.d.ts +53 -0
  60. package/dist/types/procedures/scene-yaml-legacy.d.ts +6 -0
  61. package/dist/types/procedures/terrain-draping.d.ts +165 -0
  62. package/dist/types/procedures/terrain-globe.d.ts +6 -0
  63. package/dist/types/procedures/terrain-styled-topology.d.ts +34 -0
  64. package/dist/types/procedures/web-mercator-legacy.d.ts +4 -0
  65. package/dist/types/procedures/web-mercator-math.d.ts +4 -0
  66. package/dist/types/scene/globals.d.ts +11 -0
  67. package/dist/types/scene/ground_footprint.d.ts +16 -0
  68. package/dist/types/scene/host_frame.d.ts +54 -0
  69. package/dist/types/scene/projection_math.d.ts +52 -0
  70. package/dist/types/scene/scene-resource-types.d.ts +177 -0
  71. package/dist/types/scene/scene_bundle.d.ts +69 -0
  72. package/dist/types/scene/visibility_adapter.d.ts +106 -0
  73. package/dist/types/selection/draped-terrain-surface.d.ts +78 -0
  74. package/dist/types/selection/terrain_surface.d.ts +149 -0
  75. package/dist/types/shader-modules/globe-horizon.d.ts +15 -0
  76. package/dist/types/shader-modules/height-decode.d.ts +13 -0
  77. package/dist/types/shader-modules/hillshade.d.ts +13 -0
  78. package/dist/types/shader-modules/index.d.ts +6 -0
  79. package/dist/types/shader-modules/planar.d.ts +12 -0
  80. package/dist/types/shader-modules/sphere-map.d.ts +13 -0
  81. package/dist/types/shader-modules/triplanar.d.ts +15 -0
  82. package/dist/types/sources/decoded_tile_store.d.ts +95 -0
  83. package/dist/types/sources/tile_source_metadata.d.ts +32 -0
  84. package/dist/types/styles/text/fontfaceobserver.d.ts +10 -0
  85. package/dist/types/tile/tile_id.d.ts +37 -0
  86. package/dist/types/tile/tile_traversal_adapter.d.ts +63 -0
  87. package/dist/types/types.d.ts +293 -0
  88. package/dist/types/utils/geo.d.ts +57 -0
  89. package/dist/types/utils/hash.d.ts +1 -0
  90. package/dist/types/utils/log.d.ts +27 -0
  91. package/dist/types/utils/props.d.ts +6 -0
  92. package/dist/types/utils/subscribe.d.ts +10 -0
  93. package/dist/types/utils/thread.d.ts +6 -0
  94. package/dist/types/utils/urls.d.ts +26 -0
  95. package/dist/types/utils/utils.d.ts +26 -0
  96. package/dist/types/utils/version.d.ts +2 -0
  97. package/dist/types/utils/worker-types.d.ts +68 -0
  98. package/dist/types/utils/worker_broker.d.ts +3 -0
  99. package/package.json +103 -3
  100. package/src/builders/common.ts +41 -0
  101. package/src/builders/earcut.d.ts +15 -0
  102. package/src/builders/geometry-types.ts +60 -0
  103. package/src/builders/points.ts +115 -0
  104. package/src/builders/polygons.ts +225 -0
  105. package/src/builders/polylines.ts +682 -0
  106. package/src/builders/wireframe.ts +31 -0
  107. package/src/core.d.ts +29 -0
  108. package/src/core.ts +33 -0
  109. package/src/experimental/loaders-gl-worker.ts +19 -0
  110. package/src/experimental/projected-basemaps-worker.ts +13 -0
  111. package/src/experimental/projected-mesh-cache.ts +137 -0
  112. package/src/experimental/projected-mesh-domain.ts +110 -0
  113. package/src/experimental/projected-mesh-refinement.ts +193 -0
  114. package/src/experimental/projected-mesh.ts +399 -0
  115. package/src/experimental/projected-normal.ts +26 -0
  116. package/src/experimental/projected-symbol-mesh.ts +74 -0
  117. package/src/gl/constants.ts +18 -0
  118. package/src/gl/context.ts +103 -0
  119. package/src/gl/extensions.ts +30 -0
  120. package/src/gl/globe_mesh.ts +285 -0
  121. package/src/gl/glsl.ts +257 -0
  122. package/src/gl/mesh-types.ts +56 -0
  123. package/src/gl/render_state.ts +111 -0
  124. package/src/gl/shader_program.ts +1310 -0
  125. package/src/gl/texture.ts +814 -0
  126. package/src/gl/uniform_buffer.ts +340 -0
  127. package/src/gl/vao.ts +154 -0
  128. package/src/gl/vbo_mesh.ts +349 -0
  129. package/src/gl/vertex-constants.ts +19 -0
  130. package/src/gl/vertex-types.ts +35 -0
  131. package/src/gl/vertex_data.ts +126 -0
  132. package/src/gl/vertex_elements.ts +54 -0
  133. package/src/gl/vertex_layout.ts +241 -0
  134. package/src/glsl.d.ts +14 -0
  135. package/src/gpu/luma_device_renderer.ts +579 -0
  136. package/src/gpu/pixel_readback.ts +51 -0
  137. package/src/gpu/resource_lifecycle.ts +32 -0
  138. package/src/gpu/tangram_gpu_backend.ts +195 -0
  139. package/src/index.d.ts +155 -0
  140. package/src/index.ts +105 -0
  141. package/src/labels/collision-types.ts +94 -0
  142. package/src/labels/collision.ts +273 -0
  143. package/src/labels/collision_grid.ts +61 -0
  144. package/src/labels/intersect.ts +30 -0
  145. package/src/labels/label-types.ts +91 -0
  146. package/src/labels/label.ts +207 -0
  147. package/src/labels/label_line.ts +801 -0
  148. package/src/labels/label_point.ts +131 -0
  149. package/src/labels/main-pass-types.ts +73 -0
  150. package/src/labels/main_pass.ts +221 -0
  151. package/src/labels/point_anchor.ts +100 -0
  152. package/src/labels/point_placement.ts +150 -0
  153. package/src/labels/projected-pass.ts +259 -0
  154. package/src/labels/repeat_group.ts +72 -0
  155. package/src/lights/ambient_light.glsl +18 -0
  156. package/src/lights/directional_light.glsl +39 -0
  157. package/src/lights/geographic-lights.ts +21 -0
  158. package/src/lights/light-definitions.ts +285 -0
  159. package/src/lights/light.ts +590 -0
  160. package/src/lights/lighting-uniforms.ts +80 -0
  161. package/src/lights/lighting-wgsl.ts +116 -0
  162. package/src/lights/material.glsl +237 -0
  163. package/src/lights/material.ts +145 -0
  164. package/src/lights/native-falloff.ts +37 -0
  165. package/src/lights/point_light.glsl +96 -0
  166. package/src/lights/spot_light.glsl +111 -0
  167. package/src/map-logic/draped-terrain-bounds.ts +71 -0
  168. package/src/map-logic/globe-terrain-preparation.ts +217 -0
  169. package/src/map-logic/globe-terrain.ts +296 -0
  170. package/src/map-logic/index.ts +37 -0
  171. package/src/map-logic/label-identity.ts +36 -0
  172. package/src/map-logic/label-placement.ts +117 -0
  173. package/src/map-logic/screen-bounds.ts +34 -0
  174. package/src/map-logic/terrain-data.ts +107 -0
  175. package/src/map-logic/terrain-error.ts +134 -0
  176. package/src/map-logic/terrain-ground-topology.ts +164 -0
  177. package/src/map-logic/terrain-height-field.ts +254 -0
  178. package/src/map-logic/terrain-navigation.ts +50 -0
  179. package/src/map-logic/terrain-neighborhood.ts +43 -0
  180. package/src/map-logic/terrain-query.ts +51 -0
  181. package/src/map-logic/terrain-resource-budget.ts +124 -0
  182. package/src/map-logic/terrain-transition-topology.ts +164 -0
  183. package/src/map-logic/tile-build-queue.ts +110 -0
  184. package/src/map-logic/tile-cache-policy.ts +89 -0
  185. package/src/map-logic/tile-residency.ts +50 -0
  186. package/src/map-logic/triangle-query-index.ts +265 -0
  187. package/src/procedures/js-yaml.d.ts +15 -0
  188. package/src/procedures/mesh-projector.ts +191 -0
  189. package/src/procedures/mlt-loaders.ts +55 -0
  190. package/src/procedures/mvt-dependencies.d.ts +20 -0
  191. package/src/procedures/mvt-legacy.ts +150 -0
  192. package/src/procedures/mvt-loaders.ts +53 -0
  193. package/src/procedures/mvt-parser.ts +48 -0
  194. package/src/procedures/mvt-properties.ts +36 -0
  195. package/src/procedures/mvt-tile-provider.ts +72 -0
  196. package/src/procedures/pmtiles-loader.ts +75 -0
  197. package/src/procedures/projected-coordinate-transform.ts +79 -0
  198. package/src/procedures/projection-batch-executor.ts +140 -0
  199. package/src/procedures/scene-yaml-legacy.ts +16 -0
  200. package/src/procedures/scene-yaml-loaders.ts +10 -0
  201. package/src/procedures/terrain-draping.ts +713 -0
  202. package/src/procedures/terrain-globe.ts +32 -0
  203. package/src/procedures/terrain-styled-topology.ts +184 -0
  204. package/src/procedures/web-mercator-legacy.ts +24 -0
  205. package/src/procedures/web-mercator-math.ts +50 -0
  206. package/src/scene/camera.ts +298 -0
  207. package/src/scene/camera_base.ts +116 -0
  208. package/src/scene/classic_scene.ts +20 -0
  209. package/src/scene/external_camera.ts +101 -0
  210. package/src/scene/globals.ts +120 -0
  211. package/src/scene/ground_footprint.ts +139 -0
  212. package/src/scene/host_frame.ts +326 -0
  213. package/src/scene/projected_tile_lod.ts +125 -0
  214. package/src/scene/projection_math.ts +142 -0
  215. package/src/scene/projection_shaders.ts +183 -0
  216. package/src/scene/renderer.ts +516 -0
  217. package/src/scene/scene-resource-types.ts +174 -0
  218. package/src/scene/scene.ts +2378 -0
  219. package/src/scene/scene_bundle.ts +304 -0
  220. package/src/scene/scene_debug.ts +129 -0
  221. package/src/scene/scene_loader.ts +402 -0
  222. package/src/scene/scene_worker.ts +528 -0
  223. package/src/scene/view.ts +683 -0
  224. package/src/scene/visibility_adapter.ts +319 -0
  225. package/src/scene/worker_url.ts +15 -0
  226. package/src/selection/draped-terrain-surface.ts +377 -0
  227. package/src/selection/selection-types.ts +79 -0
  228. package/src/selection/selection.ts +542 -0
  229. package/src/selection/selection_fragment.glsl +18 -0
  230. package/src/selection/selection_globals.glsl +9 -0
  231. package/src/selection/selection_pixels.ts +59 -0
  232. package/src/selection/selection_vertex.glsl +15 -0
  233. package/src/selection/terrain_surface.ts +298 -0
  234. package/src/shader-modules/globe-horizon.ts +52 -0
  235. package/src/shader-modules/height-decode.ts +41 -0
  236. package/src/shader-modules/hillshade.ts +70 -0
  237. package/src/shader-modules/index.ts +10 -0
  238. package/src/shader-modules/planar.ts +24 -0
  239. package/src/shader-modules/sphere-map.ts +38 -0
  240. package/src/shader-modules/triplanar.ts +45 -0
  241. package/src/sources/aligned_tile_source.ts +122 -0
  242. package/src/sources/data_source.ts +655 -0
  243. package/src/sources/decoded_tile_store.ts +245 -0
  244. package/src/sources/geojson.ts +300 -0
  245. package/src/sources/mvt.ts +247 -0
  246. package/src/sources/raster.ts +341 -0
  247. package/src/sources/shared_tile_source_adapter.ts +117 -0
  248. package/src/sources/sources.ts +10 -0
  249. package/src/sources/tile_source_adapter.ts +84 -0
  250. package/src/sources/tile_source_metadata.ts +65 -0
  251. package/src/sources/tile_source_state.ts +112 -0
  252. package/src/sources/topojson.ts +90 -0
  253. package/src/styles/feature_annotations.ts +16 -0
  254. package/src/styles/filter.ts +190 -0
  255. package/src/styles/geometry-style-types.ts +209 -0
  256. package/src/styles/globe_visibility_wgsl.ts +22 -0
  257. package/src/styles/layer-types.ts +45 -0
  258. package/src/styles/layer.ts +586 -0
  259. package/src/styles/lines/dasharray.ts +49 -0
  260. package/src/styles/lines/lines.ts +707 -0
  261. package/src/styles/lines/lines_wgsl.ts +224 -0
  262. package/src/styles/points/point-runtime-types.ts +125 -0
  263. package/src/styles/points/points.ts +1079 -0
  264. package/src/styles/points/points_fragment.glsl +126 -0
  265. package/src/styles/points/points_vertex.glsl +230 -0
  266. package/src/styles/points/points_wgsl.ts +189 -0
  267. package/src/styles/polygons/polygons.ts +268 -0
  268. package/src/styles/polygons/polygons_fragment.glsl +173 -0
  269. package/src/styles/polygons/polygons_vertex.glsl +213 -0
  270. package/src/styles/polygons/polygons_wgsl.ts +127 -0
  271. package/src/styles/property-types.ts +125 -0
  272. package/src/styles/raster/raster.ts +33 -0
  273. package/src/styles/raster/raster_globals.glsl +46 -0
  274. package/src/styles/style-mixing-types.ts +101 -0
  275. package/src/styles/style-runtime-types.ts +220 -0
  276. package/src/styles/style-schema.ts +133 -0
  277. package/src/styles/style.ts +1033 -0
  278. package/src/styles/style_globals.glsl +69 -0
  279. package/src/styles/style_manager.ts +464 -0
  280. package/src/styles/style_parser.ts +620 -0
  281. package/src/styles/text/font_manager.ts +180 -0
  282. package/src/styles/text/fontfaceobserver.d.ts +10 -0
  283. package/src/styles/text/text-types.ts +220 -0
  284. package/src/styles/text/text.ts +378 -0
  285. package/src/styles/text/text_canvas.ts +773 -0
  286. package/src/styles/text/text_labels.ts +436 -0
  287. package/src/styles/text/text_segments.ts +142 -0
  288. package/src/styles/text/text_settings.ts +228 -0
  289. package/src/styles/text/text_wgsl.ts +159 -0
  290. package/src/styles/text/text_wrap.ts +194 -0
  291. package/src/tile/globe_tile_preload.ts +40 -0
  292. package/src/tile/tangram_tileset_2d.ts +108 -0
  293. package/src/tile/tile-build-types.ts +155 -0
  294. package/src/tile/tile.ts +807 -0
  295. package/src/tile/tile_id.ts +172 -0
  296. package/src/tile/tile_manager.ts +889 -0
  297. package/src/tile/tile_pyramid.ts +119 -0
  298. package/src/tile/tile_resource_cache.ts +91 -0
  299. package/src/tile/tile_traversal_adapter.ts +132 -0
  300. package/src/types.ts +332 -0
  301. package/src/utils/debounce.ts +17 -0
  302. package/src/utils/debug_settings.ts +43 -0
  303. package/src/utils/errors.ts +12 -0
  304. package/src/utils/functions.ts +95 -0
  305. package/src/utils/geo.ts +364 -0
  306. package/src/utils/hash.ts +14 -0
  307. package/src/utils/log.ts +108 -0
  308. package/src/utils/media_capture.ts +193 -0
  309. package/src/utils/merge.ts +44 -0
  310. package/src/utils/obb.ts +134 -0
  311. package/src/utils/props.ts +43 -0
  312. package/src/utils/slice.ts +14 -0
  313. package/src/utils/subscribe.ts +58 -0
  314. package/src/utils/task.ts +154 -0
  315. package/src/utils/thread.ts +26 -0
  316. package/src/utils/urls.ts +223 -0
  317. package/src/utils/utils.ts +310 -0
  318. package/src/utils/vector.ts +197 -0
  319. package/src/utils/version.ts +8 -0
  320. package/src/utils/worker-types.ts +79 -0
  321. package/src/utils/worker_broker.ts +523 -0
@@ -0,0 +1,296 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {worldToLngLat} from '@math.gl/web-mercator';
6
+ import {TerrainHeightField} from './terrain-height-field';
7
+ import type {TerrainHeightGrid} from './terrain-data';
8
+ import {getTerrainEdgeNodes} from './terrain-ground-topology';
9
+
10
+ /** Spherical deck.gl common-space radius and physical reference radius, in meters. */
11
+ const GLOBE_RADIUS = 256, EARTH_RADIUS = 6370972, MERCATOR_RADIUS = 6378137;
12
+
13
+ /** Build an inclusive summed-area mask only when a source actually contains holes. */
14
+ function* buildMissingTerrainPrefix(grid: TerrainHeightGrid): Generator<void, Uint32Array | null, void> {
15
+ let missingSample = false;
16
+ for (let offset = 0; offset < grid.valid.length && !missingSample; offset += 4096) {
17
+ for (let index = offset; index < Math.min(grid.valid.length, offset + 4096); index++) if (grid.valid[index] === 0) {missingSample = true; break;}
18
+ yield;
19
+ }
20
+ if (!missingSample) return null;
21
+ const stride = grid.width + 1, prefix = new Uint32Array(stride * (grid.height + 1));
22
+ for (let row = 0; row < grid.height; row++) {
23
+ let missing = 0;
24
+ for (let column = 0; column < grid.width; column++) {
25
+ missing += Number(grid.valid[row * grid.width + column] === 0);
26
+ prefix[(row + 1) * stride + column + 1] = prefix[row * stride + column + 1] + missing;
27
+ }
28
+ if (row % 8 === 7) yield;
29
+ }
30
+ return prefix;
31
+ }
32
+
33
+ /** Conservatively include every sample supporting bilinear interpolation over a cell footprint. */
34
+ function hasMissingTerrain(grid: TerrainHeightGrid, prefix: Uint32Array, bounds: readonly [number, number, number, number]): boolean {
35
+ const west = Math.max(grid.bounds[0], bounds[0]), south = Math.max(grid.bounds[1], bounds[1]);
36
+ const east = Math.min(grid.bounds[2], bounds[2]), north = Math.min(grid.bounds[3], bounds[3]);
37
+ if (west > east || south > north) return false;
38
+ const pixelCenter = grid.registration === 'pixel-center';
39
+ const columnScale = (pixelCenter ? grid.width : grid.width - 1) / (grid.bounds[2] - grid.bounds[0]);
40
+ const rowScale = (pixelCenter ? grid.height : grid.height - 1) / (grid.bounds[3] - grid.bounds[1]);
41
+ const offset = pixelCenter ? 0.5 : 0;
42
+ const left = Math.floor(Math.max(0, (west - grid.bounds[0]) * columnScale - offset));
43
+ const right = Math.ceil(Math.min(grid.width - 1, (east - grid.bounds[0]) * columnScale - offset));
44
+ const top = Math.floor(Math.max(0, (grid.bounds[3] - north) * rowScale - offset));
45
+ const bottom = Math.ceil(Math.min(grid.height - 1, (grid.bounds[3] - south) * rowScale - offset));
46
+ const stride = grid.width + 1;
47
+ return prefix[(bottom + 1) * stride + right + 1] - prefix[top * stride + right + 1] - prefix[(bottom + 1) * stride + left] + prefix[top * stride + left] > 0;
48
+ }
49
+
50
+ /** Detached render/query buffers; original height grids are never exaggerated or modified. */
51
+ export interface GlobeTerrainMesh {
52
+ /** Longitude/latitude degrees and displayed elevation meters for deck.gl projection. */
53
+ geographicPositions: Float32Array;
54
+ /** Radius-256 spherical XYZ matching those packed geographic vertices. */
55
+ positions: Float64Array;
56
+ /** Physical source elevation per vertex, independent of exaggeration and chord error. */
57
+ physicalHeights: Float32Array;
58
+ /** Outward triangle topology; cells touching no-data samples are omitted. */
59
+ indices: Uint32Array;
60
+ /** RGB relief colors with baked common-space lighting, independent of backend. */
61
+ colors: Float32Array;
62
+ }
63
+
64
+ /** Opaque immutable sampling/topology snapshot; keep it to rescale without reading source grids again. */
65
+ export interface GlobeTerrainMeshTemplate {
66
+ /** Number of regular and stitched vertices in this snapshot. */
67
+ readonly vertexCount: number;
68
+ /** Number of outward triangles, excluding no-data cells. */
69
+ readonly triangleCount: number;
70
+ /** Owned typed-array backing storage; excludes source grids and temporary JavaScript arrays. */
71
+ readonly byteLength: number;
72
+ }
73
+
74
+ /** Private backing arrays cannot be modified through the public template handle. */
75
+ const terrainTemplates = new WeakMap<GlobeTerrainMeshTemplate, {
76
+ coordinates: Float32Array;
77
+ heights: Float64Array;
78
+ gradients: Float64Array;
79
+ frame: Float64Array;
80
+ physicalHeights: Float32Array;
81
+ indices: Uint32Array;
82
+ }>();
83
+
84
+ /** Attenuate exaggerated relief to flat after one diameter doubling; explicit physical/subphysical settings stay fixed. */
85
+ export function getGlobeTerrainExaggeration(globeScreenRatio: number, maximum = 100): number {
86
+ if (!Number.isFinite(maximum) || maximum < 0 || maximum > 1000 || !Number.isFinite(globeScreenRatio) || globeScreenRatio < 0) throw new Error('Invalid globe terrain exaggeration or screen ratio');
87
+ if (maximum <= 1) return maximum;
88
+ const progress = Math.max(0, Math.min(1, Math.log2(Math.max(1, globeScreenRatio))));
89
+ return (maximum + 1) ** (1 - progress * progress * (3 - 2 * progress)) - 1;
90
+ }
91
+
92
+ /** Resample a Mercator DEM onto curved triangles; neighbors reconcile matching tile edges. */
93
+ export function buildGlobeTerrainMesh(grid: TerrainHeightGrid, options: {
94
+ /** Effective display multiplier, not the zoom-independent maximum. */
95
+ exaggeration?: number;
96
+ /** Cells along each tile axis, bounded to keep allocations predictable; defaults to 64. */
97
+ resolution?: number;
98
+ /** Same-datum, non-overlapping neighbors with balanced 2:1 edges and the same cell resolution. */
99
+ neighbors?: readonly TerrainHeightGrid[];
100
+ } = {}): GlobeTerrainMesh {
101
+ getGlobeTerrainExaggeration(0, options.exaggeration ?? 1);
102
+ return scaleGlobeTerrainMesh(prepareGlobeTerrainMesh(grid, options), options.exaggeration ?? 1);
103
+ }
104
+
105
+ /** Snapshot source sampling, no-data, edge stitching and physical slopes once per immutable neighborhood. */
106
+ export function prepareGlobeTerrainMesh(grid: TerrainHeightGrid, options: {
107
+ /** Uniform cells per tile axis, integer [2, 256]; defaults to 64. */
108
+ resolution?: number;
109
+ /** Same-datum non-overlapping neighbors with balanced 2:1 edges. */
110
+ neighbors?: readonly TerrainHeightGrid[];
111
+ } = {}): GlobeTerrainMeshTemplate {
112
+ const steps = prepareGlobeTerrainMeshSteps(grid, options);
113
+ let result = steps.next();
114
+ while (!result.done) result = steps.next();
115
+ return result.value;
116
+ }
117
+
118
+ /** Cooperatively prepare identical topology; each yield releases control between bounded sampling chunks. */
119
+ export function* prepareGlobeTerrainMeshSteps(grid: TerrainHeightGrid, options: {
120
+ /** Uniform cells per tile axis, integer [2, 256]; defaults to 64. */
121
+ resolution?: number;
122
+ /** Same-datum non-overlapping neighbors with balanced 2:1 edges. */
123
+ neighbors?: readonly TerrainHeightGrid[];
124
+ /** Exact final typed-array admission fence; scratch is excluded. The identity is not a usable template until completion. */
125
+ onAllocate?: (bytes: number, allocation: GlobeTerrainMeshTemplate) => void;
126
+ } = {}): Generator<void, GlobeTerrainMeshTemplate, void> {
127
+ const resolution = options.resolution ?? 64;
128
+ if (!Number.isSafeInteger(resolution) || resolution < 2 || resolution > 256) throw new Error('Globe terrain resolution must be an integer in [2, 256]');
129
+ const grids = [grid, ...(options.neighbors ?? [])];
130
+ const halfWorld = Math.PI * MERCATOR_RADIUS;
131
+ for (const candidate of grids) {
132
+ if (!Number.isSafeInteger(candidate.width) || !Number.isSafeInteger(candidate.height) || candidate.width < 2 || candidate.height < 2 || candidate.heights.length !== candidate.width * candidate.height || candidate.valid.length !== candidate.heights.length ||
133
+ candidate.bounds.length !== 4 || !candidate.bounds.every(Number.isFinite) || candidate.bounds[0] >= candidate.bounds[2] || candidate.bounds[1] >= candidate.bounds[3] ||
134
+ candidate.bounds.some(value => Math.abs(value) > halfWorld + 1e-6) ||
135
+ !['grid-node', 'pixel-center'].includes(candidate.registration) || !['ellipsoidal', 'orthometric', 'unknown'].includes(candidate.verticalDatum) || candidate.verticalDatum !== grid.verticalDatum) throw new Error('Invalid globe terrain grid or incompatible vertical datum');
136
+ }
137
+ const circumference = 2 * Math.PI * MERCATOR_RADIUS;
138
+ const samplingGrids = grids.flatMap(candidate => [-1, 0, 1].map((world): TerrainHeightGrid => ({...candidate, bounds: [candidate.bounds[0] + world * circumference, candidate.bounds[1], candidate.bounds[2] + world * circumference, candidate.bounds[3]]})))
139
+ .filter(candidate => candidate.bounds[0] <= grid.bounds[2] && candidate.bounds[2] >= grid.bounds[0] && candidate.bounds[1] <= grid.bounds[3] && candidate.bounds[3] >= grid.bounds[1]);
140
+ const heightField = new TerrainHeightField(samplingGrids);
141
+ const missingRegions: {grid: TerrainHeightGrid; prefix: Uint32Array}[] = [];
142
+ for (const candidate of samplingGrids) {
143
+ const prefix = yield* buildMissingTerrainPrefix(candidate);
144
+ if (prefix) missingRegions.push({grid: candidate, prefix});
145
+ }
146
+ const [west, south, east, north] = grid.bounds;
147
+ const tolerance = Math.max(east - west, north - south) * 1e-10;
148
+ for (const candidate of samplingGrids) {
149
+ const horizontal = Math.min(east, candidate.bounds[2]) - Math.max(west, candidate.bounds[0]);
150
+ const vertical = Math.min(north, candidate.bounds[3]) - Math.max(south, candidate.bounds[1]);
151
+ const identical = candidate.bounds.every((value, index) => Math.abs(value - grid.bounds[index]) <= tolerance);
152
+ if (!identical && horizontal > tolerance && vertical > tolerance) throw new Error('Globe terrain neighbors must not overlap');
153
+ if (!identical && (horizontal > tolerance || vertical > tolerance)) {
154
+ const ratio = (candidate.bounds[2] - candidate.bounds[0]) / (east - west);
155
+ if (![0.5, 1, 2].some(value => Math.abs(ratio - value) < 1e-8) || Math.abs((candidate.bounds[3] - candidate.bounds[1]) / (north - south) - ratio) > 1e-8) throw new Error('Globe terrain edges require balanced 2:1 footprints');
156
+ }
157
+ }
158
+ const size = resolution + 1;
159
+ const geographicPositions: number[] = [], positions: number[] = [], physicalHeights: number[] = [], samplePositions: [number, number][] = [], valid: boolean[] = [];
160
+ const nodes = new Map<string, number>();
161
+ if ((east - west) / resolution >= halfWorld - 1e-6) throw new Error('Globe terrain cells must span less than 180 degrees longitude');
162
+ const worldScale = 512 / (2 * Math.PI * MERCATOR_RADIUS);
163
+ /** Shared nodes use the same packed geographic coordinates for drawing and queries. */
164
+ function vertex(column: number, row: number): number {
165
+ column = Math.round(column * 1e8) / 1e8; row = Math.round(row * 1e8) / 1e8;
166
+ const key = `${column},${row}`, retained = nodes.get(key);
167
+ if (retained !== undefined) return retained;
168
+ const index = physicalHeights.length; nodes.set(key, index);
169
+ const x = west + (east - west) * column / resolution, y = north - (north - south) * row / resolution;
170
+ const height = heightField.sampleHeight([x, y]);
171
+ if (height !== null && !Number.isFinite(height)) throw new Error('Invalid globe terrain elevation');
172
+ valid.push(height !== null); physicalHeights.push(height ?? NaN); samplePositions.push([x, y]);
173
+ const coordinate = worldToLngLat([x * worldScale + 256, y * worldScale + 256]);
174
+ const packed = [Math.fround(coordinate[0]), Math.fround(coordinate[1]), 0];
175
+ geographicPositions.push(...packed);
176
+ // Compute queries from the exact Float32 attributes submitted to deck.gl.
177
+ const longitude = packed[0] * Math.PI / 180, latitude = packed[1] * Math.PI / 180;
178
+ const radius = GLOBE_RADIUS;
179
+ positions.push(Math.sin(longitude) * Math.cos(latitude) * radius, -Math.cos(longitude) * Math.cos(latitude) * radius, Math.sin(latitude) * radius);
180
+ return index;
181
+ }
182
+ // Retain regular node ordering; additional nodes are confined to stitched boundary rings.
183
+ for (let row = 0; row < size; row++) {
184
+ for (let column = 0; column < size; column++) vertex(column, row);
185
+ yield;
186
+ }
187
+ const triangles: number[] = [];
188
+ /** Add one outward non-degenerate triangle to the shared render/query topology. */
189
+ function triangle(vertices: readonly number[]): void {
190
+ if (!vertices.every(index => valid[index])) return;
191
+ const [a, b, c] = vertices.map(index => index * 3);
192
+ const ab = [positions[b] - positions[a], positions[b + 1] - positions[a + 1], positions[b + 2] - positions[a + 2]], ac = [positions[c] - positions[a], positions[c + 1] - positions[a + 1], positions[c + 2] - positions[a + 2]];
193
+ const normal = [ab[1] * ac[2] - ab[2] * ac[1], ab[2] * ac[0] - ab[0] * ac[2], ab[0] * ac[1] - ab[1] * ac[0]];
194
+ if (Math.hypot(...normal) === 0) throw new Error('Globe terrain triangle collapses at packed coordinate precision');
195
+ const sign = normal[0] * positions[a] + normal[1] * positions[a + 1] + normal[2] * positions[a + 2] < 0 ? -1 : 1;
196
+ triangles.push(vertices[0], vertices[sign === 1 ? 1 : 2], vertices[sign === 1 ? 2 : 1]);
197
+ }
198
+ /** Include neighbor edge nodes so fine half-steps also exist on the coarse mesh. */
199
+ function edgeNodes(start: readonly [number, number], end: readonly [number, number]): number[] {
200
+ return getTerrainEdgeNodes(grid.bounds, samplingGrids.map(candidate => candidate.bounds), resolution, start, end).map(point => vertex(...point));
201
+ }
202
+ /** Only boundary triangles with extra edge nodes need a center fan. */
203
+ function reconstruct(corners: readonly [number, number][]): void {
204
+ const ring: number[] = [];
205
+ for (let edge = 0; edge < 3; edge++) {
206
+ const start = corners[edge], end = corners[(edge + 1) % 3];
207
+ ring.push(vertex(start[0], start[1]), ...edgeNodes(start, end));
208
+ }
209
+ if (!ring.every(index => valid[index])) return;
210
+ if (ring.length === 3) triangle(ring);
211
+ else {
212
+ const center = vertex(corners.reduce((sum, point) => sum + point[0] / 3, 0), corners.reduce((sum, point) => sum + point[1] / 3, 0));
213
+ for (let edge = 0; edge < ring.length; edge++) triangle([center, ring[edge], ring[(edge + 1) % ring.length]]);
214
+ }
215
+ }
216
+ for (let row = 0; row < resolution; row++) {
217
+ for (let column = 0; column < resolution; column++) {
218
+ const first = row * size + column, second = first + 1, third = first + size, fourth = third + 1;
219
+ if (![first, second, third, fourth].every(vertex => valid[vertex])) continue;
220
+ const cellBounds: [number, number, number, number] = [west + (east - west) * column / resolution, north - (north - south) * (row + 1) / resolution,
221
+ west + (east - west) * (column + 1) / resolution, north - (north - south) * row / resolution];
222
+ if (missingRegions.some(region => hasMissingTerrain(region.grid, region.prefix, cellBounds))) continue;
223
+ reconstruct([[column, row], [column, row + 1], [column + 1, row]]);
224
+ reconstruct([[column + 1, row], [column, row + 1], [column + 1, row + 1]]);
225
+ }
226
+ yield;
227
+ }
228
+ const template = Object.freeze({vertexCount: physicalHeights.length, triangleCount: triangles.length / 3,
229
+ byteLength: physicalHeights.length * 68 + triangles.length * 4});
230
+ options.onAllocate?.(template.byteLength, template);
231
+ const gradients = new Float64Array(physicalHeights.length * 2), frame = new Float64Array(physicalHeights.length * 4);
232
+ /** Identical edge sample neighborhoods yield identical spherical lighting at either LOD. */
233
+ function slope(negative: number | null, positive: number | null, center: number, distance: number): number {
234
+ return negative !== null && positive !== null ? (positive - negative) / (2 * distance) : positive !== null ? (positive - center) / distance : negative !== null ? (center - negative) / distance : 0;
235
+ }
236
+ for (let index = 0; index < physicalHeights.length; index++) {
237
+ const offset = index * 3, [x, y] = samplePositions[index], height = physicalHeights[index];
238
+ const longitude = geographicPositions[offset] * Math.PI / 180, latitude = geographicPositions[offset + 1] * Math.PI / 180;
239
+ frame.set([Math.sin(longitude), Math.cos(longitude), Math.sin(latitude), Math.cos(latitude)], index * 4);
240
+ const touching = samplingGrids.filter(candidate => x >= candidate.bounds[0] - tolerance && x <= candidate.bounds[2] + tolerance && y >= candidate.bounds[1] - tolerance && y <= candidate.bounds[3] + tolerance);
241
+ const distanceX = Math.min(...touching.map(candidate => (candidate.bounds[2] - candidate.bounds[0]) / (candidate.registration === 'pixel-center' ? candidate.width : candidate.width - 1)));
242
+ const distanceY = Math.min(...touching.map(candidate => (candidate.bounds[3] - candidate.bounds[1]) / (candidate.registration === 'pixel-center' ? candidate.height : candidate.height - 1)));
243
+ const physicalScale = Math.cos(latitude) * EARTH_RADIUS / MERCATOR_RADIUS;
244
+ gradients[index * 2] = !valid[index] ? 0 : slope(heightField.sampleHeight([x - distanceX, y]), heightField.sampleHeight([x + distanceX, y]), height, distanceX * physicalScale);
245
+ gradients[index * 2 + 1] = !valid[index] ? 0 : slope(heightField.sampleHeight([x, y - distanceY]), heightField.sampleHeight([x, y + distanceY]), height, distanceY * physicalScale);
246
+ if (index % 128 === 127) yield;
247
+ }
248
+ const buffers = {
249
+ coordinates: Float32Array.from(geographicPositions.filter((_, index) => index % 3 !== 2)),
250
+ heights: Float64Array.from(physicalHeights), gradients, frame,
251
+ physicalHeights: Float32Array.from(physicalHeights), indices: Uint32Array.from(triangles)
252
+ };
253
+ terrainTemplates.set(template, buffers);
254
+ return template;
255
+ }
256
+
257
+ /** Rebuild detached display/query buffers without resampling or restitching the immutable template. */
258
+ export function scaleGlobeTerrainMesh(template: GlobeTerrainMeshTemplate, exaggeration = 1): GlobeTerrainMesh {
259
+ const steps = scaleGlobeTerrainMeshSteps(template, exaggeration);
260
+ let result = steps.next();
261
+ while (!result.done) result = steps.next();
262
+ return result.value;
263
+ }
264
+
265
+ /** Cooperatively rescale identical packed vertices/lighting; yields every 128 vertices without resampling. */
266
+ export function* scaleGlobeTerrainMeshSteps(template: GlobeTerrainMeshTemplate, exaggeration = 1): Generator<void, GlobeTerrainMesh, void> {
267
+ getGlobeTerrainExaggeration(0, exaggeration);
268
+ const prepared = terrainTemplates.get(template);
269
+ if (!prepared) throw new Error('Globe terrain template must come from prepareGlobeTerrainMesh');
270
+ const geographicPositions = new Float32Array(template.vertexCount * 3), positions = new Float64Array(template.vertexCount * 3), colors = new Float32Array(template.vertexCount * 3);
271
+ for (let index = 0; index < template.vertexCount; index++) {
272
+ const offset = index * 3, height = prepared.heights[index];
273
+ const valid = !Number.isNaN(height);
274
+ if (valid && EARTH_RADIUS + height * exaggeration <= 0) throw new Error('Invalid globe terrain elevation');
275
+ geographicPositions[offset] = prepared.coordinates[index * 2];
276
+ geographicPositions[offset + 1] = prepared.coordinates[index * 2 + 1];
277
+ geographicPositions[offset + 2] = (valid ? height : 0) * exaggeration;
278
+ if (!Number.isFinite(geographicPositions[offset + 2])) throw new Error('Globe terrain elevation exceeds packed display range');
279
+ const radiusScale = 1 + geographicPositions[offset + 2] / EARTH_RADIUS;
280
+ const gradientX = prepared.gradients[index * 2] * exaggeration / radiusScale, gradientY = prepared.gradients[index * 2 + 1] * exaggeration / radiusScale;
281
+ const sinLongitude = prepared.frame[index * 4], cosLongitude = prepared.frame[index * 4 + 1], sinLatitude = prepared.frame[index * 4 + 2], cosLatitude = prepared.frame[index * 4 + 3];
282
+ const radialX = sinLongitude * cosLatitude, radialY = -cosLongitude * cosLatitude;
283
+ const radius = GLOBE_RADIUS * radiusScale;
284
+ positions[offset] = radialX * radius; positions[offset + 1] = radialY * radius; positions[offset + 2] = sinLatitude * radius;
285
+ const normalX = radialX - gradientX * cosLongitude - gradientY * (-sinLongitude * sinLatitude);
286
+ const normalY = radialY - gradientX * sinLongitude - gradientY * (cosLongitude * sinLatitude);
287
+ const normalZ = sinLatitude - gradientX * 0 - gradientY * cosLatitude;
288
+ if (![normalX, normalY, normalZ].every(Number.isFinite)) throw new Error('Invalid globe terrain slope');
289
+ const light = 0.4 + 0.6 * Math.max(0, (normalX - normalY + normalZ) / (Math.hypot(normalX, normalY, normalZ) * Math.sqrt(3)));
290
+ colors[offset] = (height > 2500 ? 0.9 : height <= 0 ? 0.12 : 0.36) * light;
291
+ colors[offset + 1] = (height > 2500 ? 0.92 : height <= 0 ? 0.3 : 0.58) * light;
292
+ colors[offset + 2] = (height > 2500 ? 0.96 : height <= 0 ? 0.48 : 0.34) * light;
293
+ if (index % 128 === 127) yield;
294
+ }
295
+ return {geographicPositions, positions, physicalHeights: prepared.physicalHeights.slice(), indices: prepared.indices.slice(), colors};
296
+ }
@@ -0,0 +1,37 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ export {resolveLabelPlacement} from './label-placement';
6
+ export type {ScreenLabelCandidate, LabelPlacementOptions} from './label-placement';
7
+ export {areGeographicLabelCopies} from './label-identity';
8
+ export type {GeographicLabelAnchor, GeographicLabelIdentityOptions} from './label-identity';
9
+ export {intersectsScreenBounds, unionScreenBounds, getScreenBoundsCells} from './screen-bounds';
10
+ export type {ScreenBounds, LabelViewport} from './screen-bounds';
11
+ export {default as TileBuildQueue} from './tile-build-queue';
12
+ export type {TileBuildTask} from './tile-build-queue';
13
+ export {TileResidency} from './tile-residency';
14
+ export {TileCachePolicy} from './tile-cache-policy';
15
+ export type {TileCacheRecord, TileCacheOptions, TileCacheStatistics} from './tile-cache-policy';
16
+ export {TERRARIUM_DECODER, TERRAIN_RGB_DECODER, decodeTerrainHeightGrid, sampleTerrainHeight} from './terrain-data';
17
+ export {sampleTerrainNeighborhood, resampleTerrainNodes} from './terrain-neighborhood';
18
+ export {TerrainHeightField} from './terrain-height-field';
19
+ export {TerrainResourceBudget} from './terrain-resource-budget';
20
+ export type {TerrainResourceDomain, TerrainResourceLease, TerrainResourceRequest} from './terrain-resource-budget';
21
+ export {measureTerrainError, boundTerrainMeshError, selectTerrainMeshResolution, boundProjectedDisplacement} from './terrain-error';
22
+ export type {TerrainErrorEnvelope} from './terrain-error';
23
+ export {guardTerrainMotion} from './terrain-navigation';
24
+ export type {TerrainMotionResult} from './terrain-navigation';
25
+ export type {TerrainFieldBounds, TerrainHeightFieldOptions, TerrainHeightFieldStatistics} from './terrain-height-field';
26
+ export {createTerrainGroundTopology} from './terrain-ground-topology';
27
+ export type {TerrainGroundTopology} from './terrain-ground-topology';
28
+ export type {ElevationDecoder, TerrainGridOptions, TerrainHeightGrid} from './terrain-data';
29
+ export {TriangleQueryIndex} from './triangle-query-index';
30
+ export {adaptTerrainQuerySource} from './terrain-query';
31
+ export type {TerrainQuerySource, TerrainQueryHit} from './terrain-query';
32
+ export type {TerrainSurface, TerrainSurfaceBounds, TerrainRay, TerrainSurfaceHit} from '../selection/terrain_surface';
33
+ export type {IndexedTriangleHit, TriangleQueryResult, TriangleQueryIndexOptions} from './triangle-query-index';
34
+ export {buildGlobeTerrainMesh, prepareGlobeTerrainMesh, prepareGlobeTerrainMeshSteps, scaleGlobeTerrainMesh, scaleGlobeTerrainMeshSteps, getGlobeTerrainExaggeration} from './globe-terrain';
35
+ export type {GlobeTerrainMesh, GlobeTerrainMeshTemplate} from './globe-terrain';
36
+ export {GlobeTerrainPreparation, getPreparedGlobeTerrainTemplate, assertGlobeTerrainPreparationSnapshot} from './globe-terrain-preparation';
37
+ export type {GlobeTerrainPreparationSnapshot, GlobeTerrainPreparationStatistics} from './globe-terrain-preparation';
@@ -0,0 +1,36 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ /** Geographic identity retained independently of rendered geometry and cameras. */
6
+ export interface GeographicLabelAnchor {
7
+ /** Source/style/feature/text identity; absent identities cannot deduplicate. */
8
+ readonly identity?: string;
9
+ /** Tile identity; multiple mesh parts of the same tile are not buffered copies. */
10
+ readonly sourceTileIdentity: object | string;
11
+ /** Absolute projected XY in the same units as the world width. */
12
+ readonly anchor: readonly [number, number];
13
+ /** Quantized physical elevation; distinct elevations remain separate labels. */
14
+ readonly height: number;
15
+ /** Source detail used for coordinate quantization, independent of style zoom. */
16
+ readonly sourceZoom: number;
17
+ }
18
+
19
+ /** Explicit coordinate conventions; no renderer projection or Earth radius is assumed. */
20
+ export interface GeographicLabelIdentityOptions {
21
+ /** Width of a complete projected world in anchor units. */
22
+ readonly worldWidth: number;
23
+ /** Treat horizontally wrapped copies as the same surface, as on a globe. */
24
+ readonly wrapHorizontal: boolean;
25
+ }
26
+
27
+ /** Match buffered copies within two 4096-unit source-tile quantization steps. */
28
+ export function areGeographicLabelCopies(candidate: GeographicLabelAnchor, previous: GeographicLabelAnchor,
29
+ options: GeographicLabelIdentityOptions): boolean {
30
+ if (!candidate.identity || candidate.identity !== previous.identity || candidate.sourceTileIdentity === previous.sourceTileIdentity) return false;
31
+ if (candidate.height !== previous.height) return false;
32
+ const tolerance = 2 * options.worldWidth / (4096 * 2 ** Math.min(candidate.sourceZoom, previous.sourceZoom));
33
+ const horizontal = candidate.anchor[0] - previous.anchor[0];
34
+ const difference = options.wrapHorizontal ? horizontal - Math.round(horizontal / options.worldWidth) * options.worldWidth : horizontal;
35
+ return Math.hypot(difference, candidate.anchor[1] - previous.anchor[1]) <= tolerance;
36
+ }
@@ -0,0 +1,117 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {getScreenBoundsCells, intersectsScreenBounds} from './screen-bounds';
6
+ import type {ScreenBounds, LabelViewport} from './screen-bounds';
7
+
8
+ /** Renderer-independent candidate, already projected and ordered by the caller. */
9
+ export interface ScreenLabelCandidate {
10
+ /** Unique ID within this invocation; also identifies linked candidates. */
11
+ readonly id: string;
12
+ /** Visible, padded local CSS-pixel bounds per named view. Empty means hidden. */
13
+ readonly boxes: ReadonlyMap<string, ScreenBounds>;
14
+ /** False skips overlap rejection but retains repeat and identity rules. */
15
+ readonly collide?: boolean;
16
+ /** Optional dependency; reciprocal links form an atomic pair. */
17
+ readonly linkedId?: string;
18
+ /** Key used to compare candidate copies via the supplied identity policy. */
19
+ readonly identity?: string;
20
+ /** Optional repeat group shared by labels representing the same content. */
21
+ readonly repeatGroup?: string;
22
+ /** Minimum CSS-pixel separation between box centers in every shared view. */
23
+ readonly repeatDistance?: number;
24
+ }
25
+
26
+ /** Geometry and identity policy supplied by the host, not owned by this algorithm. */
27
+ export interface LabelPlacementOptions<Candidate extends ScreenLabelCandidate> {
28
+ /** Dimensions for every view named by candidate bounds. */
29
+ readonly viewports: ReadonlyMap<string, LabelViewport>;
30
+ /** Optional buffered-copy policy; called only within matching identity groups. */
31
+ readonly isDuplicate?: (candidate: Candidate, previous: Candidate) => boolean;
32
+ }
33
+
34
+ /**
35
+ * Resolve one visibility mask across all views in caller-supplied priority order.
36
+ * Inputs must have unique IDs and finite, ordered, already clipped/padded bounds.
37
+ * No input mutation, global state, camera, tile, worker, DOM or GPU dependency.
38
+ * Reciprocal links are placed atomically; optional children may overlap their parent.
39
+ */
40
+ export function resolveLabelPlacement<Candidate extends ScreenLabelCandidate>(ordered: readonly Candidate[],
41
+ options: LabelPlacementOptions<Candidate>): ReadonlyMap<Candidate, boolean> {
42
+ const candidates = new Map(ordered.map(candidate => [candidate.id, candidate]));
43
+ const shown = new Map<Candidate, boolean>();
44
+ const identities = new Map<string, Candidate[]>();
45
+ const repeatGroups = new Map<string, Candidate[]>();
46
+ const grids = new Map<string, Map<string, Set<Candidate>>>();
47
+ const placing = new Set<Candidate>();
48
+ /** Require a matching local viewport for spatial indexing. */
49
+ function getViewport(view: string): LabelViewport {
50
+ const viewport = options.viewports.get(view);
51
+ if (!viewport) throw new Error(`Missing label viewport: ${view}`);
52
+ return viewport;
53
+ }
54
+ /** Resolve dependencies before committing one candidate or reciprocal pair. */
55
+ function place(candidate: Candidate): boolean {
56
+ const visibility = shown.get(candidate);
57
+ if (visibility !== undefined) return visibility;
58
+ if (placing.has(candidate)) return false;
59
+ placing.add(candidate);
60
+ const linked = candidate.linkedId === undefined ? undefined : candidates.get(candidate.linkedId);
61
+ const required = linked && linked.linkedId === candidate.id;
62
+ const group = required ? [candidate, linked] : [candidate];
63
+ if (linked && !required && !place(linked)) {
64
+ shown.set(candidate, false);
65
+ placing.delete(candidate);
66
+ return false;
67
+ }
68
+ const show = group.every(member => member.boxes.size > 0 &&
69
+ !(identities.get(member.identity ?? '') ?? []).some(previous => options.isDuplicate?.(member, previous)) &&
70
+ !(repeatGroups.get(member.repeatGroup ?? '') ?? []).some(previous => repeats(member, previous))) &&
71
+ group.every(member => member.collide === false || [...member.boxes].every(([view, box]) => {
72
+ const grid = grids.get(view);
73
+ if (!grid) return true;
74
+ const nearby = new Set<Candidate>();
75
+ for (const cell of getScreenBoundsCells(box, getViewport(view))) for (const previous of grid.get(cell) ?? []) nearby.add(previous);
76
+ return [...nearby].every(previous => previous === linked || !intersectsScreenBounds(box, previous.boxes.get(view)!));
77
+ }));
78
+ for (const member of group) {
79
+ shown.set(member, show);
80
+ if (!show) continue;
81
+ if (member.identity) {
82
+ const copies = identities.get(member.identity) ?? [];
83
+ copies.push(member);
84
+ identities.set(member.identity, copies);
85
+ }
86
+ if (member.repeatGroup && member.repeatDistance) {
87
+ const repeated = repeatGroups.get(member.repeatGroup) ?? [];
88
+ repeated.push(member);
89
+ repeatGroups.set(member.repeatGroup, repeated);
90
+ }
91
+ for (const [view, box] of member.boxes) {
92
+ let grid = grids.get(view);
93
+ if (!grid) grids.set(view, grid = new Map());
94
+ for (const cell of getScreenBoundsCells(box, getViewport(view))) {
95
+ let bucket = grid.get(cell);
96
+ if (!bucket) grid.set(cell, bucket = new Set());
97
+ bucket.add(member);
98
+ }
99
+ }
100
+ }
101
+ placing.delete(candidate);
102
+ return show;
103
+ }
104
+ ordered.forEach(place);
105
+ return shown;
106
+ }
107
+
108
+ /** Preserve authored spacing in any view shared by the candidate and prior label. */
109
+ function repeats(candidate: ScreenLabelCandidate, previous: ScreenLabelCandidate): boolean {
110
+ const repeatDistance = candidate.repeatDistance;
111
+ if (!repeatDistance || !candidate.repeatGroup || candidate.repeatGroup !== previous.repeatGroup) return false;
112
+ return [...candidate.boxes].some(([view, box]) => {
113
+ const other = previous.boxes.get(view);
114
+ return other !== undefined && Math.hypot((box[0] + box[2] - other[0] - other[2]) / 2,
115
+ (box[1] + box[3] - other[1] - other[3]) / 2) < repeatDistance;
116
+ });
117
+ }
@@ -0,0 +1,34 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ /** Axis-aligned CSS-pixel bounds: minimum X/Y followed by maximum X/Y. */
6
+ export type ScreenBounds = readonly [number, number, number, number];
7
+
8
+ /** Local viewport dimensions; bounds do not include a canvas/eye origin. */
9
+ export interface LabelViewport {
10
+ /** Local CSS-pixel width. */
11
+ readonly width: number;
12
+ /** Local CSS-pixel height. */
13
+ readonly height: number;
14
+ }
15
+
16
+ /** Strict rectangle overlap; touching padded edges do not collide. */
17
+ export function intersectsScreenBounds(left: ScreenBounds, right: ScreenBounds): boolean {
18
+ return left[0] < right[2] && left[2] > right[0] && left[1] < right[3] && left[3] > right[1];
19
+ }
20
+
21
+ /** Combine parts of one annotation without modifying either input. */
22
+ export function unionScreenBounds(left: ScreenBounds | undefined, right: ScreenBounds): ScreenBounds {
23
+ return left ? [Math.min(left[0], right[0]), Math.min(left[1], right[1]),
24
+ Math.max(left[2], right[2]), Math.max(left[3], right[3])] : right;
25
+ }
26
+
27
+ /** Enumerate 64-pixel spatial-index cells, clipped to the visible viewport. */
28
+ export function* getScreenBoundsCells(box: ScreenBounds, viewport: LabelViewport): Generator<string> {
29
+ for (let horizontal = Math.floor(Math.max(0, box[0]) / 64); horizontal <= Math.floor(Math.min(viewport.width, box[2]) / 64); horizontal++) {
30
+ for (let vertical = Math.floor(Math.max(0, box[1]) / 64); vertical <= Math.floor(Math.min(viewport.height, box[3]) / 64); vertical++) {
31
+ yield `${horizontal},${vertical}`;
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,107 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ /** Affine coefficients applied to byte-valued linear RGB, with output in meters. */
6
+ export interface ElevationDecoder {
7
+ /** Red channel multiplier. */
8
+ readonly rScaler: number;
9
+ /** Green channel multiplier. */
10
+ readonly gScaler: number;
11
+ /** Blue channel multiplier. */
12
+ readonly bScaler: number;
13
+ /** Meter offset after channel scaling. */
14
+ readonly offset: number;
15
+ }
16
+
17
+ /** Terrarium encoding used by Mapterhorn; compatible with loaders.gl terrain options. */
18
+ export const TERRARIUM_DECODER: ElevationDecoder = Object.freeze({rScaler: 256, gScaler: 1, bScaler: 1 / 256, offset: -32768});
19
+ /** Mapbox Terrain-RGB encoding, independent of the tile service. */
20
+ export const TERRAIN_RGB_DECODER: ElevationDecoder = Object.freeze({rScaler: 6553.6, gScaler: 25.6, bScaler: 0.1, offset: -10000});
21
+
22
+ /** Explicit raster georeferencing; row zero is north and column zero is west. */
23
+ export interface TerrainGridOptions {
24
+ /** Number of columns, at least two. */
25
+ width: number;
26
+ /** Number of rows, at least two. */
27
+ height: number;
28
+ /** Interleaved RGBA bytes, not sRGB-converted samples. */
29
+ pixels: Uint8Array | Uint8ClampedArray;
30
+ /** Decoder shared with loaders.gl. */
31
+ decoder: ElevationDecoder;
32
+ /** [west, south, east, north] in absolute EPSG:3857 meters. */
33
+ bounds: readonly [number, number, number, number];
34
+ /** Explicit vertical reference; do not infer a datum from the provider name. */
35
+ verticalDatum: 'ellipsoidal' | 'orthometric' | 'unknown';
36
+ /** Pixel-aligned validity mask; zero means no data. Alpha is not implicitly interpreted. */
37
+ valid?: Uint8Array;
38
+ /** Raster pixel centers, or samples including both grid edges. */
39
+ registration: 'pixel-center' | 'grid-node';
40
+ }
41
+
42
+ /** Detached decoded elevation data; meters are physical heights, never exaggerated. */
43
+ export interface TerrainHeightGrid {
44
+ /** Number of columns. */
45
+ readonly width: number;
46
+ /** Number of rows. */
47
+ readonly height: number;
48
+ /** North-to-south, west-to-east row-major elevation values in meters. */
49
+ readonly heights: Float32Array;
50
+ /** Detached validity mask; invalid heights are NaN. */
51
+ readonly valid: Uint8Array;
52
+ /** Absolute Mercator footprint in meters. */
53
+ readonly bounds: readonly [number, number, number, number];
54
+ /** Vertical reference, without implicit conversions. */
55
+ readonly verticalDatum: TerrainGridOptions['verticalDatum'];
56
+ /** Sampling convention of the decoded pixels. */
57
+ readonly registration: TerrainGridOptions['registration'];
58
+ }
59
+
60
+ /** Decode identical byte coefficients to loaders.gl; preserve no-data explicitly. */
61
+ export function decodeTerrainHeightGrid(options: TerrainGridOptions): TerrainHeightGrid {
62
+ const {width, height, pixels, decoder, bounds, verticalDatum, registration} = options;
63
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 2 || height < 2 || pixels.length !== width * height * 4) {
64
+ throw new Error('Terrain requires a complete RGBA grid of at least 2 by 2');
65
+ }
66
+ if (bounds.length !== 4 || !bounds.every(Number.isFinite) || bounds[0] >= bounds[2] || bounds[1] >= bounds[3]) throw new Error('Terrain bounds must have finite positive extent');
67
+ if (![decoder.rScaler, decoder.gScaler, decoder.bScaler, decoder.offset].every(Number.isFinite)) throw new Error('Terrain decoder must be finite');
68
+ if (!['ellipsoidal', 'orthometric', 'unknown'].includes(verticalDatum) || !['pixel-center', 'grid-node'].includes(registration)) throw new Error('Terrain datum and registration must be explicit');
69
+ if (options.valid && options.valid.length !== width * height) throw new Error('Terrain validity mask must match the pixels');
70
+ const heights = new Float32Array(width * height);
71
+ const valid = new Uint8Array(width * height);
72
+ for (let index = 0; index < heights.length; index++) {
73
+ valid[index] = options.valid ? Number(options.valid[index] !== 0) : 1;
74
+ const offset = index * 4;
75
+ heights[index] = valid[index] ? pixels[offset] * decoder.rScaler + pixels[offset + 1] * decoder.gScaler + pixels[offset + 2] * decoder.bScaler + decoder.offset : NaN;
76
+ if (valid[index] && !Number.isFinite(heights[index])) throw new Error('Terrain elevation exceeds the finite Float32 range');
77
+ }
78
+ return {width, height, heights, valid, bounds: [bounds[0], bounds[1], bounds[2], bounds[3]], verticalDatum, registration};
79
+ }
80
+
81
+ /** Bilinear height lookup; outside footprints and no-data neighborhoods return null. */
82
+ export function sampleTerrainHeight(grid: TerrainHeightGrid, position: readonly [number, number]): number | null {
83
+ const [west, south, east, north] = grid.bounds;
84
+ if (!position.every(Number.isFinite) || position[0] < west || position[0] > east || position[1] < south || position[1] > north) return null;
85
+ const pixelCenter = grid.registration === 'pixel-center';
86
+ const column = snapTerrainSample(Math.max(0, Math.min(grid.width - 1, (position[0] - west) / (east - west) * (pixelCenter ? grid.width : grid.width - 1) - (pixelCenter ? 0.5 : 0))), position[0], west, east, grid.width);
87
+ const row = snapTerrainSample(Math.max(0, Math.min(grid.height - 1, (north - position[1]) / (north - south) * (pixelCenter ? grid.height : grid.height - 1) - (pixelCenter ? 0.5 : 0))), position[1], south, north, grid.height);
88
+ const left = Math.floor(column), top = Math.floor(row);
89
+ const horizontal = column - left, vertical = row - top;
90
+ const samples = [top * grid.width + left, top * grid.width + Math.min(left + 1, grid.width - 1),
91
+ Math.min(top + 1, grid.height - 1) * grid.width + left, Math.min(top + 1, grid.height - 1) * grid.width + Math.min(left + 1, grid.width - 1)];
92
+ const weights = [(1 - horizontal) * (1 - vertical), horizontal * (1 - vertical), (1 - horizontal) * vertical, horizontal * vertical];
93
+ let height = 0;
94
+ for (let index = 0; index < samples.length; index++) {
95
+ if (weights[index] === 0) continue;
96
+ if (!grid.valid[samples[index]]) return null;
97
+ height += grid.heights[samples[index]] * weights[index];
98
+ }
99
+ return height;
100
+ }
101
+
102
+ /** Match shared-grid exact-sample semantics at world-scale bounds; do not broaden support beyond 1e-6 pixels. */
103
+ function snapTerrainSample(value: number, coordinate: number, minimum: number, maximum: number, size: number): number {
104
+ const nearest = Math.round(value);
105
+ const tolerance = Math.min(1e-6, 8 * Number.EPSILON * Math.max(1, Math.abs(coordinate), Math.abs(minimum), Math.abs(maximum)) / (maximum - minimum) * size);
106
+ return Math.abs(value - nearest) <= tolerance ? nearest : value;
107
+ }