@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,713 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {TerrainHeightField} from '../map-logic/terrain-height-field';
6
+ import type {TerrainHeightGrid} from '../map-logic/terrain-data';
7
+ import {PROJECTION_CONSTANTS} from '../scene/projection_math';
8
+ import {refineGlobeMesh} from '../gl/globe_mesh';
9
+ import VertexLayout from '../gl/vertex_layout';
10
+ import type {DrapedTerrainQueryMesh} from '../selection/draped-terrain-surface';
11
+ import {summarizeDrapedTerrainBounds} from '../map-logic/draped-terrain-bounds';
12
+ import {PACKED_HEIGHT_SCALE} from '../gl/vertex-constants';
13
+ import {createTerrainGroundTopology, wrapTerrainGroundGrids, type TerrainGroundTopology} from '../map-logic/terrain-ground-topology';
14
+ import {conformTerrainStyledMesh, type TerrainStyledTopology} from './terrain-styled-topology';
15
+ import {refineTerrainTransitionTopology, type TerrainTransitionTopology, type TerrainFaceReference} from '../map-logic/terrain-transition-topology';
16
+ import {createTerrainGlobePoint, interpolateTerrainGlobePoints} from './terrain-globe';
17
+
18
+ /** Opt-in physical-height basemap displacement, independent of deck, providers and GPU ownership. */
19
+ export interface TerrainDrapingOptions {
20
+ /** Non-overlapping loaded EPSG:3857 height grids; missing coverage hides geometry. */
21
+ grids: readonly TerrainHeightGrid[];
22
+ /** Previous admitted grids, sampled at the new vertices for temporal height morphing. */
23
+ previousGrids?: readonly TerrainHeightGrid[];
24
+ /** Horizontal edge target in Mercator meters; default 250, constrained by per-tile detail and vertex budgets. */
25
+ maxSegmentLength?: number;
26
+ /** Maximum additional vertices in one worker mesh; default 131072. */
27
+ maxAdditionalVertices?: number;
28
+ /** Completed interleaved vertex/index bytes per mesh; default 16 MiB. */
29
+ maxMeshBytes?: number;
30
+ /** Owned current/previous grid arrays per scene/worker, default 32 MiB. */
31
+ maxGridBytes?: number;
32
+ /** Shared height/slope interpolation duration per admitted terrain field, default 200 ms. */
33
+ transitionDuration?: number;
34
+ /** Ground style names whose exact final triangles are retained for physical queries; default empty. */
35
+ querySurfaceStyles?: readonly string[];
36
+ /** Opt-in DEM-owned ground cells per tile axis (2, 4, 8, 16, 32, 64), also conforming polygons/roads; zero keeps legacy refinement. */
37
+ groundMeshResolution?: number;
38
+ /** Previous admitted ground resolution; Renderer supplies it automatically for field replacement. */
39
+ previousGroundMeshResolution?: number;
40
+ /** Opt-in common planar refinement against previous canonical ground triangles; default false preserves arbitrary legacy previous fields. */
41
+ conformTransitions?: boolean;
42
+ /** Retained query channels per mesh, default 8 MiB; separate from completed draw bytes. */
43
+ maxQueryMeshBytes?: number;
44
+ /** Additional staged draw/query buffers across a replacement generation; default 64 MiB, excluding GPU overhead. */
45
+ maxStagedMeshBytes?: number;
46
+ /** Peak retained draw/query arrays plus estimated GPU vertex/index copies, including incumbent and staged generations; default 256 MiB. Excludes textures, driver overhead and worker scratch. */
47
+ maxResidentMeshBytes?: number;
48
+ /** Aggregate acceleration reservation for the current draped query snapshot; default 16 MiB. */
49
+ maxQueryIndexBytes?: number;
50
+ /** Retained projected picking geometry per snapshot; default 32 MiB, with exact streaming fallback. */
51
+ maxProjectedQueryBytes?: number;
52
+ /** Cooperative query-index warming target per host frame in milliseconds; default 2. */
53
+ queryPreparationBudgetMs?: number;
54
+ /** Test annotation fragments against rendered terrain and omit fully blocked collision candidates; default false. */
55
+ occludeAnnotations?: boolean;
56
+ }
57
+
58
+ /** Validated, detached physical input; typed-array contents are immutable by contract. */
59
+ export interface TerrainDrapingSnapshot extends Required<TerrainDrapingOptions> {
60
+ /** Renderer-owned field epoch; unrelated scene/source rebuilds preserve this identity. */
61
+ transitionGeneration: number;
62
+ /** Exact owned height/mask array bytes; excludes structured-clone and scratch allocations. */
63
+ byteLength: number;
64
+ /** Lowest valid current/previous height for safe spherical exaggeration. */
65
+ minimumHeight: number;
66
+ }
67
+
68
+ /** Completed worker preparation cost; does not claim a total GPU/transient memory budget. */
69
+ export interface TerrainMeshPreparation {
70
+ /** Original packed vertex count. */
71
+ inputVertices: number;
72
+ /** Prepared vertices, including retained originals. */
73
+ vertices: number;
74
+ /** Admitted triangle count. */
75
+ triangles: number;
76
+ /** Refined triangles conservatively removed for missing coverage. */
77
+ suppressedTriangles: number;
78
+ /** Completed interleaved vertex and index bytes. */
79
+ meshBytes: number;
80
+ /** Monotonic worker wall time for topology, sampling and mask admission. */
81
+ preparationMs: number;
82
+ /** Effective Mercator edge target; canonical ground reports its maximum admitted edge, not a certified error. */
83
+ edgeTarget: number;
84
+ /** The per-tile detail floor prevented the requested edge target. */
85
+ detailLimited: boolean;
86
+ /** Explicit source-independent ground resolution; absent on legacy polygons/ribbons/annotations. */
87
+ canonicalGroundResolution?: number;
88
+ /** Canonical cell resolution used to cut authored polygons or nominal expanded road faces. */
89
+ canonicalStyledResolution?: number;
90
+ /** Shared planar transition triangles after cutting against the previous generation's faces. */
91
+ transitionTriangles?: number;
92
+ /** View zoom whose built-in road width/offset was expanded and re-intersected in the worker. */
93
+ ribbonZoom?: number;
94
+ }
95
+
96
+ /** Resident preparation counters useful to compare detail policies on a real host. */
97
+ export interface TerrainDrapingStatistics {
98
+ /** Whether displacement attributes were enabled at construction. */
99
+ enabled: boolean;
100
+ /** Owned current/previous height-array bytes per scene/worker copy. */
101
+ gridBytes: number;
102
+ /** Resident completed draped meshes, including off-screen retained tiles. */
103
+ meshes: number;
104
+ /** Resident prepared vertex count. */
105
+ vertices: number;
106
+ /** Resident admitted triangles. */
107
+ triangles: number;
108
+ /** Resident rejected no-data triangles. */
109
+ suppressedTriangles: number;
110
+ /** Resident completed worker output bytes; excludes GPU copies and scratch. */
111
+ meshBytes: number;
112
+ /** Slowest resident worker mesh preparation. */
113
+ maximumPreparationMs: number;
114
+ /** Resident meshes constrained by the per-tile detail floor. */
115
+ detailLimitedMeshes: number;
116
+ /** Retained exact ground topology and height channels, excluding lazy projected query copies. */
117
+ queryBytes: number;
118
+ /** New draw/query buffers waiting for all worker batches to complete. */
119
+ stagedMeshBytes: number;
120
+ /** Draw/query arrays plus estimated GPU vertex/index copies in both admitted and staged generations. */
121
+ residentMeshBytes: number;
122
+ /** Peak mesh-domain admission limit; source, worker scratch and driver overhead remain separate. */
123
+ residentMeshByteLimit: number;
124
+ /** Snapshot-local annotation query costs; excludes separately retained host snapshots. */
125
+ annotationQueries?: import('../selection/draped-terrain-surface').DrapedTerrainQueryStatistics;
126
+ /** Resident canonical road meshes, whose corners are baked at their admitted worker view zoom. */
127
+ ribbonMeshes?: number;
128
+ /** Host-driven width replacement state; failed admission preserves the last complete geometry. */
129
+ ribbonRefinement?: {requestedZoom?: number; admittedZoom?: number; pending: boolean; error?: string};
130
+ }
131
+
132
+ /** Validate and snapshot terrain before changing a scene or rebuilding workers. */
133
+ export function normalizeTerrainDraping(options: TerrainDrapingOptions): TerrainDrapingSnapshot {
134
+ const maximum = options.maxGridBytes ?? 32 * 1024 * 1024;
135
+ const segment = options.maxSegmentLength ?? 250;
136
+ const vertices = options.maxAdditionalVertices ?? 131072;
137
+ const meshBytes = options.maxMeshBytes ?? 16 * 1024 * 1024;
138
+ const duration = options.transitionDuration ?? 200;
139
+ const querySurfaceStyles = options.querySurfaceStyles ?? [];
140
+ const groundMeshResolution = options.groundMeshResolution ?? 0;
141
+ const previousGroundMeshResolution = options.previousGroundMeshResolution ?? groundMeshResolution;
142
+ const conformTransitions = options.conformTransitions ?? false;
143
+ const maxQueryMeshBytes = options.maxQueryMeshBytes ?? 8 * 1024 * 1024;
144
+ const maxStagedMeshBytes = options.maxStagedMeshBytes ?? 64 * 1024 * 1024;
145
+ const maxResidentMeshBytes = options.maxResidentMeshBytes ?? 256 * 1024 * 1024;
146
+ const maxQueryIndexBytes = options.maxQueryIndexBytes ?? 16 * 1024 * 1024;
147
+ const maxProjectedQueryBytes = options.maxProjectedQueryBytes ?? 32 * 1024 * 1024;
148
+ const queryPreparationBudgetMs = options.queryPreparationBudgetMs ?? 2;
149
+ const occludeAnnotations = options.occludeAnnotations ?? false;
150
+ if (!Number.isSafeInteger(maximum) || maximum < 0 || !Number.isFinite(segment) || segment < 1 ||
151
+ !Number.isSafeInteger(vertices) || vertices < 0 || vertices > 262144 || !Number.isSafeInteger(meshBytes) || meshBytes < 0 ||
152
+ !Number.isFinite(duration) || duration < 0 || duration > 5000 ||
153
+ ![maxQueryMeshBytes, maxStagedMeshBytes, maxResidentMeshBytes, maxQueryIndexBytes, maxProjectedQueryBytes].every(value => Number.isSafeInteger(value) && value >= 0) ||
154
+ !Number.isFinite(queryPreparationBudgetMs) || queryPreparationBudgetMs < 0 || queryPreparationBudgetMs > 20 ||
155
+ typeof occludeAnnotations !== 'boolean' || typeof conformTransitions !== 'boolean' ||
156
+ !Number.isSafeInteger(groundMeshResolution) || groundMeshResolution !== 0 && (groundMeshResolution < 2 || groundMeshResolution > 64 || (groundMeshResolution & (groundMeshResolution - 1)) !== 0) ||
157
+ !Number.isSafeInteger(previousGroundMeshResolution) || previousGroundMeshResolution !== 0 && (previousGroundMeshResolution < 2 || previousGroundMeshResolution > 64 || (previousGroundMeshResolution & (previousGroundMeshResolution - 1)) !== 0) ||
158
+ querySurfaceStyles.some(name => typeof name !== 'string' || !name.length) || new Set(querySurfaceStyles).size !== querySurfaceStyles.length) throw new Error('Invalid terrain draping limits');
159
+ const previous = options.previousGrids ?? [];
160
+ let bytes = 0;
161
+ let minimumHeight = 0;
162
+ const datum = [...options.grids, ...previous][0]?.verticalDatum;
163
+ for (const grids of [options.grids, previous]) {
164
+ if (grids.length > 96) throw new Error('Terrain draping accepts at most 96 grids per generation');
165
+ for (const [index, grid] of grids.entries()) {
166
+ const [west, south, east, north] = grid.bounds;
167
+ if (!Number.isSafeInteger(grid.width) || !Number.isSafeInteger(grid.height) || grid.width < 2 || grid.height < 2 ||
168
+ grid.width * grid.height !== grid.heights.length || grid.valid.length !== grid.heights.length ||
169
+ grid.bounds.length !== 4 || !grid.bounds.every(Number.isFinite) || west >= east || south >= north ||
170
+ Math.max(Math.abs(south), Math.abs(north)) > Math.PI * PROJECTION_CONSTANTS.mercatorRadius ||
171
+ !['grid-node', 'pixel-center'].includes(grid.registration) ||
172
+ !['unknown', 'ellipsoidal', 'orthometric'].includes(grid.verticalDatum) || grid.verticalDatum !== datum ||
173
+ grid.valid.some((valid, position) => valid !== 0 && (valid !== 1 || !Number.isFinite(Math.fround(grid.heights[position])) || Math.abs(grid.heights[position]) > PROJECTION_CONSTANTS.earthRadius))) throw new Error('Invalid terrain draping height grid or datum');
174
+ for (const other of grids.slice(0, index)) if (Math.min(east, other.bounds[2]) > Math.max(west, other.bounds[0]) && Math.min(north, other.bounds[3]) > Math.max(south, other.bounds[1])) throw new Error('Terrain draping grids must not overlap');
175
+ for (let position = 0; position < grid.heights.length; position++) if (grid.valid[position]) minimumHeight = Math.min(minimumHeight, grid.heights[position]);
176
+ bytes += grid.heights.length * 5;
177
+ }
178
+ }
179
+ if (bytes > maximum) throw new Error('Terrain draping grid-byte budget exceeded');
180
+ if (minimumHeight <= -PROJECTION_CONSTANTS.earthRadius) throw new Error('Terrain height would invert the globe radius');
181
+ const copy = (grids: readonly TerrainHeightGrid[]): TerrainHeightGrid[] => grids.map(grid => ({...grid, bounds: [...grid.bounds], heights: Float32Array.from(grid.heights), valid: Uint8Array.from(grid.valid)}));
182
+ return {grids: copy(options.grids), previousGrids: copy(previous), maxSegmentLength: segment, maxAdditionalVertices: vertices,
183
+ maxGridBytes: maximum, maxMeshBytes: meshBytes, transitionDuration: duration, transitionGeneration: 0, byteLength: bytes, minimumHeight,
184
+ querySurfaceStyles: [...querySurfaceStyles], groundMeshResolution, previousGroundMeshResolution, conformTransitions, maxQueryMeshBytes, maxStagedMeshBytes, maxResidentMeshBytes, maxQueryIndexBytes, maxProjectedQueryBytes, queryPreparationBudgetMs, occludeAnnotations};
185
+ }
186
+
187
+ /**
188
+ * Retain the final draw's ground channels and topology, never another DEM mesh.
189
+ * @param canonicalPositions - Optional exact lattice XY, avoiding source-origin reconstruction ulps at terminal bounds.
190
+ */
191
+ export function extractDrapedTerrainQueryMesh(vertices: Uint8Array, indices: Uint16Array | Uint32Array, layout: VertexLayout,
192
+ origin: readonly [number, number], tileZoom: number, byteLimit: number, canonicalPositions?: Float64Array): DrapedTerrainQueryMesh {
193
+ const count = vertices.byteLength / layout.stride;
194
+ const data = new DataView(vertices.buffer, vertices.byteOffset, vertices.byteLength);
195
+ const hasGlobeBasis = count > 0 && layout.offset.a_terrain_globe_base !== undefined && data.getFloat32(layout.offset.a_terrain_globe_base + 12, true) > 0.5;
196
+ if (!Number.isInteger(count) || count * (hasGlobeBasis ? 80 : 32) + indices.byteLength > byteLimit) throw new Error('Terrain query mesh-byte budget exceeded');
197
+ if (canonicalPositions && (canonicalPositions.length !== count * 2 || !canonicalPositions.every(Number.isFinite))) throw new Error('Invalid canonical terrain query positions');
198
+ const positions = new Float64Array(count * 3), heights = new Float32Array(count * 2);
199
+ const globeBasis = hasGlobeBasis ? new Float32Array(count * 12) : undefined;
200
+ const scale = 2 * Math.PI * PROJECTION_CONSTANTS.mercatorRadius / (4096 * 2 ** tileZoom);
201
+ for (let vertex = 0; vertex < count; vertex++) {
202
+ const position = vertex * layout.stride + layout.offset.a_position;
203
+ positions.set([canonicalPositions?.[vertex * 2] ?? origin[0] + data.getFloat32(position, true) * scale,
204
+ canonicalPositions?.[vertex * 2 + 1] ?? origin[1] + data.getFloat32(position + 4, true) * scale,
205
+ data.getFloat32(position + 8, true) / PACKED_HEIGHT_SCALE], vertex * 3);
206
+ heights.set([data.getFloat32(vertex * layout.stride + layout.offset.a_terrain, true), data.getFloat32(vertex * layout.stride + layout.offset.a_terrain_previous, true)], vertex * 2);
207
+ if (globeBasis) for (const [group, name] of ['a_terrain_globe_base', 'a_terrain_globe_relief', 'a_terrain_globe_base_previous', 'a_terrain_globe_relief_previous'].entries()) {
208
+ for (let axis = 0; axis < 3; axis++) globeBasis[vertex * 12 + group * 3 + axis] = data.getFloat32(vertex * layout.stride + layout.offset[name] + axis * 4, true);
209
+ }
210
+ }
211
+ const bounds: [number, number, number, number] = [Infinity, Infinity, -Infinity, -Infinity];
212
+ for (const vertex of indices) {
213
+ bounds[0] = Math.min(bounds[0], positions[vertex * 3]); bounds[1] = Math.min(bounds[1], positions[vertex * 3 + 1]);
214
+ bounds[2] = Math.max(bounds[2], positions[vertex * 3]); bounds[3] = Math.max(bounds[3], positions[vertex * 3 + 1]);
215
+ }
216
+ const mesh = {positions, heights, indices: indices.slice(), bounds, ...(globeBasis ? {globeBasis} : {})};
217
+ return {...mesh, boundsSummary: summarizeDrapedTerrainBounds(mesh)};
218
+ }
219
+
220
+ const layouts = new WeakMap<VertexLayout, VertexLayout>();
221
+ /** One immutable current/previous sampling index per worker field, shared by every style mesh. */
222
+ const heightFields = new WeakMap<TerrainDrapingSnapshot, {current: TerrainHeightField; previous: TerrainHeightField}>();
223
+ /** One periodic current field and at most three absolute-world previous indexes; source arrays remain shared. */
224
+ const groundHeightFields = new WeakMap<TerrainDrapingSnapshot, {current: TerrainHeightField; previous: Map<number, TerrainHeightField>}>();
225
+
226
+ /** Reuse bounded masks and spatial lookup across styles; replaced field identities release the cache. */
227
+ function getHeightFields(terrain: TerrainDrapingSnapshot, ground = false, samplingWorldOffset = 0): {current: TerrainHeightField; previous: TerrainHeightField} {
228
+ if (ground) {
229
+ let fields = groundHeightFields.get(terrain);
230
+ if (!fields) {
231
+ fields = {current: new TerrainHeightField(wrapTerrainGroundGrids(terrain.grids)), previous: new Map()};
232
+ groundHeightFields.set(terrain, fields);
233
+ }
234
+ let previous = fields.previous.get(samplingWorldOffset);
235
+ if (!previous) {
236
+ // All previous samples share one absolute domain, retaining generic/
237
+ // quadtree boundary averaging. Previous fields need no mask prefixes.
238
+ previous = new TerrainHeightField(wrapTerrainGroundGrids(terrain.previousGrids, false, samplingWorldOffset), {maxMaskBytes: 0});
239
+ fields.previous.set(samplingWorldOffset, previous);
240
+ while (fields.previous.size > 3) fields.previous.delete(fields.previous.keys().next().value!);
241
+ }
242
+ return {current: fields.current, previous};
243
+ }
244
+ let fields = heightFields.get(terrain);
245
+ if (!fields) {
246
+ fields = {current: new TerrainHeightField(terrain.grids), previous: new TerrainHeightField(terrain.previousGrids)};
247
+ heightFields.set(terrain, fields);
248
+ }
249
+ return fields;
250
+ }
251
+ /** Admit source work only where current physical heights can produce drawable geometry. */
252
+ export function hasTerrainTileCoverage(grids: readonly TerrainHeightGrid[], coordinates: {x: number; y: number; z: number}): boolean {
253
+ const circumference = 2 * Math.PI * PROJECTION_CONSTANTS.mercatorRadius;
254
+ const edge = circumference / 2 ** coordinates.z;
255
+ const west = coordinates.x * edge - circumference / 2;
256
+ const north = circumference / 2 - coordinates.y * edge;
257
+ return grids.some(grid => west < grid.bounds[2] && west + edge > grid.bounds[0] && north > grid.bounds[1] && north - edge < grid.bounds[3]);
258
+ }
259
+ const baseLayouts = new WeakMap<VertexLayout, VertexLayout>();
260
+ /** Preserve packed identity channels, but refine terrain positions and ribbon corners without integer rounding. */
261
+ function getTerrainBaseLayout(original: VertexLayout): VertexLayout {
262
+ let layout = baseLayouts.get(original);
263
+ if (!layout) {
264
+ const ribbon = original.offset.a_extrude !== undefined;
265
+ layout = new VertexLayout(original.attribs.map(attribute => ({...attribute,
266
+ type: attribute.static == null && attribute.type === 5122 &&
267
+ (attribute.name === 'a_position' || ribbon && ['a_extrude', 'a_offset'].includes(attribute.name)) ? 5126 : attribute.type})));
268
+ baseLayouts.set(original, layout);
269
+ }
270
+ return layout;
271
+ }
272
+ /** Add terrain data only after the original packed mesh has been completed. */
273
+ export function getTerrainVertexLayout(original: VertexLayout): VertexLayout {
274
+ let layout = layouts.get(original);
275
+ if (!layout) {
276
+ const annotation = original.offset.a_shape !== undefined;
277
+ layout = new VertexLayout([...getTerrainBaseLayout(original).attribs.map(attribute => ({...attribute})),
278
+ ...['a_terrain_globe_base', 'a_terrain_globe_relief', 'a_terrain_globe_base_previous', 'a_terrain_globe_relief_previous'].map(name =>
279
+ ({name, size: 4, type: 0x1406, normalized: false, ...(annotation ? {static: [0, 0, 0, 0]} : {})})),
280
+ {name: 'a_terrain', size: 4, type: 0x1406, normalized: false},
281
+ {name: 'a_terrain_previous', size: 4, type: 0x1406, normalized: false}]);
282
+ if (layout.dynamic_attribs.length > 16) throw new Error('Terrain vertex layout exceeds the portable 16-attribute limit');
283
+ layouts.set(original, layout);
284
+ }
285
+ return layout;
286
+ }
287
+
288
+ /** Worker-side topology and samples; retains original feature/label vertex identities. */
289
+ export function drapeTerrainMesh(options: {
290
+ /** Completed original packed vertices. */
291
+ vertices: Uint8Array;
292
+ /** Triangle topology, or an implicit non-indexed triangle list. */
293
+ indices: Uint16Array | Uint32Array | false;
294
+ /** Original layout before appending height/slope data. */
295
+ layout: VertexLayout;
296
+ /** Absolute Mercator origin of tile-local coordinates. */
297
+ origin: readonly [number, number];
298
+ /** Source tile zoom and its packed XY edge size. */
299
+ tileZoom: number;
300
+ /** Styling zoom, independent of source detail; bounds possible screen-space ribbon expansion. */
301
+ styleZoom?: number;
302
+ /** Actual host view zoom for canonical road width/offset re-intersection; defaults to style zoom. */
303
+ ribbonZoom?: number;
304
+ /** Do not refine screen-space annotation quads. */
305
+ annotations?: boolean;
306
+ /** Explicit full-tile raster ground owner; source clipping, not triangle cropping, partitions cells. */
307
+ ground?: boolean;
308
+ /** Cut polygon/line style faces against the canonical surface when groundMeshResolution is enabled. */
309
+ styled?: boolean;
310
+ /** Validated immutable current/previous heights and resource policy. */
311
+ terrain: TerrainDrapingSnapshot;
312
+ }): {vertices: Uint8Array; indices: Uint16Array | Uint32Array; byteLength: number; statistics: TerrainMeshPreparation; groundClip?: boolean; groundPositions?: Float64Array} {
313
+ const started = performance.now();
314
+ const statistics: TerrainMeshPreparation = {inputVertices: 0, vertices: 0, triangles: 0, suppressedTriangles: 0, meshBytes: 0, preparationMs: 0, edgeTarget: 0, detailLimited: false};
315
+ const {terrain, layout, origin} = options;
316
+ const canonicalGround = Boolean(options.ground && terrain.groundMeshResolution);
317
+ const canonicalStyled = Boolean(options.styled && !options.annotations && terrain.groundMeshResolution);
318
+ if (canonicalGround && canonicalStyled) throw new Error('Terrain mesh cannot be both ground and styled');
319
+ if (!Number.isInteger(options.tileZoom) || options.tileZoom < 0 || options.tileZoom > 30 || !origin.every(Number.isFinite)) throw new Error('Invalid terrain draping tile');
320
+ const styleZoom = options.styleZoom ?? options.tileZoom;
321
+ if (!Number.isFinite(styleZoom) || styleZoom < 0 || styleZoom > 30) throw new Error('Invalid terrain draping style zoom');
322
+ const ribbonScale = 2 ** (options.tileZoom - styleZoom);
323
+ const metersPerUnit = 2 * Math.PI * PROJECTION_CONSTANTS.mercatorRadius / (4096 * 2 ** options.tileZoom);
324
+ const positionOffset = layout.offset.a_position;
325
+ const positionAttribute = layout.attribs.find(attribute => attribute.name === 'a_position');
326
+ const originalCount = options.vertices.byteLength / layout.stride;
327
+ statistics.inputVertices = originalCount;
328
+ if (positionOffset === undefined || positionAttribute?.type !== 5122 || positionAttribute.size < 2 || !Number.isInteger(originalCount)) throw new Error('Terrain draping requires aligned packed XY vertices');
329
+ const originalIndices = options.indices || Uint32Array.from({length: originalCount}, (_, index) => index);
330
+ if (originalIndices.length % 3 || originalIndices.some(index => index >= originalCount)) throw new Error('Terrain draping requires valid triangle indices');
331
+ if (!originalCount || !terrain.grids.length) return {vertices: new Uint8Array(), indices: new Uint16Array(), byteLength: 0, statistics};
332
+ const requestedCell = Math.min(terrain.maxSegmentLength, ...terrain.grids.flatMap(grid => [
333
+ (grid.bounds[2] - grid.bounds[0]) / (grid.registration === 'pixel-center' ? grid.width : grid.width - 1) / 2,
334
+ (grid.bounds[3] - grid.bounds[1]) / (grid.registration === 'pixel-center' ? grid.height : grid.height - 1) / 2]));
335
+ // Bound full-tile refinement independently of provider resolution. Floating
336
+ // positions retain skinny source triangles; this floor is a detail policy,
337
+ // not a packed-precision workaround or a certified terrain error bound.
338
+ const cell = canonicalGround || canonicalStyled ? requestedCell : Math.max(requestedCell, 64 * metersPerUnit);
339
+ statistics.edgeTarget = cell; statistics.detailLimited = cell > requestedCell;
340
+ const outputLayout = getTerrainVertexLayout(layout);
341
+ const baseLayout = getTerrainBaseLayout(layout);
342
+ const extrusionOffset = layout.offset.a_extrude;
343
+ const ribbonZoom = options.ribbonZoom ?? styleZoom;
344
+ if (!Number.isFinite(ribbonZoom) || ribbonZoom < 0 || ribbonZoom > 40) throw new Error('Invalid terrain ribbon view zoom');
345
+ let zoomDelta = Math.max(0, Math.min(4, ribbonZoom - styleZoom));
346
+ zoomDelta += (zoomDelta >= 1 ? 1 - zoomDelta : 0) + 2 * Math.max(0, Math.min(1, (zoomDelta - 2) / 2));
347
+ // Match the shader's width/offset interpolation at the admitted host zoom.
348
+ // Legacy noncanonical ribbons retain their style-zoom tangent correction.
349
+ const getNominalOffset = (vertex: DataView, axis: number): number => {
350
+ if (extrusionOffset === undefined) return 0;
351
+ const widthScale = positionAttribute.size > 2 ? vertex.getFloat32(baseLayout.offset.a_position + 8, true) / 1024 : 0;
352
+ const offsetScaleOffset = baseLayout.offset.a_z_and_offset_scale;
353
+ const offsetWidthScale = offsetScaleOffset === undefined ? 0 : vertex.getInt16(offsetScaleOffset + 2, true) / 1024;
354
+ const delta = canonicalStyled ? zoomDelta : 0;
355
+ const extrusion = vertex.getFloat32(baseLayout.offset.a_extrude + axis * 4, true) * (1 - widthScale * (delta * 2 - 1));
356
+ const direction = offsetWidthScale >= 0 ? 1 : -1;
357
+ const offset = baseLayout.offset.a_offset === undefined ? 0 : vertex.getFloat32(baseLayout.offset.a_offset + axis * 4, true) *
358
+ (1 - Math.abs(offsetWidthScale) * ((direction < 0 ? 1 : 0) + delta * direction));
359
+ return (extrusion + offset) * ribbonScale * 2 ** -delta;
360
+ };
361
+ // Ribbon topology is not its collapsed centerline. Intersect expanded corners;
362
+ // canonical output later bakes them and clears dynamic extrusion channels.
363
+ const readPosition = (vertex: DataView): [number, number] => [
364
+ vertex.getFloat32(baseLayout.offset.a_position, true) + getNominalOffset(vertex, 0),
365
+ vertex.getFloat32(baseLayout.offset.a_position + 4, true) + getNominalOffset(vertex, 1)
366
+ ];
367
+ const bounds = [Infinity, Infinity, -Infinity, -Infinity];
368
+ const source = new DataView(options.vertices.buffer, options.vertices.byteOffset, options.vertices.byteLength);
369
+ for (let index = 0; index < originalCount; index++) {
370
+ const offset = index * layout.stride;
371
+ const coordinate = [origin[0] + source.getInt16(offset + positionOffset, true) * metersPerUnit, origin[1] + source.getInt16(offset + positionOffset + 2, true) * metersPerUnit];
372
+ for (let axis = 0; axis < 2; axis++) {
373
+ const ribbonOffset = layout.offset.a_offset;
374
+ const extent = extrusionOffset === undefined ? 0 : 2 * ribbonScale * (Math.abs(source.getInt16(offset + extrusionOffset + axis * 2, true)) + (ribbonOffset === undefined ? 0 : Math.abs(source.getInt16(offset + ribbonOffset + axis * 2, true)))) * metersPerUnit;
375
+ bounds[axis] = Math.min(bounds[axis], coordinate[axis] - extent);
376
+ bounds[axis + 2] = Math.max(bounds[axis + 2], coordinate[axis] + extent);
377
+ }
378
+ }
379
+ if (!canonicalGround && !canonicalStyled && !terrain.grids.some(grid => bounds[0] <= grid.bounds[2] && bounds[2] >= grid.bounds[0] && bounds[1] <= grid.bounds[3] && bounds[3] >= grid.bounds[1])) {
380
+ statistics.suppressedTriangles = originalIndices.length / 3;
381
+ statistics.preparationMs = performance.now() - started;
382
+ return {vertices: new Uint8Array(), indices: new Uint16Array(), byteLength: 0, statistics};
383
+ }
384
+ if (options.vertices.byteLength / layout.stride * outputLayout.stride > terrain.maxMeshBytes) throw new Error('Terrain draping mesh-byte budget exceeded');
385
+ const floating = new Uint8Array(originalCount * baseLayout.stride);
386
+ const floatingView = new DataView(floating.buffer);
387
+ for (let index = 0; index < originalCount; index++) for (const attribute of layout.dynamic_attribs) {
388
+ const from = index * layout.stride + layout.offset[attribute.name];
389
+ const to = index * baseLayout.stride + baseLayout.offset[attribute.name];
390
+ const promoted = attribute.type !== baseLayout.dynamic_attribs.find(candidate => candidate.name === attribute.name)?.type;
391
+ if (promoted) for (let component = 0; component < attribute.size; component++) floatingView.setFloat32(to + component * 4, source.getInt16(from + component * 2, true), true);
392
+ else floating.set(options.vertices.subarray(from, from + attribute.byte_size!), to);
393
+ }
394
+ let groundTopology: TerrainGroundTopology | undefined;
395
+ let currentGroundTopology: TerrainGroundTopology | undefined, previousGroundTopology: TerrainGroundTopology | undefined;
396
+ let transitionTopology: TerrainTransitionTopology | null = null;
397
+ /** Keep source-independent endpoint planes when DEM footprints or dyadic resolution change. */
398
+ const prepareTransition = (topology: TerrainGroundTopology, topologyBounds: readonly [number, number, number, number]): TerrainGroundTopology => {
399
+ currentGroundTopology = topology;
400
+ if (!terrain.conformTransitions || !terrain.previousGrids.length || !terrain.previousGroundMeshResolution || !terrain.transitionDuration) return topology;
401
+ // Explicit canonical replacement requires the same validated quadtree contract at both endpoints.
402
+ previousGroundTopology = createStyledGroundTopology(terrain.previousGrids, topologyBounds, terrain.previousGroundMeshResolution,
403
+ Math.min(originalCount + terrain.maxAdditionalVertices, Math.floor(terrain.maxMeshBytes / outputLayout.stride)));
404
+ transitionTopology = refineTerrainTransitionTopology(topology, previousGroundTopology,
405
+ Math.min(originalCount + terrain.maxAdditionalVertices, Math.floor(terrain.maxMeshBytes / (outputLayout.stride + 12))));
406
+ if (transitionTopology) statistics.transitionTriangles = transitionTopology.topology.indices.length / 3;
407
+ return transitionTopology?.topology ?? topology;
408
+ };
409
+ if (canonicalGround) {
410
+ const corners = new Set<string>();
411
+ for (let index = 0; index < originalCount; index++) {
412
+ const point = new DataView(floating.buffer, index * baseLayout.stride, baseLayout.stride);
413
+ const x = point.getFloat32(baseLayout.offset.a_position, true), y = point.getFloat32(baseLayout.offset.a_position + 4, true);
414
+ if (![0, 4096].includes(x) || ![0, -4096].includes(y)) throw new Error('Canonical terrain ground requires a full tile raster quad');
415
+ corners.add(`${x},${y}`);
416
+ for (const attribute of baseLayout.dynamic_attribs) {
417
+ const offset = attribute.offset ?? 0, start = attribute.name === 'a_position' ? 8 : 0;
418
+ for (let byte = start; byte < (attribute.byte_size ?? 0); byte++) if (floating[index * baseLayout.stride + offset + byte] !== floating[offset + byte]) throw new Error('Canonical terrain ground requires uniform quad attributes');
419
+ }
420
+ }
421
+ if (corners.size !== 4 || originalIndices.length !== 6 || extrusionOffset !== undefined || options.annotations) throw new Error('Canonical terrain ground requires a full tile raster quad');
422
+ groundTopology = createTerrainGroundTopology(terrain.grids, [origin[0], origin[1] - 4096 * metersPerUnit, origin[0] + 4096 * metersPerUnit, origin[1]],
423
+ terrain.groundMeshResolution, Math.min(originalCount + terrain.maxAdditionalVertices, Math.floor(terrain.maxMeshBytes / outputLayout.stride)));
424
+ groundTopology = prepareTransition(groundTopology, [origin[0], origin[1] - 4096 * metersPerUnit, origin[0] + 4096 * metersPerUnit, origin[1]]);
425
+ validateCanonicalSourceLOD(terrain, options.tileZoom, [origin[0], origin[1] - 4096 * metersPerUnit, origin[0] + 4096 * metersPerUnit, origin[1]], groundTopology.sampleOffset);
426
+ statistics.canonicalGroundResolution = terrain.groundMeshResolution;
427
+ }
428
+ let styledTopology: TerrainStyledTopology | undefined;
429
+ if (canonicalStyled) {
430
+ for (let index = 0; index < originalCount; index++) {
431
+ const position = readPosition(new DataView(floating.buffer, index * baseLayout.stride, baseLayout.stride));
432
+ for (let axis = 0; axis < 2; axis++) {
433
+ const value = origin[axis] + position[axis] * metersPerUnit;
434
+ bounds[axis] = Math.min(bounds[axis], value); bounds[axis + 2] = Math.max(bounds[axis + 2], value);
435
+ }
436
+ }
437
+ const topologyBounds = [Math.min(bounds[0], origin[0]), Math.min(bounds[1], origin[1] - 4096 * metersPerUnit),
438
+ Math.max(bounds[2], origin[0] + 4096 * metersPerUnit), Math.max(bounds[3], origin[1])] as const;
439
+ groundTopology = createStyledGroundTopology(terrain.grids, topologyBounds, terrain.groundMeshResolution,
440
+ Math.min(originalCount + terrain.maxAdditionalVertices, Math.floor(terrain.maxMeshBytes / outputLayout.stride)));
441
+ groundTopology = prepareTransition(groundTopology, topologyBounds);
442
+ validateCanonicalSourceLOD(terrain, options.tileZoom, topologyBounds, groundTopology.sampleOffset);
443
+ const canonical = createCanonicalGroundVertices(groundTopology, floating, baseLayout, origin, options.tileZoom);
444
+ const canonicalView = new DataView(canonical.buffer), surfacePositions = new Float64Array(groundTopology.positions.length);
445
+ for (let index = 0; index < surfacePositions.length / 2; index++) for (let axis = 0; axis < 2; axis++) surfacePositions[index * 2 + axis] = canonicalView.getFloat32(index * baseLayout.stride + baseLayout.offset.a_position + axis * 4, true);
446
+ styledTopology = conformTerrainStyledMesh({vertices: floating, indices: originalIndices, layout: baseLayout,
447
+ surfacePositions, surfaceIndices: groundTopology.indices, getPosition: readPosition,
448
+ getElevation: record => extrusionOffset === undefined ? positionAttribute.size > 2 ? record.getFloat32(baseLayout.offset.a_position + 8, true) : 0 :
449
+ baseLayout.offset.a_z_and_offset_scale === undefined ? 0 : record.getInt16(baseLayout.offset.a_z_and_offset_scale, true),
450
+ maxVertices: Math.min(originalCount + terrain.maxAdditionalVertices, Math.floor(terrain.maxMeshBytes / (outputLayout.stride + 12)))});
451
+ statistics.canonicalStyledResolution = terrain.groundMeshResolution;
452
+ }
453
+ const refined = styledTopology ?? (groundTopology ? {vertices: createCanonicalGroundVertices(groundTopology, floating, baseLayout, origin, options.tileZoom), indices: groundTopology.indices} :
454
+ options.annotations ? {vertices: floating, indices: originalIndices} : refineGlobeMesh(floating, originalIndices, baseLayout,
455
+ {tileZoom: options.tileZoom, maxAngularSpan: Math.min(180, cell / PROJECTION_CONSTANTS.mercatorRadius * 180 / Math.PI), maxAdditionalVertices: terrain.maxAdditionalVertices, splitRemainderShortest: true, getPosition: readPosition}));
456
+ const count = refined.vertices.byteLength / baseLayout.stride;
457
+ const required = count * outputLayout.stride + refined.indices.byteLength;
458
+ if (required > terrain.maxMeshBytes) throw new Error('Terrain draping mesh-byte budget exceeded');
459
+ const vertices = new Uint8Array(count * outputLayout.stride);
460
+ const fields = getHeightFields(terrain, canonicalGround || canonicalStyled, groundTopology?.sampleOffset);
461
+ const canonicalSamples = new Map<number, ReturnType<typeof sampleTerrainChannels>>();
462
+ const canonicalCoverage = new Map<string, boolean>();
463
+ const endpointSamples = [new Map<number, ReturnType<typeof sampleTerrainChannels>>(), new Map<number, ReturnType<typeof sampleTerrainChannels>>()];
464
+ const endpointCoverage = [new Map<string, boolean>(), new Map<string, boolean>()];
465
+ /** Endpoint visibility follows the original triangle's conservative mask, not smaller replacement cuts. */
466
+ const endpointCovered = (topology: TerrainGroundTopology, reference: TerrainFaceReference, endpoint: 0 | 1): boolean => {
467
+ const key = reference.indices.join(',');
468
+ let covered = endpointCoverage[endpoint].get(key);
469
+ if (covered === undefined) {
470
+ const positions = endpoint === 0 ? topology.samplePositions : topology.positions;
471
+ covered = (endpoint === 0 ? fields.current : fields.previous).coversBounds([
472
+ Math.min(...reference.indices.map(index => positions[index * 2])), Math.min(...reference.indices.map(index => positions[index * 2 + 1])),
473
+ Math.max(...reference.indices.map(index => positions[index * 2])), Math.max(...reference.indices.map(index => positions[index * 2 + 1]))]);
474
+ endpointCoverage[endpoint].set(key, covered);
475
+ }
476
+ return covered;
477
+ };
478
+ /** Sample unchanged endpoint nodes once; interpolate their planes at newly introduced cuts. */
479
+ const interpolateEndpoint = (topology: TerrainGroundTopology, reference: TerrainFaceReference, endpoint: 0 | 1): ReturnType<typeof sampleTerrainChannels> => {
480
+ const samples = reference.indices.map(index => {
481
+ let sample = endpointSamples[endpoint].get(index);
482
+ if (!sample) {
483
+ sample = sampleTerrainChannels(fields, [topology.samplePositions[index * 2], topology.samplePositions[index * 2 + 1]],
484
+ [topology.positions[index * 2], topology.positions[index * 2 + 1]], cell);
485
+ sample.channels = sample.channels.map(Math.fround); endpointSamples[endpoint].set(index, sample);
486
+ }
487
+ return sample;
488
+ });
489
+ return {valid: samples.every(sample => sample.valid) && endpointCovered(topology, reference, endpoint),
490
+ channels: Array.from({length: 6}, (_value, channel) => samples.reduce((sum, sample, corner) => sum + sample.channels[channel] * reference.weights[corner], 0))};
491
+ };
492
+ const getCanonicalSample = (index: number) => {
493
+ let sample = canonicalSamples.get(index);
494
+ if (!sample) {
495
+ sample = transitionTopology ? interpolateEndpoint(currentGroundTopology!, transitionTopology.current[index], 0) : sampleTerrainChannels(fields, [groundTopology!.samplePositions[index * 2], groundTopology!.samplePositions[index * 2 + 1]],
496
+ [groundTopology!.positions[index * 2], groundTopology!.positions[index * 2 + 1]], cell);
497
+ if (transitionTopology) {
498
+ const reference = transitionTopology.previous[index];
499
+ const previous = reference && endpointCovered(previousGroundTopology!, reference, 1) ? interpolateEndpoint(previousGroundTopology!, reference, 1).channels.slice(3) : sample.channels.slice(0, 3);
500
+ sample.channels.splice(3, 3, ...previous);
501
+ }
502
+ sample.channels = sample.channels.map(Math.fround);
503
+ canonicalSamples.set(index, sample);
504
+ }
505
+ return sample;
506
+ };
507
+ const globeSamples = new Map<number, Float64Array>();
508
+ const endpointGlobeSamples = [new Map<number, Float64Array>(), new Map<number, Float64Array>()];
509
+ /** Each endpoint stays on its original globe triangle, including newly introduced transition cuts. */
510
+ const getCanonicalGlobe = (index: number): Float64Array => {
511
+ let result = globeSamples.get(index);
512
+ if (!result) {
513
+ const sample = getCanonicalSample(index);
514
+ const point: [number, number] = [groundTopology!.positions[index * 2], groundTopology!.positions[index * 2 + 1]];
515
+ const endpoint = (topology: TerrainGroundTopology, reference: TerrainFaceReference, channel: 0 | 3) =>
516
+ interpolateTerrainGlobePoints(reference.indices.map(node => {
517
+ const cache = endpointGlobeSamples[channel === 0 ? 0 : 1];
518
+ let point = cache.get(node);
519
+ if (!point) {
520
+ const value = endpointSamples[channel === 0 ? 0 : 1].get(node) ?? sampleTerrainChannels(fields,
521
+ [topology.samplePositions[node * 2], topology.samplePositions[node * 2 + 1]], [topology.positions[node * 2], topology.positions[node * 2 + 1]], cell);
522
+ point = createTerrainGlobePoint([topology.positions[node * 2], topology.positions[node * 2 + 1]], Math.fround(value.channels[channel]),
523
+ [topology.normalizedPositions[node * 2], topology.normalizedPositions[node * 2 + 1]]);
524
+ cache.set(node, point);
525
+ }
526
+ return point;
527
+ }), reference.weights);
528
+ const normalized: [number, number] = [groundTopology!.normalizedPositions[index * 2], groundTopology!.normalizedPositions[index * 2 + 1]];
529
+ const current = transitionTopology ? endpoint(currentGroundTopology!, transitionTopology.current[index], 0) : createTerrainGlobePoint(point, sample.channels[0], normalized);
530
+ const previousReference = transitionTopology?.previous[index];
531
+ const previous = transitionTopology ? previousReference && endpointCovered(previousGroundTopology!, previousReference, 1) ?
532
+ endpoint(previousGroundTopology!, previousReference, 3) : current : createTerrainGlobePoint(point, sample.channels[3], normalized);
533
+ result = Float64Array.from([...current, ...previous]); globeSamples.set(index, result);
534
+ }
535
+ return result;
536
+ };
537
+ const view = new DataView(vertices.buffer);
538
+ const valid = new Uint8Array(count);
539
+ const coordinates: [number, number][] = [];
540
+ const ribbonBounds: [number, number][][] = [];
541
+ for (let index = 0; index < count; index++) {
542
+ const input = new DataView(refined.vertices.buffer, refined.vertices.byteOffset + index * baseLayout.stride, baseLayout.stride);
543
+ vertices.set(refined.vertices.subarray(index * baseLayout.stride, (index + 1) * baseLayout.stride), index * outputLayout.stride);
544
+ const coordinate: [number, number] = groundTopology && !styledTopology ? [groundTopology.samplePositions[index * 2], groundTopology.samplePositions[index * 2 + 1]] :
545
+ [origin[0] + input.getFloat32(baseLayout.offset.a_position, true) * metersPerUnit, origin[1] + input.getFloat32(baseLayout.offset.a_position + 4, true) * metersPerUnit];
546
+ if (styledTopology) coordinate[0] -= groundTopology!.sampleOffset;
547
+ coordinates.push(coordinate);
548
+ const nominalOffset: [number, number] = [getNominalOffset(input, 0), getNominalOffset(input, 1)];
549
+ const sampleCoordinate: [number, number] = [coordinate[0] + nominalOffset[0] * metersPerUnit, coordinate[1] + nominalOffset[1] * metersPerUnit];
550
+ if (extrusionOffset !== undefined) {
551
+ const offset = baseLayout.offset.a_offset;
552
+ const extrude = baseLayout.offset.a_extrude;
553
+ const horizontal = 2 * ribbonScale * (Math.abs(input.getFloat32(extrude, true)) + (offset === undefined ? 0 : Math.abs(input.getFloat32(offset, true)))) * metersPerUnit;
554
+ const vertical = 2 * ribbonScale * (Math.abs(input.getFloat32(extrude + 4, true)) + (offset === undefined ? 0 : Math.abs(input.getFloat32(offset + 4, true)))) * metersPerUnit;
555
+ ribbonBounds.push(canonicalStyled ? [sampleCoordinate, sampleCoordinate] : [[coordinate[0] - horizontal, coordinate[1] - vertical], [coordinate[0] + horizontal, coordinate[1] + vertical]]);
556
+ }
557
+ const previousCoordinate: [number, number] = groundTopology ? [sampleCoordinate[0] + groundTopology.sampleOffset, sampleCoordinate[1]] : sampleCoordinate;
558
+ let samples: ReturnType<typeof sampleTerrainChannels>;
559
+ if (styledTopology) {
560
+ const nodes = Array.from(styledTopology.surfaceIndices.subarray(index * 3, index * 3 + 3));
561
+ const endpoints = nodes.map(getCanonicalSample), weights = styledTopology.surfaceWeights.subarray(index * 3, index * 3 + 3);
562
+ const key = nodes.join(',');
563
+ let covered = canonicalCoverage.get(key);
564
+ if (covered === undefined) {
565
+ covered = fields.current.coversBounds([Math.min(...nodes.map(node => groundTopology!.samplePositions[node * 2])), Math.min(...nodes.map(node => groundTopology!.samplePositions[node * 2 + 1])),
566
+ Math.max(...nodes.map(node => groundTopology!.samplePositions[node * 2])), Math.max(...nodes.map(node => groundTopology!.samplePositions[node * 2 + 1]))]);
567
+ canonicalCoverage.set(key, covered);
568
+ }
569
+ samples = {valid: covered && endpoints.every(endpoint => endpoint.valid), channels: Array.from({length: 6}, (_value, channel) =>
570
+ endpoints.reduce((sum, endpoint, corner) => sum + endpoint.channels[channel] * weights[corner], 0))};
571
+ }
572
+ else samples = groundTopology ? getCanonicalSample(index) : sampleTerrainChannels(fields, sampleCoordinate, previousCoordinate, cell);
573
+ valid[index] = Number(samples.valid);
574
+ if (groundTopology) {
575
+ const globe = styledTopology ? interpolateTerrainGlobePoints(
576
+ Array.from(styledTopology.surfaceIndices.subarray(index * 3, index * 3 + 3), getCanonicalGlobe), styledTopology.surfaceWeights.subarray(index * 3, index * 3 + 3)) : getCanonicalGlobe(index);
577
+ for (const [group, name] of ['a_terrain_globe_base', 'a_terrain_globe_relief', 'a_terrain_globe_base_previous', 'a_terrain_globe_relief_previous'].entries()) {
578
+ for (let axis = 0; axis < 4; axis++) view.setFloat32(index * outputLayout.stride + outputLayout.offset[name] + axis * 4,
579
+ axis < 3 ? globe[group * 3 + axis] : 1, true);
580
+ }
581
+ }
582
+ if (canonicalStyled && extrusionOffset !== undefined) {
583
+ // Baked corners remain on their admitted canonical face while a later width rebuild is pending.
584
+ // No shader extrusion may move these cuts across a different terrain triangle.
585
+ view.setFloat32(index * outputLayout.stride + outputLayout.offset.a_position, input.getFloat32(baseLayout.offset.a_position, true) + nominalOffset[0], true);
586
+ view.setFloat32(index * outputLayout.stride + outputLayout.offset.a_position + 4, input.getFloat32(baseLayout.offset.a_position + 4, true) + nominalOffset[1], true);
587
+ for (const name of ['a_extrude', 'a_offset']) if (outputLayout.offset[name] !== undefined) for (let axis = 0; axis < 2; axis++) view.setFloat32(index * outputLayout.stride + outputLayout.offset[name] + axis * 4, 0, true);
588
+ statistics.ribbonZoom = ribbonZoom;
589
+ }
590
+ for (const [channel, attribute] of [[0, 'a_terrain'], [3, 'a_terrain_previous']] as const) {
591
+ const offset = index * outputLayout.stride + outputLayout.offset[attribute];
592
+ // Ribbon triangles already passed validity admission. Their unused
593
+ // fourth channels carry the exact nominal corner (current X/previous Y),
594
+ // avoiding another attribute slot on devices limited to 16 attributes.
595
+ [...samples.channels.slice(channel, channel + 3), extrusionOffset === undefined ? Number(samples.valid) : canonicalStyled ? 0 : nominalOffset[attribute === 'a_terrain' ? 0 : 1]].forEach((value, component) => {
596
+ if (!Number.isFinite(Math.fround(value))) throw new Error('Terrain draping sample exceeds packed range');
597
+ view.setFloat32(offset + component * 4, value, true);
598
+ });
599
+ }
600
+ }
601
+ const indices: number[] = [];
602
+ const coverage = (positions: readonly (readonly [number, number])[]) => fields.current.coversBounds([
603
+ Math.min(...positions.map(position => position[0])), Math.min(...positions.map(position => position[1])),
604
+ Math.max(...positions.map(position => position[0])), Math.max(...positions.map(position => position[1]))]);
605
+ for (let index = 0; index < refined.indices.length; index += 3) {
606
+ const triangle = [refined.indices[index], refined.indices[index + 1], refined.indices[index + 2]];
607
+ if (triangle.every(vertex => valid[vertex]) && (options.annotations || coverage(extrusionOffset === undefined ? triangle.map(vertex => coordinates[vertex]) : triangle.flatMap(vertex => ribbonBounds[vertex])))) indices.push(...triangle);
608
+ }
609
+ const elements = count > 65535 ? Uint32Array.from(indices) : Uint16Array.from(indices);
610
+ const byteLength = vertices.byteLength + elements.byteLength;
611
+ if (groundTopology) {
612
+ let maximum = 0;
613
+ for (let index = 0; index < elements.length; index += 3) for (let edge = 0; edge < 3; edge++) {
614
+ const first = elements[index + edge], second = elements[index + (edge + 1) % 3];
615
+ if (styledTopology) {
616
+ const start = readPosition(new DataView(refined.vertices.buffer, first * baseLayout.stride, baseLayout.stride));
617
+ const end = readPosition(new DataView(refined.vertices.buffer, second * baseLayout.stride, baseLayout.stride));
618
+ maximum = Math.max(maximum, Math.hypot(start[0] - end[0], start[1] - end[1]) * metersPerUnit);
619
+ }
620
+ else maximum = Math.max(maximum, Math.hypot(groundTopology.positions[first * 2] - groundTopology.positions[second * 2], groundTopology.positions[first * 2 + 1] - groundTopology.positions[second * 2 + 1]));
621
+ }
622
+ statistics.edgeTarget = maximum; statistics.detailLimited = maximum > requestedCell;
623
+ }
624
+ Object.assign(statistics, {vertices: count, triangles: elements.length / 3, suppressedTriangles: (refined.indices.length - elements.length) / 3, meshBytes: byteLength, preparationMs: performance.now() - started});
625
+ return {vertices, indices: elements, byteLength, statistics, ...(groundTopology && !styledTopology ? {groundClip: true, groundPositions: groundTopology.positions} : {})};
626
+ }
627
+
628
+ /** Preserve world-tile buffers without broadening the public one-world topology contract.
629
+ * Split only at world/DEM boundaries; complete cells and shared lattice nodes remain unchanged.
630
+ * The existing periodic sampler owns three worlds, so larger envelopes reject explicitly.
631
+ */
632
+ function createStyledGroundTopology(grids: readonly TerrainHeightGrid[], bounds: readonly [number, number, number, number], resolution: number, maxVertices: number): TerrainGroundTopology {
633
+ const circumference = 2 * Math.PI * PROJECTION_CONSTANTS.mercatorRadius;
634
+ if (bounds[2] - bounds[0] <= circumference) return createTerrainGroundTopology(grids, bounds, resolution, maxVertices);
635
+ const firstWorld = Math.floor(bounds[0] / circumference + 0.5), lastWorld = Math.ceil(bounds[2] / circumference + 0.5) - 1;
636
+ if (!Number.isSafeInteger(firstWorld) || !Number.isSafeInteger(lastWorld) || lastWorld - firstWorld > 2) throw new Error('Canonical styled terrain exceeds the three-world sampling envelope');
637
+ const sampleOffset = Math.floor((bounds[0] + bounds[2]) / (2 * circumference) + 0.5) * circumference;
638
+ const positions: number[] = [], normalized: number[] = [], samples: number[] = [], indices: number[] = [], nodes = new Map<string, number>();
639
+ for (let world = firstWorld; world <= lastWorld; world++) {
640
+ const part = createTerrainGroundTopology(grids, [Math.max(bounds[0], (world - 0.5) * circumference), bounds[1],
641
+ Math.min(bounds[2], (world + 0.5) * circumference), bounds[3]], resolution, maxVertices);
642
+ const remapping = new Uint32Array(part.positions.length / 2);
643
+ for (let vertex = 0; vertex < remapping.length; vertex++) {
644
+ const x = part.normalizedPositions[vertex * 2], y = part.normalizedPositions[vertex * 2 + 1], key = `${x},${y}`;
645
+ let index = nodes.get(key);
646
+ if (index === undefined) {
647
+ if (nodes.size >= maxVertices) throw new Error('Canonical styled terrain vertex budget exceeded');
648
+ const sampleX = part.samplePositions[vertex * 2] + (Math.round(part.sampleOffset / circumference) - Math.round(sampleOffset / circumference)) * circumference;
649
+ if (Math.abs(sampleX) > 1.5 * circumference) throw new Error('Canonical styled terrain exceeds the three-world sampling envelope');
650
+ index = nodes.size; nodes.set(key, index);
651
+ positions.push(part.positions[vertex * 2], part.positions[vertex * 2 + 1]);
652
+ normalized.push(x, y); samples.push(sampleX, part.samplePositions[vertex * 2 + 1]);
653
+ }
654
+ remapping[vertex] = index;
655
+ }
656
+ for (const vertex of part.indices) indices.push(remapping[vertex]);
657
+ }
658
+ return {positions: Float64Array.from(positions), normalizedPositions: Float64Array.from(normalized), samplePositions: Float64Array.from(samples), indices: Uint32Array.from(indices), sampleOffset};
659
+ }
660
+
661
+ /** Apply the same explicit source/cell precision envelope to primary and repeated-world owners. */
662
+ function validateCanonicalSourceLOD(terrain: TerrainDrapingSnapshot, tileZoom: number, bounds: readonly number[], sampleOffset: number): void {
663
+ for (const grid of wrapTerrainGroundGrids(terrain.grids)) {
664
+ if (grid.bounds[2] <= bounds[0] - sampleOffset || grid.bounds[0] >= bounds[2] - sampleOffset || grid.bounds[3] <= bounds[1] || grid.bounds[1] >= bounds[3]) continue;
665
+ const cellZoom = Math.round(Math.log2(2 * Math.PI * PROJECTION_CONSTANTS.mercatorRadius / (grid.bounds[2] - grid.bounds[0]))) + Math.log2(terrain.groundMeshResolution);
666
+ const gap = tileZoom - cellZoom;
667
+ if (gap > 10 || gap < -18) throw new Error('Canonical terrain ground source/DEM LOD gap exceeds Float32 precision limits');
668
+ }
669
+ }
670
+
671
+ /** Sample one current/previous physical endpoint and its existing finite-difference normal channels. */
672
+ function sampleTerrainChannels(fields: {current: TerrainHeightField; previous: TerrainHeightField}, coordinate: readonly [number, number],
673
+ previousCoordinate: readonly [number, number], cell: number): {valid: boolean; channels: number[]} {
674
+ const current = fields.current.sampleHeight(coordinate), previousSample = fields.previous.sampleHeight(previousCoordinate);
675
+ const channels: number[] = [], latitudeCosine = 1 / Math.cosh(coordinate[1] / PROJECTION_CONSTANTS.mercatorRadius);
676
+ for (const [field, height, position] of [[fields.current, current, coordinate],
677
+ [previousSample === null ? fields.current : fields.previous, previousSample ?? current, previousSample === null ? coordinate : previousCoordinate]] as const) {
678
+ const slopes = [0, 1].map(axis => {
679
+ const negative: [number, number] = [...position], positive: [number, number] = [...position];
680
+ negative[axis] -= cell; positive[axis] += cell;
681
+ const before = field.sampleHeight(negative), after = field.sampleHeight(positive);
682
+ return height === null ? 0 : before !== null && after !== null ? (after - before) / (2 * cell * latitudeCosine) : after !== null ? (after - height) / (cell * latitudeCosine) : before !== null ? (height - before) / (cell * latitudeCosine) : 0;
683
+ });
684
+ channels.push(height ?? 0, ...slopes);
685
+ }
686
+ return {valid: current !== null, channels};
687
+ }
688
+
689
+ /** Copy constant raster style channels; UVs and ownership use the original source tile coordinate system. */
690
+ function createCanonicalGroundVertices(topology: TerrainGroundTopology, template: Uint8Array, layout: VertexLayout,
691
+ origin: readonly [number, number], tileZoom: number): Uint8Array {
692
+ const vertices = new Uint8Array(topology.positions.length / 2 * layout.stride), view = new DataView(vertices.buffer);
693
+ const circumference = 2 * Math.PI * PROJECTION_CONSTANTS.mercatorRadius, tiles = 2 ** tileZoom;
694
+ const column = Math.round((origin[0] / circumference + 0.5) * tiles), row = Math.round((0.5 - origin[1] / circumference) * tiles);
695
+ for (let index = 0; index < topology.positions.length / 2; index++) {
696
+ vertices.set(template.subarray(0, layout.stride), index * layout.stride);
697
+ const offset = index * layout.stride + layout.offset.a_position;
698
+ const pack = (value: number): number => {
699
+ const tolerance = Number.EPSILON * Math.max(4096, Math.abs(value), Math.abs(column * 4096), Math.abs(row * 4096)) * 16;
700
+ const rounded = Math.abs(value) <= tolerance ? 0 : Math.fround(value);
701
+ // Common-refinement barycentrics can introduce a few double ulps at
702
+ // otherwise exactly representable dyadic cuts. Do not admit a full
703
+ // Float32 ulp: genuinely unsupported source/DEM gaps still reject.
704
+ if (Math.abs(rounded - value) > tolerance) throw new Error(`Canonical terrain ground positions exceed Float32 precision limits (${value})`);
705
+ return rounded;
706
+ };
707
+ const x = pack((topology.normalizedPositions[index * 2] * tiles - column) * 4096);
708
+ const y = pack((row - topology.normalizedPositions[index * 2 + 1] * tiles) * 4096);
709
+ view.setFloat32(offset, x, true);
710
+ view.setFloat32(offset + 4, y, true);
711
+ }
712
+ return vertices;
713
+ }