diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index 8379b77b2c..e36af95670 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -49,6 +49,8 @@ MainWindow::MainWindow(QWidget* parent) this, &MainWindow::onSidecarElementsReady); connect(loader_, &SceneLoader::loadedFromSidecar, this, &MainWindow::onLoadedFromSidecar); + connect(loader_, &SceneLoader::dataSourceReady, + this, &MainWindow::onDataSourceReady); connect(loader_, &SceneLoader::streamedElementsReady, this, &MainWindow::onStreamedElementsReady); connect(loader_, &SceneLoader::loadedFromStream, @@ -141,7 +143,9 @@ void MainWindow::setupMenus() { void MainWindow::onFileOpen() { QStringList paths = QFileDialog::getOpenFileNames( this, "Add IFC Files", QString(), - "IFC Files (*.ifc *.ifcxml *.ifczip);;All Files (*)"); + "IFC Files (*.ifc *.ifcxml *.ifczip);;" + "IFC Viewer Cache (*.ifcview);;" + "All Files (*)"); if (!paths.isEmpty()) { addFiles(paths); } @@ -254,6 +258,18 @@ void MainWindow::onSidecarElementsReady(uint32_t mid, qDebug(" Tree build: %lld ms (%zu elements)", t.elapsed(), elements.size()); } +void MainWindow::onDataSourceReady(uint32_t mid) { + // Re-populate if the current selection belongs to this model, since + // populateProperties() now has an ifcFile() to query. + auto items = element_tree_->selectedItems(); + if (items.isEmpty()) return; + uint32_t object_id = items.first()->data(0, Qt::UserRole).toUInt(); + auto it = element_map_.find(object_id); + if (it != element_map_.end() && it->second.model_id == mid) { + populateProperties(object_id); + } +} + void MainWindow::onLoadedFromSidecar(uint32_t /*mid*/, qint64 elapsed_ms) { progress_bar_->setVisible(false); status_label_->setText(QString("%1 elements across %2 model(s) — loaded from cache in %3") diff --git a/src/ifcviewer-full/MainWindow.h b/src/ifcviewer-full/MainWindow.h index 031764d1c4..b616293d59 100644 --- a/src/ifcviewer-full/MainWindow.h +++ b/src/ifcviewer-full/MainWindow.h @@ -59,6 +59,7 @@ private slots: std::vector elements, std::string string_table); void onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms); + void onDataSourceReady(uint32_t mid); void onStreamedElementsReady(uint32_t mid, std::vector elements); void onLoadedFromStream(uint32_t mid, qint64 elapsed_ms); void onLoadCancelled(uint32_t mid); diff --git a/src/ifcviewer-full/SettingsWindow.cpp b/src/ifcviewer-full/SettingsWindow.cpp index 69e1f025b8..1f31ceacf7 100644 --- a/src/ifcviewer-full/SettingsWindow.cpp +++ b/src/ifcviewer-full/SettingsWindow.cpp @@ -50,6 +50,13 @@ void SettingsWindow::setupUi() { "closed solids; disable if you see holes in open geometry."); form->addRow("Backface Culling", backface_culling_check_); + load_data_source_check_ = new QCheckBox(this); + load_data_source_check_->setToolTip( + "Keep the .ifc/.rdb open after loading so element properties can " + "be queried. Disable for geometry-only viewing — saves memory " + "and, on sidecar hits, avoids a second file read."); + form->addRow("Load Property Data Source", load_data_source_check_); + auto* button_box = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); @@ -72,11 +79,13 @@ void SettingsWindow::syncFromSettings() { geometry_library_edit_->setText(AppSettings::instance().geometryLibrary()); show_stats_check_->setChecked(AppSettings::instance().showStats()); backface_culling_check_->setChecked(AppSettings::instance().backfaceCulling()); + load_data_source_check_->setChecked(AppSettings::instance().loadDataSource()); } void SettingsWindow::onAccepted() { AppSettings::instance().setGeometryLibrary(geometry_library_edit_->text()); AppSettings::instance().setShowStats(show_stats_check_->isChecked()); AppSettings::instance().setBackfaceCulling(backface_culling_check_->isChecked()); + AppSettings::instance().setLoadDataSource(load_data_source_check_->isChecked()); accept(); } diff --git a/src/ifcviewer-full/SettingsWindow.h b/src/ifcviewer-full/SettingsWindow.h index 967938b4a2..d7399c1e7c 100644 --- a/src/ifcviewer-full/SettingsWindow.h +++ b/src/ifcviewer-full/SettingsWindow.h @@ -44,6 +44,7 @@ private: QLineEdit* geometry_library_edit_ = nullptr; QCheckBox* show_stats_check_ = nullptr; QCheckBox* backface_culling_check_ = nullptr; + QCheckBox* load_data_source_check_ = nullptr; }; #endif diff --git a/src/ifcviewer/AppSettings.cpp b/src/ifcviewer/AppSettings.cpp index ff8d3bb3f1..04f56f9567 100644 --- a/src/ifcviewer/AppSettings.cpp +++ b/src/ifcviewer/AppSettings.cpp @@ -26,6 +26,7 @@ constexpr const char* kGeometryLibraryKey = "geometry/library"; constexpr const char* kGeometryLibraryDefault = "hybrid-cgal-simple-opencascade"; constexpr const char* kShowStatsKey = "viewport/show_stats"; constexpr const char* kBackfaceCullingKey = "viewport/backface_culling"; +constexpr const char* kLoadDataSourceKey = "loading/load_data_source"; } AppSettings& AppSettings::instance() { @@ -70,11 +71,23 @@ void AppSettings::setBackfaceCulling(bool value) { emit backfaceCullingChanged(value); } +bool AppSettings::loadDataSource() const { + return load_data_source_; +} + +void AppSettings::setLoadDataSource(bool value) { + if (load_data_source_ == value) return; + load_data_source_ = value; + persist(); + emit loadDataSourceChanged(value); +} + void AppSettings::load() { QSettings settings; geometry_library_ = settings.value(kGeometryLibraryKey, kGeometryLibraryDefault).toString(); show_stats_ = settings.value(kShowStatsKey, false).toBool(); backface_culling_ = settings.value(kBackfaceCullingKey, true).toBool(); + load_data_source_ = settings.value(kLoadDataSourceKey, true).toBool(); } void AppSettings::persist() { @@ -82,4 +95,5 @@ void AppSettings::persist() { settings.setValue(kGeometryLibraryKey, geometry_library_); settings.setValue(kShowStatsKey, show_stats_); settings.setValue(kBackfaceCullingKey, backface_culling_); + settings.setValue(kLoadDataSourceKey, load_data_source_); } diff --git a/src/ifcviewer/AppSettings.h b/src/ifcviewer/AppSettings.h index 8b38c61a33..9b909bc48c 100644 --- a/src/ifcviewer/AppSettings.h +++ b/src/ifcviewer/AppSettings.h @@ -40,10 +40,18 @@ public: bool backfaceCulling() const; void setBackfaceCulling(bool value); + // When true, the IFC/RocksDB file is kept open (and, on sidecar hits, + // opened in the background) so element properties can be queried. + // When false, only geometry is loaded — saves memory and avoids a + // second file read on sidecar hits, at the cost of no property panel. + bool loadDataSource() const; + void setLoadDataSource(bool value); + signals: void geometryLibraryChanged(const QString& value); void showStatsChanged(bool value); void backfaceCullingChanged(bool value); + void loadDataSourceChanged(bool value); private: AppSettings(); @@ -53,6 +61,7 @@ private: QString geometry_library_; bool show_stats_ = false; bool backface_culling_ = true; + bool load_data_source_ = true; }; #endif // APPSETTINGS_H diff --git a/src/ifcviewer/GeometryStreamer.cpp b/src/ifcviewer/GeometryStreamer.cpp index 62a276dad3..668bb1c7ce 100644 --- a/src/ifcviewer/GeometryStreamer.cpp +++ b/src/ifcviewer/GeometryStreamer.cpp @@ -79,6 +79,10 @@ GeometryStreamer::~GeometryStreamer() { } } +void GeometryStreamer::setIfcFile(std::unique_ptr file) { + ifc_file_ = std::move(file); +} + void GeometryStreamer::loadFile(const std::string& path, uint32_t start_object_id, uint32_t model_id, int num_threads) { if (running_.load()) { cancel(); diff --git a/src/ifcviewer/GeometryStreamer.h b/src/ifcviewer/GeometryStreamer.h index d8cbd2da4b..ac31035c43 100644 --- a/src/ifcviewer/GeometryStreamer.h +++ b/src/ifcviewer/GeometryStreamer.h @@ -53,6 +53,11 @@ public: void loadFile(const std::string& path, uint32_t start_object_id, uint32_t model_id, int num_threads = 0); void cancel(); + // Adopt an externally-opened ifcopenshell::file as the data source + // (e.g. for the sidecar-hit path, where loadFile never runs). The + // streamer must not be running geometry iteration when this is called. + void setIfcFile(std::unique_ptr file); + bool isRunning() const { return running_.load(); } int progress() const { return progress_.load(); } uint32_t lastObjectId() const { return next_object_id_; } diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp index 8241b64d5c..1b8d2c4be3 100644 --- a/src/ifcviewer/SceneLoader.cpp +++ b/src/ifcviewer/SceneLoader.cpp @@ -18,10 +18,12 @@ ********************************************************************************/ #include "SceneLoader.h" +#include "AppSettings.h" #include #include #include +#include #include #include @@ -37,6 +39,7 @@ SceneLoader::SceneLoader(ViewportWindow* viewport, QObject* parent) SceneLoader::~SceneLoader() { joinSidecarThread(); + joinDataSourceThreads(); } void SceneLoader::joinSidecarThread() { @@ -44,6 +47,13 @@ void SceneLoader::joinSidecarThread() { sidecar_read_thread_.join(); } +void SceneLoader::joinDataSourceThreads() { + for (auto& t : data_source_threads_) { + if (t.joinable()) t.join(); + } + data_source_threads_.clear(); +} + QString SceneLoader::filePath(uint32_t mid) const { auto it = models_.find(mid); return it == models_.end() ? QString() : it->second.file_path; @@ -187,10 +197,75 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) { qint64 ms = model.load_timer.elapsed(); emit loadedFromSidecar(mid, ms); + startDataSourceLoad(mid); + loading_model_id_ = 0; QTimer::singleShot(0, this, &SceneLoader::startNextLoad); } +// Match SidecarCache.cpp's sidecarPath() stem logic so we resolve the +// data-source siblings against the same stem the sidecar was keyed on. +static std::string pathStem(const std::string& path) { + std::string p = path; + while (!p.empty() && (p.back() == '/' || p.back() == '\\')) p.pop_back(); + auto slash = p.find_last_of("/\\"); + auto dot = p.find_last_of('.'); + return (dot != std::string::npos && + (slash == std::string::npos || dot > slash)) + ? p.substr(0, dot) + : p; +} + +void SceneLoader::startDataSourceLoad(uint32_t mid) { + if (!AppSettings::instance().loadDataSource()) return; + + auto it = models_.find(mid); + if (it == models_.end()) return; + + std::string original_path = it->second.file_path.toStdString(); + std::string stem = pathStem(original_path); + + // Prefer RocksDB (foo.rdb) over SPF (foo.ifc) for fast random lookups. + QString data_path; + const QString rdb_candidate = QString::fromStdString(stem + ".rdb"); + const QString ifc_candidate = QString::fromStdString(stem + ".ifc"); + if (QFileInfo::exists(rdb_candidate)) { + data_path = rdb_candidate; + } else if (QFileInfo::exists(ifc_candidate)) { + data_path = ifc_candidate; + } else { + return; + } + + std::string data_path_std = data_path.toStdString(); + data_source_threads_.emplace_back([this, mid, data_path_std]() { + QElapsedTimer t; t.start(); + std::unique_ptr file; + try { + file = std::make_unique( + data_path_std, ifcopenshell::FT_AUTODETECT, /*read_only=*/true); + } catch (const std::exception& e) { + qWarning(" Data source load failed: %s (%s)", + data_path_std.c_str(), e.what()); + return; + } + qDebug(" Data source load: %lld ms (%s)", t.elapsed(), data_path_std.c_str()); + + auto shared = std::make_shared>(std::move(file)); + QMetaObject::invokeMethod(this, [this, mid, shared]() { + auto it = models_.find(mid); + if (it == models_.end()) return; + auto* streamer = it->second.streamer; + if (streamer == nullptr) return; + // If the streamer already has a file (e.g. a later stream-fallback + // path somehow populated it), don't clobber it. + if (streamer->ifcFile() != nullptr) return; + streamer->setIfcFile(std::move(*shared)); + emit dataSourceReady(mid); + }, Qt::QueuedConnection); + }); +} + void SceneLoader::onStreamerProgressChanged(int percent) { emit progressChanged(percent); } @@ -227,6 +302,13 @@ void SceneLoader::onStreamerFinished() { qint64 ms = it->second.load_timer.elapsed(); emit loadedFromStream(mid, ms); + + // Slot(s) above run synchronously (sidecar write uses element_map_, + // not ifcFile()); drop the parsed file now to save memory if the + // user has opted out of keeping a property data source. + if (!AppSettings::instance().loadDataSource()) { + it->second.streamer->setIfcFile(nullptr); + } } } diff --git a/src/ifcviewer/SceneLoader.h b/src/ifcviewer/SceneLoader.h index 6d63e144ef..41f825a35e 100644 --- a/src/ifcviewer/SceneLoader.h +++ b/src/ifcviewer/SceneLoader.h @@ -77,6 +77,11 @@ signals: std::string string_table); void loadedFromSidecar(uint32_t mid, qint64 elapsed_ms); + // Fired after a sidecar-hit model has its .rdb/.ifc opened as a + // property data source in the background. Consumers can refresh + // any UI that queries ifcFile(mid) for attributes/properties. + void dataSourceReady(uint32_t mid); + // Fired repeatedly while streaming, as the worker thread produces // elements. Each batch contains whatever accumulated since the last // poll tick. @@ -113,7 +118,9 @@ private: void startNextLoad(); void connectStreamer(GeometryStreamer* streamer); void joinSidecarThread(); + void joinDataSourceThreads(); void applySidecarData(uint32_t mid, SidecarData data); + void startDataSourceLoad(uint32_t mid); ViewportWindow* viewport_ = nullptr; std::map models_; @@ -122,6 +129,10 @@ private: uint32_t next_object_id_ = 1; uint32_t loading_model_id_ = 0; std::thread sidecar_read_thread_; + // One thread per sidecar-hit model while its .rdb/.ifc opens in the + // background. Joined only at destruction so a slow SPF parse on model + // A never blocks the sidecar-hit path of model B. + std::vector data_source_threads_; QTimer element_poll_timer_; };