@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
@@ -0,0 +1,254 @@
1
+ import type { Nullable } from "@navaramap/core";
2
+ import {
3
+ WebGLRenderer,
4
+ WebGLRenderTarget,
5
+ Vector2,
6
+ Vector3,
7
+ Vector4,
8
+ PerspectiveCamera,
9
+ Scene,
10
+ RGBAFormat,
11
+ Texture,
12
+ ShaderMaterial,
13
+ PlaneGeometry,
14
+ Mesh,
15
+ OrthographicCamera,
16
+ } from "three";
17
+
18
+ class DepthPickPass {
19
+ private quad: Mesh;
20
+ private scene: Scene;
21
+ private camera: OrthographicCamera;
22
+ private sampleTarget: WebGLRenderTarget;
23
+ private material: ShaderMaterial;
24
+
25
+ constructor() {
26
+ // Create reusable resources
27
+ this.material = new ShaderMaterial({
28
+ uniforms: {
29
+ tDepth: { value: null },
30
+ samplePos: { value: new Vector2() },
31
+ },
32
+ vertexShader: `
33
+ varying vec2 vUv;
34
+ void main() {
35
+ vUv = uv;
36
+ gl_Position = vec4(position, 1.0);
37
+ }
38
+ `,
39
+ fragmentShader: `
40
+ uniform sampler2D tDepth;
41
+ uniform vec2 samplePos;
42
+ varying vec2 vUv;
43
+
44
+ void main() {
45
+ vec4 depthColor = texture2D(tDepth, samplePos);
46
+ gl_FragColor = depthColor;
47
+ }
48
+ `,
49
+ });
50
+
51
+ const geometry = new PlaneGeometry(2, 2);
52
+ this.quad = new Mesh(geometry, this.material);
53
+
54
+ this.scene = new Scene();
55
+ this.scene.add(this.quad);
56
+
57
+ this.camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
58
+ this.sampleTarget = new WebGLRenderTarget(1, 1, { format: RGBAFormat });
59
+ }
60
+
61
+ update(depthTexture: Texture, samplePos: Vector2) {
62
+ this.material.uniforms.tDepth.value = depthTexture;
63
+ this.material.uniforms.samplePos.value.copy(samplePos);
64
+ }
65
+
66
+ render(renderer: WebGLRenderer): Uint8Array {
67
+ renderer.setRenderTarget(this.sampleTarget);
68
+ renderer.render(this.scene, this.camera);
69
+ renderer.setRenderTarget(null);
70
+
71
+ // Read the pixel
72
+ const pixels = new Uint8Array(4);
73
+ renderer.readRenderTargetPixels(this.sampleTarget, 0, 0, 1, 1, pixels);
74
+
75
+ return pixels;
76
+ }
77
+
78
+ dispose() {
79
+ this.quad.geometry.dispose();
80
+ this.material.dispose();
81
+ this.sampleTarget.dispose();
82
+ }
83
+ }
84
+
85
+ export class TerrainPicker {
86
+ private depthPickPass: DepthPickPass;
87
+
88
+ constructor() {
89
+ this.depthPickPass = new DepthPickPass();
90
+ }
91
+ pick(
92
+ x: number,
93
+ y: number,
94
+ renderer: WebGLRenderer,
95
+ depthTexture: Texture,
96
+ camera: PerspectiveCamera,
97
+ ): Nullable<Vector3> {
98
+ const logDepthOrDepth = this._sampleDepthAt(x, y, renderer, depthTexture);
99
+ if (logDepthOrDepth === null || logDepthOrDepth > 0.99) {
100
+ return null;
101
+ }
102
+
103
+ return this._reconstructWorldPosition(
104
+ x,
105
+ y,
106
+ logDepthOrDepth,
107
+ renderer,
108
+ camera,
109
+ );
110
+ }
111
+
112
+ // Ref: https://github.com/mrdoob/three.js/blob/f38421e7bf5bc37aac7d4ebbe66ad0cc15550c39/src/renderers/shaders/ShaderChunk/packing.glsl.js#L56-L58
113
+ private _unpackRGBAToDepth(rgba: Uint8Array): number {
114
+ // Constants
115
+ const UnpackDownscale = 255 / 256; // 0..1 -> fraction (excluding 1)
116
+ const PackFactors = [1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0];
117
+
118
+ // Calculate unpack factors
119
+ const UnpackFactors4 = [
120
+ UnpackDownscale / PackFactors[0],
121
+ UnpackDownscale / PackFactors[1],
122
+ UnpackDownscale / PackFactors[2],
123
+ 1.0 / PackFactors[3],
124
+ ];
125
+
126
+ // Calculate dot product
127
+ return (
128
+ (rgba[0] / 256.0) * UnpackFactors4[0] +
129
+ (rgba[1] / 256.0) * UnpackFactors4[1] +
130
+ (rgba[2] / 256.0) * UnpackFactors4[2] +
131
+ (rgba[3] / 256.0) * UnpackFactors4[3]
132
+ );
133
+ }
134
+
135
+ // Helper function to sample depth from depth texture at screen position
136
+ private _sampleDepthAt(
137
+ x: number,
138
+ y: number,
139
+ renderer: WebGLRenderer,
140
+ depthTexture: Texture,
141
+ ): number | null {
142
+ if (!depthTexture) {
143
+ return null;
144
+ }
145
+
146
+ const width = renderer.getContext().drawingBufferWidth;
147
+ const height = renderer.getContext().drawingBufferHeight;
148
+ const pixelRatio = renderer.getPixelRatio();
149
+
150
+ // Clamp coordinates to screen bounds
151
+ const clampedX = Math.max(
152
+ 0,
153
+ Math.min(width - 1, Math.floor(x * pixelRatio)),
154
+ );
155
+ const clampedY = Math.max(
156
+ 0,
157
+ Math.min(height - 1, Math.floor(y * pixelRatio)),
158
+ );
159
+
160
+ // Update the depth pick pass with current parameters
161
+ const samplePos = new Vector2(clampedX / width, 1.0 - clampedY / height); // Flip Y
162
+ this.depthPickPass.update(depthTexture, samplePos);
163
+
164
+ // Render and get pixels
165
+ const pixels = this.depthPickPass.render(renderer);
166
+
167
+ // Unpack RGBA to depth using the same formula as the shader
168
+ return this._unpackRGBAToDepth(pixels);
169
+ }
170
+
171
+ dispose() {
172
+ this.depthPickPass.dispose();
173
+ }
174
+
175
+ private _reconstructWorldPosition(
176
+ x: number,
177
+ y: number,
178
+ depth: number,
179
+ renderer: WebGLRenderer,
180
+ camera: PerspectiveCamera,
181
+ ): Vector3 {
182
+ const near = camera.near;
183
+ const far = camera.far;
184
+
185
+ const width = renderer.getContext().drawingBufferWidth;
186
+ const height = renderer.getContext().drawingBufferHeight;
187
+ const pixelRatio = renderer.getPixelRatio();
188
+
189
+ // Convert screen coordinates to NDC [-1,1]
190
+ const screenCoords = new Vector2(
191
+ ((x * pixelRatio) / width) * 2.0 - 1.0,
192
+ -(((y * pixelRatio) / height) * 2.0 - 1.0),
193
+ );
194
+
195
+ let clipCoords: Vector4;
196
+
197
+ if (renderer.capabilities.logarithmicDepthBuffer) {
198
+ const logDepthBufFC = 2.0 / Math.log2(far + 1.0);
199
+
200
+ const linearDepth = Math.pow(2, depth / (logDepthBufFC * 0.5)) - 1.0;
201
+ const depthFromCamera = linearDepth + near;
202
+
203
+ clipCoords = new Vector4(
204
+ screenCoords.x,
205
+ screenCoords.y,
206
+ depth * 2.0 - 1.0, // Convert depth [0,1] to NDC [-1,1]
207
+ 1.0,
208
+ );
209
+
210
+ const eyeCoordinate = clipCoords
211
+ .clone()
212
+ .applyMatrix4(camera.projectionMatrixInverse);
213
+
214
+ if (eyeCoordinate.w !== 0) {
215
+ eyeCoordinate.divideScalar(eyeCoordinate.w);
216
+ }
217
+
218
+ // For logarithmic depth, we need to scale the eye coordinate by the actual depth
219
+ const eyeZ = -depthFromCamera; // Negative because camera looks down -Z
220
+ const scaleFactor = eyeZ / eyeCoordinate.z;
221
+ eyeCoordinate.x *= scaleFactor;
222
+ eyeCoordinate.y *= scaleFactor;
223
+ eyeCoordinate.z = eyeZ;
224
+
225
+ // Transform to world space
226
+ return new Vector3(
227
+ eyeCoordinate.x,
228
+ eyeCoordinate.y,
229
+ eyeCoordinate.z,
230
+ ).applyMatrix4(camera.matrixWorld);
231
+ } else {
232
+ // Linear depth buffer case
233
+ clipCoords = new Vector4(
234
+ screenCoords.x,
235
+ screenCoords.y,
236
+ depth * 2.0 - 1.0,
237
+ 1.0,
238
+ );
239
+
240
+ // Transform to eye coordinates
241
+ const eyeCoordinate = clipCoords
242
+ .clone()
243
+ .applyMatrix4(camera.projectionMatrixInverse);
244
+ eyeCoordinate.divideScalar(eyeCoordinate.w);
245
+
246
+ // Transform to world space
247
+ return new Vector3(
248
+ eyeCoordinate.x,
249
+ eyeCoordinate.y,
250
+ eyeCoordinate.z,
251
+ ).applyMatrix4(camera.matrixWorld);
252
+ }
253
+ }
254
+ }
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+
3
+ import { AttributionPlugin } from "./index";
4
+
5
+ // Importing the real @navaramap/core touches WASM/os at module load, which fails
6
+ // in the test environment. AttributionPlugin only needs `Plugin` as a runtime
7
+ // base class (everything else it imports is type-only), so stub the module.
8
+ /* eslint-disable @typescript-eslint/no-extraneous-class */
9
+ vi.mock("@navaramap/core", () => ({
10
+ Plugin: class Plugin {},
11
+ }));
12
+ /* eslint-enable @typescript-eslint/no-extraneous-class */
13
+
14
+ type Handler = (arg?: unknown) => void;
15
+
16
+ function makeLayer(id: string) {
17
+ const handlers = new Map<string, Set<Handler>>();
18
+ return {
19
+ id,
20
+ on: vi.fn((e: string, cb: Handler) => {
21
+ const set = handlers.get(e) ?? new Set<Handler>();
22
+ set.add(cb);
23
+ handlers.set(e, set);
24
+ }),
25
+ off: vi.fn((e: string, cb: Handler) => {
26
+ handlers.get(e)?.delete(cb);
27
+ }),
28
+ emit(e: string, arg: unknown) {
29
+ handlers.get(e)?.forEach((cb) => cb(arg));
30
+ },
31
+ };
32
+ }
33
+ type FakeLayer = ReturnType<typeof makeLayer>;
34
+
35
+ function makeView(layers: Record<string, FakeLayer> = {}) {
36
+ const handlers = new Map<string, Set<Handler>>();
37
+ return {
38
+ camera: { zoom: 10 },
39
+ on: vi.fn((e: string, cb: Handler) => {
40
+ const set = handlers.get(e) ?? new Set<Handler>();
41
+ set.add(cb);
42
+ handlers.set(e, set);
43
+ }),
44
+ off: vi.fn((e: string, cb: Handler) => {
45
+ handlers.get(e)?.delete(cb);
46
+ }),
47
+ findLayerById: vi.fn((id: string): FakeLayer | undefined => layers[id]),
48
+ };
49
+ }
50
+
51
+ const created: AttributionPlugin[] = [];
52
+ async function setup(layers?: Record<string, FakeLayer>) {
53
+ const view = makeView(layers);
54
+ const plugin = new AttributionPlugin();
55
+ created.push(plugin);
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ await plugin.init(view as any, {} as any);
58
+ return { plugin, view };
59
+ }
60
+
61
+ afterEach(() => {
62
+ created.forEach((p) => p.dispose());
63
+ created.length = 0;
64
+ document.body.replaceChildren();
65
+ });
66
+
67
+ const q = (sel: string): Element | null => document.querySelector(sel);
68
+ const isHidden = (sel: string): boolean =>
69
+ q(sel)?.hasAttribute("hidden") ?? true;
70
+
71
+ /** All top-level source names currently rendered in the list. */
72
+ function allTopLevelCredits(): string[] {
73
+ return Array.from(
74
+ document.querySelectorAll(
75
+ ".navara-attr-list > li.navara-attr-item > .navara-attr-name",
76
+ ),
77
+ ).map((el) => el.textContent?.trim() ?? "");
78
+ }
79
+
80
+ /**
81
+ * User-added top-level credits, i.e. everything except the built-in "Navara"
82
+ * credit that is always shown first.
83
+ */
84
+ function topLevelCredits(): string[] {
85
+ return allTopLevelCredits().filter((name) => name !== "Navara");
86
+ }
87
+
88
+ /** Nested sub-credit texts (zoom-banded children + dynamic layer credits). */
89
+ function subCredits(): string[] {
90
+ return Array.from(document.querySelectorAll(".navara-attr-related > li")).map(
91
+ (el) => el.textContent?.trim() ?? "",
92
+ );
93
+ }
94
+
95
+ describe("AttributionPlugin set management", () => {
96
+ it("renders added sources and drops exact duplicates", async () => {
97
+ const { plugin } = await setup();
98
+ plugin.add([{ attribution: "A" }, { attribution: "B" }]);
99
+ plugin.add([{ attribution: "A" }]); // duplicate — should not double
100
+ expect(topLevelCredits()).toEqual(["A", "B"]);
101
+ });
102
+
103
+ it("remove drops a structurally-matching entry and ignores unmatched", async () => {
104
+ const { plugin } = await setup();
105
+ plugin.add([{ attribution: "A" }, { attribution: "B" }]);
106
+ plugin.remove([{ attribution: "A" }]);
107
+ expect(topLevelCredits()).toEqual(["B"]);
108
+ plugin.remove([{ attribution: "does-not-exist" }]);
109
+ expect(topLevelCredits()).toEqual(["B"]);
110
+ });
111
+
112
+ it("clear drops user credits but keeps the dock + Navara; hides the logo frame", async () => {
113
+ const { plugin } = await setup();
114
+ plugin.add([{ attribution: "A", logo: "/a.png" }]);
115
+ expect(isHidden(".navara-attr-dock")).toBe(false);
116
+ expect(
117
+ document.querySelectorAll(".navara-attr-logoframe img"),
118
+ ).toHaveLength(1);
119
+
120
+ plugin.clear();
121
+ expect(topLevelCredits()).toEqual([]);
122
+ // Dock and the built-in Navara credit stay; the logo frame hides (no logos).
123
+ expect(isHidden(".navara-attr-dock")).toBe(false);
124
+ expect(allTopLevelCredits()).toEqual(["Navara"]);
125
+ expect(isHidden(".navara-attr-logoframe")).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe("AttributionPlugin built-in Navara credit", () => {
130
+ it("shows the dock with the Navara credit even with no user credits", async () => {
131
+ await setup();
132
+ expect(q(".navara-attr-dock")).not.toBeNull();
133
+ expect(isHidden(".navara-attr-dock")).toBe(false);
134
+ expect(allTopLevelCredits()).toEqual(["Navara"]);
135
+ // No logos declared, so the logo frame stays hidden.
136
+ expect(isHidden(".navara-attr-logoframe")).toBe(true);
137
+ });
138
+
139
+ it("keeps the dock + Navara visible after all user credits are removed", async () => {
140
+ const { plugin } = await setup();
141
+ plugin.add([{ attribution: "A" }]);
142
+ expect(allTopLevelCredits()).toEqual(["Navara", "A"]);
143
+ plugin.remove([{ attribution: "A" }]);
144
+ expect(isHidden(".navara-attr-dock")).toBe(false);
145
+ expect(allTopLevelCredits()).toEqual(["Navara"]);
146
+ });
147
+ });
148
+
149
+ describe("AttributionPlugin popover visibility", () => {
150
+ it("is collapsed by default and show()/hide() toggle card + aria-expanded", async () => {
151
+ const { plugin } = await setup();
152
+ plugin.add([{ attribution: "A" }]);
153
+ expect(isHidden(".navara-attr-card")).toBe(true);
154
+ expect(q(".navara-attr-toggle")?.getAttribute("aria-expanded")).toBe(
155
+ "false",
156
+ );
157
+
158
+ plugin.show();
159
+ expect(isHidden(".navara-attr-card")).toBe(false);
160
+ expect(q(".navara-attr-toggle")?.getAttribute("aria-expanded")).toBe(
161
+ "true",
162
+ );
163
+
164
+ plugin.hide();
165
+ expect(isHidden(".navara-attr-card")).toBe(true);
166
+ expect(q(".navara-attr-toggle")?.getAttribute("aria-expanded")).toBe(
167
+ "false",
168
+ );
169
+ });
170
+
171
+ it("keeps an open/closed intent set before the DOM exists", async () => {
172
+ const view = makeView();
173
+ const plugin = new AttributionPlugin();
174
+ created.push(plugin);
175
+ plugin.show(); // recorded before init/DOM — must not be lost
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ await plugin.init(view as any, {} as any);
178
+ plugin.add([{ attribution: "A" }]);
179
+ expect(isHidden(".navara-attr-card")).toBe(false);
180
+ });
181
+ });
182
+
183
+ describe("AttributionPlugin lifecycle", () => {
184
+ it("dispose removes the DOM and clears content", async () => {
185
+ const { plugin } = await setup();
186
+ plugin.add([{ attribution: "A" }]);
187
+ expect(q(".navara-attr-dock")).not.toBeNull();
188
+
189
+ plugin.dispose();
190
+ expect(q(".navara-attr-dock")).toBeNull();
191
+ expect(q(".navara-attr-logoframe")).toBeNull();
192
+ });
193
+ });
194
+
195
+ describe("AttributionPlugin dynamic layer credits", () => {
196
+ it("keeps tracked per-feature credits across an unrelated add", async () => {
197
+ const layer = makeLayer("L");
198
+ const { plugin } = await setup({ L: layer });
199
+ plugin.add([{ attribution: "Base", creditLayerId: "L" }]);
200
+ plugin.show(); // sub-credits only render while the popover is open
201
+
202
+ layer.emit("featureCreated", { featureSetId: 1n, credit: "DynCredit" });
203
+ expect(subCredits()).toContain("DynCredit");
204
+
205
+ // Adding an unrelated static source must not retrack (and wipe) layer L.
206
+ plugin.add([{ attribution: "Extra" }]);
207
+ expect(topLevelCredits()).toEqual(["Base", "Extra"]);
208
+ expect(subCredits()).toContain("DynCredit");
209
+ });
210
+ });
@@ -0,0 +1,225 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import {
4
+ aggregateCredits,
5
+ appendSanitizedHtml,
6
+ dedupeAttributionItems,
7
+ isAttributionHtml,
8
+ matchesZoom,
9
+ safeHref,
10
+ type AttributionChild,
11
+ } from "./attribution";
12
+
13
+ describe("safeHref", () => {
14
+ it("allows http / https / mailto and relative URLs", () => {
15
+ expect(safeHref("https://example.test")).toBe("https://example.test");
16
+ expect(safeHref("http://example.test")).toBe("http://example.test");
17
+ expect(safeHref("mailto:a@example.test")).toBe("mailto:a@example.test");
18
+ expect(safeHref("/path")).toBe("/path");
19
+ expect(safeHref("#section")).toBe("#section");
20
+ });
21
+
22
+ it("blocks code-executing schemes", () => {
23
+ expect(safeHref("javascript:alert(1)")).toBeUndefined();
24
+ expect(
25
+ safeHref("data:text/html,<script>alert(1)</script>"),
26
+ ).toBeUndefined();
27
+ expect(safeHref("vbscript:msgbox(1)")).toBeUndefined();
28
+ });
29
+ });
30
+
31
+ describe("aggregateCredits", () => {
32
+ it("splits on ';', trims, dedups, and orders by frequency then name", () => {
33
+ expect(aggregateCredits(["Google;Airbus", "Airbus ; Maxar"])).toEqual([
34
+ "Airbus", // count 2 → first
35
+ "Google", // count 1, alpha before Maxar
36
+ "Maxar",
37
+ ]);
38
+ });
39
+
40
+ it("ignores empty parts and whitespace", () => {
41
+ expect(aggregateCredits([";; Google ;", ""])).toEqual(["Google"]);
42
+ });
43
+
44
+ it("returns an empty list when there are no credits", () => {
45
+ expect(aggregateCredits([])).toEqual([]);
46
+ });
47
+
48
+ it("does not split a ';' inside an <a> href (URL preserved verbatim)", () => {
49
+ expect(
50
+ aggregateCredits(['<a href="https://x.com?a=1;b=2">Map</a>;Other']),
51
+ ).toEqual(['<a href="https://x.com?a=1;b=2">Map</a>', "Other"]);
52
+ });
53
+
54
+ it("does not split a ';' that terminates an HTML entity", () => {
55
+ expect(aggregateCredits(["AT&amp;T;Foo"])).toEqual(["AT&amp;T", "Foo"]);
56
+ });
57
+
58
+ it("splits inside non-anchor tags and drops the tag from the key", () => {
59
+ // Split into "Google"/"Airbus" (tag dropped), then ordered alphabetically.
60
+ expect(aggregateCredits(["<b>Google;Airbus</b>"])).toEqual([
61
+ "Airbus",
62
+ "Google",
63
+ ]);
64
+ });
65
+ });
66
+
67
+ describe("isAttributionHtml", () => {
68
+ it("returns true for raw HTML credits", () => {
69
+ expect(isAttributionHtml({ attributionHtml: "<a>x</a>" })).toBe(true);
70
+ });
71
+
72
+ it("returns false for structured sources", () => {
73
+ expect(isAttributionHtml({ attribution: "国土地理院" })).toBe(false);
74
+ });
75
+ });
76
+
77
+ describe("matchesZoom", () => {
78
+ const band: AttributionChild = { attribution: "x", minZoom: 14, maxZoom: 18 };
79
+
80
+ it("matches everything when the level is undefined", () => {
81
+ expect(matchesZoom(band, undefined)).toBe(true);
82
+ });
83
+
84
+ it("matches within the band, inclusive of both bounds", () => {
85
+ expect(matchesZoom(band, 14)).toBe(true);
86
+ expect(matchesZoom(band, 16)).toBe(true);
87
+ expect(matchesZoom(band, 18)).toBe(true);
88
+ });
89
+
90
+ it("rejects levels outside the band", () => {
91
+ expect(matchesZoom(band, 13)).toBe(false);
92
+ expect(matchesZoom(band, 19)).toBe(false);
93
+ });
94
+
95
+ it("treats a missing bound as unbounded on that side", () => {
96
+ expect(matchesZoom({ attribution: "x" }, 0)).toBe(true);
97
+ expect(matchesZoom({ attribution: "x", minZoom: 10 }, 100)).toBe(true);
98
+ expect(matchesZoom({ attribution: "x", minZoom: 10 }, 9)).toBe(false);
99
+ expect(matchesZoom({ attribution: "x", maxZoom: 10 }, 0)).toBe(true);
100
+ expect(matchesZoom({ attribution: "x", maxZoom: 10 }, 11)).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe("appendSanitizedHtml", () => {
105
+ function render(html: string): HTMLElement {
106
+ const el = document.createElement("div");
107
+ appendSanitizedHtml(el, html);
108
+ return el;
109
+ }
110
+
111
+ it("keeps plain text verbatim", () => {
112
+ expect(render("国土地理院").textContent).toBe("国土地理院");
113
+ });
114
+
115
+ it("keeps <a href> and forces safe target/rel", () => {
116
+ const el = render('see <a href="https://gsi.go.jp">GSI</a>');
117
+ const anchor = el.querySelector("a");
118
+ expect(anchor?.getAttribute("href")).toBe("https://gsi.go.jp");
119
+ expect(anchor?.target).toBe("_blank");
120
+ expect(anchor?.rel).toBe("noopener noreferrer");
121
+ expect(anchor?.textContent).toBe("GSI");
122
+ });
123
+
124
+ it("drops <a> with a javascript: href but keeps its text", () => {
125
+ const el = render('<a href="javascript:alert(1)">credit</a>');
126
+ expect(el.querySelector("a")).toBeNull();
127
+ expect(el.textContent).toBe("credit");
128
+ });
129
+
130
+ it("keeps all nodes even when the input injects a closing tag", () => {
131
+ const el = render('A</span><a href="https://x.test">B</a>');
132
+ expect(el.textContent).toContain("A");
133
+ expect(el.querySelector("a")?.textContent).toBe("B");
134
+ expect(el.querySelector("a")?.getAttribute("href")).toBe("https://x.test");
135
+ });
136
+
137
+ it("drops <script> elements (keeps only their text, never executes)", () => {
138
+ const el = render("a<script>alert(1)</script>b");
139
+ expect(el.querySelector("script")).toBeNull();
140
+ expect(el.textContent).toBe("aalert(1)b");
141
+ });
142
+
143
+ it("drops <img> entirely, removing event-handler attack surface", () => {
144
+ const el = render('<img src="x" onerror="alert(1)">');
145
+ expect(el.querySelector("img")).toBeNull();
146
+ });
147
+
148
+ it("unwraps unknown tags while preserving nested <a>", () => {
149
+ const el = render('<b>bold <a href="https://x.test">link</a></b>');
150
+ expect(el.querySelector("b")).toBeNull();
151
+ expect(el.querySelector("a")?.getAttribute("href")).toBe("https://x.test");
152
+ expect(el.textContent).toBe("bold link");
153
+ });
154
+
155
+ it("auto-links a bare http(s) URL, preserving the text verbatim", () => {
156
+ const el = render("see https://lpdaac.usgs.gov/data_access maintained by");
157
+ const anchor = el.querySelector("a");
158
+ expect(anchor?.getAttribute("href")).toBe(
159
+ "https://lpdaac.usgs.gov/data_access",
160
+ );
161
+ expect(anchor?.textContent).toBe("https://lpdaac.usgs.gov/data_access");
162
+ expect(anchor?.rel).toBe("noopener noreferrer");
163
+ expect(el.textContent).toBe(
164
+ "see https://lpdaac.usgs.gov/data_access maintained by",
165
+ );
166
+ });
167
+
168
+ it("keeps trailing punctuation outside an auto-linked URL", () => {
169
+ const el = render("(https://www.gebco.net) 海上保安庁");
170
+ expect(el.querySelector("a")?.textContent).toBe("https://www.gebco.net");
171
+ expect(el.textContent).toBe("(https://www.gebco.net) 海上保安庁");
172
+ });
173
+
174
+ it("does not nest a link when a bare URL sits inside an <a>", () => {
175
+ const el = render('<a href="https://x.test">https://y.test</a>');
176
+ expect(el.querySelectorAll("a")).toHaveLength(1);
177
+ expect(el.querySelector("a")?.getAttribute("href")).toBe("https://x.test");
178
+ expect(el.querySelector("a")?.textContent).toBe("https://y.test");
179
+ });
180
+ });
181
+
182
+ describe("dedupeAttributionItems", () => {
183
+ it("drops exact-duplicate sources, keeping first-seen order", () => {
184
+ const overture = {
185
+ attribution: "© OpenStreetMap contributors, © Overture Maps Foundation",
186
+ attributionUrl: "https://overturemaps.org",
187
+ };
188
+ expect(
189
+ dedupeAttributionItems([
190
+ { ...overture },
191
+ { ...overture },
192
+ { attribution: "© Overture Maps Foundation" },
193
+ ]),
194
+ ).toEqual([overture, { attribution: "© Overture Maps Foundation" }]);
195
+ });
196
+
197
+ it("keeps sources that differ only in creditLayerId", () => {
198
+ const items = [
199
+ { attribution: "Overture", creditLayerId: "buildings" },
200
+ { attribution: "Overture", creditLayerId: "divisions" },
201
+ ];
202
+ expect(dedupeAttributionItems(items)).toEqual(items);
203
+ });
204
+
205
+ it("keeps sources that differ only in zoom-banded children", () => {
206
+ const items = [
207
+ { attribution: "GSI", children: [{ attribution: "photo", minZoom: 14 }] },
208
+ { attribution: "GSI", children: [{ attribution: "photo", minZoom: 15 }] },
209
+ ];
210
+ expect(dedupeAttributionItems(items)).toEqual(items);
211
+ });
212
+
213
+ it("dedups identical HTML credits but keeps distinct ones", () => {
214
+ expect(
215
+ dedupeAttributionItems([
216
+ { attributionHtml: "<a>x</a>" },
217
+ { attributionHtml: "<a>x</a>" },
218
+ { attributionHtml: "<a>y</a>" },
219
+ ]),
220
+ ).toEqual([
221
+ { attributionHtml: "<a>x</a>" },
222
+ { attributionHtml: "<a>y</a>" },
223
+ ]);
224
+ });
225
+ });