Interface mockup 18

This commit is contained in:
Dion Moult
2026-05-15 17:08:08 +10:00
parent fed739847e
commit 22f25f0098
20 changed files with 727 additions and 684 deletions
@@ -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
@@ -1,71 +0,0 @@
// 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_PANELS_MODELSPANELCONTROLLER_H
#define IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H
#include "Types.h"
#include <QObject>
#include <QStringList>
class QWidget;
namespace ifcviewerfull { class SessionState; }
class ViewportWindow;
class SceneLoader;
namespace ifcviewerfull::modules::models {
class ModelsPanel;
class ModelsPanelController : public QObject {
Q_OBJECT
public:
explicit ModelsPanelController(QWidget* host,
ModelsPanel* widget,
ifcviewerfull::SessionState* session_state,
ViewportWindow* viewport,
QObject* parent = nullptr);
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();
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
#endif
+47 -74
View File
@@ -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));
+21 -10
View File
@@ -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;
+35 -14
View File
@@ -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
+5 -1
View File
@@ -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;