From 4e4553201b0b3a1eccad15bc52ecdacbaab885c9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 22 Apr 2026 11:27:53 +1000 Subject: [PATCH] Fix viewer load termination Handle streamer success, failure, and cancellation as distinct terminal states so failed or cancelled loads do not finalize as successful models. Clean up partial model/UI state in the full and minimal viewer apps when a load is cancelled or fails. Generated with the assistance of an AI coding tool. --- src/ifcviewer-full/MainWindow.cpp | 49 ++++++++++++++++++++++++- src/ifcviewer-full/MainWindow.h | 2 + src/ifcviewer-minimal/MinimalWindow.cpp | 7 ++++ src/ifcviewer-minimal/MinimalWindow.h | 1 + src/ifcviewer/GeometryStreamer.cpp | 8 +++- src/ifcviewer/GeometryStreamer.h | 2 + src/ifcviewer/SceneLoader.cpp | 35 +++++++++++++++++- src/ifcviewer/SceneLoader.h | 3 ++ 8 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index 7dfaadb143..fabd008daf 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -53,6 +53,8 @@ MainWindow::MainWindow(QWidget* parent) this, &MainWindow::onStreamedElementsReady); connect(loader_, &SceneLoader::loadedFromStream, this, &MainWindow::onLoadedFromStream); + connect(loader_, &SceneLoader::loadCancelled, + this, &MainWindow::onLoadCancelled); connect(loader_, &SceneLoader::loadError, this, &MainWindow::onLoadError); connect(loader_, &SceneLoader::allLoadsFinished, @@ -297,6 +299,42 @@ void MainWindow::writeSidecarForModel(uint32_t mid) { qDebug(" Sidecar write: %lld ms (%s)", t.elapsed(), ok ? "ok" : "FAILED"); } +void MainWindow::removeModelUi(uint32_t mid) { + auto root_it = tree_roots_.find(mid); + if (root_it != tree_roots_.end()) { + delete root_it->second; + tree_roots_.erase(root_it); + } + + for (auto it = tree_items_.begin(); it != tree_items_.end();) { + auto info_it = element_map_.find(it->first); + if (info_it != element_map_.end() && info_it->second.model_id == mid) { + it = tree_items_.erase(it); + } else { + ++it; + } + } + + for (auto it = element_map_.begin(); it != element_map_.end();) { + if (it->second.model_id == mid) { + it = element_map_.erase(it); + } else { + ++it; + } + } + + for (auto it = scoped_ifc_id_to_object_id_.begin(); it != scoped_ifc_id_to_object_id_.end();) { + if (static_cast(it->first >> 32) == mid) { + it = scoped_ifc_id_to_object_id_.erase(it); + } else { + ++it; + } + } + + viewport_->setSelectedObjectId(0); + property_table_->setRowCount(0); +} + void MainWindow::onLoadedFromStream(uint32_t mid, qint64 elapsed_ms) { progress_bar_->setVisible(false); status_label_->setText(QString("%1 elements across %2 model(s) — last loaded in %3") @@ -307,7 +345,16 @@ void MainWindow::onLoadedFromStream(uint32_t mid, qint64 elapsed_ms) { writeSidecarForModel(mid); } -void MainWindow::onLoadError(uint32_t /*mid*/, QString message) { +void MainWindow::onLoadCancelled(uint32_t mid) { + progress_bar_->setVisible(false); + removeModelUi(mid); + status_label_->setText(QString("%1 load cancelled").arg(loader_->displayName(mid))); +} + +void MainWindow::onLoadError(uint32_t mid, QString message) { + progress_bar_->setVisible(false); + removeModelUi(mid); + status_label_->setText("Error: " + message); QMessageBox::warning(this, "Error", message); } diff --git a/src/ifcviewer-full/MainWindow.h b/src/ifcviewer-full/MainWindow.h index 83c1990c96..71de00dbdf 100644 --- a/src/ifcviewer-full/MainWindow.h +++ b/src/ifcviewer-full/MainWindow.h @@ -60,6 +60,7 @@ private slots: void onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms); void onStreamedElementsReady(uint32_t mid, std::vector elements); void onLoadedFromStream(uint32_t mid, qint64 elapsed_ms); + void onLoadCancelled(uint32_t mid); void onLoadError(uint32_t mid, QString message); void onAllLoadsFinished(); @@ -75,6 +76,7 @@ private: const std::string& name, const std::string& type); void writeSidecarForModel(uint32_t mid); + void removeModelUi(uint32_t mid); void applyPendingBenchmark(); QString formatElapsed(qint64 ms) const; diff --git a/src/ifcviewer-minimal/MinimalWindow.cpp b/src/ifcviewer-minimal/MinimalWindow.cpp index 18e6eaae44..ebca8f303e 100644 --- a/src/ifcviewer-minimal/MinimalWindow.cpp +++ b/src/ifcviewer-minimal/MinimalWindow.cpp @@ -45,6 +45,8 @@ MinimalWindow::MinimalWindow(QWidget* parent) this, &MinimalWindow::onLoadedFromSidecar); connect(loader_, &SceneLoader::loadedFromStream, this, &MinimalWindow::onLoadedFromStream); + connect(loader_, &SceneLoader::loadCancelled, + this, &MinimalWindow::onLoadCancelled); connect(loader_, &SceneLoader::loadError, this, &MinimalWindow::onLoadError); connect(loader_, &SceneLoader::allLoadsFinished, @@ -100,6 +102,11 @@ void MinimalWindow::onLoadedFromStream(uint32_t mid, qint64 elapsed_ms) { .arg(formatElapsed(elapsed_ms))); } +void MinimalWindow::onLoadCancelled(uint32_t mid) { + status_label_->setText(QString("%1 load cancelled") + .arg(loader_->displayName(mid))); +} + void MinimalWindow::onLoadError(uint32_t /*mid*/, QString message) { qWarning("IfcViewerMinimal error: %s", qPrintable(message)); status_label_->setText("Error: " + message); diff --git a/src/ifcviewer-minimal/MinimalWindow.h b/src/ifcviewer-minimal/MinimalWindow.h index 723dd82fba..a70ed04144 100644 --- a/src/ifcviewer-minimal/MinimalWindow.h +++ b/src/ifcviewer-minimal/MinimalWindow.h @@ -40,6 +40,7 @@ private slots: void onLoadStarted(uint32_t mid, QString display_name); void onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms); void onLoadedFromStream(uint32_t mid, qint64 elapsed_ms); + void onLoadCancelled(uint32_t mid); void onLoadError(uint32_t mid, QString message); void onAllLoadsFinished(); diff --git a/src/ifcviewer/GeometryStreamer.cpp b/src/ifcviewer/GeometryStreamer.cpp index d3edcce19f..3f836b3490 100644 --- a/src/ifcviewer/GeometryStreamer.cpp +++ b/src/ifcviewer/GeometryStreamer.cpp @@ -89,6 +89,7 @@ void GeometryStreamer::loadFile(const std::string& path, uint32_t start_object_i } cancel_requested_ = false; + succeeded_ = false; running_ = true; progress_ = 0; next_object_id_ = start_object_id; @@ -115,7 +116,11 @@ void GeometryStreamer::loadFile(const std::string& path, uint32_t start_object_i connect(worker_thread_.get(), &QThread::finished, this, [this]() { running_ = false; - emit finished(); + if (succeeded_.load()) { + emit finished(); + } else if (cancel_requested_.load()) { + emit cancelled(); + } }); worker_thread_->start(); @@ -402,4 +407,5 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { qDebug("Streamer done: %s %.2fs shapes=%u unique_meshes=%u dedup=%.2fx", path.c_str(), stream_timer.elapsed() / 1000.0, total_shapes, total_meshes, dedup_ratio); + succeeded_ = !cancel_requested_.load(); } diff --git a/src/ifcviewer/GeometryStreamer.h b/src/ifcviewer/GeometryStreamer.h index f6201517ad..d8cbd2da4b 100644 --- a/src/ifcviewer/GeometryStreamer.h +++ b/src/ifcviewer/GeometryStreamer.h @@ -68,6 +68,7 @@ signals: void meshReady(MeshChunk chunk); void instanceReady(InstanceChunk chunk); void finished(); + void cancelled(); void errorOccurred(const QString& message); private: @@ -77,6 +78,7 @@ private: std::unique_ptr worker_thread_; std::atomic running_{false}; std::atomic cancel_requested_{false}; + std::atomic succeeded_{false}; std::atomic progress_{0}; std::mutex elements_mutex_; diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp index 16587b16bf..966e2d3d84 100644 --- a/src/ifcviewer/SceneLoader.cpp +++ b/src/ifcviewer/SceneLoader.cpp @@ -95,10 +95,19 @@ void SceneLoader::connectStreamer(GeometryStreamer* streamer) { this, &SceneLoader::onStreamerInstanceReady, Qt::QueuedConnection); connect(streamer, &GeometryStreamer::finished, this, &SceneLoader::onStreamerFinished, Qt::QueuedConnection); + connect(streamer, &GeometryStreamer::cancelled, + this, &SceneLoader::onStreamerCancelled, Qt::QueuedConnection); connect(streamer, &GeometryStreamer::errorOccurred, this, &SceneLoader::onStreamerError, Qt::QueuedConnection); } +void SceneLoader::cancelCurrentLoad() { + if (loading_model_id_ == 0) return; + auto it = models_.find(loading_model_id_); + if (it == models_.end() || it->second.streamer == nullptr) return; + it->second.streamer->cancel(); +} + void SceneLoader::startNextLoad() { if (load_queue_.empty()) { loading_model_id_ = 0; @@ -232,6 +241,28 @@ void SceneLoader::onStreamerFinished() { startNextLoad(); } -void SceneLoader::onStreamerError(const QString& msg) { - emit loadError(loading_model_id_, msg); +void SceneLoader::onStreamerCancelled() { + element_poll_timer_.stop(); + + const uint32_t mid = loading_model_id_; + loading_model_id_ = 0; + + if (mid != 0) { + viewport_->removeModel(mid); + emit loadCancelled(mid); + } + QTimer::singleShot(0, this, &SceneLoader::startNextLoad); +} + +void SceneLoader::onStreamerError(const QString& msg) { + element_poll_timer_.stop(); + + const uint32_t mid = loading_model_id_; + loading_model_id_ = 0; + + if (mid != 0) { + viewport_->removeModel(mid); + } + emit loadError(mid, msg); + QTimer::singleShot(0, this, &SceneLoader::startNextLoad); } diff --git a/src/ifcviewer/SceneLoader.h b/src/ifcviewer/SceneLoader.h index e29062808c..ad970c8468 100644 --- a/src/ifcviewer/SceneLoader.h +++ b/src/ifcviewer/SceneLoader.h @@ -57,6 +57,7 @@ public: // Callers can use these to set up per-model UI state (tree roots, etc.) // before any load signal fires. std::vector addFiles(const QStringList& paths); + void cancelCurrentLoad(); bool isLoading() const { return loading_model_id_ != 0 || !load_queue_.empty(); } size_t modelCount() const { return models_.size(); } @@ -87,6 +88,7 @@ signals: // elements to be known (e.g. sidecar write) — SceneLoader will only // start the next queued load after all slots return. void loadedFromStream(uint32_t mid, qint64 elapsed_ms); + void loadCancelled(uint32_t mid); void loadError(uint32_t mid, QString message); void allLoadsFinished(); @@ -96,6 +98,7 @@ private slots: void onStreamerMeshReady(MeshChunk chunk); void onStreamerInstanceReady(InstanceChunk chunk); void onStreamerFinished(); + void onStreamerCancelled(); void onStreamerError(const QString& msg); void onElementPollTick();