mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-31 08:56:34 +00:00
Swap interface into IfcViewerFull
Replace the old IfcViewerFull application tree with the interface-based viewer while preserving the IfcViewerFull target and build workflow. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// 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 ifcinterface::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* convert_database = components::buttons::makeButton("Convert IFC File\nto Database", ":/icons/database-restore.svg", choices);
|
||||
connect(convert_database, &QToolButton::clicked, this, [description]() {
|
||||
description->setText("IFC-to-database conversion is coming soon.");
|
||||
});
|
||||
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));
|
||||
|
||||
row->addWidget(components::buttons::makeButtonGroup("ADD", {add_ifc, add_database, add_geometry}, choices, true, 8));
|
||||
row->addWidget(components::buttons::makeButtonGroup("TOOLS", {convert_database}, choices, false, 8));
|
||||
choices_section->addBodyWidget(choices);
|
||||
|
||||
addBodyWidget(description_section);
|
||||
addBodyWidget(choices_section);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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 ifcinterface::modules::models {
|
||||
|
||||
enum class SourceMode {
|
||||
None,
|
||||
IfcFile,
|
||||
IfcDatabase,
|
||||
GeometryOnly,
|
||||
};
|
||||
|
||||
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 ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,318 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Controller.h"
|
||||
|
||||
#include "SettingsDialog.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "AddModelDialog.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/LodBuilder.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/SidecarCache.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QFileDialog>
|
||||
#include <QDebug>
|
||||
#include <QListView>
|
||||
#include <QMessageBox>
|
||||
#include <QTreeView>
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
ModelsPanelController::ModelsPanelController(QWidget* host,
|
||||
ModelsPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, host_(host)
|
||||
, widget_(widget)
|
||||
, session_state_(session_state)
|
||||
, viewport_(viewport)
|
||||
{
|
||||
connect(widget_, &ModelsPanel::visibilityToggleRequested, this,
|
||||
[this](ItemKind kind, const QString& id) {
|
||||
Federation* federation = session_state_->federation();
|
||||
if (kind == ItemKind::Group) {
|
||||
if (const Federation::Group* group = federation->findGroupById(id)) {
|
||||
federation->setGroupVisible(id, !group->visible);
|
||||
session_state_->notifyVisibilityChanged();
|
||||
session_state_->setStatusMessage("Models", group->visible ? "Group hidden" : "Group shown");
|
||||
}
|
||||
} else {
|
||||
if (const Federation::Model* model = federation->findById(id)) {
|
||||
federation->setModelVisible(id, !model->visible);
|
||||
session_state_->notifyVisibilityChanged();
|
||||
session_state_->setStatusMessage("Models", model->visible ? "Model hidden" : "Model shown");
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(widget_, &ModelsPanel::addGroupRequested, this,
|
||||
[this](const QString& parent_group_id, const QString& name) {
|
||||
session_state_->federation()->addGroup(name, parent_group_id);
|
||||
session_state_->notifyFederationStructureChanged();
|
||||
session_state_->setStatusMessage("Models", "Group added");
|
||||
});
|
||||
connect(widget_, &ModelsPanel::renameGroupRequested, this,
|
||||
[this](const QString& id, const QString& name) {
|
||||
session_state_->federation()->setGroupName(id, name);
|
||||
session_state_->notifyFederationStructureChanged();
|
||||
session_state_->setStatusMessage("Models", "Group renamed");
|
||||
});
|
||||
connect(widget_, &ModelsPanel::moveGroupRequested, this,
|
||||
[this](const QString& id, const QString& parent_group_id) {
|
||||
session_state_->federation()->setGroupParent(id, parent_group_id);
|
||||
session_state_->notifyFederationStructureChanged();
|
||||
session_state_->setStatusMessage("Models", parent_group_id.isEmpty() ? "Group moved to root"
|
||||
: "Group moved");
|
||||
});
|
||||
connect(widget_, &ModelsPanel::moveModelsRequested, this,
|
||||
[this](const QStringList& ids, const QString& parent_group_id) {
|
||||
for (const auto& id : ids) {
|
||||
session_state_->federation()->setModelGroup(id, parent_group_id);
|
||||
}
|
||||
session_state_->notifyFederationStructureChanged();
|
||||
session_state_->setStatusMessage("Models", parent_group_id.isEmpty() ? "Model(s) moved to root"
|
||||
: "Model(s) moved");
|
||||
});
|
||||
connect(widget_, &ModelsPanel::removeGroupRequested, this,
|
||||
[this](const QString& id) {
|
||||
session_state_->federation()->removeGroup(id);
|
||||
session_state_->notifyFederationStructureChanged();
|
||||
session_state_->setStatusMessage("Models", "Group removed");
|
||||
});
|
||||
connect(widget_, &ModelsPanel::removeModelRequested, this,
|
||||
[this](const QString& id) {
|
||||
removeLoadedModel(id);
|
||||
session_state_->setStatusMessage("Models", "Model removed");
|
||||
});
|
||||
connect(widget_, &components::Panel::settingsRequested, this, [this]() {
|
||||
openSettings();
|
||||
});
|
||||
}
|
||||
|
||||
void ModelsPanelController::bindLoader(SceneLoader* loader) {
|
||||
connect(loader, &SceneLoader::loadStarted, this,
|
||||
[this](uint32_t /*mid*/, const QString& display_name) {
|
||||
session_state_->setStatusMessage("Loading", display_name);
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromSidecar, this,
|
||||
[this, loader](uint32_t mid, qint64 elapsed_ms) {
|
||||
session_state_->setStatusMessage(
|
||||
"Loaded",
|
||||
QString("%1 from cache in %2")
|
||||
.arg(loader->displayName(mid))
|
||||
.arg(formatElapsed(elapsed_ms)));
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromStream, this,
|
||||
[this, loader](uint32_t mid, qint64 elapsed_ms) {
|
||||
writeSidecarForModel(loader, mid);
|
||||
session_state_->setStatusMessage(
|
||||
"Loaded",
|
||||
QString("%1 streamed in %2")
|
||||
.arg(loader->displayName(mid))
|
||||
.arg(formatElapsed(elapsed_ms)));
|
||||
});
|
||||
connect(loader, &SceneLoader::loadCancelled, this,
|
||||
[this, loader](uint32_t mid) {
|
||||
session_state_->setStatusMessage("Cancelled", loader->displayName(mid));
|
||||
});
|
||||
connect(loader, &SceneLoader::loadError, this,
|
||||
[this, host = host_](uint32_t /*mid*/, const QString& message) {
|
||||
session_state_->setStatusMessage("Error", message);
|
||||
QMessageBox::warning(host, "IfcViewer", message);
|
||||
});
|
||||
connect(loader, &SceneLoader::allLoadsFinished, this,
|
||||
[this, loader]() {
|
||||
session_state_->setStatusMessage("Loaded", QString("%1 model(s)").arg(loader->modelCount()));
|
||||
});
|
||||
}
|
||||
|
||||
void ModelsPanelController::addFiles() {
|
||||
modules::models::AddModelDialog dialog(host_);
|
||||
if (dialog.exec() != QDialog::Accepted) return;
|
||||
|
||||
QStringList paths;
|
||||
switch (dialog.selectedMode()) {
|
||||
case modules::models::SourceMode::IfcFile: {
|
||||
QFileDialog file_dialog(host_, "Add IFC Files");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
file_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() == QDialog::Accepted) {
|
||||
paths = file_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case modules::models::SourceMode::IfcDatabase: {
|
||||
QFileDialog database_dialog(host_, "Add IFC Databases");
|
||||
database_dialog.setFileMode(QFileDialog::Directory);
|
||||
database_dialog.setOption(QFileDialog::ShowDirsOnly, true);
|
||||
database_dialog.setOption(QFileDialog::DontResolveSymlinks, true);
|
||||
database_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (auto* list = database_dialog.findChild<QListView*>("listView")) {
|
||||
list->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
if (auto* tree = database_dialog.findChild<QTreeView*>()) {
|
||||
tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
if (database_dialog.exec() == QDialog::Accepted) {
|
||||
paths = database_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case modules::models::SourceMode::GeometryOnly: {
|
||||
QFileDialog file_dialog(host_, "Add Geometry Only");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||
file_dialog.setNameFilter("IFC Viewer Cache (*.ifcview);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() == QDialog::Accepted) {
|
||||
paths = file_dialog.selectedFiles();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case modules::models::SourceMode::None:
|
||||
return;
|
||||
}
|
||||
|
||||
addFiles(paths);
|
||||
}
|
||||
|
||||
void ModelsPanelController::addFiles(const QStringList& paths) {
|
||||
QStringList accepted_paths;
|
||||
QStringList accepted_fed_ids;
|
||||
for (const auto& path : paths) {
|
||||
const QString fed_id = session_state_->federation()->addModel(path);
|
||||
if (fed_id.isEmpty()) continue;
|
||||
accepted_paths << path;
|
||||
accepted_fed_ids << fed_id;
|
||||
}
|
||||
|
||||
loadModels(accepted_paths, accepted_fed_ids);
|
||||
}
|
||||
|
||||
void ModelsPanelController::loadModels(const QStringList& paths, const QStringList& fed_ids) {
|
||||
if (paths.isEmpty()) return;
|
||||
|
||||
const auto ids = session_state_->loader()->addFiles(paths);
|
||||
for (int i = 0; i < paths.size() && i < static_cast<int>(ids.size()) && i < fed_ids.size(); ++i) {
|
||||
session_state_->setModelMapping(fed_ids[i], ids[i]);
|
||||
}
|
||||
session_state_->notifyModelsChanged();
|
||||
}
|
||||
|
||||
void ModelsPanelController::removeLoadedModel(const QString& fed_id) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid == 0) {
|
||||
session_state_->federation()->removeModel(fed_id);
|
||||
return;
|
||||
}
|
||||
if (session_state_->loader()->isLoadingModel(mid)) return;
|
||||
|
||||
viewport_->setSelectedObjectId(0);
|
||||
session_state_->setSelectedObjectId(0);
|
||||
session_state_->federation()->removeModel(fed_id);
|
||||
viewport_->removeModel(mid);
|
||||
session_state_->loader()->removeModel(mid);
|
||||
session_state_->elementRegistry()->removeModel(mid);
|
||||
session_state_->removeModelMappingByFedId(fed_id);
|
||||
session_state_->notifySelectionChanged();
|
||||
session_state_->notifyModelsChanged();
|
||||
}
|
||||
|
||||
void ModelsPanelController::openSettings() {
|
||||
SettingsDialog dialog(session_state_, host_);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
QString ModelsPanelController::formatElapsed(qint64 ms) const {
|
||||
return (ms >= 1000)
|
||||
? QString::number(ms / 1000.0, 'f', 2) + " s"
|
||||
: QString::number(ms) + " ms";
|
||||
}
|
||||
|
||||
void ModelsPanelController::writeSidecarForModel(SceneLoader* loader, uint32_t mid) const {
|
||||
if (!loader || !viewport_ || !session_state_) return;
|
||||
|
||||
SidecarData sidecar_data;
|
||||
if (!viewport_->snapshotModel(mid, sidecar_data)) return;
|
||||
|
||||
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
||||
sidecar_data.has_coordinate_operation = georef->has_coordinate_operation ? 1 : 0;
|
||||
Eigen::Map<Eigen::Matrix<double, 4, 4, Eigen::ColMajor>>(
|
||||
sidecar_data.coordinate_operation_meters) = georef->coordinate_operation_meters;
|
||||
sidecar_data.project_length_to_meters = georef->units.project_length_to_meters;
|
||||
sidecar_data.map_unit_to_meters = georef->units.map_unit_to_meters;
|
||||
}
|
||||
|
||||
auto* element_registry = session_state_->elementRegistry();
|
||||
if (element_registry) {
|
||||
for (const auto& info : element_registry->basicElementInfoForModel(mid)) {
|
||||
PackedElementInfo packed;
|
||||
packed.object_id = info.object_id;
|
||||
packed.model_id = info.model_id;
|
||||
packed.ifc_id = info.ifc_id;
|
||||
packed.parent_id = info.parent_id;
|
||||
|
||||
const std::string guid = info.guid.toStdString();
|
||||
packed.guid_offset = static_cast<uint32_t>(sidecar_data.string_table.size());
|
||||
packed.guid_length = static_cast<uint32_t>(guid.size());
|
||||
sidecar_data.string_table += guid;
|
||||
|
||||
const std::string name = info.name.toStdString();
|
||||
packed.name_offset = static_cast<uint32_t>(sidecar_data.string_table.size());
|
||||
packed.name_length = static_cast<uint32_t>(name.size());
|
||||
sidecar_data.string_table += name;
|
||||
|
||||
const std::string type = info.type.toStdString();
|
||||
packed.type_offset = static_cast<uint32_t>(sidecar_data.string_table.size());
|
||||
packed.type_length = static_cast<uint32_t>(type.size());
|
||||
sidecar_data.string_table += type;
|
||||
|
||||
sidecar_data.elements.push_back(packed);
|
||||
}
|
||||
}
|
||||
|
||||
QElapsedTimer lod_timer;
|
||||
lod_timer.start();
|
||||
buildLods(sidecar_data);
|
||||
const LodStats lod_stats = summariseLods(sidecar_data);
|
||||
qDebug(" LOD build: %lld ms — %u/%u meshes got LOD1 "
|
||||
"(%u tris -> %u tris for those meshes)",
|
||||
lod_timer.elapsed(),
|
||||
lod_stats.meshes_with_lod1, lod_stats.meshes_total,
|
||||
lod_stats.tris_lod0_for_lod1, lod_stats.tris_lod1);
|
||||
|
||||
viewport_->applyLodExtension(mid, sidecar_data);
|
||||
|
||||
QElapsedTimer sidecar_timer;
|
||||
sidecar_timer.start();
|
||||
const bool ok = writeSidecar(loader->filePath(mid).toStdString(), sidecar_data);
|
||||
qDebug(" Sidecar write: %lld ms (%s)", sidecar_timer.elapsed(), ok ? "ok" : "FAILED");
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
@@ -0,0 +1,67 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H
|
||||
#define IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
class QWidget;
|
||||
namespace ifcinterface { class SessionState; }
|
||||
class ViewportWindow;
|
||||
class SceneLoader;
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
class ModelsPanel;
|
||||
|
||||
class ModelsPanelController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ModelsPanelController(QWidget* host,
|
||||
ModelsPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
void bindLoader(SceneLoader* loader);
|
||||
void addFiles();
|
||||
void addFiles(const QStringList& paths);
|
||||
void loadModels(const QStringList& paths, const QStringList& fed_ids);
|
||||
void removeLoadedModel(const QString& fed_id);
|
||||
void openSettings();
|
||||
|
||||
private:
|
||||
QString formatElapsed(qint64 ms) const;
|
||||
void writeSidecarForModel(SceneLoader* loader, uint32_t mid) const;
|
||||
|
||||
QWidget* host_ = nullptr;
|
||||
ModelsPanel* widget_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,447 @@
|
||||
// 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 "../../InterfaceSettings.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QColor>
|
||||
#include <QDataStream>
|
||||
#include <QDragEnterEvent>
|
||||
#include <QDragMoveEvent>
|
||||
#include <QHeaderView>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QMimeData>
|
||||
#include <QDropEvent>
|
||||
#include <QSizePolicy>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kDragMimeType = "application/x-ifcinterface-model-items";
|
||||
|
||||
class ModelsTreeWidget : public QTreeWidget {
|
||||
public:
|
||||
explicit ModelsTreeWidget(QWidget* parent = nullptr) : QTreeWidget(parent) {}
|
||||
|
||||
std::function<void(const QStringList&, const QString&)> on_model_drop;
|
||||
std::function<void(const QString&, const QString&)> on_group_drop;
|
||||
|
||||
protected:
|
||||
QStringList mimeTypes() const override {
|
||||
return {QString::fromUtf8(kDragMimeType)};
|
||||
}
|
||||
|
||||
QMimeData* mimeData(const QList<QTreeWidgetItem*>& items) const override {
|
||||
Q_UNUSED(items);
|
||||
const QList<QTreeWidgetItem*> selected = selectedItems();
|
||||
if (selected.isEmpty()) return nullptr;
|
||||
|
||||
const int first_kind = selected.first()->data(0, Qt::UserRole).toInt();
|
||||
if (first_kind == static_cast<int>(ItemKind::Group) && selected.size() != 1) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QByteArray payload;
|
||||
QDataStream stream(&payload, QIODevice::WriteOnly);
|
||||
stream << first_kind;
|
||||
if (first_kind == static_cast<int>(ItemKind::Group)) {
|
||||
stream << selected.first()->data(0, Qt::UserRole + 1).toString();
|
||||
} else {
|
||||
QStringList ids;
|
||||
for (QTreeWidgetItem* item : selected) {
|
||||
if (item->data(0, Qt::UserRole).toInt() != first_kind) return nullptr;
|
||||
ids.push_back(item->data(0, Qt::UserRole + 1).toString());
|
||||
}
|
||||
ids.removeDuplicates();
|
||||
stream << ids;
|
||||
}
|
||||
|
||||
auto* mime = new QMimeData();
|
||||
mime->setData(QString::fromUtf8(kDragMimeType), payload);
|
||||
return mime;
|
||||
}
|
||||
|
||||
Qt::DropActions supportedDropActions() const override {
|
||||
return Qt::MoveAction;
|
||||
}
|
||||
|
||||
void dragEnterEvent(QDragEnterEvent* event) override {
|
||||
if (event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
|
||||
event->acceptProposedAction();
|
||||
return;
|
||||
}
|
||||
QTreeWidget::dragEnterEvent(event);
|
||||
}
|
||||
|
||||
void dragMoveEvent(QDragMoveEvent* event) override {
|
||||
if (!event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
|
||||
QTreeWidget::dragMoveEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
QString group_id;
|
||||
if (!decodeDropTarget(event->position().toPoint(), group_id)) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
if (canAcceptDrop(event->mimeData(), itemAt(event->position().toPoint()), group_id)) {
|
||||
event->acceptProposedAction();
|
||||
} else {
|
||||
event->ignore();
|
||||
}
|
||||
}
|
||||
|
||||
void dropEvent(QDropEvent* event) override {
|
||||
if (!event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
|
||||
QTreeWidget::dropEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
QString target_group_id;
|
||||
if (!decodeDropTarget(event->position().toPoint(), target_group_id)) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
QTreeWidgetItem* target_item = itemAt(event->position().toPoint());
|
||||
if (!canAcceptDrop(event->mimeData(), target_item, target_group_id)) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray payload = event->mimeData()->data(QString::fromUtf8(kDragMimeType));
|
||||
QDataStream stream(&payload, QIODevice::ReadOnly);
|
||||
int kind = 0;
|
||||
stream >> kind;
|
||||
if (kind == static_cast<int>(ItemKind::Group)) {
|
||||
QString group_id;
|
||||
stream >> group_id;
|
||||
if (on_group_drop) on_group_drop(group_id, target_group_id);
|
||||
} else if (kind == static_cast<int>(ItemKind::Model)) {
|
||||
QStringList ids;
|
||||
stream >> ids;
|
||||
ids.removeDuplicates();
|
||||
if (!ids.isEmpty() && on_model_drop) on_model_drop(ids, target_group_id);
|
||||
} else {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
|
||||
private:
|
||||
bool decodeDropTarget(const QPoint& pos, QString& target_group_id) const {
|
||||
target_group_id.clear();
|
||||
QTreeWidgetItem* target_item = itemAt(pos);
|
||||
if (!target_item) return true;
|
||||
|
||||
const int kind = target_item->data(0, Qt::UserRole).toInt();
|
||||
if (kind != static_cast<int>(ItemKind::Group)) return false;
|
||||
|
||||
target_group_id = target_item->data(0, Qt::UserRole + 1).toString();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool canAcceptDrop(const QMimeData* mime,
|
||||
QTreeWidgetItem* target_item,
|
||||
const QString& target_group_id) const {
|
||||
QByteArray payload = mime->data(QString::fromUtf8(kDragMimeType));
|
||||
QDataStream stream(&payload, QIODevice::ReadOnly);
|
||||
int kind = 0;
|
||||
stream >> kind;
|
||||
|
||||
if (kind == static_cast<int>(ItemKind::Group)) {
|
||||
QString group_id;
|
||||
stream >> group_id;
|
||||
if (group_id.isEmpty()) return false;
|
||||
if (!target_item) return true;
|
||||
if (target_item->data(0, Qt::UserRole).toInt() != static_cast<int>(ItemKind::Group)) return false;
|
||||
if (group_id == target_group_id) return false;
|
||||
for (QTreeWidgetItem* cur = target_item; cur != nullptr; cur = cur->parent()) {
|
||||
if (cur->data(0, Qt::UserRole + 1).toString() == group_id) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (kind == static_cast<int>(ItemKind::Model)) {
|
||||
QStringList ids;
|
||||
stream >> ids;
|
||||
ids.removeDuplicates();
|
||||
if (ids.isEmpty()) return false;
|
||||
return target_item == nullptr || !target_group_id.isNull();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
QString promptGroupName(QWidget* parent, const QString& title, const QString& label, const QString& value) {
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(parent, title, label, QLineEdit::Normal, value, &ok);
|
||||
return ok ? name.trimmed() : QString();
|
||||
}
|
||||
|
||||
QString itemId(QTreeWidgetItem* item) {
|
||||
return item ? item->data(0, Qt::UserRole + 1).toString() : QString();
|
||||
}
|
||||
|
||||
ItemKind itemKind(QTreeWidgetItem* item) {
|
||||
return static_cast<ItemKind>(item->data(0, Qt::UserRole).toInt());
|
||||
}
|
||||
|
||||
QList<QTreeWidgetItem*> selectedItemsOfKind(QTreeWidget* tree, ItemKind kind) {
|
||||
QList<QTreeWidgetItem*> matches;
|
||||
for (QTreeWidgetItem* item : tree->selectedItems()) {
|
||||
if (itemKind(item) == kind) matches.push_back(item);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModelsPanel::ModelsPanel(QWidget* parent)
|
||||
: components::Panel("Models", nullptr, parent, true)
|
||||
{
|
||||
auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
section->setBodyExpanding(true);
|
||||
auto* tree = new ModelsTreeWidget(section);
|
||||
tree_ = tree;
|
||||
tree_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
tree_->setColumnCount(2);
|
||||
tree_->setHeaderLabels({"Model", ""});
|
||||
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_->header()->setStretchLastSection(false);
|
||||
tree_->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
tree_->header()->setSectionResizeMode(1, QHeaderView::Fixed);
|
||||
tree_->header()->resizeSection(1, 28);
|
||||
tree_->header()->hide();
|
||||
tree->on_model_drop = [this](const QStringList& ids, const QString& target_group_id) {
|
||||
emit moveModelsRequested(ids, target_group_id);
|
||||
};
|
||||
tree->on_group_drop = [this](const QString& id, const QString& target_group_id) {
|
||||
emit moveGroupRequested(id, target_group_id);
|
||||
};
|
||||
section->addBodyWidget(tree_);
|
||||
addBodyWidget(section);
|
||||
|
||||
connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int column) {
|
||||
if (!item || column != 1) return;
|
||||
emit visibilityToggleRequested(
|
||||
static_cast<ItemKind>(item->data(0, Qt::UserRole).toInt()),
|
||||
item->data(0, Qt::UserRole + 1).toString());
|
||||
});
|
||||
|
||||
connect(tree_, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
auto* item = tree_->itemAt(pos);
|
||||
QMenu menu(tree_);
|
||||
if (!item) {
|
||||
QAction* add_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "Add Group");
|
||||
connect(add_group_action, &QAction::triggered, this, [this]() {
|
||||
const QString name = promptGroupName(this, "New Group", "Group name:", "Group");
|
||||
if (!name.isEmpty()) {
|
||||
emit addGroupRequested(QString(), name);
|
||||
}
|
||||
});
|
||||
menu.exec(tree_->viewport()->mapToGlobal(pos));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto kind = itemKind(item);
|
||||
const QString id = itemId(item);
|
||||
|
||||
QAction* toggle_visibility_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/eye.svg"), "Toggle Visibility");
|
||||
connect(toggle_visibility_action, &QAction::triggered, this, [this, kind, id]() {
|
||||
emit visibilityToggleRequested(kind, id);
|
||||
});
|
||||
|
||||
if (kind == ItemKind::Group) {
|
||||
QAction* add_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Subgroup");
|
||||
connect(add_group_action, &QAction::triggered, this, [this, id]() {
|
||||
const QString name = promptGroupName(this, "New Group", "Group name:", "Group");
|
||||
if (!name.isEmpty()) {
|
||||
emit addGroupRequested(id, name);
|
||||
}
|
||||
});
|
||||
QAction* rename_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder.svg"), "Rename Group");
|
||||
connect(rename_group_action, &QAction::triggered, this, [this, item, id]() {
|
||||
const QString name = promptGroupName(
|
||||
this, "Rename Group", "Group name:", item->text(0));
|
||||
if (!name.isEmpty()) emit renameGroupRequested(id, name);
|
||||
});
|
||||
QMenu* move_menu = menu.addMenu("Move to Parent");
|
||||
QAction* move_root_action = move_menu->addAction("(Root)");
|
||||
connect(move_root_action, &QAction::triggered, this, [this, id]() {
|
||||
emit moveGroupRequested(id, QString());
|
||||
});
|
||||
move_menu->addSeparator();
|
||||
QList<QTreeWidgetItem*> stack;
|
||||
for (int i = 0; i < tree_->topLevelItemCount(); ++i) {
|
||||
stack.push_back(tree_->topLevelItem(i));
|
||||
}
|
||||
while (!stack.isEmpty()) {
|
||||
QTreeWidgetItem* candidate = stack.takeFirst();
|
||||
if (candidate != item && itemKind(candidate) == ItemKind::Group) {
|
||||
bool would_cycle = false;
|
||||
for (QTreeWidgetItem* cur = candidate; cur != nullptr; cur = cur->parent()) {
|
||||
if (cur == item) {
|
||||
would_cycle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto* action = move_menu->addAction(candidate->text(0));
|
||||
action->setEnabled(!would_cycle && candidate != item->parent());
|
||||
connect(action, &QAction::triggered, this, [this, id, candidate]() {
|
||||
emit moveGroupRequested(id, itemId(candidate));
|
||||
});
|
||||
}
|
||||
for (int i = 0; i < candidate->childCount(); ++i) {
|
||||
stack.push_back(candidate->child(i));
|
||||
}
|
||||
}
|
||||
QAction* remove_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-minus.svg"), "Remove Group");
|
||||
connect(remove_group_action, &QAction::triggered, this, [this, id]() {
|
||||
emit removeGroupRequested(id);
|
||||
});
|
||||
} else {
|
||||
QString parent_group_id;
|
||||
if (auto* parent_item = item->parent()) {
|
||||
if (itemKind(parent_item) == ItemKind::Group) {
|
||||
parent_group_id = itemId(parent_item);
|
||||
}
|
||||
}
|
||||
|
||||
QAction* add_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Group");
|
||||
connect(add_group_action, &QAction::triggered, this, [this, parent_group_id]() {
|
||||
const QString name = promptGroupName(this, "New Group", "Group name:", "Group");
|
||||
if (!name.isEmpty()) {
|
||||
emit addGroupRequested(parent_group_id, name);
|
||||
}
|
||||
});
|
||||
|
||||
QStringList selected_model_ids;
|
||||
const QList<QTreeWidgetItem*> selected_models = selectedItemsOfKind(tree_, ItemKind::Model);
|
||||
if (selected_models.contains(item)) {
|
||||
for (QTreeWidgetItem* selected : selected_models) {
|
||||
selected_model_ids.push_back(itemId(selected));
|
||||
}
|
||||
selected_model_ids.removeDuplicates();
|
||||
} else {
|
||||
selected_model_ids = {id};
|
||||
}
|
||||
|
||||
QMenu* move_menu = menu.addMenu("Move to Group");
|
||||
QAction* move_root_action = move_menu->addAction("(Root)");
|
||||
connect(move_root_action, &QAction::triggered, this, [this, selected_model_ids]() {
|
||||
emit moveModelsRequested(selected_model_ids, QString());
|
||||
});
|
||||
move_menu->addSeparator();
|
||||
QList<QTreeWidgetItem*> stack;
|
||||
for (int i = 0; i < tree_->topLevelItemCount(); ++i) {
|
||||
stack.push_back(tree_->topLevelItem(i));
|
||||
}
|
||||
while (!stack.isEmpty()) {
|
||||
QTreeWidgetItem* candidate = stack.takeFirst();
|
||||
if (itemKind(candidate) == ItemKind::Group) {
|
||||
const QString group_id = itemId(candidate);
|
||||
QAction* action = move_menu->addAction(candidate->text(0));
|
||||
connect(action, &QAction::triggered, this, [this, selected_model_ids, group_id]() {
|
||||
emit moveModelsRequested(selected_model_ids, group_id);
|
||||
});
|
||||
}
|
||||
for (int i = 0; i < candidate->childCount(); ++i) {
|
||||
stack.push_back(candidate->child(i));
|
||||
}
|
||||
}
|
||||
QAction* remove_model_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model");
|
||||
connect(remove_model_action, &QAction::triggered, this, [this, id]() {
|
||||
emit removeModelRequested(id);
|
||||
});
|
||||
}
|
||||
menu.exec(tree_->viewport()->mapToGlobal(pos));
|
||||
});
|
||||
}
|
||||
|
||||
void ModelsPanel::setNodes(const QList<TreeNode>& nodes) {
|
||||
tree_->clear();
|
||||
const bool has_hierarchy = std::any_of(nodes.begin(), nodes.end(), [](const TreeNode& node) {
|
||||
return !node.children.isEmpty() || node.kind == ItemKind::Group;
|
||||
});
|
||||
tree_->setRootIsDecorated(has_hierarchy);
|
||||
for (const auto& node : nodes) {
|
||||
addNode(tree_->invisibleRootItem(), node);
|
||||
}
|
||||
tree_->expandAll();
|
||||
}
|
||||
|
||||
void ModelsPanel::addNode(QTreeWidgetItem* parent, const TreeNode& node) {
|
||||
auto* item = new QTreeWidgetItem(parent, {node.name, ""});
|
||||
item->setData(0, Qt::UserRole, static_cast<int>(node.kind));
|
||||
item->setData(0, Qt::UserRole + 1, node.id);
|
||||
item->setSizeHint(0, QSize(0, 24));
|
||||
|
||||
if (node.kind == ItemKind::Group) {
|
||||
item->setIcon(0, components::icons::makeSvgIcon(":/icons/folder.svg"));
|
||||
item->setIcon(1, components::icons::makeSvgIcon(node.visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg"));
|
||||
} else {
|
||||
item->setIcon(0, components::icons::makeSvgIcon(":/icons/cube.svg"));
|
||||
item->setIcon(1, components::icons::makeSvgIcon(node.visible ? ":/icons/eye-solid.svg" : ":/icons/eye-closed.svg"));
|
||||
}
|
||||
|
||||
if (!node.visible) {
|
||||
const QBrush disabled_brush(
|
||||
QColor(ifcinterface::InterfaceSettings::instance().color("disabled_text")));
|
||||
item->setForeground(0, disabled_brush);
|
||||
item->setForeground(1, disabled_brush);
|
||||
}
|
||||
|
||||
for (const auto& child : node.children) {
|
||||
addNode(item, child);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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 QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
class ModelsPanel : public components::Panel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModelsPanel(QWidget* parent = nullptr);
|
||||
|
||||
void setNodes(const QList<TreeNode>& nodes);
|
||||
|
||||
signals:
|
||||
void visibilityToggleRequested(ItemKind kind, const QString& id);
|
||||
void addGroupRequested(const QString& parent_group_id, const QString& name);
|
||||
void renameGroupRequested(const QString& id, const QString& name);
|
||||
void moveGroupRequested(const QString& id, const QString& parent_group_id);
|
||||
void moveModelsRequested(const QStringList& ids, const QString& parent_group_id);
|
||||
void removeGroupRequested(const QString& id);
|
||||
void removeModelRequested(const QString& id);
|
||||
|
||||
private:
|
||||
void addNode(QTreeWidgetItem* parent, const TreeNode& node);
|
||||
|
||||
QTreeWidget* tree_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,482 @@
|
||||
// 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 ifcinterface::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(ifcinterface::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);
|
||||
}
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::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 ifcinterface {
|
||||
class SessionState;
|
||||
}
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
class SettingsView;
|
||||
|
||||
class SettingsDialog : public components::TabbedDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SettingsDialog(ifcinterface::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();
|
||||
|
||||
ifcinterface::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 ifcinterface::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 ifcinterface::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,
|
||||
ifcinterface::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 ifcinterface::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 ifcinterface {
|
||||
class SessionState;
|
||||
}
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
class SettingsDialog;
|
||||
|
||||
class SettingsView {
|
||||
public:
|
||||
explicit SettingsView(SettingsDialog* widget,
|
||||
ifcinterface::SessionState* session_state);
|
||||
|
||||
void refresh(const QString& fed_id) const;
|
||||
|
||||
private:
|
||||
SettingsDialog* widget_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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 ifcinterface::modules::models {
|
||||
|
||||
enum class ItemKind {
|
||||
Group,
|
||||
Model,
|
||||
};
|
||||
|
||||
struct TreeNode {
|
||||
QString id;
|
||||
QString name;
|
||||
ItemKind kind = ItemKind::Group;
|
||||
bool visible = true;
|
||||
QList<TreeNode> children;
|
||||
};
|
||||
|
||||
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 ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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 "Panel.h"
|
||||
|
||||
#include "../../InterfaceSettings.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
TreeNode makeGroupNode(const Federation* federation, const Federation::Group* group) {
|
||||
TreeNode node;
|
||||
node.id = group->id;
|
||||
node.name = group->display_name;
|
||||
node.kind = ItemKind::Group;
|
||||
node.visible = group->visible;
|
||||
|
||||
for (const auto& child_group : group->children) {
|
||||
node.children.append(makeGroupNode(federation, child_group.get()));
|
||||
}
|
||||
|
||||
for (const auto& model : federation->models()) {
|
||||
if (model.group_id != group->id) continue;
|
||||
node.children.append({
|
||||
model.id,
|
||||
model.display_name,
|
||||
ItemKind::Model,
|
||||
federation->isModelEffectivelyVisible(model.id),
|
||||
{}
|
||||
});
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent)
|
||||
: QObject(parent), widget_(widget), session_state_(session_state)
|
||||
{
|
||||
connect(session_state_, &ifcinterface::SessionState::modelsChanged,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcinterface::SessionState::federationStructureChanged,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcinterface::SessionState::visibilityChanged,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcinterface::SessionState::projectReset,
|
||||
this, [this]() { reload(); });
|
||||
connect(session_state_, &ifcinterface::SessionState::projectOpened,
|
||||
this, [this](const QString&) { reload(); });
|
||||
connect(&ifcinterface::InterfaceSettings::instance(),
|
||||
&ifcinterface::InterfaceSettings::themeChanged,
|
||||
this, [this]() { reload(); });
|
||||
|
||||
reload();
|
||||
}
|
||||
|
||||
void ModelsPanelView::reload() {
|
||||
Federation* federation = session_state_->federation();
|
||||
QList<TreeNode> nodes;
|
||||
for (const auto& root_group : federation->rootGroups()) {
|
||||
nodes.append(makeGroupNode(federation, root_group.get()));
|
||||
}
|
||||
for (const auto& model : federation->models()) {
|
||||
if (!model.group_id.isEmpty()) continue;
|
||||
nodes.append({
|
||||
model.id,
|
||||
model.display_name,
|
||||
ItemKind::Model,
|
||||
federation->isModelEffectivelyVisible(model.id),
|
||||
{}
|
||||
});
|
||||
}
|
||||
widget_->setNodes(nodes);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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>
|
||||
|
||||
namespace ifcinterface { class SessionState; }
|
||||
|
||||
namespace ifcinterface::modules::models {
|
||||
|
||||
class ModelsPanel;
|
||||
|
||||
class ModelsPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModelsPanelView(ModelsPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
private:
|
||||
void reload();
|
||||
|
||||
ModelsPanel* widget_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::models
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,207 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Controller.h"
|
||||
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../models/Controller.h"
|
||||
#include "../viewport/Controller.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
|
||||
namespace ifcinterface::modules::project {
|
||||
|
||||
ProjectController::ProjectController(QWidget* host,
|
||||
Federation* federation,
|
||||
ifcinterface::SessionState* session_state,
|
||||
ifcinterface::ElementRegistry* element_registry,
|
||||
ViewportWindow* viewport,
|
||||
ifcinterface::modules::models::ModelsPanelController* models_controller,
|
||||
ifcinterface::modules::viewport::ViewportController* viewport_controller,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, host_(host)
|
||||
, federation_(federation)
|
||||
, session_state_(session_state)
|
||||
, element_registry_(element_registry)
|
||||
, viewport_(viewport)
|
||||
, models_controller_(models_controller)
|
||||
, viewport_controller_(viewport_controller)
|
||||
{
|
||||
}
|
||||
|
||||
bool ProjectController::newProject() {
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
if (loader && loader->isLoading()) {
|
||||
QMessageBox::information(
|
||||
host_, "New Project",
|
||||
"Wait until the current model load finishes before creating a new project.");
|
||||
return false;
|
||||
}
|
||||
if (!confirmDiscardIfDirty()) return false;
|
||||
|
||||
clearScene();
|
||||
federation_->clear();
|
||||
viewport_controller_->applyFederatedFalseOrigin();
|
||||
session_state_->setStatusMessage("Project", "Untitled");
|
||||
session_state_->notifyProjectReset();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectController::openProject() {
|
||||
QFileDialog file_dialog(host_, "Open Project");
|
||||
file_dialog.setFileMode(QFileDialog::ExistingFile);
|
||||
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() != QDialog::Accepted) return false;
|
||||
|
||||
const QString path = file_dialog.selectedFiles().value(0);
|
||||
if (path.isEmpty()) return false;
|
||||
return openProject(path);
|
||||
}
|
||||
|
||||
bool ProjectController::openProject(const QString& path) {
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
if (loader && loader->isLoading()) {
|
||||
QMessageBox::information(
|
||||
host_, "Open Project",
|
||||
"Wait until the current model load finishes before opening another project.");
|
||||
return false;
|
||||
}
|
||||
if (!confirmDiscardIfDirty()) return false;
|
||||
|
||||
QStringList warnings;
|
||||
QString err;
|
||||
if (!federation_->load(path, &warnings, &err)) {
|
||||
QMessageBox::warning(host_, "Open Project",
|
||||
QString("Could not open project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
clearScene();
|
||||
|
||||
QStringList paths;
|
||||
QStringList fed_ids;
|
||||
for (const auto& model : federation_->models()) {
|
||||
if (model.source_kind != "local") continue;
|
||||
if (!QFileInfo::exists(model.source_path)) {
|
||||
warnings << QString("Source not found, kept in project: %1").arg(model.source_path);
|
||||
continue;
|
||||
}
|
||||
paths << model.source_path;
|
||||
fed_ids << model.id;
|
||||
}
|
||||
models_controller_->loadModels(paths, fed_ids);
|
||||
|
||||
if (!warnings.isEmpty()) {
|
||||
QMessageBox::warning(host_, "Open Project",
|
||||
"Project opened with warnings:\n\n" + warnings.join("\n"));
|
||||
}
|
||||
|
||||
federation_->markClean();
|
||||
viewport_controller_->applyFederatedFalseOrigin();
|
||||
if (federation_->hasHomeView()) {
|
||||
const auto& hv = federation_->homeView();
|
||||
viewport_->setCamera(
|
||||
hv.target.x(), hv.target.y(), hv.target.z(), hv.distance, hv.yaw, hv.pitch);
|
||||
}
|
||||
session_state_->setStatusMessage("Project", QFileInfo(path).fileName());
|
||||
session_state_->notifyProjectOpened(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectController::saveProject() {
|
||||
if (federation_->filePath().isEmpty()) return saveProjectAs();
|
||||
|
||||
QString err;
|
||||
if (!federation_->save(federation_->filePath(), &err)) {
|
||||
QMessageBox::warning(host_, "Save Project",
|
||||
QString("Could not save project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
session_state_->setStatusMessage("Project", QFileInfo(federation_->filePath()).fileName());
|
||||
session_state_->notifyProjectSaved(federation_->filePath());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectController::saveProjectAs() {
|
||||
QString suggested = federation_->filePath();
|
||||
if (suggested.isEmpty()) suggested = "project.ifcfed";
|
||||
|
||||
QFileDialog file_dialog(host_, "Save Project As", suggested);
|
||||
file_dialog.setAcceptMode(QFileDialog::AcceptSave);
|
||||
file_dialog.setFileMode(QFileDialog::AnyFile);
|
||||
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
|
||||
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
||||
if (file_dialog.exec() != QDialog::Accepted) return false;
|
||||
|
||||
QString path = file_dialog.selectedFiles().value(0);
|
||||
if (path.isEmpty()) return false;
|
||||
if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed";
|
||||
return saveProjectAs(path);
|
||||
}
|
||||
|
||||
bool ProjectController::saveProjectAs(const QString& path) {
|
||||
QString err;
|
||||
if (!federation_->save(path, &err)) {
|
||||
QMessageBox::warning(host_, "Save Project",
|
||||
QString("Could not save project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
session_state_->setStatusMessage("Project", QFileInfo(path).fileName());
|
||||
session_state_->notifyProjectSaved(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProjectController::clearScene() {
|
||||
viewport_->setSelectedObjectId(0);
|
||||
session_state_->setSelectedObjectId(0);
|
||||
session_state_->notifySelectionChanged();
|
||||
|
||||
const auto model_ids = session_state_->modelIds();
|
||||
for (uint32_t mid : model_ids) {
|
||||
viewport_->removeModel(mid);
|
||||
session_state_->loader()->removeModel(mid);
|
||||
}
|
||||
|
||||
session_state_->clearModelMappings();
|
||||
element_registry_->clear();
|
||||
session_state_->notifyModelsChanged();
|
||||
}
|
||||
|
||||
bool ProjectController::confirmDiscardIfDirty() {
|
||||
if (!federation_->isDirty()) return true;
|
||||
const auto result = QMessageBox::question(
|
||||
host_, "Unsaved Project",
|
||||
"The current project has unsaved changes. Save before continuing?",
|
||||
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
|
||||
QMessageBox::Save);
|
||||
if (result == QMessageBox::Cancel) return false;
|
||||
if (result == QMessageBox::Save) return saveProject();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::project
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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_PROJECT_CONTROLLER_H
|
||||
#define IFCINTERFACE_PANELS_PROJECT_CONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class QWidget;
|
||||
class Federation;
|
||||
class ViewportWindow;
|
||||
namespace ifcinterface { class ElementRegistry; }
|
||||
namespace ifcinterface { class SessionState; }
|
||||
namespace ifcinterface::modules::models { class ModelsPanelController; }
|
||||
namespace ifcinterface::modules::viewport { class ViewportController; }
|
||||
|
||||
namespace ifcinterface::modules::project {
|
||||
|
||||
class ProjectController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProjectController(QWidget* host,
|
||||
Federation* federation,
|
||||
ifcinterface::SessionState* session_state,
|
||||
ifcinterface::ElementRegistry* element_registry,
|
||||
ViewportWindow* viewport,
|
||||
ifcinterface::modules::models::ModelsPanelController* models_controller,
|
||||
ifcinterface::modules::viewport::ViewportController* viewport_controller,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
bool newProject();
|
||||
bool openProject();
|
||||
bool saveProject();
|
||||
bool saveProjectAs();
|
||||
|
||||
private:
|
||||
bool openProject(const QString& path);
|
||||
bool saveProjectAs(const QString& path);
|
||||
void clearScene();
|
||||
bool confirmDiscardIfDirty();
|
||||
|
||||
QWidget* host_ = nullptr;
|
||||
Federation* federation_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
ifcinterface::ElementRegistry* element_registry_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
ifcinterface::modules::models::ModelsPanelController* models_controller_ = nullptr;
|
||||
ifcinterface::modules::viewport::ViewportController* viewport_controller_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::project
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,237 @@
|
||||
// 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 "../../components/KeyValueTable.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/Style.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
|
||||
#include <QFrame>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
QWidget* makePropertySetPanel(const ifcinterface::modules::properties::PropertySet& property_set, QWidget* parent = nullptr) {
|
||||
auto* group = new QGroupBox(property_set.title, parent);
|
||||
group->setObjectName("propertySetBox");
|
||||
auto* layout = new QVBoxLayout(group);
|
||||
layout->setContentsMargins(10, 10, 10, 10);
|
||||
layout->setSpacing(0);
|
||||
|
||||
QList<ifcinterface::components::KeyValueTableRow> rows;
|
||||
for (const auto& row : property_set.rows) {
|
||||
rows.append({row.key, row.value, "keyValueValueLabel", "", "", 0});
|
||||
}
|
||||
layout->addWidget(new ifcinterface::components::KeyValueTable(rows, group));
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget* makeAttributeList(const QList<ifcinterface::modules::properties::KeyValueRow>& rows, QWidget* parent = nullptr) {
|
||||
QList<ifcinterface::components::KeyValueTableRow> table_rows;
|
||||
for (const auto& row : rows) {
|
||||
table_rows.append({row.key, row.value, "keyValueValueLabel", "", "", 0});
|
||||
}
|
||||
return new ifcinterface::components::KeyValueTable(table_rows, parent);
|
||||
}
|
||||
|
||||
QWidget* makeRelationshipList(const QList<ifcinterface::modules::properties::RelationshipRow>& rows, QWidget* parent = nullptr) {
|
||||
QList<ifcinterface::components::KeyValueTableRow> table_rows;
|
||||
for (const auto& row_data : rows) {
|
||||
table_rows.append({row_data.key,
|
||||
row_data.value,
|
||||
"keyValueValueLabel",
|
||||
":/icons/cursor-pointer.svg",
|
||||
"keyValueTrailingIconLabel",
|
||||
72});
|
||||
}
|
||||
return new ifcinterface::components::KeyValueTable(table_rows, parent);
|
||||
}
|
||||
|
||||
QWidget* makeFilterWrapper(QLineEdit** field_out, QWidget* parent = nullptr) {
|
||||
auto* wrapper = new QWidget(parent);
|
||||
wrapper->setObjectName("panelSectionFilterWrapper");
|
||||
auto* layout = new QVBoxLayout(wrapper);
|
||||
layout->setContentsMargins(ifcinterface::components::style::metrics::section_body_padding,
|
||||
0,
|
||||
ifcinterface::components::style::metrics::section_body_padding,
|
||||
0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
auto* field = new QLineEdit(wrapper);
|
||||
field->setClearButtonEnabled(true);
|
||||
field->addAction(ifcinterface::components::icons::makeSvgIcon(":/icons/filter.svg"), QLineEdit::LeadingPosition);
|
||||
field->setVisible(false);
|
||||
layout->addWidget(field);
|
||||
|
||||
if (field_out) *field_out = field;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
QFrame* makeEntityBox(const ifcinterface::modules::properties::EntitySummary& entity, QWidget* parent = nullptr) {
|
||||
auto* entity_box = new QFrame(parent);
|
||||
entity_box->setObjectName("entityClassBox");
|
||||
auto* entity_layout = new QHBoxLayout(entity_box);
|
||||
entity_layout->setContentsMargins(10, 8, 10, 8);
|
||||
entity_layout->setSpacing(10);
|
||||
|
||||
auto* entity_icon = new QLabel(entity_box);
|
||||
entity_icon->setPixmap(ifcinterface::components::icons::makeSvgPixmap(":/icons/cube-dots.svg", QSize(28, 28)));
|
||||
entity_icon->setAlignment(Qt::AlignCenter);
|
||||
|
||||
auto* entity_text = new QWidget(entity_box);
|
||||
auto* entity_text_layout = new QVBoxLayout(entity_text);
|
||||
entity_text_layout->setContentsMargins(0, 0, 0, 0);
|
||||
entity_text_layout->setSpacing(2);
|
||||
|
||||
auto* entity_class_label = new QLabel(entity.entity_class, entity_text);
|
||||
entity_class_label->setObjectName("entityClassLabel");
|
||||
auto* entity_type_label = new QLabel(entity.predefined_type, entity_text);
|
||||
entity_type_label->setProperty("textRole", "secondary");
|
||||
|
||||
entity_text_layout->addWidget(entity_class_label);
|
||||
entity_text_layout->addWidget(entity_type_label);
|
||||
entity_layout->addWidget(entity_icon, 0, Qt::AlignVCenter);
|
||||
entity_layout->addWidget(entity_text, 1, Qt::AlignVCenter);
|
||||
return entity_box;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ifcinterface::modules::properties {
|
||||
|
||||
PropertiesPanel::PropertiesPanel(QWidget* parent)
|
||||
: components::Panel("Properties", nullptr, parent, false, true)
|
||||
{
|
||||
}
|
||||
|
||||
void PropertiesPanel::render(const PropertiesPanelState& state) {
|
||||
clearBodyWidgets();
|
||||
|
||||
QList<QWidget*> property_set_widgets;
|
||||
for (const auto& property_set : state.property_sets) {
|
||||
property_set_widgets.append(makePropertySetPanel(property_set, this));
|
||||
}
|
||||
|
||||
QList<QWidget*> quantity_set_widgets;
|
||||
for (const auto& property_set : state.quantity_sets) {
|
||||
quantity_set_widgets.append(makePropertySetPanel(property_set, this));
|
||||
}
|
||||
|
||||
auto* entity_section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
entity_section->addBodyWidget(makeEntityBox(state.entity, this));
|
||||
|
||||
auto* attributes_section = new components::Section("Attributes", components::SectionHeaderMode::Visible, this);
|
||||
attributes_section->addBodyWidget(makeAttributeList(state.attributes, this));
|
||||
attributes_section->setExpanded(attributes_expanded_);
|
||||
|
||||
auto* relationships_section = new components::Section("Relationships", components::SectionHeaderMode::Visible, this);
|
||||
relationships_section->addBodyWidget(makeRelationshipList(state.relationships, this));
|
||||
relationships_section->setExpanded(relationships_expanded_);
|
||||
|
||||
auto* properties_section = new components::Section("Properties", components::SectionHeaderMode::Visible, this);
|
||||
auto* properties_filter_toggle = new QToolButton(properties_section);
|
||||
properties_filter_toggle->setObjectName("panelSectionFilterToggle");
|
||||
properties_filter_toggle->setCheckable(true);
|
||||
properties_filter_toggle->setIcon(components::icons::makeSvgIcon(":/icons/filter.svg"));
|
||||
properties_filter_toggle->setAutoRaise(true);
|
||||
properties_section->addHeaderWidget(properties_filter_toggle);
|
||||
QLineEdit* properties_filter_field = nullptr;
|
||||
auto* properties_filter_wrapper = makeFilterWrapper(&properties_filter_field, properties_section);
|
||||
properties_filter_field->setPlaceholderText("Filter properties or sets");
|
||||
properties_filter_field->setText(properties_filter_text_);
|
||||
properties_filter_wrapper->setVisible(properties_filter_visible_);
|
||||
properties_filter_field->setVisible(properties_filter_visible_);
|
||||
connect(properties_filter_toggle, &QToolButton::toggled, properties_filter_field, [this, properties_filter_field, properties_filter_wrapper](bool visible) {
|
||||
properties_filter_visible_ = visible;
|
||||
properties_filter_field->setVisible(visible);
|
||||
properties_filter_wrapper->setVisible(visible);
|
||||
if (visible) properties_filter_field->setFocus();
|
||||
});
|
||||
connect(properties_filter_field, &QLineEdit::textChanged, this, [this](const QString& text) {
|
||||
properties_filter_text_ = text;
|
||||
});
|
||||
properties_section->addBodyWidget(properties_filter_wrapper);
|
||||
for (auto* widget : property_set_widgets) properties_section->addBodyWidget(widget);
|
||||
properties_section->setExpanded(properties_expanded_);
|
||||
properties_filter_toggle->setChecked(properties_filter_visible_);
|
||||
|
||||
auto* quantities_section = new components::Section("Quantities", components::SectionHeaderMode::Visible, this);
|
||||
auto* quantities_filter_toggle = new QToolButton(quantities_section);
|
||||
quantities_filter_toggle->setObjectName("panelSectionFilterToggle");
|
||||
quantities_filter_toggle->setCheckable(true);
|
||||
quantities_filter_toggle->setIcon(components::icons::makeSvgIcon(":/icons/filter.svg"));
|
||||
quantities_filter_toggle->setAutoRaise(true);
|
||||
quantities_section->addHeaderWidget(quantities_filter_toggle);
|
||||
QLineEdit* quantities_filter_field = nullptr;
|
||||
auto* quantities_filter_wrapper = makeFilterWrapper(&quantities_filter_field, quantities_section);
|
||||
quantities_filter_field->setPlaceholderText("Filter quantities or sets");
|
||||
quantities_filter_field->setText(quantities_filter_text_);
|
||||
quantities_filter_wrapper->setVisible(quantities_filter_visible_);
|
||||
quantities_filter_field->setVisible(quantities_filter_visible_);
|
||||
connect(quantities_filter_toggle, &QToolButton::toggled, quantities_filter_field, [this, quantities_filter_field, quantities_filter_wrapper](bool visible) {
|
||||
quantities_filter_visible_ = visible;
|
||||
quantities_filter_field->setVisible(visible);
|
||||
quantities_filter_wrapper->setVisible(visible);
|
||||
if (visible) quantities_filter_field->setFocus();
|
||||
});
|
||||
connect(quantities_filter_field, &QLineEdit::textChanged, this, [this](const QString& text) {
|
||||
quantities_filter_text_ = text;
|
||||
});
|
||||
quantities_section->addBodyWidget(quantities_filter_wrapper);
|
||||
for (auto* widget : quantity_set_widgets) quantities_section->addBodyWidget(widget);
|
||||
quantities_section->setExpanded(quantities_expanded_);
|
||||
quantities_filter_toggle->setChecked(quantities_filter_visible_);
|
||||
|
||||
if (auto* button = attributes_section->findChild<QToolButton*>("panelSectionHeaderButton")) {
|
||||
connect(button, &QToolButton::toggled, this, [this](bool expanded) {
|
||||
attributes_expanded_ = expanded;
|
||||
});
|
||||
}
|
||||
if (auto* button = relationships_section->findChild<QToolButton*>("panelSectionHeaderButton")) {
|
||||
connect(button, &QToolButton::toggled, this, [this](bool expanded) {
|
||||
relationships_expanded_ = expanded;
|
||||
});
|
||||
}
|
||||
if (auto* button = properties_section->findChild<QToolButton*>("panelSectionHeaderButton")) {
|
||||
connect(button, &QToolButton::toggled, this, [this](bool expanded) {
|
||||
properties_expanded_ = expanded;
|
||||
});
|
||||
}
|
||||
if (auto* button = quantities_section->findChild<QToolButton*>("panelSectionHeaderButton")) {
|
||||
connect(button, &QToolButton::toggled, this, [this](bool expanded) {
|
||||
quantities_expanded_ = expanded;
|
||||
});
|
||||
}
|
||||
|
||||
addBodyWidget(entity_section);
|
||||
addBodyWidget(attributes_section);
|
||||
addBodyWidget(relationships_section);
|
||||
addBodyWidget(properties_section);
|
||||
addBodyWidget(quantities_section);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::properties
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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_PROPERTIES_PANEL_H
|
||||
#define IFCINTERFACE_MODULES_PROPERTIES_PANEL_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include "../../components/Panel.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QToolButton;
|
||||
|
||||
namespace ifcinterface::modules::properties {
|
||||
|
||||
class PropertiesPanel : public components::Panel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PropertiesPanel(QWidget* parent = nullptr);
|
||||
|
||||
void render(const PropertiesPanelState& state);
|
||||
|
||||
private:
|
||||
bool attributes_expanded_ = true;
|
||||
bool relationships_expanded_ = true;
|
||||
bool properties_expanded_ = true;
|
||||
bool quantities_expanded_ = true;
|
||||
bool properties_filter_visible_ = false;
|
||||
bool quantities_filter_visible_ = false;
|
||||
QString properties_filter_text_;
|
||||
QString quantities_filter_text_;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::properties
|
||||
|
||||
#endif
|
||||
@@ -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_PANELS_PROPERTIESPANELTYPES_H
|
||||
#define IFCINTERFACE_PANELS_PROPERTIESPANELTYPES_H
|
||||
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
|
||||
namespace ifcinterface::modules::properties {
|
||||
|
||||
struct KeyValueRow {
|
||||
QString key;
|
||||
QString value;
|
||||
};
|
||||
|
||||
struct RelationshipRow {
|
||||
QString key;
|
||||
QString value;
|
||||
};
|
||||
|
||||
struct PropertySet {
|
||||
QString title;
|
||||
QList<KeyValueRow> rows;
|
||||
};
|
||||
|
||||
struct EntitySummary {
|
||||
QString entity_class;
|
||||
QString predefined_type;
|
||||
};
|
||||
|
||||
struct PropertiesPanelState {
|
||||
EntitySummary entity;
|
||||
QList<KeyValueRow> attributes;
|
||||
QList<RelationshipRow> relationships;
|
||||
QList<PropertySet> property_sets;
|
||||
QList<PropertySet> quantity_sets;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::properties
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,121 @@
|
||||
// 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 "Panel.h"
|
||||
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
|
||||
namespace ifcinterface::modules::properties {
|
||||
|
||||
PropertiesPanelView::PropertiesPanelView(PropertiesPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent)
|
||||
: QObject(parent), widget_(widget), session_state_(session_state)
|
||||
{
|
||||
connect(session_state_, &ifcinterface::SessionState::selectionChanged, this, [this](uint32_t object_id) {
|
||||
refresh(object_id);
|
||||
});
|
||||
connect(session_state_, &ifcinterface::SessionState::projectReset, this, [this]() {
|
||||
refresh(0);
|
||||
});
|
||||
refresh(0);
|
||||
}
|
||||
|
||||
void PropertiesPanelView::refresh(uint32_t object_id) {
|
||||
auto* registry = session_state_->elementRegistry();
|
||||
PropertiesPanelState state;
|
||||
state.entity = {"IfcWall", "SOLIDWALL"};
|
||||
state.attributes = {
|
||||
{"GlobalId", "2Q$n5SLPP9Q8B7wQKjKfUQ"},
|
||||
{"Name", "Core-EXT-204"},
|
||||
{"Description", "External load-bearing wall"},
|
||||
};
|
||||
state.relationships = {
|
||||
{"Type", "Basic Wall: Exterior - 200mm"},
|
||||
{"Container", "Level 02"},
|
||||
};
|
||||
state.property_sets = {
|
||||
{"Pset_WallCommon",
|
||||
{{"Reference", "Core-EXT-204"},
|
||||
{"Status", "Reviewed"},
|
||||
{"Fire Rating", "120 min"},
|
||||
{"LoadBearing", "True"}}},
|
||||
{"Identity Data",
|
||||
{{"Type", "IfcWall"},
|
||||
{"Name", "Core-EXT-204"},
|
||||
{"Owner", "Architecture"},
|
||||
{"Phase", "Construction"}}},
|
||||
{"BIM Collaboration",
|
||||
{{"Issue Count", "2 open"},
|
||||
{"Last Review", "2026-04-30"},
|
||||
{"Assigned To", "Design Coordination"}}},
|
||||
};
|
||||
state.quantity_sets = {
|
||||
{"BaseQuantities",
|
||||
{{"Length", "6.20 m"},
|
||||
{"Height", "3.45 m"},
|
||||
{"Width", "0.30 m"},
|
||||
{"Volume", "6.42 m3"}}},
|
||||
{"Finish Quantities",
|
||||
{{"NetSideArea", "21.39 m2"},
|
||||
{"GrossArea", "22.10 m2"},
|
||||
{"Paint Coverage", "42.78 m2"}}},
|
||||
};
|
||||
|
||||
if (!registry) {
|
||||
widget_->render(state);
|
||||
return;
|
||||
}
|
||||
|
||||
auto entity = registry->findEntity(object_id);
|
||||
if (entity) {
|
||||
state.entity.entity_class = QString::fromStdString(entity->declaration().name());
|
||||
if (!state.property_sets.isEmpty() && !state.property_sets[1].rows.isEmpty()) {
|
||||
state.property_sets[1].rows[0].value = state.entity.entity_class;
|
||||
}
|
||||
} else {
|
||||
// No live IFC source for this object — typical when a pure-geometry
|
||||
// .ifcview sidecar was loaded without its .ifc/.rdb sibling. Fall
|
||||
// back to the basic info cached in the element registry so the
|
||||
// panel still shows class / name / guid for visible elements.
|
||||
auto info = registry->findBasicElementInfo(object_id);
|
||||
if (info && !info->type.isEmpty()) {
|
||||
state.entity.entity_class = info->type;
|
||||
if (!state.property_sets.isEmpty() && !state.property_sets[1].rows.isEmpty()) {
|
||||
state.property_sets[1].rows[0].value = info->type;
|
||||
}
|
||||
}
|
||||
if (info && !info->name.isEmpty()) {
|
||||
state.attributes[1].value = info->name;
|
||||
if (state.property_sets.size() > 1 && state.property_sets[1].rows.size() > 1) {
|
||||
state.property_sets[1].rows[1].value = info->name;
|
||||
}
|
||||
}
|
||||
if (info && !info->guid.isEmpty()) {
|
||||
state.attributes[0].value = info->guid;
|
||||
}
|
||||
}
|
||||
widget_->render(state);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::properties
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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_PROPERTIESPANELVIEW_H
|
||||
#define IFCINTERFACE_PANELS_PROPERTIESPANELVIEW_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace ifcinterface { class SessionState; }
|
||||
namespace ifcinterface::modules::properties {
|
||||
|
||||
class PropertiesPanel;
|
||||
|
||||
class PropertiesPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PropertiesPanelView(PropertiesPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
private:
|
||||
void refresh(uint32_t object_id);
|
||||
|
||||
PropertiesPanel* widget_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::properties
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,357 @@
|
||||
// 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 "Dialog.h"
|
||||
|
||||
#include "../../InterfaceSettings.h"
|
||||
#include "../../../ifcviewer/AppSettings.h"
|
||||
#include "../../components/Dialog.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../components/Style.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QColorDialog>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFrame>
|
||||
#include <QFormLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QShowEvent>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace ifcinterface::modules::settings {
|
||||
|
||||
SettingsDialog::SettingsDialog(QWidget* parent)
|
||||
: components::TabbedDialog(parent)
|
||||
{
|
||||
setObjectName("appDialog");
|
||||
setWindowTitle("Settings");
|
||||
setModal(true);
|
||||
resize(520, 420);
|
||||
setupUi();
|
||||
}
|
||||
|
||||
void SettingsDialog::showEvent(QShowEvent* event) {
|
||||
syncFromSettings();
|
||||
QDialog::showEvent(event);
|
||||
}
|
||||
|
||||
void SettingsDialog::setupUi() {
|
||||
auto* graphics_tab = new QWidget(this);
|
||||
auto* graphics_layout = new QVBoxLayout(graphics_tab);
|
||||
graphics_layout->setContentsMargins(0, 0, 0, 0);
|
||||
graphics_layout->setSpacing(components::style::metrics::padding);
|
||||
graphics_layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
auto* general_section = new components::Section("General", components::SectionHeaderMode::Visible, graphics_tab);
|
||||
auto* general_body = new QWidget(general_section);
|
||||
auto* general_form = new QFormLayout(general_body);
|
||||
general_form->setContentsMargins(0, 0, 0, 0);
|
||||
general_form->setHorizontalSpacing(16);
|
||||
general_form->setVerticalSpacing(10);
|
||||
|
||||
geometry_library_edit_ = new QLineEdit(general_body);
|
||||
geometry_library_edit_->setMinimumWidth(300);
|
||||
general_form->addRow("Geometry Library", geometry_library_edit_);
|
||||
|
||||
show_stats_check_ = new QCheckBox(general_body);
|
||||
general_form->addRow("Show Performance Stats", show_stats_check_);
|
||||
|
||||
backface_culling_check_ = new QCheckBox(general_body);
|
||||
backface_culling_check_->setToolTip(
|
||||
"Skip triangles facing away from the camera. Big FPS win on closed solids; "
|
||||
"disable if you see holes in open geometry.");
|
||||
general_form->addRow("Backface Culling", backface_culling_check_);
|
||||
|
||||
general_section->addBodyWidget(general_body);
|
||||
|
||||
auto* loading_section = new components::Section("Loading", components::SectionHeaderMode::Visible, graphics_tab);
|
||||
auto* loading_body = new QWidget(loading_section);
|
||||
auto* loading_form = new QFormLayout(loading_body);
|
||||
loading_form->setContentsMargins(0, 0, 0, 0);
|
||||
loading_form->setHorizontalSpacing(16);
|
||||
loading_form->setVerticalSpacing(10);
|
||||
|
||||
void_limit_spin_ = new QSpinBox(loading_body);
|
||||
void_limit_spin_->setRange(0, 100000);
|
||||
loading_form->addRow("Void Limit", void_limit_spin_);
|
||||
|
||||
deflection_tolerance_spin_ = new QDoubleSpinBox(loading_body);
|
||||
deflection_tolerance_spin_->setRange(0.000001, 1000.0);
|
||||
deflection_tolerance_spin_->setDecimals(6);
|
||||
deflection_tolerance_spin_->setSingleStep(0.001);
|
||||
loading_form->addRow("Deflection Tolerance", deflection_tolerance_spin_);
|
||||
|
||||
angular_tolerance_spin_ = new QDoubleSpinBox(loading_body);
|
||||
angular_tolerance_spin_->setRange(0.000001, 3.141592);
|
||||
angular_tolerance_spin_->setDecimals(6);
|
||||
angular_tolerance_spin_->setSingleStep(0.05);
|
||||
loading_form->addRow("Angular Tolerance", angular_tolerance_spin_);
|
||||
|
||||
min_pixel_radius_spin_ = new QDoubleSpinBox(loading_body);
|
||||
min_pixel_radius_spin_->setRange(0.0, 100.0);
|
||||
min_pixel_radius_spin_->setDecimals(2);
|
||||
min_pixel_radius_spin_->setSingleStep(0.5);
|
||||
min_pixel_radius_spin_->setToolTip(
|
||||
"Minimum projected sphere radius (in pixels) for an instance to "
|
||||
"be drawn. Bigger = faster but more pop-in on small detail.");
|
||||
loading_form->addRow("Min Pixel Radius", min_pixel_radius_spin_);
|
||||
|
||||
motion_min_pixel_radius_spin_ = new QDoubleSpinBox(loading_body);
|
||||
motion_min_pixel_radius_spin_->setRange(0.0, 100.0);
|
||||
motion_min_pixel_radius_spin_->setDecimals(2);
|
||||
motion_min_pixel_radius_spin_->setSingleStep(1.0);
|
||||
motion_min_pixel_radius_spin_->setToolTip(
|
||||
"Aggressive cull threshold while the camera is moving. 0 = no "
|
||||
"motion boost (motion uses the same threshold as still frames).");
|
||||
loading_form->addRow("Motion Min Pixel Radius", motion_min_pixel_radius_spin_);
|
||||
|
||||
lod1_pixel_threshold_spin_ = new QDoubleSpinBox(loading_body);
|
||||
lod1_pixel_threshold_spin_->setRange(0.0, 1000.0);
|
||||
lod1_pixel_threshold_spin_->setDecimals(1);
|
||||
lod1_pixel_threshold_spin_->setSingleStep(1.0);
|
||||
lod1_pixel_threshold_spin_->setToolTip(
|
||||
"Pixel radius below which an instance switches to its LOD1 "
|
||||
"representation. 0 disables LOD1 entirely.");
|
||||
loading_form->addRow("LOD1 Pixel Threshold", lod1_pixel_threshold_spin_);
|
||||
|
||||
hiz_enabled_check_ = new QCheckBox(loading_body);
|
||||
hiz_enabled_check_->setToolTip(
|
||||
"Enable HiZ (hierarchical Z) occlusion culling. Hides geometry "
|
||||
"behind opaque blockers based on a downsampled depth pyramid.");
|
||||
loading_form->addRow("HiZ Occlusion", hiz_enabled_check_);
|
||||
|
||||
hiz_resolution_spin_ = new QSpinBox(loading_body);
|
||||
hiz_resolution_spin_->setRange(64, 4096);
|
||||
hiz_resolution_spin_->setSingleStep(64);
|
||||
hiz_resolution_spin_->setToolTip(
|
||||
"Base HiZ pyramid width in texels (height tracks aspect). "
|
||||
"Changes take effect on next viewport reinitialization.");
|
||||
loading_form->addRow("HiZ Resolution", hiz_resolution_spin_);
|
||||
|
||||
loading_section->addBodyWidget(loading_body);
|
||||
graphics_layout->addWidget(general_section);
|
||||
graphics_layout->addWidget(loading_section);
|
||||
graphics_layout->addStretch(1);
|
||||
|
||||
// Navigation tab: orbit / pan presets. Selection always stays on
|
||||
// LMB so click + box-select keep working regardless of preset.
|
||||
auto* interface_tab = new QWidget(this);
|
||||
{
|
||||
auto* layout = new QVBoxLayout(interface_tab);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(components::style::metrics::padding);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
auto* section = new components::Section(
|
||||
"Navigation", components::SectionHeaderMode::Visible, interface_tab);
|
||||
auto* body = new QWidget(section);
|
||||
auto* form = new QFormLayout(body);
|
||||
form->setContentsMargins(0, 0, 0, 0);
|
||||
form->setHorizontalSpacing(16);
|
||||
form->setVerticalSpacing(10);
|
||||
|
||||
nav_preset_combo_ = new QComboBox(body);
|
||||
// Order must match AppSettings::NavPreset enum ordering — index
|
||||
// is what we read back via currentIndex / setCurrentIndex.
|
||||
nav_preset_combo_->addItem("Blender (Orbit MMB, Pan Shift+MMB)");
|
||||
nav_preset_combo_->addItem("Rhino (Orbit RMB, Pan Shift+RMB)");
|
||||
nav_preset_combo_->addItem("Revit (Orbit Shift+MMB, Pan MMB)");
|
||||
nav_preset_combo_->setToolTip(
|
||||
"Mouse-button mapping for orbit and pan. Selection stays on "
|
||||
"left mouse button for every preset, so click + box-select "
|
||||
"always work.");
|
||||
form->addRow("Preset", nav_preset_combo_);
|
||||
|
||||
section->addBodyWidget(body);
|
||||
layout->addWidget(section);
|
||||
|
||||
auto* theme_section = new components::Section(
|
||||
"Theme", components::SectionHeaderMode::Visible, interface_tab);
|
||||
auto* theme_body = new QWidget(theme_section);
|
||||
auto* theme_layout = new QVBoxLayout(theme_body);
|
||||
theme_layout->setContentsMargins(0, 0, 0, 0);
|
||||
theme_layout->setSpacing(components::style::metrics::padding);
|
||||
|
||||
auto* theme_form = new QFormLayout();
|
||||
theme_form->setContentsMargins(0, 0, 0, 0);
|
||||
theme_form->setHorizontalSpacing(16);
|
||||
theme_form->setVerticalSpacing(10);
|
||||
|
||||
theme_mode_combo_ = new QComboBox(theme_body);
|
||||
theme_mode_combo_->addItem("Dark", static_cast<int>(ifcinterface::InterfaceSettings::ThemeMode::Dark));
|
||||
theme_mode_combo_->addItem("Light", static_cast<int>(ifcinterface::InterfaceSettings::ThemeMode::Light));
|
||||
theme_mode_combo_->addItem("Custom", static_cast<int>(ifcinterface::InterfaceSettings::ThemeMode::Custom));
|
||||
theme_form->addRow("Preset", theme_mode_combo_);
|
||||
|
||||
theme_custom_body_ = new QWidget(theme_body);
|
||||
auto* custom_grid = new QGridLayout(theme_custom_body_);
|
||||
custom_grid->setContentsMargins(0, 0, 0, 0);
|
||||
custom_grid->setHorizontalSpacing(12);
|
||||
custom_grid->setVerticalSpacing(8);
|
||||
|
||||
int row = 0;
|
||||
for (const auto& spec : ifcinterface::InterfaceSettings::themeColorSpecs()) {
|
||||
auto* label = new QLabel(QString::fromUtf8(spec.label), theme_custom_body_);
|
||||
auto* edit = new QLineEdit(theme_custom_body_);
|
||||
edit->setPlaceholderText("#000000");
|
||||
auto* pick = new QPushButton("Pick", theme_custom_body_);
|
||||
connect(pick, &QPushButton::clicked, this, [this, edit]() { pickThemeColor(edit); });
|
||||
custom_grid->addWidget(label, row, 0);
|
||||
custom_grid->addWidget(edit, row, 1);
|
||||
custom_grid->addWidget(pick, row, 2);
|
||||
theme_color_editors_.push_back({QString::fromUtf8(spec.key), edit});
|
||||
++row;
|
||||
}
|
||||
|
||||
auto* theme_form_widget = new QWidget(theme_body);
|
||||
theme_form_widget->setLayout(theme_form);
|
||||
theme_layout->addWidget(theme_form_widget);
|
||||
theme_layout->addWidget(theme_custom_body_);
|
||||
theme_section->addBodyWidget(theme_body);
|
||||
layout->addWidget(theme_section);
|
||||
|
||||
connect(theme_mode_combo_, &QComboBox::currentIndexChanged, this, [this](int) {
|
||||
updateThemeEditorEnabled();
|
||||
});
|
||||
}
|
||||
|
||||
auto make_placeholder_tab = [this](const QString& title, const QString& detail) {
|
||||
auto* tab = new QWidget(this);
|
||||
auto* layout = new QVBoxLayout(tab);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(components::style::metrics::padding);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
auto* section = new components::Section(title, components::SectionHeaderMode::Visible, tab);
|
||||
auto* body = new QWidget(section);
|
||||
auto* body_layout = new QVBoxLayout(body);
|
||||
body_layout->setContentsMargins(0, 0, 0, 0);
|
||||
body_layout->setSpacing(8);
|
||||
|
||||
auto* heading = new QLabel(title, body);
|
||||
auto* content = new QLabel(detail, body);
|
||||
content->setProperty("textRole", "secondary");
|
||||
content->setWordWrap(true);
|
||||
|
||||
body_layout->addWidget(heading);
|
||||
body_layout->addWidget(content);
|
||||
section->addBodyWidget(body);
|
||||
layout->addWidget(section);
|
||||
layout->addStretch(1);
|
||||
return tab;
|
||||
};
|
||||
|
||||
addTab("Interface", interface_tab);
|
||||
addTab("Keybindings", make_placeholder_tab("Keybindings", "Shortcut presets and command bindings will live here."));
|
||||
addTab("Graphics", graphics_tab);
|
||||
addTab("About", make_placeholder_tab("About", "Version, credits, and environment information will live here."));
|
||||
|
||||
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, &SettingsDialog::onAccepted);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
addFooterWidget(buttons);
|
||||
}
|
||||
|
||||
void SettingsDialog::syncFromSettings() {
|
||||
geometry_library_edit_->setText(AppSettings::instance().geometryLibrary());
|
||||
show_stats_check_->setChecked(AppSettings::instance().showStats());
|
||||
backface_culling_check_->setChecked(AppSettings::instance().backfaceCulling());
|
||||
void_limit_spin_->setValue(AppSettings::instance().voidLimit());
|
||||
deflection_tolerance_spin_->setValue(AppSettings::instance().deflectionTolerance());
|
||||
angular_tolerance_spin_->setValue(AppSettings::instance().angularTolerance());
|
||||
min_pixel_radius_spin_->setValue(AppSettings::instance().minPixelRadius());
|
||||
motion_min_pixel_radius_spin_->setValue(AppSettings::instance().motionMinPixelRadius());
|
||||
lod1_pixel_threshold_spin_->setValue(AppSettings::instance().lod1PixelThreshold());
|
||||
hiz_enabled_check_->setChecked(AppSettings::instance().hizEnabled());
|
||||
hiz_resolution_spin_->setValue(AppSettings::instance().hizResolution());
|
||||
nav_preset_combo_->setCurrentIndex(static_cast<int>(AppSettings::instance().navPreset()));
|
||||
syncThemeSettings();
|
||||
}
|
||||
|
||||
void SettingsDialog::syncThemeSettings() {
|
||||
const auto& settings = ifcinterface::InterfaceSettings::instance();
|
||||
const int idx = theme_mode_combo_->findData(static_cast<int>(settings.themeMode()));
|
||||
theme_mode_combo_->setCurrentIndex(idx >= 0 ? idx : 0);
|
||||
for (auto& editor : theme_color_editors_) {
|
||||
editor.edit->setText(settings.customColor(editor.key));
|
||||
}
|
||||
updateThemeEditorEnabled();
|
||||
}
|
||||
|
||||
void SettingsDialog::updateThemeEditorEnabled() {
|
||||
if (!theme_mode_combo_ || !theme_custom_body_) return;
|
||||
const auto mode =
|
||||
static_cast<ifcinterface::InterfaceSettings::ThemeMode>(theme_mode_combo_->currentData().toInt());
|
||||
const bool is_custom = mode == ifcinterface::InterfaceSettings::ThemeMode::Custom;
|
||||
theme_custom_body_->setVisible(is_custom);
|
||||
theme_custom_body_->setEnabled(is_custom);
|
||||
}
|
||||
|
||||
void SettingsDialog::pickThemeColor(QLineEdit* edit) {
|
||||
QColorDialog dialog(QColor(edit->text()), this);
|
||||
dialog.setObjectName("appDialog");
|
||||
dialog.setWindowTitle("Choose Color");
|
||||
dialog.setOption(QColorDialog::DontUseNativeDialog, true);
|
||||
if (dialog.exec() != QDialog::Accepted) return;
|
||||
const QColor color = dialog.selectedColor();
|
||||
if (!color.isValid()) return;
|
||||
edit->setText(color.name(QColor::HexRgb));
|
||||
}
|
||||
|
||||
void SettingsDialog::onAccepted() {
|
||||
AppSettings::instance().setGeometryLibrary(geometry_library_edit_->text());
|
||||
AppSettings::instance().setShowStats(show_stats_check_->isChecked());
|
||||
AppSettings::instance().setBackfaceCulling(backface_culling_check_->isChecked());
|
||||
AppSettings::instance().setVoidLimit(void_limit_spin_->value());
|
||||
AppSettings::instance().setDeflectionTolerance(deflection_tolerance_spin_->value());
|
||||
AppSettings::instance().setAngularTolerance(angular_tolerance_spin_->value());
|
||||
AppSettings::instance().setMinPixelRadius(min_pixel_radius_spin_->value());
|
||||
AppSettings::instance().setMotionMinPixelRadius(motion_min_pixel_radius_spin_->value());
|
||||
AppSettings::instance().setLod1PixelThreshold(lod1_pixel_threshold_spin_->value());
|
||||
AppSettings::instance().setHizEnabled(hiz_enabled_check_->isChecked());
|
||||
AppSettings::instance().setHizResolution(hiz_resolution_spin_->value());
|
||||
AppSettings::instance().setNavPreset(
|
||||
static_cast<AppSettings::NavPreset>(nav_preset_combo_->currentIndex()));
|
||||
auto& interface_settings = ifcinterface::InterfaceSettings::instance();
|
||||
interface_settings.setThemeMode(
|
||||
static_cast<ifcinterface::InterfaceSettings::ThemeMode>(theme_mode_combo_->currentData().toInt()));
|
||||
for (const auto& editor : theme_color_editors_) {
|
||||
interface_settings.setCustomColor(editor.key, editor.edit->text());
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::settings
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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_SETTINGSDIALOG_H
|
||||
#define IFCINTERFACE_PANELS_SETTINGSDIALOG_H
|
||||
|
||||
#include "../../components/Dialog.h"
|
||||
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QDoubleSpinBox;
|
||||
class QLineEdit;
|
||||
class QShowEvent;
|
||||
class QSpinBox;
|
||||
class QWidget;
|
||||
|
||||
namespace ifcinterface::modules::settings {
|
||||
|
||||
class SettingsDialog : public components::TabbedDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SettingsDialog(QWidget* parent = nullptr);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent* event) override;
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void syncFromSettings();
|
||||
void syncThemeSettings();
|
||||
void updateThemeEditorEnabled();
|
||||
void pickThemeColor(QLineEdit* edit);
|
||||
void onAccepted();
|
||||
|
||||
struct ThemeColorEditor {
|
||||
QString key;
|
||||
QLineEdit* edit = nullptr;
|
||||
};
|
||||
|
||||
QLineEdit* geometry_library_edit_ = nullptr;
|
||||
QCheckBox* show_stats_check_ = nullptr;
|
||||
QCheckBox* backface_culling_check_ = nullptr;
|
||||
QSpinBox* void_limit_spin_ = nullptr;
|
||||
QDoubleSpinBox* deflection_tolerance_spin_ = nullptr;
|
||||
QDoubleSpinBox* angular_tolerance_spin_ = nullptr;
|
||||
QDoubleSpinBox* min_pixel_radius_spin_ = nullptr;
|
||||
QDoubleSpinBox* motion_min_pixel_radius_spin_ = nullptr;
|
||||
QDoubleSpinBox* lod1_pixel_threshold_spin_ = nullptr;
|
||||
QCheckBox* hiz_enabled_check_ = nullptr;
|
||||
QSpinBox* hiz_resolution_spin_ = nullptr;
|
||||
QComboBox* nav_preset_combo_ = nullptr;
|
||||
QComboBox* theme_mode_combo_ = nullptr;
|
||||
QWidget* theme_custom_body_ = nullptr;
|
||||
std::vector<ThemeColorEditor> theme_color_editors_;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::settings
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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 "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
namespace ifcinterface::modules::spatial_hierarchy {
|
||||
|
||||
SpatialHierarchyPanel::SpatialHierarchyPanel(QWidget* parent)
|
||||
: components::Panel("Spatial Hierarchy", nullptr, parent)
|
||||
{
|
||||
auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
|
||||
tree_ = new QTreeWidget(section);
|
||||
tree_->setColumnCount(2);
|
||||
tree_->setHeaderLabels({"Spatial Item", ""});
|
||||
tree_->setIconSize(QSize(16, 16));
|
||||
tree_->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
tree_->setUniformRowHeights(true);
|
||||
tree_->header()->setStretchLastSection(false);
|
||||
tree_->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
tree_->header()->setSectionResizeMode(1, QHeaderView::Fixed);
|
||||
tree_->header()->resizeSection(1, 28);
|
||||
tree_->header()->hide();
|
||||
section->addBodyWidget(tree_);
|
||||
addBodyWidget(section);
|
||||
|
||||
connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int column) {
|
||||
if (!item || column != 1) return;
|
||||
emit visibilityToggleRequested(itemPath(item));
|
||||
});
|
||||
}
|
||||
|
||||
void SpatialHierarchyPanel::setNodes(const QList<TreeNode>& nodes) {
|
||||
tree_->clear();
|
||||
for (const auto& node : nodes) {
|
||||
addNode(tree_->invisibleRootItem(), node);
|
||||
}
|
||||
tree_->expandAll();
|
||||
}
|
||||
|
||||
void SpatialHierarchyPanel::addNode(QTreeWidgetItem* parent, const TreeNode& node) {
|
||||
auto* item = new QTreeWidgetItem(parent, {node.name, ""});
|
||||
item->setData(1, Qt::UserRole, node.visible);
|
||||
item->setSizeHint(0, QSize(0, 24));
|
||||
item->setIcon(0, components::icons::makeSvgIcon(iconPath(node.kind)));
|
||||
item->setIcon(1, components::icons::makeSvgIcon(node.visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg"));
|
||||
for (const auto& child : node.children) {
|
||||
addNode(item, child);
|
||||
}
|
||||
}
|
||||
|
||||
NodePath SpatialHierarchyPanel::itemPath(QTreeWidgetItem* item) const {
|
||||
NodePath path;
|
||||
while (item) {
|
||||
path.prepend(item->text(0));
|
||||
item = item->parent();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
QString SpatialHierarchyPanel::iconPath(ItemKind kind) const {
|
||||
switch (kind) {
|
||||
case ItemKind::Site: return ":/icons/frame-alt.svg";
|
||||
case ItemKind::Building: return ":/icons/city.svg";
|
||||
case ItemKind::Storey: return ":/icons/planimetry.svg";
|
||||
case ItemKind::Space: return ":/icons/square3d-from-center.svg";
|
||||
}
|
||||
return ":/icons/frame-alt.svg";
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::spatial_hierarchy
|
||||
@@ -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_MODULES_SPATIAL_HIERARCHY_PANEL_H
|
||||
#define IFCINTERFACE_MODULES_SPATIAL_HIERARCHY_PANEL_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include "../../components/Panel.h"
|
||||
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
|
||||
namespace ifcinterface::modules::spatial_hierarchy {
|
||||
|
||||
class SpatialHierarchyPanel : public components::Panel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SpatialHierarchyPanel(QWidget* parent = nullptr);
|
||||
|
||||
void setNodes(const QList<TreeNode>& nodes);
|
||||
|
||||
signals:
|
||||
void visibilityToggleRequested(const NodePath& path);
|
||||
|
||||
private:
|
||||
void addNode(QTreeWidgetItem* parent, const TreeNode& node);
|
||||
NodePath itemPath(QTreeWidgetItem* item) const;
|
||||
QString iconPath(ItemKind kind) const;
|
||||
|
||||
QTreeWidget* tree_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::spatial_hierarchy
|
||||
|
||||
#endif
|
||||
@@ -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_PANELS_SPATIALHIERARCHYPANELTYPES_H
|
||||
#define IFCINTERFACE_PANELS_SPATIALHIERARCHYPANELTYPES_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace ifcinterface::modules::spatial_hierarchy {
|
||||
|
||||
enum class ItemKind {
|
||||
Site,
|
||||
Building,
|
||||
Storey,
|
||||
Space,
|
||||
};
|
||||
|
||||
struct TreeNode {
|
||||
QString name;
|
||||
ItemKind kind = ItemKind::Space;
|
||||
bool visible = true;
|
||||
QList<TreeNode> children;
|
||||
};
|
||||
|
||||
using NodePath = QStringList;
|
||||
|
||||
} // namespace ifcinterface::modules::spatial_hierarchy
|
||||
|
||||
#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 "Panel.h"
|
||||
|
||||
#include "../../SessionState.h"
|
||||
|
||||
namespace ifcinterface::modules::spatial_hierarchy {
|
||||
|
||||
namespace {
|
||||
|
||||
TreeNode* findNodeRecursive(QList<TreeNode>& nodes, const NodePath& path, int depth) {
|
||||
for (auto& node : nodes) {
|
||||
if (node.name != path.at(depth)) continue;
|
||||
if (depth == path.size() - 1) return &node;
|
||||
return findNodeRecursive(node.children, path, depth + 1);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpatialHierarchyPanelView::SpatialHierarchyPanelView(SpatialHierarchyPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent)
|
||||
: QObject(parent), widget_(widget), session_state_(session_state)
|
||||
{
|
||||
nodes_ = {
|
||||
{"Site A", ItemKind::Site, true,
|
||||
{{"Building 01", ItemKind::Building, true,
|
||||
{{"Level 02", ItemKind::Storey, true,
|
||||
{{"Lobby", ItemKind::Space, true, {}},
|
||||
{"Core", ItemKind::Space, true, {}}}}}}}},
|
||||
};
|
||||
|
||||
connect(widget_, &SpatialHierarchyPanel::visibilityToggleRequested, this, [this](const NodePath& path) {
|
||||
if (auto* node = findNode(path)) {
|
||||
node->visible = !node->visible;
|
||||
reload();
|
||||
session_state_->setStatusMessage("Spatial", node->visible ? "Item shown" : "Item hidden");
|
||||
}
|
||||
});
|
||||
|
||||
reload();
|
||||
}
|
||||
|
||||
void SpatialHierarchyPanelView::reload() {
|
||||
widget_->setNodes(nodes_);
|
||||
}
|
||||
|
||||
TreeNode* SpatialHierarchyPanelView::findNode(const NodePath& path) {
|
||||
if (path.isEmpty()) return nullptr;
|
||||
return findNodeRecursive(nodes_, path, 0);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::spatial_hierarchy
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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_SPATIALHIERARCHYPANELVIEW_H
|
||||
#define IFCINTERFACE_PANELS_SPATIALHIERARCHYPANELVIEW_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace ifcinterface { class SessionState; }
|
||||
namespace ifcinterface::modules::spatial_hierarchy {
|
||||
|
||||
class SpatialHierarchyPanel;
|
||||
|
||||
class SpatialHierarchyPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SpatialHierarchyPanelView(SpatialHierarchyPanel* widget,
|
||||
ifcinterface::SessionState* session_state,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
private:
|
||||
void reload();
|
||||
TreeNode* findNode(const NodePath& path);
|
||||
|
||||
SpatialHierarchyPanel* widget_ = nullptr;
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
QList<TreeNode> nodes_;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::spatial_hierarchy
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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 "../../components/Section.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace ifcinterface::modules::todo {
|
||||
|
||||
TodoPanel::TodoPanel(const QString& title, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
|
||||
auto* body = new QWidget(section);
|
||||
auto* body_layout = new QVBoxLayout(body);
|
||||
body_layout->setContentsMargins(0, 12, 0, 12);
|
||||
body_layout->setSpacing(12);
|
||||
|
||||
auto* heading = new QLabel(title, body);
|
||||
|
||||
auto* content = new QLabel("Coming soon", body);
|
||||
content->setProperty("textRole", "disabled");
|
||||
content->setAlignment(Qt::AlignCenter);
|
||||
|
||||
body_layout->addWidget(heading);
|
||||
body_layout->addStretch(1);
|
||||
body_layout->addWidget(content);
|
||||
body_layout->addStretch(1);
|
||||
|
||||
section->addBodyWidget(body);
|
||||
layout->addWidget(section);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::todo
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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_TODO_PANEL_H
|
||||
#define IFCINTERFACE_MODULES_TODO_PANEL_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace ifcinterface::modules::todo {
|
||||
|
||||
class TodoPanel : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TodoPanel(const QString& title, QWidget* parent = nullptr);
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::todo
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,337 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Controller.h"
|
||||
|
||||
#include "../../InterfaceSettings.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/AppSettings.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/SceneLoader.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
#include "../../../ifcviewer/OverlayRenderer.h"
|
||||
#include "../../Measurement.h"
|
||||
|
||||
#include <Eigen/Dense>
|
||||
#include <QVector3D>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace ifcinterface::modules::viewport {
|
||||
|
||||
ViewportController::ViewportController(ifcinterface::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, session_state_(session_state)
|
||||
, viewport_(viewport)
|
||||
, area_measurement_(std::make_unique<AreaMeasurement>())
|
||||
, length_measurement_(std::make_unique<LengthMeasurement>())
|
||||
{
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
connect(&ifcinterface::InterfaceSettings::instance(),
|
||||
&ifcinterface::InterfaceSettings::themeChanged,
|
||||
this, [this]() {
|
||||
viewport_->setBackgroundColor(QColor(ifcinterface::InterfaceSettings::instance().color("viewport_background")));
|
||||
});
|
||||
viewport_->setBackgroundColor(QColor(ifcinterface::InterfaceSettings::instance().color("viewport_background")));
|
||||
connect(federation, &Federation::federatedFalseOriginChanged,
|
||||
this, &ViewportController::applyFederatedFalseOrigin);
|
||||
connect(federation, &Federation::configChanged, this, [this]() {
|
||||
applyFederatedFalseOrigin();
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
applyModelTransformation(mid);
|
||||
}
|
||||
});
|
||||
connect(federation, &Federation::modelTransformationChanged,
|
||||
this, [this](const QString& fed_id) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelTransformation(mid);
|
||||
});
|
||||
connect(federation, &Federation::modelVisibilityChanged,
|
||||
this, [this](const QString& fed_id, bool /*visible*/) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelVisibility(mid);
|
||||
});
|
||||
connect(federation, &Federation::modelGroupChanged,
|
||||
this, [this](const QString& fed_id, const QString& /*group_id*/) {
|
||||
const uint32_t mid = session_state_->modelIdForFedId(fed_id);
|
||||
if (mid != 0) applyModelVisibility(mid);
|
||||
});
|
||||
connect(federation, &Federation::groupVisibilityChanged,
|
||||
this, [this](const QString&, bool /*visible*/) {
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
applyModelVisibility(mid);
|
||||
}
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromSidecar, this,
|
||||
[this](uint32_t mid, qint64 /*elapsed_ms*/) {
|
||||
applyCoordinateOperation(mid);
|
||||
applyModelVisibility(mid);
|
||||
maybeGuessFederatedFalseOrigin(mid);
|
||||
});
|
||||
connect(loader, &SceneLoader::dataSourceReady, this,
|
||||
[this](uint32_t mid) {
|
||||
applyCoordinateOperation(mid);
|
||||
});
|
||||
connect(loader, &SceneLoader::loadedFromStream, this,
|
||||
[this](uint32_t mid, qint64 /*elapsed_ms*/) {
|
||||
applyCoordinateOperation(mid);
|
||||
applyModelVisibility(mid);
|
||||
maybeGuessFederatedFalseOrigin(mid);
|
||||
});
|
||||
connect(viewport_, &ViewportWindow::surfacePickedInTool, this,
|
||||
[this](int x, int y, int modifiers) {
|
||||
const bool alt = (modifiers & Qt::AltModifier) != 0;
|
||||
switch (viewport_->toolMode()) {
|
||||
case ViewportWindow::ToolMode::Area:
|
||||
area_measurement_->onPick(*viewport_, x, y, alt);
|
||||
viewport_->setHudText(QString("Area: %1 m² (%2 tris)")
|
||||
.arg(area_measurement_->totalArea(), 0, 'f', 4)
|
||||
.arg(area_measurement_->triangleCount()));
|
||||
break;
|
||||
case ViewportWindow::ToolMode::Length:
|
||||
length_measurement_->onPick(*viewport_, x, y, alt);
|
||||
break;
|
||||
case ViewportWindow::ToolMode::None:
|
||||
case ViewportWindow::ToolMode::Volume:
|
||||
break;
|
||||
}
|
||||
});
|
||||
connect(viewport_, &ViewportWindow::toolModeChanged, this,
|
||||
[this](ViewportWindow::ToolMode mode) {
|
||||
area_measurement_->clear(*viewport_);
|
||||
length_measurement_->clear(*viewport_);
|
||||
switch (mode) {
|
||||
case ViewportWindow::ToolMode::None:
|
||||
viewport_->setHudText(QString());
|
||||
viewport_->setOverlayLabels({});
|
||||
session_state_->setStatusMessage("Measure", "Measurement tool off");
|
||||
break;
|
||||
case ViewportWindow::ToolMode::Length:
|
||||
viewport_->setHudText("Length tool: click first point");
|
||||
session_state_->setStatusMessage("Measure", "Length tool: LMB add point, Backspace remove last, Esc exits");
|
||||
break;
|
||||
case ViewportWindow::ToolMode::Area:
|
||||
viewport_->setHudText("Area: 0.0000 m² (0 tris)");
|
||||
session_state_->setStatusMessage("Measure", "Area tool: LMB add, Alt+LMB single tri, click again to remove, Esc exits");
|
||||
break;
|
||||
case ViewportWindow::ToolMode::Volume:
|
||||
session_state_->setStatusMessage("Measure", "Volume tool: click / box-select objects, Esc exits");
|
||||
updateVolumeReadout();
|
||||
break;
|
||||
}
|
||||
});
|
||||
connect(viewport_, &ViewportWindow::toolBackspacePressed, this, [this]() {
|
||||
if (viewport_->toolMode() == ViewportWindow::ToolMode::Length) {
|
||||
length_measurement_->removeLastPoint(*viewport_);
|
||||
}
|
||||
});
|
||||
connect(viewport_, &ViewportWindow::objectPicked, this, [this](uint32_t) {
|
||||
updateVolumeReadout();
|
||||
});
|
||||
}
|
||||
|
||||
ViewportController::~ViewportController() = default;
|
||||
|
||||
void ViewportController::applyCoordinateOperation(uint32_t mid) {
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
|
||||
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
||||
if (georef->has_coordinate_operation) {
|
||||
matrix = georef->coordinate_operation_meters;
|
||||
}
|
||||
}
|
||||
viewport_->setModelCoordinateOperation(mid, matrix);
|
||||
applyModelTransformation(mid);
|
||||
}
|
||||
|
||||
void ViewportController::applyModelTransformation(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
Eigen::Matrix4d matrix = Eigen::Matrix4d::Identity();
|
||||
const QString fed_id = session_state_->fedIdForModelId(mid);
|
||||
if (!fed_id.isEmpty()) {
|
||||
if (const Federation::Model* model = federation->findById(fed_id)) {
|
||||
ModelUnits units;
|
||||
Eigen::Matrix4d coordinate_operation = Eigen::Matrix4d::Identity();
|
||||
if (const ModelGeoref* georef = loader->modelGeoref(mid)) {
|
||||
units = georef->units;
|
||||
if (georef->has_coordinate_operation) {
|
||||
coordinate_operation = georef->coordinate_operation_meters;
|
||||
}
|
||||
}
|
||||
matrix = composeModelTransformation(
|
||||
model->model_transformation, federation->config(), units, coordinate_operation);
|
||||
}
|
||||
}
|
||||
viewport_->setModelTransformation(mid, matrix);
|
||||
}
|
||||
|
||||
void ViewportController::applyModelVisibility(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
const QString fed_id = session_state_->fedIdForModelId(mid);
|
||||
if (fed_id.isEmpty()) return;
|
||||
|
||||
if (federation->isModelEffectivelyVisible(fed_id)) {
|
||||
viewport_->showModel(mid);
|
||||
} else {
|
||||
viewport_->hideModel(mid);
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportController::applyFederatedFalseOrigin() {
|
||||
Federation* federation = session_state_->federation();
|
||||
viewport_->setFederatedFalseOrigin(
|
||||
composeFederatedFalseOrigin(federation->federatedFalseOrigin(), federation->config()));
|
||||
}
|
||||
|
||||
void ViewportController::setHomeView() {
|
||||
auto camera = viewport_->cameraState();
|
||||
Federation::HomeView home_view;
|
||||
home_view.target = camera.target;
|
||||
home_view.distance = camera.distance;
|
||||
home_view.yaw = camera.yaw;
|
||||
home_view.pitch = camera.pitch;
|
||||
session_state_->federation()->setHomeView(home_view);
|
||||
session_state_->setStatusMessage("Camera", "Home view updated");
|
||||
}
|
||||
|
||||
void ViewportController::goHomeView() {
|
||||
Federation* federation = session_state_->federation();
|
||||
if (!federation->hasHomeView()) {
|
||||
session_state_->setStatusMessage("Camera", "No home view set for this project");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& home_view = federation->homeView();
|
||||
viewport_->setCamera(
|
||||
home_view.target.x(), home_view.target.y(), home_view.target.z(),
|
||||
home_view.distance, home_view.yaw, home_view.pitch);
|
||||
session_state_->setStatusMessage("Camera", "Home view restored");
|
||||
}
|
||||
|
||||
void ViewportController::setFlyMode() {
|
||||
viewport_->requestActivate();
|
||||
viewport_->enterFpsMode();
|
||||
session_state_->setStatusMessage("Mode", "Fly mode active");
|
||||
}
|
||||
|
||||
void ViewportController::toggleSectionMode() {
|
||||
viewport_->toggleSectionTool();
|
||||
if (viewport_->sectionToolActive()) {
|
||||
session_state_->setStatusMessage("Section", "Section tool active");
|
||||
} else {
|
||||
session_state_->setStatusMessage("Section", "Section tool off");
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportController::clearSectionPlanes() {
|
||||
viewport_->clearSectionPlanes();
|
||||
session_state_->setStatusMessage("Section", "Section planes cleared");
|
||||
}
|
||||
|
||||
void ViewportController::toggleDistanceMode() {
|
||||
viewport_->toggleLengthTool();
|
||||
}
|
||||
|
||||
void ViewportController::toggleAreaMode() {
|
||||
viewport_->toggleAreaTool();
|
||||
}
|
||||
|
||||
void ViewportController::toggleVolumeMode() {
|
||||
viewport_->toggleVolumeTool();
|
||||
}
|
||||
|
||||
void ViewportController::focusSelectedObject() {
|
||||
viewport_->focusOnSelectedObject();
|
||||
}
|
||||
|
||||
void ViewportController::hideSelectedElements() {
|
||||
viewport_->hideSelectedElements();
|
||||
}
|
||||
|
||||
void ViewportController::isolateSelectedElements() {
|
||||
viewport_->isolateSelectedElements();
|
||||
}
|
||||
|
||||
void ViewportController::showAllElements() {
|
||||
viewport_->showAllElements();
|
||||
}
|
||||
|
||||
void ViewportController::invertSelection() {
|
||||
viewport_->invertElementVisibility();
|
||||
}
|
||||
|
||||
void ViewportController::updateVolumeReadout() {
|
||||
if (viewport_->toolMode() != ViewportWindow::ToolMode::Volume) return;
|
||||
|
||||
const auto& sel = viewport_->selection().selectionIds();
|
||||
if (sel.empty()) {
|
||||
viewport_->setHudText(QString());
|
||||
viewport_->setOverlayLabels({});
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> ids(sel.begin(), sel.end());
|
||||
const auto per_obj = volumesPerObject(*viewport_, ids);
|
||||
|
||||
double total = 0.0;
|
||||
std::vector<OverlayRenderer::Label> labels;
|
||||
labels.reserve(per_obj.size());
|
||||
for (const auto& [oid, v] : per_obj) {
|
||||
total += v;
|
||||
QVector3D mn, mx;
|
||||
if (!viewport_->computeObjectAabb(oid, mn, mx)) continue;
|
||||
OverlayRenderer::Label lbl;
|
||||
const QVector3D c = (mn + mx) * 0.5f;
|
||||
lbl.world_pos[0] = c.x();
|
||||
lbl.world_pos[1] = c.y();
|
||||
lbl.world_pos[2] = c.z();
|
||||
lbl.text = QString::number(v, 'f', 4) + " m³";
|
||||
labels.push_back(std::move(lbl));
|
||||
}
|
||||
|
||||
viewport_->setHudText(QString("Volume: %1 m³ (%2 object%3)")
|
||||
.arg(total, 0, 'f', 4)
|
||||
.arg(per_obj.size())
|
||||
.arg(per_obj.size() == 1 ? "" : "s"));
|
||||
viewport_->setOverlayLabels(labels);
|
||||
}
|
||||
|
||||
void ViewportController::maybeGuessFederatedFalseOrigin(uint32_t mid) {
|
||||
Federation* federation = session_state_->federation();
|
||||
SceneLoader* loader = session_state_->loader();
|
||||
if (!federation->filePath().isEmpty()) return;
|
||||
|
||||
const FederatedFalseOrigin& current = federation->federatedFalseOrigin();
|
||||
const FederatedFalseOrigin defaults;
|
||||
if (current.xyz != defaults.xyz || current.rz_deg != defaults.rz_deg) return;
|
||||
|
||||
const Eigen::Matrix4d* placement = loader->firstPlacement(mid);
|
||||
const ModelGeoref* georef = loader->modelGeoref(mid);
|
||||
if (placement == nullptr || georef == nullptr) return;
|
||||
|
||||
federation->setFederatedFalseOrigin(guessFederatedFalseOrigin(
|
||||
*placement, *georef, federation->config()));
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::viewport
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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_VIEWPORT_CONTROLLER_H
|
||||
#define IFCINTERFACE_PANELS_VIEWPORT_CONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
namespace ifcinterface { class SessionState; }
|
||||
class ViewportWindow;
|
||||
class AreaMeasurement;
|
||||
class LengthMeasurement;
|
||||
|
||||
namespace ifcinterface::modules::viewport {
|
||||
|
||||
class ViewportController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ViewportController(ifcinterface::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent = nullptr);
|
||||
~ViewportController() override;
|
||||
|
||||
void applyFederatedFalseOrigin();
|
||||
void setHomeView();
|
||||
void goHomeView();
|
||||
void setFlyMode();
|
||||
void toggleSectionMode();
|
||||
void clearSectionPlanes();
|
||||
void toggleDistanceMode();
|
||||
void toggleAreaMode();
|
||||
void toggleVolumeMode();
|
||||
void focusSelectedObject();
|
||||
void hideSelectedElements();
|
||||
void isolateSelectedElements();
|
||||
void showAllElements();
|
||||
void invertSelection();
|
||||
|
||||
private:
|
||||
void applyCoordinateOperation(uint32_t mid);
|
||||
void applyModelTransformation(uint32_t mid);
|
||||
void applyModelVisibility(uint32_t mid);
|
||||
void maybeGuessFederatedFalseOrigin(uint32_t mid);
|
||||
void updateVolumeReadout();
|
||||
|
||||
ifcinterface::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
std::unique_ptr<AreaMeasurement> area_measurement_;
|
||||
std::unique_ptr<LengthMeasurement> length_measurement_;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::viewport
|
||||
|
||||
#endif
|
||||
@@ -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/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QFrame>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
namespace ifcinterface::modules::viewport {
|
||||
|
||||
ViewportPanel::ViewportPanel(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(0, 0, 0, 0);
|
||||
root->setSpacing(0);
|
||||
|
||||
auto* shell = new QFrame(this);
|
||||
shell->setObjectName("viewportShell");
|
||||
auto* shell_layout = new QVBoxLayout(shell);
|
||||
shell_layout->setContentsMargins(10, 10, 10, 10);
|
||||
shell_layout->setSpacing(0);
|
||||
|
||||
auto* frame = new QFrame(shell);
|
||||
frame->setObjectName("viewportFrame");
|
||||
auto* frame_layout = new QVBoxLayout(frame);
|
||||
frame_layout->setContentsMargins(0, 0, 0, 0);
|
||||
frame_layout->setSpacing(0);
|
||||
|
||||
viewport_ = new ViewportWindow();
|
||||
viewport_container_ = QWidget::createWindowContainer(viewport_, frame);
|
||||
viewport_container_->setMinimumSize(400, 300);
|
||||
viewport_container_->setFocusPolicy(Qt::StrongFocus);
|
||||
|
||||
frame_layout->addWidget(viewport_container_);
|
||||
shell_layout->addWidget(frame);
|
||||
root->addWidget(shell);
|
||||
}
|
||||
|
||||
} // namespace ifcinterface::modules::viewport
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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_VIEWPORT_PANEL_H
|
||||
#define IFCINTERFACE_MODULES_VIEWPORT_PANEL_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class ViewportWindow;
|
||||
|
||||
namespace ifcinterface::modules::viewport {
|
||||
|
||||
class ViewportPanel : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ViewportPanel(QWidget* parent = nullptr);
|
||||
|
||||
ViewportWindow* viewport() const { return viewport_; }
|
||||
|
||||
private:
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
QWidget* viewport_container_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcinterface::modules::viewport
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user