@ifc-lite/viewer 1.17.3 → 1.17.6

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 (530) hide show
  1. package/.turbo/turbo-build.log +39 -30
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +132 -0
  4. package/DESKTOP_CONTRACT_VERSION +1 -0
  5. package/dist/assets/arrow-CZ5kQ26f.js +20 -0
  6. package/dist/assets/basketViewActivator-86rgogji.js +1 -0
  7. package/dist/assets/{bcf-D5-QWGO9.js → bcf-DOG9_WPX.js} +1 -1
  8. package/dist/assets/{browser-CKs-FY1P.js → browser-C5TFR7sH.js} +1 -1
  9. package/dist/assets/cesium-ADbP7waU.css +1 -0
  10. package/dist/assets/cesium-DUOzBlqv.js +17817 -0
  11. package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
  12. package/dist/assets/{exporters-C_6J153K.js → exporters-CcPS9MK5.js} +2898 -2380
  13. package/dist/assets/geometry.worker-BFUYA08u.js +1 -0
  14. package/dist/assets/ids-DQ5jY0E8.js +1 -0
  15. package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
  16. package/dist/assets/{index-jhBr1wbn.js → index-Bfms9I4A.js} +41036 -34613
  17. package/dist/assets/index-_bfZsDCC.css +1 -0
  18. package/dist/assets/{maplibre-gl-BpvwNKKy.js → maplibre-gl-CGLcoNXc.js} +1 -1
  19. package/dist/assets/native-bridge-DUyLCMZS.js +429 -0
  20. package/dist/assets/{sandbox-B79eavQ3.js → sandbox-C8575tul.js} +4342 -4324
  21. package/dist/assets/{server-client-D3bUPJJc.js → server-client-BuZK7OST.js} +4 -4
  22. package/dist/assets/tauri-core-stub-D8Fa-u43.js +1 -0
  23. package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +1 -0
  24. package/dist/assets/tauri-fs-stub-BdeRC7aK.js +1 -0
  25. package/dist/assets/wasm-bridge-JsqEGDV8.js +1 -0
  26. package/dist/assets/{zip-B-jFFAGa.js → zip-DBEtpeu6.js} +3 -3
  27. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_0.json +1 -0
  28. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_1.json +1 -0
  29. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_10.json +1 -0
  30. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_11.json +1 -0
  31. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_12.json +1 -0
  32. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_13.json +1 -0
  33. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_14.json +1 -0
  34. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_15.json +1 -0
  35. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_16.json +1 -0
  36. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_17.json +1 -0
  37. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_18.json +1 -0
  38. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_19.json +1 -0
  39. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_2.json +1 -0
  40. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_20.json +1 -0
  41. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_21.json +1 -0
  42. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_22.json +1 -0
  43. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_23.json +1 -0
  44. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_24.json +1 -0
  45. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_25.json +1 -0
  46. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_26.json +1 -0
  47. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_27.json +1 -0
  48. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_3.json +1 -0
  49. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_4.json +1 -0
  50. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_5.json +1 -0
  51. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_6.json +1 -0
  52. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_7.json +1 -0
  53. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_8.json +1 -0
  54. package/dist/cesium/Assets/IAU2006_XYS/IAU2006_XYS_9.json +1 -0
  55. package/dist/cesium/Assets/Images/bing_maps_credit.png +0 -0
  56. package/dist/cesium/Assets/Images/cesium_credit.png +0 -0
  57. package/dist/cesium/Assets/Images/google_earth_credit.png +0 -0
  58. package/dist/cesium/Assets/Images/ion-credit.png +0 -0
  59. package/dist/cesium/Assets/Textures/LensFlare/DirtMask.jpg +0 -0
  60. package/dist/cesium/Assets/Textures/LensFlare/StarBurst.jpg +0 -0
  61. package/dist/cesium/Assets/Textures/NaturalEarthII/0/0/0.jpg +0 -0
  62. package/dist/cesium/Assets/Textures/NaturalEarthII/0/1/0.jpg +0 -0
  63. package/dist/cesium/Assets/Textures/NaturalEarthII/1/0/0.jpg +0 -0
  64. package/dist/cesium/Assets/Textures/NaturalEarthII/1/0/1.jpg +0 -0
  65. package/dist/cesium/Assets/Textures/NaturalEarthII/1/1/0.jpg +0 -0
  66. package/dist/cesium/Assets/Textures/NaturalEarthII/1/1/1.jpg +0 -0
  67. package/dist/cesium/Assets/Textures/NaturalEarthII/1/2/0.jpg +0 -0
  68. package/dist/cesium/Assets/Textures/NaturalEarthII/1/2/1.jpg +0 -0
  69. package/dist/cesium/Assets/Textures/NaturalEarthII/1/3/0.jpg +0 -0
  70. package/dist/cesium/Assets/Textures/NaturalEarthII/1/3/1.jpg +0 -0
  71. package/dist/cesium/Assets/Textures/NaturalEarthII/2/0/0.jpg +0 -0
  72. package/dist/cesium/Assets/Textures/NaturalEarthII/2/0/1.jpg +0 -0
  73. package/dist/cesium/Assets/Textures/NaturalEarthII/2/0/2.jpg +0 -0
  74. package/dist/cesium/Assets/Textures/NaturalEarthII/2/0/3.jpg +0 -0
  75. package/dist/cesium/Assets/Textures/NaturalEarthII/2/1/0.jpg +0 -0
  76. package/dist/cesium/Assets/Textures/NaturalEarthII/2/1/1.jpg +0 -0
  77. package/dist/cesium/Assets/Textures/NaturalEarthII/2/1/2.jpg +0 -0
  78. package/dist/cesium/Assets/Textures/NaturalEarthII/2/1/3.jpg +0 -0
  79. package/dist/cesium/Assets/Textures/NaturalEarthII/2/2/0.jpg +0 -0
  80. package/dist/cesium/Assets/Textures/NaturalEarthII/2/2/1.jpg +0 -0
  81. package/dist/cesium/Assets/Textures/NaturalEarthII/2/2/2.jpg +0 -0
  82. package/dist/cesium/Assets/Textures/NaturalEarthII/2/2/3.jpg +0 -0
  83. package/dist/cesium/Assets/Textures/NaturalEarthII/2/3/0.jpg +0 -0
  84. package/dist/cesium/Assets/Textures/NaturalEarthII/2/3/1.jpg +0 -0
  85. package/dist/cesium/Assets/Textures/NaturalEarthII/2/3/2.jpg +0 -0
  86. package/dist/cesium/Assets/Textures/NaturalEarthII/2/3/3.jpg +0 -0
  87. package/dist/cesium/Assets/Textures/NaturalEarthII/2/4/0.jpg +0 -0
  88. package/dist/cesium/Assets/Textures/NaturalEarthII/2/4/1.jpg +0 -0
  89. package/dist/cesium/Assets/Textures/NaturalEarthII/2/4/2.jpg +0 -0
  90. package/dist/cesium/Assets/Textures/NaturalEarthII/2/4/3.jpg +0 -0
  91. package/dist/cesium/Assets/Textures/NaturalEarthII/2/5/0.jpg +0 -0
  92. package/dist/cesium/Assets/Textures/NaturalEarthII/2/5/1.jpg +0 -0
  93. package/dist/cesium/Assets/Textures/NaturalEarthII/2/5/2.jpg +0 -0
  94. package/dist/cesium/Assets/Textures/NaturalEarthII/2/5/3.jpg +0 -0
  95. package/dist/cesium/Assets/Textures/NaturalEarthII/2/6/0.jpg +0 -0
  96. package/dist/cesium/Assets/Textures/NaturalEarthII/2/6/1.jpg +0 -0
  97. package/dist/cesium/Assets/Textures/NaturalEarthII/2/6/2.jpg +0 -0
  98. package/dist/cesium/Assets/Textures/NaturalEarthII/2/6/3.jpg +0 -0
  99. package/dist/cesium/Assets/Textures/NaturalEarthII/2/7/0.jpg +0 -0
  100. package/dist/cesium/Assets/Textures/NaturalEarthII/2/7/1.jpg +0 -0
  101. package/dist/cesium/Assets/Textures/NaturalEarthII/2/7/2.jpg +0 -0
  102. package/dist/cesium/Assets/Textures/NaturalEarthII/2/7/3.jpg +0 -0
  103. package/dist/cesium/Assets/Textures/NaturalEarthII/tilemapresource.xml +14 -0
  104. package/dist/cesium/Assets/Textures/SkyBox/tycho2t3_80_mx.jpg +0 -0
  105. package/dist/cesium/Assets/Textures/SkyBox/tycho2t3_80_my.jpg +0 -0
  106. package/dist/cesium/Assets/Textures/SkyBox/tycho2t3_80_mz.jpg +0 -0
  107. package/dist/cesium/Assets/Textures/SkyBox/tycho2t3_80_px.jpg +0 -0
  108. package/dist/cesium/Assets/Textures/SkyBox/tycho2t3_80_py.jpg +0 -0
  109. package/dist/cesium/Assets/Textures/SkyBox/tycho2t3_80_pz.jpg +0 -0
  110. package/dist/cesium/Assets/Textures/maki/airfield.png +0 -0
  111. package/dist/cesium/Assets/Textures/maki/airport.png +0 -0
  112. package/dist/cesium/Assets/Textures/maki/alcohol-shop.png +0 -0
  113. package/dist/cesium/Assets/Textures/maki/america-football.png +0 -0
  114. package/dist/cesium/Assets/Textures/maki/art-gallery.png +0 -0
  115. package/dist/cesium/Assets/Textures/maki/bakery.png +0 -0
  116. package/dist/cesium/Assets/Textures/maki/bank.png +0 -0
  117. package/dist/cesium/Assets/Textures/maki/bar.png +0 -0
  118. package/dist/cesium/Assets/Textures/maki/baseball.png +0 -0
  119. package/dist/cesium/Assets/Textures/maki/basketball.png +0 -0
  120. package/dist/cesium/Assets/Textures/maki/beer.png +0 -0
  121. package/dist/cesium/Assets/Textures/maki/bicycle.png +0 -0
  122. package/dist/cesium/Assets/Textures/maki/building.png +0 -0
  123. package/dist/cesium/Assets/Textures/maki/bus.png +0 -0
  124. package/dist/cesium/Assets/Textures/maki/cafe.png +0 -0
  125. package/dist/cesium/Assets/Textures/maki/camera.png +0 -0
  126. package/dist/cesium/Assets/Textures/maki/campsite.png +0 -0
  127. package/dist/cesium/Assets/Textures/maki/car.png +0 -0
  128. package/dist/cesium/Assets/Textures/maki/cemetery.png +0 -0
  129. package/dist/cesium/Assets/Textures/maki/cesium.png +0 -0
  130. package/dist/cesium/Assets/Textures/maki/chemist.png +0 -0
  131. package/dist/cesium/Assets/Textures/maki/cinema.png +0 -0
  132. package/dist/cesium/Assets/Textures/maki/circle-stroked.png +0 -0
  133. package/dist/cesium/Assets/Textures/maki/circle.png +0 -0
  134. package/dist/cesium/Assets/Textures/maki/city.png +0 -0
  135. package/dist/cesium/Assets/Textures/maki/clothing-store.png +0 -0
  136. package/dist/cesium/Assets/Textures/maki/college.png +0 -0
  137. package/dist/cesium/Assets/Textures/maki/commercial.png +0 -0
  138. package/dist/cesium/Assets/Textures/maki/cricket.png +0 -0
  139. package/dist/cesium/Assets/Textures/maki/cross.png +0 -0
  140. package/dist/cesium/Assets/Textures/maki/dam.png +0 -0
  141. package/dist/cesium/Assets/Textures/maki/danger.png +0 -0
  142. package/dist/cesium/Assets/Textures/maki/disability.png +0 -0
  143. package/dist/cesium/Assets/Textures/maki/dog-park.png +0 -0
  144. package/dist/cesium/Assets/Textures/maki/embassy.png +0 -0
  145. package/dist/cesium/Assets/Textures/maki/emergency-telephone.png +0 -0
  146. package/dist/cesium/Assets/Textures/maki/entrance.png +0 -0
  147. package/dist/cesium/Assets/Textures/maki/farm.png +0 -0
  148. package/dist/cesium/Assets/Textures/maki/fast-food.png +0 -0
  149. package/dist/cesium/Assets/Textures/maki/ferry.png +0 -0
  150. package/dist/cesium/Assets/Textures/maki/fire-station.png +0 -0
  151. package/dist/cesium/Assets/Textures/maki/fuel.png +0 -0
  152. package/dist/cesium/Assets/Textures/maki/garden.png +0 -0
  153. package/dist/cesium/Assets/Textures/maki/gift.png +0 -0
  154. package/dist/cesium/Assets/Textures/maki/golf.png +0 -0
  155. package/dist/cesium/Assets/Textures/maki/grocery.png +0 -0
  156. package/dist/cesium/Assets/Textures/maki/hairdresser.png +0 -0
  157. package/dist/cesium/Assets/Textures/maki/harbor.png +0 -0
  158. package/dist/cesium/Assets/Textures/maki/heart.png +0 -0
  159. package/dist/cesium/Assets/Textures/maki/heliport.png +0 -0
  160. package/dist/cesium/Assets/Textures/maki/hospital.png +0 -0
  161. package/dist/cesium/Assets/Textures/maki/ice-cream.png +0 -0
  162. package/dist/cesium/Assets/Textures/maki/industrial.png +0 -0
  163. package/dist/cesium/Assets/Textures/maki/land-use.png +0 -0
  164. package/dist/cesium/Assets/Textures/maki/laundry.png +0 -0
  165. package/dist/cesium/Assets/Textures/maki/library.png +0 -0
  166. package/dist/cesium/Assets/Textures/maki/lighthouse.png +0 -0
  167. package/dist/cesium/Assets/Textures/maki/lodging.png +0 -0
  168. package/dist/cesium/Assets/Textures/maki/logging.png +0 -0
  169. package/dist/cesium/Assets/Textures/maki/london-underground.png +0 -0
  170. package/dist/cesium/Assets/Textures/maki/marker-stroked.png +0 -0
  171. package/dist/cesium/Assets/Textures/maki/marker.png +0 -0
  172. package/dist/cesium/Assets/Textures/maki/minefield.png +0 -0
  173. package/dist/cesium/Assets/Textures/maki/mobilephone.png +0 -0
  174. package/dist/cesium/Assets/Textures/maki/monument.png +0 -0
  175. package/dist/cesium/Assets/Textures/maki/museum.png +0 -0
  176. package/dist/cesium/Assets/Textures/maki/music.png +0 -0
  177. package/dist/cesium/Assets/Textures/maki/oil-well.png +0 -0
  178. package/dist/cesium/Assets/Textures/maki/park.png +0 -0
  179. package/dist/cesium/Assets/Textures/maki/park2.png +0 -0
  180. package/dist/cesium/Assets/Textures/maki/parking-garage.png +0 -0
  181. package/dist/cesium/Assets/Textures/maki/parking.png +0 -0
  182. package/dist/cesium/Assets/Textures/maki/pharmacy.png +0 -0
  183. package/dist/cesium/Assets/Textures/maki/pitch.png +0 -0
  184. package/dist/cesium/Assets/Textures/maki/place-of-worship.png +0 -0
  185. package/dist/cesium/Assets/Textures/maki/playground.png +0 -0
  186. package/dist/cesium/Assets/Textures/maki/police.png +0 -0
  187. package/dist/cesium/Assets/Textures/maki/polling-place.png +0 -0
  188. package/dist/cesium/Assets/Textures/maki/post.png +0 -0
  189. package/dist/cesium/Assets/Textures/maki/prison.png +0 -0
  190. package/dist/cesium/Assets/Textures/maki/rail-above.png +0 -0
  191. package/dist/cesium/Assets/Textures/maki/rail-light.png +0 -0
  192. package/dist/cesium/Assets/Textures/maki/rail-metro.png +0 -0
  193. package/dist/cesium/Assets/Textures/maki/rail-underground.png +0 -0
  194. package/dist/cesium/Assets/Textures/maki/rail.png +0 -0
  195. package/dist/cesium/Assets/Textures/maki/religious-christian.png +0 -0
  196. package/dist/cesium/Assets/Textures/maki/religious-jewish.png +0 -0
  197. package/dist/cesium/Assets/Textures/maki/religious-muslim.png +0 -0
  198. package/dist/cesium/Assets/Textures/maki/restaurant.png +0 -0
  199. package/dist/cesium/Assets/Textures/maki/roadblock.png +0 -0
  200. package/dist/cesium/Assets/Textures/maki/rocket.png +0 -0
  201. package/dist/cesium/Assets/Textures/maki/school.png +0 -0
  202. package/dist/cesium/Assets/Textures/maki/scooter.png +0 -0
  203. package/dist/cesium/Assets/Textures/maki/shop.png +0 -0
  204. package/dist/cesium/Assets/Textures/maki/skiing.png +0 -0
  205. package/dist/cesium/Assets/Textures/maki/slaughterhouse.png +0 -0
  206. package/dist/cesium/Assets/Textures/maki/soccer.png +0 -0
  207. package/dist/cesium/Assets/Textures/maki/square-stroked.png +0 -0
  208. package/dist/cesium/Assets/Textures/maki/square.png +0 -0
  209. package/dist/cesium/Assets/Textures/maki/star-stroked.png +0 -0
  210. package/dist/cesium/Assets/Textures/maki/star.png +0 -0
  211. package/dist/cesium/Assets/Textures/maki/suitcase.png +0 -0
  212. package/dist/cesium/Assets/Textures/maki/swimming.png +0 -0
  213. package/dist/cesium/Assets/Textures/maki/telephone.png +0 -0
  214. package/dist/cesium/Assets/Textures/maki/tennis.png +0 -0
  215. package/dist/cesium/Assets/Textures/maki/theatre.png +0 -0
  216. package/dist/cesium/Assets/Textures/maki/toilets.png +0 -0
  217. package/dist/cesium/Assets/Textures/maki/town-hall.png +0 -0
  218. package/dist/cesium/Assets/Textures/maki/town.png +0 -0
  219. package/dist/cesium/Assets/Textures/maki/triangle-stroked.png +0 -0
  220. package/dist/cesium/Assets/Textures/maki/triangle.png +0 -0
  221. package/dist/cesium/Assets/Textures/maki/village.png +0 -0
  222. package/dist/cesium/Assets/Textures/maki/warehouse.png +0 -0
  223. package/dist/cesium/Assets/Textures/maki/waste-basket.png +0 -0
  224. package/dist/cesium/Assets/Textures/maki/water.png +0 -0
  225. package/dist/cesium/Assets/Textures/maki/wetland.png +0 -0
  226. package/dist/cesium/Assets/Textures/maki/zoo.png +0 -0
  227. package/dist/cesium/Assets/Textures/moonSmall.jpg +0 -0
  228. package/dist/cesium/Assets/Textures/pin.svg +1 -0
  229. package/dist/cesium/Assets/Textures/waterNormals.jpg +0 -0
  230. package/dist/cesium/Assets/Textures/waterNormalsSmall.jpg +0 -0
  231. package/dist/cesium/Assets/approximateTerrainHeights.json +1 -0
  232. package/dist/cesium/ThirdParty/Workers/package.json +1 -0
  233. package/dist/cesium/ThirdParty/Workers/zip-web-worker.js +1 -0
  234. package/dist/cesium/ThirdParty/basis_transcoder.wasm +0 -0
  235. package/dist/cesium/ThirdParty/draco_decoder.wasm +0 -0
  236. package/dist/cesium/ThirdParty/google-earth-dbroot-parser.js +1 -0
  237. package/dist/cesium/ThirdParty/wasm_splats_bg.wasm +0 -0
  238. package/dist/cesium/ThirdParty/zip-module.wasm +0 -0
  239. package/dist/cesium/Widgets/Animation/Animation.css +127 -0
  240. package/dist/cesium/Widgets/Animation/lighter.css +70 -0
  241. package/dist/cesium/Widgets/BaseLayerPicker/BaseLayerPicker.css +108 -0
  242. package/dist/cesium/Widgets/BaseLayerPicker/lighter.css +22 -0
  243. package/dist/cesium/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector.css +102 -0
  244. package/dist/cesium/Widgets/CesiumInspector/CesiumInspector.css +113 -0
  245. package/dist/cesium/Widgets/CesiumWidget/CesiumWidget.css +119 -0
  246. package/dist/cesium/Widgets/CesiumWidget/lighter.css +14 -0
  247. package/dist/cesium/Widgets/FullscreenButton/FullscreenButton.css +8 -0
  248. package/dist/cesium/Widgets/Geocoder/Geocoder.css +70 -0
  249. package/dist/cesium/Widgets/Geocoder/lighter.css +17 -0
  250. package/dist/cesium/Widgets/I3SBuildingSceneLayerExplorer/I3SBuildingSceneLayerExplorer.css +27 -0
  251. package/dist/cesium/Widgets/Images/ImageryProviders/ArcGisMapServiceWorldHillshade.png +0 -0
  252. package/dist/cesium/Widgets/Images/ImageryProviders/ArcGisMapServiceWorldImagery.png +0 -0
  253. package/dist/cesium/Widgets/Images/ImageryProviders/ArcGisMapServiceWorldOcean.png +0 -0
  254. package/dist/cesium/Widgets/Images/ImageryProviders/azureAerial.png +0 -0
  255. package/dist/cesium/Widgets/Images/ImageryProviders/azureRoads.png +0 -0
  256. package/dist/cesium/Widgets/Images/ImageryProviders/bingAerial.png +0 -0
  257. package/dist/cesium/Widgets/Images/ImageryProviders/bingAerialLabels.png +0 -0
  258. package/dist/cesium/Widgets/Images/ImageryProviders/bingRoads.png +0 -0
  259. package/dist/cesium/Widgets/Images/ImageryProviders/blueMarble.png +0 -0
  260. package/dist/cesium/Widgets/Images/ImageryProviders/earthAtNight.png +0 -0
  261. package/dist/cesium/Widgets/Images/ImageryProviders/googleContour.png +0 -0
  262. package/dist/cesium/Widgets/Images/ImageryProviders/googleRoadmap.png +0 -0
  263. package/dist/cesium/Widgets/Images/ImageryProviders/googleSatellite.png +0 -0
  264. package/dist/cesium/Widgets/Images/ImageryProviders/googleSatelliteLabels.png +0 -0
  265. package/dist/cesium/Widgets/Images/ImageryProviders/mapQuestOpenStreetMap.png +0 -0
  266. package/dist/cesium/Widgets/Images/ImageryProviders/mapboxSatellite.png +0 -0
  267. package/dist/cesium/Widgets/Images/ImageryProviders/mapboxStreets.png +0 -0
  268. package/dist/cesium/Widgets/Images/ImageryProviders/mapboxTerrain.png +0 -0
  269. package/dist/cesium/Widgets/Images/ImageryProviders/naturalEarthII.png +0 -0
  270. package/dist/cesium/Widgets/Images/ImageryProviders/openStreetMap.png +0 -0
  271. package/dist/cesium/Widgets/Images/ImageryProviders/sentinel-2.png +0 -0
  272. package/dist/cesium/Widgets/Images/ImageryProviders/stadiaAlidadeSmooth.png +0 -0
  273. package/dist/cesium/Widgets/Images/ImageryProviders/stadiaAlidadeSmoothDark.png +0 -0
  274. package/dist/cesium/Widgets/Images/ImageryProviders/stamenToner.png +0 -0
  275. package/dist/cesium/Widgets/Images/ImageryProviders/stamenWatercolor.png +0 -0
  276. package/dist/cesium/Widgets/Images/NavigationHelp/Mouse.svg +84 -0
  277. package/dist/cesium/Widgets/Images/NavigationHelp/MouseLeft.svg +76 -0
  278. package/dist/cesium/Widgets/Images/NavigationHelp/MouseMiddle.svg +76 -0
  279. package/dist/cesium/Widgets/Images/NavigationHelp/MouseRight.svg +76 -0
  280. package/dist/cesium/Widgets/Images/NavigationHelp/Touch.svg +120 -0
  281. package/dist/cesium/Widgets/Images/NavigationHelp/TouchDrag.svg +129 -0
  282. package/dist/cesium/Widgets/Images/NavigationHelp/TouchRotate.svg +76 -0
  283. package/dist/cesium/Widgets/Images/NavigationHelp/TouchTilt.svg +135 -0
  284. package/dist/cesium/Widgets/Images/NavigationHelp/TouchZoom.svg +74 -0
  285. package/dist/cesium/Widgets/Images/TerrainProviders/CesiumWorldTerrain.png +0 -0
  286. package/dist/cesium/Widgets/Images/TerrainProviders/Ellipsoid.png +0 -0
  287. package/dist/cesium/Widgets/Images/TimelineIcons.png +0 -0
  288. package/dist/cesium/Widgets/Images/info-loading.gif +0 -0
  289. package/dist/cesium/Widgets/InfoBox/InfoBox.css +92 -0
  290. package/dist/cesium/Widgets/InfoBox/InfoBoxDescription.css +178 -0
  291. package/dist/cesium/Widgets/NavigationHelpButton/NavigationHelpButton.css +93 -0
  292. package/dist/cesium/Widgets/NavigationHelpButton/lighter.css +38 -0
  293. package/dist/cesium/Widgets/PerformanceWatchdog/PerformanceWatchdog.css +15 -0
  294. package/dist/cesium/Widgets/ProjectionPicker/ProjectionPicker.css +38 -0
  295. package/dist/cesium/Widgets/SceneModePicker/SceneModePicker.css +56 -0
  296. package/dist/cesium/Widgets/SelectionIndicator/SelectionIndicator.css +20 -0
  297. package/dist/cesium/Widgets/Timeline/Timeline.css +103 -0
  298. package/dist/cesium/Widgets/Timeline/lighter.css +23 -0
  299. package/dist/cesium/Widgets/VRButton/VRButton.css +8 -0
  300. package/dist/cesium/Widgets/Viewer/Viewer.css +107 -0
  301. package/dist/cesium/Widgets/VoxelInspector/VoxelInspector.css +16 -0
  302. package/dist/cesium/Widgets/lighter.css +237 -0
  303. package/dist/cesium/Widgets/lighterShared.css +46 -0
  304. package/dist/cesium/Widgets/shared.css +103 -0
  305. package/dist/cesium/Widgets/widgets.css +1342 -0
  306. package/dist/cesium/Workers/chunk-23ZQ2IVV.js +29 -0
  307. package/dist/cesium/Workers/chunk-2EQO3Q56.js +26 -0
  308. package/dist/cesium/Workers/chunk-2MJIIVP4.js +26 -0
  309. package/dist/cesium/Workers/chunk-2TE5NTVD.js +26 -0
  310. package/dist/cesium/Workers/chunk-2ZBHLJST.js +26 -0
  311. package/dist/cesium/Workers/chunk-5TJMAQVL.js +26 -0
  312. package/dist/cesium/Workers/chunk-6BD4U3VO.js +26 -0
  313. package/dist/cesium/Workers/chunk-7TVGLKQF.js +26 -0
  314. package/dist/cesium/Workers/chunk-BTSYJ5XU.js +26 -0
  315. package/dist/cesium/Workers/chunk-BXMEEOCS.js +63 -0
  316. package/dist/cesium/Workers/chunk-BYLCY7GP.js +29 -0
  317. package/dist/cesium/Workers/chunk-CTHM3W6I.js +26 -0
  318. package/dist/cesium/Workers/chunk-CUUSNIVQ.js +26 -0
  319. package/dist/cesium/Workers/chunk-E3JOOS3S.js +26 -0
  320. package/dist/cesium/Workers/chunk-E7KYDCM5.js +26 -0
  321. package/dist/cesium/Workers/chunk-EDVBB7SS.js +27 -0
  322. package/dist/cesium/Workers/chunk-EFBN7QNX.js +26 -0
  323. package/dist/cesium/Workers/chunk-EQ4YRVWL.js +26 -0
  324. package/dist/cesium/Workers/chunk-F6PRE7D6.js +26 -0
  325. package/dist/cesium/Workers/chunk-FC4ZZ65J.js +26 -0
  326. package/dist/cesium/Workers/chunk-FFBVWF2L.js +26 -0
  327. package/dist/cesium/Workers/chunk-GBAA6GVX.js +26 -0
  328. package/dist/cesium/Workers/chunk-ICALLYLG.js +26 -0
  329. package/dist/cesium/Workers/chunk-ILRYTWTP.js +26 -0
  330. package/dist/cesium/Workers/chunk-IRNLBSEJ.js +26 -0
  331. package/dist/cesium/Workers/chunk-IX4VMHEV.js +26 -0
  332. package/dist/cesium/Workers/chunk-L6QHHACZ.js +26 -0
  333. package/dist/cesium/Workers/chunk-LI2ZSORM.js +26 -0
  334. package/dist/cesium/Workers/chunk-LSLE2RL4.js +26 -0
  335. package/dist/cesium/Workers/chunk-M4HLDBCG.js +26 -0
  336. package/dist/cesium/Workers/chunk-MJHHSGEH.js +26 -0
  337. package/dist/cesium/Workers/chunk-NMVKML6W.js +26 -0
  338. package/dist/cesium/Workers/chunk-OCWJRAXS.js +26 -0
  339. package/dist/cesium/Workers/chunk-OIRKANTH.js +26 -0
  340. package/dist/cesium/Workers/chunk-OIT7J4IC.js +26 -0
  341. package/dist/cesium/Workers/chunk-OLZ3FYUM.js +26 -0
  342. package/dist/cesium/Workers/chunk-Q5BPHJQF.js +26 -0
  343. package/dist/cesium/Workers/chunk-QFM5DCMQ.js +26 -0
  344. package/dist/cesium/Workers/chunk-QKUIYMGC.js +28 -0
  345. package/dist/cesium/Workers/chunk-S44JILQT.js +26 -0
  346. package/dist/cesium/Workers/chunk-SLT4J352.js +26 -0
  347. package/dist/cesium/Workers/chunk-SQMIIXB7.js +26 -0
  348. package/dist/cesium/Workers/chunk-TJ4XLGBQ.js +26 -0
  349. package/dist/cesium/Workers/chunk-TNSUQXWK.js +27 -0
  350. package/dist/cesium/Workers/chunk-UBOGZS7F.js +26 -0
  351. package/dist/cesium/Workers/chunk-V3OSTMM6.js +26 -0
  352. package/dist/cesium/Workers/chunk-V7QEYVP3.js +26 -0
  353. package/dist/cesium/Workers/chunk-VUKYSU4H.js +26 -0
  354. package/dist/cesium/Workers/chunk-W37FE5GR.js +26 -0
  355. package/dist/cesium/Workers/chunk-WBOV35NL.js +26 -0
  356. package/dist/cesium/Workers/chunk-WPMZLB3Y.js +26 -0
  357. package/dist/cesium/Workers/chunk-WWWZVEEH.js +26 -0
  358. package/dist/cesium/Workers/chunk-XFIQ5DEQ.js +28 -0
  359. package/dist/cesium/Workers/chunk-XQHLGIO7.js +26 -0
  360. package/dist/cesium/Workers/chunk-XUSCFAVF.js +26 -0
  361. package/dist/cesium/Workers/chunk-YP7I5QBZ.js +26 -0
  362. package/dist/cesium/Workers/chunk-Z3QF2EHT.js +26 -0
  363. package/dist/cesium/Workers/combineGeometry.js +26 -0
  364. package/dist/cesium/Workers/createBoxGeometry.js +26 -0
  365. package/dist/cesium/Workers/createBoxOutlineGeometry.js +26 -0
  366. package/dist/cesium/Workers/createCircleGeometry.js +26 -0
  367. package/dist/cesium/Workers/createCircleOutlineGeometry.js +26 -0
  368. package/dist/cesium/Workers/createCoplanarPolygonGeometry.js +26 -0
  369. package/dist/cesium/Workers/createCoplanarPolygonOutlineGeometry.js +26 -0
  370. package/dist/cesium/Workers/createCorridorGeometry.js +26 -0
  371. package/dist/cesium/Workers/createCorridorOutlineGeometry.js +26 -0
  372. package/dist/cesium/Workers/createCylinderGeometry.js +26 -0
  373. package/dist/cesium/Workers/createCylinderOutlineGeometry.js +26 -0
  374. package/dist/cesium/Workers/createEllipseGeometry.js +26 -0
  375. package/dist/cesium/Workers/createEllipseOutlineGeometry.js +26 -0
  376. package/dist/cesium/Workers/createEllipsoidGeometry.js +26 -0
  377. package/dist/cesium/Workers/createEllipsoidOutlineGeometry.js +26 -0
  378. package/dist/cesium/Workers/createFrustumGeometry.js +26 -0
  379. package/dist/cesium/Workers/createFrustumOutlineGeometry.js +26 -0
  380. package/dist/cesium/Workers/createGeometry.js +26 -0
  381. package/dist/cesium/Workers/createGroundPolylineGeometry.js +26 -0
  382. package/dist/cesium/Workers/createPlaneGeometry.js +26 -0
  383. package/dist/cesium/Workers/createPlaneOutlineGeometry.js +26 -0
  384. package/dist/cesium/Workers/createPolygonGeometry.js +26 -0
  385. package/dist/cesium/Workers/createPolygonOutlineGeometry.js +26 -0
  386. package/dist/cesium/Workers/createPolylineGeometry.js +26 -0
  387. package/dist/cesium/Workers/createPolylineVolumeGeometry.js +26 -0
  388. package/dist/cesium/Workers/createPolylineVolumeOutlineGeometry.js +26 -0
  389. package/dist/cesium/Workers/createRectangleGeometry.js +26 -0
  390. package/dist/cesium/Workers/createRectangleOutlineGeometry.js +26 -0
  391. package/dist/cesium/Workers/createSimplePolylineGeometry.js +26 -0
  392. package/dist/cesium/Workers/createSphereGeometry.js +26 -0
  393. package/dist/cesium/Workers/createSphereOutlineGeometry.js +26 -0
  394. package/dist/cesium/Workers/createTaskProcessorWorker.js +26 -0
  395. package/dist/cesium/Workers/createVectorTileClampedPolylines.js +26 -0
  396. package/dist/cesium/Workers/createVectorTileGeometries.js +26 -0
  397. package/dist/cesium/Workers/createVectorTilePoints.js +26 -0
  398. package/dist/cesium/Workers/createVectorTilePolygons.js +26 -0
  399. package/dist/cesium/Workers/createVectorTilePolylines.js +26 -0
  400. package/dist/cesium/Workers/createVerticesFromCesium3DTilesTerrain.js +26 -0
  401. package/dist/cesium/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js +26 -0
  402. package/dist/cesium/Workers/createVerticesFromHeightmap.js +26 -0
  403. package/dist/cesium/Workers/createVerticesFromQuantizedTerrainMesh.js +26 -0
  404. package/dist/cesium/Workers/createWallGeometry.js +26 -0
  405. package/dist/cesium/Workers/createWallOutlineGeometry.js +26 -0
  406. package/dist/cesium/Workers/decodeDraco.js +26 -0
  407. package/dist/cesium/Workers/decodeGoogleEarthEnterprisePacket.js +26 -0
  408. package/dist/cesium/Workers/decodeI3S.js +26 -0
  409. package/dist/cesium/Workers/gaussianSplatSorter.js +26 -0
  410. package/dist/cesium/Workers/gaussianSplatTextureGenerator.js +26 -0
  411. package/dist/cesium/Workers/incrementallyBuildTerrainPicker.js +26 -0
  412. package/dist/cesium/Workers/transcodeKTX2.js +56 -0
  413. package/dist/cesium/Workers/transferTypedArrayTest.js +26 -0
  414. package/dist/cesium/Workers/upsampleQuantizedTerrainMesh.js +26 -0
  415. package/dist/cesium/Workers/upsampleVerticesFromCesium3DTilesTerrain.js +26 -0
  416. package/dist/index.html +13 -10
  417. package/index.html +1 -0
  418. package/package.json +18 -15
  419. package/src/App.tsx +7 -9
  420. package/src/components/viewer/BCFPanel.tsx +46 -4
  421. package/src/components/viewer/CesiumOverlay.tsx +715 -0
  422. package/src/components/viewer/CesiumSettingsDialog.tsx +100 -0
  423. package/src/components/viewer/ChatPanel.tsx +232 -90
  424. package/src/components/viewer/CommandPalette.tsx +6 -1
  425. package/src/components/viewer/DesktopEntitlementBanner.tsx +74 -0
  426. package/src/components/viewer/ExportChangesButton.tsx +6 -1
  427. package/src/components/viewer/ExportDialog.tsx +22 -6
  428. package/src/components/viewer/HierarchyPanel.tsx +196 -0
  429. package/src/components/viewer/IDSPanel.tsx +52 -3
  430. package/src/components/viewer/KeyboardShortcutsDialog.tsx +1 -1
  431. package/src/components/viewer/MainToolbar.tsx +355 -28
  432. package/src/components/viewer/PropertiesPanel.tsx +234 -81
  433. package/src/components/viewer/ScriptPanel.tsx +34 -8
  434. package/src/components/viewer/SettingsPage.tsx +581 -0
  435. package/src/components/viewer/StatusBar.tsx +17 -1
  436. package/src/components/viewer/ThemeSwitch.tsx +63 -7
  437. package/src/components/viewer/ViewerLayout.tsx +48 -6
  438. package/src/components/viewer/Viewport.tsx +61 -8
  439. package/src/components/viewer/ViewportContainer.tsx +265 -28
  440. package/src/components/viewer/ViewportOverlays.tsx +132 -27
  441. package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
  442. package/src/components/viewer/chat/ModelSelector.tsx +90 -54
  443. package/src/components/viewer/properties/GeoreferencingPanel.tsx +229 -55
  444. package/src/components/viewer/properties/LocationMap.tsx +462 -19
  445. package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
  446. package/src/components/viewer/selectionHandlers.ts +4 -3
  447. package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
  448. package/src/components/viewer/tools/SectionPanel.tsx +39 -18
  449. package/src/components/viewer/useAnimationLoop.ts +13 -1
  450. package/src/components/viewer/useGeometryStreaming.ts +127 -40
  451. package/src/components/viewer/useMouseControls.ts +4 -1
  452. package/src/components/viewer/useRenderUpdates.ts +1 -1
  453. package/src/hooks/ids/idsDataAccessor.ts +60 -24
  454. package/src/hooks/ingest/viewerModelIngest.ts +280 -0
  455. package/src/hooks/useIDS.ts +1 -1
  456. package/src/hooks/useIfc.ts +7 -1
  457. package/src/hooks/useIfcCache.ts +28 -15
  458. package/src/hooks/useIfcFederation.ts +378 -291
  459. package/src/hooks/useIfcLoader.ts +1657 -130
  460. package/src/hooks/useIfcServer.ts +0 -69
  461. package/src/hooks/useViewControls.ts +13 -5
  462. package/src/index.css +484 -10
  463. package/src/lib/desktop/desktopEntitlementEvents.ts +39 -0
  464. package/src/lib/desktop-entitlement.ts +43 -0
  465. package/src/lib/desktop-product.ts +124 -0
  466. package/src/lib/geo/cesium-bridge.ts +318 -0
  467. package/src/lib/geo/effective-georef.test.ts +73 -0
  468. package/src/lib/geo/effective-georef.ts +111 -0
  469. package/src/lib/geo/reproject.ts +249 -37
  470. package/src/lib/llm/byok-guard.test.ts +77 -0
  471. package/src/lib/llm/byok-guard.ts +39 -0
  472. package/src/lib/llm/free-models.test.ts +0 -6
  473. package/src/lib/llm/models.ts +104 -42
  474. package/src/lib/llm/stream-client.ts +74 -110
  475. package/src/lib/llm/stream-direct.test.ts +130 -0
  476. package/src/lib/llm/stream-direct.ts +316 -0
  477. package/src/lib/llm/types.ts +14 -2
  478. package/src/lib/recent-files.ts +2 -1
  479. package/src/main.tsx +1 -10
  480. package/src/services/analysis-extensions.ts +125 -0
  481. package/src/services/api-keys.ts +73 -0
  482. package/src/services/app-navigation.ts +13 -0
  483. package/src/services/bsdd.ts +53 -4
  484. package/src/services/cacheService.ts +1 -1
  485. package/src/services/desktop-cache.ts +43 -0
  486. package/src/services/desktop-export.ts +77 -0
  487. package/src/services/desktop-harness.ts +196 -0
  488. package/src/services/desktop-logger.ts +20 -0
  489. package/src/services/desktop-native-metadata.ts +207 -0
  490. package/src/services/desktop-panel-actions.ts +43 -0
  491. package/src/services/desktop-preferences.ts +44 -0
  492. package/src/services/file-dialog.ts +147 -0
  493. package/src/services/tauri-core-stub.ts +7 -0
  494. package/src/services/tauri-dialog-stub.ts +7 -0
  495. package/src/services/tauri-fs-stub.ts +7 -0
  496. package/src/store/constants.ts +20 -2
  497. package/src/store/index.ts +52 -7
  498. package/src/store/slices/cesiumSlice.ts +127 -0
  499. package/src/store/slices/chatSlice.test.ts +6 -76
  500. package/src/store/slices/chatSlice.ts +21 -58
  501. package/src/store/slices/dataSlice.ts +139 -28
  502. package/src/store/slices/desktopEntitlementSlice.ts +86 -0
  503. package/src/store/slices/loadingSlice.ts +14 -2
  504. package/src/store/slices/modelSlice.ts +58 -3
  505. package/src/store/slices/sectionSlice.test.ts +87 -7
  506. package/src/store/slices/sectionSlice.ts +151 -5
  507. package/src/store/slices/uiSlice.ts +28 -5
  508. package/src/store/types.ts +122 -2
  509. package/src/store.ts +1 -1
  510. package/src/utils/desktopModelSnapshot.ts +358 -0
  511. package/src/utils/ifcConfig.ts +6 -1
  512. package/src/utils/nativeSpatialDataStore.ts +253 -0
  513. package/src/utils/serverDataModel.ts +4 -0
  514. package/src/utils/spatialHierarchy.ts +10 -11
  515. package/src/utils/viewportUtils.ts +7 -2
  516. package/src/vite-env.d.ts +0 -4
  517. package/vite.config.ts +24 -0
  518. package/dist/assets/arrow-DJf2ErbF.js +0 -20
  519. package/dist/assets/basketViewActivator-aojwdomq.js +0 -1
  520. package/dist/assets/desktop-cache-oPzaWXYE.js +0 -1
  521. package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
  522. package/dist/assets/geometry.worker-Nz9_YIqh.js +0 -1
  523. package/dist/assets/ids-B4jTqB1O.js +0 -1
  524. package/dist/assets/ifc-lite_bg-eSkBTizQ.wasm +0 -0
  525. package/dist/assets/index-pbE7itQS.css +0 -1
  526. package/dist/assets/native-bridge-DSIyEYXG.js +0 -113
  527. package/dist/assets/wasm-bridge-B0J07fZZ.js +0 -1
  528. package/src/components/viewer/UpgradePage.tsx +0 -69
  529. package/src/lib/llm/ClerkChatSync.tsx +0 -74
  530. package/src/lib/llm/clerk-auth.ts +0 -62
@@ -11,22 +11,33 @@
11
11
  */
12
12
 
13
13
  import { useCallback, useRef } from 'react';
14
+ import { flushSync } from 'react-dom';
14
15
  import { useShallow } from 'zustand/react/shallow';
15
- import { useViewerStore } from '../store.js';
16
- import { IfcParser, detectFormat, parseIfcx, type IfcDataStore } from '@ifc-lite/parser';
16
+ import { getViewerStoreApi, useViewerStore } from '@/store';
17
+ import { IfcParser, detectFormat, type IfcDataStore } from '@ifc-lite/parser';
17
18
  import { GeometryProcessor, GeometryQuality, type MeshData, type CoordinateInfo } from '@ifc-lite/geometry';
19
+ import initIfcLiteWasm, { IfcAPI } from '@ifc-lite/wasm';
18
20
  import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
19
- import { type GeometryData, loadGLBToMeshData } from '@ifc-lite/cache';
21
+ import { type GeometryData } from '@ifc-lite/cache';
20
22
 
21
- import { SERVER_URL, USE_SERVER, CACHE_SIZE_THRESHOLD, CACHE_MAX_SOURCE_SIZE, getDynamicBatchConfig } from '../utils/ifcConfig.js';
23
+ import { SERVER_URL, USE_SERVER, CACHE_SIZE_THRESHOLD, CACHE_MAX_SOURCE_SIZE, HUGE_NATIVE_FILE_THRESHOLD, getDynamicBatchConfig } from '../utils/ifcConfig.js';
22
24
  import {
23
25
  calculateMeshBounds,
24
26
  createCoordinateInfo,
25
27
  getRenderIntervalMs,
26
28
  calculateStoreyHeights,
27
- normalizeColor,
28
29
  } from '../utils/localParsingUtils.js';
30
+ import { buildDesktopMetadataSnapshot, restoreDesktopMetadataSnapshot } from '../utils/desktopModelSnapshot.js';
31
+ import { buildIfcDataStoreFromNativeMetadata } from '../utils/nativeSpatialDataStore.js';
29
32
  import { applyColorUpdatesToMeshes } from './meshColorUpdates.js';
33
+ import { readNativeFile, type NativeFileHandle } from '../services/file-dialog.js';
34
+ import {
35
+ bootstrapNativeMetadata,
36
+ persistNativeMetadataSnapshot,
37
+ restoreNativeMetadataSnapshot,
38
+ } from '../services/desktop-native-metadata.js';
39
+ import { finalizeActiveHarnessRun, getActiveHarnessRequest } from '../services/desktop-harness.js';
40
+ import { logToDesktopTerminal } from '../services/desktop-logger.js';
30
41
 
31
42
  // Cache hook
32
43
  import { useIfcCache, getCached } from './useIfcCache.js';
@@ -34,8 +45,7 @@ import { useIfcCache, getCached } from './useIfcCache.js';
34
45
  // Server hook
35
46
  import { useIfcServer } from './useIfcServer.js';
36
47
 
37
- // Import IfcxDataStore type from federation hook
38
- import type { IfcxDataStore } from './useIfcFederation.js';
48
+ import { getMaxExpressId, parseGlbViewerModel, parseIfcxViewerModel } from './ingest/viewerModelIngest.js';
39
49
 
40
50
  /**
41
51
  * Compute a fast content fingerprint from the first and last 4KB of a buffer.
@@ -64,6 +74,78 @@ function computeFastFingerprint(buffer: ArrayBuffer): string {
64
74
  return (hash >>> 0).toString(16);
65
75
  }
66
76
 
77
+ function toExactArrayBuffer(bytes: Uint8Array): ArrayBuffer {
78
+ if (
79
+ bytes.buffer instanceof ArrayBuffer &&
80
+ bytes.byteOffset === 0 &&
81
+ bytes.byteLength === bytes.buffer.byteLength
82
+ ) {
83
+ return bytes.buffer;
84
+ }
85
+ return bytes.slice().buffer;
86
+ }
87
+
88
+ function yieldToUiThread(): Promise<void> {
89
+ return new Promise<void>((resolve) => {
90
+ const channel = new MessageChannel();
91
+ channel.port1.onmessage = () => resolve();
92
+ channel.port2.postMessage(null);
93
+ });
94
+ }
95
+
96
+ function getGeometryStreamWatchdogMs(
97
+ desktopStableWasm: boolean,
98
+ batchCount: number,
99
+ ): number {
100
+ if (desktopStableWasm) {
101
+ return batchCount > 0 ? 5_000 : 15_000;
102
+ }
103
+ return batchCount > 0 ? 15_000 : 30_000;
104
+ }
105
+
106
+ function countNativeSpatialNodes(
107
+ node: { children?: Array<{ children?: unknown[] }> } | null | undefined,
108
+ ): number {
109
+ if (!node) return 0;
110
+ const children = Array.isArray(node.children) ? node.children : [];
111
+ let total = 1;
112
+ for (let i = 0; i < children.length; i += 1) {
113
+ total += countNativeSpatialNodes(children[i] as { children?: Array<{ children?: unknown[] }> });
114
+ }
115
+ return total;
116
+ }
117
+
118
+ function computeNativeCacheKey(file: NativeFileHandle): string {
119
+ const encodedPath = new TextEncoder().encode(file.path);
120
+ const pathHash = computeFastFingerprint(toExactArrayBuffer(encodedPath));
121
+ return `native-ifc-${file.size}-${file.modifiedMs ?? 0}-${pathHash}-v1`;
122
+ }
123
+
124
+ function isNativeFileHandle(file: File | NativeFileHandle): file is NativeFileHandle {
125
+ return typeof (file as NativeFileHandle).path === 'string';
126
+ }
127
+
128
+ let metadataScanApiPromise: Promise<IfcAPI> | null = null;
129
+
130
+ async function getMetadataScanApi(): Promise<IfcAPI> {
131
+ if (!metadataScanApiPromise) {
132
+ metadataScanApiPromise = (async () => {
133
+ await initIfcLiteWasm();
134
+ return new IfcAPI();
135
+ })();
136
+ }
137
+ return metadataScanApiPromise;
138
+ }
139
+
140
+ const ENABLE_HUGE_TIME_FLUSH = import.meta.env.VITE_IFC_ENABLE_HUGE_TIME_FLUSH === 'true';
141
+
142
+ async function* startDisabledNativeDesktopRendererModel(
143
+ _path: string,
144
+ _cacheKey?: string,
145
+ ): AsyncGenerator<any, void, unknown> {
146
+ throw new Error('Native desktop renderer is disabled');
147
+ }
148
+
67
149
  /**
68
150
  * Hook providing file loading operations for single-model path
69
151
  * Includes binary cache support for fast subsequent loads
@@ -75,22 +157,36 @@ export function useIfcLoader() {
75
157
 
76
158
  const {
77
159
  setLoading,
160
+ setGeometryStreamingActive,
78
161
  setError,
79
162
  setProgress,
163
+ setGeometryProgress,
164
+ setMetadataProgress,
80
165
  setIfcDataStore,
81
166
  setGeometryResult,
167
+ setBoundedGeometryMode,
82
168
  appendGeometryBatch,
83
169
  updateMeshColors,
84
170
  updateCoordinateInfo,
171
+ upsertModel,
172
+ updateModel,
173
+ registerModelOffset,
85
174
  } = useViewerStore(useShallow((s) => ({
86
175
  setLoading: s.setLoading,
176
+ setGeometryStreamingActive: s.setGeometryStreamingActive,
87
177
  setError: s.setError,
88
178
  setProgress: s.setProgress,
179
+ setGeometryProgress: s.setGeometryProgress,
180
+ setMetadataProgress: s.setMetadataProgress,
89
181
  setIfcDataStore: s.setIfcDataStore,
90
182
  setGeometryResult: s.setGeometryResult,
183
+ setBoundedGeometryMode: s.setBoundedGeometryMode,
91
184
  appendGeometryBatch: s.appendGeometryBatch,
92
185
  updateMeshColors: s.updateMeshColors,
93
186
  updateCoordinateInfo: s.updateCoordinateInfo,
187
+ upsertModel: s.upsertModel,
188
+ updateModel: s.updateModel,
189
+ registerModelOffset: s.registerModelOffset,
94
190
  })));
95
191
 
96
192
  // Cache operations from extracted hook
@@ -99,9 +195,10 @@ export function useIfcLoader() {
99
195
  // Server operations from extracted hook
100
196
  const { loadFromServer } = useIfcServer();
101
197
 
102
- const loadFile = useCallback(async (file: File) => {
198
+ const loadFile = useCallback(async (file: File | NativeFileHandle) => {
103
199
  const { resetViewerState, clearAllModels } = useViewerStore.getState();
104
200
  const currentSession = ++loadSessionRef.current;
201
+ const primaryModelId = crypto.randomUUID();
105
202
 
106
203
  // Track total elapsed time for complete user experience
107
204
  const totalStartTime = performance.now();
@@ -113,107 +210,1377 @@ export function useIfcLoader() {
113
210
  clearAllModels();
114
211
 
115
212
  setLoading(true);
213
+ setGeometryStreamingActive(false);
116
214
  setError(null);
215
+ setBoundedGeometryMode(false);
216
+ setGeometryProgress(null);
217
+ setMetadataProgress(null);
117
218
  setProgress({ phase: 'Loading file', percent: 0 });
118
219
 
119
- // Read file from disk
120
- const fileReadStart = performance.now();
121
- const buffer = await file.arrayBuffer();
122
- const fileReadMs = performance.now() - fileReadStart;
123
- const fileSizeMB = buffer.byteLength / (1024 * 1024);
124
- console.log(`[useIfc] File: ${file.name}, size: ${fileSizeMB.toFixed(2)}MB, read in ${fileReadMs.toFixed(0)}ms`);
220
+ const fileName = file.name;
221
+ const fileSize = file.size;
222
+ const fileSizeMB = fileSize / (1024 * 1024);
223
+
224
+ upsertModel({
225
+ id: primaryModelId,
226
+ name: fileName,
227
+ ifcDataStore: null,
228
+ geometryResult: null,
229
+ visible: true,
230
+ collapsed: false,
231
+ schemaVersion: 'IFC4',
232
+ loadedAt: Date.now(),
233
+ fileSize,
234
+ sourceFile: file,
235
+ idOffset: 0,
236
+ maxExpressId: 0,
237
+ loadState: 'pending',
238
+ geometryLoadState: 'pending',
239
+ metadataLoadState: 'idle',
240
+ interactiveReady: false,
241
+ nativeMetadata: null,
242
+ cacheState: 'none',
243
+ loadError: null,
244
+ });
245
+ updateModel(primaryModelId, {
246
+ loadState: 'streaming-geometry',
247
+ geometryLoadState: 'opening',
248
+ metadataLoadState: 'idle',
249
+ interactiveReady: false,
250
+ });
125
251
 
126
- // Detect file format (IFCX/IFC5 vs IFC4 STEP vs GLB)
127
- const format = detectFormat(buffer);
252
+ const finalizePrimaryModel = (
253
+ dataStore: IfcDataStore | null,
254
+ geometryResult: { meshes: MeshData[]; totalVertices: number; totalTriangles: number; coordinateInfo: CoordinateInfo } | null,
255
+ schemaVersion: 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5',
256
+ patch?: { loadState?: 'pending' | 'streaming-geometry' | 'hydrating-metadata' | 'complete' | 'error'; cacheState?: 'none' | 'hit' | 'miss' | 'writing'; loadError?: string | null },
257
+ ) => {
258
+ let idOffset = 0;
259
+ let maxExpressId = 0;
260
+ if (dataStore && geometryResult) {
261
+ maxExpressId = getMaxExpressId(dataStore, geometryResult.meshes);
262
+ idOffset = registerModelOffset(primaryModelId, maxExpressId);
263
+ }
128
264
 
129
- // IFCX files must be parsed client-side (server only supports IFC4 STEP)
130
- if (format === 'ifcx') {
131
- setProgress({ phase: 'Parsing IFCX (client-side)', percent: 10 });
265
+ updateModel(primaryModelId, {
266
+ ifcDataStore: dataStore,
267
+ geometryResult,
268
+ schemaVersion,
269
+ idOffset,
270
+ maxExpressId,
271
+ loadState: patch?.loadState ?? 'complete',
272
+ cacheState: patch?.cacheState ?? 'none',
273
+ loadError: patch?.loadError ?? null,
274
+ });
275
+ };
276
+ const getSchemaVersion = (dataStore: IfcDataStore | null): 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5' => {
277
+ if (!dataStore) return 'IFC4';
278
+ if (dataStore.schemaVersion === 'IFC4X3') return 'IFC4X3';
279
+ if (dataStore.schemaVersion === 'IFC4') return 'IFC4';
280
+ if (dataStore.schemaVersion === 'IFC5') return 'IFC5';
281
+ return 'IFC2X3';
282
+ };
132
283
 
133
- try {
134
- const ifcxResult = await parseIfcx(buffer, {
135
- onProgress: (prog: { phase: string; percent: number }) => {
136
- setProgress({ phase: `IFCX ${prog.phase}`, percent: 10 + (prog.percent * 0.8) });
284
+ if (
285
+ isNativeFileHandle(file) &&
286
+ fileName.toLowerCase().endsWith('.ifc') &&
287
+ false
288
+ ) {
289
+ const harnessRequest = getActiveHarnessRequest();
290
+ const nativeCacheKey = computeNativeCacheKey(file);
291
+ const shouldUseNativeCache = file.size >= CACHE_SIZE_THRESHOLD;
292
+ const hugeNativeMode = file.size >= HUGE_NATIVE_FILE_THRESHOLD;
293
+ let firstBatchWaitMs: number | null = null;
294
+ let firstVisibleGeometryMs: number | null = null;
295
+ let modelOpenMs: number | null = null;
296
+ let streamCompleteMs: number | null = null;
297
+ let batchCount = 0;
298
+ let totalMeshes = 0;
299
+ let spatialReadyMs: number | null = null;
300
+ let metadataStartMs: number | null = null;
301
+ let metadataReadCompleteMs: number | null = null;
302
+ let metadataParseStartMs: number | null = null;
303
+ let metadataCompleteMs: number | null = null;
304
+ let metadataFailedMs: number | null = null;
305
+ let metadataReadDurationMs: number | null = null;
306
+ let metadataBufferCopyDurationMs: number | null = null;
307
+ let metadataParseDurationMs: number | null = null;
308
+ let metadataSnapshotWritePromise: Promise<void> | null = null;
309
+ let metadataParsingPromise: Promise<void> | null = null;
310
+ let metadataParsingStarted = false;
311
+ let geometryCompleted = false;
312
+ let nativeGeometryCacheHit = false;
313
+ let nativeMetadataSnapshotHit = false;
314
+ let nativeMetadataSource: 'snapshot' | 'ifc-parse' = 'ifc-parse';
315
+ let nativeMetadataStartGate: 'immediate' | 'afterInteractiveGeometry' | 'afterGeometryComplete' = 'immediate';
316
+ let finalCoordinateInfo: CoordinateInfo | null = null;
317
+
318
+ console.log(`[useIfc] Native renderer load: ${fileName}, size: ${fileSizeMB.toFixed(2)}MB`);
319
+ void logToDesktopTerminal(
320
+ 'info',
321
+ `[useIfc] Native renderer load start: ${fileName} (${fileSizeMB.toFixed(2)} MB) path=${file.path}`
322
+ );
323
+
324
+ setBoundedGeometryMode(true);
325
+ setGeometryResult(null);
326
+ setIfcDataStore(null);
327
+ setProgress({ phase: 'Starting native renderer', percent: 10 });
328
+
329
+ const queueNativeMetadataSnapshotWrite = (
330
+ dataStore: IfcDataStore,
331
+ sourceBuffer: ArrayBuffer,
332
+ ) => {
333
+ metadataSnapshotWritePromise = (async () => {
334
+ await yieldToUiThread();
335
+ if (typeof requestAnimationFrame === 'function') {
336
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
337
+ }
338
+ if (!shouldUseNativeCache) return;
339
+ try {
340
+ const { setNativeModelSnapshot } = await import('../services/desktop-cache.js');
341
+ const snapshotBuffer = await buildDesktopMetadataSnapshot(dataStore, sourceBuffer);
342
+ await setNativeModelSnapshot(nativeCacheKey, snapshotBuffer);
343
+ } catch (error) {
344
+ void logToDesktopTerminal(
345
+ 'warn',
346
+ `[useIfc] Native metadata snapshot write failed for ${fileName}: ${error instanceof Error ? error.message : String(error)}`
347
+ );
348
+ }
349
+ })();
350
+ };
351
+
352
+ const finalizeNativeMetadata = (dataStore: IfcDataStore) => {
353
+ if (dataStore.spatialHierarchy && dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
354
+ const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
355
+ for (const [storeyId, height] of calculatedHeights) {
356
+ dataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
357
+ }
358
+ }
359
+ setIfcDataStore(dataStore);
360
+ finalizePrimaryModel(
361
+ dataStore,
362
+ null,
363
+ getSchemaVersion(dataStore),
364
+ {
365
+ loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
366
+ cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
137
367
  },
368
+ );
369
+ };
370
+
371
+ const startNativeMetadataParsing = (): Promise<void> | null => {
372
+ if (metadataParsingStarted) return metadataParsingPromise;
373
+ metadataParsingStarted = true;
374
+ metadataStartMs = performance.now() - totalStartTime;
375
+ updateModel(primaryModelId, { loadState: 'hydrating-metadata' });
376
+ void logToDesktopTerminal(
377
+ 'info',
378
+ `[useIfc] Native metadata parse start for ${fileName} source=${nativeMetadataSource} gate=${nativeMetadataStartGate}`
379
+ );
380
+
381
+ metadataParsingPromise = (async () => {
382
+ const metadataReadStart = performance.now();
383
+ let parseStart = 0;
384
+
385
+ if (nativeMetadataSnapshotHit) {
386
+ try {
387
+ const { getNativeModelSnapshot } = await import('../services/desktop-cache.js');
388
+ const snapshotBuffer = await getNativeModelSnapshot(nativeCacheKey);
389
+ if (snapshotBuffer) {
390
+ metadataReadCompleteMs = performance.now() - totalStartTime;
391
+ metadataReadDurationMs = performance.now() - metadataReadStart;
392
+ metadataParseStartMs = performance.now() - totalStartTime;
393
+ parseStart = performance.now();
394
+ const dataStore = await restoreDesktopMetadataSnapshot(snapshotBuffer);
395
+ if (spatialReadyMs === null) {
396
+ spatialReadyMs = performance.now() - totalStartTime;
397
+ }
398
+ metadataCompleteMs = performance.now() - totalStartTime;
399
+ metadataParseDurationMs = performance.now() - parseStart;
400
+ finalizeNativeMetadata(dataStore);
401
+ return;
402
+ }
403
+ } catch (error) {
404
+ nativeMetadataSnapshotHit = false;
405
+ nativeMetadataSource = 'ifc-parse';
406
+ void logToDesktopTerminal(
407
+ 'warn',
408
+ `[useIfc] Native metadata snapshot hydration failed for ${fileName}, falling back to IFC parse: ${error instanceof Error ? error.message : String(error)}`
409
+ );
410
+ }
411
+ }
412
+
413
+ const bytes = await readNativeFile(file.path);
414
+ if (loadSessionRef.current !== currentSession) return;
415
+ metadataReadCompleteMs = performance.now() - totalStartTime;
416
+ metadataReadDurationMs = performance.now() - metadataReadStart;
417
+ const copyStart = performance.now();
418
+ const metadataBuffer = toExactArrayBuffer(bytes);
419
+ metadataBufferCopyDurationMs = performance.now() - copyStart;
420
+ metadataParseStartMs = performance.now() - totalStartTime;
421
+ parseStart = performance.now();
422
+ const parser = new IfcParser();
423
+ const wasmApi = hugeNativeMode ? await getMetadataScanApi() : undefined;
424
+ const dataStore = await parser.parseColumnar(metadataBuffer, {
425
+ wasmApi,
426
+ yieldIntervalMs: hugeNativeMode ? 32 : undefined,
427
+ deferPropertyAtomIndex: hugeNativeMode,
428
+ disableWorkerScan: false,
429
+ onSpatialReady: (partialStore) => {
430
+ if (loadSessionRef.current !== currentSession) return;
431
+ if (spatialReadyMs === null) {
432
+ spatialReadyMs = performance.now() - totalStartTime;
433
+ }
434
+ setIfcDataStore(partialStore);
435
+ },
436
+ });
437
+ queueNativeMetadataSnapshotWrite(dataStore, metadataBuffer);
438
+ metadataCompleteMs = performance.now() - totalStartTime;
439
+ metadataParseDurationMs = performance.now() - parseStart;
440
+ finalizeNativeMetadata(dataStore);
441
+ })().catch((error) => {
442
+ if (loadSessionRef.current !== currentSession) return;
443
+ metadataFailedMs = performance.now() - totalStartTime;
444
+ updateModel(primaryModelId, {
445
+ loadState: 'error',
446
+ loadError: error instanceof Error ? error.message : String(error),
447
+ });
448
+ void logToDesktopTerminal(
449
+ 'warn',
450
+ `[useIfc] Native metadata parse failed for ${fileName}: ${error instanceof Error ? error.message : String(error)}`
451
+ );
138
452
  });
139
453
 
140
- // Convert IFCX meshes to viewer format
141
- // Note: IFCX geometry extractor already handles Y-up to Z-up conversion
142
- // and applies transforms correctly in Z-up space, so we just pass through
143
-
144
- const meshes: MeshData[] = ifcxResult.meshes.map((m: { expressId?: number; express_id?: number; id?: number; positions: Float32Array | number[]; indices: Uint32Array | number[]; normals: Float32Array | number[]; color?: [number, number, number, number] | [number, number, number]; ifcType?: string; ifc_type?: string }) => {
145
- // IFCX MeshData has: expressId, ifcType, positions (Float32Array), indices (Uint32Array), normals (Float32Array), color
146
- const positions = m.positions instanceof Float32Array ? m.positions : new Float32Array(m.positions || []);
147
- const indices = m.indices instanceof Uint32Array ? m.indices : new Uint32Array(m.indices || []);
148
- const normals = m.normals instanceof Float32Array ? m.normals : new Float32Array(m.normals || []);
149
-
150
- // Normalize color to RGBA format (4 elements)
151
- const color = normalizeColor(m.color);
152
-
153
- return {
154
- expressId: m.expressId ?? m.express_id ?? m.id ?? 0,
155
- positions,
156
- indices,
157
- normals,
158
- color,
159
- ifcType: m.ifcType || m.ifc_type || 'IfcProduct',
160
- };
161
- }).filter((m: MeshData) => m.positions.length > 0 && m.indices.length > 0); // Filter out empty meshes
162
-
163
- // Check if this is an overlay-only file (no geometry)
164
- if (meshes.length === 0) {
165
- console.warn(`[useIfc] IFCX file "${file.name}" has no geometry - this appears to be an overlay file that adds properties to a base model.`);
166
- console.warn('[useIfc] To use this file, load it together with a base IFCX file (select both files at once).');
454
+ return metadataParsingPromise;
455
+ };
456
+
457
+ if (shouldUseNativeCache) {
458
+ const { hasNativeGeometryCache, hasNativeModelSnapshot } = await import('../services/desktop-cache.js');
459
+ setProgress({ phase: 'Checking native cache', percent: 5 });
460
+ nativeGeometryCacheHit = await hasNativeGeometryCache(nativeCacheKey);
461
+ nativeMetadataSnapshotHit = nativeGeometryCacheHit ? await hasNativeModelSnapshot(nativeCacheKey) : false;
462
+ nativeMetadataSource = nativeGeometryCacheHit && nativeMetadataSnapshotHit ? 'snapshot' : 'ifc-parse';
463
+ nativeMetadataStartGate = 'immediate';
464
+ updateModel(primaryModelId, { cacheState: nativeGeometryCacheHit ? 'hit' : 'miss' });
465
+ }
466
+
467
+ if (nativeMetadataStartGate === 'immediate') {
468
+ startNativeMetadataParsing();
469
+ } else {
470
+ void logToDesktopTerminal(
471
+ 'info',
472
+ `[useIfc] Deferring native metadata to ${nativeMetadataStartGate} for ${fileName}`
473
+ );
474
+ }
475
+
476
+ const nativeStream = await startDisabledNativeDesktopRendererModel(
477
+ file.path,
478
+ shouldUseNativeCache ? nativeCacheKey : undefined,
479
+ );
480
+
481
+ for await (const event of nativeStream) {
482
+ switch (event.type) {
483
+ case 'sessionReady':
484
+ void logToDesktopTerminal(
485
+ 'info',
486
+ event.cacheHit
487
+ ? `[useIfc] Native renderer cache hit for ${fileName}`
488
+ : `[useIfc] Native renderer cold load for ${fileName}`
489
+ );
490
+ break;
491
+ case 'modelOpen':
492
+ modelOpenMs = performance.now() - totalStartTime;
493
+ setProgress({ phase: 'Streaming geometry into native renderer', percent: 35 });
494
+ break;
495
+ case 'batch':
496
+ batchCount = event.batchCount;
497
+ totalMeshes = event.totalMeshes;
498
+ if (firstBatchWaitMs === null) {
499
+ firstBatchWaitMs = performance.now() - totalStartTime;
500
+ }
501
+ setProgress({
502
+ phase: `Uploading native geometry (${(event.totalMeshes ?? 0).toLocaleString()} meshes)`,
503
+ percent: Math.min(85, 35 + Math.log10(Math.max(10, event.totalMeshes ?? 0)) * 12),
504
+ });
505
+ break;
506
+ case 'firstFrame':
507
+ firstVisibleGeometryMs = performance.now() - totalStartTime;
508
+ if (nativeMetadataStartGate === 'afterInteractiveGeometry' && !metadataParsingStarted) {
509
+ startNativeMetadataParsing();
510
+ }
511
+ break;
512
+ case 'complete':
513
+ geometryCompleted = true;
514
+ streamCompleteMs = performance.now() - totalStartTime;
515
+ totalMeshes = event.totalMeshes;
516
+ finalCoordinateInfo = event.coordinateInfo;
517
+ updateCoordinateInfo(event.coordinateInfo);
518
+ if (nativeMetadataStartGate === 'afterGeometryComplete' && !metadataParsingStarted) {
519
+ startNativeMetadataParsing();
520
+ }
521
+ updateModel(primaryModelId, {
522
+ loadState: metadataParsingStarted ? 'hydrating-metadata' : 'complete',
523
+ cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
524
+ });
525
+ setProgress({
526
+ phase: metadataParsingStarted ? 'Geometry ready, hydrating metadata' : 'Native geometry ready',
527
+ percent: metadataParsingStarted ? 92 : 100,
528
+ });
529
+ break;
530
+ case 'error':
531
+ throw new Error(event.message);
532
+ }
533
+ }
534
+
535
+ if (harnessRequest?.waitForMetadataCompletion) {
536
+ if (!metadataParsingStarted) {
537
+ startNativeMetadataParsing();
538
+ }
539
+ if (metadataParsingPromise) {
540
+ await metadataParsingPromise;
541
+ }
542
+ if (metadataSnapshotWritePromise) {
543
+ await metadataSnapshotWritePromise;
544
+ }
545
+ }
546
+
547
+ if (firstVisibleGeometryMs === null && streamCompleteMs !== null) {
548
+ firstVisibleGeometryMs = streamCompleteMs;
549
+ }
550
+
551
+ if (!metadataParsingStarted) {
552
+ setLoading(false);
553
+ } else if (!harnessRequest?.waitForMetadataCompletion) {
554
+ setLoading(false);
555
+ }
556
+
557
+ await finalizeActiveHarnessRun({
558
+ schemaVersion: 1,
559
+ source: 'desktop-native',
560
+ mode: harnessRequest ? 'startup-harness' : 'manual',
561
+ success: true,
562
+ runLabel: harnessRequest?.runLabel,
563
+ cache: {
564
+ key: nativeCacheKey,
565
+ hit: nativeGeometryCacheHit,
566
+ manifestMeshCount: null,
567
+ manifestShardCount: null,
568
+ },
569
+ file: {
570
+ path: file.path,
571
+ name: file.name,
572
+ sizeBytes: file.size,
573
+ sizeMB: fileSizeMB,
574
+ },
575
+ timings: {
576
+ modelOpenMs,
577
+ firstBatchWaitMs,
578
+ firstAppendGeometryBatchMs: null,
579
+ firstVisibleGeometryMs,
580
+ streamCompleteMs,
581
+ totalWallClockMs: performance.now() - totalStartTime,
582
+ metadataStartMs,
583
+ metadataReadCompleteMs,
584
+ metadataParseStartMs,
585
+ spatialReadyMs,
586
+ metadataCompleteMs,
587
+ metadataFailedMs,
588
+ metadataReadDurationMs,
589
+ metadataBufferCopyDurationMs,
590
+ metadataParseDurationMs,
591
+ nativeRendererFirstFrameMs: firstVisibleGeometryMs,
592
+ },
593
+ batches: {
594
+ estimatedTotal: shouldUseNativeCache ? totalMeshes : null,
595
+ totalBatches: batchCount,
596
+ totalMeshes,
597
+ firstBatchMeshes: null,
598
+ firstPayloadKind: 'native-renderer',
599
+ },
600
+ nativeStats: finalCoordinateInfo
601
+ ? {
602
+ parseTimeMs: null,
603
+ entityScanTimeMs: null,
604
+ lookupTimeMs: null,
605
+ preprocessTimeMs: null,
606
+ geometryTimeMs: streamCompleteMs,
607
+ totalTimeMs: streamCompleteMs,
608
+ firstChunkReadyTimeMs: firstBatchWaitMs,
609
+ firstChunkPackTimeMs: null,
610
+ firstChunkEmittedTimeMs: null,
611
+ firstChunkEmitTimeMs: null,
612
+ }
613
+ : null,
614
+ metadata: {
615
+ started: metadataParsingStarted,
616
+ metadataStartMs,
617
+ metadataReadCompleteMs,
618
+ metadataParseStartMs,
619
+ spatialReadyMs,
620
+ metadataCompleteMs,
621
+ metadataFailedMs,
622
+ metadataReadDurationMs,
623
+ metadataBufferCopyDurationMs,
624
+ metadataParseDurationMs,
625
+ },
626
+ firstBatchTelemetry: null,
627
+ });
628
+
629
+ return;
630
+ }
631
+
632
+ // Desktop native streaming path is reserved for truly large IFC files.
633
+ // Mid-size files are more stable on the shared WASM/web loader and still
634
+ // provide full viewer parity without the native streaming complexity.
635
+ if (
636
+ isNativeFileHandle(file)
637
+ && fileName.toLowerCase().endsWith('.ifc')
638
+ && file.size >= HUGE_NATIVE_FILE_THRESHOLD
639
+ ) {
640
+ const harnessRequest = getActiveHarnessRequest();
641
+ const nativeCacheKey = computeNativeCacheKey(file);
642
+ const shouldUseNativeCache = file.size >= CACHE_SIZE_THRESHOLD;
643
+ const hugeNativeMode = file.size >= HUGE_NATIVE_FILE_THRESHOLD;
644
+ const retainAllMeshes = !hugeNativeMode;
645
+ console.log(`[useIfc] Native path load: ${fileName}, size: ${fileSizeMB.toFixed(2)}MB`);
646
+ void logToDesktopTerminal(
647
+ 'info',
648
+ `[useIfc] Native path load start: ${fileName} (${fileSizeMB.toFixed(2)} MB) path=${file.path} hugeMode=${hugeNativeMode ? 'yes' : 'no'}`
649
+ );
650
+ setBoundedGeometryMode(hugeNativeMode);
651
+ setGeometryStreamingActive(true);
652
+ setIfcDataStore(null);
653
+ setProgress({ phase: 'Starting native geometry streaming', percent: 10 });
654
+
655
+ const geometryProcessor = new GeometryProcessor({
656
+ quality: GeometryQuality.Balanced,
657
+ preferNative: true,
658
+ });
167
659
 
168
- // Check if file has data references that suggest it's an overlay
169
- const hasReferences = ifcxResult.entityCount > 0;
170
- if (hasReferences) {
171
- setError(`"${file.name}" is an overlay file with no geometry. Please load it together with a base IFCX file (select all files at once).`);
172
- setLoading(false);
660
+ let estimatedTotal = 0;
661
+ let totalMeshes = 0;
662
+ let totalVertices = 0;
663
+ let totalTriangles = 0;
664
+ const allMeshes: MeshData[] = [];
665
+ let finalCoordinateInfo: CoordinateInfo | null = null;
666
+ let batchCount = 0;
667
+ let modelOpenMs: number | null = null;
668
+ let firstGeometryTime = 0;
669
+ let firstAppendGeometryBatchMs: number | null = null;
670
+ let firstVisibleGeometryMs: number | null = null;
671
+ let jsFirstChunkReceivedMs: number | null = null;
672
+ let lastTotalMeshes = 0;
673
+ let pendingMeshes: MeshData[] = [];
674
+ let loggedFirstAppendStoreState = false;
675
+ let lastRenderTime = 0;
676
+ let streamCompleteMs: number | null = null;
677
+ let metadataStartMs: number | null = null;
678
+ let metadataReadCompleteMs: number | null = null;
679
+ let metadataParseStartMs: number | null = null;
680
+ let spatialReadyMs: number | null = null;
681
+ let metadataCompleteMs: number | null = null;
682
+ let metadataFailedMs: number | null = null;
683
+ let metadataReadDurationMs: number | null = null;
684
+ let metadataBufferCopyDurationMs: number | null = null;
685
+ let metadataParseDurationMs: number | null = null;
686
+ let metadataParsingPromise: Promise<void> | null = null;
687
+ let metadataStallWatchId: ReturnType<typeof globalThis.setInterval> | null = null;
688
+ let lastMetadataActivityTime = 0;
689
+ let currentMetadataActivity = 'idle';
690
+ let firstNativeBatchTelemetry: {
691
+ batchSequence: number;
692
+ payloadKind: string;
693
+ meshCount: number;
694
+ positionsLen: number;
695
+ normalsLen: number;
696
+ indicesLen: number;
697
+ chunkReadyTimeMs: number;
698
+ packTimeMs: number;
699
+ emittedTimeMs: number;
700
+ emitTimeMs: number;
701
+ jsReceivedTimeMs?: number;
702
+ } | null = null;
703
+ let nativeStats: {
704
+ parseTimeMs?: number;
705
+ entityScanTimeMs?: number;
706
+ lookupTimeMs?: number;
707
+ preprocessTimeMs?: number;
708
+ geometryTimeMs?: number;
709
+ totalTimeMs?: number;
710
+ firstChunkReadyTimeMs?: number;
711
+ firstChunkPackTimeMs?: number;
712
+ firstChunkEmittedTimeMs?: number;
713
+ firstChunkEmitTimeMs?: number;
714
+ } | null = null;
715
+ const RENDER_INTERVAL_MS = getRenderIntervalMs(fileSizeMB);
716
+ const NATIVE_PENDING_MESH_THRESHOLD =
717
+ fileSizeMB > 768 ? 8192 :
718
+ fileSizeMB > 512 ? 6144 :
719
+ fileSizeMB > 256 ? 4096 :
720
+ fileSizeMB > 100 ? 2048 :
721
+ 512;
722
+ const HUGE_NATIVE_APPEND_CHUNK_SIZE = fileSizeMB > 768 ? 2048 : hugeNativeMode ? 1536 : 0;
723
+ const HUGE_NATIVE_APPEND_YIELD_THRESHOLD = fileSizeMB > 768 ? 8192 : 6144;
724
+ const HUGE_NATIVE_APPEND_YIELD_BUDGET_MS = 10;
725
+ let metadataParsingStarted = false;
726
+ let geometryCompleted = false;
727
+ let fullNativeDataStore: IfcDataStore | null = null;
728
+ let nativeLoadStage: 'open' | 'streamGeometry' | 'finalizeGeometry' | 'hydrateMetadata' | 'complete' = 'open';
729
+ let nativeMetadataSource: 'snapshot' | 'ifc-parse' = 'ifc-parse';
730
+ let nativeMetadataStartGate: 'immediate' | 'afterInteractiveGeometry' | 'afterGeometryComplete' = 'immediate';
731
+
732
+ setGeometryResult(null);
733
+
734
+ const maybeBuildNativeSpatialIndex = () => {
735
+ if (
736
+ !retainAllMeshes ||
737
+ !geometryCompleted ||
738
+ !fullNativeDataStore ||
739
+ allMeshes.length === 0 ||
740
+ hugeNativeMode ||
741
+ loadSessionRef.current !== currentSession
742
+ ) {
743
+ return;
744
+ }
745
+ buildSpatialIndexGuarded(allMeshes, fullNativeDataStore, setIfcDataStore);
746
+ };
747
+
748
+ const flushPendingNativeMeshes = async (
749
+ coordinateInfo: CoordinateInfo | null | undefined,
750
+ totalMeshesSoFar: number,
751
+ ) => {
752
+ if (pendingMeshes.length === 0) {
753
+ return;
754
+ }
755
+
756
+ if (firstAppendGeometryBatchMs === null) {
757
+ firstAppendGeometryBatchMs = performance.now() - totalStartTime;
758
+ void logToDesktopTerminal(
759
+ 'info',
760
+ `[useIfc] Native first appendGeometryBatch for ${fileName}: ${firstAppendGeometryBatchMs.toFixed(0)}ms`
761
+ );
762
+ }
763
+
764
+ void totalMeshesSoFar;
765
+
766
+ const appendMeshesToStore = (meshesToAppend: MeshData[]) => {
767
+ const appendGeometryBatchToStore = getViewerStoreApi().getState().appendGeometryBatch;
768
+ if (hugeNativeMode) {
769
+ flushSync(() => {
770
+ appendGeometryBatchToStore(meshesToAppend, coordinateInfo ?? undefined);
771
+ });
173
772
  return;
174
773
  }
774
+ appendGeometryBatchToStore(meshesToAppend, coordinateInfo ?? undefined);
775
+ };
776
+
777
+ if (!hugeNativeMode || HUGE_NATIVE_APPEND_CHUNK_SIZE <= 0 || pendingMeshes.length <= HUGE_NATIVE_APPEND_CHUNK_SIZE) {
778
+ appendMeshesToStore(pendingMeshes);
779
+ if (!loggedFirstAppendStoreState) {
780
+ const stateAfterAppend = useViewerStore.getState();
781
+ void logToDesktopTerminal(
782
+ 'info',
783
+ `[useIfc] Store after append for ${fileName}: activeModelId=${stateAfterAppend.activeModelId ?? 'null'} legacyMeshes=${stateAfterAppend.geometryResult?.meshes.length ?? 0} modelMeshes=${stateAfterAppend.models.get(primaryModelId)?.geometryResult?.meshes.length ?? 0} geometryTick=${stateAfterAppend.geometryUpdateTick}`
784
+ );
785
+ loggedFirstAppendStoreState = true;
786
+ }
787
+ if (hugeNativeMode) {
788
+ await yieldToUiThread();
789
+ if (typeof requestAnimationFrame === 'function') {
790
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
791
+ }
792
+ }
793
+ pendingMeshes = [];
794
+ markFirstVisibleGeometry();
795
+ return;
175
796
  }
176
797
 
177
- // Calculate bounds and statistics
178
- const { bounds, stats } = calculateMeshBounds(meshes);
179
- const coordinateInfo = createCoordinateInfo(bounds);
798
+ let appendedSinceYield = 0;
799
+ let appendWindowStart = performance.now();
800
+ while (pendingMeshes.length > 0) {
801
+ const chunk = pendingMeshes.splice(0, HUGE_NATIVE_APPEND_CHUNK_SIZE);
802
+ appendMeshesToStore(chunk);
803
+ if (!loggedFirstAppendStoreState) {
804
+ const stateAfterAppend = useViewerStore.getState();
805
+ void logToDesktopTerminal(
806
+ 'info',
807
+ `[useIfc] Store after append for ${fileName}: activeModelId=${stateAfterAppend.activeModelId ?? 'null'} legacyMeshes=${stateAfterAppend.geometryResult?.meshes.length ?? 0} modelMeshes=${stateAfterAppend.models.get(primaryModelId)?.geometryResult?.meshes.length ?? 0} geometryTick=${stateAfterAppend.geometryUpdateTick}`
808
+ );
809
+ loggedFirstAppendStoreState = true;
810
+ }
811
+ appendedSinceYield += chunk.length;
812
+ markFirstVisibleGeometry();
813
+ if (pendingMeshes.length === 0) {
814
+ break;
815
+ }
180
816
 
181
- setGeometryResult({
182
- meshes,
183
- totalVertices: stats.totalVertices,
184
- totalTriangles: stats.totalTriangles,
185
- coordinateInfo,
817
+ const shouldYield =
818
+ appendedSinceYield >= HUGE_NATIVE_APPEND_YIELD_THRESHOLD ||
819
+ performance.now() - appendWindowStart >= HUGE_NATIVE_APPEND_YIELD_BUDGET_MS;
820
+ if (shouldYield) {
821
+ await yieldToUiThread();
822
+ if (typeof requestAnimationFrame === 'function') {
823
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
824
+ }
825
+ appendedSinceYield = 0;
826
+ appendWindowStart = performance.now();
827
+ }
828
+ }
829
+ };
830
+
831
+ const markFirstVisibleGeometry = () => {
832
+ if (firstVisibleGeometryMs !== null) return;
833
+ requestAnimationFrame(() => {
834
+ if (firstVisibleGeometryMs !== null || loadSessionRef.current !== currentSession) return;
835
+ firstVisibleGeometryMs = performance.now() - totalStartTime;
836
+ void logToDesktopTerminal(
837
+ 'info',
838
+ `[useIfc] Native first visible geometry for ${fileName}: ${firstVisibleGeometryMs.toFixed(0)}ms`
839
+ );
186
840
  });
841
+ };
187
842
 
188
- // Convert IFCX data model to IfcDataStore format
189
- // IFCX already provides entities, properties, quantities, relationships, spatialHierarchy
190
- const dataStore = {
191
- fileSize: ifcxResult.fileSize,
192
- schemaVersion: 'IFC5' as const,
193
- entityCount: ifcxResult.entityCount,
194
- parseTime: ifcxResult.parseTime,
195
- source: new Uint8Array(buffer),
196
- entityIndex: {
197
- byId: new Map(),
198
- byType: new Map(),
199
- },
200
- strings: ifcxResult.strings,
201
- entities: ifcxResult.entities,
202
- properties: ifcxResult.properties,
203
- quantities: ifcxResult.quantities,
204
- relationships: ifcxResult.relationships,
205
- spatialHierarchy: ifcxResult.spatialHierarchy,
206
- } as IfcxDataStore;
207
-
208
- // IfcxDataStore extends IfcDataStore (with schemaVersion: 'IFC5'), so this is safe
843
+ const finalizeNativeDataStore = (dataStore: IfcDataStore) => {
844
+ if (dataStore.spatialHierarchy && dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
845
+ const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
846
+ for (const [storeyId, height] of calculatedHeights) {
847
+ dataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
848
+ }
849
+ }
850
+ fullNativeDataStore = dataStore;
209
851
  setIfcDataStore(dataStore);
852
+ if (geometryCompleted) {
853
+ nativeLoadStage = 'complete';
854
+ }
855
+ finalizePrimaryModel(
856
+ dataStore,
857
+ useViewerStore.getState().geometryResult,
858
+ getSchemaVersion(dataStore),
859
+ {
860
+ loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
861
+ cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
862
+ },
863
+ );
864
+ updateModel(primaryModelId, {
865
+ geometryLoadState: geometryCompleted ? 'complete' : 'interactive',
866
+ metadataLoadState: 'complete',
867
+ interactiveReady: true,
868
+ });
869
+ maybeBuildNativeSpatialIndex();
870
+ };
871
+
872
+ const hydrateNativeSpatialDataStore = (
873
+ nativeMetadata: NonNullable<Awaited<ReturnType<typeof restoreNativeMetadataSnapshot>>>,
874
+ ) => {
875
+ const spatialDataStore = buildIfcDataStoreFromNativeMetadata(nativeMetadata);
876
+ if (!spatialDataStore) {
877
+ return;
878
+ }
879
+ if (spatialDataStore.spatialHierarchy && spatialDataStore.spatialHierarchy.storeyHeights.size === 0 && spatialDataStore.spatialHierarchy.storeyElevations.size > 1) {
880
+ const calculatedHeights = calculateStoreyHeights(spatialDataStore.spatialHierarchy.storeyElevations);
881
+ for (const [storeyId, height] of calculatedHeights) {
882
+ spatialDataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
883
+ }
884
+ }
885
+ const state = useViewerStore.getState();
886
+ const currentGeometryResult =
887
+ state.models.get(primaryModelId)?.geometryResult ??
888
+ state.geometryResult;
889
+ setIfcDataStore(spatialDataStore);
890
+ finalizePrimaryModel(
891
+ spatialDataStore,
892
+ currentGeometryResult,
893
+ nativeMetadata.schemaVersion,
894
+ {
895
+ loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
896
+ cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
897
+ },
898
+ );
899
+ };
900
+
901
+ let nativeMetadataSnapshotHit = false;
902
+ let metadataSnapshotWritePromise: Promise<void> | null = null;
903
+
904
+ const queueNativeMetadataSnapshotWrite = (
905
+ dataStore: IfcDataStore,
906
+ sourceBuffer: ArrayBuffer,
907
+ ) => {
908
+ metadataSnapshotWritePromise = (async () => {
909
+ await new Promise<void>((resolve) => {
910
+ const channel = new MessageChannel();
911
+ channel.port1.onmessage = () => resolve();
912
+ channel.port2.postMessage(null);
913
+ });
914
+ if (typeof requestAnimationFrame === 'function') {
915
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
916
+ }
917
+ await writeNativeMetadataSnapshot(dataStore, sourceBuffer);
918
+ })();
919
+ };
920
+
921
+ const writeNativeMetadataSnapshot = async (
922
+ dataStore: IfcDataStore,
923
+ sourceBuffer: ArrayBuffer,
924
+ ): Promise<void> => {
925
+ if (!shouldUseNativeCache || !nativeCacheKey) return;
926
+ try {
927
+ const { setNativeModelSnapshot } = await import('../services/desktop-cache.js');
928
+ const snapshotBuffer = await buildDesktopMetadataSnapshot(dataStore, sourceBuffer);
929
+ await setNativeModelSnapshot(nativeCacheKey, snapshotBuffer);
930
+ } catch (error) {
931
+ console.warn('[useIfc] Failed to persist native metadata snapshot:', error);
932
+ void logToDesktopTerminal(
933
+ 'warn',
934
+ `[useIfc] Native metadata snapshot write failed for ${fileName}: ${error instanceof Error ? error.message : String(error)}`
935
+ );
936
+ }
937
+ };
938
+
939
+ const noteMetadataActivity = (activity: string) => {
940
+ currentMetadataActivity = activity;
941
+ lastMetadataActivityTime = performance.now();
942
+ };
943
+
944
+ const stopMetadataStallWatch = () => {
945
+ if (metadataStallWatchId !== null) {
946
+ globalThis.clearInterval(metadataStallWatchId);
947
+ metadataStallWatchId = null;
948
+ }
949
+ };
950
+
951
+ const startMetadataStallWatch = () => {
952
+ stopMetadataStallWatch();
953
+ noteMetadataActivity('starting');
954
+ metadataStallWatchId = globalThis.setInterval(() => {
955
+ if (loadSessionRef.current !== currentSession) {
956
+ stopMetadataStallWatch();
957
+ return;
958
+ }
959
+ const idleForMs = performance.now() - lastMetadataActivityTime;
960
+ if (idleForMs < 8000) return;
961
+ lastMetadataActivityTime = performance.now();
962
+ void logToDesktopTerminal(
963
+ 'warn',
964
+ `[useIfc] Metadata stall watch for ${fileName}: stage=${nativeLoadStage} idle=${idleForMs.toFixed(0)}ms phase=${currentMetadataActivity} batches=${batchCount} meshes=${lastTotalMeshes} geometryCompleted=${geometryCompleted}`
965
+ );
966
+ }, 5000);
967
+ };
968
+
969
+ const startNativeMetadataParsing = (): Promise<void> | null => {
970
+ if (metadataParsingStarted) return metadataParsingPromise;
971
+ metadataParsingStarted = true;
972
+ nativeLoadStage = 'hydrateMetadata';
973
+ const metadataStartTime = performance.now();
974
+ metadataStartMs = metadataStartTime - totalStartTime;
975
+ let lastMetadataProgressPhase = '';
976
+ let lastMetadataProgressPercent = -1;
977
+ startMetadataStallWatch();
978
+ setMetadataProgress({ phase: 'Bootstrapping metadata', percent: 5, indeterminate: hugeNativeMode });
979
+ updateModel(primaryModelId, {
980
+ loadState: 'hydrating-metadata',
981
+ metadataLoadState: 'bootstrapping',
982
+ });
983
+ void logToDesktopTerminal(
984
+ 'info',
985
+ `[useIfc] Native metadata parse start for ${fileName} source=${nativeMetadataSource} gate=${nativeMetadataStartGate}`
986
+ );
987
+
988
+ const metadataReadStartTime = performance.now();
989
+ let parseStartTime = 0;
990
+ metadataParsingPromise = (async () => {
991
+ if (hugeNativeMode) {
992
+ noteMetadataActivity('native bootstrap');
993
+ metadataParseStartMs = performance.now() - totalStartTime;
994
+ parseStartTime = performance.now();
995
+ if (nativeMetadataSnapshotHit) {
996
+ const restoredSnapshot = await restoreNativeMetadataSnapshot(nativeCacheKey);
997
+ if (restoredSnapshot && loadSessionRef.current === currentSession) {
998
+ try {
999
+ spatialReadyMs = performance.now() - totalStartTime;
1000
+ hydrateNativeSpatialDataStore(restoredSnapshot);
1001
+ updateModel(primaryModelId, {
1002
+ nativeMetadata: restoredSnapshot,
1003
+ schemaVersion: restoredSnapshot.schemaVersion,
1004
+ metadataLoadState: 'spatial-ready',
1005
+ interactiveReady: true,
1006
+ });
1007
+ setMetadataProgress({ phase: 'Restored metadata sidecar', percent: 70 });
1008
+ } catch (error) {
1009
+ nativeMetadataSnapshotHit = false;
1010
+ nativeMetadataSource = 'ifc-parse';
1011
+ void logToDesktopTerminal(
1012
+ 'warn',
1013
+ `[useIfc] Native metadata snapshot restore incompatible for ${fileName}, continuing with live bootstrap: ${error instanceof Error ? error.message : String(error)}`
1014
+ );
1015
+ }
1016
+ }
1017
+ }
1018
+ void logToDesktopTerminal(
1019
+ 'info',
1020
+ `[useIfc] Awaiting native metadata bootstrap for ${fileName}`
1021
+ );
1022
+ const nativeMetadata = await bootstrapNativeMetadata(file.path, nativeCacheKey);
1023
+ if (loadSessionRef.current !== currentSession) {
1024
+ return null;
1025
+ }
1026
+ const spatialNodeCount = countNativeSpatialNodes(nativeMetadata.spatialTree);
1027
+ void logToDesktopTerminal(
1028
+ 'info',
1029
+ `[useIfc] Native metadata bootstrap resolved for ${fileName}: elapsed=${(performance.now() - parseStartTime).toFixed(0)}ms hasTree=${nativeMetadata.spatialTree ? 'yes' : 'no'} spatialNodes=${spatialNodeCount}`
1030
+ );
1031
+ metadataReadCompleteMs = performance.now() - totalStartTime;
1032
+ metadataReadDurationMs = metadataReadCompleteMs - metadataStartMs;
1033
+ spatialReadyMs = performance.now() - totalStartTime;
1034
+ void logToDesktopTerminal(
1035
+ 'info',
1036
+ `[useIfc] Applying native metadata to store for ${fileName}`
1037
+ );
1038
+ hydrateNativeSpatialDataStore(nativeMetadata);
1039
+ updateModel(primaryModelId, {
1040
+ nativeMetadata,
1041
+ schemaVersion: nativeMetadata.schemaVersion,
1042
+ metadataLoadState: 'spatial-ready',
1043
+ interactiveReady: true,
1044
+ });
1045
+ void logToDesktopTerminal(
1046
+ 'info',
1047
+ `[useIfc] Native metadata store update complete for ${fileName}`
1048
+ );
1049
+ setMetadataProgress({ phase: 'Spatial tree ready', percent: 70 });
1050
+ if (!nativeMetadataSnapshotHit) {
1051
+ void persistNativeMetadataSnapshot(nativeMetadata);
1052
+ }
1053
+ metadataCompleteMs = performance.now() - totalStartTime;
1054
+ metadataParseDurationMs = performance.now() - parseStartTime;
1055
+ updateModel(primaryModelId, {
1056
+ loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
1057
+ metadataLoadState: 'lazy',
1058
+ });
1059
+ setMetadataProgress({ phase: 'Metadata ready on demand', percent: 100 });
1060
+ return null;
1061
+ }
1062
+
1063
+ if (nativeGeometryCacheHit && nativeMetadataSnapshotHit) {
1064
+ try {
1065
+ const { getNativeModelSnapshot } = await import('../services/desktop-cache.js');
1066
+ const snapshotBuffer = await getNativeModelSnapshot(nativeCacheKey);
1067
+ if (!snapshotBuffer) {
1068
+ throw new Error(`missing-native-metadata-snapshot:${nativeCacheKey}`);
1069
+ }
1070
+ metadataReadCompleteMs = performance.now() - totalStartTime;
1071
+ metadataReadDurationMs = performance.now() - metadataReadStartTime;
1072
+ metadataParseStartMs = performance.now() - totalStartTime;
1073
+ parseStartTime = performance.now();
1074
+ noteMetadataActivity('snapshot hydrate');
1075
+ if (spatialReadyMs === null) {
1076
+ spatialReadyMs = performance.now() - totalStartTime;
1077
+ }
1078
+ setMetadataProgress({ phase: 'Restoring cached metadata', percent: 80 });
1079
+ return restoreDesktopMetadataSnapshot(snapshotBuffer);
1080
+ } catch (error) {
1081
+ nativeMetadataSnapshotHit = false;
1082
+ nativeMetadataSource = 'ifc-parse';
1083
+ void logToDesktopTerminal(
1084
+ 'warn',
1085
+ `[useIfc] Native metadata snapshot hydration failed for ${fileName}, falling back to IFC parse: ${error instanceof Error ? error.message : String(error)}`
1086
+ );
1087
+ }
1088
+ }
1089
+
1090
+ const bytes = await readNativeFile(file.path);
1091
+ if (loadSessionRef.current !== currentSession) {
1092
+ return null;
1093
+ }
1094
+ metadataReadCompleteMs = performance.now() - totalStartTime;
1095
+ metadataReadDurationMs = performance.now() - metadataReadStartTime;
1096
+ void logToDesktopTerminal(
1097
+ 'info',
1098
+ `[useIfc] Native metadata file read complete for ${fileName}: ${metadataReadDurationMs.toFixed(0)}ms`
1099
+ );
1100
+ const copyStartTime = performance.now();
1101
+ const metadataBuffer = toExactArrayBuffer(bytes);
1102
+ metadataBufferCopyDurationMs = performance.now() - copyStartTime;
1103
+ metadataParseStartMs = performance.now() - totalStartTime;
1104
+ parseStartTime = performance.now();
1105
+ noteMetadataActivity('parse setup');
1106
+ void logToDesktopTerminal(
1107
+ 'info',
1108
+ `[useIfc] Native metadata buffer copy complete for ${fileName}: ${metadataBufferCopyDurationMs.toFixed(0)}ms`
1109
+ );
1110
+
1111
+ const parser = new IfcParser();
1112
+ const wasmApi = hugeNativeMode ? await getMetadataScanApi() : undefined;
1113
+ const dataStore = await parser.parseColumnar(metadataBuffer, {
1114
+ wasmApi,
1115
+ yieldIntervalMs: hugeNativeMode ? 32 : undefined,
1116
+ deferPropertyAtomIndex: hugeNativeMode,
1117
+ disableWorkerScan: false,
1118
+ onProgress: (progress) => {
1119
+ if (!hugeNativeMode) return;
1120
+ noteMetadataActivity(`progress:${progress.phase}:${Math.round(progress.percent)}`);
1121
+ const roundedPercent = Math.round(progress.percent);
1122
+ const shouldLog =
1123
+ progress.phase !== lastMetadataProgressPhase ||
1124
+ roundedPercent >= lastMetadataProgressPercent + 5 ||
1125
+ roundedPercent === 100;
1126
+ if (!shouldLog) return;
1127
+ setMetadataProgress({
1128
+ phase: `Metadata ${progress.phase}`,
1129
+ percent: roundedPercent,
1130
+ indeterminate: false,
1131
+ });
1132
+ lastMetadataProgressPhase = progress.phase;
1133
+ lastMetadataProgressPercent = roundedPercent;
1134
+ void logToDesktopTerminal(
1135
+ 'info',
1136
+ `[useIfc] Native metadata progress for ${fileName}: ${progress.phase} ${roundedPercent}%`
1137
+ );
1138
+ },
1139
+ onSpatialReady: (partialStore) => {
1140
+ if (loadSessionRef.current !== currentSession) return;
1141
+ noteMetadataActivity('spatial ready');
1142
+ if (spatialReadyMs === null) {
1143
+ spatialReadyMs = performance.now() - totalStartTime;
1144
+ }
1145
+ setMetadataProgress({ phase: 'Spatial tree ready', percent: 70 });
1146
+ if (partialStore.spatialHierarchy && partialStore.spatialHierarchy.storeyHeights.size === 0 && partialStore.spatialHierarchy.storeyElevations.size > 1) {
1147
+ const calculatedHeights = calculateStoreyHeights(partialStore.spatialHierarchy.storeyElevations);
1148
+ for (const [storeyId, height] of calculatedHeights) {
1149
+ partialStore.spatialHierarchy.storeyHeights.set(storeyId, height);
1150
+ }
1151
+ }
1152
+ setIfcDataStore(partialStore);
1153
+ void logToDesktopTerminal(
1154
+ 'info',
1155
+ `[useIfc] Native spatial tree ready for ${fileName} at ${(performance.now() - totalStartTime).toFixed(0)}ms`
1156
+ );
1157
+ },
1158
+ onDiagnostic: (message) => {
1159
+ noteMetadataActivity(`diag:${message}`);
1160
+ void logToDesktopTerminal('info', `[useIfc][diag] ${fileName}: ${message}`);
1161
+ },
1162
+ });
1163
+ queueNativeMetadataSnapshotWrite(dataStore, metadataBuffer);
1164
+ return dataStore;
1165
+ })()
1166
+ .then((dataStore) => {
1167
+ stopMetadataStallWatch();
1168
+ if (loadSessionRef.current !== currentSession || !dataStore) return;
1169
+ metadataCompleteMs = performance.now() - totalStartTime;
1170
+ metadataParseDurationMs = parseStartTime > 0 ? performance.now() - parseStartTime : null;
1171
+ setMetadataProgress({ phase: 'Metadata ready', percent: 100 });
1172
+ finalizeNativeDataStore(dataStore);
1173
+ void logToDesktopTerminal(
1174
+ 'info',
1175
+ `[useIfc] Native metadata parse complete for ${fileName}: total=${(performance.now() - metadataStartTime).toFixed(0)}ms read=${metadataReadDurationMs?.toFixed(0) ?? 'n/a'}ms copy=${metadataBufferCopyDurationMs?.toFixed(0) ?? 'n/a'}ms parse=${metadataParseDurationMs?.toFixed(0) ?? 'n/a'}ms`
1176
+ );
1177
+ })
1178
+ .catch((error) => {
1179
+ if (loadSessionRef.current !== currentSession) return;
1180
+ stopMetadataStallWatch();
1181
+ metadataFailedMs = performance.now() - totalStartTime;
1182
+ console.warn('[useIfc] Native metadata parsing failed:', error);
1183
+ updateModel(primaryModelId, {
1184
+ loadState: 'error',
1185
+ metadataLoadState: 'error',
1186
+ loadError: error instanceof Error ? error.message : String(error),
1187
+ });
1188
+ setMetadataProgress({ phase: 'Metadata failed', percent: 100 });
1189
+ void logToDesktopTerminal(
1190
+ 'warn',
1191
+ `[useIfc] Native metadata parse failed for ${fileName}: ${error instanceof Error ? error.message : String(error)}`
1192
+ );
1193
+ });
1194
+ return metadataParsingPromise;
1195
+ };
1196
+
1197
+ const HUGE_NATIVE_METADATA_START_BATCH = 20;
1198
+ let metadataStartQueued = false;
1199
+ const queueNativeMetadataStart = (reason: string) => {
1200
+ if (metadataParsingStarted || metadataStartQueued) return;
1201
+ metadataStartQueued = true;
1202
+ void logToDesktopTerminal('info', `[useIfc] Queueing metadata hydration for ${fileName} after ${reason}`);
1203
+ metadataStartQueued = false;
1204
+ if (loadSessionRef.current !== currentSession || metadataParsingStarted) return;
1205
+ void logToDesktopTerminal('info', `[useIfc] Starting metadata hydration after ${reason} for ${fileName}`);
1206
+ startNativeMetadataParsing();
1207
+ };
1208
+
1209
+ let nativeGeometryCacheHit = false;
1210
+ if (shouldUseNativeCache) {
1211
+ const { hasNativeGeometryCache, hasNativeModelSnapshot } = await import('../services/desktop-cache.js');
1212
+ setProgress({ phase: 'Checking cache', percent: 5 });
1213
+ setGeometryProgress({ phase: 'Checking geometry cache', percent: 5 });
1214
+ nativeGeometryCacheHit = await hasNativeGeometryCache(nativeCacheKey);
1215
+ nativeMetadataSnapshotHit = nativeGeometryCacheHit
1216
+ ? await hasNativeModelSnapshot(nativeCacheKey)
1217
+ : false;
1218
+ nativeMetadataSource = nativeMetadataSnapshotHit ? 'snapshot' : 'ifc-parse';
1219
+ nativeMetadataStartGate = 'immediate';
1220
+ updateModel(primaryModelId, { cacheState: nativeGeometryCacheHit ? 'hit' : 'miss' });
1221
+ void logToDesktopTerminal(
1222
+ 'info',
1223
+ nativeGeometryCacheHit
1224
+ ? `[useIfc] Native geometry cache hit for ${fileName}`
1225
+ : `[useIfc] Native geometry cache miss for ${fileName}`
1226
+ );
1227
+ if (nativeMetadataStartGate === 'immediate') {
1228
+ startNativeMetadataParsing();
1229
+ } else {
1230
+ void logToDesktopTerminal(
1231
+ 'info',
1232
+ nativeMetadataStartGate === 'afterInteractiveGeometry'
1233
+ ? `[useIfc] Deferring metadata hydration until geometry batch ${HUGE_NATIVE_METADATA_START_BATCH} for ${fileName}`
1234
+ : `[useIfc] Deferring metadata hydration until geometry complete for ${fileName}`
1235
+ );
1236
+ }
1237
+ }
1238
+
1239
+ if (!shouldUseNativeCache) {
1240
+ if (nativeMetadataStartGate === 'immediate') {
1241
+ startNativeMetadataParsing();
1242
+ } else {
1243
+ void logToDesktopTerminal(
1244
+ 'info',
1245
+ `[useIfc] Deferring metadata hydration until geometry complete for ${fileName}`
1246
+ );
1247
+ }
1248
+ }
1249
+ await geometryProcessor.init();
1250
+ void logToDesktopTerminal('info', `[useIfc] GeometryProcessor.init complete for ${fileName}`);
1251
+
1252
+ const nativeStream = nativeGeometryCacheHit
1253
+ ? geometryProcessor.processStreamingCache(nativeCacheKey)
1254
+ : geometryProcessor.processStreamingPath(
1255
+ file.path,
1256
+ file.size,
1257
+ shouldUseNativeCache ? nativeCacheKey : undefined,
1258
+ );
1259
+
1260
+ for await (const event of nativeStream) {
1261
+ const eventReceived = performance.now();
1262
+
1263
+ switch (event.type) {
1264
+ case 'start':
1265
+ estimatedTotal = event.totalEstimate;
1266
+ void logToDesktopTerminal('info', `[useIfc] Native stream start for ${fileName}: estimate=${Math.round(estimatedTotal)}`);
1267
+ break;
1268
+ case 'model-open':
1269
+ nativeLoadStage = 'streamGeometry';
1270
+ setProgress({ phase: 'Processing geometry (native precompute)', percent: 50, indeterminate: true });
1271
+ setGeometryProgress({ phase: 'Opening native geometry stream', percent: 10, indeterminate: true });
1272
+ modelOpenMs = performance.now() - totalStartTime;
1273
+ console.log(`[useIfc] Native model opened at ${modelOpenMs.toFixed(0)}ms`);
1274
+ void logToDesktopTerminal('info', `[useIfc] Native model opened for ${fileName} at ${modelOpenMs.toFixed(0)}ms`);
1275
+ break;
1276
+ case 'batch': {
1277
+ batchCount++;
1278
+
1279
+ if (batchCount === 1) {
1280
+ firstGeometryTime = performance.now() - totalStartTime;
1281
+ jsFirstChunkReceivedMs = event.nativeTelemetry?.jsReceivedTimeMs ?? firstGeometryTime;
1282
+ firstNativeBatchTelemetry = event.nativeTelemetry ?? null;
1283
+ updateModel(primaryModelId, {
1284
+ geometryLoadState: 'interactive',
1285
+ interactiveReady: true,
1286
+ });
1287
+ console.log(`[useIfc] Native batch #1: ${event.meshes.length} meshes, wait: ${firstGeometryTime.toFixed(0)}ms`);
1288
+ void logToDesktopTerminal('info', `[useIfc] Native first batch for ${fileName}: meshes=${event.meshes.length}, wait=${firstGeometryTime.toFixed(0)}ms`);
1289
+ if (event.nativeTelemetry) {
1290
+ const transferLagMs = (event.nativeTelemetry.jsReceivedTimeMs ?? 0) - event.nativeTelemetry.emittedTimeMs;
1291
+ void logToDesktopTerminal(
1292
+ 'info',
1293
+ `[useIfc] Native first batch transport for ${fileName}: rustReady=${event.nativeTelemetry.chunkReadyTimeMs.toFixed(0)}ms pack=${event.nativeTelemetry.packTimeMs.toFixed(0)}ms emit=${event.nativeTelemetry.emitTimeMs.toFixed(0)}ms rustEmitted=${event.nativeTelemetry.emittedTimeMs.toFixed(0)}ms jsReceived=${(event.nativeTelemetry.jsReceivedTimeMs ?? 0).toFixed(0)}ms transfer=${transferLagMs.toFixed(0)}ms`
1294
+ );
1295
+ }
1296
+ } else if (batchCount % 20 === 0) {
1297
+ void logToDesktopTerminal('info', `[useIfc] Native batch milestone for ${fileName}: batch=${batchCount}, totalMeshes=${event.totalSoFar}`);
1298
+ }
1299
+
1300
+ for (let i = 0; i < event.meshes.length; i++) {
1301
+ const mesh = event.meshes[i];
1302
+ if (retainAllMeshes) {
1303
+ allMeshes.push(mesh);
1304
+ }
1305
+ totalVertices += mesh.positions.length / 3;
1306
+ totalTriangles += mesh.indices.length / 3;
1307
+ }
1308
+ finalCoordinateInfo = event.coordinateInfo ?? null;
1309
+ totalMeshes = event.totalSoFar;
1310
+ lastTotalMeshes = event.totalSoFar;
1311
+
1312
+ for (let i = 0; i < event.meshes.length; i++) pendingMeshes.push(event.meshes[i]);
1313
+
1314
+ if (
1315
+ nativeMetadataStartGate === 'afterInteractiveGeometry' &&
1316
+ !metadataParsingStarted &&
1317
+ batchCount >= HUGE_NATIVE_METADATA_START_BATCH &&
1318
+ firstAppendGeometryBatchMs !== null
1319
+ ) {
1320
+ queueNativeMetadataStart(`geometry batch ${batchCount}`);
1321
+ }
1322
+
1323
+ const timeSinceLastRender = eventReceived - lastRenderTime;
1324
+ const allowTimeBasedFlush = !hugeNativeMode || ENABLE_HUGE_TIME_FLUSH;
1325
+ const shouldRender =
1326
+ batchCount === 1 ||
1327
+ pendingMeshes.length >= NATIVE_PENDING_MESH_THRESHOLD ||
1328
+ (allowTimeBasedFlush && timeSinceLastRender >= RENDER_INTERVAL_MS);
1329
+
1330
+ if (shouldRender && pendingMeshes.length > 0) {
1331
+ await flushPendingNativeMeshes(event.coordinateInfo, totalMeshes);
1332
+ lastRenderTime = eventReceived;
1333
+
1334
+ const progressPercent = 50 + Math.min(45, (totalMeshes / Math.max(estimatedTotal / 10, totalMeshes || 1)) * 45);
1335
+ setProgress({
1336
+ phase: `Rendering geometry (${totalMeshes} meshes)`,
1337
+ percent: progressPercent,
1338
+ indeterminate: false,
1339
+ });
1340
+ setGeometryProgress({
1341
+ phase: `Rendering geometry (${totalMeshes} meshes)`,
1342
+ percent: Math.min(99, progressPercent),
1343
+ indeterminate: false,
1344
+ });
1345
+ }
1346
+ break;
1347
+ }
1348
+ case 'complete':
1349
+ nativeLoadStage = 'finalizeGeometry';
1350
+ geometryCompleted = true;
1351
+ streamCompleteMs = performance.now() - totalStartTime;
1352
+ if (pendingMeshes.length > 0) {
1353
+ await flushPendingNativeMeshes(event.coordinateInfo, lastTotalMeshes);
1354
+ }
1355
+
1356
+ finalCoordinateInfo = event.coordinateInfo;
1357
+ updateCoordinateInfo(finalCoordinateInfo);
1358
+ maybeBuildNativeSpatialIndex();
1359
+ if (nativeMetadataStartGate === 'afterGeometryComplete' && !metadataParsingStarted) {
1360
+ queueNativeMetadataStart('geometry complete');
1361
+ }
1362
+ setProgress({
1363
+ phase: hugeNativeMode ? 'Geometry ready, hydrating metadata' : 'Complete',
1364
+ percent: 100,
1365
+ });
1366
+ setGeometryProgress({
1367
+ phase: 'Geometry interactive',
1368
+ percent: 100,
1369
+ });
1370
+ setMetadataProgress(
1371
+ hugeNativeMode
1372
+ ? { phase: 'Preparing metadata', percent: nativeMetadataStartGate === 'afterGeometryComplete' ? 5 : 0, indeterminate: false }
1373
+ : { phase: 'Metadata complete', percent: 100 }
1374
+ );
1375
+ updateModel(primaryModelId, {
1376
+ loadState: hugeNativeMode ? 'hydrating-metadata' : 'complete',
1377
+ geometryLoadState: 'complete',
1378
+ metadataLoadState: hugeNativeMode ? 'bootstrapping' : 'complete',
1379
+ interactiveReady: true,
1380
+ cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
1381
+ });
1382
+ console.log(`[useIfc] Native geometry streaming complete: ${batchCount} batches, ${lastTotalMeshes} meshes`);
1383
+ void logToDesktopTerminal(
1384
+ 'info',
1385
+ `[useIfc] Native stream complete for ${fileName}: stage=${nativeLoadStage} batches=${batchCount}, meshes=${lastTotalMeshes}`
1386
+ );
1387
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
1388
+ if (loadSessionRef.current === currentSession) {
1389
+ setGeometryStreamingActive(false);
1390
+ }
1391
+ break;
1392
+ }
1393
+ }
1394
+
1395
+ nativeStats = geometryProcessor.getLastNativeStats();
1396
+
1397
+ const totalElapsedMs = performance.now() - totalStartTime;
1398
+ console.log(
1399
+ `[useIfc] ✓ ${fileName} (${fileSizeMB.toFixed(1)}MB) → ` +
1400
+ `${lastTotalMeshes} meshes, ${(totalVertices / 1000).toFixed(0)}k vertices | ` +
1401
+ `first: ${firstGeometryTime.toFixed(0)}ms, total: ${totalElapsedMs.toFixed(0)}ms`
1402
+ );
1403
+ if (nativeStats) {
1404
+ void logToDesktopTerminal(
1405
+ 'info',
1406
+ `[useIfc] Native timings for ${fileName}: scan=${nativeStats.entityScanTimeMs ?? 0}ms lookup=${nativeStats.lookupTimeMs ?? 0}ms preprocess=${nativeStats.preprocessTimeMs ?? 0}ms parse=${nativeStats.parseTimeMs ?? 0}ms geometry=${nativeStats.geometryTimeMs ?? 0}ms total=${nativeStats.totalTimeMs ?? 0}ms`
1407
+ );
1408
+ }
1409
+ if (!metadataParsingStarted) {
1410
+ console.warn('[useIfc] Native large-file mode completed without metadata parsing');
1411
+ void logToDesktopTerminal('warn', `[useIfc] Native large-file mode completed without metadata parsing for ${fileName}`);
1412
+ }
1413
+ if (harnessRequest?.waitForMetadataCompletion) {
1414
+ if (!metadataParsingStarted) {
1415
+ startNativeMetadataParsing();
1416
+ }
1417
+ if (metadataParsingPromise) {
1418
+ await metadataParsingPromise;
1419
+ }
1420
+ if (metadataSnapshotWritePromise) {
1421
+ await metadataSnapshotWritePromise;
1422
+ }
1423
+ }
1424
+ if (firstVisibleGeometryMs === null && firstAppendGeometryBatchMs !== null) {
1425
+ await new Promise<void>((resolve) => {
1426
+ const fallbackTimer = globalThis.setTimeout(() => {
1427
+ if (firstVisibleGeometryMs === null && loadSessionRef.current === currentSession) {
1428
+ firstVisibleGeometryMs = firstAppendGeometryBatchMs;
1429
+ }
1430
+ resolve();
1431
+ }, 250);
1432
+ requestAnimationFrame(() => {
1433
+ globalThis.clearTimeout(fallbackTimer);
1434
+ if (firstVisibleGeometryMs === null && loadSessionRef.current === currentSession) {
1435
+ firstVisibleGeometryMs = performance.now() - totalStartTime;
1436
+ }
1437
+ resolve();
1438
+ });
1439
+ });
1440
+ }
1441
+ if (hugeNativeMode) {
1442
+ setLoading(false);
1443
+ }
1444
+ const telemetryElapsedMs = performance.now() - totalStartTime;
1445
+ await finalizeActiveHarnessRun({
1446
+ schemaVersion: 1,
1447
+ source: 'desktop-native',
1448
+ mode: harnessRequest ? 'startup-harness' : 'manual',
1449
+ success: true,
1450
+ runLabel: harnessRequest?.runLabel,
1451
+ cache: {
1452
+ key: nativeCacheKey,
1453
+ hit: nativeGeometryCacheHit,
1454
+ manifestMeshCount: null,
1455
+ manifestShardCount: null,
1456
+ },
1457
+ file: {
1458
+ path: file.path,
1459
+ name: file.name,
1460
+ sizeBytes: file.size,
1461
+ sizeMB: fileSizeMB,
1462
+ },
1463
+ timings: {
1464
+ modelOpenMs,
1465
+ firstBatchWaitMs: firstGeometryTime || null,
1466
+ firstAppendGeometryBatchMs,
1467
+ firstVisibleGeometryMs,
1468
+ streamCompleteMs,
1469
+ totalWallClockMs: telemetryElapsedMs,
1470
+ metadataStartMs,
1471
+ metadataReadCompleteMs,
1472
+ metadataParseStartMs,
1473
+ spatialReadyMs,
1474
+ metadataCompleteMs,
1475
+ metadataFailedMs,
1476
+ metadataReadDurationMs,
1477
+ metadataBufferCopyDurationMs,
1478
+ metadataParseDurationMs,
1479
+ },
1480
+ batches: {
1481
+ estimatedTotal,
1482
+ totalBatches: batchCount,
1483
+ totalMeshes: lastTotalMeshes,
1484
+ firstBatchMeshes: firstNativeBatchTelemetry?.meshCount ?? null,
1485
+ firstPayloadKind: firstNativeBatchTelemetry?.payloadKind ?? null,
1486
+ },
1487
+ nativeStats: nativeStats
1488
+ ? {
1489
+ parseTimeMs: nativeStats.parseTimeMs ?? null,
1490
+ entityScanTimeMs: nativeStats.entityScanTimeMs ?? null,
1491
+ lookupTimeMs: nativeStats.lookupTimeMs ?? null,
1492
+ preprocessTimeMs: nativeStats.preprocessTimeMs ?? null,
1493
+ geometryTimeMs: nativeStats.geometryTimeMs ?? null,
1494
+ totalTimeMs: nativeStats.totalTimeMs ?? null,
1495
+ firstChunkReadyTimeMs: nativeStats.firstChunkReadyTimeMs ?? null,
1496
+ firstChunkPackTimeMs: nativeStats.firstChunkPackTimeMs ?? null,
1497
+ firstChunkEmittedTimeMs: nativeStats.firstChunkEmittedTimeMs ?? null,
1498
+ firstChunkEmitTimeMs: nativeStats.firstChunkEmitTimeMs ?? null,
1499
+ }
1500
+ : null,
1501
+ metadata: {
1502
+ started: metadataParsingStarted,
1503
+ metadataStartMs,
1504
+ metadataReadCompleteMs,
1505
+ metadataParseStartMs,
1506
+ spatialReadyMs,
1507
+ metadataCompleteMs,
1508
+ metadataFailedMs,
1509
+ metadataReadDurationMs,
1510
+ metadataBufferCopyDurationMs,
1511
+ metadataParseDurationMs,
1512
+ },
1513
+ firstBatchTelemetry: firstNativeBatchTelemetry
1514
+ ? {
1515
+ batchSequence: firstNativeBatchTelemetry.batchSequence,
1516
+ payloadKind: firstNativeBatchTelemetry.payloadKind,
1517
+ meshCount: firstNativeBatchTelemetry.meshCount,
1518
+ positionsLen: firstNativeBatchTelemetry.positionsLen,
1519
+ normalsLen: firstNativeBatchTelemetry.normalsLen,
1520
+ indicesLen: firstNativeBatchTelemetry.indicesLen,
1521
+ rustChunkReadyMs: firstNativeBatchTelemetry.chunkReadyTimeMs,
1522
+ rustPackMs: firstNativeBatchTelemetry.packTimeMs,
1523
+ rustEmittedMs: firstNativeBatchTelemetry.emittedTimeMs,
1524
+ rustEmitMs: firstNativeBatchTelemetry.emitTimeMs,
1525
+ jsReceivedMs: jsFirstChunkReceivedMs,
1526
+ transportToJsMs:
1527
+ jsFirstChunkReceivedMs !== null
1528
+ ? jsFirstChunkReceivedMs - firstNativeBatchTelemetry.emittedTimeMs
1529
+ : null,
1530
+ appendAfterReceiveMs:
1531
+ jsFirstChunkReceivedMs !== null && firstAppendGeometryBatchMs !== null
1532
+ ? firstAppendGeometryBatchMs - jsFirstChunkReceivedMs
1533
+ : null,
1534
+ visibleAfterAppendMs:
1535
+ firstVisibleGeometryMs !== null && firstAppendGeometryBatchMs !== null
1536
+ ? firstVisibleGeometryMs - firstAppendGeometryBatchMs
1537
+ : null,
1538
+ }
1539
+ : null,
1540
+ });
1541
+ if (!hugeNativeMode) {
1542
+ setLoading(false);
1543
+ }
1544
+ return;
1545
+ }
1546
+
1547
+ // Read file from disk
1548
+ const fileReadStart = performance.now();
1549
+ const buffer = isNativeFileHandle(file)
1550
+ ? toExactArrayBuffer(await readNativeFile(file.path))
1551
+ : await file.arrayBuffer();
1552
+ const fileReadMs = performance.now() - fileReadStart;
1553
+ console.log(`[useIfc] File: ${file.name}, size: ${fileSizeMB.toFixed(2)}MB, read in ${fileReadMs.toFixed(0)}ms`);
1554
+
1555
+ // Detect file format (IFCX/IFC5 vs IFC4 STEP vs GLB)
1556
+ const format = detectFormat(buffer);
1557
+
1558
+ // IFCX files must be parsed client-side (server only supports IFC4 STEP)
1559
+ if (format === 'ifcx') {
1560
+ setProgress({ phase: 'Parsing IFCX (client-side)', percent: 10 });
1561
+ setGeometryStreamingActive(false);
1562
+
1563
+ try {
1564
+ const result = await parseIfcxViewerModel(buffer, setProgress);
1565
+ setGeometryResult(result.geometryResult);
1566
+ setIfcDataStore(result.dataStore);
1567
+ finalizePrimaryModel(result.dataStore, result.geometryResult, result.schemaVersion);
210
1568
 
211
1569
  setProgress({ phase: 'Complete', percent: 100 });
212
1570
  setLoading(false);
213
1571
  return;
214
1572
  } catch (err: unknown) {
1573
+ if (err instanceof Error && err.message === 'overlay-only-ifcx') {
1574
+ console.warn(`[useIfc] IFCX file "${file.name}" has no geometry - this appears to be an overlay file that adds properties to a base model.`);
1575
+ console.warn('[useIfc] To use this file, load it together with a base IFCX file (select both files at once).');
1576
+ setError(`"${file.name}" is an overlay file with no geometry. Please load it together with a base IFCX file (select all files at once).`);
1577
+ updateModel(primaryModelId, { loadState: 'error', loadError: 'overlay-only-ifcx' });
1578
+ setLoading(false);
1579
+ return;
1580
+ }
215
1581
  console.error('[useIfc] IFCX parsing failed:', err);
216
1582
  const message = err instanceof Error ? err.message : String(err);
1583
+ updateModel(primaryModelId, { loadState: 'error', loadError: message });
217
1584
  setError(`IFCX parsing failed: ${message}`);
218
1585
  setLoading(false);
219
1586
  return;
@@ -223,28 +1590,13 @@ export function useIfcLoader() {
223
1590
  // GLB files: parse directly to MeshData (no data model, geometry only)
224
1591
  if (format === 'glb') {
225
1592
  setProgress({ phase: 'Parsing GLB', percent: 10 });
1593
+ setGeometryStreamingActive(false);
226
1594
 
227
1595
  try {
228
- const meshes = loadGLBToMeshData(new Uint8Array(buffer));
229
-
230
- if (meshes.length === 0) {
231
- setError('GLB file contains no geometry');
232
- setLoading(false);
233
- return;
234
- }
235
-
236
- const { bounds, stats } = calculateMeshBounds(meshes);
237
- const coordinateInfo = createCoordinateInfo(bounds);
238
-
239
- setGeometryResult({
240
- meshes,
241
- totalVertices: stats.totalVertices,
242
- totalTriangles: stats.totalTriangles,
243
- coordinateInfo,
244
- });
245
-
246
- // GLB files have no IFC data model - set a minimal store
1596
+ const result = await parseGlbViewerModel(buffer);
1597
+ setGeometryResult(result.geometryResult);
247
1598
  setIfcDataStore(null);
1599
+ finalizePrimaryModel(null, result.geometryResult, result.schemaVersion);
248
1600
 
249
1601
  setProgress({ phase: 'Complete', percent: 100 });
250
1602
 
@@ -253,6 +1605,7 @@ export function useIfcLoader() {
253
1605
  } catch (err: unknown) {
254
1606
  console.error('[useIfc] GLB parsing failed:', err);
255
1607
  const message = err instanceof Error ? err.message : String(err);
1608
+ updateModel(primaryModelId, { loadState: 'error', loadError: message });
256
1609
  setError(`GLB parsing failed: ${message}`);
257
1610
  setLoading(false);
258
1611
  return;
@@ -262,14 +1615,21 @@ export function useIfcLoader() {
262
1615
  // Cache key uses filename + size + content fingerprint + format version
263
1616
  // Fingerprint prevents collisions for different files with the same name and size
264
1617
  const fingerprint = computeFastFingerprint(buffer);
265
- const cacheKey = `${file.name}-${buffer.byteLength}-${fingerprint}-v4`;
1618
+ // Desktop Tauri cache commands only accept [A-Za-z0-9_-], so keep the
1619
+ // persisted key filename-safe and independent of the original filename.
1620
+ const cacheKey = `ifc-${buffer.byteLength}-${fingerprint}-v4`;
266
1621
 
267
1622
  if (buffer.byteLength >= CACHE_SIZE_THRESHOLD) {
268
1623
  setProgress({ phase: 'Checking cache', percent: 5 });
269
1624
  const cacheResult = await getCached(cacheKey);
270
1625
  if (cacheResult) {
271
- const success = await loadFromCache(cacheResult, file.name, cacheKey);
272
- if (success) {
1626
+ const cacheLoadResult = await loadFromCache(cacheResult, file.name, cacheKey);
1627
+ if (cacheLoadResult.success) {
1628
+ const state = useViewerStore.getState();
1629
+ finalizePrimaryModel(state.ifcDataStore, state.geometryResult, getSchemaVersion(state.ifcDataStore), {
1630
+ loadState: 'complete',
1631
+ cacheState: 'hit',
1632
+ });
273
1633
  console.log(`[useIfc] TOTAL LOAD TIME (from cache): ${(performance.now() - totalStartTime).toFixed(0)}ms`);
274
1634
  setLoading(false);
275
1635
  return;
@@ -283,21 +1643,29 @@ export function useIfcLoader() {
283
1643
  // Pass buffer directly - server uses File object for parsing, buffer is only for size checks
284
1644
  const serverSuccess = await loadFromServer(file, buffer, () => loadSessionRef.current !== currentSession);
285
1645
  if (serverSuccess) {
1646
+ const state = useViewerStore.getState();
1647
+ finalizePrimaryModel(state.ifcDataStore, state.geometryResult, getSchemaVersion(state.ifcDataStore));
286
1648
  console.log(`[useIfc] TOTAL LOAD TIME (server): ${(performance.now() - totalStartTime).toFixed(0)}ms`);
287
1649
  setLoading(false);
288
1650
  return;
289
1651
  }
290
1652
  // Server not available - continue with local WASM (no error logging needed)
291
1653
  } else if (format === 'unknown') {
292
- console.warn('[useIfc] Unknown file format - attempting to parse as IFC4 STEP');
293
1654
  }
294
1655
 
295
1656
  // Using local WASM parsing
296
1657
  setProgress({ phase: 'Starting geometry streaming', percent: 10 });
1658
+ setGeometryStreamingActive(true);
1659
+
1660
+ const shouldUseDesktopStableWasmGeometry =
1661
+ isNativeFileHandle(file)
1662
+ && fileName.toLowerCase().endsWith('.ifc')
1663
+ && file.size < HUGE_NATIVE_FILE_THRESHOLD;
297
1664
 
298
1665
  // Initialize geometry processor first (WASM init is fast if already loaded)
299
1666
  const geometryProcessor = new GeometryProcessor({
300
- quality: GeometryQuality.Balanced
1667
+ quality: GeometryQuality.Balanced,
1668
+ preferNative: false,
301
1669
  });
302
1670
  await geometryProcessor.init();
303
1671
 
@@ -314,14 +1682,23 @@ export function useIfcLoader() {
314
1682
 
315
1683
  const startDataModelParsing = () => {
316
1684
  const parser = new IfcParser();
317
- // wasmApi as fallback if Web Worker unavailable
318
- const wasmApi = geometryProcessor.getApi();
1685
+ metadataStartMs = performance.now() - totalStartTime;
1686
+ console.log(`[useIfc] Data model parsing start for ${file.name}: ${metadataStartMs.toFixed(0)}ms`);
1687
+ // Do not share the geometry processor's WASM API with the parser on
1688
+ // desktop fallback loads. Concurrent access can corrupt the WASM state
1689
+ // and freeze or crash the viewer. Let the parser use worker/TS scanning
1690
+ // instead.
1691
+ const parserWasmApi = isNativeFileHandle(file) ? undefined : geometryProcessor.getApi();
319
1692
  parser.parseColumnar(buffer, {
320
- wasmApi,
1693
+ wasmApi: parserWasmApi,
321
1694
  // Emit spatial hierarchy EARLY — lets the panel render while
322
1695
  // property/association parsing continues (~0.5-1s earlier).
323
1696
  onSpatialReady: (partialStore) => {
324
1697
  if (loadSessionRef.current !== currentSession) return;
1698
+ if (spatialReadyMs === null) {
1699
+ spatialReadyMs = performance.now() - totalStartTime;
1700
+ console.log(`[useIfc] Spatial tree ready for ${file.name} at ${spatialReadyMs.toFixed(0)}ms`);
1701
+ }
325
1702
  if (partialStore.spatialHierarchy && partialStore.spatialHierarchy.storeyHeights.size === 0 && partialStore.spatialHierarchy.storeyElevations.size > 1) {
326
1703
  const calculatedHeights = calculateStoreyHeights(partialStore.spatialHierarchy.storeyElevations);
327
1704
  for (const [storeyId, height] of calculatedHeights) {
@@ -332,6 +1709,7 @@ export function useIfcLoader() {
332
1709
  },
333
1710
  }).then(dataStore => {
334
1711
  if (loadSessionRef.current !== currentSession) return;
1712
+ metadataCompleteMs = performance.now() - totalStartTime;
335
1713
  // Calculate storey heights from elevation differences if not already populated
336
1714
  if (dataStore.spatialHierarchy && dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
337
1715
  const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
@@ -342,9 +1720,12 @@ export function useIfcLoader() {
342
1720
 
343
1721
  // Update with full data (includes property/association maps)
344
1722
  setIfcDataStore(dataStore);
1723
+ console.log(`[useIfc] Data model parsing complete for ${file.name}: ${metadataCompleteMs.toFixed(0)}ms`);
345
1724
  resolveDataStore(dataStore);
346
1725
  }).catch(err => {
1726
+ metadataFailedMs = performance.now() - totalStartTime;
347
1727
  console.error('[useIfc] Data model parsing failed:', err);
1728
+ console.log(`[useIfc] Data model parsing failed for ${file.name}: ${metadataFailedMs.toFixed(0)}ms`);
348
1729
  rejectDataStore(err);
349
1730
  });
350
1731
  };
@@ -363,14 +1744,19 @@ export function useIfcLoader() {
363
1744
  let capturedRtcOffset: { x: number; y: number; z: number } | null = null;
364
1745
  // Track all deferred style updates so cache data always uses final colors.
365
1746
  const cumulativeColorUpdates = new Map<number, [number, number, number, number]>();
1747
+ let firstAppendGeometryBatchMs: number | null = null;
1748
+ let firstVisibleGeometryMs: number | null = null;
1749
+ let streamCompleteMs: number | null = null;
1750
+ let metadataStartMs: number | null = null;
1751
+ let spatialReadyMs: number | null = null;
1752
+ let metadataCompleteMs: number | null = null;
1753
+ let metadataFailedMs: number | null = null;
366
1754
 
367
1755
  // Clear existing geometry result
368
1756
  setGeometryResult(null);
369
1757
 
370
1758
  // Timing instrumentation
371
1759
  let batchCount = 0;
372
- let firstGeometryTime = 0; // Time to first rendered geometry
373
- let modelOpenMs = 0;
374
1760
  let lastTotalMeshes = 0;
375
1761
 
376
1762
  // OPTIMIZATION: Accumulate meshes and batch state updates
@@ -379,15 +1765,66 @@ export function useIfcLoader() {
379
1765
  let pendingMeshes: MeshData[] = [];
380
1766
  let lastRenderTime = 0;
381
1767
  const RENDER_INTERVAL_MS = getRenderIntervalMs(fileSizeMB);
1768
+ const markFirstVisibleGeometry = () => {
1769
+ if (firstVisibleGeometryMs !== null) return;
1770
+ requestAnimationFrame(() => {
1771
+ if (firstVisibleGeometryMs !== null || loadSessionRef.current !== currentSession) return;
1772
+ firstVisibleGeometryMs = performance.now() - totalStartTime;
1773
+ console.log(`[useIfc] First visible geometry for ${file.name}: ${firstVisibleGeometryMs.toFixed(0)}ms`);
1774
+ });
1775
+ };
1776
+
1777
+ // Declare at function scope so the catch block can always reach it.
1778
+ let closeGeometryIterator: (() => Promise<void>) | null = null;
382
1779
 
383
1780
  try {
384
1781
  // Use dynamic batch sizing for optimal throughput
385
1782
  const dynamicBatchConfig = getDynamicBatchConfig(fileSizeMB);
1783
+ const geometryEvents = shouldUseDesktopStableWasmGeometry
1784
+ ? geometryProcessor.processStreaming(new Uint8Array(buffer), undefined, dynamicBatchConfig)
1785
+ : geometryProcessor.processAdaptive(new Uint8Array(buffer), {
1786
+ sizeThreshold: 2 * 1024 * 1024, // 2MB threshold
1787
+ batchSize: dynamicBatchConfig, // Dynamic batches: small first, then large
1788
+ });
1789
+ const geometryIterator = geometryEvents[Symbol.asyncIterator]();
1790
+ let geometryIteratorClosed = false;
1791
+ closeGeometryIterator = async () => {
1792
+ if (geometryIteratorClosed || typeof geometryIterator.return !== 'function') return;
1793
+ geometryIteratorClosed = true;
1794
+ try {
1795
+ await geometryIterator.return();
1796
+ } catch {
1797
+ // Ignore iterator shutdown failures during recovery.
1798
+ }
1799
+ };
1800
+
1801
+ while (true) {
1802
+ const watchdogMs = getGeometryStreamWatchdogMs(
1803
+ shouldUseDesktopStableWasmGeometry,
1804
+ batchCount,
1805
+ );
1806
+ let watchdogId: ReturnType<typeof globalThis.setTimeout> | null = null;
1807
+ const nextResult = await Promise.race([
1808
+ geometryIterator.next(),
1809
+ new Promise<never>((_, reject) => {
1810
+ watchdogId = globalThis.setTimeout(() => {
1811
+ reject(new Error(
1812
+ `Geometry stream stalled after ${watchdogMs}ms while loading ${file.name}. ` +
1813
+ `Last rendered meshes: ${lastTotalMeshes}.`
1814
+ ));
1815
+ }, watchdogMs);
1816
+ }),
1817
+ ]);
1818
+ if (watchdogId !== null) {
1819
+ globalThis.clearTimeout(watchdogId);
1820
+ }
1821
+
1822
+ if (nextResult.done) {
1823
+ await closeGeometryIterator();
1824
+ break;
1825
+ }
386
1826
 
387
- for await (const event of geometryProcessor.processAdaptive(new Uint8Array(buffer), {
388
- sizeThreshold: 2 * 1024 * 1024, // 2MB threshold
389
- batchSize: dynamicBatchConfig, // Dynamic batches: small first, then large
390
- })) {
1827
+ const event = nextResult.value;
391
1828
  const eventReceived = performance.now();
392
1829
 
393
1830
  switch (event.type) {
@@ -396,8 +1833,6 @@ export function useIfcLoader() {
396
1833
  break;
397
1834
  case 'model-open':
398
1835
  setProgress({ phase: 'Processing geometry', percent: 50 });
399
- modelOpenMs = performance.now() - totalStartTime;
400
- console.log(`[useIfc] Model opened at ${modelOpenMs.toFixed(0)}ms`);
401
1836
  break;
402
1837
  case 'colorUpdate': {
403
1838
  // Accumulate color updates locally during streaming.
@@ -424,11 +1859,8 @@ export function useIfcLoader() {
424
1859
 
425
1860
  // Track time to first geometry
426
1861
  if (batchCount === 1) {
427
- firstGeometryTime = performance.now() - totalStartTime;
428
- console.log(`[useIfc] Batch #1: ${event.meshes.length} meshes, wait: ${firstGeometryTime.toFixed(0)}ms`);
429
1862
  }
430
1863
 
431
-
432
1864
  // Collect meshes for BVH building (use loop to avoid stack overflow with large batches)
433
1865
  for (let i = 0; i < event.meshes.length; i++) allMeshes.push(event.meshes[i]);
434
1866
  finalCoordinateInfo = event.coordinateInfo ?? null;
@@ -444,9 +1876,14 @@ export function useIfcLoader() {
444
1876
  const shouldRender = batchCount === 1 || timeSinceLastRender >= RENDER_INTERVAL_MS;
445
1877
 
446
1878
  if (shouldRender && pendingMeshes.length > 0) {
1879
+ if (firstAppendGeometryBatchMs === null) {
1880
+ firstAppendGeometryBatchMs = performance.now() - totalStartTime;
1881
+ console.log(`[useIfc] First appendGeometryBatch for ${file.name}: ${firstAppendGeometryBatchMs.toFixed(0)}ms`);
1882
+ }
447
1883
  appendGeometryBatch(pendingMeshes, event.coordinateInfo);
448
1884
  pendingMeshes = [];
449
1885
  lastRenderTime = eventReceived;
1886
+ markFirstVisibleGeometry();
450
1887
 
451
1888
  // Update progress
452
1889
  const progressPercent = 50 + Math.min(45, (totalMeshes / Math.max(estimatedTotal / 10, totalMeshes)) * 45);
@@ -459,10 +1896,16 @@ export function useIfcLoader() {
459
1896
  break;
460
1897
  }
461
1898
  case 'complete':
1899
+ streamCompleteMs = performance.now() - totalStartTime;
462
1900
  // Flush any remaining pending meshes
463
1901
  if (pendingMeshes.length > 0) {
1902
+ if (firstAppendGeometryBatchMs === null) {
1903
+ firstAppendGeometryBatchMs = performance.now() - totalStartTime;
1904
+ console.log(`[useIfc] First appendGeometryBatch for ${file.name}: ${firstAppendGeometryBatchMs.toFixed(0)}ms`);
1905
+ }
464
1906
  appendGeometryBatch(pendingMeshes, event.coordinateInfo);
465
1907
  pendingMeshes = [];
1908
+ markFirstVisibleGeometry();
466
1909
  }
467
1910
 
468
1911
  finalCoordinateInfo = event.coordinateInfo ?? null;
@@ -485,13 +1928,22 @@ export function useIfcLoader() {
485
1928
  updateCoordinateInfo(finalCoordinateInfo);
486
1929
 
487
1930
  setProgress({ phase: 'Complete', percent: 100 });
1931
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
1932
+ if (loadSessionRef.current === currentSession) {
1933
+ setGeometryStreamingActive(false);
1934
+ }
488
1935
  console.log(`[useIfc] Geometry streaming complete: ${batchCount} batches, ${lastTotalMeshes} meshes`);
1936
+ console.log(`[useIfc] Stream complete for ${file.name}: ${streamCompleteMs.toFixed(0)}ms`);
489
1937
 
490
1938
  // Build spatial index and cache in background (non-blocking)
491
1939
  // Wait for data model to complete first
492
- dataStorePromise.then(dataStore => {
1940
+ dataStorePromise.then(async dataStore => {
493
1941
  // Guard: skip if user loaded a new file since this load started
494
1942
  if (loadSessionRef.current !== currentSession) return;
1943
+ finalizePrimaryModel(dataStore, useViewerStore.getState().geometryResult, getSchemaVersion(dataStore), {
1944
+ loadState: 'complete',
1945
+ cacheState: buffer.byteLength >= CACHE_SIZE_THRESHOLD ? 'writing' : 'none',
1946
+ });
495
1947
  // Build spatial index from meshes in time-sliced chunks (non-blocking).
496
1948
  // Previously this was synchronous inside requestIdleCallback, blocking
497
1949
  // the main thread for seconds on 200K+ mesh models (190M+ float reads
@@ -517,39 +1969,114 @@ export function useIfcLoader() {
517
1969
  totalTriangles: allMeshes.reduce((sum, m) => sum + m.indices.length / 3, 0),
518
1970
  coordinateInfo: finalCoordinateInfo,
519
1971
  };
520
- saveToCache(cacheKey, dataStore, geometryData, buffer, file.name);
1972
+ await saveToCache(cacheKey, dataStore, geometryData, buffer, file.name);
521
1973
  }
1974
+
1975
+ // Release closure references to MeshData objects after a delay.
1976
+ // buildSpatialIndexGuarded starts an async spatial index build that
1977
+ // reads from allMeshes — clearing immediately would corrupt it.
1978
+ // The store's geometryResult.meshes still holds references to the same
1979
+ // objects, so they remain alive for rendering/visibility.
1980
+ setTimeout(() => {
1981
+ allMeshes.length = 0;
1982
+ cumulativeColorUpdates.clear();
1983
+ }, 5000);
522
1984
  }).catch(err => {
523
1985
  // Data model parsing failed - spatial index and caching skipped
524
1986
  console.warn('[useIfc] Skipping spatial index/cache - data model unavailable:', err);
1987
+ updateModel(primaryModelId, {
1988
+ loadState: 'error',
1989
+ loadError: err instanceof Error ? err.message : String(err),
1990
+ });
525
1991
  });
526
1992
  break;
527
1993
  }
528
-
529
1994
  }
1995
+ await closeGeometryIterator?.();
530
1996
  } catch (err) {
1997
+ // Close the geometry iterator to release WASM resources on failure.
1998
+ if (closeGeometryIterator) {
1999
+ await closeGeometryIterator();
2000
+ }
531
2001
  if (loadSessionRef.current !== currentSession) return;
532
2002
  console.error('[useIfc] Error in processing:', err);
533
2003
  setError(err instanceof Error ? err.message : 'Unknown error during geometry processing');
2004
+ setLoading(false);
2005
+ setGeometryStreamingActive(false);
2006
+ return;
534
2007
  }
535
2008
 
536
2009
  if (loadSessionRef.current !== currentSession) return;
537
2010
 
2011
+ if (firstVisibleGeometryMs === null && firstAppendGeometryBatchMs !== null) {
2012
+ await new Promise<void>((resolve) => {
2013
+ const fallbackTimer = globalThis.setTimeout(() => {
2014
+ if (firstVisibleGeometryMs === null && loadSessionRef.current === currentSession) {
2015
+ firstVisibleGeometryMs = firstAppendGeometryBatchMs;
2016
+ console.log(`[useIfc] First visible geometry for ${file.name}: ${firstVisibleGeometryMs.toFixed(0)}ms`);
2017
+ }
2018
+ resolve();
2019
+ }, 250);
2020
+ requestAnimationFrame(() => {
2021
+ globalThis.clearTimeout(fallbackTimer);
2022
+ if (firstVisibleGeometryMs === null && loadSessionRef.current === currentSession) {
2023
+ firstVisibleGeometryMs = performance.now() - totalStartTime;
2024
+ console.log(`[useIfc] First visible geometry for ${file.name}: ${firstVisibleGeometryMs.toFixed(0)}ms`);
2025
+ }
2026
+ resolve();
2027
+ });
2028
+ });
2029
+ }
2030
+
538
2031
  const totalElapsedMs = performance.now() - totalStartTime;
539
2032
  const totalVertices = allMeshes.reduce((sum, m) => sum + m.positions.length / 3, 0);
540
2033
  console.log(
541
- `[useIfc] ${file.name} (${fileSizeMB.toFixed(1)}MB) → ` +
542
- `${allMeshes.length} meshes, ${(totalVertices / 1000).toFixed(0)}k vertices | ` +
543
- `first: ${firstGeometryTime.toFixed(0)}ms, total: ${totalElapsedMs.toFixed(0)}ms`
2034
+ `[ifc-lite] ${file.name} (${fileSizeMB.toFixed(1)}MB) → ${allMeshes.length} meshes, ${(totalVertices / 1000).toFixed(0)}k verts in ${(totalElapsedMs / 1000).toFixed(1)}s`
544
2035
  );
545
- console.log(`[useIfc] TOTAL LOAD TIME (local): ${totalElapsedMs.toFixed(0)}ms (${(totalElapsedMs / 1000).toFixed(1)}s)`);
546
2036
  setLoading(false);
2037
+ setGeometryStreamingActive(false);
547
2038
  } catch (err) {
548
2039
  if (loadSessionRef.current !== currentSession) return;
2040
+ updateModel(primaryModelId, {
2041
+ loadState: 'error',
2042
+ loadError: err instanceof Error ? err.message : String(err),
2043
+ });
2044
+ if (isNativeFileHandle(file)) {
2045
+ const harnessRequest = getActiveHarnessRequest();
2046
+ await finalizeActiveHarnessRun({
2047
+ schemaVersion: 1,
2048
+ source: 'desktop-native',
2049
+ mode: harnessRequest ? 'startup-harness' : 'manual',
2050
+ success: false,
2051
+ runLabel: harnessRequest?.runLabel,
2052
+ cache: {
2053
+ key: computeNativeCacheKey(file),
2054
+ hit: null,
2055
+ manifestMeshCount: null,
2056
+ manifestShardCount: null,
2057
+ },
2058
+ file: {
2059
+ path: file.path,
2060
+ name: file.name,
2061
+ sizeBytes: file.size,
2062
+ sizeMB: file.size / (1024 * 1024),
2063
+ },
2064
+ timings: {
2065
+ totalWallClockMs: performance.now() - totalStartTime,
2066
+ },
2067
+ batches: {},
2068
+ nativeStats: null,
2069
+ metadata: null,
2070
+ firstBatchTelemetry: null,
2071
+ error: err instanceof Error ? err.message : String(err),
2072
+ });
2073
+ }
2074
+ void logToDesktopTerminal('error', `[useIfc] Load failed: ${err instanceof Error ? err.message : String(err)}`);
549
2075
  setError(err instanceof Error ? err.message : 'Unknown error');
550
2076
  setLoading(false);
2077
+ setGeometryStreamingActive(false);
551
2078
  }
552
- }, [setLoading, setError, setProgress, setIfcDataStore, setGeometryResult, appendGeometryBatch, updateMeshColors, updateCoordinateInfo, loadFromCache, saveToCache, loadFromServer]);
2079
+ }, [setLoading, setGeometryStreamingActive, setError, setProgress, setIfcDataStore, setGeometryResult, appendGeometryBatch, updateMeshColors, updateCoordinateInfo, loadFromCache, saveToCache, loadFromServer]);
553
2080
 
554
2081
  return { loadFile };
555
2082
  }