From 29f5132510c7204286a253232b8594dec8c7def1 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 21 Apr 2026 18:05:19 +1000 Subject: [PATCH] ifcviewer: extract SceneLoader, remove duplicated load orchestration MainWindow and MinimalWindow each carried ~150 lines of mirrored load-queue, sidecar-thread, streamer-wiring, and ID-rebase code. Lift all of it into a SceneLoader QObject in the library; both apps now consume it via signals. Sidecar writes stay on the full-app side since they need the consumer's element metadata strings. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-full/MainWindow.cpp | 468 ++++++++---------------- src/ifcviewer-full/MainWindow.h | 65 ++-- src/ifcviewer-minimal/MinimalWindow.cpp | 204 ++--------- src/ifcviewer-minimal/MinimalWindow.h | 42 +-- src/ifcviewer/SceneLoader.cpp | 237 ++++++++++++ src/ifcviewer/SceneLoader.h | 126 +++++++ 6 files changed, 582 insertions(+), 560 deletions(-) create mode 100644 src/ifcviewer/SceneLoader.cpp create mode 100644 src/ifcviewer/SceneLoader.h diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index 865b464170..7dfaadb143 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -32,6 +32,7 @@ #include #include #include +#include MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) @@ -39,7 +40,26 @@ MainWindow::MainWindow(QWidget* parent) setupUi(); setupMenus(); - connect(viewport_, &ViewportWindow::frameStatsUpdated, this, [this](const ViewportWindow::FrameStats& s) { + loader_ = new SceneLoader(viewport_, this); + connect(loader_, &SceneLoader::loadStarted, + this, &MainWindow::onLoadStarted); + connect(loader_, &SceneLoader::progressChanged, + this, &MainWindow::onLoadProgressChanged); + connect(loader_, &SceneLoader::sidecarElementsReady, + this, &MainWindow::onSidecarElementsReady); + connect(loader_, &SceneLoader::loadedFromSidecar, + this, &MainWindow::onLoadedFromSidecar); + connect(loader_, &SceneLoader::streamedElementsReady, + this, &MainWindow::onStreamedElementsReady); + connect(loader_, &SceneLoader::loadedFromStream, + this, &MainWindow::onLoadedFromStream); + connect(loader_, &SceneLoader::loadError, + this, &MainWindow::onLoadError); + connect(loader_, &SceneLoader::allLoadsFinished, + this, &MainWindow::onAllLoadsFinished); + + connect(viewport_, &ViewportWindow::frameStatsUpdated, this, + [this](const ViewportWindow::FrameStats& s) { if (!stats_label_->isVisible()) return; stats_label_->setText( QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 gl_draws (%8 sub)") @@ -58,24 +78,13 @@ MainWindow::MainWindow(QWidget* parent) if (!show) stats_label_->clear(); }); - connect(&element_poll_timer_, &QTimer::timeout, this, &MainWindow::pollNewElements); - element_poll_timer_.setInterval(100); - setWindowTitle("IfcViewer"); resize(1400, 900); } -MainWindow::~MainWindow() { - joinSidecarThread(); -} - -void MainWindow::joinSidecarThread() { - if (sidecar_read_thread_.joinable()) - sidecar_read_thread_.join(); -} +MainWindow::~MainWindow() = default; void MainWindow::setupUi() { - // 3D Viewport as central widget viewport_ = new ViewportWindow(); viewport_container_ = QWidget::createWindowContainer(viewport_, this); viewport_container_->setMinimumSize(400, 300); @@ -84,7 +93,6 @@ void MainWindow::setupUi() { connect(viewport_, &ViewportWindow::objectPicked, this, &MainWindow::onObjectPicked); - // Element tree dock auto* tree_dock = new QDockWidget("Elements", this); tree_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); element_tree_ = new QTreeWidget(); @@ -96,7 +104,6 @@ void MainWindow::setupUi() { tree_dock->setWidget(element_tree_); addDockWidget(Qt::LeftDockWidgetArea, tree_dock); - // Properties dock auto* prop_dock = new QDockWidget("Properties", this); prop_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); property_table_ = new QTableWidget(); @@ -108,7 +115,6 @@ void MainWindow::setupUi() { prop_dock->setWidget(property_table_); addDockWidget(Qt::RightDockWidgetArea, prop_dock); - // Status bar with progress progress_bar_ = new QProgressBar(); progress_bar_->setMaximumWidth(200); progress_bar_->setVisible(false); @@ -148,171 +154,78 @@ void MainWindow::onFileSettings() { } void MainWindow::addFiles(const QStringList& paths) { - for (const auto& path : paths) { - ModelId id = next_model_id_++; - - ModelHandle handle; - handle.id = id; - handle.file_path = path; - handle.display_name = QFileInfo(path).fileName(); - handle.streamer = new GeometryStreamer(this); - - // Create top-level tree item for this model + auto ids = loader_->addFiles(paths); + for (int i = 0; i < paths.size() && i < static_cast(ids.size()); ++i) { + uint32_t id = ids[i]; + QString display = QFileInfo(paths[i]).fileName(); auto* root = new QTreeWidgetItem(element_tree_); - root->setText(0, handle.display_name); + root->setText(0, display); root->setText(1, "IFC Model"); - root->setData(0, Qt::UserRole, static_cast(0)); // 0 = not a pickable object - handle.tree_root = root; - - models_[id] = handle; - load_queue_.push_back(id); - } - - if (loading_model_id_ == 0) { - QTimer::singleShot(0, this, &MainWindow::startNextLoad); + root->setData(0, Qt::UserRole, static_cast(0)); + tree_roots_[id] = root; } } -void MainWindow::connectStreamer(GeometryStreamer* streamer) { - connect(streamer, &GeometryStreamer::progressChanged, - this, &MainWindow::onProgressChanged, Qt::QueuedConnection); - connect(streamer, &GeometryStreamer::meshReady, - this, &MainWindow::onMeshReady, Qt::QueuedConnection); - connect(streamer, &GeometryStreamer::instanceReady, - this, &MainWindow::onInstanceReady, Qt::QueuedConnection); - connect(streamer, &GeometryStreamer::finished, - this, &MainWindow::onStreamingFinished, Qt::QueuedConnection); - connect(streamer, &GeometryStreamer::errorOccurred, this, [this](const QString& msg) { - QMessageBox::warning(this, "Error", msg); - }, Qt::QueuedConnection); +void MainWindow::onLoadStarted(uint32_t /*mid*/, QString display_name) { + progress_bar_->setValue(0); + progress_bar_->setVisible(true); + status_label_->setText("Loading: " + display_name); } -void MainWindow::startNextLoad() { - if (load_queue_.empty()) { - loading_model_id_ = 0; - applyPendingBenchmark(); - return; +void MainWindow::onLoadProgressChanged(int percent) { + progress_bar_->setValue(percent); +} + +void MainWindow::appendElementToTree(uint32_t model_id, + uint32_t object_id, + int ifc_id, + int parent_ifc_id, + const std::string& guid, + const std::string& name, + const std::string& type) { + auto root_it = tree_roots_.find(model_id); + QTreeWidgetItem* parent_item = (root_it != tree_roots_.end()) ? root_it->second : nullptr; + + auto parent_obj_it = scoped_ifc_id_to_object_id_.find( + scopedKey(model_id, parent_ifc_id)); + if (parent_obj_it != scoped_ifc_id_to_object_id_.end()) { + auto tree_it = tree_items_.find(parent_obj_it->second); + if (tree_it != tree_items_.end()) { + parent_item = tree_it->second; + } } - loading_model_id_ = load_queue_.front(); - load_queue_.pop_front(); + QString display_name = QString::fromStdString(name); + if (display_name.isEmpty()) { + display_name = QString::fromStdString(type) + " #" + QString::number(ifc_id); + } - auto& model = models_[loading_model_id_]; + auto* item = new QTreeWidgetItem(parent_item); + item->setText(0, display_name); + item->setText(1, QString::fromStdString(type)); + item->setText(2, QString::fromStdString(guid)); + item->setData(0, Qt::UserRole, object_id); - load_timer_.restart(); - status_label_->setText("Loading: " + model.display_name); - - // Try sidecar on a background thread so the UI stays responsive. - std::string ifc_path = model.file_path.toStdString(); - uint64_t file_size = static_cast(QFileInfo(model.file_path).size()); - ModelId mid = loading_model_id_; - - joinSidecarThread(); - sidecar_read_thread_ = std::thread([this, ifc_path, file_size, mid]() { - QElapsedTimer rt; rt.start(); - auto cached = readSidecar(ifc_path, file_size); - qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str()); - auto result = std::make_shared>(std::move(cached)); - QMetaObject::invokeMethod(this, [this, mid, result]() { - if (*result && !(*result)->instances.empty()) { - applySidecarData(mid, std::move(**result)); - } else { - // No sidecar — fall back to streaming from IFC. - auto it = models_.find(mid); - if (it == models_.end()) return; - auto& m = it->second; - connectStreamer(m.streamer); - progress_bar_->setValue(0); - progress_bar_->setVisible(true); - status_label_->setText("Loading: " + m.display_name); - element_poll_timer_.start(); - m.streamer->loadFile( - m.file_path.toStdString(), next_object_id_, loading_model_id_); - } - }, Qt::QueuedConnection); - }); + tree_items_[object_id] = item; } -void MainWindow::applySidecarData(ModelId mid, SidecarData data) { - auto it = models_.find(mid); - if (it == models_.end()) return; - auto& model = it->second; - - qDebug("Sidecar hit: %s (%zu verts, %zu indices, %zu meshes, %zu instances, %zu elements)", - model.file_path.toStdString().c_str(), - data.vertices.size() / INSTANCED_VERTEX_STRIDE_BYTES, - data.indices.size(), - data.meshes.size(), - data.instances.size(), - data.elements.size()); +void MainWindow::onSidecarElementsReady(uint32_t mid, + std::vector elements, + std::string string_table) { + auto str = [&](uint32_t offset, uint32_t length) -> std::string { + if (length == 0 || offset + length > string_table.size()) return {}; + return string_table.substr(offset, length); + }; QElapsedTimer t; t.start(); - - // Sidecars store raw object_ids and model_ids from the session that wrote - // them. On load we must rebase both onto the current session's ID space, - // or two cached models collide (both starting at object_id=1, both - // claiming the original model_id). Offset by (next_object_id_ - min_id) - // so the first cached object takes the next free slot. - uint32_t min_oid = UINT32_MAX; - for (const auto& pe : data.elements) { - if (pe.object_id < min_oid) min_oid = pe.object_id; - } - uint32_t oid_offset = 0; - if (!data.elements.empty() && min_oid < UINT32_MAX) { - oid_offset = next_object_id_ - min_oid; - } - for (auto& pe : data.elements) { - pe.object_id += oid_offset; - pe.model_id = mid; - if (pe.object_id >= next_object_id_) - next_object_id_ = pe.object_id + 1; - } - for (auto& inst : data.instances) { - inst.object_id += oid_offset; - inst.model_id = mid; - } - - // Hand off geometry to GPU in a single call. - std::vector elements = std::move(data.elements); - std::string stbl = std::move(data.string_table); - viewport_->applyCachedModel(mid, std::move(data)); - qDebug(" GL upload: %lld ms", t.elapsed()); - - t.restart(); element_tree_->setUpdatesEnabled(false); - populateTreeFromSidecar(model, elements, stbl); - element_tree_->setUpdatesEnabled(true); - qDebug(" Tree build: %lld ms (%zu elements)", t.elapsed(), elements.size()); - - progress_bar_->setVisible(false); - - qint64 ms = load_timer_.elapsed(); - QString elapsed = (ms >= 1000) - ? QString::number(ms / 1000.0, 'f', 2) + " s" - : QString::number(ms) + " ms"; - status_label_->setText(QString("%1 elements across %2 model(s) — loaded from cache in %3") - .arg(element_map_.size()) - .arg(models_.size()) - .arg(elapsed)); - - loading_model_id_ = 0; - QTimer::singleShot(0, this, &MainWindow::startNextLoad); -} - -void MainWindow::populateTreeFromSidecar(ModelHandle& model, - const std::vector& elements, - const std::string& stbl) { - auto str = [&](uint32_t offset, uint32_t length) -> std::string { - if (length == 0 || offset + length > stbl.size()) return {}; - return stbl.substr(offset, length); - }; for (const auto& pe : elements) { ElementInfo info; info.object_id = pe.object_id; - info.model_id = pe.model_id; - info.ifc_id = pe.ifc_id; + info.model_id = pe.model_id; + info.ifc_id = pe.ifc_id; info.parent_id = pe.parent_id; info.guid = str(pe.guid_offset, pe.guid_length); info.name = str(pe.name_offset, pe.name_length); @@ -321,131 +234,90 @@ void MainWindow::populateTreeFromSidecar(ModelHandle& model, element_map_[info.object_id] = info; scoped_ifc_id_to_object_id_[scopedKey(info.model_id, info.ifc_id)] = info.object_id; - // Find parent tree item. - QTreeWidgetItem* parent_item = model.tree_root; - auto parent_obj_it = scoped_ifc_id_to_object_id_.find( - scopedKey(info.model_id, info.parent_id)); - if (parent_obj_it != scoped_ifc_id_to_object_id_.end()) { - auto tree_it = tree_items_.find(parent_obj_it->second); - if (tree_it != tree_items_.end()) { - parent_item = tree_it->second; - } - } - - QString display_name = QString::fromStdString(info.name); - if (display_name.isEmpty()) { - display_name = QString::fromStdString(info.type) + " #" + QString::number(info.ifc_id); - } - - auto* item = new QTreeWidgetItem(parent_item); - item->setText(0, display_name); - item->setText(1, QString::fromStdString(info.type)); - item->setText(2, QString::fromStdString(info.guid)); - item->setData(0, Qt::UserRole, info.object_id); - - tree_items_[info.object_id] = item; - } -} - -void MainWindow::onProgressChanged(int percent) { - progress_bar_->setValue(percent); -} - -void MainWindow::onMeshReady(MeshChunk chunk) { - viewport_->uploadMeshChunk(chunk); -} - -void MainWindow::onInstanceReady(InstanceChunk chunk) { - viewport_->uploadInstanceChunk(chunk); -} - -void MainWindow::onStreamingFinished() { - element_poll_timer_.stop(); - pollNewElements(); // drain remaining - - // Update next_object_id_ from the streamer that just finished. - if (loading_model_id_ != 0) { - auto it = models_.find(loading_model_id_); - if (it != models_.end()) { - next_object_id_ = it->second.streamer->lastObjectId(); - } + appendElementToTree(info.model_id, info.object_id, info.ifc_id, + info.parent_id, info.guid, info.name, info.type); } + element_tree_->setUpdatesEnabled(true); + qDebug(" Tree build: %lld ms (%zu elements)", t.elapsed(), elements.size()); +} + +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") + .arg(element_map_.size()) + .arg(loader_->modelCount()) + .arg(formatElapsed(elapsed_ms))); +} - qint64 ms = load_timer_.elapsed(); - QString elapsed = (ms >= 1000) - ? QString::number(ms / 1000.0, 'f', 2) + " s" - : QString::number(ms) + " ms"; +void MainWindow::onStreamedElementsReady(uint32_t /*mid*/, std::vector elements) { + for (const auto& info : elements) { + element_map_[info.object_id] = info; + scoped_ifc_id_to_object_id_[scopedKey(info.model_id, info.ifc_id)] = info.object_id; + appendElementToTree(info.model_id, info.object_id, info.ifc_id, + info.parent_id, info.guid, info.name, info.type); + } +} - size_t total_elements = element_map_.size(); - size_t num_models = models_.size(); - status_label_->setText(QString("%1 elements across %2 model(s) — last loaded in %3") - .arg(total_elements) - .arg(num_models) - .arg(elapsed)); +void MainWindow::writeSidecarForModel(uint32_t mid) { + SidecarData sd; + if (!viewport_->snapshotModel(mid, sd)) return; - // Sort instances by mesh, upload the per-model instance SSBO, and - // persist a v4 sidecar for next load. - if (loading_model_id_ != 0) { - viewport_->finalizeModel(loading_model_id_); - - auto it = models_.find(loading_model_id_); - if (it != models_.end()) { - SidecarData sd; - if (viewport_->snapshotModel(loading_model_id_, sd)) { - // Pack this model's element metadata + string table. - for (const auto& [oid, info] : element_map_) { - if (info.model_id != loading_model_id_) continue; - PackedElementInfo pe; - pe.object_id = info.object_id; - pe.model_id = info.model_id; - pe.ifc_id = info.ifc_id; - pe.parent_id = info.parent_id; - pe.guid_offset = static_cast(sd.string_table.size()); - pe.guid_length = static_cast(info.guid.size()); - sd.string_table += info.guid; - pe.name_offset = static_cast(sd.string_table.size()); - pe.name_length = static_cast(info.name.size()); - sd.string_table += info.name; - pe.type_offset = static_cast(sd.string_table.size()); - pe.type_length = static_cast(info.type.size()); - sd.string_table += info.type; - sd.elements.push_back(pe); - } - - // Build LOD1 for eligible meshes (extends sd.indices and - // populates MeshInfo::lod1_*), push the extension onto the - // live GPU state so this session benefits too, then cache. - QElapsedTimer t_lod; t_lod.start(); - buildLods(sd); - LodStats ls = summariseLods(sd); - qDebug(" LOD build: %lld ms — %u/%u meshes got LOD1 " - "(%u tris → %u tris for those meshes)", - t_lod.elapsed(), - ls.meshes_with_lod1, ls.meshes_total, - ls.tris_lod0_for_lod1, ls.tris_lod1); - viewport_->applyLodExtension(loading_model_id_, sd); - - std::string ifc_path = it->second.file_path.toStdString(); - uint64_t file_size = static_cast( - QFileInfo(it->second.file_path).size()); - QElapsedTimer t; t.start(); - bool ok = writeSidecar(ifc_path, sd, file_size); - qDebug(" Sidecar write: %lld ms (%s)", - t.elapsed(), ok ? "ok" : "FAILED"); - } - } + for (const auto& [oid, info] : element_map_) { + if (info.model_id != mid) continue; + PackedElementInfo pe; + pe.object_id = info.object_id; + pe.model_id = info.model_id; + pe.ifc_id = info.ifc_id; + pe.parent_id = info.parent_id; + pe.guid_offset = static_cast(sd.string_table.size()); + pe.guid_length = static_cast(info.guid.size()); + sd.string_table += info.guid; + pe.name_offset = static_cast(sd.string_table.size()); + pe.name_length = static_cast(info.name.size()); + sd.string_table += info.name; + pe.type_offset = static_cast(sd.string_table.size()); + pe.type_length = static_cast(info.type.size()); + sd.string_table += info.type; + sd.elements.push_back(pe); } - // Start next model if queued. - startNextLoad(); + QElapsedTimer t_lod; t_lod.start(); + buildLods(sd); + LodStats ls = summariseLods(sd); + qDebug(" LOD build: %lld ms — %u/%u meshes got LOD1 " + "(%u tris -> %u tris for those meshes)", + t_lod.elapsed(), + ls.meshes_with_lod1, ls.meshes_total, + ls.tris_lod0_for_lod1, ls.tris_lod1); + viewport_->applyLodExtension(mid, sd); + + QElapsedTimer t; t.start(); + bool ok = writeSidecar(loader_->filePath(mid).toStdString(), sd, loader_->fileSize(mid)); + qDebug(" Sidecar write: %lld ms (%s)", t.elapsed(), ok ? "ok" : "FAILED"); +} + +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") + .arg(element_map_.size()) + .arg(loader_->modelCount()) + .arg(formatElapsed(elapsed_ms))); + + writeSidecarForModel(mid); +} + +void MainWindow::onLoadError(uint32_t /*mid*/, QString message) { + QMessageBox::warning(this, "Error", message); +} + +void MainWindow::onAllLoadsFinished() { + applyPendingBenchmark(); } void MainWindow::onObjectPicked(uint32_t object_id) { viewport_->setSelectedObjectId(object_id); - // Select in tree auto it = tree_items_.find(object_id); if (it != tree_items_.end()) { element_tree_->blockSignals(true); @@ -465,45 +337,6 @@ void MainWindow::onTreeSelectionChanged() { populateProperties(object_id); } -void MainWindow::pollNewElements() { - if (loading_model_id_ == 0) return; - - auto it = models_.find(loading_model_id_); - if (it == models_.end()) return; - - auto& model = it->second; - auto elements = model.streamer->drainElements(); - - for (auto& info : elements) { - element_map_[info.object_id] = info; - scoped_ifc_id_to_object_id_[scopedKey(info.model_id, info.ifc_id)] = info.object_id; - - // Find parent tree item (scoped to this model) - QTreeWidgetItem* parent_item = model.tree_root; - auto parent_obj_it = scoped_ifc_id_to_object_id_.find( - scopedKey(info.model_id, info.parent_id)); - if (parent_obj_it != scoped_ifc_id_to_object_id_.end()) { - auto tree_it = tree_items_.find(parent_obj_it->second); - if (tree_it != tree_items_.end()) { - parent_item = tree_it->second; - } - } - - QString display_name = QString::fromStdString(info.name); - if (display_name.isEmpty()) { - display_name = QString::fromStdString(info.type) + " #" + QString::number(info.ifc_id); - } - - auto* item = new QTreeWidgetItem(parent_item); - item->setText(0, display_name); - item->setText(1, QString::fromStdString(info.type)); - item->setText(2, QString::fromStdString(info.guid)); - item->setData(0, Qt::UserRole, info.object_id); - - tree_items_[info.object_id] = item; - } -} - void MainWindow::populateProperties(uint32_t object_id) { property_table_->setRowCount(0); if (object_id == 0) return; @@ -525,17 +358,12 @@ void MainWindow::populateProperties(uint32_t object_id) { addRow("Name", QString::fromStdString(info.name)); addRow("Type", QString::fromStdString(info.type)); - // Find the correct model's file for property lookup - auto model_it = models_.find(info.model_id); - if (model_it == models_.end()) return; - - auto* file = model_it->second.streamer->ifcFile(); + auto* file = loader_->ifcFile(info.model_id); if (!file) return; auto product = file->instance_by_id(info.ifc_id); if (!product) return; - // Show all direct attributes auto& decl = product.declaration(); if (auto* entity = decl.as_entity()) { for (size_t i = 0; i < entity->attribute_count(); ++i) { @@ -586,3 +414,9 @@ void MainWindow::applyPendingBenchmark() { pending_benchmark_ = 0; } } + +QString MainWindow::formatElapsed(qint64 ms) const { + return (ms >= 1000) + ? QString::number(ms / 1000.0, 'f', 2) + " s" + : QString::number(ms) + " ms"; +} diff --git a/src/ifcviewer-full/MainWindow.h b/src/ifcviewer-full/MainWindow.h index 4f5a946998..83c1990c96 100644 --- a/src/ifcviewer-full/MainWindow.h +++ b/src/ifcviewer-full/MainWindow.h @@ -26,30 +26,16 @@ #include #include #include -#include #include #include -#include -#include #include #include "ViewportWindow.h" -#include "GeometryStreamer.h" +#include "SceneLoader.h" class SettingsWindow; -using ModelId = uint32_t; - -struct ModelHandle { - ModelId id = 0; - QString file_path; - QString display_name; - GeometryStreamer* streamer = nullptr; - QTreeWidgetItem* tree_root = nullptr; - bool visible = true; -}; - class MainWindow : public QMainWindow { Q_OBJECT public: @@ -63,27 +49,37 @@ public: private slots: void onFileOpen(); void onFileSettings(); - void onProgressChanged(int percent); - void onMeshReady(MeshChunk chunk); - void onInstanceReady(InstanceChunk chunk); - void onStreamingFinished(); void onObjectPicked(uint32_t object_id); void onTreeSelectionChanged(); - void pollNewElements(); + + void onLoadStarted(uint32_t mid, QString display_name); + void onLoadProgressChanged(int percent); + void onSidecarElementsReady(uint32_t mid, + std::vector elements, + std::string string_table); + 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 onLoadError(uint32_t mid, QString message); + void onAllLoadsFinished(); private: void setupUi(); void setupMenus(); void populateProperties(uint32_t object_id); - void startNextLoad(); - void applySidecarData(ModelId mid, SidecarData data); - void joinSidecarThread(); - void populateTreeFromSidecar(ModelHandle& model, - const std::vector& elements, - const std::string& string_table); - void connectStreamer(GeometryStreamer* streamer); + void appendElementToTree(uint32_t model_id, + uint32_t object_id, + int ifc_id, + int parent_ifc_id, + const std::string& guid, + const std::string& name, + const std::string& type); + void writeSidecarForModel(uint32_t mid); + void applyPendingBenchmark(); + QString formatElapsed(qint64 ms) const; ViewportWindow* viewport_ = nullptr; + SceneLoader* loader_ = nullptr; SettingsWindow* settings_ = nullptr; QWidget* viewport_container_ = nullptr; QTreeWidget* element_tree_ = nullptr; @@ -91,18 +87,11 @@ private: QProgressBar* progress_bar_ = nullptr; QLabel* status_label_ = nullptr; QLabel* stats_label_ = nullptr; - QTimer element_poll_timer_; - QElapsedTimer load_timer_; - // Multi-model state - std::map models_; - ModelId next_model_id_ = 1; - uint32_t next_object_id_ = 1; // monotonically increasing across all models - std::deque load_queue_; - ModelId loading_model_id_ = 0; - std::thread sidecar_read_thread_; + // Per-model tree roots, keyed by model_id. + std::map tree_roots_; - // Map object_id -> tree item and element info + // Display-side element registry for tree + property lookup. std::unordered_map element_map_; std::unordered_map tree_items_; // Scoped (model_id, ifc_id) -> object_id @@ -114,8 +103,6 @@ private: QString pending_camera_; int pending_benchmark_ = 0; - - void applyPendingBenchmark(); }; #endif // MAINWINDOW_H diff --git a/src/ifcviewer-minimal/MinimalWindow.cpp b/src/ifcviewer-minimal/MinimalWindow.cpp index 2d3593165c..18e6eaae44 100644 --- a/src/ifcviewer-minimal/MinimalWindow.cpp +++ b/src/ifcviewer-minimal/MinimalWindow.cpp @@ -19,17 +19,10 @@ #include "MinimalWindow.h" #include "AppSettings.h" -#include "SidecarCache.h" -#include -#include #include -#include #include -#include -#include - MinimalWindow::MinimalWindow(QWidget* parent) : QMainWindow(parent) { @@ -45,6 +38,18 @@ MinimalWindow::MinimalWindow(QWidget* parent) statusBar()->addWidget(status_label_, 1); statusBar()->addPermanentWidget(stats_label_); + loader_ = new SceneLoader(viewport_, this); + connect(loader_, &SceneLoader::loadStarted, + this, &MinimalWindow::onLoadStarted); + connect(loader_, &SceneLoader::loadedFromSidecar, + this, &MinimalWindow::onLoadedFromSidecar); + connect(loader_, &SceneLoader::loadedFromStream, + this, &MinimalWindow::onLoadedFromStream); + connect(loader_, &SceneLoader::loadError, + this, &MinimalWindow::onLoadError); + connect(loader_, &SceneLoader::allLoadsFinished, + this, &MinimalWindow::onAllLoadsFinished); + connect(viewport_, &ViewportWindow::frameStatsUpdated, this, [this](const ViewportWindow::FrameStats& s) { if (!stats_label_->isVisible()) return; @@ -65,187 +70,46 @@ MinimalWindow::MinimalWindow(QWidget* parent) if (!show) stats_label_->clear(); }); - // Periodically drain the streamer's pending-elements buffer so it doesn't - // grow unbounded on large models. We don't use the element data here — - // this window has no tree — but the buffer must still be flushed. - connect(&element_drain_timer_, &QTimer::timeout, this, &MinimalWindow::drainStreamerElements); - element_drain_timer_.setInterval(250); - setWindowTitle("IfcViewerMinimal"); resize(1200, 800); } -MinimalWindow::~MinimalWindow() { - joinSidecarThread(); -} - -void MinimalWindow::joinSidecarThread() { - if (sidecar_read_thread_.joinable()) - sidecar_read_thread_.join(); -} - void MinimalWindow::addFiles(const QStringList& paths) { - for (const auto& path : paths) { - uint32_t id = next_model_id_++; - ModelEntry entry; - entry.id = id; - entry.file_path = path; - entry.display_name = QFileInfo(path).fileName(); - entry.streamer = new GeometryStreamer(this); - models_[id] = entry; - load_queue_.push_back(id); - } - - if (loading_model_id_ == 0) { - QTimer::singleShot(0, this, &MinimalWindow::startNextLoad); - } + loader_->addFiles(paths); } -void MinimalWindow::connectStreamer(GeometryStreamer* streamer) { - connect(streamer, &GeometryStreamer::meshReady, - this, &MinimalWindow::onMeshReady, Qt::QueuedConnection); - connect(streamer, &GeometryStreamer::instanceReady, - this, &MinimalWindow::onInstanceReady, Qt::QueuedConnection); - connect(streamer, &GeometryStreamer::finished, - this, &MinimalWindow::onStreamingFinished, Qt::QueuedConnection); - connect(streamer, &GeometryStreamer::errorOccurred, - this, &MinimalWindow::onErrorOccurred, Qt::QueuedConnection); -} - -void MinimalWindow::startNextLoad() { - if (load_queue_.empty()) { - loading_model_id_ = 0; - status_label_->setText(QString("Loaded %1 model(s)").arg(models_.size())); - applyPendingBenchmark(); - return; - } - - loading_model_id_ = load_queue_.front(); - load_queue_.pop_front(); - - auto& model = models_[loading_model_id_]; - - load_timer_.restart(); - status_label_->setText("Loading: " + model.display_name); - - std::string ifc_path = model.file_path.toStdString(); - uint64_t file_size = static_cast(QFileInfo(model.file_path).size()); - uint32_t mid = loading_model_id_; - - joinSidecarThread(); - sidecar_read_thread_ = std::thread([this, ifc_path, file_size, mid]() { - QElapsedTimer rt; rt.start(); - auto cached = readSidecar(ifc_path, file_size); - qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str()); - auto result = std::make_shared>(std::move(cached)); - QMetaObject::invokeMethod(this, [this, mid, result]() { - if (*result && !(*result)->instances.empty()) { - applySidecarData(mid, std::move(**result)); - } else { - auto it = models_.find(mid); - if (it == models_.end()) return; - auto& m = it->second; - connectStreamer(m.streamer); - element_drain_timer_.start(); - m.streamer->loadFile( - m.file_path.toStdString(), next_object_id_, loading_model_id_); - } - }, Qt::QueuedConnection); - }); -} - -void MinimalWindow::applySidecarData(uint32_t mid, SidecarData data) { - auto it = models_.find(mid); - if (it == models_.end()) return; - auto& model = it->second; - - qDebug("Sidecar hit: %s (%zu verts, %zu indices, %zu meshes, %zu instances, %zu elements)", - model.file_path.toStdString().c_str(), - data.vertices.size() / INSTANCED_VERTEX_STRIDE_BYTES, - data.indices.size(), - data.meshes.size(), - data.instances.size(), - data.elements.size()); - - // Rebase object/model IDs onto the current session's ID space. Matches - // MainWindow::applySidecarData — two cached models both starting at - // object_id=1 would collide otherwise. - uint32_t min_oid = UINT32_MAX; - for (const auto& pe : data.elements) { - if (pe.object_id < min_oid) min_oid = pe.object_id; - } - uint32_t oid_offset = 0; - if (!data.elements.empty() && min_oid < UINT32_MAX) { - oid_offset = next_object_id_ - min_oid; - } - for (auto& pe : data.elements) { - pe.object_id += oid_offset; - pe.model_id = mid; - if (pe.object_id >= next_object_id_) - next_object_id_ = pe.object_id + 1; - } - for (auto& inst : data.instances) { - inst.object_id += oid_offset; - inst.model_id = mid; - } - - viewport_->applyCachedModel(mid, std::move(data)); - - qint64 ms = load_timer_.elapsed(); - QString elapsed = (ms >= 1000) +static QString formatElapsed(qint64 ms) { + return (ms >= 1000) ? QString::number(ms / 1000.0, 'f', 2) + " s" : QString::number(ms) + " ms"; +} + +void MinimalWindow::onLoadStarted(uint32_t /*mid*/, QString display_name) { + status_label_->setText("Loading: " + display_name); +} + +void MinimalWindow::onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms) { status_label_->setText(QString("%1 loaded from cache in %2") - .arg(model.display_name).arg(elapsed)); - - loading_model_id_ = 0; - QTimer::singleShot(0, this, &MinimalWindow::startNextLoad); + .arg(loader_->displayName(mid)) + .arg(formatElapsed(elapsed_ms))); } -void MinimalWindow::onMeshReady(MeshChunk chunk) { - viewport_->uploadMeshChunk(chunk); +void MinimalWindow::onLoadedFromStream(uint32_t mid, qint64 elapsed_ms) { + status_label_->setText(QString("%1 streamed in %2") + .arg(loader_->displayName(mid)) + .arg(formatElapsed(elapsed_ms))); } -void MinimalWindow::onInstanceReady(InstanceChunk chunk) { - viewport_->uploadInstanceChunk(chunk); -} - -void MinimalWindow::drainStreamerElements() { - if (loading_model_id_ == 0) return; - auto it = models_.find(loading_model_id_); - if (it == models_.end()) return; - (void)it->second.streamer->drainElements(); -} - -void MinimalWindow::onStreamingFinished() { - element_drain_timer_.stop(); - drainStreamerElements(); - - if (loading_model_id_ != 0) { - auto it = models_.find(loading_model_id_); - if (it != models_.end()) { - next_object_id_ = it->second.streamer->lastObjectId(); - viewport_->finalizeModel(loading_model_id_); - } - } - - qint64 ms = load_timer_.elapsed(); - QString elapsed = (ms >= 1000) - ? QString::number(ms / 1000.0, 'f', 2) + " s" - : QString::number(ms) + " ms"; - - auto it = models_.find(loading_model_id_); - QString name = (it != models_.end()) ? it->second.display_name : QString(); - status_label_->setText(QString("%1 streamed in %2").arg(name).arg(elapsed)); - - startNextLoad(); -} - -void MinimalWindow::onErrorOccurred(const QString& message) { +void MinimalWindow::onLoadError(uint32_t /*mid*/, QString message) { qWarning("IfcViewerMinimal error: %s", qPrintable(message)); status_label_->setText("Error: " + message); } +void MinimalWindow::onAllLoadsFinished() { + status_label_->setText(QString("Loaded %1 model(s)").arg(loader_->modelCount())); + applyPendingBenchmark(); +} + void MinimalWindow::setPendingCamera(const QString& params) { pending_camera_ = params; } diff --git a/src/ifcviewer-minimal/MinimalWindow.h b/src/ifcviewer-minimal/MinimalWindow.h index 12ea533c40..723dd82fba 100644 --- a/src/ifcviewer-minimal/MinimalWindow.h +++ b/src/ifcviewer-minimal/MinimalWindow.h @@ -22,62 +22,36 @@ #include #include -#include -#include - -#include -#include -#include #include "ViewportWindow.h" -#include "GeometryStreamer.h" +#include "SceneLoader.h" class MinimalWindow : public QMainWindow { Q_OBJECT public: explicit MinimalWindow(QWidget* parent = nullptr); - ~MinimalWindow(); + ~MinimalWindow() = default; void addFiles(const QStringList& paths); void setPendingCamera(const QString& params); void setPendingBenchmark(int frames); private slots: - void onMeshReady(MeshChunk chunk); - void onInstanceReady(InstanceChunk chunk); - void onStreamingFinished(); - void onErrorOccurred(const QString& message); - void drainStreamerElements(); + 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 onLoadError(uint32_t mid, QString message); + void onAllLoadsFinished(); private: - struct ModelEntry { - uint32_t id = 0; - QString file_path; - QString display_name; - GeometryStreamer* streamer = nullptr; - }; - - void startNextLoad(); - void connectStreamer(GeometryStreamer* streamer); - void joinSidecarThread(); - void applySidecarData(uint32_t mid, SidecarData data); void applyPendingBenchmark(); ViewportWindow* viewport_ = nullptr; + SceneLoader* loader_ = nullptr; QWidget* viewport_container_ = nullptr; QLabel* status_label_ = nullptr; QLabel* stats_label_ = nullptr; - std::map models_; - std::deque load_queue_; - uint32_t next_model_id_ = 1; - uint32_t next_object_id_ = 1; - uint32_t loading_model_id_ = 0; - std::thread sidecar_read_thread_; - - QTimer element_drain_timer_; - QElapsedTimer load_timer_; - QString pending_camera_; int pending_benchmark_ = 0; }; diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp new file mode 100644 index 0000000000..16587b16bf --- /dev/null +++ b/src/ifcviewer/SceneLoader.cpp @@ -0,0 +1,237 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#include "SceneLoader.h" + +#include +#include +#include + +#include +#include +#include + +SceneLoader::SceneLoader(ViewportWindow* viewport, QObject* parent) + : QObject(parent), viewport_(viewport) +{ + connect(&element_poll_timer_, &QTimer::timeout, + this, &SceneLoader::onElementPollTick); + element_poll_timer_.setInterval(100); +} + +SceneLoader::~SceneLoader() { + joinSidecarThread(); +} + +void SceneLoader::joinSidecarThread() { + if (sidecar_read_thread_.joinable()) + sidecar_read_thread_.join(); +} + +QString SceneLoader::filePath(uint32_t mid) const { + auto it = models_.find(mid); + return it == models_.end() ? QString() : it->second.file_path; +} + +QString SceneLoader::displayName(uint32_t mid) const { + auto it = models_.find(mid); + return it == models_.end() ? QString() : it->second.display_name; +} + +uint64_t SceneLoader::fileSize(uint32_t mid) const { + auto it = models_.find(mid); + if (it == models_.end()) return 0; + return static_cast(QFileInfo(it->second.file_path).size()); +} + +ifcopenshell::file* SceneLoader::ifcFile(uint32_t mid) const { + auto it = models_.find(mid); + return it == models_.end() ? nullptr : it->second.streamer->ifcFile(); +} + +std::vector SceneLoader::addFiles(const QStringList& paths) { + std::vector assigned; + assigned.reserve(paths.size()); + for (const auto& path : paths) { + uint32_t id = next_model_id_++; + Entry entry; + entry.id = id; + entry.file_path = path; + entry.display_name = QFileInfo(path).fileName(); + entry.streamer = new GeometryStreamer(this); + models_[id] = std::move(entry); + load_queue_.push_back(id); + assigned.push_back(id); + } + + if (loading_model_id_ == 0) { + QTimer::singleShot(0, this, &SceneLoader::startNextLoad); + } + return assigned; +} + +void SceneLoader::connectStreamer(GeometryStreamer* streamer) { + connect(streamer, &GeometryStreamer::progressChanged, + this, &SceneLoader::onStreamerProgressChanged, Qt::QueuedConnection); + connect(streamer, &GeometryStreamer::meshReady, + this, &SceneLoader::onStreamerMeshReady, Qt::QueuedConnection); + connect(streamer, &GeometryStreamer::instanceReady, + this, &SceneLoader::onStreamerInstanceReady, Qt::QueuedConnection); + connect(streamer, &GeometryStreamer::finished, + this, &SceneLoader::onStreamerFinished, Qt::QueuedConnection); + connect(streamer, &GeometryStreamer::errorOccurred, + this, &SceneLoader::onStreamerError, Qt::QueuedConnection); +} + +void SceneLoader::startNextLoad() { + if (load_queue_.empty()) { + loading_model_id_ = 0; + emit allLoadsFinished(); + return; + } + + loading_model_id_ = load_queue_.front(); + load_queue_.pop_front(); + + auto& model = models_[loading_model_id_]; + model.load_timer.restart(); + + emit loadStarted(model.id, model.display_name); + + std::string ifc_path = model.file_path.toStdString(); + uint64_t file_size = this->fileSize(model.id); + uint32_t mid = loading_model_id_; + + // Sidecar read on a background thread so the UI stays responsive. + joinSidecarThread(); + sidecar_read_thread_ = std::thread([this, ifc_path, file_size, mid]() { + QElapsedTimer rt; rt.start(); + auto cached = readSidecar(ifc_path, file_size); + qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str()); + auto result = std::make_shared>(std::move(cached)); + QMetaObject::invokeMethod(this, [this, mid, result]() { + if (*result && !(*result)->instances.empty()) { + applySidecarData(mid, std::move(**result)); + } else { + auto it = models_.find(mid); + if (it == models_.end()) return; + auto& m = it->second; + connectStreamer(m.streamer); + element_poll_timer_.start(); + m.streamer->loadFile( + m.file_path.toStdString(), next_object_id_, loading_model_id_); + } + }, Qt::QueuedConnection); + }); +} + +void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) { + auto it = models_.find(mid); + if (it == models_.end()) return; + auto& model = it->second; + + qDebug("Sidecar hit: %s (%zu verts, %zu indices, %zu meshes, %zu instances, %zu elements)", + model.file_path.toStdString().c_str(), + data.vertices.size() / INSTANCED_VERTEX_STRIDE_BYTES, + data.indices.size(), + data.meshes.size(), + data.instances.size(), + data.elements.size()); + + // Rebase object/model IDs onto the current session's ID space. Two + // cached models both starting at object_id=1 would collide otherwise. + uint32_t min_oid = UINT32_MAX; + for (const auto& pe : data.elements) { + if (pe.object_id < min_oid) min_oid = pe.object_id; + } + uint32_t oid_offset = 0; + if (!data.elements.empty() && min_oid < UINT32_MAX) { + oid_offset = next_object_id_ - min_oid; + } + for (auto& pe : data.elements) { + pe.object_id += oid_offset; + pe.model_id = mid; + if (pe.object_id >= next_object_id_) + next_object_id_ = pe.object_id + 1; + } + for (auto& inst : data.instances) { + inst.object_id += oid_offset; + inst.model_id = mid; + } + + std::vector elements = std::move(data.elements); + std::string stbl = std::move(data.string_table); + + viewport_->applyCachedModel(mid, std::move(data)); + + emit sidecarElementsReady(mid, std::move(elements), std::move(stbl)); + + qint64 ms = model.load_timer.elapsed(); + emit loadedFromSidecar(mid, ms); + + loading_model_id_ = 0; + QTimer::singleShot(0, this, &SceneLoader::startNextLoad); +} + +void SceneLoader::onStreamerProgressChanged(int percent) { + emit progressChanged(percent); +} + +void SceneLoader::onStreamerMeshReady(MeshChunk chunk) { + viewport_->uploadMeshChunk(chunk); +} + +void SceneLoader::onStreamerInstanceReady(InstanceChunk chunk) { + viewport_->uploadInstanceChunk(chunk); +} + +void SceneLoader::onElementPollTick() { + if (loading_model_id_ == 0) return; + auto it = models_.find(loading_model_id_); + if (it == models_.end()) return; + + auto batch = it->second.streamer->drainElements(); + if (!batch.empty()) { + emit streamedElementsReady(loading_model_id_, std::move(batch)); + } +} + +void SceneLoader::onStreamerFinished() { + element_poll_timer_.stop(); + onElementPollTick(); // drain any remaining elements + + uint32_t mid = loading_model_id_; + if (mid != 0) { + auto it = models_.find(mid); + if (it != models_.end()) { + next_object_id_ = it->second.streamer->lastObjectId(); + viewport_->finalizeModel(mid); + + qint64 ms = it->second.load_timer.elapsed(); + emit loadedFromStream(mid, ms); + } + } + + loading_model_id_ = 0; + startNextLoad(); +} + +void SceneLoader::onStreamerError(const QString& msg) { + emit loadError(loading_model_id_, msg); +} diff --git a/src/ifcviewer/SceneLoader.h b/src/ifcviewer/SceneLoader.h new file mode 100644 index 0000000000..e29062808c --- /dev/null +++ b/src/ifcviewer/SceneLoader.h @@ -0,0 +1,126 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#ifndef SCENELOADER_H +#define SCENELOADER_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "ViewportWindow.h" +#include "GeometryStreamer.h" +#include "SidecarCache.h" + +// Drives IFC file loading into a ViewportWindow. Owns the per-model +// GeometryStreamer, the load queue, the sidecar read thread, and the +// next-free object_id counter used to rebase cached models onto the +// current session's ID space. +// +// Consumers (MainWindow, MinimalWindow) observe progress through signals +// and never touch the streamer, sidecar thread, or queue directly. +// Sidecar *writes* are intentionally left to the consumer: they need the +// consumer's element metadata (guid/name/type strings) which SceneLoader +// does not retain. +class SceneLoader : public QObject { + Q_OBJECT +public: + explicit SceneLoader(ViewportWindow* viewport, QObject* parent = nullptr); + ~SceneLoader(); + + // Returns the model_ids assigned to the enqueued paths, in order. + // 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); + bool isLoading() const { return loading_model_id_ != 0 || !load_queue_.empty(); } + size_t modelCount() const { return models_.size(); } + + QString filePath(uint32_t mid) const; + QString displayName(uint32_t mid) const; + uint64_t fileSize(uint32_t mid) const; + ifcopenshell::file* ifcFile(uint32_t mid) const; + +signals: + void progressChanged(int percent); + void loadStarted(uint32_t mid, QString display_name); + + // Fired once per sidecar hit, before loadedFromSidecar, with the full + // packed element set. Consumer is responsible for decoding + tree/ + // property-map population. Moved arguments — avoid unnecessary copies. + void sidecarElementsReady(uint32_t mid, + std::vector elements, + std::string string_table); + void loadedFromSidecar(uint32_t mid, qint64 elapsed_ms); + + // Fired repeatedly while streaming, as the worker thread produces + // elements. Each batch contains whatever accumulated since the last + // poll tick. + void streamedElementsReady(uint32_t mid, std::vector elements); + + // Fired once after the streamer finishes and the viewport has been + // finalized. Consumer may synchronously perform work that needs all + // 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 loadError(uint32_t mid, QString message); + void allLoadsFinished(); + +private slots: + void onStreamerProgressChanged(int percent); + void onStreamerMeshReady(MeshChunk chunk); + void onStreamerInstanceReady(InstanceChunk chunk); + void onStreamerFinished(); + void onStreamerError(const QString& msg); + void onElementPollTick(); + +private: + struct Entry { + uint32_t id = 0; + QString file_path; + QString display_name; + GeometryStreamer* streamer = nullptr; + QElapsedTimer load_timer; + }; + + void startNextLoad(); + void connectStreamer(GeometryStreamer* streamer); + void joinSidecarThread(); + void applySidecarData(uint32_t mid, SidecarData data); + + ViewportWindow* viewport_ = nullptr; + std::map models_; + std::deque load_queue_; + uint32_t next_model_id_ = 1; + uint32_t next_object_id_ = 1; + uint32_t loading_model_id_ = 0; + std::thread sidecar_read_thread_; + QTimer element_poll_timer_; +}; + +#endif // SCENELOADER_H