mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 10:11:46 +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 "MainWindow.h"
|
||||||
#include "ViewerSettings.h"
|
#include "ViewerSettings.h"
|
||||||
#include "components/Style.h"
|
#include "components/Style.h"
|
||||||
|
#include "modules/models/Commands.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
@@ -55,6 +56,9 @@ int main(int argc, char* argv[]) {
|
|||||||
app.setApplicationName("Bonsai Viewer");
|
app.setApplicationName("Bonsai Viewer");
|
||||||
app.setOrganizationName("IfcOpenShell");
|
app.setOrganizationName("IfcOpenShell");
|
||||||
|
|
||||||
|
// Clear any .rdbview extractions left in temp by a previous session.
|
||||||
|
bonsaiviewer::modules::models::commands::cleanupRdbviewCache();
|
||||||
|
|
||||||
QSurfaceFormat fmt;
|
QSurfaceFormat fmt;
|
||||||
fmt.setVersion(4, 5);
|
fmt.setVersion(4, 5);
|
||||||
fmt.setProfile(QSurfaceFormat::CoreProfile);
|
fmt.setProfile(QSurfaceFormat::CoreProfile);
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ void AddModelDialog::setupUi() {
|
|||||||
{SourceMode::IfcDatabase, "Add IFC\nDatabase", ":/icons/database.svg",
|
{SourceMode::IfcDatabase, "Add IFC\nDatabase", ":/icons/database.svg",
|
||||||
"Add IFC RDB databases for optimised performance"},
|
"Add IFC RDB databases for optimised performance"},
|
||||||
{SourceMode::GeometryOnly, "Add Geometry", ":/icons/cube-bandage.svg",
|
{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 = {
|
const QList<Choice> cloud_choices = {
|
||||||
{SourceMode::CloudModel, "Add From\nCloud", ":/icons/cloud-square.svg",
|
{SourceMode::CloudModel, "Add From\nCloud", ":/icons/cloud-square.svg",
|
||||||
|
|||||||
@@ -56,6 +56,8 @@
|
|||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
#include <QtCore/private/qzipwriter_p.h>
|
#include <QtCore/private/qzipwriter_p.h>
|
||||||
|
#include <QtCore/private/qzipreader_p.h>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
|
||||||
#include <Eigen/Dense>
|
#include <Eigen/Dense>
|
||||||
|
|
||||||
@@ -89,8 +91,56 @@ QString formatElapsed(qint64 ms) {
|
|||||||
: QString::number(ms) + " 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
|
} // namespace
|
||||||
|
|
||||||
|
void cleanupRdbviewCache() {
|
||||||
|
QDir(rdbviewCacheRoot()).removeRecursively();
|
||||||
|
}
|
||||||
|
|
||||||
void toggleVisibility(SessionState& session, ItemKind kind, const QString& id) {
|
void toggleVisibility(SessionState& session, ItemKind kind, const QString& id) {
|
||||||
Federation* federation = session.federation();
|
Federation* federation = session.federation();
|
||||||
if (kind == ItemKind::Group) {
|
if (kind == ItemKind::Group) {
|
||||||
@@ -201,9 +251,28 @@ namespace detail {
|
|||||||
void loadModels(SessionState& session, const QStringList& paths, const QStringList& model_ids) {
|
void loadModels(SessionState& session, const QStringList& paths, const QStringList& model_ids) {
|
||||||
if (paths.isEmpty()) return;
|
if (paths.isEmpty()) return;
|
||||||
|
|
||||||
const auto session_model_ids = session.loader()->queueModels(paths);
|
// The Federation stores the source paths (e.g. a .rdbview); the loader gets
|
||||||
for (int i = 0; i < paths.size() && i < static_cast<int>(session_model_ids.size()) && i < model_ids.size(); ++i) {
|
// the resolved load path (a .rdbview is unzipped at load time to its .rdb).
|
||||||
session.setModelMapping(model_ids[i], session_model_ids[i]);
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
case SourceMode::GeometryOnly: {
|
case SourceMode::GeometryOnly: {
|
||||||
QFileDialog file_dialog(&host, "Add Geometry Only");
|
QFileDialog file_dialog(&host, "Add Geometry");
|
||||||
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
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);
|
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||||
if (file_dialog.exec() == QDialog::Accepted) {
|
if (file_dialog.exec() == QDialog::Accepted) {
|
||||||
paths = file_dialog.selectedFiles();
|
paths = file_dialog.selectedFiles();
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ void convertIfcToDatabase(SessionState& session, QWidget& host);
|
|||||||
void exportGeometryDatabase(SessionState& session, QWidget& host);
|
void exportGeometryDatabase(SessionState& session, QWidget& host);
|
||||||
void openSettings(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.
|
// Internal building blocks shared by commands here and by ProjectController.
|
||||||
// These NEVER call notify*() — the caller is responsible for emitting once
|
// These NEVER call notify*() — the caller is responsible for emitting once
|
||||||
// at the end of its execution.
|
// at the end of its execution.
|
||||||
|
|||||||
Reference in New Issue
Block a user