2026-05-09 22:00:19 +10:00
|
|
|
// This file was generated with the assistance of an AI coding tool.
|
|
|
|
|
/********************************************************************************
|
|
|
|
|
* *
|
|
|
|
|
* 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 <http://www.gnu.org/licenses/>. *
|
|
|
|
|
* *
|
|
|
|
|
********************************************************************************/
|
|
|
|
|
|
|
|
|
|
#include "Controller.h"
|
|
|
|
|
|
|
|
|
|
#include "SettingsDialog.h"
|
|
|
|
|
#include "Panel.h"
|
|
|
|
|
|
|
|
|
|
#include "../../ElementRegistry.h"
|
|
|
|
|
#include "../../SessionState.h"
|
|
|
|
|
#include "AddModelDialog.h"
|
|
|
|
|
#include "../../../ifcviewer/Federation.h"
|
2026-05-15 11:54:14 +10:00
|
|
|
#include "../../../ifcviewer/HeadlessSidecarBuilder.h"
|
2026-05-14 10:06:19 +10:00
|
|
|
#include "../../../ifcviewer/LodBuilder.h"
|
2026-05-09 22:00:19 +10:00
|
|
|
#include "../../../ifcviewer/SceneLoader.h"
|
2026-05-14 10:06:19 +10:00
|
|
|
#include "../../../ifcviewer/SidecarCache.h"
|
2026-05-09 22:00:19 +10:00
|
|
|
#include "../../../ifcviewer/ViewportWindow.h"
|
2026-05-15 08:43:48 +10:00
|
|
|
#include "../../../ifcgeom/Serializer.h"
|
|
|
|
|
#include "../../../serializers/document_serializer_plugin.h"
|
2026-05-09 22:00:19 +10:00
|
|
|
|
2026-05-15 08:43:48 +10:00
|
|
|
#include <QDebug>
|
2026-05-15 11:54:14 +10:00
|
|
|
#include <QDir>
|
|
|
|
|
#include <QDirIterator>
|
2026-05-14 10:06:19 +10:00
|
|
|
#include <QElapsedTimer>
|
2026-05-15 11:54:14 +10:00
|
|
|
#include <QFile>
|
2026-05-09 22:00:19 +10:00
|
|
|
#include <QFileDialog>
|
2026-05-15 08:43:48 +10:00
|
|
|
#include <QFileInfo>
|
2026-05-09 22:00:19 +10:00
|
|
|
#include <QListView>
|
|
|
|
|
#include <QMessageBox>
|
2026-05-15 08:43:48 +10:00
|
|
|
#include <QProgressDialog>
|
2026-05-15 11:54:14 +10:00
|
|
|
#include <QStandardPaths>
|
2026-05-15 08:43:48 +10:00
|
|
|
#include <QThread>
|
2026-05-09 22:00:19 +10:00
|
|
|
#include <QTreeView>
|
2026-05-15 11:54:14 +10:00
|
|
|
#include <QUuid>
|
|
|
|
|
|
|
|
|
|
#include <QtCore/private/qzipwriter_p.h>
|
2026-05-09 22:00:19 +10:00
|
|
|
|
2026-05-14 10:06:19 +10:00
|
|
|
#include <Eigen/Dense>
|
|
|
|
|
|
2026-05-15 08:43:48 +10:00
|
|
|
#include <memory>
|
|
|
|
|
|
2026-05-15 07:22:31 +10:00
|
|
|
namespace ifcviewerfull::modules::models {
|
2026-05-09 22:00:19 +10:00
|
|
|
|
|
|
|
|
ModelsPanelController::ModelsPanelController(QWidget* host,
|
|
|
|
|
ModelsPanel* widget,
|
2026-05-15 07:22:31 +10:00
|
|
|
ifcviewerfull::SessionState* session_state,
|
2026-05-09 22:00:19 +10:00
|
|
|
ViewportWindow* viewport,
|
|
|
|
|
QObject* parent)
|
|
|
|
|
: QObject(parent)
|
|
|
|
|
, host_(host)
|
|
|
|
|
, widget_(widget)
|
|
|
|
|
, session_state_(session_state)
|
|
|
|
|
, viewport_(viewport)
|
|
|
|
|
{
|
|
|
|
|
connect(widget_, &ModelsPanel::visibilityToggleRequested, this,
|
|
|
|
|
[this](ItemKind kind, const QString& id) {
|
|
|
|
|
Federation* federation = session_state_->federation();
|
|
|
|
|
if (kind == ItemKind::Group) {
|
|
|
|
|
if (const Federation::Group* group = federation->findGroupById(id)) {
|
|
|
|
|
federation->setGroupVisible(id, !group->visible);
|
|
|
|
|
session_state_->notifyVisibilityChanged();
|
|
|
|
|
session_state_->setStatusMessage("Models", group->visible ? "Group hidden" : "Group shown");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (const Federation::Model* model = federation->findById(id)) {
|
|
|
|
|
federation->setModelVisible(id, !model->visible);
|
|
|
|
|
session_state_->notifyVisibilityChanged();
|
|
|
|
|
session_state_->setStatusMessage("Models", model->visible ? "Model hidden" : "Model shown");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
connect(widget_, &ModelsPanel::addGroupRequested, this,
|
|
|
|
|
[this](const QString& parent_group_id, const QString& name) {
|
|
|
|
|
session_state_->federation()->addGroup(name, parent_group_id);
|
|
|
|
|
session_state_->notifyFederationStructureChanged();
|
|
|
|
|
session_state_->setStatusMessage("Models", "Group added");
|
|
|
|
|
});
|
2026-05-14 10:20:43 +10:00
|
|
|
connect(widget_, &ModelsPanel::renameGroupRequested, this,
|
|
|
|
|
[this](const QString& id, const QString& name) {
|
|
|
|
|
session_state_->federation()->setGroupName(id, name);
|
|
|
|
|
session_state_->notifyFederationStructureChanged();
|
|
|
|
|
session_state_->setStatusMessage("Models", "Group renamed");
|
|
|
|
|
});
|
|
|
|
|
connect(widget_, &ModelsPanel::moveGroupRequested, this,
|
|
|
|
|
[this](const QString& id, const QString& parent_group_id) {
|
|
|
|
|
session_state_->federation()->setGroupParent(id, parent_group_id);
|
|
|
|
|
session_state_->notifyFederationStructureChanged();
|
|
|
|
|
session_state_->setStatusMessage("Models", parent_group_id.isEmpty() ? "Group moved to root"
|
|
|
|
|
: "Group moved");
|
|
|
|
|
});
|
|
|
|
|
connect(widget_, &ModelsPanel::moveModelsRequested, this,
|
|
|
|
|
[this](const QStringList& ids, const QString& parent_group_id) {
|
|
|
|
|
for (const auto& id : ids) {
|
|
|
|
|
session_state_->federation()->setModelGroup(id, parent_group_id);
|
|
|
|
|
}
|
|
|
|
|
session_state_->notifyFederationStructureChanged();
|
|
|
|
|
session_state_->setStatusMessage("Models", parent_group_id.isEmpty() ? "Model(s) moved to root"
|
|
|
|
|
: "Model(s) moved");
|
|
|
|
|
});
|
2026-05-09 22:00:19 +10:00
|
|
|
connect(widget_, &ModelsPanel::removeGroupRequested, this,
|
|
|
|
|
[this](const QString& id) {
|
|
|
|
|
session_state_->federation()->removeGroup(id);
|
|
|
|
|
session_state_->notifyFederationStructureChanged();
|
|
|
|
|
session_state_->setStatusMessage("Models", "Group removed");
|
|
|
|
|
});
|
|
|
|
|
connect(widget_, &ModelsPanel::removeModelRequested, this,
|
|
|
|
|
[this](const QString& id) {
|
|
|
|
|
removeLoadedModel(id);
|
|
|
|
|
session_state_->setStatusMessage("Models", "Model removed");
|
|
|
|
|
});
|
|
|
|
|
connect(widget_, &components::Panel::settingsRequested, this, [this]() {
|
|
|
|
|
openSettings();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModelsPanelController::bindLoader(SceneLoader* loader) {
|
|
|
|
|
connect(loader, &SceneLoader::loadStarted, this,
|
|
|
|
|
[this](uint32_t /*mid*/, const QString& display_name) {
|
|
|
|
|
session_state_->setStatusMessage("Loading", display_name);
|
|
|
|
|
});
|
|
|
|
|
connect(loader, &SceneLoader::loadedFromSidecar, this,
|
|
|
|
|
[this, loader](uint32_t mid, qint64 elapsed_ms) {
|
|
|
|
|
session_state_->setStatusMessage(
|
|
|
|
|
"Loaded",
|
|
|
|
|
QString("%1 from cache in %2")
|
|
|
|
|
.arg(loader->displayName(mid))
|
|
|
|
|
.arg(formatElapsed(elapsed_ms)));
|
|
|
|
|
});
|
|
|
|
|
connect(loader, &SceneLoader::loadedFromStream, this,
|
|
|
|
|
[this, loader](uint32_t mid, qint64 elapsed_ms) {
|
2026-05-14 10:06:19 +10:00
|
|
|
writeSidecarForModel(loader, mid);
|
2026-05-09 22:00:19 +10:00
|
|
|
session_state_->setStatusMessage(
|
|
|
|
|
"Loaded",
|
|
|
|
|
QString("%1 streamed in %2")
|
|
|
|
|
.arg(loader->displayName(mid))
|
|
|
|
|
.arg(formatElapsed(elapsed_ms)));
|
|
|
|
|
});
|
|
|
|
|
connect(loader, &SceneLoader::loadCancelled, this,
|
|
|
|
|
[this, loader](uint32_t mid) {
|
|
|
|
|
session_state_->setStatusMessage("Cancelled", loader->displayName(mid));
|
|
|
|
|
});
|
|
|
|
|
connect(loader, &SceneLoader::loadError, this,
|
|
|
|
|
[this, host = host_](uint32_t /*mid*/, const QString& message) {
|
|
|
|
|
session_state_->setStatusMessage("Error", message);
|
2026-05-15 07:18:26 +10:00
|
|
|
QMessageBox::warning(host, "IfcViewer", message);
|
2026-05-09 22:00:19 +10:00
|
|
|
});
|
|
|
|
|
connect(loader, &SceneLoader::allLoadsFinished, this,
|
|
|
|
|
[this, loader]() {
|
|
|
|
|
session_state_->setStatusMessage("Loaded", QString("%1 model(s)").arg(loader->modelCount()));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModelsPanelController::addFiles() {
|
|
|
|
|
modules::models::AddModelDialog dialog(host_);
|
|
|
|
|
if (dialog.exec() != QDialog::Accepted) return;
|
|
|
|
|
|
|
|
|
|
QStringList paths;
|
|
|
|
|
switch (dialog.selectedMode()) {
|
|
|
|
|
case modules::models::SourceMode::IfcFile: {
|
|
|
|
|
QFileDialog file_dialog(host_, "Add IFC Files");
|
|
|
|
|
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
|
|
|
|
file_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)");
|
|
|
|
|
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
|
|
|
|
if (file_dialog.exec() == QDialog::Accepted) {
|
|
|
|
|
paths = file_dialog.selectedFiles();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case modules::models::SourceMode::IfcDatabase: {
|
|
|
|
|
QFileDialog database_dialog(host_, "Add IFC Databases");
|
|
|
|
|
database_dialog.setFileMode(QFileDialog::Directory);
|
|
|
|
|
database_dialog.setOption(QFileDialog::ShowDirsOnly, true);
|
|
|
|
|
database_dialog.setOption(QFileDialog::DontResolveSymlinks, true);
|
|
|
|
|
database_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
|
|
|
|
if (auto* list = database_dialog.findChild<QListView*>("listView")) {
|
|
|
|
|
list->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
|
|
|
|
}
|
|
|
|
|
if (auto* tree = database_dialog.findChild<QTreeView*>()) {
|
|
|
|
|
tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
|
|
|
|
}
|
|
|
|
|
if (database_dialog.exec() == QDialog::Accepted) {
|
|
|
|
|
paths = database_dialog.selectedFiles();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case modules::models::SourceMode::GeometryOnly: {
|
|
|
|
|
QFileDialog file_dialog(host_, "Add Geometry Only");
|
|
|
|
|
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
|
|
|
|
file_dialog.setNameFilter("IFC Viewer Cache (*.ifcview);;All Files (*)");
|
|
|
|
|
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
|
|
|
|
if (file_dialog.exec() == QDialog::Accepted) {
|
|
|
|
|
paths = file_dialog.selectedFiles();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-05-15 08:43:48 +10:00
|
|
|
case modules::models::SourceMode::ConvertToDatabase:
|
|
|
|
|
convertIfcToDatabase();
|
|
|
|
|
return;
|
2026-05-15 11:54:14 +10:00
|
|
|
case modules::models::SourceMode::ExportGeometryDatabase:
|
|
|
|
|
exportGeometryDatabase();
|
|
|
|
|
return;
|
2026-05-09 22:00:19 +10:00
|
|
|
case modules::models::SourceMode::None:
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addFiles(paths);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 08:43:48 +10:00
|
|
|
void ModelsPanelController::convertIfcToDatabase() {
|
|
|
|
|
QFileDialog input_dialog(host_, "Select IFC File to Convert");
|
|
|
|
|
input_dialog.setFileMode(QFileDialog::ExistingFile);
|
|
|
|
|
input_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)");
|
|
|
|
|
input_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
|
|
|
|
if (input_dialog.exec() != QDialog::Accepted) return;
|
|
|
|
|
const QStringList inputs = input_dialog.selectedFiles();
|
|
|
|
|
if (inputs.isEmpty()) return;
|
|
|
|
|
const QString input_path = inputs.first();
|
|
|
|
|
|
|
|
|
|
const QFileInfo input_info(input_path);
|
|
|
|
|
const QString default_output = input_info.absoluteDir().filePath(input_info.completeBaseName() + ".rdb");
|
|
|
|
|
|
|
|
|
|
QFileDialog output_dialog(host_, "Save IFC Database As");
|
|
|
|
|
output_dialog.setAcceptMode(QFileDialog::AcceptSave);
|
|
|
|
|
output_dialog.setFileMode(QFileDialog::AnyFile);
|
|
|
|
|
output_dialog.setNameFilter("IFC Database (*.rdb);;All Files (*)");
|
|
|
|
|
output_dialog.setDefaultSuffix("rdb");
|
|
|
|
|
output_dialog.selectFile(default_output);
|
|
|
|
|
output_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
|
|
|
|
output_dialog.setOption(QFileDialog::DontConfirmOverwrite, true);
|
|
|
|
|
if (output_dialog.exec() != QDialog::Accepted) return;
|
|
|
|
|
const QStringList outputs = output_dialog.selectedFiles();
|
|
|
|
|
if (outputs.isEmpty()) return;
|
|
|
|
|
QString output_path = outputs.first();
|
|
|
|
|
if (!output_path.endsWith(".rdb", Qt::CaseInsensitive)) {
|
|
|
|
|
output_path += ".rdb";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QFileInfo output_info(output_path);
|
|
|
|
|
if (output_info.exists()) {
|
|
|
|
|
const QString message = QString("'%1' already exists. Overwrite?").arg(output_info.fileName());
|
|
|
|
|
if (QMessageBox::question(host_, "Convert IFC to Database", message,
|
|
|
|
|
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runIfcToDatabaseConversion(input_path, output_path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModelsPanelController::runIfcToDatabaseConversion(const QString& input_path, const QString& output_path) {
|
|
|
|
|
auto* progress = new QProgressDialog(host_);
|
|
|
|
|
progress->setWindowTitle("Convert IFC to Database");
|
|
|
|
|
progress->setLabelText(QString("Converting %1 to %2…")
|
|
|
|
|
.arg(QFileInfo(input_path).fileName(),
|
|
|
|
|
QFileInfo(output_path).fileName()));
|
|
|
|
|
progress->setRange(0, 0);
|
|
|
|
|
progress->setCancelButton(nullptr);
|
|
|
|
|
progress->setMinimumDuration(0);
|
|
|
|
|
progress->setWindowModality(Qt::ApplicationModal);
|
|
|
|
|
progress->setAutoClose(false);
|
|
|
|
|
progress->setAutoReset(false);
|
|
|
|
|
progress->show();
|
|
|
|
|
|
|
|
|
|
session_state_->setStatusMessage("Converting",
|
|
|
|
|
QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName()));
|
|
|
|
|
|
|
|
|
|
auto timer = std::make_shared<QElapsedTimer>();
|
|
|
|
|
timer->start();
|
|
|
|
|
auto error_message = std::make_shared<QString>();
|
|
|
|
|
|
|
|
|
|
QThread* thread = QThread::create([input_path, output_path, error_message]() {
|
|
|
|
|
try {
|
|
|
|
|
ifcopenshell::serializers::document_serializer_context context;
|
|
|
|
|
context.file = nullptr;
|
|
|
|
|
context.input_filename = input_path.toStdString();
|
|
|
|
|
context.output_filename = output_path.toStdString();
|
|
|
|
|
context.stream = true;
|
|
|
|
|
|
|
|
|
|
auto& registry = ifcopenshell::serializers::document_serializer_registry_instance();
|
|
|
|
|
const auto* info = registry.find("rdb");
|
|
|
|
|
if (!info) {
|
|
|
|
|
throw ifcopenshell::exception(
|
|
|
|
|
"No 'rdb' document serializer is registered. The RocksDB serializer plugin may not be installed.");
|
|
|
|
|
}
|
|
|
|
|
if (!info->supports_input_filename) {
|
|
|
|
|
throw ifcopenshell::exception("RDB serializer does not support streaming from an input filename");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boost::shared_ptr<Serializer> serializer = registry.create("rdb", context);
|
|
|
|
|
serializer->finalize();
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
*error_message = QString::fromUtf8(e.what());
|
|
|
|
|
} catch (...) {
|
|
|
|
|
*error_message = "Unknown error during IFC to RDB conversion";
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
connect(thread, &QThread::finished, this,
|
|
|
|
|
[this, thread, progress, timer, error_message, input_path, output_path]() {
|
|
|
|
|
const qint64 elapsed = timer->elapsed();
|
|
|
|
|
|
|
|
|
|
progress->close();
|
|
|
|
|
progress->deleteLater();
|
|
|
|
|
thread->deleteLater();
|
|
|
|
|
|
|
|
|
|
if (!error_message->isEmpty()) {
|
|
|
|
|
session_state_->setStatusMessage("Error", *error_message);
|
|
|
|
|
QMessageBox::warning(host_, "Convert IFC to Database",
|
|
|
|
|
QString("Conversion failed:\n%1").arg(*error_message));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
session_state_->setStatusMessage(
|
|
|
|
|
"Converted",
|
|
|
|
|
QString("%1 → %2 in %3")
|
|
|
|
|
.arg(QFileInfo(input_path).fileName(),
|
|
|
|
|
QFileInfo(output_path).fileName(),
|
|
|
|
|
formatElapsed(elapsed)));
|
|
|
|
|
QMessageBox::information(host_, "Convert IFC to Database",
|
|
|
|
|
QString("Database written to:\n%1").arg(output_path));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
thread->start();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 11:54:14 +10:00
|
|
|
void ModelsPanelController::exportGeometryDatabase() {
|
|
|
|
|
QFileDialog input_dialog(host_, "Select IFC File to Export");
|
|
|
|
|
input_dialog.setFileMode(QFileDialog::ExistingFile);
|
|
|
|
|
input_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)");
|
|
|
|
|
input_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
|
|
|
|
if (input_dialog.exec() != QDialog::Accepted) return;
|
|
|
|
|
const QStringList inputs = input_dialog.selectedFiles();
|
|
|
|
|
if (inputs.isEmpty()) return;
|
|
|
|
|
const QString input_path = inputs.first();
|
|
|
|
|
|
|
|
|
|
const QFileInfo input_info(input_path);
|
|
|
|
|
const QString default_output = input_info.absoluteDir().filePath(input_info.completeBaseName() + ".rdbview");
|
|
|
|
|
|
|
|
|
|
QFileDialog output_dialog(host_, "Save Geometry Database As");
|
|
|
|
|
output_dialog.setAcceptMode(QFileDialog::AcceptSave);
|
|
|
|
|
output_dialog.setFileMode(QFileDialog::AnyFile);
|
|
|
|
|
output_dialog.setNameFilter("Geometry Database (*.rdbview);;All Files (*)");
|
|
|
|
|
output_dialog.setDefaultSuffix("rdbview");
|
|
|
|
|
output_dialog.selectFile(default_output);
|
|
|
|
|
output_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
|
|
|
|
if (output_dialog.exec() != QDialog::Accepted) return;
|
|
|
|
|
const QStringList outputs = output_dialog.selectedFiles();
|
|
|
|
|
if (outputs.isEmpty()) return;
|
|
|
|
|
QString output_path = outputs.first();
|
|
|
|
|
if (!output_path.endsWith(".rdbview", Qt::CaseInsensitive)) {
|
|
|
|
|
output_path += ".rdbview";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runGeometryDatabaseExport(input_path, output_path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path, const QString& output_path) {
|
|
|
|
|
auto* progress = new QProgressDialog(host_);
|
|
|
|
|
progress->setWindowTitle("Export Geometry Database");
|
|
|
|
|
progress->setLabelText(QString("Exporting %1 to %2…")
|
|
|
|
|
.arg(QFileInfo(input_path).fileName(),
|
|
|
|
|
QFileInfo(output_path).fileName()));
|
|
|
|
|
progress->setRange(0, 0);
|
|
|
|
|
progress->setCancelButton(nullptr);
|
|
|
|
|
progress->setMinimumDuration(0);
|
|
|
|
|
progress->setWindowModality(Qt::ApplicationModal);
|
|
|
|
|
progress->setAutoClose(false);
|
|
|
|
|
progress->setAutoReset(false);
|
|
|
|
|
progress->show();
|
|
|
|
|
|
|
|
|
|
session_state_->setStatusMessage("Exporting",
|
|
|
|
|
QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName()));
|
|
|
|
|
|
|
|
|
|
auto timer = std::make_shared<QElapsedTimer>();
|
|
|
|
|
timer->start();
|
|
|
|
|
auto error_message = std::make_shared<QString>();
|
|
|
|
|
|
|
|
|
|
QThread* thread = QThread::create([input_path, output_path, error_message]() {
|
|
|
|
|
// Scratch dir holds the intermediate .ifcview and .rdb directory
|
|
|
|
|
// until they're zipped into the .rdbview. RAII-like cleanup at the
|
|
|
|
|
// bottom of this lambda; on early exception we leak it (cheap
|
|
|
|
|
// tradeoff to keep the failure log around for the user).
|
|
|
|
|
const QString tmp_root = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation))
|
|
|
|
|
.filePath(QString("ifcviewer-export-%1")
|
|
|
|
|
.arg(QUuid::createUuid().toString(QUuid::Id128)));
|
|
|
|
|
QDir().mkpath(tmp_root);
|
|
|
|
|
|
|
|
|
|
const QString tmp_anchor = QDir(tmp_root).filePath("model.ifc");
|
|
|
|
|
const QString tmp_sidecar = QDir(tmp_root).filePath("model.ifcview");
|
|
|
|
|
const QString tmp_rdb_dir = QDir(tmp_root).filePath("model.rdb");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Step 1: lossy RDB with IfcRepresentationItem stripped.
|
|
|
|
|
ifcopenshell::serializers::document_serializer_context context;
|
|
|
|
|
context.file = nullptr;
|
|
|
|
|
context.input_filename = input_path.toStdString();
|
|
|
|
|
context.output_filename = tmp_rdb_dir.toStdString();
|
|
|
|
|
context.stream = true;
|
|
|
|
|
context.skip_supertypes = { "IfcRepresentationItem" };
|
|
|
|
|
|
|
|
|
|
auto& registry = ifcopenshell::serializers::document_serializer_registry_instance();
|
|
|
|
|
const auto* info = registry.find("rdb");
|
|
|
|
|
if (!info) {
|
|
|
|
|
throw ifcopenshell::exception(
|
|
|
|
|
"No 'rdb' document serializer is registered. The RocksDB serializer plugin may not be installed.");
|
|
|
|
|
}
|
|
|
|
|
if (!info->supports_input_filename) {
|
|
|
|
|
throw ifcopenshell::exception("RDB serializer does not support streaming from an input filename");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boost::shared_ptr<Serializer> serializer = registry.create("rdb", context);
|
|
|
|
|
serializer->finalize();
|
|
|
|
|
serializer.reset();
|
|
|
|
|
|
|
|
|
|
// Step 2: .ifcview sidecar via the headless builder.
|
|
|
|
|
HeadlessSidecarBuilder builder;
|
|
|
|
|
if (!builder.build(input_path, tmp_anchor)) {
|
|
|
|
|
throw ifcopenshell::exception(
|
|
|
|
|
("Sidecar build failed: " + builder.lastError()).toStdString());
|
|
|
|
|
}
|
|
|
|
|
if (!QFileInfo::exists(tmp_sidecar)) {
|
|
|
|
|
throw ifcopenshell::exception(
|
|
|
|
|
("Sidecar build reported success but " + tmp_sidecar + " is missing").toStdString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 3: zip the sidecar + RDB directory into the .rdbview.
|
|
|
|
|
// Write to a sibling `.tmp` then rename so a partial file never
|
|
|
|
|
// appears at the destination (matters for cloud-sync folders).
|
|
|
|
|
const QString tmp_zip = output_path + ".tmp";
|
|
|
|
|
QFile::remove(tmp_zip);
|
|
|
|
|
{
|
|
|
|
|
QZipWriter writer(tmp_zip);
|
|
|
|
|
if (writer.status() != QZipWriter::NoError) {
|
|
|
|
|
throw ifcopenshell::exception(
|
|
|
|
|
("Failed to open " + tmp_zip + " for writing").toStdString());
|
|
|
|
|
}
|
|
|
|
|
writer.setCompressionPolicy(QZipWriter::AutoCompress);
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
QFile sf(tmp_sidecar);
|
|
|
|
|
if (!sf.open(QIODevice::ReadOnly)) {
|
|
|
|
|
throw ifcopenshell::exception(
|
|
|
|
|
("Failed to read sidecar " + tmp_sidecar).toStdString());
|
|
|
|
|
}
|
|
|
|
|
writer.addFile("model.ifcview", sf.readAll());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QDirIterator it(tmp_rdb_dir, QDir::Files | QDir::NoDotAndDotDot,
|
|
|
|
|
QDirIterator::Subdirectories);
|
|
|
|
|
const QDir rdb_root(tmp_rdb_dir);
|
|
|
|
|
while (it.hasNext()) {
|
|
|
|
|
const QString file_path = it.next();
|
|
|
|
|
const QString rel = rdb_root.relativeFilePath(file_path);
|
|
|
|
|
QFile f(file_path);
|
|
|
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
|
|
|
throw ifcopenshell::exception(
|
|
|
|
|
("Failed to read " + file_path + " for zip").toStdString());
|
|
|
|
|
}
|
|
|
|
|
writer.addFile(QString("model.rdb/%1").arg(rel), f.readAll());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writer.close();
|
|
|
|
|
if (writer.status() != QZipWriter::NoError) {
|
|
|
|
|
throw ifcopenshell::exception(
|
|
|
|
|
("Failed to finalize " + tmp_zip).toStdString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QFile::remove(output_path);
|
|
|
|
|
if (!QFile::rename(tmp_zip, output_path)) {
|
|
|
|
|
QFile::remove(tmp_zip);
|
|
|
|
|
throw ifcopenshell::exception(
|
|
|
|
|
("Failed to move " + tmp_zip + " to " + output_path).toStdString());
|
|
|
|
|
}
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
*error_message = QString::fromUtf8(e.what());
|
|
|
|
|
} catch (...) {
|
|
|
|
|
*error_message = "Unknown error during geometry database export";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QDir(tmp_root).removeRecursively();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
connect(thread, &QThread::finished, this,
|
|
|
|
|
[this, thread, progress, timer, error_message, input_path, output_path]() {
|
|
|
|
|
const qint64 elapsed = timer->elapsed();
|
|
|
|
|
|
|
|
|
|
progress->close();
|
|
|
|
|
progress->deleteLater();
|
|
|
|
|
thread->deleteLater();
|
|
|
|
|
|
|
|
|
|
if (!error_message->isEmpty()) {
|
|
|
|
|
session_state_->setStatusMessage("Error", *error_message);
|
|
|
|
|
QMessageBox::warning(host_, "Export Geometry Database",
|
|
|
|
|
QString("Export failed:\n%1").arg(*error_message));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
session_state_->setStatusMessage(
|
|
|
|
|
"Exported",
|
|
|
|
|
QString("%1 → %2 in %3")
|
|
|
|
|
.arg(QFileInfo(input_path).fileName(),
|
|
|
|
|
QFileInfo(output_path).fileName(),
|
|
|
|
|
formatElapsed(elapsed)));
|
|
|
|
|
QMessageBox::information(host_, "Export Geometry Database",
|
|
|
|
|
QString("Geometry database written to:\n%1").arg(output_path));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
thread->start();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 22:00:19 +10:00
|
|
|
void ModelsPanelController::addFiles(const QStringList& paths) {
|
|
|
|
|
QStringList accepted_paths;
|
|
|
|
|
QStringList accepted_fed_ids;
|
|
|
|
|
for (const auto& path : paths) {
|
|
|
|
|
const QString fed_id = session_state_->federation()->addModel(path);
|
|
|
|
|
if (fed_id.isEmpty()) continue;
|
|
|
|
|
accepted_paths << path;
|
|
|
|
|
accepted_fed_ids << fed_id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadModels(accepted_paths, accepted_fed_ids);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModelsPanelController::loadModels(const QStringList& paths, const QStringList& fed_ids) {
|
|
|
|
|
if (paths.isEmpty()) return;
|
|
|
|
|
|
|
|
|
|
const auto ids = session_state_->loader()->addFiles(paths);
|
|
|
|
|
for (int i = 0; i < paths.size() && i < static_cast<int>(ids.size()) && i < fed_ids.size(); ++i) {
|
|
|
|
|
session_state_->setModelMapping(fed_ids[i], ids[i]);
|
|
|
|
|
}
|
|
|
|
|
session_state_->notifyModelsChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModelsPanelController::removeLoadedModel(const QString& fed_id) {
|
|
|
|
|
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
|
|
|
|
if (mid == 0) {
|
|
|
|
|
session_state_->federation()->removeModel(fed_id);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (session_state_->loader()->isLoadingModel(mid)) return;
|
|
|
|
|
|
|
|
|
|
viewport_->setSelectedObjectId(0);
|
|
|
|
|
session_state_->setSelectedObjectId(0);
|
|
|
|
|
session_state_->federation()->removeModel(fed_id);
|
|
|
|
|
viewport_->removeModel(mid);
|
|
|
|
|
session_state_->loader()->removeModel(mid);
|
|
|
|
|
session_state_->elementRegistry()->removeModel(mid);
|
|
|
|
|
session_state_->removeModelMappingByFedId(fed_id);
|
|
|
|
|
session_state_->notifySelectionChanged();
|
|
|
|
|
session_state_->notifyModelsChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ModelsPanelController::openSettings() {
|
2026-05-10 22:04:03 +10:00
|
|
|
SettingsDialog dialog(session_state_, host_);
|
2026-05-09 22:00:19 +10:00
|
|
|
dialog.exec();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString ModelsPanelController::formatElapsed(qint64 ms) const {
|
|
|
|
|
return (ms >= 1000)
|
|
|
|
|
? QString::number(ms / 1000.0, 'f', 2) + " s"
|
|
|
|
|
: QString::number(ms) + " ms";
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 10:06:19 +10:00
|
|
|
void ModelsPanelController::writeSidecarForModel(SceneLoader* loader, uint32_t mid) const {
|
|
|
|
|
if (!loader || !viewport_ || !session_state_) return;
|
|
|
|
|
|
|
|
|
|
SidecarData sidecar_data;
|
|
|
|
|
if (!viewport_->snapshotModel(mid, sidecar_data)) return;
|
|
|
|
|
|
|
|
|
|
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
|
|
|
|
sidecar_data.has_coordinate_operation = georef->has_coordinate_operation ? 1 : 0;
|
|
|
|
|
Eigen::Map<Eigen::Matrix<double, 4, 4, Eigen::ColMajor>>(
|
|
|
|
|
sidecar_data.coordinate_operation_meters) = georef->coordinate_operation_meters;
|
|
|
|
|
sidecar_data.project_length_to_meters = georef->units.project_length_to_meters;
|
|
|
|
|
sidecar_data.map_unit_to_meters = georef->units.map_unit_to_meters;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto* element_registry = session_state_->elementRegistry();
|
|
|
|
|
if (element_registry) {
|
|
|
|
|
for (const auto& info : element_registry->basicElementInfoForModel(mid)) {
|
|
|
|
|
PackedElementInfo packed;
|
|
|
|
|
packed.object_id = info.object_id;
|
|
|
|
|
packed.model_id = info.model_id;
|
|
|
|
|
packed.ifc_id = info.ifc_id;
|
|
|
|
|
packed.parent_id = info.parent_id;
|
|
|
|
|
|
|
|
|
|
const std::string guid = info.guid.toStdString();
|
|
|
|
|
packed.guid_offset = static_cast<uint32_t>(sidecar_data.string_table.size());
|
|
|
|
|
packed.guid_length = static_cast<uint32_t>(guid.size());
|
|
|
|
|
sidecar_data.string_table += guid;
|
|
|
|
|
|
|
|
|
|
const std::string name = info.name.toStdString();
|
|
|
|
|
packed.name_offset = static_cast<uint32_t>(sidecar_data.string_table.size());
|
|
|
|
|
packed.name_length = static_cast<uint32_t>(name.size());
|
|
|
|
|
sidecar_data.string_table += name;
|
|
|
|
|
|
|
|
|
|
const std::string type = info.type.toStdString();
|
|
|
|
|
packed.type_offset = static_cast<uint32_t>(sidecar_data.string_table.size());
|
|
|
|
|
packed.type_length = static_cast<uint32_t>(type.size());
|
|
|
|
|
sidecar_data.string_table += type;
|
|
|
|
|
|
|
|
|
|
sidecar_data.elements.push_back(packed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QElapsedTimer lod_timer;
|
|
|
|
|
lod_timer.start();
|
|
|
|
|
buildLods(sidecar_data);
|
|
|
|
|
const LodStats lod_stats = summariseLods(sidecar_data);
|
|
|
|
|
qDebug(" LOD build: %lld ms — %u/%u meshes got LOD1 "
|
|
|
|
|
"(%u tris -> %u tris for those meshes)",
|
|
|
|
|
lod_timer.elapsed(),
|
|
|
|
|
lod_stats.meshes_with_lod1, lod_stats.meshes_total,
|
|
|
|
|
lod_stats.tris_lod0_for_lod1, lod_stats.tris_lod1);
|
|
|
|
|
|
|
|
|
|
viewport_->applyLodExtension(mid, sidecar_data);
|
|
|
|
|
|
|
|
|
|
QElapsedTimer sidecar_timer;
|
|
|
|
|
sidecar_timer.start();
|
|
|
|
|
const bool ok = writeSidecar(loader->filePath(mid).toStdString(), sidecar_data);
|
|
|
|
|
qDebug(" Sidecar write: %lld ms (%s)", sidecar_timer.elapsed(), ok ? "ok" : "FAILED");
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 07:22:31 +10:00
|
|
|
} // namespace ifcviewerfull::modules::models
|