diff --git a/src/bonsaiviewer/main.cpp b/src/bonsaiviewer/main.cpp index a77fe2896a..612167a6ee 100644 --- a/src/bonsaiviewer/main.cpp +++ b/src/bonsaiviewer/main.cpp @@ -21,6 +21,7 @@ #include "MainWindow.h" #include "ViewerSettings.h" #include "components/Style.h" +#include "modules/models/Commands.h" #include #include @@ -55,6 +56,9 @@ int main(int argc, char* argv[]) { app.setApplicationName("Bonsai Viewer"); app.setOrganizationName("IfcOpenShell"); + // Clear any .rdbview extractions left in temp by a previous session. + bonsaiviewer::modules::models::commands::cleanupRdbviewCache(); + QSurfaceFormat fmt; fmt.setVersion(4, 5); fmt.setProfile(QSurfaceFormat::CoreProfile); diff --git a/src/bonsaiviewer/modules/models/AddModelDialog.cpp b/src/bonsaiviewer/modules/models/AddModelDialog.cpp index 98df960344..20372ac7ac 100644 --- a/src/bonsaiviewer/modules/models/AddModelDialog.cpp +++ b/src/bonsaiviewer/modules/models/AddModelDialog.cpp @@ -100,7 +100,7 @@ void AddModelDialog::setupUi() { {SourceMode::IfcDatabase, "Add IFC\nDatabase", ":/icons/database.svg", "Add IFC RDB databases for optimised performance"}, {SourceMode::GeometryOnly, "Add Geometry", ":/icons/cube-bandage.svg", - "Add pure geometry for fast visualisation"}, + "Add a viewer cache (.ifcview) or geometry database (.rdbview) for fast visualisation"}, }; const QList cloud_choices = { {SourceMode::CloudModel, "Add From\nCloud", ":/icons/cloud-square.svg", diff --git a/src/bonsaiviewer/modules/models/Commands.cpp b/src/bonsaiviewer/modules/models/Commands.cpp index c098c77eb5..4ae98e4176 100644 --- a/src/bonsaiviewer/modules/models/Commands.cpp +++ b/src/bonsaiviewer/modules/models/Commands.cpp @@ -56,6 +56,8 @@ #include #include +#include +#include #include @@ -89,8 +91,56 @@ QString formatElapsed(qint64 ms) { : QString::number(ms) + " ms"; } +// Session-scoped scratch root where .rdbview bundles are unzipped for loading. +QString rdbviewCacheRoot() { + return QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)) + .filePath("ifcviewer-rdbview"); +} + +// A .rdbview is a zip of `model.rdb/` (the data DB) + `model.ifcview` (geometry +// sidecar). Unzip it into a per-source subdir (hashed from path + mtime + size, +// so a re-open reuses an existing extraction) and return the extracted +// `model.rdb` path — from there it loads exactly like any pure .rdb (geometry +// from the co-extracted sibling .ifcview). Returns empty on failure. +QString extractRdbview(const QString& rdbview_path) { + const QFileInfo info(rdbview_path); + const QString key = rdbview_path + '|' + + QString::number(info.lastModified().toMSecsSinceEpoch()) + '|' + + QString::number(info.size()); + const QString hash = QString::fromLatin1( + QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Sha1).toHex()); + const QString dir = QDir(rdbviewCacheRoot()).filePath(hash); + const QString rdb = QDir(dir).filePath("model.rdb"); + + if (QFileInfo::exists(rdb)) return rdb; // already extracted this session + + QZipReader reader(rdbview_path); + if (reader.status() != QZipReader::NoError) return {}; + QDir().mkpath(dir); + for (const QZipReader::FileInfo& entry : reader.fileInfoList()) { + if (!entry.isFile) continue; // dirs recreated below as needed + const QString out = QDir(dir).filePath(entry.filePath); + QDir().mkpath(QFileInfo(out).absolutePath()); + QFile f(out); + if (!f.open(QIODevice::WriteOnly)) return {}; + f.write(reader.fileData(entry.filePath)); + } + return QFileInfo::exists(rdb) ? rdb : QString(); // empty if the bundle lacked model.rdb +} + +// Map a source path to the path the loader should open: a .rdbview is unzipped +// to its extracted .rdb; everything else passes through unchanged. +QString resolveLoadPath(const QString& path) { + if (path.endsWith(".rdbview", Qt::CaseInsensitive)) return extractRdbview(path); + return path; +} + } // namespace +void cleanupRdbviewCache() { + QDir(rdbviewCacheRoot()).removeRecursively(); +} + void toggleVisibility(SessionState& session, ItemKind kind, const QString& id) { Federation* federation = session.federation(); if (kind == ItemKind::Group) { @@ -201,9 +251,28 @@ namespace detail { void loadModels(SessionState& session, const QStringList& paths, const QStringList& model_ids) { if (paths.isEmpty()) return; - const auto session_model_ids = session.loader()->queueModels(paths); - for (int i = 0; i < paths.size() && i < static_cast(session_model_ids.size()) && i < model_ids.size(); ++i) { - session.setModelMapping(model_ids[i], session_model_ids[i]); + // The Federation stores the source paths (e.g. a .rdbview); the loader gets + // the resolved load path (a .rdbview is unzipped at load time to its .rdb). + // Keep model_ids aligned with the paths that actually resolve. + QStringList load_paths; + QStringList load_model_ids; + for (int i = 0; i < paths.size(); ++i) { + const QString resolved = resolveLoadPath(paths[i]); + if (resolved.isEmpty()) { + session.setStatusMessage("Error", + QString("Could not open %1").arg(QFileInfo(paths[i]).fileName())); + continue; + } + load_paths.push_back(resolved); + load_model_ids.push_back(i < model_ids.size() ? model_ids[i] : QString()); + } + if (load_paths.isEmpty()) return; + + const auto session_model_ids = session.loader()->queueModels(load_paths); + for (int i = 0; i < load_paths.size() + && i < static_cast(session_model_ids.size()) + && i < load_model_ids.size(); ++i) { + session.setModelMapping(load_model_ids[i], session_model_ids[i]); } } @@ -243,9 +312,11 @@ void addModel(SessionState& session, QWidget& host) { break; } case SourceMode::GeometryOnly: { - QFileDialog file_dialog(&host, "Add Geometry Only"); + QFileDialog file_dialog(&host, "Add Geometry"); file_dialog.setFileMode(QFileDialog::ExistingFiles); - file_dialog.setNameFilter("IFC Viewer Cache (*.ifcview);;All Files (*)"); + file_dialog.setNameFilter( + "Viewer Model (*.ifcview *.rdbview);;IFC Viewer Cache (*.ifcview);;" + "Geometry Database (*.rdbview);;All Files (*)"); file_dialog.setOption(QFileDialog::DontUseNativeDialog, true); if (file_dialog.exec() == QDialog::Accepted) { paths = file_dialog.selectedFiles(); diff --git a/src/bonsaiviewer/modules/models/Commands.h b/src/bonsaiviewer/modules/models/Commands.h index 12233598fc..78672e713e 100644 --- a/src/bonsaiviewer/modules/models/Commands.h +++ b/src/bonsaiviewer/modules/models/Commands.h @@ -74,6 +74,10 @@ void convertIfcToDatabase(SessionState& session, QWidget& host); void exportGeometryDatabase(SessionState& session, QWidget& host); void openSettings(SessionState& session, QWidget& host); +// Remove the scratch dir used to unzip .rdbview bundles for loading. Call once +// at startup to clear extractions left over from previous sessions. +void cleanupRdbviewCache(); + // Internal building blocks shared by commands here and by ProjectController. // These NEVER call notify*() — the caller is responsible for emitting once // at the end of its execution.