mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 01:31:00 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -19,17 +19,10 @@
|
||||
|
||||
#include "MinimalWindow.h"
|
||||
#include "AppSettings.h"
|
||||
#include "SidecarCache.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFileInfo>
|
||||
#include <QStatusBar>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
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<uint64_t>(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::optional<SidecarData>>(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;
|
||||
}
|
||||
|
||||
@@ -22,62 +22,36 @@
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QLabel>
|
||||
#include <QTimer>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
#include <map>
|
||||
#include <deque>
|
||||
#include <thread>
|
||||
|
||||
#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<uint32_t, ModelEntry> models_;
|
||||
std::deque<uint32_t> 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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user