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