@navaramap/three 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/LICENSE +8 -0
  2. package/dist/assets/atmosphere/README.md +1 -0
  3. package/dist/assets/atmosphere/higher_order_scattering.bin +0 -0
  4. package/dist/assets/atmosphere/higher_order_scattering.exr +0 -0
  5. package/dist/assets/atmosphere/irradiance.bin +0 -0
  6. package/dist/assets/atmosphere/irradiance.exr +0 -0
  7. package/dist/assets/atmosphere/scattering.bin +0 -0
  8. package/dist/assets/atmosphere/scattering.exr +0 -0
  9. package/dist/assets/atmosphere/single_mie_scattering.bin +0 -0
  10. package/dist/assets/atmosphere/single_mie_scattering.exr +0 -0
  11. package/dist/assets/atmosphere/stars.bin +0 -0
  12. package/dist/assets/atmosphere/transmittance.bin +0 -0
  13. package/dist/assets/atmosphere/transmittance.exr +0 -0
  14. package/dist/assets/cloud/README.md +1 -0
  15. package/dist/assets/cloud/local_weather.png +0 -0
  16. package/dist/assets/cloud/shape.bin +1 -0
  17. package/dist/assets/cloud/shape_detail.bin +1 -0
  18. package/dist/assets/cloud/turbulence.png +0 -0
  19. package/dist/assets/fontWorker-CEJEfRpb.js +2 -0
  20. package/dist/assets/fontWorker-CEJEfRpb.js.map +1 -0
  21. package/dist/assets/index-BUB-ijMO.js +12 -0
  22. package/dist/assets/index-BUB-ijMO.js.map +1 -0
  23. package/dist/assets/navara_wasm_font_worker_bg-665OLxjy.wasm +0 -0
  24. package/dist/assets/navara_wasm_worker_bg-BXhKC5bE.wasm +0 -0
  25. package/dist/assets/noise/README.md +1 -0
  26. package/dist/assets/noise/stbn.bin +0 -0
  27. package/dist/assets/water/README.md +3 -0
  28. package/dist/assets/water/waternormals.jpg +0 -0
  29. package/dist/index.d.ts +10114 -0
  30. package/dist/index.js +34481 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/index.umd.cjs +6345 -0
  33. package/dist/index.umd.cjs.map +1 -0
  34. package/package.json +95 -0
  35. package/src/Color.ts +111 -0
  36. package/src/ShadowMapViewers.ts +49 -0
  37. package/src/atmosphere.test.ts +76 -0
  38. package/src/atmosphere.ts +364 -0
  39. package/src/bufferView.ts +90 -0
  40. package/src/camera.ts +182 -0
  41. package/src/concurrency.ts +15 -0
  42. package/src/constants/assets.ts +30 -0
  43. package/src/constants/decoder.ts +7 -0
  44. package/src/constants/index.ts +2 -0
  45. package/src/core/BaseDesc.ts +142 -0
  46. package/src/core/BaseHandle.ts +126 -0
  47. package/src/core/DescRegistry.ts +88 -0
  48. package/src/core/EffectDesc.ts +343 -0
  49. package/src/core/EffectDescRegistry.ts +31 -0
  50. package/src/core/InstancedMeshDesc.ts +413 -0
  51. package/src/core/LightDesc.ts +216 -0
  52. package/src/core/LightDescRegistry.ts +32 -0
  53. package/src/core/MeshDesc.ts +457 -0
  54. package/src/core/MeshDescRegistry.ts +32 -0
  55. package/src/core/MeshDescWithSelectiveEffect.ts +129 -0
  56. package/src/core/Registries.ts +59 -0
  57. package/src/core/SelectiveEffectRegistry.ts +90 -0
  58. package/src/core/ViewContext.ts +235 -0
  59. package/src/core/errors.ts +16 -0
  60. package/src/core/index.ts +50 -0
  61. package/src/device.ts +414 -0
  62. package/src/effects/CustomEffectPass.ts +28 -0
  63. package/src/effects/copy.ts +10 -0
  64. package/src/effects/effect.ts +87 -0
  65. package/src/effects/index.ts +4 -0
  66. package/src/effects/render.ts +10 -0
  67. package/src/evaluations/FeatureEvaluator.ts +529 -0
  68. package/src/evaluations/index.ts +1 -0
  69. package/src/event/HillshadeContext.test.ts +232 -0
  70. package/src/event/HillshadeContext.ts +299 -0
  71. package/src/event/context.ts +279 -0
  72. package/src/event/feature.ts +357 -0
  73. package/src/event/featureEvent.ts +81 -0
  74. package/src/event/features/billboard.ts +30 -0
  75. package/src/event/features/model.ts +192 -0
  76. package/src/event/features/modelGpuBytes.test.ts +69 -0
  77. package/src/event/features/modelGpuBytes.ts +87 -0
  78. package/src/event/features/point.ts +25 -0
  79. package/src/event/features/polygon.ts +23 -0
  80. package/src/event/features/polygonOutline.ts +19 -0
  81. package/src/event/features/polyline.ts +19 -0
  82. package/src/event/features/text.ts +71 -0
  83. package/src/event/hillshade.test.ts +842 -0
  84. package/src/event/hillshade.ts +588 -0
  85. package/src/event/index.ts +700 -0
  86. package/src/event/loaders.ts +53 -0
  87. package/src/event/tile.ts +19 -0
  88. package/src/event/worker.ts +999 -0
  89. package/src/event/workerTaskSettlement.test.ts +243 -0
  90. package/src/event/workerTaskSettlement.ts +128 -0
  91. package/src/inMemoryBufferStore.test.ts +60 -0
  92. package/src/inMemoryBufferStore.ts +67 -0
  93. package/src/index.iife.ts +11 -0
  94. package/src/index.ts +2556 -0
  95. package/src/input.ts +144 -0
  96. package/src/layer.ts +183 -0
  97. package/src/layers/effect/FinalCopyEffectDesc.ts +29 -0
  98. package/src/layers/effect/MRTPassEffectDesc.ts +105 -0
  99. package/src/layers/effect/README.md +9 -0
  100. package/src/layers/effect/SelectiveEffectDesc.ts +113 -0
  101. package/src/layers/effect/SkyEnvMapEffectDesc.ts +58 -0
  102. package/src/layers/effect/TransparentPassEffectDesc.ts +62 -0
  103. package/src/layers/effect/index.ts +5 -0
  104. package/src/layers/index.ts +1 -0
  105. package/src/layersManager.ts +63 -0
  106. package/src/light.ts +15 -0
  107. package/src/loaders/AbortableImageLoader.ts +203 -0
  108. package/src/loaders/AbortableTextureLoader.ts +74 -0
  109. package/src/loaders/index.ts +5 -0
  110. package/src/material/enhancer/DESIGN.md +300 -0
  111. package/src/material/enhancer/MaterialEnhancer.ts +112 -0
  112. package/src/material/enhancer/ShaderReplacer.test.ts +114 -0
  113. package/src/material/enhancer/ShaderReplacer.ts +97 -0
  114. package/src/material/enhancer/batchTexture.ts +8 -0
  115. package/src/material/enhancer/index.ts +46 -0
  116. package/src/material/enhancer/instancedSprite/index.ts +7 -0
  117. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/index.test.ts +214 -0
  118. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/index.ts +134 -0
  119. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/markers.ts +21 -0
  120. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/material.ts +32 -0
  121. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.test.ts +277 -0
  122. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.ts +151 -0
  123. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/shader.test.ts +114 -0
  124. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/shader.ts +49 -0
  125. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/state.test.ts +82 -0
  126. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/state.ts +80 -0
  127. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/types.ts +146 -0
  128. package/src/material/enhancer/instancedSprite/instancedSpriteMaterialEnhancer.ts +105 -0
  129. package/src/material/enhancer/model/index.ts +25 -0
  130. package/src/material/enhancer/model/modelBaseEnhancer/index.test.ts +74 -0
  131. package/src/material/enhancer/model/modelBaseEnhancer/index.ts +110 -0
  132. package/src/material/enhancer/model/modelBaseEnhancer/markers.ts +27 -0
  133. package/src/material/enhancer/model/modelBaseEnhancer/material.test.ts +66 -0
  134. package/src/material/enhancer/model/modelBaseEnhancer/material.ts +44 -0
  135. package/src/material/enhancer/model/modelBaseEnhancer/mutates.test.ts +84 -0
  136. package/src/material/enhancer/model/modelBaseEnhancer/mutates.ts +47 -0
  137. package/src/material/enhancer/model/modelBaseEnhancer/shader.test.ts +24 -0
  138. package/src/material/enhancer/model/modelBaseEnhancer/shader.ts +162 -0
  139. package/src/material/enhancer/model/modelBaseEnhancer/state.test.ts +103 -0
  140. package/src/material/enhancer/model/modelBaseEnhancer/state.ts +52 -0
  141. package/src/material/enhancer/model/modelBaseEnhancer/types.ts +89 -0
  142. package/src/material/enhancer/model/modelMaterialEnhancer.ts +91 -0
  143. package/src/material/enhancer/model/modelWaterEnhancer/index.test.ts +71 -0
  144. package/src/material/enhancer/model/modelWaterEnhancer/index.ts +175 -0
  145. package/src/material/enhancer/model/modelWaterEnhancer/material.test.ts +48 -0
  146. package/src/material/enhancer/model/modelWaterEnhancer/material.ts +25 -0
  147. package/src/material/enhancer/model/modelWaterEnhancer/mutates.test.ts +89 -0
  148. package/src/material/enhancer/model/modelWaterEnhancer/mutates.ts +89 -0
  149. package/src/material/enhancer/model/modelWaterEnhancer/shader.test.ts +34 -0
  150. package/src/material/enhancer/model/modelWaterEnhancer/shader.ts +111 -0
  151. package/src/material/enhancer/model/modelWaterEnhancer/state.test.ts +105 -0
  152. package/src/material/enhancer/model/modelWaterEnhancer/state.ts +59 -0
  153. package/src/material/enhancer/model/modelWaterEnhancer/types.ts +141 -0
  154. package/src/material/enhancer/model/pntsEnhancer/index.test.ts +96 -0
  155. package/src/material/enhancer/model/pntsEnhancer/index.ts +82 -0
  156. package/src/material/enhancer/model/pntsEnhancer/markers.ts +6 -0
  157. package/src/material/enhancer/model/pntsEnhancer/material.test.ts +37 -0
  158. package/src/material/enhancer/model/pntsEnhancer/material.ts +21 -0
  159. package/src/material/enhancer/model/pntsEnhancer/mutates.test.ts +80 -0
  160. package/src/material/enhancer/model/pntsEnhancer/mutates.ts +29 -0
  161. package/src/material/enhancer/model/pntsEnhancer/shader.test.ts +32 -0
  162. package/src/material/enhancer/model/pntsEnhancer/shader.ts +56 -0
  163. package/src/material/enhancer/model/pntsEnhancer/state.test.ts +87 -0
  164. package/src/material/enhancer/model/pntsEnhancer/state.ts +29 -0
  165. package/src/material/enhancer/model/pntsEnhancer/types.ts +38 -0
  166. package/src/material/enhancer/polygon/index.ts +22 -0
  167. package/src/material/enhancer/polygon/polygonBaseEnhancer/index.test.ts +101 -0
  168. package/src/material/enhancer/polygon/polygonBaseEnhancer/index.ts +130 -0
  169. package/src/material/enhancer/polygon/polygonBaseEnhancer/markers.ts +23 -0
  170. package/src/material/enhancer/polygon/polygonBaseEnhancer/material.test.ts +19 -0
  171. package/src/material/enhancer/polygon/polygonBaseEnhancer/material.ts +38 -0
  172. package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.test.ts +75 -0
  173. package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +122 -0
  174. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.test.ts +25 -0
  175. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +249 -0
  176. package/src/material/enhancer/polygon/polygonBaseEnhancer/state.test.ts +102 -0
  177. package/src/material/enhancer/polygon/polygonBaseEnhancer/state.ts +92 -0
  178. package/src/material/enhancer/polygon/polygonBaseEnhancer/types.ts +150 -0
  179. package/src/material/enhancer/polygon/polygonMaterialEnhancer.ts +97 -0
  180. package/src/material/enhancer/polygon/polygonWaterEnhancer/index.test.ts +72 -0
  181. package/src/material/enhancer/polygon/polygonWaterEnhancer/index.ts +164 -0
  182. package/src/material/enhancer/polygon/polygonWaterEnhancer/material.test.ts +48 -0
  183. package/src/material/enhancer/polygon/polygonWaterEnhancer/material.ts +35 -0
  184. package/src/material/enhancer/polygon/polygonWaterEnhancer/mutates.ts +61 -0
  185. package/src/material/enhancer/polygon/polygonWaterEnhancer/shader.test.ts +34 -0
  186. package/src/material/enhancer/polygon/polygonWaterEnhancer/shader.ts +96 -0
  187. package/src/material/enhancer/polygon/polygonWaterEnhancer/state.test.ts +98 -0
  188. package/src/material/enhancer/polygon/polygonWaterEnhancer/state.ts +57 -0
  189. package/src/material/enhancer/polygon/polygonWaterEnhancer/types.ts +115 -0
  190. package/src/material/enhancer/polyline/index.ts +7 -0
  191. package/src/material/enhancer/polyline/polylineBaseEnhancer/index.test.ts +157 -0
  192. package/src/material/enhancer/polyline/polylineBaseEnhancer/index.ts +154 -0
  193. package/src/material/enhancer/polyline/polylineBaseEnhancer/markers.ts +19 -0
  194. package/src/material/enhancer/polyline/polylineBaseEnhancer/material.test.ts +42 -0
  195. package/src/material/enhancer/polyline/polylineBaseEnhancer/material.ts +38 -0
  196. package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.test.ts +120 -0
  197. package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.ts +169 -0
  198. package/src/material/enhancer/polyline/polylineBaseEnhancer/shader.test.ts +67 -0
  199. package/src/material/enhancer/polyline/polylineBaseEnhancer/shader.ts +75 -0
  200. package/src/material/enhancer/polyline/polylineBaseEnhancer/state.test.ts +55 -0
  201. package/src/material/enhancer/polyline/polylineBaseEnhancer/state.ts +91 -0
  202. package/src/material/enhancer/polyline/polylineBaseEnhancer/types.ts +170 -0
  203. package/src/material/enhancer/polyline/polylineMaterialEnhancer.ts +102 -0
  204. package/src/material/enhancer/sdfText/index.ts +7 -0
  205. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/index.ts +125 -0
  206. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/markers.ts +19 -0
  207. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/material.test.ts +35 -0
  208. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/material.ts +32 -0
  209. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.test.ts +381 -0
  210. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +232 -0
  211. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.ts +36 -0
  212. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/state.test.ts +118 -0
  213. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/state.ts +107 -0
  214. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +184 -0
  215. package/src/material/enhancer/sdfText/sdfTextMaterialEnhancer.ts +100 -0
  216. package/src/material/enhancer/shadowMap/index.ts +7 -0
  217. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/index.ts +97 -0
  218. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/markers.ts +10 -0
  219. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/material.ts +31 -0
  220. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/mutates.ts +8 -0
  221. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/shader.test.ts +17 -0
  222. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/shader.ts +53 -0
  223. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/state.ts +17 -0
  224. package/src/material/enhancer/shadowMap/shadowMapDepthEnhancer/types.ts +26 -0
  225. package/src/material/enhancer/test-utils/shaderCompatibility.ts +124 -0
  226. package/src/material/enhancer/tileComposite/compose.test.ts +95 -0
  227. package/src/material/enhancer/tileComposite/compose.ts +112 -0
  228. package/src/material/enhancer/tileComposite/index.ts +23 -0
  229. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/index.ts +11 -0
  230. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +132 -0
  231. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/shader.ts +245 -0
  232. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/types.ts +43 -0
  233. package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/index.ts +29 -0
  234. package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/mutates.ts +68 -0
  235. package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +24 -0
  236. package/src/material/enhancer/tileComposite/tileHillshadeEnhancer/index.ts +29 -0
  237. package/src/material/enhancer/tileComposite/tileHillshadeEnhancer/mutates.ts +34 -0
  238. package/src/material/enhancer/tileComposite/tileHillshadeEnhancer/shader.ts +36 -0
  239. package/src/material/enhancer/tileComposite/tileWaterEnhancer/index.ts +23 -0
  240. package/src/material/enhancer/tileComposite/tileWaterEnhancer/mutates.ts +23 -0
  241. package/src/material/enhancer/tileComposite/tileWaterEnhancer/shader.ts +8 -0
  242. package/src/material/enhancer/tileComposite/tileWatermaskEnhancer/index.ts +23 -0
  243. package/src/material/enhancer/tileComposite/tileWatermaskEnhancer/mutates.ts +34 -0
  244. package/src/material/enhancer/tileComposite/tileWatermaskEnhancer/shader.ts +13 -0
  245. package/src/material/enhancer/tileComposite/types.ts +58 -0
  246. package/src/material/index.ts +3 -0
  247. package/src/material/macro/index.ts +1 -0
  248. package/src/material/macro/tileShader.test.ts +136 -0
  249. package/src/material/macro/tileShader.ts +287 -0
  250. package/src/material/overrideMaterialsForMRT.ts +439 -0
  251. package/src/material/selectiveEffectMaterialSetup.test.ts +52 -0
  252. package/src/material/selectiveEffectMaterialSetup.ts +53 -0
  253. package/src/material/types/index.ts +1 -0
  254. package/src/material/types/uniform.ts +1 -0
  255. package/src/mesh/DrapedMesh.ts +101 -0
  256. package/src/mesh/batchTexture.test.ts +646 -0
  257. package/src/mesh/batchTexture.ts +437 -0
  258. package/src/mesh/batchedFeature.ts +186 -0
  259. package/src/mesh/batchedSdfText.ts +484 -0
  260. package/src/mesh/featureMesh.ts +43 -0
  261. package/src/mesh/index.ts +16 -0
  262. package/src/mesh/instanced.ts +117 -0
  263. package/src/mesh/model.ts +483 -0
  264. package/src/mesh/pickableMesh.ts +55 -0
  265. package/src/mesh/pickableMeshWrapper.ts +529 -0
  266. package/src/mesh/polygon.ts +699 -0
  267. package/src/mesh/polygonOutline.ts +393 -0
  268. package/src/mesh/polyline.ts +523 -0
  269. package/src/mesh/releaseGeometryArrays.ts +57 -0
  270. package/src/mesh/rtcRteHelper.ts +211 -0
  271. package/src/mesh/sdfText.test.ts +200 -0
  272. package/src/mesh/sdfText.ts +1019 -0
  273. package/src/mesh/sprite/billboardAtlas.test.ts +177 -0
  274. package/src/mesh/sprite/billboardAtlas.ts +210 -0
  275. package/src/mesh/sprite/billboardAtlasImageLoader.ts +29 -0
  276. package/src/mesh/sprite/index.ts +1 -0
  277. package/src/mesh/sprite/instancedSprite.ts +694 -0
  278. package/src/mesh/tile.test.ts +393 -0
  279. package/src/mesh/tile.ts +1790 -0
  280. package/src/object3DEvent.ts +6 -0
  281. package/src/orchestrators/RenderPassOrchestrator.ts +147 -0
  282. package/src/orchestrators/index.ts +1 -0
  283. package/src/passes/AllDepthCopyPass.ts +165 -0
  284. package/src/passes/CustomRenderPass.ts +279 -0
  285. package/src/passes/NormalCopyPass.ts +125 -0
  286. package/src/passes/RenderTargetCopyPass.ts +91 -0
  287. package/src/passes/SkyEnvMapPass.ts +58 -0
  288. package/src/passes/index.ts +5 -0
  289. package/src/pick/pickHelper.ts +273 -0
  290. package/src/pick/pickTerrain.ts +254 -0
  291. package/src/plugins/AttributionPlugin/AttributionPlugin.test.ts +210 -0
  292. package/src/plugins/AttributionPlugin/attribution.test.ts +225 -0
  293. package/src/plugins/AttributionPlugin/attribution.ts +355 -0
  294. package/src/plugins/AttributionPlugin/attributionStyles.ts +163 -0
  295. package/src/plugins/AttributionPlugin/index.ts +702 -0
  296. package/src/plugins/index.ts +1 -0
  297. package/src/quality.ts +1 -0
  298. package/src/renderOrder.ts +3 -0
  299. package/src/scene.test.ts +164 -0
  300. package/src/scene.ts +127 -0
  301. package/src/shaders/index.ts +1 -0
  302. package/src/solar.test.ts +81 -0
  303. package/src/solar.ts +101 -0
  304. package/src/source.ts +80 -0
  305. package/src/stats.ts +102 -0
  306. package/src/tasks/computeVertexNormalsAsync.ts +15 -0
  307. package/src/tasks/constructPolygonBatchedFeature.ts +34 -0
  308. package/src/tasks/constructPolylineBatchedFeature.ts +27 -0
  309. package/src/tasks/constructQuantizedMeshTerrainMesh.ts +24 -0
  310. package/src/tasks/constructTerrainMesh.ts +25 -0
  311. package/src/tasks/getImageDataFromBlob.ts +13 -0
  312. package/src/tasks/getImageDataFromImageBitmap.ts +12 -0
  313. package/src/tasks/parseMvtTile.ts +22 -0
  314. package/src/tasks/queueTask.ts +19 -0
  315. package/src/tasks/toCreasedNormalsAsync.ts +18 -0
  316. package/src/tasks/upsampleQuantizedMeshTerrainMesh.ts +44 -0
  317. package/src/tasks/upsampleTerrainMesh.ts +39 -0
  318. package/src/texture.ts +19 -0
  319. package/src/textures.ts +13 -0
  320. package/src/tileTexture/CompositeShader.snapshot.test.ts +59 -0
  321. package/src/tileTexture/CompositeShader.test.ts +168 -0
  322. package/src/tileTexture/CompositeShader.ts +53 -0
  323. package/src/tileTexture/SlotPlanner.test.ts +105 -0
  324. package/src/tileTexture/SlotPlanner.ts +102 -0
  325. package/src/tileTexture/TileTextureCache.test.ts +227 -0
  326. package/src/tileTexture/TileTextureCache.ts +192 -0
  327. package/src/tileTexture/TileTextureCompositor.test.ts +260 -0
  328. package/src/tileTexture/TileTextureCompositor.ts +421 -0
  329. package/src/tileTexture/__snapshots__/CompositeShader.snapshot.test.ts.snap +2649 -0
  330. package/src/tileTexture/index.ts +8 -0
  331. package/src/tileTexture/types.ts +150 -0
  332. package/src/type/index.ts +261 -0
  333. package/src/uniforms.ts +20 -0
  334. package/src/utils/arraysEqual.ts +9 -0
  335. package/src/utils/blendModes.ts +80 -0
  336. package/src/utils/geometry.ts +53 -0
  337. package/src/utils/hillshadeNormalMapGenerator.ts +193 -0
  338. package/src/utils/index.ts +8 -0
  339. package/src/utils/object.test.ts +14 -0
  340. package/src/utils/object.ts +7 -0
  341. package/src/utils/replacer.test.ts +42 -0
  342. package/src/utils/replacer.ts +89 -0
  343. package/src/utils/screen.ts +29 -0
  344. package/src/utils/textureFragmentIndex.test.ts +224 -0
  345. package/src/utils/textureFragmentIndex.ts +97 -0
  346. package/src/utils/worker.ts +3 -0
  347. package/src/vite-env.d.ts +2 -0
  348. package/src/worker/index.ts +13 -0
  349. package/src/worker/tasks/computeVertexNormals.ts +15 -0
  350. package/src/worker/tasks/toCreasedNormals.ts +22 -0
package/src/index.ts ADDED
@@ -0,0 +1,2556 @@
1
+ import { EventManager, EventHandler, Globe, Plugin } from "@navaramap/core";
2
+ import type {
3
+ CameraPosition,
4
+ ColorMap,
5
+ GlobeOptions,
6
+ Nullable,
7
+ XYZ,
8
+ Color as CoreColor,
9
+ LatLngHeight,
10
+ LatLng,
11
+ } from "@navaramap/core";
12
+ import initCore, {
13
+ Core,
14
+ CameraDirection,
15
+ DynamicSse as EngineDynamicSse,
16
+ LLE,
17
+ type TerrainHeightUpdatedEvent,
18
+ type TextureFragmentStatus,
19
+ } from "@navaramap/engine";
20
+ import {
21
+ FontManager,
22
+ type FontFamily,
23
+ type FontWorkerMemoryStats,
24
+ } from "@navaramap/font";
25
+ import FontWorkerURL from "@navaramap/font/fontWorker?worker&url";
26
+ import { initNavaraApi } from "@navaramap/three_api";
27
+ import {
28
+ initializeWorkerPool,
29
+ probeWorkerPoolHeap,
30
+ terminateWorkerPool,
31
+ warmUpWorkerPool,
32
+ workerPoolHeapStats,
33
+ type WorkerPoolHeapStats,
34
+ } from "@navaramap/worker";
35
+ import {
36
+ Scene,
37
+ WebGLRenderer,
38
+ Vector3,
39
+ Texture,
40
+ Vector2,
41
+ LinearFilter,
42
+ ClampToEdgeWrapping,
43
+ RepeatWrapping,
44
+ Group,
45
+ PCFShadowMap,
46
+ } from "three";
47
+ import invariant from "tiny-invariant";
48
+
49
+ import { Atmosphere, type AtmosphereOptions } from "./atmosphere";
50
+ import { ThreeViewCamera } from "./camera";
51
+ import { Color } from "./Color";
52
+ import { createDefaultConcurrencyManager } from "./concurrency";
53
+ import { WATER_NORMAL_URL } from "./constants/assets";
54
+ import {
55
+ LightDesc,
56
+ EffectDesc,
57
+ ViewContext,
58
+ type MeshConfig,
59
+ type LightConfig,
60
+ type EffectConfig,
61
+ type MeshDescConstructor,
62
+ type LightDescConstructor,
63
+ type EffectDescConstructor,
64
+ UnknownTypeError,
65
+ } from "./core";
66
+ import {
67
+ MeshHandle,
68
+ LightHandle,
69
+ EffectHandle,
70
+ type AnyMeshDesc,
71
+ } from "./core/BaseHandle";
72
+ import { Registries } from "./core/Registries";
73
+ import {
74
+ getCompositeAtlasSize,
75
+ getDefaultDynamicSse,
76
+ getDefaultLodFog,
77
+ getDefaultMemoryBudgets,
78
+ getDefaultMemoryCostHints,
79
+ getDevicePixelRatio,
80
+ isMobileDevice,
81
+ type DynamicSseSettings,
82
+ type LodFogSettings,
83
+ } from "./device";
84
+ import {
85
+ processEvent,
86
+ EventContext,
87
+ HillshadeContext,
88
+ type BufferLoader,
89
+ type FeatureHandler,
90
+ type GlobeHandler,
91
+ type LayerHandler,
92
+ type MeshHandler,
93
+ type TextureFragmentHandler,
94
+ type TileHandler,
95
+ type WorkerTaskHandler,
96
+ } from "./event";
97
+ import { TEXTURE_LOADER } from "./event/loaders";
98
+ import { InMemoryBufferStore } from "./inMemoryBufferStore";
99
+ import { registerInputEvents } from "./input";
100
+ import { Layer, type LayerEvent } from "./layer";
101
+ import {
102
+ MRTPassEffectDesc,
103
+ SkyEnvMapEffectDesc,
104
+ TransparentPassEffectDesc,
105
+ } from "./layers/effect";
106
+ import { FinalCopyEffectDesc } from "./layers/effect/FinalCopyEffectDesc";
107
+ import { LayersManager } from "./layersManager";
108
+ import { disposeTexture } from "./loaders";
109
+ import { overrideMaterialsForMRT } from "./material";
110
+ import type { TileMesh } from "./mesh/tile";
111
+ import { RenderPassOrchestrator } from "./orchestrators/RenderPassOrchestrator";
112
+ import { PickHelper } from "./pick/pickHelper";
113
+ import { TerrainPicker } from "./pick/pickTerrain";
114
+ import { AttributionPlugin, type AttributionPluginOptions } from "./plugins";
115
+ import { TexturizedSceneByTileCoordinates, type Scenes } from "./scene";
116
+ import { ShadowMapViewers } from "./ShadowMapViewers";
117
+ import { Source } from "./source";
118
+ import { RendererStats } from "./stats";
119
+ import type { TextureOptions } from "./textures";
120
+ import { TileTextureCompositor } from "./tileTexture";
121
+ import {
122
+ type AbortControllers,
123
+ type LayerDescription,
124
+ type SourceDescription,
125
+ type BuiltInEffectDescription,
126
+ type Descriptions,
127
+ type EmptyDescriptions,
128
+ type OmitType,
129
+ type MeshCache,
130
+ type PickedFeature,
131
+ type WorkerPoolPromises,
132
+ type RenderFlag,
133
+ type TileMapByHandle,
134
+ } from "./type";
135
+ import type { CommonUniforms } from "./uniforms";
136
+ import { isWorker, convertScreenPos, type TextureSlot } from "./utils";
137
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
138
+ /** @ts-ignore ignore: https://v3.vitejs.dev/guide/features.html#import-with-query-suffixes */
139
+ import WorkerURL from "./worker?url&worker";
140
+
141
+ export type { CameraOptions, CameraEvent } from "./camera";
142
+
143
+ export { ColorMap, EventHandler, Plugin } from "@navaramap/core";
144
+ export type {
145
+ Nullable,
146
+ XYZ,
147
+ LatLng,
148
+ LatLngHeight,
149
+ CameraOrientation,
150
+ CameraPosition,
151
+ ColorTuple,
152
+ LUT,
153
+ Globe,
154
+ Observed,
155
+ ObservedEvent,
156
+ } from "@navaramap/core";
157
+ export { CameraDirection } from "@navaramap/engine";
158
+ // CSM exports for advanced users
159
+ export { CascadedShadowMaps, CSMHelper } from "@navaramap/three_csm";
160
+
161
+ export * from "./type";
162
+ export * from "./constants";
163
+ export * from "./light";
164
+ export * from "./mesh";
165
+ export * from "./layer";
166
+ export * from "./source";
167
+ export * from "./effects";
168
+ export * from "./shaders";
169
+ export * from "./material";
170
+ export * from "./core";
171
+ export { BufferView } from "./bufferView";
172
+ export * from "./plugins";
173
+ export * from "./layers";
174
+ export * from "./passes";
175
+ export * from "./evaluations";
176
+ export { SKY_RENDER_ORDER, STARS_RENDER_ORDER } from "./renderOrder";
177
+ export * from "@navaramap/three_api";
178
+ export * from "./Color";
179
+ export { type BlendMode, blendFunction, createReplacer } from "./utils";
180
+ export { Atmosphere, type AtmosphereOptions } from "./atmosphere";
181
+ export type { Quality } from "./quality";
182
+ export type { CustomObject3DEventMap } from "./object3DEvent";
183
+ export type {
184
+ FontFamily,
185
+ FontFace,
186
+ UnicodeRange,
187
+ FontWorkerMemoryStats,
188
+ } from "@navaramap/font";
189
+ export {
190
+ fetchFontFamilyFromCss,
191
+ parseCssUnicodeRange,
192
+ parseFontFamilyFromCss,
193
+ } from "@navaramap/font";
194
+ export type {
195
+ CssFontFaceFilter,
196
+ FetchCssFontFamilyOptions,
197
+ ParseCssFontFamilyOptions,
198
+ } from "@navaramap/font";
199
+ export {
200
+ getDefaultCacheBytes,
201
+ getDefaultDynamicSse,
202
+ getDefaultLodFog,
203
+ getDefaultMemoryBudgets,
204
+ MB,
205
+ type DynamicSseSettings,
206
+ type LodFogSettings,
207
+ type MemoryBudgets,
208
+ } from "./device";
209
+
210
+ /**
211
+ * Snapshot of engine memory usage; see {@link ThreeView.memoryStats}. Plain
212
+ * object (not the WASM-generated class) per the WASM API policy: the class is
213
+ * copied field-by-field and freed before returning.
214
+ */
215
+ export type MemoryStats = {
216
+ bufferTotalBytes: number;
217
+ /** Bytes held in the JS-side `InMemoryBufferStore` (WASM `External`
218
+ * entries): fetched MVT pbf and worker-built geometry that never enter WASM
219
+ * linear memory. Not part of `bufferTotalBytes`. */
220
+ externalBufferBytes: number;
221
+ bufferCount: number;
222
+ gpuBytesEst: number;
223
+ externalCpuBytes: number;
224
+ reservedBytes: number;
225
+ budgetBytes: number | undefined;
226
+ evictedCount: number;
227
+ sseMultiplier: number;
228
+ retainedVector: number;
229
+ retainedTerrain: number;
230
+ retainedRaster: number;
231
+ retainedTiles3d: number;
232
+ };
233
+
234
+ /** Snapshot of worker-side memory; see {@link ThreeView.workerMemoryStats}. */
235
+ export type WorkerMemoryStats = {
236
+ /** Tile-worker pool heaps (undefined per slot until first probed). */
237
+ tileWorkers: WorkerPoolHeapStats | undefined;
238
+ /** Font worker heap/cache breakdown; undefined while no font is in use. */
239
+ fontWorker: FontWorkerMemoryStats | undefined;
240
+ };
241
+
242
+ // NOTE:
243
+ // This overrides all materials to output a normal buffer, meaning Navara operates using MRT (Multiple Render Targets).
244
+ // Currently, Navara requires two buffers, so your shader must output them.
245
+ overrideMaterialsForMRT();
246
+
247
+ /**
248
+ * Configuration options for initializing ThreeView.
249
+ */
250
+ export type Options = {
251
+ /** Container element to append the canvas to. */
252
+ container?: HTMLElement;
253
+ /** Canvas element for rendering. If not provided, a new canvas is created. */
254
+ canvas?: HTMLCanvasElement | OffscreenCanvas;
255
+ /** Device pixel ratio override. Uses device default if not specified. */
256
+ pixelRatio?: number;
257
+ /** Disables automatic resize handling on window resize events. */
258
+ disableAutoResize?: boolean;
259
+ /** Enables debug mode with performance stats overlay. */
260
+ debug?: boolean;
261
+ /** Atmosphere rendering configuration options. */
262
+ atmosphere?: AtmosphereOptions;
263
+ /** Background color of the scene. Defaults to dark color (0x0a0a0f). */
264
+ backgroundColor?: CoreColor;
265
+ /** Feature picking configuration. */
266
+ picking?: boolean;
267
+ /** When true, renders every frame. When false, renders only on changes or when forceUpdate() is called. */
268
+ animation?: boolean;
269
+ /** Number of samples for MSAA (Multi-Sample Anti-Aliasing). 0 disables MSAA. */
270
+ multisampling?: number;
271
+ /** Uses half-float precision for post-processing. Higher quality when true. @defaultValue true */
272
+ halfFloat?: boolean;
273
+ /** Enables logarithmic depth buffer for improved depth precision at large scales. @defaultValue true */
274
+ logarithmicDepthBuffer?: boolean;
275
+ /** Enables shadow mapping. Must be set at initialization time. */
276
+ shadow?: boolean;
277
+ /** Enables mobile device optimizations such as lower pixel ratio. */
278
+ mobileOptimization?: boolean;
279
+ /**
280
+ * Milliseconds of inactivity (no Rust-side events) before the `idle` event fires.
281
+ * @defaultValue 100
282
+ */
283
+ idleThreshold?: number;
284
+ /**
285
+ * Memory budget in bytes for tile caches (WASM buffers + estimated GPU
286
+ * cost). Tiles leaving the view are retained until the budget is exceeded,
287
+ * then evicted least-recently-visited first — panning back is refetch-free
288
+ * while total usage stays capped. Naming follows Cesium3DTileset.cacheBytes.
289
+ * @defaultValue device-dependent, see `getDefaultCacheBytes`
290
+ */
291
+ cacheBytes?: number;
292
+ /**
293
+ * LOD fog: distance-based screen-space-error relaxation used by tile LOD
294
+ * selection — far tiles tolerate a larger error and stay coarser while
295
+ * near tiles keep full resolution. Purely an LOD control; it never
296
+ * affects visual fog rendering. Partial overrides are merged over the
297
+ * device default.
298
+ * @defaultValue device-memory-dependent, see `getDefaultLodFog`
299
+ */
300
+ lodFog?: Partial<LodFogSettings>;
301
+ /**
302
+ * Dynamic screen-space error (CesiumJS `dynamicScreenSpaceError`
303
+ * equivalent): tilted, street-level horizon views tolerate a larger error
304
+ * for far tiles, cutting the tile working set in exactly the views that
305
+ * over-refine. Zero effect looking straight down. Partial overrides are
306
+ * merged over the default.
307
+ * @defaultValue see `getDefaultDynamicSse`
308
+ */
309
+ dynamicSse?: Partial<DynamicSseSettings>;
310
+ /**
311
+ * Overrides for the worker-side memory budgets (per-tile-worker WASM heap
312
+ * recycle threshold and the font worker's cache budget). Defaults derive
313
+ * from the device memory together with `cacheBytes` — see
314
+ * `getDefaultMemoryBudgets`. The font budget caps *further growth* of the
315
+ * font caches; WASM heaps never return memory to the OS.
316
+ */
317
+ memoryBudget?: {
318
+ maxWorkerHeapBytes?: number;
319
+ fontBudgetBytes?: number;
320
+ /** In-flight data fetch cap per tile pipeline (see `getDefaultMemoryBudgets`). */
321
+ maxPendingRequests?: number;
322
+ /**
323
+ * Resting (base) memory-pressure SSE multiplier. On mobile a value > 1
324
+ * keeps far tiles coarser even when the budget is not exceeded. Defaults
325
+ * per device — see `getDefaultSseMultiplierRange`.
326
+ */
327
+ sseMultiplierMin?: number;
328
+ /**
329
+ * Ceiling for the dynamic memory-pressure SSE degrade. A larger value lets
330
+ * the engine shed more of the visible set under pressure before the tab is
331
+ * killed. Defaults per device — see `getDefaultSseMultiplierRange`.
332
+ */
333
+ sseMultiplierMax?: number;
334
+ };
335
+ /**
336
+ * Enables shared water texture. When enabled, a single water normal texture
337
+ * is loaded once and shared across all meshes that have water effects enabled.
338
+ */
339
+ waterTexture?: {
340
+ /** Whether to enable the shared water texture. */
341
+ enabled: boolean;
342
+ /** Custom water normal texture URL. Uses built-in texture if not specified. */
343
+ url?: string;
344
+ };
345
+ /**
346
+ * Attribution (credit) UI, on by default (accessed via
347
+ * {@link ThreeView.attribution}). Pass `false` to opt out (e.g. to build your
348
+ * own UI), or an object to configure the initial style / corner. Skipped in a
349
+ * worker (no DOM), so `view.attribution` is `undefined` there.
350
+ * @defaultValue true
351
+ */
352
+ defaultAttribution?: boolean | AttributionPluginOptions;
353
+ } & GlobeOptions;
354
+
355
+ /**
356
+ * Mouse event extended with map coordinates at the event location.
357
+ */
358
+ export type MapMouseEvent = {
359
+ /** World coordinates (ECEF) at the mouse position on the globe surface. */
360
+ map: XYZ;
361
+ } & MouseEvent;
362
+
363
+ /**
364
+ * Event types emitted by ThreeView. Subscribe using `view.on(eventName, callback)`.
365
+ */
366
+ export type ViewEvents = {
367
+ /** Emitted when the view is resized. Receives width and height in pixels. */
368
+ resize: (w: number, h: number) => void;
369
+ /** Emitted when a feature is picked. Receives picked feature info or null. */
370
+ pick: (info: Nullable<PickedFeature>) => void;
371
+ /** Emitted when a layer event occurs. Receives event type, layer ID, and event arguments. */
372
+ layer: <K extends keyof LayerEvent>(
373
+ k: K,
374
+ layerId: string,
375
+ ...args: Parameters<LayerEvent[K]>
376
+ ) => void;
377
+ /** Emitted before an update process happens. Receives `DOMHighResTimeStamp` as a timestamp. */
378
+ preUpdate: (t: number) => void;
379
+ /** Emitted after an update process when state changes occurred. Receives `DOMHighResTimeStamp` as a timestamp. */
380
+ postUpdate: (t: number) => void;
381
+ /** Emitted before rendering. With `animation: true`, fires every frame. Receives `DOMHighResTimeStamp` as a timestamp. */
382
+ preRender: (t: number) => void;
383
+ /** Emitted after rendering. With `animation: true`, fires every frame. Receives `DOMHighResTimeStamp` as a timestamp. */
384
+ postRender: (t: number) => void;
385
+ /** @private Emitted when terrain height sampling completes. */
386
+ _sample_terrain_height_received: (ev: TerrainHeightUpdatedEvent) => void;
387
+ /** Emitted on mouse down with map coordinates. */
388
+ mousedown: (event: MapMouseEvent) => void;
389
+ /** Emitted when mouse enters the canvas with map coordinates. */
390
+ mouseenter: (event: MapMouseEvent) => void;
391
+ /** Emitted when mouse leaves the canvas with map coordinates. */
392
+ mouseleave: (event: MapMouseEvent) => void;
393
+ /** Emitted on mouse move with map coordinates. */
394
+ mousemove: (event: MapMouseEvent) => void;
395
+ /** Emitted on mouse up with map coordinates. */
396
+ mouseup: (event: MapMouseEvent) => void;
397
+ /** Emitted on click with map coordinates. */
398
+ click: (event: MapMouseEvent) => void;
399
+ /** Emitted when the engine becomes idle (no Rust-side events for `idleThreshold` ms). */
400
+ idle: () => void;
401
+ };
402
+
403
+ /**
404
+ * The main 3D globe view class that manages rendering, layers, camera, and user interaction.
405
+ * Create an instance and call `init()` to start the engine.
406
+ *
407
+ * @example
408
+ * ```typescript
409
+ * const view = new ThreeView();
410
+ * await view.init();
411
+ * ```
412
+ */
413
+ export default class ThreeView<
414
+ D extends Descriptions = EmptyDescriptions,
415
+ > extends EventHandler<ViewEvents> {
416
+ private _camera: ThreeViewCamera;
417
+ private _renderer: WebGLRenderer;
418
+ private _globe!: Globe;
419
+ private _atmosphere: Atmosphere;
420
+
421
+ /** Handle for the sky environment map effect descriptor. Used for sky reflections. */
422
+ private skyEnvMap!: EffectHandle<SkyEnvMapEffectDesc>;
423
+ /** Handle for the Multi-Render Target pass that outputs color and normal buffers. */
424
+ private mrtPass!: EffectHandle<MRTPassEffectDesc>;
425
+ /** Handle for the transparent objects rendering pass. */
426
+ private transparentPass!: EffectHandle<TransparentPassEffectDesc>;
427
+ /** Handle for the final compositing pass that outputs to screen. */
428
+ private finalPass!: EffectHandle<FinalCopyEffectDesc>;
429
+
430
+ /** The render pass orchestrator that manages the post-processing effect pipeline. */
431
+ private renderPassOrchestrator: RenderPassOrchestrator;
432
+
433
+ private _scenes: Scenes;
434
+
435
+ private _core: Core | undefined;
436
+ private _fontManager = new FontManager(FontWorkerURL);
437
+ private _options: Options;
438
+ private _stats: RendererStats | undefined;
439
+ private _eventDisposer: (() => void) | undefined;
440
+ private _disposed = false;
441
+ private _renderFlag: RenderFlag = {
442
+ forceUpdate: false,
443
+ animation: false,
444
+ };
445
+ private _isIdle = false;
446
+ private _uniforms: CommonUniforms;
447
+
448
+ private _meshes: MeshCache = new Map();
449
+ private _abortControllers: AbortControllers = new Map();
450
+ private _workerPoolPromises: WorkerPoolPromises = new Map();
451
+ private _loadedTexs = new Map<string, Texture>();
452
+ private _texturizedSceneByTileCoordinates: TexturizedSceneByTileCoordinates;
453
+ private _tileTextureCompositor!: TileTextureCompositor;
454
+ private _tileMapByHandle: TileMapByHandle = new Map();
455
+ // fragment id → Set of {tileMesh, slotIndex}
456
+ private _textureFragmentIndex: Map<string, Set<TextureSlot>> = new Map<
457
+ string,
458
+ Set<TextureSlot>
459
+ >();
460
+ // tileMesh → Set of fragment IDs it uses
461
+ private _tileMeshToFragmentIds: Map<TileMesh, Set<string>> = new Map<
462
+ TileMesh,
463
+ Set<string>
464
+ >();
465
+ // Hillshade processing context
466
+ // Scoped to this view instance to prevent cross-view contamination
467
+ private _hillshadeContext = new HillshadeContext();
468
+ private _initialized = false;
469
+
470
+ /**
471
+ * JS-side store for buffers WASM tracks by byte count only (`External`
472
+ * entries). Fetched MVT pbf and worker-built geometry live here instead of
473
+ * being copied into WASM linear memory, whose peak never shrinks. The
474
+ * `_buf` wrapper prefers this store, falling back to WASM for entries Rust
475
+ * still owns; handles are evicted per frame via `drainRemovedExternalHandles`.
476
+ */
477
+ private _inMemoryBufferStore = new InMemoryBufferStore();
478
+
479
+ private _buf: BufferLoader = {
480
+ // Getters prefer the JS store (type-matched) and fall back to the WASM
481
+ // view for entries Rust still owns.
482
+ u8: (handle) => {
483
+ const js = this._inMemoryBufferStore.get(handle);
484
+ if (js instanceof Uint8Array) return js;
485
+ const b = this._core?.getBufferU8(handle);
486
+ return b ?? null;
487
+ },
488
+ f32: (handle) => {
489
+ const js = this._inMemoryBufferStore.get(handle);
490
+ if (js instanceof Float32Array) return js;
491
+ const b = this._core?.getBufferF32(handle);
492
+ return b ?? null;
493
+ },
494
+ f64: (handle) => {
495
+ const js = this._inMemoryBufferStore.get(handle);
496
+ if (js instanceof Float64Array) return js;
497
+ const b = this._core?.getBufferF64(handle);
498
+ return b ?? null;
499
+ },
500
+ u32: (handle) => {
501
+ const js = this._inMemoryBufferStore.get(handle);
502
+ if (js instanceof Uint32Array) return js;
503
+ const b = this._core?.getBufferU32(handle);
504
+ return b ?? null;
505
+ },
506
+ // remove* take from the JS store when present (and clear the WASM-side
507
+ // External accounting), else fall back to the owned-copy WASM remover.
508
+ removeU8: (handle) => {
509
+ const js = this._inMemoryBufferStore.take(handle);
510
+ if (js instanceof Uint8Array) {
511
+ this._core?.removeBuffer(handle);
512
+ return js;
513
+ }
514
+ const b = this._core?.removeBufferU8(handle);
515
+ return b ?? null;
516
+ },
517
+ removeU32: (handle) => {
518
+ const js = this._inMemoryBufferStore.take(handle);
519
+ if (js instanceof Uint32Array) {
520
+ this._core?.removeBuffer(handle);
521
+ return js;
522
+ }
523
+ const b = this._core?.removeBufferU32(handle);
524
+ return b ?? null;
525
+ },
526
+ removeF32: (handle) => {
527
+ const js = this._inMemoryBufferStore.take(handle);
528
+ if (js instanceof Float32Array) {
529
+ this._core?.removeBuffer(handle);
530
+ return js;
531
+ }
532
+ const b = this._core?.removeBufferF32(handle);
533
+ return b ?? null;
534
+ },
535
+ removeF64: (handle) => {
536
+ const js = this._inMemoryBufferStore.take(handle);
537
+ if (js instanceof Float64Array) {
538
+ this._core?.removeBuffer(handle);
539
+ return js;
540
+ }
541
+ const b = this._core?.removeBufferF64(handle);
542
+ return b ?? null;
543
+ },
544
+ setU8: (handle: number, bits: bigint, b: Uint8Array) => {
545
+ if (!this._core?.hasDataRequester(bits)) {
546
+ return;
547
+ }
548
+ this._core?.setBufferU8(handle, bits, b.length, (buf: Uint8Array) => {
549
+ buf.set(b);
550
+ });
551
+ },
552
+ newU8: (b: Uint8Array) => {
553
+ return this._core?.newBufferU8(b.length, (buf: Uint8Array) => {
554
+ buf.set(b);
555
+ });
556
+ // return this._core?.newBufferU8Cloned(b);
557
+ },
558
+ newU32: (b: Uint32Array) => {
559
+ return this._core?.newBufferU32(b.length, (buf: Uint32Array) => {
560
+ buf.set(b);
561
+ });
562
+ // return this._core?.newBufferU32Cloned(b);
563
+ },
564
+ newF32: (b: Float32Array) => {
565
+ return this._core?.newBufferF32(b.length, (buf: Float32Array) => {
566
+ buf.set(b);
567
+ });
568
+ // return this._core?.newBufferF32Cloned(b);
569
+ },
570
+ newF64: (b: Float64Array) => {
571
+ return this._core?.newBufferF64(b.length, (buf: Float64Array) => {
572
+ buf.set(b);
573
+ });
574
+ // return this._core?.newBufferF32Cloned(b);
575
+ },
576
+ // adopt* register a JS-owned array (zero-copy) and mint a byte-count-only
577
+ // External handle; the array bytes never touch WASM linear memory.
578
+ adoptU8: (array: Uint8Array) => {
579
+ const handle = this._core?.newExternalBuffer(array.byteLength);
580
+ if (handle != null) this._inMemoryBufferStore.set(handle, array);
581
+ return handle;
582
+ },
583
+ adoptU32: (array: Uint32Array) => {
584
+ const handle = this._core?.newExternalBuffer(array.byteLength);
585
+ if (handle != null) this._inMemoryBufferStore.set(handle, array);
586
+ return handle;
587
+ },
588
+ adoptF32: (array: Float32Array) => {
589
+ const handle = this._core?.newExternalBuffer(array.byteLength);
590
+ if (handle != null) this._inMemoryBufferStore.set(handle, array);
591
+ return handle;
592
+ },
593
+ adoptF64: (array: Float64Array) => {
594
+ const handle = this._core?.newExternalBuffer(array.byteLength);
595
+ if (handle != null) this._inMemoryBufferStore.set(handle, array);
596
+ return handle;
597
+ },
598
+ setExternal: (handle: number, bits: bigint, array: Uint8Array) => {
599
+ if (!this._core?.hasDataRequester(bits)) {
600
+ return;
601
+ }
602
+ this._core.setExternalBuffer(handle, bits, array.byteLength);
603
+ this._inMemoryBufferStore.set(handle, array);
604
+ },
605
+ remove: (handle: number) => {
606
+ // Also drop any JS-side entry so the two stores stay in step when a
607
+ // caller removes an External handle without going through remove*.
608
+ this._inMemoryBufferStore.delete(handle);
609
+ this._core?.removeBuffer(handle);
610
+ },
611
+ drainRemovedExternalHandles: () => {
612
+ const handles = this._core?.drainRemovedExternalHandles();
613
+ if (!handles) return;
614
+ for (const handle of handles) {
615
+ this._inMemoryBufferStore.delete(handle);
616
+ }
617
+ },
618
+ triggerDataRequesterLoaded: (bits: bigint, handle: number) => {
619
+ this._core?.triggerDataRequesterLoaded(bits, handle);
620
+ },
621
+ triggerDataRequesterFailed: (bits: bigint) => {
622
+ this._core?.triggerDataRequesterFailed(bits);
623
+ },
624
+ };
625
+ private _texFragment: TextureFragmentHandler = {
626
+ triggerTextureFragmentLoaded: (
627
+ bits: bigint,
628
+ status: TextureFragmentStatus,
629
+ ) => {
630
+ this._core?.triggerTextureFragmentLoaded(bits, status);
631
+ },
632
+ };
633
+ // Per-frame snapshot of the WASM vector resolution revision, refreshed in `_render`.
634
+ private _vectorRevision = 0;
635
+ private _tileHandler: TileHandler = {
636
+ getMartini: (id) => {
637
+ return this._core?.getMartini(id);
638
+ },
639
+ getTile: (handle) => {
640
+ return this._core?.getTile(handle);
641
+ },
642
+ getParentTile: (handle) => {
643
+ return this._core?.getParentTile(handle);
644
+ },
645
+ getTileElevationDecoder: (handle) => {
646
+ return this._core?.getTileElevationDecoder(handle);
647
+ },
648
+ getVectorTileStates: (handle) => {
649
+ return this._core?.getVectorTileStates(handle);
650
+ },
651
+ // Returns the per-frame cached revision (read once in `_render`), so per-tile
652
+ // `onBeforeRender` gating costs no WASM-boundary call.
653
+ vectorRevision: () => this._vectorRevision,
654
+ reportDrapeGpuBytes: (handle, bytes) => {
655
+ this._core?.reportTerrainDrapeGpuBytes(handle, bytes);
656
+ },
657
+ calcMetersPerTexel: (tileHandle, textureZoom, textureWidth) => {
658
+ return (
659
+ this._core?.calcMetersPerTexel(tileHandle, textureZoom, textureWidth) ??
660
+ 1.0
661
+ );
662
+ },
663
+ };
664
+ private _globeHandler: GlobeHandler = {
665
+ getTransparent: () => {
666
+ return this._core?.getGlobeTransparent();
667
+ },
668
+ getMaxSse: () => {
669
+ return this._core?.getGlobeMaxSse();
670
+ },
671
+ getSegments: () => {
672
+ return this._core?.getGlobeSegments();
673
+ },
674
+ getColor: (): CoreColor | undefined => {
675
+ const hexColor = this._core?.getGlobeColor();
676
+ return hexColor === undefined ? undefined : new Color().setHex(hexColor);
677
+ },
678
+ getHideUnderground: () => {
679
+ return this._core?.getGlobeHideUnderground();
680
+ },
681
+ getUseNormal: () => {
682
+ return this._core?.getGlobeUseNormal();
683
+ },
684
+ getOpacity: () => {
685
+ return this._core?.getGlobeOpacity();
686
+ },
687
+ getWireframe: () => {
688
+ return this._core?.getGlobeWireframe();
689
+ },
690
+ getElevationColormap: () => {
691
+ return this._core?.getGlobeElevationColormap();
692
+ },
693
+ setTransparent: (value: boolean) => {
694
+ this._core?.setGlobeTransparent(value);
695
+ },
696
+ setMaxSse: (value: number) => {
697
+ this._core?.setGlobeMaxSse(value);
698
+ },
699
+ setSegments: (value: number) => {
700
+ this._core?.setGlobeSegments(value);
701
+ },
702
+ setColor: (value: CoreColor) => {
703
+ this._core?.setGlobeColor(value.toHex());
704
+ },
705
+ setHideUnderground: (value: boolean) => {
706
+ this._core?.setGlobeHideUnderground(value);
707
+ },
708
+ setUseNormal: (value: boolean) => {
709
+ this._core?.setGlobeUseNormal(value);
710
+ },
711
+ setOpacity: (value: number) => {
712
+ this._core?.setGlobeOpacity(value);
713
+ },
714
+ setWireframe: (value: boolean) => {
715
+ this._core?.setGlobeWireframe(value);
716
+ },
717
+ setElevationColormap: (value: ColorMap) => {
718
+ this._core?.setGlobeElevationColormap(value.flatten());
719
+
720
+ const canvas = value.createImage();
721
+ const texture = new Texture(canvas);
722
+ texture.wrapS = ClampToEdgeWrapping;
723
+ texture.wrapT = ClampToEdgeWrapping;
724
+ texture.minFilter = LinearFilter;
725
+ texture.magFilter = LinearFilter;
726
+ texture.generateMipmaps = false;
727
+ texture.flipY = false;
728
+ texture.needsUpdate = true;
729
+
730
+ this._uniforms.colorMapTexture.value = texture;
731
+
732
+ // Track additional texture usage
733
+ this._defaultTextureOptions.additionalTexturesInUse = {
734
+ ...this._defaultTextureOptions.additionalTexturesInUse,
735
+ colorMapTexture: true,
736
+ };
737
+ },
738
+ };
739
+ private _workerTaskHandler: WorkerTaskHandler = {
740
+ triggerWorkerTaskCompleted: (bits, result) => {
741
+ this._core?.triggerWorkerTaskCompleted(bits, result);
742
+ },
743
+ triggerWorkerTaskFailed: (delegator_id) => {
744
+ if (this._core) {
745
+ this._core.triggerWorkerTaskFailed(delegator_id);
746
+ } else {
747
+ // The engine is gone (teardown); still free the boundary object.
748
+ delegator_id.free();
749
+ }
750
+ },
751
+ hasWorkerTask: (bits) => {
752
+ return !!this._core?.hasWorkerTask(bits);
753
+ },
754
+ };
755
+ private _featureHandler: FeatureHandler = {
756
+ getTransferablePolygonBatchedFeature: (bits) => {
757
+ return this._core?.getTransferablePolygonBatchedFeature(bits);
758
+ },
759
+ getTransferablePolylineBatchedFeature: (bits) => {
760
+ return this._core?.getTransferablePolylineBatchedFeature(bits);
761
+ },
762
+ markFeatureIsRendered: (type, bits) => {
763
+ this._core?.markFeatureIsRendered(type, bits);
764
+ },
765
+ reportFeatureGpuBytes: (bits, bytes) => {
766
+ this._core?.reportFeatureGpuBytes(bits, bytes);
767
+ },
768
+ readAllBatchedProperties: (featureId, f) =>
769
+ this._core?.readAllBatchedProperties(featureId, f),
770
+ readFilteredBatchedProperties: (featureId, keys, f) =>
771
+ this._core?.readFilteredBatchedProperties(featureId, keys, f),
772
+ };
773
+ private _meshHandler: MeshHandler = {
774
+ setTileMeshPrepared: (handle: bigint) => {
775
+ this._core?.setTileMeshPrepared(handle);
776
+ },
777
+ };
778
+ private _layerHandler: LayerHandler = {
779
+ getLayerIndex: (layerId: string) => {
780
+ return this._core?.getLayerIndex(layerId);
781
+ },
782
+ };
783
+ private _ownedRootElement: HTMLElement | undefined;
784
+ private _eventManager = new EventManager();
785
+ private _pickHelper?: PickHelper;
786
+ private _terrainPicker: TerrainPicker;
787
+ private _defaultTextureOptions: TextureOptions;
788
+ private layersManager = new LayersManager();
789
+ private shadowMapViewers: ShadowMapViewers;
790
+ private eventContext!: EventContext;
791
+
792
+ // Registry support
793
+ private registries!: Registries;
794
+ private viewContext!: ViewContext;
795
+ private plugins: Plugin[] = [];
796
+
797
+ /** Built-in attribution UI; unset when disabled or in a worker (no DOM). */
798
+ private _attribution?: AttributionPlugin;
799
+
800
+ private pixelRatioMatchedMedia?: MediaQueryList;
801
+
802
+ /**
803
+ * The built-in attribution (credit) UI. `undefined` when disabled via
804
+ * `defaultAttribution: false`, or in a worker / no-DOM environment. Feed it
805
+ * credits with `view.attribution?.add([...])`.
806
+ */
807
+ get attribution(): AttributionPlugin | undefined {
808
+ return this._attribution;
809
+ }
810
+
811
+ constructor(options: Options = {}) {
812
+ super();
813
+
814
+ if (!options.canvas) {
815
+ const div = document.createElement("div");
816
+ div.id = "navara-root";
817
+ div.style.width = "100vw";
818
+ div.style.height = "100vh";
819
+
820
+ options.canvas = document.createElement("canvas");
821
+ options.canvas.id = "navara-canvas";
822
+ options.canvas.style.width = "100%";
823
+ options.canvas.style.height = "100%";
824
+
825
+ div.appendChild(options.canvas);
826
+
827
+ document.body.appendChild(div);
828
+ this._ownedRootElement = div;
829
+ }
830
+
831
+ this._options = options;
832
+
833
+ // Attribution UI on by default; skip in a worker (no DOM) or when disabled.
834
+ if (options.defaultAttribution !== false && !isWorker()) {
835
+ const cfg =
836
+ typeof options.defaultAttribution === "object"
837
+ ? options.defaultAttribution
838
+ : undefined;
839
+ this._attribution = new AttributionPlugin(cfg);
840
+ this.addPlugin(this._attribution);
841
+ }
842
+
843
+ // Initialize terrain picker
844
+ this._terrainPicker = new TerrainPicker();
845
+
846
+ // disable right-click
847
+ options.canvas?.addEventListener("contextmenu", (e) => {
848
+ e.preventDefault();
849
+ });
850
+
851
+ const renderer = new WebGLRenderer({
852
+ // If it's true, some noise will happen. So use other AA algorithm instead.
853
+ antialias: (options.multisampling ?? 0) > 0,
854
+ logarithmicDepthBuffer: options.logarithmicDepthBuffer ?? true,
855
+ canvas: options.canvas,
856
+ stencil: true,
857
+ });
858
+ renderer.info.autoReset = false;
859
+ renderer.autoClearStencil = false;
860
+ renderer.autoClearColor = false;
861
+ renderer.autoClearDepth = false;
862
+
863
+ this._renderer = renderer;
864
+
865
+ renderer.shadowMap.enabled = !!options.shadow;
866
+ this._renderer.shadowMap.type = PCFShadowMap;
867
+
868
+ // Update shadow map manually in CustomRenderPass.
869
+ renderer.shadowMap.autoUpdate = false;
870
+
871
+ const { width, height } = this._getCanvasSize() ?? {};
872
+ invariant(width && height);
873
+
874
+ if (typeof options?.pixelRatio === "number" || !isWorker()) {
875
+ const pixelRatio = isWorker()
876
+ ? 1
877
+ : getDevicePixelRatio({
878
+ override: options.pixelRatio,
879
+ mobileOptimization: options.mobileOptimization,
880
+ });
881
+ renderer.setPixelRatio(pixelRatio);
882
+ }
883
+
884
+ renderer.setSize(width, height, !isWorker());
885
+ if (options.container) {
886
+ options.container.appendChild(renderer.domElement);
887
+ }
888
+
889
+ this._texturizedSceneByTileCoordinates =
890
+ new TexturizedSceneByTileCoordinates(this._renderer);
891
+
892
+ // Compositor owns the per-tile composite atlas pool. It centralises the
893
+ // vector-scene offscreen render loop that previously lived in TileMesh,
894
+ // and is the seam through which a future MRT composite pass will reduce
895
+ // the main TileMesh shader's texture fetches.
896
+ // Atlas / drape RT side length is 512² on both desktop and mobile — the
897
+ // SAME source of truth `getDefaultMemoryCostHints` uses, so the ledger's
898
+ // per-tile GPU estimate matches the real allocation.
899
+ // `isMobileOptimized()` only reads `_options.mobileOptimization` (set
900
+ // above) and the pure `isMobileDevice()`, so it is safe here in the ctor.
901
+ this._tileTextureCompositor = new TileTextureCompositor({
902
+ renderer: this._renderer,
903
+ texturizedSceneByTileCoordinates: this._texturizedSceneByTileCoordinates,
904
+ size: getCompositeAtlasSize(this.isMobileOptimized()),
905
+ });
906
+
907
+ this._scenes = {
908
+ light: new Group(),
909
+ mrt: new Scene(),
910
+ globe: new Scene(),
911
+ draped: new Scene(),
912
+ opaque: new Scene(),
913
+ transparent: new Scene(),
914
+ skyEnvMap: new Scene(),
915
+ };
916
+
917
+ this._camera = new ThreeViewCamera();
918
+
919
+ // Setup render pass orchestrator
920
+ this.renderPassOrchestrator = new RenderPassOrchestrator(this._renderer, {
921
+ halfFloat: options.halfFloat ?? true,
922
+ multisampling: options.multisampling,
923
+ });
924
+
925
+ this.renderPassOrchestrator.setSize(width, height);
926
+
927
+ // Background color
928
+ const bgColor = options.backgroundColor
929
+ ? options.backgroundColor.toHex()
930
+ : 0x000000;
931
+ this._renderer.setClearColor(bgColor);
932
+
933
+ if (!options.disableAutoResize && !isWorker()) {
934
+ window.addEventListener("resize", this._handleResize);
935
+ // Observe a change in devicePixelRatio.
936
+ this.pixelRatioMatchedMedia = window.matchMedia(
937
+ `(resolution: ${window.devicePixelRatio}dppx)`,
938
+ );
939
+
940
+ this.pixelRatioMatchedMedia.addEventListener(
941
+ "change",
942
+ this._handleResize,
943
+ );
944
+ }
945
+
946
+ if (options.debug) {
947
+ const t = options.container || this._renderer.domElement.parentElement;
948
+ if (t) {
949
+ this._stats = new RendererStats(
950
+ {
951
+ beginRender: () => this._renderer.info.reset(),
952
+ endRender: () => ({
953
+ ...this._renderer.info.render,
954
+ memGeometries: this._renderer.info.memory.geometries,
955
+ }),
956
+ },
957
+ () => {
958
+ const stats = this._core?.getMemoryStats();
959
+ // Read the fields off the boxed wasm-bindgen struct, then free it —
960
+ // the sampler runs ~1/s and the struct otherwise leaks until the
961
+ // FinalizationRegistry runs (see fontWorker.ts for the pattern).
962
+ const bufferTotalBytes = stats?.bufferTotalBytes;
963
+ const externalBufferBytes = stats?.externalBufferBytes;
964
+ const budgetBytes = stats?.budgetBytes;
965
+ stats?.free();
966
+ const memory = (
967
+ performance as Performance & {
968
+ memory?: { usedJSHeapSize: number };
969
+ }
970
+ ).memory;
971
+ const MB = 1024 * 1024;
972
+ return {
973
+ wasmMB:
974
+ bufferTotalBytes !== undefined
975
+ ? bufferTotalBytes / MB
976
+ : undefined,
977
+ jsBufMB:
978
+ externalBufferBytes !== undefined
979
+ ? externalBufferBytes / MB
980
+ : undefined,
981
+ jsHeapMB: memory ? memory.usedJSHeapSize / MB : undefined,
982
+ budgetMB:
983
+ budgetBytes !== undefined ? budgetBytes / MB : undefined,
984
+ };
985
+ },
986
+ );
987
+ t.appendChild(this._stats.dom);
988
+ }
989
+ }
990
+
991
+ this._uniforms = {
992
+ viewportAndPixelRatio: { value: null },
993
+ frustumNearFar: { value: null },
994
+ frustumRatio: { value: null },
995
+ tGlobeDepth: { value: null },
996
+ tGlobeNormal: { value: null },
997
+ tSkyEnvMap: { value: null },
998
+ inverseProjectionMatrix: { value: null },
999
+ // TODO: Need to sync `fov` with WASM side
1000
+ fov: { value: (this._camera.raw.fov * Math.PI) / 180 },
1001
+ screenHeightPx: { value: height },
1002
+ time: { value: 0 },
1003
+ colorMapTexture: { value: null },
1004
+ waterTexture: { value: null },
1005
+ };
1006
+
1007
+ // This is necessary to avoid attaching a texture beyond the max textures capabilities of GPU.
1008
+ // TODO: Allow to change this value dynamically.
1009
+ const NUM_CASCADED_SHADOW_MAPS = 6;
1010
+
1011
+ this._defaultTextureOptions = {
1012
+ maxAnisotropy: this._renderer.capabilities.getMaxAnisotropy(),
1013
+ magFilter: LinearFilter,
1014
+ minFilter: LinearFilter,
1015
+ useMipmaps: true,
1016
+ maxTextures:
1017
+ Math.max(this._renderer.capabilities.maxTextures, 8) -
1018
+ NUM_CASCADED_SHADOW_MAPS,
1019
+ };
1020
+
1021
+ // Load shared water texture if enabled
1022
+ if (options.waterTexture?.enabled) {
1023
+ const waterUrl = options.waterTexture.url ?? WATER_NORMAL_URL;
1024
+ this._uniforms.waterTexture.value = TEXTURE_LOADER.load(
1025
+ waterUrl,
1026
+ (texture) => {
1027
+ texture.wrapS = RepeatWrapping;
1028
+ texture.wrapT = RepeatWrapping;
1029
+ },
1030
+ );
1031
+ // Track additional texture usage
1032
+ this._defaultTextureOptions.additionalTexturesInUse = {
1033
+ ...this._defaultTextureOptions.additionalTexturesInUse,
1034
+ waterTexture: true,
1035
+ };
1036
+ }
1037
+
1038
+ this._atmosphere = new Atmosphere(
1039
+ this._renderer,
1040
+ options.atmosphere,
1041
+ this._camera,
1042
+ );
1043
+ this._atmosphere.on("needsUpdate", this.forceUpdate);
1044
+
1045
+ this.on("layer", (e, id, ...args) => {
1046
+ this.layersManager.emitById(e, id, ...args);
1047
+ });
1048
+
1049
+ this._renderFlag.animation = !!options.animation;
1050
+
1051
+ this._camera.on("frustumChanged", () => {
1052
+ this.renderPassOrchestrator.effectComposer.setMainCamera(
1053
+ this._camera.raw,
1054
+ );
1055
+ });
1056
+
1057
+ this.shadowMapViewers = new ShadowMapViewers(this._scenes.light);
1058
+ }
1059
+
1060
+ /**
1061
+ * Convert a mouse event to a MapMouseEvent by adding map coordinates
1062
+ */
1063
+ private convertMouseEventToMapEvent(event: MouseEvent): MapMouseEvent | null {
1064
+ const rect = this._renderer.domElement.getBoundingClientRect();
1065
+ const x = event.clientX - rect.left;
1066
+ const y = event.clientY - rect.top;
1067
+ const xyz = convertScreenPos(this, x, y);
1068
+
1069
+ if (xyz) {
1070
+ return Object.assign(event, {
1071
+ map: {
1072
+ x: xyz.x,
1073
+ y: xyz.y,
1074
+ z: xyz.z,
1075
+ },
1076
+ }) as MapMouseEvent;
1077
+ }
1078
+
1079
+ return null;
1080
+ }
1081
+
1082
+ private initializeRenderPass() {
1083
+ this.skyEnvMap = this.addEffect<SkyEnvMapEffectDesc>({
1084
+ skyEnvMap: {},
1085
+ });
1086
+ this.mrtPass = this.addEffect<MRTPassEffectDesc>({
1087
+ mrt: {
1088
+ // debugNormal: true,
1089
+ },
1090
+ });
1091
+ this.transparentPass = this.addEffect<TransparentPassEffectDesc>({
1092
+ transparent: {},
1093
+ });
1094
+ this.finalPass = this.addEffect<FinalCopyEffectDesc>({
1095
+ final: {},
1096
+ });
1097
+ }
1098
+
1099
+ private get renderPass() {
1100
+ const instance = this.mrtPass.ref.raw;
1101
+ invariant(instance);
1102
+ return instance;
1103
+ }
1104
+
1105
+ /** The camera controller that manages view position, orientation, and projection. */
1106
+ get camera(): ThreeViewCamera {
1107
+ return this._camera;
1108
+ }
1109
+
1110
+ /** The globe instance that manages terrain, imagery layers, and globe-specific settings. */
1111
+ get globe(): Globe {
1112
+ return this._globe;
1113
+ }
1114
+
1115
+ /** The atmosphere renderer that handles sky, sun, and atmospheric scattering effects. */
1116
+ get atmosphere(): Atmosphere {
1117
+ return this._atmosphere;
1118
+ }
1119
+
1120
+ /**
1121
+ * Gets the tone mapping exposure value.
1122
+ */
1123
+ get toneMappingExposure() {
1124
+ return this._renderer.toneMappingExposure;
1125
+ }
1126
+ /**
1127
+ * Sets the tone mapping exposure value for HDR rendering.
1128
+ */
1129
+ set toneMappingExposure(v: number) {
1130
+ this._renderer.toneMappingExposure = v;
1131
+ this.forceUpdate();
1132
+ }
1133
+
1134
+ /**
1135
+ * The resolved tile-cache memory budget in bytes (see `Options.cacheBytes`).
1136
+ * `undefined` before `init()` when no explicit option was given.
1137
+ */
1138
+ get cacheBytes(): number | undefined {
1139
+ return this._options.cacheBytes;
1140
+ }
1141
+ /**
1142
+ * Updates the tile-cache memory budget at runtime. Lowering it evicts
1143
+ * retained tiles down to the new budget over the next frames. Passing
1144
+ * `undefined` disables budgeting entirely, restoring the original
1145
+ * destroy-on-unvisited tile lifecycle.
1146
+ */
1147
+ set cacheBytes(v: number | undefined) {
1148
+ this._options.cacheBytes = v;
1149
+ this._core?.setCacheBytes(v);
1150
+ }
1151
+
1152
+ /**
1153
+ * The resolved LOD fog settings (see `Options.lodFog`). `undefined`
1154
+ * before `init()`.
1155
+ */
1156
+ get lodFog(): LodFogSettings | undefined {
1157
+ const lodFog = this._options.lodFog;
1158
+ if (!lodFog) return undefined;
1159
+ return lodFog as LodFogSettings;
1160
+ }
1161
+ /**
1162
+ * Updates the LOD fog at runtime; partial values merge over the current
1163
+ * settings. The next traversal re-selects tile LODs with the new curve.
1164
+ */
1165
+ set lodFog(v: Partial<LodFogSettings>) {
1166
+ const lodFog = {
1167
+ ...getDefaultLodFog({ isMobile: this.isMobileOptimized() }),
1168
+ ...this._options.lodFog,
1169
+ ...v,
1170
+ };
1171
+ this._options.lodFog = lodFog;
1172
+ this._core?.setLodFog(lodFog.enabled, lodFog.density, lodFog.sseFactor);
1173
+ }
1174
+
1175
+ /**
1176
+ * The resolved dynamic-SSE settings (see `Options.dynamicSse`).
1177
+ * `undefined` before `init()`.
1178
+ */
1179
+ get dynamicSse(): DynamicSseSettings | undefined {
1180
+ const dynamicSse = this._options.dynamicSse;
1181
+ if (!dynamicSse) return undefined;
1182
+ return dynamicSse as DynamicSseSettings;
1183
+ }
1184
+ /**
1185
+ * Updates the dynamic SSE at runtime; partial values merge over the
1186
+ * current settings. The next traversal re-selects tile LODs with the new
1187
+ * curve.
1188
+ */
1189
+ set dynamicSse(v: Partial<DynamicSseSettings>) {
1190
+ const dynamicSse = {
1191
+ ...getDefaultDynamicSse(),
1192
+ ...this._options.dynamicSse,
1193
+ ...v,
1194
+ };
1195
+ this._options.dynamicSse = dynamicSse;
1196
+ // The WASM class is constructed internally and consumed (moved) by
1197
+ // `setDynamicSse`, so no `.free()` is needed — per the WASM API policy.
1198
+ this._core?.setDynamicSse(
1199
+ new EngineDynamicSse(
1200
+ dynamicSse.enabled,
1201
+ dynamicSse.density,
1202
+ dynamicSse.sseFactor,
1203
+ dynamicSse.heightFalloff,
1204
+ dynamicSse.minHeight,
1205
+ dynamicSse.maxHeight,
1206
+ ),
1207
+ );
1208
+ }
1209
+
1210
+ /**
1211
+ * Updates the memory-pressure SSE degrade range at runtime. `min` is the
1212
+ * resting multiplier applied even without budget pressure (> 1 coarsens far
1213
+ * tiles at rest); `max` is the ceiling the dynamic degrade can climb to. The
1214
+ * next traversal re-selects tile LODs with the new range.
1215
+ */
1216
+ setSseMultiplierRange(min: number, max: number): void {
1217
+ this._options.memoryBudget = {
1218
+ ...this._options.memoryBudget,
1219
+ sseMultiplierMin: min,
1220
+ sseMultiplierMax: max,
1221
+ };
1222
+ this._core?.setSseMultiplierRange(min, max);
1223
+ }
1224
+
1225
+ /**
1226
+ * Snapshot of engine memory usage (WASM buffer bytes, GPU estimates,
1227
+ * retained tile counts). Returns `undefined` before `init()`.
1228
+ */
1229
+ memoryStats(): MemoryStats | undefined {
1230
+ const stats = this._core?.getMemoryStats();
1231
+ if (!stats) return undefined;
1232
+ // Copy every field into a plain object and free the boxed WASM struct
1233
+ // before returning — the public API must not expose wasm-bindgen classes
1234
+ // (WASM_API_POLICY.md), and returning the class would also leak it.
1235
+ const plain: MemoryStats = {
1236
+ bufferTotalBytes: stats.bufferTotalBytes,
1237
+ externalBufferBytes: stats.externalBufferBytes,
1238
+ bufferCount: stats.bufferCount,
1239
+ gpuBytesEst: stats.gpuBytesEst,
1240
+ externalCpuBytes: stats.externalCpuBytes,
1241
+ reservedBytes: stats.reservedBytes,
1242
+ budgetBytes: stats.budgetBytes,
1243
+ evictedCount: stats.evictedCount,
1244
+ sseMultiplier: stats.sseMultiplier,
1245
+ retainedVector: stats.retainedVector,
1246
+ retainedTerrain: stats.retainedTerrain,
1247
+ retainedRaster: stats.retainedRaster,
1248
+ retainedTiles3d: stats.retainedTiles3d,
1249
+ };
1250
+ stats.free();
1251
+ return plain;
1252
+ }
1253
+
1254
+ /**
1255
+ * Snapshot of worker-side memory: per-tile-worker WASM heaps (point-in-time
1256
+ * samples from the pool's post-task probes — this call also requests fresh
1257
+ * probes, whose results show up on the *next* call) and the font worker's
1258
+ * heap/cache breakdown (`undefined` while no font has been used). Returns
1259
+ * `undefined` before `init()`.
1260
+ */
1261
+ async workerMemoryStats(): Promise<WorkerMemoryStats | undefined> {
1262
+ if (!this._initialized) return undefined;
1263
+ // Request fresh samples for the next read; probes coalesce per worker.
1264
+ probeWorkerPoolHeap();
1265
+ return {
1266
+ tileWorkers: workerPoolHeapStats(),
1267
+ fontWorker: await this._fontManager.memoryStats(),
1268
+ };
1269
+ }
1270
+
1271
+ private get globeDepthTexture() {
1272
+ return this.renderPass.globeDepthCopyPass.texture;
1273
+ }
1274
+
1275
+ private get depthTexture() {
1276
+ return this.renderPass.allDepthCopyPass.texture;
1277
+ }
1278
+
1279
+ /**
1280
+ * Returns whether mobile optimizations should be applied.
1281
+ * If `mobileOptimization` option is explicitly set, returns that value;
1282
+ * otherwise falls back to auto-detecting via `isMobileDevice()`.
1283
+ */
1284
+ isMobileOptimized(): boolean {
1285
+ const opt = this._options.mobileOptimization;
1286
+ return opt ?? isMobileDevice();
1287
+ }
1288
+
1289
+ /**
1290
+ * Forces an immediate re-render of the scene on the next frame.
1291
+ */
1292
+ forceUpdate = () => {
1293
+ this._renderFlag.forceUpdate = true;
1294
+ };
1295
+
1296
+ /**
1297
+ * Initializes the 3D engine, WASM modules, and starts the main render loop.
1298
+ * Must be called before using the view.
1299
+ */
1300
+ async init() {
1301
+ if (this._core || this._initialized) return;
1302
+
1303
+ this._initialized = true;
1304
+
1305
+ const mobileOptimized = this.isMobileOptimized();
1306
+ const concurrencyManager = createDefaultConcurrencyManager(mobileOptimized);
1307
+ if (mobileOptimized) {
1308
+ this._hillshadeContext.normalMapScale = 0.5;
1309
+ }
1310
+
1311
+ // One device-wide view of memory: the main-thread tile cache, the
1312
+ // tile-worker pool, and the font worker derive their budgets together.
1313
+ const budgets = getDefaultMemoryBudgets({
1314
+ isMobile: mobileOptimized,
1315
+ poolSize: concurrencyManager.total,
1316
+ });
1317
+
1318
+ initializeWorkerPool(WorkerURL, concurrencyManager, {
1319
+ // A worker over this WASM heap budget is recycled; tighter on mobile,
1320
+ // where the process is killed at much lower memory pressure.
1321
+ maxWorkerHeapBytes:
1322
+ this._options.memoryBudget?.maxWorkerHeapBytes ??
1323
+ budgets.maxWorkerHeapBytes,
1324
+ });
1325
+
1326
+ // 1. Pre-fetch atmosphere resources. Non-blocking: consumers receive
1327
+ // the textures through onTexturesReady() once they arrive.
1328
+ this._atmosphere._init().then(
1329
+ () => this.forceUpdate(),
1330
+ (e) => console.error("Failed to load atmosphere textures:", e),
1331
+ );
1332
+
1333
+ // 2. Fetch WASM resources.
1334
+ await Promise.all([initCore(), initNavaraApi()]);
1335
+
1336
+ this._core = new Core(newId());
1337
+ this._core.start();
1338
+
1339
+ const cacheBytes = this._options.cacheBytes ?? budgets.cacheBytes;
1340
+ this._options.cacheBytes = cacheBytes;
1341
+ this._core.setCacheBytes(cacheBytes);
1342
+
1343
+ // Cap in-flight fetches (per tile pipeline) — mobile presets shrink the
1344
+ // decode/upload burst a camera move can produce.
1345
+ this._core.setMaxPendingRequests(
1346
+ this._options.memoryBudget?.maxPendingRequests ??
1347
+ budgets.maxPendingRequests,
1348
+ );
1349
+
1350
+ // Memory-pressure SSE degrade range. min > 1 (mobile) keeps far tiles
1351
+ // coarser at rest; max is the ceiling the dynamic degrade can climb to
1352
+ // when the visible set alone exceeds the budget.
1353
+ this._core.setSseMultiplierRange(
1354
+ this._options.memoryBudget?.sseMultiplierMin ?? budgets.sseMultiplierMin,
1355
+ this._options.memoryBudget?.sseMultiplierMax ?? budgets.sseMultiplierMax,
1356
+ );
1357
+
1358
+ // Override the Rust CostHints defaults with sizes derived from this
1359
+ // device's actual composite-atlas dimensions (512² with three MRT
1360
+ // attachments, ~3MB per tile) so the ledger's per-tile GPU estimate matches.
1361
+ const costHints = getDefaultMemoryCostHints({ isMobile: mobileOptimized });
1362
+ this._core.setMemoryCostHints(
1363
+ costHints.atlasTileBytes,
1364
+ costHints.rasterTileBytes,
1365
+ );
1366
+
1367
+ // LOD fog: stronger distance-based degrade on low-memory devices.
1368
+ const lodFog = {
1369
+ ...getDefaultLodFog({ isMobile: mobileOptimized }),
1370
+ ...this._options.lodFog,
1371
+ };
1372
+ this._options.lodFog = lodFog;
1373
+ this._core.setLodFog(lodFog.enabled, lodFog.density, lodFog.sseFactor);
1374
+
1375
+ // Dynamic SSE: tilt-scaled relaxation for street-level horizon views.
1376
+ const dynamicSse = {
1377
+ ...getDefaultDynamicSse(),
1378
+ ...this._options.dynamicSse,
1379
+ };
1380
+ this._options.dynamicSse = dynamicSse;
1381
+ this._core.setDynamicSse(
1382
+ new EngineDynamicSse(
1383
+ dynamicSse.enabled,
1384
+ dynamicSse.density,
1385
+ dynamicSse.sseFactor,
1386
+ dynamicSse.heightFalloff,
1387
+ dynamicSse.minHeight,
1388
+ dynamicSse.maxHeight,
1389
+ ),
1390
+ );
1391
+
1392
+ this._fontManager.setConcurrencyManager(concurrencyManager);
1393
+ this._fontManager.setMemoryBudget(
1394
+ this._options.memoryBudget?.fontBudgetBytes ?? budgets.fontBudgetBytes,
1395
+ );
1396
+
1397
+ this._globe = new Globe(this._globeHandler, this._options);
1398
+
1399
+ // Set up Registry
1400
+ this.viewContext = new ViewContext(
1401
+ this._scenes,
1402
+ this.layersManager,
1403
+ this.renderPassOrchestrator,
1404
+ concurrencyManager,
1405
+ this._core,
1406
+ this._meshes,
1407
+ );
1408
+ this.registries = new Registries(this, this.viewContext);
1409
+ this.eventContext = new EventContext({
1410
+ eventManager: this._eventManager,
1411
+ scenes: this._scenes,
1412
+ camera: this._camera,
1413
+ meshes: this._meshes,
1414
+ abortControllers: this._abortControllers,
1415
+ buf: this._buf,
1416
+ texFragment: this._texFragment,
1417
+ tileHandler: this._tileHandler,
1418
+ workerTaskHandler: this._workerTaskHandler,
1419
+ meshHandler: this._meshHandler,
1420
+ featureHandler: this._featureHandler,
1421
+ loadedTexs: this._loadedTexs,
1422
+ workerPoolPromises: this._workerPoolPromises,
1423
+ uniforms: this._uniforms,
1424
+ texturizedSceneByTileCoordinates: this._texturizedSceneByTileCoordinates,
1425
+ tileTextureCompositor: this._tileTextureCompositor,
1426
+ tileMapByHandle: this._tileMapByHandle,
1427
+ textureOptions: this._defaultTextureOptions,
1428
+ renderFlag: this._renderFlag,
1429
+ viewEvents: this,
1430
+ layersManager: this.layersManager,
1431
+ viewContext: this.viewContext,
1432
+ layerHandler: this._layerHandler,
1433
+ fontManager: this._fontManager,
1434
+ textureFragmentIndex: this._textureFragmentIndex,
1435
+ tileMeshToFragmentIds: this._tileMeshToFragmentIds,
1436
+ hillshadeContext: this._hillshadeContext,
1437
+ });
1438
+
1439
+ // Register built-in descriptors
1440
+ this.registerBuiltIns();
1441
+
1442
+ if (!isWorker()) {
1443
+ this._eventDisposer = registerInputEvents(
1444
+ this._core,
1445
+ this._renderer.domElement,
1446
+ () => {
1447
+ const el = this._renderer.domElement;
1448
+ const rect = el.getBoundingClientRect();
1449
+ const pos = this.pickDepthPosition(
1450
+ rect.left + rect.width / 2,
1451
+ rect.top + rect.height / 2,
1452
+ );
1453
+ if (!pos) return null;
1454
+ return this._camera.raw.position.distanceTo(pos);
1455
+ },
1456
+ );
1457
+ this._pickHelper = new PickHelper(
1458
+ this._renderer.domElement,
1459
+ this._renderer,
1460
+ this._camera.raw,
1461
+ this._meshes,
1462
+ this.onPick.bind(this),
1463
+ // {
1464
+ // debug: true,
1465
+ // },
1466
+ );
1467
+ this._pickHelper.enablePick(this._options.picking ?? true);
1468
+ }
1469
+
1470
+ this.initializeRenderPass();
1471
+
1472
+ this.viewContext._setRenderPass(this.renderPass);
1473
+
1474
+ // 3. Warm up workers. Non-blocking: warms one worker, the rest follow
1475
+ // in the background off the HTTP cache; tasks dispatched to a still-cold
1476
+ // worker just await WASM inside the task.
1477
+ warmUpWorkerPool().catch((e) =>
1478
+ console.error("Failed to warm up the worker pool:", e),
1479
+ );
1480
+
1481
+ await Promise.all(this.plugins.map((p) => p.init(this, this.viewContext)));
1482
+
1483
+ this._startMainLoop();
1484
+
1485
+ const size = new Vector2();
1486
+ this._renderer.getSize(size);
1487
+ this.resize(size.width, size.height, this._renderer.getPixelRatio());
1488
+
1489
+ this._camera.core = this._core;
1490
+
1491
+ this._renderer.domElement.addEventListener("mousedown", (event) => {
1492
+ const mapEvent = this.convertMouseEventToMapEvent(event);
1493
+ if (mapEvent) {
1494
+ this.emit("mousedown", mapEvent);
1495
+ }
1496
+ });
1497
+
1498
+ this._renderer.domElement.addEventListener("mouseenter", (event) => {
1499
+ const mapEvent = this.convertMouseEventToMapEvent(event);
1500
+ if (mapEvent) {
1501
+ this.emit("mouseenter", mapEvent);
1502
+ }
1503
+ });
1504
+
1505
+ this._renderer.domElement.addEventListener("mouseleave", (event) => {
1506
+ const mapEvent = this.convertMouseEventToMapEvent(event);
1507
+ if (mapEvent) {
1508
+ this.emit("mouseleave", mapEvent);
1509
+ }
1510
+ });
1511
+
1512
+ this._renderer.domElement.addEventListener("mousemove", (event) => {
1513
+ const mapEvent = this.convertMouseEventToMapEvent(event);
1514
+ if (mapEvent) {
1515
+ this.emit("mousemove", mapEvent);
1516
+ }
1517
+ });
1518
+
1519
+ this._renderer.domElement.addEventListener("mouseup", (event) => {
1520
+ const mapEvent = this.convertMouseEventToMapEvent(event);
1521
+ if (mapEvent) {
1522
+ this.emit("mouseup", mapEvent);
1523
+ }
1524
+ });
1525
+
1526
+ this._renderer.domElement.addEventListener("click", (event) => {
1527
+ const mapEvent = this.convertMouseEventToMapEvent(event);
1528
+ if (mapEvent) {
1529
+ this.emit("click", mapEvent);
1530
+ }
1531
+ });
1532
+ }
1533
+
1534
+ /**
1535
+ * Disposes all resources and stops the render loop.
1536
+ * Call this when the view is no longer needed.
1537
+ */
1538
+ dispose() {
1539
+ this._disposed = true;
1540
+ this._initialized = false;
1541
+ // Dispose the view-owned attribution plugin (other plugins are the caller's).
1542
+ this._attribution?.dispose();
1543
+ this._attribution = undefined;
1544
+ if (!isWorker()) {
1545
+ window.removeEventListener("resize", this._handleResize);
1546
+ this.pixelRatioMatchedMedia?.removeEventListener(
1547
+ "change",
1548
+ this._handleResize,
1549
+ );
1550
+ }
1551
+
1552
+ if (this._eventDisposer) {
1553
+ this._eventDisposer();
1554
+ this._eventDisposer = undefined;
1555
+ }
1556
+
1557
+ if (this._pickHelper) {
1558
+ this._pickHelper.dispose();
1559
+ this._pickHelper = undefined;
1560
+ }
1561
+
1562
+ if (this._terrainPicker) {
1563
+ this._terrainPicker.dispose();
1564
+ }
1565
+
1566
+ this._fontManager.dispose();
1567
+ this._atmosphere._dispose();
1568
+
1569
+ this.skyEnvMap?.delete();
1570
+ this.mrtPass?.delete();
1571
+ this.transparentPass?.delete();
1572
+ this.finalPass?.delete();
1573
+
1574
+ // Abort all pending requests
1575
+ for (const controller of this._abortControllers.values()) {
1576
+ controller.abort();
1577
+ }
1578
+ this._abortControllers.clear();
1579
+
1580
+ // Dispose loaded textures (closes any ImageBitmap backing so the decoded
1581
+ // pixels are freed, not just the GL texture).
1582
+ for (const tex of this._loadedTexs.values()) {
1583
+ disposeTexture(tex);
1584
+ }
1585
+ this._loadedTexs.clear();
1586
+
1587
+ // Cleanup hillshade context (temp DEMs, generator, etc.)
1588
+ this._hillshadeContext.dispose();
1589
+
1590
+ // Clear caches and maps
1591
+ this._meshes.clear();
1592
+ this._inMemoryBufferStore.clear();
1593
+ this._workerPoolPromises.clear();
1594
+ this._tileMapByHandle.clear();
1595
+ this._textureFragmentIndex.clear();
1596
+ this._tileMeshToFragmentIds.clear();
1597
+
1598
+ // Dispose render pipeline GPU resources
1599
+ this.renderPassOrchestrator.dispose();
1600
+
1601
+ // Release per-tile composite atlases, cached shader materials, placeholder
1602
+ // textures, and the fullscreen quad geometry owned by the compositor.
1603
+ this._tileTextureCompositor.dispose();
1604
+
1605
+ // Clean up WASM core
1606
+ this._core?.free();
1607
+ this._core = undefined;
1608
+
1609
+ if (this._stats) {
1610
+ this._stats.dom.remove();
1611
+ this._stats = undefined;
1612
+ }
1613
+
1614
+ this._renderer.setAnimationLoop(null);
1615
+ if (
1616
+ "dispose" in this._renderer &&
1617
+ typeof this._renderer.dispose === "function"
1618
+ ) {
1619
+ this._renderer.dispose();
1620
+ }
1621
+
1622
+ // Remove auto-created DOM container so a new ThreeView can append its own
1623
+ this._ownedRootElement?.remove();
1624
+ this._ownedRootElement = undefined;
1625
+
1626
+ // Reset worker pool so a new ThreeView can reinitialize it
1627
+ terminateWorkerPool();
1628
+ }
1629
+
1630
+ /**
1631
+ * Resizes the renderer and updates the camera aspect ratio.
1632
+ * @param width - New width in pixels (uses canvas size if omitted)
1633
+ * @param height - New height in pixels (uses canvas size if omitted)
1634
+ * @param pixelRatio - Device pixel ratio
1635
+ */
1636
+ resize = (width?: number, height?: number, pixelRatio?: number) => {
1637
+ if (this._disposed) return;
1638
+
1639
+ const canvas = this._getCanvasSize();
1640
+ const w = typeof width === "number" ? width : canvas?.width;
1641
+ const h = typeof height === "number" ? height : canvas?.height;
1642
+ if (typeof w !== "number" || typeof h !== "number") return;
1643
+
1644
+ this._camera.raw.aspect = w / h;
1645
+ this._camera.raw.updateProjectionMatrix();
1646
+ this._renderer.setSize(w, h, !isWorker());
1647
+ this.renderPassOrchestrator.setSize(w, h);
1648
+ if (this._options.pixelRatio == null && pixelRatio) {
1649
+ this._renderer.setPixelRatio(pixelRatio);
1650
+ }
1651
+
1652
+ const drawingBufferSize = this._renderer.getDrawingBufferSize(
1653
+ new Vector2(),
1654
+ );
1655
+ this._pickHelper?.setSize(drawingBufferSize.x, drawingBufferSize.y);
1656
+
1657
+ this._core?.resize(w, h, pixelRatio ?? 1);
1658
+
1659
+ this.emit("resize", w, h);
1660
+ };
1661
+
1662
+ private _updateUniforms() {
1663
+ const viewport = this._getCanvasSize();
1664
+ const pixelRatio = this._renderer.getPixelRatio();
1665
+
1666
+ // Ref: https://github.com/CesiumGS/cesium/blob/2cf09cb06e4f7ea767da39befabcfc3444b02c49/packages/engine/Source/Core/PerspectiveFrustum.js#L208-L218
1667
+ const fovY = this._camera.fovy ?? 1;
1668
+ const top = this._camera.raw.near * Math.tan(0.5 * fovY);
1669
+ const bottom = -top;
1670
+ const right = this._camera.raw.aspect * top;
1671
+ const left = -right;
1672
+
1673
+ this._uniforms.viewportAndPixelRatio.value = [
1674
+ viewport?.width ?? 0,
1675
+ viewport?.height ?? 0,
1676
+ pixelRatio,
1677
+ ];
1678
+ this._uniforms.frustumNearFar.value = [
1679
+ this._camera.raw.near,
1680
+ this._camera.raw.far,
1681
+ ];
1682
+ this._uniforms.frustumRatio.value = [top, bottom, right, left];
1683
+ this._uniforms.tGlobeDepth.value =
1684
+ this.renderPass.globeDepthCopyPass.texture;
1685
+ this._uniforms.tGlobeNormal.value =
1686
+ this.renderPass.globeNormalCopyPass.texture;
1687
+ this._uniforms.tSkyEnvMap.value =
1688
+ this.skyEnvMap.ref.raw?.getEnvMapTexture() ?? null;
1689
+ this._uniforms.inverseProjectionMatrix.value =
1690
+ this._camera.raw.projectionMatrixInverse;
1691
+
1692
+ // TODO: Need to sync `fov` with WASM side
1693
+ this._uniforms.fov.value = (this._camera.raw.fov * Math.PI) / 180;
1694
+ this._uniforms.screenHeightPx.value = viewport?.height ?? 0;
1695
+ }
1696
+
1697
+ /** Returns true if the scene was updated and needs to be rendered. */
1698
+ private _update(updatedAt: number): boolean {
1699
+ this.emit("preUpdate", updatedAt);
1700
+
1701
+ this._core?.update(updatedAt);
1702
+
1703
+ const events = this._core?.readEvents();
1704
+ if ((!events && !this._eventManager.needsUpdate()) || !this._core) {
1705
+ return false;
1706
+ }
1707
+
1708
+ this.eventContext.updatedAt = updatedAt;
1709
+
1710
+ processEvent(this.eventContext, events);
1711
+
1712
+ events?.free();
1713
+
1714
+ // Evict JS-side `InMemoryBufferStore` entries whose WASM `External` handles
1715
+ // Rust freed this frame (tile destroy, DataRequester removal, ...), keeping
1716
+ // the two stores in step.
1717
+ this._buf.drainRemovedExternalHandles();
1718
+
1719
+ this._camera.raw.updateMatrixWorld();
1720
+
1721
+ this._updateUniforms();
1722
+
1723
+ this.emit("postUpdate", updatedAt);
1724
+
1725
+ return true;
1726
+ }
1727
+
1728
+ /**
1729
+ * Process feature updates for all descriptors
1730
+ * This is called after the main update loop to batch feature updates
1731
+ */
1732
+ private _forceFeatureUpdates(updatedAt: number) {
1733
+ // Process updates for each layer
1734
+ for (const layer of this.layersManager.getResourceLayers()) {
1735
+ if (layer._processFeatureUpdates(updatedAt)) {
1736
+ this._renderFlag.forceUpdate = true;
1737
+ }
1738
+ }
1739
+ }
1740
+
1741
+ private _render(updatedAt: number) {
1742
+ // Read the vector resolution revision once per frame so each terrain tile's
1743
+ // `onBeforeRender` can gate its slot re-fetch against it without its own FFI call.
1744
+ this._vectorRevision = this._core?.vectorRevision() ?? this._vectorRevision;
1745
+
1746
+ this._uniforms.time.value = updatedAt;
1747
+
1748
+ this._atmosphere._update();
1749
+
1750
+ this.emit("preRender", updatedAt);
1751
+
1752
+ this.renderPassOrchestrator.render();
1753
+ this._pickHelper?.renderDebugCanvas();
1754
+
1755
+ this.shadowMapViewers.render(this._renderer);
1756
+
1757
+ this.emit("postRender", updatedAt);
1758
+ }
1759
+
1760
+ /**
1761
+ * Since passing Color class to WASM is tricky, converts Navara Color
1762
+ * objects to numbers in layer descriptions.
1763
+ * Handles the two-level structure: layer -> material -> color fields.
1764
+ */
1765
+ private _convertColorsToNumbers(obj: unknown): unknown {
1766
+ if (obj === null || obj === undefined || typeof obj !== "object") {
1767
+ return obj;
1768
+ }
1769
+
1770
+ // Process the object's properties (shallow copy)
1771
+ const result: Record<string, unknown> = { ...obj };
1772
+
1773
+ for (const [key, value] of Object.entries(result)) {
1774
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1775
+ // Nested object (e.g., point, billboard, text, model, etc.)
1776
+ const nestedResult: Record<string, unknown> = { ...value };
1777
+ for (const [nestedKey, nestedValue] of Object.entries(nestedResult)) {
1778
+ if (nestedValue instanceof Color) {
1779
+ nestedResult[nestedKey] = nestedValue.toHex();
1780
+ }
1781
+ }
1782
+ result[key] = nestedResult;
1783
+ }
1784
+ }
1785
+
1786
+ return result;
1787
+ }
1788
+
1789
+ /**
1790
+ * Adds a new resource layer (GIS data layer) to the scene.
1791
+ * For meshes, lights, and effects, use `addMesh()`, `addLight()`, and `addEffect()` instead.
1792
+ * @param l - Resource layer configuration object specifying type and options
1793
+ * @returns A Layer for controlling the added layer
1794
+ */
1795
+ addLayer(l: LayerDescription): Layer {
1796
+ // Normalize a Source handle reference to its id before passing to Rust.
1797
+ const normalized =
1798
+ "source" in l && l.source instanceof Source
1799
+ ? { ...l, source: l.source.id }
1800
+ : l;
1801
+
1802
+ // Convert all Color objects to numbers before passing to Rust
1803
+ const processedLayer = this._convertColorsToNumbers(
1804
+ normalized,
1805
+ ) as LayerDescription;
1806
+
1807
+ // Existing resource layer process
1808
+ const layerId = this._core?.addLayer(processedLayer);
1809
+ invariant(layerId);
1810
+ invariant(this._core);
1811
+
1812
+ const layer = new Layer(
1813
+ layerId,
1814
+ this._core,
1815
+ this._convertColorsToNumbers.bind(this),
1816
+ );
1817
+ this.layersManager.add(layer);
1818
+
1819
+ return layer;
1820
+ }
1821
+
1822
+ /**
1823
+ * Registers a data source (GeoJSON, vector tile, raster tile, raster DEM,
1824
+ * quantized mesh, 3D Tiles) and returns a {@link Source} handle.
1825
+ * Reference the returned source from layers via `addLayer({ source })`.
1826
+ * @param s - Source configuration object specifying type and options
1827
+ * @returns A Source handle for referencing, updating, and deleting the source
1828
+ */
1829
+ addSource(s: SourceDescription): Source {
1830
+ invariant(this._core);
1831
+ invariant(s.type);
1832
+ const sourceId = this._core.addSource(s);
1833
+
1834
+ return new Source(sourceId, s.type, this._core);
1835
+ }
1836
+
1837
+ /**
1838
+ * Adds a 3D mesh to the scene.
1839
+ * The mesh kind is determined by the nested key (e.g., `{ box: { width: 200 } }`).
1840
+ * @param desc - Mesh configuration object
1841
+ * @returns A MeshHandle for controlling the added mesh
1842
+ */
1843
+ addMesh<L extends AnyMeshDesc = AnyMeshDesc>(
1844
+ config: OmitType<MeshConfig | NonNullable<D["mesh"]>>,
1845
+ ): MeshHandle<L> {
1846
+ // Find which mesh type from config
1847
+ const meshType = this.registries.mesh.findMeshType(config);
1848
+ if (!meshType) {
1849
+ throw new UnknownTypeError("mesh", config);
1850
+ }
1851
+
1852
+ // Create mesh descriptor instance
1853
+ const meshDesc = this.registries.mesh.create(meshType, config);
1854
+
1855
+ // Initialize the mesh
1856
+ meshDesc.onCreate();
1857
+
1858
+ // Set up update listener
1859
+ if (meshDesc.update) {
1860
+ this.on("preRender", meshDesc.update.bind(meshDesc));
1861
+ }
1862
+
1863
+ if (meshDesc.onResize) {
1864
+ this.on("resize", meshDesc.onResize.bind(meshDesc));
1865
+
1866
+ const canvasSize = this._getCanvasSize();
1867
+ if (canvasSize) {
1868
+ meshDesc.onResize(canvasSize.width, canvasSize.height);
1869
+ }
1870
+ }
1871
+
1872
+ // Trigger re-render
1873
+ meshDesc.on("needsUpdate", this.forceUpdate);
1874
+
1875
+ const l = new MeshHandle(meshDesc);
1876
+
1877
+ // Store the mesh descriptor
1878
+ this.layersManager.add(l);
1879
+
1880
+ // Return handle for imperative access
1881
+ return l as MeshHandle<L>;
1882
+ }
1883
+
1884
+ /**
1885
+ * Adds a light to the scene.
1886
+ * The light kind is determined by the nested key (e.g., `{ ambient: { intensity: 0.5 } }`).
1887
+ * @param desc - Light configuration object
1888
+ * @returns A LightHandle for controlling the added light
1889
+ */
1890
+ addLight<L extends LightDesc = LightDesc>(
1891
+ config: OmitType<LightConfig | NonNullable<D["light"]>>,
1892
+ ): LightHandle<L> {
1893
+ // Find which light type from config
1894
+ const lightType = this.registries.light.findLightType(config);
1895
+ if (!lightType) {
1896
+ throw new UnknownTypeError("light", config);
1897
+ }
1898
+
1899
+ // Create light descriptor instance
1900
+ const lightDesc = this.registries.light.create(lightType, config);
1901
+
1902
+ // Initialize the light
1903
+ lightDesc.onCreate();
1904
+
1905
+ // Set up update listener if the descriptor has an update method
1906
+ if (lightDesc.update) {
1907
+ this.on("preRender", lightDesc.update.bind(lightDesc));
1908
+ }
1909
+
1910
+ // Trigger re-render
1911
+ lightDesc.on("needsUpdate", this.forceUpdate);
1912
+
1913
+ const l = new LightHandle(lightDesc);
1914
+
1915
+ // Store the light descriptor
1916
+ this.layersManager.add(l);
1917
+
1918
+ // Return handle for imperative access
1919
+ return l as LightHandle<L>;
1920
+ }
1921
+
1922
+ /**
1923
+ * Adds a post-processing effect to the scene.
1924
+ * The effect kind is determined by the nested key (e.g., `{ bloom: { strength: 1.0 } }`).
1925
+ * @param desc - Effect configuration object
1926
+ * @returns An EffectHandle for controlling the added effect
1927
+ */
1928
+ addEffect<L extends EffectDesc = EffectDesc>(
1929
+ config: OmitType<
1930
+ BuiltInEffectDescription | EffectConfig | NonNullable<D["effect"]>
1931
+ >,
1932
+ ): EffectHandle<L> {
1933
+ // Find which effect type from config
1934
+ const effectType = this.registries.effect.findEffectType(config);
1935
+ if (!effectType) {
1936
+ throw new UnknownTypeError("effect", config);
1937
+ }
1938
+
1939
+ // Create effect descriptor instance
1940
+ const effectDesc = this.registries.effect.create(effectType, config);
1941
+
1942
+ // Initialize the effect
1943
+ effectDesc.onCreate();
1944
+
1945
+ // Set up update listener if the descriptor has an update method
1946
+ if (effectDesc.update) {
1947
+ this.on("preRender", effectDesc.update.bind(effectDesc));
1948
+ }
1949
+
1950
+ // Trigger re-render
1951
+ effectDesc.on("needsUpdate", this.forceUpdate);
1952
+
1953
+ const l = new EffectHandle(effectDesc);
1954
+
1955
+ // Store the effect descriptor
1956
+ this.layersManager.add(l);
1957
+
1958
+ // Return handle for imperative access
1959
+ return l as EffectHandle<L>;
1960
+ }
1961
+
1962
+ /**
1963
+ * Finds a resource layer by its ID.
1964
+ * Only returns resource layers added via `addLayer()` (not mesh, light, or
1965
+ * effect descriptors).
1966
+ * @param id - The unique identifier of the layer to find
1967
+ * @returns The matching {@link Layer}, or `undefined` if none exists
1968
+ */
1969
+ findLayerById(id: string): Layer | undefined {
1970
+ const target = this.layersManager.get(id);
1971
+ return target instanceof Layer ? target : undefined;
1972
+ }
1973
+
1974
+ /**
1975
+ * Updates an existing resource layer's configuration by its ID.
1976
+ * Only works for resource layers added via `addLayer()`.
1977
+ * @param id - The unique identifier of the layer to update
1978
+ * @param l - New layer configuration
1979
+ */
1980
+ updateLayerById(id: string, l: LayerDescription) {
1981
+ invariant(this._core);
1982
+ const target = this.layersManager.get(id);
1983
+ if (!target || !(target instanceof Layer)) return;
1984
+ target.update(l);
1985
+ }
1986
+
1987
+ /**
1988
+ * Updates an existing mesh descriptor's configuration by its ID.
1989
+ * @param id - The unique identifier of the mesh to update
1990
+ * @param updates - Partial configuration object with properties to update
1991
+ */
1992
+ updateMeshById(
1993
+ id: string,
1994
+ updates: OmitType<MeshConfig | NonNullable<D["mesh"]>>,
1995
+ ) {
1996
+ const target = this.layersManager.get(id);
1997
+ if (!target || !(target instanceof MeshHandle)) return;
1998
+ target.update(updates);
1999
+ }
2000
+
2001
+ /**
2002
+ * Updates an existing light descriptor's configuration by its ID.
2003
+ * @param id - The unique identifier of the light to update
2004
+ * @param updates - Partial configuration object with properties to update
2005
+ */
2006
+ updateLightById(
2007
+ id: string,
2008
+ updates: OmitType<LightConfig | NonNullable<D["light"]>>,
2009
+ ) {
2010
+ const target = this.layersManager.get(id);
2011
+ if (!target || !(target instanceof LightHandle)) return;
2012
+ target.update(updates);
2013
+ }
2014
+
2015
+ /**
2016
+ * Updates an existing effect descriptor's configuration by its ID.
2017
+ * @param id - The unique identifier of the effect to update
2018
+ * @param updates - Partial configuration object with properties to update
2019
+ */
2020
+ updateEffectById(
2021
+ id: string,
2022
+ updates: OmitType<
2023
+ BuiltInEffectDescription | EffectConfig | NonNullable<D["effect"]>
2024
+ >,
2025
+ ) {
2026
+ const target = this.layersManager.get(id);
2027
+ if (!target || !(target instanceof EffectHandle)) return;
2028
+ target.update(updates);
2029
+ }
2030
+
2031
+ /**
2032
+ * Deletes a resource layer from the scene by its ID.
2033
+ * @param id - The unique identifier of the layer to delete
2034
+ * @returns `true` if the layer was found and deleted, `false` otherwise
2035
+ */
2036
+ deleteLayerById(id: string): boolean {
2037
+ invariant(this._core);
2038
+ const target = this.layersManager.get(id);
2039
+ if (!target || !(target instanceof Layer)) return false;
2040
+ target.delete();
2041
+ return true;
2042
+ }
2043
+
2044
+ /**
2045
+ * Deletes a mesh descriptor from the scene by its ID.
2046
+ * @param id - The unique identifier of the mesh to delete
2047
+ * @returns `true` if the mesh was found and deleted, `false` otherwise
2048
+ */
2049
+ deleteMeshById(id: string): boolean {
2050
+ const target = this.layersManager.get(id);
2051
+ if (!target || !(target instanceof MeshHandle)) return false;
2052
+ target.delete();
2053
+ return true;
2054
+ }
2055
+
2056
+ /**
2057
+ * Deletes a light descriptor from the scene by its ID.
2058
+ * @param id - The unique identifier of the light to delete
2059
+ * @returns `true` if the light was found and deleted, `false` otherwise
2060
+ */
2061
+ deleteLightById(id: string): boolean {
2062
+ const target = this.layersManager.get(id);
2063
+ if (!target || !(target instanceof LightHandle)) return false;
2064
+ target.delete();
2065
+ return true;
2066
+ }
2067
+
2068
+ /**
2069
+ * Deletes an effect descriptor from the scene by its ID.
2070
+ * @param id - The unique identifier of the effect to delete
2071
+ * @returns `true` if the effect was found and deleted, `false` otherwise
2072
+ */
2073
+ deleteEffectById(id: string): boolean {
2074
+ const target = this.layersManager.get(id);
2075
+ if (!target || !(target instanceof EffectHandle)) return false;
2076
+ target.delete();
2077
+ return true;
2078
+ }
2079
+
2080
+ private registerBuiltIns(): void {
2081
+ this.registerBuiltInEffects();
2082
+ }
2083
+
2084
+ private registerBuiltInEffects(): void {
2085
+ this.registerEffect("skyEnvMap", SkyEnvMapEffectDesc);
2086
+ this.registerEffect("mrt", MRTPassEffectDesc);
2087
+ this.registerEffect("transparent", TransparentPassEffectDesc);
2088
+
2089
+ this.registerEffect("final", FinalCopyEffectDesc);
2090
+ }
2091
+
2092
+ /**
2093
+ * Registers a custom mesh descriptor type for use with addMesh().
2094
+ * @param name - Unique name to identify this mesh type in descriptor configurations
2095
+ * @param meshClass - The mesh descriptor class constructor
2096
+ */
2097
+ registerMesh(name: string, meshClass: MeshDescConstructor): void {
2098
+ this.registries.mesh.register(name, meshClass);
2099
+ }
2100
+
2101
+ /**
2102
+ * Registers a custom light descriptor type for use with addLight().
2103
+ * @param name - Unique name to identify this light type in descriptor configurations
2104
+ * @param lightClass - The light descriptor class constructor
2105
+ */
2106
+ registerLight(name: string, lightClass: LightDescConstructor): void {
2107
+ this.registries.light.register(name, lightClass);
2108
+ }
2109
+
2110
+ /**
2111
+ * Registers a custom post-processing effect descriptor type for use with addEffect().
2112
+ * @param name - Unique name to identify this effect type in descriptor configurations
2113
+ * @param effectClass - The effect descriptor class constructor
2114
+ */
2115
+ registerEffect(name: string, effectClass: EffectDescConstructor): void {
2116
+ this.registries.effect.register(name, effectClass);
2117
+ }
2118
+
2119
+ /**
2120
+ * Adds a plugin to the view. Plugins are initialized during `init()`.
2121
+ * Must be called before `init()`.
2122
+ * @param plugin - The plugin instance to add
2123
+ * @returns This view instance for chaining
2124
+ */
2125
+ addPlugin(plugin: Plugin): this {
2126
+ if (this._initialized)
2127
+ throw new Error("Plugin must be added before `view.init()`.");
2128
+ this.plugins.push(plugin);
2129
+ return this;
2130
+ }
2131
+
2132
+ /**
2133
+ * Register a font family with multiple faces.
2134
+ * Each face covers a set of unicode ranges and points to a separate font file URL.
2135
+ * When a text label uses this family name as its font, only the face files
2136
+ * whose unicode ranges cover the label's characters are loaded.
2137
+ *
2138
+ * Face priority and fallback:
2139
+ * - Faces are evaluated in the order given in `faces`. For each codepoint,
2140
+ * the first face whose `unicodeRanges` includes that codepoint is used,
2141
+ * so earlier entries win when ranges overlap.
2142
+ * - Codepoints not covered by any face fall back to the first face
2143
+ * (`faces[0]`), which may therefore be downloaded for uncovered characters
2144
+ * even if its declared `unicodeRanges` do not include them.
2145
+ *
2146
+ * Put the intended fallback face at index `0`: it is both the fallback for
2147
+ * uncovered codepoints and the highest-priority face when ranges overlap.
2148
+ * Order the remaining faces after it by priority, and avoid overlapping
2149
+ * ranges unless that precedence is intentional.
2150
+ *
2151
+ * @example
2152
+ * ```ts
2153
+ * view.addFontFamily({
2154
+ * family: "MapFont",
2155
+ * faces: [
2156
+ * {
2157
+ * url: "/fonts/latin.woff2",
2158
+ * unicodeRanges: [{ from: 0x0000, to: 0x024f }],
2159
+ * },
2160
+ * {
2161
+ * url: "/fonts/cjk.woff2",
2162
+ * unicodeRanges: [{ from: 0x4e00, to: 0x9fff }],
2163
+ * },
2164
+ * ],
2165
+ * });
2166
+ * ```
2167
+ *
2168
+ * Instead of writing faces and ranges by hand, they can be derived from a
2169
+ * stylesheet's `@font-face` rules (e.g. the Google Fonts CSS API) with
2170
+ * {@link fetchFontFamilyFromCss} / {@link parseFontFamilyFromCss}:
2171
+ *
2172
+ * @example
2173
+ * ```ts
2174
+ * view.addFontFamily(
2175
+ * await fetchFontFamilyFromCss(
2176
+ * "MapFont",
2177
+ * "https://fonts.googleapis.com/css2?family=Noto+Sans&family=Noto+Sans+JP",
2178
+ * ),
2179
+ * );
2180
+ * ```
2181
+ */
2182
+ addFontFamily(family: FontFamily): this {
2183
+ this._fontManager.registerFontFamily(family);
2184
+ return this;
2185
+ }
2186
+
2187
+ /** Remove a previously registered font family by name. */
2188
+ removeFontFamily(family: string): this {
2189
+ this._fontManager.unregisterFontFamily(family);
2190
+ return this;
2191
+ }
2192
+
2193
+ /**
2194
+ * Sets the camera position and orientation instantly.
2195
+ * @param camPos - Camera position with lng (degrees), lat (degrees), height (meters), and optional pitch, heading, roll (degrees)
2196
+ */
2197
+ setCamera(camPos: CameraPosition) {
2198
+ function checkFinite(value: number | undefined): value is number {
2199
+ return Number.isFinite(value) && value != null;
2200
+ }
2201
+
2202
+ const lng = camPos.lng;
2203
+ const lat = camPos.lat;
2204
+ const position =
2205
+ checkFinite(lng) && checkFinite(lat)
2206
+ ? new Float64Array([lng, lat, camPos.height ?? 0])
2207
+ : null;
2208
+
2209
+ this._core?.changeCamera(
2210
+ position,
2211
+ camPos.pitch,
2212
+ camPos.heading,
2213
+ camPos.roll,
2214
+ camPos.distance,
2215
+ );
2216
+ }
2217
+
2218
+ /**
2219
+ * Moves the camera in a specified direction.
2220
+ * @param move - Direction: `CameraDirection`
2221
+ * @param amount - Distance to move in meters
2222
+ */
2223
+ moveCamera(move: CameraDirection, amount: number) {
2224
+ switch (move) {
2225
+ case CameraDirection.Forward:
2226
+ this._core?.moveCamera(CameraDirection.Forward, amount);
2227
+ break;
2228
+ case CameraDirection.Backward:
2229
+ this._core?.moveCamera(CameraDirection.Backward, amount);
2230
+ break;
2231
+ case CameraDirection.Up:
2232
+ this._core?.moveCamera(CameraDirection.Up, amount);
2233
+ break;
2234
+ case CameraDirection.Down:
2235
+ this._core?.moveCamera(CameraDirection.Down, amount);
2236
+ break;
2237
+ case CameraDirection.Left:
2238
+ this._core?.moveCamera(CameraDirection.Left, amount);
2239
+ break;
2240
+ case CameraDirection.Right:
2241
+ this._core?.moveCamera(CameraDirection.Right, amount);
2242
+ break;
2243
+ default:
2244
+ break;
2245
+ }
2246
+ }
2247
+
2248
+ /**
2249
+ * Moves the camera along a custom direction vector.
2250
+ * @param dir - Direction vector as [x, y, z] array
2251
+ * @param amount - Distance to move in meters
2252
+ */
2253
+ moveCameraWithDirection(dir: number[], amount: number) {
2254
+ if (dir.length !== 3) {
2255
+ return;
2256
+ }
2257
+
2258
+ this._core?.moveCameraWithDirection(new Float64Array(dir), amount);
2259
+ }
2260
+
2261
+ /**
2262
+ * Animates the camera to fly to a target position.
2263
+ * @param camPos - Target position with required lng/lat (degrees). Provide either `height` (meters above the ellipsoid)
2264
+ * or `distance` (meters from the target point along the camera forward direction). Optional pitch, heading, roll (degrees).
2265
+ * @param duration - Animation duration in milliseconds
2266
+ * @param maxHeight - Maximum height during the flight arc in meters
2267
+ */
2268
+ flyTo(
2269
+ camPos: CameraPosition &
2270
+ Required<Pick<CameraPosition, "lng" | "lat">> &
2271
+ ({ height: number } | { distance: number }),
2272
+ duration?: number,
2273
+ maxHeight?: number,
2274
+ ) {
2275
+ const pos = camPos as CameraPosition & { lng: number; lat: number };
2276
+ const position = new Float64Array([pos.lng, pos.lat, pos.height ?? 0]);
2277
+
2278
+ this._core?.flyTo(
2279
+ position,
2280
+ pos.pitch,
2281
+ pos.heading,
2282
+ pos.roll,
2283
+ duration,
2284
+ maxHeight,
2285
+ pos.distance,
2286
+ );
2287
+ }
2288
+
2289
+ /**
2290
+ * Makes the camera look at a target position with an offset.
2291
+ * @param target - Target geodetic position (lng in degrees, lat in degrees, height in meters)
2292
+ * @param offset - Offset from the target in East-North-Up (ENU) coordinates (meters)
2293
+ */
2294
+ lookAt(target: LatLngHeight, offset: Vector3) {
2295
+ this._core?.lookAt(
2296
+ new Float64Array([target.lng, target.lat, target.height]),
2297
+ new Float64Array([offset.x, offset.y, offset.z]),
2298
+ );
2299
+ }
2300
+
2301
+ /**
2302
+ * Enables or disables camera following mode.
2303
+ * @param enabled - Whether to enable camera following
2304
+ * @param target - Target geodetic position to follow (lng in degrees, lat in degrees, height in meters)
2305
+ * @param offset - Offset from the target in East-North-Up (ENU) coordinates (meters)
2306
+ */
2307
+ cameraFollow(enabled: boolean, target?: LatLngHeight, offset?: Vector3) {
2308
+ const targetArray = target
2309
+ ? new Float64Array([target.lng, target.lat, target.height])
2310
+ : undefined;
2311
+ const offsetArray = offset
2312
+ ? new Float64Array([offset.x, offset.y, offset.z])
2313
+ : undefined;
2314
+
2315
+ this._core?.cameraFollow(enabled, targetArray, offsetArray);
2316
+ }
2317
+
2318
+ /**
2319
+ * Enables position-locked free-look. The camera stays at the target
2320
+ * position while mouse drag rotates orientation in place. Use for
2321
+ * first-person-view "look around" controls.
2322
+ * @param enabled - Whether to enable free-look mode
2323
+ * @param target - Target geodetic position the camera is locked to (lng/lat degrees, height meters)
2324
+ */
2325
+ cameraFreeLook(enabled: boolean, target?: LatLngHeight) {
2326
+ const targetArray = target
2327
+ ? new Float64Array([target.lng, target.lat, target.height])
2328
+ : undefined;
2329
+
2330
+ this._core?.cameraFreeLook(enabled, targetArray);
2331
+ }
2332
+
2333
+ /**
2334
+ * Samples the terrain height at a given geodetic position synchronously.
2335
+ * @param pos - Geodetic position (lat in radians, lng in radians; height is ignored)
2336
+ * @returns Terrain height in meters, or undefined if terrain data not loaded
2337
+ */
2338
+ sampleTerrainHeight(pos: LatLngHeight): number | undefined {
2339
+ const lle = new LLE(pos.lat, pos.lng, 0);
2340
+ return this._core?.sampleTerrainHeight(lle);
2341
+ }
2342
+
2343
+ /**
2344
+ * Observes terrain height changes at a position. Callback is invoked each time terrain data updates.
2345
+ * @param pos - Geodetic position to observe (lat in radians, lng in radians)
2346
+ * @param cb - Callback function receiving the terrain height in meters
2347
+ * @returns Cleanup function to stop observing
2348
+ */
2349
+ observeTerrainHeightAt(
2350
+ pos: LatLng,
2351
+ cb: (height: number) => void,
2352
+ ): () => void {
2353
+ if (!this._core) {
2354
+ return () => {};
2355
+ }
2356
+
2357
+ const lle = new LLE(pos.lat, pos.lng, 0);
2358
+ const entityBits = this._core.registerSampleTerrainHeightEvent(lle);
2359
+
2360
+ const callFunc = (ev: TerrainHeightUpdatedEvent) => {
2361
+ if (ev.bits === entityBits && ev.height) {
2362
+ cb(ev.height);
2363
+ }
2364
+ };
2365
+
2366
+ this.on("_sample_terrain_height_received", callFunc);
2367
+
2368
+ return () => {
2369
+ this._core?.unregisterSampleTerrainHeightEvent(entityBits);
2370
+ this.off("_sample_terrain_height_received", callFunc);
2371
+ };
2372
+ }
2373
+
2374
+ /**
2375
+ * Rotates the camera around an axis.
2376
+ * @param axis - Axis to rotate around (zero vector uses default)
2377
+ * @param angle - Rotation angle in radians
2378
+ */
2379
+ rotateAroundAxis(axis: Vector3, angle: number) {
2380
+ this._core?.rotateAroundAxis(
2381
+ new Float64Array([axis.x, axis.y, axis.z]),
2382
+ angle,
2383
+ );
2384
+ }
2385
+
2386
+ /**
2387
+ * Rotates the camera around the current look-at point or center of view.
2388
+ * @param angle - Rotation angle in radians
2389
+ */
2390
+ rotateAround(angle: number) {
2391
+ this._core?.rotateAroundAxis(null, angle);
2392
+ }
2393
+
2394
+ private _startMainLoop() {
2395
+ const idleThreshold = this._options.idleThreshold ?? 100;
2396
+ const loop: XRFrameRequestCallback = (time) => {
2397
+ if (this._disposed) return;
2398
+ this._stats?.begin();
2399
+
2400
+ this._forceFeatureUpdates(time);
2401
+
2402
+ const updated = this._update(time);
2403
+ if (updated || this._renderFlag.forceUpdate || this._renderFlag.animation)
2404
+ this._render(time);
2405
+ this._renderFlag.forceUpdate = false;
2406
+
2407
+ if (updated) {
2408
+ this._isIdle = false;
2409
+ } else if (
2410
+ !this._isIdle &&
2411
+ this.eventContext.updatedAt > 0 &&
2412
+ time - this.eventContext.updatedAt >= idleThreshold
2413
+ ) {
2414
+ this._isIdle = true;
2415
+ this.emit("idle");
2416
+ }
2417
+
2418
+ this._stats?.end();
2419
+ };
2420
+ this._renderer.setAnimationLoop(loop);
2421
+ }
2422
+
2423
+ private _getCanvasSize(): { width: number; height: number } | undefined {
2424
+ const element =
2425
+ this._options.container ??
2426
+ this._renderer.domElement?.parentElement ??
2427
+ this._renderer.domElement;
2428
+ if (!element) return;
2429
+
2430
+ const width = element.offsetWidth;
2431
+ const height = element.offsetHeight;
2432
+ if (typeof width !== "number" && typeof height !== "number") {
2433
+ return;
2434
+ }
2435
+
2436
+ return { width, height };
2437
+ }
2438
+
2439
+ private _handleResize = () => {
2440
+ const { width, height } = this._getCanvasSize() ?? {};
2441
+ if (!width || !height) return;
2442
+
2443
+ const pixelRatio = isWorker()
2444
+ ? (this._options.pixelRatio ?? 1)
2445
+ : window.devicePixelRatio;
2446
+ this.resize(width, height, pixelRatio);
2447
+ };
2448
+
2449
+ /**
2450
+ * Handles pick events and emits the picked feature information.
2451
+ * @param pickArr - Array of picked batch IDs
2452
+ */
2453
+ private onPick(pickArr: number[]) {
2454
+ this._renderFlag.forceUpdate = true;
2455
+
2456
+ if (pickArr.length > 0) {
2457
+ const batchId = pickArr[0];
2458
+ const prop = this._core?.readPropertyByGlobalBatchId(batchId);
2459
+ if (prop) {
2460
+ const pickedFeature: PickedFeature = {
2461
+ properties: prop.properties,
2462
+ batchId,
2463
+ layerId: prop.layerId,
2464
+ };
2465
+ this.emit("pick", pickedFeature);
2466
+ } else {
2467
+ const emptyFeature: PickedFeature = {
2468
+ properties: {},
2469
+ batchId,
2470
+ layerId: undefined,
2471
+ };
2472
+ this.emit("pick", emptyFeature);
2473
+ }
2474
+ } else {
2475
+ this.emit("pick", null);
2476
+ }
2477
+ }
2478
+
2479
+ /**
2480
+ * Gets whether continuous animation mode is enabled.
2481
+ */
2482
+ get animation() {
2483
+ return this._renderFlag.animation;
2484
+ }
2485
+ /**
2486
+ * Sets whether to render every frame continuously (true) or only on changes (false).
2487
+ */
2488
+ set animation(v: boolean) {
2489
+ this._renderFlag.animation = v;
2490
+ }
2491
+
2492
+ /**
2493
+ * Gets the current screen size in pixels.
2494
+ */
2495
+ get screenSize() {
2496
+ const size = new Vector2();
2497
+ this._renderer.getSize(size);
2498
+ return size;
2499
+ }
2500
+
2501
+ /**
2502
+ * Gets the current device pixel ratio.
2503
+ */
2504
+ get pixelRatio() {
2505
+ return this._renderer.getPixelRatio();
2506
+ }
2507
+
2508
+ /**
2509
+ * Gets whether shadow map debug viewers are displayed.
2510
+ */
2511
+ get shadowMapViewersEnabled() {
2512
+ return this.shadowMapViewers.enabled;
2513
+ }
2514
+ /**
2515
+ * Enables or disables shadow map debug viewers on screen.
2516
+ */
2517
+ set shadowMapViewersEnabled(v: boolean) {
2518
+ this.shadowMapViewers.enabled = v;
2519
+ }
2520
+
2521
+ /**
2522
+ * Picks the terrain position at screen coordinates.
2523
+ * @param x - Screen X coordinate in CSS pixels (same as MouseEvent.clientX)
2524
+ * @param y - Screen Y coordinate in CSS pixels (same as MouseEvent.clientY)
2525
+ * @returns World position Vector3 in ECEF coordinates, or null if no terrain is hit
2526
+ */
2527
+ pickTerrainPosition(x: number, y: number): Nullable<Vector3> {
2528
+ return this._terrainPicker.pick(
2529
+ x,
2530
+ y,
2531
+ this._renderer,
2532
+ this.globeDepthTexture,
2533
+ this._camera.raw,
2534
+ );
2535
+ }
2536
+
2537
+ /**
2538
+ * Picks the position at screen coordinates from the depth.
2539
+ * @param x - Screen X coordinate in CSS pixels (same as MouseEvent.clientX)
2540
+ * @param y - Screen Y coordinate in CSS pixels (same as MouseEvent.clientY)
2541
+ * @returns World position Vector3 in ECEF coordinates, or null if nothing is hit
2542
+ */
2543
+ pickDepthPosition(x: number, y: number): Nullable<Vector3> {
2544
+ return this._terrainPicker.pick(
2545
+ x,
2546
+ y,
2547
+ this._renderer,
2548
+ this.depthTexture,
2549
+ this._camera.raw,
2550
+ );
2551
+ }
2552
+ }
2553
+
2554
+ function newId() {
2555
+ return Math.random().toString(36).slice(2);
2556
+ }