mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-12 14:33:28 +00:00
Rename IfcViewerFull to Bonsai Viewer
Directory src/ifcviewer-full -> src/bonsaiviewer, CMake target IfcViewerFull -> BonsaiViewer, namespace ifcviewerfull -> bonsaiviewer, QApplication / window titles / connector path now use the new brand. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// 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 "AddModelDialog.h"
|
||||
|
||||
#include "../../components/Dialog.h"
|
||||
#include "../../components/Buttons.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/Style.h"
|
||||
|
||||
#include <QEvent>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
class HoverDescriptionFilter : public QObject {
|
||||
public:
|
||||
HoverDescriptionFilter(QLabel* label, QString hover_text, QString default_text)
|
||||
: label_(label), hover_text_(std::move(hover_text)), default_text_(std::move(default_text)) {}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* watched, QEvent* event) override {
|
||||
Q_UNUSED(watched);
|
||||
if (event->type() == QEvent::Enter) {
|
||||
label_->setText(hover_text_);
|
||||
} else if (event->type() == QEvent::Leave) {
|
||||
label_->setText(default_text_);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
QLabel* label_ = nullptr;
|
||||
QString hover_text_;
|
||||
QString default_text_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
AddModelDialog::AddModelDialog(QWidget* parent)
|
||||
: components::Dialog(parent)
|
||||
{
|
||||
setObjectName("appDialog");
|
||||
setWindowTitle("Add Model");
|
||||
setModal(true);
|
||||
setupUi();
|
||||
}
|
||||
|
||||
void AddModelDialog::setupUi() {
|
||||
if (auto* root = qobject_cast<QVBoxLayout*>(layout())) {
|
||||
root->setSizeConstraint(QLayout::SetFixedSize);
|
||||
}
|
||||
|
||||
const QString default_description = "Choose what to add to the project";
|
||||
auto* description_section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
auto* description = new QLabel(default_description, description_section);
|
||||
description->setProperty("textRole", "secondary");
|
||||
description->setWordWrap(true);
|
||||
description->setAlignment(Qt::AlignCenter);
|
||||
description->setMinimumWidth((90 * 4) + (components::style::metrics::padding * 3));
|
||||
description->setMinimumHeight(description->fontMetrics().lineSpacing() * 2 + 4);
|
||||
description_section->addBodyWidget(description);
|
||||
|
||||
auto* choices_section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
auto* choices = new QWidget(choices_section);
|
||||
auto* row = new QHBoxLayout(choices);
|
||||
row->setContentsMargins(0, 0, 0, 0);
|
||||
row->setSpacing(components::style::metrics::padding);
|
||||
|
||||
auto* add_ifc = components::buttons::makeButton("Add IFC File", ":/icons/cube.svg", choices);
|
||||
connect(add_ifc, &QToolButton::clicked, this, [this]() {
|
||||
selected_mode_ = SourceMode::IfcFile;
|
||||
accept();
|
||||
});
|
||||
add_ifc->installEventFilter(new HoverDescriptionFilter(
|
||||
description,
|
||||
"Add IFC files and load both geometry and data.",
|
||||
default_description));
|
||||
|
||||
auto* add_database = components::buttons::makeButton("Add IFC\nDatabase", ":/icons/database.svg", choices);
|
||||
connect(add_database, &QToolButton::clicked, this, [this]() {
|
||||
selected_mode_ = SourceMode::IfcDatabase;
|
||||
accept();
|
||||
});
|
||||
add_database->installEventFilter(new HoverDescriptionFilter(
|
||||
description,
|
||||
"Add IFC RDB databases for optimised performance",
|
||||
default_description));
|
||||
|
||||
auto* add_geometry = components::buttons::makeButton("Add Geometry", ":/icons/cube-bandage.svg", choices);
|
||||
connect(add_geometry, &QToolButton::clicked, this, [this]() {
|
||||
selected_mode_ = SourceMode::GeometryOnly;
|
||||
accept();
|
||||
});
|
||||
add_geometry->installEventFilter(new HoverDescriptionFilter(
|
||||
description,
|
||||
"Add pure geometry for fast visualisation",
|
||||
default_description));
|
||||
|
||||
auto* add_cloud = components::buttons::makeButton("Add From\nCloud", ":/icons/cloud-square.svg", choices);
|
||||
connect(add_cloud, &QToolButton::clicked, this, [this]() {
|
||||
selected_mode_ = SourceMode::CloudModel;
|
||||
accept();
|
||||
});
|
||||
add_cloud->installEventFilter(new HoverDescriptionFilter(
|
||||
description,
|
||||
"Browse a cloud connector and add one or more models from there.",
|
||||
default_description));
|
||||
|
||||
auto* convert_database = components::buttons::makeButton("Convert IFC File\nto Database", ":/icons/database-restore.svg", choices);
|
||||
connect(convert_database, &QToolButton::clicked, this, [this]() {
|
||||
selected_mode_ = SourceMode::ConvertToDatabase;
|
||||
accept();
|
||||
});
|
||||
convert_database->installEventFilter(new HoverDescriptionFilter(
|
||||
description,
|
||||
"Convert IFC files to databases for smaller filesizes, reduced memory, and faster access. No data is lost.",
|
||||
default_description));
|
||||
|
||||
auto* export_geometry_database = components::buttons::makeButton("Export Geometry\nDatabase", ":/icons/database-restore.svg", choices);
|
||||
connect(export_geometry_database, &QToolButton::clicked, this, [this]() {
|
||||
selected_mode_ = SourceMode::ExportGeometryDatabase;
|
||||
accept();
|
||||
});
|
||||
export_geometry_database->installEventFilter(new HoverDescriptionFilter(
|
||||
description,
|
||||
"Convert IFC files to a read-only geometry database for smaller filesizes, reduced memory, and faster access. Ideal for cloud read-only coordination workflows. Only parametric geometry editing capabilities are lost.",
|
||||
default_description));
|
||||
|
||||
row->addWidget(components::buttons::makeButtonGroup("ADD", {add_ifc, add_database, add_geometry, add_cloud}, choices, true, 8));
|
||||
row->addWidget(components::buttons::makeButtonGroup("TOOLS", {convert_database, export_geometry_database}, choices, false, 8));
|
||||
choices_section->addBodyWidget(choices);
|
||||
|
||||
addBodyWidget(description_section);
|
||||
addBodyWidget(choices_section);
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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_ADDMODELDIALOG_H
|
||||
#define IFCINTERFACE_PANELS_ADDMODELDIALOG_H
|
||||
|
||||
#include "../../components/Dialog.h"
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
enum class SourceMode {
|
||||
None,
|
||||
IfcFile,
|
||||
IfcDatabase,
|
||||
GeometryOnly,
|
||||
CloudModel,
|
||||
ConvertToDatabase,
|
||||
ExportGeometryDatabase,
|
||||
};
|
||||
|
||||
class AddModelDialog : public components::Dialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AddModelDialog(QWidget* parent = nullptr);
|
||||
|
||||
SourceMode selectedMode() const { return selected_mode_; }
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
|
||||
SourceMode selected_mode_ = SourceMode::None;
|
||||
};
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,752 @@
|
||||
// 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 "AddModelDialog.h"
|
||||
#include "SettingsDialog.h"
|
||||
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../connectors/PickerDialog.h"
|
||||
#include "../connectors/Process.h"
|
||||
#include "../connectors/Registry.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/SidecarBuilder.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
#include "../../../ifcgeom/Serializer.h"
|
||||
#include "../../../serializers/document_serializer_plugin.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QInputDialog>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QMessageBox>
|
||||
#include <QPointer>
|
||||
#include <QStandardPaths>
|
||||
#include <QThread>
|
||||
#include <QTreeView>
|
||||
#include <QUuid>
|
||||
|
||||
#include <QtCore/private/qzipwriter_p.h>
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace bonsaiviewer::modules::models::commands {
|
||||
|
||||
namespace {
|
||||
|
||||
QString formatElapsed(qint64 ms) {
|
||||
return (ms >= 1000)
|
||||
? QString::number(ms / 1000.0, 'f', 2) + " s"
|
||||
: QString::number(ms) + " ms";
|
||||
}
|
||||
|
||||
} // 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 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 SourceMode::IfcFile: {
|
||||
QFileDialog file_dialog(&host, "Add IFC Files");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
file_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() == QDialog::Accepted) {
|
||||
paths = file_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SourceMode::IfcDatabase: {
|
||||
QFileDialog database_dialog(&host, "Add IFC Databases");
|
||||
database_dialog.setFileMode(QFileDialog::Directory);
|
||||
database_dialog.setOption(QFileDialog::ShowDirsOnly, true);
|
||||
database_dialog.setOption(QFileDialog::DontResolveSymlinks, true);
|
||||
database_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (auto* list = database_dialog.findChild<QListView*>("listView")) {
|
||||
list->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
if (auto* tree = database_dialog.findChild<QTreeView*>()) {
|
||||
tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
if (database_dialog.exec() == QDialog::Accepted) {
|
||||
paths = database_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SourceMode::GeometryOnly: {
|
||||
QFileDialog file_dialog(&host, "Add Geometry Only");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
file_dialog.setNameFilter("IFC Viewer Cache (*.ifcview);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() == QDialog::Accepted) {
|
||||
paths = file_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SourceMode::CloudModel:
|
||||
addModelFromCloud(s, host);
|
||||
return;
|
||||
case SourceMode::ConvertToDatabase:
|
||||
convertIfcToDatabase(s, host);
|
||||
return;
|
||||
case SourceMode::ExportGeometryDatabase:
|
||||
exportGeometryDatabase(s, host);
|
||||
return;
|
||||
case SourceMode::None:
|
||||
return;
|
||||
}
|
||||
|
||||
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 addModelFromCloud(SessionState& s, QWidget& host) {
|
||||
auto* registry = s.connectorRegistry();
|
||||
const auto& manifests = registry->available();
|
||||
if (manifests.empty()) {
|
||||
QMessageBox::information(&host, "Add From Cloud",
|
||||
"No connectors are installed.");
|
||||
return;
|
||||
}
|
||||
|
||||
modules::connectors::ConnectorPickerDialog picker(
|
||||
manifests, "Add From Cloud",
|
||||
"Pick a connector to browse models on.", &host);
|
||||
if (picker.exec() != QDialog::Accepted) return;
|
||||
const QString connector_id = picker.selectedId();
|
||||
if (connector_id.isEmpty()) return;
|
||||
|
||||
auto* proc = registry->get(connector_id);
|
||||
if (!proc) {
|
||||
QMessageBox::warning(&host, "Add From Cloud",
|
||||
QString("Could not launch connector '%1':\n%2")
|
||||
.arg(connector_id, registry->lastError()));
|
||||
return;
|
||||
}
|
||||
|
||||
s.setStatusMessage("Cloud", QString("Browsing %1...").arg(connector_id));
|
||||
|
||||
QPointer<SessionState> sguard(&s);
|
||||
|
||||
proc->call("pull_models_interactive", QJsonValue(),
|
||||
[sguard, connector_id](const QJsonValue& result) {
|
||||
if (!sguard) return;
|
||||
const QJsonArray arr = result.toArray();
|
||||
QStringList paths;
|
||||
QStringList fed_ids;
|
||||
int added = 0;
|
||||
for (const QJsonValue& v : arr) {
|
||||
if (v.isNull() || !v.isObject()) continue;
|
||||
const QJsonObject entry = v.toObject();
|
||||
const QString display_name = entry.value("display_name").toString();
|
||||
const QString path = entry.value("path").toString();
|
||||
if (path.isEmpty()) continue;
|
||||
const QJsonObject source = entry.value("source").toObject();
|
||||
QString src_connector = source.value("connector").toString();
|
||||
if (src_connector.isEmpty()) src_connector = connector_id;
|
||||
|
||||
const QString fed_id = sguard->federation()->addCloudModel(
|
||||
display_name, src_connector, source);
|
||||
if (fed_id.isEmpty()) continue;
|
||||
|
||||
const QJsonObject meta = entry.value("metadata").toObject();
|
||||
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
|
||||
|
||||
paths << path;
|
||||
fed_ids << fed_id;
|
||||
++added;
|
||||
}
|
||||
if (!paths.isEmpty()) {
|
||||
detail::loadModels(*sguard, paths, fed_ids);
|
||||
sguard->notifyModelsChanged();
|
||||
}
|
||||
sguard->setStatusMessage("Cloud",
|
||||
QString("Added %1 model(s) from %2").arg(added).arg(connector_id));
|
||||
},
|
||||
[sguard, connector_id](int code, const QString& message) {
|
||||
qWarning() << "pull_models_interactive from" << connector_id
|
||||
<< "failed:" << code << message;
|
||||
if (sguard) {
|
||||
sguard->setStatusMessage("Cloud",
|
||||
QString("%1 reported an error (see connector UI)").arg(connector_id));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Shared "local path on disk" lookup for the right-click cloud commands:
|
||||
// the loader keeps the path keyed by mid (set when a file or pull_models
|
||||
// path was queued). Both local-sourced and resolved cloud-sourced models
|
||||
// have one; only un-resolved cloud models (where pull_models hasn't
|
||||
// returned yet) won't.
|
||||
QString localPathForModel(SessionState& s, const QString& fed_id) {
|
||||
const uint32_t mid = s.modelIdForFedId(fed_id);
|
||||
if (mid == 0 || !s.loader()) return {};
|
||||
return s.loader()->filePath(mid);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void saveModelToCloud(SessionState& s, QWidget& host, const QString& fed_id) {
|
||||
auto* fed = s.federation();
|
||||
const Federation::Model* model = fed->findById(fed_id);
|
||||
if (!model) return;
|
||||
if (model->source_connector == "local") {
|
||||
QMessageBox::information(&host, "Save Model To Cloud",
|
||||
"This model has no cloud target. Use \"Save As To Cloud\" first.");
|
||||
return;
|
||||
}
|
||||
const QString local_path = localPathForModel(s, fed_id);
|
||||
if (local_path.isEmpty()) {
|
||||
QMessageBox::warning(&host, "Save Model To Cloud",
|
||||
"Cannot find a local copy of this model to push.");
|
||||
return;
|
||||
}
|
||||
const QString connector_id = model->source_connector;
|
||||
auto* registry = s.connectorRegistry();
|
||||
auto* proc = registry->get(connector_id);
|
||||
if (!proc) {
|
||||
QMessageBox::warning(&host, "Save Model To Cloud",
|
||||
QString("Could not launch connector '%1':\n%2")
|
||||
.arg(connector_id, registry->lastError()));
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject source = model->source_data;
|
||||
source["connector"] = connector_id;
|
||||
QJsonObject params;
|
||||
params["path"] = local_path;
|
||||
params["source"] = source;
|
||||
|
||||
s.setStatusMessage("Cloud",
|
||||
QString("Saving %1 to %2...").arg(model->display_name, connector_id));
|
||||
|
||||
QPointer<SessionState> sguard(&s);
|
||||
proc->call("push_model", params,
|
||||
[sguard, fed_id, connector_id](const QJsonValue& result) {
|
||||
if (!sguard) return;
|
||||
const QJsonObject obj = result.toObject();
|
||||
const QJsonObject new_source = obj.value("source").toObject();
|
||||
QString new_connector = new_source.value("connector").toString();
|
||||
if (new_connector.isEmpty()) new_connector = connector_id;
|
||||
sguard->federation()->setModelSource(fed_id, new_connector, new_source);
|
||||
const QJsonObject meta = obj.value("metadata").toObject();
|
||||
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
|
||||
sguard->setStatusMessage("Cloud",
|
||||
QString("Saved to %1").arg(new_connector));
|
||||
},
|
||||
[sguard, connector_id](int code, const QString& message) {
|
||||
qWarning() << "push_model to" << connector_id
|
||||
<< "failed:" << code << message;
|
||||
if (sguard) {
|
||||
sguard->setStatusMessage("Cloud",
|
||||
QString("%1 reported an error (see connector UI)").arg(connector_id));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void saveModelAsToCloud(SessionState& s, QWidget& host, const QString& fed_id) {
|
||||
const Federation::Model* model = s.federation()->findById(fed_id);
|
||||
if (!model) return;
|
||||
const QString local_path = localPathForModel(s, fed_id);
|
||||
if (local_path.isEmpty()) {
|
||||
QMessageBox::warning(&host, "Save Model As To Cloud",
|
||||
"Cannot find a local copy of this model to push.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto* registry = s.connectorRegistry();
|
||||
const auto& manifests = registry->available();
|
||||
if (manifests.empty()) {
|
||||
QMessageBox::information(&host, "Save Model As To Cloud",
|
||||
"No connectors are installed.");
|
||||
return;
|
||||
}
|
||||
|
||||
modules::connectors::ConnectorPickerDialog picker(
|
||||
manifests, "Save Model As To Cloud",
|
||||
QString("Pick a connector to push '%1' to.").arg(model->display_name),
|
||||
&host);
|
||||
if (picker.exec() != QDialog::Accepted) return;
|
||||
const QString connector_id = picker.selectedId();
|
||||
if (connector_id.isEmpty()) return;
|
||||
|
||||
auto* proc = registry->get(connector_id);
|
||||
if (!proc) {
|
||||
QMessageBox::warning(&host, "Save Model As To Cloud",
|
||||
QString("Could not launch connector '%1':\n%2")
|
||||
.arg(connector_id, registry->lastError()));
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject params;
|
||||
params["path"] = local_path;
|
||||
|
||||
s.setStatusMessage("Cloud",
|
||||
QString("Pushing %1 to %2...").arg(model->display_name, connector_id));
|
||||
|
||||
QPointer<SessionState> sguard(&s);
|
||||
proc->call("push_model_interactive", params,
|
||||
[sguard, fed_id, connector_id](const QJsonValue& result) {
|
||||
if (!sguard) return;
|
||||
const QJsonObject obj = result.toObject();
|
||||
const QJsonObject new_source = obj.value("source").toObject();
|
||||
QString new_connector = new_source.value("connector").toString();
|
||||
if (new_connector.isEmpty()) new_connector = connector_id;
|
||||
sguard->federation()->setModelSource(fed_id, new_connector, new_source);
|
||||
|
||||
const QString new_name = obj.value("display_name").toString();
|
||||
if (!new_name.isEmpty()) {
|
||||
sguard->federation()->setModelDisplayName(fed_id, new_name);
|
||||
}
|
||||
const QJsonObject meta = obj.value("metadata").toObject();
|
||||
sguard->setCloudMetadata(fed_id, meta.toVariantMap());
|
||||
sguard->setStatusMessage("Cloud",
|
||||
QString("Pushed to %1").arg(new_connector));
|
||||
},
|
||||
[sguard, connector_id](int code, const QString& message) {
|
||||
qWarning() << "push_model_interactive to" << connector_id
|
||||
<< "failed:" << code << message;
|
||||
if (sguard) {
|
||||
sguard->setStatusMessage("Cloud",
|
||||
QString("%1 reported an error (see connector UI)").arg(connector_id));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
if (input_dialog.exec() != QDialog::Accepted) return;
|
||||
const QStringList inputs = input_dialog.selectedFiles();
|
||||
if (inputs.isEmpty()) return;
|
||||
const QString input_path = inputs.first();
|
||||
|
||||
const QFileInfo input_info(input_path);
|
||||
const QString default_output = input_info.absoluteDir().filePath(input_info.completeBaseName() + ".rdb");
|
||||
|
||||
QFileDialog output_dialog(&host, "Save IFC Database As");
|
||||
output_dialog.setAcceptMode(QFileDialog::AcceptSave);
|
||||
output_dialog.setFileMode(QFileDialog::AnyFile);
|
||||
output_dialog.setNameFilter("IFC Database (*.rdb);;All Files (*)");
|
||||
output_dialog.setDefaultSuffix("rdb");
|
||||
output_dialog.selectFile(default_output);
|
||||
output_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
output_dialog.setOption(QFileDialog::DontConfirmOverwrite, true);
|
||||
if (output_dialog.exec() != QDialog::Accepted) return;
|
||||
const QStringList outputs = output_dialog.selectedFiles();
|
||||
if (outputs.isEmpty()) return;
|
||||
QString output_path = outputs.first();
|
||||
if (!output_path.endsWith(".rdb", Qt::CaseInsensitive)) {
|
||||
output_path += ".rdb";
|
||||
}
|
||||
|
||||
const QFileInfo output_info(output_path);
|
||||
if (output_info.exists()) {
|
||||
const QString message = QString("'%1' already exists. Overwrite?").arg(output_info.fileName());
|
||||
if (QMessageBox::question(&host, "Convert IFC to Database", message,
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
s.beginProgress(QString("Converting %1 to %2…")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
QFileInfo(output_path).fileName()));
|
||||
s.setStatusMessage("Converting",
|
||||
QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName()));
|
||||
|
||||
auto timer = std::make_shared<QElapsedTimer>();
|
||||
timer->start();
|
||||
auto error_message = std::make_shared<QString>();
|
||||
|
||||
QThread* thread = QThread::create([input_path, output_path, error_message]() {
|
||||
try {
|
||||
ifcopenshell::serializers::document_serializer_context context;
|
||||
context.file = nullptr;
|
||||
context.input_filename = input_path.toStdString();
|
||||
context.output_filename = output_path.toStdString();
|
||||
context.stream = true;
|
||||
|
||||
auto& registry = ifcopenshell::serializers::document_serializer_registry_instance();
|
||||
const auto* info = registry.find("rdb");
|
||||
if (!info) {
|
||||
throw ifcopenshell::exception(
|
||||
"No 'rdb' document serializer is registered. The RocksDB serializer plugin may not be installed.");
|
||||
}
|
||||
if (!info->supports_input_filename) {
|
||||
throw ifcopenshell::exception("RDB serializer does not support streaming from an input filename");
|
||||
}
|
||||
|
||||
boost::shared_ptr<Serializer> serializer = registry.create("rdb", context);
|
||||
serializer->finalize();
|
||||
} catch (const std::exception& e) {
|
||||
*error_message = QString::fromUtf8(e.what());
|
||||
} catch (...) {
|
||||
*error_message = "Unknown error during IFC to RDB conversion";
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(thread, &QThread::finished, &host,
|
||||
[&s, host_ptr = &host, thread, timer, error_message, input_path, output_path]() {
|
||||
const qint64 elapsed = timer->elapsed();
|
||||
|
||||
s.endProgress();
|
||||
thread->deleteLater();
|
||||
|
||||
if (!error_message->isEmpty()) {
|
||||
s.setStatusMessage("Error", *error_message);
|
||||
QMessageBox::warning(host_ptr, "Convert IFC to Database",
|
||||
QString("Conversion failed:\n%1").arg(*error_message));
|
||||
return;
|
||||
}
|
||||
|
||||
s.setStatusMessage(
|
||||
"Converted",
|
||||
QString("%1 → %2 in %3")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
QFileInfo(output_path).fileName(),
|
||||
formatElapsed(elapsed)));
|
||||
QMessageBox::information(host_ptr, "Convert IFC to Database",
|
||||
QString("Database written to:\n%1").arg(output_path));
|
||||
});
|
||||
|
||||
thread->start();
|
||||
}
|
||||
|
||||
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);
|
||||
if (input_dialog.exec() != QDialog::Accepted) return;
|
||||
const QStringList inputs = input_dialog.selectedFiles();
|
||||
if (inputs.isEmpty()) return;
|
||||
const QString input_path = inputs.first();
|
||||
|
||||
const QFileInfo input_info(input_path);
|
||||
const QString default_output = input_info.absoluteDir().filePath(input_info.completeBaseName() + ".rdbview");
|
||||
|
||||
QFileDialog output_dialog(&host, "Save Geometry Database As");
|
||||
output_dialog.setAcceptMode(QFileDialog::AcceptSave);
|
||||
output_dialog.setFileMode(QFileDialog::AnyFile);
|
||||
output_dialog.setNameFilter("Geometry Database (*.rdbview);;All Files (*)");
|
||||
output_dialog.setDefaultSuffix("rdbview");
|
||||
output_dialog.selectFile(default_output);
|
||||
output_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (output_dialog.exec() != QDialog::Accepted) return;
|
||||
const QStringList outputs = output_dialog.selectedFiles();
|
||||
if (outputs.isEmpty()) return;
|
||||
QString output_path = outputs.first();
|
||||
if (!output_path.endsWith(".rdbview", Qt::CaseInsensitive)) {
|
||||
output_path += ".rdbview";
|
||||
}
|
||||
|
||||
s.beginProgress(QString("Exporting %1 to %2…")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
QFileInfo(output_path).fileName()));
|
||||
s.setStatusMessage("Exporting",
|
||||
QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName()));
|
||||
|
||||
auto timer = std::make_shared<QElapsedTimer>();
|
||||
timer->start();
|
||||
auto error_message = std::make_shared<QString>();
|
||||
|
||||
QThread* thread = QThread::create([input_path, output_path, error_message]() {
|
||||
// Scratch dir holds the intermediate .ifcview and .rdb directory
|
||||
// until they're zipped into the .rdbview. RAII-like cleanup at the
|
||||
// bottom of this lambda; on early exception we leak it (cheap
|
||||
// tradeoff to keep the failure log around for the user).
|
||||
const QString tmp_root = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation))
|
||||
.filePath(QString("ifcviewer-export-%1")
|
||||
.arg(QUuid::createUuid().toString(QUuid::Id128)));
|
||||
QDir().mkpath(tmp_root);
|
||||
|
||||
const QString tmp_anchor = QDir(tmp_root).filePath("model.ifc");
|
||||
const QString tmp_sidecar = QDir(tmp_root).filePath("model.ifcview");
|
||||
const QString tmp_rdb_dir = QDir(tmp_root).filePath("model.rdb");
|
||||
|
||||
try {
|
||||
ifcopenshell::serializers::document_serializer_context context;
|
||||
context.file = nullptr;
|
||||
context.input_filename = input_path.toStdString();
|
||||
context.output_filename = tmp_rdb_dir.toStdString();
|
||||
context.stream = true;
|
||||
context.skip_supertypes = { "IfcRepresentationItem" };
|
||||
|
||||
auto& registry = ifcopenshell::serializers::document_serializer_registry_instance();
|
||||
const auto* info = registry.find("rdb");
|
||||
if (!info) {
|
||||
throw ifcopenshell::exception(
|
||||
"No 'rdb' document serializer is registered. The RocksDB serializer plugin may not be installed.");
|
||||
}
|
||||
if (!info->supports_input_filename) {
|
||||
throw ifcopenshell::exception("RDB serializer does not support streaming from an input filename");
|
||||
}
|
||||
|
||||
boost::shared_ptr<Serializer> serializer = registry.create("rdb", context);
|
||||
serializer->finalize();
|
||||
serializer.reset();
|
||||
|
||||
SidecarBuilder builder;
|
||||
if (!builder.build(input_path, tmp_anchor)) {
|
||||
throw ifcopenshell::exception(
|
||||
("Sidecar build failed: " + builder.lastError()).toStdString());
|
||||
}
|
||||
if (!QFileInfo::exists(tmp_sidecar)) {
|
||||
throw ifcopenshell::exception(
|
||||
("Sidecar build reported success but " + tmp_sidecar + " is missing").toStdString());
|
||||
}
|
||||
|
||||
// Write to a sibling `.tmp` then rename so a partial file never
|
||||
// appears at the destination (matters for cloud-sync folders).
|
||||
const QString tmp_zip = output_path + ".tmp";
|
||||
QFile::remove(tmp_zip);
|
||||
{
|
||||
QZipWriter writer(tmp_zip);
|
||||
if (writer.status() != QZipWriter::NoError) {
|
||||
throw ifcopenshell::exception(
|
||||
("Failed to open " + tmp_zip + " for writing").toStdString());
|
||||
}
|
||||
writer.setCompressionPolicy(QZipWriter::AutoCompress);
|
||||
|
||||
{
|
||||
QFile sf(tmp_sidecar);
|
||||
if (!sf.open(QIODevice::ReadOnly)) {
|
||||
throw ifcopenshell::exception(
|
||||
("Failed to read sidecar " + tmp_sidecar).toStdString());
|
||||
}
|
||||
writer.addFile("model.ifcview", sf.readAll());
|
||||
}
|
||||
|
||||
QDirIterator it(tmp_rdb_dir, QDir::Files | QDir::NoDotAndDotDot,
|
||||
QDirIterator::Subdirectories);
|
||||
const QDir rdb_root(tmp_rdb_dir);
|
||||
while (it.hasNext()) {
|
||||
const QString file_path = it.next();
|
||||
const QString rel = rdb_root.relativeFilePath(file_path);
|
||||
QFile f(file_path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
throw ifcopenshell::exception(
|
||||
("Failed to read " + file_path + " for zip").toStdString());
|
||||
}
|
||||
writer.addFile(QString("model.rdb/%1").arg(rel), f.readAll());
|
||||
}
|
||||
|
||||
writer.close();
|
||||
if (writer.status() != QZipWriter::NoError) {
|
||||
throw ifcopenshell::exception(
|
||||
("Failed to finalize " + tmp_zip).toStdString());
|
||||
}
|
||||
}
|
||||
|
||||
QFile::remove(output_path);
|
||||
if (!QFile::rename(tmp_zip, output_path)) {
|
||||
QFile::remove(tmp_zip);
|
||||
throw ifcopenshell::exception(
|
||||
("Failed to move " + tmp_zip + " to " + output_path).toStdString());
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
*error_message = QString::fromUtf8(e.what());
|
||||
} catch (...) {
|
||||
*error_message = "Unknown error during geometry database export";
|
||||
}
|
||||
|
||||
QDir(tmp_root).removeRecursively();
|
||||
});
|
||||
|
||||
QObject::connect(thread, &QThread::finished, &host,
|
||||
[&s, host_ptr = &host, thread, timer, error_message, input_path, output_path]() {
|
||||
const qint64 elapsed = timer->elapsed();
|
||||
|
||||
s.endProgress();
|
||||
thread->deleteLater();
|
||||
|
||||
if (!error_message->isEmpty()) {
|
||||
s.setStatusMessage("Error", *error_message);
|
||||
QMessageBox::warning(host_ptr, "Export Geometry Database",
|
||||
QString("Export failed:\n%1").arg(*error_message));
|
||||
return;
|
||||
}
|
||||
|
||||
s.setStatusMessage(
|
||||
"Exported",
|
||||
QString("%1 → %2 in %3")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
QFileInfo(output_path).fileName(),
|
||||
formatElapsed(elapsed)));
|
||||
QMessageBox::information(host_ptr, "Export Geometry Database",
|
||||
QString("Geometry database written to:\n%1").arg(output_path));
|
||||
});
|
||||
|
||||
thread->start();
|
||||
}
|
||||
|
||||
void openSettings(SessionState& s, QWidget& host) {
|
||||
SettingsDialog dialog(&s, &host);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models::commands
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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 bonsaiviewer { class SessionState; }
|
||||
|
||||
namespace bonsaiviewer::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);
|
||||
// Connector picker → pull_models_interactive → addCloudModel + load.
|
||||
// Reachable from AddModelDialog's CloudModel button; the underlying call
|
||||
// is async, so addModelFromCloud returns immediately after kicking it off.
|
||||
void addModelFromCloud(SessionState& s, QWidget& host);
|
||||
// push_model: push a cloud-sourced model back to its existing target.
|
||||
// Only valid when model.source_connector != "local". Async.
|
||||
void saveModelToCloud(SessionState& s, QWidget& host, const QString& fed_id);
|
||||
// push_model_interactive: pick a connector and push to a fresh cloud
|
||||
// target. Valid for any model (local or already cloud-sourced). Async.
|
||||
void saveModelAsToCloud(SessionState& s, QWidget& host, const QString& fed_id);
|
||||
void convertIfcToDatabase(SessionState& s, QWidget& host);
|
||||
void exportGeometryDatabase(SessionState& s, QWidget& host);
|
||||
void openSettings(SessionState& s, QWidget& host);
|
||||
|
||||
// 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 bonsaiviewer::modules::models::commands
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,274 @@
|
||||
// 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 "FederationItemModel.h"
|
||||
|
||||
#include "../../ViewerSettings.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QColor>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
QStandardItem* siblingVisibilityItem(QStandardItem* name_item) {
|
||||
QStandardItem* parent = name_item->parent();
|
||||
if (!parent) parent = name_item->model()->invisibleRootItem();
|
||||
return parent->child(name_item->row(), 1);
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
void walkSubtree(QStandardItem* root, F visit) {
|
||||
visit(root);
|
||||
for (int i = 0; i < root->rowCount(); ++i) {
|
||||
walkSubtree(root->child(i, 0), visit);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FederationItemModel::FederationItemModel(Federation* federation, QObject* parent)
|
||||
: QStandardItemModel(parent)
|
||||
, federation_(federation)
|
||||
{
|
||||
setColumnCount(2);
|
||||
rebuildAll();
|
||||
|
||||
connect(federation_, &Federation::groupAdded, this, &FederationItemModel::onGroupAdded);
|
||||
connect(federation_, &Federation::groupRemoved, this, &FederationItemModel::onGroupRemoved);
|
||||
connect(federation_, &Federation::groupChanged, this, &FederationItemModel::onGroupChanged);
|
||||
connect(federation_, &Federation::groupVisibilityChanged, this, &FederationItemModel::onGroupVisibilityChanged);
|
||||
connect(federation_, &Federation::modelAdded, this, &FederationItemModel::onModelAdded);
|
||||
connect(federation_, &Federation::modelRemoved, this, &FederationItemModel::onModelRemoved);
|
||||
connect(federation_, &Federation::modelVisibilityChanged, this, &FederationItemModel::onModelVisibilityChanged);
|
||||
connect(federation_, &Federation::modelGroupChanged, this, &FederationItemModel::onModelGroupChanged);
|
||||
connect(federation_, &Federation::modelChanged, this, &FederationItemModel::onModelChanged);
|
||||
}
|
||||
|
||||
void FederationItemModel::rebuildAll() {
|
||||
clear();
|
||||
setColumnCount(2);
|
||||
id_to_name_item_.clear();
|
||||
|
||||
for (const auto& root_group : federation_->rootGroups()) {
|
||||
appendGroupSubtreeTo(invisibleRootItem(), root_group->id);
|
||||
}
|
||||
for (const auto& model : federation_->models()) {
|
||||
if (!model.group_id.isEmpty()) continue;
|
||||
appendModelTo(invisibleRootItem(), model.id);
|
||||
}
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::makeGroupNameItem(const QString& group_id, const QString& display_name) const {
|
||||
auto* item = new QStandardItem(components::icons::makeSvgIcon(":/icons/folder.svg"), display_name);
|
||||
item->setData(group_id, IdRole);
|
||||
item->setData(int(ItemKind::Group), KindRole);
|
||||
item->setEditable(false);
|
||||
return item;
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::makeModelNameItem(const QString& fed_id, const QString& display_name) const {
|
||||
auto* item = new QStandardItem(components::icons::makeSvgIcon(":/icons/cube.svg"), display_name);
|
||||
item->setData(fed_id, IdRole);
|
||||
item->setData(int(ItemKind::Model), KindRole);
|
||||
item->setEditable(false);
|
||||
return item;
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::makeVisibilityItem(ItemKind kind, bool visible) const {
|
||||
QString icon_path;
|
||||
if (kind == ItemKind::Group) {
|
||||
icon_path = visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg";
|
||||
} else {
|
||||
icon_path = visible ? ":/icons/eye-solid.svg" : ":/icons/eye-closed.svg";
|
||||
}
|
||||
auto* item = new QStandardItem(components::icons::makeSvgIcon(icon_path), QString());
|
||||
item->setEditable(false);
|
||||
return item;
|
||||
}
|
||||
|
||||
void FederationItemModel::styleRowVisibility(QStandardItem* name_item, bool visible) const {
|
||||
QStandardItem* vis_item = siblingVisibilityItem(name_item);
|
||||
if (visible) {
|
||||
name_item->setData(QVariant(), Qt::ForegroundRole);
|
||||
if (vis_item) vis_item->setData(QVariant(), Qt::ForegroundRole);
|
||||
} else {
|
||||
const QBrush disabled(QColor(bonsaiviewer::ViewerSettings::instance().color("disabled_text")));
|
||||
name_item->setForeground(disabled);
|
||||
if (vis_item) vis_item->setForeground(disabled);
|
||||
}
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::findItem(const QString& id) const {
|
||||
return id_to_name_item_.value(id, nullptr);
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::parentItemForGroup(const QString& parent_group_id) const {
|
||||
if (parent_group_id.isEmpty()) return invisibleRootItem();
|
||||
QStandardItem* found = findItem(parent_group_id);
|
||||
return found ? found : invisibleRootItem();
|
||||
}
|
||||
|
||||
void FederationItemModel::appendModelTo(QStandardItem* parent_item, const QString& fed_id) {
|
||||
const Federation::Model* model = federation_->findById(fed_id);
|
||||
if (!model) return;
|
||||
auto* name_item = makeModelNameItem(fed_id, model->display_name);
|
||||
auto* vis_item = makeVisibilityItem(ItemKind::Model, federation_->isModelEffectivelyVisible(fed_id));
|
||||
parent_item->appendRow({name_item, vis_item});
|
||||
id_to_name_item_.insert(fed_id, name_item);
|
||||
styleRowVisibility(name_item, federation_->isModelEffectivelyVisible(fed_id));
|
||||
}
|
||||
|
||||
void FederationItemModel::appendGroupSubtreeTo(QStandardItem* parent_item, const QString& group_id) {
|
||||
const Federation::Group* group = federation_->findGroupById(group_id);
|
||||
if (!group) return;
|
||||
auto* name_item = makeGroupNameItem(group_id, group->display_name);
|
||||
auto* vis_item = makeVisibilityItem(ItemKind::Group, group->visible);
|
||||
parent_item->appendRow({name_item, vis_item});
|
||||
id_to_name_item_.insert(group_id, name_item);
|
||||
styleRowVisibility(name_item, group->visible);
|
||||
|
||||
for (const auto& child : group->children) {
|
||||
appendGroupSubtreeTo(name_item, child->id);
|
||||
}
|
||||
for (const auto& model : federation_->models()) {
|
||||
if (model.group_id != group_id) continue;
|
||||
appendModelTo(name_item, model.id);
|
||||
}
|
||||
}
|
||||
|
||||
void FederationItemModel::refreshSubtreeVisibility(QStandardItem* root) {
|
||||
walkSubtree(root, [this](QStandardItem* item) {
|
||||
const QString id = item->data(IdRole).toString();
|
||||
if (id.isEmpty()) return;
|
||||
const auto kind = static_cast<ItemKind>(item->data(KindRole).toInt());
|
||||
bool visible = true;
|
||||
if (kind == ItemKind::Group) {
|
||||
const Federation::Group* g = federation_->findGroupById(id);
|
||||
visible = g && g->visible;
|
||||
} else {
|
||||
visible = federation_->isModelEffectivelyVisible(id);
|
||||
}
|
||||
QStandardItem* vis_item = siblingVisibilityItem(item);
|
||||
if (vis_item) {
|
||||
const QString icon_path = (kind == ItemKind::Group)
|
||||
? (visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg")
|
||||
: (visible ? ":/icons/eye-solid.svg" : ":/icons/eye-closed.svg");
|
||||
vis_item->setIcon(components::icons::makeSvgIcon(icon_path));
|
||||
}
|
||||
styleRowVisibility(item, visible);
|
||||
});
|
||||
}
|
||||
|
||||
void FederationItemModel::onGroupAdded(const QString& group_id) {
|
||||
const Federation::Group* group = federation_->findGroupById(group_id);
|
||||
if (!group) return;
|
||||
QStandardItem* parent_item = parentItemForGroup(group->parent ? group->parent->id : QString());
|
||||
appendGroupSubtreeTo(parent_item, group_id);
|
||||
}
|
||||
|
||||
void FederationItemModel::onGroupRemoved(const QString& group_id) {
|
||||
QStandardItem* item = findItem(group_id);
|
||||
if (!item) return;
|
||||
walkSubtree(item, [this](QStandardItem* descendant) {
|
||||
const QString id = descendant->data(IdRole).toString();
|
||||
if (!id.isEmpty()) id_to_name_item_.remove(id);
|
||||
});
|
||||
QStandardItem* parent_item = item->parent();
|
||||
if (!parent_item) parent_item = invisibleRootItem();
|
||||
parent_item->removeRow(item->row());
|
||||
}
|
||||
|
||||
void FederationItemModel::onGroupChanged(const QString& group_id) {
|
||||
QStandardItem* item = findItem(group_id);
|
||||
if (!item) return;
|
||||
const Federation::Group* group = federation_->findGroupById(group_id);
|
||||
if (!group) return;
|
||||
|
||||
QStandardItem* current_parent = item->parent();
|
||||
if (!current_parent) current_parent = invisibleRootItem();
|
||||
QStandardItem* target_parent = parentItemForGroup(group->parent ? group->parent->id : QString());
|
||||
|
||||
if (current_parent == target_parent) {
|
||||
item->setText(group->display_name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reparent: take row from current parent, append at target. Pointers
|
||||
// survive — id_to_name_item_ entries remain valid.
|
||||
QList<QStandardItem*> taken = current_parent->takeRow(item->row());
|
||||
taken.first()->setText(group->display_name);
|
||||
target_parent->appendRow(taken);
|
||||
refreshSubtreeVisibility(taken.first());
|
||||
}
|
||||
|
||||
void FederationItemModel::onGroupVisibilityChanged(const QString& group_id, bool /*visible*/) {
|
||||
QStandardItem* item = findItem(group_id);
|
||||
if (!item) return;
|
||||
refreshSubtreeVisibility(item);
|
||||
}
|
||||
|
||||
void FederationItemModel::onModelAdded(const QString& fed_id) {
|
||||
const Federation::Model* model = federation_->findById(fed_id);
|
||||
if (!model) return;
|
||||
QStandardItem* parent_item = parentItemForGroup(model->group_id);
|
||||
appendModelTo(parent_item, fed_id);
|
||||
}
|
||||
|
||||
void FederationItemModel::onModelRemoved(const QString& fed_id) {
|
||||
QStandardItem* item = findItem(fed_id);
|
||||
if (!item) return;
|
||||
id_to_name_item_.remove(fed_id);
|
||||
QStandardItem* parent_item = item->parent();
|
||||
if (!parent_item) parent_item = invisibleRootItem();
|
||||
parent_item->removeRow(item->row());
|
||||
}
|
||||
|
||||
void FederationItemModel::onModelVisibilityChanged(const QString& fed_id, bool /*visible*/) {
|
||||
QStandardItem* item = findItem(fed_id);
|
||||
if (!item) return;
|
||||
refreshSubtreeVisibility(item);
|
||||
}
|
||||
|
||||
void FederationItemModel::onModelChanged(const QString& fed_id) {
|
||||
QStandardItem* item = findItem(fed_id);
|
||||
if (!item) return;
|
||||
const Federation::Model* model = federation_->findById(fed_id);
|
||||
if (!model) return;
|
||||
item->setText(model->display_name);
|
||||
}
|
||||
|
||||
void FederationItemModel::onModelGroupChanged(const QString& fed_id, const QString& new_group_id) {
|
||||
QStandardItem* item = findItem(fed_id);
|
||||
if (!item) return;
|
||||
QStandardItem* current_parent = item->parent();
|
||||
if (!current_parent) current_parent = invisibleRootItem();
|
||||
QStandardItem* target_parent = parentItemForGroup(new_group_id);
|
||||
if (current_parent == target_parent) return;
|
||||
|
||||
QList<QStandardItem*> taken = current_parent->takeRow(item->row());
|
||||
target_parent->appendRow(taken);
|
||||
refreshSubtreeVisibility(taken.first());
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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_FEDERATIONITEMMODEL_H
|
||||
#define IFCINTERFACE_MODULES_MODELS_FEDERATIONITEMMODEL_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
class Federation;
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
// QStandardItemModel that mirrors the Federation tree (groups + models in
|
||||
// two columns: name + visibility icon). Subscribes directly to Federation's
|
||||
// granular signals so each mutation only touches the affected rows — view
|
||||
// state (expansion, selection, scroll) is preserved automatically.
|
||||
//
|
||||
// Coarse session events (project open/reset, theme change) are not the
|
||||
// model's concern: the owning View calls rebuildAll() in those cases.
|
||||
class FederationItemModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Role {
|
||||
IdRole = Qt::UserRole + 1,
|
||||
KindRole = Qt::UserRole + 2,
|
||||
};
|
||||
|
||||
explicit FederationItemModel(Federation* federation, QObject* parent = nullptr);
|
||||
|
||||
// Discard everything and rebuild from current Federation state. Loses
|
||||
// expansion/selection — caller is the only one that knows whether that's
|
||||
// acceptable (e.g. project reset, where there's no prior state worth
|
||||
// preserving anyway).
|
||||
void rebuildAll();
|
||||
|
||||
private slots:
|
||||
void onGroupAdded(const QString& group_id);
|
||||
void onGroupRemoved(const QString& group_id);
|
||||
void onGroupChanged(const QString& group_id);
|
||||
void onGroupVisibilityChanged(const QString& group_id, bool visible);
|
||||
void onModelAdded(const QString& fed_id);
|
||||
void onModelRemoved(const QString& fed_id);
|
||||
void onModelVisibilityChanged(const QString& fed_id, bool visible);
|
||||
void onModelGroupChanged(const QString& fed_id, const QString& new_group_id);
|
||||
void onModelChanged(const QString& fed_id);
|
||||
|
||||
private:
|
||||
QStandardItem* makeGroupNameItem(const QString& group_id, const QString& display_name) const;
|
||||
QStandardItem* makeModelNameItem(const QString& fed_id, const QString& display_name) const;
|
||||
QStandardItem* makeVisibilityItem(ItemKind kind, bool visible) const;
|
||||
void styleRowVisibility(QStandardItem* name_item, bool visible) const;
|
||||
|
||||
QStandardItem* findItem(const QString& id) const;
|
||||
QStandardItem* parentItemForGroup(const QString& parent_group_id) const;
|
||||
|
||||
void appendModelTo(QStandardItem* parent_item, const QString& fed_id);
|
||||
void appendGroupSubtreeTo(QStandardItem* parent_item, const QString& group_id);
|
||||
void refreshSubtreeVisibility(QStandardItem* root);
|
||||
|
||||
Federation* federation_ = nullptr;
|
||||
QHash<QString, QStandardItem*> id_to_name_item_; // both group_ids and fed_ids
|
||||
};
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,395 @@
|
||||
// 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 "Panel.h"
|
||||
|
||||
#include "Commands.h"
|
||||
#include "FederationItemModel.h"
|
||||
#include "View.h"
|
||||
|
||||
#include "../../SessionState.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDrag>
|
||||
#include <QDragEnterEvent>
|
||||
#include <QDragMoveEvent>
|
||||
#include <QDropEvent>
|
||||
#include <QHeaderView>
|
||||
#include <QMenu>
|
||||
#include <QMimeData>
|
||||
#include <QSizePolicy>
|
||||
#include <QTreeView>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kDragMimeType = "application/x-bonsaiviewer-model-items";
|
||||
|
||||
QString idOf(const QModelIndex& index) {
|
||||
return index.sibling(index.row(), 0).data(FederationItemModel::IdRole).toString();
|
||||
}
|
||||
|
||||
ItemKind kindOf(const QModelIndex& index) {
|
||||
return static_cast<ItemKind>(
|
||||
index.sibling(index.row(), 0).data(FederationItemModel::KindRole).toInt());
|
||||
}
|
||||
|
||||
QStringList selectedModelIdsAt(QTreeView* tree, const QModelIndex& clicked_index) {
|
||||
QStringList ids;
|
||||
const QModelIndexList selection = tree->selectionModel()->selectedRows(0);
|
||||
bool clicked_in_selection = false;
|
||||
for (const QModelIndex& index : selection) {
|
||||
if (index == clicked_index.sibling(clicked_index.row(), 0)) {
|
||||
clicked_in_selection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (clicked_in_selection) {
|
||||
for (const QModelIndex& index : selection) {
|
||||
if (kindOf(index) == ItemKind::Model) {
|
||||
ids << idOf(index);
|
||||
}
|
||||
}
|
||||
ids.removeDuplicates();
|
||||
} else {
|
||||
ids << idOf(clicked_index);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
constexpr int kVisibilityColumnWidth = 28;
|
||||
|
||||
// QTreeView subclass that handles drag-and-drop. Drop logic dispatches
|
||||
// through commands (not directly into the model) so notifications + status
|
||||
// messages happen the same way as menu-driven moves.
|
||||
class ModelsTreeView : public QTreeView {
|
||||
public:
|
||||
explicit ModelsTreeView(bonsaiviewer::SessionState* session_state, QWidget* parent)
|
||||
: QTreeView(parent), session_state_(session_state) {}
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override {
|
||||
QTreeView::resizeEvent(event);
|
||||
if (model() && model()->columnCount() >= 2) {
|
||||
const int vw = viewport()->width();
|
||||
setColumnWidth(0, std::max(40, vw - kVisibilityColumnWidth));
|
||||
setColumnWidth(1, kVisibilityColumnWidth);
|
||||
}
|
||||
}
|
||||
|
||||
void startDrag(Qt::DropActions actions) override {
|
||||
const QModelIndexList selection = selectionModel()->selectedRows(0);
|
||||
if (selection.isEmpty()) return;
|
||||
|
||||
const auto first_kind = kindOf(selection.first());
|
||||
if (first_kind == ItemKind::Group && selection.size() != 1) return;
|
||||
|
||||
QByteArray payload;
|
||||
QDataStream stream(&payload, QIODevice::WriteOnly);
|
||||
stream << static_cast<int>(first_kind);
|
||||
if (first_kind == ItemKind::Group) {
|
||||
stream << idOf(selection.first());
|
||||
} else {
|
||||
QStringList ids;
|
||||
for (const QModelIndex& index : selection) {
|
||||
if (kindOf(index) != first_kind) return;
|
||||
ids.push_back(idOf(index));
|
||||
}
|
||||
ids.removeDuplicates();
|
||||
stream << ids;
|
||||
}
|
||||
|
||||
auto* mime = new QMimeData();
|
||||
mime->setData(QString::fromUtf8(kDragMimeType), payload);
|
||||
auto* drag = new QDrag(this);
|
||||
drag->setMimeData(mime);
|
||||
drag->exec(actions);
|
||||
}
|
||||
|
||||
void dragEnterEvent(QDragEnterEvent* event) override {
|
||||
if (event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
|
||||
event->acceptProposedAction();
|
||||
return;
|
||||
}
|
||||
QTreeView::dragEnterEvent(event);
|
||||
}
|
||||
|
||||
void dragMoveEvent(QDragMoveEvent* event) override {
|
||||
if (!event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
|
||||
QTreeView::dragMoveEvent(event);
|
||||
return;
|
||||
}
|
||||
QString target_group_id;
|
||||
if (!decodeTargetGroup(event->position().toPoint(), target_group_id) ||
|
||||
!canAcceptDrop(event->mimeData(), indexAt(event->position().toPoint()), target_group_id)) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
|
||||
void dropEvent(QDropEvent* event) override {
|
||||
if (!event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
|
||||
QTreeView::dropEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
QString target_group_id;
|
||||
if (!decodeTargetGroup(event->position().toPoint(), target_group_id) ||
|
||||
!canAcceptDrop(event->mimeData(), indexAt(event->position().toPoint()), target_group_id)) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray payload = event->mimeData()->data(QString::fromUtf8(kDragMimeType));
|
||||
QDataStream stream(&payload, QIODevice::ReadOnly);
|
||||
int kind_int = 0;
|
||||
stream >> kind_int;
|
||||
const auto kind = static_cast<ItemKind>(kind_int);
|
||||
if (kind == ItemKind::Group) {
|
||||
QString group_id;
|
||||
stream >> group_id;
|
||||
commands::moveGroup(*session_state_, group_id, target_group_id);
|
||||
} else {
|
||||
QStringList ids;
|
||||
stream >> ids;
|
||||
ids.removeDuplicates();
|
||||
if (!ids.isEmpty()) commands::moveModels(*session_state_, ids, target_group_id);
|
||||
}
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
|
||||
private:
|
||||
bool decodeTargetGroup(const QPoint& pos, QString& out) const {
|
||||
out.clear();
|
||||
const QModelIndex index = indexAt(pos);
|
||||
if (!index.isValid()) return true;
|
||||
if (kindOf(index) != ItemKind::Group) return false;
|
||||
out = idOf(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool canAcceptDrop(const QMimeData* mime,
|
||||
const QModelIndex& target_index,
|
||||
const QString& target_group_id) const {
|
||||
QByteArray payload = mime->data(QString::fromUtf8(kDragMimeType));
|
||||
QDataStream stream(&payload, QIODevice::ReadOnly);
|
||||
int kind_int = 0;
|
||||
stream >> kind_int;
|
||||
const auto kind = static_cast<ItemKind>(kind_int);
|
||||
|
||||
if (kind == ItemKind::Group) {
|
||||
QString group_id;
|
||||
stream >> group_id;
|
||||
if (group_id.isEmpty()) return false;
|
||||
if (!target_index.isValid()) return true;
|
||||
if (kindOf(target_index) != ItemKind::Group) return false;
|
||||
if (group_id == target_group_id) return false;
|
||||
for (QModelIndex cur = target_index; cur.isValid(); cur = cur.parent()) {
|
||||
if (idOf(cur) == group_id) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (kind == ItemKind::Model) {
|
||||
QStringList ids;
|
||||
stream >> ids;
|
||||
ids.removeDuplicates();
|
||||
if (ids.isEmpty()) return false;
|
||||
return !target_index.isValid() || !target_group_id.isNull();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bonsaiviewer::SessionState* session_state_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ModelsPanel::ModelsPanel(bonsaiviewer::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);
|
||||
|
||||
tree_ = new ModelsTreeView(session_state_, section);
|
||||
tree_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
tree_->setIconSize(QSize(16, 16));
|
||||
tree_->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
tree_->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
tree_->setDragEnabled(true);
|
||||
tree_->viewport()->setAcceptDrops(true);
|
||||
tree_->setDropIndicatorShown(true);
|
||||
tree_->setDragDropMode(QAbstractItemView::DragDrop);
|
||||
tree_->setDefaultDropAction(Qt::MoveAction);
|
||||
tree_->setUniformRowHeights(true);
|
||||
tree_->setExpandsOnDoubleClick(false);
|
||||
tree_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
tree_->header()->hide();
|
||||
|
||||
section->addBodyWidget(tree_);
|
||||
addBodyWidget(section);
|
||||
|
||||
connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& index) {
|
||||
if (!index.isValid() || index.column() != 1) return;
|
||||
commands::toggleVisibility(*session_state_, kindOf(index), idOf(index));
|
||||
});
|
||||
|
||||
connect(tree_, &QTreeView::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
const QModelIndex index = tree_->indexAt(pos);
|
||||
QMenu menu(tree_);
|
||||
|
||||
if (!index.isValid()) {
|
||||
QAction* add_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "Add Group");
|
||||
connect(add_group_action, &QAction::triggered, this, [this]() {
|
||||
commands::addGroup(*session_state_, *this, QString());
|
||||
});
|
||||
menu.exec(tree_->viewport()->mapToGlobal(pos));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto kind = kindOf(index);
|
||||
const QString id = idOf(index);
|
||||
|
||||
QAction* toggle_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/eye.svg"), "Toggle Visibility");
|
||||
connect(toggle_action, &QAction::triggered, this, [this, kind, id]() {
|
||||
commands::toggleVisibility(*session_state_, kind, id);
|
||||
});
|
||||
|
||||
if (kind == ItemKind::Group) {
|
||||
QAction* add_subgroup = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Subgroup");
|
||||
connect(add_subgroup, &QAction::triggered, this, [this, id]() {
|
||||
commands::addGroup(*session_state_, *this, id);
|
||||
});
|
||||
QAction* rename = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder.svg"), "Rename Group");
|
||||
connect(rename, &QAction::triggered, this, [this, id]() {
|
||||
commands::renameGroup(*session_state_, *this, id);
|
||||
});
|
||||
|
||||
QMenu* move_menu = menu.addMenu("Move to Parent");
|
||||
QAction* move_root = move_menu->addAction("(Root)");
|
||||
connect(move_root, &QAction::triggered, this, [this, id]() {
|
||||
commands::moveGroup(*session_state_, id, QString());
|
||||
});
|
||||
move_menu->addSeparator();
|
||||
for (const auto& target : validMoveTargets(*session_state_->federation(), id)) {
|
||||
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 = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-minus.svg"), "Remove Group");
|
||||
connect(remove, &QAction::triggered, this, [this, id]() {
|
||||
commands::removeGroup(*session_state_, *this, id);
|
||||
});
|
||||
} else {
|
||||
QString parent_group_id;
|
||||
const QModelIndex parent_index = index.parent();
|
||||
if (parent_index.isValid() && kindOf(parent_index) == ItemKind::Group) {
|
||||
parent_group_id = idOf(parent_index);
|
||||
}
|
||||
|
||||
QAction* add_group = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Group");
|
||||
connect(add_group, &QAction::triggered, this, [this, parent_group_id]() {
|
||||
commands::addGroup(*session_state_, *this, parent_group_id);
|
||||
});
|
||||
|
||||
const QStringList selected_model_ids = selectedModelIdsAt(tree_, index);
|
||||
|
||||
QMenu* move_menu = menu.addMenu("Move to Group");
|
||||
QAction* move_root = move_menu->addAction("(Root)");
|
||||
connect(move_root, &QAction::triggered, this, [this, selected_model_ids]() {
|
||||
commands::moveModels(*session_state_, selected_model_ids, QString());
|
||||
});
|
||||
move_menu->addSeparator();
|
||||
for (const auto& target : validMoveTargets(*session_state_->federation(), QString())) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
const Federation::Model* selected = session_state_->federation()->findById(id);
|
||||
const bool has_cloud_source = selected && selected->source_connector != "local";
|
||||
|
||||
menu.addSeparator();
|
||||
QAction* save_to_cloud = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/cloud-square.svg"), "Save To Cloud");
|
||||
save_to_cloud->setEnabled(has_cloud_source);
|
||||
if (!has_cloud_source) {
|
||||
save_to_cloud->setToolTip(
|
||||
"This model has no cloud target yet. Use \"Save As To Cloud\" first.");
|
||||
}
|
||||
connect(save_to_cloud, &QAction::triggered, this, [this, id]() {
|
||||
commands::saveModelToCloud(*session_state_, *this, id);
|
||||
});
|
||||
QAction* save_as_to_cloud = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/cloud-square.svg"), "Save As To Cloud");
|
||||
connect(save_as_to_cloud, &QAction::triggered, this, [this, id]() {
|
||||
commands::saveModelAsToCloud(*session_state_, *this, id);
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
QAction* remove = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model");
|
||||
connect(remove, &QAction::triggered, this, [this, 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::setModel(FederationItemModel* model) {
|
||||
model_ = model;
|
||||
tree_->setModel(model);
|
||||
// Columns are sized by ModelsTreeView::resizeEvent — header is hidden so
|
||||
// there's no user-facing resize affordance, and Stretch mode on
|
||||
// non-last sections proved unreliable here. Manual sizing is simpler.
|
||||
tree_->header()->setMinimumSectionSize(16);
|
||||
tree_->header()->setStretchLastSection(false);
|
||||
tree_->setColumnWidth(0, std::max(40, tree_->viewport()->width() - kVisibilityColumnWidth));
|
||||
tree_->setColumnWidth(1, kVisibilityColumnWidth);
|
||||
tree_->expandAll();
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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_PANEL_H
|
||||
#define IFCINTERFACE_MODULES_MODELS_PANEL_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include "../../components/Panel.h"
|
||||
|
||||
class QTreeView;
|
||||
class ViewportWindow;
|
||||
namespace bonsaiviewer { class SessionState; }
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
class FederationItemModel;
|
||||
|
||||
// The widget for the Models dock. Owns no domain state; click handlers call
|
||||
// commands directly. The QTreeView reads from a FederationItemModel which
|
||||
// subscribes to Federation's granular signals — view state (expansion,
|
||||
// selection, scroll) is preserved across mutations automatically.
|
||||
class ModelsPanel : public components::Panel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModelsPanel(bonsaiviewer::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
// Owned externally (the View constructs and owns the model). The panel
|
||||
// assigns it to the tree view; same model can outlive setModel calls.
|
||||
void setModel(FederationItemModel* model);
|
||||
|
||||
private:
|
||||
bonsaiviewer::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
QTreeView* tree_ = nullptr;
|
||||
FederationItemModel* model_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,485 @@
|
||||
// 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 "SettingsDialog.h"
|
||||
#include "SettingsView.h"
|
||||
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/Style.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../components/Tabs.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSizePolicy>
|
||||
#include <QShowEvent>
|
||||
#include <QSignalBlocker>
|
||||
#include <QScrollBar>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
struct UnitChoice {
|
||||
const char* label;
|
||||
const char* prefix;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
const UnitChoice kUnitChoices[] = {
|
||||
{"Metres (m)", "", "METRE"},
|
||||
{"Millimetres (mm)", "MILLI", "METRE"},
|
||||
{"Centimetres (cm)", "CENTI", "METRE"},
|
||||
{"Kilometres (km)", "KILO", "METRE"},
|
||||
{"Feet (ft)", "", "foot"},
|
||||
{"Inches (in)", "", "inch"},
|
||||
{"Yards (yd)", "", "yard"},
|
||||
{"Miles (mi)", "", "mile"},
|
||||
};
|
||||
|
||||
QLineEdit* makeNumericField(QWidget* parent, const QString& placeholder = {}) {
|
||||
auto* field = new QLineEdit(parent);
|
||||
field->setPlaceholderText(placeholder);
|
||||
field->setMaximumWidth(96);
|
||||
return field;
|
||||
}
|
||||
|
||||
QWidget* makeEqualThirdsRow(QWidget* parent, QLineEdit* a, QLineEdit* b, QLineEdit* c) {
|
||||
auto* row = new QWidget(parent);
|
||||
auto* layout = new QHBoxLayout(row);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(components::style::metrics::padding);
|
||||
for (QLineEdit* field : {a, b, c}) {
|
||||
field->setMaximumWidth(QWIDGETSIZE_MAX);
|
||||
field->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
layout->addWidget(field, 1);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
QString formatNumber(double value) {
|
||||
return QString::number(value, 'f', 6);
|
||||
}
|
||||
|
||||
double parseNumber(QLineEdit* field) {
|
||||
bool ok = false;
|
||||
const double value = field->text().toDouble(&ok);
|
||||
return ok ? value : 0.0;
|
||||
}
|
||||
|
||||
QString formatVector3(const Eigen::Vector3d& value) {
|
||||
return QString("%1, %2, %3").arg(formatNumber(value.x()), formatNumber(value.y()), formatNumber(value.z()));
|
||||
}
|
||||
|
||||
Eigen::Vector3d parseVector3(const QString& text) {
|
||||
const QStringList parts = text.split(QRegularExpression("[,\\s]+"), Qt::SkipEmptyParts);
|
||||
if (parts.size() != 3) return Eigen::Vector3d::Zero();
|
||||
|
||||
bool ok_x = false;
|
||||
bool ok_y = false;
|
||||
bool ok_z = false;
|
||||
const double x = parts[0].toDouble(&ok_x);
|
||||
const double y = parts[1].toDouble(&ok_y);
|
||||
const double z = parts[2].toDouble(&ok_z);
|
||||
if (!ok_x || !ok_y || !ok_z) return Eigen::Vector3d::Zero();
|
||||
return Eigen::Vector3d(x, y, z);
|
||||
}
|
||||
|
||||
QLabel* makeReadOnlyValue(QWidget* parent) {
|
||||
auto* label = new QLabel(parent);
|
||||
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
label->setWordWrap(true);
|
||||
return label;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsDialog::SettingsDialog(bonsaiviewer::SessionState* session_state, QWidget* parent)
|
||||
: components::TabbedDialog(parent)
|
||||
, session_state_(session_state)
|
||||
, federation_(session_state ? session_state->federation() : nullptr)
|
||||
, loader_(session_state ? session_state->loader() : nullptr)
|
||||
, settings_view_(new SettingsView(this, session_state))
|
||||
{
|
||||
setObjectName("appDialog");
|
||||
setWindowTitle("Model Settings");
|
||||
setModal(true);
|
||||
resize(980, 560);
|
||||
setupUi();
|
||||
}
|
||||
|
||||
void SettingsDialog::showEvent(QShowEvent* event) {
|
||||
federation_ = session_state_ ? session_state_->federation() : federation_;
|
||||
loader_ = session_state_ ? session_state_->loader() : loader_;
|
||||
syncFromFederation();
|
||||
populateModelTable();
|
||||
QDialog::showEvent(event);
|
||||
}
|
||||
|
||||
void SettingsDialog::setupUi() {
|
||||
auto* federation_tab = new QWidget(this);
|
||||
auto* federation_layout = new QVBoxLayout(federation_tab);
|
||||
federation_layout->setContentsMargins(0, 0, 0, 0);
|
||||
federation_layout->setSpacing(components::style::metrics::padding);
|
||||
federation_layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
auto* federation_unit_section =
|
||||
new components::Section("Federation Unit", components::SectionHeaderMode::Visible, federation_tab);
|
||||
auto* unit_hint = new QLabel(
|
||||
"All measurements, federated false origins, and destination model transforms will be interpreted in this unit.",
|
||||
federation_unit_section);
|
||||
unit_hint->setProperty("textRole", "secondary");
|
||||
unit_hint->setWordWrap(true);
|
||||
auto* federation_unit_body = new QWidget(federation_unit_section);
|
||||
auto* federation_unit_form = new QFormLayout(federation_unit_body);
|
||||
federation_unit_form->setContentsMargins(0, 0, 0, 0);
|
||||
federation_unit_form->setHorizontalSpacing(16);
|
||||
federation_unit_form->setVerticalSpacing(10);
|
||||
unit_combo_ = new QComboBox(federation_unit_body);
|
||||
for (const auto& uc : kUnitChoices) {
|
||||
QStringList data;
|
||||
data << QString::fromUtf8(uc.prefix) << QString::fromUtf8(uc.name);
|
||||
unit_combo_->addItem(uc.label, data);
|
||||
}
|
||||
federation_unit_form->addRow("Unit", unit_combo_);
|
||||
federation_unit_section->addBodyWidget(unit_hint);
|
||||
federation_unit_section->addBodyWidget(federation_unit_body);
|
||||
|
||||
auto* origin_section =
|
||||
new components::Section("Federated False Origin", components::SectionHeaderMode::Visible, federation_tab);
|
||||
auto* origin_hint = new QLabel(
|
||||
"Nominate a false origin and project north to use when viewing the federation of models.",
|
||||
origin_section);
|
||||
origin_hint->setProperty("textRole", "secondary");
|
||||
origin_hint->setWordWrap(true);
|
||||
auto* origin_body = new QWidget(origin_section);
|
||||
auto* origin_form = new QFormLayout(origin_body);
|
||||
origin_form->setContentsMargins(0, 0, 0, 0);
|
||||
origin_form->setHorizontalSpacing(16);
|
||||
origin_form->setVerticalSpacing(10);
|
||||
xyz_x_ = makeNumericField(origin_body, "X");
|
||||
xyz_y_ = makeNumericField(origin_body, "Y");
|
||||
xyz_z_ = makeNumericField(origin_body, "Z");
|
||||
rz_deg_ = makeNumericField(origin_body, "deg");
|
||||
origin_form->addRow("XYZ", makeEqualThirdsRow(origin_body, xyz_x_, xyz_y_, xyz_z_));
|
||||
origin_form->addRow("Z rotation (°)", rz_deg_);
|
||||
origin_section->addBodyWidget(origin_hint);
|
||||
origin_section->addBodyWidget(origin_body);
|
||||
|
||||
federation_layout->addWidget(federation_unit_section);
|
||||
federation_layout->addWidget(origin_section);
|
||||
federation_layout->addStretch(1);
|
||||
|
||||
auto* model_tab = new QWidget(this);
|
||||
auto* model_layout = new QVBoxLayout(model_tab);
|
||||
model_layout->setContentsMargins(0, 0, 0, 0);
|
||||
model_layout->setSpacing(components::style::metrics::padding);
|
||||
model_layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
auto* georef_section =
|
||||
new components::Section("Selected Model Georeferencing", components::SectionHeaderMode::Visible, model_tab);
|
||||
auto* georef_body = new QWidget(georef_section);
|
||||
auto* georef_layout = new QHBoxLayout(georef_body);
|
||||
georef_layout->setContentsMargins(0, 0, 0, 0);
|
||||
georef_layout->setSpacing(16);
|
||||
georef_layout->setAlignment(Qt::AlignTop);
|
||||
georef_present_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_type_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_project_unit_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_map_unit_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_easting_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_northing_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_height_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_x_axis_abscissa_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_x_axis_ordinate_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_rotation_dd_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_rotation_dms_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_scale_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_factor_x_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_factor_y_value_ = makeReadOnlyValue(georef_body);
|
||||
georef_factor_z_value_ = makeReadOnlyValue(georef_body);
|
||||
|
||||
auto* georef_col_1 = new QFormLayout();
|
||||
georef_col_1->setContentsMargins(0, 0, 0, 0);
|
||||
georef_col_1->setHorizontalSpacing(12);
|
||||
georef_col_1->setVerticalSpacing(8);
|
||||
georef_col_1->addRow("Georeferenced", georef_present_value_);
|
||||
georef_col_1->addRow("Coordinate Operation", georef_type_value_);
|
||||
georef_col_1->addRow("Project Unit", georef_project_unit_value_);
|
||||
georef_col_1->addRow("Map Unit", georef_map_unit_value_);
|
||||
georef_col_1->addRow("Easting", georef_easting_value_);
|
||||
georef_col_1->addRow("Northing", georef_northing_value_);
|
||||
georef_col_1->addRow("OrthogonalHeight", georef_height_value_);
|
||||
|
||||
auto* georef_col_2 = new QFormLayout();
|
||||
georef_col_2->setContentsMargins(0, 0, 0, 0);
|
||||
georef_col_2->setHorizontalSpacing(12);
|
||||
georef_col_2->setVerticalSpacing(8);
|
||||
georef_col_2->addRow("XAxisAbscissa", georef_x_axis_abscissa_value_);
|
||||
georef_col_2->addRow("XAxisOrdinate", georef_x_axis_ordinate_value_);
|
||||
georef_col_2->addRow("Rotation (DD)", georef_rotation_dd_value_);
|
||||
georef_col_2->addRow("Rotation (DMS)", georef_rotation_dms_value_);
|
||||
|
||||
auto* georef_col_3 = new QFormLayout();
|
||||
georef_col_3->setContentsMargins(0, 0, 0, 0);
|
||||
georef_col_3->setHorizontalSpacing(12);
|
||||
georef_col_3->setVerticalSpacing(8);
|
||||
georef_col_3->addRow("Scale", georef_scale_value_);
|
||||
georef_col_3->addRow("FactorX", georef_factor_x_value_);
|
||||
georef_col_3->addRow("FactorY", georef_factor_y_value_);
|
||||
georef_col_3->addRow("FactorZ", georef_factor_z_value_);
|
||||
|
||||
auto* georef_col_1_widget = new QWidget(georef_body);
|
||||
georef_col_1_widget->setLayout(georef_col_1);
|
||||
auto* georef_col_2_widget = new QWidget(georef_body);
|
||||
georef_col_2_widget->setLayout(georef_col_2);
|
||||
auto* georef_col_3_widget = new QWidget(georef_body);
|
||||
georef_col_3_widget->setLayout(georef_col_3);
|
||||
|
||||
georef_layout->addWidget(georef_col_1_widget, 1);
|
||||
georef_layout->addWidget(georef_col_2_widget, 1);
|
||||
georef_layout->addWidget(georef_col_3_widget, 1);
|
||||
georef_section->addBodyWidget(georef_body);
|
||||
|
||||
auto* table_section =
|
||||
new components::Section("Model Transformations", components::SectionHeaderMode::Visible, model_tab);
|
||||
auto* table_body = new QWidget(table_section);
|
||||
auto* table_layout = new QVBoxLayout(table_body);
|
||||
table_layout->setContentsMargins(0, 0, 0, 0);
|
||||
table_layout->setSpacing(components::style::metrics::padding);
|
||||
|
||||
model_table_ = new QTableWidget(table_body);
|
||||
model_table_->setObjectName("modelCoordinatesTable");
|
||||
model_table_->setColumnCount(6);
|
||||
model_table_->setHorizontalHeaderLabels(
|
||||
{"Model", "From", "From Point", "To Point", "Rotate", "Pivot Point"});
|
||||
model_table_->verticalHeader()->setVisible(false);
|
||||
model_table_->horizontalHeader()->setStretchLastSection(false);
|
||||
model_table_->horizontalHeader()->setSectionsMovable(false);
|
||||
model_table_->horizontalHeader()->setSectionsClickable(true);
|
||||
model_table_->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
|
||||
model_table_->horizontalHeader()->resizeSection(0, 180);
|
||||
model_table_->horizontalHeader()->resizeSection(1, 150);
|
||||
model_table_->horizontalHeader()->resizeSection(2, 180);
|
||||
model_table_->horizontalHeader()->resizeSection(3, 180);
|
||||
model_table_->horizontalHeader()->resizeSection(4, 180);
|
||||
model_table_->horizontalHeader()->resizeSection(5, 180);
|
||||
model_table_->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
model_table_->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
model_table_->setShowGrid(true);
|
||||
model_table_->setWordWrap(false);
|
||||
model_table_->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
|
||||
model_table_->setAlternatingRowColors(false);
|
||||
model_table_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
auto* table_hint = new QLabel(
|
||||
"This section lets you override model coordinates by specifying an optional translation from a point to "
|
||||
"another desired point, and an optional rotation.",
|
||||
table_section);
|
||||
table_hint->setProperty("textRole", "secondary");
|
||||
table_hint->setWordWrap(true);
|
||||
|
||||
table_layout->addWidget(model_table_, 1);
|
||||
table_section->addBodyWidget(table_hint);
|
||||
table_section->addBodyWidget(table_body);
|
||||
model_layout->addWidget(georef_section);
|
||||
model_layout->addWidget(table_section);
|
||||
|
||||
addTab("Federation", federation_tab);
|
||||
addTab("Model", model_tab);
|
||||
|
||||
connect(model_table_, &QTableWidget::currentCellChanged, this,
|
||||
[this](int /*current_row*/, int /*current_column*/, int /*previous_row*/, int /*previous_column*/) {
|
||||
updateSelectedModelGeoref();
|
||||
});
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
if (auto* ok = buttons->button(QDialogButtonBox::Ok)) {
|
||||
ok->setText("OK");
|
||||
ok->setIcon(components::icons::makeSvgIcon(":/icons/check.svg"));
|
||||
}
|
||||
if (auto* cancel = buttons->button(QDialogButtonBox::Cancel)) {
|
||||
cancel->setText("Cancel");
|
||||
cancel->setIcon(components::icons::makeSvgIcon(":/icons/xmark-circle.svg"));
|
||||
}
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, [this]() { onAccepted(); });
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
addFooterWidget(buttons);
|
||||
}
|
||||
|
||||
void SettingsDialog::syncFromFederation() {
|
||||
if (!federation_) return;
|
||||
|
||||
const auto& cfg = federation_->config();
|
||||
int idx = -1;
|
||||
for (int i = 0; i < unit_combo_->count(); ++i) {
|
||||
const QStringList data = unit_combo_->itemData(i).toStringList();
|
||||
if (data.size() == 2 &&
|
||||
data[0].toStdString() == cfg.unit_prefix &&
|
||||
data[1].toStdString() == cfg.unit_name) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
unit_combo_->setCurrentIndex(idx >= 0 ? idx : 0);
|
||||
|
||||
const auto& origin = federation_->federatedFalseOrigin();
|
||||
xyz_x_->setText(formatNumber(origin.xyz.x()));
|
||||
xyz_y_->setText(formatNumber(origin.xyz.y()));
|
||||
xyz_z_->setText(formatNumber(origin.xyz.z()));
|
||||
rz_deg_->setText(formatNumber(origin.rz_deg));
|
||||
}
|
||||
|
||||
void SettingsDialog::populateModelTable() {
|
||||
model_rows_.clear();
|
||||
const QSignalBlocker blocker(model_table_);
|
||||
model_table_->clearContents();
|
||||
model_table_->setRowCount(0);
|
||||
if (!federation_) return;
|
||||
|
||||
int row = 0;
|
||||
for (const auto& model : federation_->models()) {
|
||||
const auto& xf = model.model_transformation;
|
||||
model_table_->insertRow(row);
|
||||
|
||||
auto* model_item = new QTableWidgetItem(model.display_name.isEmpty() ? model.id : model.display_name);
|
||||
model_item->setData(Qt::UserRole, model.id);
|
||||
model_table_->setItem(row, 0, model_item);
|
||||
|
||||
ModelRowWidgets widgets;
|
||||
widgets.fed_id = model.id;
|
||||
|
||||
widgets.frame = new QComboBox(model_table_);
|
||||
widgets.frame->addItem("Local", static_cast<int>(AFrame::ModelLocal));
|
||||
widgets.frame->addItem("Global", static_cast<int>(AFrame::ModelGlobal));
|
||||
widgets.frame->setCurrentIndex(xf.a_frame == AFrame::ModelGlobal ? 1 : 0);
|
||||
model_table_->setCellWidget(row, 1, widgets.frame);
|
||||
|
||||
widgets.from_point = new QTableWidgetItem(formatVector3(xf.a));
|
||||
widgets.to_point = new QTableWidgetItem(formatVector3(xf.b));
|
||||
widgets.rotate = new QTableWidgetItem(formatVector3(xf.rxyz_deg));
|
||||
widgets.pivot = new QTableWidgetItem(formatVector3(xf.pivot));
|
||||
model_table_->setItem(row, 2, widgets.from_point);
|
||||
model_table_->setItem(row, 3, widgets.to_point);
|
||||
model_table_->setItem(row, 4, widgets.rotate);
|
||||
model_table_->setItem(row, 5, widgets.pivot);
|
||||
|
||||
model_rows_.push_back(widgets);
|
||||
model_table_->setRowHeight(row, 42);
|
||||
++row;
|
||||
}
|
||||
|
||||
int table_height = model_table_->frameWidth() * 2 + model_table_->horizontalHeader()->height();
|
||||
for (int i = 0; i < model_table_->rowCount(); ++i) {
|
||||
table_height += model_table_->rowHeight(i);
|
||||
}
|
||||
if (model_table_->horizontalScrollBar()->isVisible()) {
|
||||
table_height += model_table_->horizontalScrollBar()->sizeHint().height();
|
||||
}
|
||||
model_table_->setMinimumHeight(table_height);
|
||||
model_table_->setMaximumHeight(table_height);
|
||||
|
||||
if (model_table_->rowCount() > 0) {
|
||||
model_table_->setCurrentCell(0, 0);
|
||||
}
|
||||
updateSelectedModelGeoref();
|
||||
}
|
||||
|
||||
void SettingsDialog::renderSelectedModelGeoref(const SelectedModelGeorefState& state) {
|
||||
georef_present_value_->setText(state.georef_present);
|
||||
georef_type_value_->setText(state.coordinate_operation_type);
|
||||
georef_project_unit_value_->setText(state.project_unit);
|
||||
georef_map_unit_value_->setText(state.map_unit);
|
||||
georef_easting_value_->setText(state.easting);
|
||||
georef_northing_value_->setText(state.northing);
|
||||
georef_height_value_->setText(state.height);
|
||||
georef_x_axis_abscissa_value_->setText(state.x_axis_abscissa);
|
||||
georef_x_axis_ordinate_value_->setText(state.x_axis_ordinate);
|
||||
georef_rotation_dd_value_->setText(state.rotation_dd);
|
||||
georef_rotation_dms_value_->setText(state.rotation_dms);
|
||||
georef_scale_value_->setText(state.scale);
|
||||
georef_factor_x_value_->setText(state.factor_x);
|
||||
georef_factor_y_value_->setText(state.factor_y);
|
||||
georef_factor_z_value_->setText(state.factor_z);
|
||||
}
|
||||
|
||||
void SettingsDialog::updateSelectedModelGeoref() {
|
||||
const int row = model_table_->currentRow();
|
||||
if (row < 0 || row >= static_cast<int>(model_rows_.size())) {
|
||||
renderSelectedModelGeoref(
|
||||
{"No model selected", "—", "—", "—", "—", "—", "—", "—", "—", "—", "—", "—", "—", "—", "—"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings_view_) {
|
||||
renderSelectedModelGeoref(
|
||||
{"Unavailable", "No settings view", "—", "—", "—", "—", "—", "—", "—", "—", "—", "—", "—", "—", "—"});
|
||||
return;
|
||||
}
|
||||
|
||||
settings_view_->refresh(model_rows_[row].fed_id);
|
||||
}
|
||||
|
||||
void SettingsDialog::onAccepted() {
|
||||
if (federation_) {
|
||||
const QStringList data = unit_combo_->currentData().toStringList();
|
||||
FederationConfig cfg;
|
||||
if (data.size() == 2) {
|
||||
cfg.unit_prefix = data[0].toStdString();
|
||||
cfg.unit_name = data[1].toStdString();
|
||||
}
|
||||
federation_->setConfig(cfg);
|
||||
|
||||
FederatedFalseOrigin origin;
|
||||
origin.xyz = Eigen::Vector3d(parseNumber(xyz_x_), parseNumber(xyz_y_), parseNumber(xyz_z_));
|
||||
origin.rz_deg = parseNumber(rz_deg_);
|
||||
federation_->setFederatedFalseOrigin(origin);
|
||||
|
||||
for (const auto& row : model_rows_) {
|
||||
ModelTransformation xf;
|
||||
xf.a_frame = static_cast<AFrame>(row.frame->currentData().toInt());
|
||||
xf.a = parseVector3(row.from_point->text());
|
||||
xf.b = parseVector3(row.to_point->text());
|
||||
xf.rxyz_deg = parseVector3(row.rotate->text());
|
||||
xf.pivot = parseVector3(row.pivot->text());
|
||||
federation_->setModelTransformation(row.fed_id, xf);
|
||||
}
|
||||
if (session_state_) {
|
||||
session_state_->notifyFederationChanged();
|
||||
}
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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_SETTINGSDIALOG_H
|
||||
#define IFCINTERFACE_MODULES_MODELS_SETTINGSDIALOG_H
|
||||
|
||||
#include "../../components/Dialog.h"
|
||||
#include "Types.h"
|
||||
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
class Federation;
|
||||
class SceneLoader;
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QShowEvent;
|
||||
class QTableWidget;
|
||||
class QTableWidgetItem;
|
||||
|
||||
namespace bonsaiviewer {
|
||||
class SessionState;
|
||||
}
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
class SettingsView;
|
||||
|
||||
class SettingsDialog : public components::TabbedDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SettingsDialog(bonsaiviewer::SessionState* session_state, QWidget* parent = nullptr);
|
||||
void renderSelectedModelGeoref(const SelectedModelGeorefState& state);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent* event) override;
|
||||
|
||||
private:
|
||||
struct ModelRowWidgets {
|
||||
QString fed_id;
|
||||
QComboBox* frame = nullptr;
|
||||
QTableWidgetItem* from_point = nullptr;
|
||||
QTableWidgetItem* to_point = nullptr;
|
||||
QTableWidgetItem* rotate = nullptr;
|
||||
QTableWidgetItem* pivot = nullptr;
|
||||
};
|
||||
|
||||
void setupUi();
|
||||
void syncFromFederation();
|
||||
void populateModelTable();
|
||||
void updateSelectedModelGeoref();
|
||||
void onAccepted();
|
||||
|
||||
bonsaiviewer::SessionState* session_state_ = nullptr;
|
||||
Federation* federation_ = nullptr;
|
||||
SceneLoader* loader_ = nullptr;
|
||||
|
||||
QComboBox* unit_combo_ = nullptr;
|
||||
QLineEdit* xyz_x_ = nullptr;
|
||||
QLineEdit* xyz_y_ = nullptr;
|
||||
QLineEdit* xyz_z_ = nullptr;
|
||||
QLineEdit* rz_deg_ = nullptr;
|
||||
|
||||
QLabel* georef_present_value_ = nullptr;
|
||||
QLabel* georef_type_value_ = nullptr;
|
||||
QLabel* georef_project_unit_value_ = nullptr;
|
||||
QLabel* georef_map_unit_value_ = nullptr;
|
||||
QLabel* georef_easting_value_ = nullptr;
|
||||
QLabel* georef_northing_value_ = nullptr;
|
||||
QLabel* georef_height_value_ = nullptr;
|
||||
QLabel* georef_x_axis_abscissa_value_ = nullptr;
|
||||
QLabel* georef_x_axis_ordinate_value_ = nullptr;
|
||||
QLabel* georef_rotation_dd_value_ = nullptr;
|
||||
QLabel* georef_rotation_dms_value_ = nullptr;
|
||||
QLabel* georef_scale_value_ = nullptr;
|
||||
QLabel* georef_factor_x_value_ = nullptr;
|
||||
QLabel* georef_factor_y_value_ = nullptr;
|
||||
QLabel* georef_factor_z_value_ = nullptr;
|
||||
|
||||
QTableWidget* model_table_ = nullptr;
|
||||
std::vector<ModelRowWidgets> model_rows_;
|
||||
SettingsView* settings_view_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,246 @@
|
||||
// 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 "SettingsView.h"
|
||||
#include "SettingsDialog.h"
|
||||
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/Geolocation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/Unit.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
QString formatNumber(double value) {
|
||||
return QString::number(value, 'f', 6);
|
||||
}
|
||||
|
||||
QString formatAngleDms(double degrees) {
|
||||
const double absolute = std::fabs(degrees);
|
||||
const int d = static_cast<int>(absolute);
|
||||
const double minutes_total = (absolute - static_cast<double>(d)) * 60.0;
|
||||
const int m = static_cast<int>(minutes_total);
|
||||
const double s = (minutes_total - static_cast<double>(m)) * 60.0;
|
||||
const QString sign = degrees < 0.0 ? "-" : "";
|
||||
return QString("%1%2° %3' %4\"").arg(sign).arg(d).arg(m, 2, 10, QChar('0')).arg(formatNumber(s));
|
||||
}
|
||||
|
||||
SelectedModelGeorefState unknownState(const QString& georef, const QString& type) {
|
||||
return {
|
||||
georef,
|
||||
type,
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
"—",
|
||||
};
|
||||
}
|
||||
|
||||
QString formatCachedUnitScale(double meters_per_unit) {
|
||||
return QString("Cached scale: 1 unit = %1 m").arg(formatNumber(meters_per_unit));
|
||||
}
|
||||
|
||||
std::string enumString(const attribute_value& av) {
|
||||
if (av.isNull()) return {};
|
||||
if (av.type() != ifcopenshell::Argument_ENUMERATION) return {};
|
||||
enumeration_reference er = av;
|
||||
return std::string(er.value() ? er.value() : "");
|
||||
}
|
||||
|
||||
QString formatNamedUnit(const express::Base& unit) {
|
||||
if (!unit) return "—";
|
||||
auto entity = unit.as<express::Entity>();
|
||||
if (unit.declaration().is("IfcSIUnit")) {
|
||||
const std::string prefix = enumString(entity.get("Prefix"));
|
||||
const std::string name = enumString(entity.get("Name"));
|
||||
QString text;
|
||||
if (!prefix.empty()) {
|
||||
text += QString::fromStdString(prefix) + " ";
|
||||
}
|
||||
text += QString::fromStdString(name);
|
||||
auto symbol_it = kUnitSymbols.find(name);
|
||||
if (symbol_it != kUnitSymbols.end()) {
|
||||
text += " (" + QString::fromStdString(symbol_it->second) + ")";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
auto name_attr = entity.get("Name");
|
||||
if (name_attr.isNull()) return "—";
|
||||
const std::string name = static_cast<std::string>(name_attr);
|
||||
QString text = QString::fromStdString(name);
|
||||
auto symbol_it = kUnitSymbols.find(name);
|
||||
if (symbol_it != kUnitSymbols.end()) {
|
||||
text += " (" + QString::fromStdString(symbol_it->second) + ")";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::optional<QString> coordinateOperationType(ifcopenshell::file* ifc_file) {
|
||||
if (!ifc_file) return std::nullopt;
|
||||
try {
|
||||
const auto coordops = ifc_file->instances_by_type("IfcCoordinateOperation");
|
||||
if (!coordops.empty()) {
|
||||
return QString::fromStdString(coordops[0].declaration().name());
|
||||
}
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (ifc_file->schema()->name() == "IFC2X3") {
|
||||
if (getHelmertTransformationParameters(ifc_file)) {
|
||||
return QStringLiteral("ePSet_MapConversion");
|
||||
}
|
||||
}
|
||||
return QStringLiteral("None");
|
||||
}
|
||||
|
||||
SelectedModelGeorefState stateFromLiveFile(ifcopenshell::file* ifc_file) {
|
||||
if (!ifc_file) return unknownState("Not available yet", "No data source");
|
||||
|
||||
const auto params = getHelmertTransformationParameters(ifc_file);
|
||||
const auto coordop_type = coordinateOperationType(ifc_file);
|
||||
const auto project_unit_value = getProjectUnit(ifc_file, "LENGTHUNIT");
|
||||
const auto map_unit_value = getMapUnit(ifc_file);
|
||||
const QString project_unit = project_unit_value ? formatNamedUnit(*project_unit_value) : QString("—");
|
||||
const QString map_unit = map_unit_value ? formatNamedUnit(*map_unit_value) : project_unit;
|
||||
|
||||
if (!params) {
|
||||
auto state = unknownState("No", coordop_type.value_or("Unknown"));
|
||||
state.project_unit = project_unit;
|
||||
state.map_unit = map_unit;
|
||||
return state;
|
||||
}
|
||||
|
||||
const double rotation_dd = xaxis2angleDeg(params->xaa, params->xao);
|
||||
return {
|
||||
"Yes",
|
||||
coordop_type.value_or("Unknown"),
|
||||
project_unit,
|
||||
map_unit,
|
||||
formatNumber(params->e),
|
||||
formatNumber(params->n),
|
||||
formatNumber(params->h),
|
||||
formatNumber(params->xaa),
|
||||
formatNumber(params->xao),
|
||||
formatNumber(rotation_dd),
|
||||
formatAngleDms(rotation_dd),
|
||||
formatNumber(params->scale),
|
||||
formatNumber(params->factor_x),
|
||||
formatNumber(params->factor_y),
|
||||
formatNumber(params->factor_z),
|
||||
};
|
||||
}
|
||||
|
||||
SelectedModelGeorefState stateFromCachedGeoref(const ModelGeoref& georef) {
|
||||
if (!georef.has_coordinate_operation) {
|
||||
return unknownState("No", "None");
|
||||
}
|
||||
|
||||
const Eigen::Matrix4d& m = georef.coordinate_operation_meters;
|
||||
const Eigen::Vector3d translation = m.block<3, 1>(0, 3);
|
||||
const Eigen::Vector3d x_axis = m.block<3, 1>(0, 0);
|
||||
const Eigen::Vector3d y_axis = m.block<3, 1>(0, 1);
|
||||
const double factor_x = x_axis.norm();
|
||||
const double factor_y = y_axis.norm();
|
||||
const double factor_z = m.block<3, 1>(0, 2).norm();
|
||||
const double scale = (factor_x + factor_y) * 0.5;
|
||||
const double x_axis_abscissa = factor_x > 0.0 ? x_axis.x() / factor_x : 1.0;
|
||||
const double x_axis_ordinate = factor_x > 0.0 ? x_axis.y() / factor_x : 0.0;
|
||||
constexpr double kRadiansToDegrees = 57.29577951308232;
|
||||
const double rotation_dd = std::atan2(x_axis_ordinate, x_axis_abscissa) * kRadiansToDegrees;
|
||||
|
||||
return {
|
||||
"Yes",
|
||||
"Cached coordinate operation",
|
||||
formatCachedUnitScale(georef.units.project_length_to_meters),
|
||||
formatCachedUnitScale(georef.units.map_unit_to_meters),
|
||||
formatNumber(translation.x()),
|
||||
formatNumber(translation.y()),
|
||||
formatNumber(translation.z()),
|
||||
formatNumber(x_axis_abscissa),
|
||||
formatNumber(x_axis_ordinate),
|
||||
formatNumber(rotation_dd),
|
||||
formatAngleDms(rotation_dd),
|
||||
formatNumber(scale),
|
||||
formatNumber(factor_x),
|
||||
formatNumber(factor_y),
|
||||
formatNumber(factor_z),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsView::SettingsView(SettingsDialog* widget,
|
||||
bonsaiviewer::SessionState* session_state)
|
||||
: widget_(widget), session_state_(session_state)
|
||||
{
|
||||
}
|
||||
|
||||
void SettingsView::refresh(const QString& fed_id) const {
|
||||
if (!widget_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session_state_) {
|
||||
widget_->renderSelectedModelGeoref(unknownState("Unavailable", "No session state"));
|
||||
return;
|
||||
}
|
||||
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
if (!loader) {
|
||||
widget_->renderSelectedModelGeoref(unknownState("Unavailable", "No loader"));
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid == 0) {
|
||||
widget_->renderSelectedModelGeoref(unknownState("Not loaded", "No live model"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* ifc_file = loader->ifcFile(mid)) {
|
||||
widget_->renderSelectedModelGeoref(stateFromLiveFile(ifc_file));
|
||||
return;
|
||||
}
|
||||
|
||||
const ModelGeoref* georef = loader->modelGeoref(mid);
|
||||
if (!georef) {
|
||||
widget_->renderSelectedModelGeoref(unknownState("Not available yet", "No data source"));
|
||||
return;
|
||||
}
|
||||
widget_->renderSelectedModelGeoref(stateFromCachedGeoref(*georef));
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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_SETTINGSVIEW_H
|
||||
#define IFCINTERFACE_MODULES_MODELS_SETTINGSVIEW_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
namespace bonsaiviewer {
|
||||
class SessionState;
|
||||
}
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
class SettingsDialog;
|
||||
|
||||
class SettingsView {
|
||||
public:
|
||||
explicit SettingsView(SettingsDialog* widget,
|
||||
bonsaiviewer::SessionState* session_state);
|
||||
|
||||
void refresh(const QString& fed_id) const;
|
||||
|
||||
private:
|
||||
SettingsDialog* widget_ = nullptr;
|
||||
bonsaiviewer::SessionState* session_state_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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_MODELSPANELTYPES_H
|
||||
#define IFCINTERFACE_PANELS_MODELSPANELTYPES_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
enum class ItemKind {
|
||||
Group,
|
||||
Model,
|
||||
};
|
||||
|
||||
struct TreeNode {
|
||||
QString id;
|
||||
QString name;
|
||||
ItemKind kind = ItemKind::Group;
|
||||
bool visible = true;
|
||||
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;
|
||||
QString project_unit;
|
||||
QString map_unit;
|
||||
QString easting;
|
||||
QString northing;
|
||||
QString height;
|
||||
QString x_axis_abscissa;
|
||||
QString x_axis_ordinate;
|
||||
QString rotation_dd;
|
||||
QString rotation_dms;
|
||||
QString scale;
|
||||
QString factor_x;
|
||||
QString factor_y;
|
||||
QString factor_z;
|
||||
};
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,75 @@
|
||||
// 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 "View.h"
|
||||
|
||||
#include "FederationItemModel.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../ViewerSettings.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
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
|
||||
|
||||
QList<GroupOption> validMoveTargets(const Federation& federation,
|
||||
const QString& exclude_subtree_root) {
|
||||
QList<GroupOption> out;
|
||||
for (const auto& root : federation.rootGroups()) {
|
||||
collectGroupsRecursive(root.get(), exclude_subtree_root, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
|
||||
bonsaiviewer::SessionState* session_state,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, widget_(widget)
|
||||
, session_state_(session_state)
|
||||
, model_(new FederationItemModel(session_state->federation(), this))
|
||||
{
|
||||
widget_->setModel(model_);
|
||||
|
||||
// Coarse signals: full rebuild + re-style. The granular Federation
|
||||
// signals are handled inside FederationItemModel and don't reach here.
|
||||
auto rebuild = [this]() { model_->rebuildAll(); };
|
||||
connect(session_state_, &SessionState::projectReset, this, rebuild);
|
||||
connect(session_state_, &SessionState::projectOpened, this, rebuild);
|
||||
connect(&bonsaiviewer::ViewerSettings::instance(),
|
||||
&bonsaiviewer::ViewerSettings::themeChanged,
|
||||
this, rebuild);
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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_MODELSPANELVIEW_H
|
||||
#define IFCINTERFACE_PANELS_MODELSPANELVIEW_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class Federation;
|
||||
namespace bonsaiviewer { class SessionState; }
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
class FederationItemModel;
|
||||
class ModelsPanel;
|
||||
|
||||
// Pure derivation used by ModelsPanel when building its "move to..." menus.
|
||||
// Walks the federation's group tree and returns every group except those in
|
||||
// the subtree rooted at exclude_subtree_root (skip a group's own subtree to
|
||||
// prevent a cyclic move). Pass an empty exclude_subtree_root to get every
|
||||
// group back.
|
||||
QList<GroupOption> validMoveTargets(const Federation& federation,
|
||||
const QString& exclude_subtree_root);
|
||||
|
||||
// Owns the FederationItemModel, hands it to the panel, and listens to the
|
||||
// coarse session signals (project open/reset, theme change) — those are the
|
||||
// "rebuild from scratch" cases the model itself doesn't subscribe to.
|
||||
// Granular Federation events are handled inside the model.
|
||||
class ModelsPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModelsPanelView(ModelsPanel* widget,
|
||||
bonsaiviewer::SessionState* session_state,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
private:
|
||||
ModelsPanel* widget_ = nullptr;
|
||||
bonsaiviewer::SessionState* session_state_ = nullptr;
|
||||
FederationItemModel* model_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user