@ifc-lite/viewer 1.17.3 → 1.17.4

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