mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
models: consume .rdbview bundles (extract at load time)
A .rdbview is a zip of model.rdb/ (the lossy IFC data DB — the rdb serializer skips IfcRepresentationItem) + model.ifcview (baked geometry). The viewer could produce them but not open them. - extractRdbview(): unzip a .rdbview (QZipReader) into a session temp dir keyed by a hash of path+mtime+size (reused on re-open), returning the extracted model.rdb. The producer's layout means sidecarPath(model.rdb) resolves the sibling model.ifcview automatically, so it then loads exactly like any pure .rdb: geometry from the sidecar, data from the .rdb via ifcopenshell::file(FT_AUTODETECT). No SceneLoader/engine changes. - detail::loadModels() resolves each source path through it before queueModels (both fresh-open and project reload go through here), so the Federation persists the .rdbview while the loader gets the extracted .rdb. - cleanupRdbviewCache() clears stale extractions at startup. - .rdbview is offered under "Add Geometry" (the file picker; "Add IFC Database" is a directory picker), not "Add IFC File" — it's a lossy viewer bundle, not a source IFC. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
#include "MainWindow.h"
|
||||
#include "ViewerSettings.h"
|
||||
#include "components/Style.h"
|
||||
#include "modules/models/Commands.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Choice> cloud_choices = {
|
||||
{SourceMode::CloudModel, "Add From\nCloud", ":/icons/cloud-square.svg",
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
#include <QUuid>
|
||||
|
||||
#include <QtCore/private/qzipwriter_p.h>
|
||||
#include <QtCore/private/qzipreader_p.h>
|
||||
#include <QCryptographicHash>
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
@@ -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<int>(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<int>(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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user