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