mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-18 11:20:21 +00:00
Interface mockup 18
This commit is contained in:
+184
-214
@@ -18,14 +18,13 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Controller.h"
|
||||
#include "Commands.h"
|
||||
|
||||
#include "AddModelDialog.h"
|
||||
#include "SettingsDialog.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "AddModelDialog.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/HeadlessSidecarBuilder.h"
|
||||
#include "../../../ifcviewer/LodBuilder.h"
|
||||
@@ -42,6 +41,8 @@
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressDialog>
|
||||
@@ -56,125 +57,144 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace ifcviewerfull::modules::models {
|
||||
namespace ifcviewerfull::modules::models::commands {
|
||||
|
||||
ModelsPanelController::ModelsPanelController(QWidget* host,
|
||||
ModelsPanel* widget,
|
||||
ifcviewerfull::SessionState* session_state,
|
||||
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");
|
||||
});
|
||||
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");
|
||||
});
|
||||
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();
|
||||
});
|
||||
namespace {
|
||||
|
||||
QString formatElapsed(qint64 ms) {
|
||||
return (ms >= 1000)
|
||||
? QString::number(ms / 1000.0, 'f', 2) + " s"
|
||||
: QString::number(ms) + " ms";
|
||||
}
|
||||
|
||||
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) {
|
||||
writeSidecarForModel(loader, mid);
|
||||
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);
|
||||
QMessageBox::warning(host, "IfcViewer", message);
|
||||
});
|
||||
connect(loader, &SceneLoader::allLoadsFinished, this,
|
||||
[this, loader]() {
|
||||
session_state_->setStatusMessage("Loaded", QString("%1 model(s)").arg(loader->modelCount()));
|
||||
});
|
||||
} // namespace
|
||||
|
||||
void toggleVisibility(SessionState& s, ItemKind kind, const QString& id) {
|
||||
Federation* fed = s.federation();
|
||||
if (kind == ItemKind::Group) {
|
||||
const Federation::Group* group = fed->findGroupById(id);
|
||||
if (!group) return;
|
||||
fed->setGroupVisible(id, !group->visible);
|
||||
s.notifyVisibilityChanged();
|
||||
s.setStatusMessage("Models", group->visible ? "Group hidden" : "Group shown");
|
||||
} else {
|
||||
const Federation::Model* model = fed->findById(id);
|
||||
if (!model) return;
|
||||
fed->setModelVisible(id, !model->visible);
|
||||
s.notifyVisibilityChanged();
|
||||
s.setStatusMessage("Models", model->visible ? "Model hidden" : "Model shown");
|
||||
}
|
||||
}
|
||||
|
||||
void ModelsPanelController::addFiles() {
|
||||
modules::models::AddModelDialog dialog(host_);
|
||||
void addGroup(SessionState& s, QWidget& host, const QString& parent_group_id) {
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
&host, "New Group", "Group name:", QLineEdit::Normal, "Group", &ok);
|
||||
if (!ok) return;
|
||||
const QString trimmed = name.trimmed();
|
||||
if (trimmed.isEmpty()) return;
|
||||
|
||||
s.federation()->addGroup(trimmed, parent_group_id);
|
||||
s.notifyFederationChanged();
|
||||
s.setStatusMessage("Models", "Group added");
|
||||
}
|
||||
|
||||
void renameGroup(SessionState& s, QWidget& host, const QString& group_id) {
|
||||
const Federation::Group* group = s.federation()->findGroupById(group_id);
|
||||
if (!group) return;
|
||||
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
&host, "Rename Group", "Group name:", QLineEdit::Normal, group->display_name, &ok);
|
||||
if (!ok) return;
|
||||
const QString trimmed = name.trimmed();
|
||||
if (trimmed.isEmpty()) return;
|
||||
|
||||
s.federation()->setGroupName(group_id, trimmed);
|
||||
s.notifyFederationChanged();
|
||||
s.setStatusMessage("Models", "Group renamed");
|
||||
}
|
||||
|
||||
void moveGroup(SessionState& s, const QString& id, const QString& parent_group_id) {
|
||||
s.federation()->setGroupParent(id, parent_group_id);
|
||||
s.notifyFederationChanged();
|
||||
s.setStatusMessage("Models", parent_group_id.isEmpty() ? "Group moved to root" : "Group moved");
|
||||
}
|
||||
|
||||
void moveModels(SessionState& s, const QStringList& ids, const QString& parent_group_id) {
|
||||
for (const auto& id : ids) {
|
||||
s.federation()->setModelGroup(id, parent_group_id);
|
||||
}
|
||||
s.notifyFederationChanged();
|
||||
s.setStatusMessage("Models", parent_group_id.isEmpty() ? "Model(s) moved to root" : "Model(s) moved");
|
||||
}
|
||||
|
||||
void removeGroup(SessionState& s, QWidget& host, const QString& group_id) {
|
||||
const Federation::Group* group = s.federation()->findGroupById(group_id);
|
||||
if (!group) return;
|
||||
|
||||
const auto choice = QMessageBox::question(
|
||||
&host, "Remove Group",
|
||||
QString("Remove group '%1'? Models inside it will move to the parent.").arg(group->display_name),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if (choice != QMessageBox::Yes) return;
|
||||
|
||||
s.federation()->removeGroup(group_id);
|
||||
s.notifyFederationChanged();
|
||||
s.setStatusMessage("Models", "Group removed");
|
||||
}
|
||||
|
||||
void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QString& fed_id) {
|
||||
const Federation::Model* model = s.federation()->findById(fed_id);
|
||||
const QString label = model ? model->display_name : fed_id;
|
||||
const auto choice = QMessageBox::question(
|
||||
&host, "Remove Model",
|
||||
QString("Remove model '%1' from the federation?").arg(label),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if (choice != QMessageBox::Yes) return;
|
||||
|
||||
const uint32_t mid = s.modelIdForFedId(fed_id);
|
||||
if (mid == 0) {
|
||||
s.federation()->removeModel(fed_id);
|
||||
s.notifyFederationChanged();
|
||||
s.setStatusMessage("Models", "Model removed");
|
||||
return;
|
||||
}
|
||||
if (s.loader()->isLoadingModel(mid)) return;
|
||||
|
||||
vp.setSelectedObjectId(0);
|
||||
s.setSelectedObjectId(0);
|
||||
s.federation()->removeModel(fed_id);
|
||||
vp.removeModel(mid);
|
||||
s.loader()->removeModel(mid);
|
||||
s.elementRegistry()->removeModel(mid);
|
||||
s.removeModelMappingByFedId(fed_id);
|
||||
s.notifySelectionChanged();
|
||||
s.notifyModelsChanged();
|
||||
s.setStatusMessage("Models", "Model removed");
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
void loadModels(SessionState& s, const QStringList& paths, const QStringList& fed_ids) {
|
||||
if (paths.isEmpty()) return;
|
||||
|
||||
const auto ids = s.loader()->addFiles(paths);
|
||||
for (int i = 0; i < paths.size() && i < static_cast<int>(ids.size()) && i < fed_ids.size(); ++i) {
|
||||
s.setModelMapping(fed_ids[i], ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
void addModel(SessionState& s, QWidget& host) {
|
||||
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");
|
||||
case 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);
|
||||
@@ -183,8 +203,8 @@ void ModelsPanelController::addFiles() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case modules::models::SourceMode::IfcDatabase: {
|
||||
QFileDialog database_dialog(host_, "Add IFC Databases");
|
||||
case 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);
|
||||
@@ -200,8 +220,8 @@ void ModelsPanelController::addFiles() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case modules::models::SourceMode::GeometryOnly: {
|
||||
QFileDialog file_dialog(host_, "Add Geometry Only");
|
||||
case 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);
|
||||
@@ -210,21 +230,30 @@ void ModelsPanelController::addFiles() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case modules::models::SourceMode::ConvertToDatabase:
|
||||
convertIfcToDatabase();
|
||||
case SourceMode::ConvertToDatabase:
|
||||
convertIfcToDatabase(s, host);
|
||||
return;
|
||||
case modules::models::SourceMode::ExportGeometryDatabase:
|
||||
exportGeometryDatabase();
|
||||
case SourceMode::ExportGeometryDatabase:
|
||||
exportGeometryDatabase(s, host);
|
||||
return;
|
||||
case modules::models::SourceMode::None:
|
||||
case SourceMode::None:
|
||||
return;
|
||||
}
|
||||
|
||||
addFiles(paths);
|
||||
QStringList accepted_paths;
|
||||
QStringList accepted_fed_ids;
|
||||
for (const auto& path : paths) {
|
||||
const QString fed_id = s.federation()->addModel(path);
|
||||
if (fed_id.isEmpty()) continue;
|
||||
accepted_paths << path;
|
||||
accepted_fed_ids << fed_id;
|
||||
}
|
||||
detail::loadModels(s, accepted_paths, accepted_fed_ids);
|
||||
s.notifyModelsChanged();
|
||||
}
|
||||
|
||||
void ModelsPanelController::convertIfcToDatabase() {
|
||||
QFileDialog input_dialog(host_, "Select IFC File to Convert");
|
||||
void convertIfcToDatabase(SessionState& s, QWidget& host) {
|
||||
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);
|
||||
@@ -236,7 +265,7 @@ void ModelsPanelController::convertIfcToDatabase() {
|
||||
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");
|
||||
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 (*)");
|
||||
@@ -255,17 +284,13 @@ void ModelsPanelController::convertIfcToDatabase() {
|
||||
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,
|
||||
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_);
|
||||
auto* progress = new QProgressDialog(&host);
|
||||
progress->setWindowTitle("Convert IFC to Database");
|
||||
progress->setLabelText(QString("Converting %1 to %2…")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
@@ -278,7 +303,7 @@ void ModelsPanelController::runIfcToDatabaseConversion(const QString& input_path
|
||||
progress->setAutoReset(false);
|
||||
progress->show();
|
||||
|
||||
session_state_->setStatusMessage("Converting",
|
||||
s.setStatusMessage("Converting",
|
||||
QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName()));
|
||||
|
||||
auto timer = std::make_shared<QElapsedTimer>();
|
||||
@@ -312,8 +337,8 @@ void ModelsPanelController::runIfcToDatabaseConversion(const QString& input_path
|
||||
}
|
||||
});
|
||||
|
||||
connect(thread, &QThread::finished, this,
|
||||
[this, thread, progress, timer, error_message, input_path, output_path]() {
|
||||
QObject::connect(thread, &QThread::finished, &host,
|
||||
[&s, host_ptr = &host, thread, progress, timer, error_message, input_path, output_path]() {
|
||||
const qint64 elapsed = timer->elapsed();
|
||||
|
||||
progress->close();
|
||||
@@ -321,27 +346,27 @@ void ModelsPanelController::runIfcToDatabaseConversion(const QString& input_path
|
||||
thread->deleteLater();
|
||||
|
||||
if (!error_message->isEmpty()) {
|
||||
session_state_->setStatusMessage("Error", *error_message);
|
||||
QMessageBox::warning(host_, "Convert IFC to Database",
|
||||
s.setStatusMessage("Error", *error_message);
|
||||
QMessageBox::warning(host_ptr, "Convert IFC to Database",
|
||||
QString("Conversion failed:\n%1").arg(*error_message));
|
||||
return;
|
||||
}
|
||||
|
||||
session_state_->setStatusMessage(
|
||||
s.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",
|
||||
QMessageBox::information(host_ptr, "Convert IFC to Database",
|
||||
QString("Database written to:\n%1").arg(output_path));
|
||||
});
|
||||
|
||||
thread->start();
|
||||
}
|
||||
|
||||
void ModelsPanelController::exportGeometryDatabase() {
|
||||
QFileDialog input_dialog(host_, "Select IFC File to Export");
|
||||
void exportGeometryDatabase(SessionState& s, QWidget& host) {
|
||||
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);
|
||||
@@ -353,7 +378,7 @@ void ModelsPanelController::exportGeometryDatabase() {
|
||||
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");
|
||||
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 (*)");
|
||||
@@ -368,11 +393,7 @@ void ModelsPanelController::exportGeometryDatabase() {
|
||||
output_path += ".rdbview";
|
||||
}
|
||||
|
||||
runGeometryDatabaseExport(input_path, output_path);
|
||||
}
|
||||
|
||||
void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path, const QString& output_path) {
|
||||
auto* progress = new QProgressDialog(host_);
|
||||
auto* progress = new QProgressDialog(&host);
|
||||
progress->setWindowTitle("Export Geometry Database");
|
||||
progress->setLabelText(QString("Exporting %1 to %2…")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
@@ -385,7 +406,7 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path,
|
||||
progress->setAutoReset(false);
|
||||
progress->show();
|
||||
|
||||
session_state_->setStatusMessage("Exporting",
|
||||
s.setStatusMessage("Exporting",
|
||||
QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName()));
|
||||
|
||||
auto timer = std::make_shared<QElapsedTimer>();
|
||||
@@ -407,7 +428,6 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path,
|
||||
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();
|
||||
@@ -429,7 +449,6 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path,
|
||||
serializer->finalize();
|
||||
serializer.reset();
|
||||
|
||||
// Step 2: .ifcview sidecar via the headless builder.
|
||||
HeadlessSidecarBuilder builder;
|
||||
if (!builder.build(input_path, tmp_anchor)) {
|
||||
throw ifcopenshell::exception(
|
||||
@@ -440,7 +459,6 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path,
|
||||
("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";
|
||||
@@ -498,8 +516,8 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path,
|
||||
QDir(tmp_root).removeRecursively();
|
||||
});
|
||||
|
||||
connect(thread, &QThread::finished, this,
|
||||
[this, thread, progress, timer, error_message, input_path, output_path]() {
|
||||
QObject::connect(thread, &QThread::finished, &host,
|
||||
[&s, host_ptr = &host, thread, progress, timer, error_message, input_path, output_path]() {
|
||||
const qint64 elapsed = timer->elapsed();
|
||||
|
||||
progress->close();
|
||||
@@ -507,83 +525,36 @@ void ModelsPanelController::runGeometryDatabaseExport(const QString& input_path,
|
||||
thread->deleteLater();
|
||||
|
||||
if (!error_message->isEmpty()) {
|
||||
session_state_->setStatusMessage("Error", *error_message);
|
||||
QMessageBox::warning(host_, "Export Geometry Database",
|
||||
s.setStatusMessage("Error", *error_message);
|
||||
QMessageBox::warning(host_ptr, "Export Geometry Database",
|
||||
QString("Export failed:\n%1").arg(*error_message));
|
||||
return;
|
||||
}
|
||||
|
||||
session_state_->setStatusMessage(
|
||||
s.setStatusMessage(
|
||||
"Exported",
|
||||
QString("%1 → %2 in %3")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
QFileInfo(output_path).fileName(),
|
||||
formatElapsed(elapsed)));
|
||||
QMessageBox::information(host_, "Export Geometry Database",
|
||||
QMessageBox::information(host_ptr, "Export Geometry Database",
|
||||
QString("Geometry database written to:\n%1").arg(output_path));
|
||||
});
|
||||
|
||||
thread->start();
|
||||
}
|
||||
|
||||
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() {
|
||||
SettingsDialog dialog(session_state_, host_);
|
||||
void openSettings(SessionState& s, QWidget& host) {
|
||||
SettingsDialog dialog(&s, &host);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
QString ModelsPanelController::formatElapsed(qint64 ms) const {
|
||||
return (ms >= 1000)
|
||||
? QString::number(ms / 1000.0, 'f', 2) + " s"
|
||||
: QString::number(ms) + " ms";
|
||||
}
|
||||
|
||||
void ModelsPanelController::writeSidecarForModel(SceneLoader* loader, uint32_t mid) const {
|
||||
if (!loader || !viewport_ || !session_state_) return;
|
||||
void writeSidecarForLoadedModel(SessionState& s, ViewportWindow& vp, uint32_t mid) {
|
||||
SceneLoader* loader = s.loader();
|
||||
if (!loader) return;
|
||||
|
||||
SidecarData sidecar_data;
|
||||
if (!viewport_->snapshotModel(mid, sidecar_data)) return;
|
||||
if (!vp.snapshotModel(mid, sidecar_data)) return;
|
||||
|
||||
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
||||
sidecar_data.has_coordinate_operation = georef->has_coordinate_operation ? 1 : 0;
|
||||
@@ -593,8 +564,7 @@ void ModelsPanelController::writeSidecarForModel(SceneLoader* loader, uint32_t m
|
||||
sidecar_data.map_unit_to_meters = georef->units.map_unit_to_meters;
|
||||
}
|
||||
|
||||
auto* element_registry = session_state_->elementRegistry();
|
||||
if (element_registry) {
|
||||
if (auto* element_registry = s.elementRegistry()) {
|
||||
for (const auto& info : element_registry->basicElementInfoForModel(mid)) {
|
||||
PackedElementInfo packed;
|
||||
packed.object_id = info.object_id;
|
||||
@@ -631,7 +601,7 @@ void ModelsPanelController::writeSidecarForModel(SceneLoader* loader, uint32_t m
|
||||
lod_stats.meshes_with_lod1, lod_stats.meshes_total,
|
||||
lod_stats.tris_lod0_for_lod1, lod_stats.tris_lod1);
|
||||
|
||||
viewport_->applyLodExtension(mid, sidecar_data);
|
||||
vp.applyLodExtension(mid, sidecar_data);
|
||||
|
||||
QElapsedTimer sidecar_timer;
|
||||
sidecar_timer.start();
|
||||
@@ -639,4 +609,4 @@ void ModelsPanelController::writeSidecarForModel(SceneLoader* loader, uint32_t m
|
||||
qDebug(" Sidecar write: %lld ms (%s)", sidecar_timer.elapsed(), ok ? "ok" : "FAILED");
|
||||
}
|
||||
|
||||
} // namespace ifcviewerfull::modules::models
|
||||
} // namespace ifcviewerfull::modules::models::commands
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_MODULES_MODELS_COMMANDS_H
|
||||
#define IFCINTERFACE_MODULES_MODELS_COMMANDS_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <cstdint>
|
||||
|
||||
class QWidget;
|
||||
class ViewportWindow;
|
||||
namespace ifcviewerfull { class SessionState; }
|
||||
|
||||
namespace ifcviewerfull::modules::models::commands {
|
||||
|
||||
// User-facing commands. Each one is responsible for emitting any notify()
|
||||
// signals exactly once, at the end of its execution.
|
||||
void toggleVisibility(SessionState& s, ItemKind kind, const QString& id);
|
||||
void addGroup(SessionState& s, QWidget& host, const QString& parent_group_id);
|
||||
void renameGroup(SessionState& s, QWidget& host, const QString& group_id);
|
||||
void moveGroup(SessionState& s, const QString& id, const QString& parent_group_id);
|
||||
void moveModels(SessionState& s, const QStringList& ids, const QString& parent_group_id);
|
||||
void removeGroup(SessionState& s, QWidget& host, const QString& group_id);
|
||||
void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QString& fed_id);
|
||||
void addModel(SessionState& s, QWidget& host);
|
||||
void convertIfcToDatabase(SessionState& s, QWidget& host);
|
||||
void exportGeometryDatabase(SessionState& s, QWidget& host);
|
||||
void openSettings(SessionState& s, QWidget& host);
|
||||
|
||||
// Snapshots the in-memory geometry + element registry for a freshly streamed
|
||||
// model and persists it as a sidecar next to the source IFC. Called after
|
||||
// SceneLoader::loadedFromStream so subsequent loads can skip the stream phase.
|
||||
void writeSidecarForLoadedModel(SessionState& s, ViewportWindow& vp, uint32_t mid);
|
||||
|
||||
// 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.
|
||||
namespace detail {
|
||||
|
||||
// Queues already-federated models on the loader and maps their fed-ids to mids.
|
||||
void loadModels(SessionState& s, const QStringList& paths, const QStringList& fed_ids);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
} // namespace ifcviewerfull::modules::models::commands
|
||||
|
||||
#endif
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
#include "Panel.h"
|
||||
|
||||
#include "Commands.h"
|
||||
|
||||
#include "../../ViewerSettings.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
@@ -30,8 +32,6 @@
|
||||
#include <QDragEnterEvent>
|
||||
#include <QDragMoveEvent>
|
||||
#include <QHeaderView>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QMimeData>
|
||||
#include <QDropEvent>
|
||||
@@ -205,12 +205,6 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
QString promptGroupName(QWidget* parent, const QString& title, const QString& label, const QString& value) {
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(parent, title, label, QLineEdit::Normal, value, &ok);
|
||||
return ok ? name.trimmed() : QString();
|
||||
}
|
||||
|
||||
QString itemId(QTreeWidgetItem* item) {
|
||||
return item ? item->data(0, Qt::UserRole + 1).toString() : QString();
|
||||
}
|
||||
@@ -229,8 +223,12 @@ QList<QTreeWidgetItem*> selectedItemsOfKind(QTreeWidget* tree, ItemKind kind) {
|
||||
|
||||
} // namespace
|
||||
|
||||
ModelsPanel::ModelsPanel(QWidget* parent)
|
||||
ModelsPanel::ModelsPanel(ifcviewerfull::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QWidget* parent)
|
||||
: components::Panel("Models", nullptr, parent, true)
|
||||
, session_state_(session_state)
|
||||
, viewport_(viewport)
|
||||
{
|
||||
auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
section->setBodyExpanding(true);
|
||||
@@ -254,19 +252,17 @@ ModelsPanel::ModelsPanel(QWidget* parent)
|
||||
tree_->header()->resizeSection(1, 28);
|
||||
tree_->header()->hide();
|
||||
tree->on_model_drop = [this](const QStringList& ids, const QString& target_group_id) {
|
||||
emit moveModelsRequested(ids, target_group_id);
|
||||
commands::moveModels(*session_state_, ids, target_group_id);
|
||||
};
|
||||
tree->on_group_drop = [this](const QString& id, const QString& target_group_id) {
|
||||
emit moveGroupRequested(id, target_group_id);
|
||||
commands::moveGroup(*session_state_, id, target_group_id);
|
||||
};
|
||||
section->addBodyWidget(tree_);
|
||||
addBodyWidget(section);
|
||||
|
||||
connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int column) {
|
||||
if (!item || column != 1) return;
|
||||
emit visibilityToggleRequested(
|
||||
static_cast<ItemKind>(item->data(0, Qt::UserRole).toInt()),
|
||||
item->data(0, Qt::UserRole + 1).toString());
|
||||
commands::toggleVisibility(*session_state_, itemKind(item), itemId(item));
|
||||
});
|
||||
|
||||
connect(tree_, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
@@ -276,10 +272,7 @@ ModelsPanel::ModelsPanel(QWidget* parent)
|
||||
QAction* add_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "Add Group");
|
||||
connect(add_group_action, &QAction::triggered, this, [this]() {
|
||||
const QString name = promptGroupName(this, "New Group", "Group name:", "Group");
|
||||
if (!name.isEmpty()) {
|
||||
emit addGroupRequested(QString(), name);
|
||||
}
|
||||
commands::addGroup(*session_state_, *this, QString());
|
||||
});
|
||||
menu.exec(tree_->viewport()->mapToGlobal(pos));
|
||||
return;
|
||||
@@ -291,59 +284,41 @@ ModelsPanel::ModelsPanel(QWidget* parent)
|
||||
QAction* toggle_visibility_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/eye.svg"), "Toggle Visibility");
|
||||
connect(toggle_visibility_action, &QAction::triggered, this, [this, kind, id]() {
|
||||
emit visibilityToggleRequested(kind, id);
|
||||
commands::toggleVisibility(*session_state_, kind, id);
|
||||
});
|
||||
|
||||
if (kind == ItemKind::Group) {
|
||||
QAction* add_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Subgroup");
|
||||
connect(add_group_action, &QAction::triggered, this, [this, id]() {
|
||||
const QString name = promptGroupName(this, "New Group", "Group name:", "Group");
|
||||
if (!name.isEmpty()) {
|
||||
emit addGroupRequested(id, name);
|
||||
}
|
||||
commands::addGroup(*session_state_, *this, id);
|
||||
});
|
||||
QAction* rename_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder.svg"), "Rename Group");
|
||||
connect(rename_group_action, &QAction::triggered, this, [this, item, id]() {
|
||||
const QString name = promptGroupName(
|
||||
this, "Rename Group", "Group name:", item->text(0));
|
||||
if (!name.isEmpty()) emit renameGroupRequested(id, name);
|
||||
connect(rename_group_action, &QAction::triggered, this, [this, id]() {
|
||||
commands::renameGroup(*session_state_, *this, id);
|
||||
});
|
||||
|
||||
QMenu* move_menu = menu.addMenu("Move to Parent");
|
||||
QAction* move_root_action = move_menu->addAction("(Root)");
|
||||
connect(move_root_action, &QAction::triggered, this, [this, id]() {
|
||||
emit moveGroupRequested(id, QString());
|
||||
commands::moveGroup(*session_state_, id, QString());
|
||||
});
|
||||
move_menu->addSeparator();
|
||||
QList<QTreeWidgetItem*> stack;
|
||||
for (int i = 0; i < tree_->topLevelItemCount(); ++i) {
|
||||
stack.push_back(tree_->topLevelItem(i));
|
||||
}
|
||||
while (!stack.isEmpty()) {
|
||||
QTreeWidgetItem* candidate = stack.takeFirst();
|
||||
if (candidate != item && itemKind(candidate) == ItemKind::Group) {
|
||||
bool would_cycle = false;
|
||||
for (QTreeWidgetItem* cur = candidate; cur != nullptr; cur = cur->parent()) {
|
||||
if (cur == item) {
|
||||
would_cycle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto* action = move_menu->addAction(candidate->text(0));
|
||||
action->setEnabled(!would_cycle && candidate != item->parent());
|
||||
connect(action, &QAction::triggered, this, [this, id, candidate]() {
|
||||
emit moveGroupRequested(id, itemId(candidate));
|
||||
});
|
||||
}
|
||||
for (int i = 0; i < candidate->childCount(); ++i) {
|
||||
stack.push_back(candidate->child(i));
|
||||
}
|
||||
const QList<GroupOption> targets =
|
||||
group_list_provider_ ? group_list_provider_(id) : QList<GroupOption>{};
|
||||
for (const auto& target : targets) {
|
||||
QAction* action = move_menu->addAction(target.display_name);
|
||||
const QString target_id = target.id;
|
||||
connect(action, &QAction::triggered, this, [this, id, target_id]() {
|
||||
commands::moveGroup(*session_state_, id, target_id);
|
||||
});
|
||||
}
|
||||
|
||||
QAction* remove_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-minus.svg"), "Remove Group");
|
||||
connect(remove_group_action, &QAction::triggered, this, [this, id]() {
|
||||
emit removeGroupRequested(id);
|
||||
commands::removeGroup(*session_state_, *this, id);
|
||||
});
|
||||
} else {
|
||||
QString parent_group_id;
|
||||
@@ -356,10 +331,7 @@ ModelsPanel::ModelsPanel(QWidget* parent)
|
||||
QAction* add_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Group");
|
||||
connect(add_group_action, &QAction::triggered, this, [this, parent_group_id]() {
|
||||
const QString name = promptGroupName(this, "New Group", "Group name:", "Group");
|
||||
if (!name.isEmpty()) {
|
||||
emit addGroupRequested(parent_group_id, name);
|
||||
}
|
||||
commands::addGroup(*session_state_, *this, parent_group_id);
|
||||
});
|
||||
|
||||
QStringList selected_model_ids;
|
||||
@@ -376,34 +348,31 @@ ModelsPanel::ModelsPanel(QWidget* parent)
|
||||
QMenu* move_menu = menu.addMenu("Move to Group");
|
||||
QAction* move_root_action = move_menu->addAction("(Root)");
|
||||
connect(move_root_action, &QAction::triggered, this, [this, selected_model_ids]() {
|
||||
emit moveModelsRequested(selected_model_ids, QString());
|
||||
commands::moveModels(*session_state_, selected_model_ids, QString());
|
||||
});
|
||||
move_menu->addSeparator();
|
||||
QList<QTreeWidgetItem*> stack;
|
||||
for (int i = 0; i < tree_->topLevelItemCount(); ++i) {
|
||||
stack.push_back(tree_->topLevelItem(i));
|
||||
}
|
||||
while (!stack.isEmpty()) {
|
||||
QTreeWidgetItem* candidate = stack.takeFirst();
|
||||
if (itemKind(candidate) == ItemKind::Group) {
|
||||
const QString group_id = itemId(candidate);
|
||||
QAction* action = move_menu->addAction(candidate->text(0));
|
||||
connect(action, &QAction::triggered, this, [this, selected_model_ids, group_id]() {
|
||||
emit moveModelsRequested(selected_model_ids, group_id);
|
||||
});
|
||||
}
|
||||
for (int i = 0; i < candidate->childCount(); ++i) {
|
||||
stack.push_back(candidate->child(i));
|
||||
}
|
||||
const QList<GroupOption> targets =
|
||||
group_list_provider_ ? group_list_provider_(QString()) : QList<GroupOption>{};
|
||||
for (const auto& target : targets) {
|
||||
QAction* action = move_menu->addAction(target.display_name);
|
||||
const QString target_id = target.id;
|
||||
connect(action, &QAction::triggered, this, [this, selected_model_ids, target_id]() {
|
||||
commands::moveModels(*session_state_, selected_model_ids, target_id);
|
||||
});
|
||||
}
|
||||
|
||||
QAction* remove_model_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model");
|
||||
connect(remove_model_action, &QAction::triggered, this, [this, id]() {
|
||||
emit removeModelRequested(id);
|
||||
commands::removeModel(*session_state_, *viewport_, *this, id);
|
||||
});
|
||||
}
|
||||
menu.exec(tree_->viewport()->mapToGlobal(pos));
|
||||
});
|
||||
|
||||
connect(this, &components::Panel::settingsRequested, this, [this]() {
|
||||
commands::openSettings(*session_state_, *this);
|
||||
});
|
||||
}
|
||||
|
||||
void ModelsPanel::setNodes(const QList<TreeNode>& nodes) {
|
||||
@@ -418,6 +387,10 @@ void ModelsPanel::setNodes(const QList<TreeNode>& nodes) {
|
||||
tree_->expandAll();
|
||||
}
|
||||
|
||||
void ModelsPanel::setGroupListProvider(GroupListProvider provider) {
|
||||
group_list_provider_ = std::move(provider);
|
||||
}
|
||||
|
||||
void ModelsPanel::addNode(QTreeWidgetItem* parent, const TreeNode& node) {
|
||||
auto* item = new QTreeWidgetItem(parent, {node.name, ""});
|
||||
item->setData(0, Qt::UserRole, static_cast<int>(node.kind));
|
||||
|
||||
@@ -25,31 +25,42 @@
|
||||
|
||||
#include "../../components/Panel.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class ViewportWindow;
|
||||
namespace ifcviewerfull { class SessionState; }
|
||||
|
||||
namespace ifcviewerfull::modules::models {
|
||||
|
||||
// The widget for the Models dock. Owns no domain state; its click handlers
|
||||
// call commands directly. The View tells it what to render (setNodes) and
|
||||
// supplies derived data for the right-click menu (setGroupListProvider).
|
||||
class ModelsPanel : public components::Panel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModelsPanel(QWidget* parent = nullptr);
|
||||
// Returns the groups a move operation may target. If exclude_subtree_root
|
||||
// is non-empty, that group + its descendants are excluded so a group can't
|
||||
// be moved into its own subtree. For model moves the panel passes an empty
|
||||
// string and gets every group back.
|
||||
using GroupListProvider =
|
||||
std::function<QList<GroupOption>(const QString& exclude_subtree_root)>;
|
||||
|
||||
explicit ModelsPanel(ifcviewerfull::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
void setNodes(const QList<TreeNode>& nodes);
|
||||
|
||||
signals:
|
||||
void visibilityToggleRequested(ItemKind kind, const QString& id);
|
||||
void addGroupRequested(const QString& parent_group_id, const QString& name);
|
||||
void renameGroupRequested(const QString& id, const QString& name);
|
||||
void moveGroupRequested(const QString& id, const QString& parent_group_id);
|
||||
void moveModelsRequested(const QStringList& ids, const QString& parent_group_id);
|
||||
void removeGroupRequested(const QString& id);
|
||||
void removeModelRequested(const QString& id);
|
||||
void setGroupListProvider(GroupListProvider provider);
|
||||
|
||||
private:
|
||||
void addNode(QTreeWidgetItem* parent, const TreeNode& node);
|
||||
|
||||
ifcviewerfull::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
QTreeWidget* tree_ = nullptr;
|
||||
GroupListProvider group_list_provider_;
|
||||
};
|
||||
|
||||
} // namespace ifcviewerfull::modules::models
|
||||
|
||||
@@ -475,6 +475,9 @@ void SettingsDialog::onAccepted() {
|
||||
xf.pivot = parseVector3(row.pivot->text());
|
||||
federation_->setModelTransformation(row.fed_id, xf);
|
||||
}
|
||||
if (session_state_) {
|
||||
session_state_->notifyFederationChanged();
|
||||
}
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@ struct TreeNode {
|
||||
QList<TreeNode> children;
|
||||
};
|
||||
|
||||
// One entry in a "move to..." menu. Computed by the View from federation state
|
||||
// and passed into the Panel so menu construction has no domain knowledge.
|
||||
struct GroupOption {
|
||||
QString id;
|
||||
QString display_name;
|
||||
};
|
||||
|
||||
struct SelectedModelGeorefState {
|
||||
QString georef_present;
|
||||
QString coordinate_operation_type;
|
||||
|
||||
@@ -54,31 +54,44 @@ TreeNode makeGroupNode(const Federation* federation, const Federation::Group* gr
|
||||
return node;
|
||||
}
|
||||
|
||||
// Walks the federation's group tree, appending every group except those under
|
||||
// exclude_subtree_root (used to prevent a group from being moved into itself).
|
||||
void collectGroupsRecursive(const Federation::Group* group,
|
||||
const QString& exclude_subtree_root,
|
||||
QList<GroupOption>& out) {
|
||||
if (group->id == exclude_subtree_root) return;
|
||||
out.append({group->id, group->display_name});
|
||||
for (const auto& child : group->children) {
|
||||
collectGroupsRecursive(child.get(), exclude_subtree_root, out);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
|
||||
ifcviewerfull::SessionState* session_state,
|
||||
QObject* parent)
|
||||
: QObject(parent), widget_(widget), session_state_(session_state)
|
||||
: QObject(parent)
|
||||
, widget_(widget)
|
||||
, session_state_(session_state)
|
||||
{
|
||||
connect(session_state_, &ifcviewerfull::SessionState::modelsChanged,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcviewerfull::SessionState::federationStructureChanged,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcviewerfull::SessionState::visibilityChanged,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcviewerfull::SessionState::projectReset,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcviewerfull::SessionState::projectOpened,
|
||||
this, [this](const QString&) { reload(); });
|
||||
connect(session_state_, &SessionState::modelsChanged, this, &ModelsPanelView::refresh);
|
||||
connect(session_state_, &SessionState::federationChanged, this, &ModelsPanelView::refresh);
|
||||
connect(session_state_, &SessionState::visibilityChanged, this, &ModelsPanelView::refresh);
|
||||
connect(session_state_, &SessionState::projectReset, this, &ModelsPanelView::refresh);
|
||||
connect(session_state_, &SessionState::projectOpened, this, [this](const QString&) { refresh(); });
|
||||
connect(&ifcviewerfull::ViewerSettings::instance(),
|
||||
&ifcviewerfull::ViewerSettings::themeChanged,
|
||||
this, [this]() { reload(); });
|
||||
this, &ModelsPanelView::refresh);
|
||||
|
||||
reload();
|
||||
widget_->setGroupListProvider([this](const QString& exclude_subtree_root) {
|
||||
return groupListForMove(exclude_subtree_root);
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void ModelsPanelView::reload() {
|
||||
void ModelsPanelView::refresh() {
|
||||
Federation* federation = session_state_->federation();
|
||||
QList<TreeNode> nodes;
|
||||
for (const auto& root_group : federation->rootGroups()) {
|
||||
@@ -97,4 +110,12 @@ void ModelsPanelView::reload() {
|
||||
widget_->setNodes(nodes);
|
||||
}
|
||||
|
||||
QList<GroupOption> ModelsPanelView::groupListForMove(const QString& exclude_subtree_root) const {
|
||||
QList<GroupOption> out;
|
||||
for (const auto& root : session_state_->federation()->rootGroups()) {
|
||||
collectGroupsRecursive(root.get(), exclude_subtree_root, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace ifcviewerfull::modules::models
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace ifcviewerfull::modules::models {
|
||||
|
||||
class ModelsPanel;
|
||||
|
||||
// Subscribes to SessionState and re-derives panel state (tree nodes, valid
|
||||
// move targets) from the federation. The panel calls commands directly for
|
||||
// input, so this object is purely state→view; it has no command knowledge.
|
||||
class ModelsPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
@@ -39,7 +42,8 @@ public:
|
||||
QObject* parent = nullptr);
|
||||
|
||||
private:
|
||||
void reload();
|
||||
void refresh();
|
||||
QList<GroupOption> groupListForMove(const QString& exclude_subtree_root) const;
|
||||
|
||||
ModelsPanel* widget_ = nullptr;
|
||||
ifcviewerfull::SessionState* session_state_ = nullptr;
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../models/Controller.h"
|
||||
#include "../viewport/Controller.h"
|
||||
#include "../models/Commands.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
@@ -35,21 +34,13 @@
|
||||
namespace ifcviewerfull::modules::project {
|
||||
|
||||
ProjectController::ProjectController(QWidget* host,
|
||||
Federation* federation,
|
||||
ifcviewerfull::SessionState* session_state,
|
||||
ifcviewerfull::ElementRegistry* element_registry,
|
||||
ViewportWindow* viewport,
|
||||
ifcviewerfull::modules::models::ModelsPanelController* models_controller,
|
||||
ifcviewerfull::modules::viewport::ViewportController* viewport_controller,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, host_(host)
|
||||
, federation_(federation)
|
||||
, session_state_(session_state)
|
||||
, element_registry_(element_registry)
|
||||
, viewport_(viewport)
|
||||
, models_controller_(models_controller)
|
||||
, viewport_controller_(viewport_controller)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -64,8 +55,7 @@ bool ProjectController::newProject() {
|
||||
if (!confirmDiscardIfDirty()) return false;
|
||||
|
||||
clearScene();
|
||||
federation_->clear();
|
||||
viewport_controller_->applyFederatedFalseOrigin();
|
||||
session_state_->federation()->clear();
|
||||
session_state_->setStatusMessage("Project", "Untitled");
|
||||
session_state_->notifyProjectReset();
|
||||
return true;
|
||||
@@ -95,7 +85,7 @@ bool ProjectController::openProject(const QString& path) {
|
||||
|
||||
QStringList warnings;
|
||||
QString err;
|
||||
if (!federation_->load(path, &warnings, &err)) {
|
||||
if (!session_state_->federation()->load(path, &warnings, &err)) {
|
||||
QMessageBox::warning(host_, "Open Project",
|
||||
QString("Could not open project:\n%1").arg(err));
|
||||
return false;
|
||||
@@ -105,7 +95,7 @@ bool ProjectController::openProject(const QString& path) {
|
||||
|
||||
QStringList paths;
|
||||
QStringList fed_ids;
|
||||
for (const auto& model : federation_->models()) {
|
||||
for (const auto& model : session_state_->federation()->models()) {
|
||||
if (model.source_kind != "local") continue;
|
||||
if (!QFileInfo::exists(model.source_path)) {
|
||||
warnings << QString("Source not found, kept in project: %1").arg(model.source_path);
|
||||
@@ -114,17 +104,16 @@ bool ProjectController::openProject(const QString& path) {
|
||||
paths << model.source_path;
|
||||
fed_ids << model.id;
|
||||
}
|
||||
models_controller_->loadModels(paths, fed_ids);
|
||||
ifcviewerfull::modules::models::commands::detail::loadModels(*session_state_, paths, fed_ids);
|
||||
|
||||
if (!warnings.isEmpty()) {
|
||||
QMessageBox::warning(host_, "Open Project",
|
||||
"Project opened with warnings:\n\n" + warnings.join("\n"));
|
||||
}
|
||||
|
||||
federation_->markClean();
|
||||
viewport_controller_->applyFederatedFalseOrigin();
|
||||
if (federation_->hasHomeView()) {
|
||||
const auto& hv = federation_->homeView();
|
||||
session_state_->federation()->markClean();
|
||||
if (session_state_->federation()->hasHomeView()) {
|
||||
const auto& hv = session_state_->federation()->homeView();
|
||||
viewport_->setCamera(
|
||||
hv.target.x(), hv.target.y(), hv.target.z(), hv.distance, hv.yaw, hv.pitch);
|
||||
}
|
||||
@@ -134,21 +123,21 @@ bool ProjectController::openProject(const QString& path) {
|
||||
}
|
||||
|
||||
bool ProjectController::saveProject() {
|
||||
if (federation_->filePath().isEmpty()) return saveProjectAs();
|
||||
if (session_state_->federation()->filePath().isEmpty()) return saveProjectAs();
|
||||
|
||||
QString err;
|
||||
if (!federation_->save(federation_->filePath(), &err)) {
|
||||
if (!session_state_->federation()->save(session_state_->federation()->filePath(), &err)) {
|
||||
QMessageBox::warning(host_, "Save Project",
|
||||
QString("Could not save project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
session_state_->setStatusMessage("Project", QFileInfo(federation_->filePath()).fileName());
|
||||
session_state_->notifyProjectSaved(federation_->filePath());
|
||||
session_state_->setStatusMessage("Project", QFileInfo(session_state_->federation()->filePath()).fileName());
|
||||
session_state_->notifyProjectSaved(session_state_->federation()->filePath());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectController::saveProjectAs() {
|
||||
QString suggested = federation_->filePath();
|
||||
QString suggested = session_state_->federation()->filePath();
|
||||
if (suggested.isEmpty()) suggested = "project.ifcfed";
|
||||
|
||||
QFileDialog file_dialog(host_, "Save Project As", suggested);
|
||||
@@ -166,7 +155,7 @@ bool ProjectController::saveProjectAs() {
|
||||
|
||||
bool ProjectController::saveProjectAs(const QString& path) {
|
||||
QString err;
|
||||
if (!federation_->save(path, &err)) {
|
||||
if (!session_state_->federation()->save(path, &err)) {
|
||||
QMessageBox::warning(host_, "Save Project",
|
||||
QString("Could not save project:\n%1").arg(err));
|
||||
return false;
|
||||
@@ -177,23 +166,20 @@ bool ProjectController::saveProjectAs(const QString& path) {
|
||||
}
|
||||
|
||||
void ProjectController::clearScene() {
|
||||
// Helper for newProject / openProject. Does not emit any notifies; the
|
||||
// caller emits projectReset / projectOpened once at the end of its flow.
|
||||
viewport_->setSelectedObjectId(0);
|
||||
session_state_->setSelectedObjectId(0);
|
||||
session_state_->notifySelectionChanged();
|
||||
|
||||
const auto model_ids = session_state_->modelIds();
|
||||
for (uint32_t mid : model_ids) {
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
viewport_->removeModel(mid);
|
||||
session_state_->loader()->removeModel(mid);
|
||||
}
|
||||
|
||||
session_state_->clearModelMappings();
|
||||
element_registry_->clear();
|
||||
session_state_->notifyModelsChanged();
|
||||
session_state_->elementRegistry()->clear();
|
||||
}
|
||||
|
||||
bool ProjectController::confirmDiscardIfDirty() {
|
||||
if (!federation_->isDirty()) return true;
|
||||
if (!session_state_->federation()->isDirty()) return true;
|
||||
const auto result = QMessageBox::question(
|
||||
host_, "Unsaved Project",
|
||||
"The current project has unsaved changes. Save before continuing?",
|
||||
|
||||
@@ -24,12 +24,8 @@
|
||||
#include <QObject>
|
||||
|
||||
class QWidget;
|
||||
class Federation;
|
||||
class ViewportWindow;
|
||||
namespace ifcviewerfull { class ElementRegistry; }
|
||||
namespace ifcviewerfull { class SessionState; }
|
||||
namespace ifcviewerfull::modules::models { class ModelsPanelController; }
|
||||
namespace ifcviewerfull::modules::viewport { class ViewportController; }
|
||||
|
||||
namespace ifcviewerfull::modules::project {
|
||||
|
||||
@@ -38,12 +34,8 @@ class ProjectController : public QObject {
|
||||
|
||||
public:
|
||||
explicit ProjectController(QWidget* host,
|
||||
Federation* federation,
|
||||
ifcviewerfull::SessionState* session_state,
|
||||
ifcviewerfull::ElementRegistry* element_registry,
|
||||
ViewportWindow* viewport,
|
||||
ifcviewerfull::modules::models::ModelsPanelController* models_controller,
|
||||
ifcviewerfull::modules::viewport::ViewportController* viewport_controller,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
bool newProject();
|
||||
@@ -58,12 +50,8 @@ private:
|
||||
bool confirmDiscardIfDirty();
|
||||
|
||||
QWidget* host_ = nullptr;
|
||||
Federation* federation_ = nullptr;
|
||||
ifcviewerfull::SessionState* session_state_ = nullptr;
|
||||
ifcviewerfull::ElementRegistry* element_registry_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
ifcviewerfull::modules::models::ModelsPanelController* models_controller_ = nullptr;
|
||||
ifcviewerfull::modules::viewport::ViewportController* viewport_controller_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcviewerfull::modules::project
|
||||
|
||||
@@ -38,6 +38,9 @@ PropertiesPanelView::PropertiesPanelView(PropertiesPanel* widget,
|
||||
connect(session_state_, &ifcviewerfull::SessionState::projectReset, this, [this]() {
|
||||
refresh(0);
|
||||
});
|
||||
connect(session_state_, &ifcviewerfull::SessionState::projectOpened, this, [this](const QString&) {
|
||||
refresh(0);
|
||||
});
|
||||
refresh(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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 "Commands.h"
|
||||
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
namespace ifcviewerfull::modules::viewport::commands {
|
||||
|
||||
void setHome(SessionState& session, ViewportWindow& vp) {
|
||||
auto camera = vp.cameraState();
|
||||
Federation::HomeView home_view;
|
||||
home_view.target = camera.target;
|
||||
home_view.distance = camera.distance;
|
||||
home_view.yaw = camera.yaw;
|
||||
home_view.pitch = camera.pitch;
|
||||
session.federation()->setHomeView(home_view);
|
||||
session.setStatusMessage("Camera", "Home view updated");
|
||||
}
|
||||
|
||||
void goHome(SessionState& session, ViewportWindow& vp) {
|
||||
Federation* federation = session.federation();
|
||||
if (!federation->hasHomeView()) {
|
||||
session.setStatusMessage("Camera", "No home view set for this project");
|
||||
return;
|
||||
}
|
||||
const auto& home_view = federation->homeView();
|
||||
vp.setCamera(
|
||||
home_view.target.x(), home_view.target.y(), home_view.target.z(),
|
||||
home_view.distance, home_view.yaw, home_view.pitch);
|
||||
session.setStatusMessage("Camera", "Home view restored");
|
||||
}
|
||||
|
||||
void viewSelected(ViewportWindow& vp) {
|
||||
vp.focusOnSelectedObject();
|
||||
}
|
||||
|
||||
void fly(SessionState& session, ViewportWindow& vp) {
|
||||
vp.requestActivate();
|
||||
vp.enterFpsMode();
|
||||
session.setStatusMessage("Mode", "Fly mode active");
|
||||
}
|
||||
|
||||
void toggleSection(SessionState& session, ViewportWindow& vp) {
|
||||
vp.toggleSectionTool();
|
||||
session.setStatusMessage("Section",
|
||||
vp.sectionToolActive() ? "Section tool active" : "Section tool off");
|
||||
}
|
||||
|
||||
void clearSection(SessionState& session, ViewportWindow& vp) {
|
||||
vp.clearSectionPlanes();
|
||||
session.setStatusMessage("Section", "Section planes cleared");
|
||||
}
|
||||
|
||||
void toggleDistance(ViewportWindow& vp) {
|
||||
vp.toggleLengthTool();
|
||||
}
|
||||
|
||||
void toggleArea(ViewportWindow& vp) {
|
||||
vp.toggleAreaTool();
|
||||
}
|
||||
|
||||
void toggleVolume(ViewportWindow& vp) {
|
||||
vp.toggleVolumeTool();
|
||||
}
|
||||
|
||||
void hideSelected(ViewportWindow& vp) {
|
||||
vp.hideSelectedElements();
|
||||
}
|
||||
|
||||
void isolateSelected(ViewportWindow& vp) {
|
||||
vp.isolateSelectedElements();
|
||||
}
|
||||
|
||||
void showAll(ViewportWindow& vp) {
|
||||
vp.showAllElements();
|
||||
}
|
||||
|
||||
void invertVisibility(ViewportWindow& vp) {
|
||||
vp.invertElementVisibility();
|
||||
}
|
||||
|
||||
} // namespace ifcviewerfull::modules::viewport::commands
|
||||
+18
-41
@@ -18,54 +18,31 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H
|
||||
#define IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H
|
||||
#ifndef IFCINTERFACE_MODULES_VIEWPORT_COMMANDS_H
|
||||
#define IFCINTERFACE_MODULES_VIEWPORT_COMMANDS_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
class QWidget;
|
||||
namespace ifcviewerfull { class SessionState; }
|
||||
class ViewportWindow;
|
||||
class SceneLoader;
|
||||
namespace ifcviewerfull { class SessionState; }
|
||||
|
||||
namespace ifcviewerfull::modules::models {
|
||||
namespace ifcviewerfull::modules::viewport::commands {
|
||||
|
||||
class ModelsPanel;
|
||||
void setHome(SessionState& session, ViewportWindow& vp);
|
||||
void goHome(SessionState& session, ViewportWindow& vp);
|
||||
void viewSelected(ViewportWindow& vp);
|
||||
|
||||
class ModelsPanelController : public QObject {
|
||||
Q_OBJECT
|
||||
void fly(SessionState& session, ViewportWindow& vp);
|
||||
void toggleSection(SessionState& session, ViewportWindow& vp);
|
||||
void clearSection(SessionState& session, ViewportWindow& vp);
|
||||
|
||||
public:
|
||||
explicit ModelsPanelController(QWidget* host,
|
||||
ModelsPanel* widget,
|
||||
ifcviewerfull::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent = nullptr);
|
||||
void toggleDistance(ViewportWindow& vp);
|
||||
void toggleArea(ViewportWindow& vp);
|
||||
void toggleVolume(ViewportWindow& vp);
|
||||
|
||||
void bindLoader(SceneLoader* loader);
|
||||
void addFiles();
|
||||
void addFiles(const QStringList& paths);
|
||||
void loadModels(const QStringList& paths, const QStringList& fed_ids);
|
||||
void removeLoadedModel(const QString& fed_id);
|
||||
void openSettings();
|
||||
void convertIfcToDatabase();
|
||||
void exportGeometryDatabase();
|
||||
void hideSelected(ViewportWindow& vp);
|
||||
void isolateSelected(ViewportWindow& vp);
|
||||
void showAll(ViewportWindow& vp);
|
||||
void invertVisibility(ViewportWindow& vp);
|
||||
|
||||
private:
|
||||
QString formatElapsed(qint64 ms) const;
|
||||
void writeSidecarForModel(SceneLoader* loader, uint32_t mid) const;
|
||||
void runIfcToDatabaseConversion(const QString& input_path, const QString& output_path);
|
||||
void runGeometryDatabaseExport(const QString& input_path, const QString& output_path);
|
||||
|
||||
QWidget* host_ = nullptr;
|
||||
ModelsPanel* widget_ = nullptr;
|
||||
ifcviewerfull::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcviewerfull::modules::models
|
||||
} // namespace ifcviewerfull::modules::viewport::commands
|
||||
|
||||
#endif
|
||||
+54
-159
@@ -18,11 +18,10 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Controller.h"
|
||||
#include "View.h"
|
||||
|
||||
#include "../../ViewerSettings.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/AppSettings.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
@@ -36,68 +35,30 @@
|
||||
|
||||
namespace ifcviewerfull::modules::viewport {
|
||||
|
||||
ViewportController::ViewportController(ifcviewerfull::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent)
|
||||
ViewportView::ViewportView(ifcviewerfull::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, session_state_(session_state)
|
||||
, viewport_(viewport)
|
||||
, area_measurement_(std::make_unique<AreaMeasurement>())
|
||||
, length_measurement_(std::make_unique<LengthMeasurement>())
|
||||
{
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
connect(&ifcviewerfull::ViewerSettings::instance(),
|
||||
&ifcviewerfull::ViewerSettings::themeChanged,
|
||||
this, [this]() {
|
||||
viewport_->setBackgroundColor(QColor(ifcviewerfull::ViewerSettings::instance().color("viewport_background")));
|
||||
});
|
||||
viewport_->setBackgroundColor(QColor(ifcviewerfull::ViewerSettings::instance().color("viewport_background")));
|
||||
connect(federation, &Federation::federatedFalseOriginChanged,
|
||||
this, &ViewportController::applyFederatedFalseOrigin);
|
||||
connect(federation, &Federation::configChanged, this, [this]() {
|
||||
applyFederatedFalseOrigin();
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
applyModelTransformation(mid);
|
||||
}
|
||||
});
|
||||
connect(federation, &Federation::modelTransformationChanged,
|
||||
this, [this](const QString& fed_id) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelTransformation(mid);
|
||||
});
|
||||
connect(federation, &Federation::modelVisibilityChanged,
|
||||
this, [this](const QString& fed_id, bool /*visible*/) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelVisibility(mid);
|
||||
});
|
||||
connect(federation, &Federation::modelGroupChanged,
|
||||
this, [this](const QString& fed_id, const QString& /*group_id*/) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelVisibility(mid);
|
||||
});
|
||||
connect(federation, &Federation::groupVisibilityChanged,
|
||||
this, [this](const QString&, bool /*visible*/) {
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
applyModelVisibility(mid);
|
||||
}
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromSidecar, this,
|
||||
[this](uint32_t mid, qint64 /*elapsed_ms*/) {
|
||||
applyCoordinateOperation(mid);
|
||||
applyModelVisibility(mid);
|
||||
maybeGuessFederatedFalseOrigin(mid);
|
||||
});
|
||||
connect(loader, &SceneLoader::dataSourceReady, this,
|
||||
[this](uint32_t mid) {
|
||||
applyCoordinateOperation(mid);
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromStream, this,
|
||||
[this](uint32_t mid, qint64 /*elapsed_ms*/) {
|
||||
applyCoordinateOperation(mid);
|
||||
applyModelVisibility(mid);
|
||||
maybeGuessFederatedFalseOrigin(mid);
|
||||
auto& settings = ifcviewerfull::ViewerSettings::instance();
|
||||
connect(&settings, &ifcviewerfull::ViewerSettings::themeChanged, this, [this]() {
|
||||
viewport_->setBackgroundColor(
|
||||
QColor(ifcviewerfull::ViewerSettings::instance().color("viewport_background")));
|
||||
});
|
||||
viewport_->setBackgroundColor(QColor(settings.color("viewport_background")));
|
||||
|
||||
connect(session_state_, &SessionState::projectReset, this, &ViewportView::refresh);
|
||||
connect(session_state_, &SessionState::projectOpened, this, [this](const QString&) { refresh(); });
|
||||
connect(session_state_, &SessionState::modelsChanged, this, &ViewportView::refresh);
|
||||
connect(session_state_, &SessionState::federationChanged, this, &ViewportView::refresh);
|
||||
connect(session_state_, &SessionState::visibilityChanged, this, &ViewportView::refresh);
|
||||
connect(session_state_, &SessionState::modelGeometryReady, this, [this](uint32_t) { refresh(); });
|
||||
|
||||
// Measurement tools — input-driven, share the View's lifetime.
|
||||
connect(viewport_, &ViewportWindow::surfacePickedInTool, this,
|
||||
[this](int x, int y, int modifiers) {
|
||||
const bool alt = (modifiers & Qt::AltModifier) != 0;
|
||||
@@ -148,11 +109,25 @@ ViewportController::ViewportController(ifcviewerfull::SessionState* session_stat
|
||||
connect(viewport_, &ViewportWindow::objectPicked, this, [this](uint32_t) {
|
||||
updateVolumeReadout();
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
ViewportController::~ViewportController() = default;
|
||||
ViewportView::~ViewportView() = default;
|
||||
|
||||
void ViewportController::applyCoordinateOperation(uint32_t mid) {
|
||||
void ViewportView::refresh() {
|
||||
Federation* federation = session_state_->federation();
|
||||
viewport_->setFederatedFalseOrigin(
|
||||
composeFederatedFalseOrigin(federation->federatedFalseOrigin(), federation->config()));
|
||||
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
applyCoordinateOperation(mid);
|
||||
applyModelVisibility(mid);
|
||||
maybeGuessFederatedFalseOrigin(mid);
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportView::applyCoordinateOperation(uint32_t mid) {
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
|
||||
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
||||
@@ -164,7 +139,7 @@ void ViewportController::applyCoordinateOperation(uint32_t mid) {
|
||||
applyModelTransformation(mid);
|
||||
}
|
||||
|
||||
void ViewportController::applyModelTransformation(uint32_t mid) {
|
||||
void ViewportView::applyModelTransformation(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
|
||||
@@ -186,7 +161,7 @@ void ViewportController::applyModelTransformation(uint32_t mid) {
|
||||
viewport_->setModelTransformation(mid, matrix);
|
||||
}
|
||||
|
||||
void ViewportController::applyModelVisibility(uint32_t mid) {
|
||||
void ViewportView::applyModelVisibility(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
const QString fed_id = session_state_->fedIdForModelId(mid);
|
||||
if (fed_id.isEmpty()) return;
|
||||
@@ -198,90 +173,27 @@ void ViewportController::applyModelVisibility(uint32_t mid) {
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportController::applyFederatedFalseOrigin() {
|
||||
void ViewportView::maybeGuessFederatedFalseOrigin(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
viewport_->setFederatedFalseOrigin(
|
||||
composeFederatedFalseOrigin(federation->federatedFalseOrigin(), federation->config()));
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
if (!federation->filePath().isEmpty()) return;
|
||||
|
||||
const FederatedFalseOrigin& current = federation->federatedFalseOrigin();
|
||||
const FederatedFalseOrigin defaults;
|
||||
if (current.xyz != defaults.xyz || current.rz_deg != defaults.rz_deg) return;
|
||||
|
||||
const Eigen::Matrix4d* placement = loader->firstPlacement(mid);
|
||||
const ModelGeoref* georef = loader->modelGeoref(mid);
|
||||
if (placement == nullptr || georef == nullptr) return;
|
||||
|
||||
federation->setFederatedFalseOrigin(guessFederatedFalseOrigin(
|
||||
*placement, *georef, federation->config()));
|
||||
// The federation mutation above will emit its own signal, but to keep
|
||||
// views off the Federation bus we re-emit through SessionState.
|
||||
session_state_->notifyFederationChanged();
|
||||
}
|
||||
|
||||
void ViewportController::setHomeView() {
|
||||
auto camera = viewport_->cameraState();
|
||||
Federation::HomeView home_view;
|
||||
home_view.target = camera.target;
|
||||
home_view.distance = camera.distance;
|
||||
home_view.yaw = camera.yaw;
|
||||
home_view.pitch = camera.pitch;
|
||||
session_state_->federation()->setHomeView(home_view);
|
||||
session_state_->setStatusMessage("Camera", "Home view updated");
|
||||
}
|
||||
|
||||
void ViewportController::goHomeView() {
|
||||
Federation* federation = session_state_->federation();
|
||||
if (!federation->hasHomeView()) {
|
||||
session_state_->setStatusMessage("Camera", "No home view set for this project");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& home_view = federation->homeView();
|
||||
viewport_->setCamera(
|
||||
home_view.target.x(), home_view.target.y(), home_view.target.z(),
|
||||
home_view.distance, home_view.yaw, home_view.pitch);
|
||||
session_state_->setStatusMessage("Camera", "Home view restored");
|
||||
}
|
||||
|
||||
void ViewportController::setFlyMode() {
|
||||
viewport_->requestActivate();
|
||||
viewport_->enterFpsMode();
|
||||
session_state_->setStatusMessage("Mode", "Fly mode active");
|
||||
}
|
||||
|
||||
void ViewportController::toggleSectionMode() {
|
||||
viewport_->toggleSectionTool();
|
||||
if (viewport_->sectionToolActive()) {
|
||||
session_state_->setStatusMessage("Section", "Section tool active");
|
||||
} else {
|
||||
session_state_->setStatusMessage("Section", "Section tool off");
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportController::clearSectionPlanes() {
|
||||
viewport_->clearSectionPlanes();
|
||||
session_state_->setStatusMessage("Section", "Section planes cleared");
|
||||
}
|
||||
|
||||
void ViewportController::toggleDistanceMode() {
|
||||
viewport_->toggleLengthTool();
|
||||
}
|
||||
|
||||
void ViewportController::toggleAreaMode() {
|
||||
viewport_->toggleAreaTool();
|
||||
}
|
||||
|
||||
void ViewportController::toggleVolumeMode() {
|
||||
viewport_->toggleVolumeTool();
|
||||
}
|
||||
|
||||
void ViewportController::focusSelectedObject() {
|
||||
viewport_->focusOnSelectedObject();
|
||||
}
|
||||
|
||||
void ViewportController::hideSelectedElements() {
|
||||
viewport_->hideSelectedElements();
|
||||
}
|
||||
|
||||
void ViewportController::isolateSelectedElements() {
|
||||
viewport_->isolateSelectedElements();
|
||||
}
|
||||
|
||||
void ViewportController::showAllElements() {
|
||||
viewport_->showAllElements();
|
||||
}
|
||||
|
||||
void ViewportController::invertSelection() {
|
||||
viewport_->invertElementVisibility();
|
||||
}
|
||||
|
||||
void ViewportController::updateVolumeReadout() {
|
||||
void ViewportView::updateVolumeReadout() {
|
||||
if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return;
|
||||
|
||||
const auto& sel = viewport_->selection().selectionIds();
|
||||
@@ -317,21 +229,4 @@ void ViewportController::updateVolumeReadout() {
|
||||
viewport_->setOverlayLabels(labels);
|
||||
}
|
||||
|
||||
void ViewportController::maybeGuessFederatedFalseOrigin(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
if (!federation->filePath().isEmpty()) return;
|
||||
|
||||
const FederatedFalseOrigin& current = federation->federatedFalseOrigin();
|
||||
const FederatedFalseOrigin defaults;
|
||||
if (current.xyz != defaults.xyz || current.rz_deg != defaults.rz_deg) return;
|
||||
|
||||
const Eigen::Matrix4d* placement = loader->firstPlacement(mid);
|
||||
const ModelGeoref* georef = loader->modelGeoref(mid);
|
||||
if (placement == nullptr || georef == nullptr) return;
|
||||
|
||||
federation->setFederatedFalseOrigin(guessFederatedFalseOrigin(
|
||||
*placement, *georef, federation->config()));
|
||||
}
|
||||
|
||||
} // namespace ifcviewerfull::modules::viewport
|
||||
+16
-22
@@ -18,8 +18,8 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_VIEWPORT_CONTROLLER_H
|
||||
#define IFCINTERFACE_PANELS_VIEWPORT_CONTROLLER_H
|
||||
#ifndef IFCINTERFACE_MODULES_VIEWPORT_VIEW_H
|
||||
#define IFCINTERFACE_MODULES_VIEWPORT_VIEW_H
|
||||
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
@@ -31,31 +31,25 @@ class LengthMeasurement;
|
||||
|
||||
namespace ifcviewerfull::modules::viewport {
|
||||
|
||||
class ViewportController : public QObject {
|
||||
// Renders SessionState into the OpenGL viewport. Subscribes to session-level
|
||||
// signals only; on each one it calls refresh() to re-derive viewport state
|
||||
// (false origin, per-model coord op + transformation + visibility) from
|
||||
// SessionState idempotently.
|
||||
//
|
||||
// Also owns the stateful measurement tools and subscribes to viewport input
|
||||
// events for them. That's a distinct concern from the state-render side but
|
||||
// kept here to avoid a second tiny QObject.
|
||||
class ViewportView : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ViewportController(ifcviewerfull::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent = nullptr);
|
||||
~ViewportController() override;
|
||||
|
||||
void applyFederatedFalseOrigin();
|
||||
void setHomeView();
|
||||
void goHomeView();
|
||||
void setFlyMode();
|
||||
void toggleSectionMode();
|
||||
void clearSectionPlanes();
|
||||
void toggleDistanceMode();
|
||||
void toggleAreaMode();
|
||||
void toggleVolumeMode();
|
||||
void focusSelectedObject();
|
||||
void hideSelectedElements();
|
||||
void isolateSelectedElements();
|
||||
void showAllElements();
|
||||
void invertSelection();
|
||||
explicit ViewportView(ifcviewerfull::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent = nullptr);
|
||||
~ViewportView() override;
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
void applyCoordinateOperation(uint32_t mid);
|
||||
void applyModelTransformation(uint32_t mid);
|
||||
void applyModelVisibility(uint32_t mid);
|
||||
Reference in New Issue
Block a user