@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,1019 @@
1
+ import { Unimplemented } from "@navaramap/core";
2
+ import type {
3
+ TextMaterial as NavaraTextMaterial,
4
+ Transform,
5
+ } from "@navaramap/engine";
6
+ import {
7
+ COLOR_GLYPH_PX_SIZE,
8
+ GlyphCharClass,
9
+ createSdfAtlasTexture,
10
+ type FontManager,
11
+ type GlyphMetrics,
12
+ type ShapedGlyph,
13
+ type ShapeTextResult,
14
+ } from "@navaramap/font";
15
+ import { degreeToRadian } from "@navaramap/three_api";
16
+ import {
17
+ BufferAttribute,
18
+ type Color,
19
+ type DataTexture,
20
+ type PerspectiveCamera,
21
+ InstancedBufferAttribute,
22
+ InstancedBufferGeometry,
23
+ Mesh,
24
+ Object3D,
25
+ ShaderMaterial,
26
+ Vector2,
27
+ } from "three";
28
+
29
+ import type { MaterialEnhancer } from "../material/enhancer/MaterialEnhancer";
30
+ import {
31
+ createSdfTextMaterialEnhancer,
32
+ type SdfTextBaseMutates,
33
+ type SdfTextBaseProps,
34
+ type SdfTextBaseState,
35
+ } from "../material/enhancer/sdfText";
36
+ import { sdfRadiusFor } from "../material/enhancer/sdfText/sdfTextBaseEnhancer/types";
37
+
38
+ import type { PickableMesh } from "./pickableMesh";
39
+
40
+ /** Must match Rust `SDF_PX_SIZE` in `crates/navara_wasm_font_worker/src/atlas.rs`. */
41
+ const SDF_PX_SIZE = 64.0;
42
+
43
+ /** Reusable Vector2 to avoid per-frame allocations in onBeforeRender. */
44
+ const _tmpSize = new Vector2();
45
+
46
+ /** Horizontal alignment of lines within a multi-line block, as the fraction
47
+ * of leftover width placed before each line. */
48
+ const ALIGN_FACTORS: Record<string, number> = {
49
+ left: 0,
50
+ center: 0.5,
51
+ right: 1,
52
+ };
53
+
54
+ /** Line width in font units: advances summed, trailing whitespace ignored so
55
+ * it never affects alignment or the block width. Exported for tests. */
56
+ export function lineWidthFu(line: ShapedGlyph[]): number {
57
+ let end = line.length;
58
+ while (end > 0 && line[end - 1].charClass === GlyphCharClass.Whitespace) {
59
+ end--;
60
+ }
61
+ let width = 0;
62
+ for (let i = 0; i < end; i++) width += line[i].xAdvance;
63
+ return width;
64
+ }
65
+
66
+ /** Strong RTL code points (Hebrew through Arabic Extended, presentation
67
+ * forms, and the supplementary-plane RTL blocks). Used for first-strong
68
+ * paragraph direction detection, mirroring the shaper's own direction guess.
69
+ */
70
+ const STRONG_RTL_RE =
71
+ /[\u0591-\u08FF\uFB1D-\uFDFD\uFE70-\uFEFC\u{10800}-\u{10FFF}\u{1E800}-\u{1EFFF}]/u;
72
+
73
+ /** Paragraph direction from the first strong directional character (UAX #9
74
+ * P2/P3 approximation): the first letter decides, RTL ranges win over other
75
+ * letters. Exported for tests. */
76
+ export function isRtlText(text: string): boolean {
77
+ for (const ch of text) {
78
+ if (STRONG_RTL_RE.test(ch)) return true;
79
+ if (/\p{L}/u.test(ch)) return false;
80
+ }
81
+ return false;
82
+ }
83
+
84
+ /**
85
+ * Split a shaped glyph run into lines: hard breaks at newline markers, greedy
86
+ * soft breaks at the last whitespace/ideographic glyph when a line would
87
+ * exceed `maxWidthFu` (font units; 0 disables wrapping). A word longer than
88
+ * the wrap width overflows rather than breaking mid-word.
89
+ *
90
+ * When `rtl` is set, glyphs are assumed to arrive in visual order (leftmost
91
+ * first — how the shaper emits RTL runs), i.e. reversed logical order. Each
92
+ * hard-break segment is wrapped in logical order so the greedy fill starts at
93
+ * the sentence start and lines stack top-to-bottom in reading order, then
94
+ * every line is flipped back to visual order for rendering. Mixed-direction
95
+ * lines may still break sub-optimally — same trade other map renderers make.
96
+ * Exported for tests.
97
+ */
98
+ export function breakLines(
99
+ glyphs: ShapedGlyph[],
100
+ maxWidthFu: number,
101
+ rtl = false,
102
+ ): ShapedGlyph[][] {
103
+ const lines: ShapedGlyph[][] = [];
104
+
105
+ const pushSegment = (segment: ShapedGlyph[]) => {
106
+ if (!rtl) {
107
+ lines.push(...wrapSegment(segment, maxWidthFu));
108
+ return;
109
+ }
110
+ segment.reverse();
111
+ for (const line of wrapSegment(segment, maxWidthFu)) {
112
+ lines.push(line.reverse());
113
+ }
114
+ };
115
+
116
+ let segment: ShapedGlyph[] = [];
117
+ for (const g of glyphs) {
118
+ if (g.charClass === GlyphCharClass.Newline) {
119
+ pushSegment(segment);
120
+ segment = [];
121
+ } else {
122
+ segment.push(g);
123
+ }
124
+ }
125
+ pushSegment(segment);
126
+ return lines;
127
+ }
128
+
129
+ /** Greedy soft-wrap of a single hard-break-free segment in logical order. */
130
+ function wrapSegment(
131
+ glyphs: ShapedGlyph[],
132
+ maxWidthFu: number,
133
+ ): ShapedGlyph[][] {
134
+ const lines: ShapedGlyph[][] = [];
135
+ let line: ShapedGlyph[] = [];
136
+ let width = 0;
137
+ let breakIdx = -1; // index in `line` of the last break opportunity
138
+
139
+ for (const g of glyphs) {
140
+ // Trailing whitespace is invisible at a line end, so it never triggers a
141
+ // wrap itself — it just gets trimmed if a later glyph wraps.
142
+ if (
143
+ maxWidthFu > 0 &&
144
+ g.charClass !== GlyphCharClass.Whitespace &&
145
+ line.length > 0 &&
146
+ breakIdx >= 0 &&
147
+ width + g.xAdvance > maxWidthFu
148
+ ) {
149
+ const head = line.slice(0, breakIdx + 1);
150
+ while (
151
+ head.length > 0 &&
152
+ head[head.length - 1].charClass === GlyphCharClass.Whitespace
153
+ ) {
154
+ head.pop();
155
+ }
156
+ lines.push(head);
157
+
158
+ const tail = line.slice(breakIdx + 1);
159
+ const start = tail.findIndex(
160
+ (gg) => gg.charClass !== GlyphCharClass.Whitespace,
161
+ );
162
+ line = start === -1 ? [] : tail.slice(start);
163
+
164
+ width = 0;
165
+ for (const rest of line) width += rest.xAdvance;
166
+ breakIdx = -1;
167
+ }
168
+
169
+ line.push(g);
170
+ width += g.xAdvance;
171
+ if (
172
+ g.charClass === GlyphCharClass.Whitespace ||
173
+ g.charClass === GlyphCharClass.Ideographic
174
+ ) {
175
+ breakIdx = line.length - 1;
176
+ }
177
+ }
178
+
179
+ lines.push(line);
180
+ return lines;
181
+ }
182
+
183
+ /**
184
+ * A text mesh that renders glyphs from an SDF atlas using instanced geometry.
185
+ *
186
+ * Each glyph is an instanced unit quad positioned by shaping data,
187
+ * sampling from a per-font SDF atlas texture. Uses billboard rendering
188
+ * so text always faces the camera.
189
+ */
190
+ export class SDFTextMesh
191
+ extends Mesh<InstancedBufferGeometry, ShaderMaterial>
192
+ implements PickableMesh
193
+ {
194
+ private _fontManager: FontManager;
195
+ private _fontUrl: string;
196
+ /** Per-material text quality. Immutable after construction — switching
197
+ * quality requires a separate (font, atlas) pair, so callers create a new
198
+ * mesh rather than mutating this one. */
199
+ private _highQuality: boolean;
200
+ private _text = "";
201
+ /** Wrap width in ems (multiples of font size); 0 disables soft wrapping. */
202
+ private _maxWidth = 0;
203
+ /** Multiplier on the font's natural line height (ascender − descender + gap). */
204
+ private _lineHeight = 1.0;
205
+ /** Alignment factor for multi-line blocks: 0 left, 0.5 center, 1 right. */
206
+ private _textAlign = 0.5;
207
+ private _atlasTexture: DataTexture | null = null;
208
+ /** Atlas texture is owned externally; do not dispose on cleanup when true. */
209
+ private _sharedAtlas = false;
210
+ /** Unique atlas glyphs this mesh currently renders (composite keys). */
211
+ private _glyphKeys: bigint[] = [];
212
+ /** The glyph set currently retained in the atlas (one visible-label
213
+ * reference per key), or null when this mesh holds no references. */
214
+ private _retainedKeys: bigint[] | null = null;
215
+ /** Re-prepare a text in the worker (re-rasterizing evicted glyphs) using the
216
+ * owner's font identity and loaded faces. Set by the parent batch. */
217
+ private _reprepare?: (text: string) => Promise<void>;
218
+ /** Ask the owner to schedule a render after an async re-prepare. */
219
+ private _requestRender?: () => void;
220
+ // The COLRv1 color atlas is always FontManager-owned; no local field needed.
221
+
222
+ private _enhancer: MaterialEnhancer<
223
+ ShaderMaterial,
224
+ { base?: SdfTextBaseProps },
225
+ SdfTextBaseState,
226
+ SdfTextBaseMutates,
227
+ readonly ["shader"]
228
+ >;
229
+
230
+ constructor(
231
+ position: Float32Array | { high: Float32Array; low: Float32Array },
232
+ material: NavaraTextMaterial,
233
+ transform: Transform,
234
+ fontManager: FontManager,
235
+ fontUrl: string,
236
+ batchId: number | undefined,
237
+ RTE: boolean,
238
+ ) {
239
+ super();
240
+
241
+ this._fontManager = fontManager;
242
+ this._fontUrl = fontUrl;
243
+ this._highQuality = material.highQuality ?? false;
244
+ this._maxWidth = material.maxWidth ?? 0;
245
+ this._lineHeight = material.lineHeight ?? 1.0;
246
+ this._textAlign = ALIGN_FACTORS[material.textAlign ?? "center"] ?? 0.5;
247
+
248
+ this.geometry = this._createBaseGeometry();
249
+
250
+ // Create empty ShaderMaterial — enhancer will set shaders and uniforms
251
+ const mat = new ShaderMaterial({
252
+ transparent: true,
253
+ // depthWrite must stay enabled: the fragment shader's per-pixel outline
254
+ // depth offset (sdfText.frag.glsl) relies on fills writing a nearer depth
255
+ // so a neighbouring glyph's fill occludes this glyph's outline at overlaps
256
+ // — without depth writes that outline-seam fix becomes a no-op.
257
+ depthWrite: true,
258
+ });
259
+
260
+ this._enhancer = createSdfTextMaterialEnhancer(mat);
261
+
262
+ // Mount enhancer with initial props
263
+ // Validate and clamp opacity values to prevent invalid alpha in shader
264
+ const initialOpacity = Number.isFinite(material.opacity ?? 1.0)
265
+ ? Math.max(0, Math.min(1, material.opacity ?? 1.0))
266
+ : 1.0;
267
+ const initialOutlineOpacity = Number.isFinite(
268
+ material.outlineOpacity ?? 1.0,
269
+ )
270
+ ? Math.max(0, Math.min(1, material.outlineOpacity ?? 1.0))
271
+ : 1.0;
272
+
273
+ this._enhancer.mount({
274
+ base: {
275
+ useRTE: RTE,
276
+ useMsdf: this._highQuality,
277
+ color: material.color ?? 0xffffff,
278
+ opacity: initialOpacity,
279
+ fontSize: material.size ?? 16.0,
280
+ center: material.center
281
+ ? [material.center.x, material.center.y]
282
+ : undefined,
283
+ sizeInMeters: material.sizeInMeters ?? true,
284
+ addHeight: material.height ?? 0.0,
285
+ offsetDepth: material.offsetDepth ?? true,
286
+ outlineWidth: material.outlineWidth ?? 0,
287
+ outlineColor: material.outlineColor ?? 0x000000,
288
+ outlineOpacity: initialOutlineOpacity,
289
+ showBackground: material.backgroundColor !== undefined,
290
+ backgroundColor: material.backgroundColor,
291
+ backgroundOutlineColor: material.borderColor ?? 0x000000,
292
+ backgroundOutlineWidth: material.borderWidth ?? 0.1,
293
+ depthTest: material.depthTest ?? true,
294
+ transparent: material.transparent ?? true,
295
+ rtcCenter: [transform.tx, transform.ty, transform.tz],
296
+ },
297
+ });
298
+
299
+ // Populate uniforms early (before onBeforeCompile fires)
300
+ const mutates = this._enhancer.mutates();
301
+ mutates.updateUniforms(mat.uniforms, this._enhancer.states());
302
+
303
+ // Set batch ID
304
+ if (batchId !== undefined) {
305
+ mutates.setBatchId(batchId);
306
+ }
307
+
308
+ // Set position
309
+ mutates.setPosition(position, RTE, [
310
+ transform.tx,
311
+ transform.ty,
312
+ transform.tz,
313
+ ]);
314
+
315
+ // Register shader hook
316
+ mat.onBeforeCompile = this._enhancer.transformShader;
317
+ mat.customProgramCacheKey = this._enhancer.programCacheKey;
318
+
319
+ // Per-frame camera updates
320
+ const state = this._enhancer.states();
321
+ mat.onBeforeRender = (renderer, _scene, camera) => {
322
+ const pCam = camera as PerspectiveCamera;
323
+ mutates.updatePerFrame(
324
+ degreeToRadian(pCam.fov),
325
+ renderer.getDrawingBufferSize(_tmpSize).y / renderer.getPixelRatio(),
326
+ pCam.far,
327
+ camera.position.x,
328
+ camera.position.y,
329
+ camera.position.z,
330
+ camera.matrixWorldInverse,
331
+ state,
332
+ );
333
+ // Keep atlas-size uniforms in sync with the (possibly resized) shared
334
+ // DataTexture so glyph pixel rects always normalize to the right UV.
335
+ mutates.updateAtlasSizes();
336
+ };
337
+
338
+ this.material = mat;
339
+ this.frustumCulled = false;
340
+ }
341
+
342
+ /** The text this mesh currently renders (its baked glyph string). */
343
+ get text(): string {
344
+ return this._text;
345
+ }
346
+
347
+ /**
348
+ * Set a shared atlas texture. When set, setText() will skip creating its own texture.
349
+ * The caller is responsible for the texture lifecycle.
350
+ */
351
+ setAtlasTexture(tex: DataTexture): void {
352
+ this._atlasTexture = tex;
353
+ this._sharedAtlas = true;
354
+ this._enhancer.mutates().setAtlasTexture({ value: tex });
355
+ }
356
+
357
+ /**
358
+ * Set a shared color (COLRv1 RGBA) atlas texture.
359
+ * Pass `null` to clear when the font has no color glyphs.
360
+ */
361
+ setColorAtlasTexture(tex: DataTexture | null): void {
362
+ this._enhancer.mutates().setColorAtlasTexture({ value: tex });
363
+ }
364
+
365
+ /**
366
+ * Wire the callbacks the parent batch uses to re-prepare evicted glyphs and
367
+ * request a render. `reprepare` must apply the batch's font identity and
368
+ * loaded faces so font refcounts stay balanced.
369
+ */
370
+ setGlyphLifecycleHandlers(
371
+ reprepare: (text: string) => Promise<void>,
372
+ requestRender: () => void,
373
+ ): void {
374
+ this._reprepare = reprepare;
375
+ this._requestRender = requestRender;
376
+ }
377
+
378
+ /** Replace the retained glyph set, releasing the old references and (if
379
+ * visible) retaining the new ones. */
380
+ private _setGlyphKeys(keys: bigint[]): void {
381
+ if (this._retainedKeys) {
382
+ this._fontManager.releaseGlyphs(
383
+ this._fontUrl,
384
+ this._highQuality,
385
+ this._retainedKeys,
386
+ );
387
+ this._retainedKeys = null;
388
+ }
389
+ this._glyphKeys = keys;
390
+ this._syncGlyphRefs();
391
+ }
392
+
393
+ /** Reconcile atlas references with current visibility: retain glyphs while
394
+ * on screen, release them when hidden. If shown after the glyphs were
395
+ * evicted (cache no longer prepared), re-prepare to re-rasterize them and
396
+ * rebuild with fresh metrics. */
397
+ private _syncGlyphRefs(): void {
398
+ const visible = this.visible && this._glyphKeys.length > 0;
399
+
400
+ if (visible && !this._retainedKeys) {
401
+ if (
402
+ this._reprepare &&
403
+ !this._fontManager.isTextPrepared(
404
+ this._fontUrl,
405
+ this._text,
406
+ this._highQuality,
407
+ )
408
+ ) {
409
+ // Glyphs were evicted while hidden; re-rasterize then rebuild (which
410
+ // re-enters here with the fresh set and retains it).
411
+ const text = this._text;
412
+ this._reprepare(text)
413
+ .then(() => {
414
+ if (this._text === text) {
415
+ this.setText(text, true);
416
+ this._requestRender?.();
417
+ }
418
+ })
419
+ .catch((err: unknown) => {
420
+ console.error("SDFTextMesh: re-prepare on show failed:", err);
421
+ });
422
+ return;
423
+ }
424
+ this._fontManager.retainGlyphs(
425
+ this._fontUrl,
426
+ this._highQuality,
427
+ this._glyphKeys,
428
+ );
429
+ this._retainedKeys = this._glyphKeys;
430
+ } else if (!visible && this._retainedKeys) {
431
+ this._fontManager.releaseGlyphs(
432
+ this._fontUrl,
433
+ this._highQuality,
434
+ this._retainedKeys,
435
+ );
436
+ this._retainedKeys = null;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Set text to render. Shapes via WASM, rebuilds instanced geometry, updates atlas texture.
442
+ */
443
+ setText(text: string, forceUpdate = false): void {
444
+ if (text === this._text && !forceUpdate) return;
445
+ this._text = text;
446
+
447
+ if (!text) {
448
+ this.geometry.instanceCount = 0;
449
+ this._setGlyphKeys([]);
450
+ return;
451
+ }
452
+
453
+ const shapeResult = this._fontManager.shapeText(
454
+ this._fontUrl,
455
+ text,
456
+ this._highQuality,
457
+ );
458
+ if (!shapeResult) {
459
+ this.geometry.instanceCount = 0;
460
+ this._setGlyphKeys([]);
461
+ return;
462
+ }
463
+
464
+ const atlasData = this._fontManager.getAtlas(
465
+ this._fontUrl,
466
+ this._highQuality,
467
+ );
468
+ if (!atlasData) return;
469
+
470
+ // Glyph rects are stored in pixel space and normalized in the shader using
471
+ // uSdfAtlasSize / uColorAtlasSize, so atlas dimensions aren't needed here.
472
+ this._buildGlyphInstances(shapeResult);
473
+
474
+ // Skip texture creation if using a shared atlas from the parent container
475
+ if (!this._sharedAtlas) {
476
+ this._updateAtlasTexture(
477
+ atlasData.data,
478
+ atlasData.width,
479
+ atlasData.height,
480
+ atlasData.channels,
481
+ );
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Update visual properties: color, size, visibility, etc.
487
+ */
488
+ setFont(fontUrl: string): void {
489
+ if (fontUrl === this._fontUrl) return;
490
+
491
+ // Release glyph references against the old font before switching; the
492
+ // baked instances are about to be rebuilt for the new font anyway.
493
+ if (this._retainedKeys) {
494
+ this._fontManager.releaseGlyphs(
495
+ this._fontUrl,
496
+ this._highQuality,
497
+ this._retainedKeys,
498
+ );
499
+ this._retainedKeys = null;
500
+ }
501
+ this._glyphKeys = [];
502
+ this._fontUrl = fontUrl;
503
+
504
+ // Clear current atlas since it's tied to the previous font
505
+ if (!this._sharedAtlas) {
506
+ this._atlasTexture?.dispose();
507
+ this._atlasTexture = null;
508
+ this._enhancer.mutates().setAtlasTexture({ value: null });
509
+ }
510
+ }
511
+
512
+ setColor(color: Color): void {
513
+ this._enhancer.update({
514
+ base: { color: color.getHex() },
515
+ });
516
+ }
517
+
518
+ setFontSize(size: number): void {
519
+ this._enhancer.update({ base: { fontSize: size } });
520
+ }
521
+
522
+ setSizeInMeters(enabled: boolean): void {
523
+ this._enhancer.update({ base: { sizeInMeters: enabled } });
524
+ }
525
+
526
+ setCenter(x: number, y: number): void {
527
+ this._enhancer.update({ base: { center: [x, y] } });
528
+ }
529
+
530
+ setHeight(height: number): void {
531
+ this._enhancer.update({ base: { addHeight: height } });
532
+ }
533
+
534
+ setSize(size: number): void {
535
+ const currentFontSize = this._enhancer.states().fontSize;
536
+ const sanitizedSize = Number.isFinite(size)
537
+ ? Math.max(0.0, size)
538
+ : currentFontSize;
539
+ this._enhancer.update({ base: { fontSize: sanitizedSize } });
540
+ }
541
+
542
+ setOpacity(opacity: number): void {
543
+ const clampedOpacity = Number.isFinite(opacity)
544
+ ? Math.max(0, Math.min(1, opacity))
545
+ : 1.0;
546
+ this._enhancer.update({ base: { opacity: clampedOpacity } });
547
+ }
548
+
549
+ _setFeatureWidth(_width: number): void {
550
+ // Width is not applicable to text meshes.
551
+ // This method is intentionally a no-op to satisfy the FeatureMesh guard.
552
+ }
553
+ _setFeatureOpacity(opacity: number): void {
554
+ const clampedOpacity = Number.isFinite(opacity)
555
+ ? Math.max(0, Math.min(1, opacity))
556
+ : 1.0;
557
+ this._enhancer.update({ base: { opacity: clampedOpacity } });
558
+ }
559
+
560
+ setPosition(
561
+ position: Float32Array | { high: Float32Array; low: Float32Array },
562
+ RTE: boolean,
563
+ transform: Transform,
564
+ ): void {
565
+ this._enhancer
566
+ .mutates()
567
+ .setPosition(position, RTE, [transform.tx, transform.ty, transform.tz]);
568
+ }
569
+
570
+ /**
571
+ * Apply material properties from WASM TextMaterial.
572
+ * Maps relevant properties to enhancer updates, with change tracking.
573
+ */
574
+ update(material: NavaraTextMaterial, forceUpdate = false): void {
575
+ const fontUrl = material.font ?? this._fontUrl;
576
+ this.setFont(fontUrl);
577
+
578
+ // Layout properties are baked into the glyph instance geometry, so a
579
+ // change forces a rebuild even when the text itself is unchanged.
580
+ const nextMaxWidth = material.maxWidth ?? 0;
581
+ const nextLineHeight = material.lineHeight ?? 1.0;
582
+ const nextTextAlign = ALIGN_FACTORS[material.textAlign ?? "center"] ?? 0.5;
583
+ const layoutChanged =
584
+ nextMaxWidth !== this._maxWidth ||
585
+ nextLineHeight !== this._lineHeight ||
586
+ nextTextAlign !== this._textAlign;
587
+ this._maxWidth = nextMaxWidth;
588
+ this._lineHeight = nextLineHeight;
589
+ this._textAlign = nextTextAlign;
590
+
591
+ const nextText = material.text;
592
+ if (nextText !== undefined && nextText !== "") {
593
+ this.setText(nextText, forceUpdate || layoutChanged);
594
+ } else if (forceUpdate || layoutChanged) {
595
+ // Font or layout changed — re-render existing text with the new settings
596
+ this.setText(this._text, true);
597
+ }
598
+
599
+ this.visible = (material.show ?? true) && !!this._text;
600
+ this._syncGlyphRefs();
601
+ if (!this.visible) return;
602
+
603
+ const state = this._enhancer.states();
604
+
605
+ // Build props for enhancer update
606
+ const baseProps: SdfTextBaseProps = {};
607
+ let hasUpdate = false;
608
+
609
+ const nextColor = material.color ?? 0xffffff;
610
+ if (nextColor !== state.color.getHex()) {
611
+ baseProps.color = nextColor;
612
+ hasUpdate = true;
613
+ }
614
+
615
+ const nextOpacityRaw = material.opacity ?? 1.0;
616
+ const nextOpacity = Number.isFinite(nextOpacityRaw)
617
+ ? Math.max(0.0, Math.min(1.0, nextOpacityRaw))
618
+ : 1.0;
619
+ if (nextOpacity !== state.opacity) {
620
+ baseProps.opacity = nextOpacity;
621
+ hasUpdate = true;
622
+ }
623
+
624
+ const nextFontSize = material.size ?? 16.0;
625
+ if (nextFontSize !== state.fontSize) {
626
+ baseProps.fontSize = nextFontSize;
627
+ hasUpdate = true;
628
+ }
629
+
630
+ const nextCenterX = material.center?.x ?? 0.5;
631
+ const nextCenterY = material.center?.y ?? 0.0;
632
+ if (nextCenterX !== state.center[0] || nextCenterY !== state.center[1]) {
633
+ baseProps.center = [nextCenterX, nextCenterY];
634
+ hasUpdate = true;
635
+ }
636
+
637
+ const nextSizeInMeters = material.sizeInMeters ?? true;
638
+ if (nextSizeInMeters !== state.sizeInMeters) {
639
+ baseProps.sizeInMeters = nextSizeInMeters;
640
+ hasUpdate = true;
641
+ }
642
+
643
+ const nextDepthTest = material.depthTest ?? true;
644
+ if (nextDepthTest !== state.depthTest) {
645
+ baseProps.depthTest = nextDepthTest;
646
+ hasUpdate = true;
647
+ }
648
+
649
+ const nextOffsetDepth = material.offsetDepth ?? true;
650
+ if (nextOffsetDepth !== state.offsetDepth) {
651
+ baseProps.offsetDepth = nextOffsetDepth;
652
+ hasUpdate = true;
653
+ }
654
+
655
+ const nextHeight = material.height ?? 0;
656
+ if (nextHeight !== state.addHeight) {
657
+ baseProps.addHeight = nextHeight;
658
+ hasUpdate = true;
659
+ }
660
+
661
+ const nextOutlineWidth = material.outlineWidth ?? 0;
662
+ if (nextOutlineWidth / sdfRadiusFor(state.useMsdf) !== state.outlineWidth) {
663
+ baseProps.outlineWidth = nextOutlineWidth;
664
+ hasUpdate = true;
665
+ }
666
+
667
+ const nextOutlineColor = material.outlineColor ?? 0x000000;
668
+ if (nextOutlineColor !== state.outlineColor.getHex()) {
669
+ baseProps.outlineColor = nextOutlineColor;
670
+ hasUpdate = true;
671
+ }
672
+
673
+ const nextOutlineOpacityRaw = material.outlineOpacity ?? 1.0;
674
+ const nextOutlineOpacity = Number.isFinite(nextOutlineOpacityRaw)
675
+ ? Math.max(0.0, Math.min(1.0, nextOutlineOpacityRaw))
676
+ : 1.0;
677
+ if (nextOutlineOpacity !== state.outlineOpacity) {
678
+ baseProps.outlineOpacity = nextOutlineOpacity;
679
+ hasUpdate = true;
680
+ }
681
+
682
+ const nextBGColor = material.backgroundColor;
683
+ if (nextBGColor !== undefined) {
684
+ if (!state.showBackground) {
685
+ baseProps.showBackground = true;
686
+ hasUpdate = true;
687
+ }
688
+ if (nextBGColor !== state.backgroundColor.getHex()) {
689
+ baseProps.backgroundColor = nextBGColor;
690
+ hasUpdate = true;
691
+ }
692
+ } else if (state.showBackground) {
693
+ baseProps.showBackground = false;
694
+ hasUpdate = true;
695
+ }
696
+
697
+ const nextBGOutlineColor = material.borderColor ?? 0x000000;
698
+ if (nextBGOutlineColor !== state.backgroundOutlineColor.getHex()) {
699
+ baseProps.backgroundOutlineColor = nextBGOutlineColor;
700
+ hasUpdate = true;
701
+ }
702
+
703
+ const nextBGOutlineWidth = material.borderWidth ?? 0;
704
+ if (nextBGOutlineWidth !== state.backgroundOutlineWidth) {
705
+ baseProps.backgroundOutlineWidth = nextBGOutlineWidth;
706
+ hasUpdate = true;
707
+ }
708
+
709
+ if (material.transparent !== undefined) {
710
+ const nextTransparent = material.transparent;
711
+ if (nextTransparent !== state.transparent) {
712
+ baseProps.transparent = nextTransparent;
713
+ hasUpdate = true;
714
+ }
715
+ }
716
+
717
+ if (hasUpdate) {
718
+ this._enhancer.update({ base: baseProps });
719
+ }
720
+ }
721
+
722
+ // --- FeatureMesh interface ---
723
+
724
+ _setFeatureColor(color: Color): void {
725
+ this.setColor(color);
726
+ }
727
+
728
+ _getFeatureColor(): Color {
729
+ const state = this._enhancer.states();
730
+ return state.color;
731
+ }
732
+
733
+ _setFeatureShow(visible: boolean): void {
734
+ this.visible = visible;
735
+ this._syncGlyphRefs();
736
+ }
737
+
738
+ _setFeatureExtrudedHeight(_height: number): void {
739
+ throw new Unimplemented();
740
+ }
741
+
742
+ _setFeatureHeight(height: number): void {
743
+ this.setHeight(height);
744
+ }
745
+
746
+ _setFrustumCulled(culled: boolean): void {
747
+ this.frustumCulled = culled;
748
+ }
749
+
750
+ // --- PickableMesh interface ---
751
+
752
+ onBeforePicking(): void {
753
+ this._enhancer.update({ base: { pickable: true } });
754
+ }
755
+
756
+ onAfterPicking(): void {
757
+ this._enhancer.update({ base: { pickable: false } });
758
+ }
759
+
760
+ getRenderable(): Object3D {
761
+ return this;
762
+ }
763
+
764
+ // --- Cleanup ---
765
+
766
+ dispose(): void {
767
+ // Release atlas glyph references so they can be evicted once unreferenced.
768
+ if (this._retainedKeys) {
769
+ this._fontManager.releaseGlyphs(
770
+ this._fontUrl,
771
+ this._highQuality,
772
+ this._retainedKeys,
773
+ );
774
+ this._retainedKeys = null;
775
+ }
776
+ this.geometry?.dispose();
777
+ if (!this._sharedAtlas) {
778
+ this._atlasTexture?.dispose();
779
+ }
780
+ // Color atlas is always FontManager-owned; never disposed here.
781
+ this.material?.dispose();
782
+ }
783
+
784
+ // --- Private ---
785
+
786
+ private _createBaseGeometry(): InstancedBufferGeometry {
787
+ const geo = new InstancedBufferGeometry();
788
+
789
+ // Unit quad: 2 triangles, 6 vertices
790
+ // prettier-ignore
791
+ const positions = new Float32Array([
792
+ -0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0,
793
+ -0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0,
794
+ ]);
795
+ // prettier-ignore
796
+ const uvs = new Float32Array([
797
+ 0, 0, 1, 0, 1, 1,
798
+ 0, 0, 1, 1, 0, 1,
799
+ ]);
800
+
801
+ geo.setAttribute("position", new BufferAttribute(positions, 3));
802
+ geo.setAttribute("uv", new BufferAttribute(uvs, 2));
803
+
804
+ geo.setAttribute(
805
+ "glyphOffset",
806
+ new InstancedBufferAttribute(new Float32Array(), 2),
807
+ );
808
+ geo.setAttribute(
809
+ "glyphSize",
810
+ new InstancedBufferAttribute(new Float32Array(), 2),
811
+ );
812
+ geo.setAttribute(
813
+ "glyphUvRect",
814
+ new InstancedBufferAttribute(new Float32Array(), 4),
815
+ );
816
+ /**
817
+ * Per-instance flag: 1.0 → sample the COLRv1 color atlas, 0.0 → sample SDF.
818
+ * Lets one batched mesh mix text and emoji from the same font family.
819
+ */
820
+ geo.setAttribute(
821
+ "glyphIsColor",
822
+ new InstancedBufferAttribute(new Float32Array(), 1),
823
+ );
824
+
825
+ geo.instanceCount = 0;
826
+
827
+ return geo;
828
+ }
829
+
830
+ private _buildGlyphInstances(shapeResult: ShapeTextResult): void {
831
+ const { glyphs, metrics, unitsPerEm, ascender, descender, lineGap } =
832
+ shapeResult;
833
+
834
+ // Build composite key -> metrics lookup.
835
+ // Keys are pre-computed by the WASM font worker (composite_key in Rust)
836
+ // to ensure the key layout is always in sync between Rust and TypeScript.
837
+ const metricsMap = new Map<bigint, GlyphMetrics>();
838
+ for (const m of metrics) {
839
+ metricsMap.set(m.compositeKey, m);
840
+ }
841
+
842
+ const fontUnitToSdfPx = SDF_PX_SIZE / unitsPerEm;
843
+ const fontUnitToColorPx = COLOR_GLYPH_PX_SIZE / unitsPerEm;
844
+
845
+ // Baseline-to-baseline distance in font units. Older cached results may
846
+ // predate line metrics; fall back to one em.
847
+ const naturalLineHeight = ascender - descender + lineGap;
848
+ const lineHeightFu =
849
+ (naturalLineHeight > 0 ? naturalLineHeight : unitsPerEm) *
850
+ this._lineHeight;
851
+
852
+ // `_maxWidth` is in ems so the wrap width tracks the font size in both
853
+ // sizeInMeters modes; font units are ems × unitsPerEm.
854
+ const lines = breakLines(
855
+ glyphs,
856
+ this._maxWidth * unitsPerEm,
857
+ isRtlText(this._text),
858
+ );
859
+ const widths = lines.map(lineWidthFu);
860
+ const blockWidthFu = Math.max(...widths);
861
+
862
+ const renderable: {
863
+ offsetEmX: number;
864
+ offsetEmY: number;
865
+ sizeEmX: number;
866
+ sizeEmY: number;
867
+ uvL: number;
868
+ uvT: number;
869
+ uvR: number;
870
+ uvB: number;
871
+ isColor: boolean;
872
+ }[] = [];
873
+
874
+ // Unique atlas glyphs this mesh references — retained while it is visible.
875
+ const glyphKeys = new Set<bigint>();
876
+
877
+ // Lay lines out top-down: line 0 keeps its baseline at y=0 (identical to
878
+ // the old single-line behavior), later baselines step down by the line
879
+ // height. Alignment shifts each line within the widest line's width.
880
+ // Each glyph carries its own normalization scale so SDF and color glyphs
881
+ // share one em-space coordinate system downstream — the two paths may use
882
+ // different raster sizes (SDF_PX_SIZE vs COLOR_GLYPH_PX_SIZE), but both
883
+ // end up in [em]-units after dividing by their respective px.
884
+ for (let li = 0; li < lines.length; li++) {
885
+ let cursorX = (blockWidthFu - widths[li]) * this._textAlign;
886
+ let cursorY = -li * lineHeightFu;
887
+
888
+ for (const glyph of lines[li]) {
889
+ const m = metricsMap.get(glyph.compositeKey);
890
+ if (m && m.atlasW > 0 && m.atlasH > 0) {
891
+ glyphKeys.add(glyph.compositeKey);
892
+ const px = m.isColor ? COLOR_GLYPH_PX_SIZE : SDF_PX_SIZE;
893
+ const fuToPx = m.isColor ? fontUnitToColorPx : fontUnitToSdfPx;
894
+ const offsetPxX = (cursorX + glyph.xOffset) * fuToPx + m.bearingX;
895
+ const offsetPxY = (cursorY + glyph.yOffset) * fuToPx + m.bearingY;
896
+
897
+ // Atlas rects are stored in PIXEL space; the shader divides by the
898
+ // current uSdfAtlasSize / uColorAtlasSize uniform to derive UVs, so
899
+ // these instance attrs survive an atlas resize without rebuilding.
900
+ renderable.push({
901
+ offsetEmX: offsetPxX / px,
902
+ offsetEmY: offsetPxY / px,
903
+ sizeEmX: m.atlasW / px,
904
+ sizeEmY: m.atlasH / px,
905
+ uvL: m.atlasX,
906
+ uvT: m.atlasY,
907
+ uvR: m.atlasX + m.atlasW,
908
+ uvB: m.atlasY + m.atlasH,
909
+ isColor: m.isColor,
910
+ });
911
+ }
912
+ cursorX += glyph.xAdvance;
913
+ cursorY += glyph.yAdvance;
914
+ }
915
+ }
916
+
917
+ // Update atlas references to this mesh's new glyph set (retains/releases
918
+ // as needed based on visibility).
919
+ this._setGlyphKeys([...glyphKeys]);
920
+
921
+ const count = renderable.length;
922
+ if (count === 0) {
923
+ this.geometry.instanceCount = 0;
924
+ return;
925
+ }
926
+
927
+ // Text-width metric uses SDF scale; same em-space as the SDF path used to.
928
+ // The block is as wide as its widest line and grows downward by one line
929
+ // height per extra line (a single line stays exactly one em tall).
930
+ const textWidth = blockWidthFu * fontUnitToSdfPx;
931
+ const textHeight =
932
+ SDF_PX_SIZE + (lines.length - 1) * lineHeightFu * fontUnitToSdfPx;
933
+
934
+ const glyphOffsetData = new Float32Array((count + 1) * 2);
935
+ const glyphSizeData = new Float32Array((count + 1) * 2);
936
+ const glyphUvRectData = new Float32Array((count + 1) * 4);
937
+ const glyphIsColorData = new Float32Array(count + 1);
938
+
939
+ // Compute actual Y bounding box of all rendered glyphs
940
+ let bgMinY = Infinity;
941
+ let bgMaxY = -Infinity;
942
+
943
+ // Index 0 is reserved for the background instance (drawn first).
944
+ // Glyph data starts at index 1.
945
+ for (let i = 0; i < count; i++) {
946
+ const g = renderable[i];
947
+ const j = i + 1; // offset by 1 to leave index 0 for background
948
+
949
+ glyphOffsetData[j * 2] = g.offsetEmX;
950
+ glyphOffsetData[j * 2 + 1] = g.offsetEmY;
951
+
952
+ glyphSizeData[j * 2] = g.sizeEmX;
953
+ glyphSizeData[j * 2 + 1] = g.sizeEmY;
954
+
955
+ bgMinY = Math.min(bgMinY, g.offsetEmY);
956
+ bgMaxY = Math.max(bgMaxY, g.offsetEmY + g.sizeEmY);
957
+
958
+ glyphUvRectData[j * 4] = g.uvL;
959
+ glyphUvRectData[j * 4 + 1] = g.uvT;
960
+ glyphUvRectData[j * 4 + 2] = g.uvR;
961
+ glyphUvRectData[j * 4 + 3] = g.uvB;
962
+
963
+ glyphIsColorData[j] = g.isColor ? 1.0 : 0.0;
964
+ }
965
+
966
+ // Recreate geometry if instance count grew beyond the current capacity.
967
+ if (this.geometry.instanceCount < count + 1) {
968
+ this.geometry.dispose();
969
+ this.geometry = this._createBaseGeometry();
970
+ }
971
+
972
+ this._setInstanceAttribute("glyphOffset", glyphOffsetData, 2);
973
+ this._setInstanceAttribute("glyphSize", glyphSizeData, 2);
974
+ this._setInstanceAttribute("glyphUvRect", glyphUvRectData, 4);
975
+ this._setInstanceAttribute("glyphIsColor", glyphIsColorData, 1);
976
+
977
+ this.geometry.instanceCount = count + 1;
978
+
979
+ // Update text dimension uniforms via mutates
980
+ this._enhancer
981
+ .mutates()
982
+ .updateTextDimensions(
983
+ textWidth / SDF_PX_SIZE,
984
+ textHeight / SDF_PX_SIZE,
985
+ bgMinY === Infinity ? 0.0 : bgMinY,
986
+ bgMaxY === -Infinity ? 1.0 : bgMaxY,
987
+ );
988
+ }
989
+
990
+ private _setInstanceAttribute(
991
+ name: string,
992
+ data: Float32Array,
993
+ itemSize: number,
994
+ ): void {
995
+ if (this.geometry.hasAttribute(name)) this.geometry.deleteAttribute(name);
996
+ this.geometry.setAttribute(
997
+ name,
998
+ new InstancedBufferAttribute(data, itemSize),
999
+ );
1000
+ }
1001
+
1002
+ private _updateAtlasTexture(
1003
+ data: Uint8Array,
1004
+ width: number,
1005
+ height: number,
1006
+ channels: number,
1007
+ ): void {
1008
+ if (this._atlasTexture) {
1009
+ // Update existing texture in-place (atlas dimensions are constant)
1010
+ this._atlasTexture.image = { data, width, height };
1011
+ this._atlasTexture.needsUpdate = true;
1012
+ return;
1013
+ }
1014
+
1015
+ const tex = createSdfAtlasTexture(data, width, height, channels);
1016
+ this._atlasTexture = tex;
1017
+ this._enhancer.mutates().setAtlasTexture({ value: tex });
1018
+ }
1019
+ }