From d0c8c84e7bdfc4e4378f917fff7a725ac339af1c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 9 May 2026 22:00:19 +1000 Subject: [PATCH] Add model coordinates settings Add the federation/model settings dialog and related interface wiring for model coordinate configuration. Generated with the assistance of an AI coding tool. --- src/interface/CMakeLists.txt | 2 + src/interface/components/Panel.cpp | 16 +- src/interface/components/Panel.h | 3 + src/interface/components/Style.cpp | 70 ++- .../modules/models/AddModelDialog.cpp | 140 ++++++ src/interface/modules/models/AddModelDialog.h | 50 ++ src/interface/modules/models/Controller.cpp | 229 ++++++++++ src/interface/modules/models/Controller.h | 66 +++ src/interface/modules/models/Panel.cpp | 133 ++++++ src/interface/modules/models/Panel.h | 54 +++ .../modules/models/SettingsDialog.cpp | 426 ++++++++++++++++++ src/interface/modules/models/SettingsDialog.h | 84 ++++ src/interface/modules/models/Types.h | 44 ++ src/interface/modules/models/View.cpp | 96 ++++ src/interface/modules/models/View.h | 50 ++ 15 files changed, 1454 insertions(+), 9 deletions(-) create mode 100644 src/interface/modules/models/AddModelDialog.cpp create mode 100644 src/interface/modules/models/AddModelDialog.h create mode 100644 src/interface/modules/models/Controller.cpp create mode 100644 src/interface/modules/models/Controller.h create mode 100644 src/interface/modules/models/Panel.cpp create mode 100644 src/interface/modules/models/Panel.h create mode 100644 src/interface/modules/models/SettingsDialog.cpp create mode 100644 src/interface/modules/models/SettingsDialog.h create mode 100644 src/interface/modules/models/Types.h create mode 100644 src/interface/modules/models/View.cpp create mode 100644 src/interface/modules/models/View.h diff --git a/src/interface/CMakeLists.txt b/src/interface/CMakeLists.txt index 9de468bebb..469da99799 100644 --- a/src/interface/CMakeLists.txt +++ b/src/interface/CMakeLists.txt @@ -49,6 +49,8 @@ set(INTERFACE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/components/Panel.h ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/AddModelDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/AddModelDialog.h + ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/SettingsDialog.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/SettingsDialog.h ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Types.h ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Controller.cpp ${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Controller.h diff --git a/src/interface/components/Panel.cpp b/src/interface/components/Panel.cpp index 7ba96631d6..b7cdccab2e 100644 --- a/src/interface/components/Panel.cpp +++ b/src/interface/components/Panel.cpp @@ -38,7 +38,10 @@ namespace { class DockTitleBar : public QWidget { public: - explicit DockTitleBar(const QString& title, bool has_settings = false, QWidget* parent = nullptr) + explicit DockTitleBar(const QString& title, + bool has_settings = false, + std::function on_settings = {}, + QWidget* parent = nullptr) : QWidget(parent) { auto* layout = new QHBoxLayout(this); @@ -58,11 +61,8 @@ public: settings->setFixedSize(18, 18); settings->setObjectName("panelTitleButton"); settings->setToolTip(QString("%1 settings").arg(title)); - connect(settings, &QToolButton::clicked, this, [this, title]() { - auto* anchor = parentWidget(); - QMenu menu(anchor); - menu.addAction(QString("%1 settings coming soon").arg(title)); - menu.exec(QCursor::pos()); + connect(settings, &QToolButton::clicked, this, [on_settings = std::move(on_settings)]() { + if (on_settings) on_settings(); }); layout->addWidget(settings); } @@ -118,7 +118,9 @@ Panel::Panel(const QString& title, QWidget* content, QWidget* parent, bool has_s setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); - setTitleBarWidget(new DockTitleBar(title, has_settings, this)); + setTitleBarWidget(new DockTitleBar(title, has_settings, [this]() { + emit settingsRequested(); + }, this)); setWidget(outer); } diff --git a/src/interface/components/Panel.h b/src/interface/components/Panel.h index fe48df51a5..c5bb32c85b 100644 --- a/src/interface/components/Panel.h +++ b/src/interface/components/Panel.h @@ -41,6 +41,9 @@ public: void addBodyWidget(QWidget* widget); void clearBodyWidgets(); +signals: + void settingsRequested(); + private: QVBoxLayout* body_layout_ = nullptr; }; diff --git a/src/interface/components/Style.cpp b/src/interface/components/Style.cpp index a115247716..2dd6664f9c 100644 --- a/src/interface/components/Style.cpp +++ b/src/interface/components/Style.cpp @@ -128,6 +128,7 @@ QString buildAppStyleSheet() { QSpinBox, QDoubleSpinBox, QCheckBox, + QRadioButton, QPushButton, QToolButton { color: ${primary_text}; @@ -203,11 +204,26 @@ QString buildAppStyleSheet() { background: ${control_background}; border: 1px solid ${border}; border-radius: ${panel_radius}px; - padding: ${padding}px ${padding}px; + padding: ${padding}px 24px ${padding}px ${padding}px; } QComboBox:focus { border: 1px solid ${control_border_focus}; } + QComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: top right; + width: 18px; + border: none; + background: transparent; + } + QComboBox::down-arrow { + width: 0px; + height: 0px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid ${secondary_text}; + margin-right: 4px; + } QComboBox QAbstractItemView { background: ${panel_background}; border: 1px solid ${border}; @@ -218,11 +234,38 @@ QString buildAppStyleSheet() { background: ${control_background}; border: 1px solid ${border}; border-radius: ${panel_radius}px; - padding: ${padding}px ${padding}px; + padding: ${padding}px 24px ${padding}px ${padding}px; } QSpinBox:focus, QDoubleSpinBox:focus { border: 1px solid ${control_border_focus}; } + QSpinBox::up-button, QDoubleSpinBox::up-button, + QSpinBox::down-button, QDoubleSpinBox::down-button { + subcontrol-origin: padding; + width: 18px; + border: none; + background: transparent; + } + QSpinBox::up-button, QDoubleSpinBox::up-button { + subcontrol-position: top right; + } + QSpinBox::down-button, QDoubleSpinBox::down-button { + subcontrol-position: bottom right; + } + QSpinBox::up-arrow, QDoubleSpinBox::up-arrow { + width: 0px; + height: 0px; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 5px solid ${secondary_text}; + } + QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { + width: 0px; + height: 0px; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid ${secondary_text}; + } QCheckBox { background: transparent; spacing: 8px; @@ -241,6 +284,29 @@ QString buildAppStyleSheet() { border: 1px solid ${selection_background}; background: ${selection_background}; } + QRadioButton { + background: transparent; + color: ${primary_text}; + spacing: 8px; + padding: 2px 0; + } + QRadioButton::indicator { + width: 16px; + height: 16px; + border: 1px solid ${border}; + border-radius: 8px; + background: ${control_background}; + } + QRadioButton::indicator:hover { + background: ${ribbon_button_hover}; + } + QRadioButton::indicator:checked { + border: 1px solid ${selection_background}; + background: ${selection_background}; + } + QRadioButton::indicator:unchecked { + background: ${control_background}; + } QPushButton { background: ${control_background}; border: 1px solid ${border}; diff --git a/src/interface/modules/models/AddModelDialog.cpp b/src/interface/modules/models/AddModelDialog.cpp new file mode 100644 index 0000000000..5976c12bfd --- /dev/null +++ b/src/interface/modules/models/AddModelDialog.cpp @@ -0,0 +1,140 @@ +// 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 . * + * * + ********************************************************************************/ + +#include "AddModelDialog.h" + +#include "../../components/Dialog.h" +#include "../../components/Buttons.h" +#include "../../components/Section.h" +#include "../../components/Style.h" + +#include +#include +#include +#include +#include + +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); + setStyleSheet(components::style::buildAppStyleSheet()); + setupUi(); +} + +void AddModelDialog::setupUi() { + if (auto* root = qobject_cast(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 diff --git a/src/interface/modules/models/AddModelDialog.h b/src/interface/modules/models/AddModelDialog.h new file mode 100644 index 0000000000..8e97409fd0 --- /dev/null +++ b/src/interface/modules/models/AddModelDialog.h @@ -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 . * + * * + ********************************************************************************/ + +#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 diff --git a/src/interface/modules/models/Controller.cpp b/src/interface/modules/models/Controller.cpp new file mode 100644 index 0000000000..7a13ba8609 --- /dev/null +++ b/src/interface/modules/models/Controller.cpp @@ -0,0 +1,229 @@ +// 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 . * + * * + ********************************************************************************/ + +#include "Controller.h" + +#include "SettingsDialog.h" +#include "Panel.h" + +#include "../../ElementRegistry.h" +#include "../../SessionState.h" +#include "AddModelDialog.h" +#include "../../../ifcviewer/Federation.h" +#include "../../../ifcviewer/SceneLoader.h" +#include "../../../ifcviewer/ViewportWindow.h" + +#include +#include +#include +#include + +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::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) { + 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, "IfcInterfaceMockup", 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("listView")) { + list->setSelectionMode(QAbstractItemView::ExtendedSelection); + } + if (auto* tree = database_dialog.findChild()) { + 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(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_->federation(), host_); + dialog.exec(); +} + +QString ModelsPanelController::formatElapsed(qint64 ms) const { + return (ms >= 1000) + ? QString::number(ms / 1000.0, 'f', 2) + " s" + : QString::number(ms) + " ms"; +} + +} // namespace ifcinterface::modules::models diff --git a/src/interface/modules/models/Controller.h b/src/interface/modules/models/Controller.h new file mode 100644 index 0000000000..d1848d4e81 --- /dev/null +++ b/src/interface/modules/models/Controller.h @@ -0,0 +1,66 @@ +// 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 . * + * * + ********************************************************************************/ + +#ifndef IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H +#define IFCINTERFACE_PANELS_MODELSPANELCONTROLLER_H + +#include "Types.h" + +#include +#include + +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; + + QWidget* host_ = nullptr; + ModelsPanel* widget_ = nullptr; + ifcinterface::SessionState* session_state_ = nullptr; + ViewportWindow* viewport_ = nullptr; +}; + +} // namespace ifcinterface::modules::models + +#endif diff --git a/src/interface/modules/models/Panel.cpp b/src/interface/modules/models/Panel.cpp new file mode 100644 index 0000000000..c55a2d2dbf --- /dev/null +++ b/src/interface/modules/models/Panel.cpp @@ -0,0 +1,133 @@ +// 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 . * + * * + ********************************************************************************/ + +#include "Panel.h" + +#include "../../components/Section.h" +#include "../../components/SvgIcon.h" + +#include +#include +#include +#include +#include +#include + +namespace ifcinterface::modules::models { + +ModelsPanel::ModelsPanel(QWidget* parent) + : components::Panel("Models", nullptr, parent, true) +{ + auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this); + + tree_ = new QTreeWidget(section); + tree_->setColumnCount(2); + tree_->setHeaderLabels({"Model", ""}); + tree_->setIconSize(QSize(16, 16)); + tree_->setSelectionMode(QAbstractItemView::ExtendedSelection); + tree_->setContextMenuPolicy(Qt::CustomContextMenu); + 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( + static_cast(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) { + menu.addAction(components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "Add Group", [this]() { + bool ok = false; + const QString name = QInputDialog::getText( + this, "New Group", "Group name:", QLineEdit::Normal, "Group", &ok); + if (ok && !name.isEmpty()) { + emit addGroupRequested(QString(), name); + } + }); + menu.exec(tree_->viewport()->mapToGlobal(pos)); + return; + } + + const auto kind = static_cast(item->data(0, Qt::UserRole).toInt()); + const QString id = item->data(0, Qt::UserRole + 1).toString(); + + menu.addAction(components::icons::makeSvgIcon(":/icons/eye.svg"), "Toggle Visibility", [this, kind, id]() { + emit visibilityToggleRequested(kind, id); + }); + + if (kind == ItemKind::Group) { + menu.addAction(components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "Add Group", [this, id]() { + bool ok = false; + const QString name = QInputDialog::getText( + this, "New Group", "Group name:", QLineEdit::Normal, "Group", &ok); + if (ok && !name.isEmpty()) { + emit addGroupRequested(id, name); + } + }); + menu.addAction(components::icons::makeSvgIcon(":/icons/folder-minus.svg"), "Remove Group", [this, id]() { + emit removeGroupRequested(id); + }); + } else { + menu.addAction(components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model", [this, id]() { + emit removeModelRequested(id); + }); + } + menu.exec(tree_->viewport()->mapToGlobal(pos)); + }); +} + +void ModelsPanel::setNodes(const QList& nodes) { + tree_->clear(); + 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(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")); + } + + for (const auto& child : node.children) { + addNode(item, child); + } +} + +} // namespace ifcinterface::modules::models diff --git a/src/interface/modules/models/Panel.h b/src/interface/modules/models/Panel.h new file mode 100644 index 0000000000..36404234ac --- /dev/null +++ b/src/interface/modules/models/Panel.h @@ -0,0 +1,54 @@ +// 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 . * + * * + ********************************************************************************/ + +#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& nodes); + +signals: + void visibilityToggleRequested(ItemKind kind, const QString& id); + void addGroupRequested(const QString& parent_group_id, const QString& name); + 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 diff --git a/src/interface/modules/models/SettingsDialog.cpp b/src/interface/modules/models/SettingsDialog.cpp new file mode 100644 index 0000000000..5938c752f0 --- /dev/null +++ b/src/interface/modules/models/SettingsDialog.cpp @@ -0,0 +1,426 @@ +// 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 . * + * * + ********************************************************************************/ + +#include "SettingsDialog.h" + +#include "../../../ifcviewer/Federation.h" +#include "../../components/Section.h" +#include "../../components/Style.h" +#include "../../components/SvgIcon.h" +#include "../../components/Tabs.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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"}, +}; + +QString unitDisplay(const FederationConfig& cfg) { + QString s; + if (!cfg.unit_prefix.empty()) s += QString::fromStdString(cfg.unit_prefix) + " "; + s += QString::fromStdString(cfg.unit_name); + return s; +} + +QLineEdit* makeNumericField(QWidget* parent, const QString& placeholder = {}) { + auto* field = new QLineEdit(parent); + field->setPlaceholderText(placeholder); + field->setMaximumWidth(96); + return field; +} + +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; +} + +QWidget* makeVector3Row(QLineEdit* x, + QLineEdit* y, + QLineEdit* z, + QWidget* parent) { + auto* row = new QWidget(parent); + auto* layout = new QHBoxLayout(row); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(components::style::metrics::padding); + layout->addWidget(x); + layout->addWidget(y); + layout->addWidget(z); + layout->addStretch(1); + return row; +} + +QWidget* makeRotationRow(QLineEdit* rx, + QLineEdit* ry, + QLineEdit* rz, + QWidget* parent) { + auto* row = new QWidget(parent); + auto* layout = new QHBoxLayout(row); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(components::style::metrics::padding); + layout->addWidget(rx); + layout->addWidget(ry); + layout->addWidget(rz); + layout->addStretch(1); + return row; +} + +} // namespace + +SettingsDialog::SettingsDialog(Federation* federation, QWidget* parent) + : components::Dialog(parent) + , federation_(federation) +{ + setObjectName("appDialog"); + setWindowTitle("Model Settings"); + setModal(true); + resize(560, 560); + setStyleSheet(components::style::buildAppStyleSheet()); + setupUi(); +} + +void SettingsDialog::showEvent(QShowEvent* event) { + syncFromFederation(); + populateModelCombo(); + refreshUnitLabels(); + if (model_combo_->count() > 0) { + syncFromModel(model_combo_->currentData().toString()); + } + QDialog::showEvent(event); +} + +void SettingsDialog::setupUi() { + auto* tabs = new components::TabWidget(this); + + auto* federation_tab = new QWidget(tabs); + auto* federation_layout = new QVBoxLayout(federation_tab); + federation_layout->setContentsMargins(0, 0, 0, 0); + federation_layout->setSpacing(components::style::metrics::padding); + + auto* federation_unit_section = new components::Section("Federation Unit", components::SectionHeaderMode::Visible, federation_tab); + 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_); + auto* unit_hint = new QLabel( + "All federated false origin and model transformation values are interpreted in this unit.", + federation_unit_body); + unit_hint->setProperty("textRole", "secondary"); + unit_hint->setWordWrap(true); + federation_unit_form->addRow(QString(), 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_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", makeVector3Row(xyz_x_, xyz_y_, xyz_z_, origin_body)); + origin_form->addRow("Z rotation (°)", rz_deg_); + auto* origin_hint = new QLabel( + "Nominate a federation point that becomes the new origin, with optional north rotation.", + origin_body); + origin_hint->setProperty("textRole", "secondary"); + origin_hint->setWordWrap(true); + origin_form->addRow(QString(), 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(tabs); + auto* model_layout = new QVBoxLayout(model_tab); + model_layout->setContentsMargins(0, 0, 0, 0); + model_layout->setSpacing(components::style::metrics::padding); + + auto* picker_section = new components::Section("", components::SectionHeaderMode::Hidden, model_tab); + auto* picker_body = new QWidget(picker_section); + auto* picker_form = new QFormLayout(picker_body); + picker_form->setContentsMargins(0, 0, 0, 0); + picker_form->setHorizontalSpacing(16); + picker_form->setVerticalSpacing(10); + model_combo_ = new QComboBox(picker_body); + picker_form->addRow("Current Model", model_combo_); + picker_section->addBodyWidget(picker_body); + + auto* a_section = new components::Section("Point A", components::SectionHeaderMode::Visible, model_tab); + auto* a_body = new QWidget(a_section); + auto* a_form = new QFormLayout(a_body); + a_form->setContentsMargins(0, 0, 0, 0); + a_form->setHorizontalSpacing(16); + a_form->setVerticalSpacing(10); + radio_local_ = new QRadioButton("ModelLocal", a_body); + radio_global_ = new QRadioButton("ModelGlobal", a_body); + radio_global_->setChecked(true); + auto* a_frame_group = new QButtonGroup(this); + a_frame_group->addButton(radio_local_); + a_frame_group->addButton(radio_global_); + auto* a_frame_row = new QHBoxLayout(); + a_frame_row->setContentsMargins(0, 0, 0, 0); + a_frame_row->setSpacing(components::style::metrics::padding); + a_frame_row->addWidget(radio_local_); + a_frame_row->addWidget(radio_global_); + a_frame_row->addStretch(1); + a_form->addRow("Frame", a_frame_row); + a_x_ = makeNumericField(a_body, "X"); + a_y_ = makeNumericField(a_body, "Y"); + a_z_ = makeNumericField(a_body, "Z"); + a_form->addRow("XYZ", makeVector3Row(a_x_, a_y_, a_z_, a_body)); + a_unit_label_ = new QLabel(a_body); + a_unit_label_->setProperty("textRole", "secondary"); + a_form->addRow("Unit", a_unit_label_); + a_section->addBodyWidget(a_body); + + auto* b_section = new components::Section("Point B", components::SectionHeaderMode::Visible, model_tab); + auto* b_body = new QWidget(b_section); + auto* b_form = new QFormLayout(b_body); + b_form->setContentsMargins(0, 0, 0, 0); + b_form->setHorizontalSpacing(16); + b_form->setVerticalSpacing(10); + b_x_ = makeNumericField(b_body, "X"); + b_y_ = makeNumericField(b_body, "Y"); + b_z_ = makeNumericField(b_body, "Z"); + b_form->addRow("XYZ", makeVector3Row(b_x_, b_y_, b_z_, b_body)); + b_unit_label_ = new QLabel(b_body); + b_unit_label_->setProperty("textRole", "secondary"); + b_form->addRow("Unit", b_unit_label_); + b_section->addBodyWidget(b_body); + + auto* rotation_section = new components::Section("Rotation", components::SectionHeaderMode::Visible, model_tab); + auto* rotation_body = new QWidget(rotation_section); + auto* rotation_form = new QFormLayout(rotation_body); + rotation_form->setContentsMargins(0, 0, 0, 0); + rotation_form->setHorizontalSpacing(16); + rotation_form->setVerticalSpacing(10); + rx_ = makeNumericField(rotation_body, "rx"); + ry_ = makeNumericField(rotation_body, "ry"); + rz_ = makeNumericField(rotation_body, "rz"); + rotation_form->addRow("RXYZ", makeRotationRow(rx_, ry_, rz_, rotation_body)); + rotation_section->addBodyWidget(rotation_body); + + auto* pivot_section = new components::Section("Rotation Pivot", components::SectionHeaderMode::Visible, model_tab); + auto* pivot_body = new QWidget(pivot_section); + auto* pivot_form = new QFormLayout(pivot_body); + pivot_form->setContentsMargins(0, 0, 0, 0); + pivot_form->setHorizontalSpacing(16); + pivot_form->setVerticalSpacing(10); + pivot_x_ = makeNumericField(pivot_body, "X"); + pivot_y_ = makeNumericField(pivot_body, "Y"); + pivot_z_ = makeNumericField(pivot_body, "Z"); + pivot_form->addRow("XYZ", makeVector3Row(pivot_x_, pivot_y_, pivot_z_, pivot_body)); + pivot_unit_label_ = new QLabel(pivot_body); + pivot_unit_label_->setProperty("textRole", "secondary"); + pivot_form->addRow("Unit", pivot_unit_label_); + pivot_section->addBodyWidget(pivot_body); + + model_layout->addWidget(picker_section); + model_layout->addWidget(a_section); + model_layout->addWidget(b_section); + model_layout->addWidget(rotation_section); + model_layout->addWidget(pivot_section); + model_layout->addStretch(1); + + tabs->addTab(federation_tab, "Federation"); + tabs->addTab(model_tab, "Model"); + + connect(model_combo_, QOverload::of(&QComboBox::currentIndexChanged), this, + [this](int) { + if (model_combo_->count() > 0) { + syncFromModel(model_combo_->currentData().toString()); + } + }); + connect(radio_local_, &QRadioButton::toggled, this, [this]() { onAFrameToggled(); }); + connect(radio_global_, &QRadioButton::toggled, this, [this]() { onAFrameToggled(); }); + + 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); + + auto* actions_section = new components::Section("", components::SectionHeaderMode::Hidden, this); + actions_section->addBodyWidget(buttons); + + addBodyWidget(tabs); + addBodyWidget(actions_section); +} + +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::populateModelCombo() { + model_combo_->blockSignals(true); + QString previous = model_combo_->currentData().toString(); + model_combo_->clear(); + if (federation_) { + for (const auto& model : federation_->models()) { + model_combo_->addItem(model.display_name.isEmpty() ? model.id : model.display_name, model.id); + } + } + int restore = -1; + for (int i = 0; i < model_combo_->count(); ++i) { + if (model_combo_->itemData(i).toString() == previous) { + restore = i; + break; + } + } + if (restore >= 0) model_combo_->setCurrentIndex(restore); + model_combo_->blockSignals(false); +} + +void SettingsDialog::syncFromModel(const QString& fed_id) { + if (!federation_) return; + const Federation::Model* model = federation_->findById(fed_id); + if (!model) return; + + const auto& xf = model->model_transformation; + radio_local_->setChecked(xf.a_frame == AFrame::ModelLocal); + radio_global_->setChecked(xf.a_frame == AFrame::ModelGlobal); + a_x_->setText(formatNumber(xf.a.x())); + a_y_->setText(formatNumber(xf.a.y())); + a_z_->setText(formatNumber(xf.a.z())); + b_x_->setText(formatNumber(xf.b.x())); + b_y_->setText(formatNumber(xf.b.y())); + b_z_->setText(formatNumber(xf.b.z())); + rx_->setText(formatNumber(xf.rxyz_deg.x())); + ry_->setText(formatNumber(xf.rxyz_deg.y())); + rz_->setText(formatNumber(xf.rxyz_deg.z())); + pivot_x_->setText(formatNumber(xf.pivot.x())); + pivot_y_->setText(formatNumber(xf.pivot.y())); + pivot_z_->setText(formatNumber(xf.pivot.z())); +} + +void SettingsDialog::refreshUnitLabels() { + if (!federation_) return; + const QString fed_unit = unitDisplay(federation_->config()); + b_unit_label_->setText("(federation: " + fed_unit + ")"); + pivot_unit_label_->setText("(federation: " + fed_unit + ")"); + onAFrameToggled(); +} + +void SettingsDialog::onAFrameToggled() { + if (!a_unit_label_) return; + if (radio_local_->isChecked()) { + a_unit_label_->setText("(model project length unit)"); + } else { + a_unit_label_->setText("(model map unit)"); + } +} + +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); + + if (model_combo_->count() > 0) { + ModelTransformation xf; + xf.a_frame = radio_local_->isChecked() ? AFrame::ModelLocal : AFrame::ModelGlobal; + xf.a = Eigen::Vector3d(parseNumber(a_x_), parseNumber(a_y_), parseNumber(a_z_)); + xf.b = Eigen::Vector3d(parseNumber(b_x_), parseNumber(b_y_), parseNumber(b_z_)); + xf.rxyz_deg = Eigen::Vector3d(parseNumber(rx_), parseNumber(ry_), parseNumber(rz_)); + xf.pivot = Eigen::Vector3d(parseNumber(pivot_x_), parseNumber(pivot_y_), parseNumber(pivot_z_)); + federation_->setModelTransformation(model_combo_->currentData().toString(), xf); + } + } + accept(); +} + +} // namespace ifcinterface::modules::models diff --git a/src/interface/modules/models/SettingsDialog.h b/src/interface/modules/models/SettingsDialog.h new file mode 100644 index 0000000000..86477dd5f3 --- /dev/null +++ b/src/interface/modules/models/SettingsDialog.h @@ -0,0 +1,84 @@ +// 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 . * + * * + ********************************************************************************/ + +#ifndef IFCINTERFACE_MODULES_MODELS_SETTINGSDIALOG_H +#define IFCINTERFACE_MODULES_MODELS_SETTINGSDIALOG_H + +#include "../../components/Dialog.h" + +#include + +class Federation; +class QComboBox; +class QLabel; +class QLineEdit; +class QRadioButton; +class QShowEvent; + +namespace ifcinterface::modules::models { + +class SettingsDialog : public components::Dialog { + Q_OBJECT +public: + explicit SettingsDialog(Federation* federation, QWidget* parent = nullptr); + +protected: + void showEvent(QShowEvent* event) override; + +private: + void setupUi(); + void syncFromFederation(); + void populateModelCombo(); + void syncFromModel(const QString& fed_id); + void refreshUnitLabels(); + void onAFrameToggled(); + void onAccepted(); + + Federation* federation_ = nullptr; + + QComboBox* unit_combo_ = nullptr; + QLineEdit* xyz_x_ = nullptr; + QLineEdit* xyz_y_ = nullptr; + QLineEdit* xyz_z_ = nullptr; + QLineEdit* rz_deg_ = nullptr; + + QComboBox* model_combo_ = nullptr; + QRadioButton* radio_local_ = nullptr; + QRadioButton* radio_global_ = nullptr; + QLineEdit* a_x_ = nullptr; + QLineEdit* a_y_ = nullptr; + QLineEdit* a_z_ = nullptr; + QLabel* a_unit_label_ = nullptr; + QLineEdit* b_x_ = nullptr; + QLineEdit* b_y_ = nullptr; + QLineEdit* b_z_ = nullptr; + QLabel* b_unit_label_ = nullptr; + QLineEdit* rx_ = nullptr; + QLineEdit* ry_ = nullptr; + QLineEdit* rz_ = nullptr; + QLineEdit* pivot_x_ = nullptr; + QLineEdit* pivot_y_ = nullptr; + QLineEdit* pivot_z_ = nullptr; + QLabel* pivot_unit_label_ = nullptr; +}; + +} // namespace ifcinterface::modules::models + +#endif diff --git a/src/interface/modules/models/Types.h b/src/interface/modules/models/Types.h new file mode 100644 index 0000000000..927995d762 --- /dev/null +++ b/src/interface/modules/models/Types.h @@ -0,0 +1,44 @@ +// 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 . * + * * + ********************************************************************************/ + +#ifndef IFCINTERFACE_PANELS_MODELSPANELTYPES_H +#define IFCINTERFACE_PANELS_MODELSPANELTYPES_H + +#include +#include + +namespace ifcinterface::modules::models { + +enum class ItemKind { + Group, + Model, +}; + +struct TreeNode { + QString id; + QString name; + ItemKind kind = ItemKind::Group; + bool visible = true; + QList children; +}; + +} // namespace ifcinterface::modules::models + +#endif diff --git a/src/interface/modules/models/View.cpp b/src/interface/modules/models/View.cpp new file mode 100644 index 0000000000..e5154eca2b --- /dev/null +++ b/src/interface/modules/models/View.cpp @@ -0,0 +1,96 @@ +// 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 . * + * * + ********************************************************************************/ + +#include "View.h" + +#include "Panel.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(); }); + + reload(); +} + +void ModelsPanelView::reload() { + Federation* federation = session_state_->federation(); + QList 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 diff --git a/src/interface/modules/models/View.h b/src/interface/modules/models/View.h new file mode 100644 index 0000000000..05df55961a --- /dev/null +++ b/src/interface/modules/models/View.h @@ -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 . * + * * + ********************************************************************************/ + +#ifndef IFCINTERFACE_PANELS_MODELSPANELVIEW_H +#define IFCINTERFACE_PANELS_MODELSPANELVIEW_H + +#include "Types.h" + +#include + +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